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.
- package/ARCHITECTURE.md +71 -0
- package/CHANGELOG.md +27 -0
- package/README.md +189 -60
- package/dist/dopamine.mjs +414 -317
- package/dist/dopamine.umd.js +6 -6
- package/package.json +13 -7
- package/src/audio/SoundManager.js +122 -17
- package/src/core/DataService.js +6 -7
- package/src/core/EventEmitter.js +58 -0
- package/src/core/RewardSystem.js +12 -34
- package/src/effects/ParticleSystem.js +5 -7
- package/src/index.js +48 -11
- package/src/ui/GameUI.js +8 -11
package/dist/dopamine.umd.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(r,h){typeof exports=="object"&&typeof module<"u"?h(exports):typeof define=="function"&&define.amd?define(["exports"],h):(r=typeof globalThis<"u"?globalThis:r||self,h(r.Dopamine={}))})(this,function(r){"use strict";class h{constructor(){this.listeners=new Map}on(t,e){return this.listeners.has(t)||this.listeners.set(t,new Set),this.listeners.get(t).add(e),()=>this.off(t,e)}off(t,e){this.listeners.has(t)&&this.listeners.get(t).delete(e)}emit(t,e){this.listeners.has(t)&&this.listeners.get(t).forEach(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,
|
|
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
|
|
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(
|
|
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
|
-
${
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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/
|
|
50
|
+
"url": "https://github.com/BaryoDev/dopaminejs.git"
|
|
45
51
|
},
|
|
46
52
|
"bugs": {
|
|
47
|
-
"url": "https://github.com/BaryoDev/
|
|
53
|
+
"url": "https://github.com/BaryoDev/dopaminejs/issues"
|
|
48
54
|
},
|
|
49
|
-
"homepage": "https://github.com/BaryoDev/
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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();
|
package/src/core/DataService.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export class DataService {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.storage = window.localStorage;
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.storage = config.storage || window.localStorage;
|
|
9
|
+
this.prefix = config.prefix || 'dopamine_';
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -16,7 +17,7 @@ export class DataService {
|
|
|
16
17
|
async save(key, data) {
|
|
17
18
|
try {
|
|
18
19
|
const serialized = JSON.stringify(data);
|
|
19
|
-
this.storage.setItem(key, serialized);
|
|
20
|
+
this.storage.setItem(this.prefix + key, serialized);
|
|
20
21
|
return true;
|
|
21
22
|
} catch (e) {
|
|
22
23
|
console.error('Error saving data:', e);
|
|
@@ -31,7 +32,7 @@ export class DataService {
|
|
|
31
32
|
*/
|
|
32
33
|
async load(key, defaultValue = null) {
|
|
33
34
|
try {
|
|
34
|
-
const data = this.storage.getItem(key);
|
|
35
|
+
const data = this.storage.getItem(this.prefix + key);
|
|
35
36
|
return data ? JSON.parse(data) : defaultValue;
|
|
36
37
|
} catch (e) {
|
|
37
38
|
console.error('Error loading data:', e);
|
|
@@ -44,8 +45,6 @@ export class DataService {
|
|
|
44
45
|
* @param {string} key
|
|
45
46
|
*/
|
|
46
47
|
async clear(key) {
|
|
47
|
-
this.storage.removeItem(key);
|
|
48
|
+
this.storage.removeItem(this.prefix + key);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
export const dataService = new DataService();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Event Emitter implementation
|
|
3
|
+
*/
|
|
4
|
+
export class EventEmitter {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.listeners = new Map();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Subscribe to an event
|
|
11
|
+
* @param {string} event - Event name
|
|
12
|
+
* @param {Function} callback - Callback function
|
|
13
|
+
* @returns {Function} Unsubscribe function
|
|
14
|
+
*/
|
|
15
|
+
on(event, callback) {
|
|
16
|
+
if (!this.listeners.has(event)) {
|
|
17
|
+
this.listeners.set(event, new Set());
|
|
18
|
+
}
|
|
19
|
+
this.listeners.get(event).add(callback);
|
|
20
|
+
|
|
21
|
+
return () => this.off(event, callback);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Unsubscribe from an event
|
|
26
|
+
* @param {string} event - Event name
|
|
27
|
+
* @param {Function} callback - Callback function
|
|
28
|
+
*/
|
|
29
|
+
off(event, callback) {
|
|
30
|
+
if (this.listeners.has(event)) {
|
|
31
|
+
this.listeners.get(event).delete(callback);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Emit an event
|
|
37
|
+
* @param {string} event - Event name
|
|
38
|
+
* @param {*} data - Event data
|
|
39
|
+
*/
|
|
40
|
+
emit(event, data) {
|
|
41
|
+
if (this.listeners.has(event)) {
|
|
42
|
+
this.listeners.get(event).forEach(callback => {
|
|
43
|
+
try {
|
|
44
|
+
callback(data);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(`Error in listener for event '${event}':`, error);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Clear all listeners
|
|
54
|
+
*/
|
|
55
|
+
clear() {
|
|
56
|
+
this.listeners.clear();
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/core/RewardSystem.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
* Manages achievements, XP, levels, streaks, and rewards
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { EventEmitter } from './EventEmitter.js';
|
|
7
7
|
|
|
8
|
-
export class RewardSystem {
|
|
9
|
-
constructor() {
|
|
8
|
+
export class RewardSystem extends EventEmitter {
|
|
9
|
+
constructor(dataService, config = {}) {
|
|
10
|
+
super();
|
|
11
|
+
this.dataService = dataService;
|
|
10
12
|
this.player = null;
|
|
11
|
-
this.achievements = this._initAchievements();
|
|
12
|
-
this.listeners = [];
|
|
13
|
+
this.achievements = { ...this._initAchievements(), ...(config.achievements || {}) };
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -17,7 +18,7 @@ export class RewardSystem {
|
|
|
17
18
|
* @returns {Promise<Object>} Player data
|
|
18
19
|
*/
|
|
19
20
|
async init() {
|
|
20
|
-
this.player = await dataService.load('player', this._getDefaultPlayer());
|
|
21
|
+
this.player = await this.dataService.load('player', this._getDefaultPlayer());
|
|
21
22
|
|
|
22
23
|
// Check daily streak
|
|
23
24
|
this._updateDailyStreak();
|
|
@@ -57,7 +58,7 @@ export class RewardSystem {
|
|
|
57
58
|
async save() {
|
|
58
59
|
if (this.player) {
|
|
59
60
|
this.player.lastPlayedAt = Date.now();
|
|
60
|
-
await dataService.save('player', this.player);
|
|
61
|
+
await this.dataService.save('player', this.player);
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -82,10 +83,10 @@ export class RewardSystem {
|
|
|
82
83
|
await this.save();
|
|
83
84
|
|
|
84
85
|
// Notify listeners
|
|
85
|
-
this.
|
|
86
|
+
this.emit('xp_gained', { amount, reason, leveledUp, newLevel });
|
|
86
87
|
|
|
87
88
|
if (leveledUp) {
|
|
88
|
-
this.
|
|
89
|
+
this.emit('level_up', { oldLevel, newLevel });
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
return { leveledUp, newLevel, xpGained: amount };
|
|
@@ -135,7 +136,7 @@ export class RewardSystem {
|
|
|
135
136
|
// Update high score if applicable
|
|
136
137
|
if (result.score !== undefined && result.score > stats.highScore) {
|
|
137
138
|
stats.highScore = result.score;
|
|
138
|
-
this.
|
|
139
|
+
this.emit('new_high_score', { gameName, score: result.score });
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
// Merge other result data into stats
|
|
@@ -200,7 +201,7 @@ export class RewardSystem {
|
|
|
200
201
|
// Award XP
|
|
201
202
|
await this.addXP(achievement.xp, `Achievement: ${achievement.name}`);
|
|
202
203
|
|
|
203
|
-
this.
|
|
204
|
+
this.emit('achievement_unlocked', achievement);
|
|
204
205
|
await this.save();
|
|
205
206
|
|
|
206
207
|
return true;
|
|
@@ -292,19 +293,6 @@ export class RewardSystem {
|
|
|
292
293
|
return yesterday.toISOString().split('T')[0];
|
|
293
294
|
}
|
|
294
295
|
|
|
295
|
-
/**
|
|
296
|
-
* Event system for UI updates
|
|
297
|
-
*/
|
|
298
|
-
on(event, callback) {
|
|
299
|
-
this.listeners.push({ event, callback });
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
_emit(event, data) {
|
|
303
|
-
this.listeners
|
|
304
|
-
.filter(l => l.event === event)
|
|
305
|
-
.forEach(l => l.callback(data));
|
|
306
|
-
}
|
|
307
|
-
|
|
308
296
|
/**
|
|
309
297
|
* Initialize achievement definitions
|
|
310
298
|
* Can be extended by games
|
|
@@ -349,14 +337,4 @@ export class RewardSystem {
|
|
|
349
337
|
}
|
|
350
338
|
};
|
|
351
339
|
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Register custom achievements
|
|
355
|
-
* @param {Object} achievements
|
|
356
|
-
*/
|
|
357
|
-
registerAchievements(achievements) {
|
|
358
|
-
this.achievements = { ...this.achievements, ...achievements };
|
|
359
|
-
}
|
|
360
340
|
}
|
|
361
|
-
|
|
362
|
-
export const rewardSystem = new RewardSystem();
|
|
@@ -4,21 +4,21 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export class ParticleSystem {
|
|
7
|
-
constructor(
|
|
8
|
-
this.canvasId = canvasId;
|
|
9
|
-
this.canvas = document.getElementById(canvasId);
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.canvasId = config.canvasId || 'dopamine-particle-canvas';
|
|
9
|
+
this.canvas = document.getElementById(this.canvasId);
|
|
10
10
|
|
|
11
11
|
if (!this.canvas) {
|
|
12
12
|
// Create overlay canvas if it doesn't exist
|
|
13
13
|
this.canvas = document.createElement('canvas');
|
|
14
|
-
this.canvas.id = canvasId;
|
|
14
|
+
this.canvas.id = this.canvasId;
|
|
15
15
|
this.canvas.style.position = 'fixed';
|
|
16
16
|
this.canvas.style.top = '0';
|
|
17
17
|
this.canvas.style.left = '0';
|
|
18
18
|
this.canvas.style.width = '100%';
|
|
19
19
|
this.canvas.style.height = '100%';
|
|
20
20
|
this.canvas.style.pointerEvents = 'none';
|
|
21
|
-
this.canvas.style.zIndex = '9999';
|
|
21
|
+
this.canvas.style.zIndex = config.zIndex || '9999';
|
|
22
22
|
document.body.appendChild(this.canvas);
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -255,5 +255,3 @@ export class ParticleSystem {
|
|
|
255
255
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
-
|
|
259
|
-
export const particleSystem = new ParticleSystem();
|
package/src/index.js
CHANGED
|
@@ -1,25 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { RewardSystem } from './core/RewardSystem.js';
|
|
2
|
+
import { DataService } from './core/DataService.js';
|
|
3
|
+
import { GameUI } from './ui/GameUI.js';
|
|
4
|
+
import { ParticleSystem } from './effects/ParticleSystem.js';
|
|
5
|
+
import { SoundManager } from './audio/SoundManager.js';
|
|
6
6
|
import './ui/dopamine.css';
|
|
7
7
|
|
|
8
|
-
export {
|
|
8
|
+
export { RewardSystem, DataService, GameUI, ParticleSystem, SoundManager };
|
|
9
9
|
|
|
10
10
|
export default class Dopamine {
|
|
11
11
|
constructor(config = {}) {
|
|
12
12
|
this.config = config;
|
|
13
|
+
|
|
14
|
+
// Initialize subsystems with config
|
|
15
|
+
this.dataService = new DataService(config.data || {});
|
|
16
|
+
this.rewardSystem = new RewardSystem(this.dataService, config.rewards || {});
|
|
17
|
+
this.soundManager = new SoundManager(config.sound || {});
|
|
18
|
+
this.particleSystem = new ParticleSystem(config.particles || {});
|
|
19
|
+
this.gameUI = new GameUI(this.particleSystem);
|
|
20
|
+
|
|
21
|
+
// Bind UI to RewardSystem events
|
|
22
|
+
this._bindEvents();
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
async init() {
|
|
16
|
-
await rewardSystem.init();
|
|
26
|
+
await this.rewardSystem.init();
|
|
17
27
|
console.log('DopamineJS Initialized 🚀');
|
|
18
28
|
return {
|
|
19
|
-
rewardSystem,
|
|
20
|
-
gameUI,
|
|
21
|
-
particleSystem,
|
|
22
|
-
soundManager
|
|
29
|
+
rewardSystem: this.rewardSystem,
|
|
30
|
+
gameUI: this.gameUI,
|
|
31
|
+
particleSystem: this.particleSystem,
|
|
32
|
+
soundManager: this.soundManager
|
|
23
33
|
};
|
|
24
34
|
}
|
|
35
|
+
|
|
36
|
+
_bindEvents() {
|
|
37
|
+
this.rewardSystem.on('xp_gained', (data) => {
|
|
38
|
+
const { total, needed } = this.rewardSystem.getXPForNextLevel();
|
|
39
|
+
this.gameUI.updateXP(this.rewardSystem.player.xp, needed, total);
|
|
40
|
+
|
|
41
|
+
if (data.xpGained > 0) {
|
|
42
|
+
// Show floating text for XP gain
|
|
43
|
+
// We might need a way to know WHERE to show this, or just show it in a standard place
|
|
44
|
+
// For now, let's just update the bar
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.rewardSystem.on('level_up', (data) => {
|
|
49
|
+
this.gameUI.updateLevel(data.newLevel);
|
|
50
|
+
this.gameUI.showLevelUp(data.oldLevel, data.newLevel);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.rewardSystem.on('achievement_unlocked', (achievement) => {
|
|
54
|
+
this.gameUI.showAchievement(achievement);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this.rewardSystem.on('new_high_score', (data) => {
|
|
58
|
+
this.gameUI.showNotification(`New High Score: ${data.score}!`, 'legendary');
|
|
59
|
+
this.soundManager.playSuccess();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
25
62
|
}
|