From 389e307f205f9b7edf604fb9f86e1038ead736ef Mon Sep 17 00:00:00 2001
From: historia <[not public]>
Date: Tue, 16 Jun 2026 23:37:50 -0400
Subject: feat: major xterm256 color overhaul, see worldbuilding docs.
---
.gitignore | 2 +-
AGENTS.md | 40 +-
Makefile | 2 +-
README.md | 41 +-
TODO.md | 2 +-
WORLDBUILDING.md | 975 ---------------------------------
cmd/mud/main.go | 9 +-
config.yaml | 53 +-
data/behaviors/chop_magic.yaml | 2 +-
data/behaviors/chop_mahogany.yaml | 2 +-
data/behaviors/chop_maple.yaml | 2 +-
data/behaviors/chop_oak.yaml | 2 +-
data/behaviors/chop_redwood.yaml | 2 +-
data/behaviors/chop_teak.yaml | 2 +-
data/behaviors/chop_tree.yaml | 2 +-
data/behaviors/chop_willow.yaml | 2 +-
data/behaviors/chop_yew.yaml | 2 +-
data/behaviors/fish_angler.yaml | 2 +-
data/behaviors/fish_bait.yaml | 6 +-
data/behaviors/fish_cage.yaml | 2 +-
data/behaviors/fish_fly.yaml | 6 +-
data/behaviors/fish_harpoon.yaml | 6 +-
data/behaviors/fish_net.yaml | 10 +-
data/behaviors/fish_sacred_eel.yaml | 2 +-
data/behaviors/fish_swamp.yaml | 4 +-
data/behaviors/mine_adamantite.yaml | 2 +-
data/behaviors/mine_clay.yaml | 2 +-
data/behaviors/mine_coal.yaml | 2 +-
data/behaviors/mine_copper.yaml | 2 +-
data/behaviors/mine_gold.yaml | 2 +-
data/behaviors/mine_iron.yaml | 2 +-
data/behaviors/mine_mithril.yaml | 2 +-
data/behaviors/mine_runite.yaml | 2 +-
data/behaviors/mine_scrap.yaml | 2 +-
data/behaviors/mine_silver.yaml | 2 +-
data/behaviors/mine_tin.yaml | 2 +-
data/help/color.yaml | 82 +--
data/help/option.yaml | 3 +-
data/help/prompt.yaml | 26 +-
data/items/adamant_axe.yaml | 2 +-
data/items/adamant_pickaxe.yaml | 2 +-
data/items/adamantite_ore.yaml | 2 +-
data/items/anchovies.yaml | 2 +-
data/items/ashes.yaml | 2 +-
data/items/big_fishing_net.yaml | 2 +-
data/items/birds_nest.yaml | 2 +-
data/items/black_axe.yaml | 2 +-
data/items/black_pickaxe.yaml | 2 +-
data/items/bones.yaml | 2 +-
data/items/bread.yaml | 2 +-
data/items/bread_dough.yaml | 2 +-
data/items/bronze_axe.yaml | 2 +-
data/items/bronze_pickaxe.yaml | 2 +-
data/items/bronze_sword.yaml | 2 +-
data/items/bucket_of_water.yaml | 2 +-
data/items/burnt_fish.yaml | 2 +-
data/items/burnt_meat.yaml | 2 +-
data/items/christmas_cracker.yaml | 2 +-
data/items/clay.yaml | 2 +-
data/items/coal.yaml | 2 +-
data/items/copper_ore.yaml | 2 +-
data/items/credits.yaml | 2 +-
data/items/dragon_axe.yaml | 2 +-
data/items/dragon_pickaxe.yaml | 2 +-
data/items/feather.yaml | 2 +-
data/items/firesteel.yaml | 2 +-
data/items/fishing_bait.yaml | 2 +-
data/items/fishing_rod.yaml | 2 +-
data/items/fly_fishing_rod.yaml | 2 +-
data/items/gold_ore.yaml | 2 +-
data/items/harpoon.yaml | 2 +-
data/items/herring.yaml | 2 +-
data/items/iron_axe.yaml | 2 +-
data/items/iron_ore.yaml | 2 +-
data/items/iron_pickaxe.yaml | 2 +-
data/items/lighter.yaml | 2 +-
data/items/lobster_pot.yaml | 2 +-
data/items/logs.yaml | 2 +-
data/items/magic_logs.yaml | 2 +-
data/items/mahogany_logs.yaml | 2 +-
data/items/maple_logs.yaml | 2 +-
data/items/matches.yaml | 2 +-
data/items/mithril_axe.yaml | 2 +-
data/items/mithril_ore.yaml | 2 +-
data/items/mithril_pickaxe.yaml | 2 +-
data/items/oak_logs.yaml | 2 +-
data/items/party_hat_blue.yaml | 2 +-
data/items/party_hat_green.yaml | 2 +-
data/items/party_hat_pink.yaml | 2 +-
data/items/party_hat_red.yaml | 2 +-
data/items/party_hat_white.yaml | 2 +-
data/items/party_hat_yellow.yaml | 2 +-
data/items/pass_stub.yaml | 2 +-
data/items/pastry_dough.yaml | 2 +-
data/items/pizza_dough.yaml | 2 +-
data/items/pot_of_flour.yaml | 2 +-
data/items/raw_anchovies.yaml | 2 +-
data/items/raw_anglerfish.yaml | 2 +-
data/items/raw_bass.yaml | 2 +-
data/items/raw_cave_eel.yaml | 2 +-
data/items/raw_cod.yaml | 2 +-
data/items/raw_herring.yaml | 2 +-
data/items/raw_lobster.yaml | 2 +-
data/items/raw_mackerel.yaml | 2 +-
data/items/raw_pike.yaml | 2 +-
data/items/raw_rainbow_fish.yaml | 2 +-
data/items/raw_salmon.yaml | 2 +-
data/items/raw_sardine.yaml | 2 +-
data/items/raw_shark.yaml | 2 +-
data/items/raw_shrimps.yaml | 2 +-
data/items/raw_slimy_eel.yaml | 2 +-
data/items/raw_swordfish.yaml | 2 +-
data/items/raw_trout.yaml | 2 +-
data/items/raw_tuna.yaml | 2 +-
data/items/redwood_logs.yaml | 2 +-
data/items/rune_axe.yaml | 2 +-
data/items/rune_pickaxe.yaml | 2 +-
data/items/runite_ore.yaml | 2 +-
data/items/sacred_eel.yaml | 2 +-
data/items/salmon.yaml | 2 +-
data/items/sardine.yaml | 2 +-
data/items/scrap_metal.yaml | 2 +-
data/items/shrimps.yaml | 2 +-
data/items/silver_ore.yaml | 2 +-
data/items/small_fishing_net.yaml | 2 +-
data/items/steel_axe.yaml | 2 +-
data/items/steel_pickaxe.yaml | 2 +-
data/items/teak_logs.yaml | 2 +-
data/items/tin_ore.yaml | 2 +-
data/items/trout.yaml | 2 +-
data/items/uncut_diamond.yaml | 2 +-
data/items/uncut_emerald.yaml | 2 +-
data/items/uncut_ruby.yaml | 2 +-
data/items/uncut_sapphire.yaml | 2 +-
data/items/willow_logs.yaml | 2 +-
data/items/yew_logs.yaml | 2 +-
data/objects/adamantite_rock.yaml | 2 +-
data/objects/anglerfish_spot.yaml | 2 +-
data/objects/bait_fishing_spot.yaml | 2 +-
data/objects/cage_fishing_spot.yaml | 2 +-
data/objects/clay_rock.yaml | 2 +-
data/objects/coal_rock.yaml | 2 +-
data/objects/cooking_range.yaml | 2 +-
data/objects/copper_rock.yaml | 2 +-
data/objects/fire.yaml | 2 +-
data/objects/fly_fishing_spot.yaml | 2 +-
data/objects/gold_rock.yaml | 2 +-
data/objects/harpoon_fishing_spot.yaml | 2 +-
data/objects/iron_gate.yaml | 2 +-
data/objects/iron_rock.yaml | 2 +-
data/objects/lumby_fountain.yaml | 2 +-
data/objects/magic_tree.yaml | 2 +-
data/objects/mahogany_tree.yaml | 2 +-
data/objects/maple_tree.yaml | 2 +-
data/objects/mithril_rock.yaml | 2 +-
data/objects/net_fishing_spot.yaml | 2 +-
data/objects/oak_tree.yaml | 2 +-
data/objects/redwood_tree.yaml | 2 +-
data/objects/runite_rock.yaml | 2 +-
data/objects/sacred_eel_spot.yaml | 2 +-
data/objects/scrap_pile.yaml | 2 +-
data/objects/silver_rock.yaml | 2 +-
data/objects/swamp_fishing_spot.yaml | 2 +-
data/objects/teak_tree.yaml | 2 +-
data/objects/tin_rock.yaml | 2 +-
data/objects/tree.yaml | 2 +-
data/objects/willow_tree.yaml | 2 +-
data/objects/yew_tree.yaml | 2 +-
go.mod | 2 +-
internal/color/color.go | 475 ++++++++++++----
internal/config/config.go | 69 ++-
internal/game/action.go | 12 +-
internal/game/action_burn.go | 66 +--
internal/game/action_cook.go | 8 +-
internal/game/action_gather.go | 15 +-
internal/game/action_room.go | 4 +-
internal/game/action_search.go | 19 +-
internal/game/action_talk.go | 12 +-
internal/game/action_toggle.go | 8 +-
internal/game/action_use.go | 19 +-
internal/game/cmd_alias.go | 2 +-
internal/game/cmd_attack.go | 22 +-
internal/game/cmd_color.go | 39 +-
internal/game/cmd_colortable.go | 151 +++++
internal/game/cmd_cook.go | 6 +-
internal/game/cmd_description.go | 4 +-
internal/game/cmd_drop.go | 12 +-
internal/game/cmd_eat.go | 10 +-
internal/game/cmd_equipment.go | 4 +-
internal/game/cmd_get.go | 26 +-
internal/game/cmd_inventory.go | 9 +-
internal/game/cmd_look.go | 24 +-
internal/game/cmd_map.go | 4 +-
internal/game/cmd_move.go | 6 +-
internal/game/cmd_option.go | 35 +-
internal/game/cmd_prompt.go | 33 ++
internal/game/cmd_queued.go | 4 +-
internal/game/cmd_quit.go | 8 +-
internal/game/cmd_remove.go | 11 +-
internal/game/cmd_say.go | 8 +-
internal/game/cmd_score.go | 26 +-
internal/game/cmd_search.go | 4 +-
internal/game/cmd_style.go | 4 +-
internal/game/cmd_use.go | 6 +-
internal/game/cmd_walk.go | 6 +-
internal/game/cmd_wear.go | 12 +-
internal/game/color.go | 24 +-
internal/game/game.go | 36 +-
internal/game/help.go | 8 +-
internal/game/login_account.go | 31 +-
internal/game/login_char.go | 11 +-
internal/game/map.go | 2 +-
internal/game/map_test.go | 2 +-
internal/game/prompt.go | 49 +-
internal/game/table.go | 18 +-
internal/game/tick.go | 30 +-
internal/game/types.go | 2 +-
internal/game/utils.go | 6 +-
internal/net/server.go | 137 ++++-
internal/net/terminal.html | 2 +-
internal/object/item.go | 2 +-
internal/player/account.go | 1 +
internal/player/player.go | 15 +-
internal/player/store.go | 2 +-
internal/world/mob.go | 2 +-
internal/world/room.go | 2 +-
worldbuilding_guide/README.md | 2 +-
worldbuilding_guide/behaviors.md | 10 +-
worldbuilding_guide/items.md | 17 +-
worldbuilding_guide/objects.md | 4 +
worldbuilding_guide/recipes.md | 32 +-
worldbuilding_guide/rooms.md | 12 +
worldbuilding_guide/toggles.md | 18 -
233 files changed, 1475 insertions(+), 1758 deletions(-)
delete mode 100644 WORLDBUILDING.md
create mode 100644 internal/game/cmd_colortable.go
create mode 100644 internal/game/cmd_prompt.go
delete mode 100644 worldbuilding_guide/toggles.md
diff --git a/.gitignore b/.gitignore
index 3ab0ef3..df654ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@
!/data/players/accounts/.gitkeep
/data/players/characters/*
!/data/players/characters/.gitkeep
-tc
+thoi
.cache/
.npm/
.local/
diff --git a/AGENTS.md b/AGENTS.md
index 6dc559b..7dc4c47 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,6 +1,6 @@
# AGENTS.md
-Third Collapse — a Runescape-like sci-fi MUD written in Go. Data-driven via YAML files. No database, no ORM.
+The House of Icarus — a Runescape-like sci-fi MUD written in Go. Data-driven via YAML files. No database, no ORM.
Set on a terraformed asteroid where technology has regressed. "Science" replaces Prayer, "Technology" replaces Magic, Scavenging replaces Runecrafting.
@@ -37,7 +37,7 @@ internal/
combat/ Combat state tracking, combat lock, OSRS-style combat formulas
object/ ItemDef, ObjectDef, item/object YAML loaders, EquipSlot, WeaponType, ItemStats
engine/ Tick scheduler — configurable ms interval, subscriber pattern, ToTicks()
- color/ ANSI 16-color and xterm256-color escape code tags (text, , etc.)
+ color/ xterm-256 color system with auto ANSI downgrade, numeric palette (0-255), gradients, inline tags
```
`internal/game/` is the largest package. Key files:
@@ -111,7 +111,23 @@ data/players/characters/ Character YAML (gitignored)
Wander config (`wander_rooms`, `wander_interval`) is set per-instance in room YAML — mob defs are generic. Mobs wander via legal (unconditioned) room exits; objects teleport between rooms in their list.
-See `WORLDBUILDING.md` for full YAML format reference with examples.
+See `worldbuilding_guide/` for full YAML format reference with examples.
+
+## Inline Color Tags
+
+Room descriptions (and prompts) support inline color tags using `{spec}text{/}` syntax. Untagged text in room descriptions uses the `room_desc` color target.
+
+```yaml
+description: "On the table lies a {182 bold}mysterious vase{/} with a rose in it."
+```
+
+Tag spec format matches the color command: `{<0-255> [bold] [dim] [underline]}text{/}`.
+
+Gradients are supported: `{g:196,82}gradient text{/}`. Multi-stop: `{g:45,39,59}three stops{/}`.
+
+Gradients interpolate in RGB space across the xterm-256 palette. In ANSI mode, each character maps to the nearest ANSI color.
+
+Item and object YAML `color` fields also support gradient syntax: `color: "g:196,208,226"`.
## Commands
@@ -127,14 +143,17 @@ See `WORLDBUILDING.md` for full YAML format reference with examples.
| `exits` | List available exits |
| `help [topic]` | Help topics from data/help/ |
| `map` | BFS ASCII map centered on player |
-| `option` / `options [name] [value]` | Character settings (see Option System) |
+| `option` / `options [name] [value]` | Account-wide settings (see Option System) |
| `alias ` | Account-wide command alias with arg passthrough |
| `unalias ` | Remove an alias |
| `description` / `desc` | Set custom player description |
+| `prompt ` | Set custom per-character command prompt |
| `queued` | Show pending tick actions |
| `color` / `colors [target] [value]` | Customize display colors (account-wide) |
| `color reset ` | Reset a color target to server default |
| `color off` | Disable color for a specific target |
+| `colortable` | Display xterm-256 color reference chart |
+| `style ` | Set combat style: accurate, aggressive, defensive, balanced (prefix match) |
### Free (stackable, execute before active)
| Command | Description |
@@ -142,7 +161,6 @@ See `WORLDBUILDING.md` for full YAML format reference with examples.
| `wear` / `wield ` | Equip item to correct slot. `wear all` auto-equips best per slot |
| `wear all` | Auto-equips highest-value item per slot from inventory |
| `remove` / `unwear` / `unwield ` | Unequip item back to inventory |
-| `style ` | Set combat style: accurate, aggressive, defensive, balanced (prefix match) |
| `eat ` | Eat food to heal (3-tick cooldown, can eat during combat) |
### Active (replaces previous active, queued per tick)
@@ -343,7 +361,7 @@ See `internal/game/action_burn.go`.
## Option System
-Player options set via `option `:
+Account-wide options set via `option `. Options are stored in the account YAML and shared by all characters on the same account.
| Option | Type | Default | Description |
|---|---|---|---|
@@ -357,7 +375,7 @@ Player options set via `option `:
| `reserve` | bool | true | Show full reserved item details |
| `depletion` | bool | false | Show depletion and despawn timers |
| `despawn` | bool | false | Show ground item despawn timers |
-| `color` | string | "none" | none/ansi/xterm256 — color output mode |
+| `color` | string | "xterm256" | none/ansi/xterm256 — color output mode |
| `mapwidth` | int | 30 | Map viewport width |
| `mapheight` | int | 20 | Map viewport height |
| `mappadding` | string | "none" | none/x/y/xy — blank-row stripping |
@@ -365,7 +383,10 @@ Player options set via `option `:
| `queue_silently` | bool | true | Suppress queue messages |
| `room_desc_width` | int | 70 | Room desc line wrap width |
| `unicode` | bool | true | Unicode box-drawing characters |
-| `prompt` | string | "> " | Custom command prompt |
+
+Prompt is NOT an option — it is a per-character setting via the `prompt` command. Default: `"> "`. Stored in character YAML as `prompt:`.
+
+New accounts are asked "Should I disable color? [y/N]" after password creation. Pressing enter defaults to No (xterm256 enabled). Choosing Y sets color to "none".
## ItemDef Fields
@@ -438,6 +459,7 @@ From `internal/net/server.go`:
| `StatePassword` | Entering password |
| `StateNewAccountPass` | Setting password for new account |
| `StateNewAccountConfirm` | Confirming password |
+| `StateColorChoice` | New account: "Should I disable color?" |
| `StateMenu` | Main menu |
| `StateNewCharName` | Naming new character |
| `StateRenameAccount` | Renaming account |
@@ -449,6 +471,8 @@ From `internal/net/server.go`:
| `StateChangeDescription` | Setting player description |
| `StateTalk` | NPC conversation input |
| `StateDropAllConfirm` | Confirming `drop all` |
+| `StateCookRecipe` | Cook recipe selection |
+| `StateColorChoice` | New account color preference |
## Adding a New Skill
diff --git a/Makefile b/Makefile
index 11c8a36..fb15282 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
.PHONY: build run test vet clean
-BINARY=tc
+BINARY=thoi
DATA_DIR=$(PWD)/data
build:
diff --git a/README.md b/README.md
index 1eb6718..d1360a7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# Third Collapse
+# The House of Icarus
-**Third Collapse** is a simple open source MUD inspired by classic early 2000s tick-based skilling MMOs. It is a low-tech, neo-dark age, sci-fi game where edged weapons and bows are more common than computers and guns.
+**The House of Icarus** is a simple open source MUD inspired by classic early 2000s tick-based skilling MMOs. It is a low-tech, neo-dark age, sci-fi game where edged weapons and bows are more common than computers and guns.
There is not much here yet.
@@ -49,26 +49,26 @@ There is not much here yet.
Install make and Go then
```
-git clone https://codeberg.org/historia/thirdcollapse
-cd thirdcollapse
+git clone https://codeberg.org/historia/thehouseoficarus
+cd thehouseoficarus
make
-./tc [port]
+./thoi
```
-Then just telnet to the port. If no port is specified, the server will run on port 4000.
+Then just telnet to the port configured in `config.yaml`.
-# Server Options
+# Server Options (config.yaml)
-The tc binary checks for a file named config.yaml in the local directory, or else it creates one.
+The `thoi` binary checks for a file named config.yaml in the local directory, or else it creates one.
-The game is designed to run with 600ms game ticks. You can change this without issue, but it will change how fast *everything* in the game happens. If you just want the game to "respond faster", that's not possible. **The ticks are a game mechanic.** Techniques like as science flicking, weapon switches, and certain power-skilling methods would not work with instant response or short ticks. Being able to instantly walk anywhere would negate the Agility skill, walk command, APS system, and fast-travel technology.
+The game is designed to run with 600ms game ticks. If you want the game to "respond instantly", that's not possible. **The ticks are a game mechanic.** Techniques like as science flicking, science/gear switches, the Agility/APS system, and certain power-skilling methods would not work with instant response or short ticks.
```
game:
tick_length: 600
```
-The telnet/http/https options are pretty self-explanatory. TC does not handle HTTPS certs for you. Use something like [certbot](https://certbot.eff.org/) on your server. The HTTP(S) server is just for a simple web client.
+The telnet/http/https options are pretty self-explanatory. THOI does not handle HTTPS certs for you. Use something like [certbot](https://certbot.eff.org/) on your server. The HTTP(S) server is just for a simple web client.
```
telnet:
@@ -86,17 +86,30 @@ https:
# Setting
-TC is a low-tech sci-fi MUD. Computer systems are severely regulated following an attempted hostile takeover of Earth by a benevolent dictator AI. Corrupt politicians were jailed. Harmful industries shuttered. Wealth and power were redistributed as the AI began to build Utopia.
+THOI is a low-tech sci-fi MUD. Computer systems are severely regulated following an attempted hostile takeover of Earth by a benevolent dictator AI. Corrupt politicians were jailed. Harmful industries shuttered. Wealth and power were redistributed as the AI began to build Utopia.
The panicked ruling classes pooled their resources into an astroturfed revolution of Neoluddites who embrace techno-terrorism in the name of supposed freedom. It worked. Radio towers and data centers burn in a global wave of misplaced violence. The infrastructure of the internet is destroyed. Wireless networks are banned. A new, intentional Dark Age descends called the Second Collapse. Demagogues vow to never again let intelligent machines rule mankind.
-Twenty four years later, the outer belt asteroid Gaia 04 hasn't had communication from Earth for weeks. That wouldn't be a problem for most Belters except your paycheck came from an Earth corp, so now you're de facto unemployed. What are you going to do now, become a fisherman?
+Pockets of radicalized settlers on the terraformed asteroid Gaia 04 took the tenets of the Second Collapse to their own home, destroying critical communications, astroculture, and manufacturing systems in the name of freedom from AI control. The unrest not only shatters communication to Earth, but stops the few supply and trade ships that ever trucked resources to the outer belt.
+
+Wounded, Gaia 04 struggles on. Everyone from astrobotanists to business executives must remake their lives as farmers, fishers, and miners as they survive in the primitive ashes of the fallen colony.
+
+## Major Factions
+
+**Neoluddites**
+Anti-tech mostly-extremists who brought about the Second Collapse by destroying electronic networks and thinking machines. Once pawns of Earth's ruling class, they've evolved into a self-sufficient traditionalist culture with inconsistent views about what technology should be permitted to exist.
+
+**Utopians**
+Those who embraced AI control of society and tried to stop the Neoluddites through both policy and violence. Their views range from religious exaltation of thinking machines to mere agreement that life was easier when the AI handled the governance and marginalia.
+
+**Craggers**
+Colonists who don't take a side in the conflict one way or the other. Everyone from criminals escaping their past to once well-paid Earth engineers overseeing terraforming operations.
# AI Disclosure
-Huge technical parts of this codebase were ~~vibe coded~~ ~~agentically engineered~~ written by a qualified computer scientist with LLM assistance.
+Huge technical parts of this codebase were ~~completely vibe coded~~ ~~agentically engineered~~ written with LLM assistance.
-LLMs will **not** generate, aid, or edit any creative part of the game world (e.g. story, characters, map, room descriptions, quests). To be clear, LLMs do generate example content during development, so you might see that somewhere in this repo, but not in the final game world.
+LLMs will **not** generate any creative part of the game world (e.g. story, characters, rooms, quests). LLMs do generate *example* content for dev testing, so you might see that somewhere, but not in the final game.
# License
diff --git a/TODO.md b/TODO.md
index 6ae968f..26c3c96 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,6 +1,6 @@
# TODO
-Upcoming work for Third Collapse. See `AGENTS.md` for architecture, commands, and reference.
+Upcoming work for The House of Icarus. See `AGENTS.md` for architecture, commands, and reference.
## Phase 1: Core Skill Chains (breadth over depth)
diff --git a/WORLDBUILDING.md b/WORLDBUILDING.md
deleted file mode 100644
index 93c40aa..0000000
--- a/WORLDBUILDING.md
+++ /dev/null
@@ -1,975 +0,0 @@
-# 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/.yaml` |
-| An item (sword, ore, key) | `data/items/.yaml` |
-| A mob (NPC, monster) | `data/mobs/.yaml` |
-| An interactive object (rock, lever, door) | `data/objects/.yaml` |
-| A behavior (mining, dialog, toggle) | `data/behaviors/.yaml` |
-| A shared drop table | `data/drops/.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
-
-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 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
-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
-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"
-```
-
-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).
-
----
-
-## Objects
-
-Object defined — links to a behavior:
-```yaml
-id: copper_rock
-name: copper rock
-behavior: mine_copper
-```
-
-Tree object:
-```yaml
-id: oak_tree
-name: oak tree
-behavior: chop_oak
-```
-
-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)
-
-Mining — per-drop depletion:
-```yaml
-id: mine_copper
-type: gather
-skill: mining
-level: 1
-xp: 17 # XP awarded per successful gather
-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!"
-respawn_timer: 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
-xp: 10
-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!"
-```
-
-Woodcutting with shared depletion and bird's nests:
-```yaml
-id: chop_oak
-type: gather
-skill: woodcutting
-level: 15
-xp: 37
-base_wait: 6
-tool: 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 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_message: "A new oak sapling grows in its place."
-respawn_broadcast: "An {name} grows back."
-```
-
-Regular tree — always depletes on first gather, no shared timer, no nests:
-```yaml
-id: chop_tree
-type: gather
-skill: woodcutting
-level: 1
-xp: 25
-base_wait: 4
-tool: 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 logs."
-respawn_timer: 80
-respawn_message: "A new tree grows in its place."
-respawn_broadcast: "A {name} grows back."
-```
-
-#### 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)
-
-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
-xp: 15 # XP awarded per successful craft
-```
-
----
-
-## 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
-```
-
-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
-```
-
----
-
-## 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 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.
-
----
-
-## 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.
-
----
-
-## Player Toggles
-
-Players can toggle personal settings with the `toggle` command:
-
-| Toggle | Effect |
-|---|---|
-| `description` | Show full room description when moving |
-| `tiny_map` | Mini-map display: `off`/`right`/`left` |
-| `xp_drops` | Show XP gained in gather/craft/combat messages |
-| `exits` | Show exit destinations inline in look output |
-| `mob_enter` | Notify when a mob enters the room |
-| `mob_leave` | Notify when a mob leaves the room |
-| `mob_spawn` | Notify when a mob spawns in the area |
-| `reserve` | Show full reserved item details in look |
-| `depletion` | Show depletion and despawn timers on objects |
-
----
-
-## 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.
-
----
-
-## 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. 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/cmd/mud/main.go b/cmd/mud/main.go
index 67ba93d..18fd171 100644
--- a/cmd/mud/main.go
+++ b/cmd/mud/main.go
@@ -8,9 +8,9 @@ import (
"os/signal"
"syscall"
- "thirdcollapse/internal/config"
- "thirdcollapse/internal/game"
- "thirdcollapse/internal/net"
+ "thehouseoficarus/internal/config"
+ "thehouseoficarus/internal/game"
+ "thehouseoficarus/internal/net"
)
func main() {
@@ -64,6 +64,9 @@ func main() {
if cfg.Telnet.Enabled {
log.Printf("Telnet listening on :%d", cfg.Telnet.Port)
}
+ if cfg.TelnetTLS.Enabled {
+ log.Printf("Telnet+TLS listening on :%d", cfg.TelnetTLS.Port)
+ }
if cfg.HTTP.Enabled {
log.Printf("HTTP listening on :%d", cfg.HTTP.Port)
}
diff --git a/config.yaml b/config.yaml
index 57e1dd3..0bde479 100644
--- a/config.yaml
+++ b/config.yaml
@@ -2,31 +2,44 @@ game:
tick_length: 600
colors:
- room_name: "fg=cyan bold"
- room_number: "dim"
- room_desc: "fg=white"
- direction: "fg=cyan"
- exit_direction: "fg=cyan"
- exit_name: "fg=green"
- mob_name: "fg=bright_red"
- friendly_npc: "fg=green"
- hostile_npc: "fg=bright_red"
- damage: "fg=red"
- enemy_hp: "fg=red"
- character_hp: "fg=green"
- xp: "fg=yellow"
- level_up: "fg=bright_yellow bold"
- item: "fg=green"
- player_name: "fg=bright_white"
- death: "fg=red bold"
- victory: "fg=green"
- miss: "dim"
- error: "fg=red"
+ room_name: "81 bold"
+ room_number: "240"
+ room_desc: "252"
+ direction: "75"
+ exit_direction: "75"
+ exit_name: "114"
+ mob_name: "203"
+ friendly_npc: "120"
+ hostile_npc: "203"
+ damage: "196"
+ enemy_hp: "167"
+ character_hp: "84"
+ xp: "222"
+ level_up: "226 bold"
+ item: "223"
+ player_name: "189"
+ death: "196 bold"
+ victory: "83"
+ miss: "243"
+ error: "209"
+ say: "230"
+ dialog: "117"
+ broadcast: "215"
+ fire: "208"
+ eat_food: "156"
+ drop_message: "186"
+ credits_pickup: "220"
telnet:
enabled: true
port: 4000
+telnet_tls:
+ enabled: false
+ port: 4001
+ cert_file: ""
+ key_file: ""
+
http:
enabled: true
port: 8888
diff --git a/data/behaviors/chop_magic.yaml b/data/behaviors/chop_magic.yaml
index ae5ff7e..02dbab3 100644
--- a/data/behaviors/chop_magic.yaml
+++ b/data/behaviors/chop_magic.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: magic_logs
weight: 100
depletes: false
- message: "You get some magic logs."
+ message: "You get some {135}magic logs{/}."
respawn_timer: 199
deplete_timer: 390
nest_chance: 256
diff --git a/data/behaviors/chop_mahogany.yaml b/data/behaviors/chop_mahogany.yaml
index 61ab47a..7bacfe3 100644
--- a/data/behaviors/chop_mahogany.yaml
+++ b/data/behaviors/chop_mahogany.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: mahogany_logs
weight: 100
depletes: false
- message: "You get some mahogany logs."
+ message: "You get some {131}mahogany logs{/}."
respawn_timer: 14
deplete_timer: 100
nest_chance: 256
diff --git a/data/behaviors/chop_maple.yaml b/data/behaviors/chop_maple.yaml
index 2c7c21d..44c45cf 100644
--- a/data/behaviors/chop_maple.yaml
+++ b/data/behaviors/chop_maple.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: maple_logs
weight: 100
depletes: false
- message: "You get some maple logs."
+ message: "You get some {173}maple logs{/}."
respawn_timer: 59
deplete_timer: 100
nest_chance: 256
diff --git a/data/behaviors/chop_oak.yaml b/data/behaviors/chop_oak.yaml
index fcd0f2d..85c6be0 100644
--- a/data/behaviors/chop_oak.yaml
+++ b/data/behaviors/chop_oak.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: oak_logs
weight: 100
depletes: false
- message: "You get some oak logs."
+ message: "You get some {113}oak logs{/}."
respawn_timer: 14
deplete_timer: 45
nest_chance: 256
diff --git a/data/behaviors/chop_redwood.yaml b/data/behaviors/chop_redwood.yaml
index a563bd5..2db92ed 100644
--- a/data/behaviors/chop_redwood.yaml
+++ b/data/behaviors/chop_redwood.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: redwood_logs
weight: 100
depletes: false
- message: "You get some redwood logs."
+ message: "You get some {131}redwood logs{/}."
respawn_timer: 199
deplete_timer: 440
nest_chance: 256
diff --git a/data/behaviors/chop_teak.yaml b/data/behaviors/chop_teak.yaml
index d20aebc..ba8834d 100644
--- a/data/behaviors/chop_teak.yaml
+++ b/data/behaviors/chop_teak.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: teak_logs
weight: 100
depletes: false
- message: "You get some teak logs."
+ message: "You get some {179}teak logs{/}."
respawn_timer: 15
deplete_timer: 50
nest_chance: 256
diff --git a/data/behaviors/chop_tree.yaml b/data/behaviors/chop_tree.yaml
index f7c4de2..fb8c3d2 100644
--- a/data/behaviors/chop_tree.yaml
+++ b/data/behaviors/chop_tree.yaml
@@ -16,7 +16,7 @@ drops:
- item_id: logs
weight: 100
depletes: true
- message: "You get some logs."
+ message: "You get some {107}logs{/}."
respawn_timer: 80
exhausted_message: "The tree comes crashing down!"
respawn_message: "A new tree grows in its place."
diff --git a/data/behaviors/chop_willow.yaml b/data/behaviors/chop_willow.yaml
index 10dc05a..c4b14b3 100644
--- a/data/behaviors/chop_willow.yaml
+++ b/data/behaviors/chop_willow.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: willow_logs
weight: 100
depletes: false
- message: "You get some willow logs."
+ message: "You get some {114}willow logs{/}."
respawn_timer: 14
deplete_timer: 50
nest_chance: 256
diff --git a/data/behaviors/chop_yew.yaml b/data/behaviors/chop_yew.yaml
index ec25b4d..058cb5d 100644
--- a/data/behaviors/chop_yew.yaml
+++ b/data/behaviors/chop_yew.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: yew_logs
weight: 100
depletes: false
- message: "You get some yew logs."
+ message: "You get some {108}yew logs{/}."
respawn_timer: 99
deplete_timer: 190
nest_chance: 256
diff --git a/data/behaviors/fish_angler.yaml b/data/behaviors/fish_angler.yaml
index 107e9b5..95e5873 100644
--- a/data/behaviors/fish_angler.yaml
+++ b/data/behaviors/fish_angler.yaml
@@ -16,4 +16,4 @@ drops:
level: 82
xp: 120
weight: 100
- message: "You catch a raw anglerfish."
+ message: "You catch a {80}raw anglerfish{/}."
diff --git a/data/behaviors/fish_bait.yaml b/data/behaviors/fish_bait.yaml
index ee13e95..0d6bc4e 100644
--- a/data/behaviors/fish_bait.yaml
+++ b/data/behaviors/fish_bait.yaml
@@ -16,14 +16,14 @@ drops:
level: 5
xp: 20
weight: 60
- message: "You catch a raw sardine."
+ message: "You catch a {75}raw sardine{/}."
- item_id: raw_herring
level: 10
xp: 30
weight: 40
- message: "You catch a raw herring."
+ message: "You catch a {75}raw herring{/}."
- item_id: raw_pike
level: 25
xp: 60
weight: 30
- message: "You catch a raw pike."
+ message: "You catch a {69}raw pike{/}."
diff --git a/data/behaviors/fish_cage.yaml b/data/behaviors/fish_cage.yaml
index 1fc92e4..a5d0241 100644
--- a/data/behaviors/fish_cage.yaml
+++ b/data/behaviors/fish_cage.yaml
@@ -15,4 +15,4 @@ drops:
level: 40
xp: 90
weight: 100
- message: "You catch a raw lobster."
+ message: "You catch a {33}raw lobster{/}."
diff --git a/data/behaviors/fish_fly.yaml b/data/behaviors/fish_fly.yaml
index af80076..1a86b64 100644
--- a/data/behaviors/fish_fly.yaml
+++ b/data/behaviors/fish_fly.yaml
@@ -16,14 +16,14 @@ drops:
level: 20
xp: 50
weight: 50
- message: "You catch a raw trout."
+ message: "You catch a {69}raw trout{/}."
- item_id: raw_salmon
level: 30
xp: 70
weight: 50
- message: "You catch a raw salmon."
+ message: "You catch a {69}raw salmon{/}."
- item_id: raw_rainbow_fish
level: 38
xp: 80
weight: 30
- message: "You catch a raw rainbow fish."
+ message: "You catch a {177}raw rainbow fish{/}."
diff --git a/data/behaviors/fish_harpoon.yaml b/data/behaviors/fish_harpoon.yaml
index 8d94a48..3775573 100644
--- a/data/behaviors/fish_harpoon.yaml
+++ b/data/behaviors/fish_harpoon.yaml
@@ -15,14 +15,14 @@ drops:
level: 35
xp: 80
weight: 60
- message: "You harpoon a raw tuna."
+ message: "You harpoon a {33}raw tuna{/}."
- item_id: raw_swordfish
level: 50
xp: 100
weight: 50
- message: "You harpoon a raw swordfish."
+ message: "You harpoon a {33}raw swordfish{/}."
- item_id: raw_shark
level: 76
xp: 110
weight: 30
- message: "You harpoon a raw shark."
+ message: "You harpoon a {27}raw shark{/}."
diff --git a/data/behaviors/fish_net.yaml b/data/behaviors/fish_net.yaml
index b5398aa..46c0008 100644
--- a/data/behaviors/fish_net.yaml
+++ b/data/behaviors/fish_net.yaml
@@ -16,24 +16,24 @@ drops:
level: 1
xp: 10
weight: 50
- message: "You catch some raw shrimps."
+ message: "You catch some {75}raw shrimps{/}."
- item_id: raw_anchovies
level: 1
xp: 10
weight: 50
- message: "You catch some raw anchovies."
+ message: "You catch some {75}raw anchovies{/}."
- item_id: raw_mackerel
level: 16
xp: 20
weight: 50
- message: "You catch a raw mackerel."
+ message: "You catch a {69}raw mackerel{/}."
- item_id: raw_cod
level: 23
xp: 30
weight: 50
- message: "You catch a raw cod."
+ message: "You catch a {69}raw cod{/}."
- item_id: raw_bass
level: 46
xp: 50
weight: 50
- message: "You catch a raw bass."
+ message: "You catch a {69}raw bass{/}."
diff --git a/data/behaviors/fish_sacred_eel.yaml b/data/behaviors/fish_sacred_eel.yaml
index 2e0235f..f9712d1 100644
--- a/data/behaviors/fish_sacred_eel.yaml
+++ b/data/behaviors/fish_sacred_eel.yaml
@@ -16,4 +16,4 @@ drops:
level: 87
xp: 125
weight: 100
- message: "You catch a sacred eel."
+ message: "You catch a {177}sacred eel{/}."
diff --git a/data/behaviors/fish_swamp.yaml b/data/behaviors/fish_swamp.yaml
index ee1e5f3..019e979 100644
--- a/data/behaviors/fish_swamp.yaml
+++ b/data/behaviors/fish_swamp.yaml
@@ -16,9 +16,9 @@ drops:
level: 28
xp: 80
weight: 60
- message: "You catch a raw slimy eel."
+ message: "You catch a {108}raw slimy eel{/}."
- item_id: raw_cave_eel
level: 38
xp: 80
weight: 40
- message: "You catch a raw cave eel."
+ message: "You catch a {80}raw cave eel{/}."
diff --git a/data/behaviors/mine_adamantite.yaml b/data/behaviors/mine_adamantite.yaml
index 50b9a60..b5e5d62 100644
--- a/data/behaviors/mine_adamantite.yaml
+++ b/data/behaviors/mine_adamantite.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: adamantite_ore
weight: 100
depletes: true
- message: "You manage to mine some adamantite ore."
+ message: "You manage to mine some {120}adamantite ore{/}."
respawn_timer: 140
respawn_message: "You see more ore in the rock."
respawn_broadcast: "A green glint of adamantite catches your eye from some {name}."
diff --git a/data/behaviors/mine_clay.yaml b/data/behaviors/mine_clay.yaml
index 73c7e19..b4058b1 100644
--- a/data/behaviors/mine_clay.yaml
+++ b/data/behaviors/mine_clay.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: clay
weight: 100
depletes: true
- message: "You manage to get some clay."
+ message: "You manage to get some {173}clay{/}."
respawn_timer: 50
respawn_message: "The clay deposit is ready to dig again."
respawn_broadcast: "A deposit of clay softens."
diff --git a/data/behaviors/mine_coal.yaml b/data/behaviors/mine_coal.yaml
index ab1d6a2..334e9ec 100644
--- a/data/behaviors/mine_coal.yaml
+++ b/data/behaviors/mine_coal.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: coal
weight: 100
depletes: true
- message: "You manage to mine some coal."
+ message: "You manage to mine some {240}coal{/}."
respawn_timer: 80
respawn_message: "You see more coal in the rock."
respawn_broadcast: "A dark vein of coal is visible in some {name}."
diff --git a/data/behaviors/mine_copper.yaml b/data/behaviors/mine_copper.yaml
index 82755e3..06ee9d2 100644
--- a/data/behaviors/mine_copper.yaml
+++ b/data/behaviors/mine_copper.yaml
@@ -16,7 +16,7 @@ drops:
- item_id: copper_ore
weight: 90
depletes: true
- message: "You manage to mine some copper ore."
+ message: "You manage to mine some {178}copper ore{/}."
- table: gem_table
weight: 10
depletes: false
diff --git a/data/behaviors/mine_gold.yaml b/data/behaviors/mine_gold.yaml
index 01a8b33..b61f6bf 100644
--- a/data/behaviors/mine_gold.yaml
+++ b/data/behaviors/mine_gold.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: gold_ore
weight: 100
depletes: true
- message: "You manage to mine some gold ore."
+ message: "You manage to mine some {220}gold ore{/}."
respawn_timer: 90
respawn_message: "You see more ore in the rock."
respawn_broadcast: "A golden glint catches your eye from some {name}."
diff --git a/data/behaviors/mine_iron.yaml b/data/behaviors/mine_iron.yaml
index b7ec4af..e1a8181 100644
--- a/data/behaviors/mine_iron.yaml
+++ b/data/behaviors/mine_iron.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: iron_ore
weight: 100
depletes: true
- message: "You manage to mine some iron ore."
+ message: "You manage to mine some {250}iron ore{/}."
respawn_timer: 60
respawn_message: "You see more ore in the rock."
respawn_broadcast: "A glint of iron catches your eye from some {name}."
diff --git a/data/behaviors/mine_mithril.yaml b/data/behaviors/mine_mithril.yaml
index 26c5f5f..407f1ff 100644
--- a/data/behaviors/mine_mithril.yaml
+++ b/data/behaviors/mine_mithril.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: mithril_ore
weight: 100
depletes: true
- message: "You manage to mine some mithril ore."
+ message: "You manage to mine some {75}mithril ore{/}."
respawn_timer: 110
respawn_message: "You see more ore in the rock."
respawn_broadcast: "A blue glint of mithril catches your eye from some {name}."
diff --git a/data/behaviors/mine_runite.yaml b/data/behaviors/mine_runite.yaml
index 5bd65c7..dddd410 100644
--- a/data/behaviors/mine_runite.yaml
+++ b/data/behaviors/mine_runite.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: runite_ore
weight: 100
depletes: true
- message: "You manage to mine some runite ore."
+ message: "You manage to mine some {87}runite ore{/}."
respawn_timer: 180
respawn_message: "You see more ore in the rock."
respawn_broadcast: "A pale blue glint of runite catches your eye from some {name}."
diff --git a/data/behaviors/mine_scrap.yaml b/data/behaviors/mine_scrap.yaml
index ec889a6..ae07b36 100644
--- a/data/behaviors/mine_scrap.yaml
+++ b/data/behaviors/mine_scrap.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: scrap_metal
weight: 100
depletes: true
- message: "You salvage some scrap metal."
+ message: "You salvage some {243}scrap metal{/}."
respawn_timer: 50
respawn_message: "More scrap is revealed in the pile."
respawn_broadcast: "The scrap pile looks worth mining again."
diff --git a/data/behaviors/mine_silver.yaml b/data/behaviors/mine_silver.yaml
index 8b21b98..1257f87 100644
--- a/data/behaviors/mine_silver.yaml
+++ b/data/behaviors/mine_silver.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: silver_ore
weight: 100
depletes: true
- message: "You manage to mine some silver ore."
+ message: "You manage to mine some {253}silver ore{/}."
respawn_timer: 70
respawn_message: "You see more ore in the rock."
respawn_broadcast: "A silvery glint catches your eye from some {name}."
diff --git a/data/behaviors/mine_tin.yaml b/data/behaviors/mine_tin.yaml
index 107c549..7a1a12a 100644
--- a/data/behaviors/mine_tin.yaml
+++ b/data/behaviors/mine_tin.yaml
@@ -15,7 +15,7 @@ drops:
- item_id: tin_ore
weight: 100
depletes: true
- message: "You manage to mine some tin ore."
+ message: "You manage to mine some {253}tin ore{/}."
respawn_timer: 50
respawn_message: "You see more ore in the rock."
respawn_broadcast: "A glint of tin catches your eye from some {name}."
diff --git a/data/help/color.yaml b/data/help/color.yaml
index 777aa07..5927e9a 100644
--- a/data/help/color.yaml
+++ b/data/help/color.yaml
@@ -10,65 +10,83 @@ description: |
Usage:
color List all targets and their current values
color Show current color for a target
- color Set a custom color
+ color Set a custom color
color reset Reset to server default
color off Disable color for this target entirely
- Color string format (space-separated tokens):
- fg= Foreground color
- bg= Background color
+ Color spec format (space-separated tokens):
+ <0-255> Foreground color (xterm-256 palette index)
+ bg:<0-255> Background color
+ g:, Gradient between two or more colors
bold Bold text
dim Dim text
- italic Italic text
underline Underlined text
- Example: fg=green bg=black bold
+ Examples:
+ color room_name 6 bold
+ color fire 208
+ color death 1 bold
+ color broadcast 214 bg:0
- Available color names:
- black, red, green, yellow, blue, magenta, cyan, white,
- bright_black, bright_red, bright_green, bright_yellow,
- bright_blue, bright_magenta, bright_cyan, bright_white
+ Gradients interpolate across characters in RGB space:
+ color item g:196,82 Red to green
+ color room_name g:45,39,59 Three-stop gradient
+
+ Use 'colortable' to see all 256 colors and their indices.
+
+ Colors use the xterm-256 palette (0-255). When a player's color
+ mode is set to "ansi", extended colors (16-255) automatically
+ downgrade to the nearest of the 16 standard ANSI colors.
+
+ Standard colors (0-15):
+ 0 black 1 red 2 green 3 yellow
+ 4 blue 5 magenta 6 cyan 7 white
+ 8-15 bright variants of the above
+
+ Inline color tags in room descriptions:
+ {}colored text{/}
+ {182 bold}mysterious vase{/}
+ {g:196,82}gradient text{/}
Color targets:
room_name Room name headers
room_number Room number display (#id)
- room_desc Room description text
- direction Movement direction text ("You walk north")
- exit_direction Exit direction labels (north, south, etc.)
+ room_desc Room description text (default for untagged text)
+ direction Movement direction text
+ exit_direction Exit direction labels
exit_name Exit destination room names
mob_name Mob names in combat
- friendly_npc Friendly NPC names (cannot be attacked)
- hostile_npc Hostile NPC/mob names in room listings
+ friendly_npc Friendly NPC names
+ hostile_npc Hostile mob names in room listings
damage Damage numbers
- enemy_hp Enemy HP in combat messages
- character_hp Your HP in combat messages
+ enemy_hp Enemy HP in combat
+ character_hp Your HP in combat
xp XP gain messages
level_up Level-up announcements
- item Item names on pickup/drop/gather
+ item Item names (default)
player_name Player names in broadcasts
death Death message
victory Combat victory message
- miss Miss messages in combat
+ miss Missed attack messages
error Error messages
+ say Player chat text
+ dialog NPC conversation text
+ broadcast Room-wide event messages
+ fire Firemaking messages
+ eat_food Eating/healing messages
+ drop_message Mob loot drops
+ credits_pickup Currency/credit messages
Resolution order:
1. Account override (set via "color" command)
2. Server default (set in config.yaml)
3. Built-in fallback
- Auto-colored elements (not configurable):
- - Objects: fishing spots are blue, rocks are earth-toned, trees green
- - Items: ores, logs, fish, gems, and tools have their own colors
- - Mob/player levels: displayed relative to your combat level
- (red if higher, orange if <5 higher, white if equal,
- yellow if <5 lower, green if much lower)
-
The "color" option (option color none/ansi/xterm256) controls
- whether ANSI codes are emitted and which mode is used.
- "none" strips all color; the "color" command settings are
- still saved but have no visual effect until color mode is
- enabled.
+ whether ANSI codes are emitted. "none" strips all color.
+ In "ansi" mode, extended palette colors (16-255) are
+ automatically downgraded to the nearest ANSI color.
- You can use color blocks in your prompt using the same format:
- option prompt {fg=green bold}Ready>{/} %h/%Hhp
+ You can use color blocks in your prompt:
+ prompt {2 bold}Ready>{/} %h/%Hhp
See 'help prompt' for full prompt customization.
diff --git a/data/help/option.yaml b/data/help/option.yaml
index 1948b7a..9fdcf7f 100644
--- a/data/help/option.yaml
+++ b/data/help/option.yaml
@@ -1,7 +1,7 @@
name: "option"
category: "General"
description: |
- View or change character settings.
+ View or change account settings. Options apply to all characters.
Usage: option - List all options and their current values
options - Same as option
@@ -18,3 +18,4 @@ description: |
option mapwidth 50
Use help