dopaminejs 1.0.0 → 1.0.2
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 +167 -60
- package/dist/dopamine.mjs +324 -291
- package/dist/dopamine.umd.js +4 -4
- package/package.json +10 -6
- package/src/audio/SoundManager.js +4 -5
- 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.umd.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(r,h){typeof exports=="object"&&typeof module<"u"?h(exports):typeof define=="function"&&define.amd?define(["exports"],h):(r=typeof globalThis<"u"?globalThis:r||self,h(r.Dopamine={}))})(this,function(r){"use strict";class h{constructor(){this.listeners=new Map}on(t,e){return this.listeners.has(t)||this.listeners.set(t,new Set),this.listeners.get(t).add(e),()=>this.off(t,e)}off(t,e){this.listeners.has(t)&&this.listeners.get(t).delete(e)}emit(t,e){this.listeners.has(t)&&this.listeners.get(t).forEach(i=>{try{i(e)}catch(s){console.error(`Error in listener for event '${t}':`,s)}})}clear(){this.listeners.clear()}}class p extends h{constructor(t,e={}){super(),this.dataService=t,this.player=null,this.achievements={...this._initAchievements(),...e.achievements||{}}}async init(){return this.player=await this.dataService.load("player",this._getDefaultPlayer()),this._updateDailyStreak(),await this.save(),this.player}_getDefaultPlayer(){return{name:"Player",xp:0,level:1,totalGamesPlayed:0,createdAt:Date.now(),lastPlayedAt:Date.now(),streak:{current:1,longest:1,lastPlayDate:this._getTodayDateString()},achievements:{},stats:{}}}async save(){this.player&&(this.player.lastPlayedAt=Date.now(),await this.dataService.save("player",this.player))}async addXP(t,e=""){const i=this.player.level;this.player.xp+=t;const s=this._calculateLevel(this.player.xp),a=s>i;return a&&(this.player.level=s),await this.save(),this.emit("xp_gained",{amount:t,reason:e,leveledUp:a,newLevel:s}),a&&this.emit("level_up",{oldLevel:i,newLevel:s}),{leveledUp:a,newLevel:s,xpGained:t}}_calculateLevel(t){const e=Math.floor((1+Math.sqrt(1+8*t/50))/2);return Math.max(1,e)}getXPForNextLevel(){const t=this.player.level+1,e=50*t*(t-1),i=50*this.player.level*(this.player.level-1);return{total:e,needed:e-this.player.xp,progress:(this.player.xp-i)/(e-i)}}async recordGame(t,e){this.player.stats[t]||(this.player.stats[t]={totalPlays:0,highScore:0});const i=this.player.stats[t];i.totalPlays++,this.player.totalGamesPlayed++,e.score!==void 0&&e.score>i.highScore&&(i.highScore=e.score,this.emit("new_high_score",{gameName:t,score:e.score})),Object.keys(e).forEach(a=>{a!=="score"&&(i[a]=(i[a]||0)+(typeof e[a]=="number"?e[a]:0))});let s=10;e.score&&(s+=Math.floor(e.score/5)),s=Math.floor(s*this.getStreakMultiplier()),await this.addXP(s,`Played ${t}`),await this.checkAchievements(t,e),await this.save()}async checkAchievements(t,e){const i=[];for(const[s,a]of Object.entries(this.achievements))this.player.achievements[s]||a.check(this.player,t,e)&&(await this.unlockAchievement(s),i.push(a));return i}async unlockAchievement(t){const e=this.achievements[t];return e?(this.player.achievements[t]={unlockedAt:Date.now(),seen:!1},await this.addXP(e.xp,`Achievement: ${e.name}`),this.emit("achievement_unlocked",e),await this.save(),!0):!1}getUnlockedAchievements(){return Object.entries(this.player.achievements).map(([t,e])=>({...this.achievements[t],id:t,...e}))}getUnseenAchievements(){return this.getUnlockedAchievements().filter(t=>!t.seen)}async markAchievementsSeen(t){for(const e of t)this.player.achievements[e]&&(this.player.achievements[e].seen=!0);await this.save()}_updateDailyStreak(){const t=this._getTodayDateString(),e=this.player.streak.lastPlayDate;if(e===t)return;const i=this._getYesterdayDateString();e===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=t}getStreakMultiplier(){const t=this.player.streak.current;return t>=7?2:t>=5?1.5:t>=3?1.25:1}_getTodayDateString(){return new Date().toISOString().split("T")[0]}_getYesterdayDateString(){const t=new Date;return t.setDate(t.getDate()-1),t.toISOString().split("T")[0]}_initAchievements(){return{first_game:{name:"First Steps",description:"Play your first game",icon:"🎮",xp:50,check:t=>t.totalGamesPlayed>=1},level_5:{name:"Rising Star",description:"Reach level 5",icon:"⭐",xp:100,check:t=>t.level>=5},level_10:{name:"Expert Player",description:"Reach level 10",icon:"🏆",xp:250,check:t=>t.level>=10},streak_3:{name:"Dedicated",description:"Play 3 days in a row",icon:"🔥",xp:100,check:t=>t.streak.current>=3},streak_7:{name:"Unstoppable",description:"Play 7 days in a row",icon:"💥",xp:300,check:t=>t.streak.current>=7}}}}class m{constructor(t={}){this.storage=t.storage||window.localStorage,this.prefix=t.prefix||"dopamine_"}async save(t,e){try{const i=JSON.stringify(e);return this.storage.setItem(this.prefix+t,i),!0}catch(i){return console.error("Error saving data:",i),!1}}async load(t,e=null){try{const i=this.storage.getItem(this.prefix+t);return i?JSON.parse(i):e}catch(i){return console.error("Error loading data:",i),e}}async clear(t){this.storage.removeItem(this.prefix+t)}}class y{constructor(t){this.particleSystem=t,this.container=null,this.xpBar=null,this.levelDisplay=null,this.streakDisplay=null,this.init()}init(){this.container=document.createElement("div"),this.container.id="game-ui-overlay",this.container.innerHTML=`
|
|
2
2
|
<div class="game-ui-top-bar">
|
|
3
3
|
<div class="level-badge" id="level-badge">
|
|
4
4
|
<span class="level-label">LVL</span>
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
<div class="achievement-xp">+${t.xp} XP</div>
|
|
29
29
|
</div>
|
|
30
30
|
</div>
|
|
31
|
-
`,document.body.appendChild(e),setTimeout(()=>{const i=e.getBoundingClientRect();
|
|
31
|
+
`,document.body.appendChild(e),setTimeout(()=>{const i=e.getBoundingClientRect();this.particleSystem.confetti(i.left+i.width/2,i.top+i.height/2,40)},300),setTimeout(()=>{e.style.animation="slideOut 0.3s ease-in forwards",setTimeout(()=>e.remove(),300)},4e3)}showLevelUp(t,e){const i=document.createElement("div");i.className="level-up-overlay",i.innerHTML=`
|
|
32
32
|
<div class="level-up-content">
|
|
33
33
|
<div class="level-up-title">LEVEL UP!</div>
|
|
34
34
|
<div class="level-up-number">${e}</div>
|
|
35
35
|
<div class="level-up-subtitle">Amazing progress!</div>
|
|
36
36
|
</div>
|
|
37
|
-
`,document.body.appendChild(i),setTimeout(()=>{for(let s=0;s<5;s++)setTimeout(()=>{const a=Math.random()*window.innerWidth,n=Math.random()*window.innerHeight*.6;
|
|
37
|
+
`,document.body.appendChild(i),setTimeout(()=>{for(let s=0;s<5;s++)setTimeout(()=>{const a=Math.random()*window.innerWidth,n=Math.random()*window.innerHeight*.6;this.particleSystem.confetti(a,n,30)},s*200)},300),setTimeout(()=>{i.style.opacity="0",setTimeout(()=>i.remove(),500)},2500)}showFloatingText(t,e,i,s="#ffd700",a="24px"){const n=document.createElement("div");n.className="floating-text",n.textContent=t,n.style.left=e+"px",n.style.top=i+"px",n.style.color=s,n.style.fontSize=a,document.body.appendChild(n),setTimeout(()=>n.remove(),2e3)}showCombo(t,e,i){this.showFloatingText(`${t}× COMBO!`,e,i,"#ff6b6b","32px"),this.particleSystem.fire(e,i,20)}showNearMiss(t,e){this.showFloatingText("CLOSE CALL!",t,e,"#45b7d1","20px"),this.particleSystem.sparkle(t,e,15,"#45b7d1")}showLuckyMoment(t,e,i){this.showFloatingText(`LUCKY ${t}×!`,e,i,"#ffd700","36px"),this.particleSystem.starBurst(e,i,12),this.particleSystem.confetti(e,i,25)}showNotification(t,e="normal"){const i=window.innerWidth/2,s=window.innerHeight/3;e==="legendary"?this.showLuckyMoment(t,i,s):e==="rare"?this.showCombo(t,i,s):this.showFloatingText(t,i,s,"#fff","24px")}showSummary(t){const e=document.createElement("div");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";let i="";t.metrics&&(i=Object.entries(t.metrics).map(([s,a])=>`<div style="display:flex;justify-content:space-between;width:100%"><span>${s}:</span> <strong>${a}</strong></div>`).join("")),e.innerHTML=`
|
|
38
38
|
<h1>Game Over</h1>
|
|
39
39
|
<div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${t.score}</div>
|
|
40
40
|
<div style="width: 100%; margin-bottom: 20px;">
|
|
@@ -42,4 +42,4 @@
|
|
|
42
42
|
</div>
|
|
43
43
|
<button class="btn" onclick="location.reload()">Play Again</button>
|
|
44
44
|
<button class="btn" onclick="window.location.href='index.html'" style="margin-top: 10px; background: #543847;">Exit</button>
|
|
45
|
-
`,document.body.appendChild(e)}}
|
|
45
|
+
`,document.body.appendChild(e)}}class v{constructor(t={}){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())}_resize(){this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight}confetti(t,e,i=50){const s=["#ff6b6b","#4ecdc4","#45b7d1","#f7dc6f","#c06c84","#6c5ce7"];for(let a=0;a<i;a++)this.particles.push({x:t,y:e,vx:(Math.random()-.5)*10,vy:(Math.random()-.5)*10-5,rotation:Math.random()*Math.PI*2,rotationSpeed:(Math.random()-.5)*.2,color:s[Math.floor(Math.random()*s.length)],size:Math.random()*8+4,life:1,decay:.015+Math.random()*.01,gravity:.3,type:"confetti"});this._startAnimation()}sparkle(t,e,i=20,s="#ffd700"){for(let a=0;a<i;a++){const n=Math.PI*2/i*a,o=2+Math.random()*3;this.particles.push({x:t,y:e,vx:Math.cos(n)*o,vy:Math.sin(n)*o,color:s,size:Math.random()*4+2,life:1,decay:.02,gravity:-.1,type:"spark"})}this._startAnimation()}coinShower(t,e,i=10){for(let s=0;s<i;s++)this.particles.push({x:t+(Math.random()-.5)*100,y:e-Math.random()*50,vx:(Math.random()-.5)*3,vy:Math.random()*-2-3,color:"#ffd700",size:12,life:1,decay:.012,gravity:.4,type:"coin"});this._startAnimation()}fire(t,e,i=15){const s=["#ff6b00","#ff8800","#ffaa00","#ff4400"];for(let a=0;a<i;a++)this.particles.push({x:t+(Math.random()-.5)*20,y:e,vx:(Math.random()-.5)*2,vy:-Math.random()*3-2,color:s[Math.floor(Math.random()*s.length)],size:Math.random()*6+3,life:1,decay:.025,gravity:-.2,type:"fire"});this._startAnimation()}starBurst(t,e,i=8){for(let s=0;s<i;s++){const a=Math.PI*2/i*s,n=4;this.particles.push({x:t,y:e,vx:Math.cos(a)*n,vy:Math.sin(a)*n,rotation:a,color:"#ffffff",size:15,life:1,decay:.03,gravity:0,type:"star"})}this._startAnimation()}_startAnimation(){this.isAnimating||(this.isAnimating=!0,this._animate())}_animate(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);for(let t=this.particles.length-1;t>=0;t--){const e=this.particles[t];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){this.particles.splice(t,1);continue}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()}this.particles.length>0?requestAnimationFrame(()=>this._animate()):this.isAnimating=!1}_drawStar(t,e,i,s,a,n){let o=Math.PI/2*3+n;const f=Math.PI/i;this.ctx.beginPath(),this.ctx.moveTo(t,e-s);for(let g=0;g<i;g++){let c=t+Math.cos(o)*s,d=e+Math.sin(o)*s;this.ctx.lineTo(c,d),o+=f,c=t+Math.cos(o)*a,d=e+Math.sin(o)*a,this.ctx.lineTo(c,d),o+=f}this.ctx.lineTo(t,e-s),this.ctx.closePath(),this.ctx.fill()}clear(){this.particles=[],this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}}class u{constructor(t={}){this.audioContext=null,this.storageKey=t.storageKey||"dopamineSoundsMuted",this.muted=localStorage.getItem(this.storageKey)==="true"}initAudio(){this.audioContext||(this.audioContext=new(window.AudioContext||window.webkitAudioContext))}toggleMute(){return this.muted=!this.muted,localStorage.setItem(this.storageKey,this.muted),this.muted}playTone(t,e,i="sine",s=.3){if(this.muted)return;this.initAudio();const a=this.audioContext.createOscillator(),n=this.audioContext.createGain();a.connect(n),n.connect(this.audioContext.destination),a.frequency.value=t,a.type=i,n.gain.setValueAtTime(s,this.audioContext.currentTime),n.gain.exponentialRampToValueAtTime(.01,this.audioContext.currentTime+e),a.start(this.audioContext.currentTime),a.stop(this.audioContext.currentTime+e)}playJump(){this.playTone(400,.1,"square",.2)}playScore(){this.playTone(800,.15,"sine",.25),setTimeout(()=>this.playTone(1e3,.15,"sine",.25),50)}playGameOver(){this.playTone(300,.2,"sawtooth",.3),setTimeout(()=>this.playTone(200,.2,"sawtooth",.3),100),setTimeout(()=>this.playTone(150,.3,"sawtooth",.3),200)}playClick(){this.playTone(600,.05,"sine",.1)}playSuccess(){this.playTone(600,.1,"sine",.2),setTimeout(()=>this.playTone(800,.2,"sine",.2),100)}playError(){this.playTone(200,.2,"sawtooth",.3)}}class x{constructor(t={}){this.config=t,this.dataService=new m(t.data||{}),this.rewardSystem=new p(this.dataService,t.rewards||{}),this.soundManager=new u(t.sound||{}),this.particleSystem=new v(t.particles||{}),this.gameUI=new y(this.particleSystem),this._bindEvents()}async init(){return await this.rewardSystem.init(),console.log("DopamineJS Initialized 🚀"),{rewardSystem:this.rewardSystem,gameUI:this.gameUI,particleSystem:this.particleSystem,soundManager:this.soundManager}}_bindEvents(){this.rewardSystem.on("xp_gained",t=>{const{total:e,needed:i}=this.rewardSystem.getXPForNextLevel();this.gameUI.updateXP(this.rewardSystem.player.xp,i,e),t.xpGained>0}),this.rewardSystem.on("level_up",t=>{this.gameUI.updateLevel(t.newLevel),this.gameUI.showLevelUp(t.oldLevel,t.newLevel)}),this.rewardSystem.on("achievement_unlocked",t=>{this.gameUI.showAchievement(t)}),this.rewardSystem.on("new_high_score",t=>{this.gameUI.showNotification(`New High Score: ${t.score}!`,"legendary"),this.soundManager.playSuccess()})}}r.DataService=m,r.GameUI=y,r.ParticleSystem=v,r.RewardSystem=p,r.SoundManager=u,r.default=x,Object.defineProperties(r,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dopaminejs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A lightweight library for adding juice, rewards, and feedback to HTML5 games.",
|
|
5
5
|
"main": "dist/dopamine.umd.js",
|
|
6
6
|
"module": "dist/dopamine.mjs",
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"dev": "vite",
|
|
24
24
|
"build": "vite build",
|
|
25
25
|
"preview": "vite preview",
|
|
26
|
+
"test": "vitest",
|
|
27
|
+
"release": "node scripts/deploy.js",
|
|
26
28
|
"prepublishOnly": "npm run build"
|
|
27
29
|
},
|
|
28
30
|
"keywords": [
|
|
@@ -37,14 +39,16 @@
|
|
|
37
39
|
"author": "Gemini",
|
|
38
40
|
"license": "MIT",
|
|
39
41
|
"devDependencies": {
|
|
40
|
-
"
|
|
42
|
+
"jsdom": "^27.2.0",
|
|
43
|
+
"vite": "^5.0.0",
|
|
44
|
+
"vitest": "^4.0.15"
|
|
41
45
|
},
|
|
42
46
|
"repository": {
|
|
43
47
|
"type": "git",
|
|
44
|
-
"url": "https://github.com/BaryoDev/
|
|
48
|
+
"url": "https://github.com/BaryoDev/dopaminejs.git"
|
|
45
49
|
},
|
|
46
50
|
"bugs": {
|
|
47
|
-
"url": "https://github.com/BaryoDev/
|
|
51
|
+
"url": "https://github.com/BaryoDev/dopaminejs/issues"
|
|
48
52
|
},
|
|
49
|
-
"homepage": "https://github.com/BaryoDev/
|
|
50
|
-
}
|
|
53
|
+
"homepage": "https://github.com/BaryoDev/dopaminejs#readme"
|
|
54
|
+
}
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export class SoundManager {
|
|
7
|
-
constructor() {
|
|
7
|
+
constructor(config = {}) {
|
|
8
8
|
this.audioContext = null;
|
|
9
|
-
this.
|
|
9
|
+
this.storageKey = config.storageKey || 'dopamineSoundsMuted';
|
|
10
|
+
this.muted = localStorage.getItem(this.storageKey) === 'true';
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -24,7 +25,7 @@ export class SoundManager {
|
|
|
24
25
|
*/
|
|
25
26
|
toggleMute() {
|
|
26
27
|
this.muted = !this.muted;
|
|
27
|
-
localStorage.setItem(
|
|
28
|
+
localStorage.setItem(this.storageKey, this.muted);
|
|
28
29
|
return this.muted;
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -85,5 +86,3 @@ export class SoundManager {
|
|
|
85
86
|
this.playTone(200, 0.2, 'sawtooth', 0.3);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
-
export const soundManager = new SoundManager();
|
package/src/core/DataService.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export class DataService {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.storage = window.localStorage;
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.storage = config.storage || window.localStorage;
|
|
9
|
+
this.prefix = config.prefix || 'dopamine_';
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -16,7 +17,7 @@ export class DataService {
|
|
|
16
17
|
async save(key, data) {
|
|
17
18
|
try {
|
|
18
19
|
const serialized = JSON.stringify(data);
|
|
19
|
-
this.storage.setItem(key, serialized);
|
|
20
|
+
this.storage.setItem(this.prefix + key, serialized);
|
|
20
21
|
return true;
|
|
21
22
|
} catch (e) {
|
|
22
23
|
console.error('Error saving data:', e);
|
|
@@ -31,7 +32,7 @@ export class DataService {
|
|
|
31
32
|
*/
|
|
32
33
|
async load(key, defaultValue = null) {
|
|
33
34
|
try {
|
|
34
|
-
const data = this.storage.getItem(key);
|
|
35
|
+
const data = this.storage.getItem(this.prefix + key);
|
|
35
36
|
return data ? JSON.parse(data) : defaultValue;
|
|
36
37
|
} catch (e) {
|
|
37
38
|
console.error('Error loading data:', e);
|
|
@@ -44,8 +45,6 @@ export class DataService {
|
|
|
44
45
|
* @param {string} key
|
|
45
46
|
*/
|
|
46
47
|
async clear(key) {
|
|
47
|
-
this.storage.removeItem(key);
|
|
48
|
+
this.storage.removeItem(this.prefix + key);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
export const dataService = new DataService();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Event Emitter implementation
|
|
3
|
+
*/
|
|
4
|
+
export class EventEmitter {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.listeners = new Map();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Subscribe to an event
|
|
11
|
+
* @param {string} event - Event name
|
|
12
|
+
* @param {Function} callback - Callback function
|
|
13
|
+
* @returns {Function} Unsubscribe function
|
|
14
|
+
*/
|
|
15
|
+
on(event, callback) {
|
|
16
|
+
if (!this.listeners.has(event)) {
|
|
17
|
+
this.listeners.set(event, new Set());
|
|
18
|
+
}
|
|
19
|
+
this.listeners.get(event).add(callback);
|
|
20
|
+
|
|
21
|
+
return () => this.off(event, callback);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Unsubscribe from an event
|
|
26
|
+
* @param {string} event - Event name
|
|
27
|
+
* @param {Function} callback - Callback function
|
|
28
|
+
*/
|
|
29
|
+
off(event, callback) {
|
|
30
|
+
if (this.listeners.has(event)) {
|
|
31
|
+
this.listeners.get(event).delete(callback);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Emit an event
|
|
37
|
+
* @param {string} event - Event name
|
|
38
|
+
* @param {*} data - Event data
|
|
39
|
+
*/
|
|
40
|
+
emit(event, data) {
|
|
41
|
+
if (this.listeners.has(event)) {
|
|
42
|
+
this.listeners.get(event).forEach(callback => {
|
|
43
|
+
try {
|
|
44
|
+
callback(data);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(`Error in listener for event '${event}':`, error);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Clear all listeners
|
|
54
|
+
*/
|
|
55
|
+
clear() {
|
|
56
|
+
this.listeners.clear();
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/core/RewardSystem.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
* Manages achievements, XP, levels, streaks, and rewards
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { EventEmitter } from './EventEmitter.js';
|
|
7
7
|
|
|
8
|
-
export class RewardSystem {
|
|
9
|
-
constructor() {
|
|
8
|
+
export class RewardSystem extends EventEmitter {
|
|
9
|
+
constructor(dataService, config = {}) {
|
|
10
|
+
super();
|
|
11
|
+
this.dataService = dataService;
|
|
10
12
|
this.player = null;
|
|
11
|
-
this.achievements = this._initAchievements();
|
|
12
|
-
this.listeners = [];
|
|
13
|
+
this.achievements = { ...this._initAchievements(), ...(config.achievements || {}) };
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -17,7 +18,7 @@ export class RewardSystem {
|
|
|
17
18
|
* @returns {Promise<Object>} Player data
|
|
18
19
|
*/
|
|
19
20
|
async init() {
|
|
20
|
-
this.player = await dataService.load('player', this._getDefaultPlayer());
|
|
21
|
+
this.player = await this.dataService.load('player', this._getDefaultPlayer());
|
|
21
22
|
|
|
22
23
|
// Check daily streak
|
|
23
24
|
this._updateDailyStreak();
|
|
@@ -57,7 +58,7 @@ export class RewardSystem {
|
|
|
57
58
|
async save() {
|
|
58
59
|
if (this.player) {
|
|
59
60
|
this.player.lastPlayedAt = Date.now();
|
|
60
|
-
await dataService.save('player', this.player);
|
|
61
|
+
await this.dataService.save('player', this.player);
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -82,10 +83,10 @@ export class RewardSystem {
|
|
|
82
83
|
await this.save();
|
|
83
84
|
|
|
84
85
|
// Notify listeners
|
|
85
|
-
this.
|
|
86
|
+
this.emit('xp_gained', { amount, reason, leveledUp, newLevel });
|
|
86
87
|
|
|
87
88
|
if (leveledUp) {
|
|
88
|
-
this.
|
|
89
|
+
this.emit('level_up', { oldLevel, newLevel });
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
return { leveledUp, newLevel, xpGained: amount };
|
|
@@ -135,7 +136,7 @@ export class RewardSystem {
|
|
|
135
136
|
// Update high score if applicable
|
|
136
137
|
if (result.score !== undefined && result.score > stats.highScore) {
|
|
137
138
|
stats.highScore = result.score;
|
|
138
|
-
this.
|
|
139
|
+
this.emit('new_high_score', { gameName, score: result.score });
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
// Merge other result data into stats
|
|
@@ -200,7 +201,7 @@ export class RewardSystem {
|
|
|
200
201
|
// Award XP
|
|
201
202
|
await this.addXP(achievement.xp, `Achievement: ${achievement.name}`);
|
|
202
203
|
|
|
203
|
-
this.
|
|
204
|
+
this.emit('achievement_unlocked', achievement);
|
|
204
205
|
await this.save();
|
|
205
206
|
|
|
206
207
|
return true;
|
|
@@ -292,19 +293,6 @@ export class RewardSystem {
|
|
|
292
293
|
return yesterday.toISOString().split('T')[0];
|
|
293
294
|
}
|
|
294
295
|
|
|
295
|
-
/**
|
|
296
|
-
* Event system for UI updates
|
|
297
|
-
*/
|
|
298
|
-
on(event, callback) {
|
|
299
|
-
this.listeners.push({ event, callback });
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
_emit(event, data) {
|
|
303
|
-
this.listeners
|
|
304
|
-
.filter(l => l.event === event)
|
|
305
|
-
.forEach(l => l.callback(data));
|
|
306
|
-
}
|
|
307
|
-
|
|
308
296
|
/**
|
|
309
297
|
* Initialize achievement definitions
|
|
310
298
|
* Can be extended by games
|
|
@@ -349,14 +337,4 @@ export class RewardSystem {
|
|
|
349
337
|
}
|
|
350
338
|
};
|
|
351
339
|
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Register custom achievements
|
|
355
|
-
* @param {Object} achievements
|
|
356
|
-
*/
|
|
357
|
-
registerAchievements(achievements) {
|
|
358
|
-
this.achievements = { ...this.achievements, ...achievements };
|
|
359
|
-
}
|
|
360
340
|
}
|
|
361
|
-
|
|
362
|
-
export const rewardSystem = new RewardSystem();
|
|
@@ -4,21 +4,21 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export class ParticleSystem {
|
|
7
|
-
constructor(
|
|
8
|
-
this.canvasId = canvasId;
|
|
9
|
-
this.canvas = document.getElementById(canvasId);
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.canvasId = config.canvasId || 'dopamine-particle-canvas';
|
|
9
|
+
this.canvas = document.getElementById(this.canvasId);
|
|
10
10
|
|
|
11
11
|
if (!this.canvas) {
|
|
12
12
|
// Create overlay canvas if it doesn't exist
|
|
13
13
|
this.canvas = document.createElement('canvas');
|
|
14
|
-
this.canvas.id = canvasId;
|
|
14
|
+
this.canvas.id = this.canvasId;
|
|
15
15
|
this.canvas.style.position = 'fixed';
|
|
16
16
|
this.canvas.style.top = '0';
|
|
17
17
|
this.canvas.style.left = '0';
|
|
18
18
|
this.canvas.style.width = '100%';
|
|
19
19
|
this.canvas.style.height = '100%';
|
|
20
20
|
this.canvas.style.pointerEvents = 'none';
|
|
21
|
-
this.canvas.style.zIndex = '9999';
|
|
21
|
+
this.canvas.style.zIndex = config.zIndex || '9999';
|
|
22
22
|
document.body.appendChild(this.canvas);
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -255,5 +255,3 @@ export class ParticleSystem {
|
|
|
255
255
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
-
|
|
259
|
-
export const particleSystem = new ParticleSystem();
|
package/src/index.js
CHANGED
|
@@ -1,25 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
6
|
import './ui/dopamine.css';
|
|
7
7
|
|
|
8
|
-
export {
|
|
8
|
+
export { RewardSystem, DataService, GameUI, ParticleSystem, SoundManager };
|
|
9
9
|
|
|
10
10
|
export default class Dopamine {
|
|
11
11
|
constructor(config = {}) {
|
|
12
12
|
this.config = config;
|
|
13
|
+
|
|
14
|
+
// Initialize subsystems with config
|
|
15
|
+
this.dataService = new DataService(config.data || {});
|
|
16
|
+
this.rewardSystem = new RewardSystem(this.dataService, config.rewards || {});
|
|
17
|
+
this.soundManager = new SoundManager(config.sound || {});
|
|
18
|
+
this.particleSystem = new ParticleSystem(config.particles || {});
|
|
19
|
+
this.gameUI = new GameUI(this.particleSystem);
|
|
20
|
+
|
|
21
|
+
// Bind UI to RewardSystem events
|
|
22
|
+
this._bindEvents();
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
async init() {
|
|
16
|
-
await rewardSystem.init();
|
|
26
|
+
await this.rewardSystem.init();
|
|
17
27
|
console.log('DopamineJS Initialized 🚀');
|
|
18
28
|
return {
|
|
19
|
-
rewardSystem,
|
|
20
|
-
gameUI,
|
|
21
|
-
particleSystem,
|
|
22
|
-
soundManager
|
|
29
|
+
rewardSystem: this.rewardSystem,
|
|
30
|
+
gameUI: this.gameUI,
|
|
31
|
+
particleSystem: this.particleSystem,
|
|
32
|
+
soundManager: this.soundManager
|
|
23
33
|
};
|
|
24
34
|
}
|
|
35
|
+
|
|
36
|
+
_bindEvents() {
|
|
37
|
+
this.rewardSystem.on('xp_gained', (data) => {
|
|
38
|
+
const { total, needed } = this.rewardSystem.getXPForNextLevel();
|
|
39
|
+
this.gameUI.updateXP(this.rewardSystem.player.xp, needed, total);
|
|
40
|
+
|
|
41
|
+
if (data.xpGained > 0) {
|
|
42
|
+
// Show floating text for XP gain
|
|
43
|
+
// We might need a way to know WHERE to show this, or just show it in a standard place
|
|
44
|
+
// For now, let's just update the bar
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.rewardSystem.on('level_up', (data) => {
|
|
49
|
+
this.gameUI.updateLevel(data.newLevel);
|
|
50
|
+
this.gameUI.showLevelUp(data.oldLevel, data.newLevel);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.rewardSystem.on('achievement_unlocked', (achievement) => {
|
|
54
|
+
this.gameUI.showAchievement(achievement);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this.rewardSystem.on('new_high_score', (data) => {
|
|
58
|
+
this.gameUI.showNotification(`New High Score: ${data.score}!`, 'legendary');
|
|
59
|
+
this.soundManager.playSuccess();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
25
62
|
}
|
package/src/ui/GameUI.js
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
* Reusable UI elements for XP bars, achievement popups, level-up screens, etc.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { particleSystem } from '../effects/ParticleSystem.js';
|
|
7
|
-
|
|
8
6
|
export class GameUI {
|
|
9
|
-
constructor() {
|
|
7
|
+
constructor(particleSystem) {
|
|
8
|
+
this.particleSystem = particleSystem;
|
|
10
9
|
this.container = null;
|
|
11
10
|
this.xpBar = null;
|
|
12
11
|
this.levelDisplay = null;
|
|
@@ -119,7 +118,7 @@ export class GameUI {
|
|
|
119
118
|
// Trigger confetti at popup location
|
|
120
119
|
setTimeout(() => {
|
|
121
120
|
const rect = popup.getBoundingClientRect();
|
|
122
|
-
particleSystem.confetti(rect.left + rect.width / 2, rect.top + rect.height / 2, 40);
|
|
121
|
+
this.particleSystem.confetti(rect.left + rect.width / 2, rect.top + rect.height / 2, 40);
|
|
123
122
|
}, 300);
|
|
124
123
|
|
|
125
124
|
// Remove after animation
|
|
@@ -151,7 +150,7 @@ export class GameUI {
|
|
|
151
150
|
setTimeout(() => {
|
|
152
151
|
const x = Math.random() * window.innerWidth;
|
|
153
152
|
const y = Math.random() * window.innerHeight * 0.6;
|
|
154
|
-
particleSystem.confetti(x, y, 30);
|
|
153
|
+
this.particleSystem.confetti(x, y, 30);
|
|
155
154
|
}, i * 200);
|
|
156
155
|
}
|
|
157
156
|
}, 300);
|
|
@@ -185,7 +184,7 @@ export class GameUI {
|
|
|
185
184
|
*/
|
|
186
185
|
showCombo(multiplier, x, y) {
|
|
187
186
|
this.showFloatingText(`${multiplier}× COMBO!`, x, y, '#ff6b6b', '32px');
|
|
188
|
-
particleSystem.fire(x, y, 20);
|
|
187
|
+
this.particleSystem.fire(x, y, 20);
|
|
189
188
|
}
|
|
190
189
|
|
|
191
190
|
/**
|
|
@@ -193,7 +192,7 @@ export class GameUI {
|
|
|
193
192
|
*/
|
|
194
193
|
showNearMiss(x, y) {
|
|
195
194
|
this.showFloatingText('CLOSE CALL!', x, y, '#45b7d1', '20px');
|
|
196
|
-
particleSystem.sparkle(x, y, 15, '#45b7d1');
|
|
195
|
+
this.particleSystem.sparkle(x, y, 15, '#45b7d1');
|
|
197
196
|
}
|
|
198
197
|
|
|
199
198
|
/**
|
|
@@ -201,8 +200,8 @@ export class GameUI {
|
|
|
201
200
|
*/
|
|
202
201
|
showLuckyMoment(multiplier, x, y) {
|
|
203
202
|
this.showFloatingText(`LUCKY ${multiplier}×!`, x, y, '#ffd700', '36px');
|
|
204
|
-
particleSystem.starBurst(x, y, 12);
|
|
205
|
-
particleSystem.confetti(x, y, 25);
|
|
203
|
+
this.particleSystem.starBurst(x, y, 12);
|
|
204
|
+
this.particleSystem.confetti(x, y, 25);
|
|
206
205
|
}
|
|
207
206
|
|
|
208
207
|
/**
|
|
@@ -254,5 +253,3 @@ export class GameUI {
|
|
|
254
253
|
document.body.appendChild(overlay);
|
|
255
254
|
}
|
|
256
255
|
}
|
|
257
|
-
|
|
258
|
-
export const gameUI = new GameUI();
|