`, 300);
+ // if (game.levelsCleared === 0) text = "";
+ // text = "Level " + (game.levelsCleared + 1) + ": " + spawn.pickList[0] + "s + " + spawn.pickList[1] + "s";
+
+ // text = text + " with population: ";
+ // for (let i = 0, len = spawn.pickList.length; i < len; ++i) {
+ // if (spawn.pickList[i] != spawn.pickList[i - 1]) {
+ // text += spawn.pickList[i] + ", ";
+ // }
+ // }
+ // this.speech(text);
+ // game.makeTextLog(text, 360);
+ },
+ addToWorld(mapName) {
+ //needs to be run to put bodies into the world
+ for (let i = 0; i < body.length; i++) {
+ //body[i].collisionFilter.group = 0;
+ body[i].collisionFilter.category = 0x010000;
+ body[i].collisionFilter.mask = 0x011111;
+ body[i].classType = "body";
+ World.add(engine.world, body[i]); //add to world
+ }
+ for (let i = 0; i < map.length; i++) {
+ //map[i].collisionFilter.group = 0;
+ map[i].collisionFilter.category = 0x000001;
+ map[i].collisionFilter.mask = 0x111111;
+ Matter.Body.setStatic(map[i], true); //make static
+ World.add(engine.world, map[i]); //add to world
+ }
+ for (let i = 0; i < cons.length; i++) {
+ World.add(engine.world, cons[i]);
+ }
+ for (let i = 0; i < consBB.length; i++) {
+ World.add(engine.world, consBB[i]);
+ }
+ }
+};
\ No newline at end of file
diff --git a/js/mobs.js b/js/mobs.js
new file mode 100644
index 0000000..201c50a
--- /dev/null
+++ b/js/mobs.js
@@ -0,0 +1,990 @@
+//create array of mobs
+let mob = [];
+//method to populate the array above
+const mobs = {
+ loop() {
+ let i = mob.length;
+ while (i--) {
+ if (mob[i].alive) {
+ mob[i].do();
+ } else {
+ mob[i].replace(i); //removing mob and replace with body, this is done here to avoid an array index bug with drawing I think
+ }
+ }
+ },
+ draw() {
+ ctx.lineWidth = 2;
+ let i = mob.length;
+ while (i--) {
+ ctx.beginPath();
+ const vertices = mob[i].vertices;
+ ctx.moveTo(vertices[0].x, vertices[0].y);
+ for (let j = 1, len = vertices.length; j < len; ++j) {
+ ctx.lineTo(vertices[j].x, vertices[j].y);
+ }
+ ctx.lineTo(vertices[0].x, vertices[0].y);
+ ctx.fillStyle = mob[i].fill;
+ ctx.strokeStyle = mob[i].stroke;
+ ctx.fill();
+ ctx.stroke();
+ }
+ },
+ alert(range) {
+ range = range * range;
+ for (let i = 0; i < mob.length; i++) {
+ if (mob[i].distanceToPlayer2() < range) mob[i].locatePlayer();
+ }
+ },
+ startle(amount) {
+ for (let i = 0; i < mob.length; i++) {
+ if (!mob[i].seePlayer.yes) {
+ mob[i].force.x += amount * mob[i].mass * (Math.random() - 0.5);
+ mob[i].force.y += amount * mob[i].mass * (Math.random() - 0.5);
+ }
+ }
+ },
+ //**********************************************************************************************
+ //**********************************************************************************************
+ spawn(xPos, yPos, sides, radius, color) {
+ let i = mob.length;
+ mob[i] = Matter.Bodies.polygon(xPos, yPos, sides, radius, {
+ //inertia: Infinity, //prevents rotation
+ mob: true,
+ density: 0.001,
+ //friction: 0,
+ frictionAir: 0.005,
+ //frictionStatic: 0,
+ restitution: 0.5,
+ collisionFilter: {
+ group: 0,
+ category: 0x000010,
+ mask: 0x011111
+ },
+ onHit: undefined,
+ alive: true,
+ index: i,
+ health: 1,
+ accelMag: 0.001,
+ cd: 0, //game cycle when cooldown will be over
+ delay: 60, //static: time between cooldowns
+ fill: color,
+ stroke: "#000",
+ seePlayer: {
+ yes: false,
+ recall: 0,
+ position: {
+ x: xPos,
+ y: yPos
+ }
+ },
+ radius: radius,
+ spawnPos: {
+ x: xPos,
+ y: yPos
+ },
+ seeAtDistance2: 4000000, //sqrt(4000000) = 2000 = max seeing range
+ distanceToPlayer() {
+ const dx = this.position.x - player.position.x;
+ const dy = this.position.y - player.position.y;
+ return Math.sqrt(dx * dx + dy * dy);
+ },
+ distanceToPlayer2() {
+ const dx = this.position.x - player.position.x;
+ const dy = this.position.y - player.position.y;
+ return dx * dx + dy * dy;
+ },
+ gravity() {
+ this.force.y += this.mass * this.g;
+ },
+ seePlayerFreq: Math.round((30 + 30 * Math.random()) * game.lookFreqScale), //how often NPC checks to see where player is, lower numbers have better vision
+ foundPlayer() {
+ this.locatePlayer();
+ if (!this.seePlayer.yes) {
+ this.alertNearByMobs();
+ this.seePlayer.yes = true;
+ }
+ },
+ lostPlayer() {
+ this.seePlayer.yes = false;
+ this.seePlayer.recall -= this.seePlayerFreq;
+ if (this.seePlayer.recall < 0) this.seePlayer.recall = 0;
+ },
+ memory: 120, //default time to remember player's location
+ locatePlayer() {
+ if (!mech.isStealth) {
+ // updates mob's memory of player location
+ this.seePlayer.recall = this.memory + Math.round(this.memory * Math.random()); //seconds before mob falls a sleep
+ this.seePlayer.position.x = player.position.x;
+ this.seePlayer.position.y = player.position.y;
+ }
+ },
+ // locatePlayerByDist() {
+ // if (this.distanceToPlayer2() < this.locateRange) {
+ // this.locatePlayer();
+ // }
+ // },
+ seePlayerCheck() {
+ if (!(game.cycle % this.seePlayerFreq)) {
+ if (
+ this.distanceToPlayer2() < this.seeAtDistance2 &&
+ Matter.Query.ray(map, this.position, this.mechPosRange()).length === 0 &&
+ Matter.Query.ray(body, this.position, this.mechPosRange()).length === 0
+ ) {
+ this.foundPlayer();
+ } else if (this.seePlayer.recall) {
+ this.lostPlayer();
+ }
+ }
+ },
+ seePlayerCheckByDistance() {
+ if (!(game.cycle % this.seePlayerFreq)) {
+ if (this.distanceToPlayer2() < this.seeAtDistance2) {
+ this.foundPlayer();
+ } else if (this.seePlayer.recall) {
+ this.lostPlayer();
+ }
+ }
+ },
+ seePlayerByDistOrLOS() {
+ if (!(game.cycle % this.seePlayerFreq)) {
+ if (
+ this.distanceToPlayer2() < this.seeAtDistance2 ||
+ (Matter.Query.ray(map, this.position, this.mechPosRange()).length === 0 && Matter.Query.ray(body, this.position, this.mechPosRange()).length === 0)
+ ) {
+ this.foundPlayer();
+ } else if (this.seePlayer.recall) {
+ this.lostPlayer();
+ }
+ }
+ },
+ seePlayerByDistAndLOS() {
+ if (!(game.cycle % this.seePlayerFreq)) {
+ if (
+ this.distanceToPlayer2() < this.seeAtDistance2 &&
+ (Matter.Query.ray(map, this.position, this.mechPosRange()).length === 0 && Matter.Query.ray(body, this.position, this.mechPosRange()).length === 0)
+ ) {
+ this.foundPlayer();
+ } else if (this.seePlayer.recall) {
+ this.lostPlayer();
+ }
+ }
+ },
+ isLookingAtPlayer(threshold) {
+ const diff = Matter.Vector.normalise(Matter.Vector.sub(player.position, this.position));
+ //make a vector for the mob's direction of length 1
+ const dir = {
+ x: Math.cos(this.angle),
+ y: Math.sin(this.angle)
+ };
+ //the dot product of diff and dir will return how much over lap between the vectors
+ const dot = Matter.Vector.dot(dir, diff);
+ // console.log(Math.cos(dot)*180/Math.PI)
+ if (dot > threshold) {
+ return true;
+ } else {
+ return false;
+ }
+ },
+ lookRange: 0.2 + Math.random() * 0.2,
+ lookTorque: 0.0000004 * (Math.random() > 0.5 ? -1 : 1),
+ seePlayerByLookingAt() {
+ if (!(game.cycle % this.seePlayerFreq) && (this.seePlayer.recall || this.isLookingAtPlayer(this.lookRange))) {
+ if (
+ this.distanceToPlayer2() < this.seeAtDistance2 &&
+ Matter.Query.ray(map, this.position, this.mechPosRange()).length === 0 &&
+ Matter.Query.ray(body, this.position, this.mechPosRange()).length === 0
+ ) {
+ this.foundPlayer();
+ } else if (this.seePlayer.recall) {
+ this.lostPlayer();
+ }
+ }
+ //if you don't recall player location rotate and draw to show where you are looking
+ if (!this.seePlayer.recall) {
+ this.torque = this.lookTorque * this.inertia;
+ //draw
+ const range = Math.PI * this.lookRange;
+ ctx.beginPath();
+ ctx.arc(this.position.x, this.position.y, this.radius * 2.5, this.angle - range, this.angle + range);
+ ctx.arc(this.position.x, this.position.y, this.radius * 1.4, this.angle + range, this.angle - range, true);
+ ctx.fillStyle = "rgba(0,0,0,0.07)";
+ ctx.fill();
+ }
+ },
+ mechPosRange() {
+ return {
+ x: player.position.x, // + (Math.random() - 0.5) * 50,
+ y: player.position.y + (Math.random() - 0.5) * 110
+ };
+ //mob vision for testing
+ // ctx.beginPath();
+ // ctx.lineWidth = "5";
+ // ctx.strokeStyle = "#ff0";
+ // ctx.moveTo(this.position.x, this.position.y);
+ // ctx.lineTo(targetPos.x, targetPos.y);
+ // ctx.stroke();
+ // return targetPos;
+ },
+ laserBeam() {
+ if (game.cycle % 7 && this.seePlayer.yes) {
+ ctx.setLineDash([125 * Math.random(), 125 * Math.random()]);
+ // ctx.lineDashOffset = 6*(game.cycle % 215);
+ if (this.distanceToPlayer() < this.laserRange) {
+ //if (Math.random()>0.2 && this.seePlayer.yes && this.distanceToPlayer2()<800000) {
+ mech.damage(0.0003 * game.dmgScale);
+ if (mech.fieldMeter > 0.1) mech.fieldMeter -= 0.005
+ ctx.beginPath();
+ ctx.moveTo(this.position.x, this.position.y);
+ ctx.lineTo(mech.pos.x, mech.pos.y);
+ ctx.lineTo(mech.pos.x + (Math.random() - 0.5) * 3000, mech.pos.y + (Math.random() - 0.5) * 3000);
+ ctx.lineWidth = 2;
+ ctx.strokeStyle = "rgb(255,0,170)";
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(mech.pos.x, mech.pos.y, 40, 0, 2 * Math.PI);
+ ctx.fillStyle = "rgba(255,0,170,0.15)";
+ ctx.fill();
+ }
+ ctx.beginPath();
+ ctx.arc(this.position.x, this.position.y, this.laserRange * 0.9, 0, 2 * Math.PI);
+ ctx.strokeStyle = "rgba(255,0,170,0.5)";
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ ctx.setLineDash([]);
+ }
+ },
+ laser() {
+ const vertexCollision = function (v1, v1End, domain) {
+ for (let i = 0; i < domain.length; ++i) {
+ let vertices = domain[i].vertices;
+ const len = vertices.length - 1;
+ for (let j = 0; j < len; j++) {
+ results = game.checkLineIntersection(v1, v1End, vertices[j], vertices[j + 1]);
+ if (results.onLine1 && results.onLine2) {
+ const dx = v1.x - results.x;
+ const dy = v1.y - results.y;
+ const dist2 = dx * dx + dy * dy;
+ if (dist2 < best.dist2 && (!domain[i].mob || domain[i].alive)) {
+ best = {
+ x: results.x,
+ y: results.y,
+ dist2: dist2,
+ who: domain[i],
+ v1: vertices[j],
+ v2: vertices[j + 1]
+ };
+ }
+ }
+ }
+ results = game.checkLineIntersection(v1, v1End, vertices[0], vertices[len]);
+ if (results.onLine1 && results.onLine2) {
+ const dx = v1.x - results.x;
+ const dy = v1.y - results.y;
+ const dist2 = dx * dx + dy * dy;
+ if (dist2 < best.dist2) {
+ best = {
+ x: results.x,
+ y: results.y,
+ dist2: dist2,
+ who: domain[i],
+ v1: vertices[0],
+ v2: vertices[len]
+ };
+ }
+ }
+ }
+ };
+ if (this.seePlayer.recall) {
+ this.torque = this.lookTorque * this.inertia * 2;
+
+ const seeRange = 2500;
+ best = {
+ x: null,
+ y: null,
+ dist2: Infinity,
+ who: null,
+ v1: null,
+ v2: null
+ };
+ const look = {
+ x: this.position.x + seeRange * Math.cos(this.angle),
+ y: this.position.y + seeRange * Math.sin(this.angle)
+ };
+ vertexCollision(this.position, look, map);
+ vertexCollision(this.position, look, body);
+ vertexCollision(this.position, look, [player]);
+ // hitting player
+ if (best.who === player) {
+ dmg = 0.004 * game.dmgScale;
+ mech.damage(dmg);
+ //draw damage
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(best.x, best.y, dmg * 2000, 0, 2 * Math.PI);
+ ctx.fill();
+ }
+ //draw beam
+ if (best.dist2 === Infinity) {
+ best = look;
+ }
+ ctx.beginPath();
+ ctx.moveTo(this.position.x, this.position.y);
+ ctx.lineTo(best.x, best.y);
+ ctx.strokeStyle = "#f00"; // Purple path
+ ctx.lineWidth = 1;
+ ctx.setLineDash([50 + 120 * Math.random(), 50 * Math.random()]);
+ ctx.stroke(); // Draw it
+ ctx.setLineDash([0, 0]);
+ }
+ },
+ searchSpring() {
+ ctx.beginPath();
+ ctx.arc(this.cons.pointA.x, this.cons.pointA.y, 6, 0, 2 * Math.PI);
+ ctx.arc(this.cons2.pointA.x, this.cons2.pointA.y, 6, 0, 2 * Math.PI);
+ // ctx.arc(this.cons.bodyB.position.x, this.cons.bodyB.position.y,6,0,2*Math.PI);
+ ctx.fillStyle = "#222";
+ ctx.fill();
+
+ if (!(game.cycle % this.seePlayerFreq)) {
+ if (
+ (this.seePlayer.recall || this.isLookingAtPlayer(this.lookRange)) &&
+ this.distanceToPlayer2() < this.seeAtDistance2 &&
+ Matter.Query.ray(map, this.position, player.position).length === 0 &&
+ Matter.Query.ray(body, this.position, player.position).length === 0
+ ) {
+ this.foundPlayer();
+ if (!(game.cycle % (this.seePlayerFreq * 2))) {
+ this.springTarget.x = this.seePlayer.position.x;
+ this.springTarget.y = this.seePlayer.position.y;
+ this.cons.length = -200;
+ this.cons2.length = 100 + 1.5 * this.radius;
+ } else {
+ this.springTarget2.x = this.seePlayer.position.x;
+ this.springTarget2.y = this.seePlayer.position.y;
+ this.cons.length = 100 + 1.5 * this.radius;
+ this.cons2.length = -200;
+ }
+ } else if (this.seePlayer.recall) {
+ this.lostPlayer();
+ }
+ }
+ //if you don't recall player location rotate and draw to show where you are looking
+ if (!this.seePlayer.recall) {
+ this.torque = this.lookTorque * this.inertia;
+ //draw
+ const range = Math.PI * this.lookRange;
+ ctx.beginPath();
+ ctx.arc(this.position.x, this.position.y, this.radius * 2.5, this.angle - range, this.angle + range);
+ ctx.arc(this.position.x, this.position.y, this.radius * 1.4, this.angle + range, this.angle - range, true);
+ ctx.fillStyle = "rgba(0,0,0,0.07)";
+ ctx.fill();
+ //spring to random place on map
+ const vertexCollision = function (v1, v1End, domain) {
+ for (let i = 0; i < domain.length; ++i) {
+ let vertices = domain[i].vertices;
+ const len = vertices.length - 1;
+ for (let j = 0; j < len; j++) {
+ results = game.checkLineIntersection(v1, v1End, vertices[j], vertices[j + 1]);
+ if (results.onLine1 && results.onLine2) {
+ const dx = v1.x - results.x;
+ const dy = v1.y - results.y;
+ const dist2 = dx * dx + dy * dy;
+ if (dist2 < best.dist2 && (!domain[i].mob || domain[i].alive)) {
+ best = {
+ x: results.x,
+ y: results.y,
+ dist2: dist2,
+ who: domain[i],
+ v1: vertices[j],
+ v2: vertices[j + 1]
+ };
+ }
+ }
+ }
+ results = game.checkLineIntersection(v1, v1End, vertices[0], vertices[len]);
+ if (results.onLine1 && results.onLine2) {
+ const dx = v1.x - results.x;
+ const dy = v1.y - results.y;
+ const dist2 = dx * dx + dy * dy;
+ if (dist2 < best.dist2) {
+ best = {
+ x: results.x,
+ y: results.y,
+ dist2: dist2,
+ who: domain[i],
+ v1: vertices[0],
+ v2: vertices[len]
+ };
+ }
+ }
+ }
+ };
+ const seeRange = 3000;
+ if (!(game.cycle % (this.seePlayerFreq * 10))) {
+ best = {
+ x: null,
+ y: null,
+ dist2: Infinity,
+ who: null,
+ v1: null,
+ v2: null
+ };
+ const look = {
+ x: this.position.x + seeRange * Math.cos(this.angle),
+ y: this.position.y + seeRange * Math.sin(this.angle)
+ };
+ vertexCollision(this.position, look, map);
+ if (best.dist2 != Infinity) {
+ this.springTarget.x = best.x;
+ this.springTarget.y = best.y;
+ this.cons.length = 100 + 1.5 * this.radius;
+ this.cons2.length = 100 + 1.5 * this.radius;
+ }
+ }
+ if (!((game.cycle + this.seePlayerFreq * 5) % (this.seePlayerFreq * 10))) {
+ best = {
+ x: null,
+ y: null,
+ dist2: Infinity,
+ who: null,
+ v1: null,
+ v2: null
+ };
+ const look = {
+ x: this.position.x + seeRange * Math.cos(this.angle),
+ y: this.position.y + seeRange * Math.sin(this.angle)
+ };
+ vertexCollision(this.position, look, map);
+ if (best.dist2 != Infinity) {
+ this.springTarget2.x = best.x;
+ this.springTarget2.y = best.y;
+ this.cons.length = 100 + 1.5 * this.radius;
+ this.cons2.length = 100 + 1.5 * this.radius;
+ }
+ }
+ }
+ },
+ alertNearByMobs() {
+ //this.alertRange2 is set at the very bottom of this mobs, after mob is made
+ for (let i = 0; i < mob.length; i++) {
+ if (!mob[i].seePlayer.recall && Matter.Vector.magnitudeSquared(Matter.Vector.sub(this.position, mob[i].position)) < this.alertRange2) {
+ mob[i].locatePlayer();
+ }
+ }
+ //add alert to draw queue
+ // game.drawList.push({
+ // x: this.position.x,
+ // y: this.position.y,
+ // radius: Math.sqrt(this.alertRange2),
+ // color: "rgba(0,0,0,0.02)",
+ // time: game.drawTime
+ // });
+ },
+ zoom() {
+ this.zoomMode--;
+ if (this.zoomMode > 150) {
+ this.drawTrail();
+ if (this.seePlayer.recall) {
+ //attraction to player
+ const forceMag = this.accelMag * this.mass;
+ const angle = Math.atan2(player.position.y - this.position.y, player.position.x - this.position.x);
+ this.force.x += forceMag * Math.cos(angle);
+ this.force.y += forceMag * Math.sin(angle);
+ }
+ } else if (this.zoomMode < 0) {
+ this.zoomMode = 300;
+ this.setupTrail();
+ }
+ },
+ setupTrail() {
+ this.trail = [];
+ for (let i = 0; i < this.trailLength; ++i) {
+ this.trail.push({
+ x: this.position.x,
+ y: this.position.y
+ });
+ }
+ },
+ drawTrail() {
+ //dont' forget to run setupTrail() after mob spawn
+ const t = this.trail;
+ const len = t.length;
+ t.pop();
+ t.unshift({
+ x: this.position.x,
+ y: this.position.y
+ });
+ //draw
+ ctx.strokeStyle = this.trailFill;
+ ctx.beginPath();
+ // ctx.moveTo(t[0].x, t[0].y);
+ // ctx.lineTo(t[0].x, t[0].y);
+ // ctx.globalAlpha = 0.2;
+ // ctx.lineWidth = this.radius * 3;
+ // ctx.stroke();
+ ctx.globalAlpha = 0.5 / len;
+ ctx.lineWidth = this.radius * 1.95;
+ for (let i = 0; i < len; ++i) {
+ // ctx.lineWidth *= 0.96;
+ ctx.lineTo(t[i].x, t[i].y);
+ ctx.stroke();
+ }
+ ctx.globalAlpha = 1;
+ },
+ curl(range = 1000, mag = -10) {
+ //cause all mobs, and bodies to rotate in a circle
+ applyCurl = function (center, array) {
+ for (let i = 0; i < array.length; ++i) {
+ const sub = Matter.Vector.sub(center, array[i].position)
+ const radius2 = Matter.Vector.magnitudeSquared(sub);
+
+ //if too close, like center mob or shield, don't curl // if too far don't curl
+ if (radius2 < range * range && radius2 > 10000) {
+ const curlVector = Matter.Vector.mult(Matter.Vector.perp(Matter.Vector.normalise(sub)), mag)
+ //apply curl force
+ Matter.Body.setVelocity(array[i], {
+ x: array[i].velocity.x * 0.94 + curlVector.x * 0.06,
+ y: array[i].velocity.y * 0.94 + curlVector.y * 0.06
+ })
+ // //draw curl
+ // ctx.beginPath();
+ // ctx.moveTo(array[i].position.x, array[i].position.y);
+ // ctx.lineTo(array[i].position.x + curlVector.x * 10, array[i].position.y + curlVector.y * 10);
+ // ctx.lineWidth = 2;
+ // ctx.strokeStyle = "#000";
+ // ctx.stroke();
+ }
+ }
+ }
+ applyCurl(this.position, mob);
+ applyCurl(this.position, body);
+ applyCurl(this.position, powerUp);
+ // applyCurl(this.position, bullet); // too powerful, just stops all bullets need to write a curl function just for bullets
+ // applyCurl(this.position, [player]);
+
+ //draw limit
+ // ctx.beginPath();
+ // ctx.arc(this.position.x, this.position.y, range, 0, 2 * Math.PI);
+ // ctx.fillStyle = "rgba(55,255,255, 0.1)";
+ // ctx.fill();
+ },
+ pullPlayer() {
+ if (this.seePlayer.yes && Matter.Vector.magnitudeSquared(Matter.Vector.sub(this.position, player.position)) < 1000000) {
+ const angle = Math.atan2(player.position.y - this.position.y, player.position.x - this.position.x);
+ player.force.x -= game.accelScale * 1.13 * Math.cos(angle) * (mech.onGround ? 2 * player.mass * game.g : player.mass * game.g);
+ player.force.y -= game.accelScale * 0.84 * player.mass * game.g * Math.sin(angle);
+
+ ctx.beginPath();
+ ctx.moveTo(this.position.x, this.position.y);
+ ctx.lineTo(mech.pos.x, mech.pos.y);
+ ctx.lineWidth = Math.min(60, this.radius * 2);
+ ctx.strokeStyle = "rgba(0,0,0,0.5)";
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.arc(mech.pos.x, mech.pos.y, 40, 0, 2 * Math.PI);
+ ctx.fillStyle = "rgba(0,0,0,0.3)";
+ ctx.fill();
+ }
+ },
+ repelBullets() {
+ if (this.seePlayer.yes) {
+ ctx.lineWidth = "8";
+ ctx.strokeStyle = this.fill;
+ ctx.beginPath();
+ for (let i = 0, len = bullet.length; i < len; ++i) {
+ const dx = bullet[i].position.x - this.position.x;
+ const dy = bullet[i].position.y - this.position.y;
+ const dist = Math.sqrt(dx * dx + dy * dy);
+ if (dist < 500) {
+ ctx.moveTo(this.position.x, this.position.y);
+ ctx.lineTo(bullet[i].position.x, bullet[i].position.y);
+ const angle = Math.atan2(dy, dx);
+ const mag = (1500 * bullet[i].mass * game.g) / dist;
+ bullet[i].force.x += mag * Math.cos(angle);
+ bullet[i].force.y += mag * Math.sin(angle);
+ }
+ }
+ ctx.stroke();
+ }
+ },
+ attraction() {
+ //accelerate towards the player
+ if (this.seePlayer.recall) {
+ // && dx * dx + dy * dy < 2000000) {
+ const forceMag = this.accelMag * this.mass;
+ const angle = Math.atan2(this.seePlayer.position.y - this.position.y, this.seePlayer.position.x - this.position.x);
+ this.force.x += forceMag * Math.cos(angle);
+ this.force.y += forceMag * Math.sin(angle);
+ }
+ },
+ repulsionRange: 500000,
+ repulsion() {
+ //accelerate towards the player
+ if (this.seePlayer.recall && this.distanceToPlayer2() < this.repulsionRange) {
+ // && dx * dx + dy * dy < 2000000) {
+ const forceMag = this.accelMag * this.mass;
+ const angle = Math.atan2(this.seePlayer.position.y - this.position.y, this.seePlayer.position.x - this.position.x);
+ this.force.x -= 2 * forceMag * Math.cos(angle);
+ this.force.y -= 2 * forceMag * Math.sin(angle); // - 0.0007 * this.mass; //antigravity
+ }
+ },
+ hop() {
+ //accelerate towards the player after a delay
+ if (this.cd < game.cycle && this.seePlayer.recall && this.speed < 1) {
+ this.cd = game.cycle + this.delay;
+ const forceMag = (this.accelMag + this.accelMag * Math.random()) * this.mass;
+ const angle = Math.atan2(this.seePlayer.position.y - this.position.y, this.seePlayer.position.x - this.position.x);
+ this.force.x += forceMag * Math.cos(angle);
+ this.force.y += forceMag * Math.sin(angle) - 0.04 * this.mass; //antigravity
+ }
+ },
+ hoverOverPlayer() {
+ if (this.seePlayer.recall) {
+ // vertical positioning
+ const rangeY = 250;
+ if (this.position.y > this.seePlayer.position.y - this.hoverElevation + rangeY) {
+ this.force.y -= this.accelMag * this.mass;
+ } else if (this.position.y < this.seePlayer.position.y - this.hoverElevation - rangeY) {
+ this.force.y += this.accelMag * this.mass;
+ }
+ // horizontal positioning
+ const rangeX = 150;
+ if (this.position.x > this.seePlayer.position.x + this.hoverXOff + rangeX) {
+ this.force.x -= this.accelMag * this.mass;
+ } else if (this.position.x < this.seePlayer.position.x + this.hoverXOff - rangeX) {
+ this.force.x += this.accelMag * this.mass;
+ }
+ }
+ // else {
+ // this.gravity();
+ // }
+ },
+ grow() {
+ if (!mech.isBodiesAsleep) {
+ if (this.seePlayer.recall) {
+ if (this.radius < 80) {
+ const scale = 1.01;
+ Matter.Body.scale(this, scale, scale);
+ this.radius *= scale;
+ // this.torque = -0.00002 * this.inertia;
+ this.fill = `hsl(144, ${this.radius}%, 50%)`;
+ }
+ } else {
+ if (this.radius > 15) {
+ const scale = 0.99;
+ Matter.Body.scale(this, scale, scale);
+ this.radius *= scale;
+ this.fill = `hsl(144, ${this.radius}%, 50%)`;
+ }
+ }
+ }
+ },
+ search() {
+ //be sure to declare searchTarget in mob spawn
+ //accelerate towards the searchTarget
+ if (!this.seePlayer.recall) {
+ const newTarget = function (that) {
+ if (Math.random() < 0.05) {
+ that.searchTarget = player.position; //chance to target player
+ } else {
+ //target random body
+ that.searchTarget = map[Math.floor(Math.random() * (map.length - 1))].position;
+ }
+ };
+
+ const sub = Matter.Vector.sub(this.searchTarget, this.position);
+ if (Matter.Vector.magnitude(sub) > this.radius * 2) {
+ // ctx.beginPath();
+ // ctx.strokeStyle = "#aaa";
+ // ctx.moveTo(this.position.x, this.position.y);
+ // ctx.lineTo(this.searchTarget.x,this.searchTarget.y);
+ // ctx.stroke();
+ //accelerate at 0.1 of normal acceleration
+ this.force = Matter.Vector.mult(Matter.Vector.normalise(sub), this.accelMag * this.mass * 0.2);
+ } else {
+ //after reaching random target switch to new target
+ newTarget(this);
+ }
+ //switch to a new target after a while
+ if (!(game.cycle % (this.seePlayerFreq * 15))) {
+ newTarget(this);
+ }
+ }
+ },
+ strike() {
+ //teleport to player when close enough on CD
+ if (this.seePlayer.recall && this.cd < game.cycle) {
+ const dist = Matter.Vector.sub(this.seePlayer.position, this.position);
+ const distMag = Matter.Vector.magnitude(dist);
+ if (distMag < 430) {
+ this.cd = game.cycle + this.delay;
+ ctx.beginPath();
+ ctx.moveTo(this.position.x, this.position.y);
+ Matter.Body.translate(this, Matter.Vector.mult(Matter.Vector.normalise(dist), distMag - 20 - radius));
+ ctx.lineTo(this.position.x, this.position.y);
+ ctx.lineWidth = radius * 2;
+ ctx.strokeStyle = this.fill; //"rgba(0,0,0,0.5)"; //'#000'
+ ctx.stroke();
+ }
+ }
+ },
+ blink() {
+ //teleport towards player as a way to move
+ if (this.seePlayer.recall && !(game.cycle % this.blinkRate)) {
+ ctx.beginPath();
+ ctx.moveTo(this.position.x, this.position.y);
+ const dist = Matter.Vector.sub(this.seePlayer.position, this.position);
+ const distMag = Matter.Vector.magnitude(dist);
+ const unitVector = Matter.Vector.normalise(dist);
+ const rando = (Math.random() - 0.5) * 50;
+ if (distMag < this.blinkLength) {
+ Matter.Body.translate(this, Matter.Vector.mult(unitVector, distMag + rando));
+ } else {
+ Matter.Body.translate(this, Matter.Vector.mult(unitVector, this.blinkLength + rando));
+ }
+ ctx.lineTo(this.position.x, this.position.y);
+ ctx.lineWidth = radius * 2;
+ ctx.strokeStyle = this.stroke; //"rgba(0,0,0,0.5)"; //'#000'
+ ctx.stroke();
+ }
+ },
+ drift() {
+ //teleport towards player as a way to move
+ if (this.seePlayer.recall && !(game.cycle % this.blinkRate)) {
+ // && !mech.lookingAtMob(this,0.5)){
+ ctx.beginPath();
+ ctx.moveTo(this.position.x, this.position.y);
+ const dist = Matter.Vector.sub(this.seePlayer.position, this.position);
+ const distMag = Matter.Vector.magnitude(dist);
+ const vector = Matter.Vector.mult(Matter.Vector.normalise(dist), this.blinkLength);
+ if (distMag < this.blinkLength) {
+ Matter.Body.setPosition(this, this.seePlayer.position);
+ Matter.Body.translate(this, {
+ x: (Math.random() - 0.5) * 50,
+ y: (Math.random() - 0.5) * 50
+ });
+ } else {
+ vector.x += (Math.random() - 0.5) * 200;
+ vector.y += (Math.random() - 0.5) * 200;
+ Matter.Body.translate(this, vector);
+ }
+ ctx.lineTo(this.position.x, this.position.y);
+ ctx.lineWidth = radius * 2;
+ ctx.strokeStyle = this.stroke;
+ ctx.stroke();
+ }
+ },
+ bomb() {
+ //throw a mob/bullet at player
+ if (
+ !(game.cycle % this.fireFreq) &&
+ Math.abs(this.position.x - this.seePlayer.position.x) < 400 && //above player
+ Matter.Query.ray(map, this.position, this.mechPosRange()).length === 0 && //see player
+ Matter.Query.ray(body, this.position, this.mechPosRange()).length === 0
+ ) {
+ spawn.bullet(this.position.x, this.position.y + this.radius * 0.5, 10 + Math.ceil(this.radius / 15), 5);
+ //add spin and speed
+ Matter.Body.setAngularVelocity(mob[mob.length - 1], (Math.random() - 0.5) * 0.5);
+ Matter.Body.setVelocity(mob[mob.length - 1], {
+ x: this.velocity.x,
+ y: this.velocity.y
+ });
+ //spin for mob as well
+ Matter.Body.setAngularVelocity(this, (Math.random() - 0.5) * 0.25);
+ }
+ },
+ fire() {
+ if (!mech.isBodiesAsleep) {
+ const setNoseShape = () => {
+ const mag = this.radius + this.radius * this.noseLength;
+ this.vertices[1].x = this.position.x + Math.cos(this.angle) * mag;
+ this.vertices[1].y = this.position.y + Math.sin(this.angle) * mag;
+ };
+ //throw a mob/bullet at player
+ if (this.seePlayer.recall) {
+ //set direction to turn to fire
+ if (!(game.cycle % this.seePlayerFreq)) {
+ this.fireDir = Matter.Vector.normalise(Matter.Vector.sub(this.seePlayer.position, this.position));
+ this.fireDir.y -= Math.abs(this.seePlayer.position.x - this.position.x) / 1600; //gives the bullet an arc
+ this.fireAngle = Math.atan2(this.fireDir.y, this.fireDir.x);
+ }
+ //rotate towards fireAngle
+ const angle = this.angle + Math.PI / 2;
+ c = Math.cos(angle) * this.fireDir.x + Math.sin(angle) * this.fireDir.y;
+ const threshold = 0.1;
+ if (c > threshold) {
+ this.torque += 0.000004 * this.inertia;
+ } else if (c < -threshold) {
+ this.torque -= 0.000004 * this.inertia;
+ } else if (this.noseLength > 1.5) {
+ //fire
+ spawn.bullet(this.vertices[1].x, this.vertices[1].y, 5 + Math.ceil(this.radius / 15), 5);
+ const v = 15;
+ Matter.Body.setVelocity(mob[mob.length - 1], {
+ x: this.velocity.x + this.fireDir.x * v + Math.random(),
+ y: this.velocity.y + this.fireDir.y * v + Math.random()
+ });
+ this.noseLength = 0;
+ // recoil
+ this.force.x -= 0.005 * this.fireDir.x * this.mass;
+ this.force.y -= 0.005 * this.fireDir.y * this.mass;
+ }
+ if (this.noseLength < 1.5) this.noseLength += this.fireFreq;
+ setNoseShape();
+ } else if (this.noseLength > 0.1) {
+ this.noseLength -= this.fireFreq / 2;
+ setNoseShape();
+ }
+ // else if (this.noseLength < -0.1) {
+ // this.noseLength += this.fireFreq / 4;
+ // setNoseShape();
+ // }
+ }
+ },
+ turnToFacePlayer() {
+ //turn to face player
+ const dx = player.position.x - this.position.x;
+ const dy = -player.position.y + this.position.y;
+ const dist = this.distanceToPlayer();
+ const angle = this.angle + Math.PI / 2;
+ c = Math.cos(angle) * dx - Math.sin(angle) * dy;
+ // if (c > 0.04) {
+ // Matter.Body.rotate(this, 0.01);
+ // } else if (c < 0.04) {
+ // Matter.Body.rotate(this, -0.01);
+ // }
+ if (c > 0.04 * dist) {
+ this.torque += 0.002 * this.mass;
+ } else if (c < 0.04) {
+ this.torque -= 0.002 * this.mass;
+ }
+ },
+ facePlayer() {
+ const unitVector = Matter.Vector.normalise(Matter.Vector.sub(this.seePlayer.position, this.position));
+ const angle = Math.atan2(unitVector.y, unitVector.x);
+ Matter.Body.setAngle(this, angle - Math.PI);
+ },
+ explode() {
+ mech.damage(Math.min(Math.max(0.02 * Math.sqrt(this.mass), 0.01), 0.35) * game.dmgScale);
+ this.dropPowerUp = false;
+ this.death(); //death with no power up or body
+ },
+ timeLimit() {
+ if (!mech.isBodiesAsleep) {
+ this.timeLeft--;
+ if (this.timeLeft < 0) {
+ this.dropPowerUp = false;
+ this.death(); //death with no power up
+ }
+ }
+ },
+ healthBar() {
+ //draw health bar
+ if (this.seePlayer.recall) {
+ // && this.health < 1
+ const h = this.radius * 0.3;
+ const w = this.radius * 2;
+ const x = this.position.x - w / 2;
+ const y = this.position.y - w * 0.7;
+ ctx.fillStyle = "rgba(100, 100, 100, 0.3)";
+ ctx.fillRect(x, y, w, h);
+ ctx.fillStyle = "rgba(255,0,0,0.7)";
+ ctx.fillRect(x, y, w * this.health, h);
+ }
+ },
+ damage(dmg) {
+ this.health -= dmg / Math.sqrt(this.mass);
+ //this.fill = this.color + this.health + ')';
+ if (this.health < 0.1) this.death();
+ this.onDamage(this); //custom damage effects
+ if (b.modEnergySiphon) mech.fieldMeter += dmg * b.modEnergySiphon
+ if (b.modHealthDrain) mech.addHealth(dmg * b.modHealthDrain)
+ },
+ onDamage() {
+ // a placeholder for custom effects on mob damage
+ //to use declare custom method in mob spawn
+ },
+ onDeath() {
+ // a placeholder for custom effects on mob death
+ //to use declare custom method in mob spawn
+ },
+ leaveBody: true,
+ dropPowerUp: true,
+ death() {
+ this.onDeath(this); //custom death effects
+ this.removeConsBB();
+ this.alive = false;
+ if (this.dropPowerUp) {
+ powerUps.spawnRandomPowerUp(this.position.x, this.position.y, this.mass, radius);
+ if (b.modSpores && Math.random() < 0.4) b.spore(this) //spawn drone
+ }
+
+ },
+ removeConsBB() {
+ for (let i = 0, len = consBB.length; i < len; ++i) {
+ if (consBB[i].bodyA === this) {
+ if (consBB[i].bodyB.shield) {
+ consBB[i].bodyB.do = function () {
+ this.death();
+ };
+ }
+ consBB[i].bodyA = consBB[i].bodyB;
+ consBB.splice(i, 1);
+ this.removeConsBB();
+ break;
+ } else if (consBB[i].bodyB === this) {
+ if (consBB[i].bodyA.shield) {
+ consBB[i].bodyA.do = function () {
+ this.death();
+ };
+ }
+ consBB[i].bodyB = consBB[i].bodyA;
+ consBB.splice(i, 1);
+ this.removeConsBB();
+ break;
+ }
+ }
+ },
+ removeCons() {
+ for (let i = 0, len = cons.length; i < len; ++i) {
+ if (cons[i].bodyA === this) {
+ cons[i].bodyA = cons[i].bodyB;
+ cons.splice(i, 1);
+ this.removeCons();
+ break;
+ } else if (cons[i].bodyB === this) {
+ cons[i].bodyB = cons[i].bodyA;
+ cons.splice(i, 1);
+ this.removeCons();
+ break;
+ }
+ }
+ },
+ //replace dead mob with a regular body
+ replace(i) {
+ if (this.leaveBody) {
+ const len = body.length;
+ body[len] = Matter.Bodies.fromVertices(this.position.x, this.position.y, this.vertices);
+ Matter.Body.setVelocity(body[len], this.velocity);
+ Matter.Body.setAngularVelocity(body[len], this.angularVelocity);
+ body[len].collisionFilter.category = 0x010000;
+ body[len].collisionFilter.mask = 0x011111;
+ // body[len].collisionFilter.category = body[len].collisionFilter.category //0x000001;
+ // body[len].collisionFilter.mask = body[len].collisionFilter.mask //0x011111;
+
+ //large mobs or too many bodies go intangible and fall until removed from game to help performance
+ if (body[len].mass > 10 || 40 + 30 * Math.random() < body.length) {
+ body[len].collisionFilter.mask = 0x001100;
+ }
+ body[len].classType = "body";
+ World.add(engine.world, body[len]); //add to world
+ }
+ Matter.World.remove(engine.world, this);
+ mob.splice(i, 1);
+ }
+ });
+ mob[i].alertRange2 = Math.pow(mob[i].radius * 3 + 200, 2);
+ World.add(engine.world, mob[i]); //add to world
+ }
+};
\ No newline at end of file
diff --git a/js/player.js b/js/player.js
new file mode 100644
index 0000000..7814280
--- /dev/null
+++ b/js/player.js
@@ -0,0 +1,1451 @@
+//global player variables for use in matter.js physics
+let player, jumpSensor, playerBody, playerHead, headSensor;
+
+// player Object Prototype *********************************************
+const mech = {
+ spawn() {
+ //load player in matter.js physic engine
+ // let vector = Vertices.fromPath("0 40 50 40 50 115 0 115 30 130 20 130"); //player as a series of vertices
+ let vector = Vertices.fromPath("0,40, 50,40, 50,115, 30,130, 20,130, 0,115, 0,40"); //player as a series of vertices
+ playerBody = Matter.Bodies.fromVertices(0, 0, vector);
+ jumpSensor = Bodies.rectangle(0, 46, 36, 6, {
+ //this sensor check if the player is on the ground to enable jumping
+ sleepThreshold: 99999999999,
+ isSensor: true
+ });
+ vector = Vertices.fromPath("16 -82 2 -66 2 -37 43 -37 43 -66 30 -82");
+ playerHead = Matter.Bodies.fromVertices(0, -55, vector); //this part of the player lowers on crouch
+ headSensor = Bodies.rectangle(0, -57, 48, 45, {
+ //senses if the player's head is empty and can return after crouching
+ sleepThreshold: 99999999999,
+ isSensor: true
+ });
+ player = Body.create({
+ //combine jumpSensor and playerBody
+ parts: [playerBody, playerHead, jumpSensor, headSensor],
+ inertia: Infinity, //prevents player rotation
+ friction: 0.002,
+ frictionAir: 0.001,
+ //frictionStatic: 0.5,
+ restitution: 0,
+ sleepThreshold: Infinity,
+ collisionFilter: {
+ group: 0,
+ category: 0x001000,
+ mask: 0x010011
+ },
+ death() {
+ mech.death();
+ }
+ });
+ Matter.Body.setMass(player, mech.mass);
+ World.add(engine.world, [player]);
+
+ mech.holdConstraint = Constraint.create({
+ //holding body constraint
+ pointA: {
+ x: 0,
+ y: 0
+ },
+ bodyB: jumpSensor, //setting constraint to jump sensor because it has to be on something until the player picks up things
+ stiffness: 0.4
+ });
+ World.add(engine.world, mech.holdConstraint);
+ },
+ cycle: 0,
+ width: 50,
+ radius: 30,
+ fillColor: "#fff",
+ fillColorDark: "#ccc",
+ height: 42,
+ yOffWhen: {
+ crouch: 22,
+ stand: 49,
+ jump: 70
+ },
+ mass: 5,
+ Fx: 0.015, //run Force on ground
+ FxAir: 0.015, //run Force in Air
+ definePlayerMass(mass = 5) {
+ Matter.Body.setMass(player, mass);
+ //reduce air and ground move forces
+ this.Fx = 0.075 / mass
+ this.FxAir = 0.375 / mass / mass
+ //make player stand a bit lower when holding heavy masses
+ this.yOffWhen.stand = Math.max(this.yOffWhen.crouch, Math.min(49, 49 - (mass - 5) * 6))
+ if (this.onGround && !this.crouch) this.yOffGoal = this.yOffWhen.stand;
+ },
+ yOff: 70,
+ yOffGoal: 70,
+ onGround: false, //checks if on ground or in air
+ standingOn: undefined,
+ numTouching: 0,
+ crouch: false,
+ isHeadClear: true,
+ spawnPos: {
+ x: 0,
+ y: 0
+ },
+ spawnVel: {
+ x: 0,
+ y: 0
+ },
+ pos: {
+ x: 0,
+ y: 0
+ },
+ setPosToSpawn(xPos, yPos) {
+ this.spawnPos.x = this.pos.x = xPos;
+ this.spawnPos.y = this.pos.y = yPos;
+ this.transX = this.transSmoothX = canvas.width2 - this.pos.x;
+ this.transY = this.transSmoothY = canvas.height2 - this.pos.y;
+ this.Vx = this.spawnVel.x;
+ this.Vy = this.spawnVel.y;
+ player.force.x = 0;
+ player.force.y = 0;
+ Matter.Body.setPosition(player, this.spawnPos);
+ Matter.Body.setVelocity(player, this.spawnVel);
+ },
+ Sy: 0, //adds a smoothing effect to vertical only
+ Vx: 0,
+ Vy: 0,
+ jumpForce: 0.38,
+ gravity: 0.0019,
+ friction: {
+ ground: 0.01,
+ air: 0.0025
+ },
+ angle: 0,
+ walk_cycle: 0,
+ stepSize: 0,
+ flipLegs: -1,
+ hip: {
+ x: 12,
+ y: 24
+ },
+ knee: {
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 0
+ },
+ foot: {
+ x: 0,
+ y: 0
+ },
+ legLength1: 55,
+ legLength2: 45,
+ transX: 0,
+ transY: 0,
+ move() {
+ this.pos.x = player.position.x;
+ this.pos.y = playerBody.position.y - this.yOff;
+ this.Vx = player.velocity.x;
+ this.Vy = player.velocity.y;
+ },
+ transSmoothX: 0,
+ transSmoothY: 0,
+ lastGroundedPositionY: 0,
+ // mouseZoom: 0,
+ look() {
+ //always on mouse look
+ this.angle = Math.atan2(
+ game.mouseInGame.y - this.pos.y,
+ game.mouseInGame.x - this.pos.x
+ );
+ //smoothed mouse look translations
+ const scale = 0.8;
+ this.transSmoothX = canvas.width2 - this.pos.x - (game.mouse.x - canvas.width2) * scale;
+ this.transSmoothY = canvas.height2 - this.pos.y - (game.mouse.y - canvas.height2) * scale;
+
+ this.transX += (this.transSmoothX - this.transX) * 0.07;
+ this.transY += (this.transSmoothY - this.transY) * 0.07;
+ },
+ doCrouch() {
+ if (!this.crouch) {
+ this.crouch = true;
+ this.yOffGoal = this.yOffWhen.crouch;
+ Matter.Body.translate(playerHead, {
+ x: 0,
+ y: 40
+ });
+ }
+ },
+ undoCrouch() {
+ if (this.crouch) {
+ this.crouch = false;
+ this.yOffGoal = this.yOffWhen.stand;
+ Matter.Body.translate(playerHead, {
+ x: 0,
+ y: -40
+ });
+ }
+ },
+ hardLandCD: 0,
+ enterAir() {
+ //triggered in engine.js on collision
+ this.onGround = false;
+ this.hardLandCD = 0 // disable hard landing
+ if (this.isHeadClear) {
+ if (this.crouch) {
+ this.undoCrouch();
+ }
+ this.yOffGoal = this.yOffWhen.jump;
+ }
+ },
+ //triggered in engine.js on collision
+ enterLand() {
+ this.onGround = true;
+ if (this.crouch) {
+ if (this.isHeadClear) {
+ this.undoCrouch();
+ } else {
+ this.yOffGoal = this.yOffWhen.crouch;
+ }
+ } else {
+ //sets a hard land where player stays in a crouch for a bit and can't jump
+ //crouch is forced in keyMove() on ground section below
+ const momentum = player.velocity.y * player.mass //player mass is 5 so this triggers at 20 down velocity, unless the player is holding something
+ if (momentum > 100) {
+ this.doCrouch();
+ this.yOff = this.yOffWhen.jump;
+ this.hardLandCD = mech.cycle + Math.min(momentum / 6 - 6, 40)
+ } else {
+ this.yOffGoal = this.yOffWhen.stand;
+ }
+ }
+ },
+ buttonCD_jump: 0, //cool down for player buttons
+ keyMove() {
+ if (this.onGround) { //on ground **********************
+ if (this.crouch) {
+ if (!(keys[83] || keys[40]) && this.isHeadClear && this.hardLandCD < mech.cycle) this.undoCrouch();
+ } else if (keys[83] || keys[40] || this.hardLandCD > mech.cycle) {
+ this.doCrouch(); //on ground && not crouched and pressing s or down
+ } else if ((keys[87] || keys[38]) && this.buttonCD_jump + 20 < mech.cycle && this.yOffWhen.stand > 23) {
+ this.buttonCD_jump = mech.cycle; //can't jump again until 20 cycles pass
+
+ //apply a fraction of the jump force to the body the player is jumping off of
+ Matter.Body.applyForce(mech.standingOn, mech.pos, {
+ x: 0,
+ y: this.jumpForce * 0.12 * Math.min(mech.standingOn.mass, 5)
+ });
+
+ player.force.y = -this.jumpForce; //player jump force
+ Matter.Body.setVelocity(player, { //zero player y-velocity for consistent jumps
+ x: player.velocity.x,
+ y: 0
+ });
+ }
+
+ //horizontal move on ground
+ //apply a force to move
+ if (keys[65] || keys[37]) { //left / a
+ player.force.x -= this.Fx
+ if (player.velocity.x > -2) player.force.x -= this.Fx * 0.5
+ } else if (keys[68] || keys[39]) { //right / d
+ player.force.x += this.Fx
+ if (player.velocity.x < 2) player.force.x += this.Fx * 0.5
+ } else {
+ const stoppingFriction = 0.92;
+ Matter.Body.setVelocity(player, {
+ x: player.velocity.x * stoppingFriction,
+ y: player.velocity.y * stoppingFriction
+ });
+ }
+ //come to a stop if fast or if no move key is pressed
+ if (player.speed > 4) {
+ const stoppingFriction = (this.crouch) ? 0.65 : 0.89; // this controls speed when crouched
+ Matter.Body.setVelocity(player, {
+ x: player.velocity.x * stoppingFriction,
+ y: player.velocity.y * stoppingFriction
+ });
+ }
+
+ } else { // in air **********************************
+ //check for short jumps
+ if (
+ this.buttonCD_jump + 60 > mech.cycle && //just pressed jump
+ !(keys[87] || keys[38]) && //but not pressing jump key
+ this.Vy < 0 //moving up
+ ) {
+ Matter.Body.setVelocity(player, {
+ //reduce player y-velocity every cycle
+ x: player.velocity.x,
+ y: player.velocity.y * 0.94
+ });
+ }
+ const limit = 125 / player.mass / player.mass
+ if (keys[65] || keys[37]) {
+ if (player.velocity.x > -limit) player.force.x -= this.FxAir; // move player left / a
+ } else if (keys[68] || keys[39]) {
+ if (player.velocity.x < limit) player.force.x += this.FxAir; //move player right / d
+ }
+ }
+
+ //smoothly move leg height towards height goal
+ this.yOff = this.yOff * 0.85 + this.yOffGoal * 0.15;
+ },
+ gamepadMove() {
+ if (this.onGround) { //on ground **********************
+ if (this.crouch) {
+ if (game.gamepad.leftAxis.y !== -1 && this.isHeadClear && this.hardLandCD < mech.cycle) this.undoCrouch();
+ } else if (game.gamepad.leftAxis.y === -1 || this.hardLandCD > mech.cycle) {
+ this.doCrouch(); //on ground && not crouched and pressing s or down
+ } else if (game.gamepad.jump && this.buttonCD_jump + 20 < mech.cycle && this.yOffWhen.stand > 23) {
+ this.buttonCD_jump = mech.cycle; //can't jump again until 20 cycles pass
+
+ //apply a fraction of the jump force to the body the player is jumping off of
+ Matter.Body.applyForce(mech.standingOn, mech.pos, {
+ x: 0,
+ y: this.jumpForce * 0.12 * Math.min(mech.standingOn.mass, 5)
+ });
+
+ player.force.y = -this.jumpForce; //player jump force
+ Matter.Body.setVelocity(player, { //zero player y-velocity for consistent jumps
+ x: player.velocity.x,
+ y: 0
+ });
+ }
+
+ //horizontal move on ground
+ //apply a force to move
+ if (game.gamepad.leftAxis.x === -1) { //left / a
+ player.force.x -= this.Fx
+ if (player.velocity.x > -2) player.force.x -= this.Fx * 0.5
+ } else if (game.gamepad.leftAxis.x === 1) { //right / d
+ player.force.x += this.Fx
+ if (player.velocity.x < 2) player.force.x += this.Fx * 0.5
+ } else {
+ const stoppingFriction = 0.92;
+ Matter.Body.setVelocity(player, {
+ x: player.velocity.x * stoppingFriction,
+ y: player.velocity.y * stoppingFriction
+ });
+ }
+ //come to a stop if fast or if no move key is pressed
+ if (player.speed > 4) {
+ const stoppingFriction = (this.crouch) ? 0.65 : 0.89;
+ Matter.Body.setVelocity(player, {
+ x: player.velocity.x * stoppingFriction,
+ y: player.velocity.y * stoppingFriction
+ });
+ }
+
+ } else { // in air **********************************
+ //check for short jumps
+ if (
+ this.buttonCD_jump + 60 > mech.cycle && //just pressed jump
+ !game.gamepad.jump && //but not pressing jump key
+ this.Vy < 0 //moving up
+ ) {
+ Matter.Body.setVelocity(player, {
+ //reduce player y-velocity every cycle
+ x: player.velocity.x,
+ y: player.velocity.y * 0.94
+ });
+ }
+ const limit = 125 / player.mass / player.mass
+ if (game.gamepad.leftAxis.x === -1) {
+ if (player.velocity.x > -limit) player.force.x -= this.FxAir; // move player left / a
+ } else if (game.gamepad.leftAxis.x === 1) {
+ if (player.velocity.x < limit) player.force.x += this.FxAir; //move player right / d
+ }
+ }
+
+ //smoothly move leg height towards height goal
+ this.yOff = this.yOff * 0.85 + this.yOffGoal * 0.15;
+ },
+ alive: true,
+ death() {
+ if (b.modIsImmortal) { //if player has the immortality buff, spawn on the same level with randomized stats
+ //remove mods
+ b.setModDefaults();
+ game.updateModHUD();
+ spawn.setSpawnList(); //new mob types
+ game.clearNow = true; //triggers a map reset
+
+ function randomizeField() {
+ if (game.levelsCleared > 5 && Math.random() < 0.9) {
+ mech.fieldUpgrades[Math.floor(Math.random() * (mech.fieldUpgrades.length))].effect();
+ } else {
+ mech.fieldUpgrades[0].effect();
+ }
+ }
+
+ function randomizeHealth() {
+ mech.health = 0.5 + 0.5 * Math.random()
+ mech.displayHealth();
+ }
+
+ function randomizeGuns() {
+ b.activeGun = null;
+ b.inventory = []; //removes guns and ammo
+ for (let i = 0, len = b.guns.length; i < len; ++i) {
+ b.guns[i].have = false;
+ if (b.guns[i].ammo !== Infinity) b.guns[i].ammo = 0;
+ }
+ if (game.levelsCleared > 0 && Math.random() < 0.95) powerUps.gun.effect();
+ if (game.levelsCleared > 1 && Math.random() < 0.89) powerUps.gun.effect();
+ if (game.levelsCleared > 3 && Math.random() < 0.6) powerUps.gun.effect();
+ if (game.levelsCleared > 5 && Math.random() < 0.5) powerUps.gun.effect();
+ if (game.levelsCleared > 7 && Math.random() < 0.4) powerUps.gun.effect();
+ //randomize ammo
+ for (let i = 0, len = b.inventory.length; i < len; i++) {
+ if (b.guns[b.inventory[i]].ammo !== Infinity) {
+ b.guns[b.inventory[i]].ammo = Math.max(0, Math.floor(2.2 * b.guns[b.inventory[i]].ammo * (Math.random() - 0.15)))
+ }
+ }
+ game.makeGunHUD(); //update gun HUD
+ }
+
+ game.wipe = function () { //set wipe to have trails
+ ctx.fillStyle = "rgba(255,255,255,0)";
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ }
+
+ randomizeGuns()
+ randomizeField()
+ randomizeHealth()
+ for (let i = 0; i < 7; i++) {
+ setTimeout(function () {
+ randomizeGuns()
+ randomizeField()
+ randomizeHealth()
+ game.makeTextLog(`probability amplitude will synchronize in ${7-i} seconds`, 1000);
+ game.wipe = function () { //set wipe to have trails
+ ctx.fillStyle = `rgba(255,255,255,${(i+1)*(i+1)*0.003})`;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ }
+ }, (i + 1) * 1000);
+ }
+
+ setTimeout(function () {
+ game.wipe = function () { //set wipe to normal
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ }
+ game.makeTextLog("your quantum probability has stabilized", 1000);
+ document.title = "n-gon: L" + (game.levelsCleared) + " " + level.levels[level.onLevel];
+ }, 8000);
+
+ } else if (this.alive) { //normal death code here
+ this.alive = false;
+ game.paused = true;
+ this.health = 0;
+ this.displayHealth();
+ document.getElementById("text-log").style.opacity = 0; //fade out any active text logs
+ document.getElementById("fade-out").style.opacity = 1; //slowly fades out
+ setTimeout(function () {
+ game.splashReturn();
+ }, 5000);
+ }
+ },
+ health: 0,
+ // regen() {
+ // if (this.health < 1 && mech.cycle % 15 === 0) {
+ // this.addHealth(0.01);
+ // }
+ // },
+ drawHealth() {
+ if (this.health < 1) {
+ ctx.fillStyle = "rgba(100, 100, 100, 0.5)";
+ ctx.fillRect(this.pos.x - this.radius, this.pos.y - 50, 60, 10);
+ ctx.fillStyle = "#f00";
+ ctx.fillRect(
+ this.pos.x - this.radius,
+ this.pos.y - 50,
+ 60 * this.health,
+ 10
+ );
+ }
+ },
+ displayHealth() {
+ id = document.getElementById("health");
+ id.style.width = Math.floor(300 * this.health) + "px";
+ //css animation blink if health is low
+ if (this.health < 0.3) {
+ id.classList.add("low-health");
+ } else {
+ id.classList.remove("low-health");
+ }
+ },
+ addHealth(heal) {
+ this.health += heal;
+ if (this.health > 1) this.health = 1;
+ this.displayHealth();
+ },
+ defaultFPSCycle: 0, //tracks when to return to normal fps
+ damage(dmg) {
+ this.health -= dmg;
+ if (this.health < 0) {
+ this.health = 0;
+ this.death();
+ return;
+ }
+ this.displayHealth();
+ document.getElementById("dmg").style.transition = "opacity 0s";
+ document.getElementById("dmg").style.opacity = 0.1 + Math.min(0.6, dmg * 4);
+
+ //drop block if holding
+ if (dmg > 0.07) {
+ this.drop();
+ }
+
+ // freeze game and display a full screen red color
+ if (dmg > 0.05) {
+ game.fpsCap = 4 //40 - Math.min(25, 100 * dmg)
+ game.fpsInterval = 1000 / game.fpsCap;
+ } else {
+ game.fpsCap = game.fpsCapDefault
+ game.fpsInterval = 1000 / game.fpsCap;
+ }
+ mech.defaultFPSCycle = mech.cycle
+
+ const normalFPS = function () {
+ if (mech.defaultFPSCycle < mech.cycle) { //back to default values
+ game.fpsCap = game.fpsCapDefault
+ game.fpsInterval = 1000 / game.fpsCap;
+ document.getElementById("dmg").style.transition = "opacity 1s";
+ document.getElementById("dmg").style.opacity = "0";
+ } else {
+ requestAnimationFrame(normalFPS);
+ }
+ };
+ requestAnimationFrame(normalFPS);
+ },
+ damageImmune: 0,
+ hitMob(i, dmg) {
+ //prevents damage happening too quick
+ },
+ buttonCD: 0, //cooldown for player buttons
+ usePowerUp(i) {
+ powerUp[i].effect();
+ Matter.World.remove(engine.world, powerUp[i]);
+ powerUp.splice(i, 1);
+ },
+ // *********************************************
+ // **************** holding ********************
+ // *********************************************
+ closest: {
+ dist: 1000,
+ index: 0
+ },
+ isHolding: false,
+ isStealth: false,
+ throwCharge: 0,
+ fireCDcycle: 0,
+ fieldCDcycle: 0,
+ fieldMode: 0, //basic field mode before upgrades
+ // these values are set on reset by setHoldDefaults()
+ fieldMeter: 0,
+ fieldRegen: 0,
+ fieldMode: 0,
+ fieldFire: false,
+ holdingMassScale: 0,
+ throwChargeRate: 0,
+ throwChargeMax: 0,
+ fieldFireCD: 0,
+ fieldDamage: 0,
+ fieldShieldingScale: 0,
+ grabRange: 0,
+ fieldArc: 0,
+ fieldThreshold: 0,
+ calculateFieldThreshold() {
+ this.fieldThreshold = Math.cos(this.fieldArc * Math.PI)
+ },
+ setHoldDefaults() {
+ this.fieldMeter = 1;
+ this.fieldRegen = 0.001;
+ this.fieldFire = false;
+ this.fieldCDcycle = 0;
+ this.isStealth = false;
+ player.collisionFilter.mask = 0x010011 //0x010011 is normal
+ this.holdingMassScale = 0.5;
+ this.throwChargeRate = 2;
+ this.throwChargeMax = 50;
+ this.fieldFireCD = 15;
+ this.fieldDamage = 0; // a value of 1.0 kills a small mob in 2-3 hits on level 1
+ this.fieldShieldingScale = 1; //scale energy loss after collision with mob
+ this.grabRange = 175;
+ this.fieldArc = 0.2;
+ this.calculateFieldThreshold();
+ this.jumpForce = 0.38;
+ this.Fx = 0.015; //run Force on ground
+ this.FxAir = 0.015; //run Force in Air
+ this.gravity = 0.0019;
+ mech.isBodiesAsleep = true;
+ mech.wakeCheck();
+ // this.phaseBlocks(0x011111)
+ },
+ drawFieldMeter(range = 60) {
+ if (this.fieldMeter < 1) {
+ mech.fieldMeter += mech.fieldRegen;
+ ctx.fillStyle = "rgba(0, 0, 0, 0.4)";
+ ctx.fillRect(this.pos.x - this.radius, this.pos.y - 50, range, 10);
+ ctx.fillStyle = "rgb(50,220,255)";
+ ctx.fillRect(this.pos.x - this.radius, this.pos.y - 50, range * this.fieldMeter, 10);
+ } else {
+ mech.fieldMeter = 1
+ }
+ },
+ lookingAt(who) {
+ //calculate a vector from body to player and make it length 1
+ const diff = Matter.Vector.normalise(Matter.Vector.sub(who.position, mech.pos));
+ //make a vector for the player's direction of length 1
+ const dir = {
+ x: Math.cos(mech.angle),
+ y: Math.sin(mech.angle)
+ };
+ //the dot product of diff and dir will return how much over lap between the vectors
+ // console.log(Matter.Vector.dot(dir, diff))
+ if (Matter.Vector.dot(dir, diff) > this.fieldThreshold) {
+ return true;
+ }
+ return false;
+ },
+ drop() {
+ if (this.isHolding) {
+ this.isHolding = false;
+ this.definePlayerMass()
+ this.holdingTarget.collisionFilter.category = 0x010000;
+ this.holdingTarget.collisionFilter.mask = 0x011111;
+ this.holdingTarget = null;
+ this.throwCharge = 0;
+ }
+ },
+ drawHold(target, stroke = true) {
+ const eye = 15;
+ const len = target.vertices.length - 1;
+ ctx.fillStyle = "rgba(110,170,200," + (0.2 + 0.4 * Math.random()) + ")";
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = "#000";
+ ctx.beginPath();
+ ctx.moveTo(
+ mech.pos.x + eye * Math.cos(this.angle),
+ mech.pos.y + eye * Math.sin(this.angle)
+ );
+ ctx.lineTo(target.vertices[len].x, target.vertices[len].y);
+ ctx.lineTo(target.vertices[0].x, target.vertices[0].y);
+ ctx.fill();
+ if (stroke) ctx.stroke();
+ for (let i = 0; i < len; i++) {
+ ctx.beginPath();
+ ctx.moveTo(
+ mech.pos.x + eye * Math.cos(this.angle),
+ mech.pos.y + eye * Math.sin(this.angle)
+ );
+ ctx.lineTo(target.vertices[i].x, target.vertices[i].y);
+ ctx.lineTo(target.vertices[i + 1].x, target.vertices[i + 1].y);
+ ctx.fill();
+ if (stroke) ctx.stroke();
+ }
+ },
+ holding() {
+ this.fieldMeter -= this.fieldRegen;
+ if (this.fieldMeter < 0) this.fieldMeter = 0;
+ Matter.Body.setPosition(this.holdingTarget, {
+ x: mech.pos.x + 70 * Math.cos(this.angle),
+ y: mech.pos.y + 70 * Math.sin(this.angle)
+ });
+ Matter.Body.setVelocity(this.holdingTarget, player.velocity);
+ Matter.Body.rotate(this.holdingTarget, 0.01 / this.holdingTarget.mass); //gently spin the block
+ },
+ throw () {
+ if ((keys[32] || game.mouseDownRight)) {
+ if (this.fieldMeter > 0.002) {
+ this.fieldMeter -= 0.002;
+ this.throwCharge += this.throwChargeRate;;
+ //draw charge
+ const x = mech.pos.x + 15 * Math.cos(this.angle);
+ const y = mech.pos.y + 15 * Math.sin(this.angle);
+ const len = this.holdingTarget.vertices.length - 1;
+ const edge = this.throwCharge * this.throwCharge * 0.02;
+ const grd = ctx.createRadialGradient(x, y, edge, x, y, edge + 5);
+ grd.addColorStop(0, "rgba(255,50,150,0.3)");
+ grd.addColorStop(1, "transparent");
+ ctx.fillStyle = grd;
+ ctx.beginPath();
+ ctx.moveTo(x, y);
+ ctx.lineTo(this.holdingTarget.vertices[len].x, this.holdingTarget.vertices[len].y);
+ ctx.lineTo(this.holdingTarget.vertices[0].x, this.holdingTarget.vertices[0].y);
+ ctx.fill();
+ for (let i = 0; i < len; i++) {
+ ctx.beginPath();
+ ctx.moveTo(x, y);
+ ctx.lineTo(this.holdingTarget.vertices[i].x, this.holdingTarget.vertices[i].y);
+ ctx.lineTo(this.holdingTarget.vertices[i + 1].x, this.holdingTarget.vertices[i + 1].y);
+ ctx.fill();
+ }
+ } else {
+ this.drop()
+ }
+ } else if (this.throwCharge > 0) {
+ //throw the body
+ this.fireCDcycle = mech.cycle + this.fieldFireCD;
+ this.isHolding = false;
+ //bullet-like collisions
+ this.holdingTarget.collisionFilter.category = 0x000100;
+ this.holdingTarget.collisionFilter.mask = 0x110111;
+ //check every second to see if player is away from thrown body, and make solid
+ const solid = function (that) {
+ const dx = that.position.x - player.position.x;
+ const dy = that.position.y - player.position.y;
+ if (dx * dx + dy * dy > 10000 && that.speed < 3 && that !== mech.holdingTarget) {
+ that.collisionFilter.category = 0x010000; //make solid
+ that.collisionFilter.mask = 0x011111;
+ } else {
+ setTimeout(solid, 50, that);
+ }
+ };
+ setTimeout(solid, 400, this.holdingTarget);
+ //throw speed scales a bit with mass
+ const speed = Math.min(85, Math.min(54 / this.holdingTarget.mass + 5, 48) * Math.min(this.throwCharge, this.throwChargeMax) / 50);
+
+ this.throwCharge = 0;
+ Matter.Body.setVelocity(this.holdingTarget, {
+ x: player.velocity.x + Math.cos(this.angle) * speed,
+ y: player.velocity.y + Math.sin(this.angle) * speed
+ });
+ //player recoil //stronger in x-dir to prevent jump hacking
+ Matter.Body.setVelocity(player, {
+ x: player.velocity.x - Math.cos(this.angle) * speed / 20 * Math.sqrt(this.holdingTarget.mass),
+ y: player.velocity.y - Math.sin(this.angle) * speed / 80 * Math.sqrt(this.holdingTarget.mass)
+ });
+ this.definePlayerMass() //return to normal player mass
+ }
+ },
+ drawField() {
+ //draw field
+ const range = this.grabRange - 20;
+ ctx.beginPath();
+ ctx.arc(this.pos.x, this.pos.y, range, this.angle - Math.PI * this.fieldArc, this.angle + Math.PI * this.fieldArc, false);
+ let eye = 13;
+ ctx.lineTo(mech.pos.x + eye * Math.cos(this.angle), mech.pos.y + eye * Math.sin(this.angle));
+ if (this.holdingTarget) {
+ ctx.fillStyle = "rgba(110,170,200," + (0.05 + 0.1 * Math.random()) + ")";
+ } else {
+ ctx.fillStyle = "rgba(110,170,200," + (0.15 + 0.15 * Math.random()) + ")";
+ }
+ ctx.fill();
+ //draw random lines in field for cool effect
+ let offAngle = this.angle + 2 * Math.PI * this.fieldArc * (Math.random() - 0.5);
+ ctx.beginPath();
+ eye = 15;
+ ctx.moveTo(mech.pos.x + eye * Math.cos(this.angle), mech.pos.y + eye * Math.sin(this.angle));
+ ctx.lineTo(this.pos.x + range * Math.cos(offAngle), this.pos.y + range * Math.sin(offAngle));
+ ctx.strokeStyle = "rgba(120,170,255,0.4)";
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ },
+ grabPowerUp() {
+ //look for power ups to grab
+ if (mech.fieldCDcycle < mech.cycle) {
+ const grabPowerUpRange2 = (this.grabRange + 220) * (this.grabRange + 220)
+ for (let i = 0, len = powerUp.length; i < len; ++i) {
+ const dxP = mech.pos.x - powerUp[i].position.x;
+ const dyP = mech.pos.y - powerUp[i].position.y;
+ const dist2 = dxP * dxP + dyP * dyP;
+ // float towards player if looking at and in range or if very close to player
+ if (dist2 < grabPowerUpRange2 && this.lookingAt(powerUp[i]) || dist2 < 16000) {
+ if (dist2 < 5000) { //use power up if it is close enough
+ Matter.Body.setVelocity(player, { //player knock back, after grabbing power up
+ x: player.velocity.x + ((powerUp[i].velocity.x * powerUp[i].mass) / player.mass) * 0.3,
+ y: player.velocity.y + ((powerUp[i].velocity.y * powerUp[i].mass) / player.mass) * 0.3
+ });
+ mech.usePowerUp(i);
+ return;
+ }
+ this.fieldMeter -= this.fieldRegen * 0.5;
+ powerUp[i].force.x += 7 * (dxP / dist2) * powerUp[i].mass;
+ powerUp[i].force.y += 7 * (dyP / dist2) * powerUp[i].mass - powerUp[i].mass * game.g; //negate gravity
+ //extra friction
+ Matter.Body.setVelocity(powerUp[i], {
+ x: powerUp[i].velocity.x * 0.11,
+ y: powerUp[i].velocity.y * 0.11
+ });
+ }
+ }
+ }
+ },
+ pushMobs() {
+ // push all mobs in range
+ for (let i = 0, len = mob.length; i < len; ++i) {
+ if (this.lookingAt(mob[i]) && Matter.Vector.magnitude(Matter.Vector.sub(mob[i].position, this.pos)) < this.grabRange && Matter.Query.ray(map, mob[i].position, this.pos).length === 0) {
+ const fieldBlockCost = Math.max(0.02, mob[i].mass * 0.012) //0.012
+ if (this.fieldMeter > fieldBlockCost) {
+ this.fieldMeter -= fieldBlockCost * this.fieldShieldingScale;
+ if (this.fieldMeter < 0) this.fieldMeter = 0;
+ if (this.fieldDamage) mob[i].damage(b.dmgScale * this.fieldDamage);
+ mob[i].locatePlayer();
+ this.drawHold(mob[i]);
+ //mob and player knock back
+ const angle = Math.atan2(player.position.y - mob[i].position.y, player.position.x - mob[i].position.x);
+ const mass = Math.min(Math.sqrt(mob[i].mass), 4);
+ Matter.Body.setVelocity(mob[i], {
+ x: player.velocity.x - (15 * Math.cos(angle)) / mass,
+ y: player.velocity.y - (15 * Math.sin(angle)) / mass
+ });
+ Matter.Body.setVelocity(player, {
+ x: player.velocity.x + 5 * Math.cos(angle) * mass,
+ y: player.velocity.y + 5 * Math.sin(angle) * mass
+ });
+ }
+ }
+ }
+ },
+ pushMobs360(range = this.grabRange * 0.75) {
+ // push all mobs in range
+ for (let i = 0, len = mob.length; i < len; ++i) {
+ if (Matter.Vector.magnitude(Matter.Vector.sub(mob[i].position, this.pos)) < range && Matter.Query.ray(map, mob[i].position, this.pos).length === 0) {
+ const fieldBlockCost = Math.max(0.02, mob[i].mass * 0.012)
+ if (this.fieldMeter > fieldBlockCost) {
+ this.fieldMeter -= fieldBlockCost * this.fieldShieldingScale;
+ if (this.fieldMeter < 0) this.fieldMeter = 0
+
+ if (this.fieldDamage) mob[i].damage(b.dmgScale * this.fieldDamage);
+ mob[i].locatePlayer();
+ this.drawHold(mob[i]);
+ //mob and player knock back
+ const angle = Math.atan2(player.position.y - mob[i].position.y, player.position.x - mob[i].position.x);
+ const mass = Math.min(Math.sqrt(mob[i].mass), 4);
+ // console.log(mob[i].mass, Math.sqrt(mob[i].mass), mass)
+ Matter.Body.setVelocity(mob[i], {
+ x: player.velocity.x - (15 * Math.cos(angle)) / mass,
+ y: player.velocity.y - (15 * Math.sin(angle)) / mass
+ });
+ Matter.Body.setVelocity(player, {
+ x: player.velocity.x + 5 * Math.cos(angle) * mass,
+ y: player.velocity.y + 5 * Math.sin(angle) * mass
+ });
+ }
+ }
+ }
+ },
+ lookForPickUp(range = this.grabRange) { //find body to pickup
+ this.fieldMeter -= this.fieldRegen;
+ const grabbing = {
+ targetIndex: null,
+ targetRange: range,
+ // lookingAt: false //false to pick up object in range, but not looking at
+ };
+ for (let i = 0, len = body.length; i < len; ++i) {
+ if (Matter.Query.ray(map, body[i].position, this.pos).length === 0) {
+ //is this next body a better target then my current best
+ const dist = Matter.Vector.magnitude(Matter.Vector.sub(body[i].position, this.pos));
+ const looking = this.lookingAt(body[i]);
+ // if (dist < grabbing.targetRange && (looking || !grabbing.lookingAt) && !body[i].isNotHoldable) {
+ if (dist < grabbing.targetRange && looking && !body[i].isNotHoldable) {
+ grabbing.targetRange = dist;
+ grabbing.targetIndex = i;
+ // grabbing.lookingAt = looking;
+ }
+ }
+ }
+ // set pick up target for when mouse is released
+ if (body[grabbing.targetIndex]) {
+ this.holdingTarget = body[grabbing.targetIndex];
+ //
+ ctx.beginPath(); //draw on each valid body
+ let vertices = this.holdingTarget.vertices;
+ ctx.moveTo(vertices[0].x, vertices[0].y);
+ for (let j = 1; j < vertices.length; j += 1) {
+ ctx.lineTo(vertices[j].x, vertices[j].y);
+ }
+ ctx.lineTo(vertices[0].x, vertices[0].y);
+ ctx.fillStyle = "rgba(190,215,230," + (0.3 + 0.7 * Math.random()) + ")";
+ ctx.fill();
+
+ ctx.globalAlpha = 0.2;
+ this.drawHold(this.holdingTarget);
+ ctx.globalAlpha = 1;
+ } else {
+ this.holdingTarget = null;
+ }
+ },
+ pickUp() {
+ //triggers when a hold target exits and field button is released
+ this.isHolding = true;
+ this.definePlayerMass(5 + this.holdingTarget.mass * this.holdingMassScale)
+ //collide with nothing
+ this.holdingTarget.collisionFilter.category = 0x000000;
+ this.holdingTarget.collisionFilter.mask = 0x000000;
+ // if (this.holdingTarget) {
+ // this.holdingTarget.collisionFilter.category = 0x010000;
+ // this.holdingTarget.collisionFilter.mask = 0x011111;
+ // }
+ // combine momentum // this doesn't feel right in game
+ // const px = player.velocity.x * player.mass + this.holdingTarget.velocity.x * this.holdingTarget.mass;
+ // const py = player.velocity.y * player.mass + this.holdingTarget.velocity.y * this.holdingTarget.mass;
+ // Matter.Body.setVelocity(player, {
+ // x: px / (player.mass + this.holdingTarget.mass),
+ // y: py / (player.mass + this.holdingTarget.mass)
+ // });
+ },
+ wakeCheck() {
+ if (mech.isBodiesAsleep) {
+ mech.isBodiesAsleep = false;
+
+ function wake(who) {
+ for (let i = 0, len = who.length; i < len; ++i) {
+ Matter.Sleeping.set(who[i], false)
+ if (who[i].storeVelocity) {
+ Matter.Body.setVelocity(who[i], {
+ x: who[i].storeVelocity.x,
+ y: who[i].storeVelocity.y
+ })
+ Matter.Body.setAngularVelocity(who[i], who[i].storeAngularVelocity)
+ }
+ }
+ }
+ wake(mob);
+ wake(body);
+ wake(bullet);
+ for (let i = 0, len = cons.length; i < len; i++) {
+ if (cons[i].stiffness === 0) {
+ cons[i].stiffness = cons[i].storeStiffness
+ }
+ }
+ // wake(powerUp);
+ }
+ },
+ hold() {},
+ fieldText() {
+ game.makeTextLog(`${mech.fieldUpgrades[mech.fieldMode].name} (right click or space bar)
${mech.fieldUpgrades[mech.fieldMode].description}
`, 1200);
+ document.getElementById("field").innerHTML = mech.fieldUpgrades[mech.fieldMode].name //add field
+ },
+ fieldUpgrades: [{
+ name: "Field Emitter",
+ description: "lets you pick up and throw objects shields you from damage",
+ effect: () => {
+ mech.fieldMode = 0;
+ mech.fieldText();
+ // game.makeTextLog(" (right click or space bar)", 1200);
+ mech.setHoldDefaults();
+ mech.hold = function () {
+ if (mech.isHolding) {
+ mech.drawHold(mech.holdingTarget);
+ mech.holding();
+ mech.throw();
+ } else if ((keys[32] || game.mouseDownRight && mech.fieldMeter > 0.1)) { //not hold but field button is pressed
+ mech.drawField();
+ mech.grabPowerUp();
+ mech.pushMobs();
+ mech.lookForPickUp();
+ } else if (mech.holdingTarget && mech.fireCDcycle < mech.cycle && mech.fieldMeter > 0.05) { //holding, but field button is released
+ mech.pickUp();
+ } else {
+ mech.holdingTarget = null; //clears holding target (this is so you only pick up right after the field button is released and a hold target exists)
+ }
+ mech.drawFieldMeter()
+ }
+ }
+ },
+ {
+ name: "Time Dilation Field",
+ description: "stop time while field is active can fire while field is active",
+ effect: () => {
+ mech.fieldMode = 1;
+ mech.fieldText();
+ mech.setHoldDefaults();
+ mech.fieldFire = true;
+ mech.grabRange = 130
+ mech.isBodiesAsleep = false;
+ mech.hold = function () {
+ if (mech.isHolding) {
+ mech.wakeCheck();
+ mech.drawHold(mech.holdingTarget);
+ mech.holding();
+ mech.throw();
+ } else if ((keys[32] || game.mouseDownRight) && mech.fieldCDcycle < mech.cycle) {
+ const DRAIN = 0.0027
+ if (mech.fieldMeter > DRAIN) {
+ mech.fieldMeter -= DRAIN;
+
+ //draw field everywhere
+ ctx.fillStyle = "rgba(110,170,200," + (0.19 + 0.16 * Math.random()) + ")";
+ ctx.fillRect(-100000, -100000, 200000, 200000)
+
+ //stop time
+ mech.isBodiesAsleep = true;
+
+ function sleep(who) {
+ for (let i = 0, len = who.length; i < len; ++i) {
+ if (!who[i].isSleeping) {
+ who[i].storeVelocity = who[i].velocity
+ who[i].storeAngularVelocity = who[i].angularVelocity
+ }
+ Matter.Sleeping.set(who[i], true)
+ }
+ }
+ sleep(mob);
+ sleep(body);
+ sleep(bullet);
+ //doesn't really work, just slows down constraints
+ for (let i = 0, len = cons.length; i < len; i++) {
+ if (cons[i].stiffness !== 0) {
+ cons[i].storeStiffness = cons[i].stiffness;
+ cons[i].stiffness = 0;
+ }
+ }
+ game.cycle--; //pause all functions that depend on game cycle increasing
+
+ mech.grabPowerUp();
+ mech.lookForPickUp(180);
+ } else {
+ mech.wakeCheck();
+ mech.fieldCDcycle = mech.cycle + 120;
+ }
+ } else if (mech.holdingTarget && mech.fireCDcycle < mech.cycle && mech.fieldMeter > 0.05) { //holding, but field button is released
+ mech.wakeCheck();
+ mech.pickUp();
+ } else {
+ mech.wakeCheck();
+ mech.holdingTarget = null; //clears holding target (this is so you only pick up right after the field button is released and a hold target exists)
+ }
+ mech.drawFieldMeter()
+ if (mech.fieldMode !== 1) {
+ //wake up if this is no longer the current field mode, like after a new power up
+ mech.wakeCheck();
+
+ }
+ }
+ }
+ },
+ {
+ name: "Electrostatic Field",
+ description: "field does damage on contact blocks are thrown at a higher velocity increased field regeneration",
+ effect: () => {
+ mech.fieldMode = 2;
+ mech.fieldText();
+ mech.setHoldDefaults();
+ //throw quicker and harder
+ mech.grabRange = 225;
+ mech.fieldShieldingScale = 2;
+ mech.fieldRegen *= 2;
+ mech.throwChargeRate = 3;
+ mech.throwChargeMax = 140;
+ mech.fieldDamage = 4; //passive field does extra damage
+ // mech.fieldArc = 0.11
+ // mech.calculateFieldThreshold(); //run after setting fieldArc, used for powerUp grab and mobPush with lookingAt(mob)
+
+ mech.hold = function () {
+ if (mech.isHolding) {
+ mech.drawHold(mech.holdingTarget);
+ mech.holding();
+ mech.throw();
+ } else if ((keys[32] || game.mouseDownRight) && mech.fieldMeter > 0.15) { //not hold but field button is pressed
+ //draw electricity
+ const Dx = Math.cos(mech.angle);
+ const Dy = Math.sin(mech.angle);
+ let x = mech.pos.x + 20 * Dx;
+ let y = mech.pos.y + 20 * Dy;
+ ctx.beginPath();
+ ctx.moveTo(x, y);
+ for (let i = 0; i < 8; i++) {
+ x += 18 * (Dx + 2 * (Math.random() - 0.5))
+ y += 18 * (Dy + 2 * (Math.random() - 0.5))
+ ctx.lineTo(x, y);
+ }
+ ctx.lineWidth = 1 //0.5 + 2 * Math.random();
+ ctx.strokeStyle = `rgba(100,20,50,${0.5+0.5*Math.random()})`;
+ ctx.stroke();
+
+ //draw field
+ const range = 170;
+ const arc = Math.PI * 0.11
+ ctx.beginPath();
+ ctx.arc(mech.pos.x, mech.pos.y, range, mech.angle - arc, mech.angle + arc, false);
+ ctx.lineTo(mech.pos.x + 13 * Math.cos(mech.angle), mech.pos.y + 13 * Math.sin(mech.angle));
+ if (mech.holdingTarget) {
+ ctx.fillStyle = "rgba(255,50,150," + (0.05 + 0.1 * Math.random()) + ")";
+ } else {
+ ctx.fillStyle = "rgba(255,50,150," + (0.13 + 0.18 * Math.random()) + ")";
+ }
+ ctx.fill();
+
+ //draw random lines in field for cool effect
+ // eye = 15;
+ // ctx.beginPath();
+ // ctx.moveTo(mech.pos.x + eye * Math.cos(mech.angle), mech.pos.y + eye * Math.sin(mech.angle));
+ // const offAngle = mech.angle + 2 * Math.PI * mech.fieldArc * (Math.random() - 0.5);
+ // ctx.lineTo(mech.pos.x + range * Math.cos(offAngle), mech.pos.y + range * Math.sin(offAngle));
+ // ctx.strokeStyle = "rgba(100,20,50,0.2)";
+ // ctx.stroke();
+
+ mech.grabPowerUp();
+ mech.pushMobs();
+ mech.lookForPickUp();
+ } else if (mech.holdingTarget && mech.fireCDcycle < mech.cycle && mech.fieldMeter > 0.05) { //holding, but field button is released
+ mech.pickUp();
+ } else {
+ mech.holdingTarget = null; //clears holding target (this is so you only pick up right after the field button is released and a hold target exists)
+ }
+ mech.drawFieldMeter()
+ }
+ }
+ },
+ {
+ name: "Negative Mass Field",
+ description: "field nullifies gravity player can hold more massive objects can fire while field is active",
+ effect: () => {
+ mech.fieldMode = 3;
+ mech.fieldText();
+ mech.setHoldDefaults();
+ mech.fieldFire = true;
+ mech.holdingMassScale = 0.05; //can hold heavier blocks with lower cost to jumping
+
+ mech.hold = function () {
+ if (mech.isHolding) {
+ mech.drawHold(mech.holdingTarget);
+ mech.holding();
+ mech.throw();
+ } else if ((keys[32] || game.mouseDownRight) && mech.fieldCDcycle < mech.cycle) { //push away
+ const DRAIN = 0.0004
+ if (mech.fieldMeter > DRAIN) {
+ mech.pushMobs360(170);
+ mech.grabPowerUp();
+ mech.lookForPickUp(170);
+ //look for nearby objects to make zero-g
+ function zeroG(who, mag = 1.06) {
+ for (let i = 0, len = who.length; i < len; ++i) {
+ sub = Matter.Vector.sub(who[i].position, mech.pos);
+ dist = Matter.Vector.magnitude(sub);
+ if (dist < mech.grabRange) {
+ who[i].force.y -= who[i].mass * (game.g * mag); //add a bit more then standard gravity
+ }
+ }
+ }
+ // zeroG(bullet); //works fine, but not that noticeable and maybe not worth the possible performance hit
+ // zeroG(mob); //mobs are too irregular to make this work?
+
+ Matter.Body.setVelocity(player, {
+ x: player.velocity.x,
+ y: player.velocity.y * 0.97
+ });
+
+ if (keys[83] || keys[40]) { //down
+ player.force.y -= 0.8 * player.mass * mech.gravity;
+ mech.grabRange = mech.grabRange * 0.97 + 400 * 0.03;
+ zeroG(powerUp, 0.85);
+ zeroG(body, 0.85);
+ } else if (keys[87] || keys[38]) { //up
+ mech.fieldMeter -= 5 * DRAIN;
+ mech.grabRange = mech.grabRange * 0.97 + 750 * 0.03;
+ player.force.y -= 1.2 * player.mass * mech.gravity;
+ zeroG(powerUp, 1.13);
+ zeroG(body, 1.13);
+ } else {
+ mech.fieldMeter -= DRAIN;
+ mech.grabRange = mech.grabRange * 0.97 + 650 * 0.03;
+ player.force.y -= 1.07 * player.mass * mech.gravity; // slow upward drift
+ zeroG(powerUp);
+ zeroG(body);
+ }
+
+ //add extra friction for horizontal motion
+ if (keys[65] || keys[68] || keys[37] || keys[39]) {
+ Matter.Body.setVelocity(player, {
+ x: player.velocity.x * 0.85,
+ y: player.velocity.y
+ });
+ }
+
+ //draw zero-G range
+ ctx.beginPath();
+ ctx.arc(mech.pos.x, mech.pos.y, mech.grabRange, 0, 2 * Math.PI);
+ ctx.fillStyle = "#f5f5ff";
+ ctx.globalCompositeOperation = "difference";
+ ctx.fill();
+ ctx.globalCompositeOperation = "source-over";
+ } else {
+ //trigger cool down
+ mech.fieldCDcycle = mech.cycle + 120;
+ }
+ } else if (mech.holdingTarget && mech.fireCDcycle < mech.cycle && mech.fieldMeter > 0.05) { //holding, but field button is released
+ mech.pickUp();
+ mech.grabRange = 0
+ } else {
+ mech.holdingTarget = null; //clears holding target (this is so you only pick up right after the field button is released and a hold target exists)
+ mech.grabRange = 0
+ }
+ mech.drawFieldMeter()
+ }
+ }
+ },
+ {
+ name: "Standing Wave Harmonics",
+ description: "oscillating shields always surround player decreased field regeneration",
+ effect: () => {
+ mech.fieldMode = 4;
+ mech.fieldText();
+ mech.setHoldDefaults();
+ mech.fieldRegen *= 0.25;
+
+ mech.hold = function () {
+ if (mech.isHolding) {
+ mech.drawHold(mech.holdingTarget);
+ mech.holding();
+ mech.throw();
+ } else if ((keys[32] || game.mouseDownRight && mech.fieldMeter > 0.1)) { //not hold but field button is pressed
+ mech.grabPowerUp();
+ mech.lookForPickUp(180);
+ } else if (mech.holdingTarget && mech.fireCDcycle < mech.cycle) { //holding, but field button is released
+ mech.pickUp();
+ } else {
+ mech.holdingTarget = null; //clears holding target (this is so you only pick up right after the field button is released and a hold target exists)
+ }
+ if (mech.fieldMeter > 0.1) {
+ const grabRange1 = 90 + 60 * Math.sin(mech.cycle / 23)
+ const grabRange2 = 85 + 70 * Math.sin(mech.cycle / 37)
+ const grabRange3 = 80 + 80 * Math.sin(mech.cycle / 47)
+ const netGrabRange = Math.max(grabRange1, grabRange2, grabRange3)
+ ctx.fillStyle = "rgba(110,170,200," + (0.15 + 0.15 * Math.random()) + ")";
+ ctx.beginPath();
+ ctx.arc(mech.pos.x, mech.pos.y, grabRange1, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.beginPath();
+ ctx.arc(mech.pos.x, mech.pos.y, grabRange2, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.beginPath();
+ ctx.arc(mech.pos.x, mech.pos.y, grabRange3, 0, 2 * Math.PI);
+ ctx.fill();
+ mech.pushMobs360(netGrabRange);
+ }
+ mech.drawFieldMeter()
+ }
+ }
+ },
+ {
+ name: "Nano-Scale Manufacturing",
+ description: "excess field energy used to build drones increased field regeneration",
+ effect: () => {
+ mech.fieldMode = 5;
+ mech.fieldText();
+ mech.setHoldDefaults();
+ mech.fieldRegen *= 3.5;
+ mech.hold = function () {
+ if (mech.fieldMeter === 1) {
+ mech.fieldMeter -= 0.5;
+ b.guns[12].fire() //spawn drone
+ }
+ if (mech.isHolding) {
+ mech.drawHold(mech.holdingTarget);
+ mech.holding();
+ mech.throw();
+ } else if ((keys[32] || game.mouseDownRight && mech.fieldMeter > 0.1)) { //not hold but field button is pressed
+ mech.pushMobs();
+ mech.drawField();
+ mech.grabPowerUp();
+ mech.lookForPickUp();
+ } else if (mech.holdingTarget && mech.fireCDcycle < mech.cycle && mech.fieldMeter > 0.05) { //holding, but field button is released
+ mech.pickUp();
+ } else {
+ mech.holdingTarget = null; //clears holding target (this is so you only pick up right after the field button is released and a hold target exists)
+ }
+ mech.drawFieldMeter()
+ }
+ }
+ },
+ {
+ name: "Phase Decoherence Field",
+ description: "intangible while field is active can't see or be seen outside field",
+ effect: () => {
+ mech.fieldMode = 6;
+ mech.fieldText();
+ mech.setHoldDefaults();
+ // mech.grabRange = 230
+ mech.hold = function () {
+ mech.isStealth = false //isStealth is checked in mob foundPlayer()
+ player.collisionFilter.mask = 0x010011 //0x010011 is normal
+ if (mech.isHolding) {
+ mech.drawHold(mech.holdingTarget);
+ mech.holding();
+ mech.throw();
+ } else if ((keys[32] || game.mouseDownRight) && mech.fieldCDcycle < mech.cycle) {
+ const DRAIN = 0.0015
+ if (mech.fieldMeter > DRAIN) {
+ mech.fieldMeter -= DRAIN;
+
+ mech.isStealth = true //isStealth is checked in mob foundPlayer()
+ player.collisionFilter.mask = 0x000001 //0x010011 is normals
+
+ ctx.beginPath();
+ ctx.arc(mech.pos.x, mech.pos.y, mech.grabRange, 0, 2 * Math.PI);
+ ctx.globalCompositeOperation = "destination-in"; //in or atop
+ ctx.fillStyle = "rgba(255,255,255,0.25)";
+ ctx.fill();
+ ctx.globalCompositeOperation = "source-over";
+ ctx.strokeStyle = "#000"
+ ctx.lineWidth = 2;
+ ctx.stroke();
+
+ mech.grabPowerUp();
+ mech.lookForPickUp(110);
+ } else {
+ mech.fieldCDcycle = mech.cycle + 120;
+ }
+ } else if (mech.holdingTarget && mech.fireCDcycle < mech.cycle && mech.fieldMeter > 0.05) { //holding, but field button is released
+ mech.pickUp();
+ } else {
+ mech.holdingTarget = null; //clears holding target (this is so you only pick up right after the field button is released and a hold target exists)
+ }
+ mech.drawFieldMeter()
+ }
+ }
+ },
+ // () => {
+ // mech.fieldMode = 7;
+ // game.makeTextLog("Thermal Radiation Field (right click or space bar)
field grows while active damages all targets within range, including player decreased field shielding efficiency
", 1200);
+ // mech.setHoldDefaults();
+ // mech.fieldShieldingScale = 10;
+ // mech.rangeSmoothing = 0
+ // mech.hold = function () {
+ // if (mech.isHolding) {
+ // mech.drawHold(mech.holdingTarget);
+ // mech.holding();
+ // mech.throw();
+ // } else if ((keys[32] || game.mouseDownRight && mech.fieldCDcycle < mech.cycle)) { //not hold but field button is pressed
+ // mech.grabPowerUp();
+ // mech.lookForPickUp(Math.max(180, mech.grabRange));
+ // mech.pushMobs360(140);
+
+ // if (mech.health > 0.1) {
+ // const DRAIN = 0.0008
+ // if (mech.fieldMeter > DRAIN) {
+ // mech.fieldMeter -= DRAIN;
+ // mech.damage(0.00005 + 0.00000012 * mech.grabRange)
+ // //draw damage field
+ // mech.grabRange = mech.grabRange * 0.997 + (1350 + 150 * Math.cos(mech.cycle / 30)) * 0.003
+ // let gradient = ctx.createRadialGradient(this.pos.x, this.pos.y, 0, this.pos.x, this.pos.y, mech.grabRange);
+ // gradient.addColorStop(0, 'rgba(255,255,255,0.7)');
+ // gradient.addColorStop(1, 'rgba(255,0,50,' + (0.6 + 0.2 * Math.random()) + ')');
+
+ // const angleOff = 2 * Math.PI * Math.random()
+ // ctx.beginPath();
+ // ctx.arc(this.pos.x, this.pos.y, mech.grabRange + Math.sqrt(mech.grabRange) * 0.7 * (Math.random() - 0.5), angleOff, 1.8 * Math.PI + angleOff);
+ // ctx.fillStyle = gradient //rgba(255,0,0,0.2)
+ // ctx.fill();
+
+ // //damage and push away mobs in range
+ // for (let i = 0, len = mob.length; i < len; ++i) {
+ // if (mob[i].alive) {
+ // sub = Matter.Vector.sub(this.pos, mob[i].position);
+ // dist = Matter.Vector.magnitude(sub);
+ // if (dist < mech.grabRange) {
+ // mob[i].damage(0.01);
+ // mob[i].locatePlayer();
+ // mob[i].force = Matter.Vector.mult(Matter.Vector.normalise(sub), -0.0001 * mob[i].mass) //gently push mobs back
+ // }
+ // }
+ // }
+ // } else {
+ // mech.fieldCDcycle = mech.cycle + 120;
+ // }
+ // } else {
+ // mech.grabRange = 180;
+ // mech.drawField();
+ // mech.grabPowerUp();
+ // mech.lookForPickUp();
+ // }
+ // } else if (mech.holdingTarget && mech.fireCDcycle < mech.cycle && mech.fieldMeter > 0.05) { //holding, but field button is released
+ // mech.grabRange = 0
+ // mech.pickUp();
+ // } else {
+ // mech.grabRange = 0
+ // mech.holdingTarget = null; //clears holding target (this is so you only pick up right after the field button is released and a hold target exists)
+ // }
+ // mech.drawFieldMeter()
+ // }
+ // },
+ ],
+ drawLeg(stroke) {
+ // if (game.mouseInGame.x > this.pos.x) {
+ if (mech.angle > -Math.PI / 2 && mech.angle < Math.PI / 2) {
+ this.flipLegs = 1;
+ } else {
+ this.flipLegs = -1;
+ }
+ ctx.save();
+ ctx.scale(this.flipLegs, 1); //leg lines
+ ctx.beginPath();
+ ctx.moveTo(this.hip.x, this.hip.y);
+ ctx.lineTo(this.knee.x, this.knee.y);
+ ctx.lineTo(this.foot.x, this.foot.y);
+ ctx.strokeStyle = stroke;
+ ctx.lineWidth = 7;
+ ctx.stroke();
+
+ //toe lines
+ ctx.beginPath();
+ ctx.moveTo(this.foot.x, this.foot.y);
+ ctx.lineTo(this.foot.x - 15, this.foot.y + 5);
+ ctx.moveTo(this.foot.x, this.foot.y);
+ ctx.lineTo(this.foot.x + 15, this.foot.y + 5);
+ ctx.lineWidth = 4;
+ ctx.stroke();
+
+ //hip joint
+ ctx.beginPath();
+ ctx.arc(this.hip.x, this.hip.y, 11, 0, 2 * Math.PI);
+ //knee joint
+ ctx.moveTo(this.knee.x + 7, this.knee.y);
+ ctx.arc(this.knee.x, this.knee.y, 7, 0, 2 * Math.PI);
+ //foot joint
+ ctx.moveTo(this.foot.x + 6, this.foot.y);
+ ctx.arc(this.foot.x, this.foot.y, 6, 0, 2 * Math.PI);
+ ctx.fillStyle = this.fillColor;
+ ctx.fill();
+ ctx.lineWidth = 2;
+ ctx.stroke();
+ ctx.restore();
+ },
+ calcLeg(cycle_offset, offset) {
+ this.hip.x = 12 + offset;
+ this.hip.y = 24 + offset;
+ //stepSize goes to zero if Vx is zero or not on ground (make this transition cleaner)
+ this.stepSize = 0.8 * this.stepSize + 0.2 * (7 * Math.sqrt(Math.min(9, Math.abs(this.Vx))) * this.onGround);
+ //changes to stepsize are smoothed by adding only a percent of the new value each cycle
+ const stepAngle = 0.034 * this.walk_cycle + cycle_offset;
+ this.foot.x = 2.2 * this.stepSize * Math.cos(stepAngle) + offset;
+ this.foot.y = offset + 1.2 * this.stepSize * Math.sin(stepAngle) + this.yOff + this.height;
+ const Ymax = this.yOff + this.height;
+ if (this.foot.y > Ymax) this.foot.y = Ymax;
+
+ //calculate knee position as intersection of circle from hip and foot
+ const d = Math.sqrt((this.hip.x - this.foot.x) * (this.hip.x - this.foot.x) + (this.hip.y - this.foot.y) * (this.hip.y - this.foot.y));
+ const l = (this.legLength1 * this.legLength1 - this.legLength2 * this.legLength2 + d * d) / (2 * d);
+ const h = Math.sqrt(this.legLength1 * this.legLength1 - l * l);
+ this.knee.x = (l / d) * (this.foot.x - this.hip.x) - (h / d) * (this.foot.y - this.hip.y) + this.hip.x + offset;
+ this.knee.y = (l / d) * (this.foot.y - this.hip.y) + (h / d) * (this.foot.x - this.hip.x) + this.hip.y;
+ },
+ draw() {
+ ctx.fillStyle = this.fillColor;
+ this.walk_cycle += this.flipLegs * this.Vx;
+
+ //draw body
+ ctx.save();
+ ctx.translate(this.pos.x, this.pos.y);
+ this.calcLeg(Math.PI, -3);
+ this.drawLeg("#4a4a4a");
+ this.calcLeg(0, 0);
+ this.drawLeg("#333");
+ ctx.rotate(this.angle);
+
+ ctx.beginPath();
+ ctx.arc(0, 0, 30, 0, 2 * Math.PI);
+ let grd = ctx.createLinearGradient(-30, 0, 30, 0);
+ grd.addColorStop(0, this.fillColorDark);
+ grd.addColorStop(1, this.fillColor);
+ ctx.fillStyle = grd;
+ ctx.fill();
+ ctx.arc(15, 0, 4, 0, 2 * Math.PI);
+ ctx.strokeStyle = "#333";
+ ctx.lineWidth = 2;
+ ctx.stroke();
+ // ctx.beginPath();
+ // ctx.arc(15, 0, 3, 0, 2 * Math.PI);
+ // ctx.fillStyle = '#9cf' //'#0cf';
+ // ctx.fill()
+ ctx.restore();
+ }
+};
\ No newline at end of file
diff --git a/js/powerups.js b/js/powerups.js
new file mode 100644
index 0000000..f4040b3
--- /dev/null
+++ b/js/powerups.js
@@ -0,0 +1,232 @@
+let powerUp = [];
+
+const powerUps = {
+ heal: {
+ name: "heal",
+ color: "#0fb",
+ size() {
+ return 40 * Math.sqrt(0.1 + Math.random() * 0.5);
+ },
+ effect() {
+ let heal = (this.size / 40) ** 2
+ heal = Math.min(1 - mech.health, heal)
+ mech.addHealth(heal);
+ if (!game.lastLogTime && heal > 0) game.makeTextLog('heal for ' + (heal * 100).toFixed(0) + '%', 180)
+ }
+ },
+ ammo: {
+ name: "ammo",
+ color: "#467",
+ size() {
+ return 17;
+ },
+ effect() {
+ //only get ammo for guns player has
+ let target;
+ // console.log(b.inventory.length)
+ if (b.inventory.length > 0) {
+ //add ammo to a gun in inventory
+ target = b.guns[b.inventory[Math.floor(Math.random() * (b.inventory.length))]];
+ //try 3 more times to give ammo to a gun with ammo, not Infinity
+ if (target.ammo === Infinity) {
+ target = b.guns[b.inventory[Math.floor(Math.random() * (b.inventory.length))]]
+ if (target.ammo === Infinity) {
+ target = b.guns[b.inventory[Math.floor(Math.random() * (b.inventory.length))]]
+ if (target.ammo === Infinity) target = b.guns[b.inventory[Math.floor(Math.random() * (b.inventory.length))]]
+ }
+ }
+ } else {
+ //if you don't have any guns just add ammo to a random gun you don't have yet
+ target = b.guns[Math.floor(Math.random() * b.guns.length)];
+ }
+ if (target.ammo === Infinity) {
+ mech.fieldMeter = 1;
+ if (!game.lastLogTime) game.makeTextLog("+energy", 180);
+ } else {
+ //ammo given scales as mobs take more hits to kill
+ const ammo = Math.ceil((target.ammoPack * (0.6 + 0.04 * Math.random())) / b.dmgScale);
+ target.ammo += ammo;
+ game.updateGunHUD();
+ if (!game.lastLogTime) game.makeTextLog("+" + ammo + " ammo: " + target.name, 180);
+ }
+ }
+ },
+ field: {
+ name: "field",
+ color: "#0bf",
+ size() {
+ return 45;
+ },
+ effect() {
+ const previousMode = mech.fieldMode
+
+ if (!this.mode) { //this.mode is set if the power up has been ejected from player
+ mode = mech.fieldMode
+ while (mode === mech.fieldMode) {
+ mode = Math.ceil(Math.random() * (mech.fieldUpgrades.length - 1))
+ }
+ mech.fieldUpgrades[mode].effect(); //choose random field upgrade that you don't already have
+ } else {
+ mech.fieldUpgrades[this.mode].effect(); //set a predetermined power up
+ }
+ //pop the old field out in case player wants to swap back
+ if (previousMode !== 0) {
+ mech.fieldCDcycle = mech.cycle + 40; //trigger fieldCD to stop power up grab automatic pick up of spawn
+ powerUps.spawn(mech.pos.x, mech.pos.y - 15, "field", false, previousMode);
+ }
+ }
+ },
+
+ mod: {
+ name: "mod",
+ color: "#a8f",
+ size() {
+ return 42;
+ },
+ effect() {
+ //find what mods I don't have
+ let options = [];
+ for (let i = 0; i < b.mods.length; i++) {
+ if (!b.mods[i].have) options.push(i);
+ }
+ //give a random mod from the mods I don't have
+ if (options.length > 0) {
+ let newMod = options[Math.floor(Math.random() * options.length)]
+ b.giveMod(newMod)
+ game.makeTextLog(`${b.mods[newMod].name}
${b.mods[newMod].description}
`, 1200);
+ } else {
+ //what should happen if you have all the mods?
+ }
+ }
+ },
+ gun: {
+ name: "gun",
+ color: "#37a",
+ size() {
+ return 35;
+ },
+ effect() {
+ //find what guns I don't have
+ let options = [];
+ if (b.activeGun === null) { //choose the first gun to be one that is good for the early game
+ options = [0, 1, 2, 3, 4, 5, 6, 8, 9, 12]
+ } else {
+ for (let i = 0; i < b.guns.length; ++i) {
+ if (!b.guns[i].have) options.push(i);
+ }
+ }
+ //give player a gun they don't already have if possible
+ if (options.length > 0) {
+ let newGun = options[Math.floor(Math.random() * options.length)];
+ // newGun = 4; //makes every gun you pick up this type //enable for testing one gun
+ if (b.activeGun === null) {
+ b.activeGun = newGun //if no active gun switch to new gun
+ game.makeTextLog(
+ // "
left mouse: fire weapon
️
",
+ "Use left mouse to fire weapon.",
+ Infinity
+ );
+ }
+ game.makeTextLog(`${b.guns[newGun].name} (left click)
${b.guns[newGun].description}
`, 1000);
+ // if (b.inventory.length === 1) { //on the second gun pick up tell player how to change guns
+ // game.makeTextLog(`(Q, E, and mouse wheel change weapons)