diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/cmd_dig.go | 55 | ||||
| -rw-r--r-- | internal/game/cmd_room_insert.go | 56 | ||||
| -rw-r--r-- | internal/game/core_command_queue.go (renamed from internal/game/command_queue.go) | 0 | ||||
| -rw-r--r-- | internal/game/core_flagstore.go (renamed from internal/game/flagstore.go) | 0 | ||||
| -rw-r--r-- | internal/game/core_object_def.go (renamed from internal/game/object_def.go) | 0 | ||||
| -rw-r--r-- | internal/game/core_validate.go (renamed from internal/game/validate_source.go) | 0 | ||||
| -rw-r--r-- | internal/game/map_test.go | 69 | ||||
| -rw-r--r-- | internal/game/production_index.go (renamed from internal/game/craft_index.go) | 0 | ||||
| -rw-r--r-- | internal/game/render_map.go | 339 | ||||
| -rw-r--r-- | internal/game/sys_safespot_state.go (renamed from internal/game/safespot_manager.go) | 0 | ||||
| -rw-r--r-- | internal/game/sys_triggers.go (renamed from internal/game/triggers.go) | 0 |
11 files changed, 196 insertions, 323 deletions
diff --git a/internal/game/cmd_dig.go b/internal/game/cmd_dig.go index 653f045..0691b90 100644 --- a/internal/game/cmd_dig.go +++ b/internal/game/cmd_dig.go @@ -264,58 +264,31 @@ func findNextRoomID(currentRoomPath string) (int, error) { } -func (g *Game) buildGridFrom(fromRoomID int) (coord map[int][2]int, roomAt map[[2]int]int) { +func (g *Game) buildGridFrom(fromRoomID int) (coord map[int][3]int, roomAt map[[3]int]int) { roomIndex := g.World.RoomIndex() - coord = map[int][2]int{fromRoomID: {0, 0}} - roomAt = map[[2]int]int{{0, 0}: fromRoomID} - queue := []int{fromRoomID} - - for len(queue) > 0 { - rid := queue[0] - queue = queue[1:] - room, err := g.World.LoadRoom(rid) - if err != nil { - continue - } - c := coord[rid] - for _, ed := range world.ExitOrder { - exit, ok := room.Exits[ed] - if !ok || exit.Room <= 0 || !roomIndex[exit.Room] { - continue + rg := world.BuildGrid(fromRoomID, + func(id int) (*world.Room, bool) { + r, err := g.World.LoadRoom(id) + if err != nil { + return nil, false } - target := exit.Room - if ed == world.Up || ed == world.Down { - continue - } - gd, ok := world.DirectionDeltas[ed] - if !ok { - continue - } - want := [2]int{c[0] + gd[0], c[1] + gd[1]} - - if _, exists := coord[target]; exists { - continue - } - if occupier, exists := roomAt[want]; exists && occupier != target { - continue - } - coord[target] = want - roomAt[want] = target - queue = append(queue, target) - } - } - return coord, roomAt + return r, true + }, + func(id int) bool { return roomIndex[id] }, + nil, + ) + return rg.Coord, rg.RoomAt } func (g *Game) findGridConflict(fromRoomID int, dir world.ExitDir) int { - delta, ok := world.DirectionDeltas[dir] + delta, ok := world.DirectionDeltas3D[dir] if !ok { return 0 } _, roomAt := g.buildGridFrom(fromRoomID) - targetCoord := [2]int{delta[0], delta[1]} + targetCoord := [3]int{delta[0], delta[1], delta[2]} if occupier, ok := roomAt[targetCoord]; ok && occupier != fromRoomID { return occupier } diff --git a/internal/game/cmd_room_insert.go b/internal/game/cmd_room_insert.go index 4fbcd10..6164e50 100644 --- a/internal/game/cmd_room_insert.go +++ b/internal/game/cmd_room_insert.go @@ -65,12 +65,16 @@ func (g *Game) roomInsert(sess *net.Session, args []string) { oppositeDir := world.OppositeExit[dir] - if delta, isHorizontal := world.DirectionDeltas[dir]; isHorizontal { + if delta3D, ok := world.DirectionDeltas3D[dir]; ok { coord, roomAt := g.buildGridFrom(p.RoomID) - sSet := g.bfsComponent(targetID, p.RoomID) + sSet := g.bfsReachable(targetID, func(_ int, _ world.ExitDir, target int) bool { + return target == p.RoomID + }) - withoutEdge := g.bfsWithoutEdge(p.RoomID, dir, targetID) + withoutEdge := g.bfsReachable(p.RoomID, func(rid int, d world.ExitDir, target int) bool { + return rid == p.RoomID && d == dir && target == targetID + }) for rid := range sSet { if withoutEdge[rid] { room, _ := g.World.LoadRoom(rid) @@ -90,7 +94,7 @@ func (g *Game) roomInsert(sess *net.Session, args []string) { continue } oldPos := coord[rid] - newPos := [2]int{oldPos[0] + delta[0], oldPos[1] + delta[1]} + newPos := [3]int{oldPos[0] + delta3D[0], oldPos[1] + delta3D[1], oldPos[2] + delta3D[2]} if occupier, ok := roomAt[newPos]; ok && !sSet[occupier] { occRoom, _ := g.World.LoadRoom(occupier) occName := fmt.Sprintf("#%d", occupier) @@ -221,7 +225,10 @@ func (g *Game) roomInsert(sess *net.Session, args []string) { g.checkAggro(sess) } -func (g *Game) bfsComponent(startID, excludeID int) map[int]bool { +// bfsReachable returns the set of rooms reachable from startID over the room +// exit graph, restricted to known rooms. skipEdge, when non-nil, prunes an +// individual directed exit (the edge from rid via dir to target) from the walk. +func (g *Game) bfsReachable(startID int, skipEdge func(rid int, dir world.ExitDir, target int) bool) map[int]bool { roomIndex := g.World.RoomIndex() visited := map[int]bool{startID: true} queue := []int{startID} @@ -234,51 +241,14 @@ func (g *Game) bfsComponent(startID, excludeID int) map[int]bool { continue } for _, ed := range world.ExitOrder { - if ed == world.Up || ed == world.Down { - continue - } exit, ok := room.Exits[ed] if !ok || exit.Room <= 0 || !roomIndex[exit.Room] { continue } target := exit.Room - if target == excludeID { - continue - } - if visited[target] { + if skipEdge != nil && skipEdge(rid, ed, target) { continue } - visited[target] = true - queue = append(queue, target) - } - } - return visited -} - -func (g *Game) bfsWithoutEdge(fromID int, skipDir world.ExitDir, skipTo int) map[int]bool { - roomIndex := g.World.RoomIndex() - visited := map[int]bool{fromID: true} - queue := []int{fromID} - - for len(queue) > 0 { - rid := queue[0] - queue = queue[1:] - room, err := g.World.LoadRoom(rid) - if err != nil { - continue - } - for _, ed := range world.ExitOrder { - if ed == world.Up || ed == world.Down { - continue - } - exit, ok := room.Exits[ed] - if !ok || exit.Room <= 0 || !roomIndex[exit.Room] { - continue - } - if rid == fromID && ed == skipDir && exit.Room == skipTo { - continue - } - target := exit.Room if visited[target] { continue } diff --git a/internal/game/command_queue.go b/internal/game/core_command_queue.go index 7afc75b..7afc75b 100644 --- a/internal/game/command_queue.go +++ b/internal/game/core_command_queue.go diff --git a/internal/game/flagstore.go b/internal/game/core_flagstore.go index ecefccf..ecefccf 100644 --- a/internal/game/flagstore.go +++ b/internal/game/core_flagstore.go diff --git a/internal/game/object_def.go b/internal/game/core_object_def.go index 6cf2464..6cf2464 100644 --- a/internal/game/object_def.go +++ b/internal/game/core_object_def.go diff --git a/internal/game/validate_source.go b/internal/game/core_validate.go index 4a2e5b9..4a2e5b9 100644 --- a/internal/game/validate_source.go +++ b/internal/game/core_validate.go diff --git a/internal/game/map_test.go b/internal/game/map_test.go index ebb2197..712202b 100644 --- a/internal/game/map_test.go +++ b/internal/game/map_test.go @@ -325,7 +325,23 @@ func TestBuildFullMap(t *testing.T) { func TestStripBlankRows(t *testing.T) { input := []string{" ", "hello", "", " world ", " "} - want := []string{"hello", " world "} + want := []string{"hello", "", " world "} + got := stripBlankRows(input) + if len(got) != len(want) { + t.Fatalf("expected %d lines, got %d", len(want), len(got)) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("line %d: want %q, got %q", i, want[i], got[i]) + } + } +} + +func TestStripBlankRowsPreservesInternal(t *testing.T) { + // Internal blank rows (between non-blank content) should be preserved. + // They represent real vertical distance between disconnected components. + input := []string{" ", "hello", "", "", "world", " "} + want := []string{"hello", "", "", "world"} got := stripBlankRows(input) if len(got) != len(want) { t.Fatalf("expected %d lines, got %d", len(want), len(got)) @@ -374,3 +390,54 @@ func TestLeftTrimCommonZero(t *testing.T) { t.Errorf("line 1: want %q, got %q", "world", got[1]) } } + +func TestMap3DDisconnectedComponent(t *testing.T) { + // Tower 1 (0,0,0) has down→bridge (0,0,-1). bridge→east→tower2base (1,0,-1). + // tower2base→up→tower2 (1,0,0). Tower 2 is at the same z=0 as tower 1 but + // unreachable via horizontal exits. The 3D map should still show it. + dir := t.TempDir() + writeTempRoom(t, dir, 1, "name: Tower One\nexits:\n down: 2\n") + writeTempRoom(t, dir, 2, "name: Bridge\nexits:\n east: 3\n up: 1\n") + writeTempRoom(t, dir, 3, "name: Tower Two Base\nexits:\n up: 4\n west: 2\n") + writeTempRoom(t, dir, 4, "name: Tower Two\nexits:\n down: 3\n") + + g := &Game{Deps: Deps{World: world.New(dir)}, Flags: NewFlagStore()} + mg := mapGlyphsForPlayer(false) + + // Player has only visited tower 1. Tower 2 should appear but dimmed (unvisited). + sess := &net.Session{Player: &player.Player{ + Stats: player.PlayerStats{RoomsVisited: map[int]bool{1: true}}, + Flags: map[string]any{}, + }} + + out := strings.Join(buildTinyMap(g, sess, 1, mg), "\n") + + // Tower 2 at (1,0,0) should appear on the same z-level map. + // Unvisited rooms render as □ (unicode) or o (ASCII). + if !strings.Contains(out, "□") && !strings.Contains(out, "o") { + t.Errorf("expected tower 2 (unvisited) to appear on the map:\n%s", out) + } +} + +func TestMap3DDifferentZExcluded(t *testing.T) { + // Room 1 at (0,0,0) with up to room 2 at (0,0,1). Room 2 is at z=1, + // different from the player's z=0 level — should NOT show on the map. + dir := t.TempDir() + writeTempRoom(t, dir, 1, "name: Ground\nexits:\n up: 2\n") + writeTempRoom(t, dir, 2, "name: Upper\nexits:\n down: 1\n") + + g := &Game{Deps: Deps{World: world.New(dir)}, Flags: NewFlagStore()} + mg := mapGlyphsForPlayer(false) + sess := &net.Session{Player: &player.Player{ + Stats: player.PlayerStats{RoomsVisited: map[int]bool{1: true, 2: true}}, + Flags: map[string]any{}, + }} + + out := strings.Join(buildTinyMap(g, sess, 1, mg), "\n") + + // Room 2 is at a different z — it should NOT render as a room symbol. + // Unvisited rooms render as □ (unicode) or o (ASCII). + if strings.Contains(out, "□") || strings.Contains(out, " o ") { + t.Errorf("room at different z should not appear:\n%s", out) + } +} diff --git a/internal/game/craft_index.go b/internal/game/production_index.go index 648ccc8..648ccc8 100644 --- a/internal/game/craft_index.go +++ b/internal/game/production_index.go diff --git a/internal/game/render_map.go b/internal/game/render_map.go index 6b80688..b210e77 100644 --- a/internal/game/render_map.go +++ b/internal/game/render_map.go @@ -47,59 +47,19 @@ type mapCell struct { } type mapGraph struct { - posToRoom map[[2]int]int - roomToPos map[int][2]int + posToRoom map[[3]int]int dist map[int]int } +// The map BFS always seeds the player's room at the grid origin, so the +// player's z-plane is constant. +const playerZ = 0 - -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 { - roomID int - x, y int - } - 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] - queue = queue[1:] - - room, ok := loadRoom(g, n.roomID) - if !ok { - continue - } - - if visited != nil && !visited[n.roomID] { - continue - } - - for dir, delta := range world.DirectionDeltas { - targetID, ok := exitTarget(room, dir) - if !ok { - continue - } - if _, seen := mg.roomToPos[targetID]; seen { - continue - } - nx, ny := n.x+delta[0], n.y+delta[1] - 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}) - } - } - - return mg +func buildGraph(g *Game, startRoomID int) *mapGraph { + rg := world.BuildGrid(startRoomID, func(id int) (*world.Room, bool) { + return loadRoom(g, id) + }, nil, nil) + return &mapGraph{posToRoom: rg.RoomAt, dist: rg.Dist} } func renderMapCells(grid [][]mapCell, colorMode string, startRow, endRow int, border rune) []string { @@ -128,7 +88,7 @@ func renderMapCells(grid [][]mapCell, colorMode string, startRow, endRow int, bo func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string { visited := roomsVisited(sess) - bg := buildGraph(g, roomID, visited) + bg := buildGraph(g, roomID) colorMode := colorModeFor(sess) atSpec := resolveMapAt(g, sess) @@ -147,9 +107,10 @@ func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string } } + pz := playerZ for y := -1; y <= 1; y++ { for x := -1; x <= 1; x++ { - pos := [2]int{x, y} + pos := [3]int{x, y, pz} rid, ok := bg.posToRoom[pos] if !ok { continue @@ -171,8 +132,8 @@ func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string for y := -1; y <= 1; y++ { for x := -1; x <= 0; x++ { - leftPos := [2]int{x, y} - rightPos := [2]int{x + 1, y} + leftPos := [3]int{x, y, pz} + rightPos := [3]int{x + 1, y, pz} leftRoom, leftOK := bg.posToRoom[leftPos] rightRoom, rightOK := bg.posToRoom[rightPos] if !leftOK || !rightOK { @@ -186,8 +147,8 @@ func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string for y := -1; y <= 0; y++ { for x := -1; x <= 1; x++ { - topPos := [2]int{x, y} - bottomPos := [2]int{x, y + 1} + topPos := [3]int{x, y, pz} + bottomPos := [3]int{x, y + 1, pz} topRoom, topOK := bg.posToRoom[topPos] bottomRoom, bottomOK := bg.posToRoom[bottomPos] if !topOK || !bottomOK { @@ -199,119 +160,50 @@ func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string } } - // NE connectors: (x,y) -> (x+1, y-1), connector at grid[2*y+1][2*x+3] + // Diagonal connectors. Each maps room (x,y) to a diagonal neighbor; the + // grid cell between them is 2*y/2*x offset by ±1 in each axis. for y := 0; y <= 1; y++ { for x := -1; x <= 0; x++ { - aPos, bPos := [2]int{x, y}, [2]int{x + 1, y - 1} - aRoom, aOK := bg.posToRoom[aPos] - bRoom, bOK := bg.posToRoom[bPos] - if !aOK || !bOK { - continue - } - if cell, ok := ctx.connectorCell(aRoom, bRoom, world.Northeast, world.Southwest); ok { - gr, gc := 2*y+1, 2*x+3 - key := [2]int{gr, gc} - if isDiagonalGlyph(grid[gr][gc].char) { - if grid[gr][gc].char != cell.char { - prev := ctx.diagPairs[key] - spec := color.Average( - color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), - color.Average(ctx.nodeSpec(aRoom), ctx.nodeSpec(bRoom)), - ) - grid[gr][gc] = mapCell{char: 'X', spec: spec} - } - } else { - grid[gr][gc] = cell - ctx.diagPairs[key] = [2]int{aRoom, bRoom} - } + // NE: (x,y) -> (x+1, y-1), connector at grid[2*y+1][2*x+3] + aRoom, aOK := bg.posToRoom[[3]int{x, y, pz}] + bRoom, bOK := bg.posToRoom[[3]int{x + 1, y - 1, pz}] + if aOK && bOK { + ctx.placeDiagonal(grid, 2*y+1, 2*x+3, aRoom, bRoom, world.Northeast, world.Southwest) } } } - // NW connectors: (x,y) -> (x-1, y-1), connector at grid[2*y+1][2*x+1] for y := 0; y <= 1; y++ { for x := 0; x <= 1; x++ { - aPos, bPos := [2]int{x, y}, [2]int{x - 1, y - 1} - aRoom, aOK := bg.posToRoom[aPos] - bRoom, bOK := bg.posToRoom[bPos] - if !aOK || !bOK { - continue - } - if cell, ok := ctx.connectorCell(aRoom, bRoom, world.Northwest, world.Southeast); ok { - gr, gc := 2*y+1, 2*x+1 - key := [2]int{gr, gc} - if isDiagonalGlyph(grid[gr][gc].char) { - if grid[gr][gc].char != cell.char { - prev := ctx.diagPairs[key] - spec := color.Average( - color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), - color.Average(ctx.nodeSpec(aRoom), ctx.nodeSpec(bRoom)), - ) - grid[gr][gc] = mapCell{char: 'X', spec: spec} - } - } else { - grid[gr][gc] = cell - ctx.diagPairs[key] = [2]int{aRoom, bRoom} - } + // NW: (x,y) -> (x-1, y-1), connector at grid[2*y+1][2*x+1] + aRoom, aOK := bg.posToRoom[[3]int{x, y, pz}] + bRoom, bOK := bg.posToRoom[[3]int{x - 1, y - 1, pz}] + if aOK && bOK { + ctx.placeDiagonal(grid, 2*y+1, 2*x+1, aRoom, bRoom, world.Northwest, world.Southeast) } } } - // SE connectors: (x,y) -> (x+1, y+1), connector at grid[2*y+3][2*x+3] for y := -1; y <= 0; y++ { for x := -1; x <= 0; x++ { - aPos, bPos := [2]int{x, y}, [2]int{x + 1, y + 1} - aRoom, aOK := bg.posToRoom[aPos] - bRoom, bOK := bg.posToRoom[bPos] - if !aOK || !bOK { - continue - } - if cell, ok := ctx.connectorCell(aRoom, bRoom, world.Southeast, world.Northwest); ok { - gr, gc := 2*y+3, 2*x+3 - key := [2]int{gr, gc} - if isDiagonalGlyph(grid[gr][gc].char) { - if grid[gr][gc].char != cell.char { - prev := ctx.diagPairs[key] - spec := color.Average( - color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), - color.Average(ctx.nodeSpec(aRoom), ctx.nodeSpec(bRoom)), - ) - grid[gr][gc] = mapCell{char: 'X', spec: spec} - } - } else { - grid[gr][gc] = cell - ctx.diagPairs[key] = [2]int{aRoom, bRoom} - } + // SE: (x,y) -> (x+1, y+1), connector at grid[2*y+3][2*x+3] + aRoom, aOK := bg.posToRoom[[3]int{x, y, pz}] + bRoom, bOK := bg.posToRoom[[3]int{x + 1, y + 1, pz}] + if aOK && bOK { + ctx.placeDiagonal(grid, 2*y+3, 2*x+3, aRoom, bRoom, world.Southeast, world.Northwest) } } } - // SW connectors: (x,y) -> (x-1, y+1), connector at grid[2*y+3][2*x+1] for y := -1; y <= 0; y++ { for x := 0; x <= 1; x++ { - aPos, bPos := [2]int{x, y}, [2]int{x - 1, y + 1} - aRoom, aOK := bg.posToRoom[aPos] - bRoom, bOK := bg.posToRoom[bPos] - if !aOK || !bOK { - continue - } - if cell, ok := ctx.connectorCell(aRoom, bRoom, world.Southwest, world.Northeast); ok { - gr, gc := 2*y+3, 2*x+1 - key := [2]int{gr, gc} - if isDiagonalGlyph(grid[gr][gc].char) { - if grid[gr][gc].char != cell.char { - prev := ctx.diagPairs[key] - spec := color.Average( - color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), - color.Average(ctx.nodeSpec(aRoom), ctx.nodeSpec(bRoom)), - ) - grid[gr][gc] = mapCell{char: 'X', spec: spec} - } - } else { - grid[gr][gc] = cell - ctx.diagPairs[key] = [2]int{aRoom, bRoom} - } + // SW: (x,y) -> (x-1, y+1), connector at grid[2*y+3][2*x+1] + aRoom, aOK := bg.posToRoom[[3]int{x, y, pz}] + bRoom, bOK := bg.posToRoom[[3]int{x - 1, y + 1, pz}] + if aOK && bOK { + ctx.placeDiagonal(grid, 2*y+3, 2*x+1, aRoom, bRoom, world.Southwest, world.Northeast) } } } + cur, _ := loadRoom(g, roomID) if cur != nil { if upTarget, hasUp := exitTarget(cur, world.Up); hasUp { @@ -354,7 +246,7 @@ func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string func buildFullMap(g *Game, sess *net.Session, roomID, mapWidth, mapHeight int, mg mapGlyphs) []string { visited := roomsVisited(sess) - bg := buildGraph(g, roomID, visited) + bg := buildGraph(g, roomID) colorMode := colorModeFor(sess) atSpec := resolveMapAt(g, sess) @@ -375,8 +267,12 @@ func buildFullMap(g *Game, sess *net.Session, roomID, mapWidth, mapHeight int, m cx := mapWidth / 2 cy := mapHeight / 2 + pz := playerZ for pos, rid := range bg.posToRoom { + if pos[2] != pz { + continue + } gr := cy + pos[1]*2 gc := cx + pos[0]*2 if gr < 0 || gr >= mapHeight || gc < 0 || gc >= mapWidth { @@ -395,9 +291,12 @@ func buildFullMap(g *Game, sess *net.Session, roomID, mapWidth, mapHeight int, m } for pos, rid := range bg.posToRoom { + if pos[2] != pz { + continue + } x, y := pos[0], pos[1] - if rightID, exists := bg.posToRoom[[2]int{x + 1, y}]; exists { + if rightID, exists := bg.posToRoom[[3]int{x + 1, y, pz}]; exists { gr := cy + y*2 gc := cx + x*2 + 1 if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { @@ -407,7 +306,7 @@ func buildFullMap(g *Game, sess *net.Session, roomID, mapWidth, mapHeight int, m } } - if bottomID, exists := bg.posToRoom[[2]int{x, y + 1}]; exists { + if bottomID, exists := bg.posToRoom[[3]int{x, y + 1, pz}]; exists { gr := cy + y*2 + 1 gc := cx + x*2 if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { @@ -417,96 +316,20 @@ func buildFullMap(g *Game, sess *net.Session, roomID, mapWidth, mapHeight int, m } } - if neID, exists := bg.posToRoom[[2]int{x + 1, y - 1}]; exists { - gr := cy + y*2 - 1 - gc := cx + x*2 + 1 - if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { - if cell, ok := ctx.connectorCell(rid, neID, world.Northeast, world.Southwest); ok { - key := [2]int{gr, gc} - if isDiagonalGlyph(grid[gr][gc].char) { - if grid[gr][gc].char != cell.char { - prev := ctx.diagPairs[key] - spec := color.Average( - color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), - color.Average(ctx.nodeSpec(rid), ctx.nodeSpec(neID)), - ) - grid[gr][gc] = mapCell{char: 'X', spec: spec} - } - } else { - grid[gr][gc] = cell - ctx.diagPairs[key] = [2]int{rid, neID} - } - } - } + if neID, exists := bg.posToRoom[[3]int{x + 1, y - 1, pz}]; exists { + ctx.placeDiagonal(grid, cy+y*2-1, cx+x*2+1, rid, neID, world.Northeast, world.Southwest) } - if nwID, exists := bg.posToRoom[[2]int{x - 1, y - 1}]; exists { - gr := cy + y*2 - 1 - gc := cx + x*2 - 1 - if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { - if cell, ok := ctx.connectorCell(rid, nwID, world.Northwest, world.Southeast); ok { - key := [2]int{gr, gc} - if isDiagonalGlyph(grid[gr][gc].char) { - if grid[gr][gc].char != cell.char { - prev := ctx.diagPairs[key] - spec := color.Average( - color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), - color.Average(ctx.nodeSpec(rid), ctx.nodeSpec(nwID)), - ) - grid[gr][gc] = mapCell{char: 'X', spec: spec} - } - } else { - grid[gr][gc] = cell - ctx.diagPairs[key] = [2]int{rid, nwID} - } - } - } + if nwID, exists := bg.posToRoom[[3]int{x - 1, y - 1, pz}]; exists { + ctx.placeDiagonal(grid, cy+y*2-1, cx+x*2-1, rid, nwID, world.Northwest, world.Southeast) } - if seID, exists := bg.posToRoom[[2]int{x + 1, y + 1}]; exists { - gr := cy + y*2 + 1 - gc := cx + x*2 + 1 - if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { - if cell, ok := ctx.connectorCell(rid, seID, world.Southeast, world.Northwest); ok { - key := [2]int{gr, gc} - if isDiagonalGlyph(grid[gr][gc].char) { - if grid[gr][gc].char != cell.char { - prev := ctx.diagPairs[key] - spec := color.Average( - color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), - color.Average(ctx.nodeSpec(rid), ctx.nodeSpec(seID)), - ) - grid[gr][gc] = mapCell{char: 'X', spec: spec} - } - } else { - grid[gr][gc] = cell - ctx.diagPairs[key] = [2]int{rid, seID} - } - } - } + if seID, exists := bg.posToRoom[[3]int{x + 1, y + 1, pz}]; exists { + ctx.placeDiagonal(grid, cy+y*2+1, cx+x*2+1, rid, seID, world.Southeast, world.Northwest) } - if swID, exists := bg.posToRoom[[2]int{x - 1, y + 1}]; exists { - gr := cy + y*2 + 1 - gc := cx + x*2 - 1 - if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { - if cell, ok := ctx.connectorCell(rid, swID, world.Southwest, world.Northeast); ok { - key := [2]int{gr, gc} - if isDiagonalGlyph(grid[gr][gc].char) { - if grid[gr][gc].char != cell.char { - prev := ctx.diagPairs[key] - spec := color.Average( - color.Average(ctx.nodeSpec(prev[0]), ctx.nodeSpec(prev[1])), - color.Average(ctx.nodeSpec(rid), ctx.nodeSpec(swID)), - ) - grid[gr][gc] = mapCell{char: 'X', spec: spec} - } - } else { - grid[gr][gc] = cell - ctx.diagPairs[key] = [2]int{rid, swID} - } - } - } + if swID, exists := bg.posToRoom[[3]int{x - 1, y + 1, pz}]; exists { + ctx.placeDiagonal(grid, cy+y*2+1, cx+x*2-1, rid, swID, world.Southwest, world.Northeast) } } @@ -629,6 +452,35 @@ func (c *mapRenderCtx) coloredCell(roomA, roomB int, glyph rune) (mapCell, bool) return mapCell{char: glyph, spec: color.Average(c.nodeSpec(roomA), c.nodeSpec(roomB))}, true } +// placeDiagonal draws the diagonal link between two grid-adjacent rooms at +// grid[gr][gc], handling the criss-cross case: when a diagonal glyph is already +// present and the new link runs the opposite way, the cell becomes a blocked +// 'X' colored from both crossing links' endpoints. Out-of-bounds targets and +// rooms with no link between them are no-ops. +func (c *mapRenderCtx) placeDiagonal(grid [][]mapCell, gr, gc, roomA, roomB int, dirAB, dirBA world.ExitDir) { + if gr < 0 || gr >= len(grid) || gc < 0 || gc >= len(grid[gr]) { + return + } + cell, ok := c.connectorCell(roomA, roomB, dirAB, dirBA) + if !ok { + return + } + key := [2]int{gr, gc} + if isDiagonalGlyph(grid[gr][gc].char) { + if grid[gr][gc].char != cell.char { + prev := c.diagPairs[key] + spec := color.Average( + color.Average(c.nodeSpec(prev[0]), c.nodeSpec(prev[1])), + color.Average(c.nodeSpec(roomA), c.nodeSpec(roomB)), + ) + grid[gr][gc] = mapCell{char: 'X', spec: spec} + } + return + } + grid[gr][gc] = cell + c.diagPairs[key] = [2]int{roomA, roomB} +} + type exitState int const ( @@ -763,13 +615,24 @@ func loadRoom(g *Game, roomID int) (*world.Room, bool) { } func stripBlankRows(lines []string) []string { - var out []string - for _, line := range lines { + top := -1 + for i, line := range lines { if strings.TrimSpace(line) != "" { - out = append(out, line) + top = i + break + } + } + if top < 0 { + return nil + } + bottom := top + for i := len(lines) - 1; i > bottom; i-- { + if strings.TrimSpace(lines[i]) != "" { + bottom = i + break } } - return out + return lines[top : bottom+1] } func leftTrimCommon(lines []string) []string { diff --git a/internal/game/safespot_manager.go b/internal/game/sys_safespot_state.go index 1520bc7..1520bc7 100644 --- a/internal/game/safespot_manager.go +++ b/internal/game/sys_safespot_state.go diff --git a/internal/game/triggers.go b/internal/game/sys_triggers.go index cd1bbd3..cd1bbd3 100644 --- a/internal/game/triggers.go +++ b/internal/game/sys_triggers.go |
