aboutsummaryrefslogtreecommitdiff
path: root/internal/game/ui_map.go
diff options
context:
space:
mode:
authorworkhorse <workhorse@localhost.localdomain>2026-06-19 20:24:52 -0400
committerworkhorse <workhorse@localhost.localdomain>2026-06-19 20:24:52 -0400
commit8ee8982b8759bcae116647cc993867f4c8b0a6ce (patch)
treeee01f7b1d745cbc94d9981108a1209527487b3e5 /internal/game/ui_map.go
parent2bec24859af8f03c80a0a58e3240519942450167 (diff)
downloadthehouseoficarus-8ee8982b8759bcae116647cc993867f4c8b0a6ce.tar.gz
reorganized items, objects, and rooms in directories. oranized module names. added startup checks.
Diffstat (limited to 'internal/game/ui_map.go')
-rw-r--r--internal/game/ui_map.go317
1 files changed, 317 insertions, 0 deletions
diff --git a/internal/game/ui_map.go b/internal/game/ui_map.go
new file mode 100644
index 0000000..32d3d7c
--- /dev/null
+++ b/internal/game/ui_map.go
@@ -0,0 +1,317 @@
+package game
+
+import (
+ "strings"
+
+ "thehouseoficarus/internal/world"
+)
+
+type mapGlyphs struct {
+ topLeft, topRight rune
+ bottomLeft, bottomRight rune
+ side rune
+ topFill rune
+ connectorH, connectorV rune
+ upArrow, downArrow rune
+}
+
+func mapGlyphsForPlayer(unicode bool) mapGlyphs {
+ if unicode {
+ return mapGlyphs{
+ topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
+ side: '║', topFill: '═', connectorH: '─', connectorV: '│',
+ upArrow: '↑', downArrow: '↓',
+ }
+ }
+ return mapGlyphs{
+ topLeft: '.', topRight: '.', bottomLeft: ':', bottomRight: ':',
+ side: ':', topFill: '.', connectorH: '-', connectorV: '|',
+ upArrow: '^', downArrow: 'v',
+ }
+}
+
+type mapGraph struct {
+ posToRoom map[[2]int]int
+ roomToPos map[int][2]int
+}
+
+var bfsDirs = []struct {
+ dir world.ExitDir
+ dx, dy int
+}{
+ {world.North, 0, -1},
+ {world.South, 0, 1},
+ {world.East, 1, 0},
+ {world.West, -1, 0},
+}
+
+func buildGraph(g *Game, startRoomID int) *mapGraph {
+ mg := &mapGraph{
+ posToRoom: make(map[[2]int]int),
+ roomToPos: make(map[int][2]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}
+
+ for len(queue) > 0 {
+ n := queue[0]
+ queue = queue[1:]
+
+ room, ok := loadRoom(g, n.roomID)
+ if !ok {
+ continue
+ }
+
+ for _, d := range bfsDirs {
+ targetID, ok := exitTarget(room, d.dir)
+ if !ok {
+ continue
+ }
+ if _, visited := mg.roomToPos[targetID]; visited {
+ continue
+ }
+ nx, ny := n.x + d.dx, n.y + d.dy
+ mg.posToRoom[[2]int{nx, ny}] = targetID
+ mg.roomToPos[targetID] = [2]int{nx, ny}
+ queue = append(queue, node{targetID, nx, ny})
+ }
+ }
+
+ return mg
+}
+
+func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string {
+ bg := buildGraph(g, roomID)
+
+ grid := make([][]rune, 5)
+ for i := range grid {
+ grid[i] = make([]rune, 5)
+ for j := range grid[i] {
+ grid[i][j] = ' '
+ }
+ }
+
+ for y := -1; y <= 1; y++ {
+ for x := -1; x <= 1; x++ {
+ pos := [2]int{x, y}
+ rid, ok := bg.posToRoom[pos]
+ if !ok {
+ continue
+ }
+ gr := (y + 1) * 2
+ gc := (x + 1) * 2
+ if rid == roomID {
+ grid[gr][gc] = '@'
+ } else {
+ grid[gr][gc] = roomMapSymbol(g, rid)
+ }
+ }
+ }
+
+ for y := -1; y <= 1; y++ {
+ for x := -1; x <= 0; x++ {
+ leftPos := [2]int{x, y}
+ rightPos := [2]int{x + 1, y}
+ leftRoom, leftOK := bg.posToRoom[leftPos]
+ rightRoom, rightOK := bg.posToRoom[rightPos]
+ if !leftOK || !rightOK {
+ continue
+ }
+ if exitsConnect(g, leftRoom, rightRoom, world.East, world.West) {
+ grid[(y+1)*2][(x+1)*2+1] = mg.connectorH
+ }
+ }
+ }
+
+ for y := -1; y <= 0; y++ {
+ for x := -1; x <= 1; x++ {
+ topPos := [2]int{x, y}
+ bottomPos := [2]int{x, y + 1}
+ topRoom, topOK := bg.posToRoom[topPos]
+ bottomRoom, bottomOK := bg.posToRoom[bottomPos]
+ if !topOK || !bottomOK {
+ continue
+ }
+ if exitsConnect(g, topRoom, bottomRoom, world.South, world.North) {
+ grid[(y+1)*2+1][(x+1)*2] = mg.connectorV
+ }
+ }
+ }
+
+ cur, _ := loadRoom(g, roomID)
+ if cur != nil {
+ if _, ok := exitTarget(cur, world.Up); ok {
+ grid[1][3] = mg.upArrow
+ }
+ if _, ok := exitTarget(cur, world.Down); ok {
+ grid[3][1] = mg.downArrow
+ }
+ }
+
+ topFill := strings.Repeat(string(mg.topFill), 5)
+ lines := make([]string, 7)
+ lines[0] = string(mg.topLeft) + topFill + string(mg.topRight)
+ for row := 0; row < 5; row++ {
+ lines[row+1] = string(mg.side) + string(grid[row]) + string(mg.side)
+ }
+ botFill := strings.Repeat(string(mg.topFill), 5)
+ lines[6] = string(mg.bottomLeft) + botFill + string(mg.bottomRight)
+
+ return lines
+}
+
+func exitsConnect(g *Game, room1, room2 int, dir12, dir21 world.ExitDir) bool {
+ r1, ok := loadRoom(g, room1)
+ if !ok {
+ return false
+ }
+ if id, ok := exitTarget(r1, dir12); ok && id == room2 {
+ return true
+ }
+ r2, ok := loadRoom(g, room2)
+ if !ok {
+ return false
+ }
+ id, ok := exitTarget(r2, dir21)
+ return ok && id == room1
+}
+
+func exitTarget(room *world.Room, dir world.ExitDir) (int, bool) {
+ if room == nil {
+ return 0, false
+ }
+ exit, ok := room.Exits[dir]
+ if !ok {
+ return 0, false
+ }
+ return exit.Room, true
+}
+
+func loadRoom(g *Game, roomID int) (*world.Room, bool) {
+ if roomID == 0 {
+ return nil, false
+ }
+ room, err := g.World.LoadRoom(roomID)
+ if err != nil {
+ return nil, false
+ }
+ return room, true
+}
+
+func roomMapSymbol(g *Game, roomID int) rune {
+ room, ok := loadRoom(g, roomID)
+ if !ok {
+ return '?'
+ }
+ if room.MapSymbol != "" {
+ runes := []rune(room.MapSymbol)
+ return runes[0]
+ }
+ return 'o'
+}
+
+func stripBlankRows(lines []string) []string {
+ var out []string
+ for _, line := range lines {
+ if strings.TrimSpace(line) != "" {
+ out = append(out, line)
+ }
+ }
+ return out
+}
+
+func leftTrimCommon(lines []string) []string {
+ min := -1
+ for _, line := range lines {
+ if strings.TrimSpace(line) == "" {
+ continue
+ }
+ n := 0
+ for _, r := range line {
+ if r == ' ' {
+ n++
+ } else {
+ break
+ }
+ }
+ if min < 0 || n < min {
+ min = n
+ }
+ }
+ if min <= 0 {
+ return lines
+ }
+ result := make([]string, len(lines))
+ for i, line := range lines {
+ if len(line) <= min {
+ result[i] = ""
+ } else {
+ result[i] = line[min:]
+ }
+ }
+ return result
+}
+
+func buildFullMap(g *Game, roomID, mapWidth, mapHeight int, mg mapGlyphs) []string {
+ bg := buildGraph(g, roomID)
+
+ grid := make([][]rune, mapHeight)
+ for i := range grid {
+ grid[i] = make([]rune, mapWidth)
+ for j := range grid[i] {
+ grid[i][j] = ' '
+ }
+ }
+
+ cx := mapWidth / 2
+ cy := mapHeight / 2
+
+ for pos, rid := range bg.posToRoom {
+ gr := cy + pos[1]*2
+ gc := cx + pos[0]*2
+ if gr < 0 || gr >= mapHeight || gc < 0 || gc >= mapWidth {
+ continue
+ }
+ if rid == roomID {
+ grid[gr][gc] = '@'
+ } else {
+ grid[gr][gc] = roomMapSymbol(g, rid)
+ }
+ }
+
+ for pos, rid := range bg.posToRoom {
+ x, y := pos[0], pos[1]
+
+ if rightID, ok := bg.posToRoom[[2]int{x + 1, y}]; ok {
+ if exitsConnect(g, rid, rightID, world.East, world.West) {
+ gr := cy + y*2
+ gc := cx + x*2 + 1
+ if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth {
+ grid[gr][gc] = mg.connectorH
+ }
+ }
+ }
+
+ if bottomID, ok := bg.posToRoom[[2]int{x, y + 1}]; ok {
+ if exitsConnect(g, rid, bottomID, world.South, world.North) {
+ gr := cy + y*2 + 1
+ gc := cx + x*2
+ if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth {
+ grid[gr][gc] = mg.connectorV
+ }
+ }
+ }
+ }
+
+ lines := make([]string, mapHeight)
+ for i := range grid {
+ lines[i] = string(grid[i])
+ }
+ return lines
+}