fighting-konoha/brand.js

129 lines
7.2 KiB
JavaScript

/* Merek dan kabar pembaruan (RENCANA 2.7 dan 2.8). Tanpa dependensi; dimuat sebelum precache.js supaya kutipan loading tampil di kartu boot.
- Kutipan loading: window.LOADING_QUOTES (content/loading.js) dipilih acak tiap halaman dimuat, tidak pernah sama dengan yang terakhir
tampil (localStorage['konoha.loadquote']); tampil di #boot (.boot-quote) dan #load-state (.load-quote). ?quote=N memaksa kutipan ke-N (uji).
- Yang baru: window.CHANGELOG (content/changelog.js). Panel di layar About lewat Brand.decorate(root, screen) yang dipanggil menu.js
setelah setiap render, dan toast "Ada yang baru" satu kali saat versi teratas berbeda dari yang terakhir dilihat
(localStorage['konoha.changelog.seen']). Pemain baru (belum punya simpanan) tidak diberi toast; versinya langsung dicatat.
Pengacak di sini hanya kosmetik, tidak memengaruhi pertandingan. Semua akses localStorage dibungkus try/catch. */
(() => {
'use strict';
const KEY_QUOTE = 'konoha.loadquote', KEY_SEEN = 'konoha.changelog.seen', KEY_SAVE = 'konoha.save.v1';
const store = {
get(k) { try { return localStorage.getItem(k); } catch (_) { return null; } },
set(k, v) { try { localStorage.setItem(k, v); } catch (_) {} }
};
const MONTHS = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
const state = { quote: '', quoteIndex: -1 };
// ---------- kutipan loading ----------
function pickQuote() {
const list = window.LOADING_QUOTES;
if (!Array.isArray(list) || !list.length) return;
const forced = new URLSearchParams(location.search).get('quote');
let i;
if (forced !== null && /^\d+$/.test(forced)) i = Number(forced) % list.length;
else {
const last = Number(store.get(KEY_QUOTE));
i = Math.floor(Math.random() * list.length);
if (list.length > 1 && i === last) i = (i + 1 + Math.floor(Math.random() * (list.length - 1))) % list.length;
}
store.set(KEY_QUOTE, String(i));
state.quote = String(list[i]); state.quoteIndex = i;
}
function paintQuote() {
if (!state.quote) return;
for (const el of document.querySelectorAll('.boot-quote,.load-quote')) el.textContent = state.quote;
}
pickQuote(); paintQuote();
// #load-state berada di <main>, yang belum ada saat skrip ini berjalan.
document.addEventListener('DOMContentLoaded', paintQuote);
// ---------- Yang baru ----------
const log = () => Array.isArray(window.CHANGELOG) ? window.CHANGELOG.filter(e => e && e.version) : [];
const latest = () => log()[0] || null;
const parts = v => String(v).split('.').map(n => parseInt(n, 10) || 0);
function cmp(a, b) {
const x = parts(a), y = parts(b);
for (let i = 0; i < Math.max(x.length, y.length); i++) { const d = (x[i] || 0) - (y[i] || 0); if (d) return d < 0 ? -1 : 1; }
return 0;
}
function dateText(iso) {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso || ''));
return m ? `${Number(m[3])} ${MONTHS[Number(m[2]) - 1] || ''} ${m[1]}`.replace(' ', ' ') : '';
}
// Versi terakhir yang dilihat pemain saat halaman dimuat (null = belum pernah). Dibaca sekali supaya lencana BARU bertahan selama sesi.
const seenAtLoad = store.get(KEY_SEEN);
const isNew = e => seenAtLoad !== null && cmp(e.version, seenAtLoad) > 0;
function markSeen() { const l = latest(); if (l) store.set(KEY_SEEN, String(l.version)); }
const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
function panel() {
const entries = log();
if (!entries.length) return null;
const box = el('aside', 'whatsnew-panel'); box.setAttribute('aria-label', 'Yang baru');
const head = el('header', 'wn-head'); head.append(el('span', 'eyebrow', 'YANG BARU'), el('h2', '', 'Catatan pembaruan'));
const scroller = el('div', 'wn-scroll'); scroller.tabIndex = 0; scroller.setAttribute('role', 'region'); scroller.setAttribute('aria-label', 'Daftar pembaruan');
entries.forEach((e, idx) => {
const sec = el('section', 'wn-entry' + (idx === 0 ? ' latest' : ''));
const h = el('h3', ''); h.append(el('b', '', 'v' + e.version));
if (e.date) h.append(el('small', '', dateText(e.date)));
if (isNew(e)) h.append(el('i', 'wn-badge', 'BARU'));
sec.append(h);
if (e.title) sec.append(el('p', 'wn-title', e.title));
const ul = el('ul', ''); for (const it of e.items || []) ul.append(el('li', '', it)); sec.append(ul);
scroller.append(sec);
});
box.append(head, scroller);
return box;
}
// ---------- toast "Ada yang baru" ----------
let toastPending = false, toastShown = false;
function showToast(root) {
const l = latest(); if (!l || toastShown) return;
toastShown = true;
const t = el('div', 'brand-toast'); t.setAttribute('role', 'status'); t.setAttribute('aria-live', 'polite');
const text = el('div', 'bt-text'); text.append(el('b', '', 'Ada yang baru'), el('span', '', `Versi ${l.version}: ${l.title || 'pembaruan baru'}.`));
const look = el('button', 'bt-look', 'Lihat'), close = el('button', 'bt-close', '✕');
look.type = close.type = 'button'; close.setAttribute('aria-label', 'Tutup pemberitahuan');
const dismiss = () => { t.classList.add('out'); setTimeout(() => t.remove(), 450); };
look.onclick = () => { dismiss(); (root || document).querySelector('[data-cmd="about"]')?.click(); };
close.onclick = dismiss;
t.append(text, look, close); document.body.append(t);
setTimeout(dismiss, 9000);
markSeen();
}
function maybeToast(root) {
const l = latest(); if (!l || toastShown || toastPending) return;
if (store.get(KEY_SEEN) === String(l.version)) { toastShown = true; return; }
// Pemain baru (tanpa simpanan dan tanpa catatan versi): cukup catat versinya, tanpa pemberitahuan.
if (seenAtLoad === null && store.get(KEY_SAVE) === null) { toastShown = true; markSeen(); return; }
toastPending = true;
setTimeout(() => { toastPending = false; if (root.dataset.screen === 'home' && !root.hidden) showToast(root); }, 900);
}
// Dipanggil menu.js di akhir render(): about mendapat panel Yang baru + tautan kredit; home memicu toast satu kali.
function decorate(root, screen) {
if (!root) return;
if (screen === 'about') {
const p = panel(); if (p) root.append(p);
const small = root.querySelector('.about-panel > small');
if (small && !small.querySelector('a')) {
small.append(document.createElement('br'));
const a = el('a', 'about-link', 'Repo asli AETHER CLASH'), b = el('a', 'about-link', 'Tutorial YouTube');
a.href = 'https://github.com/bangtutorial/aether-clash'; b.href = 'https://www.youtube.com/watch?v=UN_0bNC2wTU';
for (const x of [a, b]) { x.target = '_blank'; x.rel = 'noopener noreferrer'; }
small.append(a, document.createTextNode(' · '), b);
}
markSeen();
} else if (screen === 'home') maybeToast(root);
}
window.Brand = {
decorate, panel, markSeen,
get version() { return latest()?.version || ''; },
get quote() { return state.quote; },
state: () => ({ quote: state.quote, quoteIndex: state.quoteIndex, seen: store.get(KEY_SEEN), seenAtLoad, toast: toastShown, version: latest()?.version || '' })
};
})();