@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,250 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { baseGameStateSchema, extendGameStateSchema } from '../../src/engine/schemas/baseGameState.schema';
|
|
3
|
-
import { engineStateSchema, engineSettingsSchema } from '../../src/engine/schemas/engineState.schema';
|
|
4
|
-
import { checkpointSchema } from '../../src/engine/schemas/checkpoint.schema';
|
|
5
|
-
import { locationSchema, locationLinkSchema } from '../../src/engine/schemas/location.schema';
|
|
6
|
-
import { z } from 'zod';
|
|
7
|
-
|
|
8
|
-
describe('baseGameStateSchema', () => {
|
|
9
|
-
const validState = {
|
|
10
|
-
locationId: 'bedroom',
|
|
11
|
-
gameTime: 8,
|
|
12
|
-
flags: { intro_done: true },
|
|
13
|
-
counters: { coffee_count: 2 },
|
|
14
|
-
player: { name: 'Alice' },
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
it('validates a correct base game state', () => {
|
|
18
|
-
const result = baseGameStateSchema.safeParse(validState);
|
|
19
|
-
expect(result.success).toBe(true);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('rejects empty locationId', () => {
|
|
23
|
-
const result = baseGameStateSchema.safeParse({ ...validState, locationId: '' });
|
|
24
|
-
expect(result.success).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('rejects negative gameTime', () => {
|
|
28
|
-
const result = baseGameStateSchema.safeParse({ ...validState, gameTime: -1 });
|
|
29
|
-
expect(result.success).toBe(false);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('rejects empty player name', () => {
|
|
33
|
-
const result = baseGameStateSchema.safeParse({
|
|
34
|
-
...validState,
|
|
35
|
-
player: { name: '' },
|
|
36
|
-
});
|
|
37
|
-
expect(result.success).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('rejects missing fields', () => {
|
|
41
|
-
const result = baseGameStateSchema.safeParse({ locationId: 'test' });
|
|
42
|
-
expect(result.success).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('extendGameStateSchema', () => {
|
|
47
|
-
it('extends base schema with custom fields', () => {
|
|
48
|
-
const extended = extendGameStateSchema({
|
|
49
|
-
reputation: z.number(),
|
|
50
|
-
inventory: z.array(z.string()),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const result = extended.safeParse({
|
|
54
|
-
locationId: 'cafe',
|
|
55
|
-
gameTime: 14,
|
|
56
|
-
flags: {},
|
|
57
|
-
counters: {},
|
|
58
|
-
player: { name: 'Bob' },
|
|
59
|
-
reputation: 50,
|
|
60
|
-
inventory: ['key', 'map'],
|
|
61
|
-
});
|
|
62
|
-
expect(result.success).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('rejects extended state missing custom fields', () => {
|
|
66
|
-
const extended = extendGameStateSchema({
|
|
67
|
-
reputation: z.number(),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const result = extended.safeParse({
|
|
71
|
-
locationId: 'cafe',
|
|
72
|
-
gameTime: 14,
|
|
73
|
-
flags: {},
|
|
74
|
-
counters: {},
|
|
75
|
-
player: { name: 'Bob' },
|
|
76
|
-
// missing reputation
|
|
77
|
-
});
|
|
78
|
-
expect(result.success).toBe(false);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe('engineStateSchema', () => {
|
|
83
|
-
const validEngineState = {
|
|
84
|
-
phase: 'created',
|
|
85
|
-
background: null,
|
|
86
|
-
foreground: null,
|
|
87
|
-
dialogue: null,
|
|
88
|
-
choices: null,
|
|
89
|
-
currentLocationId: null,
|
|
90
|
-
availableActions: [],
|
|
91
|
-
availableLocations: [],
|
|
92
|
-
menuOpen: null,
|
|
93
|
-
skipMode: false,
|
|
94
|
-
autoMode: false,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
it('validates initial engine state', () => {
|
|
98
|
-
const result = engineStateSchema.safeParse(validEngineState);
|
|
99
|
-
expect(result.success).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('validates engine state with dialogue', () => {
|
|
103
|
-
const result = engineStateSchema.safeParse({
|
|
104
|
-
...validEngineState,
|
|
105
|
-
phase: 'running',
|
|
106
|
-
dialogue: { text: 'Hello', speaker: 'Alice', isNarration: false },
|
|
107
|
-
});
|
|
108
|
-
expect(result.success).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('validates engine state with choices', () => {
|
|
112
|
-
const result = engineStateSchema.safeParse({
|
|
113
|
-
...validEngineState,
|
|
114
|
-
phase: 'running',
|
|
115
|
-
choices: {
|
|
116
|
-
prompt: null,
|
|
117
|
-
options: [
|
|
118
|
-
{ text: 'Yes', value: 'yes' },
|
|
119
|
-
{ text: 'No', value: 'no' },
|
|
120
|
-
],
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
expect(result.success).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('rejects invalid phase', () => {
|
|
127
|
-
const result = engineStateSchema.safeParse({
|
|
128
|
-
...validEngineState,
|
|
129
|
-
phase: 'invalid',
|
|
130
|
-
});
|
|
131
|
-
expect(result.success).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('engineSettingsSchema', () => {
|
|
136
|
-
it('validates default settings', () => {
|
|
137
|
-
const result = engineSettingsSchema.safeParse({
|
|
138
|
-
textSpeed: 0.5,
|
|
139
|
-
autoSpeed: 0.5,
|
|
140
|
-
volume: { master: 1, music: 0.8, sfx: 0.8, voice: 1 },
|
|
141
|
-
language: 'en',
|
|
142
|
-
fullscreen: false,
|
|
143
|
-
});
|
|
144
|
-
expect(result.success).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('rejects volume out of range', () => {
|
|
148
|
-
const result = engineSettingsSchema.safeParse({
|
|
149
|
-
textSpeed: 0.5,
|
|
150
|
-
autoSpeed: 0.5,
|
|
151
|
-
volume: { master: 1.5, music: 0.8, sfx: 0.8, voice: 1 },
|
|
152
|
-
language: 'en',
|
|
153
|
-
fullscreen: false,
|
|
154
|
-
});
|
|
155
|
-
expect(result.success).toBe(false);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('checkpointSchema', () => {
|
|
160
|
-
it('validates a say checkpoint', () => {
|
|
161
|
-
const result = checkpointSchema.safeParse({
|
|
162
|
-
step: 0,
|
|
163
|
-
gameState: {
|
|
164
|
-
locationId: 'cafe',
|
|
165
|
-
gameTime: 14,
|
|
166
|
-
flags: {},
|
|
167
|
-
counters: {},
|
|
168
|
-
player: { name: 'Alice' },
|
|
169
|
-
},
|
|
170
|
-
type: 'say',
|
|
171
|
-
});
|
|
172
|
-
expect(result.success).toBe(true);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('validates a choice checkpoint with result', () => {
|
|
176
|
-
const result = checkpointSchema.safeParse({
|
|
177
|
-
step: 1,
|
|
178
|
-
gameState: {
|
|
179
|
-
locationId: 'cafe',
|
|
180
|
-
gameTime: 14,
|
|
181
|
-
flags: {},
|
|
182
|
-
counters: {},
|
|
183
|
-
player: { name: 'Alice' },
|
|
184
|
-
},
|
|
185
|
-
type: 'choice',
|
|
186
|
-
choiceResult: 'coffee',
|
|
187
|
-
});
|
|
188
|
-
expect(result.success).toBe(true);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('rejects negative step number', () => {
|
|
192
|
-
const result = checkpointSchema.safeParse({
|
|
193
|
-
step: -1,
|
|
194
|
-
gameState: {
|
|
195
|
-
locationId: 'cafe',
|
|
196
|
-
gameTime: 14,
|
|
197
|
-
flags: {},
|
|
198
|
-
counters: {},
|
|
199
|
-
player: { name: 'Alice' },
|
|
200
|
-
},
|
|
201
|
-
type: 'say',
|
|
202
|
-
});
|
|
203
|
-
expect(result.success).toBe(false);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe('locationSchema', () => {
|
|
208
|
-
it('validates a location with backgrounds', () => {
|
|
209
|
-
const result = locationSchema.safeParse({
|
|
210
|
-
id: 'cafe',
|
|
211
|
-
name: 'The Cafe',
|
|
212
|
-
backgrounds: [
|
|
213
|
-
{ path: '/assets/cafe_day.jpg', timeRange: [6, 18] },
|
|
214
|
-
{ path: '/assets/cafe_night.jpg', timeRange: [18, 6] },
|
|
215
|
-
{ path: '/assets/cafe_default.jpg' },
|
|
216
|
-
],
|
|
217
|
-
});
|
|
218
|
-
expect(result.success).toBe(true);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('validates a location with i18n name', () => {
|
|
222
|
-
const result = locationSchema.safeParse({
|
|
223
|
-
id: 'cafe',
|
|
224
|
-
name: { en: 'The Cafe', fr: 'Le Café' },
|
|
225
|
-
backgrounds: [{ path: '/assets/cafe.jpg' }],
|
|
226
|
-
});
|
|
227
|
-
expect(result.success).toBe(true);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('rejects empty location id', () => {
|
|
231
|
-
const result = locationSchema.safeParse({
|
|
232
|
-
id: '',
|
|
233
|
-
name: 'Test',
|
|
234
|
-
backgrounds: [],
|
|
235
|
-
});
|
|
236
|
-
expect(result.success).toBe(false);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
describe('locationLinkSchema', () => {
|
|
241
|
-
it('validates a link', () => {
|
|
242
|
-
const result = locationLinkSchema.safeParse({ from: 'bedroom', to: 'hallway' });
|
|
243
|
-
expect(result.success).toBe(true);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('rejects empty from', () => {
|
|
247
|
-
const result = locationLinkSchema.safeParse({ from: '', to: 'hallway' });
|
|
248
|
-
expect(result.success).toBe(false);
|
|
249
|
-
});
|
|
250
|
-
});
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { baseGameStateSchema, extendGameStateSchema } from '../../src/engine/schemas/baseGameState.schema';
|
|
3
|
-
import { StateValidationError } from '../../src/engine/errors/StateValidationError';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Tests for the Zod validation gate that runs after event execution.
|
|
8
|
-
* The engine validates state after each event to prevent corruption.
|
|
9
|
-
*/
|
|
10
|
-
describe('Event validation gate', () => {
|
|
11
|
-
function validateState(schema: z.ZodSchema, state: unknown): { valid: boolean; error?: StateValidationError } {
|
|
12
|
-
const result = schema.safeParse(state);
|
|
13
|
-
if (result.success) return { valid: true };
|
|
14
|
-
return { valid: false, error: new StateValidationError(result.error) };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
it('accepts valid state after event execution', () => {
|
|
18
|
-
const state = {
|
|
19
|
-
locationId: 'cafe',
|
|
20
|
-
gameTime: 14,
|
|
21
|
-
flags: { intro_done: true },
|
|
22
|
-
counters: { coffee: 1 },
|
|
23
|
-
player: { name: 'Alice' },
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const { valid } = validateState(baseGameStateSchema, state);
|
|
27
|
-
expect(valid).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('rejects state with corrupted locationId', () => {
|
|
31
|
-
const state = {
|
|
32
|
-
locationId: '',
|
|
33
|
-
gameTime: 14,
|
|
34
|
-
flags: {},
|
|
35
|
-
counters: {},
|
|
36
|
-
player: { name: 'Alice' },
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const { valid, error } = validateState(baseGameStateSchema, state);
|
|
40
|
-
expect(valid).toBe(false);
|
|
41
|
-
expect(error?.message).toContain('locationId');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('rejects state with wrong type for flags', () => {
|
|
45
|
-
const state = {
|
|
46
|
-
locationId: 'cafe',
|
|
47
|
-
gameTime: 14,
|
|
48
|
-
flags: 'not-an-object',
|
|
49
|
-
counters: {},
|
|
50
|
-
player: { name: 'Alice' },
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const { valid } = validateState(baseGameStateSchema, state);
|
|
54
|
-
expect(valid).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('validates extended project schema', () => {
|
|
58
|
-
const projectSchema = extendGameStateSchema({
|
|
59
|
-
reputation: z.number().min(0).max(100),
|
|
60
|
-
inventory: z.array(z.string()),
|
|
61
|
-
npcRelations: z.record(z.string(), z.number().min(-100).max(100)),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// Simulates an event that correctly modified state
|
|
65
|
-
const goodState = {
|
|
66
|
-
locationId: 'cafe',
|
|
67
|
-
gameTime: 14,
|
|
68
|
-
flags: { intro_done: true },
|
|
69
|
-
counters: {},
|
|
70
|
-
player: { name: 'Alice' },
|
|
71
|
-
reputation: 50,
|
|
72
|
-
inventory: ['map', 'key'],
|
|
73
|
-
npcRelations: { barista: 10, landlord: -5 },
|
|
74
|
-
};
|
|
75
|
-
expect(validateState(projectSchema, goodState).valid).toBe(true);
|
|
76
|
-
|
|
77
|
-
// Simulates a buggy event that set reputation to 150
|
|
78
|
-
const badState = { ...goodState, reputation: 150 };
|
|
79
|
-
const result = validateState(projectSchema, badState);
|
|
80
|
-
expect(result.valid).toBe(false);
|
|
81
|
-
expect(result.error?.message).toContain('reputation');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('rejects prototype pollution attempts', () => {
|
|
85
|
-
const state = {
|
|
86
|
-
locationId: 'cafe',
|
|
87
|
-
gameTime: 14,
|
|
88
|
-
flags: {},
|
|
89
|
-
counters: {},
|
|
90
|
-
player: { name: 'Alice' },
|
|
91
|
-
__proto__: { isAdmin: true },
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// Zod parses with structuredClone behavior, __proto__ not a valid field
|
|
95
|
-
const result = baseGameStateSchema.safeParse(state);
|
|
96
|
-
// Should either succeed (ignoring __proto__) or fail — both are safe
|
|
97
|
-
// The important thing is that isAdmin is NOT in the result
|
|
98
|
-
if (result.success) {
|
|
99
|
-
expect((result.data as Record<string, unknown>)['isAdmin']).toBeUndefined();
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { baseGameStateSchema, extendGameStateSchema } from '../../src/engine/schemas/baseGameState.schema';
|
|
3
|
-
import { StateValidationError } from '../../src/engine/errors/StateValidationError';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
describe('State validation safety', () => {
|
|
7
|
-
it('catches type coercion attempts', () => {
|
|
8
|
-
const result = baseGameStateSchema.safeParse({
|
|
9
|
-
locationId: 123, // should be string
|
|
10
|
-
gameTime: '8', // should be number
|
|
11
|
-
flags: {},
|
|
12
|
-
counters: {},
|
|
13
|
-
player: { name: 'Alice' },
|
|
14
|
-
});
|
|
15
|
-
expect(result.success).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('catches extra properties in strict mode', () => {
|
|
19
|
-
const strict = baseGameStateSchema.strict();
|
|
20
|
-
const result = strict.safeParse({
|
|
21
|
-
locationId: 'cafe',
|
|
22
|
-
gameTime: 8,
|
|
23
|
-
flags: {},
|
|
24
|
-
counters: {},
|
|
25
|
-
player: { name: 'Alice' },
|
|
26
|
-
hacked: true,
|
|
27
|
-
});
|
|
28
|
-
expect(result.success).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('StateValidationError formats issues nicely', () => {
|
|
32
|
-
const result = baseGameStateSchema.safeParse({
|
|
33
|
-
locationId: '',
|
|
34
|
-
gameTime: -1,
|
|
35
|
-
flags: {},
|
|
36
|
-
counters: {},
|
|
37
|
-
player: { name: '' },
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
expect(result.success).toBe(false);
|
|
41
|
-
if (!result.success) {
|
|
42
|
-
const error = new StateValidationError(result.error);
|
|
43
|
-
expect(error.message).toContain('State validation failed');
|
|
44
|
-
expect(error.name).toBe('StateValidationError');
|
|
45
|
-
expect(error.zodError).toBe(result.error);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('validates extended schema rejects base-only data', () => {
|
|
50
|
-
const schema = extendGameStateSchema({
|
|
51
|
-
reputation: z.number().min(0).max(100),
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const result = schema.safeParse({
|
|
55
|
-
locationId: 'cafe',
|
|
56
|
-
gameTime: 8,
|
|
57
|
-
flags: {},
|
|
58
|
-
counters: {},
|
|
59
|
-
player: { name: 'Alice' },
|
|
60
|
-
// missing reputation
|
|
61
|
-
});
|
|
62
|
-
expect(result.success).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('validates extended schema accepts complete data', () => {
|
|
66
|
-
const schema = extendGameStateSchema({
|
|
67
|
-
reputation: z.number().min(0).max(100),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const result = schema.safeParse({
|
|
71
|
-
locationId: 'cafe',
|
|
72
|
-
gameTime: 8,
|
|
73
|
-
flags: {},
|
|
74
|
-
counters: {},
|
|
75
|
-
player: { name: 'Alice' },
|
|
76
|
-
reputation: 50,
|
|
77
|
-
});
|
|
78
|
-
expect(result.success).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('validates extended schema rejects out-of-range custom field', () => {
|
|
82
|
-
const schema = extendGameStateSchema({
|
|
83
|
-
reputation: z.number().min(0).max(100),
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const result = schema.safeParse({
|
|
87
|
-
locationId: 'cafe',
|
|
88
|
-
gameTime: 8,
|
|
89
|
-
flags: {},
|
|
90
|
-
counters: {},
|
|
91
|
-
player: { name: 'Alice' },
|
|
92
|
-
reputation: 150,
|
|
93
|
-
});
|
|
94
|
-
expect(result.success).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
});
|