UNPKG

shellquest

Version:

Terminal-based procedurally generated dungeon crawler

253 lines (210 loc) 7.9 kB
/** * 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, }; } }