Add more stats, crit/block/accuracy/dodge/lifesteal

This commit is contained in:
Peter Stockings
2026-01-05 12:39:43 +11:00
parent 171abb681a
commit 86a6afd1df
14 changed files with 815 additions and 406 deletions

View File

@@ -1,9 +1,11 @@
import Phaser from "phaser";
import { FOV } from "rot-js";
import { type World, type EntityId, type Vec2 } from "../core/types";
import { TILE_SIZE } from "../core/constants";
import { idx, inBounds, isWall } from "../engine/world/world-logic";
import { idx, isWall } from "../engine/world/world-logic";
import { GAME_CONFIG } from "../core/config/GameConfig";
import { FovManager } from "./FovManager";
import { MinimapRenderer } from "./MinimapRenderer";
import { FxRenderer } from "./FxRenderer";
export class DungeonRenderer {
private scene: Phaser.Scene;
@@ -13,53 +15,23 @@ 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[] = [];
private fovManager: FovManager;
private minimapRenderer: MinimapRenderer;
private fxRenderer: FxRenderer;
// FOV
private fov!: any;
private seen!: Uint8Array;
private visible!: Uint8Array;
private visibleStrength!: Float32Array;
// State refs
private world!: World;
// Minimap
private minimapGfx!: Phaser.GameObjects.Graphics;
private minimapContainer!: Phaser.GameObjects.Container;
private minimapBg!: Phaser.GameObjects.Rectangle;
private minimapVisible = false;
constructor(scene: Phaser.Scene) {
this.scene = scene;
this.initMinimap();
}
private initMinimap() {
this.minimapContainer = this.scene.add.container(0, 0);
this.minimapContainer.setScrollFactor(0);
this.minimapContainer.setDepth(1001);
this.minimapBg = this.scene.add
.rectangle(0, 0, GAME_CONFIG.ui.minimapPanelWidth, GAME_CONFIG.ui.minimapPanelHeight, 0x000000, 0.8)
.setStrokeStyle(1, 0xffffff, 0.9)
.setInteractive();
this.minimapGfx = this.scene.add.graphics();
this.minimapContainer.add(this.minimapBg);
this.minimapContainer.add(this.minimapGfx);
this.positionMinimap();
this.minimapContainer.setVisible(false);
this.fovManager = new FovManager();
this.minimapRenderer = new MinimapRenderer(scene);
this.fxRenderer = new FxRenderer(scene);
}
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);
this.visibleStrength = new Float32Array(this.world.width * this.world.height);
this.fovManager.initialize(world);
// Setup Tilemap
if (this.map) this.map.destroy();
@@ -80,21 +52,17 @@ export class DungeonRenderer {
tile.setVisible(false);
});
// Clear old corpses
for (const sprite of this.corpseSprites) {
sprite.destroy();
}
this.corpseSprites = [];
this.fxRenderer.clearCorpses();
this.setupAnimations();
this.minimapRenderer.positionMinimap();
}
// Setup player sprite
private setupAnimations() {
// Player
if (!this.playerSprite) {
this.playerSprite = this.scene.add.sprite(0, 0, "warrior", 0);
this.playerSprite.setDepth(100);
// Calculate scale to fit 15px high sprite into 16px tile
const scale = 1.0;
this.playerSprite.setScale(scale);
this.scene.anims.create({
key: 'warrior-idle',
frames: this.scene.anims.generateFrameNumbers('warrior', { frames: [0, 0, 0, 1, 0, 0, 1, 1] }),
@@ -161,69 +129,39 @@ export class DungeonRenderer {
repeat: 0
});
}
this.fov = new FOV.PreciseShadowcasting((x: number, y: number) => {
if (!inBounds(this.world, x, y)) return false;
return !isWall(this.world, x, y);
});
this.positionMinimap();
}
private positionMinimap() {
const cam = this.scene.cameras.main;
this.minimapContainer.setPosition(cam.width / 2, cam.height / 2);
}
toggleMinimap() {
this.minimapVisible = !this.minimapVisible;
this.minimapContainer.setVisible(this.minimapVisible);
this.minimapRenderer.toggle();
}
isMinimapVisible(): boolean {
return this.minimapVisible;
return this.minimapRenderer.isVisible();
}
computeFov(playerId: EntityId) {
this.visible.fill(0);
this.visibleStrength.fill(0);
const player = this.world.actors.get(playerId)!;
const ox = player.pos.x;
const oy = player.pos.y;
this.fov.compute(ox, oy, GAME_CONFIG.player.viewRadius, (x: number, y: number, r: number, v: number) => {
if (!inBounds(this.world, x, y)) return;
const i = idx(this.world, x, y);
this.visible[i] = 1;
this.seen[i] = 1;
const radiusT = Phaser.Math.Clamp(r / GAME_CONFIG.player.viewRadius, 0, 1);
const falloff = 1 - radiusT * 0.6;
const strength = Phaser.Math.Clamp(v * falloff, 0, 1);
if (strength > this.visibleStrength[i]) this.visibleStrength[i] = strength;
});
this.fovManager.compute(this.world, playerId);
}
isSeen(x: number, y: number): boolean {
if (!this.world || !inBounds(this.world, x, y)) return false;
return this.seen[idx(this.world, x, y)] === 1;
return this.fovManager.isSeen(x, y);
}
get seenArray() {
return this.seen;
return this.fovManager.seenArray;
}
render(_playerPath: Vec2[]) {
if (!this.world || !this.layer) return;
const seen = this.fovManager.seenArray;
const visible = this.fovManager.visibleArray;
// Update Tiles
this.layer.forEachTile(tile => {
const i = idx(this.world, tile.x, tile.y);
const isSeen = this.seen[i] === 1;
const isVis = this.visible[i] === 1;
const isSeen = seen[i] === 1;
const isVis = visible[i] === 1;
if (!isSeen) {
tile.setVisible(false);
@@ -239,40 +177,57 @@ export class DungeonRenderer {
}
});
// Actors
// Actors (Combatants)
const activeEnemyIds = new Set<EntityId>();
const activeOrbIds = new Set<EntityId>();
for (const a of this.world.actors.values()) {
const i = idx(this.world, a.pos.x, a.pos.y);
const isVis = this.visible[i] === 1;
if (a.isPlayer) {
if (this.playerSprite) {
this.playerSprite.setPosition(a.pos.x * TILE_SIZE + TILE_SIZE / 2, a.pos.y * TILE_SIZE + TILE_SIZE / 2);
this.playerSprite.setVisible(true);
const isVis = visible[i] === 1;
if (a.category === "combatant") {
if (a.isPlayer) {
if (this.playerSprite) {
this.playerSprite.setPosition(a.pos.x * TILE_SIZE + TILE_SIZE / 2, a.pos.y * TILE_SIZE + TILE_SIZE / 2);
this.playerSprite.setVisible(true);
}
continue;
}
if (!isVis) continue;
activeEnemyIds.add(a.id);
let sprite = this.enemySprites.get(a.id);
const textureKey = a.type;
if (!sprite) {
sprite = this.scene.add.sprite(0, 0, textureKey, 0);
sprite.setDepth(99);
sprite.play(`${textureKey}-idle`);
this.enemySprites.set(a.id, sprite);
}
sprite.setPosition(a.pos.x * TILE_SIZE + TILE_SIZE / 2, a.pos.y * TILE_SIZE + TILE_SIZE / 2);
sprite.setVisible(true);
} else if (a.category === "collectible") {
if (a.type === "exp_orb") {
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);
}
continue;
}
if (!isVis) continue;
const enemyType = a.type as keyof typeof GAME_CONFIG.enemies;
if (!GAME_CONFIG.enemies[enemyType]) continue;
activeEnemyIds.add(a.id);
let sprite = this.enemySprites.get(a.id);
const textureKey = a.type || "rat";
if (!sprite) {
sprite = this.scene.add.sprite(0, 0, textureKey, 0);
sprite.setDepth(99);
sprite.play(`${textureKey}-idle`);
this.enemySprites.set(a.id, sprite);
}
sprite.setPosition(a.pos.x * TILE_SIZE + TILE_SIZE / 2, a.pos.y * TILE_SIZE + TILE_SIZE / 2);
sprite.setVisible(true);
}
// Cleanup sprites for removed actors
for (const [id, sprite] of this.enemySprites.entries()) {
if (!activeEnemyIds.has(id)) {
sprite.setVisible(false);
@@ -283,30 +238,6 @@ 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);
@@ -317,169 +248,39 @@ export class DungeonRenderer {
}
}
this.renderMinimap();
this.minimapRenderer.render(this.world, seen, visible);
}
private renderMinimap() {
this.minimapGfx.clear();
if (!this.world) return;
const padding = GAME_CONFIG.ui.minimapPadding;
const availableWidth = GAME_CONFIG.ui.minimapPanelWidth - padding * 2;
const availableHeight = GAME_CONFIG.ui.minimapPanelHeight - padding * 2;
const scaleX = availableWidth / this.world.width;
const scaleY = availableHeight / this.world.height;
const tileSize = Math.floor(Math.min(scaleX, scaleY));
const mapPixelWidth = this.world.width * tileSize;
const mapPixelHeight = this.world.height * tileSize;
const offsetX = -mapPixelWidth / 2;
const offsetY = -mapPixelHeight / 2;
for (let y = 0; y < this.world.height; y++) {
for (let x = 0; x < this.world.width; x++) {
const i = idx(this.world, x, y);
if (this.seen[i] !== 1) continue;
const wall = isWall(this.world, x, y);
const color = wall ? 0x666666 : 0x333333;
this.minimapGfx.fillStyle(color, 1);
this.minimapGfx.fillRect(offsetX + x * tileSize, offsetY + y * tileSize, tileSize, tileSize);
}
}
const ex = this.world.exit.x;
const ey = this.world.exit.y;
if (this.seen[idx(this.world, ex, ey)] === 1) {
this.minimapGfx.fillStyle(0xffd166, 1);
this.minimapGfx.fillRect(offsetX + ex * tileSize, offsetY + ey * tileSize, tileSize, tileSize);
}
const player = [...this.world.actors.values()].find(a => a.isPlayer);
if (player) {
this.minimapGfx.fillStyle(0x66ff66, 1);
this.minimapGfx.fillRect(offsetX + player.pos.x * tileSize, offsetY + player.pos.y * tileSize, tileSize, tileSize);
}
for (const a of this.world.actors.values()) {
if (a.isPlayer) continue;
const i = idx(this.world, a.pos.x, a.pos.y);
if (this.visible[i] === 1) {
this.minimapGfx.fillStyle(0xff6666, 1);
this.minimapGfx.fillRect(offsetX + a.pos.x * tileSize, offsetY + a.pos.y * tileSize, tileSize, tileSize);
}
}
// FX Delegations
showDamage(x: number, y: number, amount: number, isCrit = false, isBlock = false) {
this.fxRenderer.showDamage(x, y, amount, isCrit, isBlock);
}
showDamage(x: number, y: number, amount: number) {
const screenX = x * TILE_SIZE + TILE_SIZE / 2;
const screenY = y * TILE_SIZE;
const text = this.scene.add.text(screenX, screenY, amount.toString(), {
fontSize: "16px",
color: "#ff3333",
stroke: "#000",
strokeThickness: 2,
fontStyle: "bold"
}).setOrigin(0.5, 1).setDepth(200);
this.scene.tweens.add({
targets: text,
y: screenY - 24,
alpha: 0,
duration: 800,
ease: "Power1",
onComplete: () => text.destroy()
});
showDodge(x: number, y: number) {
this.fxRenderer.showDodge(x, y);
}
spawnCorpse(x: number, y: number, type: "player" | "rat" | "bat" | "exp_orb") {
if (type === "exp_orb") return;
const textureKey = type === "player" ? "warrior" : type;
showHeal(x: number, y: number, amount: number) {
this.fxRenderer.showHeal(x, y, amount);
}
const corpse = this.scene.add.sprite(
x * TILE_SIZE + TILE_SIZE / 2,
y * TILE_SIZE + TILE_SIZE / 2,
textureKey,
0
);
corpse.setDepth(50);
corpse.play(`${textureKey}-die`);
this.corpseSprites.push(corpse);
spawnCorpse(x: number, y: number, type: "player" | "rat" | "bat") {
this.fxRenderer.spawnCorpse(x, y, type);
}
showWait(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, "zZz", {
fontSize: "14px",
color: "#aaaaff",
stroke: "#000",
strokeThickness: 2,
fontStyle: "bold"
}).setOrigin(0.5, 1).setDepth(200);
this.scene.tweens.add({
targets: text,
y: screenY - 20,
alpha: 0,
duration: 600,
ease: "Power1",
onComplete: () => text.destroy()
});
this.fxRenderer.showWait(x, y);
}
spawnOrb(_orbId: EntityId, _x: number, _y: number) {
// Just to trigger a render update if needed, but render() handles it
// Handled in render()
}
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()
});
collectOrb(actorId: EntityId, amount: number, x: number, y: number) {
this.fxRenderer.collectOrb(actorId, amount, x, y);
}
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()
});
this.fxRenderer.showLevelUp(x, y);
}
}

