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.
@@ -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();