dopaminejs 2.0.2 → 2.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,45 +1,37 @@
1
- (function(o,g){typeof exports=="object"&&typeof module<"u"?g(exports):typeof define=="function"&&define.amd?define(["exports"],g):(o=typeof globalThis<"u"?globalThis:o||self,g(o.Dopamine={}))})(this,function(o){"use strict";class g{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 b extends g{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),n=i>s;return n&&(this.player.level=i),await this.save(),this.emit("xp_gained",{amount:e,reason:t,leveledUp:n,newLevel:i}),n&&this.emit("level_up",{oldLevel:s,newLevel:i}),{leveledUp:n,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(n=>{n!=="score"&&(s[n]=(s[n]||0)+(typeof t[n]=="number"?t[n]: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,n]of Object.entries(this.achievements))this.player.achievements[i]||n.check(this.player,e,t)&&(await this.unlockAchievement(i),s.push(n));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 M{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 P{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=`
1
+ (function(h,f){typeof exports=="object"&&typeof module<"u"?f(exports):typeof define=="function"&&define.amd?define(["exports"],f):(h=typeof globalThis<"u"?globalThis:h||self,f(h.Dopamine={}))})(this,function(h){"use strict";class f{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 P extends f{constructor(e,t={}){var s;super(),this.dataService=e,this.player=null,this.achievements={...this._initAchievements(),...t.achievements||{}},this.events=t.events||((s=t.kernel)==null?void 0:s.events)||null,this._batchDepth=0,this._pendingSave=!1}_publish(e,t){this.emit(e,t),this.events&&this.events.emit(e,t)}async _batch(e){this._batchDepth++;try{return await e()}finally{this._batchDepth--,this._batchDepth===0&&this._pendingSave&&(this._pendingSave=!1,await this._write())}}async init(){const e=await this.dataService.load("player",null);return this.player=this._migrate(e),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:{}}}_migrate(e){const t=this._getDefaultPlayer();return!e||typeof e!="object"?t:{...t,...e,streak:{...t.streak,...e.streak||{}},achievements:e.achievements||{},stats:e.stats||{}}}async save(){if(this.player){if(this._batchDepth>0){this._pendingSave=!0;return}await this._write()}}async _write(){this.player.lastPlayedAt=Date.now(),await this.dataService.save("player",this.player)}async addXP(e,t=""){if(!Number.isFinite(e))throw new TypeError(`[DopamineJS] addXP expects a finite number, received ${e}`);const s=this.player.level;this.player.xp+=e;const i=this._calculateLevel(this.player.xp),r=i>s;return r&&(this.player.level=i),await this.save(),this._publish("xp_gained",{amount:e,reason:t,leveledUp:r,newLevel:i}),r&&this._publish("level_up",{oldLevel:s,newLevel:i}),{leveledUp:r,newLevel:i,xpGained:e}}_calculateLevel(e){const t=Math.floor((1+Math.sqrt(1+4*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),i=t-s,r=i>0?Math.max(0,Math.min(1,(this.player.xp-s)/i)):0;return{total:t,needed:Math.max(0,t-this.player.xp),progress:r}}async recordGame(e,t){return this._batch(()=>this._recordGame(e,t))}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._publish("new_high_score",{gameName:e,score:t.score})),Object.keys(t).forEach(r=>{r!=="score"&&(s[r]=(s[r]||0)+(typeof t[r]=="number"?t[r]: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,r]of Object.entries(this.achievements)){if(this.player.achievements[i])continue;let n=!1;try{n=r.check(this.player,e,t)}catch(o){console.error(`[DopamineJS] Achievement "${i}" check threw:`,o);continue}n&&(await this.unlockAchievement(i),s.push(r))}return s}async unlockAchievement(e){const t=this.achievements[e];return t?(this.player.achievements[e]={unlockedAt:Date.now(),seen:!1},await this.addXP(Number.isFinite(t.xp)?t.xp:0,`Achievement: ${t.name}`),this._publish("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}_toLocalDateString(e){const t=e.getFullYear(),s=String(e.getMonth()+1).padStart(2,"0"),i=String(e.getDate()).padStart(2,"0");return`${t}-${s}-${i}`}_getTodayDateString(){return this._toLocalDateString(new Date)}_getYesterdayDateString(){const e=new Date;return e.setDate(e.getDate()-1),this._toLocalDateString(e)}_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}}}}function w(){const a=new Map;return{getItem:e=>a.has(e)?a.get(e):null,setItem:(e,t)=>{a.set(e,String(t))},removeItem:e=>{a.delete(e)},clear:()=>{a.clear()}}}function x(){try{if(typeof localStorage>"u")return w();const a="__dopamine_probe__";return localStorage.setItem(a,"1"),localStorage.removeItem(a),localStorage}catch{return w()}}class C{constructor(e={}){this.storage=e.storage||x(),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)}}const D="game-ui-overlay";class I{constructor(e){this.particleSystem=e,this.container=null,this.xpBar=null,this.levelDisplay=null,this.streakDisplay=null,this._timers=new Set,this._detached=[],this.init()}init(){this.container=document.createElement("div"),this.container.className="game-ui-overlay",document.getElementById(D)||(this.container.id=D),this.container.innerHTML=`
2
2
  <div class="game-ui-top-bar">
3
- <div class="level-badge" id="level-badge">
3
+ <div class="level-badge">
4
4
  <span class="level-label">LVL</span>
5
- <span class="level-number" id="level-number">1</span>
5
+ <span class="level-number">1</span>
6
6
  </div>
7
-
7
+
8
8
  <div class="xp-container">
9
9
  <div class="xp-bar-bg">
10
- <div class="xp-bar-fill" id="xp-bar-fill" style="width: 0%"></div>
10
+ <div class="xp-bar-fill" style="width: 0%"></div>
11
11
  </div>
12
- <div class="xp-text" id="xp-text">0 /100 XP</div>
12
+ <div class="xp-text">0 / 100 XP</div>
13
13
  </div>
14
-
15
- <div class="streak-badge" id="streak-badge">
14
+
15
+ <div class="streak-badge">
16
16
  <span class="streak-icon">🔥</span>
17
- <span class="streak-number" id="streak-number">1</span>
17
+ <span class="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(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=`
20
+ `,document.body.appendChild(this.container),this.levelBadge=this.container.querySelector(".level-badge"),this.streakBadge=this.container.querySelector(".streak-badge"),this.xpBar=this.container.querySelector(".xp-bar-fill"),this.xpText=this.container.querySelector(".xp-text"),this.levelDisplay=this.container.querySelector(".level-number"),this.streakDisplay=this.container.querySelector(".streak-number")}_defer(e,t){const s=setTimeout(()=>{this._timers.delete(s),e()},t);return this._timers.add(s),s}_mount(e){return document.body.appendChild(e),this._detached.push(e),e}_replayAnimation(e,t){e&&(e.style.animation="none",this._defer(()=>{e.style.animation=t},10))}updateXP(e,t,s){const i=s>0?e/s*100:0;this.xpBar.style.width=`${Math.max(0,Math.min(100,i))}%`,this.xpText.textContent=`${e} / ${s} XP`,this._replayAnimation(this.xpBar,"xp-pulse 0.3s ease-out")}updateLevel(e){this.levelDisplay.textContent=e,this._replayAnimation(this.levelBadge,"level-pulse 0.5s ease-out")}updateStreak(e){this.streakDisplay.textContent=e,e>=7?this.streakBadge.style.background="linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)":e>=3&&(this.streakBadge.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">${e.icon}</div>
23
+ <div class="achievement-icon"></div>
24
24
  <div class="achievement-info">
25
25
  <div class="achievement-title">Achievement Unlocked!</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>
26
+ <div class="achievement-name"></div>
27
+ <div class="achievement-desc"></div>
28
+ <div class="achievement-xp"></div>
29
29
  </div>
30
30
  </div>
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=`
31
+ `,t.querySelector(".achievement-icon").innerHTML=e.icon??"",t.querySelector(".achievement-name").textContent=e.name??"",t.querySelector(".achievement-desc").textContent=e.description??"",t.querySelector(".achievement-xp").textContent=`+${e.xp??0} XP`,this._mount(t),this._defer(()=>{const s=t.getBoundingClientRect();this.particleSystem.confetti(s.left+s.width/2,s.top+s.height/2,40)},300),this._defer(()=>{t.style.animation="slideOut 0.3s ease-in forwards",this._defer(()=>this._remove(t),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">${t}</div>
34
+ <div class="level-up-number"></div>
35
35
  <div class="level-up-subtitle">Amazing progress!</div>
36
36
  </div>
37
- `,document.body.appendChild(s),setTimeout(()=>{for(let i=0;i<5;i++)setTimeout(()=>{const n=Math.random()*window.innerWidth,r=Math.random()*window.innerHeight*.6;this.particleSystem.confetti(n,r,30)},i*200)},300),setTimeout(()=>{s.style.opacity="0",setTimeout(()=>s.remove(),500)},2500)}showFloatingText(e,t,s,i="#ffd700",n="24px"){const r=document.createElement("div");r.className="floating-text",r.textContent=e,r.style.left=t+"px",r.style.top=s+"px",r.style.color=i,r.style.fontSize=n,document.body.appendChild(r),setTimeout(()=>r.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,n])=>`<div style="display:flex;justify-content:space-between;width:100%"><span>${i}:</span> <strong>${n}</strong></div>`).join("")),t.innerHTML=`
38
- <h1>Game Over</h1>
39
- <div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${e.score}</div>
40
- <div style="width: 100%; margin-bottom: 20px;">
41
- ${s}
42
- </div>
43
- <button class="btn" onclick="location.reload()">Play Again</button>
44
- <button class="btn" onclick="window.location.href='index.html'" style="margin-top: 10px; background: #543847;">Exit</button>
45
- `,document.body.appendChild(t)}}class E{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 n=this.customEffects.get(e);n?n(t,s,...i):console.warn(`[DopamineJS] Effect '${e}' not found.`)}emit(e){const{x:t,y:s,count:i=10,speed:n=5,gravity:r=.1,decay:h=.02,life:c=1,color:l="#ffd700",sprite:u=null,spread:y=Math.PI*2,angle:p=0,size:w=5}=e;for(let R=0;R<i;R++){const d=this._getParticle(),j=p+(Math.random()-.5)*y,z=Math.random()*n;d.x=t,d.y=s,d.vx=Math.cos(j)*z,d.vy=Math.sin(j)*z,d.life=c,d.decay=h*(.8+Math.random()*.4),d.gravity=r,d.color=Array.isArray(l)?l[Math.floor(Math.random()*l.length)]:l,d.size=w*(.8+Math.random()*.4),d.sprite=u,d.rotation=Math.random()*Math.PI*2,d.rotationSpeed=(Math.random()-.5)*.2,this.particles.push(d)}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 n=this._getParticle(),r=Math.PI*2/s*i,h=4;n.x=e,n.y=t,n.vx=Math.cos(r)*h,n.vy=Math.sin(r)*h,n.life=1,n.decay=.03,n.gravity=0,n.color="#ffffff",n.size=15,n.sprite=null,n.type="star",n.rotation=r,n.rotationSpeed=0,this.particles.push(n)}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,n){let r=Math.PI/2*3,h=e,c=t;const l=Math.PI/s;this.ctx.beginPath(),this.ctx.moveTo(e,t-i);for(let u=0;u<s;u++)h=e+Math.cos(r)*i,c=t+Math.sin(r)*i,this.ctx.lineTo(h,c),r+=l,h=e+Math.cos(r)*n,c=t+Math.sin(r)*n,this.ctx.lineTo(h,c),r+=l;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 T{constructor(e={}){this.audioContext=null,this.storageKey=e.storageKey||"dopamineSoundsMuted",this.muted=localStorage.getItem(this.storageKey)==="true",this.assets=new Map,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(),n=await this.audioContext.decodeAudioData(i);this.assets.set(e,n)}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 n=this.audioContext.createOscillator(),r=this.audioContext.createGain();n.connect(r),r.connect(this.audioContext.destination),n.frequency.value=e,n.type=s,r.gain.setValueAtTime(i,this.audioContext.currentTime),r.gain.exponentialRampToValueAtTime(.01,this.audioContext.currentTime+t),n.start(this.audioContext.currentTime),n.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 G{constructor(e={}){this.config=e,this.dataService=new M(e.data||{}),this.rewardSystem=new b(this.dataService,e.rewards||{}),this.soundManager=new T(e.sound||{}),this.particleSystem=new E(e.particles||{}),this.gameUI=new P(this.particleSystem),this._bindEvents()}async init(){return await this.rewardSystem.init(),{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()})}}class m{constructor(){this._listeners=new Map,this._onceListeners=new Map,this._sortedCache=new Map}on(e,t,s=0){return this._listeners.has(e)||this._listeners.set(e,new Set),this._listeners.get(e).add({callback:t,priority:s}),this._sortedCache.delete(e),this}once(e,t){return this._onceListeners.has(e)||this._onceListeners.set(e,new Set),this._onceListeners.get(e).add(t),this}off(e,t){const s=this._listeners.get(e);if(s){for(const n of s)if(n.callback===t){s.delete(n),this._sortedCache.delete(e);break}}const i=this._onceListeners.get(e);return i&&i.delete(t),this}emit(e,t){const s=this._getSortedListeners(e);for(let n=0;n<s.length;n++)s[n].callback(t);const i=this._onceListeners.get(e);if(i&&i.size>0){for(const n of i)n(t);i.clear()}}_getSortedListeners(e){if(this._sortedCache.has(e))return this._sortedCache.get(e);const t=this._listeners.get(e);if(!t||t.size===0)return[];const s=Array.from(t).sort((i,n)=>n.priority-i.priority);return this._sortedCache.set(e,s),s}clear(e){e?(this._listeners.delete(e),this._onceListeners.delete(e),this._sortedCache.delete(e)):(this._listeners.clear(),this._onceListeners.clear(),this._sortedCache.clear())}hasListeners(e){const t=this._listeners.get(e),s=this._onceListeners.get(e);return t&&t.size>0||s&&s.size>0}}m.Events={TICK:"tick",FIXED_UPDATE:"fixed_update",RENDER:"render",COLLISION_ENTER:"collision_enter",COLLISION_EXIT:"collision_exit",XP_GAINED:"xp_gained",LEVEL_UP:"level_up",ACHIEVEMENT_UNLOCKED:"achievement_unlocked",NEW_HIGH_SCORE:"new_high_score",STREAK_UPDATED:"streak_updated",SYSTEM_REGISTERED:"system_registered",SYSTEM_UNREGISTERED:"system_unregistered",PLUGIN_LOADED:"plugin_loaded",PLUGIN_UNLOADED:"plugin_unloaded"};class I{constructor(e){this.kernel=e,this._systems=new Map,this._updateOrder=[],this._fixedUpdateOrder=[],this._needsRecompute=!1}register(e,t,{priority:s=0,dependencies:i=[]}={}){this._systems.has(e)&&(console.warn(`[SystemRegistry] System "${e}" already registered. Replacing.`),this.unregister(e));for(const n of i)this._systems.has(n)||console.warn(`[SystemRegistry] Dependency "${n}" not found for system "${e}"`);return this._systems.set(e,{system:t,priority:s,dependencies:i}),t.init&&t.init(this.kernel),this._needsRecompute=!0,this.kernel.events.emit("system_registered",{name:e,system:t}),this}get(e){const t=this._systems.get(e);return t?t.system:void 0}has(e){return this._systems.has(e)}unregister(e){const t=this._systems.get(e);return t?(t.system.destroy&&t.system.destroy(),this._systems.delete(e),this._needsRecompute=!0,this.kernel.events.emit("system_unregistered",{name:e}),!0):!1}update(e){this._needsRecompute&&this._recomputeUpdateOrder();for(let t=0;t<this._updateOrder.length;t++){const s=this._updateOrder[t];s.update&&s.update(e)}}fixedUpdate(e){this._needsRecompute&&this._recomputeUpdateOrder();for(let t=0;t<this._fixedUpdateOrder.length;t++){const s=this._fixedUpdateOrder[t];s.fixedUpdate&&s.fixedUpdate(e)}}_recomputeUpdateOrder(){const e=Array.from(this._systems.entries()),t=new Map,s=new Map;for(const[r,h]of e)t.set(r,h.dependencies),s.set(r,0);for(const[r,h]of t)for(const c of h)s.has(c)&&s.set(r,s.get(r)+1);const i=[],n=[];for(const[r,h]of s)h===0&&i.push(r);for(;i.length>0;){i.sort((h,c)=>{const l=this._systems.get(h).priority;return this._systems.get(c).priority-l});const r=i.shift();n.push(r);for(const[h,c]of t)if(c.includes(r)){const l=s.get(h)-1;s.set(h,l),l===0&&i.push(h)}}n.length!==e.length&&(console.error("[SystemRegistry] Circular dependency detected in systems!"),n.length=0,n.push(...e.map(([r])=>r).sort((r,h)=>this._systems.get(h).priority-this._systems.get(r).priority))),this._updateOrder=n.map(r=>this._systems.get(r).system).filter(r=>r.update),this._fixedUpdateOrder=n.map(r=>this._systems.get(r).system).filter(r=>r.fixedUpdate),this._needsRecompute=!1}getSystemNames(){return Array.from(this._systems.keys())}clear(){for(const e of this._systems.keys())this.unregister(e)}}class C{constructor(e){this.kernel=e,this._plugins=new Map,this._loadOrder=[]}use(e){if(!e.name)throw new Error("[PluginRegistry] Plugin must have a name");if(this._plugins.has(e.name))return console.warn(`[PluginRegistry] Plugin "${e.name}" already registered. Skipping.`),this;if(!e.init||typeof e.init!="function")throw new Error(`[PluginRegistry] Plugin "${e.name}" must have an init() method`);this._plugins.set(e.name,e),this._loadOrder.push(e.name);try{e.init(this.kernel),this.kernel.events.emit("plugin_loaded",{name:e.name,version:e.version})}catch(t){throw console.error(`[PluginRegistry] Failed to initialize plugin "${e.name}":`,t),this._plugins.delete(e.name),this._loadOrder.pop(),t}return this}async useAsync(e){if(!e.name)throw new Error("[PluginRegistry] Plugin must have a name");if(this._plugins.has(e.name))return console.warn(`[PluginRegistry] Plugin "${e.name}" already registered. Skipping.`),this;if(!e.init||typeof e.init!="function")throw new Error(`[PluginRegistry] Plugin "${e.name}" must have an init() method`);this._plugins.set(e.name,e),this._loadOrder.push(e.name);try{await e.init(this.kernel),this.kernel.events.emit("plugin_loaded",{name:e.name,version:e.version})}catch(t){throw console.error(`[PluginRegistry] Failed to initialize plugin "${e.name}":`,t),this._plugins.delete(e.name),this._loadOrder.pop(),t}return this}remove(e){const t=this._plugins.get(e);if(!t)return!1;if(t.destroy)try{t.destroy()}catch(i){console.error(`[PluginRegistry] Error destroying plugin "${e}":`,i)}this._plugins.delete(e);const s=this._loadOrder.indexOf(e);return s>-1&&this._loadOrder.splice(s,1),this.kernel.events.emit("plugin_unloaded",{name:e}),!0}get(e){return this._plugins.get(e)}has(e){return this._plugins.has(e)}getPluginNames(){return Array.from(this._plugins.keys())}getLoadOrder(){return[...this._loadOrder]}clear(){for(let e=this._loadOrder.length-1;e>=0;e--)this.remove(this._loadOrder[e])}}class O{constructor(e={}){this.width=e.width||800,this.height=e.height||600,this.backgroundColor=e.backgroundColor||"#000000",e.canvas?(this.canvas=e.canvas,this.canvas.width=this.width,this.canvas.height=this.height):(this.canvas=document.createElement("canvas"),this.canvas.width=this.width,this.canvas.height=this.height,(e.parent?typeof e.parent=="string"?document.querySelector(e.parent):e.parent:document.body).appendChild(this.canvas)),this.ctx=this.canvas.getContext("2d")}clear(){this.ctx.fillStyle=this.backgroundColor,this.ctx.fillRect(0,0,this.width,this.height)}resize(e,t){this.width=e,this.height=t,this.canvas.width=e,this.canvas.height=t}}class A{constructor(){this.running=!1,this.lastTime=0,this.callbacks=new Set,this._tick=this._tick.bind(this)}add(e){this.callbacks.add(e)}remove(e){this.callbacks.delete(e)}start(){this.running||(this.running=!0,this.lastTime=performance.now(),requestAnimationFrame(this._tick))}stop(){this.running=!1}_tick(e){if(!this.running)return;const t=(e-this.lastTime)/1e3;this.lastTime=e;const s=Math.min(t,.1);for(const i of this.callbacks)i(s);requestAnimationFrame(this._tick)}}class f{constructor(){this.gameObject=null,this.kernel=null}get physics(){var e;return(e=this.kernel)==null?void 0:e.systems.get("physics")}get input(){var e;return(e=this.kernel)==null?void 0:e.systems.get("input")}get loader(){var e;return(e=this.kernel)==null?void 0:e.systems.get("loader")}get events(){var e;return(e=this.kernel)==null?void 0:e.events}onAttach(){}onDetach(){}update(e){}render(e){}}class B extends f{constructor(e="box",t=50,s=50,i=25){super(),this.type=e,this.width=t,this.height=s,this.radius=i,this.tag=null}onAttach(){super.onAttach(),this.physics&&this.physics.add(this)}onDetach(){super.onDetach(),this.physics&&this.physics.remove(this)}getBounds(){const e=this.gameObject;if(this.type==="box")return{left:e.x-this.width/2,right:e.x+this.width/2,top:e.y-this.height/2,bottom:e.y+this.height/2};if(this.type==="circle")return{x:e.x,y:e.y,radius:this.radius}}}class _{constructor(){this.colliders=[],this.kernel=null}init(e){this.kernel=e}fixedUpdate(e){this.step()}destroy(){this.colliders=[]}add(e){this.colliders.push(e)}remove(e){const t=this.colliders.indexOf(e);t>-1&&this.colliders.splice(t,1)}step(){for(let e=0;e<this.colliders.length;e++)for(let t=e+1;t<this.colliders.length;t++){const s=this.colliders[e],i=this.colliders[t];this._intersects(s,s.getBounds(),i,i.getBounds())&&(this._notifyCollision(s.gameObject,i.gameObject),this._notifyCollision(i.gameObject,s.gameObject))}}_notifyCollision(e,t){!e||!e.components||e.components.forEach(s=>{s.onCollisionEnter&&s.onCollisionEnter(t)})}checkOverlap(e,t){const s=[],i=e.getBounds();for(const n of this.colliders){if(n===e||t&&n.tag!==t)continue;const r=n.getBounds();this._intersects(e,i,n,r)&&s.push(n)}return s}_intersects(e,t,s,i){if(e.type==="box"&&s.type==="box")return t.left<i.right&&t.right>i.left&&t.top<i.bottom&&t.bottom>i.top;if(e.type==="circle"&&s.type==="circle"){const n=t.x-i.x,r=t.y-i.y;return Math.sqrt(n*n+r*r)<t.radius+i.radius}return!1}addBody(e){this.add(e)}removeBody(e){this.remove(e)}checkCollision(e,t){return this._intersects(e,e.getBounds(),t,t.getBounds())}raycast(e,t,s){return null}setGravity(e,t){}}const $=new _;console.warn("[DopamineJS] GlobalPhysics is deprecated. Use kernel.physics instead.");class x{constructor(){this.keys=new Map,this.mouse={x:0,y:0,buttons:new Map},this.kernel=null,this._onKeyDown=this._onKeyDown.bind(this),this._onKeyUp=this._onKeyUp.bind(this),this._onMouseMove=this._onMouseMove.bind(this),this._onMouseDown=this._onMouseDown.bind(this),this._onMouseUp=this._onMouseUp.bind(this)}init(e){this.kernel=e,window.addEventListener("keydown",this._onKeyDown),window.addEventListener("keyup",this._onKeyUp),window.addEventListener("mousemove",this._onMouseMove),window.addEventListener("mousedown",this._onMouseDown),window.addEventListener("mouseup",this._onMouseUp)}destroy(){window.removeEventListener("keydown",this._onKeyDown),window.removeEventListener("keyup",this._onKeyUp),window.removeEventListener("mousemove",this._onMouseMove),window.removeEventListener("mousedown",this._onMouseDown),window.removeEventListener("mouseup",this._onMouseUp)}_onKeyDown(e){this.keys.set(e.key,!0)}_onKeyUp(e){this.keys.set(e.key,!1)}_onMouseMove(e){this.mouse.x=e.clientX,this.mouse.y=e.clientY}_onMouseDown(e){this.mouse.buttons.set(e.button,!0)}_onMouseUp(e){this.mouse.buttons.set(e.button,!1)}isKeyDown(e){return this.keys.get(e)||!1}isMouseButtonDown(e=0){return this.mouse.buttons.get(e)||!1}}const F=new x;console.warn("[DopamineJS] GlobalInput is deprecated. Use kernel.input instead.");class S{constructor(){this.assets=new Map,this.kernel=null}init(e){this.kernel=e}destroy(){this.assets.clear()}async loadImage(e,t){return new Promise((s,i)=>{const n=new Image;n.onload=()=>{this.assets.set(e,n),s(n)},n.onerror=i,n.src=t})}get(e){return this.assets.get(e)}}const N=new S;console.warn("[DopamineJS] GlobalLoader is deprecated. Use kernel.loader instead.");class D{constructor(e={}){this.config=e,this.events=new m,this.systems=new I(this),this.plugins=new C(this),this._registerCoreSystems(e),this.fixedTimestep=e.fixedTimestep||1/60,this._accumulator=0,this._maxAccumulator=this.fixedTimestep*5}_registerCoreSystems(e){this.systems.register("ticker",new A,{priority:100});const t={width:e.width,height:e.height,backgroundColor:e.backgroundColor,canvas:e.canvas,...e.renderer};this.systems.register("renderer",new O(t),{priority:90}),this.systems.register("input",new x,{priority:80}),this.systems.register("loader",new S,{priority:70}),this.systems.register("physics",new _,{priority:60})}start(){const e=this.systems.get("ticker");e&&(e.add(this._update.bind(this)),e.start()),this.events.emit("kernel_started")}stop(){const e=this.systems.get("ticker");e&&e.stop(),this.events.emit("kernel_stopped")}_update(e){for(this.events.emit(m.Events.TICK,{dt:e}),this._accumulator+=e,this._accumulator>this._maxAccumulator&&(this._accumulator=this._maxAccumulator);this._accumulator>=this.fixedTimestep;)this.events.emit(m.Events.FIXED_UPDATE,{dt:this.fixedTimestep}),this.systems.fixedUpdate(this.fixedTimestep),this._accumulator-=this.fixedTimestep;this.systems.update(e),this.events.emit(m.Events.RENDER,{dt:e})}get physics(){return this.systems.get("physics")}get input(){return this.systems.get("input")}get loader(){return this.systems.get("loader")}get renderer(){return this.systems.get("renderer")}get ticker(){return this.systems.get("ticker")}destroy(){this.stop(),this.plugins.clear(),this.systems.clear(),this.events.clear()}}const v={init(a){},update(a){},fixedUpdate(a){},destroy(){}};class K{constructor(){this.kernel=null}init(e){this.kernel=e}update(e){}fixedUpdate(e){}destroy(){}}const X={...v,addBody(a){},removeBody(a){},checkCollision(a,e){},raycast(a,e,t){},setGravity(a,e){},step(a){}},H={...v,registerSound(a,e){},preloadSounds(a){},play(a,e){},playTone(a,e,t,s){},stop(a){},toggleMute(){},setVolume(a){}},q={...v,registerSprite(a,e){},registerEffect(a,e){},play(a,e,t,...s){},emit(a){},clear(){},confetti(a,e,t){},coinShower(a,e,t){},sparkle(a,e,t,s){},fire(a,e,t){},starBurst(a,e,t){}};class W{constructor(){this.gameObjects=[],this.kernel=null}onEnter(){}onExit(){}add(e){return this.kernel&&(e.kernel=this.kernel),this.gameObjects.push(e),e}remove(e){const t=this.gameObjects.indexOf(e);t>-1&&this.gameObjects.splice(t,1)}update(e){for(const t of this.gameObjects)t.update(e)}render(e){for(const t of this.gameObjects)t.render(e)}}class L{constructor(e){this.game=e,this.currentScene=null}run(e){this.currentScene&&this.currentScene.onExit(),this.currentScene=e,this.currentScene.game=this.game,this.game.scene=e,e.onEnter()}}class J{constructor(e={}){this.config=e,this.kernel=new D(e),this.director=new L(this),this.kernel.systems.register("director",this.director),this.kernel.events.on(m.Events.TICK,this._update.bind(this),10),this.kernel.events.on(m.Events.RENDER,this._render.bind(this),10)}get renderer(){return this.kernel.renderer}get ticker(){return this.kernel.ticker}get physics(){return this.kernel.physics}get input(){return this.kernel.input}start(){this.kernel.start()}stop(){this.kernel.stop()}setScene(e){e.kernel=this.kernel,this.director.run(e)}_update({dt:e}){this.director.currentScene&&this.director.currentScene.update(e)}_render({dt:e}){this.renderer.clear(),this.director.currentScene&&this.director.currentScene.render(this.renderer.ctx)}destroy(){this.kernel.destroy()}}class Y{constructor(e=0,t=0){this.x=e,this.y=t,this.rotation=0,this.scale={x:1,y:1},this.components=[],this.children=[],this.parent=null,this._kernel=null}get kernel(){return this._kernel}set kernel(e){this._kernel=e;for(const t of this.components)t.kernel=e;for(const t of this.children)t.kernel=e}addChild(e){return e.parent=this,e.kernel=this.kernel,this.children.push(e),e}addComponent(e){return e.gameObject=this,e.kernel=this.kernel,this.components.push(e),e.onAttach(),e}getComponent(e){return this.components.find(t=>t instanceof e)}update(e){for(const t of this.components)t.update&&t.update(e);for(const t of this.children)t.update(e)}render(e){e.save(),e.translate(this.x,this.y),e.rotate(this.rotation),e.scale(this.scale.x,this.scale.y);for(const t of this.components)t.render&&t.render(e);for(const t of this.children)t.render(e);e.restore()}}class k{constructor(e=0,t=0){this.x=e,this.y=t}add(e){return this.x+=e.x,this.y+=e.y,this}sub(e){return this.x-=e.x,this.y-=e.y,this}scale(e){return this.x*=e,this.y*=e,this}distance(e){const t=this.x-e.x,s=this.y-e.y;return Math.sqrt(t*t+s*s)}normalize(){const e=Math.sqrt(this.x*this.x+this.y*this.y);return e>0&&(this.x/=e,this.y/=e),this}clone(){return new k(this.x,this.y)}static distance(e,t){return Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2))}}class U extends f{constructor(e){super(),this.image=e,this.anchor={x:.5,y:.5},this.width=e?e.width:0,this.height=e?e.height:0,this.frame={x:0,y:0,w:this.width,h:this.height}}setFrame(e,t,s,i){this.frame={x:e,y:t,w:s,h:i},this.width=s,this.height=i}setTexture(e){this.image=e,e&&this.setFrame(0,0,e.width,e.height)}render(e){if(!this.image)return;const t=this.frame.w,s=this.frame.h,i=-t*this.anchor.x,n=-s*this.anchor.y;e.drawImage(this.image,this.frame.x,this.frame.y,t,s,i,n,t,s)}}class V extends f{constructor(){super(),this.animations={},this.currentAnim=null,this.timer=0,this.currentFrameIndex=0,this.sprite=null}onAttach(){this.sprite=this.gameObject.getComponent(U),this.sprite||console.warn("Animator attached to GameObject without a Sprite component.")}addAnimation(e,t,s=10,i=!0){this.animations[e]={frames:t,fps:s,loop:i}}play(e){this.currentAnim&&this.currentAnim.name===e||this.animations[e]&&(this.currentAnim={name:e,...this.animations[e]},this.currentFrameIndex=0,this.timer=0,this._updateFrame())}update(e){if(!this.currentAnim||!this.sprite)return;this.timer+=e;const t=1/this.currentAnim.fps;this.timer>=t&&(this.timer-=t,this.currentFrameIndex++,this.currentFrameIndex>=this.currentAnim.frames.length&&(this.currentAnim.loop?this.currentFrameIndex=0:this.currentFrameIndex=this.currentAnim.frames.length-1),this._updateFrame())}_updateFrame(){const e=this.currentAnim.frames[this.currentFrameIndex];this.sprite.setFrame(e.x,e.y,e.w,e.h)}}class Q{static createGround(e,t,s="#333"){const i=document.createElement("canvas");i.width=e,i.height=t;const n=i.getContext("2d");n.fillStyle=s,n.fillRect(0,0,e,t);for(let r=0;r<1e3;r++){n.fillStyle=Math.random()>.5?"#444":"#222";const h=Math.random()*4+2;n.fillRect(Math.random()*e,Math.random()*t,h,h)}n.strokeStyle="rgba(255,255,255,0.05)",n.lineWidth=2,n.beginPath();for(let r=0;r<=e;r+=64)n.moveTo(r,0),n.lineTo(r,t);for(let r=0;r<=t;r+=64)n.moveTo(0,r),n.lineTo(e,r);return n.stroke(),i}static createCharacterSheet(e,t){const n=document.createElement("canvas");n.width=64*4,n.height=64;const r=n.getContext("2d");for(let h=0;h<4;h++){const c=h*64,l=0,u=c+64/2,y=l+64/2,p=h%2===0?0:-2,w=h===1?5:h===3?-5:0;r.fillStyle=e,r.beginPath(),r.arc(u,y+p,20,0,Math.PI*2),r.fill(),r.fillStyle=t,r.beginPath(),r.arc(u-15,y+p+w,8,0,Math.PI*2),r.arc(u+15,y+p-w,8,0,Math.PI*2),r.fill(),r.fillStyle=Z(e),r.beginPath(),r.arc(u,y+p,15,0,Math.PI*2),r.fill(),t==="#333"&&(r.fillStyle="#000",r.fillRect(u+10,y+p-5,25,6))}return n}}function Z(a,e){return a}class ee extends f{constructor(e){super(),this.system=e}play(e,t={}){const s=this.gameObject.x,i=this.gameObject.y;switch(e){case"confetti":this.system.confetti(s,i,t.count);break;case"fire":this.system.fire(s,i,t.count);break;case"sparkle":this.system.sparkle(s,i,t.count,t.color);break;case"blood":this.system.emit({x:s,y:i,count:10,color:["#8a0303","#ff0000"],speed:4,gravity:.1,decay:.05});break;default:this.system.emit({x:s,y:i,...t})}}}class te extends f{constructor(){super(),this.intensity=0,this.duration=0,this.timer=0,this.originalPos={x:0,y:0},this.isShaking=!1}shake(e=5,t=.2){this.intensity=e,this.duration=t,this.timer=0,this.isShaking=!0,this.timer===0&&(this.originalPos.x=this.gameObject.x,this.originalPos.y=this.gameObject.y)}update(e){if(!this.isShaking)return;if(this.timer+=e,this.timer>=this.duration){this.isShaking=!1,this.gameObject.x=this.originalPos.x,this.gameObject.y=this.originalPos.y;return}const t=(Math.random()-.5)*2*this.intensity,s=(Math.random()-.5)*2*this.intensity;this.gameObject.x=this.originalPos.x+t,this.gameObject.y=this.originalPos.y+s}}o.Animator=V,o.Collider=B,o.Component=f,o.DataService=M,o.Director=L,o.DopamineKernel=D,o.EventBus=m,o.Game=J,o.GameObject=Y,o.GameUI=P,o.GlobalInput=F,o.GlobalLoader=N,o.GlobalPhysics=$,o.IAudioSystem=H,o.IParticleSystem=q,o.IPhysicsSystem=X,o.ISystem=v,o.Input=x,o.Loader=S,o.ParticleEmitter=ee,o.ParticleSystem=E,o.Physics=_,o.PluginRegistry=C,o.Renderer=O,o.RewardSystem=b,o.Scene=W,o.ScreenShake=te,o.SoundManager=T,o.Sprite=U,o.System=K,o.SystemRegistry=I,o.TextureGenerator=Q,o.Ticker=A,o.Vector2=k,o.default=G,Object.defineProperties(o,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
37
+ `,s.querySelector(".level-up-number").textContent=t,this._mount(s),this._defer(()=>{for(let i=0;i<5;i++)this._defer(()=>{const r=Math.random()*window.innerWidth,n=Math.random()*window.innerHeight*.6;this.particleSystem.confetti(r,n,30)},i*200)},300),this._defer(()=>{s.style.opacity="0",this._defer(()=>this._remove(s),500)},2500)}showFloatingText(e,t,s,i="#ffd700",r="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=r,this._mount(n),this._defer(()=>this._remove(n),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";const s=document.createElement("h1");s.textContent="Game Over";const i=document.createElement("div");i.className="summary-score",i.style.cssText="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;",i.textContent=e.score;const r=document.createElement("div");r.className="summary-metrics",r.style.cssText="width: 100%; margin-bottom: 20px;";for(const[o,l]of Object.entries(e.metrics||{})){const c=document.createElement("div");c.style.cssText="display:flex;justify-content:space-between;width:100%";const d=document.createElement("span");d.textContent=`${o}:`;const m=document.createElement("strong");m.textContent=l,c.append(d,m),r.appendChild(c)}t.append(s,i,r);const n=document.createElement("button");if(n.className="btn",n.textContent="Play Again",n.addEventListener("click",e.onReplay||(()=>window.location.reload())),t.appendChild(n),e.onExit){const o=document.createElement("button");o.className="btn",o.textContent="Exit",o.style.cssText="margin-top: 10px; background: #543847;",o.addEventListener("click",e.onExit),t.appendChild(o)}return this._mount(t),t}_remove(e){e.remove();const t=this._detached.indexOf(e);t>-1&&this._detached.splice(t,1)}destroy(){var e;for(const t of this._timers)clearTimeout(t);this._timers.clear();for(const t of this._detached)t.remove();this._detached=[],(e=this.container)==null||e.remove(),this.container=null,this.xpBar=null,this.xpText=null,this.levelBadge=null,this.streakBadge=null,this.levelDisplay=null,this.streakDisplay=null}}class O{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.createElement("canvas"),this.canvas.classList.add("dopamine-particle-canvas"),document.getElementById(this.canvasId)||(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._onResize=()=>this._resize(),this._resizeObserver=null,this._frameHandle=null,this._resize(),window.ResizeObserver&&this.container!==document.body?(this._resizeObserver=new ResizeObserver(this._onResize),this._resizeObserver.observe(this.container)):window.addEventListener("resize",this._onResize)}_resize(){const e=this.container===document.body?window.innerWidth:this.container.clientWidth,t=this.container===document.body?window.innerHeight:this.container.clientHeight,s=window.devicePixelRatio||1;this.width=e,this.height=t,this.canvas.width=Math.round(e*s),this.canvas.height=Math.round(t*s),this.canvas.style.width=`${e}px`,this.canvas.style.height=`${t}px`,this.ctx.setTransform(s,0,0,s,0,0)}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 r=this.customEffects.get(e);r?r(t,s,...i):console.warn(`[DopamineJS] Effect '${e}' not found.`)}emit(e){const{x:t,y:s,count:i=10,speed:r=5,gravity:n=.1,decay:o=.02,life:l=1,color:c="#ffd700",sprite:d=null,spread:m=Math.PI*2,angle:y=0,size:v=5}=e;for(let B=0;B<i;B++){const u=this._getParticle(),F=y+(Math.random()-.5)*m,N=Math.random()*r;u.x=t,u.y=s,u.vx=Math.cos(F)*N,u.vy=Math.sin(F)*N,u.life=l,u.decay=o*(.8+Math.random()*.4),u.gravity=n,u.color=Array.isArray(c)?c[Math.floor(Math.random()*c.length)]:c,u.size=v*(.8+Math.random()*.4),u.sprite=d,u.rotation=Math.random()*Math.PI*2,u.rotationSpeed=(Math.random()-.5)*.2,this.particles.push(u)}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 r=this._getParticle(),n=Math.PI*2/s*i,o=4;r.x=e,r.y=t,r.vx=Math.cos(n)*o,r.vy=Math.sin(n)*o,r.life=1,r.decay=.03,r.gravity=0,r.color="#ffffff",r.size=15,r.sprite=null,r.type="star",r.rotation=n,r.rotationSpeed=0,this.particles.push(r)}this._startAnimation()}_startAnimation(){this.isAnimating||(this.isAnimating=!0,this._animate())}_animate(){this._frameHandle=null,this.ctx.clearRect(0,0,this.width,this.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?this._frameHandle=requestAnimationFrame(()=>this._animate()):this.isAnimating=!1}_drawStarPath(e,t,s,i,r){let n=Math.PI/2*3,o=e,l=t;const c=Math.PI/s;this.ctx.beginPath(),this.ctx.moveTo(e,t-i);for(let d=0;d<s;d++)o=e+Math.cos(n)*i,l=t+Math.sin(n)*i,this.ctx.lineTo(o,l),n+=c,o=e+Math.cos(n)*r,l=t+Math.sin(n)*r,this.ctx.lineTo(o,l),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.width,this.height)}destroy(){this.clear(),this.isAnimating=!1,this._frameHandle!==null&&(cancelAnimationFrame(this._frameHandle),this._frameHandle=null),this._resizeObserver&&(this._resizeObserver.disconnect(),this._resizeObserver=null),window.removeEventListener("resize",this._onResize),this.canvas.remove(),this.sprites.clear(),this.customEffects.clear(),this.pool=[]}}class T{constructor(e={}){this.audioContext=null,this.storageKey=e.storageKey||"dopamineSoundsMuted",this.storage=e.storage||x(),this.muted=this.storage.getItem(this.storageKey)==="true",this.assets=new Map,this.customSounds=e.customSounds||{},this.masterGain=null,this._volume=e.volume??1,Object.keys(this.customSounds).length>0&&this.preloadSounds(this.customSounds)}initAudio(){if(!this.audioContext){const e=typeof window<"u"?window.AudioContext||window.webkitAudioContext:void 0;if(!e)return!1;this.audioContext=new e,this.masterGain=this.audioContext.createGain(),this.masterGain.gain.value=this._volume,this.masterGain.connect(this.audioContext.destination)}return this.audioContext.state==="suspended"&&this.audioContext.resume(),!0}setVolume(e){return this._volume=Math.max(0,Math.min(1,e)),this.masterGain&&(this.masterGain.gain.value=this._volume),this._volume}getVolume(){return this._volume}_output(){return this.masterGain||this.audioContext.destination}toggleMute(){return this.muted=!this.muted,this.storage.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(),r=await this.audioContext.decodeAudioData(i);this.assets.set(e,r)}catch(s){console.warn(`[DopamineJS] Failed to load sound '${e}':`,s)}}async play(e){if(this.muted||!this.initAudio())return;if(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._output()),t.start(0)}playTone(e,t,s="sine",i=.3){if(this.muted||!this.initAudio())return;const r=this.audioContext.createOscillator(),n=this.audioContext.createGain();r.connect(n),n.connect(this._output()),r.frequency.value=e,r.type=s;const o=Math.max(1e-4,i);n.gain.setValueAtTime(o,this.audioContext.currentTime),n.gain.exponentialRampToValueAtTime(.01,this.audioContext.currentTime+t),r.start(this.audioContext.currentTime),r.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 H{constructor(e={}){this.config=e,this.dataService=new C(e.data||{}),this.rewardSystem=new P(this.dataService,e.rewards||{}),this.soundManager=new T(e.sound||{}),this.particleSystem=new O(e.particles||{}),this.gameUI=new I(this.particleSystem),this._bindEvents()}async init(){return await this.rewardSystem.init(),{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()})}}class p{constructor(){this._listeners=new Map,this._onceListeners=new Map,this._sortedCache=new Map}on(e,t,s=0){return this._listeners.has(e)||this._listeners.set(e,new Set),this._listeners.get(e).add({callback:t,priority:s}),this._sortedCache.delete(e),this}once(e,t){return this._onceListeners.has(e)||this._onceListeners.set(e,new Set),this._onceListeners.get(e).add(t),this}off(e,t){const s=this._listeners.get(e);if(s)for(const r of[...s])r.callback===t&&(s.delete(r),this._sortedCache.delete(e));const i=this._onceListeners.get(e);return i&&i.delete(t),this}emit(e,t){const s=this._getSortedListeners(e);for(let r=0;r<s.length;r++)this._invoke(s[r].callback,t,e);const i=this._onceListeners.get(e);if(i&&i.size>0){const r=[...i];i.clear();for(const n of r)this._invoke(n,t,e)}}_invoke(e,t,s){try{e(t)}catch(i){console.error(`[DopamineJS] Listener for "${s}" threw:`,i)}}_getSortedListeners(e){if(this._sortedCache.has(e))return this._sortedCache.get(e);const t=this._listeners.get(e);if(!t||t.size===0)return[];const s=Array.from(t).sort((i,r)=>r.priority-i.priority);return this._sortedCache.set(e,s),s}clear(e){e?(this._listeners.delete(e),this._onceListeners.delete(e),this._sortedCache.delete(e)):(this._listeners.clear(),this._onceListeners.clear(),this._sortedCache.clear())}hasListeners(e){const t=this._listeners.get(e),s=this._onceListeners.get(e);return!!(t!=null&&t.size)||!!(s!=null&&s.size)}}p.Events={TICK:"tick",FIXED_UPDATE:"fixed_update",RENDER:"render",COLLISION_ENTER:"collision_enter",COLLISION_EXIT:"collision_exit",XP_GAINED:"xp_gained",LEVEL_UP:"level_up",ACHIEVEMENT_UNLOCKED:"achievement_unlocked",NEW_HIGH_SCORE:"new_high_score",STREAK_UPDATED:"streak_updated",SYSTEM_REGISTERED:"system_registered",SYSTEM_UNREGISTERED:"system_unregistered",PLUGIN_LOADED:"plugin_loaded",PLUGIN_UNLOADED:"plugin_unloaded"};class A{constructor(e){this.kernel=e,this._systems=new Map,this._updateOrder=[],this._fixedUpdateOrder=[],this._needsRecompute=!1}register(e,t,{priority:s=0,dependencies:i=[]}={}){this._systems.has(e)&&(console.warn(`[SystemRegistry] System "${e}" already registered. Replacing.`),this.unregister(e));for(const r of i)this._systems.has(r)||console.warn(`[SystemRegistry] Dependency "${r}" not found for system "${e}"`);return this._systems.set(e,{system:t,priority:s,dependencies:i}),t.init&&t.init(this.kernel),this._needsRecompute=!0,this.kernel.events.emit("system_registered",{name:e,system:t}),this}get(e){const t=this._systems.get(e);return t?t.system:void 0}has(e){return this._systems.has(e)}unregister(e){const t=this._systems.get(e);return t?(t.system.destroy&&t.system.destroy(),this._systems.delete(e),this._needsRecompute=!0,this.kernel.events.emit("system_unregistered",{name:e}),!0):!1}update(e){this._needsRecompute&&this._recomputeUpdateOrder();for(let t=0;t<this._updateOrder.length;t++){const s=this._updateOrder[t];s.update&&s.update(e)}}fixedUpdate(e){this._needsRecompute&&this._recomputeUpdateOrder();for(let t=0;t<this._fixedUpdateOrder.length;t++){const s=this._fixedUpdateOrder[t];s.fixedUpdate&&s.fixedUpdate(e)}}_recomputeUpdateOrder(){const e=Array.from(this._systems.entries()),t=new Map,s=new Map;for(const[n,o]of e)t.set(n,o.dependencies),s.set(n,0);for(const[n,o]of t)for(const l of o)s.has(l)&&s.set(n,s.get(n)+1);const i=[],r=[];for(const[n,o]of s)o===0&&i.push(n);for(;i.length>0;){i.sort((o,l)=>{const c=this._systems.get(o).priority;return this._systems.get(l).priority-c});const n=i.shift();r.push(n);for(const[o,l]of t)if(l.includes(n)){const c=s.get(o)-1;s.set(o,c),c===0&&i.push(o)}}r.length!==e.length&&(console.error("[SystemRegistry] Circular dependency detected in systems!"),r.length=0,r.push(...e.map(([n])=>n).sort((n,o)=>this._systems.get(o).priority-this._systems.get(n).priority))),this._updateOrder=r.map(n=>this._systems.get(n).system).filter(n=>n.update),this._fixedUpdateOrder=r.map(n=>this._systems.get(n).system).filter(n=>n.fixedUpdate),this._needsRecompute=!1}getSystemNames(){return Array.from(this._systems.keys())}clear(){for(const e of this._systems.keys())this.unregister(e)}}class L{constructor(e){this.kernel=e,this._plugins=new Map,this._loadOrder=[]}use(e){if(!e.name)throw new Error("[PluginRegistry] Plugin must have a name");if(this._plugins.has(e.name))return console.warn(`[PluginRegistry] Plugin "${e.name}" already registered. Skipping.`),this;if(!e.init||typeof e.init!="function")throw new Error(`[PluginRegistry] Plugin "${e.name}" must have an init() method`);this._plugins.set(e.name,e),this._loadOrder.push(e.name);try{e.init(this.kernel),this.kernel.events.emit("plugin_loaded",{name:e.name,version:e.version})}catch(t){throw console.error(`[PluginRegistry] Failed to initialize plugin "${e.name}":`,t),this._plugins.delete(e.name),this._loadOrder.pop(),t}return this}async useAsync(e){if(!e.name)throw new Error("[PluginRegistry] Plugin must have a name");if(this._plugins.has(e.name))return console.warn(`[PluginRegistry] Plugin "${e.name}" already registered. Skipping.`),this;if(!e.init||typeof e.init!="function")throw new Error(`[PluginRegistry] Plugin "${e.name}" must have an init() method`);this._plugins.set(e.name,e),this._loadOrder.push(e.name);try{await e.init(this.kernel),this.kernel.events.emit("plugin_loaded",{name:e.name,version:e.version})}catch(t){throw console.error(`[PluginRegistry] Failed to initialize plugin "${e.name}":`,t),this._plugins.delete(e.name),this._loadOrder.pop(),t}return this}remove(e){const t=this._plugins.get(e);if(!t)return!1;if(t.destroy)try{t.destroy()}catch(i){console.error(`[PluginRegistry] Error destroying plugin "${e}":`,i)}this._plugins.delete(e);const s=this._loadOrder.indexOf(e);return s>-1&&this._loadOrder.splice(s,1),this.kernel.events.emit("plugin_unloaded",{name:e}),!0}get(e){return this._plugins.get(e)}has(e){return this._plugins.has(e)}getPluginNames(){return Array.from(this._plugins.keys())}getLoadOrder(){return[...this._loadOrder]}clear(){for(let e=this._loadOrder.length-1;e>=0;e--)this.remove(this._loadOrder[e])}}class U{constructor(e={}){this.width=e.width||800,this.height=e.height||600,this.backgroundColor=e.backgroundColor||"#000000",e.canvas?(this.canvas=e.canvas,this.canvas.width=this.width,this.canvas.height=this.height):(this.canvas=document.createElement("canvas"),this.canvas.width=this.width,this.canvas.height=this.height,(e.parent?typeof e.parent=="string"?document.querySelector(e.parent):e.parent:document.body).appendChild(this.canvas)),this.ctx=this.canvas.getContext("2d")}clear(){this.ctx.fillStyle=this.backgroundColor,this.ctx.fillRect(0,0,this.width,this.height)}resize(e,t){this.width=e,this.height=t,this.canvas.width=e,this.canvas.height=t}}class R{constructor(){this.running=!1,this.lastTime=0,this.callbacks=new Set,this._frameHandle=null,this._tick=this._tick.bind(this)}add(e){this.callbacks.add(e)}remove(e){this.callbacks.delete(e)}start(){this.running||(this.running=!0,this.lastTime=performance.now(),this._frameHandle!==null&&cancelAnimationFrame(this._frameHandle),this._frameHandle=requestAnimationFrame(this._tick))}stop(){this.running=!1,this._frameHandle!==null&&(cancelAnimationFrame(this._frameHandle),this._frameHandle=null)}_tick(e){if(this._frameHandle=null,!this.running)return;const t=(e-this.lastTime)/1e3;this.lastTime=e;const s=Math.min(t,.1);for(const i of[...this.callbacks])i(s);this.running&&(this._frameHandle=requestAnimationFrame(this._tick))}}function S(a,e){let t=!1;return new Proxy(a,{get(s,i,r){t||(t=!0,console.warn(e));const n=Reflect.get(s,i,r);return typeof n=="function"?n.bind(s):n},set(s,i,r){return t||(t=!0,console.warn(e)),Reflect.set(s,i,r)}})}class g{constructor(){this.gameObject=null,this.kernel=null}get physics(){var e;return(e=this.kernel)==null?void 0:e.systems.get("physics")}get input(){var e;return(e=this.kernel)==null?void 0:e.systems.get("input")}get loader(){var e;return(e=this.kernel)==null?void 0:e.systems.get("loader")}get events(){var e;return(e=this.kernel)==null?void 0:e.events}onAttach(){}onDetach(){}update(e){}render(e){}}class $ extends g{constructor(e="box",t=50,s=50,i=25){super(),this.type=e,this.width=t,this.height=s,this.radius=i,this.tag=null}onAttach(){super.onAttach(),this.physics&&this.physics.add(this)}onDetach(){super.onDetach(),this.physics&&this.physics.remove(this)}getBounds(){const e=this.gameObject;if(this.type==="box")return{left:e.x-this.width/2,right:e.x+this.width/2,top:e.y-this.height/2,bottom:e.y+this.height/2};if(this.type==="circle")return{x:e.x,y:e.y,radius:this.radius}}}class k{constructor(){this.colliders=[],this.kernel=null}init(e){this.kernel=e}fixedUpdate(e){this.step()}destroy(){this.colliders=[]}add(e){this.colliders.push(e)}remove(e){const t=this.colliders.indexOf(e);t>-1&&this.colliders.splice(t,1)}step(){for(let e=0;e<this.colliders.length;e++)for(let t=e+1;t<this.colliders.length;t++){const s=this.colliders[e],i=this.colliders[t];this._intersects(s,s.getBounds(),i,i.getBounds())&&(this._notifyCollision(s.gameObject,i.gameObject),this._notifyCollision(i.gameObject,s.gameObject))}}_notifyCollision(e,t){!e||!e.components||e.components.forEach(s=>{s.onCollisionEnter&&s.onCollisionEnter(t)})}checkOverlap(e,t){const s=[],i=e.getBounds();for(const r of this.colliders){if(r===e||t&&r.tag!==t)continue;const n=r.getBounds();this._intersects(e,i,r,n)&&s.push(r)}return s}_intersects(e,t,s,i){if(e.type==="box"&&s.type==="box")return t.left<i.right&&t.right>i.left&&t.top<i.bottom&&t.bottom>i.top;if(e.type==="circle"&&s.type==="circle"){const r=t.x-i.x,n=t.y-i.y;return Math.sqrt(r*r+n*n)<t.radius+i.radius}return!1}addBody(e){this.add(e)}removeBody(e){this.remove(e)}checkCollision(e,t){return this._intersects(e,e.getBounds(),t,t.getBounds())}raycast(e,t,s){return null}setGravity(e,t){}}const q=S(new k,"[DopamineJS] GlobalPhysics is deprecated. Use kernel.physics instead.");class b{constructor(){this.keys=new Map,this.mouse={x:0,y:0,buttons:new Map},this.kernel=null,this._onKeyDown=this._onKeyDown.bind(this),this._onKeyUp=this._onKeyUp.bind(this),this._onMouseMove=this._onMouseMove.bind(this),this._onMouseDown=this._onMouseDown.bind(this),this._onMouseUp=this._onMouseUp.bind(this)}init(e){this.kernel=e,window.addEventListener("keydown",this._onKeyDown),window.addEventListener("keyup",this._onKeyUp),window.addEventListener("mousemove",this._onMouseMove),window.addEventListener("mousedown",this._onMouseDown),window.addEventListener("mouseup",this._onMouseUp)}destroy(){window.removeEventListener("keydown",this._onKeyDown),window.removeEventListener("keyup",this._onKeyUp),window.removeEventListener("mousemove",this._onMouseMove),window.removeEventListener("mousedown",this._onMouseDown),window.removeEventListener("mouseup",this._onMouseUp)}_onKeyDown(e){this.keys.set(e.key,!0)}_onKeyUp(e){this.keys.set(e.key,!1)}_onMouseMove(e){this.mouse.x=e.clientX,this.mouse.y=e.clientY}_onMouseDown(e){this.mouse.buttons.set(e.button,!0)}_onMouseUp(e){this.mouse.buttons.set(e.button,!1)}isKeyDown(e){return this.keys.get(e)||!1}isMouseButtonDown(e=0){return this.mouse.buttons.get(e)||!1}}const K=S(new b,"[DopamineJS] GlobalInput is deprecated. Use kernel.input instead.");class M{constructor(){this.assets=new Map,this.kernel=null}init(e){this.kernel=e}destroy(){this.assets.clear()}async loadImage(e,t){return new Promise((s,i)=>{const r=new Image;r.onload=()=>{this.assets.set(e,r),s(r)},r.onerror=i,r.src=t})}get(e){return this.assets.get(e)}}const X=S(new M,"[DopamineJS] GlobalLoader is deprecated. Use kernel.loader instead.");class G{constructor(e={}){this.config=e,this._boundUpdate=this._update.bind(this),this.events=new p,this.systems=new A(this),this.plugins=new L(this),this._registerCoreSystems(e),this.fixedTimestep=e.fixedTimestep||1/60,this._accumulator=0,this._maxAccumulator=this.fixedTimestep*5}_registerCoreSystems(e){this.systems.register("ticker",new R,{priority:100});const t={width:e.width,height:e.height,backgroundColor:e.backgroundColor,canvas:e.canvas,...e.renderer};this.systems.register("renderer",new U(t),{priority:90}),this.systems.register("input",new b,{priority:80}),this.systems.register("loader",new M,{priority:70}),this.systems.register("physics",new k,{priority:60})}start(){const e=this.systems.get("ticker");e&&(e.add(this._boundUpdate),e.start()),this.events.emit("kernel_started")}stop(){const e=this.systems.get("ticker");e&&e.stop(),this.events.emit("kernel_stopped")}_update(e){for(this.events.emit(p.Events.TICK,{dt:e}),this._accumulator+=e,this._accumulator>this._maxAccumulator&&(this._accumulator=this._maxAccumulator);this._accumulator>=this.fixedTimestep;)this.events.emit(p.Events.FIXED_UPDATE,{dt:this.fixedTimestep}),this.systems.fixedUpdate(this.fixedTimestep),this._accumulator-=this.fixedTimestep;this.systems.update(e),this.events.emit(p.Events.RENDER,{dt:e})}get physics(){return this.systems.get("physics")}get input(){return this.systems.get("input")}get loader(){return this.systems.get("loader")}get renderer(){return this.systems.get("renderer")}get ticker(){return this.systems.get("ticker")}destroy(){this.stop();const e=this.systems.get("ticker");e&&e.remove(this._boundUpdate),this.plugins.clear(),this.systems.clear(),this.events.clear()}}const _={init(a){},update(a){},fixedUpdate(a){},destroy(){}};class J{constructor(){this.kernel=null}init(e){this.kernel=e}update(e){}fixedUpdate(e){}destroy(){}}const W={..._,addBody(a){},removeBody(a){},checkCollision(a,e){},raycast(a,e,t){},setGravity(a,e){},step(a){}},V={..._,registerSound(a,e){},preloadSounds(a){},play(a,e){},playTone(a,e,t,s){},stop(a){},toggleMute(){},setVolume(a){}},Y={..._,registerSprite(a,e){},registerEffect(a,e){},play(a,e,t,...s){},emit(a){},clear(){},confetti(a,e,t){},coinShower(a,e,t){},sparkle(a,e,t,s){},fire(a,e,t){},starBurst(a,e,t){}};class Q{constructor(){this.gameObjects=[],this.kernel=null}onEnter(){}onExit(){}add(e){return this.kernel&&(e.kernel=this.kernel),this.gameObjects.push(e),e}remove(e){const t=this.gameObjects.indexOf(e);t>-1&&this.gameObjects.splice(t,1)}update(e){for(const t of this.gameObjects)t.update(e)}render(e){for(const t of this.gameObjects)t.render(e)}}class z{constructor(e){this.game=e,this.currentScene=null}run(e){this.currentScene&&this.currentScene.onExit(),this.currentScene=e,this.currentScene.game=this.game,this.game.scene=e,e.onEnter()}}class Z{constructor(e={}){this.config=e,this.kernel=new G(e),this.director=new z(this),this.kernel.systems.register("director",this.director),this.kernel.events.on(p.Events.TICK,this._update.bind(this),10),this.kernel.events.on(p.Events.RENDER,this._render.bind(this),10)}get renderer(){return this.kernel.renderer}get ticker(){return this.kernel.ticker}get physics(){return this.kernel.physics}get input(){return this.kernel.input}start(){this.kernel.start()}stop(){this.kernel.stop()}setScene(e){e.kernel=this.kernel,this.director.run(e)}_update({dt:e}){this.director.currentScene&&this.director.currentScene.update(e)}_render({dt:e}){this.renderer.clear(),this.director.currentScene&&this.director.currentScene.render(this.renderer.ctx)}destroy(){this.kernel.destroy()}}class ee{constructor(e=0,t=0){this.x=e,this.y=t,this.rotation=0,this.scale={x:1,y:1},this.components=[],this.children=[],this.parent=null,this._kernel=null}get kernel(){return this._kernel}set kernel(e){this._kernel=e;for(const t of this.components)t.kernel=e;for(const t of this.children)t.kernel=e}addChild(e){return e.parent=this,e.kernel=this.kernel,this.children.push(e),e}addComponent(e){return e.gameObject=this,e.kernel=this.kernel,this.components.push(e),e.onAttach(),e}getComponent(e){return this.components.find(t=>t instanceof e)}update(e){for(const t of this.components)t.update&&t.update(e);for(const t of this.children)t.update(e)}render(e){e.save(),e.translate(this.x,this.y),e.rotate(this.rotation),e.scale(this.scale.x,this.scale.y);for(const t of this.components)t.render&&t.render(e);for(const t of this.children)t.render(e);e.restore()}}class E{constructor(e=0,t=0){this.x=e,this.y=t}add(e){return this.x+=e.x,this.y+=e.y,this}sub(e){return this.x-=e.x,this.y-=e.y,this}scale(e){return this.x*=e,this.y*=e,this}distance(e){const t=this.x-e.x,s=this.y-e.y;return Math.sqrt(t*t+s*s)}normalize(){const e=Math.sqrt(this.x*this.x+this.y*this.y);return e>0&&(this.x/=e,this.y/=e),this}clone(){return new E(this.x,this.y)}static distance(e,t){return Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2))}}class j extends g{constructor(e){super(),this.image=e,this.anchor={x:.5,y:.5},this.width=e?e.width:0,this.height=e?e.height:0,this.frame={x:0,y:0,w:this.width,h:this.height}}setFrame(e,t,s,i){this.frame={x:e,y:t,w:s,h:i},this.width=s,this.height=i}setTexture(e){this.image=e,e&&this.setFrame(0,0,e.width,e.height)}render(e){if(!this.image)return;const t=this.frame.w,s=this.frame.h,i=-t*this.anchor.x,r=-s*this.anchor.y;e.drawImage(this.image,this.frame.x,this.frame.y,t,s,i,r,t,s)}}class te extends g{constructor(){super(),this.animations={},this.currentAnim=null,this.timer=0,this.currentFrameIndex=0,this.sprite=null}onAttach(){this.sprite=this.gameObject.getComponent(j),this.sprite||console.warn("Animator attached to GameObject without a Sprite component.")}addAnimation(e,t,s=10,i=!0){this.animations[e]={frames:t,fps:s,loop:i}}play(e){this.currentAnim&&this.currentAnim.name===e||this.animations[e]&&(this.currentAnim={name:e,...this.animations[e]},this.currentFrameIndex=0,this.timer=0,this._updateFrame())}update(e){if(!this.currentAnim||!this.sprite)return;this.timer+=e;const t=1/this.currentAnim.fps;this.timer>=t&&(this.timer-=t,this.currentFrameIndex++,this.currentFrameIndex>=this.currentAnim.frames.length&&(this.currentAnim.loop?this.currentFrameIndex=0:this.currentFrameIndex=this.currentAnim.frames.length-1),this._updateFrame())}_updateFrame(){const e=this.currentAnim.frames[this.currentFrameIndex];this.sprite.setFrame(e.x,e.y,e.w,e.h)}}class se{static createGround(e,t,s="#333"){const i=document.createElement("canvas");i.width=e,i.height=t;const r=i.getContext("2d");r.fillStyle=s,r.fillRect(0,0,e,t);for(let n=0;n<1e3;n++){r.fillStyle=Math.random()>.5?"#444":"#222";const o=Math.random()*4+2;r.fillRect(Math.random()*e,Math.random()*t,o,o)}r.strokeStyle="rgba(255,255,255,0.05)",r.lineWidth=2,r.beginPath();for(let n=0;n<=e;n+=64)r.moveTo(n,0),r.lineTo(n,t);for(let n=0;n<=t;n+=64)r.moveTo(0,n),r.lineTo(e,n);return r.stroke(),i}static createCharacterSheet(e,t){const r=document.createElement("canvas");r.width=64*4,r.height=64;const n=r.getContext("2d");for(let o=0;o<4;o++){const l=o*64,c=0,d=l+64/2,m=c+64/2,y=o%2===0?0:-2,v=o===1?5:o===3?-5:0;n.fillStyle=e,n.beginPath(),n.arc(d,m+y,20,0,Math.PI*2),n.fill(),n.fillStyle=t,n.beginPath(),n.arc(d-15,m+y+v,8,0,Math.PI*2),n.arc(d+15,m+y-v,8,0,Math.PI*2),n.fill(),n.fillStyle=ie(e),n.beginPath(),n.arc(d,m+y,15,0,Math.PI*2),n.fill(),t==="#333"&&(n.fillStyle="#000",n.fillRect(d+10,m+y-5,25,6))}return r}}function ie(a,e){return a}class re extends g{constructor(e){super(),this.system=e}play(e,t={}){const s=this.gameObject.x,i=this.gameObject.y;switch(e){case"confetti":this.system.confetti(s,i,t.count);break;case"fire":this.system.fire(s,i,t.count);break;case"sparkle":this.system.sparkle(s,i,t.count,t.color);break;case"blood":this.system.emit({x:s,y:i,count:10,color:["#8a0303","#ff0000"],speed:4,gravity:.1,decay:.05});break;default:this.system.emit({x:s,y:i,...t})}}}class ne extends g{constructor(){super(),this.intensity=0,this.duration=0,this.timer=0,this.originalPos={x:0,y:0},this.isShaking=!1}shake(e=5,t=.2){this.intensity=e,this.duration=t,this.timer=0,this.isShaking=!0,this.timer===0&&(this.originalPos.x=this.gameObject.x,this.originalPos.y=this.gameObject.y)}update(e){if(!this.isShaking)return;if(this.timer+=e,this.timer>=this.duration){this.isShaking=!1,this.gameObject.x=this.originalPos.x,this.gameObject.y=this.originalPos.y;return}const t=(Math.random()-.5)*2*this.intensity,s=(Math.random()-.5)*2*this.intensity;this.gameObject.x=this.originalPos.x+t,this.gameObject.y=this.originalPos.y+s}}h.Animator=te,h.Collider=$,h.Component=g,h.DataService=C,h.Director=z,h.DopamineKernel=G,h.EventBus=p,h.EventEmitter=f,h.Game=Z,h.GameObject=ee,h.GameUI=I,h.GlobalInput=K,h.GlobalLoader=X,h.GlobalPhysics=q,h.IAudioSystem=V,h.IParticleSystem=Y,h.IPhysicsSystem=W,h.ISystem=_,h.Input=b,h.Loader=M,h.ParticleEmitter=re,h.ParticleSystem=O,h.Physics=k,h.PluginRegistry=L,h.Renderer=U,h.RewardSystem=P,h.Scene=Q,h.ScreenShake=ne,h.SoundManager=T,h.Sprite=j,h.System=J,h.SystemRegistry=A,h.TextureGenerator=se,h.Ticker=R,h.Vector2=E,h.createMemoryStorage=w,h.default=H,h.resolveStorage=x,Object.defineProperties(h,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- #game-ui-overlay{position:fixed;top:0;left:0;width:100%;pointer-events:none;z-index:1000;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.game-ui-top-bar{display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0000004d;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.level-badge{display:flex;flex-direction:column;align-items:center;background:linear-gradient(135deg,#667eea,#764ba2);padding:8px 12px;border-radius:12px;box-shadow:0 4px 12px #667eea66;min-width:60px}.level-label{font-size:10px;font-weight:600;color:#fffc;letter-spacing:.5px}.level-number{font-size:24px;font-weight:800;color:#fff;line-height:1}.xp-container{flex:1;display:flex;flex-direction:column;gap:4px}.xp-bar-bg{height:12px;background:#fff3;border-radius:6px;overflow:hidden;box-shadow:inset 0 2px 4px #0003}.xp-bar-fill{height:100%;background:linear-gradient(90deg,#4facfe,#00f2fe);border-radius:6px;transition:width .5s ease-out;box-shadow:0 0 10px #4facfe99}.xp-text{font-size:11px;color:#fff;font-weight:600;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.5)}.streak-badge{display:flex;align-items:center;gap:6px;background:linear-gradient(135deg,#f80,#f40);padding:8px 12px;border-radius:12px;box-shadow:0 4px 12px #f806}.streak-icon{font-size:20px;animation:flicker 2s infinite}.streak-number{font-size:18px;font-weight:800;color:#fff}.achievement-popup{position:fixed;top:80px;right:20px;background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;padding:16px;box-shadow:0 8px 32px #0006;animation:slideIn .5s cubic-bezier(.68,-.55,.265,1.55);z-index:10000;max-width:320px;overflow:hidden}.achievement-shine{position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.3),transparent);animation:shine 2s infinite}.achievement-content{position:relative;display:flex;gap:12px;align-items:center}.achievement-icon{font-size:48px;animation:bounce .6s ease-out}.achievement-info{flex:1}.achievement-title{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.achievement-name{font-size:18px;font-weight:800;color:#fff;margin:2px 0}.achievement-desc{font-size:13px;color:#fffc;margin-bottom:4px}.achievement-xp{display:inline-block;background:#fff3;padding:4px 8px;border-radius:8px;font-size:12px;font-weight:700;color:gold}.level-up-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:#000000d9;display:flex;align-items:center;justify-content:center;z-index:10001;animation:fadeIn .3s ease-out}.level-up-content{text-align:center;animation:scaleIn .5s cubic-bezier(.68,-.55,.265,1.55)}.level-up-title{font-size:32px;font-weight:800;background:linear-gradient(135deg,gold,#ffed4e);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;text-transform:uppercase;letter-spacing:4px;margin-bottom:16px}.level-up-number{font-size:120px;font-weight:900;color:#fff;text-shadow:0 0 20px rgba(255,215,0,.8),0 0 40px rgba(255,215,0,.6),0 0 60px rgba(255,215,0,.4);animation:glow 1s ease-in-out infinite alternate}.level-up-subtitle{font-size:24px;color:#fffc;font-weight:600;margin-top:16px}.floating-text{position:fixed;font-weight:800;text-shadow:2px 2px 0 #000,-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000;pointer-events:none;z-index:9998;animation:floatUp 2s ease-out forwards}@keyframes slideIn{0%{transform:translate(400px);opacity:0}to{transform:translate(0);opacity:1}}@keyframes slideOut{0%{transform:translate(0);opacity:1}to{transform:translate(400px);opacity:0}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes scaleIn{0%{transform:scale(.5);opacity:0}to{transform:scale(1);opacity:1}}@keyframes bounce{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes glow{0%{text-shadow:0 0 20px rgba(255,215,0,.8),0 0 40px rgba(255,215,0,.6),0 0 60px rgba(255,215,0,.4)}to{text-shadow:0 0 30px rgba(255,215,0,1),0 0 60px rgba(255,215,0,.8),0 0 90px rgba(255,215,0,.6)}}@keyframes shine{0%{left:-100%}30%{left:100%}to{left:100%}}@keyframes flicker{0%,to{opacity:1}50%{opacity:.8}}@keyframes floatUp{0%{transform:translateY(0);opacity:1}to{transform:translateY(-100px);opacity:0}}@keyframes xp-pulse{0%{transform:scaleY(1)}50%{transform:scaleY(1.2)}to{transform:scaleY(1)}}@keyframes level-pulse{0%{transform:scale(1)}50%{transform:scale(1.1)}to{transform:scale(1)}}@media (max-width: 480px){.game-ui-top-bar{gap:8px;padding:10px 12px}.level-badge{padding:6px 10px;min-width:50px}.level-number{font-size:20px}.xp-bar-bg{height:10px}.xp-text{font-size:10px}.achievement-popup{right:12px;max-width:calc(100% - 24px)}.level-up-number{font-size:80px}}
1
+ #game-ui-overlay,.game-ui-overlay{position:fixed;top:0;left:0;width:100%;pointer-events:none;z-index:1000;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.game-ui-top-bar{display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0000004d;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.level-badge{display:flex;flex-direction:column;align-items:center;background:linear-gradient(135deg,#667eea,#764ba2);padding:8px 12px;border-radius:12px;box-shadow:0 4px 12px #667eea66;min-width:60px}.level-label{font-size:10px;font-weight:600;color:#fffc;letter-spacing:.5px}.level-number{font-size:24px;font-weight:800;color:#fff;line-height:1}.xp-container{flex:1;display:flex;flex-direction:column;gap:4px}.xp-bar-bg{height:12px;background:#fff3;border-radius:6px;overflow:hidden;box-shadow:inset 0 2px 4px #0003}.xp-bar-fill{height:100%;background:linear-gradient(90deg,#4facfe,#00f2fe);border-radius:6px;transition:width .5s ease-out;box-shadow:0 0 10px #4facfe99}.xp-text{font-size:11px;color:#fff;font-weight:600;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.5)}.streak-badge{display:flex;align-items:center;gap:6px;background:linear-gradient(135deg,#f80,#f40);padding:8px 12px;border-radius:12px;box-shadow:0 4px 12px #f806}.streak-icon{font-size:20px;animation:flicker 2s infinite}.streak-number{font-size:18px;font-weight:800;color:#fff}.achievement-popup{position:fixed;top:80px;right:20px;background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;padding:16px;box-shadow:0 8px 32px #0006;animation:slideIn .5s cubic-bezier(.68,-.55,.265,1.55);z-index:10000;max-width:320px;overflow:hidden}.achievement-shine{position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.3),transparent);animation:shine 2s infinite}.achievement-content{position:relative;display:flex;gap:12px;align-items:center}.achievement-icon{font-size:48px;animation:bounce .6s ease-out}.achievement-info{flex:1}.achievement-title{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.achievement-name{font-size:18px;font-weight:800;color:#fff;margin:2px 0}.achievement-desc{font-size:13px;color:#fffc;margin-bottom:4px}.achievement-xp{display:inline-block;background:#fff3;padding:4px 8px;border-radius:8px;font-size:12px;font-weight:700;color:gold}.level-up-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:#000000d9;display:flex;align-items:center;justify-content:center;z-index:10001;animation:fadeIn .3s ease-out}.level-up-content{text-align:center;animation:scaleIn .5s cubic-bezier(.68,-.55,.265,1.55)}.level-up-title{font-size:32px;font-weight:800;background:linear-gradient(135deg,gold,#ffed4e);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;text-transform:uppercase;letter-spacing:4px;margin-bottom:16px}.level-up-number{font-size:120px;font-weight:900;color:#fff;text-shadow:0 0 20px rgba(255,215,0,.8),0 0 40px rgba(255,215,0,.6),0 0 60px rgba(255,215,0,.4);animation:glow 1s ease-in-out infinite alternate}.level-up-subtitle{font-size:24px;color:#fffc;font-weight:600;margin-top:16px}.floating-text{position:fixed;font-weight:800;text-shadow:2px 2px 0 #000,-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000;pointer-events:none;z-index:9998;animation:floatUp 2s ease-out forwards}@keyframes slideIn{0%{transform:translate(400px);opacity:0}to{transform:translate(0);opacity:1}}@keyframes slideOut{0%{transform:translate(0);opacity:1}to{transform:translate(400px);opacity:0}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes scaleIn{0%{transform:scale(.5);opacity:0}to{transform:scale(1);opacity:1}}@keyframes bounce{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes glow{0%{text-shadow:0 0 20px rgba(255,215,0,.8),0 0 40px rgba(255,215,0,.6),0 0 60px rgba(255,215,0,.4)}to{text-shadow:0 0 30px rgba(255,215,0,1),0 0 60px rgba(255,215,0,.8),0 0 90px rgba(255,215,0,.6)}}@keyframes shine{0%{left:-100%}30%{left:100%}to{left:100%}}@keyframes flicker{0%,to{opacity:1}50%{opacity:.8}}@keyframes floatUp{0%{transform:translateY(0);opacity:1}to{transform:translateY(-100px);opacity:0}}@keyframes xp-pulse{0%{transform:scaleY(1)}50%{transform:scaleY(1.2)}to{transform:scaleY(1)}}@keyframes level-pulse{0%{transform:scale(1)}50%{transform:scale(1.1)}to{transform:scale(1)}}@media (max-width: 480px){.game-ui-top-bar{gap:8px;padding:10px 12px}.level-badge{padding:6px 10px;min-width:50px}.level-number{font-size:20px}.xp-bar-bg{height:10px}.xp-text{font-size:10px}.achievement-popup{right:12px;max-width:calc(100% - 24px)}.level-up-number{font-size:80px}}
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "dopaminejs",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "description": "Lightweight Game Feel Engine - Add juice, rewards, and feedback to HTML5 games",
5
5
  "type": "module",
6
6
  "main": "./dist/dopamine.umd.cjs",
7
7
  "module": "./dist/dopamine.js",
8
8
  "exports": {
9
9
  ".": {
10
+ "types": "./types/index.d.ts",
10
11
  "import": "./dist/dopamine.js",
11
12
  "require": "./dist/dopamine.umd.cjs"
12
13
  },
@@ -14,7 +15,8 @@
14
15
  },
15
16
  "files": [
16
17
  "dist",
17
- "src"
18
+ "src",
19
+ "types"
18
20
  ],
19
21
  "scripts": {
20
22
  "dev": "vite",
@@ -44,5 +46,6 @@
44
46
  "bugs": {
45
47
  "url": "https://github.com/BaryoDev/dopaminejs/issues"
46
48
  },
47
- "homepage": "https://github.com/BaryoDev/dopaminejs#readme"
48
- }
49
+ "homepage": "https://github.com/BaryoDev/dopaminejs#readme",
50
+ "types": "./types/index.d.ts"
51
+ }
@@ -17,6 +17,10 @@ export class DopamineKernel {
17
17
  constructor(config = {}) {
18
18
  this.config = config;
19
19
 
20
+ // Bound once. Binding inside start() would hand the ticker a new
21
+ // function object on every restart, stacking duplicate update loops.
22
+ this._boundUpdate = this._update.bind(this);
23
+
20
24
  // Core registries
21
25
  this.events = new EventBus();
22
26
  this.systems = new SystemRegistry(this);
@@ -76,7 +80,7 @@ export class DopamineKernel {
76
80
  start() {
77
81
  const ticker = this.systems.get('ticker');
78
82
  if (ticker) {
79
- ticker.add(this._update.bind(this));
83
+ ticker.add(this._boundUpdate);
80
84
  ticker.start();
81
85
  }
82
86
 
@@ -153,6 +157,12 @@ export class DopamineKernel {
153
157
  */
154
158
  destroy() {
155
159
  this.stop();
160
+
161
+ const ticker = this.systems.get('ticker');
162
+ if (ticker) {
163
+ ticker.remove(this._boundUpdate);
164
+ }
165
+
156
166
  this.plugins.clear();
157
167
  this.systems.clear();
158
168
  this.events.clear();
@@ -60,12 +60,14 @@ export class EventBus {
60
60
  off(event, callback) {
61
61
  const listeners = this._listeners.get(event);
62
62
  if (listeners) {
63
- // Find and remove the listener with matching callback
64
- for (const listener of listeners) {
63
+ // Remove every registration of this callback. on() stores a fresh
64
+ // wrapper object per call, so the same function can be in here
65
+ // more than once and stopping at the first match would leave a
66
+ // live listener behind.
67
+ for (const listener of [...listeners]) {
65
68
  if (listener.callback === callback) {
66
69
  listeners.delete(listener);
67
70
  this._sortedCache.delete(event);
68
- break;
69
71
  }
70
72
  }
71
73
  }
@@ -89,16 +91,37 @@ export class EventBus {
89
91
 
90
92
  // Call regular listeners
91
93
  for (let i = 0; i < listeners.length; i++) {
92
- listeners[i].callback(data);
94
+ this._invoke(listeners[i].callback, data, event);
93
95
  }
94
96
 
95
- // Call and remove once listeners
97
+ // Call and remove once listeners.
96
98
  const onceListeners = this._onceListeners.get(event);
97
99
  if (onceListeners && onceListeners.size > 0) {
98
- for (const callback of onceListeners) {
99
- callback(data);
100
- }
100
+ // Snapshot and clear *before* dispatching. Iterating the live Set
101
+ // would pick up any listener a callback registers for this same
102
+ // event, firing it immediately and then clearing it unfired.
103
+ const batch = [...onceListeners];
101
104
  onceListeners.clear();
105
+
106
+ for (const callback of batch) {
107
+ this._invoke(callback, data, event);
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Call one listener without letting it abort the dispatch.
114
+ *
115
+ * A throwing listener used to take out every lower-priority listener for
116
+ * that event, which on 'tick' means the game silently stops updating.
117
+ *
118
+ * @private
119
+ */
120
+ _invoke(callback, data, event) {
121
+ try {
122
+ callback(data);
123
+ } catch (error) {
124
+ console.error(`[DopamineJS] Listener for "${event}" threw:`, error);
102
125
  }
103
126
  }
104
127
 
@@ -150,7 +173,7 @@ export class EventBus {
150
173
  hasListeners(event) {
151
174
  const listeners = this._listeners.get(event);
152
175
  const onceListeners = this._onceListeners.get(event);
153
- return (listeners && listeners.size > 0) || (onceListeners && onceListeners.size > 0);
176
+ return Boolean(listeners?.size) || Boolean(onceListeners?.size);
154
177
  }
155
178
  }
156
179
 
@@ -3,16 +3,23 @@
3
3
  * Handles synthesized sound effects using Web Audio API
4
4
  */
5
5
 
6
+ import { resolveStorage } from '../utils/storage.js';
7
+
6
8
  export class SoundManager {
7
9
  constructor(config = {}) {
8
10
  this.audioContext = null;
9
11
  this.storageKey = config.storageKey || 'dopamineSoundsMuted';
10
- this.muted = localStorage.getItem(this.storageKey) === 'true';
12
+ this.storage = config.storage || resolveStorage();
13
+ this.muted = this.storage.getItem(this.storageKey) === 'true';
11
14
 
12
15
  // Asset management
13
16
  this.assets = new Map(); // key -> AudioBuffer
14
17
  this.customSounds = config.customSounds || {}; // key -> url
15
18
 
19
+ // Master gain, created with the AudioContext on first use.
20
+ this.masterGain = null;
21
+ this._volume = config.volume ?? 1;
22
+
16
23
  // Preload custom sounds if provided
17
24
  if (Object.keys(this.customSounds).length > 0) {
18
25
  this.preloadSounds(this.customSounds);
@@ -24,12 +31,53 @@ export class SoundManager {
24
31
  */
25
32
  initAudio() {
26
33
  if (!this.audioContext) {
27
- const AudioContext = window.AudioContext || window.webkitAudioContext;
28
- this.audioContext = new AudioContext();
34
+ const Ctor = typeof window !== 'undefined'
35
+ ? (window.AudioContext || window.webkitAudioContext)
36
+ : undefined;
37
+
38
+ // No Web Audio API: SSR, a worker, or a very old browser. Stay
39
+ // silent rather than taking the whole game down.
40
+ if (!Ctor) return false;
41
+
42
+ this.audioContext = new Ctor();
43
+ this.masterGain = this.audioContext.createGain();
44
+ this.masterGain.gain.value = this._volume;
45
+ this.masterGain.connect(this.audioContext.destination);
29
46
  }
47
+
30
48
  if (this.audioContext.state === 'suspended') {
31
49
  this.audioContext.resume();
32
50
  }
51
+
52
+ return true;
53
+ }
54
+
55
+ /**
56
+ * Set master output volume.
57
+ * @param {number} value - 0..1
58
+ */
59
+ setVolume(value) {
60
+ this._volume = Math.max(0, Math.min(1, value));
61
+ if (this.masterGain) {
62
+ this.masterGain.gain.value = this._volume;
63
+ }
64
+ return this._volume;
65
+ }
66
+
67
+ /**
68
+ * Current master output volume.
69
+ * @returns {number}
70
+ */
71
+ getVolume() {
72
+ return this._volume;
73
+ }
74
+
75
+ /**
76
+ * Node every sound routes through, so volume and mute apply uniformly.
77
+ * @private
78
+ */
79
+ _output() {
80
+ return this.masterGain || this.audioContext.destination;
33
81
  }
34
82
 
35
83
  /**
@@ -38,7 +86,7 @@ export class SoundManager {
38
86
  */
39
87
  toggleMute() {
40
88
  this.muted = !this.muted;
41
- localStorage.setItem(this.storageKey, this.muted);
89
+ this.storage.setItem(this.storageKey, this.muted);
42
90
  return this.muted;
43
91
  }
44
92
 
@@ -92,7 +140,7 @@ export class SoundManager {
92
140
  */
93
141
  async play(key) {
94
142
  if (this.muted) return;
95
- this.initAudio();
143
+ if (!this.initAudio()) return;
96
144
 
97
145
  // 1. Try custom asset first
98
146
  if (this.assets.has(key)) {
@@ -127,7 +175,7 @@ export class SoundManager {
127
175
  _playBuffer(buffer) {
128
176
  const source = this.audioContext.createBufferSource();
129
177
  source.buffer = buffer;
130
- source.connect(this.audioContext.destination);
178
+ source.connect(this._output());
131
179
  source.start(0);
132
180
  }
133
181
 
@@ -136,18 +184,20 @@ export class SoundManager {
136
184
  */
137
185
  playTone(frequency, duration, type = 'sine', volume = 0.3) {
138
186
  if (this.muted) return;
139
- this.initAudio();
187
+ if (!this.initAudio()) return;
140
188
 
141
189
  const oscillator = this.audioContext.createOscillator();
142
190
  const gainNode = this.audioContext.createGain();
143
191
 
144
192
  oscillator.connect(gainNode);
145
- gainNode.connect(this.audioContext.destination);
193
+ gainNode.connect(this._output());
146
194
 
147
195
  oscillator.frequency.value = frequency;
148
196
  oscillator.type = type;
149
197
 
150
- gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime);
198
+ // exponentialRampToValueAtTime cannot reach or start from zero.
199
+ const peak = Math.max(0.0001, volume);
200
+ gainNode.gain.setValueAtTime(peak, this.audioContext.currentTime);
151
201
  gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
152
202
 
153
203
  oscillator.start(this.audioContext.currentTime);
@@ -3,9 +3,11 @@
3
3
  * Handles local storage persistence
4
4
  */
5
5
 
6
+ import { resolveStorage } from '../utils/storage.js';
7
+
6
8
  export class DataService {
7
9
  constructor(config = {}) {
8
- this.storage = config.storage || window.localStorage;
10
+ this.storage = config.storage || resolveStorage();
9
11
  this.prefix = config.prefix || 'dopamine_';
10
12
  }
11
13