Files
kiln/kiln-core/src/game.rs
T

190 lines
7.8 KiB
Rust
Raw Normal View History

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-06-04 23:52:33 -05:00
/// Holds the active game world and provides game-logic operations.
///
/// `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.
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,
}
impl GameState {
/// 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.
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-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-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-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
}