Change black empty tile to grass and make it destructable
This commit is contained in:
@@ -63,6 +63,28 @@ describe('Combat Simulation', () => {
|
||||
expect(orb).toBeDefined();
|
||||
expect(orb!.id).toBe(3);
|
||||
});
|
||||
|
||||
it("should destruction tile when walking on destructible-by-walk tile", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
actors.set(1, {
|
||||
id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, stats: createTestStats()
|
||||
} as any);
|
||||
|
||||
const world = createTestWorld(actors);
|
||||
// tile at 4,3 is grass (15) which is destructible by walk
|
||||
const grassIdx = 3 * 10 + 4;
|
||||
world.tiles[grassIdx] = 15; // TileType.GRASS
|
||||
|
||||
entityManager = new EntityManager(world);
|
||||
applyAction(world, 1, { type: "move", dx: 1, dy: 0 }, entityManager);
|
||||
|
||||
// Player moved to 4,3
|
||||
const player = world.actors.get(1);
|
||||
expect(player!.pos).toEqual({ x: 4, y: 3 });
|
||||
|
||||
// Tile should effectively be destroyed (turned to empty/1)
|
||||
expect(world.tiles[grassIdx]).toBe(1); // TileType.EMPTY
|
||||
});
|
||||
});
|
||||
|
||||
describe("decideEnemyAction - AI Logic", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { idx, inBounds, isWall, isBlocked } from '../world/world-logic';
|
||||
import { type World, type Tile } from '../../core/types';
|
||||
import { GAME_CONFIG } from '../../core/config/GameConfig';
|
||||
import { TileType } from '../../core/terrain';
|
||||
|
||||
|
||||
describe('World Utilities', () => {
|
||||
@@ -46,9 +46,9 @@ describe('World Utilities', () => {
|
||||
|
||||
describe('isWall', () => {
|
||||
it('should return true for wall tiles', () => {
|
||||
const tiles: Tile[] = new Array(100).fill(GAME_CONFIG.terrain.empty);
|
||||
tiles[0] = GAME_CONFIG.terrain.wall; // wall at 0,0
|
||||
tiles[55] = GAME_CONFIG.terrain.wall; // wall at 5,5
|
||||
const tiles: Tile[] = new Array(100).fill(TileType.EMPTY);
|
||||
tiles[0] = TileType.WALL; // wall at 0,0
|
||||
tiles[55] = TileType.WALL; // wall at 5,5
|
||||
|
||||
|
||||
const world = createTestWorld(10, 10, tiles);
|
||||
@@ -58,7 +58,7 @@ describe('World Utilities', () => {
|
||||
});
|
||||
|
||||
it('should return false for floor tiles', () => {
|
||||
const tiles: Tile[] = new Array(100).fill(GAME_CONFIG.terrain.empty);
|
||||
const tiles: Tile[] = new Array(100).fill(TileType.EMPTY);
|
||||
const world = createTestWorld(10, 10, tiles);
|
||||
|
||||
expect(isWall(world, 3, 3)).toBe(false);
|
||||
@@ -76,8 +76,8 @@ describe('World Utilities', () => {
|
||||
|
||||
describe('isBlocked', () => {
|
||||
it('should return true for walls', () => {
|
||||
const tiles: Tile[] = new Array(100).fill(GAME_CONFIG.terrain.empty);
|
||||
tiles[55] = GAME_CONFIG.terrain.wall; // wall at 5,5
|
||||
const tiles: Tile[] = new Array(100).fill(TileType.EMPTY);
|
||||
tiles[55] = TileType.WALL; // wall at 5,5
|
||||
|
||||
|
||||
const world = createTestWorld(10, 10, tiles);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { World, EntityId, Action, SimEvent, Actor, CombatantActor, CollectibleActor, ActorType } from "../../core/types";
|
||||
|
||||
import { isBlocked, inBounds, isWall } from "../world/world-logic";
|
||||
import { isBlocked, inBounds, isWall, tryDestructTile } from "../world/world-logic";
|
||||
import { isDestructibleByWalk } from "../../core/terrain";
|
||||
import { findPathAStar } from "../world/pathfinding";
|
||||
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||
import { type EntityManager } from "../EntityManager";
|
||||
@@ -105,6 +106,16 @@ function handleMove(w: World, actor: Actor, action: { dx: number; dy: number },
|
||||
const to = { ...actor.pos };
|
||||
const events: SimEvent[] = [{ type: "moved", actorId: actor.id, from, to }];
|
||||
|
||||
// Check for "destructible by walk" tiles (e.g. grass)
|
||||
// We check the tile at the *new* position
|
||||
const tileIdx = ny * w.width + nx;
|
||||
const tile = w.tiles[tileIdx];
|
||||
if (isDestructibleByWalk(tile)) {
|
||||
tryDestructTile(w, nx, ny);
|
||||
// Optional: Add an event if we want visual feedback immediately,
|
||||
// but the renderer usually handles map updates automatically or next frame
|
||||
}
|
||||
|
||||
if (actor.category === "combatant" && actor.isPlayer) {
|
||||
handleExpCollection(w, actor, events, em);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type World, type EntityId, type RunState, type Tile, type Actor, type Vec2 } from "../../core/types";
|
||||
import { TileType } from "../../core/terrain";
|
||||
import { idx } from "./world-logic";
|
||||
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||
import { seededRandom } from "../../core/math";
|
||||
@@ -20,7 +21,7 @@ interface Room {
|
||||
export function generateWorld(floor: number, runState: RunState): { world: World; playerId: EntityId } {
|
||||
const width = GAME_CONFIG.map.width;
|
||||
const height = GAME_CONFIG.map.height;
|
||||
const tiles: Tile[] = new Array(width * height).fill(GAME_CONFIG.terrain.wall);
|
||||
const tiles: Tile[] = new Array(width * height).fill(TileType.WALL);
|
||||
|
||||
const random = seededRandom(floor * 12345);
|
||||
|
||||
@@ -103,7 +104,7 @@ function generateRooms(width: number, height: number, tiles: Tile[], floor: numb
|
||||
dungeon.create((x: number, y: number, value: number) => {
|
||||
if (value === 0) {
|
||||
// 0 = floor, 1 = wall
|
||||
tiles[y * width + x] = GAME_CONFIG.terrain.empty;
|
||||
tiles[y * width + x] = TileType.EMPTY;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -148,7 +149,7 @@ function extractRoomsFromCave(width: number, height: number, tiles: Tile[]): Roo
|
||||
for (let y = 1; y < height - 1; y++) {
|
||||
for (let x = 1; x < width - 1; x++) {
|
||||
const idx = y * width + x;
|
||||
if (tiles[idx] === GAME_CONFIG.terrain.empty && !visited.has(idx)) {
|
||||
if (tiles[idx] === TileType.EMPTY && !visited.has(idx)) {
|
||||
const cluster = floodFill(width, height, tiles, x, y, visited);
|
||||
|
||||
// Only consider clusters larger than 20 tiles
|
||||
@@ -206,7 +207,7 @@ function floodFill(width: number, height: number, tiles: Tile[], startX: number,
|
||||
for (const { nx, ny } of neighbors) {
|
||||
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
||||
const nIdx = ny * width + nx;
|
||||
if (tiles[nIdx] === GAME_CONFIG.terrain.empty && !visited.has(nIdx)) {
|
||||
if (tiles[nIdx] === TileType.EMPTY && !visited.has(nIdx)) {
|
||||
queue.push(nIdx);
|
||||
}
|
||||
}
|
||||
@@ -220,53 +221,53 @@ function decorate(width: number, height: number, tiles: Tile[], random: () => nu
|
||||
const world = { width, height };
|
||||
|
||||
// Set exit tile
|
||||
tiles[idx(world as any, exit.x, exit.y)] = GAME_CONFIG.terrain.exit;
|
||||
tiles[idx(world as any, exit.x, exit.y)] = TileType.EXIT;
|
||||
|
||||
// Use Simplex noise for natural-looking water distribution
|
||||
const waterNoise = new ROT.Noise.Simplex();
|
||||
// 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 water vs decorations
|
||||
const waterOffset = random() * 1000;
|
||||
// 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] === GAME_CONFIG.terrain.empty) {
|
||||
// Water lakes: use noise to create organic shapes
|
||||
const waterValue = waterNoise.get((x + waterOffset) / 15, (y + waterOffset) / 15);
|
||||
if (tiles[i] === TileType.EMPTY) {
|
||||
// Grass patches: use noise to create organic shapes
|
||||
const grassValue = grassNoise.get((x + grassOffset) / 15, (y + grassOffset) / 15);
|
||||
|
||||
// Create water patches where noise is above threshold
|
||||
if (waterValue > 0.35) {
|
||||
tiles[i] = GAME_CONFIG.terrain.water;
|
||||
// Create grass patches where noise is above threshold
|
||||
if (grassValue > 0.35) {
|
||||
tiles[i] = TileType.GRASS;
|
||||
} else {
|
||||
// Floor decorations (moss/grass): clustered distribution
|
||||
// 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] = GAME_CONFIG.terrain.emptyDeco;
|
||||
tiles[i] = TileType.EMPTY_DECO;
|
||||
} else if (decoValue > 0.3 && random() < 0.3) {
|
||||
// Sparse decorations at medium noise levels
|
||||
tiles[i] = GAME_CONFIG.terrain.emptyDeco;
|
||||
tiles[i] = TileType.EMPTY_DECO;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wall decorations (algae near water)
|
||||
// Wall decorations (moss near grass)
|
||||
for (let y = 0; y < height - 1; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = idx(world as any, x, y);
|
||||
const nextY = idx(world as any, x, y + 1);
|
||||
|
||||
if (tiles[i] === GAME_CONFIG.terrain.wall &&
|
||||
tiles[nextY] === GAME_CONFIG.terrain.water &&
|
||||
if (tiles[i] === TileType.WALL &&
|
||||
tiles[nextY] === TileType.GRASS &&
|
||||
random() < 0.25) {
|
||||
tiles[i] = GAME_CONFIG.terrain.wallDeco;
|
||||
tiles[i] = TileType.WALL_DECO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { World, EntityId } from "../../core/types";
|
||||
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||
|
||||
import { isBlocking, isDestructible, getDestructionResult } from "../../core/terrain";
|
||||
import { type EntityManager } from "../EntityManager";
|
||||
|
||||
|
||||
export function inBounds(w: World, x: number, y: number): boolean {
|
||||
return x >= 0 && y >= 0 && x < w.width && y < w.height;
|
||||
}
|
||||
@@ -12,13 +12,34 @@ export function idx(w: World, x: number, y: number): number {
|
||||
}
|
||||
|
||||
export function isWall(w: World, x: number, y: number): boolean {
|
||||
// Alias for isBlocking for backward compatibility
|
||||
return isBlockingTile(w, x, y);
|
||||
}
|
||||
|
||||
export function isBlockingTile(w: World, x: number, y: number): boolean {
|
||||
const tile = w.tiles[idx(w, x, y)];
|
||||
return tile === GAME_CONFIG.terrain.wall || tile === GAME_CONFIG.terrain.wallDeco;
|
||||
return isBlocking(tile);
|
||||
}
|
||||
|
||||
export function tryDestructTile(w: World, x: number, y: number): boolean {
|
||||
if (!inBounds(w, x, y)) return false;
|
||||
|
||||
const i = idx(w, x, y);
|
||||
const tile = w.tiles[i];
|
||||
|
||||
if (isDestructible(tile)) {
|
||||
const nextTile = getDestructionResult(tile);
|
||||
if (nextTile !== undefined) {
|
||||
w.tiles[i] = nextTile;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isBlocked(w: World, x: number, y: number, em?: EntityManager): boolean {
|
||||
if (!inBounds(w, x, y)) return true;
|
||||
if (isWall(w, x, y)) return true;
|
||||
if (isBlockingTile(w, x, y)) return true;
|
||||
|
||||
if (em) {
|
||||
return em.isOccupied(x, y, "exp_orb");
|
||||
|
||||
Reference in New Issue
Block a user