dopaminejs 1.0.2 → 1.2.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(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=`
1
+ (function(o,p){typeof exports=="object"&&typeof module<"u"?p(exports):typeof define=="function"&&define.amd?define(["exports"],p):(o=typeof globalThis<"u"?globalThis:o||self,p(o.Dopamine={}))})(this,function(o){"use strict";class p{constructor(){this.listeners=new Map}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>this.off(e,t)}off(e,t){this.listeners.has(e)&&this.listeners.get(e).delete(t)}emit(e,t){this.listeners.has(e)&&this.listeners.get(e).forEach(s=>{try{s(t)}catch(i){console.error(`Error in listener for event '${e}':`,i)}})}clear(){this.listeners.clear()}}class m extends p{constructor(e,t={}){super(),this.dataService=e,this.player=null,this.achievements={...this._initAchievements(),...t.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(e,t=""){const s=this.player.level;this.player.xp+=e;const i=this._calculateLevel(this.player.xp),a=i>s;return a&&(this.player.level=i),await this.save(),this.emit("xp_gained",{amount:e,reason:t,leveledUp:a,newLevel:i}),a&&this.emit("level_up",{oldLevel:s,newLevel:i}),{leveledUp:a,newLevel:i,xpGained:e}}_calculateLevel(e){const t=Math.floor((1+Math.sqrt(1+8*e/50))/2);return Math.max(1,t)}getXPForNextLevel(){const e=this.player.level+1,t=50*e*(e-1),s=50*this.player.level*(this.player.level-1);return{total:t,needed:t-this.player.xp,progress:(this.player.xp-s)/(t-s)}}async recordGame(e,t){this.player.stats[e]||(this.player.stats[e]={totalPlays:0,highScore:0});const s=this.player.stats[e];s.totalPlays++,this.player.totalGamesPlayed++,t.score!==void 0&&t.score>s.highScore&&(s.highScore=t.score,this.emit("new_high_score",{gameName:e,score:t.score})),Object.keys(t).forEach(a=>{a!=="score"&&(s[a]=(s[a]||0)+(typeof t[a]=="number"?t[a]:0))});let i=10;t.score&&(i+=Math.floor(t.score/5)),i=Math.floor(i*this.getStreakMultiplier()),await this.addXP(i,`Played ${e}`),await this.checkAchievements(e,t),await this.save()}async checkAchievements(e,t){const s=[];for(const[i,a]of Object.entries(this.achievements))this.player.achievements[i]||a.check(this.player,e,t)&&(await this.unlockAchievement(i),s.push(a));return s}async unlockAchievement(e){const t=this.achievements[e];return t?(this.player.achievements[e]={unlockedAt:Date.now(),seen:!1},await this.addXP(t.xp,`Achievement: ${t.name}`),this.emit("achievement_unlocked",t),await this.save(),!0):!1}getUnlockedAchievements(){return Object.entries(this.player.achievements).map(([e,t])=>({...this.achievements[e],id:e,...t}))}getUnseenAchievements(){return this.getUnlockedAchievements().filter(e=>!e.seen)}async markAchievementsSeen(e){for(const t of e)this.player.achievements[t]&&(this.player.achievements[t].seen=!0);await this.save()}_updateDailyStreak(){const e=this._getTodayDateString(),t=this.player.streak.lastPlayDate;if(t===e)return;const s=this._getYesterdayDateString();t===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=e}getStreakMultiplier(){const e=this.player.streak.current;return e>=7?2:e>=5?1.5:e>=3?1.25:1}_getTodayDateString(){return new Date().toISOString().split("T")[0]}_getYesterdayDateString(){const e=new Date;return e.setDate(e.getDate()-1),e.toISOString().split("T")[0]}_initAchievements(){return{first_game:{name:"First Steps",description:"Play your first game",icon:"🎮",xp:50,check:e=>e.totalGamesPlayed>=1},level_5:{name:"Rising Star",description:"Reach level 5",icon:"⭐",xp:100,check:e=>e.level>=5},level_10:{name:"Expert Player",description:"Reach level 10",icon:"🏆",xp:250,check:e=>e.level>=10},streak_3:{name:"Dedicated",description:"Play 3 days in a row",icon:"🔥",xp:100,check:e=>e.streak.current>=3},streak_7:{name:"Unstoppable",description:"Play 7 days in a row",icon:"💥",xp:300,check:e=>e.streak.current>=7}}}}class y{constructor(e={}){this.storage=e.storage||window.localStorage,this.prefix=e.prefix||"dopamine_"}async save(e,t){try{const s=JSON.stringify(t);return this.storage.setItem(this.prefix+e,s),!0}catch(s){return console.error("Error saving data:",s),!1}}async load(e,t=null){try{const s=this.storage.getItem(this.prefix+e);return s?JSON.parse(s):t}catch(s){return console.error("Error loading data:",s),t}}async clear(e){this.storage.removeItem(this.prefix+e)}}class f{constructor(e){this.particleSystem=e,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,29 +17,29 @@
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(e,t,s){const i=e/s*100;this.xpBar.style.width=`${Math.min(100,i)}%`,this.xpText.textContent=`${e} / ${s} XP`,this.xpBar.style.animation="none",setTimeout(()=>{this.xpBar.style.animation="xp-pulse 0.3s ease-out"},10)}updateLevel(e){this.levelDisplay.textContent=e;const t=document.getElementById("level-badge");t.style.animation="none",setTimeout(()=>{t.style.animation="level-pulse 0.5s ease-out"},10)}updateStreak(e){this.streakDisplay.textContent=e;const t=document.getElementById("streak-badge");e>=7?t.style.background="linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)":e>=3&&(t.style.background="linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)")}showAchievement(e){const t=document.createElement("div");t.className="achievement-popup",t.innerHTML=`
21
21
  <div class="achievement-shine"></div>
22
22
  <div class="achievement-content">
23
- <div class="achievement-icon">${t.icon}</div>
23
+ <div class="achievement-icon">${e.icon}</div>
24
24
  <div class="achievement-info">
25
25
  <div class="achievement-title">Achievement Unlocked!</div>
26
- <div class="achievement-name">${t.name}</div>
27
- <div class="achievement-desc">${t.description}</div>
28
- <div class="achievement-xp">+${t.xp} XP</div>
26
+ <div class="achievement-name">${e.name}</div>
27
+ <div class="achievement-desc">${e.description}</div>
28
+ <div class="achievement-xp">+${e.xp} XP</div>
29
29
  </div>
30
30
  </div>
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=`
31
+ `,document.body.appendChild(t),setTimeout(()=>{const s=t.getBoundingClientRect();this.particleSystem.confetti(s.left+s.width/2,s.top+s.height/2,40)},300),setTimeout(()=>{t.style.animation="slideOut 0.3s ease-in forwards",setTimeout(()=>t.remove(),300)},4e3)}showLevelUp(e,t){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
- <div class="level-up-number">${e}</div>
34
+ <div class="level-up-number">${t}</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;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=`
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(e,t,s,i="#ffd700",a="24px"){const n=document.createElement("div");n.className="floating-text",n.textContent=e,n.style.left=t+"px",n.style.top=s+"px",n.style.color=i,n.style.fontSize=a,document.body.appendChild(n),setTimeout(()=>n.remove(),2e3)}showCombo(e,t,s){this.showFloatingText(`${e}× COMBO!`,t,s,"#ff6b6b","32px"),this.particleSystem.fire(t,s,20)}showNearMiss(e,t){this.showFloatingText("CLOSE CALL!",e,t,"#45b7d1","20px"),this.particleSystem.sparkle(e,t,15,"#45b7d1")}showLuckyMoment(e,t,s){this.showFloatingText(`LUCKY ${e}×!`,t,s,"#ffd700","36px"),this.particleSystem.starBurst(t,s,12),this.particleSystem.confetti(t,s,25)}showNotification(e,t="normal"){const s=window.innerWidth/2,i=window.innerHeight/3;t==="legendary"?this.showLuckyMoment(e,s,i):t==="rare"?this.showCombo(e,s,i):this.showFloatingText(e,s,i,"#fff","24px")}showSummary(e){const t=document.createElement("div");t.className="message-box",t.style.display="flex",t.style.position="absolute",t.style.top="50%",t.style.left="50%",t.style.transform="translate(-50%, -50%)",t.style.zIndex="1000";let s="";e.metrics&&(s=Object.entries(e.metrics).map(([i,a])=>`<div style="display:flex;justify-content:space-between;width:100%"><span>${i}:</span> <strong>${a}</strong></div>`).join("")),t.innerHTML=`
38
38
  <h1>Game Over</h1>
39
- <div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${t.score}</div>
39
+ <div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${e.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)}}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"}})});
45
+ `,document.body.appendChild(t)}}class v{constructor(e={}){this.container=e.container?typeof e.container=="string"?document.querySelector(e.container):e.container:document.body,this.canvasId=e.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.pointerEvents="none",this.canvas.style.zIndex=e.zIndex||"9999",this.container===document.body?(this.canvas.style.position="fixed",this.canvas.style.top="0",this.canvas.style.left="0"):(this.canvas.style.position="absolute",this.canvas.style.top="0",this.canvas.style.left="0",window.getComputedStyle(this.container).position==="static"&&(this.container.style.position="relative")),this.container.appendChild(this.canvas)),this.ctx=this.canvas.getContext("2d"),this.particles=[],this.pool=[],this.sprites=new Map,this.customEffects=new Map,this.isAnimating=!1,this._resize(),window.ResizeObserver&&this.container!==document.body?new ResizeObserver(()=>this._resize()).observe(this.container):window.addEventListener("resize",()=>this._resize())}_resize(){this.container===document.body?(this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight):(this.canvas.width=this.container.clientWidth,this.canvas.height=this.container.clientHeight)}registerSprite(e,t){const s=new Image;s.src=t,this.sprites.set(e,s)}registerEffect(e,t){this.customEffects.set(e,t)}play(e,t,s,...i){const a=this.customEffects.get(e);a?a(t,s,...i):console.warn(`[DopamineJS] Effect '${e}' not found.`)}emit(e){const{x:t,y:s,count:i=10,speed:a=5,gravity:n=.1,decay:l=.02,life:d=1,color:c="#ffd700",sprite:u=null,spread:M=Math.PI*2,angle:P=0,size:T=5}=e;for(let x=0;x<i;x++){const r=this._getParticle(),w=P+(Math.random()-.5)*M,S=Math.random()*a;r.x=t,r.y=s,r.vx=Math.cos(w)*S,r.vy=Math.sin(w)*S,r.life=d,r.decay=l*(.8+Math.random()*.4),r.gravity=n,r.color=Array.isArray(c)?c[Math.floor(Math.random()*c.length)]:c,r.size=T*(.8+Math.random()*.4),r.sprite=u,r.rotation=Math.random()*Math.PI*2,r.rotationSpeed=(Math.random()-.5)*.2,this.particles.push(r)}this._startAnimation()}_getParticle(){return this.pool.length>0?this.pool.pop():{}}_recycleParticle(e){this.pool.push(e)}confetti(e,t,s=50){const i=["#ff6b6b","#4ecdc4","#45b7d1","#f7dc6f","#c06c84","#6c5ce7"];this.emit({x:e,y:t,count:s,color:i,speed:10,gravity:.3,decay:.015,size:8,spread:Math.PI*2})}coinShower(e,t,s=10){this.emit({x:e,y:t,count:s,color:"#ffd700",speed:4,gravity:.4,decay:.012,size:12,angle:-Math.PI/2,spread:Math.PI/2})}sparkle(e,t,s=20,i="#ffd700"){this.emit({x:e,y:t,count:s,color:i,speed:3,gravity:-.05,decay:.03,size:3})}fire(e,t,s=15){const i=["#ff6b00","#ff8800","#ffaa00","#ff4400"];this.emit({x:e,y:t,count:s,color:i,speed:3,gravity:-.2,decay:.04,size:5,angle:-Math.PI/2,spread:Math.PI/4})}starBurst(e,t,s=8){for(let i=0;i<s;i++){const a=this._getParticle(),n=Math.PI*2/s*i,l=4;a.x=e,a.y=t,a.vx=Math.cos(n)*l,a.vy=Math.sin(n)*l,a.life=1,a.decay=.03,a.gravity=0,a.color="#ffffff",a.size=15,a.sprite=null,a.type="star",a.rotation=n,a.rotationSpeed=0,this.particles.push(a)}this._startAnimation()}_startAnimation(){this.isAnimating||(this.isAnimating=!0,this._animate())}_animate(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);for(let e=this.particles.length-1;e>=0;e--){const t=this.particles[e];if(t.vy+=t.gravity,t.x+=t.vx,t.y+=t.vy,t.life-=t.decay,t.rotationSpeed&&(t.rotation+=t.rotationSpeed),t.life<=0){this._recycleParticle(t),this.particles.splice(e,1);continue}if(this.ctx.save(),this.ctx.globalAlpha=t.life,this.ctx.translate(t.x,t.y),this.ctx.rotate(t.rotation),t.sprite&&this.sprites.has(t.sprite)){const s=this.sprites.get(t.sprite);s.complete&&this.ctx.drawImage(s,-t.size/2,-t.size/2,t.size,t.size)}else t.type==="star"?(this.ctx.fillStyle=t.color,this._drawStarPath(0,0,5,t.size,t.size/2),this.ctx.fill()):(this.ctx.fillStyle=t.color,this.ctx.beginPath(),this.ctx.arc(0,0,t.size,0,Math.PI*2),this.ctx.fill());this.ctx.restore()}this.particles.length>0?requestAnimationFrame(()=>this._animate()):this.isAnimating=!1}_drawStarPath(e,t,s,i,a){let n=Math.PI/2*3,l=e,d=t;const c=Math.PI/s;this.ctx.beginPath(),this.ctx.moveTo(e,t-i);for(let u=0;u<s;u++)l=e+Math.cos(n)*i,d=t+Math.sin(n)*i,this.ctx.lineTo(l,d),n+=c,l=e+Math.cos(n)*a,d=t+Math.sin(n)*a,this.ctx.lineTo(l,d),n+=c;this.ctx.lineTo(e,t-i),this.ctx.closePath()}clear(){this.particles.forEach(e=>this._recycleParticle(e)),this.particles=[],this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}}class g{constructor(e={}){this.audioContext=null,this.storageKey=e.storageKey||"dopamineSoundsMuted",this.muted=localStorage.getItem(this.storageKey)==="true",this.assets=new Map,this.soundPack=e.soundPack||"synth",this.customSounds=e.customSounds||{},Object.keys(this.customSounds).length>0&&this.preloadSounds(this.customSounds)}initAudio(){if(!this.audioContext){const e=window.AudioContext||window.webkitAudioContext;this.audioContext=new e}this.audioContext.state==="suspended"&&this.audioContext.resume()}toggleMute(){return this.muted=!this.muted,localStorage.setItem(this.storageKey,this.muted),this.muted}registerSound(e,t){this.customSounds[e]=t,this.audioContext&&this.loadSound(e,t)}async preloadSounds(e){const t=Object.entries(e).map(([s,i])=>this.loadSound(s,i));await Promise.allSettled(t)}async loadSound(e,t){try{if(!this.audioContext)return;const i=await(await fetch(t)).arrayBuffer(),a=await this.audioContext.decodeAudioData(i);this.assets.set(e,a)}catch(s){console.warn(`[DopamineJS] Failed to load sound '${e}':`,s)}}async play(e){if(this.muted)return;if(this.initAudio(),this.assets.has(e)){this._playBuffer(this.assets.get(e));return}if(this.customSounds[e]&&!this.assets.has(e)&&(await this.loadSound(e,this.customSounds[e]),this.assets.has(e))){this._playBuffer(this.assets.get(e));return}const t={jump:()=>this.playJump(!0),score:()=>this.playScore(!0),success:()=>this.playSuccess(!0),click:()=>this.playClick(!0),error:()=>this.playError(!0),gameover:()=>this.playGameOver(!0)};t[e]&&t[e]()}_playBuffer(e){const t=this.audioContext.createBufferSource();t.buffer=e,t.connect(this.audioContext.destination),t.start(0)}playTone(e,t,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=e,a.type=s,n.gain.setValueAtTime(i,this.audioContext.currentTime),n.gain.exponentialRampToValueAtTime(.01,this.audioContext.currentTime+t),a.start(this.audioContext.currentTime),a.stop(this.audioContext.currentTime+t)}playJump(e=!1){if(!e&&this.assets.has("jump"))return this.play("jump");this.playTone(400,.1,"square",.2)}playScore(e=!1){if(!e&&this.assets.has("score"))return this.play("score");this.playTone(800,.15,"sine",.25),setTimeout(()=>this.playTone(1e3,.15,"sine",.25),50)}playGameOver(e=!1){if(!e&&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(e=!1){if(!e&&this.assets.has("click"))return this.play("click");this.playTone(600,.05,"sine",.1)}playSuccess(e=!1){if(!e&&this.assets.has("success"))return this.play("success");this.playTone(600,.1,"sine",.2),setTimeout(()=>this.playTone(800,.2,"sine",.2),100)}playError(e=!1){if(!e&&this.assets.has("error"))return this.play("error");this.playTone(200,.2,"sawtooth",.3)}}class b{constructor(e={}){this.config=e,this.dataService=new y(e.data||{}),this.rewardSystem=new m(this.dataService,e.rewards||{}),this.soundManager=new g(e.sound||{}),this.particleSystem=new v(e.particles||{}),this.gameUI=new f(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",e=>{const{total:t,needed:s}=this.rewardSystem.getXPForNextLevel();this.gameUI.updateXP(this.rewardSystem.player.xp,s,t),e.xpGained>0}),this.rewardSystem.on("level_up",e=>{this.gameUI.updateLevel(e.newLevel),this.gameUI.showLevelUp(e.oldLevel,e.newLevel)}),this.rewardSystem.on("achievement_unlocked",e=>{this.gameUI.showAchievement(e)}),this.rewardSystem.on("new_high_score",e=>{this.gameUI.showNotification(`New High Score: ${e.score}!`,"legendary"),this.soundManager.playSuccess()})}}o.DataService=y,o.GameUI=f,o.ParticleSystem=v,o.RewardSystem=m,o.SoundManager=g,o.default=b,Object.defineProperties(o,{__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.2",
3
+ "version": "1.2.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
  ".": {
@@ -8,6 +8,16 @@ export class SoundManager {
8
8
  this.audioContext = null;
9
9
  this.storageKey = config.storageKey || 'dopamineSoundsMuted';
10
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
+ }
11
21
  }
12
22
 
13
23
  /**
@@ -15,7 +25,11 @@ export class SoundManager {
15
25
  */
16
26
  initAudio() {
17
27
  if (!this.audioContext) {
18
- 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();
19
33
  }
20
34
  }
21
35
 
@@ -29,12 +43,98 @@ export class SoundManager {
29
43
  return this.muted;
30
44
  }
31
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
+
32
136
  /**
33
137
  * Play a synthesized tone
34
- * @param {number} frequency
35
- * @param {number} duration
36
- * @param {string} type - 'sine', 'square', 'sawtooth', 'triangle'
37
- * @param {number} volume
38
138
  */
39
139
  playTone(frequency, duration, type = 'sine', volume = 0.3) {
40
140
  if (this.muted) return;
@@ -56,33 +156,39 @@ export class SoundManager {
56
156
  oscillator.stop(this.audioContext.currentTime + duration);
57
157
  }
58
158
 
59
- // --- Presets ---
159
+ // --- Presets (Synth) ---
60
160
 
61
- playJump() {
161
+ playJump(force = false) {
162
+ if (!force && this.assets.has('jump')) return this.play('jump');
62
163
  this.playTone(400, 0.1, 'square', 0.2);
63
164
  }
64
165
 
65
- playScore() {
166
+ playScore(force = false) {
167
+ if (!force && this.assets.has('score')) return this.play('score');
66
168
  this.playTone(800, 0.15, 'sine', 0.25);
67
169
  setTimeout(() => this.playTone(1000, 0.15, 'sine', 0.25), 50);
68
170
  }
69
171
 
70
- playGameOver() {
172
+ playGameOver(force = false) {
173
+ if (!force && this.assets.has('gameover')) return this.play('gameover');
71
174
  this.playTone(300, 0.2, 'sawtooth', 0.3);
72
175
  setTimeout(() => this.playTone(200, 0.2, 'sawtooth', 0.3), 100);
73
176
  setTimeout(() => this.playTone(150, 0.3, 'sawtooth', 0.3), 200);
74
177
  }
75
178
 
76
- playClick() {
179
+ playClick(force = false) {
180
+ if (!force && this.assets.has('click')) return this.play('click');
77
181
  this.playTone(600, 0.05, 'sine', 0.1);
78
182
  }
79
183
 
80
- playSuccess() {
184
+ playSuccess(force = false) {
185
+ if (!force && this.assets.has('success')) return this.play('success');
81
186
  this.playTone(600, 0.1, 'sine', 0.2);
82
187
  setTimeout(() => this.playTone(800, 0.2, 'sine', 0.2), 100);
83
188
  }
84
189
 
85
- playError() {
190
+ playError(force = false) {
191
+ if (!force && this.assets.has('error')) return this.play('error');
86
192
  this.playTone(200, 0.2, 'sawtooth', 0.3);
87
193
  }
88
194
  }