diff --git a/kiln-core/src/action.rs b/kiln-core/src/action.rs index b2a603e..b10e338 100644 --- a/kiln-core/src/action.rs +++ b/kiln-core/src/action.rs @@ -79,6 +79,12 @@ pub(crate) enum Action { /// Add `n` to the player's gem count (negative subtracts; the count is /// clamped at 0). Zero time cost. Applied to `GameState::player_gems`. AddGems(i64), + /// Give (`true`) or take (`false`) the named key color from the player. + /// + /// Color must be one of `"blue"`, `"green"`, `"cyan"`, `"red"`, `"purple"`, + /// `"orange"`, `"yellow"`, `"white"`. An unrecognized name is logged and + /// ignored. Zero time cost. Applied to `GameState::player_keys`. + SetKey(String, bool), /// Remove the source object from the board. Zero time cost. Used by grab /// things (e.g. gems) to despawn themselves from their `grab()` hook. Die, @@ -191,6 +197,11 @@ pub(crate) fn action_to_map(action: &Action) -> rhai::Map { m.insert("type".into(), ds("AddGems")); m.insert("n".into(), Dynamic::from(*n)); } + Action::SetKey(color, present) => { + m.insert("type".into(), ds("SetKey")); + m.insert("color".into(), Dynamic::from(color.clone())); + m.insert("present".into(), Dynamic::from(*present)); + } Action::Die => { m.insert("type".into(), ds("Die")); } diff --git a/kiln-core/src/archetype.rs b/kiln-core/src/archetype.rs index e590b1e..9d7b1da 100644 --- a/kiln-core/src/archetype.rs +++ b/kiln-core/src/archetype.rs @@ -110,6 +110,21 @@ builtins! { bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, script: include_str!("scripts/spinner.rhai"), }, + Key => [ + "key_red" => 12, + "key_orange" => 12, + "key_yellow" => 12, + "key_green" => 12, + "key_blue" => 12, + "key_cyan" => 12, + "key_purple" => 12, + "key_white" => 12, + ] { + behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true }, + fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 }, + bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, + script: include_str!("scripts/key.rhai"), + } } /// A class of board cell, encoding its default behavior and appearance. diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index 8d508ee..eb45622 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -18,25 +18,43 @@ pub const SAY_DURATION: f64 = 3.0; /// paired with its [`Rgba8`] color for rendering. #[derive(Copy, Clone, Default)] pub struct Keyring { - /// Blue key. - pub blue: bool, - /// Green key. - pub green: bool, - /// Cyan key. - pub cyan: bool, /// Red key. pub red: bool, - /// Purple key. - pub purple: bool, /// Orange key (custom color, not in the standard EGA palette). pub orange: bool, /// Yellow key. pub yellow: bool, + /// Green key. + pub green: bool, + /// Blue key. + pub blue: bool, + /// Cyan key. + pub cyan: bool, + /// Purple key. + pub purple: bool, /// White key. pub white: bool, } impl Keyring { + /// Sets the named key slot to `value`. Returns `false` if `name` is not a + /// recognized color (`"blue"`, `"green"`, `"cyan"`, `"red"`, `"purple"`, + /// `"orange"`, `"yellow"`, `"white"`). + pub fn set_by_name(&mut self, name: &str, value: bool) -> bool { + match name { + "blue" => self.blue = value, + "green" => self.green = value, + "cyan" => self.cyan = value, + "red" => self.red = value, + "purple" => self.purple = value, + "orange" => self.orange = value, + "yellow" => self.yellow = value, + "white" => self.white = value, + _ => return false, + } + true + } + /// Returns all eight keys in display order, each paired with its color. /// /// Order: blue, green, cyan, red, purple, orange, yellow, white. @@ -261,6 +279,7 @@ impl GameState { // Net change to the player's gem count from AddGems actions; applied to // `self` after the board borrow drops. let mut gem_delta: i64 = 0; + let mut key_changes: Vec<(String, bool)> = Vec::new(); let mut sends: Vec<(ObjectId, String, Option)> = Vec::new(); let mut new_bubbles: Vec = Vec::new(); // Collected outside the board borrow so we can assign to self.active_scroll. @@ -362,6 +381,8 @@ impl GameState { } // Accumulated and applied to `self.player_gems` after the borrow drops. Action::AddGems(n) => gem_delta += n, + // Collected and applied to `self.player_keys` after the borrow drops. + Action::SetKey(color, present) => key_changes.push((color, present)), // A grab thing despawns itself from its grab() hook. Action::Die => { board.remove_object(ba.source); @@ -383,6 +404,11 @@ impl GameState { if gem_delta != 0 { self.player_gems = (self.player_gems as i64 + gem_delta).max(0) as u32; } + for (color, present) in key_changes { + if !self.player_keys.set_by_name(&color, present) { + self.log.push(LogLine::error(format!("set_key: unknown color {color:?}"))); + } + } for (bumped, bumper) in bumps { self.scripts.run_bump(bumped, bumper); } @@ -762,4 +788,52 @@ mod tests { } assert_eq!(seq, vec![47, 0xC4, 92, 0xB3, 47]); } + + #[test] + fn set_key_gives_and_takes_keys() { + let mut sobj = ObjectDef::new(0, 0); + sobj.solid = false; + sobj.script_name = Some("s".to_string()); + let board = open_board(2, 1, (1, 0), vec![sobj]); + let scripts = HashMap::from([( + "s".to_string(), + "fn init() { set_key(\"blue\", true); set_key(\"red\", true); }".to_string(), + )]); + let mut game = GameState::with_scripts(board, scripts); + game.run_init(); + + assert!(game.player_keys.blue); + assert!(game.player_keys.red); + assert!(!game.player_keys.cyan); // cyan was not set by the script + + // A second script can take a key. + let mut sobj2 = ObjectDef::new(0, 0); + sobj2.solid = false; + sobj2.script_name = Some("t".to_string()); + let board2 = open_board(2, 1, (1, 0), vec![sobj2]); + let scripts2 = HashMap::from([( + "t".to_string(), + "fn init() { set_key(\"blue\", true); set_key(\"blue\", false); }".to_string(), + )]); + let mut game2 = GameState::with_scripts(board2, scripts2); + game2.run_init(); + + assert!(!game2.player_keys.blue); + } + + #[test] + fn set_key_unknown_color_logs_error() { + let mut sobj = ObjectDef::new(0, 0); + sobj.solid = false; + sobj.script_name = Some("s".to_string()); + let board = open_board(2, 1, (1, 0), vec![sobj]); + let scripts = HashMap::from([( + "s".to_string(), + r#"fn init() { set_key("chartreuse", true); }"#.to_string(), + )]); + let mut game = GameState::with_scripts(board, scripts); + game.run_init(); + + assert!(game.log.iter().any(|l| l.spans.iter().any(|s| s.text.contains("set_key")))); + } } diff --git a/kiln-core/src/script.rs b/kiln-core/src/script.rs index 774984c..d19bc8e 100644 --- a/kiln-core/src/script.rs +++ b/kiln-core/src/script.rs @@ -59,7 +59,7 @@ //! `move(dir)`, `delay(secs)`, `now()`, `set_tile(n)`, `log(msg)`, `say(msg)`, //! `set_fg(fg)`, `set_bg(bg)`, `set_color(fg, bg)`, `set_tag(target, tag, present)`, //! `send(target_id, fn_name [, arg])`, `teleport(x, y)`, `push(x, y, dir)`, -//! `swap([[src_x, src_y, dst_x, dst_y], …])`, `add_gems(n)`, `die()` +//! `swap([[src_x, src_y, dst_x, dst_y], …])`, `add_gems(n)`, `set_key(color, present)`, `die()` //! //! ## Queue API //! @@ -906,6 +906,15 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) { emit(&q, source_of(&ctx), Action::AddGems(n)); }); + // set_key(color, present): give (true) or take (false) the named key color. + let q = queues.clone(); + engine.register_fn( + "set_key", + move |ctx: NativeCallContext, color: ImmutableString, present: bool| { + emit(&q, source_of(&ctx), Action::SetKey(color.into(), present)); + }, + ); + // die(): remove the calling object from the board. let q = queues.clone(); engine.register_fn("die", move |ctx: NativeCallContext| { diff --git a/kiln-core/src/scripts/key.rhai b/kiln-core/src/scripts/key.rhai new file mode 100644 index 0000000..d6fba41 --- /dev/null +++ b/kiln-core/src/scripts/key.rhai @@ -0,0 +1,25 @@ +// Built-in script for the `gem` archetype (see kiln-core/src/builtin_scripts.rs). +// +// A gem is a grabbable collectible: walking onto it (or pushing it into the +// player) fires this `grab()` hook instead of blocking. We bump the player's gem +// count and remove ourselves from the board. +fn grab() { + let colors = [ + "red", + "orange", + "yellow", + "green", + "blue", + "cyan", + "purple", + "white" + ]; + + for c in colors { + if Me.has_tag(`BUILTIN_key_${c}`) { + set_key(c, true); + } + } + + die(); +}