Complete browser game inspired by OpenRCT2 with fantasy twist: - Custom roller coaster designer with physics-based ratings + on-ride POV - 10 animated rides, 7 shops, 16 scenery items, path network & guest AI - Heroes guild vs monster invasions (5 classes, XP/gear/bosses) - Magic spell system (8 spells), research tree, economy/marketing/loans - Day-night cycle, weather, park rating, awards, 4 scenarios - Save/load slots + autosave, procedural WebAudio SFX/music - Isometric canvas renderer, minimap, diagnostics overlay - Test suites: smoke(13), linkcheck, inputcheck, framecheck, rendercheck, flow
89 lines
3.1 KiB
JavaScript
89 lines
3.1 KiB
JavaScript
// ============ util.js — shared helpers ============
|
|
|
|
export const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
|
|
export const lerp = (a, b, t) => a + (b - a) * t;
|
|
export const dist2 = (x1, y1, x2, y2) => { const dx = x2 - x1, dy = y2 - y1; return dx * dx + dy * dy; };
|
|
export const dist = (x1, y1, x2, y2) => Math.sqrt(dist2(x1, y1, x2, y2));
|
|
|
|
let _uid = 1;
|
|
export const uid = () => _uid++;
|
|
export function resetUid(v) { _uid = v; }
|
|
export const curUid = () => _uid;
|
|
|
|
/** Mulberry32 seeded RNG */
|
|
export function makeRng(seed) {
|
|
let a = seed >>> 0;
|
|
const fn = function () {
|
|
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
|
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
};
|
|
fn.getState = () => a;
|
|
fn.setState = v => { a = v >>> 0; };
|
|
return fn;
|
|
}
|
|
|
|
export function fmtMoney(n, signed = false) {
|
|
const neg = n < 0;
|
|
let v = Math.abs(Math.round(n));
|
|
let s;
|
|
if (v >= 1e9) s = (v / 1e9).toFixed(2) + 'B';
|
|
else if (v >= 1e6) s = (v / 1e6).toFixed(2) + 'M';
|
|
else s = v.toLocaleString('en-US');
|
|
return (neg ? '-$' : (signed && n > 0 ? '+$' : '$')) + s;
|
|
}
|
|
export function fmtNum(n) { return Math.round(n).toLocaleString('en-US'); }
|
|
|
|
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
export function fmtDate(t) {
|
|
return `${MONTHS[t.month]} ${t.day}, Year ${t.year}`;
|
|
}
|
|
export function fmtClock(t) {
|
|
const h = Math.floor(t.hour), m = Math.floor((t.hour - h) * 60);
|
|
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
}
|
|
|
|
/** pick weighted item from [[item, weight], ...] or array with .w prop */
|
|
export function weightedPick(rng, items) {
|
|
let total = 0;
|
|
for (const it of items) total += (it.w !== undefined ? it.w : it[1]);
|
|
let r = rng() * total;
|
|
for (const it of items) {
|
|
r -= (it.w !== undefined ? it.w : it[1]);
|
|
if (r <= 0) return it.it !== undefined ? it.it : it[0];
|
|
}
|
|
return items[items.length - 1];
|
|
}
|
|
|
|
export const choice = (rng, arr) => arr[Math.floor(rng() * arr.length)];
|
|
export const chance = (rng, p) => rng() < p;
|
|
|
|
/** DOM helper */
|
|
export function el(tag, attrs = {}, ...children) {
|
|
const e = document.createElement(tag);
|
|
for (const [k, v] of Object.entries(attrs)) {
|
|
if (k === 'class') e.className = v;
|
|
else if (k === 'html') e.innerHTML = v;
|
|
else if (k.startsWith('on') && typeof v === 'function') e.addEventListener(k.slice(2).toLowerCase(), v);
|
|
else if (v !== null && v !== undefined) e.setAttribute(k, v);
|
|
}
|
|
for (const c of children.flat()) {
|
|
if (c === null || c === undefined) continue;
|
|
e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
|
}
|
|
return e;
|
|
}
|
|
|
|
export function download(filename, text) {
|
|
try {
|
|
const blob = new Blob([text], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url; a.download = filename;
|
|
document.body.appendChild(a); a.click();
|
|
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 400);
|
|
return true;
|
|
} catch (e) { console.error('download failed', e); return false; }
|
|
}
|