This commit is contained in:
2026-06-25 19:16:46 -05:00
parent db8c8e615d
commit db9a5d37b6
8 changed files with 328 additions and 213 deletions
+3 -3
View File
@@ -77,16 +77,16 @@ pub(crate) enum Action {
/// Zero time cost.
Shift(Vec<(i32, i32)>),
/// Add `n` to the player's gem count (negative subtracts; the count is
/// clamped at 0). Zero time cost. Applied to `GameState::player_gems`.
/// clamped at 0). Zero time cost. Applied to `GameState::player.gems`.
AddGems(i64),
/// Add `dh` to the player's health (clamped to `[0, max_health]`). Zero time
/// cost. Applied to `GameState::player_health`.
/// cost. Applied to `GameState::player.health`.
AlterHealth(i64),
/// Give (`true`) or take (`false`) the named key color from the player.
///
/// Color must be one of `"blue"`, `"green"`, `"cyan"`, `"red"`, `"purple"`,
/// `"orange"`, `"yellow"`, `"white"`. An unrecognized name is logged and
/// ignored. Zero time cost. Applied to `GameState::player_keys`.
/// ignored. Zero time cost. Applied to `GameState::player.keys`.
SetKey(String, bool),
/// Remove the source object from the board. Zero time cost. Used by grab
/// things (e.g. gems) to despawn themselves from their `grab()` hook.
+2 -1
View File
@@ -43,7 +43,8 @@ pub struct Board {
/// top-down; solidity ([`Board::solid_at`]) scans every layer. Access a single
/// cell with [`Board::get`]/[`Board::get_mut`] by `(z, x, y)`.
pub(crate) layers: Vec<Layer>,
/// Current player position. See [`Player`] for caveats about its future.
/// Current player position on this board. See [`PlayerPos`] for caveats
/// about its future. Game-global player *stats* live in [`crate::player::Player`].
pub player: PlayerPos,
/// Scripted objects on this board, keyed by stable [`ObjectId`]. A `BTreeMap`
/// (not a `Vec`) so an object can be removed without invalidating other
+15 -12
View File
@@ -1,7 +1,7 @@
use crate::action::Action;
use crate::board::Board;
use crate::log::LogLine;
use crate::script::ScriptHost;
use crate::script::{ScriptHost, ScriptState};
use crate::utils::{Direction, ObjectId, PlayerPos, ScriptArg};
use crate::world::World;
use std::cell::{Ref, RefMut};
@@ -67,7 +67,10 @@ pub struct GameState {
/// 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>,
/// The player's state
/// The game-global player state (health, gems, keys) — see [`Player`]. Not
/// per-board: it persists across board transitions, unlike the per-board
/// position in [`Board::player`](crate::board::Board::player). Scripts mutate
/// it via `add_gems`/`alter_health`/`set_key` and read a snapshot of it.
pub player: Player,
}
@@ -159,7 +162,7 @@ impl GameState {
/// the game is about to start — never during map deserialization, since a script
/// may inspect the board.
pub fn run_init(&mut self) {
self.scripts.run_init(self.player);
self.scripts.run_init(ScriptState(self.player));
self.resolve();
}
@@ -173,7 +176,7 @@ impl GameState {
// this runs exactly once per player interaction with a scroll.
self.handle_scroll();
let secs = dt.as_secs_f64();
self.scripts.run_tick(self.player, secs);
self.scripts.run_tick(ScriptState(self.player), secs);
// Expire speech bubbles before resolving new actions so a fresh say()
// this frame isn't immediately culled.
self.speech_bubbles.retain_mut(|b| {
@@ -308,11 +311,11 @@ impl GameState {
Action::Shift(cells) => {
logs.extend(board.apply_shift(&cells));
}
// Accumulated and applied to `self.player_gems` after the borrow drops.
// Accumulated and applied to `self.player.gems` after the borrow drops.
Action::AddGems(n) => gem_delta += n,
// Accumulated and applied to `self.player_health` after the borrow drops.
// Accumulated and applied to `self.player.health` after the borrow drops.
Action::AlterHealth(dh) => health_delta += dh,
// Collected and applied to `self.player_keys` after the borrow drops.
// Collected and applied to `self.player.keys` after the borrow drops.
Action::SetKey(color, present) => key_changes.push((color, present)),
// A grab thing despawns itself from its grab() hook.
Action::Die => {
@@ -345,10 +348,10 @@ impl GameState {
}
}
for (bumped, bumper) in bumps {
self.scripts.run_bump(self.player, bumped, bumper);
self.scripts.run_bump(ScriptState(self.player), bumped, bumper);
}
for (target, fn_name, arg) in sends {
self.scripts.run_send(self.player, target, &fn_name, arg);
self.scripts.run_send(ScriptState(self.player), target, &fn_name, arg);
}
self.drain_errors();
}
@@ -364,7 +367,7 @@ impl GameState {
if let Some(scroll) = self.active_scroll.take()
&& let Some(choice) = scroll.choice
{
self.scripts.run_send(self.player, scroll.source, &choice, None);
self.scripts.run_send(ScriptState(self.player), scroll.source, &choice, None);
self.drain_errors();
}
}
@@ -471,11 +474,11 @@ impl GameState {
// Fire the grab hook and resolve it immediately so the grabbed thing's
// die()/add_gems() apply now — no player+object overlap survives this call.
if let Some(id) = grabbed {
self.scripts.run_grab(self.player, id);
self.scripts.run_grab(ScriptState(self.player), id);
self.resolve();
}
if let Some(idx) = bumped {
self.scripts.run_bump(self.player, idx, -1);
self.scripts.run_bump(ScriptState(self.player), idx, -1);
self.drain_errors();
}
}
+8 -1
View File
@@ -1,5 +1,12 @@
use crate::keys::Keyring;
/// The game-global player state: stats that follow the player across boards.
///
/// Owned by [`GameState::player`](crate::game::GameState) (a single value, not
/// per-board), so health, gems, and keys persist through board transitions.
/// Distinct from [`PlayerPos`](crate::utils::PlayerPos), which is the player's
/// position *on a particular board*. A `Copy` snapshot is handed to each script
/// hook and exposed to Rhai (read-only) as the `Player` constant.
#[derive(Copy, Clone)]
pub struct Player {
/// The player's current health. Game-global (not per-board), so it persists
@@ -15,7 +22,7 @@ pub struct Player {
pub keys: Keyring,
/// The number of gems the player has collected. Game-global like
/// [`player_health`](GameState::player_health); starts at `0`.
/// [`health`](Player::health); starts at `0`.
pub gems: i64,
}
+18 -12
View File
@@ -81,6 +81,12 @@ use std::collections::{HashMap, HashSet, VecDeque};
use std::rc::Rc;
use crate::player::Player;
/// The host-provided context handed to every script hook for the duration of one
/// call. Currently just the player snapshot, but it exists so more host state can
/// be threaded through the `run_*` methods without changing each signature again.
#[derive(Copy, Clone)]
pub struct ScriptState(pub Player);
/// An action promoted from an object's output queue onto the board queue, tagged
/// with the object that issued it.
pub(crate) struct BoardAction {
@@ -318,18 +324,18 @@ impl ScriptHost {
}
/// Calls `init()` on every scripted object that defines it and drains each queue.
pub fn run_init(&mut self, player: Player) {
self.run("init", |c| c.has_init, (), 0.0, player);
pub fn run_init(&mut self, state: ScriptState) {
self.run("init", |c| c.has_init, (), 0.0, state);
}
/// Calls `tick(dt)` on every scripted object that defines it, then drains queues.
pub fn run_tick(&mut self, player: Player, dt: f64) {
self.run("tick", |c| c.has_tick, (dt,), dt, player);
pub fn run_tick(&mut self, state: ScriptState, dt: f64) {
self.run("tick", |c| c.has_tick, (dt,), dt, state);
}
/// Calls `bump(id)` on the object with [`ObjectId`] `object_id`, if it defines
/// the hook. After the hook, drains the object's queue with `dt = 0`.
pub fn run_bump(&mut self, player: Player, object_id: ObjectId, bumper: i64) {
pub fn run_bump(&mut self, state: ScriptState, object_id: ObjectId, bumper: i64) {
let Some(i) = self.objects.iter().position(|o| o.object_id == object_id) else {
return;
};
@@ -349,7 +355,7 @@ impl ScriptHost {
return;
}
let options = CallFnOptions::default().with_tag(object_id as i64);
obj.scope.set_or_push("Player", player);
obj.scope.set_or_push("Player", state.0);
if let Err(err) = engine.call_fn_with_options::<()>(
options,
&mut obj.scope,
@@ -372,7 +378,7 @@ impl ScriptHost {
/// Fired when the player walks onto a grab object or a grab object is pushed
/// into the player (see [`GameState`](crate::game::GameState)). The hook
/// typically increments a player stat and removes the object via `die()`.
pub fn run_grab(&mut self, player: Player, object_id: ObjectId) {
pub fn run_grab(&mut self, state: ScriptState, object_id: ObjectId) {
let Some(i) = self.objects.iter().position(|o| o.object_id == object_id) else {
return;
};
@@ -392,7 +398,7 @@ impl ScriptHost {
return;
}
let options = CallFnOptions::default().with_tag(object_id as i64);
obj.scope.set_or_push("Player", player);
obj.scope.set_or_push("Player", state.0);
if let Err(err) = engine.call_fn_with_options::<()>(
options,
&mut obj.scope,
@@ -415,7 +421,7 @@ impl ScriptHost {
/// If the function accepts zero parameters (or arg is `None`), it is called with
/// no args. If neither arity exists, the call is silently skipped.
/// After the call, drains the object's queue with `dt = 0`.
pub(crate) fn run_send(&mut self, player: Player, target_id: ObjectId, fn_name: &str, arg: Option<ScriptArg>) {
pub(crate) fn run_send(&mut self, state: ScriptState, target_id: ObjectId, fn_name: &str, arg: Option<ScriptArg>) {
let Some(i) = self.objects.iter().position(|o| o.object_id == target_id) else {
return;
};
@@ -442,7 +448,7 @@ impl ScriptHost {
.any(|f| f.name == fn_name && f.params.is_empty());
let options = CallFnOptions::default().with_tag(obj.object_id as i64);
obj.scope.set_or_push("Player", player);
obj.scope.set_or_push("Player", state.0);
let result = if has_1 {
// Call with arg (or unit if arg is absent).
let dyn_arg: Dynamic = match &arg {
@@ -530,7 +536,7 @@ impl ScriptHost {
defined: fn(&CompiledScript) -> bool,
args: A,
drain_dt: f64,
player: Player
state: ScriptState,
) {
for i in 0..self.objects.len() {
{
@@ -546,7 +552,7 @@ impl ScriptHost {
&& defined(compiled)
{
let options = CallFnOptions::default().with_tag(obj.object_id as i64);
obj.scope.set_or_push("Player", player);
obj.scope.set_or_push("Player", state.0);
if let Err(err) = engine.call_fn_with_options::<()>(
options,
&mut obj.scope,