Board layers

This commit is contained in:
2026-06-15 23:35:18 -05:00
parent d1d0824d37
commit 01cd73ca3b
29 changed files with 2166 additions and 2051 deletions
+56 -33
View File
@@ -1,12 +1,11 @@
use crate::action::Action;
use crate::archetype::Archetype;
use crate::board::Board;
use crate::log::LogLine;
use crate::script::ScriptHost;
use crate::utils::{Direction, ObjectId, Player, ScriptArg, Solid};
use crate::world::World;
use std::cell::{Ref, RefMut};
use std::time::Duration;
use crate::utils::{Direction, ObjectId, Player, ScriptArg, Solid};
/// How long a `say()` speech bubble stays on screen, in seconds.
pub const SAY_DURATION: f64 = 3.0;
@@ -85,7 +84,10 @@ impl GameState {
pub fn from_world(world: World) -> Self {
let name = world.start.clone();
let host = ScriptHost::new(
world.boards.get(&name).expect("world::load guarantees start board exists"),
world
.boards
.get(&name)
.expect("world::load guarantees start board exists"),
&world.scripts,
);
let mut state = Self {
@@ -177,7 +179,10 @@ impl GameState {
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| { b.remaining -= secs; b.remaining > 0.0 });
self.speech_bubbles.retain_mut(|b| {
b.remaining -= secs;
b.remaining > 0.0
});
self.tick_pushers(secs);
self.resolve();
}
@@ -193,18 +198,7 @@ impl GameState {
return;
}
self.pusher_timer += PUSHER_INTERVAL;
let pushers: Vec<(usize, usize)> = {
let board = self.board();
let mut v = Vec::new();
for y in 0..board.height {
for x in 0..board.width {
if matches!(board.get(x, y).1, Archetype::Pusher(_)) {
v.push((x, y));
}
}
}
v
};
let pushers: Vec<(usize, usize)> = self.board().pusher_cells();
let mut board = self.board_mut();
for (x, y) in pushers {
board.advance_pusher(x, y);
@@ -251,9 +245,17 @@ impl GameState {
}
}
Action::Log(line) => logs.push(line),
Action::SetTag { target, tag, present } => {
Action::SetTag {
target,
tag,
present,
} => {
if let Some(obj) = board.objects.get_mut(&target) {
if present { obj.tags.insert(tag); } else { obj.tags.remove(&tag); }
if present {
obj.tags.insert(tag);
} else {
obj.tags.remove(&tag);
}
}
}
// Replace any existing bubble from this object so repeated say() calls
@@ -267,26 +269,38 @@ impl GameState {
Action::Delay(_) => {}
Action::SetColor { fg, bg } => {
if let Some(obj) = board.objects.get_mut(&ba.source) {
if let Some(c) = fg { obj.glyph.fg = c; }
if let Some(c) = bg { obj.glyph.bg = c; }
if let Some(c) = fg {
obj.glyph.fg = c;
}
if let Some(c) = bg {
obj.glyph.bg = c;
}
}
}
// Collected and fired after the board borrow drops, like bumps.
Action::Send { target, fn_name, arg } => {
Action::Send {
target,
fn_name,
arg,
} => {
sends.push((target, fn_name, arg));
}
// Later scrolls overwrite earlier ones from the same tick.
Action::Scroll(lines) => {
new_scroll = Some(Scroll { source: ba.source, lines, choice: None });
new_scroll = Some(Scroll {
source: ba.source,
lines,
choice: None,
});
}
Action::Teleport { x, y } => {
if !board.in_bounds((x, y)) {
logs.push(LogLine::error(format!(
"teleport({x},{y}): out of bounds"
)));
logs.push(LogLine::error(format!("teleport({x},{y}): out of bounds")));
} else {
let (ux, uy) = (x as usize, y as usize);
let source_solid = board.objects.get(&ba.source)
let source_solid = board
.objects
.get(&ba.source)
.map(|o| o.solid)
.unwrap_or(false);
if source_solid && board.solid_at(ux, uy).is_some() {
@@ -305,7 +319,8 @@ impl GameState {
self.log.extend(logs);
for bubble in new_bubbles {
// One bubble per object: replace the existing one if present.
self.speech_bubbles.retain(|b| b.object_id != bubble.object_id);
self.speech_bubbles
.retain(|b| b.object_id != bubble.object_id);
self.speech_bubbles.push(bubble);
}
if let Some(scroll) = new_scroll {
@@ -344,12 +359,16 @@ impl GameState {
/// to play a visual effect during the switch.
pub fn enter_board(&mut self, target_map: &str, target_entry: &str) {
if !self.world.boards.contains_key(target_map) {
self.log.push(LogLine::error(format!("portal target board {target_map:?} not found")));
self.log.push(LogLine::error(format!(
"portal target board {target_map:?} not found"
)));
return;
}
// Find the named arrival portal on the target board (borrow then release).
let arrival = self.world.boards[target_map].borrow()
.portals.iter()
let arrival = self.world.boards[target_map]
.borrow()
.portals
.iter()
.find(|p| p.name == target_entry)
.map(|p| (p.x, p.y));
let (ax, ay) = match arrival {
@@ -366,7 +385,10 @@ impl GameState {
self.active_scroll = None;
// 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 = Player { x: ax as i32, y: ay as i32 };
self.board_mut().player = Player {
x: ax as i32,
y: ay as i32,
};
// Rebuild the script host for the new board's objects.
self.scripts = ScriptHost::new(
&self.world.boards[&self.current_board_name],
@@ -405,7 +427,9 @@ impl GameState {
board.player.x = nx as i32;
board.player.y = ny as i32;
// Check for a portal at the new position; clone strings to release the borrow.
portal_target = board.portals.iter()
portal_target = board
.portals
.iter()
.find(|p| p.x == nx && p.y == ny)
.map(|p| (p.target_map.clone(), p.target_entry.clone()));
} else {
@@ -453,4 +477,3 @@ fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<Object
}
bumped
}