feat(scripting): Phase 2 (onTick/onDeath) + Phase 3 (usable items) #203

Merged
marco merged 2 commits from feat/scripting-phase2-3 into main 2026-07-25 09:20:29 +02:00
Owner

Summary

Phase 2 (#195) + Phase 3 (#196) on top of the Lua scripting layer (#193): scripted creatures now act on their own and react to death, and usable items (potions/scrolls) are content.

  • onTick (heartbeat). A mobile{} can declare onTick(npc) — a periodic "think" the engine fires on GameOptions.ScriptTickCadence (~3s), so an NPC does things without being clicked. The rat now chatters ambiently.
  • onDeath. onDeath(npc, killer, x, y) fires as a scripted creature dies, with who slew it + where it fell — reward the killer, drop loot. The rat drops a coin on death.
  • Usable items (item{}). A non-weapon item is item{ kind, graphic, …, onUse = function(user) … end }, dropped into the existing use pipeline (backpack items are now usable). A Lua potion (Items.Consumables.Potion) heals on use.
  • Verbs: heal(mobile, n) (+ a Healed floaty) and spawnItem(kind, x, y) (loot). Item handle added; Player is now a Mobile in the generated api.d.lua.

Deferred (noted in #197 / the issues): onEnterTile (needs spatial indexing), onQuestStep (built from flags), one-shot schedule timers (onTick covers the periodic case), consuming a potion on use.

Invariants Check (delta)

  • Server-authoritative ✓ — verbs validate on World (heal via the intrinsic clamp setter; spawnItem rejects off-map/unknown kind); triggers dispatch committed content, never client input.
  • Single-thread sim / World HARD GATE ✓ — onTick/onDeath dispatch lives in WorldTick (above World); deaths travel as a transient TickResult.Deaths list (the accepted per-tick event pattern); World stays scripting-unaware. Handlers are instruction-budgeted + isolated (a bad handler never tears down the tick).
  • Protocol versioned ✓ (N/A bump) — effects reuse existing broadcasts (MobileSaid, Healed, ItemSpawned); no new wire shape.
  • String catalog ✓ — the ambient line is SystemMessageId.RatSniffs + Messages.Creatures.RatSniffs (typed).
  • Extend by type ✓ — new triggers = new ScriptTrigger values + one emission site; item{} auto-discovered; no switch.
  • Act on the instance ✓ — heal via Mobile.Heal; onTick cadence a field on the creature (LastThinkTick), not a side dict.
  • API self-describing / no-drift ✓ — handlers declared per-registration ([ScriptHandler] on the def) so mobile.onUse(npc,player) and item.onUse(user) get correct signatures; api.d.lua regenerated (drift-check green); host validates against the same model.
  • Persistence ✓ — onTick cadence transient; the potion is a scripted item kind; no tick-loop IO. No .

Screenshots / recording

Captured via the debug harness from a fresh DB.

onTick — the rat chatters on its own (heartbeat), no interaction:

The rat chatters on its own

The Lua potion (item{}), placed in the world (+ the rat's ambient chatter):

The Lua potion placed in the world

The heal floaty (drinking the potion) and onDeath loot are covered by integration/unit tests below — capturing the heal floaty via the harness needs damaging the player through GM targeting, which is fragile to drive blind, so it's asserted in an integration test instead of screenshotted.

How it was tested

  • Unit (IsoMmo.Scripting, fake context): onTick routing; onDeath receives (npc, killer, x, y) and can spawnItem; item onUse can heal(user, …); per-registration handler signatures + the api.d.lua drift-check.
  • Gameplay (vs World): heal raises Hits + buffers a Healed floaty to AoI observers; spawnItem rejects off-map and drops a ground item on-map.
  • Integration (WebSocket / real startup): a scripted potion heals the user on Use (Healed 15); a scripted NPC chatters on its heartbeat (RatSniffs delivered to a nearby player).
  • Gates: just lint (CSharpier + build, zero warnings), whole-solution build, dotnet test all green (543), lua-lint (luacheck + lua-language-server) clean, check-docs clean.

Checklist

  • just lint passes (zero warnings)
  • just test is green (543)
  • The whole solution builds (client + tools)
  • Multi-platform preserved (MoonSharp pure-managed; lua tooling dev/CI-only)
  • Tests added/updated
  • Linked #195 + #196; their Definition of Done is met (onEnterTile/onQuestStep/schedule explicitly deferred with reasons)
## Summary Phase 2 (#195) + Phase 3 (#196) on top of the Lua scripting layer (#193): scripted creatures now **act on their own** and **react to death**, and **usable items** (potions/scrolls) are content. - **onTick (heartbeat).** A `mobile{}` can declare `onTick(npc)` — a periodic "think" the engine fires on `GameOptions.ScriptTickCadence` (~3s), so an NPC does things without being clicked. The rat now chatters ambiently. - **onDeath.** `onDeath(npc, killer, x, y)` fires as a scripted creature dies, with who slew it + where it fell — reward the killer, drop loot. The rat drops a coin on death. - **Usable items (`item{}`).** A non-weapon item is `item{ kind, graphic, …, onUse = function(user) … end }`, dropped into the existing use pipeline (backpack items are now usable). A Lua **potion** (`Items.Consumables.Potion`) heals on use. - **Verbs:** `heal(mobile, n)` (+ a Healed floaty) and `spawnItem(kind, x, y)` (loot). `Item` handle added; `Player` is now a `Mobile` in the generated `api.d.lua`. Deferred (noted in #197 / the issues): `onEnterTile` (needs spatial indexing), `onQuestStep` (built from flags), one-shot `schedule` timers (onTick covers the periodic case), consuming a potion on use. ### Invariants Check (delta) - **Server-authoritative** ✓ — verbs validate on World (`heal` via the intrinsic clamp setter; `spawnItem` rejects off-map/unknown kind); triggers dispatch committed content, never client input. - **Single-thread sim / World HARD GATE** ✓ — onTick/onDeath dispatch lives in **WorldTick** (above World); deaths travel as a transient `TickResult.Deaths` list (the accepted per-tick event pattern); `World` stays scripting-unaware. Handlers are instruction-budgeted + isolated (a bad handler never tears down the tick). - **Protocol versioned** ✓ (N/A bump) — effects reuse existing broadcasts (`MobileSaid`, `Healed`, `ItemSpawned`); no new wire shape. - **String catalog** ✓ — the ambient line is `SystemMessageId.RatSniffs` + `Messages.Creatures.RatSniffs` (typed). - **Extend by type** ✓ — new triggers = new `ScriptTrigger` values + one emission site; `item{}` auto-discovered; no switch. - **Act on the instance** ✓ — `heal` via `Mobile.Heal`; onTick cadence a field on the creature (`LastThinkTick`), not a side dict. - **API self-describing / no-drift** ✓ — handlers declared per-registration (`[ScriptHandler]` on the def) so `mobile.onUse(npc,player)` and `item.onUse(user)` get correct signatures; `api.d.lua` regenerated (drift-check green); host validates against the same model. - **Persistence** ✓ — onTick cadence transient; the potion is a scripted item kind; no tick-loop IO. No `✗`. ## Screenshots / recording Captured via the debug harness from a fresh DB. **onTick — the rat chatters on its own (heartbeat), no interaction:** ![The rat chatters on its own](https://git.homelab.devncode.it/attachments/2b75e6a6-06dd-4462-aad3-f625e3f9c96c) **The Lua potion (`item{}`), placed in the world (+ the rat's ambient chatter):** ![The Lua potion placed in the world](https://git.homelab.devncode.it/attachments/ac48c91e-8f6e-430d-bdc6-5edae6cd3a70) The **heal floaty** (drinking the potion) and **onDeath loot** are covered by integration/unit tests below — capturing the heal floaty via the harness needs damaging the player through GM targeting, which is fragile to drive blind, so it's asserted in an integration test instead of screenshotted. ## How it was tested - **Unit (`IsoMmo.Scripting`, fake context):** onTick routing; onDeath receives `(npc, killer, x, y)` and can `spawnItem`; item `onUse` can `heal(user, …)`; per-registration handler signatures + the `api.d.lua` drift-check. - **Gameplay (vs `World`):** `heal` raises Hits + buffers a `Healed` floaty to AoI observers; `spawnItem` rejects off-map and drops a ground item on-map. - **Integration (WebSocket / real startup):** a scripted **potion heals the user** on `Use` (Healed 15); a scripted **NPC chatters on its heartbeat** (`RatSniffs` delivered to a nearby player). - **Gates:** `just lint` (CSharpier + build, zero warnings), whole-solution build, `dotnet test` all green (543), `lua-lint` (luacheck + lua-language-server) clean, `check-docs` clean. ## Checklist - [x] `just lint` passes (zero warnings) - [x] `just test` is green (543) - [x] The whole solution builds (client + tools) - [x] Multi-platform preserved (MoonSharp pure-managed; lua tooling dev/CI-only) - [x] Tests added/updated - [x] Linked #195 + #196; their Definition of Done is met (onEnterTile/onQuestStep/schedule explicitly deferred with reasons)
Phase 2 (#195): scripted creatures gain an onTick heartbeat (WorldTick-driven, cadence
GameOptions.ScriptTickCadence, instruction-budgeted) and onDeath (from the tick's death
list, with killer + where it fell). New verbs heal + spawnItem. The rat now chatters on a
heartbeat and drops a coin on death — all above World (World stays scripting-unaware; deaths
travel as a transient TickResult list).

Phase 3 (#196): usable items as content — a new item{} registration + onUse handler; a
ScriptedUsableItem drops into the existing IUsable pipeline (backpack items now usable). A
Lua potion (Items.Consumables.Potion) heals on use.

DX: handlers are declared per-registration ([ScriptHandler] on the def) with correct per-kind
signatures (mobile onUse(npc,player) vs item onUse(user)); Player : Mobile in the generated
api.d.lua; Dispatch generalised to params. Tests (host + gameplay + WebSocket integration:
potion heals, rat chatters on its heartbeat) all green; lua-lint + drift-check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(scripting): document onTick/onDeath triggers, item{} usables, heal/spawnItem verbs
All checks were successful
ci / Lua content lint (pull_request) Successful in 10s
ci / Lint & Test (pull_request) Successful in 3m42s
853285efa9
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marco merged commit 6ee56a029a into main 2026-07-25 09:20:29 +02:00
marco deleted branch feat/scripting-phase2-3 2026-07-25 09:20:30 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo!203
No description provided.