aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-29 04:21:37 -0400
committerhistoria <[not public]>2026-06-29 04:21:37 -0400
commit0fe67cfd5def4ac3e919fd611fe5acc55ca2d253 (patch)
treefbcdcdeb355bd6c9d3afab006f052a8b465f7feb /internal/game
parent72dbe734015916f98020d6a617981956f0509e26 (diff)
downloadthehouseoficarus-0fe67cfd5def4ac3e919fd611fe5acc55ca2d253.tar.gz
better dialog color, edge cases on blocked map links fixed
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/act_talk.go12
-rw-r--r--internal/game/cmd_color.go1
-rw-r--r--internal/game/map_test.go27
-rw-r--r--internal/game/render_map.go44
4 files changed, 61 insertions, 23 deletions
diff --git a/internal/game/act_talk.go b/internal/game/act_talk.go
index c87e3c5..bd5a091 100644
--- a/internal/game/act_talk.go
+++ b/internal/game/act_talk.go
@@ -106,18 +106,18 @@ func (g *Game) showTalkNode(sess *net.Session, node behavior.TalkNode) {
td.SeqIndex = 0
msg := node.Sequence[0]
if msg != "" {
- sess.WriteLine(color.ExpandTagsDefault(g.colorMode(sess), dialogSpec, msg))
+ sess.WriteLine(color.ExpandDialogTags(g.colorMode(sess), dialogSpec, msg))
}
td.SeqIndex = 1
if td.SeqIndex < len(node.Sequence) {
sess.State = net.StateTalkSequence
- sess.Write("[enter to continue]\n")
+ sess.Write(g.colorize(sess, "dialog_options", "[enter to continue]\n"))
return
}
} else {
msg := node.Message.Pick()
if msg != "" {
- sess.WriteLine(color.ExpandTagsDefault(g.colorMode(sess), dialogSpec, msg))
+ sess.WriteLine(color.ExpandDialogTags(g.colorMode(sess), dialogSpec, msg))
}
if node.Action != nil {
if g.handleNodeAction(sess, node.Action) {
@@ -128,7 +128,7 @@ func (g *Game) showTalkNode(sess *net.Session, node behavior.TalkNode) {
visible := g.visibleOptions(sess, node.Options)
if len(visible) > 0 {
for i, opt := range visible {
- sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, opt.Text))
+ sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, color.ExpandDialogTags(g.colorMode(sess), g.resolveColor(sess, "dialog_options"), opt.Text)))
}
sess.State = net.StateTalk
g.reprompt(sess)
@@ -168,7 +168,7 @@ func (g *Game) finishSequence(sess *net.Session, node behavior.TalkNode) {
visible := g.visibleOptions(sess, node.Options)
if len(visible) > 0 {
for i, opt := range visible {
- sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, opt.Text))
+ sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, color.ExpandDialogTags(g.colorMode(sess), g.resolveColor(sess, "dialog_options"), opt.Text)))
}
sess.State = net.StateTalk
g.reprompt(sess)
@@ -304,7 +304,7 @@ func (g *Game) advanceTalk(sess *net.Session, p *player.Player) {
if td.SeqIndex < len(node.Sequence) {
msg := node.Sequence[td.SeqIndex]
if msg != "" {
- sess.WriteLine(color.ExpandTagsDefault(g.colorMode(sess), dialogSpec, msg))
+ sess.WriteLine(color.ExpandDialogTags(g.colorMode(sess), dialogSpec, msg))
}
td.SeqIndex++
}
diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go
index 00b4ce8..fd9fa55 100644
--- a/internal/game/cmd_color.go
+++ b/internal/game/cmd_color.go
@@ -157,6 +157,7 @@ var colorCategoryOrder = []string{
"say",
"global",
"dialog",
+ "dialog_options",
"broadcast",
"fire",
"eat_food",
diff --git a/internal/game/map_test.go b/internal/game/map_test.go
index 4869d38..0d529cf 100644
--- a/internal/game/map_test.go
+++ b/internal/game/map_test.go
@@ -26,15 +26,17 @@ func writeTempRoom(t *testing.T, dir string, id int, body string) {
}
// 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'.
+// links draw a bar, one-way links draw a directional arrow, outward-blocked
+// links draw a blocked 'X', and links with no traversable direction draw 'X'.
func TestMapConnectorGlyphs(t *testing.T) {
const condEast = "name: One\nexits:\n east:\n room: 2\n condition:\n flag: gate_open\n"
+ const condSouth = "name: Three\nexits:\n south:\n room: 2\n condition:\n flag: gate_open\n"
cases := []struct {
name string
room1 string
room2 string
+ room3 string
flagOpen bool
wantPresent string
wantAbsent []string
@@ -54,12 +56,12 @@ func TestMapConnectorGlyphs(t *testing.T) {
wantAbsent: []string{"X", "-", "<"},
},
{
- name: "forward blocked, reverse open -> arrow not X",
+ name: "forward blocked, reverse open -> X (shortest path is blocked)",
room1: condEast,
room2: "name: Two\nexits:\n west: 1\n",
flagOpen: false,
- wantPresent: "<",
- wantAbsent: []string{"X", "-", ">"},
+ wantPresent: "X",
+ wantAbsent: []string{"-", "<", ">"},
},
{
name: "forward blocked, reverse absent -> X",
@@ -77,6 +79,18 @@ func TestMapConnectorGlyphs(t *testing.T) {
wantPresent: "-",
wantAbsent: []string{"X", "<", ">"},
},
+ {
+ name: "outward open, inward blocked -> outward arrow (dist rules)",
+ room1: "name: One\nexits:\n east: 2\n",
+ room2: "name: Two\nexits:\n west: 1\n north: 3\n",
+ room3: condSouth,
+ flagOpen: false,
+ // dist[room1]=0, dist[room2]=1, dist[room3]=2.
+ // Link 2↔3: near=2 (dist1), far=3 (dist2).
+ // out = 2→north→3 = open → '^'
+ wantPresent: "^",
+ wantAbsent: []string{"X", "v", "<", ">"},
+ },
}
for _, tc := range cases {
@@ -84,6 +98,9 @@ func TestMapConnectorGlyphs(t *testing.T) {
dir := t.TempDir()
writeTempRoom(t, dir, 1, tc.room1)
writeTempRoom(t, dir, 2, tc.room2)
+ if tc.room3 != "" {
+ writeTempRoom(t, dir, 3, tc.room3)
+ }
g := &Game{Deps: Deps{World: world.New(dir)}, Flags: NewFlagStore()}
if tc.flagOpen {
diff --git a/internal/game/render_map.go b/internal/game/render_map.go
index 58e0613..8790a1e 100644
--- a/internal/game/render_map.go
+++ b/internal/game/render_map.go
@@ -42,6 +42,7 @@ type mapCell struct {
type mapGraph struct {
posToRoom map[[2]int]int
roomToPos map[int][2]int
+ dist map[int]int
}
var bfsDirs = []struct {
@@ -58,6 +59,7 @@ 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),
+ dist: make(map[int]int),
}
type node struct {
@@ -67,6 +69,7 @@ func buildGraph(g *Game, startRoomID int, visited map[int]bool) *mapGraph {
queue := []node{{startRoomID, 0, 0}}
mg.posToRoom[[2]int{0, 0}] = startRoomID
mg.roomToPos[startRoomID] = [2]int{0, 0}
+ mg.dist[startRoomID] = 0
for len(queue) > 0 {
n := queue[0]
@@ -92,6 +95,7 @@ func buildGraph(g *Game, startRoomID int, visited map[int]bool) *mapGraph {
nx, ny := n.x+d.dx, n.y+d.dy
mg.posToRoom[[2]int{nx, ny}] = targetID
mg.roomToPos[targetID] = [2]int{nx, ny}
+ mg.dist[targetID] = mg.dist[n.roomID] + 1
queue = append(queue, node{targetID, nx, ny})
}
}
@@ -350,9 +354,16 @@ func (c *mapRenderCtx) nodeSpec(roomID int) color.ColorSpec {
// the per-direction traversability of the two exits joining them. ok is false
// when there is no link at all, so the caller draws nothing:
// - both directions open -> bidirectional bar (- / |)
-// - exactly one open -> arrow pointing along the open direction
-// - >=1 exists but none open -> blocked 'X'
-// - neither exit exists -> ok == false (no cell)
+// - outward direction open -> arrow pointing outward
+// - outward direction blocked -> blocked 'X'
+// - only inward direction open -> arrow pointing inward
+// - none traversable -> blocked 'X'
+// - neither exit exists -> ok == false (no cell)
+//
+// "Outward" is the exit from the room nearer the player (smaller BFS distance)
+// toward the farther one — i.e. the link as reached along the shortest path.
+// Since each BFS hop is a unit grid step, grid-adjacent rooms always differ in
+// distance parity, so there is never a tie to break.
//
// Bars and arrows use the normal link coloring (dim if an endpoint is unvisited,
// otherwise the gradient average); only 'X' uses the blocked color.
@@ -360,23 +371,32 @@ func (c *mapRenderCtx) connectorCell(roomA, roomB int, dirAB, dirBA world.ExitDi
fwd := exitStateTo(c.g, c.sess, roomA, dirAB, roomB) // A -> B
bwd := exitStateTo(c.g, c.sess, roomB, dirBA, roomA) // B -> A
- if fwd == exitAbsent && bwd == exitAbsent {
+ switch {
+ case fwd == exitAbsent && bwd == exitAbsent:
return mapCell{}, false
+ case fwd == exitOpen && bwd == exitOpen:
+ return c.coloredCell(roomA, roomB, barGlyph(c.mg, dirAB))
+ }
+
+ // Orient the link outward, from near room to far room.
+ outDir, inDir, out, in := dirAB, dirBA, fwd, bwd
+ if c.bg.dist[roomB] < c.bg.dist[roomA] {
+ outDir, inDir, out, in = dirBA, dirAB, bwd, fwd
}
- 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)
+ case out == exitOpen:
+ return c.coloredCell(roomA, roomB, arrowGlyph(c.mg, outDir))
+ case out == exitAbsent && in == exitOpen:
+ return c.coloredCell(roomA, roomB, arrowGlyph(c.mg, inDir))
default:
- // At least one exit exists but none are currently traversable.
return mapCell{char: 'X', spec: c.blockedSpec}, true
}
+}
+// coloredCell applies the shared link coloring logic for bar/arrow glyphs
+// (dim if either endpoint is unvisited, otherwise the gradient average).
+func (c *mapRenderCtx) coloredCell(roomA, roomB int, glyph rune) (mapCell, bool) {
if c.visited != nil && (!c.visited[roomA] || !c.visited[roomB]) {
return mapCell{char: glyph, spec: c.dimSpec}, true
}