2026-06-03 22:46:54 -05:00
|
|
|
use crate::log::LogLine;
|
2026-06-06 17:36:00 -05:00
|
|
|
use crate::script::{Action, Direction, ScriptHost};
|
2026-06-04 23:52:33 -05:00
|
|
|
use std::cell::{Ref, RefCell, RefMut};
|
|
|
|
|
use std::rc::Rc;
|
2026-06-04 20:11:55 -05:00
|
|
|
use std::time::Duration;
|
2026-06-07 00:19:53 -05:00
|
|
|
use crate::board::Board;
|
|
|
|
|
use crate::utils::{ObjectId, Solid};
|
2026-05-16 01:10:04 -05:00
|
|
|
|
2026-06-04 23:52:33 -05:00
|
|
|
/// Holds the active game world and provides game-logic operations.
|
2026-05-17 12:48:52 -05:00
|
|
|
///
|
|
|
|
|
/// `GameState` is the boundary between the engine (rendering, input) and the
|
2026-06-05 00:12:57 -05:00
|
|
|
/// game data. It owns the board (behind a shared `Rc<RefCell<Board>>`),
|
2026-06-04 23:52:33 -05:00
|
|
|
/// the message log, and the [`ScriptHost`] driving object scripts. Front-ends and
|
|
|
|
|
/// internal logic reach the board through [`board`](GameState::board) /
|
|
|
|
|
/// [`board_mut`](GameState::board_mut). Scripts read the board directly and
|
|
|
|
|
/// request mutations as commands, applied by [`apply_commands`](GameState::apply_commands)
|
|
|
|
|
/// after each script batch.
|
2026-05-16 01:10:04 -05:00
|
|
|
pub struct GameState {
|
2026-06-04 23:52:33 -05:00
|
|
|
/// The scriptable world, shared with the script host's read getters.
|
2026-06-05 00:12:57 -05:00
|
|
|
board: Rc<RefCell<Board>>,
|
2026-06-03 22:46:54 -05:00
|
|
|
/// The in-game message log, oldest first (newest pushed at the end).
|
|
|
|
|
pub log: Vec<LogLine>,
|
2026-06-04 20:11:55 -05:00
|
|
|
/// The Rhai scripting runtime driving this board's scripted objects.
|
|
|
|
|
scripts: ScriptHost,
|
2026-05-16 01:10:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl GameState {
|
2026-05-17 12:48:52 -05:00
|
|
|
/// Creates a `GameState` from a pre-loaded [`Board`].
|
2026-06-04 20:11:55 -05:00
|
|
|
///
|
|
|
|
|
/// Compiles the board's object scripts but does **not** run any of them;
|
|
|
|
|
/// call [`GameState::run_init`] once the game is ready to start. Any script
|
|
|
|
|
/// compile errors are surfaced into the log here.
|
2026-05-16 01:50:05 -05:00
|
|
|
pub fn new(board: Board) -> Self {
|
2026-06-05 00:12:57 -05:00
|
|
|
let board = Rc::new(RefCell::new(board));
|
|
|
|
|
let scripts = ScriptHost::new(&board);
|
2026-06-04 20:11:55 -05:00
|
|
|
let mut state = Self {
|
2026-06-05 00:12:57 -05:00
|
|
|
board,
|
2026-06-03 22:46:54 -05:00
|
|
|
log: Vec::new(),
|
2026-06-04 20:11:55 -05:00
|
|
|
scripts,
|
|
|
|
|
};
|
|
|
|
|
// Surface any compile-time script errors collected during ScriptHost::new.
|
2026-06-06 17:36:00 -05:00
|
|
|
state.drain_errors();
|
2026-06-04 20:11:55 -05:00
|
|
|
state
|
2026-06-03 22:46:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-06-04 23:52:33 -05:00
|
|
|
/// Borrows the active board for reading (e.g. by a front-end renderer).
|
|
|
|
|
pub fn board(&self) -> Ref<'_, Board> {
|
2026-06-05 00:12:57 -05:00
|
|
|
self.board.borrow()
|
2026-06-04 23:52:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Borrows the active board for mutation.
|
|
|
|
|
pub fn board_mut(&self) -> RefMut<'_, Board> {
|
2026-06-05 00:12:57 -05:00
|
|
|
self.board.borrow_mut()
|
2026-06-04 23:52:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 22:46:54 -05:00
|
|
|
/// Appends a styled message to the log.
|
|
|
|
|
pub fn log(&mut self, line: LogLine) {
|
|
|
|
|
self.log.push(line);
|
2026-05-16 01:10:04 -05:00
|
|
|
}
|
|
|
|
|
|
2026-06-06 17:36:00 -05:00
|
|
|
/// Runs the `init()` hook of every scripted object, pumps their queues, and
|
|
|
|
|
/// resolves the resulting actions. Call once, after the whole map is loaded and
|
|
|
|
|
/// the game is about to start — never during map deserialization, since a script
|
|
|
|
|
/// may inspect the board.
|
2026-06-04 20:11:55 -05:00
|
|
|
pub fn run_init(&mut self) {
|
|
|
|
|
self.scripts.run_init();
|
2026-06-06 17:36:00 -05:00
|
|
|
self.resolve();
|
2026-06-04 20:11:55 -05:00
|
|
|
}
|
|
|
|
|
|
2026-06-06 17:36:00 -05:00
|
|
|
/// Advances real-time game state by `dt` (the elapsed time since the last tick).
|
|
|
|
|
/// Called once per frame by the front-end's game loop. Counts down object
|
|
|
|
|
/// cooldowns, drives every scripted object's `tick(dt)` hook (pumping each queue),
|
|
|
|
|
/// then resolves the actions that were promoted onto the board queue.
|
2026-06-03 23:21:05 -05:00
|
|
|
pub fn tick(&mut self, dt: Duration) {
|
2026-06-06 17:36:00 -05:00
|
|
|
let secs = dt.as_secs_f64();
|
|
|
|
|
self.scripts.advance_timers(secs);
|
|
|
|
|
self.scripts.run_tick(secs);
|
|
|
|
|
self.resolve();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Drains errors collected by the script host into the game log.
|
|
|
|
|
fn drain_errors(&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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Resolves the board queue: applies each promoted action to the board, then fires
|
|
|
|
|
/// the `bump` hooks the moves triggered.
|
|
|
|
|
///
|
|
|
|
|
/// Done in two phases so no `board_mut` borrow is held while `bump` scripts run
|
|
|
|
|
/// (they read the board through its getters): phase A mutates the board and records
|
|
|
|
|
/// `(bumped_object, bumper)` pairs; phase B fires the bumps after the borrow drops.
|
|
|
|
|
fn resolve(&mut self) {
|
|
|
|
|
let actions = self.scripts.take_board_queue();
|
|
|
|
|
// Logs are collected here rather than pushed inline, since the board borrow
|
|
|
|
|
// below also borrows `self`.
|
|
|
|
|
let mut logs: Vec<LogLine> = Vec::new();
|
2026-06-06 20:01:07 -05:00
|
|
|
let mut bumps: Vec<(ObjectId, i64)> = Vec::new();
|
2026-06-06 17:36:00 -05:00
|
|
|
{
|
|
|
|
|
let mut board = self.board_mut();
|
|
|
|
|
for ba in actions {
|
|
|
|
|
match ba.action {
|
|
|
|
|
Action::Move(dir) => {
|
|
|
|
|
if let Some(bumped) = step_object(&mut board, ba.source, dir) {
|
|
|
|
|
bumps.push((bumped, ba.source as i64));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Action::SetTile(tile) => {
|
2026-06-06 20:01:07 -05:00
|
|
|
if let Some(obj) = board.objects.get_mut(&ba.source) {
|
2026-06-06 17:36:00 -05:00
|
|
|
obj.glyph.tile = tile;
|
|
|
|
|
}
|
2026-06-04 23:52:33 -05:00
|
|
|
}
|
2026-06-06 17:36:00 -05:00
|
|
|
Action::Log(line) => logs.push(line),
|
2026-06-04 23:52:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-06 17:36:00 -05:00
|
|
|
self.log.extend(logs);
|
|
|
|
|
for (bumped, bumper) in bumps {
|
|
|
|
|
self.scripts.run_bump(bumped, bumper);
|
2026-06-04 23:52:33 -05:00
|
|
|
}
|
2026-06-06 17:36:00 -05:00
|
|
|
self.drain_errors();
|
2026-06-03 23:21:05 -05:00
|
|
|
}
|
|
|
|
|
|
2026-06-06 14:11:20 -05:00
|
|
|
/// Attempts to move the player one cell in `dir`.
|
2026-05-17 12:48:52 -05:00
|
|
|
///
|
2026-06-06 14:11:20 -05:00
|
|
|
/// The move is ignored if the target cell is out of bounds, or it is neither
|
2026-06-06 17:36:00 -05:00
|
|
|
/// passable nor a pushable solid that can be shoved aside. No-ops silently (the
|
|
|
|
|
/// caller does not need to check). If the target cell holds a solid object, that
|
|
|
|
|
/// object's `bump(-1)` hook fires (whether or not the player ends up moving).
|
2026-06-06 14:11:20 -05:00
|
|
|
pub fn try_move(&mut self, dir: Direction) {
|
2026-06-06 17:36:00 -05:00
|
|
|
let bumped;
|
|
|
|
|
{
|
|
|
|
|
let (dx, dy): (i32, i32) = dir.into();
|
|
|
|
|
let mut board = self.board_mut();
|
|
|
|
|
let target = (board.player.x + dx, board.player.y + dy);
|
|
|
|
|
if !board.in_bounds(target) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let (nx, ny) = (target.0 as usize, target.1 as usize);
|
|
|
|
|
// A solid object in the way is bumped by the player (id -1).
|
|
|
|
|
bumped = match board.solid_at(nx, ny) {
|
2026-06-06 20:01:07 -05:00
|
|
|
Some(Solid::Object(_)) => board.solid_object_id_at(nx, ny),
|
2026-06-06 17:36:00 -05:00
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
if board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-06-06 14:11:20 -05:00
|
|
|
}
|
2026-06-06 17:36:00 -05:00
|
|
|
if let Some(idx) = bumped {
|
|
|
|
|
self.scripts.run_bump(idx, -1);
|
|
|
|
|
self.drain_errors();
|
2026-05-16 01:10:04 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-19 00:07:04 -05:00
|
|
|
}
|
2026-06-03 23:21:05 -05:00
|
|
|
|
2026-06-06 20:01:07 -05:00
|
|
|
/// Moves object `id` one cell in `dir` on `board`, returning the [`ObjectId`] of a
|
|
|
|
|
/// solid object it bumped (its target cell was occupied by that object), if any.
|
2026-06-06 17:36:00 -05:00
|
|
|
///
|
|
|
|
|
/// The move itself proceeds only if the target is in bounds and either passable or a
|
|
|
|
|
/// pushable solid the object can shove out of the way (see [`Board::can_push`]). The
|
|
|
|
|
/// bump is recorded whenever a solid object occupies the target — whether it gets
|
|
|
|
|
/// 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.
|
2026-06-06 20:01:07 -05:00
|
|
|
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<ObjectId> {
|
2026-06-06 17:36:00 -05:00
|
|
|
let (dx, dy): (i32, i32) = dir.into();
|
2026-06-06 20:01:07 -05:00
|
|
|
let (ox, oy) = board.objects.get(&id).map(|o| (o.x, o.y))?;
|
2026-06-06 17:36:00 -05:00
|
|
|
let target = (ox as i32 + dx, oy as i32 + dy);
|
|
|
|
|
if !board.in_bounds(target) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let (nx, ny) = (target.0 as usize, target.1 as usize);
|
2026-06-06 20:01:07 -05:00
|
|
|
// Capture the bumped object before any push relocates it (its id is stable).
|
2026-06-06 17:36:00 -05:00
|
|
|
let bumped = match board.solid_at(nx, ny) {
|
2026-06-06 20:01:07 -05:00
|
|
|
Some(Solid::Object(_)) => board.solid_object_id_at(nx, ny),
|
2026-06-06 17:36:00 -05:00
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
if board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
|
|
|
|
|
board.push(nx, ny, dir); // shoves a crate/object out of the way; no-op otherwise
|
2026-06-06 20:01:07 -05:00
|
|
|
let obj = board.objects.get_mut(&id).expect("id checked above");
|
2026-06-06 17:36:00 -05:00
|
|
|
obj.x = nx;
|
|
|
|
|
obj.y = ny;
|
|
|
|
|
}
|
|
|
|
|
bumped
|
|
|
|
|
}
|
|
|
|
|
|