shellquest
Version:
Terminal-based procedurally generated dungeon crawler
253 lines (210 loc) • 7.9 kB
text/typescript
/**
* DungeonLighting - Integrates the lighting system with dungeon levels
*
* Handles:
* - Wall torch placement during dungeon generation
* - Player torch light management
* - Light source lifecycle
*/
import {TILE_SIZE} from '../TileMap.ts';
import {
LightingSystem,
createTorchLight,
createPlayerTorchLight,
type LightSource,
type LightingConfig,
} from './LightingSystem.ts';
import {SeededRandom, isWall, isFloor, type TileGrid} from '../level/dungeon/DungeonUtils.ts';
// =============================================================================
// TORCH PLACEMENT CONFIGURATION
// =============================================================================
export interface TorchPlacementConfig {
minSpacing: number; // Minimum tiles between torches
frequency: number; // Base probability of placing torch on valid wall (0-1)
maxTorches: number; // Maximum torches per level
preferCorridors: boolean; // Place more torches in corridors
}
export const DEFAULT_TORCH_CONFIG: TorchPlacementConfig = {
minSpacing: 8,
frequency: 0.12,
maxTorches: 50,
preferCorridors: true,
};
// =============================================================================
// TORCH POSITION TYPE
// =============================================================================
export interface TorchPosition {
tileX: number;
tileY: number;
pixelX: number;
pixelY: number;
}
// =============================================================================
// DUNGEON LIGHTING MANAGER
// =============================================================================
export class DungeonLighting {
private lightingSystem: LightingSystem;
private torchPositions: TorchPosition[] = [];
private playerLightId: string = 'player-torch';
constructor(config: Partial<LightingConfig> = {}) {
this.lightingSystem = new LightingSystem(config);
}
// ===========================================================================
// TORCH PLACEMENT
// ===========================================================================
/**
* Place torches on valid wall faces in the dungeon
*/
placeTorches(
grid: TileGrid,
width: number,
height: number,
seed: number,
config: Partial<TorchPlacementConfig> = {},
): TorchPosition[] {
const cfg = {...DEFAULT_TORCH_CONFIG, ...config};
const rng = new SeededRandom(seed);
const placedTorches: TorchPosition[] = [];
const occupiedPositions = new Set<string>();
// Scan for valid torch positions (wall tiles with floor below)
const validPositions: {x: number; y: number; score: number}[] = [];
for (let y = 2; y < height - 2; y++) {
for (let x = 2; x < width - 2; x++) {
if (!this.isValidTorchPosition(grid, x, y)) continue;
// Score position based on surroundings
let score = 1;
// Prefer positions near corridor intersections
if (cfg.preferCorridors) {
const floorNeighbors = this.countFloorNeighbors(grid, x, y + 1);
if (floorNeighbors >= 3) score += 0.5; // Intersection or open area
if (floorNeighbors === 2) score += 0.3; // Corridor
}
validPositions.push({x, y, score});
}
}
// Shuffle positions for randomness
const shuffled = rng.shuffle(validPositions);
// Place torches respecting spacing constraints
for (const pos of shuffled) {
if (placedTorches.length >= cfg.maxTorches) break;
// Check spacing
const tooClose = placedTorches.some(
(t) => Math.abs(t.tileX - pos.x) + Math.abs(t.tileY - pos.y) < cfg.minSpacing,
);
if (tooClose) continue;
// Check if position already occupied
const key = `${pos.x},${pos.y}`;
if (occupiedPositions.has(key)) continue;
// Random roll modified by score
if (rng.next() > cfg.frequency * pos.score) continue;
// Place torch
const torch: TorchPosition = {
tileX: pos.x,
tileY: pos.y,
pixelX: pos.x * TILE_SIZE + TILE_SIZE / 2,
pixelY: pos.y * TILE_SIZE + TILE_SIZE / 2,
};
placedTorches.push(torch);
occupiedPositions.add(key);
// Create light source for this torch
const lightId = `torch-${pos.x}-${pos.y}`;
this.lightingSystem.addLight(createTorchLight(lightId, torch.pixelX, torch.pixelY));
}
this.torchPositions = placedTorches;
return placedTorches;
}
private isValidTorchPosition(grid: TileGrid, x: number, y: number): boolean {
// Must be a wall tile
if (!isWall(grid, x, y)) return false;
// Must have floor directly below (visible wall face)
if (!isFloor(grid, x, y + 1)) return false;
// Must not have wall below the floor (not a ledge)
if (isWall(grid, x, y + 2)) return false;
return true;
}
private countFloorNeighbors(grid: TileGrid, x: number, y: number): number {
let count = 0;
if (isFloor(grid, x - 1, y)) count++;
if (isFloor(grid, x + 1, y)) count++;
if (isFloor(grid, x, y - 1)) count++;
if (isFloor(grid, x, y + 1)) count++;
return count;
}
// ===========================================================================
// PLAYER TORCH
// ===========================================================================
/**
* Initialize the player's torch light
*/
initializePlayerTorch(x: number, y: number): void {
this.lightingSystem.addLight(createPlayerTorchLight(x, y));
}
/**
* Update player torch position (call each frame)
*/
updatePlayerTorch(x: number, y: number): void {
this.lightingSystem.updateLightPosition(this.playerLightId, x, y);
}
changePlayerTorch(update: (light: LightSource) => void): void {
const light = this.lightingSystem.getLight(this.playerLightId);
if (!light) return;
update(light);
}
// ===========================================================================
// LIGHTING SYSTEM ACCESS
// ===========================================================================
/**
* Update the lighting system (call each frame)
*/
update(deltaTime: number): void {
this.lightingSystem.update(deltaTime);
}
/**
* Get the lighting system for rendering
*/
getLightingSystem(): LightingSystem {
return this.lightingSystem;
}
/**
* Get all torch positions for tile rendering
*/
getTorchPositions(): TorchPosition[] {
return this.torchPositions;
}
/**
* Compute light map for the visible viewport (RGB channels)
*/
computeLightMap(
viewportWidth: number,
viewportHeight: number,
cameraX: number,
cameraY: number,
): {r: Float32Array; g: Float32Array; b: Float32Array} {
return this.lightingSystem.computeLightMap(viewportWidth, viewportHeight, cameraX, cameraY);
}
/**
* Get brightness at a specific screen position
*/
getBrightnessAt(screenX: number, screenY: number): number {
return this.lightingSystem.getBrightnessAt(screenX, screenY);
}
// ===========================================================================
// CONFIGURATION
// ===========================================================================
/**
* Set the ambient (minimum) light level
*/
setAmbientLight(level: number): void {
this.lightingSystem.setAmbientLight(level);
}
/**
* Get light statistics for debugging
*/
getStats(): {totalLights: number; enabledLights: number; torches: number} {
return {
totalLights: this.lightingSystem.getLightCount(),
enabledLights: this.lightingSystem.getEnabledLightCount(),
torches: this.torchPositions.length,
};
}
}