365 lines
28 KiB
JavaScript
365 lines
28 KiB
JavaScript
/* 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(); });
|
|
})();
|