/** * PK10 Race Animation System * Depends on: pk10-sound.js (SoundFX) * Globals expected: raceAnimState, lastAnimatedResult, idleAnimTimer, carPositions, * raceFrameTimer, ballRollTimer, FINISH_LINE, START_LINE, trafficLightTimers, * CAR_COLORS, I18N, raceModalOpen, currentView, cachedRaceResult, pendingRaceHeader */ function setCarPosition(n,pct,dur){var c=document.getElementById('carContainer'+n);var b=document.getElementById('bar'+n);if(!c)return;c.style.transition='right '+dur+'s ease';c.style.right=pct+'%';if(b){b.style.transition='width '+dur+'s ease';b.style.width=Math.min(pct+3,92)+'%';}carPositions[n]=pct;} function setCarMoving(n,moving,sprinting){var c=document.getElementById('carContainer'+n);if(!c)return;c.classList.toggle('moving',moving);c.classList.toggle('sprinting',!!sprinting);c.classList.remove('ready','braking');} function setCarBraking(n){var c=document.getElementById('carContainer'+n);if(!c)return;c.classList.remove('sprinting','moving');c.classList.add('braking');setTimeout(function(){if(c)c.classList.remove('braking');},800);} function setRankBadge(n,rank){var badge=document.getElementById('rankBadge'+n);if(!badge)return;var c=document.getElementById('carContainer'+n);if(rank>0){var labels=['1st','2nd','3rd','4th','5th','6th','7th','8th','9th','10th'];badge.textContent=labels[rank-1]||rank;if(c)c.classList.add('show-rank');}else{badge.textContent='';if(c)c.classList.remove('show-rank');}} // ANIM: idle function startIdleAnimation(){ if(idleAnimTimer)return;raceAnimState='waiting';hidePodium(); showRaceOverlay();clearTrafficLights(); for(var i=1;i<=10;i++){ setCarPosition(i,START_LINE,0.6); setCarMoving(i,false,false); setRankBadge(i,0); } idleAnimTimer=true;} function stopIdleAnimation(){if(idleAnimTimer&&idleAnimTimer!==true){clearInterval(idleAnimTimer);}idleAnimTimer=null;if(raceFrameTimer){cancelAnimationFrame(raceFrameTimer);raceFrameTimer=null;}for(var i=1;i<=10;i++){setCarMoving(i,false,false);var c=document.getElementById('carContainer'+i);if(c)c.classList.remove('ready');}} function startReadyAnimation(){stopIdleAnimation();raceAnimState='ready';hidePodium();showRaceOverlay(); for(var i=1;i<=10;i++){setCarPosition(i,START_LINE,0.8);setCarMoving(i,true,false);setRankBadge(i,0);var c=document.getElementById('carContainer'+i);if(c)c.classList.add('ready');} SoundFX.engineIdle(); startTrafficLightSequence();} // ANIM: race function triggerFinishBurst(){var fb=document.getElementById('finishBurst');if(!fb)return;fb.classList.remove('active');void fb.offsetWidth;fb.classList.add('active');setTimeout(function(){fb.classList.remove('active');},1000);} function generateConfetti(){var box=document.getElementById('confettiBox');if(!box)return;box.innerHTML='';var colors=['#ff3333','#3399ff','#ffcc00','#33ff66','#ff66cc','#ff8800','#66ffff','#ffff33','#9b59b6','#1abc9c'];for(var i=0;i<40;i++){var c=document.createElement('div');c.className='confetti';c.style.left=Math.random()*100+'%';c.style.background=colors[i%colors.length];c.style.setProperty('--cf-dur',(2.5+Math.random()*2)+'s');c.style.setProperty('--cf-delay',(Math.random()*1.5)+'s');c.style.width=(4+Math.random()*4)+'px';c.style.height=(6+Math.random()*8)+'px';c.style.borderRadius=Math.random()>.5?'50%':'2px';box.appendChild(c);}} function startTrackShake(){var t=document.getElementById('raceArea');if(t)t.classList.add('shaking');} function stopTrackShake(){var t=document.getElementById('raceArea');if(t)t.classList.remove('shaking');} /* Direct positioning (rAF driven, no CSS transition) */ function setCarPosDirect(n,pct){var c=document.getElementById('carContainer'+n),b=document.getElementById('bar'+n);if(!c)return;c.style.transition='none';c.style.right=pct+'%';if(b){b.style.transition='none';b.style.width=Math.min(pct+3,92)+'%';}carPositions[n]=pct;} /* Race script generation (high-frequency overtaking) */ function _raceScript(rankMap,carTargets){ var sc={},i,w,c,tmp,f; var N=25,T=[],PF=[],PG=[],B=[]; for(i=0;i<=N;i++){f=i/N;T.push(f); PF.push(START_LINE+f*(FINISH_LINE-START_LINE)); PG.push(f<0.82?(1.0+Math.abs(Math.sin(f*Math.PI*6))*0.8):(1.0+(f-0.82)/0.18*6.5)); B.push(f<0.78?f*0.06:0.05+(f-0.78)/0.22*0.95); }B[0]=0;B[N]=1;PG[0]=0; var FO=[];for(w=0;w<=N;w++){var a=[];for(c=1;c<=10;c++)a.push(c);for(i=9;i>0;i--){var j=Math.floor(Math.random()*(i+1));tmp=a[i];a[i]=a[j];a[j]=tmp;}FO.push(a);} var fin=[];for(c=1;c<=10;c++)fin[rankMap[c]]=c;FO[N]=fin.slice(); var W=fin[0],S2=fin[1]; for(w=1;w1){si=Math.floor(Math.random()*2);tmp=FO[w][wi];FO[w][wi]=FO[w][si];FO[w][si]=tmp;}} if(S2)for(w=Math.floor(N*.4);w2&&Math.random()>.3){tmp=FO[w][s2i];FO[w][s2i]=FO[w][0];FO[w][0]=tmp;}} for(c=1;c<=10;c++){sc[c]=[];for(w=0;w<=N;w++){ if(!w){sc[c].push({t:0,p:START_LINE});continue;} var fk=FO[w].indexOf(c),rk=rankMap[c]!==undefined?rankMap[c]:9; var br=fk*(1-B[w])+rk*B[w]; var p=PF[w]-br*PG[w]+(Math.random()-.5)*1.5; sc[c].push({t:T[w],p:Math.max(START_LINE,Math.min(FINISH_LINE+2,p))}); }}return sc;} /* Smoothstep interpolation */ function _interp(wp,t){if(t<=wp[0].t)return wp[0].p;var L=wp.length-1;if(t>=wp[L].t)return wp[L].p;for(var i=0;i=wp[i].t&&t<=wp[i+1].t){var s=(t-wp[i].t)/(wp[i+1].t-wp[i].t);s=s*s*(3-2*s);return wp[i].p+(wp[i+1].p-wp[i].p)*s;}}return wp[L].p;} /* Core race animation (dramatic chase version) */ function animateRace(result,dur){if(!result||result.length<10)return;var rk=result.join(',');if(lastAnimatedResult===rk&&raceAnimState==='finished')return; lastAnimatedResult=rk;stopIdleAnimation();hideRaceOverlay();hidePodium();raceAnimState='racing'; dur=dur||5500; var rankMap={};for(var i=0;i<10;i++)rankMap[result[i]]=i; var carTargets={};for(var i=1;i<=10;i++){var r=rankMap[i]!==undefined?rankMap[i]:9;carTargets[i]=FINISH_LINE-r*7.5;setCarMoving(i,true,false);setRankBadge(i,0);} var script=_raceScript(rankMap,carTargets); startBallRoll(dur); var t0=performance.now(),shook=false,sprinted=false; function tick(now){if(raceAnimState!=='racing')return; var el=now-t0,t=Math.min(el/dur,1); if(!shook&&t>.10){shook=true;startTrackShake();SoundFX.engineStart();} if(!sprinted&&t>.16){sprinted=true;for(var i=1;i<=10;i++)setCarMoving(i,true,true);} for(var c=1;c<=10;c++)setCarPosDirect(c,_interp(script[c],t)); if(t>=1){for(var i=1;i<=10;i++){setCarPosDirect(i,carTargets[i]);setCarBraking(i);} SoundFX.brakeSqueal();triggerFinishBurst();SoundFX.finish(); setTimeout(function(){if(raceAnimState!=='racing')return;raceAnimState='finished';stopTrackShake();stopBallRoll();for(var i=1;i<=10;i++){setCarMoving(i,false,false);var c=document.getElementById('carContainer'+i);if(c)c.classList.remove('braking');var rank=rankMap[i]!==undefined?rankMap[i]+1:0;setRankBadge(i,rank);} var badge=document.getElementById('statusBadge');if(badge){badge.textContent=I18N.drawing;badge.style.cssText='color:var(--warn)';} if(pendingRaceHeader){updateSceneHeader(pendingRaceHeader,pendingRaceHeader.period_number||'');pendingRaceHeader=null;} setTimeout(function(){generateConfetti();showPodium(result);SoundFX.podium();},800);},500);return;} raceFrameTimer=requestAnimationFrame(tick);} raceFrameTimer=requestAnimationFrame(tick);} function resetRace(){stopIdleAnimation();hideRaceOverlay();hidePodium();stopTrackShake();stopBallRoll();raceAnimState='idle';lastAnimatedResult=null;cachedRaceResult=null;pendingRaceHeader=null;if(window._podiumTimer){clearTimeout(window._podiumTimer);window._podiumTimer=null;}var fb=document.getElementById('finishBurst');if(fb)fb.classList.remove('active');var cb=document.getElementById('confettiBox');if(cb)cb.innerHTML='';for(var i=1;i<=10;i++){var c=document.getElementById('carContainer'+i);var b=document.getElementById('bar'+i);if(c){c.style.transition='right 0.5s ease';c.style.right=START_LINE+'%';c.classList.remove('moving','sprinting','ready','show-rank','braking');}if(b){b.style.transition='width 0.5s ease';b.style.width='5%';}carPositions[i]=START_LINE;setRankBadge(i,0);}} // ===== Scene controls (traffic lights + podium + scene header) ===== function showRaceOverlay(){var ov=document.getElementById('raceOverlay');if(ov)ov.classList.add('show');} function hideRaceOverlay(){var ov=document.getElementById('raceOverlay');if(ov)ov.classList.remove('show');clearTrafficLights();} function clearTrafficLights(){trafficLightTimers.forEach(function(t){clearTimeout(t);});trafficLightTimers=[];for(var i=1;i<=5;i++){var l=document.getElementById('tl'+i);if(l)l.className='tl';}} function startTrafficLightSequence(){clearTrafficLights(); trafficLightTimers.push(setTimeout(function(){document.getElementById('tl1').className='tl red';SoundFX.trafficBeep(4);},300)); trafficLightTimers.push(setTimeout(function(){document.getElementById('tl2').className='tl red';SoundFX.trafficBeep(3);},600)); trafficLightTimers.push(setTimeout(function(){document.getElementById('tl3').className='tl yellow';SoundFX.trafficBeep(2);},900)); trafficLightTimers.push(setTimeout(function(){document.getElementById('tl4').className='tl red';SoundFX.trafficBeep(1);},1200)); trafficLightTimers.push(setTimeout(function(){document.getElementById('tl5').className='tl red';SoundFX.trafficBeep(0);},1500)); trafficLightTimers.push(setTimeout(function(){for(var i=1;i<=5;i++)document.getElementById('tl'+i).className='tl green';SoundFX.trafficBeep(0);setTimeout(clearTrafficLights,500);},2500));} function updateRaceOverlayTimer(secs){var td=document.getElementById('raceTimerDisplay');if(!td)return;var m=Math.floor(secs/60),s=secs%60;td.innerHTML=(m<10?'0':'')+m+':'+(s<10?'0':'')+s+'00';} // ===== Ball roll effect ===== function startBallRoll(dur){ stopBallRoll(); var balls=document.getElementById('raceSceneBalls'); if(!balls)return; dur=dur||5500; var speed=100; function roll(){ var arr=[];for(var i=1;i<=10;i++)arr.push(i); for(var i=arr.length-1;i>0;i--){var j=Math.floor(Math.random()*(i+1));var t=arr[i];arr[i]=arr[j];arr[j]=t;} var html=''; arr.forEach(function(v){var cc=CAR_COLORS[v];html+=''+v+'';}); balls.innerHTML=html; } roll(); ballRollTimer=setInterval(roll,speed); var slow1=Math.round(dur*0.4); var slow2=Math.round(dur*0.75); setTimeout(function(){if(ballRollTimer){clearInterval(ballRollTimer);ballRollTimer=setInterval(roll,200);}},slow1); setTimeout(function(){if(ballRollTimer){clearInterval(ballRollTimer);ballRollTimer=setInterval(roll,400);}},slow2); } function stopBallRoll(){ if(ballRollTimer){clearInterval(ballRollTimer);ballRollTimer=null;} } function updateSceneHeader(result,periodNum){ var balls=document.getElementById('raceSceneBalls');var pn=document.getElementById('raceScenePeriod'); if(pn&&periodNum){var short=periodNum.length>6?periodNum.slice(-6):periodNum;pn.textContent='\u671F\u6570\uFF1A'+short;} if(balls&&result){var html='';['rank_1','rank_2','rank_3','rank_4','rank_5','rank_6','rank_7','rank_8','rank_9','rank_10'].forEach(function(k){var v=result[k];if(v){var cc=CAR_COLORS[v];html+=''+v+'';}});balls.innerHTML=html||'---';} updateSceneFooter(result,periodNum); } function updateSceneFooter(result,periodNum){ var ftPn=document.getElementById('ftPeriodNum'); if(ftPn&&periodNum)ftPn.textContent=periodNum; if(!result)return; var r1=+result.rank_1,r2=+result.rank_2; if(r1&&r2){ var sum=r1+r2; var ftSv=document.getElementById('ftSumVal');if(ftSv)ftSv.textContent=sum; var ftSb=document.getElementById('ftSumBs');if(ftSb)ftSb.textContent=sum>=12?I18N.big:I18N.small; var ftSo=document.getElementById('ftSumOe');if(ftSo)ftSo.textContent=sum%2?I18N.odd:I18N.even; } var ftDt=document.getElementById('ftDtTags'); if(ftDt){var html='';for(var i=1;i<=5;i++){var a=+result['rank_'+i],b=+result['rank_'+(11-i)];if(a&&b){var isDragon=a>b;html+=''+(isDragon?I18N.dragon:I18N.tiger)+'';}}ftDt.innerHTML=html||'---';} } // ===== Toggle scene collapse ===== function toggleRaceScene(){var scene=document.querySelector('.race-scene');if(!scene)return;scene.classList.toggle('collapsed');localStorage.setItem('raceSceneCollapsed',scene.classList.contains('collapsed')?'1':'0');} // Init collapsed state + sound icon (function(){var btn=document.querySelector('.rs-sound');if(btn)btn.textContent=SoundFX.enabled?'🔊':'🔇';if(localStorage.getItem('raceSceneCollapsed')==='1'){var s=document.querySelector('.race-scene');if(s)s.classList.add('collapsed');}})(); // ===== Modal control ===== function openRaceModal(){ document.getElementById('raceModalMask').classList.add('show'); raceModalOpen=true; document.body.style.overflow='hidden'; } function closeRaceModal(){ document.getElementById('raceModalMask').classList.remove('show'); raceModalOpen=false; document.body.style.overflow=''; SoundFX.stopAll(); } function getPodiumCarSrc(num){return '/Static/img/cars/car'+num+'.png';} function showPodium(result){if(!result||result.length<3)return;document.getElementById('podiumImg1').src=getPodiumCarSrc(result[0]);document.getElementById('podiumImg2').src=getPodiumCarSrc(result[1]);document.getElementById('podiumImg3').src=getPodiumCarSrc(result[2]);var po=document.getElementById('podiumOverlay');if(po)po.classList.add('show');} function hidePodium(){var po=document.getElementById('podiumOverlay');if(po)po.classList.remove('show');} // ===== Video/Anim switch ===== function switchVideoOrAnim(status,changed){ currentView='anim'; var desc=document.getElementById('raceEntryDesc'); if(desc){ var map={pending:'\u7B49\u5F85\u5F00\u5956\u4E2D...',locked:'\u6B63\u5728\u5F00\u5956\uFF01',drawn:'\u5F00\u5956\u7ED3\u675F',settled:'\u5DF2\u7ED3\u7B97'}; desc.textContent=map[status]||'\u70B9\u51FB\u67E5\u770B\u5F00\u5956\u52A8\u753B'; } } function tryResumeVideo(){var v=document.querySelector('#pk10VideoPlayer video');if(v){v.muted=true;v.play().catch(function(){});}}