feat: BSP dungeon generation with 2D ASCII tile map
Replace list-based room display with proper 2D tile map using Binary Space Partitioning. Rooms are carved into a 60x20 grid, connected by L-shaped corridors, and rendered with ANSI-colored ASCII art including fog of war visibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,41 +2,252 @@ package dungeon
|
|||||||
|
|
||||||
import "math/rand"
|
import "math/rand"
|
||||||
|
|
||||||
type Floor struct {
|
const (
|
||||||
Number int
|
MapWidth = 60
|
||||||
Rooms []*Room
|
MapHeight = 20
|
||||||
CurrentRoom int
|
MinLeafW = 12
|
||||||
|
MinLeafH = 8
|
||||||
|
MinRoomW = 6
|
||||||
|
MinRoomH = 4
|
||||||
|
RoomPad = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
type bspNode struct {
|
||||||
|
x, y, w, h int
|
||||||
|
left, right *bspNode
|
||||||
|
room *Room
|
||||||
|
roomIdx int
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateFloor(floorNum int) *Floor {
|
func GenerateFloor(floorNum int) *Floor {
|
||||||
numRooms := 5 + rand.Intn(4)
|
// Create tile map filled with walls
|
||||||
rooms := make([]*Room, numRooms)
|
tiles := make([][]Tile, MapHeight)
|
||||||
for i := 0; i < numRooms; i++ {
|
for y := 0; y < MapHeight; y++ {
|
||||||
|
tiles[y] = make([]Tile, MapWidth)
|
||||||
|
// TileWall is 0, so already initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
// BSP tree
|
||||||
|
root := &bspNode{x: 0, y: 0, w: MapWidth, h: MapHeight}
|
||||||
|
splitBSP(root, 0)
|
||||||
|
|
||||||
|
// Collect leaf nodes
|
||||||
|
var leaves []*bspNode
|
||||||
|
collectLeaves(root, &leaves)
|
||||||
|
|
||||||
|
// Shuffle leaves so room assignment is varied
|
||||||
|
rand.Shuffle(len(leaves), func(i, j int) {
|
||||||
|
leaves[i], leaves[j] = leaves[j], leaves[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
// We want 5-8 rooms. If we have more leaves, merge some; if fewer, accept it.
|
||||||
|
// Ensure at least 5 leaves by re-generating if needed (BSP should produce enough).
|
||||||
|
// Cap at 8 rooms.
|
||||||
|
targetRooms := 5 + rand.Intn(4) // 5..8
|
||||||
|
if len(leaves) > targetRooms {
|
||||||
|
leaves = leaves[:targetRooms]
|
||||||
|
}
|
||||||
|
// If we somehow have fewer than 5, that's fine — the BSP with 60x20 and min 12x8 gives ~5-8 naturally.
|
||||||
|
|
||||||
|
// Place rooms inside each leaf
|
||||||
|
rooms := make([]*Room, len(leaves))
|
||||||
|
for i, leaf := range leaves {
|
||||||
|
// Room with padding inside the leaf
|
||||||
|
maxW := leaf.w - 2*RoomPad
|
||||||
|
maxH := leaf.h - 2*RoomPad
|
||||||
|
if maxW < MinRoomW {
|
||||||
|
maxW = MinRoomW
|
||||||
|
}
|
||||||
|
if maxH < MinRoomH {
|
||||||
|
maxH = MinRoomH
|
||||||
|
}
|
||||||
|
|
||||||
|
rw := MinRoomW
|
||||||
|
if maxW > MinRoomW {
|
||||||
|
rw = MinRoomW + rand.Intn(maxW-MinRoomW+1)
|
||||||
|
}
|
||||||
|
rh := MinRoomH
|
||||||
|
if maxH > MinRoomH {
|
||||||
|
rh = MinRoomH + rand.Intn(maxH-MinRoomH+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position room within the leaf
|
||||||
|
rx := leaf.x + RoomPad
|
||||||
|
if leaf.w-2*RoomPad > rw {
|
||||||
|
rx += rand.Intn(leaf.w - 2*RoomPad - rw + 1)
|
||||||
|
}
|
||||||
|
ry := leaf.y + RoomPad
|
||||||
|
if leaf.h-2*RoomPad > rh {
|
||||||
|
ry += rand.Intn(leaf.h - 2*RoomPad - rh + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp to map bounds
|
||||||
|
if rx+rw > MapWidth-1 {
|
||||||
|
rw = MapWidth - 1 - rx
|
||||||
|
}
|
||||||
|
if ry+rh > MapHeight-1 {
|
||||||
|
rh = MapHeight - 1 - ry
|
||||||
|
}
|
||||||
|
if rx < 1 {
|
||||||
|
rx = 1
|
||||||
|
}
|
||||||
|
if ry < 1 {
|
||||||
|
ry = 1
|
||||||
|
}
|
||||||
|
|
||||||
rt := RandomRoomType()
|
rt := RandomRoomType()
|
||||||
rooms[i] = &Room{
|
rooms[i] = &Room{
|
||||||
Type: rt,
|
Type: rt,
|
||||||
X: (i % 3) * 20,
|
X: rx,
|
||||||
Y: (i / 3) * 10,
|
Y: ry,
|
||||||
Width: 12 + rand.Intn(6),
|
W: rw,
|
||||||
Height: 6 + rand.Intn(4),
|
H: rh,
|
||||||
Neighbors: []int{},
|
Neighbors: []int{},
|
||||||
}
|
}
|
||||||
|
leaf.room = rooms[i]
|
||||||
|
leaf.roomIdx = i
|
||||||
}
|
}
|
||||||
rooms[numRooms-1].Type = RoomBoss
|
|
||||||
for i := 0; i < numRooms-1; i++ {
|
// Last room is boss
|
||||||
|
rooms[len(rooms)-1].Type = RoomBoss
|
||||||
|
|
||||||
|
// Carve rooms into tile map
|
||||||
|
for _, room := range 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 < MapHeight && tx >= 0 && tx < MapWidth {
|
||||||
|
tiles[ty][tx] = TileFloor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect rooms: linear chain for guaranteed connectivity
|
||||||
|
for i := 0; i < len(rooms)-1; i++ {
|
||||||
rooms[i].Neighbors = append(rooms[i].Neighbors, i+1)
|
rooms[i].Neighbors = append(rooms[i].Neighbors, i+1)
|
||||||
rooms[i+1].Neighbors = append(rooms[i+1].Neighbors, i)
|
rooms[i+1].Neighbors = append(rooms[i+1].Neighbors, i)
|
||||||
|
carveCorridor(tiles, rooms[i], rooms[i+1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add 1-2 extra connections
|
||||||
extras := 1 + rand.Intn(2)
|
extras := 1 + rand.Intn(2)
|
||||||
for e := 0; e < extras; e++ {
|
for e := 0; e < extras; e++ {
|
||||||
a := rand.Intn(numRooms)
|
a := rand.Intn(len(rooms))
|
||||||
b := rand.Intn(numRooms)
|
b := rand.Intn(len(rooms))
|
||||||
if a != b && !hasNeighbor(rooms[a], b) {
|
if a != b && !hasNeighbor(rooms[a], b) {
|
||||||
rooms[a].Neighbors = append(rooms[a].Neighbors, b)
|
rooms[a].Neighbors = append(rooms[a].Neighbors, b)
|
||||||
rooms[b].Neighbors = append(rooms[b].Neighbors, a)
|
rooms[b].Neighbors = append(rooms[b].Neighbors, a)
|
||||||
|
carveCorridor(tiles, rooms[a], rooms[b])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Floor{
|
||||||
|
Number: floorNum,
|
||||||
|
Rooms: rooms,
|
||||||
|
CurrentRoom: 0,
|
||||||
|
Tiles: tiles,
|
||||||
|
Width: MapWidth,
|
||||||
|
Height: MapHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitBSP(node *bspNode, depth int) {
|
||||||
|
// Stop conditions
|
||||||
|
if depth > 4 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if node.w < MinLeafW*2 && node.h < MinLeafH*2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random chance to stop splitting (more likely at deeper levels)
|
||||||
|
if depth > 2 && rand.Float64() < 0.3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide split direction
|
||||||
|
horizontal := rand.Float64() < 0.5
|
||||||
|
if node.w < MinLeafW*2 {
|
||||||
|
horizontal = true
|
||||||
|
}
|
||||||
|
if node.h < MinLeafH*2 {
|
||||||
|
horizontal = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if horizontal {
|
||||||
|
if node.h < MinLeafH*2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
split := MinLeafH + rand.Intn(node.h-MinLeafH*2+1)
|
||||||
|
node.left = &bspNode{x: node.x, y: node.y, w: node.w, h: split}
|
||||||
|
node.right = &bspNode{x: node.x, y: node.y + split, w: node.w, h: node.h - split}
|
||||||
|
} else {
|
||||||
|
if node.w < MinLeafW*2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
split := MinLeafW + rand.Intn(node.w-MinLeafW*2+1)
|
||||||
|
node.left = &bspNode{x: node.x, y: node.y, w: split, h: node.h}
|
||||||
|
node.right = &bspNode{x: node.x + split, y: node.y, w: node.w - split, h: node.h}
|
||||||
|
}
|
||||||
|
|
||||||
|
splitBSP(node.left, depth+1)
|
||||||
|
splitBSP(node.right, depth+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectLeaves(node *bspNode, leaves *[]*bspNode) {
|
||||||
|
if node == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if node.left == nil && node.right == nil {
|
||||||
|
*leaves = append(*leaves, node)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
collectLeaves(node.left, leaves)
|
||||||
|
collectLeaves(node.right, leaves)
|
||||||
|
}
|
||||||
|
|
||||||
|
func carveCorridor(tiles [][]Tile, a, b *Room) {
|
||||||
|
// L-shaped corridor from center of a to center of b
|
||||||
|
ax := a.X + a.W/2
|
||||||
|
ay := a.Y + a.H/2
|
||||||
|
bx := b.X + b.W/2
|
||||||
|
by := b.Y + b.H/2
|
||||||
|
|
||||||
|
// Go horizontal first, then vertical
|
||||||
|
x := ax
|
||||||
|
for x != bx {
|
||||||
|
if y := ay; y >= 0 && y < MapHeight && x >= 0 && x < MapWidth {
|
||||||
|
if tiles[y][x] == TileWall {
|
||||||
|
tiles[y][x] = TileCorridor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if x < bx {
|
||||||
|
x++
|
||||||
|
} else {
|
||||||
|
x--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y := ay
|
||||||
|
for y != by {
|
||||||
|
if x >= 0 && x < MapWidth && y >= 0 && y < MapHeight {
|
||||||
|
if tiles[y][x] == TileWall {
|
||||||
|
tiles[y][x] = TileCorridor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if y < by {
|
||||||
|
y++
|
||||||
|
} else {
|
||||||
|
y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Place final tile
|
||||||
|
if bx >= 0 && bx < MapWidth && by >= 0 && by < MapHeight {
|
||||||
|
if tiles[by][bx] == TileWall {
|
||||||
|
tiles[by][bx] = TileCorridor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &Floor{Number: floorNum, Rooms: rooms, CurrentRoom: 0}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasNeighbor(r *Room, idx int) bool {
|
func hasNeighbor(r *Room, idx int) bool {
|
||||||
|
|||||||
@@ -40,3 +40,19 @@ func TestRoomTypeProbability(t *testing.T) {
|
|||||||
t.Errorf("Combat room probability: got %.1f%%, want ~45%%", combatPct)
|
t.Errorf("Combat room probability: got %.1f%%, want ~45%%", combatPct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFloorHasTileMap(t *testing.T) {
|
||||||
|
floor := GenerateFloor(1)
|
||||||
|
if floor.Tiles == nil {
|
||||||
|
t.Fatal("Floor should have tile map")
|
||||||
|
}
|
||||||
|
if floor.Width != 60 || floor.Height != 20 {
|
||||||
|
t.Errorf("Map size: got %dx%d, want 60x20", floor.Width, floor.Height)
|
||||||
|
}
|
||||||
|
// Current room should have floor tiles
|
||||||
|
room := floor.Rooms[0]
|
||||||
|
centerTile := floor.Tiles[room.Y+room.H/2][room.X+room.W/2]
|
||||||
|
if centerTile != TileFloor {
|
||||||
|
t.Errorf("Room center should be floor tile, got %d", centerTile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
225
dungeon/render.go
Normal file
225
dungeon/render.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package dungeon
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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)
|
||||||
|
if !showFog || vis != Hidden {
|
||||||
|
sym, col := roomMarker(room, 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 {
|
||||||
|
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, isCurrent bool) (string, string) {
|
||||||
|
if room.Cleared {
|
||||||
|
return ".", ansiFgGray
|
||||||
|
}
|
||||||
|
switch room.Type {
|
||||||
|
case RoomCombat:
|
||||||
|
return "D", ansiFgRed
|
||||||
|
case RoomTreasure:
|
||||||
|
return "$", ansiFgYellow
|
||||||
|
case RoomShop:
|
||||||
|
return "S", ansiFgCyan
|
||||||
|
case RoomEvent:
|
||||||
|
return "?", ansiFgMagenta
|
||||||
|
case RoomBoss:
|
||||||
|
return "B", ansiFgBrRed
|
||||||
|
case RoomEmpty:
|
||||||
|
return ".", ansiFgGray
|
||||||
|
default:
|
||||||
|
return " ", ansiReset
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,15 +17,33 @@ func (r RoomType) String() string {
|
|||||||
return [...]string{"Combat", "Treasure", "Shop", "Event", "Empty", "Boss"}[r]
|
return [...]string{"Combat", "Treasure", "Shop", "Event", "Empty", "Boss"}[r]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Tile int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TileWall Tile = iota
|
||||||
|
TileFloor
|
||||||
|
TileCorridor
|
||||||
|
TileDoor
|
||||||
|
)
|
||||||
|
|
||||||
type Room struct {
|
type Room struct {
|
||||||
Type RoomType
|
Type RoomType
|
||||||
X, Y int
|
X, Y int // top-left in tile space
|
||||||
Width, Height int
|
W, H int // dimensions in tiles
|
||||||
Visited bool
|
Visited bool
|
||||||
Cleared bool
|
Cleared bool
|
||||||
Neighbors []int
|
Neighbors []int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Floor struct {
|
||||||
|
Number int
|
||||||
|
Rooms []*Room
|
||||||
|
CurrentRoom int
|
||||||
|
Tiles [][]Tile
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
func RandomRoomType() RoomType {
|
func RandomRoomType() RoomType {
|
||||||
r := rand.Float64() * 100
|
r := rand.Float64() * 100
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -27,43 +27,9 @@ func renderMap(floor *dungeon.Floor) string {
|
|||||||
if floor == nil {
|
if floor == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
headerStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
|
headerStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
|
||||||
sb.WriteString(headerStyle.Render(fmt.Sprintf("── Catacombs B%d ──", floor.Number)))
|
header := headerStyle.Render(fmt.Sprintf("── Catacombs B%d ──", floor.Number))
|
||||||
sb.WriteString("\n\n")
|
return header + "\n" + dungeon.RenderFloor(floor, floor.CurrentRoom, true)
|
||||||
|
|
||||||
roomStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder())
|
|
||||||
dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
|
||||||
hiddenStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("236"))
|
|
||||||
|
|
||||||
for i, room := range floor.Rooms {
|
|
||||||
vis := dungeon.GetRoomVisibility(floor, i)
|
|
||||||
symbol := roomTypeSymbol(room.Type)
|
|
||||||
label := fmt.Sprintf("[%d] %s %s", i, symbol, room.Type.String())
|
|
||||||
|
|
||||||
if i == floor.CurrentRoom {
|
|
||||||
label = ">> " + label + " <<"
|
|
||||||
}
|
|
||||||
|
|
||||||
switch vis {
|
|
||||||
case dungeon.Visible:
|
|
||||||
sb.WriteString(roomStyle.Render(label))
|
|
||||||
case dungeon.Visited:
|
|
||||||
sb.WriteString(dimStyle.Render(label))
|
|
||||||
case dungeon.Hidden:
|
|
||||||
sb.WriteString(hiddenStyle.Render("[?] ???"))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range room.Neighbors {
|
|
||||||
if n > i {
|
|
||||||
sb.WriteString(" --- ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderHUD(state game.GameState, targetCursor int) string {
|
func renderHUD(state game.GameState, targetCursor int) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user