feat: Add traps

This commit is contained in:
Peter Stockings
2026-01-25 16:37:46 +11:00
parent 18d4f0cdd4
commit 9552364a60
14 changed files with 2225 additions and 11 deletions

View File

@@ -11,6 +11,9 @@ import {
} from "../../core/config/Items";
import { seededRandom } from "../../core/math";
import * as ROT from "rot-js";
import { ECSWorld } from "../ecs/World";
import { Prefabs } from "../ecs/Prefabs";
interface Room {
x: number;
@@ -23,9 +26,9 @@ interface Room {
* Generates a procedural dungeon world with rooms and corridors using rot-js Uniform algorithm
* @param floor The floor number (affects difficulty)
* @param runState Player's persistent state across floors
* @returns Generated world and player ID
* @returns Generated world, player ID, and ECS world with traps
*/
export function generateWorld(floor: number, runState: RunState): { world: World; playerId: EntityId } {
export function generateWorld(floor: number, runState: RunState): { world: World; playerId: EntityId; ecsWorld: ECSWorld } {
const width = GAME_CONFIG.map.width;
const height = GAME_CONFIG.map.height;
const tiles: Tile[] = new Array(width * height).fill(TileType.WALL);
@@ -80,6 +83,13 @@ export function generateWorld(floor: number, runState: RunState): { world: World
placeEnemies(floor, rooms, actors, random);
// Create ECS world and place traps
const ecsWorld = new ECSWorld();
const occupiedPositions = new Set<string>();
occupiedPositions.add(`${playerX},${playerY}`); // Don't place traps on player start
occupiedPositions.add(`${exit.x},${exit.y}`); // Don't place traps on exit
placeTraps(floor, rooms, ecsWorld, tiles, width, random, occupiedPositions);
// Place doors for dungeon levels (Uniform/Digger)
// Caves (Floors 10+) shouldn't have manufactured doors
if (floor <= 9) {
@@ -94,7 +104,8 @@ export function generateWorld(floor: number, runState: RunState): { world: World
return {
world: { width, height, tiles, actors, exit },
playerId
playerId,
ecsWorld
};
}
@@ -426,6 +437,70 @@ function placeEnemies(floor: number, rooms: Room[], actors: Map<EntityId, Actor>
}
}
/**
* Place traps randomly in dungeon rooms.
* Trap density increases with floor depth.
*/
function placeTraps(
floor: number,
rooms: Room[],
ecsWorld: ECSWorld,
tiles: Tile[],
width: number,
random: () => number,
occupiedPositions: Set<string>
): void {
// Trap configuration
const trapTypes = ["poison", "fire", "paralysis"] as const;
// Number of traps scales with floor (1-2 on floor 1, up to 5-6 on floor 10)
const minTraps = 1 + Math.floor(floor / 3);
const maxTraps = minTraps + 2;
const numTraps = minTraps + Math.floor(random() * (maxTraps - minTraps + 1));
for (let i = 0; i < numTraps; i++) {
// Pick a random room (not the starting room)
const roomIdx = 1 + Math.floor(random() * (rooms.length - 1));
const room = rooms[roomIdx];
// Try to find a valid position
for (let attempts = 0; attempts < 10; attempts++) {
const tx = room.x + 1 + Math.floor(random() * (room.width - 2));
const ty = room.y + 1 + Math.floor(random() * (room.height - 2));
const key = `${tx},${ty}`;
// Check if position is valid (floor tile, not occupied)
const tileIdx = ty * width + tx;
const isFloor = tiles[tileIdx] === TileType.EMPTY ||
tiles[tileIdx] === TileType.EMPTY_DECO ||
tiles[tileIdx] === TileType.GRASS_SAPLINGS;
if (isFloor && !occupiedPositions.has(key)) {
// Pick a random trap type
const trapType = trapTypes[Math.floor(random() * trapTypes.length)];
// Scale effect duration/magnitude with floor
const duration = 3 + Math.floor(floor / 3);
const magnitude = 2 + Math.floor(floor / 2);
switch (trapType) {
case "poison":
Prefabs.poisonTrap(ecsWorld, tx, ty, duration, magnitude);
break;
case "fire":
Prefabs.fireTrap(ecsWorld, tx, ty, Math.ceil(duration / 2), magnitude + 2);
break;
case "paralysis":
Prefabs.paralysisTrap(ecsWorld, tx, ty, Math.max(2, Math.ceil(duration / 2)));
break;
}
occupiedPositions.add(key);
break;
}
}
}
}
export const makeTestWorld = generateWorld;