enter hook

This commit is contained in:
2026-07-11 11:47:45 -05:00
parent cdeae455dc
commit b1b723fd1b
10 changed files with 371 additions and 43 deletions
+1 -1
View File
@@ -130,7 +130,7 @@ script_name = "tripwire"
Palette `kind` values: the meta-kinds `empty` / `object` / `portal` / `player`, or any **archetype name** directly (`wall`, `crate`, `hcrate`, `vcrate`, `pusher_north|south|east|west`, `transporter_north|south|east|west`, `spinner_cw|spinner_ccw`, `gem`, `heart`). (`floor` is **no longer** a grid kind — it is the board-level `floor` attribute.) For archetype/object kinds, `tile`/`fg`/`bg` are optional and fall back to that kind's default glyph. An unknown `kind` becomes a visible `ErrorBlock` (logged).
Colors are `"#RRGGBB"` hex strings. The player is placed by a `kind = "player"` char, which must appear **exactly once** in the grid (missing → `(0,0)` + error; multiple → first + error); that cell is transparent. `tile` accepts a single-character string (`tile = " "`) or an integer (`tile = 35`). The grid's `content` multi-line string has its leading newline trimmed by TOML and must match `width × height` (the only hard error); the `fill`/`sparse` forms are always exactly sized. An unknown `kind` produces an `ErrorBlock` and a logged warning. An object is spawned once per occurrence of its char (uppercase by convention) in reading order. **Loading is best-effort and nonfatal** (only a grid-dimension mismatch is a hard error): each problem is recorded on `Board` (see `is_valid()` / `load_errors()`). The **player wins its cell**: it silently clears solid terrain under it and drops any conflicting solid object. Script source lives in the world-level `[scripts]` table; objects reference a script by `script_name`. Scripts may define optional `init(me, state)`, `tick(me, state, dt)`, `bump(me, dir)`, and `grab(me, state)` functions — every hook receives `me` (this object, an `ObjectInfo`). `bump`'s `dir` is the `Direction` the bump came *from* (pointing toward the bumper), fired when *any* solid — the player, another object, or a pushed crate — presses into this object. They **read** the world through `state.player.*` (e.g. `state.player.x`, `.health`, `.keys.red`), `state.board.*` (`.width`, `.height`, `.can_push(x, y, dir)`, `.passable(x, y)`, `.get(id)`, `.named(name)`, `.tagged(tag)`), and `me.*` (`me.x`, `me.y`, `me.has_tag(s)`, `me.blocked(dir)`, `me.can_push(dir)`, `me.waiting`, `me.queue`), and **write** via host functions `move(dir)`, `set_tile(n)`, `set_color(fg, bg)`, `log(s)`, `say(s)`, `scroll(lines)`, `send(target, fn [, arg])`, `set_tag(target, tag, present)`, `teleport(id, x, y)` (jump entity `id` to a cell; `me.id` = self, `-1` = player), `push(x, y, dir)` (shove a chain at arbitrary coords), `shift([[x, y], …])` (rotate a ring of cells one step), `alter_gems(n)` / `alter_health(dh)` / `set_key(color, present)` (change the player's gems / health / keys), and `die()` (remove the calling object). Writes don't take effect immediately within a hook: each is queued, and **at most one `move` resolves per 250 ms** per object. The exception is **`log(s)`**, which is *not* queued — it writes to the game log the instant it is called, so a diagnostic line surfaces even when it sits behind a pending `move`/`delay`. But each object's ready actions are **applied the moment its hook returns**, before the next object runs — objects are processed in **ascending id order** each tick, so a later object observes the board *after* every lower-id object has already moved (the one exception is actions still stuck behind a `Delay`). When two objects contend for the same cell, **lowest id wins**, and the loser — whose hook ran later — can already see the winner there. The bumped object receives `bump` with the direction the bump came from; `bump`/`send` reactions fire in a follow-up pass that settles fully within the same tick/`try_move`, bounded by a per-invocation guard that fires each `(object, hook, args)` at most once (so a bump/send cycle can't loop forever). The full scripting reference lives in [`docs/script-api.md`](docs/script-api.md).
Colors are `"#RRGGBB"` hex strings. The player is placed by a `kind = "player"` char, which must appear **exactly once** in the grid (missing → `(0,0)` + error; multiple → first + error); that cell is transparent. `tile` accepts a single-character string (`tile = " "`) or an integer (`tile = 35`). The grid's `content` multi-line string has its leading newline trimmed by TOML and must match `width × height` (the only hard error); the `fill`/`sparse` forms are always exactly sized. An unknown `kind` produces an `ErrorBlock` and a logged warning. An object is spawned once per occurrence of its char (uppercase by convention) in reading order. **Loading is best-effort and nonfatal** (only a grid-dimension mismatch is a hard error): each problem is recorded on `Board` (see `is_valid()` / `load_errors()`). The **player wins its cell**: it silently clears solid terrain under it and drops any conflicting solid object. Script source lives in the world-level `[scripts]` table; objects reference a script by `script_name`. Scripts may define optional `init(me, state)`, `tick(me, state, dt)`, `bump(me, dir)`, `enter(me, dir)`, and `grab(me, state)` functions — every hook receives `me` (this object, an `ObjectInfo`). `bump`'s `dir` is the `Direction` the bump came *from* (pointing toward the bumper), fired when *any* solid — the player, another object, or a pushed crate — presses into this object. `enter` is the **non-solid** counterpart: it fires on a non-solid object when a solid *relocates onto* its cell (a player/object step, a pushed crate, a `teleport`, or a `shift`), with `dir` the side the entrant came from — exact (`dir.opposite()`) for a one-cell move/push, best-effort (dominant axis, via `Direction::from_delta`) for a `teleport`/`shift` that jumps an arbitrary distance. They **read** the world through `state.player.*` (e.g. `state.player.x`, `.health`, `.keys.red`), `state.board.*` (`.width`, `.height`, `.can_push(x, y, dir)`, `.passable(x, y)`, `.get(id)`, `.named(name)`, `.tagged(tag)`), and `me.*` (`me.x`, `me.y`, `me.has_tag(s)`, `me.blocked(dir)`, `me.can_push(dir)`, `me.waiting`, `me.queue`), and **write** via host functions `move(dir)`, `set_tile(n)`, `set_color(fg, bg)`, `log(s)`, `say(s)`, `scroll(lines)`, `send(target, fn [, arg])`, `set_tag(target, tag, present)`, `teleport(id, x, y)` (jump entity `id` to a cell; `me.id` = self, `-1` = player), `push(x, y, dir)` (shove a chain at arbitrary coords), `shift([[x, y], …])` (rotate a ring of cells one step), `alter_gems(n)` / `alter_health(dh)` / `set_key(color, present)` (change the player's gems / health / keys), and `die()` (remove the calling object). Writes don't take effect immediately within a hook: each is queued, and **at most one `move` resolves per 250 ms** per object. The exception is **`log(s)`**, which is *not* queued — it writes to the game log the instant it is called, so a diagnostic line surfaces even when it sits behind a pending `move`/`delay`. But each object's ready actions are **applied the moment its hook returns**, before the next object runs — objects are processed in **ascending id order** each tick, so a later object observes the board *after* every lower-id object has already moved (the one exception is actions still stuck behind a `Delay`). When two objects contend for the same cell, **lowest id wins**, and the loser — whose hook ran later — can already see the winner there. The bumped object receives `bump` with the direction the bump came from; `bump`/`enter`/`send` reactions fire in a follow-up pass that settles fully within the same tick/`try_move`, bounded by a per-invocation guard that fires each `(object, hook, args)` at most once (so a bump/enter/send cycle can't loop forever). The full scripting reference lives in [`docs/script-api.md`](docs/script-api.md).
**Script state across board transitions**: Rhai `Scope` local variables reset when the `ScriptHost` is rebuilt on board entry. Board-side state (object positions, tags, glyph) is preserved because all boards are held as `Rc<RefCell<Board>>` in `World::boards`. Scripts that need to persist information across transitions should encode it in board data (e.g. `set_tag(me.id, "visited", true)`) or the per-board `Registry` (which also persists across transitions).