/* 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)]; // Petak terkunci jadi slot misteri yang hidup (mystery.js, data di content/mystery.js): petunjuk bertahap, slot tetap tidak bisa dipilih. const lockedSlots=roster.length-opened.length; // 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 p2=false,runKind=null,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)'); // Latar menu utama dan About: kolase potret petarung (CSS, tanpa gambar baru; gaya di bagian Merek menu.css). Geraknya berhenti // (.bg-still) bila dimatikan di Pengaturan, saat Pengaturan terbuka, saat tab tersembunyi, atau bila pemain meminta gerak dikurangi. let backgroundEnabled=!reducedMotion?.matches; const backgroundToggle=document.querySelector('#menu-video-toggle'); function shouldAnimate(){return backgroundEnabled && !root.hidden && !document.hidden && !game.snapshot().settingsOpen;} function syncBackground(){root.classList.toggle('bg-still',!shouldAnimate());} // Susunan kiri ke kanan: petarung pertama di tengah, berikutnya bergantian ke kanan dan kiri; sisi kiri dicerminkan agar menghadap tengah. // Layar sentuh memakai paling banyak lima (hemat memori gambar besar). function collageHtml(){ const list=Object.keys(fighters).filter(id=>fighters[id].art).slice(0,touch?5:7),row=[]; list.forEach((id,i)=>{if(i%2===1||i===0)row.push(id);else row.unshift(id);}); return '
'+row.map((id,k)=>{ const f=fighters[id],pos=k-(row.length-1)/2,c=/^#[0-9a-f]{6}$/i.test(f.color||'')?f.color:'#7fb8c8'; return `
`;}).join('')+'
'; } function attachBackground(){const art=root.querySelector('.home-art');if(art&&!art.firstChild)art.innerHTML=collageHtml();art?.setAttribute('aria-hidden','true');syncBackground();} if(backgroundToggle){backgroundToggle.checked=backgroundEnabled;backgroundToggle.onchange=e=>{backgroundEnabled=e.target.checked;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='KONOHAFIGHTING✦',versionText=()=>window.Brand?.version?'VERSI '+window.Brand.version:''; const footer=()=>''; function navHeader(title,subtitle){return `
${subtitle}

${title}

${brand}
`;} function fighterPanel(id,side){const ms=window.Mystery?.panel(id,side,lockedSlots);if(ms)return ms;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=[]; root.dataset.screen=screen;root.dataset.step=String(step);root.dataset.mode=mode; if(screen==='home'){ root.innerHTML=`
GAME PARODI${versionText()?' / '+versionText():''}${canFullscreen?``:''}

KONOHA
FIGHTING✦

Parodi tokoh publik. Satu arena.

BRO WOVSSEMUANYAPARODI · SATIRE · JANGAN DIANGGAP SERIUS
`; } else if(screen==='select'){ root.innerHTML=`
${navHeader(step===0?'SELECT YOUR FIGHTER':'SELECT YOUR RIVAL',mode==='versus'?(runKind?runKind.toUpperCase():p2?'DUA PEMAIN':'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('TENTANG GAME','KONOHA FIGHTING')}
GAME PARODI

Tujuh petarung.
Satu arena.

Konoha Fighting adalah game fighting 2D di browser dengan tokoh publik versi parodi dan plesetan nama. Semua karakter adalah karikatur untuk hiburan, bukan pernyataan tentang orang sebenarnya.

MODEVS Komputer, Arcade, Survival, Dua Pemain, dan Latihan.

MEKANIKKombo, gerakan perintah, blok, parry, lempar, jurus toko, skin, dan pencapaian.

07PETARUNG05SLOT MISTERI∞RIVALRIES
Berbasis AETHER CLASH oleh Bang Tutorial (kode MIT, aset CC BY-NC) · Typography · Rajdhani
Hanya untuk penggunaan non-komersial
`; } else if(screen==='result'&&window.ResultView){ // Layar hasil meriah (vs.js ResultView): rincian koin dari Meta.award, pemenang besar + kutipan, statistik, empat tombol. const v=window.ResultView.view(result);root.innerHTML=`
${navHeader(v.title,v.eyebrow)}${v.html}`;window.ResultView.mount(root); } 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();window.Brand?.decorate(root,screen);window.Mystery?.decorate(root,screen,hover,lockedSlots); window.Tutorial?.decorate(root,screen); // tombol TUTORIAL kecil di pojok kiri atas layar utama (tutorial.js) window.Challenge?.decorate(root,screen,{player,enemy,stage,level,mode,p2,runKind,result}); // tombol KODE TANTANGAN, SALIN KODE, catatan About (challenge.js) } 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]));window.Mystery?.preview(root,hover,lockedSlots);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.':(window.Mystery?.notice(hover,lockedSlots)||'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;if(runKind){const k=runKind;runKind=null;window.Meta?.startRun(k,player);document.querySelector('#arena').focus();return;}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(){window.Meta?.cancelRun?.();if(game.startMatch({player,enemy,stage,level,mode,vs:mode==='versus'?{p2}:false,codeRule:window.Challenge?.pending?.(mode,p2)})){if(p2){game.setP2(true);window.Meta?.event('twoplayer');window.Meta?.toast('P2: ← → jalan, ↑ lompat, N serang, M / , / . = skill 1 / 2 / ultimate');}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);}} // Opens the shop/locker/collection. If meta.js is missing (stale cached index.html) it is loaded on demand. function openMeta(tab){ const go=()=>{try{window.Meta.open(tab);}catch(e){console.error(e);alert('Menu gagal dibuka: '+e.message);}}; if(window.Meta){go();return;} if(!document.querySelector('link[href^="meta.css"]')){const l=document.createElement('link');l.rel='stylesheet';l.href='meta.css?'+Date.now();document.head.append(l);} const sc=document.createElement('script');sc.src='meta.js?'+Date.now();sc.onload=go;sc.onerror=()=>alert('meta.js tidak ditemukan. Muat ulang dengan Cmd+Shift+R atau jalankan python3 serve.py.');document.head.append(sc); } 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;p2=false;runKind=null;screen='select';step=0;hover=player;render();} if(cmd==='arcade'||cmd==='survival'){mode='versus';p2=false;runKind=cmd;screen='select';step=0;hover=player;render();} if(cmd==='2p'){mode='versus';p2=true;runKind=null;screen='select';step=0;hover=player;render();} if(cmd==='shop'||cmd==='locker'||cmd==='book')openMeta(cmd);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'){if(result.run&&window.Meta){window.Meta.startRun(result.run.kind,result.player);document.querySelector('#arena').focus();return;}({player,enemy,stage,level,mode}=result);p2=!!result.p2;start();} if(cmd==='reselect'){runKind=result?.run?result.run.kind:null;p2=!!result?.p2;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(){window.Meta?.cancelRun?.();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); })();