diff --git a/src/core/types.ts b/src/core/types.ts index 55a8100..6596f34 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -32,9 +32,42 @@ export type Stats = { }; +export type ItemType = + | "Weapon" + | "Offhand" + | "BodyArmour" + | "Helmet" + | "Gloves" + | "Boots" + | "Amulet" + | "Ring" + | "Belt" + | "Currency"; + +export type Item = { + id: string; + name: string; + type: ItemType; + stats?: Partial; + icon?: string; +}; + +export type Equipment = { + mainHand?: Item; + offHand?: Item; + bodyArmour?: Item; + helmet?: Item; + gloves?: Item; + boots?: Item; + amulet?: Item; + ringLeft?: Item; + ringRight?: Item; + belt?: Item; +}; + export type Inventory = { gold: number; - items: string[]; + items: Item[]; }; export type RunState = { @@ -53,6 +86,7 @@ export type Actor = { stats?: Stats; inventory?: Inventory; + equipment?: Equipment; }; export type World = { diff --git a/src/rendering/DungeonRenderer.ts b/src/rendering/DungeonRenderer.ts index 2e7a7ea..dafa43c 100644 --- a/src/rendering/DungeonRenderer.ts +++ b/src/rendering/DungeonRenderer.ts @@ -254,10 +254,13 @@ export class DungeonRenderer { } 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 === "bat" ? "bat" : "rat"; + const textureKey = a.type || "rat"; if (!sprite) { sprite = this.scene.add.sprite(0, 0, textureKey, 0); diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index fe5f101..e0a5873 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -34,6 +34,7 @@ export class GameScene extends Phaser.Scene { // Sub-systems private dungeonRenderer!: DungeonRenderer; private isMenuOpen = false; + private isInventoryOpen = false; constructor() { super("GameScene"); @@ -63,6 +64,9 @@ export class GameScene extends Phaser.Scene { this.events.on("menu-toggled", (isOpen: boolean) => { this.isMenuOpen = isOpen; }); + this.events.on("inventory-toggled", (isOpen: boolean) => { + this.isInventoryOpen = isOpen; + }); // Load initial floor this.loadFloor(1); @@ -89,10 +93,14 @@ export class GameScene extends Phaser.Scene { this.events.emit("close-menu"); this.dungeonRenderer.toggleMinimap(); }); + this.input.keyboard?.on("keydown-B", () => { + // Toggle inventory + this.events.emit("toggle-inventory"); + }); this.input.keyboard?.on("keydown-SPACE", () => { if (!this.awaitingPlayer) return; - if (this.isMenuOpen || this.dungeonRenderer.isMinimapVisible()) return; + if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; this.commitPlayerAction({ type: "wait" }); }); @@ -114,7 +122,7 @@ export class GameScene extends Phaser.Scene { // Mouse click -> compute path (only during player turn, and not while menu/minimap is open) this.input.on("pointerdown", (p: Phaser.Input.Pointer) => { if (!this.awaitingPlayer) return; - if (this.isMenuOpen || this.dungeonRenderer.isMinimapVisible()) return; + if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; const tx = Math.floor(p.worldX / TILE_SIZE); const ty = Math.floor(p.worldY / TILE_SIZE); @@ -143,7 +151,7 @@ export class GameScene extends Phaser.Scene { update() { if (!this.awaitingPlayer) return; - if (this.isMenuOpen || this.dungeonRenderer.isMinimapVisible()) return; + if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; // Auto-walk one step per turn if (this.playerPath.length >= 2) { diff --git a/src/ui/GameUI.ts b/src/ui/GameUI.ts index 421161e..cdb8956 100644 --- a/src/ui/GameUI.ts +++ b/src/ui/GameUI.ts @@ -16,6 +16,13 @@ export default class GameUI extends Phaser.Scene { 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; @@ -29,6 +36,7 @@ export default class GameUI extends Phaser.Scene { create() { this.createHud(); this.createMenu(); + this.createInventoryOverlay(); this.createDeathScreen(); // Listen for updates from GameScene @@ -38,7 +46,11 @@ export default class GameUI extends Phaser.Scene { }); gameScene.events.on("toggle-menu", () => this.toggleMenu()); - gameScene.events.on("close-menu", () => this.setMenuOpen(false)); + gameScene.events.on("toggle-inventory", () => this.toggleInventory()); + gameScene.events.on("close-menu", () => { + this.setMenuOpen(false); + this.setInventoryOpen(false); + }); } private createHud() { @@ -114,9 +126,145 @@ export default class GameUI extends Phaser.Scene { 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; @@ -210,17 +358,46 @@ export default class GameUI extends Phaser.Scene { } private toggleMap() { - // Close menu and toggle minimap + // 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) {