Namespace variables by component
This commit is contained in:
parent
43da1388e4
commit
6b3c0c77a1
|
|
@ -66,9 +66,9 @@ export class Effect extends Component<{
|
||||||
const stat = this.entity.get(Stat, this.state.targetStat);
|
const stat = this.entity.get(Stat, this.state.targetStat);
|
||||||
if (stat) {
|
if (stat) {
|
||||||
stat.applyModifier(this.state.delta, this.state.targetField);
|
stat.applyModifier(this.state.delta, this.state.targetField);
|
||||||
}
|
|
||||||
this.active = true;
|
this.active = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override onRemove(): void {
|
override onRemove(): void {
|
||||||
if (!this.active) return;
|
if (!this.active) return;
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,9 @@ export type SlotInput =
|
||||||
export class Equipment extends Component<EquipmentState> {
|
export class Equipment extends Component<EquipmentState> {
|
||||||
#cachedVars: RPGVariables | null = null;
|
#cachedVars: RPGVariables | null = null;
|
||||||
|
|
||||||
constructor(slots: SlotInput[]) {
|
constructor(...slots: SlotInput[]) {
|
||||||
const record: Record<string, SlotRecord> = {};
|
const record: Record<string, SlotRecord> = {};
|
||||||
for (const s of slots) {
|
for (const s of slots.flat()) {
|
||||||
const slotName = typeof s === 'string' ? s : s.slotName;
|
const slotName = typeof s === 'string' ? s : s.slotName;
|
||||||
const type = typeof s === 'object' && s.type ? s.type : null;
|
const type = typeof s === 'object' && s.type ? s.type : null;
|
||||||
record[slotName] = { slotName, type, itemId: null, appliedEffectKeys: [] };
|
record[slotName] = { slotName, type, itemId: null, appliedEffectKeys: [] };
|
||||||
|
|
@ -155,7 +155,9 @@ export class Equipment extends Component<EquipmentState> {
|
||||||
|
|
||||||
const result: RPGVariables = {};
|
const result: RPGVariables = {};
|
||||||
for (const { slotName, itemId } of Object.values(this.state.slots)) {
|
for (const { slotName, itemId } of Object.values(this.state.slots)) {
|
||||||
result[slotName] = itemId ?? '';
|
if (itemId) {
|
||||||
|
result[slotName] = itemId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.#cachedVars = result;
|
this.#cachedVars = result;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,23 @@
|
||||||
import { component } from "../core/registry";
|
import { component } from "../core/registry";
|
||||||
import { Component, type Entity } from "../core/world";
|
import { Component, type Entity } from "../core/world";
|
||||||
import { variable } from "../utils/decorators";
|
import type { RPGVariables } from "../types";
|
||||||
|
import { action } from "../utils/decorators";
|
||||||
|
|
||||||
// ── FactionMember ─────────────────────────────────────────────────────────────
|
// ── FactionMember ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@component
|
@component
|
||||||
export class FactionMember extends Component<{ factionId: string }> {
|
export class FactionMember extends Component<{ factionId: string }> {
|
||||||
@variable('.') readonly member: boolean = true;
|
|
||||||
|
|
||||||
constructor(factionId: string) {
|
constructor(factionId: string) {
|
||||||
super({ factionId });
|
super({ factionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
get factionId(): string { return this.state.factionId; }
|
get factionId(): string { return this.state.factionId; }
|
||||||
|
|
||||||
|
override getVariables(): RPGVariables {
|
||||||
|
return {
|
||||||
|
[this.factionId]: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Reputation ────────────────────────────────────────────────────────────────
|
// ── Reputation ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -23,31 +28,71 @@ export class Reputation extends Component<{ factionId: string; score: number }>
|
||||||
super({ factionId, score });
|
super({ factionId, score });
|
||||||
}
|
}
|
||||||
|
|
||||||
@variable('.') get score(): number { return this.state.score; }
|
get score(): number { return this.state.score; }
|
||||||
|
|
||||||
get factionId(): string { return this.state.factionId; }
|
get factionId(): string { return this.state.factionId; }
|
||||||
|
|
||||||
adjust(delta: number): void {
|
adjust(delta: number): void {
|
||||||
this.state.score += delta;
|
this.state.score += delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override getVariables(): RPGVariables {
|
||||||
|
return {
|
||||||
|
[this.factionId]: this.score,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//── FactionManager ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@component
|
||||||
|
export class FactionManager extends Component<{}> {
|
||||||
|
constructor() { super({}); }
|
||||||
|
|
||||||
|
@action
|
||||||
|
join(factionId: string): void {
|
||||||
|
Factions.join(this.entity, factionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
leave(factionId: string): void {
|
||||||
|
Factions.leave(this.entity, factionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setReputation({ factionId, value }: { factionId: string, value: number }): void {
|
||||||
|
Factions.setReputation(this.entity, factionId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
adjustReputation({ factionId, value }: { factionId: string, value: number }): void {
|
||||||
|
Factions.adjustReputation(this.entity, factionId, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Factions namespace ────────────────────────────────────────────────────────
|
// ── Factions namespace ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function memberKey(factionId: string): string { return `faction.${factionId}.member`; }
|
|
||||||
function repKey(factionId: string): string { return `faction.${factionId}.rep`; }
|
|
||||||
|
|
||||||
export namespace Factions {
|
export namespace Factions {
|
||||||
|
function addFactionManager(entity: Entity) {
|
||||||
|
if (!entity.has(FactionManager)) {
|
||||||
|
entity.add(new FactionManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
export function join(entity: Entity, factionId: string): void {
|
export function join(entity: Entity, factionId: string): void {
|
||||||
entity.add(memberKey(factionId), new FactionMember(factionId));
|
addFactionManager(entity);
|
||||||
|
const existing = entity.get(FactionMember, (c) => c.factionId === factionId);
|
||||||
|
if (!existing) {
|
||||||
|
entity.add(new FactionMember(factionId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function leave(entity: Entity, factionId: string): void {
|
export function leave(entity: Entity, factionId: string): void {
|
||||||
entity.remove(memberKey(factionId));
|
addFactionManager(entity);
|
||||||
|
entity.removeAll(FactionMember, (c) => c.factionId === factionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMember(entity: Entity, factionId: string): boolean {
|
export function isMember(entity: Entity, factionId: string): boolean {
|
||||||
return entity.has(FactionMember, memberKey(factionId));
|
return entity.has(FactionMember, (c) => c.factionId === factionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFactions(entity: Entity): string[] {
|
export function getFactions(entity: Entity): string[] {
|
||||||
|
|
@ -55,26 +100,26 @@ export namespace Factions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getReputation(entity: Entity, factionId: string): number {
|
export function getReputation(entity: Entity, factionId: string): number {
|
||||||
return entity.get(Reputation, repKey(factionId))?.score ?? 0;
|
return entity.get(Reputation, (c) => c.factionId === factionId)?.score ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setReputation(entity: Entity, factionId: string, value: number): void {
|
export function setReputation(entity: Entity, factionId: string, value: number): void {
|
||||||
const key = repKey(factionId);
|
addFactionManager(entity);
|
||||||
const existing = entity.get(Reputation, key);
|
const existing = entity.get(Reputation, (c) => c.factionId === factionId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.state.score = value;
|
existing.state.score = value;
|
||||||
} else {
|
} else {
|
||||||
entity.add(key, new Reputation(factionId, value));
|
entity.add(new Reputation(factionId, value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adjustReputation(entity: Entity, factionId: string, delta: number): void {
|
export function adjustReputation(entity: Entity, factionId: string, delta: number): void {
|
||||||
const key = repKey(factionId);
|
addFactionManager(entity);
|
||||||
const existing = entity.get(Reputation, key);
|
const existing = entity.get(Reputation, (c) => c.factionId === factionId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.adjust(delta);
|
existing.adjust(delta);
|
||||||
} else {
|
} else {
|
||||||
entity.add(key, new Reputation(factionId, delta));
|
entity.add(new Reputation(factionId, delta));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { component } from "../core/registry";
|
||||||
import type { RPGAction } from "../types";
|
import type { RPGAction } from "../types";
|
||||||
import { action, variable } from "../utils/decorators";
|
import { action, variable } from "../utils/decorators";
|
||||||
import { executeAction } from "../utils/variables";
|
import { executeAction } from "../utils/variables";
|
||||||
|
import { Equippable } from "./equipment";
|
||||||
|
|
||||||
interface ItemState {
|
interface ItemState {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -60,15 +61,21 @@ export namespace Items {
|
||||||
maxStack?: number;
|
maxStack?: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
usable?: { actions: RPGAction[]; consumeOnUse?: boolean };
|
usable?: { actions: RPGAction[]; consumeOnUse?: boolean };
|
||||||
|
equippable?: { slotType: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function register(world: World, id: string, name: string, options?: RegisterOptions) {
|
export function register(world: World, id: string, name: string, options?: RegisterOptions) {
|
||||||
const entity = world.createEntity(id);
|
const entity = world.createEntity(id);
|
||||||
entity.add('item', new Item(name, options?.description));
|
entity.add(new Item(name, options?.description));
|
||||||
if (options?.maxStack !== undefined)
|
if (options?.maxStack !== undefined) {
|
||||||
entity.add('stackable', new Stackable(options.maxStack));
|
entity.add(new Stackable(options.maxStack));
|
||||||
if (options?.usable)
|
}
|
||||||
entity.add('usable', new Usable(options.usable.actions, options.usable.consumeOnUse));
|
if (options?.usable) {
|
||||||
|
entity.add(new Usable(options.usable.actions, options.usable.consumeOnUse));
|
||||||
|
}
|
||||||
|
if (options?.equippable) {
|
||||||
|
entity.add(new Equippable(options.equippable.slotType));
|
||||||
|
}
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,12 @@ export class Health extends Stat {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
override update(amount: number) {
|
override update(amount: number) {
|
||||||
|
if (this.value > 0) {
|
||||||
super.update(amount);
|
super.update(amount);
|
||||||
if (this.value <= 0) {
|
if (this.value <= 0) {
|
||||||
this.kill();
|
this.kill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,6 @@ interface Var {
|
||||||
value: RPGVariables[string];
|
value: RPGVariables[string];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VariablesState {
|
|
||||||
vars: RPGVariables;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic runtime key-value store set by dialog actions, quest scripts, and game events —
|
* Generic runtime key-value store set by dialog actions, quest scripts, and game events —
|
||||||
* values whose keys are only known at runtime or come from data files.
|
* values whose keys are only known at runtime or come from data files.
|
||||||
|
|
@ -20,34 +16,32 @@ interface VariablesState {
|
||||||
* (e.g. health, stats, slot definitions).
|
* (e.g. health, stats, slot definitions).
|
||||||
*/
|
*/
|
||||||
@component
|
@component
|
||||||
export class Variables extends Component<VariablesState> {
|
export class Variables extends Component<RPGVariables> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ vars: {} });
|
super({});
|
||||||
}
|
}
|
||||||
|
|
||||||
override getVariables() {
|
override getVariables() {
|
||||||
return this.state.vars;
|
return this.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
set({ key, value }: Var) {
|
set({ key, value }: Var) {
|
||||||
const prev = this.state.vars[key];
|
const prev = this.state[key];
|
||||||
this.state.vars[key] = value;
|
this.state[key] = value;
|
||||||
this.emit('set', { key, value, prev });
|
this.emit('set', { key, value, prev });
|
||||||
return this.state.vars;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
unset(key: string) {
|
unset(key: string) {
|
||||||
const prev = this.state.vars[key];
|
const prev = this.state[key];
|
||||||
delete this.state.vars[key];
|
delete this.state[key];
|
||||||
this.emit('unset', { key, prev });
|
this.emit('unset', { key, prev });
|
||||||
return this.state.vars;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
increment({ key, value }: Var) {
|
update({ key, value }: Var) {
|
||||||
const currentValue = this.state.vars[key] ?? 0;
|
const currentValue = this.state[key] ?? 0;
|
||||||
if (typeof currentValue === 'number' && typeof value === 'number') {
|
if (typeof currentValue === 'number' && typeof value === 'number') {
|
||||||
this.set({ key, value: currentValue + value });
|
this.set({ key, value: currentValue + value });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ACTION_KEYS, VARIABLE_KEYS } from '../utils/decorators';
|
import { ACTION_KEYS, VARIABLE_KEYS } from '../utils/decorators';
|
||||||
import type { RPGActions, RPGVariables } from '../types';
|
import type { RPGActions, RPGVariables } from '../types';
|
||||||
|
import { getComponentName } from './registry';
|
||||||
|
|
||||||
interface EntityEvent<T = unknown> {
|
interface EntityEvent<T = unknown> {
|
||||||
target: Entity;
|
target: Entity;
|
||||||
|
|
@ -24,9 +25,9 @@ export const WORLD_ENTITY_COUNTER = Symbol('rpg.world.entityCounter');
|
||||||
|
|
||||||
export abstract class Component<TState = Record<string, unknown>> {
|
export abstract class Component<TState = Record<string, unknown>> {
|
||||||
entity!: Entity;
|
entity!: Entity;
|
||||||
key!: string;
|
|
||||||
|
|
||||||
private _state!: TState;
|
private _state!: TState;
|
||||||
|
private _key!: string | symbol;
|
||||||
|
|
||||||
constructor(state: TState) {
|
constructor(state: TState) {
|
||||||
this._state = state;
|
this._state = state;
|
||||||
|
|
@ -35,8 +36,21 @@ export abstract class Component<TState = Record<string, unknown>> {
|
||||||
get state(): TState { return this._state; }
|
get state(): TState { return this._state; }
|
||||||
protected set state(state: TState) { this._state = state; }
|
protected set state(state: TState) { this._state = state; }
|
||||||
|
|
||||||
|
get key(): string {
|
||||||
|
return typeof this._key === 'symbol'
|
||||||
|
? getComponentName(this.constructor) ?? this.constructor.name
|
||||||
|
: this._key;
|
||||||
|
}
|
||||||
|
set key(key: string | symbol) { this._key = key; }
|
||||||
|
|
||||||
protected emit(event: string, data?: unknown): void {
|
protected emit(event: string, data?: unknown): void {
|
||||||
this.entity.emit(`${this.key}.${event}`, data);
|
const componentKey = this.key;
|
||||||
|
const componentName = getComponentName(this.constructor);
|
||||||
|
const key = componentName && componentName !== componentKey
|
||||||
|
? `${componentName}(${componentKey})`
|
||||||
|
: componentKey;
|
||||||
|
|
||||||
|
this.entity.emit(`${key}.${event}`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(): void { }
|
onAdd(): void { }
|
||||||
|
|
@ -84,7 +98,7 @@ export abstract class System {
|
||||||
type ComponentFilter<T> = (component: T) => boolean;
|
type ComponentFilter<T> = (component: T) => boolean;
|
||||||
|
|
||||||
export class Entity {
|
export class Entity {
|
||||||
readonly #components = new Map<string, Component>();
|
readonly #components = new Map<string | symbol, Component>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly id: string,
|
readonly id: string,
|
||||||
|
|
@ -95,7 +109,15 @@ export class Entity {
|
||||||
return { self: this, world: this.world };
|
return { self: this, world: this.world };
|
||||||
}
|
}
|
||||||
|
|
||||||
add<T extends Component<any>>(key: string, component: T): T {
|
add<T extends Component<any>>(component: T): T;
|
||||||
|
add<T extends Component<any>>(key: string, component: T): T;
|
||||||
|
add<T extends Component<any>>(keyOrComponent: string | T, comp?: T): T {
|
||||||
|
const key = typeof keyOrComponent === 'string' ? keyOrComponent : Symbol();
|
||||||
|
const component = (keyOrComponent instanceof Component) ? keyOrComponent : comp;
|
||||||
|
|
||||||
|
if (component == null) {
|
||||||
|
throw new Error(`Component must be an instance of Component`);
|
||||||
|
}
|
||||||
const existing = this.#components.get(key);
|
const existing = this.#components.get(key);
|
||||||
if (existing) existing.onRemove();
|
if (existing) existing.onRemove();
|
||||||
component.entity = this;
|
component.entity = this;
|
||||||
|
|
@ -143,20 +165,29 @@ export class Entity {
|
||||||
has(key: string): boolean;
|
has(key: string): boolean;
|
||||||
has<T extends Component<any>>(ctor: Class<T>): boolean;
|
has<T extends Component<any>>(ctor: Class<T>): boolean;
|
||||||
has<T extends Component<any>>(ctor: Class<T>, key: string): boolean;
|
has<T extends Component<any>>(ctor: Class<T>, key: string): boolean;
|
||||||
has<T extends Component<any>>(ctorOrKey: Class<T> | string, key?: string): boolean {
|
has<T extends Component<any>>(ctor: Class<T>, filter: ComponentFilter<T>): boolean;
|
||||||
if (typeof ctorOrKey === 'string') return this.#components.has(ctorOrKey);
|
has<T extends Component<any>>(ctorOrKey: Class<T> | string, key?: string | ComponentFilter<T>): boolean {
|
||||||
if (key !== undefined) return this.#components.get(key) instanceof ctorOrKey;
|
if (typeof ctorOrKey === 'string') {
|
||||||
|
return this.#components.has(ctorOrKey);
|
||||||
|
}
|
||||||
|
if (typeof key === 'string') {
|
||||||
|
return this.#components.get(key) instanceof ctorOrKey;
|
||||||
|
}
|
||||||
for (const c of this.#components.values()) {
|
for (const c of this.#components.values()) {
|
||||||
if (c instanceof ctorOrKey) return true;
|
if (!(c instanceof ctorOrKey)) continue;
|
||||||
|
if (typeof key === 'function' && !key(c)) continue;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(key: string): void;
|
remove(key: string): void;
|
||||||
remove<T extends Component<any>>(ctor: Class<T>): void;
|
|
||||||
remove<T extends Component<any>>(component: T): void;
|
remove<T extends Component<any>>(component: T): void;
|
||||||
|
remove<T extends Component<any>>(ctor: Class<T>): void;
|
||||||
remove<T extends Component<any>>(ctor: Class<T>, key: string): void;
|
remove<T extends Component<any>>(ctor: Class<T>, key: string): void;
|
||||||
remove<T extends Component<any>>(ctorOrKey: Class<T> | T | string, key?: string): void {
|
remove<T extends Component<any>>(ctor: Class<T>, filter: ComponentFilter<T>): void;
|
||||||
|
remove<T extends Component<any>>(ctorOrKey: Class<T> | T | string, key?: string | ComponentFilter<T>): void {
|
||||||
if (typeof ctorOrKey === 'string') {
|
if (typeof ctorOrKey === 'string') {
|
||||||
this.#removeByKey(ctorOrKey);
|
this.#removeByKey(ctorOrKey);
|
||||||
return;
|
return;
|
||||||
|
|
@ -165,16 +196,28 @@ export class Entity {
|
||||||
this.#removeByKey(ctorOrKey.key);
|
this.#removeByKey(ctorOrKey.key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key !== undefined) {
|
if (typeof key === 'string') {
|
||||||
if (this.#components.get(key) instanceof ctorOrKey) this.#removeByKey(key);
|
if (this.#components.get(key) instanceof ctorOrKey) this.#removeByKey(key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const [k, c] of this.#components) {
|
for (const [k, c] of this.#components) {
|
||||||
if (c instanceof ctorOrKey) { this.#removeByKey(k); return; }
|
if (!(c instanceof ctorOrKey)) continue;
|
||||||
|
if (typeof key === 'function' && !key(c)) continue;
|
||||||
|
|
||||||
|
this.#removeByKey(k); return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#removeByKey(key: string): void {
|
removeAll<T extends Component<any>>(ctor: Class<T>, filter?: ComponentFilter<T>): void {
|
||||||
|
for (const [k, c] of this.#components) {
|
||||||
|
if (!(c instanceof ctor)) continue;
|
||||||
|
if (typeof filter === 'function' && !filter(c)) continue;
|
||||||
|
|
||||||
|
this.#removeByKey(k); return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#removeByKey(key: string | symbol): void {
|
||||||
const c = this.#components.get(key);
|
const c = this.#components.get(key);
|
||||||
if (c) { c.onRemove(); this.#components.delete(key); }
|
if (c) { c.onRemove(); this.#components.delete(key); }
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +248,7 @@ export class Entity {
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
[Symbol.iterator](): IterableIterator<[string, Component]> {
|
[Symbol.iterator](): IterableIterator<[string, Component]> {
|
||||||
return this.#components.entries();
|
return this.#components.values().map(c => [c.key, c]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { RPGAction, RPGActions, RPGVariables } from "../types";
|
import type { RPGAction, RPGActions, RPGVariables } from "../types";
|
||||||
import { isEvalContext, World } from "../core/world";
|
import { isEvalContext, World } from "../core/world";
|
||||||
import type { EvalContext, Entity } from "../core/world";
|
import type { EvalContext, Entity } from "../core/world";
|
||||||
|
import { getComponentName } from "../core/registry";
|
||||||
|
|
||||||
export function resolveVariables(target: Entity | World): RPGVariables {
|
export function resolveVariables(target: Entity | World): RPGVariables {
|
||||||
const result: RPGVariables = {};
|
const result: RPGVariables = {};
|
||||||
|
|
@ -11,7 +12,12 @@ export function resolveVariables(target: Entity | World): RPGVariables {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const [key, component] of target) {
|
for (const [componentKey, component] of target) {
|
||||||
|
const componentName = getComponentName(component.constructor);
|
||||||
|
const key = componentName && componentName !== componentKey
|
||||||
|
? `${componentName}(${componentKey})`
|
||||||
|
: componentKey;
|
||||||
|
|
||||||
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 !== '.') {
|
||||||
|
|
@ -56,7 +62,12 @@ export function resolveActions(target: Entity | World): RPGActions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const [key, component] of target) {
|
for (const [componentKey, component] of target) {
|
||||||
|
const componentName = getComponentName(component.constructor);
|
||||||
|
const key = componentName && componentName !== componentKey
|
||||||
|
? `${componentName}(${componentKey})`
|
||||||
|
: componentKey;
|
||||||
|
|
||||||
for (const [actionKey, fn] of Object.entries(component.getActions())) {
|
for (const [actionKey, fn] of Object.entries(component.getActions())) {
|
||||||
result[`${key}.${actionKey}`] = fn;
|
result[`${key}.${actionKey}`] = fn;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,57 @@
|
||||||
import { World } from "@common/rpg/core/world";
|
import { World } from "@common/rpg/core/world";
|
||||||
import { Inventory } from "@common/rpg/components/inventory";
|
import { Inventory } from "@common/rpg/components/inventory";
|
||||||
import { Health } from "@common/rpg/components/stat";
|
import { Health, Stat } 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/quest";
|
import { QuestSystem } from "@common/rpg/systems/quest";
|
||||||
import { Items } from "@common/rpg/components/item";
|
import { Items } from "@common/rpg/components/item";
|
||||||
import { resolveVariables, resolveActions, executeAction } from "@common/rpg/utils/variables";
|
import { resolveVariables, resolveActions, executeAction } from "@common/rpg/utils/variables";
|
||||||
import { Serialization } from "@common/rpg/core/serialization";
|
import { Serialization } from "@common/rpg/core/serialization";
|
||||||
|
import { Factions } from "@common/rpg/components/faction";
|
||||||
|
import { Effect } from "@common/rpg/components/effect";
|
||||||
|
import { Equipment } from "@common/rpg/components/equipment";
|
||||||
|
|
||||||
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, 'helmet', 'Iron Helmet');
|
||||||
Items.register(world, 'boots', 'Leather Boots', { maxStack: 2 });
|
const boots = Items.register(world, 'boots', 'Leather Boots', { maxStack: 2, equippable: { slotType: 'feet' } });
|
||||||
|
boots.add(new Effect({
|
||||||
|
targetStat: 'agl',
|
||||||
|
delta: 10,
|
||||||
|
}));
|
||||||
|
|
||||||
const player = world.createEntity('player');
|
const player = world.createEntity('player');
|
||||||
player.add('inventory', new Inventory(['head', 'legs']));
|
const inventory = player.add(new Inventory());
|
||||||
player.add('health', new Health({value: 100, max: 100}));
|
player.add(new Equipment('head', { slotName: 'feet', type: 'feet' }));
|
||||||
player.add('vars', new Variables());
|
player.add(new Health({ value: 100, max: 100 }));
|
||||||
player.add('quests', new QuestLog([{
|
player.add(new Variables());
|
||||||
|
player.add(new QuestLog([{
|
||||||
id: 'test',
|
id: 'test',
|
||||||
description: 'Test quest',
|
description: 'Test quest',
|
||||||
title: 'Test',
|
title: 'Test',
|
||||||
stages: [],
|
stages: [],
|
||||||
}]));
|
}]));
|
||||||
|
player.add('str', new Stat({ value: 100 }));
|
||||||
|
player.add('agl', new Stat({ value: 100 }));
|
||||||
|
|
||||||
|
Factions.join(player, 'boobs');
|
||||||
|
Factions.adjustReputation(player, 'guards', 10);
|
||||||
|
Factions.adjustReputation(player, 'bandits', -10);
|
||||||
|
|
||||||
console.log(resolveVariables(world));
|
console.log(resolveVariables(world));
|
||||||
|
|
||||||
player.get(Inventory)?.add({ itemId: 'helmet', amount: 1, slotId: 'head' });
|
inventory.add({ itemId: 'helmet', amount: 1 });
|
||||||
|
inventory.add({ itemId: 'boots', amount: 1 });
|
||||||
|
inventory.equip('boots');
|
||||||
|
|
||||||
const vars = player.get(Variables)!;
|
const vars = player.get(Variables)!;
|
||||||
vars.set({ key: 'test', value: 'test' });
|
vars.set({ key: 'test', value: 'test' });
|
||||||
|
|
||||||
await executeAction('player.quests.test.start', world);
|
await executeAction('player.QuestLog.test.start', world);
|
||||||
|
|
||||||
console.log(resolveActions(world));
|
console.log(resolveActions(world));
|
||||||
console.log(resolveVariables(world));
|
console.log(resolveVariables(world));
|
||||||
console.log(Serialization.serialize(world));
|
console.log(JSON.parse(Serialization.serialize(world)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,15 @@ describe('Cooldown — initial state', () => {
|
||||||
it('starts not ready', () => {
|
it('starts not ready', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(5));
|
e.add(new Cooldown(5));
|
||||||
expect(e.get(Cooldown, 'cd')!.ready).toBeFalse();
|
expect(e.get(Cooldown)!.ready).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('remaining equals duration on creation', () => {
|
it('remaining equals duration on creation', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(3));
|
e.add(new Cooldown(3));
|
||||||
const cd = e.get(Cooldown, 'cd')!;
|
const cd = e.get(Cooldown)!;
|
||||||
expect(cd.state.remaining).toBe(3);
|
expect(cd.state.remaining).toBe(3);
|
||||||
expect(cd.state.duration).toBe(3);
|
expect(cd.state.duration).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
@ -31,8 +31,8 @@ describe('Cooldown — clear()', () => {
|
||||||
it('marks as ready immediately', () => {
|
it('marks as ready immediately', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(10));
|
e.add(new Cooldown(10));
|
||||||
const cd = e.get(Cooldown, 'cd')!;
|
const cd = e.get(Cooldown)!;
|
||||||
cd.clear();
|
cd.clear();
|
||||||
expect(cd.ready).toBeTrue();
|
expect(cd.ready).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
@ -40,21 +40,21 @@ describe('Cooldown — clear()', () => {
|
||||||
it("emits 'ready' event", () => {
|
it("emits 'ready' event", () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(10));
|
e.add(new Cooldown(10));
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
e.on('cd.ready', () => events.push(true));
|
e.on('Cooldown.ready', () => events.push(true));
|
||||||
e.get(Cooldown, 'cd')!.clear();
|
e.get(Cooldown)!.clear();
|
||||||
expect(events.length).toBe(1);
|
expect(events.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is no-op when already ready', () => {
|
it('is no-op when already ready', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(1));
|
e.add(new Cooldown(1));
|
||||||
const cd = e.get(Cooldown, 'cd')!;
|
const cd = e.get(Cooldown)!;
|
||||||
cd.clear();
|
cd.clear();
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
e.on('cd.ready', () => events.push(true));
|
e.on('Cooldown.ready', () => events.push(true));
|
||||||
cd.clear();
|
cd.clear();
|
||||||
expect(events.length).toBe(0);
|
expect(events.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
@ -64,8 +64,8 @@ describe('Cooldown — reset()', () => {
|
||||||
it('resets remaining to duration', () => {
|
it('resets remaining to duration', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(5));
|
e.add(new Cooldown(5));
|
||||||
const cd = e.get(Cooldown, 'cd')!;
|
const cd = e.get(Cooldown)!;
|
||||||
cd.clear();
|
cd.clear();
|
||||||
cd.reset();
|
cd.reset();
|
||||||
expect(cd.ready).toBeFalse();
|
expect(cd.ready).toBeFalse();
|
||||||
|
|
@ -75,8 +75,8 @@ describe('Cooldown — reset()', () => {
|
||||||
it('reset(duration) changes duration and remaining', () => {
|
it('reset(duration) changes duration and remaining', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(5));
|
e.add(new Cooldown(5));
|
||||||
const cd = e.get(Cooldown, 'cd')!;
|
const cd = e.get(Cooldown)!;
|
||||||
cd.reset(10);
|
cd.reset(10);
|
||||||
expect(cd.state.duration).toBe(10);
|
expect(cd.state.duration).toBe(10);
|
||||||
expect(cd.state.remaining).toBe(10);
|
expect(cd.state.remaining).toBe(10);
|
||||||
|
|
@ -87,8 +87,8 @@ describe('Cooldown — update(dt)', () => {
|
||||||
it('counts down remaining', () => {
|
it('counts down remaining', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(5));
|
e.add(new Cooldown(5));
|
||||||
const cd = e.get(Cooldown, 'cd')!;
|
const cd = e.get(Cooldown)!;
|
||||||
cd.update(2);
|
cd.update(2);
|
||||||
expect(cd.state.remaining).toBe(3);
|
expect(cd.state.remaining).toBe(3);
|
||||||
expect(cd.ready).toBeFalse();
|
expect(cd.ready).toBeFalse();
|
||||||
|
|
@ -97,8 +97,8 @@ describe('Cooldown — update(dt)', () => {
|
||||||
it('becomes ready when remaining reaches zero', () => {
|
it('becomes ready when remaining reaches zero', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(3));
|
e.add(new Cooldown(3));
|
||||||
const cd = e.get(Cooldown, 'cd')!;
|
const cd = e.get(Cooldown)!;
|
||||||
cd.update(3);
|
cd.update(3);
|
||||||
expect(cd.ready).toBeTrue();
|
expect(cd.ready).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
@ -106,18 +106,18 @@ describe('Cooldown — update(dt)', () => {
|
||||||
it("emits 'ready' when countdown completes", () => {
|
it("emits 'ready' when countdown completes", () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(2));
|
e.add(new Cooldown(2));
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
e.on('cd.ready', () => events.push(true));
|
e.on('Cooldown.ready', () => events.push(true));
|
||||||
e.get(Cooldown, 'cd')!.update(2);
|
e.get(Cooldown)!.update(2);
|
||||||
expect(events.length).toBe(1);
|
expect(events.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not go below zero', () => {
|
it('does not go below zero', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(1));
|
e.add(new Cooldown(1));
|
||||||
const cd = e.get(Cooldown, 'cd')!;
|
const cd = e.get(Cooldown)!;
|
||||||
cd.update(100);
|
cd.update(100);
|
||||||
expect(cd.state.remaining).toBe(0);
|
expect(cd.state.remaining).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
@ -125,11 +125,11 @@ describe('Cooldown — update(dt)', () => {
|
||||||
it('is no-op when already ready', () => {
|
it('is no-op when already ready', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(1));
|
e.add(new Cooldown(1));
|
||||||
const cd = e.get(Cooldown, 'cd')!;
|
const cd = e.get(Cooldown)!;
|
||||||
cd.clear();
|
cd.clear();
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
e.on('cd.ready', () => events.push(true));
|
e.on('Cooldown.ready', () => events.push(true));
|
||||||
cd.update(1);
|
cd.update(1);
|
||||||
expect(events.length).toBe(0);
|
expect(events.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
@ -139,31 +139,31 @@ describe('CooldownSystem', () => {
|
||||||
it('drives all cooldowns each tick', () => {
|
it('drives all cooldowns each tick', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(2));
|
e.add(new Cooldown(2));
|
||||||
w.update(1);
|
w.update(1);
|
||||||
expect(e.get(Cooldown, 'cd')!.state.remaining).toBe(1);
|
expect(e.get(Cooldown)!.state.remaining).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('marks cooldown ready after enough ticks', () => {
|
it('marks cooldown ready after enough ticks', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
e.add('cd', new Cooldown(2));
|
e.add(new Cooldown(2));
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
e.on('cd.ready', () => events.push(true));
|
e.on('Cooldown.ready', () => events.push(true));
|
||||||
w.update(1);
|
w.update(1);
|
||||||
w.update(1);
|
w.update(1);
|
||||||
expect(events.length).toBe(1);
|
expect(events.length).toBe(1);
|
||||||
expect(e.get(Cooldown, 'cd')!.ready).toBeTrue();
|
expect(e.get(Cooldown)!.ready).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles multiple cooldowns on different entities', () => {
|
it('handles multiple cooldowns on different entities', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const a = w.createEntity('a');
|
const a = w.createEntity('a');
|
||||||
const b = w.createEntity('b');
|
const b = w.createEntity('b');
|
||||||
a.add('cd', new Cooldown(1));
|
a.add(new Cooldown(1));
|
||||||
b.add('cd', new Cooldown(3));
|
b.add(new Cooldown(3));
|
||||||
w.update(2);
|
w.update(2);
|
||||||
expect(a.get(Cooldown, 'cd')!.ready).toBeTrue();
|
expect(a.get(Cooldown)!.ready).toBeTrue();
|
||||||
expect(b.get(Cooldown, 'cd')!.ready).toBeFalse();
|
expect(b.get(Cooldown)!.ready).toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -69,9 +69,9 @@ describe('Effect — duration', () => {
|
||||||
|
|
||||||
it('emits expired before removal', () => {
|
it('emits expired before removal', () => {
|
||||||
const { w, e } = withStat(10);
|
const { w, e } = withStat(10);
|
||||||
e.add('fx', new Effect({ targetStat: 'str', delta: 1, duration: 1 }));
|
e.add(new Effect({ targetStat: 'str', delta: 1, duration: 1 }));
|
||||||
const events: string[] = [];
|
const events: string[] = [];
|
||||||
e.on('fx.expired', () => events.push('expired'));
|
e.on('Effect.expired', () => events.push('expired'));
|
||||||
w.update(1);
|
w.update(1);
|
||||||
expect(events).toEqual(['expired']);
|
expect(events).toEqual(['expired']);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ function makeSword(w: World, id = 'sword') {
|
||||||
return sword;
|
return sword;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePlayer(w: World, slots: ConstructorParameters<typeof Equipment>[0] = [{ slotName: 'weapon', type: 'weapon' }]) {
|
function makePlayer(w: World, slots: ConstructorParameters<typeof Equipment>[0] = { slotName: 'weapon', type: 'weapon' }) {
|
||||||
const player = w.createEntity('player');
|
const player = w.createEntity('player');
|
||||||
player.add('str', new Stat({ value: 10 }));
|
player.add('str', new Stat({ value: 10 }));
|
||||||
player.add('equipment', new Equipment(slots));
|
player.add(new Equipment(slots));
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ describe('Equipment — equip', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
makeSword(w);
|
makeSword(w);
|
||||||
const player = w.createEntity('player');
|
const player = w.createEntity('player');
|
||||||
player.add('equipment', new Equipment(['slot1'])); // generic slot
|
player.add('equipment', new Equipment('slot1')); // generic slot
|
||||||
expect(player.get(Equipment)!.equip({ slotName: 'slot1', itemId: 'sword' })).toBeTrue();
|
expect(player.get(Equipment)!.equip({ slotName: 'slot1', itemId: 'sword' })).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ describe('Equipment — equip', () => {
|
||||||
makeSword(w);
|
makeSword(w);
|
||||||
const player = makePlayer(w);
|
const player = makePlayer(w);
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
player.on('equipment.equip', ({ data }) => events.push(data));
|
player.on('Equipment.equip', ({ data }) => events.push(data));
|
||||||
player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'sword' });
|
player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'sword' });
|
||||||
expect(events).toEqual([{ slotName: 'weapon', itemId: 'sword' }]);
|
expect(events).toEqual([{ slotName: 'weapon', itemId: 'sword' }]);
|
||||||
});
|
});
|
||||||
|
|
@ -165,7 +165,7 @@ describe('Equipment — unequip', () => {
|
||||||
const eq = player.get(Equipment)!;
|
const eq = player.get(Equipment)!;
|
||||||
eq.equip({ slotName: 'weapon', itemId: 'sword' });
|
eq.equip({ slotName: 'weapon', itemId: 'sword' });
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
player.on('equipment.unequip', ({ data }) => events.push(data));
|
player.on('Equipment.unequip', ({ data }) => events.push(data));
|
||||||
eq.unequip('weapon');
|
eq.unequip('weapon');
|
||||||
expect(events).toEqual([{ slotName: 'weapon', itemId: 'sword' }]);
|
expect(events).toEqual([{ slotName: 'weapon', itemId: 'sword' }]);
|
||||||
});
|
});
|
||||||
|
|
@ -177,10 +177,10 @@ describe('Equipment — queries', () => {
|
||||||
makeSword(w, 'sword1');
|
makeSword(w, 'sword1');
|
||||||
makeSword(w, 'sword2');
|
makeSword(w, 'sword2');
|
||||||
const player = w.createEntity('player');
|
const player = w.createEntity('player');
|
||||||
player.add('equipment', new Equipment([
|
player.add('equipment', new Equipment(
|
||||||
{ slotName: 'main', type: 'weapon' },
|
{ slotName: 'main', type: 'weapon' },
|
||||||
{ slotName: 'off', type: 'weapon' },
|
{ slotName: 'off', type: 'weapon' },
|
||||||
]));
|
));
|
||||||
const eq = player.get(Equipment)!;
|
const eq = player.get(Equipment)!;
|
||||||
eq.equip({ slotName: 'main', itemId: 'sword1' });
|
eq.equip({ slotName: 'main', itemId: 'sword1' });
|
||||||
eq.equip({ slotName: 'off', itemId: 'sword2' });
|
eq.equip({ slotName: 'off', itemId: 'sword2' });
|
||||||
|
|
@ -192,10 +192,10 @@ describe('Equipment — queries', () => {
|
||||||
it('findCompatibleSlot prefers typed slot over generic', () => {
|
it('findCompatibleSlot prefers typed slot over generic', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const player = w.createEntity('player');
|
const player = w.createEntity('player');
|
||||||
player.add('equipment', new Equipment([
|
player.add('equipment', new Equipment(
|
||||||
'generic',
|
'generic',
|
||||||
{ slotName: 'weapon', type: 'weapon' },
|
{ slotName: 'weapon', type: 'weapon' },
|
||||||
]));
|
));
|
||||||
const eq = player.get(Equipment)!;
|
const eq = player.get(Equipment)!;
|
||||||
expect(eq.findCompatibleSlot('weapon')).toBe('weapon');
|
expect(eq.findCompatibleSlot('weapon')).toBe('weapon');
|
||||||
});
|
});
|
||||||
|
|
@ -203,7 +203,7 @@ describe('Equipment — queries', () => {
|
||||||
it('findCompatibleSlot falls back to generic slot', () => {
|
it('findCompatibleSlot falls back to generic slot', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const player = w.createEntity('player');
|
const player = w.createEntity('player');
|
||||||
player.add('equipment', new Equipment(['generic']));
|
player.add('equipment', new Equipment('generic'));
|
||||||
const eq = player.get(Equipment)!;
|
const eq = player.get(Equipment)!;
|
||||||
expect(eq.findCompatibleSlot('weapon')).toBe('generic');
|
expect(eq.findCompatibleSlot('weapon')).toBe('generic');
|
||||||
});
|
});
|
||||||
|
|
@ -211,7 +211,7 @@ describe('Equipment — queries', () => {
|
||||||
it('findCompatibleSlot returns null when no compatible slot', () => {
|
it('findCompatibleSlot returns null when no compatible slot', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const player = w.createEntity('player');
|
const player = w.createEntity('player');
|
||||||
player.add('equipment', new Equipment([{ slotName: 'armor', type: 'armor' }]));
|
player.add('equipment', new Equipment({ slotName: 'armor', type: 'armor' }));
|
||||||
const eq = player.get(Equipment)!;
|
const eq = player.get(Equipment)!;
|
||||||
expect(eq.findCompatibleSlot('weapon')).toBeNull();
|
expect(eq.findCompatibleSlot('weapon')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ describe('Experience — award XP (array spec)', () => {
|
||||||
it("emits 'levelup' with prev and new level", () => {
|
it("emits 'levelup' with prev and new level", () => {
|
||||||
const { e, xp } = withXp([100]);
|
const { e, xp } = withXp([100]);
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
e.on('xp.levelup', ({ data }) => events.push(data));
|
e.on('Experience(xp).levelup', ({ data }) => events.push(data));
|
||||||
xp.award(100);
|
xp.award(100);
|
||||||
expect(events).toEqual([{ level: 2, prev: 1 }]);
|
expect(events).toEqual([{ level: 2, prev: 1 }]);
|
||||||
});
|
});
|
||||||
|
|
@ -62,7 +62,7 @@ describe('Experience — award XP (array spec)', () => {
|
||||||
it("emits 'levelup' for each level gained", () => {
|
it("emits 'levelup' for each level gained", () => {
|
||||||
const { e, xp } = withXp([100, 200]);
|
const { e, xp } = withXp([100, 200]);
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
e.on('xp.levelup', ({ data }) => events.push(data));
|
e.on('Experience(xp).levelup', ({ data }) => events.push(data));
|
||||||
xp.award(300); // gains 2 levels
|
xp.award(300); // gains 2 levels
|
||||||
expect(events.length).toBe(2);
|
expect(events.length).toBe(2);
|
||||||
expect((events[0] as any).level).toBe(2);
|
expect((events[0] as any).level).toBe(2);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ describe('FactionMember', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity('player');
|
const e = w.createEntity('player');
|
||||||
Factions.join(e, 'guards');
|
Factions.join(e, 'guards');
|
||||||
expect(resolveVariables(w)['player.faction.guards.member']).toBeTrue();
|
expect(resolveVariables(w)['player.FactionMember.guards']).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -85,7 +85,7 @@ describe('Reputation', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity();
|
const e = w.createEntity();
|
||||||
Factions.setReputation(e, 'guards', 10);
|
Factions.setReputation(e, 'guards', 10);
|
||||||
const comp = e.get(Reputation, 'faction.guards.rep')!;
|
const comp = e.get(Reputation, (c) => c.factionId == 'guards')!;
|
||||||
comp.adjust(30);
|
comp.adjust(30);
|
||||||
expect(comp.score).toBe(40);
|
expect(comp.score).toBe(40);
|
||||||
});
|
});
|
||||||
|
|
@ -94,7 +94,7 @@ describe('Reputation', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const e = w.createEntity('player');
|
const e = w.createEntity('player');
|
||||||
Factions.setReputation(e, 'guards', 50);
|
Factions.setReputation(e, 'guards', 50);
|
||||||
expect(resolveVariables(w)['player.faction.guards.rep']).toBe(50);
|
expect(resolveVariables(w)['player.Reputation.guards']).toBe(50);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ describe('Inventory — use', () => {
|
||||||
player.add('str', new Stat({ value: 10 }));
|
player.add('str', new Stat({ value: 10 }));
|
||||||
player.add('inv', new Inventory());
|
player.add('inv', new Inventory());
|
||||||
Items.register(w, 'potion', 'Health Potion', {
|
Items.register(w, 'potion', 'Health Potion', {
|
||||||
usable: { actions: [{ type: 'str.update', arg: 5 }], consumeOnUse: false },
|
usable: { actions: [{ type: 'Stat(str).update', arg: 5 }], consumeOnUse: false },
|
||||||
});
|
});
|
||||||
player.get(Inventory)!.add({ itemId: 'potion', amount: 1 });
|
player.get(Inventory)!.add({ itemId: 'potion', amount: 1 });
|
||||||
player.get(Inventory)!.use({ itemId: 'potion' });
|
player.get(Inventory)!.use({ itemId: 'potion' });
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ function simpleQuest(id = 'q1', actions: Quest['stages'][0]['actions'] = []): Qu
|
||||||
stages: [{
|
stages: [{
|
||||||
id: 'stage0',
|
id: 'stage0',
|
||||||
description: 'Do the thing',
|
description: 'Do the thing',
|
||||||
objectives: [{ id: 'obj', description: 'Done?', condition: 'vars.done == true' }],
|
objectives: [{ id: 'obj', description: 'Done?', condition: 'Variables(vars).done == true' }],
|
||||||
actions,
|
actions,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
@ -38,13 +38,13 @@ function twoStageQuest(id = 'q2'): Quest {
|
||||||
{
|
{
|
||||||
id: 'stage0',
|
id: 'stage0',
|
||||||
description: 'Step 1',
|
description: 'Step 1',
|
||||||
objectives: [{ id: 'obj0', description: 'Reach step 1', condition: 'vars.step >= 1' }],
|
objectives: [{ id: 'obj0', description: 'Reach step 1', condition: 'Variables(vars).step >= 1' }],
|
||||||
actions: [],
|
actions: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stage1',
|
id: 'stage1',
|
||||||
description: 'Step 2',
|
description: 'Step 2',
|
||||||
objectives: [{ id: 'obj1', description: 'Reach step 2', condition: 'vars.step >= 2' }],
|
objectives: [{ id: 'obj1', description: 'Reach step 2', condition: 'Variables(vars).step >= 2' }],
|
||||||
actions: [],
|
actions: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -54,7 +54,7 @@ function twoStageQuest(id = 'q2'): Quest {
|
||||||
function makePlayer(w: World, quests: Quest[] = []) {
|
function makePlayer(w: World, quests: Quest[] = []) {
|
||||||
const player = w.createEntity('player');
|
const player = w.createEntity('player');
|
||||||
const vars = player.add('vars', new Variables());
|
const vars = player.add('vars', new Variables());
|
||||||
const questLog = player.add('questLog', new QuestLog(quests));
|
const questLog = player.add(new QuestLog(quests));
|
||||||
return { player, vars, questLog };
|
return { player, vars, questLog };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,7 +170,7 @@ describe('QuestLog — events', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const { player, questLog } = makePlayer(w, [simpleQuest()]);
|
const { player, questLog } = makePlayer(w, [simpleQuest()]);
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
player.on('questLog.started', ({ data }) => events.push(data));
|
player.on('QuestLog.started', ({ data }) => events.push(data));
|
||||||
questLog.start('q1');
|
questLog.start('q1');
|
||||||
expect(events).toEqual([{ questId: 'q1' }]);
|
expect(events).toEqual([{ questId: 'q1' }]);
|
||||||
});
|
});
|
||||||
|
|
@ -179,7 +179,7 @@ describe('QuestLog — events', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const { player, questLog } = makePlayer(w, [simpleQuest()]);
|
const { player, questLog } = makePlayer(w, [simpleQuest()]);
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
player.on('questLog.completed', ({ data }) => events.push(data));
|
player.on('QuestLog.completed', ({ data }) => events.push(data));
|
||||||
questLog.start('q1');
|
questLog.start('q1');
|
||||||
questLog.complete('q1');
|
questLog.complete('q1');
|
||||||
expect(events).toEqual([{ questId: 'q1' }]);
|
expect(events).toEqual([{ questId: 'q1' }]);
|
||||||
|
|
@ -189,7 +189,7 @@ describe('QuestLog — events', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const { player, questLog } = makePlayer(w, [simpleQuest()]);
|
const { player, questLog } = makePlayer(w, [simpleQuest()]);
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
player.on('questLog.failed', ({ data }) => events.push(data));
|
player.on('QuestLog.failed', ({ data }) => events.push(data));
|
||||||
questLog.start('q1');
|
questLog.start('q1');
|
||||||
questLog.fail('q1');
|
questLog.fail('q1');
|
||||||
expect(events).toEqual([{ questId: 'q1' }]);
|
expect(events).toEqual([{ questId: 'q1' }]);
|
||||||
|
|
@ -199,7 +199,7 @@ describe('QuestLog — events', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const { player, questLog } = makePlayer(w, [simpleQuest()]);
|
const { player, questLog } = makePlayer(w, [simpleQuest()]);
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
player.on('questLog.abandoned', ({ data }) => events.push(data));
|
player.on('QuestLog.abandoned', ({ data }) => events.push(data));
|
||||||
questLog.start('q1');
|
questLog.start('q1');
|
||||||
questLog.abandon('q1');
|
questLog.abandon('q1');
|
||||||
expect(events).toEqual([{ questId: 'q1' }]);
|
expect(events).toEqual([{ questId: 'q1' }]);
|
||||||
|
|
@ -254,14 +254,14 @@ describe('QuestLog — availability', () => {
|
||||||
|
|
||||||
it('quest with unsatisfied condition is not available', () => {
|
it('quest with unsatisfied condition is not available', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const quest: Quest = { ...simpleQuest(), conditions: ['vars.unlocked == true'] };
|
const quest: Quest = { ...simpleQuest(), conditions: ['Variables(vars).unlocked == true'] };
|
||||||
const { player, questLog } = makePlayer(w, [quest]);
|
const { player, questLog } = makePlayer(w, [quest]);
|
||||||
expect(questLog.isAvailable('q1', player.context)).toBeFalse();
|
expect(questLog.isAvailable('q1', player.context)).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('quest with satisfied condition is available', () => {
|
it('quest with satisfied condition is available', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const quest: Quest = { ...simpleQuest(), conditions: ['vars.unlocked == true'] };
|
const quest: Quest = { ...simpleQuest(), conditions: ['Variables(vars).unlocked == true'] };
|
||||||
const { player, vars, questLog } = makePlayer(w, [quest]);
|
const { player, vars, questLog } = makePlayer(w, [quest]);
|
||||||
vars.set({ key: 'unlocked', value: true });
|
vars.set({ key: 'unlocked', value: true });
|
||||||
expect(questLog.isAvailable('q1', player.context)).toBeTrue();
|
expect(questLog.isAvailable('q1', player.context)).toBeTrue();
|
||||||
|
|
@ -315,7 +315,7 @@ describe('QuestLog — _advance', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const { player, questLog } = makePlayer(w, [twoStageQuest()]);
|
const { player, questLog } = makePlayer(w, [twoStageQuest()]);
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
player.on('questLog.stage', ({ data }) => events.push(data));
|
player.on('QuestLog.stage', ({ data }) => events.push(data));
|
||||||
questLog.start('q2');
|
questLog.start('q2');
|
||||||
questLog._advance('q2');
|
questLog._advance('q2');
|
||||||
expect((events[0] as any).questId).toBe('q2');
|
expect((events[0] as any).questId).toBe('q2');
|
||||||
|
|
@ -334,7 +334,7 @@ describe('QuestLog — _advance', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
const { player, questLog } = makePlayer(w, [simpleQuest()]);
|
const { player, questLog } = makePlayer(w, [simpleQuest()]);
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
player.on('questLog.completed', ({ data }) => events.push(data));
|
player.on('QuestLog.completed', ({ data }) => events.push(data));
|
||||||
questLog.start('q1');
|
questLog.start('q1');
|
||||||
questLog._advance('q1');
|
questLog._advance('q1');
|
||||||
expect(events).toEqual([{ questId: 'q1' }]);
|
expect(events).toEqual([{ questId: 'q1' }]);
|
||||||
|
|
@ -357,8 +357,8 @@ describe('Quests.validate', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes when action type is in the known actions list', () => {
|
it('passes when action type is in the known actions list', () => {
|
||||||
const quest = simpleQuest('q', [{ type: 'vars.set' }]);
|
const quest = simpleQuest('q', [{ type: 'Variables(vars).set' }]);
|
||||||
const errors = Quests.validate(quest, ['vars.set']);
|
const errors = Quests.validate(quest, ['Variables(vars).set']);
|
||||||
expect(errors).toHaveLength(0);
|
expect(errors).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -405,7 +405,7 @@ describe('QuestSystem — objective completion', () => {
|
||||||
it('runs stage actions before advancing', () => {
|
it('runs stage actions before advancing', () => {
|
||||||
const w = world();
|
const w = world();
|
||||||
// action sets vars.reward = true on the player entity
|
// action sets vars.reward = true on the player entity
|
||||||
const quest = simpleQuest('q1', [{ type: 'vars.set', arg: { key: 'reward', value: true } }]);
|
const quest = simpleQuest('q1', [{ type: 'Variables(vars).set', arg: { key: 'reward', value: true } }]);
|
||||||
const { vars, questLog } = makePlayer(w, [quest]);
|
const { vars, questLog } = makePlayer(w, [quest]);
|
||||||
|
|
||||||
questLog.start('q1');
|
questLog.start('q1');
|
||||||
|
|
@ -413,7 +413,7 @@ describe('QuestSystem — objective completion', () => {
|
||||||
w.update(1);
|
w.update(1);
|
||||||
|
|
||||||
expect(questLog.getState('q1')?.status).toBe('completed');
|
expect(questLog.getState('q1')?.status).toBe('completed');
|
||||||
expect(vars.state.vars['reward']).toBe(true);
|
expect(vars.state.reward).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -462,9 +462,9 @@ describe('QuestSystem — fail conditions', () => {
|
||||||
stages: [{
|
stages: [{
|
||||||
id: 'stage0',
|
id: 'stage0',
|
||||||
description: 'Do it',
|
description: 'Do it',
|
||||||
objectives: [{ id: 'obj', description: 'Done?', condition: 'vars.done == true' }],
|
objectives: [{ id: 'obj', description: 'Done?', condition: 'Variables(vars).done == true' }],
|
||||||
actions: [],
|
actions: [],
|
||||||
failConditions: ['vars.failed == true'],
|
failConditions: ['Variables(vars).failed == true'],
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
const { vars, questLog } = makePlayer(w, [quest]);
|
const { vars, questLog } = makePlayer(w, [quest]);
|
||||||
|
|
@ -485,9 +485,9 @@ describe('QuestSystem — fail conditions', () => {
|
||||||
stages: [{
|
stages: [{
|
||||||
id: 'stage0',
|
id: 'stage0',
|
||||||
description: 'Both',
|
description: 'Both',
|
||||||
objectives: [{ id: 'obj', description: 'Done?', condition: 'vars.done == true' }],
|
objectives: [{ id: 'obj', description: 'Done?', condition: 'Variables(vars).done == true' }],
|
||||||
actions: [],
|
actions: [],
|
||||||
failConditions: ['vars.done == true'], // same condition
|
failConditions: ['Variables(vars).done == true'], // same condition
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
const { vars, questLog } = makePlayer(w, [quest]);
|
const { vars, questLog } = makePlayer(w, [quest]);
|
||||||
|
|
@ -508,11 +508,11 @@ describe('QuestSystem — multiple quests', () => {
|
||||||
|
|
||||||
const q1: Quest = {
|
const q1: Quest = {
|
||||||
id: 'q1', title: 'Q1', description: '',
|
id: 'q1', title: 'Q1', description: '',
|
||||||
stages: [{ id: 's', description: '', objectives: [{ id: 'o', description: '', condition: 'vars.done1 == true' }], actions: [] }],
|
stages: [{ id: 's', description: '', objectives: [{ id: 'o', description: '', condition: 'Variables(vars).done1 == true' }], actions: [] }],
|
||||||
};
|
};
|
||||||
const q2: Quest = {
|
const q2: Quest = {
|
||||||
id: 'q2', title: 'Q2', description: '',
|
id: 'q2', title: 'Q2', description: '',
|
||||||
stages: [{ id: 's', description: '', objectives: [{ id: 'o', description: '', condition: 'vars.done2 == true' }], actions: [] }],
|
stages: [{ id: 's', description: '', objectives: [{ id: 'o', description: '', condition: 'Variables(vars).done2 == true' }], actions: [] }],
|
||||||
};
|
};
|
||||||
|
|
||||||
const log = player.add('questLog', new QuestLog([q1, q2]));
|
const log = player.add('questLog', new QuestLog([q1, q2]));
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ describe('Stat — value / base / modifiers', () => {
|
||||||
e.add('s', new Stat({ value: 10 }));
|
e.add('s', new Stat({ value: 10 }));
|
||||||
const s = e.get(Stat, 's')!;
|
const s = e.get(Stat, 's')!;
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
e.on('s.set', ({ data }) => events.push(data));
|
e.on('Stat(s).set', ({ data }) => events.push(data));
|
||||||
s.set(20);
|
s.set(20);
|
||||||
expect(events).toEqual([{ prev: 10, value: 20 }]);
|
expect(events).toEqual([{ prev: 10, value: 20 }]);
|
||||||
});
|
});
|
||||||
|
|
@ -127,7 +127,7 @@ describe('Stat — value / base / modifiers', () => {
|
||||||
e.add('s', new Stat({ value: 10, min: 0 }));
|
e.add('s', new Stat({ value: 10, min: 0 }));
|
||||||
const s = e.get(Stat, 's')!;
|
const s = e.get(Stat, 's')!;
|
||||||
const events: unknown[] = [];
|
const events: unknown[] = [];
|
||||||
e.on('s.set', ({ data }) => events.push(data));
|
e.on('Stat(s).set', ({ data }) => events.push(data));
|
||||||
s.set(-100); // clamped to 0, still changes
|
s.set(-100); // clamped to 0, still changes
|
||||||
s.set(-200); // still 0, no change
|
s.set(-200); // still 0, no change
|
||||||
expect(events.length).toBe(1);
|
expect(events.length).toBe(1);
|
||||||
|
|
@ -150,7 +150,7 @@ describe('Health', () => {
|
||||||
e.add('health', new Health({ value: 10, min: 0 }));
|
e.add('health', new Health({ value: 10, min: 0 }));
|
||||||
const h = e.get(Health)!;
|
const h = e.get(Health)!;
|
||||||
const killed: unknown[] = [];
|
const killed: unknown[] = [];
|
||||||
e.on('health.killed', () => killed.push(true));
|
e.on('Health(health).killed', () => killed.push(true));
|
||||||
h.update(-10);
|
h.update(-10);
|
||||||
expect(killed.length).toBe(1);
|
expect(killed.length).toBe(1);
|
||||||
expect(h.value).toBe(0);
|
expect(h.value).toBe(0);
|
||||||
|
|
@ -162,7 +162,7 @@ describe('Health', () => {
|
||||||
e.add('health', new Health({ value: 50, min: 0 }));
|
e.add('health', new Health({ value: 50, min: 0 }));
|
||||||
const h = e.get(Health)!;
|
const h = e.get(Health)!;
|
||||||
const killed: unknown[] = [];
|
const killed: unknown[] = [];
|
||||||
e.on('health.killed', () => killed.push(true));
|
e.on('Health(health).killed', () => killed.push(true));
|
||||||
h.kill();
|
h.kill();
|
||||||
expect(killed.length).toBe(1);
|
expect(killed.length).toBe(1);
|
||||||
expect(h.value).toBe(0);
|
expect(h.value).toBe(0);
|
||||||
|
|
@ -174,7 +174,19 @@ describe('Health', () => {
|
||||||
e.add('health', new Health({ value: 10, min: 0 }));
|
e.add('health', new Health({ value: 10, min: 0 }));
|
||||||
const h = e.get(Health)!;
|
const h = e.get(Health)!;
|
||||||
const killed: unknown[] = [];
|
const killed: unknown[] = [];
|
||||||
e.on('health.killed', () => killed.push(true));
|
e.on('Health(health).killed', () => killed.push(true));
|
||||||
|
h.update(-999);
|
||||||
|
expect(killed.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('kill() does not emit killed twice for already killed entity', () => {
|
||||||
|
const w = world();
|
||||||
|
const e = w.createEntity();
|
||||||
|
e.add('health', new Health({ value: 10, min: 0 }));
|
||||||
|
const h = e.get(Health)!;
|
||||||
|
const killed: unknown[] = [];
|
||||||
|
e.on('Health(health).killed', () => killed.push(true));
|
||||||
|
h.update(-999);
|
||||||
h.update(-999);
|
h.update(-999);
|
||||||
expect(killed.length).toBe(1);
|
expect(killed.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue