diff --git a/Cargo.lock b/Cargo.lock index 2788868..8d96b38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,6 +40,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", + "const-random", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -409,6 +411,26 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642" +[[package]] +name = "const-random" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaf16c9c2c612020bcfd042e170f6e32de9b9d75adb5277cdbbd2e2c8c8299a" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -795,6 +817,7 @@ dependencies = [ "log", "nalgebra", "rapier2d", + "rhai", "serde", "toml", "walkdir", @@ -842,6 +865,7 @@ dependencies = [ "log", "nalgebra", "rand", + "rhai", "wgpu", "winit", ] @@ -1934,6 +1958,36 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" +[[package]] +name = "rhai" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0de8df6dd1f4886ad635d7e0fc7afc086e4f6daeff129d157287b78738516b" +dependencies = [ + "ahash", + "bitflags 2.4.1", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "serde", + "serde_json", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_codegen" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89789ba6e8fd0889ae70b39c09000148431c9a1d618eb9d388373f391a55c988" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.42", +] + [[package]] name = "robust" version = "1.1.0" @@ -2137,6 +2191,21 @@ name = "smallvec" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +dependencies = [ + "serde", +] + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "serde", + "static_assertions", + "version_check", +] [[package]] name = "smithay-client-toolkit" @@ -2262,6 +2331,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thin-vec" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" +dependencies = [ + "serde", +] + [[package]] name = "thiserror" version = "1.0.51" @@ -2303,6 +2381,15 @@ dependencies = [ "weezl", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tiny-skia" version = "0.8.4" diff --git a/Cargo.toml b/Cargo.toml index 756335b..d3a8bd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,3 +74,9 @@ lazy_static = "1.4.0" clap = { version = "4.4.18", features = ["derive"] } log = "0.4.20" log4rs = { version = "1.2.0", features = ["console_appender"] } +rhai = { version = "1.17.0", features = [ + "f32_float", + "metadata", + "sync", + "no_custom_syntax", +] } diff --git a/content/config.toml b/content/config.toml index f0ac483..7d7c45e 100644 --- a/content/config.toml +++ b/content/config.toml @@ -36,3 +36,7 @@ starfield.texture = "starfield.png" # Zoom is measured as "window height in game units." zoom_min = 200.0 zoom_max = 2000.0 + +# TODO: move to user config file +ui_scale = 2 +ui_landed_scene = "landed.rhai" diff --git a/content/landed.rhai b/content/landed.rhai new file mode 100644 index 0000000..e59a39f --- /dev/null +++ b/content/landed.rhai @@ -0,0 +1,67 @@ + +fn init(state) { + let frame = SpriteBuilder( + "frame", + "ui::planet", + Rect( + 0.0, 0.0, 400.0, 297.866, + SpriteAnchor::Center, + SpriteAnchor::Center + ) + ); + + let landscape = SpriteBuilder( + "landscape", + state.planet_landscape, + Rect( + -180.0, 142.0, 274.0, 135.0, + SpriteAnchor::NorthWest, + SpriteAnchor::Center + ) + ); + landscape.set_mask("ui::landscapemask"); + + let button = SpriteBuilder( + "button", + "ui::planet::button", + Rect( + 99.0, 128.0, 73.898, 18.708, + SpriteAnchor::NorthWest, + SpriteAnchor::Center + ) + ); + + let title = TextBoxBuilder( + "title", + 10.0, 10.0, TextBoxFont::Serif, TextBoxJustify::Center, + Rect( + -70.79, 138.0, 59.867, 10.0, + SpriteAnchor::NorthWest, + SpriteAnchor::Center + ) + ); + title.set_text(state.planet_name); + + return [ + button, + landscape, + frame, + title, + ]; +} + +fn hover(element, hover_state) { + if element.has_name("button") { + if hover_state { + element.take_edge("on:top", 0.1); + } else { + element.take_edge("off:top", 0.1); + } + } +} + +//fn click(scene, name) { +// if name = "button" { +// return SceneAction::Outfitter(); +// } +//} \ No newline at end of file diff --git a/content/ship.toml b/content/ship.toml index 76f71af..3a91013 100644 --- a/content/ship.toml +++ b/content/ship.toml @@ -1,5 +1,6 @@ [ship."Gypsum"] sprite = "ship::gypsum" +thumb = "icon::gypsum" size = 100 mass = 1 hull = 200 diff --git a/content/ui.toml b/content/ui.toml deleted file mode 100644 index 1367384..0000000 --- a/content/ui.toml +++ /dev/null @@ -1,80 +0,0 @@ -[ui.status] - -# TODO: unified color value -# TODO: bar type: linear/radial -# TODO: bar as ui util struct -# TODO: mouse collider -# TODO: modular UI (how?) - -# shield_bar.pos = [-19, -19] -# shield_bar.diameter = 182 -# shield_bar.stroke = 5 -# shield_bar.color = [0.3, 0.6, 0.8, 1.0] -# -# hull_bar.pos = [-27.0, -27.0] -# hull_bar.diameter = 166.0 -# hull_bar.stroke = 5 -# hull_bar.color = [0.8, 0.7, 0.5, 1.0] -# frame.sprite = "ui::status" -# frame.pos = [-10.0, -10.0] -# frame.dim = [200.0, 200.0] - - -[ui.landed] -frame.sprite = "ui::planet" -frame.rect.pos = [0.0, 0.0] -frame.rect.dim = [800.0, 800.0] -frame.rect.anchor_self = "center" -frame.rect.anchor_parent = "center" - -landscape.mask = "ui::landscapemask" -landscape.rect.pos = [-350.0, 282.8] -landscape.rect.dim = [537.5, 270.31] -landscape.rect.anchor_self = "northwest" -landscape.rect.anchor_parent = "center" - - -button.sprite = "ui::planet::button" -button.rect.pos = [178.12, 254.6] -button.rect.dim = [175.16, 38.437] -button.rect.anchor_self = "northwest" -button.rect.anchor_parent = "center" -button.on_mouse_enter.edge = "on:top" -button.on_mouse_enter.duration = 0.1 -button.on_mouse_leave.edge = "off:top" -button.on_mouse_leave.duration = 0.1 - -planet_name.rect.pos = [-143.89, 273] -planet_name.rect.dim = [121.98, 18.094] -planet_name.rect.anchor_self = "northwest" -planet_name.rect.anchor_parent = "center" -planet_name.font_size = 19 -planet_name.line_height = 19 -planet_name.align = "center" - -planet_desc.rect.pos = [-358.43, -32] -planet_desc.rect.dim = [673.91, 153.75] -planet_desc.rect.anchor_self = "northwest" -planet_desc.rect.anchor_parent = "center" -planet_desc.font_size = 16 -planet_desc.line_height = 18 -planet_desc.align = "left" - - -[ui.outfitter] - -se_box.sprite = "ui::outfitterbox" -se_box.rect.pos = [-10.0, -10.0] -se_box.rect.dim = [512.0, 337.0] # todo: auto aspect -se_box.rect.anchor_self = "southwest" -se_box.rect.anchor_parent = "southwest" - -exit_button.sprite = "ui::button" -exit_button.rect.pos = [279.07, 135.38] -exit_button.rect.dim = [173.44, 45.01] -exit_button.rect.anchor_self = "northwest" -exit_button.rect.anchor_parent = "southwest" -exit_button.on_mouse_enter.edge = "on:top" -exit_button.on_mouse_enter.duration = 0.1 -exit_button.on_mouse_leave.edge = "off:top" -exit_button.on_mouse_leave.duration = 0.1 diff --git a/crates/content/Cargo.toml b/crates/content/Cargo.toml index 55e76e3..191a2c0 100644 --- a/crates/content/Cargo.toml +++ b/crates/content/Cargo.toml @@ -29,3 +29,4 @@ image = { workspace = true } rapier2d = { workspace = true } lazy_static = { workspace = true } log = { workspace = true } +rhai = { workspace = true } diff --git a/crates/content/src/lib.rs b/crates/content/src/lib.rs index e639d24..d8869b3 100644 --- a/crates/content/src/lib.rs +++ b/crates/content/src/lib.rs @@ -12,6 +12,7 @@ use anyhow::{bail, Context, Result}; use galactica_packer::{SpriteAtlas, SpriteAtlasImage}; use log::warn; use std::{ + //cell::OnceCell, collections::HashMap, fs::File, io::Read, @@ -33,7 +34,6 @@ mod syntax { use crate::{ config, part::{effect, faction, outfit, ship, sprite, system}, - ui, }; #[derive(Debug, Deserialize)] @@ -45,7 +45,6 @@ mod syntax { pub faction: Option>, pub effect: Option>, pub config: Option, - pub ui: Option, } fn merge_hashmap( @@ -83,7 +82,6 @@ mod syntax { faction: None, effect: None, config: None, - ui: None, } } @@ -107,15 +105,6 @@ mod syntax { } else { self.config = other.config; } - - if self.ui.is_some() { - if other.ui.is_some() { - bail!("invalid content dir, multiple ui tables") - } - } else { - self.ui = other.ui; - } - return Ok(()); } } @@ -139,16 +128,12 @@ trait Build { pub(crate) struct ContentBuildContext { /// Map effect names to handles pub effect_index: HashMap, - - /// Maps sprite handles to a map of section name -> section index - pub sprite_section_index: HashMap>, } impl ContentBuildContext { fn new() -> Self { Self { effect_index: HashMap::new(), - sprite_section_index: HashMap::new(), } } } @@ -172,7 +157,6 @@ pub struct Content { factions: Vec, effects: Vec, config: Config, - ui: Option, } // Loading methods @@ -185,10 +169,17 @@ impl Content { } /// Load content from a directory. - pub fn load_dir(path: PathBuf, asset_root: PathBuf, atlas_index: PathBuf) -> Result { + pub fn load_dir( + content_root: PathBuf, + asset_root: PathBuf, + atlas_index: PathBuf, + ) -> Result { let mut root = syntax::Root::new(); - for e in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) { + for e in WalkDir::new(&content_root) + .into_iter() + .filter_map(|e| e.ok()) + { if e.metadata().unwrap().is_file() { // TODO: better warnings match e.path().extension() { @@ -225,7 +216,7 @@ impl Content { let mut content = Self { config: { if let Some(c) = root.config { - c.build(&asset_root, &atlas) + c.build(&asset_root, &content_root, &atlas) .with_context(|| "while parsing config table")? } else { bail!("failed loading content: no config table specified") @@ -241,7 +232,6 @@ impl Content { factions: Vec::new(), effects: Vec::new(), sprite_index: HashMap::new(), - ui: None, }; // TODO: enforce sprite and image limits @@ -263,10 +253,6 @@ impl Content { )?; } - if root.ui.is_some() { - part::ui::Ui::build(root.ui.take().unwrap(), &mut build_context, &mut content)?; - } - // Order below this line does not matter if root.ship.is_some() { part::ship::Ship::build(root.ship.take().unwrap(), &mut build_context, &mut content)?; @@ -305,11 +291,8 @@ impl Content { } /// Get a handle from a sprite name - pub fn get_sprite_handle(&self, name: &str) -> SpriteHandle { - return match self.sprite_index.get(name) { - Some(s) => *s, - None => unreachable!("get_sprite_handle was called with a bad name!"), - }; + pub fn get_sprite_handle(&self, name: &str) -> Option { + self.sprite_index.get(name).map(|x| *x) } /// Get a sprite from a handle @@ -373,9 +356,23 @@ impl Content { pub fn get_config(&self) -> &Config { return &self.config; } - - /// Get ui configuration - pub fn get_ui(&self) -> &Ui { - return self.ui.as_ref().unwrap(); - } } + +/* +TODO: don't pass content around? +static mut CONTENT: OnceCell = OnceCell::new(); + +/// Initialize content::CONTENT with the given paths +pub fn init(content_dir: PathBuf, asset_dir: PathBuf, atlas_index: PathBuf) -> Result<()> { + let content = Content::load_dir(content_dir, asset_dir, atlas_index)?; + unsafe { + match CONTENT.set(content) { + Ok(()) => {} + Err(_) => { + bail!("cannot initialize content, already set.") + } + }; + } + return Ok(()); +} +*/ diff --git a/crates/content/src/part/config.rs b/crates/content/src/part/config.rs index 1e04117..5822271 100644 --- a/crates/content/src/part/config.rs +++ b/crates/content/src/part/config.rs @@ -1,8 +1,11 @@ use std::{num::NonZeroU32, path::PathBuf}; +use rhai::AST; + pub(crate) mod syntax { - use anyhow::{bail, Result}; + use anyhow::{bail, Context, Result}; use galactica_packer::SpriteAtlas; + use rhai::Engine; use serde::Deserialize; use std::path::{Path, PathBuf}; @@ -16,11 +19,18 @@ pub(crate) mod syntax { pub starfield: Starfield, pub zoom_min: f32, pub zoom_max: f32, + pub ui_scale: f32, + pub ui_landed_scene: PathBuf, } impl Config { // TODO: clean up build trait - pub fn build(self, asset_root: &Path, atlas: &SpriteAtlas) -> Result { + pub fn build( + self, + asset_root: &Path, + content_root: &Path, + atlas: &SpriteAtlas, + ) -> Result { for i in &self.fonts.files { if !asset_root.join(i).exists() { bail!("font file `{}` doesn't exist", i.display()); @@ -46,6 +56,12 @@ pub(crate) mod syntax { } }; + let engine = Engine::new(); + let ui_landed_scene = engine + .compile_file(content_root.join(self.ui_landed_scene)) + .with_context(|| format!("while loading `landed` scene")) + .with_context(|| format!("while loading config"))?; + return Ok(super::Config { sprite_root: asset_root.join(self.sprite_root), font_files: self @@ -71,6 +87,8 @@ pub(crate) mod syntax { starfield_instance_limit, zoom_max: self.zoom_max, zoom_min: self.zoom_min, + ui_scale: self.ui_scale, + ui_landed_scene, }); } } @@ -153,4 +171,10 @@ pub struct Config { /// Maximum zoom,in game units pub zoom_max: f32, + + /// Ui scale factor + pub ui_scale: f32, + + /// Ui landed scene + pub ui_landed_scene: AST, } diff --git a/crates/content/src/part/mod.rs b/crates/content/src/part/mod.rs index ec5a8d8..b7f27e9 100644 --- a/crates/content/src/part/mod.rs +++ b/crates/content/src/part/mod.rs @@ -8,7 +8,6 @@ pub(crate) mod outfitspace; pub(crate) mod ship; pub(crate) mod sprite; pub(crate) mod system; -pub(crate) mod ui; pub use config::Config; pub use effect::*; @@ -21,4 +20,3 @@ pub use ship::{ }; pub use sprite::*; pub use system::{System, SystemObject}; -pub use ui::*; diff --git a/crates/content/src/part/outfit.rs b/crates/content/src/part/outfit.rs index 890a06e..4d8c0d7 100644 --- a/crates/content/src/part/outfit.rs +++ b/crates/content/src/part/outfit.rs @@ -3,8 +3,8 @@ use serde::Deserialize; use std::collections::HashMap; use crate::{ - handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitHandle, OutfitSpace, - SectionEdge, + handle::SpriteHandle, resolve_edge_as_edge, Content, ContentBuildContext, EffectHandle, + OutfitHandle, OutfitSpace, SectionEdge, }; pub(crate) mod syntax { @@ -299,9 +299,10 @@ impl crate::Build for Outfit { None } else { let x = x.unwrap(); - let mut e = x - .resolve_as_edge(sprite_handle, build_context, 0.0) - .with_context(|| format!("in outfit `{}`", outfit_name))?; + let mut e = resolve_edge_as_edge(&x.val, 0.0, |x| { + sprite.get_section_handle_by_name(x) + }) + .with_context(|| format!("in outfit `{}`", outfit_name))?; match e { // Inherit duration from transition sequence SectionEdge::Top { @@ -334,9 +335,10 @@ impl crate::Build for Outfit { None } else { let x = x.unwrap(); - let mut e = x - .resolve_as_edge(sprite_handle, build_context, 0.0) - .with_context(|| format!("in outfit `{}`", outfit_name))?; + let mut e = resolve_edge_as_edge(&x.val, 0.0, |x| { + sprite.get_section_handle_by_name(x) + }) + .with_context(|| format!("in outfit `{}`", outfit_name))?; match e { // Inherit duration from transition sequence SectionEdge::Top { diff --git a/crates/content/src/part/ship.rs b/crates/content/src/part/ship.rs index 3c9c3a9..a8cdc80 100644 --- a/crates/content/src/part/ship.rs +++ b/crates/content/src/part/ship.rs @@ -16,6 +16,7 @@ pub(crate) mod syntax { #[derive(Debug, Deserialize)] pub struct Ship { pub sprite: String, + pub thumb: String, pub size: f32, pub engines: Vec, pub guns: Vec, @@ -96,6 +97,9 @@ pub struct Ship { /// This ship's sprite pub sprite: SpriteHandle, + /// This ship's thumbnail + pub thumb: SpriteHandle, + /// The size of this ship. /// Measured as unrotated height, /// in terms of game units. @@ -263,7 +267,7 @@ impl crate::Build for Ship { ct: &mut Content, ) -> Result<()> { for (ship_name, ship) in ship { - let handle = match ct.sprite_index.get(&ship.sprite) { + let sprite = match ct.sprite_index.get(&ship.sprite) { None => bail!( "In ship `{}`: sprite `{}` doesn't exist", ship_name, @@ -272,8 +276,17 @@ impl crate::Build for Ship { Some(t) => *t, }; + let thumb = match ct.sprite_index.get(&ship.thumb) { + None => bail!( + "In ship `{}`: thumbnail sprite `{}` doesn't exist", + ship_name, + ship.thumb + ), + Some(t) => *t, + }; + let size = ship.size; - let aspect = ct.get_sprite(handle).aspect; + let aspect = ct.get_sprite(sprite).aspect; let collapse = { if let Some(c) = ship.collapse { @@ -415,11 +428,12 @@ impl crate::Build for Ship { }; ct.ships.push(Self { + sprite, + thumb, aspect, collapse, damage, name: ship_name, - sprite: handle, mass: ship.mass, space: OutfitSpace::from(ship.space), angular_drag: ship.angular_drag, diff --git a/crates/content/src/part/sprite.rs b/crates/content/src/part/sprite.rs index 1043147..82d9c50 100644 --- a/crates/content/src/part/sprite.rs +++ b/crates/content/src/part/sprite.rs @@ -5,8 +5,8 @@ use std::collections::HashMap; use crate::{handle::SpriteHandle, Content, ContentBuildContext}; pub(crate) mod syntax { - use crate::{Content, ContentBuildContext, SpriteHandle}; - use anyhow::{anyhow, bail, Context, Ok, Result}; + use crate::{AnimSectionHandle, Content}; + use anyhow::{bail, Ok, Result}; use serde::Deserialize; use std::{collections::HashMap, path::PathBuf}; @@ -63,24 +63,26 @@ pub(crate) mod syntax { } impl SpriteSection { - pub fn add_to( + pub fn add_to( &self, - this_sprite: SpriteHandle, - build_context: &mut ContentBuildContext, - content: &mut Content, - ) -> Result<((u32, u32), super::SpriteSection)> { + ct: &mut Content, + get_handle: F, + ) -> Result<((u32, u32), super::SpriteSection)> + where + F: Fn(&str) -> Option, + { // Make sure all frames have the same size and add them // to the frame vector let mut dim = None; let mut frames: Vec = Vec::new(); for f in &self.frames { - let idx = match content.sprite_atlas.get_idx_by_path(f) { + let idx = match ct.sprite_atlas.get_idx_by_path(f) { Some(s) => s, None => { bail!("error: file `{}` isn't in the sprite atlas", f.display()); } }; - let img = &content.sprite_atlas.get_by_idx(idx); + let img = &ct.sprite_atlas.get_by_idx(idx); match dim { None => dim = Some(img.true_size), @@ -110,12 +112,12 @@ pub(crate) mod syntax { } let edge_top = match &self.top { - Some(x) => x.resolve_as_edge(this_sprite, build_context, frame_duration)?, + Some(x) => super::resolve_edge_as_edge(&x.val, frame_duration, &get_handle)?, None => super::SectionEdge::Stop, }; let edge_bot = match &self.bot { - Some(x) => x.resolve_as_edge(this_sprite, build_context, frame_duration)?, + Some(x) => super::resolve_edge_as_edge(&x.val, frame_duration, &get_handle)?, None => super::SectionEdge::Stop, }; @@ -137,77 +139,6 @@ pub(crate) mod syntax { pub struct SectionEdge { pub val: String, } - - impl SectionEdge { - pub fn resolve_as_start( - &self, - sprite: SpriteHandle, - build_context: &ContentBuildContext, - ) -> Result { - let e = self - .resolve_as_edge(sprite, build_context, 0.0) - .with_context(|| format!("while resolving start edge"))?; - match e { - super::SectionEdge::Bot { section, .. } => Ok(super::StartEdge::Bot { section }), - super::SectionEdge::Top { section, .. } => Ok(super::StartEdge::Top { section }), - _ => { - bail!("bad section start specification `{}`", self.val); - } - } - } - - pub fn resolve_as_edge( - &self, - sprite: SpriteHandle, - build_context: &ContentBuildContext, - duration: f32, - ) -> Result { - let all_sections = build_context.sprite_section_index.get(&sprite).unwrap(); - - if self.val == "hidden" { - return Ok(super::SectionEdge::Top { - section: crate::AnimSectionHandle::Hidden, - duration, - }); - } - - if self.val == "stop" { - return Ok(super::SectionEdge::Stop); - } - - if self.val == "reverse" { - return Ok(super::SectionEdge::Reverse { duration }); - } - - if self.val == "repeat" { - return Ok(super::SectionEdge::Repeat { duration }); - } - - let (s, p) = match self.val.split_once(":") { - Some(x) => x, - None => { - bail!("bad section edge specification `{}`", self.val); - } - }; - - let section = match all_sections.get(s) { - Some(s) => *s, - None => { - return Err(anyhow!("bad section edge specification `{}`", self.val)) - .with_context(|| format!("section `{}` doesn't exist", s)); - } - }; - - match p { - "top" => Ok(super::SectionEdge::Top { section, duration }), - "bot" => Ok(super::SectionEdge::Bot { section, duration }), - _ => { - return Err(anyhow!("bad section edge specification `{}`", self.val)) - .with_context(|| format!("invalid target `{}`", p)); - } - } - } - } } /// A handle for an animation section inside a sprite @@ -312,6 +243,9 @@ pub struct Sprite { /// This sprite's animation sections sections: Vec, + /// Allows us to get sprite sections by name + sections_by_name: HashMap, + /// Aspect ratio of this sprite (width / height) pub aspect: f32, } @@ -334,6 +268,24 @@ impl Sprite { } } + /// Get an animation section by name. + /// Returns None for invalid names + pub fn get_section_by_name(&self, name: &str) -> Option<&SpriteSection> { + match self.sections_by_name.get(name) { + None => return None, + Some(h) => Some(self.get_section(*h)), + } + } + + /// Get an animation section's handle by name. + /// Returns None for invalid names + pub fn get_section_handle_by_name(&self, name: &str) -> Option { + match self.sections_by_name.get(name) { + None => return None, + Some(h) => Some(*h), + } + } + /// Get the index of the texture of this sprite's first frame pub fn get_first_frame(&self) -> u32 { match self.start_at { @@ -374,18 +326,83 @@ pub struct SpriteSection { pub edge_bot: SectionEdge, } +/// Resolve an edge specification string as a StartEdge +pub fn resolve_edge_as_start(s: &str, get_handle: F) -> Result +where + F: Fn(&str) -> Option, +{ + let e = resolve_edge_as_edge(s, 0.0, get_handle) + .with_context(|| format!("while resolving start edge"))?; + match e { + super::SectionEdge::Bot { section, .. } => Ok(super::StartEdge::Bot { section }), + super::SectionEdge::Top { section, .. } => Ok(super::StartEdge::Top { section }), + _ => { + bail!("bad section start specification `{}`", s); + } + } +} + +/// Resolve an edge specifiation string as a SectionEdge +pub fn resolve_edge_as_edge(s: &str, duration: f32, get_handle: F) -> Result +where + F: Fn(&str) -> Option, +{ + if s == "hidden" { + return Ok(super::SectionEdge::Top { + section: crate::AnimSectionHandle::Hidden, + duration, + }); + } + + if s == "stop" { + return Ok(super::SectionEdge::Stop); + } + + if s == "reverse" { + return Ok(super::SectionEdge::Reverse { duration }); + } + + if s == "repeat" { + return Ok(super::SectionEdge::Repeat { duration }); + } + + let (s, p) = match s.split_once(":") { + Some(x) => x, + None => { + bail!("bad section edge specification `{}`", s); + } + }; + + let section = match get_handle(s) { + Some(s) => s, + None => { + return Err(anyhow!("bad section edge specification `{}`", s)) + .with_context(|| format!("section `{}` doesn't exist", s)); + } + }; + + match p { + "top" => Ok(super::SectionEdge::Top { section, duration }), + "bot" => Ok(super::SectionEdge::Bot { section, duration }), + _ => { + return Err(anyhow!("bad section edge specification `{}`", s)) + .with_context(|| format!("invalid target `{}`", p)); + } + } +} + impl crate::Build for Sprite { type InputSyntaxType = HashMap; fn build( sprites: Self::InputSyntaxType, - build_context: &mut ContentBuildContext, - content: &mut Content, + _build_context: &mut ContentBuildContext, + ct: &mut Content, ) -> Result<()> { for (sprite_name, t) in sprites { match t { syntax::Sprite::Static(t) => { - let idx = match content.sprite_atlas.get_idx_by_path(&t.file) { + let idx = match ct.sprite_atlas.get_idx_by_path(&t.file) { Some(s) => s, None => { return Err( @@ -399,19 +416,16 @@ impl crate::Build for Sprite { }); } }; - let img = &content.sprite_atlas.get_by_idx(idx); + let img = &ct.sprite_atlas.get_by_idx(idx); let aspect = img.w / img.h; let h = SpriteHandle { - index: content.sprites.len(), + index: ct.sprites.len(), }; - content.sprite_index.insert(sprite_name.clone(), h); - let mut smap = HashMap::new(); - smap.insert("anim".to_string(), AnimSectionHandle::Idx(0)); - build_context.sprite_section_index.insert(h, smap); + ct.sprite_index.insert(sprite_name.clone(), h); - content.sprites.push(Self { + ct.sprites.push(Self { name: sprite_name, start_at: StartEdge::Top { section: AnimSectionHandle::Idx(0), @@ -424,41 +438,42 @@ impl crate::Build for Sprite { edge_top: SectionEdge::Stop, edge_bot: SectionEdge::Stop, }], + sections_by_name: HashMap::new(), handle: h, aspect, }); } syntax::Sprite::OneSection(s) => { - let mut section_names: HashMap = HashMap::new(); - // Name the one section in this sprite "anim" - section_names.insert("anim".to_owned(), AnimSectionHandle::Idx(0)); - let sprite_handle = SpriteHandle { - index: content.sprites.len(), + index: ct.sprites.len(), }; - content - .sprite_index - .insert(sprite_name.clone(), sprite_handle); - let mut smap = HashMap::new(); - smap.insert("anim".to_string(), AnimSectionHandle::Idx(0)); - build_context - .sprite_section_index - .insert(sprite_handle, smap); + ct.sprite_index.insert(sprite_name.clone(), sprite_handle); let (dim, section) = s - .add_to(sprite_handle, build_context, content) + .add_to(ct, |s| { + if s == "anim" { + Some(AnimSectionHandle::Idx(0)) + } else { + None + } + }) .with_context(|| format!("while parsing sprite `{}`", sprite_name))?; let aspect = dim.0 as f32 / dim.1 as f32; let mut sections = Vec::new(); sections.push(section); - content.sprites.push(Self { + ct.sprites.push(Self { name: sprite_name, sections, start_at: StartEdge::Top { section: AnimSectionHandle::Idx(0), }, + sections_by_name: { + let mut h = HashMap::new(); + h.insert("anim".to_string(), AnimSectionHandle::Idx(0)); + h + }, handle: sprite_handle, aspect, }); @@ -471,19 +486,13 @@ impl crate::Build for Sprite { } let sprite_handle = SpriteHandle { - index: content.sprites.len(), + index: ct.sprites.len(), }; - content - .sprite_index - .insert(sprite_name.clone(), sprite_handle); - build_context - .sprite_section_index - .insert(sprite_handle, section_names.clone()); + ct.sprite_index.insert(sprite_name.clone(), sprite_handle); - let start_at = s - .start_at - .resolve_as_start(sprite_handle, build_context) - .with_context(|| format!("while loading sprite `{}`", sprite_name))?; + let start_at = + resolve_edge_as_start(&s.start_at.val, |x| section_names.get(x).copied()) + .with_context(|| format!("while loading sprite `{}`", sprite_name))?; let mut sections = Vec::with_capacity(section_names.len()); let mut dim = None; @@ -495,7 +504,7 @@ impl crate::Build for Sprite { for (k, _) in names { let v = s.section.get(k).unwrap(); let (d, s) = v - .add_to(sprite_handle, build_context, content) + .add_to(ct, |x| section_names.get(x).copied()) .with_context(|| format!("while parsing section `{}`", k)) .with_context(|| format!("while parsing sprite `{}`", sprite_name))?; @@ -515,11 +524,12 @@ impl crate::Build for Sprite { let dim = dim.unwrap(); let aspect = dim.0 as f32 / dim.1 as f32; - content.sprites.push(Self { + ct.sprites.push(Self { name: sprite_name, sections, start_at, handle: sprite_handle, + sections_by_name: section_names, aspect, }); } diff --git a/crates/content/src/part/system.rs b/crates/content/src/part/system.rs index bcfec32..550e388 100644 --- a/crates/content/src/part/system.rs +++ b/crates/content/src/part/system.rs @@ -148,7 +148,7 @@ fn resolve_coordinates( if sum.is_empty() { a.to_string() } else { - sum + " -> " + a + format!("{sum} -> {a}") } }) ); diff --git a/crates/content/src/part/ui.rs b/crates/content/src/part/ui.rs deleted file mode 100644 index 1e2991b..0000000 --- a/crates/content/src/part/ui.rs +++ /dev/null @@ -1,342 +0,0 @@ -use anyhow::{Context, Result}; -use nalgebra::{Point2, Vector2}; -use serde::Deserialize; - -use crate::{handle::SpriteHandle, Content, ContentBuildContext, SectionEdge}; - -pub(crate) mod syntax { - use crate::{sprite::syntax::SectionEdge, Content, ContentBuildContext}; - use anyhow::{bail, Context, Result}; - use nalgebra::{Point2, Vector2}; - use serde::Deserialize; - // Raw serde syntax structs. - // These are never seen by code outside this crate. - - #[derive(Debug, Deserialize)] - pub struct Ui { - pub landed: UiLanded, - pub outfitter: UiOutfitter, - } - - #[derive(Debug, Deserialize)] - pub struct UiLanded { - pub frame: UiSprite, - pub landscape: UiSprite, - pub button: UiSprite, - pub planet_name: UiText, - pub planet_desc: UiText, - } - - #[derive(Debug, Deserialize)] - pub struct UiOutfitter { - pub se_box: UiSprite, - pub exit_button: UiSprite, - } - - #[derive(Debug, Deserialize)] - pub struct UiRect { - pub pos: [f32; 2], - pub dim: [f32; 2], - pub anchor_self: super::UiPositionAnchor, - pub anchor_parent: super::UiPositionAnchor, - } - - #[derive(Debug, Deserialize)] - pub struct EdgeSpec { - pub edge: SectionEdge, - pub duration: f32, - } - - #[derive(Debug, Deserialize)] - pub struct UiText { - pub rect: UiRect, - pub font_size: f32, - pub line_height: f32, - pub align: super::UiTextAlign, - } - - impl UiText { - pub fn build( - self, - _build_context: &ContentBuildContext, - _ct: &Content, - ) -> Result { - let rect = { - super::UiRect { - pos: Point2::new(self.rect.pos[0], self.rect.pos[1]), - dim: Vector2::new(self.rect.dim[0], self.rect.dim[1]), - anchor_self: self.rect.anchor_self, - anchor_parent: self.rect.anchor_parent, - } - }; - - return Ok(super::UiTextConfig { - rect, - font_size: self.font_size, - line_height: self.line_height, - align: self.align, - }); - } - } - - #[derive(Debug, Deserialize)] - pub struct UiSprite { - pub sprite: Option, - pub rect: UiRect, - pub mask: Option, - pub on_mouse_enter: Option, - pub on_mouse_leave: Option, - } - - impl UiSprite { - pub fn build( - self, - build_context: &ContentBuildContext, - ct: &Content, - - // If true, fail if self.sprite is missing. - // If false, fail if self.sprite exists. - // This is false for sprites that may change---for example, planet landscapes - should_have_sprite: bool, - ) -> Result { - let sprite = { - if should_have_sprite { - if self.sprite.is_none() { - bail!("no sprite given, but expected a value") - } - match ct.sprite_index.get(self.sprite.as_ref().unwrap()) { - None => bail!("ui sprite `{}` doesn't exist", self.sprite.unwrap()), - Some(t) => Some(*t), - } - } else { - if self.sprite.is_some() { - bail!("got a sprite, but didn't expect one") - } - None - } - }; - - let mask = if let Some(mask) = self.mask { - Some(match ct.sprite_index.get(&mask) { - None => bail!("mask `{}` doesn't exist", mask), - Some(t) => *t, - }) - } else { - None - }; - - let on_mouse_enter = { - if let Some(x) = self.on_mouse_enter { - if sprite.is_none() { - bail!("got `on_mouse_enter` on a ui element with no fixed sprite") - } - - Some( - x.edge - .resolve_as_edge(sprite.unwrap(), build_context, x.duration) - .with_context(|| format!("failed to resolve mouse enter edge"))?, - ) - } else { - None - } - }; - - let on_mouse_leave = { - if let Some(x) = self.on_mouse_leave { - if sprite.is_none() { - bail!("got `on_mouse_leave` on a ui element with no fixed sprite") - } - - Some( - x.edge - .resolve_as_edge(sprite.unwrap(), build_context, x.duration) - .with_context(|| format!("failed to resolve mouse leave edge"))?, - ) - } else { - None - } - }; - - let rect = { - super::UiRect { - pos: Point2::new(self.rect.pos[0], self.rect.pos[1]), - dim: Vector2::new(self.rect.dim[0], self.rect.dim[1]), - anchor_self: self.rect.anchor_self, - anchor_parent: self.rect.anchor_parent, - } - }; - - return Ok(super::UiSpriteConfig { - sprite, - mask, - on_mouse_enter, - on_mouse_leave, - rect, - }); - } - } -} - -/// How to align text in a text box -#[derive(Debug, Deserialize, Clone, Copy)] -pub enum UiTextAlign { - /// Center-align - #[serde(rename = "center")] - Center, - - /// Left-align - #[serde(rename = "left")] - Left, -} - -/// How to position a UI sprite -#[derive(Debug, Deserialize, Clone, Copy)] -pub enum UiPositionAnchor { - /// Anchored at center - #[serde(rename = "center")] - Center, - - /// Anchored at top-left - #[serde(rename = "northwest")] - NorthWest, - - /// Anchored at top-right - #[serde(rename = "northeast")] - NorthEast, - - /// Anchored at bottom-left - #[serde(rename = "southwest")] - SouthWest, - - /// Anchored at bottom-right - #[serde(rename = "southeast")] - SouthEast, -} - -/// UI Configuration -#[derive(Debug, Clone)] -pub struct Ui { - /// Landed interface frame - pub landed_frame: UiSpriteConfig, - - /// Landed interface image - pub landed_landscape: UiSpriteConfig, - - /// Test button - pub landed_button: UiSpriteConfig, - - /// Landed planet name - pub landed_planet_name: UiTextConfig, - - /// Landed planet description - pub landed_planet_desc: UiTextConfig, - - /// Outfitter exit button - pub outfitter_exit_button: UiSpriteConfig, - - /// Outfitter south-east box - pub outfitter_se_box: UiSpriteConfig, -} - -/// A UI sprite's position -#[derive(Debug, Clone)] -pub struct UiRect { - /// The position of the center of this sprite, in logical pixels, - /// with 0, 0 at the center of the screen - pub pos: Point2, - - /// This sprite's w and h, in logical pixels. - pub dim: Vector2, - - /// The point on this sprite that pos is anchored to - pub anchor_self: UiPositionAnchor, - - /// The point on the parent that pos is relative to - pub anchor_parent: UiPositionAnchor, -} - -/// A single UI sprite instance -#[derive(Debug, Clone)] -pub struct UiSpriteConfig { - /// The sprite to show - pub sprite: Option, - - /// The mask to use - pub mask: Option, - - /// This sprite's position and size - pub rect: UiRect, - - /// Animation edge to take when mouse enters this sprite - pub on_mouse_enter: Option, - - /// Animation edge to take when mouse leaves this sprite - pub on_mouse_leave: Option, -} - -/// A UI text box -#[derive(Debug, Clone)] -pub struct UiTextConfig { - /// Text box location and dimensions - pub rect: UiRect, - - /// Text box font size - pub font_size: f32, - - /// Text box line height - pub line_height: f32, - - /// Text box alignment - pub align: UiTextAlign, -} - -impl crate::Build for Ui { - type InputSyntaxType = syntax::Ui; - - fn build( - ui: Self::InputSyntaxType, - build_context: &mut ContentBuildContext, - ct: &mut Content, - ) -> Result<()> { - ct.ui = Some(Ui { - landed_frame: ui - .landed - .frame - .build(build_context, ct, true) - .with_context(|| format!("in ui config (landed_frame)"))?, - landed_landscape: ui - .landed - .landscape - .build(build_context, ct, false) - .with_context(|| format!("in ui config (landed_landscape)"))?, - landed_button: ui - .landed - .button - .build(build_context, ct, true) - .with_context(|| format!("in ui config (landed_button)"))?, - landed_planet_name: ui - .landed - .planet_name - .build(build_context, ct) - .with_context(|| format!("in ui config (landed_planet_name)"))?, - landed_planet_desc: ui - .landed - .planet_desc - .build(build_context, ct) - .with_context(|| format!("in ui config (landed_planet_desc)"))?, - - outfitter_exit_button: ui - .outfitter - .exit_button - .build(build_context, ct, true) - .with_context(|| format!("in ui config (outfitter_exit_button)"))?, - outfitter_se_box: ui - .outfitter - .se_box - .build(build_context, ct, true) - .with_context(|| format!("in ui config (outfitter_se_box)"))?, - }); - - return Ok(()); - } -} diff --git a/crates/content/src/spriteautomaton.rs b/crates/content/src/spriteautomaton.rs index 87cbf82..a0ce422 100644 --- a/crates/content/src/spriteautomaton.rs +++ b/crates/content/src/spriteautomaton.rs @@ -247,7 +247,7 @@ impl SpriteAutomaton { // Edge case: we're stopped and got a request to transition. // we should transition right away. - if let Some(e) = self.next_edge_override { + if let Some(e) = self.next_edge_override.take() { self.take_edge(ct, e); }