Add levelling up mechanics through experience gained via killing enemies

This commit is contained in:
Peter Stockings
2026-01-04 18:36:31 +11:00
parent 42cd77998d
commit 29e46093f5
11 changed files with 373 additions and 84 deletions

View File

@@ -12,7 +12,9 @@ export class DungeonRenderer {
private playerSprite?: Phaser.GameObjects.Sprite;
private enemySprites: Map<EntityId, Phaser.GameObjects.Sprite> = new Map();
private orbSprites: Map<EntityId, Phaser.GameObjects.Arc> = new Map();
private corpseSprites: Phaser.GameObjects.Sprite[] = [];
// FOV
private fov!: any;
@@ -52,7 +54,8 @@ export class DungeonRenderer {
this.minimapContainer.setVisible(false);
}
initializeLevel(world: World) {
initializeFloor(world: World) {
this.world = world;
this.seen = new Uint8Array(this.world.width * this.world.height);
this.visible = new Uint8Array(this.world.width * this.world.height);
@@ -277,6 +280,41 @@ export class DungeonRenderer {
}
}
// Orbs
const activeOrbIds = new Set<EntityId>();
for (const a of this.world.actors.values()) {
if (a.type !== "exp_orb") continue;
const i = idx(this.world, a.pos.x, a.pos.y);
// PD usually shows items only when visible or seen. Let's do visible.
const isVis = this.visible[i] === 1;
if (!isVis) continue;
activeOrbIds.add(a.id);
let orb = this.orbSprites.get(a.id);
if (!orb) {
orb = this.scene.add.circle(0, 0, 4, GAME_CONFIG.rendering.expOrbColor);
orb.setStrokeStyle(1, 0xffffff, 0.5);
orb.setDepth(45);
this.orbSprites.set(a.id, orb);
}
orb.setPosition(a.pos.x * TILE_SIZE + TILE_SIZE / 2, a.pos.y * TILE_SIZE + TILE_SIZE / 2);
orb.setVisible(true);
}
for (const [id, orb] of this.orbSprites.entries()) {
if (!activeOrbIds.has(id)) {
orb.setVisible(false);
if (!this.world.actors.has(id)) {
orb.destroy();
this.orbSprites.delete(id);
}
}
}
this.renderMinimap();
}
@@ -355,8 +393,10 @@ export class DungeonRenderer {
});
}
spawnCorpse(x: number, y: number, type: "player" | "rat" | "bat") {
spawnCorpse(x: number, y: number, type: "player" | "rat" | "bat" | "exp_orb") {
if (type === "exp_orb") return;
const textureKey = type === "player" ? "warrior" : type;
const corpse = this.scene.add.sprite(
x * TILE_SIZE + TILE_SIZE / 2,
y * TILE_SIZE + TILE_SIZE / 2,
@@ -389,4 +429,54 @@ export class DungeonRenderer {
onComplete: () => text.destroy()
});
}
spawnOrb(_orbId: EntityId, _x: number, _y: number) {
// Just to trigger a render update if needed, but render() handles it
}
collectOrb(_actorId: EntityId, amount: number, x: number, y: number) {
const screenX = x * TILE_SIZE + TILE_SIZE / 2;
const screenY = y * TILE_SIZE;
const text = this.scene.add.text(screenX, screenY, `+${amount} EXP`, {
fontSize: "14px",
color: "#" + GAME_CONFIG.rendering.expTextColor.toString(16),
stroke: "#000",
strokeThickness: 2,
fontStyle: "bold"
}).setOrigin(0.5, 1).setDepth(200);
this.scene.tweens.add({
targets: text,
y: screenY - 32,
alpha: 0,
duration: 1000,
ease: "Power1",
onComplete: () => text.destroy()
});
}
showLevelUp(x: number, y: number) {
const screenX = x * TILE_SIZE + TILE_SIZE / 2;
const screenY = y * TILE_SIZE;
const text = this.scene.add.text(screenX, screenY - 16, "+1 LVL", {
fontSize: "20px",
color: "#" + GAME_CONFIG.rendering.levelUpColor.toString(16),
stroke: "#000",
strokeThickness: 3,
fontStyle: "bold"
}).setOrigin(0.5, 1).setDepth(210);
this.scene.tweens.add({
targets: text,
y: screenY - 60,
alpha: 0,
duration: 1500,
ease: "Cubic.out",
onComplete: () => text.destroy()
});
}
}

View File

@@ -94,6 +94,17 @@ describe('DungeonRenderer', () => {
exists: vi.fn().mockReturnValue(true),
generateFrameNumbers: vi.fn(),
},
make: {
tilemap: vi.fn().mockReturnValue({
addTilesetImage: vi.fn().mockReturnValue({}),
createLayer: vi.fn().mockReturnValue({
setDepth: vi.fn(),
forEachTile: vi.fn(),
}),
destroy: vi.fn(),
}),
},
};
mockWorld = {
@@ -107,8 +118,9 @@ describe('DungeonRenderer', () => {
renderer = new DungeonRenderer(mockScene);
});
it('should track and clear corpse sprites on level initialization', () => {
renderer.initializeLevel(mockWorld);
it('should track and clear corpse sprites on floor initialization', () => {
renderer.initializeFloor(mockWorld);
// Spawn a couple of corpses
renderer.spawnCorpse(1, 1, 'rat');
@@ -120,8 +132,9 @@ describe('DungeonRenderer', () => {
expect(mockScene.add.sprite).toHaveBeenCalledTimes(3);
// Initialize level again (changing level)
renderer.initializeLevel(mockWorld);
// Initialize floor again (changing level)
renderer.initializeFloor(mockWorld);
// Verify destroy was called on both corpse sprites
expect(corpse1.destroy).toHaveBeenCalledTimes(1);