Files
rogue/src/scenes/systems/ItemManager.ts

189 lines
5.5 KiB
TypeScript

import type { World, CombatantActor, Item, ItemDropActor, Vec2 } from "../../core/types";
import { EntityManager } from "../../engine/EntityManager";
/**
* 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 entityManager: EntityManager;
constructor(world: World, entityManager: EntityManager) {
this.world = world;
this.entityManager = entityManager;
}
/**
* Update references when world changes (e.g., new floor)
*/
updateWorld(world: World, entityManager: EntityManager): void {
this.world = world;
this.entityManager = entityManager;
}
/**
* Spawn an item drop at the specified position
*/
spawnItem(item: Item, pos: Vec2): void {
if (!this.world || !this.entityManager) 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 };
}
const id = this.entityManager.getNextId();
const drop: ItemDropActor = {
id,
pos: { x: pos.x, y: pos.y },
category: "item_drop",
item: clonedItem
};
this.entityManager.addActor(drop);
}
/**
* 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;
const actors = this.entityManager.getActorsAt(player.pos.x, player.pos.y);
const itemActor = actors.find((a): a is ItemDropActor => a.category === "item_drop");
if (itemActor) {
const item = itemActor.item;
const result = this.addItem(player, item);
// Remove from world
this.entityManager.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;
}
}