Files
deepseek ac00687480 Arcane Tycoon — Heroes & Magic theme park tycoon game
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
2026-08-23 06:59:21 +00:00

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; }
}