aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-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
38 files changed, 703 insertions, 470 deletions
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"`