@vylos/core 0.3.0 → 0.4.1

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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vylos/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/DevOpsBenjamin/Vylos"
@@ -19,8 +19,7 @@
19
19
  "pinia-plugin-persistedstate": "^3.2.3",
20
20
  "reflect-metadata": "^0.2.2",
21
21
  "tsyringe": "^4.8.0",
22
- "vue": "^3.5.13",
23
- "zod": "^3.24.1"
22
+ "vue": "^3.5.13"
24
23
  },
25
24
  "devDependencies": {
26
25
  "@vue/test-utils": "^2.4.6",
@@ -7,8 +7,12 @@
7
7
  >
8
8
  <div class="dlg-box" :class="{ 'dlg-box--history': engineState.historyBrowsing }">
9
9
  <!-- Speaker name -->
10
- <div v-if="engineState.dialogue.speaker" class="dlg-speaker">
11
- {{ engineState.dialogue.speaker }}
10
+ <div
11
+ v-if="engineState.dialogue.speaker"
12
+ class="dlg-speaker"
13
+ :style="engineState.dialogue.speaker.color ? { color: engineState.dialogue.speaker.color } : undefined"
14
+ >
15
+ {{ speakerName }}
12
16
  </div>
13
17
 
14
18
  <!-- Dialogue text -->
@@ -31,13 +35,21 @@
31
35
  </template>
32
36
 
33
37
  <script setup lang="ts">
34
- import { inject } from 'vue';
38
+ import { inject, computed } from 'vue';
35
39
  import { useEngineStateStore } from '../../stores/engineState';
36
40
  import { ENGINE_INJECT_KEY } from '../../composables/useEngine';
41
+ import { useLanguage } from '../../composables/useLanguage';
37
42
  import type { Engine } from '../../engine/core/Engine';
38
43
 
39
44
  const engineState = useEngineStateStore();
40
45
  const engine = inject<Engine>(ENGINE_INJECT_KEY);
46
+ const { resolveText } = useLanguage();
47
+
48
+ const speakerName = computed(() => {
49
+ const speaker = engineState.dialogue?.speaker;
50
+ if (!speaker) return '';
51
+ return resolveText(speaker.name);
52
+ });
41
53
 
42
54
  function handleClick(): void {
43
55
  if (!engine) return;
@@ -115,7 +127,7 @@ function handleClick(): void {
115
127
  }
116
128
 
117
129
  .dlg-speaker {
118
- color: #fde047;
130
+ color: #fde047; /* default — overridden by Character.color via :style */
119
131
  font-weight: 700;
120
132
  font-size: 1.8cqw;
121
133
  margin-bottom: 1cqh;
@@ -1,5 +1,5 @@
1
1
  import { toRaw } from 'vue';
2
- import type { BaseGameState, Checkpoint, CheckpointType, ChoiceOption } from '../types';
2
+ import type { VylosGameState, Checkpoint, CheckpointType, ChoiceOption } from '../types';
3
3
  import type { DialogueState } from '../types/engine';
4
4
 
5
5
  export interface CaptureDisplayData {
@@ -38,7 +38,7 @@ export class CheckpointManager {
38
38
  }
39
39
 
40
40
  /** Record a checkpoint at the current interaction point */
41
- capture(gameState: BaseGameState, type: CheckpointType, choiceResult?: string, display?: CaptureDisplayData): void {
41
+ capture(gameState: VylosGameState, type: CheckpointType, choiceResult?: string, display?: CaptureDisplayData): void {
42
42
  const checkpoint: Checkpoint = {
43
43
  step: this.checkpoints.length,
44
44
  gameState: structuredClone(toRaw(gameState)),
@@ -72,7 +72,7 @@ export class CheckpointManager {
72
72
  }
73
73
 
74
74
  /** Get game state snapshot at a specific step */
75
- getStateAt(step: number): BaseGameState | undefined {
75
+ getStateAt(step: number): VylosGameState | undefined {
76
76
  const checkpoint = this.checkpoints[step];
77
77
  if (!checkpoint) return undefined;
78
78
  return structuredClone(checkpoint.gameState);
@@ -84,7 +84,7 @@ export class CheckpointManager {
84
84
  }
85
85
 
86
86
  /** Prepare for rollback: set replay index to target step */
87
- prepareRollback(targetStep: number): BaseGameState | undefined {
87
+ prepareRollback(targetStep: number): VylosGameState | undefined {
88
88
  if (targetStep < 0 || targetStep >= this.checkpoints.length) return undefined;
89
89
  this.replayIndex = 0;
90
90
  // Trim checkpoints after target (re-execution will recreate them)
@@ -1,23 +1,26 @@
1
- import type { VylosEvent, BaseGameState, Checkpoint, SaveSlot } from '../types';
1
+ import type { VylosEvent, VylosGameState, Checkpoint, SaveSlot } from '../types';
2
2
  import { EventManager } from '../managers/EventManager';
3
3
  import { HistoryManager } from '../managers/HistoryManager';
4
+ import { InventoryManager } from '../managers/InventoryManager';
4
5
  import { NavigationManager, NavigationAction } from '../managers/NavigationManager';
5
6
  import { SaveManager } from '../managers/SaveManager';
6
7
  import { SettingsManager } from '../managers/SettingsManager';
7
8
  import { EventRunner } from './EventRunner';
8
9
  import { JumpSignal } from '../errors/JumpSignal';
9
10
  import { logger } from '../utils/logger';
11
+ import { attachDevConsole } from '../utils/devConsole';
10
12
 
11
13
  export interface EngineLoopCallbacks {
12
14
  /** Called each loop iteration (update UI: available locations, actions, background) */
13
- onTick?(state: BaseGameState): void;
15
+ onTick?(state: VylosGameState): void;
14
16
  /** Called when player selects an action */
15
- onAction?(actionId: string, state: BaseGameState): void;
17
+ onAction?(actionId: string, state: VylosGameState): void;
16
18
  }
17
19
 
18
20
  export interface EngineDeps {
19
21
  eventManager: EventManager;
20
22
  historyManager: HistoryManager;
23
+ inventoryManager: InventoryManager;
21
24
  navigationManager: NavigationManager;
22
25
  eventRunner: EventRunner;
23
26
  saveManager: SaveManager;
@@ -31,6 +34,7 @@ export interface EngineDeps {
31
34
  export class Engine {
32
35
  readonly eventManager: EventManager;
33
36
  readonly historyManager: HistoryManager;
37
+ readonly inventoryManager: InventoryManager;
34
38
  readonly navigationManager: NavigationManager;
35
39
  readonly eventRunner: EventRunner;
36
40
  readonly saveManager: SaveManager;
@@ -41,7 +45,7 @@ export class Engine {
41
45
  private pendingResume: {
42
46
  eventId: string;
43
47
  checkpoints: Checkpoint[];
44
- initialState: BaseGameState;
48
+ initialState: VylosGameState;
45
49
  } | null = null;
46
50
 
47
51
  /** Flag to skip event lock/push when interrupted by a load */
@@ -50,6 +54,7 @@ export class Engine {
50
54
  constructor(deps: EngineDeps) {
51
55
  this.eventManager = deps.eventManager;
52
56
  this.historyManager = deps.historyManager;
57
+ this.inventoryManager = deps.inventoryManager;
53
58
  this.navigationManager = deps.navigationManager;
54
59
  this.eventRunner = deps.eventRunner;
55
60
  this.saveManager = deps.saveManager;
@@ -57,9 +62,10 @@ export class Engine {
57
62
  }
58
63
 
59
64
  /** Register events and start the game loop */
60
- async run(events: VylosEvent[], getState: () => BaseGameState, loop?: EngineLoopCallbacks): Promise<void> {
65
+ async run(events: VylosEvent[], getState: () => VylosGameState, loop?: EngineLoopCallbacks): Promise<void> {
61
66
  this.eventManager.registerAll(events);
62
67
  this.running = true;
68
+ attachDevConsole(this, getState);
63
69
 
64
70
  logger.info('Engine started');
65
71
 
@@ -131,7 +137,7 @@ export class Engine {
131
137
  * Load a save and resume execution.
132
138
  * Restores game state, history, event lock state, and sets up mid-event resume if needed.
133
139
  */
134
- loadSave(saveData: SaveSlot, setState: (state: BaseGameState) => void): void {
140
+ loadSave(saveData: SaveSlot, setState: (state: VylosGameState) => void): void {
135
141
  // Restore game state
136
142
  setState(JSON.parse(JSON.stringify(saveData.gameState)));
137
143
 
@@ -164,7 +170,7 @@ export class Engine {
164
170
  }
165
171
 
166
172
  /** Execute a single event, handling jumps */
167
- private async executeEvent(event: VylosEvent, getState: () => BaseGameState): Promise<void> {
173
+ private async executeEvent(event: VylosEvent, getState: () => VylosGameState): Promise<void> {
168
174
  let currentEvent: VylosEvent | undefined = event;
169
175
 
170
176
  while (currentEvent) {
@@ -208,7 +214,7 @@ export class Engine {
208
214
  }
209
215
 
210
216
  /** Handle resuming a mid-event save */
211
- private async handleResume(getState: () => BaseGameState): Promise<void> {
217
+ private async handleResume(getState: () => VylosGameState): Promise<void> {
212
218
  const resume = this.pendingResume!;
213
219
  this.pendingResume = null;
214
220
 
@@ -10,6 +10,7 @@ import { SaveManager } from '../managers/SaveManager';
10
10
  import { SettingsManager } from '../managers/SettingsManager';
11
11
  import { EventRunner, type EventRunnerCallbacks } from './EventRunner';
12
12
  import { CheckpointManager } from './CheckpointManager';
13
+ import { InventoryManager } from '../managers/InventoryManager';
13
14
  import { VylosStorage } from '../storage/VylosStorage';
14
15
  import { Engine } from './Engine';
15
16
 
@@ -20,6 +21,7 @@ export const DI_TOKENS = {
20
21
  NavigationManager: 'NavigationManager',
21
22
  WaitManager: 'WaitManager',
22
23
  CheckpointManager: 'CheckpointManager',
24
+ InventoryManager: 'InventoryManager',
23
25
  EventRunner: 'EventRunner',
24
26
  Engine: 'Engine',
25
27
  } as const;
@@ -34,6 +36,7 @@ function registerDefaults(c: DependencyContainer): void {
34
36
  c.register(DI_TOKENS.NavigationManager, { useClass: NavigationManager });
35
37
  c.register(DI_TOKENS.WaitManager, { useClass: WaitManager });
36
38
  c.register(DI_TOKENS.CheckpointManager, { useClass: CheckpointManager });
39
+ c.register(DI_TOKENS.InventoryManager, { useClass: InventoryManager });
37
40
  }
38
41
 
39
42
  export interface CreateEngineOptions {
@@ -72,9 +75,10 @@ export function createEngine(options: CreateEngineOptions): Engine {
72
75
  const eventManager = childContainer.resolve<EventManager>(DI_TOKENS.EventManager);
73
76
  const historyManager = childContainer.resolve<HistoryManager>(DI_TOKENS.HistoryManager);
74
77
  const navigationManager = childContainer.resolve<NavigationManager>(DI_TOKENS.NavigationManager);
78
+ const inventoryManager = childContainer.resolve<InventoryManager>(DI_TOKENS.InventoryManager);
75
79
 
76
- // EventRunner needs callbacks, so we construct it directly
77
- const eventRunner = new EventRunner(options.callbacks);
80
+ // EventRunner needs callbacks + inventoryManager, so we construct it directly
81
+ const eventRunner = new EventRunner(options.callbacks, inventoryManager);
78
82
 
79
83
  // Storage + persistence managers
80
84
  const storage = new VylosStorage(options.projectId ?? 'default');
@@ -88,6 +92,7 @@ export function createEngine(options: CreateEngineOptions): Engine {
88
92
  eventRunner,
89
93
  saveManager,
90
94
  settingsManager,
95
+ inventoryManager,
91
96
  });
92
97
  }
93
98
 
@@ -1,15 +1,18 @@
1
1
  import { toRaw } from 'vue';
2
2
  import type {
3
3
  VylosAPI,
4
+ InventoryAPI,
4
5
  VylosEvent,
5
- BaseGameState,
6
+ VylosGameState,
6
7
  TextEntry,
7
8
  SayOptions,
8
9
  ChoiceItem,
9
10
  CheckpointType,
10
11
  ChoiceOption,
11
12
  DialogueState,
13
+ VylosCharacter,
12
14
  } from '../types';
15
+ import { InventoryManager } from '../managers/InventoryManager';
13
16
  import { CheckpointManager } from './CheckpointManager';
14
17
  import { WaitManager } from '../managers/WaitManager';
15
18
  import { JumpSignal } from '../errors/JumpSignal';
@@ -20,7 +23,7 @@ import { interpolate } from '../utils/TimeHelper';
20
23
 
21
24
  export interface EventRunnerCallbacks {
22
25
  /** Called when dialogue should be displayed */
23
- onSay(text: string, speaker: string | null): void;
26
+ onSay(text: string, speaker: VylosCharacter | null): void;
24
27
  /** Called when choices should be displayed */
25
28
  onChoice(options: Array<{ text: string; value: string; disabled?: boolean }>): void;
26
29
  /** Called to update background */
@@ -38,9 +41,9 @@ export interface EventRunnerCallbacks {
38
41
  /** Resolve a TextEntry to a string using current language */
39
42
  resolveText(entry: string | TextEntry): string;
40
43
  /** Get current game state */
41
- getState(): BaseGameState;
44
+ getState(): VylosGameState;
42
45
  /** Set game state (after rollback) */
43
- setState(state: BaseGameState): void;
46
+ setState(state: VylosGameState): void;
44
47
  }
45
48
 
46
49
  /** Data returned when browsing history steps */
@@ -71,22 +74,39 @@ export class EventRunner implements VylosAPI {
71
74
  /** History browsing index (-1 = live, not browsing) */
72
75
  private browseIndex = -1;
73
76
  /** The live dialogue being displayed when history browsing started */
74
- private liveDialogue: { text: string; speaker: string | null } | null = null;
77
+ private liveDialogue: { text: string; speaker: VylosCharacter | null } | null = null;
75
78
  /** Current background path (tracked for checkpoint storage) */
76
79
  private currentBackground: string | null = null;
77
80
 
78
81
  /** Snapshot of game state before event started (for redo) */
79
- private initialState: BaseGameState | null = null;
82
+ private initialState: VylosGameState | null = null;
80
83
  /** Reference to the currently executing event (for redo) */
81
84
  private currentEvent: VylosEvent | null = null;
82
85
  /** Pending redo request (set by UI, consumed by redo loop) */
83
86
  private pendingRedo: { step: number; choice: string } | null = null;
84
87
 
85
- constructor(callbacks: EventRunnerCallbacks) {
88
+ private inventoryManager: InventoryManager;
89
+
90
+ constructor(callbacks: EventRunnerCallbacks, inventoryManager: InventoryManager) {
86
91
  this.callbacks = callbacks;
92
+ this.inventoryManager = inventoryManager;
87
93
  this.checkpoints = new CheckpointManager();
88
94
  }
89
95
 
96
+ get inventory(): InventoryAPI {
97
+ const inv = () => this.callbacks.getState().inventories;
98
+ const im = this.inventoryManager;
99
+ return {
100
+ add: (bag, itemId, qty) => im.add(inv(), bag, itemId, qty),
101
+ remove: (bag, itemId, qty) => im.remove(inv(), bag, itemId, qty),
102
+ has: (bag, itemId, qty) => im.has(inv(), bag, itemId, qty),
103
+ hasAll: (bag, items) => im.hasAll(inv(), bag, items),
104
+ count: (bag, itemId) => im.count(inv(), bag, itemId),
105
+ list: (bag) => im.list(inv(), bag),
106
+ clear: (bag) => im.clearBag(inv(), bag),
107
+ };
108
+ }
109
+
90
110
  /** Whether the player is browsing text history */
91
111
  get isBrowsingHistory(): boolean {
92
112
  return this.browseIndex >= 0;
@@ -98,12 +118,12 @@ export class EventRunner implements VylosAPI {
98
118
  }
99
119
 
100
120
  /** Get initial state snapshot for save (deep clone) */
101
- getInitialState(): BaseGameState | null {
121
+ getInitialState(): VylosGameState | null {
102
122
  return this.initialState ? structuredClone(this.initialState) : null;
103
123
  }
104
124
 
105
125
  /** Get the live dialogue for restoring display after exiting history */
106
- getLiveDialogue(): { text: string; speaker: string | null } | null {
126
+ getLiveDialogue(): { text: string; speaker: VylosCharacter | null } | null {
107
127
  return this.liveDialogue;
108
128
  }
109
129
 
@@ -207,7 +227,7 @@ export class EventRunner implements VylosAPI {
207
227
  }
208
228
 
209
229
  /** Resume a saved mid-event execution (for load). Checkpoints must be restored externally first. */
210
- async resumeEvent(event: VylosEvent, savedInitialState: BaseGameState): Promise<void> {
230
+ async resumeEvent(event: VylosEvent, savedInitialState: VylosGameState): Promise<void> {
211
231
  this.initialState = structuredClone(savedInitialState);
212
232
  this.currentEvent = event;
213
233
  this.currentStep = 0;
@@ -248,10 +268,7 @@ export class EventRunner implements VylosAPI {
248
268
  }
249
269
 
250
270
  // Resolve speaker
251
- let speaker: string | null = null;
252
- if (options?.from) {
253
- speaker = this.callbacks.resolveText(options.from);
254
- }
271
+ const speaker: VylosCharacter | null = options?.from ?? null;
255
272
 
256
273
  // If replaying, fast-forward: capture checkpoint and resolve immediately
257
274
  if (this.checkpoints.isReplaying) {
@@ -1,4 +1,4 @@
1
- import type { VylosAction, BaseGameState, TextEntry } from '../types';
1
+ import type { VylosAction, VylosGameState, TextEntry } from '../types';
2
2
  import { logger } from '../utils/logger';
3
3
 
4
4
  /**
@@ -26,7 +26,7 @@ export class ActionManager {
26
26
  }
27
27
 
28
28
  /** Get all available actions for a location, filtered by unlock conditions */
29
- getAvailable(locationId: string, state: BaseGameState): VylosAction[] {
29
+ getAvailable(locationId: string, state: VylosGameState): VylosAction[] {
30
30
  const available: VylosAction[] = [];
31
31
 
32
32
  for (const action of this.actions.values()) {
@@ -43,7 +43,7 @@ export class ActionManager {
43
43
  }
44
44
 
45
45
  /** Execute an action by ID */
46
- execute(id: string, state: BaseGameState): boolean {
46
+ execute(id: string, state: VylosGameState): boolean {
47
47
  const action = this.actions.get(id);
48
48
  if (!action) {
49
49
  logger.warn(`Action not found: ${id}`);
@@ -1,4 +1,4 @@
1
- import type { VylosEvent, BaseGameState, DrawableEventEntry } from '../types';
1
+ import type { VylosEvent, VylosGameState, DrawableEventEntry } from '../types';
2
2
  import { EventStatus } from '../types';
3
3
  import { logger } from '../utils/logger';
4
4
 
@@ -46,7 +46,7 @@ export class EventManager {
46
46
  }
47
47
 
48
48
  /** Evaluate conditions and update statuses. Returns newly unlocked events. */
49
- evaluate(state: BaseGameState): VylosEvent[] {
49
+ evaluate(state: VylosGameState): VylosEvent[] {
50
50
  const unlocked: VylosEvent[] = [];
51
51
 
52
52
  for (const [id, entry] of this.events) {
@@ -68,7 +68,7 @@ export class EventManager {
68
68
  }
69
69
 
70
70
  /** Get the next unlocked event matching the current location (first registered wins). Skips drawable events. */
71
- getNextUnlocked(state: BaseGameState): VylosEvent | undefined {
71
+ getNextUnlocked(state: VylosGameState): VylosEvent | undefined {
72
72
  for (const entry of this.events.values()) {
73
73
  if (entry.status !== EventStatus.Unlocked) continue;
74
74
  if (entry.event.draw) continue; // Drawable events don't auto-trigger
@@ -79,7 +79,7 @@ export class EventManager {
79
79
  }
80
80
 
81
81
  /** Get all unlocked drawable events at the current location */
82
- getDrawableEvents(state: BaseGameState, resolveText?: (text: string | Record<string, string>) => string): DrawableEventEntry[] {
82
+ getDrawableEvents(state: VylosGameState, resolveText?: (text: string | Record<string, string>) => string): DrawableEventEntry[] {
83
83
  const result: DrawableEventEntry[] = [];
84
84
  for (const entry of this.events.values()) {
85
85
  if (entry.status !== EventStatus.Unlocked) continue;
@@ -108,7 +108,7 @@ export class EventManager {
108
108
  }
109
109
 
110
110
  /** Mark event as locked (completed) */
111
- setLocked(id: string, state: BaseGameState): void {
111
+ setLocked(id: string, state: VylosGameState): void {
112
112
  const entry = this.events.get(id);
113
113
  if (entry) {
114
114
  entry.status = EventStatus.Locked;
@@ -0,0 +1,149 @@
1
+ import type { VylosCategory, VylosItem, InventoryData } from '../types';
2
+
3
+ /**
4
+ * Manages item/category definitions and inventory operations.
5
+ *
6
+ * No constructor args — compatible with tsyringe `{ useClass }` DI resolution.
7
+ * Projects can extend this class and override any method via the plugin system.
8
+ */
9
+ export class InventoryManager {
10
+ private categories = new Map<string, VylosCategory>();
11
+ private items = new Map<string, VylosItem>();
12
+
13
+ // --- Category registry ---
14
+
15
+ registerCategory(cat: VylosCategory): void {
16
+ this.categories.set(cat.id, cat);
17
+ }
18
+
19
+ registerCategories(cats: VylosCategory[]): void {
20
+ for (const cat of cats) {
21
+ this.registerCategory(cat);
22
+ }
23
+ }
24
+
25
+ getCategory(id: string): VylosCategory | undefined {
26
+ return this.categories.get(id);
27
+ }
28
+
29
+ /** Returns all categories sorted by sortOrder (ascending, undefined last) */
30
+ getCategories(): VylosCategory[] {
31
+ return [...this.categories.values()].sort((a, b) => {
32
+ const sa = a.sortOrder ?? Infinity;
33
+ const sb = b.sortOrder ?? Infinity;
34
+ return sa - sb;
35
+ });
36
+ }
37
+
38
+ // --- Item registry ---
39
+
40
+ registerItem(item: VylosItem): void {
41
+ this.items.set(item.id, item);
42
+ }
43
+
44
+ registerItems(items: VylosItem[]): void {
45
+ for (const item of items) {
46
+ this.registerItem(item);
47
+ }
48
+ }
49
+
50
+ getItem(id: string): VylosItem | undefined {
51
+ return this.items.get(id);
52
+ }
53
+
54
+ getItemsByCategory(categoryId: string): VylosItem[] {
55
+ return [...this.items.values()].filter((i) => i.category === categoryId);
56
+ }
57
+
58
+ // --- Inventory operations (on state.inventories) ---
59
+
60
+ /**
61
+ * Add items to a bag. Auto-creates bag if missing.
62
+ * Respects maxStack from the item definition.
63
+ * @returns actual quantity added
64
+ */
65
+ add(inv: InventoryData, bag: string, itemId: string, qty = 1): number {
66
+ if (qty <= 0) return 0;
67
+
68
+ if (!inv[bag]) inv[bag] = {};
69
+
70
+ const current = inv[bag][itemId] ?? 0;
71
+ const item = this.items.get(itemId);
72
+ const maxStack = item?.maxStack ?? Infinity;
73
+
74
+ // Check maxSlots: if the item isn't already in the bag and we'd exceed slot count
75
+ if (current === 0 && item) {
76
+ const category = this.categories.get(item.category);
77
+ if (category?.maxSlots !== undefined) {
78
+ const slotCount = Object.keys(inv[bag]).length;
79
+ if (slotCount >= category.maxSlots) return 0;
80
+ }
81
+ }
82
+
83
+ const actual = Math.min(qty, maxStack - current);
84
+ if (actual <= 0) return 0;
85
+
86
+ inv[bag][itemId] = current + actual;
87
+ return actual;
88
+ }
89
+
90
+ /**
91
+ * Remove items from a bag. Cleans up zero entries.
92
+ * @returns actual quantity removed
93
+ */
94
+ remove(inv: InventoryData, bag: string, itemId: string, qty = 1): number {
95
+ if (qty <= 0) return 0;
96
+ if (!inv[bag]) return 0;
97
+
98
+ const current = inv[bag][itemId] ?? 0;
99
+ if (current <= 0) return 0;
100
+
101
+ const actual = Math.min(qty, current);
102
+ const remaining = current - actual;
103
+
104
+ if (remaining <= 0) {
105
+ delete inv[bag][itemId];
106
+ } else {
107
+ inv[bag][itemId] = remaining;
108
+ }
109
+
110
+ return actual;
111
+ }
112
+
113
+ /** Get the quantity of an item in a bag */
114
+ count(inv: InventoryData, bag: string, itemId: string): number {
115
+ return inv[bag]?.[itemId] ?? 0;
116
+ }
117
+
118
+ /** Check if a bag has at least `qty` of an item */
119
+ has(inv: InventoryData, bag: string, itemId: string, qty = 1): boolean {
120
+ return this.count(inv, bag, itemId) >= qty;
121
+ }
122
+
123
+ /** Check if a bag has all the specified items in required quantities */
124
+ hasAll(inv: InventoryData, bag: string, items: Record<string, number>): boolean {
125
+ for (const [itemId, qty] of Object.entries(items)) {
126
+ if (!this.has(inv, bag, itemId, qty)) return false;
127
+ }
128
+ return true;
129
+ }
130
+
131
+ /** List all items in a bag as [itemId, quantity] pairs */
132
+ list(inv: InventoryData, bag: string): Array<[string, number]> {
133
+ if (!inv[bag]) return [];
134
+ return Object.entries(inv[bag]);
135
+ }
136
+
137
+ /** Clear all items from a bag */
138
+ clearBag(inv: InventoryData, bag: string): void {
139
+ delete inv[bag];
140
+ }
141
+
142
+ // --- Cleanup ---
143
+
144
+ /** Clear all registered categories and items */
145
+ clear(): void {
146
+ this.categories.clear();
147
+ this.items.clear();
148
+ }
149
+ }
@@ -1,4 +1,4 @@
1
- import type { VylosLocation, LocationLink, BaseGameState, BackgroundEntry } from '../types';
1
+ import type { VylosLocation, LocationLink, VylosGameState, BackgroundEntry } from '../types';
2
2
  import { resolveBackground } from '../utils/TimeHelper';
3
3
  import { logger } from '../utils/logger';
4
4
 
@@ -38,7 +38,7 @@ export class LocationManager {
38
38
  }
39
39
 
40
40
  /** Get locations accessible from a given location, considering state conditions */
41
- getAccessibleFrom(locationId: string, state: BaseGameState): VylosLocation[] {
41
+ getAccessibleFrom(locationId: string, state: VylosGameState): VylosLocation[] {
42
42
  const linkedIds = this.links
43
43
  .filter(link => {
44
44
  if (link.from !== locationId) return false;
@@ -1,8 +1,8 @@
1
- import type { BaseGameState } from './game-state';
1
+ import type { VylosGameState } from './game-state';
2
2
  import type { TextEntry } from './events';
3
3
 
4
4
  /** An action available to the player at a location */
5
- export interface VylosAction<TState extends BaseGameState = BaseGameState> {
5
+ export interface VylosAction<TState extends VylosGameState = VylosGameState> {
6
6
  /** Unique action ID */
7
7
  id: string;
8
8
 
@@ -1,4 +1,4 @@
1
- import type { BaseGameState } from './game-state';
1
+ import type { VylosGameState } from './game-state';
2
2
  import type { DialogueState, ChoiceOption } from './engine';
3
3
 
4
4
  /** A checkpoint captured at each interaction point */
@@ -6,7 +6,7 @@ export interface Checkpoint {
6
6
  /** Sequential step number within the event */
7
7
  step: number;
8
8
  /** Deep clone of game state at this point */
9
- gameState: BaseGameState;
9
+ gameState: VylosGameState;
10
10
  /** Type of interaction at this step */
11
11
  type: CheckpointType;
12
12
  /** Stored choice result (for replay during rollback) */
@@ -1,14 +1,7 @@
1
1
  import type { TextEntry } from './events';
2
2
 
3
- /** Resolved dialogue line ready for display */
4
- export interface DialogueLine {
5
- text: string;
6
- speaker: string | null;
7
- isNarration: boolean;
8
- }
9
-
10
- /** Character definition for speaker resolution */
11
- export interface Character {
3
+ /** Base character projects extend this into their own Character */
4
+ export interface VylosCharacter {
12
5
  id: string;
13
6
  name: string | TextEntry;
14
7
  color?: string;
@@ -1,4 +1,5 @@
1
1
  import type { DrawableEventEntry } from './events';
2
+ import type { VylosCharacter } from './dialogue';
2
3
 
3
4
  /** Engine lifecycle phases */
4
5
  export enum EnginePhase {
@@ -32,7 +33,7 @@ export interface EngineState {
32
33
 
33
34
  export interface DialogueState {
34
35
  text: string;
35
- speaker: string | null;
36
+ speaker: VylosCharacter | null;
36
37
  isNarration: boolean;
37
38
  }
38
39