Gabung wf/t3-passive: pasif, kelemahan, dan gaya bertarung (RENCANA 3.2 dan 3.3)
Konflik index.html, meta.js, vs.js diselesaikan dengan mempertahankan kedua sisi: urutan skrip content/passives.js, passives.js, content/quotes.js, content/dialogs.js sebelum vs.js; Meta mengekspor styleOf, setStyle, dan pairEgg; VS.show memanggil Passives.decorateVS lalu memakai durasi dur milik dialog.
This commit is contained in:
commit
5a74cc5317
|
|
@ -0,0 +1,55 @@
|
||||||
|
/* Pasif dan kelemahan per karakter (RENCANA 3.2) serta gaya bertarung (RENCANA 3.3). Hanya data; mesinnya ada di passives.js (helper) dan game.js (titik kait passiveMod).
|
||||||
|
Satu perk kecil khas DAN satu kelemahan kecil per karakter agar tidak ada yang bagus di segalanya. Berlaku sama persis untuk pemain, CPU, dan P2.
|
||||||
|
`mods` memuat angka gabungan perk + kelemahan. Kunci pengali (1 = tanpa perubahan):
|
||||||
|
dmgOut semua damage yang dikeluarkan dmgBasic tambahan khusus kombo 3 pukulan dasar
|
||||||
|
dmgSkill tambahan untuk semua yang bukan kombo dasar: skill, ultimate, gerakan perintah, jurus toko, racun/terbakar
|
||||||
|
dmgIn semua damage yang diterima (di bawah 1 = lebih tahan)
|
||||||
|
walk kecepatan jalan dan lari cd kecepatan pulih semua cooldown (di atas 1 = lebih cepat)
|
||||||
|
cdSkill kecepatan pulih skill 1 dan 2 cdUlt kecepatan pulih ultimate
|
||||||
|
parry jendela parry (pemain) dan peluang blok (CPU) modeDur durasi mode pasca-ultimate
|
||||||
|
Kunci penambah (0 = tanpa perubahan): regen = HP per detik yang pulih bila tidak terkena serangan selama 3 detik.
|
||||||
|
Angka sengaja kecil (sekitar 5 sampai 12 persen). Teks angka yang tampil ke pemain disusun otomatis dari `mods`, jadi deskripsi di sini hanya cerita
|
||||||
|
dan tidak boleh memuat angka (supaya tidak ketinggalan saat angka disetel ulang). Pemisahan perk/kelemahan dibaca dari arah angkanya. */
|
||||||
|
window.PASSIVES = {
|
||||||
|
browo: {
|
||||||
|
name: 'AMUKAN PANJANG', desc: 'Amukan sawitnya awet: mode Mengamuk bertahan lebih lama.',
|
||||||
|
flaw: { name: 'JOGET DULU', desc: 'Setiap ultimate harus dibuka dengan joget, jadi pulihnya lebih lama.' },
|
||||||
|
mods: { modeDur: 1.12, cdUlt: .92 }
|
||||||
|
},
|
||||||
|
megachan: {
|
||||||
|
name: 'KEPALA BANTENG', desc: 'Keras kepala, keras tekad: pukulan lawan terasa lebih ringan.',
|
||||||
|
flaw: { name: 'LANGKAH BERAT', desc: 'Badan kokoh membuat jalannya pelan.' },
|
||||||
|
mods: { dmgIn: .92, walk: .92 }
|
||||||
|
},
|
||||||
|
sonofsolo: {
|
||||||
|
name: 'RACIKAN PEKAT', desc: 'Asam racikannya makin pekat: skill dan ultimate lebih nyelekit.',
|
||||||
|
flaw: { name: 'FISIK TIPIS', desc: 'Lebih piawai meracik daripada menahan pukulan, jadi lebih mudah luka.' },
|
||||||
|
mods: { dmgSkill: 1.08, dmgIn: 1.06 }
|
||||||
|
},
|
||||||
|
ethanolman: {
|
||||||
|
name: 'BAHAN BAKAR HIJAU', desc: 'Energi terbarukan: perlahan memulihkan HP bila lama tidak diserang.',
|
||||||
|
flaw: { name: 'MESIN DINGIN', desc: 'Selalu butuh pemanasan, jadi semua cooldown pulih lebih lambat.' },
|
||||||
|
mods: { regen: .5, cd: .95 }
|
||||||
|
},
|
||||||
|
anice: {
|
||||||
|
name: 'JAWABAN TEPAT', desc: 'Bicara terukur, bertahan terukur: jendela parry lebih lebar.',
|
||||||
|
flaw: { name: 'SANTUN BERLEBIHAN', desc: 'Terlalu sopan untuk memukul keras, jadi pukulan dasarnya lebih lemah.' },
|
||||||
|
mods: { parry: 1.25, dmgBasic: .92 }
|
||||||
|
},
|
||||||
|
soloman: {
|
||||||
|
name: 'LANGKAH RINGAN', desc: 'Si sederhana melangkah enteng: jalan dan lari lebih cepat.',
|
||||||
|
flaw: { name: 'TENAGA SEADANYA', desc: 'Serba sederhana, termasuk tenaganya: semua damage lebih kecil.' },
|
||||||
|
mods: { walk: 1.1, dmgOut: .94 }
|
||||||
|
},
|
||||||
|
gufron: {
|
||||||
|
name: 'BAHASA SEMUT', desc: 'Koloni cepat siap lagi: kedua skill pulih lebih cepat.',
|
||||||
|
flaw: { name: 'JAB NYENTRIK', desc: 'Pukulan dasarnya kurang meyakinkan, jadi damage kombo dasar lebih kecil.' },
|
||||||
|
mods: { cdSkill: 1.12, dmgBasic: .88 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Gaya bertarung per karakter: dipilih pemain di menu Karakter (disimpan di Meta.save.style[id]); CPU memakai gaya acak per pertandingan.
|
||||||
|
window.STYLES = {
|
||||||
|
agresif: { name: 'AGRESIF', desc: 'Maju terus: pukulan lebih keras, tetapi lebih rapuh dan skill lebih lama pulih.', mods: { dmgOut: 1.06, dmgIn: 1.06, cdSkill: .94 } },
|
||||||
|
seimbang: { name: 'SEIMBANG', desc: 'Tanpa perubahan angka. Cocok untuk mengenal karakter.', mods: {} },
|
||||||
|
bertahan: { name: 'BERTAHAN', desc: 'Sabar dan kokoh: lebih tahan pukulan dan jendela parry lebih lebar, tetapi damage lebih kecil.', mods: { dmgIn: .92, dmgOut: .95, parry: 1.2 } }
|
||||||
|
};
|
||||||
|
|
@ -14,6 +14,7 @@ Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks un
|
||||||
- CPU: `updateFenrAI`, `cpuProfile` (tingkat easy/medium/hard/excellent dari `match.js`).
|
- CPU: `updateFenrAI`, `cpuProfile` (tingkat easy/medium/hard/excellent dari `match.js`).
|
||||||
- Aturan ronde: `match.js` (`MatchRules`), dipakai `advanceMatch` di game.js.
|
- Aturan ronde: `match.js` (`MatchRules`), dipakai `advanceMatch` di game.js.
|
||||||
- `meta.js`: `window.Meta` — koin, toko, skin (filter warna), jurus toko (tombol K), varian skill, menu Karakter/Koleksi, pencapaian, easter egg, Arcade/Survival (`startRun`), kolom kode cheat di Pengaturan. Simpanan: `localStorage['konoha.save.v1']`.
|
- `meta.js`: `window.Meta` — koin, toko, skin (filter warna), jurus toko (tombol K), varian skill, menu Karakter/Koleksi, pencapaian, easter egg, Arcade/Survival (`startRun`), kolom kode cheat di Pengaturan. Simpanan: `localStorage['konoha.save.v1']`.
|
||||||
|
- Pasif, kelemahan, dan gaya bertarung (RENCANA 3.2/3.3): data di `content/passives.js` (`window.PASSIVES[id] = {name, desc, flaw, mods}`, `window.STYLES` agresif/seimbang/bertahan), helper dan teks angka otomatis di `passives.js` (`window.Passives`: `merge`, `describe`, `lockerHtml`, `decorateVS`). Mesin: `refreshPassives(styles)` di `startMatch` (juga `selectCharacter`, `setP2`) menggabungkan perk + kelemahan + gaya per sisi ke `sideMods`; semua pembacaan lewat satu titik kait `passiveMod(actor, key, base)` (hero = sisi pemain; selain itu = lawan, CPU atau P2, jadi simetris). Kunci: `dmgOut`, `dmgBasic`, `dmgSkill`, `dmgIn`, `walk`, `cd`, `cdSkill`, `cdUlt`, `parry`, `modeDur` (pengali) dan `regen` (HP/dtk, penambah). Damage dikali di `hitDummy`/`receiveHit`/`dotDamage` lewat `passiveDamage` (dibulatkan dengan sisa yang dibawa, tanpa pengacak); cooldown di loop cooldown `step`/`updateDummy`; jalan di `step`/`updateP2`/`updateFenrAI`; parry di `press('l')` dan peluang blok CPU di `hitDummy`. Gaya pemain disimpan di `Meta.save.style[id]` (`Meta.styleOf/setStyle`, simpanan lama tanpa kunci = Seimbang); `startMatch({styles:{player,enemy}})` menimpa, dan CPU di pertandingan ber-VS memakai gaya acak (pengacak terpisah `styleRand`). Teks tampil di menu Karakter (`renderLocker`), layar VS (`.vs-perk`), dan Daftar gerakan (`passiveRows`). API uji: `__game.passives()`, `__game.passiveMod(actor,key,base)`, `__game.setStyles({player,enemy})`. Catatan: P2 manusia tidak punya tombol blok, jadi kunci `parry` tidak berefek bagi P2.
|
||||||
- `menu.js`: menu utama (home, pilih karakter, arena, hasil, about) di `#front-menu`; `window.FrontEnd`.
|
- `menu.js`: menu utama (home, pilih karakter, arena, hasil, about) di `#front-menu`; `window.FrontEnd`.
|
||||||
- `touch.js`: kontrol sentuh; tombol memanggil `__game.input(key, down)`.
|
- `touch.js`: kontrol sentuh; tombol memanggil `__game.input(key, down)`.
|
||||||
- `brand.js` (`window.Brand`) + `content/loading.js` (`window.LOADING_QUOTES`) + `content/changelog.js` (`window.CHANGELOG`): dimuat di `index.html` tepat setelah kartu `#boot`, sebelum `precache.js`. Kutipan acak (tidak pernah sama dengan yang terakhir, kunci `konoha.loadquote`; `?quote=N` memaksa) tampil di `#boot .boot-quote` dan `#load-state .load-quote`. "Yang baru": `Brand.decorate(root, screen)` dipanggil `menu.js` di akhir `render()`; layar `about` mendapat panel `.whatsnew-panel` dan tautan kredit, layar `home` memicu toast `.brand-toast` satu kali bila versi teratas CHANGELOG berbeda dari `localStorage['konoha.changelog.seen']` (pemain tanpa simpanan `konoha.save.v1` tidak diberi toast). Menambah pembaruan: sisipkan objek baru di AWAL `content/changelog.js` dengan version lebih besar. Berkas `content/*.js` ikut `tools/build_precache.py`.
|
- `brand.js` (`window.Brand`) + `content/loading.js` (`window.LOADING_QUOTES`) + `content/changelog.js` (`window.CHANGELOG`): dimuat di `index.html` tepat setelah kartu `#boot`, sebelum `precache.js`. Kutipan acak (tidak pernah sama dengan yang terakhir, kunci `konoha.loadquote`; `?quote=N` memaksa) tampil di `#boot .boot-quote` dan `#load-state .load-quote`. "Yang baru": `Brand.decorate(root, screen)` dipanggil `menu.js` di akhir `render()`; layar `about` mendapat panel `.whatsnew-panel` dan tautan kredit, layar `home` memicu toast `.brand-toast` satu kali bila versi teratas CHANGELOG berbeda dari `localStorage['konoha.changelog.seen']` (pemain tanpa simpanan `konoha.save.v1` tidak diberi toast). Menambah pembaruan: sisipkan objek baru di AWAL `content/changelog.js` dengan version lebih besar. Berkas `content/*.js` ikut `tools/build_precache.py`.
|
||||||
|
|
|
||||||
89
game.js
89
game.js
|
|
@ -30,6 +30,49 @@
|
||||||
ko: { start: no, scale: () => 1, zoom: () => 1, drawOverlay: no, state: { on: false, cfg: { trauma: 0 } } } };
|
ko: { start: no, scale: () => 1, zoom: () => 1, drawOverlay: no, state: { on: false, cfg: { trauma: 0 } } } };
|
||||||
}
|
}
|
||||||
const Rules = window.MatchRules, Feel = window.Feel || feelFallback();
|
const Rules = window.MatchRules, Feel = window.Feel || feelFallback();
|
||||||
|
// ---- Pasif, kelemahan, dan gaya bertarung (RENCANA 3.2 / 3.3; data content/passives.js, helper passives.js) ----
|
||||||
|
// Titik kait tunggal: passiveMod(actor, key, base) membaca angka sisi aktor (hero = pemain; selain itu = lawan, CPU atau P2), jadi semua efek berlaku sama persis untuk kedua sisi.
|
||||||
|
// Kunci pengali dikalikan ke base; kunci penambah (regen) dijumlahkan. Tanpa window.Passives semua angka netral dan game tetap berjalan.
|
||||||
|
// Damage di game ini bilangan bulat, jadi pengali kecil (6%) dibulatkan dengan sisa yang dibawa ke pukulan berikutnya (pcarry): rata-rata tepat, tanpa pengacak.
|
||||||
|
const sideMods = { player: {}, enemy: {} }, sideStyle = { player: 'seimbang', enemy: 'seimbang' }, pcarry = { player: 0, enemy: 0 }, pregen = { player: 0, enemy: 0 };
|
||||||
|
let hitKind = null, styleSeed = 7331; // hitKind = 'basic' selama kombo dasar sedang mengenai (untuk dmgBasic)
|
||||||
|
function passiveMod(actor, key, base = 1) { const v = (actor === hero ? sideMods.player : sideMods.enemy)[key]; return v === undefined ? base : window.Passives?.KEYS?.[key]?.add ? base + v : base * v; }
|
||||||
|
function styleRand() { styleSeed = (Math.imul(styleSeed, 1664525) + 1013904223) >>> 0; return styleSeed / 4294967296; } // pengacak terpisah: tidak menggeser random() mesin
|
||||||
|
// styles = { player, enemy }: player kosong = pilihan di menu Karakter (Meta.styleOf), enemy kosong = Seimbang, 'acak' = gaya acak per pertandingan (CPU),
|
||||||
|
// 'p2' = pilihan menu Karakter untuk karakter lawan (pemain kedua manusia; gaya melekat pada karakter, jadi simetris dengan sisi pemain).
|
||||||
|
function refreshPassives(styles = {}) {
|
||||||
|
const P = window.Passives; if (!P) { sideMods.player = {}; sideMods.enemy = {}; return; }
|
||||||
|
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);
|
||||||
|
sideMods.player = P.merge(selectedCharacter, sideStyle.player); sideMods.enemy = P.merge(opponentCharacter, sideStyle.enemy);
|
||||||
|
}
|
||||||
|
// 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 passiveDamage(attacker, target, amount, basic) {
|
||||||
|
if (!(amount > 0)) return amount;
|
||||||
|
const mul = passiveMod(attacker, 'dmgOut') * passiveMod(attacker, basic ? 'dmgBasic' : 'dmgSkill') * passiveMod(target, 'dmgIn'); if (mul === 1) return amount;
|
||||||
|
const side = attacker === hero ? 'player' : 'enemy', v = amount * mul + pcarry[side], r = Math.round(v); pcarry[side] = v - r; return r;
|
||||||
|
}
|
||||||
|
const cdRate = (actor, name) => passiveMod(actor, 'cd') * passiveMod(actor, name === 'ultimate' ? 'cdUlt' : 'cdSkill');
|
||||||
|
function strikeAs(kind, fn) { hitKind = kind; try { fn(); } finally { hitKind = null; } }
|
||||||
|
// regen: HP kecil per detik, hanya bila aktor hidup, tidak penuh, dan tidak terkena apa pun selama 3 detik (lastHurt diisi tiap damage masuk).
|
||||||
|
function passiveRegen(dt) {
|
||||||
|
if (match && match.phase !== 'fight') return;
|
||||||
|
for (const [side, actor] of [['player', hero], ['enemy', dummy]]) {
|
||||||
|
const r = passiveMod(actor, 'regen', 0); if (!(r > 0) || !bwAlive(actor) || time - (actor.lastHurt ?? -99) < 3) continue;
|
||||||
|
if (actor === hero ? hero.hp >= BALANCE.heroMax : dummy.damage <= 0) { pregen[side] = 0; continue; }
|
||||||
|
pregen[side] += r * dt; if (pregen[side] < 1) continue;
|
||||||
|
const n = Math.floor(pregen[side]); pregen[side] -= n; if (actor === hero) hero.hp = Math.min(BALANCE.heroMax, hero.hp + n); else dummy.damage = Math.max(0, dummy.damage - n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Baris untuk Daftar gerakan: [tanda, nama, angka, cerita] perk dan kelemahan sebuah sisi, ditambah baris GAYA hanya bila bukan Seimbang (hemat tinggi daftar).
|
||||||
|
// Cerita hanya tampil di jendela cukup tinggi (CSS), angka selalu.
|
||||||
|
function passiveRows(id, side) {
|
||||||
|
const P = window.Passives, d = P?.describe(id, true); if (!d) return null;
|
||||||
|
const rows = [['\u25B2', d.perk.name, d.perk.fx, d.perk.desc], ['\u25BC', d.flaw.name, d.flaw.fx, d.flaw.desc]];
|
||||||
|
if (sideStyle[side] !== P.DEFAULT_STYLE) { const st = P.styleView(sideStyle[side], true); rows.push(['GAYA', st.name, st.fx, st.desc]); }
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
let opponentCharacter = 'fenr', difficulty = 'medium', stageId = 'bellora', menuOpen = !!window.FRONTEND_ENABLED;
|
let opponentCharacter = 'fenr', difficulty = 'medium', stageId = 'bellora', menuOpen = !!window.FRONTEND_ENABLED;
|
||||||
let match = Rules?.create('training') || null, roundAnnouncer = null;
|
let match = Rules?.create('training') || null, roundAnnouncer = null;
|
||||||
// Layar VS (vs.js): selama vsHold urutan ronde ditahan sebelum cue 'Round 1' (tidak ada yang berjalan di match.js); matchBest = kombo terbaik di seluruh ronde.
|
// Layar VS (vs.js): selama vsHold urutan ronde ditahan sebelum cue 'Round 1' (tidak ada yang berjalan di match.js); matchBest = kombo terbaik di seluruh ronde.
|
||||||
|
|
@ -194,10 +237,10 @@
|
||||||
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;
|
||||||
selectedCharacter=player;opponentCharacter=enemy;stageId=stage;difficulty=level;images.stage=images['stage-'+stage];CONFIG.groundY=Rules.stages[stage].groundY;
|
selectedCharacter=player;opponentCharacter=enemy;stageId=stage;difficulty=level;images.stage=images['stage-'+stage];CONFIG.groundY=Rules.stages[stage].groundY;refreshPassives(matchStyles(options,mode));
|
||||||
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();
|
||||||
$('#ai-toggle').checked=aiEnabled;$('#character-select').value=selectedCharacter;document.body.classList.toggle('in-versus',mode==='versus');
|
$('#ai-toggle').checked=aiEnabled;$('#character-select').value=selectedCharacter;document.body.classList.toggle('in-versus',mode==='versus');
|
||||||
if(mode==='versus'){announceTimer=0;if(!(options.vs&&vsStart(options.vs,{player,enemy,stage,level})))emitRoundCue('round',1);}else announce('TRAINING READY',1.3);
|
if(mode==='versus'){announceTimer=0;if(!(options.vs&&vsStart(options.vs,{player,enemy,stage,level,styles:{...sideStyle}})))emitRoundCue('round',1);}else announce('TRAINING READY',1.3);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Layar VS (opsional: options.vs = true | {label,p2}; hanya mode versus, tidak ada di latihan). Mengembalikan true bila layar tampil;
|
// Layar VS (opsional: options.vs = true | {label,p2}; hanya mode versus, tidak ada di latihan). Mengembalikan true bila layar tampil;
|
||||||
|
|
@ -320,7 +363,7 @@
|
||||||
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)) return;
|
||||||
keys.add(key); logInput(key);
|
keys.add(key); logInput(key);
|
||||||
if (key === 'l') { hero.parry = .16; }
|
if (key === 'l') { hero.parry = .16 * passiveMod(hero, 'parry'); }
|
||||||
if (key === 'j') startThrow();
|
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; }
|
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.
|
// A new left/right press cuts a basic attack short: right away after its hit, or as soon as the hit lands.
|
||||||
|
|
@ -492,7 +535,7 @@
|
||||||
if (hero.hp <= 0 || (match && match.phase !== 'fight')) { dummy.vx = approach(dummy.vx, 0, dt * 1500); if (grounded) dummy.state = 'idle'; return; }
|
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; }
|
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.facing = hero.x >= dummy.x ? 1 : -1;
|
||||||
dummy.vx = approach(dummy.vx, dir * CONFIG.walkSpeed, dt * 2800);
|
dummy.vx = approach(dummy.vx, dir * CONFIG.walkSpeed * passiveMod(dummy, 'walk'), dt * 2800);
|
||||||
dummy.state = dir ? 'walk' : 'idle'; dummy.walkPhase = (dummy.walkPhase || 0) + Math.abs(dummy.vx) * dt / 148;
|
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(); }
|
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(); }
|
||||||
|
|
@ -503,7 +546,7 @@
|
||||||
systemAnnouncer?.clear();
|
systemAnnouncer?.clear();
|
||||||
stopUltimateVoice();
|
stopUltimateVoice();
|
||||||
Object.assign(hero, { x: 350, y: CONFIG.groundY, vx: 0, vy: 0, facing: 1, grounded: true, jumps: 0, state: 'idle', animTime: 0, walkPhase: 0, smokeDistance:0, action: null, runDirection: 0, hp: BALANCE.heroMax, koTime:0, invuln: 0, hurtTime: 0, doublePose: 0, rollDuration: CONFIG.rollDuration, rollBuffer: null, airTime: 0, cooldowns: { skill1: 0, skill2: 0, ultimate: 0 } });
|
Object.assign(hero, { x: 350, y: CONFIG.groundY, vx: 0, vy: 0, facing: 1, grounded: true, jumps: 0, state: 'idle', animTime: 0, walkPhase: 0, smokeDistance:0, action: null, runDirection: 0, hp: BALANCE.heroMax, koTime:0, invuln: 0, hurtTime: 0, doublePose: 0, rollDuration: CONFIG.rollDuration, rollBuffer: null, airTime: 0, cooldowns: { skill1: 0, skill2: 0, ultimate: 0 } });
|
||||||
hero.spCd = hero.shopCd = hero.shield = 0; inputLog.length = 0;
|
hero.spCd = hero.shopCd = hero.shield = 0; inputLog.length = 0; pcarry.player = pcarry.enemy = pregen.player = pregen.enemy = 0; hero.lastHurt = dummy.lastHurt = -99;
|
||||||
Object.assign(dummy, { x: 915, y: CONFIG.groundY, vx: 0, vy: 0, state: 'idle', stateTime: 0, damage: 0, ko:false, flash: 0, invuln: 0, angle: 0 });
|
Object.assign(dummy, { x: 915, y: CONFIG.groundY, vx: 0, vy: 0, state: 'idle', stateTime: 0, damage: 0, ko:false, flash: 0, invuln: 0, angle: 0 });
|
||||||
if(F) {F.reset(hero);F.reset(dummy);Object.assign(dummy,{facing:-1,action:null,cooldowns:{skill1:0,skill2:0,ultimate:0},aiThink:.4,aiTime:0,aiChain:0,walkPhase:0,jumps:0});cpu={seen:new WeakMap(),decided:new WeakSet(),read:null,heroActionAt:0,heroAction:null,airborne:false,airAt:0,antiAirRead:false,plan:null,doubleAt:0};syncPlayerForm();}
|
if(F) {F.reset(hero);F.reset(dummy);Object.assign(dummy,{facing:-1,action:null,cooldowns:{skill1:0,skill2:0,ultimate:0},aiThink:.4,aiTime:0,aiChain:0,walkPhase:0,jumps:0});cpu={seen:new WeakMap(),decided:new WeakSet(),read:null,heroActionAt:0,heroAction:null,airborne:false,airAt:0,antiAirRead:false,plan:null,doubleAt:0};syncPlayerForm();}
|
||||||
fenrCutin=null;hudIdentity='';
|
fenrCutin=null;hudIdentity='';
|
||||||
|
|
@ -518,8 +561,9 @@
|
||||||
function hitDummy(damage, power, color, sourceX, impactY, knockdown = false, freezeWorld = true, shake = 1) {
|
function hitDummy(damage, power, color, sourceX, impactY, knockdown = false, freezeWorld = true, shake = 1) {
|
||||||
if (menuOpen || (match && match.phase!=='fight') || dummy.invuln > 0 || dummy.state === 'down' || dummy.state === 'recover') return false;
|
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;
|
if (dummy.action?.guard && shellGuard(dummy)) return false;
|
||||||
|
if (damage > 0) damage = Math.max(1, passiveDamage(hero, dummy, damage, hitKind === 'basic')); dummy.lastHurt = time;
|
||||||
const dir = sourceX <= dummy.x ? 1 : -1;
|
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)) {
|
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) * passiveMod(dummy, 'parry')) {
|
||||||
const chip = Math.min(Math.max(1, Math.round(damage * .2)), Math.max(0, BALANCE.dummyMax - dummy.damage - 1)); dummy.damage += chip; totalDamage += chip;
|
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', kind: 'guard', life: .6, maxLife: .6, color: Feel.table.colors.guard }); sound('step'); feelImpact('dummy', 'guard', { dir, x: dummy.x - dir * 30, y: impactY }); return true;
|
spawnParticles(dummy.x - dir * 30, impactY, '#ffd36a', 8, 180); damageNumbers.push({ x: dummy.x, y: impactY - 40, amount: 'GUARD', kind: 'guard', life: .6, maxLife: .6, color: Feel.table.colors.guard }); sound('step'); feelImpact('dummy', 'guard', { dir, x: dummy.x - dir * 30, y: impactY }); return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1456,10 +1500,12 @@
|
||||||
// cooldowns are put back when the rampage ends.
|
// cooldowns are put back when the rampage ends.
|
||||||
// The post-ultimate "mode": BRO WO's Mengamuk (bespoke) or CF[id].mode (data). Same machinery for both: timer in bwx.rage[side],
|
// The post-ultimate "mode": BRO WO's Mengamuk (bespoke) or CF[id].mode (data). Same machinery for both: timer in bwx.rage[side],
|
||||||
// a.bwRage tells the kit to use the mode's skills, cooldowns swap in and out, and an optional `form` swaps the whole atlas.
|
// a.bwRage tells the kit to use the mode's skills, cooldowns swap in and out, and an optional `form` swaps the whole atlas.
|
||||||
|
// Durasi mode memakai perk karakter (modeDur); gaya tidak mengubahnya, jadi angkanya sama di kedua sisi dan di bar ultimate.
|
||||||
|
const modeDurMod = id => window.Passives?.info(id)?.mods?.modeDur ?? 1;
|
||||||
function modeOf(id) {
|
function modeOf(id) {
|
||||||
if (id === 'browo') return BW ? { duration: BW.rage.duration, aura: '#ff6e28', label: 'MENGAMUK', announce: 'MENGAMUK!', names: BW.rageNames, icons: ['basic', 'rage-skill1', 'rage-skill2', 'rage-ultimate'], cooldowns: BW.rageBalance.cooldowns, speedMul: BW.rage.speedMul } : null;
|
if (id === 'browo') return BW ? { duration: BW.rage.duration * modeDurMod(id), aura: '#ff6e28', label: 'MENGAMUK', announce: 'MENGAMUK!', names: BW.rageNames, icons: ['basic', 'rage-skill1', 'rage-skill2', 'rage-ultimate'], cooldowns: BW.rageBalance.cooldowns, speedMul: BW.rage.speedMul } : null;
|
||||||
const m = CF[id]?.mode; if (!m) return null;
|
const m = CF[id]?.mode; if (!m) return null;
|
||||||
return { duration: m.duration || 10, aura: m.aura || '#ff5a3c', label: m.label || 'MODE', announce: m.announce || 'MODE!', names: m.names, icons: ['basic', 'mode-skill1', 'mode-skill2', 'mode-ultimate'], cooldowns: { skill1: m.cooldowns?.skill1 ?? 5, skill2: m.cooldowns?.skill2 ?? 8, ultimate: 999 }, speedMul: m.speedMul || 1.15 };
|
return { duration: (m.duration || 10) * modeDurMod(id), aura: m.aura || '#ff5a3c', label: m.label || 'MODE', announce: m.announce || 'MODE!', names: m.names, icons: ['basic', 'mode-skill1', 'mode-skill2', 'mode-ultimate'], cooldowns: { skill1: m.cooldowns?.skill1 ?? 5, skill2: m.cooldowns?.skill2 ?? 8, ultimate: 999 }, speedMul: m.speedMul || 1.15 };
|
||||||
}
|
}
|
||||||
function cfFormOn(id, side) { return !!(CF[id]?.mode?.form && bwx.rage[side] > 0); }
|
function cfFormOn(id, side) { return !!(CF[id]?.mode?.form && bwx.rage[side] > 0); }
|
||||||
function bwBaseCooldowns(id) { return id === 'browo' ? BW.balance.cooldowns : CF[id] ? CF[id].balance.cooldowns : null; }
|
function bwBaseCooldowns(id) { return id === 'browo' ? BW.balance.cooldowns : CF[id] ? CF[id].balance.cooldowns : null; }
|
||||||
|
|
@ -1482,6 +1528,7 @@
|
||||||
// Only discrete scripted bursts (combo = true) count as combo hits; per-tick DoT must not feed the combo rank, the damage reducer or coins.
|
// Only discrete scripted bursts (combo = true) count as combo hits; per-tick DoT must not feed the combo rank, the damage reducer or coins.
|
||||||
function dotDamage(target, amount, color = '#9bd05a', numbers = true, combo = false) {
|
function dotDamage(target, amount, color = '#9bd05a', numbers = true, combo = false) {
|
||||||
if (amount <= 0 || menuOpen || (match && match.phase !== 'fight') || !bwAlive(target)) return false;
|
if (amount <= 0 || menuOpen || (match && match.phase !== 'fight') || !bwAlive(target)) return false;
|
||||||
|
amount = passiveDamage(target === dummy ? hero : dummy, target, amount, false); target.lastHurt = time; if (amount <= 0) return true;
|
||||||
if (target === dummy) {
|
if (target === dummy) {
|
||||||
dummy.damage += amount; totalDamage += amount;
|
dummy.damage += amount; totalDamage += amount;
|
||||||
if (combo) { comboHits++; bestCombo = Math.max(bestCombo, comboHits); comboTimer = 2.3; }
|
if (combo) { comboHits++; bestCombo = Math.max(bestCombo, comboHits); comboTimer = 2.3; }
|
||||||
|
|
@ -1741,7 +1788,7 @@
|
||||||
if(fenrCutin) {fenrCutin.t+=dt;if(fenrCutin.t>.78)fenrCutin=null;}
|
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');}}
|
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.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);
|
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);
|
for (const name of Object.keys(hero.cooldowns)) hero.cooldowns[name] = Math.max(0, hero.cooldowns[name] - dt * cdRate(hero, name)); passiveRegen(dt);
|
||||||
if (hero.doublePose <= 0 && hero.rollBuffer && hero.hurtTime <= 0) { const buffered = hero.rollBuffer; hero.rollBuffer = null; if (buffered === 'attack') startAttack(); else cast(buffered); }
|
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;
|
comboTimer = Math.max(0, comboTimer - dt); if (!comboTimer) comboHits = comboDamage = 0;
|
||||||
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.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);
|
||||||
|
|
@ -1750,7 +1797,7 @@
|
||||||
if (!hero.action && hero.hurtTime <= 0 && hero.hp > 0) {
|
if (!hero.action && hero.hurtTime <= 0 && hero.hp > 0) {
|
||||||
if (dir && hero.doublePose <= 0) hero.facing = dir;
|
if (dir && hero.doublePose <= 0) hero.facing = dir;
|
||||||
if (hero.runDirection && dir !== hero.runDirection) hero.runDirection = 0;
|
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);
|
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)*passiveMod(hero,'walk');
|
||||||
hero.vx = approach(hero.vx, crouching || hero.blocking ? 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');
|
if (hero.grounded) state(crouching ? 'crouch' : dir ? hero.runDirection ? 'run' : 'walk' : 'idle');
|
||||||
else state(hero.doublePose > 0 ? 'doublejump' : 'jump');
|
else state(hero.doublePose > 0 ? 'doublejump' : 'jump');
|
||||||
|
|
@ -1761,7 +1808,7 @@
|
||||||
const a = hero.action; a.t += dt; state(a.name);
|
const a = hero.action; a.t += dt; state(a.name);
|
||||||
if((selectedCharacter==='fenr'||selectedCharacter==='mira'||selectedCharacter==='haldor'||selectedCharacter==='isolde'||selectedCharacter==='solan'||selectedCharacter==='nib'||CF[selectedCharacter])&&a.dash&&a.t>a.duration*.2&&a.t<a.duration*.6)hero.vx=hero.facing*a.dash;
|
if((selectedCharacter==='fenr'||selectedCharacter==='mira'||selectedCharacter==='haldor'||selectedCharacter==='isolde'||selectedCharacter==='solan'||selectedCharacter==='nib'||CF[selectedCharacter])&&a.dash&&a.t>a.duration*.2&&a.t<a.duration*.6)hero.vx=hero.facing*a.dash;
|
||||||
if (a.special && a.t < a.duration * .55 && ['lunge', 'seruduk'].includes(a.special)) hero.vx = hero.facing * (a.special === 'seruduk' ? 1000 : 760);
|
if (a.special && a.t < a.duration * .55 && ['lunge', 'seruduk'].includes(a.special)) hero.vx = hero.facing * (a.special === 'seruduk' ? 1000 : 760);
|
||||||
if (a.type !== 'ultimate' && !a.fired && a.t >= a.duration * (a.hitAt || (a.type === 'skill2' ? .53 : .5))) { a.fired = true; if (a.special) specialHit(a); else if (a.type === 'attack') meleeHit(a.index); else fireSkill(a); }
|
if (a.type !== 'ultimate' && !a.fired && a.t >= a.duration * (a.hitAt || (a.type === 'skill2' ? .53 : .5))) { a.fired = true; strikeAs(a.type === 'attack' && !a.special ? 'basic' : null, () => { if (a.special) specialHit(a); else if (a.type === 'attack') meleeHit(a.index); else fireSkill(a); }); }
|
||||||
if (a.fired && a.cancelInto && hero.action === a) { const into = a.cancelInto; hero.action = null; state(hero.grounded ? 'idle' : 'jump'); if (into === 'jump') jump(); }
|
if (a.fired && a.cancelInto && hero.action === a) { const into = a.cancelInto; hero.action = null; state(hero.grounded ? 'idle' : 'jump'); if (into === 'jump') jump(); }
|
||||||
if (a.t >= a.duration || (a.type === 'skill1' && a.fired)) { const queued = a.queued && a.index < 3, remaining = Math.max(0, a.queued - 1); const next = (a.index || 0) + 1; hero.action = null; if (queued) { startAttack(next); hero.action.queued = remaining; } else state(hero.grounded ? 'idle' : 'jump'); }
|
if (a.t >= a.duration || (a.type === 'skill1' && a.fired)) { const queued = a.queued && a.index < 3, remaining = Math.max(0, a.queued - 1); const next = (a.index || 0) + 1; hero.action = null; if (queued) { startAttack(next); hero.action.queued = remaining; } else state(hero.grounded ? 'idle' : 'jump'); }
|
||||||
}
|
}
|
||||||
|
|
@ -1787,7 +1834,7 @@
|
||||||
function currentDummyHP() {return Math.max(0,BALANCE.dummyMax-dummy.damage);}
|
function currentDummyHP() {return Math.max(0,BALANCE.dummyMax-dummy.damage);}
|
||||||
function hpLayers(value) {return {front:clamp(value-BALANCE.hpPerBar,0,BALANCE.hpPerBar),reserve:clamp(value,0,BALANCE.hpPerBar)};}
|
function hpLayers(value) {return {front:clamp(value-BALANCE.hpPerBar,0,BALANCE.hpPerBar),reserve:clamp(value,0,BALANCE.hpPerBar)};}
|
||||||
function updateDummy(dt) {
|
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);}
|
if(F){for(const name of Object.keys(dummy.cooldowns))dummy.cooldowns[name]=Math.max(0,dummy.cooldowns[name]-dt*cdRate(dummy,name));if(opponentCharacter==='fenr')F.advance(dummy,dt,!dummy.ko);}
|
||||||
dummy.stateTime += dt; dummy.invuln = Math.max(0, dummy.invuln - dt);
|
dummy.stateTime += dt; dummy.invuln = Math.max(0, dummy.invuln - dt);
|
||||||
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);
|
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);
|
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);
|
||||||
|
|
@ -1860,7 +1907,7 @@
|
||||||
}
|
}
|
||||||
function selectCharacter(id) {
|
function selectCharacter(id) {
|
||||||
if(!ready || match?.mode==='versus' || !playable().includes(id) || (id==='fenr'&&!window.FENR_HUMAN_MANIFEST))return false;
|
if(!ready || match?.mode==='versus' || !playable().includes(id) || (id==='fenr'&&!window.FENR_HUMAN_MANIFEST))return false;
|
||||||
selectedCharacter=id;reset();syncPlayerForm();warmCutins();$('#character-select').value=id;updateHud();return true;
|
selectedCharacter=id;refreshPassives({enemy:sideStyle.enemy});reset();syncPlayerForm();warmCutins();$('#character-select').value=id;updateHud();return true;
|
||||||
}
|
}
|
||||||
function showFenrTransformation(actor) {
|
function showFenrTransformation(actor) {
|
||||||
fenrCutin={t:0,owner:actor===hero?'PLAYER':'RIVAL'};cinematic=.65;
|
fenrCutin={t:0,owner:actor===hero?'PLAYER':'RIVAL'};cinematic=.65;
|
||||||
|
|
@ -2267,7 +2314,7 @@
|
||||||
if (dummy.action) {
|
if (dummy.action) {
|
||||||
const a = dummy.action; a.t += dt; dummy.state = a.name;
|
const a = dummy.action; a.t += dt; dummy.state = a.name;
|
||||||
if (a.dash && a.t > a.duration * .2 && a.t < a.duration * .6) dummy.vx = dummy.facing * a.dash;
|
if (a.dash && a.t > a.duration * .2 && a.t < a.duration * .6) dummy.vx = dummy.facing * a.dash;
|
||||||
if (!a.fired && a.type !== 'ultimate' && a.t >= a.duration * a.hitAt) { a.fired = true; enemyStrike(a); }
|
if (!a.fired && a.type !== 'ultimate' && a.t >= a.duration * a.hitAt) { a.fired = true; strikeAs(a.type === 'attack' ? 'basic' : null, () => enemyStrike(a)); }
|
||||||
if (a.t >= a.duration || (opponentCharacter !== 'fenr' && a.type === 'skill1' && a.fired)) {
|
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;
|
dummy.action = null; dummy.state = 'idle'; dummy.stateTime = 0; dummy.aiThink = P.recovery;
|
||||||
if (hero.hp > 0 && !p2Mode) cpuFollowUp(a, P);
|
if (hero.hp > 0 && !p2Mode) cpuFollowUp(a, P);
|
||||||
|
|
@ -2373,7 +2420,7 @@
|
||||||
const goal = keepOut ? Math.max(reach, threat ? threat + 18 : outside) : reach - (protectedNow && hero.hurtTime > 0 ? 20 : 12);
|
const goal = keepOut ? Math.max(reach, threat ? threat + 18 : outside) : reach - (protectedNow && hero.hurtTime > 0 ? 20 : 12);
|
||||||
wanted = dist > goal ? dummy.facing * (dist > 300 || (rushing && !punishWait) || immuneFor > 0 ? 420 : 240) : dist < Math.min(55, goal - 20) ? -dummy.facing * 150 : keepOut && dist < goal - 6 ? -dummy.facing * 300 : 0;
|
wanted = dist > goal ? dummy.facing * (dist > 300 || (rushing && !punishWait) || immuneFor > 0 ? 420 : 240) : dist < Math.min(55, goal - 20) ? -dummy.facing * 150 : keepOut && dist < goal - 6 ? -dummy.facing * 300 : 0;
|
||||||
}
|
}
|
||||||
dummy.vx = approach(dummy.vx, clamp(wanted * P.speed, -520, 520), dt * 2200); dummy.walkPhase += Math.abs(dummy.vx) * dt / 150;
|
const ws = passiveMod(dummy, 'walk'); dummy.vx = approach(dummy.vx, clamp(wanted * P.speed * ws, -520 * ws, 520 * ws), dt * 2200); dummy.walkPhase += Math.abs(dummy.vx) * dt / 150;
|
||||||
const next = Math.abs(dummy.vx) > 5 ? (Math.abs(dummy.vx) > 300 ? 'run' : 'walk') : 'idle'; if (next !== dummy.state) { dummy.state = next; dummy.stateTime = 0; }
|
const next = Math.abs(dummy.vx) > 5 ? (Math.abs(dummy.vx) > 300 ? 'run' : 'walk') : 'idle'; if (next !== dummy.state) { dummy.state = next; dummy.stateTime = 0; }
|
||||||
if (dummy.aiThink > 0 || cpu.plan?.kind === 'chain' || cpu.plan?.kind === 'sidestep') return;
|
if (dummy.aiThink > 0 || cpu.plan?.kind === 'chain' || cpu.plan?.kind === 'sidestep') return;
|
||||||
// 4. Decisions on the think timer.
|
// 4. Decisions on the think timer.
|
||||||
|
|
@ -2768,11 +2815,13 @@
|
||||||
const shop = SHOP_MOVES[window.Meta?.equippedMove?.()];
|
const shop = SHOP_MOVES[window.Meta?.equippedMove?.()];
|
||||||
sec('Kontrol dasar').append(row(['A', 'D'], 'Jalan', 'ketuk 2× = lari', 1), row(['W'], 'Lompat', '2× = lompat ganda', 1), row(['S'], 'Menunduk'), row(['L'], 'Blok', 'ketuk cepat = parry', 1), row(['J'], 'Lempar'), row(['K'], 'Jurus toko', shop ? shop.name : 'belum dipasang', 1));
|
sec('Kontrol dasar').append(row(['A', 'D'], 'Jalan', 'ketuk 2× = lari', 1), row(['W'], 'Lompat', '2× = lompat ganda', 1), row(['S'], 'Menunduk'), row(['L'], 'Blok', 'ketuk cepat = parry', 1), row(['J'], 'Lempar'), row(['K'], 'Jurus toko', shop ? shop.name : 'belum dipasang', 1));
|
||||||
sec('Serangan dan skill', me.name).append(row(['Spasi'], me.names[0], 'kombo 3 pukulan'), row(['I'], me.names[1], 'skill 1 · cooldown ' + cd('skill1')), row(['O'], me.names[2], 'skill 2 · cooldown ' + cd('skill2')), row(['P'], me.names[3], 'ultimate · cooldown ' + cd('ultimate')));
|
sec('Serangan dan skill', me.name).append(row(['Spasi'], me.names[0], 'kombo 3 pukulan'), row(['I'], me.names[1], 'skill 1 · cooldown ' + cd('skill1')), row(['O'], me.names[2], 'skill 2 · cooldown ' + cd('skill2')), row(['P'], me.names[3], 'ultimate · cooldown ' + cd('ultimate')));
|
||||||
|
const prow = ([k, n, fx, desc]) => { const r = row([k], n, fx, 1); if (desc) r.lastChild.append(el('small', 'pas-desc', desc)); return r; };
|
||||||
|
const mine = passiveRows(id, 'player'); if (mine) sec('Pasif dan gaya', me.name).append(...mine.map(prow));
|
||||||
sec('Gerakan perintah', null, 'cmd').append(row(['↓', '↓', '+', 'Spasi'], 'Uppercut', 'peluncur ke atas'), row(['↓', '→', '+', 'Spasi'], 'Tinju maju', '→ = arah lawan'));
|
sec('Gerakan perintah', null, 'cmd').append(row(['↓', '↓', '+', 'Spasi'], 'Uppercut', 'peluncur ke atas'), row(['↓', '→', '+', 'Spasi'], 'Tinju maju', '→ = arah lawan'));
|
||||||
if (md) { const ms = sec('Mode ' + md.label, Math.round(md.duration) + ' dtk'); ['Spasi', 'I', 'O', 'P'].forEach((k, i) => ms.append(row([k], md.names[i], i === 3 ? 'sekali pakai, mengakhiri mode' : '', i !== 3))); }
|
if (md) { const ms = sec('Mode ' + md.label, Math.round(md.duration) + ' dtk'); ['Spasi', 'I', 'O', 'P'].forEach((k, i) => ms.append(row([k], md.names[i], i === 3 ? 'sekali pakai, mengakhiri mode' : '', i !== 3))); }
|
||||||
const other = sec('Lainnya'); other.append(row(['Esc'], 'Jeda / lanjut'), row(['R'], 'Mulai ulang'));
|
const other = sec('Lainnya'); other.append(row(['Esc'], 'Jeda / lanjut'), row(['R'], 'Mulai ulang'));
|
||||||
if (document.documentElement.classList.contains('touch')) other.append(el('small', 'mv-note', 'Layar sentuh: UP, →P, K, BLOK, dan LEMPAR ada di sisi kanan.'));
|
if (document.documentElement.classList.contains('touch')) other.append(el('small', 'mv-note', 'Layar sentuh: UP, →P, K, BLOK, dan LEMPAR ada di sisi kanan.'));
|
||||||
if (p2Mode) { const rv = fighterInfo(opponentCharacter, 'human'), p2 = sec('Pemain 2', rv.name); p2.append(row(['←', '→'], 'Jalan'), row(['↑'], 'Lompat'), row(['N'], rv.names[0], 'kombo 3 pukulan'), row(['M'], rv.names[1], 'skill 1'), row([','], rv.names[2], 'skill 2'), row(['.'], rv.names[3], 'ultimate')); }
|
if (p2Mode) { const rv = fighterInfo(opponentCharacter, 'human'), p2 = sec('Pemain 2', rv.name); p2.append(row(['←', '→'], 'Jalan'), row(['↑'], 'Lompat'), row(['N'], rv.names[0], 'kombo 3 pukulan'), row(['M'], rv.names[1], 'skill 1'), row([','], rv.names[2], 'skill 2'), row(['.'], rv.names[3], 'ultimate')); const rp = passiveRows(opponentCharacter, 'enemy'); if (rp) sec('Pasif pemain 2', rv.name).append(...rp.map(prow)); }
|
||||||
}
|
}
|
||||||
// Pilihan lipat/buka diingat per perangkat; tanpa pilihan, terbuka bila jendela cukup lebar. Hanya klik pengguna pada ringkasan yang menyimpan.
|
// Pilihan lipat/buka diingat per perangkat; tanpa pilihan, terbuka bila jendela cukup lebar. Hanya klik pengguna pada ringkasan yang menyimpan.
|
||||||
function openMoveList() {
|
function openMoveList() {
|
||||||
|
|
@ -2873,20 +2922,22 @@
|
||||||
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); }
|
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).
|
// Deterministic QA interface: set manual=true, reset(), press/release(), advance(seconds).
|
||||||
// Positions represent feet. No networking or persistent state is changed by this interface.
|
// 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 (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,
|
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) { refreshPassives({ player: sideStyle.player, enemy: 'p2' }); 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(); },
|
advance(seconds, noDraw) { for (let i = 0; i < Math.round(seconds * 120); i++) step(1 / 120); if (noDraw) return null; 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 }; },
|
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;},
|
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;},
|
||||||
announceSelection(id){return menuOpen&&playable().includes(id)?(systemAnnouncer?.play('select_'+id)||false):false;},
|
announceSelection(id){return menuOpen&&playable().includes(id)?(systemAnnouncer?.play('select_'+id)||false):false;},
|
||||||
stopAnnouncer(){systemAnnouncer?.clear();},announcerState(){return systemAnnouncer?.snapshot()||null;},
|
stopAnnouncer(){systemAnnouncer?.clear();},announcerState(){return systemAnnouncer?.snapshot()||null;},
|
||||||
selectCharacter, setAI(value){aiEnabled=!!value;dummy.action=null;dummy.vx=0;dummy.state='idle';}, opponentPose,
|
selectCharacter, setAI(value){aiEnabled=!!value;dummy.action=null;dummy.vx=0;dummy.state='idle';}, opponentPose,
|
||||||
setPaused, setHitboxes(v) { hitboxes = !!v; $('#hitbox-toggle').checked = hitboxes; },
|
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 (heroGuard(damage, options)) return true; if (hero.action?.type?.startsWith('skill') || hero.action?.type === 'ultimate') return false; const counter = hero.action?.type === 'attack' && !hero.action.fired; 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); feelHeroHit(damage, options, counter); 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 (damage > 0) damage = Math.max(1, passiveDamage(dummy, hero, damage, hitKind === 'basic')); hero.lastHurt = time; if (heroGuard(damage, options)) return true; if (hero.action?.type?.startsWith('skill') || hero.action?.type === 'ultimate') return false; const counter = hero.action?.type === 'attack' && !hero.action.fired; 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); feelHeroHit(damage, options, counter); if (!hero.hp) {hero.koTime=1.7;clearInput();if(match?.mode!=='versus')announce('CORE RESTART',1.7);} return true; }
|
||||||
};
|
};
|
||||||
// Pemeriksaan efek jurus untuk uji headless (tests/t1-fx.mjs).
|
// Pemeriksaan efek jurus untuk uji headless (tests/t1-fx.mjs).
|
||||||
window.__game.fx = { snapshot: () => ({ ghosts: cfx.ghosts.length, hazards: cfx.hazards.filter(h => h.fire).length, dots: cfx.dots.length, later: cfx.later.length, particles: particles.length, effects: effects.map(e => e.type), tele: { hero: fxTeleP(hero), dummy: fxTeleP(dummy) }, projectiles: projectiles.map(p => ({ fx: p.fx, x: Math.round(p.x), y: Math.round(p.y), age: +(p.age || 0).toFixed(3), damage: p.damage, wait: p.wait || 0, orbit: !!(p.sw && !p.sw.go) })) }) };
|
window.__game.fx = { snapshot: () => ({ ghosts: cfx.ghosts.length, hazards: cfx.hazards.filter(h => h.fire).length, dots: cfx.dots.length, later: cfx.later.length, particles: particles.length, effects: effects.map(e => e.type), tele: { hero: fxTeleP(hero), dummy: fxTeleP(dummy) }, projectiles: projectiles.map(p => ({ fx: p.fx, x: Math.round(p.x), y: Math.round(p.y), age: +(p.age || 0).toFixed(3), damage: p.damage, wait: p.wait || 0, orbit: !!(p.sw && !p.sw.go) })) }) };
|
||||||
// Uji headless layar VS (tests/t2-vsresult.mjs): vsActive = urutan ronde sedang menunggu layar VS; vsSkip melewatinya.
|
// Uji headless layar VS (tests/t2-vsresult.mjs): vsActive = urutan ronde sedang menunggu layar VS; vsSkip melewatinya.
|
||||||
Object.assign(window.__game, { vsActive: () => vsHold, vsSkip: () => !!window.VS?.force?.() });
|
Object.assign(window.__game, { vsActive: () => vsHold, vsSkip: () => !!window.VS?.force?.() });
|
||||||
|
// Pasif dan gaya (tests/t3-passive.mjs): angka dan gaya tiap sisi, titik kait, dan pengaturan gaya langsung.
|
||||||
|
Object.assign(window.__game, { passiveMod, passives: () => ({ seed, player: { id: selectedCharacter, style: sideStyle.player, mods: { ...sideMods.player } }, enemy: { id: opponentCharacter, style: sideStyle.enemy, mods: { ...sideMods.enemy } } }), setStyles: styles => { refreshPassives(styles); hudIdentity = ''; return sideStyle; } });
|
||||||
// Hanya untuk uji headless (tests/t1-feel.mjs): pukulan langsung ke lawan, tabel rasa kena, status K.O./angka.
|
// Hanya untuk uji headless (tests/t1-feel.mjs): pukulan langsung ke lawan, tabel rasa kena, status K.O./angka.
|
||||||
Object.assign(window.__game, { hitDummy, feel: Feel, feelState: () => ({ ko: { ...Feel.ko.state, scale: Feel.ko.scale(), zoom: Feel.ko.zoom() }, kick: Feel.kickOffset(), numbers: damageNumbers.map(n => ({ amount: n.amount, kind: n.kind, label: n.label, color: n.color, life: n.life })), hitstop, trauma, realTime }) });
|
Object.assign(window.__game, { hitDummy, feel: Feel, feelState: () => ({ ko: { ...Feel.ko.state, scale: Feel.ko.scale(), zoom: Feel.ko.zoom() }, kick: Feel.kickOffset(), numbers: damageNumbers.map(n => ({ amount: n.amount, kind: n.kind, label: n.label, color: n.color, life: n.life })), hitstop, trauma, realTime }) });
|
||||||
if(F)reset();
|
if(F)reset();
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,6 @@
|
||||||
<div id="load-state" class="load-state"><span class="loading-glyph">✦</span><strong>KONOHA FIGHTING</strong><p class="load-quote"></p><span id="load-message">Menyiapkan arena…</span></div>
|
<div id="load-state" class="load-state"><span class="loading-glyph">✦</span><strong>KONOHA FIGHTING</strong><p class="load-quote"></p><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>Gerak pelan petarung di latar 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 (2× W = lompat ganda) S · Menunduk<br>SPACE · Kombo 3 pukulan ↓ ↓ + SPACE · Uppercut ↓ → + SPACE · Tinju maju<br>I / O · Skill P · Ultimate K · Jurus toko<br>L · Blok (ketuk = parry) J · Lempar<br>Esc · Jeda, berisi daftar gerakan karakter R · Mulai ulang<br>Dua pemain (P2): ← → · Gerak ↑ · Lompat N · Serang M / , / . · Skill 1 / 2 / Ultimate<br>Meter di bawah HP: bar ultimate (SIAP = bisa dipakai), saat mode aktif jadi bar waktu mode.</p></details><button class="primary-button" id="close-settings">Kembali ke arena <span>↗</span></button></dialog>
|
<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>Gerak pelan petarung di latar 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 (2× W = lompat ganda) S · Menunduk<br>SPACE · Kombo 3 pukulan ↓ ↓ + SPACE · Uppercut ↓ → + SPACE · Tinju maju<br>I / O · Skill P · Ultimate K · Jurus toko<br>L · Blok (ketuk = parry) J · Lempar<br>Esc · Jeda, berisi daftar gerakan karakter R · Mulai ulang<br>Dua pemain (P2): ← → · Gerak ↑ · Lompat N · Serang M / , / . · Skill 1 / 2 / Ultimate<br>Meter di bawah HP: bar ultimate (SIAP = bisa dipakai), saat mode aktif jadi bar waktu mode.</p></details><button class="primary-button" id="close-settings">Kembali ke arena <span>↗</span></button></dialog>
|
||||||
<dialog id="pause-panel" aria-labelledby="pause-title"><div class="pause-main"><div class="dialog-kicker">KONOHA FIGHTING / JEDA</div><h2 id="pause-title">JEDA</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></div><details class="move-list" id="move-list"><summary>Daftar gerakan</summary><div class="move-body" id="move-list-body"></div></details></dialog>
|
<dialog id="pause-panel" aria-labelledby="pause-title"><div class="pause-main"><div class="dialog-kicker">KONOHA FIGHTING / JEDA</div><h2 id="pause-title">JEDA</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></div><details class="move-list" id="move-list"><summary>Daftar gerakan</summary><div class="move-body" id="move-list-body"></div></details></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 src="feel.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="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="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="menu.js"></script><script src="meta.js"></script></body></html>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
18
meta.css
18
meta.css
|
|
@ -43,3 +43,21 @@
|
||||||
@media(max-height:760px) and (min-width:800px){.home-title h1{font-size:60px;margin:6px 0}.home-title p{margin-bottom:10px}.home-nav{gap:5px}.home-nav>button{padding:6px 14px}.home-nav>button small{display:none}.home-nav .home-row button{padding:6px 8px}.home-content{margin-top:0;margin-bottom:24px}}
|
@media(max-height:760px) and (min-width:800px){.home-title h1{font-size:60px;margin:6px 0}.home-title p{margin-bottom:10px}.home-nav{gap:5px}.home-nav>button{padding:6px 14px}.home-nav>button small{display:none}.home-nav .home-row button{padding:6px 8px}.home-content{margin-top:0;margin-bottom:24px}}
|
||||||
@media(max-height:760px){#settings-panel{padding:16px 20px}#settings-panel h2{margin:4px 0}#settings-panel>p{margin:4px 0 8px}#settings-panel .setting-row,#settings-panel .volume-row{padding-top:6px;padding-bottom:6px}}
|
@media(max-height:760px){#settings-panel{padding:16px 20px}#settings-panel h2{margin:4px 0}#settings-panel>p{margin:4px 0 8px}#settings-panel .setting-row,#settings-panel .volume-row{padding-top:6px;padding-bottom:6px}}
|
||||||
#settings-panel{max-height:calc(100vh - 16px)}
|
#settings-panel{max-height:calc(100vh - 16px)}
|
||||||
|
|
||||||
|
/* ---- Pasif, kelemahan, dan gaya bertarung (RENCANA 3.2 / 3.3): menu Karakter, layar VS, Daftar gerakan ---- */
|
||||||
|
.locker .preview{align-self:start;position:sticky;top:8px}
|
||||||
|
.pas{display:grid;gap:6px}
|
||||||
|
.pas-row{display:grid;gap:1px;padding:7px 10px;background:#101c26;border:1px solid #ffffff1c;border-left:3px solid #8dffa8;border-radius:6px;min-width:0}
|
||||||
|
.pas-row.dn{border-left-color:#ff8a7a}
|
||||||
|
.pas-row b{letter-spacing:.06em;color:#8dffa8}.pas-row.dn b{color:#ff9d8f}
|
||||||
|
.pas-row span{color:#c7d3dc;font-size:15px;line-height:1.25}
|
||||||
|
.pas-row small{color:#8fa3b3;font-size:13px;letter-spacing:.03em}
|
||||||
|
.pas-style{margin:8px 0 0;line-height:1.3}.pas-style b{color:#e8b84a;letter-spacing:.08em}.pas-style small{display:block;margin-top:2px;font-size:13px;color:#9fb3c4}
|
||||||
|
.vs-perk{margin:clamp(5px,1.2vh,10px) 0 0;display:flex;flex-wrap:wrap;gap:1px 10px;font:700 clamp(10px,1.3vh + 2px,13px)/1.2 Rajdhani,sans-serif;letter-spacing:1.2px;text-shadow:0 1px 3px #000c;animation:vs-quote .45s .8s both}
|
||||||
|
.vs-side.r .vs-perk{justify-content:flex-end}
|
||||||
|
.vs-perk .up{color:#9dffb8}.vs-perk .dn{color:#ffa797}.vs-perk em{font-style:normal;color:#ffe08a}
|
||||||
|
@media(max-width:760px),(max-height:420px){.vs-perk{font-size:9px;letter-spacing:.4px;gap:0 6px}}
|
||||||
|
.mv-row small.pas-desc{margin-top:1px;font-style:italic}
|
||||||
|
@media(max-height:700px){.mv-row small.pas-desc{display:none}}
|
||||||
|
@media(min-width:900px){#pause-panel{width:min(1160px,calc(100% - 24px))}.move-body{columns:200px;column-gap:20px}}
|
||||||
|
@media(min-height:421px) and (max-height:640px){.mv-sec{margin-bottom:9px}.mv-row{padding:2px 0}#pause-panel[open]{padding-top:20px;padding-bottom:20px}}
|
||||||
|
|
|
||||||
12
meta.js
12
meta.js
|
|
@ -61,7 +61,7 @@
|
||||||
special: ['SPESIALIS', 'Beli satu varian skill karakter.'],
|
special: ['SPESIALIS', 'Beli satu varian skill karakter.'],
|
||||||
reuni: ['REUNI', 'Temukan semua dialog rahasia.']
|
reuni: ['REUNI', 'Temukan semua dialog rahasia.']
|
||||||
};
|
};
|
||||||
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: {} });
|
const blank = () => ({ v: 1, coins: 0, earned: 0, voice: true, owned: { skin: ['standar'], move: [], alt: [] }, skin: {}, style: {}, move: null, alt: {}, best: { survival: 0 }, stats: { wins: 0, losses: 0, bestCombo: 0, perfects: 0 }, ach: {}, eggs: {} });
|
||||||
let save = blank();
|
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 (_) {}
|
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 (_) {} };
|
const persist = () => { try { localStorage.setItem(KEY, JSON.stringify(save)); } catch (_) {} };
|
||||||
|
|
@ -97,6 +97,9 @@
|
||||||
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 (_) {}
|
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 event(name) { if (name === 'parry') unlock('parry'); if (name === 'twoplayer') unlock('twoplayer'); }
|
||||||
|
// Gaya bertarung per karakter (RENCANA 3.3): agresif | seimbang | bertahan. Simpanan lama tanpa kunci style atau dengan nilai asing dibaca sebagai Seimbang.
|
||||||
|
function styleOf(id) { const k = save.style && typeof save.style === 'object' ? save.style[id] : null; return window.Passives ? window.Passives.styleKey(k) : 'seimbang'; }
|
||||||
|
function setStyle(id, k) { if (!window.Passives || !window.STYLES?.[k]) return false; if (!save.style || typeof save.style !== 'object') save.style = {}; save.style[id] = k; persist(); return true; }
|
||||||
function equippedMove() { return save.move && save.owned.move.includes(save.move) ? save.move : null; }
|
function equippedMove() { return save.move && save.owned.move.includes(save.move) ? save.move : null; }
|
||||||
let chip;
|
let chip;
|
||||||
function hud(info) {
|
function hud(info) {
|
||||||
|
|
@ -215,8 +218,8 @@
|
||||||
const skins = save.owned.skin.map(k => `<button class="pill ${k === sk ? 'on' : ''}" data-skin="${k}">${SKINS[k].name}</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 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 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);
|
const n = fnames(who), pas = window.Passives?.lockerHtml(who, styleOf(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>${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>`;
|
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>${pas}<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() {
|
function renderBook() {
|
||||||
const got = Object.keys(ACH).filter(k => save.ach[k]).length, st = save.stats;
|
const got = Object.keys(ACH).filter(k => save.ach[k]).length, st = save.stats;
|
||||||
|
|
@ -230,6 +233,7 @@
|
||||||
if (b.dataset.who || b.dataset.shopwho) { who = b.dataset.who || b.dataset.shopwho; 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.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.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.style) { setStyle(who, b.dataset.style); return render(); }
|
||||||
if (b.dataset.skin) { save.skin[who] = b.dataset.skin; 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.move !== undefined && b.hasAttribute('data-move')) { save.move = b.dataset.move || null; persist(); return render(); }
|
||||||
if (b.dataset.buy) {
|
if (b.dataset.buy) {
|
||||||
|
|
@ -306,5 +310,5 @@
|
||||||
}
|
}
|
||||||
mountCheatRow();
|
mountCheatRow();
|
||||||
|
|
||||||
window.Meta = { get lastAward() { return lastAward; }, redeem, pairEgg, 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; } };
|
window.Meta = { get lastAward() { return lastAward; }, styleOf, setStyle, redeem, pairEgg, 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; } };
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/* Helper pasif, kelemahan, dan gaya bertarung (RENCANA 3.2 dan 3.3). Data: content/passives.js (window.PASSIVES, window.STYLES).
|
||||||
|
Berkas ini tidak menyentuh mesin: game.js memanggil Passives.merge(id, gaya) saat pertandingan dimulai lalu membaca angkanya lewat passiveMod(actor, key, base).
|
||||||
|
Di sini juga teks angka yang tampil ke pemain (menu Karakter, layar VS, daftar gerakan), disusun otomatis dari `mods` agar selalu sama dengan yang dipakai mesin. */
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
// Kunci yang dikenal. up = 'good' bila angka naik menguntungkan pemilik (damage keluar, kecepatan, ...), 'bad' bila naik merugikan (damage masuk). add = kunci penambah.
|
||||||
|
const KEYS = {
|
||||||
|
dmgOut: { label: 'Damage', short: 'Damage', up: 'good' },
|
||||||
|
dmgBasic: { label: 'Damage pukulan dasar', short: 'Pukulan dasar', up: 'good' },
|
||||||
|
dmgSkill: { label: 'Damage skill dan ultimate', short: 'Skill dan ultimate', up: 'good' },
|
||||||
|
dmgIn: { label: 'Damage yang diterima', short: 'Damage diterima', up: 'bad' },
|
||||||
|
walk: { label: 'Kecepatan jalan', short: 'Jalan', up: 'good' },
|
||||||
|
cd: { label: 'Kecepatan pulih cooldown', short: 'Pulih cooldown', up: 'good' },
|
||||||
|
cdSkill: { label: 'Kecepatan pulih skill', short: 'Pulih skill', up: 'good' },
|
||||||
|
cdUlt: { label: 'Kecepatan pulih ultimate', short: 'Pulih ultimate', up: 'good' },
|
||||||
|
parry: { label: 'Jendela parry', short: 'Parry', up: 'good' },
|
||||||
|
modeDur: { label: 'Durasi mode', short: 'Durasi mode', up: 'good' },
|
||||||
|
regen: { label: 'Pemulihan HP', short: 'Pulih HP', up: 'good', add: true, unit: ' HP/dtk' }
|
||||||
|
};
|
||||||
|
const STYLE_ORDER = ['agresif', 'seimbang', 'bertahan'], DEFAULT_STYLE = 'seimbang';
|
||||||
|
const esc = t => String(t ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||||
|
const styles = () => window.STYLES || {};
|
||||||
|
const styleKey = k => (styles()[k] ? k : DEFAULT_STYLE);
|
||||||
|
const info = id => { const p = window.PASSIVES?.[id]; return p ? { id, name: p.name, desc: p.desc, flaw: p.flaw || { name: '', desc: '' }, mods: p.mods || {} } : null; };
|
||||||
|
const style = k => { const key = styleKey(k), s = styles()[key] || { name: 'SEIMBANG', desc: '', mods: {} }; return { key, name: s.name, desc: s.desc, mods: s.mods || {} }; };
|
||||||
|
// Gabungan angka perk + kelemahan karakter dan gaya yang dipilih: pengali dikalikan, kunci penambah dijumlahkan. Hanya kunci yang dikenal.
|
||||||
|
function merge(id, styleName) {
|
||||||
|
const out = {};
|
||||||
|
for (const src of [window.PASSIVES?.[id]?.mods, styles()[styleKey(styleName)]?.mods]) {
|
||||||
|
if (!src) continue;
|
||||||
|
for (const [k, v] of Object.entries(src)) { const def = KEYS[k]; if (!def || !Number.isFinite(v)) continue; out[k] = def.add ? (out[k] || 0) + v : (out[k] ?? 1) * v; }
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
// Satu butir efek sebagai teks: "Damage yang diterima -8%", "Pemulihan HP +0,5 HP/dtk". Koma desimal sesuai bahasa Indonesia.
|
||||||
|
function effectText(k, v, short) {
|
||||||
|
const def = KEYS[k]; if (!def) return '';
|
||||||
|
const label = short ? def.short || def.label : def.label;
|
||||||
|
if (def.add) return `${label} +${String(Math.round(v * 10) / 10).replace('.', ',')}${def.unit || ''}`;
|
||||||
|
const pct = Math.round((v - 1) * 100);
|
||||||
|
return pct ? `${label} ${pct > 0 ? '+' : '−'}${Math.abs(pct)}%` : '';
|
||||||
|
}
|
||||||
|
// Pisahkan efek menguntungkan (good) dan merugikan (bad) dari arah angkanya.
|
||||||
|
function effects(mods, short) {
|
||||||
|
const good = [], bad = [];
|
||||||
|
for (const [k, v] of Object.entries(mods || {})) {
|
||||||
|
const def = KEYS[k]; if (!def) continue;
|
||||||
|
const delta = def.add ? v : v - 1, text = effectText(k, v, short); if (!text || !delta) continue;
|
||||||
|
((delta > 0) === (def.up === 'good') ? good : bad).push(text);
|
||||||
|
}
|
||||||
|
return { good, bad };
|
||||||
|
}
|
||||||
|
// Perk dan kelemahan untuk ditampilkan: nama, cerita, dan angka otomatis.
|
||||||
|
function describe(id, short) {
|
||||||
|
const p = info(id); if (!p) return null;
|
||||||
|
const fx = effects(p.mods, short);
|
||||||
|
return { id, perk: { name: p.name, desc: p.desc, fx: fx.good.join(', ') }, flaw: { name: p.flaw.name, desc: p.flaw.desc, fx: fx.bad.join(', ') } };
|
||||||
|
}
|
||||||
|
function styleView(k, short) { const s = style(k), fx = effects(s.mods, short); return { key: s.key, name: s.name, desc: s.desc, good: fx.good, bad: fx.bad, fx: [...fx.good, ...fx.bad].join(', ') }; }
|
||||||
|
// Gaya acak untuk CPU. `rand` (0..1) dari pengacak kosmetik penelepon; bukan random() mesin.
|
||||||
|
const randomStyle = rand => STYLE_ORDER[Math.min(STYLE_ORDER.length - 1, Math.floor((typeof rand === 'function' ? rand() : Math.random()) * STYLE_ORDER.length))];
|
||||||
|
|
||||||
|
// ---- layar VS: satu baris kecil per sisi (perk, kelemahan, dan gaya bila bukan Seimbang) ----
|
||||||
|
function vsHtml(id, styleName) {
|
||||||
|
const d = describe(id); if (!d) return '';
|
||||||
|
const sk = styleKey(styleName), tag = sk === DEFAULT_STYLE ? '' : `<em>GAYA ${esc(style(sk).name)}</em>`;
|
||||||
|
return `<p class="vs-perk" aria-label="Pasif ${esc(d.perk.name)}. Kelemahan ${esc(d.flaw.name)}."><span class="up">▲ ${esc(d.perk.name)}</span><span class="dn">▼ ${esc(d.flaw.name)}</span>${tag}</p>`;
|
||||||
|
}
|
||||||
|
function decorateVS(el, vs = {}) {
|
||||||
|
try {
|
||||||
|
const sides = [['.vs-side.l .vs-info', vs.player, vs.styles?.player], ['.vs-side.r .vs-info', vs.enemy, vs.styles?.enemy]];
|
||||||
|
for (const [sel, id, st] of sides) { const box = el.querySelector(sel); if (box && !box.querySelector('.vs-perk')) box.insertAdjacentHTML('beforeend', vsHtml(id, st)); }
|
||||||
|
} catch (_) { /* hiasan saja; layar VS tetap tampil */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- menu Karakter (meta.js renderLocker): blok Pasif dan blok Gaya bertarung ----
|
||||||
|
function lockerHtml(id, styleName) {
|
||||||
|
const d = describe(id); if (!d) return '';
|
||||||
|
const sk = styleKey(styleName), sv = styleView(sk);
|
||||||
|
const row = (cls, mark, name, desc, fx) => `<div class="pas-row ${cls}"><b>${mark} ${esc(name)}</b><span>${esc(desc)}</span>${fx ? `<small>${esc(fx)}</small>` : ''}</div>`;
|
||||||
|
const pills = STYLE_ORDER.filter(k => styles()[k]).map(k => `<button class="pill ${k === sk ? 'on' : ''}" data-style="${k}">${esc(styles()[k].name)}</button>`).join('');
|
||||||
|
return `<h5>Pasif dan kelemahan</h5><div class="pas">${row('up', '▲', d.perk.name, d.perk.desc, d.perk.fx)}${row('dn', '▼', d.flaw.name, d.flaw.desc, d.flaw.fx)}</div>`
|
||||||
|
+ `<h5>Gaya bertarung</h5><div class="pills">${pills}</div><p class="meta-hint pas-style"><b>${esc(sv.name)}</b> — ${esc(sv.desc)}${sv.fx ? `<small>${esc(sv.fx)}</small>` : ''}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Passives = { KEYS, STYLE_ORDER, DEFAULT_STYLE, info, style, styleKey, merge, effects, describe, styleView, randomStyle, vsHtml, decorateVS, lockerHtml };
|
||||||
|
})();
|
||||||
1
vs.js
1
vs.js
|
|
@ -65,6 +65,7 @@
|
||||||
+ vsSide('l', L, 'PEMAIN 1', dlg ? dlg.l : quote(L.id, 'intro'), dlg) + vsSide('r', R, two ? 'PEMAIN 2' : 'KOMPUTER', dlg ? dlg.r : quote(R.id, 'intro'), dlg)
|
+ vsSide('l', L, 'PEMAIN 1', dlg ? dlg.l : quote(L.id, 'intro'), dlg) + vsSide('r', R, two ? 'PEMAIN 2' : 'KOMPUTER', dlg ? dlg.r : quote(R.id, 'intro'), dlg)
|
||||||
+ `<div class="vs-mark" aria-hidden="true"><b>VS</b></div><div class="vs-skip">KLIK ATAU SPASI UNTUK LEWATI</div><div class="vs-timer" aria-hidden="true"><i></i></div>`;
|
+ `<div class="vs-mark" aria-hidden="true"><b>VS</b></div><div class="vs-skip">KLIK ATAU SPASI UNTUK LEWATI</div><div class="vs-timer" aria-hidden="true"><i></i></div>`;
|
||||||
document.body.append(el);
|
document.body.append(el);
|
||||||
|
window.Passives?.decorateVS(el, info);
|
||||||
const c = cur = { el, onDone, t0: performance.now(), done: false, timers: [], dialog: dlg };
|
const c = cur = { el, onDone, t0: performance.now(), done: false, timers: [], dialog: dlg };
|
||||||
el.style.setProperty('--vs-run', (dur - FADE) + 'ms');
|
el.style.setProperty('--vs-run', (dur - FADE) + 'ms');
|
||||||
c.timers.push(setTimeout(() => finish('time'), dur - FADE), setTimeout(() => { try { window.__game?.menuSound?.('pick'); } catch (_) {} }, 520));
|
c.timers.push(setTimeout(() => finish('time'), dur - FADE), setTimeout(() => { try { window.__game?.menuSound?.('pick'); } catch (_) {} }, 520));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue