dopaminejs 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dopamine.js +1947 -0
- package/dist/dopamine.umd.cjs +45 -0
- package/package.json +24 -32
- package/src/core/Animator.js +62 -0
- package/src/core/Collider.js +50 -0
- package/src/core/Component.js +38 -0
- package/src/core/DopamineKernel.js +153 -0
- package/src/core/EventBus.js +183 -0
- package/src/core/Game.js +89 -0
- package/src/core/GameObject.js +83 -0
- package/src/core/PluginRegistry.js +197 -0
- package/src/core/Scene.js +65 -0
- package/src/core/Sprite.js +51 -0
- package/src/core/SystemRegistry.js +242 -0
- package/src/core/Vector2.js +50 -0
- package/src/{audio → dopamine/audio}/SoundManager.js +1 -3
- package/src/dopamine/components/ParticleEmitter.js +50 -0
- package/src/dopamine/components/ScreenShake.js +49 -0
- package/src/dopamine/components/index.js +4 -0
- package/src/dopamine/effects/WebGLParticleSystem.js +348 -0
- package/src/dopamine/index.js +62 -0
- package/src/dopamine/utils/TextureGenerator.js +90 -0
- package/src/index.js +34 -62
- package/src/interfaces/IAudioSystem.js +58 -0
- package/src/interfaces/IParticleSystem.js +51 -0
- package/src/interfaces/IPhysicsSystem.js +52 -0
- package/src/interfaces/ISystem.js +48 -0
- package/src/interfaces/index.js +7 -0
- package/src/renderer/Renderer.js +37 -0
- package/src/systems/Director.js +27 -0
- package/src/systems/Input.js +77 -0
- package/src/systems/Loader.js +44 -0
- package/src/systems/Physics.js +117 -0
- package/src/systems/Ticker.js +61 -0
- package/ARCHITECTURE.md +0 -71
- package/CHANGELOG.md +0 -36
- package/README.md +0 -264
- package/dist/dopamine.mjs +0 -737
- package/dist/dopamine.umd.js +0 -45
- /package/src/{core → dopamine/core}/DataService.js +0 -0
- /package/src/{core → dopamine/core}/EventEmitter.js +0 -0
- /package/src/{core → dopamine/core}/RewardSystem.js +0 -0
- /package/src/{effects → dopamine/effects}/ParticleSystem.js +0 -0
- /package/src/{ui → dopamine/ui}/GameUI.js +0 -0
- /package/src/{ui → dopamine/ui}/dopamine.css +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
(function(o,g){typeof exports=="object"&&typeof module<"u"?g(exports):typeof define=="function"&&define.amd?define(["exports"],g):(o=typeof globalThis<"u"?globalThis:o||self,g(o.Dopamine={}))})(this,function(o){"use strict";class g{constructor(){this.listeners=new Map}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>this.off(e,t)}off(e,t){this.listeners.has(e)&&this.listeners.get(e).delete(t)}emit(e,t){this.listeners.has(e)&&this.listeners.get(e).forEach(s=>{try{s(t)}catch(i){console.error(`Error in listener for event '${e}':`,i)}})}clear(){this.listeners.clear()}}class b extends g{constructor(e,t={}){super(),this.dataService=e,this.player=null,this.achievements={...this._initAchievements(),...t.achievements||{}}}async init(){return this.player=await this.dataService.load("player",this._getDefaultPlayer()),this._updateDailyStreak(),await this.save(),this.player}_getDefaultPlayer(){return{name:"Player",xp:0,level:1,totalGamesPlayed:0,createdAt:Date.now(),lastPlayedAt:Date.now(),streak:{current:1,longest:1,lastPlayDate:this._getTodayDateString()},achievements:{},stats:{}}}async save(){this.player&&(this.player.lastPlayedAt=Date.now(),await this.dataService.save("player",this.player))}async addXP(e,t=""){const s=this.player.level;this.player.xp+=e;const i=this._calculateLevel(this.player.xp),n=i>s;return n&&(this.player.level=i),await this.save(),this.emit("xp_gained",{amount:e,reason:t,leveledUp:n,newLevel:i}),n&&this.emit("level_up",{oldLevel:s,newLevel:i}),{leveledUp:n,newLevel:i,xpGained:e}}_calculateLevel(e){const t=Math.floor((1+Math.sqrt(1+8*e/50))/2);return Math.max(1,t)}getXPForNextLevel(){const e=this.player.level+1,t=50*e*(e-1),s=50*this.player.level*(this.player.level-1);return{total:t,needed:t-this.player.xp,progress:(this.player.xp-s)/(t-s)}}async recordGame(e,t){this.player.stats[e]||(this.player.stats[e]={totalPlays:0,highScore:0});const s=this.player.stats[e];s.totalPlays++,this.player.totalGamesPlayed++,t.score!==void 0&&t.score>s.highScore&&(s.highScore=t.score,this.emit("new_high_score",{gameName:e,score:t.score})),Object.keys(t).forEach(n=>{n!=="score"&&(s[n]=(s[n]||0)+(typeof t[n]=="number"?t[n]:0))});let i=10;t.score&&(i+=Math.floor(t.score/5)),i=Math.floor(i*this.getStreakMultiplier()),await this.addXP(i,`Played ${e}`),await this.checkAchievements(e,t),await this.save()}async checkAchievements(e,t){const s=[];for(const[i,n]of Object.entries(this.achievements))this.player.achievements[i]||n.check(this.player,e,t)&&(await this.unlockAchievement(i),s.push(n));return s}async unlockAchievement(e){const t=this.achievements[e];return t?(this.player.achievements[e]={unlockedAt:Date.now(),seen:!1},await this.addXP(t.xp,`Achievement: ${t.name}`),this.emit("achievement_unlocked",t),await this.save(),!0):!1}getUnlockedAchievements(){return Object.entries(this.player.achievements).map(([e,t])=>({...this.achievements[e],id:e,...t}))}getUnseenAchievements(){return this.getUnlockedAchievements().filter(e=>!e.seen)}async markAchievementsSeen(e){for(const t of e)this.player.achievements[t]&&(this.player.achievements[t].seen=!0);await this.save()}_updateDailyStreak(){const e=this._getTodayDateString(),t=this.player.streak.lastPlayDate;if(t===e)return;const s=this._getYesterdayDateString();t===s?(this.player.streak.current++,this.player.streak.current>this.player.streak.longest&&(this.player.streak.longest=this.player.streak.current)):this.player.streak.current=1,this.player.streak.lastPlayDate=e}getStreakMultiplier(){const e=this.player.streak.current;return e>=7?2:e>=5?1.5:e>=3?1.25:1}_getTodayDateString(){return new Date().toISOString().split("T")[0]}_getYesterdayDateString(){const e=new Date;return e.setDate(e.getDate()-1),e.toISOString().split("T")[0]}_initAchievements(){return{first_game:{name:"First Steps",description:"Play your first game",icon:"🎮",xp:50,check:e=>e.totalGamesPlayed>=1},level_5:{name:"Rising Star",description:"Reach level 5",icon:"⭐",xp:100,check:e=>e.level>=5},level_10:{name:"Expert Player",description:"Reach level 10",icon:"🏆",xp:250,check:e=>e.level>=10},streak_3:{name:"Dedicated",description:"Play 3 days in a row",icon:"🔥",xp:100,check:e=>e.streak.current>=3},streak_7:{name:"Unstoppable",description:"Play 7 days in a row",icon:"💥",xp:300,check:e=>e.streak.current>=7}}}}class M{constructor(e={}){this.storage=e.storage||window.localStorage,this.prefix=e.prefix||"dopamine_"}async save(e,t){try{const s=JSON.stringify(t);return this.storage.setItem(this.prefix+e,s),!0}catch(s){return console.error("Error saving data:",s),!1}}async load(e,t=null){try{const s=this.storage.getItem(this.prefix+e);return s?JSON.parse(s):t}catch(s){return console.error("Error loading data:",s),t}}async clear(e){this.storage.removeItem(this.prefix+e)}}class P{constructor(e){this.particleSystem=e,this.container=null,this.xpBar=null,this.levelDisplay=null,this.streakDisplay=null,this.init()}init(){this.container=document.createElement("div"),this.container.id="game-ui-overlay",this.container.innerHTML=`
|
|
2
|
+
<div class="game-ui-top-bar">
|
|
3
|
+
<div class="level-badge" id="level-badge">
|
|
4
|
+
<span class="level-label">LVL</span>
|
|
5
|
+
<span class="level-number" id="level-number">1</span>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="xp-container">
|
|
9
|
+
<div class="xp-bar-bg">
|
|
10
|
+
<div class="xp-bar-fill" id="xp-bar-fill" style="width: 0%"></div>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="xp-text" id="xp-text">0 /100 XP</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="streak-badge" id="streak-badge">
|
|
16
|
+
<span class="streak-icon">🔥</span>
|
|
17
|
+
<span class="streak-number" id="streak-number">1</span>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
`,document.body.appendChild(this.container),this.xpBar=document.getElementById("xp-bar-fill"),this.xpText=document.getElementById("xp-text"),this.levelDisplay=document.getElementById("level-number"),this.streakDisplay=document.getElementById("streak-number")}updateXP(e,t,s){const i=e/s*100;this.xpBar.style.width=`${Math.min(100,i)}%`,this.xpText.textContent=`${e} / ${s} XP`,this.xpBar.style.animation="none",setTimeout(()=>{this.xpBar.style.animation="xp-pulse 0.3s ease-out"},10)}updateLevel(e){this.levelDisplay.textContent=e;const t=document.getElementById("level-badge");t.style.animation="none",setTimeout(()=>{t.style.animation="level-pulse 0.5s ease-out"},10)}updateStreak(e){this.streakDisplay.textContent=e;const t=document.getElementById("streak-badge");e>=7?t.style.background="linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)":e>=3&&(t.style.background="linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)")}showAchievement(e){const t=document.createElement("div");t.className="achievement-popup",t.innerHTML=`
|
|
21
|
+
<div class="achievement-shine"></div>
|
|
22
|
+
<div class="achievement-content">
|
|
23
|
+
<div class="achievement-icon">${e.icon}</div>
|
|
24
|
+
<div class="achievement-info">
|
|
25
|
+
<div class="achievement-title">Achievement Unlocked!</div>
|
|
26
|
+
<div class="achievement-name">${e.name}</div>
|
|
27
|
+
<div class="achievement-desc">${e.description}</div>
|
|
28
|
+
<div class="achievement-xp">+${e.xp} XP</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
`,document.body.appendChild(t),setTimeout(()=>{const s=t.getBoundingClientRect();this.particleSystem.confetti(s.left+s.width/2,s.top+s.height/2,40)},300),setTimeout(()=>{t.style.animation="slideOut 0.3s ease-in forwards",setTimeout(()=>t.remove(),300)},4e3)}showLevelUp(e,t){const s=document.createElement("div");s.className="level-up-overlay",s.innerHTML=`
|
|
32
|
+
<div class="level-up-content">
|
|
33
|
+
<div class="level-up-title">LEVEL UP!</div>
|
|
34
|
+
<div class="level-up-number">${t}</div>
|
|
35
|
+
<div class="level-up-subtitle">Amazing progress!</div>
|
|
36
|
+
</div>
|
|
37
|
+
`,document.body.appendChild(s),setTimeout(()=>{for(let i=0;i<5;i++)setTimeout(()=>{const n=Math.random()*window.innerWidth,r=Math.random()*window.innerHeight*.6;this.particleSystem.confetti(n,r,30)},i*200)},300),setTimeout(()=>{s.style.opacity="0",setTimeout(()=>s.remove(),500)},2500)}showFloatingText(e,t,s,i="#ffd700",n="24px"){const r=document.createElement("div");r.className="floating-text",r.textContent=e,r.style.left=t+"px",r.style.top=s+"px",r.style.color=i,r.style.fontSize=n,document.body.appendChild(r),setTimeout(()=>r.remove(),2e3)}showCombo(e,t,s){this.showFloatingText(`${e}× COMBO!`,t,s,"#ff6b6b","32px"),this.particleSystem.fire(t,s,20)}showNearMiss(e,t){this.showFloatingText("CLOSE CALL!",e,t,"#45b7d1","20px"),this.particleSystem.sparkle(e,t,15,"#45b7d1")}showLuckyMoment(e,t,s){this.showFloatingText(`LUCKY ${e}×!`,t,s,"#ffd700","36px"),this.particleSystem.starBurst(t,s,12),this.particleSystem.confetti(t,s,25)}showNotification(e,t="normal"){const s=window.innerWidth/2,i=window.innerHeight/3;t==="legendary"?this.showLuckyMoment(e,s,i):t==="rare"?this.showCombo(e,s,i):this.showFloatingText(e,s,i,"#fff","24px")}showSummary(e){const t=document.createElement("div");t.className="message-box",t.style.display="flex",t.style.position="absolute",t.style.top="50%",t.style.left="50%",t.style.transform="translate(-50%, -50%)",t.style.zIndex="1000";let s="";e.metrics&&(s=Object.entries(e.metrics).map(([i,n])=>`<div style="display:flex;justify-content:space-between;width:100%"><span>${i}:</span> <strong>${n}</strong></div>`).join("")),t.innerHTML=`
|
|
38
|
+
<h1>Game Over</h1>
|
|
39
|
+
<div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${e.score}</div>
|
|
40
|
+
<div style="width: 100%; margin-bottom: 20px;">
|
|
41
|
+
${s}
|
|
42
|
+
</div>
|
|
43
|
+
<button class="btn" onclick="location.reload()">Play Again</button>
|
|
44
|
+
<button class="btn" onclick="window.location.href='index.html'" style="margin-top: 10px; background: #543847;">Exit</button>
|
|
45
|
+
`,document.body.appendChild(t)}}class E{constructor(e={}){this.container=e.container?typeof e.container=="string"?document.querySelector(e.container):e.container:document.body,this.canvasId=e.canvasId||"dopamine-particle-canvas",this.canvas=document.getElementById(this.canvasId),this.canvas||(this.canvas=document.createElement("canvas"),this.canvas.id=this.canvasId,this.canvas.style.pointerEvents="none",this.canvas.style.zIndex=e.zIndex||"9999",this.container===document.body?(this.canvas.style.position="fixed",this.canvas.style.top="0",this.canvas.style.left="0"):(this.canvas.style.position="absolute",this.canvas.style.top="0",this.canvas.style.left="0",window.getComputedStyle(this.container).position==="static"&&(this.container.style.position="relative")),this.container.appendChild(this.canvas)),this.ctx=this.canvas.getContext("2d"),this.particles=[],this.pool=[],this.sprites=new Map,this.customEffects=new Map,this.isAnimating=!1,this._resize(),window.ResizeObserver&&this.container!==document.body?new ResizeObserver(()=>this._resize()).observe(this.container):window.addEventListener("resize",()=>this._resize())}_resize(){this.container===document.body?(this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight):(this.canvas.width=this.container.clientWidth,this.canvas.height=this.container.clientHeight)}registerSprite(e,t){const s=new Image;s.src=t,this.sprites.set(e,s)}registerEffect(e,t){this.customEffects.set(e,t)}play(e,t,s,...i){const n=this.customEffects.get(e);n?n(t,s,...i):console.warn(`[DopamineJS] Effect '${e}' not found.`)}emit(e){const{x:t,y:s,count:i=10,speed:n=5,gravity:r=.1,decay:h=.02,life:c=1,color:l="#ffd700",sprite:u=null,spread:y=Math.PI*2,angle:p=0,size:w=5}=e;for(let R=0;R<i;R++){const d=this._getParticle(),j=p+(Math.random()-.5)*y,z=Math.random()*n;d.x=t,d.y=s,d.vx=Math.cos(j)*z,d.vy=Math.sin(j)*z,d.life=c,d.decay=h*(.8+Math.random()*.4),d.gravity=r,d.color=Array.isArray(l)?l[Math.floor(Math.random()*l.length)]:l,d.size=w*(.8+Math.random()*.4),d.sprite=u,d.rotation=Math.random()*Math.PI*2,d.rotationSpeed=(Math.random()-.5)*.2,this.particles.push(d)}this._startAnimation()}_getParticle(){return this.pool.length>0?this.pool.pop():{}}_recycleParticle(e){this.pool.push(e)}confetti(e,t,s=50){const i=["#ff6b6b","#4ecdc4","#45b7d1","#f7dc6f","#c06c84","#6c5ce7"];this.emit({x:e,y:t,count:s,color:i,speed:10,gravity:.3,decay:.015,size:8,spread:Math.PI*2})}coinShower(e,t,s=10){this.emit({x:e,y:t,count:s,color:"#ffd700",speed:4,gravity:.4,decay:.012,size:12,angle:-Math.PI/2,spread:Math.PI/2})}sparkle(e,t,s=20,i="#ffd700"){this.emit({x:e,y:t,count:s,color:i,speed:3,gravity:-.05,decay:.03,size:3})}fire(e,t,s=15){const i=["#ff6b00","#ff8800","#ffaa00","#ff4400"];this.emit({x:e,y:t,count:s,color:i,speed:3,gravity:-.2,decay:.04,size:5,angle:-Math.PI/2,spread:Math.PI/4})}starBurst(e,t,s=8){for(let i=0;i<s;i++){const n=this._getParticle(),r=Math.PI*2/s*i,h=4;n.x=e,n.y=t,n.vx=Math.cos(r)*h,n.vy=Math.sin(r)*h,n.life=1,n.decay=.03,n.gravity=0,n.color="#ffffff",n.size=15,n.sprite=null,n.type="star",n.rotation=r,n.rotationSpeed=0,this.particles.push(n)}this._startAnimation()}_startAnimation(){this.isAnimating||(this.isAnimating=!0,this._animate())}_animate(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);for(let e=this.particles.length-1;e>=0;e--){const t=this.particles[e];if(t.vy+=t.gravity,t.x+=t.vx,t.y+=t.vy,t.life-=t.decay,t.rotationSpeed&&(t.rotation+=t.rotationSpeed),t.life<=0){this._recycleParticle(t),this.particles.splice(e,1);continue}if(this.ctx.save(),this.ctx.globalAlpha=t.life,this.ctx.translate(t.x,t.y),this.ctx.rotate(t.rotation),t.sprite&&this.sprites.has(t.sprite)){const s=this.sprites.get(t.sprite);s.complete&&this.ctx.drawImage(s,-t.size/2,-t.size/2,t.size,t.size)}else t.type==="star"?(this.ctx.fillStyle=t.color,this._drawStarPath(0,0,5,t.size,t.size/2),this.ctx.fill()):(this.ctx.fillStyle=t.color,this.ctx.beginPath(),this.ctx.arc(0,0,t.size,0,Math.PI*2),this.ctx.fill());this.ctx.restore()}this.particles.length>0?requestAnimationFrame(()=>this._animate()):this.isAnimating=!1}_drawStarPath(e,t,s,i,n){let r=Math.PI/2*3,h=e,c=t;const l=Math.PI/s;this.ctx.beginPath(),this.ctx.moveTo(e,t-i);for(let u=0;u<s;u++)h=e+Math.cos(r)*i,c=t+Math.sin(r)*i,this.ctx.lineTo(h,c),r+=l,h=e+Math.cos(r)*n,c=t+Math.sin(r)*n,this.ctx.lineTo(h,c),r+=l;this.ctx.lineTo(e,t-i),this.ctx.closePath()}clear(){this.particles.forEach(e=>this._recycleParticle(e)),this.particles=[],this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}}class T{constructor(e={}){this.audioContext=null,this.storageKey=e.storageKey||"dopamineSoundsMuted",this.muted=localStorage.getItem(this.storageKey)==="true",this.assets=new Map,this.customSounds=e.customSounds||{},Object.keys(this.customSounds).length>0&&this.preloadSounds(this.customSounds)}initAudio(){if(!this.audioContext){const e=window.AudioContext||window.webkitAudioContext;this.audioContext=new e}this.audioContext.state==="suspended"&&this.audioContext.resume()}toggleMute(){return this.muted=!this.muted,localStorage.setItem(this.storageKey,this.muted),this.muted}registerSound(e,t){this.customSounds[e]=t,this.audioContext&&this.loadSound(e,t)}async preloadSounds(e){const t=Object.entries(e).map(([s,i])=>this.loadSound(s,i));await Promise.allSettled(t)}async loadSound(e,t){try{if(!this.audioContext)return;const i=await(await fetch(t)).arrayBuffer(),n=await this.audioContext.decodeAudioData(i);this.assets.set(e,n)}catch(s){console.warn(`[DopamineJS] Failed to load sound '${e}':`,s)}}async play(e){if(this.muted)return;if(this.initAudio(),this.assets.has(e)){this._playBuffer(this.assets.get(e));return}if(this.customSounds[e]&&!this.assets.has(e)&&(await this.loadSound(e,this.customSounds[e]),this.assets.has(e))){this._playBuffer(this.assets.get(e));return}const t={jump:()=>this.playJump(!0),score:()=>this.playScore(!0),success:()=>this.playSuccess(!0),click:()=>this.playClick(!0),error:()=>this.playError(!0),gameover:()=>this.playGameOver(!0)};t[e]&&t[e]()}_playBuffer(e){const t=this.audioContext.createBufferSource();t.buffer=e,t.connect(this.audioContext.destination),t.start(0)}playTone(e,t,s="sine",i=.3){if(this.muted)return;this.initAudio();const n=this.audioContext.createOscillator(),r=this.audioContext.createGain();n.connect(r),r.connect(this.audioContext.destination),n.frequency.value=e,n.type=s,r.gain.setValueAtTime(i,this.audioContext.currentTime),r.gain.exponentialRampToValueAtTime(.01,this.audioContext.currentTime+t),n.start(this.audioContext.currentTime),n.stop(this.audioContext.currentTime+t)}playJump(e=!1){if(!e&&this.assets.has("jump"))return this.play("jump");this.playTone(400,.1,"square",.2)}playScore(e=!1){if(!e&&this.assets.has("score"))return this.play("score");this.playTone(800,.15,"sine",.25),setTimeout(()=>this.playTone(1e3,.15,"sine",.25),50)}playGameOver(e=!1){if(!e&&this.assets.has("gameover"))return this.play("gameover");this.playTone(300,.2,"sawtooth",.3),setTimeout(()=>this.playTone(200,.2,"sawtooth",.3),100),setTimeout(()=>this.playTone(150,.3,"sawtooth",.3),200)}playClick(e=!1){if(!e&&this.assets.has("click"))return this.play("click");this.playTone(600,.05,"sine",.1)}playSuccess(e=!1){if(!e&&this.assets.has("success"))return this.play("success");this.playTone(600,.1,"sine",.2),setTimeout(()=>this.playTone(800,.2,"sine",.2),100)}playError(e=!1){if(!e&&this.assets.has("error"))return this.play("error");this.playTone(200,.2,"sawtooth",.3)}}class G{constructor(e={}){this.config=e,this.dataService=new M(e.data||{}),this.rewardSystem=new b(this.dataService,e.rewards||{}),this.soundManager=new T(e.sound||{}),this.particleSystem=new E(e.particles||{}),this.gameUI=new P(this.particleSystem),this._bindEvents()}async init(){return await this.rewardSystem.init(),console.log("DopamineJS Initialized 🚀"),{rewardSystem:this.rewardSystem,gameUI:this.gameUI,particleSystem:this.particleSystem,soundManager:this.soundManager}}_bindEvents(){this.rewardSystem.on("xp_gained",e=>{const{total:t,needed:s}=this.rewardSystem.getXPForNextLevel();this.gameUI.updateXP(this.rewardSystem.player.xp,s,t),e.xpGained>0}),this.rewardSystem.on("level_up",e=>{this.gameUI.updateLevel(e.newLevel),this.gameUI.showLevelUp(e.oldLevel,e.newLevel)}),this.rewardSystem.on("achievement_unlocked",e=>{this.gameUI.showAchievement(e)}),this.rewardSystem.on("new_high_score",e=>{this.gameUI.showNotification(`New High Score: ${e.score}!`,"legendary"),this.soundManager.playSuccess()})}}class m{constructor(){this._listeners=new Map,this._onceListeners=new Map,this._sortedCache=new Map}on(e,t,s=0){return this._listeners.has(e)||this._listeners.set(e,new Set),this._listeners.get(e).add({callback:t,priority:s}),this._sortedCache.delete(e),this}once(e,t){return this._onceListeners.has(e)||this._onceListeners.set(e,new Set),this._onceListeners.get(e).add(t),this}off(e,t){const s=this._listeners.get(e);if(s){for(const n of s)if(n.callback===t){s.delete(n),this._sortedCache.delete(e);break}}const i=this._onceListeners.get(e);return i&&i.delete(t),this}emit(e,t){const s=this._getSortedListeners(e);for(let n=0;n<s.length;n++)s[n].callback(t);const i=this._onceListeners.get(e);if(i&&i.size>0){for(const n of i)n(t);i.clear()}}_getSortedListeners(e){if(this._sortedCache.has(e))return this._sortedCache.get(e);const t=this._listeners.get(e);if(!t||t.size===0)return[];const s=Array.from(t).sort((i,n)=>n.priority-i.priority);return this._sortedCache.set(e,s),s}clear(e){e?(this._listeners.delete(e),this._onceListeners.delete(e),this._sortedCache.delete(e)):(this._listeners.clear(),this._onceListeners.clear(),this._sortedCache.clear())}hasListeners(e){const t=this._listeners.get(e),s=this._onceListeners.get(e);return t&&t.size>0||s&&s.size>0}}m.Events={TICK:"tick",FIXED_UPDATE:"fixed_update",RENDER:"render",COLLISION_ENTER:"collision_enter",COLLISION_EXIT:"collision_exit",XP_GAINED:"xp_gained",LEVEL_UP:"level_up",ACHIEVEMENT_UNLOCKED:"achievement_unlocked",NEW_HIGH_SCORE:"new_high_score",STREAK_UPDATED:"streak_updated",SYSTEM_REGISTERED:"system_registered",SYSTEM_UNREGISTERED:"system_unregistered",PLUGIN_LOADED:"plugin_loaded",PLUGIN_UNLOADED:"plugin_unloaded"};class I{constructor(e){this.kernel=e,this._systems=new Map,this._updateOrder=[],this._fixedUpdateOrder=[],this._needsRecompute=!1}register(e,t,{priority:s=0,dependencies:i=[]}={}){this._systems.has(e)&&(console.warn(`[SystemRegistry] System "${e}" already registered. Replacing.`),this.unregister(e));for(const n of i)this._systems.has(n)||console.warn(`[SystemRegistry] Dependency "${n}" not found for system "${e}"`);return this._systems.set(e,{system:t,priority:s,dependencies:i}),t.init&&t.init(this.kernel),this._needsRecompute=!0,this.kernel.events.emit("system_registered",{name:e,system:t}),this}get(e){const t=this._systems.get(e);return t?t.system:void 0}has(e){return this._systems.has(e)}unregister(e){const t=this._systems.get(e);return t?(t.system.destroy&&t.system.destroy(),this._systems.delete(e),this._needsRecompute=!0,this.kernel.events.emit("system_unregistered",{name:e}),!0):!1}update(e){this._needsRecompute&&this._recomputeUpdateOrder();for(let t=0;t<this._updateOrder.length;t++){const s=this._updateOrder[t];s.update&&s.update(e)}}fixedUpdate(e){this._needsRecompute&&this._recomputeUpdateOrder();for(let t=0;t<this._fixedUpdateOrder.length;t++){const s=this._fixedUpdateOrder[t];s.fixedUpdate&&s.fixedUpdate(e)}}_recomputeUpdateOrder(){const e=Array.from(this._systems.entries()),t=new Map,s=new Map;for(const[r,h]of e)t.set(r,h.dependencies),s.set(r,0);for(const[r,h]of t)for(const c of h)s.has(c)&&s.set(r,s.get(r)+1);const i=[],n=[];for(const[r,h]of s)h===0&&i.push(r);for(;i.length>0;){i.sort((h,c)=>{const l=this._systems.get(h).priority;return this._systems.get(c).priority-l});const r=i.shift();n.push(r);for(const[h,c]of t)if(c.includes(r)){const l=s.get(h)-1;s.set(h,l),l===0&&i.push(h)}}n.length!==e.length&&(console.error("[SystemRegistry] Circular dependency detected in systems!"),n.length=0,n.push(...e.map(([r])=>r).sort((r,h)=>this._systems.get(h).priority-this._systems.get(r).priority))),this._updateOrder=n.map(r=>this._systems.get(r).system).filter(r=>r.update),this._fixedUpdateOrder=n.map(r=>this._systems.get(r).system).filter(r=>r.fixedUpdate),this._needsRecompute=!1}getSystemNames(){return Array.from(this._systems.keys())}clear(){for(const e of this._systems.keys())this.unregister(e)}}class C{constructor(e){this.kernel=e,this._plugins=new Map,this._loadOrder=[]}use(e){if(!e.name)throw new Error("[PluginRegistry] Plugin must have a name");if(this._plugins.has(e.name))return console.warn(`[PluginRegistry] Plugin "${e.name}" already registered. Skipping.`),this;if(!e.init||typeof e.init!="function")throw new Error(`[PluginRegistry] Plugin "${e.name}" must have an init() method`);this._plugins.set(e.name,e),this._loadOrder.push(e.name);try{e.init(this.kernel),this.kernel.events.emit("plugin_loaded",{name:e.name,version:e.version}),console.log(`[PluginRegistry] Loaded plugin: ${e.name}${e.version?` v${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}),console.log(`[PluginRegistry] Loaded plugin: ${e.name}${e.version?` v${e.version}`:""}`)}catch(t){throw console.error(`[PluginRegistry] Failed to initialize plugin "${e.name}":`,t),this._plugins.delete(e.name),this._loadOrder.pop(),t}return this}remove(e){const t=this._plugins.get(e);if(!t)return!1;if(t.destroy)try{t.destroy()}catch(i){console.error(`[PluginRegistry] Error destroying plugin "${e}":`,i)}this._plugins.delete(e);const s=this._loadOrder.indexOf(e);return s>-1&&this._loadOrder.splice(s,1),this.kernel.events.emit("plugin_unloaded",{name:e}),!0}get(e){return this._plugins.get(e)}has(e){return this._plugins.has(e)}getPluginNames(){return Array.from(this._plugins.keys())}getLoadOrder(){return[...this._loadOrder]}clear(){for(let e=this._loadOrder.length-1;e>=0;e--)this.remove(this._loadOrder[e])}}class O{constructor(e={}){this.width=e.width||800,this.height=e.height||600,this.backgroundColor=e.backgroundColor||"#000000",this.canvas=document.createElement("canvas"),this.canvas.width=this.width,this.canvas.height=this.height,this.ctx=this.canvas.getContext("2d"),(e.parent?typeof e.parent=="string"?document.querySelector(e.parent):e.parent:document.body).appendChild(this.canvas)}clear(){this.ctx.fillStyle=this.backgroundColor,this.ctx.fillRect(0,0,this.width,this.height)}resize(e,t){this.width=e,this.height=t,this.canvas.width=e,this.canvas.height=t}}class A{constructor(){this.running=!1,this.lastTime=0,this.callbacks=new Set,this._tick=this._tick.bind(this)}add(e){this.callbacks.add(e)}remove(e){this.callbacks.delete(e)}start(){this.running||(this.running=!0,this.lastTime=performance.now(),requestAnimationFrame(this._tick))}stop(){this.running=!1}_tick(e){if(!this.running)return;const t=(e-this.lastTime)/1e3;this.lastTime=e;const s=Math.min(t,.1);for(const i of this.callbacks)i(s);requestAnimationFrame(this._tick)}}class f{constructor(){this.gameObject=null,this.kernel=null}get physics(){var e;return(e=this.kernel)==null?void 0:e.systems.get("physics")}get input(){var e;return(e=this.kernel)==null?void 0:e.systems.get("input")}get loader(){var e;return(e=this.kernel)==null?void 0:e.systems.get("loader")}get events(){var e;return(e=this.kernel)==null?void 0:e.events}onAttach(){}onDetach(){}update(e){}render(e){}}class $ extends f{constructor(e="box",t=50,s=50,i=25){super(),this.type=e,this.width=t,this.height=s,this.radius=i,this.tag=null}onAttach(){super.onAttach(),this.physics&&this.physics.add(this)}onDetach(){super.onDetach(),this.physics&&this.physics.remove(this)}getBounds(){const e=this.gameObject;if(this.type==="box")return{left:e.x-this.width/2,right:e.x+this.width/2,top:e.y-this.height/2,bottom:e.y+this.height/2};if(this.type==="circle")return{x:e.x,y:e.y,radius:this.radius}}}class _{constructor(){this.colliders=[],this.kernel=null}init(e){this.kernel=e}fixedUpdate(e){this.step()}destroy(){this.colliders=[]}add(e){this.colliders.push(e)}remove(e){const t=this.colliders.indexOf(e);t>-1&&this.colliders.splice(t,1)}step(){for(let e=0;e<this.colliders.length;e++)for(let t=e+1;t<this.colliders.length;t++){const s=this.colliders[e],i=this.colliders[t];this._intersects(s,s.getBounds(),i,i.getBounds())&&(this._notifyCollision(s.gameObject,i.gameObject),this._notifyCollision(i.gameObject,s.gameObject))}}_notifyCollision(e,t){!e||!e.components||e.components.forEach(s=>{s.onCollisionEnter&&s.onCollisionEnter(t)})}checkOverlap(e,t){const s=[],i=e.getBounds();for(const n of this.colliders){if(n===e||t&&n.tag!==t)continue;const r=n.getBounds();this._intersects(e,i,n,r)&&s.push(n)}return s}_intersects(e,t,s,i){if(e.type==="box"&&s.type==="box")return t.left<i.right&&t.right>i.left&&t.top<i.bottom&&t.bottom>i.top;if(e.type==="circle"&&s.type==="circle"){const n=t.x-i.x,r=t.y-i.y;return Math.sqrt(n*n+r*r)<t.radius+i.radius}return!1}addBody(e){this.add(e)}removeBody(e){this.remove(e)}checkCollision(e,t){return this._intersects(e,e.getBounds(),t,t.getBounds())}raycast(e,t,s){return null}setGravity(e,t){}}const B=new _;console.warn("[DopamineJS] GlobalPhysics is deprecated. Use kernel.physics instead.");class x{constructor(){this.keys=new Map,this.mouse={x:0,y:0,buttons:new Map},this.kernel=null,this._onKeyDown=this._onKeyDown.bind(this),this._onKeyUp=this._onKeyUp.bind(this),this._onMouseMove=this._onMouseMove.bind(this),this._onMouseDown=this._onMouseDown.bind(this),this._onMouseUp=this._onMouseUp.bind(this)}init(e){this.kernel=e,window.addEventListener("keydown",this._onKeyDown),window.addEventListener("keyup",this._onKeyUp),window.addEventListener("mousemove",this._onMouseMove),window.addEventListener("mousedown",this._onMouseDown),window.addEventListener("mouseup",this._onMouseUp)}destroy(){window.removeEventListener("keydown",this._onKeyDown),window.removeEventListener("keyup",this._onKeyUp),window.removeEventListener("mousemove",this._onMouseMove),window.removeEventListener("mousedown",this._onMouseDown),window.removeEventListener("mouseup",this._onMouseUp)}_onKeyDown(e){this.keys.set(e.key,!0)}_onKeyUp(e){this.keys.set(e.key,!1)}_onMouseMove(e){this.mouse.x=e.clientX,this.mouse.y=e.clientY}_onMouseDown(e){this.mouse.buttons.set(e.button,!0)}_onMouseUp(e){this.mouse.buttons.set(e.button,!1)}isKeyDown(e){return this.keys.get(e)||!1}isMouseButtonDown(e=0){return this.mouse.buttons.get(e)||!1}}const F=new x;console.warn("[DopamineJS] GlobalInput is deprecated. Use kernel.input instead.");class S{constructor(){this.assets=new Map,this.kernel=null}init(e){this.kernel=e}destroy(){this.assets.clear()}async loadImage(e,t){return new Promise((s,i)=>{const n=new Image;n.onload=()=>{this.assets.set(e,n),s(n)},n.onerror=i,n.src=t})}get(e){return this.assets.get(e)}}const N=new S;console.warn("[DopamineJS] GlobalLoader is deprecated. Use kernel.loader instead.");class D{constructor(e={}){this.config=e,this.events=new m,this.systems=new I(this),this.plugins=new C(this),this._registerCoreSystems(e),this.fixedTimestep=e.fixedTimestep||1/60,this._accumulator=0,this._maxAccumulator=this.fixedTimestep*5}_registerCoreSystems(e){this.systems.register("ticker",new A,{priority:100}),this.systems.register("renderer",new O(e.renderer||{}),{priority:90}),this.systems.register("input",new x,{priority:80}),this.systems.register("loader",new S,{priority:70}),this.systems.register("physics",new _,{priority:60})}start(){const e=this.systems.get("ticker");e&&(e.add(this._update.bind(this)),e.start()),this.events.emit("kernel_started")}stop(){const e=this.systems.get("ticker");e&&e.stop(),this.events.emit("kernel_stopped")}_update(e){for(this.events.emit(m.Events.TICK,{dt:e}),this._accumulator+=e,this._accumulator>this._maxAccumulator&&(this._accumulator=this._maxAccumulator);this._accumulator>=this.fixedTimestep;)this.events.emit(m.Events.FIXED_UPDATE,{dt:this.fixedTimestep}),this.systems.fixedUpdate(this.fixedTimestep),this._accumulator-=this.fixedTimestep;this.systems.update(e),this.events.emit(m.Events.RENDER,{dt:e})}get physics(){return this.systems.get("physics")}get input(){return this.systems.get("input")}get loader(){return this.systems.get("loader")}get renderer(){return this.systems.get("renderer")}get ticker(){return this.systems.get("ticker")}destroy(){this.stop(),this.plugins.clear(),this.systems.clear(),this.events.clear()}}const v={init(a){},update(a){},fixedUpdate(a){},destroy(){}};class K{constructor(){this.kernel=null}init(e){this.kernel=e}update(e){}fixedUpdate(e){}destroy(){}}const X={...v,addBody(a){},removeBody(a){},checkCollision(a,e){},raycast(a,e,t){},setGravity(a,e){},step(a){}},H={...v,registerSound(a,e){},preloadSounds(a){},play(a,e){},playTone(a,e,t,s){},stop(a){},toggleMute(){},setVolume(a){}},q={...v,registerSprite(a,e){},registerEffect(a,e){},play(a,e,t,...s){},emit(a){},clear(){},confetti(a,e,t){},coinShower(a,e,t){},sparkle(a,e,t,s){},fire(a,e,t){},starBurst(a,e,t){}};class J{constructor(){this.children=[],this.game=null}onEnter(){}onExit(){}update(e){for(const t of this.children)t.update&&t.update(e)}add(e){return e.kernel=this.kernel,this.gameObjects.push(e),e}remove(e){const t=this.gameObjects.indexOf(e);t>-1&&this.gameObjects.splice(t,1)}update(e){for(const t of this.gameObjects)t.update(e)}render(e){for(const t of this.gameObjects)t.render(e)}}class L{constructor(e){this.game=e,this.currentScene=null}run(e){this.currentScene&&this.currentScene.onExit(),this.currentScene=e,this.currentScene.game=this.game,this.game.scene=e,e.onEnter()}}class W{constructor(e={}){this.config=e,this.kernel=new D(e),this.director=new L(this),this.kernel.events.on(m.Events.TICK,this._update.bind(this),10),this.kernel.events.on(m.Events.RENDER,this._render.bind(this),10)}get renderer(){return this.kernel.renderer}get ticker(){return this.kernel.ticker}get physics(){return this.kernel.physics}get input(){return this.kernel.input}start(){this.kernel.start()}stop(){this.kernel.stop()}setScene(e){e.kernel=this.kernel,this.director.run(e)}_update({dt:e}){this.director.currentScene&&this.director.currentScene.update(e)}_render({dt:e}){this.renderer.clear(),this.director.currentScene&&this.director.currentScene.render(this.renderer.ctx)}destroy(){this.kernel.destroy()}}class Y{constructor(e=0,t=0){this.x=e,this.y=t,this.rotation=0,this.scale={x:1,y:1},this.components=[],this.children=[],this.parent=null,this.kernel=null}addChild(e){return e.parent=this,e.kernel=this.kernel,this.children.push(e),e}addComponent(e){return e.gameObject=this,e.kernel=this.kernel,this.components.push(e),e.onAttach(),e}getComponent(e){return this.components.find(t=>t instanceof e)}update(e){for(const t of this.components)t.update&&t.update(e);for(const t of this.children)t.update(e)}render(e){e.save(),e.translate(this.x,this.y),e.rotate(this.rotation),e.scale(this.scale.x,this.scale.y);for(const t of this.components)t.render&&t.render(e);for(const t of this.children)t.render(e);e.restore()}}class k{constructor(e=0,t=0){this.x=e,this.y=t}add(e){return this.x+=e.x,this.y+=e.y,this}sub(e){return this.x-=e.x,this.y-=e.y,this}scale(e){return this.x*=e,this.y*=e,this}distance(e){const t=this.x-e.x,s=this.y-e.y;return Math.sqrt(t*t+s*s)}normalize(){const e=Math.sqrt(this.x*this.x+this.y*this.y);return e>0&&(this.x/=e,this.y/=e),this}clone(){return new k(this.x,this.y)}static distance(e,t){return Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2))}}class U extends f{constructor(e){super(),this.image=e,this.anchor={x:.5,y:.5},this.width=e?e.width:0,this.height=e?e.height:0,this.frame={x:0,y:0,w:this.width,h:this.height}}setFrame(e,t,s,i){this.frame={x:e,y:t,w:s,h:i},this.width=s,this.height=i}setTexture(e){this.image=e,e&&this.setFrame(0,0,e.width,e.height)}render(e){if(!this.image)return;const t=this.frame.w,s=this.frame.h,i=-t*this.anchor.x,n=-s*this.anchor.y;e.drawImage(this.image,this.frame.x,this.frame.y,t,s,i,n,t,s)}}class V extends f{constructor(){super(),this.animations={},this.currentAnim=null,this.timer=0,this.currentFrameIndex=0,this.sprite=null}onAttach(){this.sprite=this.gameObject.getComponent(U),this.sprite||console.warn("Animator attached to GameObject without a Sprite component.")}addAnimation(e,t,s=10,i=!0){this.animations[e]={frames:t,fps:s,loop:i}}play(e){this.currentAnim&&this.currentAnim.name===e||this.animations[e]&&(this.currentAnim={name:e,...this.animations[e]},this.currentFrameIndex=0,this.timer=0,this._updateFrame())}update(e){if(!this.currentAnim||!this.sprite)return;this.timer+=e;const t=1/this.currentAnim.fps;this.timer>=t&&(this.timer-=t,this.currentFrameIndex++,this.currentFrameIndex>=this.currentAnim.frames.length&&(this.currentAnim.loop?this.currentFrameIndex=0:this.currentFrameIndex=this.currentAnim.frames.length-1),this._updateFrame())}_updateFrame(){const e=this.currentAnim.frames[this.currentFrameIndex];this.sprite.setFrame(e.x,e.y,e.w,e.h)}}class Q{static createGround(e,t,s="#333"){const i=document.createElement("canvas");i.width=e,i.height=t;const n=i.getContext("2d");n.fillStyle=s,n.fillRect(0,0,e,t);for(let r=0;r<1e3;r++){n.fillStyle=Math.random()>.5?"#444":"#222";const h=Math.random()*4+2;n.fillRect(Math.random()*e,Math.random()*t,h,h)}n.strokeStyle="rgba(255,255,255,0.05)",n.lineWidth=2,n.beginPath();for(let r=0;r<=e;r+=64)n.moveTo(r,0),n.lineTo(r,t);for(let r=0;r<=t;r+=64)n.moveTo(0,r),n.lineTo(e,r);return n.stroke(),i}static createCharacterSheet(e,t){const n=document.createElement("canvas");n.width=64*4,n.height=64;const r=n.getContext("2d");for(let h=0;h<4;h++){const c=h*64,l=0,u=c+64/2,y=l+64/2,p=h%2===0?0:-2,w=h===1?5:h===3?-5:0;r.fillStyle=e,r.beginPath(),r.arc(u,y+p,20,0,Math.PI*2),r.fill(),r.fillStyle=t,r.beginPath(),r.arc(u-15,y+p+w,8,0,Math.PI*2),r.arc(u+15,y+p-w,8,0,Math.PI*2),r.fill(),r.fillStyle=Z(e),r.beginPath(),r.arc(u,y+p,15,0,Math.PI*2),r.fill(),t==="#333"&&(r.fillStyle="#000",r.fillRect(u+10,y+p-5,25,6))}return n}}function Z(a,e){return a}class ee extends f{constructor(e){super(),this.system=e}play(e,t={}){const s=this.gameObject.x,i=this.gameObject.y;switch(e){case"confetti":this.system.confetti(s,i,t.count);break;case"fire":this.system.fire(s,i,t.count);break;case"sparkle":this.system.sparkle(s,i,t.count,t.color);break;case"blood":this.system.emit({x:s,y:i,count:10,color:["#8a0303","#ff0000"],speed:4,gravity:.1,decay:.05});break;default:this.system.emit({x:s,y:i,...t})}}}class te extends f{constructor(){super(),this.intensity=0,this.duration=0,this.timer=0,this.originalPos={x:0,y:0},this.isShaking=!1}shake(e=5,t=.2){this.intensity=e,this.duration=t,this.timer=0,this.isShaking=!0,this.timer===0&&(this.originalPos.x=this.gameObject.x,this.originalPos.y=this.gameObject.y)}update(e){if(!this.isShaking)return;if(this.timer+=e,this.timer>=this.duration){this.isShaking=!1,this.gameObject.x=this.originalPos.x,this.gameObject.y=this.originalPos.y;return}const t=(Math.random()-.5)*2*this.intensity,s=(Math.random()-.5)*2*this.intensity;this.gameObject.x=this.originalPos.x+t,this.gameObject.y=this.originalPos.y+s}}o.Animator=V,o.Collider=$,o.Component=f,o.DataService=M,o.Director=L,o.DopamineKernel=D,o.EventBus=m,o.Game=W,o.GameObject=Y,o.GameUI=P,o.GlobalInput=F,o.GlobalLoader=N,o.GlobalPhysics=B,o.IAudioSystem=H,o.IParticleSystem=q,o.IPhysicsSystem=X,o.ISystem=v,o.Input=x,o.Loader=S,o.ParticleEmitter=ee,o.ParticleSystem=E,o.Physics=_,o.PluginRegistry=C,o.Renderer=O,o.RewardSystem=b,o.Scene=J,o.ScreenShake=te,o.SoundManager=T,o.Sprite=U,o.System=K,o.SystemRegistry=I,o.TextureGenerator=Q,o.Ticker=A,o.Vector2=k,o.default=G,Object.defineProperties(o,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
package/package.json
CHANGED
|
@@ -1,56 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dopaminejs",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"files": [
|
|
9
|
-
"dist",
|
|
10
|
-
"src",
|
|
11
|
-
"README.md",
|
|
12
|
-
"LICENSE",
|
|
13
|
-
"CHANGELOG.md",
|
|
14
|
-
"ARCHITECTURE.md"
|
|
15
|
-
],
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Lightweight Game Feel Engine - Add juice, rewards, and feedback to HTML5 games",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/dopamine.umd.js",
|
|
7
|
+
"module": "./dist/dopamine.mjs",
|
|
16
8
|
"exports": {
|
|
17
9
|
".": {
|
|
18
10
|
"import": "./dist/dopamine.mjs",
|
|
19
|
-
"require": "./dist/dopamine.umd.js"
|
|
20
|
-
"default": "./dist/dopamine.umd.js"
|
|
11
|
+
"require": "./dist/dopamine.umd.js"
|
|
21
12
|
},
|
|
22
13
|
"./style.css": "./dist/style.css"
|
|
23
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
24
19
|
"scripts": {
|
|
25
20
|
"dev": "vite",
|
|
26
21
|
"build": "vite build",
|
|
27
|
-
"preview": "vite preview",
|
|
28
22
|
"test": "vitest",
|
|
29
|
-
"
|
|
30
|
-
"prepublishOnly": "npm run build"
|
|
23
|
+
"preview": "vite preview"
|
|
31
24
|
},
|
|
32
25
|
"keywords": [
|
|
33
|
-
"game
|
|
34
|
-
"
|
|
26
|
+
"game",
|
|
27
|
+
"gamedev",
|
|
28
|
+
"game-feel",
|
|
29
|
+
"juice",
|
|
30
|
+
"feedback",
|
|
35
31
|
"rewards",
|
|
36
32
|
"particles",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
33
|
+
"sound",
|
|
34
|
+
"xp",
|
|
35
|
+
"achievements"
|
|
40
36
|
],
|
|
41
|
-
"author": "
|
|
42
|
-
"license": "
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"jsdom": "^27.2.0",
|
|
45
|
-
"vite": "^5.0.0",
|
|
46
|
-
"vitest": "^4.0.15"
|
|
47
|
-
},
|
|
37
|
+
"author": "BaryoDev",
|
|
38
|
+
"license": "MPL-2.0",
|
|
48
39
|
"repository": {
|
|
49
40
|
"type": "git",
|
|
50
|
-
"url": "https://github.com/BaryoDev/dopaminejs.git"
|
|
41
|
+
"url": "https://github.com/BaryoDev/dopaminejs.git",
|
|
42
|
+
"directory": "packages/dopaminejs"
|
|
51
43
|
},
|
|
52
44
|
"bugs": {
|
|
53
45
|
"url": "https://github.com/BaryoDev/dopaminejs/issues"
|
|
54
46
|
},
|
|
55
47
|
"homepage": "https://github.com/BaryoDev/dopaminejs#readme"
|
|
56
|
-
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Component } from '../core/Component.js';
|
|
2
|
+
import { Sprite } from './Sprite.js';
|
|
3
|
+
|
|
4
|
+
export class Animator extends Component {
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
this.animations = {}; // { name: { frames: [{x,y,w,h}], fps: 10, loop: true } }
|
|
8
|
+
this.currentAnim = null;
|
|
9
|
+
this.timer = 0;
|
|
10
|
+
this.currentFrameIndex = 0;
|
|
11
|
+
this.sprite = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
onAttach() {
|
|
15
|
+
this.sprite = this.gameObject.getComponent(Sprite);
|
|
16
|
+
if (!this.sprite) {
|
|
17
|
+
console.warn('Animator attached to GameObject without a Sprite component.');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
addAnimation(name, frames, fps = 10, loop = true) {
|
|
22
|
+
this.animations[name] = { frames, fps, loop };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
play(name) {
|
|
26
|
+
if (this.currentAnim && this.currentAnim.name === name) return;
|
|
27
|
+
|
|
28
|
+
if (this.animations[name]) {
|
|
29
|
+
this.currentAnim = { name, ...this.animations[name] };
|
|
30
|
+
this.currentFrameIndex = 0;
|
|
31
|
+
this.timer = 0;
|
|
32
|
+
this._updateFrame();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
update(dt) {
|
|
37
|
+
if (!this.currentAnim || !this.sprite) return;
|
|
38
|
+
|
|
39
|
+
this.timer += dt;
|
|
40
|
+
const interval = 1.0 / this.currentAnim.fps;
|
|
41
|
+
|
|
42
|
+
if (this.timer >= interval) {
|
|
43
|
+
this.timer -= interval;
|
|
44
|
+
this.currentFrameIndex++;
|
|
45
|
+
|
|
46
|
+
if (this.currentFrameIndex >= this.currentAnim.frames.length) {
|
|
47
|
+
if (this.currentAnim.loop) {
|
|
48
|
+
this.currentFrameIndex = 0;
|
|
49
|
+
} else {
|
|
50
|
+
this.currentFrameIndex = this.currentAnim.frames.length - 1;
|
|
51
|
+
// Could emit 'animationComplete' here
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
this._updateFrame();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_updateFrame() {
|
|
59
|
+
const frame = this.currentAnim.frames[this.currentFrameIndex];
|
|
60
|
+
this.sprite.setFrame(frame.x, frame.y, frame.w, frame.h);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Component } from './Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Collider Component
|
|
5
|
+
* Handles collision detection for GameObjects.
|
|
6
|
+
*/
|
|
7
|
+
export class Collider extends Component {
|
|
8
|
+
constructor(type = 'box', width = 50, height = 50, radius = 25) {
|
|
9
|
+
super();
|
|
10
|
+
this.type = type; // 'box' or 'circle'
|
|
11
|
+
this.width = width;
|
|
12
|
+
this.height = height;
|
|
13
|
+
this.radius = radius;
|
|
14
|
+
this.tag = null; // For collision filtering
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onAttach() {
|
|
18
|
+
super.onAttach();
|
|
19
|
+
// Register with physics system through kernel
|
|
20
|
+
if (this.physics) {
|
|
21
|
+
this.physics.add(this);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onDetach() {
|
|
26
|
+
super.onDetach();
|
|
27
|
+
// Unregister from physics
|
|
28
|
+
if (this.physics) {
|
|
29
|
+
this.physics.remove(this);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getBounds() {
|
|
34
|
+
const go = this.gameObject;
|
|
35
|
+
if (this.type === 'box') {
|
|
36
|
+
return {
|
|
37
|
+
left: go.x - this.width / 2,
|
|
38
|
+
right: go.x + this.width / 2,
|
|
39
|
+
top: go.y - this.height / 2,
|
|
40
|
+
bottom: go.y + this.height / 2,
|
|
41
|
+
};
|
|
42
|
+
} else if (this.type === 'circle') {
|
|
43
|
+
return {
|
|
44
|
+
x: go.x,
|
|
45
|
+
y: go.y,
|
|
46
|
+
radius: this.radius,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all logic components attached to GameObjects.
|
|
3
|
+
*/
|
|
4
|
+
export class Component {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.gameObject = null;
|
|
7
|
+
this.kernel = null; // Injected by GameObject
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Access systems through kernel (no more globals!)
|
|
12
|
+
*/
|
|
13
|
+
get physics() {
|
|
14
|
+
return this.kernel?.systems.get('physics');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get input() {
|
|
18
|
+
return this.kernel?.systems.get('input');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get loader() {
|
|
22
|
+
return this.kernel?.systems.get('loader');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get events() {
|
|
26
|
+
return this.kernel?.events;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onAttach() {
|
|
30
|
+
// Subclasses can override
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onDetach() { }
|
|
34
|
+
|
|
35
|
+
update(dt) { }
|
|
36
|
+
|
|
37
|
+
render(ctx) { }
|
|
38
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { EventBus } from './EventBus.js';
|
|
2
|
+
import { SystemRegistry } from './SystemRegistry.js';
|
|
3
|
+
import { PluginRegistry } from './PluginRegistry.js';
|
|
4
|
+
import { Renderer } from '../renderer/Renderer.js';
|
|
5
|
+
import { Ticker } from '../systems/Ticker.js';
|
|
6
|
+
import { Physics } from '../systems/Physics.js';
|
|
7
|
+
import { Input } from '../systems/Input.js';
|
|
8
|
+
import { Loader } from '../systems/Loader.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* DopamineKernel - Central orchestrator for the engine
|
|
12
|
+
*
|
|
13
|
+
* Replaces global singletons and provides dependency injection for all systems.
|
|
14
|
+
* This is the new foundation that enables the plugin architecture.
|
|
15
|
+
*/
|
|
16
|
+
export class DopamineKernel {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
|
|
20
|
+
// Core registries
|
|
21
|
+
this.events = new EventBus();
|
|
22
|
+
this.systems = new SystemRegistry(this);
|
|
23
|
+
this.plugins = new PluginRegistry(this);
|
|
24
|
+
|
|
25
|
+
// Register core systems
|
|
26
|
+
this._registerCoreSystems(config);
|
|
27
|
+
|
|
28
|
+
// Fixed timestep for physics (60 FPS)
|
|
29
|
+
this.fixedTimestep = config.fixedTimestep || 1 / 60;
|
|
30
|
+
this._accumulator = 0;
|
|
31
|
+
this._maxAccumulator = this.fixedTimestep * 5; // Prevent spiral of death
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register core engine systems
|
|
36
|
+
* These can be replaced by plugins
|
|
37
|
+
* @private
|
|
38
|
+
*/
|
|
39
|
+
_registerCoreSystems(config) {
|
|
40
|
+
// Ticker - highest priority, runs first
|
|
41
|
+
this.systems.register('ticker', new Ticker(), {
|
|
42
|
+
priority: 100
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Renderer
|
|
46
|
+
this.systems.register('renderer', new Renderer(config.renderer || {}), {
|
|
47
|
+
priority: 90
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Input system
|
|
51
|
+
this.systems.register('input', new Input(), {
|
|
52
|
+
priority: 80
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Loader/Asset system
|
|
56
|
+
this.systems.register('loader', new Loader(), {
|
|
57
|
+
priority: 70
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Physics - depends on nothing, but runs in fixedUpdate
|
|
61
|
+
this.systems.register('physics', new Physics(), {
|
|
62
|
+
priority: 60
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Start the kernel (begins game loop)
|
|
68
|
+
*/
|
|
69
|
+
start() {
|
|
70
|
+
const ticker = this.systems.get('ticker');
|
|
71
|
+
if (ticker) {
|
|
72
|
+
ticker.add(this._update.bind(this));
|
|
73
|
+
ticker.start();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.events.emit('kernel_started');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Stop the kernel
|
|
81
|
+
*/
|
|
82
|
+
stop() {
|
|
83
|
+
const ticker = this.systems.get('ticker');
|
|
84
|
+
if (ticker) {
|
|
85
|
+
ticker.stop();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.events.emit('kernel_stopped');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Main update loop
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
_update(dt) {
|
|
96
|
+
// Emit tick event (high priority listeners go first)
|
|
97
|
+
this.events.emit(EventBus.Events.TICK, { dt });
|
|
98
|
+
|
|
99
|
+
// Fixed timestep for physics
|
|
100
|
+
this._accumulator += dt;
|
|
101
|
+
|
|
102
|
+
// Clamp accumulator to prevent spiral of death
|
|
103
|
+
if (this._accumulator > this._maxAccumulator) {
|
|
104
|
+
this._accumulator = this._maxAccumulator;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Run fixed updates
|
|
108
|
+
while (this._accumulator >= this.fixedTimestep) {
|
|
109
|
+
this.events.emit(EventBus.Events.FIXED_UPDATE, { dt: this.fixedTimestep });
|
|
110
|
+
this.systems.fixedUpdate(this.fixedTimestep);
|
|
111
|
+
this._accumulator -= this.fixedTimestep;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Run variable timestep updates
|
|
115
|
+
this.systems.update(dt);
|
|
116
|
+
|
|
117
|
+
// Emit render event
|
|
118
|
+
this.events.emit(EventBus.Events.RENDER, { dt });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Shorthand accessors for common systems (backward compatibility)
|
|
123
|
+
*/
|
|
124
|
+
get physics() {
|
|
125
|
+
return this.systems.get('physics');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get input() {
|
|
129
|
+
return this.systems.get('input');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get loader() {
|
|
133
|
+
return this.systems.get('loader');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get renderer() {
|
|
137
|
+
return this.systems.get('renderer');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get ticker() {
|
|
141
|
+
return this.systems.get('ticker');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Destroy the kernel and cleanup all systems
|
|
146
|
+
*/
|
|
147
|
+
destroy() {
|
|
148
|
+
this.stop();
|
|
149
|
+
this.plugins.clear();
|
|
150
|
+
this.systems.clear();
|
|
151
|
+
this.events.clear();
|
|
152
|
+
}
|
|
153
|
+
}
|