dopaminejs 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # dopaminejs
2
2
 
3
- > Game Feel Engine for the web. Add juice, rewards, and feedback to HTML5 games.
3
+ > Progression mechanics for web apps. XP, levels, achievements, and timezone-correct daily streaks, with the particles and sound to make them land.
4
4
 
5
- Zero runtime dependencies. Works with any renderer, or use the one included.
5
+ Zero runtime dependencies. Works with any framework.
6
6
 
7
7
  ```bash
8
8
  npm install dopaminejs
@@ -11,18 +11,18 @@ npm install dopaminejs
11
11
  ## Quick start
12
12
 
13
13
  ```javascript
14
- import Dopamine from 'dopaminejs';
14
+ // Named imports — the recommended way
15
+ import { RewardSystem, DataService } from 'dopaminejs';
15
16
  import 'dopaminejs/style.css';
16
17
 
17
- const dopamine = new Dopamine();
18
- await dopamine.init();
19
-
20
- // Wire it to your game loop
21
- await dopamine.rewardSystem.recordGame('breakout', { score: 4200 });
18
+ const rewards = new RewardSystem(new DataService());
19
+ await rewards.init();
20
+ await rewards.addXP(100);
22
21
  ```
23
22
 
24
- That gives you an XP bar, level-up screens, achievement popups, confetti, and
25
- synthesized sound effects, with progress persisted to `localStorage`.
23
+ > **v1.x compatibility:** The package also has a default export (`import Dopamine from 'dopaminejs'`)
24
+ > that wraps the progression layer as it existed before v2. It is preserved for backward
25
+ > compatibility but new code should use named imports.
26
26
 
27
27
  ## What's in the box
28
28
 
@@ -0,0 +1,37 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const l=require("./engine.cjs");class T{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()}}const O=new Set(["__proto__","constructor","prototype"]),f=r=>typeof r=="string"&&r.length>0&&!O.has(r),v=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),y=r=>r!==null&&typeof r=="object"&&!Array.isArray(r),g=(r,e,t=0)=>{const s=Number(r);return Number.isInteger(s)&&s>=t?s:e};class E extends T{constructor(e,t={}){super(),this.dataService=e,this.player=null,this.achievements={...this._initAchievements(),...t.achievements||{}},this.events=t.events||t.kernel?.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);this.player=this._migrate(e);const t=this._updateDailyStreak();return await this.save(),t&&this._publishStreak(),this.player}_requirePlayer(){if(!this.player)throw new Error("[DopamineJS] RewardSystem.init() must be awaited before use")}_publishStreak(){const{current:e,longest:t}=this.player.streak;this._publish("streak_updated",{current:e,longest:t})}_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();if(!y(e))return t;const s=Number(e.xp),i=Number.isFinite(s)&&s>=0?s:0,n=this._calculateLevel(i),a=y(e.streak)?e.streak:{},o=g(a.current,1,1),c=typeof a.lastPlayDate=="string"&&/^\d{4}-\d{2}-\d{2}$/.test(a.lastPlayDate)?a.lastPlayDate:t.streak.lastPlayDate;return{...t,...e,name:typeof e.name=="string"?e.name:t.name,xp:i,level:Math.max(n,g(e.level,n,1)),totalGamesPlayed:g(e.totalGamesPlayed,0),createdAt:Number.isFinite(e.createdAt)?e.createdAt:t.createdAt,lastPlayedAt:Number.isFinite(e.lastPlayedAt)?e.lastPlayedAt:t.lastPlayedAt,streak:{current:o,longest:Math.max(o,g(a.longest,o,1)),lastPlayDate:c},achievements:this._migrateAchievements(e.achievements),stats:this._migrateStats(e.stats)}}_migrateAchievements(e){const t={};if(!y(e))return t;for(const s of Object.keys(e)){const i=e[s];!f(s)||!y(i)||(t[s]={unlockedAt:Number.isFinite(i.unlockedAt)?i.unlockedAt:Date.now(),seen:i.seen===!0})}return t}_migrateStats(e){const t={};if(!y(e))return t;for(const s of Object.keys(e)){const i=e[s];if(!f(s)||!y(i))continue;const n={};for(const a of Object.keys(i))f(a)&&Number.isFinite(i[a])&&(n[a]=i[a]);n.totalPlays=g(i.totalPlays,0),n.highScore=Number.isFinite(i.highScore)?i.highScore:0,t[s]=n}return t}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)===!1&&this._publish("save_failed",{key:"player"})}async addXP(e,t=""){if(!Number.isFinite(e))throw new TypeError(`[DopamineJS] addXP expects a finite number, received ${e}`);this._requirePlayer();const s=this.player.level;e=Math.max(e,-this.player.xp),this.player.xp+=e;const i=Math.max(s,this._calculateLevel(this.player.xp)),n=i>s;n&&(this.player.level=i),await this.save();const a=this.player.xp;return this._publish("xp_gained",{amount:e,reason:t,leveledUp:n,newLevel:i,total:a}),n&&this._publish("level_up",{oldLevel:s,newLevel:i,totalXP:a}),{leveledUp:n,newLevel:i,xpGained:e}}_calculateLevel(e){if(!(e>0))return 1;const t=Math.floor((1+Math.sqrt(1+4*e/50))/2);return Math.max(1,t)}getXPForNextLevel(){this._requirePlayer();const e=this.player.level+1,t=50*e*(e-1),s=50*this.player.level*(this.player.level-1),i=t-s,n=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:n}}async recordGame(e,t={}){if(this._requirePlayer(),!f(e))throw new TypeError(`[DopamineJS] recordGame expects a game name, received ${String(e)}`);if(!y(t))throw new TypeError("[DopamineJS] recordGame expects a result object");if(t.score!==void 0&&!Number.isFinite(t.score))throw new TypeError(`[DopamineJS] recordGame expects a finite score, received ${String(t.score)}`);return this._batch(()=>this._recordGame(e,t))}async _recordGame(e,t){this._updateDailyStreak()&&this._publishStreak(),v(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(n=>{if(n!=="score"&&f(n)){const a=v(s,n)&&Number.isFinite(s[n])?s[n]:0;s[n]=a+(Number.isFinite(t[n])?t[n]:0)}});let i=10;t.score&&(i+=Math.max(0,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)){if(v(this.player.achievements,i))continue;let a=!1;try{a=n.check(this.player,e,t)}catch(o){console.error(`[DopamineJS] Achievement "${i}" check threw:`,o);continue}a&&await this.unlockAchievement(i)&&s.push(n)}return s}async unlockAchievement(e){if(this._requirePlayer(),!f(e)||!v(this.achievements,e)||v(this.player.achievements,e))return!1;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,id:e}),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)v(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!1;const s=this._getYesterdayDateString();return 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,!0}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 S(){const r=new Map;return{getItem:e=>r.has(e)?r.get(e):null,setItem:(e,t)=>{r.set(e,String(t))},removeItem:e=>{r.delete(e)},clear:()=>{r.clear()}}}function w(){try{if(typeof localStorage>"u")return S();const r="__dopamine_probe__";return localStorage.setItem(r,"1"),localStorage.removeItem(r),localStorage}catch{return S()}}class D{constructor(e={}){this.storage=e.storage||w(),this.prefix=e.prefix||"dopamine_",this._tail=null}_write(e){const t=this._tail?this._tail.then(e):e();if(typeof t?.then=="function"){const s=Promise.resolve(t).catch(()=>{}).then(()=>{this._tail===s&&(this._tail=null)});this._tail=s}return t}async save(e,t){try{const s=JSON.stringify(t);return await this._write(()=>this.storage.setItem(this.prefix+e,s)),!0}catch(s){return console.error("Error saving data:",s),!1}}async load(e,t=null){try{await this._tail;const s=await 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){await this._write(()=>this.storage.removeItem(this.prefix+e))}}const A="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(A)||(this.container.id=A),this.container.innerHTML=`
2
+ <div class="game-ui-top-bar">
3
+ <div class="level-badge">
4
+ <span class="level-label">LVL</span>
5
+ <span class="level-number">1</span>
6
+ </div>
7
+
8
+ <div class="xp-container">
9
+ <div class="xp-bar-bg">
10
+ <div class="xp-bar-fill" style="width: 0%"></div>
11
+ </div>
12
+ <div class="xp-text">0 / 100 XP</div>
13
+ </div>
14
+
15
+ <div class="streak-badge">
16
+ <span class="streak-icon">🔥</span>
17
+ <span class="streak-number">1</span>
18
+ </div>
19
+ </div>
20
+ `,this.liveRegion=document.createElement("div"),this.liveRegion.className="dopamine-live-region",this.liveRegion.setAttribute("aria-live","polite"),this.liveRegion.setAttribute("role","status"),Object.assign(this.liveRegion.style,{position:"absolute",width:"1px",height:"1px",overflow:"hidden",clipPath:"inset(50%)",whiteSpace:"nowrap"}),this.container.appendChild(this.liveRegion),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}_announce(e){this.liveRegion&&(this.liveRegion.textContent=e)}_replayAnimation(e,t){e&&(e.style.animation="none",this._defer(()=>{e.style.animation=t},10))}updateXP(e,t,s,i){const n=Number.isFinite(i)?i:s>0?e/s:0;this.xpBar.style.width=`${Math.max(0,Math.min(100,n*100))}%`,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%)":this.streakBadge.style.background=""}showAchievement(e){const t=document.createElement("div");t.className="achievement-popup",t.innerHTML=`
21
+ <div class="achievement-shine"></div>
22
+ <div class="achievement-content">
23
+ <div class="achievement-icon"></div>
24
+ <div class="achievement-info">
25
+ <div class="achievement-title">Achievement Unlocked!</div>
26
+ <div class="achievement-name"></div>
27
+ <div class="achievement-desc"></div>
28
+ <div class="achievement-xp"></div>
29
+ </div>
30
+ </div>
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._announce(`Achievement unlocked: ${e.name??""}`),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
+ <div class="level-up-content">
33
+ <div class="level-up-title">LEVEL UP!</div>
34
+ <div class="level-up-number"></div>
35
+ <div class="level-up-subtitle">Amazing progress!</div>
36
+ </div>
37
+ `,s.querySelector(".level-up-number").textContent=t,this._mount(s),this._announce(`Level up: ${t}`),this._defer(()=>{for(let i=0;i<5;i++)this._defer(()=>{const n=Math.random()*window.innerWidth,a=Math.random()*window.innerHeight*.6;this.particleSystem.confetti(n,a,30)},i*200)},300),this._defer(()=>{s.style.opacity="0",this._defer(()=>this._remove(s),500)},2500)}showFloatingText(e,t,s,i="#ffd700",n="24px"){const a=document.createElement("div");a.className="floating-text",a.textContent=e,a.style.left=t+"px",a.style.top=s+"px",a.style.color=i,a.style.fontSize=n,this._mount(a),this._defer(()=>this._remove(a),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.showFloatingText(e,s,i,"#ffd700","36px"),this.particleSystem.starBurst(s,i,12),this.particleSystem.confetti(s,i,25)):t==="rare"?(this.showFloatingText(e,s,i,"#ff6b6b","32px"),this.particleSystem.fire(s,i,20)):this.showFloatingText(e,s,i,"#fff","24px"),this._announce(e)}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 n=document.createElement("div");n.className="summary-metrics",n.style.cssText="width: 100%; margin-bottom: 20px;";for(const[o,c]of Object.entries(e.metrics||{})){const d=document.createElement("div");d.style.cssText="display:flex;justify-content:space-between;width:100%";const u=document.createElement("span");u.textContent=`${o}:`;const m=document.createElement("strong");m.textContent=c,d.append(u,m),n.appendChild(d)}t.append(s,i,n);const a=document.createElement("button");if(a.className="btn",a.textContent="Play Again",a.addEventListener("click",e.onReplay||(()=>window.location.reload())),t.appendChild(a),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(){for(const e of this._timers)clearTimeout(e);this._timers.clear();for(const e of this._detached)e.remove();this._detached=[],this.container?.remove(),this.container=null,this.liveRegion=null,this.xpBar=null,this.xpText=null,this.levelBadge=null,this.streakBadge=null,this.levelDisplay=null,this.streakDisplay=null}}class L{constructor(e={}){this.container=e.container?typeof e.container=="string"?document.querySelector(e.container):e.container:document.body,this.container||(console.warn(`[DopamineJS] Particle container '${e.container}' not found. Using document.body.`),this.container=document.body),this.maxParticles=Number.isFinite(e.maxParticles)?Math.max(0,Math.floor(e.maxParticles)):5e3,this._reducedMotion=e.respectReducedMotion!==!1&&typeof window.matchMedia=="function"?window.matchMedia("(prefers-reduced-motion: reduce)"):null,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._lastFrameTime=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 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:a=.1,decay:o=.02,life:c=1,color:d="#ffd700",sprite:u=null,spread:m=Math.PI*2,angle:p=0,size:_=5,type:z=null}=e,b=this._allowed(i);for(let P=0;P<b;P++){const h=this._getParticle(),M=p+(Math.random()-.5)*m,k=Math.random()*n;h.x=t,h.y=s,h.vx=Math.cos(M)*k,h.vy=Math.sin(M)*k,h.life=c,h.decay=o*(.8+Math.random()*.4),h.gravity=a,h.color=Array.isArray(d)?d[Math.floor(Math.random()*d.length)]:d,h.size=_*(.8+Math.random()*.4),h.sprite=u,h.type=z,h.rotation=Math.random()*Math.PI*2,h.rotationSpeed=(Math.random()-.5)*.2,this.particles.push(h)}b>0&&this._startAnimation()}_allowed(e){if(this._reducedMotion?.matches||!Number.isFinite(e))return 0;const t=this.maxParticles-this.particles.length;return Math.max(0,Math.min(Math.floor(e),t))}_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){const i=this._allowed(s);for(let n=0;n<i;n++){const a=this._getParticle(),o=Math.PI*2/i*n,c=4;a.x=e,a.y=t,a.vx=Math.cos(o)*c,a.vy=Math.sin(o)*c,a.life=1,a.decay=.03,a.gravity=0,a.color="#ffffff",a.size=15,a.sprite=null,a.type="star",a.rotation=o,a.rotationSpeed=0,this.particles.push(a)}i>0&&this._startAnimation()}_startAnimation(){this.isAnimating||(this.isAnimating=!0,this._lastFrameTime=null,this._animate())}_animate(e){this._frameHandle=null,this.ctx.clearRect(0,0,this.width,this.height);let t=1;Number.isFinite(e)&&(this._lastFrameTime!==null&&(t=Math.max(0,Math.min(3,(e-this._lastFrameTime)/(1e3/60)))),this._lastFrameTime=e);for(let s=this.particles.length-1;s>=0;s--){const i=this.particles[s];if(i.vy+=i.gravity*t,i.x+=i.vx*t,i.y+=i.vy*t,i.life-=i.decay*t,i.rotationSpeed&&(i.rotation+=i.rotationSpeed*t),i.life<=0){this._recycleParticle(i);const n=this.particles.pop();s<this.particles.length&&(this.particles[s]=n);continue}if(this.ctx.save(),this.ctx.globalAlpha=i.life,this.ctx.translate(i.x,i.y),this.ctx.rotate(i.rotation),i.sprite&&this.sprites.has(i.sprite)){const n=this.sprites.get(i.sprite);n.complete&&this.ctx.drawImage(n,-i.size/2,-i.size/2,i.size,i.size)}else i.type==="star"?(this.ctx.fillStyle=i.color,this._drawStarPath(0,0,5,i.size,i.size/2),this.ctx.fill()):(this.ctx.fillStyle=i.color,this.ctx.beginPath(),this.ctx.arc(0,0,i.size,0,Math.PI*2),this.ctx.fill());this.ctx.restore()}this.particles.length>0?this._frameHandle=requestAnimationFrame(s=>this._animate(s)):this.isAnimating=!1}_drawStarPath(e,t,s,i,n){let a=Math.PI/2*3,o=e,c=t;const d=Math.PI/s;this.ctx.beginPath(),this.ctx.moveTo(e,t-i);for(let u=0;u<s;u++)o=e+Math.cos(a)*i,c=t+Math.sin(a)*i,this.ctx.lineTo(o,c),a+=d,o=e+Math.cos(a)*n,c=t+Math.sin(a)*n,this.ctx.lineTo(o,c),a+=d;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=[]}}const C=(r,e)=>Object.prototype.hasOwnProperty.call(r,e);class G{constructor(e={}){this.audioContext=null,this.storageKey=e.storageKey||"dopamineSoundsMuted",this.storage=e.storage||w(),this.muted=this.storage.getItem(this.storageKey)==="true",this.assets=new Map,this.customSounds=e.customSounds||{},this.masterGain=null,this._volume=1,this.setVolume(e.volume??1),this._loading=new Map,this._timers=new Set}_later(e,t){const s=setTimeout(()=>{this._timers.delete(s),e()},t);this._timers.add(s)}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),this.preloadSounds(this.customSounds)}return this.audioContext.state==="suspended"&&this.audioContext.resume(),!0}setVolume(e){return Number.isFinite(e)?(this._volume=Math.max(0,Math.min(1,e)),this.masterGain&&(this.masterGain.gain.value=this._volume),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){if(!this.audioContext)return;if(this._loading.has(e))return this._loading.get(e);const s=this._load(e,t).finally(()=>this._loading.delete(e));return this._loading.set(e,s),s}async _load(e,t){try{const s=this.audioContext,i=await fetch(t);if(i.ok===!1)throw new Error(`HTTP ${i.status}`);const n=await i.arrayBuffer(),a=await s.decodeAudioData(n);this.audioContext===s&&this.assets.set(e,a)}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(C(this.customSounds,e)&&!this.assets.has(e)){const s=this.audioContext;if(await this.loadSound(e,this.customSounds[e]),this.audioContext!==s)return;if(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)};C(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 n=this.audioContext.createOscillator(),a=this.audioContext.createGain();n.connect(a),a.connect(this._output()),n.frequency.value=e,n.type=s;const o=Math.max(1e-4,i);a.gain.setValueAtTime(o,this.audioContext.currentTime),a.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),this._later(()=>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),this._later(()=>this.playTone(200,.2,"sawtooth",.3),100),this._later(()=>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),this._later(()=>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)}destroy(){for(const e of this._timers)clearTimeout(e);this._timers.clear(),this.audioContext&&this.audioContext.close?.()?.catch?.(()=>{}),this.audioContext=null,this.masterGain=null,this.assets.clear(),this._loading.clear()}}class F{constructor(e={}){this.config=e,this.dataService=new D(e.data||{}),this.rewardSystem=new E(this.dataService,e.rewards||{}),this.soundManager=new G(e.sound||{}),this.particleSystem=new L(e.particles||{}),this.gameUI=new I(this.particleSystem),this._bindEvents()}async init(){return await this.rewardSystem.init(),this._syncUI(),{rewardSystem:this.rewardSystem,gameUI:this.gameUI,particleSystem:this.particleSystem,soundManager:this.soundManager}}_syncUI(){const{player:e}=this.rewardSystem,{total:t,needed:s,progress:i}=this.rewardSystem.getXPForNextLevel();this.gameUI.updateXP(e.xp,s,t,i),this.gameUI.updateLevel(e.level),this.gameUI.updateStreak(e.streak.current)}_bindEvents(){const e=this.rewardSystem;this._unsubscribe=[e.on("xp_gained",()=>{const{total:t,needed:s,progress:i}=e.getXPForNextLevel();this.gameUI.updateXP(e.player.xp,s,t,i)}),e.on("level_up",t=>{this.gameUI.updateLevel(t.newLevel),this.gameUI.showLevelUp(t.oldLevel,t.newLevel)}),e.on("achievement_unlocked",t=>{this.gameUI.showAchievement(t)}),e.on("streak_updated",t=>{this.gameUI.updateStreak(t.current)}),e.on("new_high_score",t=>{this.gameUI.showNotification(`New High Score: ${t.score}!`,"legendary"),this.soundManager.playSuccess()})]}destroy(){for(const e of this._unsubscribe)e();this._unsubscribe=[],this.gameUI.destroy(),this.particleSystem.destroy(),this.soundManager.destroy()}}const x={init(r){},update(r){},fixedUpdate(r){},destroy(){}};class N{constructor(){this.kernel=null}init(e){this.kernel=e}update(e){}fixedUpdate(e){}destroy(){}}const R={...x,addBody(r){},removeBody(r){},checkCollision(r,e){},raycast(r,e,t){},setGravity(r,e){},step(r){}},B={...x,registerSound(r,e){},preloadSounds(r){},play(r,e){},playTone(r,e,t,s){},stop(r){},toggleMute(){},setVolume(r){}},U={...x,registerSprite(r,e){},registerEffect(r,e){},play(r,e,t,...s){},emit(r){},clear(){},confetti(r,e,t){},coinShower(r,e,t){},sparkle(r,e,t,s){},fire(r,e,t){},starBurst(r,e,t){}};class ${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 a=0;a<1e3;a++){n.fillStyle=Math.random()>.5?"#444":"#222";const o=Math.random()*4+2;n.fillRect(Math.random()*e,Math.random()*t,o,o)}n.strokeStyle="rgba(255,255,255,0.05)",n.lineWidth=2,n.beginPath();for(let a=0;a<=e;a+=64)n.moveTo(a,0),n.lineTo(a,t);for(let a=0;a<=t;a+=64)n.moveTo(0,a),n.lineTo(e,a);return n.stroke(),i}static createCharacterSheet(e,t){const n=document.createElement("canvas");n.width=256,n.height=64;const a=n.getContext("2d");for(let o=0;o<4;o++){const c=o*64,d=0,u=c+64/2,m=d+64/2,p=o%2===0?0:-2,_=o===1?5:o===3?-5:0;a.fillStyle=e,a.beginPath(),a.arc(u,m+p,20,0,Math.PI*2),a.fill(),a.fillStyle=t,a.beginPath(),a.arc(u-15,m+p+_,8,0,Math.PI*2),a.arc(u+15,m+p-_,8,0,Math.PI*2),a.fill(),a.fillStyle=q(e),a.beginPath(),a.arc(u,m+p,15,0,Math.PI*2),a.fill(),t==="#333"&&(a.fillStyle="#000",a.fillRect(u+10,m+p-5,25,6))}return n}}function q(r,e){return r}exports.Animator=l.Animator;exports.Collider=l.Collider;exports.Component=l.Component;exports.Director=l.Director;exports.DopamineKernel=l.DopamineKernel;exports.EventBus=l.EventBus;exports.Game=l.Game;exports.GameObject=l.GameObject;exports.GlobalInput=l.GlobalInput;exports.GlobalLoader=l.GlobalLoader;exports.GlobalPhysics=l.GlobalPhysics;exports.Input=l.Input;exports.Loader=l.Loader;exports.ParticleEmitter=l.ParticleEmitter;exports.Physics=l.Physics;exports.PluginRegistry=l.PluginRegistry;exports.Renderer=l.Renderer;exports.Scene=l.Scene;exports.ScreenShake=l.ScreenShake;exports.Sprite=l.Sprite;exports.SystemRegistry=l.SystemRegistry;exports.Ticker=l.Ticker;exports.Vector2=l.Vector2;exports.DataService=D;exports.EventEmitter=T;exports.GameUI=I;exports.IAudioSystem=B;exports.IParticleSystem=U;exports.IPhysicsSystem=R;exports.ISystem=x;exports.ParticleSystem=L;exports.RewardSystem=E;exports.SoundManager=G;exports.System=N;exports.TextureGenerator=$;exports.createMemoryStorage=S;exports.default=F;exports.resolveStorage=w;