@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.
Files changed (40) hide show
  1. package/package.json +2 -3
  2. package/src/components/core/DialogueBox.vue +16 -4
  3. package/src/engine/core/CheckpointManager.ts +4 -4
  4. package/src/engine/core/Engine.ts +14 -8
  5. package/src/engine/core/EngineFactory.ts +7 -2
  6. package/src/engine/core/EventRunner.ts +31 -14
  7. package/src/engine/managers/ActionManager.ts +3 -3
  8. package/src/engine/managers/EventManager.ts +5 -5
  9. package/src/engine/managers/InventoryManager.ts +149 -0
  10. package/src/engine/managers/LocationManager.ts +2 -2
  11. package/src/engine/types/actions.ts +2 -2
  12. package/src/engine/types/checkpoint.ts +2 -2
  13. package/src/engine/types/dialogue.ts +2 -9
  14. package/src/engine/types/engine.ts +2 -1
  15. package/src/engine/types/events.ts +18 -3
  16. package/src/engine/types/game-state.ts +9 -6
  17. package/src/engine/types/index.ts +1 -0
  18. package/src/engine/types/inventory.ts +26 -0
  19. package/src/engine/types/locations.ts +3 -3
  20. package/src/engine/types/save.ts +3 -3
  21. package/src/engine/utils/deepMerge.ts +40 -0
  22. package/src/engine/utils/devConsole.ts +53 -0
  23. package/src/index.ts +5 -7
  24. package/src/stores/gameState.ts +9 -7
  25. package/tests/engine/ActionManager.test.ts +4 -3
  26. package/tests/engine/CheckpointManager.test.ts +4 -3
  27. package/tests/engine/EventManager.test.ts +4 -3
  28. package/tests/engine/EventRunner.test.ts +99 -9
  29. package/tests/engine/HistoryManager.test.ts +1 -1
  30. package/tests/engine/InventoryManager.test.ts +289 -0
  31. package/tests/engine/LocationManager.test.ts +4 -3
  32. package/tests/integration/game-loop.test.ts +13 -5
  33. package/src/engine/errors/StateValidationError.ts +0 -13
  34. package/src/engine/schemas/baseGameState.schema.ts +0 -19
  35. package/src/engine/schemas/checkpoint.schema.ts +0 -11
  36. package/src/engine/schemas/engineState.schema.ts +0 -59
  37. package/src/engine/schemas/location.schema.ts +0 -21
  38. package/tests/engine/schemas.test.ts +0 -250
  39. package/tests/safety/event-validation.test.ts +0 -102
  40. 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
- });