// 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); }