dopaminejs 1.0.0 → 1.0.2

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.mjs CHANGED
@@ -1,52 +1,55 @@
1
- class v {
1
+ class p {
2
2
  constructor() {
3
- this.storage = window.localStorage;
3
+ this.listeners = /* @__PURE__ */ new Map();
4
4
  }
5
5
  /**
6
- * Save data to local storage
7
- * @param {string} key
8
- * @param {any} data
6
+ * Subscribe to an event
7
+ * @param {string} event - Event name
8
+ * @param {Function} callback - Callback function
9
+ * @returns {Function} Unsubscribe function
9
10
  */
10
- async save(e, t) {
11
- try {
12
- const i = JSON.stringify(t);
13
- return this.storage.setItem(e, i), !0;
14
- } catch (i) {
15
- return console.error("Error saving data:", i), !1;
16
- }
11
+ on(t, e) {
12
+ return this.listeners.has(t) || this.listeners.set(t, /* @__PURE__ */ new Set()), this.listeners.get(t).add(e), () => this.off(t, e);
17
13
  }
18
14
  /**
19
- * Load data from local storage
20
- * @param {string} key
21
- * @param {any} defaultValue
15
+ * Unsubscribe from an event
16
+ * @param {string} event - Event name
17
+ * @param {Function} callback - Callback function
22
18
  */
23
- async load(e, t = null) {
24
- try {
25
- const i = this.storage.getItem(e);
26
- return i ? JSON.parse(i) : t;
27
- } catch (i) {
28
- return console.error("Error loading data:", i), t;
29
- }
19
+ off(t, e) {
20
+ this.listeners.has(t) && this.listeners.get(t).delete(e);
30
21
  }
31
22
  /**
32
- * Clear specific data
33
- * @param {string} key
23
+ * Emit an event
24
+ * @param {string} event - Event name
25
+ * @param {*} data - Event data
34
26
  */
35
- async clear(e) {
36
- this.storage.removeItem(e);
27
+ emit(t, e) {
28
+ this.listeners.has(t) && this.listeners.get(t).forEach((i) => {
29
+ try {
30
+ i(e);
31
+ } catch (s) {
32
+ console.error(`Error in listener for event '${t}':`, s);
33
+ }
34
+ });
35
+ }
36
+ /**
37
+ * Clear all listeners
38
+ */
39
+ clear() {
40
+ this.listeners.clear();
37
41
  }
38
42
  }
39
- const m = new v();
40
- class u {
41
- constructor() {
42
- this.player = null, this.achievements = this._initAchievements(), this.listeners = [];
43
+ class m extends p {
44
+ constructor(t, e = {}) {
45
+ super(), this.dataService = t, this.player = null, this.achievements = { ...this._initAchievements(), ...e.achievements || {} };
43
46
  }
44
47
  /**
45
48
  * Initialize the reward system for current player
46
49
  * @returns {Promise<Object>} Player data
47
50
  */
48
51
  async init() {
49
- return this.player = await m.load("player", this._getDefaultPlayer()), this._updateDailyStreak(), await this.save(), this.player;
52
+ return this.player = await this.dataService.load("player", this._getDefaultPlayer()), this._updateDailyStreak(), await this.save(), this.player;
50
53
  }
51
54
  /**
52
55
  * Get default player data structure
@@ -75,7 +78,7 @@ class u {
75
78
  * Save player data
76
79
  */
77
80
  async save() {
78
- this.player && (this.player.lastPlayedAt = Date.now(), await m.save("player", this.player));
81
+ this.player && (this.player.lastPlayedAt = Date.now(), await this.dataService.save("player", this.player));
79
82
  }
80
83
  /**
81
84
  * Add XP and handle level ups
@@ -83,28 +86,28 @@ class u {
83
86
  * @param {string} reason - Why XP was awarded (for notifications)
84
87
  * @returns {Object} { leveledUp: boolean, newLevel: number, xpGained: number }
85
88
  */
86
- async addXP(e, t = "") {
89
+ async addXP(t, e = "") {
87
90
  const i = this.player.level;
88
- this.player.xp += e;
91
+ this.player.xp += t;
89
92
  const s = this._calculateLevel(this.player.xp), a = s > i;
90
- return a && (this.player.level = s), await this.save(), this._emit("xp_gained", { amount: e, reason: t, leveledUp: a, newLevel: s }), a && this._emit("level_up", { oldLevel: i, newLevel: s }), { leveledUp: a, newLevel: s, xpGained: e };
93
+ return a && (this.player.level = s), await this.save(), this.emit("xp_gained", { amount: t, reason: e, leveledUp: a, newLevel: s }), a && this.emit("level_up", { oldLevel: i, newLevel: s }), { leveledUp: a, newLevel: s, xpGained: t };
91
94
  }
92
95
  /**
93
96
  * Calculate level from XP (exponential curve)
94
97
  */
95
- _calculateLevel(e) {
96
- const t = Math.floor((1 + Math.sqrt(1 + 8 * e / 50)) / 2);
97
- return Math.max(1, t);
98
+ _calculateLevel(t) {
99
+ const e = Math.floor((1 + Math.sqrt(1 + 8 * t / 50)) / 2);
100
+ return Math.max(1, e);
98
101
  }
99
102
  /**
100
103
  * Get XP needed for next level
101
104
  */
102
105
  getXPForNextLevel() {
103
- const e = this.player.level + 1, t = 50 * e * (e - 1), i = 50 * this.player.level * (this.player.level - 1);
106
+ const t = this.player.level + 1, e = 50 * t * (t - 1), i = 50 * this.player.level * (this.player.level - 1);
104
107
  return {
105
- total: t,
106
- needed: t - this.player.xp,
107
- progress: (this.player.xp - i) / (t - i)
108
+ total: e,
109
+ needed: e - this.player.xp,
110
+ progress: (this.player.xp - i) / (e - i)
108
111
  };
109
112
  }
110
113
  /**
@@ -112,74 +115,74 @@ class u {
112
115
  * @param {string} gameName - Unique ID for the game
113
116
  * @param {Object} result - Game-specific result data
114
117
  */
115
- async recordGame(e, t) {
116
- this.player.stats[e] || (this.player.stats[e] = { totalPlays: 0, highScore: 0 });
117
- const i = this.player.stats[e];
118
- i.totalPlays++, this.player.totalGamesPlayed++, t.score !== void 0 && t.score > i.highScore && (i.highScore = t.score, this._emit("new_high_score", { gameName: e, score: t.score })), Object.keys(t).forEach((a) => {
119
- a !== "score" && (i[a] = (i[a] || 0) + (typeof t[a] == "number" ? t[a] : 0));
118
+ async recordGame(t, e) {
119
+ this.player.stats[t] || (this.player.stats[t] = { totalPlays: 0, highScore: 0 });
120
+ const i = this.player.stats[t];
121
+ i.totalPlays++, this.player.totalGamesPlayed++, e.score !== void 0 && e.score > i.highScore && (i.highScore = e.score, this.emit("new_high_score", { gameName: t, score: e.score })), Object.keys(e).forEach((a) => {
122
+ a !== "score" && (i[a] = (i[a] || 0) + (typeof e[a] == "number" ? e[a] : 0));
120
123
  });
121
124
  let s = 10;
122
- t.score && (s += Math.floor(t.score / 5)), s = Math.floor(s * this.getStreakMultiplier()), await this.addXP(s, `Played ${e}`), await this.checkAchievements(e, t), await this.save();
125
+ e.score && (s += Math.floor(e.score / 5)), s = Math.floor(s * this.getStreakMultiplier()), await this.addXP(s, `Played ${t}`), await this.checkAchievements(t, e), await this.save();
123
126
  }
124
127
  /**
125
128
  * Check and unlock achievements
126
129
  */
127
- async checkAchievements(e, t) {
130
+ async checkAchievements(t, e) {
128
131
  const i = [];
129
132
  for (const [s, a] of Object.entries(this.achievements))
130
- this.player.achievements[s] || a.check(this.player, e, t) && (await this.unlockAchievement(s), i.push(a));
133
+ this.player.achievements[s] || a.check(this.player, t, e) && (await this.unlockAchievement(s), i.push(a));
131
134
  return i;
132
135
  }
133
136
  /**
134
137
  * Unlock an achievement
135
138
  */
136
- async unlockAchievement(e) {
137
- const t = this.achievements[e];
138
- return t ? (this.player.achievements[e] = {
139
+ async unlockAchievement(t) {
140
+ const e = this.achievements[t];
141
+ return e ? (this.player.achievements[t] = {
139
142
  unlockedAt: Date.now(),
140
143
  seen: !1
141
- }, await this.addXP(t.xp, `Achievement: ${t.name}`), this._emit("achievement_unlocked", t), await this.save(), !0) : !1;
144
+ }, await this.addXP(e.xp, `Achievement: ${e.name}`), this.emit("achievement_unlocked", e), await this.save(), !0) : !1;
142
145
  }
143
146
  /**
144
147
  * Get all unlocked achievements
145
148
  */
146
149
  getUnlockedAchievements() {
147
- return Object.entries(this.player.achievements).map(([e, t]) => ({
148
- ...this.achievements[e],
149
- id: e,
150
- ...t
150
+ return Object.entries(this.player.achievements).map(([t, e]) => ({
151
+ ...this.achievements[t],
152
+ id: t,
153
+ ...e
151
154
  }));
152
155
  }
153
156
  /**
154
157
  * Get achievements user hasn't seen the notification for yet
155
158
  */
156
159
  getUnseenAchievements() {
157
- return this.getUnlockedAchievements().filter((e) => !e.seen);
160
+ return this.getUnlockedAchievements().filter((t) => !t.seen);
158
161
  }
159
162
  /**
160
163
  * Mark achievements as seen
161
164
  */
162
- async markAchievementsSeen(e) {
163
- for (const t of e)
164
- this.player.achievements[t] && (this.player.achievements[t].seen = !0);
165
+ async markAchievementsSeen(t) {
166
+ for (const e of t)
167
+ this.player.achievements[e] && (this.player.achievements[e].seen = !0);
165
168
  await this.save();
166
169
  }
167
170
  /**
168
171
  * Update daily streak
169
172
  */
170
173
  _updateDailyStreak() {
171
- const e = this._getTodayDateString(), t = this.player.streak.lastPlayDate;
172
- if (t === e)
174
+ const t = this._getTodayDateString(), e = this.player.streak.lastPlayDate;
175
+ if (e === t)
173
176
  return;
174
177
  const i = this._getYesterdayDateString();
175
- t === i ? (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;
178
+ e === i ? (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;
176
179
  }
177
180
  /**
178
181
  * Get streak multiplier for XP
179
182
  */
180
183
  getStreakMultiplier() {
181
- const e = this.player.streak.current;
182
- return e >= 7 ? 2 : e >= 5 ? 1.5 : e >= 3 ? 1.25 : 1;
184
+ const t = this.player.streak.current;
185
+ return t >= 7 ? 2 : t >= 5 ? 1.5 : t >= 3 ? 1.25 : 1;
183
186
  }
184
187
  /**
185
188
  * Get today's date as YYYY-MM-DD string
@@ -191,17 +194,8 @@ class u {
191
194
  * Get yesterday's date as YYYY-MM-DD string
192
195
  */
193
196
  _getYesterdayDateString() {
194
- const e = /* @__PURE__ */ new Date();
195
- return e.setDate(e.getDate() - 1), e.toISOString().split("T")[0];
196
- }
197
- /**
198
- * Event system for UI updates
199
- */
200
- on(e, t) {
201
- this.listeners.push({ event: e, callback: t });
202
- }
203
- _emit(e, t) {
204
- this.listeners.filter((i) => i.event === e).forEach((i) => i.callback(t));
197
+ const t = /* @__PURE__ */ new Date();
198
+ return t.setDate(t.getDate() - 1), t.toISOString().split("T")[0];
205
199
  }
206
200
  /**
207
201
  * Initialize achievement definitions
@@ -215,196 +209,80 @@ class u {
215
209
  description: "Play your first game",
216
210
  icon: "🎮",
217
211
  xp: 50,
218
- check: (e) => e.totalGamesPlayed >= 1
212
+ check: (t) => t.totalGamesPlayed >= 1
219
213
  },
220
214
  level_5: {
221
215
  name: "Rising Star",
222
216
  description: "Reach level 5",
223
217
  icon: "⭐",
224
218
  xp: 100,
225
- check: (e) => e.level >= 5
219
+ check: (t) => t.level >= 5
226
220
  },
227
221
  level_10: {
228
222
  name: "Expert Player",
229
223
  description: "Reach level 10",
230
224
  icon: "🏆",
231
225
  xp: 250,
232
- check: (e) => e.level >= 10
226
+ check: (t) => t.level >= 10
233
227
  },
234
228
  streak_3: {
235
229
  name: "Dedicated",
236
230
  description: "Play 3 days in a row",
237
231
  icon: "🔥",
238
232
  xp: 100,
239
- check: (e) => e.streak.current >= 3
233
+ check: (t) => t.streak.current >= 3
240
234
  },
241
235
  streak_7: {
242
236
  name: "Unstoppable",
243
237
  description: "Play 7 days in a row",
244
238
  icon: "💥",
245
239
  xp: 300,
246
- check: (e) => e.streak.current >= 7
240
+ check: (t) => t.streak.current >= 7
247
241
  }
248
242
  };
249
243
  }
250
- /**
251
- * Register custom achievements
252
- * @param {Object} achievements
253
- */
254
- registerAchievements(e) {
255
- this.achievements = { ...this.achievements, ...e };
256
- }
257
244
  }
258
- const y = new u();
259
- class f {
260
- constructor(e = "dopamine-particle-canvas") {
261
- this.canvasId = e, this.canvas = document.getElementById(e), this.canvas || (this.canvas = document.createElement("canvas"), this.canvas.id = e, this.canvas.style.position = "fixed", this.canvas.style.top = "0", this.canvas.style.left = "0", this.canvas.style.width = "100%", this.canvas.style.height = "100%", this.canvas.style.pointerEvents = "none", this.canvas.style.zIndex = "9999", document.body.appendChild(this.canvas)), this.ctx = this.canvas.getContext("2d"), this.particles = [], this.isAnimating = !1, this._resize(), window.addEventListener("resize", () => this._resize());
262
- }
263
- _resize() {
264
- this.canvas.width = window.innerWidth, this.canvas.height = window.innerHeight;
245
+ class y {
246
+ constructor(t = {}) {
247
+ this.storage = t.storage || window.localStorage, this.prefix = t.prefix || "dopamine_";
265
248
  }
266
249
  /**
267
- * Confetti explosion (for achievements, level ups)
268
- */
269
- confetti(e, t, i = 50) {
270
- const s = ["#ff6b6b", "#4ecdc4", "#45b7d1", "#f7dc6f", "#c06c84", "#6c5ce7"];
271
- for (let a = 0; a < i; a++)
272
- this.particles.push({
273
- x: e,
274
- y: t,
275
- vx: (Math.random() - 0.5) * 10,
276
- vy: (Math.random() - 0.5) * 10 - 5,
277
- rotation: Math.random() * Math.PI * 2,
278
- rotationSpeed: (Math.random() - 0.5) * 0.2,
279
- color: s[Math.floor(Math.random() * s.length)],
280
- size: Math.random() * 8 + 4,
281
- life: 1,
282
- decay: 0.015 + Math.random() * 0.01,
283
- gravity: 0.3,
284
- type: "confetti"
285
- });
286
- this._startAnimation();
287
- }
288
- /**
289
- * Sparkle effect (for score milestones, combos)
250
+ * Save data to local storage
251
+ * @param {string} key
252
+ * @param {any} data
290
253
  */
291
- sparkle(e, t, i = 20, s = "#ffd700") {
292
- for (let a = 0; a < i; a++) {
293
- const n = Math.PI * 2 / i * a, o = 2 + Math.random() * 3;
294
- this.particles.push({
295
- x: e,
296
- y: t,
297
- vx: Math.cos(n) * o,
298
- vy: Math.sin(n) * o,
299
- color: s,
300
- size: Math.random() * 4 + 2,
301
- life: 1,
302
- decay: 0.02,
303
- gravity: -0.1,
304
- // Slight upward drift
305
- type: "spark"
306
- });
254
+ async save(t, e) {
255
+ try {
256
+ const i = JSON.stringify(e);
257
+ return this.storage.setItem(this.prefix + t, i), !0;
258
+ } catch (i) {
259
+ return console.error("Error saving data:", i), !1;
307
260
  }
308
- this._startAnimation();
309
- }
310
- /**
311
- * Coin shower (for collecting coins)
312
- */
313
- coinShower(e, t, i = 10) {
314
- for (let s = 0; s < i; s++)
315
- this.particles.push({
316
- x: e + (Math.random() - 0.5) * 100,
317
- y: t - Math.random() * 50,
318
- vx: (Math.random() - 0.5) * 3,
319
- vy: Math.random() * -2 - 3,
320
- color: "#ffd700",
321
- size: 12,
322
- life: 1,
323
- decay: 0.012,
324
- gravity: 0.4,
325
- type: "coin"
326
- });
327
- this._startAnimation();
328
- }
329
- /**
330
- * Fire particles (for streaks, combos)
331
- */
332
- fire(e, t, i = 15) {
333
- const s = ["#ff6b00", "#ff8800", "#ffaa00", "#ff4400"];
334
- for (let a = 0; a < i; a++)
335
- this.particles.push({
336
- x: e + (Math.random() - 0.5) * 20,
337
- y: t,
338
- vx: (Math.random() - 0.5) * 2,
339
- vy: -Math.random() * 3 - 2,
340
- color: s[Math.floor(Math.random() * s.length)],
341
- size: Math.random() * 6 + 3,
342
- life: 1,
343
- decay: 0.025,
344
- gravity: -0.2,
345
- // Upward
346
- type: "fire"
347
- });
348
- this._startAnimation();
349
261
  }
350
262
  /**
351
- * Star burst (for "perfect" moments)
263
+ * Load data from local storage
264
+ * @param {string} key
265
+ * @param {any} defaultValue
352
266
  */
353
- starBurst(e, t, i = 8) {
354
- for (let s = 0; s < i; s++) {
355
- const a = Math.PI * 2 / i * s, n = 4;
356
- this.particles.push({
357
- x: e,
358
- y: t,
359
- vx: Math.cos(a) * n,
360
- vy: Math.sin(a) * n,
361
- rotation: a,
362
- color: "#ffffff",
363
- size: 15,
364
- life: 1,
365
- decay: 0.03,
366
- gravity: 0,
367
- type: "star"
368
- });
369
- }
370
- this._startAnimation();
371
- }
372
- _startAnimation() {
373
- this.isAnimating || (this.isAnimating = !0, this._animate());
374
- }
375
- _animate() {
376
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
377
- for (let e = this.particles.length - 1; e >= 0; e--) {
378
- const t = this.particles[e];
379
- 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) {
380
- this.particles.splice(e, 1);
381
- continue;
382
- }
383
- this.ctx.save(), this.ctx.globalAlpha = t.life, this.ctx.fillStyle = t.color, t.type === "confetti" ? (this.ctx.translate(t.x, t.y), this.ctx.rotate(t.rotation), this.ctx.fillRect(-t.size / 2, -t.size / 2, t.size, t.size / 2)) : t.type === "coin" ? (this.ctx.beginPath(), this.ctx.arc(t.x, t.y, t.size, 0, Math.PI * 2), this.ctx.fill(), this.ctx.fillStyle = "#ffee88", this.ctx.beginPath(), this.ctx.arc(t.x - t.size / 3, t.y - t.size / 3, t.size / 3, 0, Math.PI * 2), this.ctx.fill()) : t.type === "star" ? this._drawStar(t.x, t.y, 5, t.size, t.size / 2, t.rotation) : (this.ctx.beginPath(), this.ctx.arc(t.x, t.y, t.size, 0, Math.PI * 2), this.ctx.fill()), this.ctx.restore();
384
- }
385
- this.particles.length > 0 ? requestAnimationFrame(() => this._animate()) : this.isAnimating = !1;
386
- }
387
- _drawStar(e, t, i, s, a, n) {
388
- let o = Math.PI / 2 * 3 + n;
389
- const d = Math.PI / i;
390
- this.ctx.beginPath(), this.ctx.moveTo(e, t - s);
391
- for (let p = 0; p < i; p++) {
392
- let c = e + Math.cos(o) * s, h = t + Math.sin(o) * s;
393
- this.ctx.lineTo(c, h), o += d, c = e + Math.cos(o) * a, h = t + Math.sin(o) * a, this.ctx.lineTo(c, h), o += d;
267
+ async load(t, e = null) {
268
+ try {
269
+ const i = this.storage.getItem(this.prefix + t);
270
+ return i ? JSON.parse(i) : e;
271
+ } catch (i) {
272
+ return console.error("Error loading data:", i), e;
394
273
  }
395
- this.ctx.lineTo(e, t - s), this.ctx.closePath(), this.ctx.fill();
396
274
  }
397
275
  /**
398
- * Clear all particles
276
+ * Clear specific data
277
+ * @param {string} key
399
278
  */
400
- clear() {
401
- this.particles = [], this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
279
+ async clear(t) {
280
+ this.storage.removeItem(this.prefix + t);
402
281
  }
403
282
  }
404
- const l = new f();
405
- class g {
406
- constructor() {
407
- this.container = null, this.xpBar = null, this.levelDisplay = null, this.streakDisplay = null, this.init();
283
+ class v {
284
+ constructor(t) {
285
+ this.particleSystem = t, this.container = null, this.xpBar = null, this.levelDisplay = null, this.streakDisplay = null, this.init();
408
286
  }
409
287
  /**
410
288
  * Initialize UI overlay
@@ -434,69 +312,69 @@ class g {
434
312
  /**
435
313
  * Update XP bar
436
314
  */
437
- updateXP(e, t, i) {
438
- const s = e / i * 100;
439
- this.xpBar.style.width = `${Math.min(100, s)}%`, this.xpText.textContent = `${e} / ${i} XP`, this.xpBar.style.animation = "none", setTimeout(() => {
315
+ updateXP(t, e, i) {
316
+ const s = t / i * 100;
317
+ this.xpBar.style.width = `${Math.min(100, s)}%`, this.xpText.textContent = `${t} / ${i} XP`, this.xpBar.style.animation = "none", setTimeout(() => {
440
318
  this.xpBar.style.animation = "xp-pulse 0.3s ease-out";
441
319
  }, 10);
442
320
  }
443
321
  /**
444
322
  * Update level display
445
323
  */
446
- updateLevel(e) {
447
- this.levelDisplay.textContent = e;
448
- const t = document.getElementById("level-badge");
449
- t.style.animation = "none", setTimeout(() => {
450
- t.style.animation = "level-pulse 0.5s ease-out";
324
+ updateLevel(t) {
325
+ this.levelDisplay.textContent = t;
326
+ const e = document.getElementById("level-badge");
327
+ e.style.animation = "none", setTimeout(() => {
328
+ e.style.animation = "level-pulse 0.5s ease-out";
451
329
  }, 10);
452
330
  }
453
331
  /**
454
332
  * Update streak display
455
333
  */
456
- updateStreak(e) {
457
- this.streakDisplay.textContent = e;
458
- const t = document.getElementById("streak-badge");
459
- e >= 7 ? t.style.background = "linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)" : e >= 3 && (t.style.background = "linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)");
334
+ updateStreak(t) {
335
+ this.streakDisplay.textContent = t;
336
+ const e = document.getElementById("streak-badge");
337
+ t >= 7 ? e.style.background = "linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)" : t >= 3 && (e.style.background = "linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)");
460
338
  }
461
339
  /**
462
340
  * Show achievement unlocked popup
463
341
  */
464
- showAchievement(e) {
465
- const t = document.createElement("div");
466
- t.className = "achievement-popup", t.innerHTML = `
342
+ showAchievement(t) {
343
+ const e = document.createElement("div");
344
+ e.className = "achievement-popup", e.innerHTML = `
467
345
  <div class="achievement-shine"></div>
468
346
  <div class="achievement-content">
469
- <div class="achievement-icon">${e.icon}</div>
347
+ <div class="achievement-icon">${t.icon}</div>
470
348
  <div class="achievement-info">
471
349
  <div class="achievement-title">Achievement Unlocked!</div>
472
- <div class="achievement-name">${e.name}</div>
473
- <div class="achievement-desc">${e.description}</div>
474
- <div class="achievement-xp">+${e.xp} XP</div>
350
+ <div class="achievement-name">${t.name}</div>
351
+ <div class="achievement-desc">${t.description}</div>
352
+ <div class="achievement-xp">+${t.xp} XP</div>
475
353
  </div>
476
354
  </div>
477
- `, document.body.appendChild(t), setTimeout(() => {
478
- const i = t.getBoundingClientRect();
479
- l.confetti(i.left + i.width / 2, i.top + i.height / 2, 40);
355
+ `, document.body.appendChild(e), setTimeout(() => {
356
+ const i = e.getBoundingClientRect();
357
+ this.particleSystem.confetti(i.left + i.width / 2, i.top + i.height / 2, 40);
480
358
  }, 300), setTimeout(() => {
481
- t.style.animation = "slideOut 0.3s ease-in forwards", setTimeout(() => t.remove(), 300);
359
+ e.style.animation = "slideOut 0.3s ease-in forwards", setTimeout(() => e.remove(), 300);
482
360
  }, 4e3);
483
361
  }
484
362
  /**
485
363
  * Show level up screen
486
364
  */
487
- showLevelUp(e, t) {
365
+ showLevelUp(t, e) {
488
366
  const i = document.createElement("div");
489
367
  i.className = "level-up-overlay", i.innerHTML = `
490
368
  <div class="level-up-content">
491
369
  <div class="level-up-title">LEVEL UP!</div>
492
- <div class="level-up-number">${t}</div>
370
+ <div class="level-up-number">${e}</div>
493
371
  <div class="level-up-subtitle">Amazing progress!</div>
494
372
  </div>
495
373
  `, document.body.appendChild(i), setTimeout(() => {
496
374
  for (let s = 0; s < 5; s++)
497
375
  setTimeout(() => {
498
376
  const a = Math.random() * window.innerWidth, n = Math.random() * window.innerHeight * 0.6;
499
- l.confetti(a, n, 30);
377
+ this.particleSystem.confetti(a, n, 30);
500
378
  }, s * 200);
501
379
  }, 300), setTimeout(() => {
502
380
  i.style.opacity = "0", setTimeout(() => i.remove(), 500);
@@ -505,57 +383,201 @@ class g {
505
383
  /**
506
384
  * Show floating text (e.g., "+50 XP", "COMBO!")
507
385
  */
508
- showFloatingText(e, t, i, s = "#ffd700", a = "24px") {
386
+ showFloatingText(t, e, i, s = "#ffd700", a = "24px") {
509
387
  const n = document.createElement("div");
510
- n.className = "floating-text", n.textContent = e, n.style.left = t + "px", n.style.top = i + "px", n.style.color = s, n.style.fontSize = a, document.body.appendChild(n), setTimeout(() => n.remove(), 2e3);
388
+ n.className = "floating-text", n.textContent = t, n.style.left = e + "px", n.style.top = i + "px", n.style.color = s, n.style.fontSize = a, document.body.appendChild(n), setTimeout(() => n.remove(), 2e3);
511
389
  }
512
390
  /**
513
391
  * Show combo multiplier
514
392
  */
515
- showCombo(e, t, i) {
516
- this.showFloatingText(`${e}× COMBO!`, t, i, "#ff6b6b", "32px"), l.fire(t, i, 20);
393
+ showCombo(t, e, i) {
394
+ this.showFloatingText(`${t}× COMBO!`, e, i, "#ff6b6b", "32px"), this.particleSystem.fire(e, i, 20);
517
395
  }
518
396
  /**
519
397
  * Show "near miss"indicator
520
398
  */
521
- showNearMiss(e, t) {
522
- this.showFloatingText("CLOSE CALL!", e, t, "#45b7d1", "20px"), l.sparkle(e, t, 15, "#45b7d1");
399
+ showNearMiss(t, e) {
400
+ this.showFloatingText("CLOSE CALL!", t, e, "#45b7d1", "20px"), this.particleSystem.sparkle(t, e, 15, "#45b7d1");
523
401
  }
524
402
  /**
525
403
  * Show lucky moment (random multiplier)
526
404
  */
527
- showLuckyMoment(e, t, i) {
528
- this.showFloatingText(`LUCKY ${e}×!`, t, i, "#ffd700", "36px"), l.starBurst(t, i, 12), l.confetti(t, i, 25);
405
+ showLuckyMoment(t, e, i) {
406
+ this.showFloatingText(`LUCKY ${t}×!`, e, i, "#ffd700", "36px"), this.particleSystem.starBurst(e, i, 12), this.particleSystem.confetti(e, i, 25);
529
407
  }
530
408
  /**
531
409
  * Show generic notification
532
410
  */
533
- showNotification(e, t = "normal") {
411
+ showNotification(t, e = "normal") {
534
412
  const i = window.innerWidth / 2, s = window.innerHeight / 3;
535
- t === "legendary" ? this.showLuckyMoment(e, i, s) : t === "rare" ? this.showCombo(e, i, s) : this.showFloatingText(e, i, s, "#fff", "24px");
413
+ e === "legendary" ? this.showLuckyMoment(t, i, s) : e === "rare" ? this.showCombo(t, i, s) : this.showFloatingText(t, i, s, "#fff", "24px");
536
414
  }
537
415
  /**
538
416
  * Show Game Over Summary
539
417
  */
540
- showSummary(e) {
541
- const t = document.createElement("div");
542
- 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";
418
+ showSummary(t) {
419
+ const e = document.createElement("div");
420
+ 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";
543
421
  let i = "";
544
- e.metrics && (i = Object.entries(e.metrics).map(([s, a]) => `<div style="display:flex;justify-content:space-between;width:100%"><span>${s}:</span> <strong>${a}</strong></div>`).join("")), t.innerHTML = `
422
+ t.metrics && (i = Object.entries(t.metrics).map(([s, a]) => `<div style="display:flex;justify-content:space-between;width:100%"><span>${s}:</span> <strong>${a}</strong></div>`).join("")), e.innerHTML = `
545
423
  <h1>Game Over</h1>
546
- <div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${e.score}</div>
424
+ <div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${t.score}</div>
547
425
  <div style="width: 100%; margin-bottom: 20px;">
548
426
  ${i}
549
427
  </div>
550
428
  <button class="btn" onclick="location.reload()">Play Again</button>
551
429
  <button class="btn" onclick="window.location.href='index.html'" style="margin-top: 10px; background: #543847;">Exit</button>
552
- `, document.body.appendChild(t);
430
+ `, document.body.appendChild(e);
553
431
  }
554
432
  }
555
- const x = new g();
556
- class w {
557
- constructor() {
558
- this.audioContext = null, this.muted = localStorage.getItem("dopamineSoundsMuted") === "true";
433
+ class u {
434
+ constructor(t = {}) {
435
+ this.canvasId = t.canvasId || "dopamine-particle-canvas", this.canvas = document.getElementById(this.canvasId), this.canvas || (this.canvas = document.createElement("canvas"), this.canvas.id = this.canvasId, this.canvas.style.position = "fixed", this.canvas.style.top = "0", this.canvas.style.left = "0", this.canvas.style.width = "100%", this.canvas.style.height = "100%", this.canvas.style.pointerEvents = "none", this.canvas.style.zIndex = t.zIndex || "9999", document.body.appendChild(this.canvas)), this.ctx = this.canvas.getContext("2d"), this.particles = [], this.isAnimating = !1, this._resize(), window.addEventListener("resize", () => this._resize());
436
+ }
437
+ _resize() {
438
+ this.canvas.width = window.innerWidth, this.canvas.height = window.innerHeight;
439
+ }
440
+ /**
441
+ * Confetti explosion (for achievements, level ups)
442
+ */
443
+ confetti(t, e, i = 50) {
444
+ const s = ["#ff6b6b", "#4ecdc4", "#45b7d1", "#f7dc6f", "#c06c84", "#6c5ce7"];
445
+ for (let a = 0; a < i; a++)
446
+ this.particles.push({
447
+ x: t,
448
+ y: e,
449
+ vx: (Math.random() - 0.5) * 10,
450
+ vy: (Math.random() - 0.5) * 10 - 5,
451
+ rotation: Math.random() * Math.PI * 2,
452
+ rotationSpeed: (Math.random() - 0.5) * 0.2,
453
+ color: s[Math.floor(Math.random() * s.length)],
454
+ size: Math.random() * 8 + 4,
455
+ life: 1,
456
+ decay: 0.015 + Math.random() * 0.01,
457
+ gravity: 0.3,
458
+ type: "confetti"
459
+ });
460
+ this._startAnimation();
461
+ }
462
+ /**
463
+ * Sparkle effect (for score milestones, combos)
464
+ */
465
+ sparkle(t, e, i = 20, s = "#ffd700") {
466
+ for (let a = 0; a < i; a++) {
467
+ const n = Math.PI * 2 / i * a, r = 2 + Math.random() * 3;
468
+ this.particles.push({
469
+ x: t,
470
+ y: e,
471
+ vx: Math.cos(n) * r,
472
+ vy: Math.sin(n) * r,
473
+ color: s,
474
+ size: Math.random() * 4 + 2,
475
+ life: 1,
476
+ decay: 0.02,
477
+ gravity: -0.1,
478
+ // Slight upward drift
479
+ type: "spark"
480
+ });
481
+ }
482
+ this._startAnimation();
483
+ }
484
+ /**
485
+ * Coin shower (for collecting coins)
486
+ */
487
+ coinShower(t, e, i = 10) {
488
+ for (let s = 0; s < i; s++)
489
+ this.particles.push({
490
+ x: t + (Math.random() - 0.5) * 100,
491
+ y: e - Math.random() * 50,
492
+ vx: (Math.random() - 0.5) * 3,
493
+ vy: Math.random() * -2 - 3,
494
+ color: "#ffd700",
495
+ size: 12,
496
+ life: 1,
497
+ decay: 0.012,
498
+ gravity: 0.4,
499
+ type: "coin"
500
+ });
501
+ this._startAnimation();
502
+ }
503
+ /**
504
+ * Fire particles (for streaks, combos)
505
+ */
506
+ fire(t, e, i = 15) {
507
+ const s = ["#ff6b00", "#ff8800", "#ffaa00", "#ff4400"];
508
+ for (let a = 0; a < i; a++)
509
+ this.particles.push({
510
+ x: t + (Math.random() - 0.5) * 20,
511
+ y: e,
512
+ vx: (Math.random() - 0.5) * 2,
513
+ vy: -Math.random() * 3 - 2,
514
+ color: s[Math.floor(Math.random() * s.length)],
515
+ size: Math.random() * 6 + 3,
516
+ life: 1,
517
+ decay: 0.025,
518
+ gravity: -0.2,
519
+ // Upward
520
+ type: "fire"
521
+ });
522
+ this._startAnimation();
523
+ }
524
+ /**
525
+ * Star burst (for "perfect" moments)
526
+ */
527
+ starBurst(t, e, i = 8) {
528
+ for (let s = 0; s < i; s++) {
529
+ const a = Math.PI * 2 / i * s, n = 4;
530
+ this.particles.push({
531
+ x: t,
532
+ y: e,
533
+ vx: Math.cos(a) * n,
534
+ vy: Math.sin(a) * n,
535
+ rotation: a,
536
+ color: "#ffffff",
537
+ size: 15,
538
+ life: 1,
539
+ decay: 0.03,
540
+ gravity: 0,
541
+ type: "star"
542
+ });
543
+ }
544
+ this._startAnimation();
545
+ }
546
+ _startAnimation() {
547
+ this.isAnimating || (this.isAnimating = !0, this._animate());
548
+ }
549
+ _animate() {
550
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
551
+ for (let t = this.particles.length - 1; t >= 0; t--) {
552
+ const e = this.particles[t];
553
+ if (e.vy += e.gravity, e.x += e.vx, e.y += e.vy, e.life -= e.decay, e.rotationSpeed && (e.rotation += e.rotationSpeed), e.life <= 0) {
554
+ this.particles.splice(t, 1);
555
+ continue;
556
+ }
557
+ this.ctx.save(), this.ctx.globalAlpha = e.life, this.ctx.fillStyle = e.color, e.type === "confetti" ? (this.ctx.translate(e.x, e.y), this.ctx.rotate(e.rotation), this.ctx.fillRect(-e.size / 2, -e.size / 2, e.size, e.size / 2)) : e.type === "coin" ? (this.ctx.beginPath(), this.ctx.arc(e.x, e.y, e.size, 0, Math.PI * 2), this.ctx.fill(), this.ctx.fillStyle = "#ffee88", this.ctx.beginPath(), this.ctx.arc(e.x - e.size / 3, e.y - e.size / 3, e.size / 3, 0, Math.PI * 2), this.ctx.fill()) : e.type === "star" ? this._drawStar(e.x, e.y, 5, e.size, e.size / 2, e.rotation) : (this.ctx.beginPath(), this.ctx.arc(e.x, e.y, e.size, 0, Math.PI * 2), this.ctx.fill()), this.ctx.restore();
558
+ }
559
+ this.particles.length > 0 ? requestAnimationFrame(() => this._animate()) : this.isAnimating = !1;
560
+ }
561
+ _drawStar(t, e, i, s, a, n) {
562
+ let r = Math.PI / 2 * 3 + n;
563
+ const c = Math.PI / i;
564
+ this.ctx.beginPath(), this.ctx.moveTo(t, e - s);
565
+ for (let d = 0; d < i; d++) {
566
+ let l = t + Math.cos(r) * s, h = e + Math.sin(r) * s;
567
+ this.ctx.lineTo(l, h), r += c, l = t + Math.cos(r) * a, h = e + Math.sin(r) * a, this.ctx.lineTo(l, h), r += c;
568
+ }
569
+ this.ctx.lineTo(t, e - s), this.ctx.closePath(), this.ctx.fill();
570
+ }
571
+ /**
572
+ * Clear all particles
573
+ */
574
+ clear() {
575
+ this.particles = [], this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
576
+ }
577
+ }
578
+ class f {
579
+ constructor(t = {}) {
580
+ this.audioContext = null, this.storageKey = t.storageKey || "dopamineSoundsMuted", this.muted = localStorage.getItem(this.storageKey) === "true";
559
581
  }
560
582
  /**
561
583
  * Initialize Audio Context (must be called after user interaction)
@@ -568,7 +590,7 @@ class w {
568
590
  * @returns {boolean} New mute state
569
591
  */
570
592
  toggleMute() {
571
- return this.muted = !this.muted, localStorage.setItem("dopamineSoundsMuted", this.muted), this.muted;
593
+ return this.muted = !this.muted, localStorage.setItem(this.storageKey, this.muted), this.muted;
572
594
  }
573
595
  /**
574
596
  * Play a synthesized tone
@@ -577,11 +599,11 @@ class w {
577
599
  * @param {string} type - 'sine', 'square', 'sawtooth', 'triangle'
578
600
  * @param {number} volume
579
601
  */
580
- playTone(e, t, i = "sine", s = 0.3) {
602
+ playTone(t, e, i = "sine", s = 0.3) {
581
603
  if (this.muted) return;
582
604
  this.initAudio();
583
605
  const a = this.audioContext.createOscillator(), n = this.audioContext.createGain();
584
- a.connect(n), n.connect(this.audioContext.destination), a.frequency.value = e, a.type = i, n.gain.setValueAtTime(s, this.audioContext.currentTime), n.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + t), a.start(this.audioContext.currentTime), a.stop(this.audioContext.currentTime + t);
606
+ a.connect(n), n.connect(this.audioContext.destination), a.frequency.value = t, a.type = i, n.gain.setValueAtTime(s, this.audioContext.currentTime), n.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + e), a.start(this.audioContext.currentTime), a.stop(this.audioContext.currentTime + e);
585
607
  }
586
608
  // --- Presets ---
587
609
  playJump() {
@@ -603,25 +625,36 @@ class w {
603
625
  this.playTone(200, 0.2, "sawtooth", 0.3);
604
626
  }
605
627
  }
606
- const b = new w();
607
- class M {
608
- constructor(e = {}) {
609
- this.config = e;
628
+ class g {
629
+ constructor(t = {}) {
630
+ this.config = t, this.dataService = new y(t.data || {}), this.rewardSystem = new m(this.dataService, t.rewards || {}), this.soundManager = new f(t.sound || {}), this.particleSystem = new u(t.particles || {}), this.gameUI = new v(this.particleSystem), this._bindEvents();
610
631
  }
611
632
  async init() {
612
- return await y.init(), console.log("DopamineJS Initialized 🚀"), {
613
- rewardSystem: y,
614
- gameUI: x,
615
- particleSystem: l,
616
- soundManager: b
633
+ return await this.rewardSystem.init(), console.log("DopamineJS Initialized 🚀"), {
634
+ rewardSystem: this.rewardSystem,
635
+ gameUI: this.gameUI,
636
+ particleSystem: this.particleSystem,
637
+ soundManager: this.soundManager
617
638
  };
618
639
  }
640
+ _bindEvents() {
641
+ this.rewardSystem.on("xp_gained", (t) => {
642
+ const { total: e, needed: i } = this.rewardSystem.getXPForNextLevel();
643
+ this.gameUI.updateXP(this.rewardSystem.player.xp, i, e), t.xpGained > 0;
644
+ }), this.rewardSystem.on("level_up", (t) => {
645
+ this.gameUI.updateLevel(t.newLevel), this.gameUI.showLevelUp(t.oldLevel, t.newLevel);
646
+ }), this.rewardSystem.on("achievement_unlocked", (t) => {
647
+ this.gameUI.showAchievement(t);
648
+ }), this.rewardSystem.on("new_high_score", (t) => {
649
+ this.gameUI.showNotification(`New High Score: ${t.score}!`, "legendary"), this.soundManager.playSuccess();
650
+ });
651
+ }
619
652
  }
620
653
  export {
621
- m as dataService,
622
- M as default,
623
- x as gameUI,
624
- l as particleSystem,
625
- y as rewardSystem,
626
- b as soundManager
654
+ y as DataService,
655
+ v as GameUI,
656
+ u as ParticleSystem,
657
+ m as RewardSystem,
658
+ f as SoundManager,
659
+ g as default
627
660
  };