Fixed coprse bug
This commit is contained in:
@@ -11,36 +11,36 @@ describe('World Generator', () => {
|
|||||||
describe('generateWorld', () => {
|
describe('generateWorld', () => {
|
||||||
it('should generate a world with correct dimensions', () => {
|
it('should generate a world with correct dimensions', () => {
|
||||||
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 } = 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', () => {
|
||||||
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(1, runState);
|
const { world, playerId, ecsWorld } = generateWorld(1, runState);
|
||||||
const accessor = new EntityAccessor(world, playerId, ecsWorld);
|
const accessor = new EntityAccessor(world, playerId, ecsWorld);
|
||||||
|
|
||||||
expect(playerId).toBeGreaterThan(0);
|
expect(playerId).toBeGreaterThan(0);
|
||||||
const player = accessor.getPlayer();
|
const player = accessor.getPlayer();
|
||||||
expect(player).toBeDefined();
|
expect(player).toBeDefined();
|
||||||
@@ -53,36 +53,36 @@ describe('World Generator', () => {
|
|||||||
|
|
||||||
it('should create walkable rooms', () => {
|
it('should create walkable rooms', () => {
|
||||||
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(1, runState);
|
const { world, playerId, ecsWorld } = generateWorld(1, runState);
|
||||||
const accessor = new EntityAccessor(world, playerId, ecsWorld);
|
const accessor = new EntityAccessor(world, playerId, ecsWorld);
|
||||||
const player = accessor.getPlayer()!;
|
const player = accessor.getPlayer()!;
|
||||||
|
|
||||||
// Player should spawn in a walkable area
|
// Player should spawn in a walkable area
|
||||||
expect(isWall(world, player.pos.x, player.pos.y)).toBe(false);
|
expect(isWall(world, player.pos.x, player.pos.y)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should place exit in valid location', () => {
|
it('should place exit in valid location', () => {
|
||||||
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 } = generateWorld(1, runState);
|
const { world } = generateWorld(1, runState);
|
||||||
|
|
||||||
expect(inBounds(world, world.exit.x, world.exit.y)).toBe(true);
|
expect(inBounds(world, world.exit.x, world.exit.y)).toBe(true);
|
||||||
// Exit should be on a floor tile
|
// Exit should be on a floor tile
|
||||||
expect(isWall(world, world.exit.x, world.exit.y)).toBe(false);
|
expect(isWall(world, world.exit.x, world.exit.y)).toBe(false);
|
||||||
@@ -90,21 +90,21 @@ describe('World Generator', () => {
|
|||||||
|
|
||||||
it('should create enemies', () => {
|
it('should create enemies', () => {
|
||||||
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(1, runState);
|
const { world, playerId, ecsWorld } = generateWorld(1, runState);
|
||||||
const accessor = new EntityAccessor(world, playerId, ecsWorld);
|
const accessor = new EntityAccessor(world, playerId, ecsWorld);
|
||||||
|
|
||||||
const enemies = accessor.getEnemies();
|
const enemies = accessor.getEnemies();
|
||||||
expect(enemies.length).toBeGreaterThan(0);
|
expect(enemies.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Enemies should have stats
|
// Enemies should have stats
|
||||||
enemies.forEach(enemy => {
|
enemies.forEach(enemy => {
|
||||||
expect(enemy.stats).toBeDefined();
|
expect(enemy.stats).toBeDefined();
|
||||||
@@ -115,25 +115,25 @@ describe('World Generator', () => {
|
|||||||
|
|
||||||
it('should generate deterministic maps for same level', () => {
|
it('should generate deterministic maps for same level', () => {
|
||||||
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: world1, playerId: player1, ecsWorld: ecs1 } = generateWorld(1, runState);
|
const { world: world1, playerId: player1, ecsWorld: ecs1 } = generateWorld(1, runState);
|
||||||
const { world: world2, playerId: player2, ecsWorld: ecs2 } = generateWorld(1, runState);
|
const { world: world2, playerId: player2, ecsWorld: ecs2 } = generateWorld(1, runState);
|
||||||
|
|
||||||
// Same level should generate identical layouts
|
// Same level should generate identical layouts
|
||||||
expect(world1.tiles).toEqual(world2.tiles);
|
expect(world1.tiles).toEqual(world2.tiles);
|
||||||
expect(world1.exit).toEqual(world2.exit);
|
expect(world1.exit).toEqual(world2.exit);
|
||||||
|
|
||||||
const accessor1 = new EntityAccessor(world1, player1, ecs1);
|
const accessor1 = new EntityAccessor(world1, player1, ecs1);
|
||||||
const accessor2 = new EntityAccessor(world2, player2, ecs2);
|
const accessor2 = new EntityAccessor(world2, player2, ecs2);
|
||||||
|
|
||||||
const player1Pos = accessor1.getPlayer()!.pos;
|
const player1Pos = accessor1.getPlayer()!.pos;
|
||||||
const player2Pos = accessor2.getPlayer()!.pos;
|
const player2Pos = accessor2.getPlayer()!.pos;
|
||||||
expect(player1Pos).toEqual(player2Pos);
|
expect(player1Pos).toEqual(player2Pos);
|
||||||
@@ -141,45 +141,45 @@ describe('World Generator', () => {
|
|||||||
|
|
||||||
it('should generate different maps for different levels', () => {
|
it('should generate different maps for different levels', () => {
|
||||||
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: world1 } = generateWorld(1, runState);
|
const { world: world1 } = generateWorld(1, runState);
|
||||||
const { world: world2 } = generateWorld(2, runState);
|
const { world: world2 } = generateWorld(2, runState);
|
||||||
|
|
||||||
// Different levels should have different layouts
|
// Different levels should have different layouts
|
||||||
expect(world1.tiles).not.toEqual(world2.tiles);
|
expect(world1.tiles).not.toEqual(world2.tiles);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should scale enemy difficulty with level', () => {
|
it('should scale enemy difficulty with level', () => {
|
||||||
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: world1, playerId: p1, ecsWorld: ecs1 } = generateWorld(1, runState);
|
const { world: world1, playerId: p1, ecsWorld: ecs1 } = generateWorld(1, runState);
|
||||||
const { world: world5, playerId: p5, ecsWorld: ecs5 } = generateWorld(5, runState);
|
const { world: world5, playerId: p5, ecsWorld: ecs5 } = generateWorld(5, runState);
|
||||||
|
|
||||||
const accessor1 = new EntityAccessor(world1, p1, ecs1);
|
const accessor1 = new EntityAccessor(world1, p1, ecs1);
|
||||||
const accessor5 = new EntityAccessor(world5, p5, ecs5);
|
const accessor5 = new EntityAccessor(world5, p5, ecs5);
|
||||||
|
|
||||||
const enemies1 = accessor1.getEnemies();
|
const enemies1 = accessor1.getEnemies();
|
||||||
const enemies5 = accessor5.getEnemies();
|
const enemies5 = accessor5.getEnemies();
|
||||||
|
|
||||||
// Higher level should have more enemies
|
// Higher level should have more enemies
|
||||||
expect(enemies5.length).toBeGreaterThan(enemies1.length);
|
expect(enemies5.length).toBeGreaterThan(enemies1.length);
|
||||||
|
|
||||||
// Higher level enemies should have higher stats
|
// Higher level enemies should have higher stats
|
||||||
const avgHp1 = enemies1.reduce((sum, e) => sum + (e.stats.hp || 0), 0) / enemies1.length;
|
const avgHp1 = enemies1.reduce((sum, e) => sum + (e.stats.hp || 0), 0) / enemies1.length;
|
||||||
const avgHp5 = enemies5.reduce((sum, e) => sum + (e.stats.hp || 0), 0) / enemies5.length;
|
const avgHp5 = enemies5.reduce((sum, e) => sum + (e.stats.hp || 0), 0) / enemies5.length;
|
||||||
@@ -187,11 +187,11 @@ describe('World Generator', () => {
|
|||||||
});
|
});
|
||||||
it('should generate doors on dungeon floors', () => {
|
it('should generate doors on dungeon floors', () => {
|
||||||
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: [] }
|
||||||
};
|
};
|
||||||
@@ -205,17 +205,17 @@ describe('World Generator', () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(foundDoor).toBe(true);
|
expect(foundDoor).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ensure player spawns on safe tile (not grass)', () => {
|
it('should ensure player spawns on safe tile (not grass)', () => {
|
||||||
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: [] }
|
||||||
};
|
};
|
||||||
@@ -225,7 +225,7 @@ describe('World Generator', () => {
|
|||||||
const { world, playerId, ecsWorld } = generateWorld(1, runState);
|
const { world, playerId, ecsWorld } = generateWorld(1, runState);
|
||||||
const accessor = new EntityAccessor(world, playerId, ecsWorld);
|
const accessor = new EntityAccessor(world, playerId, ecsWorld);
|
||||||
const player = accessor.getPlayer()!;
|
const player = accessor.getPlayer()!;
|
||||||
|
|
||||||
// Check tile under player
|
// Check tile under player
|
||||||
const tileIdx = player.pos.y * world.width + player.pos.x;
|
const tileIdx = player.pos.y * world.width + player.pos.x;
|
||||||
const tile = world.tiles[tileIdx];
|
const tile = world.tiles[tileIdx];
|
||||||
@@ -240,7 +240,7 @@ describe('World Generator', () => {
|
|||||||
describe('Cave Generation (Floors 10+)', () => {
|
describe('Cave Generation (Floors 10+)', () => {
|
||||||
it('should generate cellular automata style maps', () => {
|
it('should generate cellular automata style maps', () => {
|
||||||
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,
|
||||||
@@ -250,17 +250,17 @@ 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should place enemies in caves', () => {
|
it('should place enemies in 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,
|
||||||
@@ -277,7 +277,7 @@ describe('World Generator', () => {
|
|||||||
|
|
||||||
it('should ensure the map is connected (Player can reach Exit)', () => {
|
it('should ensure the map is connected (Player can reach Exit)', () => {
|
||||||
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,
|
||||||
@@ -301,26 +301,26 @@ describe('World Generator', () => {
|
|||||||
pathfinder.compute(player.pos.x, player.pos.y, (x, y) => {
|
pathfinder.compute(player.pos.x, player.pos.y, (x, y) => {
|
||||||
path.push([x, y]);
|
path.push([x, y]);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(path.length).toBeGreaterThan(0);
|
expect(path.length).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)
|
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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(() => ({
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -40,19 +40,19 @@ export function renderSimEvents(
|
|||||||
case "damaged":
|
case "damaged":
|
||||||
callbacks.showDamage(ev.x, ev.y, ev.amount, ev.isCrit, ev.isBlock);
|
callbacks.showDamage(ev.x, ev.y, ev.amount, ev.isCrit, ev.isBlock);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "dodged":
|
case "dodged":
|
||||||
callbacks.showDodge(ev.x, ev.y);
|
callbacks.showDodge(ev.x, ev.y);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "healed":
|
case "healed":
|
||||||
callbacks.showHeal(ev.x, ev.y, ev.amount);
|
callbacks.showHeal(ev.x, ev.y, ev.amount);
|
||||||
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":
|
||||||
if (ev.actorId === context.playerId) {
|
if (ev.actorId === context.playerId) {
|
||||||
const pos = context.getPlayerPos();
|
const pos = context.getPlayerPos();
|
||||||
@@ -61,23 +61,23 @@ export function renderSimEvents(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "orb-spawned":
|
case "orb-spawned":
|
||||||
callbacks.spawnOrb(ev.orbId, ev.x, ev.y);
|
callbacks.spawnOrb(ev.orbId, ev.x, ev.y);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "exp-collected":
|
case "exp-collected":
|
||||||
if (ev.actorId === context.playerId) {
|
if (ev.actorId === context.playerId) {
|
||||||
callbacks.collectOrb(ev.actorId, ev.amount, ev.x, ev.y);
|
callbacks.collectOrb(ev.actorId, ev.amount, ev.x, ev.y);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "leveled-up":
|
case "leveled-up":
|
||||||
if (ev.actorId === context.playerId) {
|
if (ev.actorId === context.playerId) {
|
||||||
callbacks.showLevelUp(ev.x, ev.y);
|
callbacks.showLevelUp(ev.x, ev.y);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "enemy-alerted":
|
case "enemy-alerted":
|
||||||
callbacks.showAlert(ev.x, ev.y);
|
callbacks.showAlert(ev.x, ev.y);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user