@react-text-game/core 0.5.6 → 0.5.8

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 (69) hide show
  1. package/README.md +587 -528
  2. package/dist/audio/constants.d.ts.map +1 -1
  3. package/dist/audio/constants.js.map +1 -1
  4. package/dist/audio/index.d.ts +1 -1
  5. package/dist/audio/index.d.ts.map +1 -1
  6. package/dist/game.d.ts +26 -0
  7. package/dist/game.d.ts.map +1 -1
  8. package/dist/game.js +28 -0
  9. package/dist/game.js.map +1 -1
  10. package/dist/gameObjects/index.d.ts +3 -3
  11. package/dist/gameObjects/index.js +3 -3
  12. package/dist/gameObjects/simpleObject.d.ts.map +1 -1
  13. package/dist/gameObjects/simpleObject.js.map +1 -1
  14. package/dist/hooks/index.d.ts +6 -5
  15. package/dist/hooks/index.d.ts.map +1 -1
  16. package/dist/hooks/index.js +6 -5
  17. package/dist/hooks/index.js.map +1 -1
  18. package/dist/hooks/useCurrentPassage.d.ts +10 -5
  19. package/dist/hooks/useCurrentPassage.d.ts.map +1 -1
  20. package/dist/hooks/useCurrentPassage.js +14 -6
  21. package/dist/hooks/useCurrentPassage.js.map +1 -1
  22. package/dist/hooks/useGameIsStarted.d.ts.map +1 -1
  23. package/dist/hooks/useGameIsStarted.js +1 -1
  24. package/dist/hooks/useGameIsStarted.js.map +1 -1
  25. package/dist/hooks/useIsStoryMode.d.ts +11 -0
  26. package/dist/hooks/useIsStoryMode.d.ts.map +1 -0
  27. package/dist/hooks/useIsStoryMode.js +15 -0
  28. package/dist/hooks/useIsStoryMode.js.map +1 -0
  29. package/dist/i18n/hooks/index.d.ts +1 -1
  30. package/dist/i18n/hooks/index.js +1 -1
  31. package/dist/i18n/index.d.ts +4 -4
  32. package/dist/i18n/index.js +4 -4
  33. package/dist/i18n/init.js +2 -2
  34. package/dist/i18n/init.js.map +1 -1
  35. package/dist/i18n/utils.js +1 -1
  36. package/dist/logger.js +1 -1
  37. package/dist/options.d.ts.map +1 -1
  38. package/dist/options.js.map +1 -1
  39. package/dist/passages/story/types.d.ts.map +1 -1
  40. package/dist/passages/types/index.d.ts +2 -2
  41. package/dist/saves/db.js.map +1 -1
  42. package/dist/saves/hooks/useRestartGame.d.ts.map +1 -1
  43. package/dist/saves/hooks/useRestartGame.js +4 -1
  44. package/dist/saves/hooks/useRestartGame.js.map +1 -1
  45. package/dist/saves/hooks/useSaveSlots.d.ts.map +1 -1
  46. package/dist/saves/hooks/useSaveSlots.js +1 -1
  47. package/dist/saves/hooks/useSaveSlots.js.map +1 -1
  48. package/dist/saves/index.d.ts +4 -4
  49. package/dist/saves/index.js +3 -3
  50. package/dist/saves/migrations/EXAMPLE.d.ts.map +1 -1
  51. package/dist/saves/migrations/EXAMPLE.js.map +1 -1
  52. package/dist/saves/migrations/runner.d.ts.map +1 -1
  53. package/dist/saves/migrations/runner.js.map +1 -1
  54. package/dist/saves/migrations/types.d.ts.map +1 -1
  55. package/dist/tests/audio.test.js +22 -23
  56. package/dist/tests/audio.test.js.map +1 -1
  57. package/dist/tests/game.test.js +48 -0
  58. package/dist/tests/game.test.js.map +1 -1
  59. package/dist/tests/i18n.test.js +5 -3
  60. package/dist/tests/i18n.test.js.map +1 -1
  61. package/dist/tests/migrations.test.js.map +1 -1
  62. package/dist/tests/simpleObject.test.js +2 -1
  63. package/dist/tests/simpleObject.test.js.map +1 -1
  64. package/dist/tests/storage.test.js +1 -3
  65. package/dist/tests/storage.test.js.map +1 -1
  66. package/dist/tests/story.test.js.map +1 -1
  67. package/dist/types.d.ts +1 -3
  68. package/dist/types.d.ts.map +1 -1
  69. package/package.json +1 -1
package/README.md CHANGED
@@ -33,35 +33,35 @@ pnpm add @react-text-game/core
33
33
  ## Quick Start
34
34
 
35
35
  ```tsx
36
- import { Game, createEntity, newStory } from '@react-text-game/core';
36
+ import { Game, createEntity, newStory } from "@react-text-game/core";
37
37
 
38
38
  // IMPORTANT: Initialize the game first
39
39
  await Game.init({
40
- gameName: 'My Adventure',
41
- translations: {
42
- defaultLanguage: 'en',
43
- fallbackLanguage: 'en',
44
- resources: {
45
- en: {
46
- passages: { intro: 'Welcome to the Game' },
47
- common: { save: 'Save', load: 'Load' }
48
- },
49
- ru: {
50
- passages: { intro: 'Добро пожаловать в игру' }
51
- }
52
- }
53
- },
54
- // ...other options
40
+ gameName: "My Adventure",
41
+ translations: {
42
+ defaultLanguage: "en",
43
+ fallbackLanguage: "en",
44
+ resources: {
45
+ en: {
46
+ passages: { intro: "Welcome to the Game" },
47
+ common: { save: "Save", load: "Load" },
48
+ },
49
+ ru: {
50
+ passages: { intro: "Добро пожаловать в игру" },
51
+ },
52
+ },
53
+ },
54
+ // ...other options
55
55
  });
56
56
 
57
57
  // Create a game entity with the factory (recommended)
58
- const player = createEntity('player', {
59
- name: 'Hero',
60
- stats: {
61
- health: 100,
62
- mana: 50
63
- },
64
- inventory: [] as string[],
58
+ const player = createEntity("player", {
59
+ name: "Hero",
60
+ stats: {
61
+ health: 100,
62
+ mana: 50,
63
+ },
64
+ inventory: [] as string[],
65
65
  });
66
66
 
67
67
  // Direct property updates automatically stay reactive
@@ -71,25 +71,25 @@ player.stats.health -= 10;
71
71
  player.save();
72
72
 
73
73
  // Create a story passage
74
- const introStory = newStory('intro', () => [
75
- {
76
- type: 'header',
77
- content: 'Welcome to the Game',
78
- props: { level: 1 }
79
- },
80
- {
81
- type: 'text',
82
- content: `Hello, ${player.name}!`
83
- },
84
- {
85
- type: 'actions',
86
- content: [
87
- {
88
- label: 'Start Adventure',
89
- action: () => Game.jumpTo('adventure')
90
- }
91
- ]
92
- }
74
+ const introStory = newStory("intro", () => [
75
+ {
76
+ type: "header",
77
+ content: "Welcome to the Game",
78
+ props: { level: 1 },
79
+ },
80
+ {
81
+ type: "text",
82
+ content: `Hello, ${player.name}!`,
83
+ },
84
+ {
85
+ type: "actions",
86
+ content: [
87
+ {
88
+ label: "Start Adventure",
89
+ action: () => Game.jumpTo("adventure"),
90
+ },
91
+ ],
92
+ },
93
93
  ]);
94
94
 
95
95
  // Navigate to passage
@@ -115,13 +115,13 @@ The core engine includes a comprehensive audio system with reactive state manage
115
115
  ### Quick Start
116
116
 
117
117
  ```typescript
118
- import { createAudio, AudioManager } from '@react-text-game/core/audio';
118
+ import { createAudio, AudioManager } from "@react-text-game/core/audio";
119
119
 
120
120
  // Create an audio track
121
- const bgMusic = createAudio('/audio/background.mp3', {
122
- id: 'bg-music',
123
- volume: 0.7,
124
- loop: true,
121
+ const bgMusic = createAudio("/audio/background.mp3", {
122
+ id: "bg-music",
123
+ volume: 0.7,
124
+ loop: true,
125
125
  });
126
126
 
127
127
  // Play the track
@@ -144,20 +144,20 @@ AudioManager.pauseAll();
144
144
  Use the `createAudio` factory function to create audio tracks:
145
145
 
146
146
  ```typescript
147
- import { createAudio } from '@react-text-game/core/audio';
147
+ import { createAudio } from "@react-text-game/core/audio";
148
148
 
149
149
  // Basic audio track
150
- const sfx = createAudio('/audio/click.mp3');
150
+ const sfx = createAudio("/audio/click.mp3");
151
151
 
152
152
  // With options
153
- const music = createAudio('/audio/theme.mp3', {
154
- id: 'theme-music', // Required for persistence
155
- volume: 0.6, // 0.0 to 1.0 (default: 1.0)
156
- loop: true, // Auto-loop (default: false)
157
- playbackRate: 1.0, // Playback speed (default: 1.0)
158
- muted: false, // Start muted (default: false)
159
- autoPlay: false, // Auto-play on creation (default: false)
160
- preload: 'metadata', // 'none', 'metadata', or 'auto' (default: 'metadata')
153
+ const music = createAudio("/audio/theme.mp3", {
154
+ id: "theme-music", // Required for persistence
155
+ volume: 0.6, // 0.0 to 1.0 (default: 1.0)
156
+ loop: true, // Auto-loop (default: false)
157
+ playbackRate: 1.0, // Playback speed (default: 1.0)
158
+ muted: false, // Start muted (default: false)
159
+ autoPlay: false, // Auto-play on creation (default: false)
160
+ preload: "metadata", // 'none', 'metadata', or 'auto' (default: 'metadata')
161
161
  });
162
162
  ```
163
163
 
@@ -167,31 +167,31 @@ Each audio track provides comprehensive playback controls:
167
167
 
168
168
  ```typescript
169
169
  // Playback control
170
- await audio.play(); // Start playback (returns Promise)
171
- audio.pause(); // Pause playback
172
- audio.resume(); // Resume from pause
173
- audio.stop(); // Stop and reset to beginning
170
+ await audio.play(); // Start playback (returns Promise)
171
+ audio.pause(); // Pause playback
172
+ audio.resume(); // Resume from pause
173
+ audio.stop(); // Stop and reset to beginning
174
174
 
175
175
  // Volume and settings
176
- audio.setVolume(0.5); // Set volume (0.0 to 1.0)
177
- audio.setLoop(true); // Enable/disable looping
178
- audio.setPlaybackRate(1.5); // Set playback speed
179
- audio.setMuted(true); // Mute/unmute
176
+ audio.setVolume(0.5); // Set volume (0.0 to 1.0)
177
+ audio.setLoop(true); // Enable/disable looping
178
+ audio.setPlaybackRate(1.5); // Set playback speed
179
+ audio.setMuted(true); // Mute/unmute
180
180
 
181
181
  // Seeking
182
- audio.seek(30); // Seek to 30 seconds
182
+ audio.seek(30); // Seek to 30 seconds
183
183
 
184
184
  // Fade effects
185
- await audio.fadeIn(2000); // Fade in over 2 seconds
186
- await audio.fadeOut(1500); // Fade out over 1.5 seconds
185
+ await audio.fadeIn(2000); // Fade in over 2 seconds
186
+ await audio.fadeOut(1500); // Fade out over 1.5 seconds
187
187
 
188
188
  // State and persistence
189
189
  const state = audio.getState(); // Get reactive state
190
- audio.save(); // Save state to storage
191
- audio.load(); // Load state from storage
190
+ audio.save(); // Save state to storage
191
+ audio.load(); // Load state from storage
192
192
 
193
193
  // Cleanup
194
- audio.dispose(); // Remove and clean up
194
+ audio.dispose(); // Remove and clean up
195
195
  ```
196
196
 
197
197
  ### Reactive State
@@ -199,21 +199,21 @@ audio.dispose(); // Remove and clean up
199
199
  Audio tracks use Valtio for reactive state management, making them perfect for React integration:
200
200
 
201
201
  ```typescript
202
- const audio = createAudio('/audio/music.mp3', { id: 'music' });
202
+ const audio = createAudio("/audio/music.mp3", { id: "music" });
203
203
 
204
204
  // Get reactive state
205
205
  const state = audio.getState();
206
206
 
207
207
  // Access state properties
208
- console.log(state.isPlaying); // boolean
209
- console.log(state.isPaused); // boolean
210
- console.log(state.isStopped); // boolean
211
- console.log(state.currentTime); // number (seconds)
212
- console.log(state.duration); // number (seconds)
213
- console.log(state.volume); // number (0.0 to 1.0)
214
- console.log(state.loop); // boolean
208
+ console.log(state.isPlaying); // boolean
209
+ console.log(state.isPaused); // boolean
210
+ console.log(state.isStopped); // boolean
211
+ console.log(state.currentTime); // number (seconds)
212
+ console.log(state.duration); // number (seconds)
213
+ console.log(state.volume); // number (0.0 to 1.0)
214
+ console.log(state.loop); // boolean
215
215
  console.log(state.playbackRate); // number
216
- console.log(state.muted); // boolean
216
+ console.log(state.muted); // boolean
217
217
  ```
218
218
 
219
219
  ### Global Audio Manager
@@ -221,30 +221,31 @@ console.log(state.muted); // boolean
221
221
  The `AudioManager` provides global controls for all registered audio tracks:
222
222
 
223
223
  ```typescript
224
- import { AudioManager } from '@react-text-game/core/audio';
224
+ import { AudioManager } from "@react-text-game/core/audio";
225
225
 
226
226
  // Master volume control
227
- AudioManager.setMasterVolume(0.5); // Set master volume (0.0 to 1.0)
227
+ AudioManager.setMasterVolume(0.5); // Set master volume (0.0 to 1.0)
228
228
  const volume = AudioManager.getMasterVolume(); // Get master volume
229
229
 
230
230
  // Global playback control
231
- AudioManager.pauseAll(); // Pause all playing tracks
232
- AudioManager.resumeAll(); // Resume all paused tracks
233
- AudioManager.stopAll(); // Stop all tracks
231
+ AudioManager.pauseAll(); // Pause all playing tracks
232
+ AudioManager.resumeAll(); // Resume all paused tracks
233
+ AudioManager.stopAll(); // Stop all tracks
234
234
 
235
235
  // Global mute control
236
- AudioManager.muteAll(); // Mute all tracks
237
- AudioManager.unmuteAll(); // Unmute all tracks
236
+ AudioManager.muteAll(); // Mute all tracks
237
+ AudioManager.unmuteAll(); // Unmute all tracks
238
238
 
239
239
  // Track management
240
- const tracks = AudioManager.getAllTracks(); // Get all registered tracks
241
- const music = AudioManager.getTrackById('bg-music'); // Get specific track by ID
240
+ const tracks = AudioManager.getAllTracks(); // Get all registered tracks
241
+ const music = AudioManager.getTrackById("bg-music"); // Get specific track by ID
242
242
 
243
243
  // Cleanup
244
244
  AudioManager.disposeAll(); // Dispose all tracks
245
245
  ```
246
246
 
247
247
  **Master Volume Behavior:**
248
+
248
249
  - Master volume is a multiplier applied to all track volumes
249
250
  - Does not modify individual track volume settings
250
251
  - Example: Track at 0.8 volume with 0.5 master = 0.4 effective volume
@@ -259,37 +260,40 @@ The audio system includes React hooks for seamless component integration:
259
260
  Monitor individual audio track state with automatic re-renders:
260
261
 
261
262
  ```tsx
262
- import { createAudio } from '@react-text-game/core/audio';
263
- import { useAudio } from '@react-text-game/core';
263
+ import { createAudio } from "@react-text-game/core/audio";
264
+ import { useAudio } from "@react-text-game/core";
264
265
 
265
- const bgMusic = createAudio('/audio/background.mp3', {
266
- id: 'bg-music',
267
- loop: true,
266
+ const bgMusic = createAudio("/audio/background.mp3", {
267
+ id: "bg-music",
268
+ loop: true,
268
269
  });
269
270
 
270
271
  function MusicPlayer() {
271
- const audioState = useAudio(bgMusic);
272
-
273
- return (
274
- <div>
275
- <p>Status: {audioState.isPlaying ? 'Playing' : 'Stopped'}</p>
276
- <p>Time: {audioState.currentTime.toFixed(1)}s / {audioState.duration.toFixed(1)}s</p>
277
- <p>Volume: {(audioState.volume * 100).toFixed(0)}%</p>
278
-
279
- <button onClick={() => bgMusic.play()}>Play</button>
280
- <button onClick={() => bgMusic.pause()}>Pause</button>
281
- <button onClick={() => bgMusic.stop()}>Stop</button>
282
-
283
- <input
284
- type="range"
285
- min="0"
286
- max="1"
287
- step="0.01"
288
- value={audioState.volume}
289
- onChange={(e) => bgMusic.setVolume(parseFloat(e.target.value))}
290
- />
291
- </div>
292
- );
272
+ const audioState = useAudio(bgMusic);
273
+
274
+ return (
275
+ <div>
276
+ <p>Status: {audioState.isPlaying ? "Playing" : "Stopped"}</p>
277
+ <p>
278
+ Time: {audioState.currentTime.toFixed(1)}s /{" "}
279
+ {audioState.duration.toFixed(1)}s
280
+ </p>
281
+ <p>Volume: {(audioState.volume * 100).toFixed(0)}%</p>
282
+
283
+ <button onClick={() => bgMusic.play()}>Play</button>
284
+ <button onClick={() => bgMusic.pause()}>Pause</button>
285
+ <button onClick={() => bgMusic.stop()}>Stop</button>
286
+
287
+ <input
288
+ type="range"
289
+ min="0"
290
+ max="1"
291
+ step="0.01"
292
+ value={audioState.volume}
293
+ onChange={(e) => bgMusic.setVolume(parseFloat(e.target.value))}
294
+ />
295
+ </div>
296
+ );
293
297
  }
294
298
  ```
295
299
 
@@ -298,38 +302,40 @@ function MusicPlayer() {
298
302
  Access global audio controls in React components:
299
303
 
300
304
  ```tsx
301
- import { useAudioManager } from '@react-text-game/core';
305
+ import { useAudioManager } from "@react-text-game/core";
302
306
 
303
307
  function AudioSettings() {
304
- const audioManager = useAudioManager();
305
-
306
- return (
307
- <div>
308
- <h2>Audio Settings</h2>
309
-
310
- <label>
311
- Master Volume: {(audioManager.masterVolume * 100).toFixed(0)}%
312
- <input
313
- type="range"
314
- min="0"
315
- max="1"
316
- step="0.01"
317
- value={audioManager.masterVolume}
318
- onChange={(e) => audioManager.setMasterVolume(parseFloat(e.target.value))}
319
- />
320
- </label>
321
-
322
- <div>
323
- <button onClick={audioManager.muteAll}>Mute All</button>
324
- <button onClick={audioManager.unmuteAll}>Unmute All</button>
325
- <button onClick={audioManager.pauseAll}>Pause All</button>
326
- <button onClick={audioManager.resumeAll}>Resume All</button>
327
- </div>
328
-
329
- <p>Muted: {audioManager.isMuted ? 'Yes' : 'No'}</p>
330
- <p>Active Tracks: {audioManager.getAllTracks().length}</p>
331
- </div>
332
- );
308
+ const audioManager = useAudioManager();
309
+
310
+ return (
311
+ <div>
312
+ <h2>Audio Settings</h2>
313
+
314
+ <label>
315
+ Master Volume: {(audioManager.masterVolume * 100).toFixed(0)}%
316
+ <input
317
+ type="range"
318
+ min="0"
319
+ max="1"
320
+ step="0.01"
321
+ value={audioManager.masterVolume}
322
+ onChange={(e) =>
323
+ audioManager.setMasterVolume(parseFloat(e.target.value))
324
+ }
325
+ />
326
+ </label>
327
+
328
+ <div>
329
+ <button onClick={audioManager.muteAll}>Mute All</button>
330
+ <button onClick={audioManager.unmuteAll}>Unmute All</button>
331
+ <button onClick={audioManager.pauseAll}>Pause All</button>
332
+ <button onClick={audioManager.resumeAll}>Resume All</button>
333
+ </div>
334
+
335
+ <p>Muted: {audioManager.isMuted ? "Yes" : "No"}</p>
336
+ <p>Active Tracks: {audioManager.getAllTracks().length}</p>
337
+ </div>
338
+ );
333
339
  }
334
340
  ```
335
341
 
@@ -339,10 +345,10 @@ Audio tracks with an `id` automatically persist their state:
339
345
 
340
346
  ```typescript
341
347
  // Create audio with ID for persistence
342
- const music = createAudio('/audio/theme.mp3', {
343
- id: 'theme-music',
344
- volume: 0.7,
345
- loop: true,
348
+ const music = createAudio("/audio/theme.mp3", {
349
+ id: "theme-music",
350
+ volume: 0.7,
351
+ loop: true,
346
352
  });
347
353
 
348
354
  // State is automatically saved when it changes
@@ -351,13 +357,14 @@ music.setVolume(0.5);
351
357
  // State saved automatically
352
358
 
353
359
  // On game restart/reload
354
- const music = createAudio('/audio/theme.mp3', {
355
- id: 'theme-music', // Same ID
360
+ const music = createAudio("/audio/theme.mp3", {
361
+ id: "theme-music", // Same ID
356
362
  });
357
363
  music.load(); // Restores volume, position, playing state
358
364
  ```
359
365
 
360
366
  **What Gets Persisted:**
367
+
361
368
  - Volume level
362
369
  - Loop setting
363
370
  - Playback rate
@@ -372,19 +379,16 @@ music.load(); // Restores volume, position, playing state
372
379
  #### Background Music with Crossfade
373
380
 
374
381
  ```typescript
375
- const oldMusic = AudioManager.getTrackById('current-music');
376
- const newMusic = createAudio('/audio/new-theme.mp3', {
377
- id: 'current-music',
378
- loop: true,
382
+ const oldMusic = AudioManager.getTrackById("current-music");
383
+ const newMusic = createAudio("/audio/new-theme.mp3", {
384
+ id: "current-music",
385
+ loop: true,
379
386
  });
380
387
 
381
388
  // Crossfade between tracks
382
389
  if (oldMusic) {
383
- await Promise.all([
384
- oldMusic.fadeOut(1000),
385
- newMusic.fadeIn(1000)
386
- ]);
387
- oldMusic.dispose();
390
+ await Promise.all([oldMusic.fadeOut(1000), newMusic.fadeIn(1000)]);
391
+ oldMusic.dispose();
388
392
  }
389
393
  ```
390
394
 
@@ -393,32 +397,32 @@ if (oldMusic) {
393
397
  ```typescript
394
398
  // Create sound effect without ID (no persistence needed)
395
399
  function playSoundEffect(src: string) {
396
- const sfx = createAudio(src, {
397
- volume: 0.8,
398
- });
400
+ const sfx = createAudio(src, {
401
+ volume: 0.8,
402
+ });
399
403
 
400
- sfx.play();
404
+ sfx.play();
401
405
 
402
- // Auto-cleanup when finished
403
- sfx.audioElement.addEventListener('ended', () => {
404
- sfx.dispose();
405
- });
406
+ // Auto-cleanup when finished
407
+ sfx.audioElement.addEventListener("ended", () => {
408
+ sfx.dispose();
409
+ });
406
410
  }
407
411
 
408
- playSoundEffect('/audio/click.mp3');
412
+ playSoundEffect("/audio/click.mp3");
409
413
  ```
410
414
 
411
415
  #### Pause Audio During Dialogue
412
416
 
413
417
  ```typescript
414
418
  function showDialogue() {
415
- // Pause background music
416
- AudioManager.pauseAll();
419
+ // Pause background music
420
+ AudioManager.pauseAll();
417
421
 
418
- // Show dialogue...
422
+ // Show dialogue...
419
423
 
420
- // Resume when done
421
- AudioManager.resumeAll();
424
+ // Resume when done
425
+ AudioManager.resumeAll();
422
426
  }
423
427
  ```
424
428
 
@@ -427,25 +431,29 @@ function showDialogue() {
427
431
  Modern browsers restrict audio autoplay without user interaction. The audio system handles this gracefully:
428
432
 
429
433
  ```typescript
430
- const music = createAudio('/audio/theme.mp3', {
431
- autoPlay: true, // May be blocked by browser
434
+ const music = createAudio("/audio/theme.mp3", {
435
+ autoPlay: true, // May be blocked by browser
432
436
  });
433
437
 
434
438
  // Autoplay failures are logged but don't throw errors
435
439
  // Manually play after user interaction:
436
- document.addEventListener('click', async () => {
437
- await music.play(); // Will work after user interaction
438
- }, { once: true });
440
+ document.addEventListener(
441
+ "click",
442
+ async () => {
443
+ await music.play(); // Will work after user interaction
444
+ },
445
+ { once: true }
446
+ );
439
447
  ```
440
448
 
441
449
  ### TypeScript Types
442
450
 
443
451
  ```typescript
444
452
  import type {
445
- AudioOptions,
446
- AudioState,
447
- AudioSaveState,
448
- } from '@react-text-game/core/audio';
453
+ AudioOptions,
454
+ AudioState,
455
+ AudioSaveState,
456
+ } from "@react-text-game/core/audio";
449
457
 
450
458
  // All types include comprehensive JSDoc documentation
451
459
  ```
@@ -453,11 +461,13 @@ import type {
453
461
  ### API Reference
454
462
 
455
463
  **createAudio(src, options?)**
464
+
456
465
  - `src: string` - Audio file URL
457
466
  - `options?: AudioOptions` - Configuration options
458
467
  - Returns: `AudioTrack`
459
468
 
460
469
  **AudioTrack Methods:**
470
+
461
471
  - `play(): Promise<void>` - Start playback
462
472
  - `pause(): void` - Pause playback
463
473
  - `resume(): void` - Resume from pause
@@ -475,6 +485,7 @@ import type {
475
485
  - `dispose(): void` - Clean up and remove
476
486
 
477
487
  **AudioManager Methods:**
488
+
478
489
  - `setMasterVolume(volume: number): void` - Set master volume
479
490
  - `getMasterVolume(): number` - Get master volume
480
491
  - `muteAll(): void` - Mute all tracks
@@ -495,28 +506,28 @@ The core engine ships with first-class i18n based on `i18next` and `react-i18nex
495
506
  Pass an `I18nConfig` via the `translations` field when you call `Game.init()`:
496
507
 
497
508
  ```ts
498
- import type { I18nConfig } from '@react-text-game/core/i18n';
509
+ import type { I18nConfig } from "@react-text-game/core/i18n";
499
510
 
500
511
  const translations: I18nConfig = {
501
- defaultLanguage: 'en',
502
- fallbackLanguage: 'en',
503
- debug: false,
504
- resources: {
505
- en: {
506
- passages: { intro: 'Welcome to the game' },
507
- common: { save: 'Save', load: 'Load' }
512
+ defaultLanguage: "en",
513
+ fallbackLanguage: "en",
514
+ debug: false,
515
+ resources: {
516
+ en: {
517
+ passages: { intro: "Welcome to the game" },
518
+ common: { save: "Save", load: "Load" },
519
+ },
520
+ es: {
521
+ passages: { intro: "¡Bienvenido al juego!" },
522
+ },
508
523
  },
509
- es: {
510
- passages: { intro: '¡Bienvenido al juego!' }
511
- }
512
- },
513
- modules: []
524
+ modules: [],
514
525
  };
515
526
 
516
527
  await Game.init({
517
- gameName: 'My Adventure',
518
- translations,
519
- // ...other options
528
+ gameName: "My Adventure",
529
+ translations,
530
+ // ...other options
520
531
  });
521
532
  ```
522
533
 
@@ -531,23 +542,27 @@ A saved language preference is loaded from the settings store before i18next ini
531
542
  Use the `useGameTranslation` hook from `@react-text-game/core/i18n`:
532
543
 
533
544
  ```tsx
534
- import { useGameTranslation } from '@react-text-game/core/i18n';
545
+ import { useGameTranslation } from "@react-text-game/core/i18n";
535
546
 
536
547
  export function LanguageSwitcher() {
537
- const { t, languages, currentLanguage, changeLanguage } = useGameTranslation('common');
538
-
539
- return (
540
- <div>
541
- <p>{t('currentLanguage', { language: currentLanguage })}</p>
542
- <select value={currentLanguage} onChange={(event) => changeLanguage(event.target.value)}>
543
- {languages.map((lang) => (
544
- <option key={lang} value={lang}>
545
- {lang}
546
- </option>
547
- ))}
548
- </select>
549
- </div>
550
- );
548
+ const { t, languages, currentLanguage, changeLanguage } =
549
+ useGameTranslation("common");
550
+
551
+ return (
552
+ <div>
553
+ <p>{t("currentLanguage", { language: currentLanguage })}</p>
554
+ <select
555
+ value={currentLanguage}
556
+ onChange={(event) => changeLanguage(event.target.value)}
557
+ >
558
+ {languages.map((lang) => (
559
+ <option key={lang} value={lang}>
560
+ {lang}
561
+ </option>
562
+ ))}
563
+ </select>
564
+ </div>
565
+ );
551
566
  }
552
567
  ```
553
568
 
@@ -558,10 +573,10 @@ The hook filters out the `cimode` debug language unless you enable `debug` and p
558
573
  For game logic or utilities, grab a namespace-specific translator with `getGameTranslation`:
559
574
 
560
575
  ```ts
561
- import { getGameTranslation } from '@react-text-game/core/i18n';
576
+ import { getGameTranslation } from "@react-text-game/core/i18n";
562
577
 
563
- const t = getGameTranslation('passages');
564
- const intro = t('forest.description');
578
+ const t = getGameTranslation("passages");
579
+ const intro = t("forest.description");
565
580
  ```
566
581
 
567
582
  ### UI package integration
@@ -584,7 +599,7 @@ The `Game` class is the central orchestrator that manages:
584
599
  ```typescript
585
600
  // Initialize the game (REQUIRED)
586
601
  await Game.init({
587
- // your options
602
+ // your options
588
603
  });
589
604
 
590
605
  // Register entities
@@ -594,7 +609,7 @@ Game.registerEntity(player, inventory, quest);
594
609
  Game.registerPassage(intro, chapter1, finalBattle);
595
610
 
596
611
  // Navigate
597
- Game.jumpTo('chapter1');
612
+ Game.jumpTo("chapter1");
598
613
 
599
614
  // Save/Load
600
615
  const savedState = Game.getState();
@@ -619,19 +634,19 @@ wraps it in a `SimpleObject` that:
619
634
  - Requires explicit `save()` calls so you stay in control of persistence cadence
620
635
 
621
636
  ```typescript
622
- import { createEntity } from '@react-text-game/core';
623
-
624
- const player = createEntity('player', {
625
- name: 'Hero',
626
- health: 100,
627
- inventory: {
628
- gold: 50,
629
- items: [] as string[],
630
- },
637
+ import { createEntity } from "@react-text-game/core";
638
+
639
+ const player = createEntity("player", {
640
+ name: "Hero",
641
+ health: 100,
642
+ inventory: {
643
+ gold: 50,
644
+ items: [] as string[],
645
+ },
631
646
  });
632
647
 
633
648
  player.health -= 5; // direct property access
634
- player.inventory.items.push('sword');
649
+ player.inventory.items.push("sword");
635
650
  player.save(); // persist changes when you decide to
636
651
  ```
637
652
 
@@ -642,20 +657,20 @@ Prefer a class-based design, private fields, or inheritance? Extend
642
657
  available:
643
658
 
644
659
  ```typescript
645
- import { BaseGameObject } from '@react-text-game/core';
660
+ import { BaseGameObject } from "@react-text-game/core";
646
661
 
647
662
  class Inventory extends BaseGameObject<{ items: string[] }> {
648
- constructor() {
649
- super({
650
- id: 'inventory',
651
- variables: { items: [] },
652
- });
653
- }
663
+ constructor() {
664
+ super({
665
+ id: "inventory",
666
+ variables: { items: [] },
667
+ });
668
+ }
654
669
 
655
- addItem(item: string) {
656
- this._variables.items.push(item);
657
- this.save();
658
- }
670
+ addItem(item: string) {
671
+ this._variables.items.push(item);
672
+ this.save();
673
+ }
659
674
  }
660
675
  ```
661
676
 
@@ -668,69 +683,74 @@ Passages represent different screens or scenes in your game. Three types are ava
668
683
  Text-based narrative passages with rich components:
669
684
 
670
685
  ```typescript
671
- import { newStory } from '@react-text-game/core';
672
-
673
- const myStory = newStory('my-story', (props) => [
674
- {
675
- type: 'header',
676
- content: 'Chapter 1',
677
- props: { level: 1 }
678
- },
679
- {
680
- type: 'text',
681
- content: 'Once upon a time...'
682
- },
683
- {
684
- type: 'image',
685
- content: '/assets/scene.jpg',
686
- props: { alt: 'A beautiful scene' }
687
- },
688
- {
689
- type: 'video',
690
- content: '/assets/intro.mp4',
691
- props: { controls: true, autoPlay: false }
692
- },
693
- {
694
- type: 'conversation',
695
- content: [
696
- {
697
- content: 'Hello there!',
698
- who: { name: 'NPC', avatar: '/avatars/npc.png' },
699
- side: 'left'
700
- },
701
- {
702
- content: 'Hi!',
703
- who: { name: 'Player' },
704
- side: 'right'
705
- }
686
+ import { newStory } from "@react-text-game/core";
687
+
688
+ const myStory = newStory(
689
+ "my-story",
690
+ (props) => [
691
+ {
692
+ type: "header",
693
+ content: "Chapter 1",
694
+ props: { level: 1 },
695
+ },
696
+ {
697
+ type: "text",
698
+ content: "Once upon a time...",
699
+ },
700
+ {
701
+ type: "image",
702
+ content: "/assets/scene.jpg",
703
+ props: { alt: "A beautiful scene" },
704
+ },
705
+ {
706
+ type: "video",
707
+ content: "/assets/intro.mp4",
708
+ props: { controls: true, autoPlay: false },
709
+ },
710
+ {
711
+ type: "conversation",
712
+ content: [
713
+ {
714
+ content: "Hello there!",
715
+ who: { name: "NPC", avatar: "/avatars/npc.png" },
716
+ side: "left",
717
+ },
718
+ {
719
+ content: "Hi!",
720
+ who: { name: "Player" },
721
+ side: "right",
722
+ },
723
+ ],
724
+ props: { variant: "messenger" },
725
+ appearance: "atOnce",
726
+ },
727
+ {
728
+ type: "actions",
729
+ content: [
730
+ {
731
+ label: "Continue",
732
+ action: () => Game.jumpTo("chapter-2"),
733
+ color: "primary",
734
+ },
735
+ {
736
+ label: "Go Back",
737
+ action: () => Game.jumpTo("intro"),
738
+ color: "secondary",
739
+ variant: "bordered",
740
+ },
741
+ ],
742
+ props: { direction: "horizontal" },
743
+ },
706
744
  ],
707
- props: { variant: 'messenger' },
708
- appearance: 'atOnce'
709
- },
710
- {
711
- type: 'actions',
712
- content: [
713
- {
714
- label: 'Continue',
715
- action: () => Game.jumpTo('chapter-2'),
716
- color: 'primary'
717
- },
718
- {
719
- label: 'Go Back',
720
- action: () => Game.jumpTo('intro'),
721
- color: 'secondary',
722
- variant: 'bordered'
723
- }
724
- ],
725
- props: { direction: 'horizontal' }
726
- }
727
- ], {
728
- background: { image: '/bg.jpg' },
729
- classNames: { container: 'story-container' }
730
- });
745
+ {
746
+ background: { image: "/bg.jpg" },
747
+ classNames: { container: "story-container" },
748
+ }
749
+ );
731
750
  ```
732
751
 
733
752
  **Available Components:**
753
+
734
754
  - `text` - Text content with ReactNode support and custom styling
735
755
  - `header` - Semantic headers (h1-h6) with configurable levels
736
756
  - `image` - Images with built-in modal viewer and custom click handlers
@@ -744,78 +764,85 @@ const myStory = newStory('my-story', (props) => [
744
764
  Map-based interactive passages with hotspots:
745
765
 
746
766
  ```typescript
747
- import { newInteractiveMap } from '@react-text-game/core';
748
-
749
- const worldMap = newInteractiveMap('world-map', {
750
- caption: 'World Map',
751
- image: '/maps/world.jpg',
752
- bgImage: '/maps/world-bg.jpg',
753
- props: { bgOpacity: 0.3 },
754
- hotspots: [
755
- // Map label hotspot - positioned on the map
756
- {
757
- type: 'label',
758
- content: 'Village',
759
- position: { x: 30, y: 40 }, // Percentage-based (0-100)
760
- action: () => Game.jumpTo('village'),
761
- props: { color: 'primary', variant: 'solid' }
762
- },
763
- // Map image hotspot - with state-dependent images
764
- {
765
- type: 'image',
766
- content: {
767
- idle: '/icons/chest.png',
768
- hover: '/icons/chest-glow.png',
769
- active: '/icons/chest-open.png',
770
- disabled: '/icons/chest-locked.png'
771
- },
772
- position: { x: 60, y: 70 },
773
- action: () => openChest(),
774
- isDisabled: () => !player.hasKey,
775
- tooltip: {
776
- content: () => player.hasKey ? 'Open chest' : 'Locked',
777
- position: 'top'
778
- },
779
- props: { zoom: '150%' }
780
- },
781
- // Conditional hotspot - only visible if discovered
782
- () => player.hasDiscovered('forest') ? {
783
- type: 'label',
784
- content: 'Forest',
785
- position: { x: 80, y: 50 },
786
- action: () => Game.jumpTo('forest')
787
- } : undefined,
788
- // Side hotspot - positioned on edge
789
- {
790
- type: 'label',
791
- content: 'Menu',
792
- position: 'top', // top/bottom/left/right
793
- action: () => openMenu()
767
+ import { newInteractiveMap } from "@react-text-game/core";
768
+
769
+ const worldMap = newInteractiveMap("world-map", {
770
+ caption: "World Map",
771
+ image: "/maps/world.jpg",
772
+ bgImage: "/maps/world-bg.jpg",
773
+ props: { bgOpacity: 0.3 },
774
+ hotspots: [
775
+ // Map label hotspot - positioned on the map
776
+ {
777
+ type: "label",
778
+ content: "Village",
779
+ position: { x: 30, y: 40 }, // Percentage-based (0-100)
780
+ action: () => Game.jumpTo("village"),
781
+ props: { color: "primary", variant: "solid" },
782
+ },
783
+ // Map image hotspot - with state-dependent images
784
+ {
785
+ type: "image",
786
+ content: {
787
+ idle: "/icons/chest.png",
788
+ hover: "/icons/chest-glow.png",
789
+ active: "/icons/chest-open.png",
790
+ disabled: "/icons/chest-locked.png",
791
+ },
792
+ position: { x: 60, y: 70 },
793
+ action: () => openChest(),
794
+ isDisabled: () => !player.hasKey,
795
+ tooltip: {
796
+ content: () => (player.hasKey ? "Open chest" : "Locked"),
797
+ position: "top",
798
+ },
799
+ props: { zoom: "150%" },
800
+ },
801
+ // Conditional hotspot - only visible if discovered
802
+ () =>
803
+ player.hasDiscovered("forest")
804
+ ? {
805
+ type: "label",
806
+ content: "Forest",
807
+ position: { x: 80, y: 50 },
808
+ action: () => Game.jumpTo("forest"),
809
+ }
810
+ : undefined,
811
+ // Side hotspot - positioned on edge
812
+ {
813
+ type: "label",
814
+ content: "Menu",
815
+ position: "top", // top/bottom/left/right
816
+ action: () => openMenu(),
817
+ },
818
+ // Context menu - multiple choices at a location
819
+ {
820
+ type: "menu",
821
+ position: { x: 50, y: 50 },
822
+ direction: "vertical",
823
+ items: [
824
+ { type: "label", content: "Examine", action: () => examine() },
825
+ { type: "label", content: "Take", action: () => take() },
826
+ () =>
827
+ player.hasMagic
828
+ ? {
829
+ type: "label",
830
+ content: "Cast Spell",
831
+ action: () => castSpell(),
832
+ }
833
+ : undefined,
834
+ ],
835
+ },
836
+ ],
837
+ classNames: {
838
+ container: "bg-gradient-to-b from-sky-900 to-indigo-900",
839
+ topHotspots: "bg-muted/50 backdrop-blur-sm",
794
840
  },
795
- // Context menu - multiple choices at a location
796
- {
797
- type: 'menu',
798
- position: { x: 50, y: 50 },
799
- direction: 'vertical',
800
- items: [
801
- { type: 'label', content: 'Examine', action: () => examine() },
802
- { type: 'label', content: 'Take', action: () => take() },
803
- () => player.hasMagic ? {
804
- type: 'label',
805
- content: 'Cast Spell',
806
- action: () => castSpell()
807
- } : undefined
808
- ]
809
- }
810
- ],
811
- classNames: {
812
- container: 'bg-gradient-to-b from-sky-900 to-indigo-900',
813
- topHotspots: 'bg-muted/50 backdrop-blur-sm'
814
- }
815
841
  });
816
842
  ```
817
843
 
818
844
  **Hotspot Types:**
845
+
819
846
  - `MapLabelHotspot` - Text buttons positioned on map using percentage coordinates (x/y: 0-100)
820
847
  - `MapImageHotspot` - Image buttons with state variants (idle/hover/active/disabled) and zoom support
821
848
  - `SideLabelHotspot` - Text buttons on map edges (top/bottom/left/right)
@@ -823,6 +850,7 @@ const worldMap = newInteractiveMap('world-map', {
823
850
  - `MapMenu` - Contextual menu with multiple items at a specific position
824
851
 
825
852
  **Dynamic Features:**
853
+
826
854
  - Hotspots can be functions returning `undefined` for conditional visibility
827
855
  - Images and positions support dynamic functions: `image: () => '/maps/' + season + '.jpg'`
828
856
  - Disabled states with custom tooltips explaining why actions are unavailable
@@ -832,14 +860,15 @@ const worldMap = newInteractiveMap('world-map', {
832
860
  Custom React components as passages:
833
861
 
834
862
  ```tsx
835
- import { newWidget } from '@react-text-game/core';
836
-
837
- const customUI = newWidget('custom-ui', (
838
- <div>
839
- <h1>Custom Interface</h1>
840
- <MyCustomComponent />
841
- </div>
842
- ));
863
+ import { newWidget } from "@react-text-game/core";
864
+
865
+ const customUI = newWidget(
866
+ "custom-ui",
867
+ <div>
868
+ <h1>Custom Interface</h1>
869
+ <MyCustomComponent />
870
+ </div>
871
+ );
843
872
  ```
844
873
 
845
874
  ### Storage
@@ -847,13 +876,13 @@ const customUI = newWidget('custom-ui', (
847
876
  JSONPath-based storage system using the `jsonpath` library:
848
877
 
849
878
  ```typescript
850
- import { Storage } from '@react-text-game/core';
879
+ import { Storage } from "@react-text-game/core";
851
880
 
852
881
  // Get values
853
- const health = Storage.getValue<number>('$.player.health');
882
+ const health = Storage.getValue<number>("$.player.health");
854
883
 
855
884
  // Set values
856
- Storage.setValue('$.player.health', 75);
885
+ Storage.setValue("$.player.health", 75);
857
886
 
858
887
  // Full state
859
888
  const state = Storage.getState();
@@ -861,6 +890,7 @@ Storage.setState(state);
861
890
  ```
862
891
 
863
892
  **Key Features:**
893
+
864
894
  - JSONPath queries for flexible data access
865
895
  - Protected system paths (prefixed with `$._system`)
866
896
  - Automatic path creation
@@ -874,18 +904,19 @@ The engine includes a comprehensive save/load system built on IndexedDB (via Dex
874
904
 
875
905
  ```typescript
876
906
  import {
877
- useSaveSlots,
878
- useSaveGame,
879
- useLoadGame,
880
- useDeleteGame,
881
- useLastLoadGame,
882
- useExportSaves,
883
- useImportSaves,
884
- useRestartGame
885
- } from '@react-text-game/core/saves';
907
+ useSaveSlots,
908
+ useSaveGame,
909
+ useLoadGame,
910
+ useDeleteGame,
911
+ useLastLoadGame,
912
+ useExportSaves,
913
+ useImportSaves,
914
+ useRestartGame,
915
+ } from "@react-text-game/core/saves";
886
916
  ```
887
917
 
888
918
  **Features:**
919
+
889
920
  - **Persistent Storage** - IndexedDB for browser-based saves
890
921
  - **Multiple Save Slots** - Unlimited save slots with metadata
891
922
  - **Export/Import** - Encrypted file export/import (`.sx` format)
@@ -899,20 +930,26 @@ import {
899
930
 
900
931
  ```tsx
901
932
  function SavesList() {
902
- const slots = useSaveSlots({ count: 5 });
903
-
904
- return (
905
- <div>
906
- {slots.map((slot, index) => (
907
- <div key={index}>
908
- <p>Slot {index}: {slot.data ? 'Saved' : 'Empty'}</p>
909
- <button onClick={() => slot.save()}>Save</button>
910
- <button onClick={() => slot.load()} disabled={!slot.data}>Load</button>
911
- <button onClick={() => slot.delete()} disabled={!slot.data}>Delete</button>
933
+ const slots = useSaveSlots({ count: 5 });
934
+
935
+ return (
936
+ <div>
937
+ {slots.map((slot, index) => (
938
+ <div key={index}>
939
+ <p>
940
+ Slot {index}: {slot.data ? "Saved" : "Empty"}
941
+ </p>
942
+ <button onClick={() => slot.save()}>Save</button>
943
+ <button onClick={() => slot.load()} disabled={!slot.data}>
944
+ Load
945
+ </button>
946
+ <button onClick={() => slot.delete()} disabled={!slot.data}>
947
+ Delete
948
+ </button>
949
+ </div>
950
+ ))}
912
951
  </div>
913
- ))}
914
- </div>
915
- );
952
+ );
916
953
  }
917
954
  ```
918
955
 
@@ -920,20 +957,16 @@ function SavesList() {
920
957
 
921
958
  ```tsx
922
959
  function SaveButton({ slotNumber }) {
923
- const saveGame = useSaveGame();
960
+ const saveGame = useSaveGame();
924
961
 
925
- const handleSave = async () => {
926
- const result = await saveGame(slotNumber);
927
- if (result?.success === false) {
928
- alert(result.message);
929
- }
930
- };
962
+ const handleSave = async () => {
963
+ const result = await saveGame(slotNumber);
964
+ if (result?.success === false) {
965
+ alert(result.message);
966
+ }
967
+ };
931
968
 
932
- return (
933
- <button onClick={handleSave}>
934
- Save to Slot {slotNumber}
935
- </button>
936
- );
969
+ return <button onClick={handleSave}>Save to Slot {slotNumber}</button>;
937
970
  }
938
971
  ```
939
972
 
@@ -941,16 +974,16 @@ function SaveButton({ slotNumber }) {
941
974
 
942
975
  ```tsx
943
976
  function LoadButton({ saveId }) {
944
- const loadGame = useLoadGame();
977
+ const loadGame = useLoadGame();
945
978
 
946
- const handleLoad = async () => {
947
- const result = await loadGame(saveId);
948
- if (result?.success === false) {
949
- alert(result.message);
950
- }
951
- };
979
+ const handleLoad = async () => {
980
+ const result = await loadGame(saveId);
981
+ if (result?.success === false) {
982
+ alert(result.message);
983
+ }
984
+ };
952
985
 
953
- return <button onClick={handleLoad}>Load Game</button>;
986
+ return <button onClick={handleLoad}>Load Game</button>;
954
987
  }
955
988
  ```
956
989
 
@@ -958,16 +991,16 @@ function LoadButton({ saveId }) {
958
991
 
959
992
  ```tsx
960
993
  function DeleteButton({ saveId }) {
961
- const deleteGame = useDeleteGame();
994
+ const deleteGame = useDeleteGame();
962
995
 
963
- const handleDelete = async () => {
964
- const result = await deleteGame(saveId);
965
- if (result?.success === false) {
966
- alert(result.message);
967
- }
968
- };
996
+ const handleDelete = async () => {
997
+ const result = await deleteGame(saveId);
998
+ if (result?.success === false) {
999
+ alert(result.message);
1000
+ }
1001
+ };
969
1002
 
970
- return <button onClick={handleDelete}>Delete Save</button>;
1003
+ return <button onClick={handleDelete}>Delete Save</button>;
971
1004
  }
972
1005
  ```
973
1006
 
@@ -975,17 +1008,17 @@ function DeleteButton({ saveId }) {
975
1008
 
976
1009
  ```tsx
977
1010
  function ContinueButton() {
978
- const { hasLastSave, loadLastGame, isLoading } = useLastLoadGame();
1011
+ const { hasLastSave, loadLastGame, isLoading } = useLastLoadGame();
979
1012
 
980
- if (isLoading) {
981
- return <div>Loading...</div>;
982
- }
1013
+ if (isLoading) {
1014
+ return <div>Loading...</div>;
1015
+ }
983
1016
 
984
- return (
985
- <button onClick={loadLastGame} disabled={!hasLastSave}>
986
- Continue Last Game
987
- </button>
988
- );
1017
+ return (
1018
+ <button onClick={loadLastGame} disabled={!hasLastSave}>
1019
+ Continue Last Game
1020
+ </button>
1021
+ );
989
1022
  }
990
1023
  ```
991
1024
 
@@ -993,18 +1026,18 @@ function ContinueButton() {
993
1026
 
994
1027
  ```tsx
995
1028
  function ExportButton() {
996
- const exportSaves = useExportSaves();
997
-
998
- const handleExport = async () => {
999
- const result = await exportSaves();
1000
- if (result.success) {
1001
- console.log('Saves exported successfully');
1002
- } else {
1003
- alert(`Export failed: ${result.error}`);
1004
- }
1005
- };
1006
-
1007
- return <button onClick={handleExport}>Export Saves</button>;
1029
+ const exportSaves = useExportSaves();
1030
+
1031
+ const handleExport = async () => {
1032
+ const result = await exportSaves();
1033
+ if (result.success) {
1034
+ console.log("Saves exported successfully");
1035
+ } else {
1036
+ alert(`Export failed: ${result.error}`);
1037
+ }
1038
+ };
1039
+
1040
+ return <button onClick={handleExport}>Export Saves</button>;
1008
1041
  }
1009
1042
  ```
1010
1043
 
@@ -1012,18 +1045,18 @@ function ExportButton() {
1012
1045
 
1013
1046
  ```tsx
1014
1047
  function ImportButton() {
1015
- const importSaves = useImportSaves();
1016
-
1017
- const handleImport = async () => {
1018
- const result = await importSaves();
1019
- if (result.success) {
1020
- console.log(`Imported ${result.count} saves`);
1021
- } else {
1022
- alert(`Import failed: ${result.error}`);
1023
- }
1024
- };
1025
-
1026
- return <button onClick={handleImport}>Import Saves</button>;
1048
+ const importSaves = useImportSaves();
1049
+
1050
+ const handleImport = async () => {
1051
+ const result = await importSaves();
1052
+ if (result.success) {
1053
+ console.log(`Imported ${result.count} saves`);
1054
+ } else {
1055
+ alert(`Import failed: ${result.error}`);
1056
+ }
1057
+ };
1058
+
1059
+ return <button onClick={handleImport}>Import Saves</button>;
1027
1060
  }
1028
1061
  ```
1029
1062
 
@@ -1031,13 +1064,9 @@ function ImportButton() {
1031
1064
 
1032
1065
  ```tsx
1033
1066
  function RestartButton() {
1034
- const restartGame = useRestartGame();
1067
+ const restartGame = useRestartGame();
1035
1068
 
1036
- return (
1037
- <button onClick={restartGame}>
1038
- Restart Game
1039
- </button>
1040
- );
1069
+ return <button onClick={restartGame}>Restart Game</button>;
1041
1070
  }
1042
1071
  ```
1043
1072
 
@@ -1047,15 +1076,15 @@ For advanced use cases, you can access the database directly:
1047
1076
 
1048
1077
  ```typescript
1049
1078
  import {
1050
- saveGame,
1051
- loadGame,
1052
- getAllSaves,
1053
- deleteSave,
1054
- db
1055
- } from '@react-text-game/core/saves';
1079
+ saveGame,
1080
+ loadGame,
1081
+ getAllSaves,
1082
+ deleteSave,
1083
+ db,
1084
+ } from "@react-text-game/core/saves";
1056
1085
 
1057
1086
  // Save game manually
1058
- await saveGame('my-save', gameData, 'Description', screenshotBase64);
1087
+ await saveGame("my-save", gameData, "Description", screenshotBase64);
1059
1088
 
1060
1089
  // Load by ID
1061
1090
  const save = await loadGame(1);
@@ -1067,12 +1096,13 @@ const allSaves = await getAllSaves();
1067
1096
  await deleteSave(1);
1068
1097
 
1069
1098
  // Direct Dexie access
1070
- await db.saves.where('name').equals('my-save').first();
1099
+ await db.saves.where("name").equals("my-save").first();
1071
1100
  ```
1072
1101
 
1073
1102
  #### Save File Encryption
1074
1103
 
1075
1104
  Exported save files are encrypted using AES encryption with PBKDF2 key derivation:
1105
+
1076
1106
  - **Algorithm**: AES-256-CBC
1077
1107
  - **Key Derivation**: PBKDF2 with 1000 iterations
1078
1108
  - **Salt & IV**: Randomly generated for each export
@@ -1082,21 +1112,39 @@ Exported save files are encrypted using AES encryption with PBKDF2 key derivatio
1082
1112
 
1083
1113
  #### useCurrentPassage
1084
1114
 
1085
- Get the current passage with reactive updates:
1115
+ Get the current passage with reactive updates. Returns a tuple containing the current passage and a unique render ID that changes on each navigation:
1086
1116
 
1087
1117
  ```tsx
1088
- import { useCurrentPassage } from '@react-text-game/core';
1118
+ import { useCurrentPassage } from "@react-text-game/core";
1089
1119
 
1090
1120
  function GameScreen() {
1091
- const passage = useCurrentPassage();
1121
+ const [passage, renderId] = useCurrentPassage();
1092
1122
 
1093
- if (!passage) return <div>Loading...</div>;
1123
+ if (!passage) return <div>Loading...</div>;
1094
1124
 
1095
- // Render based on passage type
1096
- if (passage.type === 'story') {
1097
- const { components } = passage.display();
1098
- // Render story components
1099
- }
1125
+ // Render based on passage type
1126
+ if (passage.type === "story") {
1127
+ const { components } = passage.display();
1128
+ // Render story components
1129
+ }
1130
+ }
1131
+ ```
1132
+
1133
+ **Render ID Purpose:**
1134
+ The `renderId` is automatically generated on each `Game.jumpTo()` call, ensuring that React components re-render even when navigating to the same passage multiple times. This is useful for forcing component remounts, resetting animations, or clearing component state.
1135
+
1136
+ **Usage Example:**
1137
+
1138
+ ```tsx
1139
+ function PassageRenderer() {
1140
+ const [passage, renderId] = useCurrentPassage();
1141
+
1142
+ // Use renderId as a React key to force remount on navigation
1143
+ return (
1144
+ <div key={renderId} className="animate-fade-in">
1145
+ {passage && <PassageContent passage={passage} />}
1146
+ </div>
1147
+ );
1100
1148
  }
1101
1149
  ```
1102
1150
 
@@ -1105,17 +1153,17 @@ function GameScreen() {
1105
1153
  Monitor entity changes with automatic re-renders:
1106
1154
 
1107
1155
  ```tsx
1108
- import { useGameEntity } from '@react-text-game/core';
1156
+ import { useGameEntity } from "@react-text-game/core";
1109
1157
 
1110
1158
  function PlayerStats({ player }) {
1111
- const reactivePlayer = useGameEntity(player);
1159
+ const reactivePlayer = useGameEntity(player);
1112
1160
 
1113
- return (
1114
- <div>
1115
- Health: {reactivePlayer.health}
1116
- {/* Direct property access stays reactive */}
1117
- </div>
1118
- );
1161
+ return (
1162
+ <div>
1163
+ Health: {reactivePlayer.health}
1164
+ {/* Direct property access stays reactive */}
1165
+ </div>
1166
+ );
1119
1167
  }
1120
1168
  ```
1121
1169
 
@@ -1124,12 +1172,12 @@ function PlayerStats({ player }) {
1124
1172
  Check if game has started:
1125
1173
 
1126
1174
  ```tsx
1127
- import { useGameIsStarted } from '@react-text-game/core';
1175
+ import { useGameIsStarted } from "@react-text-game/core";
1128
1176
 
1129
1177
  function GameUI() {
1130
- const isStarted = useGameIsStarted();
1178
+ const isStarted = useGameIsStarted();
1131
1179
 
1132
- return isStarted ? <GameScreen /> : <MainMenu />;
1180
+ return isStarted ? <GameScreen /> : <MainMenu />;
1133
1181
  }
1134
1182
  ```
1135
1183
 
@@ -1147,6 +1195,7 @@ function GameUI() {
1147
1195
  ### Registry Pattern
1148
1196
 
1149
1197
  The engine uses two registries:
1198
+
1150
1199
  - `objectRegistry` - Stores all game entities as Valtio proxies
1151
1200
  - `passagesRegistry` - Stores all passages
1152
1201
 
@@ -1155,6 +1204,7 @@ All objects are automatically wrapped in Valtio proxies for reactive state manag
1155
1204
  ### Save System
1156
1205
 
1157
1206
  The save system consists of:
1207
+
1158
1208
  - **Entity State** - Each entity's `_variables` stored at `$.{entityId}`
1159
1209
  - **Game State** - Current passage stored at `$._system.game`
1160
1210
  - **JSONPath Access** - Flexible queries for any state data
@@ -1165,6 +1215,7 @@ The save system consists of:
1165
1215
  ### Game
1166
1216
 
1167
1217
  Static methods:
1218
+
1168
1219
  - `init(options)` - **Initialize the game (REQUIRED - must be called first)**
1169
1220
  - `registerEntity(...objects)` - Register game objects
1170
1221
  - `registerPassage(...passages)` - Register passages
@@ -1180,6 +1231,7 @@ Static methods:
1180
1231
  - `clearAutoSave()` - Clear auto-saved state
1181
1232
 
1182
1233
  Properties:
1234
+
1183
1235
  - `currentPassage` - Get current passage
1184
1236
  - `selfState` - Get game internal state
1185
1237
  - `options` - Get game options
@@ -1187,30 +1239,36 @@ Properties:
1187
1239
  ### BaseGameObject
1188
1240
 
1189
1241
  Constructor:
1242
+
1190
1243
  - `new BaseGameObject({ id, variables? })`
1191
1244
 
1192
1245
  Properties:
1246
+
1193
1247
  - `id` - Unique identifier
1194
1248
  - `variables` - Entity variables (readonly)
1195
1249
  - `_variables` - Internal variables (protected)
1196
1250
 
1197
1251
  Methods:
1252
+
1198
1253
  - `save()` - Save to storage
1199
1254
  - `load()` - Load from storage
1200
1255
 
1201
1256
  ### Passage Types
1202
1257
 
1203
1258
  **Story:**
1259
+
1204
1260
  ```typescript
1205
1261
  newStory(id: string, content: StoryContent, options?: StoryOptions): Story
1206
1262
  ```
1207
1263
 
1208
1264
  **Interactive Map:**
1265
+
1209
1266
  ```typescript
1210
1267
  newInteractiveMap(id: string, options: InteractiveMapOptions): InteractiveMap
1211
1268
  ```
1212
1269
 
1213
1270
  **Widget:**
1271
+
1214
1272
  ```typescript
1215
1273
  newWidget(id: string, content: ReactNode): Widget
1216
1274
  ```
@@ -1222,48 +1280,49 @@ Full TypeScript support with comprehensive types and detailed JSDoc documentatio
1222
1280
  ```typescript
1223
1281
  // Import types from main package
1224
1282
  import type {
1225
- GameSaveState,
1226
- JsonPath,
1227
- InitVarsType,
1228
- PassageType,
1229
- ButtonColor,
1230
- ButtonVariant
1231
- } from '@react-text-game/core';
1283
+ GameSaveState,
1284
+ JsonPath,
1285
+ InitVarsType,
1286
+ PassageType,
1287
+ ButtonColor,
1288
+ ButtonVariant,
1289
+ } from "@react-text-game/core";
1232
1290
 
1233
1291
  // Import story passage types
1234
1292
  import type {
1235
- Component,
1236
- StoryContent,
1237
- StoryOptions,
1238
- TextComponent,
1239
- HeaderComponent,
1240
- ImageComponent,
1241
- VideoComponent,
1242
- ActionsComponent,
1243
- ConversationComponent,
1244
- AnotherStoryComponent,
1245
- ActionType,
1246
- ConversationBubble,
1247
- ConversationVariant,
1248
- ConversationAppearance
1249
- } from '@react-text-game/core/passages';
1293
+ Component,
1294
+ StoryContent,
1295
+ StoryOptions,
1296
+ TextComponent,
1297
+ HeaderComponent,
1298
+ ImageComponent,
1299
+ VideoComponent,
1300
+ ActionsComponent,
1301
+ ConversationComponent,
1302
+ AnotherStoryComponent,
1303
+ ActionType,
1304
+ ConversationBubble,
1305
+ ConversationVariant,
1306
+ ConversationAppearance,
1307
+ } from "@react-text-game/core/passages";
1250
1308
 
1251
1309
  // Import interactive map types
1252
1310
  import type {
1253
- InteractiveMapOptions,
1254
- InteractiveMapType,
1255
- AnyHotspot,
1256
- MapLabelHotspot,
1257
- MapImageHotspot,
1258
- SideLabelHotspot,
1259
- SideImageHotspot,
1260
- MapMenu,
1261
- LabelHotspot,
1262
- ImageHotspot
1263
- } from '@react-text-game/core/passages';
1311
+ InteractiveMapOptions,
1312
+ InteractiveMapType,
1313
+ AnyHotspot,
1314
+ MapLabelHotspot,
1315
+ MapImageHotspot,
1316
+ SideLabelHotspot,
1317
+ SideImageHotspot,
1318
+ MapMenu,
1319
+ LabelHotspot,
1320
+ ImageHotspot,
1321
+ } from "@react-text-game/core/passages";
1264
1322
  ```
1265
1323
 
1266
1324
  All types include comprehensive JSDoc comments with:
1325
+
1267
1326
  - Detailed descriptions of each property
1268
1327
  - Usage examples and code snippets
1269
1328
  - Default value annotations