aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--AGENTS.md40
-rw-r--r--Makefile2
-rw-r--r--README.md41
-rw-r--r--TODO.md2
-rw-r--r--WORLDBUILDING.md975
-rw-r--r--cmd/mud/main.go9
-rw-r--r--config.yaml53
-rw-r--r--data/behaviors/chop_magic.yaml2
-rw-r--r--data/behaviors/chop_mahogany.yaml2
-rw-r--r--data/behaviors/chop_maple.yaml2
-rw-r--r--data/behaviors/chop_oak.yaml2
-rw-r--r--data/behaviors/chop_redwood.yaml2
-rw-r--r--data/behaviors/chop_teak.yaml2
-rw-r--r--data/behaviors/chop_tree.yaml2
-rw-r--r--data/behaviors/chop_willow.yaml2
-rw-r--r--data/behaviors/chop_yew.yaml2
-rw-r--r--data/behaviors/fish_angler.yaml2
-rw-r--r--data/behaviors/fish_bait.yaml6
-rw-r--r--data/behaviors/fish_cage.yaml2
-rw-r--r--data/behaviors/fish_fly.yaml6
-rw-r--r--data/behaviors/fish_harpoon.yaml6
-rw-r--r--data/behaviors/fish_net.yaml10
-rw-r--r--data/behaviors/fish_sacred_eel.yaml2
-rw-r--r--data/behaviors/fish_swamp.yaml4
-rw-r--r--data/behaviors/mine_adamantite.yaml2
-rw-r--r--data/behaviors/mine_clay.yaml2
-rw-r--r--data/behaviors/mine_coal.yaml2
-rw-r--r--data/behaviors/mine_copper.yaml2
-rw-r--r--data/behaviors/mine_gold.yaml2
-rw-r--r--data/behaviors/mine_iron.yaml2
-rw-r--r--data/behaviors/mine_mithril.yaml2
-rw-r--r--data/behaviors/mine_runite.yaml2
-rw-r--r--data/behaviors/mine_scrap.yaml2
-rw-r--r--data/behaviors/mine_silver.yaml2
-rw-r--r--data/behaviors/mine_tin.yaml2
-rw-r--r--data/help/color.yaml82
-rw-r--r--data/help/option.yaml3
-rw-r--r--data/help/prompt.yaml26
-rw-r--r--data/items/adamant_axe.yaml2
-rw-r--r--data/items/adamant_pickaxe.yaml2
-rw-r--r--data/items/adamantite_ore.yaml2
-rw-r--r--data/items/anchovies.yaml2
-rw-r--r--data/items/ashes.yaml2
-rw-r--r--data/items/big_fishing_net.yaml2
-rw-r--r--data/items/birds_nest.yaml2
-rw-r--r--data/items/black_axe.yaml2
-rw-r--r--data/items/black_pickaxe.yaml2
-rw-r--r--data/items/bones.yaml2
-rw-r--r--data/items/bread.yaml2
-rw-r--r--data/items/bread_dough.yaml2
-rw-r--r--data/items/bronze_axe.yaml2
-rw-r--r--data/items/bronze_pickaxe.yaml2
-rw-r--r--data/items/bronze_sword.yaml2
-rw-r--r--data/items/bucket_of_water.yaml2
-rw-r--r--data/items/burnt_fish.yaml2
-rw-r--r--data/items/burnt_meat.yaml2
-rw-r--r--data/items/christmas_cracker.yaml2
-rw-r--r--data/items/clay.yaml2
-rw-r--r--data/items/coal.yaml2
-rw-r--r--data/items/copper_ore.yaml2
-rw-r--r--data/items/credits.yaml2
-rw-r--r--data/items/dragon_axe.yaml2
-rw-r--r--data/items/dragon_pickaxe.yaml2
-rw-r--r--data/items/feather.yaml2
-rw-r--r--data/items/firesteel.yaml2
-rw-r--r--data/items/fishing_bait.yaml2
-rw-r--r--data/items/fishing_rod.yaml2
-rw-r--r--data/items/fly_fishing_rod.yaml2
-rw-r--r--data/items/gold_ore.yaml2
-rw-r--r--data/items/harpoon.yaml2
-rw-r--r--data/items/herring.yaml2
-rw-r--r--data/items/iron_axe.yaml2
-rw-r--r--data/items/iron_ore.yaml2
-rw-r--r--data/items/iron_pickaxe.yaml2
-rw-r--r--data/items/lighter.yaml2
-rw-r--r--data/items/lobster_pot.yaml2
-rw-r--r--data/items/logs.yaml2
-rw-r--r--data/items/magic_logs.yaml2
-rw-r--r--data/items/mahogany_logs.yaml2
-rw-r--r--data/items/maple_logs.yaml2
-rw-r--r--data/items/matches.yaml2
-rw-r--r--data/items/mithril_axe.yaml2
-rw-r--r--data/items/mithril_ore.yaml2
-rw-r--r--data/items/mithril_pickaxe.yaml2
-rw-r--r--data/items/oak_logs.yaml2
-rw-r--r--data/items/party_hat_blue.yaml2
-rw-r--r--data/items/party_hat_green.yaml2
-rw-r--r--data/items/party_hat_pink.yaml2
-rw-r--r--data/items/party_hat_red.yaml2
-rw-r--r--data/items/party_hat_white.yaml2
-rw-r--r--data/items/party_hat_yellow.yaml2
-rw-r--r--data/items/pass_stub.yaml2
-rw-r--r--data/items/pastry_dough.yaml2
-rw-r--r--data/items/pizza_dough.yaml2
-rw-r--r--data/items/pot_of_flour.yaml2
-rw-r--r--data/items/raw_anchovies.yaml2
-rw-r--r--data/items/raw_anglerfish.yaml2
-rw-r--r--data/items/raw_bass.yaml2
-rw-r--r--data/items/raw_cave_eel.yaml2
-rw-r--r--data/items/raw_cod.yaml2
-rw-r--r--data/items/raw_herring.yaml2
-rw-r--r--data/items/raw_lobster.yaml2
-rw-r--r--data/items/raw_mackerel.yaml2
-rw-r--r--data/items/raw_pike.yaml2
-rw-r--r--data/items/raw_rainbow_fish.yaml2
-rw-r--r--data/items/raw_salmon.yaml2
-rw-r--r--data/items/raw_sardine.yaml2
-rw-r--r--data/items/raw_shark.yaml2
-rw-r--r--data/items/raw_shrimps.yaml2
-rw-r--r--data/items/raw_slimy_eel.yaml2
-rw-r--r--data/items/raw_swordfish.yaml2
-rw-r--r--data/items/raw_trout.yaml2
-rw-r--r--data/items/raw_tuna.yaml2
-rw-r--r--data/items/redwood_logs.yaml2
-rw-r--r--data/items/rune_axe.yaml2
-rw-r--r--data/items/rune_pickaxe.yaml2
-rw-r--r--data/items/runite_ore.yaml2
-rw-r--r--data/items/sacred_eel.yaml2
-rw-r--r--data/items/salmon.yaml2
-rw-r--r--data/items/sardine.yaml2
-rw-r--r--data/items/scrap_metal.yaml2
-rw-r--r--data/items/shrimps.yaml2
-rw-r--r--data/items/silver_ore.yaml2
-rw-r--r--data/items/small_fishing_net.yaml2
-rw-r--r--data/items/steel_axe.yaml2
-rw-r--r--data/items/steel_pickaxe.yaml2
-rw-r--r--data/items/teak_logs.yaml2
-rw-r--r--data/items/tin_ore.yaml2
-rw-r--r--data/items/trout.yaml2
-rw-r--r--data/items/uncut_diamond.yaml2
-rw-r--r--data/items/uncut_emerald.yaml2
-rw-r--r--data/items/uncut_ruby.yaml2
-rw-r--r--data/items/uncut_sapphire.yaml2
-rw-r--r--data/items/willow_logs.yaml2
-rw-r--r--data/items/yew_logs.yaml2
-rw-r--r--data/objects/adamantite_rock.yaml2
-rw-r--r--data/objects/anglerfish_spot.yaml2
-rw-r--r--data/objects/bait_fishing_spot.yaml2
-rw-r--r--data/objects/cage_fishing_spot.yaml2
-rw-r--r--data/objects/clay_rock.yaml2
-rw-r--r--data/objects/coal_rock.yaml2
-rw-r--r--data/objects/cooking_range.yaml2
-rw-r--r--data/objects/copper_rock.yaml2
-rw-r--r--data/objects/fire.yaml2
-rw-r--r--data/objects/fly_fishing_spot.yaml2
-rw-r--r--data/objects/gold_rock.yaml2
-rw-r--r--data/objects/harpoon_fishing_spot.yaml2
-rw-r--r--data/objects/iron_gate.yaml2
-rw-r--r--data/objects/iron_rock.yaml2
-rw-r--r--data/objects/lumby_fountain.yaml2
-rw-r--r--data/objects/magic_tree.yaml2
-rw-r--r--data/objects/mahogany_tree.yaml2
-rw-r--r--data/objects/maple_tree.yaml2
-rw-r--r--data/objects/mithril_rock.yaml2
-rw-r--r--data/objects/net_fishing_spot.yaml2
-rw-r--r--data/objects/oak_tree.yaml2
-rw-r--r--data/objects/redwood_tree.yaml2
-rw-r--r--data/objects/runite_rock.yaml2
-rw-r--r--data/objects/sacred_eel_spot.yaml2
-rw-r--r--data/objects/scrap_pile.yaml2
-rw-r--r--data/objects/silver_rock.yaml2
-rw-r--r--data/objects/swamp_fishing_spot.yaml2
-rw-r--r--data/objects/teak_tree.yaml2
-rw-r--r--data/objects/tin_rock.yaml2
-rw-r--r--data/objects/tree.yaml2
-rw-r--r--data/objects/willow_tree.yaml2
-rw-r--r--data/objects/yew_tree.yaml2
-rw-r--r--go.mod2
-rw-r--r--internal/color/color.go475
-rw-r--r--internal/config/config.go69
-rw-r--r--internal/game/action.go12
-rw-r--r--internal/game/action_burn.go66
-rw-r--r--internal/game/action_cook.go8
-rw-r--r--internal/game/action_gather.go15
-rw-r--r--internal/game/action_room.go4
-rw-r--r--internal/game/action_search.go19
-rw-r--r--internal/game/action_talk.go12
-rw-r--r--internal/game/action_toggle.go8
-rw-r--r--internal/game/action_use.go19
-rw-r--r--internal/game/cmd_alias.go2
-rw-r--r--internal/game/cmd_attack.go22
-rw-r--r--internal/game/cmd_color.go39
-rw-r--r--internal/game/cmd_colortable.go151
-rw-r--r--internal/game/cmd_cook.go6
-rw-r--r--internal/game/cmd_description.go4
-rw-r--r--internal/game/cmd_drop.go12
-rw-r--r--internal/game/cmd_eat.go10
-rw-r--r--internal/game/cmd_equipment.go4
-rw-r--r--internal/game/cmd_get.go26
-rw-r--r--internal/game/cmd_inventory.go9
-rw-r--r--internal/game/cmd_look.go24
-rw-r--r--internal/game/cmd_map.go4
-rw-r--r--internal/game/cmd_move.go6
-rw-r--r--internal/game/cmd_option.go35
-rw-r--r--internal/game/cmd_prompt.go33
-rw-r--r--internal/game/cmd_queued.go4
-rw-r--r--internal/game/cmd_quit.go8
-rw-r--r--internal/game/cmd_remove.go11
-rw-r--r--internal/game/cmd_say.go8
-rw-r--r--internal/game/cmd_score.go26
-rw-r--r--internal/game/cmd_search.go4
-rw-r--r--internal/game/cmd_style.go4
-rw-r--r--internal/game/cmd_use.go6
-rw-r--r--internal/game/cmd_walk.go6
-rw-r--r--internal/game/cmd_wear.go12
-rw-r--r--internal/game/color.go24
-rw-r--r--internal/game/game.go36
-rw-r--r--internal/game/help.go8
-rw-r--r--internal/game/login_account.go31
-rw-r--r--internal/game/login_char.go11
-rw-r--r--internal/game/map.go2
-rw-r--r--internal/game/map_test.go2
-rw-r--r--internal/game/prompt.go49
-rw-r--r--internal/game/table.go18
-rw-r--r--internal/game/tick.go30
-rw-r--r--internal/game/types.go2
-rw-r--r--internal/game/utils.go6
-rw-r--r--internal/net/server.go137
-rw-r--r--internal/net/terminal.html2
-rw-r--r--internal/object/item.go2
-rw-r--r--internal/player/account.go1
-rw-r--r--internal/player/player.go15
-rw-r--r--internal/player/store.go2
-rw-r--r--internal/world/mob.go2
-rw-r--r--internal/world/room.go2
-rw-r--r--worldbuilding_guide/README.md2
-rw-r--r--worldbuilding_guide/behaviors.md10
-rw-r--r--worldbuilding_guide/items.md17
-rw-r--r--worldbuilding_guide/objects.md4
-rw-r--r--worldbuilding_guide/recipes.md32
-rw-r--r--worldbuilding_guide/rooms.md12
-rw-r--r--worldbuilding_guide/toggles.md18
233 files changed, 1475 insertions, 1758 deletions
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 (<red>text</red>, <bold>, 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 <name> <cmd>` | Account-wide command alias with arg passthrough |
| `unalias <name>` | Remove an alias |
| `description` / `desc` | Set custom player description |
+| `prompt <value>` | Set custom per-character command prompt |
| `queued` | Show pending tick actions |
| `color` / `colors [target] [value]` | Customize display colors (account-wide) |
| `color reset <target>` | Reset a color target to server default |
| `color <target> off` | Disable color for a specific target |
+| `colortable` | Display xterm-256 color reference chart |
+| `style <name>` | 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 <item>` | 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 <item>` | Unequip item back to inventory |
-| `style <name>` | Set combat style: accurate, aggressive, defensive, balanced (prefix match) |
| `eat <item>` | 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 <name> <value>`:
+Account-wide options set via `option <name> <value>`. 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 <name> <value>`:
| `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 <name> <value>`:
| `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/<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
-
-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 <item>
-
-Example: search nest
- search birds nest
-```
-
-Searches your inventory for the named item. If found, removes it and rolls on a drop table.
-Currently only bird's nests are searchable.
-
----
-
-## 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 <target> Show current color for a target
- color <target> <string> Set a custom color
+ color <target> <spec> Set a custom color
color <target> reset Reset to server default
color <target> off Disable color for this target entirely
- Color string format (space-separated tokens):
- fg=<color> Foreground color
- bg=<color> Background color
+ Color spec format (space-separated tokens):
+ <0-255> Foreground color (xterm-256 palette index)
+ bg:<0-255> Background color
+ g:<a>,<b> 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:
+ {<spec>}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 <option> for details on a specific option.
+ Use 'prompt' to set a custom per-character prompt.
diff --git a/data/help/prompt.yaml b/data/help/prompt.yaml
index 95d852b..9af6b01 100644
--- a/data/help/prompt.yaml
+++ b/data/help/prompt.yaml
@@ -1,9 +1,12 @@
name: "prompt"
-category: "Options"
+category: "General"
description: |
- Customize your command prompt.
+ Customize your command prompt. Prompts are per-character.
- Set via: option prompt <string>
+ Usage:
+ prompt Show current prompt
+ prompt <value> Set a custom prompt
+ prompt reset Restore default "> "
Your prompt string can include variables and color blocks.
@@ -21,15 +24,14 @@ description: |
Use %% for a literal % sign.
- Color blocks use the same format as the color command:
- {fg=green bold}text{/}
+ Color blocks:
+ {<0-255> [bold] [dim] [underline]}text{/}
+ {g:<a>,<b>[,<c>...]}gradient text{/}
- A color block starts with {color_string} and ends with {/}.
+ A color block starts with {color_spec} and ends with {/}.
Examples:
- option prompt "> "
- option prompt "{fg=cyan}%h/%Hhp $%c %s{/} >"
- option prompt "<{fg=green}%h{/}/{fg=red}%H{/}hp> "
-
- To reset to the default "> ":
- option prompt "> "
+ prompt >
+ prompt {6}%h/%Hhp $%c %s{/} >
+ prompt <{2}%h{/}/{1}%H{/}hp>
+ prompt {208 bold}%h/%Hhp{/} >
diff --git a/data/items/adamant_axe.yaml b/data/items/adamant_axe.yaml
index 6c36a2c..d96bb47 100644
--- a/data/items/adamant_axe.yaml
+++ b/data/items/adamant_axe.yaml
@@ -1,6 +1,6 @@
id: adamant_axe
name: adamant axe
-color: "fg=green"
+color: "120"
description: "A powerful adamant axe."
value: 1250
stackable: false
diff --git a/data/items/adamant_pickaxe.yaml b/data/items/adamant_pickaxe.yaml
index ead660c..c37b9d8 100644
--- a/data/items/adamant_pickaxe.yaml
+++ b/data/items/adamant_pickaxe.yaml
@@ -1,6 +1,6 @@
id: adamant_pickaxe
name: adamant pickaxe
-color: "fg=green"
+color: "120"
description: "A powerful adamant pickaxe."
value: 1250
stackable: false
diff --git a/data/items/adamantite_ore.yaml b/data/items/adamantite_ore.yaml
index 2afde1c..fabe0da 100644
--- a/data/items/adamantite_ore.yaml
+++ b/data/items/adamantite_ore.yaml
@@ -1,6 +1,6 @@
id: adamantite_ore
name: adamantite ore
-color: "fg=green"
+color: "120"
description: "A chunk of dark green adamantite ore."
value: 100
stackable: false
diff --git a/data/items/anchovies.yaml b/data/items/anchovies.yaml
index 54b1a5a..0abd846 100644
--- a/data/items/anchovies.yaml
+++ b/data/items/anchovies.yaml
@@ -1,6 +1,6 @@
id: anchovies
name: anchovies
-color: "fg=bright_red"
+color: "209"
description: "Some nicely cooked anchovies."
value: 5
stackable: false
diff --git a/data/items/ashes.yaml b/data/items/ashes.yaml
index 6835057..0b83f77 100644
--- a/data/items/ashes.yaml
+++ b/data/items/ashes.yaml
@@ -1,6 +1,6 @@
id: ashes
name: ashes
-color: "dim"
+color: "243"
description: "A pile of cold ashes. What was once ablaze is now mere dust."
value: 1
stackable: false
diff --git a/data/items/big_fishing_net.yaml b/data/items/big_fishing_net.yaml
index 4b2b69e..a8b5352 100644
--- a/data/items/big_fishing_net.yaml
+++ b/data/items/big_fishing_net.yaml
@@ -1,6 +1,6 @@
id: big_fishing_net
name: big fishing net
-color: "fg=white"
+color: "230"
description: "A larger fishing net for catching bigger fish."
value: 20
stackable: false
diff --git a/data/items/birds_nest.yaml b/data/items/birds_nest.yaml
index 18fbab5..1366b5d 100644
--- a/data/items/birds_nest.yaml
+++ b/data/items/birds_nest.yaml
@@ -1,6 +1,6 @@
id: birds_nest
name: bird's nest
-color: "fg=bright_yellow"
+color: "179"
description: "A bird's nest. You can search it for treasure."
value: 5
stackable: false
diff --git a/data/items/black_axe.yaml b/data/items/black_axe.yaml
index b0ee271..2567af8 100644
--- a/data/items/black_axe.yaml
+++ b/data/items/black_axe.yaml
@@ -1,6 +1,6 @@
id: black_axe
name: black axe
-color: "dim"
+color: "240"
description: "A sleek black axe, as fast as steel."
value: 500
stackable: false
diff --git a/data/items/black_pickaxe.yaml b/data/items/black_pickaxe.yaml
index 2896612..5dd7df5 100644
--- a/data/items/black_pickaxe.yaml
+++ b/data/items/black_pickaxe.yaml
@@ -1,6 +1,6 @@
id: black_pickaxe
name: black pickaxe
-color: "dim"
+color: "240"
description: "A sleek black pickaxe."
value: 500
stackable: false
diff --git a/data/items/bones.yaml b/data/items/bones.yaml
index 6883a16..25097d3 100644
--- a/data/items/bones.yaml
+++ b/data/items/bones.yaml
@@ -1,6 +1,6 @@
id: bones
name: bones
-color: "fg=white"
+color: "253"
description: "A pile of bones."
value: 2
stackable: false
diff --git a/data/items/bread.yaml b/data/items/bread.yaml
index 2e0a1ff..68cefdc 100644
--- a/data/items/bread.yaml
+++ b/data/items/bread.yaml
@@ -1,6 +1,6 @@
id: bread
name: bread
-color: "fg=bright_yellow"
+color: "222"
description: "A fresh loaf of bread, still warm from the oven."
value: 5
stackable: false
diff --git a/data/items/bread_dough.yaml b/data/items/bread_dough.yaml
index d40391c..47df9b4 100644
--- a/data/items/bread_dough.yaml
+++ b/data/items/bread_dough.yaml
@@ -1,6 +1,6 @@
id: bread_dough
name: bread dough
-color: "fg=bright_yellow"
+color: "222"
description: "A ball of uncooked bread dough. It needs to be baked."
value: 3
stackable: false
diff --git a/data/items/bronze_axe.yaml b/data/items/bronze_axe.yaml
index 5db0929..42427e5 100644
--- a/data/items/bronze_axe.yaml
+++ b/data/items/bronze_axe.yaml
@@ -1,6 +1,6 @@
id: bronze_axe
name: bronze axe
-color: "fg=bright_yellow"
+color: "178"
description: "A sturdy bronze axe, good for woodcutting."
value: 10
stackable: false
diff --git a/data/items/bronze_pickaxe.yaml b/data/items/bronze_pickaxe.yaml
index d81d2bb..4ee8829 100644
--- a/data/items/bronze_pickaxe.yaml
+++ b/data/items/bronze_pickaxe.yaml
@@ -1,6 +1,6 @@
id: bronze_pickaxe
name: bronze pickaxe
-color: "fg=bright_yellow"
+color: "178"
description: "A sturdy bronze pickaxe, good for mining."
value: 10
stackable: false
diff --git a/data/items/bronze_sword.yaml b/data/items/bronze_sword.yaml
index ca47743..0ae7a07 100644
--- a/data/items/bronze_sword.yaml
+++ b/data/items/bronze_sword.yaml
@@ -1,6 +1,6 @@
id: bronze_sword
name: bronze sword
-color: "fg=bright_yellow"
+color: "178"
description: "A basic bronze short sword."
value: 12
stackable: false
diff --git a/data/items/bucket_of_water.yaml b/data/items/bucket_of_water.yaml
index e3d0af8..3449cdd 100644
--- a/data/items/bucket_of_water.yaml
+++ b/data/items/bucket_of_water.yaml
@@ -1,6 +1,6 @@
id: bucket_of_water
name: bucket of water
-color: "fg=cyan"
+color: "81"
description: "A wooden bucket filled with fresh water."
value: 5
stackable: false
diff --git a/data/items/burnt_fish.yaml b/data/items/burnt_fish.yaml
index e8a41b4..7af9f03 100644
--- a/data/items/burnt_fish.yaml
+++ b/data/items/burnt_fish.yaml
@@ -1,6 +1,6 @@
id: burnt_fish
name: burnt fish
-color: "dim"
+color: "243"
description: "A badly burnt fish. Completely inedible."
value: 0
stackable: true
diff --git a/data/items/burnt_meat.yaml b/data/items/burnt_meat.yaml
index 8d960c3..92c339c 100644
--- a/data/items/burnt_meat.yaml
+++ b/data/items/burnt_meat.yaml
@@ -1,6 +1,6 @@
id: burnt_meat
name: burnt meat
-color: "dim"
+color: "243"
description: "A piece of badly burnt meat. Completely inedible."
value: 0
stackable: true
diff --git a/data/items/christmas_cracker.yaml b/data/items/christmas_cracker.yaml
index 16cd9b3..c296443 100644
--- a/data/items/christmas_cracker.yaml
+++ b/data/items/christmas_cracker.yaml
@@ -1,6 +1,6 @@
id: christmas_cracker
name: christmas cracker
-color: "fg=bright_red"
+color: "196"
description: "A festive party cracker. Pull it apart to see what's inside!"
value: 10
stackable: true
diff --git a/data/items/clay.yaml b/data/items/clay.yaml
index f8575a3..55c9b2b 100644
--- a/data/items/clay.yaml
+++ b/data/items/clay.yaml
@@ -1,6 +1,6 @@
id: clay
name: clay
-color: "fg=bright_red"
+color: "173"
description: "Some soft clay, useful for crafting and pottery."
value: 1
stackable: false
diff --git a/data/items/coal.yaml b/data/items/coal.yaml
index 23a42a2..7b79f08 100644
--- a/data/items/coal.yaml
+++ b/data/items/coal.yaml
@@ -1,6 +1,6 @@
id: coal
name: coal
-color: "dim"
+color: "240"
description: "A lump of coal, useful for smelting."
value: 5
stackable: false
diff --git a/data/items/copper_ore.yaml b/data/items/copper_ore.yaml
index 8e53691..392879a 100644
--- a/data/items/copper_ore.yaml
+++ b/data/items/copper_ore.yaml
@@ -1,6 +1,6 @@
id: copper_ore
name: copper ore
-color: "fg=bright_yellow"
+color: "178"
description: "A chunk of copper-bearing rock. Can be smelted into a bronze bar."
value: 1
stackable: false
diff --git a/data/items/credits.yaml b/data/items/credits.yaml
index 4d2c9a8..7e2de10 100644
--- a/data/items/credits.yaml
+++ b/data/items/credits.yaml
@@ -1,6 +1,6 @@
id: credits
name: credits
-color: "fg=bright_yellow"
+color: "220"
description: "A handful of universal credits."
value: 1
stackable: true
diff --git a/data/items/dragon_axe.yaml b/data/items/dragon_axe.yaml
index 53c1f82..4484a51 100644
--- a/data/items/dragon_axe.yaml
+++ b/data/items/dragon_axe.yaml
@@ -1,6 +1,6 @@
id: dragon_axe
name: dragon axe
-color: "fg=bright_red"
+color: "196"
description: "A legendary dragon axe that bites through wood like butter."
value: 100000
stackable: false
diff --git a/data/items/dragon_pickaxe.yaml b/data/items/dragon_pickaxe.yaml
index 623a390..0fa6a7c 100644
--- a/data/items/dragon_pickaxe.yaml
+++ b/data/items/dragon_pickaxe.yaml
@@ -1,6 +1,6 @@
id: dragon_pickaxe
name: dragon pickaxe
-color: "fg=bright_red"
+color: "196"
description: "A legendary dragon pickaxe that pulverizes rock."
value: 100000
stackable: false
diff --git a/data/items/feather.yaml b/data/items/feather.yaml
index 2d2e7db..25afc2a 100644
--- a/data/items/feather.yaml
+++ b/data/items/feather.yaml
@@ -1,6 +1,6 @@
id: feather
name: feather
-color: "fg=white"
+color: "230"
description: "A colorful feather. Used as fly fishing bait."
value: 1
stackable: true
diff --git a/data/items/firesteel.yaml b/data/items/firesteel.yaml
index 8d65d26..b3f9302 100644
--- a/data/items/firesteel.yaml
+++ b/data/items/firesteel.yaml
@@ -1,6 +1,6 @@
id: firesteel
name: firesteel
-color: "dim"
+color: "250"
description: "A rugged firesteel that never wears out."
value: 20
stackable: false
diff --git a/data/items/fishing_bait.yaml b/data/items/fishing_bait.yaml
index 7690a1c..2336a7d 100644
--- a/data/items/fishing_bait.yaml
+++ b/data/items/fishing_bait.yaml
@@ -1,6 +1,6 @@
id: fishing_bait
name: fishing bait
-color: "fg=yellow"
+color: "222"
description: "A handful of wriggling bait. Used with a fishing rod."
value: 1
stackable: true
diff --git a/data/items/fishing_rod.yaml b/data/items/fishing_rod.yaml
index 77b207c..a48c0a1 100644
--- a/data/items/fishing_rod.yaml
+++ b/data/items/fishing_rod.yaml
@@ -1,6 +1,6 @@
id: fishing_rod
name: fishing rod
-color: "fg=green"
+color: "107"
description: "A simple fishing rod. Use with fishing bait."
value: 5
stackable: false
diff --git a/data/items/fly_fishing_rod.yaml b/data/items/fly_fishing_rod.yaml
index 1531ea1..c0f3f78 100644
--- a/data/items/fly_fishing_rod.yaml
+++ b/data/items/fly_fishing_rod.yaml
@@ -1,6 +1,6 @@
id: fly_fishing_rod
name: fly fishing rod
-color: "fg=green"
+color: "107"
description: "A flexible rod for fly fishing with artificial flies."
value: 20
stackable: false
diff --git a/data/items/gold_ore.yaml b/data/items/gold_ore.yaml
index 1cbf4b2..e45ca03 100644
--- a/data/items/gold_ore.yaml
+++ b/data/items/gold_ore.yaml
@@ -1,6 +1,6 @@
id: gold_ore
name: gold ore
-color: "fg=bright_yellow"
+color: "220"
description: "A glittering chunk of gold ore."
value: 25
stackable: false
diff --git a/data/items/harpoon.yaml b/data/items/harpoon.yaml
index e318469..1412d5c 100644
--- a/data/items/harpoon.yaml
+++ b/data/items/harpoon.yaml
@@ -1,6 +1,6 @@
id: harpoon
name: harpoon
-color: "dim"
+color: "250"
description: "A barbed harpoon for spearing large fish."
value: 30
stackable: false
diff --git a/data/items/herring.yaml b/data/items/herring.yaml
index 5a72528..adbbd95 100644
--- a/data/items/herring.yaml
+++ b/data/items/herring.yaml
@@ -1,6 +1,6 @@
id: herring
name: herring
-color: "fg=bright_red"
+color: "209"
description: "A nicely cooked herring."
value: 7
stackable: false
diff --git a/data/items/iron_axe.yaml b/data/items/iron_axe.yaml
index 17cf6a2..99cfc8b 100644
--- a/data/items/iron_axe.yaml
+++ b/data/items/iron_axe.yaml
@@ -1,6 +1,6 @@
id: iron_axe
name: iron axe
-color: "dim"
+color: "250"
description: "A solid iron axe, better than bronze."
value: 50
stackable: false
diff --git a/data/items/iron_ore.yaml b/data/items/iron_ore.yaml
index 90cddc7..78e82a5 100644
--- a/data/items/iron_ore.yaml
+++ b/data/items/iron_ore.yaml
@@ -1,6 +1,6 @@
id: iron_ore
name: iron ore
-color: "dim"
+color: "250"
description: "A chunk of iron ore, ready for smelting."
value: 5
stackable: false
diff --git a/data/items/iron_pickaxe.yaml b/data/items/iron_pickaxe.yaml
index 4eb9a0e..5682883 100644
--- a/data/items/iron_pickaxe.yaml
+++ b/data/items/iron_pickaxe.yaml
@@ -1,6 +1,6 @@
id: iron_pickaxe
name: iron pickaxe
-color: "dim"
+color: "250"
description: "A solid iron pickaxe, better than bronze."
value: 50
stackable: false
diff --git a/data/items/lighter.yaml b/data/items/lighter.yaml
index 2f3b321..567f42e 100644
--- a/data/items/lighter.yaml
+++ b/data/items/lighter.yaml
@@ -1,6 +1,6 @@
id: lighter
name: lighter
-color: "fg=cyan"
+color: "81"
description: "A reliable butane lighter."
value: 15
stackable: false
diff --git a/data/items/lobster_pot.yaml b/data/items/lobster_pot.yaml
index a91e2b5..79771c1 100644
--- a/data/items/lobster_pot.yaml
+++ b/data/items/lobster_pot.yaml
@@ -1,6 +1,6 @@
id: lobster_pot
name: lobster pot
-color: "dim"
+color: "250"
description: "A wire cage for trapping lobsters."
value: 20
stackable: false
diff --git a/data/items/logs.yaml b/data/items/logs.yaml
index 55d89e9..1395d94 100644
--- a/data/items/logs.yaml
+++ b/data/items/logs.yaml
@@ -1,6 +1,6 @@
id: logs
name: logs
-color: "fg=green"
+color: "107"
description: "Some regular logs, good for firemaking and fletching."
value: 2
stackable: false
diff --git a/data/items/magic_logs.yaml b/data/items/magic_logs.yaml
index 5c1536b..01a804e 100644
--- a/data/items/magic_logs.yaml
+++ b/data/items/magic_logs.yaml
@@ -1,6 +1,6 @@
id: magic_logs
name: magic logs
-color: "fg=magenta"
+color: "135"
description: "Some mystical magic logs."
value: 80
stackable: false
diff --git a/data/items/mahogany_logs.yaml b/data/items/mahogany_logs.yaml
index 9b8d5a3..67b9a6c 100644
--- a/data/items/mahogany_logs.yaml
+++ b/data/items/mahogany_logs.yaml
@@ -1,6 +1,6 @@
id: mahogany_logs
name: mahogany logs
-color: "fg=bright_red"
+color: "131"
description: "Some rich mahogany logs."
value: 20
stackable: false
diff --git a/data/items/maple_logs.yaml b/data/items/maple_logs.yaml
index 3bc4947..6b081e1 100644
--- a/data/items/maple_logs.yaml
+++ b/data/items/maple_logs.yaml
@@ -1,6 +1,6 @@
id: maple_logs
name: maple logs
-color: "fg=bright_red"
+color: "173"
description: "Some smooth maple logs."
value: 16
stackable: false
diff --git a/data/items/matches.yaml b/data/items/matches.yaml
index 495ea19..d816cbc 100644
--- a/data/items/matches.yaml
+++ b/data/items/matches.yaml
@@ -1,6 +1,6 @@
id: matches
name: matches
-color: "fg=bright_red"
+color: "196"
description: "A small box of matches for starting fires."
value: 5
stackable: true
diff --git a/data/items/mithril_axe.yaml b/data/items/mithril_axe.yaml
index 4977957..09f6423 100644
--- a/data/items/mithril_axe.yaml
+++ b/data/items/mithril_axe.yaml
@@ -1,6 +1,6 @@
id: mithril_axe
name: mithril axe
-color: "fg=blue"
+color: "75"
description: "A lightweight mithril axe."
value: 500
stackable: false
diff --git a/data/items/mithril_ore.yaml b/data/items/mithril_ore.yaml
index 42339af..55d8d2c 100644
--- a/data/items/mithril_ore.yaml
+++ b/data/items/mithril_ore.yaml
@@ -1,6 +1,6 @@
id: mithril_ore
name: mithril ore
-color: "fg=blue"
+color: "75"
description: "A piece of light blue mithril ore."
value: 50
stackable: false
diff --git a/data/items/mithril_pickaxe.yaml b/data/items/mithril_pickaxe.yaml
index a72614d..032006c 100644
--- a/data/items/mithril_pickaxe.yaml
+++ b/data/items/mithril_pickaxe.yaml
@@ -1,6 +1,6 @@
id: mithril_pickaxe
name: mithril pickaxe
-color: "fg=blue"
+color: "75"
description: "A lightweight mithril pickaxe."
value: 500
stackable: false
diff --git a/data/items/oak_logs.yaml b/data/items/oak_logs.yaml
index 1176f85..840ebfd 100644
--- a/data/items/oak_logs.yaml
+++ b/data/items/oak_logs.yaml
@@ -1,6 +1,6 @@
id: oak_logs
name: oak logs
-color: "fg=green"
+color: "113"
description: "Some sturdy oak logs."
value: 4
stackable: false
diff --git a/data/items/party_hat_blue.yaml b/data/items/party_hat_blue.yaml
index 5aa5a8b..42d6bb5 100644
--- a/data/items/party_hat_blue.yaml
+++ b/data/items/party_hat_blue.yaml
@@ -1,6 +1,6 @@
id: party_hat_blue
name: blue party hat
-color: "fg=blue"
+color: "69"
description: "A festive blue party hat."
value: 2
equip_slot: head
diff --git a/data/items/party_hat_green.yaml b/data/items/party_hat_green.yaml
index 03273ac..82c3bc7 100644
--- a/data/items/party_hat_green.yaml
+++ b/data/items/party_hat_green.yaml
@@ -1,6 +1,6 @@
id: party_hat_green
name: green party hat
-color: "fg=green"
+color: "83"
description: "A festive green party hat."
value: 2
equip_slot: head
diff --git a/data/items/party_hat_pink.yaml b/data/items/party_hat_pink.yaml
index b4e350a..006e261 100644
--- a/data/items/party_hat_pink.yaml
+++ b/data/items/party_hat_pink.yaml
@@ -1,6 +1,6 @@
id: party_hat_pink
name: pink party hat
-color: "fg=magenta"
+color: "213"
description: "A festive pink party hat."
value: 2
equip_slot: head
diff --git a/data/items/party_hat_red.yaml b/data/items/party_hat_red.yaml
index 48a503a..005b3dd 100644
--- a/data/items/party_hat_red.yaml
+++ b/data/items/party_hat_red.yaml
@@ -1,6 +1,6 @@
id: party_hat_red
name: red party hat
-color: "fg=red"
+color: "196"
description: "A festive red party hat."
value: 2
equip_slot: head
diff --git a/data/items/party_hat_white.yaml b/data/items/party_hat_white.yaml
index 785fac3..c8e8f86 100644
--- a/data/items/party_hat_white.yaml
+++ b/data/items/party_hat_white.yaml
@@ -1,6 +1,6 @@
id: party_hat_white
name: white party hat
-color: "fg=white"
+color: "255"
description: "A festive white party hat."
value: 2
equip_slot: head
diff --git a/data/items/party_hat_yellow.yaml b/data/items/party_hat_yellow.yaml
index 8b7c20e..33a85f8 100644
--- a/data/items/party_hat_yellow.yaml
+++ b/data/items/party_hat_yellow.yaml
@@ -1,6 +1,6 @@
id: party_hat_yellow
name: yellow party hat
-color: "fg=yellow"
+color: "226"
description: "A festive yellow party hat."
value: 2
equip_slot: head
diff --git a/data/items/pass_stub.yaml b/data/items/pass_stub.yaml
index 8cedff1..1068190 100644
--- a/data/items/pass_stub.yaml
+++ b/data/items/pass_stub.yaml
@@ -1,6 +1,6 @@
id: pass_stub
name: pass stub
-color: "fg=yellow"
+color: "222"
description: "A crumpled slip of paper stamped with the guard's seal. Grants one-time access past the gate."
value: 0
stackable: false
diff --git a/data/items/pastry_dough.yaml b/data/items/pastry_dough.yaml
index 200d048..c08c0ae 100644
--- a/data/items/pastry_dough.yaml
+++ b/data/items/pastry_dough.yaml
@@ -1,6 +1,6 @@
id: pastry_dough
name: pastry dough
-color: "fg=bright_yellow"
+color: "222"
description: "A ball of uncooked pastry dough. It needs to be baked."
value: 3
stackable: false
diff --git a/data/items/pizza_dough.yaml b/data/items/pizza_dough.yaml
index 59043ca..1105c60 100644
--- a/data/items/pizza_dough.yaml
+++ b/data/items/pizza_dough.yaml
@@ -1,6 +1,6 @@
id: pizza_dough
name: pizza dough
-color: "fg=bright_yellow"
+color: "222"
description: "A ball of uncooked pizza dough. It needs to be baked."
value: 3
stackable: false
diff --git a/data/items/pot_of_flour.yaml b/data/items/pot_of_flour.yaml
index 100114b..c05c065 100644
--- a/data/items/pot_of_flour.yaml
+++ b/data/items/pot_of_flour.yaml
@@ -1,6 +1,6 @@
id: pot_of_flour
name: pot of flour
-color: "fg=white"
+color: "255"
description: "A clay pot filled with finely ground flour."
value: 5
stackable: false
diff --git a/data/items/raw_anchovies.yaml b/data/items/raw_anchovies.yaml
index 74d8ae5..cf5b75f 100644
--- a/data/items/raw_anchovies.yaml
+++ b/data/items/raw_anchovies.yaml
@@ -1,6 +1,6 @@
id: raw_anchovies
name: raw anchovies
-color: "fg=blue"
+color: "75"
description: "Some tiny raw anchovies."
value: 2
stackable: false
diff --git a/data/items/raw_anglerfish.yaml b/data/items/raw_anglerfish.yaml
index 53a771e..b1c7262 100644
--- a/data/items/raw_anglerfish.yaml
+++ b/data/items/raw_anglerfish.yaml
@@ -1,6 +1,6 @@
id: raw_anglerfish
name: raw anglerfish
-color: "fg=cyan"
+color: "80"
description: "An ugly but delicious raw anglerfish."
value: 20
stackable: false
diff --git a/data/items/raw_bass.yaml b/data/items/raw_bass.yaml
index 766f141..34dddc1 100644
--- a/data/items/raw_bass.yaml
+++ b/data/items/raw_bass.yaml
@@ -1,6 +1,6 @@
id: raw_bass
name: raw bass
-color: "fg=blue"
+color: "69"
description: "A freshly caught raw bass."
value: 10
stackable: false
diff --git a/data/items/raw_cave_eel.yaml b/data/items/raw_cave_eel.yaml
index 81c2e1a..000f93c 100644
--- a/data/items/raw_cave_eel.yaml
+++ b/data/items/raw_cave_eel.yaml
@@ -1,6 +1,6 @@
id: raw_cave_eel
name: raw cave eel
-color: "fg=cyan"
+color: "80"
description: "A pale eel from deep underground waters."
value: 12
stackable: false
diff --git a/data/items/raw_cod.yaml b/data/items/raw_cod.yaml
index 3124df3..fe94f35 100644
--- a/data/items/raw_cod.yaml
+++ b/data/items/raw_cod.yaml
@@ -1,6 +1,6 @@
id: raw_cod
name: raw cod
-color: "fg=blue"
+color: "69"
description: "A freshly caught raw cod."
value: 6
stackable: false
diff --git a/data/items/raw_herring.yaml b/data/items/raw_herring.yaml
index fd8f9c0..730eca5 100644
--- a/data/items/raw_herring.yaml
+++ b/data/items/raw_herring.yaml
@@ -1,6 +1,6 @@
id: raw_herring
name: raw herring
-color: "fg=blue"
+color: "75"
description: "A freshly caught raw herring."
value: 5
stackable: false
diff --git a/data/items/raw_lobster.yaml b/data/items/raw_lobster.yaml
index 71983f8..908c931 100644
--- a/data/items/raw_lobster.yaml
+++ b/data/items/raw_lobster.yaml
@@ -1,6 +1,6 @@
id: raw_lobster
name: raw lobster
-color: "fg=blue"
+color: "33"
description: "A freshly caught raw lobster."
value: 15
stackable: false
diff --git a/data/items/raw_mackerel.yaml b/data/items/raw_mackerel.yaml
index 71f7bac..42e1d09 100644
--- a/data/items/raw_mackerel.yaml
+++ b/data/items/raw_mackerel.yaml
@@ -1,6 +1,6 @@
id: raw_mackerel
name: raw mackerel
-color: "fg=blue"
+color: "69"
description: "A freshly caught raw mackerel."
value: 4
stackable: false
diff --git a/data/items/raw_pike.yaml b/data/items/raw_pike.yaml
index f4eaf51..625c019 100644
--- a/data/items/raw_pike.yaml
+++ b/data/items/raw_pike.yaml
@@ -1,6 +1,6 @@
id: raw_pike
name: raw pike
-color: "fg=blue"
+color: "69"
description: "A freshly caught raw pike."
value: 8
stackable: false
diff --git a/data/items/raw_rainbow_fish.yaml b/data/items/raw_rainbow_fish.yaml
index c86c4e7..311c9f1 100644
--- a/data/items/raw_rainbow_fish.yaml
+++ b/data/items/raw_rainbow_fish.yaml
@@ -1,6 +1,6 @@
id: raw_rainbow_fish
name: raw rainbow fish
-color: "fg=magenta"
+color: "177"
description: "A shimmering raw rainbow fish."
value: 10
stackable: false
diff --git a/data/items/raw_salmon.yaml b/data/items/raw_salmon.yaml
index abf00a3..d5ae9bd 100644
--- a/data/items/raw_salmon.yaml
+++ b/data/items/raw_salmon.yaml
@@ -1,6 +1,6 @@
id: raw_salmon
name: raw salmon
-color: "fg=blue"
+color: "69"
description: "A freshly caught raw salmon."
value: 8
stackable: false
diff --git a/data/items/raw_sardine.yaml b/data/items/raw_sardine.yaml
index c46bebb..d27a155 100644
--- a/data/items/raw_sardine.yaml
+++ b/data/items/raw_sardine.yaml
@@ -1,6 +1,6 @@
id: raw_sardine
name: raw sardine
-color: "fg=blue"
+color: "75"
description: "A freshly caught raw sardine."
value: 3
stackable: false
diff --git a/data/items/raw_shark.yaml b/data/items/raw_shark.yaml
index 90a3436..999ccb9 100644
--- a/data/items/raw_shark.yaml
+++ b/data/items/raw_shark.yaml
@@ -1,6 +1,6 @@
id: raw_shark
name: raw shark
-color: "fg=blue"
+color: "27"
description: "A freshly caught raw shark."
value: 30
stackable: false
diff --git a/data/items/raw_shrimps.yaml b/data/items/raw_shrimps.yaml
index 0eb919b..1127fda 100644
--- a/data/items/raw_shrimps.yaml
+++ b/data/items/raw_shrimps.yaml
@@ -1,6 +1,6 @@
id: raw_shrimps
name: raw shrimps
-color: "fg=blue"
+color: "75"
description: "Some small raw shrimps."
value: 2
stackable: false
diff --git a/data/items/raw_slimy_eel.yaml b/data/items/raw_slimy_eel.yaml
index 8010db1..40820e5 100644
--- a/data/items/raw_slimy_eel.yaml
+++ b/data/items/raw_slimy_eel.yaml
@@ -1,6 +1,6 @@
id: raw_slimy_eel
name: raw slimy eel
-color: "fg=green"
+color: "108"
description: "A slippery raw eel covered in slime."
value: 10
stackable: false
diff --git a/data/items/raw_swordfish.yaml b/data/items/raw_swordfish.yaml
index a0a5a73..e162460 100644
--- a/data/items/raw_swordfish.yaml
+++ b/data/items/raw_swordfish.yaml
@@ -1,6 +1,6 @@
id: raw_swordfish
name: raw swordfish
-color: "fg=blue"
+color: "33"
description: "A freshly caught raw swordfish."
value: 18
stackable: false
diff --git a/data/items/raw_trout.yaml b/data/items/raw_trout.yaml
index 248f6c1..c1d98aa 100644
--- a/data/items/raw_trout.yaml
+++ b/data/items/raw_trout.yaml
@@ -1,6 +1,6 @@
id: raw_trout
name: raw trout
-color: "fg=blue"
+color: "69"
description: "A freshly caught trout. Needs cooking."
value: 3
stackable: false
diff --git a/data/items/raw_tuna.yaml b/data/items/raw_tuna.yaml
index 1fdfcb8..6079f92 100644
--- a/data/items/raw_tuna.yaml
+++ b/data/items/raw_tuna.yaml
@@ -1,6 +1,6 @@
id: raw_tuna
name: raw tuna
-color: "fg=blue"
+color: "33"
description: "A freshly caught raw tuna."
value: 12
stackable: false
diff --git a/data/items/redwood_logs.yaml b/data/items/redwood_logs.yaml
index 25bc3d1..5ea1ab0 100644
--- a/data/items/redwood_logs.yaml
+++ b/data/items/redwood_logs.yaml
@@ -1,6 +1,6 @@
id: redwood_logs
name: redwood logs
-color: "fg=bright_red"
+color: "131"
description: "Some rare redwood logs."
value: 100
stackable: false
diff --git a/data/items/rune_axe.yaml b/data/items/rune_axe.yaml
index 8040e0b..da9b54f 100644
--- a/data/items/rune_axe.yaml
+++ b/data/items/rune_axe.yaml
@@ -1,6 +1,6 @@
id: rune_axe
name: rune axe
-color: "fg=cyan"
+color: "87"
description: "An extremely sharp rune axe."
value: 40000
stackable: false
diff --git a/data/items/rune_pickaxe.yaml b/data/items/rune_pickaxe.yaml
index 4d8c967..bdb69f0 100644
--- a/data/items/rune_pickaxe.yaml
+++ b/data/items/rune_pickaxe.yaml
@@ -1,6 +1,6 @@
id: rune_pickaxe
name: rune pickaxe
-color: "fg=cyan"
+color: "87"
description: "An extremely durable rune pickaxe."
value: 40000
stackable: false
diff --git a/data/items/runite_ore.yaml b/data/items/runite_ore.yaml
index 8b9d288..05d3248 100644
--- a/data/items/runite_ore.yaml
+++ b/data/items/runite_ore.yaml
@@ -1,6 +1,6 @@
id: runite_ore
name: runite ore
-color: "fg=cyan"
+color: "87"
description: "A rare piece of light blue runite ore."
value: 500
stackable: false
diff --git a/data/items/sacred_eel.yaml b/data/items/sacred_eel.yaml
index e0ec18e..9a03940 100644
--- a/data/items/sacred_eel.yaml
+++ b/data/items/sacred_eel.yaml
@@ -1,6 +1,6 @@
id: sacred_eel
name: sacred eel
-color: "fg=magenta"
+color: "177"
description: "A glowing eel imbued with sacred energy."
value: 25
stackable: false
diff --git a/data/items/salmon.yaml b/data/items/salmon.yaml
index 307d4ae..59b8237 100644
--- a/data/items/salmon.yaml
+++ b/data/items/salmon.yaml
@@ -1,6 +1,6 @@
id: salmon
name: salmon
-color: "fg=bright_red"
+color: "209"
description: "A nicely cooked salmon."
value: 10
stackable: false
diff --git a/data/items/sardine.yaml b/data/items/sardine.yaml
index 8414553..c11b42b 100644
--- a/data/items/sardine.yaml
+++ b/data/items/sardine.yaml
@@ -1,6 +1,6 @@
id: sardine
name: sardine
-color: "fg=bright_red"
+color: "209"
description: "A nicely cooked sardine."
value: 5
stackable: false
diff --git a/data/items/scrap_metal.yaml b/data/items/scrap_metal.yaml
index 81e068d..d9b805e 100644
--- a/data/items/scrap_metal.yaml
+++ b/data/items/scrap_metal.yaml
@@ -1,6 +1,6 @@
id: scrap_metal
name: scrap metal
-color: "dim"
+color: "243"
description: "A chunk of salvaged scrap metal."
value: 2
stackable: false
diff --git a/data/items/shrimps.yaml b/data/items/shrimps.yaml
index c7b47b4..9c280c8 100644
--- a/data/items/shrimps.yaml
+++ b/data/items/shrimps.yaml
@@ -1,6 +1,6 @@
id: shrimps
name: shrimps
-color: "fg=bright_red"
+color: "209"
description: "Some nicely cooked shrimps."
value: 5
stackable: false
diff --git a/data/items/silver_ore.yaml b/data/items/silver_ore.yaml
index c8a6b73..5b40ff0 100644
--- a/data/items/silver_ore.yaml
+++ b/data/items/silver_ore.yaml
@@ -1,6 +1,6 @@
id: silver_ore
name: silver ore
-color: "fg=white"
+color: "253"
description: "A piece of silver ore with a faint metallic sheen."
value: 10
stackable: false
diff --git a/data/items/small_fishing_net.yaml b/data/items/small_fishing_net.yaml
index f497a55..9e91f8f 100644
--- a/data/items/small_fishing_net.yaml
+++ b/data/items/small_fishing_net.yaml
@@ -1,6 +1,6 @@
id: small_fishing_net
name: small fishing net
-color: "fg=white"
+color: "230"
description: "A small hand-held fishing net."
value: 5
stackable: false
diff --git a/data/items/steel_axe.yaml b/data/items/steel_axe.yaml
index 240c921..5f03609 100644
--- a/data/items/steel_axe.yaml
+++ b/data/items/steel_axe.yaml
@@ -1,6 +1,6 @@
id: steel_axe
name: steel axe
-color: "fg=white"
+color: "253"
description: "A sharp steel axe for serious woodcutting."
value: 200
stackable: false
diff --git a/data/items/steel_pickaxe.yaml b/data/items/steel_pickaxe.yaml
index bff14ed..466ce0f 100644
--- a/data/items/steel_pickaxe.yaml
+++ b/data/items/steel_pickaxe.yaml
@@ -1,6 +1,6 @@
id: steel_pickaxe
name: steel pickaxe
-color: "fg=white"
+color: "253"
description: "A sharp steel pickaxe."
value: 200
stackable: false
diff --git a/data/items/teak_logs.yaml b/data/items/teak_logs.yaml
index 1f49678..c69f807 100644
--- a/data/items/teak_logs.yaml
+++ b/data/items/teak_logs.yaml
@@ -1,6 +1,6 @@
id: teak_logs
name: teak logs
-color: "fg=bright_yellow"
+color: "179"
description: "Some solid teak logs."
value: 12
stackable: false
diff --git a/data/items/tin_ore.yaml b/data/items/tin_ore.yaml
index 7e31d8c..0ae1cd0 100644
--- a/data/items/tin_ore.yaml
+++ b/data/items/tin_ore.yaml
@@ -1,6 +1,6 @@
id: tin_ore
name: tin ore
-color: "fg=white"
+color: "253"
description: "A lump of tin ore, ready for smelting."
value: 3
stackable: false
diff --git a/data/items/trout.yaml b/data/items/trout.yaml
index 65486cb..df6cdcc 100644
--- a/data/items/trout.yaml
+++ b/data/items/trout.yaml
@@ -1,6 +1,6 @@
id: trout
name: trout
-color: "fg=bright_red"
+color: "209"
description: "A nicely cooked trout."
value: 8
stackable: false
diff --git a/data/items/uncut_diamond.yaml b/data/items/uncut_diamond.yaml
index 1bad26c..ce7dd3e 100644
--- a/data/items/uncut_diamond.yaml
+++ b/data/items/uncut_diamond.yaml
@@ -1,6 +1,6 @@
id: uncut_diamond
name: uncut diamond
-color: "fg=white"
+color: "255"
description: "An uncut diamond. Can be cut with crafting."
value: 200
stackable: false
diff --git a/data/items/uncut_emerald.yaml b/data/items/uncut_emerald.yaml
index e97d180..71da287 100644
--- a/data/items/uncut_emerald.yaml
+++ b/data/items/uncut_emerald.yaml
@@ -1,6 +1,6 @@
id: uncut_emerald
name: uncut emerald
-color: "fg=green"
+color: "83"
description: "An uncut emerald. Can be cut with crafting."
value: 50
stackable: false
diff --git a/data/items/uncut_ruby.yaml b/data/items/uncut_ruby.yaml
index 3d45d5f..2bd2ec5 100644
--- a/data/items/uncut_ruby.yaml
+++ b/data/items/uncut_ruby.yaml
@@ -1,6 +1,6 @@
id: uncut_ruby
name: uncut ruby
-color: "fg=bright_red"
+color: "196"
description: "An uncut ruby. Can be cut with crafting."
value: 100
stackable: false
diff --git a/data/items/uncut_sapphire.yaml b/data/items/uncut_sapphire.yaml
index 4d50cb3..4d2ba91 100644
--- a/data/items/uncut_sapphire.yaml
+++ b/data/items/uncut_sapphire.yaml
@@ -1,6 +1,6 @@
id: uncut_sapphire
name: uncut sapphire
-color: "fg=blue"
+color: "69"
description: "An uncut sapphire. Can be cut with crafting."
value: 25
stackable: false
diff --git a/data/items/willow_logs.yaml b/data/items/willow_logs.yaml
index 1461ff2..dbd315b 100644
--- a/data/items/willow_logs.yaml
+++ b/data/items/willow_logs.yaml
@@ -1,6 +1,6 @@
id: willow_logs
name: willow logs
-color: "fg=green"
+color: "114"
description: "Some flexible willow logs."
value: 8
stackable: false
diff --git a/data/items/yew_logs.yaml b/data/items/yew_logs.yaml
index 557ca1d..a5a86a2 100644
--- a/data/items/yew_logs.yaml
+++ b/data/items/yew_logs.yaml
@@ -1,6 +1,6 @@
id: yew_logs
name: yew logs
-color: "fg=green"
+color: "108"
description: "Some high-quality yew logs."
value: 40
stackable: false
diff --git a/data/objects/adamantite_rock.yaml b/data/objects/adamantite_rock.yaml
index 70b7b24..04f2f0c 100644
--- a/data/objects/adamantite_rock.yaml
+++ b/data/objects/adamantite_rock.yaml
@@ -1,4 +1,4 @@
id: adamantite_rock
name: adamantite rock
-color: "fg=green"
+color: "120"
behavior: mine_adamantite
diff --git a/data/objects/anglerfish_spot.yaml b/data/objects/anglerfish_spot.yaml
index dfe244e..034816c 100644
--- a/data/objects/anglerfish_spot.yaml
+++ b/data/objects/anglerfish_spot.yaml
@@ -1,6 +1,6 @@
id: anglerfish_spot
name: anglerfish spot
-color: "fg=cyan"
+color: "81"
behavior: fish_angler
props:
description: "A faint glow emanates from the depths. Use a fishing rod with bait here."
diff --git a/data/objects/bait_fishing_spot.yaml b/data/objects/bait_fishing_spot.yaml
index 30925f6..64b0a98 100644
--- a/data/objects/bait_fishing_spot.yaml
+++ b/data/objects/bait_fishing_spot.yaml
@@ -1,6 +1,6 @@
id: bait_fishing_spot
name: bait fishing spot
-color: "fg=blue"
+color: "75"
behavior: fish_bait
props:
description: "Small fish ripple the surface. Use a fishing rod with bait here."
diff --git a/data/objects/cage_fishing_spot.yaml b/data/objects/cage_fishing_spot.yaml
index ac40567..ff6d24b 100644
--- a/data/objects/cage_fishing_spot.yaml
+++ b/data/objects/cage_fishing_spot.yaml
@@ -1,6 +1,6 @@
id: cage_fishing_spot
name: cage fishing spot
-color: "fg=blue"
+color: "75"
behavior: fish_cage
props:
description: "A rocky underwater shelf. Use a lobster pot here."
diff --git a/data/objects/clay_rock.yaml b/data/objects/clay_rock.yaml
index 1c9f6c5..dd87eba 100644
--- a/data/objects/clay_rock.yaml
+++ b/data/objects/clay_rock.yaml
@@ -1,4 +1,4 @@
id: clay_rock
name: clay deposit
-color: "fg=bright_red"
+color: "173"
behavior: mine_clay
diff --git a/data/objects/coal_rock.yaml b/data/objects/coal_rock.yaml
index 57831f5..5bccc0e 100644
--- a/data/objects/coal_rock.yaml
+++ b/data/objects/coal_rock.yaml
@@ -1,4 +1,4 @@
id: coal_rock
name: coal rock
-color: "dim"
+color: "240"
behavior: mine_coal
diff --git a/data/objects/cooking_range.yaml b/data/objects/cooking_range.yaml
index caa52c5..aa7d1b3 100644
--- a/data/objects/cooking_range.yaml
+++ b/data/objects/cooking_range.yaml
@@ -1,5 +1,5 @@
id: cooking_range
name: cooking range
-color: "fg=white"
+color: "253"
props:
description: "A sturdy iron range with multiple burners. Perfect for cooking."
diff --git a/data/objects/copper_rock.yaml b/data/objects/copper_rock.yaml
index 7b027c8..0162181 100644
--- a/data/objects/copper_rock.yaml
+++ b/data/objects/copper_rock.yaml
@@ -1,4 +1,4 @@
id: copper_rock
name: copper rock
-color: "fg=bright_yellow"
+color: "178"
behavior: mine_copper
diff --git a/data/objects/fire.yaml b/data/objects/fire.yaml
index 50cc98c..5a036a8 100644
--- a/data/objects/fire.yaml
+++ b/data/objects/fire.yaml
@@ -1,6 +1,6 @@
id: fire
name: fire
-color: "fg=bright_yellow"
+color: "208"
behavior: ""
hidden: false
inroom_description: "A fire is burning warmly."
diff --git a/data/objects/fly_fishing_spot.yaml b/data/objects/fly_fishing_spot.yaml
index 18fe6f8..59a19ce 100644
--- a/data/objects/fly_fishing_spot.yaml
+++ b/data/objects/fly_fishing_spot.yaml
@@ -1,6 +1,6 @@
id: fly_fishing_spot
name: fly fishing spot
-color: "fg=blue"
+color: "75"
behavior: fish_fly
props:
description: "Insects dance above the water. Use a fly fishing rod with feathers here."
diff --git a/data/objects/gold_rock.yaml b/data/objects/gold_rock.yaml
index 566e828..d57be8f 100644
--- a/data/objects/gold_rock.yaml
+++ b/data/objects/gold_rock.yaml
@@ -1,4 +1,4 @@
id: gold_rock
name: gold rock
-color: "fg=bright_yellow"
+color: "220"
behavior: mine_gold
diff --git a/data/objects/harpoon_fishing_spot.yaml b/data/objects/harpoon_fishing_spot.yaml
index bd29de9..23040b9 100644
--- a/data/objects/harpoon_fishing_spot.yaml
+++ b/data/objects/harpoon_fishing_spot.yaml
@@ -1,6 +1,6 @@
id: harpoon_fishing_spot
name: harpoon fishing spot
-color: "fg=blue"
+color: "75"
behavior: fish_harpoon
props:
description: "Large shadows move beneath the waves. Use a harpoon here."
diff --git a/data/objects/iron_gate.yaml b/data/objects/iron_gate.yaml
index eba87be..a1e7de5 100644
--- a/data/objects/iron_gate.yaml
+++ b/data/objects/iron_gate.yaml
@@ -1,6 +1,6 @@
id: iron_gate
name: iron gate
-color: "dim"
+color: "250"
behavior: iron_gate_toggle
hidden: true
props:
diff --git a/data/objects/iron_rock.yaml b/data/objects/iron_rock.yaml
index 4875b78..3019895 100644
--- a/data/objects/iron_rock.yaml
+++ b/data/objects/iron_rock.yaml
@@ -1,4 +1,4 @@
id: iron_rock
name: iron rock
-color: "dim"
+color: "250"
behavior: mine_iron
diff --git a/data/objects/lumby_fountain.yaml b/data/objects/lumby_fountain.yaml
index 29c79ea..ff62717 100644
--- a/data/objects/lumby_fountain.yaml
+++ b/data/objects/lumby_fountain.yaml
@@ -1,5 +1,5 @@
id: lumby_fountain
name: town fountain
-color: "fg=cyan"
+color: "81"
props:
description: "Clear water sparkles in the sunlight."
diff --git a/data/objects/magic_tree.yaml b/data/objects/magic_tree.yaml
index 4dd8140..6caaf49 100644
--- a/data/objects/magic_tree.yaml
+++ b/data/objects/magic_tree.yaml
@@ -1,4 +1,4 @@
id: magic_tree
name: magic tree
-color: "fg=magenta"
+color: "135"
behavior: chop_magic
diff --git a/data/objects/mahogany_tree.yaml b/data/objects/mahogany_tree.yaml
index 9e74031..9813b5c 100644
--- a/data/objects/mahogany_tree.yaml
+++ b/data/objects/mahogany_tree.yaml
@@ -1,4 +1,4 @@
id: mahogany_tree
name: mahogany tree
-color: "fg=bright_red"
+color: "131"
behavior: chop_mahogany
diff --git a/data/objects/maple_tree.yaml b/data/objects/maple_tree.yaml
index 9ad8cf0..a2ac67a 100644
--- a/data/objects/maple_tree.yaml
+++ b/data/objects/maple_tree.yaml
@@ -1,4 +1,4 @@
id: maple_tree
name: maple tree
-color: "fg=bright_red"
+color: "173"
behavior: chop_maple
diff --git a/data/objects/mithril_rock.yaml b/data/objects/mithril_rock.yaml
index 6ca5975..d4f4858 100644
--- a/data/objects/mithril_rock.yaml
+++ b/data/objects/mithril_rock.yaml
@@ -1,4 +1,4 @@
id: mithril_rock
name: mithril rock
-color: "fg=blue"
+color: "75"
behavior: mine_mithril
diff --git a/data/objects/net_fishing_spot.yaml b/data/objects/net_fishing_spot.yaml
index 6dda919..287c290 100644
--- a/data/objects/net_fishing_spot.yaml
+++ b/data/objects/net_fishing_spot.yaml
@@ -1,6 +1,6 @@
id: net_fishing_spot
name: net fishing spot
-color: "fg=blue"
+color: "75"
behavior: fish_net
props:
description: "Bubbles break the surface. Use a small or big fishing net here."
diff --git a/data/objects/oak_tree.yaml b/data/objects/oak_tree.yaml
index f733739..2ad1ed2 100644
--- a/data/objects/oak_tree.yaml
+++ b/data/objects/oak_tree.yaml
@@ -1,4 +1,4 @@
id: oak_tree
name: oak tree
-color: "fg=green"
+color: "113"
behavior: chop_oak
diff --git a/data/objects/redwood_tree.yaml b/data/objects/redwood_tree.yaml
index fcb8296..541095a 100644
--- a/data/objects/redwood_tree.yaml
+++ b/data/objects/redwood_tree.yaml
@@ -1,4 +1,4 @@
id: redwood_tree
name: redwood tree
-color: "fg=bright_red"
+color: "131"
behavior: chop_redwood
diff --git a/data/objects/runite_rock.yaml b/data/objects/runite_rock.yaml
index d8c793a..2475908 100644
--- a/data/objects/runite_rock.yaml
+++ b/data/objects/runite_rock.yaml
@@ -1,4 +1,4 @@
id: runite_rock
name: runite rock
-color: "fg=cyan"
+color: "87"
behavior: mine_runite
diff --git a/data/objects/sacred_eel_spot.yaml b/data/objects/sacred_eel_spot.yaml
index 772a201..449f8ea 100644
--- a/data/objects/sacred_eel_spot.yaml
+++ b/data/objects/sacred_eel_spot.yaml
@@ -1,6 +1,6 @@
id: sacred_eel_spot
name: sacred eel spot
-color: "fg=magenta"
+color: "177"
behavior: fish_sacred_eel
props:
description: "Waters shimmer with an otherworldly light. Use a fishing rod with bait here."
diff --git a/data/objects/scrap_pile.yaml b/data/objects/scrap_pile.yaml
index 0aa518e..2106822 100644
--- a/data/objects/scrap_pile.yaml
+++ b/data/objects/scrap_pile.yaml
@@ -1,4 +1,4 @@
id: scrap_pile
name: scrap pile
-color: "dim"
+color: "243"
behavior: mine_scrap
diff --git a/data/objects/silver_rock.yaml b/data/objects/silver_rock.yaml
index 0b4d646..702df59 100644
--- a/data/objects/silver_rock.yaml
+++ b/data/objects/silver_rock.yaml
@@ -1,4 +1,4 @@
id: silver_rock
name: silver rock
-color: "fg=white"
+color: "253"
behavior: mine_silver
diff --git a/data/objects/swamp_fishing_spot.yaml b/data/objects/swamp_fishing_spot.yaml
index 3605be3..bcbeb98 100644
--- a/data/objects/swamp_fishing_spot.yaml
+++ b/data/objects/swamp_fishing_spot.yaml
@@ -1,6 +1,6 @@
id: swamp_fishing_spot
name: swamp fishing spot
-color: "fg=green"
+color: "108"
behavior: fish_swamp
props:
description: "Murky water bubbles ominously. Use a fishing rod with bait here."
diff --git a/data/objects/teak_tree.yaml b/data/objects/teak_tree.yaml
index 801ebd1..a680c10 100644
--- a/data/objects/teak_tree.yaml
+++ b/data/objects/teak_tree.yaml
@@ -1,4 +1,4 @@
id: teak_tree
name: teak tree
-color: "fg=bright_yellow"
+color: "179"
behavior: chop_teak
diff --git a/data/objects/tin_rock.yaml b/data/objects/tin_rock.yaml
index a65d66d..d0969fb 100644
--- a/data/objects/tin_rock.yaml
+++ b/data/objects/tin_rock.yaml
@@ -1,4 +1,4 @@
id: tin_rock
name: tin rock
-color: "fg=white"
+color: "253"
behavior: mine_tin
diff --git a/data/objects/tree.yaml b/data/objects/tree.yaml
index d73ff7f..4f799e8 100644
--- a/data/objects/tree.yaml
+++ b/data/objects/tree.yaml
@@ -1,4 +1,4 @@
id: tree
name: tree
-color: "fg=green"
+color: "107"
behavior: chop_tree
diff --git a/data/objects/willow_tree.yaml b/data/objects/willow_tree.yaml
index 33221bd..01cc346 100644
--- a/data/objects/willow_tree.yaml
+++ b/data/objects/willow_tree.yaml
@@ -1,4 +1,4 @@
id: willow_tree
name: willow tree
-color: "fg=green"
+color: "114"
behavior: chop_willow
diff --git a/data/objects/yew_tree.yaml b/data/objects/yew_tree.yaml
index c43f06f..cb81172 100644
--- a/data/objects/yew_tree.yaml
+++ b/data/objects/yew_tree.yaml
@@ -1,4 +1,4 @@
id: yew_tree
name: yew tree
-color: "fg=green"
+color: "108"
behavior: chop_yew
diff --git a/go.mod b/go.mod
index efc3c82..e6144f8 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module thirdcollapse
+module thehouseoficarus
go 1.25.0
diff --git a/internal/color/color.go b/internal/color/color.go
index 4ef66b8..9496a1d 100644
--- a/internal/color/color.go
+++ b/internal/color/color.go
@@ -2,185 +2,420 @@ package color
import (
"fmt"
+ "math"
+ "regexp"
+ "strconv"
"strings"
)
const Reset = "\033[0m"
-var ansiFg = map[string]int{
- "black": 30,
- "red": 31,
- "green": 32,
- "yellow": 33,
- "blue": 34,
- "magenta": 35,
- "cyan": 36,
- "white": 37,
-}
-
-var ansiBg = map[string]int{
- "black": 40,
- "red": 41,
- "green": 42,
- "yellow": 43,
- "blue": 44,
- "magenta": 45,
- "cyan": 46,
- "white": 47,
-}
-
-var ansiBrightFg = map[string]int{
- "bright_black": 90,
- "bright_red": 91,
- "bright_green": 92,
- "bright_yellow": 93,
- "bright_blue": 94,
- "bright_magenta": 95,
- "bright_cyan": 96,
- "bright_white": 97,
-}
-
-var ansiBrightBg = map[string]int{
- "bright_black": 100,
- "bright_red": 101,
- "bright_green": 102,
- "bright_yellow": 103,
- "bright_blue": 104,
- "bright_magenta": 105,
- "bright_cyan": 106,
- "bright_white": 107,
-}
-
-var xtermFg = map[string]int{
- "black": 0,
- "red": 1,
- "green": 2,
- "yellow": 3,
- "blue": 4,
- "magenta": 5,
- "cyan": 6,
- "white": 7,
- "bright_black": 8,
- "bright_red": 9,
- "bright_green": 10,
- "bright_yellow": 11,
- "bright_blue": 12,
- "bright_magenta": 13,
- "bright_cyan": 14,
- "bright_white": 15,
-}
-
-var ansiStyles = map[string]string{
- "bold": "\033[1m",
- "dim": "\033[2m",
- "italic": "\033[3m",
- "underline": "\033[4m",
-}
-
-func Fg(mode, name string) string {
- if mode == "none" || mode == "" {
- return ""
+var ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
+
+var ansi16RGB = [16][3]int{
+ {0, 0, 0}, // 0 black
+ {170, 0, 0}, // 1 red
+ {0, 170, 0}, // 2 green
+ {170, 170, 0}, // 3 yellow
+ {0, 0, 170}, // 4 blue
+ {170, 0, 170}, // 5 magenta
+ {0, 170, 170}, // 6 cyan
+ {170, 170, 170}, // 7 white
+ {85, 85, 85}, // 8 bright black
+ {255, 85, 85}, // 9 bright red
+ {85, 255, 85}, // 10 bright green
+ {255, 255, 85}, // 11 bright yellow
+ {85, 85, 255}, // 12 bright blue
+ {255, 85, 255}, // 13 bright magenta
+ {85, 255, 255}, // 14 bright cyan
+ {255, 255, 255}, // 15 bright white
+}
+
+var cubeValues = [6]int{0, 95, 135, 175, 215, 255}
+
+func Xterm256ToRGB(index int) (int, int, int) {
+ if index < 16 {
+ return ansi16RGB[index][0], ansi16RGB[index][1], ansi16RGB[index][2]
}
- if mode == "xterm256" {
- if c, ok := xtermFg[name]; ok {
- return fmt.Sprintf("\033[38;5;%dm", c)
+ if index < 232 {
+ i := index - 16
+ b := cubeValues[i%6]
+ g := cubeValues[(i/6)%6]
+ r := cubeValues[(i/36)%6]
+ return r, g, b
+ }
+ v := 8 + (index-232)*10
+ return v, v, v
+}
+
+func colorDist(r1, g1, b1, r2, g2, b2 int) int {
+ dr := r1 - r2
+ dg := g1 - g2
+ db := b1 - b2
+ return dr*dr + dg*dg + db*db
+}
+
+func nearestANSI(index int) int {
+ if index < 16 {
+ if index < 8 {
+ return 30 + index
}
- return ""
+ return 90 + (index - 8)
}
- if c, ok := ansiFg[name]; ok {
- return fmt.Sprintf("\033[%dm", c)
+ r, g, b := Xterm256ToRGB(index)
+ bestDist := math.MaxInt
+ bestCode := 37
+ for i := 0; i < 16; i++ {
+ dist := colorDist(r, g, b, ansi16RGB[i][0], ansi16RGB[i][1], ansi16RGB[i][2])
+ if dist < bestDist {
+ bestDist = dist
+ if i < 8 {
+ bestCode = 30 + i
+ } else {
+ bestCode = 90 + (i - 8)
+ }
+ }
}
- if c, ok := ansiBrightFg[name]; ok {
- return fmt.Sprintf("\033[%dm", c)
+ return bestCode
+}
+
+func nearestANSIBg(index int) int {
+ fg := nearestANSI(index)
+ if fg >= 90 {
+ return fg - 90 + 100
}
- return ""
+ return fg - 30 + 40
+}
+
+func nearestCubeComponent(v int) int {
+ if v < 48 {
+ return 0
+ }
+ if v < 115 {
+ return 1
+ }
+ if v < 155 {
+ return 2
+ }
+ if v < 195 {
+ return 3
+ }
+ if v < 235 {
+ return 4
+ }
+ return 5
}
-func Bg(mode, name string) string {
- if mode == "none" || mode == "" {
+func NearestXterm256(r, g, b int) int {
+ ri := nearestCubeComponent(r)
+ gi := nearestCubeComponent(g)
+ bi := nearestCubeComponent(b)
+ cubeIdx := 16 + ri*36 + gi*6 + bi
+ cubeDist := colorDist(r, g, b, cubeValues[ri], cubeValues[gi], cubeValues[bi])
+
+ avg := (r + g + b) / 3
+ grayStep := (avg - 8 + 5) / 10
+ if grayStep < 0 {
+ grayStep = 0
+ }
+ if grayStep > 23 {
+ grayStep = 23
+ }
+ grayIdx := 232 + grayStep
+ gv := 8 + grayStep*10
+ grayDist := colorDist(r, g, b, gv, gv, gv)
+
+ bestIdx := cubeIdx
+ bestDist := cubeDist
+ if grayDist < bestDist {
+ bestIdx = grayIdx
+ bestDist = grayDist
+ }
+
+ for i := 0; i < 16; i++ {
+ d := colorDist(r, g, b, ansi16RGB[i][0], ansi16RGB[i][1], ansi16RGB[i][2])
+ if d < bestDist {
+ bestDist = d
+ bestIdx = i
+ }
+ }
+
+ return bestIdx
+}
+
+func FgCode(mode string, index int) string {
+ if mode == "none" || mode == "" || index < 0 || index > 255 {
return ""
}
if mode == "xterm256" {
- if c, ok := xtermFg[name]; ok {
- return fmt.Sprintf("\033[48;5;%dm", c)
- }
+ return fmt.Sprintf("\033[38;5;%dm", index)
+ }
+ return fmt.Sprintf("\033[%dm", nearestANSI(index))
+}
+
+func BgCode(mode string, index int) string {
+ if mode == "none" || mode == "" || index < 0 || index > 255 {
return ""
}
- if c, ok := ansiBg[name]; ok {
- return fmt.Sprintf("\033[%dm", c)
+ if mode == "xterm256" {
+ return fmt.Sprintf("\033[48;5;%dm", index)
}
- if c, ok := ansiBrightBg[name]; ok {
- return fmt.Sprintf("\033[%dm", c)
+ return fmt.Sprintf("\033[%dm", nearestANSIBg(index))
+}
+
+func StyleCode(name string) string {
+ switch name {
+ case "bold":
+ return "\033[1m"
+ case "dim":
+ return "\033[2m"
+ case "underline":
+ return "\033[4m"
}
return ""
}
-func Style(name string) string {
- if s, ok := ansiStyles[name]; ok {
- return s
+func VisibleLen(s string) int {
+ return len(ansiRe.ReplaceAllString(s, ""))
+}
+
+func ContrastFg(mode string, bgIndex int) string {
+ r, g, b := Xterm256ToRGB(bgIndex)
+ if r+g+b > 384 {
+ return FgCode(mode, 0)
}
- return ""
+ return FgCode(mode, 15)
}
type ColorSpec struct {
- Fg string
- Bg string
+ Fg int
+ Bg int
Bold bool
Dim bool
- Italic bool
Underline bool
+ Gradient []int
+}
+
+func NoColor() ColorSpec {
+ return ColorSpec{Fg: -1, Bg: -1}
}
func Parse(input string) ColorSpec {
- var spec ColorSpec
+ spec := ColorSpec{Fg: -1, Bg: -1}
for _, token := range strings.Fields(input) {
switch {
case token == "bold":
spec.Bold = true
case token == "dim":
spec.Dim = true
- case token == "italic":
- spec.Italic = true
case token == "underline":
spec.Underline = true
- case strings.HasPrefix(token, "fg="):
- spec.Fg = strings.TrimPrefix(token, "fg=")
- case strings.HasPrefix(token, "bg="):
- spec.Bg = strings.TrimPrefix(token, "bg=")
+ case strings.HasPrefix(token, "bg:"):
+ if n, err := strconv.Atoi(strings.TrimPrefix(token, "bg:")); err == nil && n >= 0 && n <= 255 {
+ spec.Bg = n
+ }
+ case strings.HasPrefix(token, "g:"):
+ parts := strings.Split(token[2:], ",")
+ var stops []int
+ for _, p := range parts {
+ if n, err := strconv.Atoi(strings.TrimSpace(p)); err == nil && n >= 0 && n <= 255 {
+ stops = append(stops, n)
+ }
+ }
+ if len(stops) >= 2 {
+ spec.Gradient = stops
+ }
+ default:
+ if n, err := strconv.Atoi(token); err == nil && n >= 0 && n <= 255 {
+ spec.Fg = n
+ }
}
}
return spec
}
func (s ColorSpec) Empty() bool {
- return s.Fg == "" && s.Bg == "" && !s.Bold && !s.Dim && !s.Italic && !s.Underline
+ return s.Fg < 0 && s.Bg < 0 && !s.Bold && !s.Dim && !s.Underline && len(s.Gradient) == 0
}
-func Render(mode string, spec ColorSpec, text string) string {
- if mode == "none" || mode == "" || spec.Empty() {
- return text
- }
+func stylePrefix(spec ColorSpec) string {
var codes []string
if spec.Bold {
- codes = append(codes, Style("bold"))
+ codes = append(codes, StyleCode("bold"))
}
if spec.Dim {
- codes = append(codes, Style("dim"))
- }
- if spec.Italic {
- codes = append(codes, Style("italic"))
+ codes = append(codes, StyleCode("dim"))
}
if spec.Underline {
- codes = append(codes, Style("underline"))
+ codes = append(codes, StyleCode("underline"))
+ }
+ return strings.Join(codes, "")
+}
+
+func Render(mode string, spec ColorSpec, text string) string {
+ if mode == "none" || mode == "" || spec.Empty() {
+ return text
+ }
+ if len(spec.Gradient) >= 2 {
+ return renderGradient(mode, spec, text)
}
- if spec.Fg != "" {
- codes = append(codes, Fg(mode, spec.Fg))
+ var codes []string
+ prefix := stylePrefix(spec)
+ if prefix != "" {
+ codes = append(codes, prefix)
+ }
+ if spec.Fg >= 0 {
+ codes = append(codes, FgCode(mode, spec.Fg))
}
- if spec.Bg != "" {
- codes = append(codes, Bg(mode, spec.Bg))
+ if spec.Bg >= 0 {
+ codes = append(codes, BgCode(mode, spec.Bg))
}
if len(codes) == 0 {
return text
}
return strings.Join(codes, "") + text + Reset
}
+
+func interpolateGradient(stops []int, t float64) int {
+ if t <= 0 {
+ return stops[0]
+ }
+ if t >= 1 {
+ return stops[len(stops)-1]
+ }
+ seg := t * float64(len(stops)-1)
+ i := int(seg)
+ if i >= len(stops)-1 {
+ i = len(stops) - 2
+ }
+ frac := seg - float64(i)
+ r1, g1, b1 := Xterm256ToRGB(stops[i])
+ r2, g2, b2 := Xterm256ToRGB(stops[i+1])
+ r := r1 + int(frac*float64(r2-r1))
+ g := g1 + int(frac*float64(g2-g1))
+ b := b1 + int(frac*float64(b2-b1))
+ return NearestXterm256(r, g, b)
+}
+
+func renderGradient(mode string, spec ColorSpec, text string) string {
+ runes := []rune(text)
+ n := len(runes)
+ if n == 0 {
+ return ""
+ }
+ var sb strings.Builder
+ prefix := stylePrefix(spec)
+ if spec.Bg >= 0 {
+ sb.WriteString(BgCode(mode, spec.Bg))
+ }
+ for i, r := range runes {
+ t := 0.0
+ if n > 1 {
+ t = float64(i) / float64(n-1)
+ }
+ idx := interpolateGradient(spec.Gradient, t)
+ if prefix != "" {
+ sb.WriteString(prefix)
+ }
+ sb.WriteString(FgCode(mode, idx))
+ sb.WriteRune(r)
+ }
+ sb.WriteString(Reset)
+ return sb.String()
+}
+
+func ExpandTags(mode string, text string) string {
+ return ExpandTagsWithDefault(mode, NoColor(), text)
+}
+
+func ExpandTagsWithDefault(mode string, def ColorSpec, text string) string {
+ if !strings.Contains(text, "{") {
+ if !def.Empty() {
+ return Render(mode, def, text)
+ }
+ return text
+ }
+
+ var sb strings.Builder
+ pos := 0
+ for pos < len(text) {
+ start := strings.Index(text[pos:], "{")
+ if start < 0 {
+ remaining := text[pos:]
+ if remaining != "" {
+ if !def.Empty() {
+ sb.WriteString(Render(mode, def, remaining))
+ } else {
+ sb.WriteString(remaining)
+ }
+ }
+ break
+ }
+ start += pos
+
+ end := strings.Index(text[start+1:], "}")
+ if end < 0 {
+ remaining := text[pos:]
+ if !def.Empty() {
+ sb.WriteString(Render(mode, def, remaining))
+ } else {
+ sb.WriteString(remaining)
+ }
+ break
+ }
+ end += start + 1
+
+ specStr := text[start+1 : end]
+
+ if specStr == "" || specStr == "/" {
+ before := text[pos : start+end-start+1]
+ if !def.Empty() {
+ sb.WriteString(Render(mode, def, before))
+ } else {
+ sb.WriteString(before)
+ }
+ pos = end + 1
+ continue
+ }
+
+ spec := Parse(specStr)
+ if spec.Empty() {
+ before := text[pos : end+1]
+ if !def.Empty() {
+ sb.WriteString(Render(mode, def, before))
+ } else {
+ sb.WriteString(before)
+ }
+ pos = end + 1
+ continue
+ }
+
+ closeStart := strings.Index(text[end+1:], "{/}")
+ if closeStart < 0 {
+ remaining := text[pos:]
+ if !def.Empty() {
+ sb.WriteString(Render(mode, def, remaining))
+ } else {
+ sb.WriteString(remaining)
+ }
+ break
+ }
+ closeStart += end + 1
+
+ before := text[pos:start]
+ if before != "" {
+ if !def.Empty() {
+ sb.WriteString(Render(mode, def, before))
+ } else {
+ sb.WriteString(before)
+ }
+ }
+
+ inner := text[end+1 : closeStart]
+ sb.WriteString(Render(mode, spec, inner))
+
+ pos = closeStart + 3
+ }
+
+ return sb.String()
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 48f1a69..d2065d2 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -7,37 +7,45 @@ import (
)
type Config struct {
- Game GameConfig `yaml:"game"`
- Colors ColorsConfig `yaml:"colors"`
- Telnet TelnetConfig `yaml:"telnet"`
- HTTP HTTPConfig `yaml:"http"`
- HTTPS HTTPSConfig `yaml:"https"`
+ Game GameConfig `yaml:"game"`
+ Colors ColorsConfig `yaml:"colors"`
+ Telnet TelnetConfig `yaml:"telnet"`
+ TelnetTLS TelnetTLSConfig `yaml:"telnet_tls"`
+ HTTP HTTPConfig `yaml:"http"`
+ HTTPS HTTPSConfig `yaml:"https"`
}
type ColorsConfig map[string]string
func DefaultColors() ColorsConfig {
return ColorsConfig{
- "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",
}
}
@@ -50,6 +58,13 @@ type TelnetConfig struct {
Port int `yaml:"port"`
}
+type TelnetTLSConfig struct {
+ Enabled bool `yaml:"enabled"`
+ Port int `yaml:"port"`
+ CertFile string `yaml:"cert_file"`
+ KeyFile string `yaml:"key_file"`
+}
+
type HTTPConfig struct {
Enabled bool `yaml:"enabled"`
Port int `yaml:"port"`
@@ -72,6 +87,10 @@ func Default() *Config {
Enabled: true,
Port: 4000,
},
+ TelnetTLS: TelnetTLSConfig{
+ Enabled: false,
+ Port: 4001,
+ },
HTTP: HTTPConfig{
Enabled: false,
Port: 8080,
diff --git a/internal/game/action.go b/internal/game/action.go
index 3857030..04e2b0a 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -6,12 +6,12 @@ import (
"strconv"
"strings"
- "thirdcollapse/internal/action"
- "thirdcollapse/internal/combat"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
)
var verbAliases = map[string]string{
diff --git a/internal/game/action_burn.go b/internal/game/action_burn.go
index 5c19510..01bf385 100644
--- a/internal/game/action_burn.go
+++ b/internal/game/action_burn.go
@@ -4,11 +4,11 @@ import (
"fmt"
"math/rand"
- "thirdcollapse/internal/action"
- "thirdcollapse/internal/engine"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doBurn(sess *net.Session, p *player.Player, input string) {
@@ -70,13 +70,13 @@ func (g *Game) startBurn(sess *net.Session, p *player.Player, itemID string, fro
}
if logDef.BurnTicks <= 0 {
- sess.WriteLine(fmt.Sprintf("You can't burn the %s.", logDef.Name))
+ sess.WriteLine(fmt.Sprintf("You can't burn the %s.", g.itemColorize(sess, logDef, logDef.Name)))
return
}
skillLevel := p.Level(player.Firemaking)
if logDef.FireLevel > 0 && skillLevel < logDef.FireLevel {
- sess.WriteLine(fmt.Sprintf("You need level %d firemaking to burn %s.", logDef.FireLevel, logDef.Name))
+ sess.WriteLine(fmt.Sprintf("You need level %d firemaking to burn %s.", logDef.FireLevel, g.itemColorize(sess, logDef, logDef.Name)))
return
}
@@ -87,13 +87,13 @@ func (g *Game) startBurn(sess *net.Session, p *player.Player, itemID string, fro
if !fromGround {
if !p.HasItem(itemID) {
- sess.WriteLine(fmt.Sprintf("You don't have any %s.", logDef.Name))
+ sess.WriteLine(fmt.Sprintf("You don't have any %s.", g.itemColorize(sess, logDef, logDef.Name)))
return
}
} else {
ground := g.World.GroundItems(p.RoomID)
if ground[itemID] <= 0 {
- sess.WriteLine(fmt.Sprintf("There are no %s on the ground here.", logDef.Name))
+ sess.WriteLine(fmt.Sprintf("There are no %s on the ground here.", g.itemColorize(sess, logDef, logDef.Name)))
return
}
}
@@ -135,7 +135,7 @@ func (g *Game) startStoke(sess *net.Session, p *player.Player, itemID string) {
}
if !p.HasItem(itemID) {
- sess.WriteLine(fmt.Sprintf("You don't have any %s.", logDef.Name))
+ sess.WriteLine(fmt.Sprintf("You don't have any %s.", g.itemColorize(sess, logDef, logDef.Name)))
return
}
@@ -171,7 +171,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
if phase == 0 {
if !p.HasItem(itemID) {
- sess.WriteLine(fmt.Sprintf("You're out of %s.", logDef.Name))
+ sess.WriteLine(fmt.Sprintf("You're out of %s.", g.itemColorize(sess, logDef, logDef.Name)))
g.CancelAction(p)
return
}
@@ -179,7 +179,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
g.World.AddGroundItem(p.RoomID, itemID, 1)
g.AccountStore.SaveCharacter(p)
- sess.WriteLine(fmt.Sprintf("You drop the %s and begin to make a fire...", logDef.Name))
+ sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You drop the %s and begin to make a fire...", g.itemColorize(sess, logDef, logDef.Name))))
g.broadcastDrop(p, logDef.Name)
data["phase"] = 1
@@ -195,7 +195,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
}
if phase == 1 {
- sess.WriteLine(fmt.Sprintf("You try burning the %s on the ground...", logDef.Name))
+ sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You try burning the %s on the ground...", g.itemColorize(sess, logDef, logDef.Name))))
data["phase"] = 2
p.Action.WaitLeft = engine.ToTicks(toolSpeed)
return
@@ -216,26 +216,26 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
g.World.AddObjInstance(p.RoomID, "fire", logDef.BurnTicks)
xp := logDef.FireXP
- if xp > 0 {
- if newLevel := p.AddSkillXP(player.Firemaking, xp); newLevel > 0 {
- sess.WriteLine(fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel))
- }
+ if xp > 0 {
+ if newLevel := p.AddSkillXP(player.Firemaking, xp); newLevel > 0 {
+ sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel)))
}
+ }
- g.AccountStore.SaveCharacter(p)
- g.CancelAction(p)
+ g.AccountStore.SaveCharacter(p)
+ g.CancelAction(p)
- msg := fmt.Sprintf("You manage to get a fire going!")
- if xp > 0 && p.OptionBool("xp_drops") {
- msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking])
- }
- sess.WriteLine(msg)
+ msg := g.colorize(sess, "fire", "You manage to get a fire going!")
+ if xp > 0 && p.OptionBool("xp_drops") {
+ msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking]))
+ }
+ sess.WriteLine(msg)
- for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
- if other != sess {
- other.WriteLine(fmt.Sprintf("\n%s started a fire!", p.Name))
- }
+ for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
+ if other != sess {
+ other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s started a fire!", p.Name)))
}
+ }
return
}
@@ -266,7 +266,7 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) {
}
if !p.HasItem(itemID) {
- sess.WriteLine(fmt.Sprintf("You're out of %s and the fire is roaring.", name))
+ sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You're out of %s and the fire is roaring.", g.itemColorize(sess, logDef, name))))
g.CancelAction(p)
return
}
@@ -276,20 +276,20 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) {
if xp > 0 {
if newLevel := p.AddSkillXP(player.Firemaking, xp); newLevel > 0 {
- sess.WriteLine(fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel))
+ sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel)))
}
}
g.AccountStore.SaveCharacter(p)
- msg := fmt.Sprintf("You throw %s onto the fire.", name)
+ msg := g.colorize(sess, "fire", fmt.Sprintf("You throw %s onto the fire.", name))
if xp > 0 && p.OptionBool("xp_drops") {
- msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking])
+ msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking]))
}
sess.WriteLine(msg)
if !p.HasItem(itemID) {
- sess.WriteLine(fmt.Sprintf("You're out of %s and the fire is roaring.", name))
+ sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You're out of %s and the fire is roaring.", g.itemColorize(sess, logDef, name))))
g.CancelAction(p)
return
}
diff --git a/internal/game/action_cook.go b/internal/game/action_cook.go
index e4d2597..a5bda41 100644
--- a/internal/game/action_cook.go
+++ b/internal/game/action_cook.go
@@ -4,10 +4,10 @@ import (
"fmt"
"math/rand"
- "thirdcollapse/internal/action"
- "thirdcollapse/internal/engine"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) startCook(sess *net.Session, p *player.Player, recipe *action.RecipeDef, stationName string) {
diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go
index e3ea105..045c51b 100644
--- a/internal/game/action_gather.go
+++ b/internal/game/action_gather.go
@@ -5,12 +5,13 @@ import (
"math/rand"
"strings"
- "thirdcollapse/internal/action"
- "thirdcollapse/internal/engine"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
)
func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.ObjectDef, st *world.ObjState) {
@@ -263,6 +264,8 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
msg := drop.Message
if msg == "" {
msg = fmt.Sprintf("You manage to get some %s.", g.itemColorize(sess, itemDef, itemName))
+ } else {
+ msg = color.ExpandTags(g.colorMode(sess), msg)
}
if xp > 0 && p.OptionBool("xp_drops") {
msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.SkillName(cfg.Skill)]))
diff --git a/internal/game/action_room.go b/internal/game/action_room.go
index 426b512..603ac7e 100644
--- a/internal/game/action_room.go
+++ b/internal/game/action_room.go
@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
- "thirdcollapse/internal/net"
+ "thehouseoficarus/internal/net"
)
func (g *Game) RunEnterSteps(sess *net.Session, roomID int) {
@@ -40,7 +40,7 @@ func (g *Game) BroadcastRespawns() {
msg := strings.ReplaceAll(cfg.RespawnBroadcast, "{name}", name)
if g.Hub != nil {
for _, sess := range g.Hub.PlayersInRoom(st.RoomID) {
- sess.WriteLine(fmt.Sprintf("\n%s", msg))
+ sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s", msg)))
}
}
}
diff --git a/internal/game/action_search.go b/internal/game/action_search.go
index e9d3d46..d45066d 100644
--- a/internal/game/action_search.go
+++ b/internal/game/action_search.go
@@ -3,10 +3,10 @@ package game
import (
"fmt"
- "thirdcollapse/internal/action"
- "thirdcollapse/internal/engine"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) startSearch(sess *net.Session, p *player.Player, itemID string, slotIdx int) {
@@ -101,23 +101,24 @@ func (g *Game) giveSearchLoot(sess *net.Session, p *player.Player, drop *action.
}
name := drop.ItemID
- if def, err := g.ItemStore.Load(drop.ItemID); err == nil {
- name = def.Name
+ lootDef, _ := g.ItemStore.Load(drop.ItemID)
+ if lootDef != nil {
+ name = lootDef.Name
}
if drop.ItemID == "credits" {
p.Credits += qty
- sess.WriteLine(fmt.Sprintf("You find %d credits.", qty))
+ sess.WriteLine(g.colorize(sess, "credits_pickup", fmt.Sprintf("You find %d credits.", qty)))
return
}
freeSlot := p.FirstFreeSlot()
if freeSlot == -1 {
g.World.AddGroundItem(p.RoomID, drop.ItemID, qty)
- sess.WriteLine(fmt.Sprintf("You find %s. It falls to the ground.", name))
+ sess.WriteLine(fmt.Sprintf("You find %s. It falls to the ground.", g.itemColorize(sess, lootDef, name)))
return
}
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty})
- sess.WriteLine(fmt.Sprintf("You find %s.", name))
+ sess.WriteLine(fmt.Sprintf("You find %s.", g.itemColorize(sess, lootDef, name)))
}
diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go
index 9e90f93..30bf206 100644
--- a/internal/game/action_talk.go
+++ b/internal/game/action_talk.go
@@ -4,11 +4,11 @@ import (
"fmt"
"strings"
- "thirdcollapse/internal/action"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
)
func (g *Game) startMobTalk(sess *net.Session, p *player.Player, mob *world.MobInstance) {
@@ -54,7 +54,7 @@ func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.Object
}
func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) {
- sess.WriteLine(fmt.Sprintf("\n%s", node.Message))
+ sess.WriteLine(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", node.Message)))
if node.Action != nil {
g.applyNodeAction(sess, node.Action)
diff --git a/internal/game/action_toggle.go b/internal/game/action_toggle.go
index f20d03c..db7a199 100644
--- a/internal/game/action_toggle.go
+++ b/internal/game/action_toggle.go
@@ -3,9 +3,9 @@ package game
import (
"fmt"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
)
func (g *Game) startToggle(sess *net.Session, p *player.Player, obj *object.ObjectDef) {
@@ -41,7 +41,7 @@ func (g *Game) startToggle(sess *net.Session, p *player.Player, obj *object.Obje
if g.Hub != nil {
for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
if other != sess && other.Player != nil {
- other.WriteLine(fmt.Sprintf("\n%s uses the %s.", p.Name, obj.Name))
+ other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s uses the %s.", p.Name, obj.Name)))
}
}
}
diff --git a/internal/game/action_use.go b/internal/game/action_use.go
index d953521..b71ec17 100644
--- a/internal/game/action_use.go
+++ b/internal/game/action_use.go
@@ -4,11 +4,11 @@ import (
"fmt"
"math/rand"
- "thirdcollapse/internal/action"
- "thirdcollapse/internal/engine"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
)
func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectDef) {
@@ -99,16 +99,17 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) {
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: cfg.Reward.ItemID, Quantity: qty})
if cfg.XP > 0 {
if newLevel := p.AddSkillXP(player.SkillName(cfg.Skill), cfg.XP); newLevel > 0 {
- sess.WriteLine(fmt.Sprintf("*** You are now level %d %s! ***", newLevel, cfg.Skill))
+ sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, cfg.Skill)))
}
}
g.AccountStore.SaveCharacter(p)
itemName := cfg.Reward.ItemID
- if def, err := g.ItemStore.Load(cfg.Reward.ItemID); err == nil {
- itemName = def.Name
+ rewardDef, _ := g.ItemStore.Load(cfg.Reward.ItemID)
+ if rewardDef != nil {
+ itemName = rewardDef.Name
}
- line := fmt.Sprintf("You make a %s.", itemName)
+ line := fmt.Sprintf("You make a %s.", g.itemColorize(sess, rewardDef, itemName))
if cfg.XP > 0 && p.OptionBool("xp_drops") {
line += fmt.Sprintf(" (+%dxp %s)", cfg.XP, player.SkillAbbr[player.SkillName(cfg.Skill)])
}
diff --git a/internal/game/cmd_alias.go b/internal/game/cmd_alias.go
index cb0537e..e7e4e12 100644
--- a/internal/game/cmd_alias.go
+++ b/internal/game/cmd_alias.go
@@ -5,7 +5,7 @@ import (
"sort"
"strings"
- "thirdcollapse/internal/net"
+ "thehouseoficarus/internal/net"
)
func (g *Game) doAlias(sess *net.Session, args []string) {
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index 2e15419..359e0ac 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -6,12 +6,12 @@ import (
"strconv"
"strings"
- "thirdcollapse/internal/combat"
- "thirdcollapse/internal/engine"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
)
func (g *Game) doAttack(sess *net.Session, input string) {
@@ -308,7 +308,7 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
op := other.Player.(*player.Player)
mobLvl := mobCombatLevel(mob)
levelStr := g.levelColorize(other, op.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl))
- other.WriteLine(fmt.Sprintf("\n%s has slain %s %s!", p.Name, mobDisplayName(mob, false), levelStr))
+ other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s has slain %s %s!", p.Name, mobDisplayName(mob, false), levelStr)))
}
}
}
@@ -324,7 +324,7 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
if !mob.Unique {
dropper = "The " + mob.Name
}
- sess.WriteLine(fmt.Sprintf(" %s drops: %s", dropper, name))
+ sess.WriteLine(g.colorize(sess, "drop_message", fmt.Sprintf(" %s drops: %s", dropper, name)))
}
if len(mob.Drops.Loot) > 0 {
@@ -345,9 +345,9 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
dropper = "The " + mob.Name
}
if qty > 1 {
- sess.WriteLine(fmt.Sprintf(" %s drops: %d x %s", dropper, qty, name))
+ sess.WriteLine(g.colorize(sess, "drop_message", fmt.Sprintf(" %s drops: %d x %s", dropper, qty, name)))
} else {
- sess.WriteLine(fmt.Sprintf(" %s drops: %s", dropper, name))
+ sess.WriteLine(g.colorize(sess, "drop_message", fmt.Sprintf(" %s drops: %s", dropper, name)))
}
}
}
@@ -476,7 +476,7 @@ func (g *Game) respawnMob(instanceID string) {
if p, ok := sess.Player.(*player.Player); ok && p.OptionBool("mob_spawn") {
mobLvl := mobCombatLevel(inst)
levelStr := g.levelColorize(sess, p.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl))
- sess.WriteLine(fmt.Sprintf("\n%s %s spawns in the area.", mobDisplayName(inst, false), levelStr))
+ sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s %s spawns in the area.", mobDisplayName(inst, false), levelStr)))
}
}
}
diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go
index 7a208a2..1dd14fa 100644
--- a/internal/game/cmd_color.go
+++ b/internal/game/cmd_color.go
@@ -4,10 +4,10 @@ import (
"fmt"
"strings"
- "thirdcollapse/internal/color"
- "thirdcollapse/internal/config"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/config"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doColor(sess *net.Session, input string) {
@@ -57,8 +57,8 @@ func (g *Game) doColor(sess *net.Session, input string) {
spec := color.Parse(value)
if spec.Empty() && value != "" {
sess.WriteLine(fmt.Sprintf("\nInvalid color string: %s", value))
- sess.WriteLine("Format: fg=<color> bg=<color> bold dim italic underline")
- sess.WriteLine("Example: fg=green bg=black bold")
+ sess.WriteLine("Format: <0-255> [bg:<0-255>] [bold] [dim] [underline]")
+ sess.WriteLine("Example: 208 bold")
return
}
@@ -150,17 +150,40 @@ var colorCategoryOrder = []string{
"victory",
"miss",
"error",
+ "say",
+ "dialog",
+ "broadcast",
+ "fire",
+ "eat_food",
+ "drop_message",
+ "credits_pickup",
}
func showColorTable(g *Game, sess *net.Session) {
p := sess.Player.(*player.Player)
+ mode := g.colorMode(sess)
table := &Table{
- Columns: []string{"Target", "Color", "Source"},
+ Columns: []string{
+ color.Render(mode, color.Parse("75"), "Target"),
+ "Color",
+ color.Render(mode, color.Parse("243"), "Source"),
+ },
}
for _, cat := range colorCategoryOrder {
val := g.getCurrentColor(sess, cat)
source := g.colorSource(sess, cat)
- table.Rows = append(table.Rows, []string{cat, val, source})
+ coloredVal := val
+ if val != "off" && val != "" {
+ spec := color.Parse(val)
+ if !spec.Empty() {
+ coloredVal = color.Render(mode, spec, val)
+ }
+ }
+ table.Rows = append(table.Rows, []string{
+ color.Render(mode, color.Parse("75"), cat),
+ coloredVal,
+ color.Render(mode, color.Parse("243"), source),
+ })
}
for _, line := range table.Render(p.OptionBool("unicode")) {
sess.WriteLine(line)
diff --git a/internal/game/cmd_colortable.go b/internal/game/cmd_colortable.go
new file mode 100644
index 0000000..90ae8c4
--- /dev/null
+++ b/internal/game/cmd_colortable.go
@@ -0,0 +1,151 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) doColortable(sess *net.Session) {
+ mode := g.colorMode(sess)
+ if mode == "none" || mode == "" {
+ sess.WriteLine("\nEnable colors first: option color ansi (or xterm256)")
+ return
+ }
+
+ p := sess.Player.(*player.Player)
+ unicode := p.OptionBool("unicode")
+
+ sess.WriteLine(fmt.Sprintf("\nColor mode: %s", mode))
+
+ sess.WriteLine("")
+ writeSectionHeader(sess, "Foreground Colors", unicode)
+ writeColorBlock(sess, mode, true)
+
+ sess.WriteLine("")
+ writeSectionHeader(sess, "Background Colors", unicode)
+ writeColorBlock(sess, mode, false)
+
+ sess.WriteLine("")
+ writeSectionHeader(sess, "Combinations", unicode)
+ writeCombinations(sess, mode)
+
+ sess.WriteLine("")
+ writeSectionHeader(sess, "ANSI Mode (16 colors)", unicode)
+ writeANSITable(sess, mode)
+}
+
+func writeSectionHeader(sess *net.Session, title string, unicode bool) {
+ if unicode {
+ sess.WriteLine(fmt.Sprintf("── %s ──", title))
+ } else {
+ sess.WriteLine(fmt.Sprintf("-- %s --", title))
+ }
+}
+
+func writeColorBlock(sess *net.Session, mode string, fg bool) {
+ sess.WriteLine("Standard:")
+ writeColorRow(sess, mode, 0, 8, fg)
+ sess.WriteLine("Bright:")
+ writeColorRow(sess, mode, 8, 16, fg)
+
+ sess.WriteLine("Color cube:")
+ for blockRow := 0; blockRow < 2; blockRow++ {
+ for row := 0; row < 6; row++ {
+ var sb strings.Builder
+ for block := 0; block < 3; block++ {
+ base := 16 + (blockRow*3+block)*36 + row*6
+ for col := 0; col < 6; col++ {
+ idx := base + col
+ if idx > 231 {
+ break
+ }
+ if fg {
+ sb.WriteString(color.FgCode(mode, idx))
+ } else {
+ sb.WriteString(color.ContrastFg(mode, idx))
+ sb.WriteString(color.BgCode(mode, idx))
+ }
+ sb.WriteString(fmt.Sprintf("%4d", idx))
+ sb.WriteString(color.Reset)
+ }
+ if block < 2 {
+ sb.WriteString(" ")
+ }
+ }
+ sess.WriteLine(sb.String())
+ }
+ if blockRow == 0 {
+ sess.WriteLine("")
+ }
+ }
+
+ sess.WriteLine("Grayscale:")
+ writeColorRow(sess, mode, 232, 244, fg)
+ writeColorRow(sess, mode, 244, 256, fg)
+}
+
+func writeColorRow(sess *net.Session, mode string, start, end int, fg bool) {
+ var sb strings.Builder
+ for i := start; i < end && i < 256; i++ {
+ if fg {
+ sb.WriteString(color.FgCode(mode, i))
+ } else {
+ sb.WriteString(color.ContrastFg(mode, i))
+ sb.WriteString(color.BgCode(mode, i))
+ }
+ sb.WriteString(fmt.Sprintf("%4d", i))
+ sb.WriteString(color.Reset)
+ }
+ sess.WriteLine(sb.String())
+}
+
+func writeCombinations(sess *net.Session, mode string) {
+ type combo struct {
+ fg int
+ bg int
+ desc string
+ }
+ combos := []combo{
+ {15, 0, "white on black"},
+ {208, 0, "orange on black"},
+ {0, 178, "black on gold"},
+ {15, 24, "white on dark blue"},
+ {0, 15, "black on white"},
+ {15, 88, "white on dark red"},
+ {178, 52, "gold on brown"},
+ {14, 0, "bright cyan on black"},
+ {0, 228, "black on pale yellow"},
+ {15, 22, "white on dark green"},
+ {9, 0, "bright red on black"},
+ {11, 17, "bright yellow on navy"},
+ }
+ for _, c := range combos {
+ spec := color.ColorSpec{Fg: c.fg, Bg: c.bg}
+ sample := color.Render(mode, spec, fmt.Sprintf(" %3d on %3d ", c.fg, c.bg))
+ sess.WriteLine(fmt.Sprintf(" %s %s", sample, c.desc))
+ }
+}
+
+func writeANSITable(sess *net.Session, mode string) {
+ names := [16]string{
+ "black", "red", "green", "yellow",
+ "blue", "magenta", "cyan", "white",
+ "brt black", "brt red", "brt green", "brt yellow",
+ "brt blue", "brt magenta", "brt cyan", "brt white",
+ }
+ for row := 0; row < 2; row++ {
+ var sb strings.Builder
+ for col := 0; col < 8; col++ {
+ idx := row*8 + col
+ sb.WriteString(color.FgCode(mode, idx))
+ sb.WriteString(fmt.Sprintf("%3d", idx))
+ sb.WriteString(color.Reset)
+ sb.WriteString(fmt.Sprintf(" %-12s", names[idx]))
+ }
+ sess.WriteLine(sb.String())
+ }
+}
diff --git a/internal/game/cmd_cook.go b/internal/game/cmd_cook.go
index be5fa3a..054a4b0 100644
--- a/internal/game/cmd_cook.go
+++ b/internal/game/cmd_cook.go
@@ -5,9 +5,9 @@ import (
"strconv"
"strings"
- "thirdcollapse/internal/action"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
type cookableEntry struct {
diff --git a/internal/game/cmd_description.go b/internal/game/cmd_description.go
index 56ce441..20ada37 100644
--- a/internal/game/cmd_description.go
+++ b/internal/game/cmd_description.go
@@ -3,8 +3,8 @@ package game
import (
"fmt"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doDescription(sess *net.Session) {
diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go
index b08eadf..702d1db 100644
--- a/internal/game/cmd_drop.go
+++ b/internal/game/cmd_drop.go
@@ -4,8 +4,8 @@ import (
"fmt"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doDropAll(sess *net.Session) {
@@ -179,11 +179,11 @@ func (g *Game) doDrop(sess *net.Session, input string) {
dropped := qty - remaining
g.AccountStore.SaveCharacter(p)
if dropped == 0 {
- sess.WriteLine(fmt.Sprintf("You don't have any %s.", name))
+ sess.WriteLine(fmt.Sprintf("You don't have any %s.", g.itemColorize(sess, def, name)))
} else if dropped == 1 {
- sess.WriteLine(fmt.Sprintf("You drop a %s.", name))
+ sess.WriteLine(fmt.Sprintf("You drop a %s.", g.itemColorize(sess, def, name)))
} else {
- sess.WriteLine(fmt.Sprintf("You drop %d x %s.", dropped, name))
+ sess.WriteLine(fmt.Sprintf("You drop %d x %s.", dropped, g.itemColorize(sess, def, name)))
}
}
@@ -215,7 +215,7 @@ func (g *Game) handleDropAllConfirm(sess *net.Session, input string) {
if def != nil {
name = def.Name
}
- dropped = append(dropped, name)
+ dropped = append(dropped, g.itemColorize(sess, def, name))
p.SetInvSlot(i, nil)
}
diff --git a/internal/game/cmd_eat.go b/internal/game/cmd_eat.go
index 5d6292b..9d4bfd0 100644
--- a/internal/game/cmd_eat.go
+++ b/internal/game/cmd_eat.go
@@ -4,9 +4,9 @@ import (
"fmt"
"time"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doEat(sess *net.Session, input string) {
@@ -65,7 +65,7 @@ func (g *Game) doEat(sess *net.Session, input string) {
}
if !p.OptionBool("queue_silently") {
- sess.WriteLine(fmt.Sprintf("\nYou prepare to eat %s.", def.Name))
+ sess.WriteLine(fmt.Sprintf("\nYou prepare to eat %s.", g.itemColorize(sess, def, def.Name)))
}
}
@@ -98,7 +98,7 @@ func (g *Game) doEatNow(sess *net.Session, p *player.Player, itemID string, def
p.ActionState = &ActionState{Type: ActionEating, TargetName: def.Name}
- sess.WriteLine(def.EatMessage)
+ sess.WriteLine(g.colorize(sess, "eat_food", def.EatMessage))
if p.HP <= 0 {
g.endCombat(sess, p, nil)
diff --git a/internal/game/cmd_equipment.go b/internal/game/cmd_equipment.go
index 4947d2b..7113880 100644
--- a/internal/game/cmd_equipment.go
+++ b/internal/game/cmd_equipment.go
@@ -3,8 +3,8 @@ package game
import (
"fmt"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doEquipment(sess *net.Session) {
diff --git a/internal/game/cmd_get.go b/internal/game/cmd_get.go
index e75c59e..8237ec9 100644
--- a/internal/game/cmd_get.go
+++ b/internal/game/cmd_get.go
@@ -3,8 +3,8 @@ package game
import (
"fmt"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doGet(sess *net.Session, input string) {
@@ -125,17 +125,9 @@ func (g *Game) doGet(sess *net.Session, input string) {
if picked == 0 {
sess.WriteLine("Your inventory is full.")
} else if picked == 1 {
- name := itemID
- if def != nil {
- name = def.Name
- }
- sess.WriteLine(fmt.Sprintf("You pick up a %s.", name))
+ sess.WriteLine(fmt.Sprintf("You pick up a %s.", g.itemColorize(sess, def, name)))
} else {
- name := itemID
- if def != nil {
- name = def.Name
- }
- sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", picked, name))
+ sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", picked, g.itemColorize(sess, def, name)))
}
}
@@ -306,7 +298,7 @@ func (g *Game) doGetAll(sess *net.Session) {
break
}
slot.Quantity += qty
- picked = append(picked, fmt.Sprintf("%s (now %d)", def.Name, slot.Quantity))
+ picked = append(picked, fmt.Sprintf("%s (now %d)", g.itemColorize(sess, def, def.Name), slot.Quantity))
stacked = true
qty = 0
break
@@ -335,7 +327,7 @@ func (g *Game) doGetAll(sess *net.Session) {
if def != nil {
name = def.Name
}
- picked = append(picked, name)
+ picked = append(picked, g.itemColorize(sess, def, name))
break
}
@@ -361,7 +353,7 @@ func (g *Game) doGetAll(sess *net.Session) {
if def != nil {
name = def.Name
}
- picked = append(picked, name)
+ picked = append(picked, g.itemColorize(sess, def, name))
qty -= take
}
}
@@ -388,9 +380,9 @@ func (g *Game) pickupCredits(sess *net.Session, p *player.Player, itemID string)
p.Credits += qty
g.AccountStore.SaveCharacter(p)
if qty == 1 {
- sess.WriteLine("You pick up 1 credit.")
+ sess.WriteLine(g.colorize(sess, "credits_pickup", "You pick up 1 credit."))
} else {
- sess.WriteLine(fmt.Sprintf("You pick up %d credits. (total: %d)", qty, p.Credits))
+ sess.WriteLine(g.colorize(sess, "credits_pickup", fmt.Sprintf("You pick up %d credits. (total: %d)", qty, p.Credits)))
}
}
diff --git a/internal/game/cmd_inventory.go b/internal/game/cmd_inventory.go
index 7017b18..8684fcd 100644
--- a/internal/game/cmd_inventory.go
+++ b/internal/game/cmd_inventory.go
@@ -3,21 +3,19 @@ package game
import (
"fmt"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doInventory(sess *net.Session) {
p := sess.Player.(*player.Player)
sess.WriteLine("")
- sess.WriteLine("Inventory (28 slots):")
+ sess.WriteLine(fmt.Sprintf("Inventory (%d free):", p.FreeSlots()))
- empty := 0
num := 0
for i := 0; i < 28; i++ {
slot := p.InvSlot(i)
if slot == nil {
- empty++
continue
}
num++
@@ -33,5 +31,4 @@ func (g *Game) doInventory(sess *net.Session) {
sess.WriteLine(fmt.Sprintf(" %2d) %s", num, coloredName))
}
}
- sess.WriteLine(fmt.Sprintf(" (%d empty slots)", empty))
}
diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go
index bd738be..8cadfd3 100644
--- a/internal/game/cmd_look.go
+++ b/internal/game/cmd_look.go
@@ -2,16 +2,16 @@ package game
import (
"fmt"
- "regexp"
"sort"
"strconv"
"strings"
- "thirdcollapse/internal/combat"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
)
func (g *Game) doLook(sess *net.Session) {
@@ -32,9 +32,11 @@ func (g *Game) doLook(sess *net.Session) {
descWidth = 70
}
rawLines := wrapText(room.Description, descWidth)
+ mode := g.colorMode(sess)
+ roomDescSpec := g.resolveColor(sess, "room_desc")
descLines := make([]string, len(rawLines))
for i, l := range rawLines {
- descLines[i] = g.colorize(sess, "room_desc", l)
+ descLines[i] = color.ExpandTagsWithDefault(mode, roomDescSpec, l)
}
wroteDesc := false
if p.OptionString("tiny_map") != "off" {
@@ -331,7 +333,7 @@ func (g *Game) doLook(sess *net.Session) {
for _, other := range others {
if other != sess && other.Player != nil {
op := other.Player.(*player.Player)
- line := fmt.Sprintf("\n%s is here", op.Name)
+ line := fmt.Sprintf("\n%s is here", g.colorize(sess, "player_name", op.Name))
if cs := combat.GetCombat(op.Name); cs != nil {
if mob := g.MobStore.GetInstance(cs.MobID); mob != nil && mob.HP > 0 {
name := mobDisplayName(mob, false)
@@ -476,7 +478,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
}
lines := []string{
"",
- def.Name,
+ g.itemColorize(sess, def, def.Name),
fmt.Sprintf(" %s", def.Description),
fmt.Sprintf(" Value: %d credits", def.Value),
}
@@ -606,10 +608,8 @@ func mobInstanceIdx(mob *world.MobInstance, roomMobs []*world.MobInstance) int {
return 0
}
-var ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
-
func visibleLen(s string) int {
- return len(ansiRe.ReplaceAllString(s, ""))
+ return color.VisibleLen(s)
}
func wrapText(text string, width int) []string {
diff --git a/internal/game/cmd_map.go b/internal/game/cmd_map.go
index f4ec11c..1c78be3 100644
--- a/internal/game/cmd_map.go
+++ b/internal/game/cmd_map.go
@@ -1,8 +1,8 @@
package game
import (
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doMap(sess *net.Session) {
diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go
index 11aa17e..a140003 100644
--- a/internal/game/cmd_move.go
+++ b/internal/game/cmd_move.go
@@ -3,9 +3,9 @@ package game
import (
"fmt"
- "thirdcollapse/internal/combat"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doMove(sess *net.Session, dir string) {
diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go
index 9302baa..3d4d420 100644
--- a/internal/game/cmd_option.go
+++ b/internal/game/cmd_option.go
@@ -5,22 +5,34 @@ import (
"strconv"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doOption(sess *net.Session, input string) {
p := sess.Player.(*player.Player)
+ mode := g.colorMode(sess)
if input == "" {
sess.WriteLine("")
table := &Table{
- Columns: []string{"Option", "Value", "Valid", "Description"},
+ Columns: []string{
+ color.Render(mode, color.Parse("75"), "Option"),
+ color.Render(mode, color.Parse("230"), "Value"),
+ color.Render(mode, color.Parse("243"), "Valid"),
+ color.Render(mode, color.Parse("252"), "Description"),
+ },
}
for _, def := range player.OptionDefs {
val := formatOptionValue(p, &def)
valid := formatValidValues(&def)
- table.Rows = append(table.Rows, []string{def.Name, val, valid, def.Description})
+ table.Rows = append(table.Rows, []string{
+ color.Render(mode, color.Parse("75"), def.Name),
+ color.Render(mode, color.Parse("230"), val),
+ color.Render(mode, color.Parse("243"), valid),
+ color.Render(mode, color.Parse("252"), def.Description),
+ })
}
for _, line := range table.Render(p.OptionBool("unicode")) {
sess.WriteLine(line)
@@ -56,7 +68,20 @@ func (g *Game) doOption(sess *net.Session, input string) {
p.Options = make(map[string]any)
}
p.Options[def.Name] = parsed
- g.AccountStore.SaveCharacter(p)
+
+ if sess.Account.Options == nil {
+ sess.Account.Options = make(map[string]any)
+ }
+ sess.Account.Options[def.Name] = parsed
+
+ acc, err := g.AccountStore.LoadAccount(sess.Account.Name)
+ if err == nil {
+ if acc.Options == nil {
+ acc.Options = make(map[string]any)
+ }
+ acc.Options[def.Name] = parsed
+ g.AccountStore.SaveAccount(acc)
+ }
sess.WriteLine(fmt.Sprintf("\n%s set to %s.", def.Name, formatOptionValue(p, def)))
}
diff --git a/internal/game/cmd_prompt.go b/internal/game/cmd_prompt.go
new file mode 100644
index 0000000..81c3409
--- /dev/null
+++ b/internal/game/cmd_prompt.go
@@ -0,0 +1,33 @@
+package game
+
+import (
+ "fmt"
+
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) doPrompt(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+
+ if input == "" {
+ prompt := p.Prompt
+ if prompt == "" {
+ prompt = "> "
+ }
+ sess.WriteLine(fmt.Sprintf("\nPrompt: %s", prompt))
+ sess.WriteLine("Use 'prompt <value>' to change. Use 'prompt reset' to restore default.")
+ return
+ }
+
+ if input == "reset" {
+ p.Prompt = "> "
+ g.AccountStore.SaveCharacter(p)
+ sess.WriteLine("\nPrompt reset to default.")
+ return
+ }
+
+ p.Prompt = input
+ g.AccountStore.SaveCharacter(p)
+ sess.WriteLine(fmt.Sprintf("\nPrompt set to: %s", input))
+}
diff --git a/internal/game/cmd_queued.go b/internal/game/cmd_queued.go
index 040ff31..d9e92d5 100644
--- a/internal/game/cmd_queued.go
+++ b/internal/game/cmd_queued.go
@@ -4,8 +4,8 @@ import (
"fmt"
"sort"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doQueued(sess *net.Session) {
diff --git a/internal/game/cmd_quit.go b/internal/game/cmd_quit.go
index 446305b..89b6e7f 100644
--- a/internal/game/cmd_quit.go
+++ b/internal/game/cmd_quit.go
@@ -1,10 +1,10 @@
package game
import (
- "thirdcollapse/internal/combat"
- "thirdcollapse/internal/engine"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doQuit(sess *net.Session) {
diff --git a/internal/game/cmd_remove.go b/internal/game/cmd_remove.go
index f511ec4..61fdc29 100644
--- a/internal/game/cmd_remove.go
+++ b/internal/game/cmd_remove.go
@@ -4,9 +4,9 @@ import (
"fmt"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doRemove(sess *net.Session, input string) {
@@ -68,8 +68,9 @@ func (g *Game) doRemove(sess *net.Session, input string) {
g.AccountStore.SaveCharacter(p)
name := foundItemID
- if def, err := g.ItemStore.Load(foundItemID); err == nil {
+ def, _ := g.ItemStore.Load(foundItemID)
+ if def != nil {
name = def.Name
}
- sess.WriteLine(fmt.Sprintf("You remove %s.", name))
+ sess.WriteLine(fmt.Sprintf("You remove %s.", g.itemColorize(sess, def, name)))
}
diff --git a/internal/game/cmd_say.go b/internal/game/cmd_say.go
index 35ec5fa..67d8165 100644
--- a/internal/game/cmd_say.go
+++ b/internal/game/cmd_say.go
@@ -3,8 +3,8 @@ package game
import (
"fmt"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doSay(sess *net.Session, msg string) {
@@ -13,9 +13,9 @@ func (g *Game) doSay(sess *net.Session, msg string) {
for _, other := range g.Hub.PlayersInRoom(roomID) {
if other == sess {
- other.WriteLine(fmt.Sprintf("You say: %s", msg))
+ other.WriteLine(fmt.Sprintf("You say: %s", g.colorize(sess, "say", msg)))
} else {
- other.WriteLine(fmt.Sprintf("\n%s says: %s", p.Name, msg))
+ other.WriteLine(fmt.Sprintf("\n%s says: %s", g.colorize(other, "player_name", p.Name), g.colorize(other, "say", msg)))
}
}
}
diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go
index 2c62839..40d3007 100644
--- a/internal/game/cmd_score.go
+++ b/internal/game/cmd_score.go
@@ -4,29 +4,35 @@ import (
"fmt"
"strconv"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doScore(sess *net.Session) {
p := sess.Player.(*player.Player)
+ mode := g.colorMode(sess)
sess.WriteLines(
"",
- fmt.Sprintf("Name: %s", p.Name),
- fmt.Sprintf("Combat Level: %d", p.CombatLevel()),
- fmt.Sprintf("HP: %d/%d", p.HP, p.MaxHP()),
- fmt.Sprintf("Credits: %d", p.Credits),
+ fmt.Sprintf("Name: %s", g.colorize(sess, "player_name", p.Name)),
+ fmt.Sprintf("Combat Level: %s", color.Render(mode, color.Parse("230"), fmt.Sprint(p.CombatLevel()))),
+ fmt.Sprintf("HP: %s/%s", g.colorize(sess, "character_hp", fmt.Sprint(p.HP)), color.Render(mode, color.Parse("230"), fmt.Sprint(p.MaxHP()))),
+ fmt.Sprintf("Credits: %s", g.colorize(sess, "credits_pickup", fmt.Sprint(p.Credits))),
)
- t := &Table{Title: "Skills", Columns: []string{"Skill", "Level", "XP"}}
+ t := &Table{Title: "Skills", Columns: []string{
+ color.Render(mode, color.Parse("75"), "Skill"),
+ color.Render(mode, color.Parse("230"), "Level"),
+ color.Render(mode, color.Parse("222"), "XP"),
+ }}
for _, s := range player.AllSkills {
level := p.Level(s)
xp := p.Skills[s]
next := player.XPForNextLevel(xp)
t.Rows = append(t.Rows, []string{
- string(s),
- strconv.Itoa(level),
- fmt.Sprintf("%d / %d XP", xp, xp+next),
+ color.Render(mode, color.Parse("75"), string(s)),
+ color.Render(mode, color.Parse("230"), strconv.Itoa(level)),
+ color.Render(mode, color.Parse("222"), fmt.Sprintf("%d / %d XP", xp, xp+next)),
})
}
for _, line := range t.Render(p.OptionBool("unicode")) {
diff --git a/internal/game/cmd_search.go b/internal/game/cmd_search.go
index d9a5b1e..d4d2e72 100644
--- a/internal/game/cmd_search.go
+++ b/internal/game/cmd_search.go
@@ -4,8 +4,8 @@ import (
"fmt"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doSearch(sess *net.Session, input string) {
diff --git a/internal/game/cmd_style.go b/internal/game/cmd_style.go
index d86920c..901d02d 100644
--- a/internal/game/cmd_style.go
+++ b/internal/game/cmd_style.go
@@ -4,8 +4,8 @@ import (
"fmt"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doStyle(sess *net.Session, input string) {
diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go
index ef36e12..56aa5a1 100644
--- a/internal/game/cmd_use.go
+++ b/internal/game/cmd_use.go
@@ -4,9 +4,9 @@ import (
"fmt"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doUse(sess *net.Session, input string) {
diff --git a/internal/game/cmd_walk.go b/internal/game/cmd_walk.go
index 31f9e18..4ff6187 100644
--- a/internal/game/cmd_walk.go
+++ b/internal/game/cmd_walk.go
@@ -5,9 +5,9 @@ import (
"strconv"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
)
func (g *Game) doWalk(sess *net.Session, args []string) {
diff --git a/internal/game/cmd_wear.go b/internal/game/cmd_wear.go
index 7c3515f..cbc2ff3 100644
--- a/internal/game/cmd_wear.go
+++ b/internal/game/cmd_wear.go
@@ -4,9 +4,9 @@ import (
"fmt"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
)
func (g *Game) doWear(sess *net.Session, input string) {
@@ -39,7 +39,7 @@ func (g *Game) doWear(sess *net.Session, input string) {
def, err := g.ItemStore.Load(slot.ItemID)
if err != nil || def.EquipSlot == "" {
- sess.WriteLine(fmt.Sprintf("You can't wear %s.", match.Name))
+ sess.WriteLine(fmt.Sprintf("You can't wear %s.", g.itemColorize(sess, def, match.Name)))
return
}
@@ -76,7 +76,7 @@ func (g *Game) doWear(sess *net.Session, input string) {
if def.WeaponType != "" {
verb = "wield"
}
- sess.WriteLine(fmt.Sprintf("You %s %s.", verb, match.Name))
+ sess.WriteLine(fmt.Sprintf("You %s %s.", verb, g.itemColorize(sess, def, match.Name)))
}
func (g *Game) doWearAll(sess *net.Session) {
@@ -147,7 +147,7 @@ func (g *Game) doWearAll(sess *net.Session) {
if def != nil {
name = def.Name
}
- equipped = append(equipped, name)
+ equipped = append(equipped, g.itemColorize(sess, def, name))
}
if len(equipped) == 0 {
diff --git a/internal/game/color.go b/internal/game/color.go
index f93f097..4fd8000 100644
--- a/internal/game/color.go
+++ b/internal/game/color.go
@@ -1,11 +1,11 @@
package game
import (
- "thirdcollapse/internal/color"
- "thirdcollapse/internal/config"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/config"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
)
func (g *Game) colorMode(sess *net.Session) string {
@@ -19,7 +19,7 @@ func (g *Game) resolveColor(sess *net.Session, category string) color.ColorSpec
if sess.Account != nil && sess.Account.Colors != nil {
if val, ok := sess.Account.Colors[category]; ok {
if val == "off" {
- return color.ColorSpec{}
+ return color.NoColor()
}
if val != "" {
return color.Parse(val)
@@ -34,7 +34,7 @@ func (g *Game) resolveColor(sess *net.Session, category string) color.ColorSpec
if val, ok := config.DefaultColors()[category]; ok && val != "" {
return color.Parse(val)
}
- return color.ColorSpec{}
+ return color.NoColor()
}
func (g *Game) colorize(sess *net.Session, category, text string) string {
@@ -46,15 +46,15 @@ func levelColorSpec(myLevel, theirLevel int) color.ColorSpec {
diff := theirLevel - myLevel
switch {
case diff == 0:
- return color.Parse("fg=white")
+ return color.Parse("7")
case diff > 0 && diff < 5:
- return color.Parse("fg=bright_yellow")
+ return color.Parse("11")
case diff >= 5:
- return color.Parse("fg=bright_red")
+ return color.Parse("9")
case diff < 0 && diff > -5:
- return color.Parse("fg=yellow")
+ return color.Parse("3")
default:
- return color.Parse("fg=green")
+ return color.Parse("2")
}
}
diff --git a/internal/game/game.go b/internal/game/game.go
index 33dd0d1..9d07ead 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -7,14 +7,14 @@ import (
"sync"
"time"
- "thirdcollapse/internal/action"
- "thirdcollapse/internal/combat"
- "thirdcollapse/internal/config"
- "thirdcollapse/internal/engine"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
- "thirdcollapse/internal/player"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/config"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
)
type CommandClass int
@@ -124,6 +124,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) {
g.handleDropAllConfirm(sess, input)
case net.StateCookRecipe:
g.handleCookRecipe(sess, input)
+ case net.StateColorChoice:
+ g.handleColorChoice(sess, input)
}
}
@@ -132,9 +134,10 @@ func classifyCommand(cmd string) CommandClass {
case "say", "score", "sc", "inventory", "i", "inv",
"equipment", "eq", "look", "l", "exits", "help",
"map", "option", "options", "alias", "unalias",
- "description", "desc", "queued", "color", "colors":
+ "description", "desc", "queued", "color", "colors",
+ "colortable", "prompt", "style":
return ClassInstant
- case "wear", "wield", "remove", "unwear", "unwield", "style":
+ case "wear", "wield", "remove", "unwear", "unwield":
return ClassFree
case "get", "take", "grab", "pick", "drop",
"attack", "kill",
@@ -310,6 +313,19 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
g.doOption(sess, strings.Join(args, " "))
case "color", "colors":
g.doColor(sess, strings.Join(args, " "))
+ case "colortable":
+ g.doColortable(sess)
+ case "prompt":
+ if len(args) == 0 {
+ g.doPrompt(sess, "")
+ } else {
+ msgStart := strings.Index(strings.ToLower(rawInput), "prompt ") + 7
+ if msgStart >= 7 && msgStart < len(rawInput) {
+ g.doPrompt(sess, rawInput[msgStart:])
+ } else {
+ g.doPrompt(sess, strings.Join(args, " "))
+ }
+ }
case "exits":
g.doExits(sess)
case "map":
diff --git a/internal/game/help.go b/internal/game/help.go
index d3f34c3..f9d822c 100644
--- a/internal/game/help.go
+++ b/internal/game/help.go
@@ -6,8 +6,8 @@ import (
"path/filepath"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
"gopkg.in/yaml.v3"
)
@@ -29,6 +29,7 @@ var commandList = []cmdEntry{
{"burn", "Active", "Start a fire"},
{"chop / cut", "Active", "Chop trees (Woodcutting)"},
{"color / colors", "Instant", "Customize display colors"},
+ {"colortable", "Instant", "Display color reference chart"},
{"cook", "Active", "Cook raw food on a fire or range"},
{"description / desc", "Instant", "Set your character description"},
{"drop", "Active", "Drop items to the ground"},
@@ -44,6 +45,7 @@ var commandList = []cmdEntry{
{"mine", "Active", "Mine rocks (Mining)"},
{"north / south / east / west / up / down", "Active", "Move in a direction"},
{"option / options", "Instant", "View or change settings"},
+ {"prompt", "Instant", "Set custom command prompt"},
{"pull / push", "Active", "Toggle levers and switches"},
{"queued", "Instant", "Show pending tick actions"},
{"quit", "Active", "Rest and disconnect"},
@@ -52,7 +54,7 @@ var commandList = []cmdEntry{
{"score / sc", "Instant", "View your stats and skills"},
{"search", "Active", "Search items for loot"},
{"stoke", "Active", "Add logs to a fire"},
- {"style", "Free", "Change combat style"},
+ {"style", "Instant", "Change combat style"},
{"talk / speak / ask", "Active", "Talk to NPCs"},
{"unalias", "Instant", "Remove command shortcuts"},
{"use", "Active", "Use an object (crafting)"},
diff --git a/internal/game/login_account.go b/internal/game/login_account.go
index 3ad1fee..176a1ee 100644
--- a/internal/game/login_account.go
+++ b/internal/game/login_account.go
@@ -5,8 +5,8 @@ import (
"os"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) handleAccountName(sess *net.Session, input string) {
@@ -69,6 +69,7 @@ func (g *Game) handlePassword(sess *net.Session, input string) {
Characters: acc.Characters,
Aliases: acc.Aliases,
Colors: acc.Colors,
+ Options: acc.Options,
}
if sess.Account.Aliases == nil {
sess.Account.Aliases = make(map[string]string)
@@ -76,6 +77,9 @@ func (g *Game) handlePassword(sess *net.Session, input string) {
if sess.Account.Colors == nil {
sess.Account.Colors = make(map[string]string)
}
+ if sess.Account.Options == nil {
+ sess.Account.Options = make(map[string]any)
+ }
g.showMenu(sess)
}
@@ -144,8 +148,27 @@ func (g *Game) handleNewAccountConfirm(sess *net.Session, input string) {
Name: acc.Name,
PasswordHash: acc.PasswordHash,
Aliases: make(map[string]string),
+ Colors: make(map[string]string),
+ Options: make(map[string]any),
}
sess.PendingPass = ""
+ sess.State = net.StateColorChoice
+ sess.Write("\nShould I disable color? [y/N] ")
+}
+
+func (g *Game) handleColorChoice(sess *net.Session, input string) {
+ input = strings.ToLower(strings.TrimSpace(input))
+ if input == "y" || input == "yes" {
+ sess.Account.Options["color"] = "none"
+ acc, err := g.AccountStore.LoadAccount(sess.Account.Name)
+ if err == nil {
+ if acc.Options == nil {
+ acc.Options = make(map[string]any)
+ }
+ acc.Options["color"] = "none"
+ g.AccountStore.SaveAccount(acc)
+ }
+ }
g.showMenu(sess)
}
@@ -158,7 +181,7 @@ func (g *Game) showMenu(sess *net.Session) {
}
if len(sess.Account.Characters) > 0 {
lines = append(lines,
- " (C)onnect character to TC",
+ " (C)onnect character to THOI",
"",
" (L)ist characters",
" (R)ename character",
@@ -171,9 +194,9 @@ func (g *Game) showMenu(sess *net.Session) {
" (A)ccount rename",
" (Q)uit",
"",
- "> ",
)
sess.WriteLines(lines...)
+ sess.Write("> ")
}
func (g *Game) handleMenu(sess *net.Session, input string) {
diff --git a/internal/game/login_char.go b/internal/game/login_char.go
index ba034bb..82065d3 100644
--- a/internal/game/login_char.go
+++ b/internal/game/login_char.go
@@ -5,8 +5,8 @@ import (
"os"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) handleNewCharName(sess *net.Session, input string) {
@@ -82,6 +82,13 @@ func (g *Game) connectCharacter(sess *net.Session, name string) {
g.loggedInChars[name] = sess
g.charsMu.Unlock()
+ if sess.Account != nil && sess.Account.Options != nil {
+ p.Options = make(map[string]any)
+ for k, v := range sess.Account.Options {
+ p.Options[k] = v
+ }
+ }
+
g.World.SeedGroundItems(p.RoomID)
g.seedRoomMobs(p.RoomID)
g.seedRoomObjects(p.RoomID)
diff --git a/internal/game/map.go b/internal/game/map.go
index 8b14e25..32d3d7c 100644
--- a/internal/game/map.go
+++ b/internal/game/map.go
@@ -3,7 +3,7 @@ package game
import (
"strings"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/world"
)
type mapGlyphs struct {
diff --git a/internal/game/map_test.go b/internal/game/map_test.go
index 8c82f4a..a695c1b 100644
--- a/internal/game/map_test.go
+++ b/internal/game/map_test.go
@@ -4,7 +4,7 @@ import (
"strings"
"testing"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/world"
)
func TestBuildTinyMap(t *testing.T) {
diff --git a/internal/game/prompt.go b/internal/game/prompt.go
index 2fc36ac..de88bc6 100644
--- a/internal/game/prompt.go
+++ b/internal/game/prompt.go
@@ -4,17 +4,17 @@ import (
"fmt"
"strings"
- "thirdcollapse/internal/color"
- "thirdcollapse/internal/combat"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
)
func (g *Game) promptStr(sess *net.Session) string {
prompt := "> "
if p, ok := sess.Player.(*player.Player); ok && p != nil {
- if custom := p.OptionString("prompt"); custom != "" {
- prompt = custom
+ if p.Prompt != "" {
+ prompt = p.Prompt
}
}
@@ -24,42 +24,7 @@ func (g *Game) promptStr(sess *net.Session) string {
}
func (g *Game) expandPromptColors(sess *net.Session, text string) string {
- mode := g.colorMode(sess)
- for {
- start := strings.Index(text, "{")
- if start < 0 {
- break
- }
-
- end := strings.Index(text[start+1:], "}")
- if end < 0 {
- break
- }
- end += start + 1
- specStr := text[start+1 : end]
-
- if specStr == "" || specStr == "/" {
- break
- }
-
- spec := color.Parse(specStr)
- if spec.Empty() {
- break
- }
-
- closeStart := strings.Index(text[end+1:], "{/}")
- if closeStart < 0 {
- break
- }
- closeStart += end + 1
- closeEnd := closeStart + 3
-
- inner := text[end+1 : closeStart]
- colored := color.Render(mode, spec, inner)
-
- text = text[:start] + colored + text[closeEnd:]
- }
- return text
+ return color.ExpandTags(g.colorMode(sess), text)
}
func (g *Game) expandPromptVars(sess *net.Session, text string) string {
diff --git a/internal/game/table.go b/internal/game/table.go
index 56c0079..b96dca1 100644
--- a/internal/game/table.go
+++ b/internal/game/table.go
@@ -1,8 +1,9 @@
package game
import (
- "fmt"
"strings"
+
+ "thehouseoficarus/internal/color"
)
type tableGlyphs struct {
@@ -50,15 +51,16 @@ func (t *Table) Render(unicode bool) []string {
colWidths := make([]int, nCols)
for i, h := range t.Columns {
- colWidths[i] = len(h)
+ colWidths[i] = color.VisibleLen(h)
}
for _, row := range t.Rows {
for i, cell := range row {
if i >= nCols {
break
}
- if len(cell) > colWidths[i] {
- colWidths[i] = len(cell)
+ vl := color.VisibleLen(cell)
+ if vl > colWidths[i] {
+ colWidths[i] = vl
}
}
}
@@ -87,7 +89,11 @@ func (t *Table) Render(unicode bool) []string {
if i < len(cells) {
cell = cells[i]
}
- b.WriteString(fmt.Sprintf(" %-*s ", colWidths[i], cell))
+ pad := colWidths[i] - color.VisibleLen(cell)
+ if pad < 0 {
+ pad = 0
+ }
+ b.WriteString(" " + cell + strings.Repeat(" ", pad) + " ")
}
b.WriteRune(g.side)
return b.String()
@@ -115,7 +121,7 @@ func (t *Table) Render(unicode bool) []string {
tr.WriteRune(g.side)
tr.WriteString(" ")
tr.WriteString(t.Title)
- padding := innerWidth - 2 - len(t.Title)
+ padding := innerWidth - 2 - color.VisibleLen(t.Title)
if padding < 0 {
padding = 0
}
diff --git a/internal/game/tick.go b/internal/game/tick.go
index 7567ca7..ab82254 100644
--- a/internal/game/tick.go
+++ b/internal/game/tick.go
@@ -4,9 +4,9 @@ import (
"fmt"
"math/rand"
- "thirdcollapse/internal/combat"
- "thirdcollapse/internal/player"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
)
func (g *Game) DisconnectTick() {
@@ -127,21 +127,21 @@ func (g *Game) WanderTick() {
if !ok {
continue
}
- if p.RoomID == m.fromRoom && p.OptionBool("mob_leave") {
- if m.level > 0 {
- sess.WriteLine(fmt.Sprintf("\n%s (level %d) leaves.", m.name, m.level))
- } else {
- sess.WriteLine(fmt.Sprintf("\n%s moves away.", m.name))
- }
+ if p.RoomID == m.fromRoom && p.OptionBool("mob_leave") {
+ if m.level > 0 {
+ sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s (level %d) leaves.", m.name, m.level)))
+ } else {
+ sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s moves away.", m.name)))
}
- if p.RoomID == m.toRoom && p.OptionBool("mob_enter") {
- if m.level > 0 {
- sess.WriteLine(fmt.Sprintf("\n%s (level %d) enters.", m.name, m.level))
- } else {
- sess.WriteLine(fmt.Sprintf("\n%s drifts in.", m.name))
- }
+ }
+ if p.RoomID == m.toRoom && p.OptionBool("mob_enter") {
+ if m.level > 0 {
+ sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s (level %d) enters.", m.name, m.level)))
+ } else {
+ sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s drifts in.", m.name)))
}
}
+ }
}
}
diff --git a/internal/game/types.go b/internal/game/types.go
index faed9e1..d41b18a 100644
--- a/internal/game/types.go
+++ b/internal/game/types.go
@@ -1,6 +1,6 @@
package game
-import "thirdcollapse/internal/object"
+import "thehouseoficarus/internal/object"
var EquipSlots = []object.EquipSlot{
object.SlotHead, object.SlotNeck, object.SlotTorso, object.SlotLegs,
diff --git a/internal/game/utils.go b/internal/game/utils.go
index 729ea97..2379fc2 100644
--- a/internal/game/utils.go
+++ b/internal/game/utils.go
@@ -6,9 +6,9 @@ import (
"strconv"
"strings"
- "thirdcollapse/internal/net"
- "thirdcollapse/internal/player"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
)
func plural(n int) string {
diff --git a/internal/net/server.go b/internal/net/server.go
index 1b6c99b..203539c 100644
--- a/internal/net/server.go
+++ b/internal/net/server.go
@@ -6,8 +6,10 @@ import (
"log"
"net"
"net/http"
+ "strings"
- "thirdcollapse/internal/config"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/config"
)
type SessionState int
@@ -30,6 +32,7 @@ const (
StateTalk
StateDropAllConfirm
StateCookRecipe
+ StateColorChoice
)
type Session struct {
@@ -51,11 +54,13 @@ type AccountEntry struct {
Characters []string
Aliases map[string]string
Colors map[string]string
+ Options map[string]any
}
type Server struct {
config *config.Config
telnetLn net.Listener
+ telnetTLSLn net.Listener
httpServer *http.Server
httpsServer *http.Server
hub *Hub
@@ -149,6 +154,26 @@ func NewServer(cfg *config.Config) (*Server, error) {
s.telnetLn = ln
}
+ if cfg.TelnetTLS.Enabled {
+ if cfg.TelnetTLS.CertFile == "" || cfg.TelnetTLS.KeyFile == "" {
+ return nil, fmt.Errorf("telnet_tls enabled but cert_file and key_file are required")
+ }
+ cert, err := tls.LoadX509KeyPair(cfg.TelnetTLS.CertFile, cfg.TelnetTLS.KeyFile)
+ if err != nil {
+ return nil, fmt.Errorf("telnet_tls cert: %w", err)
+ }
+ tlsCfg := &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ MinVersion: tls.VersionTLS12,
+ }
+ addr := fmt.Sprintf(":%d", cfg.TelnetTLS.Port)
+ ln, err := net.Listen("tcp", addr)
+ if err != nil {
+ return nil, fmt.Errorf("telnet_tls listen: %w", err)
+ }
+ s.telnetTLSLn = tls.NewListener(ln, tlsCfg)
+ }
+
if cfg.HTTP.Enabled {
s.httpServer = &http.Server{
Addr: fmt.Sprintf(":%d", cfg.HTTP.Port),
@@ -181,6 +206,10 @@ func (s *Server) ListenAndServe(handler func(*Session, string)) error {
go s.serveTelnet()
}
+ if s.telnetTLSLn != nil {
+ go s.serveTelnetTLS()
+ }
+
if s.httpServer != nil {
go s.serveHTTP()
}
@@ -189,7 +218,7 @@ func (s *Server) ListenAndServe(handler func(*Session, string)) error {
go s.serveHTTPS()
}
- if s.telnetLn == nil && s.httpServer == nil && s.httpsServer == nil {
+ if s.telnetLn == nil && s.telnetTLSLn == nil && s.httpServer == nil && s.httpsServer == nil {
return fmt.Errorf("no listeners configured")
}
@@ -211,6 +240,21 @@ func (s *Server) serveTelnet() {
}
}
+func (s *Server) serveTelnetTLS() {
+ for {
+ conn, err := s.telnetTLSLn.Accept()
+ if err != nil {
+ return
+ }
+ sess := &Session{
+ Conn: newTCPConn(conn),
+ State: StateAccountName,
+ }
+ s.hub.Add(sess)
+ go s.handleSession(sess, s.handler)
+ }
+}
+
func (s *Server) serveHTTP() {
err := s.httpServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
@@ -225,6 +269,53 @@ func (s *Server) serveHTTPS() {
}
}
+func artColor(r rune) string {
+ switch r {
+ case '@':
+ return color.FgCode("ansi", 4)
+ case '!', ':', '.':
+ return color.StyleCode("dim") + color.FgCode("ansi", 4)
+ case ',':
+ return color.FgCode("ansi", 5)
+ case '*':
+ return color.FgCode("ansi", 5)
+ case '+', '|', '-':
+ return color.FgCode("ansi", 3)
+ }
+ return ""
+}
+
+func colorizeRunes(s string) string {
+ var sb strings.Builder
+ current := ""
+ for _, r := range s {
+ want := artColor(r)
+ if want != current {
+ if current != "" {
+ sb.WriteString(color.Reset)
+ }
+ if want != "" {
+ sb.WriteString(want)
+ }
+ current = want
+ }
+ sb.WriteRune(r)
+ }
+ if current != "" {
+ sb.WriteString(color.Reset)
+ }
+ return sb.String()
+}
+
+func colorizeArt(art string) string {
+ welcomeText := "welcome to the house of icarus"
+ before, after, found := strings.Cut(art, welcomeText)
+ if found {
+ return colorizeRunes(before) + color.FgCode("ansi", 7) + welcomeText + color.Reset + colorizeRunes(after)
+ }
+ return colorizeRunes(art)
+}
+
func (s *Server) handleSession(sess *Session, handler func(*Session, string)) {
defer func() {
if r := recover(); r != nil {
@@ -236,22 +327,32 @@ func (s *Server) handleSession(sess *Session, handler func(*Session, string)) {
sess.Conn.Write([]byte("\033[2J\033[H"))
- welcomeart := `
- , 3333333 333 333 333 3333333 3333333
- | 33! 33! 333 33! 33! 333 33! 333 ':. +
- -+- * 3!! 3!3!3!3! !!3 3!3!!3! 3!3 !3! '::._ *
- | !!: !!: !!! !!: !!: :!! !!: !!! '._)
- : : : : : : : : :: : : ,
- . * ,
- , welcome to third collapse . *
-
- 3333333 333333 333 , 333 333333 3333333 333333 33333333
- !33 33! 333 33! 33! 33! 333 33! 333 !33 33!
- !3! 3!3 !3! 3!! 3!! + 3!3!3!3! 3!33!3! !33!! 3!!!:!
- :!! !!: !!! !!: !!: !!: !!! !!: !:! !!:
- :: :: : : :. : : ::.: : : ::.: : : : : : ::.: : : :: :::
-
-`
+ welcomeart := colorizeArt(`
+ | *
+ * + -+-
+ . , |
+. , . ,
+ *
+ @@@@@@@ @@@ @@@ @@@@@@@@ @@@ @@@ @@@@@@ @@@ @@@ @@@@@@ @@@@@@@@ +
+ @@! @@! @@@ @@! + @@! @@@ @@! @@@ @@! @@@ !@@ @@!
+ @!! @!@!@!@! @!!!:! @!@!@!@! @!@ !@! @!@ !@! !@@!! @!!!:! *
+ !!: !!: !!! !!: . !!: !!! !!: !!! !!: !!! !:! !!:
+ : : : : : :: ::: : : : : :. : :.:: : ::.: : : :: ::: .
+ . +
+ * welcome to the house of icarus , ,
+ , .
+ @@@@@@ @@@@@@@@ @@@ @@@@@@@ @@@@@@ @@@@@@@ @@@ @@@ @@@@@@
+ @@! @@@ @@! , @@! !@@ @@! @@@ @@! @@@ @@! @@@ !@@
+ | @!@ !@! @!!!:! !!@ !@! @!@!@!@! @!@!!@! @!@ !@! !@@!!
+ -+- !!: !!! !!: !!: :!! !!: !!! !!: :!! !!: !!! !:!
+ | : :. : : . : :: :: : : : : : : : :.:: : ::.: :
+ ,
+ . + , .
+ . ,
+ * . , *
+ +
+`)
+
sess.Write(welcomeart)
sess.Write("What's your account name? ")
diff --git a/internal/net/terminal.html b/internal/net/terminal.html
index 2d98178..3e2506a 100644
--- a/internal/net/terminal.html
+++ b/internal/net/terminal.html
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Third Collapse</title>
+<title>The House of Icarus</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
diff --git a/internal/object/item.go b/internal/object/item.go
index 7b2a107..6078712 100644
--- a/internal/object/item.go
+++ b/internal/object/item.go
@@ -3,7 +3,7 @@ package object
import (
"strings"
- "thirdcollapse/internal/world"
+ "thehouseoficarus/internal/world"
)
type EquipSlot string
diff --git a/internal/player/account.go b/internal/player/account.go
index 36fab43..99a052c 100644
--- a/internal/player/account.go
+++ b/internal/player/account.go
@@ -6,4 +6,5 @@ type Account struct {
Characters []string `yaml:"characters"`
Aliases map[string]string `yaml:"aliases"`
Colors map[string]string `yaml:"colors"`
+ Options map[string]any `yaml:"options"`
}
diff --git a/internal/player/player.go b/internal/player/player.go
index e0c7ab9..1cd8677 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -1,7 +1,7 @@
package player
-import "thirdcollapse/internal/action"
-import "thirdcollapse/internal/object"
+import "thehouseoficarus/internal/action"
+import "thehouseoficarus/internal/object"
type SkillName string
@@ -115,7 +115,7 @@ var OptionDefs = []OptionDef{
{"reserve", OptBool, true, nil, "Show full reserved item details"},
{"depletion", OptBool, false, nil, "Show depletion and despawn timers"},
{"despawn", OptBool, false, nil, "Show ground item despawn timers"},
- {"color", OptString, "ansi", []string{"none", "ansi", "xterm256"}, "Color output mode"},
+ {"color", OptString, "xterm256", []string{"none", "ansi", "xterm256"}, "Color output mode"},
{"mapwidth", OptInt, 30, nil, "Map width for the map command"},
{"mapheight", OptInt, 20, nil, "Map height for the map command"},
{"mappadding", OptString, "none", []string{"none", "x", "y", "xy"}, "Map output padding mode"},
@@ -123,7 +123,6 @@ var OptionDefs = []OptionDef{
{"queue_silently", OptBool, true, nil, "Suppress messages for queued tick actions"},
{"room_desc_width", OptInt, 70, nil, "Maximum width for room descriptions"},
{"unicode", OptBool, true, nil, "Unicode box-drawing characters"},
- {"prompt", OptString, "> ", nil, "Custom command prompt"},
}
var optionByName map[string]*OptionDef
@@ -150,7 +149,8 @@ type Player struct {
Credits int `yaml:"credits"`
Description string `yaml:"description"`
AttackStyle AttackStyle `yaml:"attack_style"`
- Options map[string]any `yaml:"options"`
+ Prompt string `yaml:"prompt"`
+ Options map[string]any `yaml:"-"`
Flags map[string]any `yaml:"flags"`
RegenerateTick int
Action *action.Action `yaml:"-"`
@@ -258,12 +258,9 @@ func New(name string) *Player {
Skills: make(map[SkillName]int),
Equipment: make(map[object.EquipSlot]string),
AttackStyle: Accurate,
- Options: make(map[string]any),
+ Prompt: "> ",
RoomID: 0,
}
- for _, def := range OptionDefs {
- p.Options[def.Name] = def.Default
- }
for _, s := range AllSkills {
p.Skills[s] = 0
}
diff --git a/internal/player/store.go b/internal/player/store.go
index c19d9fb..04af594 100644
--- a/internal/player/store.go
+++ b/internal/player/store.go
@@ -6,7 +6,7 @@ import (
"path/filepath"
"strings"
- "thirdcollapse/internal/object"
+ "thehouseoficarus/internal/object"
"gopkg.in/yaml.v3"
)
diff --git a/internal/world/mob.go b/internal/world/mob.go
index c161e10..d801b59 100644
--- a/internal/world/mob.go
+++ b/internal/world/mob.go
@@ -8,7 +8,7 @@ import (
"strings"
"sync"
- "thirdcollapse/internal/action"
+ "thehouseoficarus/internal/action"
"gopkg.in/yaml.v3"
)
diff --git a/internal/world/room.go b/internal/world/room.go
index 16fd3fd..c0f531e 100644
--- a/internal/world/room.go
+++ b/internal/world/room.go
@@ -2,7 +2,7 @@ package world
import (
"gopkg.in/yaml.v3"
- "thirdcollapse/internal/action"
+ "thehouseoficarus/internal/action"
)
type ExitDir string
diff --git a/worldbuilding_guide/README.md b/worldbuilding_guide/README.md
index 8555e49..b6f6de8 100644
--- a/worldbuilding_guide/README.md
+++ b/worldbuilding_guide/README.md
@@ -1,6 +1,6 @@
# World Building Guide
-Third Collapse is data-driven. Everything — rooms, items, mobs, objects, behaviors, drop tables, and recipes — is defined in YAML files under `data/`. No code changes needed to build a world.
+The House of Icarus is data-driven. Everything — rooms, items, mobs, objects, behaviors, drop tables, and recipes — is defined in YAML files under `data/`. No code changes needed to build a world.
## Files
diff --git a/worldbuilding_guide/behaviors.md b/worldbuilding_guide/behaviors.md
index a275c60..8e47883 100644
--- a/worldbuilding_guide/behaviors.md
+++ b/worldbuilding_guide/behaviors.md
@@ -21,7 +21,7 @@ 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."
+ message: "You manage to mine some {178}copper ore{/}."
- table: gem_table # reference a shared drop table
weight: 10
depletes: false # gem drops don't deplete the rock
@@ -31,6 +31,8 @@ respawn_message: "You see more ore in the rock."
respawn_broadcast: "A glint of copper catches your eye from some {name}."
```
+Drop messages support inline color tags: `{<0-255>}text{/}`. Use the item's color index to match its display color. Gradients also work: `{g:196,82}text{/}`.
+
Non-depleting gather (fishing):
```yaml
id: fish_trout
@@ -50,7 +52,7 @@ drops:
- item_id: raw_trout
weight: 100
depletes: false # never depletes
- message: "You catch a trout!"
+ message: "You catch a {69}raw trout{/}!"
```
Woodcutting with shared depletion and bird's nests:
@@ -72,7 +74,7 @@ drops:
- item_id: oak_logs
weight: 100
depletes: false # depletion is timer-based (deplete_timer)
- message: "You get some oak logs."
+ message: "You get some {113}oak logs{/}."
respawn_timer: 14 # ticks until tree respawns after being cut down
deplete_timer: 45 # max ticks before next gather depletes (counts down while chopping)
nest_chance: 256 # 1/256 chance for a bird's nest on each successful gather
@@ -99,7 +101,7 @@ drops:
- item_id: logs
weight: 100
depletes: true # regular tree depletes on first successful gather
- message: "You get some logs."
+ message: "You get some {107}logs{/}."
respawn_timer: 80
respawn_message: "A new tree grows in its place."
respawn_broadcast: "A {name} grows back."
diff --git a/worldbuilding_guide/items.md b/worldbuilding_guide/items.md
index 73f497b..e9e4352 100644
--- a/worldbuilding_guide/items.md
+++ b/worldbuilding_guide/items.md
@@ -3,6 +3,7 @@
```yaml
id: bronze_pickaxe
name: bronze pickaxe
+color: "178" # xterm-256 color index (0-255)
aliases: ["pick", "pickaxe"]
description: "A sturdy bronze pickaxe."
value: 10
@@ -17,6 +18,18 @@ tool_type: pickaxe # used by gather behaviors that require "tool: pic
tool_speed: 2 # reduces gather wait time
```
+### Color
+
+The `color` field accepts xterm-256 palette indices (0-255) with optional modifiers:
+
+```yaml
+color: "178" # bronze/gold
+color: "75 bold" # bold steel blue
+color: "g:196,208,226" # gradient red → orange → gold
+```
+
+In ANSI mode, extended colors (16-255) automatically downgrade to the nearest ANSI color. Use `colortable` in-game to see all 256 colors.
+
Key item (quest token, not equippable):
```yaml
id: pass_stub
@@ -43,7 +56,7 @@ burn_ticks: 24 # how long the fire burns
```yaml
id: bread
name: bread
-color: "fg=bright_yellow"
+color: "222"
description: "A fresh loaf of bread, still warm from the oven."
value: 5
heal_value: 5 # positive = heal, negative = damage
@@ -57,7 +70,7 @@ Define what items combine to make this item. Used by the `use` command.
```yaml
id: bread_dough
name: bread dough
-color: "fg=bright_yellow"
+color: "222"
made_from:
- items: [pot_of_flour]
qty: 1
diff --git a/worldbuilding_guide/objects.md b/worldbuilding_guide/objects.md
index 94aead7..6929606 100644
--- a/worldbuilding_guide/objects.md
+++ b/worldbuilding_guide/objects.md
@@ -4,6 +4,7 @@ Object defined — links to a behavior:
```yaml
id: copper_rock
name: copper rock
+color: "178" # xterm-256 color index (0-255)
behavior: mine_copper
```
@@ -11,9 +12,12 @@ Tree object:
```yaml
id: oak_tree
name: oak tree
+color: "113"
behavior: chop_oak
```
+Color accepts xterm-256 indices with optional modifiers (`bold`, `dim`, `underline`) and gradients (`g:196,82`). In ANSI mode, extended colors downgrade to the nearest ANSI color.
+
Hidden object — doesn't appear in room's object list, only discoverable via description or experimentation:
```yaml
id: iron_gate
diff --git a/worldbuilding_guide/recipes.md b/worldbuilding_guide/recipes.md
index f40a9a7..500954f 100644
--- a/worldbuilding_guide/recipes.md
+++ b/worldbuilding_guide/recipes.md
@@ -1,4 +1,4 @@
-# Third Collapse - World Building Guide
+# The House of Icarus - World Building Guide
## Recipes
@@ -13,21 +13,21 @@ Recipe files live in `data/recipes/<id>.yaml`.
### Recipe YAML Reference
-| Field | Type | Description |
-|-------|------|-------------|
-| `id` | string | Unique recipe identifier |
-| `type` | string | Recipe type (cooking, smithing, crafting, etc.) |
-| `skill` | string | Skill checked for success |
-| `level` | int | Required skill level |
-| `xp` | int | XP awarded on success |
-| `wait` | float64 | Ticks per craft cycle (default 6) |
-| `station` | []string | Station object IDs required |
-| `consume` | []ConsumeEntry | Ingredients consumed (see below) |
-| `output` | string | ItemID produced on success |
-| `fail` | string | ItemID produced on failure (optional) |
-| `message` | string | Success message |
-| `fail_message` | string | Failure message |
-| `success` | SuccessFormula | Optional skill check formula (see Behaviors) |
+| Field | Type | Description |
+| -------------- | -------------- | ----------------------------------------------- |
+| `id` | string | Unique recipe identifier |
+| `type` | string | Recipe type (cooking, smithing, crafting, etc.) |
+| `skill` | string | Skill checked for success |
+| `level` | int | Required skill level |
+| `xp` | int | XP awarded on success |
+| `wait` | float64 | Ticks per craft cycle (default 6) |
+| `station` | []string | Station object IDs required |
+| `consume` | []ConsumeEntry | Ingredients consumed (see below) |
+| `output` | string | ItemID produced on success |
+| `fail` | string | ItemID produced on failure (optional) |
+| `message` | string | Success message |
+| `fail_message` | string | Failure message |
+| `success` | SuccessFormula | Optional skill check formula (see Behaviors) |
The recipe's name in menus comes from the output item's `name` field, colored using the output item's `color`.
diff --git a/worldbuilding_guide/rooms.md b/worldbuilding_guide/rooms.md
index 95e1972..f6e74ea 100644
--- a/worldbuilding_guide/rooms.md
+++ b/worldbuilding_guide/rooms.md
@@ -11,6 +11,18 @@ exits:
east: 3
```
+### Inline Color Tags
+
+Room descriptions support inline color tags using `{spec}text{/}` syntax. Untagged text uses the `room_desc` color.
+
+```yaml
+description: "On the table lies a {182 bold}mysterious vase{/} with a rose in it."
+```
+
+Tag spec format: `{<0-255> [bold] [dim] [underline]}text{/}`
+
+Gradients: `{g:196,82}gradient text{/}`. Multi-stop: `{g:45,39,59}three stops{/}`.
+
### Exits — simple vs conditional
Simple exit — always passable:
diff --git a/worldbuilding_guide/toggles.md b/worldbuilding_guide/toggles.md
deleted file mode 100644
index 00b48ba..0000000
--- a/worldbuilding_guide/toggles.md
+++ /dev/null
@@ -1,18 +0,0 @@
-## 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 |
-
----
-