dopaminejs 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- (function(o,c){typeof exports=="object"&&typeof module<"u"?c(exports):typeof define=="function"&&define.amd?define(["exports"],c):(o=typeof globalThis<"u"?globalThis:o||self,c(o.Dopamine={}))})(this,function(o){"use strict";class c{constructor(){this.storage=window.localStorage}async save(t,e){try{const i=JSON.stringify(e);return this.storage.setItem(t,i),!0}catch(i){return console.error("Error saving data:",i),!1}}async load(t,e=null){try{const i=this.storage.getItem(t);return i?JSON.parse(i):e}catch(i){return console.error("Error loading data:",i),e}}async clear(t){this.storage.removeItem(t)}}const d=new c;class x{constructor(){this.player=null,this.achievements=this._initAchievements(),this.listeners=[]}async init(){return this.player=await d.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 d.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]}on(t,e){this.listeners.push({event:t,callback:e})}_emit(t,e){this.listeners.filter(i=>i.event===t).forEach(i=>i.callback(e))}_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}}}registerAchievements(t){this.achievements={...this.achievements,...t}}}const p=new x;class w{constructor(t="dopamine-particle-canvas"){this.canvasId=t,this.canvas=document.getElementById(t),this.canvas||(this.canvas=document.createElement("canvas"),this.canvas.id=t,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())}_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,l=2+Math.random()*3;this.particles.push({x:t,y:e,vx:Math.cos(n)*l,vy:Math.sin(n)*l,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 l=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 m=t+Math.cos(l)*s,y=e+Math.sin(l)*s;this.ctx.lineTo(m,y),l+=f,m=t+Math.cos(l)*a,y=e+Math.sin(l)*a,this.ctx.lineTo(m,y),l+=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)}}const r=new w;class b{constructor(){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=`
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(s=>{try{s(e)}catch(i){console.error(`Error in listener for event '${t}':`,i)}})}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 s=this.player.level;this.player.xp+=t;const i=this._calculateLevel(this.player.xp),a=i>s;return a&&(this.player.level=i),await this.save(),this.emit("xp_gained",{amount:t,reason:e,leveledUp:a,newLevel:i}),a&&this.emit("level_up",{oldLevel:s,newLevel:i}),{leveledUp:a,newLevel:i,xpGained:t}}_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),s=50*this.player.level*(this.player.level-1);return{total:e,needed:e-this.player.xp,progress:(this.player.xp-s)/(e-s)}}async recordGame(t,e){this.player.stats[t]||(this.player.stats[t]={totalPlays:0,highScore:0});const s=this.player.stats[t];s.totalPlays++,this.player.totalGamesPlayed++,e.score!==void 0&&e.score>s.highScore&&(s.highScore=e.score,this.emit("new_high_score",{gameName:t,score:e.score})),Object.keys(e).forEach(a=>{a!=="score"&&(s[a]=(s[a]||0)+(typeof e[a]=="number"?e[a]:0))});let i=10;e.score&&(i+=Math.floor(e.score/5)),i=Math.floor(i*this.getStreakMultiplier()),await this.addXP(i,`Played ${t}`),await this.checkAchievements(t,e),await this.save()}async checkAchievements(t,e){const s=[];for(const[i,a]of Object.entries(this.achievements))this.player.achievements[i]||a.check(this.player,t,e)&&(await this.unlockAchievement(i),s.push(a));return s}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 s=this._getYesterdayDateString();e===s?(this.player.streak.current++,this.player.streak.current>this.player.streak.longest&&(this.player.streak.longest=this.player.streak.current)):this.player.streak.current=1,this.player.streak.lastPlayDate=t}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 u{constructor(t={}){this.storage=t.storage||window.localStorage,this.prefix=t.prefix||"dopamine_"}async save(t,e){try{const s=JSON.stringify(e);return this.storage.setItem(this.prefix+t,s),!0}catch(s){return console.error("Error saving data:",s),!1}}async load(t,e=null){try{const s=this.storage.getItem(this.prefix+t);return s?JSON.parse(s):e}catch(s){return console.error("Error loading data:",s),e}}async clear(t){this.storage.removeItem(this.prefix+t)}}class m{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>
@@ -17,7 +17,7 @@
17
17
  <span class="streak-number" id="streak-number">1</span>
18
18
  </div>
19
19
  </div>
20
- `,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")}updateXP(t,e,i){const s=t/i*100;this.xpBar.style.width=`${Math.min(100,s)}%`,this.xpText.textContent=`${t} / ${i} XP`,this.xpBar.style.animation="none",setTimeout(()=>{this.xpBar.style.animation="xp-pulse 0.3s ease-out"},10)}updateLevel(t){this.levelDisplay.textContent=t;const e=document.getElementById("level-badge");e.style.animation="none",setTimeout(()=>{e.style.animation="level-pulse 0.5s ease-out"},10)}updateStreak(t){this.streakDisplay.textContent=t;const e=document.getElementById("streak-badge");t>=7?e.style.background="linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)":t>=3&&(e.style.background="linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)")}showAchievement(t){const e=document.createElement("div");e.className="achievement-popup",e.innerHTML=`
20
+ `,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")}updateXP(t,e,s){const i=t/s*100;this.xpBar.style.width=`${Math.min(100,i)}%`,this.xpText.textContent=`${t} / ${s} XP`,this.xpBar.style.animation="none",setTimeout(()=>{this.xpBar.style.animation="xp-pulse 0.3s ease-out"},10)}updateLevel(t){this.levelDisplay.textContent=t;const e=document.getElementById("level-badge");e.style.animation="none",setTimeout(()=>{e.style.animation="level-pulse 0.5s ease-out"},10)}updateStreak(t){this.streakDisplay.textContent=t;const e=document.getElementById("streak-badge");t>=7?e.style.background="linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)":t>=3&&(e.style.background="linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)")}showAchievement(t){const e=document.createElement("div");e.className="achievement-popup",e.innerHTML=`
21
21
  <div class="achievement-shine"></div>
