phaser-hooks 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +465 -22
- package/dist/hooks/type.d.ts +4 -0
- package/dist/hooks/type.d.ts.map +1 -1
- package/dist/hooks/with-computed-state.d.ts.map +1 -1
- package/dist/hooks/with-computed-state.js +1 -5
- package/dist/hooks/with-computed-state.js.map +1 -1
- package/dist/hooks/with-debounced-state.d.ts.map +1 -1
- package/dist/hooks/with-debounced-state.js +1 -5
- package/dist/hooks/with-debounced-state.js.map +1 -1
- package/dist/hooks/with-global-state.spec.js +131 -0
- package/dist/hooks/with-global-state.spec.js.map +1 -1
- package/dist/hooks/with-local-state.spec.js +295 -6
- package/dist/hooks/with-local-state.spec.js.map +1 -1
- package/dist/hooks/with-state-def.d.ts +2 -0
- package/dist/hooks/with-state-def.d.ts.map +1 -1
- package/dist/hooks/with-state-def.js +30 -43
- package/dist/hooks/with-state-def.js.map +1 -1
- package/dist/hooks/with-undoable-state.d.ts.map +1 -1
- package/dist/hooks/with-undoable-state.js +1 -5
- package/dist/hooks/with-undoable-state.js.map +1 -1
- package/dist/utils/logger.d.ts +54 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +172 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logger.spec.d.ts +2 -0
- package/dist/utils/logger.spec.d.ts.map +1 -0
- package/dist/utils/logger.spec.js +198 -0
- package/dist/utils/logger.spec.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="data/image.png" alt="logo" style="max-width: 300px">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/phaser-wind)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
1
9
|
# Phaser Hooks (like "use" hooks in React)
|
|
2
10
|
|
|
3
11
|
A comprehensive state management library for Phaser games with React-like hooks pattern.
|
|
@@ -22,9 +30,14 @@ this.game.registry.events.on('changedata-volume', (game, value) => {
|
|
|
22
30
|
// Using scene.data (local)
|
|
23
31
|
this.data.set('score', 42); // too boring too
|
|
24
32
|
const score = this.data.get('score');
|
|
25
|
-
|
|
33
|
+
|
|
34
|
+
const onChangeFn = (scene, value) => {
|
|
26
35
|
console.log('Score updated to', value);
|
|
27
|
-
}
|
|
36
|
+
};
|
|
37
|
+
this.data.events.on('changedata-score', onChangeFn); // If you pass an anonymous function, you cannot unsubscribe :(
|
|
38
|
+
|
|
39
|
+
// when move to another scene, you must unsubscribe. Boring and easy to forget
|
|
40
|
+
this.data.events.off('changeset-score', onChangeFn);
|
|
28
41
|
```
|
|
29
42
|
|
|
30
43
|
With _phaser-hooks_, you get a simple, React-like API:
|
|
@@ -34,7 +47,13 @@ With _phaser-hooks_, you get a simple, React-like API:
|
|
|
34
47
|
const volume = withGlobalState(scene, 'volume', 0.5); // woow! awesome
|
|
35
48
|
volume.get(); // 0.5
|
|
36
49
|
volume.set(0.8); // updates value
|
|
37
|
-
|
|
50
|
+
|
|
51
|
+
const unsubscribe = volume.on('change', v =>
|
|
52
|
+
console.log('Volume changed →', v)
|
|
53
|
+
); // Nice callback in event <3 - Return the easy unsubscribe function
|
|
54
|
+
|
|
55
|
+
// when move to another scene, just call :)
|
|
56
|
+
unsubscribe();
|
|
38
57
|
|
|
39
58
|
// Persisted state (localStorage / sessionStorage)
|
|
40
59
|
const score = withPersistState(scene, 'score', 0, { storage: 'local' }); // Wow! Saving in localStorage
|
|
@@ -59,6 +78,33 @@ Since this library is designed to work with Phaser games, which typically use pl
|
|
|
59
78
|
|
|
60
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.
|
|
61
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` - New value to set | `void` |
|
|
89
|
+
| `on('change', callback)` | Registers a callback for state changes | `event: 'change'`, `callback: () => void` | `() => void` - Unsubscribe function |
|
|
90
|
+
| `once('change', callback)` | Registers a callback that fires only once | `event: 'change'`, `callback: () => void` | `() => void` - Unsubscribe function |
|
|
91
|
+
| `off('change', callback)` | Removes an event listener | `event: 'change'`, `callback: () => void` | `void` |
|
|
92
|
+
| `clearListeners()` | Removes all event listeners for this state | None | `void` |
|
|
93
|
+
|
|
94
|
+
### Special Hook Methods
|
|
95
|
+
|
|
96
|
+
Some hooks have additional methods beyond the standard `HookState` interface:
|
|
97
|
+
|
|
98
|
+
#### `withUndoableState` Additional Methods:
|
|
99
|
+
|
|
100
|
+
| Method | Description | Parameters | Returns |
|
|
101
|
+
| ---------------- | ----------------------------- | ---------- | -------------------------- |
|
|
102
|
+
| `undo()` | Reverts to the previous state | None | `boolean` - Success status |
|
|
103
|
+
| `redo()` | Advances to the next state | None | `boolean` - Success status |
|
|
104
|
+
| `canUndo()` | Checks if undo is available | None | `boolean` |
|
|
105
|
+
| `canRedo()` | Checks if redo is available | None | `boolean` |
|
|
106
|
+
| `clearHistory()` | Clears the undo/redo history | None | `void` |
|
|
107
|
+
|
|
62
108
|
## Available Hooks
|
|
63
109
|
|
|
64
110
|
### Core Hooks
|
|
@@ -68,6 +114,12 @@ This approach allows you to use these state management utilities in your Phaser
|
|
|
68
114
|
Scene-specific state management that gets cleaned up when the scene is destroyed.
|
|
69
115
|
|
|
70
116
|
```typescript
|
|
117
|
+
type PlayerData = {
|
|
118
|
+
hp: number;
|
|
119
|
+
level: number;
|
|
120
|
+
exp: number;
|
|
121
|
+
};
|
|
122
|
+
|
|
71
123
|
const playerState = withLocalState<PlayerData>(scene, 'player', {
|
|
72
124
|
hp: 100,
|
|
73
125
|
level: 1,
|
|
@@ -80,24 +132,17 @@ const playerState = withLocalState<PlayerData>(scene, 'player', {
|
|
|
80
132
|
Application-wide state that persists across all scenes.
|
|
81
133
|
|
|
82
134
|
```typescript
|
|
135
|
+
type GameSettings = {
|
|
136
|
+
soundVolume: number;
|
|
137
|
+
musicEnabled: true;
|
|
138
|
+
};
|
|
139
|
+
|
|
83
140
|
const settingsState = withGlobalState<GameSettings>(scene, 'settings', {
|
|
84
141
|
soundVolume: 0.8,
|
|
85
142
|
musicEnabled: true,
|
|
86
143
|
});
|
|
87
144
|
```
|
|
88
145
|
|
|
89
|
-
#### `withStateDef`
|
|
90
|
-
|
|
91
|
-
Low-level state definition with custom behaviors and validation.
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
const customState = withStateDef<number>(scene, 'score', {
|
|
95
|
-
initialValue: 0,
|
|
96
|
-
validator: value => value >= 0,
|
|
97
|
-
onChange: (newValue, oldValue) => console.log('Score changed!'),
|
|
98
|
-
});
|
|
99
|
-
```
|
|
100
|
-
|
|
101
146
|
### Enhanced Hooks
|
|
102
147
|
|
|
103
148
|
#### `withPersistentState`
|
|
@@ -105,6 +150,11 @@ const customState = withStateDef<number>(scene, 'score', {
|
|
|
105
150
|
State with automatic localStorage persistence.
|
|
106
151
|
|
|
107
152
|
```typescript
|
|
153
|
+
type UserSettings = {
|
|
154
|
+
volume: number;
|
|
155
|
+
difficulty: 'easy' | 'normal' | 'hard';
|
|
156
|
+
};
|
|
157
|
+
|
|
108
158
|
const persistentSettings = withPersistentState<UserSettings>(
|
|
109
159
|
'settings',
|
|
110
160
|
{
|
|
@@ -163,13 +213,40 @@ Pre-built validation functions for common patterns.
|
|
|
163
213
|
```typescript
|
|
164
214
|
import { validators } from 'phaser-hooks';
|
|
165
215
|
|
|
166
|
-
|
|
216
|
+
// Number range validation (0-1000)
|
|
217
|
+
const scoreState = withGlobalState<number>(scene, 'score', 0, {
|
|
167
218
|
validator: validators.numberRange(0, 1000),
|
|
168
219
|
});
|
|
169
220
|
|
|
170
|
-
|
|
221
|
+
// Non-empty string validation
|
|
222
|
+
const nameState = withGlobalState<string>(scene, 'name', '', {
|
|
171
223
|
validator: validators.nonEmptyString,
|
|
172
224
|
});
|
|
225
|
+
|
|
226
|
+
// Array length validation (2-4 items)
|
|
227
|
+
const inventoryState = withLocalState<string[]>(scene, 'inventory', [], {
|
|
228
|
+
validator: validators.arrayLength(2, 4),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// One of allowed values validation
|
|
232
|
+
const difficultyState = withGlobalState<'easy' | 'normal' | 'hard'>(
|
|
233
|
+
scene,
|
|
234
|
+
'difficulty',
|
|
235
|
+
'normal',
|
|
236
|
+
{
|
|
237
|
+
validator: validators.oneOf(['easy', 'normal', 'hard']),
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Custom validator example
|
|
242
|
+
const healthState = withLocalState<number>(scene, 'health', 100, {
|
|
243
|
+
validator: value => {
|
|
244
|
+
const health = value as number;
|
|
245
|
+
if (health < 0) return 'Health cannot be negative';
|
|
246
|
+
if (health > 100) return 'Health cannot exceed 100';
|
|
247
|
+
return true; // Valid
|
|
248
|
+
},
|
|
249
|
+
});
|
|
173
250
|
```
|
|
174
251
|
|
|
175
252
|
#### `batchStateUpdates`
|
|
@@ -211,7 +288,7 @@ export class GameScene extends Phaser.Scene {
|
|
|
211
288
|
);
|
|
212
289
|
|
|
213
290
|
// Listen to changes
|
|
214
|
-
playerState.
|
|
291
|
+
const ubsubscribe = playerState.on('change', (newPlayer, oldPlayer) => {
|
|
215
292
|
console.log('Player health changed:', newPlayer.hp);
|
|
216
293
|
});
|
|
217
294
|
|
|
@@ -313,8 +390,7 @@ function withPlayerEnergy(scene: Phaser.Scene) {
|
|
|
313
390
|
return {
|
|
314
391
|
get: () => player.get().energy,
|
|
315
392
|
set: (value: number) => player.set({ ...player.get(), energy: value }),
|
|
316
|
-
|
|
317
|
-
player.onChange(newVal => fn(newVal.energy)),
|
|
393
|
+
...player,
|
|
318
394
|
};
|
|
319
395
|
}
|
|
320
396
|
```
|
|
@@ -328,13 +404,281 @@ console.log('Current energy:', energy.get());
|
|
|
328
404
|
|
|
329
405
|
energy.set(energy.get() - 10);
|
|
330
406
|
|
|
331
|
-
energy.
|
|
332
|
-
if (
|
|
407
|
+
energy.on('change', () => {
|
|
408
|
+
if (energy.get() <= 0) {
|
|
333
409
|
console.warn('You are out of energy!');
|
|
334
410
|
}
|
|
335
411
|
});
|
|
336
412
|
```
|
|
337
413
|
|
|
414
|
+
## Unsubscribe Events
|
|
415
|
+
|
|
416
|
+
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.
|
|
417
|
+
|
|
418
|
+
### Method 1: Using the Return Value from `.on('change')`
|
|
419
|
+
|
|
420
|
+
The `.on('change', callback)` method returns an unsubscribe function that you can call to remove the listener:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
export class GameScene extends Phaser.Scene {
|
|
424
|
+
create() {
|
|
425
|
+
const playerState = withLocalState<{ hp: number }>(this, 'player', {
|
|
426
|
+
hp: 100,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Subscribe to changes and get unsubscribe function
|
|
430
|
+
const unsubscribe = playerState.on('change', (newPlayer, oldPlayer) => {
|
|
431
|
+
console.log('Player health changed:', newPlayer.hp);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
this.add
|
|
435
|
+
.text(centerX, centerY, 'Go to another scene')
|
|
436
|
+
.setInteractive()
|
|
437
|
+
.on('pointerdown', () => {
|
|
438
|
+
// Later, unsubscribe when needed
|
|
439
|
+
unsubscribe();
|
|
440
|
+
|
|
441
|
+
// To switch to another scene in Phaser, use:
|
|
442
|
+
this.scene.start('OtherSceneKey');
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Method 2: Using `.off('change', callback)`
|
|
449
|
+
|
|
450
|
+
You can also unsubscribe by passing the same callback function to `.off('change', callback)`:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
export class GameScene extends Phaser.Scene {
|
|
454
|
+
private healthCallback?: (newPlayer: any, oldPlayer: any) => void;
|
|
455
|
+
|
|
456
|
+
create() {
|
|
457
|
+
const playerState = withLocalState<{ hp: number }>(this, 'player', {
|
|
458
|
+
hp: 100,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Define callback function
|
|
462
|
+
this.healthCallback = (newPlayer, oldPlayer) => {
|
|
463
|
+
console.log('Player health changed:', newPlayer.hp);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// Subscribe to changes
|
|
467
|
+
playerState.on('change', this.healthCallback);
|
|
468
|
+
|
|
469
|
+
this.add
|
|
470
|
+
.text(centerX, centerY, 'Go to another scene')
|
|
471
|
+
.setInteractive()
|
|
472
|
+
.on('pointerdown', () => {
|
|
473
|
+
// Later, unsubscribe when needed
|
|
474
|
+
playerState.off('change', this.healthCallback);
|
|
475
|
+
|
|
476
|
+
// To switch to another scene in Phaser, use:
|
|
477
|
+
this.scene.start('OtherSceneKey');
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
> **Note:** When using `.off`, you must pass the exact same function instance that was used with `.on`. This means you cannot use an inline closure or anonymous function—use a named function or store the callback reference to unsubscribe properly.
|
|
484
|
+
|
|
485
|
+
### Best Practices for Scene Cleanup
|
|
486
|
+
|
|
487
|
+
**⚠️ IMPORTANT DISCLAIMER**: If you don't clean up event listeners when leaving a scene, you may encounter:
|
|
488
|
+
|
|
489
|
+
- Memory leaks
|
|
490
|
+
- Unexpected behavior when returning to the scene
|
|
491
|
+
- Callbacks firing on destroyed or inactive scenes
|
|
492
|
+
- Performance issues over time
|
|
493
|
+
|
|
494
|
+
Always unsubscribe from events when transitioning between scenes:
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
export class GameScene extends Phaser.Scene {
|
|
498
|
+
private unsubscribeFunctions: (() => void)[] = [];
|
|
499
|
+
|
|
500
|
+
create() {
|
|
501
|
+
const playerState = withLocalState<{ hp: number }>(this, 'player', {
|
|
502
|
+
hp: 100,
|
|
503
|
+
});
|
|
504
|
+
const scoreState = withGlobalState<number>(this, 'score', 0);
|
|
505
|
+
|
|
506
|
+
// Store unsubscribe functions
|
|
507
|
+
this.unsubscribeFunctions.push(
|
|
508
|
+
playerState.on('change', newPlayer => {
|
|
509
|
+
console.log('Player updated:', newPlayer);
|
|
510
|
+
})
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
this.unsubscribeFunctions.push(
|
|
514
|
+
scoreState.on('change', newScore => {
|
|
515
|
+
console.log('Score updated:', newScore);
|
|
516
|
+
})
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Clean up when scene is destroyed or when transitioning
|
|
521
|
+
shutdown() {
|
|
522
|
+
// Unsubscribe from all events
|
|
523
|
+
this.unsubscribeFunctions.forEach(unsubscribe => unsubscribe());
|
|
524
|
+
this.unsubscribeFunctions = [];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Or clean up before transitioning to another scene
|
|
528
|
+
goToNextScene() {
|
|
529
|
+
// Clean up before changing scenes
|
|
530
|
+
this.unsubscribeFunctions.forEach(unsubscribe => unsubscribe());
|
|
531
|
+
this.unsubscribeFunctions = [];
|
|
532
|
+
|
|
533
|
+
// Then transition
|
|
534
|
+
this.scene.start('NextScene');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Using `clearListeners()` for Easy Cleanup
|
|
540
|
+
|
|
541
|
+
For easier cleanup, you can use the `clearListeners()` method to remove all event listeners at once:
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
export class GameScene extends Phaser.Scene {
|
|
545
|
+
private playerState: HookState<{ hp: number }>;
|
|
546
|
+
private scoreState: HookState<number>;
|
|
547
|
+
|
|
548
|
+
create() {
|
|
549
|
+
this.playerState = withLocalState<{ hp: number }>(this, 'player', { hp: 100 });
|
|
550
|
+
this.scoreState = withGlobalState<number>(this, 'score', 0);
|
|
551
|
+
|
|
552
|
+
// Add listeners
|
|
553
|
+
this.playerState.on('change', (newPlayer) => {
|
|
554
|
+
console.log('Player updated:', newPlayer);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
this.scoreState.on('change', (newScore) => {
|
|
558
|
+
console.log('Score updated:', newScore);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
shutdown() {
|
|
563
|
+
// Clear all listeners at once - much easier!
|
|
564
|
+
this.playerState.clearListeners();
|
|
565
|
+
this.scoreState.clearListeners();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
#### Important Notes about `clearListeners()`:
|
|
571
|
+
|
|
572
|
+
- **`withLocalState`**: Automatically cleans up when the scene is destroyed, but you can still use `clearListeners()` for manual cleanup
|
|
573
|
+
- **`withGlobalState`**: **Requires manual cleanup** since global state persists across scenes. Always call `clearListeners()` when the scene is destroyed:
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
export class GameScene extends Phaser.Scene {
|
|
577
|
+
private globalState: HookState<GameSettings>;
|
|
578
|
+
|
|
579
|
+
create() {
|
|
580
|
+
this.globalState = withGlobalState<GameSettings>(this, 'settings', defaultSettings);
|
|
581
|
+
|
|
582
|
+
this.globalState.on('change', (newSettings) => {
|
|
583
|
+
console.log('Settings updated:', newSettings);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// IMPORTANT: Clean up global state listeners when scene is destroyed
|
|
587
|
+
this.events.once('destroy', () => {
|
|
588
|
+
this.globalState.clearListeners();
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Multiple Subscriptions Example
|
|
595
|
+
|
|
596
|
+
You can have multiple listeners for the same state:
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
export class GameScene extends Phaser.Scene {
|
|
600
|
+
create() {
|
|
601
|
+
const playerState = withLocalState<{ hp: number; level: number }>(
|
|
602
|
+
this,
|
|
603
|
+
'player',
|
|
604
|
+
{
|
|
605
|
+
hp: 100,
|
|
606
|
+
level: 1,
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
// Multiple listeners for the same state
|
|
611
|
+
const unsubscribeHealth = playerState.on('change', newPlayer => {
|
|
612
|
+
console.log('Health changed:', newPlayer.hp);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
const unsubscribeLevel = playerState.on('change', newPlayer => {
|
|
616
|
+
console.log('Level changed:', newPlayer.level);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Unsubscribe specific listeners
|
|
620
|
+
unsubscribeHealth(); // Only removes health listener
|
|
621
|
+
// unsubscribeLevel still active
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Using `.once()` for One-Time Events
|
|
627
|
+
|
|
628
|
+
The `.once()` method registers a callback that will only fire once, then automatically unsubscribes:
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
export class GameScene extends Phaser.Scene {
|
|
632
|
+
create() {
|
|
633
|
+
const playerState = withLocalState<{ hp: number; level: number }>(
|
|
634
|
+
this,
|
|
635
|
+
'player',
|
|
636
|
+
{
|
|
637
|
+
hp: 100,
|
|
638
|
+
level: 1,
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// One-time listener - fires only once then auto-unsubscribes
|
|
643
|
+
const unsubscribeOnce = playerState.once('change', newPlayer => {
|
|
644
|
+
console.log('First level up detected!', newPlayer.level);
|
|
645
|
+
// This callback will only run once, even if the state changes multiple times
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// You can still manually unsubscribe if needed before it fires
|
|
649
|
+
// unsubscribeOnce();
|
|
650
|
+
|
|
651
|
+
// Simulate level up
|
|
652
|
+
playerState.set({ hp: 100, level: 2 }); // Fires the once callback
|
|
653
|
+
playerState.set({ hp: 100, level: 3 }); // Won't fire the once callback again
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### Validation Error Handling
|
|
659
|
+
|
|
660
|
+
When using validators, invalid values will throw errors. Handle them appropriately:
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
export class GameScene extends Phaser.Scene {
|
|
664
|
+
create() {
|
|
665
|
+
const healthState = withLocalState<number>(this, 'health', 100, {
|
|
666
|
+
validator: validators.numberRange(0, 100),
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
healthState.set(150); // This will throw an error: "Value must be between 0 and 100"
|
|
671
|
+
} catch (error) {
|
|
672
|
+
console.error('Invalid health value:', error.message);
|
|
673
|
+
// Handle the error appropriately
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Valid value
|
|
677
|
+
healthState.set(75); // This works fine
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
338
682
|
### Why use this pattern?
|
|
339
683
|
|
|
340
684
|
✅ Keeps your scene code focused on intent (e.g., energy.get()) rather than structure (player.get().energy)
|
|
@@ -368,6 +712,105 @@ const playerState = withLocalState<PlayerData>(scene, 'player', {
|
|
|
368
712
|
const currentPlayer: PlayerData = playerState.get();
|
|
369
713
|
```
|
|
370
714
|
|
|
715
|
+
## Debug Mode / Dev tool
|
|
716
|
+
|
|
717
|
+
Phaser Hooks includes a built-in debug mode that provides detailed logging for state operations. This is extremely useful for development and troubleshooting state management issues.
|
|
718
|
+
|
|
719
|
+
### How to Enable Debug Mode
|
|
720
|
+
|
|
721
|
+
To enable debug mode, simply pass `{ debug: true }` in the options parameter when creating any hook:
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
import { withLocalState } from 'phaser-hooks';
|
|
725
|
+
|
|
726
|
+
export class GameScene extends Phaser.Scene {
|
|
727
|
+
create() {
|
|
728
|
+
// Enable debug mode for this state
|
|
729
|
+
const playerState = withLocalState<{ hp: number; level: number }>(
|
|
730
|
+
this,
|
|
731
|
+
'player',
|
|
732
|
+
{
|
|
733
|
+
hp: 100,
|
|
734
|
+
level: 1,
|
|
735
|
+
},
|
|
736
|
+
{ debug: true } // Enable debug logging
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
// All operations will now be logged to the console
|
|
740
|
+
playerState.set({ hp: 90, level: 2 });
|
|
741
|
+
const currentPlayer = playerState.get();
|
|
742
|
+
|
|
743
|
+
// Listen to changes with debug info
|
|
744
|
+
playerState.on('change', (newPlayer, oldPlayer) => {
|
|
745
|
+
console.log('Player state changed:', newPlayer);
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### What Debug Mode Shows
|
|
752
|
+
|
|
753
|
+
When debug mode is enabled, you'll see detailed logs in your browser's developer console for:
|
|
754
|
+
|
|
755
|
+
- **State Initialization**: When a state is first created
|
|
756
|
+
- **State Updates**: When values are set with old and new values
|
|
757
|
+
- **State Retrieval**: When values are accessed
|
|
758
|
+
- **Event Listeners**: When listeners are added, removed, or cleared
|
|
759
|
+
- **Validation**: When validators are applied and their results
|
|
760
|
+
- **Errors**: Detailed error information with context
|
|
761
|
+
|
|
762
|
+
### Viewing Debug Logs
|
|
763
|
+
|
|
764
|
+
Debug logs appear in your browser's developer console. To view them:
|
|
765
|
+
|
|
766
|
+
1. Open your browser's Developer Tools (F12 or right-click → Inspect)
|
|
767
|
+
2. Go to the **Console** tab
|
|
768
|
+
3. Run your Phaser game
|
|
769
|
+
4. Look for logs prefixed with `[phaser-hooks]`
|
|
770
|
+
|
|
771
|
+

|
|
772
|
+
*Screenshot showing debug logs in browser console*
|
|
773
|
+
|
|
774
|
+
### Debug Log Format
|
|
775
|
+
|
|
776
|
+
Debug logs follow a consistent format with timestamps and structured information:
|
|
777
|
+
|
|
778
|
+
```
|
|
779
|
+
[phaser-hooks] 2024-01-15 10:30:45 [INIT] player - Initializing state with value: {hp: 100, level: 1}
|
|
780
|
+
[phaser-hooks] 2024-01-15 10:30:46 [SET] player - Updating state: {hp: 90, level: 2} (was: {hp: 100, level: 1})
|
|
781
|
+
[phaser-hooks] 2024-01-15 10:30:47 [GET] player - Retrieved state: {hp: 90, level: 2}
|
|
782
|
+
[phaser-hooks] 2024-01-15 10:30:48 [EVENT] player - Added change listener
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Best Practices for Debug Mode
|
|
786
|
+
|
|
787
|
+
- **Development Only**: Only enable debug mode during development. Remove `{ debug: true }` in production builds
|
|
788
|
+
- **Selective Debugging**: Enable debug mode only for the specific states you're troubleshooting
|
|
789
|
+
- **Performance**: Debug mode adds overhead, so avoid enabling it for all states in production
|
|
790
|
+
- **Console Filtering**: Use browser console filters to focus on specific log types
|
|
791
|
+
|
|
792
|
+
### Example: Debugging State Issues
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
export class DebugScene extends Phaser.Scene {
|
|
796
|
+
create() {
|
|
797
|
+
// Enable debug for problematic state
|
|
798
|
+
const inventoryState = withLocalState<string[]>(
|
|
799
|
+
this,
|
|
800
|
+
'inventory',
|
|
801
|
+
[],
|
|
802
|
+
{ debug: true }
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
// Debug logs will show exactly what's happening
|
|
806
|
+
inventoryState.set(['sword', 'potion']);
|
|
807
|
+
inventoryState.set([...inventoryState.get(), 'shield']);
|
|
808
|
+
|
|
809
|
+
// Check console for detailed operation logs
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
```
|
|
813
|
+
|
|
371
814
|
## License
|
|
372
815
|
|
|
373
816
|
MIT
|
package/dist/hooks/type.d.ts
CHANGED
|
@@ -71,5 +71,9 @@ export type HookState<T> = {
|
|
|
71
71
|
* @param callback The callback to remove
|
|
72
72
|
*/
|
|
73
73
|
off: (event: 'change', callback: () => void) => void;
|
|
74
|
+
/**
|
|
75
|
+
* Removes all event listeners for this state
|
|
76
|
+
*/
|
|
77
|
+
clearListeners: () => void;
|
|
74
78
|
};
|
|
75
79
|
//# sourceMappingURL=type.d.ts.map
|
package/dist/hooks/type.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../src/hooks/type.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC;AAExE;;;;GAIG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;AAElD;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC,CAAC;IAEb;;;OAGG;IACH,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAExB;;;OAGG;IACH,QAAQ,EAAE,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAErD;;;OAGG;IACH,EAAE,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAE1D;;;OAGG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAE5D;;;;OAIG;IACH,GAAG,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../src/hooks/type.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC;AAExE;;;;GAIG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;AAElD;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC,CAAC;IAEb;;;OAGG;IACH,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAExB;;;OAGG;IACH,QAAQ,EAAE,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAErD;;;OAGG;IACH,EAAE,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAE1D;;;OAGG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAE5D;;;;OAIG;IACH,GAAG,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAErD;;OAEG;IACH,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"with-computed-state.d.ts","sourceRoot":"","sources":["../../src/hooks/with-computed-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AAG5D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,CAAC,EACpC,OAAO,MAAM,CAAC,KAAK,EACnB,KAAK,MAAM,EACX,aAAa,SAAS,CAAC,CAAC,CAAC,EACzB,UAAU,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,KAC5B,SAAS,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"with-computed-state.d.ts","sourceRoot":"","sources":["../../src/hooks/with-computed-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AAG5D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,CAAC,EACpC,OAAO,MAAM,CAAC,KAAK,EACnB,KAAK,MAAM,EACX,aAAa,SAAS,CAAC,CAAC,CAAC,EACzB,UAAU,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,KAC5B,SAAS,CAAC,CAAC,CAoBb,CAAC"}
|
|
@@ -31,14 +31,10 @@ export const withComputedState = (scene, key, sourceState, selector) => {
|
|
|
31
31
|
computedState.set(newComputedValue);
|
|
32
32
|
});
|
|
33
33
|
return {
|
|
34
|
-
|
|
34
|
+
...computedState,
|
|
35
35
|
set: () => {
|
|
36
36
|
throw new Error(`[withComputedState] Cannot directly set computed state "${key}". Update the source state instead.`);
|
|
37
37
|
},
|
|
38
|
-
onChange: computedState.onChange,
|
|
39
|
-
on: computedState.on,
|
|
40
|
-
once: computedState.once,
|
|
41
|
-
off: computedState.off,
|
|
42
38
|
};
|
|
43
39
|
};
|
|
44
40
|
//# sourceMappingURL=with-computed-state.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"with-computed-state.js","sourceRoot":"","sources":["../../src/hooks/with-computed-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,KAAmB,EACnB,GAAW,EACX,WAAyB,EACzB,QAA6B,EACf,EAAE;IAChB,iCAAiC;IACjC,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC;IACjD,MAAM,aAAa,GAAG,cAAc,CAAI,KAAK,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC;IAElE,4CAA4C;IAC5C,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QAC5B,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QACzC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;QAClD,aAAa,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,GAAG,
|
|
1
|
+
{"version":3,"file":"with-computed-state.js","sourceRoot":"","sources":["../../src/hooks/with-computed-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,KAAmB,EACnB,GAAW,EACX,WAAyB,EACzB,QAA6B,EACf,EAAE;IAChB,iCAAiC;IACjC,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC;IACjD,MAAM,aAAa,GAAG,cAAc,CAAI,KAAK,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC;IAElE,4CAA4C;IAC5C,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QAC5B,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QACzC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;QAClD,aAAa,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,GAAG,aAAa;QAChB,GAAG,EAAE,GAAS,EAAE;YACd,MAAM,IAAI,KAAK,CACb,2DAA2D,GAAG,qCAAqC,CACpG,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"with-debounced-state.d.ts","sourceRoot":"","sources":["../../src/hooks/with-debounced-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;AAGxC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAClC,OAAO,MAAM,CAAC,KAAK,EACnB,KAAK,MAAM,EACX,cAAc,CAAC,EACf,YAAY,MAAM,KACjB,SAAS,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"with-debounced-state.d.ts","sourceRoot":"","sources":["../../src/hooks/with-debounced-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;AAGxC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAClC,OAAO,MAAM,CAAC,KAAK,EACnB,KAAK,MAAM,EACX,cAAc,CAAC,EACf,YAAY,MAAM,KACjB,SAAS,CAAC,CAAC,CAmBb,CAAC"}
|
|
@@ -31,12 +31,8 @@ export const withDebouncedState = (scene, key, initialValue, debounceMs) => {
|
|
|
31
31
|
}, debounceMs);
|
|
32
32
|
};
|
|
33
33
|
return {
|
|
34
|
-
|
|
34
|
+
...actualState,
|
|
35
35
|
set: debouncedSet,
|
|
36
|
-
onChange: actualState.onChange,
|
|
37
|
-
on: actualState.on,
|
|
38
|
-
once: actualState.once,
|
|
39
|
-
off: actualState.off,
|
|
40
36
|
};
|
|
41
37
|
};
|
|
42
38
|
//# sourceMappingURL=with-debounced-state.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"with-debounced-state.js","sourceRoot":"","sources":["../../src/hooks/with-debounced-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,KAAmB,EACnB,GAAW,EACX,YAAe,EACf,UAAkB,EACJ,EAAE;IAChB,MAAM,WAAW,GAAG,cAAc,CAAI,KAAK,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC;IAChE,IAAI,SAAS,GAAyC,IAAI,CAAC;IAE3D,MAAM,YAAY,GAAG,CAAC,KAAQ,EAAQ,EAAE;QACtC,IAAI,SAAS,EAAE,CAAC;YACd,YAAY,CAAC,SAAS,CAAC,CAAC;QAC1B,CAAC;QAED,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;YAC1B,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACvB,SAAS,GAAG,IAAI,CAAC;QACnB,CAAC,EAAE,UAAU,CAAC,CAAC;IACjB,CAAC,CAAC;IAEF,OAAO;QACL,GAAG,
|
|
1
|
+
{"version":3,"file":"with-debounced-state.js","sourceRoot":"","sources":["../../src/hooks/with-debounced-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,KAAmB,EACnB,GAAW,EACX,YAAe,EACf,UAAkB,EACJ,EAAE;IAChB,MAAM,WAAW,GAAG,cAAc,CAAI,KAAK,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC;IAChE,IAAI,SAAS,GAAyC,IAAI,CAAC;IAE3D,MAAM,YAAY,GAAG,CAAC,KAAQ,EAAQ,EAAE;QACtC,IAAI,SAAS,EAAE,CAAC;YACd,YAAY,CAAC,SAAS,CAAC,CAAC;QAC1B,CAAC;QAED,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;YAC1B,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACvB,SAAS,GAAG,IAAI,CAAC;QACnB,CAAC,EAAE,UAAU,CAAC,CAAC;IACjB,CAAC,CAAC;IAEF,OAAO;QACL,GAAG,WAAW;QACd,GAAG,EAAE,YAAY;KAClB,CAAC;AACJ,CAAC,CAAC"}
|