From fd1708688a83cad2648eb3056431e2443197885f Mon Sep 17 00:00:00 2001 From: marti Date: Wed, 31 Dec 2025 00:21:07 +0100 Subject: [PATCH] Phase 1 Complete: Dungeon Engine & Visuals. Switched to Manual Exploration Plan. --- DEVLOG.md | 13 +- implementación/implementation_plan.md | 44 ++-- implementación/task.md | 13 +- implementación/walkthrough.md | 23 +- src/engine/dungeon/DungeonGenerator.js | 305 ++++++++++++++----------- src/engine/dungeon/TileDefinitions.js | 85 +++---- src/main.js | 15 +- src/view/GameRenderer.js | 16 +- 8 files changed, 287 insertions(+), 227 deletions(-) diff --git a/DEVLOG.md b/DEVLOG.md index 8ea56b4..8dbfc3f 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -42,11 +42,14 @@ En esta sesión se ha establecido la base completa del motor de juego para **War ### Estado Actual ### Estado Actual ### Estado Actual -* El generador crea mazmorras lógicas válidas siguiendo las reglas. -* El visualizador pinta la estructura en 3D. -* **Texturas operativas**: Se ha corregido un bug crítico en la rotación (NaN) que impedía la visualización. Ahora las texturas se cargan y posicionan, aunque persisten problemas de alineación visual en las juntas. +* El generador crea mazmorras y las visualiza en 3D con texturas. +* **Problemas de Alineación**: Persisten desajustes en las conexiones de mazmorra (efecto zig-zag en puertas dobles) en la generación automática. +* **Decisión de Diseño**: Se detiene el refinamiento de la generación automática aleatoria. El enfoque cambia a implementar la **Exploración Guiada por el Jugador**, donde la mazmorra se genera pieza a pieza según la decisión del usuario, lo que simplificará la lógica de conexión y evitará casos límite generados por el azar puro. -### Próximos Pasos -* Corregir la alineación fina de las baldosas (especialmente T y L) para eliminar huecos visuales. +### Próximos Pasos (Siguiente Sesión) +* Implementar al Jugador (Héroe) y su movimiento. +* Desactivar la generación automática (`generator.step()` automático). +* Crear UI para que el jugador elija "Explorar" en una salida específica. +* Generar solo la siguiente pieza conectada a la salida elegida. * Implementar la interfaz de usuario (UI) para mostrar cartas y estado del juego. * Añadir modelos 3D para héroes y monstruos. diff --git a/implementación/implementation_plan.md b/implementación/implementation_plan.md index e221273..21176d2 100644 --- a/implementación/implementation_plan.md +++ b/implementación/implementation_plan.md @@ -17,48 +17,30 @@ The engine will consist of three main components: ## Proposed Changes -### [NEW] `src/engine/dungeon/` -We will structure the engine purely in JS logic first. +### [COMPLETED] `src/engine/dungeon/` +We structured the engine effectively. -#### [NEW] `TileDefinitions.js` -- **Data Structure**: - ```javascript - { - id: 'corridor_straight', - type: 'corridor', // 'room', 'objective' - width: 2, - length: 5, - exits: [ {x:0, y:0, dir:'N'}, ... ] // Local coords - } - ``` +#### [DONE] `TileDefinitions.js` +- **Data Structure**: Updated to use Object Map & Single Anchor Points for alignment correction. -#### [NEW] `DungeonDeck.js` +#### [DONE] `DungeonDeck.js` - Handles the stack of cards. -- Methods: `draw()`, `shuffle()`, `insert(card, position)`. -- **Campaign Injection**: Ability to inject specific "Events" or "Rooms" at certain deck depths (e.g., "After 10 cards, shuffle the Exit card into the top 3"). +- Methods: `draw()`, `shuffle()`, `insert()`. -#### [NEW] `Generator.js` -- **Grid System**: A virtual 2D array or Map `Map<"x,y", TileID>` to track occupancy. +#### [DONE] `DungeonGenerator.js` +- **Grid System**: `GridSystem.js` handles collision & spatial logic. - **Algorithm**: 1. Place Entry Room at (0,0). - 2. Add Entry Exits to `OpenExitsList`. - 3. **Step**: - - Pick an Exit from `OpenExitsList`. - - Draw Card from `DungeonDeck`. - - Attempt to place Tile at Exit. - - **IF Collision**: Discard and try alternative (or end path). - - **IF Success**: Register Tile, Remove used Exit, Add new Exits. + 2. Step-by-Step generation implemented (currently automatic 1s delay). + 3. **Refinement**: Automatic generation shows alignment issues due to random nature. **Plan Change**: Moving to Manual Player Exploration next. ### Campaign Integration - **Mission Config Payload**: ```javascript { - missionId: "campaign_1_mission_1", - deckComposition: [ ... ], - specialRules: { - forceExitAfter: 10, // Logic: Treat specific room as 'Objective' for generation purposes - exitType: "ladder_room" - } + missionId: "mission_1", + objectiveId: "room_dungeon", // Simplified for now + specialRules: {} } ``` diff --git a/implementación/task.md b/implementación/task.md index 19e8389..c7b40f1 100644 --- a/implementación/task.md +++ b/implementación/task.md @@ -5,25 +5,24 @@ - [x] Define Tile Data (Dimensions, Exits, Type) - [x] Define Dungeon Deck System (Cards, Shuffling, Probability) - [x] Define Mission Configuration Structure (Objective vs Exit) - - [ ] Define Mission Configuration Structure (Objective vs Exit) - [x] **Grid & Logic System** - [x] Implement Tile Placement Logic (Collision Detection, Alignment) - [x] Implement Connection Points (Exits/Entrances matching) - [x] Implement "Board" State (Tracking placed tiles) -- [ ] **Generation Algorithms** +- [x] **Generation Algorithms** - [x] Basic "Next Tile" Generation Rule - [x] Implement "Exit Room" Logic for Non-Final Missions - [x] Implement "Objective Room" Logic for Final Missions - - [x] Create Loop for Full Dungeon Generation + - [x] Create Loop for Full Dungeon Generation (Stopped for manual exploration) ## Phase 2: 3D Visualization & Camera -- [ ] **Scene Setup** +- [x] **Scene Setup** - [x] Setup Three.js Scene, Light, and Renderer - [x] Implement Isometric Camera (Orthographic) - [x] Implement Fixed Orbit Controls (N, S, E, W snapshots) -- [ ] **Asset Management** - - [ ] Tile Model/Texture Loading - - [ ] dynamic Tile Instancing based on Grid State +- [x] **Asset Management** + - [x] Tile Model/Texture Loading + - [x] dynamic Tile Instancing based on Grid State ## Phase 3: Game Mechanics (Loop) - [ ] **Turn System** diff --git a/implementación/walkthrough.md b/implementación/walkthrough.md index 170a933..4a8e2ae 100644 --- a/implementación/walkthrough.md +++ b/implementación/walkthrough.md @@ -1,3 +1,24 @@ # Walkthrough +## Current Status: Phase 1 (Dungeon Engine & Visualization) Completed -*Project reset. No features implemented yet.* +### Features Implemented +1. **Dungeon Logic Engine**: + * `GridSystem.js`: Manages spatial occupancy and global coordinate transformations. + * `DungeonDeck.js`: Manages card drawing probabilities and objective injection. + * `DungeonGenerator.js`: Connects tiles step-by-step. Uses a "Reference Cell" logic to align multi-cell doors (2-wide) correctly. + * `TileDefinitions.js`: Defines dimensions, layout, and specialized exit points for all Warhammer Quest base tiles (Corridors, Rooms, Objectives). + +2. **3D Visualization**: + * `GameRenderer.js`: Three.js implementation. + * **Isometric Camera**: Fixed orthographic view with controls (WASD/Arrows + Rotation Snap). + * **Texture Mapping**: Tiles render with correct `.png` textures on flat planes. + * **Debugging Aids**: Green wireframes around tiles to visualize boundary collisions and alignment. + +3. **Current Workflow**: + * The app launches and immediately starts generating a dungeon. + * A new tile is added every 1.0 seconds (Visual Debug Mode). + * Logs in the console show the decision process (Card drawn, Exit selected, Placement coordinates). + +### Known Issues & Next Steps +* **Alignment**: Automatic random placement sometimes creates awkward visual gaps due to the complexity of aligning multi-cell exits in 4 directions. +* **Next Phase**: Transitioning to **Player-Controlled Exploration**. instead of random growth. The player will click an exit to "Reveal" the next section, ensuring logical continuity. diff --git a/src/engine/dungeon/DungeonGenerator.js b/src/engine/dungeon/DungeonGenerator.js index 4ae2052..47faeca 100644 --- a/src/engine/dungeon/DungeonGenerator.js +++ b/src/engine/dungeon/DungeonGenerator.js @@ -54,84 +54,200 @@ export class DungeonGenerator { return false; } - // Rulebook: Draw next card const card = this.deck.draw(); - if (!card) { console.log("Deck empty. Dungeon complete."); this.isComplete = true; return false; } - // Try to fit the card on any pending exit - // We prioritize the "current" open exit? Rulebook implies expanding from the explored edge. - // For a generator, we treat it as a queue (BFS) or stack (DFS). Queue is better for "bushy" dungeons. - - // Let's try to fit the card onto the FIRST valid exit in our queue - let placed = false; - - // Iterate through copy of pending exits to avoid modification issues during loop - // (Though we usually just pick ONE exit to explore per turn in the board game) - // In the board game, you pick an exit and "Explore" it. - // Let's pick the first available exit. + // 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(); - console.log(`Attempting to place ${card.name} at exit ${targetExit.x},${targetExit.y} (${targetExit.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). - // We need to rotate the new card so ONE of its exits connects to 'targetExit' - // Connection rule: New Tile Exit be Opposed to Target Exit. - // Target: NORTH -> New Tile must present a SOUTH exit to connect. - const requiredInputDirection = this.getOppositeDirection(targetExit.direction); + // 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. - // Find which exit on the CANDIDATE card can serve as the input - // (A tile might have multiple potential inputs, e.g. a 4-way corridor) - for (const candidateExit of card.exits) { - // calculatedRotation: What rotation does the TILE need so that 'candidateExit' points 'requiredInputDirection'? - // candidateExit.direction (Local) + TileRotation = requiredInputDirection + // 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 where the tile top-left (x,y) must be so that the exits match positions. - const position = this.calculateTilePosition(targetExit, candidateExit, rotation); + // 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). - if (this.grid.canPlace(card, position.x, position.y, rotation)) { + // 1. Calculate the offset of Candidate 'Min Cell' relative to Tile Origin (0,0) AFTER rotation. + const rotatedOffset = this.getRotatedOffset(candidateExit, rotation); - // Success! Place it. + // 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: position.x, - y: position.y, + x: posX, + y: posY, rotation: rotation }; this.grid.placeTile(newInstance, card); this.placedTiles.push(newInstance); - // Add NEW exits, but... - // CRITICAL: The exit we just used to enter is NOT an exit anymore. It's the connection. - this.addExitsToQueue(newInstance, card, targetExit); // Pass the source to exclude it + // 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; // Stop looking for fits for this card + break; } } if (!placed) { - console.log(`Could not fit ${card.name} at selected exit. Discarding.`); - // In real game: Discard card. - // Put the exit back? Rulebook says "If room doesn't fit, nothing is placed". - // Does the exit remain open? Yes, usually. - this.pendingExits.push(targetExit); // Return exit to queue to try later? - // Or maybe discard it? - // "If you cannot place the room... the passage is a dead end." (Some editions) - // Let's keep it open for now, maybe next card fits. + 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); } - return true; // Step done + 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 }; + } + } + + findExitReference(exit) { + // If facing North/South, Reference is Minimum X. + // If facing East/West, Reference is Minimum Y. + + // 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. + + 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; + } + + 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 + + // 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; + } + + 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; + } + + return { x: rx, y: ry }; + } getOppositeDirection(dir) { switch (dir) { @@ -143,109 +259,23 @@ export class DungeonGenerator { } calculateRequiredRotation(localDir, targetGlobalDir) { - // e.g. Local=NORTH needs to become Global=EAST. - // N(0) -> E(1). Diff +1 (90 deg). - // Standard mapping: N=0, E=1, S=2, W=3 const dirs = [DIRECTIONS.NORTH, DIRECTIONS.EAST, DIRECTIONS.SOUTH, DIRECTIONS.WEST]; const localIdx = dirs.indexOf(localDir); const targetIdx = dirs.indexOf(targetGlobalDir); - - // (Local + Rotation) % 4 = Target - // Rotation = (Target - Local + 4) % 4 const diff = (targetIdx - localIdx + 4) % 4; return dirs[diff]; } - calculateTilePosition(targetExitGlobal, candidateExitLocal, rotation) { - // We know the Global Coordinate of the connection point (targetExitGlobal) - // We know the Local Coordinate of the matching exit on the new tile (candidateExitLocal) - // We need 'startX, startY' of the new tile. - - // First, transform the local exit to a rotated offset - // We reuse GridSystem logic logic ideally, but let's do math here - let offsetX, offsetY; - - // Replicating GridSystem.getGlobalPoint simple logic for vector only - // If we treat candidateExitLocal as a vector from (0,0) - const lx = candidateExitLocal.x; - const ly = candidateExitLocal.y; - - switch (rotation) { - case DIRECTIONS.NORTH: offsetX = lx; offsetY = ly; break; - case DIRECTIONS.SOUTH: offsetX = -lx; offsetY = -ly; break; - case DIRECTIONS.EAST: offsetX = ly; offsetY = -lx; break; - case DIRECTIONS.WEST: offsetX = -ly; offsetY = lx; break; - } - - // GlobalExit = TilePos + RotatedOffset - // TilePos = GlobalExit - RotatedOffset - - // Wait, 'targetExitGlobal' is the cell just OUTSIDE the previous tile? - // Or the cell OF the previous tile's exit? - // Usually targetExit is "The cell where the connection happens". - // In GridSystem, exits are defined AT the edge. - // Let's assume targetExitGlobal is the coordinate OF THE EXIT CELL on the previous tile. - // So the new tile's matching exit cell must OVERLAP this one? NO. - // They must be adjacent. - - // Correction: Tiles must connect *adjacent* to each other. - // If TargetExit is at (10,10) facing NORTH, the New Tile must attach at (10,11). - - let connectionPointX = targetExitGlobal.x; - let connectionPointY = targetExitGlobal.y; - - // Move 1 step in the target direction to find the "Anchor Point" for the new tile - switch (targetExitGlobal.direction) { - case DIRECTIONS.NORTH: connectionPointY += 1; break; - case DIRECTIONS.SOUTH: connectionPointY -= 1; break; - case DIRECTIONS.EAST: connectionPointX += 1; break; - case DIRECTIONS.WEST: connectionPointX -= 1; break; - } - - // Now align the new tile such that its candidate exit lands on connectionPoint - return { - x: connectionPointX - offsetX, - y: connectionPointY - offsetY - }; - } - - addExitsToQueue(tileInstance, tileDef, excludeSourceExit = null) { - // Calculate all global exits for this placed tile + 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); - // If this is the exit we just entered through, skip it - // Logic: connection is adjacent. - // A simpler check: if we just connected to (X,Y), don't add an exit at (X,Y). - // But we calculated 'connectionPoint' as the place where the NEW tile's exit is. + // Check if blocked immediately + const neighbor = this.getNeighborCell(globalPoint.x, globalPoint.y, globalDir); + const key = `${neighbor.x},${neighbor.y}`; - // Check adjacency to excludeSource? - // Or better: excludeSourceExit is the "Previous Tile's Exit". - // The "Entrance" on the new tile connects to that. - // We should just not add the exit that was used as input. - - // How to identify it? - // We calculated it in the main loop. - // Let's simplify: Add ALL exits. - // The logic later will filter out exits that point into occupied cells? - // Yes, checking collision also checks if the target cell is free. - // But we don't want to list "Backwards" exits. - - // Optimization: If the cell immediate to this exit is already occupied, don't add it. - // This handles the "Entrance" naturally (it points back to the previous tile). - - let neighborX = globalPoint.x; - let neighborY = globalPoint.y; - switch (globalDir) { - case DIRECTIONS.NORTH: neighborY += 1; break; - case DIRECTIONS.SOUTH: neighborY -= 1; break; - case DIRECTIONS.EAST: neighborX += 1; break; - case DIRECTIONS.WEST: neighborX -= 1; break; - } - - const neighborKey = `${neighborX},${neighborY}`; - if (!this.grid.occupiedCells.has(neighborKey)) { + if (!this.grid.occupiedCells.has(key)) { this.pendingExits.push({ x: globalPoint.x, y: globalPoint.y, @@ -254,4 +284,13 @@ export class DungeonGenerator { } } } + + 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); + }); + } } diff --git a/src/engine/dungeon/TileDefinitions.js b/src/engine/dungeon/TileDefinitions.js index 766d3c0..1fafe70 100644 --- a/src/engine/dungeon/TileDefinitions.js +++ b/src/engine/dungeon/TileDefinitions.js @@ -1,34 +1,29 @@ + import { DIRECTIONS, TILE_TYPES } from './Constants.js'; export const TILES = [ - // --- CORRIDORS (Corredores) --- { id: 'corridor_straight', name: 'Corridor', type: TILE_TYPES.CORRIDOR, width: 2, length: 6, - textures: ['/assets/images/dungeon1/tiles/corridor1.png', '/assets/images/dungeon1/tiles/corridor2.png', '/assets/images/dungeon1/tiles/corridor3.png'], // Visual variety - // Layout: 6 rows + textures: ['/assets/images/dungeon1/tiles/corridor1.png'], layout: [ - [1, 1], // y=5 (North End - Trident?) - [1, 1], // y=4 - [1, 1], // y=3 - [1, 1], // y=2 - [1, 1], // y=1 - [1, 1] // y=0 (South End - Single Input) + [1, 1], // y=5 (North) + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1] // y=0 (South) ], exits: [ - // South End (1 direction) + // South { x: 0, y: 0, direction: DIRECTIONS.SOUTH }, { x: 1, y: 0, direction: DIRECTIONS.SOUTH }, - - // North End (3 Directions: N, plus Side E/W meaning West/East in vertical) - { x: 0, y: 5, direction: DIRECTIONS.NORTH }, // Straight Out - { x: 1, y: 5, direction: DIRECTIONS.NORTH }, - - { x: 0, y: 5, direction: DIRECTIONS.WEST }, - { x: 1, y: 5, direction: DIRECTIONS.EAST } + // North + { x: 0, y: 5, direction: DIRECTIONS.NORTH }, + { x: 1, y: 5, direction: DIRECTIONS.NORTH } ] }, { @@ -38,18 +33,19 @@ export const TILES = [ width: 2, length: 6, textures: ['/assets/images/dungeon1/tiles/stairs1.png'], - // Layout includes 9 for stairs? User example used 9. layout: [ - [2, 2], // y=5 (High end) - [2, 2], - [9, 9], // Stairs - [9, 9], [1, 1], - [1, 1] // y=0 (Low end) + [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 } ] @@ -61,17 +57,17 @@ export const TILES = [ width: 4, length: 4, textures: ['/assets/images/dungeon1/tiles/L.png'], - // L Shape layout: [ - [1, 1, 1, 1], // y=3 - [1, 1, 1, 1], // y=2 + [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 + [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 } ] @@ -83,42 +79,46 @@ export const TILES = [ width: 6, length: 4, textures: ['/assets/images/dungeon1/tiles/T.png'], - // T-Shape layout: [ [1, 1, 1, 1, 1, 1], // y=3 - [1, 1, 1, 1, 1, 1], // y=2 + [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 + [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 } ] }, - - // --- ROOMS --- { 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'], + 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], - [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 } ] @@ -129,16 +129,19 @@ export const TILES = [ 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'], + textures: [ + '/assets/images/dungeon1/tiles/room_4x8_altar.png', + '/assets/images/dungeon1/tiles/room_4x8_tomb.png' + ], layout: [ - [1, 1, 1, 1], // y=7 [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] // y=0 + [1, 1, 1, 1], + [1, 1, 1, 1] // South Exit ], exits: [ { x: 1, y: 0, direction: DIRECTIONS.SOUTH }, diff --git a/src/main.js b/src/main.js index 305d391..ff8dfe1 100644 --- a/src/main.js +++ b/src/main.js @@ -45,15 +45,22 @@ console.log("Starting Dungeon Generation..."); generator.startDungeon(mission); // 4. Render Loop -const animate = () => { +let lastStepTime = 0; +const STEP_DELAY = 1000; // 1 second delay + +const animate = (time) => { requestAnimationFrame(animate); - // Logic Step + // Logic Step with Delay if (!generator.isComplete) { - generator.step(); + if (time - lastStepTime > STEP_DELAY) { + console.log("--- Executing Generation Step ---"); + generator.step(); + lastStepTime = time; + } } // Render renderer.render(cameraManager.getCamera()); }; -animate(); +animate(0); diff --git a/src/view/GameRenderer.js b/src/view/GameRenderer.js index efa8159..45afde5 100644 --- a/src/view/GameRenderer.js +++ b/src/view/GameRenderer.js @@ -104,17 +104,23 @@ export class GameRenderer { // Create Plane const geometry = new THREE.PlaneGeometry(w, l); - // Use MeshStandardMaterial for reaction to light if needed - const material = new THREE.MeshStandardMaterial({ + + // 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, side: THREE.FrontSide, // Only visible from top - alphaTest: 0.1, - roughness: 0.8, - metalness: 0.2 + alphaTest: 0.1 }); const plane = new THREE.Mesh(geometry, material); + // DEBUG: Add a wireframe border to see the physical title limits + const borderGeom = new THREE.EdgesGeometry(geometry); + const borderMat = new THREE.LineBasicMaterial({ color: 0x00ff00, linewidth: 2 }); + const border = new THREE.LineSegments(borderGeom, borderMat); + plane.add(border); + // Initial Rotation: Plane X-Y to X-Z plane.rotation.x = -Math.PI / 2;