537 lines
43 KiB
JavaScript
537 lines
43 KiB
JavaScript
/* 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 = '<span class="eyebrow">CUACA <em class="aw-note"></em></span><div class="aw-chips">' + PICKS.map(id => '<button type="button" data-w="' + id + '">' + NAME[id] + '</button>').join('') + '</div>';
|
|
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 = '<span>Efek latar<small>Awan, burung, cuaca, dan penonton di belakang petarung. Pilih Ringan atau Mati bila game terasa berat.</small></span><select id="arena-fx"></select>';
|
|
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 } }; }
|
|
};
|
|
})();
|