From 9d4b799db4868bcab291b83599ff088763cd4ed6 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 24 Jun 2026 20:58:25 -0400 Subject: feat: new type of non-violent 'combat': work --- building_guide/bank.md | 68 ++++++ building_guide/behaviors.md | 402 ++++++++++++++++++++++++++++++++++++ building_guide/conditions.md | 80 +++++++ building_guide/construction.md | 223 ++++++++++++++++++++ building_guide/courses.md | 119 +++++++++++ building_guide/doors.md | 97 +++++++++ building_guide/drops.md | 52 +++++ building_guide/hacking.md | 49 +++++ building_guide/hazards.md | 75 +++++++ building_guide/hidden_objects.md | 92 +++++++++ building_guide/items.md | 190 +++++++++++++++++ building_guide/mobs.md | 221 ++++++++++++++++++++ building_guide/objects.md | 392 +++++++++++++++++++++++++++++++++++ building_guide/quests.md | 103 +++++++++ building_guide/recipes.md | 277 +++++++++++++++++++++++++ building_guide/rooms.md | 209 +++++++++++++++++++ building_guide/search.md | 15 ++ building_guide/state.md | 8 + building_guide/tips.md | 21 ++ building_guide/wandering_mobs.md | 20 ++ building_guide/wandering_objects.md | 39 ++++ 21 files changed, 2752 insertions(+) create mode 100644 building_guide/bank.md create mode 100644 building_guide/behaviors.md create mode 100644 building_guide/conditions.md create mode 100644 building_guide/construction.md create mode 100644 building_guide/courses.md create mode 100644 building_guide/doors.md create mode 100644 building_guide/drops.md create mode 100644 building_guide/hacking.md create mode 100644 building_guide/hazards.md create mode 100644 building_guide/hidden_objects.md create mode 100644 building_guide/items.md create mode 100644 building_guide/mobs.md create mode 100644 building_guide/objects.md create mode 100644 building_guide/quests.md create mode 100644 building_guide/recipes.md create mode 100644 building_guide/rooms.md create mode 100644 building_guide/search.md create mode 100644 building_guide/state.md create mode 100644 building_guide/tips.md create mode 100644 building_guide/wandering_mobs.md create mode 100644 building_guide/wandering_objects.md (limited to 'building_guide') diff --git a/building_guide/bank.md b/building_guide/bank.md new file mode 100644 index 0000000..0a5ec88 --- /dev/null +++ b/building_guide/bank.md @@ -0,0 +1,68 @@ +# Bank System + +The bank system allows players to store items permanently, freeing up inventory space. + +## Objects + +### bank_booth + +A bank terminal that players can interact with. Has no special behavior — the `bank` command checks for any `bank_booth` object in the room. + +```yaml +id: bank_booth +name: bank booth +color: "226" +description: "..." +inroom_description: "A bank booth is set into the wall." +``` + +To place a bank booth in a room, add it to the room's `objects:` list: + +```yaml +objects: + - id: bank_booth +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `bank` | Opens bank interface (requires bank booth in room) | +| `use bank` | Same as `bank` | + +### Bank Interface Commands + +| Command | Description | +|---------|-------------| +| `deposit ` | Move item from inventory to bank | +| `deposit all` | Deposit entire inventory | +| `withdraw ` | Move item from bank to inventory | +| `browse` / `list` | Show bank contents | +| `leave` | Exit bank interface | + +## Implementation + +### Session State + +`StateBank` (defined in `internal/net/server.go`) is the session state for the bank interface. Input is routed through `handleBankInput` in `internal/game/cmd_bank.go`. + +### Player Storage + +Bank items are stored in the `Player.Bank` field (`map[int]*InventorySlot`), saved in character YAML. Bank slots are sequential and reuse freed slots. There is no slot limit. + +### Command Dispatch + +- `bank` is classified as `ClassActive` in `classifyCommand()` +- `bank` case in `executeCommand()` calls `doBank(sess)` +- `doUse()` checks for `"bank"` or `"bank booth"` targets and routes to `doBank()` +- `doBank()` checks for a `bank_booth` object in the current room +- `handleBankInput()` dispatches deposit/withdraw/browse/leave commands + +### Data Files + +| File | Purpose | +|------|---------| +| `data/objects/bank_booth.yaml` | Bank booth object definition | +| `data/help/bank.yaml` | Help topic for the bank system | + +Default bank booth locations: Town Square (room 1), Forge (room 12). diff --git a/building_guide/behaviors.md b/building_guide/behaviors.md new file mode 100644 index 0000000..22e97fe --- /dev/null +++ b/building_guide/behaviors.md @@ -0,0 +1,402 @@ +## 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. + +### Gather (mining, fishing, woodcutting) + +Mining — per-drop depletion on a rock object: +```yaml +id: copper_rock +name: copper rock +color: "178" +gather: + skill: mining + level: 1 + xp: 17 # XP awarded per successful gather + base_wait: 8 # ticks between attempts + tools: # requires item with matching tool_type + - pickaxe + success: + base: 0.40 # 40% base chance + per_level: 0.01 # +1% per level above requirement + cap: 0.95 # 95% max + gather_message: "You swing your pickaxe at the rock..." + fail_message: "You chip away but get nothing useful." + drops: + - item_id: copper_ore + weight: 90 # 90% chance when roll succeeds + depletes: true # rock becomes depleted after this drop + message: "You manage to mine some {178}copper ore{/}." + - table: gem_table # reference a shared drop 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 + respawn_broadcast: "A glint of copper catches your eye from some {name}." +``` + +Drop messages support inline color tags: `{<0-255>}text{/}`. Use the item's color +index to match its display color. Gradients also work: `{g:196,82}text{/}`. + +Non-depleting gather (fishing on a fishing spot object): +```yaml +id: bait_fishing_spot +name: fishing spot +gather: + skill: fishing + level: 1 + xp: 10 + base_wait: 4 + 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..." + fail_message: "Nothing seems to bite." + drops: + - item_id: raw_trout + weight: 100 + depletes: false # never depletes + message: "You catch a {69}raw trout{/}!" +``` + +Woodcutting with shared depletion and bird's nests: +```yaml +id: oak_tree +name: oak tree +color: "113" +gather: + skill: woodcutting + level: 15 + xp: 37 + base_wait: 6 + 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 + weight: 100 + depletes: false # depletion is timer-based (deplete_timer) + message: "You get some {113}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 + respawn_broadcast: "An {name} grows back." +``` + +Regular tree — always depletes on first gather, no shared timer, no nests: +```yaml +id: tree +name: tree +gather: + skill: woodcutting + level: 1 + xp: 25 + base_wait: 4 + tools: + - axe + success: + base: 0.50 + per_level: 0.01 + cap: 0.95 + gather_message: "You swing your axe at the tree..." + fail_message: "You swing but get no logs." + drops: + - item_id: logs + weight: 100 + depletes: true # regular tree depletes on first successful gather + message: "You get some {107}logs{/}." + respawn_timer: 80 + respawn_broadcast: "A {name} grows back." +``` + +#### GatherConfig Fields + +| Field | Type | Description | +|---|---|---| +| `skill` | string | Skill name for level check | +| `level` | int | Required level to gather | +| `xp` | int | XP per successful gather | +| `base_wait` | float64 | Base ticks per action cycle | +| `tools` | []string | Required tool_type list (e.g. `[pickaxe]`) | +| `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 | +| `drops` | []DropEntry | Weighted drop entries | +| `respawn_timer` | float64 | Ticks until depleted object respawns | +| `respawn_broadcast` | string | Broadcast message 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) | +| `per_level` | float64 | Chance increase per level above requirement | +| `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 | +| `depletes` | bool | Resource depletes on this drop | +| `quantity` | int | Amount to drop (1-3 for random) | +| `message` | string | Player message on drop | +| `level` | int | Skill level required for this drop | +| `xp` | int | Bonus XP on this drop | + +#### Shared depletion explained + +When `deplete_timer > 0`, the tree has a shared despawn timer: +- The 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. +- All players chopping the same tree are interrupted when it depletes. + +Use `deplete_timer` for trees. Use `depletes: true` on individual drops for rocks. + +#### Bird's nests + +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. + +#### XP drops + +When `xp > 0`, the gather awards XP on each successful drop. If the player's `xpdrops` +toggle is on, the output includes the XP gain: `(+37xp wct)`. + +### Talk (dialog trees) + +Talk configs go under the `talk:` key on objects or mobs. Full conversation with +conditions, actions, and player flag tracking: + +```yaml +id: guard +name: Guard +talk: + nodes: + start: + message: "\"Halt! This area is restricted.\"" + options: + - text: "\"What's behind that gate?\"" + goto: about_gate + - text: "\"I have copper ore.\"" + goto: trade_ore + condition: + has_item: copper_ore + - text: "\"I have a pass.\"" + goto: has_pass + condition: + player_flag: got_pass + value: true + - text: "\"Goodbye.\"" + end: true + + about_gate: + message: "\"Bring me some copper ore and I'll stamp you a pass.\"" + action: + set_player_flags: + talked_to_guard: true + options: + - text: "\"I'll be back.\"" + end: true + - text: "\"I have some right here.\"" + goto: trade_ore + condition: + has_item: copper_ore + + trade_ore: + message: "\"Good quality ore.\" He stamps a pass and hands it to you." + action: + take_item: copper_ore + give_item: pass_stub + set_player_flags: + got_pass: true + options: + - text: "\"Thanks.\"" + end: true + + has_pass: + message: "\"Alright, I'll open the gate for you.\"" + action: + set_flags: + gate_open: true + options: + - text: "\"Thanks.\"" + end: true +``` + +#### Node action reference + +| Field | Effect | +|---|---| +| `set_flags` | Sets world 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 | +| `cost` | Credits charged for the action | +| `shop` | Opens a buy/sell shop interface (see Shop section below) | +| `assign_task` | Assigns a random assassin task to the player based on their assassin level | +| `skip_task` | Cancels current assassin task, costs 30 reputation, resets streak | +| `extend_task` | Adds 50% more kills to current task, costs 30 reputation | +| `reputation_cost` | Deducts reputation from the player's `assassin_reputation` flag (fails node if insufficient) | +| `sawmill` | Opens sawmill plank conversion interface | + +All fields in a single action are processed together — you can give an item, take an item, +set flags, and heal all in one node. + +Example — quest completion: +```yaml +action: + take_item: dragon_head + give_item: dragon_slayer_medal + set_player_flags: + dragon_quest: complete + dragon_slain: true + heal: 99 + teleport: 1 +``` + +#### Shop (buy/sell interface) + +The `shop` node action opens a dedicated buy/sell interface. The player can browse +items, buy with credits, and sell items back. Define a shop on a talk node: + +```yaml +action: + shop: + message: "What would you like to buy or sell?" + items: + - item_id: fishing_rod + buy_price: 10 + sell_price: 2 + - item_id: fishing_bait + buy_price: 2 + sell_price: 0 +``` + +| Field | Description | +|---|---| +| `shop.message` | Greeting shown when entering the shop | +| `shop.items` | List of items for sale | +| `item_id` | Item definition ID | +| `buy_price` | Credits to buy from shop | +| `sell_price` | Credits shop pays (0 = won't buy) | + +Typical shop flow: a talk node with a "Browse" option leads to a `shop` node. +When the player leaves the shop, they return to the talk node's options. + +Full example — General Store: +```yaml +id: general_store_clerk +name: General Store Clerk +talk: + nodes: + start: + message: "\"Welcome to the General Store!\"" + options: + - text: "\"I'd like to browse.\"" + goto: shop + - text: "\"Goodbye.\"" + end: true + shop: + message: "\"Take your time.\"" + action: + shop: + message: "What would you like to buy or sell?" + items: + - item_id: fishing_rod + buy_price: 10 + sell_price: 2 + - item_id: hammer + buy_price: 20 + sell_price: 5 + options: + - text: "\"I'm done.\"" + goto: start +``` + +Node options on the shop node are shown when the player leaves the shop. +Use `goto: start` to loop back to the main greeting. + +### Object Interactions (levers, switches, gates) + +Interaction configs go under the `use_interactions:` key on objects. +Entries with no `item` field are bare interactions triggered by "use" +or "push"/"pull": + +```yaml +id: iron_gate +name: iron gate +hidden: true +description: "A heavy iron gate set into the north wall." +use_interactions: + - condition: + flag: gate_open + value: true + not: true + message: "You push the heavy iron gate open." + action: + set_flags: + gate_open: true +``` + +Item-specific interaction: +```yaml +id: bookshelf +name: bookshelf +use_interactions: + - item: dusty_tome + message: "The bookshelf slides aside, revealing a secret passage!" + action: + set_flags: + secret_passage_open: true +``` + +### Use (crafting stations) + +Use configs go under the `use:` key on objects (e.g. furnaces, ranges): + +```yaml +id: furnace +name: furnace +color: "208" +use: + message: "You place the ore in the furnace..." + wait: 4 + consume: + copper_ore: 1 + reward: + item_id: copper_bar + quantity: 1 + fail_message: "The ore crumbles to dust." + success: + base: 0.60 + per_level: 0.01 + cap: 0.95 + skill: smithing + level: 1 + xp: 15 +``` + +--- diff --git a/building_guide/conditions.md b/building_guide/conditions.md new file mode 100644 index 0000000..ea17ed6 --- /dev/null +++ b/building_guide/conditions.md @@ -0,0 +1,80 @@ +## Conditions Reference + +Conditions are used in talk options, exit gates, on-enter scripts, and use_interactions checks. + +### Simple conditions + +A bare `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 world flag is set +condition: + flag: gate_open + +# Check a world 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 new file mode 100644 index 0000000..2c4b7ce --- /dev/null +++ b/building_guide/construction.md @@ -0,0 +1,223 @@ +# Construction Skill + +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. + +## Skill + +- Skill name: `construction` +- Abbreviation: `con` +- Level range: 1-99 +- XP table: RSC standard + +## Commands + +| Command | Description | +|---------|-------------| +| `construct` / `make` | Open construction menu at a workbench | +| `construct ` | Build a specific item | +| `construct ` | Build multiple of an item | + +## Objects + +### workbench + +The primary construction station. Placed in rooms to enable construction. + +```yaml +id: workbench +name: workbench +color: "172" +description: "A sturdy wooden workbench..." +inroom_description: "A workbench stands against the wall..." +``` + +### 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. + +```yaml +id: estate_directory +name: Estate Directory +color: "39" +description: "A holographic terminal displaying property records..." +inroom_description: "An estate directory terminal..." +``` + +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 + +### saw + +Required tool for cutting logs into planks at the workbench. + +```yaml +id: saw +name: saw +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 | + +### Furniture (sellables) + +| 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 | + +## Recipes + +All recipes use `type: construction` and `station: [workbench]`. Log-to-plank recipes additionally require `tool: saw`. + +Example plank recipe: +```yaml +id: construct_oak_planks +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." +``` + +Example furniture recipe: +```yaml +id: construct_wooden_table +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." +``` + +## NPCs + +### estate_broker + +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. + +```yaml +id: estate_broker +name: estate broker +talk: + nodes: + start: + message: "\"Interested in a house?\"" + options: + - text: "\"How much?\"" + goto: offer + - text: "\"No thanks.\"" + end: true + offer: + message: "\"10 credits for a plot.\"" + action: + cost: 10 + set_player_flags: + owns_house_local_neighborhood: "local_neighborhood" + options: + - text: "\"Deal!\"" + end: true +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"}, +``` + +Usage: `trigger plank make` (all logs) or `trigger plank make ` (specific). + +## Player Houses + +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. + +House room files follow the same format as world rooms: +```yaml +id: 200 +name: "Player House: Entrance Hall" +description: "..." +exits: + south: 170 + north: 201 +objects: + - id: charging_station +``` + +The room loader (`World.LoadRoom`) checks `data/rooms/.yaml` first, then falls back to `data/rooms/player_housing/.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"}, +``` + +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 | diff --git a/building_guide/courses.md b/building_guide/courses.md new file mode 100644 index 0000000..491e2bd --- /dev/null +++ b/building_guide/courses.md @@ -0,0 +1,119 @@ +## Agility Courses + +Agility courses are defined in `data/courses/.yaml`. Each course defines a sequence of obstacle rooms, each with a specific verb the player must type to advance. Obstacle rooms themselves are standard room YAML files (typically in `data/rooms/agility/`). + +### Course YAML + +```yaml +id: "vent_shaft" +name: "Ventilation Shaft Course" +required_level: 1 +start_room: 200 +completion_xp: 40 +obstacles: + - room_id: 201 + verb: scramble + ticks_per_phase: 2 + xp: 8 + fail_damage: [1, 2] + messages: + - "You approach the corroded ventilation wall..." + - "You find footholds in the rusted panels and begin to climb..." + - "You scramble up the wall and haul yourself onto the ledge!" + - room_id: 202 + verb: balance + ticks_per_phase: 2 + xp: 8 + fail_damage: [1, 2] + messages: + - "You step onto the narrow coolant pipe..." + - "Arms outstretched, you carefully place one foot in front of the other..." + - "You reach the other side of the pipe and step onto solid ground!" +``` + +### CourseConfig Fields + +| Field | Type | Description | +| ---------------- | ------------- | ---------------------------------------------------- | +| `id` | string | Unique course identifier | +| `name` | string | Display name shown to players | +| `required_level` | int | Minimum Agility level to attempt the course | +| `start_room` | int | Hub room — teleported here on fail or lap completion | +| `completion_xp` | int | Bonus XP awarded when a full lap is completed | +| `obstacles` | []ObstacleDef | Ordered list of obstacles | + +### ObstacleDef Fields + +| Field | Type | Description | +| ----------------- | -------- | --------------------------------------------------------------------------- | +| `room_id` | int | Room ID for this obstacle | +| `verb` | string | Command the player types to attempt it (e.g. `scramble`, `jump`, `climb`) | +| `ticks_per_phase` | float64 | Ticks between each phase message (supports fractional via `engine.ToTicks`) | +| `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) | +| `messages` | []string | Exactly 3 strings for the 3-phase advancement system | + +### 3-Phase Message System + +Each obstacle advances through 3 phases, printing one message per phase: + +``` +Phase 0 (start): messages[0] printed immediately +Phase 1 (middle): messages[1] printed — FAILURE CHECK happens here +Phase 2 (end): messages[2] printed — XP awarded, teleport to next room +``` + +Failure only occurs at phase 1. Success chance is based on Agility level: +- At required level: 70% success (30% fail) +- Each level above reduces fail chance by 1% (down to minimum 5%) +- Fail chance capped at 60% + +On failure, the player takes random damage in `[fail_damage[0], fail_damage[1]]`, is teleported back to `start_room`, and must restart the course. + +### Lap Counting + +Completing all obstacles in a course increments a lap counter stored as a player flag (`agility_laps_`). The `completion_xp` bonus is awarded on the final obstacle only. + +### Obstacle Room YAML + +Each obstacle room should have an `on_enter` message telling the player which verb to use, and a `down` exit back to the course hub: + +```yaml +id: 201 +name: "Ventilation Shaft - Corroded Wall" +description: "A towering wall of corroded ventilation panels..." +on_enter: + - message: "Type 'scramble' to climb the wall." +exits: + down: + room: 200 + blocked_message: "" +``` + +The hub room (e.g. room 200) links to the first obstacle of each course: + +```yaml +id: 200 +name: "Agility Training Grounds" +exits: + south: 17 + north: 201 # Vent Shaft (level 1) + east: 210 # Rooftop (level 20) + west: 220 # Reactor (level 50) +``` + +### Supported Obstacle Verbs + +| Verb | Gerund (display) | +| ---------- | ---------------- | +| `scramble` | scrambling | +| `jump` | jumping | +| `swing` | swinging | +| `balance` | balancing | +| `climb` | climbing | +| `crawl` | crawling | +| `vault` | vaulting | +| `leap` | leaping | +| `slide` | sliding | + +Obstacle verbs are auto-registered at startup from all course YAML files. No changes to `cmd_registry.go` needed when adding new courses — any verb in a `data/courses/` file will work. diff --git a/building_guide/doors.md b/building_guide/doors.md new file mode 100644 index 0000000..1e9fb7a --- /dev/null +++ b/building_guide/doors.md @@ -0,0 +1,97 @@ +## Global vs Player State: Door Examples + +### 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 +id: door_button +name: stone button +hidden: true +use_interactions: + - condition: + flag: secret_door_open + not: true + message: "You press the stone button. You hear grinding stone in the distance." + action: + set_flags: + secret_door_open: true +``` + +**Room 3** — contains the button: +```yaml +id: 3 +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 +id: 7 +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 +id: rusty_key +name: rusty key +description: "An old iron key, still functional." +value: 0 +stackable: false +``` + +**Room 5** — locked door: +```yaml +id: 5 +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 +id: 6 +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 `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/drops.md b/building_guide/drops.md new file mode 100644 index 0000000..e5a17ff --- /dev/null +++ b/building_guide/drops.md @@ -0,0 +1,52 @@ +## Drop Tables + +Shared drop tables can be referenced by multiple behaviors: +```yaml +# data/drops/gem_table.yaml +id: gem_table +drops: + - item_id: uncut_sapphire + weight: 47 + - item_id: uncut_emerald + weight: 16 + - item_id: uncut_ruby + weight: 4 + - item_id: uncut_diamond + weight: 1 +``` + +Bird's nest drop table: +```yaml +# data/drops/birds_nest_drop.yaml +id: birds_nest_drop +drops: + - item_id: credits + weight: 50 + quantity: 200 + - item_id: credits + weight: 30 + quantity: 500 + - item_id: credits + weight: 15 + quantity: 1000 + - item_id: credits + weight: 4 + quantity: 3000 + - item_id: credits + weight: 1 + quantity: 10000 +``` + +Referenced from a gather behavior: +```yaml +drops: + - item_id: copper_ore + weight: 90 + depletes: true + - table: gem_table # pulls from data/drops/gem_table.yaml + weight: 10 + depletes: false +``` + +--- + diff --git a/building_guide/hacking.md b/building_guide/hacking.md new file mode 100644 index 0000000..8927b3e --- /dev/null +++ b/building_guide/hacking.md @@ -0,0 +1,49 @@ +## Hacking + +Hacking minigames are played by jacking into terminal objects. Terminals are YAML objects that are registered in Go's `terminalDefs` map. + +### Terminal Object YAML + +```yaml +id: terminal_basic +name: basic terminal +color: "40" +description: "A battered terminal with a cracked screen. {40}[Level 1 Hacking]{/}" +inroom_description: "A {40}basic terminal{/} hums quietly against the wall." +``` + +- `id` must match a key in `terminalDefs` (Go code in `internal/game/hacking.go`) +- `name` is used for matching in the `jack` command +- `color` controls the display color +- `description` is shown when examining the terminal +- `inroom_description` is shown in room listings +- No `behavior` field — terminals are registered in Go, not YAML behaviors + +### Adding a New Terminal + +1. Create an object YAML file in `data/objects/terminal_.yaml` +2. Add an entry to `terminalDefs` in `internal/game/hacking.go` +3. Optionally create a new minigame implementation in `internal/game/hacking_.go` +4. Place the terminal object in a room's `objects:` list + +### Adding a New Minigame + +New minigames implement the `HackingMinigame` interface: + +```go +type HackingMinigame interface { + Init(level int) string + HandleInput(input string) (output string, done bool, won bool) + Name() string +} +``` + +Optionally implement `XPBonuser` for bonus XP: + +```go +type XPBonuser interface { + BonusXP() int +} +``` + + diff --git a/building_guide/hazards.md b/building_guide/hazards.md new file mode 100644 index 0000000..453ca18 --- /dev/null +++ b/building_guide/hazards.md @@ -0,0 +1,75 @@ +## Hazards + +A **hazard** is an environmental threat attached to a room — a sandstorm, solar +radiation, falling rocks, toxic spores. It rolls an "attack" against everyone in +the room every few ticks using the **same combat formulas as a mob**: the hazard +supplies the accuracy and max hit, and the player defends with their Defense +level plus the equipment defense bonus selected by the hazard's attack type. No +new combat math, no special gear — your existing armour and Defense skill are +what keep you alive. + +Hazards are **shared, ID-referenced definitions** (like drop tables and modules): +define a hazard once in `data/hazards/.yaml`, then reference it from any number +of rooms with `hazard: ` (see `rooms.md`). This makes one hazard reusable +across a whole area and lets a future `examine` command resolve it by name. + +```yaml +id: solar_radiation +name: scorching solar radiation +description: "Unfiltered sunlight pours through a gap in the dome, burning exposed skin." +attack_type: science # stab/slash/crush/ranged/science — picks player defense + protect tech +attack: 40 # accuracy roll level +attack_bonus: 0 # equipment-equivalent accuracy bonus +max_hit: 6 # maximum damage per hit +speed: 5 # ticks between hazard rolls +warn_message: "The dome shielding fails here — raw solar radiation pours in." +hit_message: "Solar radiation sears you for %d damage." # optional single %d for the damage +miss_message: "You shield your eyes and weather a wave of solar glare." +required_item: "" # optional: an equipped item that NEGATES the hazard entirely +``` + +### Fields + +| Field | Description | +|---|---| +| `id` | Unique identifier (matches the filename) | +| `name` | Display name used in warnings and default messages | +| `description` | Flavour text (for a future `examine`) | +| `attack_type` | `stab`/`slash`/`crush`/`ranged`/`science` — selects which player defense bonus applies and which protection tech reduces the damage (empty = `crush`) | +| `attack` | Hazard accuracy level | +| `attack_bonus` | Equipment-equivalent accuracy bonus | +| `max_hit` | Maximum damage per hit | +| `speed` | Ticks between hazard rolls (default 5 if ≤ 0) | +| `warn_message` | Shown when warning the player about the area | +| `hit_message` | Message on a hit; include a single `%d` to show the damage | +| `miss_message` | Message on a miss | +| `required_item` | If set and equipped, the hazard is fully negated for that player | + +### How it behaves + +- **Applies to everyone in the room, always** — idle, passing through, working a task, + or fighting a mob. Strong incentive to work fast, take cover, or leave. +- **Tech protection applies automatically.** Because the hazard has an attack type, the + matching protection tech reduces its damage: Kinetic Barrier (melee types), + Projectile Screen (`ranged`), Neural Firewall (`science`). +- **Safespots block all hazard damage.** A hidden player behind a safespot object takes + no hazard damage, regardless of hazard type. Note that doing a *melee* attack/work step + forces you out of cover — so ranged/science work lets you stay sheltered, melee does not. +- **Right gear negates it.** If `required_item` is set and equipped, the hazard is skipped + for that player entirely. The item must exist (validated at startup). +- **Hazards never interrupt movement.** Unlike a mob hit (which aborts a flee with + "Can't escape!"), a hazard hit does not touch your walk — you can keep moving out of + the danger zone while taking hits. + +### Entry warning + +Players walking from a **safe** room into a **hazardous** one get a `[Y/n]` confirmation +(pressing return defaults to yes; anything other than empty/`y`/`yes` cancels). Moving +between two hazardous rooms does not re-prompt. Players can disable the prompt with the +account option `danger_warning` (`option danger_warning false`). + +### Examine (forward-compatible) + +Because the hazard is a shared def referenced by ID, a future `examine sandstorm` can +resolve the current room's `hazard` ID, load the def, and render its attack type, accuracy, +max hit and description — no hidden objects required. diff --git a/building_guide/hidden_objects.md b/building_guide/hidden_objects.md new file mode 100644 index 0000000..e79c026 --- /dev/null +++ b/building_guide/hidden_objects.md @@ -0,0 +1,92 @@ +## 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. + +```yaml +# data/objects/secret_lever.yaml +id: secret_lever +name: stone lever +hidden: true +description: "A cleverly concealed lever behind a loose stone." +use_interactions: + - condition: + flag: secret_passage_open + value: true + not: true + message: "You pull the lever. A grinding sound echoes from the east." + action: + set_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 +id: 1002_shuttle +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 can show different `look` text depending on the looking player's flags, +using a `descriptions` list (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 ` 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 +id: 1002_crowd +name: crowd of people +aliases: [people, crowd, passengers] +hidden: true +descriptions: + - 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 plain `description:` (with no `descriptions:` list) 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 ` (name matching works even for hidden objects) +- Using `look ` 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 new file mode 100644 index 0000000..52bfa7d --- /dev/null +++ b/building_guide/items.md @@ -0,0 +1,190 @@ +## Items + +```yaml +id: bronze_pickaxe +name: bronze pickaxe +color: "178" # xterm-256 color index (0-255) +aliases: ["pick", "pickaxe"] +description: "A sturdy bronze pickaxe." +value: 10 +stackable: false +equip_slot: main_hand # optional — where it equips +weapon_type: melee # optional — melee, ranged, or science +attack_type: stab # stab/slash/crush/ranged/science — determines accuracy type +stats: # optional — per-type combat bonuses + stab_attack: 4 + slash_attack: -2 + crush_attack: 2 + strength_bonus: 3 +speed: 5 # ticks between attacks +tool_type: pickaxe # used by gather behaviors that require "tool: pickaxe" +tool_speed: 2 # reduces gather wait time +requirements: # optional — skill levels needed to equip + accuracy: 1 +``` + +### Equipment Stats (ItemStats) + +Weapons have per-type **attack bonuses** — these determine accuracy based on the weapon's `attack_type`: + +| Field | Description | +|---|---| +| `stab_attack` | Accuracy bonus for stab attacks | +| `slash_attack` | Accuracy bonus for slash attacks | +| `crush_attack` | Accuracy bonus for crush attacks | +| `science_attack` | Accuracy bonus for science attacks | +| `ranged_attack` | Accuracy bonus for ranged attacks | + +Armor has per-type **defense bonuses** — the mob's `attack_type` determines which defense is used: + +| Field | Description | +|---|---| +| `stab_defense` | Defense vs stab attacks | +| `slash_defense` | Defense vs slash attacks | +| `crush_defense` | Defense vs crush attacks | +| `science_defense` | Defense vs science attacks (negative on metal armor) | +| `ranged_defense` | Defense vs ranged attacks | + +Other damage/utility bonuses: + +| Field | Description | +|---|---| +| `strength_bonus` | Melee max hit bonus | +| `ranged_strength` | Ranged max hit bonus (on ammo) | +| `science_damage` | Science max hit bonus | +| `technology_bonus` | Reduces tech battery drain rate | + +### Attack Type + +Weapons should always set `attack_type`. Common mappings: + +| Category | attack_type | +|---|---| +| Swords | slash | +| Daggers | stab | +| Axes | slash | +| Pickaxes | stab | +| Bows | ranged | +| Unarmed | crush (default) | + +### Equipment Requirements + +Higher-tier equipment requires skill levels to equip: + +```yaml +requirements: + accuracy: 20 # melee weapons + defense: 20 # armor + ranged: 30 # ranged weapons/armor +``` + +Standard tiers: bronze/iron (none), steel (5), black (10), mithril (20), adamant (30), rune (40), dragon (60). + +### Ranged Ammo + +Arrows and bolts use `ranged_strength` for damage. Accuracy comes from the bow: + +```yaml +id: iron_arrow +name: iron arrow +stackable: true +equip_slot: ammo +stats: + ranged_strength: 10 +``` + +### Color + +The `color` field accepts xterm-256 palette indices (0-255) with optional modifiers: + +```yaml +color: "178" # bronze/gold +color: "75 bold" # bold steel blue +color: "g:196,208,226" # gradient red → orange → gold +``` + +In ANSI mode, extended colors (16-255) automatically downgrade to the nearest ANSI color. Use `colortable` in-game to see all 256 colors. + +Key item (quest token, not equippable): +```yaml +id: pass_stub +name: pass stub +description: "A crumpled slip of paper stamped with the guard's seal." +value: 0 +stackable: false +``` + +--- + +## Firemaking + +```yaml +id: logs +name: logs +fire_level: 1 # firemaking level required +fire_xp: 40 # XP for burning/stoking +burn_ticks: 24 # how long the fire burns +``` + +## Food / Healing + +```yaml +id: bread +name: bread +color: "222" +description: "A fresh loaf of bread, still warm from the oven." +value: 5 +heal_value: 5 # positive = heal, negative = damage +eat_message: "You eat the bread. Warm and satisfying." +``` + +## MadeFrom — 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. + +```yaml +id: bread_dough +name: bread dough +color: "222" +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: + +```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 +``` + +See also `recipes.md` for station-based recipes. diff --git a/building_guide/mobs.md b/building_guide/mobs.md new file mode 100644 index 0000000..c1f9332 --- /dev/null +++ b/building_guide/mobs.md @@ -0,0 +1,221 @@ +## Mobs + +Basic combat mob with per-type defense bonuses: +```yaml +id: "man" +name: "man" +description: "A shabby-looking man." +attack: 1 +strength: 1 +defense: 1 +hp: 7 +speed: 5 +aggressive: false +respawn_ticks: 30 +attack_type: crush # stab/slash/crush/ranged/science (default: crush) +stab_defense: 0 # per-type defense bonuses +slash_defense: 0 +crush_defense: 0 +science_defense: 0 +ranged_defense: 0 +drops: + remains: "bones" # always dropped on death + loot: + - item_id: "credits" + weight: 98 + quantity: 10 + - item_id: "credits" + weight: 2 + quantity: 150 +idle_descriptions: + - "scribbles something in a small notebook" + - "gazes skyward at the clouds" +combat_descriptions: + - "is engaged in a fight to the death with %s" +``` + +### Aggressive Mobs + +Mobs with `aggressive: true` auto-attack players entering their room (1-tick delay). Only aggros if the player's combat level is at most double the mob's combat level. Checked on room enter and login. + +```yaml +id: goblin +name: goblin +aggressive: true # attacks players on sight +attack: 1 +strength: 3 +defense: 3 +hp: 12 +attack_type: slash +attack_bonus: 3 # equipment-equivalent attack bonus for mob's attack roll +strength_bonus: 2 # equipment-equivalent strength bonus for mob's max hit +stab_defense: 4 +slash_defense: 3 +crush_defense: -2 # weak to crush — negative defense is valid +science_defense: 0 +ranged_defense: 3 +``` + +### Mob Attack/Defense Fields + +| Field | Description | +|---|---| +| `attack` | Attack level (base accuracy) | +| `strength` | Strength level (base max hit) | +| `defense` | Defense level (base defense) | +| `hp` | Hit points | +| `ranged` | Ranged level (for ranged/science mobs) | +| `science` | Science level | +| `attack_bonus` | Equipment-equivalent attack bonus (added to attack roll) | +| `strength_bonus` | Equipment-equivalent strength bonus (added to max hit) | +| `attack_type` | Determines which player defense is used against this mob | +| `stab_defense` through `ranged_defense` | Per-type defense bonuses | +| `weakness` | Elemental weakness (solar/hydro/eco/bio) — for future Science combat | +| `size` | Mob size for safespot blocking: small/medium/large/massive (default: medium) | + +### Protected/Unique Mobs + +Mob with talk behavior — can be talked to via `talk:` inline config: +```yaml +id: "guard" +name: "Guard" +talk: # inline talk config (no separate behavior file) + nodes: + start: + message: "\"Halt! Who goes there?\"" + options: + - text: "\"Just passing through.\"" + end: true +unique: true # displays as "Guard" not "a guard" +protected: true # cannot be attacked +attack: 5 +strength: 5 +defense: 5 +hp: 30 +speed: 5 +aggressive: false +respawn_ticks: 60 +attack_type: slash +attack_bonus: 8 +strength_bonus: 6 +stab_defense: 12 +slash_defense: 15 +crush_defense: 10 +science_defense: -5 +ranged_defense: 12 +idle_descriptions: + - "scans the area with a watchful eye" + - "adjusts the grip on his weapon" +``` + +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). + +### Stealable Mobs + +Mobs can be stealable via the `steal` command. On failure, the mob turns hostile and attacks. + +```yaml +id: farmer +name: Farmer +steal_table: farmer_steal # Drop table for steal loot +steal_level: 10 # Required thieving level +steal_xp: 15 # XP per successful steal +steal_speed: 4 # Ticks per steal attempt +``` + +| Field | Description | +|---|---| +| `steal_table` | Drop table ID for loot when stealing | +| `steal_level` | Required thieving level | +| `steal_xp` | XP awarded per successful steal | +| `steal_speed` | Ticks per steal attempt (base wait) | + +### Assassin Mobs + +Assassin (Slayer) mobs restrict combat by assassin level and introduce finishing blows and protective equipment: + +```yaml +id: slug +name: slug +assassin_level: 1 # required assassin level to attack +finishing_blow: salt # item required to kill (mob stays at 1 HP otherwise) +attack: 3 +strength: 3 +defense: 1 +hp: 15 +speed: 6 +drops: + remains: slug_mucus + loot: + - item_id: credits + weight: 100 + quantity: 15 +``` + +Damage-without mobs deal 1.5x damage unless the player has the specified item equipped: +```yaml +id: drone +name: drone +assassin_level: 15 +damage_without: insulated_gloves # 1.5x damage without this item equipped +attack: 15 +strength: 14 +defense: 12 +hp: 45 +speed: 4 +aggressive: true +``` + +| Field | Description | +|---|---| +| `assassin_level` | Required assassin skill level to attack this mob | +| `finishing_blow` | Item ID required to kill this mob (mob stays at 1 HP until used via `use on `) | +| `damage_without` | Item ID for protection — mob deals 1.5x damage if player doesn't have it equipped | + +Mobs without these fields work normally (assassin_level defaults to 0, finishing_blow and damage_without default to empty). + +### Task Mobs (Worksites) + +A task mob is a non-violent "worksite" — build a solar panel, survey flora, prospect a rock — that reuses the entire combat engine. Set `kind: task`. Mechanically its `hp` is a hidden work pool that drains to 0 to **complete** the work; the player sees a progress bar filling toward 100%. A task mob **never attacks back** — the danger (if any) comes from a room `hazard:` (see `hazards.md`). Use the `work` command (or `attack`) on it. + +```yaml +id: solar_panel_frame +name: solar panel frame +kind: task # "" / "combat" (default) or "task" +verb: assemble # flavor verb shown in messages (default: "work on") +progress_noun: assembly # flavor noun in the progress line (default: "work") +complete_message: "You bolt the final panel into place and the array hums to life!" +hp: 40 # units of work to complete (NOT a health bar) +defense: 10 # base difficulty +speed: 5 +aggressive: false # task mobs do not fight; this is ignored +respawn_ticks: 40 # the worksite replenishes after this many ticks +attack_type: crush +# Per-type defenses become METHOD EFFICIENCY: low = that approach works well. +crush_defense: 0 # hammering panels in: efficient +stab_defense: 40 # stabbing a frame: useless +slash_defense: 40 +ranged_defense: 10 # a rivet gun / scanner (ranged weapon): works +science_defense: 10 # an analysis deck (science weapon): works +drops: # the "payment" for the labour — same drop system as combat + loot: + - item_id: credits + weight: 95 + quantity: 40 + - item_id: coal + weight: 5 + quantity: 1 +``` + +Key points: + +- **All combat styles work.** Melee, ranged (consumes ammo as "supplies") and science decks (consume junk) all apply progress. The per-type `*_defense` values steer which method is *efficient* without forbidding any — a rock might have low `crush_defense` but high `stab_defense`. +- **XP and buffs are identical to combat** — driven by your attack style (accurate/aggressive/defensive/balanced) and split into the combat skills. This is a non-violent way to train Attack/Strength/Defense/Ranged/Science/Hitpoints. +- **No special equipment.** Task mobs are designed around the weapons and bonuses you already have. There is no separate "tool" equipment set. +- **Progress decays exactly like inverted mob regen.** A task you stop working on slowly loses progress over time (the same regen that heals combat mobs, displayed inverted). +- **One worker at a time**, the same 1-v-1 lock as combat. + +--- + + diff --git a/building_guide/objects.md b/building_guide/objects.md new file mode 100644 index 0000000..427968a --- /dev/null +++ b/building_guide/objects.md @@ -0,0 +1,392 @@ +## 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. + +Gathering object (mining): +```yaml +id: copper_rock +name: copper rock +color: "178" # xterm-256 color index (0-255) +gather: + skill: mining + level: 1 + xp: 17 + base_wait: 8 + tools: + - pickaxe + success: + 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 + weight: 90 + depletes: true + message: "You manage to mine some {178}copper ore{/}." + - table: gem_table + weight: 10 + depletes: false + 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 +id: oak_tree +name: oak tree +color: "113" +gather: + skill: woodcutting + level: 15 + xp: 37 + base_wait: 6 + 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 + weight: 100 + depletes: false + message: "You get some {113}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:196,82`). In ANSI mode, extended colors downgrade to the nearest +ANSI color. + +Object interaction (gate, lever — uses `use_interactions:` key): +```yaml +id: iron_gate +name: iron gate +hidden: true +description: "A heavy iron gate set into the north wall." +use_interactions: + - condition: + flag: gate_open + value: true + not: true + message: "You push the heavy iron gate open." + action: + set_flags: + gate_open: true +``` + +Decorative object (no behavior keys — just a name/description): +```yaml +id: lumby_fountain +name: town fountain +description: "Clear water sparkles in the sunlight." +``` + +Talk object (NPC conversations — uses `talk:` key): +```yaml +id: tool_shed +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.\"" + end: true +``` + +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. + +--- + +## Use Interactions + +Objects can define `use_interactions` to handle when a player uses a specific item on the object. Each interaction can check conditions, show a message, and execute actions — using the same condition and action primitives as the talk system. + +Entries are checked top-to-bottom; the first match with a passing condition wins. + +Simple message (no action): +```yaml +id: anvil +name: anvil +use_interactions: + - 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." +``` + +Puzzle interaction (take item, set flag): +```yaml +id: crystal_slot +name: crystal slot +use_interactions: + - item_id: crystal_key + condition: + 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." + action: + take_item: crystal_key + set_flags: + crystal_inserted: true +``` + +Quest item exchange: +```yaml +use_interactions: + - item_id: ancient_scroll + condition: + player_flag: quest_started + message: "You place the scroll on the pedestal. The door rumbles open!" + action: + take_item: ancient_scroll + set_player_flags: + quest_complete: true + set_flags: + temple_door_open: true +``` + +### UseInteraction Fields + +| Field | Type | Description | +| ----------- | ---------- | ------------------------------------------------ | +| `item_id` | string | Item ID that triggers this interaction | +| `condition` | Condition | Optional condition (same as exits/talk/use_interactions) | +| `message` | string | Message shown to the player | +| `action` | NodeAction | Optional actions (same as talk node actions) | + +### Available Actions (same as talk) + +| Field | Type | Description | +| ------------------ | -------------- | ---------------------------------- | +| `set_flags` | map[string]any | Set world flags (shared) | +| `set_player_flags` | map[string]any | Set player flags (per-character) | +| `give_item` | string | Give an item to inventory | +| `take_item` | string | Remove an item from inventory | +| `teleport` | int | Move player to a room ID | +| `heal` | int | Restore hitpoints | + +### Available Conditions (same as exits/talk) + +| Field | Description | +| ------------- | -------------------------------- | +| `flag` | World flag check | +| `player_flag` | Per-character flag check | +| `has_item` | Inventory item check | +| `value` | Expected value for flag checks | +| `not` | Invert the condition | +| `all_of` | All sub-conditions must pass | +| `any_of` | Any sub-condition must pass | + +--- + +## Stealable Objects + +Objects can be stealable via the `steal` command. These use the same drop table system as mobs and searches. + +```yaml +id: market_stall +name: Market Stall +description: "A wooden stall piled with food and sundries." +steal_table: market_stall_steal +steal_level: 5 +steal_xp: 12 +steal_speed: 5 +guard_mob: guard +``` + +| Field | Description | +| ------------- | ------------------------------------------------ | +| `steal_table` | Drop table ID for loot when stealing | +| `steal_level` | Required thieving level | +| `steal_xp` | XP awarded per successful steal | +| `steal_speed` | Ticks per steal attempt (base wait) | +| `guard_mob` | Mob def ID that guards this object (watches it) | + +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. + +--- + +## Safespots + +Safespot objects provide cover that blocks melee attacks. Players use `hide ` to +crouch behind them and attack with ranged or science weapons. Melee attacks force the player +out of cover. + +```yaml +id: rock_outcrop +name: rock outcrop +color: "248" +hidden: true +inroom_description: "A jagged rock outcrop juts from the floor." +description: "A large, jagged rock formation providing natural cover." +safespot: + tier: 2 + max_block_size: large + max_occupants: 3 + unsafe_chance: 0.008 + decay_ticks: 500 + decay_chance: 0.01 + respawn_on_hide: false + respawn_ticks: 300 + levels: + - 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!" + - 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 `. + +### 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 | + +### SafespotLevel 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 | + +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. + +### Three Safespot Patterns + +The combination of `respawn_on_hide` and `respawn_ticks` creates three distinct use cases: + +**1. Standard Safespot (always available)** + +```yaml +safespot: + respawn_on_hide: true + respawn_ticks: 0 + levels: + - message: "You crouch behind the rock formation, 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)** + +```yaml +safespot: + respawn_on_hide: false + respawn_ticks: 300 + levels: + - 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!" + - message: "Only a few jagged rocks remain." + 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)** + +```yaml +safespot: + respawn_on_hide: false + respawn_ticks: 0 + levels: + - message: "The barricade offers solid cover." + degrade_message: "The barricade splinters under the assault!" + - message: "The barricade is splintering badly." + 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 + +Mobs must have a `size` field for safespot blocking to work: + +```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 + +- Melee mobs (attack_type: stab/slash/crush) cannot initiate aggro or land hits on + safespotted players if the mob's size is within the safespot's `max_block_size`. +- Ranged and science mobs (attack_type: ranged/science) ignore safespots entirely. +- 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. +- Hiding out of combat takes 1 tick. + +### Option: safespot_alert + +Players can customize the message shown when forced out of a safespot: + + option safespot_alert "{196 bold}** DANGER **{/} Cover blown!" +Default: `"{196 bold}** Your safespot has been compromised! **{/}"` + +--- + diff --git a/building_guide/quests.md b/building_guide/quests.md new file mode 100644 index 0000000..b5f18fa --- /dev/null +++ b/building_guide/quests.md @@ -0,0 +1,103 @@ +## Complete Quest Example: "Clear the Rats" + +### 1. Quest giver mob (`data/mobs/quest_giver.yaml`) +```yaml +id: "quest_giver" +name: "Elder" +unique: true +protected: true +talk: + nodes: + start: + message: "\"Rats! Rats everywhere in the cellar. Clear them out and I'll reward you.\"" + options: + - text: "\"I'll handle it.\"" + goto: accept_quest + condition: + player_flag: rat_quest + not: true + - text: "\"I killed the rats.\"" + goto: turn_in + condition: + player_flag: rat_quest + value: started + - text: "\"Goodbye.\"" + end: true + + accept_quest: + message: "\"Good lad. The cellar is west of here. Come back when they're dead.\"" + action: + set_player_flags: + rat_quest: started + options: + - text: "\"On my way.\"" + end: true + + turn_in: + message: "\"You did it! The village owes you a debt. Here — take this.\"" + action: + give_item: rusty_sword + set_player_flags: + rat_quest: complete + heal: 10 + options: + - text: "\"Thanks!\"" + end: true +attack: 1 +strength: 1 +defense: 1 +hp: 20 +speed: 5 +aggressive: false +idle_descriptions: + - "mutters about the rat infestation" +``` + +Talk configs are inline on the mob under the `talk:` key. No separate behavior file needed. + +### 2. Rat mobs (`data/mobs/rat.yaml`) +```yaml +id: "rat" +name: "giant rat" +attack: 2 +strength: 1 +defense: 1 +hp: 3 +speed: 4 +aggressive: true +respawn_ticks: 60 +drops: + remains: "rat bones" +``` + +### 3. Cellar room (`data/rooms/20.yaml`) +```yaml +id: 20 +name: "Cellar" +description: "A damp, dark cellar. The floor scuttles with movement." +exits: + east: 1 +mobs: + - "rat" + - "rat" + - "rat" + - "rat" + - "rat" +``` + +### 4. Room 1 with quest giver (`data/rooms/1.yaml`) +```yaml +id: 1 +name: "Town Square" +description: "Cobblestone paths lead in all directions. The Elder stands near the fountain." +exits: + west: 20 + north: 2 +mobs: + - "quest_giver" +objects: + - id: lumby_fountain +``` + +--- + diff --git a/building_guide/recipes.md b/building_guide/recipes.md new file mode 100644 index 0000000..450ded9 --- /dev/null +++ b/building_guide/recipes.md @@ -0,0 +1,277 @@ +# The House of Icarus - World Building Guide + +## 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. + +### Craft YAML Reference + +| Field | Type | Description | +| -------------- | -------------- | ----------------------------------------------- | +| `type` | string | Craft type (pharmacy, smithing, cooking, smelting, crafting, fletching, construction, clean, or "" for skill-less) | +| `skill` | string | Skill checked (defaults to type if omitted) | +| `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) | +| `consume` | []ConsumeEntry | Ingredients consumed (see below) | +| `output_qty` | int | Quantity produced (default 1, for stackables) | +| `fail` | string | ItemID produced on failure (optional) | +| `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. + +### Message Variables + +All message fields (`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, e.g. "stim potion") | +| `%i1` | 1st consume entry's matched item (colored) | +| `%i2` | 2nd consume entry's matched item (colored) | +| `%b1` | 1st byproduct item name (colored, success only) | +| `%b2` | 2nd byproduct item name (colored, success only) | + +Numbering follows YAML consume/byproduct order. If a consume 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 (`{196}text{/}`) which are expanded after variable substitution. + +### Message Defaults + +If a message field is omitted from the YAML, the game uses a skill-level default based on the craft `type`: + +| Type | Default `message` | Default `start_message` | +|------|-------------------|------------------------| +| `cooking` | `"Cooked to perfection. %n looks great!"` | `"You start cooking %i1."` | +| `smelting` | `"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."` | +| (unset) | `"You produce %n."` | `"You start %i1."` | + +Any of these can be overridden per-item by setting the field in YAML. For skills that never fail (smithing, crafting, fletching, pharmacy, construction), `fail_message` defaults to empty — no message is shown on failure. + +### 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. + +```yaml +craft: + type: "" + wait: 6 + consume: + - items: [map_piece_1] + quantity: 1 + - items: [map_piece_2] + quantity: 1 + - items: [map_piece_3] + quantity: 1 + steps: + - tick: 1 + message: "You try to puzzle how the pieces fit together." + - tick: 3 + message: "Aha, this edge lines up here!" + message: "You assemble the map!" +``` + +### ConsumeEntry — Multi-Item Ingredient Slots + +Each consume entry defines an ingredient slot with multiple valid items. The first matching item found in the player's inventory is consumed. + +```yaml +consume: + - 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 +consume: + - 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 +craft: + type: pharmacy + skill: pharmacy + level: 3 + xp: 25 + wait: 4 + consume: + - items: [guam_potion_unf] + quantity: 1 + - items: [eye_of_newt] + quantity: 1 + message: "You mix a %n." # %n expands to "stim potion" colored +``` + +### Station-Based Production + +```yaml +# data/items/materials/bronze_bar.yaml +craft: + type: smelting + skill: smithing + level: 1 + xp: 6 + wait: 4 + station: [furnace] + consume: + - items: [copper_ore] + quantity: 1 + - items: [tin_ore] + quantity: 1 + message: "You smelt a %n." # default is "You remove a white hot %n!" + # overridden here for simpler flavor +``` + +```yaml +# data/items/equipment/bronze_dagger.yaml +craft: + type: smithing + skill: smithing + level: 1 + xp: 12 + wait: 4 + station: [anvil] + consume: + - items: [bronze_bar] + quantity: 1 + # message omitted — uses default "You smith a %n." +``` + +For stackable outputs, use `output_qty`: + +```yaml +# data/items/ammo/bronze_nails.yaml +craft: + type: smithing + skill: smithing + level: 4 + xp: 12 + wait: 4 + station: [anvil] + consume: + - items: [bronze_bar] + quantity: 1 + output_qty: 15 + # message omitted — uses default "You smith a %n." +``` + +### Tool-Based Production + +```yaml +# data/items/ammo/arrow_shafts.yaml +craft: + type: fletching + level: 1 + xp: 5 + wait: 3 + tool: knife + consume: + - items: [logs, oak_logs, willow_logs] + quantity: 1 + 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 (Item-on-Item) + +Omit `type` (or leave it empty) for combinations with no skill check, no XP, and 100% success: + +```yaml +# data/items/consumables/bucket_of_water.yaml +craft: + wait: 0 + consume: + - items: [vial_of_water, jug_of_water] + quantity: 1 + byproducts: [empty_vial, empty_jug] + - items: [empty_bucket] + quantity: 1 + message: "You pour the %i1 into the %i2." # %i1 = water source, %i2 = bucket +``` + +### Multi-Piece Assembly (3+ items → 1) + +```yaml +# data/items/quest/ancient_map.yaml +craft: + wait: 0 + consume: + - items: [torn_page_1] + quantity: 1 + - items: [torn_page_2] + quantity: 1 + - items: [torn_page_3] + quantity: 1 + steps: + - tick: 1 + message: "You try to puzzle how the pieces fit together." + - tick: 3 + message: "Aha, this edge lines up here!" + message: "You assemble the %n." +``` + +### Clean Recipes + +Clean herb recipes use `type: clean` and are handled as background actions (one herb per cycle, directly mutating inventory): + +```yaml +# data/items/materials/guam.yaml (clean guam) +craft: + type: clean + skill: pharmacy + level: 3 + xp: 3 + wait: 2 + consume: + - items: [grimy_guam] + quantity: 1 + # 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 +id: bread +name: bread +color: "222" +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." +``` + +### Architecture + +The `CraftIndex` (`internal/game/craft_index.go`) is built once at startup from all item YAMLs that have a `craft:` block. It provides `O(1)` lookups by type, input item, and station. All crafting commands query the CraftIndex — no runtime file scanning. + +**Indexes:** +- `byType["pharmacy"]` → all pharmacy-craftable items (used by `mix`) +- `byInput["guam"]` → all items that consume 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 consume entries diff --git a/building_guide/rooms.md b/building_guide/rooms.md new file mode 100644 index 0000000..735a6b4 --- /dev/null +++ b/building_guide/rooms.md @@ -0,0 +1,209 @@ +## Rooms + +Minimal room: +```yaml +id: 1 +name: "Town Square" +description: "Cobblestone paths lead in all directions. A fountain gurgles peacefully." +exits: + north: 2 + west: 7 + east: 3 +``` + +### Inline Color Tags + +Room descriptions support inline color tags using `{spec}text{/}` syntax. Untagged text uses the `room_desc` color. + +```yaml +description: "On the table lies a {182 bold}mysterious vase{/} with a rose in it." +``` + +Tag spec format: `{<0-255> [bold] [dim] [underline]}text{/}` + +Gradients: `{g:196,82}gradient text{/}`. Multi-stop: `{g:45,39,59}three stops{/}`. + +### Exits — simple vs conditional + +Simple exit — always passable: +```yaml +exits: + north: 2 +``` + +Conditional exit — blocked until a world flag is set: +```yaml +exits: + north: + room: 11 + condition: + 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): +```yaml +exits: + east: + room: 12 + condition: + player_flag: has_vault_key + blocked_message: "The vault door is locked. You need a key." +``` + +Conditional exit with compound condition — requires both a world flag AND a player flag: +```yaml +exits: + north: + room: 20 + condition: + all_of: + - 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_flags` / `set_player_flags` are applied +only when the player actually moves through the exit (not when it's blocked): +```yaml +exits: + north: + room: 21 + condition: + player_flag: lined_up + set_player_flags: + boarded_shuttle: true # marks "left this area" on the way out +``` + +### Item Spawns — ground items that respawn + +```yaml +item_spawns: + - id: bronze_pickaxe + quantity: 1 + respawn_ticks: 30 # reappears 30 ticks (18 seconds) after being picked up + - id: copper_ore + quantity: 3 + respawn_ticks: 50 +``` + +### Mobs — NPCs placed in the room + +Simple string (no wandering): +```yaml +mobs: + - "newbie_trainer" + - "man" +``` + +With wander config per-instance: +```yaml +mobs: + - id: man + wander_interval: 10 # attempts to wander every 10 ticks + - id: man + wander_interval: 15 + 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. + +### Objects — interactive fixtures + +```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) +``` + +### On-enter scripts — messages and timed cutscenes when a player arrives + +Each `on_enter` step shows a `message`, optionally gated by a `condition`. Steps +whose condition fails are skipped. + +```yaml +on_enter: + - message: "The guard barks: \"State your business!\"" + condition: + player_flag: talked_to_guard + not: true # only the first visit + + - message: "The guard nods. \"Back again?\"" + condition: + player_flag: talked_to_guard # subsequent visits +``` + +**Timed cutscenes.** A step may also carry a `delay` (ticks to wait before it +fires) and/or set flags (`set_flags` / `set_player_flags`). If any surviving step +has a delay or sets a flag, the whole sequence runs as a scheduled cutscene; +plain message-only scripts still print instantly. + +```yaml +on_enter: + - condition: { player_flag: boarded, not: true } + delay: 5 + 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!\"" + set_player_flags: + lined_up: true # opens an exit, flips a description, etc. + - condition: { player_flag: boarded, not: true } + message: "The crowd forms a single-file line." +``` + +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 cutscene on a flag the sequence itself sets (above, `lined_up`) so it doesn't replay on a return visit. +- If a player disconnects mid-cutscene it resumes on reconnect, so they can't get stuck behind an exit the cutscene was meant to open. Plain `on_enter` messages never replay on login. + +### Conditional room descriptions + +A room can show a different description depending on the looking player's flags. +`descriptions` are checked top-to-bottom; the first whose condition passes wins, +otherwise the base `description` is used. + +```yaml +description: "A large, empty concrete pad in the middle of the ocean." +descriptions: + - condition: + player_flag: boarded + not: true + text: "A concrete pad swarming with a couple dozen anxious passengers." +``` + +### Hazardous rooms — environmental danger + +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`). + +```yaml +id: 99999181 +name: "Exposed Solar Array" +hazard: solar_radiation # ID of a data/hazards/.yaml definition +exits: + south: 99999180 +objects: + - id: rock_outcrop # a safespot object shields players from the hazard +mobs: + - solar_panel_frame # a task worksite (see mobs.md > Task Mobs) +``` + +- 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. + +--- + diff --git a/building_guide/search.md b/building_guide/search.md new file mode 100644 index 0000000..e6e2a14 --- /dev/null +++ b/building_guide/search.md @@ -0,0 +1,15 @@ +## The `search` Command + +Used to open searchable items in your inventory (bird's nests, etc.): +``` +Usage: search + +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 new file mode 100644 index 0000000..2eefcc4 --- /dev/null +++ b/building_guide/state.md @@ -0,0 +1,8 @@ +## State: World vs Player + +**World flags** (`set_flags`, checked with `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. + +**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. + +--- + diff --git a/building_guide/tips.md b/building_guide/tips.md new file mode 100644 index 0000000..307ea1e --- /dev/null +++ b/building_guide/tips.md @@ -0,0 +1,21 @@ +## Tips + +1. **Use player flags for quest progress, world flags for environmental state.** If a bridge is repaired, that's world state. If a player has read a sign, that's player state. + +2. **Conditions on enter scripts** make rooms feel alive. A guard who only barks the first time, a room that changes after a quest completes. + +3. **Hidden objects** keep room descriptions clean. Mention them in the room's description text instead of auto-listing them. + +4. **Drop tables** are shareable. The `gem_table` is used by copper rocks AND mob loot. Define once, reference everywhere. + +5. **Objects are for fixtures, mobs are for living things.** If it has HP and can die, it's a mob. If it's a rock, lever, door, or crafting station, it's an object. Both can have behaviors. + +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, ...}`. + +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. + +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. + +10. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms. diff --git a/building_guide/wandering_mobs.md b/building_guide/wandering_mobs.md new file mode 100644 index 0000000..8d49ed9 --- /dev/null +++ b/building_guide/wandering_mobs.md @@ -0,0 +1,20 @@ +## Wandering Mobs + +Mob wandering is configured per-instance in the room YAML, not on the mob definition: +```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 new file mode 100644 index 0000000..f1f91c5 --- /dev/null +++ b/building_guide/wandering_objects.md @@ -0,0 +1,39 @@ +## Wandering Objects + +Fishing spots that move between rooms: +```yaml +# data/objects/fishing_spot.yaml +id: fishing_spot +name: fishing spot +gather: + skill: fishing + level: 1 + xp: 10 + base_wait: 4 + 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 + 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. + +--- + -- cgit v1.2.3