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.
- package/README.md +213 -677
- package/dist/hooks/type.d.ts +20 -2
- package/dist/hooks/type.d.ts.map +1 -1
- package/dist/hooks/with-debounced-state.d.ts.map +1 -1
- package/dist/hooks/with-debounced-state.js +8 -1
- package/dist/hooks/with-debounced-state.js.map +1 -1
- package/dist/hooks/with-local-state.spec.js +598 -0
- package/dist/hooks/with-local-state.spec.js.map +1 -1
- package/dist/hooks/with-state-def.d.ts +6 -0
- package/dist/hooks/with-state-def.d.ts.map +1 -1
- package/dist/hooks/with-state-def.js +94 -17
- 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 +12 -3
- package/dist/hooks/with-undoable-state.js.map +1 -1
- package/dist/phaser-hooks.js +1146 -0
- package/dist/phaser-hooks.min.js +1 -0
- package/dist/utils/__tests__/merge.test.d.ts +2 -0
- package/dist/utils/__tests__/merge.test.d.ts.map +1 -0
- package/dist/utils/__tests__/merge.test.js +390 -0
- package/dist/utils/__tests__/merge.test.js.map +1 -0
- package/dist/utils/merge.d.ts +17 -0
- package/dist/utils/merge.d.ts.map +1 -0
- package/dist/utils/merge.js +70 -0
- package/dist/utils/merge.js.map +1 -0
- package/package.json +8 -3
|
@@ -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
|
+
}));
|