dopaminejs 1.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.
- package/README.md +84 -0
- package/dist/dopamine.mjs +627 -0
- package/dist/dopamine.umd.js +45 -0
- package/dist/style.css +1 -0
- package/package.json +50 -0
- package/src/audio/SoundManager.js +89 -0
- package/src/core/DataService.js +51 -0
- package/src/core/RewardSystem.js +362 -0
- package/src/effects/ParticleSystem.js +259 -0
- package/src/index.js +25 -0
- package/src/ui/GameUI.js +258 -0
- package/src/ui/dopamine.css +374 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# DopamineJS 🧠✨
|
|
2
|
+
|
|
3
|
+
A lightweight, modular JavaScript library for adding "juice", rewards, and satisfying feedback to your HTML5 games.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Reward System**: XP, Levels, Achievements, and Daily Streaks.
|
|
8
|
+
- **Game UI**: Beautiful, animated overlays for level-ups, notifications, and HUDs.
|
|
9
|
+
- **Particle Effects**: Confetti, sparkles, fire, and coin showers.
|
|
10
|
+
- **Sound Manager**: Synthesized retro sound effects (no external assets needed).
|
|
11
|
+
- **Data Persistence**: Automatic local storage management.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### Via npm (Coming Soon)
|
|
16
|
+
```bash
|
|
17
|
+
npm install dopaminejs
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Direct Include
|
|
21
|
+
Include the built script and CSS in your HTML:
|
|
22
|
+
```html
|
|
23
|
+
<link rel="stylesheet" href="dist/dopamine.css">
|
|
24
|
+
<script src="dist/dopamine.js"></script>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
import Dopamine from 'dopaminejs';
|
|
31
|
+
|
|
32
|
+
// Initialize
|
|
33
|
+
const game = new Dopamine();
|
|
34
|
+
await game.init();
|
|
35
|
+
|
|
36
|
+
// Access modules
|
|
37
|
+
const { rewardSystem, particleSystem, soundManager, gameUI } = game;
|
|
38
|
+
|
|
39
|
+
// 1. Award XP
|
|
40
|
+
await rewardSystem.addXP(100, 'Enemy Defeated');
|
|
41
|
+
|
|
42
|
+
// 2. Show Visual Feedback
|
|
43
|
+
particleSystem.confetti(window.innerWidth / 2, window.innerHeight / 2);
|
|
44
|
+
|
|
45
|
+
// 3. Play Sound
|
|
46
|
+
soundManager.playSuccess();
|
|
47
|
+
|
|
48
|
+
// 4. Show Notification
|
|
49
|
+
gameUI.showNotification('Level Up!', 'legendary');
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Modules
|
|
53
|
+
|
|
54
|
+
### RewardSystem
|
|
55
|
+
Manages player progression.
|
|
56
|
+
- `addXP(amount, reason)`
|
|
57
|
+
- `recordGame(gameId, result)`
|
|
58
|
+
- `unlockAchievement(id)`
|
|
59
|
+
|
|
60
|
+
### ParticleSystem
|
|
61
|
+
Visual effects on a dedicated canvas layer.
|
|
62
|
+
- `confetti(x, y)`
|
|
63
|
+
- `sparkle(x, y)`
|
|
64
|
+
- `fire(x, y)`
|
|
65
|
+
- `coinShower(x, y)`
|
|
66
|
+
|
|
67
|
+
### SoundManager
|
|
68
|
+
Synthesized audio effects.
|
|
69
|
+
- `playJump()`
|
|
70
|
+
- `playScore()`
|
|
71
|
+
- `playGameOver()`
|
|
72
|
+
- `toggleMute()`
|
|
73
|
+
|
|
74
|
+
### GameUI
|
|
75
|
+
Overlay system for feedback.
|
|
76
|
+
- `showNotification(msg, type)`
|
|
77
|
+
- `showLevelUp(oldLvl, newLvl)`
|
|
78
|
+
- `showSummary(data)`
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
MIT
|
|
82
|
+
|
|
83
|
+
## Support
|
|
84
|
+
[](https://ko-fi.com/T6T01CQT4R)
|
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
class v {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.storage = window.localStorage;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Save data to local storage
|
|
7
|
+
* @param {string} key
|
|
8
|
+
* @param {any} data
|
|
9
|
+
*/
|
|
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
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Load data from local storage
|
|
20
|
+
* @param {string} key
|
|
21
|
+
* @param {any} defaultValue
|
|
22
|
+
*/
|
|
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
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Clear specific data
|
|
33
|
+
* @param {string} key
|
|
34
|
+
*/
|
|
35
|
+
async clear(e) {
|
|
36
|
+
this.storage.removeItem(e);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const m = new v();
|
|
40
|
+
class u {
|
|
41
|
+
constructor() {
|
|
42
|
+
this.player = null, this.achievements = this._initAchievements(), this.listeners = [];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Initialize the reward system for current player
|
|
46
|
+
* @returns {Promise<Object>} Player data
|
|
47
|
+
*/
|
|
48
|
+
async init() {
|
|
49
|
+
return this.player = await m.load("player", this._getDefaultPlayer()), this._updateDailyStreak(), await this.save(), this.player;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get default player data structure
|
|
53
|
+
*/
|
|
54
|
+
_getDefaultPlayer() {
|
|
55
|
+
return {
|
|
56
|
+
name: "Player",
|
|
57
|
+
xp: 0,
|
|
58
|
+
level: 1,
|
|
59
|
+
totalGamesPlayed: 0,
|
|
60
|
+
createdAt: Date.now(),
|
|
61
|
+
lastPlayedAt: Date.now(),
|
|
62
|
+
streak: {
|
|
63
|
+
current: 1,
|
|
64
|
+
longest: 1,
|
|
65
|
+
lastPlayDate: this._getTodayDateString()
|
|
66
|
+
},
|
|
67
|
+
achievements: {},
|
|
68
|
+
// achievement_id: { unlockedAt: timestamp, seen: boolean }
|
|
69
|
+
stats: {
|
|
70
|
+
// Dynamic stats object, games will add their own keys
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Save player data
|
|
76
|
+
*/
|
|
77
|
+
async save() {
|
|
78
|
+
this.player && (this.player.lastPlayedAt = Date.now(), await m.save("player", this.player));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Add XP and handle level ups
|
|
82
|
+
* @param {number} amount - XP to add
|
|
83
|
+
* @param {string} reason - Why XP was awarded (for notifications)
|
|
84
|
+
* @returns {Object} { leveledUp: boolean, newLevel: number, xpGained: number }
|
|
85
|
+
*/
|
|
86
|
+
async addXP(e, t = "") {
|
|
87
|
+
const i = this.player.level;
|
|
88
|
+
this.player.xp += e;
|
|
89
|
+
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 };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Calculate level from XP (exponential curve)
|
|
94
|
+
*/
|
|
95
|
+
_calculateLevel(e) {
|
|
96
|
+
const t = Math.floor((1 + Math.sqrt(1 + 8 * e / 50)) / 2);
|
|
97
|
+
return Math.max(1, t);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get XP needed for next level
|
|
101
|
+
*/
|
|
102
|
+
getXPForNextLevel() {
|
|
103
|
+
const e = this.player.level + 1, t = 50 * e * (e - 1), i = 50 * this.player.level * (this.player.level - 1);
|
|
104
|
+
return {
|
|
105
|
+
total: t,
|
|
106
|
+
needed: t - this.player.xp,
|
|
107
|
+
progress: (this.player.xp - i) / (t - i)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Record game result and award XP
|
|
112
|
+
* @param {string} gameName - Unique ID for the game
|
|
113
|
+
* @param {Object} result - Game-specific result data
|
|
114
|
+
*/
|
|
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));
|
|
120
|
+
});
|
|
121
|
+
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();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Check and unlock achievements
|
|
126
|
+
*/
|
|
127
|
+
async checkAchievements(e, t) {
|
|
128
|
+
const i = [];
|
|
129
|
+
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));
|
|
131
|
+
return i;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Unlock an achievement
|
|
135
|
+
*/
|
|
136
|
+
async unlockAchievement(e) {
|
|
137
|
+
const t = this.achievements[e];
|
|
138
|
+
return t ? (this.player.achievements[e] = {
|
|
139
|
+
unlockedAt: Date.now(),
|
|
140
|
+
seen: !1
|
|
141
|
+
}, await this.addXP(t.xp, `Achievement: ${t.name}`), this._emit("achievement_unlocked", t), await this.save(), !0) : !1;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get all unlocked achievements
|
|
145
|
+
*/
|
|
146
|
+
getUnlockedAchievements() {
|
|
147
|
+
return Object.entries(this.player.achievements).map(([e, t]) => ({
|
|
148
|
+
...this.achievements[e],
|
|
149
|
+
id: e,
|
|
150
|
+
...t
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get achievements user hasn't seen the notification for yet
|
|
155
|
+
*/
|
|
156
|
+
getUnseenAchievements() {
|
|
157
|
+
return this.getUnlockedAchievements().filter((e) => !e.seen);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Mark achievements as seen
|
|
161
|
+
*/
|
|
162
|
+
async markAchievementsSeen(e) {
|
|
163
|
+
for (const t of e)
|
|
164
|
+
this.player.achievements[t] && (this.player.achievements[t].seen = !0);
|
|
165
|
+
await this.save();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Update daily streak
|
|
169
|
+
*/
|
|
170
|
+
_updateDailyStreak() {
|
|
171
|
+
const e = this._getTodayDateString(), t = this.player.streak.lastPlayDate;
|
|
172
|
+
if (t === e)
|
|
173
|
+
return;
|
|
174
|
+
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;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get streak multiplier for XP
|
|
179
|
+
*/
|
|
180
|
+
getStreakMultiplier() {
|
|
181
|
+
const e = this.player.streak.current;
|
|
182
|
+
return e >= 7 ? 2 : e >= 5 ? 1.5 : e >= 3 ? 1.25 : 1;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get today's date as YYYY-MM-DD string
|
|
186
|
+
*/
|
|
187
|
+
_getTodayDateString() {
|
|
188
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get yesterday's date as YYYY-MM-DD string
|
|
192
|
+
*/
|
|
193
|
+
_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));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Initialize achievement definitions
|
|
208
|
+
* Can be extended by games
|
|
209
|
+
*/
|
|
210
|
+
_initAchievements() {
|
|
211
|
+
return {
|
|
212
|
+
// Universal achievements
|
|
213
|
+
first_game: {
|
|
214
|
+
name: "First Steps",
|
|
215
|
+
description: "Play your first game",
|
|
216
|
+
icon: "🎮",
|
|
217
|
+
xp: 50,
|
|
218
|
+
check: (e) => e.totalGamesPlayed >= 1
|
|
219
|
+
},
|
|
220
|
+
level_5: {
|
|
221
|
+
name: "Rising Star",
|
|
222
|
+
description: "Reach level 5",
|
|
223
|
+
icon: "⭐",
|
|
224
|
+
xp: 100,
|
|
225
|
+
check: (e) => e.level >= 5
|
|
226
|
+
},
|
|
227
|
+
level_10: {
|
|
228
|
+
name: "Expert Player",
|
|
229
|
+
description: "Reach level 10",
|
|
230
|
+
icon: "🏆",
|
|
231
|
+
xp: 250,
|
|
232
|
+
check: (e) => e.level >= 10
|
|
233
|
+
},
|
|
234
|
+
streak_3: {
|
|
235
|
+
name: "Dedicated",
|
|
236
|
+
description: "Play 3 days in a row",
|
|
237
|
+
icon: "🔥",
|
|
238
|
+
xp: 100,
|
|
239
|
+
check: (e) => e.streak.current >= 3
|
|
240
|
+
},
|
|
241
|
+
streak_7: {
|
|
242
|
+
name: "Unstoppable",
|
|
243
|
+
description: "Play 7 days in a row",
|
|
244
|
+
icon: "💥",
|
|
245
|
+
xp: 300,
|
|
246
|
+
check: (e) => e.streak.current >= 7
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Register custom achievements
|
|
252
|
+
* @param {Object} achievements
|
|
253
|
+
*/
|
|
254
|
+
registerAchievements(e) {
|
|
255
|
+
this.achievements = { ...this.achievements, ...e };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
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;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
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)
|
|
290
|
+
*/
|
|
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
|
+
});
|
|
307
|
+
}
|
|
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
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Star burst (for "perfect" moments)
|
|
352
|
+
*/
|
|
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;
|
|
394
|
+
}
|
|
395
|
+
this.ctx.lineTo(e, t - s), this.ctx.closePath(), this.ctx.fill();
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Clear all particles
|
|
399
|
+
*/
|
|
400
|
+
clear() {
|
|
401
|
+
this.particles = [], this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
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();
|
|
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);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Show combo multiplier
|
|
514
|
+
*/
|
|
515
|
+
showCombo(e, t, i) {
|
|
516
|
+
this.showFloatingText(`${e}× COMBO!`, t, i, "#ff6b6b", "32px"), l.fire(t, i, 20);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Show "near miss"indicator
|
|
520
|
+
*/
|
|
521
|
+
showNearMiss(e, t) {
|
|
522
|
+
this.showFloatingText("CLOSE CALL!", e, t, "#45b7d1", "20px"), l.sparkle(e, t, 15, "#45b7d1");
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Show lucky moment (random multiplier)
|
|
526
|
+
*/
|
|
527
|
+
showLuckyMoment(e, t, i) {
|
|
528
|
+
this.showFloatingText(`LUCKY ${e}×!`, t, i, "#ffd700", "36px"), l.starBurst(t, i, 12), l.confetti(t, i, 25);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Show generic notification
|
|
532
|
+
*/
|
|
533
|
+
showNotification(e, t = "normal") {
|
|
534
|
+
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");
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Show Game Over Summary
|
|
539
|
+
*/
|
|
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";
|
|
543
|
+
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 = `
|
|
545
|
+
<h1>Game Over</h1>
|
|
546
|
+
<div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${e.score}</div>
|
|
547
|
+
<div style="width: 100%; margin-bottom: 20px;">
|
|
548
|
+
${i}
|
|
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";
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Initialize Audio Context (must be called after user interaction)
|
|
562
|
+
*/
|
|
563
|
+
initAudio() {
|
|
564
|
+
this.audioContext || (this.audioContext = new (window.AudioContext || window.webkitAudioContext)());
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Toggle mute state
|
|
568
|
+
* @returns {boolean} New mute state
|
|
569
|
+
*/
|
|
570
|
+
toggleMute() {
|
|
571
|
+
return this.muted = !this.muted, localStorage.setItem("dopamineSoundsMuted", this.muted), this.muted;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* 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
|
+
*/
|
|
580
|
+
playTone(e, t, i = "sine", s = 0.3) {
|
|
581
|
+
if (this.muted) return;
|
|
582
|
+
this.initAudio();
|
|
583
|
+
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);
|
|
585
|
+
}
|
|
586
|
+
// --- Presets ---
|
|
587
|
+
playJump() {
|
|
588
|
+
this.playTone(400, 0.1, "square", 0.2);
|
|
589
|
+
}
|
|
590
|
+
playScore() {
|
|
591
|
+
this.playTone(800, 0.15, "sine", 0.25), setTimeout(() => this.playTone(1e3, 0.15, "sine", 0.25), 50);
|
|
592
|
+
}
|
|
593
|
+
playGameOver() {
|
|
594
|
+
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
|
+
}
|
|
596
|
+
playClick() {
|
|
597
|
+
this.playTone(600, 0.05, "sine", 0.1);
|
|
598
|
+
}
|
|
599
|
+
playSuccess() {
|
|
600
|
+
this.playTone(600, 0.1, "sine", 0.2), setTimeout(() => this.playTone(800, 0.2, "sine", 0.2), 100);
|
|
601
|
+
}
|
|
602
|
+
playError() {
|
|
603
|
+
this.playTone(200, 0.2, "sawtooth", 0.3);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const b = new w();
|
|
607
|
+
class M {
|
|
608
|
+
constructor(e = {}) {
|
|
609
|
+
this.config = e;
|
|
610
|
+
}
|
|
611
|
+
async init() {
|
|
612
|
+
return await y.init(), console.log("DopamineJS Initialized 🚀"), {
|
|
613
|
+
rewardSystem: y,
|
|
614
|
+
gameUI: x,
|
|
615
|
+
particleSystem: l,
|
|
616
|
+
soundManager: b
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
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
|
|
627
|
+
};
|