Add quick slot and consumables (health and mana potions)
This commit is contained in:
29
src/core/config/Items.ts
Normal file
29
src/core/config/Items.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Item } from "../types";
|
||||||
|
|
||||||
|
export const ITEMS: Record<string, Item> = {
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -72,7 +72,8 @@ export type ItemType =
|
|||||||
| "Amulet"
|
| "Amulet"
|
||||||
| "Ring"
|
| "Ring"
|
||||||
| "Belt"
|
| "Belt"
|
||||||
| "Currency";
|
| "Currency"
|
||||||
|
| "Consumable";
|
||||||
|
|
||||||
export type Item = {
|
export type Item = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { type World, type EntityId, type RunState, type Tile, type Actor, type V
|
|||||||
import { TileType } from "../../core/terrain";
|
import { TileType } from "../../core/terrain";
|
||||||
import { idx } from "./world-logic";
|
import { idx } from "./world-logic";
|
||||||
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||||
|
import { ITEMS } from "../../core/config/Items";
|
||||||
import { seededRandom } from "../../core/math";
|
import { seededRandom } from "../../core/math";
|
||||||
import * as ROT from "rot-js";
|
import * as ROT from "rot-js";
|
||||||
|
|
||||||
@@ -46,7 +47,14 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
|||||||
pos: { x: playerX, y: playerY },
|
pos: { x: playerX, y: playerY },
|
||||||
speed: GAME_CONFIG.player.speed,
|
speed: GAME_CONFIG.player.speed,
|
||||||
stats: { ...runState.stats },
|
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
|
energy: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,54 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.commitPlayerAction({ type: "wait" });
|
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
|
// Zoom Control
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { InventoryOverlay } from "./components/InventoryOverlay";
|
|||||||
import { CharacterOverlay } from "./components/CharacterOverlay";
|
import { CharacterOverlay } from "./components/CharacterOverlay";
|
||||||
import { DeathOverlay } from "./components/DeathOverlay";
|
import { DeathOverlay } from "./components/DeathOverlay";
|
||||||
import { PersistentButtonsComponent } from "./components/PersistentButtonsComponent";
|
import { PersistentButtonsComponent } from "./components/PersistentButtonsComponent";
|
||||||
|
import { QuickSlotComponent } from "./components/QuickSlotComponent";
|
||||||
|
|
||||||
export default class GameUI extends Phaser.Scene {
|
export default class GameUI extends Phaser.Scene {
|
||||||
private hud: HudComponent;
|
private hud: HudComponent;
|
||||||
@@ -14,6 +15,7 @@ export default class GameUI extends Phaser.Scene {
|
|||||||
private character: CharacterOverlay;
|
private character: CharacterOverlay;
|
||||||
private death: DeathOverlay;
|
private death: DeathOverlay;
|
||||||
private persistentButtons: PersistentButtonsComponent;
|
private persistentButtons: PersistentButtonsComponent;
|
||||||
|
private quickSlots: QuickSlotComponent;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ key: "GameUI" });
|
super({ key: "GameUI" });
|
||||||
@@ -23,6 +25,7 @@ export default class GameUI extends Phaser.Scene {
|
|||||||
this.character = new CharacterOverlay(this);
|
this.character = new CharacterOverlay(this);
|
||||||
this.death = new DeathOverlay(this);
|
this.death = new DeathOverlay(this);
|
||||||
this.persistentButtons = new PersistentButtonsComponent(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.character.create();
|
||||||
this.death.create();
|
this.death.create();
|
||||||
this.persistentButtons.create();
|
this.persistentButtons.create();
|
||||||
|
this.quickSlots.create();
|
||||||
|
|
||||||
const gameScene = this.scene.get("GameScene");
|
const gameScene = this.scene.get("GameScene");
|
||||||
|
|
||||||
@@ -96,5 +100,6 @@ export default class GameUI extends Phaser.Scene {
|
|||||||
this.hud.update(player.stats, floorIndex);
|
this.hud.update(player.stats, floorIndex);
|
||||||
this.inventory.update(player);
|
this.inventory.update(player);
|
||||||
this.character.update(player);
|
this.character.update(player);
|
||||||
|
this.quickSlots.update(player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,47 @@ export class InventoryOverlay extends OverlayComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(_player: CombatantActor) {
|
update(player: CombatantActor) {
|
||||||
// Future: update items in slots
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,16 @@ export class PersistentButtonsComponent {
|
|||||||
.setOrigin(1, 1)
|
.setOrigin(1, 1)
|
||||||
.setInteractive({ useHandCursor: true });
|
.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("pointerover", () => waitBtn.setBackgroundColor("#333333"));
|
||||||
waitBtn.on("pointerout", () => waitBtn.setBackgroundColor("#1a1a1a"));
|
waitBtn.on("pointerout", () => waitBtn.setBackgroundColor("#1a1a1a"));
|
||||||
waitBtn.on("pointerdown", () => {
|
waitBtn.on("pointerdown", () => {
|
||||||
@@ -68,6 +78,18 @@ export class PersistentButtonsComponent {
|
|||||||
});
|
});
|
||||||
waitBtn.on("pointerup", () => waitBtn.setBackgroundColor("#333333"));
|
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(waitBtn);
|
||||||
|
rightContainer.add(searchBtn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
124
src/ui/components/QuickSlotComponent.ts
Normal file
124
src/ui/components/QuickSlotComponent.ts
Normal file
@@ -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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user