dopaminejs 1.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/README.md +84 -0
- package/dist/dopamine.mjs +627 -0
- package/dist/dopamine.umd.js +45 -0
- package/dist/style.css +1 -0
- package/package.json +50 -0
- package/src/audio/SoundManager.js +89 -0
- package/src/core/DataService.js +51 -0
- package/src/core/RewardSystem.js +362 -0
- package/src/effects/ParticleSystem.js +259 -0
- package/src/index.js +25 -0
- package/src/ui/GameUI.js +258 -0
- package/src/ui/dopamine.css +374 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
(function(o,c){typeof exports=="object"&&typeof module<"u"?c(exports):typeof define=="function"&&define.amd?define(["exports"],c):(o=typeof globalThis<"u"?globalThis:o||self,c(o.Dopamine={}))})(this,function(o){"use strict";class c{constructor(){this.storage=window.localStorage}async save(t,e){try{const i=JSON.stringify(e);return this.storage.setItem(t,i),!0}catch(i){return console.error("Error saving data:",i),!1}}async load(t,e=null){try{const i=this.storage.getItem(t);return i?JSON.parse(i):e}catch(i){return console.error("Error loading data:",i),e}}async clear(t){this.storage.removeItem(t)}}const d=new c;class x{constructor(){this.player=null,this.achievements=this._initAchievements(),this.listeners=[]}async init(){return this.player=await d.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 d.save("player",this.player))}async addXP(t,e=""){const i=this.player.level;this.player.xp+=t;const s=this._calculateLevel(this.player.xp),a=s>i;return a&&(this.player.level=s),await this.save(),this._emit("xp_gained",{amount:t,reason:e,leveledUp:a,newLevel:s}),a&&this._emit("level_up",{oldLevel:i,newLevel:s}),{leveledUp:a,newLevel:s,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),i=50*this.player.level*(this.player.level-1);return{total:e,needed:e-this.player.xp,progress:(this.player.xp-i)/(e-i)}}async recordGame(t,e){this.player.stats[t]||(this.player.stats[t]={totalPlays:0,highScore:0});const i=this.player.stats[t];i.totalPlays++,this.player.totalGamesPlayed++,e.score!==void 0&&e.score>i.highScore&&(i.highScore=e.score,this._emit("new_high_score",{gameName:t,score:e.score})),Object.keys(e).forEach(a=>{a!=="score"&&(i[a]=(i[a]||0)+(typeof e[a]=="number"?e[a]:0))});let s=10;e.score&&(s+=Math.floor(e.score/5)),s=Math.floor(s*this.getStreakMultiplier()),await this.addXP(s,`Played ${t}`),await this.checkAchievements(t,e),await this.save()}async checkAchievements(t,e){const i=[];for(const[s,a]of Object.entries(this.achievements))this.player.achievements[s]||a.check(this.player,t,e)&&(await this.unlockAchievement(s),i.push(a));return i}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 i=this._getYesterdayDateString();e===i?(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]}on(t,e){this.listeners.push({event:t,callback:e})}_emit(t,e){this.listeners.filter(i=>i.event===t).forEach(i=>i.callback(e))}_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}}}registerAchievements(t){this.achievements={...this.achievements,...t}}}const p=new x;class w{constructor(t="dopamine-particle-canvas"){this.canvasId=t,this.canvas=document.getElementById(t),this.canvas||(this.canvas=document.createElement("canvas"),this.canvas.id=t,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="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,i=50){const s=["#ff6b6b","#4ecdc4","#45b7d1","#f7dc6f","#c06c84","#6c5ce7"];for(let a=0;a<i;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:s[Math.floor(Math.random()*s.length)],size:Math.random()*8+4,life:1,decay:.015+Math.random()*.01,gravity:.3,type:"confetti"});this._startAnimation()}sparkle(t,e,i=20,s="#ffd700"){for(let a=0;a<i;a++){const n=Math.PI*2/i*a,l=2+Math.random()*3;this.particles.push({x:t,y:e,vx:Math.cos(n)*l,vy:Math.sin(n)*l,color:s,size:Math.random()*4+2,life:1,decay:.02,gravity:-.1,type:"spark"})}this._startAnimation()}coinShower(t,e,i=10){for(let s=0;s<i;s++)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,i=15){const s=["#ff6b00","#ff8800","#ffaa00","#ff4400"];for(let a=0;a<i;a++)this.particles.push({x:t+(Math.random()-.5)*20,y:e,vx:(Math.random()-.5)*2,vy:-Math.random()*3-2,color:s[Math.floor(Math.random()*s.length)],size:Math.random()*6+3,life:1,decay:.025,gravity:-.2,type:"fire"});this._startAnimation()}starBurst(t,e,i=8){for(let s=0;s<i;s++){const a=Math.PI*2/i*s,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,i,s,a,n){let l=Math.PI/2*3+n;const f=Math.PI/i;this.ctx.beginPath(),this.ctx.moveTo(t,e-s);for(let g=0;g<i;g++){let m=t+Math.cos(l)*s,y=e+Math.sin(l)*s;this.ctx.lineTo(m,y),l+=f,m=t+Math.cos(l)*a,y=e+Math.sin(l)*a,this.ctx.lineTo(m,y),l+=f}this.ctx.lineTo(t,e-s),this.ctx.closePath(),this.ctx.fill()}clear(){this.particles=[],this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}}const r=new w;class b{constructor(){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,i){const s=t/i*100;this.xpBar.style.width=`${Math.min(100,s)}%`,this.xpText.textContent=`${t} / ${i} 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 i=e.getBoundingClientRect();r.confetti(i.left+i.width/2,i.top+i.height/2,40)},300),setTimeout(()=>{e.style.animation="slideOut 0.3s ease-in forwards",setTimeout(()=>e.remove(),300)},4e3)}showLevelUp(t,e){const i=document.createElement("div");i.className="level-up-overlay",i.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(i),setTimeout(()=>{for(let s=0;s<5;s++)setTimeout(()=>{const a=Math.random()*window.innerWidth,n=Math.random()*window.innerHeight*.6;r.confetti(a,n,30)},s*200)},300),setTimeout(()=>{i.style.opacity="0",setTimeout(()=>i.remove(),500)},2500)}showFloatingText(t,e,i,s="#ffd700",a="24px"){const n=document.createElement("div");n.className="floating-text",n.textContent=t,n.style.left=e+"px",n.style.top=i+"px",n.style.color=s,n.style.fontSize=a,document.body.appendChild(n),setTimeout(()=>n.remove(),2e3)}showCombo(t,e,i){this.showFloatingText(`${t}× COMBO!`,e,i,"#ff6b6b","32px"),r.fire(e,i,20)}showNearMiss(t,e){this.showFloatingText("CLOSE CALL!",t,e,"#45b7d1","20px"),r.sparkle(t,e,15,"#45b7d1")}showLuckyMoment(t,e,i){this.showFloatingText(`LUCKY ${t}×!`,e,i,"#ffd700","36px"),r.starBurst(e,i,12),r.confetti(e,i,25)}showNotification(t,e="normal"){const i=window.innerWidth/2,s=window.innerHeight/3;e==="legendary"?this.showLuckyMoment(t,i,s):e==="rare"?this.showCombo(t,i,s):this.showFloatingText(t,i,s,"#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 i="";t.metrics&&(i=Object.entries(t.metrics).map(([s,a])=>`<div style="display:flex;justify-content:space-between;width:100%"><span>${s}:</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
|
+
${i}
|
|
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)}}const v=new b;class M{constructor(){this.audioContext=null,this.muted=localStorage.getItem("dopamineSoundsMuted")==="true"}initAudio(){this.audioContext||(this.audioContext=new(window.AudioContext||window.webkitAudioContext))}toggleMute(){return this.muted=!this.muted,localStorage.setItem("dopamineSoundsMuted",this.muted),this.muted}playTone(t,e,i="sine",s=.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=i,n.gain.setValueAtTime(s,this.audioContext.currentTime),n.gain.exponentialRampToValueAtTime(.01,this.audioContext.currentTime+e),a.start(this.audioContext.currentTime),a.stop(this.audioContext.currentTime+e)}playJump(){this.playTone(400,.1,"square",.2)}playScore(){this.playTone(800,.15,"sine",.25),setTimeout(()=>this.playTone(1e3,.15,"sine",.25),50)}playGameOver(){this.playTone(300,.2,"sawtooth",.3),setTimeout(()=>this.playTone(200,.2,"sawtooth",.3),100),setTimeout(()=>this.playTone(150,.3,"sawtooth",.3),200)}playClick(){this.playTone(600,.05,"sine",.1)}playSuccess(){this.playTone(600,.1,"sine",.2),setTimeout(()=>this.playTone(800,.2,"sine",.2),100)}playError(){this.playTone(200,.2,"sawtooth",.3)}}const u=new M;class k{constructor(t={}){this.config=t}async init(){return await p.init(),console.log("DopamineJS Initialized 🚀"),{rewardSystem:p,gameUI:v,particleSystem:r,soundManager:u}}}o.dataService=d,o.default=k,o.gameUI=v,o.particleSystem=r,o.rewardSystem=p,o.soundManager=u,Object.defineProperties(o,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#game-ui-overlay{position:fixed;top:0;left:0;width:100%;pointer-events:none;z-index:1000;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.game-ui-top-bar{display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0000004d;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.level-badge{display:flex;flex-direction:column;align-items:center;background:linear-gradient(135deg,#667eea,#764ba2);padding:8px 12px;border-radius:12px;box-shadow:0 4px 12px #667eea66;min-width:60px}.level-label{font-size:10px;font-weight:600;color:#fffc;letter-spacing:.5px}.level-number{font-size:24px;font-weight:800;color:#fff;line-height:1}.xp-container{flex:1;display:flex;flex-direction:column;gap:4px}.xp-bar-bg{height:12px;background:#fff3;border-radius:6px;overflow:hidden;box-shadow:inset 0 2px 4px #0003}.xp-bar-fill{height:100%;background:linear-gradient(90deg,#4facfe,#00f2fe);border-radius:6px;transition:width .5s ease-out;box-shadow:0 0 10px #4facfe99}.xp-text{font-size:11px;color:#fff;font-weight:600;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.5)}.streak-badge{display:flex;align-items:center;gap:6px;background:linear-gradient(135deg,#f80,#f40);padding:8px 12px;border-radius:12px;box-shadow:0 4px 12px #f806}.streak-icon{font-size:20px;animation:flicker 2s infinite}.streak-number{font-size:18px;font-weight:800;color:#fff}.achievement-popup{position:fixed;top:80px;right:20px;background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;padding:16px;box-shadow:0 8px 32px #0006;animation:slideIn .5s cubic-bezier(.68,-.55,.265,1.55);z-index:10000;max-width:320px;overflow:hidden}.achievement-shine{position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.3),transparent);animation:shine 2s infinite}.achievement-content{position:relative;display:flex;gap:12px;align-items:center}.achievement-icon{font-size:48px;animation:bounce .6s ease-out}.achievement-info{flex:1}.achievement-title{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.achievement-name{font-size:18px;font-weight:800;color:#fff;margin:2px 0}.achievement-desc{font-size:13px;color:#fffc;margin-bottom:4px}.achievement-xp{display:inline-block;background:#fff3;padding:4px 8px;border-radius:8px;font-size:12px;font-weight:700;color:gold}.level-up-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:#000000d9;display:flex;align-items:center;justify-content:center;z-index:10001;animation:fadeIn .3s ease-out}.level-up-content{text-align:center;animation:scaleIn .5s cubic-bezier(.68,-.55,.265,1.55)}.level-up-title{font-size:32px;font-weight:800;background:linear-gradient(135deg,gold,#ffed4e);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;text-transform:uppercase;letter-spacing:4px;margin-bottom:16px}.level-up-number{font-size:120px;font-weight:900;color:#fff;text-shadow:0 0 20px rgba(255,215,0,.8),0 0 40px rgba(255,215,0,.6),0 0 60px rgba(255,215,0,.4);animation:glow 1s ease-in-out infinite alternate}.level-up-subtitle{font-size:24px;color:#fffc;font-weight:600;margin-top:16px}.floating-text{position:fixed;font-weight:800;text-shadow:2px 2px 0 #000,-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000;pointer-events:none;z-index:9998;animation:floatUp 2s ease-out forwards}@keyframes slideIn{0%{transform:translate(400px);opacity:0}to{transform:translate(0);opacity:1}}@keyframes slideOut{0%{transform:translate(0);opacity:1}to{transform:translate(400px);opacity:0}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes scaleIn{0%{transform:scale(.5);opacity:0}to{transform:scale(1);opacity:1}}@keyframes bounce{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes glow{0%{text-shadow:0 0 20px rgba(255,215,0,.8),0 0 40px rgba(255,215,0,.6),0 0 60px rgba(255,215,0,.4)}to{text-shadow:0 0 30px rgba(255,215,0,1),0 0 60px rgba(255,215,0,.8),0 0 90px rgba(255,215,0,.6)}}@keyframes shine{0%{left:-100%}30%{left:100%}to{left:100%}}@keyframes flicker{0%,to{opacity:1}50%{opacity:.8}}@keyframes floatUp{0%{transform:translateY(0);opacity:1}to{transform:translateY(-100px);opacity:0}}@keyframes xp-pulse{0%{transform:scaleY(1)}50%{transform:scaleY(1.2)}to{transform:scaleY(1)}}@keyframes level-pulse{0%{transform:scale(1)}50%{transform:scale(1.1)}to{transform:scale(1)}}@media (max-width: 480px){.game-ui-top-bar{gap:8px;padding:10px 12px}.level-badge{padding:6px 10px;min-width:50px}.level-number{font-size:20px}.xp-bar-bg{height:10px}.xp-text{font-size:10px}.achievement-popup{right:12px;max-width:calc(100% - 24px)}.level-up-number{font-size:80px}}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dopaminejs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight library for adding juice, rewards, and feedback to HTML5 games.",
|
|
5
|
+
"main": "dist/dopamine.umd.js",
|
|
6
|
+
"module": "dist/dopamine.mjs",
|
|
7
|
+
"style": "dist/style.css",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./dist/dopamine.mjs",
|
|
17
|
+
"require": "./dist/dopamine.umd.js",
|
|
18
|
+
"default": "./dist/dopamine.umd.js"
|
|
19
|
+
},
|
|
20
|
+
"./style.css": "./dist/style.css"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "vite",
|
|
24
|
+
"build": "vite build",
|
|
25
|
+
"preview": "vite preview",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"game-dev",
|
|
30
|
+
"html5",
|
|
31
|
+
"rewards",
|
|
32
|
+
"particles",
|
|
33
|
+
"feedback",
|
|
34
|
+
"juice",
|
|
35
|
+
"gamification"
|
|
36
|
+
],
|
|
37
|
+
"author": "Gemini",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"vite": "^5.0.0"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/BaryoDev/SimpleMobileGames.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/BaryoDev/SimpleMobileGames/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/BaryoDev/SimpleMobileGames#readme"
|
|
50
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sound Manager Module
|
|
3
|
+
* Handles synthesized sound effects using Web Audio API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class SoundManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.audioContext = null;
|
|
9
|
+
this.muted = localStorage.getItem('dopamineSoundsMuted') === 'true';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initialize Audio Context (must be called after user interaction)
|
|
14
|
+
*/
|
|
15
|
+
initAudio() {
|
|
16
|
+
if (!this.audioContext) {
|
|
17
|
+
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Toggle mute state
|
|
23
|
+
* @returns {boolean} New mute state
|
|
24
|
+
*/
|
|
25
|
+
toggleMute() {
|
|
26
|
+
this.muted = !this.muted;
|
|
27
|
+
localStorage.setItem('dopamineSoundsMuted', this.muted);
|
|
28
|
+
return this.muted;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Play a synthesized tone
|
|
33
|
+
* @param {number} frequency
|
|
34
|
+
* @param {number} duration
|
|
35
|
+
* @param {string} type - 'sine', 'square', 'sawtooth', 'triangle'
|
|
36
|
+
* @param {number} volume
|
|
37
|
+
*/
|
|
38
|
+
playTone(frequency, duration, type = 'sine', volume = 0.3) {
|
|
39
|
+
if (this.muted) return;
|
|
40
|
+
this.initAudio();
|
|
41
|
+
|
|
42
|
+
const oscillator = this.audioContext.createOscillator();
|
|
43
|
+
const gainNode = this.audioContext.createGain();
|
|
44
|
+
|
|
45
|
+
oscillator.connect(gainNode);
|
|
46
|
+
gainNode.connect(this.audioContext.destination);
|
|
47
|
+
|
|
48
|
+
oscillator.frequency.value = frequency;
|
|
49
|
+
oscillator.type = type;
|
|
50
|
+
|
|
51
|
+
gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime);
|
|
52
|
+
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
|
|
53
|
+
|
|
54
|
+
oscillator.start(this.audioContext.currentTime);
|
|
55
|
+
oscillator.stop(this.audioContext.currentTime + duration);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Presets ---
|
|
59
|
+
|
|
60
|
+
playJump() {
|
|
61
|
+
this.playTone(400, 0.1, 'square', 0.2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
playScore() {
|
|
65
|
+
this.playTone(800, 0.15, 'sine', 0.25);
|
|
66
|
+
setTimeout(() => this.playTone(1000, 0.15, 'sine', 0.25), 50);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
playGameOver() {
|
|
70
|
+
this.playTone(300, 0.2, 'sawtooth', 0.3);
|
|
71
|
+
setTimeout(() => this.playTone(200, 0.2, 'sawtooth', 0.3), 100);
|
|
72
|
+
setTimeout(() => this.playTone(150, 0.3, 'sawtooth', 0.3), 200);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
playClick() {
|
|
76
|
+
this.playTone(600, 0.05, 'sine', 0.1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
playSuccess() {
|
|
80
|
+
this.playTone(600, 0.1, 'sine', 0.2);
|
|
81
|
+
setTimeout(() => this.playTone(800, 0.2, 'sine', 0.2), 100);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
playError() {
|
|
85
|
+
this.playTone(200, 0.2, 'sawtooth', 0.3);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const soundManager = new SoundManager();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Service Module
|
|
3
|
+
* Handles local storage persistence
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class DataService {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.storage = window.localStorage;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Save data to local storage
|
|
13
|
+
* @param {string} key
|
|
14
|
+
* @param {any} data
|
|
15
|
+
*/
|
|
16
|
+
async save(key, data) {
|
|
17
|
+
try {
|
|
18
|
+
const serialized = JSON.stringify(data);
|
|
19
|
+
this.storage.setItem(key, serialized);
|
|
20
|
+
return true;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error('Error saving data:', e);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load data from local storage
|
|
29
|
+
* @param {string} key
|
|
30
|
+
* @param {any} defaultValue
|
|
31
|
+
*/
|
|
32
|
+
async load(key, defaultValue = null) {
|
|
33
|
+
try {
|
|
34
|
+
const data = this.storage.getItem(key);
|
|
35
|
+
return data ? JSON.parse(data) : defaultValue;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error('Error loading data:', e);
|
|
38
|
+
return defaultValue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clear specific data
|
|
44
|
+
* @param {string} key
|
|
45
|
+
*/
|
|
46
|
+
async clear(key) {
|
|
47
|
+
this.storage.removeItem(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const dataService = new DataService();
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reward System Module
|
|
3
|
+
* Manages achievements, XP, levels, streaks, and rewards
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { dataService } from './DataService.js';
|
|
7
|
+
|
|
8
|
+
export class RewardSystem {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.player = null;
|
|
11
|
+
this.achievements = this._initAchievements();
|
|
12
|
+
this.listeners = [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the reward system for current player
|
|
17
|
+
* @returns {Promise<Object>} Player data
|
|
18
|
+
*/
|
|
19
|
+
async init() {
|
|
20
|
+
this.player = await dataService.load('player', this._getDefaultPlayer());
|
|
21
|
+
|
|
22
|
+
// Check daily streak
|
|
23
|
+
this._updateDailyStreak();
|
|
24
|
+
|
|
25
|
+
// Save updated player data
|
|
26
|
+
await this.save();
|
|
27
|
+
|
|
28
|
+
return this.player;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get default player data structure
|
|
33
|
+
*/
|
|
34
|
+
_getDefaultPlayer() {
|
|
35
|
+
return {
|
|
36
|
+
name: 'Player',
|
|
37
|
+
xp: 0,
|
|
38
|
+
level: 1,
|
|
39
|
+
totalGamesPlayed: 0,
|
|
40
|
+
createdAt: Date.now(),
|
|
41
|
+
lastPlayedAt: Date.now(),
|
|
42
|
+
streak: {
|
|
43
|
+
current: 1,
|
|
44
|
+
longest: 1,
|
|
45
|
+
lastPlayDate: this._getTodayDateString()
|
|
46
|
+
},
|
|
47
|
+
achievements: {}, // achievement_id: { unlockedAt: timestamp, seen: boolean }
|
|
48
|
+
stats: {
|
|
49
|
+
// Dynamic stats object, games will add their own keys
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Save player data
|
|
56
|
+
*/
|
|
57
|
+
async save() {
|
|
58
|
+
if (this.player) {
|
|
59
|
+
this.player.lastPlayedAt = Date.now();
|
|
60
|
+
await dataService.save('player', this.player);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Add XP and handle level ups
|
|
66
|
+
* @param {number} amount - XP to add
|
|
67
|
+
* @param {string} reason - Why XP was awarded (for notifications)
|
|
68
|
+
* @returns {Object} { leveledUp: boolean, newLevel: number, xpGained: number }
|
|
69
|
+
*/
|
|
70
|
+
async addXP(amount, reason = '') {
|
|
71
|
+
const oldLevel = this.player.level;
|
|
72
|
+
this.player.xp += amount;
|
|
73
|
+
|
|
74
|
+
// Check for level up
|
|
75
|
+
const newLevel = this._calculateLevel(this.player.xp);
|
|
76
|
+
const leveledUp = newLevel > oldLevel;
|
|
77
|
+
|
|
78
|
+
if (leveledUp) {
|
|
79
|
+
this.player.level = newLevel;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await this.save();
|
|
83
|
+
|
|
84
|
+
// Notify listeners
|
|
85
|
+
this._emit('xp_gained', { amount, reason, leveledUp, newLevel });
|
|
86
|
+
|
|
87
|
+
if (leveledUp) {
|
|
88
|
+
this._emit('level_up', { oldLevel, newLevel });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { leveledUp, newLevel, xpGained: amount };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Calculate level from XP (exponential curve)
|
|
96
|
+
*/
|
|
97
|
+
_calculateLevel(xp) {
|
|
98
|
+
// Formula: XP = 50 * level * (level - 1)
|
|
99
|
+
// Solving for level: level = (1 + sqrt(1 + 8*XP/50)) / 2
|
|
100
|
+
const level = Math.floor((1 + Math.sqrt(1 + 8 * xp / 50)) / 2);
|
|
101
|
+
return Math.max(1, level);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get XP needed for next level
|
|
106
|
+
*/
|
|
107
|
+
getXPForNextLevel() {
|
|
108
|
+
const nextLevel = this.player.level + 1;
|
|
109
|
+
const xpNeeded = 50 * nextLevel * (nextLevel - 1);
|
|
110
|
+
const currentLevelXP = 50 * this.player.level * (this.player.level - 1);
|
|
111
|
+
return {
|
|
112
|
+
total: xpNeeded,
|
|
113
|
+
needed: xpNeeded - this.player.xp,
|
|
114
|
+
progress: (this.player.xp - currentLevelXP) / (xpNeeded - currentLevelXP)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Record game result and award XP
|
|
120
|
+
* @param {string} gameName - Unique ID for the game
|
|
121
|
+
* @param {Object} result - Game-specific result data
|
|
122
|
+
*/
|
|
123
|
+
async recordGame(gameName, result) {
|
|
124
|
+
// Initialize stats for this game if not exists
|
|
125
|
+
if (!this.player.stats[gameName]) {
|
|
126
|
+
this.player.stats[gameName] = { totalPlays: 0, highScore: 0 };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const stats = this.player.stats[gameName];
|
|
130
|
+
|
|
131
|
+
// Update stats
|
|
132
|
+
stats.totalPlays++;
|
|
133
|
+
this.player.totalGamesPlayed++;
|
|
134
|
+
|
|
135
|
+
// Update high score if applicable
|
|
136
|
+
if (result.score !== undefined && result.score > stats.highScore) {
|
|
137
|
+
stats.highScore = result.score;
|
|
138
|
+
this._emit('new_high_score', { gameName, score: result.score });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Merge other result data into stats
|
|
142
|
+
Object.keys(result).forEach(key => {
|
|
143
|
+
if (key !== 'score') {
|
|
144
|
+
stats[key] = (stats[key] || 0) + (typeof result[key] === 'number' ? result[key] : 0);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Base XP for playing
|
|
149
|
+
let xp = 10;
|
|
150
|
+
|
|
151
|
+
// Score-based bonus
|
|
152
|
+
if (result.score) {
|
|
153
|
+
xp += Math.floor(result.score / 5);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Streak multiplier
|
|
157
|
+
xp = Math.floor(xp * this.getStreakMultiplier());
|
|
158
|
+
|
|
159
|
+
await this.addXP(xp, `Played ${gameName}`);
|
|
160
|
+
|
|
161
|
+
// Check for achievements
|
|
162
|
+
await this.checkAchievements(gameName, result);
|
|
163
|
+
|
|
164
|
+
await this.save();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check and unlock achievements
|
|
169
|
+
*/
|
|
170
|
+
async checkAchievements(gameName, result) {
|
|
171
|
+
const unlockedAchievements = [];
|
|
172
|
+
|
|
173
|
+
for (const [id, achievement] of Object.entries(this.achievements)) {
|
|
174
|
+
// Skip if already unlocked
|
|
175
|
+
if (this.player.achievements[id]) continue;
|
|
176
|
+
|
|
177
|
+
// Check if achievement condition is met
|
|
178
|
+
if (achievement.check(this.player, gameName, result)) {
|
|
179
|
+
await this.unlockAchievement(id);
|
|
180
|
+
unlockedAchievements.push(achievement);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return unlockedAchievements;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Unlock an achievement
|
|
189
|
+
*/
|
|
190
|
+
async unlockAchievement(achievementId) {
|
|
191
|
+
const achievement = this.achievements[achievementId];
|
|
192
|
+
if (!achievement) return false;
|
|
193
|
+
|
|
194
|
+
// Mark as unlocked
|
|
195
|
+
this.player.achievements[achievementId] = {
|
|
196
|
+
unlockedAt: Date.now(),
|
|
197
|
+
seen: false
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Award XP
|
|
201
|
+
await this.addXP(achievement.xp, `Achievement: ${achievement.name}`);
|
|
202
|
+
|
|
203
|
+
this._emit('achievement_unlocked', achievement);
|
|
204
|
+
await this.save();
|
|
205
|
+
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get all unlocked achievements
|
|
211
|
+
*/
|
|
212
|
+
getUnlockedAchievements() {
|
|
213
|
+
return Object.entries(this.player.achievements)
|
|
214
|
+
.map(([id, data]) => ({
|
|
215
|
+
...this.achievements[id],
|
|
216
|
+
id,
|
|
217
|
+
...data
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get achievements user hasn't seen the notification for yet
|
|
223
|
+
*/
|
|
224
|
+
getUnseenAchievements() {
|
|
225
|
+
return this.getUnlockedAchievements().filter(a => !a.seen);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Mark achievements as seen
|
|
230
|
+
*/
|
|
231
|
+
async markAchievementsSeen(achievementIds) {
|
|
232
|
+
for (const id of achievementIds) {
|
|
233
|
+
if (this.player.achievements[id]) {
|
|
234
|
+
this.player.achievements[id].seen = true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
await this.save();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Update daily streak
|
|
242
|
+
*/
|
|
243
|
+
_updateDailyStreak() {
|
|
244
|
+
const today = this._getTodayDateString();
|
|
245
|
+
const lastPlayed = this.player.streak.lastPlayDate;
|
|
246
|
+
|
|
247
|
+
if (lastPlayed === today) {
|
|
248
|
+
// Already played today
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const yesterday = this._getYesterdayDateString();
|
|
253
|
+
|
|
254
|
+
if (lastPlayed === yesterday) {
|
|
255
|
+
// Streak continues!
|
|
256
|
+
this.player.streak.current++;
|
|
257
|
+
if (this.player.streak.current > this.player.streak.longest) {
|
|
258
|
+
this.player.streak.longest = this.player.streak.current;
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
// Streak broken
|
|
262
|
+
this.player.streak.current = 1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.player.streak.lastPlayDate = today;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get streak multiplier for XP
|
|
270
|
+
*/
|
|
271
|
+
getStreakMultiplier() {
|
|
272
|
+
const streak = this.player.streak.current;
|
|
273
|
+
if (streak >= 7) return 2.0; // 2× at 7 days
|
|
274
|
+
if (streak >= 5) return 1.5; // 1.5× at 5 days
|
|
275
|
+
if (streak >= 3) return 1.25; // 1.25× at 3 days
|
|
276
|
+
return 1.0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get today's date as YYYY-MM-DD string
|
|
281
|
+
*/
|
|
282
|
+
_getTodayDateString() {
|
|
283
|
+
return new Date().toISOString().split('T')[0];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get yesterday's date as YYYY-MM-DD string
|
|
288
|
+
*/
|
|
289
|
+
_getYesterdayDateString() {
|
|
290
|
+
const yesterday = new Date();
|
|
291
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
292
|
+
return yesterday.toISOString().split('T')[0];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Event system for UI updates
|
|
297
|
+
*/
|
|
298
|
+
on(event, callback) {
|
|
299
|
+
this.listeners.push({ event, callback });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
_emit(event, data) {
|
|
303
|
+
this.listeners
|
|
304
|
+
.filter(l => l.event === event)
|
|
305
|
+
.forEach(l => l.callback(data));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Initialize achievement definitions
|
|
310
|
+
* Can be extended by games
|
|
311
|
+
*/
|
|
312
|
+
_initAchievements() {
|
|
313
|
+
return {
|
|
314
|
+
// Universal achievements
|
|
315
|
+
'first_game': {
|
|
316
|
+
name: 'First Steps',
|
|
317
|
+
description: 'Play your first game',
|
|
318
|
+
icon: '🎮',
|
|
319
|
+
xp: 50,
|
|
320
|
+
check: (player) => player.totalGamesPlayed >= 1
|
|
321
|
+
},
|
|
322
|
+
'level_5': {
|
|
323
|
+
name: 'Rising Star',
|
|
324
|
+
description: 'Reach level 5',
|
|
325
|
+
icon: '⭐',
|
|
326
|
+
xp: 100,
|
|
327
|
+
check: (player) => player.level >= 5
|
|
328
|
+
},
|
|
329
|
+
'level_10': {
|
|
330
|
+
name: 'Expert Player',
|
|
331
|
+
description: 'Reach level 10',
|
|
332
|
+
icon: '🏆',
|
|
333
|
+
xp: 250,
|
|
334
|
+
check: (player) => player.level >= 10
|
|
335
|
+
},
|
|
336
|
+
'streak_3': {
|
|
337
|
+
name: 'Dedicated',
|
|
338
|
+
description: 'Play 3 days in a row',
|
|
339
|
+
icon: '🔥',
|
|
340
|
+
xp: 100,
|
|
341
|
+
check: (player) => player.streak.current >= 3
|
|
342
|
+
},
|
|
343
|
+
'streak_7': {
|
|
344
|
+
name: 'Unstoppable',
|
|
345
|
+
description: 'Play 7 days in a row',
|
|
346
|
+
icon: '💥',
|
|
347
|
+
xp: 300,
|
|
348
|
+
check: (player) => player.streak.current >= 7
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Register custom achievements
|
|
355
|
+
* @param {Object} achievements
|
|
356
|
+
*/
|
|
357
|
+
registerAchievements(achievements) {
|
|
358
|
+
this.achievements = { ...this.achievements, ...achievements };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export const rewardSystem = new RewardSystem();
|