/** * levelToScene — превращает LEVELS[] в готовые primitives + blocks для проекта. * * Используется при save из 2D-редактора. Логика идентична серверному * `_p265_redesign.py` (тому что был одноразовым) — теперь это часть кода. * * Главный экспорт: buildSceneObjectsForLevels(levels, options). * options.startId — стартовый id для генерируемых примитивов (по умолчанию 30000). * * Возвращает { primitives: [], blocks: [] }. */ let _idCounter = 0; function makeIdAlloc(start) { _idCounter = start; return () => ++_idCounter; } function basePrim(over) { return Object.assign({ id: 0, x: 0, y: 0, z: 0, sx: 1, sy: 1, sz: 1, rx: 0, ry: 0, rz: 0, mass: 0, name: '', type: 'box', color: '#ffffff', visible: true, material: 'plastic', anchored: true, canCollide: false, folderId: null, }, over || {}); } function makeSpike(nid, x) { return [basePrim({ id: nid(), type: 'cone', x, y: 1.35, z: 0, sx: 0.9, sy: 1.2, sz: 0.9, color: '#e94545', material: 'neon', name: 'spike', })]; } function makeSpikeTop(nid, x) { return [basePrim({ id: nid(), type: 'cone', x, y: 6.0, z: 0, sx: 0.9, sy: 1.2, sz: 0.9, rx: 180, color: '#a44', material: 'neon', name: 'spike_top', })]; } function makeTramp(nid, x) { return [ basePrim({ id: nid(), type: 'box', x, y: 1.01, z: 0, sx: 1.8, sy: 0.02, sz: 1.8, color: '#ffcc00', material: 'neon', name: 'tramp_glow' }), basePrim({ id: nid(), type: 'box', x, y: 1.15, z: 0, sx: 1.8, sy: 0.3, sz: 1.8, color: '#ff9533', material: 'neon', name: 'tramp' }), ]; } function makeSpeedPad(nid, x) { return [ basePrim({ id: nid(), type: 'box', x, y: 1.01, z: 0, sx: 1.8, sy: 0.02, sz: 1.8, color: '#33ddff', material: 'neon', name: 'speedpad_glow' }), basePrim({ id: nid(), type: 'box', x, y: 1.10, z: 0, sx: 1.8, sy: 0.2, sz: 1.8, color: '#55ccff', material: 'neon', name: 'speedpad' }), ]; } function makeJumpOrb(nid, x, y) { const innerId = nid(); return { items: [ basePrim({ id: innerId, type: 'sphere', x, y, z: 0, sx: 0.9, sy: 0.9, sz: 0.9, color: '#ffe44a', material: 'neon', name: 'jumporb' }), basePrim({ id: nid(), type: 'sphere', x, y, z: 0, sx: 1.3, sy: 1.3, sz: 1.3, color: '#ffaa00', material: 'neon', name: 'jumporb_glow' }), ], primId: innerId, }; } function makeGravityFlip(nid, x) { const pid = nid(); return { items: [ basePrim({ id: pid, type: 'box', x, y: 3.5, z: 0, sx: 0.4, sy: 4.5, sz: 2.2, color: '#b266ff', material: 'neon', name: 'gravity_portal' }), basePrim({ id: nid(), type: 'box', x, y: 3.5, z: 0, sx: 0.45, sy: 4.7, sz: 0.5, color: '#ffffff', material: 'neon', name: 'gravity_portal_glow' }), ], primId: pid, }; } function makeMoving(nid, x, yMin) { const pid = nid(); return { items: [basePrim({ id: pid, type: 'box', x, y: yMin, z: 0, sx: 2.5, sy: 0.5, sz: 2, color: '#22cc88', material: 'plastic', name: 'movplat', canCollide: true, anchored: true, })], primId: pid, }; } function makeStep(nid, x, y, sx = 2, sy = 0.5) { return [basePrim({ id: nid(), type: 'box', x, y, z: 0, sx, sy, sz: 2, color: '#5a8aff', material: 'plastic', name: 'step', canCollide: true, anchored: true, })]; } function makeCeilingBlock(nid, x, y, length) { return [basePrim({ id: nid(), type: 'box', x, y, z: 0, sx: length, sy: 1, sz: 3, color: '#446', material: 'plastic', name: 'ceiling', canCollide: true, anchored: true, })]; } /** Сгенерировать воксельный пол для уровня (cotton-green верх + 4 stone). */ function addFloorBlocks(blocks, sx, finishX, holes) { const inHole = (col) => { for (const [a, b] of holes) if (col >= a && col < b) return true; return false; }; let count = 0; for (let dx = 0; dx <= (finishX - sx) + 4; dx++) { if (inHole(dx)) continue; const wx = sx + dx; for (const z of [-1, 0, 1]) { blocks.push({ x: wx, y: 0, z, mass: 1, type: 'cotton-green', visible: true, anchored: true, folderId: null, canCollide: true }); for (const y of [-1, -2, -3, -4]) { blocks.push({ x: wx, y, z, mass: 1, type: 'stone', visible: true, anchored: true, folderId: null, canCollide: true }); } count += 5; } } return count; } /** * Главная функция. Принимает массив уровней, возвращает примитивы и блоки. * Не трогает декор (z=13/21/22/35/50) — это отдельная функция (см. ниже). */ export function buildSceneObjectsForLevels(levels, options = {}) { const nid = makeIdAlloc(options.startId || 30000); const primitives = []; const blocks = []; const updatedLevels = []; for (const L of levels) { const sx = L.spawnX || 0; const width = L.width || 240; const finishX = (L.finishX != null) ? L.finishX : (sx + width - 5); const holes = L.holes || []; // ПОЛ addFloorBlocks(blocks, sx, finishX, holes); // СТУПЕНЬКИ for (const st of (L.steps || [])) { primitives.push(...makeStep(nid, st.x, st.y, st.sx || 2, st.sy || 0.5)); } // ШИПЫ for (const dx of (L.spikes || [])) { primitives.push(...makeSpike(nid, sx + dx)); } for (const dx of (L.spikesTop || [])) { primitives.push(...makeSpikeTop(nid, sx + dx)); } // ТРАМПЛИНЫ const trampsForScript = []; for (const t of (L.tramps || [])) { const rel = typeof t === 'number' ? t : (t.x != null ? t.x - sx : 0); trampsForScript.push(rel); primitives.push(...makeTramp(nid, sx + rel)); } // SPEED PADS const padsForScript = []; for (const p of (L.speedPads || [])) { const rel = typeof p === 'number' ? p : (p.x != null ? p.x - sx : 0); padsForScript.push(rel); primitives.push(...makeSpeedPad(nid, sx + rel)); } // JUMP ORBS const orbsForScript = []; for (const orb of (L.jumpOrbs || [])) { const wx = orb.worldX != null ? orb.worldX : sx + (orb.x || 0); const wy = orb.worldY != null ? orb.worldY : (orb.y || 3.5); const { items, primId } = makeJumpOrb(nid, wx, wy); primitives.push(...items); orbsForScript.push({ primId, worldX: wx, worldY: wy }); } // GRAVITY FLIPS const flipsForScript = []; for (const gf of (L.gravityFlips || [])) { const wx = gf.worldX != null ? gf.worldX : sx + (gf.x || 0); const { items, primId } = makeGravityFlip(nid, wx); primitives.push(...items); flipsForScript.push({ primId, worldX: wx }); } // CEILINGS (отрезки 4-блоков) for (const cl of (L.ceilings || [])) { const wx0 = cl.worldX != null ? cl.worldX : sx + (cl.x || 0); const len = cl.length || 4; const ceilingY = L.ceilingY || 7; for (let i = 0; i < len; i += 4) { primitives.push(...makeCeilingBlock(nid, wx0 + i + 2, ceilingY, 4)); } } // MOVING PLATFORMS const movingForScript = []; for (const mp of (L.movingPlatforms || [])) { const wx = mp.worldX != null ? mp.worldX : sx + (mp.x || 0); const yMin = mp.yMin != null ? mp.yMin : 1.0; const yMax = mp.yMax != null ? mp.yMax : 2.5; const period = mp.period != null ? mp.period : 3.0; const { items, primId } = makeMoving(nid, wx, yMin); primitives.push(...items); movingForScript.push({ primId, worldX: wx, yMin, yMax, period, phase: mp.phase != null ? mp.phase : Math.random() * 6.28, }); } updatedLevels.push({ id: L.id, name: L.name || `Уровень ${L.id}`, width, spawnX: sx, finishX, holes, spikes: L.spikes || [], spikesTop: L.spikesTop || [], tramps: trampsForScript, speedPads: padsForScript, movingPlatforms: movingForScript, jumpOrbs: orbsForScript, gravityFlips: flipsForScript, ceilings: L.ceilings || [], steps: L.steps || [], doubleJump: !!L.doubleJump, ceilingY: L.ceilingY || null, }); } return { primitives, blocks, updatedLevels }; }