diff options
| author | historia <[not public]> | 2026-06-10 04:44:31 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-10 04:44:31 -0400 |
| commit | 69d8757ad983697cdc9182fdcc1cdb612a77fb41 (patch) | |
| tree | 348c26d1f6defe3aef64f5aa9e7cefed0e764214 /WORLDBUILDING.md | |
| parent | a226d72e51eecb768b13600303f73483118d9104 (diff) | |
| download | thehouseoficarus-69d8757ad983697cdc9182fdcc1cdb612a77fb41.tar.gz | |
feat: yaml architecture for complex object interaction
Diffstat (limited to 'WORLDBUILDING.md')
| -rw-r--r-- | WORLDBUILDING.md | 807 |
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. |
