Tantangan harian dan hadiah hari pertama (RENCANA 7.1 dan 7.2): daily.js dan daily.css. Satu tantangan per tanggal lokal dari hash tanggal (aturan, lawan, arena, tingkat) dengan 10 aturan (Darah satu, Hanya ultimate, Tanpa blok, Lawan cepat, Lantai licin, Tanpa lompat, Kombo wajib, Satu ronde, Counter saja, Raksasa). Tombol HARIAN di menu utama membuka kartu tantangan (aturan, deskripsi, lawan, arena, tingkat, hadiah, streak, pilih petarung). Menang = 150 koin + 25 per hari beruntun, sekali per tanggal, masuk rincian koin layar hasil; MAIN LAGI mengulang tantangan yang sama. Baris aturan di layar VS dan penanda aturan di arena. Hadiah hari pertama +25 koin tiap tanggal dan kejutan tanggal (1 Jan, 14 Mar, 8 Agu, 17 Agu) sekali per tahun. Simpanan di Meta.save.daily. Kait kecil di game.js (arm, deny, hit, parry, cooldowns, slip, foeScale, heroHpShown, match.toWin di HUD) yang no-op tanpa tantangan aktif. Uji manual: ?daily=YYYY-MM-DD atau Daily.setDate(...) di konsol.
This commit is contained in:
parent
3c9980bb09
commit
33a3318e48
|
|
@ -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(); });
|
||||
})();
|
||||
|
|
@ -31,6 +31,7 @@ Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks un
|
|||
- HUD (game.js): meter ultimate `#ult-player`/`#ult-enemy` (`updateUltMeters`; mengisi/SIAP/waktu mode dari `modeOf`, `bwx.rage`, bentuk FENR; DOM hanya ditulis saat tampilan berubah). Menu jeda `#pause-panel` dua panel; `buildMoveList()` membangun ulang Daftar gerakan tiap `setPaused(true)`, pilihan lipat diingat di `localStorage['konoha.moveList']`.
|
||||
- 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).
|
||||
- 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.
|
||||
- `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
|
|
@ -241,6 +241,7 @@
|
|||
if(!images['stage-'+stage])return false;
|
||||
selectedCharacter=player;opponentCharacter=enemy;stageId=stage;difficulty=level;images.stage=images['stage-'+stage];CONFIG.groundY=Rules.stages[stage].groundY;refreshPassives(matchStyles(options,mode));
|
||||
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();
|
||||
window.MatchStats?.reset(mode==='versus');
|
||||
$('#ai-toggle').checked=aiEnabled;$('#character-select').value=selectedCharacter;document.body.classList.toggle('in-versus',mode==='versus');
|
||||
|
|
@ -268,6 +269,7 @@
|
|||
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 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() {
|
||||
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)`);
|
||||
|
|
@ -291,7 +293,7 @@
|
|||
function advanceMatch(dt) {
|
||||
if(!Rules||!match||match.mode!=='versus'||vsHold)return;
|
||||
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==='round-end') {
|
||||
matchBest=Math.max(matchBest,bestCombo);clearInput();stopUltimateVoice();bwReset();squad=enemySquad=parade=enemyParade=flock=enemyFlock=serpent=enemySerpent=quake=enemyQuake=finale=enemyFinale=skyfall=enemySkyfall=orrery=enemyOrrery=sunroar=enemySunroar=delivery=enemyDelivery=tortoise=enemyTortoise=null;fenrCutin=null;hero.action=dummy.action=null;projectiles.length=effects.length=0;hitstop=cinematic=trauma=0;
|
||||
|
|
@ -317,20 +319,21 @@
|
|||
function updateMatchHud() {
|
||||
if(!Rules)return;
|
||||
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'):'∞';
|
||||
$('.match-center').setAttribute('aria-label',versus?'Pertandingan, menang dua ronde dari maksimal tiga':'Mode latihan tanpa batas waktu');
|
||||
const toWin=match?.toWin||2; // 1 = satu ronde (tantangan harian Satu ronde, match.toWin diisi daily.js)
|
||||
$('#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;
|
||||
$('#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);
|
||||
card.hidden=!show;card.dataset.phase=match?.phase||'fight';
|
||||
$('#round-call-title').textContent=!show?'':match.phase==='intro'?(match.fightCue?'FIGHT':`ROUND ${match.round}`):match.endText||'K.O.';
|
||||
$('#round-call-detail').textContent=match?.phase==='ko'?(match.lastWinner==='draw'?'DRAW · ROUND REPLAY':(match.lastWinner==='player'?selectedCharacter:opponentCharacter).toUpperCase()+' WINS THE ROUND'):'FIRST TO TWO WINS';
|
||||
$('#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;
|
||||
}
|
||||
|
||||
function canAct() { return ready && !menuOpen && (!match || match.phase === 'fight') && !paused && !settingsOpen && hero.hurtTime <= 0 && hero.hp > 0; }
|
||||
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
|
||||
// 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; }
|
||||
|
|
@ -347,7 +350,7 @@
|
|||
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) {
|
||||
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.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]);
|
||||
|
|
@ -358,6 +361,7 @@
|
|||
// 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.
|
||||
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;
|
||||
if (basic && canAct() && (hero.cooldowns[name] || 0) <= 0) hero.action = null;
|
||||
const ok = castMove(name);
|
||||
|
|
@ -398,6 +402,7 @@
|
|||
if (key === 'r') { reset(); return; }
|
||||
if (p2Mode && P2_KEYS.includes(key)) { p2Press(key); return; }
|
||||
if (!canAct() || keys.has(key)) return;
|
||||
if (key === 'l' && window.Daily?.deny?.('guard')) return; // Tanpa blok (daily.js): blok dan parry tidak pernah terdaftar
|
||||
keys.add(key); logInput(key);
|
||||
if (key === 'l') { hero.parry = .16 * passiveMod(hero, 'parry'); }
|
||||
if (key === 'j') startThrow();
|
||||
|
|
@ -441,7 +446,7 @@
|
|||
lunge: { label: 'TINJU MAJU!', state: 'attack2', cd: 1.8, duration: .44, hitAt: .3, damage: 11, knockback: 300 }
|
||||
};
|
||||
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;
|
||||
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;
|
||||
|
|
@ -455,7 +460,7 @@
|
|||
};
|
||||
function useShopMove() {
|
||||
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');
|
||||
if (m.instant) {
|
||||
if (id === 'perisai') { hero.invuln = Math.max(hero.invuln, 1.6); hero.shield = 1.6; }
|
||||
|
|
@ -549,7 +554,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 });
|
||||
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; }
|
||||
window.MatchStats?.parry();
|
||||
window.MatchStats?.parry(); window.Daily?.parry?.(time); // Counter saja: parry membuka celah (daily.js)
|
||||
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;
|
||||
|
|
@ -560,7 +565,7 @@
|
|||
return true;
|
||||
}
|
||||
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.runDirection = 0; state('attack2'); announce('LEMPAR!', .5); window.Meta?.shout?.('Lempar!'); return true;
|
||||
}
|
||||
|
|
@ -616,6 +621,7 @@
|
|||
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 (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;
|
||||
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),
|
||||
|
|
@ -1835,12 +1841,12 @@
|
|||
if(fenrCutin) {fenrCutin.t+=dt;if(fenrCutin.t>.78)fenrCutin=null;}
|
||||
if(F&&selectedCharacter==='fenr'){const changes=hero.formChanges;F.advance(hero,dt,hero.hp>0);if(changes!==hero.formChanges){syncPlayerForm();hero.doublePose=0;state(hero.grounded?'idle':'jump');}}
|
||||
hero.airTime = hero.grounded ? 0 : hero.airTime + dt; hero.spCd = Math.max(0, (hero.spCd || 0) - dt); hero.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); }
|
||||
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.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 (dir && hero.doublePose <= 0) hero.facing = dir;
|
||||
if (hero.runDirection && dir !== hero.runDirection) hero.runDirection = 0;
|
||||
|
|
@ -1849,6 +1855,7 @@
|
|||
if (hero.grounded) state(crouching ? 'crouch' : dir ? hero.runDirection ? 'run' : 'walk' : 'idle');
|
||||
else state(hero.doublePose > 0 ? 'doublejump' : 'jump');
|
||||
} else hero.vx = approach(hero.vx, 0, dt * 1900);
|
||||
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.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) {
|
||||
|
|
@ -1882,6 +1889,7 @@
|
|||
function hpLayers(value) {return {front:clamp(value-BALANCE.hpPerBar,0,BALANCE.hpPerBar),reserve:clamp(value,0,BALANCE.hpPerBar)};}
|
||||
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);}
|
||||
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.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);
|
||||
|
|
@ -1892,6 +1900,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 === '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 (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.
|
||||
function slagSplash(p) {
|
||||
|
|
@ -2569,7 +2578,7 @@
|
|||
if(shadowFoe)drawShadowAura();
|
||||
if(!m||!img)return;
|
||||
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;
|
||||
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));}
|
||||
|
|
@ -2914,7 +2923,7 @@
|
|||
canvas.dataset.poseScale = String(currentPose.scale);
|
||||
canvas.dataset.posePivot = currentPose.pivot ? `${currentPose.pivot.x},${currentPose.pivot.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}`;
|
||||
$('#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');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<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">
|
||||
<link rel="manifest" href="manifest.webmanifest"><link rel="icon" type="image/png" sizes="192x192" href="assets/pwa/icon-192.png"><link rel="apple-touch-icon" href="assets/pwa/apple-touch-icon.png"><link rel="stylesheet" href="touchlayout.css"><script src="pwa.js"></script><script src="touchlayout.js" defer></script>
|
||||
<link rel="stylesheet" href="feel.css"><link rel="stylesheet" href="vs.css"><link rel="stylesheet" href="story.css"><link rel="stylesheet" href="mystery.css"><link rel="stylesheet" href="input.css"><link rel="stylesheet" href="access.css"><link rel="stylesheet" href="tutorial.css"></head>
|
||||
<link rel="stylesheet" href="feel.css"><link rel="stylesheet" href="vs.css"><link rel="stylesheet" href="story.css"><link rel="stylesheet" href="mystery.css"><link rel="stylesheet" href="input.css"><link rel="stylesheet" href="access.css"><link rel="stylesheet" href="tutorial.css"><link rel="stylesheet" href="daily.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">
|
||||
<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">
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
<dialog id="pause-panel" aria-labelledby="pause-title"><div class="pause-main"><div class="dialog-kicker">KONOHA FIGHTING / JEDA</div><h2 id="pause-title">JEDA</h2><p>Pertarungan dijeda. Siap melanjutkan?</p><button class="primary-button" id="resume-btn">Lanjut bertarung <span>↗</span></button><button class="text-button" id="restart-btn">Mulai ulang</button><button class="text-button" id="back-menu">Kembali ke main menu</button></div><details class="move-list" id="move-list"><summary>Daftar gerakan</summary><div class="move-body" id="move-list-body"></div></details></dialog>
|
||||
<!-- Statistik pertandingan (RENCANA 5.5): gaya dan logika di berkas sendiri; MatchStats dipanggil lewat window.MatchStats?. dari game.js, meta.js, vs.js -->
|
||||
<link rel="stylesheet" href="stats.css"><script src="stats.js"></script>
|
||||
</main><section id="front-menu" aria-label="Main menu Konoha Fighting"></section><script src="assets/mecha/manifest.js"></script><script src="assets/stage-config.js"></script><script src="assets/ui/drone-config.js"></script><script src="assets/fenr/human/manifest.js"></script><script src="assets/fenr/wolf/manifest.js"></script><script src="fenr.js"></script><script src="assets/browo/manifest.js"></script><script src="browo.js"></script><script src="assets/megachan/manifest.js"></script><script src="assets/sonofsolo/manifest.js"></script><script src="assets/ethanolman/manifest.js"></script><script src="assets/anice/manifest.js"></script><script src="assets/soloman/manifest.js"></script><script src="assets/gufron/manifest.js"></script><script src="assets/audio/announcer/manifest.js"></script><script src="announcer.js"></script><script src="match.js"></script><script src="feel.js"></script><script src="access.js"></script><script src="content/passives.js"></script><script src="passives.js"></script><script src="content/quotes.js"></script><script src="content/dialogs.js"></script><script src="vs.js"></script><script>window.FRONTEND_ENABLED=true;</script><script src="game.js"></script><script src="touch.js"></script><script src="input.js"></script><script src="content/mystery.js"></script><script src="mystery.js"></script><script src="tutorial.js"></script><script src="menu.js"></script><script src="meta.js"></script></body></html>
|
||||
</main><section id="front-menu" aria-label="Main menu Konoha Fighting"></section><script src="assets/mecha/manifest.js"></script><script src="assets/stage-config.js"></script><script src="assets/ui/drone-config.js"></script><script src="assets/fenr/human/manifest.js"></script><script src="assets/fenr/wolf/manifest.js"></script><script src="fenr.js"></script><script src="assets/browo/manifest.js"></script><script src="browo.js"></script><script src="assets/megachan/manifest.js"></script><script src="assets/sonofsolo/manifest.js"></script><script src="assets/ethanolman/manifest.js"></script><script src="assets/anice/manifest.js"></script><script src="assets/soloman/manifest.js"></script><script src="assets/gufron/manifest.js"></script><script src="assets/audio/announcer/manifest.js"></script><script src="announcer.js"></script><script src="match.js"></script><script src="feel.js"></script><script src="access.js"></script><script src="content/passives.js"></script><script src="passives.js"></script><script src="content/quotes.js"></script><script src="content/dialogs.js"></script><script src="vs.js"></script><script>window.FRONTEND_ENABLED=true;</script><script src="game.js"></script><script src="touch.js"></script><script src="input.js"></script><script src="content/mystery.js"></script><script src="mystery.js"></script><script src="tutorial.js"></script><script src="menu.js"></script><script src="meta.js"></script><script src="daily.js"></script></body></html>
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue