Blok, parry, lempar, Arcade, Survival, dua pemain, varian skill, teriakan jurus, tombol sentuh, ganti nama
This commit is contained in:
parent
dc3bdbe57d
commit
db86e3c96f
39
README.md
39
README.md
|
|
@ -1,6 +1,45 @@
|
|||
> [!NOTE]
|
||||
> **Konoha Fighting** adalah proyek turunan dari **AETHER CLASH** karya **Bang Tutorial** ([repo asli](https://github.com/bangtutorial/aether-clash), [tutorial YouTube](https://www.youtube.com/watch?v=UN_0bNC2wTU)). Kode asli berlisensi [MIT](LICENSE); aset bawaan (karakter, seni, audio, panduan) berlisensi [CC BY-NC 4.0](LICENSE-ASSETS.md), jadi **tidak untuk penggunaan komersial**. Perubahan di sini: roster karakter diganti dengan karakter parodi buatan sendiri. Bagian di bawah ini masih dokumentasi asli dan akan diperbarui bertahap.
|
||||
|
||||
# Konoha Fighting
|
||||
|
||||
Game fighting 2D di browser dengan tokoh publik versi parodi: **Bro Wo, Mega Chan, Son of Solo Man, Ethanol Man, A Nice, Solo Man, dan Gufron**, plus 5 slot misteri. Semua karakter adalah karikatur untuk hiburan; nama dan jurus hanya plesetan. Tanpa instalasi, tanpa dependensi.
|
||||
|
||||
## Menjalankan
|
||||
|
||||
```bash
|
||||
python3 serve.py 8141 # server lokal tanpa cache
|
||||
```
|
||||
|
||||
Buka `http://localhost:8141/?touch=0`. (Mode sentuh otomatis di HP.)
|
||||
|
||||
## Kontrol
|
||||
|
||||
| Tombol | Fungsi |
|
||||
| --- | --- |
|
||||
| A D / W / S | jalan, lompat (W dua kali = lompat ganda), jongkok |
|
||||
| Spasi | serang (ketuk 3x = kombo) |
|
||||
| ↓ ↓ + Spasi | uppercut (menjatuhkan lawan) |
|
||||
| ↓ → + Spasi | tinju maju |
|
||||
| I / O / P | skill 1, skill 2, ultimate |
|
||||
| K | jurus toko |
|
||||
| L (tahan) / L (ketuk) | blok / parry |
|
||||
| J | lempar (menembus blok) |
|
||||
| P2: ← → ↑ N M , . | jalan, lompat, serang, skill 1, skill 2, ultimate |
|
||||
|
||||
## Fitur
|
||||
|
||||
- Mode: VS Komputer (4 tingkat), Arcade, Survival, Dua Pemain, Latihan.
|
||||
- Ultimate sinematik dan mode pasca-ultimate untuk setiap karakter (A Nice berubah jadi Senator).
|
||||
- Koin, toko (jurus toko, varian skill per karakter, skin warna), menu Karakter, 20 pencapaian, easter egg.
|
||||
- Rencana pengembangan: [ROADMAP.md](ROADMAP.md). Alur membuat karakter baru: `work/WORKFLOW.md` di repo induk.
|
||||
|
||||
## Kredit dan lisensi
|
||||
|
||||
Berbasis **AETHER CLASH** oleh **Bang Tutorial**. Kode berlisensi MIT, aset bawaan CC BY-NC 4.0 (tidak untuk penggunaan komersial). Dokumentasi asli proyek induk tetap di bawah ini.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
# ⚔️ AETHER CLASH
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ Status per 2026-10-02. Dokumen ini adalah usulan; urutan dan isi sepenuhnya bisa
|
|||
**Sudah dikerjakan dan diuji:** jarak berdiri 54→80 px dan jangkauan pukulan +28% (1.1, 1.5); skala sprite lompat ×1.14 dan jongkok ×1.10 (1.2); gerakan perintah ↓↓+Spasi (uppercut, menjatuhkan) dan ↓→+Spasi (tinju maju); salto dua-ketuk-S dipindah (lompat ganda tetap W W) (1.4, keputusan 1); counter hit dan pengurangan damage kombo panjang plus peringkat kombo C/B/A/S (1.3 sebagian); koin, toko (5 jurus, 6 skin warna), menu Karakter (skin, jurus toko tombol K, daftar gerakan), Koleksi (15 pencapaian, statistik, ekspor/impor simpanan) (2, 3, 5 sebagian); easter egg: kode Konami, ketik `syududu` / `mbg` / `esemka`, klik judul 7x, ketuk slot tanda tanya (6 sebagian).
|
||||
**Belum:** launcher/juggle penuh, blok/parry/lempar, skill tambahan per karakter (butuh gambar), skin kostum, mode Arcade/Survival/2P, suara, gamepad, rebind tombol, hit-stop per serangan, kalibrasi ukuran kepala sejati (sekarang pakai faktor tetap).
|
||||
|
||||
**Tambahan (branch `feat/guard-arcade-audio`):** blok (tahan L), parry (ketuk L), lempar (J, menembus blok), blok CPU per tingkat, juggle di udara dan hit-stop bertingkat; varian skill per karakter (GANDA/CEPAT dan BERAT, dibeli di toko, dipilih di menu Karakter); mode Arcade (6 lawan, satu nyawa), Survival (tanpa akhir, makin sulit), Dua Pemain lokal; teriakan jurus lewat suara bawaan browser (bisa dimatikan); tombol sentuh untuk perintah, K, blok, dan lempar; nama dan tampilan diganti menjadi Konoha Fighting (kredit Bang Tutorial tetap); README baru.
|
||||
**Masih belum:** wajah Bro Wo/Mega Chan/Son of Solo Man dibuat ulang dengan foto acuan (butuh Anda memilih hasil gambar), skill baru bergambar khas tiap karakter, skin kostum, musik per arena, gamepad, rebind tombol, rekaman suara asli, hosting.
|
||||
|
||||
## 0. Kondisi sekarang
|
||||
|
||||
**Sudah ada**
|
||||
|
|
|
|||
10
files.json
10
files.json
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"files": 216,
|
||||
"bytes": 30910020,
|
||||
"bytes": 30918536,
|
||||
"list": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"bytes": 12066
|
||||
"bytes": 12429
|
||||
},
|
||||
{
|
||||
"file": "mobile.html",
|
||||
"bytes": 3638
|
||||
"bytes": 3620
|
||||
},
|
||||
{
|
||||
"file": "style.css",
|
||||
|
|
@ -24,11 +24,11 @@
|
|||
},
|
||||
{
|
||||
"file": "game.js",
|
||||
"bytes": 224243
|
||||
"bytes": 231657
|
||||
},
|
||||
{
|
||||
"file": "menu.js",
|
||||
"bytes": 21426
|
||||
"bytes": 22183
|
||||
},
|
||||
{
|
||||
"file": "match.js",
|
||||
|
|
|
|||
98
game.js
98
game.js
|
|
@ -29,6 +29,7 @@
|
|||
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;
|
||||
let time = 0, realTime = 0, lastTime = 0, accumulator = 0, hitstop = 0, trauma = 0, cinematic = 0, announceTimer = 0;
|
||||
let p2Mode = false; const keys2 = new Set(); let ignoreBlock = false;
|
||||
let seed = 18741, stepCount = 0, totalDamage = 0, comboHits = 0, bestCombo = 0, comboDamage = 0, comboTimer = 0, fps = 60;
|
||||
const keys = new Set(), taps = { a: -10, d: -10, s: -10 };
|
||||
const particles = [], effects = [], projectiles = [], damageNumbers = [];
|
||||
|
|
@ -158,7 +159,7 @@
|
|||
const duration = type === 'ultimate' ? .7 : heavy ? .26 : type === 'cast' ? .22 : .13;
|
||||
const osc = audio.createOscillator(), gain = audio.createGain();
|
||||
osc.type = type === 'jump' || type === 'cast' ? 'sine' : 'triangle';
|
||||
const frequency = ({ hit: 210, heavy: 110, cast: 430, jump: 360, ultimate: 72, step: 75, click: 550 })[type] || 190;
|
||||
const frequency = ({ hit: 210, heavy: 110, cast: 430, jump: 360, ultimate: 72, step: 75, click: 550, guard: 640, parry: 1250 })[type] || 190;
|
||||
osc.frequency.setValueAtTime(frequency, start); osc.frequency.exponentialRampToValueAtTime(type === 'jump' || type === 'cast' ? frequency * 2.5 : frequency * .22, start + duration);
|
||||
gain.gain.setValueAtTime(.001, start); gain.gain.exponentialRampToValueAtTime(heavy ? .32 : .12, start + .008); gain.gain.exponentialRampToValueAtTime(.001, start + duration);
|
||||
osc.connect(gain); gain.connect(master); osc.start(start); osc.stop(start + duration + .02); audioSources++;
|
||||
|
|
@ -180,7 +181,7 @@
|
|||
function playable() { return ['arco','fenr',...Object.keys(KITS)]; }
|
||||
function startMatch(options={}) {
|
||||
if(!ready||!Rules)return false;
|
||||
const {player='arco',enemy='fenr',stage='bellora',level='medium',mode='versus'}=options;
|
||||
const {player='arco',enemy='fenr',stage='bellora',level='medium',mode='versus'}=options; p2Mode=false; keys2.clear();
|
||||
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;
|
||||
|
|
@ -209,8 +210,9 @@
|
|||
systemAnnouncer?.play([call,outcome]);
|
||||
}
|
||||
if(event.type==='next-round'){resetCombat();announceTimer=0;emitRoundCue('round',event.round);}
|
||||
if(event.type==='complete'){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')window.FrontEnd?.showResult({winner:match.winner,playerWins:match.playerWins,enemyWins:match.enemyWins,player:selectedCharacter,enemy:opponentCharacter,stage:stageId,level:difficulty,mode:'versus'});
|
||||
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'});
|
||||
}
|
||||
}
|
||||
function updateMatchHud() {
|
||||
|
|
@ -277,7 +279,7 @@
|
|||
if(!KITS[selectedCharacter].start(hero,name))return false;
|
||||
hero.animTime=0;hero.runDirection=0;
|
||||
if(name==='ultimate')startSummon(hero);
|
||||
sound(name==='ultimate'?'ultimate':'cast');return true;
|
||||
sound(name==='ultimate'?'ultimate':'cast');if(name==='ultimate')window.Meta?.shout?.(KITS[selectedCharacter].names?.[3]);return true;
|
||||
}
|
||||
if (!canAct() || hero.action || hero.cooldowns[name] > 0 || !Object.hasOwn(cooldownMax, name) || (name === 'ultimate' && squad)) return false;
|
||||
if (hero.doublePose > 0) { hero.rollBuffer = name; return true; }
|
||||
|
|
@ -293,8 +295,11 @@
|
|||
if(menuOpen) {if(key==='escape'&&settingsOpen)closeSettings();return;}
|
||||
if (key === 'escape') { if (settingsOpen) closeSettings(); else setPaused(!paused); return; }
|
||||
if (key === 'r') { reset(); return; }
|
||||
if (p2Mode && P2_KEYS.includes(key)) { p2Press(key); return; }
|
||||
if (!canAct() || keys.has(key)) return;
|
||||
keys.add(key); logInput(key);
|
||||
if (key === 'l') { hero.parry = .16; }
|
||||
if (key === 'j') startThrow();
|
||||
if (key === 'a' || key === 'd') { const direction = key === 'a' ? -1 : 1; if (time - taps[key] <= CONFIG.doubleTapWindow) hero.runDirection = direction; else if (hero.runDirection !== direction) hero.runDirection = 0; taps[key] = time; taps[key === 'a' ? 'd' : 'a'] = -10; }
|
||||
// A new left/right press cuts a basic attack short: right away after its hit, or as soon as the hit lands.
|
||||
if ((key === 'a' || key === 'd') && hero.action?.type === 'attack') { if (hero.action.fired) { hero.action = null; state(hero.grounded ? 'idle' : 'jump'); } else { hero.action.cancelInto = 'move'; hero.action.queued = 0; } }
|
||||
|
|
@ -338,7 +343,7 @@
|
|||
const id = commandFor(); if (!id || !canAct() || hero.action || hero.doublePose > 0 || (hero.spCd || 0) > 0 || !hero.grounded) return false;
|
||||
const c = COMMANDS[id]; inputLog.length = 0; hero.spCd = c.cd;
|
||||
hero.action = { type: 'attack', name: c.state, index: 3, t: 0, duration: c.duration, hit: false, queued: 0, fired: false, hitAt: c.hitAt, special: id, color: '#ffe9a0' };
|
||||
hero.runDirection = 0; state(c.state); announce(c.label, .6); sound('cast'); return true;
|
||||
hero.runDirection = 0; state(c.state); announce(c.label, .6); sound('cast'); window.Meta?.shout?.(c.label.replace('!', '')); return true;
|
||||
}
|
||||
const SHOP_MOVES = {
|
||||
seruduk: { name: 'SERUDUK KILAT', cd: 7, state: 'attack2', duration: .45, hitAt: .3, damage: 14, knockback: 340 },
|
||||
|
|
@ -368,6 +373,10 @@
|
|||
if (Math.abs(dx) < 230 && dummy.y > CONFIG.groundY - 125) hitDummy(SHOP_MOVES.hentak.damage, 200, col, hero.x, y, true);
|
||||
return;
|
||||
}
|
||||
if (a.special === 'throw') {
|
||||
if (Math.abs(dx) < 125 && dummy.y >= CONFIG.groundY - 1 && !['down', 'recover'].includes(dummy.state)) { dummy.invuln = 0; ignoreBlock = true; hitDummy(14, 330, col, hero.x, y, true); ignoreBlock = false; dummy.x = clamp(hero.x + hero.facing * 150, 110, W - 100); sound('heavy'); trauma = Math.min(1, trauma + .3); } else sound('step');
|
||||
return;
|
||||
}
|
||||
const reach = a.special === 'uppercut' ? 150 : a.special === 'seruduk' ? 190 : 175, dmg = (COMMANDS[a.special] || SHOP_MOVES[a.special]).damage, kb = (COMMANDS[a.special] || SHOP_MOVES[a.special]).knockback;
|
||||
dust(hero.x + hero.facing * 40, 12);
|
||||
if (dx > -30 && dx < reach && near) {
|
||||
|
|
@ -377,8 +386,48 @@
|
|||
} else sound('step');
|
||||
}
|
||||
function comboRank(n) { return n >= 12 ? 'S' : n >= 8 ? 'A' : n >= 5 ? 'B' : n >= 3 ? 'C' : ''; }
|
||||
function release(key) { key = key.toLowerCase(); keys.delete(key); if ((key === 'a' && hero.runDirection === -1) || (key === 'd' && hero.runDirection === 1)) hero.runDirection = 0; }
|
||||
function clearInput() { keys.clear(); hero.runDirection = 0; taps.a = taps.d = taps.s = -10; }
|
||||
|
||||
// ---- Guard (hold L), parry (tap L just before a hit), throw (J), P2, CPU guard ----
|
||||
function heroGuard(damage, options) {
|
||||
if (!(hero.blocking || hero.parry > 0) || hero.action || options?.unblockable) return false;
|
||||
if (hero.parry > 0) {
|
||||
hero.parry = 0; hero.invuln = Math.max(hero.invuln, .35); announce('PARRY!', .6); sound('parry'); window.Meta?.shout?.('Parry!'); hitstop = Math.max(hitstop, .1); trauma = Math.min(1, trauma + .2);
|
||||
spawnParticles(hero.x + hero.facing * 40, hero.y - 100, '#fff3b0', 22, 320);
|
||||
if (!['down', 'recover'].includes(dummy.state)) { dummy.action = null; dummy.state = 'hurt'; dummy.stateTime = -.3; dummy.vx = -hero.facing * 120; }
|
||||
window.Meta?.event?.('parry'); return true;
|
||||
}
|
||||
const chip = Math.max(1, Math.round(damage * .2)); hero.hp = Math.max(1, hero.hp - chip); hero.guard += damage * 1.6; hero.vx = -hero.facing * 90;
|
||||
spawnParticles(hero.x + hero.facing * 38, hero.y - 90, '#ffd36a', 10, 200); sound('guard'); hitstop = Math.max(hitstop, .03);
|
||||
damageNumbers.push({ x: hero.x, y: hero.y - 170, amount: 'GUARD', life: .6, maxLife: .6, color: '#ffd36a' });
|
||||
if (hero.guard >= 60) { hero.guard = 0; hero.guardBreak = 1.2; hero.blocking = false; hero.hurtTime = .7; state('hurt'); announce('GUARD BREAK', .7); }
|
||||
return true;
|
||||
}
|
||||
function startThrow() {
|
||||
if (!canAct() || hero.action || hero.doublePose > 0 || !hero.grounded || (hero.throwCd || 0) > 0) return false;
|
||||
hero.throwCd = 3; hero.action = { type: 'attack', name: 'attack2', index: 3, t: 0, duration: .5, hit: false, queued: 0, fired: false, hitAt: .3, special: 'throw', color: '#ffb36a' };
|
||||
hero.runDirection = 0; state('attack2'); announce('LEMPAR!', .5); window.Meta?.shout?.('Lempar!'); return true;
|
||||
}
|
||||
// P2 (local two-player): the second fighter is driven by these keys instead of the CPU.
|
||||
const P2_KEYS = ['arrowleft', 'arrowright', 'arrowup', 'n', 'm', ',', '.'];
|
||||
let p2Chain = { i: 0, t: -9 };
|
||||
function p2Press(key) {
|
||||
keys2.add(key); if (menuOpen || paused || (match && match.phase !== 'fight') || dummy.state === 'hurt' || dummy.state === 'down' || dummy.state === 'recover') return;
|
||||
if (key === 'arrowup' && dummy.y >= CONFIG.groundY - 1 && !dummy.action) { dummy.vy = -CONFIG.jumpSpeed; dummy.y -= 1; dummy.jumps = 1; dummy.state = 'jump'; dummy.stateTime = 0; sound('jump'); return; }
|
||||
if (key === 'n') { const i = time - p2Chain.t < .7 && p2Chain.i < 3 ? p2Chain.i + 1 : 1; if (startEnemyAction('attack', i)) p2Chain = { i, t: time }; return; }
|
||||
if (key === 'm') startEnemyAction('skill1'); if (key === ',') startEnemyAction('skill2'); if (key === '.') startEnemyAction('ultimate');
|
||||
}
|
||||
function updateP2(dt) {
|
||||
const dir = (keys2.has('arrowright') ? 1 : 0) - (keys2.has('arrowleft') ? 1 : 0), grounded = dummy.y >= CONFIG.groundY - 1;
|
||||
if (hero.hp <= 0 || (match && match.phase !== 'fight')) { dummy.vx = approach(dummy.vx, 0, dt * 1500); if (grounded) dummy.state = 'idle'; return; }
|
||||
if (!grounded) { dummy.state = 'jump'; if (dir) dummy.vx = approach(dummy.vx, dir * 260, dt * 1600); return; }
|
||||
dummy.facing = hero.x >= dummy.x ? 1 : -1;
|
||||
dummy.vx = approach(dummy.vx, dir * CONFIG.walkSpeed, dt * 2800);
|
||||
dummy.state = dir ? 'walk' : 'idle'; dummy.walkPhase = (dummy.walkPhase || 0) + Math.abs(dummy.vx) * dt / 148;
|
||||
}
|
||||
function forceCommand(id) { const c = COMMANDS[id]; if (!c) return false; inputLog.length = 0; const f = hero.facing > 0 ? 'd' : 'a'; if (id === 'uppercut') inputLog.push({ k: 's', t: time - .1 }, { k: 's', t: time }); else inputLog.push({ k: 's', t: time - .1 }, { k: f, t: time }); return startCommand(); }
|
||||
const CPU_BLOCK = { easy: .05, medium: .14, hard: .24, excellent: .32 };
|
||||
function release(key) { key = key.toLowerCase(); keys.delete(key); keys2.delete(key); if ((key === 'a' && hero.runDirection === -1) || (key === 'd' && hero.runDirection === 1)) hero.runDirection = 0; }
|
||||
function clearInput() { keys.clear(); keys2.clear(); hero.runDirection = 0; taps.a = taps.d = taps.s = -10; }
|
||||
function resetCombat() {
|
||||
systemAnnouncer?.clear();
|
||||
stopUltimateVoice();
|
||||
|
|
@ -399,6 +448,10 @@
|
|||
if (menuOpen || (match && match.phase!=='fight') || dummy.invuln > 0 || dummy.state === 'down' || dummy.state === 'recover') return false;
|
||||
if (dummy.action?.guard && shellGuard(dummy)) return false;
|
||||
const dir = sourceX <= dummy.x ? 1 : -1;
|
||||
if (!ignoreBlock && F && aiEnabled && !p2Mode && match?.mode === 'versus' && dummy.y >= CONFIG.groundY - 1 && !dummy.action && (dummy.state === 'idle' || dummy.state === 'walk') && !knockdown && random() < (CPU_BLOCK[difficulty] ?? .1)) {
|
||||
const chip = Math.min(Math.max(1, Math.round(damage * .2)), Math.max(0, BALANCE.dummyMax - dummy.damage - 1)); dummy.damage += chip; totalDamage += chip;
|
||||
spawnParticles(dummy.x - dir * 30, impactY, '#ffd36a', 8, 180); damageNumbers.push({ x: dummy.x, y: impactY - 40, amount: 'GUARD', life: .6, maxLife: .6, color: '#ffd36a' }); sound('step'); hitstop = Math.max(hitstop, .03); return true;
|
||||
}
|
||||
// Counter hit: striking a rival in their wind-up deals extra; long chains scale down so none loops forever.
|
||||
if (dummy.action && !dummy.action.fired && dummy.action.type === 'attack') { damage = Math.round(damage * 1.25); if (damage > 1) announce('COUNTER!', .5); }
|
||||
if (comboHits >= 5) damage = Math.max(1, Math.round(damage * Math.max(.55, 1 - .07 * (comboHits - 4))));
|
||||
|
|
@ -409,9 +462,10 @@
|
|||
dummy.stunHits = dummy.state === 'hurt' ? (dummy.stunHits || 0) + 1 : 1;
|
||||
if (cap && dummy.stunHits >= cap) dummy.invuln = .5 + cpuProfile().immunity;
|
||||
dummy.flash = 0; dummy.vx = dir * power; dummy.stateTime = 0; dummy.state = 'hurt';
|
||||
if (dummy.y >= CONFIG.groundY - 1) dummy.juggle = 0; else if ((dummy.juggle = (dummy.juggle || 0) + 1) <= 4) dummy.vy = Math.min(dummy.vy, -260);
|
||||
dummy.action=null;
|
||||
if(F&&dummy.transformPending>0)F.end(dummy);
|
||||
if (freezeWorld) hitstop = Math.max(hitstop, damage >= 30 ? .085 : .045);
|
||||
if (freezeWorld) hitstop = Math.max(hitstop, damage >= 30 ? .085 : damage >= 14 ? .065 : .045);
|
||||
trauma = Math.min(1, trauma + (damage >= 30 ? .45 : .18) * shake);
|
||||
damageNumbers.push({ x: dummy.x + (random() - .5) * 20, y: impactY - 20, amount: damage, life: .9, maxLife: .9, color });
|
||||
spawnParticles(dummy.x - dir * 21, impactY, color, damage >= 30 ? 22 : 14, damage >= 30 ? 320 : 220);
|
||||
|
|
@ -797,8 +851,12 @@
|
|||
if (a.action || a.state === 'hurt' || a.state === 'down' || a.state === 'recover') return false;
|
||||
if (name !== 'attack' && a.cooldowns[name] > 0) return false;
|
||||
const rg = !!a.bwRage && !!d.mode && name !== 'attack';
|
||||
const alt = a === hero && !rg && (name === 'skill1' || name === 'skill2') ? window.Meta?.altSkill?.(d.id, name) : null;
|
||||
if (name !== 'attack') a.cooldowns[name] = rg ? (name === 'ultimate' ? 999 : (d.mode.cooldowns?.[name] ?? 6)) : d.balance.cooldowns[name];
|
||||
a.action = { ...move(name, index, rg), t: 0, fired: false, queued: 0 }; a.state = a.action.name; a.stateTime = 0; return true;
|
||||
a.action = { ...move(name, index, rg), t: 0, fired: false, queued: 0 };
|
||||
if (alt) { const m = a.action, proj = ['projectile', 'wave', 'homing'].includes(m.kind);
|
||||
if (alt === 'ganda') { if (proj) { m.count = (m.count || 1) * 2; m.spread = m.spread || 55; m.damage = Math.max(1, Math.round(m.damage * .65)); a.cooldowns[name] *= 1.1; } else { m.damage = Math.round(m.damage * .8); m.duration *= .75; a.cooldowns[name] *= .7; } }
|
||||
if (alt === 'berat') { m.damage = Math.round(m.damage * 1.6); m.knockback = (m.knockback || 160) * 1.3; m.duration += .14; if (m.size) m.size = Math.round(m.size * 1.35); if (m.radius) m.radius = Math.round(m.radius * 1.35); if (m.speed) m.speed = Math.round(m.speed * .85); if (m.dash) m.dash *= 1.3; if (m.reach) m.reach = Math.round(m.reach * 1.15); a.cooldowns[name] *= 1.5; } } a.state = a.action.name; a.stateTime = 0; return true;
|
||||
};
|
||||
const refund = a => { for (const n of Object.keys(d.balance.cooldowns)) a.cooldowns[n] = Math.max(0, a.cooldowns[n] - d.balance.cooldowns[n] * (d.balance.basicRefund ?? .05)); };
|
||||
return { balance: d.balance, names: d.names, combo: d.combo, move, start, refund, def: d };
|
||||
|
|
@ -1260,16 +1318,18 @@
|
|||
time += dt; hero.animTime += dt; hero.invuln = Math.max(0, hero.invuln - dt); hero.doublePose = Math.max(0, (hero.doublePose || 0) - dt);
|
||||
if(fenrCutin) {fenrCutin.t+=dt;if(fenrCutin.t>.78)fenrCutin=null;}
|
||||
if(F&&selectedCharacter==='fenr'){const changes=hero.formChanges;F.advance(hero,dt,hero.hp>0);if(changes!==hero.formChanges){syncPlayerForm();hero.doublePose=0;state(hero.grounded?'idle':'jump');}}
|
||||
hero.airTime = hero.grounded ? 0 : hero.airTime + dt; hero.spCd = Math.max(0, (hero.spCd || 0) - dt); hero.shopCd = Math.max(0, (hero.shopCd || 0) - dt); hero.shield = Math.max(0, (hero.shield || 0) - dt);
|
||||
hero.airTime = hero.grounded ? 0 : hero.airTime + dt; hero.spCd = Math.max(0, (hero.spCd || 0) - dt); hero.throwCd = Math.max(0, (hero.throwCd || 0) - dt); hero.shopCd = Math.max(0, (hero.shopCd || 0) - dt); hero.shield = Math.max(0, (hero.shield || 0) - dt);
|
||||
for (const name of Object.keys(hero.cooldowns)) hero.cooldowns[name] = Math.max(0, hero.cooldowns[name] - dt);
|
||||
if (hero.doublePose <= 0 && hero.rollBuffer && hero.hurtTime <= 0) { const buffered = hero.rollBuffer; hero.rollBuffer = null; if (buffered === 'attack') startAttack(); else cast(buffered); }
|
||||
comboTimer = Math.max(0, comboTimer - dt); if (!comboTimer) comboHits = comboDamage = 0;
|
||||
const dir = (keys.has('d') ? 1 : 0) - (keys.has('a') ? 1 : 0), crouching = keys.has('s') && hero.grounded && !hero.action;
|
||||
hero.parry = Math.max(0, (hero.parry || 0) - dt); hero.guardBreak = Math.max(0, (hero.guardBreak || 0) - dt); hero.guard = Math.max(0, (hero.guard || 0) - dt * 22);
|
||||
hero.blocking = keys.has('l') && hero.grounded && !hero.action && hero.hurtTime <= 0 && hero.hp > 0 && hero.guardBreak <= 0 && hero.doublePose <= 0;
|
||||
const dir = (keys.has('d') ? 1 : 0) - (keys.has('a') ? 1 : 0), crouching = (keys.has('s') || hero.blocking) && hero.grounded && !hero.action;
|
||||
if (!hero.action && hero.hurtTime <= 0 && hero.hp > 0) {
|
||||
if (dir && hero.doublePose <= 0) hero.facing = dir;
|
||||
if (hero.runDirection && dir !== hero.runDirection) hero.runDirection = 0;
|
||||
const speed = (hero.runDirection === dir && dir ? CONFIG.runSpeed : CONFIG.walkSpeed)*(selectedCharacter==='fenr'&&hero.form==='wolf'?1.08:1)*(bwx.rage.player>0?(modeOf(selectedCharacter)?.speedMul||1):1);
|
||||
hero.vx = approach(hero.vx, crouching ? 0 : dir * speed, dt * (hero.grounded ? 2800 : 1600));
|
||||
hero.vx = approach(hero.vx, crouching || hero.blocking ? 0 : dir * speed, dt * (hero.grounded ? 2800 : 1600));
|
||||
if (hero.grounded) state(crouching ? 'crouch' : dir ? hero.runDirection ? 'run' : 'walk' : 'idle');
|
||||
else state(hero.doublePose > 0 ? 'doublejump' : 'jump');
|
||||
} else hero.vx = approach(hero.vx, 0, dt * 1900);
|
||||
|
|
@ -1307,7 +1367,7 @@
|
|||
function updateDummy(dt) {
|
||||
if(F){for(const name of Object.keys(dummy.cooldowns))dummy.cooldowns[name]=Math.max(0,dummy.cooldowns[name]-dt);if(opponentCharacter==='fenr')F.advance(dummy,dt,!dummy.ko);}
|
||||
dummy.stateTime += dt; dummy.flash = Math.max(0, dummy.flash - dt); dummy.invuln = Math.max(0, dummy.invuln - dt);
|
||||
dummy.x = clamp(dummy.x + dummy.vx * dt, 110, W - 100); if(!F || !aiEnabled || dummy.action || ['hurt','down','recover'].includes(dummy.state))dummy.vx = approach(dummy.vx, 0, dt * 950);
|
||||
dummy.x = clamp(dummy.x + dummy.vx * dt, 110, W - 100); if(!F || (!aiEnabled && !p2Mode) || dummy.action || ['hurt','down','recover'].includes(dummy.state))dummy.vx = approach(dummy.vx, 0, dt * 950);
|
||||
if(F && !['down','recover'].includes(dummy.state) && hero.y>CONFIG.groundY-130 && dummy.y>CONFIG.groundY-130 && Math.abs(dummy.x-hero.x)<bodyGap())dummy.x=clamp(hero.x+(dummy.x>=hero.x?bodyGap():-bodyGap()),110,W-100);
|
||||
// Versus CPU follows the player's post-hurt immunity rule by difficulty, so a mashed chain cannot loop it forever.
|
||||
// Its stun lasts 0.5 s there, which still covers every 3-hit chain gap; the training dummy stays open for practice.
|
||||
|
|
@ -1786,11 +1846,12 @@
|
|||
if (!a.fired && a.type !== 'ultimate' && a.t >= a.duration * a.hitAt) { a.fired = true; enemyStrike(a); }
|
||||
if (a.t >= a.duration || (opponentCharacter !== 'fenr' && a.type === 'skill1' && a.fired)) {
|
||||
dummy.action = null; dummy.state = 'idle'; dummy.stateTime = 0; dummy.aiThink = P.recovery;
|
||||
if (hero.hp > 0) cpuFollowUp(a, P);
|
||||
if (hero.hp > 0 && !p2Mode) cpuFollowUp(a, P);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const airborne = dummy.y < CONFIG.groundY || dummy.vy < 0;
|
||||
if (p2Mode) { updateP2(dt); return; }
|
||||
if (!aiEnabled || hero.hp <= 0) { dummy.vx = approach(dummy.vx, 0, dt * 1500); if (!airborne) dummy.state = 'idle'; return; }
|
||||
if (airborne) {
|
||||
if (cpu.doubleAt && time >= cpu.doubleAt && dummy.jumps < 2) { dummy.jumps = 2; dummy.vy = -CONFIG.doubleJumpSpeed; cpu.doubleAt = 0; }
|
||||
|
|
@ -1977,6 +2038,7 @@
|
|||
const r = list[index], cw = manifest.cell?.width || manifest.cell?.w || r.w, ch = manifest.cell?.height || manifest.cell?.h || r.h;
|
||||
const anchorX = manifest.cell?.anchor_x ?? cw / 2, anchorY = manifest.cell?.anchor_y ?? ch - (manifest.cell?.safe_margin_y || 0);
|
||||
const spriteScale = pose.scale * stateK(metrics, hero.state), skin = window.Meta?.skinFilter?.(selectedCharacter);
|
||||
if (hero.blocking) { ctx.save(); ctx.strokeStyle = '#ffd36a'; ctx.lineWidth = 5; ctx.globalAlpha = .75; ctx.beginPath(); ctx.arc(hero.x + hero.facing * 20, hero.y - 90, 85, hero.facing > 0 ? -1.1 : Math.PI - 1.1, hero.facing > 0 ? 1.1 : Math.PI + 1.1); ctx.stroke(); ctx.restore(); }
|
||||
if (hero.shield > 0) { ctx.save(); ctx.strokeStyle = '#9fe8ff'; ctx.globalAlpha = Math.min(1, hero.shield) * .8; ctx.lineWidth = 4; ctx.beginPath(); ctx.ellipse(hero.x, hero.y - 90, 70, 105, 0, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); }
|
||||
ctx.save(); if (skin) ctx.filter = skin; ctx.translate(Math.round(hero.x), Math.round(hero.y)); ctx.scale(hero.facing * spriteScale, spriteScale); ctx.imageSmoothingEnabled = false;
|
||||
if (pose.pivot) { ctx.translate(pose.worldPivot.x, pose.worldPivot.y); ctx.rotate(pose.rotation); ctx.translate(-pose.pivot.x, -pose.pivot.y); }
|
||||
|
|
@ -2288,7 +2350,7 @@
|
|||
function setPaused(value) { paused = value; clearInput(); syncUltimateVoice(); $('#pause-btn').textContent = value ? '▶' : 'Ⅱ'; const panel = $('#pause-panel'); if (value && !panel.open) panel.showModal(); if (!value && panel.open) panel.close(); if (!value) canvas.focus({ preventScroll: true }); }
|
||||
function openSettings() { settingsOpen = true; clearInput(); syncUltimateVoice(); window.FrontEnd?.syncBackground(); $('#close-settings').innerHTML=(menuOpen?'Kembali ke menu':'Kembali ke arena')+' <span>↗</span>'; $('#settings-panel').showModal(); }
|
||||
function closeSettings() { settingsOpen = false; $('#settings-panel').close(); syncUltimateVoice(); window.FrontEnd?.syncBackground(); canvas.focus({ preventScroll: true }); }
|
||||
const handled = new Set(['w','a','s','d',' ','i','o','p','escape','r']);
|
||||
const handled = new Set(['w','a','s','d',' ','i','o','p','k','l','j','escape','r','arrowleft','arrowright','arrowup','n','m',',','.']);
|
||||
window.addEventListener('keydown', e => { if (!handled.has(e.key.toLowerCase()) || /INPUT|TEXTAREA|SELECT/.test(e.target.tagName)) return; if (e.target.tagName === 'BUTTON' && (e.key === ' ' || e.key === 'Enter')) return; e.preventDefault(); if (!e.repeat) press(e.key); });
|
||||
window.addEventListener('keyup', e => { if (handled.has(e.key.toLowerCase())) { if(!/INPUT|TEXTAREA|SELECT|BUTTON/.test(e.target.tagName))e.preventDefault(); release(e.key); } });
|
||||
window.addEventListener('blur', clearInput); document.addEventListener('visibilitychange', () => { clearInput(); lastTime = 0; accumulator = 0;if(document.hidden)systemAnnouncer?.clear(); syncUltimateVoice(); syncMusic(); });
|
||||
|
|
@ -2311,7 +2373,7 @@
|
|||
function tick(stamp) { if (lastTime) { const elapsed = Math.min((stamp - lastTime) / 1000, .1); fps += (1 / Math.max(elapsed, .001) - fps) * .05; if (!window.__game?.manual) { accumulator += elapsed; while (accumulator >= 1 / 120) { step(1 / 120); accumulator -= 1 / 120; } } } lastTime = stamp; draw(); requestAnimationFrame(tick); }
|
||||
// Deterministic QA interface: set manual=true, reset(), press/release(), advance(seconds).
|
||||
// Positions represent feet. No networking or persistent state is changed by this interface.
|
||||
window.__game = { hero, dummy, config: CONFIG, balance:BALANCE, particles, effects, projectiles, pose: heroPose, press: key => press(key, true), release, input(key, down) { if (down) press(key); else release(key); }, reset, cast, jump, attack: startAttack, clearInput, draw, manual: false,
|
||||
window.__game = { hero, dummy, config: CONFIG, balance:BALANCE, particles, effects, projectiles, pose: heroPose, press: key => press(key, true), release, input(key, down) { if (key.startsWith('cmd:')) { if (down) forceCommand(key.slice(4)); return; } if (down) press(key); else release(key); }, setP2(on) { p2Mode = !!on; if (on) { aiEnabled = false; const t = $('#ai-toggle'); if (t) t.checked = false; } }, command: forceCommand, reset, cast, jump, attack: startAttack, clearInput, draw, manual: false,
|
||||
advance(seconds) { for (let i = 0; i < Math.round(seconds * 120); i++) step(1 / 120); draw(); return this.snapshot(); },
|
||||
snapshot() { return { ready, paused, settingsOpen, menuOpen, selectedCharacter, opponentCharacter, difficulty, stageId, match:match?{...match}:null,roundCues:[...roundCues],aiEnabled, time, stepCount, hero: JSON.parse(JSON.stringify(hero)), dummy: { ...dummy }, healthBars:{hero:hpLayers(hero.hp),dummy:hpLayers(currentDummyHP())}, squad: squad ? JSON.parse(JSON.stringify(squad)) : null, enemySquad:enemySquad?JSON.parse(JSON.stringify(enemySquad)):null, parade: parade ? JSON.parse(JSON.stringify(parade)) : null, enemyParade: enemyParade ? JSON.parse(JSON.stringify(enemyParade)) : null, flock: flock ? JSON.parse(JSON.stringify(flock)) : null, enemyFlock: enemyFlock ? JSON.parse(JSON.stringify(enemyFlock)) : null, serpent: serpent ? JSON.parse(JSON.stringify(serpent)) : null, enemySerpent: enemySerpent ? JSON.parse(JSON.stringify(enemySerpent)) : null, quake: quake ? JSON.parse(JSON.stringify(quake)) : null, enemyQuake: enemyQuake ? JSON.parse(JSON.stringify(enemyQuake)) : null, finale: finale ? { ...finale } : null, enemyFinale: enemyFinale ? { ...enemyFinale } : null, skyfall: skyfall ? { ...skyfall } : null, enemySkyfall: enemySkyfall ? { ...enemySkyfall } : null, orrery: orrery ? { ...orrery } : null, enemyOrrery: enemyOrrery ? { ...enemyOrrery } : null, sunroar: sunroar ? { ...sunroar } : null, enemySunroar: enemySunroar ? { ...enemySunroar } : null, delivery: delivery ? { ...delivery } : null, enemyDelivery: enemyDelivery ? { ...enemyDelivery } : null, tortoise: tortoise ? { ...tortoise, spirit: undefined } : null, enemyTortoise: enemyTortoise ? { ...enemyTortoise, spirit: undefined } : null, playable: playable(), cutinVisible: !!squad && squad.t < SQUAD.cutinDuration, totalDamage, comboHits, comboDamage, hitstop, cinematic, trauma, projectileCount: projectiles.length, audioState: audio?.state || 'uninitialized', audioSources, stageLoaded: !!images.stage, spriteLoaded: !!images.hero, manifestLoaded: !!manifest }; },
|
||||
startMatch,setMenuOpen,menuSound,openSettings,toggleFullscreen,music(){return music?{on:musicOn,volume:musicVolume,playing:!music.paused,src:MUSIC_PATH,element:music}:null;},onRoundCue(fn){roundAnnouncer=typeof fn==='function'?fn:null;},
|
||||
|
|
@ -2319,7 +2381,7 @@
|
|||
stopAnnouncer(){systemAnnouncer?.clear();},announcerState(){return systemAnnouncer?.snapshot()||null;},
|
||||
selectCharacter, setAI(value){aiEnabled=!!value;dummy.action=null;dummy.vx=0;dummy.state='idle';}, opponentPose,
|
||||
setPaused, setHitboxes(v) { hitboxes = !!v; $('#hitbox-toggle').checked = hitboxes; },
|
||||
receiveHit(damage = 12,options={}) { if (menuOpen || (match&&match.phase!=='fight') || hero.hp<=0 || hero.invuln > 0) return false; if (hero.action?.guard && shellGuard(hero)) return false; if (hero.action?.type?.startsWith('skill') || hero.action?.type === 'ultimate') return false; hero.hp = Math.max(0, hero.hp - damage); hero.action = null; hero.rollBuffer = null; hero.doublePose = 0; hero.hurtTime = hero.hp>0?.42:0; hero.vx = hero.facing * -170; state(hero.hp>0?'hurt':'down'); spawnParticles(hero.x, hero.y - 75, '#ffc694', 14, 200); if(options.freeze!==false)hitstop = .05; trauma = Math.min(1, trauma + .2 * (options.shake ?? 1)); if (!hero.hp) {hero.koTime=1.7;clearInput();if(match?.mode!=='versus')announce('CORE RESTART',1.7);} return true; }
|
||||
receiveHit(damage = 12,options={}) { if (menuOpen || (match&&match.phase!=='fight') || hero.hp<=0 || hero.invuln > 0) return false; if (hero.action?.guard && shellGuard(hero)) return false; if (heroGuard(damage, options)) return true; if (hero.action?.type?.startsWith('skill') || hero.action?.type === 'ultimate') return false; hero.hp = Math.max(0, hero.hp - damage); hero.action = null; hero.rollBuffer = null; hero.doublePose = 0; hero.hurtTime = hero.hp>0?.42:0; hero.vx = hero.facing * -170; state(hero.hp>0?'hurt':'down'); spawnParticles(hero.x, hero.y - 75, '#ffc694', 14, 200); if(options.freeze!==false)hitstop = .05; trauma = Math.min(1, trauma + .2 * (options.shake ?? 1)); if (!hero.hp) {hero.koTime=1.7;clearInput();if(match?.mode!=='versus')announce('CORE RESTART',1.7);} return true; }
|
||||
};
|
||||
if(F)reset();
|
||||
if(window.FRONTEND_ENABLED)setMenuOpen(true);
|
||||
|
|
|
|||
14
index.html
14
index.html
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="id">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#101923"><script>/* Phones/tablets: mark .touch and open the landscape mobile shell (mobile.html). ?touch=1 forces it on desktop, ?touch=0 turns it off. */(()=>{const q=new URLSearchParams(location.search),f=q.get('touch'),t=f?f==='1':matchMedia('(pointer: coarse)').matches||(navigator.maxTouchPoints>0&&/Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent));if(!t)return;document.documentElement.classList.add('touch');if(window.top===window&&!q.has('embed'))location.replace('mobile.html'+location.search+location.hash);})();</script><title>AETHER CLASH — Mecha vs Demi-Human</title><link rel="preload" href="assets/fonts/Rajdhani-Bold.ttf" as="font" type="font/ttf" crossorigin><link rel="stylesheet" href="style.css"><link rel="stylesheet" href="menu.css"><link rel="stylesheet" href="meta.css"></head>
|
||||
<body><div id="boot" role="status" aria-live="polite"><div class="boot-card"><strong class="boot-logo">AETHER<br><em>CLASH</em></strong><div class="boot-bar"><i></i></div><span class="boot-status">Menyiapkan…</span></div></div><script src="precache.js"></script><script src="preload.js"></script><main id="game-shell" aria-label="Aether Clash, demo game fighting Mecha dan Demi-Human">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#101923"><script>/* Phones/tablets: mark .touch and open the landscape mobile shell (mobile.html). ?touch=1 forces it on desktop, ?touch=0 turns it off. */(()=>{const q=new URLSearchParams(location.search),f=q.get('touch'),t=f?f==='1':matchMedia('(pointer: coarse)').matches||(navigator.maxTouchPoints>0&&/Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent));if(!t)return;document.documentElement.classList.add('touch');if(window.top===window&&!q.has('embed'))location.replace('mobile.html'+location.search+location.hash);})();</script><title>Konoha Fighting</title><link rel="preload" href="assets/fonts/Rajdhani-Bold.ttf" as="font" type="font/ttf" crossorigin><link rel="stylesheet" href="style.css"><link rel="stylesheet" href="menu.css"><link rel="stylesheet" href="meta.css"></head>
|
||||
<body><div id="boot" role="status" aria-live="polite"><div class="boot-card"><strong class="boot-logo">KONOHA<br><em>FIGHTING</em></strong><div class="boot-bar"><i></i></div><span class="boot-status">Menyiapkan…</span></div></div><script src="precache.js"></script><script src="preload.js"></script><main id="game-shell" aria-label="Konoha Fighting, game fighting parodi">
|
||||
<canvas id="arena" tabindex="0" aria-label="Arena pertarungan. WASD bergerak, Spasi menyerang, I O P untuk skill."></canvas><div class="vignette" aria-hidden="true"></div>
|
||||
<header class="combat-header">
|
||||
<section class="fighter fighter-one" aria-label="Status pemain">
|
||||
|
|
@ -19,10 +19,10 @@
|
|||
<button class="skill-card " data-action="skill2" aria-label="Seismic Drive, tombol O"><span class="skill-art"><img src="assets/ui/skill2.webp" alt=""><span class="cooldown-wipe" aria-hidden="true"></span><kbd>O</kbd></span><span class="skill-name">SEISMIC DRIVE</span></button>
|
||||
<button class="skill-card ultimate" data-action="ultimate" aria-label="Helios Squadron, tombol P"><span class="skill-art"><img src="assets/ui/squadron-icon.webp" alt=""><span class="cooldown-wipe" aria-hidden="true"></span><kbd>P</kbd></span><span class="skill-name">HELIOS SQUADRON</span></button>
|
||||
</div></section><div class="stage-sign"><span id="stage-name">BELLORA</span><small id="stage-subtitle">COURTYARD</small><i></i></div><section class="training-data" aria-label="Hasil latihan"><div><span>TOTAL DAMAGE</span><strong id="total-damage">0 <small>DMG</small></strong></div><div><span>BEST CHAIN</span><strong id="best-combo">0 <small>HITS</small></strong></div><small class="reset-hint">R · RESET<span>ESC · PAUSE</span></small></section></footer>
|
||||
<section id="touch-controls" aria-hidden="true"><div class="dpad"><button class="dbtn dbtn-up" data-key="w" tabindex="-1" aria-label="Atas / lompat"><i></i></button><button class="dbtn dbtn-left" data-key="a" tabindex="-1" aria-label="Kiri"><i></i></button><button class="dbtn dbtn-down" data-key="s" tabindex="-1" aria-label="Bawah / jongkok"><i></i></button><button class="dbtn dbtn-right" data-key="d" tabindex="-1" aria-label="Kanan"><i></i></button></div><div class="action-pad"><button class="tbtn tbtn-attack" data-key=" " data-slot="attack" tabindex="-1"><img src="assets/ui/attack.webp" alt=""></button><button class="tbtn tbtn-skill tbtn-s1" data-key="i" data-slot="skill1" tabindex="-1"><img src="assets/ui/skill1.webp" alt=""><span class="cooldown-wipe"></span><em class="cooldown-num"></em></button><button class="tbtn tbtn-skill tbtn-s2" data-key="o" data-slot="skill2" tabindex="-1"><img src="assets/ui/skill2.webp" alt=""><span class="cooldown-wipe"></span><em class="cooldown-num"></em></button><button class="tbtn tbtn-skill tbtn-ult" data-key="p" data-slot="ultimate" tabindex="-1"><img src="assets/ui/squadron-icon.webp" alt=""><span class="cooldown-wipe"></span><em class="cooldown-num"></em></button></div></section>
|
||||
<div id="load-state" class="load-state"><span class="loading-glyph">✦</span><strong>AETHER CLASH</strong><span id="load-message">Menyiapkan arena…</span></div>
|
||||
<dialog id="settings-panel"><div class="dialog-kicker">AETHER CLASH / SETTINGS</div><h2>Pengaturan arena</h2><p>Atur audio, tampilan, dan kontrol permainan.</p><label class="setting-row training-only"><span>Karakter pemain<small>Mengganti karakter memulai ulang latihan</small></span><select id="character-select"><option value="browo">BRO WO · Human</option><option value="gufron">GUFRON · Human</option><option value="soloman">SOLO MAN · Human</option><option value="anice">A NICE · Human</option><option value="ethanolman">ETHANOL MAN · Human</option><option value="sonofsolo">SON OF SOLO MAN · Human</option><option value="megachan">MEGA CHAN · Human</option></select></label><label class="setting-row training-only"><span>Lawan aktif<small>Aktifkan serangan CPU saat latihan</small></span><input type="checkbox" id="ai-toggle" checked></label><label class="setting-row"><span>Visualisasi hitbox<small>Area tubuh dan jangkauan serangan</small></span><input type="checkbox" id="hitbox-toggle"></label><label class="setting-row"><span>Audio aktif<small>Suara karakter, benturan, dan skill</small></span><input type="checkbox" id="sound-toggle" checked></label><label class="setting-row"><span>Musik latar<small>Midday Showdown, diputar berulang di menu dan arena</small></span><input type="checkbox" id="music-toggle" checked></label><label class="volume-row"><span>Musik</span><input type="range" id="music-slider" min="0" max="100" value="5" aria-label="Volume musik"><output id="music-value">5%</output></label><label class="setting-row"><span>Animasi latar menu<small>Video loop pada main menu</small></span><input type="checkbox" id="menu-video-toggle" checked></label><label class="volume-row"><span>Suara</span><input type="range" id="volume-slider" min="0" max="100" value="45" aria-label="Volume audio"><output id="volume-value">45%</output></label><div class="dialog-buttons"><button id="sound-test">Tes audio</button><button id="fullscreen-btn">Layar penuh</button></div><details class="controls-help"><summary>Kontrol keyboard</summary><p>A / D · Gerak 2× A / D · Lari<br>W · Lompat S · Menunduk / turun cepat<br>2× S · Salto SPACE · Kombo 3 pukulan<br>I / O · Skill P · Ultimate</p></details><button class="primary-button" id="close-settings">Kembali ke arena <span>↗</span></button></dialog>
|
||||
<dialog id="pause-panel"><div class="dialog-kicker">AETHER CLASH / PAUSE</div><h2>PAUSED</h2><p>Pertarungan dijeda. Siap melanjutkan?</p><button class="primary-button" id="resume-btn">Lanjut bertarung <span>↗</span></button><button class="text-button" id="restart-btn">Mulai ulang</button><button class="text-button" id="back-menu">Kembali ke main menu</button></dialog>
|
||||
</main><section id="front-menu" aria-label="Main menu Aether Clash"></section><script src="assets/mecha/manifest.js"></script><script src="assets/stage-config.js"></script><script src="assets/ui/drone-config.js"></script><script src="assets/fenr/human/manifest.js"></script><script src="assets/fenr/wolf/manifest.js"></script><script src="fenr.js"></script><script src="assets/browo/manifest.js"></script><script src="browo.js"></script><script src="assets/megachan/manifest.js"></script><script src="assets/sonofsolo/manifest.js"></script><script src="assets/ethanolman/manifest.js"></script><script src="assets/anice/manifest.js"></script><script src="assets/soloman/manifest.js"></script><script src="assets/gufron/manifest.js"></script><script src="assets/audio/announcer/manifest.js"></script><script src="announcer.js"></script><script src="match.js"></script><script>window.FRONTEND_ENABLED=true;</script><script src="game.js"></script><script src="touch.js"></script><script src="menu.js"></script><script src="meta.js"></script></body></html>
|
||||
<section id="touch-controls" aria-hidden="true"><div class="dpad"><button class="dbtn dbtn-up" data-key="w" tabindex="-1" aria-label="Atas / lompat"><i></i></button><button class="dbtn dbtn-left" data-key="a" tabindex="-1" aria-label="Kiri"><i></i></button><button class="dbtn dbtn-down" data-key="s" tabindex="-1" aria-label="Bawah / jongkok"><i></i></button><button class="dbtn dbtn-right" data-key="d" tabindex="-1" aria-label="Kanan"><i></i></button></div><div class="action-pad"><button class="tbtn tbtn-attack" data-key=" " data-slot="attack" tabindex="-1"><img src="assets/ui/attack.webp" alt=""></button><button class="tbtn tbtn-skill tbtn-s1" data-key="i" data-slot="skill1" tabindex="-1"><img src="assets/ui/skill1.webp" alt=""><span class="cooldown-wipe"></span><em class="cooldown-num"></em></button><button class="tbtn tbtn-skill tbtn-s2" data-key="o" data-slot="skill2" tabindex="-1"><img src="assets/ui/skill2.webp" alt=""><span class="cooldown-wipe"></span><em class="cooldown-num"></em></button><button class="tbtn tbtn-skill tbtn-ult" data-key="p" data-slot="ultimate" tabindex="-1"><img src="assets/ui/squadron-icon.webp" alt=""><span class="cooldown-wipe"></span><em class="cooldown-num"></em></button></div><div class="extra-pad"><button class="tbtn tbtn-x" data-key="cmd:uppercut" tabindex="-1">UP</button><button class="tbtn tbtn-x" data-key="cmd:lunge" tabindex="-1">→P</button><button class="tbtn tbtn-x" data-key="k" tabindex="-1">K</button><button class="tbtn tbtn-x" data-key="l" tabindex="-1">BLOK</button><button class="tbtn tbtn-x" data-key="j" tabindex="-1">LEMPAR</button></div></section>
|
||||
<div id="load-state" class="load-state"><span class="loading-glyph">✦</span><strong>KONOHA FIGHTING</strong><span id="load-message">Menyiapkan arena…</span></div>
|
||||
<dialog id="settings-panel"><div class="dialog-kicker">KONOHA FIGHTING / SETTINGS</div><h2>Pengaturan arena</h2><p>Atur audio, tampilan, dan kontrol permainan.</p><label class="setting-row training-only"><span>Karakter pemain<small>Mengganti karakter memulai ulang latihan</small></span><select id="character-select"><option value="browo">BRO WO · Human</option><option value="gufron">GUFRON · Human</option><option value="soloman">SOLO MAN · Human</option><option value="anice">A NICE · Human</option><option value="ethanolman">ETHANOL MAN · Human</option><option value="sonofsolo">SON OF SOLO MAN · Human</option><option value="megachan">MEGA CHAN · Human</option></select></label><label class="setting-row training-only"><span>Lawan aktif<small>Aktifkan serangan CPU saat latihan</small></span><input type="checkbox" id="ai-toggle" checked></label><label class="setting-row"><span>Visualisasi hitbox<small>Area tubuh dan jangkauan serangan</small></span><input type="checkbox" id="hitbox-toggle"></label><label class="setting-row"><span>Audio aktif<small>Suara karakter, benturan, dan skill</small></span><input type="checkbox" id="sound-toggle" checked></label><label class="setting-row"><span>Musik latar<small>Midday Showdown, diputar berulang di menu dan arena</small></span><input type="checkbox" id="music-toggle" checked></label><label class="volume-row"><span>Musik</span><input type="range" id="music-slider" min="0" max="100" value="5" aria-label="Volume musik"><output id="music-value">5%</output></label><label class="setting-row"><span>Animasi latar menu<small>Video loop pada main menu</small></span><input type="checkbox" id="menu-video-toggle" checked></label><label class="volume-row"><span>Suara</span><input type="range" id="volume-slider" min="0" max="100" value="45" aria-label="Volume audio"><output id="volume-value">45%</output></label><div class="dialog-buttons"><button id="sound-test">Tes audio</button><button id="fullscreen-btn">Layar penuh</button></div><details class="controls-help"><summary>Kontrol keyboard</summary><p>A / D · Gerak 2× A / D · Lari<br>W · Lompat S · Menunduk / turun cepat<br>2× S · Salto SPACE · Kombo 3 pukulan<br>I / O · Skill P · Ultimate</p></details><button class="primary-button" id="close-settings">Kembali ke arena <span>↗</span></button></dialog>
|
||||
<dialog id="pause-panel"><div class="dialog-kicker">KONOHA FIGHTING / PAUSE</div><h2>PAUSED</h2><p>Pertarungan dijeda. Siap melanjutkan?</p><button class="primary-button" id="resume-btn">Lanjut bertarung <span>↗</span></button><button class="text-button" id="restart-btn">Mulai ulang</button><button class="text-button" id="back-menu">Kembali ke main menu</button></dialog>
|
||||
</main><section id="front-menu" aria-label="Main menu Konoha Fighting"></section><script src="assets/mecha/manifest.js"></script><script src="assets/stage-config.js"></script><script src="assets/ui/drone-config.js"></script><script src="assets/fenr/human/manifest.js"></script><script src="assets/fenr/wolf/manifest.js"></script><script src="fenr.js"></script><script src="assets/browo/manifest.js"></script><script src="browo.js"></script><script src="assets/megachan/manifest.js"></script><script src="assets/sonofsolo/manifest.js"></script><script src="assets/ethanolman/manifest.js"></script><script src="assets/anice/manifest.js"></script><script src="assets/soloman/manifest.js"></script><script src="assets/gufron/manifest.js"></script><script src="assets/audio/announcer/manifest.js"></script><script src="announcer.js"></script><script src="match.js"></script><script>window.FRONTEND_ENABLED=true;</script><script src="game.js"></script><script src="touch.js"></script><script src="menu.js"></script><script src="meta.js"></script></body></html>
|
||||
|
||||
|
||||
|
|
|
|||
20
menu.js
20
menu.js
|
|
@ -28,7 +28,7 @@
|
|||
// 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;
|
||||
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)');
|
||||
let backgroundEnabled=!reducedMotion?.matches,backgroundVideo=null,videoPlayToken=0;
|
||||
const backgroundToggle=document.querySelector('#menu-video-toggle');
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
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='<span class="wordmark">AETHER<span>CLASH</span><i>✦</i></span>';
|
||||
const brand='<span class="wordmark">KONOHA<span>FIGHTING</span><i>✦</i></span>';
|
||||
const footer=()=>'<footer class="menu-footer"><span><kbd>W A S D</kbd> / <kbd>↑ ↓ ← →</kbd> SELECT <kbd>ENTER</kbd> CONFIRM <kbd>ESC</kbd> BACK</span><span>LOCAL PLAY <i></i> BUILD 02</span></footer>';
|
||||
function navHeader(title,subtitle){return `<header class="select-header"><button class="menu-back" data-cmd="back" aria-label="Kembali">← <span>BACK</span></button><div><span class="eyebrow">${subtitle}</span><h1>${title}</h1></div>${brand}</header>`;}
|
||||
function fighterPanel(id,side){const f=info(id);return `<div class="fighter-plinth ${side} ${f?'':'unknown'} ${soon(id)?'soon':''}"><div class="plinth-light"></div><span class="side-tag">${side==='player'?'P1 / YOUR FIGHTER':'CPU / YOUR RIVAL'}</span><img class="selection-art" src="${f?.art||'assets/menu/locked.svg'}" alt="${f?.name||'Karakter belum dipilih'}"><div class="fighter-copy"><span>${f?.tag||'AWAITING CHALLENGER'}</span><h2>${f?.name||'???'}</h2><p>${f?.race||'ROSTER EXPANDING'}</p><small>${f?.detail||'A new challenger is on the way.'}</small></div></div>`;}
|
||||
|
|
@ -65,14 +65,14 @@
|
|||
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=`<div class="home-art video-backed"></div><div class="menu-grain"></div><header class="home-top"><span></span><span class="home-top-right"><span class="build-tag">FIGHTING DEMO / VOL. 02</span>${canFullscreen?`<button class="menu-fullscreen${fullscreenHint?' hinted':''}" data-cmd="fullscreen" aria-label="Layar penuh" title="Layar penuh"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg></button>`:''}</span></header><div class="home-content"><div class="home-title"><h1>AETHER<br><em>CLASH</em><i>✦</i></h1><p>Two factions. One arena.</p></div><nav class="home-nav" aria-label="Main menu"><button data-cmd="versus"><span class="nav-index">01</span><span>VS COMPUTER<small>Choose your rival. Claim the arena.</small></span><b>↗</b></button><button data-cmd="training"><span class="nav-index">02</span><span>TRAINING<small>Learn your fighter. Refine your combo.</small></span><b>↗</b></button><button data-cmd="shop" class="meta-nav"><span class="nav-index">03</span><span>TOKO<small>Beli jurus dan skin dengan koin.</small></span><b>◆</b></button><button data-cmd="locker" class="meta-nav"><span class="nav-index">04</span><span>KARAKTER<small>Atur skin, jurus toko, daftar gerakan.</small></span><b>↗</b></button><div class="home-row"><button data-cmd="book" class="meta-nav"><span>KOLEKSI</span><b>★</b></button><button data-cmd="settings"><span>PENGATURAN</span></button><button data-cmd="about"><span>ABOUT</span></button></div></nav></div><div class="home-caption"><span>MECHA</span><i>VS</i><span>DEMI-HUMAN</span><small>TECHNOLOGY MEETS INSTINCT</small></div><footer class="home-footer"><span></span><span id="menu-ready">${game.snapshot().ready?'READY TO FIGHT':'PREPARING ARENA…'}</span></footer>`;
|
||||
root.innerHTML=`<div class="home-art video-backed"></div><div class="menu-grain"></div><header class="home-top"><span></span><span class="home-top-right"><span class="build-tag">PARODY FIGHTER / VOL. 03</span>${canFullscreen?`<button class="menu-fullscreen${fullscreenHint?' hinted':''}" data-cmd="fullscreen" aria-label="Layar penuh" title="Layar penuh"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg></button>`:''}</span></header><div class="home-content"><div class="home-title"><h1>KONOHA<br><em>FIGHTING</em><i>✦</i></h1><p>Parodi tokoh publik. Satu arena.</p></div><nav class="home-nav" aria-label="Main menu"><button data-cmd="versus"><span class="nav-index">01</span><span>VS COMPUTER<small>Choose your rival. Claim the arena.</small></span><b>↗</b></button><button data-cmd="training"><span class="nav-index">02</span><span>TRAINING<small>Learn your fighter. Refine your combo.</small></span><b>↗</b></button><div class="home-row"><button data-cmd="arcade"><span>ARCADE</span></button><button data-cmd="survival"><span>SURVIVAL</span></button><button data-cmd="2p"><span>2 PEMAIN</span></button></div><button data-cmd="shop" class="meta-nav"><span class="nav-index">03</span><span>TOKO<small>Beli jurus dan skin dengan koin.</small></span><b>◆</b></button><button data-cmd="locker" class="meta-nav"><span class="nav-index">04</span><span>KARAKTER<small>Atur skin, jurus toko, daftar gerakan.</small></span><b>↗</b></button><div class="home-row"><button data-cmd="book" class="meta-nav"><span>KOLEKSI</span><b>★</b></button><button data-cmd="settings"><span>PENGATURAN</span></button><button data-cmd="about"><span>ABOUT</span></button></div></nav></div><div class="home-caption"><span>BRO WO</span><i>VS</i><span>SEMUANYA</span><small>PARODI · SATIRE · JANGAN DIANGGAP SERIUS</small></div><footer class="home-footer"><span></span><span id="menu-ready">${game.snapshot().ready?'READY TO FIGHT':'PREPARING ARENA…'}</span></footer>`;
|
||||
} else if(screen==='select'){
|
||||
root.innerHTML=`<div class="selection-backdrop"></div>${navHeader(step===0?'SELECT YOUR FIGHTER':'SELECT YOUR RIVAL',mode==='versus'?'VERSUS COMPUTER':'TRAINING ROOM')}<div class="selection-steps"><span class="${step===0?'current':'done'}">01 <b>PLAYER</b></span><i></i><span class="${step===1?'current':''}">02 <b>RIVAL</b></span><i></i><span>03 <b>ARENA</b></span></div><div class="select-stage"><div id="player-preview">${fighterPanel(step===0?hover:player,'player')}</div><div class="roster-center"><div class="roster-caption"><strong>${step===0?'PLAYER 1':'COMPUTER'}</strong><span>CHOOSE A FIGHTER</span></div><div class="roster-grid" role="group" aria-label="Roster karakter">${roster.map((id,i)=>{const f=info(id);return `<button class="roster-tile ${id===hover?'highlight':''} ${!f?'locked':''} ${soon(id)?'soon':''}" data-fighter="${id}" aria-label="${f?f.name+' — '+f.race+(soon(id)?', belum bisa dimainkan':''):'Slot '+(i+1)+' terkunci'}" aria-disabled="${!fighters[id]}"><img class="${f?'portrait-image':''}" data-portrait-side="${step===1?'enemy':'player'}" src="${f?.portrait||'assets/menu/locked.svg'}" alt=""><span>${f?.name||'LOCKED'}</span><b>${step===1&&id===player?'P1':''}</b></button>`;}).join('')}</div><div id="fighter-moves" class="fighter-moves"></div><button class="menu-primary confirm-fighter" data-cmd="confirm">${step===0?'CONFIRM FIGHTER':'CONFIRM RIVAL'} <b>→</b></button><p id="roster-notice" class="roster-notice" aria-live="polite">${roster.length>opened.length?(roster.length-opened.length)+' challenger slots reserved.':''}</p></div><div id="enemy-preview">${fighterPanel(step===1?hover:null,'enemy')}</div></div>${footer()}`;
|
||||
root.innerHTML=`<div class="selection-backdrop"></div>${navHeader(step===0?'SELECT YOUR FIGHTER':'SELECT YOUR RIVAL',mode==='versus'?(runKind?runKind.toUpperCase():p2?'DUA PEMAIN':'VERSUS COMPUTER'):'TRAINING ROOM')}<div class="selection-steps"><span class="${step===0?'current':'done'}">01 <b>PLAYER</b></span><i></i><span class="${step===1?'current':''}">02 <b>RIVAL</b></span><i></i><span>03 <b>ARENA</b></span></div><div class="select-stage"><div id="player-preview">${fighterPanel(step===0?hover:player,'player')}</div><div class="roster-center"><div class="roster-caption"><strong>${step===0?'PLAYER 1':'COMPUTER'}</strong><span>CHOOSE A FIGHTER</span></div><div class="roster-grid" role="group" aria-label="Roster karakter">${roster.map((id,i)=>{const f=info(id);return `<button class="roster-tile ${id===hover?'highlight':''} ${!f?'locked':''} ${soon(id)?'soon':''}" data-fighter="${id}" aria-label="${f?f.name+' — '+f.race+(soon(id)?', belum bisa dimainkan':''):'Slot '+(i+1)+' terkunci'}" aria-disabled="${!fighters[id]}"><img class="${f?'portrait-image':''}" data-portrait-side="${step===1?'enemy':'player'}" src="${f?.portrait||'assets/menu/locked.svg'}" alt=""><span>${f?.name||'LOCKED'}</span><b>${step===1&&id===player?'P1':''}</b></button>`;}).join('')}</div><div id="fighter-moves" class="fighter-moves"></div><button class="menu-primary confirm-fighter" data-cmd="confirm">${step===0?'CONFIRM FIGHTER':'CONFIRM RIVAL'} <b>→</b></button><p id="roster-notice" class="roster-notice" aria-live="polite">${roster.length>opened.length?(roster.length-opened.length)+' challenger slots reserved.':''}</p></div><div id="enemy-preview">${fighterPanel(step===1?hover:null,'enemy')}</div></div>${footer()}`;
|
||||
updatePreview(false);
|
||||
} else if(screen==='arena'){
|
||||
const st=MatchRules.stages[stage];root.innerHTML=`<div class="arena-screen-bg" style="background-image:url('${st.image}')"></div>${navHeader('SET THE STAGE',mode==='versus'?'VERSUS COMPUTER / MATCH SETUP':'TRAINING / ARENA SETUP')}<div class="arena-content"><div class="versus-strip"><div><img class="portrait-image" data-portrait-side="player" src="${fighters[player].portrait}" alt=""><span>P1<strong>${fighters[player].name}</strong></span></div><b>VS</b><div><span>CPU<strong>${fighters[enemy].name}</strong></span><img class="portrait-image" data-portrait-side="enemy" src="${fighters[enemy].portrait}" alt=""></div></div><div class="arena-window"><span class="arena-number">${String(Object.keys(MatchRules.stages).indexOf(stage)+1).padStart(2,'0')} / ${String(Object.keys(MatchRules.stages).length).padStart(2,'0')}</span><div><small>${st.tag}</small><h2>${st.name}</h2></div><span class="arena-day">✦ ${st.time||'SUNNY DAYLIGHT'}</span></div><div class="arena-tiles" role="group" aria-label="Pilih arena">${Object.entries(MatchRules.stages).map(([id,a])=>`<button class="arena-tile ${id===stage?'selected':''}" data-stage="${id}" aria-pressed="${id===stage}"><img src="${a.image}" alt=""><span>${a.name}</span></button>`).join('')}</div><div class="match-options"><div class="difficulty-area"><span class="eyebrow">${mode==='versus'?'CPU DIFFICULTY':'PRACTICE MODE'}</span>${mode==='versus'?`<div class="difficulty-buttons" role="group" aria-label="Tingkat kesulitan">${Object.entries(MatchRules.difficulties).map(([id,d],i)=>`<button data-level="${id}" class="${id===level?'selected':''}" aria-pressed="${id===level}"><i>${'▰'.repeat(i+1)}</i>${d.label}</button>`).join('')}</div>`:'<p class="training-note">Unlimited time · Passive rival · Reset anytime</p>'}</div><div class="match-rules">${mode==='versus'?'<b>FIRST TO 2 WINS</b><span>BEST OF 3 ROUNDS · 90 SECONDS</span>':'<b>MAKE EVERY HIT COUNT</b><span>COMBO TRACKER · FULL MOVE SET</span>'}</div></div><button class="menu-primary start-battle" data-cmd="start" ${game.snapshot().ready?'':'disabled'}>${game.snapshot().ready?(mode==='versus'?'LET’S FIGHT':'ENTER TRAINING'):'PREPARING ARENA…'} <b>↗</b></button><p id="setup-error" role="status"></p></div>${footer()}`;
|
||||
} else if(screen==='about'){
|
||||
root.innerHTML=`<div class="home-art dim"></div>${navHeader('ABOUT AETHER CLASH','MECHA VS DEMI-HUMAN')}<article class="about-panel"><span class="eyebrow">CHRONICLES OF AETHER</span><h2>Steel meets<br><em>the untamed.</em></h2><p>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.</p><div class="about-fighters"><p><b>MECHA</b><span>Perpaduan manusia dan teknologi. Rekayasa mekanis, persenjataan aether, dan teknik bertarung yang beragam.</span></p><p><b>DEMI-HUMAN</b><span>Perpaduan manusia dan hewan. Setiap ras memiliki anatomi, naluri, dan kekuatan khasnya sendiri.</span></p></div><div class="about-stats"><span><b>02</b>FACTIONS</span><span><b>12</b>ROSTER SLOTS</span><span><b>∞</b>RIVALRIES</span></div><small>Visuals · Higgsfield Voices · ElevenLabs Typography · Rajdhani<br>Local fighting demo · Single player</small><button class="menu-primary" data-cmd="home">BACK TO MAIN MENU <b>↗</b></button></article>`;
|
||||
root.innerHTML=`<div class="home-art dim"></div>${navHeader('TENTANG GAME','KONOHA FIGHTING')}<article class="about-panel"><span class="eyebrow">GAME PARODI</span><h2>Tujuh petarung.<br><em>Satu arena.</em></h2><p>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.</p><div class="about-fighters"><p><b>MODE</b><span>VS Komputer, Arcade, Survival, Dua Pemain, dan Latihan.</span></p><p><b>MEKANIK</b><span>Kombo, gerakan perintah, blok, parry, lempar, jurus toko, skin, dan pencapaian.</span></p></div><div class="about-stats"><span><b>07</b>PETARUNG</span><span><b>05</b>SLOT MISTERI</span><span><b>∞</b>RIVALRIES</span></div><small>Berbasis AETHER CLASH oleh Bang Tutorial (kode MIT, aset CC BY-NC) · Typography · Rajdhani<br>Hanya untuk penggunaan non-komersial</small><button class="menu-primary" data-cmd="home">BACK TO MAIN MENU <b>↗</b></button></article>`;
|
||||
} else if(screen==='result'){
|
||||
const won=result.winner==='player',winner=won?result.player:result.enemy;root.innerHTML=`<div class="result-bg"></div>${navHeader('MATCH COMPLETE',MatchRules.stages[result.stage].name.toUpperCase())}<div class="result-layout"><img class="result-fighter" src="${fighters[winner].art}" alt="${fighters[winner].name}"><div class="result-copy"><span class="eyebrow">${won?'PLAYER 1':'COMPUTER'} TAKES THE MATCH</span><h1>${won?'VICTORY':'DEFEAT'}</h1><p>${fighters[winner].name} WINS</p><div class="final-score"><span>${result.playerWins}</span><i>—</i><span>${result.enemyWins}</span></div><small>${fighters[result.player].name} vs ${fighters[result.enemy].name} · ${MatchRules.difficulties[result.level].label}</small><button class="menu-primary" data-cmd="rematch">REMATCH <b>↗</b></button><button class="result-secondary" data-cmd="reselect">CHARACTER SELECT</button><button class="result-secondary" data-cmd="home">MAIN MENU</button></div></div>`;
|
||||
}
|
||||
|
|
@ -90,15 +90,17 @@
|
|||
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);
|
||||
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(){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 start(){window.Meta?.cancelRun?.();if(game.startMatch({player,enemy,stage,level,mode})){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);}}
|
||||
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==='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')window.Meta?.open(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'){({player,enemy,stage,level,mode}=result);start();}
|
||||
|
|
@ -124,7 +126,7 @@
|
|||
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';}}}};
|
||||
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);
|
||||
})();
|
||||
|
|
|
|||
4
meta.css
4
meta.css
|
|
@ -32,3 +32,7 @@
|
|||
@media(max-width:700px){.locker{grid-template-columns:1fr}}
|
||||
|
||||
.home-nav .home-row{display:flex;gap:8px}.home-nav .home-row button{flex:1;justify-content:center;gap:8px;padding:9px 8px}.home-nav .home-row button>span:nth-child(1){font-size:17px;letter-spacing:.05em}
|
||||
|
||||
.extra-pad{position:absolute;right:calc(16px + env(safe-area-inset-right));bottom:calc(212px + env(safe-area-inset-bottom));display:grid;grid-template-columns:repeat(3,auto);gap:6px;pointer-events:none}
|
||||
.tbtn-x{position:static!important;width:46px;height:46px;font:700 11px Rajdhani,system-ui,sans-serif;color:#fff;letter-spacing:.04em}
|
||||
@media(max-height:360px){.extra-pad{bottom:calc(178px + env(safe-area-inset-bottom))}.tbtn-x{width:38px;height:38px;font-size:10px}}
|
||||
|
|
|
|||
86
meta.js
86
meta.js
|
|
@ -24,8 +24,20 @@
|
|||
['↓ → + SPASI', 'TINJU MAJU', 'Maju sambil memukul, jangkauan panjang.'],
|
||||
['SPASI x3', 'KOMBO', 'Rantai tiga pukulan; pukulan ketiga paling kuat.'],
|
||||
['W W', 'LOMPAT GANDA', 'Lompat kedua di udara (berputar).'],
|
||||
['K', 'JURUS TOKO', 'Memakai jurus toko yang dipasang di menu Karakter.']
|
||||
['K', 'JURUS TOKO', 'Memakai jurus toko yang dipasang di menu Karakter.'],
|
||||
['TAHAN L', 'BLOK', 'Mengurangi damage 80%. Terlalu lama diserang = guard pecah.'],
|
||||
['KETUK L', 'PARRY', 'Ketuk tepat sebelum kena: tanpa damage dan lawan terhenti.'],
|
||||
['J', 'LEMPAR', 'Menembus blok. Harus dekat dan di tanah.']
|
||||
];
|
||||
const ALT_PRICE = 450;
|
||||
function altOptions(id) {
|
||||
const f = window.FIGHTERS?.[id]; if (!f) return [];
|
||||
return ['skill1', 'skill2'].flatMap((slot, i) => {
|
||||
const kind = f.skills?.[slot]?.kind, proj = ['projectile', 'wave', 'homing'].includes(kind), nm = f.names[i + 1];
|
||||
return [{ key: `${id}:${slot}:ganda`, slot, v: 'ganda', name: nm + (proj ? ' GANDA' : ' CEPAT'), desc: proj ? 'Menembak dua kali lipat dengan damage lebih kecil per peluru.' : 'Lebih cepat dan cooldown pendek, damage lebih kecil.' },
|
||||
{ key: `${id}:${slot}:berat`, slot, v: 'berat', name: nm + ' BERAT', desc: 'Lebih besar dan keras, tetapi lebih lambat dan cooldown panjang.' }];
|
||||
});
|
||||
}
|
||||
const ACH = {
|
||||
first_win: ['PERTAMA KALI', 'Menangkan satu pertandingan.'],
|
||||
perfect: ['BERSIH', 'Menang tanpa kehilangan satu ronde pun.'],
|
||||
|
|
@ -41,9 +53,14 @@
|
|||
mbg: ['TRUK LEWAT', 'Panggil truk katering.'],
|
||||
esemka: ['KONVOI', 'Panggil konvoi.'],
|
||||
penasaran: ['PENASARAN', 'Ketuk slot tanda tanya berulang kali.'],
|
||||
logo: ['TELITI', 'Temukan rahasia di judul.']
|
||||
logo: ['TELITI', 'Temukan rahasia di judul.'],
|
||||
parry: ['TEPAT WAKTU', 'Lakukan parry yang berhasil.'],
|
||||
arcade_clear: ['TAMAT', 'Selesaikan mode Arcade.'],
|
||||
survival5: ['BERTAHAN', 'Menang 5 kali berturut-turut di Survival.'],
|
||||
twoplayer: ['TEMAN BERMAIN', 'Mainkan satu pertandingan dua pemain.'],
|
||||
special: ['SPESIALIS', 'Beli satu varian skill karakter.']
|
||||
};
|
||||
const blank = () => ({ v: 1, coins: 0, earned: 0, owned: { skin: ['standar'], move: [] }, skin: {}, move: null, stats: { wins: 0, losses: 0, bestCombo: 0, perfects: 0 }, ach: {}, eggs: {} });
|
||||
const blank = () => ({ v: 1, coins: 0, earned: 0, voice: true, owned: { skin: ['standar'], move: [], alt: [] }, skin: {}, move: null, alt: {}, best: { survival: 0 }, stats: { wins: 0, losses: 0, bestCombo: 0, perfects: 0 }, ach: {}, eggs: {} });
|
||||
let save = blank();
|
||||
try { const raw = JSON.parse(localStorage.getItem(KEY) || 'null'); if (raw && raw.v === 1) save = { ...blank(), ...raw, owned: { ...blank().owned, ...raw.owned }, stats: { ...blank().stats, ...raw.stats } }; } catch (_) {}
|
||||
const persist = () => { try { localStorage.setItem(KEY, JSON.stringify(save)); } catch (_) {} };
|
||||
|
|
@ -66,6 +83,14 @@
|
|||
|
||||
// ---- engine API ----
|
||||
function skinFilter(id) { const s = SKINS[save.skin[id] || 'standar']; return s && save.owned.skin.includes(save.skin[id] || 'standar') ? s.filter : ''; }
|
||||
function altSkill(id, slot) { const v = save.alt?.[id]?.[slot]; return v && save.owned.alt.includes(`${id}:${slot}:${v}`) ? v : null; }
|
||||
// Shouts use the browser's own text-to-speech (generic Indonesian voice), never an imitation of anyone's voice.
|
||||
let lastShout = 0;
|
||||
function shout(text) {
|
||||
if (!save.voice || !text || !window.speechSynthesis || Date.now() - lastShout < 900) return; lastShout = Date.now();
|
||||
try { speechSynthesis.cancel(); const u = new SpeechSynthesisUtterance(String(text).toLowerCase()); u.lang = 'id-ID'; u.rate = 1.15; u.pitch = 1.25; u.volume = .9; speechSynthesis.speak(u); } catch (_) {}
|
||||
}
|
||||
function event(name) { if (name === 'parry') unlock('parry'); if (name === 'twoplayer') unlock('twoplayer'); }
|
||||
function equippedMove() { return save.move && save.owned.move.includes(save.move) ? save.move : null; }
|
||||
let chip;
|
||||
function hud(info) {
|
||||
|
|
@ -88,6 +113,42 @@
|
|||
save.stats.bestCombo = Math.max(save.stats.bestCombo, r.bestCombo || 0);
|
||||
if ((r.bestCombo || 0) >= 5) unlock('combo5'); if ((r.bestCombo || 0) >= 10) unlock('combo10');
|
||||
addCoins(gain); toast('KOIN +' + gain + ' (' + parts.join(', ') + ')', 'coin');
|
||||
return run ? advanceRun(won) : false;
|
||||
}
|
||||
// ---- Arcade (fight everyone once, one life) and Survival (endless, harder each win) ----
|
||||
let run = null;
|
||||
const shuffle = a => { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; };
|
||||
function startRun(kind, player) {
|
||||
run = { kind, player, wins: 0, queue: kind === 'arcade' ? shuffle(roster().filter(x => x !== player)) : [] };
|
||||
nextFight();
|
||||
}
|
||||
function nextFight() {
|
||||
const g = window.__game, stages = Object.keys(window.MatchRules?.stages || { bellora: 1 });
|
||||
const enemy = run.kind === 'arcade' ? run.queue[run.wins] : shuffle(roster().filter(x => x !== run.player))[0];
|
||||
const level = run.kind === 'arcade' ? 'medium' : run.wins < 3 ? 'easy' : run.wins < 6 ? 'medium' : run.wins < 9 ? 'hard' : 'excellent';
|
||||
const total = run.kind === 'arcade' ? '/' + run.queue.length : '';
|
||||
toast((run.kind === 'arcade' ? 'ARCADE' : 'SURVIVAL') + ' · LAWAN ' + (run.wins + 1) + total + ': ' + fname(enemy) + ' (' + level + ')');
|
||||
let ok = false;
|
||||
for (const st of shuffle(stages.slice()).concat('bellora')) { ok = g.startMatch({ player: run.player, enemy, stage: st, level, mode: 'versus' }); if (ok) break; }
|
||||
if (!ok) { toast('Aset belum siap, coba lagi.'); run = null; window.FrontEnd?.home(); }
|
||||
}
|
||||
function advanceRun(won) {
|
||||
if (won) {
|
||||
run.wins++;
|
||||
if (run.kind === 'arcade' && run.wins >= run.queue.length) { finishRun(true); return true; }
|
||||
if (run.kind === 'survival') { save.best.survival = Math.max(save.best.survival, run.wins); if (run.wins >= 5) unlock('survival5'); persist(); }
|
||||
setTimeout(() => { if (run) nextFight(); }, 2600); return true;
|
||||
}
|
||||
finishRun(false); return true;
|
||||
}
|
||||
function finishRun(cleared) {
|
||||
const r = run; run = null; let bonus = 0;
|
||||
if (r.kind === 'arcade' && cleared) { bonus = 500; unlock('arcade_clear'); } else if (r.kind === 'survival') bonus = r.wins * 30;
|
||||
if (bonus) addCoins(bonus);
|
||||
if (!root) build(); root.hidden = false;
|
||||
const title = r.kind === 'arcade' ? (cleared ? 'ARCADE TAMAT!' : 'ARCADE BERAKHIR') : 'SURVIVAL BERAKHIR';
|
||||
const sub = r.kind === 'arcade' ? (cleared ? 'Semua lawan dikalahkan.' : 'Kalah di lawan ke-' + (r.wins + 1) + ' dari ' + r.queue.length + '.') : 'Kemenangan beruntun: ' + r.wins + ' · rekor ' + save.best.survival;
|
||||
root.innerHTML = `<div class="meta-panel run-end"><h2>${title}</h2><p>${sub}</p><p class="meta-coin">${bonus ? 'Bonus ◆ +' + bonus : ''}</p><p><button class="mt on" data-runmenu>KE MENU UTAMA</button></p></div>`;
|
||||
}
|
||||
|
||||
// ---- UI ----
|
||||
|
|
@ -121,7 +182,9 @@
|
|||
const own = save.owned.move.includes(k);
|
||||
return `<article class="item move"><h4>${m.name}</h4><p>${m.desc}</p><small>Cooldown ${m.cd} detik · tombol K</small><button data-buy="move:${k}" ${own ? 'disabled' : ''}>${own ? 'DIMILIKI' : '◆ ' + m.price}</button></article>`;
|
||||
}).join('');
|
||||
return `<h3>Jurus toko</h3><div class="grid">${moves}</div><h3>Skin warna</h3><div class="grid">${skins}</div><p class="meta-hint">Koin didapat dari menang pertandingan, kombo, dan pencapaian. Pasang barang di menu KARAKTER.</p>`;
|
||||
const alts = altOptions(who).map(o => { const own = save.owned.alt.includes(o.key); return `<article class="item move"><h4>${esc(o.name)}</h4><p>${o.desc}</p><small>${fname(who)} · ${o.slot === 'skill1' ? 'tombol I' : 'tombol O'}</small><button data-buy="alt:${o.key}" ${own ? 'disabled' : ''}>${own ? 'DIMILIKI' : '◆ ' + ALT_PRICE}</button></article>`; }).join('');
|
||||
const whoPick = roster().map(id => `<button class="pill ${id === who ? 'on' : ''}" data-shopwho="${id}">${fname(id)}</button>`).join('');
|
||||
return `<h3>Jurus toko</h3><div class="grid">${moves}</div><h3>Varian skill karakter</h3><div class="pills">${whoPick}</div><div class="grid" style="margin-top:10px">${alts || '<p class="meta-hint">Tidak ada varian untuk karakter ini.</p>'}</div><h3>Skin warna</h3><div class="grid">${skins}</div><p class="meta-hint">Koin didapat dari menang pertandingan, kombo, dan pencapaian. Pasang barang di menu KARAKTER.</p>`;
|
||||
}
|
||||
const portrait = id => `assets/${id}/ui/portrait.webp`;
|
||||
function renderLocker() {
|
||||
|
|
@ -129,28 +192,33 @@
|
|||
const pick = ids.map(id => `<button class="chip ${id === who ? 'on' : ''}" data-who="${id}"><img src="${portrait(id)}" alt=""><span>${fname(id)}</span></button>`).join('');
|
||||
const skins = save.owned.skin.map(k => `<button class="pill ${k === sk ? 'on' : ''}" data-skin="${k}">${SKINS[k].name}</button>`).join('');
|
||||
const moves = [`<button class="pill ${!equippedMove() ? 'on' : ''}" data-move="">KOSONG</button>`].concat(save.owned.move.map(k => `<button class="pill ${save.move === k ? 'on' : ''}" data-move="${k}">${MOVES[k].name}</button>`)).join('');
|
||||
const altRows = ['skill1', 'skill2'].map(slot => { const opts = altOptions(who).filter(o => o.slot === slot && save.owned.alt.includes(o.key)); const cur = save.alt?.[who]?.[slot] || ''; return `<h5>Skill ${slot === 'skill1' ? '1 (I)' : '2 (O)'}</h5><div class="pills"><button class="pill ${!cur ? 'on' : ''}" data-alt="${slot}:">ASLI</button>${opts.map(o => `<button class="pill ${cur === o.v ? 'on' : ''}" data-alt="${slot}:${o.v}">${esc(o.name)}</button>`).join('')}</div>`; }).join('');
|
||||
const n = fnames(who);
|
||||
return `<div class="roster">${pick}</div><div class="locker"><div class="preview" style="filter:${SKINS[sk]?.filter || 'none'}"><img src="${portrait(who)}" alt=""></div><div class="info"><h3>${fname(who)}</h3><h5>Skin</h5><div class="pills">${skins}</div><h5>Jurus toko (tombol K)</h5><div class="pills">${moves}</div><p class="meta-hint">${save.owned.move.length ? '' : 'Belum punya jurus toko. Beli di tab TOKO.'}</p><h5>Daftar gerakan</h5><ul class="moves">${n.length ? `<li><b>SPASI</b> ${esc(n[0] || '')}</li><li><b>I</b> ${esc(n[1] || '')}</li><li><b>O</b> ${esc(n[2] || '')}</li><li><b>P</b> ${esc(n[3] || '')}</li>` : ''}${COMMANDS.map(c => `<li><b>${c[0]}</b> ${c[1]} — ${c[2]}</li>`).join('')}</ul></div></div>`;
|
||||
return `<div class="roster">${pick}</div><div class="locker"><div class="preview" style="filter:${SKINS[sk]?.filter || 'none'}"><img src="${portrait(who)}" alt=""></div><div class="info"><h3>${fname(who)}</h3><h5>Skin</h5><div class="pills">${skins}</div><h5>Jurus toko (tombol K)</h5><div class="pills">${moves}</div><p class="meta-hint">${save.owned.move.length ? '' : 'Belum punya jurus toko. Beli di tab TOKO.'}</p>${altRows}<h5>Daftar gerakan</h5><ul class="moves">${n.length ? `<li><b>SPASI</b> ${esc(n[0] || '')}</li><li><b>I</b> ${esc(n[1] || '')}</li><li><b>O</b> ${esc(n[2] || '')}</li><li><b>P</b> ${esc(n[3] || '')}</li>` : ''}${COMMANDS.map(c => `<li><b>${c[0]}</b> ${c[1]} — ${c[2]}</li>`).join('')}</ul></div></div>`;
|
||||
}
|
||||
function renderBook() {
|
||||
const got = Object.keys(ACH).filter(k => save.ach[k]).length, st = save.stats;
|
||||
const list = Object.entries(ACH).map(([k, [n, d]]) => { const ok = !!save.ach[k], hidden = !ok && ['konami', 'syududu', 'mbg', 'esemka', 'penasaran', 'logo'].includes(k); return `<li class="${ok ? 'ok' : ''}"><b>${ok ? '★' : '☆'} ${hidden ? '???' : n}</b><span>${hidden ? 'Pencapaian rahasia.' : d}</span></li>`; }).join('');
|
||||
return `<h3>Statistik</h3><p class="stats">Menang <b>${st.wins}</b> · Kalah <b>${st.losses}</b> · Bersih <b>${st.perfects}</b> · Kombo terbaik <b>${st.bestCombo}</b> · Total koin <b>${save.earned}</b></p><h3>Pencapaian ${got}/${Object.keys(ACH).length}</h3><ul class="ach">${list}</ul><h3>Simpanan</h3><p><button class="pill" data-export>SALIN KODE SIMPANAN</button> <button class="pill" data-import>TEMPEL KODE</button> <button class="pill danger" data-reset>HAPUS SEMUA</button></p>`;
|
||||
return `<h3>Statistik</h3><p class="stats">Menang <b>${st.wins}</b> · Kalah <b>${st.losses}</b> · Bersih <b>${st.perfects}</b> · Kombo terbaik <b>${st.bestCombo}</b> · Total koin <b>${save.earned}</b></p><h3>Pencapaian ${got}/${Object.keys(ACH).length}</h3><ul class="ach">${list}</ul><h3>Suara</h3><p><button class="pill ${save.voice ? 'on' : ''}" data-voice>TERIAKAN JURUS: ${save.voice ? 'AKTIF' : 'MATI'}</button> <small class="meta-hint">Memakai suara bawaan browser.</small></p><h3>Simpanan</h3><p><button class="pill" data-export>SALIN KODE SIMPANAN</button> <button class="pill" data-import>TEMPEL KODE</button> <button class="pill danger" data-reset>HAPUS SEMUA</button></p>`;
|
||||
}
|
||||
function onClick(e) {
|
||||
const b = e.target.closest('button'); if (!b) return;
|
||||
if (b.hasAttribute('data-close')) return close();
|
||||
if (b.dataset.tab) { tab = b.dataset.tab; notice = ''; return render(); }
|
||||
if (b.dataset.who) { who = b.dataset.who; return render(); }
|
||||
if (b.dataset.who || b.dataset.shopwho) { who = b.dataset.who || b.dataset.shopwho; return render(); }
|
||||
if (b.hasAttribute('data-runmenu')) { close(); return window.FrontEnd?.home(); }
|
||||
if (b.dataset.alt !== undefined && b.hasAttribute('data-alt')) { const [slot, v] = b.dataset.alt.split(':'); (save.alt[who] = save.alt[who] || {})[slot] = v || null; persist(); return render(); }
|
||||
if (b.dataset.skin) { save.skin[who] = b.dataset.skin; persist(); return render(); }
|
||||
if (b.dataset.move !== undefined && b.hasAttribute('data-move')) { save.move = b.dataset.move || null; persist(); return render(); }
|
||||
if (b.dataset.buy) {
|
||||
const [kind, id] = b.dataset.buy.split(':'), item = (kind === 'skin' ? SKINS : MOVES)[id];
|
||||
const kind = b.dataset.buy.split(':')[0], id = b.dataset.buy.slice(kind.length + 1), item = kind === 'alt' ? { name: altOptions(who).find(o => o.key === id)?.name || id, price: ALT_PRICE } : (kind === 'skin' ? SKINS : MOVES)[id];
|
||||
if (save.owned[kind].includes(id)) return;
|
||||
if (save.coins < item.price) { notice = 'Koin belum cukup untuk ' + item.name + '.'; return render(); }
|
||||
save.coins -= item.price; save.owned[kind].push(id); if (kind === 'move' && !save.move) save.move = id;
|
||||
if (kind === 'alt') { const [c, sl, v] = id.split(':'); (save.alt[c] = save.alt[c] || {})[sl] = v; unlock('special'); }
|
||||
notice = item.name + ' dibeli.'; persist(); unlock('shopper'); if (save.owned.skin.length >= 4) unlock('fashion'); return render();
|
||||
}
|
||||
if (b.hasAttribute('data-voice')) { save.voice = !save.voice; persist(); return render(); }
|
||||
if (b.hasAttribute('data-export')) { const code = btoa(unescape(encodeURIComponent(JSON.stringify(save)))); navigator.clipboard?.writeText(code).then(() => { notice = 'Kode disalin ke papan klip.'; render(); }, () => { notice = 'Tidak bisa menyalin.'; render(); }); return; }
|
||||
if (b.hasAttribute('data-import')) { const code = prompt('Tempel kode simpanan:'); if (!code) return; try { const d = JSON.parse(decodeURIComponent(escape(atob(code.trim())))); if (d.v !== 1) throw 0; save = { ...blank(), ...d }; persist(); notice = 'Simpanan dimuat.'; } catch (_) { notice = 'Kode tidak valid.'; } return render(); }
|
||||
if (b.hasAttribute('data-reset')) { if (confirm('Hapus semua koin, skin, jurus, dan pencapaian?')) { save = blank(); persist(); notice = 'Simpanan dihapus.'; render(); } }
|
||||
|
|
@ -186,5 +254,5 @@
|
|||
if (lock) { const now = Date.now(); if (now - lt > 4000) lockClicks = 0; lt = now; lockClicks++; if (lockClicks === 3) toast('Siapa yang akan datang berikutnya…?'); if (lockClicks >= 6) { lockClicks = 0; egg('penasaran', 'RASA INGIN TAHUMU DIHARGAI', () => {}); } }
|
||||
}, true);
|
||||
|
||||
window.Meta = { skinFilter, equippedMove, hud, award, open, close, toast, addCoins, unlock, get coins() { return save.coins; }, get save() { return save; } };
|
||||
window.Meta = { shout, cancelRun() { run = null; }, altSkill, event, startRun, skinFilter, equippedMove, hud, award, open, close, toast, addCoins, unlock, get coins() { return save.coins; }, get save() { return save; } };
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<meta name="theme-color" content="#101923">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>AETHER CLASH — Mecha vs Demi-Human</title>
|
||||
<title>Konoha Fighting</title>
|
||||
<style>
|
||||
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#101923;overscroll-behavior:none;touch-action:none}
|
||||
#game{position:fixed;left:0;top:0;border:0;display:block;transform-origin:0 0;background:#101923}
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
controls included) is rotated 90 degrees, so the phone shows the same landscape layout either way. The game lives in
|
||||
an iframe so its own viewport, media queries and vh/vw units see the landscape size, not the phone's portrait size.
|
||||
-->
|
||||
<iframe id="game" title="AETHER CLASH" allow="fullscreen; autoplay"></iframe>
|
||||
<iframe id="game" title="Konoha Fighting" allow="fullscreen; autoplay"></iframe>
|
||||
<script>
|
||||
(() => {
|
||||
'use strict';
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue