From 389e307f205f9b7edf604fb9f86e1038ead736ef Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 16 Jun 2026 23:37:50 -0400 Subject: feat: major xterm256 color overhaul, see worldbuilding docs. --- .gitignore | 2 +- AGENTS.md | 40 +- Makefile | 2 +- README.md | 41 +- TODO.md | 2 +- WORLDBUILDING.md | 975 --------------------------------- cmd/mud/main.go | 9 +- config.yaml | 53 +- data/behaviors/chop_magic.yaml | 2 +- data/behaviors/chop_mahogany.yaml | 2 +- data/behaviors/chop_maple.yaml | 2 +- data/behaviors/chop_oak.yaml | 2 +- data/behaviors/chop_redwood.yaml | 2 +- data/behaviors/chop_teak.yaml | 2 +- data/behaviors/chop_tree.yaml | 2 +- data/behaviors/chop_willow.yaml | 2 +- data/behaviors/chop_yew.yaml | 2 +- data/behaviors/fish_angler.yaml | 2 +- data/behaviors/fish_bait.yaml | 6 +- data/behaviors/fish_cage.yaml | 2 +- data/behaviors/fish_fly.yaml | 6 +- data/behaviors/fish_harpoon.yaml | 6 +- data/behaviors/fish_net.yaml | 10 +- data/behaviors/fish_sacred_eel.yaml | 2 +- data/behaviors/fish_swamp.yaml | 4 +- data/behaviors/mine_adamantite.yaml | 2 +- data/behaviors/mine_clay.yaml | 2 +- data/behaviors/mine_coal.yaml | 2 +- data/behaviors/mine_copper.yaml | 2 +- data/behaviors/mine_gold.yaml | 2 +- data/behaviors/mine_iron.yaml | 2 +- data/behaviors/mine_mithril.yaml | 2 +- data/behaviors/mine_runite.yaml | 2 +- data/behaviors/mine_scrap.yaml | 2 +- data/behaviors/mine_silver.yaml | 2 +- data/behaviors/mine_tin.yaml | 2 +- data/help/color.yaml | 82 +-- data/help/option.yaml | 3 +- data/help/prompt.yaml | 26 +- data/items/adamant_axe.yaml | 2 +- data/items/adamant_pickaxe.yaml | 2 +- data/items/adamantite_ore.yaml | 2 +- data/items/anchovies.yaml | 2 +- data/items/ashes.yaml | 2 +- data/items/big_fishing_net.yaml | 2 +- data/items/birds_nest.yaml | 2 +- data/items/black_axe.yaml | 2 +- data/items/black_pickaxe.yaml | 2 +- data/items/bones.yaml | 2 +- data/items/bread.yaml | 2 +- data/items/bread_dough.yaml | 2 +- data/items/bronze_axe.yaml | 2 +- data/items/bronze_pickaxe.yaml | 2 +- data/items/bronze_sword.yaml | 2 +- data/items/bucket_of_water.yaml | 2 +- data/items/burnt_fish.yaml | 2 +- data/items/burnt_meat.yaml | 2 +- data/items/christmas_cracker.yaml | 2 +- data/items/clay.yaml | 2 +- data/items/coal.yaml | 2 +- data/items/copper_ore.yaml | 2 +- data/items/credits.yaml | 2 +- data/items/dragon_axe.yaml | 2 +- data/items/dragon_pickaxe.yaml | 2 +- data/items/feather.yaml | 2 +- data/items/firesteel.yaml | 2 +- data/items/fishing_bait.yaml | 2 +- data/items/fishing_rod.yaml | 2 +- data/items/fly_fishing_rod.yaml | 2 +- data/items/gold_ore.yaml | 2 +- data/items/harpoon.yaml | 2 +- data/items/herring.yaml | 2 +- data/items/iron_axe.yaml | 2 +- data/items/iron_ore.yaml | 2 +- data/items/iron_pickaxe.yaml | 2 +- data/items/lighter.yaml | 2 +- data/items/lobster_pot.yaml | 2 +- data/items/logs.yaml | 2 +- data/items/magic_logs.yaml | 2 +- data/items/mahogany_logs.yaml | 2 +- data/items/maple_logs.yaml | 2 +- data/items/matches.yaml | 2 +- data/items/mithril_axe.yaml | 2 +- data/items/mithril_ore.yaml | 2 +- data/items/mithril_pickaxe.yaml | 2 +- data/items/oak_logs.yaml | 2 +- data/items/party_hat_blue.yaml | 2 +- data/items/party_hat_green.yaml | 2 +- data/items/party_hat_pink.yaml | 2 +- data/items/party_hat_red.yaml | 2 +- data/items/party_hat_white.yaml | 2 +- data/items/party_hat_yellow.yaml | 2 +- data/items/pass_stub.yaml | 2 +- data/items/pastry_dough.yaml | 2 +- data/items/pizza_dough.yaml | 2 +- data/items/pot_of_flour.yaml | 2 +- data/items/raw_anchovies.yaml | 2 +- data/items/raw_anglerfish.yaml | 2 +- data/items/raw_bass.yaml | 2 +- data/items/raw_cave_eel.yaml | 2 +- data/items/raw_cod.yaml | 2 +- data/items/raw_herring.yaml | 2 +- data/items/raw_lobster.yaml | 2 +- data/items/raw_mackerel.yaml | 2 +- data/items/raw_pike.yaml | 2 +- data/items/raw_rainbow_fish.yaml | 2 +- data/items/raw_salmon.yaml | 2 +- data/items/raw_sardine.yaml | 2 +- data/items/raw_shark.yaml | 2 +- data/items/raw_shrimps.yaml | 2 +- data/items/raw_slimy_eel.yaml | 2 +- data/items/raw_swordfish.yaml | 2 +- data/items/raw_trout.yaml | 2 +- data/items/raw_tuna.yaml | 2 +- data/items/redwood_logs.yaml | 2 +- data/items/rune_axe.yaml | 2 +- data/items/rune_pickaxe.yaml | 2 +- data/items/runite_ore.yaml | 2 +- data/items/sacred_eel.yaml | 2 +- data/items/salmon.yaml | 2 +- data/items/sardine.yaml | 2 +- data/items/scrap_metal.yaml | 2 +- data/items/shrimps.yaml | 2 +- data/items/silver_ore.yaml | 2 +- data/items/small_fishing_net.yaml | 2 +- data/items/steel_axe.yaml | 2 +- data/items/steel_pickaxe.yaml | 2 +- data/items/teak_logs.yaml | 2 +- data/items/tin_ore.yaml | 2 +- data/items/trout.yaml | 2 +- data/items/uncut_diamond.yaml | 2 +- data/items/uncut_emerald.yaml | 2 +- data/items/uncut_ruby.yaml | 2 +- data/items/uncut_sapphire.yaml | 2 +- data/items/willow_logs.yaml | 2 +- data/items/yew_logs.yaml | 2 +- data/objects/adamantite_rock.yaml | 2 +- data/objects/anglerfish_spot.yaml | 2 +- data/objects/bait_fishing_spot.yaml | 2 +- data/objects/cage_fishing_spot.yaml | 2 +- data/objects/clay_rock.yaml | 2 +- data/objects/coal_rock.yaml | 2 +- data/objects/cooking_range.yaml | 2 +- data/objects/copper_rock.yaml | 2 +- data/objects/fire.yaml | 2 +- data/objects/fly_fishing_spot.yaml | 2 +- data/objects/gold_rock.yaml | 2 +- data/objects/harpoon_fishing_spot.yaml | 2 +- data/objects/iron_gate.yaml | 2 +- data/objects/iron_rock.yaml | 2 +- data/objects/lumby_fountain.yaml | 2 +- data/objects/magic_tree.yaml | 2 +- data/objects/mahogany_tree.yaml | 2 +- data/objects/maple_tree.yaml | 2 +- data/objects/mithril_rock.yaml | 2 +- data/objects/net_fishing_spot.yaml | 2 +- data/objects/oak_tree.yaml | 2 +- data/objects/redwood_tree.yaml | 2 +- data/objects/runite_rock.yaml | 2 +- data/objects/sacred_eel_spot.yaml | 2 +- data/objects/scrap_pile.yaml | 2 +- data/objects/silver_rock.yaml | 2 +- data/objects/swamp_fishing_spot.yaml | 2 +- data/objects/teak_tree.yaml | 2 +- data/objects/tin_rock.yaml | 2 +- data/objects/tree.yaml | 2 +- data/objects/willow_tree.yaml | 2 +- data/objects/yew_tree.yaml | 2 +- go.mod | 2 +- internal/color/color.go | 475 ++++++++++++---- internal/config/config.go | 69 ++- internal/game/action.go | 12 +- internal/game/action_burn.go | 66 +-- internal/game/action_cook.go | 8 +- internal/game/action_gather.go | 15 +- internal/game/action_room.go | 4 +- internal/game/action_search.go | 19 +- internal/game/action_talk.go | 12 +- internal/game/action_toggle.go | 8 +- internal/game/action_use.go | 19 +- internal/game/cmd_alias.go | 2 +- internal/game/cmd_attack.go | 22 +- internal/game/cmd_color.go | 39 +- internal/game/cmd_colortable.go | 151 +++++ internal/game/cmd_cook.go | 6 +- internal/game/cmd_description.go | 4 +- internal/game/cmd_drop.go | 12 +- internal/game/cmd_eat.go | 10 +- internal/game/cmd_equipment.go | 4 +- internal/game/cmd_get.go | 26 +- internal/game/cmd_inventory.go | 9 +- internal/game/cmd_look.go | 24 +- internal/game/cmd_map.go | 4 +- internal/game/cmd_move.go | 6 +- internal/game/cmd_option.go | 35 +- internal/game/cmd_prompt.go | 33 ++ internal/game/cmd_queued.go | 4 +- internal/game/cmd_quit.go | 8 +- internal/game/cmd_remove.go | 11 +- internal/game/cmd_say.go | 8 +- internal/game/cmd_score.go | 26 +- internal/game/cmd_search.go | 4 +- internal/game/cmd_style.go | 4 +- internal/game/cmd_use.go | 6 +- internal/game/cmd_walk.go | 6 +- internal/game/cmd_wear.go | 12 +- internal/game/color.go | 24 +- internal/game/game.go | 36 +- internal/game/help.go | 8 +- internal/game/login_account.go | 31 +- internal/game/login_char.go | 11 +- internal/game/map.go | 2 +- internal/game/map_test.go | 2 +- internal/game/prompt.go | 49 +- internal/game/table.go | 18 +- internal/game/tick.go | 30 +- internal/game/types.go | 2 +- internal/game/utils.go | 6 +- internal/net/server.go | 137 ++++- internal/net/terminal.html | 2 +- internal/object/item.go | 2 +- internal/player/account.go | 1 + internal/player/player.go | 15 +- internal/player/store.go | 2 +- internal/world/mob.go | 2 +- internal/world/room.go | 2 +- worldbuilding_guide/README.md | 2 +- worldbuilding_guide/behaviors.md | 10 +- worldbuilding_guide/items.md | 17 +- worldbuilding_guide/objects.md | 4 + worldbuilding_guide/recipes.md | 32 +- worldbuilding_guide/rooms.md | 12 + worldbuilding_guide/toggles.md | 18 - 233 files changed, 1475 insertions(+), 1758 deletions(-) delete mode 100644 WORLDBUILDING.md create mode 100644 internal/game/cmd_colortable.go create mode 100644 internal/game/cmd_prompt.go delete mode 100644 worldbuilding_guide/toggles.md diff --git a/.gitignore b/.gitignore index 3ab0ef3..df654ce 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ !/data/players/accounts/.gitkeep /data/players/characters/* !/data/players/characters/.gitkeep -tc +thoi .cache/ .npm/ .local/ diff --git a/AGENTS.md b/AGENTS.md index 6dc559b..7dc4c47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -Third Collapse — a Runescape-like sci-fi MUD written in Go. Data-driven via YAML files. No database, no ORM. +The House of Icarus — a Runescape-like sci-fi MUD written in Go. Data-driven via YAML files. No database, no ORM. Set on a terraformed asteroid where technology has regressed. "Science" replaces Prayer, "Technology" replaces Magic, Scavenging replaces Runecrafting. @@ -37,7 +37,7 @@ internal/ combat/ Combat state tracking, combat lock, OSRS-style combat formulas object/ ItemDef, ObjectDef, item/object YAML loaders, EquipSlot, WeaponType, ItemStats engine/ Tick scheduler — configurable ms interval, subscriber pattern, ToTicks() - color/ ANSI 16-color and xterm256-color escape code tags (text, , etc.) + color/ xterm-256 color system with auto ANSI downgrade, numeric palette (0-255), gradients, inline tags ``` `internal/game/` is the largest package. Key files: @@ -111,7 +111,23 @@ data/players/characters/ Character YAML (gitignored) Wander config (`wander_rooms`, `wander_interval`) is set per-instance in room YAML — mob defs are generic. Mobs wander via legal (unconditioned) room exits; objects teleport between rooms in their list. -See `WORLDBUILDING.md` for full YAML format reference with examples. +See `worldbuilding_guide/` for full YAML format reference with examples. + +## Inline Color Tags + +Room descriptions (and prompts) support inline color tags using `{spec}text{/}` syntax. Untagged text in room descriptions uses the `room_desc` color target. + +```yaml +description: "On the table lies a {182 bold}mysterious vase{/} with a rose in it." +``` + +Tag spec format matches the color command: `{<0-255> [bold] [dim] [underline]}text{/}`. + +Gradients are supported: `{g:196,82}gradient text{/}`. Multi-stop: `{g:45,39,59}three stops{/}`. + +Gradients interpolate in RGB space across the xterm-256 palette. In ANSI mode, each character maps to the nearest ANSI color. + +Item and object YAML `color` fields also support gradient syntax: `color: "g:196,208,226"`. ## Commands @@ -127,14 +143,17 @@ See `WORLDBUILDING.md` for full YAML format reference with examples. | `exits` | List available exits | | `help [topic]` | Help topics from data/help/ | | `map` | BFS ASCII map centered on player | -| `option` / `options [name] [value]` | Character settings (see Option System) | +| `option` / `options [name] [value]` | Account-wide settings (see Option System) | | `alias ` | Account-wide command alias with arg passthrough | | `unalias ` | Remove an alias | | `description` / `desc` | Set custom player description | +| `prompt ` | Set custom per-character command prompt | | `queued` | Show pending tick actions | | `color` / `colors [target] [value]` | Customize display colors (account-wide) | | `color reset ` | Reset a color target to server default | | `color off` | Disable color for a specific target | +| `colortable` | Display xterm-256 color reference chart | +| `style ` | Set combat style: accurate, aggressive, defensive, balanced (prefix match) | ### Free (stackable, execute before active) | Command | Description | @@ -142,7 +161,6 @@ See `WORLDBUILDING.md` for full YAML format reference with examples. | `wear` / `wield ` | Equip item to correct slot. `wear all` auto-equips best per slot | | `wear all` | Auto-equips highest-value item per slot from inventory | | `remove` / `unwear` / `unwield ` | Unequip item back to inventory | -| `style ` | Set combat style: accurate, aggressive, defensive, balanced (prefix match) | | `eat ` | Eat food to heal (3-tick cooldown, can eat during combat) | ### Active (replaces previous active, queued per tick) @@ -343,7 +361,7 @@ See `internal/game/action_burn.go`. ## Option System -Player options set via `option `: +Account-wide options set via `option `. Options are stored in the account YAML and shared by all characters on the same account. | Option | Type | Default | Description | |---|---|---|---| @@ -357,7 +375,7 @@ Player options set via `option `: | `reserve` | bool | true | Show full reserved item details | | `depletion` | bool | false | Show depletion and despawn timers | | `despawn` | bool | false | Show ground item despawn timers | -| `color` | string | "none" | none/ansi/xterm256 — color output mode | +| `color` | string | "xterm256" | none/ansi/xterm256 — color output mode | | `mapwidth` | int | 30 | Map viewport width | | `mapheight` | int | 20 | Map viewport height | | `mappadding` | string | "none" | none/x/y/xy — blank-row stripping | @@ -365,7 +383,10 @@ Player options set via `option `: | `queue_silently` | bool | true | Suppress queue messages | | `room_desc_width` | int | 70 | Room desc line wrap width | | `unicode` | bool | true | Unicode box-drawing characters | -| `prompt` | string | "> " | Custom command prompt | + +Prompt is NOT an option — it is a per-character setting via the `prompt` command. Default: `"> "`. Stored in character YAML as `prompt:`. + +New accounts are asked "Should I disable color? [y/N]" after password creation. Pressing enter defaults to No (xterm256 enabled). Choosing Y sets color to "none". ## ItemDef Fields @@ -438,6 +459,7 @@ From `internal/net/server.go`: | `StatePassword` | Entering password | | `StateNewAccountPass` | Setting password for new account | | `StateNewAccountConfirm` | Confirming password | +| `StateColorChoice` | New account: "Should I disable color?" | | `StateMenu` | Main menu | | `StateNewCharName` | Naming new character | | `StateRenameAccount` | Renaming account | @@ -449,6 +471,8 @@ From `internal/net/server.go`: | `StateChangeDescription` | Setting player description | | `StateTalk` | NPC conversation input | | `StateDropAllConfirm` | Confirming `drop all` | +| `StateCookRecipe` | Cook recipe selection | +| `StateColorChoice` | New account color preference | ## Adding a New Skill diff --git a/Makefile b/Makefile index 11c8a36..fb15282 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: build run test vet clean -BINARY=tc +BINARY=thoi DATA_DIR=$(PWD)/data build: diff --git a/README.md b/README.md index 1eb6718..d1360a7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Third Collapse +# The House of Icarus -**Third Collapse** is a simple open source MUD inspired by classic early 2000s tick-based skilling MMOs. It is a low-tech, neo-dark age, sci-fi game where edged weapons and bows are more common than computers and guns. +**The House of Icarus** is a simple open source MUD inspired by classic early 2000s tick-based skilling MMOs. It is a low-tech, neo-dark age, sci-fi game where edged weapons and bows are more common than computers and guns. There is not much here yet. @@ -49,26 +49,26 @@ There is not much here yet. Install make and Go then ``` -git clone https://codeberg.org/historia/thirdcollapse -cd thirdcollapse +git clone https://codeberg.org/historia/thehouseoficarus +cd thehouseoficarus make -./tc [port] +./thoi ``` -Then just telnet to the port. If no port is specified, the server will run on port 4000. +Then just telnet to the port configured in `config.yaml`. -# Server Options +# Server Options (config.yaml) -The tc binary checks for a file named config.yaml in the local directory, or else it creates one. +The `thoi` binary checks for a file named config.yaml in the local directory, or else it creates one. -The game is designed to run with 600ms game ticks. You can change this without issue, but it will change how fast *everything* in the game happens. If you just want the game to "respond faster", that's not possible. **The ticks are a game mechanic.** Techniques like as science flicking, weapon switches, and certain power-skilling methods would not work with instant response or short ticks. Being able to instantly walk anywhere would negate the Agility skill, walk command, APS system, and fast-travel technology. +The game is designed to run with 600ms game ticks. If you want the game to "respond instantly", that's not possible. **The ticks are a game mechanic.** Techniques like as science flicking, science/gear switches, the Agility/APS system, and certain power-skilling methods would not work with instant response or short ticks. ``` game: tick_length: 600 ``` -The telnet/http/https options are pretty self-explanatory. TC does not handle HTTPS certs for you. Use something like [certbot](https://certbot.eff.org/) on your server. The HTTP(S) server is just for a simple web client. +The telnet/http/https options are pretty self-explanatory. THOI does not handle HTTPS certs for you. Use something like [certbot](https://certbot.eff.org/) on your server. The HTTP(S) server is just for a simple web client. ``` telnet: @@ -86,17 +86,30 @@ https: # Setting -TC is a low-tech sci-fi MUD. Computer systems are severely regulated following an attempted hostile takeover of Earth by a benevolent dictator AI. Corrupt politicians were jailed. Harmful industries shuttered. Wealth and power were redistributed as the AI began to build Utopia. +THOI is a low-tech sci-fi MUD. Computer systems are severely regulated following an attempted hostile takeover of Earth by a benevolent dictator AI. Corrupt politicians were jailed. Harmful industries shuttered. Wealth and power were redistributed as the AI began to build Utopia. The panicked ruling classes pooled their resources into an astroturfed revolution of Neoluddites who embrace techno-terrorism in the name of supposed freedom. It worked. Radio towers and data centers burn in a global wave of misplaced violence. The infrastructure of the internet is destroyed. Wireless networks are banned. A new, intentional Dark Age descends called the Second Collapse. Demagogues vow to never again let intelligent machines rule mankind. -Twenty four years later, the outer belt asteroid Gaia 04 hasn't had communication from Earth for weeks. That wouldn't be a problem for most Belters except your paycheck came from an Earth corp, so now you're de facto unemployed. What are you going to do now, become a fisherman? +Pockets of radicalized settlers on the terraformed asteroid Gaia 04 took the tenets of the Second Collapse to their own home, destroying critical communications, astroculture, and manufacturing systems in the name of freedom from AI control. The unrest not only shatters communication to Earth, but stops the few supply and trade ships that ever trucked resources to the outer belt. + +Wounded, Gaia 04 struggles on. Everyone from astrobotanists to business executives must remake their lives as farmers, fishers, and miners as they survive in the primitive ashes of the fallen colony. + +## Major Factions + +**Neoluddites** +Anti-tech mostly-extremists who brought about the Second Collapse by destroying electronic networks and thinking machines. Once pawns of Earth's ruling class, they've evolved into a self-sufficient traditionalist culture with inconsistent views about what technology should be permitted to exist. + +**Utopians** +Those who embraced AI control of society and tried to stop the Neoluddites through both policy and violence. Their views range from religious exaltation of thinking machines to mere agreement that life was easier when the AI handled the governance and marginalia. + +**Craggers** +Colonists who don't take a side in the conflict one way or the other. Everyone from criminals escaping their past to once well-paid Earth engineers overseeing terraforming operations. # AI Disclosure -Huge technical parts of this codebase were ~~vibe coded~~ ~~agentically engineered~~ written by a qualified computer scientist with LLM assistance. +Huge technical parts of this codebase were ~~completely vibe coded~~ ~~agentically engineered~~ written with LLM assistance. -LLMs will **not** generate, aid, or edit any creative part of the game world (e.g. story, characters, map, room descriptions, quests). To be clear, LLMs do generate example content during development, so you might see that somewhere in this repo, but not in the final game world. +LLMs will **not** generate any creative part of the game world (e.g. story, characters, rooms, quests). LLMs do generate *example* content for dev testing, so you might see that somewhere, but not in the final game. # License diff --git a/TODO.md b/TODO.md index 6ae968f..26c3c96 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ # TODO -Upcoming work for Third Collapse. See `AGENTS.md` for architecture, commands, and reference. +Upcoming work for The House of Icarus. See `AGENTS.md` for architecture, commands, and reference. ## Phase 1: Core Skill Chains (breadth over depth) diff --git a/WORLDBUILDING.md b/WORLDBUILDING.md deleted file mode 100644 index 93c40aa..0000000 --- a/WORLDBUILDING.md +++ /dev/null @@ -1,975 +0,0 @@ -# World Building Guide - -Third Collapse is data-driven. Everything — rooms, items, mobs, objects, behaviors — is defined in YAML files under `data/`. No code changes needed to build a world. - -## Quick Reference - -| What you want | Where to put it | -|---|---| -| A room | `data/rooms/.yaml` | -| An item (sword, ore, key) | `data/items/.yaml` | -| A mob (NPC, monster) | `data/mobs/.yaml` | -| An interactive object (rock, lever, door) | `data/objects/.yaml` | -| A behavior (mining, dialog, toggle) | `data/behaviors/.yaml` | -| A shared drop table | `data/drops/.yaml` | - -## State: World vs Player - -**World flags** (`set_flags`, checked with `flag`) are shared by every player on the server. A door opened by one player is open for everyone. A lever pulled once changes the world for all. - -**Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest progress, "has read the sign," "paid the toll" — these are different for each player. Saved to the character YAML and persist across logins. - ---- - -## Rooms - -Minimal room: -```yaml -id: 1 -name: "Town Square" -description: "Cobblestone paths lead in all directions. A fountain gurgles peacefully." -exits: - north: 2 - west: 7 - east: 3 -``` - -### Exits — simple vs conditional - -Simple exit — always passable: -```yaml -exits: - north: 2 -``` - -Conditional exit — blocked until a world flag is set: -```yaml -exits: - north: - room: 11 - condition: - flag: gate_open - value: true - blocked_message: "A heavy iron gate blocks the way north." -``` - -Conditional exit — blocked unless the PLAYER has a flag (key, permission, quest state): -```yaml -exits: - east: - room: 12 - condition: - player_flag: has_vault_key - value: true - blocked_message: "The vault door is locked. You need a key." -``` - -Conditional exit with compound condition — requires both a world flag AND a player flag: -```yaml -exits: - north: - room: 20 - condition: - all_of: - - flag: bridge_repaired - value: true - - player_flag: paid_toll - value: true - blocked_message: "The bridge is out, and the toll collector blocks the path." -``` - -### Spawns — ground items that respawn - -```yaml -spawns: - - item_id: bronze_pickaxe - quantity: 1 - respawn_ticks: 30 # reappears 30 ticks (18 seconds) after being picked up - - item_id: copper_ore - quantity: 3 - respawn_ticks: 50 -``` - -### Mobs — NPCs placed in the room - -Simple string (no wandering): -```yaml -mobs: - - "newbie_trainer" - - "man" -``` - -With wander config per-instance: -```yaml -mobs: - - id: man - wander_interval: 10 # attempts to wander every 10 ticks - - id: man - wander_interval: 15 - wander_rooms: [1, 4, 5] # optional — only exit to these rooms -``` - -Mob wander config lives in the room YAML, not in the mob definition. This keeps mobs generic -so the same `man` can wander differently depending on where it's placed. Mobs wander through -legal (unconditioned) room exits. If no legal exits exist, the mob stays still. Mobs with -no `wander_interval` never wander. - -### Objects — interactive fixtures - -```yaml -objects: - - id: copper_rock # simple placement - - id: copper_rock # second instance - - id: fishing_spot - wander_rooms: [7, 8, 9] # teleports between these rooms - wander_interval: 12 # every 12 ticks - - id: iron_gate # hidden object (see below) -``` - -### On-enter scripts — messages when a player arrives - -```yaml -on_enter: - - message: "The guard barks: \"State your business!\"" - condition: - player_flag: talked_to_guard - not: true # only first visit - - - message: "The guard nods. \"Back again?\"" - condition: - player_flag: talked_to_guard - value: true # subsequent visits -``` - ---- - -## Items - -```yaml -id: bronze_pickaxe -name: bronze pickaxe -aliases: ["pick", "pickaxe"] -description: "A sturdy bronze pickaxe." -value: 10 -stackable: false -equip_slot: main_hand # optional — where it equips -weapon_type: melee # optional — melee or ranged -stats: # optional — combat bonuses - attack_bonus: 2 - strength_bonus: 1 -speed: 5 # ticks between attacks -tool_type: pickaxe # used by gather behaviors that require "tool: pickaxe" -tool_speed: 2 # reduces gather wait time -``` - -Key item (quest token, not equippable): -```yaml -id: pass_stub -name: pass stub -description: "A crumpled slip of paper stamped with the guard's seal." -value: 0 -stackable: false -``` - ---- - -## Mobs - -Basic combat mob: -```yaml -id: "man" -name: "man" -description: "A shabby-looking man." -attack: 1 -strength: 1 -defense: 1 -hp: 7 -speed: 5 -aggressive: false -respawn_ticks: 30 -drops: - remains: "bones" # always dropped on death - loot: - - item_id: "credits" - weight: 98 - quantity: 10 - - item_id: "credits" - weight: 2 - quantity: 150 -idle_descriptions: - - "scribbles something in a small notebook" - - "gazes skyward at the clouds" -combat_descriptions: - - "is engaged in a fight to the death with %s" -``` - -Mob with a behavior — can be talked to, toggled, etc: -```yaml -id: "guard" -name: "Guard" -behavior: guard_talk # links to data/behaviors/guard_talk.yaml -unique: true # displays as "Guard" not "a guard" -protected: true # cannot be attacked -attack: 5 -strength: 5 -defense: 5 -hp: 30 -speed: 5 -aggressive: false -respawn_ticks: 60 -idle_descriptions: - - "scans the area with a watchful eye" - - "adjusts the grip on his weapon" -``` - -Note: `wander_rooms` and `wander_interval` are NOT set on the mob definition. Wander config -is per-instance in the room YAML (see Rooms > Mobs section above). - ---- - -## Objects - -Object defined — links to a behavior: -```yaml -id: copper_rock -name: copper rock -behavior: mine_copper -``` - -Tree object: -```yaml -id: oak_tree -name: oak tree -behavior: chop_oak -``` - -Hidden object — doesn't appear in room's object list, only discoverable via description or experimentation: -```yaml -id: iron_gate -name: iron gate -behavior: iron_gate_toggle -hidden: true -props: - description: "A heavy iron gate set into the north wall." -``` - -Decorative object (no behavior): -```yaml -id: lumby_fountain -name: town fountain -behavior: "" -props: - description: "Clear water sparkles in the sunlight." -``` - ---- - -## Behaviors - -### Gather (mining, fishing, woodcutting) - -Mining — per-drop depletion: -```yaml -id: mine_copper -type: gather -skill: mining -level: 1 -xp: 17 # XP awarded per successful gather -base_wait: 8 # ticks between attempts -tool: pickaxe # requires item with tool_type: pickaxe -success: - base: 0.40 # 40% base chance - per_level: 0.01 # +1% per level above requirement - cap: 0.95 # 95% max -gather_message: "You swing your pickaxe at the rock..." -fail_message: "You chip away but get nothing useful." -drops: - - item_id: copper_ore - weight: 90 # 90% chance when roll succeeds - depletes: true # rock becomes depleted after this drop - message: "You manage to mine some copper ore." - - table: gem_table # reference a shared drop table - weight: 10 - depletes: false # gem drops don't deplete the rock - message: "You spot a glint of something valuable!" -respawn_timer: 50 # ticks until rock respawns -respawn_message: "You see more ore in the rock." -respawn_broadcast: "A glint of copper catches your eye from some {name}." -``` - -Non-depleting gather (fishing): -```yaml -id: fish_trout -type: gather -skill: fishing -level: 1 -xp: 10 -base_wait: 4 -tool: fishing_rod -success: - base: 0.30 - per_level: 0.01 - cap: 0.90 -gather_message: "You cast your line into the water..." -fail_message: "Nothing seems to bite." -drops: - - item_id: raw_trout - weight: 100 - depletes: false # never depletes - message: "You catch a trout!" -``` - -Woodcutting with shared depletion and bird's nests: -```yaml -id: chop_oak -type: gather -skill: woodcutting -level: 15 -xp: 37 -base_wait: 6 -tool: axe -success: - base: 0.40 - per_level: 0.01 - cap: 0.90 -gather_message: "You swing your axe at the oak tree..." -fail_message: "You swing but get no logs." -drops: - - item_id: oak_logs - weight: 100 - depletes: false # depletion is timer-based (deplete_timer) - message: "You get some oak logs." -respawn_timer: 14 # ticks until tree respawns after being cut down -deplete_timer: 45 # max ticks before next gather depletes (counts down while chopping) -nest_chance: 256 # 1/256 chance for a bird's nest on each successful gather -respawn_message: "A new oak sapling grows in its place." -respawn_broadcast: "An {name} grows back." -``` - -Regular tree — always depletes on first gather, no shared timer, no nests: -```yaml -id: chop_tree -type: gather -skill: woodcutting -level: 1 -xp: 25 -base_wait: 4 -tool: axe -success: - base: 0.50 - per_level: 0.01 - cap: 0.95 -gather_message: "You swing your axe at the tree..." -fail_message: "You swing but get no logs." -drops: - - item_id: logs - weight: 100 - depletes: true # regular tree depletes on first successful gather - message: "You get some logs." -respawn_timer: 80 -respawn_message: "A new tree grows in its place." -respawn_broadcast: "A {name} grows back." -``` - -#### Shared depletion explained - -When `deplete_timer > 0`, the tree has a shared despawn timer: -- The timer starts at `deplete_timer` max when the first player begins chopping. -- Each tick, if anyone is chopping, the timer counts down. -- When the timer reaches 0, the NEXT successful gather depletes the tree. -- If no one is chopping and the tree isn't depleted, the timer ticks back UP. -- All players chopping the same tree are interrupted when it depletes. - -Use `deplete_timer` for trees. Use `depletes: true` on individual drops for rocks. - -#### Bird's nests - -When `nest_chance > 0`, each successful gather has a 1/N independent chance to also drop -a bird's nest. The nest goes to inventory (or to the ground if inventory is full). -Use the `search` command to open nests — they roll on the `birds_nest_drop` table. - -#### XP drops - -When `xp > 0`, the gather awards XP on each successful drop. If the player's `xpdrops` -toggle is on, the output includes the XP gain: `(+37xp wct)`. - -### Talk (dialog trees) - -Full conversation with conditions, actions, and player flag tracking: -```yaml -id: guard_talk -type: talk -nodes: - start: - message: "\"Halt! This area is restricted.\"" - options: - - text: "\"What's behind that gate?\"" - goto: about_gate - - text: "\"I have copper ore.\"" # only shows if player has ore - goto: trade_ore - condition: - has_item: copper_ore - - text: "\"I have a pass.\"" # only shows if player earned a pass - goto: has_pass - condition: - player_flag: got_pass - value: true - - text: "\"Goodbye.\"" - end: true - - about_gate: - message: "\"Bring me some copper ore and I'll stamp you a pass.\"" - action: - set_player_flags: # player-local: only this player - talked_to_guard: true - options: - - text: "\"I'll be back.\"" - end: true - - text: "\"I have some right here.\"" - goto: trade_ore - condition: - has_item: copper_ore - - trade_ore: - message: "\"Good quality ore.\" He stamps a pass and hands it to you." - action: - take_item: copper_ore # removes 1 copper ore - give_item: pass_stub # gives pass stub - set_player_flags: - got_pass: true # player now "has a pass" - options: - - text: "\"Thanks.\"" - end: true - - has_pass: - message: "\"Alright, I'll open the gate for you.\"" - action: - set_flags: # WORLD flag: gate opens for everyone - gate_open: true - options: - - text: "\"Thanks.\"" - end: true -``` - -#### Node action reference - -| Field | Effect | -|---|---| -| `set_flags` | Sets world flags (global, shared by all players) | -| `set_player_flags` | Sets player-local flags (per-character, quest progress) | -| `give_item` | Gives an item to the player's inventory | -| `take_item` | Removes an item from the player's inventory | -| `teleport` | Moves the player to a room ID | -| `heal` | Restores that many hitpoints | - -All fields in a single action are processed together — you can give an item, take an item, set flags, and heal all in one node. - -Example — quest completion: -```yaml -action: - take_item: dragon_head - give_item: dragon_slayer_medal - set_player_flags: - dragon_quest: complete - dragon_slain: true - heal: 99 - teleport: 1 # return to town -``` - -### Toggle (levers, switches, gates) - -Simple toggle that sets a world flag: -```yaml -id: iron_gate_toggle -type: toggle -message: "You push the heavy iron gate open." -set_flags: - gate_open: true -check: # only works when gate is closed - flag: gate_open - value: true - not: true -``` - -Lever that toggles between two states: -```yaml -id: bridge_lever -type: toggle -message: "You pull the lever. Mechanisms groan somewhere in the distance." -set_flags: - bridge_extended: true -check: - flag: bridge_extended - value: true - not: true -``` - -### Use (crafting stations) - -```yaml -id: smelt_copper -type: use -message: "You place the ore in the furnace..." -wait: 4 # ticks between crafts -consume: # items consumed per craft - copper_ore: 1 -reward: # item produced - item_id: copper_bar - quantity: 1 -fail_message: "The ore crumbles to dust." -success: - base: 0.60 - per_level: 0.01 - cap: 0.95 -skill: smithing -level: 1 -xp: 15 # XP awarded per successful craft -``` - ---- - -## Conditions Reference - -Conditions are used in talk options, exit gates, on-enter scripts, and toggle checks. - -### Simple conditions - -```yaml -# Check a world flag -condition: - flag: gate_open - value: true - -# Check a world flag is NOT set -condition: - flag: gate_open - not: true - -# Check a player flag -condition: - player_flag: finished_tutorial - value: true - -# Check if player has an item -condition: - has_item: bronze_key - -# Check if player does NOT have an item -condition: - has_item: bronze_key - not: true -``` - -### Compound conditions - -All must pass: -```yaml -condition: - all_of: - - flag: gate_open - value: true - - has_item: pass_stub -``` - -Any one must pass: -```yaml -condition: - any_of: - - has_item: bronze_key - - has_item: iron_key - - player_flag: master_of_unlocking - value: true -``` - -Nested compounds: -```yaml -condition: - all_of: - - player_flag: quest_started - value: true - - any_of: - - has_item: wolf_pelt - - has_item: bear_pelt -``` - ---- - -## Drop Tables - -Shared drop tables can be referenced by multiple behaviors: -```yaml -# data/drops/gem_table.yaml -id: gem_table -drops: - - item_id: uncut_sapphire - weight: 47 - - item_id: uncut_emerald - weight: 16 - - item_id: uncut_ruby - weight: 4 - - item_id: uncut_diamond - weight: 1 -``` - -Bird's nest drop table: -```yaml -# data/drops/birds_nest_drop.yaml -id: birds_nest_drop -drops: - - item_id: credits - weight: 50 - quantity: 200 - - item_id: credits - weight: 30 - quantity: 500 - - item_id: credits - weight: 15 - quantity: 1000 - - item_id: credits - weight: 4 - quantity: 3000 - - item_id: credits - weight: 1 - quantity: 10000 -``` - -Referenced from a gather behavior: -```yaml -drops: - - item_id: copper_ore - weight: 90 - depletes: true - - table: gem_table # pulls from data/drops/gem_table.yaml - weight: 10 - depletes: false -``` - ---- - -## Global vs Player State: Door Examples - -### Example A: Door with a button in another room (GLOBAL) - -A button in room 3 opens a door in room 7. Anyone can press it. Once pressed, the door is open for everyone. - -**Button object** (`data/objects/door_button.yaml`): -```yaml -id: door_button -name: stone button -behavior: button_toggle -hidden: true -``` - -**Button behavior** (`data/behaviors/button_toggle.yaml`): -```yaml -id: button_toggle -type: toggle -message: "You press the stone button. You hear grinding stone in the distance." -set_flags: - secret_door_open: true # WORLD flag -check: - flag: secret_door_open - not: true # only works when door is closed -``` - -**Room 3** — contains the button: -```yaml -id: 3 -name: "Button Chamber" -description: "A small stone chamber. A button protrudes from the east wall." -exits: - south: 1 -objects: - - id: door_button -``` - -**Room 7** — contains the door: -```yaml -id: 7 -name: "Hidden Passage" -description: "A dusty corridor. A heavy stone door blocks the way north." -exits: - south: 2 - north: - room: 8 - condition: - flag: secret_door_open # checks WORLD flag - value: true - blocked_message: "A heavy stone door blocks the way." -objects: - - id: stone_door - hidden: true -``` - -### Example B: Key-locked door (PLAYER-LOCAL) - -A locked door that only opens for a player carrying the key. Each player must find their own key. - -**Key item** (`data/items/rusty_key.yaml`): -```yaml -id: rusty_key -name: rusty key -description: "An old iron key, still functional." -value: 0 -stackable: false -``` - -**Room 5** — locked door: -```yaml -id: 5 -name: "Locked Storage" -description: "A small storage room. The way east is blocked by a locked iron door." -exits: - west: 2 - east: - room: 6 - condition: - has_item: rusty_key # checks PLAYER inventory - blocked_message: "The iron door is locked. You need a key." -objects: - - id: iron_door - hidden: true -``` - -**Room 6** — the other side (no key needed to exit): -```yaml -id: 6 -name: "Storage Closet" -description: "Shelves of dusty crates." -exits: - west: 5 # exit back — no condition -spawns: - - item_id: uncut_ruby - quantity: 1 - respawn_ticks: 500 -``` - -Key difference: the button door uses `flag` (shared state — one player presses, everyone benefits), the key door uses `has_item` (per-player inventory check — each player needs their own key). - ---- - -## Complete Quest Example: "Clear the Rats" - -### 1. Quest giver mob (`data/mobs/quest_giver.yaml`) -```yaml -id: "quest_giver" -name: "Elder" -behavior: rat_quest_talk -unique: true -protected: true -attack: 1 -strength: 1 -defense: 1 -hp: 20 -speed: 5 -aggressive: false -idle_descriptions: - - "mutters about the rat infestation" -``` - -### 2. Dialog behavior (`data/behaviors/rat_quest_talk.yaml`) -```yaml -id: rat_quest_talk -type: talk -nodes: - start: - message: "\"Rats! Rats everywhere in the cellar. Clear them out and I'll reward you.\"" - options: - - text: "\"I'll handle it.\"" - goto: accept_quest - condition: - player_flag: rat_quest - not: true - - text: "\"I killed the rats.\"" - goto: turn_in - condition: - player_flag: rat_quest - value: started - - text: "\"Goodbye.\"" - end: true - - accept_quest: - message: "\"Good lad. The cellar is west of here. Come back when they're dead.\"" - action: - set_player_flags: - rat_quest: started - options: - - text: "\"On my way.\"" - end: true - - turn_in: - message: "\"You did it! The village owes you a debt. Here — take this.\"" - action: - give_item: rusty_sword - set_player_flags: - rat_quest: complete - heal: 10 - options: - - text: "\"Thanks!\"" - end: true -``` - -### 3. Rat mobs (`data/mobs/rat.yaml`) -```yaml -id: "rat" -name: "giant rat" -attack: 2 -strength: 1 -defense: 1 -hp: 3 -speed: 4 -aggressive: true -respawn_ticks: 60 -drops: - remains: "rat bones" -``` - -### 4. Cellar room (`data/rooms/20.yaml`) -```yaml -id: 20 -name: "Cellar" -description: "A damp, dark cellar. The floor scuttles with movement." -exits: - east: 1 -mobs: - - "rat" - - "rat" - - "rat" - - "rat" - - "rat" -``` - -### 5. Room 1 with quest giver (`data/rooms/1.yaml`) -```yaml -id: 1 -name: "Town Square" -description: "Cobblestone paths lead in all directions. The Elder stands near the fountain." -exits: - west: 20 - north: 2 -mobs: - - "quest_giver" -objects: - - id: lumby_fountain -``` - ---- - -## Wandering Objects - -Fishing spots that move between rooms: -```yaml -# data/objects/fishing_spot.yaml -id: fishing_spot -name: fishing spot -behavior: fish_trout -``` - -```yaml -# In a room: -objects: - - id: fishing_spot - wander_rooms: [7, 8, 9] - wander_interval: 12 -``` - -Objects teleport between rooms in their `wander_rooms` list. Players gathering from a -wandering object are silently interrupted when it moves. - ---- - -## Wandering Mobs - -Mob wandering is configured per-instance in the room YAML, not on the mob definition: -```yaml -# In a room: -mobs: - - id: man - wander_interval: 10 # attempt to wander every 10 ticks - - id: man - wander_interval: 15 - wander_rooms: [5, 6, 7] # optional — restrict which rooms via exits -``` - -Mobs wander through legal (unconditioned) room exits. When the wander interval expires, -the mob picks a random exit with no conditions and moves through it. If `wander_rooms` -is set, only exits leading to those room IDs are legal. Mobs without `wander_interval` -never wander. Mobs stop wandering while in combat. Dead mobs respawn at their home room. - ---- - -## Player Toggles - -Players can toggle personal settings with the `toggle` command: - -| Toggle | Effect | -|---|---| -| `description` | Show full room description when moving | -| `tiny_map` | Mini-map display: `off`/`right`/`left` | -| `xp_drops` | Show XP gained in gather/craft/combat messages | -| `exits` | Show exit destinations inline in look output | -| `mob_enter` | Notify when a mob enters the room | -| `mob_leave` | Notify when a mob leaves the room | -| `mob_spawn` | Notify when a mob spawns in the area | -| `reserve` | Show full reserved item details in look | -| `depletion` | Show depletion and despawn timers on objects | - ---- - -## The `search` Command - -Used to open searchable items in your inventory (bird's nests, etc.): -``` -Usage: search - -Example: search nest - search birds nest -``` - -Searches your inventory for the named item. If found, removes it and rolls on a drop table. -Currently only bird's nests are searchable. - ---- - -## Hidden Objects - -Objects with `hidden: true` don't appear in the room's object listing. Players discover them by reading room descriptions or trying commands. The object is still fully interactable — `push gate`, `look gate`, etc. - -```yaml -# data/objects/secret_lever.yaml -id: secret_lever -name: stone lever -behavior: secret_toggle -hidden: true -props: - description: "A cleverly concealed lever behind a loose stone." -``` - -Room description hints at it: -```yaml -description: "A dusty corridor. One of the wall stones looks slightly out of place." -``` - ---- - -## Tips - -1. **Use player flags for quest progress, world flags for environmental state.** If a bridge is repaired, that's world state. If a player has read a sign, that's player state. - -2. **Conditions on enter scripts** make rooms feel alive. A guard who only barks the first time, a room that changes after a quest completes. - -3. **Hidden objects** keep room descriptions clean. Mention them in the room's description text instead of auto-listing them. - -4. **Drop tables** are shareable. The `gem_table` is used by copper rocks AND mob loot. Define once, reference everywhere. - -5. **Objects are for fixtures, mobs are for living things.** If it has HP and can die, it's a mob. If it's a rock, lever, door, or crafting station, it's an object. Both can have behaviors. - -6. **The `behavior` field on mobs** lets you talk to them directly — `talk guard` finds the guard mob, no duplicate object entry needed. - -7. **Exits accept both `int` and `map` formats.** `north: 2` is shorthand for `north: {room: 2}`. Add `condition` and `blocked_message` only when needed. Mob room entries accept both `"man"` and `{id: man, ...}`. - -8. **Live editing works.** Room, item, mob, object, and behavior YAML files are read from disk on each access. Change a room description or dialog and it takes effect immediately — no restart needed. - -9. **Shared depletion vs per-drop depletion.** Use `deplete_timer` for trees — the timer counts down while being chopped and regens when left alone. Use `depletes: true` on individual drops for rocks — they deplete on first successful gather. - -10. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms. diff --git a/cmd/mud/main.go b/cmd/mud/main.go index 67ba93d..18fd171 100644 --- a/cmd/mud/main.go +++ b/cmd/mud/main.go @@ -8,9 +8,9 @@ import ( "os/signal" "syscall" - "thirdcollapse/internal/config" - "thirdcollapse/internal/game" - "thirdcollapse/internal/net" + "thehouseoficarus/internal/config" + "thehouseoficarus/internal/game" + "thehouseoficarus/internal/net" ) func main() { @@ -64,6 +64,9 @@ func main() { if cfg.Telnet.Enabled { log.Printf("Telnet listening on :%d", cfg.Telnet.Port) } + if cfg.TelnetTLS.Enabled { + log.Printf("Telnet+TLS listening on :%d", cfg.TelnetTLS.Port) + } if cfg.HTTP.Enabled { log.Printf("HTTP listening on :%d", cfg.HTTP.Port) } diff --git a/config.yaml b/config.yaml index 57e1dd3..0bde479 100644 --- a/config.yaml +++ b/config.yaml @@ -2,31 +2,44 @@ game: tick_length: 600 colors: - room_name: "fg=cyan bold" - room_number: "dim" - room_desc: "fg=white" - direction: "fg=cyan" - exit_direction: "fg=cyan" - exit_name: "fg=green" - mob_name: "fg=bright_red" - friendly_npc: "fg=green" - hostile_npc: "fg=bright_red" - damage: "fg=red" - enemy_hp: "fg=red" - character_hp: "fg=green" - xp: "fg=yellow" - level_up: "fg=bright_yellow bold" - item: "fg=green" - player_name: "fg=bright_white" - death: "fg=red bold" - victory: "fg=green" - miss: "dim" - error: "fg=red" + room_name: "81 bold" + room_number: "240" + room_desc: "252" + direction: "75" + exit_direction: "75" + exit_name: "114" + mob_name: "203" + friendly_npc: "120" + hostile_npc: "203" + damage: "196" + enemy_hp: "167" + character_hp: "84" + xp: "222" + level_up: "226 bold" + item: "223" + player_name: "189" + death: "196 bold" + victory: "83" + miss: "243" + error: "209" + say: "230" + dialog: "117" + broadcast: "215" + fire: "208" + eat_food: "156" + drop_message: "186" + credits_pickup: "220" telnet: enabled: true port: 4000 +telnet_tls: + enabled: false + port: 4001 + cert_file: "" + key_file: "" + http: enabled: true port: 8888 diff --git a/data/behaviors/chop_magic.yaml b/data/behaviors/chop_magic.yaml index ae5ff7e..02dbab3 100644 --- a/data/behaviors/chop_magic.yaml +++ b/data/behaviors/chop_magic.yaml @@ -15,7 +15,7 @@ drops: - item_id: magic_logs weight: 100 depletes: false - message: "You get some magic logs." + message: "You get some {135}magic logs{/}." respawn_timer: 199 deplete_timer: 390 nest_chance: 256 diff --git a/data/behaviors/chop_mahogany.yaml b/data/behaviors/chop_mahogany.yaml index 61ab47a..7bacfe3 100644 --- a/data/behaviors/chop_mahogany.yaml +++ b/data/behaviors/chop_mahogany.yaml @@ -15,7 +15,7 @@ drops: - item_id: mahogany_logs weight: 100 depletes: false - message: "You get some mahogany logs." + message: "You get some {131}mahogany logs{/}." respawn_timer: 14 deplete_timer: 100 nest_chance: 256 diff --git a/data/behaviors/chop_maple.yaml b/data/behaviors/chop_maple.yaml index 2c7c21d..44c45cf 100644 --- a/data/behaviors/chop_maple.yaml +++ b/data/behaviors/chop_maple.yaml @@ -15,7 +15,7 @@ drops: - item_id: maple_logs weight: 100 depletes: false - message: "You get some maple logs." + message: "You get some {173}maple logs{/}." respawn_timer: 59 deplete_timer: 100 nest_chance: 256 diff --git a/data/behaviors/chop_oak.yaml b/data/behaviors/chop_oak.yaml index fcd0f2d..85c6be0 100644 --- a/data/behaviors/chop_oak.yaml +++ b/data/behaviors/chop_oak.yaml @@ -15,7 +15,7 @@ drops: - item_id: oak_logs weight: 100 depletes: false - message: "You get some oak logs." + message: "You get some {113}oak logs{/}." respawn_timer: 14 deplete_timer: 45 nest_chance: 256 diff --git a/data/behaviors/chop_redwood.yaml b/data/behaviors/chop_redwood.yaml index a563bd5..2db92ed 100644 --- a/data/behaviors/chop_redwood.yaml +++ b/data/behaviors/chop_redwood.yaml @@ -15,7 +15,7 @@ drops: - item_id: redwood_logs weight: 100 depletes: false - message: "You get some redwood logs." + message: "You get some {131}redwood logs{/}." respawn_timer: 199 deplete_timer: 440 nest_chance: 256 diff --git a/data/behaviors/chop_teak.yaml b/data/behaviors/chop_teak.yaml index d20aebc..ba8834d 100644 --- a/data/behaviors/chop_teak.yaml +++ b/data/behaviors/chop_teak.yaml @@ -15,7 +15,7 @@ drops: - item_id: teak_logs weight: 100 depletes: false - message: "You get some teak logs." + message: "You get some {179}teak logs{/}." respawn_timer: 15 deplete_timer: 50 nest_chance: 256 diff --git a/data/behaviors/chop_tree.yaml b/data/behaviors/chop_tree.yaml index f7c4de2..fb8c3d2 100644 --- a/data/behaviors/chop_tree.yaml +++ b/data/behaviors/chop_tree.yaml @@ -16,7 +16,7 @@ drops: - item_id: logs weight: 100 depletes: true - message: "You get some logs." + message: "You get some {107}logs{/}." respawn_timer: 80 exhausted_message: "The tree comes crashing down!" respawn_message: "A new tree grows in its place." diff --git a/data/behaviors/chop_willow.yaml b/data/behaviors/chop_willow.yaml index 10dc05a..c4b14b3 100644 --- a/data/behaviors/chop_willow.yaml +++ b/data/behaviors/chop_willow.yaml @@ -15,7 +15,7 @@ drops: - item_id: willow_logs weight: 100 depletes: false - message: "You get some willow logs." + message: "You get some {114}willow logs{/}." respawn_timer: 14 deplete_timer: 50 nest_chance: 256 diff --git a/data/behaviors/chop_yew.yaml b/data/behaviors/chop_yew.yaml index ec25b4d..058cb5d 100644 --- a/data/behaviors/chop_yew.yaml +++ b/data/behaviors/chop_yew.yaml @@ -15,7 +15,7 @@ drops: - item_id: yew_logs weight: 100 depletes: false - message: "You get some yew logs." + message: "You get some {108}yew logs{/}." respawn_timer: 99 deplete_timer: 190 nest_chance: 256 diff --git a/data/behaviors/fish_angler.yaml b/data/behaviors/fish_angler.yaml index 107e9b5..95e5873 100644 --- a/data/behaviors/fish_angler.yaml +++ b/data/behaviors/fish_angler.yaml @@ -16,4 +16,4 @@ drops: level: 82 xp: 120 weight: 100 - message: "You catch a raw anglerfish." + message: "You catch a {80}raw anglerfish{/}." diff --git a/data/behaviors/fish_bait.yaml b/data/behaviors/fish_bait.yaml index ee13e95..0d6bc4e 100644 --- a/data/behaviors/fish_bait.yaml +++ b/data/behaviors/fish_bait.yaml @@ -16,14 +16,14 @@ drops: level: 5 xp: 20 weight: 60 - message: "You catch a raw sardine." + message: "You catch a {75}raw sardine{/}." - item_id: raw_herring level: 10 xp: 30 weight: 40 - message: "You catch a raw herring." + message: "You catch a {75}raw herring{/}." - item_id: raw_pike level: 25 xp: 60 weight: 30 - message: "You catch a raw pike." + message: "You catch a {69}raw pike{/}." diff --git a/data/behaviors/fish_cage.yaml b/data/behaviors/fish_cage.yaml index 1fc92e4..a5d0241 100644 --- a/data/behaviors/fish_cage.yaml +++ b/data/behaviors/fish_cage.yaml @@ -15,4 +15,4 @@ drops: level: 40 xp: 90 weight: 100 - message: "You catch a raw lobster." + message: "You catch a {33}raw lobster{/}." diff --git a/data/behaviors/fish_fly.yaml b/data/behaviors/fish_fly.yaml index af80076..1a86b64 100644 --- a/data/behaviors/fish_fly.yaml +++ b/data/behaviors/fish_fly.yaml @@ -16,14 +16,14 @@ drops: level: 20 xp: 50 weight: 50 - message: "You catch a raw trout." + message: "You catch a {69}raw trout{/}." - item_id: raw_salmon level: 30 xp: 70 weight: 50 - message: "You catch a raw salmon." + message: "You catch a {69}raw salmon{/}." - item_id: raw_rainbow_fish level: 38 xp: 80 weight: 30 - message: "You catch a raw rainbow fish." + message: "You catch a {177}raw rainbow fish{/}." diff --git a/data/behaviors/fish_harpoon.yaml b/data/behaviors/fish_harpoon.yaml index 8d94a48..3775573 100644 --- a/data/behaviors/fish_harpoon.yaml +++ b/data/behaviors/fish_harpoon.yaml @@ -15,14 +15,14 @@ drops: level: 35 xp: 80 weight: 60 - message: "You harpoon a raw tuna." + message: "You harpoon a {33}raw tuna{/}." - item_id: raw_swordfish level: 50 xp: 100 weight: 50 - message: "You harpoon a raw swordfish." + message: "You harpoon a {33}raw swordfish{/}." - item_id: raw_shark level: 76 xp: 110 weight: 30 - message: "You harpoon a raw shark." + message: "You harpoon a {27}raw shark{/}." diff --git a/data/behaviors/fish_net.yaml b/data/behaviors/fish_net.yaml index b5398aa..46c0008 100644 --- a/data/behaviors/fish_net.yaml +++ b/data/behaviors/fish_net.yaml @@ -16,24 +16,24 @@ drops: level: 1 xp: 10 weight: 50 - message: "You catch some raw shrimps." + message: "You catch some {75}raw shrimps{/}." - item_id: raw_anchovies level: 1 xp: 10 weight: 50 - message: "You catch some raw anchovies." + message: "You catch some {75}raw anchovies{/}." - item_id: raw_mackerel level: 16 xp: 20 weight: 50 - message: "You catch a raw mackerel." + message: "You catch a {69}raw mackerel{/}." - item_id: raw_cod level: 23 xp: 30 weight: 50 - message: "You catch a raw cod." + message: "You catch a {69}raw cod{/}." - item_id: raw_bass level: 46 xp: 50 weight: 50 - message: "You catch a raw bass." + message: "You catch a {69}raw bass{/}." diff --git a/data/behaviors/fish_sacred_eel.yaml b/data/behaviors/fish_sacred_eel.yaml index 2e0235f..f9712d1 100644 --- a/data/behaviors/fish_sacred_eel.yaml +++ b/data/behaviors/fish_sacred_eel.yaml @@ -16,4 +16,4 @@ drops: level: 87 xp: 125 weight: 100 - message: "You catch a sacred eel." + message: "You catch a {177}sacred eel{/}." diff --git a/data/behaviors/fish_swamp.yaml b/data/behaviors/fish_swamp.yaml index ee1e5f3..019e979 100644 --- a/data/behaviors/fish_swamp.yaml +++ b/data/behaviors/fish_swamp.yaml @@ -16,9 +16,9 @@ drops: level: 28 xp: 80 weight: 60 - message: "You catch a raw slimy eel." + message: "You catch a {108}raw slimy eel{/}." - item_id: raw_cave_eel level: 38 xp: 80 weight: 40 - message: "You catch a raw cave eel." + message: "You catch a {80}raw cave eel{/}." diff --git a/data/behaviors/mine_adamantite.yaml b/data/behaviors/mine_adamantite.yaml index 50b9a60..b5e5d62 100644 --- a/data/behaviors/mine_adamantite.yaml +++ b/data/behaviors/mine_adamantite.yaml @@ -15,7 +15,7 @@ drops: - item_id: adamantite_ore weight: 100 depletes: true - message: "You manage to mine some adamantite ore." + message: "You manage to mine some {120}adamantite ore{/}." respawn_timer: 140 respawn_message: "You see more ore in the rock." respawn_broadcast: "A green glint of adamantite catches your eye from some {name}." diff --git a/data/behaviors/mine_clay.yaml b/data/behaviors/mine_clay.yaml index 73c7e19..b4058b1 100644 --- a/data/behaviors/mine_clay.yaml +++ b/data/behaviors/mine_clay.yaml @@ -15,7 +15,7 @@ drops: - item_id: clay weight: 100 depletes: true - message: "You manage to get some clay." + message: "You manage to get some {173}clay{/}." respawn_timer: 50 respawn_message: "The clay deposit is ready to dig again." respawn_broadcast: "A deposit of clay softens." diff --git a/data/behaviors/mine_coal.yaml b/data/behaviors/mine_coal.yaml index ab1d6a2..334e9ec 100644 --- a/data/behaviors/mine_coal.yaml +++ b/data/behaviors/mine_coal.yaml @@ -15,7 +15,7 @@ drops: - item_id: coal weight: 100 depletes: true - message: "You manage to mine some coal." + message: "You manage to mine some {240}coal{/}." respawn_timer: 80 respawn_message: "You see more coal in the rock." respawn_broadcast: "A dark vein of coal is visible in some {name}." diff --git a/data/behaviors/mine_copper.yaml b/data/behaviors/mine_copper.yaml index 82755e3..06ee9d2 100644 --- a/data/behaviors/mine_copper.yaml +++ b/data/behaviors/mine_copper.yaml @@ -16,7 +16,7 @@ drops: - item_id: copper_ore weight: 90 depletes: true - message: "You manage to mine some copper ore." + message: "You manage to mine some {178}copper ore{/}." - table: gem_table weight: 10 depletes: false diff --git a/data/behaviors/mine_gold.yaml b/data/behaviors/mine_gold.yaml index 01a8b33..b61f6bf 100644 --- a/data/behaviors/mine_gold.yaml +++ b/data/behaviors/mine_gold.yaml @@ -15,7 +15,7 @@ drops: - item_id: gold_ore weight: 100 depletes: true - message: "You manage to mine some gold ore." + message: "You manage to mine some {220}gold ore{/}." respawn_timer: 90 respawn_message: "You see more ore in the rock." respawn_broadcast: "A golden glint catches your eye from some {name}." diff --git a/data/behaviors/mine_iron.yaml b/data/behaviors/mine_iron.yaml index b7ec4af..e1a8181 100644 --- a/data/behaviors/mine_iron.yaml +++ b/data/behaviors/mine_iron.yaml @@ -15,7 +15,7 @@ drops: - item_id: iron_ore weight: 100 depletes: true - message: "You manage to mine some iron ore." + message: "You manage to mine some {250}iron ore{/}." respawn_timer: 60 respawn_message: "You see more ore in the rock." respawn_broadcast: "A glint of iron catches your eye from some {name}." diff --git a/data/behaviors/mine_mithril.yaml b/data/behaviors/mine_mithril.yaml index 26c5f5f..407f1ff 100644 --- a/data/behaviors/mine_mithril.yaml +++ b/data/behaviors/mine_mithril.yaml @@ -15,7 +15,7 @@ drops: - item_id: mithril_ore weight: 100 depletes: true - message: "You manage to mine some mithril ore." + message: "You manage to mine some {75}mithril ore{/}." respawn_timer: 110 respawn_message: "You see more ore in the rock." respawn_broadcast: "A blue glint of mithril catches your eye from some {name}." diff --git a/data/behaviors/mine_runite.yaml b/data/behaviors/mine_runite.yaml index 5bd65c7..dddd410 100644 --- a/data/behaviors/mine_runite.yaml +++ b/data/behaviors/mine_runite.yaml @@ -15,7 +15,7 @@ drops: - item_id: runite_ore weight: 100 depletes: true - message: "You manage to mine some runite ore." + message: "You manage to mine some {87}runite ore{/}." respawn_timer: 180 respawn_message: "You see more ore in the rock." respawn_broadcast: "A pale blue glint of runite catches your eye from some {name}." diff --git a/data/behaviors/mine_scrap.yaml b/data/behaviors/mine_scrap.yaml index ec889a6..ae07b36 100644 --- a/data/behaviors/mine_scrap.yaml +++ b/data/behaviors/mine_scrap.yaml @@ -15,7 +15,7 @@ drops: - item_id: scrap_metal weight: 100 depletes: true - message: "You salvage some scrap metal." + message: "You salvage some {243}scrap metal{/}." respawn_timer: 50 respawn_message: "More scrap is revealed in the pile." respawn_broadcast: "The scrap pile looks worth mining again." diff --git a/data/behaviors/mine_silver.yaml b/data/behaviors/mine_silver.yaml index 8b21b98..1257f87 100644 --- a/data/behaviors/mine_silver.yaml +++ b/data/behaviors/mine_silver.yaml @@ -15,7 +15,7 @@ drops: - item_id: silver_ore weight: 100 depletes: true - message: "You manage to mine some silver ore." + message: "You manage to mine some {253}silver ore{/}." respawn_timer: 70 respawn_message: "You see more ore in the rock." respawn_broadcast: "A silvery glint catches your eye from some {name}." diff --git a/data/behaviors/mine_tin.yaml b/data/behaviors/mine_tin.yaml index 107c549..7a1a12a 100644 --- a/data/behaviors/mine_tin.yaml +++ b/data/behaviors/mine_tin.yaml @@ -15,7 +15,7 @@ drops: - item_id: tin_ore weight: 100 depletes: true - message: "You manage to mine some tin ore." + message: "You manage to mine some {253}tin ore{/}." respawn_timer: 50 respawn_message: "You see more ore in the rock." respawn_broadcast: "A glint of tin catches your eye from some {name}." diff --git a/data/help/color.yaml b/data/help/color.yaml index 777aa07..5927e9a 100644 --- a/data/help/color.yaml +++ b/data/help/color.yaml @@ -10,65 +10,83 @@ description: | Usage: color List all targets and their current values color Show current color for a target - color Set a custom color + color Set a custom color color reset Reset to server default color off Disable color for this target entirely - Color string format (space-separated tokens): - fg= Foreground color - bg= Background color + Color spec format (space-separated tokens): + <0-255> Foreground color (xterm-256 palette index) + bg:<0-255> Background color + g:, Gradient between two or more colors bold Bold text dim Dim text - italic Italic text underline Underlined text - Example: fg=green bg=black bold + Examples: + color room_name 6 bold + color fire 208 + color death 1 bold + color broadcast 214 bg:0 - Available color names: - black, red, green, yellow, blue, magenta, cyan, white, - bright_black, bright_red, bright_green, bright_yellow, - bright_blue, bright_magenta, bright_cyan, bright_white + Gradients interpolate across characters in RGB space: + color item g:196,82 Red to green + color room_name g:45,39,59 Three-stop gradient + + Use 'colortable' to see all 256 colors and their indices. + + Colors use the xterm-256 palette (0-255). When a player's color + mode is set to "ansi", extended colors (16-255) automatically + downgrade to the nearest of the 16 standard ANSI colors. + + Standard colors (0-15): + 0 black 1 red 2 green 3 yellow + 4 blue 5 magenta 6 cyan 7 white + 8-15 bright variants of the above + + Inline color tags in room descriptions: + {}colored text{/} + {182 bold}mysterious vase{/} + {g:196,82}gradient text{/} Color targets: room_name Room name headers room_number Room number display (#id) - room_desc Room description text - direction Movement direction text ("You walk north") - exit_direction Exit direction labels (north, south, etc.) + room_desc Room description text (default for untagged text) + direction Movement direction text + exit_direction Exit direction labels exit_name Exit destination room names mob_name Mob names in combat - friendly_npc Friendly NPC names (cannot be attacked) - hostile_npc Hostile NPC/mob names in room listings + friendly_npc Friendly NPC names + hostile_npc Hostile mob names in room listings damage Damage numbers - enemy_hp Enemy HP in combat messages - character_hp Your HP in combat messages + enemy_hp Enemy HP in combat + character_hp Your HP in combat xp XP gain messages level_up Level-up announcements - item Item names on pickup/drop/gather + item Item names (default) player_name Player names in broadcasts death Death message victory Combat victory message - miss Miss messages in combat + miss Missed attack messages error Error messages + say Player chat text + dialog NPC conversation text + broadcast Room-wide event messages + fire Firemaking messages + eat_food Eating/healing messages + drop_message Mob loot drops + credits_pickup Currency/credit messages Resolution order: 1. Account override (set via "color" command) 2. Server default (set in config.yaml) 3. Built-in fallback - Auto-colored elements (not configurable): - - Objects: fishing spots are blue, rocks are earth-toned, trees green - - Items: ores, logs, fish, gems, and tools have their own colors - - Mob/player levels: displayed relative to your combat level - (red if higher, orange if <5 higher, white if equal, - yellow if <5 lower, green if much lower) - The "color" option (option color none/ansi/xterm256) controls - whether ANSI codes are emitted and which mode is used. - "none" strips all color; the "color" command settings are - still saved but have no visual effect until color mode is - enabled. + whether ANSI codes are emitted. "none" strips all color. + In "ansi" mode, extended palette colors (16-255) are + automatically downgraded to the nearest ANSI color. - You can use color blocks in your prompt using the same format: - option prompt {fg=green bold}Ready>{/} %h/%Hhp + You can use color blocks in your prompt: + prompt {2 bold}Ready>{/} %h/%Hhp See 'help prompt' for full prompt customization. diff --git a/data/help/option.yaml b/data/help/option.yaml index 1948b7a..9fdcf7f 100644 --- a/data/help/option.yaml +++ b/data/help/option.yaml @@ -1,7 +1,7 @@ name: "option" category: "General" description: | - View or change character settings. + View or change account settings. Options apply to all characters. Usage: option - List all options and their current values options - Same as option @@ -18,3 +18,4 @@ description: | option mapwidth 50 Use help ,[,...]}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= bg= 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 ' 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 @@ -Third Collapse +The House of Icarus