dopaminejs 2.1.0 → 2.3.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/dist/dopamine.js CHANGED
@@ -1,4 +1,5 @@
1
- class C {
1
+ import { Animator as j, Collider as X, Component as J, Director as V, DopamineKernel as W, EventBus as K, Game as Y, GameObject as Q, GlobalInput as Z, GlobalLoader as tt, GlobalPhysics as et, Input as st, Loader as it, ParticleEmitter as at, Physics as nt, PluginRegistry as rt, Renderer as ot, Scene as lt, ScreenShake as ct, Sprite as ht, SystemRegistry as dt, Ticker as ut, Vector2 as pt } from "./engine.js";
2
+ class T {
2
3
  constructor() {
3
4
  this.listeners = /* @__PURE__ */ new Map();
4
5
  }
@@ -8,28 +9,28 @@ class C {
8
9
  * @param {Function} callback - Callback function
9
10
  * @returns {Function} Unsubscribe function
10
11
  */
11
- on(e, t) {
12
- return this.listeners.has(e) || this.listeners.set(e, /* @__PURE__ */ new Set()), this.listeners.get(e).add(t), () => this.off(e, t);
12
+ on(t, e) {
13
+ return this.listeners.has(t) || this.listeners.set(t, /* @__PURE__ */ new Set()), this.listeners.get(t).add(e), () => this.off(t, e);
13
14
  }
14
15
  /**
15
16
  * Unsubscribe from an event
16
17
  * @param {string} event - Event name
17
18
  * @param {Function} callback - Callback function
18
19
  */
19
- off(e, t) {
20
- this.listeners.has(e) && this.listeners.get(e).delete(t);
20
+ off(t, e) {
21
+ this.listeners.has(t) && this.listeners.get(t).delete(e);
21
22
  }
22
23
  /**
23
24
  * Emit an event
24
25
  * @param {string} event - Event name
25
26
  * @param {*} data - Event data
26
27
  */
27
- emit(e, t) {
28
- this.listeners.has(e) && this.listeners.get(e).forEach((s) => {
28
+ emit(t, e) {
29
+ this.listeners.has(t) && this.listeners.get(t).forEach((s) => {
29
30
  try {
30
- s(t);
31
+ s(e);
31
32
  } catch (i) {
32
- console.error(`Error in listener for event '${e}':`, i);
33
+ console.error(`Error in listener for event '${t}':`, i);
33
34
  }
34
35
  });
35
36
  }
@@ -40,17 +41,68 @@ class C {
40
41
  this.listeners.clear();
41
42
  }
42
43
  }
43
- class O extends C {
44
- constructor(e, t = {}) {
45
- super(), this.dataService = e, this.player = null, this.achievements = { ...this._initAchievements(), ...t.achievements || {} };
44
+ const E = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]), y = (r) => typeof r == "string" && r.length > 0 && !E.has(r), f = (r, t) => Object.prototype.hasOwnProperty.call(r, t), m = (r) => r !== null && typeof r == "object" && !Array.isArray(r), v = (r, t, e = 0) => {
45
+ const s = Number(r);
46
+ return Number.isInteger(s) && s >= e ? s : t;
47
+ };
48
+ class D extends T {
49
+ /**
50
+ * @param {Object} dataService - Persistence, see DataService
51
+ * @param {Object} [config]
52
+ * @param {Object} [config.achievements] - Extra achievement definitions
53
+ * @param {Object} [config.events] - Kernel EventBus to mirror events onto
54
+ * @param {Object} [config.kernel] - Kernel; its `events` bus is used if present
55
+ */
56
+ constructor(t, e = {}) {
57
+ super(), this.dataService = t, this.player = null, this.achievements = { ...this._initAchievements(), ...e.achievements || {} }, this.events = e.events || e.kernel?.events || null, this._batchDepth = 0, this._pendingSave = !1;
58
+ }
59
+ /**
60
+ * Emit locally, and mirror onto the kernel bus when one is attached.
61
+ * @private
62
+ */
63
+ _publish(t, e) {
64
+ this.emit(t, e), this.events && this.events.emit(t, e);
65
+ }
66
+ /**
67
+ * Run `fn` with saves collapsed into a single write at the end.
68
+ *
69
+ * Nested batches are counted, so an inner operation that batches on its
70
+ * own still results in exactly one write for the outermost call.
71
+ *
72
+ * @param {Function} fn
73
+ * @private
74
+ */
75
+ async _batch(t) {
76
+ this._batchDepth++;
77
+ try {
78
+ return await t();
79
+ } finally {
80
+ this._batchDepth--, this._batchDepth === 0 && this._pendingSave && (this._pendingSave = !1, await this._write());
81
+ }
46
82
  }
47
83
  /**
48
84
  * Initialize the reward system for current player
49
85
  * @returns {Promise<Object>} Player data
50
86
  */
51
87
  async init() {
52
- const e = await this.dataService.load("player", null);
53
- return this.player = this._migrate(e), this._updateDailyStreak(), await this.save(), this.player;
88
+ const t = await this.dataService.load("player", null);
89
+ this.player = this._migrate(t);
90
+ const e = this._updateDailyStreak();
91
+ return await this.save(), e && this._publishStreak(), this.player;
92
+ }
93
+ /**
94
+ * @private
95
+ */
96
+ _requirePlayer() {
97
+ if (!this.player)
98
+ throw new Error("[DopamineJS] RewardSystem.init() must be awaited before use");
99
+ }
100
+ /**
101
+ * @private
102
+ */
103
+ _publishStreak() {
104
+ const { current: t, longest: e } = this.player.streak;
105
+ this._publish("streak_updated", { current: t, longest: e });
54
106
  }
55
107
  /**
56
108
  * Get default player data structure
@@ -86,21 +138,80 @@ class O extends C {
86
138
  * @returns {Object} Player object guaranteed to have every field
87
139
  * @private
88
140
  */
89
- _migrate(e) {
90
- const t = this._getDefaultPlayer();
91
- return !e || typeof e != "object" ? t : {
92
- ...t,
141
+ _migrate(t) {
142
+ const e = this._getDefaultPlayer();
143
+ if (!m(t))
144
+ return e;
145
+ const s = Number(t.xp), i = Number.isFinite(s) && s >= 0 ? s : 0, n = this._calculateLevel(i), a = m(t.streak) ? t.streak : {}, o = v(a.current, 1, 1), l = typeof a.lastPlayDate == "string" && /^\d{4}-\d{2}-\d{2}$/.test(a.lastPlayDate) ? a.lastPlayDate : e.streak.lastPlayDate;
146
+ return {
93
147
  ...e,
94
- streak: { ...t.streak, ...e.streak || {} },
95
- achievements: e.achievements || {},
96
- stats: e.stats || {}
148
+ ...t,
149
+ name: typeof t.name == "string" ? t.name : e.name,
150
+ xp: i,
151
+ // A level above the curve is kept: older versions banked levels
152
+ // at half the XP and those players keep what they earned.
153
+ level: Math.max(n, v(t.level, n, 1)),
154
+ totalGamesPlayed: v(t.totalGamesPlayed, 0),
155
+ createdAt: Number.isFinite(t.createdAt) ? t.createdAt : e.createdAt,
156
+ lastPlayedAt: Number.isFinite(t.lastPlayedAt) ? t.lastPlayedAt : e.lastPlayedAt,
157
+ streak: {
158
+ current: o,
159
+ longest: Math.max(o, v(a.longest, o, 1)),
160
+ lastPlayDate: l
161
+ },
162
+ achievements: this._migrateAchievements(t.achievements),
163
+ stats: this._migrateStats(t.stats)
97
164
  };
98
165
  }
166
+ /**
167
+ * @private
168
+ */
169
+ _migrateAchievements(t) {
170
+ const e = {};
171
+ if (!m(t)) return e;
172
+ for (const s of Object.keys(t)) {
173
+ const i = t[s];
174
+ !y(s) || !m(i) || (e[s] = {
175
+ unlockedAt: Number.isFinite(i.unlockedAt) ? i.unlockedAt : Date.now(),
176
+ seen: i.seen === !0
177
+ });
178
+ }
179
+ return e;
180
+ }
181
+ /**
182
+ * @private
183
+ */
184
+ _migrateStats(t) {
185
+ const e = {};
186
+ if (!m(t)) return e;
187
+ for (const s of Object.keys(t)) {
188
+ const i = t[s];
189
+ if (!y(s) || !m(i)) continue;
190
+ const n = {};
191
+ for (const a of Object.keys(i))
192
+ y(a) && Number.isFinite(i[a]) && (n[a] = i[a]);
193
+ n.totalPlays = v(i.totalPlays, 0), n.highScore = Number.isFinite(i.highScore) ? i.highScore : 0, e[s] = n;
194
+ }
195
+ return e;
196
+ }
99
197
  /**
100
198
  * Save player data
101
199
  */
102
200
  async save() {
103
- this.player && (this.player.lastPlayedAt = Date.now(), await this.dataService.save("player", this.player));
201
+ if (this.player) {
202
+ if (this._batchDepth > 0) {
203
+ this._pendingSave = !0;
204
+ return;
205
+ }
206
+ await this._write();
207
+ }
208
+ }
209
+ /**
210
+ * The actual persistence call. Bypasses batching.
211
+ * @private
212
+ */
213
+ async _write() {
214
+ this.player.lastPlayedAt = Date.now(), await this.dataService.save("player", this.player) === !1 && this._publish("save_failed", { key: "player" });
104
215
  }
105
216
  /**
106
217
  * Add XP and handle level ups
@@ -108,30 +219,35 @@ class O extends C {
108
219
  * @param {string} reason - Why XP was awarded (for notifications)
109
220
  * @returns {Object} { leveledUp: boolean, newLevel: number, xpGained: number }
110
221
  */
111
- async addXP(e, t = "") {
112
- if (!Number.isFinite(e))
113
- throw new TypeError(`[DopamineJS] addXP expects a finite number, received ${e}`);
222
+ async addXP(t, e = "") {
223
+ if (!Number.isFinite(t))
224
+ throw new TypeError(`[DopamineJS] addXP expects a finite number, received ${t}`);
225
+ this._requirePlayer();
114
226
  const s = this.player.level;
115
- this.player.xp += e;
116
- const i = this._calculateLevel(this.player.xp), r = i > s;
117
- return r && (this.player.level = i), await this.save(), this.emit("xp_gained", { amount: e, reason: t, leveledUp: r, newLevel: i }), r && this.emit("level_up", { oldLevel: s, newLevel: i }), { leveledUp: r, newLevel: i, xpGained: e };
227
+ t = Math.max(t, -this.player.xp), this.player.xp += t;
228
+ const i = Math.max(s, this._calculateLevel(this.player.xp)), n = i > s;
229
+ n && (this.player.level = i), await this.save();
230
+ const a = this.player.xp;
231
+ return this._publish("xp_gained", { amount: t, reason: e, leveledUp: n, newLevel: i, total: a }), n && this._publish("level_up", { oldLevel: s, newLevel: i, totalXP: a }), { leveledUp: n, newLevel: i, xpGained: t };
118
232
  }
119
233
  /**
120
234
  * Calculate level from XP (exponential curve)
121
235
  */
122
- _calculateLevel(e) {
123
- const t = Math.floor((1 + Math.sqrt(1 + 4 * e / 50)) / 2);
124
- return Math.max(1, t);
236
+ _calculateLevel(t) {
237
+ if (!(t > 0)) return 1;
238
+ const e = Math.floor((1 + Math.sqrt(1 + 4 * t / 50)) / 2);
239
+ return Math.max(1, e);
125
240
  }
126
241
  /**
127
242
  * Get XP needed for next level
128
243
  */
129
244
  getXPForNextLevel() {
130
- const e = this.player.level + 1, t = 50 * e * (e - 1), s = 50 * this.player.level * (this.player.level - 1), i = t - s, r = i > 0 ? Math.max(0, Math.min(1, (this.player.xp - s) / i)) : 0;
245
+ this._requirePlayer();
246
+ const t = this.player.level + 1, e = 50 * t * (t - 1), s = 50 * this.player.level * (this.player.level - 1), i = e - s, n = i > 0 ? Math.max(0, Math.min(1, (this.player.xp - s) / i)) : 0;
131
247
  return {
132
- total: t,
133
- needed: Math.max(0, t - this.player.xp),
134
- progress: r
248
+ total: e,
249
+ needed: Math.max(0, e - this.player.xp),
250
+ progress: n
135
251
  };
136
252
  }
137
253
  /**
@@ -139,86 +255,103 @@ class O extends C {
139
255
  * @param {string} gameName - Unique ID for the game
140
256
  * @param {Object} result - Game-specific result data
141
257
  */
142
- async recordGame(e, t) {
143
- this.player.stats[e] || (this.player.stats[e] = { totalPlays: 0, highScore: 0 });
144
- const s = this.player.stats[e];
145
- s.totalPlays++, this.player.totalGamesPlayed++, t.score !== void 0 && t.score > s.highScore && (s.highScore = t.score, this.emit("new_high_score", { gameName: e, score: t.score })), Object.keys(t).forEach((r) => {
146
- r !== "score" && (s[r] = (s[r] || 0) + (typeof t[r] == "number" ? t[r] : 0));
258
+ async recordGame(t, e = {}) {
259
+ if (this._requirePlayer(), !y(t))
260
+ throw new TypeError(`[DopamineJS] recordGame expects a game name, received ${String(t)}`);
261
+ if (!m(e))
262
+ throw new TypeError("[DopamineJS] recordGame expects a result object");
263
+ if (e.score !== void 0 && !Number.isFinite(e.score))
264
+ throw new TypeError(`[DopamineJS] recordGame expects a finite score, received ${String(e.score)}`);
265
+ return this._batch(() => this._recordGame(t, e));
266
+ }
267
+ /**
268
+ * @private
269
+ */
270
+ async _recordGame(t, e) {
271
+ this._updateDailyStreak() && this._publishStreak(), f(this.player.stats, t) || (this.player.stats[t] = { totalPlays: 0, highScore: 0 });
272
+ const s = this.player.stats[t];
273
+ s.totalPlays++, this.player.totalGamesPlayed++, e.score !== void 0 && e.score > s.highScore && (s.highScore = e.score, this._publish("new_high_score", { gameName: t, score: e.score })), Object.keys(e).forEach((n) => {
274
+ if (n !== "score" && y(n)) {
275
+ const a = f(s, n) && Number.isFinite(s[n]) ? s[n] : 0;
276
+ s[n] = a + (Number.isFinite(e[n]) ? e[n] : 0);
277
+ }
147
278
  });
148
279
  let i = 10;
149
- t.score && (i += Math.floor(t.score / 5)), i = Math.floor(i * this.getStreakMultiplier()), await this.addXP(i, `Played ${e}`), await this.checkAchievements(e, t), await this.save();
280
+ e.score && (i += Math.max(0, Math.floor(e.score / 5))), i = Math.floor(i * this.getStreakMultiplier()), await this.addXP(i, `Played ${t}`), await this.checkAchievements(t, e), await this.save();
150
281
  }
151
282
  /**
152
283
  * Check and unlock achievements
153
284
  */
154
- async checkAchievements(e, t) {
285
+ async checkAchievements(t, e) {
155
286
  const s = [];
156
- for (const [i, r] of Object.entries(this.achievements)) {
157
- if (this.player.achievements[i]) continue;
158
- let n = !1;
287
+ for (const [i, n] of Object.entries(this.achievements)) {
288
+ if (f(this.player.achievements, i)) continue;
289
+ let a = !1;
159
290
  try {
160
- n = r.check(this.player, e, t);
291
+ a = n.check(this.player, t, e);
161
292
  } catch (o) {
162
293
  console.error(`[DopamineJS] Achievement "${i}" check threw:`, o);
163
294
  continue;
164
295
  }
165
- n && (await this.unlockAchievement(i), s.push(r));
296
+ a && await this.unlockAchievement(i) && s.push(n);
166
297
  }
167
298
  return s;
168
299
  }
169
300
  /**
170
301
  * Unlock an achievement
171
302
  */
172
- async unlockAchievement(e) {
173
- const t = this.achievements[e];
174
- return t ? (this.player.achievements[e] = {
303
+ async unlockAchievement(t) {
304
+ if (this._requirePlayer(), !y(t) || !f(this.achievements, t) || f(this.player.achievements, t)) return !1;
305
+ const e = this.achievements[t];
306
+ return e ? (this.player.achievements[t] = {
175
307
  unlockedAt: Date.now(),
176
308
  seen: !1
177
309
  }, await this.addXP(
178
- Number.isFinite(t.xp) ? t.xp : 0,
179
- `Achievement: ${t.name}`
180
- ), this.emit("achievement_unlocked", t), await this.save(), !0) : !1;
310
+ Number.isFinite(e.xp) ? e.xp : 0,
311
+ `Achievement: ${e.name}`
312
+ ), this._publish("achievement_unlocked", { ...e, id: t }), await this.save(), !0) : !1;
181
313
  }
182
314
  /**
183
315
  * Get all unlocked achievements
184
316
  */
185
317
  getUnlockedAchievements() {
186
- return Object.entries(this.player.achievements).map(([e, t]) => ({
187
- ...this.achievements[e],
188
- id: e,
189
- ...t
318
+ return Object.entries(this.player.achievements).map(([t, e]) => ({
319
+ ...this.achievements[t],
320
+ id: t,
321
+ ...e
190
322
  }));
191
323
  }
192
324
  /**
193
325
  * Get achievements user hasn't seen the notification for yet
194
326
  */
195
327
  getUnseenAchievements() {
196
- return this.getUnlockedAchievements().filter((e) => !e.seen);
328
+ return this.getUnlockedAchievements().filter((t) => !t.seen);
197
329
  }
198
330
  /**
199
331
  * Mark achievements as seen
200
332
  */
201
- async markAchievementsSeen(e) {
202
- for (const t of e)
203
- this.player.achievements[t] && (this.player.achievements[t].seen = !0);
333
+ async markAchievementsSeen(t) {
334
+ for (const e of t)
335
+ f(this.player.achievements, e) && (this.player.achievements[e].seen = !0);
204
336
  await this.save();
205
337
  }
206
338
  /**
207
339
  * Update daily streak
340
+ * @returns {boolean} True when the calendar day changed
208
341
  */
209
342
  _updateDailyStreak() {
210
- const e = this._getTodayDateString(), t = this.player.streak.lastPlayDate;
211
- if (t === e)
212
- return;
343
+ const t = this._getTodayDateString(), e = this.player.streak.lastPlayDate;
344
+ if (e === t)
345
+ return !1;
213
346
  const s = this._getYesterdayDateString();
214
- t === s ? (this.player.streak.current++, this.player.streak.current > this.player.streak.longest && (this.player.streak.longest = this.player.streak.current)) : this.player.streak.current = 1, this.player.streak.lastPlayDate = e;
347
+ return e === s ? (this.player.streak.current++, this.player.streak.current > this.player.streak.longest && (this.player.streak.longest = this.player.streak.current)) : this.player.streak.current = 1, this.player.streak.lastPlayDate = t, !0;
215
348
  }
216
349
  /**
217
350
  * Get streak multiplier for XP
218
351
  */
219
352
  getStreakMultiplier() {
220
- const e = this.player.streak.current;
221
- return e >= 7 ? 2 : e >= 5 ? 1.5 : e >= 3 ? 1.25 : 1;
353
+ const t = this.player.streak.current;
354
+ return t >= 7 ? 2 : t >= 5 ? 1.5 : t >= 3 ? 1.25 : 1;
222
355
  }
223
356
  /**
224
357
  * Format a Date as a YYYY-MM-DD string in the player's local timezone.
@@ -231,9 +364,9 @@ class O extends C {
231
364
  * @returns {string}
232
365
  * @private
233
366
  */
234
- _toLocalDateString(e) {
235
- const t = e.getFullYear(), s = String(e.getMonth() + 1).padStart(2, "0"), i = String(e.getDate()).padStart(2, "0");
236
- return `${t}-${s}-${i}`;
367
+ _toLocalDateString(t) {
368
+ const e = t.getFullYear(), s = String(t.getMonth() + 1).padStart(2, "0"), i = String(t.getDate()).padStart(2, "0");
369
+ return `${e}-${s}-${i}`;
237
370
  }
238
371
  /**
239
372
  * Get today's local date as YYYY-MM-DD string
@@ -245,8 +378,8 @@ class O extends C {
245
378
  * Get yesterday's local date as YYYY-MM-DD string
246
379
  */
247
380
  _getYesterdayDateString() {
248
- const e = /* @__PURE__ */ new Date();
249
- return e.setDate(e.getDate() - 1), this._toLocalDateString(e);
381
+ const t = /* @__PURE__ */ new Date();
382
+ return t.setDate(t.getDate() - 1), this._toLocalDateString(t);
250
383
  }
251
384
  /**
252
385
  * Initialize achievement definitions
@@ -260,77 +393,96 @@ class O extends C {
260
393
  description: "Play your first game",
261
394
  icon: "🎮",
262
395
  xp: 50,
263
- check: (e) => e.totalGamesPlayed >= 1
396
+ check: (t) => t.totalGamesPlayed >= 1
264
397
  },
265
398
  level_5: {
266
399
  name: "Rising Star",
267
400
  description: "Reach level 5",
268
401
  icon: "⭐",
269
402
  xp: 100,
270
- check: (e) => e.level >= 5
403
+ check: (t) => t.level >= 5
271
404
  },
272
405
  level_10: {
273
406
  name: "Expert Player",
274
407
  description: "Reach level 10",
275
408
  icon: "🏆",
276
409
  xp: 250,
277
- check: (e) => e.level >= 10
410
+ check: (t) => t.level >= 10
278
411
  },
279
412
  streak_3: {
280
413
  name: "Dedicated",
281
414
  description: "Play 3 days in a row",
282
415
  icon: "🔥",
283
416
  xp: 100,
284
- check: (e) => e.streak.current >= 3
417
+ check: (t) => t.streak.current >= 3
285
418
  },
286
419
  streak_7: {
287
420
  name: "Unstoppable",
288
421
  description: "Play 7 days in a row",
289
422
  icon: "💥",
290
423
  xp: 300,
291
- check: (e) => e.streak.current >= 7
424
+ check: (t) => t.streak.current >= 7
292
425
  }
293
426
  };
294
427
  }
295
428
  }
296
- function w() {
297
- const a = /* @__PURE__ */ new Map();
429
+ function P() {
430
+ const r = /* @__PURE__ */ new Map();
298
431
  return {
299
- getItem: (e) => a.has(e) ? a.get(e) : null,
300
- setItem: (e, t) => {
301
- a.set(e, String(t));
432
+ getItem: (t) => r.has(t) ? r.get(t) : null,
433
+ setItem: (t, e) => {
434
+ r.set(t, String(e));
302
435
  },
303
- removeItem: (e) => {
304
- a.delete(e);
436
+ removeItem: (t) => {
437
+ r.delete(t);
305
438
  },
306
439
  clear: () => {
307
- a.clear();
440
+ r.clear();
308
441
  }
309
442
  };
310
443
  }
311
- function S() {
444
+ function A() {
312
445
  try {
313
446
  if (typeof localStorage > "u")
314
- return w();
315
- const a = "__dopamine_probe__";
316
- return localStorage.setItem(a, "1"), localStorage.removeItem(a), localStorage;
447
+ return P();
448
+ const r = "__dopamine_probe__";
449
+ return localStorage.setItem(r, "1"), localStorage.removeItem(r), localStorage;
317
450
  } catch {
318
- return w();
451
+ return P();
319
452
  }
320
453
  }
321
- class A {
322
- constructor(e = {}) {
323
- this.storage = e.storage || S(), this.prefix = e.prefix || "dopamine_";
454
+ class I {
455
+ constructor(t = {}) {
456
+ this.storage = t.storage || A(), this.prefix = t.prefix || "dopamine_", this._tail = null;
457
+ }
458
+ /**
459
+ * Run a write after any write still in flight.
460
+ *
461
+ * Promise storage can finish two overlapping writes in either order, and
462
+ * the older value would win. Synchronous storage never has one in flight,
463
+ * so it still writes before this returns.
464
+ * @private
465
+ */
466
+ _write(t) {
467
+ const e = this._tail ? this._tail.then(t) : t();
468
+ if (typeof e?.then == "function") {
469
+ const s = Promise.resolve(e).catch(() => {
470
+ }).then(() => {
471
+ this._tail === s && (this._tail = null);
472
+ });
473
+ this._tail = s;
474
+ }
475
+ return e;
324
476
  }
325
477
  /**
326
478
  * Save data to local storage
327
479
  * @param {string} key
328
480
  * @param {any} data
329
481
  */
330
- async save(e, t) {
482
+ async save(t, e) {
331
483
  try {
332
- const s = JSON.stringify(t);
333
- return this.storage.setItem(this.prefix + e, s), !0;
484
+ const s = JSON.stringify(e);
485
+ return await this._write(() => this.storage.setItem(this.prefix + t, s)), !0;
334
486
  } catch (s) {
335
487
  return console.error("Error saving data:", s), !1;
336
488
  }
@@ -340,32 +492,33 @@ class A {
340
492
  * @param {string} key
341
493
  * @param {any} defaultValue
342
494
  */
343
- async load(e, t = null) {
495
+ async load(t, e = null) {
344
496
  try {
345
- const s = this.storage.getItem(this.prefix + e);
346
- return s ? JSON.parse(s) : t;
497
+ await this._tail;
498
+ const s = await this.storage.getItem(this.prefix + t);
499
+ return s ? JSON.parse(s) : e;
347
500
  } catch (s) {
348
- return console.error("Error loading data:", s), t;
501
+ return console.error("Error loading data:", s), e;
349
502
  }
350
503
  }
351
504
  /**
352
505
  * Clear specific data
353
506
  * @param {string} key
354
507
  */
355
- async clear(e) {
356
- this.storage.removeItem(this.prefix + e);
508
+ async clear(t) {
509
+ await this._write(() => this.storage.removeItem(this.prefix + t));
357
510
  }
358
511
  }
359
- const k = "game-ui-overlay";
360
- class D {
361
- constructor(e) {
362
- this.particleSystem = e, this.container = null, this.xpBar = null, this.levelDisplay = null, this.streakDisplay = null, this._timers = /* @__PURE__ */ new Set(), this._detached = [], this.init();
512
+ const M = "game-ui-overlay";
513
+ class L {
514
+ constructor(t) {
515
+ this.particleSystem = t, this.container = null, this.xpBar = null, this.levelDisplay = null, this.streakDisplay = null, this._timers = /* @__PURE__ */ new Set(), this._detached = [], this.init();
363
516
  }
364
517
  /**
365
518
  * Initialize UI overlay
366
519
  */
367
520
  init() {
368
- this.container = document.createElement("div"), this.container.className = "game-ui-overlay", document.getElementById(k) || (this.container.id = k), this.container.innerHTML = `
521
+ this.container = document.createElement("div"), this.container.className = "game-ui-overlay", document.getElementById(M) || (this.container.id = M), this.container.innerHTML = `
369
522
  <div class="game-ui-top-bar">
370
523
  <div class="level-badge">
371
524
  <span class="level-label">LVL</span>
@@ -384,32 +537,46 @@ class D {
384
537
  <span class="streak-number">1</span>
385
538
  </div>
386
539
  </div>
387
- `, document.body.appendChild(this.container), this.levelBadge = this.container.querySelector(".level-badge"), this.streakBadge = this.container.querySelector(".streak-badge"), this.xpBar = this.container.querySelector(".xp-bar-fill"), this.xpText = this.container.querySelector(".xp-text"), this.levelDisplay = this.container.querySelector(".level-number"), this.streakDisplay = this.container.querySelector(".streak-number");
540
+ `, this.liveRegion = document.createElement("div"), this.liveRegion.className = "dopamine-live-region", this.liveRegion.setAttribute("aria-live", "polite"), this.liveRegion.setAttribute("role", "status"), Object.assign(this.liveRegion.style, {
541
+ position: "absolute",
542
+ width: "1px",
543
+ height: "1px",
544
+ overflow: "hidden",
545
+ clipPath: "inset(50%)",
546
+ whiteSpace: "nowrap"
547
+ }), this.container.appendChild(this.liveRegion), document.body.appendChild(this.container), this.levelBadge = this.container.querySelector(".level-badge"), this.streakBadge = this.container.querySelector(".streak-badge"), this.xpBar = this.container.querySelector(".xp-bar-fill"), this.xpText = this.container.querySelector(".xp-text"), this.levelDisplay = this.container.querySelector(".level-number"), this.streakDisplay = this.container.querySelector(".streak-number");
388
548
  }
389
549
  /**
390
550
  * setTimeout that destroy() can cancel.
391
551
  * @private
392
552
  */
393
- _defer(e, t) {
553
+ _defer(t, e) {
394
554
  const s = setTimeout(() => {
395
- this._timers.delete(s), e();
396
- }, t);
555
+ this._timers.delete(s), t();
556
+ }, e);
397
557
  return this._timers.add(s), s;
398
558
  }
399
559
  /**
400
560
  * Append a transient element and remember it for cleanup.
401
561
  * @private
402
562
  */
403
- _mount(e) {
404
- return document.body.appendChild(e), this._detached.push(e), e;
563
+ _mount(t) {
564
+ return document.body.appendChild(t), this._detached.push(t), t;
565
+ }
566
+ /**
567
+ * Repeat a popup as text for assistive technology.
568
+ * @private
569
+ */
570
+ _announce(t) {
571
+ this.liveRegion && (this.liveRegion.textContent = t);
405
572
  }
406
573
  /**
407
574
  * Replay a CSS animation by clearing it for one tick.
408
575
  * @private
409
576
  */
410
- _replayAnimation(e, t) {
411
- e && (e.style.animation = "none", this._defer(() => {
412
- e.style.animation = t;
577
+ _replayAnimation(t, e) {
578
+ t && (t.style.animation = "none", this._defer(() => {
579
+ t.style.animation = e;
413
580
  }, 10));
414
581
  }
415
582
  /**
@@ -417,22 +584,25 @@ class D {
417
584
  * @param {number} current - XP held right now
418
585
  * @param {number} needed - XP still required for the next level
419
586
  * @param {number} total - XP total that marks the next level
587
+ * @param {number} [progress] - Position within the current level, 0 to 1.
588
+ * Without it the bar falls back to current / total, which is lifetime
589
+ * XP and starts every level after the first partly full.
420
590
  */
421
- updateXP(e, t, s) {
422
- const i = s > 0 ? e / s * 100 : 0;
423
- this.xpBar.style.width = `${Math.max(0, Math.min(100, i))}%`, this.xpText.textContent = `${e} / ${s} XP`, this._replayAnimation(this.xpBar, "xp-pulse 0.3s ease-out");
591
+ updateXP(t, e, s, i) {
592
+ const n = Number.isFinite(i) ? i : s > 0 ? t / s : 0;
593
+ this.xpBar.style.width = `${Math.max(0, Math.min(100, n * 100))}%`, this.xpText.textContent = `${t} / ${s} XP`, this._replayAnimation(this.xpBar, "xp-pulse 0.3s ease-out");
424
594
  }
425
595
  /**
426
596
  * Update level display
427
597
  */
428
- updateLevel(e) {
429
- this.levelDisplay.textContent = e, this._replayAnimation(this.levelBadge, "level-pulse 0.5s ease-out");
598
+ updateLevel(t) {
599
+ this.levelDisplay.textContent = t, this._replayAnimation(this.levelBadge, "level-pulse 0.5s ease-out");
430
600
  }
431
601
  /**
432
602
  * Update streak display
433
603
  */
434
- updateStreak(e) {
435
- this.streakDisplay.textContent = e, e >= 7 ? this.streakBadge.style.background = "linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)" : e >= 3 && (this.streakBadge.style.background = "linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)");
604
+ updateStreak(t) {
605
+ this.streakDisplay.textContent = t, t >= 7 ? this.streakBadge.style.background = "linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)" : t >= 3 ? this.streakBadge.style.background = "linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)" : this.streakBadge.style.background = "";
436
606
  }
437
607
  /**
438
608
  * Show achievement unlocked popup
@@ -442,9 +612,9 @@ class D {
442
612
  * and could carry a player-supplied or server-supplied string, so it goes
443
613
  * in as text.
444
614
  */
445
- showAchievement(e) {
446
- const t = document.createElement("div");
447
- t.className = "achievement-popup", t.innerHTML = `
615
+ showAchievement(t) {
616
+ const e = document.createElement("div");
617
+ e.className = "achievement-popup", e.innerHTML = `
448
618
  <div class="achievement-shine"></div>
449
619
  <div class="achievement-content">
450
620
  <div class="achievement-icon"></div>
@@ -455,17 +625,17 @@ class D {
455
625
  <div class="achievement-xp"></div>
456
626
  </div>
457
627
  </div>
458
- `, t.querySelector(".achievement-icon").innerHTML = e.icon ?? "", t.querySelector(".achievement-name").textContent = e.name ?? "", t.querySelector(".achievement-desc").textContent = e.description ?? "", t.querySelector(".achievement-xp").textContent = `+${e.xp ?? 0} XP`, this._mount(t), this._defer(() => {
459
- const s = t.getBoundingClientRect();
628
+ `, e.querySelector(".achievement-icon").innerHTML = t.icon ?? "", e.querySelector(".achievement-name").textContent = t.name ?? "", e.querySelector(".achievement-desc").textContent = t.description ?? "", e.querySelector(".achievement-xp").textContent = `+${t.xp ?? 0} XP`, this._mount(e), this._announce(`Achievement unlocked: ${t.name ?? ""}`), this._defer(() => {
629
+ const s = e.getBoundingClientRect();
460
630
  this.particleSystem.confetti(s.left + s.width / 2, s.top + s.height / 2, 40);
461
631
  }, 300), this._defer(() => {
462
- t.style.animation = "slideOut 0.3s ease-in forwards", this._defer(() => this._remove(t), 300);
632
+ e.style.animation = "slideOut 0.3s ease-in forwards", this._defer(() => this._remove(e), 300);
463
633
  }, 4e3);
464
634
  }
465
635
  /**
466
636
  * Show level up screen
467
637
  */
468
- showLevelUp(e, t) {
638
+ showLevelUp(t, e) {
469
639
  const s = document.createElement("div");
470
640
  s.className = "level-up-overlay", s.innerHTML = `
471
641
  <div class="level-up-content">
@@ -473,11 +643,11 @@ class D {
473
643
  <div class="level-up-number"></div>
474
644
  <div class="level-up-subtitle">Amazing progress!</div>
475
645
  </div>
476
- `, s.querySelector(".level-up-number").textContent = t, this._mount(s), this._defer(() => {
646
+ `, s.querySelector(".level-up-number").textContent = e, this._mount(s), this._announce(`Level up: ${e}`), this._defer(() => {
477
647
  for (let i = 0; i < 5; i++)
478
648
  this._defer(() => {
479
- const r = Math.random() * window.innerWidth, n = Math.random() * window.innerHeight * 0.6;
480
- this.particleSystem.confetti(r, n, 30);
649
+ const n = Math.random() * window.innerWidth, a = Math.random() * window.innerHeight * 0.6;
650
+ this.particleSystem.confetti(n, a, 30);
481
651
  }, i * 200);
482
652
  }, 300), this._defer(() => {
483
653
  s.style.opacity = "0", this._defer(() => this._remove(s), 500);
@@ -486,34 +656,34 @@ class D {
486
656
  /**
487
657
  * Show floating text (e.g., "+50 XP", "COMBO!")
488
658
  */
489
- showFloatingText(e, t, s, i = "#ffd700", r = "24px") {
490
- const n = document.createElement("div");
491
- n.className = "floating-text", n.textContent = e, n.style.left = t + "px", n.style.top = s + "px", n.style.color = i, n.style.fontSize = r, this._mount(n), this._defer(() => this._remove(n), 2e3);
659
+ showFloatingText(t, e, s, i = "#ffd700", n = "24px") {
660
+ const a = document.createElement("div");
661
+ a.className = "floating-text", a.textContent = t, a.style.left = e + "px", a.style.top = s + "px", a.style.color = i, a.style.fontSize = n, this._mount(a), this._defer(() => this._remove(a), 2e3);
492
662
  }
493
663
  /**
494
664
  * Show combo multiplier
495
665
  */
496
- showCombo(e, t, s) {
497
- this.showFloatingText(`${e}× COMBO!`, t, s, "#ff6b6b", "32px"), this.particleSystem.fire(t, s, 20);
666
+ showCombo(t, e, s) {
667
+ this.showFloatingText(`${t}× COMBO!`, e, s, "#ff6b6b", "32px"), this.particleSystem.fire(e, s, 20);
498
668
  }
499
669
  /**
500
670
  * Show "near miss" indicator
501
671
  */
502
- showNearMiss(e, t) {
503
- this.showFloatingText("CLOSE CALL!", e, t, "#45b7d1", "20px"), this.particleSystem.sparkle(e, t, 15, "#45b7d1");
672
+ showNearMiss(t, e) {
673
+ this.showFloatingText("CLOSE CALL!", t, e, "#45b7d1", "20px"), this.particleSystem.sparkle(t, e, 15, "#45b7d1");
504
674
  }
505
675
  /**
506
676
  * Show lucky moment (random multiplier)
507
677
  */
508
- showLuckyMoment(e, t, s) {
509
- this.showFloatingText(`LUCKY ${e}×!`, t, s, "#ffd700", "36px"), this.particleSystem.starBurst(t, s, 12), this.particleSystem.confetti(t, s, 25);
678
+ showLuckyMoment(t, e, s) {
679
+ this.showFloatingText(`LUCKY ${t}×!`, e, s, "#ffd700", "36px"), this.particleSystem.starBurst(e, s, 12), this.particleSystem.confetti(e, s, 25);
510
680
  }
511
681
  /**
512
682
  * Show generic notification
513
683
  */
514
- showNotification(e, t = "normal") {
684
+ showNotification(t, e = "normal") {
515
685
  const s = window.innerWidth / 2, i = window.innerHeight / 3;
516
- t === "legendary" ? this.showLuckyMoment(e, s, i) : t === "rare" ? this.showCombo(e, s, i) : this.showFloatingText(e, s, i, "#fff", "24px");
686
+ e === "legendary" ? (this.showFloatingText(t, s, i, "#ffd700", "36px"), this.particleSystem.starBurst(s, i, 12), this.particleSystem.confetti(s, i, 25)) : e === "rare" ? (this.showFloatingText(t, s, i, "#ff6b6b", "32px"), this.particleSystem.fire(s, i, 20)) : this.showFloatingText(t, s, i, "#fff", "24px"), this._announce(t);
517
687
  }
518
688
  /**
519
689
  * Show Game Over Summary
@@ -524,121 +694,130 @@ class D {
524
694
  * @param {Function} [data.onReplay] - Replay button handler (default: reload)
525
695
  * @param {Function} [data.onExit] - Exit button handler (button hidden if omitted)
526
696
  */
527
- showSummary(e) {
528
- const t = document.createElement("div");
529
- t.className = "message-box", t.style.display = "flex", t.style.position = "absolute", t.style.top = "50%", t.style.left = "50%", t.style.transform = "translate(-50%, -50%)", t.style.zIndex = "1000";
697
+ showSummary(t) {
698
+ const e = document.createElement("div");
699
+ e.className = "message-box", e.style.display = "flex", e.style.position = "absolute", e.style.top = "50%", e.style.left = "50%", e.style.transform = "translate(-50%, -50%)", e.style.zIndex = "1000";
530
700
  const s = document.createElement("h1");
531
701
  s.textContent = "Game Over";
532
702
  const i = document.createElement("div");
533
- i.className = "summary-score", i.style.cssText = "font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;", i.textContent = e.score;
534
- const r = document.createElement("div");
535
- r.className = "summary-metrics", r.style.cssText = "width: 100%; margin-bottom: 20px;";
536
- for (const [o, c] of Object.entries(e.metrics || {})) {
703
+ i.className = "summary-score", i.style.cssText = "font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;", i.textContent = t.score;
704
+ const n = document.createElement("div");
705
+ n.className = "summary-metrics", n.style.cssText = "width: 100%; margin-bottom: 20px;";
706
+ for (const [o, l] of Object.entries(t.metrics || {})) {
537
707
  const h = document.createElement("div");
538
708
  h.style.cssText = "display:flex;justify-content:space-between;width:100%";
539
- const l = document.createElement("span");
540
- l.textContent = `${o}:`;
709
+ const d = document.createElement("span");
710
+ d.textContent = `${o}:`;
541
711
  const u = document.createElement("strong");
542
- u.textContent = c, h.append(l, u), r.appendChild(h);
712
+ u.textContent = l, h.append(d, u), n.appendChild(h);
543
713
  }
544
- t.append(s, i, r);
545
- const n = document.createElement("button");
546
- if (n.className = "btn", n.textContent = "Play Again", n.addEventListener("click", e.onReplay || (() => window.location.reload())), t.appendChild(n), e.onExit) {
714
+ e.append(s, i, n);
715
+ const a = document.createElement("button");
716
+ if (a.className = "btn", a.textContent = "Play Again", a.addEventListener("click", t.onReplay || (() => window.location.reload())), e.appendChild(a), t.onExit) {
547
717
  const o = document.createElement("button");
548
- o.className = "btn", o.textContent = "Exit", o.style.cssText = "margin-top: 10px; background: #543847;", o.addEventListener("click", e.onExit), t.appendChild(o);
718
+ o.className = "btn", o.textContent = "Exit", o.style.cssText = "margin-top: 10px; background: #543847;", o.addEventListener("click", t.onExit), e.appendChild(o);
549
719
  }
550
- return this._mount(t), t;
720
+ return this._mount(e), e;
551
721
  }
552
722
  /**
553
723
  * @private
554
724
  */
555
- _remove(e) {
556
- e.remove();
557
- const t = this._detached.indexOf(e);
558
- t > -1 && this._detached.splice(t, 1);
725
+ _remove(t) {
726
+ t.remove();
727
+ const e = this._detached.indexOf(t);
728
+ e > -1 && this._detached.splice(e, 1);
559
729
  }
560
730
  /**
561
731
  * Tear down the overlay, any transient popups, and all pending timers.
562
732
  */
563
733
  destroy() {
564
- var e;
565
734
  for (const t of this._timers)
566
735
  clearTimeout(t);
567
736
  this._timers.clear();
568
737
  for (const t of this._detached)
569
738
  t.remove();
570
- this._detached = [], (e = this.container) == null || e.remove(), this.container = null, this.xpBar = null, this.xpText = null, this.levelBadge = null, this.streakBadge = null, this.levelDisplay = null, this.streakDisplay = null;
739
+ this._detached = [], this.container?.remove(), this.container = null, this.liveRegion = null, this.xpBar = null, this.xpText = null, this.levelBadge = null, this.streakBadge = null, this.levelDisplay = null, this.streakDisplay = null;
571
740
  }
572
741
  }
573
- class T {
574
- constructor(e = {}) {
575
- this.container = e.container ? typeof e.container == "string" ? document.querySelector(e.container) : e.container : document.body, this.canvasId = e.canvasId || "dopamine-particle-canvas", this.canvas = document.createElement("canvas"), this.canvas.classList.add("dopamine-particle-canvas"), document.getElementById(this.canvasId) || (this.canvas.id = this.canvasId), this.canvas.style.pointerEvents = "none", this.canvas.style.zIndex = e.zIndex || "9999", this.container === document.body ? (this.canvas.style.position = "fixed", this.canvas.style.top = "0", this.canvas.style.left = "0") : (this.canvas.style.position = "absolute", this.canvas.style.top = "0", this.canvas.style.left = "0", window.getComputedStyle(this.container).position === "static" && (this.container.style.position = "relative")), this.container.appendChild(this.canvas), this.ctx = this.canvas.getContext("2d"), this.particles = [], this.pool = [], this.sprites = /* @__PURE__ */ new Map(), this.customEffects = /* @__PURE__ */ new Map(), this.isAnimating = !1, this._onResize = () => this._resize(), this._resizeObserver = null, this._frameHandle = null, this._resize(), window.ResizeObserver && this.container !== document.body ? (this._resizeObserver = new ResizeObserver(this._onResize), this._resizeObserver.observe(this.container)) : window.addEventListener("resize", this._onResize);
742
+ class z {
743
+ constructor(t = {}) {
744
+ this.container = t.container ? typeof t.container == "string" ? document.querySelector(t.container) : t.container : document.body, this.container || (console.warn(`[DopamineJS] Particle container '${t.container}' not found. Using document.body.`), this.container = document.body), this.maxParticles = Number.isFinite(t.maxParticles) ? Math.max(0, Math.floor(t.maxParticles)) : 5e3, this._reducedMotion = t.respectReducedMotion !== !1 && typeof window.matchMedia == "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null, this.canvasId = t.canvasId || "dopamine-particle-canvas", this.canvas = document.createElement("canvas"), this.canvas.classList.add("dopamine-particle-canvas"), document.getElementById(this.canvasId) || (this.canvas.id = this.canvasId), this.canvas.style.pointerEvents = "none", this.canvas.style.zIndex = t.zIndex || "9999", this.container === document.body ? (this.canvas.style.position = "fixed", this.canvas.style.top = "0", this.canvas.style.left = "0") : (this.canvas.style.position = "absolute", this.canvas.style.top = "0", this.canvas.style.left = "0", window.getComputedStyle(this.container).position === "static" && (this.container.style.position = "relative")), this.container.appendChild(this.canvas), this.ctx = this.canvas.getContext("2d"), this.particles = [], this.pool = [], this.sprites = /* @__PURE__ */ new Map(), this.customEffects = /* @__PURE__ */ new Map(), this.isAnimating = !1, this._onResize = () => this._resize(), this._resizeObserver = null, this._frameHandle = null, this._lastFrameTime = null, this._resize(), window.ResizeObserver && this.container !== document.body ? (this._resizeObserver = new ResizeObserver(this._onResize), this._resizeObserver.observe(this.container)) : window.addEventListener("resize", this._onResize);
576
745
  }
577
746
  _resize() {
578
- const e = this.container === document.body ? window.innerWidth : this.container.clientWidth, t = this.container === document.body ? window.innerHeight : this.container.clientHeight, s = window.devicePixelRatio || 1;
579
- this.width = e, this.height = t, this.canvas.width = Math.round(e * s), this.canvas.height = Math.round(t * s), this.canvas.style.width = `${e}px`, this.canvas.style.height = `${t}px`, this.ctx.setTransform(s, 0, 0, s, 0, 0);
747
+ const t = this.container === document.body ? window.innerWidth : this.container.clientWidth, e = this.container === document.body ? window.innerHeight : this.container.clientHeight, s = window.devicePixelRatio || 1;
748
+ this.width = t, this.height = e, this.canvas.width = Math.round(t * s), this.canvas.height = Math.round(e * s), this.canvas.style.width = `${t}px`, this.canvas.style.height = `${e}px`, this.ctx.setTransform(s, 0, 0, s, 0, 0);
580
749
  }
581
750
  /**
582
751
  * Register a custom sprite image
583
752
  * @param {string} key
584
753
  * @param {string} url
585
754
  */
586
- registerSprite(e, t) {
755
+ registerSprite(t, e) {
587
756
  const s = new Image();
588
- s.src = t, this.sprites.set(e, s);
757
+ s.src = e, this.sprites.set(t, s);
589
758
  }
590
759
  /**
591
760
  * Register a custom effect
592
761
  * @param {string} name
593
762
  * @param {Function} callback - (x, y, ...args) => void
594
763
  */
595
- registerEffect(e, t) {
596
- this.customEffects.set(e, t);
764
+ registerEffect(t, e) {
765
+ this.customEffects.set(t, e);
597
766
  }
598
767
  /**
599
768
  * Play a registered effect
600
769
  */
601
- play(e, t, s, ...i) {
602
- const r = this.customEffects.get(e);
603
- r ? r(t, s, ...i) : console.warn(`[DopamineJS] Effect '${e}' not found.`);
770
+ play(t, e, s, ...i) {
771
+ const n = this.customEffects.get(t);
772
+ n ? n(e, s, ...i) : console.warn(`[DopamineJS] Effect '${t}' not found.`);
604
773
  }
605
774
  /**
606
775
  * Emit particles with custom config
607
776
  * @param {Object} config
608
777
  */
609
- emit(e) {
778
+ emit(t) {
610
779
  const {
611
- x: t,
780
+ x: e,
612
781
  y: s,
613
782
  count: i = 10,
614
- speed: r = 5,
615
- gravity: n = 0.1,
783
+ speed: n = 5,
784
+ gravity: a = 0.1,
616
785
  decay: o = 0.02,
617
- life: c = 1,
786
+ life: l = 1,
618
787
  color: h = "#ffd700",
619
- sprite: l = null,
788
+ sprite: d = null,
620
789
  spread: u = Math.PI * 2,
621
- angle: m = 0,
622
- size: f = 5
623
- } = e;
624
- for (let v = 0; v < i; v++) {
625
- const d = this._getParticle(), _ = m + (Math.random() - 0.5) * u, x = Math.random() * r;
626
- d.x = t, d.y = s, d.vx = Math.cos(_) * x, d.vy = Math.sin(_) * x, d.life = c, d.decay = o * (0.8 + Math.random() * 0.4), d.gravity = n, d.color = Array.isArray(h) ? h[Math.floor(Math.random() * h.length)] : h, d.size = f * (0.8 + Math.random() * 0.4), d.sprite = l, d.rotation = Math.random() * Math.PI * 2, d.rotationSpeed = (Math.random() - 0.5) * 0.2, this.particles.push(d);
790
+ angle: p = 0,
791
+ size: g = 5,
792
+ type: C = null
793
+ } = t, x = this._allowed(i);
794
+ for (let w = 0; w < x; w++) {
795
+ const c = this._getParticle(), S = p + (Math.random() - 0.5) * u, b = Math.random() * n;
796
+ c.x = e, c.y = s, c.vx = Math.cos(S) * b, c.vy = Math.sin(S) * b, c.life = l, c.decay = o * (0.8 + Math.random() * 0.4), c.gravity = a, c.color = Array.isArray(h) ? h[Math.floor(Math.random() * h.length)] : h, c.size = g * (0.8 + Math.random() * 0.4), c.sprite = d, c.type = C, c.rotation = Math.random() * Math.PI * 2, c.rotationSpeed = (Math.random() - 0.5) * 0.2, this.particles.push(c);
627
797
  }
628
- this._startAnimation();
798
+ x > 0 && this._startAnimation();
799
+ }
800
+ /**
801
+ * How many of `count` particles may be created right now.
802
+ * @private
803
+ */
804
+ _allowed(t) {
805
+ if (this._reducedMotion?.matches || !Number.isFinite(t)) return 0;
806
+ const e = this.maxParticles - this.particles.length;
807
+ return Math.max(0, Math.min(Math.floor(t), e));
629
808
  }
630
809
  _getParticle() {
631
810
  return this.pool.length > 0 ? this.pool.pop() : {};
632
811
  }
633
- _recycleParticle(e) {
634
- this.pool.push(e);
812
+ _recycleParticle(t) {
813
+ this.pool.push(t);
635
814
  }
636
815
  // --- Built-in Effects ---
637
- confetti(e, t, s = 50) {
816
+ confetti(t, e, s = 50) {
638
817
  const i = ["#ff6b6b", "#4ecdc4", "#45b7d1", "#f7dc6f", "#c06c84", "#6c5ce7"];
639
818
  this.emit({
640
- x: e,
641
- y: t,
819
+ x: t,
820
+ y: e,
642
821
  count: s,
643
822
  color: i,
644
823
  speed: 10,
@@ -648,10 +827,10 @@ class T {
648
827
  spread: Math.PI * 2
649
828
  });
650
829
  }
651
- coinShower(e, t, s = 10) {
830
+ coinShower(t, e, s = 10) {
652
831
  this.emit({
653
- x: e,
654
- y: t,
832
+ x: t,
833
+ y: e,
655
834
  count: s,
656
835
  color: "#ffd700",
657
836
  speed: 4,
@@ -663,10 +842,10 @@ class T {
663
842
  spread: Math.PI / 2
664
843
  });
665
844
  }
666
- sparkle(e, t, s = 20, i = "#ffd700") {
845
+ sparkle(t, e, s = 20, i = "#ffd700") {
667
846
  this.emit({
668
- x: e,
669
- y: t,
847
+ x: t,
848
+ y: e,
670
849
  count: s,
671
850
  color: i,
672
851
  speed: 3,
@@ -676,11 +855,11 @@ class T {
676
855
  size: 3
677
856
  });
678
857
  }
679
- fire(e, t, s = 15) {
858
+ fire(t, e, s = 15) {
680
859
  const i = ["#ff6b00", "#ff8800", "#ffaa00", "#ff4400"];
681
860
  this.emit({
682
- x: e,
683
- y: t,
861
+ x: t,
862
+ y: e,
684
863
  count: s,
685
864
  color: i,
686
865
  speed: 3,
@@ -691,42 +870,50 @@ class T {
691
870
  spread: Math.PI / 4
692
871
  });
693
872
  }
694
- starBurst(e, t, s = 8) {
695
- for (let i = 0; i < s; i++) {
696
- const r = this._getParticle(), n = Math.PI * 2 / s * i, o = 4;
697
- r.x = e, r.y = t, r.vx = Math.cos(n) * o, r.vy = Math.sin(n) * o, r.life = 1, r.decay = 0.03, r.gravity = 0, r.color = "#ffffff", r.size = 15, r.sprite = null, r.type = "star", r.rotation = n, r.rotationSpeed = 0, this.particles.push(r);
873
+ starBurst(t, e, s = 8) {
874
+ const i = this._allowed(s);
875
+ for (let n = 0; n < i; n++) {
876
+ const a = this._getParticle(), o = Math.PI * 2 / i * n, l = 4;
877
+ a.x = t, a.y = e, a.vx = Math.cos(o) * l, a.vy = Math.sin(o) * l, a.life = 1, a.decay = 0.03, a.gravity = 0, a.color = "#ffffff", a.size = 15, a.sprite = null, a.type = "star", a.rotation = o, a.rotationSpeed = 0, this.particles.push(a);
698
878
  }
699
- this._startAnimation();
879
+ i > 0 && this._startAnimation();
700
880
  }
701
881
  _startAnimation() {
702
- this.isAnimating || (this.isAnimating = !0, this._animate());
882
+ this.isAnimating || (this.isAnimating = !0, this._lastFrameTime = null, this._animate());
703
883
  }
704
- _animate() {
884
+ /**
885
+ * @param {number} [time] - requestAnimationFrame timestamp
886
+ */
887
+ _animate(t) {
705
888
  this._frameHandle = null, this.ctx.clearRect(0, 0, this.width, this.height);
706
- for (let e = this.particles.length - 1; e >= 0; e--) {
707
- const t = this.particles[e];
708
- if (t.vy += t.gravity, t.x += t.vx, t.y += t.vy, t.life -= t.decay, t.rotationSpeed && (t.rotation += t.rotationSpeed), t.life <= 0) {
709
- this._recycleParticle(t), this.particles.splice(e, 1);
889
+ let e = 1;
890
+ Number.isFinite(t) && (this._lastFrameTime !== null && (e = Math.max(0, Math.min(3, (t - this._lastFrameTime) / (1e3 / 60)))), this._lastFrameTime = t);
891
+ for (let s = this.particles.length - 1; s >= 0; s--) {
892
+ const i = this.particles[s];
893
+ if (i.vy += i.gravity * e, i.x += i.vx * e, i.y += i.vy * e, i.life -= i.decay * e, i.rotationSpeed && (i.rotation += i.rotationSpeed * e), i.life <= 0) {
894
+ this._recycleParticle(i);
895
+ const n = this.particles.pop();
896
+ s < this.particles.length && (this.particles[s] = n);
710
897
  continue;
711
898
  }
712
- if (this.ctx.save(), this.ctx.globalAlpha = t.life, this.ctx.translate(t.x, t.y), this.ctx.rotate(t.rotation), t.sprite && this.sprites.has(t.sprite)) {
713
- const s = this.sprites.get(t.sprite);
714
- s.complete && this.ctx.drawImage(s, -t.size / 2, -t.size / 2, t.size, t.size);
715
- } else t.type === "star" ? (this.ctx.fillStyle = t.color, this._drawStarPath(0, 0, 5, t.size, t.size / 2), this.ctx.fill()) : (this.ctx.fillStyle = t.color, this.ctx.beginPath(), this.ctx.arc(0, 0, t.size, 0, Math.PI * 2), this.ctx.fill());
899
+ if (this.ctx.save(), this.ctx.globalAlpha = i.life, this.ctx.translate(i.x, i.y), this.ctx.rotate(i.rotation), i.sprite && this.sprites.has(i.sprite)) {
900
+ const n = this.sprites.get(i.sprite);
901
+ n.complete && this.ctx.drawImage(n, -i.size / 2, -i.size / 2, i.size, i.size);
902
+ } else i.type === "star" ? (this.ctx.fillStyle = i.color, this._drawStarPath(0, 0, 5, i.size, i.size / 2), this.ctx.fill()) : (this.ctx.fillStyle = i.color, this.ctx.beginPath(), this.ctx.arc(0, 0, i.size, 0, Math.PI * 2), this.ctx.fill());
716
903
  this.ctx.restore();
717
904
  }
718
- this.particles.length > 0 ? this._frameHandle = requestAnimationFrame(() => this._animate()) : this.isAnimating = !1;
905
+ this.particles.length > 0 ? this._frameHandle = requestAnimationFrame((s) => this._animate(s)) : this.isAnimating = !1;
719
906
  }
720
- _drawStarPath(e, t, s, i, r) {
721
- let n = Math.PI / 2 * 3, o = e, c = t;
907
+ _drawStarPath(t, e, s, i, n) {
908
+ let a = Math.PI / 2 * 3, o = t, l = e;
722
909
  const h = Math.PI / s;
723
- this.ctx.beginPath(), this.ctx.moveTo(e, t - i);
724
- for (let l = 0; l < s; l++)
725
- o = e + Math.cos(n) * i, c = t + Math.sin(n) * i, this.ctx.lineTo(o, c), n += h, o = e + Math.cos(n) * r, c = t + Math.sin(n) * r, this.ctx.lineTo(o, c), n += h;
726
- this.ctx.lineTo(e, t - i), this.ctx.closePath();
910
+ this.ctx.beginPath(), this.ctx.moveTo(t, e - i);
911
+ for (let d = 0; d < s; d++)
912
+ o = t + Math.cos(a) * i, l = e + Math.sin(a) * i, this.ctx.lineTo(o, l), a += h, o = t + Math.cos(a) * n, l = e + Math.sin(a) * n, this.ctx.lineTo(o, l), a += h;
913
+ this.ctx.lineTo(t, e - i), this.ctx.closePath();
727
914
  }
728
915
  clear() {
729
- this.particles.forEach((e) => this._recycleParticle(e)), this.particles = [], this.ctx.clearRect(0, 0, this.width, this.height);
916
+ this.particles.forEach((t) => this._recycleParticle(t)), this.particles = [], this.ctx.clearRect(0, 0, this.width, this.height);
730
917
  }
731
918
  /**
732
919
  * Stop animating, detach resize handling, and remove the canvas.
@@ -738,18 +925,29 @@ class T {
738
925
  this.clear(), this.isAnimating = !1, this._frameHandle !== null && (cancelAnimationFrame(this._frameHandle), this._frameHandle = null), this._resizeObserver && (this._resizeObserver.disconnect(), this._resizeObserver = null), window.removeEventListener("resize", this._onResize), this.canvas.remove(), this.sprites.clear(), this.customEffects.clear(), this.pool = [];
739
926
  }
740
927
  }
741
- class I {
742
- constructor(e = {}) {
743
- this.audioContext = null, this.storageKey = e.storageKey || "dopamineSoundsMuted", this.storage = e.storage || S(), this.muted = this.storage.getItem(this.storageKey) === "true", this.assets = /* @__PURE__ */ new Map(), this.customSounds = e.customSounds || {}, this.masterGain = null, this._volume = e.volume ?? 1, Object.keys(this.customSounds).length > 0 && this.preloadSounds(this.customSounds);
928
+ const k = (r, t) => Object.prototype.hasOwnProperty.call(r, t);
929
+ class F {
930
+ constructor(t = {}) {
931
+ this.audioContext = null, this.storageKey = t.storageKey || "dopamineSoundsMuted", this.storage = t.storage || A(), this.muted = this.storage.getItem(this.storageKey) === "true", this.assets = /* @__PURE__ */ new Map(), this.customSounds = t.customSounds || {}, this.masterGain = null, this._volume = 1, this.setVolume(t.volume ?? 1), this._loading = /* @__PURE__ */ new Map(), this._timers = /* @__PURE__ */ new Set();
932
+ }
933
+ /**
934
+ * setTimeout that destroy() can cancel.
935
+ * @private
936
+ */
937
+ _later(t, e) {
938
+ const s = setTimeout(() => {
939
+ this._timers.delete(s), t();
940
+ }, e);
941
+ this._timers.add(s);
744
942
  }
745
943
  /**
746
944
  * Initialize Audio Context (must be called after user interaction)
747
945
  */
748
946
  initAudio() {
749
947
  if (!this.audioContext) {
750
- const e = typeof window < "u" ? window.AudioContext || window.webkitAudioContext : void 0;
751
- if (!e) return !1;
752
- this.audioContext = new e(), this.masterGain = this.audioContext.createGain(), this.masterGain.gain.value = this._volume, this.masterGain.connect(this.audioContext.destination);
948
+ const t = typeof window < "u" ? window.AudioContext || window.webkitAudioContext : void 0;
949
+ if (!t) return !1;
950
+ this.audioContext = new t(), this.masterGain = this.audioContext.createGain(), this.masterGain.gain.value = this._volume, this.masterGain.connect(this.audioContext.destination), this.preloadSounds(this.customSounds);
753
951
  }
754
952
  return this.audioContext.state === "suspended" && this.audioContext.resume(), !0;
755
953
  }
@@ -757,8 +955,8 @@ class I {
757
955
  * Set master output volume.
758
956
  * @param {number} value - 0..1
759
957
  */
760
- setVolume(e) {
761
- return this._volume = Math.max(0, Math.min(1, e)), this.masterGain && (this.masterGain.gain.value = this._volume), this._volume;
958
+ setVolume(t) {
959
+ return Number.isFinite(t) ? (this._volume = Math.max(0, Math.min(1, t)), this.masterGain && (this.masterGain.gain.value = this._volume), this._volume) : this._volume;
762
960
  }
763
961
  /**
764
962
  * Current master output volume.
@@ -786,45 +984,60 @@ class I {
786
984
  * @param {string} key - Unique identifier (e.g., 'jump', 'win')
787
985
  * @param {string} url - Path to audio file
788
986
  */
789
- registerSound(e, t) {
790
- this.customSounds[e] = t, this.audioContext && this.loadSound(e, t);
987
+ registerSound(t, e) {
988
+ this.customSounds[t] = e, this.audioContext && this.loadSound(t, e);
791
989
  }
792
990
  /**
793
991
  * Preload multiple sounds
794
992
  * @param {Object} soundMap - { key: url }
795
993
  */
796
- async preloadSounds(e) {
797
- const t = Object.entries(e).map(([s, i]) => this.loadSound(s, i));
798
- await Promise.allSettled(t);
994
+ async preloadSounds(t) {
995
+ const e = Object.entries(t).map(([s, i]) => this.loadSound(s, i));
996
+ await Promise.allSettled(e);
799
997
  }
800
998
  /**
801
999
  * Load and decode a sound file
802
1000
  */
803
- async loadSound(e, t) {
1001
+ async loadSound(t, e) {
1002
+ if (!this.audioContext) return;
1003
+ if (this._loading.has(t))
1004
+ return this._loading.get(t);
1005
+ const s = this._load(t, e).finally(() => this._loading.delete(t));
1006
+ return this._loading.set(t, s), s;
1007
+ }
1008
+ /**
1009
+ * @private
1010
+ */
1011
+ async _load(t, e) {
804
1012
  try {
805
- if (!this.audioContext)
806
- return;
807
- const i = await (await fetch(t)).arrayBuffer(), r = await this.audioContext.decodeAudioData(i);
808
- this.assets.set(e, r);
1013
+ const s = this.audioContext, i = await fetch(e);
1014
+ if (i.ok === !1)
1015
+ throw new Error(`HTTP ${i.status}`);
1016
+ const n = await i.arrayBuffer(), a = await s.decodeAudioData(n);
1017
+ this.audioContext === s && this.assets.set(t, a);
809
1018
  } catch (s) {
810
- console.warn(`[DopamineJS] Failed to load sound '${e}':`, s);
1019
+ console.warn(`[DopamineJS] Failed to load sound '${t}':`, s);
811
1020
  }
812
1021
  }
813
1022
  /**
814
1023
  * Play a sound by key (custom) or fallback to synth
815
1024
  * @param {string} key - 'jump', 'score', 'success', etc.
816
1025
  */
817
- async play(e) {
1026
+ async play(t) {
818
1027
  if (this.muted || !this.initAudio()) return;
819
- if (this.assets.has(e)) {
820
- this._playBuffer(this.assets.get(e));
1028
+ if (this.assets.has(t)) {
1029
+ this._playBuffer(this.assets.get(t));
821
1030
  return;
822
1031
  }
823
- if (this.customSounds[e] && !this.assets.has(e) && (await this.loadSound(e, this.customSounds[e]), this.assets.has(e))) {
824
- this._playBuffer(this.assets.get(e));
825
- return;
1032
+ if (k(this.customSounds, t) && !this.assets.has(t)) {
1033
+ const s = this.audioContext;
1034
+ if (await this.loadSound(t, this.customSounds[t]), this.audioContext !== s) return;
1035
+ if (this.assets.has(t)) {
1036
+ this._playBuffer(this.assets.get(t));
1037
+ return;
1038
+ }
826
1039
  }
827
- const t = {
1040
+ const e = {
828
1041
  jump: () => this.playJump(!0),
829
1042
  score: () => this.playScore(!0),
830
1043
  success: () => this.playSuccess(!0),
@@ -832,793 +1045,128 @@ class I {
832
1045
  error: () => this.playError(!0),
833
1046
  gameover: () => this.playGameOver(!0)
834
1047
  };
835
- t[e] && t[e]();
1048
+ k(e, t) && e[t]();
836
1049
  }
837
- _playBuffer(e) {
838
- const t = this.audioContext.createBufferSource();
839
- t.buffer = e, t.connect(this._output()), t.start(0);
1050
+ _playBuffer(t) {
1051
+ const e = this.audioContext.createBufferSource();
1052
+ e.buffer = t, e.connect(this._output()), e.start(0);
840
1053
  }
841
1054
  /**
842
1055
  * Play a synthesized tone
843
1056
  */
844
- playTone(e, t, s = "sine", i = 0.3) {
1057
+ playTone(t, e, s = "sine", i = 0.3) {
845
1058
  if (this.muted || !this.initAudio()) return;
846
- const r = this.audioContext.createOscillator(), n = this.audioContext.createGain();
847
- r.connect(n), n.connect(this._output()), r.frequency.value = e, r.type = s;
1059
+ const n = this.audioContext.createOscillator(), a = this.audioContext.createGain();
1060
+ n.connect(a), a.connect(this._output()), n.frequency.value = t, n.type = s;
848
1061
  const o = Math.max(1e-4, i);
849
- n.gain.setValueAtTime(o, this.audioContext.currentTime), n.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + t), r.start(this.audioContext.currentTime), r.stop(this.audioContext.currentTime + t);
1062
+ a.gain.setValueAtTime(o, this.audioContext.currentTime), a.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + e), n.start(this.audioContext.currentTime), n.stop(this.audioContext.currentTime + e);
850
1063
  }
851
1064
  // --- Presets (Synth) ---
852
- playJump(e = !1) {
853
- if (!e && this.assets.has("jump")) return this.play("jump");
1065
+ playJump(t = !1) {
1066
+ if (!t && this.assets.has("jump")) return this.play("jump");
854
1067
  this.playTone(400, 0.1, "square", 0.2);
855
1068
  }
856
- playScore(e = !1) {
857
- if (!e && this.assets.has("score")) return this.play("score");
858
- this.playTone(800, 0.15, "sine", 0.25), setTimeout(() => this.playTone(1e3, 0.15, "sine", 0.25), 50);
1069
+ playScore(t = !1) {
1070
+ if (!t && this.assets.has("score")) return this.play("score");
1071
+ this.playTone(800, 0.15, "sine", 0.25), this._later(() => this.playTone(1e3, 0.15, "sine", 0.25), 50);
859
1072
  }
860
- playGameOver(e = !1) {
861
- if (!e && this.assets.has("gameover")) return this.play("gameover");
862
- this.playTone(300, 0.2, "sawtooth", 0.3), setTimeout(() => this.playTone(200, 0.2, "sawtooth", 0.3), 100), setTimeout(() => this.playTone(150, 0.3, "sawtooth", 0.3), 200);
1073
+ playGameOver(t = !1) {
1074
+ if (!t && this.assets.has("gameover")) return this.play("gameover");
1075
+ this.playTone(300, 0.2, "sawtooth", 0.3), this._later(() => this.playTone(200, 0.2, "sawtooth", 0.3), 100), this._later(() => this.playTone(150, 0.3, "sawtooth", 0.3), 200);
863
1076
  }
864
- playClick(e = !1) {
865
- if (!e && this.assets.has("click")) return this.play("click");
1077
+ playClick(t = !1) {
1078
+ if (!t && this.assets.has("click")) return this.play("click");
866
1079
  this.playTone(600, 0.05, "sine", 0.1);
867
1080
  }
868
- playSuccess(e = !1) {
869
- if (!e && this.assets.has("success")) return this.play("success");
870
- this.playTone(600, 0.1, "sine", 0.2), setTimeout(() => this.playTone(800, 0.2, "sine", 0.2), 100);
1081
+ playSuccess(t = !1) {
1082
+ if (!t && this.assets.has("success")) return this.play("success");
1083
+ this.playTone(600, 0.1, "sine", 0.2), this._later(() => this.playTone(800, 0.2, "sine", 0.2), 100);
871
1084
  }
872
- playError(e = !1) {
873
- if (!e && this.assets.has("error")) return this.play("error");
1085
+ playError(t = !1) {
1086
+ if (!t && this.assets.has("error")) return this.play("error");
874
1087
  this.playTone(200, 0.2, "sawtooth", 0.3);
875
1088
  }
1089
+ /**
1090
+ * Cancel queued tones, close the audio context and drop decoded buffers.
1091
+ * Browsers cap the number of live AudioContexts per page.
1092
+ */
1093
+ destroy() {
1094
+ for (const t of this._timers)
1095
+ clearTimeout(t);
1096
+ this._timers.clear(), this.audioContext && this.audioContext.close?.()?.catch?.(() => {
1097
+ }), this.audioContext = null, this.masterGain = null, this.assets.clear(), this._loading.clear();
1098
+ }
876
1099
  }
877
1100
  class N {
878
- constructor(e = {}) {
879
- this.config = e, this.dataService = new A(e.data || {}), this.rewardSystem = new O(this.dataService, e.rewards || {}), this.soundManager = new I(e.sound || {}), this.particleSystem = new T(e.particles || {}), this.gameUI = new D(this.particleSystem), this._bindEvents();
1101
+ constructor(t = {}) {
1102
+ this.config = t, this.dataService = new I(t.data || {}), this.rewardSystem = new D(this.dataService, t.rewards || {}), this.soundManager = new F(t.sound || {}), this.particleSystem = new z(t.particles || {}), this.gameUI = new L(this.particleSystem), this._bindEvents();
880
1103
  }
881
1104
  async init() {
882
- return await this.rewardSystem.init(), {
1105
+ return await this.rewardSystem.init(), this._syncUI(), {
883
1106
  rewardSystem: this.rewardSystem,
884
1107
  gameUI: this.gameUI,
885
1108
  particleSystem: this.particleSystem,
886
1109
  soundManager: this.soundManager
887
1110
  };
888
1111
  }
889
- _bindEvents() {
890
- this.rewardSystem.on("xp_gained", (e) => {
891
- const { total: t, needed: s } = this.rewardSystem.getXPForNextLevel();
892
- this.gameUI.updateXP(this.rewardSystem.player.xp, s, t), e.xpGained > 0;
893
- }), this.rewardSystem.on("level_up", (e) => {
894
- this.gameUI.updateLevel(e.newLevel), this.gameUI.showLevelUp(e.oldLevel, e.newLevel);
895
- }), this.rewardSystem.on("achievement_unlocked", (e) => {
896
- this.gameUI.showAchievement(e);
897
- }), this.rewardSystem.on("new_high_score", (e) => {
898
- this.gameUI.showNotification(`New High Score: ${e.score}!`, "legendary"), this.soundManager.playSuccess();
899
- });
900
- }
901
- }
902
- class p {
903
- constructor() {
904
- this._listeners = /* @__PURE__ */ new Map(), this._onceListeners = /* @__PURE__ */ new Map(), this._sortedCache = /* @__PURE__ */ new Map();
905
- }
906
- /**
907
- * Register an event listener
908
- * @param {string} event - Event name
909
- * @param {Function} callback - Callback function
910
- * @param {number} priority - Higher priority = called first (default: 0)
911
- * @returns {EventBus} - For chaining
912
- */
913
- on(e, t, s = 0) {
914
- return this._listeners.has(e) || this._listeners.set(e, /* @__PURE__ */ new Set()), this._listeners.get(e).add({ callback: t, priority: s }), this._sortedCache.delete(e), this;
915
- }
916
- /**
917
- * Register a one-time event listener
918
- * @param {string} event - Event name
919
- * @param {Function} callback - Callback function
920
- * @returns {EventBus} - For chaining
921
- */
922
- once(e, t) {
923
- return this._onceListeners.has(e) || this._onceListeners.set(e, /* @__PURE__ */ new Set()), this._onceListeners.get(e).add(t), this;
924
- }
925
- /**
926
- * Remove an event listener
927
- * @param {string} event - Event name
928
- * @param {Function} callback - Callback function to remove
929
- * @returns {EventBus} - For chaining
930
- */
931
- off(e, t) {
932
- const s = this._listeners.get(e);
933
- if (s)
934
- for (const r of [...s])
935
- r.callback === t && (s.delete(r), this._sortedCache.delete(e));
936
- const i = this._onceListeners.get(e);
937
- return i && i.delete(t), this;
938
- }
939
- /**
940
- * Emit an event to all registered listeners
941
- * @param {string} event - Event name
942
- * @param {*} data - Event data
943
- */
944
- emit(e, t) {
945
- const s = this._getSortedListeners(e);
946
- for (let r = 0; r < s.length; r++)
947
- this._invoke(s[r].callback, t, e);
948
- const i = this._onceListeners.get(e);
949
- if (i && i.size > 0) {
950
- const r = [...i];
951
- i.clear();
952
- for (const n of r)
953
- this._invoke(n, t, e);
954
- }
955
- }
956
- /**
957
- * Call one listener without letting it abort the dispatch.
958
- *
959
- * A throwing listener used to take out every lower-priority listener for
960
- * that event, which on 'tick' means the game silently stops updating.
961
- *
962
- * @private
963
- */
964
- _invoke(e, t, s) {
965
- try {
966
- e(t);
967
- } catch (i) {
968
- console.error(`[DopamineJS] Listener for "${s}" threw:`, i);
969
- }
970
- }
971
1112
  /**
972
- * Get sorted listeners for an event (with caching)
1113
+ * Draw the loaded player. Without this the overlay keeps its placeholder
1114
+ * (level 1, 0 XP, streak 1) until the first event arrives.
973
1115
  * @private
974
1116
  */
975
- _getSortedListeners(e) {
976
- if (this._sortedCache.has(e))
977
- return this._sortedCache.get(e);
978
- const t = this._listeners.get(e);
979
- if (!t || t.size === 0)
980
- return [];
981
- const s = Array.from(t).sort((i, r) => r.priority - i.priority);
982
- return this._sortedCache.set(e, s), s;
983
- }
984
- /**
985
- * Remove all listeners for an event, or all events if no event specified
986
- * @param {string} [event] - Event name (optional)
987
- */
988
- clear(e) {
989
- e ? (this._listeners.delete(e), this._onceListeners.delete(e), this._sortedCache.delete(e)) : (this._listeners.clear(), this._onceListeners.clear(), this._sortedCache.clear());
990
- }
991
- /**
992
- * Check if an event has listeners
993
- * @param {string} event - Event name
994
- * @returns {boolean}
995
- */
996
- hasListeners(e) {
997
- const t = this._listeners.get(e), s = this._onceListeners.get(e);
998
- return !!(t != null && t.size) || !!(s != null && s.size);
999
- }
1000
- }
1001
- p.Events = {
1002
- // Core game loop events
1003
- TICK: "tick",
1004
- FIXED_UPDATE: "fixed_update",
1005
- RENDER: "render",
1006
- // Physics events
1007
- COLLISION_ENTER: "collision_enter",
1008
- COLLISION_EXIT: "collision_exit",
1009
- // Dopamine events (from RewardSystem)
1010
- XP_GAINED: "xp_gained",
1011
- LEVEL_UP: "level_up",
1012
- ACHIEVEMENT_UNLOCKED: "achievement_unlocked",
1013
- NEW_HIGH_SCORE: "new_high_score",
1014
- STREAK_UPDATED: "streak_updated",
1015
- // System lifecycle events
1016
- SYSTEM_REGISTERED: "system_registered",
1017
- SYSTEM_UNREGISTERED: "system_unregistered",
1018
- PLUGIN_LOADED: "plugin_loaded",
1019
- PLUGIN_UNLOADED: "plugin_unloaded"
1020
- };
1021
- class L {
1022
- constructor(e) {
1023
- this.kernel = e, this._systems = /* @__PURE__ */ new Map(), this._updateOrder = [], this._fixedUpdateOrder = [], this._needsRecompute = !1;
1024
- }
1025
- /**
1026
- * Register a system
1027
- * @param {string} name - Unique system name
1028
- * @param {Object} system - System instance (must implement ISystem interface)
1029
- * @param {Object} options - Configuration options
1030
- * @param {number} options.priority - Update priority (higher = earlier, default: 0)
1031
- * @param {string[]} options.dependencies - System names this depends on
1032
- * @returns {SystemRegistry} - For chaining
1033
- */
1034
- register(e, t, { priority: s = 0, dependencies: i = [] } = {}) {
1035
- this._systems.has(e) && (console.warn(`[SystemRegistry] System "${e}" already registered. Replacing.`), this.unregister(e));
1036
- for (const r of i)
1037
- this._systems.has(r) || console.warn(`[SystemRegistry] Dependency "${r}" not found for system "${e}"`);
1038
- return this._systems.set(e, {
1039
- system: t,
1040
- priority: s,
1041
- dependencies: i
1042
- }), t.init && t.init(this.kernel), this._needsRecompute = !0, this.kernel.events.emit("system_registered", { name: e, system: t }), this;
1043
- }
1044
- /**
1045
- * Get a system by name
1046
- * @param {string} name - System name
1047
- * @returns {Object|undefined} - System instance
1048
- */
1049
- get(e) {
1050
- const t = this._systems.get(e);
1051
- return t ? t.system : void 0;
1052
- }
1053
- /**
1054
- * Check if a system is registered
1055
- * @param {string} name - System name
1056
- * @returns {boolean}
1057
- */
1058
- has(e) {
1059
- return this._systems.has(e);
1117
+ _syncUI() {
1118
+ const { player: t } = this.rewardSystem, { total: e, needed: s, progress: i } = this.rewardSystem.getXPForNextLevel();
1119
+ this.gameUI.updateXP(t.xp, s, e, i), this.gameUI.updateLevel(t.level), this.gameUI.updateStreak(t.streak.current);
1060
1120
  }
1061
- /**
1062
- * Unregister a system
1063
- * @param {string} name - System name
1064
- * @returns {boolean} - True if system was found and removed
1065
- */
1066
- unregister(e) {
1067
- const t = this._systems.get(e);
1068
- return t ? (t.system.destroy && t.system.destroy(), this._systems.delete(e), this._needsRecompute = !0, this.kernel.events.emit("system_unregistered", { name: e }), !0) : !1;
1069
- }
1070
- /**
1071
- * Update all systems (called each frame)
1072
- * @param {number} dt - Delta time in seconds
1073
- */
1074
- update(e) {
1075
- this._needsRecompute && this._recomputeUpdateOrder();
1076
- for (let t = 0; t < this._updateOrder.length; t++) {
1077
- const s = this._updateOrder[t];
1078
- s.update && s.update(e);
1079
- }
1080
- }
1081
- /**
1082
- * Fixed update for physics (called at fixed timestep)
1083
- * @param {number} dt - Fixed delta time
1084
- */
1085
- fixedUpdate(e) {
1086
- this._needsRecompute && this._recomputeUpdateOrder();
1087
- for (let t = 0; t < this._fixedUpdateOrder.length; t++) {
1088
- const s = this._fixedUpdateOrder[t];
1089
- s.fixedUpdate && s.fixedUpdate(e);
1090
- }
1091
- }
1092
- /**
1093
- * Recompute the update order based on dependencies and priorities
1094
- * Uses topological sort for dependencies
1095
- * @private
1096
- */
1097
- _recomputeUpdateOrder() {
1098
- const e = Array.from(this._systems.entries()), t = /* @__PURE__ */ new Map(), s = /* @__PURE__ */ new Map();
1099
- for (const [n, o] of e)
1100
- t.set(n, o.dependencies), s.set(n, 0);
1101
- for (const [n, o] of t)
1102
- for (const c of o)
1103
- s.has(c) && s.set(n, s.get(n) + 1);
1104
- const i = [], r = [];
1105
- for (const [n, o] of s)
1106
- o === 0 && i.push(n);
1107
- for (; i.length > 0; ) {
1108
- i.sort((o, c) => {
1109
- const h = this._systems.get(o).priority;
1110
- return this._systems.get(c).priority - h;
1111
- });
1112
- const n = i.shift();
1113
- r.push(n);
1114
- for (const [o, c] of t)
1115
- if (c.includes(n)) {
1116
- const h = s.get(o) - 1;
1117
- s.set(o, h), h === 0 && i.push(o);
1118
- }
1119
- }
1120
- r.length !== e.length && (console.error("[SystemRegistry] Circular dependency detected in systems!"), r.length = 0, r.push(...e.map(([n]) => n).sort((n, o) => this._systems.get(o).priority - this._systems.get(n).priority))), this._updateOrder = r.map((n) => this._systems.get(n).system).filter((n) => n.update), this._fixedUpdateOrder = r.map((n) => this._systems.get(n).system).filter((n) => n.fixedUpdate), this._needsRecompute = !1;
1121
- }
1122
- /**
1123
- * Get all registered system names
1124
- * @returns {string[]}
1125
- */
1126
- getSystemNames() {
1127
- return Array.from(this._systems.keys());
1128
- }
1129
- /**
1130
- * Clear all systems
1131
- */
1132
- clear() {
1133
- for (const e of this._systems.keys())
1134
- this.unregister(e);
1135
- }
1136
- }
1137
- class U {
1138
- constructor(e) {
1139
- this.kernel = e, this._plugins = /* @__PURE__ */ new Map(), this._loadOrder = [];
1140
- }
1141
- /**
1142
- * Register and initialize a plugin
1143
- *
1144
- * Plugin interface:
1145
- * {
1146
- * name: string,
1147
- * version?: string,
1148
- * init(kernel): void,
1149
- * destroy?(): void
1150
- * }
1151
- *
1152
- * @param {Object} plugin - Plugin instance
1153
- * @returns {PluginRegistry} - For chaining
1154
- */
1155
- use(e) {
1156
- if (!e.name)
1157
- throw new Error("[PluginRegistry] Plugin must have a name");
1158
- if (this._plugins.has(e.name))
1159
- return console.warn(`[PluginRegistry] Plugin "${e.name}" already registered. Skipping.`), this;
1160
- if (!e.init || typeof e.init != "function")
1161
- throw new Error(`[PluginRegistry] Plugin "${e.name}" must have an init() method`);
1162
- this._plugins.set(e.name, e), this._loadOrder.push(e.name);
1163
- try {
1164
- e.init(this.kernel), this.kernel.events.emit("plugin_loaded", {
1165
- name: e.name,
1166
- version: e.version
1167
- });
1168
- } catch (t) {
1169
- throw console.error(`[PluginRegistry] Failed to initialize plugin "${e.name}":`, t), this._plugins.delete(e.name), this._loadOrder.pop(), t;
1170
- }
1171
- return this;
1172
- }
1173
- /**
1174
- * Register and initialize a plugin asynchronously
1175
- * Useful for plugins that need to load external resources
1176
- *
1177
- * @param {Object} plugin - Plugin instance with async init
1178
- * @returns {Promise<PluginRegistry>} - For chaining
1179
- */
1180
- async useAsync(e) {
1181
- if (!e.name)
1182
- throw new Error("[PluginRegistry] Plugin must have a name");
1183
- if (this._plugins.has(e.name))
1184
- return console.warn(`[PluginRegistry] Plugin "${e.name}" already registered. Skipping.`), this;
1185
- if (!e.init || typeof e.init != "function")
1186
- throw new Error(`[PluginRegistry] Plugin "${e.name}" must have an init() method`);
1187
- this._plugins.set(e.name, e), this._loadOrder.push(e.name);
1188
- try {
1189
- await e.init(this.kernel), this.kernel.events.emit("plugin_loaded", {
1190
- name: e.name,
1191
- version: e.version
1192
- });
1193
- } catch (t) {
1194
- throw console.error(`[PluginRegistry] Failed to initialize plugin "${e.name}":`, t), this._plugins.delete(e.name), this._loadOrder.pop(), t;
1195
- }
1196
- return this;
1197
- }
1198
- /**
1199
- * Remove a plugin
1200
- * @param {string} name - Plugin name
1201
- * @returns {boolean} - True if plugin was found and removed
1202
- */
1203
- remove(e) {
1204
- const t = this._plugins.get(e);
1205
- if (!t)
1206
- return !1;
1207
- if (t.destroy)
1208
- try {
1209
- t.destroy();
1210
- } catch (i) {
1211
- console.error(`[PluginRegistry] Error destroying plugin "${e}":`, i);
1212
- }
1213
- this._plugins.delete(e);
1214
- const s = this._loadOrder.indexOf(e);
1215
- return s > -1 && this._loadOrder.splice(s, 1), this.kernel.events.emit("plugin_unloaded", { name: e }), !0;
1216
- }
1217
- /**
1218
- * Get a plugin by name
1219
- * @param {string} name - Plugin name
1220
- * @returns {Object|undefined} - Plugin instance
1221
- */
1222
- get(e) {
1223
- return this._plugins.get(e);
1224
- }
1225
- /**
1226
- * Check if a plugin is registered
1227
- * @param {string} name - Plugin name
1228
- * @returns {boolean}
1229
- */
1230
- has(e) {
1231
- return this._plugins.has(e);
1232
- }
1233
- /**
1234
- * Get all registered plugin names
1235
- * @returns {string[]}
1236
- */
1237
- getPluginNames() {
1238
- return Array.from(this._plugins.keys());
1239
- }
1240
- /**
1241
- * Get plugin load order
1242
- * @returns {string[]}
1243
- */
1244
- getLoadOrder() {
1245
- return [...this._loadOrder];
1246
- }
1247
- /**
1248
- * Clear all plugins (in reverse load order)
1249
- */
1250
- clear() {
1251
- for (let e = this._loadOrder.length - 1; e >= 0; e--)
1252
- this.remove(this._loadOrder[e]);
1253
- }
1254
- }
1255
- class R {
1256
- constructor(e = {}) {
1257
- this.width = e.width || 800, this.height = e.height || 600, this.backgroundColor = e.backgroundColor || "#000000", e.canvas ? (this.canvas = e.canvas, this.canvas.width = this.width, this.canvas.height = this.height) : (this.canvas = document.createElement("canvas"), this.canvas.width = this.width, this.canvas.height = this.height, (e.parent ? typeof e.parent == "string" ? document.querySelector(e.parent) : e.parent : document.body).appendChild(this.canvas)), this.ctx = this.canvas.getContext("2d");
1258
- }
1259
- /**
1260
- * Clear the canvas.
1261
- */
1262
- clear() {
1263
- this.ctx.fillStyle = this.backgroundColor, this.ctx.fillRect(0, 0, this.width, this.height);
1264
- }
1265
- resize(e, t) {
1266
- this.width = e, this.height = t, this.canvas.width = e, this.canvas.height = t;
1267
- }
1268
- }
1269
- class z {
1270
- constructor() {
1271
- this.running = !1, this.lastTime = 0, this.callbacks = /* @__PURE__ */ new Set(), this._frameHandle = null, this._tick = this._tick.bind(this);
1272
- }
1273
- /**
1274
- * Add a callback to the loop.
1275
- * @param {Function} callback - Function to call each frame (receives dt).
1276
- */
1277
- add(e) {
1278
- this.callbacks.add(e);
1279
- }
1280
- /**
1281
- * Remove a callback from the loop.
1282
- * @param {Function} callback
1283
- */
1284
- remove(e) {
1285
- this.callbacks.delete(e);
1286
- }
1287
- /**
1288
- * Start the loop.
1289
- */
1290
- start() {
1291
- this.running || (this.running = !0, this.lastTime = performance.now(), this._frameHandle !== null && cancelAnimationFrame(this._frameHandle), this._frameHandle = requestAnimationFrame(this._tick));
1292
- }
1293
- /**
1294
- * Stop the loop.
1295
- */
1296
- stop() {
1297
- this.running = !1, this._frameHandle !== null && (cancelAnimationFrame(this._frameHandle), this._frameHandle = null);
1298
- }
1299
- _tick(e) {
1300
- if (this._frameHandle = null, !this.running) return;
1301
- const t = (e - this.lastTime) / 1e3;
1302
- this.lastTime = e;
1303
- const s = Math.min(t, 0.1);
1304
- for (const i of [...this.callbacks])
1305
- i(s);
1306
- this.running && (this._frameHandle = requestAnimationFrame(this._tick));
1307
- }
1308
- }
1309
- class y {
1310
- constructor() {
1311
- this.gameObject = null, this.kernel = null;
1312
- }
1313
- /**
1314
- * Access systems through kernel (no more globals!)
1315
- */
1316
- get physics() {
1317
- var e;
1318
- return (e = this.kernel) == null ? void 0 : e.systems.get("physics");
1319
- }
1320
- get input() {
1321
- var e;
1322
- return (e = this.kernel) == null ? void 0 : e.systems.get("input");
1323
- }
1324
- get loader() {
1325
- var e;
1326
- return (e = this.kernel) == null ? void 0 : e.systems.get("loader");
1327
- }
1328
- get events() {
1329
- var e;
1330
- return (e = this.kernel) == null ? void 0 : e.events;
1331
- }
1332
- onAttach() {
1333
- }
1334
- onDetach() {
1335
- }
1336
- update(e) {
1337
- }
1338
- render(e) {
1339
- }
1340
- }
1341
- class H extends y {
1342
- constructor(e = "box", t = 50, s = 50, i = 25) {
1343
- super(), this.type = e, this.width = t, this.height = s, this.radius = i, this.tag = null;
1344
- }
1345
- onAttach() {
1346
- super.onAttach(), this.physics && this.physics.add(this);
1347
- }
1348
- onDetach() {
1349
- super.onDetach(), this.physics && this.physics.remove(this);
1350
- }
1351
- getBounds() {
1352
- const e = this.gameObject;
1353
- if (this.type === "box")
1354
- return {
1355
- left: e.x - this.width / 2,
1356
- right: e.x + this.width / 2,
1357
- top: e.y - this.height / 2,
1358
- bottom: e.y + this.height / 2
1359
- };
1360
- if (this.type === "circle")
1361
- return {
1362
- x: e.x,
1363
- y: e.y,
1364
- radius: this.radius
1365
- };
1366
- }
1367
- }
1368
- class b {
1369
- constructor() {
1370
- this.colliders = [], this.kernel = null;
1371
- }
1372
- /**
1373
- * ISystem interface - called when registered
1374
- */
1375
- init(e) {
1376
- this.kernel = e;
1377
- }
1378
- /**
1379
- * ISystem interface - called at fixed timestep
1380
- */
1381
- fixedUpdate(e) {
1382
- this.step();
1383
- }
1384
- /**
1385
- * ISystem interface - cleanup
1386
- */
1387
- destroy() {
1388
- this.colliders = [];
1389
- }
1390
- add(e) {
1391
- this.colliders.push(e);
1392
- }
1393
- remove(e) {
1394
- const t = this.colliders.indexOf(e);
1395
- t > -1 && this.colliders.splice(t, 1);
1396
- }
1397
- /**
1398
- * Main Physics Step.
1399
- * Checks all pairs and triggers callbacks.
1400
- */
1401
- step() {
1402
- for (let e = 0; e < this.colliders.length; e++)
1403
- for (let t = e + 1; t < this.colliders.length; t++) {
1404
- const s = this.colliders[e], i = this.colliders[t];
1405
- this._intersects(s, s.getBounds(), i, i.getBounds()) && (this._notifyCollision(s.gameObject, i.gameObject), this._notifyCollision(i.gameObject, s.gameObject));
1406
- }
1407
- }
1408
- _notifyCollision(e, t) {
1409
- !e || !e.components || e.components.forEach((s) => {
1410
- s.onCollisionEnter && s.onCollisionEnter(t);
1411
- });
1412
- }
1413
- /**
1414
- * Check if a specific collider overlaps with anything in the target group (tag).
1415
- * @param {Collider} source
1416
- * @param {string} targetTag
1417
- */
1418
- checkOverlap(e, t) {
1419
- const s = [], i = e.getBounds();
1420
- for (const r of this.colliders) {
1421
- if (r === e || t && r.tag !== t) continue;
1422
- const n = r.getBounds();
1423
- this._intersects(e, i, r, n) && s.push(r);
1424
- }
1425
- return s;
1426
- }
1427
- _intersects(e, t, s, i) {
1428
- if (e.type === "box" && s.type === "box")
1429
- return t.left < i.right && t.right > i.left && t.top < i.bottom && t.bottom > i.top;
1430
- if (e.type === "circle" && s.type === "circle") {
1431
- const r = t.x - i.x, n = t.y - i.y;
1432
- return Math.sqrt(r * r + n * n) < t.radius + i.radius;
1433
- }
1434
- return !1;
1435
- }
1436
- // IPhysicsSystem interface methods (for plugin compatibility)
1437
- addBody(e) {
1438
- this.add(e);
1439
- }
1440
- removeBody(e) {
1441
- this.remove(e);
1442
- }
1443
- checkCollision(e, t) {
1444
- return this._intersects(e, e.getBounds(), t, t.getBounds());
1445
- }
1446
- raycast(e, t, s) {
1447
- return null;
1448
- }
1449
- setGravity(e, t) {
1450
- }
1451
- }
1452
- const $ = new b();
1453
- console.warn("[DopamineJS] GlobalPhysics is deprecated. Use kernel.physics instead.");
1454
- class M {
1455
- constructor() {
1456
- this.keys = /* @__PURE__ */ new Map(), this.mouse = { x: 0, y: 0, buttons: /* @__PURE__ */ new Map() }, this.kernel = null, this._onKeyDown = this._onKeyDown.bind(this), this._onKeyUp = this._onKeyUp.bind(this), this._onMouseMove = this._onMouseMove.bind(this), this._onMouseDown = this._onMouseDown.bind(this), this._onMouseUp = this._onMouseUp.bind(this);
1457
- }
1458
- /**
1459
- * ISystem interface - called when registered
1460
- */
1461
- init(e) {
1462
- this.kernel = e, window.addEventListener("keydown", this._onKeyDown), window.addEventListener("keyup", this._onKeyUp), window.addEventListener("mousemove", this._onMouseMove), window.addEventListener("mousedown", this._onMouseDown), window.addEventListener("mouseup", this._onMouseUp);
1463
- }
1464
- /**
1465
- * ISystem interface - cleanup
1466
- */
1467
- destroy() {
1468
- window.removeEventListener("keydown", this._onKeyDown), window.removeEventListener("keyup", this._onKeyUp), window.removeEventListener("mousemove", this._onMouseMove), window.removeEventListener("mousedown", this._onMouseDown), window.removeEventListener("mouseup", this._onMouseUp);
1469
- }
1470
- _onKeyDown(e) {
1471
- this.keys.set(e.key, !0);
1472
- }
1473
- _onKeyUp(e) {
1474
- this.keys.set(e.key, !1);
1475
- }
1476
- _onMouseMove(e) {
1477
- this.mouse.x = e.clientX, this.mouse.y = e.clientY;
1478
- }
1479
- _onMouseDown(e) {
1480
- this.mouse.buttons.set(e.button, !0);
1481
- }
1482
- _onMouseUp(e) {
1483
- this.mouse.buttons.set(e.button, !1);
1484
- }
1485
- isKeyDown(e) {
1486
- return this.keys.get(e) || !1;
1487
- }
1488
- isMouseButtonDown(e = 0) {
1489
- return this.mouse.buttons.get(e) || !1;
1490
- }
1491
- }
1492
- const q = new M();
1493
- console.warn("[DopamineJS] GlobalInput is deprecated. Use kernel.input instead.");
1494
- class E {
1495
- constructor() {
1496
- this.assets = /* @__PURE__ */ new Map(), this.kernel = null;
1497
- }
1498
- /**
1499
- * ISystem interface - called when registered
1500
- */
1501
- init(e) {
1502
- this.kernel = e;
1503
- }
1504
- /**
1505
- * ISystem interface - cleanup
1506
- */
1507
- destroy() {
1508
- this.assets.clear();
1509
- }
1510
- async loadImage(e, t) {
1511
- return new Promise((s, i) => {
1512
- const r = new Image();
1513
- r.onload = () => {
1514
- this.assets.set(e, r), s(r);
1515
- }, r.onerror = i, r.src = t;
1516
- });
1517
- }
1518
- get(e) {
1519
- return this.assets.get(e);
1520
- }
1521
- }
1522
- const K = new E();
1523
- console.warn("[DopamineJS] GlobalLoader is deprecated. Use kernel.loader instead.");
1524
- class B {
1525
- constructor(e = {}) {
1526
- this.config = e, this._boundUpdate = this._update.bind(this), this.events = new p(), this.systems = new L(this), this.plugins = new U(this), this._registerCoreSystems(e), this.fixedTimestep = e.fixedTimestep || 1 / 60, this._accumulator = 0, this._maxAccumulator = this.fixedTimestep * 5;
1527
- }
1528
- /**
1529
- * Register core engine systems
1530
- * These can be replaced by plugins
1531
- * @private
1532
- */
1533
- _registerCoreSystems(e) {
1534
- this.systems.register("ticker", new z(), {
1535
- priority: 100
1536
- });
1537
- const t = {
1538
- width: e.width,
1539
- height: e.height,
1540
- backgroundColor: e.backgroundColor,
1541
- canvas: e.canvas,
1542
- ...e.renderer
1543
- };
1544
- this.systems.register("renderer", new R(t), {
1545
- priority: 90
1546
- }), this.systems.register("input", new M(), {
1547
- priority: 80
1548
- }), this.systems.register("loader", new E(), {
1549
- priority: 70
1550
- }), this.systems.register("physics", new b(), {
1551
- priority: 60
1552
- });
1553
- }
1554
- /**
1555
- * Start the kernel (begins game loop)
1556
- */
1557
- start() {
1558
- const e = this.systems.get("ticker");
1559
- e && (e.add(this._boundUpdate), e.start()), this.events.emit("kernel_started");
1560
- }
1561
- /**
1562
- * Stop the kernel
1563
- */
1564
- stop() {
1565
- const e = this.systems.get("ticker");
1566
- e && e.stop(), this.events.emit("kernel_stopped");
1567
- }
1568
- /**
1569
- * Main update loop
1570
- * @private
1571
- */
1572
- _update(e) {
1573
- for (this.events.emit(p.Events.TICK, { dt: e }), this._accumulator += e, this._accumulator > this._maxAccumulator && (this._accumulator = this._maxAccumulator); this._accumulator >= this.fixedTimestep; )
1574
- this.events.emit(p.Events.FIXED_UPDATE, { dt: this.fixedTimestep }), this.systems.fixedUpdate(this.fixedTimestep), this._accumulator -= this.fixedTimestep;
1575
- this.systems.update(e), this.events.emit(p.Events.RENDER, { dt: e });
1576
- }
1577
- /**
1578
- * Shorthand accessors for common systems (backward compatibility)
1579
- */
1580
- get physics() {
1581
- return this.systems.get("physics");
1582
- }
1583
- get input() {
1584
- return this.systems.get("input");
1585
- }
1586
- get loader() {
1587
- return this.systems.get("loader");
1588
- }
1589
- get renderer() {
1590
- return this.systems.get("renderer");
1591
- }
1592
- get ticker() {
1593
- return this.systems.get("ticker");
1594
- }
1595
- /**
1596
- * Destroy the kernel and cleanup all systems
1121
+ _bindEvents() {
1122
+ const t = this.rewardSystem;
1123
+ this._unsubscribe = [
1124
+ t.on("xp_gained", () => {
1125
+ const { total: e, needed: s, progress: i } = t.getXPForNextLevel();
1126
+ this.gameUI.updateXP(t.player.xp, s, e, i);
1127
+ }),
1128
+ t.on("level_up", (e) => {
1129
+ this.gameUI.updateLevel(e.newLevel), this.gameUI.showLevelUp(e.oldLevel, e.newLevel);
1130
+ }),
1131
+ t.on("achievement_unlocked", (e) => {
1132
+ this.gameUI.showAchievement(e);
1133
+ }),
1134
+ t.on("streak_updated", (e) => {
1135
+ this.gameUI.updateStreak(e.current);
1136
+ }),
1137
+ t.on("new_high_score", (e) => {
1138
+ this.gameUI.showNotification(`New High Score: ${e.score}!`, "legendary"), this.soundManager.playSuccess();
1139
+ })
1140
+ ];
1141
+ }
1142
+ /**
1143
+ * Remove the overlay and canvas, stop audio, and detach from the reward
1144
+ * system. The reward system itself stays usable.
1597
1145
  */
1598
1146
  destroy() {
1599
- this.stop();
1600
- const e = this.systems.get("ticker");
1601
- e && e.remove(this._boundUpdate), this.plugins.clear(), this.systems.clear(), this.events.clear();
1147
+ for (const t of this._unsubscribe)
1148
+ t();
1149
+ this._unsubscribe = [], this.gameUI.destroy(), this.particleSystem.destroy(), this.soundManager.destroy();
1602
1150
  }
1603
1151
  }
1604
- const g = {
1152
+ const _ = {
1605
1153
  /**
1606
1154
  * Called when system is registered to the kernel
1607
1155
  * @param {DopamineKernel} kernel - The kernel instance
1608
1156
  */
1609
- init(a) {
1157
+ init(r) {
1610
1158
  },
1611
1159
  /**
1612
1160
  * Called each frame (variable timestep)
1613
1161
  * @param {number} dt - Delta time in seconds
1614
1162
  */
1615
- update(a) {
1163
+ update(r) {
1616
1164
  },
1617
1165
  /**
1618
1166
  * Called at fixed timestep (for physics)
1619
1167
  * @param {number} dt - Fixed delta time
1620
1168
  */
1621
- fixedUpdate(a) {
1169
+ fixedUpdate(r) {
1622
1170
  },
1623
1171
  /**
1624
1172
  * Called when system is unregistered
@@ -1626,33 +1174,33 @@ const g = {
1626
1174
  destroy() {
1627
1175
  }
1628
1176
  };
1629
- class X {
1177
+ class G {
1630
1178
  constructor() {
1631
1179
  this.kernel = null;
1632
1180
  }
1633
- init(e) {
1634
- this.kernel = e;
1181
+ init(t) {
1182
+ this.kernel = t;
1635
1183
  }
1636
- update(e) {
1184
+ update(t) {
1637
1185
  }
1638
- fixedUpdate(e) {
1186
+ fixedUpdate(t) {
1639
1187
  }
1640
1188
  destroy() {
1641
1189
  }
1642
1190
  }
1643
- const J = {
1644
- ...g,
1191
+ const R = {
1192
+ ..._,
1645
1193
  /**
1646
1194
  * Add a physics body to the simulation
1647
1195
  * @param {Object} body - Physics body (implementation-specific)
1648
1196
  */
1649
- addBody(a) {
1197
+ addBody(r) {
1650
1198
  },
1651
1199
  /**
1652
1200
  * Remove a physics body from the simulation
1653
1201
  * @param {Object} body - Physics body to remove
1654
1202
  */
1655
- removeBody(a) {
1203
+ removeBody(r) {
1656
1204
  },
1657
1205
  /**
1658
1206
  * Check collision between two bodies
@@ -1660,7 +1208,7 @@ const J = {
1660
1208
  * @param {Object} b - Second body
1661
1209
  * @returns {boolean} - True if colliding
1662
1210
  */
1663
- checkCollision(a, e) {
1211
+ checkCollision(r, t) {
1664
1212
  },
1665
1213
  /**
1666
1214
  * Perform a raycast
@@ -1669,43 +1217,43 @@ const J = {
1669
1217
  * @param {number} distance - Maximum ray distance
1670
1218
  * @returns {Object|null} - Hit result or null
1671
1219
  */
1672
- raycast(a, e, t) {
1220
+ raycast(r, t, e) {
1673
1221
  },
1674
1222
  /**
1675
1223
  * Set gravity for the physics world
1676
1224
  * @param {number} x - Gravity X component
1677
1225
  * @param {number} y - Gravity Y component
1678
1226
  */
1679
- setGravity(a, e) {
1227
+ setGravity(r, t) {
1680
1228
  },
1681
1229
  /**
1682
1230
  * Step the physics simulation
1683
1231
  * @param {number} dt - Delta time
1684
1232
  */
1685
- step(a) {
1233
+ step(r) {
1686
1234
  }
1687
- }, W = {
1688
- ...g,
1235
+ }, B = {
1236
+ ..._,
1689
1237
  /**
1690
1238
  * Register a sound with a key
1691
1239
  * @param {string} key - Sound identifier
1692
1240
  * @param {string} url - Path to audio file
1693
1241
  */
1694
- registerSound(a, e) {
1242
+ registerSound(r, t) {
1695
1243
  },
1696
1244
  /**
1697
1245
  * Preload multiple sounds
1698
1246
  * @param {Object} soundMap - Map of key -> url
1699
1247
  * @returns {Promise<void>}
1700
1248
  */
1701
- preloadSounds(a) {
1249
+ preloadSounds(r) {
1702
1250
  },
1703
1251
  /**
1704
1252
  * Play a sound by key
1705
1253
  * @param {string} key - Sound identifier
1706
1254
  * @param {Object} options - Playback options (volume, loop, etc.)
1707
1255
  */
1708
- play(a, e) {
1256
+ play(r, t) {
1709
1257
  },
1710
1258
  /**
1711
1259
  * Play a synthesized tone
@@ -1714,13 +1262,13 @@ const J = {
1714
1262
  * @param {string} type - Waveform type ('sine', 'square', etc.)
1715
1263
  * @param {number} volume - Volume (0-1)
1716
1264
  */
1717
- playTone(a, e, t, s) {
1265
+ playTone(r, t, e, s) {
1718
1266
  },
1719
1267
  /**
1720
1268
  * Stop a playing sound
1721
1269
  * @param {string} key - Sound identifier
1722
1270
  */
1723
- stop(a) {
1271
+ stop(r) {
1724
1272
  },
1725
1273
  /**
1726
1274
  * Toggle mute state
@@ -1732,23 +1280,23 @@ const J = {
1732
1280
  * Set master volume
1733
1281
  * @param {number} volume - Volume (0-1)
1734
1282
  */
1735
- setVolume(a) {
1283
+ setVolume(r) {
1736
1284
  }
1737
- }, Y = {
1738
- ...g,
1285
+ }, U = {
1286
+ ..._,
1739
1287
  /**
1740
1288
  * Register a sprite for particles
1741
1289
  * @param {string} key - Sprite identifier
1742
1290
  * @param {string} url - Path to image
1743
1291
  */
1744
- registerSprite(a, e) {
1292
+ registerSprite(r, t) {
1745
1293
  },
1746
1294
  /**
1747
1295
  * Register a custom effect
1748
1296
  * @param {string} name - Effect name
1749
1297
  * @param {Function} callback - Effect function (x, y, ...args) => void
1750
1298
  */
1751
- registerEffect(a, e) {
1299
+ registerEffect(r, t) {
1752
1300
  },
1753
1301
  /**
1754
1302
  * Play a registered effect
@@ -1757,13 +1305,13 @@ const J = {
1757
1305
  * @param {number} y - Y position
1758
1306
  * @param {...any} args - Additional arguments
1759
1307
  */
1760
- play(a, e, t, ...s) {
1308
+ play(r, t, e, ...s) {
1761
1309
  },
1762
1310
  /**
1763
1311
  * Emit particles with custom configuration
1764
1312
  * @param {Object} config - Particle configuration
1765
1313
  */
1766
- emit(a) {
1314
+ emit(r) {
1767
1315
  },
1768
1316
  /**
1769
1317
  * Clear all particles
@@ -1771,380 +1319,86 @@ const J = {
1771
1319
  clear() {
1772
1320
  },
1773
1321
  // Built-in effects (optional, but recommended)
1774
- confetti(a, e, t) {
1322
+ confetti(r, t, e) {
1775
1323
  },
1776
- coinShower(a, e, t) {
1324
+ coinShower(r, t, e) {
1777
1325
  },
1778
- sparkle(a, e, t, s) {
1326
+ sparkle(r, t, e, s) {
1779
1327
  },
1780
- fire(a, e, t) {
1328
+ fire(r, t, e) {
1781
1329
  },
1782
- starBurst(a, e, t) {
1330
+ starBurst(r, t, e) {
1783
1331
  }
1784
1332
  };
1785
- class V {
1786
- constructor() {
1787
- this.gameObjects = [], this.kernel = null;
1788
- }
1789
- /**
1790
- * Called when the scene is added to the Game.
1791
- */
1792
- onEnter() {
1793
- }
1794
- /**
1795
- * Called when the scene is removed from the Game.
1796
- */
1797
- onExit() {
1798
- }
1799
- /**
1800
- * Add a GameObject to the scene.
1801
- * @param {GameObject} gameObject
1802
- */
1803
- add(e) {
1804
- return this.kernel && (e.kernel = this.kernel), this.gameObjects.push(e), e;
1805
- }
1806
- /**
1807
- * Remove a GameObject from the scene.
1808
- * @param {GameObject} gameObject
1809
- */
1810
- remove(e) {
1811
- const t = this.gameObjects.indexOf(e);
1812
- t > -1 && this.gameObjects.splice(t, 1);
1813
- }
1814
- /**
1815
- * Update loop for the scene.
1816
- * @param {number} dt
1817
- */
1818
- update(e) {
1819
- for (const t of this.gameObjects)
1820
- t.update(e);
1821
- }
1822
- /**
1823
- * Render loop for the scene.
1824
- * @param {CanvasRenderingContext2D} ctx
1825
- */
1826
- render(e) {
1827
- for (const t of this.gameObjects)
1828
- t.render(e);
1829
- }
1830
- }
1831
- class G {
1832
- constructor(e) {
1833
- this.game = e, this.currentScene = null;
1834
- }
1835
- /**
1836
- * Switch to a new scene.
1837
- * @param {Scene} scene
1838
- */
1839
- run(e) {
1840
- this.currentScene && this.currentScene.onExit(), this.currentScene = e, this.currentScene.game = this.game, this.game.scene = e, e.onEnter();
1841
- }
1842
- }
1843
- class Q {
1844
- constructor(e = {}) {
1845
- this.config = e, this.kernel = new B(e), this.director = new G(this), this.kernel.systems.register("director", this.director), this.kernel.events.on(p.Events.TICK, this._update.bind(this), 10), this.kernel.events.on(p.Events.RENDER, this._render.bind(this), 10);
1846
- }
1847
- /**
1848
- * Backward compatibility accessors
1849
- */
1850
- get renderer() {
1851
- return this.kernel.renderer;
1852
- }
1853
- get ticker() {
1854
- return this.kernel.ticker;
1855
- }
1856
- get physics() {
1857
- return this.kernel.physics;
1858
- }
1859
- get input() {
1860
- return this.kernel.input;
1861
- }
1862
- /**
1863
- * Start the game loop.
1864
- */
1865
- start() {
1866
- this.kernel.start();
1867
- }
1868
- /**
1869
- * Stop the game loop.
1870
- */
1871
- stop() {
1872
- this.kernel.stop();
1873
- }
1874
- /**
1875
- * Switch to a new scene.
1876
- * @param {Scene} newScene
1877
- */
1878
- setScene(e) {
1879
- e.kernel = this.kernel, this.director.run(e);
1880
- }
1881
- _update({ dt: e }) {
1882
- this.director.currentScene && this.director.currentScene.update(e);
1883
- }
1884
- _render({ dt: e }) {
1885
- this.renderer.clear(), this.director.currentScene && this.director.currentScene.render(this.renderer.ctx);
1886
- }
1887
- /**
1888
- * Destroy the game and cleanup
1889
- */
1890
- destroy() {
1891
- this.kernel.destroy();
1892
- }
1893
- }
1894
- class Z {
1895
- constructor(e = 0, t = 0) {
1896
- this.x = e, this.y = t, this.rotation = 0, this.scale = { x: 1, y: 1 }, this.components = [], this.children = [], this.parent = null, this._kernel = null;
1897
- }
1898
- get kernel() {
1899
- return this._kernel;
1900
- }
1901
- set kernel(e) {
1902
- this._kernel = e;
1903
- for (const t of this.components)
1904
- t.kernel = e;
1905
- for (const t of this.children)
1906
- t.kernel = e;
1907
- }
1908
- /**
1909
- * Add a child object.
1910
- * @param {GameObject} child
1911
- */
1912
- addChild(e) {
1913
- return e.parent = this, e.kernel = this.kernel, this.children.push(e), e;
1914
- }
1915
- /**
1916
- * Add a component.
1917
- * @param {Component} component
1918
- */
1919
- addComponent(e) {
1920
- return e.gameObject = this, e.kernel = this.kernel, this.components.push(e), e.onAttach(), e;
1921
- }
1922
- /**
1923
- * Get a component by class type.
1924
- * @param {class} type
1925
- */
1926
- getComponent(e) {
1927
- return this.components.find((t) => t instanceof e);
1928
- }
1929
- update(e) {
1930
- for (const t of this.components)
1931
- t.update && t.update(e);
1932
- for (const t of this.children)
1933
- t.update(e);
1934
- }
1935
- render(e) {
1936
- e.save(), e.translate(this.x, this.y), e.rotate(this.rotation), e.scale(this.scale.x, this.scale.y);
1937
- for (const t of this.components)
1938
- t.render && t.render(e);
1939
- for (const t of this.children)
1940
- t.render(e);
1941
- e.restore();
1942
- }
1943
- }
1944
- class P {
1945
- constructor(e = 0, t = 0) {
1946
- this.x = e, this.y = t;
1947
- }
1948
- add(e) {
1949
- return this.x += e.x, this.y += e.y, this;
1950
- }
1951
- sub(e) {
1952
- return this.x -= e.x, this.y -= e.y, this;
1953
- }
1954
- scale(e) {
1955
- return this.x *= e, this.y *= e, this;
1956
- }
1957
- distance(e) {
1958
- const t = this.x - e.x, s = this.y - e.y;
1959
- return Math.sqrt(t * t + s * s);
1960
- }
1961
- normalize() {
1962
- const e = Math.sqrt(this.x * this.x + this.y * this.y);
1963
- return e > 0 && (this.x /= e, this.y /= e), this;
1964
- }
1965
- clone() {
1966
- return new P(this.x, this.y);
1967
- }
1968
- static distance(e, t) {
1969
- return Math.sqrt(Math.pow(t.x - e.x, 2) + Math.pow(t.y - e.y, 2));
1970
- }
1971
- }
1972
- class j extends y {
1973
- constructor(e) {
1974
- super(), this.image = e, this.anchor = { x: 0.5, y: 0.5 }, this.width = e ? e.width : 0, this.height = e ? e.height : 0, this.frame = { x: 0, y: 0, w: this.width, h: this.height };
1975
- }
1976
- setFrame(e, t, s, i) {
1977
- this.frame = { x: e, y: t, w: s, h: i }, this.width = s, this.height = i;
1978
- }
1979
- setTexture(e) {
1980
- this.image = e, e && this.setFrame(0, 0, e.width, e.height);
1981
- }
1982
- render(e) {
1983
- if (!this.image) return;
1984
- const t = this.frame.w, s = this.frame.h, i = -t * this.anchor.x, r = -s * this.anchor.y;
1985
- e.drawImage(
1986
- this.image,
1987
- this.frame.x,
1988
- this.frame.y,
1989
- t,
1990
- s,
1991
- // Source
1992
- i,
1993
- r,
1994
- t,
1995
- s
1996
- // Destination
1997
- );
1998
- }
1999
- }
2000
- class ee extends y {
2001
- constructor() {
2002
- super(), this.animations = {}, this.currentAnim = null, this.timer = 0, this.currentFrameIndex = 0, this.sprite = null;
2003
- }
2004
- onAttach() {
2005
- this.sprite = this.gameObject.getComponent(j), this.sprite || console.warn("Animator attached to GameObject without a Sprite component.");
2006
- }
2007
- addAnimation(e, t, s = 10, i = !0) {
2008
- this.animations[e] = { frames: t, fps: s, loop: i };
2009
- }
2010
- play(e) {
2011
- this.currentAnim && this.currentAnim.name === e || this.animations[e] && (this.currentAnim = { name: e, ...this.animations[e] }, this.currentFrameIndex = 0, this.timer = 0, this._updateFrame());
2012
- }
2013
- update(e) {
2014
- if (!this.currentAnim || !this.sprite) return;
2015
- this.timer += e;
2016
- const t = 1 / this.currentAnim.fps;
2017
- this.timer >= t && (this.timer -= t, this.currentFrameIndex++, this.currentFrameIndex >= this.currentAnim.frames.length && (this.currentAnim.loop ? this.currentFrameIndex = 0 : this.currentFrameIndex = this.currentAnim.frames.length - 1), this._updateFrame());
2018
- }
2019
- _updateFrame() {
2020
- const e = this.currentAnim.frames[this.currentFrameIndex];
2021
- this.sprite.setFrame(e.x, e.y, e.w, e.h);
2022
- }
2023
- }
2024
- class te {
2025
- static createGround(e, t, s = "#333") {
1333
+ class $ {
1334
+ static createGround(t, e, s = "#333") {
2026
1335
  const i = document.createElement("canvas");
2027
- i.width = e, i.height = t;
2028
- const r = i.getContext("2d");
2029
- r.fillStyle = s, r.fillRect(0, 0, e, t);
2030
- for (let n = 0; n < 1e3; n++) {
2031
- r.fillStyle = Math.random() > 0.5 ? "#444" : "#222";
1336
+ i.width = t, i.height = e;
1337
+ const n = i.getContext("2d");
1338
+ n.fillStyle = s, n.fillRect(0, 0, t, e);
1339
+ for (let a = 0; a < 1e3; a++) {
1340
+ n.fillStyle = Math.random() > 0.5 ? "#444" : "#222";
2032
1341
  const o = Math.random() * 4 + 2;
2033
- r.fillRect(Math.random() * e, Math.random() * t, o, o);
1342
+ n.fillRect(Math.random() * t, Math.random() * e, o, o);
2034
1343
  }
2035
- r.strokeStyle = "rgba(255,255,255,0.05)", r.lineWidth = 2, r.beginPath();
2036
- for (let n = 0; n <= e; n += 64)
2037
- r.moveTo(n, 0), r.lineTo(n, t);
2038
- for (let n = 0; n <= t; n += 64)
2039
- r.moveTo(0, n), r.lineTo(e, n);
2040
- return r.stroke(), i;
2041
- }
2042
- static createCharacterSheet(e, t) {
2043
- const r = document.createElement("canvas");
2044
- r.width = 64 * 4, r.height = 64;
2045
- const n = r.getContext("2d");
1344
+ n.strokeStyle = "rgba(255,255,255,0.05)", n.lineWidth = 2, n.beginPath();
1345
+ for (let a = 0; a <= t; a += 64)
1346
+ n.moveTo(a, 0), n.lineTo(a, e);
1347
+ for (let a = 0; a <= e; a += 64)
1348
+ n.moveTo(0, a), n.lineTo(t, a);
1349
+ return n.stroke(), i;
1350
+ }
1351
+ static createCharacterSheet(t, e) {
1352
+ const n = document.createElement("canvas");
1353
+ n.width = 256, n.height = 64;
1354
+ const a = n.getContext("2d");
2046
1355
  for (let o = 0; o < 4; o++) {
2047
- const c = o * 64, h = 0, l = c + 64 / 2, u = h + 64 / 2, m = o % 2 === 0 ? 0 : -2, f = o === 1 ? 5 : o === 3 ? -5 : 0;
2048
- n.fillStyle = e, n.beginPath(), n.arc(l, u + m, 20, 0, Math.PI * 2), n.fill(), n.fillStyle = t, n.beginPath(), n.arc(l - 15, u + m + f, 8, 0, Math.PI * 2), n.arc(l + 15, u + m - f, 8, 0, Math.PI * 2), n.fill(), n.fillStyle = F(e), n.beginPath(), n.arc(l, u + m, 15, 0, Math.PI * 2), n.fill(), t === "#333" && (n.fillStyle = "#000", n.fillRect(l + 10, u + m - 5, 25, 6));
2049
- }
2050
- return r;
2051
- }
2052
- }
2053
- function F(a, e) {
2054
- return a;
2055
- }
2056
- class se extends y {
2057
- constructor(e) {
2058
- super(), this.system = e;
2059
- }
2060
- /**
2061
- * Trigger an effect at the GameObject's current position.
2062
- * @param {string} effectName - 'confetti', 'fire', 'sparkle', etc.
2063
- * @param {object} options - override options
2064
- */
2065
- play(e, t = {}) {
2066
- const s = this.gameObject.x, i = this.gameObject.y;
2067
- switch (e) {
2068
- case "confetti":
2069
- this.system.confetti(s, i, t.count);
2070
- break;
2071
- case "fire":
2072
- this.system.fire(s, i, t.count);
2073
- break;
2074
- case "sparkle":
2075
- this.system.sparkle(s, i, t.count, t.color);
2076
- break;
2077
- case "blood":
2078
- this.system.emit({
2079
- x: s,
2080
- y: i,
2081
- count: 10,
2082
- color: ["#8a0303", "#ff0000"],
2083
- speed: 4,
2084
- gravity: 0.1,
2085
- decay: 0.05
2086
- });
2087
- break;
2088
- default:
2089
- this.system.emit({
2090
- x: s,
2091
- y: i,
2092
- ...t
2093
- });
1356
+ const l = o * 64, h = 0, d = l + 64 / 2, u = h + 64 / 2, p = o % 2 === 0 ? 0 : -2, g = o === 1 ? 5 : o === 3 ? -5 : 0;
1357
+ a.fillStyle = t, a.beginPath(), a.arc(d, u + p, 20, 0, Math.PI * 2), a.fill(), a.fillStyle = e, a.beginPath(), a.arc(d - 15, u + p + g, 8, 0, Math.PI * 2), a.arc(d + 15, u + p - g, 8, 0, Math.PI * 2), a.fill(), a.fillStyle = O(t), a.beginPath(), a.arc(d, u + p, 15, 0, Math.PI * 2), a.fill(), e === "#333" && (a.fillStyle = "#000", a.fillRect(d + 10, u + p - 5, 25, 6));
2094
1358
  }
1359
+ return n;
2095
1360
  }
2096
1361
  }
2097
- class ie extends y {
2098
- constructor() {
2099
- super(), this.intensity = 0, this.duration = 0, this.timer = 0, this.originalPos = { x: 0, y: 0 }, this.isShaking = !1;
2100
- }
2101
- shake(e = 5, t = 0.2) {
2102
- this.intensity = e, this.duration = t, this.timer = 0, this.isShaking = !0, this.timer === 0 && (this.originalPos.x = this.gameObject.x, this.originalPos.y = this.gameObject.y);
2103
- }
2104
- update(e) {
2105
- if (!this.isShaking) return;
2106
- if (this.timer += e, this.timer >= this.duration) {
2107
- this.isShaking = !1, this.gameObject.x = this.originalPos.x, this.gameObject.y = this.originalPos.y;
2108
- return;
2109
- }
2110
- const t = (Math.random() - 0.5) * 2 * this.intensity, s = (Math.random() - 0.5) * 2 * this.intensity;
2111
- this.gameObject.x = this.originalPos.x + t, this.gameObject.y = this.originalPos.y + s;
2112
- }
1362
+ function O(r, t) {
1363
+ return r;
2113
1364
  }
2114
1365
  export {
2115
- ee as Animator,
2116
- H as Collider,
2117
- y as Component,
2118
- A as DataService,
2119
- G as Director,
2120
- B as DopamineKernel,
2121
- p as EventBus,
2122
- Q as Game,
2123
- Z as GameObject,
2124
- D as GameUI,
2125
- q as GlobalInput,
2126
- K as GlobalLoader,
2127
- $ as GlobalPhysics,
2128
- W as IAudioSystem,
2129
- Y as IParticleSystem,
2130
- J as IPhysicsSystem,
2131
- g as ISystem,
2132
- M as Input,
2133
- E as Loader,
2134
- se as ParticleEmitter,
2135
- T as ParticleSystem,
2136
- b as Physics,
2137
- U as PluginRegistry,
2138
- R as Renderer,
2139
- O as RewardSystem,
2140
- V as Scene,
2141
- ie as ScreenShake,
2142
- I as SoundManager,
2143
- j as Sprite,
2144
- X as System,
2145
- L as SystemRegistry,
2146
- te as TextureGenerator,
2147
- z as Ticker,
2148
- P as Vector2,
2149
- N as default
1366
+ j as Animator,
1367
+ X as Collider,
1368
+ J as Component,
1369
+ I as DataService,
1370
+ V as Director,
1371
+ W as DopamineKernel,
1372
+ K as EventBus,
1373
+ T as EventEmitter,
1374
+ Y as Game,
1375
+ Q as GameObject,
1376
+ L as GameUI,
1377
+ Z as GlobalInput,
1378
+ tt as GlobalLoader,
1379
+ et as GlobalPhysics,
1380
+ B as IAudioSystem,
1381
+ U as IParticleSystem,
1382
+ R as IPhysicsSystem,
1383
+ _ as ISystem,
1384
+ st as Input,
1385
+ it as Loader,
1386
+ at as ParticleEmitter,
1387
+ z as ParticleSystem,
1388
+ nt as Physics,
1389
+ rt as PluginRegistry,
1390
+ ot as Renderer,
1391
+ D as RewardSystem,
1392
+ lt as Scene,
1393
+ ct as ScreenShake,
1394
+ F as SoundManager,
1395
+ ht as Sprite,
1396
+ G as System,
1397
+ dt as SystemRegistry,
1398
+ $ as TextureGenerator,
1399
+ ut as Ticker,
1400
+ pt as Vector2,
1401
+ P as createMemoryStorage,
1402
+ N as default,
1403
+ A as resolveStorage
2150
1404
  };