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

120 lines
3.9 KiB
JavaScript

// ============ path.js — BFS pathfinding over path tiles ============
const DIRS = [[1, 0], [0, 1], [-1, 0], [0, -1]];
/**
* BFS from (sx,sy) to (tx,ty) over walkable path tiles.
* Returns array of [x,y] steps (excluding start, including target) or null.
*/
export function findPath(map, sx, sy, tx, ty, maxNodes = 6000) {
if (!map.isWalkable(sx, sy) || !map.isWalkable(tx, ty)) return null;
if (sx === tx && sy === ty) return [];
const size = map.size;
const prev = new Int32Array(size * size).fill(-1);
const visited = new Uint8Array(size * size);
const startIdx = sy * size + sx;
const queue = [startIdx];
visited[startIdx] = 1;
let head = 0, nodes = 0;
const targetIdx = ty * size + tx;
while (head < queue.length && nodes < maxNodes) {
const cur = queue[head++];
nodes++;
if (cur === targetIdx) break;
const cx = cur % size, cy = (cur / size) | 0;
for (const [dx, dy] of DIRS) {
const nx = cx + dx, ny = cy + dy;
if (nx < 0 || ny < 0 || nx >= size || ny >= size) continue;
const ni = ny * size + nx;
if (visited[ni] || !map.isWalkable(nx, ny)) continue;
visited[ni] = 1;
prev[ni] = cur;
queue.push(ni);
}
}
if (!visited[targetIdx]) return null;
const out = [];
let cur = targetIdx;
while (cur !== startIdx) {
out.push([cur % size, (cur / size) | 0]);
cur = prev[cur];
if (cur < 0) return null;
}
out.reverse();
return out;
}
/** BFS flood to collect all reachable path tiles within radius r of (x,y) */
export function reachableWithin(map, x, y, r, outSet) {
const size = map.size;
const seen = outSet || new Set();
const startIdx = y * size + x;
if (!map.isWalkable(x, y)) return seen;
const q = [[x, y, 0]];
seen.add(startIdx);
let head = 0;
while (head < q.length) {
const [cx, cy, d] = q[head++];
if (d >= r) continue;
for (const [dx, dy] of DIRS) {
const nx = cx + dx, ny = cy + dy;
if (!map.isWalkable(nx, ny)) continue;
const ni = ny * size + nx;
if (seen.has(ni)) continue;
seen.add(ni);
q.push([nx, ny, d + 1]);
}
}
return seen;
}
/** Find nearest tile satisfying predicate via expanding ring search on paths */
export function findNearestPathTile(map, x, y, pred, maxR = 30) {
for (let r = 0; r <= maxR; r++) {
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
if (Math.max(Math.abs(dx), Math.abs(dy)) !== r) continue;
const nx = x + dx, ny = y + dy;
if (map.isWalkable(nx, ny) && pred(nx, ny)) return [nx, ny];
}
}
}
return null;
}
/** Random reachable path tile within radius (for wandering) */
export function randomNearbyPath(map, rng, x, y, minR = 3, maxR = 12) {
const r = minR + Math.floor(rng() * (maxR - minR));
const cands = [];
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
const nx = Math.round(x + dx), ny = Math.round(y + dy);
if (Math.abs(dx) + Math.abs(dy) > r || Math.abs(dx) + Math.abs(dy) < minR * 0.6) continue;
if (map.isWalkable(nx, ny)) cands.push([nx, ny]);
}
}
if (!cands.length) {
// fallback: any adjacent path
for (const [dx, dy] of DIRS) {
const nx = Math.round(x + dx), ny = Math.round(y + dy);
if (map.isWalkable(nx, ny)) return [nx, ny];
}
return null;
}
return cands[Math.floor(rng() * cands.length)];
}
/** Snap a world position to the nearest walkable path tile (searching outward) */
export function snapToPath(map, x, y, maxR = 4) {
const rx = Math.round(x), ry = Math.round(y);
if (map.isWalkable(rx, ry)) return [rx, ry];
for (let r = 1; r <= maxR; r++) {
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
if (Math.max(Math.abs(dx), Math.abs(dy)) !== r) continue;
if (map.isWalkable(rx + dx, ry + dy)) return [rx + dx, ry + dy];
}
}
}
return [Math.min(map.size - 1, Math.max(0, rx)), Math.min(map.size - 1, Math.max(0, ry))];
}