diff --git a/content/quotes.js b/content/quotes.js new file mode 100644 index 0000000..ce5d367 --- /dev/null +++ b/content/quotes.js @@ -0,0 +1,42 @@ +/* Kutipan karakter (RENCANA 2.2, 2.3, 2.6): window.QUOTES[id] = { intro, win, lose }. + Dipakai layar VS (intro) dan layar hasil (win untuk pemenang, lose untuk yang kalah). Dipilih oleh vs.js dengan pengacak kosmetik. + Pedoman isi: plesetan ringan terhadap citra publik yang sudah menjadi bahan lelucon umum. Tanpa tuduhan fakta atau skandal nyata, + tanpa unsur agama, tanpa merendahkan pribadi atau keluarga. Gufron hanya meme ringan (semut dan 'syududu'). */ +window.QUOTES = window.QUOTES || {}; +Object.assign(window.QUOTES, { + browo: { + intro: ['Topi sudah rapi, joget sudah siap. Ayo mulai!', 'Santai, ini cuma safari kecil.', 'Joget dulu, pukul belakangan.', 'Hutan ini luas, arenanya kecil. Pas!'], + win: ['Gemoy itu strategi, bukan kelemahan!', 'Satu pohon sawit lagi tertanam di arena.', 'Joget kemenangan, semuanya ikut!', 'Safari hari ini sukses besar.'], + lose: ['Tidak apa-apa, besok jogetnya lebih panjang.', 'Pohonnya belum cukup tinggi. Tunggu musim depan.', 'Ini baru pemanasan, belum sempat joget.', 'Topiku jatuh, konsentrasi buyar!'] + }, + gufron: { + intro: ['Semut-semut sudah baris. Syududu!', 'Satu, dua, tiga koloni siap berangkat.', 'Jangan injak semutku, nanti dia lapor.', 'Psst... kata koloni, kamu gampang ditebak.'], + win: ['Koloni menang, remah pun dibagi. Syududu!', 'Semut itu kecil, kemenangan itu besar.', 'Terima kasih, semut-semutku. Pesta remah!', 'Bahasa semut: "tuh kan, kami bilang juga apa".'], + lose: ['Semutku istirahat dulu, besok masuk lagi.', 'Koloninya salah belok. Syududu...', 'Ada yang bawa gula? Semutku butuh tenaga.', 'Aku kalah, tapi semutnya masih bangga.'] + }, + soloman: { + intro: ['Sederhana saja. Kita lihat nanti.', 'Saya blusukan ke arena dulu, ya.', 'Sarung sudah dikibas. Silakan.', 'Esemka sudah dipanasi. Siap jalan.'], + win: ['Ya, begitu saja. Sederhana, kan?', 'Konvoi lewat, lawan minggir. Maaf, ya.', 'Kerja, kerja, kerja. Selesai.', 'Hasilnya terlihat. Kalau belum, blusukan lagi.'], + lose: ['Hmm... kita evaluasi sambil jalan.', 'Esemkanya mogok sebentar. Biasa.', 'Tidak masalah, saya turun ke lapangan lagi.', 'Kalah itu biasa, yang penting tetap sederhana.'] + }, + anice: { + intro: ['Izinkan saya membuka dengan satu narasi singkat.', 'Gagasan sudah disiapkan. Pukulan menyusul.', 'Mimbar siap, lawan siap. Mari.', 'Dengarkan baik-baik, kalimat saya ada tiga bagian.'], + win: ['Seperti kalimat yang tersusun rapi, begitu pula kemenangan ini.', 'Narasi ditutup dengan titik. Titiknya berupa pukulan.', 'Sekian pidato singkat saya. Yang panjang menyusul.', 'Gagasan yang rapi memang sulit dikalahkan.'], + lose: ['Izinkan saya menyampaikan keberatan secara tertib.', 'Narasinya terpotong. Mohon waktu tambahan.', 'Pidatonya belum selesai, waktunya sudah habis.', 'Kalah satu pukulan, menang seribu kata.'] + }, + ethanolman: { + intro: ['Hahaha! Tangki penuh, semangat penuh!', 'Tenang, semuanya sudah dihitung. Mungkin.', 'Ayo, kita campur sedikit etanol di arena ini!', 'Putih bersih, hijau terisi. Berangkat!'], + win: ['Hahaha! Bahan bakar hijau selalu menang!', 'Isi penuh, tancap gas, selesai!', 'Tawa saya lebih kencang dari pukulan ini!', 'Ini namanya swasembada tawa.'], + lose: ['Hahaha... tangkinya bocor sedikit.', 'Tidak apa-apa, tertawa dulu biar pulih.', 'Etanolnya kurang. Kita tambah di ronde depan!', 'Ini bukan kalah, ini efisiensi bahan bakar.'] + }, + sonofsolo: { + intro: ['Tatapan tenang, labu siap. Silakan.', 'Hati-hati, ini bukan air mineral.', 'Santai dulu. Saya cuma mau bertanya sesuatu.', 'Asam sulfat sudah siap. Pertanyaan jebakan juga.'], + win: ['Jawabannya: asam sulfat. Terima kasih.', 'Tenang, tenang. Cuma sedikit percikan.', 'Kenapa berteriak? Karena memang perlu.', 'Pertanyaan terjawab. Skornya sudah jelas.'], + lose: ['Oke. Saya catat dulu, nanti saya tanya balik.', 'Labunya pecah. Saya butuh labu baru.', 'Ini belum selesai. Saya masih punya satu pertanyaan.', 'Tatapan tetap tenang, hasilnya yang goyang.'] + }, + megachan: { + intro: ['Banteng sudah dilepas. Minggir!', 'Merah itu berani. Siapa mau coba?', 'Dengarkan orasi saya dulu, baru kita mulai!', 'Kepalan ini sudah menunggu dari tadi.'], + win: ['Banteng tidak pernah mundur. Catat itu!', 'Merah menyala, lawan tersapu!', 'Sudah dengar orasi saya? Sekarang lihat hasilnya.', 'Kepalan ini lebih keras dari pidato saya.'], + lose: ['Banteng istirahat sebentar. Jangan senang dulu!', 'Ini bukan akhir. Orasinya baru pemanasan.', 'Merah tidak pernah padam!', 'Kalah satu ronde itu biasa. Banteng akan kembali.'] + } +}); diff --git a/game.js b/game.js index 44eafee..aabb928 100644 --- a/game.js +++ b/game.js @@ -32,6 +32,8 @@ const Rules = window.MatchRules, Feel = window.Feel || feelFallback(); let opponentCharacter = 'fenr', difficulty = 'medium', stageId = 'bellora', menuOpen = !!window.FRONTEND_ENABLED; let match = Rules?.create('training') || null, roundAnnouncer = null; + // Layar VS (vs.js): selama vsHold urutan ronde ditahan sebelum cue 'Round 1' (tidak ada yang berjalan di match.js); matchBest = kombo terbaik di seluruh ronde. + let vsHold = false, vsToken = 0, matchBest = 0; const roundCues = []; let ready = false, paused = false, settingsOpen = false, hitboxes = false, muted = false, volume = .45; let scale = 1, offsetX = 0, offsetY = 0, viewW = W, viewH = H, pixelRatio = 1; @@ -182,6 +184,7 @@ } function setMenuOpen(value) { menuOpen=!!value;clearInput();if(menuOpen){systemAnnouncer?.clear();stopUltimateVoice();bwReset();squad=enemySquad=parade=enemyParade=flock=enemyFlock=serpent=enemySerpent=quake=enemyQuake=finale=enemyFinale=skyfall=enemySkyfall=orrery=enemyOrrery=sunroar=enemySunroar=delivery=enemyDelivery=tortoise=enemyTortoise=null;fenrCutin=null;projectiles.length=effects.length=0;Feel.reset();setPaused(false);} + if(menuOpen)vsAbort(); document.body.classList.toggle('in-menu',menuOpen);$('#front-menu').hidden=!menuOpen; window.FrontEnd?.syncBackground(); } @@ -192,22 +195,30 @@ if(!playable().includes(player)||!playable().includes(enemy)||!Object.hasOwn(Rules.stages,stage)||!Object.hasOwn(Rules.difficulties,level)||!['versus','training'].includes(mode))return false; if(!images['stage-'+stage])return false; selectedCharacter=player;opponentCharacter=enemy;stageId=stage;difficulty=level;images.stage=images['stage-'+stage];CONFIG.groundY=Rules.stages[stage].groundY; - aiEnabled=mode==='versus';match=Rules.create(mode);roundCues.length=0;setMenuOpen(false);resetCombat();warmCutins(); + aiEnabled=mode==='versus';match=Rules.create(mode);matchBest=0;roundCues.length=0;vsAbort();setMenuOpen(false);resetCombat();warmCutins(); $('#ai-toggle').checked=aiEnabled;$('#character-select').value=selectedCharacter;document.body.classList.toggle('in-versus',mode==='versus'); - if(mode==='versus'){announceTimer=0;emitRoundCue('round',1);}else announce('TRAINING READY',1.3); + if(mode==='versus'){announceTimer=0;if(!(options.vs&&vsStart(options.vs,{player,enemy,stage,level})))emitRoundCue('round',1);}else announce('TRAINING READY',1.3); return true; } + // Layar VS (opsional: options.vs = true | {label,p2}; hanya mode versus, tidak ada di latihan). Mengembalikan true bila layar tampil; + // cue 'Round 1' baru keluar saat layar selesai/dilewati, sisanya (intro, FIGHT, announcer) berjalan persis seperti tanpa layar VS. + function vsStart(opt,info){ + const tok=++vsToken;vsHold=true; + const shown=window.VS?.show({...info,...(typeof opt==='object'?opt:{})},()=>{if(tok!==vsToken||!vsHold)return;vsHold=false;announceTimer=0;emitRoundCue('round',1);}); + if(!shown)vsHold=false;return !!shown; + } + function vsAbort(){vsToken++;vsHold=false;window.VS?.cancel?.();} function emitRoundCue(cue,round) { const event={cue,round,text:cue==='round'?`Round ${round}`:'Fight!'};roundCues.push(event);if(roundCues.length>12)roundCues.shift(); if(audio)menuSound(cue);if(roundAnnouncer)try{roundAnnouncer(event);}catch(_){} systemAnnouncer?.play(cue==='round'?`round_${round}`:'fight',{replace:cue==='round'}); } function advanceMatch(dt) { - if(!Rules||!match||match.mode!=='versus')return; + if(!Rules||!match||match.mode!=='versus'||vsHold)return; for(const event of Rules.tick(match,dt,hero.hp,currentDummyHP())) { if(event.type==='cue')emitRoundCue(event.cue,event.round); if(event.type==='round-end') { - clearInput();stopUltimateVoice();bwReset();squad=enemySquad=parade=enemyParade=flock=enemyFlock=serpent=enemySerpent=quake=enemyQuake=finale=enemyFinale=skyfall=enemySkyfall=orrery=enemyOrrery=sunroar=enemySunroar=delivery=enemyDelivery=tortoise=enemyTortoise=null;fenrCutin=null;hero.action=dummy.action=null;projectiles.length=effects.length=0;hitstop=cinematic=trauma=0; + matchBest=Math.max(matchBest,bestCombo);clearInput();stopUltimateVoice();bwReset();squad=enemySquad=parade=enemyParade=flock=enemyFlock=serpent=enemySerpent=quake=enemyQuake=finale=enemyFinale=skyfall=enemySkyfall=orrery=enemyOrrery=sunroar=enemySunroar=delivery=enemyDelivery=tortoise=enemyTortoise=null;fenrCutin=null;hero.action=dummy.action=null;projectiles.length=effects.length=0;hitstop=cinematic=trauma=0; if(hero.hp<=0)state('down');else state('idle');if(currentDummyHP()>0){dummy.state='idle';dummy.stateTime=0;} if(!event.timeout&&event.winner!=='draw'||event.doubleKO)startKoFx(event.doubleKO?'both':event.winner==='player'?'dummy':'hero',!!match.winner); match.endText=event.doubleKO?'DOUBLE K.O.':event.timeout?'TIME UP':'K.O.';announceTimer=0;sound('heavy'); @@ -218,9 +229,12 @@ systemAnnouncer?.play([call,outcome]); } if(event.type==='next-round'){resetCombat();announceTimer=0;emitRoundCue('round',event.round);} - let metaHandled=false; - if(event.type==='complete'){metaHandled=!!window.Meta?.award?.({winner:match.winner,playerWins:match.playerWins,enemyWins:match.enemyWins,player:selectedCharacter,enemy:opponentCharacter,level:difficulty,bestCombo,perfect:match.winner==='player'&&match.enemyWins===0});} - if(event.type==='complete'&&!metaHandled)window.FrontEnd?.showResult({winner:match.winner,playerWins:match.playerWins,enemyWins:match.enemyWins,player:selectedCharacter,enemy:opponentCharacter,stage:stageId,level:difficulty,mode:'versus'}); + if(event.type==='complete'){ + // bestCombo (kombo terbaik ronde terakhir) tetap dasar bonus koin; matchBest (seluruh ronde) hanya untuk layar hasil. + const info={winner:match.winner,playerWins:match.playerWins,enemyWins:match.enemyWins,player:selectedCharacter,enemy:opponentCharacter,stage:stageId,level:difficulty,bestCombo,matchBest:Math.max(matchBest,bestCombo),p2:p2Mode,perfect:match.winner==='player'&&match.enemyWins===0}; + const metaHandled=!!window.Meta?.award?.(info); + if(!metaHandled)window.FrontEnd?.showResult({...info,mode:'versus',award:window.Meta?.lastAward||null}); + } } } function updateMatchHud() { @@ -230,7 +244,7 @@ $('.match-center').setAttribute('aria-label',versus?'Pertandingan, menang dua ronde dari maksimal tiga':'Mode latihan tanpa batas waktu'); $('#match-score').textContent=versus?`${match.playerWins} — ${match.enemyWins}`:'VS';$('#stage-name').textContent=s.short;$('#stage-subtitle').textContent=s.subtitle; $('#round-wins-player').textContent=versus?'◆'.repeat(match.playerWins)+'◇'.repeat(2-match.playerWins):'';$('#round-wins-enemy').textContent=versus?'◆'.repeat(match.enemyWins)+'◇'.repeat(2-match.enemyWins):''; - const card=$('#round-call'),show=!menuOpen&&versus&&['intro','ko'].includes(match.phase); + const card=$('#round-call'),show=!menuOpen&&versus&&!vsHold&&['intro','ko'].includes(match.phase); card.hidden=!show;card.dataset.phase=match?.phase||'fight'; $('#round-call-title').textContent=!show?'':match.phase==='intro'?(match.fightCue?'FIGHT':`ROUND ${match.round}`):match.endText||'K.O.'; $('#round-call-detail').textContent=match?.phase==='ko'?(match.lastWinner==='draw'?'DRAW · ROUND REPLAY':(match.lastWinner==='player'?selectedCharacter:opponentCharacter).toUpperCase()+' WINS THE ROUND'):'FIRST TO TWO WINS'; @@ -2871,6 +2885,8 @@ }; // Pemeriksaan efek jurus untuk uji headless (tests/t1-fx.mjs). window.__game.fx = { snapshot: () => ({ ghosts: cfx.ghosts.length, hazards: cfx.hazards.filter(h => h.fire).length, dots: cfx.dots.length, later: cfx.later.length, particles: particles.length, effects: effects.map(e => e.type), tele: { hero: fxTeleP(hero), dummy: fxTeleP(dummy) }, projectiles: projectiles.map(p => ({ fx: p.fx, x: Math.round(p.x), y: Math.round(p.y), age: +(p.age || 0).toFixed(3), damage: p.damage, wait: p.wait || 0, orbit: !!(p.sw && !p.sw.go) })) }) }; + // Uji headless layar VS (tests/t2-vsresult.mjs): vsActive = urutan ronde sedang menunggu layar VS; vsSkip melewatinya. + Object.assign(window.__game, { vsActive: () => vsHold, vsSkip: () => !!window.VS?.force?.() }); // Hanya untuk uji headless (tests/t1-feel.mjs): pukulan langsung ke lawan, tabel rasa kena, status K.O./angka. Object.assign(window.__game, { hitDummy, feel: Feel, feelState: () => ({ ko: { ...Feel.ko.state, scale: Feel.ko.scale(), zoom: Feel.ko.zoom() }, kick: Feel.kickOffset(), numbers: damageNumbers.map(n => ({ amount: n.amount, kind: n.kind, label: n.label, color: n.color, life: n.life })), hitstop, trauma, realTime }) }); if(F)reset(); diff --git a/index.html b/index.html index 1ba82da..43ad4f0 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@