phaser-hooks 0.5.0 → 0.6.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.
- package/README.md +222 -825
- package/dist/hooks/type.d.ts +8 -6
- package/dist/hooks/type.d.ts.map +1 -1
- package/dist/hooks/with-debounced-state.js +2 -2
- package/dist/hooks/with-debounced-state.js.map +1 -1
- package/dist/hooks/with-local-state.spec.js +36 -4
- package/dist/hooks/with-local-state.spec.js.map +1 -1
- package/dist/hooks/with-state-def.d.ts +1 -1
- package/dist/hooks/with-state-def.d.ts.map +1 -1
- package/dist/hooks/with-state-def.js +40 -17
- package/dist/hooks/with-state-def.js.map +1 -1
- package/dist/hooks/with-undoable-state.js +2 -2
- package/dist/hooks/with-undoable-state.js.map +1 -1
- package/dist/phaser-hooks.js +1146 -0
- package/dist/phaser-hooks.min.js +1 -0
- package/dist/utils/__tests__/merge.test.d.ts +2 -0
- package/dist/utils/__tests__/merge.test.d.ts.map +1 -0
- package/dist/utils/__tests__/merge.test.js +390 -0
- package/dist/utils/__tests__/merge.test.js.map +1 -0
- package/dist/utils/merge.d.ts +17 -0
- package/dist/utils/merge.d.ts.map +1 -0
- package/dist/utils/merge.js +70 -0
- package/dist/utils/merge.js.map +1 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,65 +1,64 @@
|
|
|
1
|
-
<p align="center">
|
|
1
|
+
<p align="center" style="margin: 0 auto;">
|
|
2
2
|
<img src="data/image.png" alt="logo" style="max-width: 300px">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/phaser-hooks)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
|
|
9
|
-
# Phaser Hooks
|
|
9
|
+
# Phaser Hooks
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
React-like state management for Phaser 3 games. Simple, type-safe, and powerful.
|
|
12
12
|
|
|
13
13
|
## Why phaser-hooks?
|
|
14
14
|
|
|
15
|
-
Phaser
|
|
16
|
-
|
|
17
|
-
- `registry` → global state across the game
|
|
18
|
-
- `data` → local state inside a scene or game object
|
|
19
|
-
|
|
20
|
-
They work, but the API is a bit… verbose:
|
|
15
|
+
Phaser gives you `registry` (global) and `data` (local) for state management. They work fine, but the API is verbose and error-prone:
|
|
21
16
|
|
|
22
17
|
```ts
|
|
23
|
-
//
|
|
24
|
-
this.game.registry.set('volume', 0.5);
|
|
18
|
+
// Phaser's built-in way - this == scene
|
|
19
|
+
this.game.registry.set('volume', 0.5);
|
|
25
20
|
const volume = this.game.registry.get('volume');
|
|
21
|
+
|
|
26
22
|
this.game.registry.events.on('changedata-volume', (game, value) => {
|
|
27
23
|
console.log('Volume changed to', value);
|
|
28
24
|
});
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
this.data.set('score', 42); // too boring too
|
|
26
|
+
this.data.set('score', 42);
|
|
32
27
|
const score = this.data.get('score');
|
|
33
28
|
|
|
34
|
-
|
|
29
|
+
this.onChangeFn = (scene, value) => {
|
|
35
30
|
console.log('Score updated to', value);
|
|
36
31
|
};
|
|
37
|
-
this.data.events.on('changedata-score', onChangeFn); //
|
|
32
|
+
this.data.events.on('changedata-score', this.onChangeFn); // if you pass an anonymous function, you cannot unsubscribe
|
|
38
33
|
|
|
39
34
|
// when move to another scene, you must unsubscribe. Boring and easy to forget
|
|
40
|
-
this.data.events.off('
|
|
35
|
+
this.data.events.off('changedata-score', this.onChangeFn);
|
|
41
36
|
```
|
|
42
37
|
|
|
43
|
-
With _phaser-hooks_, you get a simple, React-like API
|
|
38
|
+
**With _phaser-hooks_, you get a simple, React-like API:**
|
|
44
39
|
|
|
45
40
|
```ts
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
volume.
|
|
49
|
-
volume.set(0.8); // updates value
|
|
41
|
+
const volume = withGlobalState(this, 'volume', 0.5);
|
|
42
|
+
volume.get(); // Returns: 0.5
|
|
43
|
+
volume.set(0.8); // updates the value
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
console.log('Volume changed →',
|
|
53
|
-
); //
|
|
45
|
+
this.unsubscribe = volume.on('change', () => {
|
|
46
|
+
console.log('Volume changed →', volume.get())
|
|
47
|
+
}); // Returns the easy unsubscribe function
|
|
54
48
|
|
|
55
|
-
// when
|
|
56
|
-
unsubscribe();
|
|
57
|
-
|
|
58
|
-
// Persisted state (localStorage / sessionStorage)
|
|
59
|
-
const score = withPersistState(scene, 'score', 0, { storage: 'local' }); // Wow! Saving in localStorage
|
|
60
|
-
score.set(100); // Update localStorage!! Wow! I love this lib <3
|
|
49
|
+
// when changing scenes
|
|
50
|
+
this.unsubscribe();
|
|
61
51
|
```
|
|
62
52
|
|
|
53
|
+
### Key Benefits
|
|
54
|
+
|
|
55
|
+
- ✅ **React-like patterns** - Hooks work just like React: same key = same state
|
|
56
|
+
- ✅ **Type-safe** - Full TypeScript support with inference
|
|
57
|
+
- ✅ **Memory safe** - Auto-cleanup prevents memory leaks
|
|
58
|
+
- ✅ **Feature-rich** - Persistence, computed state, undo/redo, validation
|
|
59
|
+
- ✅ **Familiar** - React-like patterns for easier onboarding
|
|
60
|
+
|
|
61
|
+
|
|
63
62
|
## Installation
|
|
64
63
|
|
|
65
64
|
```bash
|
|
@@ -69,807 +68,248 @@ pnpm add phaser-hooks
|
|
|
69
68
|
# or
|
|
70
69
|
yarn add phaser-hooks
|
|
71
70
|
```
|
|
71
|
+
> **Note:** This library uses "with" prefix (e.g., `withLocalState`) instead of "use" to avoid ESLint warnings in `.ts` files.
|
|
72
72
|
|
|
73
|
-
##
|
|
74
|
-
|
|
75
|
-
While React hooks traditionally use the "use" prefix (e.g., useState, useEffect), this library intentionally uses "with" to avoid linting issues. Many linting configurations, including ESLint's built-in hooks rules, expect functions starting with "use" to be used only within React components and in .jsx/.tsx files.
|
|
76
|
-
|
|
77
|
-
Since this library is designed to work with Phaser games, which typically use plain TypeScript/JavaScript files (.ts/.js), using the "with" prefix helps avoid false positives from linters while maintaining a clear and consistent naming convention that indicates the hook-like pattern these functions follow.
|
|
78
|
-
|
|
79
|
-
This approach allows you to use these state management utilities in your Phaser games without having to modify your linting configuration or suppress warnings.
|
|
80
|
-
|
|
81
|
-
## Hook API Reference
|
|
82
|
-
|
|
83
|
-
All hooks return a `HookState` object with the following methods:
|
|
84
|
-
|
|
85
|
-
| Method | Description | Parameters | Returns |
|
|
86
|
-
| -------------------------- | ---------------------------------------------------- | ----------------------------------------- | ----------------------------------- |
|
|
87
|
-
| `get()` | Gets the current state value | None | `T` - Current state value |
|
|
88
|
-
| `set(value)` | Sets a new state value and triggers change listeners | `value: T \| ((currentState: T) => T)` - New value to set or updater function | `void` |
|
|
89
|
-
| `patch(value)` | Patches object state with partial updates | `value: Partial<T> \| ((currentState: T) => Partial<T>)` - Partial object or updater function | `void` |
|
|
90
|
-
| `on('change', callback)` | Registers a callback for state changes | `event: 'change'`, `callback: () => void` | `() => void` - Unsubscribe function |
|
|
91
|
-
| `once('change', callback)` | Registers a callback that fires only once | `event: 'change'`, `callback: () => void` | `() => void` - Unsubscribe function |
|
|
92
|
-
| `off('change', callback)` | Removes an event listener | `event: 'change'`, `callback: () => void` | `void` |
|
|
93
|
-
| `clearListeners()` | Removes all event listeners for this state | None | `void` |
|
|
94
|
-
|
|
95
|
-
### State Updater Functions
|
|
96
|
-
|
|
97
|
-
The `set()` method supports both direct values and updater functions, similar to React's `useState`:
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
// Direct value assignment
|
|
101
|
-
playerState.set({ hp: 100, level: 5 });
|
|
102
|
-
|
|
103
|
-
// Using updater function (receives current state, returns new state)
|
|
104
|
-
playerState.set((currentState) => ({
|
|
105
|
-
...currentState,
|
|
106
|
-
level: currentState.level + 1
|
|
107
|
-
}));
|
|
108
|
-
|
|
109
|
-
// Equivalent to:
|
|
110
|
-
const newState = { ...playerState.get(), level: playerState.get().level + 1 };
|
|
111
|
-
playerState.set(newState);
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
**Benefits of updater functions:**
|
|
115
|
-
- ✅ **Immutable updates**: Always work with the latest state
|
|
116
|
-
- ✅ **Race condition safe**: No risk of using stale state
|
|
117
|
-
- ✅ **Cleaner code**: No need to manually get current state
|
|
118
|
-
- ✅ **Functional approach**: Encourages immutable state patterns
|
|
119
|
-
|
|
120
|
-
**Example with complex state updates:**
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
// Instead of this verbose approach:
|
|
124
|
-
const currentPlayer = playerState.get();
|
|
125
|
-
playerState.set({
|
|
126
|
-
...currentPlayer,
|
|
127
|
-
hp: Math.min(currentPlayer.hp + 20, currentPlayer.maxHp),
|
|
128
|
-
level: currentPlayer.exp >= 100 ? currentPlayer.level + 1 : currentPlayer.level,
|
|
129
|
-
exp: currentPlayer.exp >= 100 ? 0 : currentPlayer.exp + 10
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// Use this clean updater function:
|
|
133
|
-
playerState.set((player) => ({
|
|
134
|
-
...player,
|
|
135
|
-
hp: Math.min(player.hp + 20, player.maxHp),
|
|
136
|
-
level: player.exp >= 100 ? player.level + 1 : player.level,
|
|
137
|
-
exp: player.exp >= 100 ? 0 : player.exp + 10
|
|
138
|
-
}));
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### State Patching
|
|
142
|
-
|
|
143
|
-
The `patch()` method allows you to update only specific properties of an object state, similar to React's state updates:
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
// Direct partial object patching
|
|
147
|
-
playerState.patch({ life: 90 });
|
|
148
|
-
// Only updates 'life', preserves other properties
|
|
149
|
-
|
|
150
|
-
// Using updater function for patching
|
|
151
|
-
playerState.patch((currentState) => ({
|
|
152
|
-
life: currentState.life - 10,
|
|
153
|
-
level: currentState.level + 1
|
|
154
|
-
}));
|
|
155
|
-
// Updates multiple properties based on current state
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
**Benefits of patching:**
|
|
159
|
-
- ✅ **Partial updates**: Only change the properties you need
|
|
160
|
-
- ✅ **Preserves other data**: Unchanged properties remain untouched
|
|
161
|
-
- ✅ **Deep merging**: Works with nested objects using lodash.merge
|
|
162
|
-
- ✅ **Type safety**: TypeScript ensures you only patch valid properties
|
|
163
|
-
- ✅ **Performance**: More efficient than full object replacement
|
|
164
|
-
|
|
165
|
-
**Example with complex state updates:**
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
// Instead of this verbose approach:
|
|
169
|
-
const currentPlayer = playerState.get();
|
|
170
|
-
playerState.set({
|
|
171
|
-
...currentPlayer,
|
|
172
|
-
stats: {
|
|
173
|
-
...currentPlayer.stats,
|
|
174
|
-
hp: currentPlayer.stats.hp - 20,
|
|
175
|
-
mp: currentPlayer.stats.mp + 10
|
|
176
|
-
},
|
|
177
|
-
position: {
|
|
178
|
-
...currentPlayer.position,
|
|
179
|
-
x: currentPlayer.position.x + 5
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// Use this clean patch approach:
|
|
184
|
-
playerState.patch({
|
|
185
|
-
stats: {
|
|
186
|
-
hp: playerState.get().stats.hp - 20,
|
|
187
|
-
mp: playerState.get().stats.mp + 10
|
|
188
|
-
},
|
|
189
|
-
position: {
|
|
190
|
-
x: playerState.get().position.x + 5
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// Or even cleaner with updater function:
|
|
195
|
-
playerState.patch((player) => ({
|
|
196
|
-
stats: {
|
|
197
|
-
hp: player.stats.hp - 20,
|
|
198
|
-
mp: player.stats.mp + 10
|
|
199
|
-
},
|
|
200
|
-
position: {
|
|
201
|
-
x: player.position.x + 5
|
|
202
|
-
}
|
|
203
|
-
}));
|
|
204
|
-
```
|
|
73
|
+
## 🌐 UMD/CDN (JavaScript)
|
|
205
74
|
|
|
206
|
-
|
|
75
|
+
If you prefer not to use TypeScript or want to include the library via CDN, you can use the UMD build:
|
|
207
76
|
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
gameState.patch({
|
|
211
|
-
player: {
|
|
212
|
-
character: {
|
|
213
|
-
stats: {
|
|
214
|
-
primary: {
|
|
215
|
-
strength: 15
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
// Only updates the strength value, preserves all other nested properties
|
|
77
|
+
```html
|
|
78
|
+
<script src="https://cdn.jsdelivr.net/npm/phaser-hooks@0.6.0/dist/phaser-hooks.min.js"></script>
|
|
222
79
|
```
|
|
223
80
|
|
|
224
|
-
|
|
81
|
+
The library will be available globally as `window.PhaserHooks`. You can use it like this:
|
|
225
82
|
|
|
226
|
-
```
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
items: [...inventoryState.get().items, 'new-item']
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// Or with updater function
|
|
233
|
-
inventoryState.patch((inventory) => ({
|
|
234
|
-
items: [...inventory.items, 'new-item']
|
|
235
|
-
}));
|
|
83
|
+
```javascript
|
|
84
|
+
// Create local state
|
|
85
|
+
const playerState = window.PhaserHooks.withLocalState(scene, 'player', { hp: 100 });
|
|
236
86
|
```
|
|
237
87
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
Some hooks have additional methods beyond the standard `HookState` interface:
|
|
241
|
-
|
|
242
|
-
#### `withUndoableState` Additional Methods:
|
|
88
|
+
> **⚠️ Note**: While UMD builds are available, we **strongly recommend using TypeScript** for better type safety, IntelliSense, and development experience. The TypeScript version provides better error detection and autocomplete features.
|
|
243
89
|
|
|
244
|
-
|
|
245
|
-
| ---------------- | ----------------------------- | ---------- | -------------------------- |
|
|
246
|
-
| `undo()` | Reverts to the previous state | None | `boolean` - Success status |
|
|
247
|
-
| `redo()` | Advances to the next state | None | `boolean` - Success status |
|
|
248
|
-
| `canUndo()` | Checks if undo is available | None | `boolean` |
|
|
249
|
-
| `canRedo()` | Checks if redo is available | None | `boolean` |
|
|
250
|
-
| `clearHistory()` | Clears the undo/redo history | None | `void` |
|
|
90
|
+
## Quick Start
|
|
251
91
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
### Core Hooks
|
|
255
|
-
|
|
256
|
-
#### `withLocalState`
|
|
257
|
-
|
|
258
|
-
Scene-specific state management that gets cleaned up when the scene is destroyed.
|
|
92
|
+
Here's a complete example showing the basics:
|
|
259
93
|
|
|
260
94
|
```typescript
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
exp: number;
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const playerState = withLocalState<PlayerData>(scene, 'player', {
|
|
268
|
-
hp: 100,
|
|
269
|
-
level: 1,
|
|
270
|
-
exp: 0,
|
|
271
|
-
});
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
#### `withGlobalState`
|
|
95
|
+
// hooks/withPlayerState.ts
|
|
96
|
+
import { withLocalState } from 'phaser-hooks';
|
|
97
|
+
// or const { withLocalState } from 'phaser-hooks';
|
|
275
98
|
|
|
276
|
-
|
|
99
|
+
const withPlayer = (scene: Phaser.Scene) => {
|
|
100
|
+
const player = withLocalState(scene, 'player', {
|
|
101
|
+
hp: 100,
|
|
102
|
+
maxHp: 100,
|
|
103
|
+
level: 1,
|
|
104
|
+
});
|
|
277
105
|
|
|
278
|
-
|
|
279
|
-
type GameSettings = {
|
|
280
|
-
soundVolume: number;
|
|
281
|
-
musicEnabled: true;
|
|
106
|
+
return player;
|
|
282
107
|
};
|
|
283
108
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
musicEnabled: true,
|
|
287
|
-
});
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
### Enhanced Hooks
|
|
109
|
+
// hooks/withSettings.ts
|
|
110
|
+
import { withGlobalState } from 'phaser-hooks';
|
|
291
111
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
State with automatic localStorage persistence.
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
type UserSettings = {
|
|
298
|
-
volume: number;
|
|
299
|
-
difficulty: 'easy' | 'normal' | 'hard';
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const persistentSettings = withPersistentState<UserSettings>(
|
|
303
|
-
'settings',
|
|
304
|
-
{
|
|
112
|
+
const withSettings = (scene: Phaser.Scene) => {
|
|
113
|
+
const settings = withGlobalState(scene, 'settings', {
|
|
305
114
|
volume: 0.8,
|
|
306
|
-
difficulty: 'normal'
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
#### `withComputedState`
|
|
313
|
-
|
|
314
|
-
Derived state that automatically updates when source state changes.
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
const healthPercentage = withComputedState(
|
|
318
|
-
scene,
|
|
319
|
-
'healthPercent',
|
|
320
|
-
playerState,
|
|
321
|
-
player => (player.hp / player.maxHp) * 100
|
|
322
|
-
);
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
#### `withUndoableState`
|
|
326
|
-
|
|
327
|
-
State with undo/redo functionality.
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
const undoableText = withUndoableState<string>(scene, 'text', 'initial', 10);
|
|
331
|
-
|
|
332
|
-
undoableText.set('first change');
|
|
333
|
-
undoableText.set('second change');
|
|
334
|
-
undoableText.undo(); // Back to 'first change'
|
|
335
|
-
undoableText.redo(); // Forward to 'second change'
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
#### `withDebouncedState`
|
|
339
|
-
|
|
340
|
-
State with debounced updates to prevent rapid successive changes.
|
|
341
|
-
|
|
342
|
-
```typescript
|
|
343
|
-
const debouncedSearch = withDebouncedState<string>(scene, 'search', '', 300);
|
|
344
|
-
|
|
345
|
-
// These rapid calls will be debounced
|
|
346
|
-
debouncedSearch.set('a');
|
|
347
|
-
debouncedSearch.set('ab');
|
|
348
|
-
debouncedSearch.set('abc'); // Only this final value will be set after 300ms
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### Utilities
|
|
352
|
-
|
|
353
|
-
#### `validators`
|
|
354
|
-
|
|
355
|
-
Pre-built validation functions for common patterns.
|
|
356
|
-
|
|
357
|
-
```typescript
|
|
358
|
-
import { validators } from 'phaser-hooks';
|
|
115
|
+
difficulty: 'normal'
|
|
116
|
+
});
|
|
117
|
+
return settings;
|
|
118
|
+
};
|
|
359
119
|
|
|
360
|
-
// Number range validation (0-1000)
|
|
361
|
-
const scoreState = withGlobalState<number>(scene, 'score', 0, {
|
|
362
|
-
validator: validators.numberRange(0, 1000),
|
|
363
|
-
});
|
|
364
120
|
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
validator: validators.nonEmptyString,
|
|
368
|
-
});
|
|
121
|
+
// scenes/gameScene.ts
|
|
122
|
+
import { withLocalState, withGlobalState } from 'phaser-hooks';
|
|
369
123
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
validator: validators.arrayLength(2, 4),
|
|
373
|
-
});
|
|
124
|
+
class GameScene extends Phaser.Scene {
|
|
125
|
+
private unsubscribe?: () => void;
|
|
374
126
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
'difficulty',
|
|
379
|
-
'normal',
|
|
380
|
-
{
|
|
381
|
-
validator: validators.oneOf(['easy', 'normal', 'hard']),
|
|
382
|
-
}
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
// Custom validator example
|
|
386
|
-
const healthState = withLocalState<number>(scene, 'health', 100, {
|
|
387
|
-
validator: value => {
|
|
388
|
-
const health = value as number;
|
|
389
|
-
if (health < 0) return 'Health cannot be negative';
|
|
390
|
-
if (health > 100) return 'Health cannot exceed 100';
|
|
391
|
-
return true; // Valid
|
|
392
|
-
},
|
|
393
|
-
});
|
|
394
|
-
```
|
|
127
|
+
create() {
|
|
128
|
+
// 1. Local state (scene-specific, auto-cleanup)
|
|
129
|
+
const player = withPlayer(this); // clean and reusable within the same scene
|
|
395
130
|
|
|
396
|
-
|
|
131
|
+
// 2. Global state (persists across scenes)
|
|
132
|
+
const settings = withSettings(this); // the same instance in all scenes
|
|
397
133
|
|
|
398
|
-
|
|
134
|
+
// 3. Update state
|
|
135
|
+
player.patch({ hp: 90 }); // Partial update
|
|
136
|
+
settings.set({ volume: 0.5, difficulty: 'hard' }); // Full update
|
|
399
137
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
inventoryState.set([...inventoryState.get(), 'new-item']);
|
|
404
|
-
scoreState.set(scoreState.get() + 100);
|
|
405
|
-
});
|
|
406
|
-
```
|
|
138
|
+
// 4. Read state
|
|
139
|
+
console.log(player.get().hp); // 90
|
|
140
|
+
console.log(settings.get().volume); // 0.5
|
|
407
141
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
export class GameScene extends Phaser.Scene {
|
|
414
|
-
create() {
|
|
415
|
-
// Local state - specific to this scene
|
|
416
|
-
const playerState = withLocalState<{ hp: number; mp: number }>(
|
|
417
|
-
this,
|
|
418
|
-
'player',
|
|
419
|
-
{
|
|
420
|
-
hp: 100,
|
|
421
|
-
mp: 50,
|
|
142
|
+
// 5. Listen to changes
|
|
143
|
+
this.unsubscribe = player.on('change', (newPlayer, oldPlayer) => {
|
|
144
|
+
if (newPlayer.hp < 20) {
|
|
145
|
+
console.warn(`Low health! Your old HP was ${oldPlayer.hp}`);
|
|
422
146
|
}
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
// Global state - persists across scenes
|
|
426
|
-
const gameState = withGlobalState<{ score: number; level: number }>(
|
|
427
|
-
'game',
|
|
428
|
-
{
|
|
429
|
-
score: 0,
|
|
430
|
-
level: 1,
|
|
431
|
-
}
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
// Listen to changes
|
|
435
|
-
const ubsubscribe = playerState.on('change', (newPlayer, oldPlayer) => {
|
|
436
|
-
console.log('Player health changed:', newPlayer.hp);
|
|
437
147
|
});
|
|
438
|
-
|
|
439
|
-
// Update state - using patch method (recommended for partial updates)
|
|
440
|
-
playerState.patch({ hp: playerState.get().hp - 10 });
|
|
441
|
-
|
|
442
|
-
// Alternative: using updater function with set
|
|
443
|
-
// playerState.set((currentPlayer) => ({
|
|
444
|
-
// ...currentPlayer,
|
|
445
|
-
// hp: currentPlayer.hp - 10,
|
|
446
|
-
// }));
|
|
447
|
-
|
|
448
|
-
// Alternative: direct value assignment
|
|
449
|
-
// playerState.set({
|
|
450
|
-
// ...playerState.get(),
|
|
451
|
-
// hp: playerState.get().hp - 10,
|
|
452
|
-
// });
|
|
453
148
|
}
|
|
454
|
-
}
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
## Advanced Example
|
|
458
|
-
|
|
459
|
-
```typescript
|
|
460
|
-
import {
|
|
461
|
-
withPersistentState,
|
|
462
|
-
withComputedState,
|
|
463
|
-
withUndoableState,
|
|
464
|
-
validators,
|
|
465
|
-
} from 'phaser-hooks';
|
|
466
|
-
|
|
467
|
-
export class AdvancedGameScene extends Phaser.Scene {
|
|
468
|
-
create() {
|
|
469
|
-
// Persistent settings
|
|
470
|
-
const settings = withPersistentState<GameSettings>('settings', {
|
|
471
|
-
soundVolume: 0.8,
|
|
472
|
-
musicVolume: 0.6,
|
|
473
|
-
difficulty: 'normal',
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
// Player state with validation
|
|
477
|
-
const player = withLocalState<PlayerData>(
|
|
478
|
-
this,
|
|
479
|
-
'player',
|
|
480
|
-
{
|
|
481
|
-
hp: 100,
|
|
482
|
-
maxHp: 100,
|
|
483
|
-
level: 1,
|
|
484
|
-
},
|
|
485
|
-
{
|
|
486
|
-
validator: validators.oneOf(['easy', 'normal', 'hard']),
|
|
487
|
-
}
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
// Computed health percentage
|
|
491
|
-
const healthPercent = withComputedState(this, 'healthPercent', player, p =>
|
|
492
|
-
Math.round((p.hp / p.maxHp) * 100)
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
// Undoable action system
|
|
496
|
-
const actionHistory = withUndoableState<string>(this, 'actions', 'start');
|
|
497
149
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (healthPercent.get() < 20) {
|
|
502
|
-
console.log('Low health warning!');
|
|
503
|
-
}
|
|
150
|
+
shutdown() {
|
|
151
|
+
// 6. Clean up (local state auto-cleans, but it’s good practice)
|
|
152
|
+
this.unsubscribe?.();
|
|
504
153
|
}
|
|
505
154
|
}
|
|
506
155
|
```
|
|
507
156
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
You can compose your own hooks using other with\* hooks — similar to how custom React hooks are built. This is a powerful way to isolate logic, reuse behavior, and keep your scenes clean and focused.
|
|
511
|
-
|
|
512
|
-
Example: Extracting a withPlayerEnergy hook from withPlayerState
|
|
513
|
-
|
|
514
|
-
Imagine you have a local player state like this:
|
|
515
|
-
|
|
516
|
-
```ts
|
|
517
|
-
interface PlayerAttributes {
|
|
518
|
-
energy: number;
|
|
519
|
-
stamina: number;
|
|
520
|
-
strength: number;
|
|
521
|
-
agility: number;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const playerState = withLocalState<PlayerAttributes>(scene, 'player', {
|
|
525
|
-
energy: 100,
|
|
526
|
-
stamina: 80,
|
|
527
|
-
strength: 50,
|
|
528
|
-
agility: 40,
|
|
529
|
-
});
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
You can now create a custom hook focused only on energy:
|
|
533
|
-
|
|
534
|
-
```ts
|
|
535
|
-
function withPlayerEnergy(scene: Phaser.Scene) {
|
|
536
|
-
const player = withLocalState<PlayerAttributes>(scene, 'player', {
|
|
537
|
-
energy: 100,
|
|
538
|
-
stamina: 80,
|
|
539
|
-
strength: 50,
|
|
540
|
-
agility: 40,
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
return {
|
|
544
|
-
...player,
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
Usage in a scene
|
|
550
|
-
|
|
551
|
-
```ts
|
|
552
|
-
const energy = withPlayerEnergy(this);
|
|
553
|
-
|
|
554
|
-
console.log('Current energy:', energy.get());
|
|
555
|
-
|
|
556
|
-
// Using updater function (recommended)
|
|
557
|
-
energy.set((currentEnergy) => currentEnergy - 10);
|
|
157
|
+
**That's it!** You now have reactive, type-safe state management in your Phaser game.
|
|
558
158
|
|
|
559
|
-
|
|
560
|
-
// energy.set(energy.get() - 10);
|
|
561
|
-
|
|
562
|
-
energy.on('change', () => {
|
|
563
|
-
if (energy.get() <= 0) {
|
|
564
|
-
console.warn('You are out of energy!');
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
## Unsubscribe Events
|
|
570
|
-
|
|
571
|
-
When you subscribe to state changes using `.on('change', callback)`, it's crucial to properly unsubscribe to prevent memory leaks and unexpected behavior. Phaser Hooks provides two ways for unsubscribing from events.
|
|
572
|
-
|
|
573
|
-
### Method 1: Using the Return Value from `.on('change')`
|
|
574
|
-
|
|
575
|
-
The `.on('change', callback)` method returns an unsubscribe function that you can call to remove the listener:
|
|
159
|
+
### Recommended: Create Custom Hooks
|
|
576
160
|
|
|
161
|
+
**Just like in React**, the real power comes from creating reusable hooks (but without React):
|
|
577
162
|
```typescript
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const playerState = withLocalState<{ hp: number }>(this, 'player', {
|
|
581
|
-
hp: 100,
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
// Subscribe to changes and get unsubscribe function
|
|
585
|
-
const unsubscribe = playerState.on('change', (newPlayer, oldPlayer) => {
|
|
586
|
-
console.log('Player health changed:', newPlayer.hp);
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
this.add
|
|
590
|
-
.text(centerX, centerY, 'Go to another scene')
|
|
591
|
-
.setInteractive()
|
|
592
|
-
.on('pointerdown', () => {
|
|
593
|
-
// Later, unsubscribe when needed
|
|
594
|
-
unsubscribe();
|
|
163
|
+
// GameScene.ts
|
|
164
|
+
import { withPlayerState } from './hooks/withPlayerState';
|
|
595
165
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
166
|
+
class GameScene extends Phaser.Scene {
|
|
167
|
+
create() {
|
|
168
|
+
const player = withPlayerState(this); // Clean and reusable!
|
|
169
|
+
player.patch({ hp: 90 });
|
|
599
170
|
}
|
|
600
171
|
}
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
### Method 2: Using `.off('change', callback)`
|
|
604
|
-
|
|
605
|
-
You can also unsubscribe by passing the same callback function to `.off('change', callback)`:
|
|
606
172
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const
|
|
613
|
-
|
|
173
|
+
// HealthBar.ts - Access the SAME state!
|
|
174
|
+
class HealthBar extends Phaser.GameObjects.Container {
|
|
175
|
+
constructor(scene: Phaser.Scene) {
|
|
176
|
+
super(scene, 0, 0);
|
|
177
|
+
|
|
178
|
+
const player = withPlayerState(scene); // Same state instance!
|
|
179
|
+
|
|
180
|
+
player.on('change', (newPlayer) => {
|
|
181
|
+
this.updateDisplay(newPlayer.hp, newPlayer.maxHp);
|
|
614
182
|
});
|
|
615
|
-
|
|
616
|
-
// Define callback function
|
|
617
|
-
this.healthCallback = (newPlayer, oldPlayer) => {
|
|
618
|
-
console.log('Player health changed:', newPlayer.hp);
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
// Subscribe to changes
|
|
622
|
-
playerState.on('change', this.healthCallback);
|
|
623
|
-
|
|
624
|
-
this.add
|
|
625
|
-
.text(centerX, centerY, 'Go to another scene')
|
|
626
|
-
.setInteractive()
|
|
627
|
-
.on('pointerdown', () => {
|
|
628
|
-
// Later, unsubscribe when needed
|
|
629
|
-
playerState.off('change', this.healthCallback);
|
|
630
|
-
|
|
631
|
-
// To switch to another scene in Phaser, use:
|
|
632
|
-
this.scene.start('OtherSceneKey');
|
|
633
|
-
});
|
|
634
183
|
}
|
|
635
184
|
}
|
|
636
185
|
```
|
|
637
186
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
### Best Practices for Scene Cleanup
|
|
187
|
+
**Key insight:** Using the same `key` returns the same state instance, just like React hooks! This allows you to access state from anywhere: scenes, components, systems, etc.
|
|
188
|
+
**Initial value:** The initial value is only applied during the first execution. On subsequent calls, the same state instance is reused — just like in React Hooks.
|
|
641
189
|
|
|
642
|
-
|
|
190
|
+
### Advanced: Hooks with Custom Methods
|
|
643
191
|
|
|
644
|
-
|
|
645
|
-
- Unexpected behavior when returning to the scene
|
|
646
|
-
- Callbacks firing on destroyed or inactive scenes
|
|
647
|
-
- Performance issues over time
|
|
192
|
+
> 💡 If you’re not using TypeScript, don’t worry — all hooks work with plain JavaScript too.
|
|
648
193
|
|
|
649
|
-
|
|
194
|
+
However, defining full types for your state object, hook return, and custom methods gives you complete end-to-end type safety with full IntelliSense for every method and return value.
|
|
650
195
|
|
|
651
196
|
```typescript
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
create() {
|
|
656
|
-
const playerState = withLocalState<{ hp: number }>(this, 'player', {
|
|
657
|
-
hp: 100,
|
|
658
|
-
});
|
|
659
|
-
const scoreState = withGlobalState<number>(this, 'score', 0);
|
|
660
|
-
|
|
661
|
-
// Store unsubscribe functions
|
|
662
|
-
this.unsubscribeFunctions.push(
|
|
663
|
-
playerState.on('change', newPlayer => {
|
|
664
|
-
console.log('Player updated:', newPlayer);
|
|
665
|
-
})
|
|
666
|
-
);
|
|
667
|
-
|
|
668
|
-
this.unsubscribeFunctions.push(
|
|
669
|
-
scoreState.on('change', newScore => {
|
|
670
|
-
console.log('Score updated:', newScore);
|
|
671
|
-
})
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Clean up when scene is destroyed or when transitioning
|
|
676
|
-
shutdown() {
|
|
677
|
-
// Unsubscribe from all events
|
|
678
|
-
this.unsubscribeFunctions.forEach(unsubscribe => unsubscribe());
|
|
679
|
-
this.unsubscribeFunctions = [];
|
|
680
|
-
}
|
|
197
|
+
// hooks/withPlayerState.ts
|
|
198
|
+
import { withLocalState, type HookState } from 'phaser-hooks';
|
|
681
199
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
// Then transition
|
|
689
|
-
this.scene.start('NextScene');
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
### Using `clearListeners()` for Easy Cleanup
|
|
695
|
-
|
|
696
|
-
For easier cleanup, you can use the `clearListeners()` method to remove all event listeners at once:
|
|
200
|
+
export type PlayerState = {
|
|
201
|
+
hp: number;
|
|
202
|
+
maxHp: number;
|
|
203
|
+
level: number;
|
|
204
|
+
};
|
|
697
205
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
206
|
+
export type PlayerHook = HookState<PlayerState> & {
|
|
207
|
+
takeDamage: (amount: number) => void;
|
|
208
|
+
heal: (amount: number) => void;
|
|
209
|
+
levelUp: () => void;
|
|
210
|
+
};
|
|
702
211
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
212
|
+
const initialPlayerState: PlayerState = {
|
|
213
|
+
hp: 100,
|
|
214
|
+
maxHp: 100,
|
|
215
|
+
level: 1,
|
|
216
|
+
};
|
|
706
217
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
218
|
+
export function withPlayerState(scene: Phaser.Scene): PlayerHook {
|
|
219
|
+
const state = withLocalState<PlayerState>(scene, 'player', initialPlayerState);
|
|
220
|
+
const takeDamage = (amount: number): void => {
|
|
221
|
+
const current = state.get();
|
|
222
|
+
state.patch({
|
|
223
|
+
hp: Math.max(0, current.hp - amount),
|
|
710
224
|
});
|
|
225
|
+
};
|
|
711
226
|
|
|
712
|
-
|
|
713
|
-
|
|
227
|
+
const heal = (amount: number): void => {
|
|
228
|
+
const current = state.get();
|
|
229
|
+
state.patch({
|
|
230
|
+
hp: Math.min(current.maxHp, current.hp + amount),
|
|
714
231
|
});
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
shutdown() {
|
|
718
|
-
// Clear all listeners at once - much easier!
|
|
719
|
-
this.playerState.clearListeners();
|
|
720
|
-
this.scoreState.clearListeners();
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
#### Important Notes about `clearListeners()`:
|
|
726
|
-
|
|
727
|
-
- **`withLocalState`**: Automatically cleans up when the scene is destroyed, but you can still use `clearListeners()` for manual cleanup
|
|
728
|
-
- **`withGlobalState`**: **Requires manual cleanup** since global state persists across scenes. Always call `clearListeners()` when the scene is destroyed:
|
|
729
|
-
|
|
730
|
-
```typescript
|
|
731
|
-
export class GameScene extends Phaser.Scene {
|
|
732
|
-
private globalState: HookState<GameSettings>;
|
|
232
|
+
};
|
|
733
233
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
234
|
+
const levelUp = (): void => {
|
|
235
|
+
const current = state.get();
|
|
236
|
+
state.patch({
|
|
237
|
+
level: current.level + 1,
|
|
238
|
+
maxHp: current.maxHp + 10,
|
|
239
|
+
hp: current.maxHp + 10,
|
|
739
240
|
});
|
|
241
|
+
};
|
|
740
242
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
243
|
+
return {
|
|
244
|
+
...state, // get, set, patch, on, once, off, clearListeners
|
|
245
|
+
takeDamage,
|
|
246
|
+
heal,
|
|
247
|
+
levelUp,
|
|
248
|
+
};
|
|
746
249
|
}
|
|
747
|
-
```
|
|
748
250
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
// Multiple listeners for the same state
|
|
766
|
-
const unsubscribeHealth = playerState.on('change', newPlayer => {
|
|
767
|
-
console.log('Health changed:', newPlayer.hp);
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
const unsubscribeLevel = playerState.on('change', newPlayer => {
|
|
771
|
-
console.log('Level changed:', newPlayer.level);
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
// Unsubscribe specific listeners
|
|
775
|
-
unsubscribeHealth(); // Only removes health listener
|
|
776
|
-
// unsubscribeLevel still active
|
|
777
|
-
}
|
|
778
|
-
}
|
|
251
|
+
// Usage in your scene
|
|
252
|
+
const player = withPlayerState(this);
|
|
253
|
+
console.log(player.get());
|
|
254
|
+
player.takeDamage(30);
|
|
255
|
+
console.log(player.get());
|
|
256
|
+
player.heal(10);
|
|
257
|
+
console.log(player.get());
|
|
258
|
+
player.levelUp();
|
|
259
|
+
console.log(player.get());
|
|
260
|
+
/**
|
|
261
|
+
* Output:
|
|
262
|
+
* {hp: 100, maxHp: 100, level: 1}
|
|
263
|
+
* {hp: 70, maxHp: 100, level: 1}
|
|
264
|
+
* {hp: 80, maxHp: 100, level: 1}
|
|
265
|
+
* {hp: 110, maxHp: 110, level: 2}
|
|
266
|
+
*/
|
|
779
267
|
```
|
|
780
268
|
|
|
781
|
-
###
|
|
782
|
-
|
|
783
|
-
The `.once()` method registers a callback that will only fire once, then automatically unsubscribes:
|
|
784
|
-
|
|
785
|
-
```typescript
|
|
786
|
-
export class GameScene extends Phaser.Scene {
|
|
787
|
-
create() {
|
|
788
|
-
const playerState = withLocalState<{ hp: number; level: number }>(
|
|
789
|
-
this,
|
|
790
|
-
'player',
|
|
791
|
-
{
|
|
792
|
-
hp: 100,
|
|
793
|
-
level: 1,
|
|
794
|
-
}
|
|
795
|
-
);
|
|
796
|
-
|
|
797
|
-
// One-time listener - fires only once then auto-unsubscribes
|
|
798
|
-
const unsubscribeOnce = playerState.once('change', newPlayer => {
|
|
799
|
-
console.log('First level up detected!', newPlayer.level);
|
|
800
|
-
// This callback will only run once, even if the state changes multiple times
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
// You can still manually unsubscribe if needed before it fires
|
|
804
|
-
// unsubscribeOnce();
|
|
269
|
+
### Next Steps
|
|
805
270
|
|
|
806
|
-
|
|
807
|
-
playerState.set({ hp: 100, level: 2 }); // Fires the once callback
|
|
808
|
-
playerState.set({ hp: 100, level: 3 }); // Won't fire the once callback again
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
```
|
|
271
|
+
- 📚 [Full documentation and examples](https://toolkit.cassino.dev/phaser-hooks)
|
|
812
272
|
|
|
813
|
-
|
|
273
|
+
## Core Concepts
|
|
814
274
|
|
|
815
|
-
|
|
275
|
+
### Updater Functions
|
|
816
276
|
|
|
277
|
+
Both `set()` and `patch()` accept updater functions for race-condition-safe updates:
|
|
817
278
|
```typescript
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const healthState = withLocalState<number>(this, 'health', 100, {
|
|
821
|
-
validator: validators.numberRange(0, 100),
|
|
822
|
-
});
|
|
279
|
+
// Direct value
|
|
280
|
+
player.set({ hp: 90, level: 2 });
|
|
823
281
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
} catch (error) {
|
|
827
|
-
console.error('Invalid health value:', error.message);
|
|
828
|
-
// Handle the error appropriately
|
|
829
|
-
}
|
|
282
|
+
// Updater function (recommended when based on current state)
|
|
283
|
+
player.set(current => ({ ...current, hp: current.hp - 10 }));
|
|
830
284
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
834
|
-
}
|
|
285
|
+
// Patch with updater
|
|
286
|
+
player.patch(current => ({ hp: current.hp + 20 }));
|
|
835
287
|
```
|
|
836
288
|
|
|
837
|
-
|
|
289
|
+
**Why use updater functions?** They always work with the latest state, preventing race conditions in async scenarios.
|
|
838
290
|
|
|
839
|
-
|
|
291
|
+
---
|
|
840
292
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
✅ Makes it easier to refactor or share logic across scenes and systems
|
|
844
|
-
|
|
845
|
-
You can extend this idea to compose computed hooks, persistent hooks, undoable hooks, and more — everything works with the same API.
|
|
846
|
-
|
|
847
|
-
## TypeScript Support
|
|
848
|
-
|
|
849
|
-
All hooks are fully typed and provide excellent TypeScript support:
|
|
293
|
+
### `set()` vs `patch()`
|
|
850
294
|
|
|
295
|
+
- **`set()`** - Full state replacement
|
|
296
|
+
- **`patch()`** - Partial update with deep merge (only for objects)
|
|
851
297
|
```typescript
|
|
852
|
-
|
|
853
|
-
hp:
|
|
854
|
-
maxHp:
|
|
855
|
-
level:
|
|
856
|
-
inventory: string[];
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const playerState = withLocalState<PlayerData>(scene, 'player', {
|
|
860
|
-
hp: 100,
|
|
861
|
-
maxHp: 100,
|
|
862
|
-
level: 1,
|
|
863
|
-
inventory: [],
|
|
298
|
+
const player = withLocalState(this, 'player', {
|
|
299
|
+
hp: 100,
|
|
300
|
+
maxHp: 100,
|
|
301
|
+
level: 1
|
|
864
302
|
});
|
|
865
303
|
|
|
866
|
-
//
|
|
867
|
-
|
|
304
|
+
player.set({ hp: 90, maxHp: 100, level: 1 }); // Must provide all properties
|
|
305
|
+
player.patch({ hp: 90 }); // Only updates hp, preserves maxHp and level
|
|
868
306
|
```
|
|
869
307
|
|
|
308
|
+
**Rule of thumb:** Use `patch()` for object states when you only need to update specific properties.
|
|
309
|
+
|
|
870
310
|
## Debug Mode / Dev tool
|
|
871
311
|
|
|
872
|
-
Phaser Hooks includes a built-in debug mode that provides detailed logging for state operations. This is extremely useful
|
|
312
|
+
Phaser Hooks includes a built-in debug mode that provides detailed logging for state operations. This is extremely useful when developing or debugging state-related issues.
|
|
873
313
|
|
|
874
314
|
### How to Enable Debug Mode
|
|
875
315
|
|
|
@@ -878,93 +318,50 @@ To enable debug mode, simply pass `{ debug: true }` in the options parameter whe
|
|
|
878
318
|
```typescript
|
|
879
319
|
import { withLocalState } from 'phaser-hooks';
|
|
880
320
|
|
|
881
|
-
export
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
{ debug: true } // Enable debug logging
|
|
892
|
-
);
|
|
893
|
-
|
|
894
|
-
// All operations will now be logged to the console
|
|
895
|
-
playerState.set({ hp: 90, level: 2 });
|
|
896
|
-
const currentPlayer = playerState.get();
|
|
897
|
-
|
|
898
|
-
// Listen to changes with debug info
|
|
899
|
-
playerState.on('change', (newPlayer, oldPlayer) => {
|
|
900
|
-
console.log('Player state changed:', newPlayer);
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
```
|
|
905
|
-
|
|
906
|
-
### What Debug Mode Shows
|
|
907
|
-
|
|
908
|
-
When debug mode is enabled, you'll see detailed logs in your browser's developer console for:
|
|
909
|
-
|
|
910
|
-
- **State Initialization**: When a state is first created
|
|
911
|
-
- **State Updates**: When values are set with old and new values
|
|
912
|
-
- **State Retrieval**: When values are accessed
|
|
913
|
-
- **Event Listeners**: When listeners are added, removed, or cleared
|
|
914
|
-
- **Validation**: When validators are applied and their results
|
|
915
|
-
- **Errors**: Detailed error information with context
|
|
916
|
-
|
|
917
|
-
### Viewing Debug Logs
|
|
321
|
+
export const withPlayer = (scene: Phaser.Scene) => {
|
|
322
|
+
const playerState = withLocalState<{ hp: number; level: number }>(
|
|
323
|
+
this,
|
|
324
|
+
'player',
|
|
325
|
+
{
|
|
326
|
+
hp: 100,
|
|
327
|
+
level: 1,
|
|
328
|
+
},
|
|
329
|
+
{ debug: true }, // Enable debug logging
|
|
330
|
+
);
|
|
918
331
|
|
|
919
|
-
|
|
332
|
+
return playerState;
|
|
333
|
+
}
|
|
920
334
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
4. Look for logs prefixed with `[phaser-hooks]`
|
|
335
|
+
// in your scene
|
|
336
|
+
playerState.patch((current) => ({ hp: current.hp - 10 })); // Log here
|
|
337
|
+
```
|
|
925
338
|
|
|
926
|
-

|
|
927
|
-
*Screenshot showing debug logs in browser console*
|
|
928
339
|
|
|
929
|
-
|
|
340
|
+
## Hook API Reference
|
|
930
341
|
|
|
931
|
-
|
|
342
|
+
All hooks return a `HookState<T>` object with the following methods:
|
|
932
343
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
344
|
+
| Method | Description | Parameters | Returns |
|
|
345
|
+
|--------|-------------|------------|---------|
|
|
346
|
+
| `get()` | Gets the current state value | None | `T` - Current state value |
|
|
347
|
+
| `set(value)` | Sets a new state value and triggers change listeners | `value: T \| ((current: T) => T)` | `void` |
|
|
348
|
+
| `patch(value)` | Patches object state with partial updates (deep merge) | `value: Partial<T> \| ((current: T) => Partial<T>)` | `void` |
|
|
349
|
+
| `on('change', callback)` | Registers a callback for state changes | `callback: (newValue: T, oldValue: T) => void` | `() => void` - Unsubscribe function |
|
|
350
|
+
| `once('change', callback)` | Registers a callback that fires only once | `callback: (newValue: T, oldValue: T) => void` | `() => void` - Unsubscribe function |
|
|
351
|
+
| `off('change', callback)` | Removes a specific event listener | `callback: (newValue: T, oldValue: T) => void` | `void` |
|
|
352
|
+
| `clearListeners()` | Removes all event listeners for this state | None | `void` |
|
|
939
353
|
|
|
940
|
-
###
|
|
354
|
+
### Notes
|
|
941
355
|
|
|
942
|
-
-
|
|
943
|
-
-
|
|
944
|
-
-
|
|
945
|
-
-
|
|
356
|
+
- **`set()`** accepts either a value or an updater function for safe updates
|
|
357
|
+
- **`patch()`** only works with object states and performs deep merging
|
|
358
|
+
- **`on()`/`once()`/`off()`** only support the `'change'` event
|
|
359
|
+
- **`off()`** requires the exact same function reference that was passed to `on()`
|
|
946
360
|
|
|
947
|
-
### Example:
|
|
361
|
+
### Example:
|
|
948
362
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
create() {
|
|
952
|
-
// Enable debug for problematic state
|
|
953
|
-
const inventoryState = withLocalState<string[]>(
|
|
954
|
-
this,
|
|
955
|
-
'inventory',
|
|
956
|
-
[],
|
|
957
|
-
{ debug: true }
|
|
958
|
-
);
|
|
959
|
-
|
|
960
|
-
// Debug logs will show exactly what's happening
|
|
961
|
-
inventoryState.set(['sword', 'potion']);
|
|
962
|
-
inventoryState.set([...inventoryState.get(), 'shield']);
|
|
963
|
-
|
|
964
|
-
// Check console for detailed operation logs
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
```
|
|
363
|
+

|
|
364
|
+
*Screenshot showing debug logs in the browser console*
|
|
968
365
|
|
|
969
366
|
## License
|
|
970
367
|
|