This commit is contained in:
2026-06-13 16:24:29 -05:00
parent 78547696d7
commit 49d32eedf5
9 changed files with 410 additions and 15 deletions
+60 -1
View File
@@ -5,7 +5,7 @@ use crate::script::ScriptHost;
use crate::world::World;
use std::cell::{Ref, RefMut};
use std::time::Duration;
use crate::utils::{Direction, ObjectId, ScriptArg, Solid};
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;
@@ -57,6 +57,10 @@ pub struct GameState {
/// An active scroll overlay opened by `scroll()`. `Some` while the overlay is
/// visible; the front-end pauses ticks and closes it via [`close_scroll`](GameState::close_scroll).
pub active_scroll: Option<Scroll>,
/// Remaining seconds for the board-entry transition animation, set to `1.0`
/// by [`enter_board`](GameState::enter_board). Front-ends tick this down and
/// may block input or show a visual effect while it is `Some(t)` where `t > 0`.
pub board_transition: Option<f64>,
}
impl GameState {
@@ -79,6 +83,7 @@ impl GameState {
scripts: host,
speech_bubbles: Vec::new(),
active_scroll: None,
board_transition: None,
};
state.drain_errors();
state
@@ -253,6 +258,48 @@ impl GameState {
}
}
/// Switches the active board, placing the player at the named arrival portal,
/// rebuilding the script host, and running `init()` hooks on the new board's objects.
///
/// Called automatically by [`try_move`](GameState::try_move) when the player
/// steps onto a portal. Front-ends may check [`board_transition`](GameState::board_transition)
/// 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")));
return;
}
// Find the named arrival portal on the target board (borrow then release).
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 {
Some(pos) => pos,
None => {
self.log.push(LogLine::error(format!(
"portal entry {target_entry:?} not found on board {target_map:?}"
)));
return;
}
};
// Clear per-board transient state.
self.speech_bubbles.clear();
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 };
// Rebuild the script host for the new board's objects.
self.scripts = ScriptHost::new(
&self.world.boards[&self.current_board_name],
&self.world.scripts,
);
// Stub hook for the front-end transition animation (1 second).
self.board_transition = Some(1.0);
// Run init hooks and resolve their queued actions.
self.run_init();
}
/// Attempts to move the player one cell in `dir`.
///
/// The move is ignored if the target cell is out of bounds, or it is neither
@@ -261,6 +308,7 @@ impl GameState {
/// object's `bump(-1)` hook fires (whether or not the player ends up moving).
pub fn try_move(&mut self, dir: Direction) {
let bumped;
let portal_target;
{
let (dx, dy): (i32, i32) = dir.into();
let mut board = self.board_mut();
@@ -278,8 +326,19 @@ impl GameState {
board.push(nx, ny, dir); // shoves a crate/object out of the way; no-op otherwise
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()
.find(|p| p.x == nx && p.y == ny)
.map(|p| (p.target_map.clone(), p.target_entry.clone()));
} else {
portal_target = None;
}
}
// A portal takes priority: board transitions skip the bump hook.
if let Some((target_map, target_entry)) = portal_target {
self.enter_board(&target_map, &target_entry);
return;
}
if let Some(idx) = bumped {
self.scripts.run_bump(idx, -1);
self.drain_errors();