import Matter from "https://cdn.jsdelivr.net/npm/matter-js@0.19.0/+esm"; export class GameWorld { constructor() { this.engine = Matter.Engine.create(); this.world = this.engine.world; this.engine.world.gravity.y = 0;//0.01; Matter.Runner.run(this.engine); let ground = Matter.Bodies.rectangle(400, 600, 800, 50, { isStatic: true }); Matter.World.add(this.world, ground); this.levelData = null; this.currentLevel = 0; this.waypoints = []; this.prevWaypointsReached = []; this.waypointsReached = []; this.obstacles = []; this.robots = []; this.lineColor = `rgba(0,0,0,1)`; this.floorLines = [ { x: 170, y: 75 }, { x: 300, y: 75 }, { x: 500, y: 300 }, { x: 500, y: 400 }, { x: 450, y: 450 }, { x: 400, y: 450 }, { x: 100, y: 350 }, { x: 50, y: 250 }, { x: 80, y: 150 }, { x: 170, y: 75 }, // add more points as needed ]; this.lineWidth = 3; } reset(player = null) { this.robots = [] this.obstacles = [] this.waypoints = []; this.waypointsReached = []; this.prevWaypointsReached = []; Matter.World.clear(this.world); // Clear the world without resetting the engine let level = this.levelData[this.currentLevel]; if (player) { console.log("Resetting player position to:", level.robots["player"]["position"]); console.log(this.robots); player.x = level.robots["player"]["position"]["x"]; player.y = level.robots["player"]["position"]["y"]; this.addRobot(player); } for (let i = 0; i < level.obstacles.length; i++) { let obstacle = level.obstacles[i]; this.addObstacle(obstacle.vertices, obstacle.position, obstacle.strokeColor, obstacle.fillColor); } for (let i = 0; i < level.waypoints.length; i++) { let obstacle = level.waypoints[i]; console.log("Adding waypoint:", obstacle); this.addWaypoint(obstacle.vertices, obstacle.position, obstacle.strokeColor, obstacle.fillColor); this.waypointsReached.push(false); // Initialize as not reached } this.prevWaypointsReached = [...this.waypointsReached]; console.log(level.waypointsReached) // this.addObstacle([ // { x: -100, y: -100 }, // Vertex 1 // { x: 100, y: -100 }, // Vertex 2 // { x: 100, y: 100 }, // Vertex 3 // { x: -100, y: 100 } // Vertex 4 // ], { x: 400, y: 300 }); // this.addObstacle([ // { x: 300, y: 380 }, // Vertex 1 // { x: 420, y: 380 }, // Vertex 2 // { x: 350, y: 550 }, // Vertex 3 // { x: 280, y: 420 } // Vertex 4 // ], { x: 400, y: -50 }); } update() { // Update the physics simulation for (let id in this.robots) { this.robots[id].update(this); } for (let i = 0; i < this.waypoints.length; i++) { if (this.waypointsReached[i]) { } else { let waypoint = this.waypoints[i]; let playerPos = this.robots[0].body.position; let waypointBounds = waypoint.bounds; if (Matter.Bounds.contains(waypointBounds, playerPos)) { console.log("Player is inside the waypoint's bounding box."); this.waypointsReached[i] = true; // Mark this waypoint as reached } } } //this.checkPlayerCompletedTask(); Matter.Engine.update(this.engine); } waypointsReached() { return this.waypointsReached; } waypointsReachedChanged() { // Check if waypointsReached has changed since the last update let changed = false; for (let i = 0; i < this.waypointsReached.length; i++) { if (this.waypointsReached[i] !== this.prevWaypointsReached[i]) { changed = true; break; } } // Update prevWaypointsReached to the current state this.prevWaypointsReached = [...this.waypointsReached]; return changed; } // Needs to be updated to handle different win conditions // checkPlayerCompletedTask() { // let playerPos = this.robots[0].body.position; // let waypointBounds = this.waypoints[0].bounds; // if (Matter.Bounds.contains(waypointBounds, playerPos)) { // console.log("Player is inside the waypoint's bounding box."); // return true; // } // return false; // } addWaypoint(vertices, position = { x: 0, y: 0 }, strokeColor = "yellow", fillColor = "yellow") { // Convert the polygon points into a Matter.js body const sortedVertices = Matter.Vertices.clockwiseSort(vertices); // Compute centroid const centroid = this.getCentroid(sortedVertices); // Create relative vertices centered around centroid const relativeVertices = sortedVertices.map(v => ({ x: v.x - centroid.x, y: v.y - centroid.y })); const body = Matter.Bodies.fromVertices( centroid.x, centroid.y, [relativeVertices], { isSensor: true, isStatic: true, label: "zone", }, true ); body.strokeColor = 'rgba(0, 0, 255, 0.1)'; body.fillColor = 'rgba(0, 0, 255, 0.1)'; Matter.World.add(this.world, body); this.waypoints.push(body); } getCentroid(vertices) { let area = 0, cx = 0, cy = 0; for (let i = 0; i < vertices.length; i++) { const p1 = vertices[i]; const p2 = vertices[(i + 1) % vertices.length]; const cross = p1.x * p2.y - p2.x * p1.y; area += cross; cx += (p1.x + p2.x) * cross; cy += (p1.y + p2.y) * cross; } area *= 0.5; if (area === 0) return vertices[0]; // fallback to first point cx /= (6 * area); cy /= (6 * area); return { x: cx, y: cy }; } addObstacle(vertices, strokeColor = "black", fillColor = "gray") { // Sort vertices clockwise (Matter requires this) const sortedVertices = Matter.Vertices.clockwiseSort(vertices); // Compute centroid const centroid = this.getCentroid(sortedVertices); // Create relative vertices centered around centroid const relativeVertices = sortedVertices.map(v => ({ x: v.x - centroid.x, y: v.y - centroid.y })); // Create the body with options to reduce auto corrections const body = Matter.Bodies.fromVertices( centroid.x, centroid.y, [relativeVertices], { isStatic: true, // If your Matter.js supports these: // removeCollinear: 0.01, // minimumArea: 10 }, true // autoHull (convex decomposition) enabled ); body.strokeColor = strokeColor; body.fillColor = fillColor; Matter.World.add(this.world, body); this.obstacles.push(body); } addRobot(robot) { console.log("added robot"); // Create the robot's Matter.js body let robotBody = Matter.Bodies.fromVertices(robot.x, robot.y, [robot.hull], { friction: 0.05, frictionAir: 0.02, // Air resistance slows it down naturally restitution: 0.2, // Slight bounce }); // Add to the world Matter.World.add(this.world, robotBody); robot.body = robotBody; this.robots.push(robot); } // Return the floor color based on (x, y) coordinates getFloorColor(x, y) { const isUnderLine = this.isPointNearLine(x, y, this.floorLines, this.lineWidth); return isUnderLine; // if (isUnderLine) { // console.log("Robot is over a floor line"); // } } // Draw the game world (e.g., obstacles, background) draw(ctx) { //this.render(ctx); this.drawFloorLines(ctx); // Draw obstacles ctx.strokeStyle = "gray"; // Obstacle outline color ctx.lineWidth = 2; // Optional: to make the outline thicker // Draw obstacles this.obstacles.forEach(body => { ctx.beginPath(); let vertices = body.vertices; ctx.moveTo(vertices[0].x, vertices[0].y); for (let i = 1; i < vertices.length; i++) { ctx.lineTo(vertices[i].x, vertices[i].y); } ctx.fillStyle = body.fillColor;; ctx.strokeStyle = body.strokeColor; ctx.closePath(); ctx.fill(); ctx.stroke(); }); // Draw Waypoints for (let w = 0; w < this.waypoints.length; w++) { let body = this.waypoints[w]; ctx.beginPath(); let vertices = body.vertices; ctx.moveTo(vertices[0].x, vertices[0].y); for (let i = 1; i < vertices.length; i++) { ctx.lineTo(vertices[i].x, vertices[i].y); } if (this.waypointsReached[w]) { ctx.globalAlpha = 0.05; // Applies to all drawing ctx.fillStyle = "green"; // Color for reached waypoints ctx.strokeStyle = "rgba(0, 128, 0, 0.2)"; // green with 80% opacity } else { ctx.globalAlpha = 0.2; // Applies to all drawing ctx.fillStyle = body.fillColor; // Default color for waypoints ctx.strokeStyle = body.strokeColor; } ctx.closePath(); ctx.fill(); ctx.globalAlpha = 1.0; // Reset after if needed ctx.stroke(); } this.robots.forEach(robot => { let body = robot.body; // Get the Matter.js body from the robot object ctx.strokeStyle = "blue"; let vertices = body.vertices; ctx.beginPath(); ctx.moveTo(vertices[0].x, vertices[0].y); for (let i = 1; i < vertices.length; i++) { ctx.lineTo(vertices[i].x, vertices[i].y); } ctx.closePath(); ctx.stroke(); robot.draw(ctx); // Draw the robot's hull and sensors }); } drawFloorLines(ctx) { if (this.floorLines.length < 2) return; ctx.strokeStyle = this.lineColor; ctx.lineWidth = this.lineWidth; ctx.beginPath(); ctx.moveTo(this.floorLines[0].x, this.floorLines[0].y); for (let i = 1; i < this.floorLines.length; i++) { ctx.lineTo(this.floorLines[i].x, this.floorLines[i].y); } ctx.stroke(); } resizeCanvasToDisplaySize(ctx) { let canvas = ctx.canvas; const width = canvas.clientWidth; const height = canvas.clientHeight; if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } } render(ctx) { let canvas = ctx.canvas; this.resizeCanvasToDisplaySize(canvas); // 👈 ensures drawing resolution matches display size ctx.clearRect(0, 0, canvas.width, canvas.height); game.draw(ctx); requestAnimationFrame(render); } rayCast(startX, startY, endX, endY, ignoreBodies = []) { let closestIntersection = null; let startPoint = { x: startX, y: startY }; let endPoint = { x: endX, y: endY }; ignoreBodies.push(...this.waypoints); // Add all obstacles to the ignore list // Get all bodies in the world let allBodies = Matter.Composite.allBodies(this.engine.world); // ✅ Filter out ignored bodies instead of removing them let filteredBodies = allBodies.filter(body => !ignoreBodies.includes(body)); // Debugging //console.log("Ignoring bodies: ", ignoreBodies.map(b => b.id)); //console.log("Filtered bodies: ", filteredBodies.map(b => b.id)); // Perform raycast only on the filtered bodies let hit = this.getRayHitPoint(startPoint, endPoint, filteredBodies); if (hit) { closestIntersection = hit.point; } return closestIntersection; } getRayHitPoint(start, end, bodies) { let closestHit = null; let minDistance = Infinity; bodies.forEach(body => { let vertices = body.vertices; // Loop through each edge of the polygon body for (let i = 0; i < vertices.length; i++) { let v1 = vertices[i]; let v2 = vertices[(i + 1) % vertices.length]; // Wrap around to close the shape let intersection = this.getLineIntersection(start, end, v1, v2); if (intersection) { let distance = Matter.Vector.magnitude(Matter.Vector.sub(intersection, start)); // Keep track of the closest intersection if (distance < minDistance) { minDistance = distance; closestHit = { point: intersection, body }; } } } }); return closestHit; } // Line intersection helper function getLineIntersection(p1, p2, p3, p4) { let denom = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y); if (denom === 0) return null; // Parallel lines, no intersection let ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denom; let ub = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denom; if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) { return { x: p1.x + ua * (p2.x - p1.x), y: p1.y + ua * (p2.y - p1.y) }; } return null; // No valid intersection } isPointNearLine(x, y, linePoints, width) { const threshold = width / 2; for (let i = 0; i < linePoints.length - 1; i++) { const p1 = linePoints[i]; const p2 = linePoints[i + 1]; const dist = this.pointToSegmentDistance(x, y, p1.x, p1.y, p2.x, p2.y); if (dist <= threshold) { return true; } } return false; } pointToSegmentDistance(px, py, x1, y1, x2, y2) { const dx = x2 - x1; const dy = y2 - y1; const lengthSquared = dx * dx + dy * dy; if (lengthSquared === 0) { // p1 == p2 const dxp = px - x1; const dyp = py - y1; return Math.sqrt(dxp * dxp + dyp * dyp); } // Project point onto the segment let t = ((px - x1) * dx + (py - y1) * dy) / lengthSquared; t = Math.max(0, Math.min(1, t)); // clamp to segment const projX = x1 + t * dx; const projY = y1 + t * dy; const dxp = px - projX; const dyp = py - projY; return Math.sqrt(dxp * dxp + dyp * dyp); } }