Files
Catacombs/dungeon/render.go
2026-03-24 14:50:40 +09:00

265 lines
6.3 KiB
Go

package dungeon
import "fmt"
type FloorTheme struct {
WallColor string
FloorColor string
Name string
}
func GetFloorTheme(floorNum int) FloorTheme {
switch {
case floorNum <= 5:
return FloorTheme{"90", "245", "Stone Halls"}
case floorNum <= 10:
return FloorTheme{"22", "28", "Mossy Caverns"}
case floorNum <= 15:
return FloorTheme{"88", "202", "Lava Depths"}
default:
return FloorTheme{"53", "129", "Shadow Realm"}
}
}
// ANSI color codes
const (
ansiReset = "\033[0m"
ansiBright = "\033[1m"
ansiDim = "\033[2m"
ansiFgWhite = "\033[97m"
ansiFgGray = "\033[90m"
ansiFgGreen = "\033[92m"
ansiFgRed = "\033[91m"
ansiFgBrRed = "\033[1;91m"
ansiFgYellow = "\033[93m"
ansiFgCyan = "\033[96m"
ansiFgMagenta = "\033[95m"
)
// roomOwnership maps each tile coordinate to the room index that contains it (-1 = none).
func roomOwnership(floor *Floor) [][]int {
owner := make([][]int, floor.Height)
for y := 0; y < floor.Height; y++ {
owner[y] = make([]int, floor.Width)
for x := 0; x < floor.Width; x++ {
owner[y][x] = -1
}
}
for i, room := range floor.Rooms {
for dy := 0; dy < room.H; dy++ {
for dx := 0; dx < room.W; dx++ {
ty := room.Y + dy
tx := room.X + dx
if ty >= 0 && ty < floor.Height && tx >= 0 && tx < floor.Width {
owner[ty][tx] = i
}
}
}
}
return owner
}
// corridorVisibility determines if a corridor tile should be visible.
// A corridor is visible if it's adjacent to a visible or visited room.
func corridorVisible(floor *Floor, owner [][]int, x, y int) Visibility {
best := Hidden
// Check neighboring tiles for room ownership
for dy := -1; dy <= 1; dy++ {
for dx := -1; dx <= 1; dx++ {
ny, nx := y+dy, x+dx
if ny >= 0 && ny < floor.Height && nx >= 0 && nx < floor.Width {
ri := owner[ny][nx]
if ri >= 0 {
v := GetRoomVisibility(floor, ri)
if v > best {
best = v
}
}
}
}
}
// Also check along the corridor path: if this corridor connects two rooms,
// it should be visible if either room is visible/visited.
// The adjacency check above handles most cases.
return best
}
// wallVisibility determines if a wall tile should be shown based on adjacent rooms.
func wallVisible(floor *Floor, owner [][]int, x, y int) Visibility {
best := Hidden
for dy := -1; dy <= 1; dy++ {
for dx := -1; dx <= 1; dx++ {
ny, nx := y+dy, x+dx
if ny >= 0 && ny < floor.Height && nx >= 0 && nx < floor.Width {
if floor.Tiles[ny][nx] == TileFloor {
ri := owner[ny][nx]
if ri >= 0 {
v := GetRoomVisibility(floor, ri)
if v > best {
best = v
}
}
}
if floor.Tiles[ny][nx] == TileCorridor {
cv := corridorVisible(floor, owner, nx, ny)
if cv > best {
best = cv
}
}
}
}
}
return best
}
// RenderFloor renders the tile map as a colored ASCII string.
func RenderFloor(floor *Floor, currentRoom int, showFog bool) string {
theme := GetFloorTheme(floor.Number)
if floor == nil || floor.Tiles == nil {
return ""
}
owner := roomOwnership(floor)
// Find room centers for content markers
type marker struct {
symbol string
color string
}
markers := make(map[[2]int]marker)
for i, room := range floor.Rooms {
cx := room.X + room.W/2
cy := room.Y + room.H/2
vis := GetRoomVisibility(floor, i)
// Show current room and its neighbors (so player knows where to go)
showRoom := !showFog || vis != Hidden
if !showRoom && currentRoom >= 0 && currentRoom < len(floor.Rooms) {
for _, n := range floor.Rooms[currentRoom].Neighbors {
if n == i {
showRoom = true
break
}
}
}
if showRoom {
sym, col := roomMarker(room, i, i == currentRoom)
markers[[2]int{cy, cx}] = marker{sym, col}
}
}
// Player position at center of current room
var playerPos [2]int
if currentRoom >= 0 && currentRoom < len(floor.Rooms) {
r := floor.Rooms[currentRoom]
playerPos = [2]int{r.Y + r.H/2, r.X + r.W/2}
}
buf := make([]byte, 0, floor.Width*floor.Height*4)
for y := 0; y < floor.Height; y++ {
for x := 0; x < floor.Width; x++ {
tile := floor.Tiles[y][x]
// Determine visibility of this tile
var vis Visibility
if showFog {
switch tile {
case TileFloor:
ri := owner[y][x]
if ri >= 0 {
vis = GetRoomVisibility(floor, ri)
} else {
vis = Hidden
}
case TileCorridor:
vis = corridorVisible(floor, owner, x, y)
case TileWall:
vis = wallVisible(floor, owner, x, y)
default:
vis = Hidden
}
} else {
vis = Visible
}
if vis == Hidden {
buf = append(buf, ' ')
continue
}
// Check for player marker
if y == playerPos[0] && x == playerPos[1] {
buf = append(buf, []byte(fmt.Sprintf("%s%s@%s", ansiBright, ansiFgGreen, ansiReset))...)
continue
}
// Check for room content marker
if m, ok := markers[[2]int{y, x}]; ok {
if vis == Visible {
buf = append(buf, []byte(fmt.Sprintf("%s%s%s", m.color, m.symbol, ansiReset))...)
} else {
buf = append(buf, []byte(fmt.Sprintf("%s%s%s", ansiFgGray, m.symbol, ansiReset))...)
}
continue
}
// Render tile
var ch byte
switch tile {
case TileWall:
ch = '#'
case TileFloor:
ch = '.'
case TileCorridor:
ch = '+'
case TileDoor:
ch = '/'
default:
ch = ' '
}
if vis == Visible {
switch tile {
case TileWall:
buf = append(buf, []byte(fmt.Sprintf("\033[38;5;%sm%c\033[0m", theme.WallColor, ch))...)
case TileFloor:
buf = append(buf, []byte(fmt.Sprintf("\033[38;5;%sm%c\033[0m", theme.FloorColor, ch))...)
default:
buf = append(buf, []byte(fmt.Sprintf("%s%s%c%s", ansiBright, ansiFgWhite, ch, ansiReset))...)
}
} else {
// Visited but not current — dim
buf = append(buf, []byte(fmt.Sprintf("%s%c%s", ansiFgGray, ch, ansiReset))...)
}
}
buf = append(buf, '\n')
}
return string(buf)
}
func roomMarker(room *Room, roomIdx int, isCurrent bool) (string, string) {
// Show room index number so player knows which key to press
num := fmt.Sprintf("%d", roomIdx)
if room.Cleared {
return num, ansiFgGray
}
switch room.Type {
case RoomCombat:
return num, ansiFgRed
case RoomTreasure:
return num, ansiFgYellow
case RoomShop:
return num, ansiFgCyan
case RoomEvent:
return num, ansiFgMagenta
case RoomBoss:
return num, ansiFgBrRed
case RoomEmpty:
return num, ansiFgGray
default:
return num, ansiReset
}
}