diff --git a/kiln-core/src/api/mod.rs b/kiln-core/src/api/mod.rs index f8d5515..140c8f5 100644 --- a/kiln-core/src/api/mod.rs +++ b/kiln-core/src/api/mod.rs @@ -1,4 +1,3 @@ -pub mod state; pub mod player; pub mod board; pub mod object_info; diff --git a/kiln-core/src/api/player.rs b/kiln-core/src/api/player.rs index 9d36615..29423e3 100644 --- a/kiln-core/src/api/player.rs +++ b/kiln-core/src/api/player.rs @@ -1,21 +1,22 @@ use rhai::Engine; -use crate::player::Player; +use crate::api::board::BoardRef; +use crate::player::PlayerRef; use crate::script::Registerable; -use crate::utils::{ErrorSink, PlayerPos}; +use crate::utils::ErrorSink; /// GameState stores player state but Board stores its position, and we want one /// object to register with Rhai -#[derive(Copy, Clone)] -pub struct PlayerWithPos(pub Player, pub PlayerPos); +#[derive(Clone)] +pub struct PlayerWithPos(pub PlayerRef, pub BoardRef); impl Registerable for PlayerWithPos { fn register(engine: &mut Engine, _error_sink: ErrorSink) { engine.register_type_with_name::("Player") - .register_get("gems", |player: &mut PlayerWithPos| player.0.gems) - .register_get("health", |player: &mut PlayerWithPos| player.0.health) - .register_get("max_health", |player: &mut PlayerWithPos| player.0.max_health) - .register_get("keys", |player: &mut PlayerWithPos| player.0.keys) - .register_get("x", |player: &mut PlayerWithPos| player.1.x) - .register_get("y", |player: &mut PlayerWithPos| player.1.y); + .register_get("gems", |player: &mut PlayerWithPos| player.0.borrow().gems) + .register_get("health", |player: &mut PlayerWithPos| player.0.borrow().health) + .register_get("max_health", |player: &mut PlayerWithPos| player.0.borrow().max_health) + .register_get("keys", |player: &mut PlayerWithPos| player.0.borrow().keys) + .register_get("x", |player: &mut PlayerWithPos| player.1.borrow().player.x) + .register_get("y", |player: &mut PlayerWithPos| player.1.borrow().player.y); } } diff --git a/kiln-core/src/api/registry.rs b/kiln-core/src/api/registry.rs index 494733f..3a6b86d 100644 --- a/kiln-core/src/api/registry.rs +++ b/kiln-core/src/api/registry.rs @@ -11,7 +11,7 @@ use crate::utils::{ErrorSink, RegistryValue}; pub struct Registry(pub BoardRef); impl Registerable for Registry { - fn register(engine: &mut Engine, error_sink: ErrorSink) { + fn register(engine: &mut Engine, _error_sink: ErrorSink) { engine.register_type_with_name::("Registry"); // Registry.get(key) -> Dynamic — returns () if the key is absent. diff --git a/kiln-core/src/api/state.rs b/kiln-core/src/api/state.rs deleted file mode 100644 index 8011121..0000000 --- a/kiln-core/src/api/state.rs +++ /dev/null @@ -1,32 +0,0 @@ -use rhai::Engine; -use crate::api::board::BoardRef; -use crate::api::player::PlayerWithPos; -use crate::game::GameState; -use crate::script::Registerable; -use crate::utils::ErrorSink; - -/// The host-provided context handed to every script hook for the duration of one -/// call. Currently just the player snapshot, but it exists so more host state can -/// be threaded through the `run_*` methods without changing each signature again. -#[derive(Clone)] -pub struct ScriptState { - pub player: PlayerWithPos, - pub board: BoardRef, -} - -impl ScriptState { - pub fn from_game_state(game_state: &GameState) -> Self { - Self { - player: PlayerWithPos(game_state.player, game_state.board().player), - board: game_state.board_rc(), - } - } -} - -impl Registerable for ScriptState { - fn register(engine: &mut Engine, _error_sink: ErrorSink) { - engine.register_type_with_name::("State") - .register_get("player", |state: &mut ScriptState| state.player) - .register_get("board", |state: &mut ScriptState| state.board.clone()); - } -} diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index 6a98a92..54c3405 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -13,8 +13,7 @@ pub const SAY_DURATION: f64 = 3.0; // Re-export ScrollLine so kiln-tui can pattern-match scroll content without // accessing the private `action` module directly. pub use crate::action::ScrollLine; -use crate::api::state::ScriptState; -use crate::player::Player; +use crate::player::{Player, PlayerRef}; /// An active scroll overlay opened by a scripted object via `scroll()`. /// @@ -72,7 +71,7 @@ pub struct GameState { /// per-board: it persists across board transitions, unlike the per-board /// position in [`Board::player`](crate::board::Board::player). Scripts mutate /// it via `add_gems`/`alter_health`/`set_key` and read a snapshot of it. - pub player: Player, + pub player: PlayerRef, } impl GameState { @@ -84,11 +83,13 @@ impl GameState { /// Compile errors are surfaced into the log immediately. pub fn from_world(world: World) -> Self { let name = world.start.clone(); + let player = Player::new_ref(); let host = ScriptHost::new( world .boards .get(&name) - .expect("world::load guarantees start board exists"), + .expect("world::load guarantees start board exists").clone(), + player.clone(), &world.scripts, ); let mut state = Self { @@ -99,7 +100,7 @@ impl GameState { speech_bubbles: Vec::new(), active_scroll: None, board_transition: None, - player: Player::default() + player }; state.drain_errors(); state @@ -163,7 +164,7 @@ impl GameState { /// the game is about to start — never during map deserialization, since a script /// may inspect the board. pub fn run_init(&mut self) { - self.scripts.run_init(ScriptState::from_game_state(self)); + self.scripts.run_init(); self.resolve(); } @@ -177,7 +178,7 @@ impl GameState { // this runs exactly once per player interaction with a scroll. self.handle_scroll(); let secs = dt.as_secs_f64(); - self.scripts.run_tick(ScriptState::from_game_state(self), secs); + self.scripts.run_tick(secs); // Expire speech bubbles before resolving new actions so a fresh say() // this frame isn't immediately culled. self.speech_bubbles.retain_mut(|b| { @@ -338,23 +339,22 @@ impl GameState { } // Apply the net gem change (clamped at 0, since the count is unsigned). if gem_delta != 0 { - self.player.alter_gems(gem_delta); + self.player.borrow_mut().alter_gems(gem_delta); } // Apply the net health change (clamped to [0, max_health]). if health_delta != 0 { - self.player.alter_health(health_delta); + self.player.borrow_mut().alter_health(health_delta); } for (color, present) in key_changes { - if !self.player.keys.set_by_name(&color, present) { + if !self.player.borrow_mut().keys.set_by_name(&color, present) { self.log.push(LogLine::error(format!("set_key: unknown color {color:?}"))); } } - let state = ScriptState::from_game_state(self); for (bumped, bumper) in bumps { - self.scripts.run_bump(state.clone(), bumped, bumper); + self.scripts.run_bump(bumped, bumper); } for (target, fn_name, arg) in sends { - self.scripts.run_send(state.clone(), target, &fn_name, arg); + self.scripts.run_send(target, &fn_name, arg); } self.drain_errors(); } @@ -370,7 +370,7 @@ impl GameState { if let Some(scroll) = self.active_scroll.take() && let Some(choice) = scroll.choice { - self.scripts.run_send(ScriptState::from_game_state(self), scroll.source, &choice, SendArg::None); + self.scripts.run_send(scroll.source, &choice, SendArg::None); self.drain_errors(); } } @@ -416,7 +416,8 @@ impl GameState { self.board_mut().clear_all_queues(); // Rebuild the script host for the new board's objects. self.scripts = ScriptHost::new( - &self.world.boards[&self.current_board_name], + self.world.boards[&self.current_board_name].clone(), + self.player.clone(), &self.world.scripts, ); // Stub hook for the front-end transition animation (1 second). @@ -477,13 +478,12 @@ impl GameState { } // Fire the grab hook and resolve it immediately so the grabbed thing's // die()/add_gems() apply now — no player+object overlap survives this call. - let state = ScriptState::from_game_state(self); if let Some(id) = grabbed { - self.scripts.run_grab(state.clone(), id); + self.scripts.run_grab(id); self.resolve(); } if let Some(idx) = bumped { - self.scripts.run_bump(state, idx, -1); + self.scripts.run_bump(idx, -1); self.drain_errors(); } } @@ -539,7 +539,7 @@ mod tests { game.try_move(Direction::East); // The gem was grabbed: gem count up, gem object gone, player on its cell. - assert_eq!(game.player.gems, 1); + assert_eq!(game.player.borrow().gems, 1); assert!(game.board().objects.is_empty()); assert_eq!((game.board().player.x, game.board().player.y), (1, 0)); } @@ -567,7 +567,7 @@ mod tests { game.tick(Duration::from_millis(16)); // No grab: the gem is untouched and the player never moved. - assert_eq!(game.player.gems, 0); + assert_eq!(game.player.borrow().gems, 0); assert!(game.board().objects.values().any(|o| o.grab)); assert_eq!((game.board().player.x, game.board().player.y), (2, 0)); } @@ -591,7 +591,7 @@ mod tests { // crate moves to (3,0), empty moves back to (2,0). let mut game = game_with_object_script( 5, - "fn tick(m,s,dt) { if m.queue.length == 0 { shift([[2, 0], [3, 0]]); } }", + "fn tick(m,dt) { if m.queue.length == 0 { shift([[2, 0], [3, 0]]); } }", ); game.tick(Duration::from_millis(16)); let b = game.board(); @@ -609,10 +609,10 @@ mod tests { obj.script_name = Some("s".to_string()); let mut board = open_board(4, 1, (3, 0), vec![obj]); crate_at(&mut board, 2, 0); - let src = "fn init(m,s) { \ - log(if s.board.passable(1, 0) { \"empty:yes\" } else { \"empty:no\" }); \ - log(if s.board.passable(2, 0) { \"crate:yes\" } else { \"crate:no\" }); \ - log(if s.board.passable(9, 0) { \"off:yes\" } else { \"off:no\" }); }"; + let src = "fn init(m) { \ + log(if Board.passable(1, 0) { \"empty:yes\" } else { \"empty:no\" }); \ + log(if Board.passable(2, 0) { \"crate:yes\" } else { \"crate:no\" }); \ + log(if Board.passable(9, 0) { \"off:yes\" } else { \"off:no\" }); }"; let scripts = HashMap::from([("s".to_string(), src.to_string())]); let mut game = GameState::with_scripts(board, scripts); game.run_init(); @@ -727,14 +727,15 @@ mod tests { let board = open_board(2, 1, (1, 0), vec![sobj]); let scripts = HashMap::from([( "s".to_string(), - "fn init(m, s) { set_key(\"blue\", true); set_key(\"red\", true); }".to_string(), + "fn init(m) { 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 + let keys = game.player.borrow().keys; + assert!(keys.blue); + assert!(keys.red); + assert!(!keys.cyan); // cyan was not set by the script // A second script can take a key. let mut sobj2 = ObjectDef::new(0, 0); @@ -743,12 +744,12 @@ mod tests { let board2 = open_board(2, 1, (1, 0), vec![sobj2]); let scripts2 = HashMap::from([( "t".to_string(), - "fn init(m, s) { set_key(\"blue\", true); set_key(\"blue\", false); }".to_string(), + "fn init(m) { 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); + assert!(!game2.player.borrow().keys.blue); } #[test] @@ -759,7 +760,7 @@ mod tests { let board = open_board(2, 1, (1, 0), vec![sobj]); let scripts = HashMap::from([( "s".to_string(), - r#"fn init(m,s) { set_key("chartreuse", true); }"#.to_string(), + r#"fn init(m) { set_key("chartreuse", true); }"#.to_string(), )]); let mut game = GameState::with_scripts(board, scripts); game.run_init(); diff --git a/kiln-core/src/player.rs b/kiln-core/src/player.rs index 66e725e..2fca41a 100644 --- a/kiln-core/src/player.rs +++ b/kiln-core/src/player.rs @@ -1,3 +1,5 @@ +use std::cell::RefCell; +use std::rc::Rc; use crate::keys::Keyring; /// The game-global player state: stats that follow the player across boards. @@ -26,7 +28,14 @@ pub struct Player { pub gems: i64, } +pub type PlayerRef = Rc>; + impl Player { + /// Create a new PlayerRef from a default player + pub fn new_ref() -> PlayerRef { + Rc::new(RefCell::from(Player::default())) + } + /// Attempt to modify the gem total by the given amount, but maintain a minimum of zero. /// If the modification would take up below zero, leave it alone and return false. pub fn alter_gems(&mut self, delta: i64) -> bool { diff --git a/kiln-core/src/script.rs b/kiln-core/src/script.rs index 8a20a31..d56acd8 100644 --- a/kiln-core/src/script.rs +++ b/kiln-core/src/script.rs @@ -35,7 +35,7 @@ use crate::game::SAY_DURATION; use crate::log::LogLine; use crate::map_file::parse_color; use crate::object_def::ObjectDef; -use crate::utils::{Direction, ErrorSink, Hook, ObjectId, RegistryValue}; +use crate::utils::{Direction, ErrorSink, Hook, ObjectId}; use rhai::{ Array, CallFnOptions, Dynamic, Engine, ImmutableString, Module, NativeCallContext, Scope, AST, @@ -48,9 +48,9 @@ use crate::api::object_info::ObjectInfo; use crate::api::player::PlayerWithPos; use crate::api::queue::ObjQueue; use crate::api::registry::Registry; -use crate::api::state::ScriptState; use crate::glyph::Glyph; use crate::keys::Keyring; +use crate::player::PlayerRef; /// Types which can be registered to be sent to Rhai pub trait Registerable { @@ -100,6 +100,7 @@ pub struct ScriptHost { scopes: HashMap>, board_queue: BoardQueue, errors: ErrorSink, + board: BoardRef } impl ScriptHost { @@ -110,7 +111,7 @@ impl ScriptHost { /// /// `scripts` is the world-level script pool (script name → Rhai source); it is /// read only during construction and not retained afterward. - pub fn new(board_cell: &BoardRef, script_sources: &HashMap) -> Self { + pub fn new(board_ref: BoardRef, player: PlayerRef, script_sources: &HashMap) -> Self { let board_queue: BoardQueue = Rc::new(RefCell::new(Vec::new())); let errors = ErrorSink::new(); let mut scopes = HashMap::new(); @@ -120,16 +121,15 @@ impl ScriptHost { PlayerWithPos::register(&mut engine, errors.clone()); Keyring::register(&mut engine, errors.clone()); BoardRef::register(&mut engine, errors.clone()); - ScriptState::register(&mut engine, errors.clone()); ObjectInfo::register(&mut engine, errors.clone()); Glyph::register(&mut engine, errors.clone()); ObjQueue::register(&mut engine, errors.clone()); Registry::register(&mut engine, errors.clone()); - register_write_api(&mut engine, board_cell.clone()); - register_global_constants(&mut engine, board_cell.clone()); + register_write_api(&mut engine, board_ref.clone()); + register_global_constants(&mut engine, board_ref.clone(), player.clone()); - let board = board_cell.borrow(); + let board = board_ref.borrow(); // Compile each referenced script once, keyed by `script_key` (the object's // `script_name`: a world-pool name for named scripts, or a synthetic @@ -166,10 +166,10 @@ impl ScriptHost { scripts.insert( key, CompiledScript { - has_init: defines("init", 2), - has_tick: defines("tick", 3), - has_bump: defines("bump", 3), - has_grab: defines("grab", 2), + has_init: defines("init", 1), + has_tick: defines("tick", 2), + has_bump: defines("bump", 2), + has_grab: defines("grab", 1), ast, }, ); @@ -199,43 +199,41 @@ impl ScriptHost { board_queue, errors, scopes, + board: board_ref } } /// Calls `tick(dt)` on every scripted object that defines it, then drains queues. - pub fn run_tick(&mut self, state: ScriptState, dt: f64) { - self.run_hook_on_all(Hook::Tick, state, dt); + pub fn run_tick(&mut self, dt: f64) { + self.run_hook_on_all(Hook::Tick, dt); } - pub fn run_init(&mut self, state: ScriptState) { - self.run_hook_on_all(Hook::Init, state, 0.0) + pub fn run_init(&mut self) { + self.run_hook_on_all(Hook::Init, 0.0) } /// Run the given hook on every object that defines it. For hooks other than /// `Tick`, dt should just be 0.0 - fn run_hook_on_all(&mut self, hook: Hook, state: ScriptState, dt: f64) { - let all_ids = state.board.borrow().all_ids(); + fn run_hook_on_all(&mut self, hook: Hook, dt: f64) { + let all_ids = self.board.borrow().all_ids(); let arg = match hook { Hook::Tick => Some(Dynamic::from(dt)), _ => None, }; for id in all_ids { - self.run_hook_on_one(hook, state.clone(), id, arg.clone(), dt) + self.run_hook_on_one(hook, id, arg.clone(), dt) } } - fn run_hook_on_one(&mut self, hook: Hook, state: ScriptState, id: ObjectId, arg: Option, dt: f64) { - if let Some(mut info) = ObjectInfo::from_id(id, state.board.clone()) { + fn run_hook_on_one(&mut self, hook: Hook, id: ObjectId, arg: Option, dt: f64) { + if let Some(mut info) = ObjectInfo::from_id(id, self.board.clone()) { if let Some(script_key) = info.script_name.as_ref() && let Some(script) = self.scripts.get(script_key) && let Some(scope) = self.scopes.get_mut(&id) { if script.has(hook) { - let mut args = vec![ - Dynamic::from(info.clone()), - Dynamic::from(state.clone()) - ]; + let mut args = vec![Dynamic::from(info.clone())]; if let Some(d) = arg { args.push(d) } // Call this with opts tagging this call as our id. The write API will read @@ -261,8 +259,8 @@ impl ScriptHost { /// Calls `bump(id)` on the object with [`ObjectId`] `object_id`, if it defines /// the hook. After the hook, drains the object's queue with `dt = 0`. - pub fn run_bump(&mut self, state: ScriptState, id: ObjectId, bumper: i64) { - self.run_hook_on_one(Hook::Bump, state, id, Some(Dynamic::from(bumper)), 0.0); + pub fn run_bump(&mut self, id: ObjectId, bumper: i64) { + self.run_hook_on_one(Hook::Bump, id, Some(Dynamic::from(bumper)), 0.0); } /// Calls `grab()` on the object with [`ObjectId`] `object_id`, if it defines the @@ -271,21 +269,20 @@ impl ScriptHost { /// Fired when the player walks onto a grab object or a grab object is pushed /// into the player (see [`GameState`](crate::game::GameState)). The hook /// typically increments a player stat and removes the object via `die()`. - pub fn run_grab(&mut self, state: ScriptState, object_id: ObjectId) { - self.run_hook_on_one(Hook::Grab, state, object_id, None, 0.0); + pub fn run_grab(&mut self, object_id: ObjectId) { + self.run_hook_on_one(Hook::Grab, object_id, None, 0.0); } /// Calls the named function on the object with [`ObjectId`] `target_id`. /// /// What we pass depends on arity, in this order: - /// - If it has arity 3, we pass `[ObjectInfo, ScriptState, SendArg]` - /// - If it has arity 2, we pass `[ObjectInfo, ScriptState]` - /// - If it has arity 1, we pass the arg + /// - If it has arity 2, we pass `[ObjectInfo, arg]` + /// - If it has arity 1, we pass the ObjectInfo alone /// - If it has arity 0, we pass nothing /// /// In cases where the arg isn't provided but we have the arity, we pass `Dynamic::UNIT` - pub(crate) fn run_send(&mut self, state: ScriptState, id: ObjectId, fn_name: &str, arg: SendArg) { - if let Some(mut info) = ObjectInfo::from_id(id, state.board.clone()) { + pub(crate) fn run_send(&mut self, id: ObjectId, fn_name: &str, arg: SendArg) { + if let Some(mut info) = ObjectInfo::from_id(id, self.board.clone()) { if let Some(script_key) = info.script_name.as_ref() && let Some(script) = self.scripts.get(script_key) && let Some(scope) = self.scopes.get_mut(&id) { @@ -307,15 +304,11 @@ impl ScriptHost { // Assemble the args let mut args = vec![]; - if arities.contains(&3) { + if arities.contains(&2) { args.push(Dynamic::from(info.clone())); - args.push(Dynamic::from(state.clone())); args.push(arg.into()); - } else if arities.contains(&2) { - args.push(Dynamic::from(info.clone())); - args.push(Dynamic::from(state.clone())); } else if arities.contains(&1) { - args.push(arg.into()); + args.push(Dynamic::from(info.clone())); } // Call this with opts tagging this call as our id. The write API will read @@ -614,9 +607,10 @@ fn read_coord_array(arr: &Array) -> Result, ()> { /// Registers direction and color constants as a global Rhai module so they are /// visible to every function at any call depth, including Rhai-to-Rhai calls. /// (Scope-level constants are only visible to the top-level function Rust calls.) -fn register_global_constants(engine: &mut Engine, board: BoardRef) { +fn register_global_constants(engine: &mut Engine, board: BoardRef, player: PlayerRef) { let mut m = Module::new(); - m.set_var("Board", board); + m.set_var("Board", board.clone()); + m.set_var("Player", PlayerWithPos(player.clone(), board)); m.set_var("North", Direction::North); m.set_var("South", Direction::South); m.set_var("East", Direction::East); diff --git a/kiln-core/src/scripts/gem.rhai b/kiln-core/src/scripts/gem.rhai index 0d78b1c..be33f7b 100644 --- a/kiln-core/src/scripts/gem.rhai +++ b/kiln-core/src/scripts/gem.rhai @@ -3,7 +3,7 @@ // 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(me, state) { +fn grab(me) { add_gems(1); die(); } diff --git a/kiln-core/src/scripts/heart.rhai b/kiln-core/src/scripts/heart.rhai index bb1a666..7d951e1 100644 --- a/kiln-core/src/scripts/heart.rhai +++ b/kiln-core/src/scripts/heart.rhai @@ -2,7 +2,7 @@ // // A heart is a grabbable collectible: walking onto it fires `grab()` instead // of blocking. It restores 1 health and removes itself from the board. -fn grab(me, state) { +fn grab(me) { alter_health(1); die(); } diff --git a/kiln-core/src/scripts/key.rhai b/kiln-core/src/scripts/key.rhai index 99f2d9b..e0e2862 100644 --- a/kiln-core/src/scripts/key.rhai +++ b/kiln-core/src/scripts/key.rhai @@ -3,7 +3,7 @@ // 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(me, state) { +fn grab(me) { let colors = [ "red", "orange", diff --git a/kiln-core/src/scripts/pusher.rhai b/kiln-core/src/scripts/pusher.rhai index a5515c6..9752179 100644 --- a/kiln-core/src/scripts/pusher.rhai +++ b/kiln-core/src/scripts/pusher.rhai @@ -6,7 +6,7 @@ // pusher shares one compiled copy and learns its direction from the // `BUILTIN_pusher_` tag the map loader attached to it. -fn tick(me, state, dt) { +fn tick(me, dt) { // Only queue a step when idle. `move` shoves the chain ahead via step_object // and is a no-op when blocked; the delay pads each step out to ~0.5s (move // itself costs 0.25s), matching the old global pusher heartbeat. diff --git a/kiln-core/src/scripts/spinner.rhai b/kiln-core/src/scripts/spinner.rhai index b6a13d1..5e4f36d 100644 --- a/kiln-core/src/scripts/spinner.rhai +++ b/kiln-core/src/scripts/spinner.rhai @@ -5,7 +5,7 @@ // engine and the script just hands it the ring. The spin direction comes from the // `BUILTIN_spinner_*` tag the map loader attaches (it defaults to clockwise when no // tag is present). -fn tick(me, state, dt) { +fn tick(me, dt) { // Only start a new rotation when the previous one (and its delay) has drained, // exactly like the built-in pusher's pacing. if me.waiting { return; } diff --git a/kiln-core/src/tests/actions.rs b/kiln-core/src/tests/actions.rs index cfb3514..3a41e1b 100644 --- a/kiln-core/src/tests/actions.rs +++ b/kiln-core/src/tests/actions.rs @@ -8,7 +8,7 @@ use std::time::Duration; fn move_command_relocates_the_source_object() { let board = open_board(5, 3, (0, 0), vec![scripted_object(2, 1, "m")]); let mut game = - GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")])); + GameState::with_scripts(board, scripts_from(&[("m", "fn init(m) { move(East); }")])); game.run_init(); let b = game.board(); // East increments x by one; the object started at (2, 1). @@ -20,7 +20,7 @@ fn move_into_a_wall_or_edge_is_a_noop() { // Object at the west edge moving west: out of bounds, ignored. let board = open_board(5, 3, (0, 0), vec![scripted_object(0, 1, "m")]); let mut game = - GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(West); }")])); + GameState::with_scripts(board, scripts_from(&[("m", "fn init(m) { move(West); }")])); game.run_init(); assert_eq!(game.board().objects[&1].x, 0); } @@ -29,7 +29,7 @@ fn move_into_a_wall_or_edge_is_a_noop() { fn set_tile_command_changes_the_source_glyph() { let board = open_board(5, 3, (0, 0), vec![scripted_object(2, 1, "s")]); let mut game = - GameState::with_scripts(board, scripts_from(&[("s", "fn init(m,s) { set_tile(7); }")])); + GameState::with_scripts(board, scripts_from(&[("s", "fn init(m) { set_tile(7); }")])); game.run_init(); assert_eq!(game.board().objects[&1].glyph.tile, 7); } @@ -40,7 +40,7 @@ fn object_pushes_crate_on_init() { let mut board = open_board(4, 1, (0, 0), vec![scripted_object(1, 0, "m")]); crate_at(&mut board, 2, 0); let mut game = - GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")])); + GameState::with_scripts(board, scripts_from(&[("m", "fn init(m) { move(East); }")])); game.run_init(); let b = game.board(); assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); @@ -53,7 +53,7 @@ fn object_push_into_player() { // A scripted object moving into the player pushes the player when there's room. let board = open_board(5, 1, (2, 0), vec![scripted_object(1, 0, "m")]); let mut game = - GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")])); + GameState::with_scripts(board, scripts_from(&[("m", "fn init(m) { move(East); }")])); game.run_init(); { let b = game.board(); @@ -65,7 +65,7 @@ fn object_push_into_player() { let mut board = open_board(4, 1, (2, 0), vec![scripted_object(1, 0, "m")]); wall_at(&mut board, 3, 0); let mut game = - GameState::with_scripts(board, scripts_from(&[("m", "fn init(m,s) { move(East); }")])); + GameState::with_scripts(board, scripts_from(&[("m", "fn init(m) { move(East); }")])); game.run_init(); let b = game.board(); assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0)); @@ -79,7 +79,7 @@ fn move_cost_rate_limits_repeated_moves() { let board = open_board(5, 1, (0, 0), vec![scripted_object(1, 0, "m")]); let mut game = GameState::with_scripts( board, - scripts_from(&[("m", "fn init(m,s) { move(East); move(East); }")]), + scripts_from(&[("m", "fn init(m) { move(East); move(East); }")]), ); game.run_init(); assert_eq!(game.board().objects[&1].x, 2); // first move applied (1 -> 2) @@ -102,7 +102,7 @@ fn inline_delay_paces_subsequent_moves() { wall_at(&mut board, 2, 1); let mut game = GameState::with_scripts( board, - scripts_from(&[("m", "fn init(m,s) { move(East); move(South); }")]), + scripts_from(&[("m", "fn init(m) { move(East); move(South); }")]), ); game.run_init(); // First (eastward) move is blocked by the wall: object hasn't moved. @@ -136,7 +136,7 @@ fn queue_length_reports_pending_actions() { board, scripts_from(&[( "q", - "fn init(m,s) { set_tile(5); set_tile(6); log(`len=${m.queue.length}`); }", + "fn init(m) { set_tile(5); set_tile(6); log(`len=${m.queue.length}`); }", )]), ); game.run_init(); @@ -166,7 +166,7 @@ fn blocked_reports_solid_and_clear() { board, scripts_from(&[( "b", - "fn init(m, s) { if m.blocked(East) { set_tile(9); } else { set_tile(7); } }", + "fn init(m) { if m.blocked(East) { set_tile(9); } else { set_tile(7); } }", )]), ); game.run_init(); @@ -178,7 +178,7 @@ fn blocked_reports_solid_and_clear() { board, scripts_from(&[( "b", - "fn init(m, s) { if m.blocked(East) { set_tile(9); } else { set_tile(7); } }", + "fn init(m) { if m.blocked(East) { set_tile(9); } else { set_tile(7); } }", )]), ); game.run_init(); diff --git a/kiln-core/src/tests/collision.rs b/kiln-core/src/tests/collision.rs index 3a1c24c..20de094 100644 --- a/kiln-core/src/tests/collision.rs +++ b/kiln-core/src/tests/collision.rs @@ -18,11 +18,11 @@ fn collision_priority_resolves_in_array_order_and_bumps() { scripts_from(&[ ( "e", - "fn init(m,s) { move(East); } fn bump(m,s,id) { log(`o0 by ${id}`); }", + "fn init(m) { move(East); } fn bump(m,id) { log(`o0 by ${id}`); }", ), ( "w", - "fn init(m,s) { move(West); } fn bump(m,s,id) { log(`o1 by ${id}`); }", + "fn init(m) { move(West); } fn bump(m,id) { log(`o1 by ${id}`); }", ), ]), ); diff --git a/kiln-core/src/tests/scripting.rs b/kiln-core/src/tests/scripting.rs index 41ee1c3..72f3443 100644 --- a/kiln-core/src/tests/scripting.rs +++ b/kiln-core/src/tests/scripting.rs @@ -8,7 +8,7 @@ use std::time::Duration; fn init_runs_only_on_run_init_not_at_construction() { let (board, scripts) = board_with_object( Some("greet"), - &[("greet", r#"fn init(m,s) { log("hello"); }"#)], + &[("greet", r#"fn init(m) { log("hello"); }"#)], ); let mut game = GameState::with_scripts(board, scripts); // init must not fire during construction / deserialization. @@ -22,7 +22,7 @@ fn init_runs_only_on_run_init_not_at_construction() { fn tick_calls_script_tick_with_elapsed_seconds() { let (board, scripts) = board_with_object( Some("t"), - &[("t", r#"fn tick(m,s,dt) { log(dt.to_string()); }"#)], + &[("t", r#"fn tick(m,dt) { log(dt.to_string()); }"#)], ); let mut game = GameState::with_scripts(board, scripts); game.run_init(); @@ -69,7 +69,7 @@ fn script_reads_board_through_view() { let board = open_board(5, 3, (3, 1), vec![scripted_object(2, 1, "r")]); let mut game = GameState::with_scripts( board, - scripts_from(&[("r", "fn init(m,s) { log(s.player.x.to_string()); }")]), + scripts_from(&[("r", "fn init(m) { log(Player.x.to_string()); }")]), ); game.run_init(); assert_eq!(log_texts(&game), vec!["3"]); @@ -88,8 +88,8 @@ fn commands_are_routed_to_their_own_source_object() { let mut game = GameState::with_scripts( board, scripts_from(&[ - ("e", "fn init(m,s) { move(East); }"), - ("w", "fn init(m,s) { move(West); }"), + ("e", "fn init(m) { move(East); }"), + ("w", "fn init(m) { move(West); }"), ]), ); game.run_init(); @@ -127,7 +127,7 @@ fn set_tag_adds_and_removes_via_my_id() { // present on the object afterward. let (board, scripts) = board_with_object( Some("t"), - &[("t", r#"fn init(m,s) { set_tag(m.id, "active", true); }"#)], + &[("t", r#"fn init(m) { set_tag(m.id, "active", true); }"#)], ); let mut game = GameState::with_scripts(board, scripts); game.run_init(); @@ -136,7 +136,7 @@ fn set_tag_adds_and_removes_via_my_id() { // A script removes a pre-existing tag. let (mut board2, scripts2) = board_with_object( Some("t2"), - &[("t2", r#"fn init(m,s) { set_tag(m.id, "active", false); }"#)], + &[("t2", r#"fn init(m) { set_tag(m.id, "active", false); }"#)], ); // Seed the tag before construction. board2 @@ -158,8 +158,8 @@ fn has_tag_reads_own_tags() { Some("t"), &[( "t", - r#"fn init(m,s) { set_tag(m.id, "active", true); } - fn tick(m,s,dt) { log(m.has_tag("active").to_string()); }"#, + r#"fn init(m) { set_tag(m.id, "active", true); } + fn tick(m,dt) { log(m.has_tag("active").to_string()); }"#, )], ); let mut game = GameState::with_scripts(board, scripts); @@ -182,8 +182,8 @@ fn objects_with_tag_returns_matching_ids() { scripts_from(&[ ( "q", - r#"fn init(m,s) { - let infos = s.board.tagged("enemy"); + r#"fn init(m) { + let infos = Board.tagged("enemy"); log(infos.len().to_string()); log(infos[0].id.to_string()); }"#, @@ -201,7 +201,7 @@ fn objects_with_tag_returns_matching_ids() { fn my_name_returns_name_or_empty_string() { // An object with a name set on its ObjectDef should see it via my_name(). let (mut board, scripts) = - board_with_object(Some("n"), &[("n", r#"fn init(m,s) { log(m.name); }"#)]); + board_with_object(Some("n"), &[("n", r#"fn init(m) { log(m.name); }"#)]); board.objects.get_mut(&1).unwrap().name = Some("beacon".to_string()); let mut game = GameState::with_scripts(board, scripts); game.run_init(); @@ -209,7 +209,7 @@ fn my_name_returns_name_or_empty_string() { // An unnamed object should get (). let (board2, scripts2) = - board_with_object(Some("n"), &[("n", r#"fn init(m,s) { if m.name == () { log("null"); }}"#)]); + board_with_object(Some("n"), &[("n", r#"fn init(m) { if m.name == () { log("null"); }}"#)]); let mut game2 = GameState::with_scripts(board2, scripts2); game2.run_init(); assert_eq!(log_texts(&game2), vec!["null"]); @@ -228,9 +228,9 @@ fn object_id_for_name_finds_by_name() { scripts_from(&[ ( "q", - r#"fn init(m,s) { - log(s.board.named("target").id.to_string()); - let miss = s.board.named("missing"); + r#"fn init(m) { + log(Board.named("target").id.to_string()); + let miss = Board.named("missing"); log(if miss == () { "not_found" } else { miss.id.to_string() }); }"#, ), @@ -251,7 +251,7 @@ fn player_bump_fires_with_negative_one() { let board = open_board(3, 1, (0, 0), vec![scripted_object(1, 0, "b")]); let mut game = GameState::with_scripts( board, - scripts_from(&[("b", "fn bump(m,s,id) { log(`bumped by ${id}`); }")]), + scripts_from(&[("b", "fn bump(m,id) { log(`bumped by ${id}`); }")]), ); game.run_init(); game.try_move(Direction::East); @@ -271,7 +271,7 @@ fn scroll_opens_on_player_bump() { board, scripts_from(&[( "s", - r#"fn bump(m,s,id) { scroll(["Hello world", ["eat", "Eat it"]]); }"#, + r#"fn bump(m,id) { scroll(["Hello world", ["eat", "Eat it"]]); }"#, )]), ); game.run_init(); @@ -295,7 +295,7 @@ fn handle_scroll_without_choice_clears_it() { let board = open_board(3, 1, (0, 0), vec![scripted_object(1, 0, "s")]); let mut game = GameState::with_scripts( board, - scripts_from(&[("s", r#"fn bump(m,s,id) { scroll(["Hello"]); }"#)]), + scripts_from(&[("s", r#"fn bump(m,id) { scroll(["Hello"]); }"#)]), ); game.run_init(); game.try_move(Direction::East); @@ -317,7 +317,7 @@ fn handle_scroll_with_choice_dispatches_send_to_source() { scripts_from(&[( "s", r#" - fn bump(m,s,id) { scroll(["Muffin?", ["eat", "Eat it"]]); } + fn bump(m,id) { scroll(["Muffin?", ["eat", "Eat it"]]); } fn eat() { log("eaten"); } "#, )]), diff --git a/kiln-tui/src/main.rs b/kiln-tui/src/main.rs index 403857a..470ca20 100644 --- a/kiln-tui/src/main.rs +++ b/kiln-tui/src/main.rs @@ -342,7 +342,7 @@ fn draw_play(frame: &mut Frame, game: &GameState, ui: &mut Ui) { .spacing(Spacing::Overlap(1)) .areas(frame.area()); frame.render_widget( - StatusSidebarWidget(game.player), + StatusSidebarWidget(game.player.clone()), sidebar_area, ); rest diff --git a/kiln-tui/src/status.rs b/kiln-tui/src/status.rs index 552df1a..523b6da 100644 --- a/kiln-tui/src/status.rs +++ b/kiln-tui/src/status.rs @@ -15,7 +15,7 @@ use ratatui::style::{Color, Style}; use ratatui::symbols::merge::MergeStrategy; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph, Widget}; -use kiln_core::player::Player; +use kiln_core::player::PlayerRef; /// A filled heart: bright red. const HEART_FULL: Color = Color::Rgb(255, 0, 0); @@ -27,11 +27,11 @@ const HEART_EMPTY: Color = Color::Rgb(90, 0, 0); /// Shows a `Health:` label above a row of [`MAX_HEARTS`] heart glyphs, a /// `Gems: ♦ N` line, and a `Keys:` row of 8 `♀` glyphs colored when held /// and dark gray when absent. -pub struct StatusSidebarWidget(pub Player); +pub struct StatusSidebarWidget(pub PlayerRef); impl Widget for StatusSidebarWidget { fn render(self, area: Rect, buf: &mut Buffer) { - let player = self.0; + let player = self.0.borrow(); // One bright-red span per filled heart, dark-red for the rest, so a // partly-depleted bar reads at a glance. let filled = player.health; diff --git a/maps/start.toml b/maps/start.toml index a0e596b..f82ea2a 100644 --- a/maps/start.toml +++ b/maps/start.toml @@ -6,15 +6,15 @@ start = "start" # Objects reference a script by name via `script_name`. [scripts] greeter = """ -fn init(me, state) { - log(`hello from object — player at ${state.player.x}, ${state.player.y}`); +fn init(me) { + log(`hello from object — player at ${Player.x}, ${Player.y}`); set_tile(2); // change my glyph to ☻ — proves the write path say("Hello there,\\ntraveller!"); - log(`Player health: ${state.player.health}`); + log(`Player health: ${Player.health}`); } -fn tick(me, state, dt) { - if state.player.x == me.x && state.player.y == me.y && !me.waiting { +fn tick(me, dt) { + if Player.x == me.x && Player.y == me.y && !me.waiting { say("Hey! Get offa me!"); me.delay(5.0); } @@ -22,7 +22,7 @@ fn tick(me, state, dt) { """ mover = """ -fn init(me, state) { +fn init(me) { log(`q: ${me.queue.length}`); if Board.registry.get("dancer_x") != () { let new_x = Board.registry.get("dancer_x"); @@ -58,14 +58,14 @@ fn dance(me) { } // Fires when something steps into this object; id is the bumper's object id, // or -1 for the player. -fn bump(me, state, id) { +fn bump(me, id) { log(`mover bumped by ${id}`); say("Ow!"); } """ muffin = """ -fn bump(me, state, _id) { +fn bump(me, _id) { scroll([ "You find a small muffin on the ground, your favorite kind.", "It smells of cinnamon and warm mornings.", @@ -73,7 +73,7 @@ fn bump(me, state, _id) { ["ignore", "This is obviously a trap — ignore it."] ]); } -fn eat(me, state) { +fn eat(me) { say("Yeah it was poisoned."); alter_health(-2); } @@ -84,7 +84,7 @@ fn ignore() { """ noticeboard = """ -fn bump(me, state, id) { +fn bump(me, id) { scroll([ " TOWN NOTICE BOARD", " ", @@ -117,7 +117,7 @@ fn bump(me, state, id) { """ bookshelf = """ -fn bump(me, state, id) { +fn bump(me, id) { scroll([ " THE BOOKSHELF", " ", @@ -136,13 +136,13 @@ fn bump(me, state, id) { """ fireplace = """ -fn bump(me, state, id) { +fn bump(me, id) { say("The fire crackles warmly.\\nYou feel at ease."); } """ chest = """ -fn bump(me, state, id) { +fn bump(me, id) { scroll([ " THE CHEST", " ", @@ -160,7 +160,7 @@ fn bump(me, state, id) { """ yammerer = """ -fn tick(me, state, dt) { +fn tick(me, dt) { if !me.waiting { say("blahblahblah"); delay(1); @@ -169,7 +169,7 @@ fn tick(me, state, dt) { """ shifter = """ -fn tick(me, state, dt) { +fn tick(me, dt) { let x = me.x; let y = me.y;