/* Front door: player -> rival -> arena/difficulty, with future locked roster slots. */ (() => { 'use strict'; const root=document.querySelector('#front-menu'),game=window.__game; const fighters={ ...(window.Browo&&window.BROWO_MANIFEST?{browo:{name:'BRO WO',tag:'THE PALM RANGER',race:'HUMAN',portrait:'assets/browo/ui/portrait.webp',art:'assets/menu/browo-select.webp',detail:'Steady boots. Wide hat. Whole forests, then a rampage.',basic:'TINJU SAFARI',ultimate:'HUTAN SAWIT',style:'Bruiser / palm trees and area slam',color:'#c9b87a'}}:{}), }; // Data-driven fighters (window.FIGHTERS, one entry per assets//manifest.js) join the roster automatically. for(const [id,f] of Object.entries(window.FIGHTERS||{}))fighters[id]={name:f.name,tag:f.tag,race:f.race,portrait:'assets/'+id+'/ui/portrait.webp',art:f.select,detail:f.detail,basic:f.names[0],ultimate:f.names[3],style:f.style,color:f.color}; // Showcase fighters (assets/showcase/showcase.js) fill roster slots with avatar and art; they cannot be confirmed yet. const showcase=Object.fromEntries(Object.entries(window.SHOWCASE_FIGHTERS||{}).filter(([id])=>!fighters[id])); const info=id=>fighters[id]||showcase[id],soon=id=>!!showcase[id]; const opened=[...Object.keys(fighters),...Object.keys(showcase)]; // Touch screens have no hover: a tap only previews a fighter; the yellow CONFIRM button picks it. const touch=document.documentElement.classList.contains('touch'); // Phones get a fullscreen button on the home screen (no trip to Settings). Hidden where the page cannot go fullscreen // (iPhone Safari); inside the mobile shell the shell's document is the one that goes fullscreen. const canFullscreen=touch&&(()=>{try{const d=window.parent!==window?window.parent.document:document;return !!(d.fullscreenEnabled||d.webkitFullscreenEnabled);}catch(_){return false;}})(); // Until the phone has been fullscreen once (button or the shell's first-touch fullscreen), the button glows. const hintKey='aether.fullscreenHint'; let fullscreenHint=canFullscreen&&(()=>{try{return localStorage.getItem(hintKey)!=='seen';}catch(_){return true;}})(); function dismissFullscreenHint(){if(!fullscreenHint)return;fullscreenHint=false;try{localStorage.setItem(hintKey,'seen');}catch(_){}root.querySelector('.menu-fullscreen')?.classList.remove('hinted');} if(fullscreenHint)try{const d=window.parent!==window?window.parent.document:document;d.addEventListener('fullscreenchange',()=>{if(d.fullscreenElement)dismissFullscreenHint();});}catch(_){} // Warm the small roster portraits so the grid appears at once. The large select art and arena pictures are fetched when // shown and then come from the browser cache; holding all of them decoded would waste a phone's image memory. const warm=opened.map(id=>info(id).portrait).filter(Boolean).map(src=>{const im=new Image();im.decoding='async';im.src=src;return im;}); const roster=[...opened,...Array.from({length:Math.max(0,12-opened.length)},(_,i)=>'locked-'+i)]; // lockIn: the pending step change while a confirmed fighter flashes 3 times (PICK_FLASH s each); input waits for it. const PICK_FLASH=.3;let lockIn=null,lockTicks=[]; const ids=Object.keys(fighters); let screen='home',step=0,mode='versus',player=ids[0],enemy=ids[1]||ids[0],hover=ids[0],stage='bellora',level='medium',result=null,interacted=false,lastSound=0; const reducedMotion=window.matchMedia?.('(prefers-reduced-motion: reduce)'); let backgroundEnabled=!reducedMotion?.matches,backgroundVideo=null,videoPlayToken=0; const backgroundToggle=document.querySelector('#menu-video-toggle'); function shouldAnimate(){return backgroundEnabled && screen==='home' && !root.hidden && !document.hidden && !game.snapshot().settingsOpen;} function syncBackground(){ if(!backgroundVideo)return;const token=++videoPlayToken; if(!shouldAnimate()){backgroundVideo.pause();return;} if(backgroundVideo.paused){const p=backgroundVideo.play();p?.catch(()=>{if(token===videoPlayToken)backgroundVideo.dataset.state='autoplay-blocked';});} } function attachBackground(){ if(screen!=='home')return; if(!backgroundVideo && backgroundEnabled){ backgroundVideo=document.createElement('video');backgroundVideo.id='menu-background-video';backgroundVideo.className='menu-background-video'; backgroundVideo.muted=true;backgroundVideo.defaultMuted=true;backgroundVideo.autoplay=true;backgroundVideo.loop=true;backgroundVideo.playsInline=true;backgroundVideo.preload='auto'; backgroundVideo.setAttribute('muted','');backgroundVideo.setAttribute('playsinline','');backgroundVideo.setAttribute('aria-hidden','true');backgroundVideo.tabIndex=-1;backgroundVideo.disablePictureInPicture=true; backgroundVideo.poster='assets/menu/home-factions.webp';backgroundVideo.dataset.state='loading'; backgroundVideo.addEventListener('playing',()=>{if(!shouldAnimate()){backgroundVideo.pause();return;}backgroundVideo.classList.add('is-playing');backgroundVideo.dataset.state='playing';}); backgroundVideo.addEventListener('error',()=>{backgroundVideo.classList.remove('is-playing');backgroundVideo.dataset.state='poster-fallback';}); backgroundVideo.src='assets/menu/home-factions-loop.mp4'; } if(backgroundVideo)root.querySelector('.home-art')?.prepend(backgroundVideo); syncBackground(); } if(backgroundToggle){backgroundToggle.checked=backgroundEnabled;backgroundToggle.onchange=e=>{backgroundEnabled=e.target.checked;attachBackground();syncBackground();};} document.addEventListener('visibilitychange',syncBackground); function sfx(kind='move',explicit=false){if(!interacted&&!explicit)return;interacted=true;const now=performance.now();if(kind==='move'&&now-lastSound<75)return;lastSound=now;game.menuSound(kind);} root.addEventListener('pointerdown',()=>{interacted=true;},true); const brand='AETHERCLASH✦'; const footer=()=>''; function navHeader(title,subtitle){return `
${subtitle}

${title}

${brand}
`;} function fighterPanel(id,side){const f=info(id);return `
${side==='player'?'P1 / YOUR FIGHTER':'CPU / YOUR RIVAL'}${f?.name||'Karakter belum dipilih'}
${f?.tag||'AWAITING CHALLENGER'}

${f?.name||'???'}

${f?.race||'ROSTER EXPANDING'}

${f?.detail||'A new challenger is on the way.'}
`;} function render(){ if(lockIn){clearTimeout(lockIn);lockIn=null;}for(const t of lockTicks)clearTimeout(t);lockTicks=[]; if(backgroundVideo){videoPlayToken++;backgroundVideo.pause();backgroundVideo.remove();} root.dataset.screen=screen;root.dataset.step=String(step);root.dataset.mode=mode; if(screen==='home'){ root.innerHTML=`
FIGHTING DEMO / VOL. 02${canFullscreen?``:''}

AETHER
CLASH✦

Two factions. One arena.

MECHAVSDEMI-HUMANTECHNOLOGY MEETS INSTINCT
`; } else if(screen==='select'){ root.innerHTML=`
${navHeader(step===0?'SELECT YOUR FIGHTER':'SELECT YOUR RIVAL',mode==='versus'?'VERSUS COMPUTER':'TRAINING ROOM')}
01 PLAYER02 RIVAL03 ARENA
${fighterPanel(step===0?hover:player,'player')}
${step===0?'PLAYER 1':'COMPUTER'}CHOOSE A FIGHTER
${roster.map((id,i)=>{const f=info(id);return ``;}).join('')}

${roster.length>opened.length?(roster.length-opened.length)+' challenger slots reserved.':''}

${fighterPanel(step===1?hover:null,'enemy')}
${footer()}`; updatePreview(false); } else if(screen==='arena'){ const st=MatchRules.stages[stage];root.innerHTML=`
${navHeader('SET THE STAGE',mode==='versus'?'VERSUS COMPUTER / MATCH SETUP':'TRAINING / ARENA SETUP')}
P1${fighters[player].name}
VS
CPU${fighters[enemy].name}
${String(Object.keys(MatchRules.stages).indexOf(stage)+1).padStart(2,'0')} / ${String(Object.keys(MatchRules.stages).length).padStart(2,'0')}
${st.tag}

${st.name}

✦ ${st.time||'SUNNY DAYLIGHT'}
${Object.entries(MatchRules.stages).map(([id,a])=>``).join('')}
${mode==='versus'?'CPU DIFFICULTY':'PRACTICE MODE'}${mode==='versus'?`
${Object.entries(MatchRules.difficulties).map(([id,d],i)=>``).join('')}
`:'

Unlimited time · Passive rival · Reset anytime

'}
${mode==='versus'?'FIRST TO 2 WINSBEST OF 3 ROUNDS · 90 SECONDS':'MAKE EVERY HIT COUNTCOMBO TRACKER · FULL MOVE SET'}

${footer()}`; } else if(screen==='about'){ root.innerHTML=`
${navHeader('ABOUT AETHER CLASH','MECHA VS DEMI-HUMAN')}
CHRONICLES OF AETHER

Steel meets
the untamed.

Di dunia Renaisans yang ditenagai aether, dua faksi bertemu di arena: Mecha dengan rekayasa tubuh dan mesin, serta Demi-Human dengan kekuatan manusia dan hewan. Para petarung membawa asal-usul, kemampuan, dan tujuan mereka sendiri.

MECHAPerpaduan manusia dan teknologi. Rekayasa mekanis, persenjataan aether, dan teknik bertarung yang beragam.

DEMI-HUMANPerpaduan manusia dan hewan. Setiap ras memiliki anatomi, naluri, dan kekuatan khasnya sendiri.

02FACTIONS12ROSTER SLOTS∞RIVALRIES
Visuals · Higgsfield   Voices · ElevenLabs   Typography · Rajdhani
Local fighting demo · Single player
`; } else if(screen==='result'){ const won=result.winner==='player',winner=won?result.player:result.enemy;root.innerHTML=`
${navHeader('MATCH COMPLETE',MatchRules.stages[result.stage].name.toUpperCase())}
${fighters[winner].name}
${won?'PLAYER 1':'COMPUTER'} TAKES THE MATCH

${won?'VICTORY':'DEFEAT'}

${fighters[winner].name} WINS

${result.playerWins}—${result.enemyWins}
${fighters[result.player].name} vs ${fighters[result.enemy].name} · ${MatchRules.difficulties[result.level].label}
`; } bind();attachBackground(); } function updatePreview(play=true){ root.querySelectorAll('[data-fighter]').forEach(b=>b.classList.toggle('highlight',b.dataset.fighter===hover)); const panel=root.querySelector(step===0?'#player-preview':'#enemy-preview');if(panel)panel.innerHTML=fighterPanel(hover,step===0?'player':'enemy'); const f=info(hover),moves=root.querySelector('#fighter-moves');if(moves)moves.innerHTML=f?`${f.style}${soon(hover)?'':`${f.ultimate}`}`:'NEW CHALLENGERCOMING LATER'; const confirm=root.querySelector('.confirm-fighter');if(confirm)confirm.setAttribute('aria-disabled',String(!fighters[hover]));if(play)sfx('move'); } function focusFighter(id){if(hover===id||lockIn)return;hover=id;updatePreview();} function confirm(){if(lockIn)return;if(!fighters[hover]){sfx('locked',true);root.querySelector('#roster-notice').textContent=soon(hover)?showcase[hover].name+' belum bisa dimainkan.':'Karakter ini belum terbuka.';return;} // The pick flashes 3 times (tile and big art) with a lock-in chime before the next step, so the screen does not jump. sfx('pick',true);game.announceSelection(hover);root.dataset.picking='true'; root.querySelector(`[data-fighter="${hover}"]`)?.classList.add('picked');root.querySelector(step===0?'#player-preview':'#enemy-preview')?.classList.add('picked'); lockTicks=[1,2].map(i=>setTimeout(()=>sfx('blink',true),i*PICK_FLASH*1000)); lockIn=setTimeout(()=>{lockIn=null;delete root.dataset.picking;if(step===0){player=hover;step=1;hover=ids.find(x=>x!==player)||player;}else{enemy=hover;screen='arena';step=2;}render();},3*PICK_FLASH*1000); } function back(){if(lockIn)return;game.stopAnnouncer();sfx('back',true);if(screen==='arena'){screen='select';step=1;hover=enemy;}else if(screen==='select'&&step===1){step=0;hover=player;}else screen='home';render();} function start(){if(game.startMatch({player,enemy,stage,level,mode})){document.querySelector('#arena').focus();}else {const error=root.querySelector('#setup-error');if(error)error.textContent='Aset belum siap. Tunggu sebentar lalu coba lagi.';sfx('locked',true);}} function command(cmd){ if(lockIn)return; if(cmd==='fullscreen'){sfx('move',true);dismissFullscreenHint();game.toggleFullscreen?.();return;} if(cmd==='confirm'){confirm();return;}if(cmd==='back'){back();return;}sfx('confirm',true); if(cmd==='versus'||cmd==='training'){mode=cmd;screen='select';step=0;hover=player;render();} if(cmd==='settings')game.openSettings();if(cmd==='about'){screen='about';render();} if(cmd==='home'){game.stopAnnouncer();screen='home';render();}if(cmd==='start')start(); if(cmd==='rematch'){({player,enemy,stage,level,mode}=result);start();} if(cmd==='reselect'){screen='select';step=0;hover=player;render();} } function bind(){ root.querySelectorAll('[data-cmd]').forEach(b=>{b.onclick=()=>command(b.dataset.cmd);b.onpointerenter=()=>sfx('move');b.onfocus=()=>sfx('move');}); root.querySelectorAll('[data-fighter]').forEach(b=>{b.onpointerenter=()=>focusFighter(b.dataset.fighter);b.onfocus=()=>focusFighter(b.dataset.fighter);b.onclick=()=>{focusFighter(b.dataset.fighter);if(!touch)confirm();};}); root.querySelectorAll('[data-stage]').forEach(b=>b.onclick=()=>{stage=b.dataset.stage;sfx('move',true);render();root.querySelector(`[data-stage="${stage}"]`).focus();}); root.querySelectorAll('[data-level]').forEach(b=>b.onclick=()=>{level=b.dataset.level;sfx('move',true);render();root.querySelector(`[data-level="${level}"]`).focus();}); } document.addEventListener('keydown',e=>{ if(root.hidden||document.querySelector('dialog[open]')||e.repeat)return;interacted=true; if(lockIn){e.preventDefault();return;} if(e.key==='Escape'){e.preventDefault();back();return;} if(screen==='select'&&['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','a','d','w','s'].includes(e.key)){e.preventDefault();const offset={ArrowLeft:-1,a:-1,ArrowRight:1,d:1,ArrowUp:-4,w:-4,ArrowDown:4,s:4}[e.key],i=(roster.indexOf(hover)+offset+roster.length)%roster.length;focusFighter(roster[i]);root.querySelector(`[data-fighter="${hover}"]`).focus();return;} if(screen==='select'&&(e.key==='Enter'||e.key===' ')){const cmd=e.target.closest?.('[data-cmd]')?.dataset.cmd;if(cmd&&cmd!=='confirm')return;e.preventDefault();confirm();return;} if(screen==='arena'&&['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','a','d','w','s'].includes(e.key)){ e.preventDefault();const levels=Object.keys(MatchRules.difficulties),stages=Object.keys(MatchRules.stages),vertical=['ArrowUp','ArrowDown','w','s'].includes(e.key),backward=['ArrowLeft','ArrowUp','a','w'].includes(e.key); if(vertical&&mode==='versus'){level=levels[(levels.indexOf(level)+(backward?-1:1)+levels.length)%levels.length];}else{stage=stages[(stages.indexOf(stage)+(backward?-1:1)+stages.length)%stages.length];}sfx('move');render();return; } if(screen==='arena'&&e.key==='Enter'&&!e.target.closest?.('button')){e.preventDefault();command('start');return;} if(screen==='home'&&e.key==='Enter'&&!e.target.closest?.('button')){e.preventDefault();command('versus');return;} if(screen==='home'&&['ArrowUp','ArrowDown','w','s'].includes(e.key)){e.preventDefault();const list=[...root.querySelectorAll('.home-nav button')],current=list.indexOf(document.activeElement),next=(current+(['ArrowUp','w'].includes(e.key)?-1:1)+list.length)%list.length;list[next].focus();} }); window.FrontEnd={syncBackground,home(){screen='home';game.setMenuOpen(true);render();},showResult(data){result=data;({player,enemy,stage,level,mode}=data);screen='result';game.setMenuOpen(true);render();},assetsReady(ok=true){if(game.snapshot().menuOpen){render();if(!ok){const status=root.querySelector('#menu-ready');if(status)status.textContent='ASSET ERROR · RELOAD REQUIRED';}}}}; // First paint after the loading screen's download, so menu pictures come straight from the cache. Promise.resolve(window.AETHER_PRELOAD).then(render); })();