/* 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
, 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 || '' }) }; })();