Add placeholder backpack and inventory UI
This commit is contained in:
@@ -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<Stats>;
|
||||||
|
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 = {
|
export type Inventory = {
|
||||||
gold: number;
|
gold: number;
|
||||||
items: string[];
|
items: Item[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RunState = {
|
export type RunState = {
|
||||||
@@ -53,6 +86,7 @@ export type Actor = {
|
|||||||
|
|
||||||
stats?: Stats;
|
stats?: Stats;
|
||||||
inventory?: Inventory;
|
inventory?: Inventory;
|
||||||
|
equipment?: Equipment;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type World = {
|
export type World = {
|
||||||
|
|||||||
@@ -254,10 +254,13 @@ export class DungeonRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isVis) continue;
|
if (!isVis) continue;
|
||||||
|
|
||||||
|
const enemyType = a.type as keyof typeof GAME_CONFIG.enemies;
|
||||||
|
if (!GAME_CONFIG.enemies[enemyType]) continue;
|
||||||
|
|
||||||
activeEnemyIds.add(a.id);
|
activeEnemyIds.add(a.id);
|
||||||
let sprite = this.enemySprites.get(a.id);
|
let sprite = this.enemySprites.get(a.id);
|
||||||
const textureKey = a.type === "bat" ? "bat" : "rat";
|
const textureKey = a.type || "rat";
|
||||||
|
|
||||||
if (!sprite) {
|
if (!sprite) {
|
||||||
sprite = this.scene.add.sprite(0, 0, textureKey, 0);
|
sprite = this.scene.add.sprite(0, 0, textureKey, 0);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
// Sub-systems
|
// Sub-systems
|
||||||
private dungeonRenderer!: DungeonRenderer;
|
private dungeonRenderer!: DungeonRenderer;
|
||||||
private isMenuOpen = false;
|
private isMenuOpen = false;
|
||||||
|
private isInventoryOpen = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("GameScene");
|
super("GameScene");
|
||||||
@@ -63,6 +64,9 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.events.on("menu-toggled", (isOpen: boolean) => {
|
this.events.on("menu-toggled", (isOpen: boolean) => {
|
||||||
this.isMenuOpen = isOpen;
|
this.isMenuOpen = isOpen;
|
||||||
});
|
});
|
||||||
|
this.events.on("inventory-toggled", (isOpen: boolean) => {
|
||||||
|
this.isInventoryOpen = isOpen;
|
||||||
|
});
|
||||||
|
|
||||||
// Load initial floor
|
// Load initial floor
|
||||||
this.loadFloor(1);
|
this.loadFloor(1);
|
||||||
@@ -89,10 +93,14 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.events.emit("close-menu");
|
this.events.emit("close-menu");
|
||||||
this.dungeonRenderer.toggleMinimap();
|
this.dungeonRenderer.toggleMinimap();
|
||||||
});
|
});
|
||||||
|
this.input.keyboard?.on("keydown-B", () => {
|
||||||
|
// Toggle inventory
|
||||||
|
this.events.emit("toggle-inventory");
|
||||||
|
});
|
||||||
|
|
||||||
this.input.keyboard?.on("keydown-SPACE", () => {
|
this.input.keyboard?.on("keydown-SPACE", () => {
|
||||||
if (!this.awaitingPlayer) return;
|
if (!this.awaitingPlayer) return;
|
||||||
if (this.isMenuOpen || this.dungeonRenderer.isMinimapVisible()) return;
|
if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return;
|
||||||
this.commitPlayerAction({ type: "wait" });
|
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)
|
// Mouse click -> compute path (only during player turn, and not while menu/minimap is open)
|
||||||
this.input.on("pointerdown", (p: Phaser.Input.Pointer) => {
|
this.input.on("pointerdown", (p: Phaser.Input.Pointer) => {
|
||||||
if (!this.awaitingPlayer) return;
|
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 tx = Math.floor(p.worldX / TILE_SIZE);
|
||||||
const ty = Math.floor(p.worldY / TILE_SIZE);
|
const ty = Math.floor(p.worldY / TILE_SIZE);
|
||||||
@@ -143,7 +151,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
if (!this.awaitingPlayer) return;
|
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
|
// Auto-walk one step per turn
|
||||||
if (this.playerPath.length >= 2) {
|
if (this.playerPath.length >= 2) {
|
||||||
|
|||||||
181
src/ui/GameUI.ts
181
src/ui/GameUI.ts
@@ -16,6 +16,13 @@ export default class GameUI extends Phaser.Scene {
|
|||||||
private menuBg!: Phaser.GameObjects.Rectangle;
|
private menuBg!: Phaser.GameObjects.Rectangle;
|
||||||
private menuButton!: Phaser.GameObjects.Container;
|
private menuButton!: Phaser.GameObjects.Container;
|
||||||
private mapButton!: 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<string, Phaser.GameObjects.Container> = new Map();
|
||||||
|
private backpackSlots: Phaser.GameObjects.Container[] = [];
|
||||||
|
|
||||||
// Death Screen
|
// Death Screen
|
||||||
private deathContainer!: Phaser.GameObjects.Container;
|
private deathContainer!: Phaser.GameObjects.Container;
|
||||||
@@ -29,6 +36,7 @@ export default class GameUI extends Phaser.Scene {
|
|||||||
create() {
|
create() {
|
||||||
this.createHud();
|
this.createHud();
|
||||||
this.createMenu();
|
this.createMenu();
|
||||||
|
this.createInventoryOverlay();
|
||||||
this.createDeathScreen();
|
this.createDeathScreen();
|
||||||
|
|
||||||
// Listen for updates from GameScene
|
// 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("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() {
|
private createHud() {
|
||||||
@@ -114,9 +126,145 @@ export default class GameUI extends Phaser.Scene {
|
|||||||
placePanel();
|
placePanel();
|
||||||
this.scale.on("resize", 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);
|
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() {
|
private createDeathScreen() {
|
||||||
const cam = this.cameras.main;
|
const cam = this.cameras.main;
|
||||||
const panelW = GAME_CONFIG.ui.menuPanelWidth + 40;
|
const panelW = GAME_CONFIG.ui.menuPanelWidth + 40;
|
||||||
@@ -210,17 +358,46 @@ export default class GameUI extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private toggleMap() {
|
private toggleMap() {
|
||||||
// Close menu and toggle minimap
|
// Close all and toggle minimap
|
||||||
this.setMenuOpen(false);
|
this.setMenuOpen(false);
|
||||||
|
this.setInventoryOpen(false);
|
||||||
const gameScene = this.scene.get("GameScene");
|
const gameScene = this.scene.get("GameScene");
|
||||||
gameScene.events.emit("toggle-minimap");
|
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) {
|
private updateUI(world: World, playerId: EntityId, floorIndex: number) {
|
||||||
this.updateHud(world, playerId, floorIndex);
|
this.updateHud(world, playerId, floorIndex);
|
||||||
if (this.menuOpen) {
|
if (this.menuOpen) {
|
||||||
this.updateMenuText(world, playerId, floorIndex);
|
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) {
|
private updateHud(world: World, playerId: EntityId, floorIndex: number) {
|
||||||
|
|||||||
Reference in New Issue
Block a user