From 0fe67cfd5def4ac3e919fd611fe5acc55ca2d253 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Mon, 29 Jun 2026 04:21:37 -0400 Subject: better dialog color, edge cases on blocked map links fixed --- internal/game/act_talk.go | 12 ++++++------ internal/game/cmd_color.go | 1 + internal/game/map_test.go | 27 ++++++++++++++++++++++----- internal/game/render_map.go | 44 ++++++++++++++++++++++++++++++++------------ 4 files changed, 61 insertions(+), 23 deletions(-) (limited to 'internal/game') 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 } -- cgit v1.2.3