194 lines
5.8 KiB
TypeScript
194 lines
5.8 KiB
TypeScript
import type { World, CombatantActor, Item, ItemDropActor, Vec2 } from "../../core/types";
|
|
import { EntityAccessor } from "../../engine/EntityAccessor";
|
|
import { type ECSWorld } from "../../engine/ecs/World";
|
|
import { EntityBuilder } from "../../engine/ecs/EntityBuilder";
|
|
|
|
/**
|
|
* Result of attempting to use an item
|
|
*/
|
|
export interface ItemUseResult {
|
|
success: boolean;
|
|
consumed: boolean;
|
|
message?: string;
|
|
}
|
|
|
|
/**
|
|
* Manages item-related operations including spawning, pickup, and usage.
|
|
* Extracted from GameScene to centralize item logic and reduce complexity.
|
|
*/
|
|
export class ItemManager {
|
|
private world: World;
|
|
private entityAccessor: EntityAccessor;
|
|
private ecsWorld?: ECSWorld;
|
|
|
|
constructor(world: World, entityAccessor: EntityAccessor, ecsWorld?: ECSWorld) {
|
|
this.world = world;
|
|
this.entityAccessor = entityAccessor;
|
|
this.ecsWorld = ecsWorld;
|
|
}
|
|
|
|
/**
|
|
* Update references when world changes (e.g., new floor)
|
|
*/
|
|
updateWorld(world: World, entityAccessor: EntityAccessor, ecsWorld?: ECSWorld): void {
|
|
this.world = world;
|
|
this.entityAccessor = entityAccessor;
|
|
if (ecsWorld) this.ecsWorld = ecsWorld;
|
|
}
|
|
|
|
/**
|
|
* Spawn an item drop at the specified position
|
|
*/
|
|
spawnItem(item: Item, pos: Vec2): void {
|
|
if (!this.world || !this.ecsWorld) return;
|
|
|
|
// Deep clone item (crucial for items with mutable stats like ammo)
|
|
const clonedItem = { ...item } as Item;
|
|
if ('stats' in clonedItem && clonedItem.stats) {
|
|
(clonedItem as any).stats = { ...clonedItem.stats };
|
|
}
|
|
|
|
// ECS Path: Spawn using EntityBuilder
|
|
EntityBuilder.create(this.ecsWorld)
|
|
.withPosition(pos.x, pos.y)
|
|
.asGroundItem(clonedItem)
|
|
.build();
|
|
}
|
|
|
|
/**
|
|
* Try to pickup an item at the player's position
|
|
* @returns The picked up item, or null if nothing to pick up
|
|
*/
|
|
tryPickup(player: CombatantActor): Item | null {
|
|
if (!player || !player.inventory) return null;
|
|
|
|
let itemActor: ItemDropActor | null = null;
|
|
|
|
// Use EntityAccessor to find item on the ground
|
|
if (this.entityAccessor) {
|
|
itemActor = this.entityAccessor.findItemDropAt(player.pos.x, player.pos.y);
|
|
}
|
|
|
|
if (itemActor) {
|
|
const item = itemActor.item;
|
|
const result = this.addItem(player, item);
|
|
|
|
// Remove from world
|
|
this.entityAccessor.removeActor(itemActor.id);
|
|
|
|
console.log("Picked up:", item.name);
|
|
return result;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Add an item to player inventory, handling stacking if applicable
|
|
* @returns The added or modified item
|
|
*/
|
|
addItem(player: CombatantActor, item: Item): Item {
|
|
if (!player.inventory) throw new Error("Player has no inventory");
|
|
|
|
// Deep clone item (crucial for items with mutable stats like ammo or when picking up from ground)
|
|
const itemToAdd = { ...item } as Item;
|
|
if ('stats' in itemToAdd && itemToAdd.stats) {
|
|
(itemToAdd as any).stats = { ...itemToAdd.stats };
|
|
}
|
|
|
|
// Stacking Logic
|
|
if (itemToAdd.stackable) {
|
|
const existingItem = player.inventory.items.find(it => it.id === itemToAdd.id);
|
|
if (existingItem) {
|
|
existingItem.quantity = (existingItem.quantity || 1) + (itemToAdd.quantity || 1);
|
|
console.log(`Stacked ${itemToAdd.name}. New quantity: ${existingItem.quantity}`);
|
|
return existingItem;
|
|
}
|
|
}
|
|
|
|
// Add to inventory
|
|
itemToAdd.quantity = itemToAdd.quantity || 1;
|
|
player.inventory.items.push(itemToAdd);
|
|
return itemToAdd;
|
|
}
|
|
|
|
/**
|
|
* Handle using an item from inventory
|
|
* Returns information about what happened
|
|
*/
|
|
handleUse(itemId: string, player: CombatantActor): ItemUseResult {
|
|
if (!player || !player.inventory) {
|
|
return { success: false, consumed: false, message: "Invalid player state" };
|
|
}
|
|
|
|
const itemIdx = player.inventory.items.findIndex(it => it.id === itemId);
|
|
if (itemIdx === -1) {
|
|
return { success: false, consumed: false, message: "Item not found" };
|
|
}
|
|
|
|
const item = player.inventory.items[itemIdx];
|
|
|
|
// Check if item is a healing consumable
|
|
if (item.type === "Consumable" && item.stats?.hp) {
|
|
const healAmount = item.stats.hp;
|
|
|
|
if (player.stats.hp >= player.stats.maxHp) {
|
|
return { success: false, consumed: false, message: "Already at full health" };
|
|
}
|
|
|
|
player.stats.hp = Math.min(player.stats.hp + healAmount, player.stats.maxHp);
|
|
|
|
// Consume item (check stack)
|
|
if (item.quantity && item.quantity > 1) {
|
|
item.quantity--;
|
|
} else {
|
|
player.inventory.items.splice(itemIdx, 1);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
consumed: true,
|
|
message: `Healed for ${healAmount} HP`
|
|
};
|
|
}
|
|
|
|
// Throwable items
|
|
if (item.type === "Consumable" && item.throwable) {
|
|
return {
|
|
success: true,
|
|
consumed: false,
|
|
message: "Throwable item - use targeting"
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
consumed: false,
|
|
message: "Item has no effect"
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Remove an item from player inventory by ID
|
|
*/
|
|
removeFromInventory(player: CombatantActor, itemId: string): boolean {
|
|
if (!player || !player.inventory) return false;
|
|
|
|
const itemIdx = player.inventory.items.findIndex(it => it.id === itemId);
|
|
if (itemIdx === -1) return false;
|
|
|
|
player.inventory.items.splice(itemIdx, 1);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get an item from player inventory by ID
|
|
*/
|
|
getItem(player: CombatantActor, itemId: string): Item | null {
|
|
if (!player || !player.inventory) return null;
|
|
|
|
const item = player.inventory.items.find(it => it.id === itemId);
|
|
return item || null;
|
|
}
|
|
}
|