kit props go physical: colliders, rideable tops, grinds, gaps, bonks
level.js adopts skatemakerpro's COLLIDERS export shape (box|circle) so
editor parks drop in: box tops join heightAt (land on the picnic table),
circles push out (bins, trunks, hydrants). player.js: hard-edge step
block in roll + bail, blocker resolve in fall, bonk -> stumble + flash
('STRAIGHT INTO THE BIN'). Kit grinds append to RAILS (bench boardslides,
trolley 50-50s); props declare gap triggers vs terrain height (y0).
New Paddo street set: two trolleys, slappy kerbs, planters, parking
block, jersey barrier. Verified: bin stop at contact, planter stand
y=0.50, bench + trolley grinds, 'Tre Flip + TROLLEY GAP + 50-50
(trolley) x3 (2109)'.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7afae85599
commit
f62d83f488
61
js/level.js
61
js/level.js
@ -47,7 +47,52 @@ function spine(x, z, cx0, cx1, cz, r) { // back-to-back quarters a
|
||||
return d >= r ? 0 : tranny(d, r);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ prop physics
|
||||
// COLLIDERS uses skatemakerpro's export shape verbatim ({type:'box'|'circle',
|
||||
// x, z, rot, hw/hd/r, h}) so editor-exported parks drop in unchanged. Boxes are
|
||||
// RIDEABLE: their tops join heightAt (land on the picnic table); circles are
|
||||
// blockers only (bins, trunks, hydrants — resolveBlocker pushes you out).
|
||||
// main.js fills this from placed kit props; exported levels ship it pre-filled.
|
||||
export const COLLIDERS = [];
|
||||
|
||||
export function registerColliders(list) {
|
||||
for (const c of list) {
|
||||
c.top = terrainAt(c.x, c.z) + c.h; // world top, precomputed once
|
||||
if (c.type === 'box') { c.c = Math.cos(c.rot || 0); c.s = Math.sin(c.rot || 0); }
|
||||
COLLIDERS.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
function boxTop(c, x, z) {
|
||||
const dx = x - c.x, dz = z - c.z;
|
||||
const lx = dx * c.c - dz * c.s, lz = dx * c.s + dz * c.c;
|
||||
return (Math.abs(lx) <= c.hw && Math.abs(lz) <= c.hd) ? c.top : -Infinity;
|
||||
}
|
||||
|
||||
// circle-vs-player push-out. Mutates pos; returns the collider hit (for bonks).
|
||||
export function resolveBlocker(pos, pr = 0.3) {
|
||||
for (const c of COLLIDERS) {
|
||||
if (c.type !== 'circle' || pos.y > c.top) continue;
|
||||
const dx = pos.x - c.x, dz = pos.z - c.z;
|
||||
const d = Math.hypot(dx, dz), min = c.r + pr;
|
||||
if (d < min) {
|
||||
const f = d > 1e-4 ? min / d : 1;
|
||||
pos.x = c.x + dx * (d > 1e-4 ? f : min);
|
||||
pos.z = c.z + (d > 1e-4 ? dz * f : 0);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function heightAt(x, z) {
|
||||
let h = terrainAt(x, z);
|
||||
for (const c of COLLIDERS)
|
||||
if (c.type === 'box' && c.top > h) h = Math.max(h, boxTop(c, x, z));
|
||||
return h;
|
||||
}
|
||||
|
||||
function terrainAt(x, z) {
|
||||
// outside the slab: grass, rising gently away from the park
|
||||
const inPark = IN(x, BOUNDS.x0, BOUNDS.x1) && IN(z, BOUNDS.z0, BOUNDS.z1);
|
||||
if (!inPark) {
|
||||
@ -179,17 +224,27 @@ export const PROPS = [
|
||||
{ id: 'tree_fig', x: -14, z: -30, s: 1.05, rot: 4.0 },
|
||||
{ id: 'tree_gum', x: -40, z: 24, rot: 1.2 }, { id: 'tree_gum', x: 40, z: 10, rot: 3.4 },
|
||||
// furniture along the south parkland + plaza edge
|
||||
{ id: 'bench_park', x: 24, z: 15, rot: Math.PI }, { id: 'bin_council', x: 26.2, z: 15 },
|
||||
{ id: 'bench_park', x: -8, z: 17, rot: Math.PI }, { id: 'picnic_table', x: -16, z: 20, rot: 0.4 },
|
||||
{ id: 'bench_park', x: 24, z: 15, rot: Math.PI, gap: 'BENCH HOP' },
|
||||
{ id: 'bin_council', x: 26.2, z: 15 },
|
||||
{ id: 'bench_park', x: -8, z: 17, rot: Math.PI },
|
||||
{ id: 'picnic_table', x: -16, z: 20, rot: 0.4, gap: 'TABLE TOPPER' },
|
||||
{ id: 'bleachers_3', x: -24, z: 22, rot: Math.PI }, // watching the bowl line
|
||||
{ id: 'shade_sail', x: 18, z: 23 },
|
||||
// Caxton St side: fence sections + lights + a hydrant on the footpath
|
||||
// Caxton St side: fence sections + lights + footpath street furniture
|
||||
{ id: 'fence_chain_3m', x: -16, z: -38.5 }, { id: 'fence_chain_3m', x: -13, z: -38.5 },
|
||||
{ id: 'fence_chain_3m', x: 19, z: -38.5 }, { id: 'fence_chain_3m', x: 22, z: -38.5 },
|
||||
{ id: 'floodlight', x: -38, z: -20 }, { id: 'floodlight', x: 38, z: 16 },
|
||||
{ id: 'hydrant', x: 14, z: -37 },
|
||||
{ id: 'kerb_3m', x: 8, z: -37.2 }, { id: 'kerb_3m', x: 5, z: -37.2 }, // slappy kerbs
|
||||
// the wallride wall in the west parkland
|
||||
{ id: 'wall_graffiti', x: -45, z: 4, rot: Math.PI / 2 },
|
||||
// the physical street set: bonk it, grind it, or gap it
|
||||
{ id: 'trolley', x: -6, z: -8, rot: 0.7, gap: 'TROLLEY GAP' },
|
||||
{ id: 'trolley', x: 2, z: 4.5, rot: 3.6, gap: 'BOOKQUOY TROLLEY' }, // on the island, obviously
|
||||
{ id: 'planter_box', x: 20, z: -8, gap: 'PLANTER HOP' },
|
||||
{ id: 'planter_box', x: 20, z: -12 },
|
||||
{ id: 'parking_block', x: 30, z: -18, rot: 0.2 },
|
||||
{ id: 'jersey_barrier', x: -20, z: 14, rot: 1.1, gap: 'K-RAIL CLEAR' },
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------- build visuals
|
||||
|
||||
43
js/main.js
43
js/main.js
@ -2,7 +2,8 @@
|
||||
// Three disciplines: SKATE, BMX, and BLADES (locals throw rubbish at bladers. tradition.)
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { buildLevel, GAPS, SPAWN, heightAt, PROPS } from './level.js';
|
||||
import { buildLevel, GAPS, SPAWN, heightAt, PROPS, RAILS,
|
||||
registerColliders } from './level.js';
|
||||
import { KITS, RUBBISH_INSULTS, GAP_SCORE, LETTER_SCORE, ALL_LETTERS_SCORE } from './tricks.js';
|
||||
import { Player } from './player.js';
|
||||
import { Hud } from './hud.js';
|
||||
@ -60,6 +61,39 @@ async function boot() {
|
||||
}, undefined, () => console.warn('kit prop missing (is ../park_kit checked out?)', p.id));
|
||||
}
|
||||
|
||||
// Physics from the kit manifest: colliders (skatemakerpro's COLLIDERS shape,
|
||||
// cylinder->circle), grind edges appended to RAILS, per-prop gap triggers.
|
||||
// Registered AFTER buildLevel so the terrain mesh stays pure terrain.
|
||||
try {
|
||||
const man = await (await fetch(KIT_BASE + 'manifest.json')).json();
|
||||
const byId = Object.fromEntries(man.props.map(d => [d.id, d]));
|
||||
const solids = [];
|
||||
for (const p of PROPS) {
|
||||
const def = byId[p.id]; if (!def) continue;
|
||||
const s = p.s || 1, rot = p.rot || 0;
|
||||
const c = Math.cos(rot), sn = Math.sin(rot);
|
||||
const W = (lx, lz) => [p.x + (lx * c + lz * sn) * s, p.z + (-lx * sn + lz * c) * s];
|
||||
const col = def.collider;
|
||||
if (col?.type === 'box')
|
||||
solids.push({ type: 'box', name: p.id, x: p.x, z: p.z, rot,
|
||||
hw: col.hw * s, hd: col.hd * s, h: col.h * s });
|
||||
else if (col?.type === 'cylinder')
|
||||
solids.push({ type: 'circle', name: p.id, x: p.x, z: p.z,
|
||||
r: col.r * s, h: col.h * s });
|
||||
const base = heightAt(p.x, p.z);
|
||||
for (const g of def.grinds || []) {
|
||||
const [ax, az] = W(g.a[0], g.a[1]), [bx, bz] = W(g.b[0], g.b[1]);
|
||||
RAILS.push({ name: p.id.replace(/_/g, ' '), a: [ax, az], b: [bx, bz],
|
||||
ya: base + g.ya * s, yb: base + g.yb * s,
|
||||
kind: g.kind === 'ledge' ? 'ledge' : 'rail' });
|
||||
}
|
||||
// y0 = TERRAIN height (colliders not yet registered) — the gap check
|
||||
// must not measure against the prop's own box top
|
||||
if (p.gap) GAPS.push({ name: p.gap, x: p.x, z: p.z, r: 2.3, y0: base });
|
||||
}
|
||||
registerColliders(solids);
|
||||
} catch (e) { console.warn('kit manifest unavailable — props stay ghosts', e); }
|
||||
|
||||
const [rigG, deck, deckBank, skateRider, bike, bikeBank, bmxRider, pairsSkate, pairsBmx] =
|
||||
await Promise.all([
|
||||
load('assets/woman_raver_01.glb'), load('assets/deck.glb'),
|
||||
@ -130,6 +164,11 @@ function setDiscipline(name) {
|
||||
onGrindTick: pts => { if (mode === 'run') hud.addGrindPoints(pts); },
|
||||
onLand: () => { airGapsHit.clear(); landTimer = 0.9; },
|
||||
onBail: () => { airGapsHit.clear(); hud.dropCombo(); },
|
||||
onBonk: c => {
|
||||
const what = (c.name || 'that').split('_')[0].toUpperCase();
|
||||
const lines = ['BONK!!', `STRAIGHT INTO THE ${what}`, `THE ${what} WON`, 'EYES UP!!'];
|
||||
hud.flash(lines[(Math.random() * lines.length) | 0], '#ffb02e');
|
||||
},
|
||||
});
|
||||
player.reset(SPAWN);
|
||||
for (const id of ['skate', 'bmx', 'blades'])
|
||||
@ -242,7 +281,7 @@ function seshUpdate(dt) {
|
||||
for (const g of GAPS) {
|
||||
if (airGapsHit.has(g.name)) continue;
|
||||
const d = Math.hypot(player.pos.x - g.x, player.pos.z - g.z);
|
||||
if (d < g.r && player.pos.y > heightAt(g.x, g.z) + 0.25) {
|
||||
if (d < g.r && player.pos.y > (g.y0 !== undefined ? g.y0 : heightAt(g.x, g.z)) + 0.25) {
|
||||
airGapsHit.add(g.name);
|
||||
hud.addTrick(g.name, GAP_SCORE);
|
||||
hud.flash(g.name);
|
||||
|
||||
19
js/player.js
19
js/player.js
@ -4,7 +4,7 @@
|
||||
// Discipline behaviour comes entirely from the KIT (see tricks.js).
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { heightAt, normalAt, RAILS } from './level.js';
|
||||
import { heightAt, normalAt, RAILS, resolveBlocker } from './level.js';
|
||||
|
||||
const GRAV = 18;
|
||||
const MAX_SPEED = 10.5, PUSH_ACC = 6.5, FRICTION = 0.28, OFFROAD = 2.2;
|
||||
@ -128,8 +128,13 @@ export class Player {
|
||||
case 'bail': {
|
||||
this.bailT -= dt;
|
||||
this.speed = Math.max(0, this.speed - 6 * dt);
|
||||
const bx0 = p.x, bz0 = p.z;
|
||||
p.x += Math.sin(this.heading) * this.speed * dt;
|
||||
p.z += Math.cos(this.heading) * this.speed * dt;
|
||||
if (heightAt(p.x, p.z) - p.y > 0.32) { // sliding into a prop: stop there
|
||||
p.x = bx0; p.z = bz0; this.speed = 0;
|
||||
}
|
||||
resolveBlocker(p, 0.3);
|
||||
p.y = heightAt(p.x, p.z);
|
||||
if (this.bailT <= 0) { this.state = 'roll'; this.play(this.kit.idle); }
|
||||
break;
|
||||
@ -180,6 +185,7 @@ export class Player {
|
||||
p.x += Math.sin(this.heading) * this.speed * dt;
|
||||
p.z += Math.cos(this.heading) * this.speed * dt;
|
||||
p.y += f.vy * dt;
|
||||
resolveBlocker(p, 0.3);
|
||||
const h = heightAt(p.x, p.z);
|
||||
if (this.tryAttachGrind()) break;
|
||||
if (p.y <= h) {
|
||||
@ -217,10 +223,14 @@ export class Player {
|
||||
const off = p.x < -40 || p.x > 40 || p.z < -26 || p.z > 26;
|
||||
this.speed = Math.max(0, this.speed - (off ? OFFROAD : FRICTION) * dt * (0.4 + this.speed * 0.08));
|
||||
const oldH = p.y;
|
||||
const px0 = p.x, pz0 = p.z;
|
||||
p.x += Math.sin(this.heading) * this.speed * dt;
|
||||
p.z += Math.cos(this.heading) * this.speed * dt;
|
||||
const h = heightAt(p.x, p.z);
|
||||
if (h < oldH - 0.55) {
|
||||
if (h - oldH > 0.32) {
|
||||
// prop wall / hard edge (bench side, fence, trolley): you stop, it doesn't
|
||||
p.x = px0; p.z = pz0; this.speed *= 0.7;
|
||||
} else if (h < oldH - 0.55) {
|
||||
this.state = 'fall'; this.fall = { vy: 0, from: oldH };
|
||||
this.play(this.kit.airFall, 0.08);
|
||||
} else {
|
||||
@ -228,6 +238,11 @@ export class Player {
|
||||
if (this.cur !== this.kit.push + '|' || this.pushT <= 0.05)
|
||||
this.play(this.speed > 0.4 ? this.kit.roll : this.kit.idle, 0.15);
|
||||
}
|
||||
const bonk = resolveBlocker(p, 0.3);
|
||||
if (bonk) {
|
||||
if (this.speed > 5.5) { this.ev.onBonk && this.ev.onBonk(bonk); this.stumble(); }
|
||||
else this.speed *= 0.8;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user