feat(editor): add new terrain section types and fix physics body generation
Add four new section types to the level editor:
- dropOff: instant vertical drop with gentle downward tail
- gap: flat lead-in, x-gap, flat landing at same height
- lip: flat run into a steepening (t²) kicker ramp
- dropGap: like gap but landing is well below takeoff
Fix Terrain physics body generation to extrude straight down from
segment endpoints instead of using rotated rectangles. A previous
perpendicular-offset approach caused near-vertical segments (dropOff
cliffs) to balloon physics bodies sideways under adjacent terrain,
making the bus ride above the drawn ground.
Update level03 ("Mind the Gap") to use editor-generated terrain data.
Generalize levelBuilder smoothing to detect gap-shaped sections
(structurally via takeoffPoints/landingPoints) rather than by type
string, so future gap types work automatically.
This commit is contained in:
parent
6cf184fd4d
commit
f298dc513c
|
|
@ -1,42 +1,241 @@
|
||||||
import { WORLD_SCALE } from '../../config.js';
|
// Generated by editor.html - review before dropping into src/data/levels/.
|
||||||
|
|
||||||
// Hand-tuned profile: flat approach -> steep valley drop -> steep climb out ->
|
|
||||||
// a long stretch of tight, fast bumps that punish sloppy lean control.
|
|
||||||
function buildPoints() {
|
|
||||||
const step = 60 * WORLD_SCALE;
|
|
||||||
const points = [];
|
|
||||||
for (let x = -200 * WORLD_SCALE; x <= 3900 * WORLD_SCALE; x += step) {
|
|
||||||
let y;
|
|
||||||
if (x < 1200 * WORLD_SCALE) {
|
|
||||||
y = 420 * WORLD_SCALE + Math.sin(x / (500 * WORLD_SCALE)) * 10 * WORLD_SCALE;
|
|
||||||
} else if (x < 1500 * WORLD_SCALE) {
|
|
||||||
y = 420 * WORLD_SCALE + 220 * WORLD_SCALE * ((x - 1200 * WORLD_SCALE) / (300 * WORLD_SCALE));
|
|
||||||
} else if (x < 1650 * WORLD_SCALE) {
|
|
||||||
y = 640 * WORLD_SCALE;
|
|
||||||
} else if (x < 1950 * WORLD_SCALE) {
|
|
||||||
y = 640 * WORLD_SCALE - 260 * WORLD_SCALE * ((x - 1650 * WORLD_SCALE) / (300 * WORLD_SCALE));
|
|
||||||
} else if (x < 3650 * WORLD_SCALE) {
|
|
||||||
y = 380 * WORLD_SCALE + Math.sin(x / (150 * WORLD_SCALE)) * 45 * WORLD_SCALE;
|
|
||||||
} else {
|
|
||||||
y = 380 * WORLD_SCALE;
|
|
||||||
}
|
|
||||||
points.push({ x, y: Math.round(y) });
|
|
||||||
}
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
|
|
||||||
const END_X = 3900 * WORLD_SCALE;
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: 'level03',
|
id: "level03",
|
||||||
name: 'Tight Squeeze',
|
name: "Mind the Gap",
|
||||||
description: 'A steep valley and a long run of sharp bumps - lean early, lean often.',
|
description: "TV smash!!!",
|
||||||
kidsAboard: 4,
|
kidsAboard: 3,
|
||||||
timeLimit: 20,
|
timeLimit: 39,
|
||||||
startPosition: { x: 100 * WORLD_SCALE, y: 350 * WORLD_SCALE },
|
startPosition: { x: 200, y: 700 },
|
||||||
startAngle: 0,
|
startAngle: 0,
|
||||||
terrain: [{ points: buildPoints() }],
|
terrain: [
|
||||||
|
{ points: [
|
||||||
|
{ x: -400, y: 840 },
|
||||||
|
{ x: -330, y: 840 },
|
||||||
|
{ x: -260, y: 840 },
|
||||||
|
{ x: -190, y: 840 },
|
||||||
|
{ x: -120, y: 840 },
|
||||||
|
{ x: -50, y: 840 },
|
||||||
|
{ x: 20, y: 840 },
|
||||||
|
{ x: 90, y: 840 },
|
||||||
|
{ x: 160, y: 840 },
|
||||||
|
{ x: 230, y: 840 },
|
||||||
|
{ x: 300, y: 840 },
|
||||||
|
{ x: 370, y: 840 },
|
||||||
|
{ x: 440, y: 840 },
|
||||||
|
{ x: 510, y: 840 },
|
||||||
|
{ x: 580, y: 840 },
|
||||||
|
{ x: 650, y: 840 },
|
||||||
|
{ x: 720, y: 840 },
|
||||||
|
{ x: 790, y: 840 },
|
||||||
|
{ x: 860, y: 840 },
|
||||||
|
{ x: 930, y: 840 },
|
||||||
|
{ x: 1000, y: 840 },
|
||||||
|
{ x: 1070, y: 838 },
|
||||||
|
{ x: 1140, y: 834 },
|
||||||
|
{ x: 1210, y: 830 },
|
||||||
|
{ x: 1280, y: 822 },
|
||||||
|
{ x: 1350, y: 812 },
|
||||||
|
{ x: 1420, y: 802 },
|
||||||
|
{ x: 1490, y: 790 },
|
||||||
|
{ x: 1560, y: 776 },
|
||||||
|
{ x: 1630, y: 764 },
|
||||||
|
{ x: 1700, y: 750 },
|
||||||
|
{ x: 1770, y: 736 },
|
||||||
|
{ x: 1840, y: 724 },
|
||||||
|
{ x: 1910, y: 710 },
|
||||||
|
{ x: 1980, y: 698 },
|
||||||
|
{ x: 2050, y: 688 },
|
||||||
|
{ x: 2120, y: 678 },
|
||||||
|
{ x: 2190, y: 670 },
|
||||||
|
{ x: 2260, y: 666 },
|
||||||
|
{ x: 2330, y: 662 },
|
||||||
|
{ x: 2400, y: 660 },
|
||||||
|
{ x: 2470, y: 658 },
|
||||||
|
{ x: 2540, y: 652 },
|
||||||
|
{ x: 2610, y: 642 },
|
||||||
|
{ x: 2680, y: 626 },
|
||||||
|
{ x: 2750, y: 606 },
|
||||||
|
{ x: 2820, y: 582 },
|
||||||
|
{ x: 2890, y: 558 },
|
||||||
|
{ x: 2960, y: 534 },
|
||||||
|
{ x: 3030, y: 510 },
|
||||||
|
{ x: 3100, y: 486 },
|
||||||
|
{ x: 3170, y: 462 },
|
||||||
|
{ x: 3240, y: 438 },
|
||||||
|
{ x: 3310, y: 414 },
|
||||||
|
{ x: 3380, y: 390 },
|
||||||
|
{ x: 3450, y: 366 },
|
||||||
|
{ x: 3520, y: 342 },
|
||||||
|
{ x: 3590, y: 318 },
|
||||||
|
{ x: 3660, y: 294 },
|
||||||
|
{ x: 3730, y: 270 },
|
||||||
|
{ x: 3800, y: 246 },
|
||||||
|
{ x: 3870, y: 846 },
|
||||||
|
{ x: 3940, y: 852 },
|
||||||
|
{ x: 4010, y: 858 },
|
||||||
|
{ x: 4080, y: 864 },
|
||||||
|
{ x: 4150, y: 870 },
|
||||||
|
{ x: 4220, y: 876 },
|
||||||
|
{ x: 4290, y: 882 },
|
||||||
|
{ x: 4360, y: 888 },
|
||||||
|
{ x: 4430, y: 894 },
|
||||||
|
{ x: 4500, y: 900 },
|
||||||
|
{ x: 4570, y: 904 },
|
||||||
|
{ x: 4640, y: 910 },
|
||||||
|
{ x: 4710, y: 916 },
|
||||||
|
{ x: 4780, y: 922 },
|
||||||
|
{ x: 4850, y: 928 },
|
||||||
|
{ x: 4920, y: 934 },
|
||||||
|
{ x: 4990, y: 940 },
|
||||||
|
{ x: 5060, y: 946 },
|
||||||
|
{ x: 5130, y: 952 },
|
||||||
|
{ x: 5200, y: 956 },
|
||||||
|
{ x: 5270, y: 960 },
|
||||||
|
{ x: 5340, y: 966 },
|
||||||
|
{ x: 5410, y: 972 },
|
||||||
|
{ x: 5480, y: 980 },
|
||||||
|
{ x: 5550, y: 990 },
|
||||||
|
{ x: 5620, y: 1000 },
|
||||||
|
{ x: 5690, y: 1012 },
|
||||||
|
{ x: 5760, y: 1024 },
|
||||||
|
{ x: 5830, y: 1038 },
|
||||||
|
{ x: 5900, y: 1050 },
|
||||||
|
{ x: 5970, y: 1064 },
|
||||||
|
{ x: 6040, y: 1078 },
|
||||||
|
{ x: 6110, y: 1090 },
|
||||||
|
{ x: 6180, y: 1102 },
|
||||||
|
{ x: 6250, y: 1112 },
|
||||||
|
{ x: 6320, y: 1122 },
|
||||||
|
{ x: 6390, y: 1130 },
|
||||||
|
{ x: 6460, y: 1136 },
|
||||||
|
{ x: 6530, y: 1140 },
|
||||||
|
{ x: 6600, y: 1140 },
|
||||||
|
{ x: 6600, y: 1142 },
|
||||||
|
{ x: 6670, y: 1142 },
|
||||||
|
{ x: 6740, y: 1138 },
|
||||||
|
{ x: 6810, y: 1132 },
|
||||||
|
{ x: 6880, y: 1122 },
|
||||||
|
{ x: 6950, y: 1110 },
|
||||||
|
{ x: 7020, y: 1094 },
|
||||||
|
{ x: 7090, y: 1078 },
|
||||||
|
{ x: 7160, y: 1062 },
|
||||||
|
{ x: 7230, y: 1046 },
|
||||||
|
{ x: 7300, y: 1030 },
|
||||||
|
{ x: 7370, y: 1014 },
|
||||||
|
{ x: 7440, y: 998 },
|
||||||
|
{ x: 7510, y: 982 },
|
||||||
|
{ x: 7580, y: 966 },
|
||||||
|
{ x: 7650, y: 950 },
|
||||||
|
{ x: 7720, y: 934 },
|
||||||
|
{ x: 7790, y: 918 },
|
||||||
|
{ x: 7860, y: 902 },
|
||||||
|
{ x: 7930, y: 886 },
|
||||||
|
{ x: 8000, y: 870 },
|
||||||
|
{ x: 8070, y: 852 },
|
||||||
|
{ x: 8140, y: 828 },
|
||||||
|
{ x: 8210, y: 798 },
|
||||||
|
{ x: 8280, y: 760 },
|
||||||
|
{ x: 8350, y: 720 },
|
||||||
|
{ x: 8420, y: 682 },
|
||||||
|
{ x: 8490, y: 642 },
|
||||||
|
{ x: 8560, y: 602 },
|
||||||
|
{ x: 8630, y: 564 },
|
||||||
|
{ x: 8700, y: 524 },
|
||||||
|
{ x: 8770, y: 484 },
|
||||||
|
{ x: 8840, y: 444 },
|
||||||
|
{ x: 8880, y: 422 },
|
||||||
|
{ x: 8950, y: 422 },
|
||||||
|
{ x: 9020, y: 422 },
|
||||||
|
{ x: 9090, y: 422 },
|
||||||
|
{ x: 9160, y: 422 },
|
||||||
|
{ x: 9230, y: 422 },
|
||||||
|
{ x: 9300, y: 422 },
|
||||||
|
{ x: 9370, y: 422 },
|
||||||
|
{ x: 9440, y: 422 },
|
||||||
|
{ x: 9510, y: 422 },
|
||||||
|
{ x: 9580, y: 422 },
|
||||||
|
{ x: 9650, y: 422 },
|
||||||
|
{ x: 9720, y: 422 },
|
||||||
|
{ x: 9790, y: 422 },
|
||||||
|
{ x: 9860, y: 422 },
|
||||||
|
{ x: 9930, y: 422 },
|
||||||
|
{ x: 10000, y: 422 },
|
||||||
|
{ x: 10070, y: 422 },
|
||||||
|
{ x: 10140, y: 422 },
|
||||||
|
{ x: 10210, y: 422 },
|
||||||
|
{ x: 10280, y: 422 },
|
||||||
|
{ x: 10350, y: 422 },
|
||||||
|
{ x: 10420, y: 422 },
|
||||||
|
{ x: 10490, y: 422 },
|
||||||
|
{ x: 10560, y: 422 },
|
||||||
|
{ x: 10630, y: 422 },
|
||||||
|
{ x: 10700, y: 422 },
|
||||||
|
{ x: 10770, y: 422 },
|
||||||
|
{ x: 10840, y: 422 },
|
||||||
|
{ x: 10910, y: 422 },
|
||||||
|
{ x: 10980, y: 422 },
|
||||||
|
{ x: 11050, y: 422 },
|
||||||
|
{ x: 11120, y: 422 },
|
||||||
|
{ x: 11190, y: 422 },
|
||||||
|
{ x: 11260, y: 422 },
|
||||||
|
{ x: 11288, y: 422 },
|
||||||
|
{ x: 11358, y: 418 },
|
||||||
|
{ x: 11428, y: 404 },
|
||||||
|
{ x: 11498, y: 382 },
|
||||||
|
{ x: 11568, y: 350 },
|
||||||
|
{ x: 11638, y: 310 },
|
||||||
|
{ x: 11680, y: 282 },
|
||||||
|
{ x: 11750, y: 282 },
|
||||||
|
] },
|
||||||
|
{ points: [
|
||||||
|
{ x: 12800, y: 762 },
|
||||||
|
{ x: 12870, y: 762 },
|
||||||
|
{ x: 12940, y: 762 },
|
||||||
|
{ x: 13010, y: 762 },
|
||||||
|
{ x: 13080, y: 762 },
|
||||||
|
{ x: 13150, y: 762 },
|
||||||
|
{ x: 13220, y: 766 },
|
||||||
|
{ x: 13290, y: 772 },
|
||||||
|
{ x: 13360, y: 780 },
|
||||||
|
{ x: 13430, y: 790 },
|
||||||
|
{ x: 13500, y: 800 },
|
||||||
|
{ x: 13570, y: 812 },
|
||||||
|
{ x: 13640, y: 824 },
|
||||||
|
{ x: 13710, y: 838 },
|
||||||
|
{ x: 13780, y: 852 },
|
||||||
|
{ x: 13850, y: 866 },
|
||||||
|
{ x: 13920, y: 878 },
|
||||||
|
{ x: 13990, y: 890 },
|
||||||
|
{ x: 14060, y: 902 },
|
||||||
|
{ x: 14130, y: 914 },
|
||||||
|
{ x: 14200, y: 922 },
|
||||||
|
{ x: 14270, y: 930 },
|
||||||
|
{ x: 14340, y: 936 },
|
||||||
|
{ x: 14410, y: 940 },
|
||||||
|
{ x: 14480, y: 942 },
|
||||||
|
{ x: 14550, y: 942 },
|
||||||
|
{ x: 14620, y: 942 },
|
||||||
|
{ x: 14690, y: 942 },
|
||||||
|
{ x: 14760, y: 942 },
|
||||||
|
{ x: 14830, y: 942 },
|
||||||
|
{ x: 14900, y: 942 },
|
||||||
|
{ x: 14970, y: 942 },
|
||||||
|
{ x: 15040, y: 942 },
|
||||||
|
{ x: 15110, y: 942 },
|
||||||
|
{ x: 15180, y: 942 },
|
||||||
|
{ x: 15250, y: 942 },
|
||||||
|
{ x: 15320, y: 942 },
|
||||||
|
{ x: 15390, y: 942 },
|
||||||
|
{ x: 15460, y: 942 },
|
||||||
|
{ x: 15530, y: 942 },
|
||||||
|
{ x: 15600, y: 942 },
|
||||||
|
{ x: 15670, y: 942 },
|
||||||
|
{ x: 15740, y: 942 },
|
||||||
|
{ x: 15810, y: 942 },
|
||||||
|
{ x: 15880, y: 942 },
|
||||||
|
] },
|
||||||
|
],
|
||||||
obstacles: [],
|
obstacles: [],
|
||||||
goal: { x: END_X - 120 * WORLD_SCALE, y: 260 * WORLD_SCALE, width: 140 * WORLD_SCALE, height: 400 * WORLD_SCALE },
|
goal: { x: 15640, y: 702, width: 280, height: 640 },
|
||||||
cameraBounds: { x: -300 * WORLD_SCALE, y: 0, width: END_X + 700 * WORLD_SCALE, height: 1000 * WORLD_SCALE },
|
cameraBounds: { x: -600, y: -554, width: 17280, height: 2496 },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -175,8 +175,12 @@ export function buildLevelData(sections, metadata) {
|
||||||
const result = generateSection(section.type, baseX, baseY, section.width, section.vScale);
|
const result = generateSection(section.type, baseX, baseY, section.width, section.vScale);
|
||||||
let endY = result.endY;
|
let endY = result.endY;
|
||||||
|
|
||||||
if (result.type === 'jump') {
|
if (result.takeoffPoints) {
|
||||||
// Only the takeoff (the jump's own "beginning") gets smoothed - the
|
// Any generator producing a real x-gap (jump, gap, dropGap, ...) returns
|
||||||
|
// takeoffPoints/landingPoints instead of a single points run - checked
|
||||||
|
// structurally rather than by a specific type string so new gap-shaped
|
||||||
|
// section types don't need to masquerade as 'jump'.
|
||||||
|
// Only the takeoff (the section's own "beginning") gets smoothed - the
|
||||||
// far side of the gap is a fresh terrain segment with no seam to
|
// far side of the gap is a fresh terrain segment with no seam to
|
||||||
// round off, and a takeoff shifted by a small delta doesn't need
|
// round off, and a takeoff shifted by a small delta doesn't need
|
||||||
// correcting against the landing since nothing spans the gap.
|
// correcting against the landing since nothing spans the gap.
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,9 @@ const downloadBtn = document.getElementById('download-btn');
|
||||||
|
|
||||||
const labelFor = (typeId) => SECTION_TYPES.find((t) => t.id === typeId)?.label || typeId;
|
const labelFor = (typeId) => SECTION_TYPES.find((t) => t.id === typeId)?.label || typeId;
|
||||||
|
|
||||||
// 'flat' has no rise/amplitude/depth for vScale to multiply - disable that
|
// 'flat' and 'gap' have no rise/amplitude/depth for vScale to multiply -
|
||||||
// control rather than leave it silently do nothing.
|
// disable that control rather than leave it silently do nothing.
|
||||||
const FLAT_LIKE_TYPES = new Set(['flat']);
|
const FLAT_LIKE_TYPES = new Set(['flat', 'gap']);
|
||||||
|
|
||||||
function buildPalette() {
|
function buildPalette() {
|
||||||
paletteEl.innerHTML = '';
|
paletteEl.innerHTML = '';
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ export const SECTION_TYPES = [
|
||||||
{ id: 'rollingHills', label: 'Rolling Hills' },
|
{ id: 'rollingHills', label: 'Rolling Hills' },
|
||||||
{ id: 'ditch', label: 'Ditch' },
|
{ id: 'ditch', label: 'Ditch' },
|
||||||
{ id: 'jump', label: 'Jump' },
|
{ id: 'jump', label: 'Jump' },
|
||||||
|
{ id: 'dropOff', label: 'Drop Off' },
|
||||||
|
{ id: 'gap', label: 'Gap' },
|
||||||
|
{ id: 'lip', label: 'Lip' },
|
||||||
|
{ id: 'dropGap', label: 'Drop Gap' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const RISE_SMOOTH = 90;
|
const RISE_SMOOTH = 90;
|
||||||
|
|
@ -38,6 +42,35 @@ const JUMP_TAKEOFF_RISE = 140;
|
||||||
const JUMP_LANDING_DROP = 60;
|
const JUMP_LANDING_DROP = 60;
|
||||||
const JUMP_LIP_LEN = POINT_STEP;
|
const JUMP_LIP_LEN = POINT_STEP;
|
||||||
|
|
||||||
|
// "Instant" edges (a drop, or the start of a gap) happen within a single
|
||||||
|
// point-step so they render (and collide, via Terrain._buildSegment's
|
||||||
|
// per-point-pair rectangles) as a near-vertical wall/edge, rather than
|
||||||
|
// eating a chunk of the section's own width on a flat lead-in - callers are
|
||||||
|
// expected to place their own approach section before these if they want one.
|
||||||
|
const LEAD_LEN = POINT_STEP;
|
||||||
|
|
||||||
|
const DROP_OFF_DROP = 300;
|
||||||
|
const DROP_OFF_TAIL_RISE = 55;
|
||||||
|
|
||||||
|
// One lead-in point-step off the edge, a real x-gap wide enough that missing
|
||||||
|
// it is fatal (see jump's comment on how levelBuilder splits terrainSegments
|
||||||
|
// on takeoffPoints/landingPoints), then a flat landing back at the same
|
||||||
|
// height for the remainder of the width.
|
||||||
|
const GAP_GAP_FRACTION = 0.35;
|
||||||
|
// GAP_LANDING_FRACTION is implicit: 1 - GAP_GAP_FRACTION (minus LEAD_LEN)
|
||||||
|
|
||||||
|
// A flat run, then a short ramp eased with t*t (slope 0 at the flat, steepest
|
||||||
|
// right at the edge) so it bows below a straight line - the "under arched"
|
||||||
|
// kicker shape - rather than jump's smoothstep ramp.
|
||||||
|
const LIP_FLAT_FRACTION = 0.72;
|
||||||
|
const LIP_RISE = 70;
|
||||||
|
|
||||||
|
// Like gap, but the landing is a deliberately small flat stretch well below
|
||||||
|
// the takeoff instead of a same-height landing filling most of the width -
|
||||||
|
// the gap eats whatever's left after the lead-in and that small landing.
|
||||||
|
const DROP_GAP_LANDING_FRACTION = 0.2;
|
||||||
|
const DROP_GAP_DROP = 240;
|
||||||
|
|
||||||
function smoothstep(t) {
|
function smoothstep(t) {
|
||||||
const c = Math.max(0, Math.min(1, t));
|
const c = Math.max(0, Math.min(1, t));
|
||||||
return c * c * (3 - 2 * c);
|
return c * c * (3 - 2 * c);
|
||||||
|
|
@ -134,6 +167,71 @@ function jump(startX, startY, width, vScale) {
|
||||||
return { type: 'jump', takeoffPoints, landingPoints, endY };
|
return { type: 'jump', takeoffPoints, landingPoints, endY };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An instant large drop (a near-vertical wall within one point-step) followed
|
||||||
|
// by a gentle downward-angled tail for the rest of the width - no gap, a
|
||||||
|
// single continuous point run.
|
||||||
|
function dropOff(startX, startY, width, vScale) {
|
||||||
|
const drop = DROP_OFF_DROP * vScale;
|
||||||
|
const tailRise = DROP_OFF_TAIL_RISE * vScale;
|
||||||
|
const dropX = startX + LEAD_LEN;
|
||||||
|
const dropY = startY + drop;
|
||||||
|
|
||||||
|
const points = [
|
||||||
|
{ x: startX, y: Math.round(startY) },
|
||||||
|
{ x: dropX, y: Math.round(dropY) },
|
||||||
|
];
|
||||||
|
const tail = sampleBlock(dropX, dropY, width - LEAD_LEN, (t) => tailRise * t);
|
||||||
|
points.push(...tail.slice(1));
|
||||||
|
|
||||||
|
return { type: 'simple', points, endY: dropY + tailRise };
|
||||||
|
}
|
||||||
|
|
||||||
|
// One lead-in point-step, a real x-gap, flat landing back at the same height
|
||||||
|
// for the rest of the width - falling short of the landing means falling out
|
||||||
|
// of the world (see PlayScene's cameraBounds fail checks). No approach: the
|
||||||
|
// caller is expected to place a flat/other section before this one.
|
||||||
|
function gap(startX, startY, width) {
|
||||||
|
const w1 = LEAD_LEN;
|
||||||
|
const w2 = width * GAP_GAP_FRACTION;
|
||||||
|
const w3 = width - w1 - w2;
|
||||||
|
|
||||||
|
const takeoffPoints = sampleBlock(startX, startY, w1, () => 0);
|
||||||
|
const landingStartX = startX + w1 + w2;
|
||||||
|
const landingPoints = sampleBlock(landingStartX, startY, w3, () => 0);
|
||||||
|
|
||||||
|
return { type: 'gap', takeoffPoints, landingPoints, endY: startY };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flat run into a short, steepening (t*t) kicker ramp - no gap, just a small
|
||||||
|
// launch off the end.
|
||||||
|
function lip(startX, startY, width, vScale) {
|
||||||
|
const rise = LIP_RISE * vScale;
|
||||||
|
const w1 = width * LIP_FLAT_FRACTION;
|
||||||
|
const w2 = width - w1;
|
||||||
|
|
||||||
|
const flatPoints = sampleBlock(startX, startY, w1, () => 0);
|
||||||
|
const rampPoints = sampleBlock(startX + w1, startY, w2, (t) => -rise * t * t);
|
||||||
|
const points = flatPoints.concat(rampPoints.slice(1));
|
||||||
|
|
||||||
|
return { type: 'simple', points, endY: startY - rise };
|
||||||
|
}
|
||||||
|
|
||||||
|
// One lead-in point-step, then a gap, then a small flat landing well below
|
||||||
|
// the takeoff instead of matching its height. No approach, same as gap.
|
||||||
|
function dropGap(startX, startY, width, vScale) {
|
||||||
|
const drop = DROP_GAP_DROP * vScale;
|
||||||
|
const w1 = LEAD_LEN;
|
||||||
|
const w3 = width * DROP_GAP_LANDING_FRACTION;
|
||||||
|
const w2 = width - w1 - w3;
|
||||||
|
|
||||||
|
const takeoffPoints = sampleBlock(startX, startY, w1, () => 0);
|
||||||
|
const landingStartX = startX + w1 + w2;
|
||||||
|
const landingY = startY + drop;
|
||||||
|
const landingPoints = sampleBlock(landingStartX, landingY, w3, () => 0);
|
||||||
|
|
||||||
|
return { type: 'gap', takeoffPoints, landingPoints, endY: landingY };
|
||||||
|
}
|
||||||
|
|
||||||
const GENERATORS = {
|
const GENERATORS = {
|
||||||
flat,
|
flat,
|
||||||
smoothIncline,
|
smoothIncline,
|
||||||
|
|
@ -143,6 +241,10 @@ const GENERATORS = {
|
||||||
rollingHills,
|
rollingHills,
|
||||||
ditch,
|
ditch,
|
||||||
jump,
|
jump,
|
||||||
|
dropOff,
|
||||||
|
gap,
|
||||||
|
lip,
|
||||||
|
dropGap,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function generateSection(typeId, startX, startY, width = SECTION_WIDTH, vScale = 1) {
|
export function generateSection(typeId, startX, startY, width = SECTION_WIDTH, vScale = 1) {
|
||||||
|
|
|
||||||
|
|
@ -63,27 +63,41 @@ export default class Terrain {
|
||||||
const b = points[i + 1];
|
const b = points[i + 1];
|
||||||
const dx = b.x - a.x;
|
const dx = b.x - a.x;
|
||||||
const dy = b.y - a.y;
|
const dy = b.y - a.y;
|
||||||
const length = Math.hypot(dx, dy);
|
if (Math.hypot(dx, dy) < 1) continue;
|
||||||
if (length < 1) continue;
|
// A perfectly vertical a-b (dx === 0, seen in hand-authored data as
|
||||||
|
// float-rounding near-duplicate points, e.g. two points a couple of Y
|
||||||
|
// units apart at the same X) makes the straight-down-extruded quad
|
||||||
|
// below degenerate: shifting a vertical edge straight down keeps every
|
||||||
|
// corner at the same X, so it has zero width/area, which Matter's
|
||||||
|
// fromVertices can't turn into a body. Skip it - the segments on
|
||||||
|
// either side still meet at both of this pair's endpoints, so physics
|
||||||
|
// coverage stays continuous either way.
|
||||||
|
if (Math.abs(dx) < 1) continue;
|
||||||
|
|
||||||
// The rectangle's top edge needs to sit exactly on the a-b line, which
|
// Built from the exact same 4 corners _fillFromTop draws (a, b, and
|
||||||
// means offsetting its center from the segment's own midpoint by
|
// both shifted straight down by TERRAIN_DEPTH) rather than a rotated
|
||||||
// TERRAIN_DEPTH/2 in the direction PERPENDICULAR to the segment - both
|
// rectangle, so the body's top edge is a-b itself and can never
|
||||||
// an X and a Y component (-sin/cos of the segment's angle), not just
|
// diverge from the drawn line. A previous version offset a rotated
|
||||||
// straight down in world Y. Omitting the X term (as a prior version of
|
// rectangle's center straight down, which shifted its top edge
|
||||||
// this did) left every non-flat segment's physics body shifted
|
// sideways from a-b by TERRAIN_DEPTH/2*sin(angle) on any slope; a
|
||||||
// sideways from its drawn line by TERRAIN_DEPTH/2 * sin(angle) - with
|
// later fix offset perpendicular to the segment instead, which kept
|
||||||
// TERRAIN_DEPTH deliberately huge (deep enough nothing ever tunnels
|
// the top edge glued to a-b but, on a near-vertical segment (a steep
|
||||||
// through the bottom), that shift is large even at modest slopes,
|
// dropOff cliff), pointed that offset almost entirely sideways -
|
||||||
// which is exactly what made the bus visibly ride above/through the
|
// ballooning the body out under whatever terrain preceded the cliff,
|
||||||
// drawn ground on anything but flat terrain.
|
// well above where anything was ever drawn. Extruding straight down
|
||||||
const angle = Math.atan2(dy, dx);
|
// from the real points (matching the visual fill exactly, at every
|
||||||
const midX = (a.x + b.x) / 2 - (TERRAIN_DEPTH / 2) * Math.sin(angle);
|
// slope) has no such failure mode.
|
||||||
const midY = (a.y + b.y) / 2 + (TERRAIN_DEPTH / 2) * Math.cos(angle);
|
const cx = (a.x + b.x) / 2;
|
||||||
|
const cy = (a.y + b.y) / 2 + TERRAIN_DEPTH / 2;
|
||||||
|
const vertices = [
|
||||||
|
{ x: a.x, y: a.y },
|
||||||
|
{ x: b.x, y: b.y },
|
||||||
|
{ x: b.x, y: b.y + TERRAIN_DEPTH },
|
||||||
|
{ x: a.x, y: a.y + TERRAIN_DEPTH },
|
||||||
|
];
|
||||||
|
|
||||||
const body = this.scene.matter.add.rectangle(midX, midY, length, TERRAIN_DEPTH, {
|
const body = this.scene.matter.add.fromVertices(cx, cy, vertices, {
|
||||||
isStatic: true,
|
isStatic: true,
|
||||||
angle,
|
|
||||||
friction: 0.95,
|
friction: 0.95,
|
||||||
label: 'terrain',
|
label: 'terrain',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue