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.
Files changed (46) hide show
  1. package/dist/dopamine.js +1947 -0
  2. package/dist/dopamine.umd.cjs +45 -0
  3. package/package.json +24 -32
  4. package/src/core/Animator.js +62 -0
  5. package/src/core/Collider.js +50 -0
  6. package/src/core/Component.js +38 -0
  7. package/src/core/DopamineKernel.js +153 -0
  8. package/src/core/EventBus.js +183 -0
  9. package/src/core/Game.js +89 -0
  10. package/src/core/GameObject.js +83 -0
  11. package/src/core/PluginRegistry.js +197 -0
  12. package/src/core/Scene.js +65 -0
  13. package/src/core/Sprite.js +51 -0
  14. package/src/core/SystemRegistry.js +242 -0
  15. package/src/core/Vector2.js +50 -0
  16. package/src/{audio → dopamine/audio}/SoundManager.js +1 -3
  17. package/src/dopamine/components/ParticleEmitter.js +50 -0
  18. package/src/dopamine/components/ScreenShake.js +49 -0
  19. package/src/dopamine/components/index.js +4 -0
  20. package/src/dopamine/effects/ParticleSystem.js +327 -0
  21. package/src/dopamine/effects/WebGLParticleSystem.js +348 -0
  22. package/src/dopamine/index.js +62 -0
  23. package/src/dopamine/utils/TextureGenerator.js +90 -0
  24. package/src/index.js +34 -62
  25. package/src/interfaces/IAudioSystem.js +58 -0
  26. package/src/interfaces/IParticleSystem.js +51 -0
  27. package/src/interfaces/IPhysicsSystem.js +52 -0
  28. package/src/interfaces/ISystem.js +48 -0
  29. package/src/interfaces/index.js +7 -0
  30. package/src/renderer/Renderer.js +37 -0
  31. package/src/systems/Director.js +27 -0
  32. package/src/systems/Input.js +77 -0
  33. package/src/systems/Loader.js +44 -0
  34. package/src/systems/Physics.js +117 -0
  35. package/src/systems/Ticker.js +61 -0
  36. package/ARCHITECTURE.md +0 -71
  37. package/CHANGELOG.md +0 -27
  38. package/README.md +0 -213
  39. package/dist/dopamine.mjs +0 -724
  40. package/dist/dopamine.umd.js +0 -45
  41. package/src/effects/ParticleSystem.js +0 -257
  42. /package/src/{core → dopamine/core}/DataService.js +0 -0
  43. /package/src/{core → dopamine/core}/EventEmitter.js +0 -0
  44. /package/src/{core → dopamine/core}/RewardSystem.js +0 -0
  45. /package/src/{ui → dopamine/ui}/GameUI.js +0 -0
  46. /package/src/{ui → dopamine/ui}/dopamine.css +0 -0
@@ -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