This commit is contained in:
2026-06-23 20:07:53 -05:00
parent 2ea2ce0212
commit cbbe522fb1
5 changed files with 143 additions and 9 deletions
+11
View File
@@ -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"));
}
+15
View File
@@ -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.
+82 -8
View File
@@ -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<ScriptArg>)> = Vec::new();
let mut new_bubbles: Vec<SpeechBubble> = 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"))));
}
}
+10 -1
View File
@@ -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| {
+25
View File
@@ -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();
}