feat(editor): improve level preview and auto-calculate time limits
- Fix terrain preview aspect ratio to match in-game rendering by using consistent X/Y scaling, preventing slopes from appearing distorted - Calculate timeLimit automatically based on level length and pace (TIME_LIMIT_PACE) to ensure consistent difficulty scaling across levels - Add max-height and vertical scrolling to the level preview panel - Generate level02 (The Big Jump) from the editor with manual tweak
This commit is contained in:
parent
7f4c632cce
commit
ef47daea6d
|
|
@ -174,6 +174,8 @@
|
|||
}
|
||||
#preview-scroll {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
max-height: 640px;
|
||||
border: 1px solid #232c40;
|
||||
border-radius: 4px;
|
||||
background: #0b0e14;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,260 @@
|
|||
import { generateWave, withRamp } from './terrainHelpers.js';
|
||||
import { WORLD_SCALE } from '../../config.js';
|
||||
|
||||
const RUNWAY_END_X = 1850 * WORLD_SCALE;
|
||||
const LANDING_START_X = 2200 * WORLD_SCALE;
|
||||
const END_X = 3900 * WORLD_SCALE;
|
||||
|
||||
const runway = generateWave(-200 * WORLD_SCALE, RUNWAY_END_X, 60 * WORLD_SCALE, 420 * WORLD_SCALE, 10 * WORLD_SCALE, 500 * WORLD_SCALE);
|
||||
withRamp(runway, 1500 * WORLD_SCALE, RUNWAY_END_X, -170 * WORLD_SCALE); // launch ramp climbing up to the jump-off edge
|
||||
|
||||
const landing = generateWave(LANDING_START_X, END_X, 80 * WORLD_SCALE, 460 * WORLD_SCALE, 15 * WORLD_SCALE, 400 * WORLD_SCALE);
|
||||
|
||||
// Generated by editor.html - review before dropping into src/data/levels/.
|
||||
export default {
|
||||
id: 'level02',
|
||||
name: 'Big Air',
|
||||
description: 'Clear the gap and land level, or the kids pay for it.',
|
||||
id: "level02",
|
||||
name: "The Big Jump",
|
||||
description: "Make the Jump or the kids pay!",
|
||||
kidsAboard: 3,
|
||||
timeLimit: 20,
|
||||
startPosition: { x: 100 * WORLD_SCALE, y: 350 * WORLD_SCALE },
|
||||
timeLimit: 41,
|
||||
startPosition: { x: 200, y: 700 },
|
||||
startAngle: 0,
|
||||
terrain: [{ points: runway }, { points: landing }],
|
||||
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: 654 },
|
||||
{ x: 2610, y: 650 },
|
||||
{ x: 2680, y: 642 },
|
||||
{ x: 2750, y: 632 },
|
||||
{ x: 2820, y: 622 },
|
||||
{ x: 2890, y: 610 },
|
||||
{ x: 2960, y: 596 },
|
||||
{ x: 3030, y: 584 },
|
||||
{ x: 3100, y: 570 },
|
||||
{ x: 3170, y: 556 },
|
||||
{ x: 3240, y: 544 },
|
||||
{ x: 3310, y: 530 },
|
||||
{ x: 3380, y: 518 },
|
||||
{ x: 3450, y: 508 },
|
||||
{ x: 3520, y: 498 },
|
||||
{ x: 3590, y: 490 },
|
||||
{ x: 3660, y: 486 },
|
||||
{ x: 3730, y: 482 },
|
||||
{ x: 3800, y: 480 },
|
||||
{ x: 3870, y: 464 },
|
||||
{ x: 3940, y: 448 },
|
||||
{ x: 4010, y: 432 },
|
||||
{ x: 4080, y: 416 },
|
||||
{ x: 4150, y: 400 },
|
||||
{ x: 4220, y: 384 },
|
||||
{ x: 4290, y: 368 },
|
||||
{ x: 4360, y: 352 },
|
||||
{ x: 4430, y: 336 },
|
||||
{ x: 4500, y: 320 },
|
||||
{ x: 4570, y: 304 },
|
||||
{ x: 4640, y: 288 },
|
||||
{ x: 4710, y: 272 },
|
||||
{ x: 4780, y: 256 },
|
||||
{ x: 4850, y: 240 },
|
||||
{ x: 4920, y: 224 },
|
||||
{ x: 4990, y: 208 },
|
||||
{ x: 5060, y: 192 },
|
||||
{ x: 5130, y: 176 },
|
||||
{ x: 5200, y: 160 },
|
||||
{ x: 5270, y: 144 },
|
||||
{ x: 5340, y: 126 },
|
||||
{ x: 5410, y: 104 },
|
||||
{ x: 5480, y: 80 },
|
||||
{ x: 5550, y: 52 },
|
||||
{ x: 5620, y: 22 },
|
||||
{ x: 5690, y: -8 },
|
||||
{ x: 5760, y: -40 },
|
||||
{ x: 5830, y: -70 },
|
||||
{ x: 5900, y: -100 },
|
||||
{ x: 5970, y: -130 },
|
||||
{ x: 6040, y: -160 },
|
||||
{ x: 6110, y: -192 },
|
||||
{ x: 6180, y: -222 },
|
||||
{ x: 6250, y: -252 },
|
||||
{ x: 6320, y: -282 },
|
||||
{ x: 6390, y: -312 },
|
||||
{ x: 6460, y: -344 },
|
||||
{ x: 6530, y: -374 },
|
||||
{ x: 6600, y: -404 },
|
||||
{ x: 6670, y: -434 },
|
||||
{ x: 6740, y: -466 },
|
||||
{ x: 6810, y: -498 },
|
||||
{ x: 6880, y: -530 },
|
||||
{ x: 6950, y: -566 },
|
||||
{ x: 7020, y: -602 },
|
||||
{ x: 7090, y: -638 },
|
||||
{ x: 7160, y: -674 },
|
||||
{ x: 7230, y: -712 },
|
||||
{ x: 7300, y: -742 },
|
||||
{ x: 7356, y: -742 },
|
||||
] },
|
||||
{ points: [
|
||||
{ x: 7776, y: -612 },
|
||||
{ x: 7846, y: -600 },
|
||||
{ x: 7916, y: -572 },
|
||||
{ x: 7986, y: -534 },
|
||||
{ x: 8056, y: -490 },
|
||||
{ x: 8126, y: -450 },
|
||||
{ x: 8196, y: -418 },
|
||||
{ x: 8266, y: -404 },
|
||||
{ x: 8280, y: -404 },
|
||||
{ x: 8350, y: -370 },
|
||||
{ x: 8420, y: -336 },
|
||||
{ x: 8490, y: -302 },
|
||||
{ x: 8560, y: -268 },
|
||||
{ x: 8630, y: -236 },
|
||||
{ x: 8700, y: -202 },
|
||||
{ x: 8770, y: -168 },
|
||||
{ x: 8840, y: -134 },
|
||||
{ x: 8910, y: -100 },
|
||||
{ x: 8980, y: -68 },
|
||||
{ x: 9050, y: -34 },
|
||||
{ x: 9120, y: 0 },
|
||||
{ x: 9190, y: 34 },
|
||||
{ x: 9260, y: 68 },
|
||||
{ x: 9330, y: 100 },
|
||||
{ x: 9400, y: 134 },
|
||||
{ x: 9470, y: 168 },
|
||||
{ x: 9540, y: 202 },
|
||||
{ x: 9610, y: 236 },
|
||||
{ x: 9680, y: 268 },
|
||||
{ x: 9750, y: 296 },
|
||||
{ x: 9820, y: 322 },
|
||||
{ x: 9890, y: 342 },
|
||||
{ x: 9960, y: 354 },
|
||||
{ x: 10030, y: 358 },
|
||||
{ x: 10100, y: 354 },
|
||||
{ x: 10170, y: 342 },
|
||||
{ x: 10240, y: 322 },
|
||||
{ x: 10310, y: 296 },
|
||||
{ x: 10380, y: 268 },
|
||||
{ x: 10450, y: 240 },
|
||||
{ x: 10520, y: 216 },
|
||||
{ x: 10590, y: 196 },
|
||||
{ x: 10660, y: 184 },
|
||||
{ x: 10730, y: 178 },
|
||||
{ x: 10800, y: 184 },
|
||||
{ x: 10870, y: 196 },
|
||||
{ x: 10940, y: 216 },
|
||||
{ x: 11010, y: 240 },
|
||||
{ x: 11080, y: 268 },
|
||||
{ x: 11150, y: 312 },
|
||||
{ x: 11220, y: 350 },
|
||||
{ x: 11290, y: 382 },
|
||||
{ x: 11360, y: 402 },
|
||||
{ x: 11430, y: 408 },
|
||||
{ x: 11500, y: 402 },
|
||||
{ x: 11570, y: 382 },
|
||||
{ x: 11640, y: 350 },
|
||||
{ x: 11710, y: 312 },
|
||||
{ x: 11780, y: 268 },
|
||||
{ x: 11850, y: 226 },
|
||||
{ x: 11920, y: 186 },
|
||||
{ x: 11990, y: 156 },
|
||||
{ x: 12060, y: 136 },
|
||||
{ x: 12130, y: 130 },
|
||||
{ x: 12200, y: 136 },
|
||||
{ x: 12270, y: 156 },
|
||||
{ x: 12340, y: 186 },
|
||||
{ x: 12410, y: 226 },
|
||||
{ x: 12480, y: 268 },
|
||||
{ x: 12550, y: 328 },
|
||||
{ x: 12620, y: 382 },
|
||||
{ x: 12690, y: 426 },
|
||||
{ x: 12760, y: 452 },
|
||||
{ x: 12830, y: 462 },
|
||||
{ x: 12900, y: 452 },
|
||||
{ x: 12970, y: 426 },
|
||||
{ x: 13040, y: 382 },
|
||||
{ x: 13110, y: 328 },
|
||||
{ x: 13180, y: 268 },
|
||||
{ x: 13250, y: 208 },
|
||||
{ x: 13320, y: 154 },
|
||||
{ x: 13390, y: 112 },
|
||||
{ x: 13460, y: 84 },
|
||||
{ x: 13530, y: 76 },
|
||||
{ x: 13600, y: 84 },
|
||||
{ x: 13670, y: 112 },
|
||||
{ x: 13740, y: 154 },
|
||||
{ x: 13810, y: 208 },
|
||||
{ x: 13880, y: 268 },
|
||||
{ x: 13950, y: 326 },
|
||||
{ x: 14020, y: 376 },
|
||||
{ x: 14090, y: 418 },
|
||||
{ x: 14160, y: 448 },
|
||||
{ x: 14230, y: 470 },
|
||||
{ x: 14300, y: 488 },
|
||||
{ x: 14370, y: 504 },
|
||||
{ x: 14440, y: 520 },
|
||||
{ x: 14510, y: 536 },
|
||||
{ x: 14580, y: 552 },
|
||||
{ x: 14650, y: 568 },
|
||||
{ x: 14720, y: 584 },
|
||||
{ x: 14790, y: 600 },
|
||||
{ x: 14860, y: 616 },
|
||||
{ x: 14930, y: 632 },
|
||||
{ x: 15000, y: 648 },
|
||||
{ x: 15070, y: 664 },
|
||||
{ x: 15140, y: 680 },
|
||||
{ x: 15210, y: 696 },
|
||||
{ x: 15280, y: 712 },
|
||||
{ x: 15350, y: 712 },
|
||||
{ x: 15420, y: 712 },
|
||||
{ x: 15490, y: 712 },
|
||||
{ x: 15560, y: 712 },
|
||||
{ x: 15630, y: 712 },
|
||||
{ x: 15700, y: 712 },
|
||||
{ x: 15770, y: 712 },
|
||||
{ x: 15840, y: 712 },
|
||||
{ x: 15910, y: 712 },
|
||||
{ x: 15980, y: 712 },
|
||||
{ x: 16050, y: 712 },
|
||||
{ x: 16120, y: 712 },
|
||||
{ x: 16190, y: 712 },
|
||||
{ x: 16260, y: 712 },
|
||||
{ x: 16330, y: 712 },
|
||||
{ x: 16400, y: 712 },
|
||||
{ x: 16470, y: 712 },
|
||||
{ x: 16540, y: 712 },
|
||||
{ x: 16610, y: 712 },
|
||||
{ x: 16680, y: 712 },
|
||||
] },
|
||||
],
|
||||
obstacles: [],
|
||||
goal: { x: END_X - 120 * WORLD_SCALE, y: 340 * WORLD_SCALE, width: 140 * WORLD_SCALE, height: 320 * WORLD_SCALE },
|
||||
cameraBounds: { x: -300 * WORLD_SCALE, y: 0, width: END_X + 700 * WORLD_SCALE, height: 1000 * WORLD_SCALE },
|
||||
goal: { x: 16440, y: 472, width: 280, height: 640 },
|
||||
cameraBounds: { x: -600, y: -1542, width: 18080, height: 3182 },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export function exportLevelToSource(levelData) {
|
|||
` name: ${JSON.stringify(levelData.name)},`,
|
||||
` description: ${JSON.stringify(levelData.description)},`,
|
||||
` kidsAboard: ${levelData.kidsAboard},`,
|
||||
` timeLimit: ${levelData.timeLimit},`,
|
||||
` startPosition: { x: ${levelData.startPosition.x}, y: ${levelData.startPosition.y} },`,
|
||||
` startAngle: ${levelData.startAngle},`,
|
||||
` terrain: ${terrainLiteral(levelData.terrain)},`,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,17 @@ const CAMERA_RIGHT_MARGIN = 700;
|
|||
const CAMERA_VERTICAL_PADDING = 400;
|
||||
const CAMERA_MIN_HEIGHT = 1000;
|
||||
|
||||
// PlayScene reads level.timeLimit directly with no fallback (see
|
||||
// PlayScene._updateTimer) - a level missing it entirely counts down from
|
||||
// `undefined`, which never satisfies <= 0, so the level can simply never
|
||||
// time out and the HUD clock reads "NaN:NaN". Auto-deriving it from the
|
||||
// level's own start-to-goal distance means every exported level always has
|
||||
// one, tuned by playtesting to a pace matching level01/03/04's
|
||||
// hand-authored limits (~380-420 scaled units/sec each) rather than a flat
|
||||
// number that wouldn't scale with track length.
|
||||
const TIME_LIMIT_PACE = 400; // scaled world-units of track per second
|
||||
const TIME_LIMIT_MIN = 15;
|
||||
|
||||
function scalePoint(p) {
|
||||
return { x: Math.round(p.x * WORLD_SCALE), y: Math.round(p.y * WORLD_SCALE) };
|
||||
}
|
||||
|
|
@ -197,6 +208,10 @@ export function buildLevelData(sections, metadata) {
|
|||
const endX = Math.round(baseX * WORLD_SCALE);
|
||||
const groundYAtEnd = Math.round(baseY * WORLD_SCALE);
|
||||
|
||||
const spawnX = Math.round(BUS_SPAWN_X * WORLD_SCALE);
|
||||
const goalX = endX - Math.round(GOAL_END_MARGIN * WORLD_SCALE);
|
||||
const timeLimit = Math.max(TIME_LIMIT_MIN, Math.round((goalX - spawnX) / TIME_LIMIT_PACE));
|
||||
|
||||
let minTerrainY = Infinity;
|
||||
let maxTerrainY = -Infinity;
|
||||
for (const segment of terrainSegments) {
|
||||
|
|
@ -218,15 +233,16 @@ export function buildLevelData(sections, metadata) {
|
|||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
kidsAboard: metadata.kidsAboard,
|
||||
timeLimit,
|
||||
startPosition: {
|
||||
x: Math.round(BUS_SPAWN_X * WORLD_SCALE),
|
||||
x: spawnX,
|
||||
y: Math.round((INITIAL_GROUND_Y - BUS_SPAWN_DROP) * WORLD_SCALE),
|
||||
},
|
||||
startAngle: 0,
|
||||
terrain: terrainSegments,
|
||||
obstacles: [],
|
||||
goal: {
|
||||
x: endX - Math.round(GOAL_END_MARGIN * WORLD_SCALE),
|
||||
x: goalX,
|
||||
y: groundYAtEnd - Math.round(GOAL_HEIGHT_OFFSET * WORLD_SCALE),
|
||||
width: Math.round(GOAL_WIDTH * WORLD_SCALE),
|
||||
height: Math.round(GOAL_HEIGHT * WORLD_SCALE),
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ removeBtn.addEventListener('click', () => {
|
|||
|
||||
const PADDING = 40;
|
||||
const H_PX_PER_UNIT = 0.12;
|
||||
const CANVAS_HEIGHT = 460;
|
||||
const MIN_CANVAS_HEIGHT = 260;
|
||||
|
||||
function computeBounds(levelData) {
|
||||
let minX = Infinity;
|
||||
|
|
@ -251,14 +251,20 @@ function drawPreview(levelData) {
|
|||
const contentWidth = Math.max(1, maxX - minX);
|
||||
const contentHeight = Math.max(1, maxY - minY);
|
||||
|
||||
// One scale factor for both axes - the actual game (Terrain.js) always
|
||||
// scales X and Y identically (everything just multiplies by WORLD_SCALE),
|
||||
// so fitting height to a fixed canvas box with its own independently
|
||||
// derived vertical scale (the old behavior) stretched or squashed every
|
||||
// incline/hill relative to how it actually plays. Height now just follows
|
||||
// from content height at the same scale, with a floor so a nearly-flat
|
||||
// level doesn't render as a sliver.
|
||||
const width = Math.max(600, Math.round(contentWidth * H_PX_PER_UNIT) + PADDING * 2);
|
||||
const height = CANVAS_HEIGHT;
|
||||
const height = Math.max(MIN_CANVAS_HEIGHT, Math.round(contentHeight * H_PX_PER_UNIT) + PADDING * 2);
|
||||
previewCanvas.width = width;
|
||||
previewCanvas.height = height;
|
||||
|
||||
const vScale = (height - PADDING * 2) / contentHeight;
|
||||
const screenX = (worldX) => (worldX - minX) * H_PX_PER_UNIT + PADDING;
|
||||
const screenY = (worldY) => (worldY - minY) * vScale + PADDING;
|
||||
const screenY = (worldY) => (worldY - minY) * H_PX_PER_UNIT + PADDING;
|
||||
|
||||
const ctx = previewCanvas.getContext('2d');
|
||||
ctx.fillStyle = '#0b0e14';
|
||||
|
|
|
|||
Loading…
Reference in New Issue