Fixed coprse bug

This commit is contained in:
2026-02-07 12:54:25 +11:00
parent 4b50e341a7
commit 02f850da35
12 changed files with 326 additions and 192 deletions

View File

@@ -22,9 +22,9 @@ describe('World Generator', () => {
const { world } = generateWorld(1, runState); const { world } = generateWorld(1, runState);
expect(world.width).toBe(60); expect(world.width).toBe(120);
expect(world.height).toBe(40); expect(world.height).toBe(80);
expect(world.tiles.length).toBe(60 * 40); expect(world.tiles.length).toBe(120 * 80);
}); });
it('should place player actor', () => { it('should place player actor', () => {
@@ -252,8 +252,8 @@ describe('World Generator', () => {
const { world } = generateWorld(10, runState); const { world } = generateWorld(10, runState);
// Basic validity checks // Basic validity checks
expect(world.width).toBe(60); expect(world.width).toBe(120);
expect(world.height).toBe(40); expect(world.height).toBe(80);
expect(world.tiles.some(t => t === TileType.EMPTY)).toBe(true); expect(world.tiles.some(t => t === TileType.EMPTY)).toBe(true);
expect(world.tiles.some(t => t === TileType.WALL)).toBe(true); expect(world.tiles.some(t => t === TileType.WALL)).toBe(true);
}); });
@@ -307,20 +307,20 @@ describe('World Generator', () => {
}); });
it('should verify safe spawn logic on caves', () => { it('should verify safe spawn logic on caves', () => {
const runState = { const runState = {
stats: { stats: {
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10, maxMana: 20, mana: 20, maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10, maxMana: 20, mana: 20,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0, critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
passiveNodes: [] passiveNodes: []
}, },
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] }
}; };
const { world, playerId, ecsWorld } = generateWorld(12, runState); const { world, playerId, ecsWorld } = generateWorld(12, runState);
const accessor = new EntityAccessor(world, playerId, ecsWorld); const accessor = new EntityAccessor(world, playerId, ecsWorld);
const player = accessor.getPlayer()!; const player = accessor.getPlayer()!;
expect(world.tiles[player.pos.y * world.width + player.pos.x]).toBe(TileType.EMPTY); expect(world.tiles[player.pos.y * world.width + player.pos.x]).toBe(TileType.EMPTY);
}); });
}); });
}); });

View File

@@ -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);
});
});
}); });

View File

