1575 lines
52 KiB
XML
1575 lines
52 KiB
XML
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
<svg viewBox="-200 -150 400 300" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
|
<!-- <svg viewBox="-20 -20 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg"> -->
|
|
<style>
|
|
foreignObject {
|
|
font-size: 4pt;
|
|
font-family: courier;
|
|
color: white;
|
|
}
|
|
|
|
#info {
|
|
position: absolute;
|
|
right: 0px;
|
|
padding: 1px;
|
|
}
|
|
|
|
#pointer {
|
|
position: absolute;
|
|
right: 0px;
|
|
bottom: 0px;
|
|
padding: 1px;
|
|
}
|
|
|
|
rect#bg {
|
|
fill: gray;
|
|
}
|
|
|
|
.ship circle.body {
|
|
fill: white;
|
|
}
|
|
|
|
circle.bullet {
|
|
fill: yellow;
|
|
}
|
|
|
|
line.bullet {
|
|
stroke: black;
|
|
opacity: 0.5;
|
|
transition: opacity 2s ease-out;
|
|
}
|
|
|
|
line.bullet.fade {
|
|
opacity: 0;
|
|
}
|
|
|
|
#triangles polygon {
|
|
fill-opacity: 0.2;
|
|
stroke-width: 1px;
|
|
fill: none;
|
|
stroke: none;
|
|
}
|
|
|
|
#triangles polygon.clockwise-orientation {
|
|
fill: white;
|
|
stroke: red;
|
|
}
|
|
|
|
#triangles polygon.obtuse {
|
|
stroke: orangered;
|
|
stroke-dasharray: 5 10;
|
|
}
|
|
|
|
#triangles polygon.anti-clockwise {
|
|
stroke: orange;
|
|
stroke-dasharray: 1 5;
|
|
}
|
|
|
|
#edges line {
|
|
stroke: gold;
|
|
}
|
|
|
|
.legs {
|
|
display: none;
|
|
}
|
|
|
|
.wall {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.wall.inverse {
|
|
fill: gray;
|
|
}
|
|
|
|
line:not(.cannon *) {
|
|
stroke-width: 0.5px;
|
|
}
|
|
|
|
line#velocity-indicator {
|
|
stroke: none;
|
|
/* stroke-width: 1px; */
|
|
}
|
|
|
|
line#acceleration-indicator {
|
|
stroke: none;
|
|
/* stroke-width: 0.5px; */
|
|
}
|
|
|
|
#lines circle {
|
|
fill: purple;
|
|
opacity: 0.2;
|
|
r: 5px;
|
|
}
|
|
|
|
.cannon .tip {
|
|
transform: translate(-2px);
|
|
}
|
|
|
|
.cannon .tip.recoil {
|
|
transform: translate(0px);
|
|
transition: transform 0.25s ease-out;
|
|
}
|
|
</style>
|
|
|
|
<rect id="bg" x="-200" y="-150" width="400" height="300"/>
|
|
<!-- <polygon class="wall inverse" points="-180,-40 -170,-120 40,-130 170,-120 180,40 170,130 -40,130 -170,120" /> -->
|
|
<!-- <polygon class="wall inverse" points="-160,-40 -170,-120 40,-110 170,-120 160,40 170,130 -40,110 -170,120" /> -->
|
|
|
|
<g id="ship1" class="ship">
|
|
<circle class="body" cx="0" cy="0" r="5"/>
|
|
<circle cx="0" cy="0" r="3" fill="transparent" stroke="green" />
|
|
<g class="cannon">
|
|
<line class="tip recoil" x1="4" y1="0" x2="6.5" y2="0" stroke="black"/>
|
|
<line x1="1" y1="0" x2="4" y2="0" stroke="white" stroke-width="3" />
|
|
<line x1="1" y1="0" x2="4" y2="0" stroke="black" stroke-width="1.5" />
|
|
</g>
|
|
<line id="velocity-indicator" x1="0" y1="0" x2="0" y2="0"/>
|
|
<line id="acceleration-indicator" x1="0" y1="0" x2="0" y2="0"/>
|
|
<g class="legs">
|
|
<path d="M 3 2 l 2 2 v 2 m -1.5 0 h 3" stroke="black" fill="none" />
|
|
<path d="M -3 2 l -2 2 v 2 m -1.5 0 h 3" stroke="black" fill="none" />
|
|
</g>
|
|
</g>
|
|
|
|
<g id="ship2" class="ship">
|
|
<circle class="body" cx="0" cy="0" r="5"/>
|
|
<circle cx="0" cy="0" r="3" fill="transparent" stroke="#CC0000" />
|
|
<g class="cannon">
|
|
<line class="tip recoil" x1="4" y1="0" x2="6.5" y2="0" stroke="black"/>
|
|
<line x1="1" y1="0" x2="4" y2="0" stroke="white" stroke-width="3" />
|
|
<line x1="1" y1="0" x2="4" y2="0" stroke="black" stroke-width="1.5" />
|
|
</g>
|
|
</g>
|
|
|
|
<!-- <g> -->
|
|
<!-- <polygon class="wall" points="20,20 30,20 40,40 20,40" /> -->
|
|
<!-- <polygon class="wall" points="-50,-50 -60,-50 -60,-60 -50,-60" /> -->
|
|
<!-- <polygon class="wall" points="10,-50 3,-50 3,-60 10,-60" /> -->
|
|
<!-- <polygon class="wall" points="-10,50 -3,50 -3,60 -10,60" /> -->
|
|
<!-- <polygon class="wall" points="-10,-40 10,-40 10,-15 -10,-15" /> -->
|
|
<!-- <polygon class="wall" points="-20,-10 0,10 -20,20 -30,0" /> -->
|
|
<!---->
|
|
<!-- <polygon class="wall" points="-20,-50 -10,-50 -10,-60 -20,-60" /> -->
|
|
<!-- <polygon class="wall" points="20,50 10,50 10,60 20,60" /> -->
|
|
<!-- <polygon class="wall" points="34,56 56,78 45,98 23,89" /> -->
|
|
<!-- <polygon class="wall" points="-55,-44 -33,-33 -55,-22 -66,-33" /> -->
|
|
<!-- <polygon class="wall" points="77,-22 133,-6 99,0 88,-5" /> -->
|
|
<!-- <polygon class="wall" points="-77,99 -66,88 -44,122 -88,133" /> -->
|
|
<!-- <polygon class="wall" points="-99,44 -77,44 -88,55" /> -->
|
|
<!---->
|
|
<!-- <polygon class="wall" points="-50,50 -40,60 -50,70 -60,60" /> -->
|
|
<!-- <polygon class="wall" points="50,-30 40,-60 50,-70 60,-60" /> -->
|
|
<!-- <polygon class="wall" points="50,50 60,60 50,70 40,60" /> -->
|
|
<!-- <polygon class="wall" points="-10,20 10,10 10,20" /> -->
|
|
<!-- </g> -->
|
|
|
|
<!-- <polygon class="wall" points="20,20 50,20 70,40 70,70 50,90 20,90 0,70 0,40" /> -->
|
|
<!-- <polygon class="wall" points="-10,-10 -20,-10 -20,-20 -10,-20" /> -->
|
|
|
|
<!-- <polygon class="wall" points="20,-50 10,-50 10,-60 20,-60" /> -->
|
|
<!-- <polygon class="wall" points="-10,10 10,10 10,40 -10,40" /> -->
|
|
<!-- <polygon class="wall" points="-10,-40 10,-40 10,-20 -10,-20" /> -->
|
|
|
|
|
|
<!-- ** -->
|
|
<!-- <polygon class="wall" points="-20,-10 20,10 -10,100 -100,100" /> -->
|
|
|
|
<!-- <polygon class="wall" points="-10,0 10,0 10,10 -10,10" /> -->
|
|
|
|
<!-- <polygon class="wall" points="-10,10 10,30 -10,40 -20,20" /> -->
|
|
|
|
<!-- <polygon class="wall" points="-100,-50 -10,-50 -10,-60 -100,-60" /> -->
|
|
|
|
<!-- <polygon class="wall" points="-100,-60 -10,-60 -10,-50 -100,-50" /> -->
|
|
<!-- <polygon class="wall" points="10,50 100,50 100,60 10,60" /> -->
|
|
|
|
<!-- <polygon class="wall" points="44,55 55,66 33,88 22,66" /> -->
|
|
<!-- <polygon class="wall" points="-50,-50 -60,-60 -50,-70 -40,-60" /> -->
|
|
|
|
<!-- <polygon class="wall" points="20,20 40,20 40,40 20,40" /> -->
|
|
<!-- <polygon class="wall" points="10,10 20,10 20,20 10,20" /> -->
|
|
<!-- <polygon class="wall" points="20,-50 -50,-50 -60,-70 -50,-100 80,-100 80,-90 -20,-90 -20,-60 40,-60 40,40 20,40" /> -->\
|
|
|
|
<!-- Wrench shape -->
|
|
<polygon class="wall" points="-10,-30 -10,-40 30,-50 60,-30 80,0 150,0 150,10 60,50 -10,40 -20,20 20,20 20,-20" />
|
|
|
|
<!-- <g> -->
|
|
<!-- <polygon class="wall" points="-130,-80 -40,-70 -70,-10" /> -->
|
|
<!-- <polygon class="wall" points="50,70 90,-10 130,70" /> -->
|
|
<!-- </g> -->
|
|
<!-- <g> -->
|
|
<!-- <polygon class="wall" points="-130,-80 -40,-70 -70,-10" /> -->
|
|
<!-- <polygon class="wall" points="50,70 90,-10 130,70" /> -->
|
|
<!-- <polygon class="wall" points="-130,100 -70,50 -40,110" /> -->
|
|
<!-- </g> -->
|
|
<!-- <g> -->
|
|
<!-- <polygon class="wall" points="-5,-25 -20,-40 20,-40 5,-25" /> -->
|
|
<!-- <polygon class="wall" points="-30,20 -20,30 -20,50 -50,20" /> -->
|
|
<!-- <polygon class="wall" points="20,30 30,20 50,20 20,50" /> -->
|
|
<!-- </g> -->
|
|
|
|
<g id="triangles"></g>
|
|
<g id="edges"></g>
|
|
<g id="lines"></g>
|
|
<g id="bullets"></g>
|
|
|
|
<foreignObject x="-200" y="-150" width="100%" height="100%">
|
|
<div id="info" xmlns="http://www.w3.org/1999/xhtml">
|
|
<span id="time" xmlns="http://www.w3.org/1999/xhtml">0</span> s
|
|
<span id="fps" xmlns="http://www.w3.org/1999/xhtml">-</span> fps
|
|
<br/>
|
|
<span id="position" xmlns="http://www.w3.org/1999/xhtml">-,-</span> x,y position
|
|
<br/>
|
|
<span id="velocity" xmlns="http://www.w3.org/1999/xhtml">-,-</span> x,y velocity
|
|
</div>
|
|
<!-- <ul xmlns="http://www.w3.org/1999/xhtml"> -->
|
|
<!-- <li xmlns="http://www.w3.org/1999/xhtml">bounce from collisions</li> -->
|
|
<!-- <li xmlns="http://www.w3.org/1999/xhtml">fall off screen after crash</li> -->
|
|
<!-- <li xmlns="http://www.w3.org/1999/xhtml">make ship a helicopter</li> -->
|
|
<!-- <li xmlns="http://www.w3.org/1999/xhtml">use paths for walls</li> -->
|
|
<!-- <li xmlns="http://www.w3.org/1999/xhtml">stop reading data from elements</li> -->
|
|
<!-- <li xmlns="http://www.w3.org/1999/xhtml">limited fuel</li> -->
|
|
<!-- <li xmlns="http://www.w3.org/1999/xhtml">additional cannon firing modes</li> -->
|
|
<!-- <li xmlns="http://www.w3.org/1999/xhtml">keep ship position at 0,0 actual</li> -->
|
|
<!-- <li xmlns="http://www.w3.org/1999/xhtml">only start on movement not just any keypress</li> -->
|
|
<!-- </ul> -->
|
|
<pre id="debug" xmlns="http://www.w3.org/1999/xhtml"></pre>
|
|
<div id="pointer" xmlns="http://www.w3.org/1999/xhtml">
|
|
x: <span class="x" xmlns="http://www.w3.org/1999/xhtml">-</span>,
|
|
y: <span class="y" xmlns="http://www.w3.org/1999/xhtml">-</span>
|
|
</div>
|
|
</foreignObject>
|
|
|
|
<script type="text/javascript">//<![CDATA[
|
|
// entities
|
|
const Ships = [
|
|
{ entity_id: "ship1" },
|
|
{ entity_id: "ship2" }
|
|
];
|
|
// const Walls = [{ entity_id: "wall_1" }, { entity_id: "wall_2" }];
|
|
|
|
// components
|
|
const Velocity = {};
|
|
const Position = {};
|
|
const Acceleration = {};
|
|
const AngularAcceleration = {};
|
|
const AngularVelocity = {};
|
|
const Degrees = {};
|
|
const Nodes = {};
|
|
const CannonNodes = {};
|
|
|
|
// Points = {
|
|
// "wall_1": "0,0 2,0 1,1",
|
|
// "wall_2": "0,0 -1,1 -2,0",
|
|
// };
|
|
|
|
// systems
|
|
const Shoot = (() => {
|
|
const cannonLength = 8;
|
|
const scalar = 50;
|
|
|
|
return {
|
|
update: ({ entity_id }) => {
|
|
const radians = Degrees[entity_id] * Math.PI / 180; // toFixed(15)?
|
|
const { x, y } = Position[entity_id];
|
|
const rise = Math.sin(radians) * cannonLength;
|
|
const run = Math.cos(radians) * cannonLength;
|
|
const origin = { x: x + run, y: y + rise };
|
|
|
|
const { x: vx, y: vy } = Velocity[entity_id];
|
|
Velocity[entity_id] = { x: vx - run, y: vy - rise };
|
|
|
|
return {
|
|
origin,
|
|
target: {
|
|
x: origin.x + run * scalar,
|
|
y: origin.y + rise * scalar
|
|
}
|
|
};
|
|
}
|
|
};
|
|
})();
|
|
|
|
const Draw = (() => {
|
|
return {
|
|
update: ({ entity_id }) => {
|
|
const node = Nodes[entity_id];
|
|
const gun = CannonNodes[entity_id];
|
|
newPos = Position[entity_id];
|
|
node.style.transform = `translate(${newPos.x}px, ${newPos.y}px)`;
|
|
gun.style.transform = `rotate(${Degrees[entity_id]}deg)`;
|
|
}
|
|
};
|
|
})();
|
|
|
|
const Rotate = (() => {
|
|
const metersPerMillisecond = 0.001;
|
|
|
|
return {
|
|
update: ({ entity_id }, elapsed) => {
|
|
const angularVel = AngularVelocity[entity_id];
|
|
const angularAcc = AngularAcceleration[entity_id];
|
|
const degrees = Degrees[entity_id];
|
|
|
|
let angularVelocity = angularVel + angularAcc;
|
|
const friction = 0.05;
|
|
const limit = 3;
|
|
|
|
if (angularVelocity > 0) {
|
|
if (angularVelocity > limit) angularVelocity = limit;
|
|
angularVelocity -= angularVelocity > friction ? friction : angularVelocity;
|
|
} else if (angularVelocity < 0) {
|
|
if (angularVelocity < -limit) angularVelocity = -limit;
|
|
angularVelocity += -angularVelocity > friction ? friction : -angularVelocity;
|
|
}
|
|
|
|
const turnRadians = elapsed * angularVelocity * metersPerMillisecond;
|
|
const radians = degrees * Math.PI / 180; // toFixed(15)?
|
|
const dDelta = turnRadians * 180 / Math.PI;
|
|
|
|
AngularVelocity[entity_id] = angularVelocity;
|
|
Degrees[entity_id] = degrees + dDelta;
|
|
}
|
|
};
|
|
})();
|
|
|
|
const Move = (() => {
|
|
const metersPerMillisecond = 0.001;
|
|
|
|
// Triangle has a clockwise orientation
|
|
function isClockwise([xa, ya], [xb, yb], [xc, yc]) {
|
|
|
|
// https://en.wikipedia.org/wiki/Curve_orientation#Practical_considerations
|
|
// Determinant for a convex polygon
|
|
const det = (xb - xa) * (yc - ya) - (xc - xa) * (yb - ya);
|
|
|
|
// const subR = (a, b) => Math.round(a * 100 - b * 100) / 100;
|
|
//
|
|
// const xba = subR(xb, xa);
|
|
// const yca = subR(yc, ya);
|
|
// const xca = subR(xc, xa);
|
|
// const yba = subR(yb, ya);
|
|
//
|
|
// const det = subR(xba * yca, xca * yba);
|
|
// console.log("dett", det, "edge", xa, ya, xb, yb, "position", xc, yc, "aaa");
|
|
|
|
// (8.0046e+33) / 1e30
|
|
// -> 8004.599999999999
|
|
// 8.0046e+33 * 1e-30
|
|
// -> 8004.6
|
|
// -0.574 * 1e-8
|
|
// -57399999.99999999
|
|
// -0.574 / 1e8
|
|
// -> -5.74e-9
|
|
// -0.574 * 1e9
|
|
// -574000000
|
|
// -0.574 / 1e-8
|
|
// -57399999.99999999
|
|
// -0.574 * 1e8
|
|
// -57399999.99999999
|
|
// -0.574 * 1e9
|
|
// -574000000
|
|
// -0.574 * 1e10
|
|
// -5740000000
|
|
// -0.574 * 1e11
|
|
// -57399999999.99999
|
|
|
|
// if det == 0, that means we are in contact with that edge
|
|
return det < 0;
|
|
}
|
|
|
|
function isAcute([xa, ya], [xb, yb], [xc, yc]) {
|
|
// console.log("isAcute", xa, ya, xb, yb, xc, yc);
|
|
const da = distance(xa, ya, xc, yc);
|
|
const db = distance(xb, yb, xc, yc);
|
|
const dc = distance(xa, ya, xb, yb);
|
|
|
|
// https://en.wikipedia.org/wiki/Law_of_cosines
|
|
// Solve for angles alpha and beta with inverse cosine (arccosine)
|
|
const numeratorA = Math.round(db ** 2 + dc ** 2 - da ** 2);
|
|
const denomA = Math.round(2 * db * dc);
|
|
const alphar = Math.acos(numeratorA / denomA);
|
|
const numeratorB = Math.round(da ** 2 + dc ** 2 - db ** 2);
|
|
const denomB = Math.round(2 * da * dc);
|
|
const betar = Math.acos(numeratorB / denomB);
|
|
|
|
const numA = db ** 2 + dc ** 2 - da ** 2;
|
|
const numAr = Math.round((db ** 2 * 100 + dc ** 2 * 100 - da ** 2 * 100) / 100);
|
|
const denA = Math.round(2 * db * dc);
|
|
const numB = da ** 2 + dc ** 2 - db ** 2;
|
|
const numBr = Math.round((da ** 2 * 100 + dc ** 2 * 100 - db ** 2 * 100) / 100);
|
|
const denB = Math.round(2 * da * dc);
|
|
|
|
const aRad = numAr / denA;
|
|
const bRad = numBr / denB;
|
|
const thetaA = aRad > 1 ? 1 : aRad;
|
|
const thetaB = bRad > 1 ? 1 : bRad;
|
|
|
|
const alpha = Math.acos(thetaA);
|
|
const beta = Math.acos(thetaB);
|
|
|
|
// const alpha = Math.acos((db ** 2 + dc ** 2 - da ** 2) / (2 * db * dc));
|
|
// const beta = Math.acos((da ** 2 + dc ** 2 - db ** 2) / (2 * da * dc));
|
|
|
|
// console.log("2 * db * dc", denA, "dett");
|
|
// console.log("2 * da * dc", denB, "dett");
|
|
// console.log("db ** 2 + dc ** 2 - da ** 2", numAr, "dett");
|
|
// console.log("da ** 2 + dc ** 2 - db ** 2", numBr, "dett");
|
|
// console.log("(db ** 2 + dc ** 2 - da ** 2) / (2 * db * dc)", thetaA, "dett");
|
|
// console.log("(da ** 2 + dc ** 2 - db ** 2) / (2 * da * dc)", thetaB, "dett");
|
|
// console.log("alpha", alpha, "beta", beta, "da, db, dc", da, db, dc, "dett");
|
|
|
|
return alpha < halfPi && beta < halfPi;
|
|
}
|
|
|
|
function getForwardEdges(edges, { x, y }) {
|
|
return edges.filter(({ edge: { xa, ya, xb, yb } }) => {
|
|
return isClockwise([xa, ya], [xb, yb], [x, y]) && isAcute([xa, ya], [xb, yb], [x, y]);
|
|
});
|
|
}
|
|
|
|
function getForwardCorners(corners, position, velocity) {
|
|
const { x: x1, y: y1 } = position;
|
|
const { x: x2, y: y2 } = velocity;
|
|
const { x: vx, y: vy } = velocity;
|
|
|
|
let perppts = {};
|
|
|
|
if (vx === 0 && vy === 0) {
|
|
// none
|
|
} else if (vx === 0 && vy > 0) {
|
|
perppts = { a: { x: x1 - 1, y: y1 }, b: { x: x1 + 1, y: y1 }};
|
|
} else if (vx === 0 && vy < 0) {
|
|
perppts = { a: { x: x1 + 1, y: y1 }, b: { x: x1 - 1, y: y1 }};
|
|
} else if (vy === 0 && vx > 0) {
|
|
perppts = { a: { x: x1, y: y1 + 1 }, b: { x: x1, y: y1 - 1 }};
|
|
} else if (vy === 0 && vx < 0) {
|
|
perppts = { a: { x: x1, y: y1 - 1 }, b: { x: x1, y: y1 + 1 }};
|
|
} else if (vy > 0 && vx > 0) {
|
|
const vslope = vy / vx;
|
|
const pslope = 1 / -vslope;
|
|
// Point-slope line equation
|
|
const pya = pslope * (x1 - 1) - pslope * x1 + y1;
|
|
const pyb = pslope * (x1 + 1) - pslope * x1 + y1;
|
|
|
|
perppts = { a: { x: x1 - 1, y: pya }, b: { x: x1 + 1, y: pyb }};
|
|
} else if (vy > 0 && vx < 0) {
|
|
const vslope = vy / vx;
|
|
const pslope = 1 / -vslope;
|
|
const pya = pslope * (x1 - 1) - pslope * x1 + y1;
|
|
const pyb = pslope * (x1 + 1) - pslope * x1 + y1;
|
|
|
|
perppts = { a: { x: x1 - 1, y: pya }, b: { x: x1 + 1, y: pyb }};
|
|
} else if (vy < 0 && vx > 0) {
|
|
const vslope = vy / vx;
|
|
const pslope = 1 / -vslope;
|
|
const pya = pslope * (x1 + 1) - pslope * x1 + y1;
|
|
const pyb = pslope * (x1 - 1) - pslope * x1 + y1;
|
|
|
|
perppts = { a: { x: x1 + 1, y: pya }, b: { x: x1 - 1, y: pyb }};
|
|
} else if (vy < 0 && vx < 0) {
|
|
const vslope = vy / vx;
|
|
const pslope = 1 / -vslope;
|
|
const pya = pslope * (x1 + 1) - pslope * x1 + y1;
|
|
const pyb = pslope * (x1 - 1) - pslope * x1 + y1;
|
|
|
|
perppts = { a: { x: x1 + 1, y: pya }, b: { x: x1 - 1, y: pyb }};
|
|
} else {
|
|
//
|
|
}
|
|
|
|
const { a, b } = perppts;
|
|
// if (a && b) drawLine(a.x, a.y, b.x, b.y);
|
|
|
|
return corners.filter(({ corner: c }) => {
|
|
if (!a || !b) return;
|
|
const det = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
return det > 0;
|
|
});
|
|
}
|
|
|
|
function getEdgeCollisionBoundary(edge, dist) {
|
|
const { xa, ya, xb, yb } = edge;
|
|
const length = distance(xa, ya, xb, yb);
|
|
const rise = yb - ya;
|
|
const run = xb - xa;
|
|
const riol = rise / length * dist;
|
|
const ruol = run / length * dist;
|
|
const subR = (a, b) => Math.round(a * 100 - b * 100) / 100;
|
|
const addR = (a, b) => Math.round(a * 100 + b * 100) / 100;
|
|
|
|
// return { xa: xa + riol, ya: ya - ruol, xb: xb + riol, yb: yb - ruol};
|
|
return { xa: addR(xa, riol), ya: subR(ya, ruol), xb: addR(xb, riol), yb: subR(yb, ruol) };
|
|
}
|
|
|
|
function detectEdgeCollision([xc, yc], [x, y], radius, gearDown) {
|
|
|
|
return (collision) => {
|
|
if (xc === x && yc === y) return;
|
|
const { edge, wall } = collision;
|
|
// const dist = edge.xa < edge.xb && edge.ya === edge.yb && gearDown ? radius + 1.5 : radius;
|
|
const dist = radius;
|
|
const edgeSeg = getEdgeCollisionBoundary(edge, dist);
|
|
const positionSeg = { xa: x, ya: y, xb: xc, yb: yc };
|
|
|
|
// console.log("edge", edge);
|
|
// console.log("edgeSeg", edgeSeg);
|
|
|
|
const { xa: x1, ya: y1, xb: x2, yb: y2 } = positionSeg;
|
|
const { xa: x3, ya: y3, xb: x4, yb: y4 } = edgeSeg;
|
|
// const { xa: x3, ya: y3, xb: x4, yb: y4 } = edge;
|
|
|
|
// console.log("x1", x1, "y1", y1, "x2", x2, "y2", y2);
|
|
// console.log("x3", x3, "y3", y3, "x4", x4, "y4", y4);
|
|
|
|
// https://en.wikipedia.org/wiki/Intersection_(geometry)#Two_line_segments
|
|
// https://en.wikipedia.org/wiki/Cramer%27s_rule#Explicit_formulas_for_small_systems
|
|
// const denom = (x2-x1)*(y4-y3)-(x4-x3)*(y2-y1);
|
|
|
|
// const x21 = +(x2-x1).toPrecision(13);
|
|
// const y43 = +(y4-y3).toPrecision(13);
|
|
// const x43 = +(x4-x3).toPrecision(13);
|
|
// const y21 = +(y2-y1).toPrecision(13);
|
|
// const denom = +(x21*y43-x43*y21).toPrecision(13);
|
|
// const x31 = +(x3-x1).toPrecision(13);
|
|
// const y31 = +(y3-y1).toPrecision(13);
|
|
|
|
const subR = (a, b) => Math.round(a * 100 - b * 100) / 100;
|
|
|
|
const x21 = x2-x1;
|
|
const x21r = subR(x2, x1);
|
|
// Math.round(3.8 * 100 - 3.74 * 100) / 100
|
|
|
|
const y43 = y4-y3;
|
|
const y43r = subR(y4, y3);
|
|
|
|
const x43 = x4-x3;
|
|
const x43r = subR(x4, x3);
|
|
|
|
const y21 = y2-y1;
|
|
const y21r = subR(y2, y1);
|
|
|
|
const denom = x21*y43-x43*y21;
|
|
const denomr = Math.round(x21r*100*y43r-x43r*y21r*100) / 100;
|
|
|
|
const x31 = x3-x1;
|
|
const x31r = subR(x3, x1);
|
|
|
|
const y31 = y3-y1;
|
|
const y31r = subR(y3, y1);
|
|
|
|
// if (denom) {
|
|
if (denomr) {
|
|
// const s = ((x3-x1)*(y4-y3)-(x4-x3)*(y3-y1))/denom;
|
|
// const t = -((x2-x1)*(y3-y1)-(x3-x1)*(y2-y1))/denom;
|
|
// const roundedT = +t.toFixed(2);
|
|
// const roundedS = +s.toFixed(2);
|
|
// const roundedT = +t.toFixed(15);
|
|
// const roundedS = +s.toFixed(15);
|
|
// const s = +(+(x31*y43-x43*y31).toPrecision(13) / denom).toPrecision(13);
|
|
// const t = +(-(x21*y31-x31*y21).toPrecision(13) / denom).toPrecision(13);
|
|
const s = (x31*y43-x43*y31) / denom;
|
|
const t = -(x21*y31-x31*y21) / denom;
|
|
// const sr = (x31r*y43r-x43r*y31r) / denomr;
|
|
const sr = Math.round((x31r*y43r-x43r*y31r) / denomr * 100) / 100;
|
|
|
|
// const tr = (-(x21r*y31r*100*100-x31r*y21r*100*100) / denomr)/10000;
|
|
const tr = -Math.round((x21r*y31r-x31r*y21r) / denomr * 100) / 100;
|
|
|
|
// console.log("s", s, "t", t);
|
|
// console.log("sr", sr, "tr", tr);
|
|
const x1r = Math.round(x1 * 100);
|
|
const x2r = Math.round(x2 * 100);
|
|
const y1r = Math.round(y1 * 100);
|
|
const y2r = Math.round(y2 * 100);
|
|
const x3r = Math.round(x3 * 100);
|
|
const x4r = Math.round(x4 * 100);
|
|
const y3r = Math.round(y3 * 100);
|
|
const y4r = Math.round(y4 * 100);
|
|
|
|
// 2.03 * 10
|
|
// 20.299999999999997
|
|
|
|
|
|
if (sr >= 0 && sr <= 1 && tr >= 0 && tr <= 1) {
|
|
const xs = x1 + s * (x2 - x1);
|
|
const ys = y1 + s * (y2 - y1);
|
|
|
|
const xsr = Math.round(x1r + sr * (x2r - x1r)) / 100;
|
|
const ysr = Math.round(y1r + sr * (y2r - y1r)) / 100;
|
|
|
|
const xtr = Math.round(x3r + tr * (x4r - x3r)) / 100;
|
|
const ytr = Math.round(y3r + tr * (y4r - y3r)) / 100;
|
|
|
|
// offset position slightly (by adding 1 to sr), so contact point is
|
|
// not inside the wall
|
|
// const xsrOff = Math.round(x1r + (sr - dist) * (x2r - x1r)) / 100;
|
|
// const ysrOff = Math.round(y1r + (sr - dist) * (y2r - y1r)) / 100;
|
|
const xsrOff = Math.round(x1r + (sr - 1) * (x2r - x1r)) / 100;
|
|
const ysrOff = Math.round(y1r + (sr - 1) * (y2r - y1r)) / 100;
|
|
|
|
collision.position = { x: xsrOff, y: ysrOff };
|
|
// drawLine(x3, y3, x4, y4, "red");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// return roundedS >= 0 && roundedS <= 1 && roundedT >= 0 && roundedT <= 1;
|
|
return;
|
|
};
|
|
}
|
|
|
|
function perpIntxn(baseSlope, xa, ya, xc, yc) {
|
|
let isx, isy;
|
|
|
|
// base is vertical
|
|
if (baseSlope === -Infinity || baseSlope === Infinity) {
|
|
isx = xa;
|
|
isy = yc;
|
|
} else if (baseSlope === 0) { // base is horizontal
|
|
isx = xc;
|
|
isy = ya;
|
|
} else {
|
|
const altitudeSlope = 1 / -baseSlope;
|
|
isx = (-altitudeSlope * xc + yc + baseSlope * xa - ya) / (baseSlope - altitudeSlope);
|
|
isy = altitudeSlope * isx - altitudeSlope * xc + yc;
|
|
}
|
|
|
|
return { x: isx, y: isy };
|
|
}
|
|
|
|
function slope({ xa, ya, xb, yb }) {
|
|
return (yb - ya) / (xb - xa);
|
|
}
|
|
|
|
function distance(x1, y1, x2, y2) {
|
|
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
|
// return Math.round(Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) * 100) / 100;
|
|
}
|
|
|
|
function detectCornerCollision([xc, yc], [x, y], radius) {
|
|
return c => {
|
|
if (xc === x && yc === y) return;
|
|
|
|
const d = distance(c.corner.x, c.corner.y, xc, yc);
|
|
|
|
if (d <= radius) return true;
|
|
|
|
const positionSeg = { xa: xc, ya: yc, xb: x, yb: y };
|
|
const posNormIntxn = perpIntxn(slope(positionSeg), x, y, c.corner.x, c.corner.y);
|
|
const cornerSeg = { xa: c.corner.x, ya: c.corner.y, xb: posNormIntxn.x, yb: posNormIntxn.y };
|
|
|
|
const { x: x0, y: y0 } = c.corner;
|
|
const { xa: x1, ya: y1, xb: x2, yb: y2 } = positionSeg;
|
|
const { xa: x3, ya: y3, xb: x4, yb: y4 } = cornerSeg;
|
|
|
|
// https://en.wikipedia.org/wiki/Intersection_(geometry)#Two_line_segments
|
|
// https://en.wikipedia.org/wiki/Cramer%27s_rule#Explicit_formulas_for_small_systems
|
|
const s = ((x3-x1)*(y4-y3)-(x4-x3)*(y3-y1))/((x2-x1)*(y4-y3)-(x4-x3)*(y2-y1));
|
|
const t = -((x2-x1)*(y3-y1)-(x3-x1)*(y2-y1))/((x2-x1)*(y4-y3)-(x4-x3)*(y2-y1));
|
|
const roundedT = +t.toFixed(2);
|
|
|
|
if (s >= 0 && roundedT <= 1) {
|
|
const xs = (x1 + s * (x2 - x1));
|
|
const ys = (y1 + s * (y2 - y1));
|
|
const xt = (x3 + roundedT * (x4 - x3));
|
|
const yt = (y3 + roundedT * (y4 - y3));
|
|
// [xs, ys] and [xt, yt] should be equal ([xs, ys] === [xy, yt])
|
|
// (...or about equal, notwithstanding rounding errors)
|
|
const sCollisionPt = [xs, ys];
|
|
const tCollisionPt = [xt, yt];
|
|
// drawCircle(posNormIntxn.x, posNormIntxn.y, "red");
|
|
// drawCircle(...tCollisionPt, "blue");
|
|
}
|
|
|
|
return s >= 0 && roundedT <= 1;
|
|
};
|
|
}
|
|
|
|
function cornerContactPosition(xc, yc, x, y, corner, cLength) {
|
|
const positionSeg = { xa: xc, ya: yc, xb: x, yb: y };
|
|
const posNormIntxn = perpIntxn(slope(positionSeg), x, y, corner.x, corner.y);
|
|
|
|
// shortest distance between corner and path
|
|
const aLength = distance(corner.x, corner.y, posNormIntxn.x, posNormIntxn.y);
|
|
// distance from position/normal intersection
|
|
const bLength = Math.sqrt(Math.abs(cLength ** 2 - aLength ** 2));
|
|
|
|
const intxnSeg = document.createElementNS(namespaceURIsvg, 'line');
|
|
intxnSeg.setAttribute('x1', posNormIntxn.x);
|
|
intxnSeg.setAttribute('y1', posNormIntxn.y);
|
|
intxnSeg.setAttribute('x2', x);
|
|
intxnSeg.setAttribute('y2', y);
|
|
|
|
return intxnSeg.getPointAtLength(bLength);
|
|
}
|
|
|
|
function withinCollisionDistance({ x: x1, y: y1 }, { x: x2, y: y2 }, distance) {
|
|
const diffx = x2;
|
|
const diffy = y2;
|
|
const detv = x2 * y1 - y2 * x1;
|
|
const dv = Math.sqrt(diffy ** 2 + diffx ** 2);
|
|
const slopev = slope({ xa: x1, ya: y1, xb: x1 + x2, yb: y1 + y2 });
|
|
|
|
return ({ corner: { x: x0, y: y0 }}) => {
|
|
const velNormIntxn = perpIntxn(slopev, x1, y1, x0, y0);
|
|
const dx = Math.max(x0, velNormIntxn.x) - Math.min(x0, velNormIntxn.x);
|
|
const dy = Math.max(y0, velNormIntxn.y) - Math.min(y0, velNormIntxn.y);
|
|
const d = Math.sqrt(dy ** 2 + dx ** 2);
|
|
return d <= distance;
|
|
};
|
|
}
|
|
|
|
function detectContacts(currentPos, intendedPos, velocity, radius, { edges, corners }, gearDown) {
|
|
// console.log("current position passed to detectContacts", currentPos);
|
|
const { x: xc, y: yc } = intendedPos;
|
|
const [x, y] = currentPos;
|
|
|
|
// edges oriented clockwise with entity
|
|
const fwdEdges = getForwardEdges(edges, { x, y });
|
|
const edgeColl = fwdEdges.filter(detectEdgeCollision([xc, yc], [x, y], radius, gearDown));
|
|
|
|
// corners ahead of ship
|
|
const fwdCorners = getForwardCorners(corners, { x, y }, velocity);
|
|
const cornersInPath = fwdCorners.filter(withinCollisionDistance({ x, y }, velocity, radius));
|
|
const cornerColl = cornersInPath.filter(detectCornerCollision([xc, yc], [x, y], radius));
|
|
|
|
return [...edgeColl, ...cornerColl];
|
|
}
|
|
|
|
function vector(x, y) {
|
|
const vector = { x: x, y: y };
|
|
vector.magnitude = Math.sqrt(x**2+y**2);
|
|
vector.dx = vector.x / vector.magnitude;
|
|
vector.dy = vector.y / vector.magnitude;
|
|
vector.rightNormal = { x: vector.y, y: -vector.x };
|
|
vector.rightNormal.dx = vector.rightNormal.x / vector.magnitude;
|
|
vector.rightNormal.dy = vector.rightNormal.y / vector.magnitude;
|
|
|
|
return vector;
|
|
}
|
|
|
|
function bounceVector(v, run, rise) {
|
|
const vec1 = vector(v.x, v.y);
|
|
const vec2 = vector(run, rise);
|
|
|
|
// From https://stackoverflow.com/a/14886099
|
|
|
|
// 1. Find the dot product of vec1 and vec2
|
|
// Note: dx and dy are vx and vy divided over the length of the vector (magnitude)
|
|
// var dpA:Number = vec1.vx * vec2.dx + vec1.vy * vec2.dy;
|
|
const dpA = vec1.x * vec2.dx + vec1.y * vec2.dy;
|
|
|
|
// 2. Project vec1 over vec2
|
|
// var prA_vx:Number = dpA * vec2.dx;
|
|
const prAvx = dpA * vec2.dx;
|
|
// var prA_vy:Number = dpA * vec2.dy;
|
|
const prAvy = dpA * vec2.dy;
|
|
|
|
// 3. Find the dot product of vec1 and vec2's normal
|
|
// (left or right normal depending on line's direction, let's say left)
|
|
// vec.leftNormal --> vx = vec.vy; vy = -vec.vx;
|
|
// vec.rightNormal --> vx = -vec.vy; vy = vec.vx;
|
|
// var dpB:Number = vec1.vx * vec2.leftNormal.dx + vec1.vy * vec2.leftNormal.dy;
|
|
const dpB = vec1.x * vec2.rightNormal.dx + vec1.y * vec2.rightNormal.dy;
|
|
|
|
// 4. Project vec1 over vec2's left normal
|
|
// var prB_vx:Number = dpB * vec2.leftNormal.dx;
|
|
const prBvx = dpB * vec2.rightNormal.dx;
|
|
// var prB_vy:Number = dpB * vec2.leftNormal.dy;
|
|
const prBvy = dpB * vec2.rightNormal.dy;
|
|
|
|
// 5. Add the first projection prA to the reverse of the second -prB
|
|
// var new_vx:Number = prA_vx - prB_vx;
|
|
// var new_vy:Number = prA_vy - prB_vy;
|
|
return {
|
|
x: prAvx - prBvx,
|
|
y: prAvy - prBvy,
|
|
vec1,
|
|
vec2
|
|
};
|
|
}
|
|
|
|
return {
|
|
update: ({ entity_id }, elapsed) => {
|
|
const gravity = 0.1;
|
|
// affectedByWalls & affectedByGravity toggles?
|
|
const { x: px, y: py } = Position[entity_id];
|
|
const { x: vx, y: vy } = Velocity[entity_id];
|
|
let { x: ax, y: ay } = Acceleration[entity_id];
|
|
ay += gravity;
|
|
|
|
const vr = {
|
|
x: Math.round(vx * 100 + ax * 100) / 100,
|
|
y: Math.round(vy * 100 + ay * 100) / 100
|
|
};
|
|
|
|
const v = {
|
|
x: vx > 0 && vr.x <= 0 ? 0 : vr.x,
|
|
y: vy > 0 && vr.y <= 0 ? 0 : vr.y
|
|
};
|
|
|
|
// const v = {
|
|
// x: vx > 0 && vx + ax <= 0 ? 0 : vx + ax,
|
|
// y: vy > 0 && vy + ay <= 0 ? 0 : vy + ay
|
|
// };
|
|
|
|
// console.log("v", v, "ax", ax, "ay", ay);
|
|
|
|
const p = {
|
|
// x: px + elapsed * v.x * metersPerMillisecond,
|
|
// y: py + elapsed * v.y * metersPerMillisecond
|
|
x: Math.round((px + elapsed * v.x * metersPerMillisecond) * 100) / 100,
|
|
y: Math.round((py + elapsed * v.y * metersPerMillisecond) * 100) / 100,
|
|
};
|
|
|
|
// console.log("----------elapsed", elapsed)
|
|
// 0.57 * 1000 * 20 / 1000
|
|
|
|
// p.x = +p.x.toPrecision(13);
|
|
// p.y = +p.y.toPrecision(13);
|
|
|
|
const contacts = detectContacts([px, py], p, v, s.radius, map, false);
|
|
|
|
if (contacts.length !== 0) {
|
|
// console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
|
// console.log("CONTACTS", contacts);
|
|
let posP;
|
|
const contact = contacts[0];
|
|
|
|
if (contacts.length == 1 && contact.corner) {
|
|
contact.position = cornerContactPosition(p.x, p.y, px, py, contact.corner, s.radius);
|
|
contact.position.x = +contact.position.x.toPrecision(13);
|
|
contact.position.y = +contact.position.y.toPrecision(13);
|
|
|
|
// drawLine(contact.position.x, contact.position.y, contact.position.x + vx, contact.position.y + vy, "blue");
|
|
|
|
const contactV = { x: contact.position.y - contact.corner.y, y: contact.position.x - contact.corner.x };
|
|
const normalVect = { x: contact.position.x - contact.corner.x, y: contact.position.y - contact.corner.y };
|
|
// drawLine(contact.corner.x, contact.corner.y, contact.corner.x + normalVect.x, contact.corner.y + normalVect.y, "black");
|
|
|
|
const tangentVect = { x: normalVect.y, y: -normalVect.x };
|
|
// drawLine(contact.position.x, contact.position.y, contact.position.x + tangentVect.x, contact.position.y + tangentVect.y, "green");
|
|
contact.velocity = bounceVector(v, tangentVect.x, tangentVect.y);
|
|
// drawLine(contact.position.x, contact.position.y, contact.position.x + contact.velocity.x, contact.position.y + contact.velocity.y, "blue");
|
|
|
|
// Velocity[entity_id] = { x: 0, y: 0 };
|
|
Velocity[entity_id] = contact.velocity;
|
|
Position[entity_id] = cornerContactPosition(p.x, p.y, px, py, contact.corner, s.radius);
|
|
} else if (contacts.length == 1 && contact.edge) {
|
|
// if (isLandable(contact.edge) && s.gearDown) s.isLanded = true;
|
|
const rise = contact.edge.yb-contact.edge.ya;
|
|
const run = contact.edge.xb-contact.edge.xa;
|
|
const edgeNrmlVect = vector(rise, -run);
|
|
// const velocityVect = vector(v.x + ax, v.y + ay);
|
|
const velocityVect = vector(v.x, v.y);
|
|
|
|
const vDotn = edgeNrmlVect.x * velocityVect.x + edgeNrmlVect.y * velocityVect.y;
|
|
const denom = edgeNrmlVect.x ** 2 + edgeNrmlVect.y ** 2;
|
|
|
|
const pVect = {
|
|
x: vDotn / denom * edgeNrmlVect.x,
|
|
y: vDotn / denom * edgeNrmlVect.y
|
|
};
|
|
|
|
// console.log("vDotn", vDotn);
|
|
// console.log("denom", denom);
|
|
// console.log("pVect", pVect);
|
|
|
|
const reverseP = { x: -pVect.x, y: -pVect.y };
|
|
|
|
// add reverseP and v vectors together and a vector
|
|
const prVonNx = Math.round(reverseP.x*100 + v.x*100)/100;
|
|
const prVonNy = Math.round(reverseP.y*100 + v.y*100)/100;
|
|
|
|
// console.log("intended velocity", v);
|
|
// console.log("counter velocity", prVonNx, prVonNy);
|
|
|
|
// drawLine(px, py, px + velocityVect.x, py + velocityVect.y, "blue");
|
|
// drawLine(px, py, px + edgeNrmlVect.x, py + edgeNrmlVect.y, "black");
|
|
// drawLine(contact.position.x, contact.position.y, contact.position.x + prVonNx, contact.position.y + prVonNy, "teal");
|
|
|
|
// contact.velocity = { x: prVonNx, y: prVonNy };
|
|
Velocity[entity_id] = { x: prVonNx, y: prVonNy };
|
|
// console.log("v", v);
|
|
// console.log("velocity", Velocity[entity_id]);
|
|
|
|
// console.log("current position", px, py);
|
|
// drawCircle(px, py, "pink");
|
|
|
|
// console.log("contact.position", contact.position, "dett");
|
|
// drawCircle(contact.position.x, contact.position.y, "limegreen");
|
|
|
|
// console.log("intended next position", p);
|
|
// drawCircle(p.x, p.y, "red");
|
|
|
|
let newP = {
|
|
// x: contact.position.x + elapsed * prVonNx * metersPerMillisecond,
|
|
// y: contact.position.y + elapsed * prVonNy * metersPerMillisecond
|
|
x: Math.round((contact.position.x + elapsed * prVonNx * metersPerMillisecond) * 100) / 100,
|
|
y: Math.round((contact.position.y + elapsed * prVonNy * metersPerMillisecond) * 100) / 100,
|
|
};
|
|
|
|
// console.log("calculated next position", newP, "dett");
|
|
let isC = isClockwise([contact.edge.xa, contact.edge.ya], [contact.edge.xb, contact.edge.yb], [newP.x, newP.y]);
|
|
// console.log("is calculated next position clockwise with current contact", isC, "aaa");
|
|
let isA = isAcute([contact.edge.xa, contact.edge.ya], [contact.edge.xb, contact.edge.yb], [newP.x, newP.y]);
|
|
// console.log("is calculated next position acute with current contact", isA, "aaa");
|
|
|
|
// console.log("calculated next", newP, "aaa");
|
|
|
|
const edgeVect = { x: run, y: rise };
|
|
// const npVect = { x: newP.x - run, y: newP.y - rise };
|
|
const npVect = { x: newP.x - contact.edge.xa, y: newP.y - contact.edge.ya };
|
|
|
|
// drawLine(contact.edge.xa, contact.edge.ya, contact.edge.xa + npVect.x, contact.edge.ya + npVect.y);
|
|
|
|
const projection = ({ of, onto }) => {
|
|
const ofDotOnto = onto.x * of.x + onto.y * of.y;
|
|
const ontoMag = onto.x ** 2 + onto.y ** 2;
|
|
const scalar = 2 * ofDotOnto / ontoMag;
|
|
const ontoV = { x: scalar * onto.x, y: scalar * onto.y };
|
|
|
|
return {
|
|
x: ontoV.x - of.x,
|
|
y: ontoV.y - of.y
|
|
};
|
|
}
|
|
|
|
const projNpOntoEdge = projection({ of: npVect, onto: edgeVect });
|
|
projNpOntoEdge.x = projNpOntoEdge.x + contact.edge.xa;
|
|
projNpOntoEdge.y = projNpOntoEdge.y + contact.edge.ya;
|
|
|
|
// shouldnt change isClockwise if isAcute isn't also changing
|
|
// if we go from clockwise to not clockwise without changing acuteness,
|
|
// that means we went through a wall
|
|
if (!isC && isA) newP = projNpOntoEdge;
|
|
|
|
// if an edge passes through a point, is that point in the wall's fill? yes
|
|
// so touching a wall is technically being in its fill
|
|
|
|
// drawCircle(newP.x, newP.y, "black");
|
|
|
|
Position[entity_id] = newP;
|
|
} else if (contacts.every(c => c.edge)) {
|
|
// all edges
|
|
// doing nothing seems to kind of work (but it's buggy), ideally we
|
|
// should add the vectors of all the contacts together
|
|
}
|
|
} else {
|
|
Velocity[entity_id] = { x: v.x, y: v.y };
|
|
Position[entity_id] = { x: p.x, y: p.y };
|
|
}
|
|
}
|
|
};
|
|
})();
|
|
|
|
const namespaceURIsvg = 'http://www.w3.org/2000/svg';
|
|
const bullets = [];
|
|
const halfPi = Math.PI / 2;
|
|
const maxSpeed = 100;
|
|
|
|
const drawCollisionLines = false;
|
|
|
|
let previous, zero, frameCount = 0;
|
|
let rotate = 0;
|
|
|
|
const s = {};
|
|
s.node = document.querySelector("#ship1");
|
|
s.radius = +s.node.querySelector(".body").getAttribute('r');
|
|
const gun = s.node.querySelector('.cannon');
|
|
const legs = s.node.querySelector(".legs");
|
|
|
|
const svg = document.querySelector('svg');
|
|
const bg = svg.querySelector('#bg');
|
|
const fps = document.querySelector("#fps");
|
|
const time = document.querySelector("#time");
|
|
const positionEl = document.querySelector("#position");
|
|
const velocityEl = document.querySelector("#velocity");
|
|
const debug = document.querySelector("#debug");
|
|
const wallElements = document.querySelectorAll('.wall');
|
|
const bulletsContainer = document.querySelector("#bullets");
|
|
const triangleContainer = document.querySelector('#triangles');
|
|
const linesContainer = document.querySelector("#lines");
|
|
const edgeContainer = document.querySelector('#edges');
|
|
const velIndic = document.querySelector('#velocity-indicator');
|
|
const acclIndic = document.querySelector('#acceleration-indicator');
|
|
|
|
const bulletPt = svg.createSVGPoint();
|
|
const cornerPt = svg.createSVGPoint();
|
|
|
|
const map = (function(els) {
|
|
let corners, edges;
|
|
|
|
return {
|
|
walls: [...els].map(node => {
|
|
const corners = node.getAttribute('points').split(' ').map(coords => {
|
|
const [x, y] = coords.split(',');
|
|
const pt = svg.createSVGPoint();
|
|
pt.x = +x;
|
|
pt.y = +y;
|
|
return pt;
|
|
});
|
|
|
|
const edges = corners.map(({ x: xa, y: ya }, i, arr) => {
|
|
const { x: xb, y: yb } = arr[(i + 1) % arr.length];
|
|
return { xa: xa, ya: ya, xb: xb, yb: yb };
|
|
});
|
|
|
|
return { node, corners, edges };
|
|
}),
|
|
|
|
get corners() {
|
|
if (corners) return corners;
|
|
|
|
return this.walls.reduce((acc, wall) =>
|
|
[...acc, ...wall.corners.map(c => ({ corner: c, wall: wall }))], []);
|
|
},
|
|
|
|
get edges() {
|
|
if (edges) return edges;
|
|
|
|
return this.walls.reduce((acc, wall) =>
|
|
[...acc, ...wall.edges.map(e => ({ edge: e, wall: wall }))], []);
|
|
}
|
|
};
|
|
})(wallElements);
|
|
|
|
let allStartingEdges;
|
|
let started = false;
|
|
let restart = false;
|
|
let isReadingKeys = true;
|
|
|
|
function init() {
|
|
started = false;
|
|
const mult = 10;
|
|
|
|
s.collision = null;
|
|
s.isLanded = false;
|
|
s.gearDown = false;
|
|
|
|
const setInits = (entity_id, s) => {
|
|
Acceleration[entity_id] = { x: s.acceleration.x, y: s.acceleration.y };
|
|
Velocity[entity_id] = { x: s.velocity.x, y: s.velocity.y };
|
|
Position[entity_id] = { x: s.position.x, y: s.position.y };
|
|
|
|
AngularAcceleration[entity_id] = s.angularAcceleration;
|
|
AngularVelocity[entity_id] = s.angularVelocity;
|
|
Degrees[entity_id] = s.degrees;
|
|
Nodes[entity_id] = document.querySelector(`#${entity_id}`);
|
|
CannonNodes[entity_id] = document.querySelector(`#${entity_id} .cannon`);
|
|
// let node = document.querySelector(`#${entity_id}`);
|
|
// node.style.transform = `translate(${s.position.x}px, ${s.position.y}px)`;
|
|
}
|
|
|
|
Ships.forEach(({ entity_id }) => {
|
|
setInits(entity_id, {
|
|
acceleration: { x: 0, y: 0 },
|
|
velocity: { x: 0, y: 0 },
|
|
position: { x: -50, y: 0 },
|
|
angularAcceleration: 0,
|
|
angularVelocity: 0,
|
|
degrees: 0
|
|
});
|
|
});
|
|
|
|
setInits("ship1", {
|
|
acceleration: { x: 0, y: 0 },
|
|
velocity: { x: 0, y: 0 },
|
|
position: { x: 3, y: -6 },
|
|
angularAcceleration: 0,
|
|
angularVelocity: 0,
|
|
degrees: 0
|
|
});
|
|
|
|
Ships.forEach(({ entity_id }) => {
|
|
Draw.update({ entity_id });
|
|
});
|
|
|
|
[...edgeContainer.children].forEach(c => c.remove());;
|
|
|
|
wallElements.forEach(w => w.setAttribute('fill', 'black'));
|
|
velIndic.setAttribute('x2', 0);
|
|
velIndic.setAttribute('y2', 0);
|
|
acclIndic.setAttribute('x2', 0);
|
|
acclIndic.setAttribute('y2', 0);
|
|
|
|
time.innerText = "0";
|
|
}
|
|
|
|
function drawTriangles(container, walls, [positionX, positionY]) {
|
|
walls.forEach(pts =>
|
|
pts.forEach(([[x1, y1], [x2, y2]]) => {
|
|
const el = document.createElementNS(namespaceURIsvg, 'polygon');
|
|
const attr = `${x1},${y1} ${x2},${y2} ${positionX},${positionY}`
|
|
el.setAttribute('points', attr);
|
|
container.appendChild(el);
|
|
})
|
|
);
|
|
}
|
|
|
|
function updateTriangles([positionX, positionY]) {
|
|
const delim = ' ';
|
|
const className = 'clockwise-orientation';
|
|
|
|
// if (!triangleContainer.childElementCount)
|
|
// drawTriangles(triangleContainer, allEdgePts, position);
|
|
|
|
const triangles = triangleContainer.querySelectorAll('polygon');
|
|
|
|
triangles.forEach(t => {
|
|
const attr = t.getAttribute('points').split(delim);
|
|
const [a, b,] = attr.map(t => t.split(','));
|
|
|
|
const cw = isClockwise(a, b, [positionX, positionY]);
|
|
const acute = isAcute(a, b, [positionX, positionY]);
|
|
const pos = `${positionX},${positionY}`;
|
|
|
|
if (pos !== attr.pop()) {
|
|
attr.push(pos);
|
|
t.setAttribute('points', attr.join(delim));
|
|
}
|
|
|
|
t.classList[cw && acute ? "add" : "remove"](className);
|
|
t.classList[cw && !acute ? "add" : "remove"]("obtuse");
|
|
t.classList[!cw ? "add" : "remove"]("anti-clockwise");
|
|
});
|
|
}
|
|
|
|
function wrapPos(positionX, positionY) {
|
|
let x, y;
|
|
|
|
if (positionY > 150) y = positionY - 300;
|
|
else if (positionY < -150) y = positionY + 300;
|
|
else y = positionY;
|
|
|
|
if (positionX > 200) x = positionX - 400;
|
|
else if (positionX < -200) x = positionX + 400;
|
|
else x = positionX;
|
|
|
|
return [x, y];
|
|
}
|
|
|
|
function drawLine(xa, ya, xb, yb, color = "black") {
|
|
const el = document.createElementNS(namespaceURIsvg, 'line');
|
|
el.setAttribute('x1', xa);
|
|
el.setAttribute('y1', ya);
|
|
el.setAttribute('x2', xb);
|
|
el.setAttribute('y2', yb);
|
|
el.setAttribute('stroke', color);
|
|
const arrow = document.createElementNS(namespaceURIsvg, 'circle');
|
|
arrow.setAttribute('cx', xb);
|
|
arrow.setAttribute('cy', yb);
|
|
arrow.setAttribute('r', 1);
|
|
arrow.setAttribute('fill', color);
|
|
|
|
svg.appendChild(arrow);
|
|
svg.appendChild(el);
|
|
return el;
|
|
}
|
|
|
|
function drawCircle(cx, cy, color = "black", r = 1) {
|
|
const el = document.createElementNS(namespaceURIsvg, 'circle');
|
|
el.setAttribute('cx', cx);
|
|
el.setAttribute('cy', cy);
|
|
el.setAttribute('r', r);
|
|
el.setAttribute('fill', color);
|
|
svg.appendChild(el);
|
|
return el;
|
|
}
|
|
|
|
function isLandable(edge) {
|
|
return edge.xa < edge.xb && edge.ya === edge.yb;
|
|
// return Object.is(slope(edge), +0);
|
|
}
|
|
|
|
function lineIntxnPt({ x1, y1, x2, y2 }, { x1: x3, y1: y3, x2: x4, y2: y4 }) {
|
|
// https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
|
|
const denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
|
const l1Det = x1 * y2 - y1 * x2;
|
|
const l2Det = x3 * y4 - y3 * x4;
|
|
const x = (l1Det * (x3 - x4) - (x1 - x2) * l2Det) / denominator;
|
|
const y = (l1Det * (y3 - y4) - (y1 - y2) * l2Det) / denominator;
|
|
|
|
return { x: x, y: y };
|
|
}
|
|
|
|
function updateShip(s, elapsed) {
|
|
// const gravity = 0.25;
|
|
const gravity = 0;
|
|
const { x: px, y: py } = s.position;
|
|
const { x: vx, y: vy } = s.velocity;
|
|
let { x: ax, y: ay } = s.acceleration;
|
|
ay += gravity;
|
|
|
|
s.velocity = {
|
|
x: vx > 0 && vx + ax <= 0 ? 0 : vx + ax,
|
|
y: vy > 0 && vy + ay <= 0 ? 0 : vy + ay
|
|
};
|
|
|
|
velIndic.setAttribute('x2', s.velocity.x);
|
|
velIndic.setAttribute('y2', s.velocity.y);
|
|
acclIndic.setAttribute('x2', s.acceleration.x);
|
|
acclIndic.setAttribute('y2', s.acceleration.y);
|
|
|
|
const metersPerMillisecond = 0.001;
|
|
|
|
const pDelta = {
|
|
x: elapsed * s.velocity.x * metersPerMillisecond,
|
|
y: elapsed * s.velocity.y * metersPerMillisecond
|
|
};
|
|
|
|
const p = { x: pDelta.x + px, y: pDelta.y + py };
|
|
|
|
current = s.collision;
|
|
|
|
// s.collision = detectCollision([px, py], p, s.velocity, s.radius, map, s.gearDown);
|
|
// if (!current && s.collision) console.log("COLLISION", s.collision);
|
|
|
|
legs.style.display = s.gearDown ? "initial" : "none";
|
|
|
|
if (s.collision) {
|
|
let posP;
|
|
if (s.collision.corner) {
|
|
// posP = cornerContactPosition(p.x, p.y, px, py, s.collision.corner, s.radius);
|
|
} else if (s.collision.edge) {
|
|
if (isLandable(s.collision.edge) && s.gearDown) s.isLanded = true;
|
|
|
|
posP = s.collision.position;
|
|
}
|
|
|
|
s.velocity = { x: 0, y: 0 };
|
|
// s.position = { x: posP.x, y: posP.y }
|
|
s.position = { x: p.x, y: p.y };
|
|
|
|
// s.node.style.transform = `translate(${s.position.x}px, ${s.position.y}px)`;
|
|
// } else if (current && s.collision) {
|
|
|
|
// if (s.isLanded && s.velocity.y < 0) {
|
|
// s.gearDown = false;
|
|
// s.isLanded = false;
|
|
// s.position = { x: p.x, y: p.y };
|
|
// s.node.style.transform = `translate(${s.position.x}px, ${s.position.y}px)`;
|
|
// s.collision = null;
|
|
// } else {
|
|
// s.velocity = { x: 0, y: 0 };
|
|
// }
|
|
} else {
|
|
s.position = { x: p.x, y: p.y };
|
|
// s.node.style.transform = `translate(${s.position.x}px, ${s.position.y}px)`;
|
|
}
|
|
return s.position;
|
|
}
|
|
|
|
function updateEdges(position) {
|
|
// const collisionEdges = findAllEdges(allEdgePts, position);
|
|
const collisionEdges = [];
|
|
|
|
[...edgeContainer.children].forEach(l => {
|
|
const x1 = l.getAttribute('x1');
|
|
const y1 = l.getAttribute('y1');
|
|
const x2 = l.getAttribute('x2');
|
|
const y2 = l.getAttribute('y2');
|
|
const edge = `${x1},${y1} ${x2},${y2}`;
|
|
|
|
if (collisionEdges.includes(edge))
|
|
if ([
|
|
edgeContainer.childElementCount <= allStartingEdges.length,
|
|
!allStartingEdges.includes(edge)
|
|
].some(c => c))
|
|
l.remove();
|
|
});
|
|
}
|
|
|
|
function updateLines(elapsed, walls, position, velocity) {
|
|
const edges = walls.reduce((acc, wall) => {
|
|
return [...acc, ...wall.edges];
|
|
}, []);
|
|
|
|
const edgeIds = edges.map(e => `normal${e.xa}-${e.ya}-${e.xb}-${e.yb}`);
|
|
const nodes = [...linesContainer.children];
|
|
|
|
nodes.forEach(n => {
|
|
if (!edgeIds.includes(n.id)) n.remove();
|
|
});
|
|
|
|
edges.forEach(({ xa, ya, xb, yb }) => {
|
|
const id = `normal${xa}-${ya}-${xb}-${yb}`;
|
|
const g = linesContainer.querySelector(`#${id}`) || document.createElementNS(namespaceURIsvg, 'g');
|
|
const el = g.querySelector('line') || document.createElementNS(namespaceURIsvg, 'line');
|
|
const star = g.querySelector('circle') || document.createElementNS(namespaceURIsvg, 'circle');
|
|
star.setAttribute('r', 1);
|
|
|
|
const baseSlope = slope({ xa, ya, xb, yb });
|
|
|
|
let isx, isy;
|
|
|
|
|
|
if (baseSlope === -Infinity || baseSlope === Infinity) {
|
|
isx = xa;
|
|
isy = position.y;
|
|
} else if (baseSlope === 0) { // base is horizontal
|
|
isx = position.x;
|
|
isy = ya;
|
|
} else {
|
|
// const clPt = collisionPosition({ xa, ya, xb, yb }, position, velocity);
|
|
// isx = clPt.x;
|
|
// isy = clPt.y;
|
|
star.setAttribute('cx', 0);
|
|
star.setAttribute('cy', 0);
|
|
}
|
|
|
|
return g;
|
|
});
|
|
}
|
|
|
|
function firstFrame(timestamp) {
|
|
zero = timestamp;
|
|
zeroForTimer = timestamp;
|
|
previous = timestamp;
|
|
animate(timestamp);
|
|
}
|
|
|
|
function animate(timestamp) {
|
|
// console.log("----------animate timestamp", timestamp, "previous", previous);
|
|
|
|
// const elapsed = (timestamp * 1000 - previous * 1000) / 1000;
|
|
const elapsed = (Math.round(timestamp * 1000) - Math.round(previous * 1000)) / 1000;
|
|
const delta = (timestamp * 1000 - zero * 1000) / 1000;
|
|
previous = timestamp;
|
|
|
|
if (delta >= 1000) {
|
|
fps.innerText = frameCount;
|
|
|
|
// debug.innerText = `velocity ${velocity}\n`
|
|
// + 'bullets\nx\ty\tvx\tvy\n'
|
|
// + bullets.map(b => {
|
|
// return `${b.x.toFixed(2)}\t${b.y.toFixed(2)}\t${b.vx.toFixed(2)}\t${b.vy.toFixed(2)}`;
|
|
// }).join("\n");
|
|
|
|
zero = timestamp;
|
|
frameCount = 0;
|
|
} else {
|
|
frameCount++;
|
|
}
|
|
|
|
Ships.forEach(({ entity_id }) => {
|
|
Move.update({ entity_id }, elapsed);
|
|
Rotate.update({ entity_id }, elapsed);
|
|
Draw.update({ entity_id });
|
|
});
|
|
|
|
// updateEdges(position);
|
|
if (drawCollisionLines) updateTriangles(position);
|
|
|
|
// stop game if ship touches a wall
|
|
// if (s.collision && !s.isLanded) {
|
|
// started = false;
|
|
// isReadingKeys = false;
|
|
// s.collision.wall.node.setAttribute('fill', 'red');
|
|
// }
|
|
|
|
if (restart) {
|
|
started = false;
|
|
restart = false;
|
|
init();
|
|
time.innerText = 0;
|
|
}
|
|
|
|
// const finished = edgeContainer.childElementCount <= 0;
|
|
// if (finished) started = false;
|
|
|
|
if (started) {
|
|
time.innerText = ((timestamp - zeroForTimer) * 0.001).toFixed(3);
|
|
// requestAnimationFrame(t => animate(t));
|
|
requestAnimationFrame(animate);
|
|
}
|
|
}
|
|
|
|
let force = 1;
|
|
let torque = 0.1;
|
|
let spacePressed = false;
|
|
let restartPressed = false;
|
|
let upPressed = false;
|
|
let downPressed = false;
|
|
let leftPressed = false;
|
|
let rightPressed = false;
|
|
let rotateCWPressed = false;
|
|
let rotateCCWPressed = false;
|
|
const { entity_id } = Ships[0];
|
|
|
|
function drawShot(shot) {
|
|
const lineEl = document.createElementNS(namespaceURIsvg, 'line');
|
|
lineEl.classList.add('bullet');
|
|
lineEl.setAttribute('x1', shot.origin.x);
|
|
lineEl.setAttribute('y1', shot.origin.y);
|
|
lineEl.setAttribute('x2', shot.target.x);
|
|
lineEl.setAttribute('y2', shot.target.y);
|
|
lineEl.addEventListener('transitionend', e => e.target.remove());
|
|
setTimeout(() => lineEl.classList.add('fade'), 1000);
|
|
const tipEl = document.querySelector("#ship1 .cannon .tip")
|
|
tipEl.classList.remove("recoil");
|
|
setTimeout(() => tipEl.classList.add('recoil'), 0);
|
|
|
|
let pt, hit;
|
|
for (let i = 0; i <= lineEl.getTotalLength(); i++) {
|
|
pt = lineEl.getPointAtLength(i);
|
|
hit = [...wallElements].find(el => el.isPointInFill(pt));
|
|
if (hit) break;
|
|
}
|
|
|
|
if (hit) {
|
|
lineEl.setAttribute('x2', pt.x);
|
|
lineEl.setAttribute('y2', pt.y);
|
|
}
|
|
|
|
return bulletsContainer.appendChild(lineEl);
|
|
}
|
|
|
|
document.addEventListener("keydown", function(e) {
|
|
if (!isReadingKeys) return;
|
|
|
|
if (!started) {
|
|
started = true;
|
|
frameCount = 0;
|
|
requestAnimationFrame(firstFrame);
|
|
}
|
|
|
|
switch (e.code) {
|
|
case "Space":
|
|
if (!spacePressed) {
|
|
spacePressed = true;
|
|
drawShot(Shoot.update(Ships[0]));
|
|
}
|
|
break;
|
|
case "KeyW":
|
|
case "ArrowUp":
|
|
if (!upPressed) {
|
|
upPressed = true;
|
|
Acceleration[entity_id].y += -force;
|
|
}
|
|
break;
|
|
case "KeyS":
|
|
case "ArrowDown":
|
|
if (!downPressed) {
|
|
downPressed = true;
|
|
Acceleration[entity_id].y += force;
|
|
}
|
|
break;
|
|
case "KeyA":
|
|
case "ArrowLeft":
|
|
if (!leftPressed) {
|
|
leftPressed = true;
|
|
if (!s.gearDown) Acceleration[entity_id].x += -force;
|
|
}
|
|
break;
|
|
case "KeyD":
|
|
case "ArrowRight":
|
|
if (!rightPressed) {
|
|
rightPressed = true;
|
|
if (!s.gearDown) Acceleration[entity_id].x += force;
|
|
}
|
|
break;
|
|
case "KeyQ":
|
|
case "Comma":
|
|
if (!rotateCCWPressed) {
|
|
rotateCCWPressed = true;
|
|
s.angularAcceleration -= torque;
|
|
AngularAcceleration[entity_id] -= torque;
|
|
}
|
|
break;
|
|
case "KeyE":
|
|
case "Period":
|
|
if (!rotateCWPressed) {
|
|
rotateCWPressed = true;
|
|
s.angularAcceleration += torque;
|
|
AngularAcceleration[entity_id] += torque;
|
|
}
|
|
break;
|
|
case "KeyP": // Pause
|
|
started = !started;
|
|
break;
|
|
case "KeyG": // Landing gear
|
|
s.gearDown = !s.gearDown;
|
|
break;
|
|
}
|
|
});
|
|
|
|
document.addEventListener("keyup", function(e) {
|
|
switch (e.code) {
|
|
case "Space":
|
|
spacePressed = false;
|
|
break;
|
|
case "KeyR":
|
|
isReadingKeys = true;
|
|
!started ? init() : restart = true;
|
|
break;
|
|
case "KeyW":
|
|
case "ArrowUp":
|
|
if (upPressed) {
|
|
upPressed = false;
|
|
Acceleration[entity_id].y -= -force;
|
|
}
|
|
break;
|
|
case "KeyS":
|
|
case "ArrowDown":
|
|
if (downPressed) {
|
|
downPressed = false;
|
|
Acceleration[entity_id].y -= force;
|
|
}
|
|
break;
|
|
case "KeyA":
|
|
case "ArrowLeft":
|
|
if (leftPressed) {
|
|
leftPressed = false;
|
|
if (!s.gearDown) Acceleration[entity_id].x -= -force;
|
|
}
|
|
break;
|
|
case "KeyD":
|
|
case "ArrowRight":
|
|
if (rightPressed) {
|
|
rightPressed = false;
|
|
if (!s.gearDown) Acceleration[entity_id].x -= force;
|
|
}
|
|
break;
|
|
case "KeyQ":
|
|
case "Comma":
|
|
if (rotateCCWPressed) {
|
|
rotateCCWPressed = false;
|
|
s.angularAcceleration += torque;
|
|
AngularAcceleration[entity_id] += torque;
|
|
}
|
|
break;
|
|
case "KeyE":
|
|
case "Period":
|
|
if (rotateCWPressed) {
|
|
rotateCWPressed = false;
|
|
s.angularAcceleration -= torque;
|
|
AngularAcceleration[entity_id] -= torque;
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
const pointer = svg.querySelector('#pointer');
|
|
const xp = pointer.querySelector('.x');
|
|
const yp = pointer.querySelector('.y');
|
|
const pointerPt = svg.createSVGPoint();
|
|
|
|
svg.addEventListener("pointermove", function({ clientX, clientY }) {
|
|
pointerPt.x = clientX;
|
|
pointerPt.y = clientY;
|
|
// https://www.sitepoint.com/how-to-translate-from-dom-to-svg-coordinates-and-back-again/
|
|
const svgP = pointerPt.matrixTransform(svg.getScreenCTM().inverse());
|
|
|
|
if (bg.isPointInFill(svgP)) {
|
|
xp.innerText = Math.trunc(svgP.x);
|
|
yp.innerText = Math.trunc(svgP.y);
|
|
}
|
|
});
|
|
|
|
init();
|
|
|
|
//]]></script>
|
|
</svg>
|