phaser-hooks 0.3.0 → 0.5.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 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
+ [![NPM Version](https://img.shields.io/npm/v/phaser-wind)](https://www.npmjs.com/package/phaser-wind)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](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
- this.data.events.on('changedata-score', (scene, value) => {
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
- volume.onChange(v => console.log('Volume changed →', v)); // Nice callback <3
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,177 @@ 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 \| ((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
+ ```
205
+
206
+ **Deep object patching:**
207
+
208
+ ```typescript
209
+ // Patch deeply nested properties
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
222
+ ```
223
+
224
+ **Array property patching:**
225
+
226
+ ```typescript
227
+ // Update array properties
228
+ inventoryState.patch({
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
+ }));
236
+ ```
237
+
238
+ ### Special Hook Methods
239
+
240
+ Some hooks have additional methods beyond the standard `HookState` interface:
241
+
242
+ #### `withUndoableState` Additional Methods:
243
+
244
+ | Method | Description | Parameters | Returns |
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` |
251
+
62
252
  ## Available Hooks
63
253
 
64
254
  ### Core Hooks
@@ -68,6 +258,12 @@ This approach allows you to use these state management utilities in your Phaser
68
258
  Scene-specific state management that gets cleaned up when the scene is destroyed.
69
259
 
70
260
  ```typescript
261
+ type PlayerData = {
262
+ hp: number;
263
+ level: number;
264
+ exp: number;
265
+ };
266
+
71
267
  const playerState = withLocalState<PlayerData>(scene, 'player', {
72
268
  hp: 100,
73
269
  level: 1,
@@ -80,24 +276,17 @@ const playerState = withLocalState<PlayerData>(scene, 'player', {
80
276
  Application-wide state that persists across all scenes.
81
277
 
82
278
  ```typescript
279
+ type GameSettings = {
280
+ soundVolume: number;
281
+ musicEnabled: true;
282
+ };
283
+
83
284
  const settingsState = withGlobalState<GameSettings>(scene, 'settings', {
84
285
  soundVolume: 0.8,
85
286
  musicEnabled: true,
86
287
  });
87
288
  ```
88
289
 
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
290
  ### Enhanced Hooks
102
291
 
103
292
  #### `withPersistentState`
@@ -105,6 +294,11 @@ const customState = withStateDef<number>(scene, 'score', {
105
294
  State with automatic localStorage persistence.
106
295
 
107
296
  ```typescript
297
+ type UserSettings = {
298
+ volume: number;
299
+ difficulty: 'easy' | 'normal' | 'hard';
300
+ };
301
+
108
302
  const persistentSettings = withPersistentState<UserSettings>(
109
303
  'settings',
110
304
  {
@@ -163,13 +357,40 @@ Pre-built validation functions for common patterns.
163
357
  ```typescript
164
358
  import { validators } from 'phaser-hooks';
165
359
 
166
- const scoreState = withGlobalState<number>('score', 0, {
360
+ // Number range validation (0-1000)
361
+ const scoreState = withGlobalState<number>(scene, 'score', 0, {
167
362
  validator: validators.numberRange(0, 1000),
168
363
  });
169
364
 
170
- const nameState = withGlobalState<string>('name', '', {
365
+ // Non-empty string validation
366
+ const nameState = withGlobalState<string>(scene, 'name', '', {
171
367
  validator: validators.nonEmptyString,
172
368
  });
369
+
370
+ // Array length validation (2-4 items)
371
+ const inventoryState = withLocalState<string[]>(scene, 'inventory', [], {
372
+ validator: validators.arrayLength(2, 4),
373
+ });
374
+
375
+ // One of allowed values validation
376
+ const difficultyState = withGlobalState<'easy' | 'normal' | 'hard'>(
377
+ scene,
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
+ });
173
394
  ```
174
395
 
175
396
  #### `batchStateUpdates`
@@ -211,15 +432,24 @@ export class GameScene extends Phaser.Scene {
211
432
  );
212
433
 
213
434
  // Listen to changes
214
- playerState.onChange((newPlayer, oldPlayer) => {
435
+ const ubsubscribe = playerState.on('change', (newPlayer, oldPlayer) => {
215
436
  console.log('Player health changed:', newPlayer.hp);
216
437
  });
217
438
 
218
- // Update state
219
- playerState.set({
220
- ...playerState.get(),
221
- hp: playerState.get().hp - 10,
222
- });
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
+ // });
223
453
  }
224
454
  }
225
455
  ```
@@ -311,10 +541,7 @@ function withPlayerEnergy(scene: Phaser.Scene) {
311
541
  });
312
542
 
313
543
  return {
314
- get: () => player.get().energy,
315
- set: (value: number) => player.set({ ...player.get(), energy: value }),
316
- onChange: (fn: (energy: number) => void) =>
317
- player.onChange(newVal => fn(newVal.energy)),
544
+ ...player,
318
545
  };
319
546
  }
320
547
  ```
@@ -326,15 +553,287 @@ const energy = withPlayerEnergy(this);
326
553
 
327
554
  console.log('Current energy:', energy.get());
328
555
 
329
- energy.set(energy.get() - 10);
556
+ // Using updater function (recommended)
557
+ energy.set((currentEnergy) => currentEnergy - 10);
558
+
559
+ // Alternative: direct value
560
+ // energy.set(energy.get() - 10);
330
561
 
331
- energy.onChange(newEnergy => {
332
- if (newEnergy <= 0) {
562
+ energy.on('change', () => {
563
+ if (energy.get() <= 0) {
333
564
  console.warn('You are out of energy!');
334
565
  }
335
566
  });
336
567
  ```
337
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:
576
+
577
+ ```typescript
578
+ export class GameScene extends Phaser.Scene {
579
+ create() {
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();
595
+
596
+ // To switch to another scene in Phaser, use:
597
+ this.scene.start('OtherSceneKey');
598
+ });
599
+ }
600
+ }
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
+
607
+ ```typescript
608
+ export class GameScene extends Phaser.Scene {
609
+ private healthCallback?: (newPlayer: any, oldPlayer: any) => void;
610
+
611
+ create() {
612
+ const playerState = withLocalState<{ hp: number }>(this, 'player', {
613
+ hp: 100,
614
+ });
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
+ }
635
+ }
636
+ ```
637
+
638
+ > **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.
639
+
640
+ ### Best Practices for Scene Cleanup
641
+
642
+ **⚠️ IMPORTANT DISCLAIMER**: If you don't clean up event listeners when leaving a scene, you may encounter:
643
+
644
+ - Memory leaks
645
+ - Unexpected behavior when returning to the scene
646
+ - Callbacks firing on destroyed or inactive scenes
647
+ - Performance issues over time
648
+
649
+ Always unsubscribe from events when transitioning between scenes:
650
+
651
+ ```typescript
652
+ export class GameScene extends Phaser.Scene {
653
+ private unsubscribeFunctions: (() => void)[] = [];
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
+ }
681
+
682
+ // Or clean up before transitioning to another scene
683
+ goToNextScene() {
684
+ // Clean up before changing scenes
685
+ this.unsubscribeFunctions.forEach(unsubscribe => unsubscribe());
686
+ this.unsubscribeFunctions = [];
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:
697
+
698
+ ```typescript
699
+ export class GameScene extends Phaser.Scene {
700
+ private playerState: HookState<{ hp: number }>;
701
+ private scoreState: HookState<number>;
702
+
703
+ create() {
704
+ this.playerState = withLocalState<{ hp: number }>(this, 'player', { hp: 100 });
705
+ this.scoreState = withGlobalState<number>(this, 'score', 0);
706
+
707
+ // Add listeners
708
+ this.playerState.on('change', (newPlayer) => {
709
+ console.log('Player updated:', newPlayer);
710
+ });
711
+
712
+ this.scoreState.on('change', (newScore) => {
713
+ console.log('Score updated:', newScore);
714
+ });
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>;
733
+
734
+ create() {
735
+ this.globalState = withGlobalState<GameSettings>(this, 'settings', defaultSettings);
736
+
737
+ this.globalState.on('change', (newSettings) => {
738
+ console.log('Settings updated:', newSettings);
739
+ });
740
+
741
+ // IMPORTANT: Clean up global state listeners when scene is destroyed
742
+ this.events.once('destroy', () => {
743
+ this.globalState.clearListeners();
744
+ });
745
+ }
746
+ }
747
+ ```
748
+
749
+ ### Multiple Subscriptions Example
750
+
751
+ You can have multiple listeners for the same state:
752
+
753
+ ```typescript
754
+ export class GameScene extends Phaser.Scene {
755
+ create() {
756
+ const playerState = withLocalState<{ hp: number; level: number }>(
757
+ this,
758
+ 'player',
759
+ {
760
+ hp: 100,
761
+ level: 1,
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
+ }
779
+ ```
780
+
781
+ ### Using `.once()` for One-Time Events
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();
805
+
806
+ // Simulate level up
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
+ ```
812
+
813
+ ### Validation Error Handling
814
+
815
+ When using validators, invalid values will throw errors. Handle them appropriately:
816
+
817
+ ```typescript
818
+ export class GameScene extends Phaser.Scene {
819
+ create() {
820
+ const healthState = withLocalState<number>(this, 'health', 100, {
821
+ validator: validators.numberRange(0, 100),
822
+ });
823
+
824
+ try {
825
+ healthState.set(150); // This will throw an error: "Value must be between 0 and 100"
826
+ } catch (error) {
827
+ console.error('Invalid health value:', error.message);
828
+ // Handle the error appropriately
829
+ }
830
+
831
+ // Valid value
832
+ healthState.set(75); // This works fine
833
+ }
834
+ }
835
+ ```
836
+
338
837
  ### Why use this pattern?
339
838
 
340
839
  ✅ Keeps your scene code focused on intent (e.g., energy.get()) rather than structure (player.get().energy)
@@ -368,6 +867,105 @@ const playerState = withLocalState<PlayerData>(scene, 'player', {
368
867
  const currentPlayer: PlayerData = playerState.get();
369
868
  ```
370
869
 
870
+ ## Debug Mode / Dev tool
871
+
872
+ 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.
873
+
874
+ ### How to Enable Debug Mode
875
+
876
+ To enable debug mode, simply pass `{ debug: true }` in the options parameter when creating any hook:
877
+
878
+ ```typescript
879
+ import { withLocalState } from 'phaser-hooks';
880
+
881
+ export class GameScene extends Phaser.Scene {
882
+ create() {
883
+ // Enable debug mode for this state
884
+ const playerState = withLocalState<{ hp: number; level: number }>(
885
+ this,
886
+ 'player',
887
+ {
888
+ hp: 100,
889
+ level: 1,
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
918
+
919
+ Debug logs appear in your browser's developer console. To view them:
920
+
921
+ 1. Open your browser's Developer Tools (F12 or right-click → Inspect)
922
+ 2. Go to the **Console** tab
923
+ 3. Run your Phaser game
924
+ 4. Look for logs prefixed with `[phaser-hooks]`
925
+
926
+ ![Debug Console Screenshot](data/debug-mode.png)
927
+ *Screenshot showing debug logs in browser console*
928
+
929
+ ### Debug Log Format
930
+
931
+ Debug logs follow a consistent format with timestamps and structured information:
932
+
933
+ ```
934
+ [phaser-hooks] 2024-01-15 10:30:45 [INIT] player - Initializing state with value: {hp: 100, level: 1}
935
+ [phaser-hooks] 2024-01-15 10:30:46 [SET] player - Updating state: {hp: 90, level: 2} (was: {hp: 100, level: 1})
936
+ [phaser-hooks] 2024-01-15 10:30:47 [GET] player - Retrieved state: {hp: 90, level: 2}
937
+ [phaser-hooks] 2024-01-15 10:30:48 [EVENT] player - Added change listener
938
+ ```
939
+
940
+ ### Best Practices for Debug Mode
941
+
942
+ - **Development Only**: Only enable debug mode during development. Remove `{ debug: true }` in production builds
943
+ - **Selective Debugging**: Enable debug mode only for the specific states you're troubleshooting
944
+ - **Performance**: Debug mode adds overhead, so avoid enabling it for all states in production
945
+ - **Console Filtering**: Use browser console filters to focus on specific log types
946
+
947
+ ### Example: Debugging State Issues
948
+
949
+ ```typescript
950
+ export class DebugScene extends Phaser.Scene {
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
+ ```
968
+
371
969
  ## License
372
970
 
373
971
  MIT