versión inicial del juego
This commit is contained in:
257
src/engine/dungeon/DungeonGenerator.js
Normal file
257
src/engine/dungeon/DungeonGenerator.js
Normal file
@@ -0,0 +1,257 @@
|
||||
import { DIRECTIONS } from './Constants.js';
|
||||
import { GridSystem } from './GridSystem.js';
|
||||
import { DungeonDeck } from './DungeonDeck.js';
|
||||
import { TILES } from './TileDefinitions.js';
|
||||
|
||||
export class DungeonGenerator {
|
||||
constructor() {
|
||||
this.grid = new GridSystem();
|
||||
this.deck = new DungeonDeck();
|
||||
this.pendingExits = []; // Array of global {x, y, direction}
|
||||
this.placedTiles = [];
|
||||
this.isComplete = false;
|
||||
}
|
||||
|
||||
startDungeon(missionConfig) {
|
||||
// 1. Prepare Deck (Rulebook: 13 cards, 6+1+6)
|
||||
// We need an objective tile ID from the config
|
||||
const objectiveId = missionConfig.type === 'quest' ? 'room_objective' : 'room_dungeon'; // Fallback for now
|
||||
this.deck.generateMissionDeck(objectiveId);
|
||||
|
||||
// 2. Rulebook Step 4: "Flip the first card. This is the entrance."
|
||||
const startCard = this.deck.draw();
|
||||
|
||||
if (!startCard) {
|
||||
console.error("Deck is empty on start!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Place the Entry Tile at (0,0)
|
||||
// We assume rotation NORTH by default for the first piece
|
||||
const startInstance = {
|
||||
id: `tile_0_${startCard.id}`,
|
||||
defId: startCard.id,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: DIRECTIONS.NORTH
|
||||
};
|
||||
|
||||
if (this.grid.canPlace(startCard, 0, 0, DIRECTIONS.NORTH)) {
|
||||
this.grid.placeTile(startInstance, startCard);
|
||||
this.placedTiles.push(startInstance);
|
||||
this.addExitsToQueue(startInstance, startCard);
|
||||
console.log(`Dungeon started with ${startCard.name}`);
|
||||
} else {
|
||||
console.error("Failed to place starting tile (Grid collision at 0,0?)");
|
||||
}
|
||||
}
|
||||
|
||||
step() {
|
||||
if (this.isComplete) return false;
|
||||
if (this.pendingExits.length === 0) {
|
||||
console.log("No more exits available. Dungeon generation stopped.");
|
||||
this.isComplete = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rulebook: Draw next card
|
||||
const card = this.deck.draw();
|
||||
|
||||
if (!card) {
|
||||
console.log("Deck empty. Dungeon complete.");
|
||||
this.isComplete = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to fit the card on any pending exit
|
||||
// We prioritize the "current" open exit? Rulebook implies expanding from the explored edge.
|
||||
// For a generator, we treat it as a queue (BFS) or stack (DFS). Queue is better for "bushy" dungeons.
|
||||
|
||||
// Let's try to fit the card onto the FIRST valid exit in our queue
|
||||
let placed = false;
|
||||
|
||||
// Iterate through copy of pending exits to avoid modification issues during loop
|
||||
// (Though we usually just pick ONE exit to explore per turn in the board game)
|
||||
// In the board game, you pick an exit and "Explore" it.
|
||||
// Let's pick the first available exit.
|
||||
const targetExit = this.pendingExits.shift();
|
||||
|
||||
console.log(`Attempting to place ${card.name} at exit ${targetExit.x},${targetExit.y} (${targetExit.direction})`);
|
||||
|
||||
// We need to rotate the new card so ONE of its exits connects to 'targetExit'
|
||||
// Connection rule: New Tile Exit be Opposed to Target Exit.
|
||||
// Target: NORTH -> New Tile must present a SOUTH exit to connect.
|
||||
const requiredInputDirection = this.getOppositeDirection(targetExit.direction);
|
||||
|
||||
// Find which exit on the CANDIDATE card can serve as the input
|
||||
// (A tile might have multiple potential inputs, e.g. a 4-way corridor)
|
||||
for (const candidateExit of card.exits) {
|
||||
// calculatedRotation: What rotation does the TILE need so that 'candidateExit' points 'requiredInputDirection'?
|
||||
// candidateExit.direction (Local) + TileRotation = requiredInputDirection
|
||||
|
||||
const rotation = this.calculateRequiredRotation(candidateExit.direction, requiredInputDirection);
|
||||
|
||||
// Now calculate where the tile top-left (x,y) must be so that the exits match positions.
|
||||
const position = this.calculateTilePosition(targetExit, candidateExit, rotation);
|
||||
|
||||
if (this.grid.canPlace(card, position.x, position.y, rotation)) {
|
||||
|
||||
// Success! Place it.
|
||||
const newInstance = {
|
||||
id: `tile_${this.placedTiles.length}_${card.id}`,
|
||||
defId: card.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
rotation: rotation
|
||||
};
|
||||
|
||||
this.grid.placeTile(newInstance, card);
|
||||
this.placedTiles.push(newInstance);
|
||||
|
||||
// Add NEW exits, but...
|
||||
// CRITICAL: The exit we just used to enter is NOT an exit anymore. It's the connection.
|
||||
this.addExitsToQueue(newInstance, card, targetExit); // Pass the source to exclude it
|
||||
|
||||
placed = true;
|
||||
break; // Stop looking for fits for this card
|
||||
}
|
||||
}
|
||||
|
||||
if (!placed) {
|
||||
console.log(`Could not fit ${card.name} at selected exit. Discarding.`);
|
||||
// In real game: Discard card.
|
||||
// Put the exit back? Rulebook says "If room doesn't fit, nothing is placed".
|
||||
// Does the exit remain open? Yes, usually.
|
||||
this.pendingExits.push(targetExit); // Return exit to queue to try later?
|
||||
// Or maybe discard it?
|
||||
// "If you cannot place the room... the passage is a dead end." (Some editions)
|
||||
// Let's keep it open for now, maybe next card fits.
|
||||
}
|
||||
|
||||
return true; // Step done
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
getOppositeDirection(dir) {
|
||||
switch (dir) {
|
||||
case DIRECTIONS.NORTH: return DIRECTIONS.SOUTH;
|
||||
case DIRECTIONS.SOUTH: return DIRECTIONS.NORTH;
|
||||
case DIRECTIONS.EAST: return DIRECTIONS.WEST;
|
||||
case DIRECTIONS.WEST: return DIRECTIONS.EAST;
|
||||
}
|
||||
}
|
||||
|
||||
calculateRequiredRotation(localDir, targetGlobalDir) {
|
||||
// e.g. Local=NORTH needs to become Global=EAST.
|
||||
// N(0) -> E(1). Diff +1 (90 deg).
|
||||
// Standard mapping: N=0, E=1, S=2, W=3
|
||||
const dirs = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST];
|
||||
const localIdx = dirs.indexOf(localDir);
|
||||
const targetIdx = dirs.indexOf(targetGlobalDir);
|
||||
|
||||
// (Local + Rotation) % 4 = Target
|
||||
// Rotation = (Target - Local + 4) % 4
|
||||
const diff = (targetIdx - localIdx + 4) % 4;
|
||||
return dirs[diff];
|
||||
}
|
||||
|
||||
calculateTilePosition(targetExitGlobal, candidateExitLocal, rotation) {
|
||||
// We know the Global Coordinate of the connection point (targetExitGlobal)
|
||||
// We know the Local Coordinate of the matching exit on the new tile (candidateExitLocal)
|
||||
// We need 'startX, startY' of the new tile.
|
||||
|
||||
// First, transform the local exit to a rotated offset
|
||||
// We reuse GridSystem logic logic ideally, but let's do math here
|
||||
let offsetX, offsetY;
|
||||
|
||||
// Replicating GridSystem.getGlobalPoint simple logic for vector only
|
||||
// If we treat candidateExitLocal as a vector from (0,0)
|
||||
const lx = candidateExitLocal.x;
|
||||
const ly = candidateExitLocal.y;
|
||||
|
||||
switch (rotation) {
|
||||
case DIRECTIONS.NORTH: offsetX = lx; offsetY = ly; break;
|
||||
case DIRECTIONS.SOUTH: offsetX = -lx; offsetY = -ly; break;
|
||||
case DIRECTIONS.EAST: offsetX = ly; offsetY = -lx; break;
|
||||
case DIRECTIONS.WEST: offsetX = -ly; offsetY = lx; break;
|
||||
}
|
||||
|
||||
// GlobalExit = TilePos + RotatedOffset
|
||||
// TilePos = GlobalExit - RotatedOffset
|
||||
|
||||
// Wait, 'targetExitGlobal' is the cell just OUTSIDE the previous tile?
|
||||
// Or the cell OF the previous tile's exit?
|
||||
// Usually targetExit is "The cell where the connection happens".
|
||||
// In GridSystem, exits are defined AT the edge.
|
||||
// Let's assume targetExitGlobal is the coordinate OF THE EXIT CELL on the previous tile.
|
||||
// So the new tile's matching exit cell must OVERLAP this one? NO.
|
||||
// They must be adjacent.
|
||||
|
||||
// Correction: Tiles must connect *adjacent* to each other.
|
||||
// If TargetExit is at (10,10) facing NORTH, the New Tile must attach at (10,11).
|
||||
|
||||
let connectionPointX = targetExitGlobal.x;
|
||||
let connectionPointY = targetExitGlobal.y;
|
||||
|
||||
// Move 1 step in the target direction to find the "Anchor Point" for the new tile
|
||||
switch (targetExitGlobal.direction) {
|
||||
case DIRECTIONS.NORTH: connectionPointY += 1; break;
|
||||
case DIRECTIONS.SOUTH: connectionPointY -= 1; break;
|
||||
case DIRECTIONS.EAST: connectionPointX += 1; break;
|
||||
case DIRECTIONS.WEST: connectionPointX -= 1; break;
|
||||
}
|
||||
|
||||
// Now align the new tile such that its candidate exit lands on connectionPoint
|
||||
return {
|
||||
x: connectionPointX - offsetX,
|
||||
y: connectionPointY - offsetY
|
||||
};
|
||||
}
|
||||
|
||||
addExitsToQueue(tileInstance, tileDef, excludeSourceExit = null) {
|
||||
// Calculate all global exits for this placed tile
|
||||
for (const exit of tileDef.exits) {
|
||||
const globalPoint = this.grid.getGlobalPoint(exit.x, exit.y, tileInstance);
|
||||
const globalDir = this.grid.getRotatedDirection(exit.direction, tileInstance.rotation);
|
||||
|
||||
// If this is the exit we just entered through, skip it
|
||||
// Logic: connection is adjacent.
|
||||
// A simpler check: if we just connected to (X,Y), don't add an exit at (X,Y).
|
||||
// But we calculated 'connectionPoint' as the place where the NEW tile's exit is.
|
||||
|
||||
// Check adjacency to excludeSource?
|
||||
// Or better: excludeSourceExit is the "Previous Tile's Exit".
|
||||
// The "Entrance" on the new tile connects to that.
|
||||
// We should just not add the exit that was used as input.
|
||||
|
||||
// How to identify it?
|
||||
// We calculated it in the main loop.
|
||||
// Let's simplify: Add ALL exits.
|
||||
// The logic later will filter out exits that point into occupied cells?
|
||||
// Yes, checking collision also checks if the target cell is free.
|
||||
// But we don't want to list "Backwards" exits.
|
||||
|
||||
// Optimization: If the cell immediate to this exit is already occupied, don't add it.
|
||||
// This handles the "Entrance" naturally (it points back to the previous tile).
|
||||
|
||||
let neighborX = globalPoint.x;
|
||||
let neighborY = globalPoint.y;
|
||||
switch (globalDir) {
|
||||
case DIRECTIONS.NORTH: neighborY += 1; break;
|
||||
case DIRECTIONS.SOUTH: neighborY -= 1; break;
|
||||
case DIRECTIONS.EAST: neighborX += 1; break;
|
||||
case DIRECTIONS.WEST: neighborX -= 1; break;
|
||||
}
|
||||
|
||||
const neighborKey = `${neighborX},${neighborY}`;
|
||||
if (!this.grid.occupiedCells.has(neighborKey)) {
|
||||
this.pendingExits.push({
|
||||
x: globalPoint.x,
|
||||
y: globalPoint.y,
|
||||
direction: globalDir
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user