dopaminejs 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/dopamine.js +1947 -0
  2. package/dist/dopamine.umd.cjs +45 -0
  3. package/package.json +24 -32
  4. package/src/core/Animator.js +62 -0
  5. package/src/core/Collider.js +50 -0
  6. package/src/core/Component.js +38 -0
  7. package/src/core/DopamineKernel.js +153 -0
  8. package/src/core/EventBus.js +183 -0
  9. package/src/core/Game.js +89 -0
  10. package/src/core/GameObject.js +83 -0
  11. package/src/core/PluginRegistry.js +197 -0
  12. package/src/core/Scene.js +65 -0
  13. package/src/core/Sprite.js +51 -0
  14. package/src/core/SystemRegistry.js +242 -0
  15. package/src/core/Vector2.js +50 -0
  16. package/src/{audio → dopamine/audio}/SoundManager.js +1 -3
  17. package/src/dopamine/components/ParticleEmitter.js +50 -0
  18. package/src/dopamine/components/ScreenShake.js +49 -0
  19. package/src/dopamine/components/index.js +4 -0
  20. package/src/dopamine/effects/WebGLParticleSystem.js +348 -0
  21. package/src/dopamine/index.js +62 -0
  22. package/src/dopamine/utils/TextureGenerator.js +90 -0
  23. package/src/index.js +34 -62
  24. package/src/interfaces/IAudioSystem.js +58 -0
  25. package/src/interfaces/IParticleSystem.js +51 -0
  26. package/src/interfaces/IPhysicsSystem.js +52 -0
  27. package/src/interfaces/ISystem.js +48 -0
  28. package/src/interfaces/index.js +7 -0
  29. package/src/renderer/Renderer.js +37 -0
  30. package/src/systems/Director.js +27 -0
  31. package/src/systems/Input.js +77 -0
  32. package/src/systems/Loader.js +44 -0
  33. package/src/systems/Physics.js +117 -0
  34. package/src/systems/Ticker.js +61 -0
  35. package/ARCHITECTURE.md +0 -71
  36. package/CHANGELOG.md +0 -36
  37. package/README.md +0 -264
  38. package/dist/dopamine.mjs +0 -737
  39. package/dist/dopamine.umd.js +0 -45
  40. /package/src/{core → dopamine/core}/DataService.js +0 -0
  41. /package/src/{core → dopamine/core}/EventEmitter.js +0 -0
  42. /package/src/{core → dopamine/core}/RewardSystem.js +0 -0
  43. /package/src/{effects → dopamine/effects}/ParticleSystem.js +0 -0
  44. /package/src/{ui → dopamine/ui}/GameUI.js +0 -0
  45. /package/src/{ui → dopamine/ui}/dopamine.css +0 -0
package/dist/dopamine.mjs DELETED
@@ -1,737 +0,0 @@
1
- class g {
2
- constructor() {
3
- this.listeners = /* @__PURE__ */ new Map();
4
- }
5
- /**
6
- * Subscribe to an event
7
- * @param {string} event - Event name
8
- * @param {Function} callback - Callback function
9
- * @returns {Function} Unsubscribe function
10
- */
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);
13
- }
14
- /**
15
- * Unsubscribe from an event
16
- * @param {string} event - Event name
17
- * @param {Function} callback - Callback function
18
- */
19
- off(e, t) {
20
- this.listeners.has(e) && this.listeners.get(e).delete(t);
21
- }
22
- /**
23
- * Emit an event
24
- * @param {string} event - Event name
25
- * @param {*} data - Event data
26
- */
27
- emit(e, t) {
28
- this.listeners.has(e) && this.listeners.get(e).forEach((s) => {
29
- try {
30
- s(t);
31
- } catch (i) {
32
- console.error(`Error in listener for event '${e}':`, i);
33
- }
34
- });
35
- }
36
- /**
37
- * Clear all listeners
38
- */
39
- clear() {
40
- this.listeners.clear();
41
- }
42
- }
43
- class x extends g {
44
- constructor(e, t = {}) {
45
- super(), this.dataService = e, this.player = null, this.achievements = { ...this._initAchievements(), ...t.achievements || {} };
46
- }
47
- /**
48
- * Initialize the reward system for current player
49
- * @returns {Promise<Object>} Player data
50
- */
51
- async init() {
52
- return this.player = await this.dataService.load("player", this._getDefaultPlayer()), this._updateDailyStreak(), await this.save(), this.player;
53
- }
54
- /**
55
- * Get default player data structure
56
- */
57
- _getDefaultPlayer() {
58
- return {
59
- name: "Player",
60
- xp: 0,
61
- level: 1,
62
- totalGamesPlayed: 0,
63
- createdAt: Date.now(),
64
- lastPlayedAt: Date.now(),
65
- streak: {
66
- current: 1,
67
- longest: 1,
68
- lastPlayDate: this._getTodayDateString()
69
- },
70
- achievements: {},
71
- // achievement_id: { unlockedAt: timestamp, seen: boolean }
72
- stats: {
73
- // Dynamic stats object, games will add their own keys
74
- }
75
- };
76
- }
77
- /**
78
- * Save player data
79
- */
80
- async save() {
81
- this.player && (this.player.lastPlayedAt = Date.now(), await this.dataService.save("player", this.player));
82
- }
83
- /**
84
- * Add XP and handle level ups
85
- * @param {number} amount - XP to add
86
- * @param {string} reason - Why XP was awarded (for notifications)
87
- * @returns {Object} { leveledUp: boolean, newLevel: number, xpGained: number }
88
- */
89
- async addXP(e, t = "") {
90
- const s = this.player.level;
91
- this.player.xp += e;
92
- const i = this._calculateLevel(this.player.xp), a = i > s;
93
- return a && (this.player.level = i), await this.save(), this.emit("xp_gained", { amount: e, reason: t, leveledUp: a, newLevel: i }), a && this.emit("level_up", { oldLevel: s, newLevel: i }), { leveledUp: a, newLevel: i, xpGained: e };
94
- }
95
- /**
96
- * Calculate level from XP (exponential curve)
97
- */
98
- _calculateLevel(e) {
99
- const t = Math.floor((1 + Math.sqrt(1 + 8 * e / 50)) / 2);
100
- return Math.max(1, t);
101
- }
102
- /**
103
- * Get XP needed for next level
104
- */
105
- getXPForNextLevel() {
106
- const e = this.player.level + 1, t = 50 * e * (e - 1), s = 50 * this.player.level * (this.player.level - 1);
107
- return {
108
- total: t,
109
- needed: t - this.player.xp,
110
- progress: (this.player.xp - s) / (t - s)
111
- };
112
- }
113
- /**
114
- * Record game result and award XP
115
- * @param {string} gameName - Unique ID for the game
116
- * @param {Object} result - Game-specific result data
117
- */
118
- async recordGame(e, t) {
119
- this.player.stats[e] || (this.player.stats[e] = { totalPlays: 0, highScore: 0 });
120
- const s = this.player.stats[e];
121
- s.totalPlays++, this.player.totalGamesPlayed++, t.score !== void 0 && t.score > s.highScore && (s.highScore = t.score, this.emit("new_high_score", { gameName: e, score: t.score })), Object.keys(t).forEach((a) => {
122
- a !== "score" && (s[a] = (s[a] || 0) + (typeof t[a] == "number" ? t[a] : 0));
123
- });
124
- let i = 10;
125
- 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();
126
- }
127
- /**
128
- * Check and unlock achievements
129
- */
130
- async checkAchievements(e, t) {
131
- const s = [];
132
- for (const [i, a] of Object.entries(this.achievements))
133
- this.player.achievements[i] || a.check(this.player, e, t) && (await this.unlockAchievement(i), s.push(a));
134
- return s;
135
- }
136
- /**
137
- * Unlock an achievement
138
- */
139
- async unlockAchievement(e) {
140
- const t = this.achievements[e];
141
- return t ? (this.player.achievements[e] = {
142
- unlockedAt: Date.now(),
143
- seen: !1
144
- }, await this.addXP(t.xp, `Achievement: ${t.name}`), this.emit("achievement_unlocked", t), await this.save(), !0) : !1;
145
- }
146
- /**
147
- * Get all unlocked achievements
148
- */
149
- getUnlockedAchievements() {
150
- return Object.entries(this.player.achievements).map(([e, t]) => ({
151
- ...this.achievements[e],
152
- id: e,
153
- ...t
154
- }));
155
- }
156
- /**
157
- * Get achievements user hasn't seen the notification for yet
158
- */
159
- getUnseenAchievements() {
160
- return this.getUnlockedAchievements().filter((e) => !e.seen);
161
- }
162
- /**
163
- * Mark achievements as seen
164
- */
165
- async markAchievementsSeen(e) {
166
- for (const t of e)
167
- this.player.achievements[t] && (this.player.achievements[t].seen = !0);
168
- await this.save();
169
- }
170
- /**
171
- * Update daily streak
172
- */
173
- _updateDailyStreak() {
174
- const e = this._getTodayDateString(), t = this.player.streak.lastPlayDate;
175
- if (t === e)
176
- return;
177
- const s = this._getYesterdayDateString();
178
- 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;
179
- }
180
- /**
181
- * Get streak multiplier for XP
182
- */
183
- getStreakMultiplier() {
184
- const e = this.player.streak.current;
185
- return e >= 7 ? 2 : e >= 5 ? 1.5 : e >= 3 ? 1.25 : 1;
186
- }
187
- /**
188
- * Get today's date as YYYY-MM-DD string
189
- */
190
- _getTodayDateString() {
191
- return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
192
- }
193
- /**
194
- * Get yesterday's date as YYYY-MM-DD string
195
- */
196
- _getYesterdayDateString() {
197
- const e = /* @__PURE__ */ new Date();
198
- return e.setDate(e.getDate() - 1), e.toISOString().split("T")[0];
199
- }
200
- /**
201
- * Initialize achievement definitions
202
- * Can be extended by games
203
- */
204
- _initAchievements() {
205
- return {
206
- // Universal achievements
207
- first_game: {
208
- name: "First Steps",
209
- description: "Play your first game",
210
- icon: "🎮",
211
- xp: 50,
212
- check: (e) => e.totalGamesPlayed >= 1
213
- },
214
- level_5: {
215
- name: "Rising Star",
216
- description: "Reach level 5",
217
- icon: "⭐",
218
- xp: 100,
219
- check: (e) => e.level >= 5
220
- },
221
- level_10: {
222
- name: "Expert Player",
223
- description: "Reach level 10",
224
- icon: "🏆",
225
- xp: 250,
226
- check: (e) => e.level >= 10
227
- },
228
- streak_3: {
229
- name: "Dedicated",
230
- description: "Play 3 days in a row",
231
- icon: "🔥",
232
- xp: 100,
233
- check: (e) => e.streak.current >= 3
234
- },
235
- streak_7: {
236
- name: "Unstoppable",
237
- description: "Play 7 days in a row",
238
- icon: "💥",
239
- xp: 300,
240
- check: (e) => e.streak.current >= 7
241
- }
242
- };
243
- }
244
- }
245
- class w {
246
- constructor(e = {}) {
247
- this.storage = e.storage || window.localStorage, this.prefix = e.prefix || "dopamine_";
248
- }
249
- /**
250
- * Save data to local storage
251
- * @param {string} key
252
- * @param {any} data
253
- */
254
- async save(e, t) {
255
- try {
256
- const s = JSON.stringify(t);
257
- return this.storage.setItem(this.prefix + e, s), !0;
258
- } catch (s) {
259
- return console.error("Error saving data:", s), !1;
260
- }
261
- }
262
- /**
263
- * Load data from local storage
264
- * @param {string} key
265
- * @param {any} defaultValue
266
- */
267
- async load(e, t = null) {
268
- try {
269
- const s = this.storage.getItem(this.prefix + e);
270
- return s ? JSON.parse(s) : t;
271
- } catch (s) {
272
- return console.error("Error loading data:", s), t;
273
- }
274
- }
275
- /**
276
- * Clear specific data
277
- * @param {string} key
278
- */
279
- async clear(e) {
280
- this.storage.removeItem(this.prefix + e);
281
- }
282
- }
283
- class S {
284
- constructor(e) {
285
- this.particleSystem = e, this.container = null, this.xpBar = null, this.levelDisplay = null, this.streakDisplay = null, this.init();
286
- }
287
- /**
288
- * Initialize UI overlay
289
- */
290
- init() {
291
- this.container = document.createElement("div"), this.container.id = "game-ui-overlay", this.container.innerHTML = `
292
- <div class="game-ui-top-bar">
293
- <div class="level-badge" id="level-badge">
294
- <span class="level-label">LVL</span>
295
- <span class="level-number" id="level-number">1</span>
296
- </div>
297
-
298
- <div class="xp-container">
299
- <div class="xp-bar-bg">
300
- <div class="xp-bar-fill" id="xp-bar-fill" style="width: 0%"></div>
301
- </div>
302
- <div class="xp-text" id="xp-text">0 /100 XP</div>
303
- </div>
304
-
305
- <div class="streak-badge" id="streak-badge">
306
- <span class="streak-icon">🔥</span>
307
- <span class="streak-number" id="streak-number">1</span>
308
- </div>
309
- </div>
310
- `, document.body.appendChild(this.container), this.xpBar = document.getElementById("xp-bar-fill"), this.xpText = document.getElementById("xp-text"), this.levelDisplay = document.getElementById("level-number"), this.streakDisplay = document.getElementById("streak-number");
311
- }
312
- /**
313
- * Update XP bar
314
- */
315
- updateXP(e, t, s) {
316
- const i = e / s * 100;
317
- this.xpBar.style.width = `${Math.min(100, i)}%`, this.xpText.textContent = `${e} / ${s} XP`, this.xpBar.style.animation = "none", setTimeout(() => {
318
- this.xpBar.style.animation = "xp-pulse 0.3s ease-out";
319
- }, 10);
320
- }
321
- /**
322
- * Update level display
323
- */
324
- updateLevel(e) {
325
- this.levelDisplay.textContent = e;
326
- const t = document.getElementById("level-badge");
327
- t.style.animation = "none", setTimeout(() => {
328
- t.style.animation = "level-pulse 0.5s ease-out";
329
- }, 10);
330
- }
331
- /**
332
- * Update streak display
333
- */
334
- updateStreak(e) {
335
- this.streakDisplay.textContent = e;
336
- const t = document.getElementById("streak-badge");
337
- e >= 7 ? t.style.background = "linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)" : e >= 3 && (t.style.background = "linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)");
338
- }
339
- /**
340
- * Show achievement unlocked popup
341
- */
342
- showAchievement(e) {
343
- const t = document.createElement("div");
344
- t.className = "achievement-popup", t.innerHTML = `
345
- <div class="achievement-shine"></div>
346
- <div class="achievement-content">
347
- <div class="achievement-icon">${e.icon}</div>
348
- <div class="achievement-info">
349
- <div class="achievement-title">Achievement Unlocked!</div>
350
- <div class="achievement-name">${e.name}</div>
351
- <div class="achievement-desc">${e.description}</div>
352
- <div class="achievement-xp">+${e.xp} XP</div>
353
- </div>
354
- </div>
355
- `, document.body.appendChild(t), setTimeout(() => {
356
- const s = t.getBoundingClientRect();
357
- this.particleSystem.confetti(s.left + s.width / 2, s.top + s.height / 2, 40);
358
- }, 300), setTimeout(() => {
359
- t.style.animation = "slideOut 0.3s ease-in forwards", setTimeout(() => t.remove(), 300);
360
- }, 4e3);
361
- }
362
- /**
363
- * Show level up screen
364
- */
365
- showLevelUp(e, t) {
366
- const s = document.createElement("div");
367
- s.className = "level-up-overlay", s.innerHTML = `
368
- <div class="level-up-content">
369
- <div class="level-up-title">LEVEL UP!</div>
370
- <div class="level-up-number">${t}</div>
371
- <div class="level-up-subtitle">Amazing progress!</div>
372
- </div>
373
- `, document.body.appendChild(s), setTimeout(() => {
374
- for (let i = 0; i < 5; i++)
375
- setTimeout(() => {
376
- const a = Math.random() * window.innerWidth, n = Math.random() * window.innerHeight * 0.6;
377
- this.particleSystem.confetti(a, n, 30);
378
- }, i * 200);
379
- }, 300), setTimeout(() => {
380
- s.style.opacity = "0", setTimeout(() => s.remove(), 500);
381
- }, 2500);
382
- }
383
- /**
384
- * Show floating text (e.g., "+50 XP", "COMBO!")
385
- */
386
- showFloatingText(e, t, s, i = "#ffd700", a = "24px") {
387
- const n = document.createElement("div");
388
- n.className = "floating-text", n.textContent = e, n.style.left = t + "px", n.style.top = s + "px", n.style.color = i, n.style.fontSize = a, document.body.appendChild(n), setTimeout(() => n.remove(), 2e3);
389
- }
390
- /**
391
- * Show combo multiplier
392
- */
393
- showCombo(e, t, s) {
394
- this.showFloatingText(`${e}× COMBO!`, t, s, "#ff6b6b", "32px"), this.particleSystem.fire(t, s, 20);
395
- }
396
- /**
397
- * Show "near miss"indicator
398
- */
399
- showNearMiss(e, t) {
400
- this.showFloatingText("CLOSE CALL!", e, t, "#45b7d1", "20px"), this.particleSystem.sparkle(e, t, 15, "#45b7d1");
401
- }
402
- /**
403
- * Show lucky moment (random multiplier)
404
- */
405
- showLuckyMoment(e, t, s) {
406
- this.showFloatingText(`LUCKY ${e}×!`, t, s, "#ffd700", "36px"), this.particleSystem.starBurst(t, s, 12), this.particleSystem.confetti(t, s, 25);
407
- }
408
- /**
409
- * Show generic notification
410
- */
411
- showNotification(e, t = "normal") {
412
- const s = window.innerWidth / 2, i = window.innerHeight / 3;
413
- t === "legendary" ? this.showLuckyMoment(e, s, i) : t === "rare" ? this.showCombo(e, s, i) : this.showFloatingText(e, s, i, "#fff", "24px");
414
- }
415
- /**
416
- * Show Game Over Summary
417
- */
418
- showSummary(e) {
419
- const t = document.createElement("div");
420
- 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";
421
- let s = "";
422
- e.metrics && (s = Object.entries(e.metrics).map(([i, a]) => `<div style="display:flex;justify-content:space-between;width:100%"><span>${i}:</span> <strong>${a}</strong></div>`).join("")), t.innerHTML = `
423
- <h1>Game Over</h1>
424
- <div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${e.score}</div>
425
- <div style="width: 100%; margin-bottom: 20px;">
426
- ${s}
427
- </div>
428
- <button class="btn" onclick="location.reload()">Play Again</button>
429
- <button class="btn" onclick="window.location.href='index.html'" style="margin-top: 10px; background: #543847;">Exit</button>
430
- `, document.body.appendChild(t);
431
- }
432
- }
433
- class b {
434
- constructor(e = {}) {
435
- 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.getElementById(this.canvasId), this.canvas || (this.canvas = document.createElement("canvas"), 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._resize(), window.ResizeObserver && this.container !== document.body ? new ResizeObserver(() => this._resize()).observe(this.container) : window.addEventListener("resize", () => this._resize());
436
- }
437
- _resize() {
438
- this.container === document.body ? (this.canvas.width = window.innerWidth, this.canvas.height = window.innerHeight) : (this.canvas.width = this.container.clientWidth, this.canvas.height = this.container.clientHeight);
439
- }
440
- /**
441
- * Register a custom sprite image
442
- * @param {string} key
443
- * @param {string} url
444
- */
445
- registerSprite(e, t) {
446
- const s = new Image();
447
- s.src = t, this.sprites.set(e, s);
448
- }
449
- /**
450
- * Register a custom effect
451
- * @param {string} name
452
- * @param {Function} callback - (x, y, ...args) => void
453
- */
454
- registerEffect(e, t) {
455
- this.customEffects.set(e, t);
456
- }
457
- /**
458
- * Play a registered effect
459
- */
460
- play(e, t, s, ...i) {
461
- const a = this.customEffects.get(e);
462
- a ? a(t, s, ...i) : console.warn(`[DopamineJS] Effect '${e}' not found.`);
463
- }
464
- /**
465
- * Emit particles with custom config
466
- * @param {Object} config
467
- */
468
- emit(e) {
469
- const {
470
- x: t,
471
- y: s,
472
- count: i = 10,
473
- speed: a = 5,
474
- gravity: n = 0.1,
475
- decay: o = 0.02,
476
- life: h = 1,
477
- color: l = "#ffd700",
478
- sprite: d = null,
479
- spread: y = Math.PI * 2,
480
- angle: v = 0,
481
- size: f = 5
482
- } = e;
483
- for (let p = 0; p < i; p++) {
484
- const r = this._getParticle(), u = v + (Math.random() - 0.5) * y, m = Math.random() * a;
485
- r.x = t, r.y = s, r.vx = Math.cos(u) * m, r.vy = Math.sin(u) * m, r.life = h, r.decay = o * (0.8 + Math.random() * 0.4), r.gravity = n, r.color = Array.isArray(l) ? l[Math.floor(Math.random() * l.length)] : l, r.size = f * (0.8 + Math.random() * 0.4), r.sprite = d, r.rotation = Math.random() * Math.PI * 2, r.rotationSpeed = (Math.random() - 0.5) * 0.2, this.particles.push(r);
486
- }
487
- this._startAnimation();
488
- }
489
- _getParticle() {
490
- return this.pool.length > 0 ? this.pool.pop() : {};
491
- }
492
- _recycleParticle(e) {
493
- this.pool.push(e);
494
- }
495
- // --- Built-in Effects ---
496
- confetti(e, t, s = 50) {
497
- const i = ["#ff6b6b", "#4ecdc4", "#45b7d1", "#f7dc6f", "#c06c84", "#6c5ce7"];
498
- this.emit({
499
- x: e,
500
- y: t,
501
- count: s,
502
- color: i,
503
- speed: 10,
504
- gravity: 0.3,
505
- decay: 0.015,
506
- size: 8,
507
- spread: Math.PI * 2
508
- });
509
- }
510
- coinShower(e, t, s = 10) {
511
- this.emit({
512
- x: e,
513
- y: t,
514
- count: s,
515
- color: "#ffd700",
516
- speed: 4,
517
- gravity: 0.4,
518
- decay: 0.012,
519
- size: 12,
520
- angle: -Math.PI / 2,
521
- // Upwards initially
522
- spread: Math.PI / 2
523
- });
524
- }
525
- sparkle(e, t, s = 20, i = "#ffd700") {
526
- this.emit({
527
- x: e,
528
- y: t,
529
- count: s,
530
- color: i,
531
- speed: 3,
532
- gravity: -0.05,
533
- // Float up
534
- decay: 0.03,
535
- size: 3
536
- });
537
- }
538
- fire(e, t, s = 15) {
539
- const i = ["#ff6b00", "#ff8800", "#ffaa00", "#ff4400"];
540
- this.emit({
541
- x: e,
542
- y: t,
543
- count: s,
544
- color: i,
545
- speed: 3,
546
- gravity: -0.2,
547
- decay: 0.04,
548
- size: 5,
549
- angle: -Math.PI / 2,
550
- spread: Math.PI / 4
551
- });
552
- }
553
- starBurst(e, t, s = 8) {
554
- for (let i = 0; i < s; i++) {
555
- const a = this._getParticle(), n = Math.PI * 2 / s * i, o = 4;
556
- a.x = e, a.y = t, a.vx = Math.cos(n) * o, a.vy = Math.sin(n) * o, a.life = 1, a.decay = 0.03, a.gravity = 0, a.color = "#ffffff", a.size = 15, a.sprite = null, a.type = "star", a.rotation = n, a.rotationSpeed = 0, this.particles.push(a);
557
- }
558
- this._startAnimation();
559
- }
560
- _startAnimation() {
561
- this.isAnimating || (this.isAnimating = !0, this._animate());
562
- }
563
- _animate() {
564
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
565
- for (let e = this.particles.length - 1; e >= 0; e--) {
566
- const t = this.particles[e];
567
- 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) {
568
- this._recycleParticle(t), this.particles.splice(e, 1);
569
- continue;
570
- }
571
- 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)) {
572
- const s = this.sprites.get(t.sprite);
573
- s.complete && this.ctx.drawImage(s, -t.size / 2, -t.size / 2, t.size, t.size);
574
- } 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());
575
- this.ctx.restore();
576
- }
577
- this.particles.length > 0 ? requestAnimationFrame(() => this._animate()) : this.isAnimating = !1;
578
- }
579
- _drawStarPath(e, t, s, i, a) {
580
- let n = Math.PI / 2 * 3, o = e, h = t;
581
- const l = Math.PI / s;
582
- this.ctx.beginPath(), this.ctx.moveTo(e, t - i);
583
- for (let d = 0; d < s; d++)
584
- o = e + Math.cos(n) * i, h = t + Math.sin(n) * i, this.ctx.lineTo(o, h), n += l, o = e + Math.cos(n) * a, h = t + Math.sin(n) * a, this.ctx.lineTo(o, h), n += l;
585
- this.ctx.lineTo(e, t - i), this.ctx.closePath();
586
- }
587
- clear() {
588
- this.particles.forEach((e) => this._recycleParticle(e)), this.particles = [], this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
589
- }
590
- }
591
- class P {
592
- constructor(e = {}) {
593
- this.audioContext = null, this.storageKey = e.storageKey || "dopamineSoundsMuted", this.muted = localStorage.getItem(this.storageKey) === "true", this.assets = /* @__PURE__ */ new Map(), this.soundPack = e.soundPack || "synth", this.customSounds = e.customSounds || {}, Object.keys(this.customSounds).length > 0 && this.preloadSounds(this.customSounds);
594
- }
595
- /**
596
- * Initialize Audio Context (must be called after user interaction)
597
- */
598
- initAudio() {
599
- if (!this.audioContext) {
600
- const e = window.AudioContext || window.webkitAudioContext;
601
- this.audioContext = new e();
602
- }
603
- this.audioContext.state === "suspended" && this.audioContext.resume();
604
- }
605
- /**
606
- * Toggle mute state
607
- * @returns {boolean} New mute state
608
- */
609
- toggleMute() {
610
- return this.muted = !this.muted, localStorage.setItem(this.storageKey, this.muted), this.muted;
611
- }
612
- /**
613
- * Register a custom sound URL
614
- * @param {string} key - Unique identifier (e.g., 'jump', 'win')
615
- * @param {string} url - Path to audio file
616
- */
617
- registerSound(e, t) {
618
- this.customSounds[e] = t, this.audioContext && this.loadSound(e, t);
619
- }
620
- /**
621
- * Preload multiple sounds
622
- * @param {Object} soundMap - { key: url }
623
- */
624
- async preloadSounds(e) {
625
- const t = Object.entries(e).map(([s, i]) => this.loadSound(s, i));
626
- await Promise.allSettled(t);
627
- }
628
- /**
629
- * Load and decode a sound file
630
- */
631
- async loadSound(e, t) {
632
- try {
633
- if (!this.audioContext)
634
- return;
635
- const i = await (await fetch(t)).arrayBuffer(), a = await this.audioContext.decodeAudioData(i);
636
- this.assets.set(e, a);
637
- } catch (s) {
638
- console.warn(`[DopamineJS] Failed to load sound '${e}':`, s);
639
- }
640
- }
641
- /**
642
- * Play a sound by key (custom) or fallback to synth
643
- * @param {string} key - 'jump', 'score', 'success', etc.
644
- */
645
- async play(e) {
646
- if (this.muted) return;
647
- if (this.initAudio(), this.assets.has(e)) {
648
- this._playBuffer(this.assets.get(e));
649
- return;
650
- }
651
- if (this.customSounds[e] && !this.assets.has(e) && (await this.loadSound(e, this.customSounds[e]), this.assets.has(e))) {
652
- this._playBuffer(this.assets.get(e));
653
- return;
654
- }
655
- const t = {
656
- jump: () => this.playJump(!0),
657
- // true = force synth
658
- score: () => this.playScore(!0),
659
- success: () => this.playSuccess(!0),
660
- click: () => this.playClick(!0),
661
- error: () => this.playError(!0),
662
- gameover: () => this.playGameOver(!0)
663
- };
664
- t[e] && t[e]();
665
- }
666
- _playBuffer(e) {
667
- const t = this.audioContext.createBufferSource();
668
- t.buffer = e, t.connect(this.audioContext.destination), t.start(0);
669
- }
670
- /**
671
- * Play a synthesized tone
672
- */
673
- playTone(e, t, s = "sine", i = 0.3) {
674
- if (this.muted) return;
675
- this.initAudio();
676
- const a = this.audioContext.createOscillator(), n = this.audioContext.createGain();
677
- a.connect(n), n.connect(this.audioContext.destination), a.frequency.value = e, a.type = s, n.gain.setValueAtTime(i, this.audioContext.currentTime), n.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + t), a.start(this.audioContext.currentTime), a.stop(this.audioContext.currentTime + t);
678
- }
679
- // --- Presets (Synth) ---
680
- playJump(e = !1) {
681
- if (!e && this.assets.has("jump")) return this.play("jump");
682
- this.playTone(400, 0.1, "square", 0.2);
683
- }
684
- playScore(e = !1) {
685
- if (!e && this.assets.has("score")) return this.play("score");
686
- this.playTone(800, 0.15, "sine", 0.25), setTimeout(() => this.playTone(1e3, 0.15, "sine", 0.25), 50);
687
- }
688
- playGameOver(e = !1) {
689
- if (!e && this.assets.has("gameover")) return this.play("gameover");
690
- 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);
691
- }
692
- playClick(e = !1) {
693
- if (!e && this.assets.has("click")) return this.play("click");
694
- this.playTone(600, 0.05, "sine", 0.1);
695
- }
696
- playSuccess(e = !1) {
697
- if (!e && this.assets.has("success")) return this.play("success");
698
- this.playTone(600, 0.1, "sine", 0.2), setTimeout(() => this.playTone(800, 0.2, "sine", 0.2), 100);
699
- }
700
- playError(e = !1) {
701
- if (!e && this.assets.has("error")) return this.play("error");
702
- this.playTone(200, 0.2, "sawtooth", 0.3);
703
- }
704
- }
705
- class M {
706
- constructor(e = {}) {
707
- this.config = e, this.dataService = new w(e.data || {}), this.rewardSystem = new x(this.dataService, e.rewards || {}), this.soundManager = new P(e.sound || {}), this.particleSystem = new b(e.particles || {}), this.gameUI = new S(this.particleSystem), this._bindEvents();
708
- }
709
- async init() {
710
- return await this.rewardSystem.init(), console.log("DopamineJS Initialized 🚀"), {
711
- rewardSystem: this.rewardSystem,
712
- gameUI: this.gameUI,
713
- particleSystem: this.particleSystem,
714
- soundManager: this.soundManager
715
- };
716
- }
717
- _bindEvents() {
718
- this.rewardSystem.on("xp_gained", (e) => {
719
- const { total: t, needed: s } = this.rewardSystem.getXPForNextLevel();
720
- this.gameUI.updateXP(this.rewardSystem.player.xp, s, t), e.xpGained > 0;
721
- }), this.rewardSystem.on("level_up", (e) => {
722
- this.gameUI.updateLevel(e.newLevel), this.gameUI.showLevelUp(e.oldLevel, e.newLevel);
723
- }), this.rewardSystem.on("achievement_unlocked", (e) => {
724
- this.gameUI.showAchievement(e);
725
- }), this.rewardSystem.on("new_high_score", (e) => {
726
- this.gameUI.showNotification(`New High Score: ${e.score}!`, "legendary"), this.soundManager.playSuccess();
727
- });
728
- }
729
- }
730
- export {
731
- w as DataService,
732
- S as GameUI,
733
- b as ParticleSystem,
734
- x as RewardSystem,
735
- P as SoundManager,
736
- M as default
737
- };