diff options
Diffstat (limited to 'building_guide')
| -rw-r--r-- | building_guide/behaviors.md | 605 | ||||
| -rw-r--r-- | building_guide/conditions.md | 83 | ||||
| -rw-r--r-- | building_guide/construction.md | 307 | ||||
| -rw-r--r-- | building_guide/courses.md | 1 | ||||
| -rw-r--r-- | building_guide/doors.md | 93 | ||||
| -rw-r--r-- | building_guide/hidden_objects.md | 92 | ||||
| -rw-r--r-- | building_guide/items.md | 60 | ||||
| -rw-r--r-- | building_guide/mobs.md | 7 | ||||
| -rw-r--r-- | building_guide/objects.md | 531 | ||||
| -rw-r--r-- | building_guide/quests.md | 15 | ||||
| -rw-r--r-- | building_guide/recipes.md | 215 | ||||
| -rw-r--r-- | building_guide/rooms.md | 462 | ||||
| -rw-r--r-- | building_guide/search.md | 15 | ||||
| -rw-r--r-- | building_guide/state.md | 13 | ||||
| -rw-r--r-- | building_guide/tips.md | 10 | ||||
| -rw-r--r-- | building_guide/triggers.md | 23 | ||||
| -rw-r--r-- | building_guide/wandering_mobs.md | 20 | ||||
| -rw-r--r-- | building_guide/wandering_objects.md | 37 |
18 files changed, 1145 insertions, 1444 deletions
diff --git a/building_guide/behaviors.md b/building_guide/behaviors.md index 3b7d769..f358a33 100644 --- a/building_guide/behaviors.md +++ b/building_guide/behaviors.md @@ -1,45 +1,45 @@ ## Behaviors -Behaviors are **inline configs** placed directly inside object YAML (`gather:`, `talk:`, -`use:`, `safespot:`) or mob YAML (`talk:`). Each section below shows the keys you can use under each behavior type. - -The examples below are object/mob files; remember the filename is the ID for these. +Behaviors are **inline configs** placed directly inside object YAML (`gather:`, `talk:`, `use:`, +`safespot:`) or mob YAML (`talk:`, `shop:`, `steal:`). Each section below shows the keys for +each behavior type. ### Gather (mining, fishing, woodcutting) -Mining — per-drop depletion on a rock object: +Gather configs live under the `gather:` key on an object. The object's filename is its ID. + +Mining — per-drop depletion on a rock: + ```yaml name: copper rock color: "B2" gather: skill: mining - tools: # requires item with matching tool_type + tools: - pickaxe success: - base: 0.40 # 40% base chance - per_level: 0.01 # +1% per skill level - cap: 0.95 # 95% max + base: 0.40 + per_level: 0.01 + cap: 0.95 gather_message: "You swing your pickaxe at the rock..." fail_message: "You chip away but get nothing useful." drops: - item_id: copper_ore - level: 1 # required skill level for this drop - xp: 17 # XP awarded when this drop lands - weight: 90 # 90% chance when roll succeeds - depletes: true # rock becomes depleted after this drop - message: "You manage to mine some {B2}copper ore{/}." - - table: gem_table # reference a shared drop table + level: 1 + xp: 17 + weight: 90 + depletes: true + success_message: "You manage to mine some {B2}copper ore{/}." + - table: gem_table weight: 10 - depletes: false # gem drops don't deplete the rock - message: "You spot a glint of something valuable!" - respawn_timer: 50 # ticks until rock respawns + depletes: false + success_message: "You spot a glint of something valuable!" + respawn_timer: 50 respawn_broadcast: "A glint of copper catches your eye from some {name}." ``` -Drop messages support inline color tags: `{<00-FF>}text{/}`. Use the item's color -index to match its display color. Gradients also work: `{g:C4,52}text{/}`. +Fishing — non-depleting: -Non-depleting gather (fishing on a fishing spot object): ```yaml name: fishing spot gather: @@ -55,14 +55,15 @@ gather: fail_message: "Nothing seems to bite." drops: - item_id: raw_trout - level: 5 # required skill level for this drop - xp: 20 # XP awarded when this drop lands + level: 5 + xp: 20 weight: 100 - depletes: false # never depletes - message: "You catch a {45}raw trout{/}!" + depletes: false + success_message: "You catch a {45}raw trout{/}!" ``` -Woodcutting with shared depletion and bird's nests: +Woodcutting — shared depletion with bird's nests: + ```yaml name: oak tree color: "71" @@ -78,18 +79,19 @@ gather: fail_message: "You swing but get no logs." drops: - item_id: oak_logs - level: 15 # required skill level for this drop - xp: 37 # XP awarded when this drop lands + level: 15 + xp: 37 weight: 100 - depletes: false # depletion is timer-based (deplete_timer) - message: "You get some {71}oak logs{/}." - respawn_timer: 14 # ticks until tree respawns after being cut down - deplete_timer: 45 # max ticks before next gather depletes (counts down while chopping) - nest_chance: 256 # 1/256 chance for a bird's nest on each successful gather + depletes: false + success_message: "You get some {71}oak logs{/}." + respawn_timer: 14 + deplete_timer: 45 + nest_chance: 256 respawn_broadcast: "An {name} grows back." ``` -Regular tree — always depletes on first gather, no shared timer, no nests: +Regular tree — always depletes on first gather: + ```yaml name: tree gather: @@ -104,11 +106,11 @@ gather: fail_message: "You swing but get no logs." drops: - item_id: logs - level: 1 # required skill level for this drop - xp: 25 # XP awarded when this drop lands + level: 1 + xp: 25 weight: 100 - depletes: true # regular tree depletes on first successful gather - message: "You get some {6B}logs{/}." + depletes: true + success_message: "You get some {6B}logs{/}." respawn_timer: 80 respawn_broadcast: "A {name} grows back." ``` @@ -116,46 +118,52 @@ gather: #### GatherConfig Fields | Field | Type | Description | -|---|---|---| +|-------|------|-------------| | `skill` | string | Skill name for level check | -| `tools` | []string | Required tool_type list (e.g. `[pickaxe]`) | +| `tools` | []string | Required tool_type list (e.g. `[pickaxe]`, `[axe]`) | +| `no_tool_speed` | float64 | Base wait in ticks when no tool is held | | `bait` | string | Required bait item (for fishing) | | `success` | SuccessFormula | Base + per_level, capped | -| `gather_message` | string | Custom "You gather..." message | -| `depleted_message` | string | Custom "The rock is depleted" message | -| `exhausted_message` | string | Custom "The tree falls" message | -| `fail_message` | string | Custom failure message | +| `gather_message` | string | Message when gathering starts | +| `depleted_message` | string | Message when resource is depleted | +| `exhausted_message` | string | Message when tree falls | +| `fail_message` | string | Message on failed gather | +| `broadcast_message` | string | Broadcast to room on successful gather | | `drops` | []DropEntry | Weighted drop entries | | `respawn_timer` | float64 | Ticks until depleted object respawns | -| `respawn_broadcast` | string | Broadcast message when object respawns | +| `respawn_broadcast` | string | Broadcast when object respawns | | `deplete_timer` | float64 | Ticks for shared depletion (trees) | | `nest_chance` | int | 1/N chance for bird's nest alongside normal drop | #### SuccessFormula | Field | Type | Description | -|---|---|---| -| `base` | float64 | Base success chance (0.0-1.0) | +|-------|------|-------------| +| `base` | float64 | Base success chance (0.0–1.0) | | `per_level` | float64 | Chance increase per skill level | | `cap` | float64 | Maximum success chance | #### DropEntry | Field | Type | Description | -|---|---|---| +|-------|------|-------------| | `item_id` | string | Item ID to drop | | `table` | string | Reference to a shared drop table in `data/drops/` | | `weight` | int | Relative drop weight | | `level` | int | Skill level required for this drop | | `xp` | int | XP awarded when this drop lands | | `depletes` | bool | Resource depletes on this drop | -| `quantity` | int | Amount to drop (1-3 for random) | -| `message` | string | Player message on drop | +| `quantity` | int | Amount to drop (1–3 for random) | +| `success_message` | string | Player message on drop (supports color tags) | +| `tool` | string | Only drops when this tool_type is active | -#### Shared depletion explained +Drop messages support inline color tags: `{<00–FF>}text{/}` and gradients `{g:C4,52}text{/}`. +Use the item's color index to match its display color. + +#### Shared depletion When `deplete_timer > 0`, the tree has a shared despawn timer: -- The timer starts at `deplete_timer` max when the first player begins chopping. +- Timer starts at `deplete_timer` max when the first player begins chopping. - Each tick, if anyone is chopping, the timer counts down. - When the timer reaches 0, the NEXT successful gather depletes the tree. - If no one is chopping and the tree isn't depleted, the timer ticks back UP. @@ -163,22 +171,27 @@ When `deplete_timer > 0`, the tree has a shared despawn timer: Use `deplete_timer` for trees. Use `depletes: true` on individual drops for rocks. -#### Bird's nests +#### Bird's nests and the `search` command -When `nest_chance > 0`, each successful gather has a 1/N independent chance to also drop -a bird's nest. The nest goes to inventory (or to the ground if inventory is full). -Use the `search` command to open nests — they roll on the `birds_nest_drop` table. +When `nest_chance > 0`, each successful gather has a 1/N independent chance to also drop a +bird's nest. The nest goes to inventory (or to the ground if inventory is full). -#### XP drops +Use the `search` command to open searchable items in your inventory: -Every successful drop awards the XP defined on that drop entry. If the player's `xpdrops` -toggle is on, the output includes the XP gain: `(+37xp wct)`. +``` +search <item> e.g. search nest + e.g. search birds nest +``` + +Searches your inventory for the named item. If found, removes it and rolls on a drop table +(currently `birds_nest_drop`). Only bird's nests are searchable at present. + +--- ### Talk (dialog trees) Talk configs go under the `talk:` key on objects or mobs. A conversation is a set of named -**nodes**, each with a message, optional action, optional auto-advance target, and choices -for the player. +**nodes**, each with messages, optional action, and choices for the player. ```yaml name: Guard @@ -198,13 +211,11 @@ talk: goto: has_pass condition: player_flag: got_pass - value: true - text: "\"Goodbye.\"" - # no goto = end conversation about_gate: messages: - - "\"Supplies. Weapons.\" He shifts his weight. \"Bring me some copper ore and I'll stamp you a pass.\"" + - "\"Supplies. Weapons. Bring me copper ore and I'll stamp you a pass.\"" action: set_player_flags: talked_to_guard: true @@ -239,120 +250,41 @@ talk: #### TalkNode fields | Field | Type | Description | -|---|---|---| -| `messages` | []string | NPC dialogue, shown one at a time. The player presses enter to advance through each message. Options appear after the last message. A single-element list works like a simple one-shot line. | -| `condition` | Condition | Optional. If the condition fails, the node is skipped entirely — its messages, action, and options are not shown. Use with `goto` to auto-advance to a different node. | -| `action` | NodeAction | Fires when the node is entered (give items, set flags, etc). | -| `options` | []TalkOption | Player choices. If empty and `goto` is set, auto-advances. | -| `goto` | string | Node ID to auto-advance to when there are no visible options OR when the node condition fails. | +|-------|------|-------------| +| `messages` | []string | NPC dialogue, shown one at a time. Player presses enter to advance. Options appear after the last message. | +| `message` | string/list | Single message or randomized list (picks one at random). | +| `condition` | Condition | If condition fails, the node is skipped via `goto`. | +| `action` | NodeAction | Fires when the node is entered. | +| `options` | []TalkOption | Player choices. Empty + `goto` set = auto-advance. | +| `goto` | string | Node ID to auto-advance to when no visible options or condition fails. | #### TalkOption fields | Field | Type | Description | -|---|---|---| -| `text` | string | Player-facing choice text. | -| `goto` | string | Node ID to navigate to if this choice is selected. Omit to end the conversation. | -| `condition` | Condition | Only show this option if the condition passes. | -| `action` | NodeAction | Fires when this option is selected, BEFORE navigating to `goto`. | - -#### Node action reference - -| Field | Effect | -|---|---| -| `set_global_flags` | Sets global flags (global, shared by all players). | -| `set_player_flags` | Sets player-local flags (per-character, quest progress). | -| `give_item` | Gives an item to the player's inventory. | -| `take_item` | Removes an item from the player's inventory. | -| `teleport` | Moves the player to a room ID. | -| `heal` | Restores that many hitpoints. | -| `credits` | Credits charged (negative) or awarded (positive). | -| `aps_node` | Marks this room's APS node as unlocked. | - -The full effect vocabulary (used by all interaction types and on_enter/trigger -steps) additionally includes `message`, `broadcast`, `broadcast_global`, -`spawn_mob`, `despawn_mob`, and `delay` — see [interaction -reference](#interaction-reference) below. - -> **Planned (not yet implemented):** `assign_task`, `skip_task`, `extend_task`, and `sawmill` are under design and will be added in a future update. - -All fields in a single action are processed together — give an item, take an item, set flags, -and heal in one node. - -Example — quest completion with multiple actions: -```yaml -action: - take_item: dragon_head - give_item: dragon_slayer_medal - set_player_flags: - dragon_quest: complete - dragon_slain: true - heal: 99 - teleport: 1 -``` - -#### Option-level actions - -Actions can be placed directly on options. The action fires when the player selects that -choice, BEFORE navigating to `goto`. This eliminates needing a separate "action node" for -simple transactions: - -```yaml -nodes: - start: - messages: - - "Welcome! Houses are 10 credits. Interested?" - options: - - text: "Yes, I'll buy one." - action: - credits: -10 - set_player_flags: - owns_house: true - goto: purchased - condition: - player_flag: owns_house - not: true - - text: "No thanks." - purchased: - messages: - - "Excellent! The plot is yours." - options: - - text: "Thanks!" - goto: start -``` - -If an option has both an `action` and a `goto`, the action runs first, then the player -navigates to the target node (which may also have its own action, fired on entry). +|-------|------|-------------| +| `text` | string | Player-facing choice text | +| `goto` | string | Node ID to navigate to. Omit to end conversation. | +| `condition` | Condition | Only show if condition passes | +| `action` | NodeAction | Fires when selected, BEFORE navigating to `goto` | #### Multi-message nodes -`messages` is a list of NPC lines shown one at a time. The player presses -enter to advance through each message. After the last message, the node's options -(if any) are displayed. This is the preferred way to deliver NPC monologues — no -named nodes or `goto` chains needed: +`messages` is a list of NPC lines shown one at a time with `[enter to continue]` prompts: ```yaml nodes: sign_reminder: messages: - "\"Yeah, just us two! Not a lot of people heading into the belt these days.\"" - - "\"But we'll be landing shortly! Please look at the information sign.\"" + - "\"But we'll be landing shortly! Look at the information sign.\"" options: - text: "\"Okay, thanks.\"" - text: "\"Goodbye.\"" ``` -Each `[enter to continue]` prompt is automatic. The node's action fires once on -entry, before the first message. Node conditions still work normally — -if the condition fails, the entire sequence is skipped via `goto`. - -If a multi-message node has no options after the last message and `goto` is set, it -auto-advances to the target node. Non-empty input during messages cancels the -conversation (just like invalid input does during option selection). - #### Randomized messages -`message` accepts either a plain string or a list. A list picks one entry at random each -time the node is visited: +Use a list under `message` to pick one at random each time the node is visited: ```yaml nodes: @@ -368,38 +300,30 @@ nodes: #### Linear auto-advance -When a node has no options (or all its options are hidden by conditions) and `goto` is set, -the conversation automatically advances to the target node. This creates dialogue chains -without prompting the player: +When a node has no visible options and `goto` is set, it auto-advances: ```yaml nodes: monologue_1: - message: "The elder clears his throat and begins..." + message: "The elder clears his throat..." goto: monologue_2 monologue_2: message: "\"Long ago, before the asteroid was settled...\"" goto: monologue_3 monologue_3: - message: "\"...a great darkness fell upon these halls.\" He pauses." + message: "\"...a great darkness fell upon these halls.\"" goto: choice_point choice_point: - message: "\"Do you understand the weight of what I'm telling you?\"" + message: "\"Do you understand?\"" options: - text: "\"I think so.\"" - text: "\"Not really.\"" ``` -If a node has both `goto` and options, the options take priority — `goto` is only used -when no options are visible. - #### Conditional nodes -A node can have its own `condition` that gates the entire node — message, action, -and options. When the condition fails, the node is skipped and the conversation -automatically advances to `goto` (if set). This lets you build branching first -messages or entire mutually-exclusive conversation paths based on player flags, -items, or any other condition: +A node with a `condition` gates the entire node. When the condition fails, the node is skipped +and goes to `goto`. This lets you branch the entire conversation based on player state: ```yaml nodes: @@ -407,107 +331,134 @@ nodes: condition: player_flag: finished_tutorial not: true - message: "\"Welcome, newcomer! Need any help getting started?\"" + messages: + - "\"Welcome, newcomer! Need help getting started?\"" goto: returning_player options: - text: "\"Yes, where do I go?\"" goto: directions - - text: "\"I'll figure it out.\"" returning_player: - message: "\"Ah, you're back! I heard you handled that raid beautifully.\"" + messages: + - "\"Ah, you're back! I heard you handled that raid.\"" options: - text: "\"The loot was worth it.\"" - - text: "\"Barely made it out alive.\"" - directions: - message: "\"Head north through the gate and follow the road.\"" +``` + +When `finished_tutorial` is set, `start` fails → jumps to `returning_player`. When unset, +the player sees the newcomer message and options. Node conditions compose with option +conditions: if the node condition passes, each option's condition is checked independently. + +#### Option-level actions + +Actions on options fire when selected, BEFORE navigating to `goto`: + +```yaml +nodes: + start: + messages: + - "Houses are 10 credits. Interested?" options: - - text: "\"Thanks!\"" + - text: "Yes, I'll buy one." + action: + credits: -10 + set_player_flags: + owns_house: true + goto: purchased + condition: + player_flag: owns_house + not: true + - text: "No thanks." + purchased: + messages: + - "The plot is yours. Use the Estate Directory to enter." + options: + - text: "Thanks!" + goto: start ``` -When `finished_tutorial` is set, the `start` node's condition fails, so the player -never sees "Welcome, newcomer" — it jumps straight to `returning_player`. When the -flag is unset, the condition passes and the player sees the newcomer message and its -options. +If an option has both `action` and `goto`, action runs first, then navigation. + +#### Node/option action vocabulary + +| Field | Effect | +|-------|--------| +| `set_global_flags` | Sets global flags (shared by all players) | +| `set_player_flags` | Sets player-local flags (per-character, saved to YAML) | +| `give_item` | Gives an item to the player's inventory | +| `take_item` | Removes an item from the player's inventory | +| `teleport` | Moves the player to a room ID | +| `heal` | Restores that many hitpoints (clamped to MaxHP) | +| `credits` | Positive = award, negative = charge | +| `aps_node` | Marks this room's APS node as unlocked | + +All fields in a single action are processed together: + +```yaml +action: + take_item: dragon_head + give_item: dragon_slayer_medal + set_player_flags: + dragon_quest: complete + dragon_slain: true + heal: 99 + teleport: 1 +``` -Node conditions compose cleanly with option conditions. When the node condition -passes, each option's own condition is then checked independently to determine -visibility — exactly as option conditions always work. When the node condition -fails, the node (and all its options) are skipped entirely. +--- ### Shops -A shop is a **root-level `shop:` property on a mob def** (not part of the talk tree). -Any mob in a room that has a `shop:` block can be traded with directly from the game -prompt — there is no separate shop mode. Players use the ordinary commands: +A shop is a **root-level `shop:` property on a mob** (not part of the talk tree). Any mob +with a `shop:` block can be traded with from the game prompt — no separate shop mode: ``` -list Show the shop inventory of the mob in the room -buy <item> Buy an item (partial names work, e.g. "buy bait") +list Show shop inventory +buy <item> Buy an item (partial names work) buy <N> <item> Buy a quantity -buy all <item> Buy as many as you can afford and carry -sell <item> Sell an item from your inventory +buy all <item> Buy as many as you can afford/carry +sell <item> Sell an item sell <N> <item> Sell a quantity sell all <item> Sell your entire held stack ``` -When a room has more than one shop mob, disambiguate with the mob name: - -``` -list <mob> -buy <item> from <mob> -sell <item> to <mob> -``` - -If several shops are present and no mob is named, the game replies "Which shop?" -and lists the options. +With multiple shops in a room, disambiguate: `list <mob>`, `buy <item> from <mob>`, `sell +<item> to <mob>`. #### Pricing Shops never define per-item prices — prices derive from each item's `value:` field: - -- **Buy price** (player buying) is always **100% of the item's value**. -- **Sell price** (player selling) uses the OSRS store formula: - - ``` - sell = value × max(10, buy_percentage − stock × change_percentage) / 100 - ``` - - where `stock` is the shop's current holding of that item. With the defaults - (`buy_percentage: 40`, `change_percentage: 3`), an item the shop has none of - sells for 40% of value, dropping 3% per unit already in stock down to a floor - of 10% of value (reached at 10 in stock). Selling multiple units in one command - prices each unit at the rising stock level. +- **Buy price** is always **100% of the item's value**. +- **Sell price** uses the OSRS store formula: `value × max(10, buy_percent − stock × + change_percent) / 100`. With defaults (buy_percent: 40, change_percent: 3), an item sells + for 40% of value at zero stock, dropping 3% per unit in stock, floor 10%. #### Stock and restocking -Each shop item carries a `stock` (its target/maximum, and its starting amount). -Buying depletes stock — an item at 0 stock is out of stock. Selling raises stock, -lowering subsequent sell prices. Every `restock_ticks` the current stock moves one -step toward the configured target (refilling when low, decaying overstock when -high). An item that omits `restock_ticks` uses the global default from -`game_constants.shop_default_restock` in `config.yaml` (1000 ticks). Items a player -sold that the shop didn't originally list are held as dynamic stock that decays -back to 0. +Each shop item carries a `stock` (target/max and starting amount). Buying depletes stock; +selling raises stock, lowering sell prices. Every `restock_ticks` the current stock moves +one step toward the target. Items with no `restock_ticks` use the global default (1000 +ticks). Items a player sold that the shop didn't list are held as dynamic stock that decays +to 0. #### Shop fields | Field | Description | -|---|---| -| `shop.message` | Optional greeting shown at the top of `list`. | -| `shop.buy_percentage` | Sell price at zero stock, as % of value (default 40). | -| `shop.change_percentage` | Sell price drop per unit of stock, as % of value (default 3). | -| `shop.buys_anything` | Whether the shop buys items not in its list (default true). | -| `shop.items[].item_id` | Item definition ID. | -| `shop.items[].stock` | Target/maximum and starting stock. | -| `shop.items[].restock_ticks` | Per-item restock interval (default `game_constants.shop_default_restock`, 1000). | - -Full example — General Store: +|-------|-------------| +| `shop.message` | Optional greeting shown at top of `list` | +| `shop.buy_percentage` | Sell price at zero stock, % of value (default 40) | +| `shop.change_percentage` | Sell price drop per unit of stock, % of value (default 3) | +| `shop.buys_anything` | Accept items not in its list (default true) | +| `shop.items[].item_id` | Item definition ID | +| `shop.items[].stock` | Target/maximum and starting stock | +| `shop.items[].restock_ticks` | Per-item restock interval (default 1000) | + +Example — General Store: + ```yaml name: Shopkeeper protected: true -hp: 50 shop: - message: "\"Welcome to the General Store! Have a look around.\"" + message: "\"Welcome to the General Store!\"" items: - item_id: fishing_rod stock: 10 @@ -518,114 +469,78 @@ shop: talk: nodes: start: - message: "\"Welcome! Type 'list' to see my wares.\"" + message: "\"Type 'list' to see my wares.\"" options: - text: "\"Goodbye.\"" ``` -A specialty shop that only buys back its own stock sets `buys_anything: false`. +--- -### Object Interactions (levers, switches, gates) — `on_use` and `on_look` +### Interaction Reference -Interactions are the unified, conditional action system shared by objects -(`on_use`, `on_look`), mobs (`on_kill`), exit traversal -(`on_traverse`), on_enter steps, room/global triggers, talk nodes, and talk -options. Each interaction has an optional condition, an optional message, -and an optional `action` (the full effect vocabulary). When a list of -interactions is evaluated, the first whose condition passes wins and fires. +The `Interaction` struct is the shared shape used by `on_use`, `on_look`, `on_kill`, +`on_traverse`, on_enter steps, and trigger steps. When a list of interactions is evaluated, +the first whose condition passes wins and fires. -On objects, `on_use` runs when the player types `use <obj>` (bare) or -`use <item> on <obj>` (item-specific). Entries with no `item_id` are bare (fire when `use <obj>` is typed) — -triggered by `use <obj>`: +| Field | Type | Description | +|-------|------|-------------| +| `item_id` | string | `on_use`: required item (empty = bare `use <obj>`). `on_look`: only fires if carrying this item. `on_kill`: only fires if wielding this weapon. | +| `condition` | Condition | Optional gate | +| `message` | string | Message shown to the player when this entry fires | +| `action` | StepAction | Optional effects (see below) | -```yaml -name: iron gate -hidden: true -description: "A heavy iron gate set into the north wall." -on_use: - - condition: - global_flag: gate_open - value: true - not: true - message: "You push the heavy iron gate open." - action: - set_global_flags: - gate_open: true -``` +#### StepAction / NodeAction vocabulary -Item-specific interaction: -```yaml -name: bookshelf -on_use: - - item_id: dusty_tome - message: "The bookshelf slides aside, revealing a secret passage!" - action: - set_global_flags: - secret_passage_open: true -``` +The universal effect vocabulary used by all interaction types, talk node actions, on_enter +steps, and trigger steps: -`on_look` runs after the description is shown when the player types -`look <obj>`. It is a list of interactions (same shape), with `item_id` -acting as a *has_item* gate: an entry with an `item_id` only fires if the -player currently carries that item in their inventory (empty = always fires -on look). -typically empty: +| Field | Type | Description | +|-------|------|-------------| +| `set_global_flags` | map[string]any | Set global flags (cascades other triggers) | +| `set_player_flags` | map[string]any | Set player flags (per-character, saved to YAML) | +| `give_item` | string | Give one unit of an item to inventory | +| `take_item` | string | Remove one unit of an item from inventory | +| `teleport` | int | Move player to a room ID | +| `heal` | int | Restore hitpoints (clamped to MaxHP) | +| `credits` | int | Add (positive) or deduct (negative) credits | +| `aps_node` | bool | Mark current room as discovered APS node | +| `message` | string | Direct message to the player (`%p`/`%v` supported) | +| `broadcast` | string | Announce to all in the room | +| `broadcast_global` | string | Announce to all online | +| `spawn_mob` | string/map | Spawn a transient mob | +| `despawn_mob` | string | Despawn all transient mobs of this id | +| `delay` | int | Ticks to wait (on_enter/triggers only) | +| `condition` | Condition | Per-step gate (on_enter/trigger steps only) | + +#### SpawnMobConfig + +For transient mob spawning, pass either a bare string (the mob ID) or a map: ```yaml -name: sign -on_look: - - set_player_flags: - read_sign: true +spawn_mob: rat + +# Equivalent to: +spawn_mob: + id: rat ``` +Full config: + +| Field | Description | +|-------|-------------| +| `id` | Mob ID to spawn (required) | +| `owner_only` | Private to the triggering player | +| `despawn_on_leave` | Fades when owner leaves allowed rooms | +| `despawn_rooms` | Rooms where the spawn survives | +| `despawn_ticks` | Despawn countdown in ticks | + --- -### Interaction Reference +### On Kill — Mob Interactions -The `Interaction` struct is the shared per-entry shape: - -| Field | Type | Description | -| ----------- | ---------- | --------------------------------------------------------------- | -| `item_id` | string | `on_use`: required item id (empty = bare `use <object>`). `on_look`: only fires if you carry this item. `on_kill`: only fires if you wield this weapon (main_hand or off_hand) when the mob is defeated. | -| `condition` | Condition | Optional gate (see [conditions](conditions.md)) | -| `message` | string | Message shown to the player when this entry fires | -| `action` | StepAction | Optional effects — the full superset below | - -The `StepAction` superset is the universal effect container used by every -interaction type, talk node actions, on_enter steps, and trigger steps. The -inline-only `NodeAction` fields are the subset historically used by talk -nodes; the additional sequence-only fields (`message`, `broadcast`, -`broadcast_global`, `spawn_mob`, `despawn_mob`, `delay`) are available on -all of them but primarily meaningful for on_enter/triggers (inline callers -may use `broadcast` to announce to the room — handy for `on_kill` -announcements). - -| Field | Type | Description | -| ------------------ | -------------- | -------------------------------------------------------------------- | -| `set_global_flags` | map[string]any | Set global flags (shared by all players; cascades other triggers) | -| `set_player_flags` | map[string]any | Set player flags (per-character, saved to YAML; cascades) | -| `give_item` | string | Give one unit of an item to inventory | -| `take_item` | string | Remove one unit of an item from inventory | -| `teleport` | int | Move player to a room ID; runs `look` + `on_enter` of the destination | -| `heal` | int | Restore hitpoints (clamped to MaxHP) | -| `credits` | int | Add (positive) or deduct (negative) credits (gated by affordability) | -| `aps_node` | bool | Mark the current room as a discovered APS node on the datapad | -| `message` | string | Direct message to the player (template vars `%p`/`%v` supported) | -| `broadcast` | string | Announce to all in the room (`%p`/`%v` substituted per recipient) | -| `broadcast_global` | string | Announce to all online (`%p`/`%v` substituted per recipient) | -| `spawn_mob` | string/map | Spawn a transient mob — bare string (id) or full [config](#spawn-mob-config) | -| `despawn_mob` | string | Despawn all transient mobs of this id (optionally owner-filtered) | -| `delay` | int | Ticks to wait before this step fires (on_enter/triggers only) | -| `condition` | Condition | Per-step gate (on_enter steps and trigger steps; evaluated before firing) | - -### Mob Interactions (On Kill) - -Mobs can define `on_kill` (fires when this mob is defeated — either by a -combat kill or by a `kind: task` mob's HP draining to zero, the -"completion" of the work). It is additive — standard loot `drops` still -hit the ground first, then the first matching interaction fires. 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. +Mobs can define `on_kill` — fires when the mob is defeated. Standard loot `drops` hit the +ground first, then the first matching interaction fires. An entry with `item_id` only fires +if the player wields that weapon: ```yaml name: boss @@ -633,7 +548,7 @@ combat: ... on_kill: - condition: global_flag: boss_quest_active - message: "The boss crumbles to dust. The Guardian Stone reverberates!" + message: "The boss crumbles to dust!" action: set_global_flags: boss_slain: true @@ -646,42 +561,16 @@ on_kill: dragon_slain: true ``` +Task mob `on_kill` works the same way — fires when the work is completed: + ```yaml name: reactor panel task: ... on_kill: - - message: "With a final burst of effort, the panel snaps into place." + - message: "The panel snaps into place." action: set_global_flags: reactor_repaired: true set_player_flags: repaired_reactor: true ``` - -### Spawn Mob Config - -For transient mob spawning (in `spawn_mob:` fields), pass either a bare -string (the mob ID) or a map with these fields: - -```yaml -spawn_mob: rat -``` - -```yaml -spawn_mob: - id: boss_add - owner_only: true - despawn_on_leave: true - despawn_rooms: [100, 101] - despawn_ticks: 30.0 -``` - -| Field | Description | -| ----------------- | ------------------------------------------------------------------------ | -| `id` | Mob ID to spawn (required when a map) | -| `owner_only` | If true, the spawn is private to the triggering player | -| `despawn_on_leave`| Trigger-spawned mob fades after leaving the owner's room (grace window) | -| `despawn_rooms` | List of rooms where the spawn survives without ticking the despawn timer | -| `despawn_ticks` | Despawn countdown in ticks (float OK; rounded probabilistically via `engine.ToTicks`) | - ---- diff --git a/building_guide/conditions.md b/building_guide/conditions.md deleted file mode 100644 index 2fb3d7a..0000000 --- a/building_guide/conditions.md +++ /dev/null @@ -1,83 +0,0 @@ -## Conditions Reference - -Conditions are used in talk option guards, talk node conditions, exit gates, -on-enter scripts, on_use / on_look / on_kill interactions, -exit on_traverse interactions, room descriptions, object descriptions, and -trigger value matching. - -### Simple conditions - -A bare `global_flag:` / `player_flag:` check passes when the flag is **set to a truthy -value** (`true`, a non-zero number, a non-empty string). Add `value:` only when -you need to match a specific value (e.g. a numeric quest stage). `not: true` -inverts any check. - -```yaml -# Check a global flag is set -condition: - flag: gate_open - -# Check a global flag is NOT set -condition: - flag: gate_open - not: true - -# Check a player flag is set -condition: - player_flag: finished_tutorial - -# Check a player flag is NOT set (e.g. only on the first visit) -condition: - player_flag: finished_tutorial - not: true - -# Match a specific (non-boolean) value -condition: - player_flag: quest_stage - value: 3 - -# Check if player has an item -condition: - has_item: bronze_key - -# Check if player does NOT have an item -condition: - has_item: bronze_key - not: true - -# Check if player has enough credits -condition: - min_credits: 50 -``` - -### Compound conditions - -All must pass: -```yaml -condition: - all_of: - - flag: gate_open - - has_item: pass_stub -``` - -Any one must pass: -```yaml -condition: - any_of: - - has_item: bronze_key - - has_item: iron_key - - player_flag: master_of_unlocking -``` - -Nested compounds: -```yaml -condition: - all_of: - - player_flag: quest_started - - any_of: - - has_item: wolf_pelt - - has_item: bear_pelt -``` - ---- - diff --git a/building_guide/construction.md b/building_guide/construction.md index dd63fd7..fef4a7b 100644 --- a/building_guide/construction.md +++ b/building_guide/construction.md @@ -1,216 +1,209 @@ -# Construction Skill +## Construction -The Construction skill allows players to process logs into planks and assemble planks into furniture at a workbench. Higher-tier logs produce more valuable planks and furniture. +The Construction skill lets players saw logs into planks and assemble planks into furniture at a +workbench. Recipes live on the output item's YAML via the `craft:` block — just like every other +crafting skill (see `recipes.md`). -> **Filenames are IDs.** The item/object YAML shown below have no `id:` field — each file's name is its ID (e.g. `construct_oak_planks.yaml` → `construct_oak_planks`). See `items.md` / `objects.md`. +### Required Objects -## Skill - -- Skill name: `construction` -- Abbreviation: `con` -- Level range: 1-99 -- XP table: classic standard - -## Commands - -| Command | Description | -|---------|-------------| -| `construct` / `make` | Open construction menu at a workbench | -| `construct <item>` | Build a specific item | -| `construct <qty> <item>` | Build multiple of an item | - -## Objects - -### workbench - -The primary construction station. Placed in rooms to enable construction. +**Workbench** (`data/objects/workbench.yaml`) — the station where construction happens: ```yaml name: workbench color: "AC" -description: "A sturdy wooden workbench..." -inroom_description: "A workbench stands against the wall..." +description: + - text: "A sturdy wooden workbench covered in sawdust, nails, and blueprints." +inroom_description: "A workbench stands against the wall, cluttered with tools and wood shavings." ``` -### estate_directory - -A terminal that shows registered homeowners and lets them enter their house. Handled entirely in Go code (`internal/game/cmd_estate_directory.go`), not via YAML behaviors. +**Estate Directory** (`data/objects/estate_directory.yaml`) — terminal for homeowners: ```yaml name: Estate Directory color: "27" -description: "A holographic terminal displaying property records..." -inroom_description: "An estate directory terminal..." +description: + - text: "A holographic terminal displaying property records." +inroom_description: "An estate directory terminal stands in the center of the neighborhood." ``` -When a player looks at the directory, it scans `data/players/characters/*.yaml` for the `owns_house_local_neighborhood` player flag and lists all homeowners. When a player uses or talks to the directory and owns a house, they can teleport to their house entrance room. - -## Tools +The directory is handled by Go code: when a player looks at or uses it, it scans character +YAML files for the `owns_house_local_neighborhood` player flag and lets homeowners teleport +into their house. -### saw +### Tools -Required tool for cutting logs into planks at the workbench. +**Saw** (`data/items/tools/saw.yaml`) — required for log-to-plank recipes: ```yaml name: saw -tool_type: saw +color: "98" +description: "A sharp-toothed saw for cutting planks from logs at a workbench." +tool: + type: saw value: 30 ``` -## Items - ### Planks (4 tiers) -| Item ID | Name | Level | Value | Made From | -|---------|------|-------|-------|-----------| -| `planks` | planks | 1 | 20 | logs | -| `oak_planks` | oak planks | 15 | 40 | oak_logs | -| `teak_planks` | teak planks | 35 | 80 | teak_logs | -| `mahogany_planks` | mahogany planks | 50 | 120 | mahogany_logs | +Recipes on each plank item convert logs into planks at the workbench with a saw: + +```yaml +# data/items/materials/planks.yaml +name: planks +color: "89" +value: 20 +craft: + - type: construction + level: 1 + xp: 5 + ticks_per_cycle: 20 + station: [workbench] + tool: saw + ingredients: + - items: [logs] + quantity: 1 + success_message: "You saw the logs into %n." +``` + +```yaml +# data/items/materials/oak_planks.yaml +name: oak planks +color: "66" +value: 40 +craft: + - type: construction + level: 15 + xp: 10 + ticks_per_cycle: 20 + station: [workbench] + tool: saw + ingredients: + - items: [oak_logs] + quantity: 1 + success_message: "You saw the oak logs into %n." +``` -### Furniture (sellables) +| Item | Level | Logs → | Value | +|------|-------|--------|-------| +| `planks` | 1 | `logs` | 20 | +| `oak_planks` | 15 | `oak_logs` | 40 | +| `teak_planks` | 35 | `teak_logs` | 80 | +| `mahogany_planks` | 50 | `mahogany_logs` | 120 | -| Item ID | Level | Planks Required | -|---------|-------|-----------------| -| `wooden_shelf` | 4 | 2 planks | -| `wooden_table` | 6 | 3 planks | -| `wooden_chair` | 8 | 2 planks | -| `oak_shelf` | 18 | 2 oak planks | -| `oak_table` | 20 | 3 oak planks | -| `teak_table` | 38 | 3 teak planks | -| `mahogany_table` | 52 | 3 mahogany planks | +### Furniture -## Recipes +Furniture recipes consume planks at the workbench (no saw needed): -All recipes use `type: construction` and `station: [workbench]`. Log-to-plank recipes additionally require `tool: saw`. +```yaml +# data/items/misc/wooden_chair.yaml +name: wooden chair +color: "82" +value: 35 +craft: + - type: construction + level: 8 + xp: 30 + ticks_per_cycle: 6 + station: [workbench] + ingredients: + - items: [planks] + quantity: 2 +``` -Example plank recipe: ```yaml -type: construction -level: 15 -xp: 10 -wait: 20 -station: [workbench] -tool: saw -consume: - - items: [oak_logs] - quantity: 1 -output: oak_planks -message: "You saw the oak logs into sturdy oak planks." +# data/items/misc/wooden_table.yaml +name: wooden table +color: "82" +value: 50 +craft: + - type: construction + level: 6 + xp: 25 + ticks_per_cycle: 6 + station: [workbench] + ingredients: + - items: [planks] + quantity: 3 ``` -Example furniture recipe: ```yaml -type: construction -level: 6 -xp: 25 -wait: 6 -station: [workbench] -consume: - - items: [planks] - quantity: 3 -output: wooden_table -message: "You build a sturdy wooden table." +# data/items/misc/oak_table.yaml +name: oak table +color: "5E" +value: 100 +craft: + - type: construction + level: 20 + xp: 50 + ticks_per_cycle: 6 + station: [workbench] + ingredients: + - items: [oak_planks] + quantity: 3 ``` -## NPCs +| Item | Level | Planks | XP | +|------|-------|--------|----| +| `wooden_shelf` | 4 | 2 planks | 15 | +| `wooden_table` | 6 | 3 planks | 25 | +| `wooden_chair` | 8 | 2 planks | 30 | +| `oak_shelf` | 18 | 2 oak planks | 35 | +| `oak_table` | 20 | 3 oak planks | 50 | +| `teak_table` | 38 | 3 teak planks | 75 | +| `mahogany_table` | 52 | 3 mahogany planks | 100 | -### estate_broker +### NPCs -Located in room 16 (Construction Site). Talk behavior sells a house plot for 10 credits via `cost` + `set_player_flags`. Sets `owns_house_local_neighborhood: "local_neighborhood"` on purchase. +**Estate Broker** — sells a house plot for 10 credits. Talk node action uses `credits` (negative = charge) and `set_player_flags`: ```yaml name: estate broker +protected: true +unique: true talk: nodes: start: - message: "\"Interested in a house?\"" + messages: + - "Welcome! I'm selling housing plots for 10 credits. Interested?" options: - - text: "\"How much?\"" - goto: offer - - text: "\"No thanks.\"" - offer: - message: "\"10 credits for a plot.\"" - action: - cost: 10 - set_player_flags: - owns_house_local_neighborhood: "local_neighborhood" + - text: "Yes, I'd like to buy a house." + condition: + player_flag: owns_house_local_neighborhood + not: true + action: + credits: -10 + set_player_flags: + owns_house_local_neighborhood: local_neighborhood + goto: purchased + - text: "No thanks." + goto: goodbye + purchased: + messages: + - "The plot is yours. Use the Estate Directory to enter." options: - - text: "\"Deal!\"" -unique: true -``` - -### sawmill_operator - -Located in room 171 (Lumberyard). Talk behavior processes all logs in inventory into planks for a credit fee. Uses the `sawmill: true` node action which triggers `processSawmill()` in `internal/game/action_talk.go`. - -Fees: regular (5cr), oak (10cr), teak (20cr), mahogany (40cr). - -## Science Mod: Plank Make - -Level 86 Science utility mod that converts logs to planks at 70% of sawmill cost. Uses naturejunk, solarjunk, and scrap. - -```go -{ID: "plank_make", Name: "Plank Make", Level: 86, BaseXP: 90.0, - JunkCost: map[string]int{"naturejunk": 5, "solarjunk": 1, "scrap": 1}, - Category: ModUtility, TargetType: "inventory"}, + - text: "Thanks!" + goto: start + goodbye: + messages: + - "Come back anytime!" ``` -Usage: `trigger plank make` (all logs) or `trigger plank make <log type>` (specific). - -## Player Houses +**Sawmill Operator** — converts logs to planks for credits via dialog. Fees per log: regular (5cr), oak (10cr), teak (20cr), mahogany (40cr). Use option conditions with `any_of` + `has_item` to gate the "process my logs" choice. -Player houses are real YAML rooms stored in `data/rooms/player_housing/`. Each house area (e.g. Local Neighborhood) has entrance + workshop rooms with unique room IDs in the 200+ range. +### Player Houses +Player houses are real YAML rooms in `data/rooms/player_housing/`. Each area has entrance + workshop rooms. House room files follow the same format as world rooms: + ```yaml name: "Player House: Entrance Hall" -description: "..." +description: "A modest but welcoming entrance hall." exits: south: 170 north: 201 objects: - - id: charging_station -``` - -The room loader (`World.LoadRoom`) checks `data/rooms/<id>.yaml` first, then falls back to `data/rooms/player_housing/<id>.yaml`. - -Ownership is tracked via player flags (e.g. `owns_house_local_neighborhood: "local_neighborhood"`). The Estate Directory scans character YAML files for these flags. - -## Implementation - -### Production Integration - -Construction is added to `productionTypes` in `action_production.go`: -```go -"construction": {"construct", "constructing"}, + - id: workbench ``` -This enables automatic support through the unified production system (`advanceProduction`, `startProductionFromRecipe`, etc.). - -### Command - -`cmd_construct.go` follows the same pattern as `cmd_craft.go`, using per-recipe `Station` and `Tool` checks. Station: `workbench`, Tool: `saw` (for plank recipes only). - -### Option - -`construct_all` (OptBool, default false): Auto-start construction when only one product is available. - -## Data File Summary - -| Path | Purpose | -|------|---------| -| `data/objects/workbench.yaml` | Workbench station object | -| `data/objects/estate_directory.yaml` | Directory terminal object | -| `data/items/saw.yaml` | Saw tool | -| `data/items/planks.yaml` - `mahogany_planks.yaml` | Plank items (4 tiers) | -| `data/items/wooden_shelf.yaml` - `mahogany_table.yaml` | Furniture items (7 items) | -| Item YAMLs with `craft:` blocks (planks, furniture) | Construction recipes — inline on output item YAML | -| `data/mobs/estate_broker.yaml` | Estate broker mob (talk inline) | -| `data/mobs/sawmill_operator.yaml` | Sawmill operator mob (talk inline) | -| `data/rooms/16.yaml` | Construction Site (updated) | -| `data/rooms/170.yaml` | Local Neighborhood | -| `data/rooms/171.yaml` | Lumberyard | -| `data/rooms/player_housing/200.yaml` | Player house entrance | -| `data/rooms/player_housing/201.yaml` | Player house workshop | -| `data/help/construct.yaml` | Help topic | +Ownership is tracked via player flags. The Estate Directory scans character files for flags +like `owns_house_local_neighborhood` to show registered homeowners. diff --git a/building_guide/courses.md b/building_guide/courses.md index 71dd255..e799002 100644 --- a/building_guide/courses.md +++ b/building_guide/courses.md @@ -51,6 +51,7 @@ obstacles: | -------------- | ------------- | ---------------------------------------------------------------------------- | | `room_id` | int | Room ID for this obstacle | | `verb` | string | Command the player types to attempt it (e.g. `scramble`, `jump`, `climb`) | +| `exit_dir` | string | Direction for the always-blocked exit created by admin GUI between obstacles (e.g. `south`, `east`) | | `xp` | int | XP awarded for successfully completing this single obstacle | | `fail_damage` | [2]int | `[min, max]` damage on failure (HP clamped to minimum 1 — cannot kill) | | `fail_chance` | float64 | Optional explicit failure probability (0–1). Omit to derive dynamically. | diff --git a/building_guide/doors.md b/building_guide/doors.md deleted file mode 100644 index c987d0f..0000000 --- a/building_guide/doors.md +++ /dev/null @@ -1,93 +0,0 @@ -## Global vs Player State: Door Examples - -> **Filenames are IDs.** Object and room YAML below carry no `id:` field — the filename is the ID (e.g. `door_button.yaml` → `door_button`). A nested `- id:` under a room's `objects:` list is a *reference* and stays. - -### Example A: Door with a button in another room (GLOBAL) - -A button in room 3 opens a door in room 7. Anyone can press it. Once pressed, the door is open for everyone. - -**Button object** (`data/objects/door_button.yaml`): -```yaml -name: stone button -hidden: true -on_use: - - condition: - global_flag: secret_door_open - not: true - message: "You press the stone button. You hear grinding stone in the distance." - action: - set_global_flags: - secret_door_open: true -``` - -**Room 3** — contains the button: -```yaml -name: "Button Chamber" -description: "A small stone chamber. A button protrudes from the east wall." -exits: - south: 1 -objects: - - id: door_button -``` - -**Room 7** — contains the door: -```yaml -name: "Hidden Passage" -description: "A dusty corridor. A heavy stone door blocks the way north." -exits: - south: 2 - north: - room: 8 - condition: - flag: secret_door_open # checks WORLD flag - value: true - blocked_message: "A heavy stone door blocks the way." -objects: - - id: stone_door - hidden: true -``` - -### Example B: Key-locked door (PLAYER-LOCAL) - -A locked door that only opens for a player carrying the key. Each player must find their own key. - -**Key item** (`data/items/rusty_key.yaml`): -```yaml -name: rusty key -description: "An old iron key, still functional." -value: 0 -stackable: false -``` - -**Room 5** — locked door: -```yaml -name: "Locked Storage" -description: "A small storage room. The way east is blocked by a locked iron door." -exits: - west: 2 - east: - room: 6 - condition: - has_item: rusty_key # checks PLAYER inventory - blocked_message: "The iron door is locked. You need a key." -objects: - - id: iron_door - hidden: true -``` - -**Room 6** — the other side (no key needed to exit): -```yaml -name: "Storage Closet" -description: "Shelves of dusty crates." -exits: - west: 5 # exit back — no condition -spawns: - - item_id: uncut_ruby - quantity: 1 - respawn_ticks: 500 -``` - -Key difference: the button door uses `global_flag` (shared state — one player presses, everyone benefits), the key door uses `has_item` (per-player inventory check — each player needs their own key). - ---- - diff --git a/building_guide/hidden_objects.md b/building_guide/hidden_objects.md deleted file mode 100644 index 1866d67..0000000 --- a/building_guide/hidden_objects.md +++ /dev/null @@ -1,92 +0,0 @@ -## Hidden Objects - -Objects with `hidden: true` don't appear in the room's object listing. Players discover them by reading room descriptions or trying commands. The object is still fully interactable — `push gate`, `look gate`, etc. - -**The filename is the ID** (`secret_lever.yaml` → `secret_lever`); don't add an `id:` field (see `objects.md`). - -```yaml -# data/objects/secret_lever.yaml -name: stone lever -hidden: true -description: "A cleverly concealed lever behind a loose stone." -on_use: - - condition: - global_flag: secret_passage_open - value: true - not: true - message: "You pull the lever. A grinding sound echoes from the east." - action: - set_global_flags: - secret_passage_open: true -``` - -Room description hints at it: -```yaml -description: "A dusty corridor. One of the wall stones looks slightly out of place." -``` - -### Multiple names (`aliases`) - -By default an object matches its `name` (word-prefix matching) and the words in -its `id`. Add `aliases` to accept extra names: - -```yaml -name: landing craft # matches "landing", "craft", "landing craft" -aliases: [shuttle, ship] # also matches "shuttle" and "ship" -hidden: true -``` - -If a player's input matches more than one distinct object, `look` lists the -candidates ("That's ambiguous, which one?") instead of guessing — so keep aliases -specific enough to avoid overlap between objects in the same room. - -### Descriptions that react to flags - -An object's `description` can be a plain string or a list of conditional variants -(first matching condition wins). This is the simplest way to fake per-player -scenery — a crowd that becomes a queue, an NPC that "appears" partway through a -scene — without spawning real objects or mobs. - -**If none of the variants match, the object is treated as absent for that -player** — `look <it>` reports nothing is there. Combined with `hidden: true`, the -same shared object can appear only while a player's flags warrant it (e.g. during -an intro) and vanish on return visits. - -```yaml -name: crowd of people -aliases: [people, crowd, passengers] -hidden: true -description: - - condition: # after the pilot arrives - all_of: - - player_flag: boarded - not: true - - player_flag: lined_up - text: "The passengers have formed a single-file line." - - condition: # before the pilot arrives - all_of: - - player_flag: boarded - not: true - - player_flag: lined_up - not: true - text: "A couple dozen anxious passengers mill about the pad." - # once `boarded` is set, no variant matches -> the crowd is gone -``` - -A bare string in `description:` maps to a single unconditional entry, so a plain -object is always present as before. - -### Safespot Objects - -Safespot objects should always be `hidden: true`. They only become visible in `look` when the player's effective safespot tier meets the object's `safespot.tier` requirement. This creates a natural discovery mechanic: completing quests and achievements reveals new coverage opportunities. - -Players can discover safespots by: -- Reading room descriptions for hints -- Trying `hide <object_name>` (name matching works even for hidden objects) -- Using `look <object>` once they know the name -- Gaining tier through progression, which reveals safespots in `look` - -See [Safespots](objects.md#safespots) for the full YAML format. - ---- - diff --git a/building_guide/items.md b/building_guide/items.md index e03622d..6280993 100644 --- a/building_guide/items.md +++ b/building_guide/items.md @@ -162,52 +162,24 @@ heal_value: 5 # positive = heal, negative = damage eat_message: "You eat the bread. Warm and satisfying." ``` -## MadeFrom — Item Combinations +## Item Combinations -Define what items combine to make this item. Used by the `use` command. Combinations run through the production system — players are prompted "How many?" and the action loops with a timer. +Item-on-item combinations use the `craft:` block (same as all other recipes). Define the +output item with `type: ""` (or omit it) and the ingredients. See `recipes.md` for the full +craft reference — byproducts, multi-item ingredient slots, message variables, and multi-step +assembly. -```yaml -name: bread dough -color: "DE" -ticks: 2 -made_from: - - items: [pot_of_flour] - quantity: 1 - byproducts: [empty_pot] - - items: [bucket_of_water, pitcher_of_water] - quantity: 1 - byproducts: [empty_bucket, empty_pitcher] -``` - -Each entry is an ingredient slot. Multiple `items` means any of them works. The first matching item found in inventory is consumed. - -`ticks` controls how many game ticks each production cycle takes (default 2). - -### Byproducts - -`byproducts` is a list matching the `items` list by index. When `bucket_of_water` (items[0]) is consumed, `empty_bucket` (byproducts[0]) is returned to inventory. When `pitcher_of_water` (items[1]) is consumed, `empty_pitcher` (byproducts[1]) is returned instead. - -Use `""` for items that produce no byproduct: - -```yaml -made_from: - - items: [pot_of_flour, loose_flour] - quantity: 1 - byproducts: [empty_pot, ""] # pot returns empty_pot, loose flour returns nothing - - items: [bucket_of_water] - quantity: 1 - byproducts: [empty_bucket] -``` - -Omit `byproducts` entirely if no items in that slot produce a byproduct: +Quick example — filling a bucket: ```yaml -made_from: - - items: [pot_of_flour] - quantity: 1 - byproducts: [empty_pot] # returns empty_pot - - items: [salt] - quantity: 1 # no byproducts field — salt is consumed entirely +name: bucket of water +craft: + ticks_per_cycle: 0 + ingredients: + - items: [vial_of_water, jug_of_water] + quantity: 1 + byproducts: [empty_vial, empty_jug] + - items: [empty_bucket] + quantity: 1 + success_message: "You pour the %i1 into the %i2." ``` - -See also `recipes.md` for station-based recipes. diff --git a/building_guide/mobs.md b/building_guide/mobs.md index 470a00e..e7790c0 100644 --- a/building_guide/mobs.md +++ b/building_guide/mobs.md @@ -208,6 +208,13 @@ shop: Note: `wander_rooms` and `wander_interval` are NOT set on the mob definition. Wander config is per-instance in the room YAML (see Rooms > Mobs section above). +### On Kill Interactions + +Mobs can define `on_kill` — fires when the mob is defeated. Standard loot `drops` hit the +ground first, then the first matching interaction fires. An entry with `item_id` only fires +if the player is wielding that weapon. See the [On Kill section of +behaviors](behaviors.md#on-kill--mob-interactions) for full examples. + ### Stealable Mobs Stealable mobs have a root-level `steal:` block. On failed steal, the mob turns hostile and attacks. diff --git a/building_guide/objects.md b/building_guide/objects.md index db72cf3..cb38e0c 100644 --- a/building_guide/objects.md +++ b/building_guide/objects.md @@ -1,90 +1,156 @@ ## Objects -Objects interact with the world through **inline behavior configs** under -`gather:`, `talk:`, or `use:` keys. There is no separate behavior -directory — everything goes directly in the object's YAML. +Objects interact with the world through **inline behavior configs** under `gather:`, `talk:`, +`use:`, or `safespot:` keys. There is no separate behavior directory — everything goes +directly in the object's YAML. ### Naming & file organization -**The filename is the ID.** An object is looked up by its filename stem -(`copper_rock.yaml` → `copper_rock`), and the loader derives `ObjectDef.ID` from that -filename. Rooms reference the object by this bare ID regardless of which folder the file -lives in. **Do not put an `id:` field in the file** — it is ignored. (A nested `- id:` under -a room's `objects:` list is a different thing: a *reference* to an object by its filename, -and is still required there.) - -- **Generic, shared objects** (rocks, trees, altars, stations) use a semantic name and live - flat in `data/objects/` — e.g. `data/objects/copper_rock.yaml`. -- **Room-specific one-offs** (signs, set-dressing, scenery for a single room): if they are - description-only (the passive subset), prefer defining them **locally in the room** (see - [Local objects](#local-objects-defined-in-the-room) below). If a one-off needs - interactable behavior (`gather`/`talk`/`use`/etc.) it must be a file — prefix it with the - room number and place it in `data/objects/unique/`, e.g. `data/objects/unique/1002_sign.yaml` - (ID `1002_sign`, referenced from room `1002`). - -Loading walks `data/objects/` recursively, so subdirectories are purely organizational and -need no changes to room references when a file is moved. They do **not** namespace IDs: every -filename stem must be globally unique across all of `data/objects/`. Use `hidden: true` for -signage/scenery so it doesn't appear in room listings. +**The filename is the ID.** An object is looked up by its filename stem (`copper_rock.yaml` +→ `copper_rock`). Rooms reference the object by this bare ID. Do not put an `id:` field in +the file — it is ignored. (A nested `- id:` under a room's `objects:` list is a *reference* +to an object by filename, and is still required.) + +- **Generic, shared objects** (rocks, trees, altars, stations) live flat in `data/objects/`. +- **Room-specific one-offs** (signs, set-dressing) that are description-only should be + defined as **local objects** in the room (see below). If a one-off needs interactable + behavior (`gather`/`talk`/`use`/etc.) it must be a file — place it in + `data/objects/unique/`, e.g. `data/objects/unique/1002_sign.yaml`. + +Loading walks `data/objects/` recursively, so subdirectories are purely organizational. All +filename stems must be globally unique across all of `data/objects/`. ### Local objects (defined in the room) -Room-specific **description / scenery** objects can be defined directly inside the room's -`objects:` list instead of as separate files in `data/objects/unique/`. An entry in the list -is treated as **local** when it carries a `name:` (or any other content field — -`description`, `aliases`, `inroom_description`, `color`, `hidden`, `on_look`). An entry with -only `- id: <file>` is a plain **reference** to a file object, as before. `id:` is reserved -for references; **local objects do not take an `id:`** — their identity comes from the name. +Room-specific description/scenery objects can be defined directly inside the room's +`objects:` list instead of as separate files. An entry is **local** when it carries a +`name:` (or any other content field — `description`, `aliases`, `inroom_description`, +`color`, `hidden`, `on_look`). An entry with only `- id: <file>` is a plain **reference**: ```yaml # in data/rooms/intro/1001.yaml objects: - id: workbench # reference: resolves to data/objects/workbench.yaml - - name: window # local: identity derived from the name, no file + - name: window # local: identity from name, no file hidden: true description: "A thick trim with rounded bolt heads frames the tiny window." - name: sign aliases: [safety card, frame] - inroom_description: A large framed sign is mounted near the cockpit door. - description: |- - Conditional or multi-line descriptions work exactly like file objects. + inroom_description: "A large framed sign is mounted near the cockpit door." + description: "Safety instructions are printed in bold lettering." on_look: - set_player_flags: 1001_look_sign: true ``` - **Identity comes from `name`.** Internally the object's id is the name, normalized - (lowercased, surrounding/redundant spaces trimmed, spaces kept — `"Instrument Panel"` → - `instrument panel`). You never write an `id:`; doing so on a local entry is ignored and - warned about at startup. -- **Identity is room-scoped.** Two different rooms may each have an object named `sign` - without conflict — no room-number prefixing needed (unlike the old `data/objects/unique/` - files). -- **Each object in a room must have a unique name.** Two *distinct* objects in the same room - with the same exact name — whether both local, both file references, or one of each — is a - startup **error** (the player could not disambiguate them). Multiple instances of the *same* - object are still fine via repeated references (e.g. two `- id: copper_rock`). -- **Partial-name siblings are fine.** `rusty sign` and `shiny sign` can coexist; `look sign` - matches both and prompts *"which one?"*, while `look rusty sign` resolves directly. + (lowercased, spaces kept — `"Instrument Panel"` → `instrument panel`). +- **Identity is room-scoped.** Two rooms may each have an object named `sign` without + conflict. +- **Each object in a room must have a unique name.** Two distinct objects in the same room + with the same name is a startup error. +- **Partial-name siblings are fine.** `rusty sign` and `shiny sign` can coexist; `look + sign` matches both with "which one?", while `look rusty sign` resolves directly. - Local objects support only the **passive subset**: `name`, `aliases`, `color`, `hidden`, `inroom_description`, `description` (including conditional variants), and `on_look`. -- Interactable / stateful behavior (`gather`, `talk`, `use`, `safespot`, `steal`, `guard_mob`, - `removal_item`, `on_use`, craft stations) **must** be a standalone object file; - startup validation errors if those appear locally. +- Interactable behavior (`gather`, `talk`, `use`, `safespot`, `steal`, `guard_mob`, + `removal_item`, `on_use`, craft stations) **must** be standalone files; startup validation + errors if those appear locally. - Local objects are re-read from the room file on every access, so edits take effect immediately (file objects are cached after first load). Prefer local for one-room flavor; keep generic/reusable/interactable objects as files. +--- + +### Hidden objects + +Objects with `hidden: true` don't appear in the room's object listing. Players discover them +by reading room descriptions or trying commands. The object is still fully interactable — +`push gate`, `look gate`, `hide outcrop` all work. + +```yaml +name: stone lever +hidden: true +description: "A cleverly concealed lever behind a loose stone." +on_use: + - condition: + global_flag: secret_passage_open + not: true + message: "You pull the lever. Grinding echoes from the east." + action: + set_global_flags: + secret_passage_open: true +``` + +Room description hints at it: +```yaml +description: "A dusty corridor. One of the wall stones looks slightly out of place." +``` + +#### Aliases + +By default an object matches its `name` (word-prefix matching). Add `aliases` for extra +names players can type: + +```yaml +name: landing craft # matches "landing", "craft", "landing craft" +aliases: [shuttle, ship] # also matches "shuttle" and "ship" +hidden: true +``` + +If a player's input matches more than one distinct object, `look` lists the candidates +("That's ambiguous, which one?") — keep aliases specific enough to avoid overlap. + +#### Conditional object descriptions + +An object's `description` can be a plain string or a list of conditional variants (first +matching condition wins). If **none** of the variants match, the object is treated as absent +for that player — `look <it>` reports nothing is there. Combined with `hidden: true`, the +same object can appear only while a player's flags warrant it (e.g. during an intro) and +vanish on return visits: + +```yaml +name: crowd of people +aliases: [people, crowd, passengers] +hidden: true +description: + - condition: # after the pilot arrives + all_of: + - player_flag: boarded + not: true + - player_flag: lined_up + text: "The passengers have formed a single-file line." + - condition: # before the pilot arrives + all_of: + - player_flag: boarded + not: true + - player_flag: lined_up + not: true + text: "A couple dozen anxious passengers mill about the pad." + # once `boarded` is set, no variant matches → the crowd is gone +``` + +A bare string in `description:` maps to a single unconditional entry, so a plain object is +always present. + +--- + +### Gather behavior + +Gather behaviors (`gather:`) define mining, fishing, and woodcutting object interactions. +See the [Gather section of behaviors](behaviors.md#gather-mining-fishing-woodcutting) for +the complete reference — SuccessFormula, DropEntry, shared depletion, bird's nests, and all +GatherConfig fields. + +Quick example (copper rock): -Gathering object (mining): ```yaml name: copper rock -color: "B2" # xterm-256 color index (00-FF) +color: "B2" gather: skill: mining - tools: - - pickaxe + tools: [pickaxe] success: base: 0.40 per_level: 0.01 @@ -93,51 +159,42 @@ gather: fail_message: "You chip away but get nothing useful." drops: - item_id: copper_ore - level: 1 # required skill level for this drop - xp: 17 # XP awarded when this drop lands + level: 1 + xp: 17 weight: 90 depletes: true - message: "You manage to mine some {B2}copper ore{/}." + success_message: "You manage to mine some {B2}copper ore{/}." - table: gem_table weight: 10 depletes: false - message: "You spot a glint of something valuable!" + success_message: "You spot a glint of something valuable!" respawn_timer: 50 respawn_broadcast: "A glint of copper catches your eye from some {name}." ``` -Tree object (woodcutting with shared depletion): -```yaml -name: oak tree -color: "71" -gather: - skill: woodcutting - tools: - - axe - success: - base: 0.40 - per_level: 0.01 - cap: 0.90 - gather_message: "You swing your axe at the oak tree..." - fail_message: "You swing but get no logs." - drops: - - item_id: oak_logs - level: 15 - xp: 37 - weight: 100 - depletes: false - message: "You get some {71}oak logs{/}." - respawn_timer: 14 - deplete_timer: 45 - nest_chance: 256 - respawn_broadcast: "An {name} grows back." -``` +--- -Color accepts xterm-256 indices with optional modifiers (`bold`, `dim`, `underline`) -and gradients (`g:C4,52`). In ANSI mode, extended colors downgrade to the nearest -ANSI color. +### Talk behavior (conversations) + +Objects can host dialog trees via the `talk:` key. This is the same format as talk configs +on mobs. See the [Talk section of behaviors](behaviors.md#talk-dialog-trees) for the full +reference — multi-message nodes, randomized messages, conditional nodes, option-level +actions, and linear auto-advance. + +--- + +### Interactions — on_use / on_look + +Objects can define `on_use` and/or `on_look` to react to player actions. Both take a list of +interactions; entries are checked top-to-bottom, the first whose `condition` passes wins. +The full `Interaction` shape (condition, message, action with the entire StepAction +vocabulary) is documented in the [Interaction Reference](behaviors.md#interaction-reference). + +#### on_use — using items on objects + +`on_use` runs when the player types `use <obj>` (bare) or `use <item> on <obj>` +(item-specific). Entries with no `item_id` are bare: -Object interaction (gate, lever — uses `on_use:` key): ```yaml name: iron gate hidden: true @@ -145,7 +202,6 @@ description: "A heavy iron gate set into the north wall." on_use: - condition: global_flag: gate_open - value: true not: true message: "You push the heavy iron gate open." action: @@ -153,67 +209,20 @@ on_use: gate_open: true ``` -Decorative object (no behavior keys — just a name/description): -```yaml -name: town fountain -description: "Clear water sparkles in the sunlight." -``` - -Talk object (NPC conversations — uses `talk:` key): -```yaml -name: Tool Shed -description: "A small shed with an open window." -talk: - nodes: - start: - message: "\"Welcome to the tool shed!\"" - options: - - text: "\"What do you have?\"" - goto: shop - - text: "\"Goodbye.\"" -``` - -On-look interaction (runs when a player examines an object with `look <name>`): -```yaml -name: sign -aliases: [notice, board] -description: "A wooden signpost with faded writing." -on_look: - - set_player_flags: - read_sign: true -``` -`on_look` is a list of interactions (same shape as `on_use`). The first entry -whose `item_id` and `condition` pass wins, and fires AFTER the object's -description is shown. An entry with an `item_id` only fires if the player -currently carries that item in their inventory (empty `item_id` = always -fires on look). It uses the same `interaction` shape as `on_use` — supports -`condition`, `message`, and a full `action` (`set_global_flags`, -`set_player_flags`, `give_item`, `take_item`, `teleport`, `heal`, `credits`, -`aps_node`, `broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`, -`delay`), per the [interaction reference](behaviors.md#interaction-reference). - -See the [Talk section of behaviors](behaviors.md#talk-dialog-trees) for the full -dialog tree format. See [Stealable Objects](#stealable-objects) for theft mechanics. - ---- - -## Interactions (On Use / On Look) - -Objects can define `on_use` and/or `on_look` to react to player actions. Both take a -list of interactions (entries are checked top-to-bottom; the first whose -`condition` passes wins — one fires per use/look). +Item-specific: -Simple message (no action): ```yaml -name: anvil +name: bookshelf on_use: - - item_id: silver_bar - message: "You should use this with a mold at a furnace." - - item_id: gold_bar - message: "You should use this with a mold at a furnace." + - item_id: dusty_tome + message: "The bookshelf slides aside, revealing a secret passage!" + action: + set_global_flags: + secret_passage_open: true ``` -Puzzle interaction (take item, set flag): +Puzzle interaction: + ```yaml name: crystal slot on_use: @@ -222,7 +231,7 @@ on_use: global_flag: crystal_inserted message: "The crystal key is already in the slot." - item_id: crystal_key - message: "You insert the crystal key into the slot. It clicks into place." + message: "You insert the crystal key. It clicks into place." action: take_item: crystal_key set_global_flags: @@ -230,6 +239,7 @@ on_use: ``` Quest item exchange: + ```yaml on_use: - item_id: ancient_scroll @@ -244,55 +254,24 @@ on_use: temple_door_open: true ``` -### Interaction Fields - -| Field | Type | Description | -| ----------- | ---------- | --------------------------------------------------------------- | -| `item_id` | string | `on_use`: required item id (empty = bare `use <object>`). `on_look`: only fires if you carry this item. (Mobs' `on_kill`: only fires if you wield this weapon in main_hand or off_hand.) | -| `condition` | Condition | Optional condition (same as exits/talk/on_enter/triggers) | -| `message` | string | Message shown to the player when this entry fires | -| `action` | StepAction | Optional actions (see [interaction reference](behaviors.md#interaction-reference)) | - -### Available Actions (same superset as on_enter steps/triggers/talk) - -| Field | Type | Description | -| ------------------ | -------------- | ----------------------------------------------------- | -| `set_global_flags` | map[string]any | Set global flags (shared by all players) | -| `set_player_flags` | map[string]any | Set player flags (per-character, saved to YAML) | -| `give_item` | string | Give an item to inventory (1 unit) | -| `take_item` | string | Remove an item from inventory (1 unit) | -| `teleport` | int | Move player to a room ID | -| `heal` | int | Restore hitpoints (clamped to MaxHP) | -| `credits` | int | Add (positive) or deduct (negative) credits | -| `aps_node` | bool | Mark current room as a discovered APS node | -| `broadcast` | string | Announce to all in the room (uses `%p`, `%v`) | -| `broadcast_global` | string | Announce to all online (uses `%p`, `%v`) | -| `spawn_mob` | string/map | Spawn a transient mob (`id` or full [config](behaviors.md#spawn-mob-config)) | -| `despawn_mob` | string | Despawn all transient mobs of this id | -| `message` | string | Direct message to the player (also valid here) | -| `delay` | int | Ticks to wait before this step (only meaningful in on_enter/triggers) | - -### Available Conditions (same as exits/talk/on_enter/triggers) - -| Field | Description | -| ------------- | ------------------------------------------------------- | -| `global_flag` | Global flag check | -| `player_flag` | Per-character flag check | -| `has_item` | Inventory item check | -| `min_credits` | Minimum credits | -| `value` | Expected value for flag checks | -| `not` | Invert the condition | -| `all_of` | All sub-conditions must pass | -| `any_of` | Any sub-condition must pass | +#### on_look — actions when examining an object + +`on_look` fires AFTER the object's description is shown. An entry with `item_id` only fires +if the player carries that item. Supports the full Interaction shape: + +```yaml +name: sign +on_look: + - set_player_flags: + read_sign: true +``` --- -## Stealable Objects +### Stealable objects -Objects can be stealable via the `steal` command. These use drop entries — either -item IDs or drop table references — resolved by weighted pick. The `steal` command -supports auto-selection when only one target is available, or `steal <name>` for -explicit targeting, including `steal <n>.<name>` to disambiguate identical defs. +Objects can be stealable via the `steal` command. These use drop entries — item IDs or drop +table references — resolved by weighted pick: ```yaml name: Market Stall @@ -305,33 +284,28 @@ steal: xp: 12 speed: 5 guard_mob: guard + guard_range: 2 ``` -| Field | Description | -| ---------------- | ------------------------------------------------ | -| `steal.drops` | Array of drop entries (item_id + weight, or table + weight) | -| `steal.level` | Required thieving level | -| `steal.xp` | XP awarded per successful steal | -| `steal.speed` | Ticks per steal attempt (base wait) | -| `steal.guard_mob`| Mob def ID that guards this object (watches it) | - -Each drop entry: -| Field | Description | -| ------------ | ------------------------------------------------ | -| `item_id` | Item ID (mutually exclusive with `table`) | -| `table` | Drop table ID to sub-resolve (mutually exclusive with `item_id`) | -| `weight` | Weight in the weighted-pick pool | -| `quantity` | Override quantity (0 = use sub-item's own qty for tables, or 1 for items) | +| Field | Description | +|-------|-------------| +| `steal.drops` | Array of drop entries (item_id + weight, or table + weight) | +| `steal.level` | Required thieving level | +| `steal.xp` | XP awarded per successful steal | +| `steal.speed` | Ticks per steal attempt (base wait) | +| `steal.guard_mob` | Mob def ID that guards this object | +| `steal.guard_range` | Room range within which the guard watches (default 0 = same room only) | -When `guard_mob` is set, a mob with that def ID in the same room watches the object -on a tick-based cycle (8 ticks watching, 4 ticks looking away). While the guard is -watching, steal success chance is halved and failures trigger a confrontation dialog. +When `guard_mob` is set, a mob with that def ID in the same room watches the object on a +tick-based cycle (8 ticks watching, 4 ticks looking away). While the guard is watching, +steal success chance is halved and failures trigger a confrontation dialog. Use the `sneak` +command to see guard watch state changes in real time. -Use the `sneak` command to see guard watch state changes in real time. +Mobs can also be stealable (see `mobs.md`). --- -## Safespots +### Safespots Safespot objects provide cover that blocks melee attacks. Players use `hide <object>` to crouch behind them and attack with ranged or science weapons. Melee attacks force the player @@ -344,7 +318,7 @@ hidden: true inroom_description: "A jagged rock outcrop juts from the floor." description: "A large, jagged rock formation providing natural cover." safespot: - tier: 2 + tier: 1 max_block_size: large max_occupants: 3 unsafe_chance: 0.008 @@ -356,65 +330,67 @@ safespot: - message: "The rock formation stands solid." degrade_message: "The rock structure begins crumbling!" - message: "The rock outcrop is cracked and worn." - degrade_message: "A few jagged bits are all that's left of the rock outcrop!" + degrade_message: "A few jagged bits are all that's left!" - message: "Only a few jagged rocks remain." degrade_message: "The rock outcrop crumbles to nothing!" ``` Safespot objects should be `hidden: true`. They never appear in room descriptions — players -discover them through quest guidance, `inroom_description` text in the room's YAML, or by -examining objects directly with `look <name>`. +discover them through room text hints, quest guidance, or by trying `look <name>`. Safespots +only become visible in `look` when the player's effective safespot tier meets the object's +`safespot.tier` requirement. Gaining tier through quest completion and achievements +gradually reveals new coverage: -### SafespotConfig Fields - -| Field | Type | Default | Description | -|---|---|---|---| -| `tier` | int | 0 | Required safespot tier to use this object | -| `max_block_size` | string | `""` (all) | Largest mob size blocked: small/medium/large/massive | -| `max_occupants` | int | 0 | Max players that can hide behind this object (0 = unlimited) | -| `unsafe_chance` | float64 | 0 | Per-tick chance (0.0-1.0) to be forced out of cover | -| `decay_ticks` | float64 | 0 | Guaranteed ticks before object degrades one level | -| `decay_chance` | float64 | 0 | Per-tick random chance (0.0-1.0) to degrade one level | -| `respawn_on_hide` | bool | false | If true, hiding behind an empty safespot resets it to max level. Only triggers when no occupants are present. | -| `respawn_ticks` | float64 | 0 | Ticks until a destroyed safespot respawns (0 = never respawns — object is deleted from the room) | -| `levels` | []SafespotLevel | required | At least one level entry | +| Flag | Source | Tier bonus | +|------|--------|------------| +| `quest_animal_magnetism` | Quest | +1 | +| `quest_dragon_slayer` | Quest | +1 | +| `achieve_medium_combat` | Achievement | +1 | +| `achieve_hard_combat` | Achievement | +2 | -### SafespotLevel Fields +#### SafespotConfig fields -| Field | Type | Description | -|---|---|---| -| `message` | string | Shown to the player when the safespot is at this level | -| `degrade_message` | string | Broadcast to the room when the safespot degrades FROM this level | +| Field | Default | Description | +|-------|---------|-------------| +| `tier` | 0 | Required safespot tier to use this object | +| `max_block_size` | `""` (all) | Largest mob size blocked: small/medium/large/massive | +| `max_occupants` | 0 (unlimited) | Max players behind this object | +| `unsafe_chance` | 0 | Per-tick chance (0.0–1.0) to be forced out of cover | +| `decay_ticks` | 0 | Guaranteed ticks before degrading one level | +| `decay_chance` | 0 | Per-tick random chance (0.0–1.0) to degrade | +| `respawn_on_hide` | false | First occupant resets safespot to max level | +| `respawn_ticks` | 0 | Ticks until destroyed safespot respawns (0 = deleted) | +| `levels` | required | At least one SafespotLevel entry | -Levels are ordered from highest to lowest. The first entry is the safespot's best state; -the last entry is its weakest state before destruction. When the last level degrades, -the safespot is destroyed. +#### SafespotLevel fields -### Three Safespot Patterns +| Field | Description | +|-------|-------------| +| `message` | Shown to the player when safespot is at this level | +| `degrade_message` | Broadcast to room when safespot degrades FROM this level | -The combination of `respawn_on_hide` and `respawn_ticks` creates three distinct use cases: +#### Three safespot patterns -**1. Standard Safespot (always available)** +**Standard safespot** — always available, permanent: ```yaml safespot: + tier: 1 + max_block_size: small respawn_on_hide: true respawn_ticks: 0 levels: - - message: "You crouch behind the rock formation, using it as cover." + - message: "You crouch behind the fence post, using it as cover." ``` -The safespot starts at full health each time a player hides behind it (first occupant only — -subsequent players don't reset the level). Ideal for skilling areas and general exploration. -Since `respawn_ticks: 0`, a destroyed safespot would be deleted, but with a single level and -no decay, it never degrades. Simple and permanent. - -**2. Regenerating Safespot (timer-based respawn)** +**Regenerating safespot** — degrades across hides, respawns after destruction: ```yaml safespot: respawn_on_hide: false respawn_ticks: 300 + decay_ticks: 500 + decay_chance: 0.01 levels: - message: "The rock formation stands solid." degrade_message: "The rock structure begins crumbling!" @@ -424,12 +400,7 @@ safespot: degrade_message: "The rock outcrop crumbles to nothing!" ``` -Degradation persists between hides — a player who leaves and re-hides finds the -safespot at its previous level. After total destruction, a timer (`respawn_ticks`) -counts down, then the safespot reforms at full health. Suits areas with heavy use -where a temporary respite is valuable. - -**3. Disposable Safespot (boss fight cover)** +**Disposable safespot** — permanent degradation, deleted on destruction: ```yaml safespot: @@ -442,57 +413,25 @@ safespot: degrade_message: "The barricade shatters completely!" ``` -Degradation persists between hides. When the last level degrades, the safespot is -**deleted from the room entirely** — it does not respawn. Perfect for boss encounters -where a room script places temporary cover that the party consumes during the fight. -Once destroyed, it's gone for good (until the boss room re-instantiates it). +#### Mob Size -### Mob Size +Mobs must have a `size` field for safespot blocking to work (small < medium < large < +massive, default medium). A safespot with `max_block_size: large` blocks mobs of size small, +medium, and large, but not massive. -Mobs must have a `size` field for safespot blocking to work: +#### Combat integration -```yaml -# data/mobs/cow.yaml -size: small - -# data/mobs/moss_giant.yaml -size: large -``` - -Size ordering: `small` < `medium` < `large` < `massive`. Defaults to `medium` when absent. -A safespot with `max_block_size: large` blocks mobs of size small, medium, and large, -but not massive. - -### Progression - -Players gain safespot tier by completing quests and combat achievements, which set -player flags via talk node actions: - -| Flag | Source | Tier bonus | -|---|---|---| -| `quest_animal_magnetism` | Quest completion | +1 | -| `quest_dragon_slayer` | Quest completion | +1 | -| `achieve_medium_combat` | Combat achievement | +1 | -| `achieve_hard_combat` | Combat achievement | +2 | - -### Combat Integration - -- A safespot blocks a mob's **melee** attacks if the mob's size is within the - safespot's `max_block_size`. -- When a mob's melee is blocked, if it also has a `ranged`/`science` attack type it - switches to its strongest ranged/science attack (by max hit) and keeps attacking. - A mob with only melee is fully blocked (cannot aggro or land hits). -- Mobs whose size exceeds `max_block_size` are never blocked and always use melee. -- If a safespotted player attacks with melee, they automatically leave cover. -- Hiding in combat takes 4 ticks; if the mob hits during this time, the hide fails. +- A safespot blocks melee attacks from mobs within `max_block_size`. +- When a mob's melee is blocked but it has a ranged/science attack type, it switches to its + strongest ranged/science attack. +- Mobs exceeding `max_block_size` are never blocked and always use melee. +- Melee attacks from a safespotted player force them out of cover. +- Hiding in combat takes 4 ticks; taking a hit during this cancels the hide. - Hiding out of combat takes 1 tick. +- Safespots block all hazard damage (see `hazards.md`). -### Option: safespot_alert +#### Option: safespot_alert -Players can customize the message shown when forced out of a safespot: +Players can customize the alert message when forced out of cover: option safespot_alert "{C4 bold}** DANGER **{/} Cover blown!" -Default: `"{C4 bold}** Your safespot has been compromised! **{/}"` - ---- - diff --git a/building_guide/quests.md b/building_guide/quests.md index ff04118..2c0cbac 100644 --- a/building_guide/quests.md +++ b/building_guide/quests.md @@ -7,6 +7,8 @@ name: "Elder" unique: true protected: true +idle_descriptions: + - "mutters about the rat infestation" talk: nodes: start: @@ -41,19 +43,6 @@ talk: heal: 10 options: - text: "\"Thanks!\"" -combat: - kind: combat - aggressive: false - attack_types: [crush] - respawn_ticks: 30 - stats: - attack: 1 - strength: 1 - defense: 1 - hp: 20 - speed: 5 -idle_descriptions: - - "mutters about the rat infestation" ``` Talk configs are inline on the mob under the `talk:` key. No separate behavior file needed. diff --git a/building_guide/recipes.md b/building_guide/recipes.md index 96bf0f9..67048c1 100644 --- a/building_guide/recipes.md +++ b/building_guide/recipes.md @@ -1,74 +1,97 @@ -# The House of Icarus - World Building Guide +## Crafting & Recipes -## Crafting +Crafting information lives on the **output item's YAML file** via the `craft:` block. There is no +separate recipe directory. To find out how to make `bronze_dagger`, open +`data/items/equipment/bronze_dagger.yaml` and look for the `craft:` block. -Crafting information lives on the **output item's YAML file** via the `craft:` block. There is no separate recipe directory. To find out how to make `bronze_dagger`, open `data/items/equipment/bronze_dagger.yaml` and look for the `craft:` block. (The item's **filename is its ID** — `bronze_dagger.yaml` → `bronze_dagger`; there is no `id:` field. `ingredients`/output entries reference items by filename.) +**The filename is the ID** (`bronze_dagger.yaml` → `bronze_dagger`). Ingredients reference items by +filename. The output item IS the craft — no separate `output` field. ### Craft YAML Reference -| Field | Type | Description | -| -------------- | -------------- | ----------------------------------------------- | -| `type` | string | Craft type (smithing, cooking, crafting, fletching, pharmacy, construction, combine, or "" for skill-less). Always matches a player skill name. | -| `subtype` | string | Production method override (`smelt` for furnace recipes, `clean` for herb cleaning). Defaults empty (no override). | -| `level` | int | Required skill level | -| `xp` | int | XP awarded on success | -| `wait` | float64 | Ticks per craft cycle | -| `station` | []string | Station object IDs required (optional) | -| `tool` | string | Required tool_type (optional) | -| `ingredients` | []IngredientEntry | Ingredients consumed (see below) | -| `output_qty` | int | Quantity produced (default 1, for stackables) | -| `fail` | string | ItemID produced on failure (optional) | -| `success_message` | string | Success message per cycle (optional, see message defaults) | -| `fail_message` | string | Failure message (optional, empty = silent fail) | -| `start_message`| string | Message when action begins (optional, see message defaults) | -| `end_message` | string | Message when action completes (optional, see message defaults) | -| `steps` | []CraftStep | Multi-step messages at tick intervals (optional)| -| `success` | SuccessFormula | Optional skill check formula (see Behaviors) | - -The output is the item itself — no `output` field needed. The item ID IS the craft ID. +| Field | Type | Description | +|-------|------|-------------| +| `type` | string | Skill name: `smithing`, `cooking`, `crafting`, `fletching`, `pharmacy`, `construction`, or `""` for skill-less. | +| `subtype` | string | Production method override: `smelt` for furnace, `clean` for herb cleaning. | +| `level` | int | Required skill level | +| `xp` | int | XP awarded on success | +| `ticks_per_cycle` | float64 | Ticks per craft cycle | +| `station` | []string | Station object IDs required (e.g. `[anvil]` or `[furnace]`) | +| `tool` | string | Required tool_type (e.g. `knife`, `saw`). Tool is NOT consumed. | +| `ingredients` | []IngredientEntry | Ingredients consumed (see below) | +| `output_qty` | int | Quantity produced per cycle (default 1) | +| `fail` | string | Item ID produced on failure | +| `success_message` | string | Success message per cycle (%n, %i1, %i2, %b1, %b2) | +| `fail_message` | string | Failure message; empty = silent fail | +| `start_message` | string | Message when action begins | +| `end_message` | string | Message when action completes | +| `steps` | []CraftStep | Multi-step messages at tick offsets | +| `success` | SuccessFormula | Optional skill check formula (base, per_level, cap) | ### Message Variables -All message fields (`success_message`, `fail_message`, `start_message`, `end_message`, and `steps[].message`) support variables that expand to colorized item names: +| Variable | Expands to | +|----------|-----------| +| `%n` | Output item name (colored) | +| `%i1` | 1st ingredient's matched item (colored) | +| `%i2` | 2nd ingredient's matched item (colored) | +| `%b1` | 1st ingredient's byproduct name (colored, success only) | +| `%b2` | 2nd ingredient's byproduct name (colored, success only) | -| Variable | Expands to | -| -------- | ----------------------------------------------- | -| `%n` | Output item name (colored, e.g. "stim potion") | -| `%i1` | 1st ingredient entry's matched item (colored) | -| `%i2` | 2nd ingredient entry's matched item (colored) | -| `%b1` | 1st byproduct item name (colored, success only) | -| `%b2` | 2nd byproduct item name (colored, success only) | - -Numbering follows YAML ingredient/byproduct order. If an ingredient entry has multiple alternative items (`items: [a, b]`), the variable expands to whichever the player actually possesses, colored with that item's `color:` field. - -Variables work alongside inline color tags (`{C4}text{/}`) which are expanded after variable substitution. +Numbering follows YAML ingredient/byproduct order. If an ingredient entry has multiple +alternatives (`items: [a, b]`), the variable expands to whichever the player has. Inline +color tags (`{C4}text{/}`) work alongside these variables. ### Message Defaults -If a message field is omitted from the YAML, the game uses a skill-level default based on the craft `type`: +If a message field is omitted, the game uses a skill-level default: -| Type | Default `success_message` | Default `start_message` | -|------|-------------------|------------------------| +| Type | Default success_message | Default start_message | +|------|------------------------|----------------------| | `cooking` | `"Cooked to perfection. %n looks great!"` | `"You start cooking %i1."` | -| `smelt` | `"You remove a white hot %n!"` | `"You place the %i1 into the furnace."` | +| `smelt` (subtype) | `"You remove a white hot %n!"` | `"You place the %i1 into the furnace."` | | `smithing` | `"You smith a %n."` | `"You begin smithing %i1."` | | `crafting` | `"You craft a %n."` | `"You begin crafting %i1."` | | `fletching` | `"You fletch a %n."` | `"You begin fletching %i1."` | | `pharmacy` | `"You mix a %n."` | `"You start mixing %i1."` | | `construction` | `"You construct a %n."` | `"You begin constructing %i1."` | -| `clean` | `"You clean the %i1."` | `"You begin cleaning herbs."` | +| `clean` (subtype) | `"You clean the %i1."` | `"You begin cleaning herbs."` | | (unset) | `"You produce %n."` | `"You start <verb> %i1."` | -Default messages are keyed by `subtype` when set, otherwise by `type`. For skills that never fail (smithing, crafting, fletching, pharmacy, construction), `fail_message` defaults to empty — no message is shown on failure. +For skills that never fail (smithing, crafting, fletching, pharmacy, construction), +`fail_message` defaults to empty — no message on failure. + +### IngredientEntry — Multi-Item Ingredient Slots + +Each ingredient entry defines a slot with multiple valid alternatives. The first matching +item in the player's inventory is consumed. + +```yaml +ingredients: + - items: [item_id, alternative_id, ...] + quantity: 1 + byproducts: [byproduct_for_item, byproduct_for_alt, ...] +``` + +`byproducts` matches the `items` list by index. Use `""` for items with no byproduct: + +```yaml +ingredients: + - items: [bucket_of_water, vial_of_water] + quantity: 1 + byproducts: [empty_bucket, ""] # bucket returns empty, vial is consumed entirely + - items: [herb] + quantity: 1 # no byproducts — herb is consumed entirely +``` ### CraftStep — Multi-Step Messages -Optional interim messages fired at specific tick offsets during the craft cycle. The `tick` is the number of ticks elapsed since the start of the cycle. +Interim messages fired at specific tick offsets during the cycle: ```yaml craft: type: "" - wait: 6 + ticks_per_cycle: 6 ingredients: - items: [map_piece_1] quantity: 1 @@ -84,105 +107,90 @@ craft: success_message: "You assemble the map!" ``` -### IngredientEntry — Multi-Item Ingredient Slots - -Each ingredient entry defines an ingredient slot with multiple valid items. The first matching item found in the player's inventory is consumed. - -```yaml -ingredients: - - items: [item_id, alternative_id, ...] - quantity: 1 - byproducts: [byproduct_for_item, byproduct_for_alt, ...] -``` - -`byproducts` is optional. When present, it matches the `items` list by index — consuming `items[0]` returns `byproducts[0]` to inventory. Use `""` for items with no byproduct: - -```yaml -ingredients: - - items: [bucket_of_water, vial_of_water] - quantity: 1 - byproducts: [empty_bucket, ""] # bucket returns empty, vial is consumed entirely - - items: [herb] - quantity: 1 # no byproducts — herb is consumed entirely -``` - ### Skill-Based Production ```yaml # data/items/consumables/stim_potion.yaml +name: stim potion craft: type: pharmacy level: 3 xp: 25 - wait: 4 + ticks_per_cycle: 4 ingredients: - items: [guam_potion_unf] quantity: 1 - items: [eye_of_newt] quantity: 1 - success_message: "You mix a %n." # %n expands to "stim potion" colored + success_message: "You mix a %n." ``` ### Station-Based Production +Furnace (smelting): + ```yaml # data/items/materials/bronze_bar.yaml +name: bronze bar craft: type: smithing subtype: smelt level: 1 xp: 6 - wait: 4 + ticks_per_cycle: 4 station: [furnace] ingredients: - items: [copper_ore] quantity: 1 - items: [tin_ore] quantity: 1 - success_message: "You smelt a %n." # default is "You remove a white hot %n!" - # overridden here for simpler flavor + fail_message: "You fail to smelt a usable bar." ``` +Anvil (smithing): + ```yaml # data/items/equipment/bronze_dagger.yaml +name: bronze dagger craft: type: smithing level: 1 xp: 12 - wait: 4 + ticks_per_cycle: 4 station: [anvil] ingredients: - items: [bronze_bar] quantity: 1 - # success_message omitted — uses default "You smith a %n." ``` -For stackable outputs, use `output_qty`: +Stackable outputs use `output_qty`: ```yaml # data/items/ammo/bronze_nails.yaml +name: bronze nails +stackable: true craft: type: smithing level: 4 xp: 12 - wait: 4 + ticks_per_cycle: 4 station: [anvil] ingredients: - items: [bronze_bar] quantity: 1 output_qty: 15 - # success_message omitted — uses default "You smith a %n." ``` ### Tool-Based Production ```yaml # data/items/ammo/arrow_shafts.yaml +name: arrow shafts craft: type: fletching level: 1 xp: 5 - wait: 3 + ticks_per_cycle: 3 tool: knife ingredients: - items: [logs, oak_logs, willow_logs] @@ -190,31 +198,31 @@ craft: output_qty: 15 ``` -The `tool` field requires the player to have an item with that `tool_type` equipped or in inventory. The tool is NOT consumed. +### Skill-Less Combinations -### Skill-Less Combinations (Item-on-Item) - -Omit `type` (or leave it empty) for combinations with no skill check, no XP, and 100% success: +Omit `type` (or leave empty) for combinations with no skill check and 100% success: ```yaml # data/items/consumables/bucket_of_water.yaml +name: bucket of water craft: - wait: 0 + ticks_per_cycle: 0 ingredients: - items: [vial_of_water, jug_of_water] quantity: 1 byproducts: [empty_vial, empty_jug] - items: [empty_bucket] quantity: 1 - success_message: "You pour the %i1 into the %i2." # %i1 = water source, %i2 = bucket + success_message: "You pour the %i1 into the %i2." ``` -### Multi-Piece Assembly (3+ items → 1) +### Multi-Piece Assembly ```yaml # data/items/quest/ancient_map.yaml +name: ancient map craft: - wait: 0 + ticks_per_cycle: 0 ingredients: - items: [torn_page_1] quantity: 1 @@ -230,46 +238,31 @@ craft: success_message: "You assemble the %n." ``` -### Clean Recipes +### Clean Recipes (Herb Cleaning) -Clean herb recipes use `type: pharmacy` with `subtype: clean` and are handled as background actions (one herb per cycle, directly mutating inventory): +Use `type: pharmacy` with `subtype: clean`: ```yaml -# data/items/materials/guam.yaml (clean guam) +# data/items/materials/guam.yaml (clean herb) +name: clean guam craft: type: pharmacy subtype: clean level: 3 xp: 3 - wait: 2 + ticks_per_cycle: 2 ingredients: - items: [grimy_guam] quantity: 1 - # success_message omitted — uses default "You clean the %i1." - # %i1 expands to the grimy herb name with its color ``` -### Food/Healing Items - -Items with `heal_value` and `eat_message` can be consumed via the `eat` command. - -```yaml -name: bread -color: "DE" -description: "A fresh loaf of bread, still warm from the oven." -value: 5 -heal_value: 5 -eat_message: "You eat the bread. Warm and satisfying." -``` +### Food / Healing -### Architecture +Items with `heal_value` and `eat_message` can be consumed via the `eat` command. See +`items.md` for the full food item format. -The `CraftIndex` (`internal/game/production_index.go`) is built once at startup from all item YAMLs that have a `craft:` block. It provides `O(1)` lookups by type, subtype, input item, and station. All crafting commands query the CraftIndex — no runtime file scanning. +### Construction -**Indexes:** -- `byType["pharmacy"]` → all pharmacy-craftable items (used by `mix`) -- `bySubtype["clean"]` → all items with subtype `clean` (grimy herb cleaning) -- `bySubtype["smelt"]` → all items with subtype `smelt` (furnace smelting) -- `byInput["guam"]` → all items that use guam (used by `use` to find combinations) -- `byStation["anvil"]` → all items craftable at an anvil (used by `use bar on anvil`) -- `FindByTwoInputs(a, b)` → items consuming both inputs in different ingredient entries +`type: construction` recipes use `station: [workbench]`. Log-to-plank recipes additionally +require `tool: saw`. See `construction.md` for plank tiers, furniture, NPCs, and player +housing. diff --git a/building_guide/rooms.md b/building_guide/rooms.md index f77aa42..3834a56 100644 --- a/building_guide/rooms.md +++ b/building_guide/rooms.md @@ -15,48 +15,65 @@ exits: east: 3 ``` -### Map color — default symbol color on the map +### Map color -A room may set a default `color` that tints its node on the map and the links connecting it to -neighbouring nodes. A player's per-room `symbol` color (set via the `symbol` command) overrides it. +A room may set a `color` that tints its node on the map and the links connecting it: ```yaml color: "5E" ``` +A player's per-room `symbol` (set via the `symbol` command) overrides the room color. + ### Inline Color Tags -Room descriptions support inline color tags using `{spec}text{/}` syntax. Untagged text uses the `room_desc` color. +Room descriptions support inline color tags using `{spec}text{/}` syntax: ```yaml description: "On the table lies a {B6 bold}mysterious vase{/} with a rose in it." ``` -Tag spec format: `{<00-FF> [bold] [dim] [underline]}text{/}` +Format: `{<00–FF> [bold] [dim] [underline]}text{/}` Gradients: `{g:C4,52}gradient text{/}`. Multi-stop: `{g:2D,27,3B}three stops{/}`. -### Exits — simple vs conditional +Untagged text uses the `room_desc` theme color. + +--- + +### Exits + +#### Simple exits + +Always passable, any direction: -Simple exit — always passable: ```yaml exits: north: 2 - ne: 3 # ne/nw/se/sw also work + ne: 3 southeast: 4 ``` -Conditional exit — blocked until a global flag is set: +#### Conditional exits + +Blocked until a condition passes: + ```yaml exits: north: room: 11 condition: - flag: gate_open + global_flag: gate_open blocked_message: "A heavy iron gate blocks the way north." ``` -Conditional exit — blocked unless the PLAYER has a flag (key, permission, quest state): +The `condition:` field uses the same condition vocabulary as everywhere else — see the +[Conditions Reference](#conditions-reference) below. + +#### Player-flag exits + +Blocked unless the PLAYER has a flag: + ```yaml exits: east: @@ -66,39 +83,57 @@ exits: blocked_message: "The vault door is locked. You need a key." ``` -Conditional exit with compound condition — requires both a global flag AND a player flag: +#### Inventory-check exit + +Blocked unless the player has an item: + +```yaml +exits: + east: + room: 6 + condition: + has_item: rusty_key + blocked_message: "The iron door is locked. You need a key." +``` + +#### Compound conditions + ```yaml exits: north: room: 20 condition: all_of: - - flag: bridge_repaired + - global_flag: bridge_repaired - player_flag: paid_toll blocked_message: "The bridge is out, and the toll collector blocks the path." ``` -Exit that sets flags when used — `set_global_flags` / `set_player_flags` are applied -only when the player actually moves through the exit (not when it's blocked): +#### Exit on_traverse — actions when walked through + +An exit can carry an `on_traverse` list. Interactions fire when the player moves through the +exit (not when it's blocked). The first whose condition passes wins. Supports the full +`Interaction` shape — condition, message, and action: + ```yaml exits: north: room: 21 condition: player_flag: lined_up - set_player_flags: - boarded_shuttle: true # marks "left this area" on the way out + blocked_message: "You're not in line yet." + on_traverse: + - set_player_flags: + boarded_shuttle: true ``` -### Hidden exits +#### Hidden exits -Hidden exits are invisible in `look` and `verbs` output until the player -discovers them by traversing them once. Even hidden, the exit is still -traversable — just not listed. Once a player moves through a hidden exit, it -becomes "discovered" for that character and appears in listing with a -`(HIDDEN)` tag. On the map, hidden exits (and rooms only reachable via them) -do not appear until the character discovers them. Gods in `god` mode see all -hidden exits regardless of discovery. +Hidden exits are invisible in `look` and `verbs` output until the player discovers them by +traversing them once. Even hidden, the exit is traversable — just not listed. Once a player +moves through a hidden exit, it becomes "discovered" for that character and appears with a +`(HIDDEN)` tag. On the map, hidden exits (and rooms only reachable via them) do not appear +until the character discovers them. Gods in `god` mode see all hidden exits. ```yaml exits: @@ -107,14 +142,12 @@ exits: hidden: true ``` -### Always-blocked exits +#### Always-blocked exits -Always-blocked exits are blocked to players but always visible. They appear in -`look`/`verbs`/`exits` tagged `(blocked)` (same as a conditionally-blocked exit), -refuse movement with the standard "The way <dir> is blocked." message, and -render on the map as blocked `X` connectors. Gods in `god` mode can traverse -them (and see no `(blocked)` tag). Primarily used for agility course -room-to-room links so the map can lay the course out visually. +Always-blocked exits are blocked to players but always visible. They show `(blocked)` in +`look`/`verbs`/`exits`, refuse movement, and render on the map as blocked `X` connectors. +Gods in `god` mode can traverse them. Used for agility course room-to-room links so the map +can lay the course out visually. ```yaml exits: @@ -123,47 +156,43 @@ exits: always_blocked: true ``` -`always_blocked` is absolute: when set, a `condition:` on the same exit is not -evaluated (the exit is blocked unconditionally). Don't combine the two — if you -need a conditionally-gated exit, use `condition:` alone. +`always_blocked` is absolute — a `condition:` on the same exit is not evaluated. Don't +combine them. Hidden and always_blocked can be combined: the exit is invisible until +discovered, then shows as blocked. -Hidden and always_blocked can be combined: the exit is invisible until -discovered (or seen by a god), and then shows as blocked. +--- ### Map grid & one-way exits -All exits (north/south/east/west/ne/nw/se/sw) must form a consistent 3D grid: from -any starting room, walking horizontal directions should never land two different -rooms on the same 3D coordinate, and a given room must always resolve to the same -spot. Up/down move on the z-axis within this same grid (different floors share the -same x/y coordinate space). +All horizontal exits (n/s/e/w/ne/nw/se/sw) must form a consistent 3D grid. Startup +validation enforces this from `startup_validation.root_rooms` in config.yaml — it reports +**overlap** (two rooms on one cell) or **twist** (one room on two cells) as errors. -Startup validation enforces this, starting from `startup_validation.root_rooms` -(see config.yaml). It reports an **overlap** (two rooms on one grid cell) or a -**twist** (one room on two cells) as an ERROR so you can fix the wiring. +Exits don't have to be reciprocal. A one-way link renders on the map as a directional arrow +instead of a two-way bar. -Exits don't have to be reciprocal. A one-way link (e.g. room 2001 has `east: 2006` -but 2006 has no `west` back) renders on the map as a directional arrow -(`← ↑ → ↓`, or `< ^ > v` in ASCII, with `↗↖↘↙` for diagonal) instead of a -two-way bar. If a link is open in one direction but blocked (failing condition) in -the other, the open direction's arrow is shown; only a link with no traversable -direction shows the blocked `X`. Blocked up/down exits also show the blocked `X`. +--- + +### Item Spawns -### Item Spawns — ground items that respawn +Ground items that respawn after being picked up: ```yaml item_spawns: - id: bronze_pickaxe quantity: 1 - respawn_ticks: 30 # reappears 30 ticks (18 seconds) after being picked up + respawn_ticks: 30 # reappears 30 ticks (18s) after pickup - id: copper_ore quantity: 3 respawn_ticks: 50 ``` +--- + ### Mobs — NPCs placed in the room -Simple string (no wandering): +Simple placement (no wandering): + ```yaml mobs: - "newbie_trainer" @@ -171,6 +200,7 @@ mobs: ``` With wander config per-instance: + ```yaml mobs: - id: man @@ -180,27 +210,61 @@ mobs: wander_rooms: [1, 4, 5] # optional — only exit to these rooms ``` -Mob wander config lives in the room YAML, not in the mob definition. This keeps mobs generic -so the same `man` can wander differently depending on where it's placed. Mobs wander through -legal (unconditioned) room exits. If no legal exits exist, the mob stays still. Mobs with -no `wander_interval` never wander. +Wander config lives in the room YAML, not the mob definition. The same `man` can wander +differently depending on where it's placed. Mobs wander through un-conditioned room exits. +If no legal exits exist, the mob stays still. Mobs with no `wander_interval` never wander. +Mobs stop wandering while in combat. Dead mobs respawn at their home room. + +--- ### Objects — interactive fixtures +Simple placement: + +```yaml +objects: + - id: copper_rock + - id: copper_rock # second instance +``` + +With wandering config: + ```yaml objects: - - id: copper_rock # simple placement - - id: copper_rock # second instance - id: fishing_spot - wander_rooms: [7, 8, 9] # teleports between these rooms - wander_interval: 12 # every 12 ticks - - id: iron_gate # hidden object (see below) + wander_rooms: [7, 8, 9] # teleports between these rooms + wander_interval: 12 # every 12 ticks +``` + +Wandering objects teleport between rooms in their `wander_rooms` list. Players gathering +from a wandering object are silently interrupted when it moves. + +Local objects (defined directly in the room, no file needed): + +```yaml +objects: + - id: workbench # reference: resolves to data/objects/workbench.yaml + - name: window # local: no file, identity comes from name + hidden: true + description: "A thick trim with rounded bolt heads frames the tiny window." + - name: sign + aliases: [safety card, frame] + inroom_description: "A large framed sign is mounted near the cockpit door." + description: "Safety instructions are printed in bold lettering." + on_look: + - set_player_flags: + 1001_look_sign: true ``` -### On-enter scripts — messages and timed sequences when a player arrives +See `objects.md` for the full object reference — local objects, file objects, behaviors, +interactions, safespots, stealing, and hidden objects. + +--- + +### On-enter scripts -Each `on_enter` step shows a `message`, optionally gated by a `condition`. Steps -whose condition fails are skipped. +Each `on_enter` step shows a `message`, optionally gated by a `condition`. Steps whose +condition fails are skipped. ```yaml on_enter: @@ -214,10 +278,11 @@ on_enter: player_flag: talked_to_guard # subsequent visits ``` -**Timed sequences.** A step may also carry a `delay` (ticks to wait before it -fires) and/or set flags (`set_global_flags` / `set_player_flags`). If any surviving step -has a delay or sets a flag, the whole sequence runs as a scheduled enter sequence; -plain message-only scripts still print instantly. +#### Timed sequences + +A step may carry a `delay` (ticks to wait before firing) and/or set flags. If any step has a +delay or sets a flag, the whole sequence runs as a scheduled enter sequence; plain +message-only scripts print instantly. ```yaml on_enter: @@ -226,17 +291,19 @@ on_enter: message: "Some of the crowd look you up and down." - condition: { player_flag: boarded, not: true } delay: 5 - message: "The pilot calls out: \"Tickets, please! Nice and orderly!\"" + message: "The pilot calls out: \"Tickets, please!\"" set_player_flags: - lined_up: true # opens an exit, flips a description, etc. + lined_up: true # opens an exit, flips a description, etc. - condition: { player_flag: boarded, not: true } message: "The crowd forms a single-file line." ``` -**Step actions.** On-enter steps support all the same actions as trigger steps: -`broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`, `give_item`, -`take_item`, `teleport`, and `heal`. See the [trigger step actions](triggers.md#step-actions) -table for details. +#### Step actions + +On-enter steps support all the same actions as triggers: `broadcast`, `broadcast_global`, +`spawn_mob`, `despawn_mob`, `give_item`, `take_item`, `teleport`, `heal`, `credits`, +`aps_node`, `set_global_flags`, and `set_player_flags`. See the +[trigger step actions](triggers.md#step-actions) table. ```yaml on_enter: @@ -252,57 +319,76 @@ on_enter: despawn_on_leave: true ``` +#### Step-level conditions + +Each step can have a `condition:` that gates it individually. Unlike the step's action +fields, the condition is evaluated **once** on entry — a flag set by a later step won't +cancel an earlier step. + +```yaml +on_enter: + - condition: + player_flag: 1001_welcome + not: true + delay: 5 + message: "Welcome to The House of Icarus" +``` + Notes: -- `delay` counts ticks before the step fires; `delay: 0` (or omitted) fires on the next tick. -- Conditions are evaluated **once** on entry, so a flag a step sets won't cancel a later step in the same sequence. -- Gate a sequence on a flag the sequence itself sets (above, `lined_up`) so it doesn't replay on a return visit. -- If a player disconnects mid-sequence it resumes on reconnect, so they can't get stuck behind an exit the sequence was meant to open. Plain `on_enter` messages never replay on login (except the first room for new characters). -- Setting player flags from on_enter steps fires room triggers watching those flags — this is how room 1001's touchdown sequence works. +- `delay: 0` (or omitted) fires on the next tick. +- Gate a sequence on a flag the sequence itself sets so it doesn't replay on return visits. +- If a player disconnects mid-sequence it resumes on reconnect. +- Setting player flags from on_enter steps fires room triggers watching those flags. + +--- ### Conditional room descriptions -A room's `description` can be a plain string or a list of conditional variants. -Entries are checked top-to-bottom; the first whose condition passes wins. An -entry with no condition always matches — put it last as the fallback. +A room's `description` can be a plain string or a list of conditional variants. Entries are +checked top-to-bottom; the first whose condition passes wins. An entry with no condition +always matches — put it last as fallback. ```yaml description: - condition: player_flag: boarded not: true - text: "A concrete pad swarming with a couple dozen anxious passengers." + text: "A concrete pad swarming with anxious passengers." - text: "A large, empty concrete pad in the middle of the ocean." ``` -### Hazardous rooms — environmental danger +--- + +### Hazardous rooms -A room can reference a shared hazard by ID. The hazard rolls an attack against -everyone in the room every few ticks using the combat formulas (see `hazards.md`). +A room can reference a shared hazard by ID. The hazard rolls an attack against everyone in +the room every few ticks using combat formulas. ```yaml name: "Exposed Solar Array" -hazard: solar_radiation # ID of a data/hazards/<id>.yaml definition +hazard: solar_radiation # ID of data/hazards/solar_radiation.yaml exits: south: 99999180 objects: - - id: rock_outcrop # a safespot object shields players from the hazard + - id: rock_outcrop # safespot object shields players from the hazard mobs: - - solar_panel_frame # a task worksite (see mobs.md > Task Mobs) + - solar_panel_frame # task worksite (see mobs.md) ``` -- Players are warned with a `[Y/n]` prompt before walking from a **safe** room into a - **hazardous** one (unless they disable the `danger_warning` option). Moving between - two hazardous rooms does not re-prompt. -- A safespot object (an overhang, alcove, rock outcrop, …) shields a hidden player from - **all** hazard damage — but performing a melee attack/work step forces you out of cover. -- Hazards stack with mobs: an aggressive mob in a hazardous room hits you while the room - hazard also rolls against you. +- Players get a `[Y/n]` confirmation when walking from a safe room into a hazardous one + (unless `danger_warning` option is disabled). Moving between two hazardous rooms does not + re-prompt. +- Safespot objects shield hidden players from ALL hazard damage. +- Hazards stack with mobs: an aggressive mob in a hazardous room hits you while the hazard + also rolls. See `hazards.md` for the full hazard definition reference. -### Room triggers — scripted events when flags change +--- + +### Room triggers -A room can carry a `triggers:` block. Each trigger watches a player or global flag -and fires a sequence of timed steps when that flag's value changes. See `triggers.md` -for the full reference. +A room can carry a `triggers:` block. Each trigger watches a player or global flag and fires +a sequence of timed steps when the flag's value changes. See `triggers.md` for the full +reference. ```yaml triggers: @@ -316,9 +402,175 @@ triggers: 1001_touchdown: true ``` -Room triggers are scoped to the room — broadcasts go to that room, mobs spawn there, -and the trigger only fires when the flag-setting player is in that room. For -server-wide events, use global triggers in `data/triggers/` instead. +Room triggers are scoped to the room — broadcasts go to that room, mobs spawn there, and the +trigger only fires when the flag-setting player is in that room. For server-wide events, use +global triggers in `data/triggers/` instead. + +--- + +### Conditions Reference + +Conditions are used in exit gates, on-enter steps, room descriptions, on_use/on_look/on_kill +interactions, talk option guards, talk node conditions, and trigger value matching. + +#### Simple conditions + +A bare `global_flag:` / `player_flag:` check passes when the flag is **set to a truthy +value** (`true`, a non-zero number, a non-empty string). Add `value:` to match a specific +value. `not: true` inverts any check. + +```yaml +# Global flag is set +condition: + global_flag: gate_open + +# Global flag is NOT set +condition: + global_flag: gate_open + not: true + +# Player flag is set +condition: + player_flag: finished_tutorial + +# Player flag is NOT set (e.g. only on first visit) +condition: + player_flag: finished_tutorial + not: true + +# Match a specific (non-boolean) value +condition: + player_flag: quest_stage + value: 3 + +# Player has an item +condition: + has_item: bronze_key + +# Player does NOT have an item +condition: + has_item: bronze_key + not: true + +# Player has enough credits +condition: + min_credits: 50 +``` + +#### Compound conditions + +All must pass: + +```yaml +condition: + all_of: + - global_flag: gate_open + - has_item: pass_stub +``` + +Any one must pass: + +```yaml +condition: + any_of: + - has_item: bronze_key + - has_item: iron_key + - player_flag: master_of_unlocking +``` + +Nested compounds: + +```yaml +condition: + all_of: + - player_flag: quest_started + - any_of: + - has_item: wolf_pelt + - has_item: bear_pelt +``` --- +### Door Pattern Examples + +#### Global door: Button in another room (shared state) + +A button in room 3 opens a door in room 7. Anyone can press it. Once pressed, the door is +open for everyone. + +**Button object** (`data/objects/door_button.yaml`): + +```yaml +name: stone button +hidden: true +on_use: + - condition: + global_flag: secret_door_open + not: true + message: "You press the stone button. You hear grinding stone in the distance." + action: + set_global_flags: + secret_door_open: true +``` + +**Room 7** — contains the door: + +```yaml +name: "Hidden Passage" +exits: + south: 2 + north: + room: 8 + condition: + global_flag: secret_door_open + blocked_message: "A heavy stone door blocks the way." +objects: + - id: stone_door + hidden: true +``` + +#### Player door: Key-locked (per-player state) + +A locked door that only opens for a player carrying the key. Each player must find their +own key. + +**Key item** (`data/items/rusty_key.yaml`): + +```yaml +name: rusty key +description: "An old iron key, still functional." +value: 0 +stackable: false +``` + +**Room 5** — locked door: + +```yaml +name: "Locked Storage" +exits: + west: 2 + east: + room: 6 + condition: + has_item: rusty_key + blocked_message: "The iron door is locked. You need a key." +objects: + - id: iron_door + hidden: true +``` + +**Room 6** — the other side (no key needed to exit): + +```yaml +name: "Storage Closet" +exits: + west: 5 # exit back — no condition +item_spawns: + - id: uncut_ruby + quantity: 1 + respawn_ticks: 500 +``` + +The key difference: the button door uses `global_flag` (shared — one player presses, +everyone benefits); the key door uses `has_item` (per-player inventory — each player needs +their own key). diff --git a/building_guide/search.md b/building_guide/search.md deleted file mode 100644 index e6e2a14..0000000 --- a/building_guide/search.md +++ /dev/null @@ -1,15 +0,0 @@ -## The `search` Command - -Used to open searchable items in your inventory (bird's nests, etc.): -``` -Usage: search <item> - -Example: search nest - search birds nest -``` - -Searches your inventory for the named item. If found, removes it and rolls on a drop table. -Currently only bird's nests are searchable. - ---- - diff --git a/building_guide/state.md b/building_guide/state.md deleted file mode 100644 index fe526bf..0000000 --- a/building_guide/state.md +++ /dev/null @@ -1,13 +0,0 @@ -## State: World vs Player - -**Global flags** (`set_global_flags`, checked with `global_flag`) are shared by every player on the server. A door opened by one player is open for everyone. A lever pulled once changes the world for all. Stored in memory (`GlobalFlagStore`) — **lost on server restart**. Numeric values are compared by coercion (int/int64/float64 are normalized), so a YAML-decoded `3` matches a code-set `int(3)`. - -**Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest progress, "has read the sign," "paid the toll" — these are different for each player. Saved to the character YAML and persist across logins. - -### Triggers — flag-change events - -Any flag change (world or player) can activate a **trigger** — a timed sequence of -messages, broadcasts, spawns, and further flag mutations. See `triggers.md` for -the full system. This is how you turn "player looked at the sign" into "spaceship -landed and stairs opened." - diff --git a/building_guide/tips.md b/building_guide/tips.md index 61c595f..0c598a5 100644 --- a/building_guide/tips.md +++ b/building_guide/tips.md @@ -12,12 +12,10 @@ 6. **The `talk` field on mobs** lets you talk to them directly — `talk guard` finds the guard mob, no duplicate object entry needed. -7. **Exits accept both `int` and `map` formats.** `north: 2` is shorthand for `north: {room: 2}`. Add `condition` and `blocked_message` only when needed. Mob room entries accept both `"man"` and `{id: man, ...}`. +7. **Live editing works.** Room, item, mob, object, and behavior YAML files are read from disk on each access. Change a room description or dialog and it takes effect immediately — no restart needed. -8. **Live editing works.** Room, item, mob, object, and behavior YAML files are read from disk on each access. Change a room description or dialog and it takes effect immediately — no restart needed. +8. **Shared depletion vs per-drop depletion.** Use `deplete_timer` for trees — the timer counts down while being chopped and regens when left alone. Use `depletes: true` on individual drops for rocks — they deplete on first successful gather. -9. **Shared depletion vs per-drop depletion.** Use `deplete_timer` for trees — the timer counts down while being chopped and regens when left alone. Use `depletes: true` on individual drops for rocks — they deplete on first successful gather. +9. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms. -10. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms. - -11. **Use triggers to decouple cause and effect.** Objects set flags; triggers watch flags. This way the same "pull lever" action can spawn a boss in one room, open a door in another, and broadcast to the server — all by watching the same flag from different triggers. Room triggers keep local events local; global triggers (`data/triggers/`) handle server-wide events. +10. **Use triggers to decouple cause and effect.** Objects set flags; triggers watch flags. This way the same "pull lever" action can spawn a boss in one room, open a door in another, and broadcast to the server — all by watching the same flag from different triggers. Room triggers keep local events local; global triggers (`data/triggers/`) handle server-wide events. diff --git a/building_guide/triggers.md b/building_guide/triggers.md index 69cd3d1..38dbda6 100644 --- a/building_guide/triggers.md +++ b/building_guide/triggers.md @@ -1,7 +1,28 @@ ## Triggers Triggers let you script a sequence of timed events that fire when a flag changes -value. They come in two flavors: +value. + +### Flags: Global vs Player State + +**Global flags** (`set_global_flags`, checked with `global_flag`) are shared by every +player on the server. A door opened by one player is open for everyone. A lever pulled once +changes the world for all. Stored in memory — lost on server restart. Numeric values are +compared by coercion (int/int64/float64 are normalized), so a YAML-decoded `3` matches a +code-set `int(3)`. + +**Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest +progress, "has read the sign," "paid the toll" — these are different for each player. Saved +to the character YAML and persist across logins. + +**Any flag change** — world or player, set from on_look, on_use, on_kill, talk nodes, +on-enter steps, exit on_traverse, trigger steps, or admin commands — can activate a +trigger. This is how you turn "player looked at the sign" into "spaceship landed and stairs +opened." + +### Trigger flavors + +Triggers come in two flavors: - **Room triggers** — defined in room YAML under a `triggers:` block. The room is the action context (broadcasts go to that room, mobs spawn there). diff --git a/building_guide/wandering_mobs.md b/building_guide/wandering_mobs.md deleted file mode 100644 index 7e2bfb6..0000000 --- a/building_guide/wandering_mobs.md +++ /dev/null @@ -1,20 +0,0 @@ -## Wandering Mobs - -Mob wandering is configured per-instance in the room YAML, not on the mob definition. (`- id: man` here references the mob whose **filename** is `man.yaml`.) -```yaml -# In a room: -mobs: - - id: man - wander_interval: 10 # attempt to wander every 10 ticks - - id: man - wander_interval: 15 - wander_rooms: [5, 6, 7] # optional — restrict which rooms via exits -``` - -Mobs wander through legal (unconditioned) room exits. When the wander interval expires, -the mob picks a random exit with no conditions and moves through it. If `wander_rooms` -is set, only exits leading to those room IDs are legal. Mobs without `wander_interval` -never wander. Mobs stop wandering while in combat. Dead mobs respawn at their home room. - ---- - diff --git a/building_guide/wandering_objects.md b/building_guide/wandering_objects.md deleted file mode 100644 index a106a0c..0000000 --- a/building_guide/wandering_objects.md +++ /dev/null @@ -1,37 +0,0 @@ -## Wandering Objects - -Fishing spots that move between rooms (**the filename is the ID** — `fishing_spot.yaml` → `fishing_spot`, no `id:` field): -```yaml -# data/objects/fishing_spot.yaml -name: fishing spot -gather: - skill: fishing - tools: - - fishing_rod - bait: fishing_bait - success: - base: 0.30 - per_level: 0.01 - cap: 0.90 - gather_message: "You cast your line into the water..." - drops: - - item_id: raw_trout - level: 5 - xp: 20 - weight: 100 - message: "You catch a raw trout!" -``` - -```yaml -# In a room: -objects: - - id: fishing_spot - wander_rooms: [7, 8, 9] - wander_interval: 12 -``` - -Objects teleport between rooms in their `wander_rooms` list. Players gathering from a -wandering object are silently interrupted when it moves. - ---- - |
