dopaminejs 2.2.0 → 2.3.1

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