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.
@@ -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
+ }
@@ -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();