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

106 lines
3.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============ 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;
}
}