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.
@@ -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(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();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 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;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(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)}}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 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.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
- "vite": "^5.0.0"
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/SimpleMobileGames.git"
48
+ "url": "https://github.com/BaryoDev/dopaminejs.git"
45
49
  },
46
50
  "bugs": {
47
- "url": "https://github.com/BaryoDev/SimpleMobileGames/issues"
51
+ "url": "https://github.com/BaryoDev/dopaminejs/issues"
48
52
  },
49
- "homepage": "https://github.com/BaryoDev/SimpleMobileGames#readme"
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.muted = localStorage.getItem('dopamineSoundsMuted') === 'true';
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('dopamineSoundsMuted', this.muted);
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();
@@ -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
  }
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();