Half changes to switch to exit level, Ran out of credits, re added enemies
This commit is contained in:
BIN
public/assets/sprites/items/track_switch.png
Normal file
BIN
public/assets/sprites/items/track_switch.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 896 B |
@@ -73,8 +73,8 @@ export const GAME_CONFIG = {
|
||||
},
|
||||
|
||||
enemyScaling: {
|
||||
baseCount: 0,
|
||||
baseCountPerFloor: 0,
|
||||
baseCount: 15,
|
||||
baseCountPerFloor: 5,
|
||||
hpPerFloor: 5,
|
||||
attackPerTwoFloors: 1,
|
||||
expMultiplier: 1.2
|
||||
@@ -190,7 +190,8 @@ export const GAME_CONFIG = {
|
||||
{ key: "mine_cart", path: "assets/sprites/items/mine_cart.png" },
|
||||
{ key: "track_straight", path: "assets/sprites/items/track_straight.png" },
|
||||
{ key: "track_corner", path: "assets/sprites/items/track_corner.png" },
|
||||
{ key: "track_vertical", path: "assets/sprites/items/track_vertical.png" }
|
||||
{ key: "track_vertical", path: "assets/sprites/items/track_vertical.png" },
|
||||
{ key: "track_switch", path: "assets/sprites/items/track_switch.png" }
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ function createMockWorld(): World {
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,21 +94,21 @@ describe("EntityAccessor", () => {
|
||||
function syncActor(actor: Actor) {
|
||||
ecsWorld.addComponent(actor.id, "position", actor.pos);
|
||||
ecsWorld.addComponent(actor.id, "name", { name: actor.id.toString() });
|
||||
|
||||
|
||||
if (actor.category === "combatant") {
|
||||
const c = actor as CombatantActor;
|
||||
ecsWorld.addComponent(actor.id, "stats", c.stats);
|
||||
ecsWorld.addComponent(actor.id, "energy", { current: c.energy, speed: c.speed });
|
||||
ecsWorld.addComponent(actor.id, "actorType", { type: c.type });
|
||||
if (c.isPlayer) {
|
||||
ecsWorld.addComponent(actor.id, "player", {});
|
||||
} else {
|
||||
ecsWorld.addComponent(actor.id, "ai", { state: "wandering" });
|
||||
}
|
||||
const c = actor as CombatantActor;
|
||||
ecsWorld.addComponent(actor.id, "stats", c.stats);
|
||||
ecsWorld.addComponent(actor.id, "energy", { current: c.energy, speed: c.speed });
|
||||
ecsWorld.addComponent(actor.id, "actorType", { type: c.type });
|
||||
if (c.isPlayer) {
|
||||
ecsWorld.addComponent(actor.id, "player", {});
|
||||
} else {
|
||||
ecsWorld.addComponent(actor.id, "ai", { state: "wandering" });
|
||||
}
|
||||
} else if (actor.category === "collectible") {
|
||||
ecsWorld.addComponent(actor.id, "collectible", { type: "exp_orb", amount: (actor as CollectibleActor).expAmount });
|
||||
ecsWorld.addComponent(actor.id, "collectible", { type: "exp_orb", amount: (actor as CollectibleActor).expAmount });
|
||||
} else if (actor.category === "item_drop") {
|
||||
ecsWorld.addComponent(actor.id, "groundItem", { item: (actor as ItemDropActor).item });
|
||||
ecsWorld.addComponent(actor.id, "groundItem", { item: (actor as ItemDropActor).item });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +130,7 @@ describe("EntityAccessor", () => {
|
||||
|
||||
const pos = accessor.getPlayerPos();
|
||||
expect(pos).toEqual({ x: 3, y: 4 });
|
||||
|
||||
|
||||
// Verify it's a copy
|
||||
if (pos) {
|
||||
pos.x = 99;
|
||||
@@ -253,11 +254,11 @@ describe("EntityAccessor", () => {
|
||||
describe("updateWorld", () => {
|
||||
it("updates references correctly", () => {
|
||||
syncActor(createPlayer(PLAYER_ID, 1, 1));
|
||||
|
||||
|
||||
const newWorld = createMockWorld();
|
||||
const newEcsWorld = new ECSWorld();
|
||||
const newPlayerId = 10;
|
||||
|
||||
|
||||
const newPlayer = createPlayer(newPlayerId, 8, 8);
|
||||
// Manually add to newEcsWorld
|
||||
newEcsWorld.addComponent(newPlayer.id, "position", newPlayer.pos);
|
||||
@@ -266,7 +267,7 @@ describe("EntityAccessor", () => {
|
||||
newEcsWorld.addComponent(newPlayer.id, "player", {});
|
||||
|
||||
accessor.updateWorld(newWorld, newPlayerId as EntityId, newEcsWorld);
|
||||
|
||||
|
||||
const player = accessor.getPlayer();
|
||||
expect(player?.id).toBe(newPlayerId);
|
||||
expect(player?.pos).toEqual({ x: 8, y: 8 });
|
||||
|
||||
@@ -10,10 +10,11 @@ const createTestWorld = (): World => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(TileType.EMPTY),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
const createTestStats = (overrides: Partial<any> = {}) => ({
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: [],
|
||||
@@ -43,7 +44,7 @@ describe('AI Behavior & Scheduling', () => {
|
||||
if (c.isPlayer) {
|
||||
ecsWorld.addComponent(actor.id, "player", {});
|
||||
} else {
|
||||
ecsWorld.addComponent(actor.id, "ai", {
|
||||
ecsWorld.addComponent(actor.id, "ai", {
|
||||
state: c.aiState || "wandering",
|
||||
alertedAt: c.alertedAt,
|
||||
lastKnownPlayerPos: c.lastKnownPlayerPos
|
||||
@@ -61,33 +62,33 @@ describe('AI Behavior & Scheduling', () => {
|
||||
it("should allow slower actors to act eventually", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
// Player Speed 100
|
||||
const player = {
|
||||
id: 1 as EntityId, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 },
|
||||
speed: 100, stats: createTestStats(), energy: 0
|
||||
const player = {
|
||||
id: 1 as EntityId, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 },
|
||||
speed: 100, stats: createTestStats(), energy: 0
|
||||
} as any;
|
||||
|
||||
|
||||
// Rat Speed 80 (Slow)
|
||||
const rat = {
|
||||
id: 2 as EntityId, category: "combatant", isPlayer: false, pos: { x: 9, y: 9 },
|
||||
speed: 80, stats: createTestStats(), aiState: "wandering", energy: 0
|
||||
const rat = {
|
||||
id: 2 as EntityId, category: "combatant", isPlayer: false, pos: { x: 9, y: 9 },
|
||||
speed: 80, stats: createTestStats(), aiState: "wandering", energy: 0
|
||||
} as any;
|
||||
|
||||
|
||||
actors.set(1 as EntityId, player);
|
||||
actors.set(2 as EntityId, rat);
|
||||
const world = createTestWorld();
|
||||
syncToECS(actors);
|
||||
accessor = new EntityAccessor(world, 1 as EntityId, ecsWorld);
|
||||
|
||||
|
||||
let ratMoves = 0;
|
||||
|
||||
|
||||
// Simulate 20 player turns
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const result = stepUntilPlayerTurn(world, 1 as EntityId, accessor);
|
||||
const enemyActs = result.events.filter(e =>
|
||||
(e.type === "moved" || e.type === "waited" || e.type === "enemy-alerted") &&
|
||||
const enemyActs = result.events.filter(e =>
|
||||
(e.type === "moved" || e.type === "waited" || e.type === "enemy-alerted") &&
|
||||
((e as any).actorId === 2 || (e as any).enemyId === 2)
|
||||
);
|
||||
|
||||
|
||||
if (enemyActs.length > 0) ratMoves++;
|
||||
}
|
||||
expect(ratMoves).toBeGreaterThan(0);
|
||||
@@ -107,20 +108,20 @@ describe('AI Behavior & Scheduling', () => {
|
||||
terrainTypes.forEach(({ type, name }) => {
|
||||
it(`should see player when standing on ${name}`, () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
actors.set(1 as EntityId, { id: 1 as EntityId, category: "combatant", isPlayer: true, pos: { x: 5, y: 0 }, stats: createTestStats(), energy: 0 } as any);
|
||||
actors.set(2 as EntityId, {
|
||||
id: 2 as EntityId, category: "combatant", isPlayer: false, pos: { x: 0, y: 0 },
|
||||
stats: createTestStats(), aiState: "wandering", energy: 0
|
||||
} as any);
|
||||
|
||||
actors.set(1 as EntityId, { id: 1 as EntityId, category: "combatant", isPlayer: true, pos: { x: 5, y: 0 }, stats: createTestStats(), energy: 0 } as any);
|
||||
actors.set(2 as EntityId, {
|
||||
id: 2 as EntityId, category: "combatant", isPlayer: false, pos: { x: 0, y: 0 },
|
||||
stats: createTestStats(), aiState: "wandering", energy: 0
|
||||
} as any);
|
||||
|
||||
const world = createTestWorld();
|
||||
world.tiles[0] = type;
|
||||
syncToECS(actors);
|
||||
|
||||
|
||||
const testAccessor = new EntityAccessor(world, 1 as EntityId, ecsWorld);
|
||||
// Rat at 0,0. Player at 5,0.
|
||||
decideEnemyAction(world, testAccessor.getCombatant(2 as EntityId) as any, testAccessor.getCombatant(1 as EntityId) as any, testAccessor);
|
||||
|
||||
|
||||
const updatedRat = testAccessor.getCombatant(2 as EntityId);
|
||||
expect(updatedRat?.aiState).toBe("alerted");
|
||||
});
|
||||
@@ -132,56 +133,56 @@ describe('AI Behavior & Scheduling', () => {
|
||||
// -------------------------------------------------------------------------
|
||||
describe('AI Aggression State Machine', () => {
|
||||
it('should become pursuing when damaged by player, even if not sighting player', () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
// Player far away/invisible (simulated logic)
|
||||
const player = { id: 1 as EntityId, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 }, stats: createTestStats({ attack: 1, accuracy: 100 }), energy: 0 } as any;
|
||||
const enemy = {
|
||||
id: 2 as EntityId, category: "combatant", isPlayer: false, pos: { x: 0, y: 5 },
|
||||
stats: createTestStats({ hp: 10, defense: 0, evasion: 0 }), aiState: "wandering", energy: 0
|
||||
} as any;
|
||||
|
||||
actors.set(1 as EntityId, player);
|
||||
actors.set(2 as EntityId, enemy);
|
||||
const world = createTestWorld();
|
||||
syncToECS(actors);
|
||||
|
||||
const testAccessor = new EntityAccessor(world, 1 as EntityId, ecsWorld);
|
||||
applyAction(world, 1 as EntityId, { type: "attack", targetId: 2 as EntityId }, testAccessor);
|
||||
|
||||
const updatedEnemy = testAccessor.getCombatant(2 as EntityId);
|
||||
expect(updatedEnemy?.aiState).toBe("pursuing");
|
||||
expect(updatedEnemy?.lastKnownPlayerPos).toEqual(player.pos);
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
// Player far away/invisible (simulated logic)
|
||||
const player = { id: 1 as EntityId, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 }, stats: createTestStats({ attack: 1, accuracy: 100 }), energy: 0 } as any;
|
||||
const enemy = {
|
||||
id: 2 as EntityId, category: "combatant", isPlayer: false, pos: { x: 0, y: 5 },
|
||||
stats: createTestStats({ hp: 10, defense: 0, evasion: 0 }), aiState: "wandering", energy: 0
|
||||
} as any;
|
||||
|
||||
actors.set(1 as EntityId, player);
|
||||
actors.set(2 as EntityId, enemy);
|
||||
const world = createTestWorld();
|
||||
syncToECS(actors);
|
||||
|
||||
const testAccessor = new EntityAccessor(world, 1 as EntityId, ecsWorld);
|
||||
applyAction(world, 1 as EntityId, { type: "attack", targetId: 2 as EntityId }, testAccessor);
|
||||
|
||||
const updatedEnemy = testAccessor.getCombatant(2 as EntityId);
|
||||
expect(updatedEnemy?.aiState).toBe("pursuing");
|
||||
expect(updatedEnemy?.lastKnownPlayerPos).toEqual(player.pos);
|
||||
});
|
||||
|
||||
it("should transition from alerted to pursuing after delay even if sight is blocked", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
const player = { id: 1 as EntityId, category: "combatant", isPlayer: true, pos: { x: 9, y: 9 }, stats: createTestStats(), energy: 0 } as any;
|
||||
const enemy = {
|
||||
id: 2 as EntityId,
|
||||
category: "combatant",
|
||||
isPlayer: false,
|
||||
pos: { x: 0, y: 0 },
|
||||
const enemy = {
|
||||
id: 2 as EntityId,
|
||||
category: "combatant",
|
||||
isPlayer: false,
|
||||
pos: { x: 0, y: 0 },
|
||||
stats: createTestStats(),
|
||||
aiState: "alerted",
|
||||
alertedAt: Date.now() - 2000, // Alerted 2 seconds ago
|
||||
lastKnownPlayerPos: { x: 9, y: 9 }, // Known position
|
||||
energy: 0
|
||||
} as any;
|
||||
|
||||
|
||||
actors.set(1 as EntityId, player);
|
||||
actors.set(2 as EntityId, enemy);
|
||||
const world = createTestWorld();
|
||||
|
||||
|
||||
// Player is far away and potentially blocked
|
||||
world.tiles[1] = TileType.WALL; // x=1, y=0 blocked
|
||||
syncToECS(actors);
|
||||
|
||||
|
||||
const testAccessor = new EntityAccessor(world, 1 as EntityId, ecsWorld);
|
||||
const rat = testAccessor.getCombatant(2 as EntityId)!;
|
||||
decideEnemyAction(world, rat, testAccessor.getPlayer()!, testAccessor);
|
||||
|
||||
|
||||
// alerted -> pursuing (due to time) -> searching (due to no sight)
|
||||
expect(rat.aiState).toBe("searching");
|
||||
expect(rat.aiState).toBe("searching");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,11 +7,11 @@ import { ECSWorld } from "../ecs/World";
|
||||
|
||||
describe("CombatLogic - getClosestVisibleEnemy", () => {
|
||||
let ecsWorld: ECSWorld;
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
ecsWorld = new ECSWorld();
|
||||
});
|
||||
|
||||
|
||||
// Helper to create valid default stats for testing
|
||||
const createMockStats = () => ({
|
||||
hp: 10, maxHp: 10, attack: 1, defense: 0,
|
||||
@@ -28,7 +28,8 @@ describe("CombatLogic - getClosestVisibleEnemy", () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
@@ -70,7 +71,8 @@ describe("CombatLogic - getClosestVisibleEnemy", () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
@@ -123,7 +125,8 @@ describe("CombatLogic - getClosestVisibleEnemy", () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
|
||||
@@ -11,15 +11,16 @@ describe('Pathfinding', () => {
|
||||
width,
|
||||
height,
|
||||
tiles: new Array(width * height).fill(tileType),
|
||||
exit: { x: 0, y: 0 }
|
||||
exit: { x: 0, y: 0 },
|
||||
trackPath: []
|
||||
});
|
||||
|
||||
it('should find a path between two reachable points', () => {
|
||||
const world = createTestWorld(10, 10);
|
||||
const seen = new Uint8Array(100).fill(1);
|
||||
|
||||
|
||||
const path = findPathAStar(world, seen, { x: 0, y: 0 }, { x: 0, y: 3 });
|
||||
|
||||
|
||||
expect(path.length).toBe(4); // 0,0 -> 0,1 -> 0,2 -> 0,3
|
||||
expect(path[0]).toEqual({ x: 0, y: 0 });
|
||||
expect(path[3]).toEqual({ x: 0, y: 3 });
|
||||
@@ -29,36 +30,36 @@ describe('Pathfinding', () => {
|
||||
const world = createTestWorld(10, 10);
|
||||
world.tiles[30] = TileType.WALL; // Wall at 0,3
|
||||
const seen = new Uint8Array(100).fill(1);
|
||||
|
||||
|
||||
const path = findPathAStar(world, seen, { x: 0, y: 0 }, { x: 0, y: 3 });
|
||||
|
||||
|
||||
expect(path).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array if no path exists', () => {
|
||||
const world = createTestWorld(10, 10);
|
||||
// Create a wall blockage
|
||||
for(let x=0; x<10; x++) world.tiles[10 + x] = TileType.WALL;
|
||||
|
||||
for (let x = 0; x < 10; x++) world.tiles[10 + x] = TileType.WALL;
|
||||
|
||||
const seen = new Uint8Array(100).fill(1);
|
||||
|
||||
|
||||
const path = findPathAStar(world, seen, { x: 0, y: 0 }, { x: 0, y: 5 });
|
||||
|
||||
|
||||
expect(path).toEqual([]);
|
||||
});
|
||||
|
||||
it('should respect ignoreBlockedTarget option', () => {
|
||||
const world = createTestWorld(10, 10);
|
||||
const ecsWorld = new ECSWorld();
|
||||
|
||||
|
||||
// Place an actor at target
|
||||
ecsWorld.addComponent(1 as EntityId, "position", { x: 0, y: 3 });
|
||||
ecsWorld.addComponent(1 as EntityId, "actorType", { type: "rat" });
|
||||
ecsWorld.addComponent(1 as EntityId, "stats", { hp: 10 } as any);
|
||||
|
||||
|
||||
const seen = new Uint8Array(100).fill(1);
|
||||
const accessor = new EntityAccessor(world, 0 as EntityId, ecsWorld);
|
||||
|
||||
|
||||
// With accessor, it should be blocked
|
||||
const pathBlocked = findPathAStar(world, seen, { x: 0, y: 0 }, { x: 0, y: 3 }, { accessor });
|
||||
expect(pathBlocked).toEqual([]);
|
||||
@@ -72,11 +73,11 @@ describe('Pathfinding', () => {
|
||||
it('should respect ignoreSeen option', () => {
|
||||
const world = createTestWorld(10, 10);
|
||||
const seen = new Uint8Array(100).fill(0); // Nothing seen
|
||||
|
||||
|
||||
// Without ignoreSeen, should fail because target/path is unseen
|
||||
const pathUnseen = findPathAStar(world, seen, { x: 0, y: 0 }, { x: 0, y: 3 });
|
||||
expect(pathUnseen).toEqual([]);
|
||||
|
||||
|
||||
// With ignoreSeen, should succeed
|
||||
const pathSeenIgnored = findPathAStar(world, seen, { x: 0, y: 0 }, { x: 0, y: 3 }, { ignoreSeen: true });
|
||||
expect(pathSeenIgnored.length).toBe(4);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,8 @@ const createTestWorld = (): World => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0), // 0 = Floor
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { idx, inBounds, isWall, isBlocked, tryDestructTile, isPlayerOnExit } from '../world/world-logic';
|
||||
import { idx, inBounds, isWall, isBlocked, tryDestructTile } from '../world/world-logic';
|
||||
import { type World, type Tile } from '../../core/types';
|
||||
import { TileType } from '../../core/terrain';
|
||||
|
||||
@@ -9,13 +9,14 @@ describe('World Utilities', () => {
|
||||
width,
|
||||
height,
|
||||
tiles,
|
||||
exit: { x: 0, y: 0 }
|
||||
exit: { x: 0, y: 0 },
|
||||
trackPath: []
|
||||
});
|
||||
|
||||
describe('idx', () => {
|
||||
it('should calculate correct index for 2D coordinates', () => {
|
||||
const world = createTestWorld(10, 10, []);
|
||||
|
||||
|
||||
expect(idx(world, 0, 0)).toBe(0);
|
||||
expect(idx(world, 5, 0)).toBe(5);
|
||||
expect(idx(world, 0, 1)).toBe(10);
|
||||
@@ -26,7 +27,7 @@ describe('World Utilities', () => {
|
||||
describe('inBounds', () => {
|
||||
it('should return true for coordinates within bounds', () => {
|
||||
const world = createTestWorld(10, 10, []);
|
||||
|
||||
|
||||
expect(inBounds(world, 0, 0)).toBe(true);
|
||||
expect(inBounds(world, 5, 5)).toBe(true);
|
||||
expect(inBounds(world, 9, 9)).toBe(true);
|
||||
@@ -34,7 +35,7 @@ describe('World Utilities', () => {
|
||||
|
||||
it('should return false for coordinates outside bounds', () => {
|
||||
const world = createTestWorld(10, 10, []);
|
||||
|
||||
|
||||
expect(inBounds(world, -1, 0)).toBe(false);
|
||||
expect(inBounds(world, 0, -1)).toBe(false);
|
||||
expect(inBounds(world, 10, 0)).toBe(false);
|
||||
@@ -49,9 +50,9 @@ describe('World Utilities', () => {
|
||||
tiles[0] = TileType.WALL; // wall at 0,0
|
||||
tiles[55] = TileType.WALL; // wall at 5,5
|
||||
|
||||
|
||||
|
||||
const world = createTestWorld(10, 10, tiles);
|
||||
|
||||
|
||||
expect(isWall(world, 0, 0)).toBe(true);
|
||||
expect(isWall(world, 5, 5)).toBe(true);
|
||||
});
|
||||
@@ -59,7 +60,7 @@ describe('World Utilities', () => {
|
||||
it('should return false for floor tiles', () => {
|
||||
const tiles: Tile[] = new Array(100).fill(TileType.EMPTY);
|
||||
const world = createTestWorld(10, 10, tiles);
|
||||
|
||||
|
||||
expect(isWall(world, 3, 3)).toBe(false);
|
||||
expect(isWall(world, 7, 7)).toBe(false);
|
||||
|
||||
@@ -67,7 +68,7 @@ describe('World Utilities', () => {
|
||||
|
||||
it('should return false for out of bounds coordinates', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(0));
|
||||
|
||||
|
||||
expect(isWall(world, -1, 0)).toBe(false);
|
||||
expect(isWall(world, 10, 10)).toBe(false);
|
||||
});
|
||||
@@ -78,7 +79,7 @@ describe('World Utilities', () => {
|
||||
const tiles: Tile[] = new Array(100).fill(TileType.EMPTY);
|
||||
tiles[55] = TileType.WALL; // wall at 5,5
|
||||
|
||||
|
||||
|
||||
const world = createTestWorld(10, 10, tiles);
|
||||
const mockAccessor = { getActorsAt: () => [] } as any;
|
||||
|
||||
@@ -88,19 +89,19 @@ describe('World Utilities', () => {
|
||||
it('should return true for actor positions', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(0));
|
||||
const mockAccessor = {
|
||||
getActorsAt: (x: number, y: number) => {
|
||||
if (x === 3 && y === 3) return [{ category: "combatant" }];
|
||||
return [];
|
||||
}
|
||||
getActorsAt: (x: number, y: number) => {
|
||||
if (x === 3 && y === 3) return [{ category: "combatant" }];
|
||||
return [];
|
||||
}
|
||||
} as any;
|
||||
|
||||
|
||||
expect(isBlocked(world, 3, 3, mockAccessor)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for empty floor tiles', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(0));
|
||||
const mockAccessor = { getActorsAt: () => [] } as any;
|
||||
|
||||
|
||||
expect(isBlocked(world, 3, 3, mockAccessor)).toBe(false);
|
||||
expect(isBlocked(world, 7, 7, mockAccessor)).toBe(false);
|
||||
});
|
||||
@@ -108,7 +109,7 @@ describe('World Utilities', () => {
|
||||
it('should return true for out of bounds', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(0));
|
||||
const mockAccessor = { getActorsAt: () => [] } as any;
|
||||
|
||||
|
||||
expect(isBlocked(world, -1, 0, mockAccessor)).toBe(true);
|
||||
expect(isBlocked(world, 10, 10, mockAccessor)).toBe(true);
|
||||
});
|
||||
@@ -120,7 +121,7 @@ describe('World Utilities', () => {
|
||||
const world = createTestWorld(10, 10, tiles);
|
||||
|
||||
const result = tryDestructTile(world, 0, 0);
|
||||
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(world.tiles[0]).toBe(TileType.GRASS_SAPLINGS);
|
||||
});
|
||||
@@ -131,49 +132,14 @@ describe('World Utilities', () => {
|
||||
const world = createTestWorld(10, 10, tiles);
|
||||
|
||||
const result = tryDestructTile(world, 0, 0);
|
||||
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(world.tiles[0]).toBe(TileType.WALL);
|
||||
});
|
||||
|
||||
|
||||
it('should return false for out of bounds', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(TileType.EMPTY));
|
||||
expect(tryDestructTile(world, -1, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPlayerOnExit', () => {
|
||||
it('should return true when player is on exit', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(TileType.EMPTY));
|
||||
world.exit = { x: 5, y: 5 };
|
||||
|
||||
const mockAccessor = {
|
||||
getPlayer: () => ({ pos: { x: 5, y: 5 } })
|
||||
} as any;
|
||||
|
||||
expect(isPlayerOnExit(world, mockAccessor)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when player is not on exit', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(TileType.EMPTY));
|
||||
world.exit = { x: 5, y: 5 };
|
||||
|
||||
const mockAccessor = {
|
||||
getPlayer: () => ({ pos: { x: 4, y: 4 } })
|
||||
} as any;
|
||||
|
||||
expect(isPlayerOnExit(world, mockAccessor)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when player does not exist', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(TileType.EMPTY));
|
||||
world.exit = { x: 5, y: 5 };
|
||||
|
||||
const mockAccessor = {
|
||||
getPlayer: () => null
|
||||
} as any;
|
||||
|
||||
expect(isPlayerOnExit(world, mockAccessor)).toBe(false);
|
||||
expect(tryDestructTile(world, -1, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,7 +174,7 @@ export class EntityBuilder {
|
||||
effectDuration?: number;
|
||||
}): this {
|
||||
this.components.trigger = {
|
||||
onEnter: options.onEnter ?? true,
|
||||
onEnter: options.onEnter ?? false,
|
||||
onExit: options.onExit,
|
||||
onInteract: options.onInteract,
|
||||
oneShot: options.oneShot,
|
||||
|
||||
@@ -243,9 +243,11 @@ export const Prefabs = {
|
||||
return EntityBuilder.create(world)
|
||||
.withPosition(x, y)
|
||||
.withName("Track Switch")
|
||||
.withSprite("dungeon", 31) // TileType.SWITCH_OFF
|
||||
.withSprite("track_switch", 0)
|
||||
.asTrigger({
|
||||
onEnter: false,
|
||||
onInteract: true,
|
||||
oneShot: true,
|
||||
targetId: cartId
|
||||
})
|
||||
.build();
|
||||
|
||||
@@ -12,7 +12,8 @@ describe('ECS Removal and Accessor', () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 0, y: 0 }
|
||||
exit: { x: 0, y: 0 },
|
||||
trackPath: []
|
||||
};
|
||||
const accessor = new EntityAccessor(gameWorld, 999 as EntityId, ecsWorld);
|
||||
|
||||
|
||||
@@ -105,9 +105,9 @@ export class TriggerSystem extends System {
|
||||
if (mineCart) {
|
||||
mineCart.isMoving = true;
|
||||
|
||||
// Change switch sprite to "on" (using dungeon sprite 32)
|
||||
// Change switch sprite if applicable (optional for now as we only have one frame)
|
||||
const sprite = world.getComponent(triggerId, "sprite");
|
||||
if (sprite) {
|
||||
if (sprite && sprite.texture === "dungeon") {
|
||||
sprite.index = 32;
|
||||
}
|
||||
}
|
||||
@@ -149,9 +149,9 @@ export class TriggerSystem extends System {
|
||||
if (trigger.oneShot) {
|
||||
trigger.triggered = true;
|
||||
|
||||
// Change sprite to triggered appearance (dungeon sprite 23)
|
||||
// Change sprite to triggered appearance if it's a dungeon sprite
|
||||
const sprite = world.getComponent(triggerId, "sprite");
|
||||
if (sprite) {
|
||||
if (sprite && sprite.texture === "dungeon") {
|
||||
sprite.index = 23; // Triggered/spent trap appearance
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@ describe('CombatLogic', () => {
|
||||
const setWall = (x: number, y: number) => {
|
||||
mockWorld.tiles[y * mockWorld.width + x] = TileType.WALL;
|
||||
};
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
mockWorld = {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(TileType.EMPTY),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
ecsWorld = new ECSWorld();
|
||||
// Shooter ID 1
|
||||
@@ -44,12 +45,12 @@ describe('CombatLogic', () => {
|
||||
it('should travel full path if no obstacles', () => {
|
||||
const start = { x: 0, y: 0 };
|
||||
const end = { x: 5, y: 0 };
|
||||
|
||||
|
||||
const result = traceProjectile(mockWorld, start, end, accessor);
|
||||
|
||||
|
||||
expect(result.blockedPos).toEqual(end);
|
||||
expect(result.hitActorId).toBeUndefined();
|
||||
expect(result.path).toHaveLength(6);
|
||||
expect(result.path).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('should stop at wall', () => {
|
||||
@@ -58,7 +59,7 @@ describe('CombatLogic', () => {
|
||||
setWall(3, 0); // Wall at (3,0)
|
||||
|
||||
const result = traceProjectile(mockWorld, start, end, accessor);
|
||||
|
||||
|
||||
expect(result.blockedPos).toEqual({ x: 2, y: 0 });
|
||||
expect(result.hitActorId).toBeUndefined();
|
||||
});
|
||||
@@ -66,7 +67,7 @@ describe('CombatLogic', () => {
|
||||
it('should stop at enemy', () => {
|
||||
const start = { x: 0, y: 0 };
|
||||
const end = { x: 5, y: 0 };
|
||||
|
||||
|
||||
// Place enemy at (3,0)
|
||||
const enemyId = 2 as EntityId;
|
||||
const enemy = {
|
||||
@@ -79,36 +80,36 @@ describe('CombatLogic', () => {
|
||||
syncActor(enemy);
|
||||
|
||||
const result = traceProjectile(mockWorld, start, end, accessor, 1 as EntityId); // Shooter 1
|
||||
|
||||
|
||||
expect(result.blockedPos).toEqual({ x: 3, y: 0 });
|
||||
expect(result.hitActorId).toBe(enemyId);
|
||||
});
|
||||
|
||||
it('should ignore shooter position', () => {
|
||||
const start = { x: 0, y: 0 };
|
||||
const end = { x: 5, y: 0 };
|
||||
|
||||
// Shooter at start
|
||||
const shooter = {
|
||||
id: 1 as EntityId,
|
||||
type: 'player',
|
||||
category: 'combatant',
|
||||
pos: { x: 0, y: 0 },
|
||||
isPlayer: true
|
||||
};
|
||||
syncActor(shooter);
|
||||
const start = { x: 0, y: 0 };
|
||||
const end = { x: 5, y: 0 };
|
||||
|
||||
const result = traceProjectile(mockWorld, start, end, accessor, 1 as EntityId);
|
||||
|
||||
// Should not hit self
|
||||
expect(result.hitActorId).toBeUndefined();
|
||||
expect(result.blockedPos).toEqual(end);
|
||||
// Shooter at start
|
||||
const shooter = {
|
||||
id: 1 as EntityId,
|
||||
type: 'player',
|
||||
category: 'combatant',
|
||||
pos: { x: 0, y: 0 },
|
||||
isPlayer: true
|
||||
};
|
||||
syncActor(shooter);
|
||||
|
||||
const result = traceProjectile(mockWorld, start, end, accessor, 1 as EntityId);
|
||||
|
||||
// Should not hit self
|
||||
expect(result.hitActorId).toBeUndefined();
|
||||
expect(result.blockedPos).toEqual(end);
|
||||
});
|
||||
|
||||
it('should ignore non-combatant actors (e.g. items)', () => {
|
||||
const start = { x: 0, y: 0 };
|
||||
const end = { x: 5, y: 0 };
|
||||
|
||||
|
||||
// Item at (3,0)
|
||||
const item = {
|
||||
id: 99 as EntityId,
|
||||
@@ -119,10 +120,10 @@ describe('CombatLogic', () => {
|
||||
syncActor(item);
|
||||
|
||||
const result = traceProjectile(mockWorld, start, end, accessor);
|
||||
|
||||
|
||||
// Should pass through item
|
||||
expect(result.blockedPos).toEqual(end);
|
||||
expect(result.hitActorId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,12 +18,13 @@ describe("Fireable Weapons & Ammo System", () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
ecsWorld = new ECSWorld();
|
||||
accessor = new EntityAccessor(world, 1 as EntityId, ecsWorld);
|
||||
itemManager = new ItemManager(world, accessor, ecsWorld);
|
||||
|
||||
|
||||
player = {
|
||||
id: 1 as EntityId,
|
||||
pos: { x: 0, y: 0 },
|
||||
@@ -53,14 +54,14 @@ describe("Fireable Weapons & Ammo System", () => {
|
||||
ecsWorld.addComponent(player.id, "actorType", { type: "player" });
|
||||
ecsWorld.addComponent(player.id, "inventory", player.inventory!);
|
||||
ecsWorld.addComponent(player.id, "energy", { current: 0, speed: 100 });
|
||||
|
||||
|
||||
// Avoid ID collisions between manually added player (ID 1) and spawned entities
|
||||
ecsWorld.setNextId(10);
|
||||
});
|
||||
|
||||
it("should stack ammo correctly", () => {
|
||||
const playerActor = accessor.getPlayer()!;
|
||||
|
||||
|
||||
// Spawn Ammo pack 1
|
||||
const ammo1 = createAmmo("ammo_9mm", 10);
|
||||
itemManager.spawnItem(ammo1, { x: 0, y: 0 });
|
||||
@@ -85,7 +86,7 @@ describe("Fireable Weapons & Ammo System", () => {
|
||||
// Create pistol using factory (already has currentAmmo initialized)
|
||||
const pistol = createRangedWeapon("pistol");
|
||||
playerActor.inventory!.items.push(pistol);
|
||||
|
||||
|
||||
// Sanity Check - currentAmmo is now top-level
|
||||
expect(pistol.currentAmmo).toBe(6);
|
||||
expect(pistol.stats.magazineSize).toBe(6);
|
||||
@@ -110,7 +111,7 @@ describe("Fireable Weapons & Ammo System", () => {
|
||||
// Logic mimic from GameScene
|
||||
const needed = pistol.stats.magazineSize - pistol.currentAmmo; // 6
|
||||
const toTake = Math.min(needed, ammo.quantity!); // 6
|
||||
|
||||
|
||||
pistol.currentAmmo += toTake;
|
||||
ammo.quantity! -= toTake;
|
||||
|
||||
@@ -121,7 +122,7 @@ describe("Fireable Weapons & Ammo System", () => {
|
||||
it("should handle partial reload if not enough ammo", () => {
|
||||
const playerActor = accessor.getPlayer()!;
|
||||
const pistol = createRangedWeapon("pistol");
|
||||
pistol.currentAmmo = 0;
|
||||
pistol.currentAmmo = 0;
|
||||
playerActor.inventory!.items.push(pistol);
|
||||
|
||||
const ammo = createAmmo("ammo_9mm", 3); // Only 3 bullets
|
||||
@@ -130,32 +131,32 @@ describe("Fireable Weapons & Ammo System", () => {
|
||||
// Logic mimic
|
||||
const needed = pistol.stats.magazineSize - pistol.currentAmmo; // 6
|
||||
const toTake = Math.min(needed, ammo.quantity!); // 3
|
||||
|
||||
|
||||
pistol.currentAmmo += toTake;
|
||||
ammo.quantity! -= toTake;
|
||||
|
||||
expect(pistol.currentAmmo).toBe(3);
|
||||
expect(ammo.quantity).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
it("should deep clone on spawn so pistols remain independent", () => {
|
||||
const playerActor = accessor.getPlayer()!;
|
||||
const pistol1 = createRangedWeapon("pistol");
|
||||
|
||||
// Spawn 1
|
||||
itemManager.spawnItem(pistol1, {x:0, y:0});
|
||||
const picked1 = itemManager.tryPickup(playerActor)! as RangedWeaponItem;
|
||||
|
||||
// Spawn 2
|
||||
const pistol2 = createRangedWeapon("pistol");
|
||||
itemManager.spawnItem(pistol2, {x:0, y:0});
|
||||
const picked2 = itemManager.tryPickup(playerActor)! as RangedWeaponItem;
|
||||
|
||||
expect(picked1).not.toBe(picked2);
|
||||
expect(picked1.stats).not.toBe(picked2.stats); // Critical!
|
||||
|
||||
// Modifying one should not affect other
|
||||
picked1.currentAmmo = 0;
|
||||
expect(picked2.currentAmmo).toBe(6);
|
||||
const playerActor = accessor.getPlayer()!;
|
||||
const pistol1 = createRangedWeapon("pistol");
|
||||
|
||||
// Spawn 1
|
||||
itemManager.spawnItem(pistol1, { x: 0, y: 0 });
|
||||
const picked1 = itemManager.tryPickup(playerActor)! as RangedWeaponItem;
|
||||
|
||||
// Spawn 2
|
||||
const pistol2 = createRangedWeapon("pistol");
|
||||
itemManager.spawnItem(pistol2, { x: 0, y: 0 });
|
||||
const picked2 = itemManager.tryPickup(playerActor)! as RangedWeaponItem;
|
||||
|
||||
expect(picked1).not.toBe(picked2);
|
||||
expect(picked1.stats).not.toBe(picked2.stats); // Critical!
|
||||
|
||||
// Modifying one should not affect other
|
||||
picked1.currentAmmo = 0;
|
||||
expect(picked2.currentAmmo).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,11 +19,12 @@ describe('Movement Blocking Behavior', () => {
|
||||
width: 3,
|
||||
height: 3,
|
||||
tiles: new Array(9).fill(TileType.GRASS),
|
||||
exit: { x: 2, y: 2 }
|
||||
exit: { x: 2, y: 2 },
|
||||
trackPath: []
|
||||
};
|
||||
|
||||
|
||||
// Blocking wall at (1, 0)
|
||||
world.tiles[1] = TileType.WALL;
|
||||
world.tiles[1] = TileType.WALL;
|
||||
|
||||
player = {
|
||||
id: 1 as EntityId,
|
||||
@@ -35,7 +36,7 @@ describe('Movement Blocking Behavior', () => {
|
||||
energy: 0,
|
||||
stats: { ...GAME_CONFIG.player.initialStats }
|
||||
};
|
||||
|
||||
|
||||
ecsWorld = new ECSWorld();
|
||||
ecsWorld.addComponent(player.id, "position", player.pos);
|
||||
ecsWorld.addComponent(player.id, "stats", player.stats);
|
||||
@@ -49,7 +50,7 @@ describe('Movement Blocking Behavior', () => {
|
||||
it('should return move-blocked event when moving into a wall', () => {
|
||||
const action: Action = { type: 'move', dx: 1, dy: 0 }; // Try to move right into wall at (1,0)
|
||||
const events = applyAction(world, player.id, action, accessor);
|
||||
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]).toMatchObject({
|
||||
type: 'move-blocked',
|
||||
@@ -62,7 +63,7 @@ describe('Movement Blocking Behavior', () => {
|
||||
it('should return moved event when moving into empty space', () => {
|
||||
const action: Action = { type: 'move', dx: 0, dy: 1 }; // Move down to (0,1) - valid
|
||||
const events = applyAction(world, player.id, action, accessor);
|
||||
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]).toMatchObject({
|
||||
type: 'moved',
|
||||
|
||||
@@ -99,8 +99,25 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
|
||||
const exit = { ...trackPath[trackPath.length - 1] };
|
||||
|
||||
// Place Switch at the end of the track
|
||||
Prefabs.trackSwitch(ecsWorld, exit.x, exit.y, cartId);
|
||||
// Place Switch adjacent to the end of the track
|
||||
let switchPos = { x: exit.x, y: exit.y };
|
||||
const neighbors = [
|
||||
{ x: exit.x + 1, y: exit.y },
|
||||
{ x: exit.x - 1, y: exit.y },
|
||||
{ x: exit.x, y: exit.y + 1 },
|
||||
{ x: exit.x, y: exit.y - 1 },
|
||||
];
|
||||
for (const n of neighbors) {
|
||||
if (n.x >= 1 && n.x < width - 1 && n.y >= 1 && n.y < height - 1) {
|
||||
const t = tiles[n.y * width + n.x];
|
||||
if (t === TileType.EMPTY || t === TileType.EMPTY_DECO || t === TileType.GRASS || t === TileType.TRACK) {
|
||||
switchPos = n;
|
||||
// Don't break if it's track, try to find a real empty spot first
|
||||
if (t !== TileType.TRACK) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Prefabs.trackSwitch(ecsWorld, switchPos.x, switchPos.y, cartId);
|
||||
|
||||
// Mark all track and room tiles as occupied for objects
|
||||
const occupiedPositions = new Set<string>();
|
||||
@@ -366,7 +383,7 @@ function placeEnemies(
|
||||
const room = rooms[roomIdx];
|
||||
|
||||
// Try to find an empty spot in the room
|
||||
for (let attempts = 0; attempts < 5; attempts++) {
|
||||
for (let attempts = 0; attempts < 20; attempts++) {
|
||||
|
||||
const ex = room.x + 1 + Math.floor(random() * (room.width - 2));
|
||||
const ey = room.y + 1 + Math.floor(random() * (room.height - 2));
|
||||
@@ -389,6 +406,9 @@ function placeEnemies(
|
||||
EntityBuilder.create(ecsWorld)
|
||||
.asEnemy(type)
|
||||
.withPosition(ex, ey)
|
||||
.withSprite(type, 0)
|
||||
.withName(type.charAt(0).toUpperCase() + type.slice(1))
|
||||
.withCombat()
|
||||
.withStats({
|
||||
maxHp: scaledHp + Math.floor(random() * 4),
|
||||
hp: scaledHp + Math.floor(random() * 4),
|
||||
@@ -396,7 +416,6 @@ function placeEnemies(
|
||||
defense: enemyDef.baseDefense,
|
||||
})
|
||||
.withEnergy(speed) // Configured speed
|
||||
// Note: Other stats like crit/evasion are defaults from EntityBuilder or BaseStats
|
||||
.build();
|
||||
|
||||
occupiedPositions.add(k);
|
||||
|
||||
@@ -43,7 +43,20 @@ export function isBlocked(w: World, x: number, y: number, accessor: EntityAccess
|
||||
|
||||
if (!accessor) return false;
|
||||
const actors = accessor.getActorsAt(x, y);
|
||||
return actors.some(a => a.category === "combatant");
|
||||
if (actors.some(a => a.category === "combatant")) return true;
|
||||
|
||||
// Check for interactable entities (switches, etc.) that should block movement
|
||||
if (accessor.context) {
|
||||
const ecs = accessor.context;
|
||||
const isInteractable = ecs.getEntitiesWith("position", "trigger").some(id => {
|
||||
const p = ecs.getComponent(id, "position");
|
||||
const t = ecs.getComponent(id, "trigger");
|
||||
return p?.x === x && p?.y === y && t?.onInteract;
|
||||
});
|
||||
if (isInteractable) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ export class DungeonRenderer {
|
||||
const spriteData = this.ecsWorld.getComponent(entId, "sprite");
|
||||
if (pos && spriteData) {
|
||||
try {
|
||||
const isStandalone = spriteData.texture === "mine_cart" || spriteData.texture === "ceramic_dragon_head";
|
||||
const isStandalone = spriteData.texture === "mine_cart" || spriteData.texture === "ceramic_dragon_head" || spriteData.texture === "track_switch";
|
||||
const sprite = this.scene.add.sprite(
|
||||
pos.x * TILE_SIZE + TILE_SIZE / 2,
|
||||
pos.y * TILE_SIZE + TILE_SIZE / 2,
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('DungeonRenderer', () => {
|
||||
killTweensOf: vi.fn(),
|
||||
},
|
||||
time: {
|
||||
now: 0
|
||||
now: 0
|
||||
}
|
||||
};
|
||||
|
||||
@@ -152,6 +152,7 @@ describe('DungeonRenderer', () => {
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
ecsWorld = new ECSWorld();
|
||||
accessor = new EntityAccessor(mockWorld, 1 as EntityId, ecsWorld);
|
||||
@@ -186,7 +187,7 @@ describe('DungeonRenderer', () => {
|
||||
|
||||
it('should render exp_orb correctly', () => {
|
||||
renderer.initializeFloor(mockWorld, ecsWorld, accessor);
|
||||
|
||||
|
||||
// Add an exp_orb to the ECS world
|
||||
ecsWorld.addComponent(2 as EntityId, "position", { x: 2, y: 1 });
|
||||
ecsWorld.addComponent(2 as EntityId, "collectible", { type: "exp_orb", amount: 10 });
|
||||
@@ -206,7 +207,7 @@ describe('DungeonRenderer', () => {
|
||||
|
||||
it('should render any enemy type as a sprite', () => {
|
||||
renderer.initializeFloor(mockWorld, ecsWorld, accessor);
|
||||
|
||||
|
||||
// Add a rat
|
||||
ecsWorld.addComponent(3 as EntityId, "position", { x: 3, y: 1 });
|
||||
ecsWorld.addComponent(3 as EntityId, "actorType", { type: "rat" });
|
||||
@@ -224,7 +225,7 @@ describe('DungeonRenderer', () => {
|
||||
|
||||
it('should initialize new enemy sprites at target position and not tween them', () => {
|
||||
renderer.initializeFloor(mockWorld, ecsWorld, accessor);
|
||||
|
||||
|
||||
// Position 5,5 -> 5*16 + 8 = 88
|
||||
const TILE_SIZE = 16;
|
||||
const targetX = 5 * TILE_SIZE + TILE_SIZE / 2;
|
||||
@@ -242,7 +243,7 @@ describe('DungeonRenderer', () => {
|
||||
|
||||
// Check spawn position
|
||||
expect(mockScene.add.sprite).toHaveBeenCalledWith(targetX, targetY, 'rat', 0);
|
||||
|
||||
|
||||
// Should NOT tween because it's the first spawn
|
||||
expect(mockScene.tweens.add).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -13,7 +13,8 @@ describe('ItemManager', () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(1), // Floor
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
|
||||
entityAccessor = {
|
||||
|
||||
Reference in New Issue
Block a user