diff --git a/src/core/config/Items.ts b/src/core/config/Items.ts new file mode 100644 index 0000000..951522d --- /dev/null +++ b/src/core/config/Items.ts @@ -0,0 +1,29 @@ +import type { Item } from "../types"; + +export const ITEMS: Record = { + "health_potion": { + id: "health_potion", + name: "Health Potion", + type: "Consumable", + icon: "potion_red", + stats: {} // Special logic for usage + }, + "iron_sword": { + id: "iron_sword", + name: "Iron Sword", + type: "Weapon", + icon: "sword_iron", + stats: { + attack: 2 + } + }, + "leather_armor": { + id: "leather_armor", + name: "Leather Armor", + type: "BodyArmour", + icon: "armor_leather", + stats: { + defense: 2 + } + } +}; diff --git a/src/core/types.ts b/src/core/types.ts index 6b29fdd..b30b65f 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -72,7 +72,8 @@ export type ItemType = | "Amulet" | "Ring" | "Belt" - | "Currency"; + | "Currency" + | "Consumable"; export type Item = { id: string; diff --git a/src/engine/world/generator.ts b/src/engine/world/generator.ts index cc8b849..a7cc8ff 100644 --- a/src/engine/world/generator.ts +++ b/src/engine/world/generator.ts @@ -2,6 +2,7 @@ import { type World, type EntityId, type RunState, type Tile, type Actor, type V import { TileType } from "../../core/terrain"; import { idx } from "./world-logic"; import { GAME_CONFIG } from "../../core/config/GameConfig"; +import { ITEMS } from "../../core/config/Items"; import { seededRandom } from "../../core/math"; import * as ROT from "rot-js"; @@ -46,7 +47,14 @@ export function generateWorld(floor: number, runState: RunState): { world: World pos: { x: playerX, y: playerY }, speed: GAME_CONFIG.player.speed, stats: { ...runState.stats }, - inventory: { gold: runState.inventory.gold, items: [...runState.inventory.items] }, + inventory: { + gold: runState.inventory.gold, + items: [ + ...runState.inventory.items, + // Add starting items for testing if empty + ...(runState.inventory.items.length === 0 ? [ITEMS["health_potion"], ITEMS["health_potion"], ITEMS["iron_sword"]] : []) + ] + }, energy: 0 }); diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index 2bea62e..6fd9de6 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -150,6 +150,54 @@ export class GameScene extends Phaser.Scene { this.commitPlayerAction({ type: "wait" }); }); + this.events.on("player-search", () => { + if (!this.awaitingPlayer) return; + if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; + + console.log("Player searching..."); + // Search takes a turn (functionally same as wait for now, but semantically distinct) + this.commitPlayerAction({ type: "wait" }); + }); + + this.events.on("use-item", (data: { itemId: string }) => { + if (!this.awaitingPlayer) return; + // Don't block item usage if inventory is open, as we might use it from there or hotbar. + // But if we use it from inventory, we might want to close inventory or update it. + + const player = this.world.actors.get(this.playerId) as CombatantActor; + if (!player || !player.inventory) return; + + if (data.itemId === "health_potion") { + // Heal logic + const healAmount = 5; + if (player.stats.hp < player.stats.maxHp) { + player.stats.hp = Math.min(player.stats.hp + healAmount, player.stats.maxHp); + + // Visuals handled by diff in stats usually? No, we need explicit heal event or simple floating text + // commitPlayerAction triggers simulation which might generate events. + // But healing from item is instant effect before turn passes? + // Or we treat it as an action. + + // Let's remove item first + const idx = player.inventory.items.findIndex(it => it.id === "health_potion"); + if (idx !== -1) { + player.inventory.items.splice(idx, 1); + + // Show visual + this.dungeonRenderer.showHeal(player.pos.x, player.pos.y, healAmount); + + // Pass turn + this.commitPlayerAction({ type: "wait" }); + this.emitUIUpdate(); + } + } else { + console.log("Already at full health"); + } + } else { + console.log("Used item:", data.itemId); + } + }); + // Zoom Control diff --git a/src/ui/GameUI.ts b/src/ui/GameUI.ts index 7e2ef0e..831bb7b 100644 --- a/src/ui/GameUI.ts +++ b/src/ui/GameUI.ts @@ -6,6 +6,7 @@ import { InventoryOverlay } from "./components/InventoryOverlay"; import { CharacterOverlay } from "./components/CharacterOverlay"; import { DeathOverlay } from "./components/DeathOverlay"; import { PersistentButtonsComponent } from "./components/PersistentButtonsComponent"; +import { QuickSlotComponent } from "./components/QuickSlotComponent"; export default class GameUI extends Phaser.Scene { private hud: HudComponent; @@ -14,6 +15,7 @@ export default class GameUI extends Phaser.Scene { private character: CharacterOverlay; private death: DeathOverlay; private persistentButtons: PersistentButtonsComponent; + private quickSlots: QuickSlotComponent; constructor() { super({ key: "GameUI" }); @@ -23,6 +25,7 @@ export default class GameUI extends Phaser.Scene { this.character = new CharacterOverlay(this); this.death = new DeathOverlay(this); this.persistentButtons = new PersistentButtonsComponent(this); + this.quickSlots = new QuickSlotComponent(this); } @@ -33,6 +36,7 @@ export default class GameUI extends Phaser.Scene { this.character.create(); this.death.create(); this.persistentButtons.create(); + this.quickSlots.create(); const gameScene = this.scene.get("GameScene"); @@ -96,5 +100,6 @@ export default class GameUI extends Phaser.Scene { this.hud.update(player.stats, floorIndex); this.inventory.update(player); this.character.update(player); + this.quickSlots.update(player); } } diff --git a/src/ui/components/InventoryOverlay.ts b/src/ui/components/InventoryOverlay.ts index 7b5daf7..c777356 100644 --- a/src/ui/components/InventoryOverlay.ts +++ b/src/ui/components/InventoryOverlay.ts @@ -81,7 +81,47 @@ export class InventoryOverlay extends OverlayComponent { } } - update(_player: CombatantActor) { - // Future: update items in slots + update(player: CombatantActor) { + if (!player.inventory) return; + + // Clear existing items from slots + this.backpackSlots.forEach(slot => { + if (slot.list.length > 1) { // 0 is bg + // Remove all children after bg + slot.removeBetween(1, undefined, true); + } + }); + + // Populate items + player.inventory.items.forEach((item, index) => { + if (index >= this.backpackSlots.length) return; + + const slot = this.backpackSlots[index]; + + let color = "#ffffff"; + let label = item.name.substring(0, 2).toUpperCase(); + + if (item.type === "Consumable") { + color = "#ff5555"; + } else if (item.type === "Weapon") { + color = "#aaaaaa"; + } else if (item.type === "BodyArmour") { + color = "#aa5500"; + } + + const txt = this.scene.add.text(0, 0, label, { + fontSize: "10px", + color: color, + fontStyle: "bold" + }).setOrigin(0.5); + + slot.add(txt); + + // Add simple tooltip on hover (console log for now) or click + slot.setInteractive(new Phaser.Geom.Rectangle(-20, -20, 40, 40), Phaser.Geom.Rectangle.Contains); + slot.on("pointerdown", () => { + console.log("Clicked item:", item); + }); + }); } } diff --git a/src/ui/components/PersistentButtonsComponent.ts b/src/ui/components/PersistentButtonsComponent.ts index 0d79d19..10df566 100644 --- a/src/ui/components/PersistentButtonsComponent.ts +++ b/src/ui/components/PersistentButtonsComponent.ts @@ -59,6 +59,16 @@ export class PersistentButtonsComponent { .setOrigin(1, 1) .setInteractive({ useHandCursor: true }); + const searchBtn = this.scene.add.text(-40, 0, "🔍", { // Offset to the left of wait button + fontSize: "24px", + color: "#ffffff", + backgroundColor: "#1a1a1a", + padding: { x: 10, y: 6 }, + fontStyle: "bold" + }) + .setOrigin(1, 1) + .setInteractive({ useHandCursor: true }); + waitBtn.on("pointerover", () => waitBtn.setBackgroundColor("#333333")); waitBtn.on("pointerout", () => waitBtn.setBackgroundColor("#1a1a1a")); waitBtn.on("pointerdown", () => { @@ -68,6 +78,18 @@ export class PersistentButtonsComponent { }); waitBtn.on("pointerup", () => waitBtn.setBackgroundColor("#333333")); + searchBtn.on("pointerover", () => searchBtn.setBackgroundColor("#333333")); + searchBtn.on("pointerout", () => searchBtn.setBackgroundColor("#1a1a1a")); + searchBtn.on("pointerdown", () => { + searchBtn.setBackgroundColor("#444444"); + // Implementing search visual logic later, for now just log + console.log("Searching..."); + const gameScene = this.scene.scene.get("GameScene"); + gameScene.events.emit("player-search"); + }); + searchBtn.on("pointerup", () => searchBtn.setBackgroundColor("#333333")); + rightContainer.add(waitBtn); + rightContainer.add(searchBtn); } } diff --git a/src/ui/components/QuickSlotComponent.ts b/src/ui/components/QuickSlotComponent.ts new file mode 100644 index 0000000..69204b5 --- /dev/null +++ b/src/ui/components/QuickSlotComponent.ts @@ -0,0 +1,124 @@ +import Phaser from "phaser"; +import type { CombatantActor, Item } from "../../core/types"; + +export class QuickSlotComponent { + private scene: Phaser.Scene; + private container!: Phaser.GameObjects.Container; + private slots: Phaser.GameObjects.Container[] = []; + private itemMap: (Item | null)[] = [null, null, null, null]; // 4 slots + private assignedIds: string[] = ["health_potion", "", "", ""]; // Default slot 1 to HP pot + + constructor(scene: Phaser.Scene) { + this.scene = scene; + } + + create() { + const { width, height } = this.scene.scale; + // Position bottom center-ish + this.container = this.scene.add.container(width / 2 - 100, height - 50); + this.container.setScrollFactor(0).setDepth(1500); + + for (let i = 0; i < 4; i++) { + const x = i * 50; + const g = this.scene.add.graphics(); + + // Slot bg + g.fillStyle(0x1a1a1a, 0.8); + g.fillRect(0, 0, 40, 40); + g.lineStyle(1, 0x555555); + g.strokeRect(0, 0, 40, 40); + + // Hotkey label + const key = this.scene.add.text(2, 2, `${i + 1}`, { + fontSize: "10px", + color: "#aaaaaa" + }); + + const slotContainer = this.scene.add.container(x, 0, [g, key]); + this.slots.push(slotContainer); + this.container.add(slotContainer); + + // Input + const hitArea = new Phaser.Geom.Rectangle(0, 0, 40, 40); + slotContainer.setInteractive(hitArea, Phaser.Geom.Rectangle.Contains); + slotContainer.on("pointerdown", () => { + this.activateSlot(i); + }); + } + + // Keyboard inputs + this.scene.input.keyboard?.on("keydown-ONE", () => this.activateSlot(0)); + this.scene.input.keyboard?.on("keydown-TWO", () => this.activateSlot(1)); + this.scene.input.keyboard?.on("keydown-THREE", () => this.activateSlot(2)); + this.scene.input.keyboard?.on("keydown-FOUR", () => this.activateSlot(3)); + } + + update(player: CombatantActor) { + if (!player.inventory) return; + + // Update slots based on inventory availability + for (let i = 0; i < 4; i++) { + const desiredId = this.assignedIds[i]; + const slot = this.slots[i]; + + // Clear previous item icon if any (children > 2, since 0=bg, 1=text) + // Clear previous item icon if any (children > 2, since 0=bg, 1=text) + if (slot.list.length > 2) { + slot.removeBetween(2, undefined, true); + } + + if (desiredId) { + const foundItem = player.inventory.items.find(it => it.id === desiredId); + this.itemMap[i] = foundItem || null; + + if (foundItem) { + // Determine color based on item ID for now since we don't have real assets loaded for everything yet + let color = 0xffffff; + let label = "?"; + + if (foundItem.id === "health_potion") { + color = 0xff3333; + label = "HP"; + } + // Draw simple icon representation + const icon = this.scene.add.text(20, 20, label, { + fontSize: "14px", + color: "#ffffff", + fontStyle: "bold" + }).setOrigin(0.5); + + // Add bg circle for color + const circle = this.scene.add.graphics(); + circle.fillStyle(color, 1); + circle.fillCircle(20, 20, 10); + + // Move text to front + slot.add(circle); + slot.add(icon); + + // Add count if stackable (future) + const count = player.inventory.items.filter(it => it.id === desiredId).length; + const countText = this.scene.add.text(38, 38, `${count}`, { + fontSize: "10px", + color: "#ffffff" + }).setOrigin(1, 1); + slot.add(countText); + } + } else { + this.itemMap[i] = null; + } + } + } + + private activateSlot(index: number) { + const item = this.itemMap[index]; + if (item) { + console.log(`Activating slot ${index + 1}: ${item.name}`); + // Emit event to GameScene to handle item usage + const gameScene = this.scene.scene.get("GameScene"); + gameScene.events.emit("use-item", { itemId: item.id }); + } else { + console.log(`Slot ${index + 1} is empty`); + } + } +}