diff options
| author | historia <[not public]> | 2026-06-29 04:21:37 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-29 04:21:37 -0400 |
| commit | 0fe67cfd5def4ac3e919fd611fe5acc55ca2d253 (patch) | |
| tree | fbcdcdeb355bd6c9d3afab006f052a8b465f7feb | |
| parent | 72dbe734015916f98020d6a617981956f0509e26 (diff) | |
| download | thehouseoficarus-0fe67cfd5def4ac3e919fd611fe5acc55ca2d253.tar.gz | |
better dialog color, edge cases on blocked map links fixed
| -rw-r--r-- | data/mobs/spaceport_attendant.yaml | 66 | ||||
| -rw-r--r-- | data/objects/unique/1002_building.yaml | 3 | ||||
| -rw-r--r-- | data/objects/unique/1002_crowd.yaml | 17 | ||||
| -rw-r--r-- | data/objects/unique/1002_pilot.yaml | 9 | ||||
| -rw-r--r-- | data/objects/unique/1002_shuttle.yaml | 8 | ||||
| -rw-r--r-- | data/objects/unique/1002_sign.yaml | 2 | ||||
| -rw-r--r-- | data/rooms/intro/1001.yaml | 9 | ||||
| -rw-r--r-- | data/rooms/intro/1002.yaml | 46 | ||||
| -rw-r--r-- | data/rooms/intro/1003.yaml | 18 | ||||
| -rw-r--r-- | internal/color/color.go | 19 | ||||
| -rw-r--r-- | internal/color/color_test.go | 90 | ||||
| -rw-r--r-- | internal/config/config.go | 1 | ||||
| -rw-r--r-- | internal/game/act_talk.go | 12 | ||||
| -rw-r--r-- | internal/game/cmd_color.go | 1 | ||||
| -rw-r--r-- | internal/game/map_test.go | 27 | ||||
| -rw-r--r-- | internal/game/render_map.go | 44 |
16 files changed, 295 insertions, 77 deletions
diff --git a/data/mobs/spaceport_attendant.yaml b/data/mobs/spaceport_attendant.yaml new file mode 100644 index 0000000..ca69ccc --- /dev/null +++ b/data/mobs/spaceport_attendant.yaml @@ -0,0 +1,66 @@ +name: spaceport attendant +description: A lanky man in a wrinkled grey uniform with a name badge that reads "GERRY". He's shuffling papers at a podium. +unique: false +protected: true +idle_descriptions: + - scans the room nervously + - adjusts his collar + - checks his datapad + - looks at you expectantly +talk: + nodes: + start: + condition: + player_flag: 1003_attendant_moved + not: true + goto: show_datapad + message: '"Oh! Change your mind about the flight?"' + options: + - text: '"No, I''m an arrival... but no one''s at the other desk."' + goto: attendant_moved + - text: '"Goodbye."' + attendant_moved: + action: + set_player_flags: + 1003_attendant_moved: true + sequence: + - '"An... arrival!" he says, failing to contain his nerves. "Oh my, one second."' + - 'The lanky man scurries around the counter, across the room, and to the apparently identical counter on the other side.' + - '"Can I see your datapad?"' + options: + - text: '"No"' + goto: denied + - text: '"Present the datapad"' + goto: datapad_done + show_datapad: + condition: + player_flag: 1003_talked + not: true + goto: already_talked + message: '"Can I see your datapad?"' + options: + - text: '"No"' + goto: denied + - text: '"Present the datapad"' + goto: datapad_done + denied: + message: '"Well, I can''t let you onto Vesta until we can confirm your identity."' + datapad_done: + action: + set_player_flags: + 1003_talked: true + sequence: + - 'The man taps through menus on your PDA and writes down some of your personal details onto a clipboard. He flips through a notebook, apparently refreshing himself on arrival procedure, before he seems satisfied and hands back the datapad.' + - '"Just needed to make sure you''re not a terrorist!" he says, not a hint of sarcasm in his voice.' + - 'He presses a button. "I''ve opened the north door for you. Follow the tunnel all the way through to Numisia."' + already_talked: + message: '"Hello again."' + options: + - text: '"Where should I go in town?"' + goto: town_info + - text: '"Goodbye."' + town_info: + sequence: + - '"Well, there''s nearby places to mine, fish, cook, and chop wood if you''re here for work. We haven''t had new labor since we lost contact with Earth, so surely people are looking for help who can get you started."' + - '"The attendant tries to hide how he''s measuring your appearance. He still can''t tell if you''re an escaped prisoner, businessman, or terrorist."' + - '"Or, err, if you have business in Numisia, you''re probably looking for Director Joanna Lucci. She''s on the west side of town in the Governance and Gaming building."' diff --git a/data/objects/unique/1002_building.yaml b/data/objects/unique/1002_building.yaml deleted file mode 100644 index f7650dc..0000000 --- a/data/objects/unique/1002_building.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: building -aliases: [customs] -description: "A squat, tunnel-shaped building with no doors that looks somewhat like a lowercase 'n'. It's built from large chunks of gray rock that must've come from the surface of Vesta." diff --git a/data/objects/unique/1002_crowd.yaml b/data/objects/unique/1002_crowd.yaml deleted file mode 100644 index 5d0105d..0000000 --- a/data/objects/unique/1002_crowd.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: crowd of people -aliases: [line, group, passengers] -hidden: true -description: - - condition: - all_of: - - player_flag: 1002_intro_done - not: true - - player_flag: 1002_lined_up - text: "A mass of people still onto the crowded platform, blocking your exit!" - - condition: - all_of: - - player_flag: 1002_intro_done - not: true - - player_flag: 1002_lined_up - not: true - text: "A dozen or so passengers ready to leave, all with luggage. Their eyes are a mix of impatience, anxiety, and perhaps suspicion." diff --git a/data/objects/unique/1002_pilot.yaml b/data/objects/unique/1002_pilot.yaml deleted file mode 100644 index 846cded..0000000 --- a/data/objects/unique/1002_pilot.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: pilot -hidden: true -description: - - condition: - all_of: - - player_flag: 1002_intro_done - not: true - - player_flag: 1002_lined_up - text: "The pilot stands straight as if about to escort royalty looking down his nose a bit at the passengers. He is quickly and professionally checking tickets." diff --git a/data/objects/unique/1002_shuttle.yaml b/data/objects/unique/1002_shuttle.yaml deleted file mode 100644 index 64375f5..0000000 --- a/data/objects/unique/1002_shuttle.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: landing craft -aliases: [shuttle, ship] -hidden: true -description: - - condition: - player_flag: 1002_intro_done - not: true - text: "The small white craft has short, cute wings, a wide snub-nosed cockpit, and the rear hatch opened into a staircase. The low hum of its power plant vibrates the platform as it idles. You might guess it's a 95 ton Type-QS Passenger Shuttle. Just a guess though." diff --git a/data/objects/unique/1002_sign.yaml b/data/objects/unique/1002_sign.yaml deleted file mode 100644 index cdd3633..0000000 --- a/data/objects/unique/1002_sign.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: sign -description: "The sign simply says 'Processing' in bold red letters. Beneath in smaller text it says 'Welcome to Vesta', seemingly added afterwards to try to give the sign some warmth." diff --git a/data/rooms/intro/1001.yaml b/data/rooms/intro/1001.yaml index 8e2f573..6f84799 100644 --- a/data/rooms/intro/1001.yaml +++ b/data/rooms/intro/1001.yaml @@ -4,13 +4,12 @@ description: condition: player_flag: 1001_touchdown not: true - - text: "A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large framed sign hangs next to the closed cockpit door. A piston-powered staircase leads down out the back of the small craft with a smiling attendant ready to help disembark." + - text: "A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large {11}framed sign{/} hangs next to the closed cockpit door. A piston-powered staircase leads down out the back of the small craft with a smiling attendant ready to help disembark." objects: - - name: sign - aliases: [safety card, safety instructions, frame] - inroom_description: A large framed sign is mounted near the cockpit door. + - name: framed sign + hidden: true description: |- - We hope you enjoy your trip on our shuttle. Your pilot is [TYLER]. + We hope you enjoy your trip on our shuttle! Your pilot is [TYLER]. When you arrive at your final destination, please be aware of the following basic commands: diff --git a/data/rooms/intro/1002.yaml b/data/rooms/intro/1002.yaml index c3116e3..c7195ea 100644 --- a/data/rooms/intro/1002.yaml +++ b/data/rooms/intro/1002.yaml @@ -6,13 +6,47 @@ description: text: "A large, circular, concrete landing pad sits in the middle of a blue ocean with a transport shuttle taking up nearly all of the walkable area. A crowd of perhaps a dozen people are shambling out of a squat, doorless building to the north. A sign above the entryway reads \"Processing\"" - text: "A large, circular, concrete landing pad sits empty in the middle of a blue ocean that stretches to the horizon. North is a concrete path through a squat, doorless building with sign above the entryway reading \"Processing\"" objects: - - id: 1002_crowd - - id: 1002_shuttle - - id: 1002_pilot - - id: 1002_building - - id: 1002_sign + - name: crowd of people + aliases: [line, group, passengers] + hidden: true + description: + - condition: + all_of: + - player_flag: 1002_intro_done + not: true + - player_flag: 1002_lined_up + text: "A mass of people still onto the crowded platform, blocking your exit!" + - condition: + all_of: + - player_flag: 1002_intro_done + not: true + - player_flag: 1002_lined_up + not: true + text: "A dozen or so passengers ready to leave, all with luggage. Their eyes are a mix of impatience, anxiety, and perhaps suspicion." + - name: landing craft + aliases: [shuttle, ship] + hidden: true + description: + - condition: + player_flag: 1002_intro_done + not: true + text: "The small white craft has short, cute wings, a wide snub-nosed cockpit, and the rear hatch opened into a staircase. The low hum of its power plant vibrates the platform as it idles. You might guess it's a 95 ton Type-QS Passenger Shuttle. Just a guess though." + - name: pilot + hidden: true + description: + - condition: + all_of: + - player_flag: 1002_intro_done + not: true + - player_flag: 1002_lined_up + text: "The pilot stands straight as if about to escort royalty looking down his nose a bit at the passengers. He is quickly and professionally checking tickets." + - name: building + aliases: [processing] + description: "A building with no door on this side that looks like a giant cup of tea spilled towards you. It's built from large chunks of gray rock that must've come from the surface of Vesta." + - name: sign + description: "The sign simply says 'Processing' in bold red letters. Beneath in smaller text it says 'Welcome to Vesta', seemingly added afterwards to try to give the sign some warmth." exits: - south: + up: room: 1001 condition: player_flag: always_false diff --git a/data/rooms/intro/1003.yaml b/data/rooms/intro/1003.yaml index d19b0b6..77cb096 100644 --- a/data/rooms/intro/1003.yaml +++ b/data/rooms/intro/1003.yaml @@ -1,17 +1,27 @@ id: 1003 -name: Customs Building +name: Processing Building color: "" description: - - text: A cramped customs checkpoint lit by flickering panels. Officials in grey uniforms wave the new arrivals through, one at a time. (This area is still under construction.) + - text: A short tunnel-like building with queue ropes and desks on each side, and signs directing "Departures" and "Arrivals" above each. An attendant stands doing paperwork on the Departures side. No one is on the Arrivals side. exits: north: room: 1004 + condition: + player_flag: 1003_talked south: room: 1002 objects: [] item_spawns: [] -mobs: [] -on_enter: [] +mobs: + - spaceport_attendant +on_enter: + - condition: + player_flag: 1003_first_time + not: true + delay: 5 + message: "{bold}The ground rumbles as the shuttle departs the pad behind you!{/}" + set_player_flags: + 1003_first_time: true hazard: "" block_transport: false triggers: [] diff --git a/internal/color/color.go b/internal/color/color.go index 5a1aa54..92535d1 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -521,3 +521,22 @@ func ExpandTagsDefault(mode string, def ColorSpec, text string) string { return sb.String() } + +func ExpandDialogTags(mode string, dialogSpec ColorSpec, text string) string { + parts := strings.Split(text, `"`) + var sb strings.Builder + for i, part := range parts { + if i%2 == 0 { + if part != "" { + sb.WriteString(ExpandTagsDefault(mode, NoColor(), part)) + } + } else { + if i < len(parts)-1 { + sb.WriteString(ExpandTagsDefault(mode, dialogSpec, `"`+part+`"`)) + } else { + sb.WriteString(ExpandTagsDefault(mode, dialogSpec, `"`+part)) + } + } + } + return sb.String() +} diff --git a/internal/color/color_test.go b/internal/color/color_test.go index bffc0d7..f6b79f0 100644 --- a/internal/color/color_test.go +++ b/internal/color/color_test.go @@ -117,3 +117,93 @@ func TestAverage(t *testing.T) { t.Errorf("Average(none, none).Fg = %d, want -1", got.Fg) } } + +func TestExpandDialogTags_NoQuotes(t *testing.T) { + dialogSpec := Parse("D0") + got := ExpandDialogTags("xterm256", dialogSpec, "The Netrunner looks up.") + if strings.Contains(got, "\033") { + t.Errorf("text without quotes should not contain ANSI codes, got %q", got) + } +} + +func TestExpandDialogTags_AllQuoted(t *testing.T) { + dialogSpec := Parse("D0") + got := ExpandDialogTags("xterm256", dialogSpec, `"Hello there."`) + expectFg := FgCode("xterm256", 208) + if !strings.Contains(got, expectFg+`"Hello there."`+Reset) && !strings.Contains(got, `"`+expectFg+`Hello there."`+Reset) { + t.Errorf("fully quoted text should be dialog-colored, got %q", got) + } + if !strings.Contains(got, `"`) { + t.Error("quote characters should be present") + } +} + +func TestExpandDialogTags_Mixed(t *testing.T) { + dialogSpec := Parse("D0") + got := ExpandDialogTags("xterm256", dialogSpec, `He says, "Hello, world!"`) + expectFg := FgCode("xterm256", 208) + if !strings.Contains(got, `He says, `) { + t.Errorf("narrative text missing, got %q", got) + } + if !strings.Contains(got, expectFg+`"Hello, world!"`+Reset) { + t.Errorf("quoted text should be dialog-colored, got %q", got) + } +} + +func TestExpandDialogTags_InlineTagsInQuotes(t *testing.T) { + dialogSpec := Parse("D0") + got := ExpandDialogTags("xterm256", dialogSpec, `"Please {0B bold}look{/} carefully."`) + inlineFg := FgCode("xterm256", 11) + if !strings.Contains(got, inlineFg+`look`+Reset) { + t.Errorf("inline tag inside quotes should override dialog color, got %q", got) + } +} + +func TestExpandDialogTags_InlineTagsOutsideQuotes(t *testing.T) { + dialogSpec := Parse("D0") + got := ExpandDialogTags("xterm256", dialogSpec, `{0B bold}Notice:{/} "Something moved."`) + inlineFg := FgCode("xterm256", 11) + if !strings.Contains(got, inlineFg+`Notice:`+Reset) { + t.Errorf("inline tag outside quotes should work, got %q", got) + } +} + +func TestExpandDialogTags_MultipleQuotes(t *testing.T) { + dialogSpec := Parse("D0") + got := ExpandDialogTags("xterm256", dialogSpec, `"Hello," she said. "How are you?"`) + count := strings.Count(got, FgCode("xterm256", 208)) + if count < 2 { + t.Errorf("expected at least 2 dialog-colored segments, got %d in %q", count, got) + } +} + +func TestExpandDialogTags_UnmatchedQuote(t *testing.T) { + dialogSpec := Parse("D0") + got := ExpandDialogTags("xterm256", dialogSpec, `"Something started but never finished`) + expectFg := FgCode("xterm256", 208) + if !strings.Contains(got, expectFg) { + t.Errorf("unmatched quote should still be dialog-colored, got %q", got) + } + if !strings.Contains(got, `"Something started but never finished`) { + t.Errorf("unmatched quote text should be present, got %q", got) + } +} + +func TestExpandDialogTags_Empty(t *testing.T) { + dialogSpec := Parse("D0") + got := ExpandDialogTags("xterm256", dialogSpec, "") + if got != "" { + t.Errorf("empty input should yield empty output, got %q", got) + } +} + +func TestExpandDialogTags_NoColorMode(t *testing.T) { + dialogSpec := Parse("D0") + got := ExpandDialogTags("none", dialogSpec, `"Hello."`) + if strings.Contains(got, "\033") { + t.Errorf("no-color mode should suppress ANSI codes, got %q", got) + } + if got != `"Hello."` { + t.Errorf("no-color mode should preserve text, got %q", got) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 6393816..c8d8830 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -44,6 +44,7 @@ func DefaultColors() ColorsConfig { "say": "E6", "global": "2D", "dialog": "75", + "dialog_options": "bold", "broadcast": "D7", "sequence": "off", "fire": "D0", 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 } |
