skatemakerpro/js/parkcheck.js
type-two a962ac41d9 Modular custom bowls: polygon pools with per-vertex depths
New bowlpoly element: the rim is a polygon, every vertex a draggable handle
with its own depth slider. Inside/outside corners, hips, shallow-to-deep
escalators and waterfall drops all come from the same continuous carve (IDW
floor blend, quarter-pipe wall transition) — no seams by construction. Auto
pool coping (bullnose tube, exported as grindable ledge segments per rim
edge) and tile band baked into the ground texture. Ships parks/pool_demo.json
(THE POOLROOM) as a premade showcase; ridden end-to-end in test mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 20:26:47 +10:00

106 lines
4.7 KiB
JavaScript
Raw 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.

// SKATEMAKER PRO — park design linter. Encodes the real-park design brief
// (docs/DESIGN_PRINCIPLES.md) as live checks over the park data. World units are
// metres: street trannies want r 1.8-2.4, bowls 2.7-3.4, run-outs 3-4.5m of flat.
import { makeHeightAt, elementOutline } from './parkmath.js';
const STREET_KINDS = new Set(['ledge', 'funbox', 'stairs', 'pyramid', 'wedgepad']);
const TRANNY_KINDS = new Set(['quarter', 'spine', 'bowl', 'bank', 'mogul', 'kicker', 'roller', 'corner', 'volcano', 'bowlpoly']);
export function checkPark(park) {
const out = [];
const warn = (sel, msg) => out.push({ sel, msg });
const heightAt = makeHeightAt(park);
const els = park.elements || [];
// --- pumping geometry: transition radii in the natural-feel band
for (const e of els) {
if (e.kind === 'quarter') {
const r = Math.min(e.r0, e.r1);
if (r < 0.9) warn({ type: 'element', id: e.id },
`tight quarter (r ${r}) — pumping geometry wants ~1.82.4m for street flow`);
}
if (e.kind === 'spine' && e.r < 0.9)
warn({ type: 'element', id: e.id },
`tight spine (r ${e.r}) — hard to pump; 1.2+ feels natural`);
if (e.kind === 'bowl' && e.depth > 1.2 && e.depth > e.size * 0.45)
warn({ type: 'element', id: e.id },
`bowl walls near-vertical (depth ${e.depth} vs size ${e.size}) — deep bowls want 2.73.4m radii`);
}
// --- run-outs: flat clear concrete along the facing direction of ramps
const isObstacleAt = (x, z, ignore) => {
const B = park.bounds;
if (x < B.x0 || x > B.x1 || z < B.z0 || z > B.z1) return true; // grass = no run-out
return heightAt(x, z) > 0.16;
};
for (const e of els) {
if (!['quarter', 'bank', 'stairs', 'kicker'].includes(e.kind)) continue;
const rot = e.rot || 0, c = Math.cos(rot), s = Math.sin(rot);
const start = e.kind === 'quarter' ? Math.max(e.r0, e.r1)
: e.kind === 'bank' || e.kind === 'kicker' ? e.run
: e.steps * e.going;
let clear = 0;
for (let d = 0.5; d <= 4.5; d += 0.5) {
const v = start + d;
const x = e.x - v * s, z = e.z + v * c; // +v dir = (-sin, cos)
if (isObstacleAt(x, z)) break;
clear = d;
}
if (clear < 3) warn({ type: 'element', id: e.id },
`short run-out (${clear.toFixed(1)}m clear) — landings want 34.5m of flat before the next thing`);
}
// --- collision-free traffic: heavy footprint overlaps between big elements
const boxes = els.filter(e => e.kind !== 'mogul' && e.kind !== 'dome').map(e => {
const pts = elementOutline(e);
const xs = pts.map(p => p[0]), zs = pts.map(p => p[1]);
return { e, x0: Math.min(...xs), x1: Math.max(...xs), z0: Math.min(...zs), z1: Math.max(...zs) };
});
for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++) {
const a = boxes[i], b = boxes[j];
const ox = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
const oz = Math.min(a.z1, b.z1) - Math.max(a.z0, b.z0);
if (ox > 1.2 && oz > 1.2)
warn({ type: 'element', id: a.e.id },
`${a.e.kind} overlaps ${b.e.kind} — crossing lines create bail zones (intentional combos are fine)`);
}
// --- skill tiering: someone has to be able to learn here
const ledges = els.filter(e => e.kind === 'ledge');
if (ledges.length >= 2 && !ledges.some(e => e.h <= 0.28))
warn(null, 'no micro-ledge (≤0.25m) — beginners need low tier features off the main lines');
const qs = els.filter(e => e.kind === 'quarter');
if (qs.length >= 2 && !qs.some(e => Math.min(e.r0, e.r1) <= 1.0))
warn(null, 'all quarters are big — add a mellow one so beginners can drop in');
// --- terrain balance: street vs transition
const street = els.filter(e => STREET_KINDS.has(e.kind)).length;
const tranny = els.filter(e => TRANNY_KINDS.has(e.kind)).length;
if (street + tranny >= 6) {
if (street === 0) warn(null, 'all transition, no street — add ledges/stairs/flat rails');
if (tranny === 0) warn(null, 'all street, no transition — add quarters/banks/spines');
}
// --- steel: rails floating or buried (any node of the polyline)
for (const r of park.rails || []) {
for (const pt of r.pts || []) {
const h = heightAt(pt.x, pt.z);
if (pt.y < h - 0.05) {
warn({ type: 'rail', id: r.id }, `"${r.name}" is buried in concrete — raise its points`);
break;
}
if (pt.y - h > 1.8) {
warn({ type: 'rail', id: r.id }, `"${r.name}" floats way above the deck — grinds will feel wrong`);
break;
}
}
}
// --- spawn sanity
const sp = park.spawn;
if (heightAt(sp.x, sp.z) > 0.5)
warn({ type: 'spawn' }, 'spawn is on top of an obstacle — riders drop in blind');
return out;
}