Gabung wf/t7-code: kode tantangan dan kebiasaan bermain
This commit is contained in:
commit
1550c4fc6d
|
|
@ -0,0 +1,68 @@
|
||||||
|
/* Kode tantangan dan kebiasaan bermain (challenge.js, RENCANA 7.5 dan 7.6). Tinggi jendela terendah yang didukung 520 px (ponsel lanskap 844x390 juga dijaga):
|
||||||
|
tombol-tombol kecil memakai ruang yang sudah ada (absolut di bilah atas, atau di kolom kanan yang lebih rendah dari kolom kirinya), sehingga tinggi layar tidak bertambah. */
|
||||||
|
|
||||||
|
/* ---- Menu utama: tombol KODE TANTANGAN di sebelah TUTORIAL (pojok kiri atas), di luar alur bilah .home-top ---- */
|
||||||
|
#front-menu .home-top{position:relative}
|
||||||
|
#front-menu .ch-entry{position:absolute;left:4.5%;top:50%;z-index:1;display:inline-flex;align-items:center;gap:8px;min-height:26px;padding:3px 14px 3px 10px;background:#102b37c4;border:1px solid #67e4dd66;border-left:3px solid #67e4dd;color:#f6eedb;font:700 13px/1.1 Rajdhani,Arial,sans-serif;letter-spacing:2.5px;cursor:pointer;transform:translateY(-50%) skewX(-7deg);transition:background .14s,border-color .14s,transform .14s}
|
||||||
|
#front-menu .tut-entry~.ch-entry{left:calc(4.5% + 142px)}
|
||||||
|
#front-menu .ch-entry>*{transform:skewX(7deg)}
|
||||||
|
#front-menu .ch-entry i{font-style:normal;font-size:12px;color:#67e4dd}
|
||||||
|
#front-menu .ch-entry:hover,#front-menu .ch-entry:focus-visible{background:#9be9e2;color:#102734;border-color:#d8fffb;transform:translateY(-50%) skewX(-7deg) translateX(4px)}
|
||||||
|
#front-menu .ch-entry:hover i,#front-menu .ch-entry:focus-visible i{color:#102734}
|
||||||
|
@media(max-width:560px){#front-menu .ch-entry{font-size:11px;letter-spacing:1.5px;padding:3px 9px 3px 7px}#front-menu .tut-entry~.ch-entry{left:calc(4.5% + 112px)}}
|
||||||
|
html.touch #front-menu .ch-entry{min-height:30px}
|
||||||
|
|
||||||
|
/* ---- Layar arena: baris ATURAN / SALIN KODE / TAUTAN di kolom kanan .match-rules (kolom kiri berisi tombol tingkat dan lebih tinggi, jadi tinggi baris tidak naik) ---- */
|
||||||
|
#front-menu .ch-tools{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:5px;margin-top:6px;letter-spacing:0;text-align:left}
|
||||||
|
#front-menu .ch-code{display:none;padding:4px 8px;font:700 13px/1 Rajdhani,Arial,sans-serif;letter-spacing:2px;color:#cfe9e2;background:#07151d99;border:1px dashed #9fd3c455;user-select:all}
|
||||||
|
@media(min-width:900px){#front-menu .ch-code{display:inline-block}}
|
||||||
|
#front-menu .ch-btn{margin:0;padding:4px 9px;font:700 11px/1.15 Rajdhani,Arial,sans-serif;letter-spacing:1.4px;color:#e6ecea;background:#13303d;border:1px solid #7dada177;cursor:pointer;white-space:nowrap}
|
||||||
|
#front-menu .ch-btn small{display:inline;margin:0 5px 0 0;font-size:9px;letter-spacing:1px;color:#9fb8bc}
|
||||||
|
#front-menu .ch-btn:hover,#front-menu .ch-btn:focus-visible{background:#e6c78f;color:#102734;border-color:#fff0c8}
|
||||||
|
#front-menu .ch-btn:hover small,#front-menu .ch-btn:focus-visible small{color:#35505a}
|
||||||
|
#front-menu .ch-btn.on{border-color:#e6c78f;color:#f3d796;background:#2a3b3a}
|
||||||
|
#front-menu .ch-btn.on:hover,#front-menu .ch-btn.on:focus-visible{color:#102734;background:#e6c78f}
|
||||||
|
@media(max-height:420px){#front-menu .ch-tools{margin-top:3px;gap:4px}#front-menu .ch-btn{padding:2px 7px;font-size:10px}#front-menu .ch-btn small{font-size:8px}#front-menu .ch-code{padding:2px 6px;font-size:11px}}
|
||||||
|
@media(pointer:coarse) and (min-height:421px){#front-menu .ch-btn{min-height:26px}}
|
||||||
|
|
||||||
|
/* ---- Layar hasil: SALIN KODE / TAUTAN di pojok kanan atas kepala hasil (tombol STATISTIK ada di pojok kanan bawah) ---- */
|
||||||
|
#front-menu .rs-head{position:relative}
|
||||||
|
#front-menu .ch-share{position:absolute;right:0;top:0;z-index:2;display:flex;gap:6px;animation:rs-up .5s .9s both}
|
||||||
|
@media(max-width:700px){#front-menu .ch-share .ch-btn[data-ch=link]{display:none}}
|
||||||
|
|
||||||
|
/* ---- HUD: penanda tantangan tepat di bawah kolom tengah (jam), ikut posisi bilah atas di semua ukuran ---- */
|
||||||
|
.ch-badge{position:absolute;left:50%;top:100%;transform:translateX(-50%);margin-top:6px;padding:2px 10px;white-space:nowrap;font:700 10px/1.35 Rajdhani,Arial,sans-serif;letter-spacing:1.8px;color:#102734;background:#e6c78f;border:1px solid #fff0c8;box-shadow:0 2px 6px #0007}
|
||||||
|
@media(max-width:950px){.ch-badge{font-size:9px;letter-spacing:1.2px;padding:1px 7px;margin-top:4px}}
|
||||||
|
@media(max-height:500px) and (min-width:620px){.ch-badge{margin-top:3px}}
|
||||||
|
@media(max-width:620px){.ch-badge{font-size:8px;letter-spacing:.8px;padding:1px 5px}}
|
||||||
|
|
||||||
|
/* ---- Dialog Masukkan kode / undangan ---- */
|
||||||
|
#ch-dialog{width:min(470px,calc(100% - 24px));padding:22px 26px 20px}
|
||||||
|
#ch-dialog h2{font-size:30px;margin:6px 0 4px}
|
||||||
|
#ch-dialog .ch-lead{margin:4px 0 12px;font-size:15px;line-height:1.4}
|
||||||
|
#ch-dialog .ch-field{display:flex;align-items:center;gap:12px}
|
||||||
|
#ch-dialog .ch-field>span{flex:none;font-size:12px;font-weight:700;letter-spacing:2px;color:var(--gold)}
|
||||||
|
#ch-input{flex:1;min-width:0;padding:8px 12px;font:700 26px/1.1 Rajdhani,Arial,sans-serif;letter-spacing:5px;text-transform:uppercase;color:#fff8e8;background:#0a1620;border:1px solid #8da8b1aa;border-radius:0}
|
||||||
|
#ch-input::placeholder{color:#5f7782;letter-spacing:5px}
|
||||||
|
#ch-input:focus{border-color:var(--gold)}
|
||||||
|
#ch-dialog .ch-preview{min-height:76px;margin:12px 0 4px;padding:9px 12px;font-size:15px;line-height:1.4;color:#b9cbd0;background:#ffffff0a;border-left:3px solid #ffffff2e}
|
||||||
|
#ch-dialog .ch-preview.ok{border-left-color:#67e4dd;color:#e8f6f1}
|
||||||
|
#ch-dialog .ch-preview.err{border-left-color:#ff9a7a;color:#ffc7b8}
|
||||||
|
#ch-dialog .ch-vs{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 10px;font-size:22px;line-height:1.15}
|
||||||
|
#ch-dialog .ch-vs b,#ch-dialog .ch-line b{color:#fff0c8}
|
||||||
|
#ch-dialog .ch-vs i{font-style:normal;font-size:13px;letter-spacing:2px;color:var(--gold)}
|
||||||
|
#ch-dialog .ch-actions{display:flex;align-items:center;gap:12px;margin-top:10px}
|
||||||
|
#ch-dialog .ch-actions .primary-button{width:auto;flex:1;margin:0}
|
||||||
|
#ch-dialog .ch-actions .primary-button:disabled{background:#6d6e66;color:#2a3338;cursor:not-allowed}
|
||||||
|
#ch-dialog .ch-actions .text-button{margin:0;white-space:nowrap}
|
||||||
|
@media(max-height:480px){#ch-dialog{padding:14px 22px 12px}#ch-dialog h2{font-size:24px;margin:2px 0}#ch-dialog .ch-lead{display:none}#ch-input{font-size:22px;padding:6px 10px}#ch-dialog .ch-preview{min-height:60px;margin-top:8px;padding:6px 10px;font-size:14px}#ch-dialog .ch-vs{font-size:19px}}
|
||||||
|
|
||||||
|
/* ---- Koleksi: "Kebiasaan bermainmu" (disisipkan challenge.js sebelum daftar pencapaian) ---- */
|
||||||
|
.hb-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:8px;margin:12px 0 0}
|
||||||
|
.hb-grid>div{min-width:0;padding:8px 12px;background:#101c26;border:1px solid #ffffff1c;border-radius:8px}
|
||||||
|
.hb-grid dt{font-size:13px;letter-spacing:.06em;color:#9fb3c4;text-transform:uppercase}
|
||||||
|
.hb-grid dd{margin:2px 0 0;font:700 20px/1.15 Rajdhani,Arial,sans-serif;color:#f6eedb;overflow-wrap:anywhere}
|
||||||
|
.hb-grid dd small{display:block;margin-top:1px;font-size:14px;font-weight:600;color:#8fa3b3}
|
||||||
|
|
||||||
|
/* ---- About: catatan privasi satu baris ---- */
|
||||||
|
.about-panel>small.ch-about{margin-top:3px;line-height:1.45;color:#8fb0b6}
|
||||||
|
|
@ -0,0 +1,432 @@
|
||||||
|
/* Kode tantangan (RENCANA 7.5) dan kebiasaan bermain lokal (RENCANA 7.6). Gaya: challenge.css. Tanpa jaringan dan tanpa pustaka luar.
|
||||||
|
|
||||||
|
KODE TANTANGAN (window.Challenge)
|
||||||
|
- Bentuk: 8 karakter dari 30 simbol yang tidak membingungkan (2-9 dan A-Z tanpa I, L, O, U), tampil XXXX-XXXX. Saat mengetik, huruf kecil, spasi, dan tanda hubung diabaikan;
|
||||||
|
tempelan berupa tautan (?kode=...) atau pesan panjang ditelusuri sampai ketemu kode yang lolos pemeriksaan.
|
||||||
|
- Isi 24 bit (versi 2, variasi 2, aturan 4, tingkat 2, arena 4, lawan 5, pemain 5) + 15 bit pemeriksa, lalu dicampur supaya kode yang mirip tidak tampak mirip. Salah ketik lolos
|
||||||
|
pemeriksa dengan peluang sekitar 1 : 32.000 dan masih harus menghasilkan karakter, arena, dan aturan yang ada, jadi praktis selalu ditolak ("kode tidak valid").
|
||||||
|
"Variasi" dipakai penyandi untuk menghindari kata kasar yang kebetulan terbentuk (BLOCKED); tanpa itu selalu 0.
|
||||||
|
- Daftar karakter (BASE_FIGHTERS), arena (BASE_STAGES), tingkat (LEVELS), dan aturan (RULES) menentukan arti kode: urutannya TIDAK BOLEH diubah, hanya ditambah di belakang.
|
||||||
|
Karakter/arena baru yang belum ada di daftar ikut otomatis di belakangnya menurut urutan deklarasi (window.FIGHTERS, MatchRules.stages); sebaiknya tetap dimasukkan ke daftar di sini.
|
||||||
|
- Aturan khusus (RULES): satu per kode, kecil dan aman, hanya mengubah angka lewat kait sekecil mungkin di game.js. Kuncinya (blitz, one, fast, noblock, heavy) hanya hidup di berkas ini
|
||||||
|
dan di dalam kode; tidak ada yang disimpan di localStorage, jadi tidak bentrok dengan aturan tantangan harian (daily.js).
|
||||||
|
- Alur: Challenge.open() = dialog "Masukkan kode" (tombol KODE TANTANGAN di pojok kiri atas menu utama); ?kode=XXXXXXXX di alamat membuka dialog yang sama sebagai undangan
|
||||||
|
(isi sudah terisi, tinggal TERIMA); Challenge.start(cfg) memulai pertandingan. Layar arena (menu.js) mendapat baris ATURAN / SALIN KODE / TAUTAN, layar hasil tombol SALIN KODE / TAUTAN.
|
||||||
|
Aturan yang dipilih di layar arena berlaku juga untuk tombol LET'S FIGHT dan MAIN LAGI (menu.js start() memanggil Challenge.pending); kembali ke menu utama mengosongkannya.
|
||||||
|
Kait di game.js (semua window.Challenge?.x?.(), jadi tanpa berkas ini game berjalan seperti biasa): begin(options) di startMatch sesudah validasi (juga mencatat kebiasaan bermain),
|
||||||
|
tune(sideMods) di akhir refreshPassives, roundStart(hero, dummy, BALANCE) di akhir resetCombat, roundEnd(match, event) dan finish(info) di advanceMatch, hud(match) di updateMatchHud,
|
||||||
|
dan noBlock di press (tombol L) serta heroGuard. Kait di menu.js: decorate(root, screen, ctx) di akhir render() dan pending(mode, p2) di start(). startMatch menerima opsi
|
||||||
|
codeRule: {rule, code}; hanya berlaku untuk versus melawan CPU (bukan dua pemain, bukan Bayangan). Semua kait dibungkus guard: galat hanya dicatat di konsol.
|
||||||
|
Tidak memakai Math.random/Date.now: aturan tidak mengubah pengacak pertandingan.
|
||||||
|
|
||||||
|
KEBIASAAN BERMAIN (window.Habits)
|
||||||
|
- localStorage['konoha.habits.v1'] = {v:1, n, ch:{idKarakter:jumlah}, mode:{...}, stage:{...}, foe:{...}}. Dicatat di begin() tiap pertandingan dimulai (latihan juga, Bayangan tidak
|
||||||
|
dihitung sebagai "lawan"). Tanpa waktu, tanpa identitas, tanpa jaringan. Terpisah dari Meta.save, jadi simpanan lama tidak tersentuh; data rusak atau kosong dibaca sebagai nol.
|
||||||
|
- Bagian "Kebiasaan bermainmu" disisipkan ke tab Koleksi lewat MutationObserver (meta.js tidak diubah), lengkap dengan tombol HAPUS STATISTIK. Catatan privasi di layar About lewat decorate().
|
||||||
|
Konsol: Challenge.state(), Challenge.encode({player, enemy, stage, level, rule}), Challenge.decode('XXXX-XXXX'), Habits.data(). */
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
const KEY_HABITS = 'konoha.habits.v1';
|
||||||
|
const $ = s => document.querySelector(s);
|
||||||
|
const esc = t => String(t == null ? '' : t).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||||
|
const guard = (fn, fallback) => (...a) => { try { return fn(...a); } catch (e) { console.error('Challenge:', e); return fallback; } };
|
||||||
|
const store = {
|
||||||
|
get(k) { try { return localStorage.getItem(k); } catch (_) { return null; } },
|
||||||
|
set(k, v) { try { localStorage.setItem(k, v); } catch (_) { /* penyimpanan penuh atau diblokir: statistik hanya hidup di sesi ini */ } },
|
||||||
|
del(k) { try { localStorage.removeItem(k); } catch (_) { /* abaikan */ } }
|
||||||
|
};
|
||||||
|
const sfx = kind => { try { window.__game?.menuSound?.(kind); } catch (_) { /* suara hanya hiasan */ } };
|
||||||
|
const say = msg => { try { window.Meta?.toast?.(msg); } catch (_) { /* toast hanya hiasan */ } };
|
||||||
|
|
||||||
|
// ---------- nama dan ketersediaan (sama dengan yang dipakai menu.js) ----------
|
||||||
|
const fighterName = id => window.FIGHTERS?.[id]?.name || (id === 'browo' ? 'BRO WO' : String(id || '?').toUpperCase());
|
||||||
|
const hasFighter = id => !!(window.FIGHTERS?.[id] || (id === 'browo' && window.Browo && window.BROWO_MANIFEST));
|
||||||
|
const stageName = id => window.MatchRules?.stages?.[id]?.name || String(id || '?');
|
||||||
|
const hasStage = id => !!window.MatchRules?.stages && Object.hasOwn(window.MatchRules.stages, id);
|
||||||
|
|
||||||
|
// ---------- daftar kode (urutan tetap, hanya ditambah di belakang) ----------
|
||||||
|
const BASE_FIGHTERS = ['browo', 'megachan', 'sonofsolo', 'ethanolman', 'anice', 'soloman', 'gufron'];
|
||||||
|
const BASE_STAGES = ['bellora', 'sunspire', 'harbor', 'elderwood', 'moonrise'];
|
||||||
|
const LEVELS = ['easy', 'medium', 'hard', 'excellent'];
|
||||||
|
const LEVEL_NAME = { easy: 'Mudah', medium: 'Sedang', hard: 'Sulit', excellent: 'Sangat sulit' };
|
||||||
|
const fighterList = () => BASE_FIGHTERS.concat(Object.keys(window.FIGHTERS || {}).filter(id => !BASE_FIGHTERS.includes(id)));
|
||||||
|
const stageList = () => BASE_STAGES.concat(Object.keys(window.MatchRules?.stages || {}).filter(id => !BASE_STAGES.includes(id)));
|
||||||
|
// Aturan khusus. Kolom: rounds = jumlah ronde penentu (default 3 ronde, menang 2), hp = pecahan HP awal kedua petarung, mods = pengali kunci passiveMod (passives.js KEYS)
|
||||||
|
// per sisi, noBlock = blok dan parry tidak berlaku. Menambah aturan: tambahkan di BELAKANG dan jangan menggeser yang lama.
|
||||||
|
const FAST = { walk: 1.25, cd: 1.35 }, HEAVY = 1.5, HALF = .5, pct = v => Math.round((v - 1) * 100);
|
||||||
|
const RULES = [
|
||||||
|
{ id: 'none', name: 'Tanpa aturan khusus', short: 'BIASA', desc: 'Pertandingan seperti biasa.' },
|
||||||
|
{ id: 'blitz', name: 'Adu cepat', short: 'ADU CEPAT', desc: 'Satu ronde saja dan nyawa kedua petarung tinggal separuh.', rounds: 1, hp: HALF },
|
||||||
|
{ id: 'one', name: 'Satu ronde', short: 'SATU RONDE', desc: 'Hanya satu ronde. Siapa menang, dialah juaranya.', rounds: 1 },
|
||||||
|
{ id: 'fast', name: 'Lawan gesit', short: 'LAWAN GESIT', desc: `Lawan berjalan ${pct(FAST.walk)}% lebih cepat dan jurusnya pulih ${pct(FAST.cd)}% lebih cepat.`, mods: { enemy: FAST } },
|
||||||
|
{ id: 'noblock', name: 'Tanpa blok', short: 'TANPA BLOK', desc: 'Blok dan parry tidak berlaku, untuk kedua petarung.', noBlock: true, mods: { enemy: { parry: 0 } } },
|
||||||
|
{ id: 'heavy', name: 'Pukulan berat', short: 'PUKULAN BERAT', desc: `Semua damage naik ${pct(HEAVY)}%, untuk kedua petarung.`, mods: { player: { dmgOut: HEAVY }, enemy: { dmgOut: HEAVY } } }
|
||||||
|
];
|
||||||
|
const ruleOf = id => RULES.find(r => r.id === id) || RULES[0];
|
||||||
|
|
||||||
|
// ---------- penyandi ----------
|
||||||
|
const ALPHA = '23456789ABCDEFGHJKMNPQRSTVWXYZ', BASE = ALPHA.length, TOTAL = 2 ** 39; // 30 simbol; 24 bit isi + 15 bit pemeriksa = 39 bit < 30^8
|
||||||
|
const BLOCKED = ['SEX', 'ASS', 'SETAN', 'SATAN', 'BANGSAT', 'KAMPRET', 'KEPARAT', 'JAHANAM']; // kata yang tidak boleh muncul kebetulan di kode
|
||||||
|
const mix = x => { x = Math.imul(x ^ (x >>> 16), 0x7feb352d); x = Math.imul(x ^ (x >>> 15), 0x846ca68b); return (x ^ (x >>> 16)) >>> 0; };
|
||||||
|
const check15 = p => mix((p ^ 0x2f6b9d35) >>> 0) & 0x7fff;
|
||||||
|
const mask24 = c => mix((c + 0x1b873593) >>> 0) & 0xffffff;
|
||||||
|
function pack(p) {
|
||||||
|
const c = check15(p), m = (p ^ mask24(c)) & 0xffffff;
|
||||||
|
let n = m * 32768 + c, out = '';
|
||||||
|
for (let i = 0; i < 8; i++) { const d = n % BASE; out = ALPHA[d] + out; n = (n - d) / BASE; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
// 24 bit isi, atau -1 bila pemeriksa tidak cocok.
|
||||||
|
function unpack(s) {
|
||||||
|
let n = 0; for (const ch of s) n = n * BASE + ALPHA.indexOf(ch);
|
||||||
|
if (n >= TOTAL) return -1;
|
||||||
|
const c = n % 32768, m = (n - c) / 32768, p = (m ^ mask24(c)) & 0xffffff;
|
||||||
|
return check15(p) === c ? p : -1;
|
||||||
|
}
|
||||||
|
const pretty = code => { const c = String(code || ''); return c.length > 4 ? c.slice(0, 4) + '-' + c.slice(4) : c; }; // K7M2QX9T -> K7M2-QX9T (juga untuk kode yang belum lengkap)
|
||||||
|
// cfg = {player, enemy, stage, level, rule?} -> 'K7M2QX9T' (tanpa tanda hubung) atau null bila ada yang belum bisa disandikan.
|
||||||
|
function encode(cfg) {
|
||||||
|
const fl = fighterList(), sl = stageList();
|
||||||
|
const a = fl.indexOf(cfg?.player), b = fl.indexOf(cfg?.enemy), s = sl.indexOf(cfg?.stage), l = LEVELS.indexOf(cfg?.level), r = RULES.findIndex(x => x.id === (cfg?.rule || 'none'));
|
||||||
|
if (a < 0 || a > 31 || b < 0 || b > 31 || s < 0 || s > 15 || l < 0 || r < 0 || r > 15) return null;
|
||||||
|
const body = a | (b << 5) | (s << 10) | (l << 14) | (r << 16);
|
||||||
|
let code = '';
|
||||||
|
for (let variant = 0; variant < 4; variant++) { code = pack(body | (variant << 20)); if (!BLOCKED.some(w => code.includes(w))) break; }
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
// Mengambil kode dari apa pun yang diketik atau ditempel: kode polos, tautan dengan ?kode=..., atau kalimat yang memuat kode.
|
||||||
|
function extract(raw) {
|
||||||
|
const text = String(raw == null ? '' : raw).trim(); if (!text) return '';
|
||||||
|
const link = /[?&#]kode=([A-Za-z0-9-]+)/i.exec(text); if (link) return link[1].toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||||
|
const flat = text.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||||
|
if (flat.length <= 8) return flat;
|
||||||
|
const up = text.toUpperCase(), re = /([A-Z0-9]{4})[\s-]?([A-Z0-9]{4})/g; let m;
|
||||||
|
while ((m = re.exec(up))) { const c = m[1] + m[2]; if ([...c].every(ch => ALPHA.includes(ch)) && unpack(c) >= 0) return c; re.lastIndex = m.index + 1; }
|
||||||
|
return flat;
|
||||||
|
}
|
||||||
|
const fail = (err, msg, soft) => ({ ok: false, err, msg, soft: !!soft });
|
||||||
|
// -> {ok:true, code, cfg:{player, enemy, stage, level, rule}} atau {ok:false, err, msg, soft}; soft = belum selesai diketik (bukan galat).
|
||||||
|
function decode(raw) {
|
||||||
|
const s = extract(raw);
|
||||||
|
if (!s) return fail('empty', 'Ketik atau tempel kode tantangannya dulu.', true);
|
||||||
|
if (s.length > 8) return fail('long', 'Tidak ketemu kode tantangan yang valid di teks ini. Kode terdiri dari 8 huruf dan angka.');
|
||||||
|
if ([...s].some(ch => !ALPHA.includes(ch))) return fail('chars', 'Ada karakter yang tidak dipakai di kode tantangan (0, 1, I, L, O, U). Periksa lagi ya.');
|
||||||
|
if (s.length < 8) return fail('short', `Kodenya baru ${s.length} dari 8 karakter.`, true);
|
||||||
|
const p = unpack(s); if (p < 0) return fail('check', 'Kode ini tidak valid. Mungkin ada huruf atau angka yang salah ketik.');
|
||||||
|
if (p >>> 22) return fail('version', 'Kode ini dibuat oleh versi game yang lebih baru. Muat ulang atau perbarui game dulu.');
|
||||||
|
const fl = fighterList(), sl = stageList();
|
||||||
|
const cfg = { player: fl[p & 31], enemy: fl[(p >> 5) & 31], stage: sl[(p >> 10) & 15], level: LEVELS[(p >> 14) & 3], rule: RULES[(p >> 16) & 15]?.id };
|
||||||
|
if (!cfg.rule || !hasFighter(cfg.player) || !hasFighter(cfg.enemy) || !cfg.stage || !hasStage(cfg.stage)) return fail('unknown', 'Kode ini memakai karakter, arena, atau aturan yang belum ada di game ini.');
|
||||||
|
return { ok: true, code: s, cfg };
|
||||||
|
}
|
||||||
|
function shareUrl(code) {
|
||||||
|
try { const u = new URL(location.href); u.search = ''; u.hash = ''; if (u.protocol !== 'file:') u.pathname = u.pathname.replace(/index\.html$/, ''); return u.href + '?kode=' + code; } catch (_) { return '?kode=' + code; }
|
||||||
|
}
|
||||||
|
// Papan klip: API modern, lalu execCommand, lalu jendela prompt supaya pemain bisa menyalin sendiri. Hasil true = sudah tersalin.
|
||||||
|
async function copyText(text) {
|
||||||
|
try { if (navigator.clipboard?.writeText && window.isSecureContext !== false) { await navigator.clipboard.writeText(text); return true; } } catch (_) { /* coba cadangan */ }
|
||||||
|
try {
|
||||||
|
const ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0';
|
||||||
|
document.body.append(ta); ta.select(); ta.setSelectionRange(0, text.length);
|
||||||
|
const ok = document.execCommand('copy'); ta.remove(); if (ok) return true;
|
||||||
|
} catch (_) { /* coba prompt */ }
|
||||||
|
try { window.prompt('Salin teks ini (Ctrl+C), lalu bagikan:', text); } catch (_) { /* tanpa prompt: tidak ada yang bisa dilakukan */ }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- keadaan dan kait mesin ----------
|
||||||
|
let selected = 'none'; // aturan yang dipilih di layar arena atau dari kode; dibaca menu.js start() lewat pending()
|
||||||
|
let active = null; // {rule, code} selama pertandingan berkode berjalan; kosong di pertandingan biasa
|
||||||
|
let last = { rule: 'none' }; // pertandingan terakhir (untuk SALIN KODE di layar hasil)
|
||||||
|
const cur = () => (active ? ruleOf(active.rule) : null);
|
||||||
|
const label = r => 'TANTANGAN' + (r && r.id !== 'none' ? ' · ' + r.short : '');
|
||||||
|
const els = {}, q = sel => (els[sel] && els[sel].isConnected ? els[sel] : (els[sel] = $(sel)));
|
||||||
|
const setText = (sel, t) => { const e = q(sel); if (e && e.textContent !== t) e.textContent = t; };
|
||||||
|
function paintBadge(rule) {
|
||||||
|
const host = $('.match-center'); if (!host) return;
|
||||||
|
let b = host.querySelector('.ch-badge');
|
||||||
|
if (!rule) { b?.remove(); return; }
|
||||||
|
if (!b) { b = document.createElement('div'); b.className = 'ch-badge'; b.setAttribute('role', 'note'); host.append(b); }
|
||||||
|
b.textContent = label(rule); b.title = rule.desc;
|
||||||
|
}
|
||||||
|
const begin = guard(options => {
|
||||||
|
const o = options || {}, mode = o.mode === undefined ? 'versus' : o.mode, c = o.codeRule;
|
||||||
|
track(o); // kebiasaan bermain (bagian bawah); galatnya tidak boleh menghentikan sisa kait ini
|
||||||
|
active = c && typeof c === 'object' && mode === 'versus' && !(o.vs && o.vs.p2) && !o.shadow ? { rule: ruleOf(c.rule).id, code: String(c.code || '') } : null;
|
||||||
|
if (active && o.vs) { const vs = typeof o.vs === 'object' ? o.vs : {}; if (!vs.label) o.vs = { ...vs, label: label(ruleOf(active.rule)) }; } // judul layar VS
|
||||||
|
last = { rule: active ? active.rule : 'none' };
|
||||||
|
paintBadge(active ? ruleOf(active.rule) : null);
|
||||||
|
});
|
||||||
|
// refreshPassives: aturan dikalikan ke angka pasif tiap sisi (kunci dan arti sama dengan passiveMod).
|
||||||
|
const tune = guard(mods => {
|
||||||
|
const r = cur(); if (!r?.mods || !mods) return;
|
||||||
|
for (const side of ['player', 'enemy']) { const src = r.mods[side], dst = mods[side]; if (src && dst) for (const k of Object.keys(src)) dst[k] = (dst[k] === undefined ? 1 : dst[k]) * src[k]; }
|
||||||
|
});
|
||||||
|
// resetCombat (awal pertandingan dan tiap ronde): HP awal. Maksimum HP tetap, jadi bilah dan penentu waktu habis memakai skala yang sama; yang berubah hanya titik awalnya.
|
||||||
|
const roundStart = guard((hero, dummy, bal) => {
|
||||||
|
const r = cur(); if (!r?.hp || !hero || !dummy || !bal) return;
|
||||||
|
hero.hp = Math.max(1, Math.round(bal.heroMax * r.hp));
|
||||||
|
dummy.damage = bal.dummyMax - Math.max(1, Math.round(bal.dummyMax * r.hp));
|
||||||
|
});
|
||||||
|
// Satu ronde: begitu ronde berakhir dengan pemenang, pertandingan dianggap selesai (seri tetap mengulang ronde). MatchRules.tick sudah menaruh match.winner = null di sini.
|
||||||
|
const roundEnd = guard((m, e) => { const r = cur(); if (r?.rounds === 1 && m && !m.winner && e && e.winner && e.winner !== 'draw') m.winner = e.winner; });
|
||||||
|
// Pertandingan selesai: ingat pengaturannya untuk SALIN KODE di layar hasil. "Bersih" (menang tanpa kehilangan ronde) tidak berlaku bila hanya ada satu ronde.
|
||||||
|
const finish = guard(info => {
|
||||||
|
if (!info) return;
|
||||||
|
const r = cur();
|
||||||
|
last = { player: info.player, enemy: info.enemy, stage: info.stage, level: info.level, rule: r ? r.id : 'none' };
|
||||||
|
if (r?.rounds === 1) info.perfect = false;
|
||||||
|
});
|
||||||
|
// Teks HUD untuk aturan satu ronde (updateMatchHud menulis ulang tiap frame; ini menimpanya sesudah itu, hanya bila perlu).
|
||||||
|
const hud = guard(m => {
|
||||||
|
const r = cur(); if (r?.rounds !== 1 || !m || m.mode !== 'versus') return;
|
||||||
|
setText('#mode-label', 'SATU RONDE');
|
||||||
|
setText('#round-wins-player', m.playerWins ? '◆' : '◇'); setText('#round-wins-enemy', m.enemyWins ? '◆' : '◇');
|
||||||
|
const d = q('#round-call-detail'); if (d && d.textContent === 'FIRST TO TWO WINS') d.textContent = 'SATU RONDE MENENTUKAN';
|
||||||
|
});
|
||||||
|
const pending = (mode, p2) => (selected !== 'none' && mode === 'versus' && !p2 ? { rule: selected } : null);
|
||||||
|
try { new MutationObserver(() => { if (active && document.body.classList.contains('in-menu')) active = null; }).observe(document.body, { attributes: true, attributeFilter: ['class'] }); } catch (_) { /* tanpa pengamat: pertandingan berikutnya tetap menimpa */ }
|
||||||
|
|
||||||
|
// ---------- memulai pertandingan dari kode ----------
|
||||||
|
function start(cfg, code) {
|
||||||
|
const g = window.__game; if (!g || !cfg) return false;
|
||||||
|
const rule = ruleOf(cfg.rule), before = selected; selected = rule.id;
|
||||||
|
let ok = false;
|
||||||
|
try { window.Meta?.cancelRun?.(); ok = !!g.startMatch({ player: cfg.player, enemy: cfg.enemy, stage: cfg.stage, level: cfg.level, mode: 'versus', vs: { label: label(rule) }, codeRule: { rule: rule.id, code: code || '' } }); } catch (e) { console.error(e); }
|
||||||
|
if (!ok) selected = before;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- dialog Masukkan kode / undangan ----------
|
||||||
|
let dlg = null, parsed = null;
|
||||||
|
function summaryHtml(cfg) {
|
||||||
|
const rule = ruleOf(cfg.rule);
|
||||||
|
return `<div class="ch-vs"><b>${esc(fighterName(cfg.player))}</b><i>VS</i><b>${esc(fighterName(cfg.enemy))}</b></div>`
|
||||||
|
+ `<div class="ch-line">Arena <b>${esc(stageName(cfg.stage))}</b> · Tingkat <b>${esc(LEVEL_NAME[cfg.level])}</b></div>`
|
||||||
|
+ `<div class="ch-line">Aturan <b>${esc(rule.name)}</b>${rule.id === 'none' ? '' : ' · ' + esc(rule.desc)}</div>`;
|
||||||
|
}
|
||||||
|
function refresh() {
|
||||||
|
const input = dlg.querySelector('#ch-input'), box = dlg.querySelector('#ch-preview'), go = dlg.querySelector('#ch-go');
|
||||||
|
const r = parsed = decode(input.value);
|
||||||
|
if (r.ok) { box.className = 'ch-preview ok'; box.innerHTML = summaryHtml(r.cfg); go.disabled = false; }
|
||||||
|
else { box.className = 'ch-preview ' + (r.soft ? 'hint' : 'err'); box.textContent = r.msg; go.disabled = true; }
|
||||||
|
}
|
||||||
|
// Selama mengetik kode polos, tanda hubung di tengah dipasang otomatis (hanya bila kursor di ujung, supaya penyuntingan di tengah tidak melompat).
|
||||||
|
function reformat(input) {
|
||||||
|
const v = input.value;
|
||||||
|
if (!/^[A-Za-z0-9\s-]*$/.test(v) || input.selectionStart !== v.length) return;
|
||||||
|
const flat = v.toUpperCase().replace(/[^A-Z0-9]/g, ''); if (flat.length > 8) return;
|
||||||
|
const t = pretty(flat); if (t !== v) input.value = t;
|
||||||
|
}
|
||||||
|
function closeDialog() { if (dlg?.open) { try { dlg.close(); } catch (_) { dlg.removeAttribute('open'); } } }
|
||||||
|
function go() {
|
||||||
|
if (!dlg || !parsed) return;
|
||||||
|
const input = dlg.querySelector('#ch-input');
|
||||||
|
if (!parsed.ok) { sfx('locked'); input.focus(); return; }
|
||||||
|
sfx('confirm');
|
||||||
|
if (start(parsed.cfg, parsed.code)) { closeDialog(); $('#arena')?.focus(); return; }
|
||||||
|
const box = dlg.querySelector('#ch-preview'); box.className = 'ch-preview err'; box.textContent = 'Aset game belum siap. Tunggu sebentar, lalu coba lagi.';
|
||||||
|
}
|
||||||
|
function build() {
|
||||||
|
dlg = document.createElement('dialog'); dlg.id = 'ch-dialog'; dlg.setAttribute('aria-labelledby', 'ch-title');
|
||||||
|
dlg.innerHTML = '<div class="dialog-kicker" id="ch-kicker"></div><h2 id="ch-title"></h2><p class="ch-lead" id="ch-lead"></p>'
|
||||||
|
+ '<label class="ch-field"><span>KODE</span><input id="ch-input" type="text" autocomplete="off" autocapitalize="characters" spellcheck="false" maxlength="240" placeholder="XXXX-XXXX" aria-describedby="ch-preview"></label>'
|
||||||
|
+ '<div id="ch-preview" class="ch-preview" role="status" aria-live="polite"></div>'
|
||||||
|
+ '<div class="ch-actions"><button type="button" class="primary-button" id="ch-go" disabled></button><button type="button" class="text-button" id="ch-close"></button></div>';
|
||||||
|
document.body.append(dlg);
|
||||||
|
const input = dlg.querySelector('#ch-input');
|
||||||
|
input.addEventListener('input', () => { reformat(input); refresh(); });
|
||||||
|
input.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); go(); } });
|
||||||
|
// Selama dialog terbuka tidak ada tombol yang boleh bocor ke menu atau game (Esc ditutup di sini sendiri, karena game.js memanggil preventDefault pada Esc).
|
||||||
|
dlg.addEventListener('keydown', e => { e.stopPropagation(); if (e.key === 'Escape') { e.preventDefault(); closeDialog(); } });
|
||||||
|
dlg.addEventListener('keyup', e => e.stopPropagation());
|
||||||
|
dlg.addEventListener('cancel', e => { e.preventDefault(); closeDialog(); });
|
||||||
|
dlg.querySelector('#ch-go').addEventListener('click', go);
|
||||||
|
dlg.querySelector('#ch-close').addEventListener('click', () => { sfx('back'); closeDialog(); });
|
||||||
|
}
|
||||||
|
// opts.raw = isi awal (kode, tautan, atau pesan); opts.invite = tampil sebagai undangan dari tautan ?kode=.
|
||||||
|
const open = guard((opts = {}) => {
|
||||||
|
if (!dlg || !dlg.isConnected) build();
|
||||||
|
const invite = !!opts.invite, input = dlg.querySelector('#ch-input');
|
||||||
|
dlg.dataset.invite = invite ? '1' : '';
|
||||||
|
dlg.querySelector('#ch-kicker').textContent = invite ? 'KONOHA FIGHTING / TANTANGAN' : 'KONOHA FIGHTING / KODE TANTANGAN';
|
||||||
|
dlg.querySelector('#ch-title').textContent = invite ? 'Ada yang menantangmu!' : 'Masukkan kode';
|
||||||
|
dlg.querySelector('#ch-lead').textContent = invite ? 'Temanmu mengirim tautan tantangan. Cek pengaturannya, lalu mulai kalau sudah siap.' : 'Tempel atau ketik kode dari temanmu. Pertandingan langsung dimulai dengan pengaturan yang sama.';
|
||||||
|
dlg.querySelector('#ch-go').innerHTML = (invite ? 'TERIMA TANTANGAN' : 'MULAI TANTANGAN') + ' <span>↗</span>';
|
||||||
|
dlg.querySelector('#ch-close').textContent = invite ? 'Nanti saja' : 'Batal';
|
||||||
|
const raw = String(opts.raw || '').trim(), code = extract(raw);
|
||||||
|
input.value = code && code.length <= 8 ? pretty(code) : raw;
|
||||||
|
refresh();
|
||||||
|
try { dlg.showModal(); } catch (_) { dlg.setAttribute('open', ''); }
|
||||||
|
(invite && parsed?.ok ? dlg.querySelector('#ch-go') : input).focus();
|
||||||
|
});
|
||||||
|
// Tautan ?kode=...: tawarkan tantangannya sesudah layar muat selesai, lalu hapus parameter dari alamat supaya muat ulang tidak menawarkan lagi.
|
||||||
|
function dropParam() {
|
||||||
|
const strip = w => { try { const u = new URL(w.location.href); if (!u.searchParams.has('kode')) return; u.searchParams.delete('kode'); w.history.replaceState(null, '', u.pathname + u.search + u.hash); } catch (_) { /* alamat tidak bisa diubah: biarkan */ } };
|
||||||
|
strip(window); try { if (window.parent !== window) strip(window.parent); } catch (_) { /* induk lintas asal */ }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const raw = new URLSearchParams(location.search).get('kode');
|
||||||
|
// Kode di tautan yang valid tampil sebagai undangan; yang tidak valid atau kosong membuka dialog biasa (dengan pesan galatnya) supaya bisa diperbaiki.
|
||||||
|
if (raw !== null) Promise.resolve(window.AETHER_PRELOAD).then(() => setTimeout(() => { open({ invite: decode(raw).ok, raw }); dropParam(); }, 650));
|
||||||
|
} catch (_) { /* tanpa parameter */ }
|
||||||
|
|
||||||
|
// ---------- tombol di layar menu ----------
|
||||||
|
function flash(btn, text) {
|
||||||
|
if (!btn) return; const old = btn.dataset.ch0 ?? (btn.dataset.ch0 = btn.textContent); btn.textContent = text; clearTimeout(btn._ch);
|
||||||
|
btn._ch = setTimeout(() => { if (btn.isConnected) btn.textContent = old; }, 1700);
|
||||||
|
}
|
||||||
|
async function share(kind, cfg, btn) {
|
||||||
|
const code = encode(cfg); if (!code) { sfx('locked'); return; }
|
||||||
|
const text = kind === 'link' ? shareUrl(code) : pretty(code), ok = await copyText(text);
|
||||||
|
sfx('confirm');
|
||||||
|
if (ok) { flash(btn, 'TERSALIN ✓'); say(kind === 'link' ? 'Tautan tantangan disalin. Kirim ke temanmu!' : 'Kode ' + pretty(code) + ' disalin. Kirim ke temanmu!'); }
|
||||||
|
}
|
||||||
|
function cycleRule() {
|
||||||
|
const i = RULES.findIndex(r => r.id === selected), n = RULES[(i + 1) % RULES.length]; selected = n.id; sfx('move');
|
||||||
|
say(n.id === 'none' ? 'Aturan khusus dimatikan.' : n.name.toUpperCase() + ': ' + n.desc);
|
||||||
|
}
|
||||||
|
function addEntry(root) {
|
||||||
|
const slot = root.querySelector('.home-top'); if (!slot || slot.querySelector('.ch-entry')) return;
|
||||||
|
const b = document.createElement('button'); b.type = 'button'; b.className = 'ch-entry'; b.setAttribute('aria-label', 'Masukkan kode tantangan dari teman');
|
||||||
|
b.innerHTML = '<i aria-hidden="true">#</i><span>KODE TANTANGAN</span>';
|
||||||
|
b.onclick = () => { sfx('confirm'); open(); };
|
||||||
|
slot.append(b);
|
||||||
|
}
|
||||||
|
function toolsHtml(cfg) {
|
||||||
|
const rule = ruleOf(selected), code = encode({ ...cfg, rule: rule.id }), pc = pretty(code);
|
||||||
|
return `<code class="ch-code" title="Kode tantangan untuk pengaturan ini">${esc(pc)}</code>`
|
||||||
|
+ `<button type="button" class="ch-btn ch-rule${rule.id === 'none' ? '' : ' on'}" data-ch="rule" title="${esc(rule.name + ': ' + rule.desc)}" aria-label="Aturan khusus: ${esc(rule.name)}. Tekan untuk ganti aturan."><small>ATURAN</small>${esc(rule.short)}</button>`
|
||||||
|
+ `<button type="button" class="ch-btn" data-ch="code" title="Salin kode ${esc(pc)}">SALIN KODE</button>`
|
||||||
|
+ `<button type="button" class="ch-btn" data-ch="link" title="Salin tautan yang membuka tantangan ini">TAUTAN</button>`;
|
||||||
|
}
|
||||||
|
function addArenaTools(root, ctx) {
|
||||||
|
const host = root.querySelector('.match-rules');
|
||||||
|
if (!host || !ctx || ctx.mode !== 'versus' || ctx.p2 || ctx.runKind || !encode({ ...ctx, rule: 'none' })) return;
|
||||||
|
const cfg = { player: ctx.player, enemy: ctx.enemy, stage: ctx.stage, level: ctx.level };
|
||||||
|
const head = host.querySelector(':scope > b'), sub = host.querySelector(':scope > span'), was = [head?.textContent, sub?.textContent]; // "FIRST TO 2 WINS / BEST OF 3 ROUNDS"
|
||||||
|
const box = document.createElement('div'); box.className = 'ch-tools'; box.setAttribute('role', 'group'); box.setAttribute('aria-label', 'Kode tantangan'); host.append(box);
|
||||||
|
const paint = () => {
|
||||||
|
box.innerHTML = toolsHtml(cfg);
|
||||||
|
const one = ruleOf(selected).rounds === 1; // teks ronde mengikuti aturan satu ronde
|
||||||
|
if (head) head.textContent = one ? 'SATU RONDE' : was[0];
|
||||||
|
if (sub) sub.textContent = one ? 'SATU RONDE MENENTUKAN · 90 DETIK' : was[1];
|
||||||
|
};
|
||||||
|
paint();
|
||||||
|
box.addEventListener('click', e => {
|
||||||
|
const b = e.target.closest('[data-ch]'); if (!b) return;
|
||||||
|
if (b.dataset.ch === 'rule') { cycleRule(); paint(); box.querySelector('[data-ch=rule]')?.focus(); }
|
||||||
|
else share(b.dataset.ch, { ...cfg, rule: selected }, b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function addResultTools(root, ctx) {
|
||||||
|
const r = ctx?.result, head = root.querySelector('.rs-head');
|
||||||
|
if (!r || !head || r.run || r.p2 || r.boss) return;
|
||||||
|
const cfg = { player: r.player, enemy: r.enemy, stage: r.stage, level: r.level, rule: last.rule || 'none' }, code = encode(cfg); if (!code) return;
|
||||||
|
const rule = ruleOf(cfg.rule), box = document.createElement('div'), pc = pretty(code);
|
||||||
|
box.className = 'ch-share'; box.setAttribute('role', 'group'); box.setAttribute('aria-label', 'Bagikan tantangan ini');
|
||||||
|
box.innerHTML = `<button type="button" class="ch-btn" data-ch="code" title="Salin kode ${esc(pc)}">SALIN KODE</button><button type="button" class="ch-btn" data-ch="link" title="Salin tautan yang membuka tantangan ini">TAUTAN</button>`;
|
||||||
|
box.addEventListener('click', e => { const b = e.target.closest('[data-ch]'); if (b) share(b.dataset.ch, cfg, b); });
|
||||||
|
head.append(box);
|
||||||
|
if (rule.id !== 'none') { const k = head.querySelector('.rs-kicker'); if (k) k.textContent += ' · ' + rule.short; }
|
||||||
|
}
|
||||||
|
function addAboutNote(root) {
|
||||||
|
const panel = root.querySelector('.about-panel'); if (!panel || panel.querySelector('.ch-about')) return;
|
||||||
|
const n = document.createElement('small'); n.className = 'ch-about'; n.textContent = 'Semua data permainan (koin, pencapaian, statistik kebiasaan bermain) hanya tersimpan di browser ini dan tidak dikirim ke mana pun.';
|
||||||
|
const credit = panel.querySelector(':scope > small'); if (credit) credit.after(n); else panel.querySelector('.menu-primary')?.before(n);
|
||||||
|
}
|
||||||
|
// menu.js memanggil ini di akhir tiap render(); ctx = {player, enemy, stage, level, mode, p2, runKind, result}.
|
||||||
|
const decorate = guard((root, screen, ctx) => {
|
||||||
|
if (!root) return;
|
||||||
|
if (screen === 'home') { selected = 'none'; addEntry(root); }
|
||||||
|
else if (screen === 'arena') addArenaTools(root, ctx);
|
||||||
|
else if (screen === 'result') addResultTools(root, ctx);
|
||||||
|
else if (screen === 'about') addAboutNote(root);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- kebiasaan bermain ----------
|
||||||
|
const MODE_NAME = { versus: 'VS Komputer', training: 'Latihan', arcade: 'Arcade', survival: 'Survival', '2p': 'Dua Pemain', challenge: 'Tantangan', daily: 'Tantangan harian' };
|
||||||
|
const KEY_OK = /^[\w-]{1,40}$/;
|
||||||
|
const cleanMap = m => { const out = {}; if (m && typeof m === 'object' && !Array.isArray(m)) for (const [k, v] of Object.entries(m)) { const n = Math.floor(+v); if (KEY_OK.test(k) && n > 0 && n < 1e9) out[k] = n; } return out; };
|
||||||
|
function loadHabits() {
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(store.get(KEY_HABITS) || 'null');
|
||||||
|
if (raw && typeof raw === 'object') { const h = { v: 1, ch: cleanMap(raw.ch), mode: cleanMap(raw.mode), stage: cleanMap(raw.stage), foe: cleanMap(raw.foe) }; h.n = Math.max(Math.floor(+raw.n) || 0, Object.values(h.mode).reduce((a, b) => a + b, 0)); return h; }
|
||||||
|
} catch (_) { /* data rusak: mulai dari nol */ }
|
||||||
|
return { v: 1, n: 0, ch: {}, mode: {}, stage: {}, foe: {} };
|
||||||
|
}
|
||||||
|
let hab = loadHabits();
|
||||||
|
function habitMode(o, challenge) {
|
||||||
|
if (o.mode === 'training') return 'training';
|
||||||
|
if (challenge) return 'challenge';
|
||||||
|
const vs = o.vs && typeof o.vs === 'object' ? o.vs : null, tag = String(vs?.label || '').toUpperCase();
|
||||||
|
if (vs?.p2) return '2p';
|
||||||
|
if (o.shadow || tag.startsWith('ARCADE')) return 'arcade';
|
||||||
|
if (tag.startsWith('SURVIVAL')) return 'survival';
|
||||||
|
return typeof o.kind === 'string' && Object.hasOwn(MODE_NAME, o.kind) ? o.kind : 'versus'; // o.kind: mode baru bisa menandai dirinya
|
||||||
|
}
|
||||||
|
// Dipanggil begin() untuk tiap pertandingan yang benar-benar dimulai. Bayangan (lawan = salinan pemain) tidak dihitung sebagai karakter lawan.
|
||||||
|
function recordHabits(o) {
|
||||||
|
const mode = o.mode === undefined ? 'versus' : o.mode, ch = !!(o.codeRule && typeof o.codeRule === 'object' && mode === 'versus' && !(o.vs && o.vs.p2) && !o.shadow);
|
||||||
|
const bump = (m, k) => { if (typeof k === 'string' && KEY_OK.test(k)) m[k] = (m[k] || 0) + 1; };
|
||||||
|
hab.n++; bump(hab.ch, o.player); bump(hab.mode, habitMode(o, ch)); bump(hab.stage, o.stage); if (!o.shadow) bump(hab.foe, o.enemy);
|
||||||
|
store.set(KEY_HABITS, JSON.stringify(hab));
|
||||||
|
}
|
||||||
|
const track = guard(recordHabits);
|
||||||
|
// Pilihan teratas satu kelompok: jumlah terbanyak, seri menurut nama kunci (supaya tidak berubah-ubah); yang sudah tidak ada di game dilewati.
|
||||||
|
function topOf(map, known, nameOf) {
|
||||||
|
let best = null; for (const [k, n] of Object.entries(map)) { if (!known(k)) continue; if (!best || n > best.n || (n === best.n && k < best.k)) best = { k, n }; }
|
||||||
|
return best ? { name: nameOf(best.k), n: best.n, pct: hab.n ? Math.round(100 * best.n / hab.n) : 0 } : null;
|
||||||
|
}
|
||||||
|
const habitSummary = () => ({
|
||||||
|
n: hab.n, ch: topOf(hab.ch, hasFighter, fighterName), mode: topOf(hab.mode, k => Object.hasOwn(MODE_NAME, k), k => MODE_NAME[k]),
|
||||||
|
stage: topOf(hab.stage, hasStage, stageName), foe: topOf(hab.foe, hasFighter, fighterName)
|
||||||
|
});
|
||||||
|
function habitsHtml() {
|
||||||
|
const s = habitSummary(), tile = (name, v) => `<div><dt>${name}</dt><dd>${v ? `${esc(v.name)}<small>${v.n}× · ${v.pct}%</small>` : '—'}</dd></div>`;
|
||||||
|
return '<h3>Kebiasaan bermainmu</h3>' + (s.n
|
||||||
|
? `<dl class="hb-grid" aria-label="Kebiasaan bermain">${tile('Karakter favorit', s.ch)}${tile('Mode favorit', s.mode)}${tile('Arena favorit', s.stage)}${tile('Lawan paling sering', s.foe)}</dl>`
|
||||||
|
+ `<p class="meta-hint">Dihitung dari ${s.n} pertandingan yang kamu mulai. Hanya tersimpan di browser ini dan tidak dikirim ke mana pun.</p>`
|
||||||
|
: '<p class="meta-hint">Belum ada data. Mainkan beberapa pertandingan, lalu kebiasaanmu muncul di sini. Hanya tersimpan di browser ini.</p>')
|
||||||
|
+ '<p><button type="button" class="pill danger" data-hb-clear>HAPUS STATISTIK</button></p>';
|
||||||
|
}
|
||||||
|
function clearHabits() { hab = { v: 1, n: 0, ch: {}, mode: {}, stage: {}, foe: {} }; store.del(KEY_HABITS); }
|
||||||
|
// Tab Koleksi dibuat ulang oleh meta.js setiap render; bagian ini disisipkan sesudahnya (sebelum daftar pencapaian) tanpa mengubah meta.js.
|
||||||
|
function injectBook(ov) {
|
||||||
|
const panel = ov.querySelector('.meta-panel'), on = panel?.querySelector('nav .mt.on'); if (!on || on.dataset.tab !== 'book') return;
|
||||||
|
const body = panel.querySelector('.meta-body'); if (!body || body.querySelector('.hb')) return;
|
||||||
|
const sec = document.createElement('section'); sec.className = 'hb'; sec.innerHTML = habitsHtml();
|
||||||
|
const anchor = [...body.querySelectorAll('h3')].find(h => /^Pencapaian/i.test(h.textContent.trim()));
|
||||||
|
if (anchor) anchor.before(sec); else body.append(sec);
|
||||||
|
}
|
||||||
|
function onBookClick(e, ov) {
|
||||||
|
if (!e.target.closest?.('[data-hb-clear]')) return;
|
||||||
|
if (!hab.n) { say('Belum ada statistik yang tersimpan.'); return; }
|
||||||
|
if (!confirm('Hapus statistik kebiasaan bermain? Koin, skin, dan pencapaianmu tidak ikut terhapus.')) return;
|
||||||
|
clearHabits(); const sec = ov.querySelector('.hb'); if (sec) sec.innerHTML = habitsHtml(); say('Statistik kebiasaan bermain dihapus.');
|
||||||
|
}
|
||||||
|
function watchMeta() {
|
||||||
|
const hook = ov => {
|
||||||
|
if (ov._chWatch) return; ov._chWatch = true;
|
||||||
|
const run = () => { try { injectBook(ov); } catch (e) { console.error(e); } };
|
||||||
|
new MutationObserver(run).observe(ov, { childList: true, subtree: true });
|
||||||
|
ov.addEventListener('click', e => onBookClick(e, ov)); run();
|
||||||
|
};
|
||||||
|
const found = document.getElementById('meta-overlay'); if (found) { hook(found); return; }
|
||||||
|
const mo = new MutationObserver(() => { const ov = document.getElementById('meta-overlay'); if (ov) { mo.disconnect(); hook(ov); } });
|
||||||
|
mo.observe(document.body, { childList: true }); // meta.js membuat hamparan ini saat pertama kali dibuka
|
||||||
|
}
|
||||||
|
try { watchMeta(); } catch (e) { console.error(e); }
|
||||||
|
|
||||||
|
window.Challenge = {
|
||||||
|
encode: guard(cfg => encode(cfg), null), decode: guard(raw => decode(raw), fail('error', 'Kode tidak bisa dibaca.')), pretty, shareUrl, copyText,
|
||||||
|
RULES: RULES.map(r => ({ id: r.id, name: r.name, short: r.short, desc: r.desc })), ruleOf,
|
||||||
|
open, start: guard(start, false), pending, decorate,
|
||||||
|
begin, tune, roundStart, roundEnd, finish, hud,
|
||||||
|
get noBlock() { return !!cur()?.noBlock; },
|
||||||
|
get rule() { return selected; }, set rule(id) { selected = ruleOf(id).id; },
|
||||||
|
get active() { return active ? { ...active } : null; },
|
||||||
|
state: () => ({ selected, active: active ? { ...active } : null, last: { ...last } })
|
||||||
|
};
|
||||||
|
window.Habits = { data: () => JSON.parse(JSON.stringify(hab)), summary: habitSummary, html: habitsHtml, record: track, clear: clearHabits, KEY: KEY_HABITS };
|
||||||
|
})();
|
||||||
|
|
@ -32,6 +32,7 @@ Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks un
|
||||||
- HUD (game.js): meter ultimate `#ult-player`/`#ult-enemy` (`updateUltMeters`; mengisi/SIAP/waktu mode dari `modeOf`, `bwx.rage`, bentuk FENR; DOM hanya ditulis saat tampilan berubah). Menu jeda `#pause-panel` dua panel; `buildMoveList()` membangun ulang Daftar gerakan tiap `setPaused(true)`, pilihan lipat diingat di `localStorage['konoha.moveList']`.
|
- HUD (game.js): meter ultimate `#ult-player`/`#ult-enemy` (`updateUltMeters`; mengisi/SIAP/waktu mode dari `modeOf`, `bwx.rage`, bentuk FENR; DOM hanya ditulis saat tampilan berubah). Menu jeda `#pause-panel` dua panel; `buildMoveList()` membangun ulang Daftar gerakan tiap `setPaused(true)`, pilihan lipat diingat di `localStorage['konoha.moveList']`.
|
||||||
- Mode cerita Arcade dan bos Bayangan (Tahap 3.5 dan 3.6): `content/story.js` (`window.STORY`: `intro[id]` 2-3 kalimat, `between[A][B]` 1-2 kalimat (atau fungsi), `ending[id]` 3 kalimat, `boss` = pembuka, ucapan VS/kalah/menang, kalimat akhir, gelar; `generic` cadangan dengan penampung `{name}` `{rival}` `{n}`) dan `story.js` + `story.css` (`window.Story`). `Story.show({kind: intro|between|boss|ending, player, enemy, step, total, stage, boss}, onDone)` menampilkan kartu layar penuh (`#story-screen`, z-index 45, di atas layar VS 40) dan memanggil onDone tepat satu kali: klik/ketuk/Spasi/Enter/Esc setelah 0,5 dtk, atau otomatis (kartu antar-lawan sekitar 4 dtk, kartu panjang lebih lama); selama tampil semua tombol ditahan dan `Meta` menahan toast (`Story.active`). Mengembalikan false tanpa kartu bila tidak ada teks, jadi alur tetap jalan tanpa cerita. `meta.js` Arcade: `startRun` (kartu pembuka) -> `nextFight` -> menang -> `betweenFights` (kartu antar-lawan, panggung lawan berikutnya jadi latar kartu) ... lawan terakhir kalah -> `arcadeCleared` (hadiah tamat 500 + pencapaian langsung dibayar seperti dulu) -> kartu bos -> `startBoss` -> `finishRun(true, 'win'|'lose')` -> kartu akhir -> layar hasil. Survival tidak memakai cerita. `run.boss` = null | 'pending' | 'fight'. Menang atas Bayangan: +1000 koin, pencapaian BAYANGAN (+50), gelar PENAKLUK BAYANGAN (`Meta.save.title`, tampil di kartu akhir dan plat pemenang layar hasil); kalah: Arcade tetap tamat, tanpa bonus bos. Uji manual: `?arcade=N` memendekkan Arcade menjadi N lawan. Bayangan = `startMatch({player: X, enemy: X, level: 'excellent', shadow: true})` (id sama; state `hero` dan `dummy` tetap terpisah): `setShadowFoe` mengatur `BALANCE.dummyMax` (+40%), `foeDamage` menambah 10% pada damage ke pemain (pembungkus `__game.receiveHit` di akhir game.js dan `dotDamage`), `dummyHpShown` memberi skala HP dasar untuk bar dan penentu waktu habis (`MatchRules.tick`), `drawDummy` memakai `ctx.filter` gelap + lapisan ungu `drawShadowTint` + aura, percikan ungu lewat `fxr()` di `updateVisuals`. HUD: nama BAYANGAN dengan label nama asli, kelas `.shadow-foe` (story.css). VS: game.js memanggil `Story.decorateVs(info)` tepat setelah `VS.show` (nama 'BAYANGAN <NAMA>', ucapan bos); layar hasil: `ResultView.view` membaca `run.boss` (Bayangan di sisi kalah; kalah dari Bayangan tetap tampil TAMAT). `__game.shadowFoe()` melaporkan keadaan.
|
- Mode cerita Arcade dan bos Bayangan (Tahap 3.5 dan 3.6): `content/story.js` (`window.STORY`: `intro[id]` 2-3 kalimat, `between[A][B]` 1-2 kalimat (atau fungsi), `ending[id]` 3 kalimat, `boss` = pembuka, ucapan VS/kalah/menang, kalimat akhir, gelar; `generic` cadangan dengan penampung `{name}` `{rival}` `{n}`) dan `story.js` + `story.css` (`window.Story`). `Story.show({kind: intro|between|boss|ending, player, enemy, step, total, stage, boss}, onDone)` menampilkan kartu layar penuh (`#story-screen`, z-index 45, di atas layar VS 40) dan memanggil onDone tepat satu kali: klik/ketuk/Spasi/Enter/Esc setelah 0,5 dtk, atau otomatis (kartu antar-lawan sekitar 4 dtk, kartu panjang lebih lama); selama tampil semua tombol ditahan dan `Meta` menahan toast (`Story.active`). Mengembalikan false tanpa kartu bila tidak ada teks, jadi alur tetap jalan tanpa cerita. `meta.js` Arcade: `startRun` (kartu pembuka) -> `nextFight` -> menang -> `betweenFights` (kartu antar-lawan, panggung lawan berikutnya jadi latar kartu) ... lawan terakhir kalah -> `arcadeCleared` (hadiah tamat 500 + pencapaian langsung dibayar seperti dulu) -> kartu bos -> `startBoss` -> `finishRun(true, 'win'|'lose')` -> kartu akhir -> layar hasil. Survival tidak memakai cerita. `run.boss` = null | 'pending' | 'fight'. Menang atas Bayangan: +1000 koin, pencapaian BAYANGAN (+50), gelar PENAKLUK BAYANGAN (`Meta.save.title`, tampil di kartu akhir dan plat pemenang layar hasil); kalah: Arcade tetap tamat, tanpa bonus bos. Uji manual: `?arcade=N` memendekkan Arcade menjadi N lawan. Bayangan = `startMatch({player: X, enemy: X, level: 'excellent', shadow: true})` (id sama; state `hero` dan `dummy` tetap terpisah): `setShadowFoe` mengatur `BALANCE.dummyMax` (+40%), `foeDamage` menambah 10% pada damage ke pemain (pembungkus `__game.receiveHit` di akhir game.js dan `dotDamage`), `dummyHpShown` memberi skala HP dasar untuk bar dan penentu waktu habis (`MatchRules.tick`), `drawDummy` memakai `ctx.filter` gelap + lapisan ungu `drawShadowTint` + aura, percikan ungu lewat `fxr()` di `updateVisuals`. HUD: nama BAYANGAN dengan label nama asli, kelas `.shadow-foe` (story.css). VS: game.js memanggil `Story.decorateVs(info)` tepat setelah `VS.show` (nama 'BAYANGAN <NAMA>', ucapan bos); layar hasil: `ResultView.view` membaca `run.boss` (Bayangan di sisi kalah; kalah dari Bayangan tetap tampil TAMAT). `__game.shadowFoe()` melaporkan keadaan.
|
||||||
- Statistik pertandingan (RENCANA 5.5): `stats.js` (`window.MatchStats`) + `stats.css`; tag-nya ada di baris sendiri sebelum `</main>` di `index.html`. Statistik pemain 1 per pertandingan versus (juga 2 pemain, Arcade, Survival, Bayangan; latihan = `reset(false)`, tanpa statistik). Kait di game.js, semuanya satu baris dengan `window.MatchStats?.`: `reset(versus)` di `startMatch` dan `reset`; `tick(dt, match, hero.hp, dummy.damage, BALANCE.dummyMax)` di awal `advanceMatch` (damage keluar/diterima = selisih HP per langkah, durasi = jumlah waktu fase fight, ronde = peralihan fight -> ko); `cast(jenis, hero.action)` di `cast` dan `startAttack`; `hit(hitSource(), counter, comboHits, hero.action)` di `hitDummy` (juga jalur chip blok CPU) dan `hit(null, ...)` di burst `dotDamage`; `parry()`/`block()` di `heroGuard`; `finish(info)` di event 'complete' menaruh ringkasan di `info.stats` (dibawa `Meta.award` dan `FrontEnd.showResult`). Akurasi jurus = pemakaian yang kena sekali; pukulan proyektil/ultimate dikaitkan ke jurus terakhir dalam jendela waktu (`MatchStats.WINDOW`), jadi bila dua jurus bertumpuk bisa meleset satu. Layar hasil: `ResultView.view` memanggil `MatchStats.resultHtml` (tombol STATISTIK di kanan baris skor + kartu `.rs-ms` yang menutup kolom tengah di atas tombol aksi, diatur `MatchStats.mount`; Esc menutup kartu lebih dulu daripada kembali ke menu); tanpa `info.stats` (data uji) tidak ada tombol. Simpanan seumur hidup: `Meta.award` memanggil `MatchStats.commit(save.stats, r.stats)` (kunci baru `dmgDealt, dmgTaken, parries, blocks, throws, counters, rounds, playSec, statMatches`; `bestCombo` ikut dinaikkan ke kombo terbaik seluruh ronde; simpanan lama tanpa kunci terbaca 0), tampil di Koleksi lewat `MatchStats.lifetimeHtml`. Uji manual cepat: `MatchStats.snapshot()` di konsol saat pertandingan berjalan. Belum ada skenario otomatis khusus statistik (mode ringan, diuji manual).
|
- Statistik pertandingan (RENCANA 5.5): `stats.js` (`window.MatchStats`) + `stats.css`; tag-nya ada di baris sendiri sebelum `</main>` di `index.html`. Statistik pemain 1 per pertandingan versus (juga 2 pemain, Arcade, Survival, Bayangan; latihan = `reset(false)`, tanpa statistik). Kait di game.js, semuanya satu baris dengan `window.MatchStats?.`: `reset(versus)` di `startMatch` dan `reset`; `tick(dt, match, hero.hp, dummy.damage, BALANCE.dummyMax)` di awal `advanceMatch` (damage keluar/diterima = selisih HP per langkah, durasi = jumlah waktu fase fight, ronde = peralihan fight -> ko); `cast(jenis, hero.action)` di `cast` dan `startAttack`; `hit(hitSource(), counter, comboHits, hero.action)` di `hitDummy` (juga jalur chip blok CPU) dan `hit(null, ...)` di burst `dotDamage`; `parry()`/`block()` di `heroGuard`; `finish(info)` di event 'complete' menaruh ringkasan di `info.stats` (dibawa `Meta.award` dan `FrontEnd.showResult`). Akurasi jurus = pemakaian yang kena sekali; pukulan proyektil/ultimate dikaitkan ke jurus terakhir dalam jendela waktu (`MatchStats.WINDOW`), jadi bila dua jurus bertumpuk bisa meleset satu. Layar hasil: `ResultView.view` memanggil `MatchStats.resultHtml` (tombol STATISTIK di kanan baris skor + kartu `.rs-ms` yang menutup kolom tengah di atas tombol aksi, diatur `MatchStats.mount`; Esc menutup kartu lebih dulu daripada kembali ke menu); tanpa `info.stats` (data uji) tidak ada tombol. Simpanan seumur hidup: `Meta.award` memanggil `MatchStats.commit(save.stats, r.stats)` (kunci baru `dmgDealt, dmgTaken, parries, blocks, throws, counters, rounds, playSec, statMatches`; `bestCombo` ikut dinaikkan ke kombo terbaik seluruh ronde; simpanan lama tanpa kunci terbaca 0), tampil di Koleksi lewat `MatchStats.lifetimeHtml`. Uji manual cepat: `MatchStats.snapshot()` di konsol saat pertandingan berjalan. Belum ada skenario otomatis khusus statistik (mode ringan, diuji manual).
|
||||||
|
- Kode tantangan dan kebiasaan bermain (RENCANA 7.5 dan 7.6): `challenge.js` (`window.Challenge`, `window.Habits`) + `challenge.css`; tag-nya sebelum `menu.js` di `index.html`. **Kode** = 8 karakter dari 30 simbol tanpa 0 1 I L O U (contoh `TNNB-TPZM`), isinya 24 bit (versi, variasi, aturan, tingkat, arena, lawan, pemain) + 15 bit pemeriksa yang dicampur; `Challenge.encode({player, enemy, stage, level, rule})` / `Challenge.decode(teks)` (menerima huruf kecil, spasi, tanda hubung, tautan `?kode=`, atau kalimat yang memuat kode; hasil `{ok, cfg}` atau `{ok:false, err, msg}`). Daftar karakter, arena, tingkat, dan aturan di berkas itu menentukan arti kode: **urutannya tidak boleh diubah, hanya ditambah di belakang** (karakter/arena baru yang belum didaftarkan ikut otomatis di belakang menurut urutan deklarasi). **Aturan khusus** (`RULES`, satu per kode): `blitz` Adu cepat (satu ronde, HP awal separuh), `one` Satu ronde, `fast` Lawan gesit (jalan +25%, cooldown pulih +35%), `noblock` Tanpa blok (blok dan parry mati untuk kedua sisi), `heavy` Pukulan berat (damage +50% kedua sisi); kuncinya hanya hidup di kode, tidak disimpan di localStorage (jadi tidak bentrok dengan aturan harian). **Alur**: tombol KODE TANTANGAN di pojok kiri atas menu utama (di sebelah TUTORIAL, absolut supaya tinggi menu tidak berubah) membuka dialog `#ch-dialog` yang memvalidasi sambil mengetik dan langsung memulai (`Challenge.start(cfg)` memanggil `startMatch({... mode:'versus', vs:{label}, codeRule:{rule, code}})`); `?kode=XXXXXXXX` di alamat membuka dialog yang sama sebagai undangan setelah boot (parameter dihapus dari alamat sesudahnya; `mobile.html` meneruskan query ke iframe). Layar arena (hanya versus melawan CPU) mendapat baris `ATURAN` (menggilir aturan, berlaku juga untuk LET'S FIGHT dan MAIN LAGI lewat `Challenge.pending(mode, p2)` di `menu.js start()`; menu utama mengosongkannya) / `SALIN KODE` / `TAUTAN` di kolom kanan `.match-rules`; layar hasil mendapat `SALIN KODE` / `TAUTAN` di pojok kanan atas `.rs-head` (tidak untuk Arcade, Survival, dua pemain, atau Bayangan). Penyalinan: `navigator.clipboard`, lalu `execCommand('copy')`, lalu `prompt()`. **Kait** (semua `window.Challenge?.x?.()`, dibungkus `guard`: galat hanya dicatat di konsol, tanpa berkas ini game berjalan seperti dulu): game.js `begin(options)` di `startMatch` sesudah validasi dan sebelum `refreshPassives` (menetapkan aturan aktif, judul layar VS "TANTANGAN · ...", penanda `.ch-badge` di bawah jam HUD, dan mencatat kebiasaan), `tune(sideMods)` di akhir `refreshPassives` (aturan = pengali kunci `passiveMod`), `roundStart(hero, dummy, BALANCE)` di akhir `resetCombat` (HP awal), `roundEnd(match, event)` dan `finish(info)` di `advanceMatch` (satu ronde menetapkan `match.winner` sebelum efek K.O. akhir; `finish` mengingat pengaturan untuk SALIN KODE dan mematikan `info.perfect` pada aturan satu ronde supaya bonus/pencapaian BERSIH tidak murah), `hud(match)` di `updateMatchHud` (teks "SATU RONDE"), `noBlock` di `press` (tombol L) dan `heroGuard`; menu.js `decorate(root, screen, {player, enemy, stage, level, mode, p2, runKind, result})` di akhir `render()`. `startMatch` mengabaikan `codeRule` untuk latihan, dua pemain, dan Bayangan. Aturan tidak memakai pengacak, jadi hasil pertandingan tetap deterministik. **Kebiasaan bermain** (`Habits`): `localStorage['konoha.habits.v1']` = `{v:1, n, ch, mode, stage, foe}` (hitungan per karakter yang dipilih, mode, arena, dan karakter lawan; tanpa waktu dan identitas), dicatat di `begin()` tiap pertandingan dimulai (latihan juga; Bayangan tidak dihitung sebagai lawan; mode dikenali dari `options.mode`, `vs.p2`, awalan label ARCADE/SURVIVAL, `codeRule`, atau `options.kind` untuk mode baru). Terpisah dari `Meta.save`, jadi simpanan lama tak tersentuh dan data rusak dibaca sebagai nol. Bagian "Kebiasaan bermainmu" (karakter, mode, arena favorit, lawan paling sering, tombol HAPUS STATISTIK) disisipkan ke tab Koleksi oleh `MutationObserver` pada `#meta-overlay` (sebelum daftar pencapaian; `meta.js` tidak diubah); catatan privasi satu baris di layar About lewat `decorate`. Belum ada skenario otomatis dan UI belum diperiksa di peramban oleh penulisnya (hanya `node --check`, smoke, dan pemeriksaan penyandi sekali pakai: 5.880 kombinasi bolak-balik tanpa tabrakan): uji manual di 1024x520 dan 844x390. `precache.js`/`files.json` perlu dibangun ulang agar `challenge.js`/`challenge.css` ikut tersimpan offline. Konsol: `Challenge.state()`, `Challenge.open()`, `Habits.data()`.
|
||||||
- `assets/<id>/` atlas sprite (15 state), `manifest.js` (data + metrik), `ui/`; `work/` (di repo induk, luar folder ini) berisi pipeline pembuat karakter.
|
- `assets/<id>/` atlas sprite (15 state), `manifest.js` (data + metrik), `ui/`; `work/` (di repo induk, luar folder ini) berisi pipeline pembuat karakter.
|
||||||
- `precache.js`/`files.json`: **dibuat otomatis** oleh `tools/build_precache.py` — jangan diedit atau di-commit oleh cabang fitur; perintahkan integrator membangun ulang.
|
- `precache.js`/`files.json`: **dibuat otomatis** oleh `tools/build_precache.py` — jangan diedit atau di-commit oleh cabang fitur; perintahkan integrator membangun ulang.
|
||||||
|
|
||||||
|
|
|
||||||
9
game.js
9
game.js
|
|
@ -45,6 +45,7 @@
|
||||||
sideStyle.player = P.styleKey(styles.player ?? window.Meta?.styleOf?.(selectedCharacter));
|
sideStyle.player = P.styleKey(styles.player ?? window.Meta?.styleOf?.(selectedCharacter));
|
||||||
sideStyle.enemy = P.styleKey(styles.enemy === 'acak' ? P.randomStyle(styleRand) : styles.enemy === 'p2' ? window.Meta?.styleOf?.(opponentCharacter) : styles.enemy);
|
sideStyle.enemy = P.styleKey(styles.enemy === 'acak' ? P.randomStyle(styleRand) : styles.enemy === 'p2' ? window.Meta?.styleOf?.(opponentCharacter) : styles.enemy);
|
||||||
sideMods.player = P.merge(selectedCharacter, sideStyle.player); sideMods.enemy = P.merge(opponentCharacter, sideStyle.enemy);
|
sideMods.player = P.merge(selectedCharacter, sideStyle.player); sideMods.enemy = P.merge(opponentCharacter, sideStyle.enemy);
|
||||||
|
window.Challenge?.tune?.(sideMods); // aturan khusus kode tantangan mengalikan angka kedua sisi (challenge.js)
|
||||||
}
|
}
|
||||||
// Pertandingan sungguhan melawan CPU (punya layar VS, bukan P2) memakai gaya acak untuk CPU; dua pemain memakai gaya pilihan tiap karakter; latihan dan uji tanpa options.styles memakai Seimbang.
|
// Pertandingan sungguhan melawan CPU (punya layar VS, bukan P2) memakai gaya acak untuk CPU; dua pemain memakai gaya pilihan tiap karakter; latihan dan uji tanpa options.styles memakai Seimbang.
|
||||||
function matchStyles(options, mode) { const o = options.styles || {}; return { player: o.player, enemy: o.enemy ?? (mode === 'versus' && options.vs ? (options.vs.p2 ? 'p2' : 'acak') : 'seimbang') }; }
|
function matchStyles(options, mode) { const o = options.styles || {}; return { player: o.player, enemy: o.enemy ?? (mode === 'versus' && options.vs ? (options.vs.p2 ? 'p2' : 'acak') : 'seimbang') }; }
|
||||||
|
|
@ -239,6 +240,7 @@
|
||||||
const {player='arco',enemy='fenr',stage='bellora',level='medium',mode='versus'}=options; p2Mode=false; keys2.clear();
|
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(!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;
|
if(!images['stage-'+stage])return false;
|
||||||
|
window.Challenge?.begin?.(options); // kode tantangan (challenge.js): aturan khusus (options.codeRule) dan statistik kebiasaan; harus sebelum refreshPassives
|
||||||
selectedCharacter=player;opponentCharacter=enemy;stageId=stage;difficulty=level;images.stage=images['stage-'+stage];CONFIG.groundY=Rules.stages[stage].groundY;refreshPassives(matchStyles(options,mode));
|
selectedCharacter=player;opponentCharacter=enemy;stageId=stage;difficulty=level;images.stage=images['stage-'+stage];CONFIG.groundY=Rules.stages[stage].groundY;refreshPassives(matchStyles(options,mode));
|
||||||
setShadowFoe(!!options.shadow&&mode==='versus');
|
setShadowFoe(!!options.shadow&&mode==='versus');
|
||||||
aiEnabled=mode==='versus';match=Rules.create(mode);matchBest=0;roundCues.length=0;vsAbort();setMenuOpen(false);resetCombat();warmCutins();
|
aiEnabled=mode==='versus';match=Rules.create(mode);matchBest=0;roundCues.length=0;vsAbort();setMenuOpen(false);resetCombat();warmCutins();
|
||||||
|
|
@ -294,6 +296,7 @@
|
||||||
for(const event of Rules.tick(match,dt,hero.hp,dummyHpShown())) {
|
for(const event of Rules.tick(match,dt,hero.hp,dummyHpShown())) {
|
||||||
if(event.type==='cue')emitRoundCue(event.cue,event.round);
|
if(event.type==='cue')emitRoundCue(event.cue,event.round);
|
||||||
if(event.type==='round-end') {
|
if(event.type==='round-end') {
|
||||||
|
window.Challenge?.roundEnd?.(match,event); // aturan "satu ronde": menetapkan match.winner sebelum efek K.O. akhir (challenge.js)
|
||||||
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;
|
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(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);
|
if(!event.timeout&&event.winner!=='draw'||event.doubleKO)startKoFx(event.doubleKO?'both':event.winner==='player'?'dummy':'hero',!!match.winner);
|
||||||
|
|
@ -308,6 +311,7 @@
|
||||||
if(event.type==='complete'){
|
if(event.type==='complete'){
|
||||||
// bestCombo (kombo terbaik ronde terakhir) tetap dasar bonus koin; matchBest (seluruh ronde) hanya untuk layar hasil.
|
// 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,boss:shadowFoe};
|
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,boss:shadowFoe};
|
||||||
|
window.Challenge?.finish?.(info); // mengingat pengaturan untuk SALIN KODE; "bersih" tidak berlaku untuk aturan satu ronde (challenge.js)
|
||||||
info.stats=window.MatchStats?.finish(info)||null;
|
info.stats=window.MatchStats?.finish(info)||null;
|
||||||
const metaHandled=!!window.Meta?.award?.(info);
|
const metaHandled=!!window.Meta?.award?.(info);
|
||||||
if(!metaHandled)window.FrontEnd?.showResult({...info,mode:'versus',award:window.Meta?.lastAward||null});
|
if(!metaHandled)window.FrontEnd?.showResult({...info,mode:'versus',award:window.Meta?.lastAward||null});
|
||||||
|
|
@ -326,6 +330,7 @@
|
||||||
$('#round-call-title').textContent=!show?'':match.phase==='intro'?(match.fightCue?'FIGHT':`ROUND ${match.round}`):match.endText||'K.O.';
|
$('#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';
|
$('#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';
|
||||||
canvas.dataset.mode=match?.mode||'training';canvas.dataset.matchPhase=match?.phase||'fight';canvas.dataset.round=String(match?.round||1);canvas.dataset.opponent=opponentCharacter;canvas.dataset.difficulty=difficulty;canvas.dataset.stage=stageId;
|
canvas.dataset.mode=match?.mode||'training';canvas.dataset.matchPhase=match?.phase||'fight';canvas.dataset.round=String(match?.round||1);canvas.dataset.opponent=opponentCharacter;canvas.dataset.difficulty=difficulty;canvas.dataset.stage=stageId;
|
||||||
|
window.Challenge?.hud?.(match); // teks ronde untuk aturan "satu ronde" (challenge.js)
|
||||||
}
|
}
|
||||||
|
|
||||||
function canAct() { return ready && !menuOpen && (!match || match.phase === 'fight') && !paused && !settingsOpen && hero.hurtTime <= 0 && hero.hp > 0; }
|
function canAct() { return ready && !menuOpen && (!match || match.phase === 'fight') && !paused && !settingsOpen && hero.hurtTime <= 0 && hero.hp > 0; }
|
||||||
|
|
@ -397,7 +402,7 @@
|
||||||
if (key === 'escape') { if (settingsOpen) closeSettings(); else setPaused(!paused); return; }
|
if (key === 'escape') { if (settingsOpen) closeSettings(); else setPaused(!paused); return; }
|
||||||
if (key === 'r') { reset(); return; }
|
if (key === 'r') { reset(); return; }
|
||||||
if (p2Mode && P2_KEYS.includes(key)) { p2Press(key); return; }
|
if (p2Mode && P2_KEYS.includes(key)) { p2Press(key); return; }
|
||||||
if (!canAct() || keys.has(key)) return;
|
if (!canAct() || keys.has(key) || (key === 'l' && window.Challenge?.noBlock)) return; // aturan "tanpa blok" (challenge.js): L tidak berbuat apa-apa
|
||||||
keys.add(key); logInput(key);
|
keys.add(key); logInput(key);
|
||||||
if (key === 'l') { hero.parry = .16 * passiveMod(hero, 'parry'); }
|
if (key === 'l') { hero.parry = .16 * passiveMod(hero, 'parry'); }
|
||||||
if (key === 'j') startThrow();
|
if (key === 'j') startThrow();
|
||||||
|
|
@ -541,6 +546,7 @@
|
||||||
// Sesudahnya hero.autoGuard menahan sikap blok 0,3 dtk (lihat step) supaya bingkai blok terlihat dan pukulan berantai ikut ditahan.
|
// Sesudahnya hero.autoGuard menahan sikap blok 0,3 dtk (lihat step) supaya bingkai blok terlihat dan pukulan berantai ikut ditahan.
|
||||||
const autoGuardReady = () => !!window.Access?.autoBlock && hero.grounded && hero.hurtTime <= 0 && (hero.guardBreak || 0) <= 0 && hero.doublePose <= 0 && (dummy.x - hero.x) * hero.facing > 0;
|
const autoGuardReady = () => !!window.Access?.autoBlock && hero.grounded && hero.hurtTime <= 0 && (hero.guardBreak || 0) <= 0 && hero.doublePose <= 0 && (dummy.x - hero.x) * hero.facing > 0;
|
||||||
function heroGuard(damage, options) {
|
function heroGuard(damage, options) {
|
||||||
|
if (window.Challenge?.noBlock) return false; // aturan "tanpa blok" (challenge.js), termasuk blok otomatis bantuan
|
||||||
const auto = !hero.blocking && !(hero.parry > 0) && autoGuardReady();
|
const auto = !hero.blocking && !(hero.parry > 0) && autoGuardReady();
|
||||||
if (!(hero.blocking || hero.parry > 0 || auto) || hero.action || options?.unblockable) return false;
|
if (!(hero.blocking || hero.parry > 0 || auto) || hero.action || options?.unblockable) return false;
|
||||||
if (auto || hero.autoGuard > 0) hero.autoGuard = .3;
|
if (auto || hero.autoGuard > 0) hero.autoGuard = .3;
|
||||||
|
|
@ -596,6 +602,7 @@
|
||||||
particles.length = effects.length = projectiles.length = damageNumbers.length = 0; Feel.reset(); hero.flash = dummy.flash = 0; koSeen.dummy = koSeen.hero = false; totalDamage = comboHits = bestCombo = comboDamage = comboTimer = hitstop = trauma = cinematic = 0; clearInput(); setPaused(false); announce('TRAINING READY', 1.25);
|
particles.length = effects.length = projectiles.length = damageNumbers.length = 0; Feel.reset(); hero.flash = dummy.flash = 0; koSeen.dummy = koSeen.hero = false; totalDamage = comboHits = bestCombo = comboDamage = comboTimer = hitstop = trauma = cinematic = 0; clearInput(); setPaused(false); announce('TRAINING READY', 1.25);
|
||||||
bwReset(); squad = null; enemySquad = null; parade = null; enemyParade = null; flock = null; enemyFlock = null; serpent = null; enemySerpent = null; quake = null; enemyQuake = null; finale = null; enemyFinale = null; skyfall = null; enemySkyfall = null; orrery = null; enemyOrrery = null; sunroar = null; enemySunroar = null; delivery = null; enemyDelivery = null; tortoise = null; enemyTortoise = null;
|
bwReset(); squad = null; enemySquad = null; parade = null; enemyParade = null; flock = null; enemyFlock = null; serpent = null; enemySerpent = null; quake = null; enemyQuake = null; finale = null; enemyFinale = null; skyfall = null; enemySkyfall = null; orrery = null; enemyOrrery = null; sunroar = null; enemySunroar = null; delivery = null; enemyDelivery = null; tortoise = null; enemyTortoise = null;
|
||||||
for(const name of Object.keys(rechargePulse)) rechargePulse[name]=0;
|
for(const name of Object.keys(rechargePulse)) rechargePulse[name]=0;
|
||||||
|
window.Challenge?.roundStart?.(hero, dummy, BALANCE); // HP awal aturan khusus kode tantangan (challenge.js)
|
||||||
}
|
}
|
||||||
function reset() {
|
function reset() {
|
||||||
if(Rules)match=Rules.create(match?.mode||'training');
|
if(Rules)match=Rules.create(match?.mode||'training');
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="id">
|
<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>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><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">
|
||||||
<link rel="manifest" href="manifest.webmanifest"><link rel="icon" type="image/png" sizes="192x192" href="assets/pwa/icon-192.png"><link rel="apple-touch-icon" href="assets/pwa/apple-touch-icon.png"><link rel="stylesheet" href="touchlayout.css"><script src="pwa.js"></script><script src="touchlayout.js" defer></script>
|
<link rel="manifest" href="manifest.webmanifest"><link rel="icon" type="image/png" sizes="192x192" href="assets/pwa/icon-192.png"><link rel="apple-touch-icon" href="assets/pwa/apple-touch-icon.png"><link rel="stylesheet" href="touchlayout.css"><script src="pwa.js"></script><script src="touchlayout.js" defer></script>
|
||||||
<link rel="stylesheet" href="feel.css"><link rel="stylesheet" href="vs.css"><link rel="stylesheet" href="story.css"><link rel="stylesheet" href="mystery.css"><link rel="stylesheet" href="input.css"><link rel="stylesheet" href="access.css"><link rel="stylesheet" href="tutorial.css"></head>
|
<link rel="stylesheet" href="feel.css"><link rel="stylesheet" href="vs.css"><link rel="stylesheet" href="story.css"><link rel="stylesheet" href="mystery.css"><link rel="stylesheet" href="input.css"><link rel="stylesheet" href="access.css"><link rel="stylesheet" href="tutorial.css"><link rel="stylesheet" href="challenge.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><p class="boot-quote"></p></div></div><script src="content/loading.js"></script><script src="content/changelog.js"></script><script src="brand.js"></script><script src="content/story.js"></script><script src="story.js"></script><script src="precache.js"></script><script src="preload.js"></script><main id="game-shell" aria-label="Konoha Fighting, game fighting parodi">
|
<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><p class="boot-quote"></p></div></div><script src="content/loading.js"></script><script src="content/changelog.js"></script><script src="brand.js"></script><script src="content/story.js"></script><script src="story.js"></script><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>
|
<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">
|
<header class="combat-header">
|
||||||
|
|
@ -29,6 +29,6 @@
|
||||||
<link rel="stylesheet" href="stats.css"><script src="stats.js"></script>
|
<link rel="stylesheet" href="stats.css"><script src="stats.js"></script>
|
||||||
<!-- Rekam ulang K.O. dan tangkapan layar (RENCANA 7.3 dan 7.4): record.js memasang tombol kamera di HUD, baris di jeda dan Pengaturan; kait kecil di vs.js (layar hasil) -->
|
<!-- Rekam ulang K.O. dan tangkapan layar (RENCANA 7.3 dan 7.4): record.js memasang tombol kamera di HUD, baris di jeda dan Pengaturan; kait kecil di vs.js (layar hasil) -->
|
||||||
<link rel="stylesheet" href="record.css"><script src="record.js"></script>
|
<link rel="stylesheet" href="record.css"><script src="record.js"></script>
|
||||||
</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 src="feel.js"></script><script src="access.js"></script><script src="content/passives.js"></script><script src="passives.js"></script><script src="content/quotes.js"></script><script src="content/dialogs.js"></script><script src="vs.js"></script><script>window.FRONTEND_ENABLED=true;</script><script src="game.js"></script><script src="touch.js"></script><script src="input.js"></script><script src="content/mystery.js"></script><script src="mystery.js"></script><script src="tutorial.js"></script><script src="menu.js"></script><script src="meta.js"></script></body></html>
|
</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 src="feel.js"></script><script src="access.js"></script><script src="content/passives.js"></script><script src="passives.js"></script><script src="content/quotes.js"></script><script src="content/dialogs.js"></script><script src="vs.js"></script><script>window.FRONTEND_ENABLED=true;</script><script src="game.js"></script><script src="touch.js"></script><script src="input.js"></script><script src="content/mystery.js"></script><script src="mystery.js"></script><script src="tutorial.js"></script><script src="challenge.js"></script><script src="menu.js"></script><script src="meta.js"></script></body></html>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
3
menu.js
3
menu.js
|
|
@ -76,6 +76,7 @@
|
||||||
}
|
}
|
||||||
bind();attachBackground();window.Brand?.decorate(root,screen);window.Mystery?.decorate(root,screen,hover,lockedSlots);
|
bind();attachBackground();window.Brand?.decorate(root,screen);window.Mystery?.decorate(root,screen,hover,lockedSlots);
|
||||||
window.Tutorial?.decorate(root,screen); // tombol TUTORIAL kecil di pojok kiri atas layar utama (tutorial.js)
|
window.Tutorial?.decorate(root,screen); // tombol TUTORIAL kecil di pojok kiri atas layar utama (tutorial.js)
|
||||||
|
window.Challenge?.decorate(root,screen,{player,enemy,stage,level,mode,p2,runKind,result}); // tombol KODE TANTANGAN, SALIN KODE, catatan About (challenge.js)
|
||||||
}
|
}
|
||||||
function updatePreview(play=true){
|
function updatePreview(play=true){
|
||||||
root.querySelectorAll('[data-fighter]').forEach(b=>b.classList.toggle('highlight',b.dataset.fighter===hover));
|
root.querySelectorAll('[data-fighter]').forEach(b=>b.classList.toggle('highlight',b.dataset.fighter===hover));
|
||||||
|
|
@ -92,7 +93,7 @@
|
||||||
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);
|
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 back(){if(lockIn)return;game.stopAnnouncer();sfx('back',true);if(screen==='arena'){screen='select';step=1;hover=enemy;}else if(screen==='select'&&step===1){step=0;hover=player;}else screen='home';render();}
|
||||||
function start(){window.Meta?.cancelRun?.();if(game.startMatch({player,enemy,stage,level,mode,vs:mode==='versus'?{p2}:false})){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 start(){window.Meta?.cancelRun?.();if(game.startMatch({player,enemy,stage,level,mode,vs:mode==='versus'?{p2}:false,codeRule:window.Challenge?.pending?.(mode,p2)})){if(p2){game.setP2(true);window.Meta?.event('twoplayer');window.Meta?.toast('P2: ← → jalan, ↑ lompat, N serang, M / , / . = skill 1 / 2 / ultimate');}document.querySelector('#arena').focus();}else {const error=root.querySelector('#setup-error');if(error)error.textContent='Aset belum siap. Tunggu sebentar lalu coba lagi.';sfx('locked',true);}}
|
||||||
// Opens the shop/locker/collection. If meta.js is missing (stale cached index.html) it is loaded on demand.
|
// Opens the shop/locker/collection. If meta.js is missing (stale cached index.html) it is loaded on demand.
|
||||||
function openMeta(tab){
|
function openMeta(tab){
|
||||||
const go=()=>{try{window.Meta.open(tab);}catch(e){console.error(e);alert('Menu gagal dibuka: '+e.message);}};
|
const go=()=>{try{window.Meta.open(tab);}catch(e){console.error(e);alert('Menu gagal dibuka: '+e.message);}};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue