import { DIRECTIONS } from './Constants.js'; import { GridSystem } from './GridSystem.js'; import { DungeonDeck } from './DungeonDeck.js'; const PLACEMENT_STATE = { WAITING_DOOR: 'WAITING_DOOR', PLACING_TILE: 'PLACING_TILE', COMPLETE: 'COMPLETE' }; export class DungeonGenerator { constructor() { this.grid = new GridSystem(); this.deck = new DungeonDeck(); this.placedTiles = []; this.availableExits = []; // Exits where player can choose to expand // Placement State this.state = PLACEMENT_STATE.WAITING_DOOR; this.currentCard = null; this.placementRotation = DIRECTIONS.NORTH; this.placementX = 0; this.placementY = 0; this.selectedExit = null; // Callbacks for UI this.onStateChange = null; this.onPlacementUpdate = null; this.onDoorBlocked = null; } startDungeon(missionConfig) { const objectiveId = missionConfig?.type === 'quest' ? 'room_objective' : 'room_dungeon'; this.deck.generateMissionDeck(objectiveId); // 1. Draw and place first card automatically at origin const firstCard = this.deck.draw(); if (!firstCard) { console.error("❌ Empty deck"); return; } this.placeCardFinal(firstCard, 0, 0, DIRECTIONS.NORTH); // 2. Transition to door selection this.state = PLACEMENT_STATE.WAITING_DOOR; this.notifyStateChange(); } /** * Player selects a door to expand from */ selectDoor(exitPoint) { if (this.state !== PLACEMENT_STATE.WAITING_DOOR) { console.warn("Not in door selection mode"); return false; } if (!exitPoint) { console.error("exitPoint is undefined!"); return false; } // Find the full exit object from availableExits (so we have tileId, etc.) const foundExit = this.availableExits.find( e => e.x === exitPoint.x && e.y === exitPoint.y && e.direction === exitPoint.direction ); if (!foundExit) { console.warn("Invalid exit selected"); return false; } this.selectedExit = foundExit; // Draw next card this.currentCard = this.deck.draw(); if (!this.currentCard) { this.state = PLACEMENT_STATE.COMPLETE; this.notifyStateChange(); return false; } // Calculate initial placement position (3 units above the connection point) const connectionPoint = this.neighbor(exitPoint.x, exitPoint.y, exitPoint.direction); // Start with NORTH rotation this.placementRotation = DIRECTIONS.NORTH; const variant = this.currentCard.variants[this.placementRotation]; // Find the exit on the new tile that should connect to selectedExit const requiredDirection = this.opposite(exitPoint.direction); const matchingExits = variant.exits.filter(e => e.direction === requiredDirection); if (matchingExits.length > 0) { // Use first matching exit as anchor const anchor = matchingExits[0]; this.placementX = Math.round(connectionPoint.x - anchor.x); this.placementY = Math.round(connectionPoint.y - anchor.y); } else { // Fallback: center on connection point this.placementX = Math.round(connectionPoint.x); this.placementY = Math.round(connectionPoint.y); } this.state = PLACEMENT_STATE.PLACING_TILE; this.notifyPlacementUpdate(); this.notifyStateChange(); return true; } /** * Rotate placement tile 90° clockwise */ rotatePlacement() { if (this.state !== PLACEMENT_STATE.PLACING_TILE) return; const rotations = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST]; const currentIndex = rotations.indexOf(this.placementRotation); this.placementRotation = rotations[(currentIndex + 1) % 4]; this.notifyPlacementUpdate(); } /** * Move placement tile by offset */ movePlacement(dx, dy) { if (this.state !== PLACEMENT_STATE.PLACING_TILE) return; this.placementX += dx; this.placementY += dy; this.notifyPlacementUpdate(); } /** * Check if current placement is valid */ isPlacementValid() { if (!this.currentCard || this.state !== PLACEMENT_STATE.PLACING_TILE) return false; const variant = this.currentCard.variants[this.placementRotation]; // 1. Basic Grid Collision if (!this.grid.canPlace(variant, this.placementX, this.placementY)) { return false; } // 2. Strict Door Alignment Check if (this.selectedExit) { // Identify the full "door" group (e.g., the pair of cells forming the exit) // We look for other available exits on the same tile, facing the same way, and adjacent. const sourceExits = this.availableExits.filter(e => e.tileId === this.selectedExit.tileId && e.direction === this.selectedExit.direction && (Math.abs(e.x - this.selectedExit.x) + Math.abs(e.y - this.selectedExit.y)) <= 1 ); // For every cell in the source door, the new tile MUST have a connecting exit for (const source of sourceExits) { // The coordinate where the new tile's exit should be const targetPos = this.neighbor(source.x, source.y, source.direction); const requiredDirection = this.opposite(source.direction); // Does the new tile provide an exit here? const hasMatch = variant.exits.some(localExit => { const gx = this.placementX + localExit.x; const gy = this.placementY + localExit.y; return gx === targetPos.x && gy === targetPos.y && localExit.direction === requiredDirection; }); if (!hasMatch) { return false; // Misalignment: New tile doesn't connect to all parts of the door } } } return true; } cancelPlacement() { if (this.state !== PLACEMENT_STATE.PLACING_TILE) return; // 1. Mark door as blocked visually if (this.onDoorBlocked && this.selectedExit) { this.onDoorBlocked(this.selectedExit); } // 2. Remove the selected exit from available exits if (this.selectedExit) { this.availableExits = this.availableExits.filter(e => !(e.x === this.selectedExit.x && e.y === this.selectedExit.y && e.direction === this.selectedExit.direction) ); } // 3. Reset state this.currentCard = null; this.selectedExit = null; this.state = PLACEMENT_STATE.WAITING_DOOR; this.notifyPlacementUpdate(); this.notifyStateChange(); } /** * Confirm and finalize tile placement */ confirmPlacement() { if (this.state !== PLACEMENT_STATE.PLACING_TILE) { console.warn("Not in placement mode"); return false; } if (!this.isPlacementValid()) { console.warn("❌ Cannot place tile - invalid position"); return false; } // Round to integers (tiles must be on grid cells) const finalX = Math.round(this.placementX); const finalY = Math.round(this.placementY); // Place the tile this.placeCardFinal( this.currentCard, finalX, finalY, this.placementRotation ); // Reset placement state this.currentCard = null; this.selectedExit = null; this.state = PLACEMENT_STATE.WAITING_DOOR; this.notifyPlacementUpdate(); // Clear preview this.notifyStateChange(); return true; } /** * Internal: Actually place a card on the grid */ placeCardFinal(card, x, y, rotation) { const variant = card.variants[rotation]; // Randomize Texture if multiple are available let selectedTexture = null; if (card.textures && card.textures.length > 0) { const idx = Math.floor(Math.random() * card.textures.length); selectedTexture = card.textures[idx]; console.log(`[DungeonGenerator] Selected texture for ${card.id}:`, selectedTexture); } const instance = { id: `tile_${this.placedTiles.length}`, defId: card.id, x, y, rotation, name: card.name, texture: selectedTexture }; this.grid.placeTile(instance, variant, card); this.placedTiles.push(instance); // Update available exits this.updateAvailableExits(instance, variant, x, y); } /** * Update list of exits player can choose from */ updateAvailableExits(instance, variant, anchorX, anchorY) { // Add new exits from this tile for (const ex of variant.exits) { const gx = anchorX + ex.x; const gy = anchorY + ex.y; const leadingTo = this.neighbor(gx, gy, ex.direction); const isOccupied = this.grid.isOccupied(leadingTo.x, leadingTo.y); if (!isOccupied) { this.availableExits.push({ x: gx, y: gy, direction: ex.direction, tileId: instance.id }); } } // Remove exits that are now blocked or connected this.availableExits = this.availableExits.filter(exit => { const leadingTo = this.neighbor(exit.x, exit.y, exit.direction); return !this.grid.isOccupied(leadingTo.x, leadingTo.y); }); } /** * Get current placement preview data for renderer */ getPlacementPreview() { if (this.state !== PLACEMENT_STATE.PLACING_TILE || !this.currentCard) { return null; } const variant = this.currentCard.variants[this.placementRotation]; const cells = this.grid.calculateCells(variant, this.placementX, this.placementY); const isValid = this.isPlacementValid(); return { card: this.currentCard, rotation: this.placementRotation, x: this.placementX, y: this.placementY, cells, isValid, variant }; } // --- Helpers --- notifyStateChange() { if (this.onStateChange) { this.onStateChange(this.state); } } notifyPlacementUpdate() { if (this.onPlacementUpdate) { const preview = this.getPlacementPreview(); this.onPlacementUpdate(preview); } } neighbor(x, y, dir) { switch (dir) { case DIRECTIONS.NORTH: return { x, y: y + 1 }; case DIRECTIONS.SOUTH: return { x, y: y - 1 }; case DIRECTIONS.EAST: return { x: x + 1, y }; case DIRECTIONS.WEST: return { x: x - 1, y }; } } opposite(dir) { const map = { [DIRECTIONS.NORTH]: DIRECTIONS.SOUTH, [DIRECTIONS.SOUTH]: DIRECTIONS.NORTH, [DIRECTIONS.EAST]: DIRECTIONS.WEST, [DIRECTIONS.WEST]: DIRECTIONS.EAST }; return map[dir]; } }