Fixed coprse bug
This commit is contained in:
@@ -22,9 +22,9 @@ describe('World Generator', () => {
|
||||
|
||||
const { world } = generateWorld(1, runState);
|
||||
|
||||
expect(world.width).toBe(60);
|
||||
expect(world.height).toBe(40);
|
||||
expect(world.tiles.length).toBe(60 * 40);
|
||||
expect(world.width).toBe(120);
|
||||
expect(world.height).toBe(80);
|
||||
expect(world.tiles.length).toBe(120 * 80);
|
||||
});
|
||||
|
||||
it('should place player actor', () => {
|
||||
@@ -252,8 +252,8 @@ describe('World Generator', () => {
|
||||
const { world } = generateWorld(10, runState);
|
||||
|
||||
// Basic validity checks
|
||||
expect(world.width).toBe(60);
|
||||
expect(world.height).toBe(40);
|
||||
expect(world.width).toBe(120);
|
||||
expect(world.height).toBe(80);
|
||||
expect(world.tiles.some(t => t === TileType.EMPTY)).toBe(true);
|
||||
expect(world.tiles.some(t => t === TileType.WALL)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -602,4 +602,28 @@ describe('Combat Simulation', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Death Cleanup", () => {
|
||||
it("should remove combatants with 0 HP during turn processing", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 }, speed: 10, stats: createTestStats(), energy: 0 } as any;
|
||||
// Enemy with 0 HP (e.g. killed by status effect prior to turn)
|
||||
const enemy = {
|
||||
id: 2, category: "combatant", isPlayer: false, pos: { x: 5, y: 5 }, speed: 100,
|
||||
stats: createTestStats({ hp: 0 }), energy: 0, type: "rat"
|
||||
} as any;
|
||||
|
||||
actors.set(1 as EntityId, player);
|
||||
actors.set(2 as EntityId, enemy);
|
||||
const world = createTestWorld();
|
||||
syncToECS(actors);
|
||||
const accessor = new EntityAccessor(world, 1 as EntityId, ecsWorld);
|
||||
|
||||
// This should trigger checkDeaths
|
||||
const result = stepUntilPlayerTurn(world, 1 as EntityId, accessor);
|
||||
|
||||
expect(accessor.hasActor(2 as EntityId)).toBe(false);
|
||||
expect(result.events.some(e => e.type === "killed" && e.targetId === 2)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,6 @@ export const Prefabs = {
|
||||
return EntityBuilder.create(world)
|
||||
.withPosition(x, y)
|
||||
.withName("Rat")
|
||||
.withSprite("rat", 0)
|
||||
.asEnemy("rat")
|
||||
.withStats({
|
||||
maxHp: config.baseHp + floorBonus,
|
||||
@@ -41,7 +40,6 @@ export const Prefabs = {
|
||||
return EntityBuilder.create(world)
|
||||
.withPosition(x, y)
|
||||
.withName("Bat")
|
||||
.withSprite("bat", 0)
|
||||
.asEnemy("bat")
|
||||
.withStats({
|
||||
maxHp: config.baseHp + floorBonus,
|
||||
@@ -217,7 +215,6 @@ export const Prefabs = {
|
||||
return EntityBuilder.create(world)
|
||||
.withPosition(x, y)
|
||||
.withName("Player")
|
||||
.withSprite("PriestessSouth", 0)
|
||||
.asPlayer()
|
||||
.withStats(config.initialStats)
|
||||
.withEnergy(config.speed)
|
||||
|
||||
@@ -48,7 +48,8 @@ export interface TriggerComponent {
|
||||
onExit?: boolean; // Trigger when entity leaves this tile
|
||||
onInteract?: boolean; // Trigger when entity interacts with this
|
||||
oneShot?: boolean; // Destroy/disable after triggering once
|
||||
triggered?: boolean; // Has already triggered (for oneShot triggers)
|
||||
triggered?: boolean; // Is currently triggered/active
|
||||
spent?: boolean; // Has already triggered (for oneShot triggers)
|
||||
targetId?: EntityId; // Target entity for this trigger (e.g., mine cart for a switch)
|
||||
damage?: number; // Damage to deal on trigger (for traps)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export class TriggerSystem extends System {
|
||||
const triggerPos = world.getComponent(triggerId, "position");
|
||||
|
||||
if (!trigger || !triggerPos) continue;
|
||||
if (trigger.triggered && trigger.oneShot) continue; // Already triggered one-shot
|
||||
if (trigger.spent && trigger.oneShot) continue; // Already spent one-shot
|
||||
|
||||
// Check for entities at this trigger's position
|
||||
for (const entityId of allWithPosition) {
|
||||
@@ -50,11 +50,11 @@ export class TriggerSystem extends System {
|
||||
const wasOnTrigger = this.wasEntityOnTrigger(entityId, triggerPos);
|
||||
|
||||
// Handle enter or manual trigger
|
||||
if ((trigger.onEnter && isOnTrigger && !wasOnTrigger) || (trigger.triggered && !trigger.oneShot)) {
|
||||
if ((trigger.onEnter && isOnTrigger && !wasOnTrigger) || trigger.triggered) {
|
||||
this.activateTrigger(triggerId, entityId, trigger, world);
|
||||
|
||||
// If it was manually triggered, we should probably reset the flag if its not oneShot
|
||||
if (trigger.triggered && !trigger.oneShot) {
|
||||
// If it was manually triggered, we should reset the flag
|
||||
if (trigger.triggered) {
|
||||
trigger.triggered = false;
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ export class TriggerSystem extends System {
|
||||
triggered?: boolean;
|
||||
targetId?: EntityId;
|
||||
onInteract?: boolean;
|
||||
|
||||
spent?: boolean;
|
||||
},
|
||||
world: ECSWorld
|
||||
): void {
|
||||
@@ -147,7 +147,8 @@ export class TriggerSystem extends System {
|
||||
|
||||
// Mark as triggered for one-shot triggers and update sprite
|
||||
if (trigger.oneShot) {
|
||||
trigger.triggered = true;
|
||||
trigger.spent = true;
|
||||
trigger.triggered = false;
|
||||
|
||||
// Change sprite to triggered appearance if it's a dungeon sprite
|
||||
const sprite = world.getComponent(triggerId, "sprite");
|
||||
|
||||
@@ -29,6 +29,8 @@ export function applyAction(w: World, actorId: EntityId, action: Action, accesso
|
||||
break;
|
||||
}
|
||||
|
||||
checkDeaths(events, accessor);
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
@@ -192,10 +194,18 @@ function handleAttack(_w: World, actor: Actor, action: { targetId: EntityId }, a
|
||||
});
|
||||
|
||||
if (target.stats.hp <= 0) {
|
||||
killActor(target, events, accessor, actor.id);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
return [{ type: "waited", actorId: actor.id }];
|
||||
}
|
||||
|
||||
export function killActor(target: CombatantActor, events: SimEvent[], accessor: EntityAccessor, killerId?: EntityId): void {
|
||||
events.push({
|
||||
type: "killed",
|
||||
targetId: target.id,
|
||||
killerId: actor.id,
|
||||
killerId: killerId ?? (0 as EntityId),
|
||||
x: target.pos.x,
|
||||
y: target.pos.y,
|
||||
victimType: target.type as ActorType
|
||||
@@ -214,20 +224,23 @@ function handleAttack(_w: World, actor: Actor, action: { targetId: EntityId }, a
|
||||
for (const fireId of firesAtPos) {
|
||||
ecsWorld.destroyEntity(fireId);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn EXP Orb
|
||||
const enemyDef = (GAME_CONFIG.enemies as any)[target.type || ""];
|
||||
const expAmount = enemyDef?.expValue || 0;
|
||||
|
||||
if (ecsWorld) {
|
||||
const orbId = Prefabs.expOrb(ecsWorld, target.pos.x, target.pos.y, expAmount);
|
||||
events.push({ type: "orb-spawned", orbId, x: target.pos.x, y: target.pos.y });
|
||||
}
|
||||
}
|
||||
return events;
|
||||
|
||||
export function checkDeaths(events: SimEvent[], accessor: EntityAccessor): void {
|
||||
const combatants = accessor.getCombatants();
|
||||
for (const c of combatants) {
|
||||
if (c.stats.hp <= 0) {
|
||||
killActor(c, events, accessor);
|
||||
}
|
||||
}
|
||||
return [{ type: "waited", actorId: actor.id }];
|
||||
}
|
||||
|
||||
|
||||
@@ -308,6 +321,7 @@ export function stepUntilPlayerTurn(w: World, playerId: EntityId, accessor: Enti
|
||||
}
|
||||
|
||||
events.push(...applyAction(w, actor.id, decision.action, accessor));
|
||||
checkDeaths(events, accessor);
|
||||
|
||||
if (!accessor.isPlayerAlive()) {
|
||||
return { awaitingPlayerId: null as any, events };
|
||||
|
||||
@@ -140,10 +140,13 @@ export class DungeonRenderer {
|
||||
console.log(`[DungeonRenderer] Creating ECS sprites...`);
|
||||
const spriteEntities = this.ecsWorld.getEntitiesWith("position", "sprite");
|
||||
for (const entId of spriteEntities) {
|
||||
// Skip player as it's handled separately
|
||||
// Skip combatants as they are handled separately (player and enemies)
|
||||
const player = this.ecsWorld.getComponent(entId, "player");
|
||||
if (player) continue;
|
||||
|
||||
const actorType = this.ecsWorld.getComponent(entId, "actorType");
|
||||
if (actorType) continue;
|
||||
|
||||
const pos = this.ecsWorld.getComponent(entId, "position");
|
||||
const spriteData = this.ecsWorld.getComponent(entId, "sprite");
|
||||
if (pos && spriteData) {
|
||||
@@ -282,7 +285,13 @@ export class DungeonRenderer {
|
||||
const pos = this.ecsWorld.getComponent(trapId, "position");
|
||||
const spriteData = this.ecsWorld.getComponent(trapId, "sprite");
|
||||
|
||||
if (pos && spriteData) {
|
||||
// Handle missing components (entity destroyed)
|
||||
if (!pos || !spriteData) {
|
||||
sprite.destroy();
|
||||
this.trapSprites.delete(trapId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bounds check
|
||||
if (pos.x < 0 || pos.x >= this.world.width || pos.y < 0 || pos.y >= this.world.height) {
|
||||
sprite.setVisible(false);
|
||||
@@ -358,7 +367,6 @@ export class DungeonRenderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actors (Combatants)
|
||||
const activeEnemyIds = new Set<EntityId>();
|
||||
@@ -538,7 +546,19 @@ export class DungeonRenderer {
|
||||
this.fxRenderer.showHeal(x, y, amount);
|
||||
}
|
||||
|
||||
spawnCorpse(x: number, y: number, type: ActorType) {
|
||||
spawnCorpse(x: number, y: number, type: ActorType, targetId?: EntityId) {
|
||||
if (targetId !== undefined) {
|
||||
if (targetId === this.entityAccessor.playerId) {
|
||||
if (this.playerSprite) {
|
||||
this.playerSprite.setVisible(false);
|
||||
}
|
||||
} else {
|
||||
const sprite = this.enemySprites.get(targetId);
|
||||
if (sprite) {
|
||||
sprite.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.fxRenderer.spawnCorpse(x, y, type);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@ vi.mock('phaser', () => {
|
||||
play: vi.fn().mockReturnThis(),
|
||||
setPosition: vi.fn().mockReturnThis(),
|
||||
setVisible: vi.fn().mockReturnThis(),
|
||||
setDisplaySize: vi.fn().mockReturnThis(),
|
||||
destroy: vi.fn(),
|
||||
frame: { name: '0' },
|
||||
setFrame: vi.fn(),
|
||||
setAlpha: vi.fn(),
|
||||
setAngle: vi.fn(),
|
||||
clearTint: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -89,10 +91,12 @@ describe('DungeonRenderer', () => {
|
||||
play: vi.fn().mockReturnThis(),
|
||||
setPosition: vi.fn().mockReturnThis(),
|
||||
setVisible: vi.fn().mockReturnThis(),
|
||||
setDisplaySize: vi.fn().mockReturnThis(),
|
||||
destroy: vi.fn(),
|
||||
frame: { name: '0' },
|
||||
setFrame: vi.fn(),
|
||||
setAlpha: vi.fn(),
|
||||
setAngle: vi.fn(),
|
||||
clearTint: vi.fn(),
|
||||
})),
|
||||
circle: vi.fn().mockReturnValue({
|
||||
@@ -134,6 +138,14 @@ describe('DungeonRenderer', () => {
|
||||
setDepth: vi.fn(),
|
||||
forEachTile: vi.fn(),
|
||||
}),
|
||||
createBlankLayer: vi.fn().mockReturnValue({
|
||||
setDepth: vi.fn().mockReturnThis(),
|
||||
forEachTile: vi.fn().mockReturnThis(),
|
||||
putTileAt: vi.fn(),
|
||||
setScale: vi.fn().mockReturnThis(),
|
||||
setScrollFactor: vi.fn().mockReturnThis(),
|
||||
setVisible: vi.fn().mockReturnThis(),
|
||||
}),
|
||||
destroy: vi.fn(),
|
||||
}),
|
||||
},
|
||||
@@ -247,4 +259,43 @@ describe('DungeonRenderer', () => {
|
||||
// Should NOT tween because it's the first spawn
|
||||
expect(mockScene.tweens.add).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should hide the original sprite when spawnCorpse is called with targetId', () => {
|
||||
renderer.initializeFloor(mockWorld, ecsWorld, accessor);
|
||||
|
||||
// Add a rat
|
||||
const enemyId = 100 as EntityId;
|
||||
ecsWorld.addComponent(enemyId, "position", { x: 3, y: 1 });
|
||||
ecsWorld.addComponent(enemyId, "actorType", { type: "rat" });
|
||||
ecsWorld.addComponent(enemyId, "stats", { hp: 10, maxHp: 10 } as any);
|
||||
|
||||
(renderer as any).fovManager.visibleArray[1 * mockWorld.width + 3] = 1;
|
||||
renderer.render([]);
|
||||
|
||||
// Verify sprite was created and is visible
|
||||
const sprite = (renderer as any).enemySprites.get(enemyId);
|
||||
expect(sprite).toBeDefined();
|
||||
expect(sprite.setVisible).toHaveBeenCalledWith(true);
|
||||
|
||||
// Call spawnCorpse with targetId
|
||||
renderer.spawnCorpse(3, 1, 'rat', enemyId);
|
||||
|
||||
// Verify original sprite was hidden
|
||||
expect(sprite.setVisible).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should hide the player sprite when spawnCorpse is called with playerId', () => {
|
||||
renderer.initializeFloor(mockWorld, ecsWorld, accessor);
|
||||
|
||||
// Verify player sprite was created and is visible
|
||||
const playerSprite = (renderer as any).playerSprite;
|
||||
expect(playerSprite).toBeDefined();
|
||||
playerSprite.setVisible(true); // Force visible for test
|
||||
|
||||
// Call spawnCorpse with playerId
|
||||
renderer.spawnCorpse(1, 1, 'player', accessor.playerId);
|
||||
|
||||
// Verify player sprite was hidden
|
||||
expect(playerSprite.setVisible).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ vi.mock('phaser', () => {
|
||||
setAlpha: vi.fn().mockReturnThis(),
|
||||
setTint: vi.fn().mockReturnThis(),
|
||||
clearTint: vi.fn().mockReturnThis(),
|
||||
setDisplaySize: vi.fn().mockReturnThis(),
|
||||
destroy: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -42,6 +43,7 @@ describe('FxRenderer', () => {
|
||||
setAlpha: vi.fn().mockReturnThis(),
|
||||
setTint: vi.fn().mockReturnThis(),
|
||||
clearTint: vi.fn().mockReturnThis(),
|
||||
setDisplaySize: vi.fn().mockReturnThis(),
|
||||
destroy: vi.fn(),
|
||||
})),
|
||||
text: vi.fn(() => ({
|
||||
|
||||
@@ -30,8 +30,8 @@ export class GameRenderer implements EventRenderCallbacks {
|
||||
this.dungeonRenderer.showHeal(x, y, amount);
|
||||
}
|
||||
|
||||
spawnCorpse(x: number, y: number, type: ActorType): void {
|
||||
this.dungeonRenderer.spawnCorpse(x, y, type);
|
||||
spawnCorpse(x: number, y: number, type: ActorType, targetId?: EntityId): void {
|
||||
this.dungeonRenderer.spawnCorpse(x, y, type, targetId);
|
||||
}
|
||||
|
||||
showWait(x: number, y: number): void {
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface EventRenderCallbacks {
|
||||
showDamage(x: number, y: number, amount: number, isCrit?: boolean, isBlock?: boolean): void;
|
||||
showDodge(x: number, y: number): void;
|
||||
showHeal(x: number, y: number, amount: number): void;
|
||||
spawnCorpse(x: number, y: number, type: ActorType): void;
|
||||
spawnCorpse(x: number, y: number, type: ActorType, targetId?: EntityId): void;
|
||||
showWait(x: number, y: number): void;
|
||||
spawnOrb(orbId: EntityId, x: number, y: number): void;
|
||||
collectOrb(actorId: EntityId, amount: number, x: number, y: number): void;
|
||||
@@ -50,7 +50,7 @@ export function renderSimEvents(
|
||||
break;
|
||||
|
||||
case "killed":
|
||||
callbacks.spawnCorpse(ev.x, ev.y, ev.victimType || "rat");
|
||||
callbacks.spawnCorpse(ev.x, ev.y, ev.victimType || "rat", ev.targetId);
|
||||
break;
|
||||
|
||||
case "waited":
|
||||
|
||||
@@ -97,11 +97,19 @@ describe('TargetingSystem', () => {
|
||||
const enemyPos = { x: 3, y: 3 };
|
||||
(getClosestVisibleEnemy as any).mockReturnValue(enemyPos);
|
||||
|
||||
const mockAccessor = {
|
||||
getCombatant: vi.fn().mockReturnValue({
|
||||
pos: playerPos,
|
||||
inventory: { items: [{ id: 'item-1' }] }
|
||||
}),
|
||||
context: {}
|
||||
};
|
||||
|
||||
targetingSystem.startTargeting(
|
||||
'item-1',
|
||||
playerPos,
|
||||
mockWorld,
|
||||
{} as any, // accessor
|
||||
mockAccessor as any,
|
||||
1 as EntityId, // playerId
|
||||
new Uint8Array(100),
|
||||
10
|
||||
@@ -118,11 +126,19 @@ describe('TargetingSystem', () => {
|
||||
const mousePos = { x: 5, y: 5 };
|
||||
(getClosestVisibleEnemy as any).mockReturnValue(null);
|
||||
|
||||
const mockAccessor = {
|
||||
getCombatant: vi.fn().mockReturnValue({
|
||||
pos: playerPos,
|
||||
inventory: { items: [{ id: 'item-1' }] }
|
||||
}),
|
||||
context: {}
|
||||
};
|
||||
|
||||
targetingSystem.startTargeting(
|
||||
'item-1',
|
||||
playerPos,
|
||||
mockWorld,
|
||||
{} as any, // accessor
|
||||
mockAccessor as any,
|
||||
1 as EntityId, // playerId
|
||||
new Uint8Array(100),
|
||||
10,
|
||||
@@ -143,12 +159,20 @@ describe('TargetingSystem', () => {
|
||||
path: []
|
||||
});
|
||||
|
||||
const mockAccessor = {
|
||||
getCombatant: vi.fn().mockReturnValue({
|
||||
pos: playerPos,
|
||||
inventory: { items: [{ id: 'item-1' }] }
|
||||
}),
|
||||
context: {}
|
||||
};
|
||||
|
||||
// Start targeting
|
||||
targetingSystem.startTargeting(
|
||||
'item-1',
|
||||
playerPos,
|
||||
mockWorld,
|
||||
{} as any, // accessor
|
||||
mockAccessor as any,
|
||||
1 as EntityId,
|
||||
new Uint8Array(100),
|
||||
10,
|
||||
|
||||
Reference in New Issue
Block a user