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
This commit is contained in:
2026-08-23 06:59:21 +00:00
commit ac00687480
30 changed files with 6772 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
// ============ map.js — tile world, terrain gen, object placement ============
import { makeRng } from '../core/util.js';
export const T_GRASS = 0, T_SAND = 1, T_ROCK = 2, T_WATER = 3;
export class GameMap {
constructor(size = 52) {
this.size = size;
this.terrain = new Uint8Array(size * size); // terrain type
this.pathType = new Uint8Array(size * size); // 0 none, 1 pavement, 2 cobble
this.litter = new Float32Array(size * size); // 0..1 litter amount
this.vomit = new Float32Array(size * size);
// occupancy: null or {kind:'ride'|'shop'|'scenery'|'guild', id, ox, oy} where id indexes state arrays
this.objects = new Array(size * size).fill(null);
// coaster track occupies cells too: {kind:'track', rideId, pieceIndex}
this.beauty = new Float32Array(size * size); // accumulated scenery beauty for rating
}
idx(x, y) { return y * this.size + x; }
inBounds(x, y) { return x >= 0 && y >= 0 && x < this.size && y < this.size; }
terrainAt(x, y) { return this.inBounds(x, y) ? this.terrain[this.idx(x, y)] : -1; }
isPath(x, y) { return this.inBounds(x, y) && this.pathType[this.idx(x, y)] > 0; }
isWalkable(x, y) {
if (!this.inBounds(x, y)) return false;
const i = this.idx(x, y);
if (this.pathType[i] > 0) return true;
return false;
}
isBuildable(x, y) {
if (!this.inBounds(x, y)) return false;
const i = this.idx(x, y);
if (this.terrain[i] === T_WATER) return false;
return true;
}
occupied(x, y) {
if (!this.inBounds(x, y)) return true;
return this.objects[this.idx(x, y)] !== null;
}
setObject(x, y, obj) { this.objects[this.idx(x, y)] = obj; }
getObject(x, y) { return this.inBounds(x, y) ? this.objects[this.idx(x, y)] : { kind: 'out' }; }
clearObject(x, y) { this.objects[this.idx(x, y)] = null; }
/** Generate terrain from scenario gen spec */
generate(gen, seed) {
const rng = makeRng(seed);
const n = this.size;
// value-noise-ish rolling grass with rock patches
const rockNoise = makeRng(seed ^ 0x9e3779b9);
const lakeCount = gen.lake ?? 1;
const lakes = [];
for (let i = 0; i < lakeCount; i++) {
lakes.push({ x: 8 + rng() * (n - 20), y: 8 + rng() * (n - 20), r: 4 + rng() * 4 });
}
// entrance at south edge center
this.entranceX = Math.floor(n / 2);
this.entranceY = n - 3;
for (let y = 0; y < n; y++) {
for (let x = 0; x < n; x++) {
const i = this.idx(x, y);
let t = T_GRASS;
// rocky regions
if (gen.rocks) {
const v = rockNoise();
if (v < 0.10 + (gen.rocky ? 0.14 : 0)) t = T_ROCK;
} else if (rng() < 0.03) t = T_ROCK;
// sand near water
for (const L of lakes) {
const d = Math.hypot(x - L.x, y - L.y);
if (d < L.r) t = T_WATER;
else if (d < L.r + 1.6 && gen.sand) t = T_SAND;
}
this.terrain[i] = t;
}
}
// scatter rocks as scenery later by renderer using deterministic rng
this.scatterSeed = seed;
}
/** find a free rect area of w×h buildable & unoccupied tiles near cx,cy */
findFreeRect(w, h, cx, cy, maxR = 40) {
for (let r = 0; r < maxR; r++) {
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
const x = cx + dx, y = cy + dy;
let ok = true;
for (let yy = 0; yy < h && ok; yy++) {
for (let xx = 0; xx < w && ok; xx++) {
if (!this.isBuildable(x + xx, y + yy) || this.occupied(x + xx, y + yy)) ok = false;
}
}
if (ok) return { x, y };
}
}
}
return null;
}
countLitter() {
let c = 0;
for (let i = 0; i < this.litter.length; i++) if (this.litter[i] > 0.25) c++;
return c;
}
}