diff --git a/Cargo.toml b/Cargo.toml index 13b1021..7316151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ arrayvec = "0.5" enum_dispatch = "0.3" enum-iterator = "0.6" js-sys = "0.3" -num-derive = "0.3" +num-derive = "0.4" num-traits = "0.2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src/constants.rs b/src/constants.rs index af8697b..5f8dde9 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -70,7 +70,7 @@ mod types; //pub mod look; // TODO: most/all of this is World specific //pub mod find; // TODO: most/all of this is World specific -pub use self::{numbers::*, small_enums::*, types::*}; +pub use self::{extra::*, numbers::*, small_enums::*, types::*}; //pub use self::{extra::*, look::*, recipes::FactoryRecipe}; // TODO: most/all // of this is World specific diff --git a/src/constants/extra.rs b/src/constants/extra.rs index d50d19b..3fec3ac 100644 --- a/src/constants/extra.rs +++ b/src/constants/extra.rs @@ -75,3 +75,7 @@ pub const RANGED_MASS_ATTACK_POWER_RANGE_2: u32 = 4; /// /// [`Creep::ranged_mass_attack`]: crate::objects::Creep::ranged_mass_attack pub const RANGED_MASS_ATTACK_POWER_RANGE_3: u32 = 1; + +pub const ROOM_WIDTH: u8 = 100; + +pub const ROOM_HEIGHT: u8 = 100; diff --git a/src/constants/small_enums.rs b/src/constants/small_enums.rs index 6b5eb11..f7e5622 100644 --- a/src/constants/small_enums.rs +++ b/src/constants/small_enums.rs @@ -95,6 +95,23 @@ pub enum Direction { TopLeft = 8, } +impl From for (i8, i8) { + /// Returns the change in (x, y) when moving in each direction. + #[inline] + fn from(direction: Direction) -> (i8, i8) { + match direction { + Direction::Top => (0, -1), + Direction::TopRight => (1, -1), + Direction::Right => (1, 0), + Direction::BottomRight => (1, 1), + Direction::Bottom => (0, 1), + Direction::BottomLeft => (-1, 1), + Direction::Left => (-1, 0), + Direction::TopLeft => (-1, -1), + } + } +} + impl ::std::ops::Neg for Direction { type Output = Direction; diff --git a/src/game.rs b/src/game.rs index 2eece84..2fd226a 100644 --- a/src/game.rs +++ b/src/game.rs @@ -9,6 +9,7 @@ use wasm_bindgen::prelude::*; pub mod pathfinder; pub mod utils; +pub mod visual; #[wasm_bindgen(module = "game")] extern "C" { diff --git a/src/game/pathfinder.rs b/src/game/pathfinder.rs index 348a878..b524870 100644 --- a/src/game/pathfinder.rs +++ b/src/game/pathfinder.rs @@ -1,13 +1,70 @@ +use crate::constants::{Direction, ROOM_HEIGHT, ROOM_WIDTH}; use js_sys::{Array, Object}; use serde::{Deserialize, Serialize}; +use std::{fmt, ops::Add}; use wasm_bindgen::prelude::*; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct Position { pub x: u8, pub y: u8, } +impl fmt::Display for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[pos {},{}]", self.x, self.y) + } +} + +impl Position { + pub fn range_to(&self, pos: &Position) -> u8 { + std::cmp::max(self.x.abs_diff(pos.x), self.y.abs_diff(pos.y)) + } + + pub fn checked_add_direction(&self, dir: Direction) -> Option { + let delta: (i8, i8) = dir.into(); + if (self.x == 0 && delta.0 < 0) + || (self.x == ROOM_WIDTH - 1 && delta.0 > 0) + || (self.y == 0 && delta.1 < 0) + || (self.y == ROOM_HEIGHT - 1 && delta.1 > 0) + { + None + } else { + Some(Position { + x: self.x.wrapping_add_signed(delta.0), + y: self.y.wrapping_add_signed(delta.1), + }) + } + } + + pub fn saturating_add_direction(&self, dir: Direction) -> Position { + let mut delta: (i8, i8) = dir.into(); + if self.x >= ROOM_WIDTH - 1 && delta.0 > 0 { + delta.0 = 0; + } + if self.y >= ROOM_HEIGHT - 1 && delta.1 > 0 { + delta.1 = 0; + } + Position { + x: self.x.saturating_add_signed(delta.0), + y: self.y.saturating_add_signed(delta.1), + } + } +} + +impl From for JsValue { + fn from(pos: Position) -> JsValue { + serde_wasm_bindgen::to_value(&pos).expect("serializable Position") + } +} + +impl Add for Position { + type Output = Position; + fn add(self, other: Direction) -> Self::Output { + self.checked_add_direction(other).expect("room boundaries") + } +} + #[wasm_bindgen(module = "game/path-finder")] extern "C" { /// Find an optimal path between origin and goal. Note that searchPath diff --git a/src/game/visual.rs b/src/game/visual.rs new file mode 100644 index 0000000..69140f8 --- /dev/null +++ b/src/game/visual.rs @@ -0,0 +1,493 @@ +use crate::{game::pathfinder::Position, traits::GameObjectProperties}; +use js_sys::Array; +use serde::Serialize; +use wasm_bindgen::prelude::*; + +#[derive(Debug, Clone, Serialize)] +pub struct VisualPosition { + pub x: f32, + pub y: f32, +} + +impl VisualPosition { + pub fn offset(&self, x: f32, y: f32) -> VisualPosition { + VisualPosition { + x: self.x + x, + y: self.y + y, + } + } +} + +impl From<&Position> for VisualPosition { + fn from(pos: &Position) -> Self { + VisualPosition { + x: pos.x as f32, + y: pos.y as f32, + } + } +} + +impl From for VisualPosition { + fn from(pos: Position) -> Self { + VisualPosition { + x: pos.x as f32, + y: pos.y as f32, + } + } +} + +impl From for VisualPosition +where + T: GameObjectProperties, +{ + fn from(obj: T) -> Self { + VisualPosition { + x: obj.x() as f32, + y: obj.y() as f32, + } + } +} + +impl From for JsValue { + fn from(pos: VisualPosition) -> JsValue { + serde_wasm_bindgen::to_value(&pos).expect("serializable VisualPosition") + } +} + +impl From<&VisualPosition> for JsValue { + fn from(pos: &VisualPosition) -> JsValue { + serde_wasm_bindgen::to_value(pos).expect("serializable VisualPosition") + } +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CircleStyle { + #[serde(skip_serializing_if = "Option::is_none")] + radius: Option, + #[serde(skip_serializing_if = "Option::is_none")] + fill: Option, + #[serde(skip_serializing_if = "Option::is_none")] + opacity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stroke: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stroke_width: Option, +} + +impl CircleStyle { + pub fn radius(mut self, val: f32) -> CircleStyle { + self.radius = Some(val); + self + } + + pub fn fill(mut self, val: &str) -> CircleStyle { + self.fill = Some(val.to_string()); + self + } + + pub fn opacity(mut self, val: f32) -> CircleStyle { + self.opacity = Some(val); + self + } + + pub fn stroke(mut self, val: &str) -> CircleStyle { + self.stroke = Some(val.to_string()); + self + } + + pub fn stroke_width(mut self, val: f32) -> CircleStyle { + self.stroke_width = Some(val); + self + } +} + +impl From for JsValue { + fn from(style: CircleStyle) -> JsValue { + serde_wasm_bindgen::to_value(&style).expect("serializable CircleStyle") + } +} + +impl From<&CircleStyle> for JsValue { + fn from(circle: &CircleStyle) -> JsValue { + serde_wasm_bindgen::to_value(circle).expect("serializable CircleStyle") + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum LineDrawStyle { + #[default] + Solid, + Dashed, + Dotted, +} + +impl LineDrawStyle { + pub fn is_solid(&self) -> bool { + matches!(self, LineDrawStyle::Solid) + } +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LineStyle { + #[serde(skip_serializing_if = "Option::is_none")] + width: Option, + #[serde(skip_serializing_if = "Option::is_none")] + color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + opacity: Option, + #[serde(skip_serializing_if = "LineDrawStyle::is_solid")] + line_style: LineDrawStyle, +} + +impl LineStyle { + pub fn width(mut self, val: f32) -> LineStyle { + self.width = Some(val); + self + } + + pub fn color(mut self, val: &str) -> LineStyle { + self.color = Some(val.to_string()); + self + } + + pub fn opacity(mut self, val: f32) -> LineStyle { + self.opacity = Some(val); + self + } + + pub fn line_style(mut self, val: LineDrawStyle) -> LineStyle { + self.line_style = val; + self + } +} + +impl From for JsValue { + fn from(style: LineStyle) -> JsValue { + serde_wasm_bindgen::to_value(&style).expect("serializable LineStyle") + } +} + +impl From<&LineStyle> for JsValue { + fn from(style: &LineStyle) -> JsValue { + serde_wasm_bindgen::to_value(style).expect("serializable LineStyle") + } +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RectStyle { + #[serde(skip_serializing_if = "Option::is_none")] + fill: Option, + #[serde(skip_serializing_if = "Option::is_none")] + opacity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stroke: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stroke_width: Option, + #[serde(skip_serializing_if = "LineDrawStyle::is_solid")] + line_style: LineDrawStyle, +} + +impl RectStyle { + pub fn fill(mut self, val: &str) -> RectStyle { + self.fill = Some(val.to_string()); + self + } + + pub fn opacity(mut self, val: f32) -> RectStyle { + self.opacity = Some(val); + self + } + + pub fn stroke(mut self, val: &str) -> RectStyle { + self.stroke = Some(val.to_string()); + self + } + + pub fn stroke_width(mut self, val: f32) -> RectStyle { + self.stroke_width = Some(val); + self + } + + pub fn line_style(mut self, val: LineDrawStyle) -> RectStyle { + self.line_style = val; + self + } +} + +impl From for JsValue { + fn from(style: RectStyle) -> JsValue { + serde_wasm_bindgen::to_value(&style).expect("serializable RectStyle") + } +} + +impl From<&RectStyle> for JsValue { + fn from(style: &RectStyle) -> JsValue { + serde_wasm_bindgen::to_value(style).expect("serializable RectStyle") + } +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PolyStyle { + #[serde(skip_serializing_if = "Option::is_none")] + fill: Option, + #[serde(skip_serializing_if = "Option::is_none")] + opacity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stroke: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stroke_width: Option, + #[serde(skip_serializing_if = "LineDrawStyle::is_solid")] + line_style: LineDrawStyle, +} + +impl PolyStyle { + pub fn fill(mut self, val: &str) -> PolyStyle { + self.fill = Some(val.to_string()); + self + } + + pub fn opacity(mut self, val: f32) -> PolyStyle { + self.opacity = Some(val); + self + } + + pub fn stroke(mut self, val: &str) -> PolyStyle { + self.stroke = Some(val.to_string()); + self + } + + pub fn stroke_width(mut self, val: f32) -> PolyStyle { + self.stroke_width = Some(val); + self + } + + pub fn line_style(mut self, val: LineDrawStyle) -> PolyStyle { + self.line_style = val; + self + } +} + +impl From for JsValue { + fn from(style: PolyStyle) -> JsValue { + serde_wasm_bindgen::to_value(&style).expect("serializable PolyStyle") + } +} + +impl From<&PolyStyle> for JsValue { + fn from(style: &PolyStyle) -> JsValue { + serde_wasm_bindgen::to_value(style).expect("serializable PolyStyle") + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +enum FontStyle { + Size(f32), + Custom(String), +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TextAlign { + #[default] + Center, + Left, + Right, +} + +impl TextAlign { + pub fn is_center(&self) -> bool { + matches!(self, TextAlign::Center) + } +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TextStyle { + #[serde(skip_serializing_if = "Option::is_none")] + color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + font: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stroke: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stroke_width: Option, + #[serde(skip_serializing_if = "Option::is_none")] + background_color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + background_padding: Option, + #[serde(skip_serializing_if = "TextAlign::is_center")] + align: TextAlign, + #[serde(skip_serializing_if = "Option::is_none")] + opacity: Option, +} + +impl TextStyle { + pub fn color(mut self, val: &str) -> TextStyle { + self.color = Some(val.to_string()); + self + } + + pub fn font_size(mut self, val: f32) -> TextStyle { + self.font = Some(FontStyle::Size(val)); + self + } + + pub fn font_style(mut self, val: &str) -> TextStyle { + self.font = Some(FontStyle::Custom(val.to_string())); + self + } + + pub fn stroke(mut self, val: &str) -> TextStyle { + self.stroke = Some(val.to_string()); + self + } + + pub fn stroke_width(mut self, val: f32) -> TextStyle { + self.stroke_width = Some(val); + self + } + + pub fn background_color(mut self, val: &str) -> TextStyle { + self.background_color = Some(val.to_string()); + self + } + + pub fn background_padding(mut self, val: f32) -> TextStyle { + self.background_padding = Some(val); + self + } + + pub fn align(mut self, val: TextAlign) -> TextStyle { + self.align = val; + self + } + + pub fn opacity(mut self, val: f32) -> TextStyle { + self.opacity = Some(val); + self + } +} + +impl From for JsValue { + fn from(style: TextStyle) -> JsValue { + serde_wasm_bindgen::to_value(&style).expect("serializable TextStyle") + } +} + +impl From<&TextStyle> for JsValue { + fn from(style: &TextStyle) -> JsValue { + serde_wasm_bindgen::to_value(style).expect("serializable TextStyle") + } +} + +#[wasm_bindgen(module = "game/visual")] +extern "C" { + #[wasm_bindgen] + pub type Visual; + + #[wasm_bindgen(constructor)] + pub fn new(layer: Option, persistent: bool) -> Visual; + + #[wasm_bindgen(method, getter)] + pub fn layer(this: &Visual) -> i32; + + #[wasm_bindgen(method, getter)] + pub fn persistent(this: &Visual) -> bool; + + #[wasm_bindgen(method, js_name = circle)] + pub fn circle_internal(this: &Visual, pos: &JsValue, style: &JsValue) -> Visual; + + #[wasm_bindgen(method, js_name = line)] + pub fn line_internal(this: &Visual, from: &JsValue, to: &JsValue, style: &JsValue) -> Visual; + + #[wasm_bindgen(method, js_name = rect)] + pub fn rect_internal( + this: &Visual, + top_left: &JsValue, + width: f32, + height: f32, + style: &JsValue, + ) -> Visual; + + #[wasm_bindgen(method, js_name = poly)] + pub fn poly_internal(this: &Visual, points: &Array, style: &JsValue) -> Visual; + + #[wasm_bindgen(method, js_name = text)] + pub fn text_internal(this: &Visual, text: &str, pos: &JsValue, style: &JsValue) -> Visual; +} + +impl Visual { + pub fn circle(self: &Visual, pos: &VisualPosition, style: Option<&CircleStyle>) -> Visual { + match style { + Some(style) => self.circle_internal(&JsValue::from(pos), &JsValue::from(style)), + None => self.circle_internal(&JsValue::from(pos), &JsValue::UNDEFINED), + } + } + + pub fn line( + self: &Visual, + from: &VisualPosition, + to: &VisualPosition, + style: Option<&LineStyle>, + ) -> Visual { + match style { + Some(style) => self.line_internal( + &JsValue::from(from), + &JsValue::from(to), + &JsValue::from(style), + ), + None => self.line_internal( + &JsValue::from(from), + &JsValue::from(to), + &JsValue::UNDEFINED, + ), + } + } + + pub fn rect( + self: &Visual, + top_left: &VisualPosition, + width: f32, + height: f32, + style: Option<&RectStyle>, + ) -> Visual { + match style { + Some(style) => self.rect_internal( + &JsValue::from(top_left), + width, + height, + &JsValue::from(style), + ), + None => { + self.rect_internal(&JsValue::from(top_left), width, height, &JsValue::UNDEFINED) + } + } + } + + pub fn poly(self: &Visual, points: &[VisualPosition], style: Option<&PolyStyle>) -> Visual { + let points = points.iter().cloned().map(JsValue::from).collect(); + match style { + Some(style) => self.poly_internal(&points, &JsValue::from(style)), + None => self.poly_internal(&points, &JsValue::UNDEFINED), + } + } + + pub fn text( + self: &Visual, + text: &str, + pos: &VisualPosition, + style: Option<&TextStyle>, + ) -> Visual { + match style { + Some(style) => self.text_internal(text, &JsValue::from(pos), &JsValue::from(style)), + + None => self.text_internal(text, &JsValue::from(pos), &JsValue::UNDEFINED), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index d4170b8..815281a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,9 @@ // to build locally with doc_cfg enabled, run: // `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` #![cfg_attr(docsrs, feature(doc_cfg))] +// temporary workaround for https://github.com/rust-lang/rust-clippy/issues/12377 +// fix not being in current stable rust 1.78; should be fixed in 1.79 +#![allow(clippy::empty_docs)] pub mod constants; pub mod enums; diff --git a/src/objects/impls/creep.rs b/src/objects/impls/creep.rs index 7ce6295..184a37b 100644 --- a/src/objects/impls/creep.rs +++ b/src/objects/impls/creep.rs @@ -104,6 +104,10 @@ extern "C" { #[wasm_bindgen(final, method, js_name = rangedMassAttack)] pub fn ranged_mass_attack(this: &Creep) -> ReturnCode; + /// This Creep attribute is only documented in the typescript typings. + #[wasm_bindgen(method, getter)] + pub fn spawning(this: &Creep) -> bool; + // todo not yet in game but should be like this // #[wasm_bindgen(final, method)] // pub fn repair(this: &Creep, target: &GameObject) -> ReturnCode; diff --git a/src/objects/impls/game_object.rs b/src/objects/impls/game_object.rs index b845dfc..dd82565 100644 --- a/src/objects/impls/game_object.rs +++ b/src/objects/impls/game_object.rs @@ -1,5 +1,5 @@ use crate::{ - game::pathfinder::{FindPathOptions, SearchResults}, + game::pathfinder::{FindPathOptions, Position, SearchResults}, prelude::*, }; use js_sys::{Array, JsString, Object}; @@ -79,6 +79,13 @@ impl GameObject { } } } + + pub fn pos(&self) -> Position { + Position { + x: self.x(), + y: self.y(), + } + } } impl GameObjectProperties for T @@ -101,6 +108,10 @@ where GameObject::y(self.as_ref()) } + fn pos(&self) -> Position { + GameObject::pos(self.as_ref()) + } + fn ticks_to_decay(&self) -> Option { GameObject::ticks_to_decay(self.as_ref()) } diff --git a/src/traits.rs b/src/traits.rs index 2b91c68..f6590d7 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -3,7 +3,7 @@ use js_sys::{Array, JsString, Object}; use crate::{ enums::*, - game::pathfinder::{FindPathOptions, SearchResults}, + game::pathfinder::{FindPathOptions, Position, SearchResults}, objects::*, }; @@ -152,6 +152,8 @@ pub trait GameObjectProperties { /// The Y coordinate in the room. fn y(&self) -> u8; + fn pos(&self) -> Position; + /// If defined, then this object will disappear after this number of ticks. fn ticks_to_decay(&self) -> Option;