Real object ids, fixed warping bug
This commit is contained in:
@@ -54,11 +54,11 @@ Root `Cargo.toml`: `members = ["kiln-core", "kiln-tui"]`.
|
|||||||
- `Solid<'a>` — the single solid occupant of a cell, returned by `Board::solid_at`: `Player` (the player; pushable any direction), `Cell(Archetype)` (a solid grid archetype like a wall) or `Object(&ObjectDef)` (a solid object). At most one solid may occupy a cell — enforced at load time. The player is checked first, so its cell reports `Solid::Player` (and is impassable to movers).
|
- `Solid<'a>` — the single solid occupant of a cell, returned by `Board::solid_at`: `Player` (the player; pushable any direction), `Cell(Archetype)` (a solid grid archetype like a wall) or `Object(&ObjectDef)` (a solid object). At most one solid may occupy a cell — enforced at load time. The player is checked first, so its cell reports `Solid::Player` (and is impassable to movers).
|
||||||
- `Archetype` (`Copy`, `PartialEq`) — enum of named element types: `Empty`, `Wall`, `Crate`, `HCrate`, `VCrate`, `ErrorBlock`. Each variant provides `behavior()`, `name()` (used in map files), and `default_glyph()` (used by the editor when stamping a cell). `Crate` is solid and pushable any direction (CP437 ■, char 254, light gray on black); `HCrate`/`VCrate` are crates pushable only east/west (↔, char 29) / north/south (↕, char 18) respectively, same colors. `ErrorBlock` is a sentinel for unknown archetype names — renders as yellow `?` on red.
|
- `Archetype` (`Copy`, `PartialEq`) — enum of named element types: `Empty`, `Wall`, `Crate`, `HCrate`, `VCrate`, `ErrorBlock`. Each variant provides `behavior()`, `name()` (used in map files), and `default_glyph()` (used by the editor when stamping a cell). `Crate` is solid and pushable any direction (CP437 ■, char 254, light gray on black); `HCrate`/`VCrate` are crates pushable only east/west (↔, char 29) / north/south (↕, char 18) respectively, same colors. `ErrorBlock` is a sentinel for unknown archetype names — renders as yellow `?` on red.
|
||||||
- `ALL_ARCHETYPES: &[Archetype]` — ordered list of valid editor choices (excludes `ErrorBlock`).
|
- `ALL_ARCHETYPES: &[Archetype]` — ordered list of valid editor choices (excludes `ErrorBlock`).
|
||||||
- `Board` — the complete game unit (ZZT-style "board"): `width`, `height`, `cells: Vec<(Glyph, Archetype)>` (row-major; each cell owns its visual and behavioral class directly), `floor: Vec<Glyph>` + `floor_spec: Option<FloorSpec>` (the visual floor layer — see `floor.rs`), `player: Player`, `objects: Vec<ObjectDef>`, `portals: Vec<PortalDef>`, `font: Option<FontSpec>`. `cells`, `floor`, and `floor_spec` are `pub(crate)`. `display_glyph(x, y)` returns the static-layer glyph for a cell — the grid archetype's glyph, or the `floor[idx]` glyph when the cell is `Empty` (front-ends call this for the non-object/non-player layer; an `Empty` cell's *own* glyph is ignored, so a cell vacated by a pushed crate reveals the floor). `solid_at(x, y) -> Option<Solid>` returns the cell's single solid occupant (object checked before grid archetype); `is_passable(x, y)` is the convenience inverse (`solid_at(...).is_none()`). `in_bounds((i32, i32))` bounds-checks a (possibly negative) coordinate. Push support is split into the read-only `can_push(x, y, dir)` (does the chain of pushable solids starting here end at open space?) and the mutating `push(x, y, dir)` (shoves that chain one cell, leaving `Empty` behind); the read-only half lets a mover answer "can I move here?" via `is_passable || can_push` before committing. `is_valid()` / `load_errors()` expose nonfatal problems collected while loading (a non-serialized `Vec<LogLine>`); `report_error(msg)` appends a red-on-black line and `is_valid()` is just "no load errors".
|
- `Board` — the complete game unit (ZZT-style "board"): `width`, `height`, `cells: Vec<(Glyph, Archetype)>` (row-major; each cell owns its visual and behavioral class directly), `floor: Vec<Glyph>` + `floor_spec: Option<FloorSpec>` (the visual floor layer — see `floor.rs`), `player: Player`, `objects: BTreeMap<ObjectId, ObjectDef>` (keyed by stable id; iterates in ascending-id = load order), `next_object_id: ObjectId` (counter starting at 1), `portals: Vec<PortalDef>`, `font: Option<FontSpec>`. `cells`, `floor`, and `floor_spec` are `pub(crate)`. `ObjectId` is a `pub type ObjectId = u32` (in `game.rs`). `add_object(obj) -> ObjectId` allocates the next id, inserts, and returns it; `object_id_at(x, y) -> Option<ObjectId>` and `object_at(x, y) -> Option<&ObjectDef>` look up by position. `display_glyph(x, y)` returns the static-layer glyph for a cell — the grid archetype's glyph, or the `floor[idx]` glyph when the cell is `Empty` (front-ends call this for the non-object/non-player layer; an `Empty` cell's *own* glyph is ignored, so a cell vacated by a pushed crate reveals the floor). `solid_at(x, y) -> Option<Solid>` returns the cell's single solid occupant (object checked before grid archetype); `is_passable(x, y)` is the convenience inverse (`solid_at(...).is_none()`). `in_bounds((i32, i32))` bounds-checks a (possibly negative) coordinate. Push support is split into the read-only `can_push(x, y, dir)` (does the chain of pushable solids starting here end at open space?) and the mutating `push(x, y, dir)` (shoves that chain one cell, leaving `Empty` behind); the read-only half lets a mover answer "can I move here?" via `is_passable || can_push` before committing. `is_valid()` / `load_errors()` expose nonfatal problems collected while loading (a non-serialized `Vec<LogLine>`); `report_error(msg)` appends a red-on-black line and `is_valid()` is just "no load errors".
|
||||||
- `Player` — `x: i32, y: i32`
|
- `Player` — `x: i32, y: i32`
|
||||||
- `ObjectDef` — scripted object placed on the board: `x`, `y`, `glyph: Glyph`, `solid: bool`, `opaque: bool`, `pushable: bool`, `script_name: Option<String>`. `solid` and `opaque` default `true`, `pushable` defaults `false` in map files. Its `init()`/`tick(dt)`/`bump(id)` hooks are run by the scripting runtime (see `script.rs`).
|
- `ObjectDef` — scripted object placed on the board: `x`, `y`, `glyph: Glyph`, `solid: bool`, `opaque: bool`, `pushable: bool`, `script_name: Option<String>`. `solid` and `opaque` default `true`, `pushable` defaults `false` in map files. Its `init()`/`tick(dt)`/`bump(id)` hooks are run by the scripting runtime (see `script.rs`).
|
||||||
- `PortalDef` — parsed from map files, stored on Board; not yet runtime-wired
|
- `PortalDef` — parsed from map files, stored on Board; not yet runtime-wired
|
||||||
- `GameState` — holds `board: Rc<RefCell<Board>>`, the message `log: Vec<LogLine>`, and a `ScriptHost`. The board sits behind `Rc<RefCell<…>>` as a sibling of the `ScriptHost` so script host functions can hold a shared handle to it without aliasing the borrow that's running the engine. Front-ends/logic reach the board through `board() -> Ref<Board>` / `board_mut() -> RefMut<Board>` (there is no `pub board` field). `try_move(dir: Direction)` moves the player (pushing a crate/solid out of the way if possible) and fires `bump(-1)` on a solid object it walks into; `run_init()` runs object `init()` hooks once at startup; `tick(dt)` advances each object's cooldown then runs `tick(dt)` hooks every frame. Both end by calling `resolve()`, which drains the board queue (`take_board_queue()`) and applies each `Action`: `Log` → `log`, `SetTile` → the source object's glyph, `Move(dir)` → the free fn `step_object` (gates on `in_bounds` then `is_passable || can_push`, pushing before it relocates, and returns the index of any solid object it bumped). Resolution is two-phase — mutate the board collecting `(bumped, bumper)` pairs, then drop the board borrow and fire `run_bump` for each (a `bump` script reads `Board.*`, so no `board_mut` borrow may be held while it runs). A bumped object's own emitted actions wait for the next tick's pump (no same-tick cascade). `drain_errors()` moves the script error sink into `log`. This deferred apply (writes after the batch) is what keeps script execution borrow-safe.
|
- `GameState` — holds `board: Rc<RefCell<Board>>`, the message `log: Vec<LogLine>`, and a `ScriptHost`. The board sits behind `Rc<RefCell<…>>` as a sibling of the `ScriptHost` so script host functions can hold a shared handle to it without aliasing the borrow that's running the engine. Front-ends/logic reach the board through `board() -> Ref<Board>` / `board_mut() -> RefMut<Board>` (there is no `pub board` field). `try_move(dir: Direction)` moves the player (pushing a crate/solid out of the way if possible) and fires `bump(-1)` on a solid object it walks into; `run_init()` runs object `init()` hooks once at startup; `tick(dt)` advances each object's cooldown then runs `tick(dt)` hooks every frame. Both end by calling `resolve()`, which drains the board queue (`take_board_queue()`) and applies each `Action`: `Log` → `log`, `SetTile` → the source object's glyph, `Move(dir)` → the free fn `step_object` (gates on `in_bounds` then `is_passable || can_push`, pushing before it relocates, and returns the `ObjectId` of any solid object it bumped). Resolution is two-phase — mutate the board collecting `(bumped, bumper)` pairs, then drop the board borrow and fire `run_bump` for each (a `bump` script reads `Board.*`, so no `board_mut` borrow may be held while it runs). A bumped object's own emitted actions wait for the next tick's pump (no same-tick cascade). `drain_errors()` moves the script error sink into `log`. This deferred apply (writes after the batch) is what keeps script execution borrow-safe.
|
||||||
|
|
||||||
**`kiln-core/src/floor.rs`** — the visual floor layer:
|
**`kiln-core/src/floor.rs`** — the visual floor layer:
|
||||||
- The floor is a cosmetic per-cell background drawn wherever a cell would render as `Archetype::Empty` (it is *how* `Empty` is drawn; the cell's own `Empty` glyph is ignored). Computed once at load and cached on `Board::floor`, so there is no per-frame cost / flicker; the raw `FloorSpec` is also kept (`Board::floor_spec`) so `map_file::save` round-trips it.
|
- The floor is a cosmetic per-cell background drawn wherever a cell would render as `Archetype::Empty` (it is *how* `Empty` is drawn; the cell's own `Empty` glyph is ignored). Computed once at load and cached on `Board::floor`, so there is no per-frame cost / flicker; the raw `FloorSpec` is also kept (`Board::floor_spec`) so `map_file::save` round-trips it.
|
||||||
@@ -71,13 +71,13 @@ Root `Cargo.toml`: `members = ["kiln-core", "kiln-tui"]`.
|
|||||||
|
|
||||||
**`kiln-core/src/script.rs`** — Rhai scripting runtime:
|
**`kiln-core/src/script.rs`** — Rhai scripting runtime:
|
||||||
- `ScriptHost` — owns the Rhai `Engine`, the compiled scripts referenced by a board's objects (compiled once per name), a per-object persistent `Scope` + output queue + ready timer, and the shared board queue. Built with `ScriptHost::new(&Rc<RefCell<Board>>)` (registers the API, compiles scripts, reports compile/unknown-script failures onto the error sink; runs nothing).
|
- `ScriptHost` — owns the Rhai `Engine`, the compiled scripts referenced by a board's objects (compiled once per name), a per-object persistent `Scope` + output queue + ready timer, and the shared board queue. Built with `ScriptHost::new(&Rc<RefCell<Board>>)` (registers the API, compiles scripts, reports compile/unknown-script failures onto the error sink; runs nothing).
|
||||||
- Lifecycle hooks per object: `init()` (zero-arg), `tick(dt)` (elapsed seconds as `f64`), and `bump(id)` (the bumper's object index, or `-1` for the player), all optional — detected via `AST::iter_functions()` by name and arity. Driven by `run_init()` / `run_tick(dt)` / `run_bump(idx, id)`; runtime errors go to the error sink (drained to the log), not fatal.
|
- Lifecycle hooks per object: `init()` (zero-arg), `tick(dt)` (elapsed seconds as `f64`), and `bump(id)` (the bumper's `ObjectId`, or `-1` for the player), all optional — detected via `AST::iter_functions()` by name and arity. Driven by `run_init()` / `run_tick(dt)` / `run_bump(object_id, bumper)`; runtime errors go to the error sink (drained to the log), not fatal.
|
||||||
- **Reads (direct):** read getters (`player_x`, `player_y`, `width`, `height`) are registered on `Rc<RefCell<Board>>` (the `BoardRef` alias, exposed to Rhai under the type name `Board`) — getters only, so read-only by construction. A clone of the handle is pushed into each scope as the constant `Board`. `blocked(dir) -> bool` is also a read fn: true if the caller's target cell is off-board, already solid, or the destination of a pending move on the board queue (an object never sees its *own* move, which is pumped only after its script returns). Each getter briefly borrows the shared `Board`.
|
- **Reads (direct):** read getters (`player_x`, `player_y`, `width`, `height`) are registered on `Rc<RefCell<Board>>` (the `BoardRef` alias, exposed to Rhai under the type name `Board`) — getters only, so read-only by construction. A clone of the handle is pushed into each scope as the constant `Board`. `blocked(dir) -> bool` is also a read fn: true if the caller's target cell is off-board, already solid, or the destination of a pending move on the board queue (an object never sees its *own* move, which is pumped only after its script returns). Each getter briefly borrows the shared `Board`.
|
||||||
- **Actions & rate limiting (two-tier queues):** host fns `move(dir)`, `set_tile(n)`, `log(s)` append an `Action` (`Move`/`SetTile`/`Log`; `Action::time_cost()` is `MOVE_COST = 0.25` s for `Move`, else 0) to the *issuing object's* output queue (routed by the per-call tag via a `HashMap<usize, ObjQueue>`). `ScriptHost::pump(i)` promotes ready actions onto the shared **board queue** (`Vec<BoardAction { source, action }>`): the leading run of zero-cost actions plus at most one timed action, which arms that object's **ready timer** and ends the pump. While a ready timer is `> 0` nothing is pulled (this caps object speed); `advance_timers(dt)` counts them down each frame. `GameState` drains the board queue with `take_board_queue()` and applies it after the batch — so scripts mutate without a `&mut GameState` borrow. Errors (compile/runtime) bypass the queues via the error sink (`take_errors()`).
|
- **Actions & rate limiting (two-tier queues):** host fns `move(dir)`, `set_tile(n)`, `log(s)` append an `Action` (`Move`/`SetTile`/`Log`; `Action::time_cost()` is `MOVE_COST = 0.25` s for `Move`, else 0) to the *issuing object's* output queue (routed by the per-call tag via a `HashMap<usize, ObjQueue>`). `ScriptHost::pump(i)` promotes ready actions onto the shared **board queue** (`Vec<BoardAction { source, action }>`): the leading run of zero-cost actions plus at most one timed action, which arms that object's **ready timer** and ends the pump. While a ready timer is `> 0` nothing is pulled (this caps object speed); `advance_timers(dt)` counts them down each frame. `GameState` drains the board queue with `take_board_queue()` and applies it after the batch — so scripts mutate without a `&mut GameState` borrow. Errors (compile/runtime) bypass the queues via the error sink (`take_errors()`).
|
||||||
- The `Queue` object (a handle to the calling object's output queue) is pushed into each scope; scripts call `Queue.length()` and `Queue.clear()`. Safe to mutate from script because the host only touches output queues between calls (during `pump`).
|
- The `Queue` object (a handle to the calling object's output queue) is pushed into each scope; scripts call `Queue.length()` and `Queue.clear()`. Safe to mutate from script because the host only touches output queues between calls (during `pump`).
|
||||||
- **Sender identity:** `source` (which object issued an action) rides the per-call **tag** — `run` calls `call_fn_with_options(...with_tag(object_index)...)` and the host fns read it via `NativeCallContext::tag()`. Scripts write `move(North)` without naming themselves; `North`/`South`/`East`/`West` are `Direction` constants in scope, and `impl From<Direction> for (i32,i32)` gives the delta.
|
- **Sender identity:** `source` (which object issued an action) rides the per-call **tag** — `run` calls `call_fn_with_options(...with_tag(object_id)...)` and the host fns read it via `NativeCallContext::tag()` (decoded back to an `ObjectId` by `source_of`). Scripts write `move(North)` without naming themselves; `North`/`South`/`East`/`West` are `Direction` constants in scope, and `impl From<Direction> for (i32,i32)` gives the delta.
|
||||||
- `GameState` (hence the `Engine`/`Scope`) is single-threaded / not `Send`; fine for kiln-tui.
|
- `GameState` (hence the `Engine`/`Scope`) is single-threaded / not `Send`; fine for kiln-tui.
|
||||||
- **TODO in-code:** `BoardAction.source` (and `ObjectRuntime.object_index`, and the `QueueMap` keys) are array indices into `Board::objects` — a stopgap that breaks under object spawn/destroy/reorder; should become a monotonically-increasing unique object id with objects keyed by it.
|
- **Sender identity uses stable ids:** `BoardAction.source`, `ObjectRuntime.object_id`, and the `QueueMap` keys are all the object's `ObjectId` (matching `Board::objects`' `BTreeMap` keys), so they survive object spawn/destroy/reorder. The per-call tag carries the id as an `i64` (id 0 — never valid — is the no-source fallback). `ScriptHost::objects` is still a `Vec<ObjectRuntime>` built by iterating the board map, so it stays in ascending-id order.
|
||||||
|
|
||||||
**`kiln-core/src/map_file.rs`** — map file loading:
|
**`kiln-core/src/map_file.rs`** — map file loading:
|
||||||
- `MapFile` and friends — serde `Deserialize` types for TOML map files
|
- `MapFile` and friends — serde `Deserialize` types for TOML map files
|
||||||
@@ -249,7 +249,7 @@ fn init() { # optional, run once at startup
|
|||||||
fn tick(dt) {
|
fn tick(dt) {
|
||||||
if Queue.length() == 0 && !blocked(North) { move(North); }
|
if Queue.length() == 0 && !blocked(North) { move(North); }
|
||||||
}
|
}
|
||||||
# optional, fires when something steps into this object; id = bumper's array index,
|
# optional, fires when something steps into this object; id = bumper's object id,
|
||||||
# or -1 for the player.
|
# or -1 for the player.
|
||||||
fn bump(id) { log(`bumped by ${id}`); }
|
fn bump(id) { log(`bumped by ${id}`); }
|
||||||
"""
|
"""
|
||||||
@@ -257,7 +257,7 @@ fn bump(id) { log(`bumped by ${id}`); }
|
|||||||
|
|
||||||
The optional `[floor]` section declares the visual floor layer (drawn under every empty cell, and revealed when a crate/object is pushed away); see `floor.rs`. It takes one of four forms: omitted (black-on-black space, the default), a generator name (`floor = "grass"`), a single fixed glyph (`[floor]` with `tile`/`fg`/`bg`), or a grid (`[floor] content` + `[floor.palette]`, whose entries are generator names or fixed glyphs). The three built-in generators are `grass`/`dirt`/`stone`. Floor parsing is best-effort: an unknown generator/char or a grid-dimension mismatch is recorded on `Board::load_errors` and those cells fall back to the default empty glyph.
|
The optional `[floor]` section declares the visual floor layer (drawn under every empty cell, and revealed when a crate/object is pushed away); see `floor.rs`. It takes one of four forms: omitted (black-on-black space, the default), a generator name (`floor = "grass"`), a single fixed glyph (`[floor]` with `tile`/`fg`/`bg`), or a grid (`[floor] content` + `[floor.palette]`, whose entries are generator names or fixed glyphs). The three built-in generators are `grass`/`dirt`/`stone`. Floor parsing is best-effort: an unknown generator/char or a grid-dimension mismatch is recorded on `Board::load_errors` and those cells fall back to the default empty glyph.
|
||||||
|
|
||||||
Colors are `"#RRGGBB"` hex strings. `player_start` accepts either `[x, y]` coordinates **or** a single grid char to locate (convention `"@"`; that cell loads as `Empty`, like an object placeholder). The `tile` field accepts either a single-character string (`tile = " "`) or an integer (`tile = 35`); both are valid and existing char-style map files continue to work. The grid multi-line string's leading newline is trimmed by TOML; trailing newline is handled correctly by `str::lines()`. Unknown archetype names produce an `ErrorBlock` cell and a logged warning, as does any grid character that is neither a `[palette]` key nor an object/player `palette` char. Objects may be placed by `x`/`y` or by a single grid `palette` char (uppercase by convention). **Loading is best-effort and nonfatal** (only a grid-dimension mismatch is a hard error): a placement char that appears multiple times uses the first occurrence, a missing object char drops that object, a missing player char falls back to `(0, 0)`, and one-solid-per-cell conflicts drop the later solid — each problem is recorded on `Board` (see `is_valid()` / `load_errors()`). The **player joins one-solid-per-cell** and *wins* its cell: it is placed first, silently clearing solid terrain under it and dropping any solid object that lands there. Script source lives in the `[scripts]` table (name → Rhai source); objects reference a script by `script_name`. Scripts may define optional `init()`, `tick(dt)`, and `bump(id)` functions; they **read** the world through `Board.*` (e.g. `Board.player_x`), check `blocked(dir) -> bool`, inspect/empty their pending actions via `Queue.length()`/`Queue.clear()`, and **write** via host functions `move(dir)` (`dir` ∈ `North`/`South`/`East`/`West`), `set_tile(n)`, and `log(s)`. Writes don't take effect immediately: each is queued and **at most one `move` resolves per 250 ms** per object (a blocked move still costs the cooldown), so objects can't act infinitely fast. When two objects move into the same cell on one tick, **array order wins** (the earlier object lands; the later is pushed or blocked) and the object that was moved into receives `bump(id)`.
|
Colors are `"#RRGGBB"` hex strings. `player_start` accepts either `[x, y]` coordinates **or** a single grid char to locate (convention `"@"`; that cell loads as `Empty`, like an object placeholder). The `tile` field accepts either a single-character string (`tile = " "`) or an integer (`tile = 35`); both are valid and existing char-style map files continue to work. The grid multi-line string's leading newline is trimmed by TOML; trailing newline is handled correctly by `str::lines()`. Unknown archetype names produce an `ErrorBlock` cell and a logged warning, as does any grid character that is neither a `[palette]` key nor an object/player `palette` char. Objects may be placed by `x`/`y` or by a single grid `palette` char (uppercase by convention). **Loading is best-effort and nonfatal** (only a grid-dimension mismatch is a hard error): a placement char that appears multiple times uses the first occurrence, a missing object char drops that object, a missing player char falls back to `(0, 0)`, and one-solid-per-cell conflicts drop the later solid — each problem is recorded on `Board` (see `is_valid()` / `load_errors()`). The **player joins one-solid-per-cell** and *wins* its cell: it is placed first, silently clearing solid terrain under it and dropping any solid object that lands there. Script source lives in the `[scripts]` table (name → Rhai source); objects reference a script by `script_name`. Scripts may define optional `init()`, `tick(dt)`, and `bump(id)` functions; they **read** the world through `Board.*` (e.g. `Board.player_x`), check `blocked(dir) -> bool`, inspect/empty their pending actions via `Queue.length()`/`Queue.clear()`, and **write** via host functions `move(dir)` (`dir` ∈ `North`/`South`/`East`/`West`), `set_tile(n)`, and `log(s)`. Writes don't take effect immediately: each is queued and **at most one `move` resolves per 250 ms** per object (a blocked move still costs the cooldown), so objects can't act infinitely fast. When two objects move into the same cell on one tick, **lowest id wins** (= load order; the earlier-loaded object lands, the later is pushed or blocked) and the object that was moved into receives `bump(id)`.
|
||||||
|
|
||||||
### Key design decisions
|
### Key design decisions
|
||||||
|
|
||||||
@@ -285,6 +285,6 @@ Today's baked-in assumptions that will need to change (don't make `Board.player`
|
|||||||
### Other not-yet-implemented threads
|
### Other not-yet-implemented threads
|
||||||
|
|
||||||
- **Scripting growth** — more event hooks beyond `init`/`tick`/`bump` (e.g. `on_touch` when the player steps onto an object), a larger action/read vocabulary (more actions could carry a `time_cost`), and persisting script-local state across ticks (needs `call_fn_raw` so the per-object `Scope` isn't rewound each call).
|
- **Scripting growth** — more event hooks beyond `init`/`tick`/`bump` (e.g. `on_touch` when the player steps onto an object), a larger action/read vocabulary (more actions could carry a `time_cost`), and persisting script-local state across ticks (needs `call_fn_raw` so the per-object `Scope` isn't rewound each call).
|
||||||
- **Stable object ids** — `BoardAction.source` / `ObjectRuntime.object_index` / `QueueMap` keys / `bump` ids are array indices into `Board::objects`, which break under spawn/destroy/reorder; replace with monotonic unique ids (a `// TODO` marks this in `script.rs`).
|
- **Runtime spawn/destroy of objects** — objects now have stable `ObjectId`s (`Board::objects` is a `BTreeMap<ObjectId, ObjectDef>` with a `next_object_id` counter; `add_object` allocates), so references survive reordering. What's still missing: a `remove_object`, and wiring live spawn/destroy into the `ScriptHost` (it builds one `ObjectRuntime` per object once in `GameState::new`, so an object added after that has no script runtime until the host is rebuilt).
|
||||||
- **Portals & multi-board** — `PortalDef` is parsed but not runtime-wired; there is no multi-board world/loading yet (the engine loads a single map).
|
- **Portals & multi-board** — `PortalDef` is parsed but not runtime-wired; there is no multi-board world/loading yet (the engine loads a single map).
|
||||||
- **Load-error surfacing** — `Board::load_errors()` is collected but no front-end displays it yet (kiln-tui could append it to the in-game log at startup).
|
- **Load-error surfacing** — `Board::load_errors()` is collected but no front-end displays it yet (kiln-tui could append it to the in-game log at startup).
|
||||||
|
|||||||
+98
-52
@@ -3,7 +3,7 @@ use crate::script::{Action, Direction, ScriptHost};
|
|||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::cell::{Ref, RefCell, RefMut};
|
use std::cell::{Ref, RefCell, RefMut};
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -280,6 +280,15 @@ pub const ALL_ARCHETYPES: &[Archetype] = &[
|
|||||||
Archetype::VCrate,
|
Archetype::VCrate,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// A stable, unique identifier for a board object.
|
||||||
|
///
|
||||||
|
/// Ids are handed out by [`Board::add_object`] from the per-board
|
||||||
|
/// [`Board::next_object_id`] counter (starting at 1) and never reused, so a
|
||||||
|
/// reference to an object stays valid even as other objects are added or removed.
|
||||||
|
/// This replaces the old "index into `Board::objects`" scheme, which shifted when
|
||||||
|
/// objects were reordered/destroyed.
|
||||||
|
pub type ObjectId = u32;
|
||||||
|
|
||||||
/// A scripted object placed on the board, loaded from a map file.
|
/// A scripted object placed on the board, loaded from a map file.
|
||||||
///
|
///
|
||||||
/// `ObjectDef` represents a tile that has Rhai script attached to it.
|
/// `ObjectDef` represents a tile that has Rhai script attached to it.
|
||||||
@@ -440,8 +449,14 @@ pub struct Board {
|
|||||||
pub(crate) floor_spec: Option<crate::floor::FloorSpec>,
|
pub(crate) floor_spec: Option<crate::floor::FloorSpec>,
|
||||||
/// Current player position. See [`Player`] for caveats about its future.
|
/// Current player position. See [`Player`] for caveats about its future.
|
||||||
pub player: Player,
|
pub player: Player,
|
||||||
/// Scripted objects on this board. Parsed from the map file; not yet active.
|
/// Scripted objects on this board, keyed by stable [`ObjectId`]. A `BTreeMap`
|
||||||
pub objects: Vec<ObjectDef>,
|
/// (not a `Vec`) so an object can be removed without invalidating other
|
||||||
|
/// objects' ids; iteration is in ascending-id order, which equals load order
|
||||||
|
/// (ids are assigned sequentially as the map loads).
|
||||||
|
pub objects: BTreeMap<ObjectId, ObjectDef>,
|
||||||
|
/// The next [`ObjectId`] to hand out (starts at 1, monotonically increasing).
|
||||||
|
/// See [`Board::add_object`].
|
||||||
|
pub next_object_id: ObjectId,
|
||||||
/// Portals on this board. Parsed from the map file; not yet active.
|
/// Portals on this board. Parsed from the map file; not yet active.
|
||||||
pub portals: Vec<PortalDef>,
|
pub portals: Vec<PortalDef>,
|
||||||
/// Optional font override for this board. When `None`, the app default is used.
|
/// Optional font override for this board. When `None`, the app default is used.
|
||||||
@@ -545,8 +560,7 @@ impl Board {
|
|||||||
return Some(Solid::Player);
|
return Some(Solid::Player);
|
||||||
}
|
}
|
||||||
// A solid object shadows the grid cell it sits on.
|
// A solid object shadows the grid cell it sits on.
|
||||||
if let Some(obj) = self.object_at(x, y)
|
if let Some(obj) = self.solid_object_at(x, y)
|
||||||
&& obj.solid
|
|
||||||
{
|
{
|
||||||
return Some(Solid::Object(obj));
|
return Some(Solid::Object(obj));
|
||||||
}
|
}
|
||||||
@@ -645,11 +659,11 @@ impl Board {
|
|||||||
if self.player.x == x as i32 && self.player.y == y as i32 {
|
if self.player.x == x as i32 && self.player.y == y as i32 {
|
||||||
self.player.x = tx as i32;
|
self.player.x = tx as i32;
|
||||||
self.player.y = ty as i32;
|
self.player.y = ty as i32;
|
||||||
} else if let Some(idx) = self.object_index_at(x, y)
|
} else if let Some(id) = self.solid_object_id_at(x, y)
|
||||||
&& self.objects[idx].solid
|
|
||||||
{
|
{
|
||||||
self.objects[idx].x = tx;
|
let obj = self.objects.get_mut(&id).expect("id from object_id_at");
|
||||||
self.objects[idx].y = ty;
|
obj.x = tx;
|
||||||
|
obj.y = ty;
|
||||||
} else {
|
} else {
|
||||||
let moved = *self.get(x, y);
|
let moved = *self.get(x, y);
|
||||||
*self.get_mut(tx, ty) = moved;
|
*self.get_mut(tx, ty) = moved;
|
||||||
@@ -657,14 +671,36 @@ impl Board {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the index into [`Board::objects`] of the object at `(x, y)`, if any.
|
/// Returns the [`ObjectId`]s of the objects at `(x, y)`, if any.
|
||||||
pub fn object_index_at(&self, x: usize, y: usize) -> Option<usize> {
|
pub fn object_ids_at(&self, x: usize, y: usize) -> Vec<ObjectId> {
|
||||||
self.objects.iter().position(|o| o.x == x && o.y == y)
|
self.objects
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, o)| o.x == x && o.y == y)
|
||||||
|
.map(|(&id, _)| id).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a borrow of the actual object at `(x, y)` if any
|
/// Returns a borrow of the actual object at `(x, y)` if any
|
||||||
pub fn object_at(&self, x: usize, y: usize) -> Option<&ObjectDef> {
|
pub fn solid_object_at(&self, x: usize, y: usize) -> Option<&ObjectDef> {
|
||||||
self.object_index_at(x, y).map(|idx| &self.objects[idx])
|
self.objects.values().find(|o| o.x == x && o.y == y && o.solid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a borrow of the actual object at `(x, y)` if any
|
||||||
|
pub fn solid_object_id_at(&self, x: usize, y: usize) -> Option<ObjectId> {
|
||||||
|
self.objects
|
||||||
|
.iter()
|
||||||
|
.find(|(id, o)| o.x == x && o.y == y && o.solid)
|
||||||
|
.map(|(&id, _)| id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts `object`, assigning it the next free [`ObjectId`], and returns that id.
|
||||||
|
///
|
||||||
|
/// Ids start at 1 and increase monotonically; an id is never reused, so it
|
||||||
|
/// stays a valid handle to this object for the board's lifetime.
|
||||||
|
pub fn add_object(&mut self, object: ObjectDef) -> ObjectId {
|
||||||
|
let id = self.next_object_id;
|
||||||
|
self.next_object_id += 1;
|
||||||
|
self.objects.insert(id, object);
|
||||||
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,7 +795,7 @@ impl GameState {
|
|||||||
// Logs are collected here rather than pushed inline, since the board borrow
|
// Logs are collected here rather than pushed inline, since the board borrow
|
||||||
// below also borrows `self`.
|
// below also borrows `self`.
|
||||||
let mut logs: Vec<LogLine> = Vec::new();
|
let mut logs: Vec<LogLine> = Vec::new();
|
||||||
let mut bumps: Vec<(usize, i64)> = Vec::new();
|
let mut bumps: Vec<(ObjectId, i64)> = Vec::new();
|
||||||
{
|
{
|
||||||
let mut board = self.board_mut();
|
let mut board = self.board_mut();
|
||||||
for ba in actions {
|
for ba in actions {
|
||||||
@@ -770,7 +806,7 @@ impl GameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::SetTile(tile) => {
|
Action::SetTile(tile) => {
|
||||||
if let Some(obj) = board.objects.get_mut(ba.source) {
|
if let Some(obj) = board.objects.get_mut(&ba.source) {
|
||||||
obj.glyph.tile = tile;
|
obj.glyph.tile = tile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -803,7 +839,7 @@ impl GameState {
|
|||||||
let (nx, ny) = (target.0 as usize, target.1 as usize);
|
let (nx, ny) = (target.0 as usize, target.1 as usize);
|
||||||
// A solid object in the way is bumped by the player (id -1).
|
// A solid object in the way is bumped by the player (id -1).
|
||||||
bumped = match board.solid_at(nx, ny) {
|
bumped = match board.solid_at(nx, ny) {
|
||||||
Some(Solid::Object(_)) => board.object_index_at(nx, ny),
|
Some(Solid::Object(_)) => board.solid_object_id_at(nx, ny),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
|
if board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
|
||||||
@@ -819,30 +855,30 @@ impl GameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Moves object `idx` one cell in `dir` on `board`, returning the index of a solid
|
/// Moves object `id` one cell in `dir` on `board`, returning the [`ObjectId`] of a
|
||||||
/// object it bumped (its target cell was occupied by that object), if any.
|
/// solid object it bumped (its target cell was occupied by that object), if any.
|
||||||
///
|
///
|
||||||
/// The move itself proceeds only if the target is in bounds and either passable or a
|
/// 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
|
/// 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
|
/// 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
|
/// 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.
|
/// crates carry no script, so only solid objects yield a bump.
|
||||||
fn step_object(board: &mut Board, idx: usize, dir: Direction) -> Option<usize> {
|
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<ObjectId> {
|
||||||
let (dx, dy): (i32, i32) = dir.into();
|
let (dx, dy): (i32, i32) = dir.into();
|
||||||
let (ox, oy) = board.objects.get(idx).map(|o| (o.x, o.y))?;
|
let (ox, oy) = board.objects.get(&id).map(|o| (o.x, o.y))?;
|
||||||
let target = (ox as i32 + dx, oy as i32 + dy);
|
let target = (ox as i32 + dx, oy as i32 + dy);
|
||||||
if !board.in_bounds(target) {
|
if !board.in_bounds(target) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let (nx, ny) = (target.0 as usize, target.1 as usize);
|
let (nx, ny) = (target.0 as usize, target.1 as usize);
|
||||||
// Capture the bumped object before any push relocates it (its index is stable).
|
// Capture the bumped object before any push relocates it (its id is stable).
|
||||||
let bumped = match board.solid_at(nx, ny) {
|
let bumped = match board.solid_at(nx, ny) {
|
||||||
Some(Solid::Object(_)) => board.object_index_at(nx, ny),
|
Some(Solid::Object(_)) => board.solid_object_id_at(nx, ny),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
|
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.push(nx, ny, dir); // shoves a crate/object out of the way; no-op otherwise
|
||||||
let obj = &mut board.objects[idx];
|
let obj = board.objects.get_mut(&id).expect("id checked above");
|
||||||
obj.x = nx;
|
obj.x = nx;
|
||||||
obj.y = ny;
|
obj.y = ny;
|
||||||
}
|
}
|
||||||
@@ -866,7 +902,8 @@ mod tests {
|
|||||||
floor: vec![Archetype::Empty.default_glyph()],
|
floor: vec![Archetype::Empty.default_glyph()],
|
||||||
floor_spec: None,
|
floor_spec: None,
|
||||||
player: Player { x: 0, y: 0 },
|
player: Player { x: 0, y: 0 },
|
||||||
objects: vec![object],
|
objects: BTreeMap::from([(1, object)]),
|
||||||
|
next_object_id: 2,
|
||||||
portals: Vec::new(),
|
portals: Vec::new(),
|
||||||
font: None,
|
font: None,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
@@ -896,6 +933,14 @@ mod tests {
|
|||||||
objects: Vec<ObjectDef>,
|
objects: Vec<ObjectDef>,
|
||||||
scripts: &[(&str, &str)],
|
scripts: &[(&str, &str)],
|
||||||
) -> Board {
|
) -> Board {
|
||||||
|
// Assign sequential ids (1..=n) just like the map loader, so tests that
|
||||||
|
// index by id can rely on first-object == id 1.
|
||||||
|
let mut object_map: BTreeMap<ObjectId, ObjectDef> = BTreeMap::new();
|
||||||
|
let mut next_object_id: ObjectId = 1;
|
||||||
|
for o in objects {
|
||||||
|
object_map.insert(next_object_id, o);
|
||||||
|
next_object_id += 1;
|
||||||
|
}
|
||||||
Board {
|
Board {
|
||||||
name: "test".into(),
|
name: "test".into(),
|
||||||
width: w,
|
width: w,
|
||||||
@@ -907,7 +952,8 @@ mod tests {
|
|||||||
x: player.0,
|
x: player.0,
|
||||||
y: player.1,
|
y: player.1,
|
||||||
},
|
},
|
||||||
objects,
|
objects: object_map,
|
||||||
|
next_object_id,
|
||||||
portals: Vec::new(),
|
portals: Vec::new(),
|
||||||
font: None,
|
font: None,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
@@ -935,7 +981,7 @@ mod tests {
|
|||||||
let mut board = open_board(4, 1, (3, 0), vec![], &[]);
|
let mut board = open_board(4, 1, (3, 0), vec![], &[]);
|
||||||
board.cells[1] = (Archetype::Wall.default_glyph(), Archetype::Wall);
|
board.cells[1] = (Archetype::Wall.default_glyph(), Archetype::Wall);
|
||||||
// A solid object on the otherwise-empty cell (2, 0).
|
// A solid object on the otherwise-empty cell (2, 0).
|
||||||
board.objects.push(ObjectDef::new(2, 0)); // solid by default
|
board.add_object(ObjectDef::new(2, 0)); // solid by default
|
||||||
|
|
||||||
// Empty floor: nothing solid.
|
// Empty floor: nothing solid.
|
||||||
assert!(board.solid_at(0, 0).is_none());
|
assert!(board.solid_at(0, 0).is_none());
|
||||||
@@ -1078,7 +1124,7 @@ mod tests {
|
|||||||
game.try_move(Direction::East);
|
game.try_move(Direction::East);
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.player.x, b.player.y), (1, 0));
|
assert_eq!((b.player.x, b.player.y), (1, 0));
|
||||||
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 0)); // object shoved east
|
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object shoved east
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1095,7 +1141,7 @@ mod tests {
|
|||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.run_init();
|
game.run_init();
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 0)); // object advanced
|
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object advanced
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::Empty); // crate left (2,0)
|
assert_eq!(b.get(2, 0).1, Archetype::Empty); // crate left (2,0)
|
||||||
assert_eq!(b.get(3, 0).1, Archetype::Crate); // crate pushed to (3,0)
|
assert_eq!(b.get(3, 0).1, Archetype::Crate); // crate pushed to (3,0)
|
||||||
}
|
}
|
||||||
@@ -1268,7 +1314,7 @@ mod tests {
|
|||||||
game.run_init();
|
game.run_init();
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
// East increments x by one; the object started at (2, 1).
|
// East increments x by one; the object started at (2, 1).
|
||||||
assert_eq!((b.objects[0].x, b.objects[0].y), (3, 1));
|
assert_eq!((b.objects[&1].x, b.objects[&1].y), (3, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1283,7 +1329,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.run_init();
|
game.run_init();
|
||||||
assert_eq!(game.board().objects[0].x, 0);
|
assert_eq!(game.board().objects[&1].x, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1297,7 +1343,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.run_init();
|
game.run_init();
|
||||||
assert_eq!(game.board().objects[0].glyph.tile, 7);
|
assert_eq!(game.board().objects[&1].glyph.tile, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1316,7 +1362,7 @@ mod tests {
|
|||||||
"greeter init should log a greeting"
|
"greeter init should log a greeting"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
game.board().objects.iter().any(|o| o.glyph.tile == 2),
|
game.board().objects.values().any(|o| o.glyph.tile == 2),
|
||||||
"greeter set_tile(2) should change its glyph"
|
"greeter set_tile(2) should change its glyph"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1339,8 +1385,8 @@ mod tests {
|
|||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.run_init();
|
game.run_init();
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!(b.objects[0].x, 1); // moved east from 0
|
assert_eq!(b.objects[&1].x, 1); // moved east from 0
|
||||||
assert_eq!(b.objects[1].x, 3); // moved west from 4
|
assert_eq!(b.objects[&2].x, 3); // moved west from 4
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1356,16 +1402,16 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.run_init();
|
game.run_init();
|
||||||
assert_eq!(game.board().objects[0].x, 2); // first move applied (1 -> 2)
|
assert_eq!(game.board().objects[&1].x, 2); // first move applied (1 -> 2)
|
||||||
|
|
||||||
// 200 ms of ticks: still inside the cooldown, no further movement.
|
// 200 ms of ticks: still inside the cooldown, no further movement.
|
||||||
game.tick(Duration::from_millis(100));
|
game.tick(Duration::from_millis(100));
|
||||||
game.tick(Duration::from_millis(100));
|
game.tick(Duration::from_millis(100));
|
||||||
assert_eq!(game.board().objects[0].x, 2);
|
assert_eq!(game.board().objects[&1].x, 2);
|
||||||
|
|
||||||
// Crossing the 250 ms mark releases the queued second move.
|
// Crossing the 250 ms mark releases the queued second move.
|
||||||
game.tick(Duration::from_millis(100));
|
game.tick(Duration::from_millis(100));
|
||||||
assert_eq!(game.board().objects[0].x, 3);
|
assert_eq!(game.board().objects[&1].x, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1384,7 +1430,7 @@ mod tests {
|
|||||||
game.run_init();
|
game.run_init();
|
||||||
// First (eastward) move is blocked by the wall: object hasn't moved.
|
// First (eastward) move is blocked by the wall: object hasn't moved.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(game.board().objects[0].x, game.board().objects[0].y),
|
(game.board().objects[&1].x, game.board().objects[&1].y),
|
||||||
(1, 1)
|
(1, 1)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1392,14 +1438,14 @@ mod tests {
|
|||||||
game.tick(Duration::from_millis(100));
|
game.tick(Duration::from_millis(100));
|
||||||
game.tick(Duration::from_millis(100));
|
game.tick(Duration::from_millis(100));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(game.board().objects[0].x, game.board().objects[0].y),
|
(game.board().objects[&1].x, game.board().objects[&1].y),
|
||||||
(1, 1)
|
(1, 1)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Past 250 ms the queued South move resolves.
|
// Past 250 ms the queued South move resolves.
|
||||||
game.tick(Duration::from_millis(100));
|
game.tick(Duration::from_millis(100));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(game.board().objects[0].x, game.board().objects[0].y),
|
(game.board().objects[&1].x, game.board().objects[&1].y),
|
||||||
(1, 2)
|
(1, 2)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1421,7 +1467,7 @@ mod tests {
|
|||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.run_init();
|
game.run_init();
|
||||||
assert!(log_texts(&game).iter().any(|t| t == "len=2"));
|
assert!(log_texts(&game).iter().any(|t| t == "len=2"));
|
||||||
assert_eq!(game.board().objects[0].glyph.tile, 6); // last set_tile won
|
assert_eq!(game.board().objects[&1].glyph.tile, 6); // last set_tile won
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1437,7 +1483,7 @@ mod tests {
|
|||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.run_init();
|
game.run_init();
|
||||||
game.tick(Duration::from_millis(300));
|
game.tick(Duration::from_millis(300));
|
||||||
assert_eq!(game.board().objects[0].x, 1); // never moved
|
assert_eq!(game.board().objects[&1].x, 1); // never moved
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1456,7 +1502,7 @@ mod tests {
|
|||||||
wall_at(&mut board, 2, 0);
|
wall_at(&mut board, 2, 0);
|
||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.run_init();
|
game.run_init();
|
||||||
assert_eq!(game.board().objects[0].glyph.tile, 9);
|
assert_eq!(game.board().objects[&1].glyph.tile, 9);
|
||||||
|
|
||||||
// Open ahead, nothing pending: blocked() is false.
|
// Open ahead, nothing pending: blocked() is false.
|
||||||
let board = open_board(
|
let board = open_board(
|
||||||
@@ -1471,7 +1517,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.run_init();
|
game.run_init();
|
||||||
assert_eq!(game.board().objects[0].glyph.tile, 7);
|
assert_eq!(game.board().objects[&1].glyph.tile, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1497,8 +1543,8 @@ mod tests {
|
|||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.tick(Duration::from_millis(16));
|
game.tick(Duration::from_millis(16));
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 1)); // mover advanced
|
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 1)); // mover advanced
|
||||||
assert_eq!(b.objects[1].glyph.tile, 1); // checker saw the pending move
|
assert_eq!(b.objects[&2].glyph.tile, 1); // checker saw the pending move
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1528,11 +1574,11 @@ mod tests {
|
|||||||
game.tick(Duration::from_millis(300));
|
game.tick(Duration::from_millis(300));
|
||||||
{
|
{
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.objects[0].x, b.objects[0].y), (1, 0)); // obj0 won the cell
|
assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0)); // obj0 won the cell
|
||||||
assert_eq!((b.objects[1].x, b.objects[1].y), (2, 0)); // obj1 blocked
|
assert_eq!((b.objects[&2].x, b.objects[&2].y), (2, 0)); // obj1 blocked
|
||||||
}
|
}
|
||||||
let logs = log_texts(&game);
|
let logs = log_texts(&game);
|
||||||
assert!(logs.iter().any(|t| t == "o0 by 1")); // obj0 bumped by obj1
|
assert!(logs.iter().any(|t| t == "o0 by 2")); // obj0 bumped by obj1
|
||||||
assert!(!logs.iter().any(|t| t.starts_with("o1 by"))); // obj1 not bumped
|
assert!(!logs.iter().any(|t| t.starts_with("o1 by"))); // obj1 not bumped
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1663,7 +1709,7 @@ mod tests {
|
|||||||
game.run_init();
|
game.run_init();
|
||||||
{
|
{
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 0)); // object took player's cell
|
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object took player's cell
|
||||||
assert_eq!((b.player.x, b.player.y), (3, 0)); // player shoved east
|
assert_eq!((b.player.x, b.player.y), (3, 0)); // player shoved east
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1679,7 +1725,7 @@ mod tests {
|
|||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.run_init();
|
game.run_init();
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.objects[0].x, b.objects[0].y), (1, 0)); // object blocked
|
assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0)); // object blocked
|
||||||
assert_eq!((b.player.x, b.player.y), (2, 0)); // player not pushed
|
assert_eq!((b.player.x, b.player.y), (2, 0)); // player not pushed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::floor::{FloorSpec, build_floor};
|
use crate::floor::{FloorSpec, build_floor};
|
||||||
use crate::game::{Archetype, Board, FontSpec, Glyph, ObjectDef, Player, PortalDef};
|
use crate::game::{Archetype, Board, FontSpec, Glyph, ObjectDef, ObjectId, Player, PortalDef};
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -462,7 +462,10 @@ impl TryFrom<MapFile> for Board {
|
|||||||
}
|
}
|
||||||
solid_occupied[pidx] = true;
|
solid_occupied[pidx] = true;
|
||||||
|
|
||||||
let mut objects: Vec<ObjectDef> = Vec::new();
|
// Objects get sequential ids in load order (so BTreeMap iteration order ==
|
||||||
|
// load order, preserving the documented "lowest id wins a collision" rule).
|
||||||
|
let mut objects: BTreeMap<ObjectId, ObjectDef> = BTreeMap::new();
|
||||||
|
let mut next_object_id: ObjectId = 1;
|
||||||
for (i, e) in mf.objects.into_iter().enumerate() {
|
for (i, e) in mf.objects.into_iter().enumerate() {
|
||||||
if skip[i] {
|
if skip[i] {
|
||||||
continue;
|
continue;
|
||||||
@@ -516,7 +519,8 @@ impl TryFrom<MapFile> for Board {
|
|||||||
}
|
}
|
||||||
solid_occupied[idx] = true;
|
solid_occupied[idx] = true;
|
||||||
}
|
}
|
||||||
objects.push(obj);
|
objects.insert(next_object_id, obj);
|
||||||
|
next_object_id += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Board {
|
Ok(Board {
|
||||||
@@ -531,6 +535,7 @@ impl TryFrom<MapFile> for Board {
|
|||||||
y: py as i32,
|
y: py as i32,
|
||||||
},
|
},
|
||||||
objects,
|
objects,
|
||||||
|
next_object_id,
|
||||||
portals: mf.portals,
|
portals: mf.portals,
|
||||||
font,
|
font,
|
||||||
zoom: mf.map.zoom,
|
zoom: mf.map.zoom,
|
||||||
@@ -621,7 +626,7 @@ impl From<&Board> for MapFile {
|
|||||||
// Convert ObjectDef → MapFileObjectEntry, encoding the glyph as TOML-compatible fields.
|
// Convert ObjectDef → MapFileObjectEntry, encoding the glyph as TOML-compatible fields.
|
||||||
let objects: Vec<MapFileObjectEntry> = board
|
let objects: Vec<MapFileObjectEntry> = board
|
||||||
.objects
|
.objects
|
||||||
.iter()
|
.values()
|
||||||
.map(|o| MapFileObjectEntry {
|
.map(|o| MapFileObjectEntry {
|
||||||
// Saving always emits concrete coordinates; the palette form is a
|
// Saving always emits concrete coordinates; the palette form is a
|
||||||
// load-time convenience only.
|
// load-time convenience only.
|
||||||
@@ -742,7 +747,7 @@ content = """
|
|||||||
// `G` appears once, isn't a palette key → object lands there, cell is Empty.
|
// `G` appears once, isn't a palette key → object lands there, cell is Empty.
|
||||||
let board = load_board(&map_3x1("G..", &obj_palette("G", "")));
|
let board = load_board(&map_3x1("G..", &obj_palette("G", "")));
|
||||||
assert_eq!(board.objects.len(), 1);
|
assert_eq!(board.objects.len(), 1);
|
||||||
assert_eq!((board.objects[0].x, board.objects[0].y), (0, 0));
|
assert_eq!((board.objects[&1].x, board.objects[&1].y), (0, 0));
|
||||||
// The cell under the object is canonical Empty floor.
|
// The cell under the object is canonical Empty floor.
|
||||||
assert_eq!(board.get(0, 0).1, Archetype::Empty);
|
assert_eq!(board.get(0, 0).1, Archetype::Empty);
|
||||||
}
|
}
|
||||||
@@ -760,7 +765,7 @@ content = """
|
|||||||
// map is flagged invalid (an extra appearance was reported).
|
// map is flagged invalid (an extra appearance was reported).
|
||||||
let board = load_board(&map_3x1("G.G", &obj_palette("G", "")));
|
let board = load_board(&map_3x1("G.G", &obj_palette("G", "")));
|
||||||
assert_eq!(board.objects.len(), 1);
|
assert_eq!(board.objects.len(), 1);
|
||||||
assert_eq!((board.objects[0].x, board.objects[0].y), (0, 0));
|
assert_eq!((board.objects[&1].x, board.objects[&1].y), (0, 0));
|
||||||
assert!(!board.is_valid());
|
assert!(!board.is_valid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,7 +1014,7 @@ bg = "#000000"
|
|||||||
let mf: MapFile = toml::from_str(toml).unwrap();
|
let mf: MapFile = toml::from_str(toml).unwrap();
|
||||||
let board = Board::try_from(mf).unwrap();
|
let board = Board::try_from(mf).unwrap();
|
||||||
assert_eq!(board.objects.len(), 1);
|
assert_eq!(board.objects.len(), 1);
|
||||||
let obj = &board.objects[0];
|
let obj = &board.objects[&1];
|
||||||
assert_eq!(obj.x, 1);
|
assert_eq!(obj.x, 1);
|
||||||
assert_eq!(obj.y, 1);
|
assert_eq!(obj.y, 1);
|
||||||
assert_eq!(obj.glyph.tile, 64);
|
assert_eq!(obj.glyph.tile, 64);
|
||||||
@@ -1037,7 +1042,7 @@ bg = "#000000"
|
|||||||
let toml_out = toml::to_string_pretty(&map_file).unwrap();
|
let toml_out = toml::to_string_pretty(&map_file).unwrap();
|
||||||
let mf2: MapFile = toml::from_str(&toml_out).unwrap();
|
let mf2: MapFile = toml::from_str(&toml_out).unwrap();
|
||||||
let board2 = Board::try_from(mf2).unwrap();
|
let board2 = Board::try_from(mf2).unwrap();
|
||||||
let obj2 = &board2.objects[0];
|
let obj2 = &board2.objects[&1];
|
||||||
assert_eq!(obj2.glyph.tile, obj.glyph.tile);
|
assert_eq!(obj2.glyph.tile, obj.glyph.tile);
|
||||||
assert_eq!(obj2.glyph.fg, obj.glyph.fg);
|
assert_eq!(obj2.glyph.fg, obj.glyph.fg);
|
||||||
assert_eq!(obj2.glyph.bg, obj.glyph.bg);
|
assert_eq!(obj2.glyph.bg, obj.glyph.bg);
|
||||||
|
|||||||
+36
-33
@@ -31,7 +31,7 @@
|
|||||||
//! [`take_board_queue`]: ScriptHost::take_board_queue
|
//! [`take_board_queue`]: ScriptHost::take_board_queue
|
||||||
//! [`GameState`]: crate::game::GameState
|
//! [`GameState`]: crate::game::GameState
|
||||||
|
|
||||||
use crate::game::Board;
|
use crate::game::{Board, ObjectId};
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use rhai::{AST, CallFnOptions, Engine, FuncArgs, ImmutableString, NativeCallContext, Scope};
|
use rhai::{AST, CallFnOptions, Engine, FuncArgs, ImmutableString, NativeCallContext, Scope};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
@@ -68,18 +68,14 @@ impl Action {
|
|||||||
/// An action promoted from an object's output queue onto the board queue, tagged
|
/// An action promoted from an object's output queue onto the board queue, tagged
|
||||||
/// with the object that issued it.
|
/// with the object that issued it.
|
||||||
pub struct BoardAction {
|
pub struct BoardAction {
|
||||||
/// The object that issued the action.
|
/// The stable [`ObjectId`] of the object that issued the action.
|
||||||
// TODO: `source` is an index into `Board::objects`, which is a stopgap — it
|
pub source: ObjectId,
|
||||||
// breaks under object spawn/destroy/reorder. Replace with a monotonically
|
|
||||||
// increasing unique object id, with objects stored keyed by it (e.g.
|
|
||||||
// `BTreeMap<ObjectId, ObjectDef>`). Mirrored on `ObjectRuntime::object_index`.
|
|
||||||
pub source: usize,
|
|
||||||
/// The action to apply.
|
/// The action to apply.
|
||||||
pub action: Action,
|
pub action: Action,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A cardinal movement direction.
|
/// A cardinal movement direction.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum Direction {
|
pub enum Direction {
|
||||||
North,
|
North,
|
||||||
South,
|
South,
|
||||||
@@ -112,9 +108,9 @@ type ObjQueue = Rc<RefCell<VecDeque<Action>>>;
|
|||||||
/// so the `blocked` host function can scan pending moves during script execution.
|
/// so the `blocked` host function can scan pending moves during script execution.
|
||||||
type BoardQueue = Rc<RefCell<Vec<BoardAction>>>;
|
type BoardQueue = Rc<RefCell<Vec<BoardAction>>>;
|
||||||
|
|
||||||
/// Maps an object index to its output queue, so the global write host functions can
|
/// Maps an [`ObjectId`] to its output queue, so the global write host functions can
|
||||||
/// route an emitted action to the right object via the per-call tag.
|
/// route an emitted action to the right object via the per-call tag.
|
||||||
type QueueMap = Rc<RefCell<HashMap<usize, ObjQueue>>>;
|
type QueueMap = Rc<RefCell<HashMap<ObjectId, ObjQueue>>>;
|
||||||
|
|
||||||
/// Channel for engine/compile/runtime errors, surfaced straight to the game log
|
/// Channel for engine/compile/runtime errors, surfaced straight to the game log
|
||||||
/// (errors never go through the action queues).
|
/// (errors never go through the action queues).
|
||||||
@@ -135,10 +131,9 @@ struct CompiledScript {
|
|||||||
/// The runtime state of one scripted object: which script it runs, its persistent
|
/// The runtime state of one scripted object: which script it runs, its persistent
|
||||||
/// Rhai scope, its pending actions, and its cooldown timer.
|
/// Rhai scope, its pending actions, and its cooldown timer.
|
||||||
struct ObjectRuntime {
|
struct ObjectRuntime {
|
||||||
/// Index into [`crate::game::Board::objects`]; passed to scripts as the
|
/// The stable [`ObjectId`] of this object; passed to scripts as the per-call
|
||||||
/// per-call tag so host functions know which object issued an action.
|
/// tag so host functions know which object issued an action.
|
||||||
// TODO: see [`BoardAction::source`] — array indices should become stable ids.
|
object_id: ObjectId,
|
||||||
object_index: usize,
|
|
||||||
/// Key into [`ScriptHost::scripts`] naming this object's compiled script.
|
/// Key into [`ScriptHost::scripts`] naming this object's compiled script.
|
||||||
script_name: String,
|
script_name: String,
|
||||||
/// Per-object scope (holds the `Board`/`Queue` views + direction constants, and
|
/// Per-object scope (holds the `Board`/`Queue` views + direction constants, and
|
||||||
@@ -187,7 +182,7 @@ impl ScriptHost {
|
|||||||
// object that references the script.
|
// object that references the script.
|
||||||
let mut scripts: HashMap<String, CompiledScript> = HashMap::new();
|
let mut scripts: HashMap<String, CompiledScript> = HashMap::new();
|
||||||
let mut failed: HashSet<String> = HashSet::new();
|
let mut failed: HashSet<String> = HashSet::new();
|
||||||
for obj in board.objects.iter() {
|
for obj in board.objects.values() {
|
||||||
let Some(name) = obj.script_name.as_ref() else {
|
let Some(name) = obj.script_name.as_ref() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -230,7 +225,7 @@ impl ScriptHost {
|
|||||||
// compiled. The queue is registered in `queues` so host functions can find
|
// compiled. The queue is registered in `queues` so host functions can find
|
||||||
// it by the object's index, and shared into the scope as the `Queue` object.
|
// it by the object's index, and shared into the scope as the `Queue` object.
|
||||||
let mut objects = Vec::new();
|
let mut objects = Vec::new();
|
||||||
for (i, obj) in board.objects.iter().enumerate() {
|
for (&id, obj) in board.objects.iter() {
|
||||||
let Some(name) = obj.script_name.as_ref() else {
|
let Some(name) = obj.script_name.as_ref() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -238,9 +233,9 @@ impl ScriptHost {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let queue: ObjQueue = Rc::new(RefCell::new(VecDeque::new()));
|
let queue: ObjQueue = Rc::new(RefCell::new(VecDeque::new()));
|
||||||
queues.borrow_mut().insert(i, queue.clone());
|
queues.borrow_mut().insert(id, queue.clone());
|
||||||
objects.push(ObjectRuntime {
|
objects.push(ObjectRuntime {
|
||||||
object_index: i,
|
object_id: id,
|
||||||
script_name: name.clone(),
|
script_name: name.clone(),
|
||||||
scope: new_object_scope(board_cell, &queue),
|
scope: new_object_scope(board_cell, &queue),
|
||||||
output_queue: queue,
|
output_queue: queue,
|
||||||
@@ -271,12 +266,12 @@ impl ScriptHost {
|
|||||||
self.run("tick", |c| c.has_tick, (dt,));
|
self.run("tick", |c| c.has_tick, (dt,));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calls `bump(id)` on the object at `object_index`, if it defines the hook.
|
/// Calls `bump(id)` on the object with [`ObjectId`] `object_id`, if it defines the hook.
|
||||||
///
|
///
|
||||||
/// `bumper` is the index of the object that moved into it, or `-1` for the player.
|
/// `bumper` is the id of the object that moved into it, or `-1` for the player.
|
||||||
/// Does **not** pump: any actions the hook emits wait for the next tick's pump, so
|
/// Does **not** pump: any actions the hook emits wait for the next tick's pump, so
|
||||||
/// a bump can't cascade within the same resolution pass.
|
/// a bump can't cascade within the same resolution pass.
|
||||||
pub fn run_bump(&mut self, object_index: usize, bumper: i64) {
|
pub fn run_bump(&mut self, object_id: ObjectId, bumper: i64) {
|
||||||
let Self {
|
let Self {
|
||||||
engine,
|
engine,
|
||||||
scripts,
|
scripts,
|
||||||
@@ -284,7 +279,7 @@ impl ScriptHost {
|
|||||||
errors,
|
errors,
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
let Some(obj) = objects.iter_mut().find(|o| o.object_index == object_index) else {
|
let Some(obj) = objects.iter_mut().find(|o| o.object_id == object_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(compiled) = scripts.get(&obj.script_name) else {
|
let Some(compiled) = scripts.get(&obj.script_name) else {
|
||||||
@@ -293,7 +288,7 @@ impl ScriptHost {
|
|||||||
if !compiled.has_bump {
|
if !compiled.has_bump {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let options = CallFnOptions::default().with_tag(object_index as i64);
|
let options = CallFnOptions::default().with_tag(object_id as i64);
|
||||||
if let Err(err) = engine.call_fn_with_options::<()>(
|
if let Err(err) = engine.call_fn_with_options::<()>(
|
||||||
options,
|
options,
|
||||||
&mut obj.scope,
|
&mut obj.scope,
|
||||||
@@ -333,7 +328,7 @@ impl ScriptHost {
|
|||||||
/// action (which arms the timer and ends the pump). The board-queue guard keeps a
|
/// action (which arms the timer and ends the pump). The board-queue guard keeps a
|
||||||
/// second pump in the same tick from double-promoting.
|
/// second pump in the same tick from double-promoting.
|
||||||
fn pump(&mut self, i: usize) {
|
fn pump(&mut self, i: usize) {
|
||||||
let oid = self.objects[i].object_index;
|
let oid = self.objects[i].object_id;
|
||||||
// Don't re-promote if this object already has actions awaiting resolution.
|
// Don't re-promote if this object already has actions awaiting resolution.
|
||||||
if self.board_queue.borrow().iter().any(|ba| ba.source == oid) {
|
if self.board_queue.borrow().iter().any(|ba| ba.source == oid) {
|
||||||
return;
|
return;
|
||||||
@@ -388,8 +383,8 @@ impl ScriptHost {
|
|||||||
if let Some(compiled) = scripts.get(&obj.script_name)
|
if let Some(compiled) = scripts.get(&obj.script_name)
|
||||||
&& defined(compiled)
|
&& defined(compiled)
|
||||||
{
|
{
|
||||||
// The tag carries this object's index to the host functions.
|
// The tag carries this object's id to the host functions.
|
||||||
let options = CallFnOptions::default().with_tag(obj.object_index as i64);
|
let options = CallFnOptions::default().with_tag(obj.object_id as i64);
|
||||||
if let Err(err) = engine.call_fn_with_options::<()>(
|
if let Err(err) = engine.call_fn_with_options::<()>(
|
||||||
options,
|
options,
|
||||||
&mut obj.scope,
|
&mut obj.scope,
|
||||||
@@ -436,9 +431,14 @@ fn register_read_api(engine: &mut Engine, board: &BoardRef, board_queue: &BoardQ
|
|||||||
///
|
///
|
||||||
/// Pending moves are matched by their *immediate* target cell only; a queued move that
|
/// Pending moves are matched by their *immediate* target cell only; a queued move that
|
||||||
/// would push a chain of solids into the cell is not accounted for (an approximation).
|
/// would push a chain of solids into the cell is not accounted for (an approximation).
|
||||||
fn is_blocked(board: &BoardRef, board_queue: &BoardQueue, source: usize, dir: Direction) -> bool {
|
fn is_blocked(
|
||||||
|
board: &BoardRef,
|
||||||
|
board_queue: &BoardQueue,
|
||||||
|
source: ObjectId,
|
||||||
|
dir: Direction,
|
||||||
|
) -> bool {
|
||||||
let board = board.borrow();
|
let board = board.borrow();
|
||||||
let Some(obj) = board.objects.get(source) else {
|
let Some(obj) = board.objects.get(&source) else {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
let (dx, dy): (i32, i32) = dir.into();
|
let (dx, dy): (i32, i32) = dir.into();
|
||||||
@@ -453,7 +453,7 @@ fn is_blocked(board: &BoardRef, board_queue: &BoardQueue, source: usize, dir: Di
|
|||||||
// Any pending move whose immediate target is this cell would put a solid here.
|
// Any pending move whose immediate target is this cell would put a solid here.
|
||||||
board_queue.borrow().iter().any(|ba| {
|
board_queue.borrow().iter().any(|ba| {
|
||||||
if let Action::Move(d) = &ba.action
|
if let Action::Move(d) = &ba.action
|
||||||
&& let Some(mover) = board.objects.get(ba.source)
|
&& let Some(mover) = board.objects.get(&ba.source)
|
||||||
{
|
{
|
||||||
let (mdx, mdy): (i32, i32) = (*d).into();
|
let (mdx, mdy): (i32, i32) = (*d).into();
|
||||||
return (mover.x as i32 + mdx, mover.y as i32 + mdy) == target;
|
return (mover.x as i32 + mdx, mover.y as i32 + mdy) == target;
|
||||||
@@ -500,17 +500,20 @@ fn register_queue_api(engine: &mut Engine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Appends `action` to the output queue of the object identified by `source`.
|
/// Appends `action` to the output queue of the object identified by `source`.
|
||||||
fn emit(queues: &QueueMap, source: usize, action: Action) {
|
fn emit(queues: &QueueMap, source: ObjectId, action: Action) {
|
||||||
if let Some(queue) = queues.borrow().get(&source) {
|
if let Some(queue) = queues.borrow().get(&source) {
|
||||||
queue.borrow_mut().push_back(action);
|
queue.borrow_mut().push_back(action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the issuing object's index from the call tag (set in [`ScriptHost::run`]).
|
/// Reads the issuing object's [`ObjectId`] from the call tag (set in [`ScriptHost::run`]).
|
||||||
fn source_of(ctx: &NativeCallContext) -> usize {
|
///
|
||||||
|
/// Falls back to `0` (never a valid id, since ids start at 1) if the tag is missing
|
||||||
|
/// or out of range, which makes [`emit`] a harmless no-op for an unknown source.
|
||||||
|
fn source_of(ctx: &NativeCallContext) -> ObjectId {
|
||||||
ctx.tag()
|
ctx.tag()
|
||||||
.and_then(|t| t.as_int().ok())
|
.and_then(|t| t.as_int().ok())
|
||||||
.and_then(|n| usize::try_from(n).ok())
|
.and_then(|n| ObjectId::try_from(n).ok())
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ impl Widget for BoardWidget<'_> {
|
|||||||
// Resolve which glyph wins this cell: player > object > grid floor.
|
// Resolve which glyph wins this cell: player > object > grid floor.
|
||||||
let glyph = if board.player.x == bx as i32 && board.player.y == by as i32 {
|
let glyph = if board.player.x == bx as i32 && board.player.y == by as i32 {
|
||||||
Glyph::player()
|
Glyph::player()
|
||||||
} else if let Some(obj) = board.object_at(bx, by) {
|
} else if let Some(id) = board.object_ids_at(bx, by).get(0) {
|
||||||
obj.glyph
|
board.objects[id].glyph
|
||||||
} else {
|
} else {
|
||||||
// Static layer: the grid archetype's glyph, or the floor layer
|
// Static layer: the grid archetype's glyph, or the floor layer
|
||||||
// for an empty cell (see `Board::display_glyph`).
|
// for an empty cell (see `Board::display_glyph`).
|
||||||
|
|||||||
+3
-3
@@ -64,9 +64,9 @@ content = """
|
|||||||
# # #
|
# # #
|
||||||
# +++ # #
|
# +++ # #
|
||||||
# # #
|
# # #
|
||||||
|
# | #
|
||||||
# #
|
# #
|
||||||
# #
|
# oo G - @ #
|
||||||
# oo G - | @ #
|
|
||||||
# #
|
# #
|
||||||
# #
|
# #
|
||||||
# #
|
# #
|
||||||
@@ -119,7 +119,7 @@ fn tick(dt) {
|
|||||||
move(East);
|
move(East);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fires when something steps into this object; id is the bumper's array index,
|
// Fires when something steps into this object; id is the bumper's object id,
|
||||||
// or -1 for the player.
|
// or -1 for the player.
|
||||||
fn bump(id) {
|
fn bump(id) {
|
||||||
log(`mover bumped by ${id}`);
|
log(`mover bumped by ${id}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user