Another refactor

This commit is contained in:
Peter Stockings
2026-01-05 13:24:56 +11:00
parent ac86d612e2
commit ce68470ab1
17 changed files with 853 additions and 801 deletions

View File

@@ -1,699 +1,100 @@
import Phaser from "phaser";
import { type World, type EntityId, type Stats, type CombatantActor } from "../core/types";
import { GAME_CONFIG } from "../core/config/GameConfig";
import { type World, type EntityId, type CombatantActor, type Stats } from "../core/types";
import { HudComponent } from "./components/HudComponent";
import { MenuComponent } from "./components/MenuComponent";
import { InventoryOverlay } from "./components/InventoryOverlay";
import { CharacterOverlay } from "./components/CharacterOverlay";
import { DeathOverlay } from "./components/DeathOverlay";
import { PersistentButtonsComponent } from "./components/PersistentButtonsComponent";
export default class GameUI extends Phaser.Scene {
// HUD
private floorText!: Phaser.GameObjects.Text;
private healthBar!: Phaser.GameObjects.Graphics;
private expBar!: Phaser.GameObjects.Graphics;
// Menu
private menuOpen = false;
private menuContainer!: Phaser.GameObjects.Container;
private menuText!: Phaser.GameObjects.Text;
private menuBg!: Phaser.GameObjects.Rectangle;
private menuButton!: Phaser.GameObjects.Container;
private mapButton!: Phaser.GameObjects.Container;
private backpackButton!: Phaser.GameObjects.Container;
private characterButton!: Phaser.GameObjects.Container;
// Inventory/Equipment Overlay
private inventoryOpen = false;
private invContainer!: Phaser.GameObjects.Container;
private equipmentSlots: Map<string, Phaser.GameObjects.Container> = new Map();
private backpackSlots: Phaser.GameObjects.Container[] = [];
// Character Overlay
private characterOpen = false;
private charContainer!: Phaser.GameObjects.Container;
private attrText!: Phaser.GameObjects.Text;
private skillPointsText!: Phaser.GameObjects.Text;
private statPointsText!: Phaser.GameObjects.Text;
private charStatsText!: Phaser.GameObjects.Text;
// Death Screen
private deathContainer!: Phaser.GameObjects.Container;
private deathText!: Phaser.GameObjects.Text;
private restartButton!: Phaser.GameObjects.Container;
private hud: HudComponent;
private menu: MenuComponent;
private inventory: InventoryOverlay;
private character: CharacterOverlay;
private death: DeathOverlay;
private persistentButtons: PersistentButtonsComponent;
constructor() {
super({ key: "GameUI" });
this.hud = new HudComponent(this);
this.menu = new MenuComponent(this);
this.inventory = new InventoryOverlay(this);
this.character = new CharacterOverlay(this);
this.death = new DeathOverlay(this);
this.persistentButtons = new PersistentButtonsComponent(this);
}
create() {
this.createHud();
this.createMenu();
this.createInventoryOverlay();
this.createCharacterOverlay();
this.createDeathScreen();
// Listen for updates from GameScene
create() {
this.hud.create();
this.menu.create();
this.inventory.create();
this.character.create();
this.death.create();
this.persistentButtons.create();
const gameScene = this.scene.get("GameScene");
// Listen for updates from GameScene
gameScene.events.on("update-ui", (data: { world: World; playerId: EntityId; floorIndex: number }) => {
this.updateUI(data.world, data.playerId, data.floorIndex);
});
gameScene.events.on("toggle-menu", () => this.toggleMenu());
gameScene.events.on("toggle-inventory", () => this.toggleInventory());
gameScene.events.on("toggle-character", () => this.toggleCharacter());
gameScene.events.on("toggle-menu", () => {
this.menu.toggle();
this.emitMenuStates();
});
gameScene.events.on("toggle-inventory", () => {
const open = this.inventory.toggle();
if (open) {
this.menu.setVisible(false);
this.character.setVisible(false);
}
this.emitMenuStates();
});
gameScene.events.on("toggle-character", () => {
const open = this.character.toggle();
if (open) {
this.menu.setVisible(false);
this.inventory.setVisible(false);
}
this.emitMenuStates();
});
gameScene.events.on("close-menu", () => {
this.setMenuOpen(false);
this.setInventoryOpen(false);
this.setCharacterOpen(false);
this.menu.setVisible(false);
this.inventory.setVisible(false);
this.character.setVisible(false);
this.emitMenuStates();
});
gameScene.events.on("restart-game", () => {
this.death.hide();
});
}
private createHud() {
this.floorText = this.add.text(10, 10, "Floor 1", {
fontSize: "20px",
color: "#ffffff",
fontStyle: "bold"
}).setDepth(100);
this.healthBar = this.add.graphics().setDepth(100);
this.expBar = this.add.graphics().setDepth(100);
private emitMenuStates() {
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("menu-toggled", this.menu.isOpen);
gameScene.events.emit("inventory-toggled", this.inventory.isOpen);
gameScene.events.emit("character-toggled", this.character.isOpen);
}
private createMenu() {
const cam = this.cameras.main;
const btnW = 90;
const btnH = 28;
// Menu Button
const btnBg = this.add.rectangle(0, 0, btnW, btnH, 0x000000, 0.6).setStrokeStyle(1, 0xffffff, 0.8);
const btnLabel = this.add.text(0, 0, "Menu", { fontSize: "14px", color: "#ffffff" }).setOrigin(0.5);
this.menuButton = this.add.container(0, 0, [btnBg, btnLabel]);
this.menuButton.setDepth(1000);
const placeButton = () => {
this.menuButton.setPosition(cam.width - btnW / 2 - 10, btnH / 2 + 10);
};
placeButton();
this.scale.on("resize", placeButton);
btnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => this.toggleMenu());
// Map Button (left of Menu button)
const mapBtnBg = this.add.rectangle(0, 0, btnW, btnH, 0x000000, 0.6).setStrokeStyle(1, 0xffffff, 0.8);
const mapBtnLabel = this.add.text(0, 0, "Map", { fontSize: "14px", color: "#ffffff" }).setOrigin(0.5);
this.mapButton = this.add.container(0, 0, [mapBtnBg, mapBtnLabel]);
this.mapButton.setDepth(1000);
const placeMapButton = () => {
this.mapButton.setPosition(cam.width - btnW / 2 - 10 - btnW - 5, btnH / 2 + 10);
};
placeMapButton();
this.scale.on("resize", placeMapButton);
mapBtnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => this.toggleMap());
// Panel (center)
const panelW = GAME_CONFIG.ui.menuPanelWidth;
const panelH = GAME_CONFIG.ui.menuPanelHeight;
this.menuBg = this.add
.rectangle(0, 0, panelW, panelH, 0x000000, 0.8)
.setStrokeStyle(1, 0xffffff, 0.9)
.setInteractive(); // capture clicks
this.menuText = this.add
.text(-panelW / 2 + 14, -panelH / 2 + 12, "", {
fontSize: "14px",
color: "#ffffff",
wordWrap: { width: panelW - 28 }
})
.setOrigin(0, 0);
this.menuContainer = this.add.container(0, 0, [this.menuBg, this.menuText]);
this.menuContainer.setDepth(1001);
const placePanel = () => {
this.menuContainer.setPosition(cam.width / 2, cam.height / 2);
};
placePanel();
this.scale.on("resize", placePanel);
// Backpack Button (Bottom Left)
const bpBtnBg = this.add.rectangle(0, 0, btnW, btnH, 0x000000, 0.6).setStrokeStyle(1, 0xffffff, 0.8);
const bpBtnLabel = this.add.text(0, 0, "Backpack", { fontSize: "14px", color: "#ffffff" }).setOrigin(0.5);
this.backpackButton = this.add.container(0, 0, [bpBtnBg, bpBtnLabel]);
this.backpackButton.setDepth(1000);
const placeBpButton = () => {
this.backpackButton.setPosition(btnW / 2 + 10, cam.height - btnH / 2 - 10);
};
placeBpButton();
this.scale.on("resize", placeBpButton);
bpBtnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => this.toggleInventory());
// Character Button (Right of Backpack)
const charBtnBg = this.add.rectangle(0, 0, btnW, btnH, 0x000000, 0.6).setStrokeStyle(1, 0xffffff, 0.8);
const charBtnLabel = this.add.text(0, 0, "Character", { fontSize: "14px", color: "#ffffff" }).setOrigin(0.5);
this.characterButton = this.add.container(0, 0, [charBtnBg, charBtnLabel]);
this.characterButton.setDepth(1000);
const placeCharButton = () => {
this.characterButton.setPosition(btnW / 2 + 10 + btnW + 5, cam.height - btnH / 2 - 10);
};
placeCharButton();
this.scale.on("resize", placeCharButton);
charBtnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => this.toggleCharacter());
this.setMenuOpen(false);
}
private createInventoryOverlay() {
const cam = this.cameras.main;
const panelW = 850;
const panelH = 550;
// Premium Background with Gradient
const bg = this.add.graphics();
bg.fillStyle(0x000000, 0.9);
bg.fillRect(-panelW / 2, -panelH / 2, panelW, panelH);
// Make the area interactive to capture clicks
const hitArea = new Phaser.Geom.Rectangle(-panelW / 2, -panelH / 2, panelW, panelH);
this.add.zone(0, 0, panelW, panelH).setInteractive(hitArea, Phaser.Geom.Rectangle.Contains);
bg.lineStyle(3, 0x443322, 1);
bg.strokeRect(-panelW / 2, -panelH / 2, panelW, panelH);
// Subtle inner border
bg.lineStyle(1, 0x887766, 0.3);
bg.strokeRect(-panelW / 2 + 5, -panelH / 2 + 5, panelW - 10, panelH - 10);
const title = this.add.text(0, -panelH / 2 + 25, "INVENTORY", {
fontSize: "28px",
color: "#d4af37",
fontStyle: "bold",
shadow: { blur: 2, color: "#000000", fill: true, offsetY: 2 }
}).setOrigin(0.5);
this.invContainer = this.add.container(0, 0, [bg, title]);
this.invContainer.setDepth(1001);
// --- Equipment Section (PoE Style) ---
const eqX = -200;
const eqY = 10;
const createSlot = (x: number, y: number, w: number, h: number, label: string, key: string) => {
const g = this.add.graphics();
// Outer border
g.lineStyle(2, 0x444444, 1);
g.strokeRect(-w / 2, -h / 2, w, h);
// Inner gradient-like background
g.fillStyle(0x1a1a1a, 1);
g.fillRect(-w / 2 + 1, -h / 2 + 1, w - 2, h - 2);
// Bottom highlight
g.lineStyle(1, 0x333333, 1);
g.lineBetween(-w / 2 + 2, h / 2 - 2, w / 2 - 2, h / 2 - 2);
const txt = this.add.text(0, 0, label, { fontSize: "11px", color: "#666666", fontStyle: "bold" }).setOrigin(0.5);
const container = this.add.container(x, y, [g, txt]);
this.equipmentSlots.set(key, container);
this.invContainer.add(container);
return container;
};
// Sizes based on PoE proportions
const sSmall = 54;
const sMed = 70;
const sLargeW = 90;
const sLargeH = 160;
// Central Column
createSlot(eqX, eqY - 140, sMed, sMed, "Head", "helmet"); // Helmet
createSlot(eqX, eqY - 20, sLargeW, 130, "Body", "bodyArmour"); // Body Armour
createSlot(eqX, eqY + 80, 100, 36, "Belt", "belt"); // Belt
// Sides (Large)
createSlot(eqX - 140, eqY - 50, sLargeW, sLargeH, "Main Hand", "mainHand"); // Main Hand
createSlot(eqX + 140, eqY - 50, sLargeW, sLargeH, "Off Hand", "offHand"); // Off Hand
// Inner Column Left (Ring)
createSlot(eqX - 80, eqY - 30, sSmall, sSmall, "Ring", "ringLeft");
// Inner Column Right (Ring)
createSlot(eqX + 80, eqY - 30, sSmall, sSmall, "Ring", "ringRight");
// Bottom Corners
createSlot(eqX - 100, eqY + 70, sMed, sMed, "Hands", "gloves");
createSlot(eqX + 100, eqY + 70, sMed, sMed, "Boots", "boots");
// --- Backpack Section (Right Side) ---
const bpX = 120;
const bpY = -panelH / 2 + 100;
const rows = 10;
const cols = 6;
const bpSlotSize = 42;
const bpTitle = this.add.text(bpX + (cols * (bpSlotSize + 4)) / 2 - 20, bpY - 40, "BACKPACK", {
fontSize: "18px",
color: "#d4af37",
fontStyle: "bold"
}).setOrigin(0.5);
this.invContainer.add(bpTitle);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = bpX + c * (bpSlotSize + 4);
const y = bpY + r * (bpSlotSize + 4);
const g = this.add.graphics();
g.lineStyle(1, 0x333333, 1);
g.strokeRect(-bpSlotSize / 2, -bpSlotSize / 2, bpSlotSize, bpSlotSize);
g.fillStyle(0x0c0c0c, 1);
g.fillRect(-bpSlotSize / 2 + 0.5, -bpSlotSize / 2 + 0.5, bpSlotSize - 1, bpSlotSize - 1);
const container = this.add.container(x, y, [g]);
this.invContainer.add(container);
this.backpackSlots.push(container);
}
}
const placeInv = () => {
this.invContainer.setPosition(cam.width / 2, cam.height / 2);
};
placeInv();
this.scale.on("resize", placeInv);
this.setInventoryOpen(false);
}
private createDeathScreen() {
const cam = this.cameras.main;
const panelW = GAME_CONFIG.ui.menuPanelWidth + 40;
const panelH = GAME_CONFIG.ui.menuPanelHeight + 60;
const bg = this.add
.rectangle(0, 0, cam.width, cam.height, 0x000000, 0.85)
.setOrigin(0)
.setInteractive();
const panel = this.add
.rectangle(cam.width / 2, cam.height / 2, panelW, panelH, 0x000000, 0.9)
.setStrokeStyle(2, 0xff3333, 1);
const title = this.add
.text(cam.width / 2, cam.height / 2 - panelH / 2 + 30, "YOU HAVE PERISHED", {
fontSize: "28px",
color: "#ff3333",
fontStyle: "bold"
})
.setOrigin(0.5);
this.deathText = this.add
.text(cam.width / 2, cam.height / 2 - 20, "", {
fontSize: "16px",
color: "#ffffff",
align: "center",
lineSpacing: 10
})
.setOrigin(0.5);
// Restart Button
const btnW = 160;
const btnH = 40;
const btnBg = this.add.rectangle(0, 0, btnW, btnH, 0x440000, 1).setStrokeStyle(2, 0xff3333, 1);
const btnLabel = this.add.text(0, 0, "NEW GAME", { fontSize: "18px", color: "#ffffff", fontStyle: "bold" }).setOrigin(0.5);
this.restartButton = this.add.container(cam.width / 2, cam.height / 2 + panelH / 2 - 50, [btnBg, btnLabel]);
btnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => {
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("restart-game");
this.hideDeathScreen();
});
this.deathContainer = this.add.container(0, 0, [bg, panel, title, this.deathText, this.restartButton]);
this.deathContainer.setDepth(2000);
this.deathContainer.setVisible(false);
}
showDeathScreen(data: { floor: number; gold: number; stats: Stats }) {
const lines = [
`Dungeon Floor: ${data.floor}`,
`Gold Collected: ${data.gold}`,
"",
`Experience gained: ${data.stats.exp}`,
`Final HP: 0 / ${data.stats.maxHp}`,
`Attack: ${data.stats.attack}`,
`Defense: ${data.stats.defense}`
];
this.deathText.setText(lines.join("\n"));
this.deathContainer.setVisible(true);
// Disable other UI interactions
this.menuButton.setVisible(false);
this.mapButton.setVisible(false);
}
hideDeathScreen() {
this.deathContainer.setVisible(false);
this.menuButton.setVisible(true);
this.mapButton.setVisible(true);
}
private toggleMenu() {
this.setMenuOpen(!this.menuOpen);
// Request UI update when menu is opened to populate the text
if (this.menuOpen) {
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("request-ui-update");
}
}
private setMenuOpen(open: boolean) {
this.menuOpen = open;
this.menuContainer.setVisible(open);
// Notify GameScene back?
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("menu-toggled", open);
}
private toggleMap() {
// Close all and toggle minimap
this.setMenuOpen(false);
this.setInventoryOpen(false);
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("toggle-minimap");
}
private toggleInventory() {
this.setInventoryOpen(!this.inventoryOpen);
if (this.inventoryOpen) {
this.setMenuOpen(false);
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("request-ui-update");
}
}
private setInventoryOpen(open: boolean) {
this.inventoryOpen = open;
this.invContainer.setVisible(open);
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("inventory-toggled", open);
}
private toggleCharacter() {
this.setCharacterOpen(!this.characterOpen);
if (this.characterOpen) {
this.setMenuOpen(false);
this.setInventoryOpen(false);
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("request-ui-update");
}
}
private setCharacterOpen(open: boolean) {
this.characterOpen = open;
this.charContainer.setVisible(open);
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("character-toggled", open);
}
private createCharacterOverlay() {
const cam = this.cameras.main;
const panelW = 850;
const panelH = 550;
const bg = this.add.graphics();
bg.fillStyle(0x000000, 0.9);
bg.fillRect(-panelW / 2, -panelH / 2, panelW, panelH);
// Capture clicks
const hitArea = new Phaser.Geom.Rectangle(-panelW / 2, -panelH / 2, panelW, panelH);
this.add.zone(0, 0, panelW, panelH).setInteractive(hitArea, Phaser.Geom.Rectangle.Contains);
bg.lineStyle(3, 0x443322, 1);
bg.strokeRect(-panelW / 2, -panelH / 2, panelW, panelH);
bg.lineStyle(1, 0x887766, 0.3);
bg.strokeRect(-panelW / 2 + 5, -panelH / 2 + 5, panelW - 10, panelH - 10);
const title = this.add.text(0, -panelH / 2 + 25, "CHARACTER", {
fontSize: "28px",
color: "#d4af37",
fontStyle: "bold",
shadow: { blur: 2, color: "#000000", fill: true, offsetY: 2 }
}).setOrigin(0.5);
this.charContainer = this.add.container(0, 0, [bg, title]);
this.charContainer.setDepth(1001);
// --- Attributes Section ---
const attrX = -300;
const attrY = -145;
const treeX = 50;
const treeY = 0;
const attrTitle = this.add.text(attrX, attrY - 50, "ATTRIBUTES", { fontSize: "20px", color: "#d4af37", fontStyle: "bold" }).setOrigin(0.5);
this.charContainer.add(attrTitle);
this.attrText = this.add.text(attrX - 20, attrY + 30, "", { fontSize: "16px", color: "#ffffff", lineSpacing: 40 }).setOrigin(1, 0.5);
this.charContainer.add(this.attrText);
// Stat allocation buttons
const statsNames = ["strength", "dexterity", "intelligence"];
statsNames.forEach((name, i) => {
const btn = this.add.text(attrX + 50, attrY - 25 + i * 56, "[ + ]", { fontSize: "16px", color: "#00ff00" }).setOrigin(0, 0.5);
btn.setInteractive({ useHandCursor: true }).on("pointerdown", () => {
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("allocate-stat", name);
});
this.charContainer.add(btn);
});
this.statPointsText = this.add.text(attrX, attrY + 150, "Stat Points: 0", { fontSize: "16px", color: "#d4af37" }).setOrigin(0.5);
this.charContainer.add(this.statPointsText);
this.skillPointsText = this.add.text(treeX, panelH / 2 - 40, "Skill Points: 0", { fontSize: "20px", color: "#d4af37", fontStyle: "bold" }).setOrigin(0.5);
this.charContainer.add(this.skillPointsText);
// Derived Stats
this.charStatsText = this.add.text(-attrX, 0, "", { fontSize: "14px", color: "#ffffff", lineSpacing: 10 }).setOrigin(0.5);
this.charContainer.add(this.charStatsText);
// --- Skill Tree Section ---
const treeTitle = this.add.text(treeX, -panelH / 2 + 80, "PASSIVE SKILL TREE", { fontSize: "20px", color: "#d4af37", fontStyle: "bold" }).setOrigin(0.5);
this.charContainer.add(treeTitle);
// Simple Grid for Tree
const nodes = [
{ id: "off_1", label: "Martial Arts", x: treeX - 100, y: treeY - 100, color: 0xff4444 },
{ id: "off_2", label: "Brutality", x: treeX - 100, y: treeY + 100, color: 0xcc0000 },
{ id: "def_1", label: "Thick Skin", x: treeX + 100, y: treeY - 100, color: 0x44ff44 },
{ id: "def_2", label: "Juggernaut", x: treeX + 100, y: treeY + 100, color: 0x00cc00 },
{ id: "util_1", label: "Fleetfoot", x: treeX, y: treeY - 150, color: 0x4444ff },
{ id: "util_2", label: "Cunning", x: treeX, y: treeY + 150, color: 0x0000cc },
];
// Connections
const connections = [
["off_1", "off_2"], ["def_1", "def_2"],
["util_1", "off_1"], ["util_1", "def_1"],
["util_2", "off_2"], ["util_2", "def_2"]
];
const treeLines = this.add.graphics();
treeLines.lineStyle(2, 0x333333, 1);
connections.forEach(conn => {
const n1 = nodes.find(n => n.id === conn[0])!;
const n2 = nodes.find(n => n.id === conn[1])!;
treeLines.lineBetween(n1.x, n1.y, n2.x, n2.y);
});
this.charContainer.add(treeLines);
treeLines.setDepth(-1); // Behind nodes
nodes.forEach(n => {
const circle = this.add.circle(n.x, n.y, 25, 0x1a1a1a).setStrokeStyle(2, n.color);
const label = this.add.text(n.x, n.y + 35, n.label, { fontSize: "12px", color: "#ffffff" }).setOrigin(0.5);
circle.setInteractive({ useHandCursor: true }).on("pointerdown", () => {
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("allocate-passive", n.id);
});
this.charContainer.add([circle, label]);
});
const placeChar = () => {
this.charContainer.setPosition(cam.width / 2, cam.height / 2);
};
placeChar();
this.scale.on("resize", placeChar);
this.setCharacterOpen(false);
this.death.show(data);
}
private updateUI(world: World, playerId: EntityId, floorIndex: number) {
this.updateHud(world, playerId, floorIndex);
if (this.menuOpen) {
this.updateMenuText(world, playerId, floorIndex);
}
if (this.inventoryOpen) {
this.updateInventoryUI(world, playerId);
}
if (this.characterOpen) {
this.updateCharacterUI(world, playerId);
}
}
const player = world.actors.get(playerId) as CombatantActor;
if (!player) return;
private updateCharacterUI(world: World, playerId: EntityId) {
const p = world.actors.get(playerId) as CombatantActor;
if (!p || p.category !== "combatant" || !p.stats) return;
const s = p.stats;
this.attrText.setText(`STR: ${s.strength}\nDEX: ${s.dexterity}\nINT: ${s.intelligence}`);
this.statPointsText.setText(`Unspent Points: ${s.statPoints}`);
this.skillPointsText.setText(`PASSIVE SKILL POINTS: ${s.skillPoints}`);
const statsLines = [
"SECONDARY STATS",
"",
`Max HP: ${s.maxHp}`,
`Attack: ${s.attack}`,
`Defense: ${s.defense}`,
`Speed: ${p.speed}`,
"",
`Accuracy: ${s.accuracy}%`,
`Crit Chance: ${s.critChance}%`,
`Crit Mult: ${s.critMultiplier}%`,
`Evasion: ${s.evasion}%`,
`Block: ${s.blockChance}%`,
`Lifesteal: ${s.lifesteal}%`,
`Luck: ${s.luck}`,
"",
`Passive Nodes: ${s.passiveNodes.length > 0 ? s.passiveNodes.join(", ") : "(none)"}`
];
this.charStatsText.setText(statsLines.join("\n"));
}
private updateInventoryUI(world: World, playerId: EntityId) {
const p = world.actors.get(playerId) as CombatantActor;
if (!p || p.category !== "combatant") return;
// Clear existing item icons/text from slots if needed (future refinement)
// For now we just show names or placeholders
}
private updateHud(world: World, playerId: EntityId, floorIndex: number) {
this.floorText.setText(`Floor ${floorIndex}`);
const p = world.actors.get(playerId) as CombatantActor;
if (!p || p.category !== "combatant" || !p.stats) return;
const barX = 40;
const barY = 40;
const barW = 180;
const barH = 16;
this.healthBar.clear();
// Heart Icon
const iconX = 20;
const iconY = barY + barH / 2;
this.healthBar.fillStyle(0xff0000, 1);
// Draw simple heart
this.healthBar.fillCircle(iconX - 4, iconY - 2, 5);
this.healthBar.fillCircle(iconX + 4, iconY - 2, 5);
this.healthBar.fillTriangle(iconX - 9, iconY - 1, iconX + 9, iconY - 1, iconX, iconY + 9);
this.healthBar.fillStyle(0x444444, 1);
this.healthBar.fillRect(barX, barY, barW, barH);
const hp = Math.max(0, p.stats.hp);
const maxHp = Math.max(1, p.stats.maxHp);
const pct = Phaser.Math.Clamp(hp / maxHp, 0, 1);
const fillW = Math.floor(barW * pct);
this.healthBar.fillStyle(0xff0000, 1);
this.healthBar.fillRect(barX, barY, fillW, barH);
this.healthBar.lineStyle(2, 0xffffff, 1);
this.healthBar.strokeRect(barX, barY, barW, barH);
// EXP Bar
const expY = barY + barH + 6;
const expH = 10;
this.expBar.clear();
// EXP Icon (Star/Orb)
const expIconY = expY + expH / 2;
this.expBar.fillStyle(GAME_CONFIG.rendering.expOrbColor, 1);
this.expBar.fillCircle(iconX, expIconY, 6);
this.expBar.fillStyle(0xffffff, 0.5);
this.expBar.fillCircle(iconX - 2, expIconY - 2, 2);
this.expBar.fillStyle(0x444444, 1);
this.expBar.fillRect(barX, expY, barW, expH);
const exp = p.stats.exp;
const nextExp = Math.max(1, p.stats.expToNextLevel);
const expPct = Phaser.Math.Clamp(exp / nextExp, 0, 1);
const expFillW = Math.floor(barW * expPct);
this.expBar.fillStyle(GAME_CONFIG.rendering.expOrbColor, 1);
this.expBar.fillRect(barX, expY, expFillW, expH);
this.expBar.lineStyle(1, 0xffffff, 0.8);
this.expBar.strokeRect(barX, expY, barW, expH);
}
private updateMenuText(world: World, playerId: EntityId, _floorIndex: number) {
const p = world.actors.get(playerId) as CombatantActor;
if (!p || p.category !== "combatant") return;
const stats = p.stats;
const inv = p.inventory;
const lines: string[] = [];
lines.push(`Level ${stats?.level ?? 1}`);
lines.push("");
lines.push("Stats");
lines.push(` HP: ${stats?.hp ?? 0}/${stats?.maxHp ?? 0}`);
lines.push(` EXP: ${stats?.exp ?? 0}/${stats?.expToNextLevel ?? 0}`);
lines.push(` Attack: ${stats?.attack ?? 0}`);
lines.push(` Defense: ${stats?.defense ?? 0}`);
lines.push(` Speed: ${p?.speed ?? 0}`);
lines.push(` Crit: ${stats?.critChance ?? 0}%`);
lines.push(` Crit x: ${stats?.critMultiplier ?? 0}%`);
lines.push(` Accuracy: ${stats?.accuracy ?? 0}%`);
lines.push(` Evasion: ${stats?.evasion ?? 0}%`);
lines.push("");
lines.push("Inventory");
lines.push(` Gold: ${inv?.gold ?? 0}`);
lines.push(` Items: ${(inv?.items?.length ?? 0) === 0 ? "(none)" : ""}`);
if (inv?.items?.length) {
for (const it of inv.items) lines.push(` - ${it}`);
}
lines.push("");
lines.push("Hotkeys: I to toggle, Esc to close");
this.menuText.setText(lines.join("\n"));
this.hud.update(player.stats, floorIndex);
this.inventory.update(player);
this.character.update(player);
}
}