import Phaser from "phaser"; import { type World, type EntityId, type Stats } from "../core/types"; import { GAME_CONFIG } from "../core/config/GameConfig"; 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; // Inventory/Equipment Overlay private inventoryOpen = false; private invContainer!: Phaser.GameObjects.Container; private equipmentSlots: Map = new Map(); private backpackSlots: Phaser.GameObjects.Container[] = []; // Death Screen private deathContainer!: Phaser.GameObjects.Container; private deathText!: Phaser.GameObjects.Text; private restartButton!: Phaser.GameObjects.Container; constructor() { super({ key: "GameUI" }); } create() { this.createHud(); this.createMenu(); this.createInventoryOverlay(); this.createDeathScreen(); // Listen for updates from GameScene const gameScene = this.scene.get("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("close-menu", () => { this.setMenuOpen(false); this.setInventoryOpen(false); }); } 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 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()); 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 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); } } private updateInventoryUI(world: World, playerId: EntityId) { const p = world.actors.get(playerId); if (!p) 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); if (!p || !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); 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(""); 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")); } }