Compare commits

..

8 Commits

Author SHA1 Message Date
Mark 7272f2a00a
Added stats & purchase buttons 2024-02-16 18:27:46 -08:00
Mark de07e763c3
Minor bugfix 2024-02-16 18:26:55 -08:00
Mark d3195983ec
Updated TODO 2024-02-16 18:26:44 -08:00
Mark 82f2f5fc85
API tweaks 2024-02-16 18:26:37 -08:00
Mark 9f19becf0c
Added outfit purchase directives 2024-02-16 18:26:21 -08:00
Mark a6bc1a021d
Minor edits 2024-02-16 18:25:59 -08:00
Mark f5c9540fb7
Added outfitspace to_hashmap 2024-02-16 18:24:28 -08:00
Mark ab17930449
Added outfit cost 2024-02-16 18:24:10 -08:00
19 changed files with 341 additions and 65 deletions

View File

@ -17,6 +17,7 @@
- fps textbox positioning
- shield generation curve
- clippy & rules
- reject unknown toml
## Small jobs
- Better planet icons in radar

View File

@ -1,6 +1,7 @@
[outfit."plasma engines"]
name = "Plasma Engines"
thumbnail = "icon::engine"
cost = 30
desc = """
This is the smallest of the plasma propulsion systems produced by
@ -21,6 +22,7 @@ steering.power = 20
[outfit."shield generator"]
thumbnail = "icon::shield"
name = "Shield Generator"
cost = 10
desc = """
This is the standard shield generator for fighters and interceptors,
@ -40,6 +42,7 @@ shield.delay = 2.0
[outfit."blaster"]
thumbnail = "icon::blaster"
name = "Blaster"
cost = 20
desc = """
Although not the most accurate or damaging of weapons, the Energy Blaster is popular because it

View File

@ -113,10 +113,15 @@ fn event(state, event) {
}
fn step(state) {
if state.player_ship().stat_shield_strength() == 0.0 {
radialbar::set_val("shield", 0.0);
} else {
radialbar::set_val("shield",
state.player_ship().current_shields()
/ state.player_ship().stat_shield_strength()
);
}
radialbar::set_val("hull",
state.player_ship().current_hull()
/ state.player_ship().total_hull()

View File

@ -347,6 +347,9 @@ fn init(state) {
fn event(state, event) {
// TODO: update on ship outfit change only
update_ship_info(state);
if type_of(event) == "MouseHoverEvent" {
let element = event.element();
@ -394,6 +397,24 @@ fn event(state, event) {
return PlayerDirective::None;
}
if element == "buy_button" {
if selected_outfit != false {
let outfit = ct::get_outfit(selected_outfit);
if outfit.is_some() {
return PlayerDirective::BuyOutfit(outfit, 1);
}
}
}
if element == "sell_button" {
if selected_outfit != false {
let outfit = ct::get_outfit(selected_outfit);
if outfit.is_some() {
return PlayerDirective::SellOutfit(outfit, 1);
}
}
}
if (
element.starts_with("outfit.backg.") &&
event.element().split("outfit.backg.".len())[1] != selected_outfit
@ -475,6 +496,41 @@ fn update_outfit_info(selected_outfit) {
let stats = "";
let tlen = 20;
{
let s = "cost ";
s.pad(tlen, " ");
stats += s + outfit.cost();
stats += "\n";
}
{
let s = "needs gun port ";
s.pad(tlen, " ");
if outfit.stat_is_gun() {
stats += s + "yes";
} else {
stats += s + "no"
}
stats += "\n";
}
{
let s = outfit.required_space();
for k in s.keys() {
let v = s[k];
let s = k + " space needed ";
s.pad(tlen, " ");
if v != 0 {
stats += s + v;
stats += "\n";
}
}
}
stats += "\n";
if outfit.stat_thrust() != 0 {
let s = "thrust ";
s.pad(tlen, " ");
@ -527,8 +583,28 @@ fn update_ship_info(state) {
let tlen = 20;
{
let u = ship.used_space();
let t = ship.total_space();
for k in u.keys() {
let vu = u[k];
let vt = t[k];
let s = k + " space ";
s.pad(tlen, " ");
stats += s + (vt - vu) + "/" + vt;
stats += "\n";
}
}
{
let s = "gun points ";
s.pad(tlen, " ");
stats += s + ship.free_gun_points() + "/" + ship.total_gun_points();
stats += "\n\n";
}
// TODO: outfits add mass
// TODO: calculate radial acceleration
// TODO: calculate acceleration
{
let s = "shield strength ";
s.pad(tlen, " ");

View File

@ -24,6 +24,7 @@ pub(crate) mod syntax {
pub thumbnail: ContentIndex,
pub name: String,
pub desc: String,
pub cost: u32,
pub engine: Option<Engine>,
pub steering: Option<Steering>,
pub space: outfitspace::syntax::OutfitSpace,
@ -140,6 +141,9 @@ pub struct Outfit {
/// This outfit's thumbnail
pub thumbnail: Arc<Sprite>,
/// The cost of this outfit, in credits
pub cost: u32,
/// How much space this outfit requires
pub space: OutfitSpace,
@ -324,6 +328,7 @@ impl crate::Build for Outfit {
let mut o = Self {
index: ContentIndex::new(&outfit_name),
display_name: outfit.name,
cost: outfit.cost,
thumbnail,
gun,
desc: outfit.desc,

View File

@ -1,4 +1,7 @@
use std::ops::{Add, AddAssign, Sub, SubAssign};
use std::{
collections::HashMap,
ops::{Add, AddAssign, Sub, SubAssign},
};
pub(crate) mod syntax {
use serde::Deserialize;
@ -44,6 +47,15 @@ impl OutfitSpace {
&& self.weapon >= smaller.weapon
&& self.engine >= smaller.engine
}
/// Return a map of "space name" -> number
pub fn to_hashmap(&self) -> HashMap<String, u32> {
let mut m = HashMap::new();
m.insert("outfit".to_string(), self.outfit);
m.insert("weapon".to_string(), self.weapon);
m.insert("engine".to_string(), self.engine);
m
}
}
impl Sub for OutfitSpace {

View File

@ -5,6 +5,7 @@ use galactica_system::phys::{PhysImage, PhysSim, PhysSimShipHandle, PhysStepReso
use galactica_system::PlayerDirective;
use galactica_util::timing::Timing;
use nalgebra::Point2;
use std::iter;
use std::sync::Arc;
use std::time::Instant;
@ -51,16 +52,6 @@ impl<'a> Game {
.get(&ContentIndex::new("shield generator"))
.unwrap()
.clone(),
self.ct
.outfits
.get(&ContentIndex::new("blaster"))
.unwrap()
.clone(),
self.ct
.outfits
.get(&ContentIndex::new("blaster"))
.unwrap()
.clone(),
]);
return player;
@ -131,9 +122,43 @@ impl<'a> Game {
}
}
pub fn apply_directive(&mut self, directive: PlayerDirective, player: &PlayerAgent) {
pub fn apply_directive(&mut self, directive: PlayerDirective, player: &mut PlayerAgent) {
match directive {
_ => self.phys_sim.apply_directive(directive, player),
PlayerDirective::None => {}
PlayerDirective::BuyOutfit { outfit, count } => {
let cost = outfit.cost * count;
if player.credits >= cost {
player.credits -= cost;
let ship = self
.phys_sim
.get_ship_mut(&PhysSimShipHandle(player.ship.unwrap()))
.unwrap();
ship.add_outfits(iter::repeat(outfit).take(count as usize))
}
}
PlayerDirective::SellOutfit { outfit, count } => {
let ship = self
.phys_sim
.get_ship_mut(&PhysSimShipHandle(player.ship.unwrap()))
.unwrap();
if ship.get_data().get_outfits().has_outfit(&outfit, count) {
for _ in 0..count {
ship.remove_outfit(&outfit);
}
}
player.credits += outfit.cost * count;
}
PlayerDirective::Engine(_)
| PlayerDirective::Guns(_)
| PlayerDirective::Land
| PlayerDirective::TurnLeft(_)
| PlayerDirective::TurnRight(_)
| PlayerDirective::UnLand => self.phys_sim.apply_directive(directive, player),
}
}

View File

@ -15,6 +15,7 @@ use log4rs::{
Config,
};
use std::{
cell::RefCell,
fs,
path::{Path, PathBuf},
sync::Arc,
@ -127,7 +128,7 @@ fn try_main() -> Result<()> {
current_time: game.get_current_time(),
ct: content.clone(),
phys_img: PhysImage::new(),
player: PlayerAgent::new(&content, p.0),
player: RefCell::new(PlayerAgent::new(&content, p.0)),
// TODO: this is a hack for testing.
current_system: content.systems.values().next().unwrap().clone(),
timing: game.get_timing().clone(),
@ -184,13 +185,13 @@ fn try_main() -> Result<()> {
},
)
.unwrap();
game.apply_directive(directive, &input.player);
game.apply_directive(directive, &mut input.player.borrow_mut());
}
WindowEvent::CursorMoved { position, .. } => {
let directive = gpu
.process_input(&input, InputEvent::MouseMove(position.cast()))
.unwrap();
game.apply_directive(directive, &input.player);
game.apply_directive(directive, &mut input.player.borrow_mut());
}
WindowEvent::MouseInput { state, button, .. } => {
let down = state == &ElementState::Pressed;
@ -201,7 +202,7 @@ fn try_main() -> Result<()> {
};
if let Some(event) = event {
let directive = gpu.process_input(&input, event).unwrap();
game.apply_directive(directive, &input.player);
game.apply_directive(directive, &mut input.player.borrow_mut());
}
}
WindowEvent::MouseWheel { delta, .. } => {
@ -214,7 +215,7 @@ fn try_main() -> Result<()> {
}),
)
.unwrap();
game.apply_directive(directive, &input.player);
game.apply_directive(directive, &mut input.player.borrow_mut());
}
WindowEvent::Resized(_) => {
gpu.resize(&content);

View File

@ -29,13 +29,17 @@ pub struct PlayerAgent {
/// Which ship this player is controlling
pub ship: Option<ColliderHandle>,
/// What the player has selected
/// What this player has selected
pub selection: PlayerSelection,
/// The amount of money this player has
pub credits: u32,
}
impl PlayerAgent {
pub fn new(ct: &Content, ship: ColliderHandle) -> Self {
Self {
credits: 1000,
ship: Some(ship),
selection: PlayerSelection::OrbitingBody(
ct.systems

View File

@ -287,7 +287,7 @@ impl GPUState {
/// Main render function. Draws sprites on a window.
pub fn render(&mut self, input: &Arc<RenderInput>) -> Result<(), wgpu::SurfaceError> {
if let Some(ship) = input.player.ship {
if let Some(ship) = input.player.borrow().ship {
let o = input.phys_img.get_ship(&PhysSimShipHandle(ship));
if let Some(o) = o {
match o.ship.get_data().get_state() {

View File

@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{cell::RefCell, sync::Arc};
use galactica_content::{Content, System};
use galactica_playeragent::PlayerAgent;
@ -9,7 +9,7 @@ use galactica_util::timing::Timing;
#[derive(Debug)]
pub struct RenderInput {
/// Player ship data
pub player: PlayerAgent,
pub player: RefCell<PlayerAgent>,
/// The system we're currently in
pub current_system: Arc<System>,

View File

@ -4,6 +4,7 @@ use rhai::{plugin::*, Dynamic, Module};
#[allow(non_snake_case)]
#[allow(non_upper_case_globals)]
pub mod player_directive_module {
use crate::ui::api::OutfitState;
use galactica_system::PlayerDirective;
pub const None: PlayerDirective = PlayerDirective::None;
@ -25,4 +26,24 @@ pub mod player_directive_module {
pub fn Guns(state: bool) -> PlayerDirective {
PlayerDirective::Guns(state)
}
pub fn BuyOutfit(outfit: OutfitState, count: i64) -> PlayerDirective {
if count <= 0 {
return PlayerDirective::None;
}
PlayerDirective::BuyOutfit {
outfit: outfit.get_outfit().unwrap(),
count: count as u32,
}
}
pub fn SellOutfit(outfit: OutfitState, count: i64) -> PlayerDirective {
if count <= 0 {
return PlayerDirective::None;
}
PlayerDirective::SellOutfit {
outfit: outfit.get_outfit().unwrap(),
count: count as u32,
}
}
}

View File

@ -1,6 +1,8 @@
use galactica_content::{Content, Outfit};
use galactica_content::{Content, Outfit, OutfitSpace};
use log::error;
use rhai::{CustomType, FnNamespace, FuncRegistration, ImmutableString, Module, TypeBuilder};
use rhai::{
CustomType, Dynamic, FnNamespace, FuncRegistration, ImmutableString, Map, Module, TypeBuilder,
};
use std::sync::Arc;
pub fn build_ct_module(ct_src: Arc<Content>) -> Module {
@ -40,6 +42,10 @@ impl OutfitState {
pub fn new_none() -> Self {
Self { outfit: None }
}
pub fn get_outfit(&self) -> Option<Arc<Outfit>> {
self.outfit.as_ref().cloned()
}
}
impl CustomType for OutfitState {
@ -71,6 +77,20 @@ impl CustomType for OutfitState {
.map(|x| x.thumbnail.index.to_string())
.unwrap_or("".to_string())
})
.with_fn("cost", |s: &mut Self| {
s.outfit.as_ref().map(|x| x.cost).unwrap_or(0)
})
.with_fn("required_space", |s: &mut Self| {
Map::from_iter(
s.outfit
.as_ref()
.map(|x| x.space)
.unwrap_or(OutfitSpace::new())
.to_hashmap()
.iter()
.map(|(k, v)| (ImmutableString::from(k).into(), Dynamic::from(*v as i64))),
)
})
//
// Stat getters
//
@ -104,6 +124,9 @@ impl CustomType for OutfitState {
.map(|x| x.stats.shield_delay)
.unwrap_or(0.0)
})
.with_fn("stat_is_gun", |s: &mut Self| {
s.outfit.as_ref().map(|x| x.gun.is_some()).unwrap_or(false)
})
.with_fn("stat_shield_dps", |s: &mut Self| {
s.outfit
.as_ref()

View File

@ -7,7 +7,7 @@ use galactica_util::to_degrees;
use log::error;
use nalgebra::Vector2;
use rapier2d::dynamics::RigidBody;
use rhai::{Array, CustomType, Dynamic, ImmutableString, TypeBuilder};
use rhai::{Array, CustomType, Dynamic, ImmutableString, Map, TypeBuilder};
use std::sync::Arc;
use super::{functions::OutfitState, Color, UiVector};
@ -94,14 +94,6 @@ impl CustomType for ShipState {
s.get_content().thumbnail.index.clone().into()
})
.with_fn("landed_on", |s: &mut Self| s.landed_on())
.with_fn("current_shields", |s: &mut Self| {
s.get_ship().get_data().get_shields()
})
.with_fn("total_hull", |s: &mut Self| s.get_content().hull)
.with_fn("empty_mass", |s: &mut Self| s.get_content().mass)
.with_fn("current_hull", |s: &mut Self| {
s.get_ship().get_data().get_hull()
})
.with_fn("get_size", |s: &mut Self| s.get_content().size)
.with_fn("phys_uid", |s: &mut Self| format!("{}", s.get_ship().uid))
.with_fn("get_pos", |s: &mut Self| {
@ -114,7 +106,46 @@ impl CustomType for ShipState {
Color::new(c[0], c[1], c[2], 1.0)
})
//
// Stat getters
// Ship stat getters
//
.with_fn("used_space", |s: &mut Self| {
Map::from_iter(
s.get_ship()
.get_data()
.get_outfits()
.get_used_space()
.to_hashmap()
.iter()
.map(|(k, v)| (ImmutableString::from(k).into(), Dynamic::from(*v as i64))),
)
})
.with_fn("total_space", |s: &mut Self| {
Map::from_iter(
s.get_ship()
.get_data()
.get_outfits()
.get_total_space()
.to_hashmap()
.iter()
.map(|(k, v)| (ImmutableString::from(k).into(), Dynamic::from(*v as i64))),
)
})
.with_fn("total_gun_points", |s: &mut Self| {
s.get_ship().get_data().get_outfits().total_gun_points() as i64
})
.with_fn("free_gun_points", |s: &mut Self| {
s.get_ship().get_data().get_outfits().free_gun_points() as i64
})
.with_fn("current_shields", |s: &mut Self| {
s.get_ship().get_data().get_shields()
})
.with_fn("total_hull", |s: &mut Self| s.get_content().hull)
.with_fn("empty_mass", |s: &mut Self| s.get_content().mass)
.with_fn("current_hull", |s: &mut Self| {
s.get_ship().get_data().get_hull()
})
//
// Outfit stat getters
//
.with_fn("stat_thrust", |s: &mut Self| {
s.get_ship()
@ -264,7 +295,12 @@ impl State {
pub fn player_ship(&mut self) -> ShipState {
ShipState {
input: self.input.clone(),
ship: self.input.player.ship.map(|x| PhysSimShipHandle(x)),
ship: self
.input
.player
.borrow()
.ship
.map(|x| PhysSimShipHandle(x)),
}
}

View File

@ -301,7 +301,7 @@ impl UiScriptExecutor {
// Send player state change events
if {
let player = input.player.ship;
let player = input.player.borrow().ship;
if let Some(player) = player {
let ship = input.phys_img.get_ship(&PhysSimShipHandle(player)).unwrap();
if self.last_player_state == 0

View File

@ -104,6 +104,7 @@ impl OutfitSet {
if outfit.is_none() {
*outfit = Some(o.clone());
added = true;
break;
}
}
if !added {
@ -112,14 +113,15 @@ impl OutfitSet {
}
self.used_space += o.space;
self.stats.add(&o.stats);
if o.stats.shield_generation != 0.0 {
self.shield_generators.push(ShieldGenerator {
outfit: o.clone(),
delay: o.stats.shield_delay,
generation: o.stats.shield_generation,
});
}
if self.outfits.contains_key(&o.index) {
self.outfits.get_mut(&o.index).unwrap().1 += 1;
@ -133,26 +135,32 @@ impl OutfitSet {
pub(super) fn remove(&mut self, o: &Arc<Outfit>) -> OutfitRemoveResult {
if !self.outfits.contains_key(&o.index) {
return OutfitRemoveResult::NotExist;
} else {
}
let n = self.outfits.get_mut(&o.index).unwrap();
if n.1 == 1u32 {
self.outfits.remove(&o.index);
} else {
self.outfits.get_mut(&o.index).unwrap().1 -= 1;
}
if o.gun.is_some() {
let (_, x) = self
.gun_points
.iter_mut()
.find(|(_, x)| x.is_some() && x.as_ref().unwrap().index == o.index)
.unwrap();
*x = None;
}
self.used_space -= o.space;
self.stats.subtract(&o.stats);
{
// This index will exist, since we checked the hashmap
let index = self
.shield_generators
.iter()
.position(|g| g.outfit.index == o.index)
.unwrap();
.position(|g| g.outfit.index == o.index);
if let Some(index) = index {
self.shield_generators.remove(index);
}
@ -198,6 +206,29 @@ impl OutfitSet {
&self.used_space
}
/// Number of available (used & free) gun points
pub fn total_gun_points(&self) -> usize {
self.gun_points.len()
}
/// Number of free gun points
pub fn free_gun_points(&self) -> usize {
self.iter_gun_points().filter(|(_, o)| o.is_none()).count()
}
/// Does this set contain `count` of `outfit`?
pub fn has_outfit(&self, outfit: &Arc<Outfit>, mut count: u32) -> bool {
for i in self.iter_outfits() {
if count <= 0 {
return true;
}
if i.0.index == outfit.index {
count -= 1;
}
}
return count <= 0;
}
/// Get the combined stats of all outfits in this set.
/// There are two things to note here:
/// First, shield_delay is always zero. That is handled

View File

@ -14,7 +14,7 @@ use rapier2d::{
use super::{autopilot, collapse::ShipCollapseSequence, controller::ShipController, ShipControls};
use crate::{
data::{ShipAutoPilot, ShipData, ShipPersonality, ShipState},
data::{OutfitRemoveResult, ShipAutoPilot, ShipData, ShipPersonality, ShipState},
phys::{
get_phys_id,
objects::{PhysEffect, PhysProjectile},
@ -505,8 +505,8 @@ impl PhysShip {
}
/// Add one outfit to this ship
pub fn add_outfit(&mut self, o: Arc<Outfit>) {
self.data.add_outfit(&o);
pub fn add_outfit(&mut self, o: &Arc<Outfit>) {
self.data.add_outfit(o);
self.update_flares();
}
@ -517,6 +517,13 @@ impl PhysShip {
}
self.update_flares();
}
/// Remove one outfit from this ship
pub fn remove_outfit(&mut self, o: &Arc<Outfit>) -> OutfitRemoveResult {
let r = self.data.remove_outfit(o);
self.update_flares();
return r;
}
}
/// Public immutable

View File

@ -192,7 +192,7 @@ impl PhysSim {
}
/// Update a player ship's controls
pub fn apply_directive(&mut self, directive: PlayerDirective, player: &PlayerAgent) {
pub fn apply_directive(&mut self, directive: PlayerDirective, player: &mut PlayerAgent) {
if player.ship.is_none() {
return;
}

View File

@ -1,3 +1,7 @@
use std::sync::Arc;
use galactica_content::Outfit;
/// An action the player wants to take in the game.
#[derive(Debug, Clone)]
pub enum PlayerDirective {
@ -21,4 +25,26 @@ pub enum PlayerDirective {
/// Take off from the planet we're landed on
UnLand,
/// Buy an outfit.
/// This directive is ignored if the player does not have enough
/// credits to buy the given number of outfits.
BuyOutfit {
/// The outfit to buy
outfit: Arc<Outfit>,
/// How many of this outfit to buy
count: u32,
},
/// Sell an outfit.
/// This directive is ignored if the player does not have enough
/// outfits to sell.
SellOutfit {
/// The outfit to sell
outfit: Arc<Outfit>,
/// How many of this outfit to sell
count: u32,
},
}