@vylos/core 0.2.1 → 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
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { InventoryManager } from '../../src/engine/managers/InventoryManager';
|
|
3
|
+
import type { VylosCategory, VylosItem, InventoryData } from '../../src/engine/types';
|
|
4
|
+
|
|
5
|
+
function makeInv(): InventoryData {
|
|
6
|
+
return {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('InventoryManager', () => {
|
|
10
|
+
let im: InventoryManager;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
im = new InventoryManager();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// --- Category registry ---
|
|
17
|
+
|
|
18
|
+
describe('categories', () => {
|
|
19
|
+
const reagents: VylosCategory = { id: 'reagents', name: 'Reagents', icon: '🧪', sortOrder: 1 };
|
|
20
|
+
const potions: VylosCategory = { id: 'potions', name: 'Potions', icon: '🧫', sortOrder: 2 };
|
|
21
|
+
const misc: VylosCategory = { id: 'misc', name: 'Misc' }; // no sortOrder
|
|
22
|
+
|
|
23
|
+
it('registers and retrieves a category', () => {
|
|
24
|
+
im.registerCategory(reagents);
|
|
25
|
+
expect(im.getCategory('reagents')).toEqual(reagents);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns undefined for unknown category', () => {
|
|
29
|
+
expect(im.getCategory('nope')).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('registerCategories registers multiple', () => {
|
|
33
|
+
im.registerCategories([reagents, potions, misc]);
|
|
34
|
+
expect(im.getCategories()).toHaveLength(3);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('getCategories sorts by sortOrder (undefined last)', () => {
|
|
38
|
+
im.registerCategories([misc, potions, reagents]);
|
|
39
|
+
const sorted = im.getCategories();
|
|
40
|
+
expect(sorted.map((c) => c.id)).toEqual(['reagents', 'potions', 'misc']);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// --- Item registry ---
|
|
45
|
+
|
|
46
|
+
describe('items', () => {
|
|
47
|
+
const moonflower: VylosItem = { id: 'moonflower', name: 'Moonflower', category: 'reagents' };
|
|
48
|
+
const sunpetal: VylosItem = { id: 'sunpetal', name: 'Sun Petal', category: 'reagents' };
|
|
49
|
+
const healPotion: VylosItem = { id: 'heal_potion', name: 'Heal Potion', category: 'potions' };
|
|
50
|
+
|
|
51
|
+
it('registers and retrieves an item', () => {
|
|
52
|
+
im.registerItem(moonflower);
|
|
53
|
+
expect(im.getItem('moonflower')).toEqual(moonflower);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns undefined for unknown item', () => {
|
|
57
|
+
expect(im.getItem('nope')).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('registerItems registers multiple', () => {
|
|
61
|
+
im.registerItems([moonflower, sunpetal, healPotion]);
|
|
62
|
+
expect(im.getItem('moonflower')).toBeDefined();
|
|
63
|
+
expect(im.getItem('sunpetal')).toBeDefined();
|
|
64
|
+
expect(im.getItem('heal_potion')).toBeDefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('getItemsByCategory filters correctly', () => {
|
|
68
|
+
im.registerItems([moonflower, sunpetal, healPotion]);
|
|
69
|
+
const reagentItems = im.getItemsByCategory('reagents');
|
|
70
|
+
expect(reagentItems.map((i) => i.id)).toEqual(['moonflower', 'sunpetal']);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// --- Inventory operations ---
|
|
75
|
+
|
|
76
|
+
describe('add', () => {
|
|
77
|
+
it('adds items to a bag, auto-creates bag', () => {
|
|
78
|
+
const inv = makeInv();
|
|
79
|
+
const added = im.add(inv, 'reagents', 'moonflower', 3);
|
|
80
|
+
expect(added).toBe(3);
|
|
81
|
+
expect(inv.reagents.moonflower).toBe(3);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('defaults to qty 1', () => {
|
|
85
|
+
const inv = makeInv();
|
|
86
|
+
im.add(inv, 'bag', 'item');
|
|
87
|
+
expect(inv.bag.item).toBe(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('stacks with existing quantity', () => {
|
|
91
|
+
const inv = makeInv();
|
|
92
|
+
im.add(inv, 'bag', 'item', 5);
|
|
93
|
+
im.add(inv, 'bag', 'item', 3);
|
|
94
|
+
expect(inv.bag.item).toBe(8);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns 0 for qty <= 0', () => {
|
|
98
|
+
const inv = makeInv();
|
|
99
|
+
expect(im.add(inv, 'bag', 'item', 0)).toBe(0);
|
|
100
|
+
expect(im.add(inv, 'bag', 'item', -1)).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('respects maxStack from item definition', () => {
|
|
104
|
+
im.registerItem({ id: 'potion', name: 'Potion', category: 'potions', maxStack: 5 });
|
|
105
|
+
const inv = makeInv();
|
|
106
|
+
im.add(inv, 'potions', 'potion', 3);
|
|
107
|
+
const added = im.add(inv, 'potions', 'potion', 10);
|
|
108
|
+
expect(added).toBe(2); // 5 - 3 = 2 remaining capacity
|
|
109
|
+
expect(inv.potions.potion).toBe(5);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns 0 when already at maxStack', () => {
|
|
113
|
+
im.registerItem({ id: 'potion', name: 'Potion', category: 'potions', maxStack: 5 });
|
|
114
|
+
const inv = makeInv();
|
|
115
|
+
im.add(inv, 'potions', 'potion', 5);
|
|
116
|
+
expect(im.add(inv, 'potions', 'potion', 1)).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('respects maxSlots from category definition', () => {
|
|
120
|
+
im.registerCategory({ id: 'reagents', name: 'Reagents', maxSlots: 2 });
|
|
121
|
+
im.registerItem({ id: 'a', name: 'A', category: 'reagents' });
|
|
122
|
+
im.registerItem({ id: 'b', name: 'B', category: 'reagents' });
|
|
123
|
+
im.registerItem({ id: 'c', name: 'C', category: 'reagents' });
|
|
124
|
+
|
|
125
|
+
const inv = makeInv();
|
|
126
|
+
im.add(inv, 'reagents', 'a', 1);
|
|
127
|
+
im.add(inv, 'reagents', 'b', 1);
|
|
128
|
+
// Bag now has 2 slots, adding a 3rd should fail
|
|
129
|
+
const added = im.add(inv, 'reagents', 'c', 1);
|
|
130
|
+
expect(added).toBe(0);
|
|
131
|
+
expect(inv.reagents.c).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('allows stacking existing item even when at maxSlots', () => {
|
|
135
|
+
im.registerCategory({ id: 'reagents', name: 'Reagents', maxSlots: 2 });
|
|
136
|
+
im.registerItem({ id: 'a', name: 'A', category: 'reagents' });
|
|
137
|
+
im.registerItem({ id: 'b', name: 'B', category: 'reagents' });
|
|
138
|
+
|
|
139
|
+
const inv = makeInv();
|
|
140
|
+
im.add(inv, 'reagents', 'a', 1);
|
|
141
|
+
im.add(inv, 'reagents', 'b', 1);
|
|
142
|
+
// Stacking existing item should work even though 2 slots are full
|
|
143
|
+
const added = im.add(inv, 'reagents', 'a', 3);
|
|
144
|
+
expect(added).toBe(3);
|
|
145
|
+
expect(inv.reagents.a).toBe(4);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('allows unregistered items (no maxStack enforcement)', () => {
|
|
149
|
+
const inv = makeInv();
|
|
150
|
+
im.add(inv, 'bag', 'unknown_item', 999);
|
|
151
|
+
expect(inv.bag.unknown_item).toBe(999);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('remove', () => {
|
|
156
|
+
it('removes items from a bag', () => {
|
|
157
|
+
const inv: InventoryData = { bag: { item: 5 } };
|
|
158
|
+
const removed = im.remove(inv, 'bag', 'item', 3);
|
|
159
|
+
expect(removed).toBe(3);
|
|
160
|
+
expect(inv.bag.item).toBe(2);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('defaults to qty 1', () => {
|
|
164
|
+
const inv: InventoryData = { bag: { item: 5 } };
|
|
165
|
+
im.remove(inv, 'bag', 'item');
|
|
166
|
+
expect(inv.bag.item).toBe(4);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('clamps to available quantity', () => {
|
|
170
|
+
const inv: InventoryData = { bag: { item: 2 } };
|
|
171
|
+
const removed = im.remove(inv, 'bag', 'item', 10);
|
|
172
|
+
expect(removed).toBe(2);
|
|
173
|
+
expect(inv.bag.item).toBeUndefined(); // cleaned up
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('cleans up zero entries', () => {
|
|
177
|
+
const inv: InventoryData = { bag: { item: 3 } };
|
|
178
|
+
im.remove(inv, 'bag', 'item', 3);
|
|
179
|
+
expect(inv.bag.item).toBeUndefined();
|
|
180
|
+
expect('item' in inv.bag).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('returns 0 for missing bag', () => {
|
|
184
|
+
const inv = makeInv();
|
|
185
|
+
expect(im.remove(inv, 'bag', 'item', 1)).toBe(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('returns 0 for missing item', () => {
|
|
189
|
+
const inv: InventoryData = { bag: {} };
|
|
190
|
+
expect(im.remove(inv, 'bag', 'item', 1)).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('returns 0 for qty <= 0', () => {
|
|
194
|
+
const inv: InventoryData = { bag: { item: 5 } };
|
|
195
|
+
expect(im.remove(inv, 'bag', 'item', 0)).toBe(0);
|
|
196
|
+
expect(im.remove(inv, 'bag', 'item', -1)).toBe(0);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('count', () => {
|
|
201
|
+
it('returns quantity', () => {
|
|
202
|
+
const inv: InventoryData = { bag: { item: 7 } };
|
|
203
|
+
expect(im.count(inv, 'bag', 'item')).toBe(7);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('returns 0 for missing bag', () => {
|
|
207
|
+
expect(im.count(makeInv(), 'bag', 'item')).toBe(0);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('returns 0 for missing item', () => {
|
|
211
|
+
const inv: InventoryData = { bag: {} };
|
|
212
|
+
expect(im.count(inv, 'bag', 'item')).toBe(0);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('has', () => {
|
|
217
|
+
it('returns true when sufficient quantity', () => {
|
|
218
|
+
const inv: InventoryData = { bag: { item: 5 } };
|
|
219
|
+
expect(im.has(inv, 'bag', 'item', 5)).toBe(true);
|
|
220
|
+
expect(im.has(inv, 'bag', 'item', 3)).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('returns false when insufficient', () => {
|
|
224
|
+
const inv: InventoryData = { bag: { item: 2 } };
|
|
225
|
+
expect(im.has(inv, 'bag', 'item', 3)).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('defaults to qty 1', () => {
|
|
229
|
+
const inv: InventoryData = { bag: { item: 1 } };
|
|
230
|
+
expect(im.has(inv, 'bag', 'item')).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('returns false for missing bag/item', () => {
|
|
234
|
+
expect(im.has(makeInv(), 'bag', 'item')).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('hasAll', () => {
|
|
239
|
+
it('returns true when all items present', () => {
|
|
240
|
+
const inv: InventoryData = { bag: { a: 3, b: 5, c: 1 } };
|
|
241
|
+
expect(im.hasAll(inv, 'bag', { a: 2, b: 5 })).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('returns false when any item is missing', () => {
|
|
245
|
+
const inv: InventoryData = { bag: { a: 3 } };
|
|
246
|
+
expect(im.hasAll(inv, 'bag', { a: 2, b: 1 })).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('returns true for empty requirements', () => {
|
|
250
|
+
expect(im.hasAll(makeInv(), 'bag', {})).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('list', () => {
|
|
255
|
+
it('lists all items in a bag', () => {
|
|
256
|
+
const inv: InventoryData = { bag: { a: 1, b: 3 } };
|
|
257
|
+
const items = im.list(inv, 'bag');
|
|
258
|
+
expect(items).toEqual([['a', 1], ['b', 3]]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('returns empty array for missing bag', () => {
|
|
262
|
+
expect(im.list(makeInv(), 'bag')).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('clearBag', () => {
|
|
267
|
+
it('removes entire bag', () => {
|
|
268
|
+
const inv: InventoryData = { bag: { a: 1, b: 2 } };
|
|
269
|
+
im.clearBag(inv, 'bag');
|
|
270
|
+
expect(inv.bag).toBeUndefined();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('no-ops for missing bag', () => {
|
|
274
|
+
const inv = makeInv();
|
|
275
|
+
im.clearBag(inv, 'bag'); // should not throw
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('clear', () => {
|
|
280
|
+
it('clears all registered categories and items', () => {
|
|
281
|
+
im.registerCategory({ id: 'cat', name: 'Cat' });
|
|
282
|
+
im.registerItem({ id: 'item', name: 'Item', category: 'cat' });
|
|
283
|
+
im.clear();
|
|
284
|
+
expect(im.getCategory('cat')).toBeUndefined();
|
|
285
|
+
expect(im.getItem('item')).toBeUndefined();
|
|
286
|
+
expect(im.getCategories()).toHaveLength(0);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { LocationManager } from '../../src/engine/managers/LocationManager';
|
|
3
|
-
import type { VylosLocation, LocationLink,
|
|
3
|
+
import type { VylosLocation, LocationLink, 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: 'bedroom',
|
|
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
|
}
|
|
@@ -3,27 +3,29 @@ import { EventRunner, type EventRunnerCallbacks } from '../../src/engine/core/Ev
|
|
|
3
3
|
import { EventManager } from '../../src/engine/managers/EventManager';
|
|
4
4
|
import { HistoryManager } from '../../src/engine/managers/HistoryManager';
|
|
5
5
|
import { NavigationManager } from '../../src/engine/managers/NavigationManager';
|
|
6
|
+
import { InventoryManager } from '../../src/engine/managers/InventoryManager';
|
|
6
7
|
import { SaveManager } from '../../src/engine/managers/SaveManager';
|
|
7
8
|
import { SettingsManager } from '../../src/engine/managers/SettingsManager';
|
|
8
9
|
import { VylosStorage } from '../../src/engine/storage/VylosStorage';
|
|
9
10
|
import { Engine } from '../../src/engine/core/Engine';
|
|
10
|
-
import type { VylosAPI, VylosEvent,
|
|
11
|
+
import type { VylosAPI, VylosEvent, VylosGameState } from '../../src/engine/types';
|
|
11
12
|
import { createEngine, clearComponentOverrides, getComponentOverride } from '../../src/engine/core/EngineFactory';
|
|
12
13
|
import type { VylosPlugin } from '../../src/engine/types';
|
|
13
14
|
import { defineComponent } from 'vue';
|
|
14
15
|
|
|
15
|
-
function makeState(overrides: Partial<
|
|
16
|
+
function makeState(overrides: Partial<VylosGameState> = {}): VylosGameState {
|
|
16
17
|
return {
|
|
17
18
|
locationId: 'cafe',
|
|
18
19
|
gameTime: 8,
|
|
19
20
|
flags: {},
|
|
20
21
|
counters: {},
|
|
21
|
-
player: { name: 'Alice' },
|
|
22
|
+
player: { id: 'alice', name: 'Alice' },
|
|
23
|
+
inventories: {},
|
|
22
24
|
...overrides,
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
function makeCallbacks(state?:
|
|
28
|
+
function makeCallbacks(state?: VylosGameState): EventRunnerCallbacks & { state: VylosGameState } {
|
|
27
29
|
const s = state ?? makeState();
|
|
28
30
|
return {
|
|
29
31
|
state: s,
|
|
@@ -37,7 +39,7 @@ function makeCallbacks(state?: BaseGameState): EventRunnerCallbacks & { state: B
|
|
|
37
39
|
onClear: vi.fn(),
|
|
38
40
|
resolveText: vi.fn((text: unknown) => typeof text === 'string' ? text : 'resolved'),
|
|
39
41
|
getState: vi.fn(() => s),
|
|
40
|
-
setState: vi.fn((newState:
|
|
42
|
+
setState: vi.fn((newState: VylosGameState) => { Object.assign(s, newState); }),
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -53,9 +55,12 @@ describe('Game loop integration', () => {
|
|
|
53
55
|
const saveManager = new SaveManager(storage);
|
|
54
56
|
const settingsManager = new SettingsManager(storage);
|
|
55
57
|
|
|
58
|
+
const inventoryManager = new InventoryManager();
|
|
59
|
+
|
|
56
60
|
const engine = new Engine({
|
|
57
61
|
eventManager,
|
|
58
62
|
historyManager,
|
|
63
|
+
inventoryManager,
|
|
59
64
|
navigationManager,
|
|
60
65
|
eventRunner,
|
|
61
66
|
saveManager,
|
|
@@ -107,9 +112,12 @@ describe('Game loop integration', () => {
|
|
|
107
112
|
const saveManager = new SaveManager(storage);
|
|
108
113
|
const settingsManager = new SettingsManager(storage);
|
|
109
114
|
|
|
115
|
+
const inventoryManager = new InventoryManager();
|
|
116
|
+
|
|
110
117
|
const engine = new Engine({
|
|
111
118
|
eventManager,
|
|
112
119
|
historyManager,
|
|
120
|
+
inventoryManager,
|
|
113
121
|
navigationManager,
|
|
114
122
|
eventRunner,
|
|
115
123
|
saveManager,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { ZodError } from 'zod';
|
|
2
|
-
|
|
3
|
-
/** Thrown when state fails Zod validation after event execution */
|
|
4
|
-
export class StateValidationError extends Error {
|
|
5
|
-
readonly zodError: ZodError;
|
|
6
|
-
|
|
7
|
-
constructor(zodError: ZodError) {
|
|
8
|
-
const issues = zodError.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');
|
|
9
|
-
super(`State validation failed: ${issues}`);
|
|
10
|
-
this.name = 'StateValidationError';
|
|
11
|
-
this.zodError = zodError;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
/** Base game state schema — projects extend this with their own fields */
|
|
4
|
-
export const baseGameStateSchema = z.object({
|
|
5
|
-
locationId: z.string().min(1),
|
|
6
|
-
gameTime: z.number().nonnegative(),
|
|
7
|
-
flags: z.record(z.string(), z.boolean()),
|
|
8
|
-
counters: z.record(z.string(), z.number()),
|
|
9
|
-
player: z.object({
|
|
10
|
-
name: z.string().min(1),
|
|
11
|
-
}),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
export type BaseGameStateFromSchema = z.infer<typeof baseGameStateSchema>;
|
|
15
|
-
|
|
16
|
-
/** Create an extended game state schema by merging with project-specific fields */
|
|
17
|
-
export function extendGameStateSchema<T extends z.ZodRawShape>(extension: T) {
|
|
18
|
-
return baseGameStateSchema.extend(extension);
|
|
19
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { baseGameStateSchema } from './baseGameState.schema';
|
|
3
|
-
|
|
4
|
-
export const checkpointSchema = z.object({
|
|
5
|
-
step: z.number().int().nonnegative(),
|
|
6
|
-
gameState: baseGameStateSchema,
|
|
7
|
-
type: z.enum(['say', 'choice', 'wait', 'overlay']),
|
|
8
|
-
choiceResult: z.string().optional(),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
export type CheckpointFromSchema = z.infer<typeof checkpointSchema>;
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const dialogueStateSchema = z.object({
|
|
4
|
-
text: z.string(),
|
|
5
|
-
speaker: z.string().nullable(),
|
|
6
|
-
isNarration: z.boolean(),
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
const choiceOptionSchema = z.object({
|
|
10
|
-
text: z.string(),
|
|
11
|
-
value: z.string(),
|
|
12
|
-
disabled: z.boolean().optional(),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const choiceStateSchema = z.object({
|
|
16
|
-
prompt: z.string().nullable(),
|
|
17
|
-
options: z.array(choiceOptionSchema),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const actionEntrySchema = z.object({
|
|
21
|
-
id: z.string(),
|
|
22
|
-
label: z.string(),
|
|
23
|
-
locationId: z.string(),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const locationEntrySchema = z.object({
|
|
27
|
-
id: z.string(),
|
|
28
|
-
name: z.string(),
|
|
29
|
-
accessible: z.boolean(),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
export const engineStateSchema = z.object({
|
|
33
|
-
phase: z.enum(['created', 'loading', 'main_menu', 'running', 'paused']),
|
|
34
|
-
background: z.string().nullable(),
|
|
35
|
-
foreground: z.string().nullable(),
|
|
36
|
-
dialogue: dialogueStateSchema.nullable(),
|
|
37
|
-
choices: choiceStateSchema.nullable(),
|
|
38
|
-
currentLocationId: z.string().nullable(),
|
|
39
|
-
availableActions: z.array(actionEntrySchema),
|
|
40
|
-
availableLocations: z.array(locationEntrySchema),
|
|
41
|
-
menuOpen: z.enum(['save_load', 'settings']).nullable(),
|
|
42
|
-
skipMode: z.boolean(),
|
|
43
|
-
autoMode: z.boolean(),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
export type EngineStateFromSchema = z.infer<typeof engineStateSchema>;
|
|
47
|
-
|
|
48
|
-
export const engineSettingsSchema = z.object({
|
|
49
|
-
textSpeed: z.number().min(0).max(1),
|
|
50
|
-
autoSpeed: z.number().min(0).max(1),
|
|
51
|
-
volume: z.object({
|
|
52
|
-
master: z.number().min(0).max(1),
|
|
53
|
-
music: z.number().min(0).max(1),
|
|
54
|
-
sfx: z.number().min(0).max(1),
|
|
55
|
-
voice: z.number().min(0).max(1),
|
|
56
|
-
}),
|
|
57
|
-
language: z.string().min(1),
|
|
58
|
-
fullscreen: z.boolean(),
|
|
59
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const backgroundEntrySchema = z.object({
|
|
4
|
-
path: z.string().min(1),
|
|
5
|
-
timeRange: z.tuple([z.number(), z.number()]).optional(),
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
export const locationSchema = z.object({
|
|
9
|
-
id: z.string().min(1),
|
|
10
|
-
name: z.union([z.string(), z.record(z.string(), z.string())]),
|
|
11
|
-
backgrounds: z.array(backgroundEntrySchema),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
export type LocationFromSchema = z.infer<typeof locationSchema>;
|
|
15
|
-
|
|
16
|
-
export const locationLinkSchema = z.object({
|
|
17
|
-
from: z.string().min(1),
|
|
18
|
-
to: z.string().min(1),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export type LocationLinkFromSchema = z.infer<typeof locationLinkSchema>;
|