Fix tile rendering dimensions and alignment, update tile definitions to use height
This commit is contained in:
@@ -1,85 +1,68 @@
|
||||
|
||||
import { TILES } from './TileDefinitions.js';
|
||||
|
||||
export class DungeonDeck {
|
||||
|
||||
|
||||
constructor() {
|
||||
this.cards = [];
|
||||
this.discards = [];
|
||||
// We don't initialize automatically anymore
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the deck according to the specific Warhammer Quest rules.
|
||||
* Rulebook steps:
|
||||
* 1. Take 6 random Dungeon Cards (Bottom pool).
|
||||
* 2. Add Objective Room card to Bottom pool.
|
||||
* 3. Shuffle Bottom pool (7 cards).
|
||||
* 4. Take 6 random Dungeon Cards (Top pool).
|
||||
* 5. Stack Top pool on Bottom pool.
|
||||
* Total: 13 cards.
|
||||
*
|
||||
* @param {string} objectiveTileId - ID of the objective/exit room.
|
||||
*/
|
||||
generateMissionDeck(objectiveTileId) {
|
||||
console.log("🔍 Inspecting TILES object keys:", Object.keys(TILES));
|
||||
this.cards = [];
|
||||
|
||||
// 1. Create a "Pool" of standard dungeon tiles (Rooms & Corridors)
|
||||
// We replicate the physical deck distribution first
|
||||
// 1. Create a "Pool" of standard dungeon tiles
|
||||
let pool = [];
|
||||
const composition = [
|
||||
{ id: 'room_dungeon', count: 6 },
|
||||
// Objective room is special, handled separately
|
||||
{ id: 'corridor_straight', count: 7 },
|
||||
{ id: 'corridor_steps', count: 1 },
|
||||
{ id: 'corridor_corner', count: 1 },
|
||||
{ id: 'corridor_corner', count: 1 }, // L-Shape
|
||||
{ id: 'junction_t', count: 3 }
|
||||
];
|
||||
|
||||
composition.forEach(item => {
|
||||
const tileDef = TILES.find(t => t.id === item.id);
|
||||
// FIXED: Access by Key string directly
|
||||
const tileDef = TILES[item.id];
|
||||
|
||||
if (tileDef) {
|
||||
for (let i = 0; i < item.count; i++) {
|
||||
pool.push(tileDef);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Missing Tile Definition for ID: ${item.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to pull random cards
|
||||
const drawRandom = (source, count) => {
|
||||
const drawn = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (source.length === 0) break;
|
||||
const idx = Math.floor(Math.random() * source.length);
|
||||
drawn.push(source[idx]);
|
||||
source.splice(idx, 1); // Remove from pool
|
||||
source.splice(idx, 1);
|
||||
}
|
||||
return drawn;
|
||||
};
|
||||
|
||||
// --- Step 1 & 2: Bottom Pool (6 Random + Objective) ---
|
||||
// --- Step 1 & 2: Bottom Pool ---
|
||||
const bottomPool = drawRandom(pool, 6);
|
||||
|
||||
// Add Objective Card
|
||||
const objectiveDef = TILES.find(t => t.id === objectiveTileId);
|
||||
const objectiveDef = TILES[objectiveTileId];
|
||||
if (objectiveDef) {
|
||||
bottomPool.push(objectiveDef);
|
||||
} else {
|
||||
console.error("Objective Tile ID not found:", objectiveTileId);
|
||||
// Fallback: Add a generic room if objective missing?
|
||||
}
|
||||
|
||||
// --- Step 3: Shuffle Bottom Pool ---
|
||||
this.shuffleArray(bottomPool);
|
||||
|
||||
// --- Step 4: Top Pool (6 Random) ---
|
||||
// --- Step 4: Top Pool ---
|
||||
const topPool = drawRandom(pool, 6);
|
||||
// Note: No shuffle explicitly needed for Top Pool if drawn randomly,
|
||||
// but shuffling ensures random order of the 6 drawn.
|
||||
this.shuffleArray(topPool);
|
||||
|
||||
// --- Step 5: Stack (Top on Bottom) ---
|
||||
// Array[0] is the "Top" card (first to be drawn)
|
||||
// --- Step 5: Stack ---
|
||||
this.cards = [...topPool, ...bottomPool];
|
||||
|
||||
console.log(`Deck Generated: ${this.cards.length} cards.`);
|
||||
@@ -93,15 +76,12 @@ export class DungeonDeck {
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.cards.length === 0) {
|
||||
return null; // Deck empty
|
||||
}
|
||||
return this.cards.shift(); // Take from top
|
||||
if (this.cards.length === 0) return null;
|
||||
return this.cards.shift();
|
||||
}
|
||||
|
||||
// Useful for Campaign logic: Insert a specific card at position
|
||||
insertCard(tileId, position = 0) {
|
||||
const tileDef = TILES.find(t => t.id === tileId);
|
||||
const tileDef = TILES[tileId];
|
||||
if (tileDef) {
|
||||
this.cards.splice(position, 0, tileDef);
|
||||
}
|
||||
|
||||
@@ -1,296 +1,317 @@
|
||||
|
||||
import { DIRECTIONS } from './Constants.js';
|
||||
import { GridSystem } from './GridSystem.js';
|
||||
import { DungeonDeck } from './DungeonDeck.js';
|
||||
import { TILES } from './TileDefinitions.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.pendingExits = []; // Array of global {x, y, direction}
|
||||
this.placedTiles = [];
|
||||
this.isComplete = false;
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
const objectiveId = missionConfig?.type === 'quest' ? 'room_objective' : 'room_dungeon';
|
||||
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!");
|
||||
// 1. Draw and place first card automatically at origin
|
||||
const firstCard = this.deck.draw();
|
||||
if (!firstCard) {
|
||||
console.error("❌ Empty deck");
|
||||
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
|
||||
};
|
||||
this.placeCardFinal(firstCard, 0, 0, DIRECTIONS.NORTH);
|
||||
console.log(`🏰 Dungeon started with ${firstCard.name}`);
|
||||
|
||||
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?)");
|
||||
}
|
||||
// 2. Transition to door selection
|
||||
this.state = PLACEMENT_STATE.WAITING_DOOR;
|
||||
this.notifyStateChange();
|
||||
}
|
||||
|
||||
step() {
|
||||
if (this.isComplete) return false;
|
||||
if (this.pendingExits.length === 0) {
|
||||
console.log("No more exits available. Dungeon generation stopped.");
|
||||
this.isComplete = true;
|
||||
/**
|
||||
* Player selects a door to expand from
|
||||
*/
|
||||
selectDoor(exitPoint) {
|
||||
console.log('[DungeonGenerator] selectDoor called with:', exitPoint);
|
||||
console.log('[DungeonGenerator] Current state:', this.state);
|
||||
console.log('[DungeonGenerator] Available exits:', this.availableExits);
|
||||
|
||||
if (this.state !== PLACEMENT_STATE.WAITING_DOOR) {
|
||||
console.warn("Not in door selection mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
const card = this.deck.draw();
|
||||
if (!card) {
|
||||
console.log("Deck empty. Dungeon complete.");
|
||||
this.isComplete = true;
|
||||
if (!exitPoint) {
|
||||
console.error("exitPoint is undefined!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// We process exits in groups now?
|
||||
// Or simply: When we pick an exit, we verify if it is part of a larger door.
|
||||
// Actually, 'pendingExits' contains individual cells.
|
||||
// Let's pick one.
|
||||
const targetExit = this.pendingExits.shift();
|
||||
// Validate exit exists
|
||||
const exitExists = this.availableExits.some(
|
||||
e => e.x === exitPoint.x && e.y === exitPoint.y && e.direction === exitPoint.direction
|
||||
);
|
||||
|
||||
// 1. Identify the "Global Reference Point" for the door this exit belongs to.
|
||||
// (If door is 2-wide, we want the One with the LOWEST X or LOWEST Y).
|
||||
// WE MUST FIND ITS SIBLING if it exists in 'pendingExits'.
|
||||
// This stops us from trying to attach a door twice (once per cell).
|
||||
|
||||
// Simple heuristic: If we have an exit at (x,y), check (x+1,y) or (x,y+1) depending on dir.
|
||||
// If the sibling is also in pendingExits, we effectively "consume" it too for this placement.
|
||||
|
||||
// Better: Find the "Left-Most" or "Bottom-Most" cell of this specific connection interface.
|
||||
// And use THAT as the target.
|
||||
|
||||
const targetRef = this.findExitReference(targetExit);
|
||||
console.log(`Attempting to place ${card.name} at Global Ref ${targetRef.x},${targetRef.y} (${targetRef.direction})`);
|
||||
|
||||
const requiredInputDirection = this.getOppositeDirection(targetRef.direction);
|
||||
let placed = false;
|
||||
|
||||
// Try to fit the card
|
||||
// We iterate input exits on the NEW card.
|
||||
// We only look at "Reference" exits on the new card too (min x/y) to avoid duplicate attempts.
|
||||
const candidateExits = this.UniqueExits(card);
|
||||
|
||||
for (const candidateExit of candidateExits) {
|
||||
|
||||
const rotation = this.calculateRequiredRotation(candidateExit.direction, requiredInputDirection);
|
||||
|
||||
// Now calculate ALIGNMENT.
|
||||
// We want the "Min Cell" of the Candidate Door (after rotation)
|
||||
// To overlap with the "Neighbor Cell" of the "Min Cell" of the Target Door?
|
||||
// NO.
|
||||
// Target Door Min Cell is at (TX, TY).
|
||||
// Its "Connection Neighbor" is at (NX, NY).
|
||||
// We want Candidate Door (Rotated) Min Cell to be at (NX, NY).
|
||||
|
||||
// 1. Calculate the offset of Candidate 'Min Cell' relative to Tile Origin (0,0) AFTER rotation.
|
||||
const rotatedOffset = this.getRotatedOffset(candidateExit, rotation);
|
||||
|
||||
// 2. Calculate the global connection point input
|
||||
const connectionPoint = this.getNeighborCell(targetRef.x, targetRef.y, targetRef.direction);
|
||||
|
||||
// 3. Tile Position
|
||||
const posX = connectionPoint.x - rotatedOffset.x;
|
||||
const posY = connectionPoint.y - rotatedOffset.y;
|
||||
|
||||
if (this.grid.canPlace(card, posX, posY, rotation)) {
|
||||
// Success
|
||||
const newInstance = {
|
||||
id: `tile_${this.placedTiles.length}_${card.id}`,
|
||||
defId: card.id,
|
||||
x: posX,
|
||||
y: posY,
|
||||
rotation: rotation
|
||||
};
|
||||
|
||||
this.grid.placeTile(newInstance, card);
|
||||
this.placedTiles.push(newInstance);
|
||||
|
||||
// Add NEW exits
|
||||
this.addExitsToQueue(newInstance, card);
|
||||
|
||||
// Cleanup: Remove the used exit(s) from pendingExits
|
||||
// We used targetRef. We must also remove its sibling if it exists.
|
||||
// Or simply: filter out any pending exit that is now blocked.
|
||||
this.cleanupPendingExits();
|
||||
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (!exitExists) {
|
||||
console.warn("Invalid exit selected");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!placed) {
|
||||
console.log(`Could not fit ${card.name}. Discarding.`);
|
||||
// If failed, return the exit to the pool?
|
||||
// Or discard the exit as "Dead End"?
|
||||
// For now, put it back at the end of queue.
|
||||
this.pendingExits.push(targetExit);
|
||||
this.selectedExit = exitPoint;
|
||||
|
||||
// Draw next card
|
||||
this.currentCard = this.deck.draw();
|
||||
if (!this.currentCard) {
|
||||
console.log("Deck empty - dungeon complete");
|
||||
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();
|
||||
|
||||
console.log(`📦 Placing ${this.currentCard.name} at (${this.placementX}, ${this.placementY})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
getNeighborCell(x, y, dir) {
|
||||
switch (dir) {
|
||||
case DIRECTIONS.NORTH: return { x: x, y: y + 1 };
|
||||
case DIRECTIONS.SOUTH: return { x: x, y: y - 1 };
|
||||
case DIRECTIONS.EAST: return { x: x + 1, y: y };
|
||||
case DIRECTIONS.WEST: return { x: x - 1, y: y };
|
||||
}
|
||||
/**
|
||||
* 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];
|
||||
|
||||
console.log(`🔄 Rotated to ${this.placementRotation}`);
|
||||
this.notifyPlacementUpdate();
|
||||
}
|
||||
|
||||
findExitReference(exit) {
|
||||
// If facing North/South, Reference is Minimum X.
|
||||
// If facing East/West, Reference is Minimum Y.
|
||||
/**
|
||||
* Move placement tile by offset
|
||||
*/
|
||||
movePlacement(dx, dy) {
|
||||
if (this.state !== PLACEMENT_STATE.PLACING_TILE) return;
|
||||
|
||||
// This function assumes 'exit' is from pendingExits (Global coords).
|
||||
// It checks if there is a "Lower" sibling also in pendingExits.
|
||||
// If so, returns the lower sibling. BEFORE using this exit.
|
||||
this.placementX += dx;
|
||||
this.placementY += dy;
|
||||
|
||||
let bestExit = exit;
|
||||
|
||||
// Check for siblings in pendingExits that match direction and are < coordinate
|
||||
// This is O(N) but N is small.
|
||||
for (const other of this.pendingExits) {
|
||||
if (other === exit) continue;
|
||||
if (other.direction !== exit.direction) continue;
|
||||
|
||||
if (exit.direction === DIRECTIONS.NORTH || exit.direction === DIRECTIONS.SOUTH) {
|
||||
// Check X. Adjacent implies y same, x diff 1.
|
||||
if (other.y === exit.y && Math.abs(other.x - exit.x) === 1) {
|
||||
if (other.x < bestExit.x) bestExit = other;
|
||||
}
|
||||
} else {
|
||||
// Check Y. adjacent implies x same, y diff 1.
|
||||
if (other.x === exit.x && Math.abs(other.y - exit.y) === 1) {
|
||||
if (other.y < bestExit.y) bestExit = other;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestExit;
|
||||
console.log(`↔️ Moved to (${this.placementX}, ${this.placementY})`);
|
||||
this.notifyPlacementUpdate();
|
||||
}
|
||||
|
||||
UniqueExits(tileDef) {
|
||||
// Filter tileDef.exits to only return the "Reference" (Min x/y) for each face/group.
|
||||
// This prevents trying to attach the same door 2 times.
|
||||
const unique = [];
|
||||
const seen = new Set(); // store "dir_coord" keys
|
||||
/**
|
||||
* Check if current placement is valid
|
||||
*/
|
||||
isPlacementValid() {
|
||||
if (!this.currentCard || this.state !== PLACEMENT_STATE.PLACING_TILE) return false;
|
||||
|
||||
// Sort exits to ensure we find Min first
|
||||
const sorted = [...tileDef.exits].sort((a, b) => {
|
||||
if (a.direction !== b.direction) return a.direction.localeCompare(b.direction);
|
||||
if (a.x !== b.x) return a.x - b.x;
|
||||
return a.y - b.y;
|
||||
});
|
||||
|
||||
for (const ex of sorted) {
|
||||
// Identifier for the "Door Group".
|
||||
// If North/South: ID is "Dir_Y". (X varies)
|
||||
// If East/West: ID is "Dir_X". (Y varies)
|
||||
// Actually, we just need to pick the first one we see (since we sorted by X then Y).
|
||||
// If we have (0,0) and (1,0) for SOUTH. Sorted -> (0,0) comes first.
|
||||
// We take (0,0). We assume (1,0) is part of same door.
|
||||
|
||||
// Heuristic: If this exit is adjacent to the last added unique exit of same direction, skip it.
|
||||
const last = unique[unique.length - 1];
|
||||
let isSameDoor = false;
|
||||
|
||||
if (last && last.direction === ex.direction) {
|
||||
if (ex.direction === DIRECTIONS.NORTH || ex.direction === DIRECTIONS.SOUTH) {
|
||||
// Vertical door, check horizontal adjacency
|
||||
if (last.y === ex.y && Math.abs(last.x - ex.x) <= 1) isSameDoor = true;
|
||||
} else {
|
||||
// Horizontal door, check vertical adjacency
|
||||
if (last.x === ex.x && Math.abs(last.y - ex.y) <= 1) isSameDoor = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSameDoor) {
|
||||
unique.push(ex);
|
||||
}
|
||||
}
|
||||
return unique;
|
||||
const variant = this.currentCard.variants[this.placementRotation];
|
||||
return this.grid.canPlace(variant, this.placementX, this.placementY);
|
||||
}
|
||||
|
||||
getRotatedOffset(localExit, rotation) {
|
||||
// Calculate where the 'localExit' ends up relative to (0,0) after rotation.
|
||||
// localExit is the "Reference" (Min) of the candidate door.
|
||||
|
||||
let rx, ry;
|
||||
const lx = localExit.x;
|
||||
const ly = localExit.y;
|
||||
|
||||
switch (rotation) {
|
||||
case DIRECTIONS.NORTH: rx = lx; ry = ly; break;
|
||||
case DIRECTIONS.SOUTH: rx = -lx; ry = -ly; break;
|
||||
case DIRECTIONS.EAST: rx = ly; ry = -lx; break;
|
||||
case DIRECTIONS.WEST: rx = -ly; ry = lx; break;
|
||||
/**
|
||||
* Confirm and finalize tile placement
|
||||
*/
|
||||
confirmPlacement() {
|
||||
if (this.state !== PLACEMENT_STATE.PLACING_TILE) {
|
||||
console.warn("Not in placement mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
return { x: rx, y: ry };
|
||||
}
|
||||
|
||||
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;
|
||||
if (!this.isPlacementValid()) {
|
||||
console.warn("❌ Cannot place tile - invalid position");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`[confirmPlacement] Placing at (${this.placementX}, ${this.placementY}) rotation: ${this.placementRotation}`);
|
||||
|
||||
// Round to integers (tiles must be on grid cells)
|
||||
const finalX = Math.round(this.placementX);
|
||||
const finalY = Math.round(this.placementY);
|
||||
|
||||
console.log(`[confirmPlacement] Rounded to (${finalX}, ${finalY})`);
|
||||
|
||||
// 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();
|
||||
console.log("✅ Tile placed successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
calculateRequiredRotation(localDir, targetGlobalDir) {
|
||||
const dirs = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST];
|
||||
const localIdx = dirs.indexOf(localDir);
|
||||
const targetIdx = dirs.indexOf(targetGlobalDir);
|
||||
const diff = (targetIdx - localIdx + 4) % 4;
|
||||
return dirs[diff];
|
||||
/**
|
||||
* Internal: Actually place a card on the grid
|
||||
*/
|
||||
placeCardFinal(card, x, y, rotation) {
|
||||
console.log('[placeCardFinal] Card:', card);
|
||||
console.log('[placeCardFinal] Card.variants:', card.variants);
|
||||
console.log('[placeCardFinal] Rotation:', rotation, 'Type:', typeof rotation);
|
||||
|
||||
const variant = card.variants[rotation];
|
||||
console.log('[placeCardFinal] Variant:', variant);
|
||||
|
||||
const instance = {
|
||||
id: `tile_${this.placedTiles.length}`,
|
||||
defId: card.id,
|
||||
x, y, rotation,
|
||||
name: card.name
|
||||
};
|
||||
|
||||
this.grid.placeTile(instance, variant, card);
|
||||
this.placedTiles.push(instance);
|
||||
|
||||
// Update available exits
|
||||
this.updateAvailableExits(instance, variant, x, y);
|
||||
}
|
||||
|
||||
addExitsToQueue(tileInstance, tileDef) {
|
||||
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);
|
||||
/**
|
||||
* Update list of exits player can choose from
|
||||
*/
|
||||
updateAvailableExits(instance, variant, anchorX, anchorY) {
|
||||
console.log('[updateAvailableExits] ===== NUEVO CODIGO ===== Called for tile:', instance.id);
|
||||
console.log('[updateAvailableExits] Variant exits:', variant.exits);
|
||||
console.log('[updateAvailableExits] Anchor:', anchorX, anchorY);
|
||||
|
||||
// Check if blocked immediately
|
||||
const neighbor = this.getNeighborCell(globalPoint.x, globalPoint.y, globalDir);
|
||||
const key = `${neighbor.x},${neighbor.y}`;
|
||||
// Add new exits from this tile
|
||||
for (const ex of variant.exits) {
|
||||
const gx = anchorX + ex.x;
|
||||
const gy = anchorY + ex.y;
|
||||
|
||||
if (!this.grid.occupiedCells.has(key)) {
|
||||
this.pendingExits.push({
|
||||
x: globalPoint.x,
|
||||
y: globalPoint.y,
|
||||
direction: globalDir
|
||||
const leadingTo = this.neighbor(gx, gy, ex.direction);
|
||||
const isOccupied = this.grid.isOccupied(leadingTo.x, leadingTo.y);
|
||||
|
||||
console.log(`[updateAvailableExits] Exit at (${gx}, ${gy}) dir ${ex.direction} -> leads to (${leadingTo.x}, ${leadingTo.y}) occupied: ${isOccupied}`);
|
||||
|
||||
if (!isOccupied) {
|
||||
this.availableExits.push({
|
||||
x: gx,
|
||||
y: gy,
|
||||
direction: ex.direction,
|
||||
tileId: instance.id
|
||||
});
|
||||
console.log('[updateAvailableExits] ✓ Added exit');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[updateAvailableExits] Total available exits now:', this.availableExits.length);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
cleanupPendingExits() {
|
||||
// Remove exits that now point to occupied cells (blocked by newly placed tile)
|
||||
this.pendingExits = this.pendingExits.filter(ex => {
|
||||
const neighbor = this.getNeighborCell(ex.x, ex.y, ex.direction);
|
||||
const key = `${neighbor.x},${neighbor.y}`;
|
||||
return !this.grid.occupiedCells.has(key);
|
||||
});
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
50
src/engine/dungeon/DungeonGenerator.js.temp
Normal file
50
src/engine/dungeon/DungeonGenerator.js.temp
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
tryPlaceCard(card, targetExit) {
|
||||
const requiredDirection = this.opposite(targetExit.direction);
|
||||
const targetCell = this.neighbor(targetExit.x, targetExit.y, targetExit.direction);
|
||||
|
||||
const rotations = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST];
|
||||
// const shuffled = this.shuffle(rotations);
|
||||
|
||||
// 🔧 DEBUG: Force SOUTH rotation only
|
||||
const forcedRotations = [DIRECTIONS.SOUTH];
|
||||
console.log('⚠️ FORCED ROTATION TO SOUTH ONLY FOR TESTING');
|
||||
|
||||
let bestPlacement = null;
|
||||
let maxConnections = -1;
|
||||
|
||||
for (const rotation of forcedRotations) {
|
||||
const rotatedExits = this.rotateExits(card.exits, rotation);
|
||||
const candidates = rotatedExits.filter(e => e.direction === requiredDirection);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const anchorX = targetCell.x - candidate.x;
|
||||
const anchorY = targetCell.y - candidate.y;
|
||||
|
||||
if (this.grid.canPlace(card, anchorX, anchorY, rotation)) {
|
||||
let score = 0;
|
||||
for (const exit of rotatedExits) {
|
||||
const globalX = anchorX + exit.x;
|
||||
const globalY = anchorY + exit.y;
|
||||
const neighbor = this.neighbor(globalX, globalY, exit.direction);
|
||||
if (this.grid.occupiedCells.has(`${neighbor.x},${neighbor.y}`)) {
|
||||
score++;
|
||||
}
|
||||
}
|
||||
|
||||
if (score > maxConnections) {
|
||||
maxConnections = score;
|
||||
bestPlacement = { card, anchorX, anchorY, rotation };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPlacement) {
|
||||
this.placeTileAt(bestPlacement.card, bestPlacement.anchorX, bestPlacement.anchorY, bestPlacement.rotation);
|
||||
console.log(`✅ Best Placement Selected: Score ${maxConnections}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,184 +1,106 @@
|
||||
import { DIRECTIONS } from './Constants.js';
|
||||
|
||||
export class GridSystem {
|
||||
/**
|
||||
* The GridSystem maintains the "Source of Truth" for the dungeon layout.
|
||||
* It knows which cells are occupied and by whom.
|
||||
* Dependencies: Constants.js (DIRECTIONS)
|
||||
*/
|
||||
constructor() {
|
||||
// We use a Map for O(1) lookups.
|
||||
// Key: "x,y" (String) -> Value: "tileId" (String)
|
||||
// Map "x,y" -> "tileId"
|
||||
this.occupiedCells = new Map();
|
||||
|
||||
// We also keep a list of placed tile objects for easier iteration if needed later.
|
||||
this.tiles = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a tile can be placed at the given coordinates with the given rotation.
|
||||
* Needs: The Tile Definition (to know size), the target X,Y, and desired Rotation.
|
||||
* Checks if a specific VARIANT can be placed at anchorX, anchorY.
|
||||
* Does NOT rotate anything. Assumes variant is already the correct shape.
|
||||
*/
|
||||
canPlace(tileDef, startX, startY, rotation) {
|
||||
// 1. Calculate the real-world coordinates of every single cell this tile would occupy.
|
||||
const cells = this.getGlobalCells(tileDef, startX, startY, rotation);
|
||||
canPlace(variant, anchorX, anchorY) {
|
||||
const layout = variant.layout;
|
||||
const rows = layout.length;
|
||||
|
||||
// 2. Check each cell against our Map of occupied spots.
|
||||
for (const cell of cells) {
|
||||
const key = `${cell.x},${cell.y}`;
|
||||
if (this.occupiedCells.has(key)) {
|
||||
return false; // COLLISION! Spot already taken.
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const rowData = layout[row];
|
||||
const cols = rowData.length;
|
||||
|
||||
for (let col = 0; col < cols; col++) {
|
||||
// If cell is 0 (empty), skip
|
||||
if (rowData[col] === 0) continue;
|
||||
|
||||
// Calculate Global Position
|
||||
// Matrix Row 0 is Top (Max Y). Matrix Row Max is Bottom (Y=0).
|
||||
const lx = col;
|
||||
const ly = (rows - 1) - row;
|
||||
|
||||
const gx = anchorX + lx;
|
||||
const gy = anchorY + ly;
|
||||
const key = `${gx},${gy}`;
|
||||
|
||||
// Collision Check
|
||||
if (this.occupiedCells.has(key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true; // All clear.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Officially registers a tile onto the board.
|
||||
* Should only be called AFTER canPlace returns true.
|
||||
* Registers the tile cells as occupied.
|
||||
*/
|
||||
placeTile(tileInstance, tileDef) {
|
||||
const cells = this.getGlobalCells(tileDef, tileInstance.x, tileInstance.y, tileInstance.rotation);
|
||||
placeTile(tileInstance, variant) {
|
||||
const layout = variant.layout;
|
||||
const rows = layout.length;
|
||||
const anchorX = tileInstance.x;
|
||||
const anchorY = tileInstance.y;
|
||||
|
||||
// Record every cell in our Map
|
||||
for (const cell of cells) {
|
||||
const key = `${cell.x},${cell.y}`;
|
||||
this.occupiedCells.set(key, tileInstance.id);
|
||||
}
|
||||
|
||||
// Store the instance
|
||||
this.tiles.push(tileInstance);
|
||||
console.log(`Placed tile ${tileInstance.id} at ${tileInstance.x},${tileInstance.y}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* THE MAGIC MATH FUNCTION.
|
||||
* Converts a simplified abstract tile (width/length) into actual grid coordinates.
|
||||
* Handles the Rotation logic (N, S, E, W).
|
||||
* NOW SUPPORTS: Matrix Layouts (0 = Empty).
|
||||
*/
|
||||
getGlobalCells(tileDef, startX, startY, rotation) {
|
||||
const cells = [];
|
||||
const layout = tileDef.layout;
|
||||
|
||||
// Safety check: if no layout, fallback to full rectangle (optional, but good for stability)
|
||||
// usage: const w = tileDef.width; const l = tileDef.length;
|
||||
|
||||
if (!layout) {
|
||||
console.error("Tile definition missing layout. ID:", tileDef?.id);
|
||||
console.warn("Invalid tileDef object:", tileDef);
|
||||
return cells;
|
||||
}
|
||||
|
||||
const numberOfRows = layout.length; // usually equals tileDef.length
|
||||
|
||||
// Iterate through matrix rows
|
||||
for (let row = 0; row < numberOfRows; row++) {
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const rowData = layout[row];
|
||||
const numberOfCols = rowData.length; // usually equals tileDef.width
|
||||
const cols = rowData.length;
|
||||
|
||||
for (let col = 0; col < numberOfCols; col++) {
|
||||
const cellValue = rowData[col];
|
||||
|
||||
// CRITICAL: Skip empty cells (0)
|
||||
if (cellValue === 0) continue;
|
||||
|
||||
// Map Matrix (Row, Col) to Local Grid (lx, ly)
|
||||
// Matrix Row 0 is the "Top" (Max Y).
|
||||
// Matrix Row (Rows-1) is the "Bottom" (Y=0).
|
||||
// So: ly = (numberOfRows - 1) - row
|
||||
// lx = col
|
||||
for (let col = 0; col < cols; col++) {
|
||||
if (rowData[col] === 0) continue;
|
||||
|
||||
const lx = col;
|
||||
const ly = (numberOfRows - 1) - row;
|
||||
const ly = (rows - 1) - row;
|
||||
|
||||
let gx, gy;
|
||||
const gx = anchorX + lx;
|
||||
const gy = anchorY + ly;
|
||||
const key = `${gx},${gy}`;
|
||||
|
||||
// Apply Rotation to the local (lx, ly) point relative to (0,0) anchor
|
||||
switch (rotation) {
|
||||
case DIRECTIONS.NORTH:
|
||||
// Standard: +X is Right, +Y is Forward
|
||||
gx = startX + lx;
|
||||
gy = startY + ly;
|
||||
break;
|
||||
case DIRECTIONS.SOUTH:
|
||||
// 180 degrees: Extension goes "Backwards" and "Leftwards" relative to pivot
|
||||
gx = startX - lx;
|
||||
gy = startY - ly;
|
||||
break;
|
||||
case DIRECTIONS.EAST:
|
||||
// 90 degrees Clockwise: Width becomes "Length", Length becomes "Width"
|
||||
// x' = y, y' = -x
|
||||
gx = startX + ly;
|
||||
gy = startY - lx;
|
||||
break;
|
||||
case DIRECTIONS.WEST:
|
||||
// 270 degrees Clockwise (or 90 Counter-Clockwise)
|
||||
// x' = -y, y' = x
|
||||
gx = startX - ly;
|
||||
gy = startY + lx;
|
||||
break;
|
||||
default:
|
||||
gx = startX + lx;
|
||||
gy = startY + ly;
|
||||
}
|
||||
this.occupiedCells.set(key, tileInstance.id);
|
||||
}
|
||||
}
|
||||
this.tiles.push(tileInstance);
|
||||
console.log(`[Grid] Placed ${tileInstance.id} at ${anchorX},${anchorY} (Rot: ${tileInstance.rotation})`);
|
||||
}
|
||||
|
||||
// We could also store the 'cellValue' (height) if we wanted.
|
||||
cells.push({ x: gx, y: gy, value: cellValue });
|
||||
/**
|
||||
* Helper to return global cells for logic/renderer without modifying state.
|
||||
*/
|
||||
calculateCells(variant, anchorX, anchorY) {
|
||||
const cells = [];
|
||||
const layout = variant.layout;
|
||||
const rows = layout.length;
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const rowData = layout[row];
|
||||
const cols = rowData.length;
|
||||
|
||||
for (let col = 0; col < cols; col++) {
|
||||
if (rowData[col] === 0) continue;
|
||||
|
||||
const lx = col;
|
||||
const ly = (rows - 1) - row;
|
||||
|
||||
const gx = anchorX + lx;
|
||||
const gy = anchorY + ly;
|
||||
|
||||
cells.push({ x: gx, y: gy, value: rowData[col] });
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a local point (like an exit definition) to Global Coordinates.
|
||||
* Useful for calculating where an exit actually ends up on the board.
|
||||
* Helper to see if a specific global coordinate is occupied
|
||||
*/
|
||||
getGlobalPoint(localX, localY, tileInstance) {
|
||||
let gx, gy;
|
||||
const startX = tileInstance.x;
|
||||
const startY = tileInstance.y;
|
||||
const rotation = tileInstance.rotation;
|
||||
|
||||
switch (rotation) {
|
||||
case DIRECTIONS.NORTH:
|
||||
gx = startX + localX;
|
||||
gy = startY + localY;
|
||||
break;
|
||||
case DIRECTIONS.SOUTH:
|
||||
gx = startX - localX;
|
||||
gy = startY - localY;
|
||||
break;
|
||||
case DIRECTIONS.EAST:
|
||||
gx = startX + localY;
|
||||
gy = startY - localX;
|
||||
break;
|
||||
case DIRECTIONS.WEST:
|
||||
gx = startX - localY;
|
||||
gy = startY + localX;
|
||||
break;
|
||||
default:
|
||||
gx = startX + localX;
|
||||
gy = startY + localY;
|
||||
}
|
||||
return { x: gx, y: gy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a direction (N, S, E, W) by a given amount.
|
||||
* Useful for calculating which way an exit faces after the tile is rotated.
|
||||
*/
|
||||
getRotatedDirection(originalDirection, tileRotation) {
|
||||
// N=0, E=1, S=2, W=3
|
||||
const dirs = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST];
|
||||
const idx = dirs.indexOf(originalDirection);
|
||||
|
||||
let rotationSteps = 0;
|
||||
if (tileRotation === DIRECTIONS.EAST) rotationSteps = 1;
|
||||
if (tileRotation === DIRECTIONS.SOUTH) rotationSteps = 2;
|
||||
if (tileRotation === DIRECTIONS.WEST) rotationSteps = 3;
|
||||
|
||||
const newIdx = (idx + rotationSteps) % 4;
|
||||
return dirs[newIdx];
|
||||
isOccupied(x, y) {
|
||||
return this.occupiedCells.has(`${x},${y}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,151 +1,379 @@
|
||||
|
||||
import { DIRECTIONS, TILE_TYPES } from './Constants.js';
|
||||
|
||||
export const TILES = [
|
||||
{
|
||||
export const TILES = {
|
||||
// -------------------------------------------------------------------------
|
||||
// CORRIDOR STRAIGHT
|
||||
// -------------------------------------------------------------------------
|
||||
'corridor_straight': {
|
||||
id: 'corridor_straight',
|
||||
name: 'Corridor',
|
||||
type: TILE_TYPES.CORRIDOR,
|
||||
width: 2,
|
||||
length: 6,
|
||||
textures: ['/assets/images/dungeon1/tiles/corridor1.png'],
|
||||
layout: [
|
||||
[1, 1], // y=5 (North)
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1] // y=0 (South)
|
||||
],
|
||||
exits: [
|
||||
// South
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
// North
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH },
|
||||
{ x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 2, height: 6,
|
||||
layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 2, height: 6,
|
||||
layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 6, height: 2,
|
||||
layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST },
|
||||
{ x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 6, height: 2,
|
||||
layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST },
|
||||
{ x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// CORRIDOR STEPS
|
||||
// -------------------------------------------------------------------------
|
||||
'corridor_steps': {
|
||||
id: 'corridor_steps',
|
||||
name: 'Steps',
|
||||
type: TILE_TYPES.CORRIDOR,
|
||||
width: 2,
|
||||
length: 6,
|
||||
textures: ['/assets/images/dungeon1/tiles/stairs1.png'],
|
||||
layout: [
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[1, 1]
|
||||
],
|
||||
exits: [
|
||||
// South
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
// North
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH },
|
||||
{ x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 2, height: 6,
|
||||
layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 2, height: 6,
|
||||
layout: [[1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 6, height: 2,
|
||||
layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST },
|
||||
{ x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 6, height: 2,
|
||||
layout: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST },
|
||||
{ x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// CORNER (L-Shape)
|
||||
// -------------------------------------------------------------------------
|
||||
'corridor_corner': {
|
||||
id: 'corridor_corner',
|
||||
name: 'Corner',
|
||||
type: TILE_TYPES.CORRIDOR,
|
||||
width: 4,
|
||||
length: 4,
|
||||
textures: ['/assets/images/dungeon1/tiles/L.png'],
|
||||
layout: [
|
||||
[1, 1, 1, 1], // y=3 (Top)
|
||||
[1, 1, 1, 1], // y=2 (East Exit here at x=3)
|
||||
[1, 1, 0, 0], // y=1
|
||||
[1, 1, 0, 0] // y=0 (South Exit here at x=0,1)
|
||||
],
|
||||
exits: [
|
||||
// South
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
// East
|
||||
{ x: 3, y: 2, direction: DIRECTIONS.EAST },
|
||||
{ x: 3, y: 3, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 0, 0],
|
||||
[1, 1, 0, 0]
|
||||
],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 3, y: 2, direction: DIRECTIONS.EAST }, { x: 3, y: 3, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [
|
||||
[1, 1, 0, 0],
|
||||
[1, 1, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 2, y: 3, direction: DIRECTIONS.NORTH }, { x: 3, y: 3, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 2, y: 3, direction: DIRECTIONS.NORTH }, { x: 3, y: 3, direction: DIRECTIONS.NORTH },
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 3, y: 2, direction: DIRECTIONS.EAST }, { x: 3, y: 3, direction: DIRECTIONS.EAST },
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 0, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// T-JUNCTION
|
||||
// -------------------------------------------------------------------------
|
||||
'junction_t': {
|
||||
id: 'junction_t',
|
||||
name: 'T-Junction',
|
||||
type: TILE_TYPES.JUNCTION,
|
||||
width: 6,
|
||||
length: 4,
|
||||
textures: ['/assets/images/dungeon1/tiles/T.png'],
|
||||
layout: [
|
||||
[1, 1, 1, 1, 1, 1], // y=3
|
||||
[1, 1, 1, 1, 1, 1], // y=2 (West at x=0, East at x=5)
|
||||
[0, 0, 1, 1, 0, 0], // y=1
|
||||
[0, 0, 1, 1, 0, 0] // y=0 (South at x=2,3)
|
||||
],
|
||||
exits: [
|
||||
// South
|
||||
{ x: 2, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 3, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
// West
|
||||
{ x: 0, y: 2, direction: DIRECTIONS.WEST },
|
||||
{ x: 0, y: 3, direction: DIRECTIONS.WEST },
|
||||
// East
|
||||
{ x: 5, y: 2, direction: DIRECTIONS.EAST },
|
||||
{ x: 5, y: 3, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 6, height: 4,
|
||||
layout: [
|
||||
[1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1],
|
||||
[0, 0, 1, 1, 0, 0],
|
||||
[0, 0, 1, 1, 0, 0]
|
||||
],
|
||||
exits: [
|
||||
{ x: 2, y: 0, direction: DIRECTIONS.SOUTH }, { x: 3, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 0, y: 2, direction: DIRECTIONS.WEST }, { x: 0, y: 3, direction: DIRECTIONS.WEST },
|
||||
{ x: 5, y: 2, direction: DIRECTIONS.EAST }, { x: 5, y: 3, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 4, height: 6,
|
||||
layout: [
|
||||
[1, 1, 0, 0],
|
||||
[1, 1, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 0, 0],
|
||||
[1, 1, 0, 0]
|
||||
],
|
||||
exits: [
|
||||
{ x: 2, y: 0, direction: DIRECTIONS.SOUTH }, { x: 3, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 2, y: 5, direction: DIRECTIONS.NORTH }, { x: 3, y: 5, direction: DIRECTIONS.NORTH },
|
||||
{ x: 0, y: 2, direction: DIRECTIONS.WEST }, { x: 0, y: 3, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 6, height: 4,
|
||||
layout: [
|
||||
[0, 0, 1, 1, 0, 0],
|
||||
[0, 0, 1, 1, 0, 0],
|
||||
[1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 2, y: 3, direction: DIRECTIONS.NORTH }, { x: 3, y: 3, direction: DIRECTIONS.NORTH },
|
||||
{ x: 5, y: 0, direction: DIRECTIONS.EAST }, { x: 5, y: 1, direction: DIRECTIONS.EAST },
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.WEST }, { x: 0, y: 1, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 4, height: 6,
|
||||
layout: [
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 3, y: 2, direction: DIRECTIONS.EAST }, { x: 3, y: 3, direction: DIRECTIONS.EAST },
|
||||
{ x: 0, y: 5, direction: DIRECTIONS.NORTH }, { x: 1, y: 5, direction: DIRECTIONS.NORTH },
|
||||
{ x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ROOM DUNGEON
|
||||
// -------------------------------------------------------------------------
|
||||
'room_dungeon': {
|
||||
id: 'room_dungeon',
|
||||
name: 'Dungeon Room',
|
||||
type: TILE_TYPES.ROOM,
|
||||
width: 4,
|
||||
length: 4,
|
||||
textures: [
|
||||
'/assets/images/dungeon1/tiles/room_4x4_circle.png',
|
||||
'/assets/images/dungeon1/tiles/room_4x4_orange.png',
|
||||
'/assets/images/dungeon1/tiles/room_4x4_squeleton.png'
|
||||
],
|
||||
layout: [
|
||||
[1, 1, 1, 1], // y=3 (North Exit at x=1,2)
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1] // y=0 (South Exit at x=1,2)
|
||||
],
|
||||
exits: [
|
||||
// South
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 2, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
// North
|
||||
{ x: 1, y: 3, direction: DIRECTIONS.NORTH },
|
||||
{ x: 2, y: 3, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH }, { x: 2, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 1, y: 3, direction: DIRECTIONS.NORTH }, { x: 2, y: 3, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 2, direction: DIRECTIONS.WEST },
|
||||
{ x: 3, y: 1, direction: DIRECTIONS.EAST }, { x: 3, y: 2, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 1, y: 3, direction: DIRECTIONS.NORTH }, { x: 2, y: 3, direction: DIRECTIONS.NORTH },
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH }, { x: 2, y: 0, direction: DIRECTIONS.SOUTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 3, y: 1, direction: DIRECTIONS.EAST }, { x: 3, y: 2, direction: DIRECTIONS.EAST },
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 2, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ROOM OBJECTIVE
|
||||
// -------------------------------------------------------------------------
|
||||
'room_objective': {
|
||||
id: 'room_objective',
|
||||
name: 'Dungeon Room',
|
||||
type: TILE_TYPES.ROOM,
|
||||
textures: [
|
||||
'/assets/images/dungeon1/tiles/room_4x4_circle.png',
|
||||
'/assets/images/dungeon1/tiles/room_4x4_orange.png',
|
||||
'/assets/images/dungeon1/tiles/room_4x4_squeleton.png'
|
||||
],
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH }, { x: 2, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 1, y: 3, direction: DIRECTIONS.NORTH }, { x: 2, y: 3, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 2, direction: DIRECTIONS.WEST },
|
||||
{ x: 3, y: 1, direction: DIRECTIONS.EAST }, { x: 3, y: 2, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 1, y: 3, direction: DIRECTIONS.NORTH }, { x: 2, y: 3, direction: DIRECTIONS.NORTH },
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH }, { x: 2, y: 0, direction: DIRECTIONS.SOUTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 4, height: 4,
|
||||
layout: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
|
||||
exits: [
|
||||
{ x: 3, y: 1, direction: DIRECTIONS.EAST }, { x: 3, y: 2, direction: DIRECTIONS.EAST },
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 2, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ROOM OBJECTIVE
|
||||
// -------------------------------------------------------------------------
|
||||
'room_objective': {
|
||||
id: 'room_objective',
|
||||
name: 'Objective Room',
|
||||
type: TILE_TYPES.OBJECTIVE_ROOM,
|
||||
width: 4,
|
||||
length: 8,
|
||||
textures: [
|
||||
'/assets/images/dungeon1/tiles/room_4x8_altar.png',
|
||||
'/assets/images/dungeon1/tiles/room_4x8_tomb.png'
|
||||
],
|
||||
layout: [
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1],
|
||||
[1, 1, 1, 1] // South Exit
|
||||
],
|
||||
exits: [
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH },
|
||||
{ x: 2, y: 0, direction: DIRECTIONS.SOUTH }
|
||||
]
|
||||
variants: {
|
||||
[DIRECTIONS.NORTH]: {
|
||||
width: 4, height: 8,
|
||||
layout: [
|
||||
[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1],
|
||||
[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 1, y: 0, direction: DIRECTIONS.SOUTH }, { x: 2, y: 0, direction: DIRECTIONS.SOUTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.EAST]: {
|
||||
width: 8, height: 4,
|
||||
layout: [
|
||||
[1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 0, y: 1, direction: DIRECTIONS.WEST }, { x: 0, y: 2, direction: DIRECTIONS.WEST }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.SOUTH]: {
|
||||
width: 4, height: 8,
|
||||
layout: [
|
||||
[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1],
|
||||
[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 1, y: 7, direction: DIRECTIONS.NORTH }, { x: 2, y: 7, direction: DIRECTIONS.NORTH }
|
||||
]
|
||||
},
|
||||
[DIRECTIONS.WEST]: {
|
||||
width: 8, height: 4,
|
||||
layout: [
|
||||
[1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
exits: [
|
||||
{ x: 7, y: 1, direction: DIRECTIONS.EAST }, { x: 7, y: 2, direction: DIRECTIONS.EAST }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
181
src/engine/dungeon/old/DungeonGenerator.js
Normal file
181
src/engine/dungeon/old/DungeonGenerator.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import { DIRECTIONS } from './Constants.js';
|
||||
import { GridSystem } from './GridSystem.js';
|
||||
import { DungeonDeck } from './DungeonDeck.js';
|
||||
|
||||
export class DungeonGenerator {
|
||||
constructor() {
|
||||
this.grid = new GridSystem();
|
||||
this.deck = new DungeonDeck();
|
||||
this.pendingExits = [];
|
||||
this.placedTiles = [];
|
||||
this.isComplete = false;
|
||||
}
|
||||
|
||||
startDungeon(missionConfig) {
|
||||
const objectiveId = missionConfig?.type === 'quest' ? 'room_objective' : 'room_dungeon';
|
||||
this.deck.generateMissionDeck(objectiveId);
|
||||
|
||||
const firstCard = this.deck.draw();
|
||||
if (!firstCard) {
|
||||
console.error("❌ Deck empty!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Place first tile with NORTH variant
|
||||
this.placeTileAt(firstCard, 0, 0, DIRECTIONS.NORTH);
|
||||
console.log(`🏰 Dungeon started with ${firstCard.name}`);
|
||||
}
|
||||
|
||||
step() {
|
||||
if (this.isComplete || this.pendingExits.length === 0) {
|
||||
this.isComplete = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const card = this.deck.draw();
|
||||
if (!card) {
|
||||
this.isComplete = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetExit = this.pendingExits.shift();
|
||||
const placed = this.tryPlaceCard(card, targetExit);
|
||||
|
||||
if (!placed) {
|
||||
this.pendingExits.push(targetExit);
|
||||
console.warn(`⚠️ Failed to place ${card.name} - returning exit to pending queue`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
tryPlaceCard(card, targetExit) {
|
||||
console.log(`\n🃏 Trying to place card: ${card.name} (ID: ${card.id})`);
|
||||
|
||||
// Use standard opposite logic for Directions (String) since names don't change
|
||||
const requiredDirection = this.opposite(targetExit.direction);
|
||||
const targetCell = this.neighbor(targetExit.x, targetExit.y, targetExit.direction);
|
||||
|
||||
const rotations = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST];
|
||||
const shuffled = this.shuffle(rotations);
|
||||
|
||||
let bestPlacement = null;
|
||||
let maxConnections = -1;
|
||||
|
||||
// Try ALL rotations
|
||||
for (const rotation of shuffled) {
|
||||
// 1. Get the pre-calculated VARIANT for this rotation
|
||||
const variant = card.variants[rotation];
|
||||
if (!variant) continue;
|
||||
|
||||
const variantExits = variant.exits;
|
||||
|
||||
// 2. Find candidate exits on this variant that face the required direction
|
||||
// Note: variant.exits are ALREADY correctly oriented for this rotation
|
||||
const candidates = variantExits.filter(e => e.direction === requiredDirection);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
// Determine absolute anchor position
|
||||
// Since variant coords are all positive relative to (0,0), anchor calculation is simple subtraction
|
||||
const anchorX = targetCell.x - candidate.x;
|
||||
const anchorY = targetCell.y - candidate.y;
|
||||
|
||||
if (this.grid.canPlace(card, anchorX, anchorY, rotation)) {
|
||||
// Start Score at 0
|
||||
let score = 0;
|
||||
|
||||
// Calculate score
|
||||
for (const exit of variantExits) {
|
||||
const globalX = anchorX + exit.x;
|
||||
const globalY = anchorY + exit.y;
|
||||
|
||||
const neighbor = this.neighbor(globalX, globalY, exit.direction);
|
||||
const neighborKey = `${neighbor.x},${neighbor.y}`;
|
||||
|
||||
if (this.grid.occupiedCells.has(neighborKey)) {
|
||||
score++;
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer higher score.
|
||||
if (score > maxConnections) {
|
||||
maxConnections = score;
|
||||
bestPlacement = { card, anchorX, anchorY, rotation };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPlacement) {
|
||||
this.placeTileAt(bestPlacement.card, bestPlacement.anchorX, bestPlacement.anchorY, bestPlacement.rotation);
|
||||
console.log(`✅ Placed ${card.name} at (${bestPlacement.anchorX}, ${bestPlacement.anchorY}) Rot:${bestPlacement.rotation} Score:${maxConnections}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`❌ Could not place ${card.name} in any rotation`);
|
||||
return false;
|
||||
}
|
||||
|
||||
placeTileAt(tileDef, x, y, rotation) {
|
||||
const instance = {
|
||||
id: `tile_${this.placedTiles.length}`,
|
||||
defId: tileDef.id,
|
||||
x, y, rotation
|
||||
};
|
||||
|
||||
this.grid.placeTile(instance, tileDef); // Grid system now handles looking up the variant
|
||||
this.placedTiles.push(instance);
|
||||
|
||||
// Add exits from the variant
|
||||
const variant = tileDef.variants[rotation];
|
||||
|
||||
for (const exit of variant.exits) {
|
||||
const globalX = x + exit.x;
|
||||
const globalY = y + exit.y;
|
||||
const neighborCell = this.neighbor(globalX, globalY, exit.direction);
|
||||
const key = `${neighborCell.x},${neighborCell.y}`;
|
||||
|
||||
// Only add exit if it leads to empty space
|
||||
if (!this.grid.occupiedCells.has(key)) {
|
||||
this.pendingExits.push({
|
||||
x: globalX,
|
||||
y: globalY,
|
||||
direction: exit.direction
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup pending exits that are now connected
|
||||
this.pendingExits = this.pendingExits.filter(ex => {
|
||||
const n = this.neighbor(ex.x, ex.y, ex.direction);
|
||||
return !this.grid.occupiedCells.has(`${n.x},${n.y}`);
|
||||
});
|
||||
}
|
||||
|
||||
neighbor(x, y, direction) {
|
||||
switch (direction) {
|
||||
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(direction) {
|
||||
switch (direction) {
|
||||
case DIRECTIONS.NORTH: return DIRECTIONS.SOUTH;
|
||||
case DIRECTIONS.SOUTH: return DIRECTIONS.NORTH;
|
||||
case DIRECTIONS.EAST: return DIRECTIONS.WEST;
|
||||
case DIRECTIONS.WEST: return DIRECTIONS.EAST;
|
||||
}
|
||||
}
|
||||
|
||||
shuffle(arr) {
|
||||
const copy = [...arr];
|
||||
for (let i = copy.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[copy[i], copy[j]] = [copy[j], copy[i]];
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
134
src/engine/dungeon/old/GridSystem.js
Normal file
134
src/engine/dungeon/old/GridSystem.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import { DIRECTIONS } from './Constants.js';
|
||||
|
||||
export class GridSystem {
|
||||
/**
|
||||
* The GridSystem maintains the "Source of Truth" for the dungeon layout.
|
||||
* It knows which cells are occupied and by whom.
|
||||
* Dependencies: Constants.js (DIRECTIONS)
|
||||
*/
|
||||
constructor() {
|
||||
// We use a Map for O(1) lookups.
|
||||
// Key: "x,y" (String) -> Value: "tileId" (String)
|
||||
this.occupiedCells = new Map();
|
||||
|
||||
// We also keep a list of placed tile objects for easier iteration if needed later.
|
||||
this.tiles = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a tile can be placed at the given coordinates with the given rotation.
|
||||
* Needs: The Tile Definition (to know size), the target X,Y, and desired Rotation.
|
||||
*/
|
||||
canPlace(tileDef, startX, startY, rotation) {
|
||||
// 1. Calculate the real-world coordinates of every single cell this tile would occupy.
|
||||
const cells = this.getGlobalCells(tileDef, startX, startY, rotation);
|
||||
|
||||
// 2. Check each cell against our Map of occupied spots.
|
||||
for (const cell of cells) {
|
||||
const key = `${cell.x},${cell.y}`;
|
||||
if (this.occupiedCells.has(key)) {
|
||||
return false; // COLLISION! Spot already taken.
|
||||
}
|
||||
}
|
||||
return true; // All clear.
|
||||
}
|
||||
|
||||
/**
|
||||
* Officially registers a tile onto the board.
|
||||
* Should only be called AFTER canPlace returns true.
|
||||
*/
|
||||
placeTile(tileInstance, tileDef) {
|
||||
const cells = this.getGlobalCells(tileDef, tileInstance.x, tileInstance.y, tileInstance.rotation);
|
||||
|
||||
// Record every cell in our Map
|
||||
for (const cell of cells) {
|
||||
const key = `${cell.x},${cell.y}`;
|
||||
this.occupiedCells.set(key, tileInstance.id);
|
||||
}
|
||||
|
||||
// Store the instance
|
||||
this.tiles.push(tileInstance);
|
||||
console.log(`Placed tile ${tileInstance.id} at ${tileInstance.x},${tileInstance.y}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* THE CORRECTED MAGIC MATH FUNCTION.
|
||||
* Now uses Pre-Calculated Variants from TileDefinitions.
|
||||
* NO DYNAMIC ROTATION MATH performed here.
|
||||
* All variants are treated as "North" oriented blocks.
|
||||
*/
|
||||
getGlobalCells(tileDef, startX, startY, rotation) {
|
||||
const cells = [];
|
||||
|
||||
// 1. Retrieve the specific variant for this rotation
|
||||
const variant = tileDef.variants[rotation];
|
||||
if (!variant) {
|
||||
console.error(`Missing variant for rotation ${rotation} in tile ${tileDef.id}`);
|
||||
return cells;
|
||||
}
|
||||
|
||||
const layout = variant.layout;
|
||||
|
||||
if (!layout) {
|
||||
console.error("Variant missing layout. ID:", tileDef?.id, rotation);
|
||||
return cells;
|
||||
}
|
||||
|
||||
const numberOfRows = layout.length;
|
||||
|
||||
// Iterate through matrix rows
|
||||
for (let row = 0; row < numberOfRows; row++) {
|
||||
const rowData = layout[row];
|
||||
const numberOfCols = rowData.length;
|
||||
|
||||
for (let col = 0; col < numberOfCols; col++) {
|
||||
const cellValue = rowData[col];
|
||||
|
||||
// Skip empty cells (0)
|
||||
if (cellValue === 0) continue;
|
||||
|
||||
// Map Matrix (Row, Col) to Local Grid (lx, ly)
|
||||
// Matrix Row 0 is the "Top" (Max Y) relative to the tile's origin
|
||||
// Matrix Row (Rows-1) is the "Bottom" (Y=0)
|
||||
// lx grows right (col)
|
||||
// ly grows up (rows reversed)
|
||||
const lx = col;
|
||||
const ly = (numberOfRows - 1) - row;
|
||||
|
||||
// SIMPLIFIED LOGIC:
|
||||
// Since the variant layout is ALREADY rotated, we always just ADD the offsets.
|
||||
// We treat every variant as if it's placed "North-wise" at the anchor point.
|
||||
|
||||
const gx = startX + lx;
|
||||
const gy = startY + ly;
|
||||
|
||||
cells.push({ x: gx, y: gy, value: cellValue });
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a local point (like an exit definition) to Global Coordinates.
|
||||
* Simplified: Just adds offsets because variants carry pre-rotated coords.
|
||||
*/
|
||||
getGlobalPoint(localX, localY, tileInstance) {
|
||||
const startX = tileInstance.x;
|
||||
const startY = tileInstance.y;
|
||||
|
||||
// Simple translation. Rotation is handled by the variant properties upstream.
|
||||
return {
|
||||
x: startX + localX,
|
||||
y: startY + localY
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATED / LEGACY
|
||||
* Kept just in case, but shouldn't be needed with explicit variants.
|
||||
*/
|
||||
getRotatedDirection(originalDirection, tileRotation) {
|
||||
// With explicit variants, the exit.direction is ALREADY the final global direction.
|
||||
return originalDirection;
|
||||
}
|
||||
}
|
||||
@@ -1,196 +1,105 @@
|
||||
|
||||
import { DungeonGenerator } from '../dungeon/DungeonGenerator.js';
|
||||
import { TurnManager } from './TurnManager.js';
|
||||
import { GAME_PHASES } from './GameConstants.js';
|
||||
import { Entity } from './Entity.js';
|
||||
|
||||
/**
|
||||
* GameEngine for Manual Dungeon Construction with Player Movement
|
||||
*/
|
||||
export class GameEngine {
|
||||
constructor() {
|
||||
this.dungeon = new DungeonGenerator();
|
||||
this.turnManager = new TurnManager();
|
||||
this.missionConfig = null;
|
||||
|
||||
this.player = null;
|
||||
this.selectedEntity = null;
|
||||
this.isRunning = false;
|
||||
|
||||
this.selectedPath = [];
|
||||
this.selectedEntityId = null;
|
||||
|
||||
// Simple event callbacks (external systems can assign these)
|
||||
this.onPathChange = null; // (path) => {}
|
||||
this.onEntityUpdate = null; // (entity) => {}
|
||||
this.onEntityMove = null; // (entity, path) => {}
|
||||
this.onEntitySelect = null; // (entityId, isSelected) => {}
|
||||
|
||||
this.setupEventHooks();
|
||||
}
|
||||
|
||||
setupEventHooks() {
|
||||
this.turnManager.on('phase_changed', (phase) => this.handlePhaseChange(phase));
|
||||
// Callbacks
|
||||
this.onEntityUpdate = null;
|
||||
this.onEntityMove = null;
|
||||
this.onEntitySelect = null;
|
||||
this.onPathChange = null;
|
||||
}
|
||||
|
||||
startMission(missionConfig) {
|
||||
this.missionConfig = missionConfig;
|
||||
console.log(`[GameEngine] Starting mission: ${missionConfig.name}`);
|
||||
|
||||
// 1. Initialize Dungeon (Places the first room)
|
||||
console.log('[GameEngine] Starting mission:', missionConfig.name);
|
||||
this.dungeon.startDungeon(missionConfig);
|
||||
|
||||
// 2. Initialize Player
|
||||
// Find a valid starting spot (first occupied cell)
|
||||
const startingSpot = this.dungeon.grid.occupiedCells.keys().next().value;
|
||||
const [startX, startY] = startingSpot ? startingSpot.split(',').map(Number) : [0, 0];
|
||||
// Create player at center of first tile
|
||||
this.createPlayer(1.5, 2.5); // Center of 2x6 corridor
|
||||
|
||||
console.log(`[GameEngine] Spawning player at valid spot: ${startX}, ${startY}`);
|
||||
this.player = new Entity('p1', 'Barbaro', 'hero', startX, startY, '/assets/images/dungeon1/standees/barbaro.png');
|
||||
if (this.onEntityUpdate) this.onEntityUpdate(this.player);
|
||||
|
||||
// 3. Start the Turn Sequence
|
||||
this.turnManager.startGame();
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
// Continuous logic
|
||||
createPlayer(x, y) {
|
||||
this.player = {
|
||||
id: 'p1',
|
||||
name: 'Barbarian',
|
||||
x: Math.floor(x),
|
||||
y: Math.floor(y),
|
||||
texturePath: '/assets/images/dungeon1/standees/barbaro.png'
|
||||
};
|
||||
|
||||
if (this.onEntityUpdate) {
|
||||
this.onEntityUpdate(this.player);
|
||||
}
|
||||
|
||||
console.log('[GameEngine] Player created at', this.player.x, this.player.y);
|
||||
}
|
||||
|
||||
handlePhaseChange(phase) {
|
||||
console.log(`[GameEngine] Phase Changed to: ${phase}`);
|
||||
}
|
||||
|
||||
// --- Interaction ---
|
||||
onCellClick(x, y) {
|
||||
if (this.turnManager.currentPhase !== GAME_PHASES.HERO) return;
|
||||
|
||||
console.log(`Cell Click: ${x},${y}`);
|
||||
|
||||
// 1. Selector Check (Click on Player?)
|
||||
if (this.player.x === x && this.player.y === y) {
|
||||
if (this.selectedEntityId === this.player.id) {
|
||||
// Deselect
|
||||
this.selectedEntityId = null;
|
||||
console.log("Player Deselected");
|
||||
if (this.onEntitySelect) this.onEntitySelect(this.player.id, false);
|
||||
|
||||
// Clear path too
|
||||
this.selectedPath = [];
|
||||
this._notifyPath();
|
||||
} else {
|
||||
// Select
|
||||
this.selectedEntityId = this.player.id;
|
||||
console.log("Player Selected");
|
||||
if (this.onEntitySelect) this.onEntitySelect(this.player.id, true);
|
||||
// If no player selected, select player on click
|
||||
if (!this.selectedEntity && this.player && x === this.player.x && y === this.player.y) {
|
||||
this.selectedEntity = this.player;
|
||||
if (this.onEntitySelect) {
|
||||
this.onEntitySelect(this.player.id, true);
|
||||
}
|
||||
console.log('[GameEngine] Player selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// If nothing selected, ignore floor clicks
|
||||
if (!this.selectedEntityId) return;
|
||||
|
||||
// 2. Check if valid Floor (Occupied Cell)
|
||||
if (!this.dungeon.grid.occupiedCells.has(`${x},${y}`)) {
|
||||
console.log("Invalid cell: Void");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Logic: Path Building (Only if Selected)
|
||||
|
||||
// A. If clicking on last selected -> Deselect (Remove last step)
|
||||
if (this.selectedPath.length > 0) {
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
if (last.x === x && last.y === y) {
|
||||
this.selectedPath.pop();
|
||||
this._notifyPath();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// B. Determine Previous Point (Player or Last Path Node)
|
||||
let prevX = this.player.x;
|
||||
let prevY = this.player.y;
|
||||
|
||||
if (this.selectedPath.length > 0) {
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
prevX = last.x;
|
||||
prevY = last.y;
|
||||
}
|
||||
|
||||
// Note: Manhattan distance 1 = Adjacency (No diagonals)
|
||||
const dist = Math.abs(x - prevX) + Math.abs(y - prevY);
|
||||
|
||||
if (dist === 1) {
|
||||
// Check Path Length Limit (Speed)
|
||||
if (this.selectedPath.length < this.player.stats.move) {
|
||||
this.selectedPath.push({ x, y });
|
||||
this._notifyPath();
|
||||
// If player selected, move to clicked cell
|
||||
if (this.selectedEntity === this.player) {
|
||||
if (this.canMoveTo(x, y)) {
|
||||
this.movePlayer(x, y);
|
||||
} else {
|
||||
console.log("Max movement reached");
|
||||
}
|
||||
} else {
|
||||
// Restart path if clicking adjacent to player
|
||||
const distFromPlayer = Math.abs(x - this.player.x) + Math.abs(y - this.player.y);
|
||||
if (distFromPlayer === 1) {
|
||||
this.selectedPath = [{ x, y }];
|
||||
this._notifyPath();
|
||||
console.log('[GameEngine] Cannot move there');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCellRightClick(x, y) {
|
||||
if (this.turnManager.currentPhase !== GAME_PHASES.HERO) return;
|
||||
if (!this.selectedEntityId) return; // Must satisfy selection rule
|
||||
canMoveTo(x, y) {
|
||||
// Check if cell is walkable (occupied by a tile)
|
||||
return this.dungeon.grid.isOccupied(x, y);
|
||||
}
|
||||
|
||||
// Must be clicking the last tile of the path to confirm
|
||||
if (this.selectedPath.length === 0) return;
|
||||
movePlayer(x, y) {
|
||||
// Simple direct movement (no pathfinding for now)
|
||||
const path = [{ x, y }];
|
||||
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
if (last.x === x && last.y === y) {
|
||||
console.log("Confirming Move...");
|
||||
this.confirmMove();
|
||||
this.player.x = x;
|
||||
this.player.y = y;
|
||||
|
||||
if (this.onEntityMove) {
|
||||
this.onEntityMove(this.player, path);
|
||||
}
|
||||
}
|
||||
|
||||
confirmMove() {
|
||||
if (this.selectedPath.length === 0) return;
|
||||
|
||||
const target = this.selectedPath[this.selectedPath.length - 1];
|
||||
const pathCopy = [...this.selectedPath];
|
||||
|
||||
// 1. Trigger Animation Sequence
|
||||
if (this.onEntityMove) this.onEntityMove(this.player, pathCopy);
|
||||
|
||||
// 2. Update Logical Position
|
||||
this.player.setPosition(target.x, target.y);
|
||||
|
||||
// 3. Cleanup
|
||||
this.selectedPath = [];
|
||||
this._notifyPath();
|
||||
|
||||
// Note: Exploration is now manual via door interaction
|
||||
}
|
||||
|
||||
_notifyPath() {
|
||||
if (this.onPathChange) this.onPathChange(this.selectedPath);
|
||||
}
|
||||
|
||||
exploreExit(exitCell) {
|
||||
console.log('[GameEngine] Exploring exit:', exitCell);
|
||||
|
||||
// Find this exit in pendingExits
|
||||
const exit = this.dungeon.pendingExits.find(ex => ex.x === exitCell.x && ex.y === exitCell.y);
|
||||
|
||||
if (exit) {
|
||||
// Prioritize this exit
|
||||
const idx = this.dungeon.pendingExits.indexOf(exit);
|
||||
if (idx > -1) {
|
||||
this.dungeon.pendingExits.splice(idx, 1);
|
||||
this.dungeon.pendingExits.unshift(exit);
|
||||
}
|
||||
|
||||
// Trigger exploration
|
||||
this.turnManager.triggerExploration();
|
||||
this.dungeon.step();
|
||||
this.turnManager.setPhase(GAME_PHASES.HERO);
|
||||
} else {
|
||||
console.warn('[GameEngine] Exit not found in pendingExits');
|
||||
// Deselect after move
|
||||
this.selectedEntity = null;
|
||||
if (this.onEntitySelect) {
|
||||
this.onEntitySelect(this.player.id, false);
|
||||
}
|
||||
|
||||
console.log('[GameEngine] Player moved to', x, y);
|
||||
}
|
||||
|
||||
isPlayerAdjacentToDoor(doorExit) {
|
||||
if (!this.player) return false;
|
||||
|
||||
const dx = Math.abs(this.player.x - doorExit.x);
|
||||
const dy = Math.abs(this.player.y - doorExit.y);
|
||||
|
||||
// Adjacent means distance of 1 in one direction and 0 in the other
|
||||
return (dx === 1 && dy === 0) || (dx === 0 && dy === 1);
|
||||
}
|
||||
|
||||
update(time) {
|
||||
// Minimal update loop
|
||||
}
|
||||
}
|
||||
|
||||
196
src/engine/game/old/GameEngine_20260102.js
Normal file
196
src/engine/game/old/GameEngine_20260102.js
Normal file
@@ -0,0 +1,196 @@
|
||||
|
||||
import { DungeonGenerator } from '../dungeon/DungeonGenerator.js';
|
||||
import { TurnManager } from './TurnManager.js';
|
||||
import { GAME_PHASES } from './GameConstants.js';
|
||||
import { Entity } from './Entity.js';
|
||||
|
||||
export class GameEngine {
|
||||
constructor() {
|
||||
this.dungeon = new DungeonGenerator();
|
||||
this.turnManager = new TurnManager();
|
||||
this.missionConfig = null;
|
||||
|
||||
this.player = null;
|
||||
|
||||
this.selectedPath = [];
|
||||
this.selectedEntityId = null;
|
||||
|
||||
// Simple event callbacks (external systems can assign these)
|
||||
this.onPathChange = null; // (path) => {}
|
||||
this.onEntityUpdate = null; // (entity) => {}
|
||||
this.onEntityMove = null; // (entity, path) => {}
|
||||
this.onEntitySelect = null; // (entityId, isSelected) => {}
|
||||
|
||||
this.setupEventHooks();
|
||||
}
|
||||
|
||||
setupEventHooks() {
|
||||
this.turnManager.on('phase_changed', (phase) => this.handlePhaseChange(phase));
|
||||
}
|
||||
|
||||
startMission(missionConfig) {
|
||||
this.missionConfig = missionConfig;
|
||||
console.log(`[GameEngine] Starting mission: ${missionConfig.name}`);
|
||||
|
||||
// 1. Initialize Dungeon (Places the first room)
|
||||
this.dungeon.startDungeon(missionConfig);
|
||||
|
||||
// 2. Initialize Player
|
||||
// Find a valid starting spot (first occupied cell)
|
||||
const startingSpot = this.dungeon.grid.occupiedCells.keys().next().value;
|
||||
const [startX, startY] = startingSpot ? startingSpot.split(',').map(Number) : [0, 0];
|
||||
|
||||
console.log(`[GameEngine] Spawning player at valid spot: ${startX}, ${startY}`);
|
||||
this.player = new Entity('p1', 'Barbaro', 'hero', startX, startY, '/assets/images/dungeon1/standees/barbaro.png');
|
||||
if (this.onEntityUpdate) this.onEntityUpdate(this.player);
|
||||
|
||||
// 3. Start the Turn Sequence
|
||||
this.turnManager.startGame();
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
// Continuous logic
|
||||
}
|
||||
|
||||
handlePhaseChange(phase) {
|
||||
console.log(`[GameEngine] Phase Changed to: ${phase}`);
|
||||
}
|
||||
|
||||
// --- Interaction ---
|
||||
onCellClick(x, y) {
|
||||
if (this.turnManager.currentPhase !== GAME_PHASES.HERO) return;
|
||||
|
||||
console.log(`Cell Click: ${x},${y}`);
|
||||
|
||||
// 1. Selector Check (Click on Player?)
|
||||
if (this.player.x === x && this.player.y === y) {
|
||||
if (this.selectedEntityId === this.player.id) {
|
||||
// Deselect
|
||||
this.selectedEntityId = null;
|
||||
console.log("Player Deselected");
|
||||
if (this.onEntitySelect) this.onEntitySelect(this.player.id, false);
|
||||
|
||||
// Clear path too
|
||||
this.selectedPath = [];
|
||||
this._notifyPath();
|
||||
} else {
|
||||
// Select
|
||||
this.selectedEntityId = this.player.id;
|
||||
console.log("Player Selected");
|
||||
if (this.onEntitySelect) this.onEntitySelect(this.player.id, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If nothing selected, ignore floor clicks
|
||||
if (!this.selectedEntityId) return;
|
||||
|
||||
// 2. Check if valid Floor (Occupied Cell)
|
||||
if (!this.dungeon.grid.occupiedCells.has(`${x},${y}`)) {
|
||||
console.log("Invalid cell: Void");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Logic: Path Building (Only if Selected)
|
||||
|
||||
// A. If clicking on last selected -> Deselect (Remove last step)
|
||||
if (this.selectedPath.length > 0) {
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
if (last.x === x && last.y === y) {
|
||||
this.selectedPath.pop();
|
||||
this._notifyPath();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// B. Determine Previous Point (Player or Last Path Node)
|
||||
let prevX = this.player.x;
|
||||
let prevY = this.player.y;
|
||||
|
||||
if (this.selectedPath.length > 0) {
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
prevX = last.x;
|
||||
prevY = last.y;
|
||||
}
|
||||
|
||||
// Note: Manhattan distance 1 = Adjacency (No diagonals)
|
||||
const dist = Math.abs(x - prevX) + Math.abs(y - prevY);
|
||||
|
||||
if (dist === 1) {
|
||||
// Check Path Length Limit (Speed)
|
||||
if (this.selectedPath.length < this.player.stats.move) {
|
||||
this.selectedPath.push({ x, y });
|
||||
this._notifyPath();
|
||||
} else {
|
||||
console.log("Max movement reached");
|
||||
}
|
||||
} else {
|
||||
// Restart path if clicking adjacent to player
|
||||
const distFromPlayer = Math.abs(x - this.player.x) + Math.abs(y - this.player.y);
|
||||
if (distFromPlayer === 1) {
|
||||
this.selectedPath = [{ x, y }];
|
||||
this._notifyPath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCellRightClick(x, y) {
|
||||
if (this.turnManager.currentPhase !== GAME_PHASES.HERO) return;
|
||||
if (!this.selectedEntityId) return; // Must satisfy selection rule
|
||||
|
||||
// Must be clicking the last tile of the path to confirm
|
||||
if (this.selectedPath.length === 0) return;
|
||||
|
||||
const last = this.selectedPath[this.selectedPath.length - 1];
|
||||
if (last.x === x && last.y === y) {
|
||||
console.log("Confirming Move...");
|
||||
this.confirmMove();
|
||||
}
|
||||
}
|
||||
|
||||
confirmMove() {
|
||||
if (this.selectedPath.length === 0) return;
|
||||
|
||||
const target = this.selectedPath[this.selectedPath.length - 1];
|
||||
const pathCopy = [...this.selectedPath];
|
||||
|
||||
// 1. Trigger Animation Sequence
|
||||
if (this.onEntityMove) this.onEntityMove(this.player, pathCopy);
|
||||
|
||||
// 2. Update Logical Position
|
||||
this.player.setPosition(target.x, target.y);
|
||||
|
||||
// 3. Cleanup
|
||||
this.selectedPath = [];
|
||||
this._notifyPath();
|
||||
|
||||
// Note: Exploration is now manual via door interaction
|
||||
}
|
||||
|
||||
_notifyPath() {
|
||||
if (this.onPathChange) this.onPathChange(this.selectedPath);
|
||||
}
|
||||
|
||||
exploreExit(exitCell) {
|
||||
console.log('[GameEngine] Exploring exit:', exitCell);
|
||||
|
||||
// Find this exit in pendingExits
|
||||
const exit = this.dungeon.pendingExits.find(ex => ex.x === exitCell.x && ex.y === exitCell.y);
|
||||
|
||||
if (exit) {
|
||||
// Prioritize this exit
|
||||
const idx = this.dungeon.pendingExits.indexOf(exit);
|
||||
if (idx > -1) {
|
||||
this.dungeon.pendingExits.splice(idx, 1);
|
||||
this.dungeon.pendingExits.unshift(exit);
|
||||
}
|
||||
|
||||
// Trigger exploration
|
||||
this.turnManager.triggerExploration();
|
||||
this.dungeon.step();
|
||||
this.turnManager.setPhase(GAME_PHASES.HERO);
|
||||
} else {
|
||||
console.warn('[GameEngine] Exit not found in pendingExits');
|
||||
}
|
||||
}
|
||||
}
|
||||
154
src/main.js
154
src/main.js
@@ -1,69 +1,50 @@
|
||||
|
||||
import { GameEngine } from './engine/game/GameEngine.js';
|
||||
import { GameRenderer } from './view/GameRenderer.js';
|
||||
import { CameraManager } from './view/CameraManager.js';
|
||||
import { UIManager } from './view/UIManager.js';
|
||||
import { DoorModal } from './view/DoorModal.js';
|
||||
import { MissionConfig, MISSION_TYPES } from './engine/dungeon/MissionConfig.js';
|
||||
|
||||
console.log("Initializing Warhammer Quest Engine... SYSTEM: GAME_LOOP_ARC_V1");
|
||||
window.TEXTURE_DEBUG = true;
|
||||
console.log("🏗️ Warhammer Quest - Manual Dungeon Construction");
|
||||
|
||||
// 1. Setup Mission
|
||||
const mission = new MissionConfig({
|
||||
id: 'mission_1',
|
||||
name: 'The First Dive',
|
||||
name: 'Manual Construction',
|
||||
type: MISSION_TYPES.ESCAPE,
|
||||
minTiles: 6
|
||||
minTiles: 13
|
||||
});
|
||||
|
||||
// 2. Initialize Core Systems
|
||||
const renderer = new GameRenderer('app'); // Visuals
|
||||
const cameraManager = new CameraManager(renderer); // Camera
|
||||
const game = new GameEngine(); // Logic Brain
|
||||
|
||||
// 3. Initialize UI
|
||||
// UIManager currently reads directly from DungeonGenerator for minimap
|
||||
const renderer = new GameRenderer('app');
|
||||
const cameraManager = new CameraManager(renderer);
|
||||
const game = new GameEngine();
|
||||
const ui = new UIManager(cameraManager, game);
|
||||
const doorModal = new DoorModal();
|
||||
|
||||
// Global Access for Debugging in Browser Console
|
||||
// Global Access
|
||||
window.GAME = game;
|
||||
window.RENDERER = renderer;
|
||||
|
||||
// 4. Bridge Logic & View (Event Hook)
|
||||
// When logic places a tile, we tell the renderer to spawn 3D meshes.
|
||||
// Ideally, this should be an Event in GameEngine, but we keep this patch for now to verify.
|
||||
// 3. Connect Dungeon Generator to Renderer
|
||||
const generator = game.dungeon;
|
||||
const originalPlaceTile = generator.grid.placeTile.bind(generator.grid);
|
||||
|
||||
generator.grid.placeTile = (instance, def) => {
|
||||
// 1. Execute Logic
|
||||
originalPlaceTile(instance, def);
|
||||
generator.grid.placeTile = (instance, variant, card) => {
|
||||
originalPlaceTile(instance, variant);
|
||||
|
||||
// 2. Execute Visuals
|
||||
const cells = generator.grid.getGlobalCells(def, instance.x, instance.y, instance.rotation);
|
||||
renderer.addTile(cells, def.type, def, instance);
|
||||
const cells = generator.grid.calculateCells(variant, instance.x, instance.y);
|
||||
renderer.addTile(cells, card.type, card, instance);
|
||||
|
||||
// 3. Update Exits Visuals
|
||||
setTimeout(() => {
|
||||
renderer.renderExits(generator.pendingExits);
|
||||
}, 50); // Small delay to ensure logic updated pendingExits
|
||||
renderer.renderExits(generator.availableExits);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
// 5. Connect UI Buttons to Game Actions (Temporary)
|
||||
// We will add a temporary button in pure JS here or modify UIManager later.
|
||||
// For now, let's expose a global function for the UI to call if needed,
|
||||
// or simply rely on UIManager updates.
|
||||
|
||||
// 6. Start the Game
|
||||
// 5a. Bridge Game Interactions
|
||||
// 5a. Bridge Game Interactions
|
||||
// 4. Connect Player to Renderer
|
||||
game.onEntityUpdate = (entity) => {
|
||||
renderer.addEntity(entity);
|
||||
renderer.updateEntityPosition(entity);
|
||||
|
||||
// Initial Center on Player Spawn
|
||||
// Center camera on player spawn
|
||||
if (entity.id === 'p1' && !entity._centered) {
|
||||
cameraManager.centerOn(entity.x, entity.y);
|
||||
entity._centered = true;
|
||||
@@ -79,85 +60,84 @@ game.onEntitySelect = (entityId, isSelected) => {
|
||||
};
|
||||
|
||||
renderer.onHeroFinishedMove = (x, y) => {
|
||||
// x, y are World Coordinates (x, -z grid)
|
||||
// Actually, renderer returns Mesh Position.
|
||||
// Mesh X = Grid X. Mesh Z = -Grid Y.
|
||||
// Camera centerOn takes (Grid X, Grid Y).
|
||||
// So we need to convert back?
|
||||
// centerOn implementation: this.target.set(x, 0, -y);
|
||||
// If onHeroFinishedMove passes (mesh.x, -mesh.z), that is (Grid X, Grid Y).
|
||||
|
||||
// Let's verify what we passed in renderer:
|
||||
// this.onHeroFinishedMove(mesh.position.x, -mesh.position.z);
|
||||
// So if mesh is at (5, 1.5, -5), we pass (5, 5).
|
||||
// centerOn(5, 5) -> target(5, 0, -5). Correct.
|
||||
|
||||
cameraManager.centerOn(x, y);
|
||||
};
|
||||
|
||||
game.onPathChange = (path) => {
|
||||
renderer.highlightCells(path);
|
||||
// 5. Connect Generator State to UI
|
||||
generator.onStateChange = (state) => {
|
||||
console.log(`[Main] State: ${state}`);
|
||||
|
||||
if (state === 'PLACING_TILE') {
|
||||
ui.showPlacementControls(true);
|
||||
} else {
|
||||
ui.showPlacementControls(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Custom click handler that checks for doors first
|
||||
const handleCellClick = async (x, y, doorMesh) => {
|
||||
// If doorMesh is provided, user clicked directly on a door texture
|
||||
generator.onPlacementUpdate = (preview) => {
|
||||
if (preview) {
|
||||
renderer.showPlacementPreview(preview);
|
||||
ui.updatePlacementStatus(preview.isValid);
|
||||
} else {
|
||||
renderer.hidePlacementPreview();
|
||||
}
|
||||
};
|
||||
|
||||
// 6. Handle Clicks
|
||||
const handleClick = (x, y, doorMesh) => {
|
||||
// PRIORITY 1: Tile Placement Mode - ignore all clicks
|
||||
if (generator.state === 'PLACING_TILE') {
|
||||
console.log('[Main] Use placement controls to place tile');
|
||||
return;
|
||||
}
|
||||
|
||||
// PRIORITY 2: Door Click (must be adjacent to player)
|
||||
if (doorMesh && doorMesh.userData.isDoor && !doorMesh.userData.isOpen) {
|
||||
// Get player position
|
||||
const player = game.player;
|
||||
if (!player) {
|
||||
console.log('[Main] Player not found');
|
||||
return;
|
||||
}
|
||||
const doorExit = doorMesh.userData.cells[0];
|
||||
|
||||
// Check if player is adjacent to the door
|
||||
if (renderer.isPlayerAdjacentToDoor(player.x, player.y, doorMesh)) {
|
||||
// Show modal
|
||||
const confirmed = await doorModal.show('¿Quieres abrir la puerta?');
|
||||
if (game.isPlayerAdjacentToDoor(doorExit)) {
|
||||
console.log('[Main] 🚪 Opening door and drawing tile...');
|
||||
|
||||
if (confirmed) {
|
||||
// Open the door
|
||||
renderer.openDoor(doorMesh);
|
||||
// Open door visually
|
||||
renderer.openDoor(doorMesh);
|
||||
|
||||
// Trigger exploration of the next tile
|
||||
const exitCell = doorMesh.userData.cells[0];
|
||||
console.log('[Main] Opening door at exit:', exitCell);
|
||||
|
||||
// Call game logic to explore through this exit
|
||||
game.exploreExit(exitCell);
|
||||
// Get proper exit data with direction
|
||||
const exitData = doorMesh.userData.exitData;
|
||||
if (exitData) {
|
||||
generator.selectDoor(exitData);
|
||||
} else {
|
||||
console.error('[Main] Door missing exitData');
|
||||
}
|
||||
} else {
|
||||
console.log('[Main] Player is not adjacent to the door. Move closer first.');
|
||||
console.log('[Main] ⚠️ Move adjacent to the door first');
|
||||
}
|
||||
} else if (x !== null && y !== null) {
|
||||
// Normal cell click (no door involved)
|
||||
return;
|
||||
}
|
||||
|
||||
// PRIORITY 3: Normal cell click (player selection/movement)
|
||||
if (x !== null && y !== null) {
|
||||
game.onCellClick(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
renderer.setupInteraction(
|
||||
() => cameraManager.getCamera(),
|
||||
handleCellClick,
|
||||
(x, y) => game.onCellRightClick(x, y)
|
||||
handleClick,
|
||||
() => { } // No right-click
|
||||
);
|
||||
|
||||
console.log("--- Starting Game Session ---");
|
||||
// 7. Start
|
||||
console.log("--- Starting Game ---");
|
||||
game.startMission(mission);
|
||||
|
||||
// 7. Render Loop
|
||||
// 8. Render Loop
|
||||
const animate = (time) => {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
// Update Game Logic (State Machine, Timers, etc)
|
||||
game.update(time);
|
||||
|
||||
// Update Camera Animations
|
||||
cameraManager.update(time);
|
||||
|
||||
// Update Visual Animations
|
||||
renderer.updateAnimations(time);
|
||||
|
||||
// Render Frame
|
||||
renderer.render(cameraManager.getCamera());
|
||||
};
|
||||
animate(0);
|
||||
|
||||
console.log("✅ Ready - Move barbarian next to a door and click it");
|
||||
|
||||
172
src/old/main_20260102.js
Normal file
172
src/old/main_20260102.js
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
import { GameEngine } from './engine/game/GameEngine.js';
|
||||
import { GameRenderer } from './view/GameRenderer.js';
|
||||
import { CameraManager } from './view/CameraManager.js';
|
||||
import { UIManager } from './view/UIManager.js';
|
||||
import { DoorModal } from './view/DoorModal.js';
|
||||
import { MissionConfig, MISSION_TYPES } from './engine/dungeon/MissionConfig.js';
|
||||
|
||||
console.log("Initializing Warhammer Quest Engine... SYSTEM: MANUAL_PLACEMENT_V2");
|
||||
window.TEXTURE_DEBUG = true;
|
||||
|
||||
// 1. Setup Mission
|
||||
const mission = new MissionConfig({
|
||||
id: 'mission_1',
|
||||
name: 'The First Dive',
|
||||
type: MISSION_TYPES.ESCAPE,
|
||||
minTiles: 6
|
||||
});
|
||||
|
||||
// 2. Initialize Core Systems
|
||||
const renderer = new GameRenderer('app');
|
||||
const cameraManager = new CameraManager(renderer);
|
||||
const game = new GameEngine();
|
||||
|
||||
// 3. Initialize UI
|
||||
const ui = new UIManager(cameraManager, game);
|
||||
const doorModal = new DoorModal();
|
||||
|
||||
// Global Access for Debugging
|
||||
window.GAME = game;
|
||||
window.RENDERER = renderer;
|
||||
|
||||
// 4. Bridge Logic & View
|
||||
const generator = game.dungeon;
|
||||
const originalPlaceTile = generator.grid.placeTile.bind(generator.grid);
|
||||
|
||||
// Override placeTile to trigger rendering
|
||||
generator.grid.placeTile = (instance, variant, def) => {
|
||||
originalPlaceTile(instance, variant);
|
||||
|
||||
const cells = generator.grid.calculateCells(variant, instance.x, instance.y);
|
||||
renderer.addTile(cells, def.type, def, instance);
|
||||
|
||||
// Update exits visualization
|
||||
setTimeout(() => {
|
||||
renderer.renderExits(generator.availableExits);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
// 5. Connect Generator State Changes to UI
|
||||
generator.onStateChange = (state) => {
|
||||
console.log(`[Main] Generator state: ${state}`);
|
||||
|
||||
if (state === 'PLACING_TILE') {
|
||||
ui.showPlacementControls(true);
|
||||
} else {
|
||||
ui.showPlacementControls(false);
|
||||
}
|
||||
|
||||
if (state === 'WAITING_DOOR') {
|
||||
// Enable door selection mode
|
||||
renderer.enableDoorSelection(true);
|
||||
} else {
|
||||
renderer.enableDoorSelection(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 6. Connect Placement Updates to Renderer
|
||||
generator.onPlacementUpdate = (preview) => {
|
||||
if (preview) {
|
||||
renderer.showPlacementPreview(preview);
|
||||
ui.updatePlacementStatus(preview.isValid);
|
||||
} else {
|
||||
renderer.hidePlacementPreview();
|
||||
}
|
||||
};
|
||||
|
||||
// 7. Handle Door Selection
|
||||
renderer.onDoorSelected = (exitPoint) => {
|
||||
console.log('[Main] Door selected:', exitPoint);
|
||||
generator.selectDoor(exitPoint);
|
||||
};
|
||||
|
||||
// 8. Bridge Game Interactions (Player movement, etc.)
|
||||
game.onEntityUpdate = (entity) => {
|
||||
renderer.addEntity(entity);
|
||||
renderer.updateEntityPosition(entity);
|
||||
|
||||
if (entity.id === 'p1' && !entity._centered) {
|
||||
cameraManager.centerOn(entity.x, entity.y);
|
||||
entity._centered = true;
|
||||
}
|
||||
};
|
||||
|
||||
game.onEntityMove = (entity, path) => {
|
||||
renderer.moveEntityAlongPath(entity, path);
|
||||
};
|
||||
|
||||
game.onEntitySelect = (entityId, isSelected) => {
|
||||
renderer.toggleEntitySelection(entityId, isSelected);
|
||||
};
|
||||
|
||||
renderer.onHeroFinishedMove = (x, y) => {
|
||||
cameraManager.centerOn(x, y);
|
||||
};
|
||||
|
||||
game.onPathChange = (path) => {
|
||||
renderer.highlightCells(path);
|
||||
};
|
||||
|
||||
// 9. Custom click handler
|
||||
const handleCellClick = async (x, y, doorMesh) => {
|
||||
// PRIORITY 1: Check if we're in door selection mode (dungeon building)
|
||||
if (generator.state === 'WAITING_DOOR') {
|
||||
if (doorMesh && doorMesh.userData.isExit) {
|
||||
// This is an exit door that can be selected for expansion
|
||||
const exitData = doorMesh.userData.exitData;
|
||||
if (exitData) {
|
||||
console.log('[Main] Door selected for expansion:', exitData);
|
||||
generator.selectDoor(exitData);
|
||||
}
|
||||
return; // Don't process any other click logic
|
||||
}
|
||||
// If not clicking on a door in WAITING_DOOR mode, ignore the click
|
||||
console.log('[Main] Click ignored - waiting for door selection');
|
||||
return;
|
||||
}
|
||||
|
||||
// PRIORITY 2: Normal door interaction (during gameplay with player)
|
||||
if (doorMesh && doorMesh.userData.isDoor && !doorMesh.userData.isOpen) {
|
||||
const player = game.player;
|
||||
if (!player) {
|
||||
console.log('[Main] Player not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderer.isPlayerAdjacentToDoor(player.x, player.y, doorMesh)) {
|
||||
const confirmed = await doorModal.show('¿Quieres abrir la puerta?');
|
||||
|
||||
if (confirmed) {
|
||||
renderer.openDoor(doorMesh);
|
||||
const exitCell = doorMesh.userData.cells[0];
|
||||
console.log('[Main] Opening door at exit:', exitCell);
|
||||
game.exploreExit(exitCell);
|
||||
}
|
||||
} else {
|
||||
console.log('[Main] Player is not adjacent to the door. Move closer first.');
|
||||
}
|
||||
} else if (x !== null && y !== null) {
|
||||
game.onCellClick(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
renderer.setupInteraction(
|
||||
() => cameraManager.getCamera(),
|
||||
handleCellClick,
|
||||
(x, y) => game.onCellRightClick(x, y)
|
||||
);
|
||||
|
||||
console.log("--- Starting Game Session ---");
|
||||
game.startMission(mission);
|
||||
|
||||
// 10. Render Loop
|
||||
const animate = (time) => {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
game.update(time);
|
||||
cameraManager.update(time);
|
||||
renderer.updateAnimations(time);
|
||||
renderer.render(cameraManager.getCamera());
|
||||
};
|
||||
animate(0);
|
||||
@@ -412,11 +412,18 @@ export class GameRenderer {
|
||||
mesh.rotation.y = angle;
|
||||
|
||||
// Store door data for interaction (new doors always start closed)
|
||||
// Convert numeric direction to string for generator compatibility
|
||||
const dirMap = { 0: 'N', 1: 'E', 2: 'S', 3: 'W' };
|
||||
mesh.userData = {
|
||||
isDoor: true,
|
||||
isOpen: false,
|
||||
cells: [d1, d2],
|
||||
direction: dir
|
||||
direction: dir,
|
||||
exitData: {
|
||||
x: d1.x,
|
||||
y: d1.y,
|
||||
direction: dirMap[dir] || 'N'
|
||||
}
|
||||
};
|
||||
mesh.name = `door_${idx}`;
|
||||
|
||||
@@ -480,14 +487,38 @@ export class GameRenderer {
|
||||
|
||||
// Load texture with callback
|
||||
this.getTexture(texturePath, (texture) => {
|
||||
const w = tileDef.width;
|
||||
const l = tileDef.length;
|
||||
|
||||
// Create Plane
|
||||
const geometry = new THREE.PlaneGeometry(w, l);
|
||||
// --- NEW LOGIC: Calculate center based on DIMENSIONS, not CELLS ---
|
||||
|
||||
// 1. Get the specific variant for this rotation to know the VISUAL bounds
|
||||
// (The shape the grid sees: e.g. 4x2 for East)
|
||||
const currentVariant = tileDef.variants[tileInstance.rotation];
|
||||
|
||||
if (!currentVariant) {
|
||||
console.error(`[GameRenderer] Missing variant for rotation ${tileInstance.rotation}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const rotWidth = currentVariant.width;
|
||||
const rotHeight = currentVariant.height;
|
||||
|
||||
// 2. Calculate the Geometric Center of the tile relative to the anchor
|
||||
// Formula: anchor + (dimension - 1) / 2
|
||||
// (Subtract 1 because width 1 is just offset 0)
|
||||
const cx = tileInstance.x + (rotWidth - 1) / 2;
|
||||
const cy = tileInstance.y + (rotHeight - 1) / 2;
|
||||
|
||||
console.log(`[GameRenderer] Dimensions (Rotated): ${rotWidth}x${rotHeight}`);
|
||||
console.log(`[GameRenderer] Calculated Center: (${cx}, ${cy})`);
|
||||
|
||||
// 3. Use BASE dimensions from NORTH variant for the Plane
|
||||
// (Since we are rotating the plane itself, we start with the un-rotated image size)
|
||||
const baseWidth = tileDef.variants.N.width;
|
||||
const baseHeight = tileDef.variants.N.height;
|
||||
|
||||
// Create Plane with BASE dimensions
|
||||
const geometry = new THREE.PlaneGeometry(baseWidth, baseHeight);
|
||||
|
||||
// SWITCH TO BASIC MATERIAL FOR DEBUGGING TEXTURE VISIBILITY
|
||||
// Standard material heavily depends on lights. If light is not hitting correctly, it looks black.
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
@@ -496,42 +527,32 @@ export class GameRenderer {
|
||||
});
|
||||
const plane = new THREE.Mesh(geometry, material);
|
||||
|
||||
|
||||
|
||||
// Initial Rotation: Plane X-Y to X-Z
|
||||
// Initial Rotation: Plane X-Y to X-Z (Flat on ground)
|
||||
plane.rotation.x = -Math.PI / 2;
|
||||
|
||||
// Handle Rotation safely (Support both 0-3 and N-W)
|
||||
const rotMap = { 'N': 0, '0': 0, 0: 0, 'E': 1, '1': 1, 1: 1, 'S': 2, '2': 2, 2: 2, 'W': 3, '3': 3, 3: 3 };
|
||||
// Handle Rotation
|
||||
const rotMap = { 'N': 0, 'E': 1, 'S': 2, 'W': 3 };
|
||||
const r = rotMap[tileInstance.rotation] !== undefined ? rotMap[tileInstance.rotation] : 0;
|
||||
|
||||
// Apply Tile Rotation
|
||||
// Apply Tile Rotation (Z-axis is Up in this local frame before X-rotation? No, after X-rot)
|
||||
// Actually, standard hierarchy: Rotate Z first?
|
||||
// ThreeJS rotation order XYZ.
|
||||
// We want to rotate around the Y axis of the world (which is Z of the plane before x-rotation?)
|
||||
// Simplest: Rotate Z of the plane, which corresponds to world Y.
|
||||
// Note: We use negative rotation because ThreeJS is CCW, but our grid might be different,
|
||||
// but usually -r * PI/2 works for this setup.
|
||||
plane.rotation.z = -r * (Math.PI / 2);
|
||||
|
||||
// Calculate Center Offset for Positioning
|
||||
const midX = (tileDef.width - 1) / 2;
|
||||
const midY = (tileDef.length - 1) / 2;
|
||||
|
||||
// Rotate the offset vector based on tile rotation
|
||||
let dx, dy;
|
||||
|
||||
if (r === 0) { dx = midX; dy = midY; }
|
||||
else if (r === 1) { dx = midY; dy = -midX; }
|
||||
else if (r === 2) { dx = -midX; dy = -midY; }
|
||||
else if (r === 3) { dx = -midY; dy = midX; }
|
||||
|
||||
const centerX = tileInstance.x + dx;
|
||||
const centerY = tileInstance.y + dy;
|
||||
|
||||
// Set at almost 0 height to avoid z-fighting with grid helper, but effectively on floor
|
||||
plane.position.set(centerX, 0.01, -centerY);
|
||||
// Position at the calculated center
|
||||
// Notice: World Z is -Grid Y
|
||||
plane.position.set(cx, 0.01, -cy);
|
||||
plane.receiveShadow = true;
|
||||
|
||||
this.scene.add(plane);
|
||||
console.log(`[GameRenderer] ✓ Tile plane added at (${centerX}, 0.01, ${-centerY}) for ${tileDef.id}`);
|
||||
console.log(`[GameRenderer] ✓ Tile plane added at (${cx}, 0.01, ${-cy})`);
|
||||
});
|
||||
} else {
|
||||
console.warn(`[GameRenderer] details missing for texture render. def: ${!!tileDef}, inst: ${!!tileInstance}, tex: ${tileDef?.textures?.length}`);
|
||||
console.warn(`[GameRenderer] details missing for texture render. def: ${!!tileDef}, inst: ${!!tileInstance}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,4 +602,156 @@ export class GameRenderer {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ========== MANUAL PLACEMENT SYSTEM ==========
|
||||
|
||||
enableDoorSelection(enabled) {
|
||||
this.doorSelectionEnabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
// Highlight available exits
|
||||
this.highlightAvailableExits();
|
||||
} else {
|
||||
// Remove highlights
|
||||
if (this.exitHighlightGroup) {
|
||||
this.exitHighlightGroup.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
highlightAvailableExits() {
|
||||
if (!this.exitHighlightGroup) {
|
||||
this.exitHighlightGroup = new THREE.Group();
|
||||
this.scene.add(this.exitHighlightGroup);
|
||||
}
|
||||
|
||||
this.exitHighlightGroup.clear();
|
||||
|
||||
// Highlight each exit door with a pulsing glow
|
||||
if (this.exitGroup) {
|
||||
this.exitGroup.children.forEach(doorMesh => {
|
||||
if (doorMesh.userData.isDoor && !doorMesh.userData.isOpen) {
|
||||
// Create highlight ring
|
||||
const ringGeom = new THREE.RingGeometry(1.2, 1.4, 32);
|
||||
const ringMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x00ff00,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.6
|
||||
});
|
||||
const ring = new THREE.Mesh(ringGeom, ringMat);
|
||||
ring.rotation.x = -Math.PI / 2;
|
||||
ring.position.copy(doorMesh.position);
|
||||
ring.position.y = 0.05;
|
||||
|
||||
|
||||
// Store reference to door for click handling
|
||||
doorMesh.userData.isExit = true;
|
||||
// Create proper exit data with all required fields
|
||||
const firstCell = doorMesh.userData.cells[0];
|
||||
// Convert numeric direction (0,1,2,3) to string ('N','E','S','W')
|
||||
const dirMap = { 0: 'N', 1: 'E', 2: 'S', 3: 'W' };
|
||||
doorMesh.userData.exitData = {
|
||||
x: firstCell.x,
|
||||
y: firstCell.y,
|
||||
direction: dirMap[doorMesh.userData.direction] || 'N'
|
||||
};
|
||||
|
||||
this.exitHighlightGroup.add(ring);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showPlacementPreview(preview) {
|
||||
if (!preview) {
|
||||
this.hidePlacementPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create preview groups if they don't exist
|
||||
if (!this.previewGroup) {
|
||||
this.previewGroup = new THREE.Group();
|
||||
this.scene.add(this.previewGroup);
|
||||
}
|
||||
|
||||
if (!this.projectionGroup) {
|
||||
this.projectionGroup = new THREE.Group();
|
||||
this.scene.add(this.projectionGroup);
|
||||
}
|
||||
|
||||
// Clear previous preview
|
||||
this.previewGroup.clear();
|
||||
this.projectionGroup.clear();
|
||||
|
||||
const { card, cells, isValid, x, y, rotation } = preview;
|
||||
|
||||
// Calculate bounds for tile - OLD LOGIC (Removed)
|
||||
// Note: We ignore 'cells' for positioning the texture, but keep them for the Ground Projection (Green/Red squares)
|
||||
|
||||
// 1. FLOATING TILE (Y = 3)
|
||||
if (card.textures && card.textures.length > 0) {
|
||||
this.getTexture(card.textures[0], (texture) => {
|
||||
|
||||
// Get Current Rotation Variant for Dimensions
|
||||
const currentVariant = card.variants[rotation];
|
||||
const rotWidth = currentVariant.width;
|
||||
const rotHeight = currentVariant.height;
|
||||
|
||||
// Calculate Center based on Anchor (x, y) and Dimensions
|
||||
const cx = x + (rotWidth - 1) / 2;
|
||||
const cy = y + (rotHeight - 1) / 2;
|
||||
|
||||
// Use BASE dimensions from NORTH variant
|
||||
const baseWidth = card.variants.N.width;
|
||||
const baseHeight = card.variants.N.height;
|
||||
|
||||
const geometry = new THREE.PlaneGeometry(baseWidth, baseHeight);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity: 0.8,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
const floatingTile = new THREE.Mesh(geometry, material);
|
||||
|
||||
floatingTile.rotation.x = -Math.PI / 2;
|
||||
|
||||
// Apply Z rotation
|
||||
const rotMap = { 'N': 0, 'E': 1, 'S': 2, 'W': 3 };
|
||||
const r = rotMap[rotation] !== undefined ? rotMap[rotation] : 0;
|
||||
floatingTile.rotation.z = -r * (Math.PI / 2);
|
||||
|
||||
console.log(`[Preview] Rotation: ${rotation}, Center: (${cx}, ${cy})`);
|
||||
|
||||
floatingTile.position.set(cx, 3, -cy);
|
||||
this.previewGroup.add(floatingTile);
|
||||
});
|
||||
}
|
||||
|
||||
// 2. GROUND PROJECTION (Green/Red)
|
||||
const projectionColor = isValid ? 0x00ff00 : 0xff0000;
|
||||
cells.forEach(cell => {
|
||||
const geometry = new THREE.PlaneGeometry(0.95, 0.95);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: projectionColor,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
const projection = new THREE.Mesh(geometry, material);
|
||||
projection.rotation.x = -Math.PI / 2;
|
||||
projection.position.set(cell.x, 0.02, -cell.y);
|
||||
this.projectionGroup.add(projection);
|
||||
});
|
||||
}
|
||||
|
||||
hidePlacementPreview() {
|
||||
if (this.previewGroup) {
|
||||
this.previewGroup.clear();
|
||||
}
|
||||
if (this.projectionGroup) {
|
||||
this.projectionGroup.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -134,6 +134,137 @@ export class UIManager {
|
||||
|
||||
// Set initial active button (North)
|
||||
this.updateActiveViewButton(DIRECTIONS.NORTH);
|
||||
|
||||
// --- Tile Placement Controls (Bottom Center) ---
|
||||
this.placementPanel = document.createElement('div');
|
||||
this.placementPanel.style.position = 'absolute';
|
||||
this.placementPanel.style.bottom = '20px';
|
||||
this.placementPanel.style.left = '50%';
|
||||
this.placementPanel.style.transform = 'translateX(-50%)';
|
||||
this.placementPanel.style.display = 'none'; // Hidden by default
|
||||
this.placementPanel.style.pointerEvents = 'auto';
|
||||
this.placementPanel.style.backgroundColor = 'rgba(0, 0, 0, 0.85)';
|
||||
this.placementPanel.style.padding = '15px';
|
||||
this.placementPanel.style.borderRadius = '8px';
|
||||
this.placementPanel.style.border = '2px solid #666';
|
||||
this.container.appendChild(this.placementPanel);
|
||||
|
||||
// Status text
|
||||
this.placementStatus = document.createElement('div');
|
||||
this.placementStatus.style.color = '#fff';
|
||||
this.placementStatus.style.fontSize = '16px';
|
||||
this.placementStatus.style.fontFamily = 'sans-serif';
|
||||
this.placementStatus.style.marginBottom = '10px';
|
||||
this.placementStatus.style.textAlign = 'center';
|
||||
this.placementStatus.textContent = 'Coloca la loseta';
|
||||
this.placementPanel.appendChild(this.placementStatus);
|
||||
|
||||
// Controls container
|
||||
const placementControls = document.createElement('div');
|
||||
placementControls.style.display = 'flex';
|
||||
placementControls.style.gap = '15px';
|
||||
placementControls.style.alignItems = 'center';
|
||||
this.placementPanel.appendChild(placementControls);
|
||||
|
||||
// Movement arrows (4-way grid)
|
||||
const arrowGrid = document.createElement('div');
|
||||
arrowGrid.style.display = 'grid';
|
||||
arrowGrid.style.gridTemplateColumns = '40px 40px 40px';
|
||||
arrowGrid.style.gap = '3px';
|
||||
|
||||
const createArrow = (label, dx, dy) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = label;
|
||||
btn.style.width = '40px';
|
||||
btn.style.height = '40px';
|
||||
btn.style.backgroundColor = '#444';
|
||||
btn.style.color = '#fff';
|
||||
btn.style.border = '1px solid #888';
|
||||
btn.style.cursor = 'pointer';
|
||||
btn.style.fontSize = '18px';
|
||||
btn.onclick = () => {
|
||||
if (this.dungeon) {
|
||||
this.dungeon.movePlacement(dx, dy);
|
||||
}
|
||||
};
|
||||
return btn;
|
||||
};
|
||||
|
||||
const arrowUp = createArrow('↑', 0, 1);
|
||||
const arrowLeft = createArrow('←', -1, 0);
|
||||
const arrowRight = createArrow('→', 1, 0);
|
||||
const arrowDown = createArrow('↓', 0, -1);
|
||||
|
||||
arrowUp.style.gridColumn = '2';
|
||||
arrowLeft.style.gridColumn = '1';
|
||||
arrowRight.style.gridColumn = '3';
|
||||
arrowDown.style.gridColumn = '2';
|
||||
|
||||
arrowGrid.appendChild(arrowUp);
|
||||
arrowGrid.appendChild(arrowLeft);
|
||||
arrowGrid.appendChild(arrowRight);
|
||||
arrowGrid.appendChild(arrowDown);
|
||||
|
||||
placementControls.appendChild(arrowGrid);
|
||||
|
||||
// Rotate button
|
||||
this.rotateBtn = document.createElement('button');
|
||||
this.rotateBtn.textContent = '🔄 Rotar';
|
||||
this.rotateBtn.style.padding = '10px 20px';
|
||||
this.rotateBtn.style.backgroundColor = '#555';
|
||||
this.rotateBtn.style.color = '#fff';
|
||||
this.rotateBtn.style.border = '1px solid #888';
|
||||
this.rotateBtn.style.cursor = 'pointer';
|
||||
this.rotateBtn.style.fontSize = '16px';
|
||||
this.rotateBtn.style.borderRadius = '4px';
|
||||
this.rotateBtn.onclick = () => {
|
||||
if (this.dungeon) {
|
||||
this.dungeon.rotatePlacement();
|
||||
}
|
||||
};
|
||||
placementControls.appendChild(this.rotateBtn);
|
||||
|
||||
// Place button
|
||||
this.placeBtn = document.createElement('button');
|
||||
this.placeBtn.textContent = '⬇ Bajar';
|
||||
this.placeBtn.style.padding = '10px 20px';
|
||||
this.placeBtn.style.backgroundColor = '#2a5';
|
||||
this.placeBtn.style.color = '#fff';
|
||||
this.placeBtn.style.border = '1px solid #888';
|
||||
this.placeBtn.style.cursor = 'pointer';
|
||||
this.placeBtn.style.fontSize = '16px';
|
||||
this.placeBtn.style.borderRadius = '4px';
|
||||
this.placeBtn.onclick = () => {
|
||||
if (this.dungeon) {
|
||||
const success = this.dungeon.confirmPlacement();
|
||||
if (!success) {
|
||||
alert('❌ No se puede colocar la loseta en esta posición');
|
||||
}
|
||||
}
|
||||
};
|
||||
placementControls.appendChild(this.placeBtn);
|
||||
}
|
||||
|
||||
showPlacementControls(show) {
|
||||
if (this.placementPanel) {
|
||||
this.placementPanel.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
updatePlacementStatus(isValid) {
|
||||
if (this.placementStatus) {
|
||||
if (isValid) {
|
||||
this.placementStatus.textContent = '✅ Posición válida';
|
||||
this.placementStatus.style.color = '#0f0';
|
||||
this.placeBtn.style.backgroundColor = '#2a5';
|
||||
this.placeBtn.style.cursor = 'pointer';
|
||||
} else {
|
||||
this.placementStatus.textContent = '❌ Posición inválida';
|
||||
this.placementStatus.style.color = '#f44';
|
||||
this.placeBtn.style.backgroundColor = '#555';
|
||||
this.placeBtn.style.cursor = 'not-allowed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateActiveViewButton(activeDirection) {
|
||||
@@ -201,13 +332,15 @@ export class UIManager {
|
||||
ctx.fillRect(cx, cy, cellSize, cellSize);
|
||||
}
|
||||
|
||||
// Draw Exits (Pending)
|
||||
// Draw Exits (Available)
|
||||
ctx.fillStyle = '#0f0'; // Green dots for open exits
|
||||
this.dungeon.pendingExits.forEach(exit => {
|
||||
const ex = centerX + (exit.x * cellSize);
|
||||
const ey = centerY - (exit.y * cellSize);
|
||||
ctx.fillRect(ex, ey, cellSize, cellSize);
|
||||
});
|
||||
if (this.dungeon.availableExits) {
|
||||
this.dungeon.availableExits.forEach(exit => {
|
||||
const ex = centerX + (exit.x * cellSize);
|
||||
const ey = centerY - (exit.y * cellSize);
|
||||
ctx.fillRect(ex, ey, cellSize, cellSize);
|
||||
});
|
||||
}
|
||||
|
||||
// Draw Entry (0,0) cross
|
||||
ctx.strokeStyle = '#f00';
|
||||
|
||||
Reference in New Issue
Block a user