diff options
| author | historia <[not public]> | 2026-06-22 04:24:11 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-22 04:24:11 -0400 |
| commit | 5b9a75a1520a198c9c6b1f1f55e05c9f4aab5629 (patch) | |
| tree | 18f500ca3492ba206a3f66e2f01146b05a1675b2 | |
| parent | 7f07c7236578e1a7bcef0282ff9d7f5bf8c7c045 (diff) | |
| download | thehouseoficarus-5b9a75a1520a198c9c6b1f1f55e05c9f4aab5629.tar.gz | |
feat: map now only reveals with exploration. players can set custom map symbols.
105 files changed, 769 insertions, 497 deletions
diff --git a/data/help/safespot.yaml b/data/help/safespot.yaml index b18142c..7a41e97 100644 --- a/data/help/safespot.yaml +++ b/data/help/safespot.yaml @@ -31,8 +31,8 @@ description: | "quest_animal_magnetism" or achievement flags like "achieve_medium_combat". - You can customize the alert shown when you are forced out of a - safespot with: option safespot_alert <message> - Set to "none" to disable. Uses inline color tags. + You can customize the alert shown when you are forced out of a + safespot with: option safespot_alert <message> + Uses inline color tags. See also: help hide, help combat, help option diff --git a/data/help/symbol.yaml b/data/help/symbol.yaml new file mode 100644 index 0000000..56c91d1 --- /dev/null +++ b/data/help/symbol.yaml @@ -0,0 +1,21 @@ +name: "symbol" +category: "Exploration" +description: | + Set a custom map symbol for the current room. + + Usage: symbol <char> [color...] + symbol clear + symbol + + Examples: + symbol M Set current room symbol to M + symbol M 232 Set symbol to M with foreground color 232 + symbol M 232 bold Set symbol to M with color 232 and bold + symbol clear Remove custom symbol for current room + symbol Show current room's custom symbol + + The default map symbol for all rooms is ■. You can override + it per-room with any single character and an optional color spec. + Colors use standard xterm-256 numbering (0-255) and support + bold, dim, and underline modifiers. Check the command + "colortable" for all colors. diff --git a/data/mobs/ancient_construct.yaml b/data/mobs/ancient_construct.yaml index 9e4e765..530146a 100644 --- a/data/mobs/ancient_construct.yaml +++ b/data/mobs/ancient_construct.yaml @@ -26,6 +26,7 @@ combat_descriptions: - "slices at %s with a blade of pure light" - "unleashes a cascade of ancient energy at %s" - "teleports behind %s and strikes" +size: massive drops: remains: scrap loot: diff --git a/data/mobs/bandit.yaml b/data/mobs/bandit.yaml index e3cc0ae..7efb481 100644 --- a/data/mobs/bandit.yaml +++ b/data/mobs/bandit.yaml @@ -25,6 +25,7 @@ combat_descriptions: - "slashes at %s with a serrated blade" - "delivers a crushing punch with a cybernetic fist" - "lunges at %s with a battle cry" +size: medium drops: remains: bones loot: diff --git a/data/mobs/bioengineer.yaml b/data/mobs/bioengineer.yaml index a6f2823..5c67643 100644 --- a/data/mobs/bioengineer.yaml +++ b/data/mobs/bioengineer.yaml @@ -27,6 +27,7 @@ steal_table: bioengineer_steal steal_level: 38 steal_xp: 45 steal_speed: 4 +size: small drops: remains: "bones" loot: diff --git a/data/mobs/client.yaml b/data/mobs/client.yaml index 5e03cde..8e8ba9b 100644 --- a/data/mobs/client.yaml +++ b/data/mobs/client.yaml @@ -217,3 +217,4 @@ talk: - goto: start text: '"Back."' unique: true +size: small diff --git a/data/mobs/corrupted_scientist.yaml b/data/mobs/corrupted_scientist.yaml index 962dd3a..cb7461e 100644 --- a/data/mobs/corrupted_scientist.yaml +++ b/data/mobs/corrupted_scientist.yaml @@ -27,6 +27,7 @@ combat_descriptions: - "blasts %s with a burst of unstable energy" - "unleashes a wave of chaotic force at %s" - "channels raw power through their hands at %s" +size: medium drops: remains: bones loot: diff --git a/data/mobs/cow.yaml b/data/mobs/cow.yaml index d42015b..bf97360 100644 --- a/data/mobs/cow.yaml +++ b/data/mobs/cow.yaml @@ -19,6 +19,7 @@ idle_descriptions: - "chews on some grass" - "swishes its tail lazily" - "stares at you with big brown eyes" +size: large drops: remains: bones loot: diff --git a/data/mobs/crawler.yaml b/data/mobs/crawler.yaml index ace034b..6253836 100644 --- a/data/mobs/crawler.yaml +++ b/data/mobs/crawler.yaml @@ -19,6 +19,7 @@ combat_descriptions: - "snaps its mandibles at %s" - "lashes out with a barbed tail at %s" - "charges headlong into %s" +size: large drops: remains: chitin_plate loot: diff --git a/data/mobs/drone.yaml b/data/mobs/drone.yaml index 4ceafc0..7737294 100644 --- a/data/mobs/drone.yaml +++ b/data/mobs/drone.yaml @@ -19,6 +19,7 @@ combat_descriptions: - "fires an electrical bolt at %s" - "charges its capacitors and zaps %s" - "swoops down on %s with crackling energy" +size: small drops: remains: circuit_board loot: diff --git a/data/mobs/elite_synth.yaml b/data/mobs/elite_synth.yaml index 9a4a3e8..e858b03 100644 --- a/data/mobs/elite_synth.yaml +++ b/data/mobs/elite_synth.yaml @@ -27,6 +27,7 @@ combat_descriptions: - "unleashes a coherent energy beam at %s" - "fires a pulse of disruptor energy at %s" - "overloads its emitters and blasts %s" +size: medium drops: remains: circuit_board loot: diff --git a/data/mobs/enforcer_bot.yaml b/data/mobs/enforcer_bot.yaml index 62a5992..28508f7 100644 --- a/data/mobs/enforcer_bot.yaml +++ b/data/mobs/enforcer_bot.yaml @@ -26,6 +26,7 @@ combat_descriptions: - "smashes %s with a shock baton" - "slams its riot shield into %s" - "delivers a stunning blow to %s" +size: large drops: remains: scrap loot: diff --git a/data/mobs/estate_broker.yaml b/data/mobs/estate_broker.yaml index a8b3ed7..451ffe8 100644 --- a/data/mobs/estate_broker.yaml +++ b/data/mobs/estate_broker.yaml @@ -52,3 +52,4 @@ talk: - next: info text: What do I get with a house? unique: true +size: small diff --git a/data/mobs/farmer.yaml b/data/mobs/farmer.yaml index 7b12b9f..65fb430 100644 --- a/data/mobs/farmer.yaml +++ b/data/mobs/farmer.yaml @@ -27,6 +27,7 @@ steal_table: farmer_steal steal_level: 10 steal_xp: 15 steal_speed: 4 +size: small drops: remains: "bones" loot: diff --git a/data/mobs/feral_cyborg.yaml b/data/mobs/feral_cyborg.yaml index 7b3fc19..1b01d1b 100644 --- a/data/mobs/feral_cyborg.yaml +++ b/data/mobs/feral_cyborg.yaml @@ -25,6 +25,7 @@ combat_descriptions: - "slashes %s with forearm blades" - "delivers a powered kick to %s" - "grapples %s with augmented strength" +size: medium drops: remains: bones loot: diff --git a/data/mobs/general_store_clerk.yaml b/data/mobs/general_store_clerk.yaml index cc6d014..518ffba 100644 --- a/data/mobs/general_store_clerk.yaml +++ b/data/mobs/general_store_clerk.yaml @@ -69,3 +69,4 @@ talk: - end: true text: '"Goodbye."' unique: true +size: small diff --git a/data/mobs/ghoul.yaml b/data/mobs/ghoul.yaml index e4888d9..cd50a7e 100644 --- a/data/mobs/ghoul.yaml +++ b/data/mobs/ghoul.yaml @@ -26,6 +26,7 @@ combat_descriptions: - "claws at %s with irradiated hands" - "lunges at %s with a gurgling shriek" - "bites %s with decaying teeth" +size: medium drops: remains: bones loot: diff --git a/data/mobs/goblin.yaml b/data/mobs/goblin.yaml index af03fc7..0d09654 100644 --- a/data/mobs/goblin.yaml +++ b/data/mobs/goblin.yaml @@ -21,6 +21,7 @@ idle_descriptions: - "waves a rusty blade around" - "picks its nose" - "kicks a pebble" +size: small drops: remains: bones loot: diff --git a/data/mobs/greater_drone.yaml b/data/mobs/greater_drone.yaml index 7f786f1..d11827f 100644 --- a/data/mobs/greater_drone.yaml +++ b/data/mobs/greater_drone.yaml @@ -23,6 +23,7 @@ idle_descriptions: - "recalibrates its targeting array" - "emits a warning klaxon" - "deploys sensor probes" +size: medium drops: loot: - item_id: scrap diff --git a/data/mobs/guard.yaml b/data/mobs/guard.yaml index 04aacf5..b834d9b 100644 --- a/data/mobs/guard.yaml +++ b/data/mobs/guard.yaml @@ -74,3 +74,4 @@ talk: - end: true text: '"Thanks."' unique: true +size: medium diff --git a/data/mobs/heavy_mech.yaml b/data/mobs/heavy_mech.yaml index 129bc88..7cc1028 100644 --- a/data/mobs/heavy_mech.yaml +++ b/data/mobs/heavy_mech.yaml @@ -26,6 +26,7 @@ combat_descriptions: - "fires its rotary cannon at %s" - "smashes %s with a hydraulic fist" - "unleashes a missile salvo at %s" +size: massive drops: remains: scrap loot: diff --git a/data/mobs/iron_sentinel.yaml b/data/mobs/iron_sentinel.yaml index 0256b93..5026e17 100644 --- a/data/mobs/iron_sentinel.yaml +++ b/data/mobs/iron_sentinel.yaml @@ -21,6 +21,7 @@ idle_descriptions: - "turns its armored head with a mechanical grind" - "vents steam from exhaust ports" - "scans the area with a red optical sensor" +size: large drops: loot: - item_id: scrap diff --git a/data/mobs/lesser_drone.yaml b/data/mobs/lesser_drone.yaml index b90976f..e49e946 100644 --- a/data/mobs/lesser_drone.yaml +++ b/data/mobs/lesser_drone.yaml @@ -23,6 +23,7 @@ idle_descriptions: - "emits a low electronic hum" - "projects a thin beam of light across the floor" - "rotates slowly in place" +size: small drops: loot: - item_id: scrap diff --git a/data/mobs/man.yaml b/data/mobs/man.yaml index c65f85c..e710349 100644 --- a/data/mobs/man.yaml +++ b/data/mobs/man.yaml @@ -31,6 +31,7 @@ slash_defense: 0 crush_defense: 0 science_defense: 0 ranged_defense: 0 +size: small drops: remains: "bones" loot: diff --git a/data/mobs/molten_elemental.yaml b/data/mobs/molten_elemental.yaml index 7032965..3c02979 100644 --- a/data/mobs/molten_elemental.yaml +++ b/data/mobs/molten_elemental.yaml @@ -26,6 +26,7 @@ combat_descriptions: - "hurls a glob of molten metal at %s" - "engulfs %s in a wave of liquid fire" - "smashes %s with a superheated limb" +size: large drops: remains: scrap loot: diff --git a/data/mobs/moss_giant.yaml b/data/mobs/moss_giant.yaml index 7070b38..0499572 100644 --- a/data/mobs/moss_giant.yaml +++ b/data/mobs/moss_giant.yaml @@ -21,6 +21,7 @@ idle_descriptions: - "pulls a vine from its shoulder" - "stamps the ground, sending tremors" - "scratches its mossy hide" +size: massive drops: remains: bones loot: diff --git a/data/mobs/mutant_rat.yaml b/data/mobs/mutant_rat.yaml index 7dfd751..38c5624 100644 --- a/data/mobs/mutant_rat.yaml +++ b/data/mobs/mutant_rat.yaml @@ -26,6 +26,7 @@ combat_descriptions: - "bites %s with glowing teeth" - "slashes %s with mutated claws" - "lunges at %s with unnatural speed" +size: small drops: remains: bones loot: diff --git a/data/mobs/newbie_trainer.yaml b/data/mobs/newbie_trainer.yaml index 100bfd6..6da23a4 100644 --- a/data/mobs/newbie_trainer.yaml +++ b/data/mobs/newbie_trainer.yaml @@ -23,3 +23,4 @@ idle_descriptions: - "offers a friendly nod to passersby" - "polishes a wooden shield" - "checks a stopwatch and jots something down" +size: small diff --git a/data/mobs/phantom.yaml b/data/mobs/phantom.yaml index 3fb1893..d9953fa 100644 --- a/data/mobs/phantom.yaml +++ b/data/mobs/phantom.yaml @@ -19,6 +19,7 @@ combat_descriptions: - "blasts %s with a psychic wave" - "phases through %s's defenses" - "unleashes a spectral shriek at %s" +size: medium drops: remains: ectoplasm loot: diff --git a/data/mobs/plasma_wraith.yaml b/data/mobs/plasma_wraith.yaml index 9600ddc..adfc83a 100644 --- a/data/mobs/plasma_wraith.yaml +++ b/data/mobs/plasma_wraith.yaml @@ -27,6 +27,7 @@ combat_descriptions: - "lashes %s with a whip of plasma" - "engulfs %s in a superheated cloud" - "discharges a concentrated beam at %s" +size: medium drops: remains: ectoplasm loot: diff --git a/data/mobs/psionic_entity.yaml b/data/mobs/psionic_entity.yaml index 4b71836..f1e3b7b 100644 --- a/data/mobs/psionic_entity.yaml +++ b/data/mobs/psionic_entity.yaml @@ -26,6 +26,7 @@ combat_descriptions: - "assaults %s's mind with psionic force" - "tears at %s's perception of reality" - "unleashes a devastating mental blast at %s" +size: large drops: remains: ectoplasm loot: diff --git a/data/mobs/rat.yaml b/data/mobs/rat.yaml index 488c76c..c1f8466 100644 --- a/data/mobs/rat.yaml +++ b/data/mobs/rat.yaml @@ -18,5 +18,6 @@ idle_descriptions: - "sniffs around the ground" - "nibbles on something" - "scurries in circles" +size: small drops: remains: bones diff --git a/data/mobs/sawmill_operator.yaml b/data/mobs/sawmill_operator.yaml index 99da8cf..03416ab 100644 --- a/data/mobs/sawmill_operator.yaml +++ b/data/mobs/sawmill_operator.yaml @@ -47,3 +47,4 @@ talk: - next: goodbye text: Nothing for now. unique: true +size: small diff --git a/data/mobs/scrap_bot.yaml b/data/mobs/scrap_bot.yaml index 78b8563..560bffd 100644 --- a/data/mobs/scrap_bot.yaml +++ b/data/mobs/scrap_bot.yaml @@ -25,6 +25,7 @@ combat_descriptions: - "rams %s with its chassis" - "swings a broken manipulator arm at %s" - "discharges a spark at %s" +size: medium drops: remains: scrap loot: diff --git a/data/mobs/security_turret.yaml b/data/mobs/security_turret.yaml index 25bcbf9..dca9e4a 100644 --- a/data/mobs/security_turret.yaml +++ b/data/mobs/security_turret.yaml @@ -27,6 +27,7 @@ combat_descriptions: - "fires twin bolts at %s" - "unleashes a burst of rounds at %s" - "locks onto %s and opens fire" +size: small drops: remains: scrap loot: diff --git a/data/mobs/skeleton.yaml b/data/mobs/skeleton.yaml index 88bdfe8..d3b1440 100644 --- a/data/mobs/skeleton.yaml +++ b/data/mobs/skeleton.yaml @@ -20,6 +20,7 @@ idle_descriptions: - "rattles ominously" - "clicks its jaw open and shut" - "stares with empty eye sockets" +size: medium drops: remains: bones loot: diff --git a/data/mobs/slug.yaml b/data/mobs/slug.yaml index 2275ca7..dd88cfc 100644 --- a/data/mobs/slug.yaml +++ b/data/mobs/slug.yaml @@ -19,6 +19,7 @@ combat_descriptions: - "lunges slimily at %s" - "sprays mucus toward %s" - "writhes in combat with %s" +size: small drops: remains: slug_mucus loot: diff --git a/data/mobs/tanner.yaml b/data/mobs/tanner.yaml index a00f497..3689e81 100644 --- a/data/mobs/tanner.yaml +++ b/data/mobs/tanner.yaml @@ -66,3 +66,4 @@ talk: - end: true text: '"Thanks."' unique: true +size: small diff --git a/data/mobs/titan_sentinel.yaml b/data/mobs/titan_sentinel.yaml index ad8b69f..9a7154b 100644 --- a/data/mobs/titan_sentinel.yaml +++ b/data/mobs/titan_sentinel.yaml @@ -26,6 +26,7 @@ combat_descriptions: - "smashes %s with a titan-grade fist" - "unleashes a devastating plasma barrage at %s" - "drives %s into the ground with overwhelming force" +size: massive drops: loot: - item_id: scrap diff --git a/data/mobs/veteran_merc.yaml b/data/mobs/veteran_merc.yaml index a33e73d..b3b7926 100644 --- a/data/mobs/veteran_merc.yaml +++ b/data/mobs/veteran_merc.yaml @@ -26,6 +26,7 @@ combat_descriptions: - "fires a high-caliber round at %s" - "puts %s in their crosshairs and squeezes the trigger" - "unleashes suppressive fire at %s" +size: medium drops: remains: bones loot: diff --git a/data/mobs/void_reaper.yaml b/data/mobs/void_reaper.yaml index 8c48d07..6180cba 100644 --- a/data/mobs/void_reaper.yaml +++ b/data/mobs/void_reaper.yaml @@ -28,6 +28,7 @@ combat_descriptions: - "swings its void scythe at %s" - "drains the life force from %s" - "envelops %s in absolute darkness" +size: massive drops: remains: ectoplasm loot: diff --git a/data/mobs/war_drone.yaml b/data/mobs/war_drone.yaml index d06563d..c785092 100644 --- a/data/mobs/war_drone.yaml +++ b/data/mobs/war_drone.yaml @@ -26,6 +26,7 @@ combat_descriptions: - "fires a plasma bolt at %s" - "slashes at %s with a rotary blade" - "launches a concussion grenade at %s" +size: medium drops: remains: circuit_board loot: diff --git a/data/mobs/weapons_smith.yaml b/data/mobs/weapons_smith.yaml index 4138a1b..d9566a5 100644 --- a/data/mobs/weapons_smith.yaml +++ b/data/mobs/weapons_smith.yaml @@ -63,3 +63,4 @@ talk: - end: true text: '"Nothing right now."' unique: true +size: small diff --git a/data/mobs/zombie.yaml b/data/mobs/zombie.yaml index 59a43e3..67e9b11 100644 --- a/data/mobs/zombie.yaml +++ b/data/mobs/zombie.yaml @@ -22,6 +22,7 @@ idle_descriptions: - "reaches out with decaying arms" - "stumbles and catches itself" - "stares blankly ahead" +size: medium drops: remains: bones loot: diff --git a/data/rooms/town/1.yaml b/data/rooms/town/1.yaml index 85f6a37..81e9351 100644 --- a/data/rooms/town/1.yaml +++ b/data/rooms/town/1.yaml @@ -1,7 +1,6 @@ id: 1 name: "Town Square" description: "The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district." -map_symbol: "S" exits: east: 2 west: 22 diff --git a/data/rooms/town/2.yaml b/data/rooms/town/2.yaml index 686dc65..8d3fe2d 100644 --- a/data/rooms/town/2.yaml +++ b/data/rooms/town/2.yaml @@ -1,7 +1,6 @@ id: 2 name: "Fishing Spot" description: "A quiet spot along the riverbank. The water is clear and fish can be seen swimming beneath the surface. You can cast a line here with a fishing rod and bait." -map_symbol: "F" exits: east: 4 north: 3 diff --git a/data/rooms/town/21.yaml b/data/rooms/town/21.yaml index 987f5ea..ac6dfad 100644 --- a/data/rooms/town/21.yaml +++ b/data/rooms/town/21.yaml @@ -1,7 +1,6 @@ id: 21 name: "Combat Arena" description: "A sandy training ground with practice dummies and sparring rings. Several men wander here looking for a fight." -map_symbol: "C" exits: west: 20 mobs: diff --git a/data/rooms/town/22.yaml b/data/rooms/town/22.yaml index 478995b..2199783 100644 --- a/data/rooms/town/22.yaml +++ b/data/rooms/town/22.yaml @@ -1,7 +1,6 @@ id: 22 name: "West of Town Square" description: "" -map_symbol: "W" exits: east: 1 west: 23 diff --git a/data/rooms/town/23.yaml b/data/rooms/town/23.yaml index f7301f6..45a9d18 100644 --- a/data/rooms/town/23.yaml +++ b/data/rooms/town/23.yaml @@ -1,7 +1,6 @@ id: 23 name: "West of West of Town Square" description: "Here is a description of where we are!" -map_symbol: "D" exits: east: 22 south: 24 diff --git a/data/rooms/town/24.yaml b/data/rooms/town/24.yaml index 60bf0d8..e1c9068 100644 --- a/data/rooms/town/24.yaml +++ b/data/rooms/town/24.yaml @@ -1,7 +1,6 @@ id: 24 name: "South of West of West of Town Square" description: "Here is a description of where we are!" -map_symbol: "N" exits: north: 23 east: 25 diff --git a/data/rooms/town/25.yaml b/data/rooms/town/25.yaml index a3dd843..78bc181 100644 --- a/data/rooms/town/25.yaml +++ b/data/rooms/town/25.yaml @@ -1,7 +1,6 @@ id: 25 name: "East of South of West of West of Town Square" description: "Here is a description of where we are!" -map_symbol: "N" exits: west: 24 east: 26 diff --git a/data/rooms/town/26.yaml b/data/rooms/town/26.yaml index a2899e7..0db976e 100644 --- a/data/rooms/town/26.yaml +++ b/data/rooms/town/26.yaml @@ -1,7 +1,6 @@ id: 26 name: "East of South of West of West of Town Square" description: "Here is a description of where we are!" -map_symbol: "Q" exits: west: 25 south: 27 diff --git a/data/rooms/town/27.yaml b/data/rooms/town/27.yaml index d435424..d820374 100644 --- a/data/rooms/town/27.yaml +++ b/data/rooms/town/27.yaml @@ -1,7 +1,6 @@ id: 27 name: "East of South of West of West of Town Square" description: "Here is a description of where we are!" -map_symbol: "J" exits: west: 28 north: 26 diff --git a/data/rooms/town/28.yaml b/data/rooms/town/28.yaml index 03913d5..b34ada0 100644 --- a/data/rooms/town/28.yaml +++ b/data/rooms/town/28.yaml @@ -1,6 +1,5 @@ id: 28 name: "East of South of West of West of Town Square" description: "Here is a description of where we are!" -map_symbol: "r" exits: east: 27 diff --git a/data/rooms/town/3.yaml b/data/rooms/town/3.yaml index 16ce1c8..8a1cade 100644 --- a/data/rooms/town/3.yaml +++ b/data/rooms/town/3.yaml @@ -1,7 +1,6 @@ id: 3 name: "Fly Fishing Pool" description: "A calm pool where insects hover over the water. Perfect for fly fishing." -map_symbol: "X" exits: south: 2 objects: diff --git a/data/rooms/town/30.yaml b/data/rooms/town/30.yaml index 8c167eb..ddce834 100644 --- a/data/rooms/town/30.yaml +++ b/data/rooms/town/30.yaml @@ -1,6 +1,5 @@ id: 30 name: "Above Town Square" description: "The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district." -map_symbol: "A" exits: down: 1 diff --git a/data/rooms/town/31.yaml b/data/rooms/town/31.yaml index 3d010df..5b5f4d5 100644 --- a/data/rooms/town/31.yaml +++ b/data/rooms/town/31.yaml @@ -1,7 +1,6 @@ id: 31 name: "Below Town Square" description: "The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district." -map_symbol: "b" exits: up: 1 down: 50 diff --git a/data/rooms/town/4.yaml b/data/rooms/town/4.yaml index 5a8b036..9453651 100644 --- a/data/rooms/town/4.yaml +++ b/data/rooms/town/4.yaml @@ -1,7 +1,6 @@ id: 4 name: "Woodcutting Glade" description: "A sunny glade at the edge of the forest. A sturdy tree stands here, perfect for practicing woodcutting. A solid rock formation near the edge of the glade looks like it could provide cover." -map_symbol: "W" exits: east: 6 north: 5 diff --git a/data/rooms/town/5.yaml b/data/rooms/town/5.yaml index bd760b1..cdf84d8 100644 --- a/data/rooms/town/5.yaml +++ b/data/rooms/town/5.yaml @@ -1,7 +1,6 @@ id: 5 name: "Oak Grove" description: "A stand of young oak trees. The wood here is tougher but more valuable." -map_symbol: "O" exits: south: 4 objects: diff --git a/data/rooms/town/6.yaml b/data/rooms/town/6.yaml index 0fb9c59..34b5bd0 100644 --- a/data/rooms/town/6.yaml +++ b/data/rooms/town/6.yaml @@ -1,7 +1,6 @@ id: 6 name: "Mining Pit" description: "A small mining pit. The rocky walls glint with veins of copper ore." -map_symbol: "M" exits: east: 8 north: 7 diff --git a/data/rooms/wilderness/150.yaml b/data/rooms/wilderness/150.yaml index 36b86ea..3e74da2 100644 --- a/data/rooms/wilderness/150.yaml +++ b/data/rooms/wilderness/150.yaml @@ -1,7 +1,6 @@ id: 150 name: "Hydroponics Bay" description: "A large enclosed area with climate-controlled growing stations. {34}Lush green patches{/} of soil are laid out in neat rows under artificial UV lamps. A {94}tool shed{/} stands near the entrance. Exits lead to specialized growing areas." -map_symbol: "H" exits: north: 15 east: 151 diff --git a/data/rooms/wilderness/151.yaml b/data/rooms/wilderness/151.yaml index 1bb25bf..4242270 100644 --- a/data/rooms/wilderness/151.yaml +++ b/data/rooms/wilderness/151.yaml @@ -1,7 +1,6 @@ id: 151 name: "Herb Garden" description: "A dedicated section of the hydroponics bay for growing herbs. {28}Herb patches{/} are arranged in two rows under specialized grow lights. The air here smells faintly of fresh vegetation." -map_symbol: "G" exits: west: 150 objects: diff --git a/data/rooms/wilderness/152.yaml b/data/rooms/wilderness/152.yaml index 5d2f62d..787bbbd 100644 --- a/data/rooms/wilderness/152.yaml +++ b/data/rooms/wilderness/152.yaml @@ -1,7 +1,6 @@ id: 152 name: "Allotment Field" description: "A section with larger soil beds for growing vegetables. {94}Allotment patches{/} stretch across the floor under warm overhead lamps. A single {213}flower patch{/} sits in the corner." -map_symbol: "A" exits: north: 150 objects: diff --git a/data/rooms/wilderness/160.yaml b/data/rooms/wilderness/160.yaml index dea9d00..7f30c34 100644 --- a/data/rooms/wilderness/160.yaml +++ b/data/rooms/wilderness/160.yaml @@ -1,7 +1,6 @@ id: 160 name: "Market Square" description: "A noisy open-air market wedged between crumbling hab-blocks. Vendors hawk salvaged tech and reconstituted food from makeshift stalls. A {220 bold}Guard{/} watches over the area with a stern expression." -map_symbol: "M" exits: north: 1 east: 161 diff --git a/data/rooms/wilderness/161.yaml b/data/rooms/wilderness/161.yaml index c887347..7c2c140 100644 --- a/data/rooms/wilderness/161.yaml +++ b/data/rooms/wilderness/161.yaml @@ -1,7 +1,6 @@ id: 161 name: "Farm Outpost" description: "A cluster of hydroponic grow-pods on the asteroid's surface, shielded by a flickering atmospheric dome. Rows of bio-luminescent crops stretch into the distance." -map_symbol: "F" exits: west: 160 mobs: diff --git a/data/rooms/wilderness/162.yaml b/data/rooms/wilderness/162.yaml index a8333d8..6aab585 100644 --- a/data/rooms/wilderness/162.yaml +++ b/data/rooms/wilderness/162.yaml @@ -1,6 +1,5 @@ id: 162 name: "Detention Cell" description: "A small, grimy holding cell. The walls are scratched with tally marks from previous occupants. A heavy door bars the only exit." -map_symbol: "J" exits: north: 1
\ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index 874d6a3..af00297 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -56,6 +56,7 @@ func DefaultColors() ColorsConfig { "warning": "208", "farm_grow": "34", "farm_disease": "196", + "map_at": "15", } } diff --git a/internal/game/action_farm.go b/internal/game/action_farm.go index 87ea6b7..d8c7e00 100644 --- a/internal/game/action_farm.go +++ b/internal/game/action_farm.go @@ -502,7 +502,7 @@ func (g *Game) farmObjSuffix(p *player.Player, defID string, index int) string { return "" } -func (g *Game) showFarmPatches(sess *net.Session, p *player.Player) { +func (g *Game) showFarmPatches(sess *net.Session, p *player.Player) []string { objInstances := g.World.AllObjInstances(p.RoomID) hasFarmPatches := false for _, obj := range objInstances { @@ -512,7 +512,7 @@ func (g *Game) showFarmPatches(sess *net.Session, p *player.Player) { } } if !hasFarmPatches { - return + return nil } type farmDisplay struct { @@ -609,13 +609,15 @@ func (g *Game) showFarmPatches(sess *net.Session, p *player.Player) { } if len(displays) == 0 { - return + return nil } - sess.WriteLine("") + var lines []string + lines = append(lines, "") for _, d := range displays { - sess.WriteLine(fmt.Sprintf("A %s: %s.", d.name, d.line)) + lines = append(lines, fmt.Sprintf("A %s: %s.", d.name, d.line)) } + return lines } func (g *Game) countPatchesByType(instances []world.ObjState, defID string) int { diff --git a/internal/game/action_finishing_blow.go b/internal/game/action_finishing_blow.go index 070559d..d404fb3 100644 --- a/internal/game/action_finishing_blow.go +++ b/internal/game/action_finishing_blow.go @@ -72,7 +72,7 @@ func (g *Game) doFinishingBlow(sess *net.Session, p *player.Player, mob *world.M if consumed { p.RemoveItem(mob.FinishingBlow, 1) } - sess.WriteLine(fmt.Sprintf("\nYou use the %s on %s!", g.itemColorize(sess, fbDef, fbDef.Name), g.colorize(sess, "mob", mobDisplayName(mob, true)))) + sess.WriteLine(fmt.Sprintf("You use the %s on %s!", g.itemColorize(sess, fbDef, fbDef.Name), g.colorize(sess, "mob", mobDisplayName(mob, true)))) mob.HP = 0 g.endCombat(sess, p, mob) } diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go index 00c9070..135a7be 100644 --- a/internal/game/action_gather.go +++ b/internal/game/action_gather.go @@ -146,7 +146,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { return } } - sess.WriteLine(fmt.Sprintf("\n%s", cfg.GatherMsg)) + sess.WriteLine(fmt.Sprintf("%s", cfg.GatherMsg)) p.Action.Data["step"] = 1 p.Action.WaitLeft = engine.ToTicks(wait) return diff --git a/internal/game/action_room.go b/internal/game/action_room.go index b997352..af5b99b 100644 --- a/internal/game/action_room.go +++ b/internal/game/action_room.go @@ -17,7 +17,7 @@ func (g *Game) runEnterSteps(sess *net.Session, roomID int) { continue } if step.Message != "" { - sess.WriteLine(fmt.Sprintf("\n%s", step.Message)) + sess.WriteLine(fmt.Sprintf("%s", step.Message)) } } } diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go index c37134f..d57181a 100644 --- a/internal/game/action_talk.go +++ b/internal/game/action_talk.go @@ -55,7 +55,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", g.colorize(sess, "dialog", node.Message))) + sess.WriteLine(fmt.Sprintf("%s", g.colorize(sess, "dialog", node.Message))) if node.Action != nil { if node.Action.Shop != nil { diff --git a/internal/game/action_use.go b/internal/game/action_use.go index b4c4b25..0c7d45c 100644 --- a/internal/game/action_use.go +++ b/internal/game/action_use.go @@ -46,7 +46,7 @@ func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectD Data: map[string]any{"step": 0, "use_cfg": cfg}, } - sess.WriteLine(fmt.Sprintf("\n%s", cfg.Message)) + sess.WriteLine(fmt.Sprintf("%s", cfg.Message)) } func (g *Game) advanceUse(sess *net.Session, p *player.Player) { diff --git a/internal/game/cmd_alias.go b/internal/game/cmd_alias.go index 890b904..33d8de7 100644 --- a/internal/game/cmd_alias.go +++ b/internal/game/cmd_alias.go @@ -24,10 +24,10 @@ func (g *Game) doAlias(sess *net.Session, args []string) { if len(args) == 0 { if len(sess.Account.Aliases) == 0 { - sess.WriteLine("\nNo aliases defined. Use ALIAS <name> <command> to create one.") + sess.WriteLine("No aliases defined. Use ALIAS <name> <command> to create one.") return } - sess.WriteLine("\nAliases:") + sess.WriteLine("Aliases:") names := make([]string, 0, len(sess.Account.Aliases)) maxLen := 0 for name := range sess.Account.Aliases { @@ -67,7 +67,7 @@ func (g *Game) doAlias(sess *net.Session, args []string) { return } - sess.WriteLine(fmt.Sprintf("\nAlias set: %s -> %s", aliasName, aliasCmd)) + sess.WriteLine(fmt.Sprintf("Alias set: %s -> %s", aliasName, aliasCmd)) } func (g *Game) doUnalias(sess *net.Session, args []string) { @@ -77,7 +77,7 @@ func (g *Game) doUnalias(sess *net.Session, args []string) { } if len(args) == 0 { - sess.WriteLine("\nUsage: UNALIAS <name>") + sess.WriteLine("Usage: UNALIAS <name>") sess.WriteLine("Removes an alias. Use ALIAS with no arguments to see your aliases.") return } @@ -106,5 +106,5 @@ func (g *Game) doUnalias(sess *net.Session, args []string) { return } - sess.WriteLine(fmt.Sprintf("\nAlias '%s' removed.", aliasName)) + sess.WriteLine(fmt.Sprintf("Alias '%s' removed.", aliasName)) } diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index ce396e8..1f18d00 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -151,14 +151,14 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn if len(styleParts) > 0 { styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")" } - sess.WriteLine(fmt.Sprintf("\nYou attack %s!%s", g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) + sess.WriteLine(fmt.Sprintf("You attack %s!%s", g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) } else { mod := GetMod(p.AutotriggerMod) modName := p.AutotriggerMod if mod != nil { modName = mod.Name } - sess.WriteLine(fmt.Sprintf("\nYou attack %s with %s!", + sess.WriteLine(fmt.Sprintf("You attack %s with %s!", g.colorize(sess, "mob", mobDisplayName(mob, true)), g.colorize(sess, "science_mod", modName))) } diff --git a/internal/game/cmd_bank.go b/internal/game/cmd_bank.go index d95f7c0..b31dd16 100644 --- a/internal/game/cmd_bank.go +++ b/internal/game/cmd_bank.go @@ -339,7 +339,7 @@ func (g *Game) doBank(sess *net.Session) { } sess.State = net.StateBank - sess.WriteLine(g.colorize(sess, "dialog", "\nYou access the bank terminal.")) + sess.WriteLine(g.colorize(sess, "dialog", "You access the bank terminal.")) g.showBankBrowse(sess) g.writeBankPrompt(sess) } diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go index fe2fe62..51f8158 100644 --- a/internal/game/cmd_color.go +++ b/internal/game/cmd_color.go @@ -11,7 +11,7 @@ import ( func (g *Game) doColor(sess *net.Session, input string) { if sess.Account == nil { - sess.WriteLine("\nYou must be logged into an account to set colors.") + sess.WriteLine("You must be logged into an account to set colors.") return } @@ -24,7 +24,7 @@ func (g *Game) doColor(sess *net.Session, input string) { target := strings.ToLower(parts[0]) if _, ok := config.DefaultColors()[target]; !ok { - sess.WriteLine(fmt.Sprintf("\nUnknown color target: %s", target)) + sess.WriteLine(fmt.Sprintf("Unknown color target: %s", target)) sess.WriteLine("Available targets:") for _, t := range colorCategoryOrder { sess.WriteLine(fmt.Sprintf(" %s", t)) @@ -35,7 +35,7 @@ func (g *Game) doColor(sess *net.Session, input string) { if len(parts) == 1 { current := g.getCurrentColor(sess, target) source := g.colorSource(sess, target) - sess.WriteLine(fmt.Sprintf("\n%s = %s [source: %s]", target, current, source)) + sess.WriteLine(fmt.Sprintf("%s = %s [source: %s]", target, current, source)) return } @@ -43,26 +43,26 @@ func (g *Game) doColor(sess *net.Session, input string) { if len(parts) == 2 && parts[1] == "reset" { g.saveAccountColor(sess, target, "") - sess.WriteLine(fmt.Sprintf("\n%s reset to default (%s).", target, config.DefaultColors()[target])) + sess.WriteLine(fmt.Sprintf("%s reset to default (%s).", target, config.DefaultColors()[target])) return } if value == "off" { g.saveAccountColor(sess, target, "off") - sess.WriteLine(fmt.Sprintf("\n%s set to off.", target)) + sess.WriteLine(fmt.Sprintf("%s set to off.", target)) return } spec := color.Parse(value) if spec.Empty() && value != "" { - sess.WriteLine(fmt.Sprintf("\nInvalid color string: %s", value)) + sess.WriteLine(fmt.Sprintf("Invalid color string: %s", value)) sess.WriteLine("Format: <0-255> [bg:<0-255>] [bold] [dim] [underline]") sess.WriteLine("Example: 208 bold") return } g.saveAccountColor(sess, target, value) - sess.WriteLine(fmt.Sprintf("\n%s set to %s.", target, value)) + sess.WriteLine(fmt.Sprintf("%s set to %s.", target, value)) } func (g *Game) saveAccountColor(sess *net.Session, target, value string) { diff --git a/internal/game/cmd_colortable.go b/internal/game/cmd_colortable.go index 86299dc..baa30e8 100644 --- a/internal/game/cmd_colortable.go +++ b/internal/game/cmd_colortable.go @@ -11,14 +11,14 @@ import ( 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)") + sess.WriteLine("Enable colors first: option color ansi (or xterm256)") return } p := sess.Player unicode := p.OptionBool("unicode") - sess.WriteLine(fmt.Sprintf("\nColor mode: %s", mode)) + sess.WriteLine(fmt.Sprintf("Color mode: %s", mode)) sess.WriteLine("") writeSectionHeader(sess, "Foreground Colors", unicode) diff --git a/internal/game/cmd_consume.go b/internal/game/cmd_consume.go index 3782237..fa55d3d 100644 --- a/internal/game/cmd_consume.go +++ b/internal/game/cmd_consume.go @@ -72,7 +72,7 @@ func (g *Game) doEat(sess *net.Session, input string) { } if !p.OptionBool("queue_silently") { - sess.WriteLine(fmt.Sprintf("\nYou prepare to eat %s.", g.itemColorize(sess, def, def.Name))) + sess.WriteLine(fmt.Sprintf("You prepare to eat %s.", g.itemColorize(sess, def, def.Name))) } } @@ -210,7 +210,7 @@ func (g *Game) doDrink(sess *net.Session, args []string) { } if !p.OptionBool("queue_silently") { - sess.WriteLine(fmt.Sprintf("\nYou prepare to drink the %s.", g.itemColorize(sess, def, def.Name))) + sess.WriteLine(fmt.Sprintf("You prepare to drink the %s.", g.itemColorize(sess, def, def.Name))) } } @@ -234,7 +234,7 @@ func (g *Game) applyPotion(sess *net.Session, p *player.Player, itemID string, d if p.Battery > 100 { p.Battery = 100 } - sess.WriteLine(fmt.Sprintf("\nYou drink the %s. Your battery is restored by %d%%.", + sess.WriteLine(fmt.Sprintf("You drink the %s. Your battery is restored by %d%%.", g.itemColorize(sess, def, def.Name), batteryRestore)) case "heal", "": @@ -253,7 +253,7 @@ func (g *Game) applyPotion(sess *net.Session, p *player.Player, itemID string, d p.HP += healAmount } actualHeal := p.HP - before - sess.WriteLine(fmt.Sprintf("\nYou drink the %s. You restore %d hitpoints.", + sess.WriteLine(fmt.Sprintf("You drink the %s. You restore %d hitpoints.", g.itemColorize(sess, def, def.Name), actualHeal)) case "all_combat": @@ -266,7 +266,7 @@ func (g *Game) applyPotion(sess *net.Session, p *player.Player, itemID string, d TicksLeft: duration, }) } - sess.WriteLine(fmt.Sprintf("\nYou drink the %s. Your combat stats are boosted by %d%% for %d ticks.", + sess.WriteLine(fmt.Sprintf("You drink the %s. Your combat stats are boosted by %d%% for %d ticks.", g.itemColorize(sess, def, def.Name), def.PotionBonus, duration)) default: @@ -276,7 +276,7 @@ func (g *Game) applyPotion(sess *net.Session, p *player.Player, itemID string, d BonusPercent: def.PotionBonus, TicksLeft: duration, }) - sess.WriteLine(fmt.Sprintf("\nYou drink the %s. Your %s is boosted by %d%% for %d ticks.", + sess.WriteLine(fmt.Sprintf("You drink the %s. Your %s is boosted by %d%% for %d ticks.", g.itemColorize(sess, def, def.Name), effect, def.PotionBonus, duration)) } diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go index ca3c244..ffed913 100644 --- a/internal/game/cmd_drop.go +++ b/internal/game/cmd_drop.go @@ -23,7 +23,7 @@ func (g *Game) doDropAll(sess *net.Session) { return } sess.State = net.StateDropAllConfirm - sess.WriteLine(fmt.Sprintf("\nDrop all %d items? [y/N]", count)) + sess.WriteLine(fmt.Sprintf("Drop all %d items? [y/N]", count)) } func (g *Game) doDropAllNamed(sess *net.Session, input string) { @@ -230,9 +230,9 @@ func (g *Game) handleDropAll(sess *net.Session, input string) { sess.State = net.StateGame if len(dropped) == 0 { - sess.WriteLine("\nYou have nothing to drop.") + sess.WriteLine("You have nothing to drop.") } else { - sess.Write(fmt.Sprintf("\nYou drop your ")) + sess.Write(fmt.Sprintf("You drop your ")) formatPickupList(sess, dropped) } g.writePrompt(sess) diff --git a/internal/game/cmd_hide.go b/internal/game/cmd_hide.go index 2dca478..9f567ca 100644 --- a/internal/game/cmd_hide.go +++ b/internal/game/cmd_hide.go @@ -112,9 +112,7 @@ func (g *Game) executeHide(sess *net.Session, args []string, rawInput string) { p.ActionState = &ActionState{Type: ActionHiding, TargetName: objDef.Name} if inCombat { - sess.WriteLine(fmt.Sprintf("\nYou try to get behind the %s for cover...", objDef.Name)) - } else { - sess.WriteLine(fmt.Sprintf("You position yourself behind the %s.", objDef.Name)) + sess.WriteLine(fmt.Sprintf("You try to get behind the %s for cover...", objDef.Name)) } } diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 903b32d..98d48fd 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -23,10 +23,29 @@ func (g *Game) doLook(sess *net.Session) { } g.showRoomName(sess, p, room) - g.showRoomDescription(sess, p, room) - g.showRoomObjects(sess, p, room) - g.showRoomMobs(sess, p, room) - g.showGroundItems(sess, p, room) + + var mapLines []string + if p.OptionString("tiny_map") != "off" { + mapLines = buildTinyMap(g, sess, p.RoomID, mapGlyphsForPlayer(p.OptionBool("unicode"))) + if len(mapLines) != 7 { + mapLines = nil + } + } + + var content []string + content = append(content, g.showRoomDescription(sess, p, room)...) + content = append(content, g.showRoomObjects(sess, p, room)...) + content = append(content, g.showRoomMobs(sess, p, room)...) + content = append(content, g.showGroundItems(sess, p, room)...) + + if len(mapLines) == 7 { + g.writeLookSideBySide(sess, p, content, mapLines) + } else { + for _, line := range content { + sess.WriteLine(line) + } + } + g.showRoomExits(sess, p, room) g.showRoomPlayers(sess, p, room) } @@ -38,7 +57,7 @@ func (g *Game) showRoomName(sess *net.Session, p *player.Player, room *world.Roo ) } -func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *world.Room) { +func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *world.Room) []string { descWidth := p.OptionInt("room_desc_width") if descWidth <= 0 { descWidth = 70 @@ -46,319 +65,316 @@ func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *wo rawLines := wrapText(room.Description, descWidth) mode := g.colorMode(sess) roomDescSpec := g.resolveColor(sess, "room_desc") - descLines := make([]string, len(rawLines)) + lines := make([]string, len(rawLines)) for i, l := range rawLines { - descLines[i] = color.ExpandTagsDefault(mode, roomDescSpec, l) - } - wroteDesc := false - if p.OptionString("tiny_map") != "off" { - mapLines := buildTinyMap(g, p.RoomID, mapGlyphsForPlayer(p.OptionBool("unicode"))) - if len(mapLines) == 7 { - g.writeLookSideBySide(sess, p, descLines, mapLines) - wroteDesc = true - } - } - if !wroteDesc { - for _, line := range descLines { - sess.WriteLine(line) - } + lines[i] = color.ExpandTagsDefault(mode, roomDescSpec, l) } + return lines } -func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Room) { +func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Room) []string { mobs := g.MobStore.MobsInRoom(p.RoomID) - if len(mobs) > 0 { - sort.Slice(mobs, func(i, j int) bool { - iDamaged := mobs[i].HP < mobs[i].MaxHP - jDamaged := mobs[j].HP < mobs[j].MaxHP - if iDamaged != jDamaged { - return iDamaged - } - return mobs[i].InstanceID < mobs[j].InstanceID - }) - sess.WriteLine("") - playerLevel := p.CombatLevel() - for _, m := range mobs { - hp := "" - if m.HP < m.MaxHP { - hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("167"), fmt.Sprint(m.HP)), m.MaxHP) - } - var desc string - if combat.IsMobInCombat(m.InstanceID) { - def, err := g.MobStore.LoadDef(m.DefID) - if err == nil && len(def.CombatDescriptions) > 0 { - target := combat.GetMobTarget(m.InstanceID) - pattern := def.CombatDescriptions[randInt(len(def.CombatDescriptions))] - desc = " " + fmt.Sprintf(pattern, target) - } - } else if m.IdleDescription != "" { - desc = fmt.Sprintf(" %s", m.IdleDescription) - } - displayName := m.Name - if !m.Unique { - displayName = "A " + m.Name - } - mobColor := "mob" - if m.Protected { - mobColor = "protected_mob" - } - mobLevel := mobCombatLevel(m) - levelStr := g.levelColorize(sess, playerLevel, mobLevel, fmt.Sprintf("(level %d)", mobLevel)) - sess.WriteLine(fmt.Sprintf("%s %s%s%s", g.colorize(sess, mobColor, displayName), levelStr, hp, desc)) + if len(mobs) == 0 { + return nil + } + sort.Slice(mobs, func(i, j int) bool { + iDamaged := mobs[i].HP < mobs[i].MaxHP + jDamaged := mobs[j].HP < mobs[j].MaxHP + if iDamaged != jDamaged { + return iDamaged + } + return mobs[i].InstanceID < mobs[j].InstanceID + }) + var lines []string + lines = append(lines, "") + playerLevel := p.CombatLevel() + for _, m := range mobs { + hp := "" + if m.HP < m.MaxHP { + hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("167"), fmt.Sprint(m.HP)), m.MaxHP) + } + var desc string + if combat.IsMobInCombat(m.InstanceID) { + def, err := g.MobStore.LoadDef(m.DefID) + if err == nil && len(def.CombatDescriptions) > 0 { + target := combat.GetMobTarget(m.InstanceID) + pattern := def.CombatDescriptions[randInt(len(def.CombatDescriptions))] + desc = " " + fmt.Sprintf(pattern, target) + } + } else if m.IdleDescription != "" { + desc = fmt.Sprintf(" %s", m.IdleDescription) + } + displayName := m.Name + if !m.Unique { + displayName = "A " + m.Name + } + mobColor := "mob" + if m.Protected { + mobColor = "protected_mob" } + mobLevel := mobCombatLevel(m) + levelStr := g.levelColorize(sess, playerLevel, mobLevel, fmt.Sprintf("(level %d)", mobLevel)) + lines = append(lines, fmt.Sprintf("%s %s%s%s", g.colorize(sess, mobColor, displayName), levelStr, hp, desc)) } + return lines } -func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world.Room) { +func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world.Room) []string { objs := g.World.AllObjInstances(p.RoomID) - if len(objs) > 0 { - sess.WriteLine("") - grouped := make(map[string]int) - var order []string - for _, o := range objs { - if _, ok := grouped[o.DefID]; !ok { - order = append(order, o.DefID) - } - grouped[o.DefID]++ + if len(objs) == 0 { + return g.showFarmPatches(sess, p) + } + var lines []string + lines = append(lines, "") + grouped := make(map[string]int) + var order []string + for _, o := range objs { + if _, ok := grouped[o.DefID]; !ok { + order = append(order, o.DefID) + } + grouped[o.DefID]++ + } + for _, objID := range order { + count := grouped[objID] + def, err := g.ObjectStore.Load(objID) + if err != nil { + continue } - for _, objID := range order { - count := grouped[objID] - def, err := g.ObjectStore.Load(objID) - if err != nil { - continue - } - if def.Hidden { - continue - } + if def.Hidden { + continue + } + + if farmPatchDefIDs[objID] { + continue + } - if farmPatchDefIDs[objID] { + type instInfo struct { + idx int + depleted bool + sharedMax int + sharedCur int + respawnIn int + quality int + } + var instances []instInfo + for i := 0; i < count; i++ { + st := g.World.GetObjState(p.RoomID, objID, i) + if st == nil { continue } + instances = append(instances, instInfo{ + idx: i + 1, + depleted: st.Depleted, + sharedMax: int(st.SharedMax), + sharedCur: st.SharedTimer, + respawnIn: int(st.DepleteTimer), + quality: int(st.Quality), + }) + } + multi := len(instances) > 1 + showTimers := p.OptionBool("depletion") + + var freshIdxs []int + var timed, depleted []instInfo + for _, ins := range instances { + if ins.depleted { + depleted = append(depleted, ins) + } else if ins.sharedMax > 0 && ins.sharedCur < ins.sharedMax { + timed = append(timed, ins) + } else { + freshIdxs = append(freshIdxs, ins.idx) + } + } - type instInfo struct { - idx int - depleted bool - sharedMax int - sharedCur int - respawnIn int - quality int + roomDesc := def.InRoomDescription + if roomDesc != "" { + roomDesc = color.ExpandTags(g.colorMode(sess), roomDesc) + } + coloredName := g.objColorize(sess, def, def.Name) + coloredPlural := g.objColorize(sess, def, def.Name+"s") + + if len(freshIdxs) > 0 { + var line string + if roomDesc != "" { + line = roomDesc + } else if len(freshIdxs) == 1 { + line = fmt.Sprintf("A %s is here.", coloredName) + } else { + line = fmt.Sprintf("%d %s are here.", len(freshIdxs), coloredPlural) } - var instances []instInfo - for i := 0; i < count; i++ { - st := g.World.GetObjState(p.RoomID, objID, i) - if st == nil { - continue - } - instances = append(instances, instInfo{ - idx: i + 1, - depleted: st.Depleted, - sharedMax: int(st.SharedMax), - sharedCur: st.SharedTimer, - respawnIn: int(st.DepleteTimer), - quality: int(st.Quality), - }) + var suffix string + if multi && (len(timed) > 0 || len(depleted) > 0) { + suffix = fmt.Sprintf(" [%s]", joinInts(freshIdxs)) } - multi := len(instances) > 1 - showTimers := p.OptionBool("depletion") - - var freshIdxs []int - var timed, depleted []instInfo - for _, ins := range instances { - if ins.depleted { - depleted = append(depleted, ins) - } else if ins.sharedMax > 0 && ins.sharedCur < ins.sharedMax { - timed = append(timed, ins) - } else { - freshIdxs = append(freshIdxs, ins.idx) - } + qualityTimer := "" + if showTimers && len(instances) > 0 && instances[0].quality > 0 { + qualityTimer = fmt.Sprintf(" (Burning for %d more ticks)", instances[0].quality) } + lines = append(lines, fmt.Sprintf("%s%s%s", line, suffix, qualityTimer)) + } - roomDesc := def.InRoomDescription + for _, ins := range timed { + var line string if roomDesc != "" { - roomDesc = color.ExpandTags(g.colorMode(sess), roomDesc) + line = roomDesc + } else { + line = fmt.Sprintf("A %s is here.", coloredName) } - coloredName := g.objColorize(sess, def, def.Name) - coloredPlural := g.objColorize(sess, def, def.Name+"s") - - if len(freshIdxs) > 0 { - var line string - if roomDesc != "" { - line = roomDesc - } else if len(freshIdxs) == 1 { - line = fmt.Sprintf("A %s is here.", coloredName) - } else { - line = fmt.Sprintf("%d %s are here.", len(freshIdxs), coloredPlural) - } - var suffix string - if multi && (len(timed) > 0 || len(depleted) > 0) { - suffix = fmt.Sprintf(" [%s]", joinInts(freshIdxs)) - } - qualityTimer := "" - if showTimers && len(instances) > 0 && instances[0].quality > 0 { - qualityTimer = fmt.Sprintf(" (Burning for %d more ticks)", instances[0].quality) - } - sess.WriteLine(fmt.Sprintf("%s%s%s", line, suffix, qualityTimer)) + tag := "" + if multi { + tag = fmt.Sprintf(" [%d]", ins.idx) } - - for _, ins := range timed { - var line string - if roomDesc != "" { - line = roomDesc - } else { - line = fmt.Sprintf("A %s is here.", coloredName) - } - tag := "" - if multi { - tag = fmt.Sprintf(" [%d]", ins.idx) - } - timer := "" - if showTimers { - timer = fmt.Sprintf(" (despawn: %d/%d)", ins.sharedCur, ins.sharedMax) - } - sess.WriteLine(fmt.Sprintf("%s%s%s", line, tag, timer)) + timer := "" + if showTimers { + timer = fmt.Sprintf(" (despawn: %d/%d)", ins.sharedCur, ins.sharedMax) } + lines = append(lines, fmt.Sprintf("%s%s%s", line, tag, timer)) + } - for _, ins := range depleted { - var line string - if roomDesc != "" { - line = roomDesc - } else { - line = fmt.Sprintf("A %s is here.", coloredName) - } - tag := "" - if multi { - tag = fmt.Sprintf(" [%d]", ins.idx) - } - timer := "" - if showTimers { - timer = fmt.Sprintf(" (respawns in %d ticks)", ins.respawnIn) - } - sess.WriteLine(fmt.Sprintf("%s (depleted)%s%s", line, tag, timer)) + for _, ins := range depleted { + var line string + if roomDesc != "" { + line = roomDesc + } else { + line = fmt.Sprintf("A %s is here.", coloredName) + } + tag := "" + if multi { + tag = fmt.Sprintf(" [%d]", ins.idx) + } + timer := "" + if showTimers { + timer = fmt.Sprintf(" (respawns in %d ticks)", ins.respawnIn) } + lines = append(lines, fmt.Sprintf("%s (depleted)%s%s", line, tag, timer)) } } - g.showFarmPatches(sess, p) + lines = append(lines, g.showFarmPatches(sess, p)...) + return lines } -func (g *Game) showGroundItems(sess *net.Session, p *player.Player, room *world.Room) { +func (g *Game) showGroundItems(sess *net.Session, p *player.Player, room *world.Room) []string { ground := g.World.GroundItemsDetailed(p.RoomID) - if len(ground) > 0 { - showDespawn := p.OptionBool("despawn") - showReserve := p.OptionBool("reserve") + if len(ground) == 0 { + return nil + } + showDespawn := p.OptionBool("despawn") + showReserve := p.OptionBool("reserve") - type displayLine struct { - name string - quantity int - colorName string - annotation string - } + type displayLine struct { + name string + quantity int + colorName string + annotation string + } - type groupKey struct { - itemID string - despawnTimer int - } + type groupKey struct { + itemID string + despawnTimer int + } - var lines []displayLine - groups := make(map[groupKey]*displayLine) - var groupOrder []groupKey + var lines []displayLine + groups := make(map[groupKey]*displayLine) + var groupOrder []groupKey - for _, info := range ground { - def, err := g.ItemStore.Load(info.ItemID) - name := info.ItemID - if err == nil { - name = def.Name - } - coloredName := g.itemColorize(sess, def, name) + for _, info := range ground { + def, err := g.ItemStore.Load(info.ItemID) + name := info.ItemID + if err == nil { + name = def.Name + } + coloredName := g.itemColorize(sess, def, name) - if info.ReservedFor != "" { - var parts []string - if showReserve { - parts = append(parts, fmt.Sprintf("reserved for %s %dt", info.ReservedFor, info.ReserveTimer)) - } else { - parts = append(parts, "reserved") - } - if showDespawn && !info.IsSpawn { - parts = append(parts, fmt.Sprintf("despawns %dt", info.DespawnTimer)) - } - annotation := "" - if len(parts) > 0 { - annotation = fmt.Sprintf(" (%s)", strings.Join(parts, ", ")) - } - lines = append(lines, displayLine{ - name: name, - quantity: info.Quantity, - colorName: coloredName, - annotation: annotation, - }) - continue + if info.ReservedFor != "" { + var parts []string + if showReserve { + parts = append(parts, fmt.Sprintf("reserved for %s %dt", info.ReservedFor, info.ReserveTimer)) + } else { + parts = append(parts, "reserved") } + if showDespawn && !info.IsSpawn { + parts = append(parts, fmt.Sprintf("despawns %dt", info.DespawnTimer)) + } + annotation := "" + if len(parts) > 0 { + annotation = fmt.Sprintf(" (%s)", strings.Join(parts, ", ")) + } + lines = append(lines, displayLine{ + name: name, + quantity: info.Quantity, + colorName: coloredName, + annotation: annotation, + }) + continue + } - timerKey := -1 + timerKey := -1 + if showDespawn && !info.IsSpawn { + timerKey = info.DespawnTimer + } + key := groupKey{itemID: info.ItemID, despawnTimer: timerKey} + + if existing, ok := groups[key]; ok { + existing.quantity += info.Quantity + } else { + annotation := "" if showDespawn && !info.IsSpawn { - timerKey = info.DespawnTimer + annotation = fmt.Sprintf(" (despawns %dt)", info.DespawnTimer) } - key := groupKey{itemID: info.ItemID, despawnTimer: timerKey} - - if existing, ok := groups[key]; ok { - existing.quantity += info.Quantity - } else { - annotation := "" - if showDespawn && !info.IsSpawn { - annotation = fmt.Sprintf(" (despawns %dt)", info.DespawnTimer) - } - dl := &displayLine{ - name: name, - quantity: info.Quantity, - colorName: coloredName, - annotation: annotation, - } - groups[key] = dl - groupOrder = append(groupOrder, key) + dl := &displayLine{ + name: name, + quantity: info.Quantity, + colorName: coloredName, + annotation: annotation, } + groups[key] = dl + groupOrder = append(groupOrder, key) } + } - for _, key := range groupOrder { - lines = append(lines, *groups[key]) - } + for _, key := range groupOrder { + lines = append(lines, *groups[key]) + } - sort.Slice(lines, func(i, j int) bool { - return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name) - }) + sort.Slice(lines, func(i, j int) bool { + return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name) + }) - sess.WriteLine("") - sess.WriteLine("On the ground:") + var out []string + out = append(out, "") + out = append(out, "On the ground:") - type fmtLine struct { - prefix string - annotation string - } - var fmtLines []fmtLine - maxPrefix := 0 + type fmtLine struct { + prefix string + annotation string + } + var fmtLines []fmtLine + maxPrefix := 0 - for _, dl := range lines { - prefix := "" - if dl.quantity > 1 { - prefix = fmt.Sprintf(" %d x %s", dl.quantity, dl.colorName) - } else { - prefix = fmt.Sprintf(" %s", dl.colorName) - } - if visibleLen(prefix) > maxPrefix { - maxPrefix = visibleLen(prefix) - } - fmtLines = append(fmtLines, fmtLine{prefix, dl.annotation}) + for _, dl := range lines { + prefix := "" + if dl.quantity > 1 { + prefix = fmt.Sprintf(" %d x %s", dl.quantity, dl.colorName) + } else { + prefix = fmt.Sprintf(" %s", dl.colorName) } + if visibleLen(prefix) > maxPrefix { + maxPrefix = visibleLen(prefix) + } + fmtLines = append(fmtLines, fmtLine{prefix, dl.annotation}) + } - for _, l := range fmtLines { - if l.annotation != "" { - pad := maxPrefix + 1 + (len(l.prefix) - visibleLen(l.prefix)) - sess.WriteLine(fmt.Sprintf("%-*s%s", pad, l.prefix, l.annotation)) - } else { - sess.WriteLine(l.prefix) - } + for _, l := range fmtLines { + if l.annotation != "" { + pad := maxPrefix + 1 + (len(l.prefix) - visibleLen(l.prefix)) + out = append(out, fmt.Sprintf("%-*s%s", pad, l.prefix, l.annotation)) + } else { + out = append(out, l.prefix) } } + return out } func (g *Game) showRoomExits(sess *net.Session, p *player.Player, room *world.Room) { @@ -763,8 +779,8 @@ func wrapText(text string, width int) []string { return lines } -func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, descLines []string, mapLines []string) { - total := len(descLines) +func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, contentLines []string, mapLines []string) { + total := len(contentLines) if len(mapLines) > total { total = len(mapLines) } @@ -774,22 +790,23 @@ func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, descLine mapWidth = 70 } for i := 0; i < total; i++ { - desc := "" - if i < len(descLines) { - desc = descLines[i] + content := "" + if i < len(contentLines) { + content = contentLines[i] } - mapLine := "" if i < len(mapLines) { - mapLine = mapLines[i] - } - if leftMap { - sess.WriteLine(fmt.Sprintf("%s %s", mapLine, desc)) - } else { - pad := mapWidth + (len(desc) - visibleLen(desc)) - if pad < 0 { - pad = 0 + mapLine := mapLines[i] + if leftMap { + sess.WriteLine(fmt.Sprintf("%s %s", mapLine, content)) + } else { + pad := mapWidth + (len(content) - visibleLen(content)) + if pad < 0 { + pad = 0 + } + sess.WriteLine(fmt.Sprintf("%-*s %s", pad, content, mapLine)) } - sess.WriteLine(fmt.Sprintf("%-*s %s", pad, desc, mapLine)) + } else { + sess.WriteLine(content) } } } diff --git a/internal/game/cmd_map.go b/internal/game/cmd_map.go index 90f58ab..fc4ebf1 100644 --- a/internal/game/cmd_map.go +++ b/internal/game/cmd_map.go @@ -19,7 +19,7 @@ func (g *Game) doMap(sess *net.Session) { mapHeight = 5 } - lines := buildFullMap(g, p.RoomID, mapWidth, mapHeight, mapGlyphsForPlayer(p.OptionBool("unicode"))) + lines := buildFullMap(g, sess, p.RoomID, mapWidth, mapHeight, mapGlyphsForPlayer(p.OptionBool("unicode"))) mode := p.OptionString("map_padding") if mode == "none" || mode == "x" { @@ -30,7 +30,7 @@ func (g *Game) doMap(sess *net.Session) { } if len(lines) == 0 { - sess.WriteLine("\nNo map data to display.") + sess.WriteLine("No map data to display.") return } sess.WriteLine("") diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go index a5f1690..85abbdf 100644 --- a/internal/game/cmd_move.go +++ b/internal/game/cmd_move.go @@ -148,7 +148,7 @@ func (g *Game) completeMove(sess *net.Session, p *player.Player) { } } - sess.WriteLine(fmt.Sprintf("\nYou walk %s.", g.colorize(sess, "direction", exitDir))) + sess.WriteLine(fmt.Sprintf("You walk %s.", g.colorize(sess, "direction", exitDir))) p.ActionState = &ActionState{Type: ActionMoving, Direction: exitDir} if p.OptionBool("description") { g.doLook(sess) diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go index ff7725c..a5bf11c 100644 --- a/internal/game/cmd_option.go +++ b/internal/game/cmd_option.go @@ -45,14 +45,14 @@ func (g *Game) doOption(sess *net.Session, input string) { name := strings.ToLower(parts[0]) def := player.GetOptionDef(name) if def == nil { - sess.WriteLine(fmt.Sprintf("\nUnknown option: %s", name)) + sess.WriteLine(fmt.Sprintf("Unknown option: %s", name)) return } if len(parts) == 1 { val := formatOptionValue(p, def) valid := formatValidValues(def) - sess.WriteLine(fmt.Sprintf("\n%s = %s [%s]", def.Name, val, valid)) + sess.WriteLine(fmt.Sprintf("%s = %s [%s]", def.Name, val, valid)) sess.WriteLine(fmt.Sprintf(" %s", def.Description)) return } @@ -61,7 +61,7 @@ func (g *Game) doOption(sess *net.Session, input string) { parsed, ok := parseOptionValue(def, value) if !ok { valid := formatValidValues(def) - sess.WriteLine(fmt.Sprintf("\nInvalid value for %s: %s [%s]", def.Name, value, valid)) + sess.WriteLine(fmt.Sprintf("Invalid value for %s: %s [%s]", def.Name, value, valid)) return } diff --git a/internal/game/cmd_prompt.go b/internal/game/cmd_prompt.go index d91556a..110d08c 100644 --- a/internal/game/cmd_prompt.go +++ b/internal/game/cmd_prompt.go @@ -28,7 +28,7 @@ func (g *Game) doPrompt(sess *net.Session, input string) { if prompt == "" { prompt = "> " } - sess.WriteLine(fmt.Sprintf("\nPrompt: %s", prompt)) + sess.WriteLine(fmt.Sprintf("Prompt: %s", prompt)) sess.WriteLine("Use 'prompt <value>' to change. Use 'prompt none' for blank. Use 'prompt reset' to restore default.") return } @@ -36,18 +36,18 @@ func (g *Game) doPrompt(sess *net.Session, input string) { if input == "reset" { p.Prompt = "> " g.AccountStore.SaveCharacter(p) - sess.WriteLine("\nPrompt reset to default.") + sess.WriteLine("Prompt reset to default.") return } if input == "none" { - p.Prompt = " " + p.Prompt = "" g.AccountStore.SaveCharacter(p) - sess.WriteLine("\nPrompt set to blank.") + sess.WriteLine("Prompt set to blank.") return } p.Prompt = input g.AccountStore.SaveCharacter(p) - sess.WriteLine(fmt.Sprintf("\nPrompt set to: %s", input)) + sess.WriteLine(fmt.Sprintf("Prompt set to: %s", input)) } diff --git a/internal/game/cmd_queued.go b/internal/game/cmd_queued.go index 58ac74b..303a3ec 100644 --- a/internal/game/cmd_queued.go +++ b/internal/game/cmd_queued.go @@ -18,7 +18,7 @@ func (g *Game) doQueued(sess *net.Session) { activeCmd := g.activeQueue[p.Name] if len(freeCmds) == 0 && activeCmd == nil { - sess.WriteLine("\nNo actions queued.") + sess.WriteLine("No actions queued.") return } diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go index 381f34d..fe3d90a 100644 --- a/internal/game/cmd_registry.go +++ b/internal/game/cmd_registry.go @@ -60,6 +60,7 @@ var commandRegistry = map[string]commandDef{ "prompt": {(*Game).executePrompt, ClassInstant}, "exits": {(*Game).executeExits, ClassInstant}, "map": {(*Game).executeMap, ClassInstant}, + "symbol": {(*Game).executeSymbol, ClassInstant}, "queued": {(*Game).executeQueued, ClassInstant}, "help": {(*Game).executeHelp, ClassInstant}, "mine": {(*Game).executeGather, ClassActive}, diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go index f0e18fe..59cf5ee 100644 --- a/internal/game/cmd_shop.go +++ b/internal/game/cmd_shop.go @@ -108,7 +108,7 @@ func (g *Game) showShopBrowse(sess *net.Session) { } if cfg.Message != "" { - sess.WriteLine(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", cfg.Message))) + sess.WriteLine(fmt.Sprintf("%s", g.colorize(sess, "dialog", cfg.Message))) } if len(cfg.Items) == 0 { diff --git a/internal/game/cmd_style.go b/internal/game/cmd_style.go index 94dd234..043c65f 100644 --- a/internal/game/cmd_style.go +++ b/internal/game/cmd_style.go @@ -13,7 +13,7 @@ func (g *Game) doStyle(sess *net.Session, input string) { styles := []string{"accurate", "aggressive", "defensive", "balanced"} if input == "" { - sess.WriteLine("\nCombat styles:") + sess.WriteLine("Combat styles:") for _, s := range styles { marker := " " if string(p.AttackStyle) == s { @@ -34,14 +34,14 @@ func (g *Game) doStyle(sess *net.Session, input string) { if len(matches) == 1 { p.AttackStyle = player.AttackStyle(matches[0]) g.AccountStore.SaveCharacter(p) - sess.WriteLine(fmt.Sprintf("\nCombat style set to %s.", matches[0])) + sess.WriteLine(fmt.Sprintf("Combat style set to %s.", matches[0])) return } if len(matches) > 1 { - sess.WriteLine(fmt.Sprintf("\nAmbiguous style: %s. Choices: %s", input, strings.Join(matches, ", "))) + sess.WriteLine(fmt.Sprintf("Ambiguous style: %s. Choices: %s", input, strings.Join(matches, ", "))) return } - sess.WriteLine(fmt.Sprintf("\nUnknown style: %s. Choices: accurate, aggressive, defensive, balanced", input)) + sess.WriteLine(fmt.Sprintf("Unknown style: %s. Choices: accurate, aggressive, defensive, balanced", input)) } func (g *Game) executeStyle(sess *net.Session, args []string, rawInput string) { diff --git a/internal/game/cmd_symbol.go b/internal/game/cmd_symbol.go new file mode 100644 index 0000000..ec8d68b --- /dev/null +++ b/internal/game/cmd_symbol.go @@ -0,0 +1,91 @@ +package game + +import ( + "fmt" + "strings" + "unicode/utf8" + + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) executeSymbol(sess *net.Session, args []string, rawInput string) { + p := sess.Player + if len(args) == 0 { + data, ok := p.MapSymbols[p.RoomID] + if !ok { + sess.WriteLine("No custom symbol set for this room.") + return + } + msg := fmt.Sprintf("Custom symbol: %s", data.Char) + if data.Color != "" { + spec := color.Parse(data.Color) + colored := color.Render(g.colorMode(sess), spec, data.Char) + msg = fmt.Sprintf("Custom symbol: %s (%s)", colored, data.Color) + } + sess.WriteLine(msg) + return + } + + // ponytail: extract symbol char from rawInput to preserve case, since + // handleGameCommand lowercases args before dispatch (same pattern as say). + char := args[0] + if idx := strings.Index(strings.ToLower(rawInput), "symbol"); idx >= 0 { + rest := strings.TrimSpace(rawInput[idx+6:]) + if space := strings.Index(rest, " "); space >= 0 { + char = rest[:space] + } else { + char = rest + } + } + + cleared := false + if strings.EqualFold(char, "clear") || strings.EqualFold(char, "remove") { + delete(p.MapSymbols, p.RoomID) + if err := g.AccountStore.SaveCharacter(p); err != nil { + sess.WriteLine("Error saving: " + err.Error()) + return + } + sess.WriteLine("Custom symbol cleared for this room.") + cleared = true + } + + if !cleared { + r, size := utf8.DecodeRuneInString(char) + if size == 0 || size != len(char) || r == utf8.RuneError { + sess.WriteLine("Usage: symbol <single character> [color...]") + return + } + + colorSpec := "" + if len(args) > 1 { + colorSpec = strings.Join(args[1:], " ") + if colorSpec != "" { + spec := color.Parse(colorSpec) + if spec.Empty() { + sess.WriteLine("Invalid color spec. Use a color number like 232 and/or bold/dim/underline.") + return + } + } + } + + p.MapSymbols[p.RoomID] = player.MapSymbolData{ + Char: char, + Color: colorSpec, + } + + if err := g.AccountStore.SaveCharacter(p); err != nil { + sess.WriteLine("Error saving: " + err.Error()) + return + } + + msg := fmt.Sprintf("Map symbol set to: %s", char) + if colorSpec != "" { + spec := color.Parse(colorSpec) + colored := color.Render(g.colorMode(sess), spec, char) + msg = fmt.Sprintf("Map symbol set to: %s", colored) + } + sess.WriteLine(msg) + } +} diff --git a/internal/game/cmd_tech.go b/internal/game/cmd_tech.go index cb4c309..d4a6adc 100644 --- a/internal/game/cmd_tech.go +++ b/internal/game/cmd_tech.go @@ -14,7 +14,7 @@ func (g *Game) doTech(sess *net.Session, input string) { techLevel := p.Level(player.Technology) if techLevel < 1 { - sess.WriteLine("\nYou need at least level 1 Technology to use tech.") + sess.WriteLine("You need at least level 1 Technology to use tech.") return } @@ -44,7 +44,7 @@ func (g *Game) doTech(sess *net.Session, input string) { matches := TechByPrefixMatch(lower) if len(matches) == 0 { - sess.WriteLine(fmt.Sprintf("\nUnknown tech: %s", input)) + sess.WriteLine(fmt.Sprintf("Unknown tech: %s", input)) return } if len(matches) > 1 { @@ -52,7 +52,7 @@ func (g *Game) doTech(sess *net.Session, input string) { for _, m := range matches { names = append(names, m.Name) } - sess.WriteLine(fmt.Sprintf("\nAmbiguous tech: %s. Matches: %s", input, strings.Join(names, ", "))) + sess.WriteLine(fmt.Sprintf("Ambiguous tech: %s. Matches: %s", input, strings.Join(names, ", "))) return } @@ -62,7 +62,7 @@ func (g *Game) doTech(sess *net.Session, input string) { func (g *Game) toggleTech(sess *net.Session, p *player.Player, techID string) { def := GetTechDef(techID) if def == nil { - sess.WriteLine("\nUnknown tech.") + sess.WriteLine("Unknown tech.") return } @@ -70,27 +70,27 @@ func (g *Game) toggleTech(sess *net.Session, p *player.Player, techID string) { if p.HasActiveTech(techID) { p.DeactivateTech(techID) - sess.WriteLine(fmt.Sprintf("\n%s deactivated.", def.Name)) + sess.WriteLine(fmt.Sprintf("%s deactivated.", def.Name)) return } if techLevel < def.Level { - sess.WriteLine(fmt.Sprintf("\nYou need level %d Technology to use %s.", def.Level, def.Name)) + sess.WriteLine(fmt.Sprintf("You need level %d Technology to use %s.", def.Level, def.Name)) return } if p.Battery <= 0 { - sess.WriteLine("\nYour battery is depleted!") + sess.WriteLine("Your battery is depleted!") return } deactivated := g.deactivateGroup(p, def.Group) for _, name := range deactivated { - sess.WriteLine(fmt.Sprintf("\n%s deactivated.", name)) + sess.WriteLine(fmt.Sprintf("%s deactivated.", name)) } p.ActivateTech(techID) - sess.WriteLine(fmt.Sprintf("\n%s activated.", def.Name)) + sess.WriteLine(fmt.Sprintf("%s activated.", def.Name)) } func (g *Game) deactivateGroup(p *player.Player, group string) []string { @@ -164,21 +164,21 @@ func (g *Game) doTechList(sess *net.Session) { func (g *Game) doTechQuick(sess *net.Session, p *player.Player, input string) { if input == "" { if p.QuickTech == "" { - sess.WriteLine("\nNo quick tech set. Usage: tech quick <name>") + sess.WriteLine("No quick tech set. Usage: tech quick <name>") } else { def := GetTechDef(p.QuickTech) name := p.QuickTech if def != nil { name = def.Name } - sess.WriteLine(fmt.Sprintf("\nQuick tech: %s", name)) + sess.WriteLine(fmt.Sprintf("Quick tech: %s", name)) } return } matches := TechByPrefixMatch(strings.ToLower(input)) if len(matches) == 0 { - sess.WriteLine(fmt.Sprintf("\nUnknown tech: %s", input)) + sess.WriteLine(fmt.Sprintf("Unknown tech: %s", input)) return } if len(matches) > 1 { @@ -186,13 +186,13 @@ func (g *Game) doTechQuick(sess *net.Session, p *player.Player, input string) { for _, m := range matches { names = append(names, m.Name) } - sess.WriteLine(fmt.Sprintf("\nAmbiguous tech: %s. Matches: %s", input, strings.Join(names, ", "))) + sess.WriteLine(fmt.Sprintf("Ambiguous tech: %s. Matches: %s", input, strings.Join(names, ", "))) return } p.QuickTech = matches[0].ID g.AccountStore.SaveCharacter(p) - sess.WriteLine(fmt.Sprintf("\nQuick tech set to %s.", matches[0].Name)) + sess.WriteLine(fmt.Sprintf("Quick tech set to %s.", matches[0].Name)) } func (g *Game) executeTech(sess *net.Session, args []string, rawInput string) { diff --git a/internal/game/cmd_trigger_transport.go b/internal/game/cmd_trigger_transport.go index b745232..d3f7368 100644 --- a/internal/game/cmd_trigger_transport.go +++ b/internal/game/cmd_trigger_transport.go @@ -28,7 +28,7 @@ func (g *Game) triggerTransport(sess *net.Session, p *player.Player, mod *ModDef } } - sess.WriteLine(fmt.Sprintf("\nYou activate %s...", mod.Name)) + sess.WriteLine(fmt.Sprintf("You activate %s...", mod.Name)) p.RoomID = mod.Destination p.Stats.RecordRoomVisit(mod.Destination) diff --git a/internal/game/core_hacking.go b/internal/game/core_hacking.go index 3457728..b270593 100644 --- a/internal/game/core_hacking.go +++ b/internal/game/core_hacking.go @@ -57,15 +57,15 @@ func (g *Game) endHacking(sess *net.Session, p *player.Player, completed bool, w if completed && won { xp = hacking.CalcXP(tDef.BaseXPWin, hs.Level, hs.ReqLevel) - sess.WriteLine("\nConnection terminated. Contract complete.") + sess.WriteLine("Connection terminated. Contract complete.") } else if completed { xp = hacking.CalcXP(tDef.BaseXPLose, hs.Level, hs.ReqLevel) - sess.WriteLine("\nConnection lost.") + sess.WriteLine("Connection lost.") } else if hs.Started { xp = hacking.CalcXP(tDef.BaseXPLose, hs.Level, hs.ReqLevel) - sess.WriteLine("\nYou jack out of the terminal.") + sess.WriteLine("You jack out of the terminal.") } else { - sess.WriteLine("\nYou jack out of the terminal.") + sess.WriteLine("You jack out of the terminal.") } if won { diff --git a/internal/game/game.go b/internal/game/game.go index d3bb391..a1b7bbe 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -235,7 +235,7 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { Timestamp: time.Now(), }) if !p.OptionBool("queue_silently") { - sess.WriteLine(fmt.Sprintf("\nYou prepare to %s.", cmd)) + sess.WriteLine(fmt.Sprintf("You prepare to %s.", cmd)) } return } @@ -254,7 +254,7 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { } g.cancelRest(p.Name) if !p.OptionBool("queue_silently") { - sess.WriteLine(fmt.Sprintf("\nYou prepare to %s.", cmd)) + sess.WriteLine(fmt.Sprintf("You prepare to %s.", cmd)) } } @@ -340,7 +340,9 @@ func (g *Game) ProcessQueuedCommands() { if len(parts) > 0 { g.executeCommand(qc.Session, parts[0], parts[1:], raw) } - isBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil || p.MoveTicks > 0 + as, hasAction := p.ActionState.(*ActionState) + isHiding := hasAction && as.Type == ActionHiding + isBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil || p.MoveTicks > 0 || isHiding _, isResting := g.restTimers[p.Name] if !isResting && !isBusy && qc.Session.State == net.StateGame { g.writePrompt(qc.Session) diff --git a/internal/game/map_test.go b/internal/game/map_test.go index a695c1b..b7d9f01 100644 --- a/internal/game/map_test.go +++ b/internal/game/map_test.go @@ -4,6 +4,7 @@ import ( "strings" "testing" + "thehouseoficarus/internal/color" "thehouseoficarus/internal/world" ) @@ -51,7 +52,7 @@ func TestBuildTinyMap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - lines := buildTinyMap(g, tt.roomID, mg) + lines := buildTinyMap(g, nil, tt.roomID, mg) if len(lines) != 7 { t.Fatalf("expected 7 lines, got %d", len(lines)) } @@ -65,8 +66,8 @@ func TestBuildTinyMap(t *testing.T) { if !strings.HasPrefix(lines[i], "║") || !strings.HasSuffix(lines[i], "║") { t.Errorf("line %d: should have ║ borders, got %s", i, lines[i]) } - if len([]rune(lines[i])) != 7 { - t.Errorf("line %d: expected 7 runes, got %d in %q", i, len([]rune(lines[i])), lines[i]) + if color.VisibleLen(lines[i]) != 7 { + t.Errorf("line %d: expected 7 visible runes, got %d in %q", i, color.VisibleLen(lines[i]), lines[i]) } } t.Logf("Room %d map:\n%s", tt.roomID, strings.Join(lines, "\n")) @@ -115,13 +116,13 @@ func TestBuildFullMap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - lines := buildFullMap(g, tt.roomID, tt.width, tt.height, mg) + lines := buildFullMap(g, nil, tt.roomID, tt.width, tt.height, mg) if len(lines) != tt.height { t.Errorf("expected %d lines, got %d", tt.height, len(lines)) } for i, line := range lines { - if len([]rune(line)) != tt.width { - t.Errorf("line %d: expected %d runes, got %d in %q", i, tt.width, len([]rune(line)), line) + if color.VisibleLen(line) != tt.width { + t.Errorf("line %d: expected %d visible runes, got %d in %q", i, tt.width, color.VisibleLen(line), line) } } t.Logf("Room %d %dx%d map:\n%s", tt.roomID, tt.width, tt.height, strings.Join(lines, "\n")) diff --git a/internal/game/sys_safespot.go b/internal/game/sys_safespot.go index b0e94c2..da999c0 100644 --- a/internal/game/sys_safespot.go +++ b/internal/game/sys_safespot.go @@ -121,6 +121,7 @@ func (g *Game) activateSafespot(sess *net.Session, p *player.Player, ss *Safespo msg = fmt.Sprintf("You crouch behind the %s, using it as cover.", objDef.Name) } sess.WriteLine(g.colorize(sess, "combat_info", msg)) + g.writePrompt(sess) } func (g *Game) safespotMaintenance(sess *net.Session, p *player.Player, ss *SafespotState) { @@ -150,11 +151,7 @@ func (g *Game) safespotMaintenance(sess *net.Session, p *player.Player, ss *Safe } if cfg.UnsafeChance > 0 && rand.Float64() < cfg.UnsafeChance { - alert := p.OptionString("safespot_alert") g.forceLeaveSafespot(sess, p, ss, "You slip and are no longer covered!") - if alert != "" { - sess.WriteLine(color.ExpandTags(g.colorMode(sess), alert)) - } return } @@ -283,6 +280,10 @@ func (g *Game) forceLeaveSafespot(sess *net.Session, p *player.Player, ss *Safes if reason != "" { sess.WriteLine(reason) + alert := p.OptionString("safespot_alert") + if alert != "" { + sess.WriteLine(color.ExpandTags(g.colorMode(sess), alert)) + } } p.ActionState = nil } diff --git a/internal/game/sys_technology.go b/internal/game/sys_technology.go index a24c66d..bb48375 100644 --- a/internal/game/sys_technology.go +++ b/internal/game/sys_technology.go @@ -202,14 +202,14 @@ func (g *Game) tryRecharge(sess *net.Session, p *player.Player, input string) bo instances := g.World.FindObjInstances(p.RoomID, lower) for _, st := range instances { if st.DefID == "charging_station" || st.DefID == "power_conduit" { - if p.Battery >= p.MaxBattery() { - sess.WriteLine("\nYour battery is already full.") - } else { - p.Battery = p.MaxBattery() - g.AccountStore.SaveCharacter(p) - sess.WriteLine(g.colorize(sess, "battery", - "\nYou connect to the charging station. Your battery is fully recharged.")) - } + if p.Battery >= p.MaxBattery() { + sess.WriteLine("Your battery is already full.") + } else { + p.Battery = p.MaxBattery() + g.AccountStore.SaveCharacter(p) + sess.WriteLine(g.colorize(sess, "battery", + "You connect to the charging station. Your battery is fully recharged.")) + } return true } } diff --git a/internal/game/ui_help.go b/internal/game/ui_help.go index ee8ad97..d482fce 100644 --- a/internal/game/ui_help.go +++ b/internal/game/ui_help.go @@ -70,6 +70,7 @@ var commandList = []cmdEntry{ {"steal", "Active", "Steal from mobs (Thieving)"}, {"stoke", "Active", "Add logs to a fire"}, {"style", "Instant", "Change combat style"}, + {"symbol", "Instant", "Set custom map symbol for current room"}, {"talk / speak / ask", "Active", "Talk to mobs"}, {"tech", "Instant", "Toggle technology abilities"}, {"trigger", "Active", "Trigger a science module"}, diff --git a/internal/game/ui_map.go b/internal/game/ui_map.go index 32d3d7c..a07606f 100644 --- a/internal/game/ui_map.go +++ b/internal/game/ui_map.go @@ -2,7 +2,10 @@ package game import ( "strings" + "unicode/utf8" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" "thehouseoficarus/internal/world" ) @@ -19,7 +22,7 @@ func mapGlyphsForPlayer(unicode bool) mapGlyphs { if unicode { return mapGlyphs{ topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝', - side: '║', topFill: '═', connectorH: '─', connectorV: '│', + side: '║', topFill: '═', connectorH: '-', connectorV: '│', upArrow: '↑', downArrow: '↓', } } @@ -30,6 +33,11 @@ func mapGlyphsForPlayer(unicode bool) mapGlyphs { } } +type mapCell struct { + char rune + spec color.ColorSpec +} + type mapGraph struct { posToRoom map[[2]int]int roomToPos map[int][2]int @@ -45,7 +53,7 @@ var bfsDirs = []struct { {world.West, -1, 0}, } -func buildGraph(g *Game, startRoomID int) *mapGraph { +func buildGraph(g *Game, startRoomID int, visited map[int]bool) *mapGraph { mg := &mapGraph{ posToRoom: make(map[[2]int]int), roomToPos: make(map[int][2]int), @@ -68,15 +76,19 @@ func buildGraph(g *Game, startRoomID int) *mapGraph { continue } + if visited != nil && !visited[n.roomID] { + continue + } + for _, d := range bfsDirs { targetID, ok := exitTarget(room, d.dir) if !ok { continue } - if _, visited := mg.roomToPos[targetID]; visited { + if _, seen := mg.roomToPos[targetID]; seen { continue } - nx, ny := n.x + d.dx, n.y + d.dy + nx, ny := n.x+d.dx, n.y+d.dy mg.posToRoom[[2]int{nx, ny}] = targetID mg.roomToPos[targetID] = [2]int{nx, ny} queue = append(queue, node{targetID, nx, ny}) @@ -86,14 +98,43 @@ func buildGraph(g *Game, startRoomID int) *mapGraph { return mg } -func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string { - bg := buildGraph(g, roomID) +func renderMapCells(grid [][]mapCell, colorMode string, startRow, endRow int, border rune) []string { + lines := make([]string, 0, endRow-startRow) + for row := startRow; row < endRow; row++ { + var sb strings.Builder + if border != 0 { + sb.WriteRune(border) + } + for _, cell := range grid[row] { + if cell.char == ' ' { + sb.WriteRune(' ') + } else if !cell.spec.Empty() { + sb.WriteString(color.Render(colorMode, cell.spec, string(cell.char))) + } else { + sb.WriteRune(cell.char) + } + } + if border != 0 { + sb.WriteRune(border) + } + lines = append(lines, sb.String()) + } + return lines +} + +func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string { + visited := roomsVisited(sess) + bg := buildGraph(g, roomID, visited) - grid := make([][]rune, 5) + colorMode := colorModeFor(sess) + atSpec := resolveMapAt(g, sess) + dimSpec := resolveDim(g, sess) + + grid := make([][]mapCell, 5) for i := range grid { - grid[i] = make([]rune, 5) + grid[i] = make([]mapCell, 5) for j := range grid[i] { - grid[i][j] = ' ' + grid[i][j] = mapCell{char: ' '} } } @@ -107,9 +148,14 @@ func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string { gr := (y + 1) * 2 gc := (x + 1) * 2 if rid == roomID { - grid[gr][gc] = '@' + grid[gr][gc] = mapCell{char: '@', spec: atSpec} } else { - grid[gr][gc] = roomMapSymbol(g, rid) + unvisited := visited != nil && !visited[rid] + ch, spec := roomMapSymbol(g, sess, rid, unvisited) + if unvisited { + spec = dimSpec + } + grid[gr][gc] = mapCell{char: ch, spec: spec} } } } @@ -124,7 +170,11 @@ func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string { continue } if exitsConnect(g, leftRoom, rightRoom, world.East, world.West) { - grid[(y+1)*2][(x+1)*2+1] = mg.connectorH + connSpec := color.NoColor() + if visited != nil && (!visited[leftRoom] || !visited[rightRoom]) { + connSpec = dimSpec + } + grid[(y+1)*2][(x+1)*2+1] = mapCell{char: mg.connectorH, spec: connSpec} } } } @@ -139,7 +189,11 @@ func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string { continue } if exitsConnect(g, topRoom, bottomRoom, world.South, world.North) { - grid[(y+1)*2+1][(x+1)*2] = mg.connectorV + connSpec := color.NoColor() + if visited != nil && (!visited[topRoom] || !visited[bottomRoom]) { + connSpec = dimSpec + } + grid[(y+1)*2+1][(x+1)*2] = mapCell{char: mg.connectorV, spec: connSpec} } } } @@ -147,25 +201,152 @@ func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string { cur, _ := loadRoom(g, roomID) if cur != nil { if _, ok := exitTarget(cur, world.Up); ok { - grid[1][3] = mg.upArrow + grid[1][3] = mapCell{char: mg.upArrow, spec: color.NoColor()} } if _, ok := exitTarget(cur, world.Down); ok { - grid[3][1] = mg.downArrow + grid[3][1] = mapCell{char: mg.downArrow, spec: color.NoColor()} } } topFill := strings.Repeat(string(mg.topFill), 5) lines := make([]string, 7) lines[0] = string(mg.topLeft) + topFill + string(mg.topRight) - for row := 0; row < 5; row++ { - lines[row+1] = string(mg.side) + string(grid[row]) + string(mg.side) - } + inner := renderMapCells(grid, colorMode, 0, 5, mg.side) + copy(lines[1:], inner) botFill := strings.Repeat(string(mg.topFill), 5) lines[6] = string(mg.bottomLeft) + botFill + string(mg.bottomRight) return lines } +func buildFullMap(g *Game, sess *net.Session, roomID, mapWidth, mapHeight int, mg mapGlyphs) []string { + visited := roomsVisited(sess) + bg := buildGraph(g, roomID, visited) + + colorMode := colorModeFor(sess) + atSpec := resolveMapAt(g, sess) + dimSpec := resolveDim(g, sess) + + grid := make([][]mapCell, mapHeight) + for i := range grid { + grid[i] = make([]mapCell, mapWidth) + for j := range grid[i] { + grid[i][j] = mapCell{char: ' '} + } + } + + cx := mapWidth / 2 + cy := mapHeight / 2 + + for pos, rid := range bg.posToRoom { + gr := cy + pos[1]*2 + gc := cx + pos[0]*2 + if gr < 0 || gr >= mapHeight || gc < 0 || gc >= mapWidth { + continue + } + if rid == roomID { + grid[gr][gc] = mapCell{char: '@', spec: atSpec} + } else { + unvisited := visited != nil && !visited[rid] + ch, spec := roomMapSymbol(g, sess, rid, unvisited) + if unvisited { + spec = dimSpec + } + grid[gr][gc] = mapCell{char: ch, spec: spec} + } + } + + for pos, rid := range bg.posToRoom { + x, y := pos[0], pos[1] + + if rightID, ok := bg.posToRoom[[2]int{x + 1, y}]; ok { + if exitsConnect(g, rid, rightID, world.East, world.West) { + gr := cy + y*2 + gc := cx + x*2 + 1 + if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { + connSpec := color.NoColor() + if visited != nil && (!visited[rid] || !visited[rightID]) { + connSpec = dimSpec + } + grid[gr][gc] = mapCell{char: mg.connectorH, spec: connSpec} + } + } + } + + if bottomID, ok := bg.posToRoom[[2]int{x, y + 1}]; ok { + if exitsConnect(g, rid, bottomID, world.South, world.North) { + gr := cy + y*2 + 1 + gc := cx + x*2 + if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { + connSpec := color.NoColor() + if visited != nil && (!visited[rid] || !visited[bottomID]) { + connSpec = dimSpec + } + grid[gr][gc] = mapCell{char: mg.connectorV, spec: connSpec} + } + } + } + } + + return renderMapCells(grid, colorMode, 0, mapHeight, 0) +} + +func roomsVisited(sess *net.Session) map[int]bool { + if sess != nil && sess.Player != nil { + return sess.Player.Stats.RoomsVisited + } + return nil +} + +func colorModeFor(sess *net.Session) string { + if sess != nil && sess.Player != nil { + return sess.Player.OptionString("color") + } + return "none" +} + +func resolveMapAt(g *Game, sess *net.Session) color.ColorSpec { + if sess != nil { + return g.resolveColor(sess, "map_at") + } + return color.NoColor() +} + +func resolveDim(g *Game, sess *net.Session) color.ColorSpec { + if sess != nil { + return g.resolveColor(sess, "dim") + } + return color.Parse("243 dim") +} + +func roomMapSymbol(g *Game, sess *net.Session, roomID int, unvisited bool) (rune, color.ColorSpec) { + if sess != nil && sess.Player != nil { + if data, ok := sess.Player.MapSymbols[roomID]; ok { + r, size := utf8.DecodeRuneInString(data.Char) + if size > 0 && r != utf8.RuneError { + spec := color.NoColor() + if data.Color != "" { + spec = color.Parse(data.Color) + } + // ponytail: non-ASCII custom symbols fall back to 'o' + // when unicode mode is off, but preserve the color. + if !sess.Player.OptionBool("unicode") && r > 127 { + return 'o', spec + } + return r, spec + } + } + if sess.Player.OptionBool("unicode") { + if unvisited { + return '□', color.NoColor() + } + return '■', color.NoColor() + } + return 'o', color.NoColor() + } + return '■', color.NoColor() +} + func exitsConnect(g *Game, room1, room2 int, dir12, dir21 world.ExitDir) bool { r1, ok := loadRoom(g, room1) if !ok { @@ -204,18 +385,6 @@ func loadRoom(g *Game, roomID int) (*world.Room, bool) { return room, true } -func roomMapSymbol(g *Game, roomID int) rune { - room, ok := loadRoom(g, roomID) - if !ok { - return '?' - } - if room.MapSymbol != "" { - runes := []rune(room.MapSymbol) - return runes[0] - } - return 'o' -} - func stripBlankRows(lines []string) []string { var out []string for _, line := range lines { @@ -257,61 +426,3 @@ func leftTrimCommon(lines []string) []string { } return result } - -func buildFullMap(g *Game, roomID, mapWidth, mapHeight int, mg mapGlyphs) []string { - bg := buildGraph(g, roomID) - - grid := make([][]rune, mapHeight) - for i := range grid { - grid[i] = make([]rune, mapWidth) - for j := range grid[i] { - grid[i][j] = ' ' - } - } - - cx := mapWidth / 2 - cy := mapHeight / 2 - - for pos, rid := range bg.posToRoom { - gr := cy + pos[1]*2 - gc := cx + pos[0]*2 - if gr < 0 || gr >= mapHeight || gc < 0 || gc >= mapWidth { - continue - } - if rid == roomID { - grid[gr][gc] = '@' - } else { - grid[gr][gc] = roomMapSymbol(g, rid) - } - } - - for pos, rid := range bg.posToRoom { - x, y := pos[0], pos[1] - - if rightID, ok := bg.posToRoom[[2]int{x + 1, y}]; ok { - if exitsConnect(g, rid, rightID, world.East, world.West) { - gr := cy + y*2 - gc := cx + x*2 + 1 - if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { - grid[gr][gc] = mg.connectorH - } - } - } - - if bottomID, ok := bg.posToRoom[[2]int{x, y + 1}]; ok { - if exitsConnect(g, rid, bottomID, world.South, world.North) { - gr := cy + y*2 + 1 - gc := cx + x*2 - if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { - grid[gr][gc] = mg.connectorV - } - } - } - } - - lines := make([]string, mapHeight) - for i := range grid { - lines[i] = string(grid[i]) - } - return lines -} diff --git a/internal/game/ui_prompt.go b/internal/game/ui_prompt.go index 2ecf367..6263ac2 100644 --- a/internal/game/ui_prompt.go +++ b/internal/game/ui_prompt.go @@ -13,9 +13,7 @@ import ( func (g *Game) promptStr(sess *net.Session) string { prompt := "> " if p := sess.Player; p != nil { - if p.Prompt != "" { - prompt = p.Prompt - } + prompt = p.Prompt } prompt = g.expandPromptColors(sess, prompt) diff --git a/internal/player/player.go b/internal/player/player.go index ac2c8ce..ebba376 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -138,7 +138,7 @@ var OptionDefs = []OptionDef{ {"mix_all", OptBool, false, nil, "Auto-start mixing when only one product is possible"}, {"construct_all", OptBool, false, nil, "Auto-start constructing when only one product is possible"}, {"craft_all", OptBool, false, nil, "Auto-start crafting when only one product is possible"}, - {"safespot_alert", OptString, "{196 bold}** Your safespot has been compromised! **{/}", nil, "Message shown when forced out of a safespot (set to \"none\" to disable)"}, + {"safespot_alert", OptString, "{196 bold}** Your safespot has been compromised! **{/}", nil, "Message shown when forced out of a safespot"}, } var optionByName map[string]*OptionDef @@ -191,9 +191,15 @@ type Player struct { Sneaking bool `yaml:"-"` SneakNotified map[string]bool `yaml:"-"` ActiveBuffs []PotionBuff `yaml:"-"` + MapSymbols map[int]MapSymbolData `yaml:"map_symbols,omitempty"` Stats PlayerStats `yaml:"stats"` } +type MapSymbolData struct { + Char string `yaml:"char"` + Color string `yaml:"color,omitempty"` +} + type PotionBuff struct { Stat string BonusPercent int @@ -314,6 +320,7 @@ func New(name string) *Player { AttackStyle: Accurate, Prompt: "> ", RoomID: 0, + MapSymbols: make(map[int]MapSymbolData), Stats: PlayerStats{ RoomsVisited: make(map[int]bool), MobKills: make(map[string]int), diff --git a/internal/player/store.go b/internal/player/store.go index 04af594..97ff13f 100644 --- a/internal/player/store.go +++ b/internal/player/store.go @@ -95,6 +95,9 @@ func (s *AccountStore) LoadCharacter(name string) (*Player, error) { if p.Inventory == nil { p.Inventory = make(map[int]*InventorySlot) } + if p.MapSymbols == nil { + p.MapSymbols = make(map[int]MapSymbolData) + } return &p, nil } diff --git a/internal/world/room.go b/internal/world/room.go index 9226a16..752ec41 100644 --- a/internal/world/room.go +++ b/internal/world/room.go @@ -67,7 +67,6 @@ type Room struct { ID int `yaml:"id"` Name string `yaml:"name"` Description string `yaml:"description"` - MapSymbol string `yaml:"map_symbol"` Exits map[ExitDir]ExitDef `yaml:"exits"` Objects []RoomObject `yaml:"objects"` ItemSpawns []SpawnDef `yaml:"item_spawns"` diff --git a/worldbuilding_guide/objects.md b/worldbuilding_guide/objects.md index 0350a37..427968a 100644 --- a/worldbuilding_guide/objects.md +++ b/worldbuilding_guide/objects.md @@ -386,8 +386,6 @@ player flags via talk node actions: Players can customize the message shown when forced out of a safespot: option safespot_alert "{196 bold}** DANGER **{/} Cover blown!" - option safespot_alert none - Default: `"{196 bold}** Your safespot has been compromised! **{/}"` --- |
