Reworked inventory
This commit is contained in:
parent
9a44605469
commit
b7bba137a1
|
|
@ -1,6 +1,7 @@
|
|||
import type { InventoryOptions, InventorySlotInput, SlotId } from "../types";
|
||||
import type { InventorySlotInput, SlotId } from "../types";
|
||||
import { action } from "../utils/decorators";
|
||||
import { Component } from "../core/world";
|
||||
import { Stackable, Usable } from "./item";
|
||||
|
||||
interface SlotEntry {
|
||||
readonly slotId: SlotId;
|
||||
|
|
@ -16,9 +17,8 @@ interface SlotUpdateArgs {
|
|||
|
||||
export class Inventory extends Component {
|
||||
private readonly slots: Map<SlotId, SlotEntry>;
|
||||
private readonly maxAmountPerItem: Record<string, number>;
|
||||
|
||||
constructor(slotDefs: Array<InventorySlotInput>, options?: InventoryOptions) {
|
||||
constructor(slotDefs: Array<InventorySlotInput>) {
|
||||
super();
|
||||
this.slots = new Map(
|
||||
slotDefs.map(def => {
|
||||
|
|
@ -27,17 +27,15 @@ export class Inventory extends Component {
|
|||
return [slotId, { slotId, limit, state: null }];
|
||||
})
|
||||
);
|
||||
this.maxAmountPerItem = options?.maxAmountPerItem ?? {};
|
||||
}
|
||||
|
||||
// Max amount of itemId allowed in a single slot (min of slot.limit and maxAmountPerItem cap)
|
||||
private slotCapFor(slot: SlotEntry, itemId: string): number {
|
||||
const limitCap = slot.limit ?? Infinity;
|
||||
const itemCap = this.maxAmountPerItem[itemId] ?? Infinity;
|
||||
return Math.min(limitCap, itemCap);
|
||||
const stackable = this.entity.world.getEntity(itemId)?.get(Stackable);
|
||||
const stackCap = stackable ? stackable.maxStack : 1;
|
||||
return Math.min(limitCap, stackCap);
|
||||
}
|
||||
|
||||
// Remaining space in slot for itemId
|
||||
private slotRoomFor(slot: SlotEntry, itemId: string): number {
|
||||
if (slot.state !== null && slot.state.itemId !== itemId) return 0;
|
||||
return this.slotCapFor(slot, itemId) - (slot.state?.amount ?? 0);
|
||||
|
|
@ -48,6 +46,11 @@ export class Inventory extends Component {
|
|||
if (amount < 0) return false;
|
||||
if (amount === 0) return true;
|
||||
|
||||
if (!this.entity.world.getEntity(itemId)) {
|
||||
console.warn(`[Inventory] add: item entity '${itemId}' not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slotId !== undefined) {
|
||||
const slot = this.slots.get(slotId);
|
||||
if (!slot) return false;
|
||||
|
|
@ -134,6 +137,35 @@ export class Inventory extends Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
@action
|
||||
async use(itemId?: string): Promise<boolean> {
|
||||
if (!itemId) return false;
|
||||
|
||||
if (this.getAmount(itemId) === 0) {
|
||||
console.warn(`[Inventory] use: item '${itemId}' not in inventory`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const itemEntity = this.entity.world.getEntity(itemId);
|
||||
if (!itemEntity) {
|
||||
console.warn(`[Inventory] use: item entity '${itemId}' not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const usable = itemEntity.get(Usable);
|
||||
if (!usable) {
|
||||
console.warn(`[Inventory] use: item '${itemId}' has no Usable component`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (usable.consumeOnUse) {
|
||||
this.remove({ itemId, amount: 1 });
|
||||
}
|
||||
|
||||
await usable.use({ self: this.entity, world: this.entity.world });
|
||||
return true;
|
||||
}
|
||||
|
||||
getAmount(itemId: string, slotId?: SlotId): number {
|
||||
if (slotId !== undefined) {
|
||||
const slot = this.slots.get(slotId);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import { Component, type EvalContext } from "../core/world";
|
||||
import type { RPGAction } from "../types";
|
||||
import { action, variable } from "../utils/decorators";
|
||||
import { executeAction } from "../utils/variables";
|
||||
|
||||
export class Item extends Component {
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly description: string = '',
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class Stackable extends Component {
|
||||
@variable readonly maxStack: number;
|
||||
constructor(maxStack: number) {
|
||||
super();
|
||||
this.maxStack = maxStack;
|
||||
}
|
||||
}
|
||||
|
||||
export class Usable extends Component {
|
||||
@variable readonly consumeOnUse: boolean;
|
||||
constructor(private readonly actions: RPGAction[], consumeOnUse = true) {
|
||||
super();
|
||||
this.consumeOnUse = consumeOnUse;
|
||||
}
|
||||
|
||||
@action
|
||||
async use(ctx: EvalContext): Promise<void> {
|
||||
for (const action of this.actions) {
|
||||
await executeAction(action, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ export class Entity {
|
|||
|
||||
constructor(
|
||||
readonly id: string,
|
||||
private readonly world: World,
|
||||
readonly world: World,
|
||||
) { }
|
||||
|
||||
add<T extends Component>(key: string, component: T): T {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export type RPGVariables = Record<string, string | number | boolean | undefined>
|
|||
export type RPGActions = Record<string, (arg?: any) => unknown>;
|
||||
export type RPGAction = Static<typeof RPGActionScheme>;
|
||||
|
||||
|
||||
// ── Dialog ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const DialogChoiceScheme = Type.Object({
|
||||
|
|
@ -85,6 +86,4 @@ export interface InventorySlotDefinition {
|
|||
|
||||
export type InventorySlotInput = SlotId | InventorySlotDefinition;
|
||||
|
||||
export interface InventoryOptions {
|
||||
maxAmountPerItem?: Record<string, number>;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue