aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-22 04:24:11 -0400
committerhistoria <[not public]>2026-06-22 04:24:11 -0400
commit5b9a75a1520a198c9c6b1f1f55e05c9f4aab5629 (patch)
tree18f500ca3492ba206a3f66e2f01146b05a1675b2
parent7f07c7236578e1a7bcef0282ff9d7f5bf8c7c045 (diff)
downloadthehouseoficarus-5b9a75a1520a198c9c6b1f1f55e05c9f4aab5629.tar.gz
feat: map now only reveals with exploration. players can set custom map symbols.
-rw-r--r--data/help/safespot.yaml6
-rw-r--r--data/help/symbol.yaml21
-rw-r--r--data/mobs/ancient_construct.yaml1
-rw-r--r--data/mobs/bandit.yaml1
-rw-r--r--data/mobs/bioengineer.yaml1
-rw-r--r--data/mobs/client.yaml1
-rw-r--r--data/mobs/corrupted_scientist.yaml1
-rw-r--r--data/mobs/cow.yaml1
-rw-r--r--data/mobs/crawler.yaml1
-rw-r--r--data/mobs/drone.yaml1
-rw-r--r--data/mobs/elite_synth.yaml1
-rw-r--r--data/mobs/enforcer_bot.yaml1
-rw-r--r--data/mobs/estate_broker.yaml1
-rw-r--r--data/mobs/farmer.yaml1
-rw-r--r--data/mobs/feral_cyborg.yaml1
-rw-r--r--data/mobs/general_store_clerk.yaml1
-rw-r--r--data/mobs/ghoul.yaml1
-rw-r--r--data/mobs/goblin.yaml1
-rw-r--r--data/mobs/greater_drone.yaml1
-rw-r--r--data/mobs/guard.yaml1
-rw-r--r--data/mobs/heavy_mech.yaml1
-rw-r--r--data/mobs/iron_sentinel.yaml1
-rw-r--r--data/mobs/lesser_drone.yaml1
-rw-r--r--data/mobs/man.yaml1
-rw-r--r--data/mobs/molten_elemental.yaml1
-rw-r--r--data/mobs/moss_giant.yaml1
-rw-r--r--data/mobs/mutant_rat.yaml1
-rw-r--r--data/mobs/newbie_trainer.yaml1
-rw-r--r--data/mobs/phantom.yaml1
-rw-r--r--data/mobs/plasma_wraith.yaml1
-rw-r--r--data/mobs/psionic_entity.yaml1
-rw-r--r--data/mobs/rat.yaml1
-rw-r--r--data/mobs/sawmill_operator.yaml1
-rw-r--r--data/mobs/scrap_bot.yaml1
-rw-r--r--data/mobs/security_turret.yaml1
-rw-r--r--data/mobs/skeleton.yaml1
-rw-r--r--data/mobs/slug.yaml1
-rw-r--r--data/mobs/tanner.yaml1
-rw-r--r--data/mobs/titan_sentinel.yaml1
-rw-r--r--data/mobs/veteran_merc.yaml1
-rw-r--r--data/mobs/void_reaper.yaml1
-rw-r--r--data/mobs/war_drone.yaml1
-rw-r--r--data/mobs/weapons_smith.yaml1
-rw-r--r--data/mobs/zombie.yaml1
-rw-r--r--data/rooms/town/1.yaml1
-rw-r--r--data/rooms/town/2.yaml1
-rw-r--r--data/rooms/town/21.yaml1
-rw-r--r--data/rooms/town/22.yaml1
-rw-r--r--data/rooms/town/23.yaml1
-rw-r--r--data/rooms/town/24.yaml1
-rw-r--r--data/rooms/town/25.yaml1
-rw-r--r--data/rooms/town/26.yaml1
-rw-r--r--data/rooms/town/27.yaml1
-rw-r--r--data/rooms/town/28.yaml1
-rw-r--r--data/rooms/town/3.yaml1
-rw-r--r--data/rooms/town/30.yaml1
-rw-r--r--data/rooms/town/31.yaml1
-rw-r--r--data/rooms/town/4.yaml1
-rw-r--r--data/rooms/town/5.yaml1
-rw-r--r--data/rooms/town/6.yaml1
-rw-r--r--data/rooms/wilderness/150.yaml1
-rw-r--r--data/rooms/wilderness/151.yaml1
-rw-r--r--data/rooms/wilderness/152.yaml1
-rw-r--r--data/rooms/wilderness/160.yaml1
-rw-r--r--data/rooms/wilderness/161.yaml1
-rw-r--r--data/rooms/wilderness/162.yaml1
-rw-r--r--internal/config/config.go1
-rw-r--r--internal/game/action_farm.go12
-rw-r--r--internal/game/action_finishing_blow.go2
-rw-r--r--internal/game/action_gather.go2
-rw-r--r--internal/game/action_room.go2
-rw-r--r--internal/game/action_talk.go2
-rw-r--r--internal/game/action_use.go2
-rw-r--r--internal/game/cmd_alias.go10
-rw-r--r--internal/game/cmd_attack.go4
-rw-r--r--internal/game/cmd_bank.go2
-rw-r--r--internal/game/cmd_color.go14
-rw-r--r--internal/game/cmd_colortable.go4
-rw-r--r--internal/game/cmd_consume.go12
-rw-r--r--internal/game/cmd_drop.go6
-rw-r--r--internal/game/cmd_hide.go4
-rw-r--r--internal/game/cmd_look.go579
-rw-r--r--internal/game/cmd_map.go4
-rw-r--r--internal/game/cmd_move.go2
-rw-r--r--internal/game/cmd_option.go6
-rw-r--r--internal/game/cmd_prompt.go10
-rw-r--r--internal/game/cmd_queued.go2
-rw-r--r--internal/game/cmd_registry.go1
-rw-r--r--internal/game/cmd_shop.go2
-rw-r--r--internal/game/cmd_style.go8
-rw-r--r--internal/game/cmd_symbol.go91
-rw-r--r--internal/game/cmd_tech.go28
-rw-r--r--internal/game/cmd_trigger_transport.go2
-rw-r--r--internal/game/core_hacking.go8
-rw-r--r--internal/game/game.go8
-rw-r--r--internal/game/map_test.go13
-rw-r--r--internal/game/sys_safespot.go9
-rw-r--r--internal/game/sys_technology.go16
-rw-r--r--internal/game/ui_help.go1
-rw-r--r--internal/game/ui_map.go287
-rw-r--r--internal/game/ui_prompt.go4
-rw-r--r--internal/player/player.go9
-rw-r--r--internal/player/store.go3
-rw-r--r--internal/world/room.go1
-rw-r--r--worldbuilding_guide/objects.md2
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! **{/}"`
---