skatemakerpro/js/colliders.js
type-two d68fa84f92 Kit thumbnails + prop colliders
js/kitthumbs.js renders each kit GLB to a 128x96 sprite once (offscreen
renderer, framed by bounding sphere), POSTs to /api/kit-thumb, and all 52
are committed under assets/kit_thumbs/ so every checkout gets instant thumbs;
the generator only runs for props without one (future kit drops). KIT drawer
shows them with category groups and filter.

js/colliders.js derives world-space colliders from kit manifest hints +
a table for builtin props (bins, planters, trunks, fences...). Test ride
resolves circle-vs-box/circle push-out — you scrub and stop instead of
riding through furniture. Exports gain an additive COLLIDERS array for
bookquoy to adopt when its player is ready; existing contract untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 21:13:36 +10:00

87 lines
3.6 KiB
JavaScript

// SKATEMAKER PRO — prop colliders. Furniture you can't ride through: bins, planters,
// tree trunks, bollards. Derived (never stored): kit props carry a `collider` hint in
// the manifest; builtin props have a table here. Test ride resolves rider-vs-collider
// as 2D circle-vs-shape push-out, and exports embed the world-space list as COLLIDERS
// so the game can adopt the same rule when it's ready (additive — nothing existing
// in the level.js contract changes).
//
// world entry: { type:'box', x, z, rot, hw, hd, h } | { type:'circle', x, z, r, h }
export const BUILTIN_COLLIDERS = {
jersey: { type: 'box', hw: 0.31, hd: 1.0, h: 0.94 },
picnic: { type: 'box', hw: 0.8, hd: 0.77, h: 0.78 },
bench: { type: 'box', hw: 0.8, hd: 0.22, h: 0.75 },
trashcan: { type: 'circle', r: 0.36, h: 0.96 },
hydrant: { type: 'circle', r: 0.18, h: 0.78 },
fence: { type: 'box', hw: 1.2, hd: 0.06, h: 1.85 },
floodlight: { type: 'circle', r: 0.13, h: 6.5 },
bleachers: { type: 'box', hw: 1.5, hd: 0.84, h: 1.0 },
fountain: { type: 'circle', r: 0.23, h: 0.9 },
fig: { type: 'circle', r: 0.45, h: 2.6 },
// parkingblock / grate / shadesail posts: low or sparse — ride over/through is fine
};
// kit manifest collider -> normalized local shape
function kitShape(c) {
if (!c || c.type === 'none') return null;
if (c.type === 'cylinder') return { type: 'circle', r: c.r, h: c.h };
if (c.type === 'box') return { type: 'box', hw: c.hw, hd: c.hd, h: c.h };
return null;
}
// all world-space colliders for a park. kitMap: id -> manifest def (or null/empty)
export function propColliders(park, kitMap = {}) {
const out = [];
for (const pr of park.props || []) {
let shape = null;
if (pr.src.startsWith('builtin:')) shape = BUILTIN_COLLIDERS[pr.src.slice(8)];
else if (pr.src.startsWith('kit:')) shape = kitShape(kitMap[pr.src.slice(4)]?.collider);
if (!shape) continue;
const sc = pr.scale || 1;
const e = { ...shape, x: pr.x, z: pr.z, rot: pr.rot || 0, h: shape.h * sc };
if (e.type === 'circle') e.r *= sc; else { e.hw *= sc; e.hd *= sc; }
out.push(e);
}
return out;
}
// push a rider circle (px, pz, radius R, foot height y) out of colliders.
// returns null (no contact) or the corrected [x, z].
export function resolveCollision(colliders, px, pz, R, y) {
let hit = null;
for (const c of colliders) {
if (y > c.h) continue; // sailing over it
if (c.type === 'circle') {
const dx = px - c.x, dz = pz - c.z;
const d = Math.hypot(dx, dz), min = c.r + R;
if (d < min) {
const k = d > 1e-4 ? (min / d) : 1, nx = d > 1e-4 ? dx / d : 1;
px = c.x + (d > 1e-4 ? dx * k : min); pz = c.z + (d > 1e-4 ? dz * k : 0);
hit = true;
}
} else {
const co = Math.cos(c.rot), sn = Math.sin(c.rot);
const dx = px - c.x, dz = pz - c.z;
const lu = dx * co - dz * sn, lv = dx * sn + dz * co; // three Y-rot inverse
const cu = Math.max(-c.hw, Math.min(c.hw, lu));
const cv = Math.max(-c.hd, Math.min(c.hd, lv));
const du = lu - cu, dv = lv - cv;
const d = Math.hypot(du, dv);
if (d < R) {
let nu, nv;
if (d > 1e-4) { nu = du / d; nv = dv / d; }
else { // inside: push out shortest face
const eu = c.hw - Math.abs(lu), ev = c.hd - Math.abs(lv);
if (eu < ev) { nu = Math.sign(lu) || 1; nv = 0; }
else { nu = 0; nv = Math.sign(lv) || 1; }
}
const ou = cu + nu * R, ov = cv + nv * R;
px = c.x + ou * co + ov * sn;
pz = c.z - ou * sn + ov * co;
hit = true;
}
}
}
return hit ? [px, pz] : null;
}