Phase 1 Complete: Dungeon Engine & Visuals. Switched to Manual Exploration Plan.
This commit is contained in:
13
DEVLOG.md
13
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.
|
||||
|
||||
@@ -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: {}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -5,25 +5,24 @@
|
||||
- [x] Define Tile Data (Dimensions, Exits, Type) <!-- id: 1 -->
|
||||
- [x] Define Dungeon Deck System (Cards, Shuffling, Probability) <!-- id: 2 -->
|
||||
- [x] Define Mission Configuration Structure (Objective vs Exit) <!-- id: 3 -->
|
||||
- [ ] Define Mission Configuration Structure (Objective vs Exit) <!-- id: 3 -->
|
||||
- [x] **Grid & Logic System**
|
||||
- [x] Implement Tile Placement Logic (Collision Detection, Alignment) <!-- id: 4 -->
|
||||
- [x] Implement Connection Points (Exits/Entrances matching) <!-- id: 5 -->
|
||||
- [x] Implement "Board" State (Tracking placed tiles) <!-- id: 6 -->
|
||||
- [ ] **Generation Algorithms**
|
||||
- [x] **Generation Algorithms**
|
||||
- [x] Basic "Next Tile" Generation Rule <!-- id: 7 -->
|
||||
- [x] Implement "Exit Room" Logic for Non-Final Missions <!-- id: 8 -->
|
||||
- [x] Implement "Objective Room" Logic for Final Missions <!-- id: 9 -->
|
||||
- [x] Create Loop for Full Dungeon Generation <!-- id: 10 -->
|
||||
- [x] Create Loop for Full Dungeon Generation (Stopped for manual exploration) <!-- id: 10 -->
|
||||
|
||||
## Phase 2: 3D Visualization & Camera
|
||||
- [ ] **Scene Setup**
|
||||
- [x] **Scene Setup**
|
||||
- [x] Setup Three.js Scene, Light, and Renderer <!-- id: 20 -->
|
||||
- [x] Implement Isometric Camera (Orthographic) <!-- id: 21 -->
|
||||
- [x] Implement Fixed Orbit Controls (N, S, E, W snapshots) <!-- id: 22 -->
|
||||
- [ ] **Asset Management**
|
||||
- [ ] Tile Model/Texture Loading <!-- id: 23 -->
|
||||
- [ ] dynamic Tile Instancing based on Grid State <!-- id: 24 -->
|
||||
- [x] **Asset Management**
|
||||
- [x] Tile Model/Texture Loading <!-- id: 23 -->
|
||||
- [x] dynamic Tile Instancing based on Grid State <!-- id: 24 -->
|
||||
|
||||
## Phase 3: Game Mechanics (Loop)
|
||||
- [ ] **Turn System**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
15
src/main.js
15
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user