monsterplex/src/editor/sections.js

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