Files
rogue/src/scenes/systems/ItemManager.ts
2026-01-27 13:46:19 +11:00

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