Fix bugs
This commit is contained in:
parent
2d4a59f2b3
commit
0ad5c60cdd
|
|
@ -1,12 +1,61 @@
|
|||
import type { RPGActions, RPGVariables } from "../types";
|
||||
import { ACTION_KEYS, VARIABLE_KEYS } from "../decorators";
|
||||
|
||||
export interface RPGComponent {
|
||||
getVariables: () => RPGVariables;
|
||||
getActions: () => RPGActions;
|
||||
export interface RPGEvent<T = unknown> {
|
||||
target: RPGComponent;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
export interface RPGContext {
|
||||
dispatch(event: string, payload: RPGEvent): void;
|
||||
on(event: string, handler: (payload: RPGEvent) => void): () => void;
|
||||
off(event: string, handler: (payload: RPGEvent) => void): void;
|
||||
}
|
||||
|
||||
export abstract class RPGComponent {
|
||||
#path: string = '';
|
||||
protected _ctx: RPGContext | null = null;
|
||||
|
||||
get path(): string {
|
||||
return this.#path;
|
||||
}
|
||||
|
||||
set path(path: string) {
|
||||
this.#path = path;
|
||||
}
|
||||
|
||||
attach(ctx: RPGContext, path: string): void {
|
||||
this.path = path;
|
||||
this._ctx = ctx;
|
||||
this.onAttach();
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
this.onDetach();
|
||||
this.path = '';
|
||||
this._ctx = null;
|
||||
}
|
||||
|
||||
protected onAttach(): void {}
|
||||
protected onDetach(): void {}
|
||||
|
||||
protected emit<T = unknown>(event: string, data?: T): void {
|
||||
this._ctx?.dispatch(this.resolve(event), { target: this, data });
|
||||
}
|
||||
|
||||
on<T = unknown>(event: string, handler: (payload: RPGEvent<T>) => void): () => void {
|
||||
return this._ctx?.on(this.resolve(event), handler as (e: RPGEvent) => void) ?? (() => {});
|
||||
}
|
||||
|
||||
off<T = unknown>(event: string, handler: (payload: RPGEvent<T>) => void): void {
|
||||
this._ctx?.off(this.resolve(event), handler as (e: RPGEvent) => void);
|
||||
}
|
||||
|
||||
protected resolve(name: string): string {
|
||||
if (name.startsWith('$.')) return name.slice(2);
|
||||
return this.path ? `${this.path}.${name}` : name;
|
||||
}
|
||||
|
||||
export abstract class RPGComponentBase implements RPGComponent {
|
||||
getVariables(): RPGVariables {
|
||||
const meta = (this.constructor as Function)[Symbol.metadata];
|
||||
const keys = meta?.[VARIABLE_KEYS] as Map<string | symbol, string> | undefined;
|
||||
|
|
@ -40,24 +89,92 @@ export abstract class RPGComponentBase implements RPGComponent {
|
|||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
tick(_dt: number) {}
|
||||
}
|
||||
|
||||
export class RPGEntity implements RPGComponent {
|
||||
function matchesWildcard(event: string, pattern: string): boolean {
|
||||
const ep = event.split('.');
|
||||
const pp = pattern.split('.');
|
||||
return (function match(ei: number, pi: number): boolean {
|
||||
if (pi === pp.length) return ei === ep.length;
|
||||
if (pp[pi] === '*') {
|
||||
for (let skip = 0; ei + skip <= ep.length; skip++) {
|
||||
if (match(ei + skip, pi + 1)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return ei < ep.length && pp[pi] === ep[ei] && match(ei + 1, pi + 1);
|
||||
})(0, 0);
|
||||
}
|
||||
|
||||
export class RPGEntity extends RPGComponent implements RPGContext {
|
||||
private readonly handlers = new Map<string, Set<(payload: RPGEvent) => void>>();
|
||||
private readonly wildcardHandlers = new Map<string, Set<(payload: RPGEvent) => void>>();
|
||||
private components = new Map<string, RPGComponent>();
|
||||
|
||||
constructor(public readonly id: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
// RPGContext: raw dispatch into this entity's handler map
|
||||
dispatch(event: string, payload: RPGEvent): void {
|
||||
this.handlers.get(event)?.forEach(h => h(payload));
|
||||
for (const [pattern, handlers] of this.wildcardHandlers) {
|
||||
if (matchesWildcard(event, pattern)) handlers.forEach(h => h(payload));
|
||||
}
|
||||
}
|
||||
|
||||
// RPGContext: register locally if root, delegate up if nested
|
||||
override on<T = unknown>(event: string, handler: (payload: RPGEvent<T>) => void): () => void {
|
||||
const resolved = this.resolve(event);
|
||||
const h = handler as (payload: RPGEvent) => void;
|
||||
if (this._ctx) {
|
||||
return this._ctx.on(resolved, h);
|
||||
}
|
||||
const map = resolved.includes('*') ? this.wildcardHandlers : this.handlers;
|
||||
if (!map.has(resolved)) map.set(resolved, new Set());
|
||||
map.get(resolved)!.add(h);
|
||||
return () => map.get(resolved)?.delete(h);
|
||||
}
|
||||
|
||||
override off<T = unknown>(event: string, handler: (payload: RPGEvent<T>) => void): void {
|
||||
const resolved = this.resolve(event);
|
||||
const h = handler as (payload: RPGEvent) => void;
|
||||
if (this._ctx) {
|
||||
this._ctx.off(resolved, h);
|
||||
return;
|
||||
}
|
||||
const map = resolved.includes('*') ? this.wildcardHandlers : this.handlers;
|
||||
map.get(resolved)?.delete(h);
|
||||
}
|
||||
|
||||
override attach(ctx: RPGContext, path: string): void {
|
||||
super.attach(ctx, path);
|
||||
for (const [key, component] of this.components) {
|
||||
component.attach(ctx, `${path}.${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
addComponent(id: string, component: RPGComponent): void {
|
||||
this.components.set(id, component);
|
||||
const path = this.path ? `${this.path}.${id}` : id;
|
||||
component.attach(this._ctx ?? this, path);
|
||||
}
|
||||
|
||||
removeComponent(id: string): void {
|
||||
const component = this.components.get(id);
|
||||
if (component) {
|
||||
component.detach();
|
||||
this.components.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
getComponent<T extends RPGComponent>(id: string): T | undefined {
|
||||
return this.components.get(id) as T | undefined;
|
||||
}
|
||||
|
||||
getVariables(): RPGVariables {
|
||||
override getVariables(): RPGVariables {
|
||||
const variables: RPGVariables = {};
|
||||
for (const [componentKey, component] of this.components) {
|
||||
for (const [key, value] of Object.entries(component.getVariables())) {
|
||||
|
|
@ -69,7 +186,7 @@ export class RPGEntity implements RPGComponent {
|
|||
return variables;
|
||||
}
|
||||
|
||||
getActions() {
|
||||
override getActions(): RPGActions {
|
||||
const actions: RPGActions = {};
|
||||
for (const [componentKey, component] of this.components) {
|
||||
for (const [key, action] of Object.entries(component.getActions())) {
|
||||
|
|
@ -78,4 +195,17 @@ export class RPGEntity implements RPGComponent {
|
|||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
override tick(dt: number) {
|
||||
for (const component of this.components.values()) {
|
||||
component.tick(dt);
|
||||
}
|
||||
}
|
||||
|
||||
override set path(value: string) {
|
||||
super.path = value;
|
||||
for (const [key, component] of this.components) {
|
||||
component.path = value ? `${value}.${key}` : key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { InventoryOptions, InventorySlotInput, SlotId } from "../types";
|
||||
import { action } from "../decorators";
|
||||
import { RPGComponentBase } from "./entity";
|
||||
import { RPGComponent } from "./entity";
|
||||
|
||||
interface SlotEntry {
|
||||
readonly slotId: SlotId;
|
||||
|
|
@ -13,7 +13,7 @@ interface SlotUpdateArgs {
|
|||
slotId?: SlotId;
|
||||
}
|
||||
|
||||
export class Inventory extends RPGComponentBase {
|
||||
export class Inventory extends RPGComponent {
|
||||
private readonly slots: Map<SlotId, SlotEntry>;
|
||||
private readonly maxAmountPerItem: Record<string, number>;
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ export class Inventory extends RPGComponentBase {
|
|||
}
|
||||
|
||||
@action
|
||||
addItem({ itemId, amount, slotId }: SlotUpdateArgs): boolean {
|
||||
add({ itemId, amount, slotId }: SlotUpdateArgs): boolean {
|
||||
if (amount < 0) return false;
|
||||
if (amount === 0) return true;
|
||||
|
||||
|
|
@ -52,6 +52,7 @@ export class Inventory extends RPGComponentBase {
|
|||
if (!slot) return false;
|
||||
if (this.slotRoomFor(slot, itemId) < amount) return false;
|
||||
slot.state = { itemId, amount: (slot.state?.amount ?? 0) + amount };
|
||||
this.emit('add', { itemId, amount, slotIds: [slotId] });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -67,20 +68,29 @@ export class Inventory extends RPGComponentBase {
|
|||
|
||||
// Apply
|
||||
remaining = amount;
|
||||
for (const slot of this.slots.values()) {
|
||||
const slotIds: SlotId[] = [];
|
||||
for (const [id, slot] of this.slots) {
|
||||
if (slot.state?.itemId === itemId) {
|
||||
const take = Math.min(this.slotRoomFor(slot, itemId), remaining);
|
||||
slot.state.amount += take;
|
||||
remaining -= take;
|
||||
if (remaining === 0) return true;
|
||||
slotIds.push(id);
|
||||
if (remaining === 0) {
|
||||
this.emit('add', { itemId, amount, slotIds });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const slot of this.slots.values()) {
|
||||
}
|
||||
for (const [id, slot] of this.slots) {
|
||||
if (slot.state === null) {
|
||||
const take = Math.min(this.slotCapFor(slot, itemId), remaining);
|
||||
slot.state = { itemId, amount: take };
|
||||
remaining -= take;
|
||||
if (remaining === 0) return true;
|
||||
slotIds.push(id);
|
||||
if (remaining === 0) {
|
||||
this.emit('add', { itemId, amount, slotIds });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +98,7 @@ export class Inventory extends RPGComponentBase {
|
|||
}
|
||||
|
||||
@action
|
||||
removeItem({ itemId, amount, slotId }: SlotUpdateArgs): boolean {
|
||||
remove({ itemId, amount, slotId }: SlotUpdateArgs): boolean {
|
||||
if (amount < 0) return false;
|
||||
if (amount === 0) return true;
|
||||
|
||||
|
|
@ -98,23 +108,34 @@ export class Inventory extends RPGComponentBase {
|
|||
if (slot.state.amount < amount) return false;
|
||||
slot.state.amount -= amount;
|
||||
if (slot.state.amount === 0) slot.state = null;
|
||||
this.emit('remove', { itemId, amount, slotIds: [slotId] });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.getAmount(itemId) < amount) return false;
|
||||
|
||||
let remaining = amount;
|
||||
for (const slot of this.slots.values()) {
|
||||
const slotIds: SlotId[] = [];
|
||||
for (const [id, slot] of this.slots) {
|
||||
if (slot.state?.itemId === itemId) {
|
||||
const take = Math.min(slot.state.amount, remaining);
|
||||
slot.state.amount -= take;
|
||||
if (slot.state.amount === 0) slot.state = null;
|
||||
remaining -= take;
|
||||
if (remaining === 0) return true;
|
||||
slotIds.push(id);
|
||||
if (remaining === 0) {
|
||||
this.emit('remove', { itemId, amount, slotIds });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return remaining === 0;
|
||||
if (remaining === 0) {
|
||||
this.emit('remove', { itemId, amount, slotIds });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getAmount(itemId: string, slotId?: SlotId): number {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { action, variable } from "../decorators";
|
||||
import { RPGComponentBase } from "./entity";
|
||||
import { RPGComponent } from "./entity";
|
||||
|
||||
export class Stat extends RPGComponentBase {
|
||||
export class Stat extends RPGComponent {
|
||||
@variable private value: number;
|
||||
@variable private maxValue: number | undefined;
|
||||
|
||||
|
|
@ -13,9 +13,13 @@ export class Stat extends RPGComponentBase {
|
|||
|
||||
@action
|
||||
update(amount: number) {
|
||||
const prev = this.value;
|
||||
this.value = Math.max(0, this.value + amount);
|
||||
if (this.maxValue) {
|
||||
if (this.maxValue != null) {
|
||||
this.value = Math.min(this.value, this.maxValue);
|
||||
}
|
||||
if (prev !== this.value) {
|
||||
this.emit('update', { prev, value: this.value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import type { RPGVariables } from "../types";
|
||||
import { action } from "../decorators";
|
||||
import { RPGComponentBase } from "./entity";
|
||||
import { RPGComponent } from "./entity";
|
||||
|
||||
interface Var {
|
||||
key: string;
|
||||
value: RPGVariables[string];
|
||||
}
|
||||
|
||||
export class Variables extends RPGComponentBase {
|
||||
export class Variables extends RPGComponent {
|
||||
private readonly variables: RPGVariables = {};
|
||||
|
||||
override getVariables() {
|
||||
|
|
@ -16,13 +16,19 @@ export class Variables extends RPGComponentBase {
|
|||
|
||||
@action
|
||||
set({ key, value }: Var) {
|
||||
const prev = this.variables[key];
|
||||
this.variables[key] = value;
|
||||
|
||||
this.emit('set', { key, value, prev });
|
||||
|
||||
return this.variables;
|
||||
}
|
||||
|
||||
@action
|
||||
unset(key: string) {
|
||||
const prev = this.variables[key];
|
||||
delete this.variables[key];
|
||||
this.emit('unset', { key, prev });
|
||||
return this.variables;
|
||||
}
|
||||
|
||||
|
|
@ -30,7 +36,7 @@ export class Variables extends RPGComponentBase {
|
|||
increment({ key, value }: Var) {
|
||||
const currentValue = this.variables[key] ?? 0;
|
||||
if (typeof currentValue === 'number' && typeof value === 'number') {
|
||||
this.variables[key] = currentValue + value;
|
||||
this.set({ key, value: currentValue + value });
|
||||
}
|
||||
return this.variables;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export namespace Dialogs {
|
|||
actions.add(action.type);
|
||||
}
|
||||
}
|
||||
return Array.from(actions);
|
||||
}
|
||||
|
||||
export function validate(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { RPGComponentBase } from "./components/entity";
|
||||
import { evaluateConditions } from "./conditions";
|
||||
import { RPGComponent } from "./components/entity";
|
||||
import { evaluateCondition, parseCondition } from "./conditions";
|
||||
import { action, variable } from "./decorators";
|
||||
import {
|
||||
isQuest,
|
||||
|
|
@ -39,7 +39,7 @@ export namespace Quests {
|
|||
}
|
||||
}
|
||||
|
||||
export class QuestEngine extends RPGComponentBase {
|
||||
export class QuestEngine extends RPGComponent {
|
||||
@variable('status') private _status: QuestStatus = 'inactive';
|
||||
@variable('stage') private _stageIndex: number = 0;
|
||||
|
||||
|
|
@ -54,14 +54,23 @@ export class QuestEngine extends RPGComponentBase {
|
|||
return this.quest.id;
|
||||
}
|
||||
|
||||
private evaluateConditions(conditions: string[] | undefined) {
|
||||
const variables = this.options.getVariables();
|
||||
return (conditions ?? []).every(condition => {
|
||||
const parsed = parseCondition(condition);
|
||||
return evaluateCondition({ ...parsed, variable: this.resolve(parsed.variable) }, variables);
|
||||
});
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return evaluateConditions(this.quest.conditions ?? [], this.options.getVariables());
|
||||
return this.evaluateConditions(this.quest.conditions);
|
||||
}
|
||||
|
||||
@action
|
||||
start(): void {
|
||||
this._status = 'active';
|
||||
this._stageIndex = 0;
|
||||
this.emit('started');
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
@ -71,7 +80,7 @@ export class QuestEngine extends RPGComponentBase {
|
|||
const stage = this.quest.stages[this._stageIndex];
|
||||
if (!stage) return;
|
||||
|
||||
const allDone = evaluateConditions(stage.objectives.map(obj => obj.condition), this.options.getVariables());
|
||||
const allDone = this.evaluateConditions(stage.objectives.map(obj => obj.condition));
|
||||
|
||||
if (!allDone) return;
|
||||
|
||||
|
|
@ -82,8 +91,10 @@ export class QuestEngine extends RPGComponentBase {
|
|||
|
||||
if (this._stageIndex + 1 < this.quest.stages.length) {
|
||||
this._stageIndex++;
|
||||
this.emit('stage', { index: this._stageIndex, stage: this.quest.stages[this._stageIndex] });
|
||||
} else {
|
||||
this._status = 'completed';
|
||||
this.emit('completed');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +107,7 @@ export class QuestEngine extends RPGComponentBase {
|
|||
}
|
||||
}
|
||||
|
||||
export class QuestManager extends RPGComponentBase {
|
||||
export class QuestManager extends RPGComponent {
|
||||
private readonly engines: Map<string, QuestEngine>;
|
||||
|
||||
constructor(quests: Quest[], options: QuestRuntimeOptions) {
|
||||
|
|
@ -104,6 +115,12 @@ export class QuestManager extends RPGComponentBase {
|
|||
this.engines = new Map(quests.map(q => [q.id, new QuestEngine(q, options)]));
|
||||
}
|
||||
|
||||
protected override onAttach(): void {
|
||||
for (const [questId, engine] of this.engines) {
|
||||
engine.attach(this._ctx!, `${this.path}.${questId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
start(questId: string): void {
|
||||
this.engines.get(questId)?.start();
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { Variables } from "@common/rpg/components/variables";
|
|||
import { QuestManager } from "@common/rpg/quest";
|
||||
|
||||
export default async function main() {
|
||||
const game = new RPGEntity();
|
||||
const player = new RPGEntity();
|
||||
const game = new RPGEntity('game');
|
||||
const player = new RPGEntity('player');
|
||||
const inventory = new Inventory(['head', 'legs']);
|
||||
const quests = new QuestManager([{
|
||||
id: 'test',
|
||||
|
|
@ -23,7 +23,7 @@ export default async function main() {
|
|||
player.addComponent('health', new Stat(100));
|
||||
console.log(game.getActions());
|
||||
|
||||
inventory.addItem({
|
||||
inventory.add({
|
||||
itemId: 'helmet',
|
||||
amount: 1,
|
||||
slotId: 'head',
|
||||
|
|
@ -32,7 +32,7 @@ export default async function main() {
|
|||
itemId: 'boots',
|
||||
amount: 2,
|
||||
});
|
||||
inventory.addItem({
|
||||
inventory.add({
|
||||
itemId: 'belt',
|
||||
amount: 1,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue