dopaminejs 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +71 -0
- package/CHANGELOG.md +27 -0
- package/README.md +189 -60
- package/dist/dopamine.mjs +414 -317
- package/dist/dopamine.umd.js +6 -6
- package/package.json +13 -7
- package/src/audio/SoundManager.js +122 -17
- package/src/core/DataService.js +6 -7
- package/src/core/EventEmitter.js +58 -0
- package/src/core/RewardSystem.js +12 -34
- package/src/effects/ParticleSystem.js +5 -7
- package/src/index.js +48 -11
- package/src/ui/GameUI.js +8 -11
package/dist/dopamine.mjs
CHANGED
|
@@ -1,52 +1,55 @@
|
|
|
1
|
-
class
|
|
1
|
+
class p {
|
|
2
2
|
constructor() {
|
|
3
|
-
this.
|
|
3
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* @param {string}
|
|
8
|
-
* @param {
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
*
|
|
20
|
-
* @param {string}
|
|
21
|
-
* @param {
|
|
15
|
+
* Unsubscribe from an event
|
|
16
|
+
* @param {string} event - Event name
|
|
17
|
+
* @param {Function} callback - Callback function
|
|
22
18
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
*
|
|
33
|
-
* @param {string}
|
|
23
|
+
* Emit an event
|
|
24
|
+
* @param {string} event - Event name
|
|
25
|
+
* @param {*} data - Event data
|
|
26
|
+
*/
|
|
27
|
+
emit(t, e) {
|
|
28
|
+
this.listeners.has(t) && this.listeners.get(t).forEach((s) => {
|
|
29
|
+
try {
|
|
30
|
+
s(e);
|
|
31
|
+
} catch (i) {
|
|
32
|
+
console.error(`Error in listener for event '${t}':`, i);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Clear all listeners
|
|
34
38
|
*/
|
|
35
|
-
|
|
36
|
-
this.
|
|
39
|
+
clear() {
|
|
40
|
+
this.listeners.clear();
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
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(
|
|
87
|
-
const
|
|
88
|
-
this.player.xp +=
|
|
89
|
-
const
|
|
90
|
-
return a && (this.player.level =
|
|
89
|
+
async addXP(t, e = "") {
|
|
90
|
+
const s = this.player.level;
|
|
91
|
+
this.player.xp += t;
|
|
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: t, reason: e, leveledUp: a, newLevel: i }), a && this.emit("level_up", { oldLevel: s, newLevel: i }), { leveledUp: a, newLevel: i, xpGained: t };
|
|
91
94
|
}
|
|
92
95
|
/**
|
|
93
96
|
* Calculate level from XP (exponential curve)
|
|
94
97
|
*/
|
|
95
|
-
_calculateLevel(
|
|
96
|
-
const
|
|
97
|
-
return Math.max(1,
|
|
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
|
|
106
|
+
const t = this.player.level + 1, e = 50 * t * (t - 1), s = 50 * this.player.level * (this.player.level - 1);
|
|
104
107
|
return {
|
|
105
|
-
total:
|
|
106
|
-
needed:
|
|
107
|
-
progress: (this.player.xp -
|
|
108
|
+
total: e,
|
|
109
|
+
needed: e - this.player.xp,
|
|
110
|
+
progress: (this.player.xp - s) / (e - s)
|
|
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(
|
|
116
|
-
this.player.stats[
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
a !== "score" && (
|
|
118
|
+
async recordGame(t, e) {
|
|
119
|
+
this.player.stats[t] || (this.player.stats[t] = { totalPlays: 0, highScore: 0 });
|
|
120
|
+
const s = this.player.stats[t];
|
|
121
|
+
s.totalPlays++, this.player.totalGamesPlayed++, e.score !== void 0 && e.score > s.highScore && (s.highScore = e.score, this.emit("new_high_score", { gameName: t, score: e.score })), Object.keys(e).forEach((a) => {
|
|
122
|
+
a !== "score" && (s[a] = (s[a] || 0) + (typeof e[a] == "number" ? e[a] : 0));
|
|
120
123
|
});
|
|
121
|
-
let
|
|
122
|
-
|
|
124
|
+
let i = 10;
|
|
125
|
+
e.score && (i += 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();
|
|
123
126
|
}
|
|
124
127
|
/**
|
|
125
128
|
* Check and unlock achievements
|
|
126
129
|
*/
|
|
127
|
-
async checkAchievements(
|
|
128
|
-
const
|
|
129
|
-
for (const [
|
|
130
|
-
this.player.achievements[
|
|
131
|
-
return
|
|
130
|
+
async checkAchievements(t, e) {
|
|
131
|
+
const s = [];
|
|
132
|
+
for (const [i, a] of Object.entries(this.achievements))
|
|
133
|
+
this.player.achievements[i] || a.check(this.player, t, e) && (await this.unlockAchievement(i), s.push(a));
|
|
134
|
+
return s;
|
|
132
135
|
}
|
|
133
136
|
/**
|
|
134
137
|
* Unlock an achievement
|
|
135
138
|
*/
|
|
136
|
-
async unlockAchievement(
|
|
137
|
-
const
|
|
138
|
-
return
|
|
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(
|
|
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(([
|
|
148
|
-
...this.achievements[
|
|
149
|
-
id:
|
|
150
|
-
...
|
|
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((
|
|
160
|
+
return this.getUnlockedAchievements().filter((t) => !t.seen);
|
|
158
161
|
}
|
|
159
162
|
/**
|
|
160
163
|
* Mark achievements as seen
|
|
161
164
|
*/
|
|
162
|
-
async markAchievementsSeen(
|
|
163
|
-
for (const
|
|
164
|
-
this.player.achievements[
|
|
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
|
|
172
|
-
if (
|
|
174
|
+
const t = this._getTodayDateString(), e = this.player.streak.lastPlayDate;
|
|
175
|
+
if (e === t)
|
|
173
176
|
return;
|
|
174
|
-
const
|
|
175
|
-
|
|
177
|
+
const s = this._getYesterdayDateString();
|
|
178
|
+
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;
|
|
176
179
|
}
|
|
177
180
|
/**
|
|
178
181
|
* Get streak multiplier for XP
|
|
179
182
|
*/
|
|
180
183
|
getStreakMultiplier() {
|
|
181
|
-
const
|
|
182
|
-
return
|
|
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
|
|
195
|
-
return
|
|
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,50 +209,230 @@ class u {
|
|
|
215
209
|
description: "Play your first game",
|
|
216
210
|
icon: "🎮",
|
|
217
211
|
xp: 50,
|
|
218
|
-
check: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
240
|
+
check: (t) => t.streak.current >= 7
|
|
247
241
|
}
|
|
248
242
|
};
|
|
249
243
|
}
|
|
244
|
+
}
|
|
245
|
+
class u {
|
|
246
|
+
constructor(t = {}) {
|
|
247
|
+
this.storage = t.storage || window.localStorage, this.prefix = t.prefix || "dopamine_";
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Save data to local storage
|
|
251
|
+
* @param {string} key
|
|
252
|
+
* @param {any} data
|
|
253
|
+
*/
|
|
254
|
+
async save(t, e) {
|
|
255
|
+
try {
|
|
256
|
+
const s = JSON.stringify(e);
|
|
257
|
+
return this.storage.setItem(this.prefix + t, 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(t, e = null) {
|
|
268
|
+
try {
|
|
269
|
+
const s = this.storage.getItem(this.prefix + t);
|
|
270
|
+
return s ? JSON.parse(s) : e;
|
|
271
|
+
} catch (s) {
|
|
272
|
+
return console.error("Error loading data:", s), e;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
250
275
|
/**
|
|
251
|
-
*
|
|
252
|
-
* @param {
|
|
276
|
+
* Clear specific data
|
|
277
|
+
* @param {string} key
|
|
253
278
|
*/
|
|
254
|
-
|
|
255
|
-
this.
|
|
279
|
+
async clear(t) {
|
|
280
|
+
this.storage.removeItem(this.prefix + t);
|
|
256
281
|
}
|
|
257
282
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
283
|
+
class y {
|
|
284
|
+
constructor(t) {
|
|
285
|
+
this.particleSystem = t, 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(t, e, s) {
|
|
316
|
+
const i = t / s * 100;
|
|
317
|
+
this.xpBar.style.width = `${Math.min(100, i)}%`, this.xpText.textContent = `${t} / ${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(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";
|
|
329
|
+
}, 10);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Update streak display
|
|
333
|
+
*/
|
|
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%)");
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Show achievement unlocked popup
|
|
341
|
+
*/
|
|
342
|
+
showAchievement(t) {
|
|
343
|
+
const e = document.createElement("div");
|
|
344
|
+
e.className = "achievement-popup", e.innerHTML = `
|
|
345
|
+
<div class="achievement-shine"></div>
|
|
346
|
+
<div class="achievement-content">
|
|
347
|
+
<div class="achievement-icon">${t.icon}</div>
|
|
348
|
+
<div class="achievement-info">
|
|
349
|
+
<div class="achievement-title">Achievement Unlocked!</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>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
`, document.body.appendChild(e), setTimeout(() => {
|
|
356
|
+
const s = e.getBoundingClientRect();
|
|
357
|
+
this.particleSystem.confetti(s.left + s.width / 2, s.top + s.height / 2, 40);
|
|
358
|
+
}, 300), setTimeout(() => {
|
|
359
|
+
e.style.animation = "slideOut 0.3s ease-in forwards", setTimeout(() => e.remove(), 300);
|
|
360
|
+
}, 4e3);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Show level up screen
|
|
364
|
+
*/
|
|
365
|
+
showLevelUp(t, e) {
|
|
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">${e}</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(t, e, s, i = "#ffd700", a = "24px") {
|
|
387
|
+
const n = document.createElement("div");
|
|
388
|
+
n.className = "floating-text", n.textContent = t, n.style.left = e + "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(t, e, s) {
|
|
394
|
+
this.showFloatingText(`${t}× COMBO!`, e, s, "#ff6b6b", "32px"), this.particleSystem.fire(e, s, 20);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Show "near miss"indicator
|
|
398
|
+
*/
|
|
399
|
+
showNearMiss(t, e) {
|
|
400
|
+
this.showFloatingText("CLOSE CALL!", t, e, "#45b7d1", "20px"), this.particleSystem.sparkle(t, e, 15, "#45b7d1");
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Show lucky moment (random multiplier)
|
|
404
|
+
*/
|
|
405
|
+
showLuckyMoment(t, e, s) {
|
|
406
|
+
this.showFloatingText(`LUCKY ${t}×!`, e, s, "#ffd700", "36px"), this.particleSystem.starBurst(e, s, 12), this.particleSystem.confetti(e, s, 25);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Show generic notification
|
|
410
|
+
*/
|
|
411
|
+
showNotification(t, e = "normal") {
|
|
412
|
+
const s = window.innerWidth / 2, i = window.innerHeight / 3;
|
|
413
|
+
e === "legendary" ? this.showLuckyMoment(t, s, i) : e === "rare" ? this.showCombo(t, s, i) : this.showFloatingText(t, s, i, "#fff", "24px");
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Show Game Over Summary
|
|
417
|
+
*/
|
|
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";
|
|
421
|
+
let s = "";
|
|
422
|
+
t.metrics && (s = Object.entries(t.metrics).map(([i, a]) => `<div style="display:flex;justify-content:space-between;width:100%"><span>${i}:</span> <strong>${a}</strong></div>`).join("")), e.innerHTML = `
|
|
423
|
+
<h1>Game Over</h1>
|
|
424
|
+
<div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${t.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(e);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
class v {
|
|
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());
|
|
262
436
|
}
|
|
263
437
|
_resize() {
|
|
264
438
|
this.canvas.width = window.innerWidth, this.canvas.height = window.innerHeight;
|
|
@@ -266,17 +440,17 @@ class f {
|
|
|
266
440
|
/**
|
|
267
441
|
* Confetti explosion (for achievements, level ups)
|
|
268
442
|
*/
|
|
269
|
-
confetti(
|
|
270
|
-
const
|
|
271
|
-
for (let a = 0; a <
|
|
443
|
+
confetti(t, e, s = 50) {
|
|
444
|
+
const i = ["#ff6b6b", "#4ecdc4", "#45b7d1", "#f7dc6f", "#c06c84", "#6c5ce7"];
|
|
445
|
+
for (let a = 0; a < s; a++)
|
|
272
446
|
this.particles.push({
|
|
273
|
-
x:
|
|
274
|
-
y:
|
|
447
|
+
x: t,
|
|
448
|
+
y: e,
|
|
275
449
|
vx: (Math.random() - 0.5) * 10,
|
|
276
450
|
vy: (Math.random() - 0.5) * 10 - 5,
|
|
277
451
|
rotation: Math.random() * Math.PI * 2,
|
|
278
452
|
rotationSpeed: (Math.random() - 0.5) * 0.2,
|
|
279
|
-
color:
|
|
453
|
+
color: i[Math.floor(Math.random() * i.length)],
|
|
280
454
|
size: Math.random() * 8 + 4,
|
|
281
455
|
life: 1,
|
|
282
456
|
decay: 0.015 + Math.random() * 0.01,
|
|
@@ -288,15 +462,15 @@ class f {
|
|
|
288
462
|
/**
|
|
289
463
|
* Sparkle effect (for score milestones, combos)
|
|
290
464
|
*/
|
|
291
|
-
sparkle(
|
|
292
|
-
for (let a = 0; a <
|
|
293
|
-
const n = Math.PI * 2 /
|
|
465
|
+
sparkle(t, e, s = 20, i = "#ffd700") {
|
|
466
|
+
for (let a = 0; a < s; a++) {
|
|
467
|
+
const n = Math.PI * 2 / s * a, r = 2 + Math.random() * 3;
|
|
294
468
|
this.particles.push({
|
|
295
|
-
x:
|
|
296
|
-
y:
|
|
297
|
-
vx: Math.cos(n) *
|
|
298
|
-
vy: Math.sin(n) *
|
|
299
|
-
color:
|
|
469
|
+
x: t,
|
|
470
|
+
y: e,
|
|
471
|
+
vx: Math.cos(n) * r,
|
|
472
|
+
vy: Math.sin(n) * r,
|
|
473
|
+
color: i,
|
|
300
474
|
size: Math.random() * 4 + 2,
|
|
301
475
|
life: 1,
|
|
302
476
|
decay: 0.02,
|
|
@@ -310,11 +484,11 @@ class f {
|
|
|
310
484
|
/**
|
|
311
485
|
* Coin shower (for collecting coins)
|
|
312
486
|
*/
|
|
313
|
-
coinShower(
|
|
314
|
-
for (let
|
|
487
|
+
coinShower(t, e, s = 10) {
|
|
488
|
+
for (let i = 0; i < s; i++)
|
|
315
489
|
this.particles.push({
|
|
316
|
-
x:
|
|
317
|
-
y:
|
|
490
|
+
x: t + (Math.random() - 0.5) * 100,
|
|
491
|
+
y: e - Math.random() * 50,
|
|
318
492
|
vx: (Math.random() - 0.5) * 3,
|
|
319
493
|
vy: Math.random() * -2 - 3,
|
|
320
494
|
color: "#ffd700",
|
|
@@ -329,15 +503,15 @@ class f {
|
|
|
329
503
|
/**
|
|
330
504
|
* Fire particles (for streaks, combos)
|
|
331
505
|
*/
|
|
332
|
-
fire(
|
|
333
|
-
const
|
|
334
|
-
for (let a = 0; a <
|
|
506
|
+
fire(t, e, s = 15) {
|
|
507
|
+
const i = ["#ff6b00", "#ff8800", "#ffaa00", "#ff4400"];
|
|
508
|
+
for (let a = 0; a < s; a++)
|
|
335
509
|
this.particles.push({
|
|
336
|
-
x:
|
|
337
|
-
y:
|
|
510
|
+
x: t + (Math.random() - 0.5) * 20,
|
|
511
|
+
y: e,
|
|
338
512
|
vx: (Math.random() - 0.5) * 2,
|
|
339
513
|
vy: -Math.random() * 3 - 2,
|
|
340
|
-
color:
|
|
514
|
+
color: i[Math.floor(Math.random() * i.length)],
|
|
341
515
|
size: Math.random() * 6 + 3,
|
|
342
516
|
life: 1,
|
|
343
517
|
decay: 0.025,
|
|
@@ -350,12 +524,12 @@ class f {
|
|
|
350
524
|
/**
|
|
351
525
|
* Star burst (for "perfect" moments)
|
|
352
526
|
*/
|
|
353
|
-
starBurst(
|
|
354
|
-
for (let
|
|
355
|
-
const a = Math.PI * 2 /
|
|
527
|
+
starBurst(t, e, s = 8) {
|
|
528
|
+
for (let i = 0; i < s; i++) {
|
|
529
|
+
const a = Math.PI * 2 / s * i, n = 4;
|
|
356
530
|
this.particles.push({
|
|
357
|
-
x:
|
|
358
|
-
y:
|
|
531
|
+
x: t,
|
|
532
|
+
y: e,
|
|
359
533
|
vx: Math.cos(a) * n,
|
|
360
534
|
vy: Math.sin(a) * n,
|
|
361
535
|
rotation: a,
|
|
@@ -374,25 +548,25 @@ class f {
|
|
|
374
548
|
}
|
|
375
549
|
_animate() {
|
|
376
550
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
377
|
-
for (let
|
|
378
|
-
const
|
|
379
|
-
if (
|
|
380
|
-
this.particles.splice(
|
|
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);
|
|
381
555
|
continue;
|
|
382
556
|
}
|
|
383
|
-
this.ctx.save(), this.ctx.globalAlpha =
|
|
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();
|
|
384
558
|
}
|
|
385
559
|
this.particles.length > 0 ? requestAnimationFrame(() => this._animate()) : this.isAnimating = !1;
|
|
386
560
|
}
|
|
387
|
-
_drawStar(e,
|
|
388
|
-
let
|
|
389
|
-
const
|
|
390
|
-
this.ctx.beginPath(), this.ctx.moveTo(
|
|
391
|
-
for (let
|
|
392
|
-
let
|
|
393
|
-
this.ctx.lineTo(
|
|
561
|
+
_drawStar(t, e, s, i, a, n) {
|
|
562
|
+
let r = Math.PI / 2 * 3 + n;
|
|
563
|
+
const c = Math.PI / s;
|
|
564
|
+
this.ctx.beginPath(), this.ctx.moveTo(t, e - i);
|
|
565
|
+
for (let d = 0; d < s; d++) {
|
|
566
|
+
let l = t + Math.cos(r) * i, h = e + Math.sin(r) * i;
|
|
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;
|
|
394
568
|
}
|
|
395
|
-
this.ctx.lineTo(
|
|
569
|
+
this.ctx.lineTo(t, e - i), this.ctx.closePath(), this.ctx.fill();
|
|
396
570
|
}
|
|
397
571
|
/**
|
|
398
572
|
* Clear all particles
|
|
@@ -401,227 +575,150 @@ class f {
|
|
|
401
575
|
this.particles = [], this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
402
576
|
}
|
|
403
577
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
this.container = null, this.xpBar = null, this.levelDisplay = null, this.streakDisplay = null, this.init();
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Initialize UI overlay
|
|
411
|
-
*/
|
|
412
|
-
init() {
|
|
413
|
-
this.container = document.createElement("div"), this.container.id = "game-ui-overlay", this.container.innerHTML = `
|
|
414
|
-
<div class="game-ui-top-bar">
|
|
415
|
-
<div class="level-badge" id="level-badge">
|
|
416
|
-
<span class="level-label">LVL</span>
|
|
417
|
-
<span class="level-number" id="level-number">1</span>
|
|
418
|
-
</div>
|
|
419
|
-
|
|
420
|
-
<div class="xp-container">
|
|
421
|
-
<div class="xp-bar-bg">
|
|
422
|
-
<div class="xp-bar-fill" id="xp-bar-fill" style="width: 0%"></div>
|
|
423
|
-
</div>
|
|
424
|
-
<div class="xp-text" id="xp-text">0 /100 XP</div>
|
|
425
|
-
</div>
|
|
426
|
-
|
|
427
|
-
<div class="streak-badge" id="streak-badge">
|
|
428
|
-
<span class="streak-icon">🔥</span>
|
|
429
|
-
<span class="streak-number" id="streak-number">1</span>
|
|
430
|
-
</div>
|
|
431
|
-
</div>
|
|
432
|
-
`, 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");
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Update XP bar
|
|
436
|
-
*/
|
|
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(() => {
|
|
440
|
-
this.xpBar.style.animation = "xp-pulse 0.3s ease-out";
|
|
441
|
-
}, 10);
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Update level display
|
|
445
|
-
*/
|
|
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";
|
|
451
|
-
}, 10);
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Update streak display
|
|
455
|
-
*/
|
|
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%)");
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Show achievement unlocked popup
|
|
463
|
-
*/
|
|
464
|
-
showAchievement(e) {
|
|
465
|
-
const t = document.createElement("div");
|
|
466
|
-
t.className = "achievement-popup", t.innerHTML = `
|
|
467
|
-
<div class="achievement-shine"></div>
|
|
468
|
-
<div class="achievement-content">
|
|
469
|
-
<div class="achievement-icon">${e.icon}</div>
|
|
470
|
-
<div class="achievement-info">
|
|
471
|
-
<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>
|
|
475
|
-
</div>
|
|
476
|
-
</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);
|
|
480
|
-
}, 300), setTimeout(() => {
|
|
481
|
-
t.style.animation = "slideOut 0.3s ease-in forwards", setTimeout(() => t.remove(), 300);
|
|
482
|
-
}, 4e3);
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* Show level up screen
|
|
486
|
-
*/
|
|
487
|
-
showLevelUp(e, t) {
|
|
488
|
-
const i = document.createElement("div");
|
|
489
|
-
i.className = "level-up-overlay", i.innerHTML = `
|
|
490
|
-
<div class="level-up-content">
|
|
491
|
-
<div class="level-up-title">LEVEL UP!</div>
|
|
492
|
-
<div class="level-up-number">${t}</div>
|
|
493
|
-
<div class="level-up-subtitle">Amazing progress!</div>
|
|
494
|
-
</div>
|
|
495
|
-
`, document.body.appendChild(i), setTimeout(() => {
|
|
496
|
-
for (let s = 0; s < 5; s++)
|
|
497
|
-
setTimeout(() => {
|
|
498
|
-
const a = Math.random() * window.innerWidth, n = Math.random() * window.innerHeight * 0.6;
|
|
499
|
-
l.confetti(a, n, 30);
|
|
500
|
-
}, s * 200);
|
|
501
|
-
}, 300), setTimeout(() => {
|
|
502
|
-
i.style.opacity = "0", setTimeout(() => i.remove(), 500);
|
|
503
|
-
}, 2500);
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Show floating text (e.g., "+50 XP", "COMBO!")
|
|
507
|
-
*/
|
|
508
|
-
showFloatingText(e, t, i, s = "#ffd700", a = "24px") {
|
|
509
|
-
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);
|
|
578
|
+
class f {
|
|
579
|
+
constructor(t = {}) {
|
|
580
|
+
this.audioContext = null, this.storageKey = t.storageKey || "dopamineSoundsMuted", this.muted = localStorage.getItem(this.storageKey) === "true", this.assets = /* @__PURE__ */ new Map(), this.soundPack = t.soundPack || "synth", this.customSounds = t.customSounds || {}, Object.keys(this.customSounds).length > 0 && this.preloadSounds(this.customSounds);
|
|
511
581
|
}
|
|
512
582
|
/**
|
|
513
|
-
*
|
|
583
|
+
* Initialize Audio Context (must be called after user interaction)
|
|
514
584
|
*/
|
|
515
|
-
|
|
516
|
-
this.
|
|
585
|
+
initAudio() {
|
|
586
|
+
if (!this.audioContext) {
|
|
587
|
+
const t = window.AudioContext || window.webkitAudioContext;
|
|
588
|
+
this.audioContext = new t();
|
|
589
|
+
}
|
|
590
|
+
this.audioContext.state === "suspended" && this.audioContext.resume();
|
|
517
591
|
}
|
|
518
592
|
/**
|
|
519
|
-
*
|
|
593
|
+
* Toggle mute state
|
|
594
|
+
* @returns {boolean} New mute state
|
|
520
595
|
*/
|
|
521
|
-
|
|
522
|
-
this.
|
|
596
|
+
toggleMute() {
|
|
597
|
+
return this.muted = !this.muted, localStorage.setItem(this.storageKey, this.muted), this.muted;
|
|
523
598
|
}
|
|
524
599
|
/**
|
|
525
|
-
*
|
|
600
|
+
* Register a custom sound URL
|
|
601
|
+
* @param {string} key - Unique identifier (e.g., 'jump', 'win')
|
|
602
|
+
* @param {string} url - Path to audio file
|
|
526
603
|
*/
|
|
527
|
-
|
|
528
|
-
this.
|
|
604
|
+
registerSound(t, e) {
|
|
605
|
+
this.customSounds[t] = e, this.audioContext && this.loadSound(t, e);
|
|
529
606
|
}
|
|
530
607
|
/**
|
|
531
|
-
*
|
|
608
|
+
* Preload multiple sounds
|
|
609
|
+
* @param {Object} soundMap - { key: url }
|
|
532
610
|
*/
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
|
|
611
|
+
async preloadSounds(t) {
|
|
612
|
+
const e = Object.entries(t).map(([s, i]) => this.loadSound(s, i));
|
|
613
|
+
await Promise.allSettled(e);
|
|
536
614
|
}
|
|
537
615
|
/**
|
|
538
|
-
*
|
|
616
|
+
* Load and decode a sound file
|
|
539
617
|
*/
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
</div>
|
|
550
|
-
<button class="btn" onclick="location.reload()">Play Again</button>
|
|
551
|
-
<button class="btn" onclick="window.location.href='index.html'" style="margin-top: 10px; background: #543847;">Exit</button>
|
|
552
|
-
`, document.body.appendChild(t);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
const x = new g();
|
|
556
|
-
class w {
|
|
557
|
-
constructor() {
|
|
558
|
-
this.audioContext = null, this.muted = localStorage.getItem("dopamineSoundsMuted") === "true";
|
|
618
|
+
async loadSound(t, e) {
|
|
619
|
+
try {
|
|
620
|
+
if (!this.audioContext)
|
|
621
|
+
return;
|
|
622
|
+
const i = await (await fetch(e)).arrayBuffer(), a = await this.audioContext.decodeAudioData(i);
|
|
623
|
+
this.assets.set(t, a);
|
|
624
|
+
} catch (s) {
|
|
625
|
+
console.warn(`[DopamineJS] Failed to load sound '${t}':`, s);
|
|
626
|
+
}
|
|
559
627
|
}
|
|
560
628
|
/**
|
|
561
|
-
*
|
|
629
|
+
* Play a sound by key (custom) or fallback to synth
|
|
630
|
+
* @param {string} key - 'jump', 'score', 'success', etc.
|
|
562
631
|
*/
|
|
563
|
-
|
|
564
|
-
|
|
632
|
+
async play(t) {
|
|
633
|
+
if (this.muted) return;
|
|
634
|
+
if (this.initAudio(), this.assets.has(t)) {
|
|
635
|
+
this._playBuffer(this.assets.get(t));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (this.customSounds[t] && !this.assets.has(t) && (await this.loadSound(t, this.customSounds[t]), this.assets.has(t))) {
|
|
639
|
+
this._playBuffer(this.assets.get(t));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const e = {
|
|
643
|
+
jump: () => this.playJump(!0),
|
|
644
|
+
// true = force synth
|
|
645
|
+
score: () => this.playScore(!0),
|
|
646
|
+
success: () => this.playSuccess(!0),
|
|
647
|
+
click: () => this.playClick(!0),
|
|
648
|
+
error: () => this.playError(!0),
|
|
649
|
+
gameover: () => this.playGameOver(!0)
|
|
650
|
+
};
|
|
651
|
+
e[t] && e[t]();
|
|
565
652
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
*/
|
|
570
|
-
toggleMute() {
|
|
571
|
-
return this.muted = !this.muted, localStorage.setItem("dopamineSoundsMuted", this.muted), this.muted;
|
|
653
|
+
_playBuffer(t) {
|
|
654
|
+
const e = this.audioContext.createBufferSource();
|
|
655
|
+
e.buffer = t, e.connect(this.audioContext.destination), e.start(0);
|
|
572
656
|
}
|
|
573
657
|
/**
|
|
574
658
|
* Play a synthesized tone
|
|
575
|
-
* @param {number} frequency
|
|
576
|
-
* @param {number} duration
|
|
577
|
-
* @param {string} type - 'sine', 'square', 'sawtooth', 'triangle'
|
|
578
|
-
* @param {number} volume
|
|
579
659
|
*/
|
|
580
|
-
playTone(
|
|
660
|
+
playTone(t, e, s = "sine", i = 0.3) {
|
|
581
661
|
if (this.muted) return;
|
|
582
662
|
this.initAudio();
|
|
583
663
|
const a = this.audioContext.createOscillator(), n = this.audioContext.createGain();
|
|
584
|
-
a.connect(n), n.connect(this.audioContext.destination), a.frequency.value =
|
|
664
|
+
a.connect(n), n.connect(this.audioContext.destination), a.frequency.value = t, a.type = s, n.gain.setValueAtTime(i, this.audioContext.currentTime), n.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + e), a.start(this.audioContext.currentTime), a.stop(this.audioContext.currentTime + e);
|
|
585
665
|
}
|
|
586
|
-
// --- Presets ---
|
|
587
|
-
playJump() {
|
|
666
|
+
// --- Presets (Synth) ---
|
|
667
|
+
playJump(t = !1) {
|
|
668
|
+
if (!t && this.assets.has("jump")) return this.play("jump");
|
|
588
669
|
this.playTone(400, 0.1, "square", 0.2);
|
|
589
670
|
}
|
|
590
|
-
playScore() {
|
|
671
|
+
playScore(t = !1) {
|
|
672
|
+
if (!t && this.assets.has("score")) return this.play("score");
|
|
591
673
|
this.playTone(800, 0.15, "sine", 0.25), setTimeout(() => this.playTone(1e3, 0.15, "sine", 0.25), 50);
|
|
592
674
|
}
|
|
593
|
-
playGameOver() {
|
|
675
|
+
playGameOver(t = !1) {
|
|
676
|
+
if (!t && this.assets.has("gameover")) return this.play("gameover");
|
|
594
677
|
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);
|
|
595
678
|
}
|
|
596
|
-
playClick() {
|
|
679
|
+
playClick(t = !1) {
|
|
680
|
+
if (!t && this.assets.has("click")) return this.play("click");
|
|
597
681
|
this.playTone(600, 0.05, "sine", 0.1);
|
|
598
682
|
}
|
|
599
|
-
playSuccess() {
|
|
683
|
+
playSuccess(t = !1) {
|
|
684
|
+
if (!t && this.assets.has("success")) return this.play("success");
|
|
600
685
|
this.playTone(600, 0.1, "sine", 0.2), setTimeout(() => this.playTone(800, 0.2, "sine", 0.2), 100);
|
|
601
686
|
}
|
|
602
|
-
playError() {
|
|
687
|
+
playError(t = !1) {
|
|
688
|
+
if (!t && this.assets.has("error")) return this.play("error");
|
|
603
689
|
this.playTone(200, 0.2, "sawtooth", 0.3);
|
|
604
690
|
}
|
|
605
691
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
this.config = e;
|
|
692
|
+
class g {
|
|
693
|
+
constructor(t = {}) {
|
|
694
|
+
this.config = t, this.dataService = new u(t.data || {}), this.rewardSystem = new m(this.dataService, t.rewards || {}), this.soundManager = new f(t.sound || {}), this.particleSystem = new v(t.particles || {}), this.gameUI = new y(this.particleSystem), this._bindEvents();
|
|
610
695
|
}
|
|
611
696
|
async init() {
|
|
612
|
-
return await
|
|
613
|
-
rewardSystem:
|
|
614
|
-
gameUI:
|
|
615
|
-
particleSystem:
|
|
616
|
-
soundManager:
|
|
697
|
+
return await this.rewardSystem.init(), console.log("DopamineJS Initialized 🚀"), {
|
|
698
|
+
rewardSystem: this.rewardSystem,
|
|
699
|
+
gameUI: this.gameUI,
|
|
700
|
+
particleSystem: this.particleSystem,
|
|
701
|
+
soundManager: this.soundManager
|
|
617
702
|
};
|
|
618
703
|
}
|
|
704
|
+
_bindEvents() {
|
|
705
|
+
this.rewardSystem.on("xp_gained", (t) => {
|
|
706
|
+
const { total: e, needed: s } = this.rewardSystem.getXPForNextLevel();
|
|
707
|
+
this.gameUI.updateXP(this.rewardSystem.player.xp, s, e), t.xpGained > 0;
|
|
708
|
+
}), this.rewardSystem.on("level_up", (t) => {
|
|
709
|
+
this.gameUI.updateLevel(t.newLevel), this.gameUI.showLevelUp(t.oldLevel, t.newLevel);
|
|
710
|
+
}), this.rewardSystem.on("achievement_unlocked", (t) => {
|
|
711
|
+
this.gameUI.showAchievement(t);
|
|
712
|
+
}), this.rewardSystem.on("new_high_score", (t) => {
|
|
713
|
+
this.gameUI.showNotification(`New High Score: ${t.score}!`, "legendary"), this.soundManager.playSuccess();
|
|
714
|
+
});
|
|
715
|
+
}
|
|
619
716
|
}
|
|
620
717
|
export {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
718
|
+
u as DataService,
|
|
719
|
+
y as GameUI,
|
|
720
|
+
v as ParticleSystem,
|
|
721
|
+
m as RewardSystem,
|
|
722
|
+
f as SoundManager,
|
|
723
|
+
g as default
|
|
627
724
|
};
|