@vylos/core 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -3
- package/src/components/core/DialogueBox.vue +16 -4
- package/src/engine/core/CheckpointManager.ts +4 -4
- package/src/engine/core/Engine.ts +14 -8
- package/src/engine/core/EngineFactory.ts +7 -2
- package/src/engine/core/EventRunner.ts +31 -14
- package/src/engine/managers/ActionManager.ts +3 -3
- package/src/engine/managers/EventManager.ts +5 -5
- package/src/engine/managers/InventoryManager.ts +149 -0
- package/src/engine/managers/LocationManager.ts +2 -2
- package/src/engine/types/actions.ts +2 -2
- package/src/engine/types/checkpoint.ts +2 -2
- package/src/engine/types/dialogue.ts +2 -9
- package/src/engine/types/engine.ts +2 -1
- package/src/engine/types/events.ts +18 -3
- package/src/engine/types/game-state.ts +9 -6
- package/src/engine/types/index.ts +1 -0
- package/src/engine/types/inventory.ts +26 -0
- package/src/engine/types/locations.ts +3 -3
- package/src/engine/types/save.ts +3 -3
- package/src/engine/utils/deepMerge.ts +40 -0
- package/src/engine/utils/devConsole.ts +53 -0
- package/src/index.ts +5 -7
- package/src/stores/gameState.ts +9 -7
- package/tests/engine/ActionManager.test.ts +4 -3
- package/tests/engine/CheckpointManager.test.ts +4 -3
- package/tests/engine/EventManager.test.ts +4 -3
- package/tests/engine/EventRunner.test.ts +99 -9
- package/tests/engine/HistoryManager.test.ts +1 -1
- package/tests/engine/InventoryManager.test.ts +289 -0
- package/tests/engine/LocationManager.test.ts +4 -3
- package/tests/integration/game-loop.test.ts +13 -5
- package/src/engine/errors/StateValidationError.ts +0 -13
- package/src/engine/schemas/baseGameState.schema.ts +0 -19
- package/src/engine/schemas/checkpoint.schema.ts +0 -11
- package/src/engine/schemas/engineState.schema.ts +0 -59
- package/src/engine/schemas/location.schema.ts +0 -21
- package/tests/engine/schemas.test.ts +0 -250
- package/tests/safety/event-validation.test.ts +0 -102
- package/tests/safety/state-schema.test.ts +0 -96
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { VylosGameState } from './game-state';
|
|
2
|
+
import type { VylosCharacter } from './dialogue';
|
|
2
3
|
|
|
3
4
|
/** A text entry with per-language strings */
|
|
4
5
|
export type TextEntry = Record<string, string>;
|
|
5
6
|
|
|
6
7
|
/** Options for engine.say() */
|
|
7
8
|
export interface SayOptions {
|
|
8
|
-
from?:
|
|
9
|
+
from?: VylosCharacter;
|
|
9
10
|
variables?: Record<string, string | number>;
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -17,6 +18,17 @@ export interface ChoiceItem<T extends string = string> {
|
|
|
17
18
|
condition?: () => boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/** Inventory operations available to event authors via engine.inventory */
|
|
22
|
+
export interface InventoryAPI {
|
|
23
|
+
add(bag: string, itemId: string, qty?: number): number;
|
|
24
|
+
remove(bag: string, itemId: string, qty?: number): number;
|
|
25
|
+
has(bag: string, itemId: string, qty?: number): boolean;
|
|
26
|
+
hasAll(bag: string, items: Record<string, number>): boolean;
|
|
27
|
+
count(bag: string, itemId: string): number;
|
|
28
|
+
list(bag: string): Array<[string, number]>;
|
|
29
|
+
clear(bag: string): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
/**
|
|
21
33
|
* The API available to event execute() functions.
|
|
22
34
|
* This is what visual novel authors interact with.
|
|
@@ -46,6 +58,9 @@ export interface VylosAPI {
|
|
|
46
58
|
/** End the current event */
|
|
47
59
|
end(): never;
|
|
48
60
|
|
|
61
|
+
/** Inventory operations (add, remove, has, count, etc.) */
|
|
62
|
+
readonly inventory: InventoryAPI;
|
|
63
|
+
|
|
49
64
|
/** Wait for a specified duration (ms) */
|
|
50
65
|
wait(ms: number): Promise<void>;
|
|
51
66
|
|
|
@@ -81,7 +96,7 @@ export interface DrawableEventEntry {
|
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
/** A visual novel event definition */
|
|
84
|
-
export interface VylosEvent<TState extends
|
|
99
|
+
export interface VylosEvent<TState extends VylosGameState = VylosGameState> {
|
|
85
100
|
/** Unique event ID (derived from file path if not specified) */
|
|
86
101
|
id: string;
|
|
87
102
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { VylosCharacter } from './dialogue';
|
|
2
|
+
import type { InventoryData } from './inventory';
|
|
3
|
+
|
|
4
|
+
/** Base game state — projects extend this into their own GameState */
|
|
5
|
+
export interface VylosGameState {
|
|
3
6
|
/** Current location ID */
|
|
4
7
|
locationId: string;
|
|
5
8
|
/** In-game time (hours since game start, e.g. 14.5 = 2:30 PM) */
|
|
@@ -8,8 +11,8 @@ export interface BaseGameState {
|
|
|
8
11
|
flags: Record<string, boolean>;
|
|
9
12
|
/** Generic counters */
|
|
10
13
|
counters: Record<string, number>;
|
|
11
|
-
/** Player
|
|
12
|
-
player:
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
/** Player character */
|
|
15
|
+
player: VylosCharacter;
|
|
16
|
+
/** Inventory data: bag/category ID -> item ID -> quantity */
|
|
17
|
+
inventories: InventoryData;
|
|
15
18
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TextEntry } from './events';
|
|
2
|
+
|
|
3
|
+
/** Category definition for organizing inventory items */
|
|
4
|
+
export interface VylosCategory {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string | TextEntry;
|
|
7
|
+
icon?: string;
|
|
8
|
+
sortOrder?: number;
|
|
9
|
+
/** Max distinct item types in this category (default: unlimited) */
|
|
10
|
+
maxSlots?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Item definition with metadata */
|
|
14
|
+
export interface VylosItem {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string | TextEntry;
|
|
17
|
+
/** Category ID this item belongs to */
|
|
18
|
+
category: string;
|
|
19
|
+
/** Max stack size per slot (default: Infinity) */
|
|
20
|
+
maxStack?: number;
|
|
21
|
+
description?: string | TextEntry;
|
|
22
|
+
icon?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Inventory data: bag/category ID -> item ID -> quantity */
|
|
26
|
+
export type InventoryData = Record<string, Record<string, number>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { VylosGameState } from './game-state';
|
|
2
2
|
import type { TextEntry } from './events';
|
|
3
3
|
|
|
4
4
|
/** Background resolution entry — time-based or default */
|
|
@@ -20,7 +20,7 @@ export interface VylosLocation {
|
|
|
20
20
|
backgrounds: BackgroundEntry[];
|
|
21
21
|
|
|
22
22
|
/** Whether this location is accessible — defaults to true */
|
|
23
|
-
accessible?<TState extends
|
|
23
|
+
accessible?<TState extends VylosGameState>(state: TState): boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/** Location link (navigation graph edge) */
|
|
@@ -28,5 +28,5 @@ export interface LocationLink {
|
|
|
28
28
|
from: string;
|
|
29
29
|
to: string;
|
|
30
30
|
/** Whether this link is currently traversable */
|
|
31
|
-
condition?<TState extends
|
|
31
|
+
condition?<TState extends VylosGameState>(state: TState): boolean;
|
|
32
32
|
}
|
package/src/engine/types/save.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { VylosGameState } from './game-state';
|
|
2
2
|
import type { Checkpoint } from './checkpoint';
|
|
3
3
|
|
|
4
4
|
/** A single save slot */
|
|
@@ -10,7 +10,7 @@ export interface SaveSlot {
|
|
|
10
10
|
/** Save format version for migration */
|
|
11
11
|
version: number;
|
|
12
12
|
/** Game state snapshot */
|
|
13
|
-
gameState:
|
|
13
|
+
gameState: VylosGameState;
|
|
14
14
|
/** Current event ID (if mid-event) */
|
|
15
15
|
eventId: string | null;
|
|
16
16
|
/** Checkpoint step number (if mid-event) */
|
|
@@ -22,7 +22,7 @@ export interface SaveSlot {
|
|
|
22
22
|
/** Checkpoints for mid-event resume */
|
|
23
23
|
checkpoints?: Checkpoint[];
|
|
24
24
|
/** Game state before event started (for redo support after load) */
|
|
25
|
-
initialState?:
|
|
25
|
+
initialState?: VylosGameState;
|
|
26
26
|
/** Completed event history */
|
|
27
27
|
history?: Array<{ eventId: string; checkpoints: Checkpoint[] }>;
|
|
28
28
|
/** Current history navigation index */
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep merge a source object onto a target object.
|
|
3
|
+
* Arrays are overwritten, but plain objects are merged recursively.
|
|
4
|
+
*/
|
|
5
|
+
export function deepMerge<T extends Record<string, any>>(target: T, source: Record<string, any>): T {
|
|
6
|
+
const output = { ...target };
|
|
7
|
+
|
|
8
|
+
for (const key in source) {
|
|
9
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
10
|
+
const sourceValue = source[key];
|
|
11
|
+
const targetValue = output[key];
|
|
12
|
+
|
|
13
|
+
// If both are plain objects, merge recursively
|
|
14
|
+
if (
|
|
15
|
+
isPlainObject(sourceValue) &&
|
|
16
|
+
isPlainObject(targetValue)
|
|
17
|
+
) {
|
|
18
|
+
(output as any)[key] = deepMerge(targetValue, sourceValue);
|
|
19
|
+
} else if (sourceValue !== undefined) {
|
|
20
|
+
// Otherwise grab the new value if defined
|
|
21
|
+
(output as any)[key] = sourceValue;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return output;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Check if an item is a plain object */
|
|
30
|
+
function isPlainObject(item: any): item is Record<string, any> {
|
|
31
|
+
if (item === null || typeof item !== 'object') {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Exclude arrays and dates/regexps
|
|
36
|
+
if (Array.isArray(item)) return false;
|
|
37
|
+
|
|
38
|
+
const proto = Object.getPrototypeOf(item);
|
|
39
|
+
return proto === Object.prototype || proto === null;
|
|
40
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Engine } from '../core/Engine';
|
|
2
|
+
import type { VylosGameState } from '../types';
|
|
3
|
+
import { logger } from './logger';
|
|
4
|
+
|
|
5
|
+
export interface VylosConsole {
|
|
6
|
+
/** Current game state (read/write — changes are live) */
|
|
7
|
+
readonly state: VylosGameState;
|
|
8
|
+
/** Inventory shorthand */
|
|
9
|
+
readonly inventory: {
|
|
10
|
+
add(bag: string, itemId: string, qty?: number): number;
|
|
11
|
+
remove(bag: string, itemId: string, qty?: number): number;
|
|
12
|
+
has(bag: string, itemId: string, qty?: number): boolean;
|
|
13
|
+
count(bag: string, itemId: string): number;
|
|
14
|
+
list(bag: string): Array<[string, number]>;
|
|
15
|
+
clear(bag: string): void;
|
|
16
|
+
};
|
|
17
|
+
/** Raw engine reference */
|
|
18
|
+
readonly engine: Engine;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
interface Window {
|
|
23
|
+
Vylos: VylosConsole;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Attach `window.Vylos` for console cheating/debugging */
|
|
28
|
+
export function attachDevConsole(engine: Engine, getState: () => VylosGameState): void {
|
|
29
|
+
const im = engine.inventoryManager;
|
|
30
|
+
|
|
31
|
+
Object.defineProperty(window, 'Vylos', {
|
|
32
|
+
configurable: true,
|
|
33
|
+
get() {
|
|
34
|
+
const state = getState();
|
|
35
|
+
return {
|
|
36
|
+
get state() { return state; },
|
|
37
|
+
get inventory() {
|
|
38
|
+
return {
|
|
39
|
+
add: (bag: string, itemId: string, qty?: number) => im.add(state.inventories, bag, itemId, qty),
|
|
40
|
+
remove: (bag: string, itemId: string, qty?: number) => im.remove(state.inventories, bag, itemId, qty),
|
|
41
|
+
has: (bag: string, itemId: string, qty?: number) => im.has(state.inventories, bag, itemId, qty),
|
|
42
|
+
count: (bag: string, itemId: string) => im.count(state.inventories, bag, itemId),
|
|
43
|
+
list: (bag: string) => im.list(state.inventories, bag),
|
|
44
|
+
clear: (bag: string) => im.clearBag(state.inventories, bag),
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
get engine() { return engine; },
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
logger.info('DevConsole ready — type Vylos in the browser console');
|
|
53
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,21 +3,17 @@ import 'reflect-metadata';
|
|
|
3
3
|
// Types
|
|
4
4
|
export * from './engine/types';
|
|
5
5
|
|
|
6
|
-
// Schemas
|
|
7
|
-
export { baseGameStateSchema, extendGameStateSchema } from './engine/schemas/baseGameState.schema';
|
|
8
|
-
export { engineStateSchema, engineSettingsSchema } from './engine/schemas/engineState.schema';
|
|
9
|
-
export { checkpointSchema } from './engine/schemas/checkpoint.schema';
|
|
10
|
-
export { locationSchema, locationLinkSchema } from './engine/schemas/location.schema';
|
|
11
|
-
|
|
12
6
|
// Errors
|
|
13
7
|
export { JumpSignal } from './engine/errors/JumpSignal';
|
|
14
8
|
export { EventEndError } from './engine/errors/EventEndError';
|
|
15
9
|
export { InterruptSignal } from './engine/errors/InterruptSignal';
|
|
16
|
-
export { StateValidationError } from './engine/errors/StateValidationError';
|
|
17
10
|
|
|
18
11
|
// Utils
|
|
19
12
|
export { logger, LogLevel } from './engine/utils/logger';
|
|
20
13
|
export { resolveBackground, formatGameTime, interpolate } from './engine/utils/TimeHelper';
|
|
14
|
+
export { deepMerge } from './engine/utils/deepMerge';
|
|
15
|
+
export { attachDevConsole } from './engine/utils/devConsole';
|
|
16
|
+
export type { VylosConsole } from './engine/utils/devConsole';
|
|
21
17
|
export { assetUrl } from './utils/assetUrl';
|
|
22
18
|
|
|
23
19
|
// Stores
|
|
@@ -29,10 +25,12 @@ export { Engine } from './engine/core/Engine';
|
|
|
29
25
|
export type { EngineLoopCallbacks } from './engine/core/Engine';
|
|
30
26
|
export { EventRunner } from './engine/core/EventRunner';
|
|
31
27
|
export type { EventRunnerCallbacks, HistoryStep } from './engine/core/EventRunner';
|
|
28
|
+
export type { InventoryAPI } from './engine/types/events';
|
|
32
29
|
export { CheckpointManager } from './engine/core/CheckpointManager';
|
|
33
30
|
export { createEngine, getComponentOverride, clearComponentOverrides, DI_TOKENS } from './engine/core/EngineFactory';
|
|
34
31
|
|
|
35
32
|
// Managers
|
|
33
|
+
export { InventoryManager } from './engine/managers/InventoryManager';
|
|
36
34
|
export { EventManager } from './engine/managers/EventManager';
|
|
37
35
|
export { HistoryManager } from './engine/managers/HistoryManager';
|
|
38
36
|
export { NavigationManager, NavigationAction } from './engine/managers/NavigationManager';
|
package/src/stores/gameState.ts
CHANGED
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
import { defineStore } from 'pinia';
|
|
2
2
|
import { ref } from 'vue';
|
|
3
|
-
import type {
|
|
3
|
+
import type { VylosGameState } from '../engine/types';
|
|
4
4
|
|
|
5
5
|
/** Default initial game state */
|
|
6
|
-
function createDefaultState():
|
|
6
|
+
function createDefaultState(): VylosGameState {
|
|
7
7
|
return {
|
|
8
8
|
locationId: '',
|
|
9
9
|
gameTime: 8,
|
|
10
10
|
flags: {},
|
|
11
11
|
counters: {},
|
|
12
12
|
player: {
|
|
13
|
+
id: 'player',
|
|
13
14
|
name: 'Player',
|
|
14
15
|
},
|
|
16
|
+
inventories: {},
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export const useGameStateStore = defineStore('gameState', () => {
|
|
19
|
-
const state = ref<
|
|
21
|
+
const state = ref<VylosGameState>(createDefaultState());
|
|
20
22
|
|
|
21
|
-
function getState():
|
|
23
|
+
function getState(): VylosGameState {
|
|
22
24
|
return state.value;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
function setState(newState:
|
|
27
|
+
function setState(newState: VylosGameState) {
|
|
26
28
|
state.value = newState;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
function getSnapshot():
|
|
31
|
+
function getSnapshot(): VylosGameState {
|
|
30
32
|
return structuredClone(state.value);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
function restoreSnapshot(snapshot:
|
|
35
|
+
function restoreSnapshot(snapshot: VylosGameState) {
|
|
34
36
|
state.value = structuredClone(snapshot);
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { ActionManager } from '../../src/engine/managers/ActionManager';
|
|
3
|
-
import type { VylosAction,
|
|
3
|
+
import type { VylosAction, VylosGameState } from '../../src/engine/types';
|
|
4
4
|
|
|
5
|
-
function makeState(overrides: Partial<
|
|
5
|
+
function makeState(overrides: Partial<VylosGameState> = {}): VylosGameState {
|
|
6
6
|
return {
|
|
7
7
|
locationId: 'cafe',
|
|
8
8
|
gameTime: 12,
|
|
9
9
|
flags: {},
|
|
10
10
|
counters: {},
|
|
11
|
-
player: { name: 'Alice' },
|
|
11
|
+
player: { id: 'alice', name: 'Alice' },
|
|
12
|
+
inventories: {},
|
|
12
13
|
...overrides,
|
|
13
14
|
};
|
|
14
15
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { CheckpointManager } from '../../src/engine/core/CheckpointManager';
|
|
3
|
-
import type {
|
|
3
|
+
import type { VylosGameState } from '../../src/engine/types';
|
|
4
4
|
import { CheckpointType } from '../../src/engine/types';
|
|
5
5
|
|
|
6
|
-
function makeState(overrides: Partial<
|
|
6
|
+
function makeState(overrides: Partial<VylosGameState> = {}): VylosGameState {
|
|
7
7
|
return {
|
|
8
8
|
locationId: 'cafe',
|
|
9
9
|
gameTime: 8,
|
|
10
10
|
flags: {},
|
|
11
11
|
counters: {},
|
|
12
|
-
player: { name: 'Alice' },
|
|
12
|
+
player: { id: 'alice', name: 'Alice' },
|
|
13
|
+
inventories: {},
|
|
13
14
|
...overrides,
|
|
14
15
|
};
|
|
15
16
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { EventManager } from '../../src/engine/managers/EventManager';
|
|
3
|
-
import type { VylosEvent, VylosAPI,
|
|
3
|
+
import type { VylosEvent, VylosAPI, VylosGameState } from '../../src/engine/types';
|
|
4
4
|
import { EventStatus } from '../../src/engine/types';
|
|
5
5
|
|
|
6
|
-
function makeState(overrides: Partial<
|
|
6
|
+
function makeState(overrides: Partial<VylosGameState> = {}): VylosGameState {
|
|
7
7
|
return {
|
|
8
8
|
locationId: 'cafe',
|
|
9
9
|
gameTime: 8,
|
|
10
10
|
flags: {},
|
|
11
11
|
counters: {},
|
|
12
|
-
player: { name: 'Alice' },
|
|
12
|
+
player: { id: 'alice', name: 'Alice' },
|
|
13
|
+
inventories: {},
|
|
13
14
|
...overrides,
|
|
14
15
|
};
|
|
15
16
|
}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { EventRunner, type EventRunnerCallbacks } from '../../src/engine/core/EventRunner';
|
|
3
|
-
import
|
|
3
|
+
import { InventoryManager } from '../../src/engine/managers/InventoryManager';
|
|
4
|
+
import type { VylosEvent, VylosAPI, VylosGameState } from '../../src/engine/types';
|
|
4
5
|
import { JumpSignal } from '../../src/engine/errors/JumpSignal';
|
|
5
6
|
import { EventEndError } from '../../src/engine/errors/EventEndError';
|
|
6
7
|
|
|
7
|
-
function makeState(overrides: Partial<
|
|
8
|
+
function makeState(overrides: Partial<VylosGameState> = {}): VylosGameState {
|
|
8
9
|
return {
|
|
9
10
|
locationId: 'cafe',
|
|
10
11
|
gameTime: 8,
|
|
11
12
|
flags: {},
|
|
12
13
|
counters: {},
|
|
13
|
-
player: { name: 'Alice' },
|
|
14
|
+
player: { id: 'alice', name: 'Alice' },
|
|
15
|
+
inventories: {},
|
|
14
16
|
...overrides,
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
function makeCallbacks(state?:
|
|
20
|
+
function makeCallbacks(state?: VylosGameState): EventRunnerCallbacks & { state: VylosGameState } {
|
|
19
21
|
const s = state ?? makeState();
|
|
20
22
|
return {
|
|
21
23
|
state: s,
|
|
@@ -29,7 +31,7 @@ function makeCallbacks(state?: BaseGameState): EventRunnerCallbacks & { state: B
|
|
|
29
31
|
onClear: vi.fn(),
|
|
30
32
|
resolveText: vi.fn((text: unknown) => typeof text === 'string' ? text : 'resolved'),
|
|
31
33
|
getState: vi.fn(() => s),
|
|
32
|
-
setState: vi.fn((newState:
|
|
34
|
+
setState: vi.fn((newState: VylosGameState) => { Object.assign(s, newState); }),
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -39,7 +41,7 @@ describe('EventRunner', () => {
|
|
|
39
41
|
|
|
40
42
|
beforeEach(() => {
|
|
41
43
|
callbacks = makeCallbacks();
|
|
42
|
-
runner = new EventRunner(callbacks);
|
|
44
|
+
runner = new EventRunner(callbacks, new InventoryManager());
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
describe('say()', () => {
|
|
@@ -66,18 +68,19 @@ describe('EventRunner', () => {
|
|
|
66
68
|
expect(callbacks.onClear).toHaveBeenCalled();
|
|
67
69
|
});
|
|
68
70
|
|
|
69
|
-
it('
|
|
71
|
+
it('passes Character object as speaker', async () => {
|
|
72
|
+
const barista = { id: 'barista', name: 'Barista' };
|
|
70
73
|
const event: VylosEvent = {
|
|
71
74
|
id: 'test-speaker',
|
|
72
75
|
async execute(engine: VylosAPI) {
|
|
73
|
-
await engine.say('Hi!', { from:
|
|
76
|
+
await engine.say('Hi!', { from: barista });
|
|
74
77
|
},
|
|
75
78
|
};
|
|
76
79
|
|
|
77
80
|
const promise = runner.executeEvent(event);
|
|
78
81
|
|
|
79
82
|
await vi.waitFor(() => {
|
|
80
|
-
expect(callbacks.onSay).toHaveBeenCalledWith('Hi!',
|
|
83
|
+
expect(callbacks.onSay).toHaveBeenCalledWith('Hi!', barista);
|
|
81
84
|
});
|
|
82
85
|
|
|
83
86
|
runner.resolveWait();
|
|
@@ -273,6 +276,93 @@ describe('EventRunner', () => {
|
|
|
273
276
|
});
|
|
274
277
|
});
|
|
275
278
|
|
|
279
|
+
describe('inventory API', () => {
|
|
280
|
+
it('add() delegates to InventoryManager with current state', async () => {
|
|
281
|
+
const event: VylosEvent = {
|
|
282
|
+
id: 'test-inv-add',
|
|
283
|
+
async execute(engine: VylosAPI) {
|
|
284
|
+
engine.inventory.add('backpack', 'potion', 3);
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
await runner.executeEvent(event);
|
|
289
|
+
expect(callbacks.state.inventories['backpack']?.['potion']).toBe(3);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('has() reads current state', async () => {
|
|
293
|
+
callbacks.state.inventories = { backpack: { potion: 5 } };
|
|
294
|
+
let result = false;
|
|
295
|
+
|
|
296
|
+
const event: VylosEvent = {
|
|
297
|
+
id: 'test-inv-has',
|
|
298
|
+
async execute(engine: VylosAPI) {
|
|
299
|
+
result = engine.inventory.has('backpack', 'potion', 3);
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
await runner.executeEvent(event);
|
|
304
|
+
expect(result).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('remove() mutates state', async () => {
|
|
308
|
+
callbacks.state.inventories = { backpack: { potion: 5 } };
|
|
309
|
+
|
|
310
|
+
const event: VylosEvent = {
|
|
311
|
+
id: 'test-inv-remove',
|
|
312
|
+
async execute(engine: VylosAPI) {
|
|
313
|
+
engine.inventory.remove('backpack', 'potion', 2);
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
await runner.executeEvent(event);
|
|
318
|
+
expect(callbacks.state.inventories['backpack']?.['potion']).toBe(3);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('count() returns correct value', async () => {
|
|
322
|
+
callbacks.state.inventories = { backpack: { potion: 7 } };
|
|
323
|
+
let result = 0;
|
|
324
|
+
|
|
325
|
+
const event: VylosEvent = {
|
|
326
|
+
id: 'test-inv-count',
|
|
327
|
+
async execute(engine: VylosAPI) {
|
|
328
|
+
result = engine.inventory.count('backpack', 'potion');
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
await runner.executeEvent(event);
|
|
333
|
+
expect(result).toBe(7);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('list() returns bag contents', async () => {
|
|
337
|
+
callbacks.state.inventories = { backpack: { potion: 3, sword: 1 } };
|
|
338
|
+
let result: Array<[string, number]> = [];
|
|
339
|
+
|
|
340
|
+
const event: VylosEvent = {
|
|
341
|
+
id: 'test-inv-list',
|
|
342
|
+
async execute(engine: VylosAPI) {
|
|
343
|
+
result = engine.inventory.list('backpack');
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
await runner.executeEvent(event);
|
|
348
|
+
expect(result).toEqual([['potion', 3], ['sword', 1]]);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('clear() empties a bag', async () => {
|
|
352
|
+
callbacks.state.inventories = { backpack: { potion: 3, sword: 1 } };
|
|
353
|
+
|
|
354
|
+
const event: VylosEvent = {
|
|
355
|
+
id: 'test-inv-clear',
|
|
356
|
+
async execute(engine: VylosAPI) {
|
|
357
|
+
engine.inventory.clear('backpack');
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
await runner.executeEvent(event);
|
|
362
|
+
expect(callbacks.state.inventories['backpack']).toBeUndefined();
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
276
366
|
describe('checkpoints', () => {
|
|
277
367
|
it('captures checkpoint for each say', async () => {
|
|
278
368
|
const event: VylosEvent = {
|