feat: weight-based inventory with element stacking (Phase 4.2)
Inventory uses real atomic/molecular masses (AMU). Same items auto-stack. Respects weight limits and slot limits. Supports elements and compounds via chemistry registries. 28 new tests (162 total). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
149
src/player/inventory.ts
Normal file
149
src/player/inventory.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Player Inventory — Weight-Based with Element Stacking
|
||||
*
|
||||
* Items are elements (by symbol) or compounds (by id).
|
||||
* Weight = real atomic/molecular mass.
|
||||
* Same items stack automatically (count increases).
|
||||
*/
|
||||
|
||||
import { ElementRegistry } from '../chemistry/elements';
|
||||
import { CompoundRegistry } from '../chemistry/compounds';
|
||||
|
||||
/** A single inventory entry */
|
||||
export interface InventoryItem {
|
||||
readonly id: string;
|
||||
readonly count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mass of an item (element or compound).
|
||||
* Returns atomic mass for elements, molecular mass for compounds, 0 for unknown.
|
||||
*/
|
||||
export function getItemMass(id: string): number {
|
||||
const el = ElementRegistry.getBySymbol(id);
|
||||
if (el) return el.atomicMass;
|
||||
|
||||
const comp = CompoundRegistry.getById(id);
|
||||
if (comp) return comp.mass;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Weight-based inventory with slot limits and auto-stacking.
|
||||
*
|
||||
* Weight uses real atomic/molecular masses (AMU).
|
||||
* Same items stack into a single slot.
|
||||
*/
|
||||
export class Inventory {
|
||||
private counts = new Map<string, number>();
|
||||
|
||||
/** Maximum total weight (AMU) */
|
||||
readonly maxWeight: number;
|
||||
/** Maximum number of unique item types (slots) */
|
||||
readonly maxSlots: number;
|
||||
|
||||
constructor(maxWeight = 500, maxSlots = 20) {
|
||||
this.maxWeight = maxWeight;
|
||||
this.maxSlots = maxSlots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add items to inventory.
|
||||
* @returns actual count added (may be less if weight limit reached, 0 if impossible)
|
||||
*/
|
||||
addItem(id: string, count = 1): number {
|
||||
if (count <= 0) return 0;
|
||||
|
||||
const mass = getItemMass(id);
|
||||
if (mass <= 0) return 0;
|
||||
|
||||
// New item needs an available slot
|
||||
const isNewItem = !this.counts.has(id);
|
||||
if (isNewItem && this.counts.size >= this.maxSlots) return 0;
|
||||
|
||||
// Calculate how many can fit by weight
|
||||
const currentWeight = this.getTotalWeight();
|
||||
const spaceLeft = this.maxWeight - currentWeight;
|
||||
const maxByWeight = Math.floor(spaceLeft / mass);
|
||||
const actualAdd = Math.min(count, maxByWeight);
|
||||
|
||||
if (actualAdd <= 0) return 0;
|
||||
|
||||
const existing = this.counts.get(id) ?? 0;
|
||||
this.counts.set(id, existing + actualAdd);
|
||||
|
||||
return actualAdd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove items from inventory.
|
||||
* @returns actual count removed (may be less if not enough in stock)
|
||||
*/
|
||||
removeItem(id: string, count = 1): number {
|
||||
if (count <= 0) return 0;
|
||||
|
||||
const current = this.counts.get(id) ?? 0;
|
||||
if (current <= 0) return 0;
|
||||
|
||||
const actualRemove = Math.min(count, current);
|
||||
const newCount = current - actualRemove;
|
||||
|
||||
if (newCount <= 0) {
|
||||
this.counts.delete(id);
|
||||
} else {
|
||||
this.counts.set(id, newCount);
|
||||
}
|
||||
|
||||
return actualRemove;
|
||||
}
|
||||
|
||||
/** Get item count (0 if not in inventory) */
|
||||
getCount(id: string): number {
|
||||
return this.counts.get(id) ?? 0;
|
||||
}
|
||||
|
||||
/** Check if inventory has at least `count` of an item */
|
||||
hasItem(id: string, count = 1): boolean {
|
||||
return (this.counts.get(id) ?? 0) >= count;
|
||||
}
|
||||
|
||||
/** Get all items as array of { id, count } */
|
||||
getItems(): InventoryItem[] {
|
||||
const items: InventoryItem[] = [];
|
||||
for (const [id, count] of this.counts) {
|
||||
items.push({ id, count });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/** Total weight of all items (sum of mass * count) */
|
||||
getTotalWeight(): number {
|
||||
let total = 0;
|
||||
for (const [id, count] of this.counts) {
|
||||
total += getItemMass(id) * count;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/** Weight of a single item stack */
|
||||
getItemWeight(id: string): number {
|
||||
const count = this.counts.get(id) ?? 0;
|
||||
return getItemMass(id) * count;
|
||||
}
|
||||
|
||||
/** Number of unique item types (occupied slots) */
|
||||
get slotCount(): number {
|
||||
return this.counts.size;
|
||||
}
|
||||
|
||||
/** Whether inventory has zero items */
|
||||
isEmpty(): boolean {
|
||||
return this.counts.size === 0;
|
||||
}
|
||||
|
||||
/** Remove all items */
|
||||
clear(): void {
|
||||
this.counts.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user