View File

@@ -0,0 +1,68 @@
import { FOV } from "rot-js";
import { type World, type EntityId } from "../core/types";
import { idx, inBounds, isWall } from "../engine/world/world-logic";
import { GAME_CONFIG } from "../core/config/GameConfig";
import Phaser from "phaser";
export class FovManager {
private fov!: any;
private seen!: Uint8Array;
private visible!: Uint8Array;
private visibleStrength!: Float32Array;
private worldWidth: number = 0;
private worldHeight: number = 0;
initialize(world: World) {
this.worldWidth = world.width;
this.worldHeight = world.height;
this.seen = new Uint8Array(world.width * world.height);
this.visible = new Uint8Array(world.width * world.height);
this.visibleStrength = new Float32Array(world.width * world.height);
this.fov = new FOV.PreciseShadowcasting((x: number, y: number) => {
if (!inBounds(world, x, y)) return false;
return !isWall(world, x, y);
});
}
compute(world: World, playerId: EntityId) {
this.visible.fill(0);
this.visibleStrength.fill(0);
const player = world.actors.get(playerId)!;
const ox = player.pos.x;
const oy = player.pos.y;
this.fov.compute(ox, oy, GAME_CONFIG.player.viewRadius, (x: number, y: number, r: number, v: number) => {
if (!inBounds(world, x, y)) return;
const i = idx(world, x, y);
this.visible[i] = 1;
this.seen[i] = 1;
const radiusT = Phaser.Math.Clamp(r / GAME_CONFIG.player.viewRadius, 0, 1);
const falloff = 1 - radiusT * 0.6;
const strength = Phaser.Math.Clamp(v * falloff, 0, 1);
if (strength > this.visibleStrength[i]) this.visibleStrength[i] = strength;
});
}
isSeen(x: number, y: number): boolean {
if (!inBounds({ width: this.worldWidth, height: this.worldHeight } as any, x, y)) return false;
return this.seen[y * this.worldWidth + x] === 1;
}
isVisible(x: number, y: number): boolean {
if (!inBounds({ width: this.worldWidth, height: this.worldHeight } as any, x, y)) return false;
return this.visible[y * this.worldWidth + x] === 1;
}
get seenArray() {
return this.seen;
}
get visibleArray() {
return this.visible;
}
}

