dopaminejs 2.1.0 → 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,4 +1,4 @@
1
- (function(h,g){typeof exports=="object"&&typeof module<"u"?g(exports):typeof define=="function"&&define.amd?define(["exports"],g):(h=typeof globalThis<"u"?globalThis:h||self,g(h.Dopamine={}))})(this,function(h){"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(){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(){this.player&&(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.emit("xp_gained",{amount:e,reason:t,leveledUp:r,newLevel:i}),r&&this.emit("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){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(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.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}_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 M(){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 E(){try{if(typeof localStorage>"u")return M();const a="__dopamine_probe__";return localStorage.setItem(a,"1"),localStorage.removeItem(a),localStorage}catch{return M()}}class P{constructor(e={}){this.storage=e.storage||E(),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 C="game-ui-overlay";class D{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(C)||(this.container.id=C),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
3
  <div class="level-badge">
4
4
  <span class="level-label">LVL</span>
@@ -34,4 +34,4 @@
34
34
  <div class="level-up-number"></div>
35
35
  <div class="level-up-subtitle">Amazing progress!</div>
36
36
  </div>
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 I{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:_=5}=e;for(let j=0;j<i;j++){const u=this._getParticle(),B=y+(Math.random()-.5)*m,F=Math.random()*r;u.x=t,u.y=s,u.vx=Math.cos(B)*F,u.vy=Math.sin(B)*F,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=_*(.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 O{constructor(e={}){this.audioContext=null,this.storageKey=e.storageKey||"dopamineSoundsMuted",this.storage=e.storage||E(),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 N{constructor(e={}){this.config=e,this.dataService=new P(e.data||{}),this.rewardSystem=new b(this.dataService,e.rewards||{}),this.soundManager=new O(e.sound||{}),this.particleSystem=new I(e.particles||{}),this.gameUI=new D(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 T{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 A{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 L{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 U{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))}}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 H 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 w{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 $=new w;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 q=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 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 K=new S;console.warn("[DopamineJS] GlobalLoader is deprecated. Use kernel.loader instead.");class R{constructor(e={}){this.config=e,this._boundUpdate=this._update.bind(this),this.events=new p,this.systems=new T(this),this.plugins=new A(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 U,{priority:100});const t={width:e.width,height:e.height,backgroundColor:e.backgroundColor,canvas:e.canvas,...e.renderer};this.systems.register("renderer",new L(t),{priority:90}),this.systems.register("input",new x,{priority:80}),this.systems.register("loader",new S,{priority:70}),this.systems.register("physics",new w,{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 v={init(a){},update(a){},fixedUpdate(a){},destroy(){}};class X{constructor(){this.kernel=null}init(e){this.kernel=e}update(e){}fixedUpdate(e){}destroy(){}}const J={...v,addBody(a){},removeBody(a){},checkCollision(a,e){},raycast(a,e,t){},setGravity(a,e){},step(a){}},W={...v,registerSound(a,e){},preloadSounds(a){},play(a,e){},playTone(a,e,t,s){},stop(a){},toggleMute(){},setVolume(a){}},V={...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 Y{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 Q{constructor(e={}){this.config=e,this.kernel=new R(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 Z{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 G 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,r=-s*this.anchor.y;e.drawImage(this.image,this.frame.x,this.frame.y,t,s,i,r,t,s)}}class ee extends f{constructor(){super(),this.animations={},this.currentAnim=null,this.timer=0,this.currentFrameIndex=0,this.sprite=null}onAttach(){this.sprite=this.gameObject.getComponent(G),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 te{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,_=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+_,8,0,Math.PI*2),n.arc(d+15,m+y-_,8,0,Math.PI*2),n.fill(),n.fillStyle=se(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 se(a,e){return a}class ie 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 re 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}}h.Animator=ee,h.Collider=H,h.Component=f,h.DataService=P,h.Director=z,h.DopamineKernel=R,h.EventBus=p,h.Game=Q,h.GameObject=Z,h.GameUI=D,h.GlobalInput=q,h.GlobalLoader=K,h.GlobalPhysics=$,h.IAudioSystem=W,h.IParticleSystem=V,h.IPhysicsSystem=J,h.ISystem=v,h.Input=x,h.Loader=S,h.ParticleEmitter=ie,h.ParticleSystem=I,h.Physics=w,h.PluginRegistry=A,h.Renderer=L,h.RewardSystem=b,h.Scene=Y,h.ScreenShake=re,h.SoundManager=O,h.Sprite=G,h.System=X,h.SystemRegistry=T,h.TextureGenerator=te,h.Ticker=U,h.Vector2=k,h.default=N,Object.defineProperties(h,{__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/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "dopaminejs",
3
- "version": "2.1.0",
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"
49
+ "homepage": "https://github.com/BaryoDev/dopaminejs#readme",
50
+ "types": "./types/index.d.ts"
48
51
  }
@@ -6,11 +6,62 @@
6
6
  import { EventEmitter } from './EventEmitter.js';
7
7
 
8
8
  export class RewardSystem extends EventEmitter {
9
+ /**
10
+ * @param {Object} dataService - Persistence, see DataService
11
+ * @param {Object} [config]
12
+ * @param {Object} [config.achievements] - Extra achievement definitions
13
+ * @param {Object} [config.events] - Kernel EventBus to mirror events onto
14
+ * @param {Object} [config.kernel] - Kernel; its `events` bus is used if present
15
+ */
9
16
  constructor(dataService, config = {}) {
10
17
  super();
11
18
  this.dataService = dataService;
12
19
  this.player = null;
13
20
  this.achievements = { ...this._initAchievements(), ...(config.achievements || {}) };
21
+
22
+ // Optional bridge to the kernel bus. EventBus.Events already declared
23
+ // XP_GAINED, LEVEL_UP and ACHIEVEMENT_UNLOCKED with nothing emitting
24
+ // them; this connects the two event systems without changing the
25
+ // existing rewardSystem.on(...) surface that games already use.
26
+ this.events = config.events || config.kernel?.events || null;
27
+
28
+ // Write batching. Every mutation used to hit storage directly, so one
29
+ // recordGame produced four full serializations of the player object.
30
+ this._batchDepth = 0;
31
+ this._pendingSave = false;
32
+ }
33
+
34
+ /**
35
+ * Emit locally, and mirror onto the kernel bus when one is attached.
36
+ * @private
37
+ */
38
+ _publish(event, data) {
39
+ this.emit(event, data);
40
+ if (this.events) {
41
+ this.events.emit(event, data);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Run `fn` with saves collapsed into a single write at the end.
47
+ *
48
+ * Nested batches are counted, so an inner operation that batches on its
49
+ * own still results in exactly one write for the outermost call.
50
+ *
51
+ * @param {Function} fn
52
+ * @private
53
+ */
54
+ async _batch(fn) {
55
+ this._batchDepth++;
56
+ try {
57
+ return await fn();
58
+ } finally {
59
+ this._batchDepth--;
60
+ if (this._batchDepth === 0 && this._pendingSave) {
61
+ this._pendingSave = false;
62
+ await this._write();
63
+ }
64
+ }
14
65
  }
15
66
 
16
67
  /**
@@ -84,10 +135,24 @@ export class RewardSystem extends EventEmitter {
84
135
  * Save player data
85
136
  */
86
137
  async save() {
87
- if (this.player) {
88
- this.player.lastPlayedAt = Date.now();
89
- await this.dataService.save('player', this.player);
138
+ if (!this.player) return;
139
+
140
+ // Inside a batch, mark dirty and let the outermost call do the write.
141
+ if (this._batchDepth > 0) {
142
+ this._pendingSave = true;
143
+ return;
90
144
  }
145
+
146
+ await this._write();
147
+ }
148
+
149
+ /**
150
+ * The actual persistence call. Bypasses batching.
151
+ * @private
152
+ */
153
+ async _write() {
154
+ this.player.lastPlayedAt = Date.now();
155
+ await this.dataService.save('player', this.player);
91
156
  }
92
157
 
93
158
  /**
@@ -117,10 +182,10 @@ export class RewardSystem extends EventEmitter {
117
182
  await this.save();
118
183
 
119
184
  // Notify listeners
120
- this.emit('xp_gained', { amount, reason, leveledUp, newLevel });
185
+ this._publish('xp_gained', { amount, reason, leveledUp, newLevel });
121
186
 
122
187
  if (leveledUp) {
123
- this.emit('level_up', { oldLevel, newLevel });
188
+ this._publish('level_up', { oldLevel, newLevel });
124
189
  }
125
190
 
126
191
  return { leveledUp, newLevel, xpGained: amount };
@@ -165,6 +230,15 @@ export class RewardSystem extends EventEmitter {
165
230
  * @param {Object} result - Game-specific result data
166
231
  */
167
232
  async recordGame(gameName, result) {
233
+ // Batched: addXP and each unlockAchievement below also save, which
234
+ // meant four full serializations of the player object per game.
235
+ return this._batch(() => this._recordGame(gameName, result));
236
+ }
237
+
238
+ /**
239
+ * @private
240
+ */
241
+ async _recordGame(gameName, result) {
168
242
  // Initialize stats for this game if not exists
169
243
  if (!this.player.stats[gameName]) {
170
244
  this.player.stats[gameName] = { totalPlays: 0, highScore: 0 };
@@ -179,7 +253,7 @@ export class RewardSystem extends EventEmitter {
179
253
  // Update high score if applicable
180
254
  if (result.score !== undefined && result.score > stats.highScore) {
181
255
  stats.highScore = result.score;
182
- this.emit('new_high_score', { gameName, score: result.score });
256
+ this._publish('new_high_score', { gameName, score: result.score });
183
257
  }
184
258
 
185
259
  // Merge other result data into stats
@@ -254,7 +328,7 @@ export class RewardSystem extends EventEmitter {
254
328
  await this.addXP(Number.isFinite(achievement.xp) ? achievement.xp : 0,
255
329
  `Achievement: ${achievement.name}`);
256
330
 
257
- this.emit('achievement_unlocked', achievement);
331
+ this._publish('achievement_unlocked', achievement);
258
332
  await this.save();
259
333
 
260
334
  return true;
package/src/index.js CHANGED
@@ -12,6 +12,12 @@ export { PluginRegistry } from './core/PluginRegistry.js';
12
12
  // Export System Interfaces
13
13
  export * from './interfaces/index.js';
14
14
 
15
+ // The emitter RewardSystem extends, so consumers can type their listeners.
16
+ export { EventEmitter } from './dopamine/core/EventEmitter.js';
17
+
18
+ // Storage resolution, useful for tests and for SSR where localStorage is absent.
19
+ export { createMemoryStorage, resolveStorage } from './dopamine/utils/storage.js';
20
+
15
21
  // Export New Engine Core
16
22
  export { Game } from './core/Game.js';
17
23
  export { Scene } from './core/Scene.js';
@@ -1,3 +1,5 @@
1
+ import { deprecatedGlobal } from './deprecate.js';
2
+
1
3
  /**
2
4
  * Input System
3
5
  * Tracks keyboard and mouse input.
@@ -71,7 +73,9 @@ export class Input {
71
73
  }
72
74
  }
73
75
 
74
- // DEPRECATED: Keep for backward compatibility
75
- export const GlobalInput = new Input();
76
- // Note: GlobalInput won't have listeners attached unless init() is called
77
- console.warn('[DopamineJS] GlobalInput is deprecated. Use kernel.input instead.');
76
+ // DEPRECATED: kept for v1 compatibility. Warns on first use, not on import.
77
+ // Note: listeners are not attached unless init() is called.
78
+ export const GlobalInput = deprecatedGlobal(
79
+ new Input(),
80
+ '[DopamineJS] GlobalInput is deprecated. Use kernel.input instead.'
81
+ );
@@ -1,3 +1,5 @@
1
+ import { deprecatedGlobal } from './deprecate.js';
2
+
1
3
  /**
2
4
  * Asset Loader
3
5
  * Loads and caches images, audio, etc.
@@ -39,6 +41,8 @@ export class Loader {
39
41
  }
40
42
  }
41
43
 
42
- // DEPRECATED: Keep for backward compatibility
43
- export const GlobalLoader = new Loader();
44
- console.warn('[DopamineJS] GlobalLoader is deprecated. Use kernel.loader instead.');
44
+ // DEPRECATED: kept for v1 compatibility. Warns on first use, not on import.
45
+ export const GlobalLoader = deprecatedGlobal(
46
+ new Loader(),
47
+ '[DopamineJS] GlobalLoader is deprecated. Use kernel.loader instead.'
48
+ );
@@ -1,3 +1,5 @@
1
+ import { deprecatedGlobal } from './deprecate.js';
2
+
1
3
  import { Collider } from '../core/Collider.js';
2
4
 
3
5
  export class Physics {
@@ -112,6 +114,8 @@ export class Physics {
112
114
  setGravity(x, y) { /* TODO: Add gravity support */ }
113
115
  }
114
116
 
115
- // DEPRECATED: Keep for backward compatibility, but log warning
116
- export const GlobalPhysics = new Physics();
117
- console.warn('[DopamineJS] GlobalPhysics is deprecated. Use kernel.physics instead.');
117
+ // DEPRECATED: kept for v1 compatibility. Warns on first use, not on import.
118
+ export const GlobalPhysics = deprecatedGlobal(
119
+ new Physics(),
120
+ '[DopamineJS] GlobalPhysics is deprecated. Use kernel.physics instead.'
121
+ );
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Deprecation helper for the v1 global singletons.
3
+ *
4
+ * These used to warn at module scope, which meant importing the package
5
+ * printed a warning for every legacy global whether or not the consumer
6
+ * touched one. A deprecation notice should cost you something only when you
7
+ * use the deprecated thing, otherwise it is just noise that trains people to
8
+ * ignore warnings.
9
+ *
10
+ * @param {Object} instance - The real system to delegate to
11
+ * @param {string} message - Warning text, emitted once on first use
12
+ * @returns {Object} A proxy that warns on first property access
13
+ */
14
+ export function deprecatedGlobal(instance, message) {
15
+ let warned = false;
16
+
17
+ return new Proxy(instance, {
18
+ get(target, prop, receiver) {
19
+ if (!warned) {
20
+ warned = true;
21
+ console.warn(message);
22
+ }
23
+
24
+ const value = Reflect.get(target, prop, receiver);
25
+ // Methods must stay bound to the real instance, not the proxy,
26
+ // or internal `this` access re-enters the trap on every field.
27
+ return typeof value === 'function' ? value.bind(target) : value;
28
+ },
29
+
30
+ set(target, prop, value) {
31
+ if (!warned) {
32
+ warned = true;
33
+ console.warn(message);
34
+ }
35
+ return Reflect.set(target, prop, value);
36
+ }
37
+ });
38
+ }