xxxxxxxxxx
1384
/*
sim environment to test stuff for https://www.codingame.com/ide/puzzle/mars-lander-episode-3
TODO:
- [ ] dont add colliding lines
- [ ] physics
- [ ] 'lander' (player)
- [ ] lines into quadtree
- [ ] collision detection using quadtree
- [ ] pathfinding using A* & raymarch
- [ ] path to bezier curve
- [ ] automatic fly navigation
class LanderStateMachine {
constructor() {
this.state = 'Initialization';
}
update() {
switch (this.state) {
case 'Initialization':
this.initialize();
break;
case 'User Input Handling':
this.handleInput();
break;
case 'Physics Update':
this.updatePhysics();
break;
case 'Collision Detection':
this.detectCollisions();
break;
case 'Render':
this.render();
break;
case 'Reset/GameOver':
this.resetOrGameOver();
break;
}
}
initialize() {
// Load resources, set up initial conditions
// Transition to 'User Input Handling' after initialization
this.state = 'User Input Handling';
}
handleInput() {
// Process user inputs to adjust lander's thrust and orientation
// Always transition to 'Physics Update'
this.state = 'Physics Update';
}
updatePhysics() {
// Apply gravity, update velocity and position
// Always transition to 'Collision Detection'
this.state = 'Collision Detection';
}
detectCollisions() {
// Use quadtree to detect collisions with the terrain
// Transition to 'Render' if no collision, else 'Reset/GameOver'
if (this.collidesWithTerrain()) {
this.state = 'Reset/GameOver';
} else {
this.state = 'Render';
}
}
render() {
// Draw the lander, trajectory, and terrain
// Transition back to 'User Input Handling' to continue the simulation
this.state = 'User Input Handling';
}
resetOrGameOver() {
// Handle game over logic or reset the lander
// Transition to 'Initialization' to restart or end the simulation
this.state = 'Initialization';
}
collidesWithTerrain() {
// Implement collision detection logic using quadtree
// Return true if a collision is detected, false otherwise
}
}
// Usage
const landerSM = new LanderStateMachine();
function draw() {
landerSM.update();
}
*/
const GRAVITY = -3.711; // Upwards, because the coordinate system is inverted
const LANDER_TRAJECTORY_STEPS = 10;
const INITIAL_ANGLE = 100; // 90 is DOWN aka up for the lander, its complicated.
const INITIAL_THRUST = 4; // should 0 if not debugging
let openSetLander = [];
let closedSetLander = new Map();
let landingPathFound = false;
let bestLanderNode = null;
// Define your flat landing zone (replace these with your actual values)
let flatLandingZone = {
startX: 2000,
endX: 3500,
y: 500,
midX: 2750 // midpoint of the landing zone
};
// ----- Helper Functions -----
// Create a unique hash for a state to reduce duplicate expansion (rounding for tolerance)
function stateHashLander(state) {
return `${Math.round(state.x)},${Math.round(state.y)},${Math.round(state.vx)},${Math.round(state.vy)},${Math.round(state.angle)},${state.thrust},${Math.round(state.fuel)}`;
}
// Heuristic: combines distance from flat zone, nonzero angle, and high speeds.
function heuristicLander(state, flatZone) {
let dx = state.x - flatZone.midX;
let dy = state.y - flatZone.y;
let dist = Math.sqrt(dx * dx + dy * dy);
let anglePenalty = Math.abs(state.angle);
let speedPenalty = Math.abs(state.vx) + Math.abs(state.vy);
return dist + anglePenalty * 10 + speedPenalty * 5;
}
// Check if a state meets safe landing conditions.
function isLandingState(state, flatZone) {
if (state.x < flatZone.startX || state.x > flatZone.endX) return false;
if (Math.abs(state.angle) > 1) return false; // nearly vertical only
if (Math.abs(state.vy) > 40) return false;
if (Math.abs(state.vx) > 20) return false;
if (Math.abs(state.y - flatZone.y) > 5) return false;
return true;
}
// Simulate one tick given a state and a chosen action,
// enforcing a maximum change of ±15° for angle and ±1 for thrust.
function simulateTickLander(state, action) {
const GRAVITY = 3.711;
let newAngle = state.angle;
if (action.targetAngle > state.angle) {
newAngle = Math.min(state.angle + 15, action.targetAngle, 90);
} else if (action.targetAngle < state.angle) {
newAngle = Math.max(state.angle - 15, action.targetAngle, -90);
}
let newThrust = state.thrust;
if (action.targetThrust > state.thrust) {
newThrust = Math.min(state.thrust + 1, action.targetThrust, 4);
} else if (action.targetThrust < state.thrust) {
newThrust = Math.max(state.thrust - 1, action.targetThrust, 0);
}
let rad = newAngle * Math.PI / 180;
let ax = newThrust * Math.sin(rad);
let ay = newThrust * Math.cos(rad) - GRAVITY;
let newVx = state.vx + ax;
let newVy = state.vy + ay;
let newX = state.x + newVx;
let newY = state.y + newVy;
let newFuel = Math.max(state.fuel - newThrust, 0);
return {
x: newX,
y: newY,
vx: newVx,
vy: newVy,
angle: newAngle,
thrust: newThrust,
fuel: newFuel,
tick: state.tick + 1
};
}
// Initialize the A* search with an initial state and flat landing zone.
function initAstarLander(initialState, flatZone) {
openSetLander = [];
closedSetLander = new Map();
landingPathFound = false;
bestLanderNode = null;
flatLandingZone = flatZone;
let initialNode = {
state: initialState,
parent: null,
action: null,
g: 0,
h: heuristicLander(initialState, flatZone),
f: heuristicLander(initialState, flatZone),
children: []
};
openSetLander.push(initialNode);
}
// ----- Steppable A* Function for Mars Lander -----
// Call this function repeatedly (e.g., a set number of iterations per draw() tick).
// Returns true if a landing path has been found (or search exhausted), false otherwise.
function stepAstarLander() {
if (openSetLander.length === 0) {
console.log("No path found!");
return true; // End search: no solution exists
}
// Get the node with the lowest f-score.
openSetLander.sort((a, b) => a.f - b.f);
let current = openSetLander.shift();
// Debug: visualize the expanded node.
debugDrawNode(current);
// Check if this state meets landing conditions.
if (isLandingState(current.state, flatLandingZone)) {
landingPathFound = true;
bestLanderNode = current;
debugDrawPath(current);
return true; // Path found
}
let currentHash = stateHashLander(current.state);
if (!closedSetLander.has(currentHash) || closedSetLander.get(currentHash) > current.g) {
closedSetLander.set(currentHash, current.g);
// Expand neighbors by trying various thrust (0-4) and angle changes (±15° steps).
for (let thrust = 0; thrust <= 4; thrust++) {
for (let angleDelta of [-15, 0, 15]) {
let targetAngle = current.state.angle + angleDelta;
targetAngle = Math.max(Math.min(targetAngle, 90), -90);
let action = { targetAngle: targetAngle, targetThrust: thrust };
let newState = simulateTickLander(current.state, action);
// Discard states outside simulation bounds.
if (newState.x < 0 || newState.x > 7000 || newState.y < 0 || newState.y > 3000) continue;
// For cost, we use the thrust value as a proxy for fuel usage.
let cost = thrust;
let newG = current.g + cost;
let newH = heuristicLander(newState, flatLandingZone);
let childNode = {
state: newState,
parent: current,
action: action,
g: newG,
h: newH,
f: newG + newH,
children: []
};
current.children.push(childNode);
debugDrawEdge(current, childNode);
openSetLander.push(childNode);
}
}
}
return false; // Continue searching
}
// Reconstruct the optimal actions queue from the final node back to the start.
function reconstructLanderPath(node) {
let actionsQueue = [];
while (node.parent !== null) {
actionsQueue.unshift(node.action);
node = node.parent;
}
return actionsQueue;
}
// ----- Debug Visualization Stub Functions -----
// Replace these with your canvas drawing code as needed.
function debugDrawNode(node) {
// Example: draw a circle at (node.state.x, node.state.y)
console.log("Draw node at:", node.state.x, node.state.y);
}
function debugDrawEdge(parent, child) {
// Example: draw a line from parent.state to child.state
console.log("Draw edge from:", parent.state.x, parent.state.y, "to", child.state.x, child.state.y);
}
function debugDrawPath(node) {
let path = [];
while (node) {
path.push({ x: node.state.x, y: node.state.y });
node = node.parent;
}
console.log("Final path:", path.reverse());
}
const lander = {
reset: function () {
this.position = { x: 350, y: 150 };
this.angle = INITIAL_ANGLE;
this.fuel = 100;
this.acceleration = INITIAL_THRUST;
this.velocity = { x: 0, y: 0 };
this.SAFELY_LANDING_FLAG = false; // calculated in plotTrajectory fucntion
},
position: { x: 350, y: 150 }, // Initial position
fuel: 100,
velocity: { x: 0, y: 0 }, // Initial velocity
acceleration: INITIAL_THRUST, // Initial acceleration
angle: INITIAL_ANGLE, // Initial angle
size: 20, // Size of the triangle (distance from center to vertex)
drawTrajectory: function () {
push(); // Start a new drawing state
stroke("rgba(189,59,250,0.72)"); // Default trajectory color, e.g., purple
strokeWeight(2); // Set the thickness of the trajectory line
noFill(); // Ensure the shape is not filled
let pos = createVector(lander.position.x, lander.position.y); // Start from the current position
let velocity = createVector(lander.velocity.x, lander.velocity.y);
let acceleration = p5.Vector.fromAngle(radians(lander.angle))
.mult(lander.acceleration)
.add(createVector(0, GRAVITY));
for (let t = 1; t <= 50; t++) {
// Calculate the new position
let newPos = pos.copy();
let deltaPosFromVel = p5.Vector.mult(velocity, 1); // Time step of 1 second
let deltaPosFromAcc = p5.Vector.mult(acceleration, 0.5 * 1 * 1);
newPos.add(deltaPosFromVel).add(deltaPosFromAcc);
// Update velocity
velocity.add(p5.Vector.mult(acceleration, 1));
// Check for intersection with terrain
// INTERSECTION == CRASH, EXPLOSION, COLLISION!
// from last line position
let l = createLineObj(pos.x, pos.y, newPos.x, newPos.y);
// query lines around line in quadtree.
if (anyLinesIntersectQT(l)) {
// if intersction with any lines, we are colliding with the line here.
print(velocity)
stroke("red"); // Change color to red if intersection is detected
fill("orange");
// Draw up to the point of intersection
beginShape();
vertex(pos.x, pos.y);
vertex(newPos.x, newPos.y);
endShape();
// Add visual explosion marker at point of intersection
star(newPos.x, newPos.y, 10, 7, 12);
break; // Stop drawing the trajectory
} else {
// If no intersection, continue drawing the trajectory
beginShape();
vertex(pos.x, pos.y);
vertex(newPos.x, newPos.y);
endShape();
}
// Prepare for the next iteration
pos = newPos;
}
pop(); // Restore original drawing state
},
calcPositionAtTime: function (initialPos, t) {
let pos = createVector(initialPos.x, initialPos.y); // Original position
let vel = createVector(this.velocity.x, this.velocity.y); // Constant initial velocity
// Acceleration calculation remains the same
let acc = p5.Vector.fromAngle(radians(this.angle)).mult(this.acceleration);
acc.add(createVector(0, GRAVITY)); // Adding gravity
let deltaPosFromVel = p5.Vector.mult(vel, t);
let deltaPosFromAcc = p5.Vector.mult(acc, 0.5 * t * t);
let newPos = pos.add(deltaPosFromVel).add(deltaPosFromAcc);
return newPos;
},
update: function () {
needflip = inverty_chk.checked() && ! invertx_chk.checked()
// Handle rotation
if (keyIsDown(65)) {
// A key for left rotation
this.angle -= needflip? -5 : 5;
}
if (keyIsDown(68)) {
// D key for right rotation
this.angle += needflip? -5 : 5;
}
// Adjust acceleration and fuel consumption
if (keyIsDown(87) && this.fuel > 0) {
// W key for increasing thrust
this.acceleration = min(this.acceleration + 1, 12); // Max thrust to counteract gravity
this.fuel -= this.acceleration / 60; // Fuel consumption rate, adjust based on your game's time scale
} else if (keyIsDown(83)) {
// S key for decreasing thrust
this.acceleration = max(this.acceleration - 1, 0);
}
// Apply thrust based on lander's angle and gravity
this.velocity.x += (this.acceleration * cos(radians(this.angle))) / 60;
this.velocity.y +=
(this.acceleration * sin(radians(this.angle)) + GRAVITY) / 60;
// Update position
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
// Reset if colliding or out of bounds
if (this.isColliding() || this.isOutOfBounds()) {
if (this.SAFELY_LANDING_FLAG){
// TODO ACTUAL LANDING AND THEN ELSE THE OTHER PART. I CBA I TRIED THIS. CTRL-ZED MY WAY OUT OF THIS BECAUSE ITS IMPOSSIBLE. LANDER GETS STUCK. NEED CLEAN WAY TO DO THIS. GO NEXT
}
disable_resets_chk.checked() ? null : this.reset();
}
},
isOutOfBounds: function () {
const p = this.position;
return p.x < 0 || p.x > width || p.y < 0 || p.y > width;
},
isColliding: function () {
// check if the obj is colliding
let vertices = calculateTriangleVertices(
this.position,
this.size,
this.angle
);
let bbox = calculateBoundingBox(vertices);
let candidates = lines_tree.retrieve(bbox);
let collided = false;
for (let candidate of candidates) {
for (let i = 0; i < vertices.length; i++) {
let nextVertex = vertices[(i + 1) % vertices.length];
let landerLine = {
x1: vertices[i].x,
y1: vertices[i].y,
x2: nextVertex.x,
y2: nextVertex.y,
};
if (linesIntersect(landerLine, candidate)) {
console.log("Collision detected");
// Simple collision response: Stop movement
return true;
}
}
}
return false;
},
draw: function () {
// this outline might serve as debug,
// it shows the outline of the lander boundaries (triangle)
// Calculate the vertices of the triangle based on the current position, size, and angle
// let vertices = calculateTriangleVertices(
// this.position,
// this.size,
// this.angle
// );
// // Draw the triangle
// triangle(
// vertices[0].x,
// vertices[0].y,
// vertices[1].x,
// vertices[1].y,
// vertices[2].x,
// vertices[2].y
// );
// Run a set number of A* iterations per tick (adjust AStarStepSlider.value or a constant)
let iterations = 10; // or use a slider value
for (let i = 0; i < iterations; i++) {
if (stepAstarLander()) {
// If a landing path is found or search is complete, break out.
break;
}
}
// Call your usual simulation or visualization drawing functions.
// if thrust, show flames
if (this.acceleration > 0 ){
let accV = p5.Vector.fromAngle(radians(this.angle)).mult(this.acceleration);
accV.rotate(radians(180))
push()
translate(this.position.x, this.position.y);
drawThrustEffect(accV, this.acceleration);
pop();
}
// lander image
push()
translate(this.position.x, this.position.y);
imageMode(CENTER)
rotate(radians(this.angle + 90))
image(landerImg, 0, 0);
pop();
},
};
function drawThrustEffect(accV, acceleration) {
// Define the tip of the cone
let cone_tip = createVector(0, 0);
// Scale the base of the cone according to acceleration, ensuring the cone shape is maintained
let cone_base = accV.copy().mult(3 * acceleration);
let cone_length = cone_base.mag();
let cone_direction = cone_base.copy().normalize();
// Colors for the flame
let colorStart = color(255, 223, 0); // Bright yellow
let colorEnd = color(255, 0, 0); // Red
// Particle generation loop
for (let i = 0; i < 100; i++) {
// Positioning along the cone's length
let positionFactor = random(); // Random factor for position along the cone's length
let particlePosition = cone_tip.copy().add(cone_direction.copy().mult(cone_length * positionFactor));
// Adjusting the width of the cone base proportionally to its length
let cone_width = map(particlePosition.mag(), 0, cone_length, 0, cone_length / 3);
let perpDirection = createVector(-cone_direction.y, cone_direction.x);
let displacement = perpDirection.copy().mult((random() - 0.5) * cone_width);
particlePosition.add(displacement);
// Determine the particle size based on its distance from the tip
let particleSize = map(positionFactor, 0, 1, 5, 1); // Larger at base, smaller at tip
// Color interpolation based on position along the cone
let particleColor = lerpColor(colorStart, colorEnd, positionFactor);
// Set drawing properties
fill(particleColor);
noStroke();
// Shape variation
let shapeType = floor(random(3));
switch (shapeType) {
case 0: // Circle
ellipse(particlePosition.x, particlePosition.y, particleSize, particleSize);
break;
case 1: // Square
rect(particlePosition.x, particlePosition.y, particleSize, particleSize);
break;
case 2: // Triangle
triangle(
particlePosition.x, particlePosition.y - particleSize,
particlePosition.x - particleSize, particlePosition.y + particleSize,
particlePosition.x + particleSize, particlePosition.y + particleSize
);
break;
}
}
}
function star(x, y, radius1, radius2, npoints) {
/* draw a starshape, for explosion symbol */
let angle = TWO_PI / npoints;
let halfAngle = angle / 2.0;
push();
beginShape();
for (let a = 0; a < TWO_PI; a += angle) {
let sx = x + cos(a) * radius2;
let sy = y + sin(a) * radius2;
vertex(sx, sy);
sx = x + cos(a + halfAngle) * radius1;
sy = y + sin(a + halfAngle) * radius1;
vertex(sx, sy);
}
endShape(CLOSE);
pop();
}
function debugLander() {
// Display lander's state information
push();
fill(0);
textSize(12);
text(
`Speed: ${Math.sqrt(
lander.velocity.x ** 2 + lander.velocity.y ** 2
).toFixed(2)}`,
10,
20
);
text(`Acceleration: ${lander.acceleration.toFixed(2)}`, 10, 35);
text(`Angle: ${lander.angle.toFixed(2)}`, 10, 50);
text(`Fuel: ${lander.fuel.toFixed(1)}`, 10, 50 + 50 - 35);
// Calculate and draw bounding box
let vertices = calculateTriangleVertices(
lander.position,
lander.size,
lander.angle
);
let bbox = calculateBoundingBox(vertices);
stroke("rgba(255, 0, 0, 0.5)");
noFill();
// big bounding box aroudn the lander... bit too big TODO FIX !!!!!!!!!!!!
// but this is used to query the tree and that size is actually correct imo
rect(
bbox.x - bbox.width,
bbox.y - bbox.height,
bbox.width * 3,
bbox.height * 3
);
rect(
lander.position.x-50,
lander.position.y-50,
100,100
);
// bbox.x = bbox.x - bbox.width;
// bbox.y = bbox.y - bbox.height;
// bbox.width = bbox.width * 3;
// bbox.height = bbox.height * 3;
// Highlight quadtree queried items
let candidates = lines_tree.retrieve({
x:lander.position.x,
y:lander.position.y,
width:50,
height:50
});
for (let candidate of candidates) {
push()
strokeWeight(10)
stroke("rgba(0, 255, 0, 0.5)");
line(candidate.x1, candidate.y1, candidate.x2, candidate.y2);
pop()
}
pop();
}
let angle = 0;
let thrust = 0;
let lines_tree;
// these lines are just used for rendering, no collision detection
let landscape_lines;
let lander_debug_chk;
let buildmode_chk;
// these lines are just used for rendering, no collision detection
let buildmode_lines = [];
let disable_resets_chk;
function mouseOnCanvas() {
return !(mouseX < 0 || mouseX > width || mouseY < 0 || mouseY > height);
}
let bgImage;
let landerImg;
function preload() {
bgImage = loadImage("marslandscape 2d - DALL·E 2024-03-03 15.webp");
landerImg = loadImage('lander.png')
// note that the lines are under the .landscape attribute idfk why i had to do that
// otherwise it wouldnt load properly
landscape_lines = loadJSON('initial_landscape.json')
}
function setup() {
let c = createCanvas(700, 300);
c.parent("canvas-container");
bgImage.resize(width, height);
landerImg.resize(45,45);
strokeWeight(2);
// map the old lines to the quadtree style
landscape_lines = landscape_lines.landscape.map(function (l) {
return createLineObjFromOld(l);
});
lines_tree = new Quadtree({
x: 0,
y: 0,
width: width,
height: height,
});
frameRate(10);
createSlider(1, 30, 10, 1)
.input(function () {
frameRate(this.value());
print(`set framerate to ${this.value()}`);
})
.parent("controls-container");
landscape_lines.map((o) => lines_tree.insert(o));
invert_txt = createP("the inverting system is not done, it sucks");
invertx_chk = createCheckbox("invert X ");
inverty_chk = createCheckbox("invert Y").checked(true)
.input(function (btn) {
this.checked()
? (invertx_chk.show(), invert_txt.show())
: (invertx_chk.hide(), invertx_chk.checked(false), invert_txt.hide());
})
.parent("controls-container");
invert_txt.hide();
invert_txt.parent(invertx_chk);
inverty_chk.checked() ? null : invertx_chk.hide();
invertx_chk.parent(inverty_chk);
buildmode_tip = createP("When building, press R to reset points");
buildmode_chk = createCheckbox("builder mode")
.input(function (btn) {
if (this.checked()) {
buildmode_tip.show();
print("Enabled buildmode");
} else {
buildmode_tip.hide();
print("Disabled buildmode");
endBuild(false);
}
})
.parent("controls-container");
buildmode_tip.hide();
buildmode_tip.parent(buildmode_chk);
debugmode_chk = createCheckbox("debugmode")
.parent("controls-container")
.checked(false);
debugdiv = createDiv().parent(debugmode_chk);
quadtree_debug_region_chk = createCheckbox("debug quadtree")
.checked(true)
.parent(debugdiv);
lander_debug_chk = createCheckbox("debug lander")
.checked(true)
.parent(debugdiv);
debugmode_chk.input(function (e) {
if (this.checked()) {
quadtree_debug_region_chk.show();
lander_debug_chk.show();
} else {
lander_debug_chk.checked(false);
quadtree_debug_region_chk.checked(false);
quadtree_debug_region_chk.hide();
lander_debug_chk.hide();
}
});
// turn off other debugmodes if the main debug is off too at start, is here... for... antidebugging. yes.
if (!debugmode_chk.checked()) {
quadtree_debug_region_chk.checked(false);
lander_debug_chk.checked(false);
lander_debug_chk.hide()
quadtree_debug_region_chk.hide()
}
disable_resets_chk = createCheckbox('disable reset on collision').parent("controls-container");
// some debug features, export
// this button is hidden by default, its to export the lines as json
let exprtbtn = createButton("export lines to json")
.mousePressed(function (x) {
saveJSON(
{'landscape':landscape_lines}, "lines.json");
})
.hide();
// chkbox to enable visibility on export button(s)
let exprtchk = createCheckbox("show export buttons")
.input(function (btn) {
this.checked() ? exprtbtn.show() : exprtbtn.hide();
})
.parent("controls-container");
// set the chkbox as parent for the export button(s), so they follow it.
exprtbtn.parent(exprtchk);
}
function draw_landscape_lines(lines) {
for (const l of lines) {
line(l.x1, l.y1, l.x2, l.y2);
}
}
function renderGame() {
draw_landscape_lines(landscape_lines);
if (buildmode_chk.checked()) {
renderBuildMode();
} else {
gameloop();
lander.update();
lander.draw();
// lander.drawTrajectory();
plotTrajectory(50);
if (lander_debug_chk.checked()) {
debugLander();
}
}
if (quadtree_debug_region_chk.checked()) {
renderQuadtreeDebug();
}
}
const drawline = function (l) {
line(l.x1, l.y1, l.x2, l.y2);
};
function keyPressed() {
switch (key) {
case "r":
print("pressed R to reset buildmode lines");
buildmode_lines = [];
break;
default:
break;
}
}
function createLineObjFromOld(l) {
// converter function because I cba to replace the
// 6 instances of the old function :))))))
return createLineObj(l.x1, l.y1, l.x2, l.y2);
}
function createLineObj(x1, y1, x2, y2) {
// Calculate the top-left corner of the bounding box & size
const x = Math.min(x1, x2);
const y = Math.min(y1, y2);
const w = Math.abs(x2 - x1);
const h = Math.abs(y2 - y1);
// Adjust width and height to ensure non-zero dimensions for the quadtree
// Quadtree may require non-zero area to correctly index the item
const adjustedWidth = w === 0 ? 1 : w;
const adjustedHeight = h === 0 ? 1 : h;
// Return the original line object augmented with quadtree-compatible properties
return {
x1: x1,
y1: y1,
x2: x2,
y2: y2,
x: x,
y: y,
width: adjustedWidth,
height: adjustedHeight,
};
}
function mousePressed() {
if (buildmode_chk.checked() && mouseOnCanvas()) {
buildModeAddPoint();
}
}
function buildModeAddPoint() {
/* adds new LINES to the environment */
const l = buildmode_lines.length;
let MX = invertx_chk.checked() ? width-mouseX : mouseX
let MY = inverty_chk.checked() ? height-mouseY : mouseY
let side = MX < width / 2 ? -5 : width + 5;
let newx = min(max(MX, 10), width - 10);
let newy = min(max(MY, 10), height - 10);
let new_line;
endbuild = false; //TODO unused flag?
if (l > 0) {
last_line = buildmode_lines[l - 1];
if (abs(last_line.y2 - newy) < 5) {
newy = last_line.y2;
}
if (abs(last_line.x2 - newx) < 5) {
newx = last_line.x2;
}
line(last_line.x2, last_line.y2, newx, newy);
new_line = createLineObjFromOld({
x1: last_line.x2,
y1: last_line.y2,
x2: newx,
y2: newy,
});
} else {
let side = MX < width / 2 ? 0 : width;
line(side, newy, newx, newy);
new_line = createLineObjFromOld({ x1: side, y1: newy, x2: newx, y2: newy });
}
if (MX < 15 || MX > width - 15) {
let last_line = createLineObjFromOld({
x1: newx,
y1: newy,
x2: side,
y2: newy,
});
// we can append this here, the order doesnt matter
addBuilderLine(new_line);
addBuilderLine(last_line);
endBuild(true);
} else {
addBuilderLine(new_line);
}
}
function renderBuildMode() {
// build box
push();
stroke("rgba(10,192,232,0.91)");
line(15, 10, width - 15, 10);
line(15, height - 10, width - 15, height - 10);
line(15, 10, 15, height - 10);
line(width - 15, 10, width - 15, height - 10);
pop();
for (const l of buildmode_lines) {
push();
stroke(l.y1 == l.y2 ? "green" : "orange");
drawline(l);
pop();
}
const l = buildmode_lines.length;
let line_to_mouse;
let MX = invertx_chk.checked() ? width-mouseX : mouseX
let MY = inverty_chk.checked() ? height-mouseY : mouseY
let side = MX < width / 2 ? 0 : width;
let newx = min(max(MX, 10), width - 10);
let newy = min(max(MY, 10), height - 10);
if (l > 0) {
last_line = buildmode_lines[l - 1];
// to make it a bit easier to draw new straight lines, clamp at y=5 dist
if (abs(last_line.y2 - newy) < 5) {
newy = last_line.y2;
}
if (abs(last_line.x2 - newx) < 5) {
newx = last_line.x2;
}
line_to_mouse = createLineObjFromOld({
x1: last_line.x2,
y1: last_line.y2,
x2: newx,
y2: newy,
});
// if there's also a last line
if (MX < 15 || MX > width - 15) {
let last_line = createLineObjFromOld({
x1: newx,
y1: newy,
x2: side,
y2: newy,
});
push();
stroke("red");
drawline(last_line);
pop();
}
} else {
line_to_mouse = createLineObjFromOld({
x1: side,
y1: newy,
x2: MX,
y2: newy,
});
}
let intersects = false;
for (let i = 0; i < l - 1; i++) {
if (linesIntersect(line_to_mouse, buildmode_lines[i])) {
// Found an intersection with an existing line
intersects = true;
break; // No need to check further
}
}
push();
stroke(intersects ? "red" : "blue");
fill(intersects ? "red" : "blue");
drawline(line_to_mouse);
circle(MX, MY, 5);
pop();
}
function endBuild(byLastLine) {
/*
end the build by replacing the old landscape with the new
and adding the last point to the last line
*/
if (!byLastLine) {
// print('Ended buildmode'); print is elsewhere .-.
return;
}
if (buildmode_lines.length <= 1) {
print("Not enough lines");
return;
}
if (byLastLine) print("Last line added, ending build");
landscape_lines = buildmode_lines;
// reset the old quadtree
lines_tree.clear();
// add the new lines to the quadtree
for (const l of landscape_lines) {
lines_tree.insert(l);
}
buildmode_lines = [];
buildmode_chk.checked(false);
}
function renderQuadtreeDebug() {
// show the regions of the mouse, objects... etc.
// move to other funtions tbh to skip double drawings... but..
// this might prevent having to pop/push alot for each line for eaxmple.
push();
noFill();
stroke("rgba(19,235,19,0.2)");
// but thne again there's not going to be many object.s... i hope.
for (const o of lines_tree.retrieve({
x: 0,
y: 0,
width: width,
height: height,
})) {
rect(o.x + 1, o.y + 1, o.width - 2, o.height - 2);
}
// yes this is cheating. haven't you counted how many times i wrote 'cba' yet?
for (const o of buildmode_lines) {
rect(o.x + 1, o.y + 1, o.width - 2, o.height - 2);
}
pop();
// lets make a mouse thingy., this is an overlay over the rest.
push();
noFill();
for (const o of lines_tree.retrieve({
x: 0,
y: 0,
width: width,
height: height,
})) {
rect(o.x + 1, o.y + 1, o.width - 2, o.height - 2);
}
pop();
}
function gameloop() {}
function draw() {
// TODO: just preinvert the image unless find a better fix for this
if (inverty_chk.checked()) {
image(bgImage, 0, 0);
if (invertx_chk.checked()) {
translate(width, height);
scale(-1, -1);
} else {
translate(0, height);
scale(1, -1);
}
} else {
push();
translate(0, height);
scale(1, -1);
image(bgImage, 0, 0);
pop();
}
renderGame();
}
function plotTrajectory(seconds) {
/*
the lander is globally accessible. this function attempts to plot its trajectory
and indicate whether the craft will crash or land.
if in the second
*/
seconds = 40;
push();
noFill();
strokeWeight(1);
stroke("rgba(251,5,251,0.52)");
// Clone the current state of the lander to simulate without affecting the actual lander
let simulatedPosition = { x: lander.position.x, y: lander.position.y };
let previousPosition = { x: simulatedPosition.x, y: simulatedPosition.y }; // Initialize previous position
let simulatedVelocity = { x: lander.velocity.x, y: lander.velocity.y };
let simulatedAcceleration = lander.acceleration;
let angle = lander.angle;
let crash = false;
let crashAt = null;
// Start drawing the trajectory
beginShape();
for (let t = 0; t <= seconds; t++) {
// Apply thrust based on lander's angle and gravity, similar to the update function
let accX = (simulatedAcceleration * cos(radians(angle))) / 60;
let accY = (simulatedAcceleration * sin(radians(angle)) + GRAVITY) / 60;
// Update velocity
simulatedVelocity.x += accX;
simulatedVelocity.y += accY;
let l = createLineObj(
previousPosition.x,
previousPosition.y,
simulatedPosition.x,
simulatedPosition.y
);
let lineIfIntersection = lineIfAnyLineIntersectsQuadTree(l)
// erm.. its line if it is, otherwise its false.
if (lineIfIntersection) {
crash = true;
crashAt = { x: simulatedPosition.x,
y: simulatedPosition.y,
v: simulatedVelocity,
lineSegment : lineIfIntersection,
angle : lander.angle
};
break;
}
// Update previous position to the last position before changing the current position
previousPosition.x = simulatedPosition.x;
previousPosition.y = simulatedPosition.y;
simulatedPosition.x += simulatedVelocity.x;
simulatedPosition.y += simulatedVelocity.y;
// Plot the point on the trajectory
vertex(simulatedPosition.x, simulatedPosition.y);
// Here, you can use previousPosition and simulatedPosition for your new feature
}
endShape();
pop();
// Check for intersection with terrain
// INTERSECTION == CRASH, EXPLOSION, COLLISION!
// from last line position
// query lines around line in quadtree.
if (crash) { // ctrl-f help if(crash) ifcrash crash if (crash)
// this is called crash but it doesnt nessarily indicate a crash...
// code just needs a big cleanup .
const x_ok = abs(crashAt.v.x) < 1
const y_ok = abs(crashAt.v.y) < 3
const angle_ok = abs(90-crashAt.angle) < 10 // this is a bit... excessive. have to adjust later.
const segmentFlat_ok = abs (crashAt.lineSegment.y1 - crashAt.lineSegment.y2) < 5
if (x_ok) {
const seg = crashAt.lineSegment
const x1 = seg.x1
const x2 = seg.x2
const y1 = seg.y1
const y2 = seg.y2
print(`safe x=${crashAt.v.x} ${JSON.stringify(seg)}`)
}
if (y_ok) {
print(`safe y=${crashAt.v.y}`)
}
if (angle_ok) {
print(`safe angle=${crashAt.angle} `)
}
if (segmentFlat_ok){
print(`landing pad is flat enough`)
}
if (x_ok && y_ok && angle_ok){
// not crashing!!!
push()
if (segmentFlat_ok) {
// green landing area if landing area is flat enough
stroke(0,255,0,50)
fill(0,255,0,50)
lander.SAFELY_LANDING_FLAG = true;
} else {
// else, make it orange
// and TODO color the segment itself red to show the angle is wrong!
stroke(255,100,0,50)
fill(255,255,0,50)
}
rectMode(CENTER)
rect(crashAt.x, crashAt.y, 25, 10)
pop()
} else {
lander.SAFELY_LANDING_FLAG = false;
push();
stroke("red"); // Change color to red if intersection is detected
fill("orange");
star(crashAt.x, crashAt.y, 10, 7, 12);
pop();
}
}
// Optionally, reset the drawing settings if you've changed stroke, fill, etc.
}
function calculateTriangleVertices(center, size, angle) {
// Define an array to store the vertices
let vertices = [];
// Angle offset for an equilateral triangle, 120 degrees between vertices
let angleOffset = 120;
// Calculate each vertex position
for (let i = 0; i < 3; i++) {
let vertexAngle = radians(angle + i * angleOffset); // Convert to radians and calculate the angle for each vertex
let x = center.x + size * cos(vertexAngle);
let y = center.y + size * sin(vertexAngle);
vertices.push({ x: x, y: y });
}
return vertices;
}
function calculateBoundingBox(vertices) {
// Initialize min and max values with the first vertex
let minX = vertices[0].x;
let maxX = vertices[0].x;
let minY = vertices[0].y;
let maxY = vertices[0].y;
// Iterate through vertices to find min and max values
for (let i = 1; i < vertices.length; i++) {
minX = Math.min(minX, vertices[i].x);
maxX = Math.max(maxX, vertices[i].x);
minY = Math.min(minY, vertices[i].y);
maxY = Math.max(maxY, vertices[i].y);
}
// Calculate width and height
let width = maxX - minX;
let height = maxY - minY;
// Return the bounding box object
return { x: minX, y: minY, width: width, height: height };
}
function linesIntersect(line1, line2) {
// Calculate the direction of the lines
const dx1 = line1.x2 - line1.x1;
const dy1 = line1.y2 - line1.y1;
const dx2 = line2.x2 - line2.x1;
const dy2 = line2.y2 - line2.y1;
// Solving the system of equations for the intersection point
const denominator = dx1 * dy2 - dy1 * dx2;
// If denominator is 0, lines are parallel and have no intersection
if (denominator == 0) return false;
const dx = line2.x1 - line1.x1;
const dy = line2.y1 - line1.y1;
const t = (dx * dy2 - dy * dx2) / denominator;
const u = (dx * dy1 - dy * dx1) / denominator;
// If 0 <= t <= 1 and 0 <= u <= 1, lines intersect within their segments
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
}
function lineIfAnyLineIntersectsQuadTree(l){
for (const o of lines_tree.retrieve(l)) {
if (linesIntersect(l, o)) {
return o;
}
}
return false;
}
function anyLinesIntersectQT(l) {
for (const o of lines_tree.retrieve(l)) {
if (linesIntersect(l, o)) {
return true;
}
}
return false;
}
// Use this function to check for any overlap among all lines
function checkAnyLineOverlap(lines) {
for (let i = 0; i < lines.length; i++) {
for (let j = i + 1; j < lines.length; j++) {
if (linesIntersect(lines[i], lines[j])) {
return true; // Found overlapping lines
}
}
}
return false; // No overlapping lines found
}
function addBuilderLine(newLine) {
const len = buildmode_lines.length;
if (len < 1) {
// If no lines have been added yet, just add the new line
buildmode_lines.push(newLine);
} else {
// Check if the new line intersects with any existing line
let intersects = false;
for (let i = 0; i < len - 1; i++) {
if (linesIntersect(newLine, buildmode_lines[i])) {
// Found an intersection with an existing line
intersects = true;
break; // No need to check further
}
}
if (!intersects) {
// If there's no intersection, add the new line
buildmode_lines.push(newLine);
} else {
print("Can't add that line, it intersects with another line!");
}
}
}