22
22
  <div class="achievement-content">
23
23
  <div class="achievement-icon">${t.icon}</div>
@@ -28,18 +28,18 @@
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();r.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=`
31
+ `,document.body.appendChild(e),setTimeout(()=>{const s=e.getBoundingClientRect();this.particleSystem.confetti(s.left+s.width/2,s.top+s.height/2,40)},300),setTimeout(()=>{e.style.animation="slideOut 0.3s ease-in forwards",setTimeout(()=>e.remove(),300)},4e3)}showLevelUp(t,e){const s=document.createElement("div");s.className="level-up-overlay",s.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;r.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"),r.fire(e,i,20)}showNearMiss(t,e){this.showFloatingText("CLOSE CALL!",t,e,"#45b7d1","20px"),r.sparkle(t,e,15,"#45b7d1")}showLuckyMoment(t,e,i){this.showFloatingText(`LUCKY ${t}×!`,e,i,"#ffd700","36px"),r.starBurst(e,i,12),r.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=`
37
+ `,document.body.appendChild(s),setTimeout(()=>{for(let i=0;i<5;i++)setTimeout(()=>{const a=Math.random()*window.innerWidth,n=Math.random()*window.innerHeight*.6;this.particleSystem.confetti(a,n,30)},i*200)},300),setTimeout(()=>{s.style.opacity="0",setTimeout(()=>s.remove(),500)},2500)}showFloatingText(t,e,s,i="#ffd700",a="24px"){const n=document.createElement("div");n.className="floating-text",n.textContent=t,n.style.left=e+"px",n.style.top=s+"px",n.style.color=i,n.style.fontSize=a,document.body.appendChild(n),setTimeout(()=>n.remove(),2e3)}showCombo(t,e,s){this.showFloatingText(`${t}× COMBO!`,e,s,"#ff6b6b","32px"),this.particleSystem.fire(e,s,20)}showNearMiss(t,e){this.showFloatingText("CLOSE CALL!",t,e,"#45b7d1","20px"),this.particleSystem.sparkle(t,e,15,"#45b7d1")}showLuckyMoment(t,e,s){this.showFloatingText(`LUCKY ${t}×!`,e,s,"#ffd700","36px"),this.particleSystem.starBurst(e,s,12),this.particleSystem.confetti(e,s,25)}showNotification(t,e="normal"){const s=window.innerWidth/2,i=window.innerHeight/3;e==="legendary"?this.showLuckyMoment(t,s,i):e==="rare"?this.showCombo(t,s,i):this.showFloatingText(t,s,i,"#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 s="";t.metrics&&(s=Object.entries(t.metrics).map(([i,a])=>`<div style="display:flex;justify-content:space-between;width:100%"><span>${i}:</span> <strong>${a}</strong></div>`).join("")),e.innerHTML=`
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;">
41
- ${i}
41
+ ${s}
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)}}const v=new b;class M{constructor(){this.audioContext=null,this.muted=localStorage.getItem("dopamineSoundsMuted")==="true"}initAudio(){this.audioContext||(this.audioContext=new(window.AudioContext||window.webkitAudioContext))}toggleMute(){return this.muted=!this.muted,localStorage.setItem("dopamineSoundsMuted",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)}}const u=new M;class k{constructor(t={}){this.config=t}async init(){return await p.init(),console.log("DopamineJS Initialized 🚀"),{rewardSystem:p,gameUI:v,particleSystem:r,soundManager:u}}}o.dataService=d,o.default=k,o.gameUI=v,o.particleSystem=r,o.rewardSystem=p,o.soundManager=u,Object.defineProperties(o,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
45
+ `,document.body.appendChild(e)}}class y{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,s=50){const i=["#ff6b6b","#4ecdc4","#45b7d1","#f7dc6f","#c06c84","#6c5ce7"];for(let a=0;a<s;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:i[Math.floor(Math.random()*i.length)],size:Math.random()*8+4,life:1,decay:.015+Math.random()*.01,gravity:.3,type:"confetti"});this._startAnimation()}sparkle(t,e,s=20,i="#ffd700"){for(let a=0;a<s;a++){const n=Math.PI*2/s*a,o=2+Math.random()*3;this.particles.push({x:t,y:e,vx:Math.cos(n)*o,vy:Math.sin(n)*o,color:i,size:Math.random()*4+2,life:1,decay:.02,gravity:-.1,type:"spark"})}this._startAnimation()}coinShower(t,e,s=10){for(let i=0;i<s;i++)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,s=15){const i=["#ff6b00","#ff8800","#ffaa00","#ff4400"];for(let a=0;a<s;a++)this.particles.push({x:t+(Math.random()-.5)*20,y:e,vx:(Math.random()-.5)*2,vy:-Math.random()*3-2,color:i[Math.floor(Math.random()*i.length)],size:Math.random()*6+3,life:1,decay:.025,gravity:-.2,type:"fire"});this._startAnimation()}starBurst(t,e,s=8){for(let i=0;i<s;i++){const a=Math.PI*2/s*i,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,s,i,a,n){let o=Math.PI/2*3+n;const f=Math.PI/s;this.ctx.beginPath(),this.ctx.moveTo(t,e-i);for(let g=0;g<s;g++){let c=t+Math.cos(o)*i,d=e+Math.sin(o)*i;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-i),this.ctx.closePath(),this.ctx.fill()}clear(){this.particles=[],this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}}class v{constructor(t={}){this.audioContext=null,this.storageKey=t.storageKey||"dopamineSoundsMuted",this.muted=localStorage.getItem(this.storageKey)==="true",this.assets=new Map,this.soundPack=t.soundPack||"synth",this.customSounds=t.customSounds||{},Object.keys(this.customSounds).length>0&&this.preloadSounds(this.customSounds)}initAudio(){if(!this.audioContext){const t=window.AudioContext||window.webkitAudioContext;this.audioContext=new t}this.audioContext.state==="suspended"&&this.audioContext.resume()}toggleMute(){return this.muted=!this.muted,localStorage.setItem(this.storageKey,this.muted),this.muted}registerSound(t,e){this.customSounds[t]=e,this.audioContext&&this.loadSound(t,e)}async preloadSounds(t){const e=Object.entries(t).map(([s,i])=>this.loadSound(s,i));await Promise.allSettled(e)}async loadSound(t,e){try{if(!this.audioContext)return;const i=await(await fetch(e)).arrayBuffer(),a=await this.audioContext.decodeAudioData(i);this.assets.set(t,a)}catch(s){console.warn(`[DopamineJS] Failed to load sound '${t}':`,s)}}async play(t){if(this.muted)return;if(this.initAudio(),this.assets.has(t)){this._playBuffer(this.assets.get(t));return}if(this.customSounds[t]&&!this.assets.has(t)&&(await this.loadSound(t,this.customSounds[t]),this.assets.has(t))){this._playBuffer(this.assets.get(t));return}const e={jump:()=>this.playJump(!0),score:()=>this.playScore(!0),success:()=>this.playSuccess(!0),click:()=>this.playClick(!0),error:()=>this.playError(!0),gameover:()=>this.playGameOver(!0)};e[t]&&e[t]()}_playBuffer(t){const e=this.audioContext.createBufferSource();e.buffer=t,e.connect(this.audioContext.destination),e.start(0)}playTone(t,e,s="sine",i=.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=s,n.gain.setValueAtTime(i,this.audioContext.currentTime),n.gain.exponentialRampToValueAtTime(.01,this.audioContext.currentTime+e),a.start(this.audioContext.currentTime),a.stop(this.audioContext.currentTime+e)}playJump(t=!1){if(!t&&this.assets.has("jump"))return this.play("jump");this.playTone(400,.1,"square",.2)}playScore(t=!1){if(!t&&this.assets.has("score"))return this.play("score");this.playTone(800,.15,"sine",.25),setTimeout(()=>this.playTone(1e3,.15,"sine",.25),50)}playGameOver(t=!1){if(!t&&this.assets.has("gameover"))return this.play("gameover");this.playTone(300,.2,"sawtooth",.3),setTimeout(()=>this.playTone(200,.2,"sawtooth",.3),100),setTimeout(()=>this.playTone(150,.3,"sawtooth",.3),200)}playClick(t=!1){if(!t&&this.assets.has("click"))return this.play("click");this.playTone(600,.05,"sine",.1)}playSuccess(t=!1){if(!t&&this.assets.has("success"))return this.play("success");this.playTone(600,.1,"sine",.2),setTimeout(()=>this.playTone(800,.2,"sine",.2),100)}playError(t=!1){if(!t&&this.assets.has("error"))return this.play("error");this.playTone(200,.2,"sawtooth",.3)}}class x{constructor(t={}){this.config=t,this.dataService=new u(t.data||{}),this.rewardSystem=new p(this.dataService,t.rewards||{}),this.soundManager=new v(t.sound||{}),this.particleSystem=new y(t.particles||{}),this.gameUI=new m(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:s}=this.rewardSystem.getXPForNextLevel();this.gameUI.updateXP(this.rewardSystem.player.xp,s,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=u,r.GameUI=m,r.ParticleSystem=y,r.RewardSystem=p,r.SoundManager=v,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.0",
3
+ "version": "1.1.0",
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",
@@ -9,7 +9,9 @@
9
9
  "dist",
10
10
  "src",
11
11
  "README.md",
12
- "LICENSE"
12
+ "LICENSE",
13
+ "CHANGELOG.md",
14
+ "ARCHITECTURE.md"
13
15
  ],
14
16
  "exports": {
15
17
  ".": {
@@ -23,6 +25,8 @@
23
25
  "dev": "vite",
24
26
  "build": "vite build",
25
27
  "preview": "vite preview",
28
+ "test": "vitest",
29
+ "release": "node scripts/deploy.js",
26
30
  "prepublishOnly": "npm run build"
27
31
  },
28
32
  "keywords": [
@@ -37,14 +41,16 @@
37
41
  "author": "Gemini",
38
42
  "license": "MIT",
39
43
  "devDependencies": {
40
- "vite": "^5.0.0"
44
+ "jsdom": "^27.2.0",
45
+ "vite": "^5.0.0",
46
+ "vitest": "^4.0.15"
41
47
  },
42
48
  "repository": {
43
49
  "type": "git",
44
- "url": "https://github.com/BaryoDev/SimpleMobileGames.git"
50
+ "url": "https://github.com/BaryoDev/dopaminejs.git"
45
51
  },
46
52
  "bugs": {
47
- "url": "https://github.com/BaryoDev/SimpleMobileGames/issues"
53
+ "url": "https://github.com/BaryoDev/dopaminejs/issues"
48
54
  },
49
- "homepage": "https://github.com/BaryoDev/SimpleMobileGames#readme"
50
- }
55
+ "homepage": "https://github.com/BaryoDev/dopaminejs#readme"
56
+ }
@@ -4,9 +4,20 @@
4
4
  */
5
5
 
6
6
  export class SoundManager {
7
- constructor() {
7
+ constructor(config = {}) {
8
8
  this.audioContext = null;
9
- this.muted = localStorage.getItem('dopamineSoundsMuted') === 'true';
9
+ this.storageKey = config.storageKey || 'dopamineSoundsMuted';
10
+ this.muted = localStorage.getItem(this.storageKey) === 'true';
11
+
12
+ // Asset management
13
+ this.assets = new Map(); // key -> AudioBuffer
14
+ this.soundPack = config.soundPack || 'synth'; // 'synth' or custom
15
+ this.customSounds = config.customSounds || {}; // key -> url
16
+
17
+ // Preload custom sounds if provided
18
+ if (Object.keys(this.customSounds).length > 0) {
19
+ this.preloadSounds(this.customSounds);
20
+ }
10
21
  }
11
22
 
12
23
  /**
@@ -14,7 +25,11 @@ export class SoundManager {
14
25
  */
15
26
  initAudio() {
16
27
  if (!this.audioContext) {
17
- this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
28
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
29
+ this.audioContext = new AudioContext();
30
+ }
31
+ if (this.audioContext.state === 'suspended') {
32
+ this.audioContext.resume();
18
33
  }
19
34
  }
20
35
 
@@ -24,16 +39,102 @@ export class SoundManager {
24
39
  */
25
40
  toggleMute() {
26
41
  this.muted = !this.muted;
27
- localStorage.setItem('dopamineSoundsMuted', this.muted);
42
+ localStorage.setItem(this.storageKey, this.muted);
28
43
  return this.muted;
29
44
  }
30
45
 
46
+ /**
47
+ * Register a custom sound URL
48
+ * @param {string} key - Unique identifier (e.g., 'jump', 'win')
49
+ * @param {string} url - Path to audio file
50
+ */
51
+ registerSound(key, url) {
52
+ this.customSounds[key] = url;
53
+ // Auto-load if context exists, otherwise wait for init
54
+ if (this.audioContext) {
55
+ this.loadSound(key, url);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Preload multiple sounds
61
+ * @param {Object} soundMap - { key: url }
62
+ */
63
+ async preloadSounds(soundMap) {
64
+ const promises = Object.entries(soundMap).map(([key, url]) => this.loadSound(key, url));
65
+ await Promise.allSettled(promises);
66
+ }
67
+
68
+ /**
69
+ * Load and decode a sound file
70
+ */
71
+ async loadSound(key, url) {
72
+ try {
73
+ // We need audio context to decode
74
+ if (!this.audioContext) {
75
+ // If no context yet, we can't decode.
76
+ // In a real app, we might fetch buffer first, decode later.
77
+ // For simplicity, we'll wait for initAudio() or lazy load.
78
+ return;
79
+ }
80
+
81
+ const response = await fetch(url);
82
+ const arrayBuffer = await response.arrayBuffer();
83
+ const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
84
+ this.assets.set(key, audioBuffer);
85
+ } catch (error) {
86
+ console.warn(`[DopamineJS] Failed to load sound '${key}':`, error);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Play a sound by key (custom) or fallback to synth
92
+ * @param {string} key - 'jump', 'score', 'success', etc.
93
+ */
94
+ async play(key) {
95
+ if (this.muted) return;
96
+ this.initAudio();
97
+
98
+ // 1. Try custom asset first
99
+ if (this.assets.has(key)) {
100
+ this._playBuffer(this.assets.get(key));
101
+ return;
102
+ }
103
+
104
+ // 2. Try to load if registered but not loaded
105
+ if (this.customSounds[key] && !this.assets.has(key)) {
106
+ await this.loadSound(key, this.customSounds[key]);
107
+ if (this.assets.has(key)) {
108
+ this._playBuffer(this.assets.get(key));
109
+ return;
110
+ }
111
+ }
112
+
113
+ // 3. Fallback to synth based on key name
114
+ // Map common keys to synth methods
115
+ const synthMap = {
116
+ 'jump': () => this.playJump(true), // true = force synth
117
+ 'score': () => this.playScore(true),
118
+ 'success': () => this.playSuccess(true),
119
+ 'click': () => this.playClick(true),
120
+ 'error': () => this.playError(true),
121
+ 'gameover': () => this.playGameOver(true)
122
+ };
123
+
124
+ if (synthMap[key]) {
125
+ synthMap[key]();
126
+ }
127
+ }
128
+
129
+ _playBuffer(buffer) {
130
+ const source = this.audioContext.createBufferSource();
131
+ source.buffer = buffer;
132
+ source.connect(this.audioContext.destination);
133
+ source.start(0);
134
+ }
135
+
31
136
  /**
32
137
  * Play a synthesized tone
33
- * @param {number} frequency
34
- * @param {number} duration
35
- * @param {string} type - 'sine', 'square', 'sawtooth', 'triangle'
36
- * @param {number} volume
37
138
  */
38
139
  playTone(frequency, duration, type = 'sine', volume = 0.3) {
39
140
  if (this.muted) return;
@@ -55,35 +156,39 @@ export class SoundManager {
55
156
  oscillator.stop(this.audioContext.currentTime + duration);
56
157
  }
57
158
 
58
- // --- Presets ---
159
+ // --- Presets (Synth) ---
59
160
 
60
- playJump() {
161
+ playJump(force = false) {
162
+ if (!force && this.assets.has('jump')) return this.play('jump');
61
163
  this.playTone(400, 0.1, 'square', 0.2);
62
164
  }
63
165
 
64
- playScore() {
166
+ playScore(force = false) {
167
+ if (!force && this.assets.has('score')) return this.play('score');
65
168
  this.playTone(800, 0.15, 'sine', 0.25);
66
169
  setTimeout(() => this.playTone(1000, 0.15, 'sine', 0.25), 50);
67
170
  }
68
171
 
69
- playGameOver() {
172
+ playGameOver(force = false) {
173
+ if (!force && this.assets.has('gameover')) return this.play('gameover');
70
174
  this.playTone(300, 0.2, 'sawtooth', 0.3);
71
175
  setTimeout(() => this.playTone(200, 0.2, 'sawtooth', 0.3), 100);
72
176
  setTimeout(() => this.playTone(150, 0.3, 'sawtooth', 0.3), 200);
73
177
  }
74
178
 
75
- playClick() {
179
+ playClick(force = false) {
180
+ if (!force && this.assets.has('click')) return this.play('click');
76
181
  this.playTone(600, 0.05, 'sine', 0.1);
77
182
  }
78
183
 
79
- playSuccess() {
184
+ playSuccess(force = false) {
185
+ if (!force && this.assets.has('success')) return this.play('success');
80
186
  this.playTone(600, 0.1, 'sine', 0.2);
81
187
  setTimeout(() => this.playTone(800, 0.2, 'sine', 0.2), 100);
82
188
  }
83
189
 
84
- playError() {
190
+ playError(force = false) {
191
+ if (!force && this.assets.has('error')) return this.play('error');
85
192
  this.playTone(200, 0.2, 'sawtooth', 0.3);
86
193
  }
87
194
  }
88
-
89
- export const soundManager = new SoundManager();
@@ -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
+ }
@@ -3,13 +3,14 @@
3
3
  * Manages achievements, XP, levels, streaks, and rewards
4
4
  */
5
5
 
6
- import { dataService } from './DataService.js';
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._emit('xp_gained', { amount, reason, leveledUp, newLevel });
86
+ this.emit('xp_gained', { amount, reason, leveledUp, newLevel });
86
87
 
87
88
  if (leveledUp) {
88
- this._emit('level_up', { oldLevel, newLevel });
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._emit('new_high_score', { gameName, score: result.score });
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._emit('achievement_unlocked', achievement);
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(canvasId = 'dopamine-particle-canvas') {
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 { 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';
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 { rewardSystem, dataService, gameUI, particleSystem, soundManager };
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
  }