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 +10 -10
- package/dist/dopamine.cjs +37 -0
- package/dist/dopamine.js +584 -1403
- package/dist/engine.cjs +1 -0
- package/dist/engine.js +1158 -0
- package/dist/style.css +1 -1
- package/package.json +27 -13
- package/src/core/Collider.js +43 -7
- package/src/core/PluginRegistry.js +84 -7
- package/src/core/Scene.js +50 -5
- package/src/core/SystemRegistry.js +32 -11
- package/src/dopamine/audio/SoundManager.js +91 -20
- package/src/dopamine/components/ScreenShake.js +7 -6
- package/src/dopamine/components/index.js +0 -2
- package/src/dopamine/core/DataService.js +31 -3
- package/src/dopamine/core/RewardSystem.js +178 -22
- package/src/dopamine/effects/ParticleSystem.js +72 -15
- package/src/dopamine/index.js +56 -24
- package/src/dopamine/ui/GameUI.js +48 -5
- package/src/dopamine/ui/dopamine.css +18 -0
- package/src/engine.js +38 -0
- package/src/systems/Director.js +7 -0
- package/src/systems/Ticker.js +14 -3
- package/types/index.d.ts +59 -14
- package/dist/dopamine.umd.cjs +0 -37
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# dopaminejs
|
|
2
2
|
|
|
3
|
-
>
|
|
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
|
|
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
|
-
|
|
14
|
+
// Named imports — the recommended way
|
|
15
|
+
import { RewardSystem, DataService } from 'dopaminejs';
|
|
15
16
|
import 'dopaminejs/style.css';
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
await
|
|
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
|
-
|
|
25
|
-
|
|
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;
|