Big API refactor

This commit is contained in:
2026-06-28 00:12:52 -05:00
parent db9a5d37b6
commit 9de31d933b
31 changed files with 1036 additions and 1029 deletions
+32 -41
View File
@@ -1,8 +1,8 @@
use crate::action::Action;
use crate::action::{Action, SendArg};
use crate::board::Board;
use crate::log::LogLine;
use crate::script::{ScriptHost, ScriptState};
use crate::utils::{Direction, ObjectId, PlayerPos, ScriptArg};
use crate::script::ScriptHost;
use crate::utils::{Direction, ObjectId, PlayerPos};
use crate::world::World;
use std::cell::{Ref, RefMut};
use std::time::Duration;
@@ -13,6 +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;
/// An active scroll overlay opened by a scripted object via `scroll()`.
@@ -162,7 +163,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(self.player));
self.scripts.run_init(ScriptState::from_game_state(&self));
self.resolve();
}
@@ -176,7 +177,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(self.player), secs);
self.scripts.run_tick(ScriptState::from_game_state(&self), secs);
// Expire speech bubbles before resolving new actions so a fresh say()
// this frame isn't immediately culled.
self.speech_bubbles.retain_mut(|b| {
@@ -212,10 +213,11 @@ impl GameState {
let mut gem_delta: i64 = 0;
let mut health_delta: i64 = 0;
let mut key_changes: Vec<(String, bool)> = Vec::new();
let mut sends: Vec<(ObjectId, String, Option<ScriptArg>)> = Vec::new();
let mut sends: Vec<(ObjectId, String, SendArg)> = Vec::new();
let mut new_bubbles: Vec<SpeechBubble> = Vec::new();
// Collected outside the board borrow so we can assign to self.active_scroll.
let mut new_scroll: Option<Scroll> = None;
{
let mut board = self.board_mut();
for ba in actions {
@@ -347,11 +349,12 @@ impl GameState {
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(ScriptState(self.player), bumped, bumper);
self.scripts.run_bump(state.clone(), bumped, bumper);
}
for (target, fn_name, arg) in sends {
self.scripts.run_send(ScriptState(self.player), target, &fn_name, arg);
self.scripts.run_send(state.clone(), target, &fn_name, arg);
}
self.drain_errors();
}
@@ -367,7 +370,7 @@ impl GameState {
if let Some(scroll) = self.active_scroll.take()
&& let Some(choice) = scroll.choice
{
self.scripts.run_send(ScriptState(self.player), scroll.source, &choice, None);
self.scripts.run_send(ScriptState::from_game_state(&self), scroll.source, &choice, SendArg::None);
self.drain_errors();
}
}
@@ -407,8 +410,8 @@ impl GameState {
// Switch to the new board and place the player at the arrival portal.
self.current_board_name = target_map.to_string();
self.board_mut().player = PlayerPos {
x: ax as i32,
y: ay as i32,
x: ax as i64,
y: ay as i64,
};
// Rebuild the script host for the new board's objects.
self.scripts = ScriptHost::new(
@@ -432,9 +435,9 @@ impl GameState {
let grabbed;
let portal_target;
{
let (dx, dy): (i32, i32) = dir.into();
let (dx, dy): (i64, i64) = dir.into();
let mut board = self.board_mut();
let target = (board.player.x + dx, board.player.y + dy);
let target = (board.player.x as i64 + dx, board.player.y as i64 + dy);
if !board.in_bounds(target) {
return;
}
@@ -454,8 +457,8 @@ impl GameState {
if grabbed.is_none() {
board.push(nx, ny, dir);
}
board.player.x = nx as i32;
board.player.y = ny as i32;
board.player.x = nx as i64;
board.player.y = ny as i64;
// Check for a portal at the new position; clone strings to release the borrow.
portal_target = board
.portals
@@ -473,12 +476,13 @@ 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(ScriptState(self.player), id);
self.scripts.run_grab(state.clone(), id);
self.resolve();
}
if let Some(idx) = bumped {
self.scripts.run_bump(ScriptState(self.player), idx, -1);
self.scripts.run_bump(state, idx, -1);
self.drain_errors();
}
}
@@ -493,9 +497,9 @@ impl GameState {
/// pushed aside or blocks the move — since something tried to move into it. Walls and
/// crates carry no script, so only solid objects yield a bump.
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<ObjectId> {
let (dx, dy): (i32, i32) = dir.into();
let (dx, dy): (i64, i64) = dir.into();
let (ox, oy) = board.objects.get(&id).map(|o| (o.x, o.y))?;
let target = (ox as i32 + dx, oy as i32 + dy);
let target = (ox as i64 + dx, oy as i64 + dy);
if !board.in_bounds(target) {
return None;
}
@@ -572,7 +576,7 @@ mod tests {
let mut obj = ObjectDef::new(0, 0);
obj.solid = false;
obj.script_name = Some("s".to_string());
let mut board = open_board(board_w, 1, (board_w as i32 - 1, 0), vec![obj]);
let mut board = open_board(board_w, 1, (board_w as i64 - 1, 0), vec![obj]);
crate_at(&mut board, 2, 0);
let scripts = HashMap::from([("s".to_string(), src.to_string())]);
let mut game = GameState::with_scripts(board, scripts);
@@ -580,26 +584,13 @@ mod tests {
game
}
#[test]
fn script_push_shoves_a_crate() {
// The object pushes the crate at (2,0) one step east on its first tick.
let mut game = game_with_object_script(
5,
"fn tick(dt) { if Queue.length() == 0 { push(2, 0, East); } }",
);
game.tick(Duration::from_millis(16));
let b = game.board();
assert_eq!(b.get(0, 2, 0).1, Archetype::Empty);
assert_eq!(b.get(0, 3, 0).1, Archetype::Crate);
}
#[test]
fn script_shift_rotates_crates() {
// Rotate the crate at (2,0) with the empty cell at (3,0) in a two-cell cycle:
// crate moves to (3,0), empty moves back to (2,0).
let mut game = game_with_object_script(
5,
"fn tick(dt) { if Queue.length() == 0 { shift([[2, 0], [3, 0]]); } }",
"fn tick(m,s,dt) { if m.queue.length == 0 { shift([[2, 0], [3, 0]]); } }",
);
game.tick(Duration::from_millis(16));
let b = game.board();
@@ -617,10 +608,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() { \
log(if passable(1, 0) { \"empty:yes\" } else { \"empty:no\" }); \
log(if passable(2, 0) { \"crate:yes\" } else { \"crate:no\" }); \
log(if passable(9, 0) { \"off:yes\" } else { \"off:no\" }); }";
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 scripts = HashMap::from([("s".to_string(), src.to_string())]);
let mut game = GameState::with_scripts(board, scripts);
game.run_init();
@@ -735,7 +726,7 @@ mod tests {
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(),
"fn init(m, s) { set_key(\"blue\", true); set_key(\"red\", true); }".to_string(),
)]);
let mut game = GameState::with_scripts(board, scripts);
game.run_init();
@@ -751,7 +742,7 @@ mod tests {
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(),
"fn init(m, s) { set_key(\"blue\", true); set_key(\"blue\", false); }".to_string(),
)]);
let mut game2 = GameState::with_scripts(board2, scripts2);
game2.run_init();
@@ -767,7 +758,7 @@ mod tests {
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(),
r#"fn init(m,s) { set_key("chartreuse", true); }"#.to_string(),
)]);
let mut game = GameState::with_scripts(board, scripts);
game.run_init();