1
0
Fork 0

Compare commits

..

No commits in common. "f6b3b5b66eb7a2158449f57f53585b647025fe75" and "9a446054691b3f888400e30af56de95b9003fd15" have entirely different histories.

7 changed files with 21 additions and 146 deletions

View File

@ -1,8 +1,6 @@
import type { InventorySlotInput, RPGVariables, SlotId } from "../types"; import type { InventoryOptions, InventorySlotInput, SlotId } from "../types";
import { action } from "../utils/decorators"; import { action } from "../utils/decorators";
import { Component, type EvalContext } from "../core/world"; import { Component } from "../core/world";
import { Stackable, Usable } from "./item";
import { resolveVariables } from "../utils/variables";
interface SlotEntry { interface SlotEntry {
readonly slotId: SlotId; readonly slotId: SlotId;
@ -18,8 +16,9 @@ interface SlotUpdateArgs {
export class Inventory extends Component { export class Inventory extends Component {
private readonly slots: Map<SlotId, SlotEntry>; private readonly slots: Map<SlotId, SlotEntry>;
private readonly maxAmountPerItem: Record<string, number>;
constructor(slotDefs: Array<InventorySlotInput>) { constructor(slotDefs: Array<InventorySlotInput>, options?: InventoryOptions) {
super(); super();
this.slots = new Map( this.slots = new Map(
slotDefs.map(def => { slotDefs.map(def => {
@ -28,15 +27,17 @@ export class Inventory extends Component {
return [slotId, { slotId, limit, state: null }]; 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 { private slotCapFor(slot: SlotEntry, itemId: string): number {
const limitCap = slot.limit ?? Infinity; const limitCap = slot.limit ?? Infinity;
const stackable = this.entity.world.getEntity(itemId)?.get(Stackable); const itemCap = this.maxAmountPerItem[itemId] ?? Infinity;
const stackCap = stackable ? stackable.maxStack : 1; return Math.min(limitCap, itemCap);
return Math.min(limitCap, stackCap);
} }
// Remaining space in slot for itemId
private slotRoomFor(slot: SlotEntry, itemId: string): number { private slotRoomFor(slot: SlotEntry, itemId: string): number {
if (slot.state !== null && slot.state.itemId !== itemId) return 0; if (slot.state !== null && slot.state.itemId !== itemId) return 0;
return this.slotCapFor(slot, itemId) - (slot.state?.amount ?? 0); return this.slotCapFor(slot, itemId) - (slot.state?.amount ?? 0);
@ -47,11 +48,6 @@ export class Inventory extends Component {
if (amount < 0) return false; if (amount < 0) return false;
if (amount === 0) return true; 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) { if (slotId !== undefined) {
const slot = this.slots.get(slotId); const slot = this.slots.get(slotId);
if (!slot) return false; if (!slot) return false;
@ -138,36 +134,6 @@ export class Inventory extends Component {
return false; return false;
} }
@action
async use(itemId?: string, ctx?: EvalContext): 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 });
}
const resolvedCtx = ctx ?? { self: this.entity, world: this.entity.world };
await usable.use(resolvedCtx);
return true;
}
getAmount(itemId: string, slotId?: SlotId): number { getAmount(itemId: string, slotId?: SlotId): number {
if (slotId !== undefined) { if (slotId !== undefined) {
const slot = this.slots.get(slotId); const slot = this.slots.get(slotId);
@ -190,17 +156,7 @@ export class Inventory extends Component {
return result; return result;
} }
override getVariables(): RPGVariables { override getVariables(): Record<string, number> {
const result: RPGVariables = {}; return Object.fromEntries(this.getItems());
for (const [itemId, amount] of this.getItems()) {
result[itemId] = amount;
const itemEntity = this.entity.world.getEntity(itemId);
if (itemEntity) {
for (const [key, value] of Object.entries(resolveVariables(itemEntity))) {
result[`${itemId}.${key}`] = value;
}
}
}
return result;
} }
} }

View File

@ -1,56 +0,0 @@
import { Component, type EvalContext, type World } 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(arg?: EvalContext, ctx?: EvalContext): Promise<void> {
ctx = arg ?? ctx ?? this.context;
if (!ctx) return;
for (const action of this.actions) {
await executeAction(action, ctx);
}
}
}
export namespace Items {
export interface RegisterOptions {
maxStack?: number;
description?: string;
usable?: { actions: RPGAction[]; consumeOnUse?: boolean };
}
export function register(world: World, id: string, name: string, options?: RegisterOptions) {
const entity = world.createEntity(id);
entity.add('item', new Item(name, options?.description));
if (options?.maxStack !== undefined)
entity.add('stackable', new Stackable(options.maxStack));
if (options?.usable)
entity.add('usable', new Usable(options.usable.actions, options.usable.consumeOnUse));
return entity;
}
}

View File

@ -57,10 +57,6 @@ export abstract class Component {
} }
return actions; return actions;
} }
get context(): EvalContext {
return { self: this.entity, world: this.entity.world };
}
} }
export abstract class System { export abstract class System {
@ -74,7 +70,7 @@ export class Entity {
constructor( constructor(
readonly id: string, readonly id: string,
readonly world: World, private readonly world: World,
) { } ) { }
add<T extends Component>(key: string, component: T): T { add<T extends Component>(key: string, component: T): T {
@ -281,10 +277,6 @@ export class World {
map.get(key)!.add(handler); map.get(key)!.add(handler);
return () => map.get(key)?.delete(handler); return () => map.get(key)?.delete(handler);
} }
*[Symbol.iterator]() {
yield* this.#entities.values();
}
} }
export function isEvalContext(v: unknown): v is EvalContext { export function isEvalContext(v: unknown): v is EvalContext {

View File

@ -12,7 +12,6 @@ export type RPGVariables = Record<string, string | number | boolean | undefined>
export type RPGActions = Record<string, (arg?: any) => unknown>; export type RPGActions = Record<string, (arg?: any) => unknown>;
export type RPGAction = Static<typeof RPGActionScheme>; export type RPGAction = Static<typeof RPGActionScheme>;
// ── Dialog ──────────────────────────────────────────────────────────────────── // ── Dialog ────────────────────────────────────────────────────────────────────
const DialogChoiceScheme = Type.Object({ const DialogChoiceScheme = Type.Object({
@ -86,4 +85,6 @@ export interface InventorySlotDefinition {
export type InventorySlotInput = SlotId | InventorySlotDefinition; export type InventorySlotInput = SlotId | InventorySlotDefinition;
export interface InventoryOptions {
maxAmountPerItem?: Record<string, number>;
}

View File

@ -3,15 +3,13 @@ import type { RPGVariables } from "../types";
export const ACTION_KEYS = Symbol('rpg.actions'); export const ACTION_KEYS = Symbol('rpg.actions');
export const VARIABLE_KEYS = Symbol('rpg.variables'); export const VARIABLE_KEYS = Symbol('rpg.variables');
export function action<T extends (arg?: any, ctx?: any) => unknown>( export function action<T extends (arg?: any) => unknown>(
target: T, target: T,
context: ClassMethodDecoratorContext<unknown, T> context: ClassMethodDecoratorContext<unknown, T>
): T { ): T {
const prev = context.metadata[ACTION_KEYS] as Set<string | symbol> | undefined; const prev = context.metadata[ACTION_KEYS] as Set<string | symbol> | undefined;
context.metadata[ACTION_KEYS] = new Set(prev).add(context.name); context.metadata[ACTION_KEYS] = new Set(prev).add(context.name);
return function(this: any, arg?: any, ctx?: any) { return target;
return target.call(this, arg, ctx ?? this.context);
} as unknown as T;
} }
type VariableContext<T extends RPGVariables[string]> = type VariableContext<T extends RPGVariables[string]> =

View File

@ -1,21 +1,9 @@
import type { RPGAction, RPGActions, RPGVariables } from "../types"; import type { RPGAction, RPGActions, RPGVariables } from "../types";
import { World } from "../core/world";
import type { EvalContext, Entity } from "../core/world"; import type { EvalContext, Entity } from "../core/world";
export function resolveVariables(entity: Entity): RPGVariables; export function resolveVariables(entity: Entity): RPGVariables {
export function resolveVariables(world: World): RPGVariables;
export function resolveVariables(entityOrWorld: Entity | World): RPGVariables {
if (entityOrWorld instanceof World) {
const result: RPGVariables = {};
for (const entity of entityOrWorld) {
for (const [key, value] of Object.entries(resolveVariables(entity))) {
result[`${entity.id}.${key}`] = value;
}
}
return result;
}
const result: RPGVariables = {}; const result: RPGVariables = {};
for (const [key, component] of entityOrWorld) { for (const [key, component] of entity) {
for (const [varKey, value] of Object.entries(component.getVariables())) { for (const [varKey, value] of Object.entries(component.getVariables())) {
if (value != null) { if (value != null) {
if (varKey && varKey !== '.') { if (varKey && varKey !== '.') {

View File

@ -4,16 +4,12 @@ import { Health } from "@common/rpg/components/stat";
import { Variables } from "@common/rpg/components/variables"; import { Variables } from "@common/rpg/components/variables";
import { QuestLog } from "@common/rpg/components/questLog"; import { QuestLog } from "@common/rpg/components/questLog";
import { QuestSystem } from "@common/rpg/systems/questSystem"; import { QuestSystem } from "@common/rpg/systems/questSystem";
import { Items } from "@common/rpg/components/item";
import { resolveVariables, resolveActions } from "@common/rpg/utils/variables"; import { resolveVariables, resolveActions } from "@common/rpg/utils/variables";
export default async function main() { export default async function main() {
const world = new World(); const world = new World();
world.addSystem(new QuestSystem()); world.addSystem(new QuestSystem());
Items.register(world, 'helmet', 'Iron Helmet');
Items.register(world, 'boots', 'Leather Boots', { maxStack: 2 });
const player = world.createEntity('player'); const player = world.createEntity('player');
player.add('inventory', new Inventory(['head', 'legs'])); player.add('inventory', new Inventory(['head', 'legs']));
player.add('health', new Health(100, 100)); player.add('health', new Health(100, 100));
@ -28,7 +24,7 @@ export default async function main() {
stages: [], stages: [],
}); });
console.log(resolveVariables(world)); console.log(resolveVariables(player));
const inventory = player.get(Inventory)!; const inventory = player.get(Inventory)!;
inventory.add({ itemId: 'helmet', amount: 1, slotId: 'head' }); inventory.add({ itemId: 'helmet', amount: 1, slotId: 'head' });
@ -37,5 +33,5 @@ export default async function main() {
actions['inventory.add']({ itemId: 'boots', amount: 2 }); actions['inventory.add']({ itemId: 'boots', amount: 2 });
console.log(actions); console.log(actions);
console.log(resolveVariables(world)); console.log(resolveVariables(player));
} }