diff --git a/index.html b/index.html index 5badfa5..84e7079 100644 --- a/index.html +++ b/index.html @@ -99,7 +99,11 @@ let OPTION_PREFS=loadOptionPrefs(); // BUG FIX 1: Removed duplicate VERSION constant ('0.9.1' shadowed '0.9.2') -const VERSION = 'pre-1.0.0 (patch_03012026)'; +const VERSION = '1.0.0'; +const SUPPORTED_VERSIONS = ['1.0.0','pre-1.0.0 (patch_03052026)']; +const SEASON_LIVE='SEASON 1'; +const SEASON_NEXT='SEASON 2'; +const SEASON_NEXT_RELEASE='Sunday, April 5, 2026'; // Adventure stage configs const ADV_STAGES = [ @@ -139,6 +143,7 @@ ], forcedPowerups:['X2_SCORE','JETPACK','SHIELD','MAGNET','REGEN','HEALTH_BOOST','IMMUNITY','X2_GEMS','GEM_DROP'], stagePowerupStage:2, volcanoFX:true}, {id:'frostbite', specialName:'ICE FIELD', specialCol:'#7af0ff', name:'FROSTBITE', desc:'Frozen lanes and glide control', cardCol:'#7af0ff', stageDuration:35, stages:[{name:'STAGE 1',label:'CHILL',spd:150,spdMax:320,shrink:0.003,obsInt:[4.8,6.8]},{name:'STAGE 2',label:'COLD',spd:220,spdMax:430,shrink:0.005,obsInt:[3.4,5.2]},{name:'STAGE 3',label:'BLIZZARD',spd:320,spdMax:560,shrink:0.007,obsInt:[2.4,4.0]}], forcedPowerups:['X2_SCORE','SHIELD','MAGNET','REGEN','GEM_DROP']}, {id:'metro', specialName:'RAIL RUSH', specialCol:'#ff4dd2', name:'NEON METRO', desc:'City rails and fast pickups', cardCol:'#ff4dd2', stageDuration:34, stages:[{name:'STAGE 1',label:'RUSH',spd:180,spdMax:340,shrink:0.004,obsInt:[4.4,6.2]},{name:'STAGE 2',label:'HYPER',spd:260,spdMax:460,shrink:0.0064,obsInt:[3.1,4.8]},{name:'STAGE 3',label:'BLAZE',spd:360,spdMax:620,shrink:0.008,obsInt:[2.0,3.5]}], forcedPowerups:['X2_SCORE','MAGNET','REGEN','X2_GEMS','GEM_DROP']}, + {id:'slope_instant', specialName:'SLOPE INSTANT!', specialCol:'#ffb347', name:'SLOPE INSTANT!', desc:'3 stages • 20s each • rapid pace', cardCol:'#ffb347', stageDuration:20, stages:[{name:'STAGE 1',label:'RAPID',spd:240,spdMax:420,shrink:0.006,obsInt:[3.1,4.8]},{name:'STAGE 2',label:'BURST',spd:340,spdMax:560,shrink:0.008,obsInt:[2.2,3.6]},{name:'STAGE 3',label:'INSTANT!',spd:460,spdMax:760,shrink:0.011,obsInt:[1.4,2.6]}], forcedPowerups:['MINI_JETPACK','JETPACK','SHIELD','IMMUNITY','X2_GEMS']}, {id:'storm', specialName:'THUNDER CORE', specialCol:'#d3c4ff', name:'THUNDER RUN', desc:'Electric storms and shake', cardCol:'#d3c4ff', stageDuration:38, stages:[{name:'STAGE 1',label:'STATIC',spd:170,spdMax:320,shrink:0.0035,obsInt:[4.8,6.8]},{name:'STAGE 2',label:'SPARK',spd:240,spdMax:450,shrink:0.006,obsInt:[3.0,4.8]},{name:'STAGE 3',label:'THUNDER',spd:330,spdMax:610,shrink:0.0078,obsInt:[2.1,3.3]}], forcedPowerups:['X2_SCORE','SHIELD','IMMUNITY','REGEN','GEM_DROP']}, {id:'hacker', specialName:'GLITCH BREAK', specialCol:'#45ffcc', name:'GLITCH GRID', desc:'Digital chaos and pulse', cardCol:'#45ffcc', stageDuration:32, stages:[{name:'STAGE 1',label:'PATCHED',spd:160,spdMax:330,shrink:0.0036,obsInt:[4.7,6.5]},{name:'STAGE 2',label:'BREACH',spd:250,spdMax:470,shrink:0.0063,obsInt:[3.0,4.7]},{name:'STAGE 3',label:'MELTDOWN',spd:350,spdMax:620,shrink:0.0081,obsInt:[2.0,3.2]}], forcedPowerups:['X2_SCORE','MAGNET','X4_GEMS','REGEN','GEM_DROP']}, {id:'void', specialName:'VOID SHELL', specialCol:'#a98bff', name:'VOID TUNNEL', desc:'Ultra dark precision route', cardCol:'#a98bff', stageDuration:42, stages:[{name:'STAGE 1',label:'DUSK',spd:170,spdMax:330,shrink:0.0041,obsInt:[4.8,6.4]},{name:'STAGE 2',label:'NIGHT',spd:260,spdMax:470,shrink:0.0066,obsInt:[2.8,4.6]},{name:'STAGE 3',label:'VOID',spd:360,spdMax:640,shrink:0.0088,obsInt:[1.8,3.0]}], forcedPowerups:['X2_SCORE','SHIELD','MAGNET','REGEN','IMMUNITY']}, @@ -195,11 +200,170 @@ let joyL=false, joyR=false; let usingTouchJoy=false; const isTouchDevice=('ontouchstart' in window)||(navigator.maxTouchPoints>0); -const GP={left:false,right:false,pauseEdge:false,lastPause:false}; +const GP={left:false,right:false,up:false,down:false,confirmEdge:false,pauseEdge:false,lastPause:false,lastConfirm:false}; var gs='LOADING'; var profileUploadState={active:false,mode:'idle',error:'',rawData:'',img:null,zoom:1,offX:0,offY:0,drag:false,lastX:0,lastY:0}; var fps=0, fpsFrames=0, fpsTimer=0; let mouseCanvasPos={x:-9999,y:-9999}; +let uiNavButtons=[]; +let uiNavFocusIdx=-1; +let uiNavScope='base'; +let uiNavLastMoveAt=0; +let uiNavLastInputAt=0; +let uiNavEnabled=false; +const UI_NAV_MOVE_COOLDOWN=140; + +function resetUiNavFrame(){ uiNavButtons.length=0; } +function setUiNavScope(scope='base'){ uiNavScope=scope||'base'; } +function activeUiNavScope(){ + if (guestProfilePrompt.active) return 'guestPrompt'; + if (inputDialog.active) return 'inputDialog'; + if (confirmDialog.active) return 'confirmDialog'; + if (showOptimizeConfirm) return 'optimizeConfirm'; + if (actionPopup.active) return 'actionPopup'; + if (usernameChangeFlow.active) return 'usernameChange'; + if (playerCardPopup.active) return 'playerCard'; + return ''; +} +function uiNavCandidates(){ + const lock=activeUiNavScope(); + const out=[]; + for (let i=0;i0&&h>0)) return; + uiNavButtons.push({x,y,w,h,cx:x+w/2,cy:y+h/2,scope:uiNavScope||'base'}); +} +function hasUiNavOverlay(){ + return !!(actionPopup.active || confirmDialog.active || inputDialog.active || showOptimizeConfirm || guestProfilePrompt.active || playerCardPopup.active || usernameChangeFlow.active); +} +function shouldUseUiNav(){ + return gs!=='LOADING' && gs!=='PLAY' && gs!=='IMPOSSIBLE_INTRO'; +} +function nudgeUiScroll(dir){ + const amt=30*dir; + if (gs==='QUESTS') { questScroll=clamp(questScroll+amt,minQuestScroll(),0); return true; } + if (gs==='LEADERBOARD') { leaderboardScroll=clamp(leaderboardScroll+amt,minLeaderboardScroll(),0); return true; } + if (gs==='PROFILE') { profileScroll=clamp(profileScroll+amt,minProfileScroll(),0); return true; } + if (gs==='SHOP') { shopScroll=clamp(shopScroll+amt,minShopScroll(),0); return true; } + if (gs==='SETTINGS') { settingsScroll=clamp(settingsScroll+amt,minSettingsScroll(),0); return true; } + if (gs==='OPTIONS') { optionsScroll=clamp(optionsScroll+amt,minOptionsScroll(),0); return true; } + if (gs==='ADMIN_MENU') { adminScroll=clamp(adminScroll+amt,minAdminScroll(),0); return true; } + if (gs==='ADMIN_LTM') { adminLtmScroll=clamp(adminLtmScroll+amt,minAdminLtmScroll(),0); return true; } + if (gs==='FRIEND_REQUESTS' || gs==='FRIENDS_LIST') { socialView.scroll=clamp(socialView.scroll+amt,minSocialScroll(),0); return true; } + if (gs==='NOTIFICATIONS') return true; + return false; +} +function setUiNavEnabled(v){ + uiNavEnabled=!!v; + if (uiNavEnabled) uiNavLastInputAt=Date.now(); +} +function disableUiNavFromPointer(){ setUiNavEnabled(false); } + +function ensureUiFocus(){ + const cands=uiNavCandidates(); + if (!cands.length) { uiNavFocusIdx=-1; return; } + if (cands.some(c=>c.idx===uiNavFocusIdx)) return; + let best=cands[0].idx,bestScore=1e9; + for (const c of cands) { + const b=c.btn; + const sc=b.cy*1000+b.cx; + if (scc.idx===uiNavFocusIdx)||cands[0]).btn; + let best=-1,bestScore=1e9; + for (const c of cands) { + if (c.idx===uiNavFocusIdx) continue; + const b=c.btn; + const vx=b.cx-cur.cx, vy=b.cy-cur.cy; + const primary=(dx!==0)?vx*dx:vy*dy; + if (primary<=3) continue; + const secondary=(dx!==0)?Math.abs(vy):Math.abs(vx); + const score=primary*1 + secondary*2; + if (score=0) { uiNavFocusIdx=best; return true; } + return false; +} +function activateUiFocus(){ + ensureUiFocus(); + const cands=uiNavCandidates(); + const b=(cands.find(c=>c.idx===uiNavFocusIdx)||{}).btn; + if (!b) return; + const rect=canvas.getBoundingClientRect(); + const clientX=rect.left + (b.cx/LW)*rect.width; + const clientY=rect.top + (b.cy/LH)*rect.height; + handleTap({clientX,clientY}); +} +function drawUiFocusHighlight(t){ + if (!uiNavEnabled || !shouldUseUiNav() || !uiNavButtons.length) return; + ensureUiFocus(); + const cands=uiNavCandidates(); + const b=(cands.find(c=>c.idx===uiNavFocusIdx)||{}).btn; + if (!b) return; + const pad=4; + ctx.save(); + const col=CFG.YL; + const a=0.65+0.35*Math.sin(t*8); + ctx.globalAlpha=a; + ctx.strokeStyle=col; ctx.lineWidth=2.5; glow(col,12); + ctx.strokeRect(b.x-pad,b.y-pad,b.w+pad*2,b.h+pad*2); + noGlow(); setFont(7,'900',"'Share Tech Mono',monospace"); + txt('SELECT',b.x+b.w+pad+4,b.y-2,col,'left'); + ctx.restore(); +} +function handleUiNavKey(e){ + if (!shouldUseUiNav()) return false; + const k=e.key.toLowerCase(); + let moved=false; + if (k==='arrowleft' || k==='a') { setUiNavEnabled(true); moved=moveUiFocus(-1,0); } + else if (k==='arrowright' || k==='d') { setUiNavEnabled(true); moved=moveUiFocus(1,0); } + else if (k==='arrowup' || k==='w') { setUiNavEnabled(true); moved=moveUiFocus(0,-1); } + else if (k==='arrowdown' || k==='s') { setUiNavEnabled(true); moved=moveUiFocus(0,1); } + else if (e.key==='Enter' || e.key===' ') { + setUiNavEnabled(true); + activateUiFocus(); + uiNavLastInputAt=Date.now(); + e.preventDefault(); + return true; + } + if (moved) { + uiNavLastInputAt=Date.now(); + e.preventDefault(); + return true; + } + if ((k==='arrowup' || k==='w') && nudgeUiScroll(1)) { uiNavLastInputAt=Date.now(); e.preventDefault(); return true; } + if ((k==='arrowdown' || k==='s') && nudgeUiScroll(-1)) { uiNavLastInputAt=Date.now(); e.preventDefault(); return true; } + return false; +} +function updateUiNavFromGamepad(){ + if (!shouldUseUiNav()) return; + const now=performance.now(); + if ((GP.left||GP.right||GP.up||GP.down) && now-uiNavLastMoveAt>UI_NAV_MOVE_COOLDOWN) { + setUiNavEnabled(true); + let moved=false; + if (GP.left) moved=moveUiFocus(-1,0) || moved; + if (GP.right) moved=moveUiFocus(1,0) || moved; + if (GP.up) moved=moveUiFocus(0,-1) || moved; + if (GP.down) moved=moveUiFocus(0,1) || moved; + if (!moved) { + if (GP.up) moved=nudgeUiScroll(1) || moved; + if (GP.down) moved=nudgeUiScroll(-1) || moved; + } + if (moved) { uiNavLastMoveAt=now; uiNavLastInputAt=Date.now(); } + } + if (GP.confirmEdge) { setUiNavEnabled(true); activateUiFocus(); uiNavLastInputAt=Date.now(); } +} function getCanvasXY(e) { const rect = canvas.getBoundingClientRect(); @@ -213,7 +377,7 @@ const BTNS = { start:null, retry:null, home:null, settings:null, back:null, - pause:null, resume:null, menuFromPause:null, + pause:null, resume:null, menuFromPause:null, pauseSkins:null, pauseQuests:null, shopSkip:null, shopBack:null, charBack:null, charPrev:null, charNext:null, charShop:null, modeEndless:null, modeAdventure:null, modeImpossible:null, modeLtm:null, @@ -231,6 +395,7 @@ pauseOptions:null, optionsBack:null, optionsToggleQuests:null, optionsToggleTutorials:null, hudOptions:null, signinEmail:null, signinPass:null, signupName:null, signupEmail:null, signupPass:null, signupConfirm:null, signupSwitch:null, signinSwitch:null, signinGoogle:null, signupGoogle:null, profileChangeUsername:null, profileChangePassword:null, profileAvatar:null, profileTrash:null, profileUploadOk:null, profileUploadCancel:null, profileUploadZoomIn:null, profileUploadZoomOut:null, profileUploadReset:null, profileUploadErrOk:null, profileInfoBox:null, profileUidToggle:null, profileUidCopy:null, profileStatusDefault:null, profileStatusIdle:null, profileStatusDnd:null, profileStatusOffline:null, usernameTryAgain:null, usernameExit:null, usernameContinue:null, playerCardClose:null, playerCardEdit:null, playerCardFav:null, menuProfileBox:null, guestPromptSignIn:null, guestPromptContinue:null, publicProfileBack:null, publicProfileRequests:null, publicProfileFriends:null, playerCardFind:null, playerCardAdd:null, playerCardFollow:null, playerCardUnfriend:null, playerCardFriends:null, playerCardFollowers:null, playerCardFollowing:null, socialTabFriends:null, socialTabFollowers:null, socialTabFollowing:null, socialActionA:null, socialActionB:null, profileDelete:null, + menuAlerts:null, pauseAlerts:null, pauseProfileBox:null, notificationsBack:null, menuLevelBadge:null, pauseLevelBadge:null, profileLevelBadge:null, levelBack:null, levelClaim:null, levelXpOpen:null, charShopTabButtons:[], bundlePopupBuy:null, bundlePopupEquip:null, bundlePopupClose:null, rightSkinCategory:null, adminBack:null, adminAuthInput:null, adminAuthSubmit:null, adminSetGems:null, adminGrantSelected:null, adminUnlockAll:null, adminLockAll:null, adminLockSelectedSkin:null, adminLockSelectedAura:null, adminLockSelectedTheme:null, adminVerifyUid:null, adminVerifyToggle:null, adminVerifyApply:null, adminViewLtm:null, adminLtmBack:null, controls:null, controlsBack:null, touchJoyL:null, touchJoyR:null, noAds:null, noAdsBuy:null, noAdsContinue:null, overLeaderboard:null, }; @@ -279,7 +444,7 @@ let socialCountsCache={}; let uidVisible=false; -let playerCardPopup={active:false,isSelf:false,favorite:false,fromMenu:false,viewed:null}; +let playerCardPopup={active:false,isSelf:false,favorite:false,fromMenu:false,viewed:null,lite:false}; let guestProfilePrompt={active:false}; const PRIVACY_LEVELS=['FRIENDS OF FRIENDS','FRIENDS ONLY','PRIVATE']; const PUBLIC_PROFILE_KEY='slope_public_profile'; @@ -495,13 +660,17 @@ const cardUser=playerCardPopup.viewed||{}; const rel=getCardRelation(cardUser.uid); if (playerCardPopup.isSelf) { - BTNS.playerCardEdit={x:x+10,y:y+10,w:56,h:24}; - BTNS.playerCardFav={x:x+72,y:y+10,w:28,h:24}; - BTNS.playerCardFind={x:x+106,y:y+10,w:56,h:24}; BTNS.playerCardAdd=null; BTNS.playerCardFollow=null; BTNS.playerCardUnfriend=null; BTNS.playerCardMenuUnfriend=null; - drawNeonBtn(BTNS.playerCardEdit.x,BTNS.playerCardEdit.y,56,24,CFG.CY,'EDIT',8,1,0); - drawNeonBtn(BTNS.playerCardFav.x,BTNS.playerCardFav.y,28,24,playerCardPopup.favorite?'#ff9a1a':CFG.YL,playerCardPopup.favorite?'★':'☆',10,1,0); - drawNeonBtn(BTNS.playerCardFind.x,BTNS.playerCardFind.y,56,24,CFG.GR,'FIND',8,1,0); + if (playerCardPopup.lite) { + BTNS.playerCardEdit=null; BTNS.playerCardFav=null; BTNS.playerCardFind=null; + } else { + BTNS.playerCardEdit={x:x+10,y:y+10,w:56,h:24}; + BTNS.playerCardFav={x:x+72,y:y+10,w:28,h:24}; + BTNS.playerCardFind={x:x+106,y:y+10,w:56,h:24}; + drawNeonBtn(BTNS.playerCardEdit.x,BTNS.playerCardEdit.y,56,24,CFG.CY,'EDIT',8,1,0); + drawNeonBtn(BTNS.playerCardFav.x,BTNS.playerCardFav.y,28,24,playerCardPopup.favorite?'#ff9a1a':CFG.YL,playerCardPopup.favorite?'★':'☆',10,1,0); + drawNeonBtn(BTNS.playerCardFind.x,BTNS.playerCardFind.y,56,24,CFG.GR,'FIND',8,1,0); + } } else { BTNS.playerCardEdit=null; BTNS.playerCardFind=null; BTNS.playerCardFav={x:x+146,y:y+10,w:28,h:24}; @@ -589,6 +758,7 @@ setFont(8,'700',"'Share Tech Mono',monospace"); noGlow(); const cardBest=playerCardPopup.isSelf?bestScore:Number(cardUser.bestScore||0); txt(`Best: ${Math.floor(cardBest||0)}`,x+14,y+122,'rgba(220,240,255,0.9)','left'); + if (playerCardPopup.isSelf) txt(`Level: ${currentLevel()}/100`,x+116,y+122,'#ffd84d','left'); const skinName=playerCardPopup.isSelf?String((CHAR_CATALOG.find(c=>c.id===selectedChar)||CHAR_CATALOG[0]).name):String(cardUser.selectedSkin||cardUser.skin||'Unknown'); const auraName=playerCardPopup.isSelf?String((AURA_CATALOG.find(a=>a.id===selectedAura)||AURA_CATALOG[0]).name):String(cardUser.selectedAura||cardUser.aura||'Unknown'); const themeName=playerCardPopup.isSelf?String((THEME_CATALOG.find(t2=>t2.id===selectedTheme)||THEME_CATALOG[0]).name):String(cardUser.selectedTheme||cardUser.theme||'Unknown'); @@ -671,6 +841,14 @@ ctx.restore(); } + +function backFromSocialOverlay(){ + if (socialBackState==='PLAYER_CARD') { + playerCardPopup.active=true; + } + doTransition(socialBackTarget||'PUBLIC_PROFILE'); +} + function handleTap(e) { const p = getCanvasXY(e); @@ -691,7 +869,7 @@ SFX.click(); playerCardPopup.active=false; socialBackState='PLAYER_CARD'; - socialBackTarget='MENU'; + socialBackTarget=(gs==='PAUSE'?'PAUSE':'MENU'); socialView.ownerUid=String(v.uid||''); socialView.ownerName=String(v.name||'Player'); socialView.ownerIsSelf=!!playerCardPopup.isSelf; @@ -708,7 +886,7 @@ SFX.click(); playerCardPopup.active=false; socialBackState='PLAYER_CARD'; - socialBackTarget='MENU'; + socialBackTarget=(gs==='PAUSE'?'PAUSE':'MENU'); socialView.ownerUid=String(v.uid||''); socialView.ownerName=String(v.name||'Player'); socialView.ownerIsSelf=!!playerCardPopup.isSelf; @@ -725,7 +903,7 @@ SFX.click(); playerCardPopup.active=false; socialBackState='PLAYER_CARD'; - socialBackTarget='MENU'; + socialBackTarget=(gs==='PAUSE'?'PAUSE':'MENU'); socialView.ownerUid=String(v.uid||''); socialView.ownerName=String(v.name||'Player'); socialView.ownerIsSelf=!!playerCardPopup.isSelf; @@ -806,9 +984,11 @@ if (hitBtn(p, BTNS.quests)) { SFX.click(); doTransition('QUESTS'); return; } if (hitBtn(p, BTNS.leaderboard)) { SFX.click(); leaderboardReturnState='MENU'; doTransition('LEADERBOARD'); triggerLeaderboardFetch(); return; } if (hitBtn(p, BTNS.upgrades)) { SFX.click(); doTransition('UPGRADES'); return; } - if (hitBtn(p, BTNS.charShop)) { SFX.click(); doTransition('CHARSHOP'); charShopPage=0; return; } - if (hitBtn(p, BTNS.authAction)) { SFX.click(); if (isSignedIn) { playerCardPopup.active=true; playerCardPopup.isSelf=true; playerCardPopup.fromMenu=true; playerCardPopup.viewed=null; } else guestProfilePrompt.active=true; authFlowState='idle'; authFlowTimer=0; authFlowError=''; return; } - if (hitBtn(p, BTNS.menuProfileBox)) { SFX.click(); if (isSignedIn) { playerCardPopup.active=true; playerCardPopup.isSelf=true; playerCardPopup.fromMenu=true; playerCardPopup.viewed=null; } else guestProfilePrompt.active=true; return; } + if (hitBtn(p, BTNS.charShop)) { SFX.click(); charShopReturnState='MENU'; charShopView='shop'; charShopCategoryPage=0; charShopActiveTab='featured'; charShopFilterOpen=false; doTransition('CHARSHOP'); return; } + if (hitBtn(p, BTNS.authAction)) { SFX.click(); if (isSignedIn) { playerCardPopup.active=true; playerCardPopup.isSelf=true; playerCardPopup.fromMenu=true; playerCardPopup.viewed=null; playerCardPopup.lite=false; } else guestProfilePrompt.active=true; authFlowState='idle'; authFlowTimer=0; authFlowError=''; return; } + if (hitBtn(p, BTNS.menuProfileBox)) { SFX.click(); if (isSignedIn) { playerCardPopup.active=true; playerCardPopup.isSelf=true; playerCardPopup.fromMenu=true; playerCardPopup.viewed=null; playerCardPopup.lite=false; } else guestProfilePrompt.active=true; return; } + if (hitBtn(p, BTNS.menuAlerts) && isSignedIn && navigator.onLine) { SFX.click(); notificationsReturnState='MENU'; doTransition('NOTIFICATIONS'); return; } + if (hitBtn(p, BTNS.menuLevelBadge)) { SFX.click(); levelsReturnState='MENU'; doTransition('LEVELS'); return; } return; } if (gs === 'MODESELECT') { @@ -816,13 +996,14 @@ if (hitBtn(p, BTNS.modeAdventure)) { SFX.click(); gameMode='ADVENTURE'; doTransition('SHOP'); return; } if (hitBtn(p, BTNS.modeImpossible)) { SFX.click(); openConfirm('IMPOSSIBLE_START','IMPOSSIBLE MODE WARNING','One life only. No starter powerups. Are you sure?'); return; } if (hitBtn(p, BTNS.modeLtm)) { - if (!navigator.onLine) { addNotif('LTM REQUIRES INTERNET',CFG.PK); return; } + if (!isSignedIn || !navigator.onLine) { addNotif('LTM REQUIRES SIGN-IN + INTERNET',CFG.PK); return; } SFX.click(); gameMode='LTM'; if (isAdminUser) doTransition('ADMIN_LTM'); else doTransition('SHOP'); return; } + if (hitBtn(p, BTNS.modeNeonRush)) { SFX.click(); gameMode='NEON_RUSH'; doTransition('SHOP'); return; } if (hitBtn(p, BTNS.back)) { SFX.click(); doTransition('MENU'); return; } return; } @@ -840,7 +1021,7 @@ handleSettingsTap(p); return; } if (gs === 'QUESTS') { - if (hitBtn(p, BTNS.questsBack)) { SFX.click(); doTransition('MENU'); return; } + if (hitBtn(p, BTNS.questsBack)) { SFX.click(); doTransition(questsReturnState||'MENU'); questsReturnState='MENU'; return; } if (hitBtn(p, BTNS.questsFilter)) { SFX.click(); questFilterOpen=!questFilterOpen; return; } const fOpts=[BTNS.questsFilterOpt0,BTNS.questsFilterOpt1,BTNS.questsFilterOpt2,BTNS.questsFilterOpt3]; for (let i=0;i { + disableUiNavFromPointer(); markPresenceActivity('touchstart'); e.preventDefault(); const p=getCanvasXY(e); @@ -1026,6 +1221,7 @@ }, { passive:false }); canvas.addEventListener('touchmove', e => { + disableUiNavFromPointer(); e.preventDefault(); if (usingTouchJoy) return; if (!touchActive) return; @@ -1080,6 +1276,26 @@ shopTouchY=t.clientY; return; } + if (gs==='SETTINGS') { + const t=e.touches[0]; + if (settingsTouchY!==null) { + const dy=t.clientY-settingsTouchY; + if (Math.abs(dy)>2) touchMoved=true; + settingsScroll=clamp(settingsScroll+dy, minSettingsScroll(), 0); + } + settingsTouchY=t.clientY; + return; + } + if (gs==='OPTIONS') { + const t=e.touches[0]; + if (optionsTouchY!==null) { + const dy=t.clientY-optionsTouchY; + if (Math.abs(dy)>2) touchMoved=true; + optionsScroll=clamp(optionsScroll+dy, minOptionsScroll(), 0); + } + optionsTouchY=t.clientY; + return; + } if (gs==='FRIEND_REQUESTS' || gs==='FRIENDS_LIST') { const t=e.touches[0]; if (socialTouchY!==null) { @@ -1134,6 +1350,7 @@ // Mouse (desktop) canvas.addEventListener('mousedown', e => { + disableUiNavFromPointer(); markPresenceActivity('mousedown'); if (gs==='PROFILE') { const p=getCanvasXY(e); @@ -1152,6 +1369,7 @@ } }); canvas.addEventListener('mousemove', e => { + disableUiNavFromPointer(); markPresenceActivity('mousemove'); const mp=getCanvasXY(e); mouseCanvasPos.x=mp.x; mouseCanvasPos.y=mp.y; @@ -1174,12 +1392,14 @@ canvas.addEventListener('mouseleave', () => { touchActive=false; touchSwipe=0; touchSwipeTarget=0; joyL=false; joyR=false; usingTouchJoy=false; profileAvatarPressing=false; profileAvatarHoldFX=false; stopProfileCropInteraction(); }); document.addEventListener('click', e => { + disableUiNavFromPointer(); if (Date.now() - _lastTouchEnd < 450) return; handleTap(e); }); document.addEventListener('wheel', e => { + disableUiNavFromPointer(); if (gs==='QUESTS') { e.preventDefault(); questScroll=clamp(questScroll - e.deltaY*0.6, minQuestScroll(), 0); @@ -1231,6 +1451,20 @@ // Keyboard window.addEventListener('keydown', e => { markPresenceActivity('keydown'); + if (actionPopup.active) { + if (!actionPopup.loading && (e.key==='Enter' || e.key===' ' || e.key==='Escape')) { + const cb=actionPopup.onContinue; + actionPopup.active=false; + actionPopup.onContinue=null; + if (typeof cb==='function' && (e.key==='Enter' || e.key===' ')) cb(); + e.preventDefault(); + return; + } + if (actionPopup.loading && (e.key==='Enter' || e.key===' ' || e.key==='Escape')) { + e.preventDefault(); + return; + } + } if (inputDialog.active) { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase()==='a') { inputDialog.selectAll=true; e.preventDefault(); return; } if (e.key==='Escape') { closeInputDialog(false); e.preventDefault(); return; } @@ -1274,6 +1508,8 @@ } } + if (handleUiNavKey(e)) return; + const l = e.key==='ArrowLeft' || e.key.toLowerCase()==='a'; const r = e.key==='ArrowRight' || e.key.toLowerCase()==='d'; if (l) K.left = true; @@ -1281,20 +1517,22 @@ if ((e.key===' '||e.key==='Enter') && gs==='MENU') { doTransition('MODESELECT'); } if ((e.key===' '||e.key==='Enter') && gs==='OVER') { doTransition('SHOP'); shuffleShop(); } if (e.key==='Escape' && gs==='SHOP') { doTransition('MODESELECT'); } - if (e.key==='Escape' && gs==='CHARSHOP') { doTransition('MENU'); } + if (e.key==='Escape' && gs==='CHARSHOP') { doTransition(charShopReturnState||'MENU'); charShopReturnState='MENU'; } if (e.key==='Escape' && gs==='MODESELECT'){ doTransition('MENU'); } - if (e.key==='Escape' && gs==='QUESTS') { doTransition('MENU'); } + if (e.key==='Escape' && gs==='QUESTS') { doTransition(questsReturnState||'MENU'); questsReturnState='MENU'; } if (e.key==='Escape' && gs==='ABOUT') { doTransition('MENU'); } if (e.key==='Escape' && gs==='CONTROLS') { doTransition('MENU'); } if (e.key==='Escape' && gs==='LEADERBOARD'){ doTransition(leaderboardReturnState==='PAUSE'?'PAUSE':'MENU'); } + if (e.key==='Escape' && gs==='NOTIFICATIONS') { doTransition(notificationsReturnState||'MENU'); notificationsReturnState='MENU'; } + if (e.key==='Escape' && gs==='LEVELS') { doTransition(levelsReturnState||'MENU'); levelsReturnState='MENU'; } if (e.key==='Escape' && gs==='CONTINUE') { openConfirm('CONTINUE_GIVEUP','GIVE UP RUN?','You will lose this run progress.'); } if (e.key==='Escape' && gs==='UPGRADES') { doTransition('MENU'); } if (e.key==='Escape' && gs==='SIGNIN') { doTransition('MENU'); } if (e.key==='Escape' && gs==='SIGNUP') { doTransition('SIGNIN'); } if (e.key==='Escape' && gs==='PROFILE') { doTransition('MENU'); } if (e.key==='Escape' && gs==='PUBLIC_PROFILE') { doTransition('PROFILE'); } - if (e.key==='Escape' && gs==='FRIEND_REQUESTS') { doTransition(socialBackTarget||'PUBLIC_PROFILE'); } - if (e.key==='Escape' && gs==='FRIENDS_LIST') { doTransition(socialBackTarget||'PUBLIC_PROFILE'); } + if (e.key==='Escape' && gs==='FRIEND_REQUESTS') { backFromSocialOverlay(); } + if (e.key==='Escape' && gs==='FRIENDS_LIST') { backFromSocialOverlay(); } if (e.key==='Escape' && gs==='ADMIN_AUTH') { doTransition('MENU'); } if (e.key==='Escape' && gs==='ADMIN_MENU') { doTransition('MENU'); } if (e.key==='Escape' && gs==='ADMIN_LTM') { doTransition('MODESELECT'); } @@ -1325,13 +1563,21 @@ function pollGamepad() { const pads=navigator.getGamepads?navigator.getGamepads():[]; const gp=(pads&&pads[0])?pads[0]:null; - GP.left=false; GP.right=false; GP.pauseEdge=false; - if (!gp) { GP.lastPause=false; return; } + GP.left=false; GP.right=false; GP.up=false; GP.down=false; GP.confirmEdge=false; GP.pauseEdge=false; + if (!gp) { GP.lastPause=false; GP.lastConfirm=false; return; } const ax=(gp.axes&&gp.axes.length)?gp.axes[0]:0; + const ay=(gp.axes&&gp.axes.length>1)?gp.axes[1]:0; const dL=gp.buttons&&gp.buttons[14]&&gp.buttons[14].pressed; const dR=gp.buttons&&gp.buttons[15]&&gp.buttons[15].pressed; + const dU=gp.buttons&&gp.buttons[12]&&gp.buttons[12].pressed; + const dD=gp.buttons&&gp.buttons[13]&&gp.buttons[13].pressed; GP.left=ax<-0.35||!!dL; GP.right=ax>0.35||!!dR; + GP.up=ay<-0.35||!!dU; + GP.down=ay>0.35||!!dD; + const confirmNow=(gp.buttons&&gp.buttons[0]&&gp.buttons[0].pressed); + GP.confirmEdge=confirmNow&&!GP.lastConfirm; + GP.lastConfirm=!!confirmNow; const pauseNow=(gp.buttons&&gp.buttons[9]&&gp.buttons[9].pressed)||(gp.buttons&&gp.buttons[7]&&gp.buttons[7].pressed); GP.pauseEdge=pauseNow&&!GP.lastPause; GP.lastPause=!!pauseNow; @@ -1389,6 +1635,7 @@ ctx.clip(); } function drawNeonBtn(x,y,w,h,col,label,fontSize=14,alpha=1,pulse=1) { + addUiNavButton(x,y,w,h); ctx.save(); ctx.globalAlpha=alpha*(0.7+0.3*pulse); glow(col,18); ctx.strokeStyle=col; ctx.lineWidth=2; ctx.strokeRect(x,y,w,h); @@ -1474,14 +1721,16 @@ function drawTopOverlays(t){ drawSocialToasts(); drawGlobalFps(); - drawPlayerCardPopup(t); - drawUsernameChangeOverlay(t); - drawActionPopup(t); - drawAuthOverlay(t); - drawOptimizeConfirmPopup(t); - drawConfirmDialog(); - drawInputDialog(t); - drawGuestProfilePrompt(t); + setUiNavScope('playerCard'); drawPlayerCardPopup(t); + setUiNavScope('usernameChange'); drawUsernameChangeOverlay(t); + setUiNavScope('actionPopup'); drawActionPopup(t); + setUiNavScope('base'); drawAuthOverlay(t); + setUiNavScope('optimizeConfirm'); drawOptimizeConfirmPopup(t); + setUiNavScope('confirmDialog'); drawConfirmDialog(); + setUiNavScope('inputDialog'); drawInputDialog(t); + setUiNavScope('guestPrompt'); drawGuestProfilePrompt(t); + setUiNavScope('base'); + drawUiFocusHighlight(t); } function drawGlobalFps(){ @@ -1734,14 +1983,22 @@ const notifs=[]; const socialToasts=[]; +const notifHistory=[]; const socialToastSquares=[]; +const levelUpToasts=[]; function addNotif(str,col) { - notifs.push({str,col,t:1.8}); + const msg=String(str||'').slice(0,72); + notifs.push({str:msg,col,t:1.8}); + notifHistory.push({str:msg,col,at:Date.now(),kind:'system'}); if (notifs.length>3) notifs.shift(); + while (notifHistory.length>40) notifHistory.shift(); } function addSocialToast(str,col=CFG.CY){ + if (!isSignedIn || !navigator.onLine) return; const msg=String(str||'').slice(0,72); socialToasts.push({str:msg,col,t:2.8,dur:2.8}); + notifHistory.push({str:msg,col,at:Date.now(),kind:'social'}); + while (notifHistory.length>40) notifHistory.shift(); while (socialToasts.length>3) socialToasts.shift(); const cx=LW/2; for (let i=0;i<16;i++) { @@ -1758,6 +2015,10 @@ p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=35*dt; p.t-=dt; if (p.t<=0) socialToastSquares.splice(i,1); } + for (let i=levelUpToasts.length-1;i>=0;i--) { + levelUpToasts[i].t-=dt; + if (levelUpToasts[i].t<=0) levelUpToasts.splice(i,1); + } } function drawNotifs() { notifs.forEach((n,i)=>{ @@ -1767,6 +2028,28 @@ ctx.save(); ctx.globalAlpha=a; setFont(13,'700'); glow(n.col,16); txt(`◆ ${n.str} ◆`,LW/2,y,n.col); ctx.restore(); }); + levelUpToasts.forEach((n,i)=>{ + const inA=clamp((n.dur-n.t)/0.22,0,1); + const outA=clamp(n.t/0.45,0,1); + const a=Math.min(inA,outA); + const spin=(1-outA)*Math.PI*2; + const size=30; + const x=LW-42; + const y=64+i*40; + ctx.save(); + ctx.globalAlpha=a; + ctx.translate(x,y); + ctx.rotate(spin); + ctx.strokeStyle='#ffd84d'; + ctx.lineWidth=2; + glow('#ffd84d',12); + ctx.strokeRect(-size/2,-size/2,size,size); + ctx.fillStyle='rgba(255,216,77,0.2)'; + ctx.fillRect(-size/2,-size/2,size,size); + ctx.restore(); + setFont(10,'900'); glow('#ffd84d',8); txt(String(n.to),x,y+4,'#ffd84d'); + setFont(7,'700',"'Share Tech Mono',monospace"); noGlow(); txt('+LV',x,y+18,'#dff6ff'); + }); } function drawSocialToasts(){ socialToastSquares.forEach(p=>{ @@ -1800,7 +2083,7 @@ function clearAllFX() { particles.length=0; exhaustParts.length=0; fogParts.length=0; rings.length=0; - floatTexts.length=0; notifs.length=0; socialToasts.length=0; socialToastSquares.length=0; + floatTexts.length=0; notifs.length=0; socialToasts.length=0; socialToastSquares.length=0; levelUpToasts.length=0; notifHistory.length=0; } @@ -1865,6 +2148,10 @@ const PLAYER_BASE_Y=LH*0.38; const SIDE_BORDER_PAD_BASE=6; const IMMUNITY_COLLISION_COOLDOWN=3.0; +const IMMUNITY_POLICY=Object.freeze({ + hit:Object.freeze({ENDLESS:3.0,ADVENTURE:3.0,LTM:3.0,IMPOSSIBLE:1.5}), + powerup:Object.freeze({ENDLESS:3.0,ADVENTURE:3.0,LTM:3.0,IMPOSSIBLE:1.5}) +}); class Player { constructor() { this.reset(); } @@ -2010,6 +2297,7 @@ SLOW: {col:CFG.GR, label:'SLOW-MO', icon:'⏱', dur:4.5 }, BOOST: {col:CFG.YL, label:'SCORE ×2', icon:'⚡', dur:10.0}, MAGNET: {col:CFG.PU, label:'MAGNET', icon:'✦', dur:10.0}, + IMMUNITY: {col:CFG.GR, label:'IMMUNITY', icon:'🟢', dur:10.0}, JETPACK: {col:CFG.OR, label:'JETPACK', icon:'🚀', dur:5.0 }, HEALTH_BOOST: {col:'#ff44aa',label:'HEALTH BOOST', icon:'❤', dur:30 }, REGEN: {col:'#ff6699',label:'REGENERATION', icon:'✚', dur:0 }, @@ -2027,6 +2315,7 @@ ...Array(4).fill('SLOW'), ...Array(4).fill('BOOST'), ...Array(4).fill('MAGNET'), + ...Array(3).fill('IMMUNITY'), ...Array(5).fill('JETPACK'), ...Array(3).fill('HEALTH_BOOST'), ...Array(3).fill('REGEN'), @@ -2051,7 +2340,7 @@ if (Math.random()<0.10) return 'SICK'; if (Math.random()<0.12) return 'LTM_SPECIAL'; if (forced.length) { - const map={X2_SCORE:'BOOST',JETPACK:'JETPACK',MINI_JETPACK:'MINI_JETPACK',SHIELD:'SHIELD',MAGNET:'MAGNET',REGEN:'REGEN',HEALTH_BOOST:'HEALTH_BOOST',IMMUNITY:'SHIELD',X2_GEMS:'X2_GEMS',X4_GEMS:'X4_GEMS',GEM_DROP:'GEM_DROP'}; + const map={X2_SCORE:'BOOST',JETPACK:'JETPACK',MINI_JETPACK:'MINI_JETPACK',SHIELD:'SHIELD',MAGNET:'MAGNET',REGEN:'REGEN',HEALTH_BOOST:'HEALTH_BOOST',IMMUNITY:'IMMUNITY',X2_GEMS:'X2_GEMS',X4_GEMS:'X4_GEMS',GEM_DROP:'GEM_DROP'}; const key=forced[rndI(0,forced.length-1)]; return map[key]||'BOOST'; } @@ -2306,6 +2595,22 @@ glow(tc,8); circle(trail[i].x,trail[i].y,CFG.PR*t2*0.65,tc); ctx.restore(); spark(trail[i].x,trail[i].y,tc); } + if (activePwr==='MAGNET') { + ctx.save(); + ctx.globalAlpha=0.26+0.16*Math.sin(menuT*7); + glow(CFG.PU,22); ctx.strokeStyle=CFG.PU; ctx.lineWidth=2; + circle(x,y,CFG.PR+18,CFG.PU,false); + ctx.globalAlpha=0.28; setFont(8,'900'); noGlow(); txt('✦',x+CFG.PR+12,y+3,CFG.PU); + txt('✦',x-CFG.PR-12,y-2,CFG.PU); + ctx.restore(); + } + if (player.shield) { + ctx.save(); + ctx.globalAlpha=0.22+0.12*Math.sin(menuT*5); + glow(CFG.BL,20); ctx.strokeStyle=CFG.BL; ctx.lineWidth=2; + circle(x,y,CFG.PR+20,CFG.BL,false); + ctx.restore(); + } if (jetpackActive) { ctx.save(); for (let i=0;i<3;i++) { @@ -2344,6 +2649,22 @@ glow(ac,22); ctx.strokeStyle=ac; ctx.lineWidth=2; circle(x,y,CFG.PR+13,ac,false); ctx.restore(); } + if (activePwr==='MAGNET') { + ctx.save(); + ctx.globalAlpha=0.26+0.16*Math.sin(menuT*7); + glow(CFG.PU,22); ctx.strokeStyle=CFG.PU; ctx.lineWidth=2; + circle(x,y,CFG.PR+18,CFG.PU,false); + ctx.globalAlpha=0.28; setFont(8,'900'); noGlow(); txt('✦',x+CFG.PR+12,y+3,CFG.PU); + txt('✦',x-CFG.PR-12,y-2,CFG.PU); + ctx.restore(); + } + if (player.shield) { + ctx.save(); + ctx.globalAlpha=0.22+0.12*Math.sin(menuT*5); + glow(CFG.BL,20); ctx.strokeStyle=CFG.BL; ctx.lineWidth=2; + circle(x,y,CFG.PR+20,CFG.BL,false); + ctx.restore(); + } if (jetpackActive) { ctx.save(); ctx.globalAlpha=0.5+0.3*Math.sin(menuT*12); @@ -2413,9 +2734,10 @@ ctx.restore(); } - ctx.save(); setFont(9,'400',"'Share Tech Mono',monospace"); - txt('HIGHSCORE',pdX+22,pdY+8,'rgba(0,245,255,0.42)','center'); - setFont(14,'700'); glow(CFG.CY,10); txt(`${Math.floor(bestScore)}`,pdX+22,pdY+22,CFG.CY,'center'); + ctx.save(); setFont(8,'400',"'Share Tech Mono',monospace"); + const hsLabel=gameMode==='ADVENTURE'?'ADV BEST':gameMode==='IMPOSSIBLE'?'IMP BEST':gameMode==='LTM'?'LTM BEST':'END BEST'; + txt(hsLabel,pdX+22,pdY+8,'rgba(0,245,255,0.42)','center'); + setFont(14,'700'); glow(CFG.CY,10); txt(`${Math.floor(currentModeBestScore())}`,pdX+22,pdY+22,CFG.CY,'center'); ctx.restore(); // Adventure stage indicator @@ -2453,7 +2775,7 @@ ctx.fillStyle=hexToRgba(CFG.CY,isPaused?0.18:0.06); ctx.fillRect(pbX,pbY,pbW,pbH); setFont(9,'900'); glow(pbCol,8); txt(isPaused?'▶':'❚❚',pbX+pbW/2,pbY+pbH/2+3.5,pbCol); ctx.restore(); - const obX=pbX+44,obY=pbY,obW=42,obH=20; + const obX=pbX,obY=pbY+24,obW=42,obH=20; BTNS.hudOptions={x:obX,y:obY,w:obW,h:obH}; ctx.save(); ctx.strokeStyle='rgba(204,0,255,0.6)'; ctx.lineWidth=1.3; glow(CFG.PU,6); ctx.strokeRect(obX,obY,obW,obH); @@ -2555,8 +2877,13 @@ } if (player.shield) { - ctx.save(); setFont(10,'700'); glow(CFG.BL,14); - txt('🛡 SHIELD',pdX+2,LH-pdY-38,CFG.BL,'left'); ctx.restore(); + const shX=pdX-2, shY=LH-pdY-54, shW=112, shH=24; + ctx.save(); + ctx.fillStyle='rgba(68,136,255,0.12)'; ctx.fillRect(shX,shY,shW,shH); + ctx.strokeStyle='rgba(68,136,255,0.9)'; ctx.lineWidth=1.5; glow(CFG.BL,10); ctx.strokeRect(shX,shY,shW,shH); + setFont(9,'900',"'Share Tech Mono',monospace"); + txt('🛡 SHIELD ACTIVE',shX+8,shY+16,CFG.BL,'left'); + ctx.restore(); } // Touch hint @@ -2701,9 +3028,9 @@ setFont(60,'900'); glow(CFG.CY,30); ctx.fillStyle=CFG.CY; ctx.textAlign='center'; ctx.fillText('SLOPE',LW/2,titleY); setFont(13,'900'); glow(CFG.CY,10); - txt('SEASON 1',LW/2+seasonScrollX,titleY+34,'rgba(0,245,255,0.8)'); + txt(SEASON_LIVE,LW/2+seasonScrollX,titleY+34,'rgba(0,245,255,0.8)'); setFont(11,'400',"'Share Tech Mono',monospace"); glow(CFG.CY,8); - txt('NEON RUSH',LW/2,titleY+50,'rgba(0,245,255,0.7)'); + txt(`${SEASON_NEXT} • ${SEASON_NEXT_RELEASE}`,LW/2,titleY+50,'rgba(0,245,255,0.7)'); txt(`VERSION ${VERSION}`,LW/2,titleY+64,'rgba(0,245,255,0.35)'); // Divider @@ -2795,9 +3122,26 @@ if (isSignedIn && auth && auth.admin===true) { drawRoleBadgeSquare(bx,21,8,CFG.YL,t,false); bx-=12; } if (isSignedIn && auth && auth.verified===true) { drawRoleBadgeSquare(bx,21,8,'#33aaff',t,true); } ctx.restore(); + const lvlNow=currentLevel(); + const showSocialUi=isSignedIn&&navigator.onLine; + const notifCount=Math.max(0,Number(socialCounters.received||0)+Number(socialCounters.sent||0)); + BTNS.menuAlerts=null; + if (showSocialUi) { + BTNS.menuAlerts={x:LW-nameBannerW-14-82,y:10,w:76,h:22}; + ctx.save(); + const nX=BTNS.menuAlerts.x,nY=BTNS.menuAlerts.y,nW=BTNS.menuAlerts.w,nH=BTNS.menuAlerts.h; + ctx.fillStyle='rgba(32,120,230,0.55)'; ctx.fillRect(nX,nY,nW,nH); + ctx.strokeStyle='rgba(170,220,255,0.9)'; ctx.lineWidth=1; ctx.strokeRect(nX,nY,nW,nH); + setFont(8,'700',"'Share Tech Mono',monospace"); noGlow(); txt(`🔔 ${notifCount}`,nX+8,nY+15,'#e7f5ff','left'); + ctx.restore(); + } BTNS.noAds={x:8,y:8,w:28,h:28}; ctx.save(); ctx.fillStyle='rgba(255,30,30,0.75)'; ctx.fillRect(8,8,28,28); ctx.strokeStyle='#ff9090'; ctx.strokeRect(8,8,28,28); setFont(10,'900',"'Share Tech Mono',monospace"); noGlow(); txt('⛔',22,27,'#fff'); ctx.restore(); + BTNS.menuLevelBadge={x:8,y:40,w:28,h:28}; + ctx.save(); ctx.fillStyle='rgba(255,216,77,0.20)'; ctx.fillRect(8,40,28,28); ctx.strokeStyle='#ffd84d'; ctx.lineWidth=1.1; glow('#ffd84d',8); ctx.strokeRect(8,40,28,28); + drawLevelBadge(22,54,14,lvlNow,'#ffd84d'); + ctx.restore(); if (!navigator.onLine) { setFont(14,'900'); glow(CFG.PK,12); txt('PLAYER OFFLINE',LW/2,LH/2-8,CFG.PK); setFont(9,'400',"'Share Tech Mono',monospace"); noGlow(); txt('Connect to internet to load leaderboard.',LW/2,LH/2+16,CFG.CY); @@ -2809,8 +3153,8 @@ } btnY+=authH+gap; - ctx.save(); noGlow(); setFont(10,'700'); glow(CFG.CY,8); ctx.globalAlpha=0.7; - txt(`◆ ${totalGems}`,LW-14,LH-8,CFG.CY,'right'); ctx.restore(); + ctx.save(); noGlow(); setFont(10,'700'); glow(CFG.CY,8); ctx.globalAlpha=0.8; + txt(`⬡ EXP ${fmtExpShort(levelXp)} ◆ ${totalGems}`,LW-14,LH-8,CFG.CY,'right'); ctx.restore(); if (noAdsPopup) { ctx.save(); ctx.fillStyle='rgba(0,0,0,0.74)'; ctx.fillRect(0,0,LW,LH); @@ -2883,23 +3227,23 @@ ctx.strokeStyle='rgba(255,230,0,0.35)'; ctx.strokeRect(x,y,w,h); setFont(22,'900'); glow(CFG.YL,12); txt(title,LW/2,60,CFG.YL); setFont(9,'700',"'Share Tech Mono',monospace"); noGlow(); txt(sub,LW/2,80,'rgba(255,240,150,0.9)'); - BTNS.socialTabFriends={x:x+8,y:y-2,w:104,h:24}; - BTNS.socialTabFollowers={x:x+118,y:y-2,w:104,h:24}; - if (mode==='list') BTNS.socialTabFollowing={x:x+228,y:y-2,w:104,h:24}; else BTNS.socialTabFollowing=null; + BTNS.socialTabFriends={x:x+8,y:y+4,w:104,h:24}; + BTNS.socialTabFollowers={x:x+118,y:y+4,w:104,h:24}; + if (mode==='list') BTNS.socialTabFollowing={x:x+228,y:y+4,w:104,h:24}; else BTNS.socialTabFollowing=null; drawNeonBtn(BTNS.socialTabFriends.x,BTNS.socialTabFriends.y,BTNS.socialTabFriends.w,BTNS.socialTabFriends.h,CFG.CY,mode==='requests'?'RECEIVED':'FRIENDS',8,1,0); drawNeonBtn(BTNS.socialTabFollowers.x,BTNS.socialTabFollowers.y,BTNS.socialTabFollowers.w,BTNS.socialTabFollowers.h,CFG.PU,mode==='requests'?'SENT':'FOLLOWERS',8,1,0); if (mode==='list') drawNeonBtn(BTNS.socialTabFollowing.x,BTNS.socialTabFollowing.y,BTNS.socialTabFollowing.w,BTNS.socialTabFollowing.h,CFG.GR,'FOLLOWING',8,1,0); - const listX=x+8,listY=y+30,listW=w-16,listH=h-38; + const listX=x+8,listY=y+38,listW=w-16,listH=h-46; socialView.viewH=listH; - socialView.contentH=Math.max(listH,Math.max(1,list.length)*58+8); + socialView.contentH=Math.max(listH,Math.max(1,list.length)*64+10); socialView.scroll=clamp(socialView.scroll,minSocialScroll(),0); ctx.save(); ctx.beginPath(); ctx.rect(listX,listY,listW,listH); ctx.clip(); ctx.translate(0,socialView.scroll); socialView.renderCount=Math.min(list.length,socialView.renderCount+4); for (let i=0;i{ if (!line) { y+=10; return; } - const head=/^v0\./.test(line); + const head=/^(pre-|v0\.|0\.)/.test(line); setFont(head?11:9,head?'900':'700',"'Share Tech Mono',monospace"); if (head) glow(CFG.CY,10); else noGlow(); txt(line,boxX+14,y,head?CFG.CY:'rgba(230,255,255,0.82)','left'); @@ -3061,6 +3406,10 @@ } // ── MODE SELECT ─────────────────────────────────────────────────── +function isLtmModeVisible(){ + return !!(isSignedIn && navigator.onLine); +} + function drawModeSelect(t) { drawPerspGrid(terrain.scrollY,0.05); drawTerrain(); drawBgSquares(); @@ -3071,15 +3420,19 @@ txt('Pick a lane • each mode has unique pacing + rewards',LW/2,82,'rgba(0,245,255,0.55)'); ctx.globalAlpha=1; const cardY=102, cardH=382, cardW=88, gap=8; - const startX=(LW-(cardW*4+gap*3))/2; + const ltmVisible=isLtmModeVisible(); + const cardCount=ltmVisible?4:3; + const startX=(LW-(cardW*cardCount+gap*(cardCount-1)))/2; const cards=[ {key:'modeEndless',x:startX,col:CFG.CY,title:'ENDLESS',sub:'CLASSIC',icon:'∞',feats:['Classic run','No cap','Score chase'],btn:'PLAY'}, {key:'modeAdventure',x:startX+cardW+gap,col:CFG.GR,title:'ADVENTURE',sub:'STAGES',icon:'🌀',feats:['Stage flow','Ramp','Rewards'],btn:'PLAY'}, {key:'modeImpossible',x:startX+(cardW+gap)*2,col:'#ff4455',title:'IMPOSSIBLE',sub:'ONE LIFE',icon:'☠',feats:['4 brutal','No regen','Red storm'],btn:'PLAY'}, - {key:'modeLtm',x:startX+(cardW+gap)*3,col:'#ffd700',title:'LTM',sub:'GLOBAL LIVE',icon:'★',feats:['Global RTDB mode','Internet required','10 themed rotations'],btn:'SELECT'} + ...(ltmVisible?[{key:'modeLtm',x:startX+(cardW+gap)*3,col:'#ffd700',title:'LTM',sub:'GLOBAL LIVE',icon:'★',feats:['Global RTDB mode','Internet required','10 themed rotations'],btn:'SELECT'}]:[]) ]; + BTNS.modeLtm=null; BTNS.modeNeonRush=null; + cards.forEach((c,i)=>{ const x=c.x,y=cardY; BTNS[c.key]={x,y,w:cardW,h:cardH}; @@ -3093,6 +3446,13 @@ ctx.save(); ctx.fillStyle='#ffe600'; ctx.fillRect(x+cardW-34,y+4,30,12); setFont(7,'900',"'Share Tech Mono',monospace"); noGlow(); txt('NEW!',x+cardW-19,y+13,'#000'); ctx.restore(); } + if (c.key==='modeLtm') { + const cTxt=getDailyCountdown(); + ctx.save(); + ctx.fillStyle='#ffe600'; ctx.fillRect(x+cardW-74,y+8,66,14); + setFont(7,'900',"'Share Tech Mono',monospace"); noGlow(); txt(cTxt,x+cardW-41,y+18,'#000'); + ctx.restore(); + } setFont(26,'900'); glow(c.col,14); txt(c.icon,x+cardW/2,y+100,c.col); setFont(8,'400',"'Share Tech Mono',monospace"); noGlow(); c.feats.forEach((f,j)=>txt(`• ${f}`,x+10,y+136+j*16,c.col,'left')); @@ -3104,12 +3464,12 @@ ctx.restore(); }); - if (navigator.onLine && (Date.now()-ltmConfigFetchAt>16000)) loadGlobalLtmConfig(false); + if (navigator.onLine && (Date.now()-ltmConfigFetchAt>16000)) { loadGlobalLtmConfig(false); loadGlobalShopConfig(false); } const daily=getActiveLtm(); const dailyIdx=clamp(ltmSelectedIndex>=0?ltmSelectedIndex:getDailyLtmIndex(),0,Math.max(0,LTM_MODES.length-1)); const themeA=daily.cardCol||'#ffd700'; const nextCol=(LTM_MODES[(dailyIdx+1)%LTM_MODES.length]&<M_MODES[(dailyIdx+1)%LTM_MODES.length].cardCol)||'#ffe45a'; - const soonW=cardW*4+gap*3, soonH=62, soonX=startX, soonY=cardY+cardH+12; + const soonW=cardW*cardCount+gap*(cardCount-1), soonH=62, soonX=startX, soonY=cardY+cardH+12; ctx.save(); const bg=ctx.createLinearGradient(soonX,0,soonX+soonW,0); bg.addColorStop(0,hexToRgba(themeA,0.18)); @@ -3126,10 +3486,22 @@ txt(`LTM ${dailyIdx+1}/10 • ${daily.name}`,soonX+10,soonY+18,'#0d0d0d','left'); setFont(8,'700',"'Share Tech Mono',monospace"); txt(`${daily.desc||'Live special mode'} • ${(daily.specialName||'Mode bonus').slice(0,26)}`,soonX+10,soonY+34,'#ffffff','left'); - const netTxt=navigator.onLine?`GLOBAL ROTATION • NEXT UPDATE IN ${getDailyCountdown()}`:'OFFLINE • LTM PLAY DISABLED'; + const netTxt=ltmVisible?`GLOBAL ROTATION • NEXT UPDATE IN ${getDailyCountdown()}`:'SIGN IN + INTERNET REQUIRED FOR LTM'; txt(netTxt,soonX+10,soonY+50,'#0d0d0d','left'); ctx.restore(); + + const nrY=soonY+soonH+10; + const nrW=soonW; + const nrH=32; + ctx.save(); + ctx.fillStyle='rgba(0,245,255,0.12)'; ctx.fillRect(soonX,nrY,nrW,nrH); + ctx.strokeStyle='rgba(0,245,255,0.8)'; ctx.lineWidth=1.5; ctx.strokeRect(soonX,nrY,nrW,nrH); + setFont(9,'900',"'Share Tech Mono',monospace"); glow(CFG.CY,8); txt('NEON RUSH! (NEW!)',soonX+12,nrY+20,CFG.CY,'left'); + BTNS.modeNeonRush={x:soonX+nrW-96,y:nrY+3,w:90,h:26}; + drawNeonBtn(BTNS.modeNeonRush.x,BTNS.modeNeonRush.y,90,26,CFG.YL,'PLAY',10,1,Math.sin(t*3)); + ctx.restore(); + const bW=130,bH=34,bX=(LW-bW)/2,bY=LH-46; BTNS.back={x:bX,y:bY,w:bW,h:bH}; drawNeonBtn(bX,bY,bW,bH,CFG.PK,'← BACK',11,0.85,Math.sin(t*2)); @@ -3181,6 +3553,17 @@ const viewH=(LH-150)-8; return Math.min(0, viewH-contentH); } +function questUnlockPreview(obj){ + const id=String((obj&&obj.id)||''); + if (!id) return null; + const skin=CHAR_CATALOG.find(v=>v.unlock==='obj'&&v.objId===id); + if (skin) return {kind:'skin',name:skin.name,icon:skin.icon||'●',col:skin.col||CFG.CY,owned:unlockedChars.includes(skin.id),rarity:'rare'}; + const aura=AURA_CATALOG.find(v=>v.unlock==='obj'&&v.objId===id); + if (aura) return {kind:'aura',name:aura.name,icon:aura.icon||'◍',col:aura.col||CFG.GR,owned:unlockedAuras.includes(aura.id),rarity:'epic'}; + const theme=THEME_CATALOG.find(v=>v.unlock==='obj'&&v.objId===id); + if (theme) return {kind:'theme',name:theme.name,icon:theme.icon||'▦',col:theme.col||CFG.PU,owned:unlockedThemes.includes(theme.id),rarity:'mythic'}; + return null; +} function drawQuestsScreen(t) { refreshHourlyObjectives(); questScroll=clamp(questScroll,minQuestScroll(),0); @@ -3196,7 +3579,7 @@ const filteredObjectives=getFilteredObjectives(); setFont(9,'400',"'Share Tech Mono',monospace"); noGlow(); ctx.globalAlpha=0.55; const done=OBJECTIVES.filter(o=>doneObjs.includes(o.id)).length; - txt(`${done}/${OBJECTIVES.length} total completed · ${filteredObjectives.length} shown`,LW/2,82,'rgba(255,230,0,0.6)'); + txt(`${done}/${OBJECTIVES.length} total completed · ${filteredObjectives.length} shown · ⬡ EXP ${fmtExpShort(levelXp)} ◆ ${totalGems}`,LW/2,82,'rgba(255,230,0,0.6)'); ctx.globalAlpha=1; // Filter dropdown @@ -3238,12 +3621,27 @@ txt(obj.desc,bx+34,y+32,done?'rgba(57,255,20,0.7)':'rgba(0,245,255,0.6)','left'); // Reward - setFont(9,'700'); glow(CFG.YL,done?6:4); ctx.globalAlpha=done?0.9:0.6; - txt(`+${obj.reward} ◆`,bx+bw-12,y+26,CFG.YL,'right'); + const qXp=(obj.reward>=1200)?1000:(obj.reward>=800)?500:(obj.reward>=450)?200:(obj.reward>=220)?100:50; + setFont(9,'700'); glow(CFG.YL,done?6:4); ctx.globalAlpha=done?0.9:0.7; + txt(`+${obj.reward} ◆ +${qXp} ⬡`,bx+bw-16,y+24,CFG.YL,'right'); + + const unlock=questUnlockPreview(obj); + if (unlock) { + const ux=bx+bw-20, uy=y+bh-14, ur=10; + ctx.save(); + const ring=unlock.rarity==='mythic'?'#ff66ff':unlock.rarity==='epic'?'#a66bff':unlock.rarity==='rare'?'#4fd8ff':'#78ff9d'; + ctx.fillStyle='rgba(8,12,18,0.95)'; circle(ux,uy,ur,'rgba(8,12,18,0.95)',true); + ctx.lineWidth=1.6; ctx.strokeStyle=unlock.owned?'#39ff14':ring; glow(unlock.owned?'#39ff14':ring,8); circle(ux,uy,ur+1,ring,false); + setFont(8,'900'); glow(unlock.col,8); txt(unlock.icon,ux,uy+3,unlock.col); + setFont(6,'700',"'Share Tech Mono',monospace"); noGlow(); txt(unlock.owned?'OWNED':unlock.kind.toUpperCase(),ux-ur-26,uy+3,unlock.owned?CFG.GR:'#bfe8ff','left'); + ctx.restore(); + setFont(7,'400',"'Share Tech Mono',monospace"); noGlow(); ctx.globalAlpha=0.55; + txt(`UNLOCK: ${unlock.name}`,bx+34,y+48,unlock.col,'left'); + } if (done) { ctx.save(); setFont(8,'400',"'Share Tech Mono',monospace"); noGlow(); ctx.globalAlpha=0.4; - txt('COMPLETED',bx+bw-12,y+42,CFG.GR,'right'); ctx.restore(); + txt('COMPLETED',bx+bw-74,y+42,CFG.GR,'right'); ctx.restore(); } ctx.restore(); }); @@ -3512,9 +3910,93 @@ ctx.restore(); ctx.restore(); } + + +function drawLevelsScreen(t){ + ctx.save(); + ctx.fillStyle='#000'; ctx.fillRect(0,0,LW,LH); + drawPerspGrid(0,0.05); drawBgSquares(); + setFont(30,'900'); glow('#ffd84d',16); txt('LEVEL TIERS',LW/2,50,'#ffd84d'); + const prog=levelProgress(); + setFont(10,'700',"'Share Tech Mono',monospace"); noGlow(); + txt(`LEVEL ${prog.lvl}/100 • XP ${Math.floor(prog.cur)}/${Math.floor(prog.need)}`,LW/2,70,'#dff6ff'); + drawLevelBadge(50,94,26,prog.lvl,'#ffd84d'); + const bx=84,by=84,bw=LW-110,bh=20; + ctx.fillStyle='rgba(20,30,40,0.92)'; ctx.fillRect(bx,by,bw,bh); + const pgw=Math.floor(bw*prog.pct); + ctx.fillStyle='rgba(255,216,77,0.9)'; ctx.fillRect(bx,by,pgw,bh); + ctx.strokeStyle='rgba(255,216,77,0.7)'; ctx.lineWidth=1.2; ctx.strokeRect(bx,by,bw,bh); + for (let i=1;i<10;i++){ const gx=bx+Math.floor(i*bw/10); ctx.strokeStyle='rgba(150,170,190,0.22)'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(gx,by); ctx.lineTo(gx,by+bh); ctx.stroke(); } + BTNS.levelXpOpen={x:26,y:80,w:52,h:30}; + drawNeonBtn(BTNS.levelXpOpen.x,BTNS.levelXpOpen.y,BTNS.levelXpOpen.w,BTNS.levelXpOpen.h,'#ffd84d',`XP ${levelXpOpen?'▴':'▾'}`,8,1,Math.sin(t*2)); + if (levelXpOpen) { + const xpRows=[ + `TOTAL XP: ${Math.floor(levelXp)}`, + `CURRENT LV XP: ${Math.floor(prog.cur)} / ${Math.floor(prog.need)}`, + 'QUEST XP RANGE: +50 to +1000', + 'RUN XP: TIME + SCORE + COMBO + MODE' + ]; + ctx.save(); + ctx.fillStyle='rgba(8,14,22,0.96)'; ctx.fillRect(26,112,260,74); + ctx.strokeStyle='rgba(255,216,77,0.72)'; ctx.lineWidth=1; ctx.strokeRect(26,112,260,74); + setFont(8,'700',"'Share Tech Mono',monospace"); noGlow(); + xpRows.forEach((r,i)=>txt(r,34,128+i*14,'#dff6ff','left')); + ctx.restore(); + } + + const tiers=[1,5,10,15,20,25,30,35,40,45,50,60,70,80,90,100]; + const gx=26,gy=110,gw=76,gh=56,gap=8; + tiers.forEach((lv,i)=>{ + const col=i%4,row=Math.floor(i/4); + const x=gx+col*(gw+gap), y=gy+row*(gh+gap); + const unlocked=currentLevel()>=lv; + const claimed=claimedLevelRewards.includes(lv); + ctx.fillStyle=claimed?'rgba(57,255,20,0.16)':(unlocked?'rgba(255,216,77,0.14)':'rgba(60,70,80,0.20)'); + ctx.fillRect(x,y,gw,gh); + ctx.strokeStyle=claimed?'#39ff14':(unlocked?'#ffd84d':'#4a5a66'); ctx.lineWidth=1.5; ctx.strokeRect(x,y,gw,gh); + setFont(12,'900'); glow(unlocked?'#ffd84d':'#4a5a66',8); txt(`LV ${lv}`,x+gw/2,y+20,unlocked?'#ffd84d':'#7f8f9b'); + setFont(8,'700',"'Share Tech Mono',monospace"); noGlow(); + txt(claimed?'CLAIMED':(unlocked?'UNLOCKED':'LOCKED'),x+gw/2,y+38,claimed?'#39ff14':'#b7d9ea'); + }); + BTNS.levelClaim={x:26,y:LH-84,w:144,h:30}; + drawNeonBtn(BTNS.levelClaim.x,BTNS.levelClaim.y,BTNS.levelClaim.w,BTNS.levelClaim.h,'#ffd84d',`CLAIM LV ${currentLevel()}`,9,1,Math.sin(t*2)); + BTNS.levelBack={x:LW-170,y:LH-84,w:144,h:30}; + drawNeonBtn(BTNS.levelBack.x,BTNS.levelBack.y,BTNS.levelBack.w,BTNS.levelBack.h,CFG.PK,'← BACK',10,1,Math.sin(t*2)); + ctx.restore(); +} + +function drawNotificationsScreen(t){ + ctx.save(); + ctx.fillStyle='#000'; ctx.fillRect(0,0,LW,LH); + drawPerspGrid(0,0.06); drawBgSquares(); + setFont(28,'900'); glow(CFG.CY,16); txt('NOTIFICATIONS',LW/2,52,CFG.CY); + setFont(9,'400',"'Share Tech Mono',monospace"); noGlow(); + txt('Status + social updates',LW/2,70,'rgba(180,230,255,0.7)'); + const listX=20,listY=92,listW=LW-40,listH=LH-150; + ctx.fillStyle='rgba(0,0,0,0.58)'; ctx.fillRect(listX,listY,listW,listH); + ctx.strokeStyle='rgba(0,245,255,0.45)'; ctx.lineWidth=1.2; ctx.strokeRect(listX,listY,listW,listH); + const rows=notifHistory.slice().reverse().slice(0,12); + if (!rows.length) { + setFont(10,'700'); noGlow(); txt('No notifications yet.',LW/2,listY+listH/2,'rgba(200,220,245,0.7)'); + } else { + for (let i=0;i=MAX_LEVEL) return {lvl,cur:xpRequiredForLevel(MAX_LEVEL),need:xpRequiredForLevel(MAX_LEVEL),pct:1}; const base=totalXpForLevel(lvl); const cur=Math.max(0,levelXp-base); const need=xpRequiredForLevel(lvl); return {lvl,cur,need,pct:clamp(cur/Math.max(1,need),0,1)}; } + +const LEVEL_SPECIAL_REWARDS={ + 1:{title:'Starter reward pack',skin:'default'},5:{title:'Rookie Glow title'},10:{skin:'arctic'},15:{aura:'static_ring'},20:{theme:'retrowave'}, + 25:{skin:'titanium'},30:{aura:'volt_core'},35:{theme:'deep_space'},40:{skin:'nebula'},45:{aura:'radiant_halo'},50:{theme:'prism',badge:'milestone'}, + 55:{skin:'cosmic'},60:{aura:'quantum_pulse'},65:{theme:'electric_sky'},70:{skin:'phantom'},75:{aura:'mythic_spark'},80:{theme:'lava_rift',badge:'elite'}, + 85:{skin:'spectrum'},90:{aura:'celestial_ring'},95:{theme:'solar_crown'},100:{skin:'ascendant',aura:'neon_crown',theme:'omega_grid',badge:'master'} +}; +function levelGemReward(lvl){ + const table={1:500,2:300,3:300,4:400,5:500,6:400,7:400,8:500,9:500,10:750,11:500,12:500,13:600,14:600,15:1000,16:600,17:700,18:700,19:700,20:1250, + 21:700,22:800,23:800,24:800,25:1500,26:800,27:900,28:900,29:900,30:1750,31:900,32:1000,33:1000,34:1000,35:2000,36:1000,37:1100,38:1100,39:1100,40:2250, + 41:1100,42:1200,43:1200,44:1200,45:2500,46:1200,47:1300,48:1300,49:1300,50:3000,51:1300,52:1400,53:1400,54:1400,55:3250,56:1400,57:1500,58:1500,59:1500,60:3500, + 61:1500,62:1600,63:1600,64:1600,65:3750,66:1600,67:1700,68:1700,69:1700,70:4000,71:1700,72:1800,73:1800,74:1800,75:4250,76:1800,77:1900,78:1900,79:1900,80:4500, + 81:2000,82:2000,83:2000,84:2100,85:4750,86:2100,87:2100,88:2200,89:2200,90:5000,91:2200,92:2300,93:2300,94:2400,95:5500,96:2400,97:2500,98:2500,99:3000,100:10000}; + return Number(table[lvl]||0); +} +function rewardLevel(lvl){ + const n=Math.max(1,Math.min(MAX_LEVEL,Number(lvl)||1)); + if (claimedLevelRewards.includes(n)) return false; + claimedLevelRewards.push(n); saveClaimedLevelRewards(claimedLevelRewards); + const gems=levelGemReward(n); if (gems>0) totalGems=addToBank(gems); + const sp=LEVEL_SPECIAL_REWARDS[n]||{}; + if (sp.skin && CHAR_CATALOG.find(c=>c.id===sp.skin) && !unlockedChars.includes(sp.skin)) { unlockChar(sp.skin); unlockedChars=loadUnlocked(); } + if (sp.aura && AURA_CATALOG.find(a=>a.id===sp.aura) && !unlockedAuras.includes(sp.aura)) { unlockGeneric(sp.aura,unlockedAuras,saveAuraUnlocks); } + if (sp.theme && THEME_CATALOG.find(t=>t.id===sp.theme) && !unlockedThemes.includes(sp.theme)) { unlockGeneric(sp.theme,unlockedThemes,saveThemeUnlocks); } + addNotif(`LEVEL ${n} REWARD CLAIMED +${gems}◆`,CFG.YL); + return true; +} +function gainXP(amount,source='run'){ + const add=Math.max(0,Math.floor(Number(amount)||0)); + if (!add) return; + const before=currentLevel(); + levelXp+=add; saveLevelXP(levelXp); + const after=currentLevel(); + if (after>before) { + addNotif(`LEVEL UP! ${before} → ${after}`,CFG.GR); + levelUpToasts.push({from:before,to:after,t:1.5,dur:1.5}); + while (levelUpToasts.length>2) levelUpToasts.shift(); + } +} + +function addStatCount(key,inc=1){ const v=Math.max(0,Number(localStorage.getItem(key)||'0')+Number(inc||0)); localStorage.setItem(key,String(v)); return v; } +function addSpentGems(n){ return addStatCount('slope_spent_gems',Math.max(0,Number(n)||0)); } +function fmtExpShort(v){ + const n=Math.max(0,Math.floor(Number(v)||0)); + if (n>=1_000_000) return `${(n/1_000_000).toFixed(1)}M`; + if (n>=1_000) return `${(n/1_000).toFixed(1)}K`; + return String(n); +} +function drawLevelBadge(x,y,size,lvl,col=CFG.CY){ + ctx.save(); ctx.translate(x,y); ctx.rotate((menuT||0)*0.45); + ctx.strokeStyle=col; ctx.lineWidth=2; glow(col,10); ctx.strokeRect(-size/2,-size/2,size,size); + ctx.fillStyle=hexToRgba(col,0.14); ctx.fillRect(-size/2,-size/2,size,size); ctx.restore(); + setFont(Math.max(9,Math.floor(size*0.38)),'900'); glow(col,8); txt(String(lvl),x,y+4,col); +} + function makeQuest(id,label,desc,reward,check){ return {id,label,desc,reward,check}; } function buildObjectivePool() { @@ -3993,6 +4582,28 @@ addId('gems200','Collect 200 Gems','In a single run',500,()=>gemsCollected>=200); addId('score5000','Score 5000','Reach 5000 points',500,()=>score>=5000); + const EXTREME_QUESTS=[ + ['ext1','Survive 90 seconds in one run',()=>gameTime>=90],['ext2','Survive 120 seconds in one run',()=>gameTime>=120],['ext3','Survive 150 seconds in one run',()=>gameTime>=150], + ['ext4','Reach Stage 5',()=>adventureStage>=4||impossibleStage>=4],['ext5','Reach Stage 6',()=>adventureStage>=5||impossibleStage>=5],['ext6','Reach Stage 7',()=>adventureStage>=6||impossibleStage>=6], + ['ext7','Reach max stage in any standard mode',()=>!canAdvanceStage()&&isStageMode()],['ext8','Score 10,000 in one run',()=>score>=10000],['ext9','Score 20,000 in one run',()=>score>=20000],['ext10','Score 30,000 in one run',()=>score>=30000], + ['ext11','Score 50,000 in one run',()=>score>=50000],['ext12','Earn x3 combo',()=>maxCombo>=3],['ext13','Earn x5 combo',()=>maxCombo>=5],['ext14','Earn x8 combo',()=>maxCombo>=8],['ext15','Earn x10 combo',()=>maxCombo>=10], + ['ext16','Collect 100 gems in one run',()=>gemsCollected>=100],['ext17','Collect 250 gems in one run',()=>gemsCollected>=250],['ext18','Collect 500 gems in one run',()=>gemsCollected>=500], + ['ext19','Collect 1,000 total gems across runs',()=>totalGems>=1000],['ext20','Collect 5,000 total gems across runs',()=>totalGems>=5000], + ['ext21','Use 3 powerups in one run',()=>Object.values(pwrCounts).reduce((a,b)=>a+b,0)>=3],['ext22','Use 5 powerups in one run',()=>Object.values(pwrCounts).reduce((a,b)=>a+b,0)>=5], + ['ext23','Use 10 powerups across runs',()=>Number(localStorage.getItem('slope_total_pwr')||'0')>=10],['ext24','Collect 15 powerups across runs',()=>Number(localStorage.getItem('slope_total_pwr')||'0')>=15],['ext25','Collect 30 powerups across runs',()=>Number(localStorage.getItem('slope_total_pwr')||'0')>=30], + ['ext26','Finish a run with 2 lives remaining',()=>gs==='OVER'&&lives>=2],['ext27','Finish a run with full lives remaining',()=>gs==='OVER'&&lives===maxLives],['ext28','Survive 45 seconds without collecting a gem',()=>gameTime>=45&&gemsCollected===0], + ['ext29','Survive 60 seconds without using a powerup',()=>gameTime>=60&&Object.values(pwrCounts).reduce((a,b)=>a+b,0)===0],['ext30','Survive 30 seconds at high speed',()=>gameTime>=30&&terrain.speed>420], + ['ext31','Reach Stage 3 in SLOPE INSTANT!',()=>gameMode==='LTM'&&getActiveLtm().id==='slope_instant'&<mStage>=2],['ext32','Reach max stage in SLOPE INSTANT!',()=>gameMode==='LTM'&&getActiveLtm().id==='slope_instant'&&!canAdvanceStage()], + ['ext33','Complete 3 daily quests in one day',()=>Number(localStorage.getItem('slope_daily_completed')||'0')>=3],['ext34','Complete 5 daily quests in one day',()=>Number(localStorage.getItem('slope_daily_completed')||'0')>=5], + ['ext35','Complete 10 daily quests total',()=>Number(localStorage.getItem('slope_daily_total')||'0')>=10],['ext36','Complete 25 daily quests total',()=>Number(localStorage.getItem('slope_daily_total')||'0')>=25],['ext37','Complete 50 daily quests total',()=>Number(localStorage.getItem('slope_daily_total')||'0')>=50], + ['ext38','Clear a challenge-unlock cosmetic mission',()=>doneObjs.includes('score5000')||doneObjs.includes('survive60')],['ext39','Unlock 5 skins',()=>unlockedChars.length>=5],['ext40','Unlock 10 skins',()=>unlockedChars.length>=10],['ext41','Unlock 5 auras',()=>unlockedAuras.length>=5],['ext42','Unlock 10 themes',()=>unlockedThemes.length>=10], + ['ext43','Spend 25,000 gems',()=>Number(localStorage.getItem('slope_spent_gems')||'0')>=25000],['ext44','Spend 100,000 gems',()=>Number(localStorage.getItem('slope_spent_gems')||'0')>=100000], + ['ext45','Get a personal best score',()=>score>=bestScore],['ext46','Beat your personal best twice',()=>Number(localStorage.getItem('slope_pb_beats')||'0')>=2], + ['ext47','Survive in Impossible Mode for 45 seconds',()=>gameMode==='IMPOSSIBLE'&&gameTime>=45],['ext48','Survive in Impossible Mode for 75 seconds',()=>gameMode==='IMPOSSIBLE'&&gameTime>=75],['ext49','Reach x5 combo in Impossible Mode',()=>gameMode==='IMPOSSIBLE'&&maxCombo>=5], + ['ext50','Complete a full seasonal milestone chain',()=>currentLevel()>=100] + ]; + EXTREME_QUESTS.forEach((q,i)=>addId(q[0],`EXTREME ${i+1}: ${q[1]}`,q[1],2200+i*45,q[2])); + for (let t=10;t<=400;t+=5) add(`Collect ${t} Gems`,`In a single run`,80+Math.floor(t*1.8),()=>gemsCollected>=t); for (let t=500;t<=25000;t+=250) add(`Score ${t}`,`Reach ${t} points`,90+Math.floor(t/40),()=>score>=t); for (let t=20;t<=500;t+=10) add(`Survive ${t}s`,`Stay alive ${t} seconds`,100+Math.floor(t*3.2),()=>gameTime>=t); @@ -4067,6 +4678,8 @@ if (obj.check()) { doneObjs.push(obj.id); saveDoneObjs(doneObjs); totalGems=addToBank(obj.reward); + const qXp=(obj.reward>=1200)?1000:(obj.reward>=800)?500:(obj.reward>=450)?200:(obj.reward>=220)?100:50; + gainXP(qXp,'quest'); objAnims.push({id:obj.id,t:3.5,label:obj.label,reward:obj.reward,col:CFG.YL}); addNotif(`QUEST: ${obj.label} +${obj.reward}◆`,CFG.YL); CHAR_CATALOG.forEach(c=>{ @@ -4147,6 +4760,12 @@ {id:'legend', name:'LEGEND', col:'#ffe600', trailCol:'#ff7700', unlock:'obj', objId:'score5000',objLabel:'Score 5000', icon:'●'}, {id:'cyber', name:'CYBER', col:'#3cf1ff', trailCol:'#1ac8df', unlock:'buy', cost:3600, icon:'●'}, {id:'mint', name:'MINT', col:'#53ffcf', trailCol:'#2acea3', unlock:'buy', cost:3700, icon:'●'}, + {id:'onyx', name:'ONYX', col:'#111722', trailCol:'#2d4058', unlock:'buy', cost:3800, icon:'●'}, + {id:'plasma', name:'PLASMA', col:'#b468ff', trailCol:'#7f44dd', unlock:'buy', cost:3900, icon:'●'}, + {id:'volt', name:'VOLT', col:'#e4ff57', trailCol:'#b3cc33', unlock:'buy', cost:3950, icon:'●'}, + {id:'nebula', name:'NEBULA', col:'#8e7bff', trailCol:'#5a46d6', unlock:'buy', cost:4100, icon:'●'}, + {id:'blaze', name:'BLAZE', col:'#ff6f3c', trailCol:'#d9481a', unlock:'buy', cost:4200, icon:'●'}, + {id:'tidal', name:'TIDAL', col:'#54d9ff', trailCol:'#28a9d6', unlock:'buy', cost:4200, icon:'●'}, ]; const AURA_CATALOG=[ @@ -4158,6 +4777,12 @@ {id:'mythic',name:'MYTHIC',col:'#cc88ff',unlock:'obj',objId:'survive60',objLabel:'Survive 60s',icon:'✶'}, {id:'glacier',name:'GLACIER',col:'#9fe7ff',unlock:'buy',cost:2800,icon:'❄'}, {id:'storm',name:'STORM',col:'#9dbeff',unlock:'buy',cost:3000,icon:'✷'}, + {id:'halo',name:'HALO',col:'#66f5ff',unlock:'buy',cost:3200,icon:'◍'}, + {id:'hex',name:'HEX',col:'#9cff66',unlock:'buy',cost:3250,icon:'⬡'}, + {id:'royal',name:'ROYAL',col:'#d69bff',unlock:'buy',cost:3300,icon:'✶'}, + {id:'rift',name:'RIFT',col:'#7af0ff',unlock:'buy',cost:3400,icon:'◈'}, + {id:'flare',name:'FLARE',col:'#ffb85a',unlock:'buy',cost:3500,icon:'✧'}, + {id:'venom',name:'VENOM',col:'#7cff6e',unlock:'buy',cost:3500,icon:'☣'}, ]; const THEME_CATALOG=[ {id:'classic',name:'CLASSIC',col:'#00f5ff',unlock:'free',icon:'▦'}, @@ -4183,18 +4808,73 @@ {id:'frost',name:'FROST',col:'#ccf5ff',unlock:'buy',cost:3600,icon:'❄'}, {id:'aurora',name:'AURORA',col:'#66ffd5',unlock:'buy',cost:3700,icon:'✺'}, {id:'emberglow',name:'EMBERGLOW',col:'#ff9955',unlock:'buy',cost:3700,icon:'✷'}, + {id:'matrix',name:'MATRIX',col:'#5fff9a',unlock:'buy',cost:3800,icon:'▣'}, + {id:'celestial',name:'CELESTIAL',col:'#9fd6ff',unlock:'buy',cost:3900,icon:'✺'}, + {id:'royale',name:'ROYALE',col:'#c4a0ff',unlock:'buy',cost:4000,icon:'♕'}, + {id:'eclipse',name:'ECLIPSE',col:'#8f9bff',unlock:'buy',cost:4100,icon:'◑'}, + {id:'neoncity',name:'NEON CITY',col:'#5df9ff',unlock:'buy',cost:4200,icon:'⌬'}, + {id:'verdant',name:'VERDANT',col:'#7dff9e',unlock:'buy',cost:4200,icon:'✿'}, +]; + +const BUNDLE_CATALOG=[ + {id:'starter_pack',name:'STARTER PACK',icon:'📦',col:'#66d9ff',cost:5200,items:[{kind:'skins',id:'crimson'},{kind:'auras',id:'pulse'},{kind:'themes',id:'night'}],gems:500}, + {id:'neon_rush',name:'NEON RUSH',icon:'⚡',col:'#ffd84d',cost:8800,items:[{kind:'skins',id:'solar'},{kind:'auras',id:'nova'},{kind:'themes',id:'dawn'}],gems:900}, + {id:'mythic_core',name:'MYTHIC CORE',icon:'✶',col:'#d58bff',cost:13200,items:[{kind:'skins',id:'nebula'},{kind:'auras',id:'mythic'},{kind:'themes',id:'prism'}],gems:1400}, ]; const SHOP_PAGES=[ - {title:'SKINS SHOP', key:'skins', list:CHAR_CATALOG}, - {title:'AURAS SHOP', key:'auras', list:AURA_CATALOG}, - {title:'THEMES SHOP', key:'themes', list:THEME_CATALOG}, + {title:'FEATURED SHOP', key:'featured', list:[...CHAR_CATALOG.slice(0,8),...AURA_CATALOG.slice(0,5),...THEME_CATALOG.slice(0,5)].map(v=>({...v,_kind:CHAR_CATALOG.includes(v)?'skins':(AURA_CATALOG.includes(v)?'auras':'themes')}))}, + {title:'SKINS SHOP', key:'skins', list:CHAR_CATALOG.map(v=>({...v,_kind:'skins'}))}, + {title:'AURAS SHOP', key:'auras', list:AURA_CATALOG.map(v=>({...v,_kind:'auras'}))}, + {title:'THEMES SHOP', key:'themes', list:THEME_CATALOG.map(v=>({...v,_kind:'themes'}))}, + {title:'BUNDLES SHOP', key:'bundles', list:BUNDLE_CATALOG.map(v=>({...v,_kind:'bundle'}))}, + {title:'BOOSTS SHOP', key:'boosts', list:[...CHAR_CATALOG.slice(20),...AURA_CATALOG.slice(10),...THEME_CATALOG.slice(10)].map(v=>({...v,_kind:CHAR_CATALOG.includes(v)?'skins':(AURA_CATALOG.includes(v)?'auras':'themes')}))}, ]; -let charShopPage=0; -const SHOP_ITEMS_PER_PAGE=6; +const CLOUD_SHOP_CONFIG='SLOPE/config/shopSections'; +let shopConfigFetchAt=0; +function shopEntryByKind(kind,id){ + if (kind==='skins') { const row=CHAR_CATALOG.find(v=>v.id===id); return row?{...row,_kind:'skins'}:null; } + if (kind==='auras') { const row=AURA_CATALOG.find(v=>v.id===id); return row?{...row,_kind:'auras'}:null; } + if (kind==='themes') { const row=THEME_CATALOG.find(v=>v.id===id); return row?{...row,_kind:'themes'}:null; } + return null; +} +function defaultShopSectionPayload(){ + const out={}; + SHOP_PAGES.forEach(p=>{ out[p.key]=p.list.map(v=>({kind:v._kind||p.key,id:v.id})); }); + return out; +} +function applyShopSectionsConfig(val){ + if (!val || typeof val!=='object') return; + SHOP_PAGES.forEach(page=>{ + const rows=Array.isArray(val[page.key])?val[page.key]:null; + if (!rows||!rows.length) return; + const mapped=rows.map(r=>shopEntryByKind(String((r&&r.kind)||''),String((r&&r.id)||''))).filter(Boolean); + if (mapped.length>=3) page.list=mapped; + }); +} +async function loadGlobalShopConfig(force=false){ + if (!fbReady || !fbDb) return; + const now=Date.now(); + if (!force && now-shopConfigFetchAt<15000) return; + shopConfigFetchAt=now; + try { + const snap=await fbDb.ref(CLOUD_SHOP_CONFIG).get(); + const val=(snap&&snap.val)?(snap.val()||{}):{}; + applyShopSectionsConfig(val); + } catch(e) {} +} + +let charShopCategoryPage=0; +let charShopInvPage=0; +let charShopView='shop'; +let charShopInvFilter='default'; +let charShopFilterOpen=false; +let charShopActiveTab='featured'; +const SHOP_ITEMS_PER_PAGE=12; const charBtnRects=[]; let charShopMsg='', charShopMsgT=0; +let bundlePopup={active:false,item:null}; function loadAuraUnlocks(){ try{return JSON.parse(localStorage.getItem('slope_auras')||'["none"]');}catch{return['none'];} } function saveAuraUnlocks(arr){ localStorage.setItem('slope_auras',JSON.stringify(arr)); if (typeof queueCloudSync==='function') queueCloudSync(); } @@ -4208,12 +4888,22 @@ function setSelectedTheme(id){ selectedTheme=id; localStorage.setItem('slope_selectedTheme',id); if (typeof queueCloudSync==='function') queueCloudSync(); } function unlockGeneric(id, store, saver){ if (!store.includes(id)){ store.push(id); saver(store); } } +function grantBundleItem(entry){ + if (!entry) return; + if (entry.kind==='skins') { unlockChar(entry.id); unlockedChars=loadUnlocked(); } + else if (entry.kind==='auras') unlockGeneric(entry.id,unlockedAuras,saveAuraUnlocks); + else if (entry.kind==='themes') unlockGeneric(entry.id,unlockedThemes,saveThemeUnlocks); +} +function bundleOwned(bundle){ + if (!bundle || !Array.isArray(bundle.items)) return false; + return bundle.items.every(it=> (it.kind==='skins'&&unlockedChars.includes(it.id)) || (it.kind==='auras'&&unlockedAuras.includes(it.id)) || (it.kind==='themes'&&unlockedThemes.includes(it.id)) ); +} function handleShopCategoryItem(item, pageKey){ if (pageKey==='skins') { if (unlockedChars.includes(item.id)) { setSelectedChar(item.id); charShopMsg=`${item.name} selected!`; charShopMsgT=2; return; } if (item.unlock==='buy') { - if (totalGems>=item.cost) { totalGems-=item.cost; saveBank(totalGems); unlockChar(item.id); unlockedChars=loadUnlocked(); setSelectedChar(item.id); charShopMsg=`${item.name} unlocked!`; charShopMsgT=2.5; SFX.pwr(); } + if (totalGems>=item.cost) { totalGems-=item.cost; addSpentGems(item.cost); saveBank(totalGems); unlockChar(item.id); unlockedChars=loadUnlocked(); setSelectedChar(item.id); charShopMsg=`${item.name} unlocked!`; charShopMsgT=2.5; SFX.pwr(); } else { charShopMsg=`Need ${item.cost}◆ gems!`; charShopMsgT=1.5; SFX.hit(); } } else { charShopMsg=`Complete: ${item.objLabel}`; charShopMsgT=2; } return; @@ -4221,7 +4911,7 @@ if (pageKey==='auras') { if (unlockedAuras.includes(item.id)) { setSelectedAura(item.id); charShopMsg=`${item.name} aura selected!`; charShopMsgT=2; return; } if (item.unlock==='buy') { - if (totalGems>=item.cost) { totalGems-=item.cost; saveBank(totalGems); unlockGeneric(item.id,unlockedAuras,saveAuraUnlocks); setSelectedAura(item.id); charShopMsg=`${item.name} aura unlocked!`; charShopMsgT=2.5; SFX.pwr(); } + if (totalGems>=item.cost) { totalGems-=item.cost; addSpentGems(item.cost); saveBank(totalGems); unlockGeneric(item.id,unlockedAuras,saveAuraUnlocks); setSelectedAura(item.id); charShopMsg=`${item.name} aura unlocked!`; charShopMsgT=2.5; SFX.pwr(); } else { charShopMsg=`Need ${item.cost}◆ gems!`; charShopMsgT=1.5; SFX.hit(); } } else { charShopMsg=`Complete: ${item.objLabel}`; charShopMsgT=2; } return; @@ -4229,88 +4919,325 @@ if (pageKey==='themes') { if (unlockedThemes.includes(item.id)) { setSelectedTheme(item.id); charShopMsg=`${item.name} theme selected!`; charShopMsgT=2; return; } if (item.unlock==='buy') { - if (totalGems>=item.cost) { totalGems-=item.cost; saveBank(totalGems); unlockGeneric(item.id,unlockedThemes,saveThemeUnlocks); setSelectedTheme(item.id); charShopMsg=`${item.name} theme unlocked!`; charShopMsgT=2.5; SFX.pwr(); } + if (totalGems>=item.cost) { totalGems-=item.cost; addSpentGems(item.cost); saveBank(totalGems); unlockGeneric(item.id,unlockedThemes,saveThemeUnlocks); setSelectedTheme(item.id); charShopMsg=`${item.name} theme unlocked!`; charShopMsgT=2.5; SFX.pwr(); } else { charShopMsg=`Need ${item.cost}◆ gems!`; charShopMsgT=1.5; SFX.hit(); } } else { charShopMsg=`Complete: ${item.objLabel}`; charShopMsgT=2; } + return; + } + if (pageKey==='bundle') { + bundlePopup.active=true; + bundlePopup.item=item; + return; } } +function inventoryPoolByFilter(){ + const skins=CHAR_CATALOG.filter(i=>unlockedChars.includes(i.id)); + const auras=AURA_CATALOG.filter(i=>unlockedAuras.includes(i.id)); + const themes=THEME_CATALOG.filter(i=>unlockedThemes.includes(i.id)); + if (charShopInvFilter==='skins') return skins.map(i=>({...i,_kind:'skins'})); + if (charShopInvFilter==='auras') return auras.map(i=>({...i,_kind:'auras'})); + if (charShopInvFilter==='themes') return themes.map(i=>({...i,_kind:'themes'})); + return [ + ...skins.map(i=>({...i,_kind:'skins'})), + ...auras.map(i=>({...i,_kind:'auras'})), + ...themes.map(i=>({...i,_kind:'themes'})), + ]; +} + function handleCharShopTap(p) { - if (hitBtn(p,BTNS.charBack)) { SFX.click(); doTransition('MENU'); return; } - if (hitBtn(p,BTNS.charPrev)) { SFX.click(); charShopPage=Math.max(0,charShopPage-1); return; } - if (hitBtn(p,BTNS.charNext)) { SFX.click(); charShopPage=Math.min(SHOP_PAGES.length-1,charShopPage+1); return; } - const page=SHOP_PAGES[charShopPage]||SHOP_PAGES[0]; - charBtnRects.forEach((r,i)=>{ - const item=page.list[i]; - if (!item||!hitBtn(p,r)) return; - handleShopCategoryItem(item,page.key); + if (bundlePopup.active) { + if (hitBtn(p,BTNS.bundlePopupClose)) { SFX.click(); bundlePopup.active=false; return; } + if (hitBtn(p,BTNS.bundlePopupBuy)) { + SFX.click(); + const b=bundlePopup.item; + if (!b) { bundlePopup.active=false; return; } + const owned=bundleOwned(b); + if (!owned) { + if (totalGems<(b.cost||0)) { charShopMsg=`Need ${b.cost}◆ gems!`; charShopMsgT=1.5; SFX.hit(); return; } + totalGems-=Math.max(0,Number(b.cost)||0); addSpentGems(Math.max(0,Number(b.cost)||0)); saveBank(totalGems); + (b.items||[]).forEach(grantBundleItem); + if (Number(b.gems||0)>0) totalGems=addToBank(Math.floor(Number(b.gems)||0)); + charShopMsg=`${b.name} bundle purchased!`; charShopMsgT=2.2; + } else { + const first=(b.items||[])[0]||null; + if (first&&first.kind==='skins') setSelectedChar(first.id); + if (first&&first.kind==='auras') setSelectedAura(first.id); + if (first&&first.kind==='themes') setSelectedTheme(first.id); + charShopMsg=`${b.name} equipped!`; charShopMsgT=2; + } + bundlePopup.active=false; + SFX.pwr(); + return; + } + return; + } + if (hitBtn(p,BTNS.charBack)) { SFX.click(); doTransition(charShopReturnState||'MENU'); charShopReturnState='MENU'; charShopView='shop'; return; } + if (charShopView==='inventory') { + if (hitBtn(p,BTNS.charInvFilter)) { SFX.click(); charShopFilterOpen=!charShopFilterOpen; return; } + const opts=[['default',BTNS.charInvOptDefault],['skins',BTNS.charInvOptSkins],['auras',BTNS.charInvOptAuras],['themes',BTNS.charInvOptThemes]]; + for (const [k,b] of opts) { + if (hitBtn(p,b)) { SFX.click(); charShopInvFilter=k; charShopInvPage=0; charShopFilterOpen=false; return; } + } + if (charShopFilterOpen) { charShopFilterOpen=false; } + const pool=inventoryPoolByFilter(); + const totalPages=Math.max(1,Math.ceil(pool.length/SHOP_ITEMS_PER_PAGE)); + if (hitBtn(p,BTNS.charPrev)) { SFX.click(); charShopInvPage=Math.max(0,charShopInvPage-1); return; } + if (hitBtn(p,BTNS.charNext)) { SFX.click(); charShopInvPage=Math.min(totalPages-1,charShopInvPage+1); return; } + const base=charShopInvPage*SHOP_ITEMS_PER_PAGE; + charBtnRects.forEach((r,i)=>{ + const item=pool[base+i]; + if (!item || !hitBtn(p,r)) return; + if (item._kind==='skins') { setSelectedChar(item.id); charShopMsg=`${item.name} selected!`; } + else if (item._kind==='auras') { setSelectedAura(item.id); charShopMsg=`${item.name} aura selected!`; } + else { setSelectedTheme(item.id); charShopMsg=`${item.name} theme selected!`; } + charShopMsgT=2; + SFX.pwr(); + }); + return; + } + + const tabs=Array.isArray(BTNS.charShopTabButtons)?BTNS.charShopTabButtons:[]; + for (const tabBtn of tabs) { + if (hitBtn(p,tabBtn)) { + SFX.click(); + charShopActiveTab=tabBtn.tab; + return; + } + } + + const page=SHOP_PAGES.find(v=>v.key===charShopActiveTab)||SHOP_PAGES[0]; + const pageKey=page.key; + charBtnRects.forEach((r)=>{ + if (!hitBtn(p,r)) return; + const item=r._item; + if (!item) return; + handleShopCategoryItem(item,item._kind||pageKey); }); } +function fmtCountdown(ms){ + let v=Math.max(0,Math.floor(ms/1000)); + const h=Math.floor(v/3600); v-=h*3600; + const m=Math.floor(v/60); const sec=v-m*60; + return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}`; +} +function shopCountdownHour(){ const now=new Date(); const next=new Date(now); next.setMinutes(60,0,0); return fmtCountdown(next-now); } +function shopCountdownDaily(){ + const now=new Date(); + const next=new Date(now.getFullYear(),now.getMonth(),now.getDate(),20,0,0,0); + if (next<=now) next.setDate(next.getDate()+1); + return fmtCountdown(next-now); +} + function drawCharShop(t) { ctx.fillStyle='#000'; ctx.fillRect(0,0,LW,LH); drawPerspGrid(0,0.08); drawBgSquares(); ctx.save(); ctx.fillStyle='#000'; ctx.fillRect(0,0,LW,LH); - const page=SHOP_PAGES[charShopPage]||SHOP_PAGES[0]; - setFont(28,'900'); glow(CFG.YL,18); txt(page.title,LW/2,52,CFG.YL); - setFont(11,'700'); glow(CFG.CY,10); txt(`◆ ${totalGems} GEMS`,LW/2,70,CFG.CY); + let pageKey='skins'; + let pageTitle='INVENTORY'; + let list=[]; + let pageIndex=0; + let totalPages=1; + const isInv=charShopView==='inventory'; + if (isInv) { + list=inventoryPoolByFilter(); + totalPages=Math.max(1,Math.ceil(list.length/SHOP_ITEMS_PER_PAGE)); + charShopInvPage=Math.min(charShopInvPage,totalPages-1); + pageIndex=charShopInvPage; + pageTitle=charShopInvFilter==='default'?'INVENTORY':`${charShopInvFilter.toUpperCase()} INVENTORY`; + } else { + const page=SHOP_PAGES.find(v=>v.key===charShopActiveTab)||SHOP_PAGES[0]; + pageKey=page.key; + pageTitle=page.title; + list=page.list; + } + + setFont(26,'900'); glow(CFG.YL,18); txt(pageTitle,LW/2,62,CFG.YL); + setFont(11,'700'); glow(CFG.CY,10); txt(`◆ ${totalGems} GEMS`,LW/2,80,CFG.CY); + + BTNS.charShopTabButtons=[]; + BTNS.rightSkinCategory=null; + if (!isInv) { + const tabs=[ + {k:'featured',lbl:'FEAT',col:CFG.CY,icon:'◉'}, + {k:'auras',lbl:'AURA',col:'#57ff9a',icon:'◍'}, + {k:'themes',lbl:'THEM',col:'#c78bff',icon:'▦'}, + {k:'bundles',lbl:'BUND',col:'#ff9b4d',icon:'📦'}, + {k:'boosts',lbl:'BST',col:'#ffe85c',icon:'⚡'}, + ]; + let tx=12,ty=10; + tabs.forEach(tab=>{ + const tw=56; + if (tx+tw>LW-62) return; + const active=charShopActiveTab===tab.k; + const col=active?tab.col:'#3f5668'; + const b={x:tx,y:ty,w:tw,h:20,tab:tab.k}; + BTNS.charShopTabButtons.push(b); + drawNeonBtn(tx,ty,tw,20,col,`${tab.icon} ${tab.lbl}`,7,0.9,active?Math.sin(t*2.4):0); + tx+=tw+3; + }); + + const sx=LW-48, sy=10, sw=38, sh=64; + const skinsOn=charShopActiveTab==='skins'; + BTNS.rightSkinCategory={x:sx,y:sy,w:sw,h:sh,tab:'skins'}; + BTNS.charShopTabButtons.push(BTNS.rightSkinCategory); + ctx.save(); + ctx.fillStyle=skinsOn?'rgba(90,165,255,0.18)':'rgba(20,30,40,0.86)'; ctx.fillRect(sx,sy,sw,sh); + ctx.strokeStyle=skinsOn?'#5aa5ff':'#4a5a67'; ctx.lineWidth=1.4; glow(skinsOn?'#5aa5ff':'#4a5a67',8); ctx.strokeRect(sx,sy,sw,sh); + setFont(14,'900'); glow('#5aa5ff',8); txt('◉',sx+sw/2,sy+22,'#5aa5ff'); + setFont(7,'900',"'Share Tech Mono',monospace"); noGlow(); txt('SKINS',sx+sw/2,sy+40,skinsOn?'#bfe8ff':'#8fa3b8'); + setFont(6,'700',"'Share Tech Mono',monospace"); noGlow(); txt('ICON',sx+sw/2,sy+52,'#7f95aa'); + ctx.restore(); + + setFont(8,'700',"'Share Tech Mono',monospace"); noGlow(); + txt(`TODAY'S SPOTLIGHT • ${shopCountdownHour()}`,16,104,'#9feeff','left'); + txt(`ROTATES • ${shopCountdownDaily()}`,LW-56,104,'#9feeff','right'); + setFont(8,'700',"'Share Tech Mono',monospace"); noGlow(); + txt('SPOTLIGHT',16,240,'rgba(159,238,255,0.8)','left'); + txt('CATALOG',LW-56,240,'rgba(159,238,255,0.8)','right'); + } ctx.save(); ctx.strokeStyle='rgba(255,230,0,0.3)'; ctx.lineWidth=1; - ctx.beginPath(); ctx.moveTo(40,80); ctx.lineTo(LW-40,80); ctx.stroke(); ctx.restore(); + ctx.beginPath(); ctx.moveTo(40,90); ctx.lineTo(LW-40,90); ctx.stroke(); ctx.restore(); charBtnRects.length=0; - const cols=3,cW=120,cH=108,gapX=7,gapY=7; - const gridW=cols*(cW+gapX)-gapX,gridX=(LW-gridW)/2; - for (let i=0;i({...v,_kind:(v._kind||pageKey)})); + const catalog=list.slice(3,15).map(v=>({...v,_kind:(v._kind||pageKey)})); + const topW=3*(cW+gapX)-gapX; + const topX=((LW-58)-topW)/2; + spotlight.forEach((item,i)=>{ + const bx=topX+i*(cW+gapX),by=118; + charBtnRects.push({x:bx,y:by,w:cW,h:cH,_item:item}); + }); + const cols=3; + const gridW=cols*(cW+gapX)-gapX; + const gridX=((LW-58)-gridW)/2; + catalog.forEach((item,i)=>{ + const col=i%cols,row=Math.floor(i/cols); + const bx=gridX+col*(cW+gapX),by=252+row*(cH+gapY); + charBtnRects.push({x:bx,y:by,w:cW,h:cH,_item:item}); + }); + } else { + const cols=3; + const gridW=cols*(cW+gapX)-gapX; + const gridX=(LW-gridW)/2; + const base=pageIndex*SHOP_ITEMS_PER_PAGE; + for (let i=0;i=item.cost?hexToRgba(CFG.YL,0.12):'rgba(255,0,0,0.08)'; - ctx.fillRect(bx+10,by+cH-20,cW-20,15); + ctx.fillRect(bx+10,by+r.h-20,r.w-20,15); const cc=totalGems>=item.cost?CFG.YL:'rgba(255,100,100,0.8)'; - setFont(8,'700'); noGlow(); txt(`◆${item.cost}`,bx+cW/2,by+cH-8,cc); - } else { - ctx.fillStyle='rgba(0,245,255,0.08)'; ctx.fillRect(bx+10,by+cH-20,cW-20,15); + setFont(8,'700'); noGlow(); txt(`◆${item.cost}`,bx+r.w/2,by+r.h-8,cc); + } else if (!isInv) { + ctx.fillStyle='rgba(0,245,255,0.08)'; ctx.fillRect(bx+10,by+r.h-20,r.w-20,15); setFont(7,'400',"'Share Tech Mono',monospace"); noGlow(); ctx.globalAlpha=0.6; - txt('CHALLENGE',bx+cW/2,by+cH-8,'rgba(0,245,255,0.7)'); + txt('CHALLENGE',bx+r.w/2,by+r.h-8,'rgba(0,245,255,0.7)'); } ctx.restore(); - ctx.restore(); } - const totalPages=SHOP_PAGES.length,pgY=LH-76; + if (isInv) { + const invLabel=charShopInvFilter==='default'?'INVENTORY':(charShopInvFilter==='skins'?'SKINS':(charShopInvFilter==='auras'?'AURAS':'THEMES')); + BTNS.charInvFilter={x:10,y:10,w:132,h:24}; + drawNeonBtn(10,10,132,24,CFG.CY,`${invLabel} ▾`,8,1,0); + BTNS.charInvOptDefault=BTNS.charInvOptSkins=BTNS.charInvOptAuras=BTNS.charInvOptThemes=null; + if (charShopFilterOpen) { + ctx.save(); + ctx.fillStyle='rgba(4,10,16,0.95)'; ctx.fillRect(10,34,132,88); + ctx.strokeStyle='rgba(0,245,255,0.7)'; ctx.lineWidth=1; ctx.strokeRect(10,34,132,88); + ctx.restore(); + BTNS.charInvOptDefault={x:10,y:34,w:132,h:22}; + BTNS.charInvOptSkins={x:10,y:56,w:132,h:22}; + BTNS.charInvOptAuras={x:10,y:78,w:132,h:22}; + BTNS.charInvOptThemes={x:10,y:100,w:132,h:22}; + drawNeonBtn(10,34,132,22,charShopInvFilter==='default'?CFG.CY:'#4a5a67','INVENTORY',8,1,0); + drawNeonBtn(10,56,132,22,charShopInvFilter==='skins'?CFG.YL:'#5b5130','SKINS',8,1,0); + drawNeonBtn(10,78,132,22,charShopInvFilter==='auras'?CFG.GR:'#305142','AURAS',8,1,0); + drawNeonBtn(10,100,132,22,charShopInvFilter==='themes'?CFG.PU:'#4d325b','THEMES',8,1,0); + } + } else { + charShopFilterOpen=false; + BTNS.charInvFilter=BTNS.charInvOptDefault=BTNS.charInvOptSkins=BTNS.charInvOptAuras=BTNS.charInvOptThemes=null; + } + + const pgY=LH-76; BTNS.charPrev={x:20,y:pgY,w:50,h:28}; BTNS.charNext={x:LW-70,y:pgY,w:50,h:28}; - if (charShopPage>0) drawNeonBtn(20,pgY,50,28,CFG.CY,'◀',12,0.7,0); - if (charShopPage0) drawNeonBtn(20,pgY,50,28,CFG.CY,'◀',12,0.7,0); + if (pageIndex{ + const rowY=y+62+i*20; + const icon=(it.kind==='skins'?'◉':it.kind==='auras'?'◍':'▦'); + const src=it.kind==='skins'?CHAR_CATALOG.find(v=>v.id===it.id):it.kind==='auras'?AURA_CATALOG.find(v=>v.id===it.id):THEME_CATALOG.find(v=>v.id===it.id); + const nm=src?src.name:it.id; + const col=src&&src.col?src.col:'#d8f6ff'; + setFont(9,'700'); noGlow(); txt(`${icon} ${nm}`,x+18,rowY,col,'left'); + }); + setFont(9,'700'); glow(CFG.YL,8); txt(owned?'OWNED':`COST ◆${b.cost||0}`,LW/2,y+h-64,owned?CFG.GR:CFG.YL); + BTNS.bundlePopupBuy={x:x+18,y:y+h-52,w:164,h:30}; + BTNS.bundlePopupClose={x:x+w-98,y:y+h-52,w:80,h:30}; + drawNeonBtn(BTNS.bundlePopupBuy.x,BTNS.bundlePopupBuy.y,BTNS.bundlePopupBuy.w,BTNS.bundlePopupBuy.h,owned?CFG.CY:CFG.YL,owned?(selectedChar===(b.items&&b.items[0]&&b.items[0].id)?'EQUIPPED':'EQUIP'):'PURCHASE',10,1,Math.sin(t*2)); + drawNeonBtn(BTNS.bundlePopupClose.x,BTNS.bundlePopupClose.y,BTNS.bundlePopupClose.w,BTNS.bundlePopupClose.h,CFG.PK,'CLOSE',10,1,0); + ctx.restore(); + } else { + BTNS.bundlePopupBuy=null; + BTNS.bundlePopupClose=null; + } if (charShopMsgT>0) { ctx.save(); ctx.globalAlpha=Math.min(1,charShopMsgT); @@ -4635,6 +5562,8 @@ admin:String(!!(auth&&auth.admin)), ...extra, }; + const secureSeed=`${payload.uid}|${payload.email}|${payload.updatedAt}|${payload.bestScore}|${payload.gems}|${payload.selectedSkin}|${payload.selectedAura}|${payload.selectedTheme}`; + payload.secureSig=String(hashStr(secureSeed)); return payload; } @@ -4658,13 +5587,16 @@ async function ensureSupportedVersions(){ if (!fbReady || !fbDb) return; try { - await fbDb.ref('SLOPE/config/currentVersion').get(); + await fbDb.ref('SLOPE/config/currentVersion').set(VERSION); const sv=await fbDb.ref('SLOPE/config/supportedVersions').get(); const list=(sv&&sv.val)?(sv.val()||[]):[]; - if (Array.isArray(list) && !list.includes(VERSION)) { - await fbDb.ref('SLOPE/config/supportedVersions').set([...list,VERSION].slice(-40)); - } + const merged=Array.from(new Set([...(Array.isArray(list)?list:[]), ...SUPPORTED_VERSIONS])); + await fbDb.ref('SLOPE/config/supportedVersions').set(merged.slice(-40)); + const shopSnap=await fbDb.ref(CLOUD_SHOP_CONFIG).get(); + const shopCfg=(shopSnap&&shopSnap.val)?(shopSnap.val()||null):null; + if (!shopCfg || typeof shopCfg!=='object') await fbDb.ref(CLOUD_SHOP_CONFIG).set(defaultShopSectionPayload()); await loadGlobalLtmConfig(true); + await loadGlobalShopConfig(true); } catch (e) {} } @@ -5604,6 +6536,7 @@ ['Email',auth.email||''], ['Verified',auth.verified?'YES':'NO'], ['Best Score',String(Math.floor(bestScore))], + ['EXP',`${fmtExpShort(levelXp)}`], ['Gems (Local)',`${Math.floor(loadBank())}`], ['Gems (Cloud)',`${Math.floor(cloudGemCount||0)}`], ['Cloud Path',cloudProfileRoot.replace('SLOPE/','')], @@ -5620,6 +6553,7 @@ ['Upgrade Levels',Object.values(loadUpgrades()).join(' / ')], ] : [ ['Time Played',`${totalTimeSec.toFixed(1)}s`], + ['EXP',`${fmtExpShort(levelXp)}`], ['Gems',`${Math.floor(loadBank())}`], ]; profileViewH=infoH; @@ -5839,7 +6773,7 @@ return; } if (hitBtn(p,BTNS.authBack)) { SFX.click(); doTransition('MENU'); return; } - if (hitBtn(p,BTNS.profileInfoBox)) { SFX.click(); playerCardPopup.active=true; playerCardPopup.isSelf=isSignedIn; return; } + if (hitBtn(p,BTNS.profileInfoBox)) { SFX.click(); playerCardPopup.active=true; playerCardPopup.isSelf=isSignedIn; playerCardPopup.lite=false; return; } if (hitBtn(p,BTNS.profileUidToggle)) { SFX.click(); uidVisible=!uidVisible; return; } if (hitBtn(p,BTNS.profileUidCopy)) { SFX.click(); const uid=String((auth&&auth.uid)||''); try { if (navigator.clipboard&&uid) navigator.clipboard.writeText(uid); addNotif(uid?'UID copied.':'No UID to copy.',uid?CFG.GR:CFG.PK); } catch(e) { addNotif('Copy failed.',CFG.PK); } return; } if (hitBtn(p,BTNS.profileStatusDefault)) { SFX.click(); setPlayerStatus('online'); return; } @@ -6040,6 +6974,12 @@ let impossibleStage=0, impossibleIntroT=0, impossibleIntroDone=false; let score=0, bestScore=0, lives=0, maxLives=CFG.LIVES, multi=1, gemStreak=0, maxCombo=1; let modeScores={endless:0,adventure:0,impossible:0,ltm:0}; +function currentModeBestScore(){ + if (gameMode==='ADVENTURE') return Number(modeScores.adventure||0); + if (gameMode==='IMPOSSIBLE') return Number(modeScores.impossible||0); + if (gameMode==='LTM') return Number(modeScores.ltm||0); + return Number(modeScores.endless||0); +} let gemsCollected=0; let worldSpeedMult=1; let activePwr=null, pwrTimer=0; @@ -6388,7 +7328,9 @@ const now=new Date(); const key=`${now.getUTCFullYear()}-${now.getUTCMonth()+1}-${now.getUTCDate()}`; let h=0; for (let i=0;i>>0; - return h%Math.max(1,LTM_MODES.length); + let idx=h%Math.max(1,LTM_MODES.length); + if ((LTM_MODES[idx]&<M_MODES[idx].id)==='metro') idx=(idx+1)%Math.max(1,LTM_MODES.length); + return idx; } function getDailyCountdown(){ const now=new Date(); @@ -6529,10 +7471,13 @@ return false; } -function activateImmunity(sec=immunityDuration()) { - if (gameMode==='IMPOSSIBLE') sec=Math.min(sec,1.5); +function activateImmunity(sec=immunityDuration(),source='powerup') { + const modeKey=(gameMode==='ADVENTURE'||gameMode==='LTM'||gameMode==='IMPOSSIBLE')?gameMode:'ENDLESS'; + const table=(source==='hit')?IMMUNITY_POLICY.hit:IMMUNITY_POLICY.powerup; + const policyDur=Number(table[modeKey]||IMMUNITY_COLLISION_COOLDOWN); + const finalDur=Math.max(0, source==='hit'?policyDur:Math.max(policyDur,Number(sec)||0)); immunityActive=true; - immunityTimer=Math.max(immunityTimer,sec); + immunityTimer=Math.max(immunityTimer,finalDur); player.inv=true; player.blink=true; player.blinkT=0; @@ -6739,7 +7684,6 @@ lastTime=nowPerf; touchSwipe=0; touchSwipeTarget=0; gs='PLAY'; - setTimeout(()=>{ try { SFX.resume(); } catch(e){} },0); touchSwipe=0; touchActive=false; K.left=false; K.right=false; } } @@ -6774,7 +7718,7 @@ const w=terrain.getWalls(player.y); player.x=(w.left+w.right)/2; addNotif(`-1 LIFE! (${lives}/${maxLives})`,CFG.PK); - activateImmunity(IMMUNITY_COLLISION_COOLDOWN); + activateImmunity(IMMUNITY_COLLISION_COOLDOWN,'hit'); } } @@ -6783,11 +7727,12 @@ continueOfferTimer=0; gs='OVER'; SFX.gameOver(); - if (score>bestScore) bestScore=score; + if (score>bestScore) { bestScore=score; addStatCount('slope_pb_beats',1); } if (gameMode==='ENDLESS') modeScores.endless=Math.max(modeScores.endless||0,score); if (gameMode==='ADVENTURE') modeScores.adventure=Math.max(modeScores.adventure||0,score); if (gameMode==='IMPOSSIBLE') modeScores.impossible=Math.max(modeScores.impossible||0,score); if (gameMode==='LTM') modeScores.ltm=Math.max(modeScores.ltm||0,score); + gainXP(Math.floor(gameTime*1.2) + Math.floor(score/220) + Math.floor(Math.max(0,maxCombo-1)*22) + (gameMode==='IMPOSSIBLE'?90:(gameMode==='LTM'?70:40)),'run_end'); const runs=parseInt(localStorage.getItem('slope_runs')||'0',10)+1; const total=parseFloat(localStorage.getItem('slope_totalTime')||'0')+Math.max(0,gameTime); localStorage.setItem('slope_runs',String(runs)); @@ -6851,11 +7796,13 @@ else if (gemStreak>=5) multi=2; else multi=1; if (multi>maxCombo) maxCombo=multi; + gainXP(Math.max(0,multi-1),'combo'); const pts=gem.type.pts*scoreMultiplier()*(activePwr==='BOOST'?scoreBoostMult():1); score+=pts; scorePopT=0.3; const gm=(gemDoubleActive?Math.max(2,gemBoostMult):1); const earned=gem.type.gemVal*gm; totalGems=addToBank(earned); + gainXP(2 + Math.floor(earned*0.8),'gems'); const tier=GEM_TYPES.indexOf(gem.type); SFX.gem(tier); burst(gem.x,gem.y,gem.type.col,12,180); @@ -6866,12 +7813,14 @@ function collectPwr(pwr) { pwr.collected=true; pwr.active=false; + addStatCount('slope_total_pwr',1); const col=pwr.info.col; if (pwr.key==='JETPACK') { SFX.jetpack(); player.activateJetpack(10.0); worldSpeedMult=1.3; jetpackCount++; pwrCounts.JETPACK++; } else if (pwr.key==='SHIELD') { SFX.pwr(); activatePwr('SHIELD',shieldDuration(150)); pwrCounts.SHIELD++; addRing(pwr.x,pwr.y,CFG.BL,70,240,2.5); } else if (pwr.key==='SLOW') { SFX.slow(); activatePwr('SLOW',4.5); worldSpeedMult=0.42; pwrCounts.SLOW++; } else if (pwr.key==='BOOST') { SFX.boost(); activatePwr('BOOST',10.0); pwrCounts.BOOST++; } else if (pwr.key==='MAGNET') { SFX.magnet(); activatePwr('MAGNET',10.0); pwrCounts.MAGNET++; } + else if (pwr.key==='IMMUNITY') { SFX.pwr(); activateImmunity(PWR_TYPES.IMMUNITY.dur,'powerup'); pwrCounts.SHIELD++; addRing(pwr.x,pwr.y,CFG.GR,80,220,2.6); } else if (pwr.key==='HEALTH_BOOST') { SFX.regen(); activateHealthBoost(healthBoostDuration(30)); pwrCounts.HEALTH_BOOST++; } else if (pwr.key==='REGEN') { SFX.regen(); collectRegen(pwr.x,pwr.y); pwrCounts.REGEN++; } else if (pwr.key==='MINI_JETPACK') { SFX.jetpack(); activateMiniJetpack(); pwrCounts.MINI_JETPACK++; } @@ -6884,8 +7833,9 @@ else if (pwr.key==='BAD_LUCK') { SFX.hit(); activateBadLuck(); pwrCounts.BAD_LUCK++; } else SFX.pwr(); burst(pwr.x,pwr.y,col,24,220); ringBurst(pwr.x,pwr.y,col); + gainXP(28,'powerup'); addShake(6,0.2); flashAlpha=0.35; flashCol=col; - if (pwr.key!=='HEALTH_BOOST'&&pwr.key!=='REGEN'&&pwr.key!=='MINI_JETPACK'&&pwr.key!=='NEW_STAGE'&&pwr.key!=='X2_GEMS'&&pwr.key!=='X4_GEMS'&&pwr.key!=='SICK'&&pwr.key!=='LTM_SPECIAL'&&pwr.key!=='GEM_DROP'&&pwr.key!=='BAD_LUCK') { + if (pwr.key!=='HEALTH_BOOST'&&pwr.key!=='REGEN'&&pwr.key!=='MINI_JETPACK'&&pwr.key!=='NEW_STAGE'&&pwr.key!=='X2_GEMS'&&pwr.key!=='X4_GEMS'&&pwr.key!=='SICK'&&pwr.key!=='LTM_SPECIAL'&&pwr.key!=='GEM_DROP'&&pwr.key!=='BAD_LUCK'&&pwr.key!=='IMMUNITY') { addNotif(`${pwr.info.label} ACTIVATED!`,col); addFloat(pwr.x,pwr.y-16,pwr.info.icon+' '+pwr.info.label,col); } @@ -7171,6 +8121,7 @@ const LOAD_DURATION=2.5; function loop(ts) { + resetUiNavFrame(); // FPS limiting const fpsLimitMs=FPS_VALUES[SETTINGS.fpsLimit]>0?(1000/FPS_VALUES[SETTINGS.fpsLimit]):0; if (fpsLimitMs>0&&ts-lastTime=0.5) { fps=Math.round(fpsFrames/fpsTimer); fpsFrames=0; fpsTimer=0; } + pollGamepad(); menuT+=dt; if (gs==='PLAY' || gs==='PAUSE') touchSwipe=lerp(touchSwipe,touchSwipeTarget,clamp(dt*14,0,1)); else touchSwipe=lerp(touchSwipe,0,clamp(dt*10,0,1)); @@ -7192,6 +8144,7 @@ if (charShopMsgT>0) charShopMsgT=Math.max(0,charShopMsgT-dt); if (stageTransT>0) stageTransT=Math.max(0,stageTransT-dt); if (gameOverFlash>0) gameOverFlash=Math.max(0,gameOverFlash-dt); + updateUiNavFromGamepad(); // Transition fade in/out if (transDir===1&&transAlpha>0) transAlpha=Math.max(0,transAlpha-dt*4); @@ -7423,6 +8376,20 @@ requestAnimationFrame(loop); return; } + if (gs==='NOTIFICATIONS') { + drawNotificationsScreen(menuT); + drawTransition(); + drawTopOverlays(menuT); + requestAnimationFrame(loop); return; + } + + if (gs==='LEVELS') { + drawLevelsScreen(menuT); + drawTransition(); + drawTopOverlays(menuT); + requestAnimationFrame(loop); return; + } + // ── CONTINUE OFFER (frozen gameplay) ───────────────────────── if (gs==='CONTINUE') { ctx.save(); applyShake(); @@ -7459,7 +8426,8 @@ requestAnimationFrame(loop); return; } - // ── PAUSE ─────────────────────────────────────────────────── + +// ── PAUSE ─────────────────────────────────────────────────── if (gs==='PAUSE') { ctx.save(); applyShake(); drawPerspGrid(terrain.scrollY,terrain.speedFrac());