/* ============================================================ * core.js — utilities, isometric math, event bus * ============================================================ */ 'use strict'; const TW = 64, TH = 32; // iso tile width/height const WALL_H = 46; // wall pixel height const LOT_W = 34, LOT_H = 34; // lot size in tiles /* ---------- tiny helpers ---------- */ function clamp(v, a, b) { return v < a ? a : (v > b ? b : v); } function lerp(a, b, t) { return a + (b - a) * t; } function dist2(ax, ay, bx, by) { const dx = ax - bx, dy = ay - by; return dx * dx + dy * dy; } let __uid = 1; function uid() { return __uid++; } function rand(a, b) { return a + Math.random() * (b - a); } function randi(a, b) { return Math.floor(rand(a, b + 1)); } function choice(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function chance(p) { return Math.random() < p; } /** Deterministic-ish name pools for CAS random & visitors */ const FIRST_NAMES_M = ['Mortimer','Bob','Michael','Gunther','Mickey','Goopy','Skip','Dustin','Dirk','Romeo','Puck','Tybalt','Mercutio','Patrizio','Don','Victor']; const FIRST_NAMES_F = ['Bella','Cassandra','Dina','Nina','Cornelia','Riley','Kaylynn','Jenny','Brandi','Angela','Lilith','Hermia','Juliet','Isabel','Kayla','Molly']; const LAST_NAMES = ['Goth','Bachelor','Dreamer','Pleasant','Caliente','Broke','Oldie','Rosalie','Monty','Capp','Summerdream','Smith','Newbie','Grunt','Jacquet','Freshe','Tricou','Moore','Oasis','Lothario']; /* ---------- isometric transforms ---------- * World coordinates: tile floats (x,y). Screen coords pre-camera. */ function isoToScreen(x, y) { return [ (x - y) * TW / 2, (x + y) * TH / 2 ]; } /** screen (pre-camera) -> world tile float */ function screenToIso(sx, sy) { const x = (sx / (TW / 2) + sy / (TH / 2)) / 2; const y = (sy / (TH / 2) - sx / (TW / 2)) / 2; return [x, y]; } /** camera helpers attached to game object G */ function worldToPx(wx, wy) { const [sx, sy] = isoToScreen(wx, wy); return [ sx * G.cam.zoom + G.cam.x, sy * G.cam.zoom + G.cam.y ]; } function pxToWorld(px, py) { const sx = (px - G.cam.x) / G.cam.zoom, sy = (py - G.cam.y) / G.cam.zoom; return screenToIso(sx, sy); } /* Facing directions: 0=S(+y), 1=W(-x), 2=N(-y), 3=E(+x) — matches sprite art */ const DIRS = [ { dx: 0, dy: 1 }, // S { dx: -1, dy: 0 }, // W { dx: 0, dy: -1 }, // N { dx: 1, dy: 0 }, // E ]; function dirFromDelta(dx, dy) { if (Math.abs(dx) >= Math.abs(dy)) return dx > 0 ? 3 : 1; return dy > 0 ? 0 : 2; } /* ---------- colors ---------- */ const SKINS = ['#f6d7c4','#eeb98f','#c98a5e','#8d5a3b']; const HAIRS = ['#2a1c12','#5a3a1e','#a56a2f','#d9b24a','#b23a2a','#777777','#101010','#e8e0cf']; const SHIRTS = ['#d94a4a','#4a72d9','#43b05a','#e8a33d','#8a52c9','#3ab5b0','#e06aa8','#556077','#f0f0f0','#22262e']; const PANTS = ['#2e3542','#3d5a99','#6b4226','#7a7f88','#274832','#803040']; /* ---------- event bus ---------- */ const Bus = { map: {}, on(ev, fn) { (this.map[ev] ||= []).push(fn); }, emit(ev, data) { (this.map[ev] || []).forEach(fn => fn(data)); }, }; /* ---------- money formatting ---------- */ function fmtMoney(n) { return '§' + Math.round(n).toLocaleString('en-US'); } /** trace a rounded-rectangle path (caller fills/strokes) */ function roundRect(ctx, x, y, w, h, r) { r = Math.min(r, w / 2, h / 2); ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); } /* ---------- time helpers ---------- */ function hourTo12(h) { h = ((h % 24) + 24) % 24; const ampm = h < 12 ? 'AM' : 'PM'; let hh = h % 12; if (hh === 0) hh = 12; return hh + ':' + String(G.time.min).padStart(2, '0') + ' ' + ampm; } const DAY_NAMES = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; /* ---------- deep merge for save/load safety ---------- */ function isObj(v) { return v && typeof v === 'object' && !Array.isArray(v); }