191
src/rendering/FxRenderer.ts Normal file
View File

@@ -0,0 +1,191 @@
import Phaser from "phaser";
import { type EntityId } from "../core/types";
import { TILE_SIZE } from "../core/constants";
import { GAME_CONFIG } from "../core/config/GameConfig";
export class FxRenderer {
private scene: Phaser.Scene;
private corpseSprites: Phaser.GameObjects.Sprite[] = [];
constructor(scene: Phaser.Scene) {
this.scene = scene;
}
clearCorpses() {
for (const sprite of this.corpseSprites) {
sprite.destroy();
}
this.corpseSprites = [];
}
showDamage(x: number, y: number, amount: number, isCrit = false, isBlock = false) {
const screenX = x * TILE_SIZE + TILE_SIZE / 2;
const screenY = y * TILE_SIZE;
let textStr = amount.toString();
let color = "#ff3333";
let fontSize = "16px";
if (isCrit) {
textStr += "!";
color = "#ffff00";
fontSize = "22px";
}
const text = this.scene.add.text(screenX, screenY, textStr, {
fontSize,
color,
stroke: "#000",
strokeThickness: 2,
fontStyle: "bold"
}).setOrigin(0.5, 1).setDepth(200);
if (isBlock) {
const blockText = this.scene.add.text(screenX + 10, screenY - 10, "Blocked", {
fontSize: "10px",
color: "#888888",
fontStyle: "bold"
}).setOrigin(0, 1).setDepth(200);
this.scene.tweens.add({
targets: blockText,
y: screenY - 34,
alpha: 0,
duration: 800,
onComplete: () => blockText.destroy()
});
}
this.scene.tweens.add({
targets: text,
y: screenY - 24,
alpha: 0,
duration: isCrit ? 1200 : 800,
ease: isCrit ? "Bounce.out" : "Power1",
onComplete: () => text.destroy()
});
}
showDodge(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, "Dodge", {
fontSize: "14px",
color: "#ffffff",
stroke: "#000",
strokeThickness: 2,
fontStyle: "italic"
}).setOrigin(0.5, 1).setDepth(200);
this.scene.tweens.add({
targets: text,
x: screenX + (Math.random() > 0.5 ? 20 : -20),
y: screenY - 20,
alpha: 0,
duration: 600,
onComplete: () => text.destroy()
});
}
showHeal(x: number, y: number, amount: number) {
const screenX = x * TILE_SIZE + TILE_SIZE / 2;
const screenY = y * TILE_SIZE;
const text = this.scene.add.text(screenX, screenY, `+${amount}`, {
fontSize: "16px",
color: "#33ff33",
stroke: "#000",
strokeThickness: 2,
fontStyle: "bold"
}).setOrigin(0.5, 1).setDepth(200);
this.scene.tweens.add({
targets: text,
y: screenY - 30,
alpha: 0,
duration: 1000,
onComplete: () => text.destroy()
});
}
spawnCorpse(x: number, y: number, type: "player" | "rat" | "bat") {
const textureKey = type === "player" ? "warrior" : type;
const corpse = this.scene.add.sprite(
x * TILE_SIZE + TILE_SIZE / 2,
y * TILE_SIZE + TILE_SIZE / 2,
textureKey,
0
);
corpse.setDepth(50);
corpse.play(`${textureKey}-die`);
this.corpseSprites.push(corpse);
}
showWait(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, "zZz", {
fontSize: "14px",
color: "#aaaaff",
stroke: "#000",
strokeThickness: 2,
fontStyle: "bold"
}).setOrigin(0.5, 1).setDepth(200);
this.scene.tweens.add({
targets: text,
y: screenY - 20,
alpha: 0,
duration: 600,
ease: "Power1",
onComplete: () => text.destroy()
});
}
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

