import { TERRAIN_DEPTH, WORLD_SCALE } from '../config.js'; // Ground is stored as left-to-right surface polylines (easy to hand-author), // each converted into a chain of angled static rectangle bodies extruded // downward - simpler and more robust than concave polygon decomposition. export default class Terrain { constructor(scene, levelData) { this.scene = scene; this.bodies = []; this.graphics = scene.add.graphics(); for (const segment of levelData.terrain) { this._buildSegment(segment.points); this._drawSegment(segment.points); } if (levelData.obstacles) { for (const obstacle of levelData.obstacles) { this._buildObstacle(obstacle); } } } _buildSegment(points) { for (let i = 0; i < points.length - 1; i++) { const a = points[i]; const b = points[i + 1]; const dx = b.x - a.x; const dy = b.y - a.y; const length = Math.hypot(dx, dy); if (length < 1) continue; const angle = Math.atan2(dy, dx); const midX = (a.x + b.x) / 2; const midY = (a.y + b.y) / 2 + (TERRAIN_DEPTH / 2) * Math.cos(angle); const body = this.scene.matter.add.rectangle(midX, midY, length, TERRAIN_DEPTH, { isStatic: true, angle, friction: 0.95, label: 'terrain', }); this.bodies.push(body); } } _buildObstacle(obstacle) { if (obstacle.type !== 'ramp') return; const body = this.scene.matter.add.rectangle(obstacle.x, obstacle.y, obstacle.width, obstacle.height, { isStatic: true, angle: obstacle.angle || 0, friction: 0.95, label: 'terrain', }); this.bodies.push(body); } _drawSegment(points) { const g = this.graphics; g.fillStyle(0x3c8f3c, 1); g.beginPath(); g.moveTo(points[0].x, points[0].y + TERRAIN_DEPTH); for (const p of points) g.lineTo(p.x, p.y); const last = points[points.length - 1]; g.lineTo(last.x, last.y + TERRAIN_DEPTH); g.closePath(); g.fillPath(); g.lineStyle(4 * WORLD_SCALE, 0x2c6e2c, 1); g.beginPath(); g.moveTo(points[0].x, points[0].y); for (const p of points) g.lineTo(p.x, p.y); g.strokePath(); } }