skatemakerpro/js/parkcheck.js
type-two 7a00c9f9a7 Library expansion: full obstacle set, polyline rails, street props
New heightfield kinds: hip/corner quarter, kicker (tunable concavity), roller,
wedge/manny pad, volcano, round-bowl option, quarter vert extension. Rails are
now polylines (kink/dbl-kink/rainbow/A-frame/pole-jam/donkey presets, round or
square profile, per-node drag handles) that flatten to straight segments on
export so bookquoy grind detection is untouched. 13 parametric street props
(jersey barrier, picnic table, parking block, fence, floodlight, bleachers...).
Palette regrouped TRANSITION/STREET/STEEL/PROPS; park check covers new kinds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 20:19:08 +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']);
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;
}