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
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Particle Effect System
|
|
3
|
+
* Creates satisfying visual feedback for achievements and rewards
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class ParticleSystem {
|
|
7
|
+
constructor(canvasId = 'dopamine-particle-canvas') {
|
|
8
|
+
this.canvasId = canvasId;
|
|
9
|
+
this.canvas = document.getElementById(canvasId);
|
|
10
|
+
|
|
11
|
+
if (!this.canvas) {
|
|
12
|
+
// Create overlay canvas if it doesn't exist
|
|
13
|
+
this.canvas = document.createElement('canvas');
|
|
14
|
+
this.canvas.id = canvasId;
|
|
15
|
+
this.canvas.style.position = 'fixed';
|
|
16
|
+
this.canvas.style.top = '0';
|
|
17
|
+
this.canvas.style.left = '0';
|
|
18
|
+
this.canvas.style.width = '100%';
|
|
19
|
+
this.canvas.style.height = '100%';
|
|
20
|
+
this.canvas.style.pointerEvents = 'none';
|
|
21
|
+
this.canvas.style.zIndex = '9999';
|
|
22
|
+
document.body.appendChild(this.canvas);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.ctx = this.canvas.getContext('2d');
|
|
26
|
+
this.particles = [];
|
|
27
|
+
this.isAnimating = false;
|
|
28
|
+
|
|
29
|
+
this._resize();
|
|
30
|
+
window.addEventListener('resize', () => this._resize());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_resize() {
|
|
34
|
+
this.canvas.width = window.innerWidth;
|
|
35
|
+
this.canvas.height = window.innerHeight;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Confetti explosion (for achievements, level ups)
|
|
40
|
+
*/
|
|
41
|
+
confetti(x, y, count = 50) {
|
|
42
|
+
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f7dc6f', '#c06c84', '#6c5ce7'];
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < count; i++) {
|
|
45
|
+
this.particles.push({
|
|
46
|
+
x, y,
|
|
47
|
+
vx: (Math.random() - 0.5) * 10,
|
|
48
|
+
vy: (Math.random() - 0.5) * 10 - 5,
|
|
49
|
+
rotation: Math.random() * Math.PI * 2,
|
|
50
|
+
rotationSpeed: (Math.random() - 0.5) * 0.2,
|
|
51
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
52
|
+
size: Math.random() * 8 + 4,
|
|
53
|
+
life: 1.0,
|
|
54
|
+
decay: 0.015 + Math.random() * 0.01,
|
|
55
|
+
gravity: 0.3,
|
|
56
|
+
type: 'confetti'
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this._startAnimation();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sparkle effect (for score milestones, combos)
|
|
65
|
+
*/
|
|
66
|
+
sparkle(x, y, count = 20, color = '#ffd700') {
|
|
67
|
+
for (let i = 0; i < count; i++) {
|
|
68
|
+
const angle = (Math.PI * 2 / count) * i;
|
|
69
|
+
const speed = 2 + Math.random() * 3;
|
|
70
|
+
|
|
71
|
+
this.particles.push({
|
|
72
|
+
x, y,
|
|
73
|
+
vx: Math.cos(angle) * speed,
|
|
74
|
+
vy: Math.sin(angle) * speed,
|
|
75
|
+
color,
|
|
76
|
+
size: Math.random() * 4 + 2,
|
|
77
|
+
life: 1.0,
|
|
78
|
+
decay: 0.02,
|
|
79
|
+
gravity: -0.1, // Slight upward drift
|
|
80
|
+
type: 'spark'
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this._startAnimation();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Coin shower (for collecting coins)
|
|
89
|
+
*/
|
|
90
|
+
coinShower(x, y, count = 10) {
|
|
91
|
+
for (let i = 0; i < count; i++) {
|
|
92
|
+
this.particles.push({
|
|
93
|
+
x: x + (Math.random() - 0.5) * 100,
|
|
94
|
+
y: y - Math.random() * 50,
|
|
95
|
+
vx: (Math.random() - 0.5) * 3,
|
|
96
|
+
vy: Math.random() * -2 - 3,
|
|
97
|
+
color: '#ffd700',
|
|
98
|
+
size: 12,
|
|
99
|
+
life: 1.0,
|
|
100
|
+
decay: 0.012,
|
|
101
|
+
gravity: 0.4,
|
|
102
|
+
type: 'coin'
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this._startAnimation();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fire particles (for streaks, combos)
|
|
111
|
+
*/
|
|
112
|
+
fire(x, y, count = 15) {
|
|
113
|
+
const colors = ['#ff6b00', '#ff8800', '#ffaa00', '#ff4400'];
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < count; i++) {
|
|
116
|
+
this.particles.push({
|
|
117
|
+
x: x + (Math.random() - 0.5) * 20,
|
|
118
|
+
y,
|
|
119
|
+
vx: (Math.random() - 0.5) * 2,
|
|
120
|
+
vy: -Math.random() * 3 - 2,
|
|
121
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
122
|
+
size: Math.random() * 6 + 3,
|
|
123
|
+
life: 1.0,
|
|
124
|
+
decay: 0.025,
|
|
125
|
+
gravity: -0.2, // Upward
|
|
126
|
+
type: 'fire'
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this._startAnimation();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Star burst (for "perfect" moments)
|
|
135
|
+
*/
|
|
136
|
+
starBurst(x, y, count = 8) {
|
|
137
|
+
for (let i = 0; i < count; i++) {
|
|
138
|
+
const angle = (Math.PI * 2 / count) * i;
|
|
139
|
+
const speed = 4;
|
|
140
|
+
|
|
141
|
+
this.particles.push({
|
|
142
|
+
x, y,
|
|
143
|
+
vx: Math.cos(angle) * speed,
|
|
144
|
+
vy: Math.sin(angle) * speed,
|
|
145
|
+
rotation: angle,
|
|
146
|
+
color: '#ffffff',
|
|
147
|
+
size: 15,
|
|
148
|
+
life: 1.0,
|
|
149
|
+
decay: 0.03,
|
|
150
|
+
gravity: 0,
|
|
151
|
+
type: 'star'
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this._startAnimation();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_startAnimation() {
|
|
159
|
+
if (!this.isAnimating) {
|
|
160
|
+
this.isAnimating = true;
|
|
161
|
+
this._animate();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_animate() {
|
|
166
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
167
|
+
|
|
168
|
+
// Update and draw particles
|
|
169
|
+
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|
170
|
+
const p = this.particles[i];
|
|
171
|
+
|
|
172
|
+
// Update physics
|
|
173
|
+
p.vy += p.gravity;
|
|
174
|
+
p.x += p.vx;
|
|
175
|
+
p.y += p.vy;
|
|
176
|
+
p.life -= p.decay;
|
|
177
|
+
|
|
178
|
+
if (p.rotationSpeed) {
|
|
179
|
+
p.rotation += p.rotationSpeed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Remove dead particles
|
|
183
|
+
if (p.life <= 0) {
|
|
184
|
+
this.particles.splice(i, 1);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Draw particle
|
|
189
|
+
this.ctx.save();
|
|
190
|
+
this.ctx.globalAlpha = p.life;
|
|
191
|
+
this.ctx.fillStyle = p.color;
|
|
192
|
+
|
|
193
|
+
if (p.type === 'confetti') {
|
|
194
|
+
this.ctx.translate(p.x, p.y);
|
|
195
|
+
this.ctx.rotate(p.rotation);
|
|
196
|
+
this.ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size / 2);
|
|
197
|
+
} else if (p.type === 'coin') {
|
|
198
|
+
this.ctx.beginPath();
|
|
199
|
+
this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
200
|
+
this.ctx.fill();
|
|
201
|
+
// Add shine
|
|
202
|
+
this.ctx.fillStyle = '#ffee88';
|
|
203
|
+
this.ctx.beginPath();
|
|
204
|
+
this.ctx.arc(p.x - p.size / 3, p.y - p.size / 3, p.size / 3, 0, Math.PI * 2);
|
|
205
|
+
this.ctx.fill();
|
|
206
|
+
} else if (p.type === 'star') {
|
|
207
|
+
this._drawStar(p.x, p.y, 5, p.size, p.size / 2, p.rotation);
|
|
208
|
+
} else {
|
|
209
|
+
// Default: circle
|
|
210
|
+
this.ctx.beginPath();
|
|
211
|
+
this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
212
|
+
this.ctx.fill();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.ctx.restore();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Continue animation if particles remain
|
|
219
|
+
if (this.particles.length > 0) {
|
|
220
|
+
requestAnimationFrame(() => this._animate());
|
|
221
|
+
} else {
|
|
222
|
+
this.isAnimating = false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_drawStar(cx, cy, spikes, outerRadius, innerRadius, rotation) {
|
|
227
|
+
let rot = Math.PI / 2 * 3 + rotation;
|
|
228
|
+
const step = Math.PI / spikes;
|
|
229
|
+
|
|
230
|
+
this.ctx.beginPath();
|
|
231
|
+
this.ctx.moveTo(cx, cy - outerRadius);
|
|
232
|
+
|
|
233
|
+
for (let i = 0; i < spikes; i++) {
|
|
234
|
+
let x = cx + Math.cos(rot) * outerRadius;
|
|
235
|
+
let y = cy + Math.sin(rot) * outerRadius;
|
|
236
|
+
this.ctx.lineTo(x, y);
|
|
237
|
+
rot += step;
|
|
238
|
+
|
|
239
|
+
x = cx + Math.cos(rot) * innerRadius;
|
|
240
|
+
y = cy + Math.sin(rot) * innerRadius;
|
|
241
|
+
this.ctx.lineTo(x, y);
|
|
242
|
+
rot += step;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.ctx.lineTo(cx, cy - outerRadius);
|
|
246
|
+
this.ctx.closePath();
|
|
247
|
+
this.ctx.fill();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Clear all particles
|
|
252
|
+
*/
|
|
253
|
+
clear() {
|
|
254
|
+
this.particles = [];
|
|
255
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const particleSystem = new ParticleSystem();
|
package/src/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { rewardSystem } from './core/RewardSystem.js';
|
|
2
|
+
import { dataService } from './core/DataService.js';
|
|
3
|
+
import { gameUI } from './ui/GameUI.js';
|
|
4
|
+
import { particleSystem } from './effects/ParticleSystem.js';
|
|
5
|
+
import { soundManager } from './audio/SoundManager.js';
|
|
6
|
+
import './ui/dopamine.css';
|
|
7
|
+
|
|
8
|
+
export { rewardSystem, dataService, gameUI, particleSystem, soundManager };
|
|
9
|
+
|
|
10
|
+
export default class Dopamine {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async init() {
|
|
16
|
+
await rewardSystem.init();
|
|
17
|
+
console.log('DopamineJS Initialized 🚀');
|
|
18
|
+
return {
|
|
19
|
+
rewardSystem,
|
|
20
|
+
gameUI,
|
|
21
|
+
particleSystem,
|
|
22
|
+
soundManager
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/ui/GameUI.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Components for Dopamine-Driven Games
|
|
3
|
+
* Reusable UI elements for XP bars, achievement popups, level-up screens, etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { particleSystem } from '../effects/ParticleSystem.js';
|
|
7
|
+
|
|
8
|
+
export class GameUI {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.container = null;
|
|
11
|
+
this.xpBar = null;
|
|
12
|
+
this.levelDisplay = null;
|
|
13
|
+
this.streakDisplay = null;
|
|
14
|
+
this.init();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize UI overlay
|
|
19
|
+
*/
|
|
20
|
+
init() {
|
|
21
|
+
// Create main container
|
|
22
|
+
this.container = document.createElement('div');
|
|
23
|
+
this.container.id = 'game-ui-overlay';
|
|
24
|
+
this.container.innerHTML = `
|
|
25
|
+
<div class="game-ui-top-bar">
|
|
26
|
+
<div class="level-badge" id="level-badge">
|
|
27
|
+
<span class="level-label">LVL</span>
|
|
28
|
+
<span class="level-number" id="level-number">1</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="xp-container">
|
|
32
|
+
<div class="xp-bar-bg">
|
|
33
|
+
<div class="xp-bar-fill" id="xp-bar-fill" style="width: 0%"></div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="xp-text" id="xp-text">0 /100 XP</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="streak-badge" id="streak-badge">
|
|
39
|
+
<span class="streak-icon">🔥</span>
|
|
40
|
+
<span class="streak-number" id="streak-number">1</span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
document.body.appendChild(this.container);
|
|
46
|
+
|
|
47
|
+
// Cache elements
|
|
48
|
+
this.xpBar = document.getElementById('xp-bar-fill');
|
|
49
|
+
this.xpText = document.getElementById('xp-text');
|
|
50
|
+
this.levelDisplay = document.getElementById('level-number');
|
|
51
|
+
this.streakDisplay = document.getElementById('streak-number');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update XP bar
|
|
56
|
+
*/
|
|
57
|
+
updateXP(current, needed, total) {
|
|
58
|
+
const progress = (current / total) * 100;
|
|
59
|
+
this.xpBar.style.width = `${Math.min(100, progress)}%`;
|
|
60
|
+
this.xpText.textContent = `${current} / ${total} XP`;
|
|
61
|
+
|
|
62
|
+
// Add pulse animation when gaining XP
|
|
63
|
+
this.xpBar.style.animation = 'none';
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
this.xpBar.style.animation = 'xp-pulse 0.3s ease-out';
|
|
66
|
+
}, 10);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Update level display
|
|
71
|
+
*/
|
|
72
|
+
updateLevel(level) {
|
|
73
|
+
this.levelDisplay.textContent = level;
|
|
74
|
+
|
|
75
|
+
// Pulse animation
|
|
76
|
+
const badge = document.getElementById('level-badge');
|
|
77
|
+
badge.style.animation = 'none';
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
badge.style.animation = 'level-pulse 0.5s ease-out';
|
|
80
|
+
}, 10);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Update streak display
|
|
85
|
+
*/
|
|
86
|
+
updateStreak(days) {
|
|
87
|
+
this.streakDisplay.textContent = days;
|
|
88
|
+
|
|
89
|
+
// Change color based on streak
|
|
90
|
+
const badge = document.getElementById('streak-badge');
|
|
91
|
+
if (days >= 7) {
|
|
92
|
+
badge.style.background = 'linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)';
|
|
93
|
+
} else if (days >= 3) {
|
|
94
|
+
badge.style.background = 'linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Show achievement unlocked popup
|
|
100
|
+
*/
|
|
101
|
+
showAchievement(achievement) {
|
|
102
|
+
const popup = document.createElement('div');
|
|
103
|
+
popup.className = 'achievement-popup';
|
|
104
|
+
popup.innerHTML = `
|
|
105
|
+
<div class="achievement-shine"></div>
|
|
106
|
+
<div class="achievement-content">
|
|
107
|
+
<div class="achievement-icon">${achievement.icon}</div>
|
|
108
|
+
<div class="achievement-info">
|
|
109
|
+
<div class="achievement-title">Achievement Unlocked!</div>
|
|
110
|
+
<div class="achievement-name">${achievement.name}</div>
|
|
111
|
+
<div class="achievement-desc">${achievement.description}</div>
|
|
112
|
+
<div class="achievement-xp">+${achievement.xp} XP</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
document.body.appendChild(popup);
|
|
118
|
+
|
|
119
|
+
// Trigger confetti at popup location
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
const rect = popup.getBoundingClientRect();
|
|
122
|
+
particleSystem.confetti(rect.left + rect.width / 2, rect.top + rect.height / 2, 40);
|
|
123
|
+
}, 300);
|
|
124
|
+
|
|
125
|
+
// Remove after animation
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
popup.style.animation = 'slideOut 0.3s ease-in forwards';
|
|
128
|
+
setTimeout(() => popup.remove(), 300);
|
|
129
|
+
}, 4000);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Show level up screen
|
|
134
|
+
*/
|
|
135
|
+
showLevelUp(oldLevel, newLevel) {
|
|
136
|
+
const overlay = document.createElement('div');
|
|
137
|
+
overlay.className = 'level-up-overlay';
|
|
138
|
+
overlay.innerHTML = `
|
|
139
|
+
<div class="level-up-content">
|
|
140
|
+
<div class="level-up-title">LEVEL UP!</div>
|
|
141
|
+
<div class="level-up-number">${newLevel}</div>
|
|
142
|
+
<div class="level-up-subtitle">Amazing progress!</div>
|
|
143
|
+
</div>
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
document.body.appendChild(overlay);
|
|
147
|
+
|
|
148
|
+
// Fireworks effect
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
for (let i = 0; i < 5; i++) {
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
const x = Math.random() * window.innerWidth;
|
|
153
|
+
const y = Math.random() * window.innerHeight * 0.6;
|
|
154
|
+
particleSystem.confetti(x, y, 30);
|
|
155
|
+
}, i * 200);
|
|
156
|
+
}
|
|
157
|
+
}, 300);
|
|
158
|
+
|
|
159
|
+
// Remove after 3 seconds
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
overlay.style.opacity = '0';
|
|
162
|
+
setTimeout(() => overlay.remove(), 500);
|
|
163
|
+
}, 2500);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Show floating text (e.g., "+50 XP", "COMBO!")
|
|
168
|
+
*/
|
|
169
|
+
showFloatingText(text, x, y, color = '#ffd700', size = '24px') {
|
|
170
|
+
const floater = document.createElement('div');
|
|
171
|
+
floater.className = 'floating-text';
|
|
172
|
+
floater.textContent = text;
|
|
173
|
+
floater.style.left = x + 'px';
|
|
174
|
+
floater.style.top = y + 'px';
|
|
175
|
+
floater.style.color = color;
|
|
176
|
+
floater.style.fontSize = size;
|
|
177
|
+
|
|
178
|
+
document.body.appendChild(floater);
|
|
179
|
+
|
|
180
|
+
setTimeout(() => floater.remove(), 2000);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Show combo multiplier
|
|
185
|
+
*/
|
|
186
|
+
showCombo(multiplier, x, y) {
|
|
187
|
+
this.showFloatingText(`${multiplier}× COMBO!`, x, y, '#ff6b6b', '32px');
|
|
188
|
+
particleSystem.fire(x, y, 20);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Show "near miss"indicator
|
|
193
|
+
*/
|
|
194
|
+
showNearMiss(x, y) {
|
|
195
|
+
this.showFloatingText('CLOSE CALL!', x, y, '#45b7d1', '20px');
|
|
196
|
+
particleSystem.sparkle(x, y, 15, '#45b7d1');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Show lucky moment (random multiplier)
|
|
201
|
+
*/
|
|
202
|
+
showLuckyMoment(multiplier, x, y) {
|
|
203
|
+
this.showFloatingText(`LUCKY ${multiplier}×!`, x, y, '#ffd700', '36px');
|
|
204
|
+
particleSystem.starBurst(x, y, 12);
|
|
205
|
+
particleSystem.confetti(x, y, 25);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Show generic notification
|
|
210
|
+
*/
|
|
211
|
+
showNotification(message, type = 'normal') {
|
|
212
|
+
const x = window.innerWidth / 2;
|
|
213
|
+
const y = window.innerHeight / 3;
|
|
214
|
+
|
|
215
|
+
if (type === 'legendary') {
|
|
216
|
+
this.showLuckyMoment(message, x, y);
|
|
217
|
+
} else if (type === 'rare') {
|
|
218
|
+
this.showCombo(message, x, y);
|
|
219
|
+
} else {
|
|
220
|
+
this.showFloatingText(message, x, y, '#fff', '24px');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Show Game Over Summary
|
|
226
|
+
*/
|
|
227
|
+
showSummary(data) {
|
|
228
|
+
const overlay = document.createElement('div');
|
|
229
|
+
overlay.className = 'message-box';
|
|
230
|
+
overlay.style.display = 'flex';
|
|
231
|
+
overlay.style.position = 'absolute';
|
|
232
|
+
overlay.style.top = '50%';
|
|
233
|
+
overlay.style.left = '50%';
|
|
234
|
+
overlay.style.transform = 'translate(-50%, -50%)';
|
|
235
|
+
overlay.style.zIndex = '1000';
|
|
236
|
+
|
|
237
|
+
let metricsHtml = '';
|
|
238
|
+
if (data.metrics) {
|
|
239
|
+
metricsHtml = Object.entries(data.metrics)
|
|
240
|
+
.map(([key, value]) => `<div style="display:flex;justify-content:space-between;width:100%"><span>${key}:</span> <strong>${value}</strong></div>`)
|
|
241
|
+
.join('');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
overlay.innerHTML = `
|
|
245
|
+
<h1>Game Over</h1>
|
|
246
|
+
<div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${data.score}</div>
|
|
247
|
+
<div style="width: 100%; margin-bottom: 20px;">
|
|
248
|
+
${metricsHtml}
|
|
249
|
+
</div>
|
|
250
|
+
<button class="btn" onclick="location.reload()">Play Again</button>
|
|
251
|
+
<button class="btn" onclick="window.location.href='index.html'" style="margin-top: 10px; background: #543847;">Exit</button>
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
document.body.appendChild(overlay);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const gameUI = new GameUI();
|