@@ -0,0 +1,104 @@
import Phaser from "phaser";
import { type World, type CombatantActor } from "../core/types";
import { idx, isWall } from "../engine/world/world-logic";
import { GAME_CONFIG } from "../core/config/GameConfig";
export class MinimapRenderer {
private scene: Phaser.Scene;
private minimapGfx!: Phaser.GameObjects.Graphics;
private minimapContainer!: Phaser.GameObjects.Container;
private minimapBg!: Phaser.GameObjects.Rectangle;
private minimapVisible = false;
constructor(scene: Phaser.Scene) {
this.scene = scene;
this.initMinimap();
}
private initMinimap() {
this.minimapContainer = this.scene.add.container(0, 0);
this.minimapContainer.setScrollFactor(0);
this.minimapContainer.setDepth(1001);
this.minimapBg = this.scene.add
.rectangle(0, 0, GAME_CONFIG.ui.minimapPanelWidth, GAME_CONFIG.ui.minimapPanelHeight, 0x000000, 0.8)
.setStrokeStyle(1, 0xffffff, 0.9)
.setInteractive();
this.minimapGfx = this.scene.add.graphics();
this.minimapContainer.add(this.minimapBg);
this.minimapContainer.add(this.minimapGfx);
this.positionMinimap();
this.minimapContainer.setVisible(false);
}
positionMinimap() {
const cam = this.scene.cameras.main;
this.minimapContainer.setPosition(cam.width / 2, cam.height / 2);
}
toggle() {
this.minimapVisible = !this.minimapVisible;
this.minimapContainer.setVisible(this.minimapVisible);
}
isVisible(): boolean {
return this.minimapVisible;
}
render(world: World, seen: Uint8Array, visible: Uint8Array) {
this.minimapGfx.clear();
if (!world) return;
const padding = GAME_CONFIG.ui.minimapPadding;
const availableWidth = GAME_CONFIG.ui.minimapPanelWidth - padding * 2;
const availableHeight = GAME_CONFIG.ui.minimapPanelHeight - padding * 2;
const scaleX = availableWidth / world.width;
const scaleY = availableHeight / world.height;
const tileSize = Math.floor(Math.min(scaleX, scaleY));
const mapPixelWidth = world.width * tileSize;
const mapPixelHeight = world.height * tileSize;
const offsetX = -mapPixelWidth / 2;
const offsetY = -mapPixelHeight / 2;
for (let y = 0; y < world.height; y++) {
for (let x = 0; x < world.width; x++) {
const i = idx(world, x, y);
if (seen[i] !== 1) continue;
const wall = isWall(world, x, y);
const color = wall ? 0x666666 : 0x333333;
this.minimapGfx.fillStyle(color, 1);
this.minimapGfx.fillRect(offsetX + x * tileSize, offsetY + y * tileSize, tileSize, tileSize);
}
}
const ex = world.exit.x;
const ey = world.exit.y;
if (seen[idx(world, ex, ey)] === 1) {
this.minimapGfx.fillStyle(0xffd166, 1);
this.minimapGfx.fillRect(offsetX + ex * tileSize, offsetY + ey * tileSize, tileSize, tileSize);
}
const player = [...world.actors.values()].find(a => a.category === "combatant" && a.isPlayer) as CombatantActor;
if (player) {
this.minimapGfx.fillStyle(0x66ff66, 1);
this.minimapGfx.fillRect(offsetX + player.pos.x * tileSize, offsetY + player.pos.y * tileSize, tileSize, tileSize);
}
for (const a of world.actors.values()) {
if (a.category === "combatant") {
if (a.isPlayer) continue;
const i = idx(world, a.pos.x, a.pos.y);
if (visible[i] === 1) {
this.minimapGfx.fillStyle(0xff6666, 1);
this.minimapGfx.fillRect(offsetX + a.pos.x * tileSize, offsetY + a.pos.y * tileSize, tileSize, tileSize);
}
}
}
}
}

