aboutsummaryrefslogtreecommitdiff
path: root/WORLDBUILDING.md
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-10 04:44:31 -0400
committerhistoria <[not public]>2026-06-10 04:44:31 -0400
commit69d8757ad983697cdc9182fdcc1cdb612a77fb41 (patch)
tree348c26d1f6defe3aef64f5aa9e7cefed0e764214 /WORLDBUILDING.md
parenta226d72e51eecb768b13600303f73483118d9104 (diff)
downloadthehouseoficarus-69d8757ad983697cdc9182fdcc1cdb612a77fb41.tar.gz
feat: yaml architecture for complex object interaction
Diffstat (limited to 'WORLDBUILDING.md')
-rw-r--r--WORLDBUILDING.md807
1 files changed, 807 insertions, 0 deletions
diff --git a/WORLDBUILDING.md b/WORLDBUILDING.md
new file mode 100644
index 0000000..a0ca168
--- /dev/null
+++ b/WORLDBUILDING.md
@@ -0,0 +1,807 @@
+# World Building Guide
+
+Third Collapse is data-driven. Everything — rooms, items, mobs, objects, behaviors — is defined in YAML files under `data/`. No code changes needed to build a world.
+
+## Quick Reference
+
+| What you want | Where to put it |
+|---|---|
+| A room | `data/rooms/<id>.yaml` |
+| An item (sword, ore, key) | `data/items/<id>.yaml` |
+| A mob (NPC, monster) | `data/mobs/<id>.yaml` |
+| An interactive object (rock, lever, door) | `data/objects/<id>.yaml` |
+| A behavior (mining, dialog, toggle) | `data/behaviors/<id>.yaml` |
+| A shared drop table | `data/drops/<id>.yaml` |
+
+## 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.
+
+---
+
+## 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
+```
+
+### 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
+ value: true
+ 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
+ value: true
+ 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
+ value: true
+ - player_flag: paid_toll
+ value: true
+ blocked_message: "The bridge is out, and the toll collector blocks the path."
+```
+
+### Spawns — ground items that respawn
+
+```yaml
+spawns:
+ - item_id: bronze_pickaxe
+ quantity: 1
+ respawn_ticks: 30 # reappears 30 ticks (18 seconds) after being picked up
+ - item_id: copper_ore
+ quantity: 3
+ respawn_ticks: 50
+```
+
+### Mobs — NPCs placed in the room
+
+```yaml
+mobs:
+ - "man" # spawns a "man" mob
+ - "man" # second "man" — duplicate IDs create multiple instances
+ - "guard" # unique named mob
+```
+
+### Objects — interactive fixtures
+
+```yaml
+objects:
+ - id: copper_rock # simple placement
+ - id: copper_rock # second instance
+ - id: fishing_spot
+ wander_rooms: [7, 8, 9] # moves between these rooms
+ wander_interval: 12 # every 12 ticks
+ - id: iron_gate # hidden object (see below)
+```
+
+### On-enter scripts — messages when a player arrives
+
+```yaml
+on_enter:
+ - message: "The guard barks: \"State your business!\""
+ condition:
+ player_flag: talked_to_guard
+ not: true # only first visit
+
+ - message: "The guard nods. \"Back again?\""
+ condition:
+ player_flag: talked_to_guard
+ value: true # subsequent visits
+```
+
+---
+
+## Items
+
+```yaml
+id: bronze_pickaxe
+name: bronze pickaxe
+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 or ranged
+stats: # optional — combat bonuses
+ attack_bonus: 2
+ strength_bonus: 1
+speed: 5 # ticks between attacks
+toolbelt: true # usable from toolbelt (doesn't need to be in inventory)
+tool_type: pickaxe # used by gather behaviors that require "tool: pickaxe"
+tool_speed: 2 # reduces gather wait time
+```
+
+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
+```
+
+---
+
+## Mobs
+
+Basic combat mob:
+```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
+wander_rooms: [1, 2, 3]
+wander_interval: 10
+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"
+```
+
+Mob with a behavior — can be talked to, toggled, etc:
+```yaml
+id: "guard"
+name: "Guard"
+behavior: guard_talk # links to data/behaviors/guard_talk.yaml
+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
+idle_descriptions:
+ - "scans the area with a watchful eye"
+ - "adjusts the grip on his weapon"
+```
+
+---
+
+## Objects
+
+Object defined — links to a behavior:
+```yaml
+id: copper_rock
+name: copper rock
+behavior: mine_copper
+```
+
+Hidden object — doesn't appear in room's object list, only discoverable via description or experimentation:
+```yaml
+id: iron_gate
+name: iron gate
+behavior: iron_gate_toggle
+hidden: true
+props:
+ description: "A heavy iron gate set into the north wall."
+```
+
+Decorative object (no behavior):
+```yaml
+id: lumby_fountain
+name: town fountain
+behavior: ""
+props:
+ description: "Clear water sparkles in the sunlight."
+```
+
+---
+
+## Behaviors
+
+### Gather (mining, fishing, woodcutting)
+
+```yaml
+id: mine_copper
+type: gather
+skill: mining
+level: 1
+base_wait: 8 # ticks between attempts
+tool: pickaxe # requires item with 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 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!"
+deplete_delay: 50 # ticks until rock respawns
+respawn_message: "You see more ore in the rock."
+respawn_broadcast: "A glint of copper catches your eye from some {name}."
+```
+
+Non-depleting gather (fishing):
+```yaml
+id: fish_trout
+type: gather
+skill: fishing
+level: 1
+base_wait: 4
+tool: fishing_rod
+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 trout!"
+```
+
+### Talk (dialog trees)
+
+Full conversation with conditions, actions, and player flag tracking:
+```yaml
+id: guard_talk
+type: talk
+nodes:
+ start:
+ message: "\"Halt! This area is restricted.\""
+ options:
+ - text: "\"What's behind that gate?\""
+ goto: about_gate
+ - text: "\"I have copper ore.\"" # only shows if player has ore
+ goto: trade_ore
+ condition:
+ has_item: copper_ore
+ - text: "\"I have a pass.\"" # only shows if player earned 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: # player-local: only this player
+ 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 # removes 1 copper ore
+ give_item: pass_stub # gives pass stub
+ set_player_flags:
+ got_pass: true # player now "has a pass"
+ options:
+ - text: "\"Thanks.\""
+ end: true
+
+ has_pass:
+ message: "\"Alright, I'll open the gate for you.\""
+ action:
+ set_flags: # WORLD flag: gate opens for everyone
+ 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 |
+
+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 # return to town
+```
+
+### Toggle (levers, switches, gates)
+
+Simple toggle that sets a world flag:
+```yaml
+id: iron_gate_toggle
+type: toggle
+message: "You push the heavy iron gate open."
+set_flags:
+ gate_open: true
+check: # only works when gate is closed
+ flag: gate_open
+ value: true
+ not: true
+```
+
+Lever that toggles between two states:
+```yaml
+id: bridge_lever
+type: toggle
+message: "You pull the lever. Mechanisms groan somewhere in the distance."
+set_flags:
+ bridge_extended: true
+check:
+ flag: bridge_extended
+ value: true
+ not: true
+```
+
+### Use (crafting stations)
+
+```yaml
+id: smelt_copper
+type: use
+message: "You place the ore in the furnace..."
+wait: 4 # ticks between crafts
+consume: # items consumed per craft
+ copper_ore: 1
+reward: # item produced
+ 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
+```
+
+---
+
+## Conditions Reference
+
+Conditions are used in talk options, exit gates, on-enter scripts, and toggle checks.
+
+### Simple conditions
+
+```yaml
+# Check a world flag
+condition:
+ flag: gate_open
+ value: true
+
+# Check a world flag is NOT set
+condition:
+ flag: gate_open
+ not: true
+
+# Check a player flag
+condition:
+ player_flag: finished_tutorial
+ value: true
+
+# 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
+```
+
+### Compound conditions
+
+All must pass:
+```yaml
+condition:
+ all_of:
+ - flag: gate_open
+ value: true
+ - 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
+ value: true
+```
+
+Nested compounds:
+```yaml
+condition:
+ all_of:
+ - player_flag: quest_started
+ value: true
+ - any_of:
+ - has_item: wolf_pelt
+ - has_item: bear_pelt
+```
+
+---
+
+## 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
+```
+
+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
+```
+
+---
+
+## 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
+behavior: button_toggle
+hidden: true
+```
+
+**Button behavior** (`data/behaviors/button_toggle.yaml`):
+```yaml
+id: button_toggle
+type: toggle
+message: "You press the stone button. You hear grinding stone in the distance."
+set_flags:
+ secret_door_open: true # WORLD flag
+check:
+ flag: secret_door_open
+ not: true # only works when door is closed
+```
+
+**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).
+
+---
+
+## Complete Quest Example: "Clear the Rats"
+
+### 1. Quest giver mob (`data/mobs/quest_giver.yaml`)
+```yaml
+id: "quest_giver"
+name: "Elder"
+behavior: rat_quest_talk
+unique: true
+protected: true
+attack: 1
+strength: 1
+defense: 1
+hp: 20
+speed: 5
+aggressive: false
+idle_descriptions:
+ - "mutters about the rat infestation"
+```
+
+### 2. Dialog behavior (`data/behaviors/rat_quest_talk.yaml`)
+```yaml
+id: rat_quest_talk
+type: 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
+```
+
+### 3. 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"
+```
+
+### 4. 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"
+```
+
+### 5. 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
+```
+
+---
+
+## Wandering Objects
+
+Fishing spots that move between rooms:
+```yaml
+# data/objects/fishing_spot.yaml
+id: fishing_spot
+name: fishing spot
+behavior: fish_trout
+```
+
+```yaml
+# In rooms 7, 8, and 9:
+objects:
+ - id: fishing_spot
+ wander_rooms: [7, 8, 9]
+ wander_interval: 12
+```
+
+Players gathering from a wandering object are silently interrupted when it moves.
+
+---
+
+## Wandering Mobs
+
+```yaml
+# mobs that roam between rooms:
+wander_rooms: [1, 2, 3, 4]
+wander_interval: 10 # moves every 10 ticks
+```
+
+Mobs stop wandering while in combat. Dead mobs respawn at their home room after `respawn_ticks`.
+
+---
+
+## 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
+behavior: secret_toggle
+hidden: true
+props:
+ description: "A cleverly concealed lever behind a loose stone."
+```
+
+Room description hints at it:
+```yaml
+description: "A dusty corridor. One of the wall stones looks slightly out of place."
+```
+
+---
+
+## 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 `behavior` 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.
+
+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.