new layer model
This commit is contained in:
+67
-40
@@ -135,7 +135,7 @@ impl GameState {
|
||||
board_transition: None,
|
||||
player
|
||||
};
|
||||
state.drain_errors();
|
||||
state.drain_log();
|
||||
state
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ impl GameState {
|
||||
// Fire any bump/send reactions, then flush errors.
|
||||
let mut called = CalledSet::new();
|
||||
self.settle(ev, &mut called);
|
||||
self.drain_errors();
|
||||
self.drain_log();
|
||||
}
|
||||
|
||||
/// Advances real-time game state by `dt` (the elapsed time since the last tick).
|
||||
@@ -238,15 +238,16 @@ impl GameState {
|
||||
// Fire the bump/send reactions those ticks triggered, then flush errors.
|
||||
let mut called = CalledSet::new();
|
||||
self.settle(ev, &mut called);
|
||||
self.drain_errors();
|
||||
self.drain_log();
|
||||
}
|
||||
|
||||
/// Drains errors collected by the script host into the game log.
|
||||
fn drain_errors(&mut self) {
|
||||
/// Drains the log lines collected by the script host — script `log()` output
|
||||
/// plus engine/runtime errors — into the game log.
|
||||
fn drain_log(&mut self) {
|
||||
// TODO: errors are only logged for now. This is the place to halt execution /
|
||||
// set an error state when a script faults.
|
||||
let errors = self.scripts.take_errors();
|
||||
self.log.extend(errors);
|
||||
let lines = self.scripts.take_logs();
|
||||
self.log.extend(lines);
|
||||
}
|
||||
|
||||
/// Applies one object's drained `actions` to the board and returns the `bump`
|
||||
@@ -259,9 +260,12 @@ impl GameState {
|
||||
/// reactions are returned rather than fired here, so the caller can run them once
|
||||
/// all object hooks in this pass have applied.
|
||||
fn apply_actions(&mut self, actions: Vec<BoardAction>) -> Events {
|
||||
// Logs are collected here rather than pushed inline, since the board borrow
|
||||
// below also borrows `self`.
|
||||
let mut logs: Vec<LogLine> = Vec::new();
|
||||
// Application-time errors (teleport/push/shift failures) go straight onto
|
||||
// the shared LogSink — the same immediate channel as script `log()` output,
|
||||
// so everything lands in the log in one emission order and is flushed by
|
||||
// `drain_log`. A cheap Rc clone lets us push while the board borrow (which
|
||||
// also borrows `self`) is held.
|
||||
let log_sink = self.scripts.log_sink().clone();
|
||||
let mut bumps: Vec<(ObjectId, Direction)> = Vec::new();
|
||||
// Net change to player stats from AddGems / AlterHealth actions; applied
|
||||
// to `self` after the board borrow drops.
|
||||
@@ -289,7 +293,6 @@ impl GameState {
|
||||
obj.glyph.tile = tile;
|
||||
}
|
||||
}
|
||||
Action::Log(line) => logs.push(line),
|
||||
Action::SetTag {
|
||||
target,
|
||||
tag,
|
||||
@@ -340,9 +343,9 @@ impl GameState {
|
||||
}
|
||||
Action::Teleport { target, x, y } => {
|
||||
if !board.in_bounds((x, y)) {
|
||||
logs.push(LogLine::error(format!(
|
||||
log_sink.error(format!(
|
||||
"teleport({target},{x},{y}): out of bounds"
|
||||
)));
|
||||
));
|
||||
} else if target == -1 {
|
||||
// Move the player. A solid *other than the player itself*
|
||||
// blocks the destination.
|
||||
@@ -352,9 +355,9 @@ impl GameState {
|
||||
Some(s) if !s.player()
|
||||
);
|
||||
if blocked {
|
||||
logs.push(LogLine::error(format!(
|
||||
log_sink.error(format!(
|
||||
"teleport(player,{x},{y}): destination is solid"
|
||||
)));
|
||||
));
|
||||
} else {
|
||||
board.player.x = ux as i64;
|
||||
board.player.y = uy as i64;
|
||||
@@ -364,9 +367,9 @@ impl GameState {
|
||||
// a solid mover, unless the occupant is that same object.
|
||||
let (ux, uy) = (x as usize, y as usize);
|
||||
match board.objects.get(&tid) {
|
||||
None => logs.push(LogLine::error(format!(
|
||||
None => log_sink.error(format!(
|
||||
"teleport({target},{x},{y}): no such object"
|
||||
))),
|
||||
)),
|
||||
Some(obj) => {
|
||||
let source_solid = obj.solid;
|
||||
let blocked = source_solid
|
||||
@@ -375,9 +378,9 @@ impl GameState {
|
||||
Some(s) if s.object_id() != Some(tid)
|
||||
);
|
||||
if blocked {
|
||||
logs.push(LogLine::error(format!(
|
||||
log_sink.error(format!(
|
||||
"teleport({target},{x},{y}): destination is solid"
|
||||
)));
|
||||
));
|
||||
} else if let Some(obj) = board.objects.get_mut(&tid) {
|
||||
obj.x = ux;
|
||||
obj.y = uy;
|
||||
@@ -385,22 +388,24 @@ impl GameState {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logs.push(LogLine::error(format!(
|
||||
log_sink.error(format!(
|
||||
"teleport({target},{x},{y}): invalid target id"
|
||||
)));
|
||||
));
|
||||
}
|
||||
}
|
||||
// push() self-checks can_push, so an in-bounds guard is all we add.
|
||||
Action::Push { x, y, dir } => {
|
||||
if !board.in_bounds((x, y)) {
|
||||
logs.push(LogLine::error(format!("push({x},{y}): out of bounds")));
|
||||
log_sink.error(format!("push({x},{y}): out of bounds"));
|
||||
} else {
|
||||
board.push(x as usize, y as usize, dir);
|
||||
}
|
||||
}
|
||||
// apply_shift moves the named cells.
|
||||
// apply_shift moves the named cells, returning any error lines.
|
||||
Action::Shift(cells) => {
|
||||
logs.extend(board.apply_shift(&cells));
|
||||
for line in board.apply_shift(&cells) {
|
||||
log_sink.line(line);
|
||||
}
|
||||
}
|
||||
// Accumulated and applied to `self.player.gems` after the borrow drops.
|
||||
Action::AddGems(n) => gem_delta += n,
|
||||
@@ -415,7 +420,6 @@ impl GameState {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.log.extend(logs);
|
||||
for bubble in new_bubbles {
|
||||
// One bubble per object: replace the existing one if present.
|
||||
self.speech_bubbles
|
||||
@@ -435,7 +439,7 @@ impl GameState {
|
||||
}
|
||||
for (color, present) in key_changes {
|
||||
if !self.player.borrow_mut().keys.set_by_name(&color, present) {
|
||||
self.log.push(LogLine::error(format!("set_key: unknown color {color:?}")));
|
||||
log_sink.error(format!("set_key: unknown color {color:?}"));
|
||||
}
|
||||
}
|
||||
// Return the reactions for the caller's settle pass rather than firing them here.
|
||||
@@ -490,7 +494,7 @@ impl GameState {
|
||||
let ev = self.apply_actions(actions);
|
||||
let mut called = CalledSet::new();
|
||||
self.settle(ev, &mut called);
|
||||
self.drain_errors();
|
||||
self.drain_log();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,7 +616,7 @@ impl GameState {
|
||||
// Settle the grab/bump reactions (and any they cascade into) before returning.
|
||||
let mut called = CalledSet::new();
|
||||
self.settle(ev, &mut called);
|
||||
self.drain_errors();
|
||||
self.drain_log();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,8 +728,8 @@ mod tests {
|
||||
);
|
||||
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);
|
||||
assert_eq!(b.get(2, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get(3, 0).1, Archetype::Crate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -754,6 +758,29 @@ mod tests {
|
||||
assert_eq!(lines, vec!["empty:yes", "crate:no", "off:no"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_is_immediate_and_not_paced_by_the_queue() {
|
||||
// `delay(5.0)` parks the object's action queue for 5 seconds, then `log()`
|
||||
// fires. Because logging bypasses the queue entirely, the line must appear
|
||||
// right after run_init (dt = 0) — under the old queued-Action behavior it
|
||||
// would have been stuck behind the delay and absent here.
|
||||
let mut obj = ObjectDef::new(0, 0);
|
||||
obj.solid = false;
|
||||
obj.script_name = Some("s".to_string());
|
||||
let board = open_board(4, 1, (3, 0), vec![obj]);
|
||||
let src = "fn init(m) { delay(5.0); log(\"immediate\"); }";
|
||||
let scripts = HashMap::from([("s".to_string(), src.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 == "immediate")),
|
||||
"log() should hit the game log immediately, even behind a delay"
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a 3×3 board with a non-solid clockwise spinner object (running the
|
||||
/// real `scripts/spinner.rhai`) at the centre and the player parked on it.
|
||||
fn spinner_board(crates: &[(usize, usize)], walls: &[(usize, usize)]) -> GameState {
|
||||
@@ -797,9 +824,9 @@ mod tests {
|
||||
let mut game = spinner_board(&[(1, 0), (2, 0)], &[(2, 1)]);
|
||||
game.tick(Duration::from_millis(16));
|
||||
let b = game.board();
|
||||
assert_eq!(b.get(0, 1, 0).1, Archetype::Crate); // N kept
|
||||
assert_eq!(b.get(0, 2, 0).1, Archetype::Crate); // NE kept (not destroyed)
|
||||
assert_eq!(b.get(0, 2, 1).1, Archetype::Wall); // wall kept
|
||||
assert_eq!(b.get(1, 0).1, Archetype::Crate); // N kept
|
||||
assert_eq!(b.get(2, 0).1, Archetype::Crate); // NE kept (not destroyed)
|
||||
assert_eq!(b.get(2, 1).1, Archetype::Wall); // wall kept
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -809,10 +836,10 @@ mod tests {
|
||||
let mut game = spinner_board(&[(0, 1), (0, 0), (1, 0)], &[]);
|
||||
game.tick(Duration::from_millis(16));
|
||||
let b = game.board();
|
||||
assert_eq!(b.get(0, 2, 0).1, Archetype::Crate); // NE: filled by N
|
||||
assert_eq!(b.get(0, 1, 0).1, Archetype::Crate); // N: filled by NW
|
||||
assert_eq!(b.get(0, 0, 0).1, Archetype::Crate); // NW: filled by W
|
||||
assert_eq!(b.get(0, 0, 1).1, Archetype::Empty); // W: vacated (hole moved here)
|
||||
assert_eq!(b.get(2, 0).1, Archetype::Crate); // NE: filled by N
|
||||
assert_eq!(b.get(1, 0).1, Archetype::Crate); // N: filled by NW
|
||||
assert_eq!(b.get(0, 0).1, Archetype::Crate); // NW: filled by W
|
||||
assert_eq!(b.get(0, 1).1, Archetype::Empty); // W: vacated (hole moved here)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -822,9 +849,9 @@ mod tests {
|
||||
let mut game = spinner_board_dir(&[(1, 0)], &[], true);
|
||||
game.tick(Duration::from_millis(16));
|
||||
let b = game.board();
|
||||
assert_eq!(b.get(0, 0, 0).1, Archetype::Crate); // NW: crate rotated counter-clockwise
|
||||
assert_eq!(b.get(0, 2, 0).1, Archetype::Empty); // NE: untouched
|
||||
assert_eq!(b.get(0, 1, 0).1, Archetype::Empty); // N: vacated
|
||||
assert_eq!(b.get(0, 0).1, Archetype::Crate); // NW: crate rotated counter-clockwise
|
||||
assert_eq!(b.get(2, 0).1, Archetype::Empty); // NE: untouched
|
||||
assert_eq!(b.get(1, 0).1, Archetype::Empty); // N: vacated
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user