/* Latar hidup dan cuaca (RENCANA 6.1 dan 6.3). Semua digambar prosedural di kanvas: tanpa gambar baru, tanpa pustaka. Kait di game.js (draw): Arena.back(ctx, env) tepat sesudah drawBackground, di belakang petarung (warna cuaca, awan, burung, kilau air, perahu, debu cahaya, daun, kunang-kunang, hujan jauh, kabut, sorotan lantai); Arena.front(ctx) paling akhir, layar penuh (penonton di tepi bawah, kabut depan, hujan dekat, kilat). Vinyet dan kerudung warna depan adalah elemen DOM #arena-vig (gaya di arena.css) tepat di atas kanvas: nol biaya gambar, dan tidak ikut zoom kamera. Biaya: operasi layar penuh dijaga sedikit (vinyet berskala dan blend besar sangat mahal pada rasterisasi perangkat lunak), tekstur kabut dibuat sesuai ukuran layar lalu digambar 1:1, gradasi langit memakai strip 1x256 yang diregangkan, dan blend besar memakai source-over. env = { viewW, viewH, scale, offsetX, offsetY, shakeX, shakeY, groundY, stage, time (realTime), menu, combo (comboHits), cin (cinematic) }. Kait di menu.js: Arena.decorate(root, screen) di akhir render() menambah baris Cuaca di layar arena, dan startMatch({weather: Arena.pick}). Pengaturan mendapat baris "Efek latar" (Penuh/Ringan/Mati) yang disisipkan modul ini sendiri. Aturan: hanya tampilan. Semua gerak adalah fungsi waktu tanpa status (hash bilangan bulat sebagai pengacak kosmetik, bukan random() mesin dan bukan Date), jadi hasil pertandingan tidak berubah, jeda membekukan latar, dan lompatan waktu dari advance() di uji tidak merusak apa pun. Satu-satunya status adalah semangat penonton. Gerak dikurangi (Access.motion = 0 atau prefers-reduced-motion): waktu latar dibekukan, burung/perahu/daun/debu/bintang jatuh/kilat/lompatan penonton dimatikan. Konsol: Arena.state(), Arena.force('hujan') (null = lepas), Arena.setFx('light'); URL ?cuaca=hujan memaksa cuaca di semua pertandingan. Bila ada galat gambar, modul mematikan diri setelah beberapa kali supaya loop game tidak pernah terganggu. */ (() => { 'use strict'; const W = 1280, H = 720, TAU = Math.PI * 2; const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); const lerp = (a, b, k) => a + (b - a) * k; const fract = v => v - Math.floor(v); const smooth = u => u * u * (3 - 2 * u); // Pengacak kosmetik tanpa status (hash bilangan bulat -> 0..1). Tiap efek punya salt sendiri; i = nomor elemen (+ siklus * 64), k = nomor parameter (< 16). function hash(n) { n |= 0; n = Math.imul(n ^ (n >>> 16), 0x7feb352d); n = Math.imul(n ^ (n >>> 15), 0x846ca68b); return ((n ^ (n >>> 16)) >>> 0) / 4294967296; } const rnd = (salt, i, k) => hash(salt * 100003 + i * 16 + k); const rgb = (r, g, b) => 'rgb(' + (r | 0) + ',' + (g | 0) + ',' + (b | 0) + ')'; const rgba = (c, a) => 'rgba(' + (c[0] | 0) + ',' + (c[1] | 0) + ',' + (c[2] | 0) + ',' + clamp(a, 0, 1).toFixed(3) + ')'; const KEY = 'konoha.arena.v1'; const PICKS = ['auto', 'cerah', 'hujan', 'kabut', 'senja', 'malam']; const NAME = { auto: 'Otomatis', cerah: 'Cerah', hujan: 'Hujan', kabut: 'Kabut', senja: 'Senja', malam: 'Malam' }; const FX = ['full', 'light', 'off'], FXNAME = { full: 'Penuh', light: 'Ringan', off: 'Mati' }; const DEF_FX = document.documentElement.classList.contains('touch') ? 'light' : 'full'; // HP: Ringan sebagai bawaan const S = { fx: DEF_FX, pick: 'auto', active: 'auto', forced: null }; // pick = pilihan di layar arena; active = cuaca pertandingan berjalan; forced = ?cuaca= atau Arena.force const E = { ok: false, dead: false, fails: 0, depth: 0, last: 0, dt: 0, spot: 640, on: false }; const C = { ex: 0, hold: 0, clap: 0, last: 0, ko: false }; // semangat penonton // ---- Data arena (koordinat 1280x720 pada gambar latar) ---- // horizon = garis perbukitan; sky = batas bawah langit terbuka untuk bintang; sun = letak matahari saat senja; cloud/bird = jalur awan dan burung (v0..v1; tanpa cloud = tanpa langit yang terlihat); keep = kotak [u0, u1, v0, v1] // bangunan tinggi di depan langit (awan dan bintang memudar saat menutupinya, karena awan digambar di atas gambar latar); water = kolam kilau air (cu/cv = sudut dermaga yang dikecualikan); // boat = perahu kecil; dust = debu cahaya; fire = kunang-kunang; auto = [cuaca, kekuatan] untuk pilihan Otomatis; crowd = siluet penonton. const ARENA = { _: { auto: ['cerah', 1], horizon: 340, sun: [900, 250], cloud: { n: 4, v0: 25, v1: 240 }, bird: { style: 'swallow', v0: 60, v1: 250 }, crowd: { tone: '#2a2433', rim: '#9a8fbf' } }, bellora: { auto: ['cerah', 1], horizon: 335, sky: 200, sun: [1010, 235], cloud: { n: 4, v0: 25, v1: 200 }, keep: [[375, 500, 0, 330], [0, 190, 115, 330], [1000, 1280, 105, 330], [760, 800, 222, 330]], bird: { style: 'swallow', v0: 60, v1: 240 }, crowd: { tone: '#2b2236', rim: '#8a76b6', acc: 'flag', flags: ['#2f5aa8', '#e0b84a'] } }, sunspire: { auto: ['cerah', 1], horizon: 470, sky: 330, sun: [640, 330], cloud: { n: 5, v0: 15, v1: 240 }, keep: [[105, 185, 150, 480], [155, 375, 200, 500], [855, 1280, 290, 530], [1145, 1240, 170, 500]], bird: { style: 'swallow', v0: 50, v1: 300 }, dust: { n: 26, v0: 150, v1: 540, col: 'gold' }, shaft: 1, crowd: { tone: '#3a2630', rim: '#e8b27a', acc: 'flag', flags: ['#c04a4a', '#f0c860'] } }, harbor: { auto: ['cerah', 1], horizon: 405, sky: 300, sun: [560, 300], cloud: { n: 3, v0: 10, v1: 190 }, keep: [[855, 950, 55, 345], [1045, 1280, 95, 265], [300, 455, 235, 440]], bird: { style: 'gull', v0: 55, v1: 290 }, water: { u0: 238, u1: 604, v0: 432, v1: 504, cu: 540, cv: 448 }, boat: { v: 470, u0: 255, u1: 585 }, crowd: { tone: '#1c2a38', rim: '#7fb6d8', acc: 'hat' } }, elderwood: { auto: ['kabut', .5], horizon: 400, sun: [470, 100], leaves: { n: 14 }, dust: { n: 22, v0: 120, v1: 520, col: 'leaf' }, crowd: { tone: '#101f14', rim: '#8fd08a', acc: 'hat' } }, moonrise: { auto: ['malam', 1], night: 1, horizon: 420, sky: 330, sun: [200, 410], cloud: { n: 3, v0: 30, v1: 300 }, keep: [[960, 1130, 100, 520]], bird: { style: 'bat', v0: 80, v1: 330 }, water: { u0: 440, u1: 640, v0: 445, v1: 515, dim: .6 }, fire: { n: 16 }, crowd: { tone: '#080b22', rim: '#8fa4ff', acc: 'lantern' } } }; // ---- Cuaca ---- // mul = warna pengali di atas latar [r, g, b, kekuatan]; veil = kerudung warna biasa; top = gradasi dari atas (langit); glow = cahaya cakrawala; cloud = nada awan, cn = jumlah awan; // rain/fog/mist = tingkat 0..1; vig = [nada vinyet, kekuatan]; spot = sorotan lantai; bolt = kilat. const WX = { cerah: {}, hujan: { mul: [170, 184, 204, .9], veil: [96, 110, 126, .14], top: [54, 66, 82, .5], cloud: 'storm', cn: 7, rain: 1, mist: .4, vig: ['dark', .34], bolt: 1 }, kabut: { veil: [222, 230, 235, .24], top: [234, 240, 244, .3], cloud: 'mist', cn: 3, fog: 1, vig: ['dark', .1] }, senja: { mul: [255, 186, 134, .85], top: [88, 54, 130, .34], glow: [255, 140, 78, .34], sun: 1, spot: 'warm', cloud: 'dusk', vig: ['dusk', .3] }, malam: { mul: [86, 104, 172, .78], spot: 'cool', cloud: 'night', vig: ['night', .46] } }; const FRONT_VEIL = { hujan: [70, 84, 100, .07], kabut: [222, 230, 236, .07], senja: [255, 150, 70, .06], malam: [10, 20, 60, .1] }; // ---- Sprite kecil: digambar sekali di kanvas luar layar lalu dipakai ulang lewat drawImage ---- const SPR = {}; function sprite(key, w, h, paint) { let c = SPR[key]; if (!c) { c = document.createElement('canvas'); c.width = Math.max(1, Math.round(w)); c.height = Math.max(1, Math.round(h)); paint(c.getContext('2d'), c.width, c.height); SPR[key] = c; } return c; } const GLOW = { gold: [255, 218, 128], leaf: [214, 238, 150], fly: [214, 255, 120], warm: [255, 198, 112], sun: [255, 190, 110], cool: [200, 222, 255] }; const glow = name => sprite('g' + name, 64, 64, (x, w) => { const c = GLOW[name], g = x.createRadialGradient(w / 2, w / 2, 0, w / 2, w / 2, w / 2); g.addColorStop(0, rgba(c, 1)); g.addColorStop(.3, rgba(c, .55)); g.addColorStop(1, rgba(c, 0)); x.fillStyle = g; x.fillRect(0, 0, w, w); }); const CLOUD = { day: { hi: [255, 255, 255], lo: [182, 206, 232], a: .92, o: .85 }, dusk: { hi: [255, 218, 196], lo: [150, 100, 150], a: .9, o: .85 }, night: { hi: [128, 146, 208], lo: [40, 50, 104], a: .8, o: .55 }, storm: { hi: [156, 166, 178], lo: [72, 82, 96], a: .95, o: .9 }, mist: { hi: [228, 234, 238], lo: [172, 184, 196], a: .8, o: .6 } }; const CLOUD_SHAPE = [ { w: 300, h: 130, sy: 1, c: [[60, 76, 32], [105, 58, 42], [160, 48, 50], [215, 60, 42], [256, 80, 30], [150, 78, 38], [100, 86, 32], [205, 88, 32]] }, { w: 176, h: 96, sy: 1, c: [[44, 56, 26], [84, 44, 34], [128, 54, 28], [90, 56, 30]] }, { w: 350, h: 64, sy: .5, c: [[50, 52, 36], [112, 50, 44], [182, 54, 46], [252, 52, 40], [304, 56, 28]] } ]; function cloud(kind, tone) { const sh = CLOUD_SHAPE[kind], T = CLOUD[tone], K = 1.5; return sprite('c' + kind + tone, sh.w * K, sh.h * K, x => { x.scale(K, K * sh.sy); const blob = (cx, cy, r, c, a) => { const g = x.createRadialGradient(cx, cy, r * .12, cx, cy, r); g.addColorStop(0, rgba(c, a)); g.addColorStop(.6, rgba(c, a * .7)); g.addColorStop(1, rgba(c, 0)); x.fillStyle = g; x.beginPath(); x.arc(cx, cy, r, 0, TAU); x.fill(); }; for (const [cx, cy, r] of sh.c) blob(cx, cy + r * .26, r, T.lo, T.a * .8); for (const [cx, cy, r] of sh.c) blob(cx, cy - r * .06, r * .9, T.hi, T.a); }); } const FOG = { day: [224, 232, 238], gray: [170, 182, 194], night: [96, 112, 156] }; const fogTex = (tone, v) => sprite('f' + tone + v, 512, 160, (x, w, h) => { const c = FOG[tone]; for (let i = 0; i < 16; i++) { const cx = rnd(10 + v, i, 1) * w, cy = h * (.38 + .3 * rnd(10 + v, i, 2)), r = 58 + 70 * rnd(10 + v, i, 3), a = .35 + .3 * rnd(10 + v, i, 4); for (const dx of [-w, 0, w]) { // tiga salinan supaya ubin menyambung di sisi kiri dan kanan x.save(); x.translate(cx + dx, cy); x.scale(1, .42); const g = x.createRadialGradient(0, 0, 0, 0, 0, r); g.addColorStop(0, rgba(c, a)); g.addColorStop(.55, rgba(c, a * .45)); g.addColorStop(1, rgba(c, 0)); x.fillStyle = g; x.beginPath(); x.arc(0, 0, r, 0, TAU); x.fill(); x.restore(); } } x.globalCompositeOperation = 'destination-in'; // pudar di atas dan bawah supaya pita tidak bertepi keras const m = x.createLinearGradient(0, 0, 0, h); m.addColorStop(0, 'rgba(0,0,0,0)'); m.addColorStop(.4, 'rgba(0,0,0,1)'); m.addColorStop(.7, 'rgba(0,0,0,1)'); m.addColorStop(1, 'rgba(0,0,0,0)'); x.fillStyle = m; x.fillRect(0, 0, w, h); }); const spotSpr = tone => sprite('s' + tone, 128, 64, (x, w, h) => { const c = GLOW[tone === 'warm' ? 'warm' : 'cool']; x.translate(w / 2, h / 2); x.scale(1, h / w); const g = x.createRadialGradient(0, 0, 0, 0, 0, w / 2); g.addColorStop(0, rgba(c, 1)); g.addColorStop(.5, rgba(c, .45)); g.addColorStop(1, rgba(c, 0)); x.fillStyle = g; x.beginPath(); x.arc(0, 0, w / 2, 0, TAU); x.fill(); }); // Berkas cahaya (Sunspire): gradasi vertikal lalu ditipiskan di kedua sisi (destination-in). const shaftSpr = () => sprite('shaft', 64, 256, (x, w, h) => { const g = x.createLinearGradient(0, 0, 0, h); g.addColorStop(0, 'rgba(255,246,214,.9)'); g.addColorStop(1, 'rgba(255,246,214,0)'); x.fillStyle = g; x.fillRect(0, 0, w, h); x.globalCompositeOperation = 'destination-in'; const m = x.createLinearGradient(0, 0, w, 0); m.addColorStop(0, 'rgba(0,0,0,0)'); m.addColorStop(.5, 'rgba(0,0,0,1)'); m.addColorStop(1, 'rgba(0,0,0,0)'); x.fillStyle = m; x.fillRect(0, 0, w, h); }); // Strip gradasi 1x256 (alpha penuh di atas lalu memudar, atau puncak di tengah bila peak diberikan); diregangkan lebar saat digambar (jauh lebih murah daripada gradasi per frame). const strip = (c, peak) => sprite('t' + c[0] + ',' + c[1] + ',' + c[2] + (peak || ''), 1, 256, (x, w, h) => { const g = x.createLinearGradient(0, 0, 0, h); if (peak) { g.addColorStop(0, rgba(c, 0)); g.addColorStop(peak, rgba(c, 1)); g.addColorStop(1, rgba(c, 0)); } else { g.addColorStop(0, rgba(c, 1)); g.addColorStop(1, rgba(c, 0)); } x.fillStyle = g; x.fillRect(0, 0, w, h); }); const VIG = { dark: [0, 0, 0], night: [4, 8, 36], dusk: [44, 10, 40] }; // ---- Penyimpanan, pilihan, pengaturan ---- function load() { try { const o = JSON.parse(localStorage.getItem(KEY) || '{}'); if (FX.includes(o.fx)) S.fx = o.fx; } catch (_) { /* tanpa penyimpanan: pakai bawaan */ } } function save() { try { if (S.fx === DEF_FX) localStorage.removeItem(KEY); else localStorage.setItem(KEY, JSON.stringify({ fx: S.fx })); } catch (_) { /* penyimpanan diblokir */ } } try { const q = new URLSearchParams(location.search).get('cuaca'); if (PICKS.includes(q) && q !== 'auto') S.forced = q; } catch (_) { /* tanpa URL */ } const motion = (() => { let mq = null; try { mq = matchMedia('(prefers-reduced-motion: reduce)'); } catch (_) { /* tanpa matchMedia */ } return () => { try { return window.Access ? window.Access.motion : mq && mq.matches ? 0 : 1; } catch (_) { return 1; } }; })(); function resolve(stage) { const p = S.forced || S.active; if (p && p !== 'auto' && WX[p]) return [p, 1]; return (ARENA[stage] || ARENA._).auto; } const autoLabel = stage => { const r = (ARENA[stage] || ARENA._).auto; return NAME[r[0]] + (r[1] < 1 ? ' tipis' : ''); }; // ---- Lapisan DOM: vinyet dan kerudung warna depan (#arena-vig di atas kanvas, di bawah HUD); DOM hanya ditulis saat nilainya berubah ---- let OV = null, ovKey = '', ovSig = null; function syncOverlay() { if (!OV) { const cv = document.getElementById('arena'); if (!cv || !cv.parentNode) return; OV = document.createElement('div'); OV.id = 'arena-vig'; OV.setAttribute('aria-hidden', 'true'); cv.after(OV); } const sig = E.on ? E.wx + '|' + E.wk + '|' + E.light + '|' + !!E.A.night : ''; if (sig === ovSig) return; ovSig = sig; let av = '0,0,0', ak = '0', aw = 'transparent', key = ''; if (E.on) { const v = E.P.vig, w = FRONT_VEIL[E.wx], k = E.wk; if (v) { av = VIG[v[0]].join(','); ak = clamp(v[1] * k, 0, 1).toFixed(2); } if (w && !E.light) aw = rgba(w, w[3] * k * (w[0] > 200 && E.A.night ? .35 : 1)); if (v || w) key = av + '|' + ak + '|' + aw; } if (key === ovKey) return; ovKey = key; if (!key) { OV.style.opacity = '0'; return; } OV.style.setProperty('--av', av); OV.style.setProperty('--ak', ak); OV.style.setProperty('--aw', aw); OV.style.opacity = '1'; } // ---- Per frame ---- function push(ctx) { ctx.save(); E.depth++; } function pop(ctx) { ctx.restore(); E.depth--; } function prep(e) { E.vw = e.viewW; E.vh = e.viewH; E.sc = e.scale; E.ox = e.offsetX; E.oy = e.offsetY; E.shx = e.shakeX * e.scale; E.shy = e.shakeY * e.scale; E.gy = e.groundY; E.stage = e.stage; E.t = e.time; E.menu = !!e.menu; E.combo = e.combo | 0; E.fade = 1 - Math.min(.65, (e.cin || 0) * 1.5); // fade = redup latar saat sinematik ultimate const dt = clamp(e.time - E.last, 0, .1); E.last = e.time; E.dt = dt; E.on = !E.menu && S.fx !== 'off'; if (E.menu) { C.ex = C.hold = C.clap = C.last = 0; C.ko = false; } if (!E.on) { syncOverlay(); return; } E.light = S.fx === 'light'; E.calm = motion() < 1; E.T = E.calm ? 0 : e.time; // Pemetaan gambar latar ke layar sama dengan drawBackground (menutupi layar, lantai gambar sejajar lantai tempur). const bs = Math.max(E.vw / W, E.vh / H); E.bs = bs; E.ix = (E.vw - W * bs) / 2; E.iy = E.vh * .815 - E.gy * bs; E.uL = -E.ix / bs; E.uR = (E.vw - E.ix) / bs; E.vT = -E.iy / bs; E.vB = (E.vh - E.iy) / bs; E.wL = -E.ox / E.sc; E.wR = (E.vw - E.ox) / E.sc; E.wB = (E.vh - E.oy) / E.sc; // sisi tampak dunia (lantai di E.gy) const r = resolve(E.stage); E.wx = r[0]; E.wk = r[1]; E.A = ARENA[E.stage] || ARENA._; E.P = WX[E.wx]; E.night = E.wx === 'malam' || (!!E.A.night && E.wx === 'cerah'); // langit malam: bintang, kelelawar, kunang-kunang syncOverlay(); if (!E.calm) crowdTick(dt); } function fail(ctx, err) { while (E.depth > 0) { try { ctx.restore(); } catch (_) { /* tumpukan sudah kosong */ } E.depth--; } E.ok = false; if (++E.fails <= 2) console.error('[arena] galat gambar:', err); if (E.fails >= 6) { E.dead = true; console.error('[arena] dimatikan sesudah galat berulang'); if (OV) OV.style.opacity = '0'; } } function back(ctx, e) { E.ok = false; try { if (E.dead || !ctx || !e) return; prep(e); if (!E.on) return; drawBack(ctx); E.ok = true; } catch (err) { fail(ctx, err); } } function front(ctx) { if (!E.ok) return; E.ok = false; try { drawFront(ctx); } catch (err) { fail(ctx, err); } } // ---- Penonton: semangat naik saat kombo besar (5, 8, 12 pukulan), saat kombo besar berakhir (tepuk), dan saat K.O. ---- function crowdTick(dt) { const F = window.Feel, ko = !!(F && F.ko && F.ko.state && F.ko.state.on), cb = E.combo; const pulse = v => { if (v > C.ex) C.ex = v; }; if (cb > C.last) { if (C.last < 12 && cb >= 12) pulse(1); else if (C.last < 8 && cb >= 8) pulse(.8); else if (C.last < 5 && cb >= 5) pulse(.55); } else if (cb === 0 && C.last >= 5) { C.clap = 1.3; pulse(.45); } C.last = cb; if (ko && !C.ko) { C.hold = F.ko.state.final ? 2.6 : 1.7; pulse(1); } C.ko = ko; C.hold = Math.max(0, C.hold - dt); C.clap = Math.max(0, C.clap - dt); C.ex = C.hold > 0 ? Math.max(C.ex, .9) : Math.max(0, C.ex - dt * .5); } // ---- Lapisan belakang ---- function drawBack(ctx) { const A = E.A, P = E.P, k = E.wk, ga = E.fade; push(ctx); ctx.imageSmoothingEnabled = true; tintBack(ctx, A, P, k); push(ctx); ctx.translate(E.ix + E.shx, E.iy + E.shy); ctx.scale(E.bs, E.bs); imageLayers(ctx, A, P, k, ga); pop(ctx); if (P.rain) rain(ctx, true, k); if (P.fog) { fogBand(ctx, 0, k); if (!E.light) fogBand(ctx, 1, k); } else if (P.mist) fogBand(ctx, 0, P.mist * k); push(ctx); ctx.translate(E.ox + E.shx, E.oy + E.shy); ctx.scale(E.sc, E.sc); if (P.rain && !E.calm) splashes(ctx, ga); pop(ctx); if (P.spot) spotlight(ctx, P, k, ga); pop(ctx); } function tintBack(ctx, A, P, k) { const lt = A.night ? .35 : 1; // arena yang sudah malam: kerudung terang dikurangi, tint gelap tidak digandakan if (P.mul) { const s = P.mul[3] * k * (A.night && E.wx === 'malam' ? .3 : 1); ctx.globalCompositeOperation = 'multiply'; ctx.fillStyle = rgb(lerp(255, P.mul[0], s), lerp(255, P.mul[1], s), lerp(255, P.mul[2], s)); ctx.fillRect(0, 0, E.vw, E.vh); ctx.globalCompositeOperation = 'source-over'; } if (P.veil && !E.light) { ctx.globalAlpha = P.veil[3] * k * (P.veil[0] > 200 ? lt : 1); ctx.fillStyle = rgb(P.veil[0], P.veil[1], P.veil[2]); ctx.fillRect(0, 0, E.vw, E.vh); ctx.globalAlpha = 1; } } // Semua yang menempel pada gambar latar (koordinat gambar 1280x720; sudah di-translate dan di-scale). function imageLayers(ctx, A, P, k, ga) { const uL = E.uL, uR = E.uR, vT = E.vT, light = E.light, calm = E.calm, lt = A.night ? .35 : 1; if (P.top && !light) { const bot = A.horizon + 30; ctx.globalAlpha = clamp(P.top[3] * k * (P.top[0] > 200 ? lt : 1), 0, 1); ctx.drawImage(strip(P.top), 0, 0, 1, 256, uL, vT, uR - uL, Math.max(40, bot - vT)); ctx.globalAlpha = 1; } if (P.glow && !light) { ctx.globalAlpha = clamp(P.glow[3] * k * 2, 0, 1); ctx.drawImage(strip(P.glow, .7), 0, 0, 1, 256, uL, A.horizon - 170, uR - uL, 240); ctx.globalAlpha = 1; } if (P.sun) { const sp = glow('sun'), su = A.sun[0], sv = A.sun[1]; ctx.globalAlpha = .55 * k * ga; ctx.drawImage(sp, su - 260, sv - 260, 520, 520); ctx.globalAlpha = .6 * k * ga; ctx.drawImage(sp, su - 80, sv - 80, 160, 160); ctx.globalAlpha = 1; } if (E.night && A.cloud) stars(ctx, A, ga); if (A.shaft && !calm && !light && (E.wx === 'cerah' || E.wx === 'senja')) shafts(ctx, k, ga); if (A.cloud) clouds(ctx, A, P, ga); if (!calm && A.bird) { const st = E.night ? 'bat' : (E.wx === 'cerah' || E.wx === 'senja') ? A.bird.style : ''; if (st) birds(ctx, A, st, ga); } if (A.water && (E.wx === 'cerah' || E.wx === 'senja' || E.wx === 'malam')) water(ctx, A, ga); if (!calm && A.dust && (E.wx === 'cerah' || E.wx === 'senja' || E.wx === 'kabut')) dust(ctx, A, k, ga); if (!calm && A.leaves && E.wx !== 'malam' && E.wx !== 'hujan') leaves(ctx, A, ga); if (E.night) { fireflies(ctx, A, ga); if (!calm && !light && A.cloud) shooting(ctx, ga); } } function stars(ctx, A, ga) { const n = E.light ? 12 : 26, T = E.T, v1 = Math.max(E.vT + 40, A.sky || A.horizon - 50); ctx.fillStyle = '#eaf2ff'; for (let i = 0; i < n; i++) { const tw = .5 + .5 * Math.sin(T * (1 + 2 * rnd(1, i, 2)) + rnd(1, i, 3) * TAU), s = 1 + 1.4 * rnd(1, i, 4), u = lerp(E.uL, E.uR, rnd(1, i, 1)), v = lerp(E.vT + 4, v1, rnd(1, i, 5)), K = A.keep; let hid = false; if (K) for (let j = 0; j < K.length; j++) { const q = K[j]; if (u > q[0] && u < q[1] && v > q[2] && v < q[3]) { hid = true; break; } } if (hid) continue; ctx.globalAlpha = (.25 + .65 * tw) * ga * (A.night ? .8 : 1); ctx.fillRect(u, v, s, s); } ctx.globalAlpha = 1; } function shafts(ctx, k, ga) { // berkas cahaya miring dari kiri atas (Sunspire) const spr = shaftSpr(), T = E.T, h = Math.min(E.vB, 560) - E.vT; for (let i = 0; i < 3; i++) { const w = 130 + 70 * rnd(2, i, 1), x = lerp(E.uL, E.uR, (i + .3 + .4 * rnd(2, i, 2)) / 3); ctx.globalAlpha = (.07 + .05 * (.5 + .5 * Math.sin(T * .22 + i * 2.1))) * k * ga; push(ctx); ctx.translate(x, E.vT); ctx.transform(1, 0, .32, 1, 0, 0); ctx.drawImage(spr, -w / 2, 0, w, h); pop(ctx); } ctx.globalAlpha = 1; } function clouds(ctx, A, P, ga) { const n = Math.ceil((P.cn || A.cloud.n) * (E.light ? .6 : 1)), tone = P.cloud || (A.night ? 'night' : 'day'), co = CLOUD[tone].o, T = E.T, uL = E.uL, uR = E.uR; const vlo = Math.max(A.cloud.v0, E.vT + 6), vhi = Math.max(vlo + 20, Math.min(A.cloud.v1, A.horizon - 24)), fast = tone === 'storm' ? 1.7 : 1; for (let i = 0; i < n; i++) { const sh = CLOUD_SHAPE[i % 3], sz = .75 + .6 * rnd(3, i, 1), w = sh.w * sz, h = sh.h * sz, span = (uR - uL) + 2 * w; const x = uL - w + fract((rnd(3, i, 2) * span + T * (4 + 7 * rnd(3, i, 3)) * fast) / span) * span, y = lerp(vlo, vhi, rnd(3, i, 4)) + Math.sin(T * .05 + i * 1.7) * 3, K = A.keep; let vis = 1; if (K) for (let j = 0; j < K.length; j++) { const q = K[j], ox = Math.min(x + w, q[1]) - Math.max(x, q[0]), oy = Math.min(y + h / 2, q[3]) - Math.max(y - h / 2, q[2]); if (ox > 0 && oy > 0) vis *= 1 - clamp(ox * oy / (w * h) * 4, 0, 1); } if (vis < .02) continue; ctx.globalAlpha = co * (.78 + .22 * rnd(3, i, 5)) * ga * vis; ctx.drawImage(cloud(i % 3, tone), x, y - h / 2, w, h); } ctx.globalAlpha = 1; } // Burung melintas sesekali: tiap slot punya siklus (30-60 dtk) dan melintas satu kali per siklus; arah, tinggi, dan jumlah diundi per siklus. function birds(ctx, A, style, ga) { const T = E.t, B = A.bird, slots = E.light ? 1 : 3, vlo = Math.max(B.v0, E.vT + 10), vhi = Math.max(vlo + 20, B.v1), span = E.uR - E.uL; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; for (let i = 0; i < slots; i++) { const per = 30 + 30 * rnd(4, i, 1), ph = rnd(4, i, 2) * per, cyc = Math.floor((T + ph) / per), age = T + ph - cyc * per, id = i + cyc * 64; const sp = (style === 'bat' ? 110 : 85) + 55 * rnd(4, id, 3), pass = (span + 160) / sp; if (age >= pass) continue; const dir = rnd(4, id, 4) < .5 ? 1 : -1, n = 1 + Math.floor(rnd(4, id, 5) * (style === 'gull' ? 2.99 : 3.99)), base = lerp(vlo, vhi, rnd(4, id, 6)); const sz = (style === 'gull' ? 9 : style === 'bat' ? 8 : 6.5) * (.85 + .3 * rnd(4, id, 7)), x0 = dir > 0 ? E.uL - 80 + age * sp : E.uR + 80 - age * sp; for (let j = 0; j < n; j++) { const bx = x0 - dir * j * sz * 3.2, by = base + Math.sin(age * 1.3 + j * .9 + id) * (style === 'bat' ? 16 : 7) + j * sz * 1.4 * (j % 2 ? 1 : -1); birdShape(ctx, style, bx, by, sz, Math.sin(T * (style === 'bat' ? 15 : 9) + j * 1.7 + i * 3.1), ga); } } } function birdShape(ctx, style, x, y, s, fl, ga) { const a = fl * s * .55; ctx.beginPath(); if (style === 'bat') { ctx.moveTo(x, y + s * .15); ctx.quadraticCurveTo(x - s * .7, y - s * (.9 + .5 * fl), x - s * 1.5, y - s * .1 * fl); ctx.quadraticCurveTo(x - s * .95, y + s * .05, x - s * .55, y + s * .5); ctx.quadraticCurveTo(x - s * .15, y + s * .2, x, y + s * .5); ctx.quadraticCurveTo(x + s * .15, y + s * .2, x + s * .55, y + s * .5); ctx.quadraticCurveTo(x + s * .95, y + s * .05, x + s * 1.5, y - s * .1 * fl); ctx.quadraticCurveTo(x + s * .7, y - s * (.9 + .5 * fl), x, y + s * .15); ctx.closePath(); ctx.globalAlpha = .88 * ga; ctx.fillStyle = '#080a1e'; ctx.fill(); } else { ctx.moveTo(x - s * 1.25, y - a); ctx.quadraticCurveTo(x - s * .5, y - s * .7 + a * .3, x, y); ctx.quadraticCurveTo(x + s * .5, y - s * .7 + a * .3, x + s * 1.25, y - a); if (style === 'gull') { ctx.globalAlpha = .4 * ga; ctx.strokeStyle = '#3d5568'; ctx.lineWidth = s * .42; ctx.stroke(); ctx.globalAlpha = .95 * ga; ctx.strokeStyle = '#fbfdff'; ctx.lineWidth = s * .26; ctx.stroke(); } else { ctx.globalAlpha = .8 * ga; ctx.strokeStyle = '#28323f'; ctx.lineWidth = Math.max(1.3, s * .24); ctx.stroke(); } } ctx.globalAlpha = 1; } // Kilau air (garis silang kecil, satu goresan) dan perahu layar kecil yang melintas pelan (Azure Harbor); Moonrise hanya kilau di sungai. function water(ctx, A, ga) { const Wt = A.water, T = E.T, n = Math.round((E.light ? 12 : 26) * (Wt.dim || 1)); ctx.globalCompositeOperation = 'lighter'; ctx.strokeStyle = E.night ? 'rgb(190,214,255)' : E.wx === 'senja' ? 'rgb(255,214,170)' : 'rgb(236,250,255)'; ctx.lineWidth = 1.2; ctx.lineCap = 'round'; ctx.globalAlpha = .85 * ga * (E.night ? .75 : 1); ctx.beginPath(); for (let i = 0; i < n; i++) { const per = 1.5 + 2.4 * rnd(5, i, 1), ph = rnd(5, i, 2) * per, c = Math.floor((T + ph) / per), age = (T + ph) / per - c, b = Math.pow(Math.sin(age * Math.PI), 3); if (b < .08) continue; const u = lerp(Wt.u0, Wt.u1, rnd(5, i + c * 64, 3)), v = lerp(Wt.v0, Wt.v1, rnd(5, i + c * 64, 4)), s = 1 + 5 * b; if (Wt.cu && u > Wt.cu && v < Wt.cv) continue; // sudut dermaga bukan air ctx.moveTo(u - s, v); ctx.lineTo(u + s, v); ctx.moveTo(u, v - s * .7); ctx.lineTo(u, v + s * .7); } ctx.stroke(); ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1; const B = A.boat; if (B && !E.calm && (E.wx === 'cerah' || E.wx === 'senja')) { const per = 120, c = Math.floor((E.t + 23) / per), age = E.t + 23 - c * per, dur = 80; if (age < dur) { const dir = c % 2 ? -1 : 1, p = age / dur, x = dir > 0 ? lerp(B.u0, B.u1, p) : lerp(B.u1, B.u0, p); push(ctx); ctx.translate(x, B.v + Math.sin(E.t * 1.1) * 1.4); ctx.rotate(Math.sin(E.t * .8) * .03); ctx.scale(dir, 1); ctx.globalAlpha = Math.min(1, p * 8, (1 - p) * 8) * ga; ctx.strokeStyle = 'rgba(255,255,255,.4)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(-26, 8); ctx.lineTo(-58, 10); ctx.moveTo(-22, 10); ctx.lineTo(-48, 13); ctx.stroke(); // jejak air ctx.fillStyle = '#3b2c25'; ctx.beginPath(); ctx.moveTo(-24, 0); ctx.lineTo(24, 0); ctx.lineTo(17, 8); ctx.lineTo(-17, 8); ctx.closePath(); ctx.fill(); // lambung ctx.fillStyle = '#f2e8cf'; ctx.beginPath(); ctx.moveTo(1, -2); ctx.lineTo(1, -38); ctx.lineTo(21, -4); ctx.closePath(); ctx.fill(); // layar utama ctx.fillStyle = '#e2d3b0'; ctx.beginPath(); ctx.moveTo(-2, -4); ctx.lineTo(-2, -30); ctx.lineTo(-17, -4); ctx.closePath(); ctx.fill(); // layar depan ctx.fillStyle = '#4a3a30'; ctx.fillRect(-.5, -40, 1.6, 40); // tiang pop(ctx); } } } function dust(ctx, A, k, ga) { // debu cahaya melayang pelan (Sunspire emas, Elderwood hijau keemasan) const D = A.dust, n = Math.round(D.n * (E.light ? .5 : 1)), T = E.T, spr = glow(D.col), span = E.uR - E.uL, v0 = Math.max(D.v0, E.vT + 10), v1 = Math.max(v0 + 30, Math.min(D.v1, E.vB - 10)); ctx.globalCompositeOperation = 'lighter'; for (let i = 0; i < n; i++) { const x = E.uL + fract((rnd(6, i, 1) * span + T * (2 + 3 * rnd(6, i, 2))) / span) * span, y = lerp(v0, v1, rnd(6, i, 3)) + Math.sin(T * (.2 + .3 * rnd(6, i, 4)) + rnd(6, i, 5) * TAU) * 14; const tw = .5 + .5 * Math.sin(T * (.6 + .9 * rnd(6, i, 6)) + rnd(6, i, 7) * TAU), s = 6 + 9 * rnd(6, i, 8); ctx.globalAlpha = (.12 + .38 * tw) * ga * k; ctx.drawImage(spr, x - s / 2, y - s / 2, s, s); } ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1; } const LEAF = ['#7fb84a', '#a6c95a', '#d7c05a', '#5f9d4a', '#c9a24a', '#f4a9c7']; // warna daun dan kelopak (indeks 5 = kelopak merah muda) yang jatuh pelan di Elderwood function leaves(ctx, A, ga) { const T = E.T, n = Math.round(A.leaves.n * (E.light ? .55 : 1)), span = E.uR - E.uL; for (let i = 0; i < n; i++) { const per = 12 + 10 * rnd(7, i, 1), ph = rnd(7, i, 2) * per, c = Math.floor((T + ph) / per), age = (T + ph) / per - c, id = i + c * 64; const x = E.uL + rnd(7, id, 3) * span + Math.sin(T * (.6 + .8 * rnd(7, id, 4)) + rnd(7, id, 5) * TAU) * 28 + age * per * (-8 + 16 * rnd(7, id, 6)), y = lerp(E.vT - 16, E.gy + 28, age); const flip = Math.abs(Math.cos(T * (1.2 + 1.2 * rnd(7, id, 7)) + rnd(7, id, 8) * TAU)), sz = 4.5 + 4 * rnd(7, id, 9), ci = Math.floor(rnd(7, id, 10) * LEAF.length); ctx.globalAlpha = Math.min(1, age * 10) * Math.min(1, (1 - age) * 7) * .9 * ga; ctx.fillStyle = LEAF[ci]; ctx.beginPath(); ctx.ellipse(x, y, ci === 5 ? sz * .75 : sz, Math.max(.8, sz * .42 * flip), T * (.7 + 1.1 * rnd(7, id, 11)) + rnd(7, id, 12) * TAU, 0, TAU); ctx.fill(); } ctx.globalAlpha = 1; } function fireflies(ctx, A, ga) { const n = Math.round(((A.fire && A.fire.n) || 12) * (E.light ? .5 : 1)), T = E.T, spr = glow('fly'), span = E.uR - E.uL, v0 = Math.max(E.vT + 40, E.gy - 280), v1 = Math.max(v0 + 40, Math.min(E.vB - 20, E.gy + 20)); ctx.globalCompositeOperation = 'lighter'; for (let i = 0; i < n; i++) { const x = E.uL + rnd(8, i, 1) * span + Math.sin(T * (.22 + .3 * rnd(8, i, 3)) + rnd(8, i, 4) * TAU) * 30 + Math.sin(T * .09 + rnd(8, i, 5) * TAU) * 44; const y = lerp(v0, v1, rnd(8, i, 2)) + Math.cos(T * (.27 + .3 * rnd(8, i, 6)) + rnd(8, i, 7) * TAU) * 18; const bl = .5 + .5 * Math.sin(T * (.7 + 1.1 * rnd(8, i, 8)) + rnd(8, i, 9) * TAU), a = smooth(clamp((bl - .2) / .6, 0, 1)), s = 14 + 14 * rnd(8, i, 10); if (a < .03) continue; ctx.globalAlpha = a * .9 * ga; ctx.drawImage(spr, x - s / 2, y - s / 2, s, s); ctx.globalAlpha = a * ga; ctx.drawImage(spr, x - 3, y - 3, 6, 6); } ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1; } function shooting(ctx, ga) { // bintang jatuh sesekali (tiap 31 dtk, 0,9 dtk) const per = 31, c = Math.floor((E.t + 9) / per), age = E.t + 9 - c * per; if (age > .9) return; const p = age / .9, dx = -330, dy = 190, hx = lerp(E.uL + 100, E.uR - 200, rnd(9, c, 1)) + dx * p, hy = E.vT + 20 + 70 * rnd(9, c, 2) + dy * p, tx = hx - dx * .22, ty = hy - dy * .22; const g = ctx.createLinearGradient(tx, ty, hx, hy); g.addColorStop(0, 'rgba(255,255,255,0)'); g.addColorStop(1, 'rgba(255,255,255,.95)'); ctx.globalAlpha = Math.sin(p * Math.PI) * ga; ctx.strokeStyle = g; ctx.lineWidth = 1.6; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(tx, ty); ctx.lineTo(hx, hy); ctx.stroke(); ctx.globalAlpha = 1; } // Hujan: garis miring, satu goresan per lapisan; posisi = fungsi waktu (membungkus di tepi layar). far = di belakang petarung, dekat = di depan. function rain(ctx, far, k) { const vw = E.vw, vh = E.vh, T = E.T, n = Math.round((far ? 64 : 48) * (E.light ? .5 : 1) * k), spd = (far ? .85 : 1.25) * vh, len = (far ? .03 : .052) * vh, sl = -.2, m = Math.hypot(sl, 1), ux = sl / m, uy = 1 / m; const PW = vw + 80, PH = vh + len + 20, salt = far ? 11 : 12; ctx.beginPath(); for (let i = 0; i < n; i++) { const x = fract(rnd(salt, i, 1) + T * spd * ux / PW) * PW - 40, y = fract(rnd(salt, i, 2) + T * spd * uy / PH) * PH - len; ctx.moveTo(x, y); ctx.lineTo(x + ux * len, y + uy * len); } ctx.lineWidth = far ? 1 : 1.3; ctx.strokeStyle = far ? 'rgba(210,225,240,.3)' : 'rgba(225,238,250,.42)'; ctx.lineCap = 'butt'; ctx.stroke(); // garis tipis tanpa ujung bulat: jauh lebih murah dirasterisasi } function splashes(ctx, ga) { // riak kecil di lantai (koordinat dunia) const n = E.light ? 8 : 16, T = E.T, depth = clamp(E.wB - E.gy - 10, 8, 86); ctx.lineWidth = 1.4; ctx.strokeStyle = 'rgb(225,238,250)'; for (let i = 0; i < n; i++) { const per = .5 + .45 * rnd(13, i, 1), ph = rnd(13, i, 2) * per, c = Math.floor((T + ph) / per), age = (T + ph) / per - c, id = i + c * 64, r = 2 + 9 * age; ctx.globalAlpha = (1 - age) * (1 - age) * .5 * ga; ctx.beginPath(); ctx.ellipse(lerp(E.wL, E.wR, rnd(13, id, 3)), E.gy + 6 + rnd(13, id, 4) * depth, r, r * .32, 0, 0, TAU); ctx.stroke(); } ctx.globalAlpha = 1; } // Pita kabut: ubin selebar layar dibuat sekali (dari tekstur dasar) lalu digambar 1:1 pada x bulat dan digeser pelan (koordinat layar). which: 0 belakang bawah, 1 belakang atas, 2 depan. const FOGB = [[-.27, .36, .5, 14, 0], [-.54, .3, .3, -9, 1], [-.1, .24, .2, 22, 2]], FOGT = [null, null, null]; function fogTile(which, tone, v, tw, th) { const key = tone + v + ':' + tw + 'x' + th, e = FOGT[which]; if (e && e.key === key) return e.c; const c = document.createElement('canvas'); c.width = tw; c.height = th; c.getContext('2d').drawImage(fogTex(tone, v), 0, 0, tw, th); FOGT[which] = { key, c }; return c; } function fogBand(ctx, which, k) { const f = FOGB[which], tone = E.A.night ? 'night' : E.wx === 'hujan' ? 'gray' : 'day', th = Math.max(8, Math.round(f[1] * E.vh)), tw = Math.max(64, Math.round(E.vw)), top = Math.round(E.oy + E.gy * E.sc + E.shy + f[0] * E.vh); const tile = fogTile(which, tone, f[4], tw, th); ctx.globalAlpha = clamp(f[2] * k, 0, 1); for (let x = -Math.floor(fract((E.T * f[3] * E.sc) / tw + .13 * which) * tw); x < E.vw; x += tw) ctx.drawImage(tile, x, top); ctx.globalAlpha = 1; } function spotlight(ctx, P, k, ga) { // genangan cahaya di lantai yang mengikuti petarung (dunia; source-over supaya murah) const g = window.__game, hx = g && g.hero && g.hero.x, dx = g && g.dummy && g.dummy.x, mid = typeof hx === 'number' && typeof dx === 'number' && isFinite(hx + dx) ? (hx + dx) / 2 : 640; E.spot += (mid - E.spot) * Math.min(1, E.dt * 3); push(ctx); ctx.translate(E.ox + E.shx, E.oy + E.shy); ctx.scale(E.sc, E.sc); ctx.globalAlpha = clamp((P.spot === 'warm' ? .5 : .62) * k * (E.A.night ? .55 : 1) * ga, 0, 1); ctx.drawImage(spotSpr(P.spot), E.spot - 300, E.gy - 62, 600, 150); pop(ctx); } // ---- Lapisan depan (layar penuh) ---- function drawFront(ctx) { const A = E.A, P = E.P, k = E.wk; push(ctx); ctx.imageSmoothingEnabled = true; crowd(ctx, A); if (P.fog && k >= .75 && !E.light) fogBand(ctx, 2, k); if (P.rain) rain(ctx, false, k); if (P.bolt && !E.light && !E.calm) { // kilat lembut sesekali (tiap 19 dtk, dua kedipan singkat, redup) const age = E.t + 4 - Math.floor((E.t + 4) / 19) * 19, f = age < .12 ? (1 - age / .12) * .16 : age > .2 && age < .3 ? (1 - (age - .2) / .1) * .1 : 0; if (f > 0) { ctx.globalAlpha = f; ctx.fillStyle = 'rgb(205,220,255)'; ctx.fillRect(0, 0, E.vw, E.vh); ctx.globalAlpha = 1; } } pop(ctx); } // Siluet penonton di tepi bawah layar: satu path untuk semua badan per baris. Melompat dan mengangkat tangan menurut semangat C.ex, bertepuk saat kombo besar berakhir. const MX = new Float32Array(64), MY = new Float32Array(64), MS = new Float32Array(64), MH = new Uint8Array(64), HX = new Float32Array(64), HY = new Float32Array(64); function crowd(ctx, A) { const cr = A.crowd || ARENA._.crowd, u = E.sc, light = E.light, calm = E.calm, t = calm ? 0 : E.t, ex = calm ? 0 : C.ex, clap = calm ? 0 : C.clap, holder = cr.acc === 'flag' || cr.acc === 'lantern'; const n = clamp(Math.round(E.vw / (62 * u) * (light ? .7 : 1)), 6, 36), step = E.vw / n, ar = smooth(clamp(ex * 1.7, 0, 1)), cw = clap > 0 ? smooth(Math.min(1, clap * 2)) : 0; for (let row = light ? 0 : 1; row >= 0; row--) { // baris belakang (1, lebih tinggi di layar) lebih dulu, lalu baris depan (0) const back = row === 1, by = E.vh + (back ? -8 : 5) * u, sz0 = back ? .8 : 1; for (let i = 0; i < n; i++) { const id = i + row * 64, f = 2 + 1.5 * rnd(14, id, 5), ph = rnd(14, id, 6) * TAU; const jump = ex > .02 ? ex * (8 + 10 * rnd(14, id, 7)) * u * Math.abs(Math.sin(t * f * Math.PI + ph)) : 0; MX[i] = (i + .5 + (rnd(14, id, 3) - .5) * .7 + (back ? .5 : 0)) * step; MY[i] = by - jump + Math.sin(t * .8 + ph) * .8 * u; MS[i] = sz0 * (.9 + .24 * rnd(14, id, 4)) * u * 1.1; MH[i] = rnd(14, id, 8) < (cr.acc === 'hat' ? .4 : .3) ? 1 : 0; // pembawa aksesori (topi kerucut, bendera, lentera) } ctx.globalAlpha = back ? .74 : .94; ctx.fillStyle = cr.tone; ctx.strokeStyle = cr.tone; ctx.beginPath(); for (let i = 0; i < n; i++) { const x = MX[i], y = MY[i], s = MS[i]; ctx.moveTo(x + 21 * s, y - 9 * s); ctx.ellipse(x, y - 9 * s, 21 * s, 15 * s, 0, 0, TAU); ctx.moveTo(x + 11 * s, y - 34 * s); ctx.arc(x, y - 34 * s, 11 * s, 0, TAU); } ctx.fill(); if (cr.acc === 'hat') { // topi kerucut ctx.beginPath(); for (let i = 0; i < n; i++) if (MH[i]) { const x = MX[i], y = MY[i], s = MS[i]; ctx.moveTo(x - 19 * s, y - 41 * s); ctx.lineTo(x, y - 62 * s); ctx.lineTo(x + 19 * s, y - 41 * s); ctx.closePath(); } ctx.fill(); } // Lengan: terangkat saat bersemangat, merapat di depan dada saat bertepuk. Pembawa bendera/lentera menahan satu tangan (kanan) walau penonton tenang. ctx.beginPath(); ctx.lineWidth = 7 * sz0 * u; ctx.lineCap = 'round'; for (let i = 0; i < n; i++) { const hold = holder && MH[i] === 1; if (ar < .03 && !hold) continue; const x = MX[i], y = MY[i], s = MS[i]; for (let d = -1; d <= 1; d += 2) { if (d < 0 && ar < .03) continue; const hx = x + d * ((22 + 6 * Math.sin(t * 9 + i * 1.9 + d)) * (1 - cw) + (6 + 10 * (.5 + .5 * Math.sin(t * 15 + i))) * cw) * s, hy = y - (24 + 30 * ar * (1 - .55 * cw)) * s; ctx.moveTo(x + d * 15 * s, y - 17 * s); ctx.lineTo(hx, hy); if (d > 0) { HX[i] = hx; HY[i] = hy; } } } ctx.stroke(); if (!light && !back) { // cahaya tepi di atas kepala ctx.globalAlpha = .45; ctx.strokeStyle = cr.rim; ctx.lineWidth = 1.6 * u; ctx.beginPath(); for (let i = 0; i < n; i++) { const x = MX[i], y = MY[i], s = MS[i]; ctx.moveTo(x + 11 * s * Math.cos(3.5), y - 34 * s + 11 * s * Math.sin(3.5)); ctx.arc(x, y - 34 * s, 11 * s, 3.5, 5.9); } ctx.stroke(); } if (!back && holder) accessories(ctx, cr, n, u, t); } ctx.globalAlpha = 1; } function accessories(ctx, cr, n, u, t) { // bendera kecil atau lentera di tangan kanan pembawa for (let i = 0; i < n; i++) { if (!MH[i]) continue; const s = MS[i], hx = HX[i], hy = HY[i]; if (cr.acc === 'flag') { const sw = Math.sin(t * 5 + i) * 3 * s, tx = hx + sw * .3; ctx.globalAlpha = .95; ctx.strokeStyle = cr.tone; ctx.lineWidth = 2 * u; ctx.beginPath(); ctx.moveTo(hx, hy + 4 * s); ctx.lineTo(tx, hy - 30 * s); ctx.stroke(); ctx.fillStyle = cr.flags[i % cr.flags.length]; ctx.beginPath(); ctx.moveTo(tx, hy - 30 * s); ctx.lineTo(tx + 17 * s + sw, hy - 25 * s); ctx.lineTo(tx, hy - 19 * s); ctx.closePath(); ctx.fill(); } else { const g = 34 * s; ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = .9; ctx.drawImage(glow('warm'), hx - g / 2, hy - 8 * s - g / 2, g, g); ctx.globalCompositeOperation = 'source-over'; } } } // ---- Menu: baris Cuaca di layar pilih arena ---- let syncRow = null; function decorate(root, screen) { try { if (screen !== 'arena' || !root || root.querySelector('.arena-row')) return; const btn = root.querySelector('.start-battle'); if (!btn) return; const row = document.createElement('div'), box = document.createElement('div'); row.className = 'arena-row'; box.className = 'arena-weather'; box.setAttribute('role', 'group'); box.setAttribute('aria-label', 'Cuaca arena'); box.innerHTML = 'CUACA
' + PICKS.map(id => '').join('') + '
'; btn.before(row); row.append(box, btn); const stage = root.querySelector('.arena-tile.selected')?.dataset.stage || 'bellora', note = box.querySelector('.aw-note'); const sync = () => { const off = S.fx === 'off'; box.querySelectorAll('button[data-w]').forEach(b => { const on = b.dataset.w === S.pick; b.classList.toggle('selected', on); b.setAttribute('aria-pressed', String(on)); b.disabled = off; }); note.textContent = off ? 'Efek latar dimatikan di Pengaturan' : S.pick === 'auto' ? 'Otomatis: ' + autoLabel(stage) : 'Tampilan saja, tanpa memengaruhi pertarungan'; }; box.addEventListener('click', ev => { const b = ev.target.closest('button[data-w]'); if (!b || b.disabled) return; S.pick = b.dataset.w; sync(); }); syncRow = sync; sync(); } catch (err) { console.error('[arena] menu:', err); } } // ---- Pengaturan: baris Efek latar ---- function setFx(v) { if (!FX.includes(v)) return false; S.fx = v; save(); const sel = document.getElementById('arena-fx'); if (sel) sel.value = v; try { syncRow?.(); } catch (_) { /* baris menu sudah dilepas */ } return true; } function mountSettings() { const panel = document.getElementById('settings-panel'); if (!panel || panel.querySelector('#arena-fx-row')) return; const row = document.createElement('label'); row.className = 'setting-row'; row.id = 'arena-fx-row'; row.innerHTML = 'Efek latarAwan, burung, cuaca, dan penonton di belakang petarung. Pilih Ringan atau Mati bila game terasa berat.'; const sel = row.querySelector('select'); for (const id of FX) { const o = document.createElement('option'); o.value = id; o.textContent = FXNAME[id] + (id === DEF_FX ? ' (bawaan)' : ''); sel.append(o); } sel.value = S.fx; sel.addEventListener('change', () => setFx(sel.value)); (panel.querySelector('.dialog-buttons') || panel.lastElementChild).before(row); } // Cuaca per pertandingan: menu.js mengirim {weather} lewat startMatch; pembungkus ini mengingatnya hanya bila pertandingan benar-benar mulai. // Arcade, Survival, tutorial, dan pemanggil lain tidak mengirim apa pun, jadi memakai Otomatis (deterministik dari arena). function hookGame() { const g = window.__game; if (!g || typeof g.startMatch !== 'function' || g.startMatch.__arena) return; const sm = g.startMatch, wrapped = function (o) { const w = o && o.weather, ok = sm.apply(this, arguments); if (ok) S.active = PICKS.includes(w) ? w : 'auto'; return ok; }; wrapped.__arena = true; g.startMatch = wrapped; } load(); try { mountSettings(); } catch (err) { console.error('[arena] pengaturan:', err); } const boot = () => { try { hookGame(); mountSettings(); } catch (err) { console.error('[arena] awal:', err); } }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); window.Arena = { back, front, decorate, setFx, names: NAME, get pick() { return S.pick; }, set pick(v) { if (PICKS.includes(v)) S.pick = v; syncRow?.(); }, get fx() { return S.fx; }, force(w) { S.forced = PICKS.includes(w) && w !== 'auto' ? w : null; return S.forced; }, state() { return { on: E.on, dead: E.dead, fails: E.fails, stage: E.stage, weather: E.wx, strength: E.wk, fx: S.fx, pick: S.pick, active: S.active, forced: S.forced, calm: E.calm, night: E.night, crowd: { ...C } }; } }; })();