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; } }