Gabung wf/t7-daily: tantangan harian dan hadiah
This commit is contained in:
commit
f78a6dd622
|
|
@ -0,0 +1,74 @@
|
||||||
|
/* Tantangan harian (daily.js, RENCANA 7.1 dan 7.2): tombol HARIAN di menu utama, kartu tantangan, baris aturan di layar VS, penanda aturan di arena.
|
||||||
|
Tinggi minimum yang didukung 520 px; ada aturan ringkas untuk jendela pendek (<= 560 px) dan ponsel lanskap (<= 420 px). */
|
||||||
|
|
||||||
|
/* ---- Tombol HARIAN di menu utama: di dalam .home-top-right, tepat di kiri teks versi, sebagai item flex biasa. Margin atas dan bawahnya negatif sebesar
|
||||||
|
setengah tingginya, jadi kotak margin-nya setinggi 0: bilah atas tidak bertambah tinggi sedikit pun (menu tetap muat di 520 px) dan tombol
|
||||||
|
tetap di tengah vertikal. Sengaja tidak memakai position:absolute atau menjadikan .home-top-right sebagai induk posisi: tombol layar penuh
|
||||||
|
(.menu-fullscreen, absolut) memakai .home-top sebagai induknya. ---- */
|
||||||
|
#front-menu .daily-entry{display:inline-flex;align-items:center;gap:8px;min-height:26px;margin:-13px 18px -13px 0;padding:3px 12px 3px 10px;background:#102b37c4;border:1px solid #d7b87d77;border-left:3px solid #d7b87d;color:#f6eedb;font:700 13px/1.1 Rajdhani,Arial,sans-serif;letter-spacing:2px;white-space:nowrap;cursor:pointer;transform:skewX(-7deg);transition:background .14s,border-color .14s,color .14s}
|
||||||
|
#front-menu .daily-entry>*{transform:skewX(7deg)}
|
||||||
|
#front-menu .daily-entry i{font-style:normal;font-size:11px;color:#d7b87d}
|
||||||
|
#front-menu .daily-entry .de-n{color:#f0d49d}
|
||||||
|
#front-menu .daily-entry b{padding:1px 6px;font-size:10px;letter-spacing:1.5px;color:#102b37;background:#e6c78f}
|
||||||
|
#front-menu .daily-entry em{font-style:normal;font-size:11px;letter-spacing:1px;color:#a9c0c4}
|
||||||
|
#front-menu .daily-entry:not(.done){animation:daily-nudge 2.4s ease-in-out infinite}
|
||||||
|
#front-menu .daily-entry:hover,#front-menu .daily-entry:focus-visible{background:#f0d49d;color:#122837;border-color:#fff0c8;animation:none}
|
||||||
|
#front-menu .daily-entry:hover i,#front-menu .daily-entry:focus-visible i,#front-menu .daily-entry:hover .de-n,#front-menu .daily-entry:focus-visible .de-n,#front-menu .daily-entry:hover em,#front-menu .daily-entry:focus-visible em{color:#122837}
|
||||||
|
#front-menu .daily-entry:hover b,#front-menu .daily-entry:focus-visible b{background:#122837;color:#f0d49d}
|
||||||
|
#front-menu .daily-entry.done{border-left-color:#8dffa8}
|
||||||
|
#front-menu .daily-entry.done i{color:#8dffa8}
|
||||||
|
#front-menu .daily-entry.done b{background:#8dffa8}
|
||||||
|
@keyframes daily-nudge{0%,100%{box-shadow:0 0 0 #d7b87d00}50%{box-shadow:0 0 14px #d7b87d88}}
|
||||||
|
html.touch #front-menu .daily-entry{min-height:32px;margin-top:-16px;margin-bottom:-16px}
|
||||||
|
@media(max-width:700px){#front-menu .daily-entry .de-n,#front-menu .daily-entry em{display:none}}
|
||||||
|
/* Ponsel lanskap: bilah atas hanya 9 px; kotak margin tetap 0 tetapi tombol digeser 3 px ke bawah supaya tepi atasnya tidak terpotong bila teks versi tersembunyi. */
|
||||||
|
@media(max-height:420px){#front-menu .daily-entry{min-height:22px;margin-top:-8px;margin-bottom:-14px;padding:2px 9px 2px 8px;font-size:11px;letter-spacing:1.5px}html.touch #front-menu .daily-entry{min-height:28px;margin-top:-11px;margin-bottom:-17px}}
|
||||||
|
|
||||||
|
/* ---- Kartu tantangan: <dialog> modal memakai gaya dialog bawaan (style.css) dengan isi yang dipadatkan ---- */
|
||||||
|
.daily-dialog{width:min(580px,calc(100% - 24px));padding:16px 22px 14px}
|
||||||
|
.daily-dialog .dialog-kicker{margin-bottom:2px}
|
||||||
|
.dl-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
||||||
|
.daily-dialog h2{margin:4px 0 0;font-size:34px;font-style:italic;letter-spacing:1px}
|
||||||
|
.dl-state{padding:2px 10px;font-size:12px;font-weight:700;letter-spacing:2px;color:#102b37;background:var(--gold);transform:skewX(-8deg)}
|
||||||
|
.dl-state.ok{background:#8dffa8}
|
||||||
|
.daily-dialog .dl-desc{margin:6px 0 10px;font-size:16px;line-height:1.3;color:#cfdde0}
|
||||||
|
.dl-facts{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:6px;margin:0 0 10px}
|
||||||
|
.dl-f{grid-column:span 2;min-width:0;padding:5px 9px;background:#0b1d29;border:1px solid #ffffff1c;border-left:3px solid #7fa698}
|
||||||
|
.dl-f:nth-child(n+4){grid-column:span 3}
|
||||||
|
.dl-f dt{font-size:10px;font-weight:700;letter-spacing:2px;color:#9fb3c4}
|
||||||
|
.dl-f dd{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:2px 0 0;font-size:16px;font-weight:700;letter-spacing:.5px;color:#f6eedb}
|
||||||
|
.dl-f dd img{width:24px;height:24px;object-fit:cover;border-radius:3px}
|
||||||
|
.dl-f dd small{flex-basis:100%;font-size:12px;font-weight:500;letter-spacing:0;color:#9fb3c4}
|
||||||
|
.dl-pick>span{display:block;margin:0 0 5px;font-size:10px;font-weight:700;letter-spacing:2px;color:#9fb3c4}
|
||||||
|
.dl-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
|
||||||
|
.dl-chip{display:grid;justify-items:center;gap:2px;width:62px;padding:3px 2px 4px;background:#13222d;border:1px solid #ffffff33;font:700 10px/1.1 Rajdhani,Arial,sans-serif;letter-spacing:.4px;color:#dfe9ea}
|
||||||
|
.dl-chip img{width:34px;height:34px;object-fit:cover;border-radius:3px}
|
||||||
|
.dl-chip span{max-width:58px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.dl-chip:hover{background:#315263}
|
||||||
|
.dl-chip[aria-pressed=true]{background:var(--gold);color:#102b37;border-color:#fff0c8}
|
||||||
|
.dl-chip:disabled{opacity:.4;cursor:not-allowed}
|
||||||
|
.dl-chip:disabled:hover{background:#13222d}
|
||||||
|
.daily-dialog .dialog-buttons .dl-go{background:var(--gold);color:#132b37;border-color:#fff0c8}
|
||||||
|
.daily-dialog .dialog-buttons .dl-go:hover{background:#fae0b4}
|
||||||
|
.dl-note{display:block;margin-top:8px;font-size:12px;letter-spacing:.3px;color:#8fa3b3}
|
||||||
|
html.touch .dl-chip{width:66px;padding:5px 2px 6px}
|
||||||
|
html.touch .daily-dialog .dialog-buttons button{padding:12px 8px}
|
||||||
|
@media(max-height:560px){.daily-dialog{padding:12px 18px 10px}.daily-dialog h2{font-size:28px}.daily-dialog .dl-desc{font-size:14px;margin:4px 0 7px}.dl-f{padding:3px 8px}.dl-f dd{font-size:14px}.dl-f dd small{font-size:11px}.dl-chip{width:56px}.dl-chip img{width:28px;height:28px}.dl-chips{margin-bottom:8px}.dl-note{margin-top:5px}}
|
||||||
|
@media(max-height:420px){.daily-dialog{padding:8px 14px 8px}.daily-dialog h2{font-size:22px}.dl-state{font-size:10px}.daily-dialog .dl-desc{font-size:13px;margin:2px 0 5px}.dl-facts{gap:4px;margin-bottom:6px}.dl-f dd{font-size:13px}.dl-f dd img{width:18px;height:18px}.dl-chip{width:48px;padding:2px 1px 3px}.dl-chip img{width:22px;height:22px}.dl-chip span{max-width:46px;font-size:9px}.dl-pick>span{margin-bottom:3px}.dl-chips{gap:4px;margin-bottom:6px}.daily-dialog .dialog-buttons button{padding:5px;font-size:13px}.dl-note{display:none}}
|
||||||
|
|
||||||
|
/* ---- Layar VS: satu baris aturan di bawah bilah atas (label DIALOG RAHASIA turun satu baris bila ada) ---- */
|
||||||
|
.vs-daily{position:absolute;z-index:6;left:50%;top:calc(clamp(8px,2.4vh,22px) + clamp(22px,3.6vh + 2px,32px));transform:translateX(-50%);display:flex;align-items:baseline;gap:10px;max-width:92vw;padding:2px 14px;white-space:nowrap;background:#06121bd9;border-left:3px solid #e0bf85;border-right:3px solid #e0bf85;font-family:Rajdhani,Arial,sans-serif;animation:vs-fade .4s .5s both}
|
||||||
|
.vs-daily b{font-size:clamp(9px,1.1vh + 3px,12px);letter-spacing:3px;color:#e0bf85}
|
||||||
|
.vs-daily strong{font-size:clamp(13px,1.8vh + 4px,19px);font-style:italic;letter-spacing:2px;color:#fff0c8}
|
||||||
|
.vs-daily span{overflow:hidden;text-overflow:ellipsis;font-size:clamp(11px,1.4vh + 3px,15px);font-weight:600;color:#cfdde0}
|
||||||
|
#vs-screen.has-daily .vs-secret{top:calc(clamp(8px,2.4vh,22px) + clamp(22px,3.6vh + 2px,32px) + clamp(24px,3.6vh + 4px,34px))}
|
||||||
|
@media(max-width:760px),(max-height:420px){.vs-daily b,.vs-daily span{display:none}}
|
||||||
|
|
||||||
|
/* ---- Arena: penanda aturan di tepi atas tengah (di atas bilah HUD, tidak mengubah tata letak). Berkedip bila pemain mencoba aksi yang dimatikan aturan. ---- */
|
||||||
|
#daily-tag{position:absolute;left:50%;top:0;z-index:3;transform:translateX(-50%);padding:0 12px;white-space:nowrap;pointer-events:none;font:700 10px/12px Rajdhani,Arial,sans-serif;letter-spacing:2px;color:#f0d49d;background:#06121bcc;border:1px solid #d7b87d66;border-top:0;transition:background .12s,color .12s}
|
||||||
|
#daily-tag.hit{background:#e6c78f;color:#102b37}
|
||||||
|
.in-menu #daily-tag{display:none}
|
||||||
|
@media(max-height:500px){#daily-tag{font-size:9px;line-height:11px;letter-spacing:1.5px}}
|
||||||
|
/* Hanya ultimate: kartu dan tombol sentuh yang dimatikan diredupkan. */
|
||||||
|
body[data-daily-rule=ultimate] .skill-card:not(.ultimate),body[data-daily-rule=ultimate] .tbtn-attack,body[data-daily-rule=ultimate] .tbtn-s1,body[data-daily-rule=ultimate] .tbtn-s2{opacity:.4;filter:grayscale(.8)}
|
||||||
|
@media(prefers-reduced-motion:reduce){#daily-tag{transition:none}}
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
/* Tantangan harian dan hadiah harian (RENCANA 7.1 dan 7.2). Gaya: daily.css. Dimuat paling akhir di index.html, sesudah meta.js.
|
||||||
|
Tanggal hanya dipakai memilih aturan harian dan menentukan hadiah; di dalam simulasi pertandingan tidak ada Date maupun Math.random.
|
||||||
|
|
||||||
|
7.1 Tantangan harian: tiap tanggal lokal (YYYY-MM-DD) punya satu tantangan yang sama untuk semua pemain: aturan, lawan, arena, dan tingkat dipilih dari hash
|
||||||
|
tanggal (hash % jumlah pilihan; aturan juga digeser bila kebetulan sama dengan aturan kemarin). Pemain memilih petarungnya sendiri di kartu tantangan.
|
||||||
|
Tombol kecil HARIAN di pojok atas menu utama membuka kartu (nama aturan, deskripsi, lawan, arena, tingkat, hadiah, streak, pilihan petarung). Tantangan
|
||||||
|
dimainkan sebagai satu pertandingan versus biasa lewat __game.startMatch({..., daily: tanggal}); MAIN LAGI di layar hasil mengulang tantangan yang sama.
|
||||||
|
Hadiah: menang = 150 koin + 25 per hari beruntun (maksimal +150), sekali per tanggal; kalah boleh coba lagi. Streak = hari beruntun yang selesai; terputus
|
||||||
|
bila ada hari terlewat. Simpanan di Meta.save.daily = { last, streak, best, total, greet, fest, pick } (ikut kode simpanan; simpanan lama tanpa kunci ini
|
||||||
|
terbaca kosong); Meta.addCoins (juga satu-satunya cara memicu simpan) dipanggil sesudah isinya diubah.
|
||||||
|
7.2 Hadiah hari pertama: sekali per tanggal saat menu utama pertama tampil (+25 koin, toast), ditambah kejutan tanggal tertentu sekali per tahun (FEST).
|
||||||
|
|
||||||
|
Sepuluh aturan (RULES). Yang tidak butuh kait khusus diterapkan lewat pengubah sisi (sideMods) dan BALANCE yang diberikan game.js ke arm():
|
||||||
|
Darah satu (HP kedua pihak 20), Lawan cepat (jalan dan pulih cooldown lawan naik), Tanpa blok (peluang blok CPU 0), Raksasa (HP lawan 1,5x + gambar lebih besar).
|
||||||
|
Sisanya lewat kait kecil di game.js yang semuanya berbentuk window.Daily?.xxx (tanpa modul ini atau tanpa tantangan aktif semuanya no-op):
|
||||||
|
startMatch arm(options.daily, {sideMods, balance, versus}) mengaktifkan atau mematikan aturan; dipanggil di setiap startMatch
|
||||||
|
jump, startAttack, cast, startCommand, startThrow, useShopMove deny(jenis) Tanpa lompat dan Hanya ultimate
|
||||||
|
press('l') deny('guard') Tanpa blok
|
||||||
|
step cooldowns(hero.cooldowns) Hanya ultimate (ultimate selalu siap); slip(vx lama, vx baru) Lantai licin
|
||||||
|
updateDummy slip(...) untuk lawan
|
||||||
|
hitDummy hit(damage, counter, kombo, waktu mesin) Kombo wajib dan Counter saja
|
||||||
|
heroGuard (parry berhasil) parry(waktu mesin)
|
||||||
|
drawDummy foeScale (Raksasa)
|
||||||
|
Satu ronde: MatchRules.create dan MatchRules.tick dibungkus di sini (match.toWin = 1; pemenang ditetapkan di akhir ronde pertama), HUD game.js membaca match.toWin.
|
||||||
|
Bungkus lain: VS.show (baris aturan di layar VS), Meta.award (hadiah di rincian koin layar hasil), klik MAIN LAGI (mengulang tantangan yang sama).
|
||||||
|
|
||||||
|
Uji manual: ?daily=YYYY-MM-DD memajukan atau memundurkan tanggal untuk seluruh modul (aturan, hadiah, streak); di konsol Daily.setDate('2026-08-17'),
|
||||||
|
Daily.setDate(null) (kembali ke tanggal asli), Daily.table(14), Daily.status(), Daily.startup(), Daily.resetState(), Daily.open(), Daily.start('megachan'). */
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
// ---- angka ----
|
||||||
|
const REWARD = 150, REWARD_STEP = 25, REWARD_STEPS = 6, GREET = 25; // hadiah tantangan 150 + 25 per hari beruntun (maksimal +150); hadiah hari pertama main
|
||||||
|
const HP_DARAH = 20, HP_RAKSASA = 1.5, SKALA_RAKSASA = 1.18, LICIN = .22; // HP Darah satu; pengali HP dan skala gambar Raksasa; bagian percepatan yang tersisa di lantai licin
|
||||||
|
const COUNTER_OPEN = 2.5, COUNTER_BIASA = .45, COUNTER_BUKA = 1.2; // Counter saja: lama celah terbuka (detik waktu mesin), pengali pukulan biasa dan pukulan di celah
|
||||||
|
const LAWAN_JALAN = 1.4, LAWAN_CD = 1.35; // Lawan cepat: pengali jalan dan kecepatan pulih cooldown
|
||||||
|
const LEVEL = { easy: 'MUDAH', medium: 'SEDANG', hard: 'SULIT', excellent: 'SANGAT SULIT' };
|
||||||
|
const ALL3 = ['easy', 'medium', 'hard'], EASY2 = ['easy', 'medium'];
|
||||||
|
// levels = tingkat yang boleh dipilih tanggal; aturan yang menyulitkan pemain hanya memakai tingkat ringan.
|
||||||
|
const RULES = [
|
||||||
|
{ id: 'darah', name: 'DARAH SATU', vs: 'Kedua pihak cuma punya 20 HP', levels: EASY2, desc: 'Kedua petarung cuma punya 20 HP. Satu serangan parah dan semuanya selesai, jadi jangan kedip.' },
|
||||||
|
{ id: 'ultimate', name: 'HANYA ULTIMATE', vs: 'Pukulan dan skill mati, ultimate selalu siap', levels: ALL3, desc: 'Pukulan dan skill mogok kerja. Cuma ultimate yang mau masuk, dan dia selalu siap.' },
|
||||||
|
{ id: 'blok', name: 'TANPA BLOK', vs: 'Blok dan parry mati untuk semua', levels: ALL3, desc: 'Tombol blok dan parry macet, buat kamu dan lawan. Pertahanan terbaik adalah kaki.' },
|
||||||
|
{ id: 'cepat', name: 'LAWAN CEPAT', vs: 'Lawan lebih cepat, skill cepat pulih', levels: EASY2, desc: 'Lawanmu habis minum kopi tiga gelas: jalannya kencang dan skillnya cepat pulih.' },
|
||||||
|
{ id: 'licin', name: 'LANTAI LICIN', vs: 'Semua petarung meluncur di lantai', levels: ALL3, desc: 'Lantai baru dipel. Semua petarung meluncur dan susah berhenti tepat waktu.' },
|
||||||
|
{ id: 'lompat', name: 'TANPA LOMPAT', vs: 'Kamu tidak bisa lompat', levels: ALL3, desc: 'Kakimu terpaku ke lantai. Lompat dan lompat ganda tidak bisa dipakai.' },
|
||||||
|
{ id: 'kombo', name: 'KOMBO WAJIB', vs: 'Damage naik di tiap pukulan kombo', levels: ALL3, desc: 'Pukulan tunggal cuma menggelitik. Damage naik di setiap pukulan kombo, jadi rangkai terus.' },
|
||||||
|
{ id: 'ronde', name: 'SATU RONDE', vs: 'Satu ronde menentukan pemenang', levels: ALL3, desc: 'Tidak ada ronde ulang: satu ronde, satu pemenang. Kalau seri, rondenya diulang.' },
|
||||||
|
{ id: 'counter', name: 'COUNTER SAJA', vs: 'Damage penuh hanya setelah COUNTER atau PARRY', levels: EASY2, desc: 'Pukulan biasa cuma menggores. Buka celah dengan COUNTER atau PARRY, lalu sikat selama celahnya terbuka.' },
|
||||||
|
{ id: 'raksasa', name: 'RAKSASA', vs: 'Lawan raksasa dengan HP 1,5 kali', levels: EASY2, desc: 'Lawan membesar dan HP-nya 1,5 kali lipat. Sabar, atur jarak, dan jangan panik.' }
|
||||||
|
];
|
||||||
|
const DENY = { ultimate: ['attack', 'skill1', 'skill2', 'command', 'throw', 'shop'], lompat: ['jump'], blok: ['guard'] };
|
||||||
|
// Kejutan tanggal (bulan-hari): sekali per tahun. Hanya hari netral; teks ringan.
|
||||||
|
const FEST = {
|
||||||
|
'01-01': { coins: 100, text: 'SELAMAT TAHUN BARU! Semoga kombomu panjang dan dompetmu tebal.' },
|
||||||
|
'03-14': { coins: 50, text: 'HARI PI! Hari ini semua serba bulat: pukulan, kombo, dan koinmu.' },
|
||||||
|
'08-08': { coins: 50, text: 'HARI KUCING SEDUNIA! Meong. Satu koin jatuh dari meja, kamu yang pungut.' },
|
||||||
|
'08-17': { coins: 100, text: 'DIRGAHAYU INDONESIA! Selamat 17-an, semoga pukulanmu sekencang lomba balap karung.' }
|
||||||
|
};
|
||||||
|
const QUIPS = ['Absen dulu, baru berantem.', 'Koin receh, semangat gede.', 'Pemanasan gratis dari arena.', 'Selamat datang kembali, jagoan.', 'Satu hari, satu hadiah. Adil, kan?', 'Arena kangen kamu.'];
|
||||||
|
const $ = s => document.querySelector(s);
|
||||||
|
const esc = t => String(t ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||||
|
const toast = (m, k) => { try { window.Meta?.toast?.(m, k); } catch (_) { /* toast hanya hiasan */ } };
|
||||||
|
|
||||||
|
// ---- tanggal: hanya untuk memilih aturan dan hadiah ----
|
||||||
|
const DAY = 86400000, pad = n => String(n).padStart(2, '0'), ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||||
|
// Nomor hari dari 'YYYY-MM-DD' lewat UTC (selisih hari tidak terpengaruh zona waktu atau DST); null bila bukan tanggal yang sah.
|
||||||
|
function dayNum(iso) {
|
||||||
|
const m = ISO.exec(String(iso)); if (!m) return null;
|
||||||
|
const t = Date.UTC(+m[1], +m[2] - 1, +m[3]), d = new Date(t);
|
||||||
|
return d.getUTCFullYear() === +m[1] && d.getUTCMonth() === +m[2] - 1 && d.getUTCDate() === +m[3] ? Math.round(t / DAY) : null;
|
||||||
|
}
|
||||||
|
const isoOf = n => new Date(n * DAY).toISOString().slice(0, 10);
|
||||||
|
let override = null; // tanggal uji dari ?daily=YYYY-MM-DD atau Daily.setDate(...)
|
||||||
|
function today() { if (override) return override; const d = new Date(); return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
|
||||||
|
function dateText(iso) {
|
||||||
|
const n = dayNum(iso); if (n == null) return String(iso);
|
||||||
|
try { return new Intl.DateTimeFormat('id-ID', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' }).format(new Date(n * DAY)); } catch (_) { return String(iso); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- pemilihan tantangan dari tanggal ----
|
||||||
|
// FNV-1a 32 bit dengan pengacakan akhir (murmur), jadi tanggal yang berurutan tidak menghasilkan indeks yang berpola.
|
||||||
|
function hash(s) {
|
||||||
|
let h = 0x811c9dc5;
|
||||||
|
for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 0x01000193);
|
||||||
|
h ^= h >>> 16; h = Math.imul(h, 0x85ebca6b); h ^= h >>> 13; h = Math.imul(h, 0xc2b2ae35); h ^= h >>> 16;
|
||||||
|
return h >>> 0;
|
||||||
|
}
|
||||||
|
const idxOf = (iso, salt, n) => hash(salt + ':' + iso) % n;
|
||||||
|
// Aturan hari ini; bila kebetulan sama dengan indeks mentah kemarin, geser satu supaya hari berturut-turut jarang sama.
|
||||||
|
function ruleIdx(iso) {
|
||||||
|
const n = RULES.length, r = idxOf(iso, 'rule', n), dn = dayNum(iso);
|
||||||
|
return dn != null && r === idxOf(isoOf(dn - 1), 'rule', n) ? (r + 1) % n : r;
|
||||||
|
}
|
||||||
|
const roster = () => { const a = Object.keys(window.FIGHTERS || {}); if (window.Browo && window.BROWO_MANIFEST && !a.includes('browo')) a.push('browo'); return a.sort(); };
|
||||||
|
const fname = id => window.FIGHTERS?.[id]?.name || (id === 'browo' ? 'BRO WO' : String(id).toUpperCase());
|
||||||
|
const portrait = id => `assets/${id}/ui/portrait.webp`;
|
||||||
|
function challenge(iso = today()) {
|
||||||
|
if (dayNum(iso) == null) return null;
|
||||||
|
const rule = RULES[ruleIdx(iso)], ids = roster(), stages = Object.keys(window.MatchRules?.stages || {}).sort();
|
||||||
|
if (!ids.length || !stages.length) return null;
|
||||||
|
return { date: iso, rule, foe: ids[idxOf(iso, 'foe', ids.length)], stage: stages[idxOf(iso, 'stage', stages.length)], level: rule.levels[idxOf(iso, 'level', rule.levels.length)] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- simpanan: Meta.save.daily ----
|
||||||
|
const okIso = v => typeof v === 'string' && dayNum(v) != null, cnt = v => (Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
|
||||||
|
function view() { // pembacaan aman: kunci hilang atau rusak dibaca kosong
|
||||||
|
const d = window.Meta?.save?.daily, o = d && typeof d === 'object' ? d : {};
|
||||||
|
return { last: okIso(o.last) ? o.last : null, streak: cnt(o.streak), best: cnt(o.best), total: cnt(o.total), greet: okIso(o.greet) ? o.greet : null, pick: typeof o.pick === 'string' ? o.pick : null, fest: o.fest && typeof o.fest === 'object' ? o.fest : {} };
|
||||||
|
}
|
||||||
|
function store() { const s = window.Meta?.save; if (!s) return null; if (!s.daily || typeof s.daily !== 'object') s.daily = {}; return s.daily; }
|
||||||
|
const persist = () => { try { window.Meta?.addCoins?.(0); } catch (_) { /* tanpa simpanan */ } }; // addCoins menyimpan seluruh Meta.save
|
||||||
|
const rewardFor = streak => REWARD + Math.min(Math.max(streak - 1, 0), REWARD_STEPS) * REWARD_STEP;
|
||||||
|
// streak yang masih hidup: selesai hari ini, atau selesai kemarin (tinggal diselesaikan hari ini); selain itu terputus.
|
||||||
|
function status(iso = today()) {
|
||||||
|
const v = view(), dn = dayNum(iso), done = v.last === iso, alive = done || (dn != null && v.last === isoOf(dn - 1)), streak = alive ? v.streak : 0;
|
||||||
|
return { date: iso, done, streak, best: Math.max(v.best, streak), total: v.total, reward: rewardFor(streak + 1) };
|
||||||
|
}
|
||||||
|
// Hadiah tantangan untuk tanggal iso; null bila sudah diambil. Sekali per tanggal.
|
||||||
|
function claim(iso) {
|
||||||
|
const d = store(); if (!d || !okIso(iso)) return null;
|
||||||
|
const v = view(); if (v.last === iso) return null;
|
||||||
|
const streak = v.last === isoOf(dayNum(iso) - 1) ? v.streak + 1 : 1, coins = rewardFor(streak);
|
||||||
|
d.last = iso; d.streak = streak; d.best = Math.max(v.best, streak); d.total = v.total + 1;
|
||||||
|
window.Meta.addCoins(coins);
|
||||||
|
return { coins, streak, best: d.best };
|
||||||
|
}
|
||||||
|
// 7.2: hadiah hari pertama bermain tiap tanggal, dan kejutan tanggal tertentu sekali per tahun. Aman dipanggil berulang (hanya membayar yang belum diambil).
|
||||||
|
function startup(iso = today()) {
|
||||||
|
const d = store(); if (!d || !okIso(iso) || !window.Meta?.addCoins) return null;
|
||||||
|
const got = { greet: 0, fest: 0 };
|
||||||
|
if (view().greet !== iso) {
|
||||||
|
d.greet = iso; got.greet = GREET; window.Meta.addCoins(GREET);
|
||||||
|
toast(`HADIAH HARI INI +${GREET} koin · ${QUIPS[hash('quip:' + iso) % QUIPS.length]}`, 'coin');
|
||||||
|
}
|
||||||
|
const key = iso.slice(5), year = iso.slice(0, 4), f = FEST[key];
|
||||||
|
if (f && view().fest[key] !== year) {
|
||||||
|
if (!d.fest || typeof d.fest !== 'object') d.fest = {};
|
||||||
|
d.fest[key] = year; got.fest = f.coins; window.Meta.addCoins(f.coins);
|
||||||
|
setTimeout(() => toast(`${f.text} +${f.coins} koin`), got.greet ? 1100 : 0);
|
||||||
|
}
|
||||||
|
return got;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- aturan di mesin (kait dipanggil game.js) ----
|
||||||
|
let cur = null, redo = null, again = false, baseHp = null, openUntil = -1;
|
||||||
|
// cur = tantangan yang sedang dimainkan; redo = tantangan terakhir (untuk MAIN LAGI di layar hasil); again = MAIN LAGI baru ditekan.
|
||||||
|
// flag = tanggal 'YYYY-MM-DD' (atau true = hari ini) bila startMatch dimulai dari kartu tantangan. Dipanggil di setiap startMatch, jadi pertandingan biasa
|
||||||
|
// otomatis mematikan aturan dan mengembalikan HP pemain.
|
||||||
|
function arm(flag, ctx) { // galat di sini tidak boleh menghalangi startMatch: tantangan dimatikan dan pertandingan jalan seperti biasa
|
||||||
|
try { armRun(flag, ctx); } catch (e) { console.error(e); cur = redo = null; }
|
||||||
|
}
|
||||||
|
function armRun(flag, ctx) {
|
||||||
|
const c = ctx || {};
|
||||||
|
let date = typeof flag === 'string' ? flag : flag ? today() : null;
|
||||||
|
if (!date && again && redo) date = redo.date;
|
||||||
|
again = false; openUntil = -1;
|
||||||
|
if (c.balance) { if (baseHp == null) baseHp = c.balance.heroMax; c.balance.heroMax = baseHp; } // setShadowFoe sudah mengembalikan HP lawan
|
||||||
|
cur = redo = date && c.versus ? challenge(date) : null;
|
||||||
|
paintTag();
|
||||||
|
if (!cur) return;
|
||||||
|
const id = cur.rule.id, B = c.balance, E = c.sideMods?.enemy;
|
||||||
|
if (id === 'darah' && B) { B.heroMax = HP_DARAH; B.dummyMax = HP_DARAH; }
|
||||||
|
else if (id === 'raksasa' && B) B.dummyMax = Math.round(B.dummyMax * HP_RAKSASA);
|
||||||
|
else if (id === 'cepat' && E) { E.walk = (E.walk ?? 1) * LAWAN_JALAN; E.cd = (E.cd ?? 1) * LAWAN_CD; }
|
||||||
|
else if (id === 'blok' && E) E.parry = 0; // peluang blok CPU = dasar x parry
|
||||||
|
}
|
||||||
|
// true bila aksi pemain dilarang aturan hari ini (dan penanda aturan berkedip sebagai umpan balik).
|
||||||
|
const deny = kind => { const no = !!cur && !!DENY[cur.rule.id]?.includes(kind); if (no) nudge(); return no; };
|
||||||
|
// Damage pukulan pemain ke lawan (dipanggil sesudah bonus counter bawaan). combo = jumlah pukulan yang sudah kena sebelum ini; t = waktu mesin.
|
||||||
|
function hit(d, counter, combo, t) {
|
||||||
|
const id = cur?.rule.id; if ((id !== 'kombo' && id !== 'counter') || !(d > 0)) return d;
|
||||||
|
if (id === 'kombo') d *= Math.min(2, .6 + .2 * combo);
|
||||||
|
else { if (counter) openUntil = t + COUNTER_OPEN; d *= t < openUntil ? COUNTER_BUKA : COUNTER_BIASA; }
|
||||||
|
return Math.max(1, Math.round(d));
|
||||||
|
}
|
||||||
|
const parry = t => { if (cur?.rule.id === 'counter') openUntil = t + COUNTER_OPEN; };
|
||||||
|
const cooldowns = cd => { if (cur?.rule.id === 'ultimate' && cd) cd.ultimate = 0; };
|
||||||
|
// Lantai licin: perubahan kecepatan mendatar per langkah hanya sebagian kecil dari biasanya, jadi percepatan dan gesekan sama-sama melemah.
|
||||||
|
const slip = (prev, next) => (cur?.rule.id === 'licin' ? prev + (next - prev) * LICIN : next);
|
||||||
|
|
||||||
|
// Satu ronde dan pembungkus lain dipasang sekali; semuanya no-op bila tidak ada tantangan aktif.
|
||||||
|
function wrapRules() {
|
||||||
|
const R = window.MatchRules; if (!R || R.__daily || typeof R.create !== 'function' || typeof R.tick !== 'function') return;
|
||||||
|
R.__daily = true; const create = R.create, tick = R.tick;
|
||||||
|
R.create = function () { const m = create.apply(this, arguments); if (m && m.mode === 'versus' && cur?.rule.id === 'ronde') m.toWin = 1; return m; };
|
||||||
|
R.tick = function (m) {
|
||||||
|
const ev = tick.apply(this, arguments);
|
||||||
|
// Satu ronde: pemenang ronde pertama menang pertandingan (seri tetap mengulang ronde). Ditetapkan sebelum game.js membaca match.winner.
|
||||||
|
if (m?.toWin === 1 && !m.winner) for (const e of ev) if (e.type === 'round-end' && (e.winner === 'player' || e.winner === 'enemy')) m.winner = e.winner;
|
||||||
|
return ev;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function wrapVS() {
|
||||||
|
const V = window.VS; if (!V || V.__daily || typeof V.show !== 'function') return;
|
||||||
|
V.__daily = true; const show = V.show;
|
||||||
|
V.show = function (info, onDone) {
|
||||||
|
const c = cur && info && !info.shadow ? cur : null;
|
||||||
|
const ok = show.call(this, c ? { ...info, label: 'TANTANGAN HARIAN' } : info, onDone);
|
||||||
|
if (ok && c) try { decorateVS(c); } catch (e) { console.error(e); }
|
||||||
|
return ok;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function wrapMeta() {
|
||||||
|
const M = window.Meta; if (!M || M.__daily || typeof M.award !== 'function') return;
|
||||||
|
M.__daily = true; const award = M.award;
|
||||||
|
M.award = function (r) { const h = award.apply(this, arguments); try { finish(r); } catch (e) { console.error(e); } return h; };
|
||||||
|
}
|
||||||
|
// Pertandingan tantangan selesai: menang = hadiah (sekali per tanggal) yang masuk rincian koin layar hasil lewat Meta.lastAward.
|
||||||
|
function finish(r) {
|
||||||
|
const c = cur; if (!c || !r) return;
|
||||||
|
r.daily = { id: c.rule.id, date: c.date };
|
||||||
|
if (r.winner !== 'player') return;
|
||||||
|
const got = claim(c.date); if (!got) return;
|
||||||
|
const aw = window.Meta.lastAward;
|
||||||
|
if (aw && Array.isArray(aw.lines)) { aw.lines.push({ key: 'daily', label: 'Tantangan harian', amount: got.coins, note: got.streak > 1 ? 'Beruntun ' + got.streak + ' hari' : c.rule.name }); aw.gain += got.coins; }
|
||||||
|
if (!(window.ResultView && window.FrontEnd?.showResult)) toast(`TANTANGAN HARIAN SELESAI +${got.coins} koin (beruntun ${got.streak} hari)`, 'coin');
|
||||||
|
}
|
||||||
|
// MAIN LAGI di layar hasil memanggil startMatch tanpa penanda; klik itu (fase tangkap, sebelum penangan tombol) menandai bahwa yang diulang adalah tantangan terakhir.
|
||||||
|
// startMatch berjalan di klik yang sama; penanda dicabut sebentar kemudian supaya rematch yang gagal tidak bocor ke pertandingan biasa berikutnya.
|
||||||
|
let againT = 0;
|
||||||
|
document.addEventListener('click', e => { if (redo && e.target.closest?.('[data-cmd="rematch"]')) { again = true; clearTimeout(againT); againT = setTimeout(() => { again = false; }, 600); } }, true);
|
||||||
|
|
||||||
|
// ---- penanda aturan di arena, baris aturan di layar VS ----
|
||||||
|
let tag = null, nudgeT = 0;
|
||||||
|
function paintTag() {
|
||||||
|
const b = document.body; if (!b) return;
|
||||||
|
if (!cur) { tag?.remove(); tag = null; delete b.dataset.dailyRule; return; }
|
||||||
|
b.dataset.dailyRule = cur.rule.id;
|
||||||
|
const host = $('#game-shell'); if (!host) return;
|
||||||
|
if (!tag?.isConnected) { tag = document.createElement('div'); tag.id = 'daily-tag'; tag.setAttribute('aria-hidden', 'true'); host.append(tag); }
|
||||||
|
tag.textContent = '◆ HARIAN · ' + cur.rule.name;
|
||||||
|
}
|
||||||
|
function nudge() { if (!tag) return; tag.classList.add('hit'); clearTimeout(nudgeT); nudgeT = setTimeout(() => tag?.classList.remove('hit'), 450); }
|
||||||
|
function decorateVS(c) {
|
||||||
|
const el = $('#vs-screen'); if (!el) return;
|
||||||
|
el.classList.add('has-daily');
|
||||||
|
const d = document.createElement('div'); d.className = 'vs-daily';
|
||||||
|
d.innerHTML = `<b>ATURAN HARI INI</b><strong>${esc(c.rule.name)}</strong><span>${esc(c.rule.vs)}</span>`;
|
||||||
|
el.append(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- tombol di menu utama dan kartu tantangan ----
|
||||||
|
const root = $('#front-menu');
|
||||||
|
let dlg = null, pick = null, started = null, startT = 0;
|
||||||
|
// Petarung pemain: pilihan di kartu, lalu petarung terakhir yang dimainkan di sesi ini, lalu pilihan tersimpan; tidak boleh sama dengan lawan hari ini.
|
||||||
|
function chosen(ch, want) {
|
||||||
|
const ids = roster(), ok = id => id && ids.includes(id) && id !== ch.foe;
|
||||||
|
if (ok(want)) return want; if (ok(pick)) return pick;
|
||||||
|
let snap = null; try { snap = window.__game?.snapshot?.().selectedCharacter; } catch (_) { /* tanpa snapshot */ }
|
||||||
|
if (ok(snap)) return snap; const s = view().pick; if (ok(s)) return s;
|
||||||
|
return ids.find(id => id !== ch.foe) || ids[0];
|
||||||
|
}
|
||||||
|
function go(who) {
|
||||||
|
const ch = challenge(), G = window.__game; if (!ch || !G?.startMatch) return false;
|
||||||
|
const me = chosen(ch, who); pick = me;
|
||||||
|
const d = store(); if (d) d.pick = me;
|
||||||
|
try { window.Meta?.cancelRun?.(); } catch (_) { /* tanpa rangkaian aktif */ }
|
||||||
|
let ok = false;
|
||||||
|
try { ok = G.startMatch({ player: me, enemy: ch.foe, stage: ch.stage, level: ch.level, mode: 'versus', daily: ch.date, vs: { label: 'TANTANGAN HARIAN' } }); } catch (e) { console.error(e); }
|
||||||
|
if (!ok) { toast('Aset belum siap. Tunggu sebentar lalu coba lagi.'); return false; }
|
||||||
|
persist(); closeCard(); $('#arena')?.focus({ preventScroll: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function cardHtml(ch, st, who) {
|
||||||
|
const R = ch.rule, ids = roster(), stage = window.MatchRules?.stages?.[ch.stage]?.name || ch.stage;
|
||||||
|
const fact = (k, v, small) => `<div class="dl-f"><dt>${k}</dt><dd>${v}${small ? `<small>${small}</small>` : ''}</dd></div>`;
|
||||||
|
const chips = ids.map(id => `<button type="button" class="dl-chip" data-pick="${esc(id)}" aria-pressed="${id === who}"${id === ch.foe ? ' disabled title="Lawan hari ini"' : ''}><img src="${esc(portrait(id))}" alt=""><span>${esc(fname(id))}</span></button>`).join('');
|
||||||
|
return `<div class="dialog-kicker">KONOHA FIGHTING / TANTANGAN HARIAN</div>`
|
||||||
|
+ `<div class="dl-head"><h2 id="daily-title">${esc(R.name)}</h2><span class="dl-state${st.done ? ' ok' : ''}">${st.done ? 'SELESAI ✓' : 'BELUM SELESAI'}</span></div>`
|
||||||
|
+ `<p class="dl-desc">${esc(R.desc)}</p>`
|
||||||
|
+ `<dl class="dl-facts">`
|
||||||
|
+ fact('LAWAN', `<img src="${esc(portrait(ch.foe))}" alt="">${esc(fname(ch.foe))}`)
|
||||||
|
+ fact('ARENA', esc(stage)) + fact('TINGKAT', LEVEL[ch.level] || '')
|
||||||
|
+ fact('HADIAH', st.done ? 'SUDAH DIAMBIL' : `◆ ${st.reward}`, st.done ? `besok ◆ ${st.reward}` : st.streak ? `hari ke-${st.streak + 1} beruntun` : 'hari pertama')
|
||||||
|
+ fact('STREAK', st.streak ? `${st.streak} HARI` : '—', st.streak ? `terbaik ${st.best}${st.done ? '' : ', selesaikan hari ini agar lanjut'}` : 'belum ada')
|
||||||
|
+ `</dl><div class="dl-pick"><span>PILIH PETARUNGMU</span><div class="dl-chips" role="group" aria-label="Pilih petarung">${chips}</div></div>`
|
||||||
|
+ `<div class="dialog-buttons"><button type="button" class="dl-go" data-go>${st.done ? 'MAIN LAGI (TANPA HADIAH)' : 'MULAI TANTANGAN'}</button><button type="button" data-close>TUTUP</button></div>`
|
||||||
|
+ `<small class="dl-note">${esc(dateText(ch.date))}${override ? ' · TANGGAL UJI' : ''} · aturan baru tiap hari · hadiah sekali sehari</small>`;
|
||||||
|
}
|
||||||
|
function paintCard() {
|
||||||
|
const ch = challenge(); if (!ch || !dlg) return;
|
||||||
|
pick = chosen(ch, pick);
|
||||||
|
dlg.innerHTML = cardHtml(ch, status(ch.date), pick);
|
||||||
|
}
|
||||||
|
function onCardClick(e) {
|
||||||
|
if (e.target === dlg) { const r = dlg.getBoundingClientRect(); if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) closeCard(); return; } // klik di luar kartu
|
||||||
|
const b = e.target.closest('button'); if (!b) return;
|
||||||
|
if (b.dataset.pick) { pick = b.dataset.pick; paintCard(); [...dlg.querySelectorAll('[data-pick]')].find(x => x.dataset.pick === pick)?.focus(); }
|
||||||
|
else if (b.hasAttribute('data-go')) go();
|
||||||
|
else if (b.hasAttribute('data-close')) closeCard();
|
||||||
|
}
|
||||||
|
// game.js menelan Esc (preventDefault) sehingga penutupan bawaan dialog tidak jalan. Pendengar fase tangkap di window (dipasang sekali, hanya bekerja selama kartu terbuka)
|
||||||
|
// menangani Esc dan panah kiri/kanan (pindah antarpilihan petarung) dan tidak meneruskannya ke game.js, bahkan bila fokus sedang tidak di dalam kartu.
|
||||||
|
function onCardKey(e) {
|
||||||
|
if (!dlg?.open) return;
|
||||||
|
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeCard(); return; }
|
||||||
|
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
||||||
|
const chips = [...dlg.querySelectorAll('[data-pick]:not(:disabled)')]; if (!chips.length) return;
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
const i = chips.indexOf(document.activeElement);
|
||||||
|
chips[i < 0 ? 0 : (i + (e.key === 'ArrowLeft' ? -1 : 1) + chips.length) % chips.length].focus();
|
||||||
|
}
|
||||||
|
function openCard() {
|
||||||
|
if (!challenge()) { toast('Tantangan belum siap. Tunggu sebentar lalu coba lagi.'); return false; }
|
||||||
|
if (!dlg) {
|
||||||
|
dlg = document.createElement('dialog'); dlg.id = 'daily-dialog'; dlg.className = 'daily-dialog'; dlg.setAttribute('aria-labelledby', 'daily-title');
|
||||||
|
dlg.addEventListener('click', onCardClick);
|
||||||
|
dlg.addEventListener('close', () => { if (root && !root.hidden) root.querySelector('.daily-entry')?.focus({ preventScroll: true }); });
|
||||||
|
document.body.append(dlg);
|
||||||
|
}
|
||||||
|
paintCard();
|
||||||
|
if (!dlg.open) dlg.showModal();
|
||||||
|
dlg.querySelector('[data-go]')?.focus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function closeCard() { if (dlg?.open) dlg.close(); }
|
||||||
|
function decorate() {
|
||||||
|
const slot = root.querySelector('.home-top-right') || root.querySelector('.home-top'), ch = challenge(); if (!slot || !ch) return;
|
||||||
|
const st = status(ch.date); let b = slot.querySelector('.daily-entry');
|
||||||
|
if (!b) {
|
||||||
|
b = document.createElement('button'); b.type = 'button'; b.className = 'daily-entry';
|
||||||
|
b.addEventListener('click', () => { try { window.__game?.menuSound?.('confirm'); } catch (_) { /* tanpa suara */ } openCard(); });
|
||||||
|
slot.prepend(b);
|
||||||
|
}
|
||||||
|
b.classList.toggle('done', st.done); b.title = override ? 'Tanggal uji: ' + override : '';
|
||||||
|
b.setAttribute('aria-label', `Tantangan harian: ${ch.rule.name}. ${st.done ? 'Sudah selesai.' : 'Belum selesai.'}${st.streak ? ' Beruntun ' + st.streak + ' hari.' : ''} Buka kartu tantangan.`);
|
||||||
|
b.innerHTML = `<i aria-hidden="true">◆</i><span class="de-t">HARIAN</span><span class="de-n">${esc(ch.rule.name)}</span>${st.done ? '<b class="de-ok">✓</b>' : '<b>BARU</b>'}${st.streak > 1 ? `<em>${st.streak} HARI</em>` : ''}`;
|
||||||
|
}
|
||||||
|
// Hadiah hari pertama dibayar sedikit sesudah menu utama tampil, dan hanya bila menu utama masih tampil saat itu (pertandingan yang langsung dimulai
|
||||||
|
// tidak diganggu; hadiah menunggu menu utama berikutnya). Sekali per tanggal per muatan halaman; sisanya dijaga simpanan.
|
||||||
|
function maybeStartup() {
|
||||||
|
if (started === today() || startT) return;
|
||||||
|
startT = setTimeout(() => {
|
||||||
|
startT = 0;
|
||||||
|
try { if (root.hidden || root.dataset.screen !== 'home') return; started = today(); startup(started); } catch (e) { console.error(e); }
|
||||||
|
}, 700);
|
||||||
|
}
|
||||||
|
function sync() { // dipanggil tiap menu utama digambar ulang (menu.js mengganti isi #front-menu) dan saat tab kembali terlihat
|
||||||
|
try { if (!root || root.hidden || root.dataset.screen !== 'home') return; decorate(); maybeStartup(); } catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- uji manual ----
|
||||||
|
function setDate(iso) {
|
||||||
|
if (iso == null || iso === '') override = null; else if (dayNum(iso) != null) override = String(iso); else return false;
|
||||||
|
started = null; sync(); startup(today());
|
||||||
|
return today();
|
||||||
|
}
|
||||||
|
function resetState() { const s = window.Meta?.save; if (!s) return false; delete s.daily; persist(); started = null; sync(); return true; }
|
||||||
|
function table(n = 14, from = today()) {
|
||||||
|
const dn = dayNum(from); if (dn == null) return [];
|
||||||
|
return Array.from({ length: Math.max(1, Math.min(120, n | 0)) }, (_, i) => { const c = challenge(isoOf(dn + i)); return c && { date: c.date, rule: c.rule.id, foe: c.foe, stage: c.stage, level: c.level }; });
|
||||||
|
}
|
||||||
|
try { const q = new URLSearchParams(location.search).get('daily'); if (q && dayNum(q) != null) override = q; } catch (_) { /* tanpa parameter */ }
|
||||||
|
wrapRules(); wrapVS(); wrapMeta();
|
||||||
|
window.Daily = {
|
||||||
|
rules: RULES, today, challenge, status, setDate, resetState, table, startup, open: openCard, close: closeCard, start: go,
|
||||||
|
arm, deny, hit, parry, cooldowns, slip,
|
||||||
|
get foeScale() { return cur?.rule.id === 'raksasa' ? SKALA_RAKSASA : 1; },
|
||||||
|
get cur() { return cur; }, get active() { return !!cur; }, get override() { return override; }
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onCardKey, true);
|
||||||
|
if (root) { new MutationObserver(sync).observe(root, { childList: true }); sync(); }
|
||||||
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) sync(); });
|
||||||
|
})();
|
||||||
|
|
@ -33,6 +33,7 @@ Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks un
|
||||||
- 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()`.
|
- 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()`.
|
||||||
|
- Tantangan harian dan hadiah harian (RENCANA 7.1 dan 7.2): `daily.js` (`window.Daily`) + `daily.css`, tag `<script>` paling akhir di `index.html` (sesudah `meta.js`, karena membungkus `Meta.award`) dan `<link>` CSS di `<head>`. Tanggal lokal -> hash -> satu tantangan yang sama untuk semua pemain: aturan (10 entri `RULES`: Darah satu, Hanya ultimate, Tanpa blok, Lawan cepat, Lantai licin, Tanpa lompat, Kombo wajib, Satu ronde, Counter saja, Raksasa), lawan, arena, dan tingkat (tiap aturan punya daftar `levels`; aturan yang menyulitkan hanya memakai easy/medium). Tombol kecil HARIAN dipasang `MutationObserver` pada `#front-menu` di layar `home` (item flex di `.home-top-right`, tepat di kiri teks versi, dengan margin atas dan bawah negatif setinggi setengah tombol sehingga kotak margin-nya 0 dan tinggi menu tidak berubah; sengaja bukan `position:absolute` dan `.home-top-right` tidak dijadikan induk posisi karena tombol layar penuh memakai `.home-top` sebagai induknya; tanpa kait di menu.js) dan membuka kartu `<dialog id="daily-dialog">` (Esc dan panah ditangani pendengar `keydown` fase tangkap di window karena game.js menelan Esc): aturan, deskripsi, lawan, arena, tingkat, hadiah, streak, pilihan petarung (lawan hari ini tidak bisa dipilih). Mulai = `__game.startMatch({..., mode:'versus', daily:'YYYY-MM-DD', vs:{label}})`; pertandingan versus biasa. `Daily.arm(options.daily, {sideMods, balance, versus})` dipanggil di SETIAP startMatch (setelah `setShadowFoe`), jadi pertandingan biasa otomatis mematikan aturan dan mengembalikan `BALANCE.heroMax`. MAIN LAGI di layar hasil mengulang tantangan yang sama (klik pada `[data-cmd="rematch"]` ditangkap fase capture dan menandai `again`). Menang = 150 koin + 25 per hari beruntun (maks +150) lewat `Meta.addCoins`, sekali per tanggal; pembungkus `Meta.award` menambah satu baris ke `Meta.lastAward.lines` (muncul di rincian koin layar hasil) dan menaikkan `gain`. Simpanan `Meta.save.daily = {last, streak, best, total, greet, fest, pick}` (ikut kode simpanan dan HAPUS SEMUA; simpanan lama tanpa kunci terbaca kosong; menyimpan = `Meta.addCoins`). 7.2: hadiah hari pertama +25 koin sekali per tanggal saat menu utama pertama tampil (toast), kejutan tanggal sekali per tahun (`FEST`: 1 Jan, 14 Mar Hari Pi, 8 Agu Hari Kucing, 17 Agu Dirgahayu 100 koin). Kait di game.js, semuanya `window.Daily?.xxx` dan no-op tanpa tantangan aktif: `arm` (startMatch), `deny(jenis)` (jump, startAttack, cast, startCommand, startThrow, useShopMove, `press('l')`), `cooldowns` dan `slip` (step), `slip` (updateDummy), `hit` (hitDummy, sesudah bonus counter), `parry` (heroGuard), `foeScale` (drawDummy). Aturan yang cukup dengan angka memakai `sideMods` dan `BALANCE` yang diberikan ke `arm` (HP Darah satu dan Raksasa, `walk`/`cd` lawan, `parry` lawan 0). HP pemain kini lewat `heroHpShown()` (skala 200 seperti `dummyHpShown`) untuk bar HP dan penentu waktu habis. Satu ronde: `MatchRules.create` dan `tick` dibungkus di daily.js (`match.toWin = 1`, pemenang ronde pertama ditetapkan sebelum game.js membaca `match.winner`); `updateMatchHud` membaca `match.toWin`. Layar VS: pembungkus `VS.show` menambah baris `.vs-daily` (label VS jadi TANTANGAN HARIAN); arena: `#daily-tag` di tepi atas tengah (berkedip saat aksi yang dilarang ditekan) dan `body[data-daily-rule]`. Tanggal tidak dipakai di dalam simulasi (hanya memilih aturan dan hadiah). Uji manual: `?daily=YYYY-MM-DD` mengganti tanggal untuk seluruh modul (aturan, hadiah, streak); konsol `Daily.setDate('2026-08-17')` / `Daily.setDate(null)`, `Daily.table(14)`, `Daily.status()`, `Daily.startup()`, `Daily.resetState()`, `Daily.open()`, `Daily.start('megachan')`. Belum ada skenario otomatis; angka aturan (`COUNTER_*`, `LICIN`, `LAWAN_*`, `HP_*`) belum disetel dengan permainan nyata. `precache.js`/`files.json` perlu dibangun ulang oleh integrator agar `daily.js` dan `daily.css` ikut tersimpan offline.
|
||||||
- `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.
|
||||||
- Bahaya arena (RENCANA 6.2): `hazard.js` (`window.Hazard`) + `hazard.css`, dimuat tepat sesudah `game.js`. Satu kejutan per arena (tabel `DEFS`; arena tanpa entri tidak punya bahaya): bellora = genting jatuh (6 damage), sunspire = silau matahari (layar memutih, tanpa damage; CPU ragu 0,55 dtk), harbor = ombak dari kiri yang mendorong ke kanan sampai batas bertanda (3 damage, lompat atau berdiri di kanan garis batas untuk menghindar), elderwood = dua akar menyembul (5 damage), moonrise = petir arkana (7 damage). Tiap bahaya diberi aba-aba 1 detik (cincin/garis di lantai, kilau di langit, suara `sound()`, dan teks "Awas: ..." di baris kecil `.hz-tag` dalam `.stage-sign`; saat tenang barisnya "Bahaya: <nama>"). Aturan: hanya `match.mode === 'versus'` fase fight (latihan dan tutorial tidak); bahaya pertama 6-12 dtk sesudah ronde mulai, berikutnya 9-14 dtk sesudah aba-aba sebelumnya; tidak mulai di 4 dtk pertama, saat ultimate/sinematik (`hazardCtx.busy`), saat salah satu petarung jatuh, atau di 3,5 dtk terakhir ronde. Damage dikali pasif `dmgIn` sisi yang kena, tidak pernah mematikan (menyisakan 1 HP), tanpa luka/knockback, tidak menambah kombo, dan melewati petarung yang sedang kebal (`invuln`); zona = jarak datar ke pusat tanda (sama dengan cincin yang tergambar) dan tinggi kaki di atas lantai (70, petir 100), jadi lompat atau geser menghindar. Pemain, CPU, dan P2 kena aturan yang sama; CPU mencoba keluar dari zona lewat rencana `sidestep` bawaan (peluang = `evade` tingkat, jeda = `reaction`; hanya bahaya berzona, bukan ombak). Deterministik: pengacak LCG sendiri berbenih (benih mesin saat ronde mulai + nomor ronde + arena) dan waktu = `dt` simulasi; pengacak kosmetik terpisah, dan "Kurangi gerakan" (`Access.motion`) hanya mengubah efek (tanpa getar dan kilat putih, silau jadi semburat redup, serpihan sedikit), bukan jalannya pertandingan. Kait di game.js: `Hazard.step(dt)` di akhir `step()` (sebelum `feelWatchKo`), `Hazard.draw(ctx, 'back'|'front')` di `draw()` (tanda lantai sebelum petarung, efek sebelum `drawSquadron`/`drawEffects`), dan `__game.hazardCtx` (getter `match`, `stage`, `time`, `seed`, `menu`, `busy`, `cpu`; `sound`; `cpuRead()`; `cpuDodge(dir, dur)`). Semburat layar memakai elemen DOM `#hz-glare` (di atas kanvas, di bawah HUD). Dimatikan lewat Pengaturan ("Bahaya arena", bawaan Aktif, `localStorage['konoha.hazard']`) atau `Hazard.enabled = false` (sementara, tidak disimpan; untuk tantangan khusus); keduanya harus menyala. Uji manual: `?hazard=cepat` di URL (bahaya tiap 3-4,5 dtk), `Hazard.trigger()` di konsol saat versus berjalan, `Hazard.state()`, `Hazard.onEvent(fn)` (jenis `aba-aba` dan `kena`). Catatan: statistik pertandingan menghitung damage dari selisih HP, jadi damage bahaya ikut tercatat sebagai diterima pemain atau dikeluarkan pemain; simulator keseimbangan (`tests/bot.mjs`, arena Bellora) ikut memainkan bahaya, set `Hazard.enabled = false` di sana bila ingin angka tanpa bahaya. Belum ada skenario otomatis dan belum dilihat di peramban oleh penulisnya (hanya `node --check` dan smoke).
|
- Bahaya arena (RENCANA 6.2): `hazard.js` (`window.Hazard`) + `hazard.css`, dimuat tepat sesudah `game.js`. Satu kejutan per arena (tabel `DEFS`; arena tanpa entri tidak punya bahaya): bellora = genting jatuh (6 damage), sunspire = silau matahari (layar memutih, tanpa damage; CPU ragu 0,55 dtk), harbor = ombak dari kiri yang mendorong ke kanan sampai batas bertanda (3 damage, lompat atau berdiri di kanan garis batas untuk menghindar), elderwood = dua akar menyembul (5 damage), moonrise = petir arkana (7 damage). Tiap bahaya diberi aba-aba 1 detik (cincin/garis di lantai, kilau di langit, suara `sound()`, dan teks "Awas: ..." di baris kecil `.hz-tag` dalam `.stage-sign`; saat tenang barisnya "Bahaya: <nama>"). Aturan: hanya `match.mode === 'versus'` fase fight (latihan dan tutorial tidak); bahaya pertama 6-12 dtk sesudah ronde mulai, berikutnya 9-14 dtk sesudah aba-aba sebelumnya; tidak mulai di 4 dtk pertama, saat ultimate/sinematik (`hazardCtx.busy`), saat salah satu petarung jatuh, atau di 3,5 dtk terakhir ronde. Damage dikali pasif `dmgIn` sisi yang kena, tidak pernah mematikan (menyisakan 1 HP), tanpa luka/knockback, tidak menambah kombo, dan melewati petarung yang sedang kebal (`invuln`); zona = jarak datar ke pusat tanda (sama dengan cincin yang tergambar) dan tinggi kaki di atas lantai (70, petir 100), jadi lompat atau geser menghindar. Pemain, CPU, dan P2 kena aturan yang sama; CPU mencoba keluar dari zona lewat rencana `sidestep` bawaan (peluang = `evade` tingkat, jeda = `reaction`; hanya bahaya berzona, bukan ombak). Deterministik: pengacak LCG sendiri berbenih (benih mesin saat ronde mulai + nomor ronde + arena) dan waktu = `dt` simulasi; pengacak kosmetik terpisah, dan "Kurangi gerakan" (`Access.motion`) hanya mengubah efek (tanpa getar dan kilat putih, silau jadi semburat redup, serpihan sedikit), bukan jalannya pertandingan. Kait di game.js: `Hazard.step(dt)` di akhir `step()` (sebelum `feelWatchKo`), `Hazard.draw(ctx, 'back'|'front')` di `draw()` (tanda lantai sebelum petarung, efek sebelum `drawSquadron`/`drawEffects`), dan `__game.hazardCtx` (getter `match`, `stage`, `time`, `seed`, `menu`, `busy`, `cpu`; `sound`; `cpuRead()`; `cpuDodge(dir, dur)`). Semburat layar memakai elemen DOM `#hz-glare` (di atas kanvas, di bawah HUD). Dimatikan lewat Pengaturan ("Bahaya arena", bawaan Aktif, `localStorage['konoha.hazard']`) atau `Hazard.enabled = false` (sementara, tidak disimpan; untuk tantangan khusus); keduanya harus menyala. Uji manual: `?hazard=cepat` di URL (bahaya tiap 3-4,5 dtk), `Hazard.trigger()` di konsol saat versus berjalan, `Hazard.state()`, `Hazard.onEvent(fn)` (jenis `aba-aba` dan `kena`). Catatan: statistik pertandingan menghitung damage dari selisih HP, jadi damage bahaya ikut tercatat sebagai diterima pemain atau dikeluarkan pemain; simulator keseimbangan (`tests/bot.mjs`, arena Bellora) ikut memainkan bahaya, set `Hazard.enabled = false` di sana bila ingin angka tanpa bahaya. Belum ada skenario otomatis dan belum dilihat di peramban oleh penulisnya (hanya `node --check` dan smoke).
|
||||||
- `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.
|
||||||
|
|
|
||||||
39
game.js
39
game.js
|
|
@ -243,6 +243,7 @@
|
||||||
window.Challenge?.begin?.(options); // kode tantangan (challenge.js): aturan khusus (options.codeRule) dan statistik kebiasaan; harus sebelum refreshPassives
|
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');
|
||||||
|
window.Daily?.arm?.(options.daily,{sideMods,balance:BALANCE,versus:mode==='versus'&&!options.shadow}); // tantangan harian (daily.js): aturan, HP, dan pengubah sisi; tanpa options.daily aturan dimatikan
|
||||||
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();
|
||||||
window.MatchStats?.reset(mode==='versus');
|
window.MatchStats?.reset(mode==='versus');
|
||||||
$('#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');
|
||||||
|
|
@ -270,6 +271,7 @@
|
||||||
function setShadowFoe(on) { shadowFoe = !!on; BALANCE.dummyMax = shadowFoe ? Math.round(DUMMY_MAX * SHADOW.hp) : DUMMY_MAX; hudIdentity = ''; }
|
function setShadowFoe(on) { shadowFoe = !!on; BALANCE.dummyMax = shadowFoe ? Math.round(DUMMY_MAX * SHADOW.hp) : DUMMY_MAX; hudIdentity = ''; }
|
||||||
const foeDamage = n => (shadowFoe && n > 0 ? Math.max(1, Math.round(n * SHADOW.damage)) : n);
|
const foeDamage = n => (shadowFoe && n > 0 ? Math.max(1, Math.round(n * SHADOW.damage)) : n);
|
||||||
const dummyHpShown = () => currentDummyHP() * DUMMY_MAX / BALANCE.dummyMax;
|
const dummyHpShown = () => currentDummyHP() * DUMMY_MAX / BALANCE.dummyMax;
|
||||||
|
const heroHpShown = () => hero.hp * DUMMY_MAX / BALANCE.heroMax; // skala 200 seperti dummyHpShown (heroMax berbeda hanya di tantangan harian Darah satu)
|
||||||
function drawShadowAura() {
|
function drawShadowAura() {
|
||||||
const x = dummy.x, y = dummy.y - 88, k = .5 + .5 * Math.sin(realTime * 3.4), g = ctx.createRadialGradient(x, y, 10, x, y, 150);
|
const x = dummy.x, y = dummy.y - 88, k = .5 + .5 * Math.sin(realTime * 3.4), g = ctx.createRadialGradient(x, y, 10, x, y, 150);
|
||||||
g.addColorStop(0, `rgba(${SHADOW.aura},${(.2 + .08 * k).toFixed(3)})`); g.addColorStop(1, `rgba(${SHADOW.aura},0)`);
|
g.addColorStop(0, `rgba(${SHADOW.aura},${(.2 + .08 * k).toFixed(3)})`); g.addColorStop(1, `rgba(${SHADOW.aura},0)`);
|
||||||
|
|
@ -293,7 +295,7 @@
|
||||||
function advanceMatch(dt) {
|
function advanceMatch(dt) {
|
||||||
if(!Rules||!match||match.mode!=='versus'||vsHold)return;
|
if(!Rules||!match||match.mode!=='versus'||vsHold)return;
|
||||||
window.MatchStats?.tick(dt,match,hero.hp,dummy.damage,BALANCE.dummyMax);
|
window.MatchStats?.tick(dt,match,hero.hp,dummy.damage,BALANCE.dummyMax);
|
||||||
for(const event of Rules.tick(match,dt,hero.hp,dummyHpShown())) {
|
for(const event of Rules.tick(match,dt,heroHpShown(),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)
|
window.Challenge?.roundEnd?.(match,event); // aturan "satu ronde": menetapkan match.winner sebelum efek K.O. akhir (challenge.js)
|
||||||
|
|
@ -321,21 +323,22 @@
|
||||||
function updateMatchHud() {
|
function updateMatchHud() {
|
||||||
if(!Rules)return;
|
if(!Rules)return;
|
||||||
const versus=match?.mode==='versus',s=Rules.stages[stageId];
|
const versus=match?.mode==='versus',s=Rules.stages[stageId];
|
||||||
$('#mode-label').textContent=versus?`ROUND ${match.round} / 3`:'FREE TRAINING';$('#match-clock').textContent=versus?String(Math.ceil(match.seconds)).padStart(2,'0'):'∞';
|
const toWin=match?.toWin||2; // 1 = satu ronde (tantangan harian Satu ronde, match.toWin diisi daily.js)
|
||||||
$('.match-center').setAttribute('aria-label',versus?'Pertandingan, menang dua ronde dari maksimal tiga':'Mode latihan tanpa batas waktu');
|
$('#mode-label').textContent=versus?(toWin===1?'SATU RONDE':`ROUND ${match.round} / 3`):'FREE TRAINING';$('#match-clock').textContent=versus?String(Math.ceil(match.seconds)).padStart(2,'0'):'∞';
|
||||||
|
$('.match-center').setAttribute('aria-label',versus?(toWin===1?'Pertandingan satu ronde, pemenang ronde menang':'Pertandingan, menang dua ronde dari maksimal tiga'):'Mode latihan tanpa batas waktu');
|
||||||
$('#match-score').textContent=versus?`${match.playerWins} — ${match.enemyWins}`:'VS';$('#stage-name').textContent=s.short;$('#stage-subtitle').textContent=s.subtitle;
|
$('#match-score').textContent=versus?`${match.playerWins} — ${match.enemyWins}`:'VS';$('#stage-name').textContent=s.short;$('#stage-subtitle').textContent=s.subtitle;
|
||||||
$('#round-wins-player').textContent=versus?'◆'.repeat(match.playerWins)+'◇'.repeat(2-match.playerWins):'';$('#round-wins-enemy').textContent=versus?'◆'.repeat(match.enemyWins)+'◇'.repeat(2-match.enemyWins):'';
|
$('#round-wins-player').textContent=versus?'◆'.repeat(match.playerWins)+'◇'.repeat(Math.max(0,toWin-match.playerWins)):'';$('#round-wins-enemy').textContent=versus?'◆'.repeat(match.enemyWins)+'◇'.repeat(Math.max(0,toWin-match.enemyWins)):'';
|
||||||
const card=$('#round-call'),show=!menuOpen&&versus&&!vsHold&&['intro','ko'].includes(match.phase);
|
const card=$('#round-call'),show=!menuOpen&&versus&&!vsHold&&['intro','ko'].includes(match.phase);
|
||||||
card.hidden=!show;card.dataset.phase=match?.phase||'fight';
|
card.hidden=!show;card.dataset.phase=match?.phase||'fight';
|
||||||
$('#round-call-title').textContent=!show?'':match.phase==='intro'?(match.fightCue?'FIGHT':`ROUND ${match.round}`):match.endText||'K.O.';
|
$('#round-call-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'):toWin===1?'SATU RONDE, SATU PEMENANG':'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)
|
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; }
|
||||||
function jump(second = false) {
|
function jump(second = false) {
|
||||||
if (!canAct()) return false;
|
if (!canAct() || window.Daily?.deny?.('jump')) return false; // tantangan harian Tanpa lompat (daily.js)
|
||||||
// A basic attack never locks the player: after its hit a jump cancels the recovery; pressed during the wind-up it is
|
// A basic attack never locks the player: after its hit a jump cancels the recovery; pressed during the wind-up it is
|
||||||
// kept and happens the moment the hit lands (the swing still connects).
|
// kept and happens the moment the hit lands (the swing still connects).
|
||||||
if (hero.action?.type === 'attack') { if (!hero.action.fired) { hero.action.cancelInto = 'jump'; hero.action.queued = 0; return true; } hero.action = null; }
|
if (hero.action?.type === 'attack') { if (!hero.action.fired) { hero.action.cancelInto = 'jump'; hero.action.queued = 0; return true; } hero.action = null; }
|
||||||
|
|
@ -352,7 +355,7 @@
|
||||||
hero.jumps = 1; hero.vy = -CONFIG.jumpSpeed; hero.grounded = false; hero.airTime = 0; state('jump'); dust(hero.x, 12); sound('jump'); return true;
|
hero.jumps = 1; hero.vy = -CONFIG.jumpSpeed; hero.grounded = false; hero.airTime = 0; state('jump'); dust(hero.x, 12); sound('jump'); return true;
|
||||||
}
|
}
|
||||||
function startAttack(index = 1) {
|
function startAttack(index = 1) {
|
||||||
if (!canAct()) return false;
|
if (!canAct() || window.Daily?.deny?.('attack')) return false; // Hanya ultimate (daily.js)
|
||||||
if (hero.doublePose > 0) { hero.rollBuffer = 'attack'; return true; }
|
if (hero.doublePose > 0) { hero.rollBuffer = 'attack'; return true; }
|
||||||
if (hero.action) { if (hero.action.type === 'attack' && hero.action.index < 3) { hero.action.queued = Math.min(3 - hero.action.index, hero.action.queued + 1); hero.action.cancelInto = null; } return false; }
|
if (hero.action) { if (hero.action.type === 'attack' && hero.action.index < 3) { hero.action.queued = Math.min(3 - hero.action.index, hero.action.queued + 1); hero.action.cancelInto = null; } return false; }
|
||||||
const name = `attack${index}`, duration = selectedCharacter === 'fenr' ? F.move(hero,'attack',index).duration : KITS[selectedCharacter] ? KITS[selectedCharacter].move('attack',index).duration : animationDuration(name, [.39, .43, .57][index - 1]);
|
const name = `attack${index}`, duration = selectedCharacter === 'fenr' ? F.move(hero,'attack',index).duration : KITS[selectedCharacter] ? KITS[selectedCharacter].move('attack',index).duration : animationDuration(name, [.39, .43, .57][index - 1]);
|
||||||
|
|
@ -363,6 +366,7 @@
|
||||||
// Skills cancel a basic attack at any point, so the kit never waits for the combo to finish. A skill that cannot start
|
// Skills cancel a basic attack at any point, so the kit never waits for the combo to finish. A skill that cannot start
|
||||||
// (cooldown, summon still out) leaves the attack untouched.
|
// (cooldown, summon still out) leaves the attack untouched.
|
||||||
function cast(name) {
|
function cast(name) {
|
||||||
|
if (window.Daily?.deny?.(name)) return false; // Hanya ultimate (daily.js): skill1 dan skill2 dilarang
|
||||||
const basic = hero.action?.type === 'attack' ? hero.action : null;
|
const basic = hero.action?.type === 'attack' ? hero.action : null;
|
||||||
if (basic && canAct() && (hero.cooldowns[name] || 0) <= 0) hero.action = null;
|
if (basic && canAct() && (hero.cooldowns[name] || 0) <= 0) hero.action = null;
|
||||||
const ok = castMove(name);
|
const ok = castMove(name);
|
||||||
|
|
@ -403,6 +407,7 @@
|
||||||
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) || (key === 'l' && window.Challenge?.noBlock)) return; // aturan "tanpa blok" (challenge.js): L tidak berbuat apa-apa
|
if (!canAct() || keys.has(key) || (key === 'l' && window.Challenge?.noBlock)) return; // aturan "tanpa blok" (challenge.js): L tidak berbuat apa-apa
|
||||||
|
if (key === 'l' && window.Daily?.deny?.('guard')) return; // Tanpa blok (daily.js): blok dan parry tidak pernah terdaftar
|
||||||
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();
|
||||||
|
|
@ -446,7 +451,7 @@
|
||||||
lunge: { label: 'TINJU MAJU!', state: 'attack2', cd: 1.8, duration: .44, hitAt: .3, damage: 11, knockback: 300 }
|
lunge: { label: 'TINJU MAJU!', state: 'attack2', cd: 1.8, duration: .44, hitAt: .3, damage: 11, knockback: 300 }
|
||||||
};
|
};
|
||||||
function startCommand() {
|
function startCommand() {
|
||||||
const id = commandFor(); if (!id || !canAct() || hero.action || hero.doublePose > 0 || (hero.spCd || 0) > 0 || !hero.grounded) return false;
|
const id = commandFor(); if (!id || window.Daily?.deny?.('command') || !canAct() || hero.action || hero.doublePose > 0 || (hero.spCd || 0) > 0 || !hero.grounded) return false;
|
||||||
const c = COMMANDS[id]; inputLog.length = 0; hero.spCd = c.cd;
|
const c = COMMANDS[id]; inputLog.length = 0; hero.spCd = c.cd;
|
||||||
hero.action = { type: 'attack', name: c.state, index: 3, t: 0, duration: c.duration, hit: false, queued: 0, fired: false, hitAt: c.hitAt, special: id, color: '#ffe9a0' };
|
hero.action = { type: 'attack', name: c.state, index: 3, t: 0, duration: c.duration, hit: false, queued: 0, fired: false, hitAt: c.hitAt, special: id, color: '#ffe9a0' };
|
||||||
hero.runDirection = 0; state(c.state); announce(c.label, .6); sound('cast'); window.Meta?.shout?.(c.label.replace('!', '')); return true;
|
hero.runDirection = 0; state(c.state); announce(c.label, .6); sound('cast'); window.Meta?.shout?.(c.label.replace('!', '')); return true;
|
||||||
|
|
@ -460,7 +465,7 @@
|
||||||
};
|
};
|
||||||
function useShopMove() {
|
function useShopMove() {
|
||||||
const id = window.Meta?.equippedMove?.(), m = SHOP_MOVES[id];
|
const id = window.Meta?.equippedMove?.(), m = SHOP_MOVES[id];
|
||||||
if (!m || !canAct() || hero.action || hero.doublePose > 0 || (hero.shopCd || 0) > 0) return false;
|
if (!m || window.Daily?.deny?.('shop') || !canAct() || hero.action || hero.doublePose > 0 || (hero.shopCd || 0) > 0) return false;
|
||||||
hero.shopCd = window.Access?.noCooldown ? 0 : m.cd; announce(m.name, .7); sound('cast');
|
hero.shopCd = window.Access?.noCooldown ? 0 : m.cd; announce(m.name, .7); sound('cast');
|
||||||
if (m.instant) {
|
if (m.instant) {
|
||||||
if (id === 'perisai') { hero.invuln = Math.max(hero.invuln, 1.6); hero.shield = 1.6; }
|
if (id === 'perisai') { hero.invuln = Math.max(hero.invuln, 1.6); hero.shield = 1.6; }
|
||||||
|
|
@ -555,7 +560,7 @@
|
||||||
feelImpact('hero', 'parry', { dir: -hero.facing, x: hero.x + hero.facing * 40, y: hero.y - 100 }); damageNumbers.push({ x: hero.x, y: hero.y - 170, amount: 'PARRY', kind: 'parry', life: .8, maxLife: .8, color: Feel.table.colors.parry });
|
feelImpact('hero', 'parry', { dir: -hero.facing, x: hero.x + hero.facing * 40, y: hero.y - 100 }); damageNumbers.push({ x: hero.x, y: hero.y - 170, amount: 'PARRY', kind: 'parry', life: .8, maxLife: .8, color: Feel.table.colors.parry });
|
||||||
spawnParticles(hero.x + hero.facing * 40, hero.y - 100, '#cfe9ff', 22, 320);
|
spawnParticles(hero.x + hero.facing * 40, hero.y - 100, '#cfe9ff', 22, 320);
|
||||||
if (!['down', 'recover'].includes(dummy.state)) { dummy.action = null; dummy.state = 'hurt'; dummy.stateTime = -.3; dummy.vx = -hero.facing * 120; }
|
if (!['down', 'recover'].includes(dummy.state)) { dummy.action = null; dummy.state = 'hurt'; dummy.stateTime = -.3; dummy.vx = -hero.facing * 120; }
|
||||||
window.MatchStats?.parry();
|
window.MatchStats?.parry(); window.Daily?.parry?.(time); // Counter saja: parry membuka celah (daily.js)
|
||||||
window.Meta?.event?.('parry'); return true;
|
window.Meta?.event?.('parry'); return true;
|
||||||
}
|
}
|
||||||
const chip = Math.max(1, Math.round(damage * .2)); hero.hp = Math.max(1, hero.hp - chip); hero.guard += damage * 1.6; hero.vx = -hero.facing * 90;
|
const chip = Math.max(1, Math.round(damage * .2)); hero.hp = Math.max(1, hero.hp - chip); hero.guard += damage * 1.6; hero.vx = -hero.facing * 90;
|
||||||
|
|
@ -566,7 +571,7 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
function startThrow() {
|
function startThrow() {
|
||||||
if (!canAct() || hero.action || hero.doublePose > 0 || !hero.grounded || (hero.throwCd || 0) > 0) return false;
|
if (!canAct() || window.Daily?.deny?.('throw') || hero.action || hero.doublePose > 0 || !hero.grounded || (hero.throwCd || 0) > 0) return false;
|
||||||
hero.throwCd = 3; hero.action = { type: 'attack', name: 'attack2', index: 3, t: 0, duration: .5, hit: false, queued: 0, fired: false, hitAt: .3, special: 'throw', color: '#ffb36a' };
|
hero.throwCd = 3; hero.action = { type: 'attack', name: 'attack2', index: 3, t: 0, duration: .5, hit: false, queued: 0, fired: false, hitAt: .3, special: 'throw', color: '#ffb36a' };
|
||||||
hero.runDirection = 0; state('attack2'); announce('LEMPAR!', .5); window.Meta?.shout?.('Lempar!'); return true;
|
hero.runDirection = 0; state('attack2'); announce('LEMPAR!', .5); window.Meta?.shout?.('Lempar!'); return true;
|
||||||
}
|
}
|
||||||
|
|
@ -623,6 +628,7 @@
|
||||||
const counter = !!(dummy.action && !dummy.action.fired && dummy.action.type === 'attack');
|
const counter = !!(dummy.action && !dummy.action.fired && dummy.action.type === 'attack');
|
||||||
if (counter) { damage = Math.round(damage * 1.25); if (damage > 1) announce('COUNTER!', .5); }
|
if (counter) { damage = Math.round(damage * 1.25); if (damage > 1) announce('COUNTER!', .5); }
|
||||||
if (comboHits >= 5) damage = Math.max(1, Math.round(damage * Math.max(.55, 1 - .07 * (comboHits - 4))));
|
if (comboHits >= 5) damage = Math.max(1, Math.round(damage * Math.max(.55, 1 - .07 * (comboHits - 4))));
|
||||||
|
damage = window.Daily?.hit?.(damage, counter, comboHits, time) ?? damage; // tantangan harian Kombo wajib dan Counter saja (daily.js)
|
||||||
dummy.damage += damage; totalDamage += damage; comboHits++; bestCombo = Math.max(bestCombo, comboHits); comboDamage += damage; comboTimer = 2.3;
|
dummy.damage += damage; totalDamage += damage; comboHits++; bestCombo = Math.max(bestCombo, comboHits); comboDamage += damage; comboTimer = 2.3;
|
||||||
window.MatchStats?.hit(hitSource(), counter, comboHits, hero.action);
|
window.MatchStats?.hit(hitSource(), counter, comboHits, hero.action);
|
||||||
// Versus combo limit: after comboCap hits in one continuous stun the CPU gets its immunity (like the player's),
|
// Versus combo limit: after comboCap hits in one continuous stun the CPU gets its immunity (like the player's),
|
||||||
|
|
@ -1842,12 +1848,12 @@
|
||||||
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 * cdRate(hero, name)); window.Access?.capCooldowns?.(hero.cooldowns); passiveRegen(dt); // "Tanpa cooldown" (access.js)
|
for (const name of Object.keys(hero.cooldowns)) hero.cooldowns[name] = Math.max(0, hero.cooldowns[name] - dt * cdRate(hero, name)); window.Access?.capCooldowns?.(hero.cooldowns); window.Daily?.cooldowns?.(hero.cooldowns); passiveRegen(dt); // "Tanpa cooldown" (access.js); Hanya ultimate: ultimate selalu siap (daily.js)
|
||||||
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.autoGuard = Math.max(0, (hero.autoGuard || 0) - dt);
|
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.autoGuard = Math.max(0, (hero.autoGuard || 0) - dt);
|
||||||
hero.blocking = (keys.has('l') || hero.autoGuard > 0) && hero.grounded && !hero.action && hero.hurtTime <= 0 && hero.hp > 0 && hero.guardBreak <= 0 && hero.doublePose <= 0;
|
hero.blocking = (keys.has('l') || hero.autoGuard > 0) && hero.grounded && !hero.action && hero.hurtTime <= 0 && hero.hp > 0 && hero.guardBreak <= 0 && hero.doublePose <= 0;
|
||||||
const dir = (keys.has('d') ? 1 : 0) - (keys.has('a') ? 1 : 0), crouching = (keys.has('s') || hero.blocking) && hero.grounded && !hero.action;
|
const dir = (keys.has('d') ? 1 : 0) - (keys.has('a') ? 1 : 0), crouching = (keys.has('s') || hero.blocking) && hero.grounded && !hero.action, vx0 = hero.vx;
|
||||||
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;
|
||||||
|
|
@ -1856,6 +1862,7 @@
|
||||||
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');
|
||||||
} else hero.vx = approach(hero.vx, 0, dt * 1900);
|
} else hero.vx = approach(hero.vx, 0, dt * 1900);
|
||||||
|
if (hero.grounded) hero.vx = window.Daily?.slip?.(vx0, hero.vx) ?? hero.vx; // Lantai licin (daily.js): percepatan dan gesekan di lantai melemah
|
||||||
if (hero.hurtTime > 0) { hero.hurtTime -= dt; state('hurt'); if (hero.hurtTime <= 0) hero.invuln = .9; }
|
if (hero.hurtTime > 0) { hero.hurtTime -= dt; state('hurt'); if (hero.hurtTime <= 0) hero.invuln = .9; }
|
||||||
if(hero.hp<=0) { hero.koTime=Math.max(0,hero.koTime-dt); state('down'); if(!hero.koTime&&match?.mode!=='versus') {hero.hp=BALANCE.heroMax;hero.invuln=.9;state(hero.grounded?'idle':'jump');} }
|
if(hero.hp<=0) { hero.koTime=Math.max(0,hero.koTime-dt); state('down'); if(!hero.koTime&&match?.mode!=='versus') {hero.hp=BALANCE.heroMax;hero.invuln=.9;state(hero.grounded?'idle':'jump');} }
|
||||||
if (hero.action) {
|
if (hero.action) {
|
||||||
|
|
@ -1890,6 +1897,7 @@
|
||||||
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*cdRate(dummy,name));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);}
|
||||||
|
const vx0 = dummy.vx; // Lantai licin (daily.js): kecepatan sebelum gesekan dan AI, dipakai di akhir fungsi
|
||||||
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);
|
||||||
|
|
@ -1900,6 +1908,7 @@
|
||||||
if (dummy.state === 'down') { if (dummy.y < CONFIG.groundY || dummy.vy < 0) { dummy.vy += CONFIG.gravity * dt; dummy.y += dummy.vy * dt; if (dummy.y >= CONFIG.groundY) { dummy.y = CONFIG.groundY; dummy.vy = 0; dust(dummy.x, 24); trauma = Math.min(1, trauma + .3); } } if (dummy.stateTime > 1.7 && (match?.mode!=='versus' || !dummy.ko)) { dummy.state = 'recover'; dummy.stateTime = 0; } }
|
if (dummy.state === 'down') { if (dummy.y < CONFIG.groundY || dummy.vy < 0) { dummy.vy += CONFIG.gravity * dt; dummy.y += dummy.vy * dt; if (dummy.y >= CONFIG.groundY) { dummy.y = CONFIG.groundY; dummy.vy = 0; dust(dummy.x, 24); trauma = Math.min(1, trauma + .3); } } if (dummy.stateTime > 1.7 && (match?.mode!=='versus' || !dummy.ko)) { dummy.state = 'recover'; dummy.stateTime = 0; } }
|
||||||
if (dummy.state === 'recover' && dummy.stateTime > .4) { dummy.state = 'idle'; dummy.stateTime = 0; if(dummy.ko) dummy.damage = 0; dummy.ko=false; dummy.invuln = .5; }
|
if (dummy.state === 'recover' && dummy.stateTime > .4) { dummy.state = 'idle'; dummy.stateTime = 0; if(dummy.ko) dummy.damage = 0; dummy.ko=false; dummy.invuln = .5; }
|
||||||
if(F)updateFenrAI(dt);
|
if(F)updateFenrAI(dt);
|
||||||
|
if (dummy.y >= CONFIG.groundY - 1) dummy.vx = window.Daily?.slip?.(vx0, dummy.vx) ?? dummy.vx;
|
||||||
}
|
}
|
||||||
// HALDOR Slag Shot landing: a small molten splash on the floor where the lob comes down.
|
// HALDOR Slag Shot landing: a small molten splash on the floor where the lob comes down.
|
||||||
function slagSplash(p) {
|
function slagSplash(p) {
|
||||||
|
|
@ -2577,7 +2586,7 @@
|
||||||
if(shadowFoe)drawShadowAura();
|
if(shadowFoe)drawShadowAura();
|
||||||
if(!m||!img)return;
|
if(!m||!img)return;
|
||||||
const r=m.frame_layout.rows[pose.state][pose.frame];
|
const r=m.frame_layout.rows[pose.state][pose.frame];
|
||||||
ctx.save();ctx.translate(Math.round(dummy.x),Math.round(dummy.y));{const om=CF[opponentCharacter]?(cfFormOn(opponentCharacter,'enemy')?CF[opponentCharacter].mode?.form?.metrics:CF[opponentCharacter].metrics):null,k=stateK(om,pose.state);ctx.scale((dummy.facing||-1)*k,k);}ctx.imageSmoothingEnabled=false;
|
ctx.save();ctx.translate(Math.round(dummy.x),Math.round(dummy.y));{const om=CF[opponentCharacter]?(cfFormOn(opponentCharacter,'enemy')?CF[opponentCharacter].mode?.form?.metrics:CF[opponentCharacter].metrics):null,k=stateK(om,pose.state)*(window.Daily?.foeScale||1);ctx.scale((dummy.facing||-1)*k,k);}ctx.imageSmoothingEnabled=false;
|
||||||
if(shadowFoe&&FILTER_OK)ctx.filter=SHADOW.filter;
|
if(shadowFoe&&FILTER_OK)ctx.filter=SHADOW.filter;
|
||||||
ctx.drawImage(img,r.x,r.y,r.w,r.h,-m.cell.width/2,-(m.cell.height-m.cell.safe_margin_y),r.w,r.h);
|
ctx.drawImage(img,r.x,r.y,r.w,r.h,-m.cell.width/2,-(m.cell.height-m.cell.safe_margin_y),r.w,r.h);
|
||||||
if(shadowFoe){if(FILTER_OK)ctx.filter='none';else drawShadowTint(img,r,-m.cell.width/2,-(m.cell.height-m.cell.safe_margin_y));}
|
if(shadowFoe){if(FILTER_OK)ctx.filter='none';else drawShadowTint(img,r,-m.cell.width/2,-(m.cell.height-m.cell.safe_margin_y));}
|
||||||
|
|
@ -2924,7 +2933,7 @@
|
||||||
canvas.dataset.poseScale = String(currentPose.scale);
|
canvas.dataset.poseScale = String(currentPose.scale);
|
||||||
canvas.dataset.posePivot = currentPose.pivot ? `${currentPose.pivot.x},${currentPose.pivot.y}` : '';
|
canvas.dataset.posePivot = currentPose.pivot ? `${currentPose.pivot.x},${currentPose.pivot.y}` : '';
|
||||||
canvas.dataset.poseWorldPivot = currentPose.worldPivot ? `${currentPose.worldPivot.x},${currentPose.worldPivot.y}` : '';
|
canvas.dataset.poseWorldPivot = currentPose.worldPivot ? `${currentPose.worldPivot.x},${currentPose.worldPivot.y}` : '';
|
||||||
const h=hpLayers(hero.hp), dh=hpLayers(dummyHpShown());
|
const h=hpLayers(heroHpShown()), dh=hpLayers(dummyHpShown());
|
||||||
$('#health-fill').style.width = `${h.front}%`; $('#health-reserve').style.width = `${h.reserve}%`; $('#health-label').textContent = `${hero.hp} / ${BALANCE.heroMax}`;
|
$('#health-fill').style.width = `${h.front}%`; $('#health-reserve').style.width = `${h.reserve}%`; $('#health-label').textContent = `${hero.hp} / ${BALANCE.heroMax}`;
|
||||||
$('#guard-fill').style.width = `${dh.front}%`; $('#guard-reserve').style.width = `${dh.reserve}%`; $('#guard-label').textContent = `HP ${currentDummyHP()} / ${BALANCE.dummyMax}`;
|
$('#guard-fill').style.width = `${dh.front}%`; $('#guard-reserve').style.width = `${dh.reserve}%`; $('#guard-label').textContent = `HP ${currentDummyHP()} / ${BALANCE.dummyMax}`;
|
||||||
$('#dummy-state').textContent = dummy.ko?'K.O.':({ idle: aiEnabled?'WATCHING':'PRACTICE', hurt: 'HIT CONFIRMED', down: 'DOWN', recover: 'RECOVERING' })[dummy.state] || (dummy.action?'ATTACKING':'APPROACHING');
|
$('#dummy-state').textContent = dummy.ko?'K.O.':({ idle: aiEnabled?'WATCHING':'PRACTICE', hurt: 'HIT CONFIRMED', down: 'DOWN', recover: 'RECOVERING' })[dummy.state] || (dummy.action?'ATTACKING':'APPROACHING');
|
||||||
|
|
|
||||||
|
|
@ -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"><link rel="stylesheet" href="hazard.css"><link rel="stylesheet" href="challenge.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="daily.css"><link rel="stylesheet" href="hazard.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="hazard.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>
|
</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="hazard.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><script src="daily.js"></script></body></html>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue