@react-text-game/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +744 -0
- package/dist/baseGameObject.d.ts +90 -0
- package/dist/baseGameObject.d.ts.map +1 -0
- package/dist/baseGameObject.js +109 -0
- package/dist/baseGameObject.js.map +1 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +12 -0
- package/dist/constants.js.map +1 -0
- package/dist/game.d.ts +294 -0
- package/dist/game.d.ts.map +1 -0
- package/dist/game.js +489 -0
- package/dist/game.js.map +1 -0
- package/dist/helpers.d.ts +2 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +6 -0
- package/dist/helpers.js.map +1 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useCurrentPassage.d.ts +10 -0
- package/dist/hooks/useCurrentPassage.d.ts.map +1 -0
- package/dist/hooks/useCurrentPassage.js +17 -0
- package/dist/hooks/useCurrentPassage.js.map +1 -0
- package/dist/hooks/useGameEntity.d.ts +21 -0
- package/dist/hooks/useGameEntity.d.ts.map +1 -0
- package/dist/hooks/useGameEntity.js +70 -0
- package/dist/hooks/useGameEntity.js.map +1 -0
- package/dist/hooks/useGameIsStarted.d.ts +12 -0
- package/dist/hooks/useGameIsStarted.d.ts.map +1 -0
- package/dist/hooks/useGameIsStarted.js +18 -0
- package/dist/hooks/useGameIsStarted.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +36 -0
- package/dist/logger.js.map +1 -0
- package/dist/options.d.ts +13 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +15 -0
- package/dist/options.js.map +1 -0
- package/dist/passages/interactiveMap/fabric.d.ts +4 -0
- package/dist/passages/interactiveMap/fabric.d.ts.map +1 -0
- package/dist/passages/interactiveMap/fabric.js +3 -0
- package/dist/passages/interactiveMap/fabric.js.map +1 -0
- package/dist/passages/interactiveMap/index.d.ts +4 -0
- package/dist/passages/interactiveMap/index.d.ts.map +1 -0
- package/dist/passages/interactiveMap/index.js +4 -0
- package/dist/passages/interactiveMap/index.js.map +1 -0
- package/dist/passages/interactiveMap/interactiveMap.d.ts +89 -0
- package/dist/passages/interactiveMap/interactiveMap.d.ts.map +1 -0
- package/dist/passages/interactiveMap/interactiveMap.js +103 -0
- package/dist/passages/interactiveMap/interactiveMap.js.map +1 -0
- package/dist/passages/interactiveMap/types.d.ts +822 -0
- package/dist/passages/interactiveMap/types.d.ts.map +1 -0
- package/dist/passages/interactiveMap/types.js +2 -0
- package/dist/passages/interactiveMap/types.js.map +1 -0
- package/dist/passages/passage.d.ts +57 -0
- package/dist/passages/passage.d.ts.map +1 -0
- package/dist/passages/passage.js +64 -0
- package/dist/passages/passage.js.map +1 -0
- package/dist/passages/story/fabric.d.ts +4 -0
- package/dist/passages/story/fabric.d.ts.map +1 -0
- package/dist/passages/story/fabric.js +3 -0
- package/dist/passages/story/fabric.js.map +1 -0
- package/dist/passages/story/index.d.ts +5 -0
- package/dist/passages/story/index.d.ts.map +1 -0
- package/dist/passages/story/index.js +5 -0
- package/dist/passages/story/index.js.map +1 -0
- package/dist/passages/story/start.d.ts +14 -0
- package/dist/passages/story/start.d.ts.map +1 -0
- package/dist/passages/story/start.js +22 -0
- package/dist/passages/story/start.js.map +1 -0
- package/dist/passages/story/story.d.ts +84 -0
- package/dist/passages/story/story.d.ts.map +1 -0
- package/dist/passages/story/story.js +88 -0
- package/dist/passages/story/story.js.map +1 -0
- package/dist/passages/story/types.d.ts +911 -0
- package/dist/passages/story/types.d.ts.map +1 -0
- package/dist/passages/story/types.js +2 -0
- package/dist/passages/story/types.js.map +1 -0
- package/dist/passages/types/index.d.ts +3 -0
- package/dist/passages/types/index.d.ts.map +1 -0
- package/dist/passages/types/index.js +2 -0
- package/dist/passages/types/index.js.map +1 -0
- package/dist/passages/widget.d.ts +62 -0
- package/dist/passages/widget.d.ts.map +1 -0
- package/dist/passages/widget.js +66 -0
- package/dist/passages/widget.js.map +1 -0
- package/dist/saves/constants.d.ts +17 -0
- package/dist/saves/constants.d.ts.map +1 -0
- package/dist/saves/constants.js +17 -0
- package/dist/saves/constants.js.map +1 -0
- package/dist/saves/db.d.ts +119 -0
- package/dist/saves/db.d.ts.map +1 -0
- package/dist/saves/db.js +231 -0
- package/dist/saves/db.js.map +1 -0
- package/dist/saves/helpers.d.ts +28 -0
- package/dist/saves/helpers.d.ts.map +1 -0
- package/dist/saves/helpers.js +84 -0
- package/dist/saves/helpers.js.map +1 -0
- package/dist/saves/hooks/index.d.ts +10 -0
- package/dist/saves/hooks/index.d.ts.map +1 -0
- package/dist/saves/hooks/index.js +10 -0
- package/dist/saves/hooks/index.js.map +1 -0
- package/dist/saves/hooks/useDeleteAllSlots.d.ts +18 -0
- package/dist/saves/hooks/useDeleteAllSlots.d.ts.map +1 -0
- package/dist/saves/hooks/useDeleteAllSlots.js +18 -0
- package/dist/saves/hooks/useDeleteAllSlots.js.map +1 -0
- package/dist/saves/hooks/useDeleteGame.d.ts +22 -0
- package/dist/saves/hooks/useDeleteGame.d.ts.map +1 -0
- package/dist/saves/hooks/useDeleteGame.js +33 -0
- package/dist/saves/hooks/useDeleteGame.js.map +1 -0
- package/dist/saves/hooks/useExportSaves.d.ts +27 -0
- package/dist/saves/hooks/useExportSaves.d.ts.map +1 -0
- package/dist/saves/hooks/useExportSaves.js +54 -0
- package/dist/saves/hooks/useExportSaves.js.map +1 -0
- package/dist/saves/hooks/useImportSaves.d.ts +29 -0
- package/dist/saves/hooks/useImportSaves.d.ts.map +1 -0
- package/dist/saves/hooks/useImportSaves.js +108 -0
- package/dist/saves/hooks/useImportSaves.js.map +1 -0
- package/dist/saves/hooks/useLastLoadGame.d.ts +39 -0
- package/dist/saves/hooks/useLastLoadGame.d.ts.map +1 -0
- package/dist/saves/hooks/useLastLoadGame.js +72 -0
- package/dist/saves/hooks/useLastLoadGame.js.map +1 -0
- package/dist/saves/hooks/useLoadGame.d.ts +22 -0
- package/dist/saves/hooks/useLoadGame.d.ts.map +1 -0
- package/dist/saves/hooks/useLoadGame.js +40 -0
- package/dist/saves/hooks/useLoadGame.js.map +1 -0
- package/dist/saves/hooks/useRestartGame.d.ts +20 -0
- package/dist/saves/hooks/useRestartGame.d.ts.map +1 -0
- package/dist/saves/hooks/useRestartGame.js +29 -0
- package/dist/saves/hooks/useRestartGame.js.map +1 -0
- package/dist/saves/hooks/useSaveGame.d.ts +22 -0
- package/dist/saves/hooks/useSaveGame.d.ts.map +1 -0
- package/dist/saves/hooks/useSaveGame.js +34 -0
- package/dist/saves/hooks/useSaveGame.js.map +1 -0
- package/dist/saves/hooks/useSaveSlots.d.ts +45 -0
- package/dist/saves/hooks/useSaveSlots.d.ts.map +1 -0
- package/dist/saves/hooks/useSaveSlots.js +42 -0
- package/dist/saves/hooks/useSaveSlots.js.map +1 -0
- package/dist/saves/index.d.ts +4 -0
- package/dist/saves/index.d.ts.map +1 -0
- package/dist/saves/index.js +3 -0
- package/dist/saves/index.js.map +1 -0
- package/dist/saves/types.d.ts +52 -0
- package/dist/saves/types.d.ts.map +1 -0
- package/dist/saves/types.js +2 -0
- package/dist/saves/types.js.map +1 -0
- package/dist/storage.d.ts +124 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +229 -0
- package/dist/storage.js.map +1 -0
- package/dist/tests/game.test.d.ts +2 -0
- package/dist/tests/game.test.d.ts.map +1 -0
- package/dist/tests/game.test.js +602 -0
- package/dist/tests/game.test.js.map +1 -0
- package/dist/tests/interactiveMap.test.d.ts +2 -0
- package/dist/tests/interactiveMap.test.d.ts.map +1 -0
- package/dist/tests/interactiveMap.test.js +1003 -0
- package/dist/tests/interactiveMap.test.js.map +1 -0
- package/dist/tests/storage.test.d.ts +2 -0
- package/dist/tests/storage.test.d.ts.map +1 -0
- package/dist/tests/storage.test.js +328 -0
- package/dist/tests/storage.test.js.map +1 -0
- package/dist/tests/story.test.d.ts +2 -0
- package/dist/tests/story.test.d.ts.map +1 -0
- package/dist/tests/story.test.js +698 -0
- package/dist/tests/story.test.js.map +1 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
# @react-text-game/core
|
|
2
|
+
|
|
3
|
+
A powerful, reactive text-based game engine built for React applications. This package provides a comprehensive framework for creating interactive narrative experiences with support for story passages, interactive maps, and state management.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Reactive State Management** - Built on Valtio for automatic UI updates
|
|
8
|
+
- **Multiple Passage Types** - Story, Interactive Map, and Widget passages
|
|
9
|
+
- **Flexible Save System** - JSONPath-based storage with auto-save support
|
|
10
|
+
- **Entity Registry** - Automatic registration and proxying of game objects
|
|
11
|
+
- **Type-Safe** - Full TypeScript support with comprehensive types
|
|
12
|
+
- **React Hooks** - Built-in hooks for seamless React integration
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun add @react-text-game/core
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { Game, BaseGameObject, newStory } from '@react-text-game/core';
|
|
24
|
+
|
|
25
|
+
// IMPORTANT: Initialize the game first
|
|
26
|
+
await Game.init({
|
|
27
|
+
// your game options
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Create a game entity
|
|
31
|
+
class Player extends BaseGameObject<{ health: number; name: string }> {
|
|
32
|
+
constructor() {
|
|
33
|
+
super({
|
|
34
|
+
id: 'player',
|
|
35
|
+
variables: { health: 100, name: 'Hero' }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const player = new Player();
|
|
41
|
+
|
|
42
|
+
// Create a story passage
|
|
43
|
+
const introStory = newStory('intro', () => [
|
|
44
|
+
{
|
|
45
|
+
type: 'header',
|
|
46
|
+
content: 'Welcome to the Game',
|
|
47
|
+
props: { level: 1 }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'text',
|
|
51
|
+
content: `Hello, ${player.variables.name}!`
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'actions',
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
label: 'Start Adventure',
|
|
58
|
+
action: () => Game.jumpTo('adventure')
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
// Navigate to passage
|
|
65
|
+
Game.jumpTo(introStory);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Core Concepts
|
|
69
|
+
|
|
70
|
+
### Game
|
|
71
|
+
|
|
72
|
+
The `Game` class is the central orchestrator that manages:
|
|
73
|
+
|
|
74
|
+
- **Initialization** - **MUST call `Game.init(options)` before using any other methods**
|
|
75
|
+
- **Entity Registry** - All game objects (entities) are registered and proxied
|
|
76
|
+
- **Passage Registry** - All passages (screens/scenes) are registered
|
|
77
|
+
- **Navigation** - `jumpTo()` and `setCurrent()` for passage navigation
|
|
78
|
+
- **State Management** - `getState()` / `setState()` for full serialization
|
|
79
|
+
- **Auto-Save** - Optional auto-save to session storage with debouncing
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Initialize the game (REQUIRED)
|
|
83
|
+
await Game.init({
|
|
84
|
+
// your options
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Register entities
|
|
88
|
+
Game.registerEntity(player, inventory, quest);
|
|
89
|
+
|
|
90
|
+
// Register passages
|
|
91
|
+
Game.registerPassage(intro, chapter1, finalBattle);
|
|
92
|
+
|
|
93
|
+
// Navigate
|
|
94
|
+
Game.jumpTo('chapter1');
|
|
95
|
+
|
|
96
|
+
// Save/Load
|
|
97
|
+
const savedState = Game.getState();
|
|
98
|
+
Game.setState(savedState);
|
|
99
|
+
|
|
100
|
+
// Auto-save
|
|
101
|
+
Game.enableAutoSave();
|
|
102
|
+
Game.loadFromSessionStorage();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### BaseGameObject
|
|
106
|
+
|
|
107
|
+
Base class for all game entities. Provides:
|
|
108
|
+
|
|
109
|
+
- **Auto-registration** - Entities register with `Game` on construction
|
|
110
|
+
- **Reactive Variables** - `_variables` object for state storage
|
|
111
|
+
- **Persistence** - `save()` / `load()` methods sync with `Storage`
|
|
112
|
+
- **JSONPath Integration** - Each entity has a unique storage path
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
class Inventory extends BaseGameObject<{ items: string[] }> {
|
|
116
|
+
constructor() {
|
|
117
|
+
super({
|
|
118
|
+
id: 'inventory',
|
|
119
|
+
variables: { items: [] }
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
addItem(item: string) {
|
|
124
|
+
this._variables.items.push(item);
|
|
125
|
+
this.save(); // Persist to storage
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Passages
|
|
131
|
+
|
|
132
|
+
Passages represent different screens or scenes in your game. Three types are available:
|
|
133
|
+
|
|
134
|
+
#### Story Passages
|
|
135
|
+
|
|
136
|
+
Text-based narrative passages with rich components:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { newStory } from '@react-text-game/core';
|
|
140
|
+
|
|
141
|
+
const myStory = newStory('my-story', (props) => [
|
|
142
|
+
{
|
|
143
|
+
type: 'header',
|
|
144
|
+
content: 'Chapter 1',
|
|
145
|
+
props: { level: 1 }
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: 'text',
|
|
149
|
+
content: 'Once upon a time...'
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'image',
|
|
153
|
+
content: '/assets/scene.jpg',
|
|
154
|
+
props: { alt: 'A beautiful scene' }
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: 'video',
|
|
158
|
+
content: '/assets/intro.mp4',
|
|
159
|
+
props: { controls: true, autoPlay: false }
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: 'conversation',
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
content: 'Hello there!',
|
|
166
|
+
who: { name: 'NPC', avatar: '/avatars/npc.png' },
|
|
167
|
+
side: 'left'
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
content: 'Hi!',
|
|
171
|
+
who: { name: 'Player' },
|
|
172
|
+
side: 'right'
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
props: { variant: 'messenger' },
|
|
176
|
+
appearance: 'atOnce'
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: 'actions',
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
label: 'Continue',
|
|
183
|
+
action: () => Game.jumpTo('chapter-2'),
|
|
184
|
+
color: 'primary'
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
label: 'Go Back',
|
|
188
|
+
action: () => Game.jumpTo('intro'),
|
|
189
|
+
color: 'secondary',
|
|
190
|
+
variant: 'bordered'
|
|
191
|
+
}
|
|
192
|
+
],
|
|
193
|
+
props: { direction: 'horizontal' }
|
|
194
|
+
}
|
|
195
|
+
], {
|
|
196
|
+
background: { image: '/bg.jpg' },
|
|
197
|
+
classNames: { container: 'story-container' }
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Available Components:**
|
|
202
|
+
- `text` - Text content with ReactNode support and custom styling
|
|
203
|
+
- `header` - Semantic headers (h1-h6) with configurable levels
|
|
204
|
+
- `image` - Images with built-in modal viewer and custom click handlers
|
|
205
|
+
- `video` - HTML5 video with autoplay, loop, mute, and controls options
|
|
206
|
+
- `actions` - Interactive button groups with tooltips and disabled states
|
|
207
|
+
- `conversation` - Dialogue with chat/messenger variants and progressive reveal (byClick/atOnce)
|
|
208
|
+
- `anotherStory` - Embed other story passages for composition and reuse
|
|
209
|
+
|
|
210
|
+
#### Interactive Map Passages
|
|
211
|
+
|
|
212
|
+
Map-based interactive passages with hotspots:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { newInteractiveMap } from '@react-text-game/core';
|
|
216
|
+
|
|
217
|
+
const worldMap = newInteractiveMap('world-map', {
|
|
218
|
+
caption: 'World Map',
|
|
219
|
+
image: '/maps/world.jpg',
|
|
220
|
+
bgImage: '/maps/world-bg.jpg',
|
|
221
|
+
props: { bgOpacity: 0.3 },
|
|
222
|
+
hotspots: [
|
|
223
|
+
// Map label hotspot - positioned on the map
|
|
224
|
+
{
|
|
225
|
+
type: 'label',
|
|
226
|
+
content: 'Village',
|
|
227
|
+
position: { x: 30, y: 40 }, // Percentage-based (0-100)
|
|
228
|
+
action: () => Game.jumpTo('village'),
|
|
229
|
+
props: { color: 'primary', variant: 'solid' }
|
|
230
|
+
},
|
|
231
|
+
// Map image hotspot - with state-dependent images
|
|
232
|
+
{
|
|
233
|
+
type: 'image',
|
|
234
|
+
content: {
|
|
235
|
+
idle: '/icons/chest.png',
|
|
236
|
+
hover: '/icons/chest-glow.png',
|
|
237
|
+
active: '/icons/chest-open.png',
|
|
238
|
+
disabled: '/icons/chest-locked.png'
|
|
239
|
+
},
|
|
240
|
+
position: { x: 60, y: 70 },
|
|
241
|
+
action: () => openChest(),
|
|
242
|
+
isDisabled: () => !player.hasKey,
|
|
243
|
+
tooltip: {
|
|
244
|
+
content: () => player.hasKey ? 'Open chest' : 'Locked',
|
|
245
|
+
position: 'top'
|
|
246
|
+
},
|
|
247
|
+
props: { zoom: '150%' }
|
|
248
|
+
},
|
|
249
|
+
// Conditional hotspot - only visible if discovered
|
|
250
|
+
() => player.hasDiscovered('forest') ? {
|
|
251
|
+
type: 'label',
|
|
252
|
+
content: 'Forest',
|
|
253
|
+
position: { x: 80, y: 50 },
|
|
254
|
+
action: () => Game.jumpTo('forest')
|
|
255
|
+
} : undefined,
|
|
256
|
+
// Side hotspot - positioned on edge
|
|
257
|
+
{
|
|
258
|
+
type: 'label',
|
|
259
|
+
content: 'Menu',
|
|
260
|
+
position: 'top', // top/bottom/left/right
|
|
261
|
+
action: () => openMenu()
|
|
262
|
+
},
|
|
263
|
+
// Context menu - multiple choices at a location
|
|
264
|
+
{
|
|
265
|
+
type: 'menu',
|
|
266
|
+
position: { x: 50, y: 50 },
|
|
267
|
+
direction: 'vertical',
|
|
268
|
+
items: [
|
|
269
|
+
{ type: 'label', content: 'Examine', action: () => examine() },
|
|
270
|
+
{ type: 'label', content: 'Take', action: () => take() },
|
|
271
|
+
() => player.hasMagic ? {
|
|
272
|
+
type: 'label',
|
|
273
|
+
content: 'Cast Spell',
|
|
274
|
+
action: () => castSpell()
|
|
275
|
+
} : undefined
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
],
|
|
279
|
+
classNames: {
|
|
280
|
+
container: 'bg-gradient-to-b from-sky-900 to-indigo-900',
|
|
281
|
+
topHotspots: 'bg-muted/50 backdrop-blur-sm'
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Hotspot Types:**
|
|
287
|
+
- `MapLabelHotspot` - Text buttons positioned on map using percentage coordinates (x/y: 0-100)
|
|
288
|
+
- `MapImageHotspot` - Image buttons with state variants (idle/hover/active/disabled) and zoom support
|
|
289
|
+
- `SideLabelHotspot` - Text buttons on map edges (top/bottom/left/right)
|
|
290
|
+
- `SideImageHotspot` - Image buttons on map edges
|
|
291
|
+
- `MapMenu` - Contextual menu with multiple items at a specific position
|
|
292
|
+
|
|
293
|
+
**Dynamic Features:**
|
|
294
|
+
- Hotspots can be functions returning `undefined` for conditional visibility
|
|
295
|
+
- Images and positions support dynamic functions: `image: () => '/maps/' + season + '.jpg'`
|
|
296
|
+
- Disabled states with custom tooltips explaining why actions are unavailable
|
|
297
|
+
|
|
298
|
+
#### Widget Passages
|
|
299
|
+
|
|
300
|
+
Custom React components as passages:
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
import { newWidget } from '@react-text-game/core';
|
|
304
|
+
|
|
305
|
+
const customUI = newWidget('custom-ui', (
|
|
306
|
+
<div>
|
|
307
|
+
<h1>Custom Interface</h1>
|
|
308
|
+
<MyCustomComponent />
|
|
309
|
+
</div>
|
|
310
|
+
));
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Storage
|
|
314
|
+
|
|
315
|
+
JSONPath-based storage system using the `jsonpath` library:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { Storage } from '@react-text-game/core';
|
|
319
|
+
|
|
320
|
+
// Get values
|
|
321
|
+
const health = Storage.getValue<number>('$.player.health');
|
|
322
|
+
|
|
323
|
+
// Set values
|
|
324
|
+
Storage.setValue('$.player.health', 75);
|
|
325
|
+
|
|
326
|
+
// Full state
|
|
327
|
+
const state = Storage.getState();
|
|
328
|
+
Storage.setState(state);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Key Features:**
|
|
332
|
+
- JSONPath queries for flexible data access
|
|
333
|
+
- Protected system paths (prefixed with `$._system`)
|
|
334
|
+
- Automatic path creation
|
|
335
|
+
- Type-safe with generics
|
|
336
|
+
|
|
337
|
+
### Save System
|
|
338
|
+
|
|
339
|
+
The engine includes a comprehensive save/load system built on IndexedDB (via Dexie) with encryption support for export/import:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import {
|
|
343
|
+
useSaveSlots,
|
|
344
|
+
useSaveGame,
|
|
345
|
+
useLoadGame,
|
|
346
|
+
useDeleteGame,
|
|
347
|
+
useLastLoadGame,
|
|
348
|
+
useExportSaves,
|
|
349
|
+
useImportSaves,
|
|
350
|
+
useRestartGame
|
|
351
|
+
} from '@react-text-game/core/saves';
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Features:**
|
|
355
|
+
- **Persistent Storage** - IndexedDB for browser-based saves
|
|
356
|
+
- **Multiple Save Slots** - Unlimited save slots with metadata
|
|
357
|
+
- **Export/Import** - Encrypted file export/import (`.sx` format)
|
|
358
|
+
- **System Saves** - Hidden initial state for game restart
|
|
359
|
+
- **Real-time Updates** - Live queries for reactive save lists
|
|
360
|
+
- **Type-Safe** - Full TypeScript support
|
|
361
|
+
|
|
362
|
+
#### Save Management Hooks
|
|
363
|
+
|
|
364
|
+
**useSaveSlots** - Get save slots with live updates and action methods:
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
function SavesList() {
|
|
368
|
+
const slots = useSaveSlots({ count: 5 });
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<div>
|
|
372
|
+
{slots.map((slot, index) => (
|
|
373
|
+
<div key={index}>
|
|
374
|
+
<p>Slot {index}: {slot.data ? 'Saved' : 'Empty'}</p>
|
|
375
|
+
<button onClick={() => slot.save()}>Save</button>
|
|
376
|
+
<button onClick={() => slot.load()} disabled={!slot.data}>Load</button>
|
|
377
|
+
<button onClick={() => slot.delete()} disabled={!slot.data}>Delete</button>
|
|
378
|
+
</div>
|
|
379
|
+
))}
|
|
380
|
+
</div>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**useSaveGame** - Save current game state to a slot:
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
function SaveButton({ slotNumber }) {
|
|
389
|
+
const saveGame = useSaveGame();
|
|
390
|
+
|
|
391
|
+
const handleSave = async () => {
|
|
392
|
+
const result = await saveGame(slotNumber);
|
|
393
|
+
if (result?.success === false) {
|
|
394
|
+
alert(result.message);
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
<button onClick={handleSave}>
|
|
400
|
+
Save to Slot {slotNumber}
|
|
401
|
+
</button>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**useLoadGame** - Load a saved game by ID:
|
|
407
|
+
|
|
408
|
+
```tsx
|
|
409
|
+
function LoadButton({ saveId }) {
|
|
410
|
+
const loadGame = useLoadGame();
|
|
411
|
+
|
|
412
|
+
const handleLoad = async () => {
|
|
413
|
+
const result = await loadGame(saveId);
|
|
414
|
+
if (result?.success === false) {
|
|
415
|
+
alert(result.message);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
return <button onClick={handleLoad}>Load Game</button>;
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**useDeleteGame** - Delete a saved game by ID:
|
|
424
|
+
|
|
425
|
+
```tsx
|
|
426
|
+
function DeleteButton({ saveId }) {
|
|
427
|
+
const deleteGame = useDeleteGame();
|
|
428
|
+
|
|
429
|
+
const handleDelete = async () => {
|
|
430
|
+
const result = await deleteGame(saveId);
|
|
431
|
+
if (result?.success === false) {
|
|
432
|
+
alert(result.message);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
return <button onClick={handleDelete}>Delete Save</button>;
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**useLastLoadGame** - Load the most recent saved game:
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
function ContinueButton() {
|
|
444
|
+
const { hasLastSave, loadLastGame, isLoading } = useLastLoadGame();
|
|
445
|
+
|
|
446
|
+
if (isLoading) {
|
|
447
|
+
return <div>Loading...</div>;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<button onClick={loadLastGame} disabled={!hasLastSave}>
|
|
452
|
+
Continue Last Game
|
|
453
|
+
</button>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**useExportSaves** - Export all saves to encrypted file:
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
function ExportButton() {
|
|
462
|
+
const exportSaves = useExportSaves();
|
|
463
|
+
|
|
464
|
+
const handleExport = async () => {
|
|
465
|
+
const result = await exportSaves();
|
|
466
|
+
if (result.success) {
|
|
467
|
+
console.log('Saves exported successfully');
|
|
468
|
+
} else {
|
|
469
|
+
alert(`Export failed: ${result.error}`);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
return <button onClick={handleExport}>Export Saves</button>;
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**useImportSaves** - Import saves from encrypted file:
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
function ImportButton() {
|
|
481
|
+
const importSaves = useImportSaves();
|
|
482
|
+
|
|
483
|
+
const handleImport = async () => {
|
|
484
|
+
const result = await importSaves();
|
|
485
|
+
if (result.success) {
|
|
486
|
+
console.log(`Imported ${result.count} saves`);
|
|
487
|
+
} else {
|
|
488
|
+
alert(`Import failed: ${result.error}`);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
return <button onClick={handleImport}>Import Saves</button>;
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**useRestartGame** - Restart game from initial state:
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
function RestartButton() {
|
|
500
|
+
const restartGame = useRestartGame();
|
|
501
|
+
|
|
502
|
+
return (
|
|
503
|
+
<button onClick={restartGame}>
|
|
504
|
+
Restart Game
|
|
505
|
+
</button>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### Direct Database Access
|
|
511
|
+
|
|
512
|
+
For advanced use cases, you can access the database directly:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import {
|
|
516
|
+
saveGame,
|
|
517
|
+
loadGame,
|
|
518
|
+
getAllSaves,
|
|
519
|
+
deleteSave,
|
|
520
|
+
db
|
|
521
|
+
} from '@react-text-game/core/saves';
|
|
522
|
+
|
|
523
|
+
// Save game manually
|
|
524
|
+
await saveGame('my-save', gameData, 'Description', screenshotBase64);
|
|
525
|
+
|
|
526
|
+
// Load by ID
|
|
527
|
+
const save = await loadGame(1);
|
|
528
|
+
|
|
529
|
+
// Get all saves
|
|
530
|
+
const allSaves = await getAllSaves();
|
|
531
|
+
|
|
532
|
+
// Delete a save
|
|
533
|
+
await deleteSave(1);
|
|
534
|
+
|
|
535
|
+
// Direct Dexie access
|
|
536
|
+
await db.saves.where('name').equals('my-save').first();
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
#### Save File Encryption
|
|
540
|
+
|
|
541
|
+
Exported save files are encrypted using AES encryption with PBKDF2 key derivation:
|
|
542
|
+
- **Algorithm**: AES-256-CBC
|
|
543
|
+
- **Key Derivation**: PBKDF2 with 1000 iterations
|
|
544
|
+
- **Salt & IV**: Randomly generated for each export
|
|
545
|
+
- **Password**: Derived from `gameId` and `SAVE_POSTFIX`
|
|
546
|
+
|
|
547
|
+
### React Hooks
|
|
548
|
+
|
|
549
|
+
#### useCurrentPassage
|
|
550
|
+
|
|
551
|
+
Get the current passage with reactive updates:
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
import { useCurrentPassage } from '@react-text-game/core';
|
|
555
|
+
|
|
556
|
+
function GameScreen() {
|
|
557
|
+
const passage = useCurrentPassage();
|
|
558
|
+
|
|
559
|
+
if (!passage) return <div>Loading...</div>;
|
|
560
|
+
|
|
561
|
+
// Render based on passage type
|
|
562
|
+
if (passage.type === 'story') {
|
|
563
|
+
const { components } = passage.display();
|
|
564
|
+
// Render story components
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
#### useGameEntity
|
|
570
|
+
|
|
571
|
+
Monitor entity changes with automatic re-renders:
|
|
572
|
+
|
|
573
|
+
```tsx
|
|
574
|
+
import { useGameEntity } from '@react-text-game/core';
|
|
575
|
+
|
|
576
|
+
function PlayerStats({ player }) {
|
|
577
|
+
const reactivePlayer = useGameEntity(player);
|
|
578
|
+
|
|
579
|
+
return (
|
|
580
|
+
<div>
|
|
581
|
+
Health: {reactivePlayer.variables.health}
|
|
582
|
+
{/* Updates automatically when health changes */}
|
|
583
|
+
</div>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
#### useGameIsStarted
|
|
589
|
+
|
|
590
|
+
Check if game has started:
|
|
591
|
+
|
|
592
|
+
```tsx
|
|
593
|
+
import { useGameIsStarted } from '@react-text-game/core';
|
|
594
|
+
|
|
595
|
+
function GameUI() {
|
|
596
|
+
const isStarted = useGameIsStarted();
|
|
597
|
+
|
|
598
|
+
return isStarted ? <GameScreen /> : <MainMenu />;
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## Architecture
|
|
603
|
+
|
|
604
|
+
### State Flow
|
|
605
|
+
|
|
606
|
+
1. **Initialization** - Call `Game.init(options)` to initialize the game engine
|
|
607
|
+
2. **Entities** extend `BaseGameObject` and auto-register on construction
|
|
608
|
+
3. **Passages** extend `Passage` and auto-register on construction
|
|
609
|
+
4. **All state changes** go through Valtio proxies for reactivity
|
|
610
|
+
5. **Storage** uses JSONPath queries for flexible state access
|
|
611
|
+
6. **Auto-save** (if enabled) debounces writes to session storage
|
|
612
|
+
|
|
613
|
+
### Registry Pattern
|
|
614
|
+
|
|
615
|
+
The engine uses two registries:
|
|
616
|
+
- `objectRegistry` - Stores all game entities as Valtio proxies
|
|
617
|
+
- `passagesRegistry` - Stores all passages
|
|
618
|
+
|
|
619
|
+
All objects are automatically wrapped in Valtio proxies for reactive state management.
|
|
620
|
+
|
|
621
|
+
### Save System
|
|
622
|
+
|
|
623
|
+
The save system consists of:
|
|
624
|
+
- **Entity State** - Each entity's `_variables` stored at `$.{entityId}`
|
|
625
|
+
- **Game State** - Current passage stored at `$._system.game`
|
|
626
|
+
- **JSONPath Access** - Flexible queries for any state data
|
|
627
|
+
- **Auto-Save** - Debounced saves to session storage (500ms)
|
|
628
|
+
|
|
629
|
+
## API Reference
|
|
630
|
+
|
|
631
|
+
### Game
|
|
632
|
+
|
|
633
|
+
Static methods:
|
|
634
|
+
- `init(options)` - **Initialize the game (REQUIRED - must be called first)**
|
|
635
|
+
- `registerEntity(...objects)` - Register game objects
|
|
636
|
+
- `registerPassage(...passages)` - Register passages
|
|
637
|
+
- `jumpTo(passage)` - Navigate to passage
|
|
638
|
+
- `setCurrent(passage)` - Set current passage
|
|
639
|
+
- `getPassageById(id)` - Get passage by ID
|
|
640
|
+
- `getAllPassages()` - Get all passages
|
|
641
|
+
- `getState()` - Get full game state
|
|
642
|
+
- `setState(state)` - Restore game state
|
|
643
|
+
- `enableAutoSave()` - Enable auto-save
|
|
644
|
+
- `disableAutoSave()` - Disable auto-save
|
|
645
|
+
- `loadFromSessionStorage()` - Load from session storage
|
|
646
|
+
- `clearAutoSave()` - Clear auto-saved state
|
|
647
|
+
|
|
648
|
+
Properties:
|
|
649
|
+
- `currentPassage` - Get current passage
|
|
650
|
+
- `selfState` - Get game internal state
|
|
651
|
+
- `options` - Get game options
|
|
652
|
+
|
|
653
|
+
### BaseGameObject
|
|
654
|
+
|
|
655
|
+
Constructor:
|
|
656
|
+
- `new BaseGameObject({ id, variables? })`
|
|
657
|
+
|
|
658
|
+
Properties:
|
|
659
|
+
- `id` - Unique identifier
|
|
660
|
+
- `variables` - Entity variables (readonly)
|
|
661
|
+
- `_variables` - Internal variables (protected)
|
|
662
|
+
|
|
663
|
+
Methods:
|
|
664
|
+
- `save()` - Save to storage
|
|
665
|
+
- `load()` - Load from storage
|
|
666
|
+
|
|
667
|
+
### Passage Types
|
|
668
|
+
|
|
669
|
+
**Story:**
|
|
670
|
+
```typescript
|
|
671
|
+
newStory(id: string, content: StoryContent, options?: StoryOptions): Story
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
**Interactive Map:**
|
|
675
|
+
```typescript
|
|
676
|
+
newInteractiveMap(id: string, options: InteractiveMapOptions): InteractiveMap
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
**Widget:**
|
|
680
|
+
```typescript
|
|
681
|
+
newWidget(id: string, content: ReactNode): Widget
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
## TypeScript
|
|
685
|
+
|
|
686
|
+
Full TypeScript support with comprehensive types and detailed JSDoc documentation:
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
// Import types from main package
|
|
690
|
+
import type {
|
|
691
|
+
GameSaveState,
|
|
692
|
+
JsonPath,
|
|
693
|
+
InitVarsType,
|
|
694
|
+
PassageType,
|
|
695
|
+
ButtonColor,
|
|
696
|
+
ButtonVariant
|
|
697
|
+
} from '@react-text-game/core';
|
|
698
|
+
|
|
699
|
+
// Import story passage types
|
|
700
|
+
import type {
|
|
701
|
+
Component,
|
|
702
|
+
StoryContent,
|
|
703
|
+
StoryOptions,
|
|
704
|
+
TextComponent,
|
|
705
|
+
HeaderComponent,
|
|
706
|
+
ImageComponent,
|
|
707
|
+
VideoComponent,
|
|
708
|
+
ActionsComponent,
|
|
709
|
+
ConversationComponent,
|
|
710
|
+
AnotherStoryComponent,
|
|
711
|
+
ActionType,
|
|
712
|
+
ConversationBubble,
|
|
713
|
+
ConversationVariant,
|
|
714
|
+
ConversationAppearance
|
|
715
|
+
} from '@react-text-game/core/passages';
|
|
716
|
+
|
|
717
|
+
// Import interactive map types
|
|
718
|
+
import type {
|
|
719
|
+
InteractiveMapOptions,
|
|
720
|
+
InteractiveMapType,
|
|
721
|
+
AnyHotspot,
|
|
722
|
+
MapLabelHotspot,
|
|
723
|
+
MapImageHotspot,
|
|
724
|
+
SideLabelHotspot,
|
|
725
|
+
SideImageHotspot,
|
|
726
|
+
MapMenu,
|
|
727
|
+
LabelHotspot,
|
|
728
|
+
ImageHotspot
|
|
729
|
+
} from '@react-text-game/core/passages';
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
All types include comprehensive JSDoc comments with:
|
|
733
|
+
- Detailed descriptions of each property
|
|
734
|
+
- Usage examples and code snippets
|
|
735
|
+
- Default value annotations
|
|
736
|
+
- Remarks about behavior and implementation details
|
|
737
|
+
|
|
738
|
+
## Examples
|
|
739
|
+
|
|
740
|
+
See the `apps/example-game` directory for a complete implementation example.
|
|
741
|
+
|
|
742
|
+
## License
|
|
743
|
+
|
|
744
|
+
MIT (c) laruss
|