Added flamethrower with buring effects
This commit is contained in:
@@ -2,13 +2,14 @@ import { type World, type EntityId, type RunState, type Tile, type Vec2 } from "
|
||||
import { TileType } from "../../core/terrain";
|
||||
import { idx } from "./world-logic";
|
||||
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||
import {
|
||||
createConsumable,
|
||||
createMeleeWeapon,
|
||||
createRangedWeapon,
|
||||
import {
|
||||
createConsumable,
|
||||
createMeleeWeapon,
|
||||
createRangedWeapon,
|
||||
createArmour,
|
||||
createUpgradeScroll,
|
||||
createAmmo
|
||||
createAmmo,
|
||||
createFlamethrower
|
||||
} from "../../core/config/Items";
|
||||
import { seededRandom } from "../../core/math";
|
||||
import * as ROT from "rot-js";
|
||||
@@ -36,7 +37,7 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
const tiles: Tile[] = new Array(width * height).fill(TileType.WALL);
|
||||
|
||||
const random = seededRandom(floor * 12345);
|
||||
|
||||
|
||||
// Create ECS World first
|
||||
const ecsWorld = new ECSWorld(); // Starts at ID 1 by default
|
||||
|
||||
@@ -44,28 +45,29 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
ROT.RNG.setSeed(floor * 12345);
|
||||
|
||||
const rooms = generateRooms(width, height, tiles, floor, random);
|
||||
|
||||
|
||||
// Place player in first room
|
||||
const firstRoom = rooms[0];
|
||||
const playerX = firstRoom.x + Math.floor(firstRoom.width / 2);
|
||||
const playerY = firstRoom.y + Math.floor(firstRoom.height / 2);
|
||||
|
||||
|
||||
// Create Player Entity in ECS
|
||||
const runInventory = {
|
||||
gold: runState.inventory.gold,
|
||||
items: [
|
||||
...runState.inventory.items,
|
||||
// Add starting items for testing if empty
|
||||
...(runState.inventory.items.length === 0 ? [
|
||||
createConsumable("health_potion", 2),
|
||||
createMeleeWeapon("iron_sword", "sharp"),
|
||||
createConsumable("throwing_dagger", 3),
|
||||
createRangedWeapon("pistol"),
|
||||
createAmmo("ammo_9mm", 10),
|
||||
createArmour("leather_armor", "heavy"),
|
||||
createUpgradeScroll(2)
|
||||
] : [])
|
||||
]
|
||||
const runInventory = {
|
||||
gold: runState.inventory.gold,
|
||||
items: [
|
||||
...runState.inventory.items,
|
||||
// Add starting items for testing if empty
|
||||
...(runState.inventory.items.length === 0 ? [
|
||||
createConsumable("health_potion", 2),
|
||||
createMeleeWeapon("iron_sword", "sharp"),
|
||||
createConsumable("throwing_dagger", 3),
|
||||
createRangedWeapon("pistol"),
|
||||
createAmmo("ammo_9mm", 10),
|
||||
createFlamethrower(),
|
||||
createArmour("leather_armor", "heavy"),
|
||||
createUpgradeScroll(2)
|
||||
] : [])
|
||||
]
|
||||
};
|
||||
|
||||
const playerId = EntityBuilder.create(ecsWorld)
|
||||
@@ -78,7 +80,7 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
.build();
|
||||
|
||||
// No more legacy Actors Map
|
||||
|
||||
|
||||
// Place exit in last room
|
||||
const lastRoom = rooms[rooms.length - 1];
|
||||
const exit: Vec2 = {
|
||||
@@ -87,14 +89,14 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
};
|
||||
|
||||
placeEnemies(floor, rooms, ecsWorld, random);
|
||||
|
||||
|
||||
// Place traps (using same 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) {
|
||||
@@ -102,13 +104,13 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
}
|
||||
|
||||
decorate(width, height, tiles, random, exit);
|
||||
|
||||
|
||||
// CRITICAL FIX: Ensure player start position is always clear!
|
||||
// Otherwise spawning in Grass (which blocks vision) makes the player blind.
|
||||
tiles[playerY * width + playerX] = TileType.EMPTY;
|
||||
|
||||
return {
|
||||
world: { width, height, tiles, exit },
|
||||
|
||||
return {
|
||||
world: { width, height, tiles, exit },
|
||||
playerId,
|
||||
ecsWorld
|
||||
};
|
||||
@@ -118,10 +120,10 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
// Update generateRooms signature to accept random
|
||||
function generateRooms(width: number, height: number, tiles: Tile[], floor: number, random: () => number): Room[] {
|
||||
const rooms: Room[] = [];
|
||||
|
||||
|
||||
// Choose dungeon algorithm based on floor depth
|
||||
let dungeon: any;
|
||||
|
||||
|
||||
if (floor <= 4) {
|
||||
// Floors 1-4: Uniform (organic, irregular rooms)
|
||||
dungeon = new ROT.Map.Uniform(width, height, {
|
||||
@@ -142,7 +144,7 @@ function generateRooms(width: number, height: number, tiles: Tile[], floor: numb
|
||||
born: [4, 5, 6, 7, 8],
|
||||
survive: [2, 3, 4, 5],
|
||||
});
|
||||
|
||||
|
||||
// Cellular needs randomization and smoothing
|
||||
dungeon.randomize(0.5);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
@@ -160,7 +162,7 @@ function generateRooms(width: number, height: number, tiles: Tile[], floor: numb
|
||||
|
||||
// Extract room information from the generated dungeon
|
||||
const roomData = (dungeon as any).getRooms?.();
|
||||
|
||||
|
||||
if (roomData && roomData.length > 0) {
|
||||
// Traditional dungeons (Uniform/Digger) have explicit rooms
|
||||
for (const room of roomData) {
|
||||
@@ -174,7 +176,7 @@ function generateRooms(width: number, height: number, tiles: Tile[], floor: numb
|
||||
} else {
|
||||
// Cellular caves don't have explicit rooms, so find connected floor areas
|
||||
rooms.push(...extractRoomsFromCave(width, height, tiles));
|
||||
|
||||
|
||||
// Connect the isolated cave rooms
|
||||
connectRooms(width, tiles, rooms, random);
|
||||
}
|
||||
@@ -196,13 +198,13 @@ function generateRooms(width: number, height: number, tiles: Tile[], floor: numb
|
||||
function connectRooms(width: number, tiles: Tile[], rooms: Room[], random: () => number) {
|
||||
for (let i = 0; i < rooms.length - 1; i++) {
|
||||
const r1 = rooms[i];
|
||||
const r2 = rooms[i+1];
|
||||
|
||||
const r2 = rooms[i + 1];
|
||||
|
||||
const c1x = r1.x + Math.floor(r1.width / 2);
|
||||
const c1y = r1.y + Math.floor(r1.height / 2);
|
||||
const c2x = r2.x + Math.floor(r2.width / 2);
|
||||
const c2y = r2.y + Math.floor(r2.height / 2);
|
||||
|
||||
|
||||
if (random() < 0.5) {
|
||||
digH(width, tiles, c1x, c2x, c1y);
|
||||
digV(width, tiles, c1y, c2y, c2x);
|
||||
@@ -241,14 +243,14 @@ function digV(width: number, tiles: Tile[], y1: number, y2: number, x: number) {
|
||||
function extractRoomsFromCave(width: number, height: number, tiles: Tile[]): Room[] {
|
||||
const rooms: Room[] = [];
|
||||
const visited = new Set<number>();
|
||||
|
||||
|
||||
// Find large connected floor areas
|
||||
for (let y = 1; y < height - 1; y++) {
|
||||
for (let x = 1; x < width - 1; x++) {
|
||||
const idx = y * width + x;
|
||||
if (tiles[idx] === TileType.EMPTY && !visited.has(idx)) {
|
||||
const cluster = floodFill(width, height, tiles, x, y, visited);
|
||||
|
||||
|
||||
// Only consider clusters larger than 20 tiles
|
||||
if (cluster.length > 20) {
|
||||
// Create bounding box for this cluster
|
||||
@@ -261,7 +263,7 @@ function extractRoomsFromCave(width: number, height: number, tiles: Tile[]): Roo
|
||||
minY = Math.min(minY, cy);
|
||||
maxY = Math.max(maxY, cy);
|
||||
}
|
||||
|
||||
|
||||
rooms.push({
|
||||
x: minX,
|
||||
y: minY,
|
||||
@@ -272,7 +274,7 @@ function extractRoomsFromCave(width: number, height: number, tiles: Tile[]): Roo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
@@ -282,17 +284,17 @@ function extractRoomsFromCave(width: number, height: number, tiles: Tile[]): Roo
|
||||
function floodFill(width: number, height: number, tiles: Tile[], startX: number, startY: number, visited: Set<number>): number[] {
|
||||
const cluster: number[] = [];
|
||||
const queue: number[] = [startY * width + startX];
|
||||
|
||||
|
||||
while (queue.length > 0) {
|
||||
const idx = queue.shift()!;
|
||||
if (visited.has(idx)) continue;
|
||||
|
||||
|
||||
visited.add(idx);
|
||||
cluster.push(idx);
|
||||
|
||||
|
||||
const x = idx % width;
|
||||
const y = Math.floor(idx / width);
|
||||
|
||||
|
||||
// Check 4 directions
|
||||
const neighbors = [
|
||||
{ nx: x + 1, ny: y },
|
||||
@@ -300,7 +302,7 @@ function floodFill(width: number, height: number, tiles: Tile[], startX: number,
|
||||
{ nx: x, ny: y + 1 },
|
||||
{ nx: x, ny: y - 1 },
|
||||
];
|
||||
|
||||
|
||||
for (const { nx, ny } of neighbors) {
|
||||
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
||||
const nIdx = ny * width + nx;
|
||||
@@ -310,7 +312,7 @@ function floodFill(width: number, height: number, tiles: Tile[], startX: number,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
@@ -323,19 +325,19 @@ function decorate(width: number, height: number, tiles: Tile[], random: () => nu
|
||||
// Use Simplex noise for natural-looking grass distribution
|
||||
const grassNoise = new ROT.Noise.Simplex();
|
||||
const decorationNoise = new ROT.Noise.Simplex();
|
||||
|
||||
|
||||
// Offset noise to get different patterns for grass vs decorations
|
||||
const grassOffset = random() * 1000;
|
||||
const decorOffset = random() * 1000;
|
||||
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = idx(world as any, x, y);
|
||||
|
||||
|
||||
if (tiles[i] === TileType.EMPTY) {
|
||||
// Grass patches: use noise to create organic shapes
|
||||
const grassValue = grassNoise.get((x + grassOffset) / 15, (y + grassOffset) / 15);
|
||||
|
||||
|
||||
// Create grass patches where noise is above threshold
|
||||
if (grassValue > 0.35) {
|
||||
tiles[i] = TileType.GRASS;
|
||||
@@ -345,7 +347,7 @@ function decorate(width: number, height: number, tiles: Tile[], random: () => nu
|
||||
} else {
|
||||
// Floor decorations (moss/rocks): clustered distribution
|
||||
const decoValue = decorationNoise.get((x + decorOffset) / 8, (y + decorOffset) / 8);
|
||||
|
||||
|
||||
// Dense clusters where noise is high
|
||||
if (decoValue > 0.5) {
|
||||
tiles[i] = TileType.EMPTY_DECO;
|
||||
@@ -364,9 +366,9 @@ function decorate(width: number, height: number, tiles: Tile[], random: () => nu
|
||||
const i = idx(world as any, x, y);
|
||||
const nextY = idx(world as any, x, y + 1);
|
||||
|
||||
if (tiles[i] === TileType.WALL &&
|
||||
tiles[nextY] === TileType.GRASS &&
|
||||
random() < 0.25) {
|
||||
if (tiles[i] === TileType.WALL &&
|
||||
tiles[nextY] === TileType.GRASS &&
|
||||
random() < 0.25) {
|
||||
tiles[i] = TileType.WALL_DECO;
|
||||
}
|
||||
}
|
||||
@@ -374,8 +376,8 @@ function decorate(width: number, height: number, tiles: Tile[], random: () => nu
|
||||
}
|
||||
|
||||
function placeEnemies(floor: number, rooms: Room[], ecsWorld: ECSWorld, random: () => number): void {
|
||||
const numEnemies = GAME_CONFIG.enemyScaling.baseCount + floor * GAME_CONFIG.enemyScaling.baseCountPerFloor;
|
||||
|
||||
const numEnemies = GAME_CONFIG.enemyScaling.baseCount + floor * GAME_CONFIG.enemyScaling.baseCountPerFloor;
|
||||
|
||||
const enemyTypes = Object.keys(GAME_CONFIG.enemies);
|
||||
const occupiedPositions = new Set<string>();
|
||||
|
||||
@@ -383,7 +385,7 @@ function placeEnemies(floor: number, rooms: Room[], ecsWorld: ECSWorld, random:
|
||||
// Pick a random room (not the starting room 0)
|
||||
const roomIdx = 1 + Math.floor(random() * (rooms.length - 1));
|
||||
const room = rooms[roomIdx];
|
||||
|
||||
|
||||
// Try to find an empty spot in the room
|
||||
for (let attempts = 0; attempts < 5; attempts++) {
|
||||
|
||||
@@ -397,23 +399,23 @@ function placeEnemies(floor: number, rooms: Room[], ecsWorld: ECSWorld, random:
|
||||
|
||||
const scaledHp = enemyDef.baseHp + floor * GAME_CONFIG.enemyScaling.hpPerFloor;
|
||||
const scaledAttack = enemyDef.baseAttack + Math.floor(floor / 2) * GAME_CONFIG.enemyScaling.attackPerTwoFloors;
|
||||
|
||||
|
||||
const speed = enemyDef.minSpeed + Math.floor(random() * (enemyDef.maxSpeed - enemyDef.minSpeed));
|
||||
|
||||
// Create Enemy in ECS
|
||||
EntityBuilder.create(ecsWorld)
|
||||
.asEnemy(type)
|
||||
.withPosition(ex, ey)
|
||||
.withStats({
|
||||
maxHp: scaledHp + Math.floor(random() * 4),
|
||||
hp: scaledHp + Math.floor(random() * 4),
|
||||
attack: scaledAttack + Math.floor(random() * 2),
|
||||
defense: enemyDef.baseDefense,
|
||||
})
|
||||
.withEnergy(speed) // Configured speed
|
||||
// Note: Other stats like crit/evasion are defaults from EntityBuilder or BaseStats
|
||||
.build();
|
||||
|
||||
.asEnemy(type)
|
||||
.withPosition(ex, ey)
|
||||
.withStats({
|
||||
maxHp: scaledHp + Math.floor(random() * 4),
|
||||
hp: scaledHp + Math.floor(random() * 4),
|
||||
attack: scaledAttack + Math.floor(random() * 2),
|
||||
defense: enemyDef.baseDefense,
|
||||
})
|
||||
.withEnergy(speed) // Configured speed
|
||||
// Note: Other stats like crit/evasion are defaults from EntityBuilder or BaseStats
|
||||
.build();
|
||||
|
||||
occupiedPositions.add(k);
|
||||
break;
|
||||
}
|
||||
@@ -436,37 +438,37 @@ function placeTraps(
|
||||
): 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;
|
||||
|
||||
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);
|
||||
@@ -478,7 +480,7 @@ function placeTraps(
|
||||
Prefabs.paralysisTrap(ecsWorld, tx, ty, Math.max(2, Math.ceil(duration / 2)));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
occupiedPositions.add(key);
|
||||
break;
|
||||
}
|
||||
@@ -494,7 +496,7 @@ function placeDoors(width: number, height: number, tiles: Tile[], rooms: Room[],
|
||||
const i = idx({ width, height } as any, x, y);
|
||||
if (tiles[i] === TileType.EMPTY) {
|
||||
// Found a connection (floor tile on perimeter)
|
||||
|
||||
|
||||
// 50% chance to place a door
|
||||
if (random() < 0.5) {
|
||||
// 90% chance for closed door, 10% for open
|
||||
@@ -507,7 +509,7 @@ function placeDoors(width: number, height: number, tiles: Tile[], rooms: Room[],
|
||||
// Scan top and bottom walls
|
||||
const topY = room.y - 1;
|
||||
const bottomY = room.y + room.height;
|
||||
|
||||
|
||||
// Scan horizontal perimeters (iterate x from left-1 to right+1 to cover corners too if needed,
|
||||
// but usually doors are in the middle segments. Let's cover the full range adjacent to room.)
|
||||
for (let x = room.x; x < room.x + room.width; x++) {
|
||||
@@ -518,7 +520,7 @@ function placeDoors(width: number, height: number, tiles: Tile[], rooms: Room[],
|
||||
// Scan left and right walls
|
||||
const leftX = room.x - 1;
|
||||
const rightX = room.x + room.width;
|
||||
|
||||
|
||||
for (let y = room.y; y < room.y + room.height; y++) {
|
||||
if (leftX >= 0) checkAndPlaceDoor(leftX, y);
|
||||
if (rightX < width) checkAndPlaceDoor(rightX, y);
|
||||
|
||||
Reference in New Issue
Block a user