@@ -21,7 +21,6 @@ export const Prefabs = {
return EntityBuilder.create(world) return EntityBuilder.create(world)
.withPosition(x, y) .withPosition(x, y)
.withName("Rat") .withName("Rat")
.withSprite("rat", 0)
.asEnemy("rat") .asEnemy("rat")
.withStats({ .withStats({
maxHp: config.baseHp + floorBonus, maxHp: config.baseHp + floorBonus,
@@ -41,7 +40,6 @@ export const Prefabs = {
return EntityBuilder.create(world) return EntityBuilder.create(world)
.withPosition(x, y) .withPosition(x, y)
.withName("Bat") .withName("Bat")
.withSprite("bat", 0)
.asEnemy("bat") .asEnemy("bat")
.withStats({ .withStats({
maxHp: config.baseHp + floorBonus, maxHp: config.baseHp + floorBonus,
@@ -217,7 +215,6 @@ export const Prefabs = {
return EntityBuilder.create(world) return EntityBuilder.create(world)
.withPosition(x, y) .withPosition(x, y)
.withName("Player") .withName("Player")
.withSprite("PriestessSouth", 0)
.asPlayer() .asPlayer()
.withStats(config.initialStats) .withStats(config.initialStats)
.withEnergy(config.speed) .withEnergy(config.speed)

View File

@@ -48,7 +48,8 @@ export interface TriggerComponent {
onExit?: boolean; // Trigger when entity leaves this tile onExit?: boolean; // Trigger when entity leaves this tile
onInteract?: boolean; // Trigger when entity interacts with this onInteract?: boolean; // Trigger when entity interacts with this
oneShot?: boolean; // Destroy/disable after triggering once 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) targetId?: EntityId; // Target entity for this trigger (e.g., mine cart for a switch)
damage?: number; // Damage to deal on trigger (for traps) damage?: number; // Damage to deal on trigger (for traps)

View File

@@ -37,7 +37,7 @@ export class TriggerSystem extends System {
const triggerPos = world.getComponent(triggerId, "position"); const triggerPos = world.getComponent(triggerId, "position");
if (!trigger || !triggerPos) continue; 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 // Check for entities at this trigger's position
for (const entityId of allWithPosition) { for (const entityId of allWithPosition) {
@@ -50,11 +50,11 @@ export class TriggerSystem extends System {
const wasOnTrigger = this.wasEntityOnTrigger(entityId, triggerPos); const wasOnTrigger = this.wasEntityOnTrigger(entityId, triggerPos);
// Handle enter or manual trigger // 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); this.activateTrigger(triggerId, entityId, trigger, world);
// If it was manually triggered, we should probably reset the flag if its not oneShot // If it was manually triggered, we should reset the flag
if (trigger.triggered && !trigger.oneShot) { if (trigger.triggered) {
trigger.triggered = false; trigger.triggered = false;
} }
} }
@@ -88,7 +88,7 @@ export class TriggerSystem extends System {
triggered?: boolean; triggered?: boolean;
targetId?: EntityId; targetId?: EntityId;
onInteract?: boolean; onInteract?: boolean;
spent?: boolean;
}, },
world: ECSWorld world: ECSWorld
): void { ): void {
@@ -147,7 +147,8 @@ export class TriggerSystem extends System {
// Mark as triggered for one-shot triggers and update sprite // Mark as triggered for one-shot triggers and update sprite
if (trigger.oneShot) { if (trigger.oneShot) {
trigger.triggered = true; trigger.spent = true;
trigger.triggered = false;
// Change sprite to triggered appearance if it's a dungeon sprite // Change sprite to triggered appearance if it's a dungeon sprite
const sprite = world.getComponent(triggerId, "sprite"); const sprite = world.getComponent(triggerId, "sprite");

View File

@@ -29,6 +29,8 @@ export function applyAction(w: World, actorId: EntityId, action: Action, accesso
break; break;
} }
checkDeaths(events, accessor);
return events; return events;
} }
@@ -192,44 +194,55 @@ function handleAttack(_w: World, actor: Actor, action: { targetId: EntityId }, a
}); });
if (target.stats.hp <= 0) { if (target.stats.hp <= 0) {
events.push({ killActor(target, events, accessor, actor.id);
type: "killed",
targetId: target.id,
killerId: actor.id,
x: target.pos.x,
y: target.pos.y,
victimType: target.type as ActorType
});
accessor.removeActor(target.id);
// Extinguish fire at the death position
const ecsWorld = accessor.context;
if (ecsWorld) {
const firesAtPos = ecsWorld.getEntitiesWith("position", "name").filter(id => {
const p = ecsWorld.getComponent(id, "position");
const n = ecsWorld.getComponent(id, "name");
return p?.x === target.pos.x && p?.y === target.pos.y && n?.name === "Fire";
});
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; return events;
} }
return [{ type: "waited", actorId: actor.id }]; 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: killerId ?? (0 as EntityId),
x: target.pos.x,
y: target.pos.y,
victimType: target.type as ActorType
});
accessor.removeActor(target.id);
// Extinguish fire at the death position
const ecsWorld = accessor.context;
if (ecsWorld) {
const firesAtPos = ecsWorld.getEntitiesWith("position", "name").filter(id => {
const p = ecsWorld.getComponent(id, "position");
const n = ecsWorld.getComponent(id, "name");
return p?.x === target.pos.x && p?.y === target.pos.y && n?.name === "Fire";
});
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;
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 });
}
}
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);
}
}
}
/** /**
@@ -308,6 +321,7 @@ export function stepUntilPlayerTurn(w: World, playerId: EntityId, accessor: Enti
} }
events.push(...applyAction(w, actor.id, decision.action, accessor)); events.push(...applyAction(w, actor.id, decision.action, accessor));
checkDeaths(events, accessor);
if (!accessor.isPlayerAlive()) { if (!accessor.isPlayerAlive()) {
return { awaitingPlayerId: null as any, events }; return { awaitingPlayerId: null as any, events };

View File

@@ -140,10 +140,13 @@ export class DungeonRenderer {
console.log(`[DungeonRenderer] Creating ECS sprites...`); console.log(`[DungeonRenderer] Creating ECS sprites...`);
const spriteEntities = this.ecsWorld.getEntitiesWith("position", "sprite"); const spriteEntities = this.ecsWorld.getEntitiesWith("position", "sprite");
for (const entId of spriteEntities) { 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"); const player = this.ecsWorld.getComponent(entId, "player");
if (player) continue; if (player) continue;
const actorType = this.ecsWorld.getComponent(entId, "actorType");
if (actorType) continue;
const pos = this.ecsWorld.getComponent(entId, "position"); const pos = this.ecsWorld.getComponent(entId, "position");
const spriteData = this.ecsWorld.getComponent(entId, "sprite"); const spriteData = this.ecsWorld.getComponent(entId, "sprite");
if (pos && spriteData) { if (pos && spriteData) {
@@ -282,79 +285,84 @@ export class DungeonRenderer {
const pos = this.ecsWorld.getComponent(trapId, "position"); const pos = this.ecsWorld.getComponent(trapId, "position");
const spriteData = this.ecsWorld.getComponent(trapId, "sprite"); const spriteData = this.ecsWorld.getComponent(trapId, "sprite");
if (pos && spriteData) { // Handle missing components (entity destroyed)
// Bounds check if (!pos || !spriteData) {
if (pos.x < 0 || pos.x >= this.world.width || pos.y < 0 || pos.y >= this.world.height) { sprite.destroy();
sprite.setVisible(false); this.trapSprites.delete(trapId);
continue; continue;
} }
const i = idx(this.world, pos.x, pos.y); // Bounds check
const isSeen = seen[i] === 1; if (pos.x < 0 || pos.x >= this.world.width || pos.y < 0 || pos.y >= this.world.height) {
const isVis = visible[i] === 1; sprite.setVisible(false);
continue;
sprite.setVisible(isSeen); }
// Update position (with simple smoothing) const i = idx(this.world, pos.x, pos.y);
const targetX = pos.x * TILE_SIZE + TILE_SIZE / 2; const isSeen = seen[i] === 1;
const targetY = pos.y * TILE_SIZE + TILE_SIZE / 2; const isVis = visible[i] === 1;
if (sprite.x !== targetX || sprite.y !== targetY) { sprite.setVisible(isSeen);
// Check if it's far away (teleport) or nearby (tween)
const dist = Phaser.Math.Distance.Between(sprite.x, sprite.y, targetX, targetY); // Update position (with simple smoothing)
if (dist > TILE_SIZE * 2) { const targetX = pos.x * TILE_SIZE + TILE_SIZE / 2;
this.scene.tweens.killTweensOf(sprite); const targetY = pos.y * TILE_SIZE + TILE_SIZE / 2;
sprite.setPosition(targetX, targetY);
} else if (!this.scene.tweens.isTweening(sprite)) { if (sprite.x !== targetX || sprite.y !== targetY) {
this.scene.tweens.add({ // Check if it's far away (teleport) or nearby (tween)
targets: sprite, const dist = Phaser.Math.Distance.Between(sprite.x, sprite.y, targetX, targetY);
x: targetX, if (dist > TILE_SIZE * 2) {
y: targetY, this.scene.tweens.killTweensOf(sprite);
duration: GAME_CONFIG.rendering.moveDuration, sprite.setPosition(targetX, targetY);
ease: 'Power1' } else if (!this.scene.tweens.isTweening(sprite)) {
}); this.scene.tweens.add({
} targets: sprite,
x: targetX,
y: targetY,
duration: GAME_CONFIG.rendering.moveDuration,
ease: 'Power1'
});
} }
}
// Update sprite frame in case trap was triggered // Update sprite frame in case trap was triggered
const isStandalone = spriteData.texture === "mine_cart" || spriteData.texture === "ceramic_dragon_head"; const isStandalone = spriteData.texture === "mine_cart" || spriteData.texture === "ceramic_dragon_head";
if (!isStandalone && sprite.frame.name !== String(spriteData.index)) { if (!isStandalone && sprite.frame.name !== String(spriteData.index)) {
sprite.setFrame(spriteData.index); sprite.setFrame(spriteData.index);
} }
// Dim if not currently visible // Dim if not currently visible
if (isSeen && !isVis) { if (isSeen && !isVis) {
sprite.setAlpha(0.4); sprite.setAlpha(0.4);
sprite.setTint(0x888888); sprite.setTint(0x888888);
} else { } else {
// Flickering effect for Fire // Flickering effect for Fire
const name = this.ecsWorld.getComponent(trapId, "name"); const name = this.ecsWorld.getComponent(trapId, "name");
if (name?.name === "Fire") { if (name?.name === "Fire") {
const flicker = 0.8 + Math.sin(this.scene.time.now / 100) * 0.2; const flicker = 0.8 + Math.sin(this.scene.time.now / 100) * 0.2;
sprite.setAlpha(flicker); sprite.setAlpha(flicker);
sprite.setScale(0.9 + Math.sin(this.scene.time.now / 150) * 0.1); sprite.setScale(0.9 + Math.sin(this.scene.time.now / 150) * 0.1);
// Tint based on underlying tile // Tint based on underlying tile
const tileIdx = idx(this.world, pos.x, pos.y); const tileIdx = idx(this.world, pos.x, pos.y);
const worldTile = this.world.tiles[tileIdx]; const worldTile = this.world.tiles[tileIdx];
if (worldTile === TileType.GRASS) { if (worldTile === TileType.GRASS) {
sprite.setTint(0xff3300); // Bright red-orange for burning grass sprite.setTint(0xff3300); // Bright red-orange for burning grass
} else if (worldTile === TileType.DOOR_CLOSED || worldTile === TileType.DOOR_OPEN) { } else if (worldTile === TileType.DOOR_CLOSED || worldTile === TileType.DOOR_OPEN) {
// Pulse between yellow and red for doors // Pulse between yellow and red for doors
const pulse = (Math.sin(this.scene.time.now / 150) + 1) / 2; const pulse = (Math.sin(this.scene.time.now / 150) + 1) / 2;
const r = 255; const r = 255;
const g = Math.floor(200 * (1 - pulse)); const g = Math.floor(200 * (1 - pulse));
const b = 0; const b = 0;
sprite.setTint((r << 16) | (g << 8) | b); sprite.setTint((r << 16) | (g << 8) | b);
} else {
sprite.setTint(0xffaa44); // Default orange
}
} else { } else {
sprite.setAlpha(1); sprite.setTint(0xffaa44); // Default orange
sprite.clearTint();
} }
} else {
sprite.setAlpha(1);
sprite.clearTint();
} }
} }
} }
@@ -538,7 +546,19 @@ export class DungeonRenderer {
this.fxRenderer.showHeal(x, y, amount); 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); this.fxRenderer.spawnCorpse(x, y, type);
} }

View File

@@ -9,10 +9,12 @@ vi.mock('phaser', () => {
play: vi.fn().mockReturnThis(), play: vi.fn().mockReturnThis(),
setPosition: vi.fn().mockReturnThis(), setPosition: vi.fn().mockReturnThis(),
setVisible: vi.fn().mockReturnThis(), setVisible: vi.fn().mockReturnThis(),
setDisplaySize: vi.fn().mockReturnThis(),
destroy: vi.fn(), destroy: vi.fn(),
frame: { name: '0' }, frame: { name: '0' },
setFrame: vi.fn(), setFrame: vi.fn(),
setAlpha: vi.fn(), setAlpha: vi.fn(),
setAngle: vi.fn(),
clearTint: vi.fn(), clearTint: vi.fn(),
}; };
@@ -89,10 +91,12 @@ describe('DungeonRenderer', () => {
play: vi.fn().mockReturnThis(), play: vi.fn().mockReturnThis(),
setPosition: vi.fn().mockReturnThis(), setPosition: vi.fn().mockReturnThis(),
setVisible: vi.fn().mockReturnThis(), setVisible: vi.fn().mockReturnThis(),
setDisplaySize: vi.fn().mockReturnThis(),
destroy: vi.fn(), destroy: vi.fn(),
frame: { name: '0' }, frame: { name: '0' },
setFrame: vi.fn(), setFrame: vi.fn(),
setAlpha: vi.fn(), setAlpha: vi.fn(),
setAngle: vi.fn(),
clearTint: vi.fn(), clearTint: vi.fn(),
})), })),
circle: vi.fn().mockReturnValue({ circle: vi.fn().mockReturnValue({
@@ -134,6 +138,14 @@ describe('DungeonRenderer', () => {
setDepth: vi.fn(), setDepth: vi.fn(),
forEachTile: 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(), destroy: vi.fn(),
}), }),
}, },
@@ -247,4 +259,43 @@ describe('DungeonRenderer', () => {
// Should NOT tween because it's the first spawn // Should NOT tween because it's the first spawn
expect(mockScene.tweens.add).not.toHaveBeenCalled(); 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);
});
}); });

View File

@@ -11,6 +11,7 @@ vi.mock('phaser', () => {
setAlpha: vi.fn().mockReturnThis(), setAlpha: vi.fn().mockReturnThis(),
setTint: vi.fn().mockReturnThis(), setTint: vi.fn().mockReturnThis(),
clearTint: vi.fn().mockReturnThis(), clearTint: vi.fn().mockReturnThis(),
setDisplaySize: vi.fn().mockReturnThis(),
destroy: vi.fn(), destroy: vi.fn(),
}; };
@@ -42,6 +43,7 @@ describe('FxRenderer', () => {
setAlpha: vi.fn().mockReturnThis(), setAlpha: vi.fn().mockReturnThis(),
setTint: vi.fn().mockReturnThis(), setTint: vi.fn().mockReturnThis(),
clearTint: vi.fn().mockReturnThis(), clearTint: vi.fn().mockReturnThis(),
setDisplaySize: vi.fn().mockReturnThis(),
destroy: vi.fn(), destroy: vi.fn(),
})), })),
text: vi.fn(() => ({ text: vi.fn(() => ({

View File

@@ -30,8 +30,8 @@ export class GameRenderer implements EventRenderCallbacks {
this.dungeonRenderer.showHeal(x, y, amount); this.dungeonRenderer.showHeal(x, y, amount);
} }
spawnCorpse(x: number, y: number, type: ActorType): void { spawnCorpse(x: number, y: number, type: ActorType, targetId?: EntityId): void {
this.dungeonRenderer.spawnCorpse(x, y, type); this.dungeonRenderer.spawnCorpse(x, y, type, targetId);
} }
showWait(x: number, y: number): void { showWait(x: number, y: number): void {

View File

@@ -8,7 +8,7 @@ export interface EventRenderCallbacks {
showDamage(x: number, y: number, amount: number, isCrit?: boolean, isBlock?: boolean): void; showDamage(x: number, y: number, amount: number, isCrit?: boolean, isBlock?: boolean): void;
showDodge(x: number, y: number): void; showDodge(x: number, y: number): void;
showHeal(x: number, y: number, amount: 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; showWait(x: number, y: number): void;
spawnOrb(orbId: EntityId, x: number, y: number): void; spawnOrb(orbId: EntityId, x: number, y: number): void;
collectOrb(actorId: EntityId, amount: number, x: number, y: number): void; collectOrb(actorId: EntityId, amount: number, x: number, y: number): void;
@@ -50,7 +50,7 @@ export function renderSimEvents(
break; break;
case "killed": case "killed":
callbacks.spawnCorpse(ev.x, ev.y, ev.victimType || "rat"); callbacks.spawnCorpse(ev.x, ev.y, ev.victimType || "rat", ev.targetId);
break; break;
case "waited": case "waited":

View File

@@ -97,11 +97,19 @@ describe('TargetingSystem', () => {
const enemyPos = { x: 3, y: 3 }; const enemyPos = { x: 3, y: 3 };
(getClosestVisibleEnemy as any).mockReturnValue(enemyPos); (getClosestVisibleEnemy as any).mockReturnValue(enemyPos);
const mockAccessor = {
getCombatant: vi.fn().mockReturnValue({
pos: playerPos,
inventory: { items: [{ id: 'item-1' }] }
}),
context: {}
};
targetingSystem.startTargeting( targetingSystem.startTargeting(
'item-1', 'item-1',
playerPos, playerPos,
mockWorld, mockWorld,
{} as any, // accessor mockAccessor as any,
1 as EntityId, // playerId 1 as EntityId, // playerId
new Uint8Array(100), new Uint8Array(100),
10 10
@@ -118,11 +126,19 @@ describe('TargetingSystem', () => {
const mousePos = { x: 5, y: 5 }; const mousePos = { x: 5, y: 5 };
(getClosestVisibleEnemy as any).mockReturnValue(null); (getClosestVisibleEnemy as any).mockReturnValue(null);
const mockAccessor = {
getCombatant: vi.fn().mockReturnValue({
pos: playerPos,
inventory: { items: [{ id: 'item-1' }] }
}),
context: {}
};
targetingSystem.startTargeting( targetingSystem.startTargeting(
'item-1', 'item-1',
playerPos, playerPos,
mockWorld, mockWorld,
{} as any, // accessor mockAccessor as any,
1 as EntityId, // playerId 1 as EntityId, // playerId
new Uint8Array(100), new Uint8Array(100),
10, 10,
@@ -143,12 +159,20 @@ describe('TargetingSystem', () => {
path: [] path: []
}); });
const mockAccessor = {
getCombatant: vi.fn().mockReturnValue({
pos: playerPos,
inventory: { items: [{ id: 'item-1' }] }
}),
context: {}
};
// Start targeting // Start targeting
targetingSystem.startTargeting( targetingSystem.startTargeting(
'item-1', 'item-1',
playerPos, playerPos,
mockWorld, mockWorld,
{} as any, // accessor mockAccessor as any,
1 as EntityId, 1 as EntityId,
new Uint8Array(100), new Uint8Array(100),
10, 10,