aboutsummaryrefslogtreecommitdiff
path: root/internal/game/ui_map.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-22 04:24:11 -0400
committerhistoria <[not public]>2026-06-22 04:24:11 -0400
commit5b9a75a1520a198c9c6b1f1f55e05c9f4aab5629 (patch)
tree18f500ca3492ba206a3f66e2f01146b05a1675b2 /internal/game/ui_map.go
parent7f07c7236578e1a7bcef0282ff9d7f5bf8c7c045 (diff)
downloadthehouseoficarus-5b9a75a1520a198c9c6b1f1f55e05c9f4aab5629.tar.gz
feat: map now only reveals with exploration. players can set custom map symbols.
Diffstat (limited to 'internal/game/ui_map.go')
-rw-r--r--internal/game/ui_map.go287
1 files changed, 199 insertions, 88 deletions
diff --git a/internal/game/ui_map.go b/internal/game/ui_map.go
index 32d3d7c..a07606f 100644
--- a/internal/game/ui_map.go
+++ b/internal/game/ui_map.go
@@ -2,7 +2,10 @@ package game
import (
"strings"
+ "unicode/utf8"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/net"
"thehouseoficarus/internal/world"
)
@@ -19,7 +22,7 @@ func mapGlyphsForPlayer(unicode bool) mapGlyphs {
if unicode {
return mapGlyphs{
topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
- side: '║', topFill: '═', connectorH: '─', connectorV: '│',
+ side: '║', topFill: '═', connectorH: '-', connectorV: '│',
upArrow: '↑', downArrow: '↓',
}
}
@@ -30,6 +33,11 @@ func mapGlyphsForPlayer(unicode bool) mapGlyphs {
}
}
+type mapCell struct {
+ char rune
+ spec color.ColorSpec
+}
+
type mapGraph struct {
posToRoom map[[2]int]int
roomToPos map[int][2]int
@@ -45,7 +53,7 @@ var bfsDirs = []struct {
{world.West, -1, 0},
}
-func buildGraph(g *Game, startRoomID int) *mapGraph {
+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),
@@ -68,15 +76,19 @@ func buildGraph(g *Game, startRoomID int) *mapGraph {
continue
}
+ if visited != nil && !visited[n.roomID] {
+ continue
+ }
+
for _, d := range bfsDirs {
targetID, ok := exitTarget(room, d.dir)
if !ok {
continue
}
- if _, visited := mg.roomToPos[targetID]; visited {
+ if _, seen := mg.roomToPos[targetID]; seen {
continue
}
- nx, ny := n.x + d.dx, n.y + d.dy
+ 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})
@@ -86,14 +98,43 @@ func buildGraph(g *Game, startRoomID int) *mapGraph {
return mg
}
-func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string {
- bg := buildGraph(g, roomID)
+func renderMapCells(grid [][]mapCell, colorMode string, startRow, endRow int, border rune) []string {
+ lines := make([]string, 0, endRow-startRow)
+ for row := startRow; row < endRow; row++ {
+ var sb strings.Builder
+ if border != 0 {
+ sb.WriteRune(border)
+ }
+ for _, cell := range grid[row] {
+ if cell.char == ' ' {
+ sb.WriteRune(' ')
+ } else if !cell.spec.Empty() {
+ sb.WriteString(color.Render(colorMode, cell.spec, string(cell.char)))
+ } else {
+ sb.WriteRune(cell.char)
+ }
+ }
+ if border != 0 {
+ sb.WriteRune(border)
+ }
+ lines = append(lines, sb.String())
+ }
+ return lines
+}
+
+func buildTinyMap(g *Game, sess *net.Session, roomID int, mg mapGlyphs) []string {
+ visited := roomsVisited(sess)
+ bg := buildGraph(g, roomID, visited)
- grid := make([][]rune, 5)
+ colorMode := colorModeFor(sess)
+ atSpec := resolveMapAt(g, sess)
+ dimSpec := resolveDim(g, sess)
+
+ grid := make([][]mapCell, 5)
for i := range grid {
- grid[i] = make([]rune, 5)
+ grid[i] = make([]mapCell, 5)
for j := range grid[i] {
- grid[i][j] = ' '
+ grid[i][j] = mapCell{char: ' '}
}
}
@@ -107,9 +148,14 @@ func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string {
gr := (y + 1) * 2
gc := (x + 1) * 2
if rid == roomID {
- grid[gr][gc] = '@'
+ grid[gr][gc] = mapCell{char: '@', spec: atSpec}
} else {
- grid[gr][gc] = roomMapSymbol(g, rid)
+ unvisited := visited != nil && !visited[rid]
+ ch, spec := roomMapSymbol(g, sess, rid, unvisited)
+ if unvisited {
+ spec = dimSpec
+ }
+ grid[gr][gc] = mapCell{char: ch, spec: spec}
}
}
}
@@ -124,7 +170,11 @@ func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string {
continue
}
if exitsConnect(g, leftRoom, rightRoom, world.East, world.West) {
- grid[(y+1)*2][(x+1)*2+1] = mg.connectorH
+ connSpec := color.NoColor()
+ if visited != nil && (!visited[leftRoom] || !visited[rightRoom]) {
+ connSpec = dimSpec
+ }
+ grid[(y+1)*2][(x+1)*2+1] = mapCell{char: mg.connectorH, spec: connSpec}
}
}
}
@@ -139,7 +189,11 @@ func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string {
continue
}
if exitsConnect(g, topRoom, bottomRoom, world.South, world.North) {
- grid[(y+1)*2+1][(x+1)*2] = mg.connectorV
+ connSpec := color.NoColor()
+ if visited != nil && (!visited[topRoom] || !visited[bottomRoom]) {
+ connSpec = dimSpec
+ }
+ grid[(y+1)*2+1][(x+1)*2] = mapCell{char: mg.connectorV, spec: connSpec}
}
}
}
@@ -147,25 +201,152 @@ func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string {
cur, _ := loadRoom(g, roomID)
if cur != nil {
if _, ok := exitTarget(cur, world.Up); ok {
- grid[1][3] = mg.upArrow
+ grid[1][3] = mapCell{char: mg.upArrow, spec: color.NoColor()}
}
if _, ok := exitTarget(cur, world.Down); ok {
- grid[3][1] = mg.downArrow
+ grid[3][1] = mapCell{char: mg.downArrow, spec: color.NoColor()}
}
}
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)
- }
+ inner := renderMapCells(grid, colorMode, 0, 5, mg.side)
+ copy(lines[1:], inner)
botFill := strings.Repeat(string(mg.topFill), 5)
lines[6] = string(mg.bottomLeft) + botFill + string(mg.bottomRight)
return lines
}
+func buildFullMap(g *Game, sess *net.Session, roomID, mapWidth, mapHeight int, mg mapGlyphs) []string {
+ visited := roomsVisited(sess)
+ bg := buildGraph(g, roomID, visited)
+
+ colorMode := colorModeFor(sess)
+ atSpec := resolveMapAt(g, sess)
+ dimSpec := resolveDim(g, sess)
+
+ grid := make([][]mapCell, mapHeight)
+ for i := range grid {
+ grid[i] = make([]mapCell, mapWidth)
+ for j := range grid[i] {
+ grid[i][j] = mapCell{char: ' '}
+ }
+ }
+
+ 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] = mapCell{char: '@', spec: atSpec}
+ } else {
+ unvisited := visited != nil && !visited[rid]
+ ch, spec := roomMapSymbol(g, sess, rid, unvisited)
+ if unvisited {
+ spec = dimSpec
+ }
+ grid[gr][gc] = mapCell{char: ch, spec: spec}
+ }
+ }
+
+ 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 {
+ connSpec := color.NoColor()
+ if visited != nil && (!visited[rid] || !visited[rightID]) {
+ connSpec = dimSpec
+ }
+ grid[gr][gc] = mapCell{char: mg.connectorH, spec: connSpec}
+ }
+ }
+ }
+
+ 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 {
+ connSpec := color.NoColor()
+ if visited != nil && (!visited[rid] || !visited[bottomID]) {
+ connSpec = dimSpec
+ }
+ grid[gr][gc] = mapCell{char: mg.connectorV, spec: connSpec}
+ }
+ }
+ }
+ }
+
+ return renderMapCells(grid, colorMode, 0, mapHeight, 0)
+}
+
+func roomsVisited(sess *net.Session) map[int]bool {
+ if sess != nil && sess.Player != nil {
+ return sess.Player.Stats.RoomsVisited
+ }
+ return nil
+}
+
+func colorModeFor(sess *net.Session) string {
+ if sess != nil && sess.Player != nil {
+ return sess.Player.OptionString("color")
+ }
+ return "none"
+}
+
+func resolveMapAt(g *Game, sess *net.Session) color.ColorSpec {
+ if sess != nil {
+ return g.resolveColor(sess, "map_at")
+ }
+ return color.NoColor()
+}
+
+func resolveDim(g *Game, sess *net.Session) color.ColorSpec {
+ if sess != nil {
+ return g.resolveColor(sess, "dim")
+ }
+ return color.Parse("243 dim")
+}
+
+func roomMapSymbol(g *Game, sess *net.Session, roomID int, unvisited bool) (rune, color.ColorSpec) {
+ if sess != nil && sess.Player != nil {
+ if data, ok := sess.Player.MapSymbols[roomID]; ok {
+ r, size := utf8.DecodeRuneInString(data.Char)
+ if size > 0 && r != utf8.RuneError {
+ spec := color.NoColor()
+ if data.Color != "" {
+ spec = color.Parse(data.Color)
+ }
+ // ponytail: non-ASCII custom symbols fall back to 'o'
+ // when unicode mode is off, but preserve the color.
+ if !sess.Player.OptionBool("unicode") && r > 127 {
+ return 'o', spec
+ }
+ return r, spec
+ }
+ }
+ if sess.Player.OptionBool("unicode") {
+ if unvisited {
+ return '□', color.NoColor()
+ }
+ return '■', color.NoColor()
+ }
+ return 'o', color.NoColor()
+ }
+ return '■', color.NoColor()
+}
+
func exitsConnect(g *Game, room1, room2 int, dir12, dir21 world.ExitDir) bool {
r1, ok := loadRoom(g, room1)
if !ok {
@@ -204,18 +385,6 @@ func loadRoom(g *Game, roomID int) (*world.Room, bool) {
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 {
@@ -257,61 +426,3 @@ func leftTrimCommon(lines []string) []string {
}
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
-}