dopaminejs 1.1.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/ParticleSystem.js +327 -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 -27
- package/README.md +0 -213
- package/dist/dopamine.mjs +0 -724
- package/dist/dopamine.umd.js +0 -45
- package/src/effects/ParticleSystem.js +0 -257
- /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/{ui → dopamine/ui}/GameUI.js +0 -0
- /package/src/{ui → dopamine/ui}/dopamine.css +0 -0
package/dist/dopamine.umd.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
(function(r,h){typeof exports=="object"&&typeof module<"u"?h(exports):typeof define=="function"&&define.amd?define(["exports"],h):(r=typeof globalThis<"u"?globalThis:r||self,h(r.Dopamine={}))})(this,function(r){"use strict";class h{constructor(){this.listeners=new Map}on(t,e){return this.listeners.has(t)||this.listeners.set(t,new Set),this.listeners.get(t).add(e),()=>this.off(t,e)}off(t,e){this.listeners.has(t)&&this.listeners.get(t).delete(e)}emit(t,e){this.listeners.has(t)&&this.listeners.get(t).forEach(s=>{try{s(e)}catch(i){console.error(`Error in listener for event '${t}':`,i)}})}clear(){this.listeners.clear()}}class p extends h{constructor(t,e={}){super(),this.dataService=t,this.player=null,this.achievements={...this._initAchievements(),...e.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(t,e=""){const s=this.player.level;this.player.xp+=t;const i=this._calculateLevel(this.player.xp),a=i>s;return a&&(this.player.level=i),await this.save(),this.emit("xp_gained",{amount:t,reason:e,leveledUp:a,newLevel:i}),a&&this.emit("level_up",{oldLevel:s,newLevel:i}),{leveledUp:a,newLevel:i,xpGained:t}}_calculateLevel(t){const e=Math.floor((1+Math.sqrt(1+8*t/50))/2);return Math.max(1,e)}getXPForNextLevel(){const t=this.player.level+1,e=50*t*(t-1),s=50*this.player.level*(this.player.level-1);return{total:e,needed:e-this.player.xp,progress:(this.player.xp-s)/(e-s)}}async recordGame(t,e){this.player.stats[t]||(this.player.stats[t]={totalPlays:0,highScore:0});const s=this.player.stats[t];s.totalPlays++,this.player.totalGamesPlayed++,e.score!==void 0&&e.score>s.highScore&&(s.highScore=e.score,this.emit("new_high_score",{gameName:t,score:e.score})),Object.keys(e).forEach(a=>{a!=="score"&&(s[a]=(s[a]||0)+(typeof e[a]=="number"?e[a]:0))});let i=10;e.score&&(i+=Math.floor(e.score/5)),i=Math.floor(i*this.getStreakMultiplier()),await this.addXP(i,`Played ${t}`),await this.checkAchievements(t,e),await this.save()}async checkAchievements(t,e){const s=[];for(const[i,a]of Object.entries(this.achievements))this.player.achievements[i]||a.check(this.player,t,e)&&(await this.unlockAchievement(i),s.push(a));return s}async unlockAchievement(t){const e=this.achievements[t];return e?(this.player.achievements[t]={unlockedAt:Date.now(),seen:!1},await this.addXP(e.xp,`Achievement: ${e.name}`),this.emit("achievement_unlocked",e),await this.save(),!0):!1}getUnlockedAchievements(){return Object.entries(this.player.achievements).map(([t,e])=>({...this.achievements[t],id:t,...e}))}getUnseenAchievements(){return this.getUnlockedAchievements().filter(t=>!t.seen)}async markAchievementsSeen(t){for(const e of t)this.player.achievements[e]&&(this.player.achievements[e].seen=!0);await this.save()}_updateDailyStreak(){const t=this._getTodayDateString(),e=this.player.streak.lastPlayDate;if(e===t)return;const s=this._getYesterdayDateString();e===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=t}getStreakMultiplier(){const t=this.player.streak.current;return t>=7?2:t>=5?1.5:t>=3?1.25:1}_getTodayDateString(){return new Date().toISOString().split("T")[0]}_getYesterdayDateString(){const t=new Date;return t.setDate(t.getDate()-1),t.toISOString().split("T")[0]}_initAchievements(){return{first_game:{name:"First Steps",description:"Play your first game",icon:"🎮",xp:50,check:t=>t.totalGamesPlayed>=1},level_5:{name:"Rising Star",description:"Reach level 5",icon:"⭐",xp:100,check:t=>t.level>=5},level_10:{name:"Expert Player",description:"Reach level 10",icon:"🏆",xp:250,check:t=>t.level>=10},streak_3:{name:"Dedicated",description:"Play 3 days in a row",icon:"🔥",xp:100,check:t=>t.streak.current>=3},streak_7:{name:"Unstoppable",description:"Play 7 days in a row",icon:"💥",xp:300,check:t=>t.streak.current>=7}}}}class u{constructor(t={}){this.storage=t.storage||window.localStorage,this.prefix=t.prefix||"dopamine_"}async save(t,e){try{const s=JSON.stringify(e);return this.storage.setItem(this.prefix+t,s),!0}catch(s){return console.error("Error saving data:",s),!1}}async load(t,e=null){try{const s=this.storage.getItem(this.prefix+t);return s?JSON.parse(s):e}catch(s){return console.error("Error loading data:",s),e}}async clear(t){this.storage.removeItem(this.prefix+t)}}class m{constructor(t){this.particleSystem=t,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(t,e,s){const i=t/s*100;this.xpBar.style.width=`${Math.min(100,i)}%`,this.xpText.textContent=`${t} / ${s} XP`,this.xpBar.style.animation="none",setTimeout(()=>{this.xpBar.style.animation="xp-pulse 0.3s ease-out"},10)}updateLevel(t){this.levelDisplay.textContent=t;const e=document.getElementById("level-badge");e.style.animation="none",setTimeout(()=>{e.style.animation="level-pulse 0.5s ease-out"},10)}updateStreak(t){this.streakDisplay.textContent=t;const e=document.getElementById("streak-badge");t>=7?e.style.background="linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)":t>=3&&(e.style.background="linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)")}showAchievement(t){const e=document.createElement("div");e.className="achievement-popup",e.innerHTML=`
|
|
21
|
-
<div class="achievement-shine"></div>
|
|
22
|
-
<div class="achievement-content">
|
|
23
|
-
<div class="achievement-icon">${t.icon}</div>
|
|
24
|
-
<div class="achievement-info">
|
|
25
|
-
<div class="achievement-title">Achievement Unlocked!</div>
|
|
26
|
-
<div class="achievement-name">${t.name}</div>
|
|
27
|
-
<div class="achievement-desc">${t.description}</div>
|
|
28
|
-
<div class="achievement-xp">+${t.xp} XP</div>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
`,document.body.appendChild(e),setTimeout(()=>{const s=e.getBoundingClientRect();this.particleSystem.confetti(s.left+s.width/2,s.top+s.height/2,40)},300),setTimeout(()=>{e.style.animation="slideOut 0.3s ease-in forwards",setTimeout(()=>e.remove(),300)},4e3)}showLevelUp(t,e){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">${e}</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 a=Math.random()*window.innerWidth,n=Math.random()*window.innerHeight*.6;this.particleSystem.confetti(a,n,30)},i*200)},300),setTimeout(()=>{s.style.opacity="0",setTimeout(()=>s.remove(),500)},2500)}showFloatingText(t,e,s,i="#ffd700",a="24px"){const n=document.createElement("div");n.className="floating-text",n.textContent=t,n.style.left=e+"px",n.style.top=s+"px",n.style.color=i,n.style.fontSize=a,document.body.appendChild(n),setTimeout(()=>n.remove(),2e3)}showCombo(t,e,s){this.showFloatingText(`${t}× COMBO!`,e,s,"#ff6b6b","32px"),this.particleSystem.fire(e,s,20)}showNearMiss(t,e){this.showFloatingText("CLOSE CALL!",t,e,"#45b7d1","20px"),this.particleSystem.sparkle(t,e,15,"#45b7d1")}showLuckyMoment(t,e,s){this.showFloatingText(`LUCKY ${t}×!`,e,s,"#ffd700","36px"),this.particleSystem.starBurst(e,s,12),this.particleSystem.confetti(e,s,25)}showNotification(t,e="normal"){const s=window.innerWidth/2,i=window.innerHeight/3;e==="legendary"?this.showLuckyMoment(t,s,i):e==="rare"?this.showCombo(t,s,i):this.showFloatingText(t,s,i,"#fff","24px")}showSummary(t){const e=document.createElement("div");e.className="message-box",e.style.display="flex",e.style.position="absolute",e.style.top="50%",e.style.left="50%",e.style.transform="translate(-50%, -50%)",e.style.zIndex="1000";let s="";t.metrics&&(s=Object.entries(t.metrics).map(([i,a])=>`<div style="display:flex;justify-content:space-between;width:100%"><span>${i}:</span> <strong>${a}</strong></div>`).join("")),e.innerHTML=`
|
|
38
|
-
<h1>Game Over</h1>
|
|
39
|
-
<div style="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;">${t.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(e)}}class y{constructor(t={}){this.canvasId=t.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.position="fixed",this.canvas.style.top="0",this.canvas.style.left="0",this.canvas.style.width="100%",this.canvas.style.height="100%",this.canvas.style.pointerEvents="none",this.canvas.style.zIndex=t.zIndex||"9999",document.body.appendChild(this.canvas)),this.ctx=this.canvas.getContext("2d"),this.particles=[],this.isAnimating=!1,this._resize(),window.addEventListener("resize",()=>this._resize())}_resize(){this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight}confetti(t,e,s=50){const i=["#ff6b6b","#4ecdc4","#45b7d1","#f7dc6f","#c06c84","#6c5ce7"];for(let a=0;a<s;a++)this.particles.push({x:t,y:e,vx:(Math.random()-.5)*10,vy:(Math.random()-.5)*10-5,rotation:Math.random()*Math.PI*2,rotationSpeed:(Math.random()-.5)*.2,color:i[Math.floor(Math.random()*i.length)],size:Math.random()*8+4,life:1,decay:.015+Math.random()*.01,gravity:.3,type:"confetti"});this._startAnimation()}sparkle(t,e,s=20,i="#ffd700"){for(let a=0;a<s;a++){const n=Math.PI*2/s*a,o=2+Math.random()*3;this.particles.push({x:t,y:e,vx:Math.cos(n)*o,vy:Math.sin(n)*o,color:i,size:Math.random()*4+2,life:1,decay:.02,gravity:-.1,type:"spark"})}this._startAnimation()}coinShower(t,e,s=10){for(let i=0;i<s;i++)this.particles.push({x:t+(Math.random()-.5)*100,y:e-Math.random()*50,vx:(Math.random()-.5)*3,vy:Math.random()*-2-3,color:"#ffd700",size:12,life:1,decay:.012,gravity:.4,type:"coin"});this._startAnimation()}fire(t,e,s=15){const i=["#ff6b00","#ff8800","#ffaa00","#ff4400"];for(let a=0;a<s;a++)this.particles.push({x:t+(Math.random()-.5)*20,y:e,vx:(Math.random()-.5)*2,vy:-Math.random()*3-2,color:i[Math.floor(Math.random()*i.length)],size:Math.random()*6+3,life:1,decay:.025,gravity:-.2,type:"fire"});this._startAnimation()}starBurst(t,e,s=8){for(let i=0;i<s;i++){const a=Math.PI*2/s*i,n=4;this.particles.push({x:t,y:e,vx:Math.cos(a)*n,vy:Math.sin(a)*n,rotation:a,color:"#ffffff",size:15,life:1,decay:.03,gravity:0,type:"star"})}this._startAnimation()}_startAnimation(){this.isAnimating||(this.isAnimating=!0,this._animate())}_animate(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);for(let t=this.particles.length-1;t>=0;t--){const e=this.particles[t];if(e.vy+=e.gravity,e.x+=e.vx,e.y+=e.vy,e.life-=e.decay,e.rotationSpeed&&(e.rotation+=e.rotationSpeed),e.life<=0){this.particles.splice(t,1);continue}this.ctx.save(),this.ctx.globalAlpha=e.life,this.ctx.fillStyle=e.color,e.type==="confetti"?(this.ctx.translate(e.x,e.y),this.ctx.rotate(e.rotation),this.ctx.fillRect(-e.size/2,-e.size/2,e.size,e.size/2)):e.type==="coin"?(this.ctx.beginPath(),this.ctx.arc(e.x,e.y,e.size,0,Math.PI*2),this.ctx.fill(),this.ctx.fillStyle="#ffee88",this.ctx.beginPath(),this.ctx.arc(e.x-e.size/3,e.y-e.size/3,e.size/3,0,Math.PI*2),this.ctx.fill()):e.type==="star"?this._drawStar(e.x,e.y,5,e.size,e.size/2,e.rotation):(this.ctx.beginPath(),this.ctx.arc(e.x,e.y,e.size,0,Math.PI*2),this.ctx.fill()),this.ctx.restore()}this.particles.length>0?requestAnimationFrame(()=>this._animate()):this.isAnimating=!1}_drawStar(t,e,s,i,a,n){let o=Math.PI/2*3+n;const f=Math.PI/s;this.ctx.beginPath(),this.ctx.moveTo(t,e-i);for(let g=0;g<s;g++){let c=t+Math.cos(o)*i,d=e+Math.sin(o)*i;this.ctx.lineTo(c,d),o+=f,c=t+Math.cos(o)*a,d=e+Math.sin(o)*a,this.ctx.lineTo(c,d),o+=f}this.ctx.lineTo(t,e-i),this.ctx.closePath(),this.ctx.fill()}clear(){this.particles=[],this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}}class v{constructor(t={}){this.audioContext=null,this.storageKey=t.storageKey||"dopamineSoundsMuted",this.muted=localStorage.getItem(this.storageKey)==="true",this.assets=new Map,this.soundPack=t.soundPack||"synth",this.customSounds=t.customSounds||{},Object.keys(this.customSounds).length>0&&this.preloadSounds(this.customSounds)}initAudio(){if(!this.audioContext){const t=window.AudioContext||window.webkitAudioContext;this.audioContext=new t}this.audioContext.state==="suspended"&&this.audioContext.resume()}toggleMute(){return this.muted=!this.muted,localStorage.setItem(this.storageKey,this.muted),this.muted}registerSound(t,e){this.customSounds[t]=e,this.audioContext&&this.loadSound(t,e)}async preloadSounds(t){const e=Object.entries(t).map(([s,i])=>this.loadSound(s,i));await Promise.allSettled(e)}async loadSound(t,e){try{if(!this.audioContext)return;const i=await(await fetch(e)).arrayBuffer(),a=await this.audioContext.decodeAudioData(i);this.assets.set(t,a)}catch(s){console.warn(`[DopamineJS] Failed to load sound '${t}':`,s)}}async play(t){if(this.muted)return;if(this.initAudio(),this.assets.has(t)){this._playBuffer(this.assets.get(t));return}if(this.customSounds[t]&&!this.assets.has(t)&&(await this.loadSound(t,this.customSounds[t]),this.assets.has(t))){this._playBuffer(this.assets.get(t));return}const e={jump:()=>this.playJump(!0),score:()=>this.playScore(!0),success:()=>this.playSuccess(!0),click:()=>this.playClick(!0),error:()=>this.playError(!0),gameover:()=>this.playGameOver(!0)};e[t]&&e[t]()}_playBuffer(t){const e=this.audioContext.createBufferSource();e.buffer=t,e.connect(this.audioContext.destination),e.start(0)}playTone(t,e,s="sine",i=.3){if(this.muted)return;this.initAudio();const a=this.audioContext.createOscillator(),n=this.audioContext.createGain();a.connect(n),n.connect(this.audioContext.destination),a.frequency.value=t,a.type=s,n.gain.setValueAtTime(i,this.audioContext.currentTime),n.gain.exponentialRampToValueAtTime(.01,this.audioContext.currentTime+e),a.start(this.audioContext.currentTime),a.stop(this.audioContext.currentTime+e)}playJump(t=!1){if(!t&&this.assets.has("jump"))return this.play("jump");this.playTone(400,.1,"square",.2)}playScore(t=!1){if(!t&&this.assets.has("score"))return this.play("score");this.playTone(800,.15,"sine",.25),setTimeout(()=>this.playTone(1e3,.15,"sine",.25),50)}playGameOver(t=!1){if(!t&&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(t=!1){if(!t&&this.assets.has("click"))return this.play("click");this.playTone(600,.05,"sine",.1)}playSuccess(t=!1){if(!t&&this.assets.has("success"))return this.play("success");this.playTone(600,.1,"sine",.2),setTimeout(()=>this.playTone(800,.2,"sine",.2),100)}playError(t=!1){if(!t&&this.assets.has("error"))return this.play("error");this.playTone(200,.2,"sawtooth",.3)}}class x{constructor(t={}){this.config=t,this.dataService=new u(t.data||{}),this.rewardSystem=new p(this.dataService,t.rewards||{}),this.soundManager=new v(t.sound||{}),this.particleSystem=new y(t.particles||{}),this.gameUI=new m(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",t=>{const{total:e,needed:s}=this.rewardSystem.getXPForNextLevel();this.gameUI.updateXP(this.rewardSystem.player.xp,s,e),t.xpGained>0}),this.rewardSystem.on("level_up",t=>{this.gameUI.updateLevel(t.newLevel),this.gameUI.showLevelUp(t.oldLevel,t.newLevel)}),this.rewardSystem.on("achievement_unlocked",t=>{this.gameUI.showAchievement(t)}),this.rewardSystem.on("new_high_score",t=>{this.gameUI.showNotification(`New High Score: ${t.score}!`,"legendary"),this.soundManager.playSuccess()})}}r.DataService=u,r.GameUI=m,r.ParticleSystem=y,r.RewardSystem=p,r.SoundManager=v,r.default=x,Object.defineProperties(r,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Particle Effect System
|
|
3
|
-
* Creates satisfying visual feedback for achievements and rewards
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export class ParticleSystem {
|
|
7
|
-
constructor(config = {}) {
|
|
8
|
-
this.canvasId = config.canvasId || 'dopamine-particle-canvas';
|
|
9
|
-
this.canvas = document.getElementById(this.canvasId);
|
|
10
|
-
|
|
11
|
-
if (!this.canvas) {
|
|
12
|
-
// Create overlay canvas if it doesn't exist
|
|
13
|
-
this.canvas = document.createElement('canvas');
|
|
14
|
-
this.canvas.id = this.canvasId;
|
|
15
|
-
this.canvas.style.position = 'fixed';
|
|
16
|
-
this.canvas.style.top = '0';
|
|
17
|
-
this.canvas.style.left = '0';
|
|
18
|
-
this.canvas.style.width = '100%';
|
|
19
|
-
this.canvas.style.height = '100%';
|
|
20
|
-
this.canvas.style.pointerEvents = 'none';
|
|
21
|
-
this.canvas.style.zIndex = config.zIndex || '9999';
|
|
22
|
-
document.body.appendChild(this.canvas);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
this.ctx = this.canvas.getContext('2d');
|
|
26
|
-
this.particles = [];
|
|
27
|
-
this.isAnimating = false;
|
|
28
|
-
|
|
29
|
-
this._resize();
|
|
30
|
-
window.addEventListener('resize', () => this._resize());
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
_resize() {
|
|
34
|
-
this.canvas.width = window.innerWidth;
|
|
35
|
-
this.canvas.height = window.innerHeight;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Confetti explosion (for achievements, level ups)
|
|
40
|
-
*/
|
|
41
|
-
confetti(x, y, count = 50) {
|
|
42
|
-
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f7dc6f', '#c06c84', '#6c5ce7'];
|
|
43
|
-
|
|
44
|
-
for (let i = 0; i < count; i++) {
|
|
45
|
-
this.particles.push({
|
|
46
|
-
x, y,
|
|
47
|
-
vx: (Math.random() - 0.5) * 10,
|
|
48
|
-
vy: (Math.random() - 0.5) * 10 - 5,
|
|
49
|
-
rotation: Math.random() * Math.PI * 2,
|
|
50
|
-
rotationSpeed: (Math.random() - 0.5) * 0.2,
|
|
51
|
-
color: colors[Math.floor(Math.random() * colors.length)],
|
|
52
|
-
size: Math.random() * 8 + 4,
|
|
53
|
-
life: 1.0,
|
|
54
|
-
decay: 0.015 + Math.random() * 0.01,
|
|
55
|
-
gravity: 0.3,
|
|
56
|
-
type: 'confetti'
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
this._startAnimation();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Sparkle effect (for score milestones, combos)
|
|
65
|
-
*/
|
|
66
|
-
sparkle(x, y, count = 20, color = '#ffd700') {
|
|
67
|
-
for (let i = 0; i < count; i++) {
|
|
68
|
-
const angle = (Math.PI * 2 / count) * i;
|
|
69
|
-
const speed = 2 + Math.random() * 3;
|
|
70
|
-
|
|
71
|
-
this.particles.push({
|
|
72
|
-
x, y,
|
|
73
|
-
vx: Math.cos(angle) * speed,
|
|
74
|
-
vy: Math.sin(angle) * speed,
|
|
75
|
-
color,
|
|
76
|
-
size: Math.random() * 4 + 2,
|
|
77
|
-
life: 1.0,
|
|
78
|
-
decay: 0.02,
|
|
79
|
-
gravity: -0.1, // Slight upward drift
|
|
80
|
-
type: 'spark'
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
this._startAnimation();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Coin shower (for collecting coins)
|
|
89
|
-
*/
|
|
90
|
-
coinShower(x, y, count = 10) {
|
|
91
|
-
for (let i = 0; i < count; i++) {
|
|
92
|
-
this.particles.push({
|
|
93
|
-
x: x + (Math.random() - 0.5) * 100,
|
|
94
|
-
y: y - Math.random() * 50,
|
|
95
|
-
vx: (Math.random() - 0.5) * 3,
|
|
96
|
-
vy: Math.random() * -2 - 3,
|
|
97
|
-
color: '#ffd700',
|
|
98
|
-
size: 12,
|
|
99
|
-
life: 1.0,
|
|
100
|
-
decay: 0.012,
|
|
101
|
-
gravity: 0.4,
|
|
102
|
-
type: 'coin'
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
this._startAnimation();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Fire particles (for streaks, combos)
|
|
111
|
-
*/
|
|
112
|
-
fire(x, y, count = 15) {
|
|
113
|
-
const colors = ['#ff6b00', '#ff8800', '#ffaa00', '#ff4400'];
|
|
114
|
-
|
|
115
|
-
for (let i = 0; i < count; i++) {
|
|
116
|
-
this.particles.push({
|
|
117
|
-
x: x + (Math.random() - 0.5) * 20,
|
|
118
|
-
y,
|
|
119
|
-
vx: (Math.random() - 0.5) * 2,
|
|
120
|
-
vy: -Math.random() * 3 - 2,
|
|
121
|
-
color: colors[Math.floor(Math.random() * colors.length)],
|
|
122
|
-
size: Math.random() * 6 + 3,
|
|
123
|
-
life: 1.0,
|
|
124
|
-
decay: 0.025,
|
|
125
|
-
gravity: -0.2, // Upward
|
|
126
|
-
type: 'fire'
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
this._startAnimation();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Star burst (for "perfect" moments)
|
|
135
|
-
*/
|
|
136
|
-
starBurst(x, y, count = 8) {
|
|
137
|
-
for (let i = 0; i < count; i++) {
|
|
138
|
-
const angle = (Math.PI * 2 / count) * i;
|
|
139
|
-
const speed = 4;
|
|
140
|
-
|
|
141
|
-
this.particles.push({
|
|
142
|
-
x, y,
|
|
143
|
-
vx: Math.cos(angle) * speed,
|
|
144
|
-
vy: Math.sin(angle) * speed,
|
|
145
|
-
rotation: angle,
|
|
146
|
-
color: '#ffffff',
|
|
147
|
-
size: 15,
|
|
148
|
-
life: 1.0,
|
|
149
|
-
decay: 0.03,
|
|
150
|
-
gravity: 0,
|
|
151
|
-
type: 'star'
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
this._startAnimation();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
_startAnimation() {
|
|
159
|
-
if (!this.isAnimating) {
|
|
160
|
-
this.isAnimating = true;
|
|
161
|
-
this._animate();
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
_animate() {
|
|
166
|
-
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
167
|
-
|
|
168
|
-
// Update and draw particles
|
|
169
|
-
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|
170
|
-
const p = this.particles[i];
|
|
171
|
-
|
|
172
|
-
// Update physics
|
|
173
|
-
p.vy += p.gravity;
|
|
174
|
-
p.x += p.vx;
|
|
175
|
-
p.y += p.vy;
|
|
176
|
-
p.life -= p.decay;
|
|
177
|
-
|
|
178
|
-
if (p.rotationSpeed) {
|
|
179
|
-
p.rotation += p.rotationSpeed;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Remove dead particles
|
|
183
|
-
if (p.life <= 0) {
|
|
184
|
-
this.particles.splice(i, 1);
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Draw particle
|
|
189
|
-
this.ctx.save();
|
|
190
|
-
this.ctx.globalAlpha = p.life;
|
|
191
|
-
this.ctx.fillStyle = p.color;
|
|
192
|
-
|
|
193
|
-
if (p.type === 'confetti') {
|
|
194
|
-
this.ctx.translate(p.x, p.y);
|
|
195
|
-
this.ctx.rotate(p.rotation);
|
|
196
|
-
this.ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size / 2);
|
|
197
|
-
} else if (p.type === 'coin') {
|
|
198
|
-
this.ctx.beginPath();
|
|
199
|
-
this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
200
|
-
this.ctx.fill();
|
|
201
|
-
// Add shine
|
|
202
|
-
this.ctx.fillStyle = '#ffee88';
|
|
203
|
-
this.ctx.beginPath();
|
|
204
|
-
this.ctx.arc(p.x - p.size / 3, p.y - p.size / 3, p.size / 3, 0, Math.PI * 2);
|
|
205
|
-
this.ctx.fill();
|
|
206
|
-
} else if (p.type === 'star') {
|
|
207
|
-
this._drawStar(p.x, p.y, 5, p.size, p.size / 2, p.rotation);
|
|
208
|
-
} else {
|
|
209
|
-
// Default: circle
|
|
210
|
-
this.ctx.beginPath();
|
|
211
|
-
this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
212
|
-
this.ctx.fill();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
this.ctx.restore();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Continue animation if particles remain
|
|
219
|
-
if (this.particles.length > 0) {
|
|
220
|
-
requestAnimationFrame(() => this._animate());
|
|
221
|
-
} else {
|
|
222
|
-
this.isAnimating = false;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
_drawStar(cx, cy, spikes, outerRadius, innerRadius, rotation) {
|
|
227
|
-
let rot = Math.PI / 2 * 3 + rotation;
|
|
228
|
-
const step = Math.PI / spikes;
|
|
229
|
-
|
|
230
|
-
this.ctx.beginPath();
|
|
231
|
-
this.ctx.moveTo(cx, cy - outerRadius);
|
|
232
|
-
|
|
233
|
-
for (let i = 0; i < spikes; i++) {
|
|
234
|
-
let x = cx + Math.cos(rot) * outerRadius;
|
|
235
|
-
let y = cy + Math.sin(rot) * outerRadius;
|
|
236
|
-
this.ctx.lineTo(x, y);
|
|
237
|
-
rot += step;
|
|
238
|
-
|
|
239
|
-
x = cx + Math.cos(rot) * innerRadius;
|
|
240
|
-
y = cy + Math.sin(rot) * innerRadius;
|
|
241
|
-
this.ctx.lineTo(x, y);
|
|
242
|
-
rot += step;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
this.ctx.lineTo(cx, cy - outerRadius);
|
|
246
|
-
this.ctx.closePath();
|
|
247
|
-
this.ctx.fill();
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Clear all particles
|
|
252
|
-
*/
|
|
253
|
-
clear() {
|
|
254
|
-
this.particles = [];
|
|
255
|
-
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|