diff options
| author | historia <[not public]> | 2026-06-22 04:24:11 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-22 04:24:11 -0400 |
| commit | 5b9a75a1520a198c9c6b1f1f55e05c9f4aab5629 (patch) | |
| tree | 18f500ca3492ba206a3f66e2f01146b05a1675b2 /internal/game/ui_map.go | |
| parent | 7f07c7236578e1a7bcef0282ff9d7f5bf8c7c045 (diff) | |
| download | thehouseoficarus-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.go | 287 |
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 -} |
