phaser-hooks 0.4.0 → 0.6.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.
@@ -0,0 +1,1146 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.PhaserHooks = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ /**
8
+ * Utility to batch multiple state updates
9
+ * @param updateFn Function that performs multiple state updates
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * batchStateUpdates(() => {
14
+ * playerState.set({...playerState.get(), hp: 90});
15
+ * inventoryState.set([...inventoryState.get(), 'new-item']);
16
+ * scoreState.set(scoreState.get() + 100);
17
+ * });
18
+ * ```
19
+ */
20
+ var batchStateUpdates = function (updateFn) {
21
+ // Note: This is a placeholder for potential batching optimization
22
+ // In a more advanced implementation, you might collect all updates
23
+ // and apply them in a single registry update
24
+ updateFn();
25
+ };
26
+
27
+ /**
28
+ * Creates a state validator function for common patterns
29
+ */
30
+ var validators = {
31
+ /**
32
+ * Validates that a number is within a range
33
+ */
34
+ numberRange: function (min, max) {
35
+ return function (value) {
36
+ var num = value;
37
+ if (typeof num !== 'number' || Number.isNaN(num)) {
38
+ return 'Value must be a number';
39
+ }
40
+ if (num < min || num > max) {
41
+ return "Value must be between ".concat(min, " and ").concat(max);
42
+ }
43
+ return true;
44
+ };
45
+ },
46
+ /**
47
+ * Validates that a string is not empty
48
+ */
49
+ nonEmptyString: function (value) {
50
+ var str = value;
51
+ if (typeof str !== 'string' || str.trim().length === 0) {
52
+ return 'Value must be a non-empty string';
53
+ }
54
+ return true;
55
+ },
56
+ /**
57
+ * Validates that an array has a specific length range
58
+ */
59
+ arrayLength: function (min, max) {
60
+ return function (value) {
61
+ var arr = value;
62
+ if (!Array.isArray(arr)) {
63
+ return 'Value must be an array';
64
+ }
65
+ if (arr.length < min) {
66
+ return "Array must have at least ".concat(min, " items");
67
+ }
68
+ if (max !== undefined && arr.length > max) {
69
+ return "Array must have at most ".concat(max, " items");
70
+ }
71
+ return true;
72
+ };
73
+ },
74
+ /**
75
+ * Validates that a value is one of the allowed options
76
+ */
77
+ oneOf: function (allowedValues) {
78
+ return function (value) {
79
+ if (!allowedValues.includes(value)) {
80
+ return "Value must be one of: ".concat(allowedValues.join(', '));
81
+ }
82
+ return true;
83
+ };
84
+ },
85
+ };
86
+
87
+ /******************************************************************************
88
+ Copyright (c) Microsoft Corporation.
89
+
90
+ Permission to use, copy, modify, and/or distribute this software for any
91
+ purpose with or without fee is hereby granted.
92
+
93
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
94
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
95
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
96
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
97
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
98
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
99
+ PERFORMANCE OF THIS SOFTWARE.
100
+ ***************************************************************************** */
101
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
102
+
103
+
104
+ var __assign = function() {
105
+ __assign = Object.assign || function __assign(t) {
106
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
107
+ s = arguments[i];
108
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
109
+ }
110
+ return t;
111
+ };
112
+ return __assign.apply(this, arguments);
113
+ };
114
+
115
+ function __spreadArray(to, from, pack) {
116
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
117
+ if (ar || !(i in from)) {
118
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
119
+ ar[i] = from[i];
120
+ }
121
+ }
122
+ return to.concat(ar || Array.prototype.slice.call(from));
123
+ }
124
+
125
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
126
+ var e = new Error(message);
127
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
128
+ };
129
+
130
+ /* eslint-disable no-console */
131
+ /* eslint-disable sonarjs/no-duplicate-string */
132
+ /**
133
+ * Professional logging utility for phaser-hooks
134
+ * Provides structured, searchable logs with datetime and library identification
135
+ */
136
+ /**
137
+ * Formats datetime for consistent logging
138
+ */
139
+ var formatDateTime = function () {
140
+ var now = new Date();
141
+ return now.toISOString().replace('T', ' ').replace('Z', '');
142
+ };
143
+ /**
144
+ * Creates a searchable prefix for phaser-hooks logs with colors
145
+ */
146
+ var createLogPrefix = function (operation) {
147
+ var timestamp = formatDateTime();
148
+ var libName = '%c[phaser-hooks]%c'; // Blue color for library name
149
+ var operationStr = operation ? " %c".concat(operation, "%c") : '';
150
+ return "%c[".concat(timestamp, "]%c ").concat(libName).concat(operationStr);
151
+ };
152
+ /**
153
+ * Creates CSS styles for the colored prefix
154
+ */
155
+ var createLogStyles = function (operation) {
156
+ var styles = [
157
+ 'color: #bd93f9; font-weight: bold;', // Dracula purple for timestamp
158
+ 'color: inherit;', // Reset after timestamp
159
+ 'color: #2563eb; font-weight: bold;', // Blue for [phaser-hooks]
160
+ 'color: inherit;', // Reset after library name
161
+ ];
162
+ if (operation) {
163
+ styles.push('color: #059669; font-weight: bold;', // Green for operation
164
+ 'color: inherit;' // Reset after operation
165
+ );
166
+ }
167
+ return styles;
168
+ };
169
+ /**
170
+ * Log state initialization
171
+ */
172
+ var logStateInit = function (key, initialValue) {
173
+ var prefix = createLogPrefix('STATE_INIT');
174
+ var styles = createLogStyles('STATE_INIT');
175
+ console.groupCollapsed.apply(console, __spreadArray(["".concat(prefix, " Initializing state \"").concat(key, "\"")], styles, false));
176
+ console.log('🔧 Key:', key);
177
+ console.log('📦 Initial Value:', initialValue);
178
+ console.log('⏰ Timestamp:', formatDateTime());
179
+ console.groupEnd();
180
+ };
181
+ /**
182
+ * Log state value retrieval
183
+ */
184
+ var logStateGet = function (key, value) {
185
+ var prefix = createLogPrefix('STATE_GET');
186
+ var styles = createLogStyles('STATE_GET');
187
+ console.log.apply(console, __spreadArray(__spreadArray(["".concat(prefix, " Getting state \"").concat(key, "\":")], styles, false), [value], false));
188
+ };
189
+ /**
190
+ * Log state value update
191
+ */
192
+ var logStateSet = function (key, oldValue, newValue) {
193
+ var prefix = createLogPrefix('STATE_SET');
194
+ var styles = createLogStyles('STATE_SET');
195
+ console.groupCollapsed.apply(console, __spreadArray(["".concat(prefix, " Updating state \"").concat(key, "\"")], styles, false));
196
+ console.log('🔑 Key:', key);
197
+ console.log('📤 Old Value:', oldValue);
198
+ console.log('📥 New Value:', newValue);
199
+ console.log('🔄 Changed:', oldValue !== newValue);
200
+ console.log('⏰ Timestamp:', formatDateTime());
201
+ console.groupEnd();
202
+ };
203
+ /**
204
+ * Log event listener registration
205
+ */
206
+ var logEventListenerAdd = function (key, event, callback) {
207
+ var prefix = createLogPrefix('EVENT_ADD');
208
+ var styles = createLogStyles('EVENT_ADD');
209
+ console.groupCollapsed.apply(console, __spreadArray(["".concat(prefix, " Adding listener for \"").concat(key, "\"")], styles, false));
210
+ console.log('🔑 Key:', key);
211
+ console.log('📡 Event:', event);
212
+ console.log('🎯 Callback:', callback.name || 'anonymous');
213
+ console.log('⏰ Timestamp:', formatDateTime());
214
+ console.groupEnd();
215
+ };
216
+ /**
217
+ * Log event listener removal
218
+ */
219
+ var logEventListenerRemove = function (key, event, callback) {
220
+ var prefix = createLogPrefix('EVENT_REMOVE');
221
+ var styles = createLogStyles('EVENT_REMOVE');
222
+ console.groupCollapsed.apply(console, __spreadArray(["".concat(prefix, " Removing listener for \"").concat(key, "\"")], styles, false));
223
+ console.log('🔑 Key:', key);
224
+ console.log('📡 Event:', event);
225
+ console.log('🎯 Callback:', callback.name || 'anonymous');
226
+ console.log('⏰ Timestamp:', formatDateTime());
227
+ console.groupEnd();
228
+ };
229
+ /**
230
+ * Log clearing all listeners
231
+ */
232
+ var logClearListeners = function (key) {
233
+ var prefix = createLogPrefix('CLEAR_LISTENERS');
234
+ var styles = createLogStyles('CLEAR_LISTENERS');
235
+ console.groupCollapsed.apply(console, __spreadArray(["".concat(prefix, " Clearing all listeners for \"").concat(key, "\"")], styles, false));
236
+ console.log('🔑 Key:', key);
237
+ console.log('🧹 Action:', 'Removing all event listeners');
238
+ console.log('⏰ Timestamp:', formatDateTime());
239
+ console.groupEnd();
240
+ };
241
+ /**
242
+ * Log error with context
243
+ */
244
+ var logError = function (operation, error, context) {
245
+ var prefix = createLogPrefix(operation);
246
+ var styles = createLogStyles(operation);
247
+ console.groupCollapsed.apply(console, __spreadArray(["".concat(prefix, " ERROR")], styles, false));
248
+ console.error('🚨 Operation:', operation);
249
+ console.error('💥 Error:', error);
250
+ if (context) {
251
+ console.error('📋 Context:', context);
252
+ }
253
+ console.error('⏰ Timestamp:', formatDateTime());
254
+ console.groupEnd();
255
+ };
256
+ /**
257
+ * Log warning with context
258
+ */
259
+ var logWarning = function (operation, message, context) {
260
+ var prefix = createLogPrefix(operation);
261
+ var styles = createLogStyles(operation);
262
+ console.groupCollapsed.apply(console, __spreadArray(["".concat(prefix, " WARNING")], styles, false));
263
+ console.warn('⚠️ Operation:', operation);
264
+ console.warn('📢 Message:', message);
265
+ if (context) {
266
+ console.warn('📋 Context:', context);
267
+ }
268
+ console.warn('⏰ Timestamp:', formatDateTime());
269
+ console.groupEnd();
270
+ };
271
+
272
+ /**
273
+ * Deep merge utility function that recursively merges objects
274
+ * Similar to lodash.merge but implemented natively
275
+ */
276
+ /**
277
+ * Checks if a value is a plain object (not array, null, or other types)
278
+ */
279
+ var isPlainObject = function (value) {
280
+ return (value !== null &&
281
+ typeof value === 'object' &&
282
+ !Array.isArray(value) &&
283
+ Object.prototype.toString.call(value) === '[object Object]');
284
+ };
285
+ /**
286
+ * Deep clone utility function that creates new references for all nested objects
287
+ * @param obj - The object to clone
288
+ * @returns A deep clone of the object with new references
289
+ */
290
+ var deepClone = function (obj) {
291
+ if (obj === null || typeof obj !== 'object') {
292
+ return obj;
293
+ }
294
+ if (Array.isArray(obj)) {
295
+ return obj.map(function (item) { return deepClone(item); });
296
+ }
297
+ if (isPlainObject(obj)) {
298
+ var cloned = {};
299
+ for (var key in obj) {
300
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
301
+ cloned[key] = deepClone(obj[key]);
302
+ }
303
+ }
304
+ return cloned;
305
+ }
306
+ return obj;
307
+ };
308
+ /**
309
+ * Deep merge multiple objects into the first one
310
+ * @param target - The target object to merge into
311
+ * @param sources - Source objects to merge from
312
+ * @returns The merged object
313
+ */
314
+ var merge = function (target) {
315
+ var sources = [];
316
+ for (var _i = 1; _i < arguments.length; _i++) {
317
+ sources[_i - 1] = arguments[_i];
318
+ }
319
+ if (!isPlainObject(target)) {
320
+ return target;
321
+ }
322
+ // Start with a deep clone of the target
323
+ var result = deepClone(target);
324
+ for (var _a = 0, sources_1 = sources; _a < sources_1.length; _a++) {
325
+ var source = sources_1[_a];
326
+ if (!isPlainObject(source)) {
327
+ continue;
328
+ }
329
+ // Deep clone the source to ensure we create new references
330
+ var clonedSource = deepClone(source);
331
+ for (var key in clonedSource) {
332
+ if (Object.prototype.hasOwnProperty.call(clonedSource, key)) {
333
+ var sourceValue = clonedSource[key];
334
+ var targetValue = result[key];
335
+ if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {
336
+ result[key] = merge(targetValue, sourceValue);
337
+ }
338
+ else if (sourceValue !== undefined) {
339
+ result[key] = sourceValue;
340
+ }
341
+ }
342
+ }
343
+ }
344
+ return result;
345
+ };
346
+
347
+ /**
348
+ * Map of callbacks to their wrapped versions
349
+ * @type {WeakMap<Function, Function>}
350
+ */
351
+ var callbackMap = new WeakMap();
352
+ /**
353
+ * Gets the current value for a given key from the registry.
354
+ * @template T
355
+ * @param {Phaser.Data.DataManager} registry - The Phaser data manager (scene.data or scene.registry)
356
+ * @param {string} key - The key to retrieve
357
+ * @param {boolean} debug - Whether to log debug info
358
+ * @returns {T} The value stored in the registry for the given key
359
+ */
360
+ var get = function (registry, key, debug) {
361
+ var value = registry.get(key);
362
+ if (debug) {
363
+ logStateGet(key, value);
364
+ }
365
+ return value;
366
+ };
367
+ /**
368
+ * Sets a new state value in the registry.
369
+ * @template T
370
+ * @param {Phaser.Data.DataManager} registry - The Phaser data manager
371
+ * @param {string} key - The key to set
372
+ * @param {T | ((currentState: T) => T)} value - The value to set or a function that receives current state and returns new state
373
+ * @param {boolean} debug - Whether to log debug info
374
+ * @param {(value: unknown) => boolean | string} [validator] - Optional validator function
375
+ * @throws {Error} If the validator returns false or an error message
376
+ */
377
+ var set = function (registry, key, value, debug, validator) {
378
+ var currentValue = registry.get(key);
379
+ // If value is a function, execute it with current state
380
+ var newValue = typeof value === 'function' ? value(currentValue) : value;
381
+ if (validator) {
382
+ var validationResult = validator(newValue);
383
+ if (validationResult !== true) {
384
+ var message = typeof validationResult === 'string'
385
+ ? validationResult
386
+ : "Invalid value for key \"".concat(key, "\"");
387
+ throw new Error("[withStateDef] ".concat(message));
388
+ }
389
+ }
390
+ registry.set(key, newValue);
391
+ if (debug) {
392
+ logStateSet(key, currentValue, newValue);
393
+ }
394
+ };
395
+ /**
396
+ * Applies a shallow merge ("patch") to an object state in the registry.
397
+ *
398
+ * This method attempts to update the current state associated with the given key by performing a shallow merge
399
+ * between the existing state and the provided value or the result of an updater function.
400
+ * The function will throw an error if the current state is not an object.
401
+ * An optional validator function can be provided to ensure the new value is valid before committing the patch.
402
+ *
403
+ * @template T
404
+ * @param {Phaser.Data.DataManager} registry - The Phaser data manager
405
+ * @param {string} key - The key to patch in the registry
406
+ * @param {T | ((currentState: T) => T)} value - The value object to merge, or a function that returns a value given the current state
407
+ * @param {boolean} debug - Whether to enable debug logging
408
+ * @param {(value: unknown) => boolean | string} [validator] - Optional validator function; must return true or an error message
409
+ * @throws {Error} If the current value is not an object or the validator returns false/an error message
410
+ */
411
+ var patch = function (registry, key, value, debug, validator) {
412
+ var currentValue = registry.get(key);
413
+ if (typeof currentValue !== 'object' || currentValue === null) {
414
+ throw new Error('[withStateDef] Current value is not an object');
415
+ }
416
+ // If value is a function, execute it with current state to get the patch
417
+ var patchValue = typeof value === 'function' ? value(currentValue) : value;
418
+ var newValue = merge({}, currentValue, patchValue);
419
+ if (validator) {
420
+ var validationResult = validator(newValue);
421
+ if (validationResult !== true) {
422
+ var message = typeof validationResult === 'string'
423
+ ? validationResult
424
+ : "Invalid value for key \"".concat(key, "\"");
425
+ throw new Error("[withStateDef] ".concat(message));
426
+ }
427
+ }
428
+ registry.set(key, newValue);
429
+ if (debug) {
430
+ logStateSet(key, currentValue, newValue);
431
+ }
432
+ };
433
+ /**
434
+ * Registers a change listener for this state (DEPRECATED).
435
+ * @template T
436
+ * @param {Phaser.Data.DataManager} registry - The Phaser data manager
437
+ * @param {string} key - The key to listen for changes
438
+ * @param {boolean} debug - Whether to log debug info
439
+ * @param {StateChangeCallback<T>} callback - Callback to invoke on change
440
+ * @deprecated Use .on('change', callback) or .once('change', callback) instead.
441
+ * @throws {Error} If callback is not a function
442
+ */
443
+ var onChange = function (registry, key, debug, callback) {
444
+ logWarning('DEPRECATED_ONCHANGE', "onChange callback is deprecated in phaser-hooks. Use .on('change', callback) or .once('change', callback) instead.", { key: key });
445
+ if (!callback || typeof callback !== 'function') {
446
+ throw new Error('[withStateDef] onChange callback must be a function');
447
+ }
448
+ registry.events.on("changedata-".concat(key), // reserved word in Phaser
449
+ function (_parent, key, value, previousValue) {
450
+ if (debug) {
451
+ logStateSet(key, previousValue, value);
452
+ }
453
+ try {
454
+ callback(value, previousValue);
455
+ }
456
+ catch (error) {
457
+ logError('ONCHANGE_CALLBACK_ERROR', error, { key: key });
458
+ }
459
+ });
460
+ };
461
+ /**
462
+ * Registers an event listener for the state.
463
+ * Only the 'change' event is supported.
464
+ * @param {Phaser.Data.DataManager} registry - The Phaser data manager
465
+ * @param {string | symbol} event - The event name ('change')
466
+ * @param {string} key - The key to listen for changes
467
+ * @param {boolean} debug - Whether to log debug info
468
+ * @param {Function} callback - The callback to invoke on event - receives the new value and the old value
469
+ * @returns {() => void} Unsubscribe function to remove the listener
470
+ * @throws {Error} If event is not 'change'
471
+ */
472
+ var on = function (registry, event, key, debug, callback) {
473
+ if (event !== 'change') {
474
+ throw new Error('[withStateDef] Invalid event. Only "change" is supported.');
475
+ }
476
+ // Wrapper to remove the first argument (scene)
477
+ var wrappedCallback = function (_scene, newValue, oldValue) {
478
+ callback(newValue, oldValue);
479
+ };
480
+ if (!callbackMap.has(callback)) {
481
+ callbackMap.set(callback, wrappedCallback);
482
+ }
483
+ if (debug) {
484
+ logEventListenerAdd(key, event, wrappedCallback);
485
+ }
486
+ registry.events.on("changedata-".concat(key), wrappedCallback);
487
+ return function () {
488
+ if (debug) {
489
+ logEventListenerRemove(key, event, wrappedCallback);
490
+ }
491
+ registry.events.off("changedata-".concat(key), wrappedCallback);
492
+ };
493
+ };
494
+ /**
495
+ * Validates the scene and options for the state hook.
496
+ * @param {Phaser.Scene} scene - The Phaser scene
497
+ * @param {StateDefOptions} options - State definition options
498
+ * @throws {Error} If scene or registry/data is not available
499
+ */
500
+ var validateHook = function (scene, options, key) {
501
+ if (!scene) {
502
+ throw new Error('[withStateDef] Scene parameter is required');
503
+ }
504
+ if (!key || typeof key !== 'string' || key.trim().length === 0) {
505
+ throw new Error('[withStateDef] Key must be a non-empty string');
506
+ }
507
+ if (options.global && !scene.registry) {
508
+ throw new Error('[withStateDef] Scene registry is not available. Ensure the scene is properly initialized.');
509
+ }
510
+ else if (!options.global && !scene.data) {
511
+ throw new Error('[withStateDef] Scene data is not available. Ensure the scene is properly initialized.');
512
+ }
513
+ };
514
+ /**
515
+ * Initializes the state in the registry if not already set.
516
+ * @template T
517
+ * @param {Phaser.Data.DataManager} registry - The Phaser data manager
518
+ * @param {string} key - The key to initialize
519
+ * @param {T} [initialValue] - The initial value to set
520
+ * @param {boolean} debug - Whether to log debug info
521
+ * @param {(value: unknown) => boolean | string} [validator] - Optional validator function
522
+ * @throws {Error} If the validator returns false or an error message
523
+ */
524
+ var initializeState = function (registry, key, debug, validator, initialValue) {
525
+ // Validate and set initial value if provided
526
+ if (!registry.has(key) && initialValue !== undefined) {
527
+ if (validator) {
528
+ var validationResult = validator(initialValue);
529
+ if (validationResult !== true) {
530
+ var message = typeof validationResult === 'string'
531
+ ? validationResult
532
+ : "Invalid initial value for key \"".concat(key, "\"");
533
+ throw new Error("[withStateDef] ".concat(message));
534
+ }
535
+ }
536
+ registry.set(key, initialValue);
537
+ if (debug) {
538
+ logStateInit(key, initialValue);
539
+ }
540
+ }
541
+ };
542
+ /**
543
+ * Registers a one-time event listener for the state.
544
+ * Only the 'change' event is supported.
545
+ * @param {Phaser.Data.DataManager} registry - The Phaser data manager
546
+ * @param {string | symbol} event - The event name ('change')
547
+ * @param {string} key - The key to listen for changes
548
+ * @param {boolean} debug - Whether to log debug info
549
+ * @param {Function} callback - The callback to invoke on event
550
+ * @returns {() => void} Unsubscribe function to remove the listener
551
+ * @throws {Error} If event is not 'change'
552
+ */
553
+ var once = function (registry, event, key, debug, callback) {
554
+ if (event !== 'change') {
555
+ throw new Error('[withStateDef] Invalid event. Only "change" is supported.');
556
+ }
557
+ // Wrapper to remove the first argument (scene)
558
+ var wrappedCallback = function (_scene, newValue, oldValue) {
559
+ callback(newValue, oldValue);
560
+ };
561
+ if (!callbackMap.has(callback)) {
562
+ callbackMap.set(callback, wrappedCallback);
563
+ }
564
+ if (debug) {
565
+ logEventListenerAdd(key, event, wrappedCallback);
566
+ }
567
+ registry.events.once("changedata-".concat(key), wrappedCallback);
568
+ return function () {
569
+ if (debug) {
570
+ logEventListenerRemove(key, event, wrappedCallback);
571
+ }
572
+ registry.events.off("changedata-".concat(key), wrappedCallback);
573
+ };
574
+ };
575
+ /**
576
+ * Removes an event listener for the state.
577
+ * Only the 'change' event is supported.
578
+ * @param {Phaser.Data.DataManager} registry - The Phaser data manager
579
+ * @param {string | symbol} event - The event name ('change')
580
+ * @param {string} key - The key to remove the listener from
581
+ * @param {boolean} debug - Whether to log debug info
582
+ * @param {Function} callback - The callback to remove
583
+ * @throws {Error} If event is not 'change'
584
+ */
585
+ var off = function (registry, event, key, debug, callback) {
586
+ if (event !== 'change') {
587
+ throw new Error('[withStateDef] Invalid event. Only "change" is supported.');
588
+ }
589
+ // Get the wrapped callback from the map
590
+ var callbackToRemove = callbackMap.get(callback);
591
+ if (!callbackToRemove) {
592
+ logWarning('CALLBACK_NOT_FOUND', "Callback not found for key \"".concat(key, "\""), { key: key });
593
+ return;
594
+ }
595
+ registry.events.off("changedata-".concat(key), callbackToRemove);
596
+ if (debug) {
597
+ logEventListenerRemove(key, event, callbackToRemove);
598
+ }
599
+ };
600
+ /**
601
+ * Removes all event listeners for the state.
602
+ * @param {Phaser.Data.DataManager} registry - The Phaser data manager
603
+ * @param {string} key - The key to remove all listeners from
604
+ * @param {boolean} debug - Whether to log debug info
605
+ */
606
+ var clearListeners = function (registry, key, debug) {
607
+ registry.events.removeAllListeners("changedata-".concat(key));
608
+ if (debug) {
609
+ logClearListeners(key);
610
+ }
611
+ };
612
+ /**
613
+ * Low-level state management hook that directly interfaces with Phaser's registry system.
614
+ * This is the foundation for all other state hooks and provides direct access to
615
+ * Phaser's scene registry with additional safety and TypeScript support.
616
+ *
617
+ * ⚠️ **Note**: This is a low-level hook. Consider using `withLocalState` or `withGlobalState`
618
+ * for most use cases unless you need specific registry control.
619
+ *
620
+ * @template T The type of the state value
621
+ * @param {Phaser.Scene} scene The Phaser scene instance that owns this state
622
+ * @param {string} key Unique identifier for the state in the registry
623
+ * @param {T} [initialValue] Optional initial value to set if key doesn't exist
624
+ * @param {StateDefOptions} [options] Optional configuration for state behavior
625
+ * @returns {HookState<T>} HookState interface for managing the state
626
+ *
627
+ * @throws {Error} When scene or registry is not available
628
+ * @throws {Error} When key is invalid (empty or non-string)
629
+ * @throws {Error} When validator rejects the initial value
630
+ *
631
+ * @example
632
+ * ```typescript
633
+ * // Basic usage
634
+ * const playerState = withStateDef<{name: string, level: number}>(
635
+ * scene,
636
+ * 'player',
637
+ * { name: 'Player1', level: 1 }
638
+ * );
639
+ *
640
+ * // With validation
641
+ * const healthState = withStateDef<number>(
642
+ * scene,
643
+ * 'health',
644
+ * 100,
645
+ * {
646
+ * validator: (value) => {
647
+ * const health = value as number;
648
+ * return health >= 0 && health <= 100 ? true : 'Health must be between 0-100';
649
+ * }
650
+ * }
651
+ * );
652
+ *
653
+ * // With debug logging
654
+ * const debugState = withStateDef<string>(
655
+ * scene,
656
+ * 'debug-info',
657
+ * 'initial',
658
+ * { debug: true }
659
+ * );
660
+ *
661
+ * // Setting values directly
662
+ * playerState.set({ name: 'Player2', level: 5 });
663
+ *
664
+ * // Setting values using updater function
665
+ * playerState.set((currentState) => ({ ...currentState, level: currentState.level + 1 }));
666
+ *
667
+ * // Listening to changes
668
+ * playerState.on('change', (newValue, oldValue) => {
669
+ * console.log('Player state changed:', newValue, oldValue);
670
+ * });
671
+ *
672
+ * // Listening to changes only once
673
+ * playerState.once('change', (newValue, oldValue) => {
674
+ * console.log('Player state changed once:', newValue, oldValue);
675
+ * });
676
+ *
677
+ * // Removing a listener
678
+ * const unsubscribe = playerState.on('change', callback);
679
+ * unsubscribe(); // Removes the listener
680
+ * playerState.off('change', callback); // Also removes the listener
681
+ * playerState.clearListeners(); // Removes all listeners
682
+ * ```
683
+ *
684
+ * @method get Gets the current state value
685
+ * @method set Sets a new state value and triggers change listeners
686
+ * @method on Registers a callback to be called whenever the state changes. Returns an unsubscribe function.
687
+ * @method once Registers a callback to be called once when the state changes. Returns an unsubscribe function.
688
+ * @method off Removes an event listener for the state
689
+ * @method clearListeners Removes all event listeners for this state
690
+ * @method onChange (DEPRECATED) Registers a callback to be called whenever the state changes
691
+ */
692
+ var withStateDef = function (scene, key, initialValue, options) {
693
+ if (options === void 0) { options = {}; }
694
+ validateHook(scene, options, key);
695
+ var validator = options.validator, _a = options.debug, debug = _a === void 0 ? false : _a, _b = options.global, global = _b === void 0 ? false : _b;
696
+ var registry = global ? scene.registry : scene.data;
697
+ // Fix: move debug and validator before initialValue to match required params before optional
698
+ initializeState(registry, key, debug, validator, initialValue);
699
+ return {
700
+ /**
701
+ * Gets the current state value.
702
+ * @returns {T}
703
+ */
704
+ get: function () { return get(registry, key, debug); },
705
+ /**
706
+ * Sets a new state value and triggers change listeners.
707
+ * @param {T | ((currentState: T) => T)} value - The new value to set or a function that receives current state and returns new state
708
+ */
709
+ set: function (value) { return set(registry, key, value, debug, validator); },
710
+ /**
711
+ * Patches the current state value with a new value.
712
+ * @param {DeepPartial<T> | StatePatchUpdater<T>} value - The new value to patch or a function that receives current state and returns new state
713
+ */
714
+ patch: function (value) { return patch(registry, key, value, debug, validator); },
715
+ /**
716
+ * Registers a callback to be called whenever the state changes (DEPRECATED).
717
+ * @param {StateChangeCallback<T>} callback
718
+ */
719
+ onChange: function (callback) {
720
+ return onChange(registry, key, debug, callback);
721
+ },
722
+ /**
723
+ * Registers a callback to be called whenever the state changes.
724
+ * Only the 'change' event is supported.
725
+ * @param {'change'} event
726
+ * @param {Function} fn
727
+ * @returns {() => void} Unsubscribe function
728
+ */
729
+ on: function (event, fn) {
730
+ return on(registry, event, key, debug, fn);
731
+ },
732
+ /**
733
+ * Registers a callback to be called once when the state changes.
734
+ * Only the 'change' event is supported.
735
+ * @param {'change'} event
736
+ * @param {Function} fn
737
+ * @returns {() => void} Unsubscribe function
738
+ */
739
+ once: function (event, fn) {
740
+ return once(registry, event, key, debug, fn);
741
+ },
742
+ /**
743
+ * Removes an event listener for the state.
744
+ * Only the 'change' event is supported.
745
+ * @param {'change'} event
746
+ * @param {Function} fn
747
+ */
748
+ off: function (event, fn) {
749
+ return off(registry, event, key, debug, fn);
750
+ },
751
+ /**
752
+ * Removes all event listeners for this state.
753
+ */
754
+ clearListeners: function () { return clearListeners(registry, key, debug); },
755
+ };
756
+ };
757
+
758
+ /**
759
+ * Creates a local state hook scoped to a specific Phaser scene.
760
+ * Local state is isolated to the scene instance and doesn't persist across scene changes.
761
+ * This is ideal for UI state, temporary game state, or scene-specific data.
762
+ *
763
+ * @template T The type of the state value
764
+ * @param scene The Phaser scene instance that owns this state
765
+ * @param key Unique identifier for the state within this scene
766
+ * @param initialValue Optional initial value to set if state doesn't exist
767
+ * @param options Optional configuration for state behavior
768
+ * @returns HookState interface for managing the local state
769
+ *
770
+ * @throws {Error} When scene is not available or key is invalid
771
+ *
772
+ * @example
773
+ * ```typescript
774
+ * // Simple counter state
775
+ * const counterState = withLocalState<number>(scene, 'counter', 0);
776
+ *
777
+ * // Complex object state
778
+ * interface GameUI {
779
+ * isMenuOpen: boolean;
780
+ * selectedTab: string;
781
+ * }
782
+ *
783
+ * const uiState = withLocalState<GameUI>(scene, 'ui', {
784
+ * isMenuOpen: false,
785
+ * selectedTab: 'main'
786
+ * });
787
+ *
788
+ * // Listen to changes
789
+ * uiState.onChange((newUI, oldUI) => {
790
+ * if (newUI.isMenuOpen !== oldUI.isMenuOpen) {
791
+ * console.log('Menu visibility changed');
792
+ * }
793
+ * });
794
+ *
795
+ * // Update state
796
+ * uiState.set({ ...uiState.get(), isMenuOpen: true });
797
+ *
798
+ * // Array state example
799
+ * const inventoryState = withLocalState<string[]>(scene, 'inventory', []);
800
+ * inventoryState.set([...inventoryState.get(), 'new-item']);
801
+ * ```
802
+ *
803
+ * @example
804
+ * ```typescript
805
+ * // With validation
806
+ * const playerHealthState = withLocalState<number>(
807
+ * scene,
808
+ * 'health',
809
+ * 100,
810
+ * {
811
+ * validator: (value) => {
812
+ * const health = value as number;
813
+ * return health >= 0 && health <= 100 ? true : 'Health must be 0-100';
814
+ * }
815
+ * }
816
+ * );
817
+ * ```
818
+ *
819
+ * @see {@link withGlobalState} For state that persists across scenes
820
+ * @see {@link withStateDef} For low-level state management
821
+ */
822
+ var withLocalState = function (scene, key, initialValue, options) {
823
+ var _a;
824
+ if (!scene) {
825
+ throw new Error('[withLocalState] Scene parameter is required');
826
+ }
827
+ // Prefix the key with scene key to ensure locality
828
+ var sceneKey = ((_a = scene.scene) === null || _a === void 0 ? void 0 : _a.key) || 'unknown-scene';
829
+ var localKey = "phaser-hooks:local:".concat(sceneKey, ":").concat(key);
830
+ return withStateDef(scene, localKey, initialValue, __assign(__assign({}, options), { persistent: false, global: false }));
831
+ };
832
+
833
+ /**
834
+ * Creates a computed state that derives its value from other states
835
+ * @template T The input state type
836
+ * @template U The computed result type
837
+ * @param scene The Phaser scene instance
838
+ * @param key Unique identifier for the computed state
839
+ * @param sourceState The source state to derive from
840
+ * @param selector Function to compute the derived value
841
+ * @returns HookState with computed value
842
+ *
843
+ * @example
844
+ * ```typescript
845
+ * const playerState = withLocalState<{hp: number, maxHp: number}>(scene, 'player', {...});
846
+ * const healthPercentage = withComputedState(
847
+ * scene,
848
+ * 'healthPercent',
849
+ * playerState,
850
+ * (player) => (player.hp / player.maxHp) * 100
851
+ * );
852
+ * ```
853
+ */
854
+ var withComputedState = function (scene, key, sourceState, selector) {
855
+ // Initialize with computed value
856
+ var initialValue = selector(sourceState.get());
857
+ var computedState = withLocalState(scene, key, initialValue);
858
+ // Update computed state when source changes
859
+ sourceState.on('change', function () {
860
+ var newSourceValue = sourceState.get();
861
+ var newComputedValue = selector(newSourceValue);
862
+ computedState.set(newComputedValue);
863
+ });
864
+ return __assign(__assign({}, computedState), { set: function () {
865
+ throw new Error("[withComputedState] Cannot directly set computed state \"".concat(key, "\". Update the source state instead."));
866
+ } });
867
+ };
868
+
869
+ /**
870
+ * Creates a state hook with debounced updates
871
+ * @template T The type of the state value
872
+ * @param scene The Phaser scene instance
873
+ * @param key Unique identifier for the state
874
+ * @param initialValue Initial value for the state
875
+ * @param debounceMs Debounce delay in milliseconds
876
+ * @returns HookState with debounced set operations
877
+ *
878
+ * @example
879
+ * ```typescript
880
+ * const debouncedSearch = withDebouncedState<string>(scene, 'search', '', 300);
881
+ *
882
+ * // These rapid calls will be debounced
883
+ * debouncedSearch.set('a');
884
+ * debouncedSearch.set('ab');
885
+ * debouncedSearch.set('abc'); // Only this final value will be set after 300ms
886
+ * ```
887
+ */
888
+ var withDebouncedState = function (scene, key, initialValue, debounceMs) {
889
+ var actualState = withLocalState(scene, key, initialValue);
890
+ var timeoutId = null;
891
+ var debouncedSet = function (value) {
892
+ var newValue = typeof value === 'function' ? value(actualState.get()) : value;
893
+ if (timeoutId) {
894
+ clearTimeout(timeoutId);
895
+ }
896
+ timeoutId = setTimeout(function () {
897
+ actualState.set(newValue);
898
+ timeoutId = null;
899
+ }, debounceMs);
900
+ };
901
+ var debouncedPatch = function (value) {
902
+ var patchValue = typeof value === 'function' ? value(actualState.get()) : value;
903
+ debouncedSet(function (currentState) { return merge(currentState, patchValue); });
904
+ };
905
+ return __assign(__assign({}, actualState), { set: debouncedSet, patch: debouncedPatch });
906
+ };
907
+
908
+ /**
909
+ * Creates a local state hook scoped to a specific Phaser scene.
910
+ * Local state is isolated to the scene instance and doesn't persist across scene changes.
911
+ * This is ideal for UI state, temporary game state, or scene-specific data.
912
+ *
913
+ * @template T The type of the state value
914
+ * @param scene The Phaser scene instance that owns this state
915
+ * @param key Unique identifier for the state within this scene
916
+ * @param initialValue Optional initial value to set if state doesn't exist
917
+ * @param options Optional configuration for state behavior
918
+ * @returns HookState interface for managing the local state
919
+ *
920
+ * @throws {Error} When scene is not available or key is invalid
921
+ *
922
+ * @example
923
+ * ```typescript
924
+ * // Simple counter state
925
+ * const counterState = withGlobalState<number>(scene, 'counter', 0);
926
+ *
927
+ * // Complex object state
928
+ * interface GameUI {
929
+ * isMenuOpen: boolean;
930
+ * selectedTab: string;
931
+ * }
932
+ *
933
+ * const uiState = withGlobalState<GameUI>(scene, 'ui', {
934
+ * isMenuOpen: false,
935
+ * selectedTab: 'main'
936
+ * });
937
+ *
938
+ * // Listen to changes
939
+ * uiState.onChange((newUI, oldUI) => {
940
+ * if (newUI.isMenuOpen !== oldUI.isMenuOpen) {
941
+ * console.log('Menu visibility changed');
942
+ * }
943
+ * });
944
+ *
945
+ * // Update state
946
+ * uiState.set({ ...uiState.get(), isMenuOpen: true });
947
+ *
948
+ * // Array state example
949
+ * const inventoryState = withGlobalState<string[]>(scene, 'inventory', []);
950
+ * inventoryState.set([...inventoryState.get(), 'new-item']);
951
+ * ```
952
+ *
953
+ * @example
954
+ * ```typescript
955
+ * // With validation
956
+ * const playerHealthState = withGlobalState<number>(
957
+ * scene,
958
+ * 'health',
959
+ * 100,
960
+ * {
961
+ * validator: (value) => {
962
+ * const health = value as number;
963
+ * return health >= 0 && health <= 100 ? true : 'Health must be 0-100';
964
+ * }
965
+ * }
966
+ * );
967
+ * ```
968
+ *
969
+ * @see {@link withGlobalState} For state that persists across scenes
970
+ * @see {@link withStateDef} For low-level state management
971
+ */
972
+ var withGlobalState = function (scene, key, initialValue, options) {
973
+ if (!scene) {
974
+ throw new Error('[withGlobalState] Scene parameter is required');
975
+ }
976
+ // Prefix the key with scene key to ensure locality
977
+ var localKey = "phaser-hooks:global:".concat(key);
978
+ return withStateDef(scene, localKey, initialValue, __assign(__assign({}, options), { persistent: false, global: true }));
979
+ };
980
+
981
+ /**
982
+ * Creates a state hook with automatic localStorage persistence
983
+ * @template T The type of the state value
984
+ * @param key Unique identifier for the state
985
+ * @param initialValue Initial value to use if no stored value exists
986
+ * @param storageKey Optional custom localStorage key (defaults to the state key)
987
+ * @returns HookState with automatic persistence
988
+ *
989
+ * @example
990
+ * ```typescript
991
+ * const persistentSettings = withPersistentState<UserSettings>(
992
+ * 'settings',
993
+ * { volume: 0.8, difficulty: 'normal' }
994
+ * );
995
+ * ```
996
+ */
997
+ var withPersistentState = function (scene, key, initialValue, storageKey, storageType) {
998
+ if (storageType === void 0) { storageType = 'local'; }
999
+ var actualStorageKey = storageKey !== null && storageKey !== void 0 ? storageKey : "phaser-hooks-state:".concat(key);
1000
+ // Load from localStorage if available
1001
+ var storedValue = initialValue;
1002
+ try {
1003
+ var stored = storageType === 'local' ? localStorage.getItem(actualStorageKey) : sessionStorage.getItem(actualStorageKey);
1004
+ if (stored) {
1005
+ storedValue = JSON.parse(stored);
1006
+ }
1007
+ }
1008
+ catch (error) {
1009
+ // eslint-disable-next-line no-console
1010
+ console.warn("[withPersistentState] Failed to load stored state for \"".concat(key, "\":"), error);
1011
+ }
1012
+ // @ts-ignore
1013
+ var state = withGlobalState(scene, key, storedValue);
1014
+ // Save to localStorage on changes
1015
+ state.onChange(function (newValue) {
1016
+ try {
1017
+ var storage = storageType === 'local' ? localStorage : sessionStorage;
1018
+ storage.setItem(actualStorageKey, JSON.stringify(newValue));
1019
+ }
1020
+ catch (error) {
1021
+ // eslint-disable-next-line no-console
1022
+ console.warn("[withPersistentState] Failed to save state for \"".concat(key, "\":"), error);
1023
+ }
1024
+ });
1025
+ return state;
1026
+ };
1027
+
1028
+ /**
1029
+ * Creates a state hook with undo/redo functionality
1030
+ * @template T The type of the state value
1031
+ * @param scene The Phaser scene instance
1032
+ * @param key Unique identifier for the state
1033
+ * @param initialValue Initial value for the state
1034
+ * @param maxHistorySize Maximum number of history entries to keep
1035
+ * @returns Enhanced HookState with undo/redo capabilities
1036
+ *
1037
+ * @example
1038
+ * ```typescript
1039
+ * const undoableState = withUndoableState<string>(scene, 'text', 'initial', 10);
1040
+ *
1041
+ * undoableState.set('first change');
1042
+ * undoableState.set('second change');
1043
+ * undoableState.undo(); // Back to 'first change'
1044
+ * undoableState.redo(); // Forward to 'second change'
1045
+ * ```
1046
+ */
1047
+ /* eslint-disable max-lines-per-function */
1048
+ /* eslint-disable complexity */
1049
+ /* eslint-disable sonarjs/cognitive-complexity */
1050
+ var withUndoableState = function (scene, key, initialValue, maxHistorySize) {
1051
+ var currentState = withLocalState(scene, key, initialValue);
1052
+ var historyState = withLocalState(scene, "".concat(key, ":history"), [
1053
+ initialValue,
1054
+ ]);
1055
+ var historyIndexState = withLocalState(scene, "".concat(key, ":historyIndex"), 0);
1056
+ var addToHistory = function (value) {
1057
+ var history = historyState.get();
1058
+ var currentIndex = historyIndexState.get();
1059
+ // Remove any "future" history if we're not at the end
1060
+ var newHistory = history.slice(0, currentIndex + 1);
1061
+ // Add new value
1062
+ newHistory.push(value);
1063
+ // Limit history size
1064
+ if (newHistory.length > maxHistorySize) {
1065
+ newHistory.shift();
1066
+ }
1067
+ else {
1068
+ historyIndexState.set(currentIndex + 1);
1069
+ }
1070
+ historyState.set(newHistory);
1071
+ };
1072
+ var set = function (value) {
1073
+ var newValue = typeof value === 'function' ? value(currentState.get()) : value;
1074
+ addToHistory(newValue);
1075
+ currentState.set(newValue);
1076
+ };
1077
+ var undo = function () {
1078
+ var currentIndex = historyIndexState.get();
1079
+ if (currentIndex > 0) {
1080
+ var newIndex = currentIndex - 1;
1081
+ historyIndexState.set(newIndex);
1082
+ var history_1 = historyState.get();
1083
+ var previousValue = history_1[newIndex];
1084
+ if (previousValue !== undefined) {
1085
+ currentState.set(previousValue);
1086
+ return true;
1087
+ }
1088
+ }
1089
+ return false;
1090
+ };
1091
+ var redo = function () {
1092
+ var currentIndex = historyIndexState.get();
1093
+ var history = historyState.get();
1094
+ if (currentIndex < history.length - 1) {
1095
+ var newIndex = currentIndex + 1;
1096
+ historyIndexState.set(newIndex);
1097
+ var nextValue = history[newIndex];
1098
+ if (nextValue !== undefined) {
1099
+ currentState.set(nextValue);
1100
+ return true;
1101
+ }
1102
+ }
1103
+ return false;
1104
+ };
1105
+ var canUndo = function () { return historyIndexState.get() > 0; };
1106
+ var canRedo = function () {
1107
+ return historyIndexState.get() < historyState.get().length - 1;
1108
+ };
1109
+ var clearHistory = function () {
1110
+ var current = currentState.get();
1111
+ historyState.set([current]);
1112
+ historyIndexState.set(0);
1113
+ };
1114
+ return __assign(__assign({}, currentState), { set: function (value) {
1115
+ var newValue = typeof value === 'function' ? value(currentState.get()) : value;
1116
+ set(newValue);
1117
+ }, patch: function (value) {
1118
+ var patchValue = typeof value === 'function' ? value(currentState.get()) : value;
1119
+ set(function (currentValue) { return merge(currentValue, patchValue); });
1120
+ }, undo: undo, redo: redo, canUndo: canUndo, canRedo: canRedo, clearHistory: clearHistory });
1121
+ };
1122
+
1123
+ /**
1124
+ * Utility function to check if a scene is valid for state management
1125
+ * @param scene The scene to validate
1126
+ * @returns true if scene is valid, false otherwise
1127
+ */
1128
+ var isValidScene = function (scene) {
1129
+ return (scene != null &&
1130
+ typeof scene === 'object' &&
1131
+ 'registry' in scene &&
1132
+ 'scene' in scene);
1133
+ };
1134
+
1135
+ exports.batchStateUpdates = batchStateUpdates;
1136
+ exports.isValidScene = isValidScene;
1137
+ exports.validators = validators;
1138
+ exports.withComputedState = withComputedState;
1139
+ exports.withDebouncedState = withDebouncedState;
1140
+ exports.withGlobalState = withGlobalState;
1141
+ exports.withLocalState = withLocalState;
1142
+ exports.withPersistentState = withPersistentState;
1143
+ exports.withStateDef = withStateDef;
1144
+ exports.withUndoableState = withUndoableState;
1145
+
1146
+ }));