aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-29 18:13:33 -0400
committerhistoria <[not public]>2026-06-29 18:13:33 -0400
commit7d76673fa0707d204c7d084c8f90c8133c22a31a (patch)
tree6fc832b9f3fde6ed455abb535f6e28b0e5db1fd9 /internal/game
parentfeb80b7dde200d4121cd9f9c583a08f9869c5588 (diff)
downloadthehouseoficarus-7d76673fa0707d204c7d084c8f90c8133c22a31a.tar.gz
feat: migrated map (and bfs) to full 3D instead of individual 2D maps on different planes.
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/cmd_dig.go55
-rw-r--r--internal/game/cmd_room_insert.go56
-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.go69
-rw-r--r--internal/game/production_index.go (renamed from internal/game/craft_index.go)0
-rw-r--r--internal/game/render_map.go339
-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