- DungeonGenerator: Selects a random texture from the card definition when finalizing tile placement. - GameRenderer: Renders the specific chosen texture for each tile instance instead of the default.
380 lines
11 KiB
JavaScript
380 lines
11 KiB
JavaScript
|
|
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];
|
|
}
|
|
}
|