add_gems -> alter_gems
This commit is contained in:
+2
-2
@@ -70,7 +70,7 @@ The core types that were formerly monolithic in `game.rs` are now split into foc
|
||||
- `SpeechBubble { object_id, text, remaining }` — an active speech bubble created by a script's `say(s)` call. `remaining` counts down in `GameState::tick`; when it reaches zero the bubble is removed. At most one bubble per object (a new `say()` replaces the old one).
|
||||
- `SAY_DURATION: f64` — how long a bubble lives (3.0 seconds).
|
||||
- `Scroll { source: ObjectId, lines: Vec<ScrollLine> }` — an active scroll overlay opened by a script's `scroll(lines)` call. Lives on `GameState::active_scroll: Option<Scroll>`; front-ends pause ticks while it's `Some` and close it via `close_scroll(choice)`. `ScrollLine` is re-exported from `action.rs` through `game.rs` so front-ends import both from `kiln_core::game`.
|
||||
- `GameState` — owns `world: World` (all boards as `Rc<RefCell<Board>>` + world scripts) and `current_board_name: String` (key of the active board). `pub log: Vec<LogLine>`, `scripts: ScriptHost`, `pub speech_bubbles: Vec<SpeechBubble>`, `pub active_scroll: Option<Scroll>`, and `pub player: Player` — the game-global player state ([`player::Player`]: `health`/`max_health`/`gems`/`keys`) that persists across board transitions (it is *not* per-board; `Board::player` holds only the per-board `PlayerPos`). `player.gems` is changed at runtime by the `add_gems(n)` script fn (e.g. grabbing a gem); `player.health` is changed by `alter_health(dh)` (clamped to `[0, max_health]`, e.g. grabbing a heart); `player.keys` is changed by `set_key(color, present)`. Each script-hook call is handed a `ScriptState` bundle — a `Copy` snapshot of `player` plus a shared handle to the active board — passed to the hook as its `state` parameter (Rhai type `State`, read via `state.player`/`state.board`; see `api::state`). Front-ends reach the active board through `board() -> Ref<Board>` / `board_mut() -> RefMut<Board>` (both look up `world.boards[current_board_name]`). `current_board_name() -> &str` returns the active key. **`from_world(world: World) -> Self`** is the primary constructor: clones the start board's `Rc<RefCell<Board>>` for the `ScriptHost`, compiles scripts from `world.scripts`. `new(board)` and `with_scripts(board, scripts)` are `#[cfg(test)]`-only sugar that build a minimal single-board `World`. `try_move(dir)` moves the player (pushing if possible) and fires `bump(-1)` on any solid object walked into; walking onto a `grab` thing (via `Board::grab_object_at`) instead moves onto it and fires `grab()` + an immediate `resolve()` so its `die()`/`add_gems()`/`alter_health()` apply before the call returns (no player+object overlap). **`try_move` is the only grab trigger** — a grab thing pushed or swapped into the player is an ordinary solid (it slides/blocks/refuses like any other). `run_init()` runs object `init()` hooks once at startup. `tick(dt)` advances cooldowns, expires speech bubbles, and runs `tick(dt)` hooks every frame. Both end by calling `resolve()`, which drains the board queue and applies each `Action` (`Log` → `log`, `SetTile`/`SetColor` → source glyph, `Move(dir)` → `step_object`, `Say` → `speech_bubbles`, `Scroll` → `active_scroll`, `AddGems(n)` → `player.gems`, `AlterHealth(dh)` → `player.health` clamped to `[0, max_health]`, `SetKey` → `player.keys`, `Shift` → `apply_shift`, `Push`/`Teleport` → board moves, `Send` → `run_send`, `Die` → `remove_object(source)`). Resolution is two-phase: mutate the board collecting `(bumped, bumper)` pairs, drop the borrow, then fire `run_bump` for each pair. `drain_errors()` moves script errors into `log`. `close_scroll(choice)` clears `active_scroll` and optionally fires `run_send(source, choice, None)` to dispatch the player's selection back to the object.
|
||||
- `GameState` — owns `world: World` (all boards as `Rc<RefCell<Board>>` + world scripts) and `current_board_name: String` (key of the active board). `pub log: Vec<LogLine>`, `scripts: ScriptHost`, `pub speech_bubbles: Vec<SpeechBubble>`, `pub active_scroll: Option<Scroll>`, and `pub player: Player` — the game-global player state ([`player::Player`]: `health`/`max_health`/`gems`/`keys`) that persists across board transitions (it is *not* per-board; `Board::player` holds only the per-board `PlayerPos`). `player.gems` is changed at runtime by the `alter_gems(n)` script fn (e.g. grabbing a gem); `player.health` is changed by `alter_health(dh)` (clamped to `[0, max_health]`, e.g. grabbing a heart); `player.keys` is changed by `set_key(color, present)`. Each script-hook call is handed a `ScriptState` bundle — a `Copy` snapshot of `player` plus a shared handle to the active board — passed to the hook as its `state` parameter (Rhai type `State`, read via `state.player`/`state.board`; see `api::state`). Front-ends reach the active board through `board() -> Ref<Board>` / `board_mut() -> RefMut<Board>` (both look up `world.boards[current_board_name]`). `current_board_name() -> &str` returns the active key. **`from_world(world: World) -> Self`** is the primary constructor: clones the start board's `Rc<RefCell<Board>>` for the `ScriptHost`, compiles scripts from `world.scripts`. `new(board)` and `with_scripts(board, scripts)` are `#[cfg(test)]`-only sugar that build a minimal single-board `World`. `try_move(dir)` moves the player (pushing if possible) and fires `bump(-1)` on any solid object walked into; walking onto a `grab` thing (via `Board::grab_object_at`) instead moves onto it and fires `grab()` + an immediate `resolve()` so its `die()`/`alter_gems()`/`alter_health()` apply before the call returns (no player+object overlap). **`try_move` is the only grab trigger** — a grab thing pushed or swapped into the player is an ordinary solid (it slides/blocks/refuses like any other). `run_init()` runs object `init()` hooks once at startup. `tick(dt)` advances cooldowns, expires speech bubbles, and runs `tick(dt)` hooks every frame. Both end by calling `resolve()`, which drains the board queue and applies each `Action` (`Log` → `log`, `SetTile`/`SetColor` → source glyph, `Move(dir)` → `step_object`, `Say` → `speech_bubbles`, `Scroll` → `active_scroll`, `AddGems(n)` → `player.gems`, `AlterHealth(dh)` → `player.health` clamped to `[0, max_health]`, `SetKey` → `player.keys`, `Shift` → `apply_shift`, `Push`/`Teleport` → board moves, `Send` → `run_send`, `Die` → `remove_object(source)`). Resolution is two-phase: mutate the board collecting `(bumped, bumper)` pairs, drop the borrow, then fire `run_bump` for each pair. `drain_errors()` moves script errors into `log`. `close_scroll(choice)` clears `active_scroll` and optionally fires `run_send(source, choice, None)` to dispatch the player's selection back to the object.
|
||||
|
||||
**`kiln-core/src/action.rs`** — the `Action` enum and its supporting types:
|
||||
- `MOVE_COST: f64` — how long a move occupies an object before it can act again (0.25 s).
|
||||
@@ -92,7 +92,7 @@ The core types that were formerly monolithic in `game.rs` are now split into foc
|
||||
- Lifecycle hooks, each taking `me` (an `ObjectInfo`) and `state` (a `ScriptState`) before any hook-specific arg: `init(me, state)`, `tick(me, state, dt)`, `bump(me, state, id)` (the bumper's `ObjectId`, or `-1` for the player), and `grab(me, state)` (fired when the player walks onto a `grab` thing — the only grab trigger). All optional — detected via `AST::iter_functions()` by name **and arity** (2/3/3/2). Driven by `run_init(state)` / `run_tick(state, dt)` / `run_bump(state, id, bumper)` / `run_grab(state, id)`, all funneling through `run_hook_on_one`: it builds a fresh `ObjectInfo` for the object, pushes `[me, state, (arg)]`, and calls the hook tagged with the object's id. After the call — **whether or not the hook is defined** — it always drains the object's queue, so a delay still advances on an object that has no `tick`. Runtime errors go to the error sink (drained to the log), not fatal.
|
||||
- `run_send(state, id, fn_name, arg)` — calls an arbitrary named function on an object (backs the `send()` action and scroll-choice dispatch). Picks the param list by the function's arity: 3 → `(me, state, arg)`, 2 → `(me, state)`, 1 → `(arg)`, 0 → `()`; a missing arg is `Dynamic::UNIT`. Errors if no function of that name exists.
|
||||
- **Reads** are methods/getters on the `api` types handed to the hook — `state.player.*`, `state.board.*`, `me.*` (see the `api/` section). There are **no read free functions** anymore.
|
||||
- **Write API** (`register_write_api`) — free functions that enqueue an `Action` onto the **issuing object's** `queue` (resolved from the per-call tag): `move(dir)` (enqueues `Move` then a `MOVE_COST` delay), `delay(secs)`/`now()` (queue pacing), `set_tile(n)`, `set_fg`/`set_bg`/`set_color`, `set_tag(target, tag, present)`, `log(s)`, `say(s)`/`say(s, dur)`, `scroll(lines)`, `send(target, fn [, arg])`, `teleport(x, y)`, `push(x, y, dir)`, `shift([[x, y], …])`, `add_gems(n)`, `alter_health(dh)`, `set_key(color, present)`, `die()`. `scroll(lines)` takes a Rhai array whose elements are a string (text line) or a two-element `[choice_key, display_text]` (selectable choice). `shift` takes an array of two-int `[x, y]` arrays (a malformed entry logs an error) and rotates that ring of cells via `Board::apply_shift`.
|
||||
- **Write API** (`register_write_api`) — free functions that enqueue an `Action` onto the **issuing object's** `queue` (resolved from the per-call tag): `move(dir)` (enqueues `Move` then a `MOVE_COST` delay), `delay(secs)`/`now()` (queue pacing), `set_tile(n)`, `set_fg`/`set_bg`/`set_color`, `set_tag(target, tag, present)`, `log(s)`, `say(s)`/`say(s, dur)`, `scroll(lines)`, `send(target, fn [, arg])`, `teleport(x, y)`, `push(x, y, dir)`, `shift([[x, y], …])`, `alter_gems(n)`, `alter_health(dh)`, `set_key(color, present)`, `die()`. `scroll(lines)` takes a Rhai array whose elements are a string (text line) or a two-element `[choice_key, display_text]` (selectable choice). `shift` takes an array of two-int `[x, y]` arrays (a malformed entry logs an error) and rotates that ring of cells via `Board::apply_shift`.
|
||||
- **Queue draining (two-tier):** each object's actions sit in its own `ObjQueue` until `ObjectInfo::drain(board_queue, dt)` promotes them onto the shared **board queue** (`Vec<BoardAction>`): it first eats leading `Delay` actions with `dt` (a delay longer than `dt` is decremented and stops the drain — this caps object speed), then moves the run of ready actions across. `GameState` takes the board queue with `take_board_queue()` and applies it after the batch, so scripts mutate without a `&mut GameState` borrow. Errors bypass the queues via the error sink (`take_errors()`).
|
||||
- **Constants in scope:** only `Registry` is pushed into each object's scope (the `me`/`state` views are hook *parameters* now). `register_global_constants` registers the `North`/`South`/`East`/`West` `Direction` values and the 16 named colors as a **global module**, visible at any call depth (including Rhai→Rhai calls and helper functions) — unlike scope constants such as `Registry`.
|
||||
- `Registry` (a `Board`-handle wrapper) — `Registry.get(key)` / `set(key, value)` / `get_or(key, default)` read and write the board's `registry: HashMap<String, RegistryValue>`, which **persists across board transitions**. `set(key, ())` removes a key; unsupported value types are ignored; `get_or` only returns the stored value when it matches the default's Rhai type.
|
||||
|
||||
@@ -70,7 +70,7 @@ pub struct GameState {
|
||||
/// 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.
|
||||
/// it via `alter_gems`/`alter_health`/`set_key` and read a snapshot of it.
|
||||
pub player: PlayerRef,
|
||||
}
|
||||
|
||||
@@ -477,7 +477,7 @@ impl GameState {
|
||||
return;
|
||||
}
|
||||
// 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.
|
||||
// die()/alter_gems() apply now — no player+object overlap survives this call.
|
||||
if let Some(id) = grabbed {
|
||||
self.scripts.run_grab(id);
|
||||
self.resolve();
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
//! `move(dir)`, `delay(secs)`, `now()`, `set_tile(n)`, `log(msg)`, `say(msg)`,
|
||||
//! `set_fg(fg)`, `set_bg(bg)`, `set_color(fg, bg)`, `set_tag(target, tag, present)`,
|
||||
//! `send(target_id, fn_name [, arg])`, `teleport(x, y)`, `push(x, y, dir)`,
|
||||
//! `shift([[x, y], …])`, `add_gems(n)`, `alter_health(dh)`, `set_key(color, present)`, `die()`
|
||||
//! `shift([[x, y], …])`, `alter_gems(n)`, `alter_health(dh)`, `set_key(color, present)`, `die()`
|
||||
|
||||
use crate::action::{Action, BoardAction, ScrollLine, SendArg, MOVE_COST};
|
||||
use crate::game::SAY_DURATION;
|
||||
@@ -377,9 +377,9 @@ fn register_write_api(engine: &mut Engine, board: BoardRef) {
|
||||
emit(&b, source_of(&ctx), Action::SetTile(tile as u32));
|
||||
});
|
||||
|
||||
// add_gems(n): change the player's gem count (negative subtracts; clamped at 0).
|
||||
// alter_gems(n): change the player's gem count (negative subtracts; clamped at 0).
|
||||
let b = board.clone();
|
||||
engine.register_fn("add_gems", move |ctx: NativeCallContext, n: i64| {
|
||||
engine.register_fn("alter_gems", move |ctx: NativeCallContext, n: i64| {
|
||||
emit(&b, source_of(&ctx), Action::AddGems(n));
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
// player) fires this `grab()` hook instead of blocking. We bump the player's gem
|
||||
// count and remove ourselves from the board.
|
||||
fn grab(me) {
|
||||
add_gems(1);
|
||||
alter_gems(1);
|
||||
die();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user