From 09d325dcf5e779eab5550d3fd3377bde50101428 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 8 Jul 2026 19:59:14 -0400 Subject: feat: unify on use, on look, and on kill. all support same conditions/actions now. --- AGENTS.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) (limited to 'AGENTS.md') diff --git a/AGENTS.md b/AGENTS.md index 893ba59..9016e73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ File prefixes in `internal/game/`: `game.go/cmd_registry.go/tick.go` (core), `co ## Key Conventions -**YAML-driven.** Rooms, items, objects, mobs, drops, modules, courses under `data/`. Read from disk on every access — edit and it takes effect immediately. **The filename is the ID** for every data type: lookup by filename stem. **Do not put a top-level `id:` field** (ignored). A nested `- id:` under a room's `objects:`/`mobs:` is a *reference* and is still required. Subdirectories are organizational only. **Local objects:** a room `objects:` entry that carries content fields (`name`/`description`/`aliases`/`inroom_description`/`color`/`hidden`/`on_look`) is a fully local, room-scoped object def instead of a reference — passive subset only (no gather/talk/use/safespot/steal/guard_mob/use_interactions). Local identity derives from the normalized `name` (lowercased, whitespace-collapsed, spaces kept); `id:` is reserved for references and ignored+warned on local objects. Each object in a room must have a unique name (startup error on collision, incl. local-name vs referenced file id); names may repeat across different rooms. +**YAML-driven.** Rooms, items, objects, mobs, drops, modules, courses under `data/`. Read from disk on every access — edit and it takes effect immediately. **The filename is the ID** for every data type: lookup by filename stem. **Do not put a top-level `id:` field** (ignored). A nested `- id:` under a room's `objects:`/`mobs:` is a *reference* and is still required. Subdirectories are organizational only. **Local objects:** a room `objects:` entry that carries content fields (`name`/`description`/`aliases`/`inroom_description`/`color`/`hidden`/`on_look`) is a fully local, room-scoped object def instead of a reference — passive subset only (no gather/talk/use/safespot/steal/guard_mob/on_use). Local identity derives from the normalized `name` (lowercased, whitespace-collapsed, spaces kept); `id:` is reserved for references and ignored+warned on local objects. Each object in a room must have a unique name (startup error on collision, incl. local-name vs referenced file id); names may repeat across different rooms. **Live state.** Player data persisted to YAML on every change. Ground items, mob instances, object states are in-memory only. @@ -38,9 +38,9 @@ File prefixes in `internal/game/`: `game.go/cmd_registry.go/tick.go` (core), `co ## Room Scripting -- **`on_enter` steps**: message + optional condition. A step may have `delay` and/or `set_global_flags`/`set_player_flags`. Timed steps run as scheduled enter sequences; otherwise messages print synchronously. Conditions snapshotted once at entry. Resumes on reconnect if interrupted. +- **`on_enter` steps** (`[]behavior.StepAction`): message + optional `condition` + `delay` + any of the shared `StepAction` effects (`set_global_flags`/`set_player_flags`, `give_item`/`take_item`, `teleport`, `heal`, `credits`, `aps_node`, `broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`). Timed steps (any non-zero field triggers the timed path) run as scheduled enter sequences; otherwise messages print synchronously. Conditions snapshotted once at entry. Resumes on reconnect if interrupted. - **Conditional descriptions** (`description: [{text, condition}]`): first passing variant wins. For objects, if no variant matches the object is absent for that player. Accepts plain string or list. -- **Exits** can `set_global_flags`/`set_player_flags` on successful traverse. +- **Exits** carry an `on_traverse:` list of `behavior.Interaction` (same shape as on_use) — first passing entry fires AFTER `p.RoomID` is updated to the destination, so `aps_node`/`teleport` resolve against the new room. `ExitDef.Condition` still gates whether the exit is passable at all; each `on_traverse` entry has its own additional `Condition`. ## Data Files @@ -77,9 +77,29 @@ Actions route through `startAction()` in `act.go`: normalize verb → resolve ta **Behavior types (YAML-driven):** `gather` (mine/chop/cut/fish), `use` (crafting stations), `talk` (NPC dialog). **Hardcoded actions:** burn/stoke, search, production (cook/smelt/smith/craft/mix/fletch/clean), steal, agility, farm, identify, finishing blow. Gather behaviors use SuccessChance = Base + (level-required)*PerLevel, clamped `[0, Cap]`. Action types and all YAML config fields are self-documenting in the source (`act_state.go`, `behavior.go`, `item/item.go`). +### Interactions (unified conditional-action model) + +`behavior.Interaction` (struct in `behavior/behavior.go`) is the shared per-entry shape for every "when X happens, do Y with optional condition" feature in the game: + +- **On Use** (`on_use:` on objects) — fires on `use ` (bare, no `item_id`) or `use on ` (item-keyed). Triggered in `cmd_use.go`; queued as a one-tick action; tick executor in `act_use_interaction.go`. +- **On Look** (`on_look:` on objects) — fires after the object's description is shown on `look `. Triggered in `look_target.go`; runs the first passing entry inline (no queue). An entry with an `item_id` only fires if the player currently carries that item in their inventory. +- **On Kill** (`on_kill:` on mobs) — fires (additively over standard loot) when a combat kill of the mob completes OR when a `kind: task` mob's work drains its HP to zero (the "completion" of the task). Triggered from `endCombat` in `combat_mob.go`. An entry with an `item_id` only fires if the player is wielding that item in a weapon-hand slot (main_hand or off_hand) at the moment of the kill. +- **Exit On Traverse** (`on_traverse:` on exits) — fires AFTER `p.RoomID` is set to the destination, so `aps_node`/`teleport` resolve against the new room. Stashed in `Player.MovePendingInteraction` at move-classify time, applied in `completeMove` (`cmd_move.go`). +- **On Enter steps** (`on_enter:` on rooms) — list of `behavior.StepAction`; each step has its own `Condition` + `Message`/`Delay` + full effect set. Reconnect-resume. Driven by `enterSeq`/`EnterSeqTick` in `act_room.go`. +- **Trigger steps** (`steps:` on room/global `TriggerDef`) — same `StepAction` per step, newly supports per-step `Condition`. Trigger watcher + async playback in `sys_triggers.go` / `trigger_store.go`. The event-listener / cascade / re-entry-guard machinery is separate from interactions' inline dispatch. +- **Talk** — `TalkNode.Action` and `TalkOption.Action` are `*NodeAction` (inline subset). Wrap via `handleNodeAction` → `applyNodeAction`. + +Each list of Interactions is walked top-to-bottom; the **first** entry whose item filter and `Condition` pass wins and fires (one entry per triggering event). `applyInteraction` (`act_effects.go`) is the convenience wrapper for that walk + fire; individual callers can loop directly. The item filter is interaction-kind-specific: on_use matches the held item, on_look requires `p.HasItem`, on_kill requires `p.IsWielding`; nil disables the filter (exit traversal, on_traverse). + +**Effect superset — `behavior.StepAction`.** The universal effect container embedded by all interaction `action` fields. Holds the inline-only `NodeAction` fields (`set_global_flags`, `set_player_flags`, `give_item`, `take_item`, `teleport`, `heal`, `credits`, `aps_node`) PLUS the sequence-only fields (`message`, `broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`, `delay`) and a per-step `Condition`. Inline callers (use/look/kill/talk) ignore the sequence-only fields; on_enter / trigger steps use them. + +**One executor — `applyStepAction(sess, p, step, roomID, scope, flagValue)` in `act_effects.go`.** Scopes: `scopePlayer` (inline: flags + items + heal + credits + teleport + aps_node + broadcast), `scopePlayerSeq` (on_enter / trigger player seq: above + broadcast_global + spawn_mob + despawn_mob), `scopeGlobal` (trigger global seq: broadcasts + global flags + world-owned spawn/despawn only). `applyNodeAction` (`act_talk.go`) is a thin wrapper passing a NodeAction-wrapped StepAction at scopePlayer; `fireEnterStep` / `executeTriggerStep` / `executeGlobalTriggerStep` are thin wrappers at scopePlayerSeq / scopeGlobal. The old duplicate executors (`applyNodeAction` body, `fireEnterStep` body, `executeTriggerStep`, `executeGlobalTriggerStep`) collapsed into one in `act_effects.go`. + +`aps_node` retains its dedicated bool field — it stamps `aps_node_: true` on the player, which the `aps` command (`cmd_aps.go`) reads to build the datapad. (The room-id is dynamic, so it can't be expressed as a static `set_player_flags` entry without template substitution.) `reputation_cost` was removed (assassin reputation shop is not yet wired up). + ### Condition System -Used by exits, talk options, on-enter, use_interactions: `global_flag` (global), `player_flag` (per-char), `value` (exact match), `has_item`, `min_credits`, `all_of`, `any_of`, `not`. Bare global_flag/player_flag passes on truthy. +Used by exits (exit gate + `on_traverse`), talk options/nodes, on-enter steps, on_use / on_look (objects), on_kill (mobs), trigger steps: `global_flag` (global), `player_flag` (per-char), `value` (exact match), `has_item`, `min_credits`, `all_of`, `any_of`, `not`. Bare global_flag/player_flag passes on truthy. ## Two Depletion Mechanics @@ -145,7 +165,7 @@ Full field reference in `item/item.go`. Key fields: equip_slot, weapon_type, att - `startAction` searches objects first, then mobs. Mobs need `behavior:` on YAML def to be interactable. - Exits: old `north: 2` still works via custom `UnmarshalYAML`. Same for `RoomMob`. -- `checkCondition` is the single condition evaluator — shared across exits, talk, on-enter, use_interactions. +- `checkCondition` is the single condition evaluator — shared across exits, talk, on-enter, on_use/on_look (objects), on_kill (mobs), trigger steps. Every interaction type and every timed step shares one condition language. - Object `hidden: true` = not in room listings but still interactable. - Alias expansion happens before command dispatch and can shadow built-ins. - `say` preserves case from raw input. -- cgit v1.2.3