From abd612c15799f604e671e83dc7c410ed2b44185f Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 25 Jun 2026 15:40:48 -0400 Subject: slop refactor --- internal/game/render_map.go | 428 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 internal/game/render_map.go (limited to 'internal/game/render_map.go') diff --git a/internal/game/render_map.go b/internal/game/render_map.go new file mode 100644 index 0000000..2bac749 --- /dev/null +++ b/internal/game/render_map.go @@ -0,0 +1,428 @@ +package game + +import ( + "strings" + "unicode/utf8" + + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" + "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 mapCell struct { + char rune + spec color.ColorSpec +} + +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, visited map[int]bool) *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 + } + + if visited != nil && !visited[n.roomID] { + continue + } + + for _, d := range bfsDirs { + targetID, ok := exitTarget(room, d.dir) + if !ok { + continue + } + if _, seen := mg.roomToPos[targetID]; seen { + 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 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) + + colorMode := colorModeFor(sess) + atSpec := resolveMapAt(g, sess) + dimSpec := resolveDim(g, sess) + + grid := make([][]mapCell, 5) + for i := range grid { + grid[i] = make([]mapCell, 5) + for j := range grid[i] { + grid[i][j] = mapCell{char: ' '} + } + } + + 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] = 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 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) { + 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} + } + } + } + + 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) { + 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} + } + } + } + + cur, _ := loadRoom(g, roomID) + if cur != nil { + if _, ok := exitTarget(cur, world.Up); ok { + grid[1][3] = mapCell{char: mg.upArrow, spec: color.NoColor()} + } + if _, ok := exitTarget(cur, world.Down); ok { + 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) + 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 { + 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 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 +} -- cgit v1.2.3