View File

@@ -147,17 +147,16 @@ describe('DungeonRenderer', () => {
renderer.initializeFloor(mockWorld);
// Add an exp_orb to the world
mockWorld.actors.set(99, {
id: 99,
isPlayer: false,
type: 'exp_orb',
pos: { x: 5, y: 5 },
speed: 0,
energy: 0
mockWorld.actors.set(2, {
id: 2,
category: "collectible",
type: "exp_orb",
pos: { x: 2, y: 1 },
expAmount: 10
});
// Make the tile visible for it to render
(renderer as any).visible[5 * mockWorld.width + 5] = 1;
(renderer as any).fovManager.visibleArray[1 * mockWorld.width + 2] = 1;
// Reset mocks
mockScene.add.sprite.mockClear();
@@ -186,17 +185,18 @@ describe('DungeonRenderer', () => {
renderer.initializeFloor(mockWorld);
// Add a rat (defined in config)
mockWorld.actors.set(100, {
id: 100,
mockWorld.actors.set(3, {
id: 3,
category: "combatant",
isPlayer: false,
type: 'rat',
pos: { x: 2, y: 2 },
speed: 100,
type: "rat",
pos: { x: 3, y: 1 },
speed: 10,
energy: 0,
stats: { hp: 10, maxHp: 10, attack: 2, defense: 0, level: 1, exp: 0, expToNextLevel: 0, statPoints: 0, skillPoints: 0, strength: 0, dexterity: 0, intelligence: 0, passiveNodes: [] }
stats: { hp: 10, maxHp: 10, attack: 2, defense: 0 } as any
});
(renderer as any).visible[2 * mockWorld.width + 2] = 1;
(renderer as any).fovManager.visibleArray[1 * mockWorld.width + 3] = 1;
mockScene.add.sprite.mockClear();
renderer.render([]);