Refactor codebase

This commit is contained in:
Peter Stockings
2026-01-04 15:56:18 +11:00
parent 3785885abe
commit bfe5ebae8c
18 changed files with 380 additions and 191 deletions

197
src/ui/GameUI.ts Normal file
View File

@@ -0,0 +1,197 @@
import Phaser from "phaser";
import { type World, type EntityId } from "../core/types";
import { GAME_CONFIG } from "../core/config/GameConfig";
export default class GameUI extends Phaser.Scene {
// HUD
private levelText!: Phaser.GameObjects.Text;
private healthBar!: 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;
constructor() {
super({ key: "GameUI" });
}
create() {
this.createHud();
this.createMenu();
// Listen for updates from GameScene
const gameScene = this.scene.get("GameScene");
gameScene.events.on("update-ui", (data: { world: World; playerId: EntityId; levelIndex: number }) => {
this.updateUI(data.world, data.playerId, data.levelIndex);
});
gameScene.events.on("toggle-menu", () => this.toggleMenu());
gameScene.events.on("close-menu", () => this.setMenuOpen(false));
}
private createHud() {
this.levelText = this.add.text(10, 10, "Level 1", {
fontSize: "20px",
color: "#ffffff",
fontStyle: "bold"
}).setDepth(100);
this.healthBar = this.add.graphics().setDepth(100);
}
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);
this.setMenuOpen(false);
}
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 menu and toggle minimap
this.setMenuOpen(false);
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("toggle-minimap");
}
private updateUI(world: World, playerId: EntityId, levelIndex: number) {
this.updateHud(world, playerId, levelIndex);
if (this.menuOpen) {
this.updateMenuText(world, playerId, levelIndex);
}
}
private updateHud(world: World, playerId: EntityId, levelIndex: number) {
this.levelText.setText(`Level ${levelIndex}`);
const p = world.actors.get(playerId);
if (!p || !p.stats) return;
const barX = 10;
const barY = 40;
const barW = 200;
const barH = 16;
this.healthBar.clear();
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);
}
private updateMenuText(world: World, playerId: EntityId, levelIndex: number) {
const p = world.actors.get(playerId);
const stats = p?.stats;
const inv = p?.inventory;
const lines: string[] = [];
lines.push(`Level ${levelIndex}`);
lines.push("");
lines.push("Stats");
lines.push(` HP: ${stats?.hp ?? 0}/${stats?.maxHp ?? 0}`);
lines.push(` Attack: ${stats?.attack ?? 0}`);
lines.push(` Defense: ${stats?.defense ?? 0}`);
lines.push(` Speed: ${p?.speed ?? 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"));
}
}