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