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

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

View File

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

View File

@@ -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(() => ({