153 lines
5.4 KiB
JavaScript
153 lines
5.4 KiB
JavaScript
// Pure geometry - zero imports, no WORLD_SCALE, no DOM. Every generator works
|
|
// in "base design units" (the same pre-WORLD_SCALE space level files author
|
|
// in) so this module is trivially usable from plain Node for testing and
|
|
// stays decoupled from both Phaser and the editor's DOM code.
|
|
|
|
export const SECTION_WIDTH = 700;
|
|
export const POINT_STEP = 35;
|
|
|
|
// Bounds the editor's width/vertical-scale controls clamp to. Width floor
|
|
// keeps jump's fixed-length lip (JUMP_LIP_LEN) from eating the whole takeoff
|
|
// ramp at the smallest width; the rest are just sane "still looks like a
|
|
// track section" limits, not physically derived.
|
|
export const SECTION_WIDTH_MIN = 250;
|
|
export const SECTION_WIDTH_MAX = 2100;
|
|
export const SECTION_VSCALE_MIN = 0.3;
|
|
export const SECTION_VSCALE_MAX = 2.5;
|
|
|
|
export const SECTION_TYPES = [
|
|
{ id: 'flat', label: 'Flat' },
|
|
{ id: 'smoothIncline', label: 'Smooth Incline' },
|
|
{ id: 'sharpIncline', label: 'Sharp Incline' },
|
|
{ id: 'smoothDecline', label: 'Smooth Decline' },
|
|
{ id: 'sharpDecline', label: 'Sharp Decline' },
|
|
{ id: 'rollingHills', label: 'Rolling Hills' },
|
|
{ id: 'ditch', label: 'Ditch' },
|
|
{ id: 'jump', label: 'Jump' },
|
|
];
|
|
|
|
const RISE_SMOOTH = 90;
|
|
const RISE_SHARP = 160;
|
|
const AMP_HILLS = 45;
|
|
const DEPTH_DITCH = 130;
|
|
|
|
const JUMP_TAKEOFF_FRACTION = 0.45;
|
|
const JUMP_GAP_FRACTION = 0.25;
|
|
// JUMP_LANDING_FRACTION is implicit: 1 - JUMP_TAKEOFF_FRACTION - JUMP_GAP_FRACTION
|
|
const JUMP_TAKEOFF_RISE = 140;
|
|
const JUMP_LANDING_DROP = 60;
|
|
const JUMP_LIP_LEN = POINT_STEP;
|
|
|
|
function smoothstep(t) {
|
|
const c = Math.max(0, Math.min(1, t));
|
|
return c * c * (3 - 2 * c);
|
|
}
|
|
|
|
// Samples x from startX to startX+width every POINT_STEP (inclusive of both
|
|
// ends), calling shape(t) for the y-offset at each sample, t in [0,1].
|
|
function sampleBlock(startX, startY, width, shape) {
|
|
const points = [];
|
|
for (let x = startX; x <= startX + width; x += POINT_STEP) {
|
|
const t = (x - startX) / width;
|
|
points.push({ x, y: Math.round(startY + shape(t)) });
|
|
}
|
|
// Guard against floating-point step accumulation leaving the last sample
|
|
// short of startX+width.
|
|
const last = points[points.length - 1];
|
|
if (last.x < startX + width - 1) {
|
|
points.push({ x: startX + width, y: Math.round(startY + shape(1)) });
|
|
}
|
|
return points;
|
|
}
|
|
|
|
function flat(startX, startY, width) {
|
|
const points = sampleBlock(startX, startY, width, () => 0);
|
|
return { type: 'simple', points, endY: startY };
|
|
}
|
|
|
|
function smoothIncline(startX, startY, width, vScale) {
|
|
const rise = RISE_SMOOTH * vScale;
|
|
const points = sampleBlock(startX, startY, width, (t) => -rise * smoothstep(t));
|
|
return { type: 'simple', points, endY: startY - rise };
|
|
}
|
|
|
|
function sharpIncline(startX, startY, width, vScale) {
|
|
const rise = RISE_SHARP * vScale;
|
|
const points = sampleBlock(startX, startY, width, (t) => -rise * t);
|
|
return { type: 'simple', points, endY: startY - rise };
|
|
}
|
|
|
|
function smoothDecline(startX, startY, width, vScale) {
|
|
const rise = RISE_SMOOTH * vScale;
|
|
const points = sampleBlock(startX, startY, width, (t) => rise * smoothstep(t));
|
|
return { type: 'simple', points, endY: startY + rise };
|
|
}
|
|
|
|
function sharpDecline(startX, startY, width, vScale) {
|
|
const rise = RISE_SHARP * vScale;
|
|
const points = sampleBlock(startX, startY, width, (t) => rise * t);
|
|
return { type: 'simple', points, endY: startY + rise };
|
|
}
|
|
|
|
function rollingHills(startX, startY, width, vScale) {
|
|
const amp = AMP_HILLS * vScale;
|
|
const points = sampleBlock(startX, startY, width, (t) => amp * Math.sin(2 * Math.PI * t));
|
|
return { type: 'simple', points, endY: startY };
|
|
}
|
|
|
|
function ditch(startX, startY, width, vScale) {
|
|
const depth = DEPTH_DITCH * vScale;
|
|
const points = sampleBlock(startX, startY, width, (t) => depth * Math.sin(Math.PI * t));
|
|
return { type: 'simple', points, endY: startY };
|
|
}
|
|
|
|
// The one type that produces two separate point runs (a real terrain gap in
|
|
// between), mirroring how level02.js builds its jump: a "runway" segment
|
|
// ending at a raised lip, then a real x-gap, then a "landing" segment
|
|
// starting lower (simulating the fall across the gap).
|
|
function jump(startX, startY, width, vScale) {
|
|
const takeoffRise = JUMP_TAKEOFF_RISE * vScale;
|
|
const landingDrop = JUMP_LANDING_DROP * vScale;
|
|
|
|
const w1 = width * JUMP_TAKEOFF_FRACTION;
|
|
const w2 = width * JUMP_GAP_FRACTION;
|
|
const w3 = width - w1 - w2;
|
|
|
|
const rampLen = w1 - JUMP_LIP_LEN;
|
|
const lipY = startY - takeoffRise;
|
|
const takeoffPoints = [];
|
|
for (let x = startX; x <= startX + w1; x += POINT_STEP) {
|
|
const localX = x - startX;
|
|
const y = localX <= rampLen ? startY - takeoffRise * (localX / rampLen) : lipY;
|
|
takeoffPoints.push({ x, y: Math.round(y) });
|
|
}
|
|
const takeoffLast = takeoffPoints[takeoffPoints.length - 1];
|
|
if (takeoffLast.x < startX + w1 - 1) {
|
|
takeoffPoints.push({ x: startX + w1, y: Math.round(lipY) });
|
|
}
|
|
|
|
const landingStartX = startX + w1 + w2;
|
|
const landingStartY = lipY + landingDrop;
|
|
const endY = startY;
|
|
const landingPoints = sampleBlock(landingStartX, landingStartY, w3, (t) => (endY - landingStartY) * smoothstep(t));
|
|
|
|
return { type: 'jump', takeoffPoints, landingPoints, endY };
|
|
}
|
|
|
|
const GENERATORS = {
|
|
flat,
|
|
smoothIncline,
|
|
sharpIncline,
|
|
smoothDecline,
|
|
sharpDecline,
|
|
rollingHills,
|
|
ditch,
|
|
jump,
|
|
};
|
|
|
|
export function generateSection(typeId, startX, startY, width = SECTION_WIDTH, vScale = 1) {
|
|
const generator = GENERATORS[typeId];
|
|
if (!generator) throw new Error(`Unknown section type: ${typeId}`);
|
|
return generator(startX, startY, width, vScale);
|
|
}
|