aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/cmd_aps.go12
-rw-r--r--internal/game/cmd_color.go15
-rw-r--r--internal/game/cmd_colortable.go79
-rw-r--r--internal/game/cmd_mods.go16
-rw-r--r--internal/game/cmd_option.go16
-rw-r--r--internal/game/cmd_score.go8
-rw-r--r--internal/game/cmd_skills.go20
-rw-r--r--internal/game/cmd_symbol.go4
-rw-r--r--internal/game/cmd_tech.go12
-rw-r--r--internal/game/core_login_account.go16
-rw-r--r--internal/game/hacking/mastermind.go12
-rw-r--r--internal/game/hacking/wumpus.go6
-rw-r--r--internal/game/look_entities.go2
-rw-r--r--internal/game/look_room.go26
-rw-r--r--internal/game/look_target.go2
-rw-r--r--internal/game/map_test.go99
-rw-r--r--internal/game/production_menu.go2
-rw-r--r--internal/game/render_color.go10
-rw-r--r--internal/game/render_map.go179
-rw-r--r--internal/game/validate_source.go2
20 files changed, 354 insertions, 184 deletions
diff --git a/internal/game/cmd_aps.go b/internal/game/cmd_aps.go
index 265277a..5e209a6 100644
--- a/internal/game/cmd_aps.go
+++ b/internal/game/cmd_aps.go
@@ -129,11 +129,11 @@ func displayApsNodes(g *Game, sess *net.Session, p *player.Player, known []int)
for i, info := range infos {
distStr := strconv.Itoa(info.Distance)
if info.Distance == 0 {
- distStr = color.Render(mode, color.Parse("82"), "here")
+ distStr = color.Render(mode, color.Parse("52"), "here")
}
rows[i] = []string{
- color.Render(mode, color.Parse("245"), fmt.Sprintf("#%d", info.ID)),
- color.Render(mode, color.Parse("75"), info.Name),
+ color.Render(mode, color.Parse("F5"), fmt.Sprintf("#%d", info.ID)),
+ color.Render(mode, color.Parse("4B"), info.Name),
distStr,
}
}
@@ -141,9 +141,9 @@ func displayApsNodes(g *Game, sess *net.Session, p *player.Player, known []int)
t := &ui.Table{
Title: "APS Datapad",
Columns: []string{
- color.Render(mode, color.Parse("245"), "Room"),
- color.Render(mode, color.Parse("75"), "Name"),
- color.Render(mode, color.Parse("230"), "Distance"),
+ color.Render(mode, color.Parse("F5"), "Room"),
+ color.Render(mode, color.Parse("4B"), "Name"),
+ color.Render(mode, color.Parse("E6"), "Distance"),
},
Rows: rows,
}
diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go
index 33b406d..8e90f02 100644
--- a/internal/game/cmd_color.go
+++ b/internal/game/cmd_color.go
@@ -57,8 +57,8 @@ func (g *Game) doColor(sess *net.Session, input string) {
spec := color.Parse(value)
if spec.Empty() && value != "" {
sess.WriteLine(fmt.Sprintf("Invalid color string: %s", value))
- sess.WriteLine("Format: <0-255> [bg:<0-255>] [bold] [dim] [underline]")
- sess.WriteLine("Example: 208 bold")
+ sess.WriteLine("Format: <00-FF> [bg:<00-FF>] [bold] [dim] [underline]")
+ sess.WriteLine("Example: D0 bold")
return
}
@@ -162,6 +162,9 @@ var colorCategoryOrder = []string{
"eat_food",
"drop_message",
"credits_pickup",
+
+ "map_at",
+ "map_blocked",
}
func showColorTable(g *Game, sess *net.Session) {
@@ -169,9 +172,9 @@ func showColorTable(g *Game, sess *net.Session) {
mode := g.colorMode(sess)
table := &ui.Table{
Columns: []string{
- color.Render(mode, color.Parse("75"), "Target"),
+ color.Render(mode, color.Parse("4B"), "Target"),
"Color",
- color.Render(mode, color.Parse("243"), "Source"),
+ color.Render(mode, color.Parse("F3"), "Source"),
},
}
for _, cat := range colorCategoryOrder {
@@ -185,9 +188,9 @@ func showColorTable(g *Game, sess *net.Session) {
}
}
table.Rows = append(table.Rows, []string{
- color.Render(mode, color.Parse("75"), cat),
+ color.Render(mode, color.Parse("4B"), cat),
coloredVal,
- color.Render(mode, color.Parse("243"), source),
+ color.Render(mode, color.Parse("F3"), source),
})
}
for _, line := range table.Render(p.OptionBool("unicode")) {
diff --git a/internal/game/cmd_colortable.go b/internal/game/cmd_colortable.go
index baa30e8..d228a34 100644
--- a/internal/game/cmd_colortable.go
+++ b/internal/game/cmd_colortable.go
@@ -15,44 +15,18 @@ func (g *Game) doColortable(sess *net.Session) {
return
}
- p := sess.Player
- unicode := p.OptionBool("unicode")
-
- sess.WriteLine(fmt.Sprintf("Color mode: %s", mode))
-
- sess.WriteLine("")
- writeSectionHeader(sess, "Foreground Colors", unicode)
writeColorBlock(sess, mode, true)
-
sess.WriteLine("")
- writeSectionHeader(sess, "Background Colors", unicode)
writeColorBlock(sess, mode, false)
-
- sess.WriteLine("")
- writeSectionHeader(sess, "Combinations", unicode)
- writeCombinations(sess, mode)
-
- sess.WriteLine("")
- writeSectionHeader(sess, "ANSI Mode (16 colors)", unicode)
- writeANSITable(sess, mode)
}
func (g *Game) executeColortable(sess *net.Session, args []string, rawInput string) {
g.doColortable(sess)
}
-func writeSectionHeader(sess *net.Session, title string, unicode bool) {
- if unicode {
- sess.WriteLine(fmt.Sprintf("── %s ──", title))
- } else {
- sess.WriteLine(fmt.Sprintf("-- %s --", title))
- }
-}
-
func writeColorBlock(sess *net.Session, mode string, fg bool) {
- sess.WriteLine("Standard:")
+ sess.WriteLine("ANSI 16 Colors:")
writeColorRow(sess, mode, 0, 8, fg)
- sess.WriteLine("Bright:")
writeColorRow(sess, mode, 8, 16, fg)
sess.WriteLine("Color cube:")
@@ -72,7 +46,7 @@ func writeColorBlock(sess *net.Session, mode string, fg bool) {
sb.WriteString(color.ContrastFg(mode, idx))
sb.WriteString(color.BgCode(mode, idx))
}
- sb.WriteString(fmt.Sprintf("%4d", idx))
+ sb.WriteString(fmt.Sprintf(" %02X", idx))
sb.WriteString(color.Reset)
}
if block < 2 {
@@ -100,55 +74,8 @@ func writeColorRow(sess *net.Session, mode string, start, end int, fg bool) {
sb.WriteString(color.ContrastFg(mode, i))
sb.WriteString(color.BgCode(mode, i))
}
- sb.WriteString(fmt.Sprintf("%4d", i))
+ sb.WriteString(fmt.Sprintf(" %02X", i))
sb.WriteString(color.Reset)
}
sess.WriteLine(sb.String())
}
-
-func writeCombinations(sess *net.Session, mode string) {
- type combo struct {
- fg int
- bg int
- desc string
- }
- combos := []combo{
- {15, 0, "white on black"},
- {208, 0, "orange on black"},
- {0, 178, "black on gold"},
- {15, 24, "white on dark blue"},
- {0, 15, "black on white"},
- {15, 88, "white on dark red"},
- {178, 52, "gold on brown"},
- {14, 0, "bright cyan on black"},
- {0, 228, "black on pale yellow"},
- {15, 22, "white on dark green"},
- {9, 0, "bright red on black"},
- {11, 17, "bright yellow on navy"},
- }
- for _, c := range combos {
- spec := color.ColorSpec{Fg: c.fg, Bg: c.bg}
- sample := color.Render(mode, spec, fmt.Sprintf(" %3d on %3d ", c.fg, c.bg))
- sess.WriteLine(fmt.Sprintf(" %s %s", sample, c.desc))
- }
-}
-
-func writeANSITable(sess *net.Session, mode string) {
- names := [16]string{
- "black", "red", "green", "yellow",
- "blue", "magenta", "cyan", "white",
- "brt black", "brt red", "brt green", "brt yellow",
- "brt blue", "brt magenta", "brt cyan", "brt white",
- }
- for row := 0; row < 2; row++ {
- var sb strings.Builder
- for col := 0; col < 8; col++ {
- idx := row*8 + col
- sb.WriteString(color.FgCode(mode, idx))
- sb.WriteString(fmt.Sprintf("%3d", idx))
- sb.WriteString(color.Reset)
- sb.WriteString(fmt.Sprintf(" %-12s", names[idx]))
- }
- sess.WriteLine(sb.String())
- }
-}
diff --git a/internal/game/cmd_mods.go b/internal/game/cmd_mods.go
index 2fe9ae5..4f3f9ff 100644
--- a/internal/game/cmd_mods.go
+++ b/internal/game/cmd_mods.go
@@ -55,10 +55,10 @@ func (g *Game) doMods(sess *net.Session, showAll bool) {
t := &ui.Table{
Title: cat.Name,
Columns: []string{
- color.Render(mode, color.Parse("75"), "Module"),
- color.Render(mode, color.Parse("245"), "Level"),
- color.Render(mode, color.Parse("222"), "Cost"),
- color.Render(mode, color.Parse("179"), "XP"),
+ color.Render(mode, color.Parse("4B"), "Module"),
+ color.Render(mode, color.Parse("F5"), "Level"),
+ color.Render(mode, color.Parse("DE"), "Cost"),
+ color.Render(mode, color.Parse("B3"), "XP"),
},
}
@@ -77,10 +77,10 @@ func (g *Game) doMods(sess *net.Session, showAll bool) {
})
} else if unlocked {
t.Rows = append(t.Rows, []string{
- color.Render(mode, color.Parse("75"), m.Name),
- color.Render(mode, color.Parse("245"), levelStr),
- color.Render(mode, color.Parse("222"), costStr),
- color.Render(mode, color.Parse("179"), xpStr),
+ color.Render(mode, color.Parse("4B"), m.Name),
+ color.Render(mode, color.Parse("F5"), levelStr),
+ color.Render(mode, color.Parse("DE"), costStr),
+ color.Render(mode, color.Parse("B3"), xpStr),
})
} else {
t.Rows = append(t.Rows, []string{
diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go
index 836a150..a2e54c2 100644
--- a/internal/game/cmd_option.go
+++ b/internal/game/cmd_option.go
@@ -18,10 +18,10 @@ func (g *Game) doOption(sess *net.Session, input string) {
if input == "" {
table := &ui.Table{
Columns: []string{
- color.Render(mode, color.Parse("75"), "Option"),
- color.Render(mode, color.Parse("230"), "Value"),
- color.Render(mode, color.Parse("243"), "Valid"),
- color.Render(mode, color.Parse("252"), "Description"),
+ color.Render(mode, color.Parse("4B"), "Option"),
+ color.Render(mode, color.Parse("E6"), "Value"),
+ color.Render(mode, color.Parse("F3"), "Valid"),
+ color.Render(mode, color.Parse("FC"), "Description"),
},
}
for _, def := range player.OptionDefs {
@@ -29,10 +29,10 @@ func (g *Game) doOption(sess *net.Session, input string) {
val = truncateForTable(val)
valid := formatValidValues(&def)
table.Rows = append(table.Rows, []string{
- color.Render(mode, color.Parse("75"), def.Name),
- color.Render(mode, color.Parse("230"), val),
- color.Render(mode, color.Parse("243"), valid),
- color.Render(mode, color.Parse("252"), def.Description),
+ color.Render(mode, color.Parse("4B"), def.Name),
+ color.Render(mode, color.Parse("E6"), val),
+ color.Render(mode, color.Parse("F3"), valid),
+ color.Render(mode, color.Parse("FC"), def.Description),
})
}
for _, line := range table.Render(p.OptionBool("unicode")) {
diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go
index 20c61d9..0554f32 100644
--- a/internal/game/cmd_score.go
+++ b/internal/game/cmd_score.go
@@ -92,13 +92,13 @@ func (g *Game) doScore(sess *net.Session) {
sep()
nameStr := g.colorize(sess, "player_name", p.Name)
- clStr := color.Render(mode, color.Parse("230"), fmt.Sprintf("Combat Lvl %d", p.CombatLevel()))
+ clStr := color.Render(mode, color.Parse("E6"), fmt.Sprintf("Combat Lvl %d", p.CombatLevel()))
roomStr := g.colorize(sess, "room_number", fmt.Sprintf("Room #%d", p.RoomID))
row1 := padded(nameStr, 20) + padded(clStr, 24) + roomStr
content(row1)
creditsStr := g.colorize(sess, "credits_pickup", formatInt(p.Credits))
- styleStr := color.Render(mode, color.Parse("75"), fmt.Sprintf("Style: %s", p.AttackStyle))
+ styleStr := color.Render(mode, color.Parse("4B"), fmt.Sprintf("Style: %s", p.AttackStyle))
invStr := fmt.Sprintf("Inv: %d/28", 28-p.FreeSlots())
row2 := padded("Credits: "+creditsStr, 20) + padded(styleStr, 24) + invStr
content(row2)
@@ -111,7 +111,7 @@ func (g *Game) doScore(sess *net.Session) {
hpLine := fmt.Sprintf("HP [%s] %s / %s",
hpBarStr,
color.Render(mode, color.ColorSpec{Fg: hpFg}, fmt.Sprint(p.HP)),
- color.Render(mode, color.Parse("230"), fmt.Sprint(p.MaxHP())))
+ color.Render(mode, color.Parse("E6"), fmt.Sprint(p.MaxHP())))
content(hpLine)
batBarRaw := ui.RenderBarF(p.Battery, p.MaxBattery(), 26, unicode)
@@ -119,7 +119,7 @@ func (g *Game) doScore(sess *net.Session) {
batLine := fmt.Sprintf("BAT [%s] %s / %s",
batBarStr,
color.Render(mode, color.ColorSpec{Fg: 45}, fmt.Sprintf("%.1f", p.Battery)),
- color.Render(mode, color.Parse("230"), fmt.Sprintf("%.0f", p.MaxBattery())))
+ color.Render(mode, color.Parse("E6"), fmt.Sprintf("%.0f", p.MaxBattery())))
content(batLine)
gap()
diff --git a/internal/game/cmd_skills.go b/internal/game/cmd_skills.go
index 9dba5b7..9321b96 100644
--- a/internal/game/cmd_skills.go
+++ b/internal/game/cmd_skills.go
@@ -17,22 +17,22 @@ func (g *Game) doSkills(sess *net.Session) {
p := sess.Player
mode := g.colorMode(sess)
t := &ui.Table{Title: "Skills", Columns: []string{
- color.Render(mode, color.Parse("75"), "Skill"),
- color.Render(mode, color.Parse("245"), "Abbr"),
- color.Render(mode, color.Parse("230"), "Level"),
- color.Render(mode, color.Parse("222"), "XP"),
- color.Render(mode, color.Parse("179"), "XP to Next"),
+ color.Render(mode, color.Parse("4B"), "Skill"),
+ color.Render(mode, color.Parse("F5"), "Abbr"),
+ color.Render(mode, color.Parse("E6"), "Level"),
+ color.Render(mode, color.Parse("DE"), "XP"),
+ color.Render(mode, color.Parse("B3"), "XP to Next"),
}}
for _, s := range player.AllSkills {
level := p.Level(s)
xp := p.Skills[s]
next := player.XPForNextLevel(xp)
t.Rows = append(t.Rows, []string{
- color.Render(mode, color.Parse("75"), string(s)),
- color.Render(mode, color.Parse("245"), player.SkillAbbr[s]),
- color.Render(mode, color.Parse("230"), strconv.Itoa(level)),
- color.Render(mode, color.Parse("222"), strconv.Itoa(xp)),
- color.Render(mode, color.Parse("179"), strconv.Itoa(next)),
+ color.Render(mode, color.Parse("4B"), string(s)),
+ color.Render(mode, color.Parse("F5"), player.SkillAbbr[s]),
+ color.Render(mode, color.Parse("E6"), strconv.Itoa(level)),
+ color.Render(mode, color.Parse("DE"), strconv.Itoa(xp)),
+ color.Render(mode, color.Parse("B3"), strconv.Itoa(next)),
})
}
for _, line := range t.Render(p.OptionBool("unicode")) {
diff --git a/internal/game/cmd_symbol.go b/internal/game/cmd_symbol.go
index bb97010..0c3014b 100644
--- a/internal/game/cmd_symbol.go
+++ b/internal/game/cmd_symbol.go
@@ -58,7 +58,7 @@ func (g *Game) executeSymbol(sess *net.Session, args []string, rawInput string)
colorSpec := strings.Join(args, " ")
spec := color.Parse(colorSpec)
if spec.Empty() {
- sess.WriteLine("Invalid color spec. Use a color number like 232 and/or bold/dim/underline.")
+ sess.WriteLine("Invalid color spec. Use a hex color code like FF and/or bold/dim/underline.")
return
}
existing, ok := p.MapSymbols[p.RoomID]
@@ -82,7 +82,7 @@ func (g *Game) executeSymbol(sess *net.Session, args []string, rawInput string)
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.")
+ sess.WriteLine("Invalid color spec. Use a hex color code like FF and/or bold/dim/underline.")
return
}
}
diff --git a/internal/game/cmd_tech.go b/internal/game/cmd_tech.go
index 19b5425..8c6f14f 100644
--- a/internal/game/cmd_tech.go
+++ b/internal/game/cmd_tech.go
@@ -118,7 +118,7 @@ func (g *Game) doTechList(sess *net.Session) {
"",
fmt.Sprintf("Battery: %s/%s",
g.colorize(sess, "battery", fmt.Sprintf("%.1f", p.Battery)),
- color.Render(mode, color.Parse("230"), fmt.Sprintf("%.0f", p.MaxBattery())),
+ color.Render(mode, color.Parse("E6"), fmt.Sprintf("%.0f", p.MaxBattery())),
),
)
@@ -129,14 +129,14 @@ func (g *Game) doTechList(sess *net.Session) {
nameStr := tech.Name
if tech.Level <= techLevel {
if p.HasActiveTech(tech.ID) {
- status = color.Render(mode, color.Parse("82"), "ON")
+ status = color.Render(mode, color.Parse("52"), "ON")
} else {
- status = color.Render(mode, color.Parse("240"), "off")
+ status = color.Render(mode, color.Parse("F0"), "off")
}
- nameStr = color.Render(mode, color.Parse("75"), tech.Name)
+ nameStr = color.Render(mode, color.Parse("4B"), tech.Name)
} else {
- status = color.Render(mode, color.Parse("160"), "locked")
- nameStr = color.Render(mode, color.Parse("240"), tech.Name)
+ status = color.Render(mode, color.Parse("A0"), "locked")
+ nameStr = color.Render(mode, color.Parse("F0"), tech.Name)
}
effect := techEffectString(tech)
diff --git a/internal/game/core_login_account.go b/internal/game/core_login_account.go
index 093ae71..fc4a764 100644
--- a/internal/game/core_login_account.go
+++ b/internal/game/core_login_account.go
@@ -194,18 +194,18 @@ func (g *Game) showMenu(sess *net.Session) {
}
if len(sess.Account.Characters) > 0 {
lines = append(lines,
- color.ExpandTags(mode, " {3}(C){/}{4}onnect character to THOI{/}"),
+ color.ExpandTags(mode, " {03}(C){/}{04}onnect character to THOI{/}"),
"",
- color.ExpandTags(mode, " {3}(L){/}{4}ist characters{/}"),
- color.ExpandTags(mode, " {3}(R){/}{4}ename character{/}"),
- color.ExpandTags(mode, " {3}(D){/}{4}elete character{/}"),
+ color.ExpandTags(mode, " {03}(L){/}{04}ist characters{/}"),
+ color.ExpandTags(mode, " {03}(R){/}{04}ename character{/}"),
+ color.ExpandTags(mode, " {03}(D){/}{04}elete character{/}"),
)
}
lines = append(lines,
- color.ExpandTags(mode, " {3}(N){/}{4}ew character{/}"),
- color.ExpandTags(mode, " {3}(P){/}{4}urge account{/}"),
- color.ExpandTags(mode, " {3}(A){/}{4}ccount rename{/}"),
- color.ExpandTags(mode, " {3}(Q){/}{4}uit{/}"),
+ color.ExpandTags(mode, " {03}(N){/}{04}ew character{/}"),
+ color.ExpandTags(mode, " {03}(P){/}{04}urge account{/}"),
+ color.ExpandTags(mode, " {03}(A){/}{04}ccount rename{/}"),
+ color.ExpandTags(mode, " {03}(Q){/}{04}uit{/}"),
"",
)
sess.WriteLines(lines...)
diff --git a/internal/game/hacking/mastermind.go b/internal/game/hacking/mastermind.go
index 8d4eb1c..9a20907 100644
--- a/internal/game/hacking/mastermind.go
+++ b/internal/game/hacking/mastermind.go
@@ -45,8 +45,8 @@ func (m *MastermindGame) Init(level int) string {
sb.WriteString("No symbol repeats. You have 10 attempts to crack the code.\n")
sb.WriteString("\n")
sb.WriteString("After each guess, you'll see:\n")
- sb.WriteString(" {40}[X]{/} = correct symbol in correct position\n")
- sb.WriteString(" {226}[O]{/} = correct symbol in wrong position\n")
+ sb.WriteString(" {28}[X]{/} = correct symbol in correct position\n")
+ sb.WriteString(" {E2}[O]{/} = correct symbol in wrong position\n")
sb.WriteString(" [ ] = symbol not in code\n")
sb.WriteString("\n")
sb.WriteString("Commands:\n")
@@ -108,11 +108,11 @@ func (m *MastermindGame) HandleInput(input string) (string, bool, bool) {
for i, d := range guess {
match := "[ ]"
if guess[i] == m.code[i] {
- match = "{40}[X]{/}"
+ match = "{28}[X]{/}"
} else {
for j := 0; j < 4; j++ {
if guess[i] == m.code[j] {
- match = "{226}[O]{/}"
+ match = "{E2}[O]{/}"
break
}
}
@@ -150,7 +150,7 @@ func (m *MastermindGame) doStatus() string {
for j, d := range g.Digits {
match := "[ ]"
if g.Digits[j] == m.code[j] {
- match = "{40}[X]{/}"
+ match = "{28}[X]{/}"
} else {
found := false
for k := 0; k < 4; k++ {
@@ -160,7 +160,7 @@ func (m *MastermindGame) doStatus() string {
}
}
if found {
- match = "{226}[O]{/}"
+ match = "{E2}[O]{/}"
}
}
_ = d
diff --git a/internal/game/hacking/wumpus.go b/internal/game/hacking/wumpus.go
index 91b9243..1f0a434 100644
--- a/internal/game/hacking/wumpus.go
+++ b/internal/game/hacking/wumpus.go
@@ -218,16 +218,16 @@ func (w *WumpusGame) roomDesc() string {
for _, a := range dodecahedron[cur] {
if a == w.wumpus {
- sb.WriteString("{196}You detect corrupted data nearby...{/}\n")
+ sb.WriteString("{C4}You detect corrupted data nearby...{/}\n")
}
for _, p := range w.pits {
if a == p {
- sb.WriteString("{208}You sense a void in the network...{/}\n")
+ sb.WriteString("{D0}You sense a void in the network...{/}\n")
}
}
for _, i := range w.ice {
if a == i {
- sb.WriteString("{226}You hear static crackling...{/}\n")
+ sb.WriteString("{E2}You hear static crackling...{/}\n")
}
}
}
diff --git a/internal/game/look_entities.go b/internal/game/look_entities.go
index 36cff07..f6b58a1 100644
--- a/internal/game/look_entities.go
+++ b/internal/game/look_entities.go
@@ -37,7 +37,7 @@ func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Roo
}
hp = fmt.Sprintf(" [%d%% complete]", pct)
} else {
- hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("167"), fmt.Sprint(m.HP)), m.MaxHP)
+ hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("A7"), fmt.Sprint(m.HP)), m.MaxHP)
}
}
var desc string
diff --git a/internal/game/look_room.go b/internal/game/look_room.go
index 8a1da04..585efb0 100644
--- a/internal/game/look_room.go
+++ b/internal/game/look_room.go
@@ -11,10 +11,28 @@ import (
)
func (g *Game) showRoomName(sess *net.Session, p *player.Player, room *world.Room) {
- sess.WriteLines(
- "",
- fmt.Sprintf("%s (%s)", g.colorize(sess, "room_name", room.Name), g.colorize(sess, "room_number", fmt.Sprintf("#%d", room.ID))),
- )
+ title := fmt.Sprintf("%s (%s)", g.colorize(sess, "room_name", room.Name), g.colorize(sess, "room_number", fmt.Sprintf("#%d", room.ID)))
+ if bracket := g.roomSymbolBracket(sess, p, room); bracket != "" {
+ title += " " + bracket
+ }
+ sess.WriteLines("", title)
+}
+
+// roomSymbolBracket renders a dim-bracketed colored symbol (e.g. "[D]") when the
+// player has set a custom symbol for this room, else "".
+func (g *Game) roomSymbolBracket(sess *net.Session, p *player.Player, room *world.Room) string {
+ if p == nil {
+ return ""
+ }
+ if _, ok := p.MapSymbols[room.ID]; !ok {
+ return ""
+ }
+ ch, spec := roomMapSymbol(g, sess, room.ID, false)
+ mode := g.colorMode(sess)
+ dimSpec := g.resolveColor(sess, "dim")
+ return color.Render(mode, dimSpec, "[") +
+ color.Render(mode, spec, string(ch)) +
+ color.Render(mode, dimSpec, "]")
}
func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *world.Room) []string {
diff --git a/internal/game/look_target.go b/internal/game/look_target.go
index 96dc174..c5fb072 100644
--- a/internal/game/look_target.go
+++ b/internal/game/look_target.go
@@ -59,7 +59,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
}
var levelStr string
if best.Protected {
- levelStr = color.Render(g.colorMode(sess), color.Parse("223"), "(protected)")
+ levelStr = color.Render(g.colorMode(sess), color.Parse("DF"), "(protected)")
} else {
mobLevel := mobCombatLevel(best)
levelStr = g.levelColorize(sess, p.CombatLevel(), mobLevel, fmt.Sprintf("(level %d)", mobLevel))
diff --git a/internal/game/map_test.go b/internal/game/map_test.go
index 9a85806..05e70d0 100644
--- a/internal/game/map_test.go
+++ b/internal/game/map_test.go
@@ -1,13 +1,112 @@
package game
import (
+ "os"
+ "path/filepath"
+ "strconv"
"strings"
"testing"
"thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
"thehouseoficarus/internal/world"
)
+func writeTempRoom(t *testing.T, dir string, id int, body string) {
+ t.Helper()
+ roomsDir := filepath.Join(dir, "rooms")
+ if err := os.MkdirAll(roomsDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(roomsDir, strconv.Itoa(id)+".yaml")
+ if err := os.WriteFile(path, []byte(body), 0644); err != nil {
+ t.Fatal(err)
+ }
+}
+
+
+// TestMapConnectorGlyphs verifies the directional link rendering: bidirectional
+// links draw a bar, one-way (or one-side-blocked) links draw a directional
+// arrow, and links with no traversable direction draw a blocked 'X'.
+func TestMapConnectorGlyphs(t *testing.T) {
+ const condEast = "name: One\nexits:\n east:\n room: 2\n condition:\n flag: gate_open\n"
+
+ cases := []struct {
+ name string
+ room1 string
+ room2 string
+ flagOpen bool
+ wantPresent string
+ wantAbsent []string
+ }{
+ {
+ name: "bidirectional bar",
+ room1: "name: One\nexits:\n east: 2\n",
+ room2: "name: Two\nexits:\n west: 1\n",
+ wantPresent: "-",
+ wantAbsent: []string{"X", "<", ">"},
+ },
+ {
+ name: "one-way east arrow",
+ room1: "name: One\nexits:\n east: 2\n",
+ room2: "name: Two\n",
+ wantPresent: ">",
+ wantAbsent: []string{"X", "-", "<"},
+ },
+ {
+ name: "forward blocked, reverse open -> arrow not X",
+ room1: condEast,
+ room2: "name: Two\nexits:\n west: 1\n",
+ flagOpen: false,
+ wantPresent: "<",
+ wantAbsent: []string{"X", "-", ">"},
+ },
+ {
+ name: "forward blocked, reverse absent -> X",
+ room1: condEast,
+ room2: "name: Two\n",
+ flagOpen: false,
+ wantPresent: "X",
+ wantAbsent: []string{"-", "<", ">"},
+ },
+ {
+ name: "conditional unblocked -> bar",
+ room1: condEast,
+ room2: "name: Two\nexits:\n west: 1\n",
+ flagOpen: true,
+ wantPresent: "-",
+ wantAbsent: []string{"X", "<", ">"},
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ dir := t.TempDir()
+ writeTempRoom(t, dir, 1, tc.room1)
+ writeTempRoom(t, dir, 2, tc.room2)
+
+ g := &Game{Deps: Deps{World: world.New(dir)}, Flags: NewFlagStore()}
+ if tc.flagOpen {
+ g.Flags.Set("gate_open", true)
+ }
+ sess := &net.Session{Player: &player.Player{Flags: map[string]any{}}}
+
+ out := strings.Join(buildTinyMap(g, sess, 1, mapGlyphsForPlayer(false)), "\n")
+ if !strings.Contains(out, tc.wantPresent) {
+ t.Errorf("want %q present:\n%s", tc.wantPresent, out)
+ }
+ for _, a := range tc.wantAbsent {
+ if strings.Contains(out, a) {
+ t.Errorf("want %q absent:\n%s", a, out)
+ }
+ }
+ })
+ }
+}
+
+
+
func TestBuildTinyMap(t *testing.T) {
g := &Game{
Deps: Deps{World: world.New("../../data")},
diff --git a/internal/game/production_menu.go b/internal/game/production_menu.go
index c1d9a96..f625d54 100644
--- a/internal/game/production_menu.go
+++ b/internal/game/production_menu.go
@@ -127,7 +127,7 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, items []
mode := g.colorMode(sess)
skillLevel := p.Level(player.SkillName(skill))
- dimSpec := color.Parse("240")
+ dimSpec := color.Parse("F0")
tbl := &ui.Table{
Title: title,
diff --git a/internal/game/render_color.go b/internal/game/render_color.go
index a5ab8fa..0e31538 100644
--- a/internal/game/render_color.go
+++ b/internal/game/render_color.go
@@ -46,15 +46,15 @@ func levelColorSpec(myLevel, theirLevel int) color.ColorSpec {
diff := theirLevel - myLevel
switch {
case diff == 0:
- return color.Parse("231")
+ return color.Parse("E7")
case diff > 0 && diff < 5:
- return color.Parse("208")
+ return color.Parse("D0")
case diff >= 5:
- return color.Parse("196")
+ return color.Parse("C4")
case diff < 0 && diff > -5:
- return color.Parse("190")
+ return color.Parse("BE")
default:
- return color.Parse("34")
+ return color.Parse("22")
}
}
diff --git a/internal/game/render_map.go b/internal/game/render_map.go
index 2bac749..049db06 100644
--- a/internal/game/render_map.go
+++ b/internal/game/render_map.go
@@ -16,6 +16,7 @@ type mapGlyphs struct {
topFill rune
connectorH, connectorV rune
upArrow, downArrow rune
+ leftArrow, rightArrow rune
}
func mapGlyphsForPlayer(unicode bool) mapGlyphs {
@@ -23,13 +24,13 @@ func mapGlyphsForPlayer(unicode bool) mapGlyphs {
return mapGlyphs{
topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
side: '║', topFill: '═', connectorH: '-', connectorV: '│',
- upArrow: '↑', downArrow: '↓',
+ upArrow: '↑', downArrow: '↓', leftArrow: '←', rightArrow: '→',
}
}
return mapGlyphs{
topLeft: '.', topRight: '.', bottomLeft: ':', bottomRight: ':',
side: ':', topFill: '.', connectorH: '-', connectorV: '|',
- upArrow: '^', downArrow: 'v',
+ upArrow: '^', downArrow: 'v', leftArrow: '<', rightArrow: '>',
}
}
@@ -129,6 +130,10 @@ func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string
colorMode := colorModeFor(sess)
atSpec := resolveMapAt(g, sess)
dimSpec := resolveDim(g, sess)
+ ctx := &mapRenderCtx{
+ g: g, sess: sess, bg: bg, visited: visited, currentRoom: roomID,
+ atSpec: atSpec, dimSpec: dimSpec, blockedSpec: resolveMapBlocked(g, sess), mg: mg,
+ }
grid := make([][]mapCell, 5)
for i := range grid {
@@ -170,11 +175,7 @@ func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string
continue
}
if exitsConnect(g, leftRoom, rightRoom, world.East, world.West) {
- 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}
+ grid[(y+1)*2][(x+1)*2+1] = ctx.connectorCell(leftRoom, rightRoom, world.East, world.West)
}
}
}
@@ -189,11 +190,7 @@ func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string
continue
}
if exitsConnect(g, topRoom, bottomRoom, world.South, world.North) {
- 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}
+ grid[(y+1)*2+1][(x+1)*2] = ctx.connectorCell(topRoom, bottomRoom, world.South, world.North)
}
}
}
@@ -226,6 +223,10 @@ func buildFullMap(g *Game, sess *net.Session, roomID, mapWidth, mapHeight int, m
colorMode := colorModeFor(sess)
atSpec := resolveMapAt(g, sess)
dimSpec := resolveDim(g, sess)
+ ctx := &mapRenderCtx{
+ g: g, sess: sess, bg: bg, visited: visited, currentRoom: roomID,
+ atSpec: atSpec, dimSpec: dimSpec, blockedSpec: resolveMapBlocked(g, sess), mg: mg,
+ }
grid := make([][]mapCell, mapHeight)
for i := range grid {
@@ -264,11 +265,7 @@ func buildFullMap(g *Game, sess *net.Session, roomID, mapWidth, mapHeight int, m
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}
+ grid[gr][gc] = ctx.connectorCell(rid, rightID, world.East, world.West)
}
}
}
@@ -278,11 +275,7 @@ func buildFullMap(g *Game, sess *net.Session, roomID, mapWidth, mapHeight int, m
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}
+ grid[gr][gc] = ctx.connectorCell(rid, bottomID, world.South, world.North)
}
}
}
@@ -316,15 +309,145 @@ func resolveDim(g *Game, sess *net.Session) color.ColorSpec {
if sess != nil {
return g.resolveColor(sess, "dim")
}
- return color.Parse("243 dim")
+ return color.Parse("F3 dim")
+}
+
+func resolveMapBlocked(g *Game, sess *net.Session) color.ColorSpec {
+ if sess != nil {
+ return g.resolveColor(sess, "map_blocked")
+ }
+ return color.Parse("C4")
+}
+
+// mapRenderCtx bundles the per-render state shared by node and connector drawing
+// so the tiny and full maps build cells the same way.
+type mapRenderCtx struct {
+ g *Game
+ sess *net.Session
+ bg *mapGraph
+ visited map[int]bool
+ currentRoom int
+ atSpec color.ColorSpec
+ dimSpec color.ColorSpec
+ blockedSpec color.ColorSpec
+ mg mapGlyphs
+}
+
+// nodeSpec returns the effective color a room's node is drawn with, mirroring
+// the logic used when placing room glyphs.
+func (c *mapRenderCtx) nodeSpec(roomID int) color.ColorSpec {
+ if roomID == c.currentRoom {
+ return c.atSpec
+ }
+ if c.visited != nil && !c.visited[roomID] {
+ return c.dimSpec
+ }
+ _, spec := roomMapSymbol(c.g, c.sess, roomID, false)
+ return spec
+}
+
+// connectorCell builds the link cell between two grid-adjacent rooms based on
+// the per-direction traversability of the two exits joining them:
+// - both directions open -> bidirectional bar (- / |)
+// - exactly one open -> arrow pointing along the open direction
+// - neither open (>=1 blocked) -> blocked 'X'
+//
+// Bars and arrows use the normal link coloring (dim if an endpoint is unvisited,
+// otherwise the gradient average); only 'X' uses the blocked color.
+func (c *mapRenderCtx) connectorCell(roomA, roomB int, dirAB, dirBA world.ExitDir) mapCell {
+ fwd := exitStateTo(c.g, c.sess, roomA, dirAB, roomB) // A -> B
+ bwd := exitStateTo(c.g, c.sess, roomB, dirBA, roomA) // B -> A
+
+ var glyph rune
+ switch {
+ case fwd == exitOpen && bwd == exitOpen:
+ glyph = barGlyph(c.mg, dirAB)
+ case fwd == exitOpen:
+ glyph = arrowGlyph(c.mg, dirAB)
+ case bwd == exitOpen:
+ glyph = arrowGlyph(c.mg, dirBA)
+ default:
+ // The caller only draws a connector when at least one exit exists, so
+ // reaching here means every existing direction is blocked.
+ return mapCell{char: 'X', spec: c.blockedSpec}
+ }
+
+ if c.visited != nil && (!c.visited[roomA] || !c.visited[roomB]) {
+ return mapCell{char: glyph, spec: c.dimSpec}
+ }
+ return mapCell{char: glyph, spec: color.Average(c.nodeSpec(roomA), c.nodeSpec(roomB))}
+}
+
+type exitState int
+
+const (
+ exitAbsent exitState = iota
+ exitOpen
+ exitBlocked
+)
+
+// exitStateTo reports whether the exit from `from` in `dir` leads to `neighbor`
+// and, if so, whether it is currently traversable for this player. A missing
+// session/player (e.g. in tests or background renders) treats conditional exits
+// as open so rendering never depends on player evaluation.
+func exitStateTo(g *Game, sess *net.Session, from int, dir world.ExitDir, neighbor int) exitState {
+ room, ok := loadRoom(g, from)
+ if !ok {
+ return exitAbsent
+ }
+ exit, ok := room.Exits[dir]
+ if !ok || exit.Room != neighbor {
+ return exitAbsent
+ }
+ if exit.Condition == nil || sess == nil || sess.Player == nil {
+ return exitOpen
+ }
+ if g.checkCondition(sess, exit.Condition) {
+ return exitOpen
+ }
+ return exitBlocked
+}
+
+// barGlyph returns the bidirectional connector glyph for a link's orientation.
+func barGlyph(mg mapGlyphs, dir world.ExitDir) rune {
+ if dir == world.East || dir == world.West {
+ return mg.connectorH
+ }
+ return mg.connectorV
+}
+
+// arrowGlyph returns the one-way arrow pointing along the direction of travel.
+func arrowGlyph(mg mapGlyphs, dir world.ExitDir) rune {
+ switch dir {
+ case world.East:
+ return mg.rightArrow
+ case world.West:
+ return mg.leftArrow
+ case world.South:
+ return mg.downArrow
+ case world.North:
+ return mg.upArrow
+ }
+ return mg.connectorH
+}
+
+// roomDefaultColorSpec resolves the room's own default map color (feature 3).
+// Empty/unset returns NoColor.
+func roomDefaultColorSpec(g *Game, roomID int) color.ColorSpec {
+ if room, ok := loadRoom(g, roomID); ok && room.Color != "" {
+ return color.Parse(room.Color)
+ }
+ return color.NoColor()
}
func roomMapSymbol(g *Game, sess *net.Session, roomID int, unvisited bool) (rune, color.ColorSpec) {
+ roomSpec := roomDefaultColorSpec(g, roomID)
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()
+ // precedence: player symbol color > room default color > none
+ spec := roomSpec
if data.Color != "" {
spec = color.Parse(data.Color)
}
@@ -338,13 +461,13 @@ func roomMapSymbol(g *Game, sess *net.Session, roomID int, unvisited bool) (rune
}
if sess.Player.OptionBool("unicode") {
if unvisited {
- return '□', color.NoColor()
+ return '□', roomSpec
}
- return '■', color.NoColor()
+ return '■', roomSpec
}
- return 'o', color.NoColor()
+ return 'o', roomSpec
}
- return '■', color.NoColor()
+ return '■', roomSpec
}
func exitsConnect(g *Game, room1, room2 int, dir12, dir21 world.ExitDir) bool {
diff --git a/internal/game/validate_source.go b/internal/game/validate_source.go
index b93c45a..4a2e5b9 100644
--- a/internal/game/validate_source.go
+++ b/internal/game/validate_source.go
@@ -47,7 +47,7 @@ func (g *Game) validationSource() validate.Source {
Courses: courseViews,
Techs: techViews,
TechIDs: techIDs,
- CheckSources: g.ValidationConfig.CheckSources,
+ RootRooms: g.ValidationConfig.RootRooms,
IgnoreUnreachable: g.ValidationConfig.IgnoreUnreachable,
}
}