Files
WarhammerQuest/src/engine/dungeon/DungeonGenerator.js
marti cd6abb016f Implement randomized tile textures.
- 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.
2026-01-03 00:19:30 +01:00

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];
}
}