From f7674f4ffdbed5a09832f6ef0a0a5c1da347f4d0 Mon Sep 17 00:00:00 2001 From: landgreen Date: Wed, 30 Oct 2019 06:00:08 -0700 Subject: [PATCH] added spore mod --- index.html | 422 ++++++++++++ js/bullets.js | 1366 ++++++++++++++++++++++++++++++++++++++ js/engine.js | 174 +++++ js/game.js | 1028 +++++++++++++++++++++++++++++ js/index.js | 197 ++++++ js/level.js | 1593 ++++++++++++++++++++++++++++++++++++++++++++ js/mobs.js | 990 ++++++++++++++++++++++++++++ js/player.js | 1451 ++++++++++++++++++++++++++++++++++++++++ js/powerups.js | 232 +++++++ js/spawn.js | 1722 ++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 255 +++++++ 11 files changed, 9430 insertions(+) create mode 100644 index.html create mode 100644 js/bullets.js create mode 100644 js/engine.js create mode 100644 js/game.js create mode 100644 js/index.js create mode 100644 js/level.js create mode 100644 js/mobs.js create mode 100644 js/player.js create mode 100644 js/powerups.js create mode 100644 js/spawn.js create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..0de6999 --- /dev/null +++ b/index.html @@ -0,0 +1,422 @@ + + + + + + + + + + + + + + + + + + + n-gon + + + + + + + +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ info +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
FIREleft mouse
FIELDright mouse / spacebar
MOVEWASD / arrows
GUNSQ / E / mouse wheel
ZOOM+ / -
PAUSEP
+
+ + + + + Github + + + +

Written by Ross Landgreen

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Q + E + W + S + D + A + + + + + + + + + + + + + + switch + guns + move + fire + field + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/bullets.js b/js/bullets.js new file mode 100644 index 0000000..cc25883 --- /dev/null +++ b/js/bullets.js @@ -0,0 +1,1366 @@ +let bullet = []; + +const b = { + dmgScale: null, //scales all gun damage from momentum, but not raw .dmg //this is reset in game.reset + gravity: 0.0006, //most other bodies have gravity = 0.001 + //variables use for gun mod upgrades + modFireRate: null, + modExplosionRadius: null, + modBulletSize: null, + modEnergySiphon: null, + modHealthDrain: null, + modNoAmmo: null, + modBulletsLastLonger: null, + modIsImmortal: null, + modSpores: null, + setModDefaults() { + b.modFireRate = 1; + b.modExplosionRadius = 1; + b.modBulletSize = 1; + b.modEnergySiphon = 0; + b.modHealthDrain = 0; + b.modNoAmmo = 0; + b.modBulletsLastLonger = 1; + b.modIsImmortal = false; + b.modSpores = false; + for (let i = 0; i < b.mods.length; i++) { + b.mods[i].have = false; + } + }, + mods: [{ + name: "Depleted Uranium Rounds", + description: "your bullets are larger and do more physical damage", + have: false, + effect: () => { + //good for guns that do mostly projectile damage: + //testing done at 1.15: one shot(+0.38), rapid fire(+0.25), spray, wave beam(+0.4 adds range and dmg), needles(+0.1) + //testing at 1.08: spray(point blank)(+0.25), one shot(+0.16), wave beam(point blank)(+0.14) + b.modBulletSize = 1.07; + //ADD: maybe add in something that changes game play + } + }, + { + name: "Auto-Loading Heuristics", + description: "your rate of fire is 15% faster", + have: false, + effect: () => { + //good for guns with extra ammo: needles, M80, rapid fire, flak, super balls + b.modFireRate = 0.85 + //ADD: maybe add in something that changes game play + } + }, + { + name: "Desublimated Ammunition", + description: "use 50% less ammo when crouching", + have: false, + effect: () => { + //good with guns that have less ammo: one shot, grenades, missiles, super balls, spray + b.modNoAmmo = 1 + } + }, + { + name: "Corrosion Resistant Topology", + description: "your bullets last 40% longer", + have: false, + effect: () => { + //good with: drones, super balls, spore, missiles, wave beam(range), rapid fire(range), flak(range) + b.modBulletsLastLonger = 1.40 + } + }, + { + name: "Anti-Matter Cores", + description: "the radius of your explosions is doubled", + have: false, + effect: () => { + //at 1.4 gives a flat 40% increase, and increased range, balanced by limited guns and self damage + //testing at 1.3: grenade(+0.3), missiles, flak, M80 + b.modExplosionRadius = 2; //good for guns with explosions: + } + }, + { + name: "Energy Siphon", + description: "regenerate energy proportional to your damage done", + have: false, + effect: () => { + //good with laser, and all fields + b.modEnergySiphon = 0.25; + } + }, + { + name: "Entropy Transfer", + description: "heal proportional to your damage done", + have: false, + effect: () => { + //good with guns that overkill: one shot, grenade + b.modHealthDrain = 0.01; + } + }, + { + name: "Quantum Immortality", + description: "after you die continue in an alternate reality with randomized abilities", + have: false, + effect: () => { + b.modIsImmortal = true; + } + }, + { + name: "Zoospore Vector", + description: "when an enemy dies it has a 40% chance to release a spore", + have: false, + effect: () => { + b.modSpores = true; //good late game + } + }, + // () => { + // b.mod = 8; + // game.makeTextLog("Relativistic Velocity
(left click)

Your bullets are effected extra by your own velocity

", 1200); + // b.setModDefaults(); //good with: one shot, rapid fire, spray, super balls + // }, + ], + giveMod(i) { + b.mods[i].effect(); //give specific mod + b.mods[i].have = true + game.updateModHUD(); + }, + activeGun: null, //current gun in use by player + inventoryGun: 0, + inventory: [0], //list of what guns player has // 0 starts with basic gun + giveGuns(gun = "all", ammoPacks = 2) { + if (gun === "all") { + b.activeGun = 0; + for (let i = 0; i < b.guns.length; i++) { + b.guns[i].have = true; + b.guns[i].ammo = b.guns[i].ammoPack * ammoPacks; + b.inventory[i] = i; + } + } else { + if (!b.guns[gun].have) b.inventory.push(gun); + b.activeGun = gun; + b.guns[gun].have = true; + b.guns[gun].ammo = b.guns[gun].ammoPack * ammoPacks; + } + game.makeGunHUD(); + }, + fire() { + if (game.mouseDown && mech.fireCDcycle < mech.cycle && (!(keys[32] || game.mouseDownRight) || mech.fieldFire) && b.inventory.length) { + if (b.guns[b.activeGun].ammo > 0) { + b.guns[b.activeGun].fire(); + if (b.modNoAmmo && mech.crouch) { + if (b.modNoAmmo % 2) { + b.guns[b.activeGun].ammo--; + game.updateGunHUD(); + } + b.modNoAmmo++ //makes the no ammo toggle off and on + } else { + b.guns[b.activeGun].ammo--; + game.updateGunHUD(); + } + } else { + mech.fireCDcycle = mech.cycle + 30; //cooldown + // game.makeTextLog("
NO AMMO
E / Q", 200); + game.makeTextLog("
NO AMMO

Q, E, and mouse wheel change weapons

", 200); + } + if (mech.isHolding) { + mech.drop(); + } + } + }, + draw() { + ctx.beginPath(); + let i = bullet.length; + while (i--) { + //draw + let vertices = bullet[i].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); + //remove bullet if at end cycle for that bullet + if (bullet[i].endCycle < game.cycle) { + bullet[i].onEnd(i); //some bullets do stuff on end + if (bullet[i]) { + Matter.World.remove(engine.world, bullet[i]); + bullet.splice(i, 1); + } else { + break; //if bullet[i] doesn't exist don't complete the for loop, because the game probably reset + } + } + } + ctx.fillStyle = "#000"; + ctx.fill(); + //do things + for (let i = 0, len = bullet.length; i < len; i++) { + bullet[i].do(); + } + }, + fireProps(cd, speed, dir, me) { + mech.fireCDcycle = mech.cycle + Math.floor(cd * b.modFireRate); // cool down + Matter.Body.setVelocity(bullet[me], { + x: mech.Vx / 2 + speed * Math.cos(dir), + y: mech.Vy / 2 + speed * Math.sin(dir) + }); + World.add(engine.world, bullet[me]); //add bullet to world + }, + fireAttributes(dir, rotate = true) { + if (rotate) { + return { + // density: 0.0015, //frictionAir: 0.01, //restitution: 0, + angle: dir, + friction: 0.5, + frictionAir: 0, + dmg: 0, //damage done in addition to the damage from momentum + classType: "bullet", + collisionFilter: { + category: 0x000100, + mask: 0x010011 //mask: 0x000101, //for self collision + }, + minDmgSpeed: 10, + onDmg() {}, //this.endCycle = 0 //triggers despawn + onEnd() {} + }; + } else { + return { + // density: 0.0015, //frictionAir: 0.01, //restitution: 0, + inertia: Infinity, //prevents rotation + angle: dir, + friction: 0.5, + frictionAir: 0, + dmg: 0, //damage done in addition to the damage from momentum + classType: "bullet", + collisionFilter: { + category: 0x000100, + mask: 0x010011 //mask: 0x000101, //for self collision + }, + minDmgSpeed: 10, + onDmg() {}, //this.endCycle = 0 //triggers despawn + onEnd() {} + }; + } + }, + muzzleFlash(radius = 10) { + ctx.fillStyle = "#fb0"; + ctx.beginPath(); + ctx.arc(mech.pos.x + 35 * Math.cos(mech.angle), mech.pos.y + 35 * Math.sin(mech.angle), radius, 0, 2 * Math.PI); + ctx.fill(); + }, + drawOneBullet(vertices) { + ctx.beginPath(); + 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 = "#000"; + ctx.fill(); + }, + removeConsBB(me) { + for (let i = 0, len = consBB.length; i < len; ++i) { + if (consBB[i].bodyA === me) { + consBB[i].bodyA = consBB[i].bodyB; + consBB.splice(i, 1); + // b.removeConsBB(me); + break; + } else if (consBB[i].bodyB === me) { + consBB[i].bodyB = consBB[i].bodyA; + consBB.splice(i, 1); + // b.removeConsBB(me); + break; + } + } + }, + explode(me) { + // typically explode is used for some bullets with .onEnd + const radius = bullet[me].explodeRad * b.modExplosionRadius + //add dmg to draw queue + game.drawList.push({ + x: bullet[me].position.x, + y: bullet[me].position.y, + radius: radius, + color: "rgba(255,0,0,0.4)", + time: game.drawTime + }); + let dist, sub, knock; + const dmg = b.dmgScale * radius * 0.01; + + const alertRange = 100 + radius * 2; //alert range + //add alert to draw queue + game.drawList.push({ + x: bullet[me].position.x, + y: bullet[me].position.y, + radius: alertRange, + color: "rgba(100,20,0,0.03)", + time: game.drawTime + }); + + //player damage and knock back + sub = Matter.Vector.sub(bullet[me].position, player.position); + dist = Matter.Vector.magnitude(sub); + if (dist < radius) { + mech.damage(radius * 0.00035); + knock = Matter.Vector.mult(Matter.Vector.normalise(sub), -Math.sqrt(dmg) * player.mass / 30); + player.force.x += knock.x; + player.force.y += knock.y; + mech.drop(); + } else if (dist < alertRange) { + knock = Matter.Vector.mult(Matter.Vector.normalise(sub), -Math.sqrt(dmg) * player.mass / 55); + player.force.x += knock.x; + player.force.y += knock.y; + mech.drop(); + } + + + //body knock backs + for (let i = 0, len = body.length; i < len; ++i) { + sub = Matter.Vector.sub(bullet[me].position, body[i].position); + dist = Matter.Vector.magnitude(sub); + if (dist < radius) { + knock = Matter.Vector.mult(Matter.Vector.normalise(sub), (-Math.sqrt(dmg) * body[i].mass) / 18); + body[i].force.x += knock.x; + body[i].force.y += knock.y; + } else if (dist < alertRange) { + knock = Matter.Vector.mult(Matter.Vector.normalise(sub), (-Math.sqrt(dmg) * body[i].mass) / 40); + body[i].force.x += knock.x; + body[i].force.y += knock.y; + } + } + + //power up knock backs + for (let i = 0, len = powerUp.length; i < len; ++i) { + sub = Matter.Vector.sub(bullet[me].position, powerUp[i].position); + dist = Matter.Vector.magnitude(sub); + if (dist < radius) { + knock = Matter.Vector.mult(Matter.Vector.normalise(sub), (-Math.sqrt(dmg) * powerUp[i].mass) / 26); + powerUp[i].force.x += knock.x; + powerUp[i].force.y += knock.y; + } else if (dist < alertRange) { + knock = Matter.Vector.mult(Matter.Vector.normalise(sub), (-Math.sqrt(dmg) * powerUp[i].mass) / 40); + powerUp[i].force.x += knock.x; + powerUp[i].force.y += knock.y; + } + } + + // bullet knock backs (not working: no effect for drones, crash for superballs) + // for (let i = 0, len = bullet.length; i < len; ++i) { + // if (me !== i) { + // sub = Matter.Vector.sub(bullet[me].position, bullet[i].position); + // dist = Matter.Vector.magnitude(sub); + // if (dist < radius) { + // knock = Matter.Vector.mult(Matter.Vector.normalise(sub), (-Math.sqrt(dmg) * bullet[i].mass) / 10); + // bullet[i].force.x += knock.x; + // bullet[i].force.y += knock.y; + // console.log(sub, dist, knock) + // } else if (dist < alertRange) { + // knock = Matter.Vector.mult(Matter.Vector.normalise(sub), (-Math.sqrt(dmg) * bullet[i].mass) / 20); + // bullet[i].force.x += knock.x; + // bullet[i].force.y += knock.y; + // } + // } + // } + + //destroy all bullets in range + // for (let i = 0, len = bullet.length; i < len; ++i) { + // if (me != i) { + // sub = Matter.Vector.sub(bullet[me].position, bullet[i].position); + // dist = Matter.Vector.magnitude(sub); + // if (dist < radius) { + // bullet[i].endCycle = mech.cycle; + // } + // } + // } + + //mob damage and knock back with no alert + // for (let i = 0, len = mob.length; i < len; ++i) { + // if (mob[i].alive) { + // let vertices = mob[i].vertices; + // for (let j = 0, len = vertices.length; j < len; j++) { + // sub = Matter.Vector.sub(bullet[me].position, vertices[j]); + // dist = Matter.Vector.magnitude(sub); + // if (dist < radius) { + // mob[i].damage(dmg); + // mob[i].locatePlayer(); + // knock = Matter.Vector.mult(Matter.Vector.normalise(sub), -Math.sqrt(dmg) * mob[i].mass / 18); + // mob[i].force.x += knock.x; + // mob[i].force.y += knock.y; + // break; + // } + // } + // } + // } + + //mob damage and knock back with alert + let damageScale = 1; // reduce dmg for each new target to limit total AOE damage + for (let i = 0, len = mob.length; i < len; ++i) { + if (mob[i].alive) { + sub = Matter.Vector.sub(bullet[me].position, mob[i].position); + dist = Matter.Vector.magnitude(sub); + if (dist < radius) { + mob[i].damage(dmg * damageScale); + mob[i].locatePlayer(); + knock = Matter.Vector.mult(Matter.Vector.normalise(sub), (-Math.sqrt(dmg * damageScale) * mob[i].mass) / 18); + mob[i].force.x += knock.x; + mob[i].force.y += knock.y; + damageScale *= 0.8 //reduced damage for each additional explosion target + } else if (!mob[i].seePlayer.recall && dist < alertRange) { + mob[i].locatePlayer(); + knock = Matter.Vector.mult(Matter.Vector.normalise(sub), (-Math.sqrt(dmg * damageScale) * mob[i].mass) / 35); + mob[i].force.x += knock.x; + mob[i].force.y += knock.y; + } + } + } + + // Matter.Vector.magnitudeSquared(Matter.Vector.sub(bullet[me].position, mob[i].position)) + }, + spore(who) { //used with the mod upgrade in mob.death() + const bIndex = bullet.length; + const RADIUS = (4 + 2 * Math.random()) * b.modBulletSize; + bullet[bIndex] = Bodies.circle(who.position.x, who.position.y, RADIUS, { + // density: 0.0015, //frictionAir: 0.01, + inertia: Infinity, + restitution: 0.9, + angle: Math.random() * 2 * Math.PI, + friction: 0, + frictionAir: 0.01, + dmg: 1.65, //damage done in addition to the damage from momentum + classType: "bullet", + collisionFilter: { + category: 0x000100, + mask: 0x000011 //no collide with body + }, + endCycle: game.cycle + Math.floor((300 + Math.floor(Math.random() * 240)) * b.modBulletsLastLonger), + minDmgSpeed: 0, + onDmg() { + this.endCycle = 0; //bullet ends cycle after doing damage + }, + onEnd() {}, + lookFrequency: 67 + Math.floor(47 * Math.random()), + do() { + //find mob targets + if (!(game.cycle % this.lookFrequency)) { + this.closestTarget = null; + this.lockedOn = null; + let closeDist = Infinity; + for (let i = 0, len = mob.length; i < len; ++i) { + if (Matter.Query.ray(map, this.position, mob[i].position).length === 0) { + // Matter.Query.ray(body, this.position, mob[i].position).length === 0 + const targetVector = Matter.Vector.sub(this.position, mob[i].position) + const dist = Matter.Vector.magnitude(targetVector); + if (dist < closeDist) { + this.closestTarget = mob[i].position; + closeDist = dist; + this.lockedOn = Matter.Vector.normalise(targetVector); + if (0.3 > Math.random()) break //doesn't always target the closest mob + } + } + } + } + //accelerate towards mobs + const THRUST = this.mass * 0.0008 + if (this.lockedOn) { + this.force.x -= THRUST * this.lockedOn.x + this.force.y -= THRUST * this.lockedOn.y + } else { + this.force.y += this.mass * 0.00027; //gravity + } + }, + }); + const SPEED = 9; + const ANGLE = 2 * Math.PI * Math.random() + Matter.Body.setVelocity(bullet[bIndex], { + x: SPEED * Math.cos(ANGLE), + y: SPEED * Math.sin(ANGLE) + }); + World.add(engine.world, bullet[bIndex]); //add bullet to world + }, + guns: [{ + name: "laser", + description: "fire a beam of coherent light
reflects off walls at 75% intensity
uses energy instead of ammunition", + ammo: 0, + // ammoPack: 350, + ammoPack: Infinity, + have: false, + fire() { + // mech.fireCDcycle = mech.cycle + 1 + //laser drains energy as well as bullets + const FIELD_DRAIN = 0.002 + const damage = 0.05 + if (mech.fieldMeter < FIELD_DRAIN) { + mech.fireCDcycle = mech.cycle + 100; // cool down if out of energy + } else { + mech.fieldMeter -= mech.fieldRegen + FIELD_DRAIN + let best; + const color = "#f00"; + const range = 3000; + const path = [{ + x: mech.pos.x + 20 * Math.cos(mech.angle), + y: mech.pos.y + 20 * Math.sin(mech.angle) + }, + { + x: mech.pos.x + range * Math.cos(mech.angle), + y: mech.pos.y + range * Math.sin(mech.angle) + } + ]; + 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 && (!domain[i].mob || domain[i].alive)) { + best = { + x: results.x, + y: results.y, + dist2: dist2, + who: domain[i], + v1: vertices[0], + v2: vertices[len] + }; + } + } + } + }; + const checkforCollisions = function () { + best = { + x: null, + y: null, + dist2: Infinity, + who: null, + v1: null, + v2: null + }; + vertexCollision(path[path.length - 2], path[path.length - 1], mob); + vertexCollision(path[path.length - 2], path[path.length - 1], map); + vertexCollision(path[path.length - 2], path[path.length - 1], body); + }; + const laserHitMob = function (dmg) { + if (best.who.alive) { + dmg *= b.dmgScale * damage; + best.who.damage(dmg); + best.who.locatePlayer(); + //draw mob damage circle + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(path[path.length - 1].x, path[path.length - 1].y, Math.sqrt(dmg) * 100, 0, 2 * Math.PI); + ctx.fill(); + } + }; + + const reflection = function () { + // https://math.stackexchange.com/questions/13261/how-to-get-a-reflection-vector + const n = Matter.Vector.perp(Matter.Vector.normalise(Matter.Vector.sub(best.v1, best.v2))); + const d = Matter.Vector.sub(path[path.length - 1], path[path.length - 2]); + const nn = Matter.Vector.mult(n, 2 * Matter.Vector.dot(d, n)); + const r = Matter.Vector.normalise(Matter.Vector.sub(d, nn)); + path[path.length] = Matter.Vector.add(Matter.Vector.mult(r, range), path[path.length - 1]); + }; + //beam before reflection + checkforCollisions(); + if (best.dist2 != Infinity) { + //if hitting something + path[path.length - 1] = { + x: best.x, + y: best.y + }; + laserHitMob(1); + + //1st reflection beam + reflection(); + //ugly bug fix: this stops the reflection on a bug where the beam gets trapped inside a body + let who = best.who; + checkforCollisions(); + if (best.dist2 != Infinity) { + //if hitting something + path[path.length - 1] = { + x: best.x, + y: best.y + }; + laserHitMob(0.75); + + //2nd reflection beam + //ugly bug fix: this stops the reflection on a bug where the beam gets trapped inside a body + if (who !== best.who) { + reflection(); + checkforCollisions(); + if (best.dist2 != Infinity) { + //if hitting something + path[path.length - 1] = { + x: best.x, + y: best.y + }; + laserHitMob(0.5); + } + } + } + } + ctx.fillStyle = color; + ctx.strokeStyle = color; + ctx.lineWidth = 2; + ctx.lineDashOffset = 300 * Math.random() + // ctx.setLineDash([200 * Math.random(), 250 * Math.random()]); + + ctx.setLineDash([50 + 120 * Math.random(), 50 * Math.random()]); + for (let i = 1, len = path.length; i < len; ++i) { + ctx.beginPath(); + ctx.moveTo(path[i - 1].x, path[i - 1].y); + ctx.lineTo(path[i].x, path[i].y); + ctx.stroke(); + ctx.globalAlpha *= 0.5; //reflections are less intense + // ctx.globalAlpha -= 0.1; //reflections are less intense + } + ctx.setLineDash([0, 0]); + ctx.globalAlpha = 1; + } + } + // }, { + // name: "entropic beam", + // description: "steal entropy to heal
reflects off walls at 75% intensity
uses energy instead of ammunition", + // ammo: 0, + // // ammoPack: 350, + // ammoPack: Infinity, + // have: false, + // fire() { + // // mech.fireCDcycle = mech.cycle + 1 + // //laser drains energy as well as bullets + // const FIELD_DRAIN = 0.0001 //should be 0.001 + // if (mech.fieldMeter < FIELD_DRAIN) { + // mech.fireCDcycle = mech.cycle + 100; // cool down if out of energy + // } else { + // mech.fieldMeter -= mech.fieldRegen + FIELD_DRAIN + // let best; + // const color = "#a0a"; + // const range = 600; + // const path = [{ + // x: mech.pos.x + 20 * Math.cos(mech.angle), + // y: mech.pos.y + 20 * Math.sin(mech.angle) + // }, + // { + // x: mech.pos.x + range * Math.cos(mech.angle), + // y: mech.pos.y + range * Math.sin(mech.angle) + // } + // ]; + // 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 && (!domain[i].mob || domain[i].alive)) { + // best = { + // x: results.x, + // y: results.y, + // dist2: dist2, + // who: domain[i], + // v1: vertices[0], + // v2: vertices[len] + // }; + // } + // } + // } + // }; + // const checkforCollisions = function () { + // best = { + // x: null, + // y: null, + // dist2: Infinity, + // who: null, + // v1: null, + // v2: null + // }; + // vertexCollision(path[path.length - 2], path[path.length - 1], mob); + // vertexCollision(path[path.length - 2], path[path.length - 1], map); + // vertexCollision(path[path.length - 2], path[path.length - 1], body); + // }; + // const laserHitMob = function (dmg) { + // if (best.who.alive) { + // dmg *= b.dmgScale * 0.02; + // mech.addHealth(0.002) + // best.who.damage(dmg); + // best.who.locatePlayer(); + // //draw mob damage circle + // ctx.fillStyle = color; + // ctx.beginPath(); + // ctx.arc(path[path.length - 1].x, path[path.length - 1].y, Math.sqrt(dmg) * 100, 0, 2 * Math.PI); + // ctx.fill(); + // } + // }; + // checkforCollisions(); + // if (best.dist2 != Infinity) { + // //if hitting something + // path[path.length - 1] = { + // x: best.x, + // y: best.y + // }; + // laserHitMob(1); + // } + // ctx.fillStyle = color; + // ctx.strokeStyle = color; + // ctx.lineWidth = 4; + // for (let i = 1, len = path.length; i < len; ++i) { + // d = { + // x: path[i].x - path[i - 1].x, + // y: path[i].y - path[i - 1].y + // } + // const a = mech.cycle * 5 + // p1 = { + // x: d.x / 2 * Math.cos(a) - d.y / 2 * Math.sin(a), + // y: d.x / 2 * Math.sin(a) + d.y / 2 * Math.cos(a), + // } + // const wave = 300 / Math.sqrt(d.x * d.x + d.y * d.y) + // ctx.beginPath(); + // ctx.moveTo(path[i - 1].x, path[i - 1].y); + // ctx.bezierCurveTo(path[i - 1].x + p1.x * wave, path[i - 1].y + p1.y * wave, path[i].x + p1.x * wave, path[i].y + p1.y * wave, path[i].x, path[i].y); + // ctx.stroke(); + // } + // } + // } + }, { + name: "one shot", + description: "fire a huge bullet with high recoil
effective at long distances", + ammo: 0, + ammoPack: 5, + have: false, + fire() { + b.muzzleFlash(45); + // mobs.alert(800); + const me = bullet.length; + const dir = mech.angle; + bullet[me] = Bodies.rectangle(mech.pos.x + 50 * Math.cos(mech.angle), mech.pos.y + 50 * Math.sin(mech.angle), 70 * b.modBulletSize, 30 * b.modBulletSize, b.fireAttributes(dir)); + b.fireProps(mech.crouch ? 55 : 40, 50, dir, me); //cd , speed + bullet[me].endCycle = game.cycle + Math.floor(180 * b.modBulletsLastLonger); + bullet[me].do = function () { + this.force.y += this.mass * 0.0005; + }; + + //knock back + const KNOCK = ((mech.crouch) ? 0.025 : 0.25) * b.modBulletSize * b.modBulletSize + player.force.x -= KNOCK * Math.cos(dir) + player.force.y -= KNOCK * Math.sin(dir) * 0.5 //reduce knock back in vertical direction to stop super jumps + } + }, + { + name: "rapid fire", + description: "fire a stream of bullets", + ammo: 0, + ammoPack: 105, + have: false, + fire() { + const me = bullet.length; + b.muzzleFlash(15); + // if (Math.random() > 0.2) mobs.alert(500); + const dir = mech.angle + (Math.random() - 0.5) * ((mech.crouch) ? 0.07 : 0.16); + bullet[me] = Bodies.rectangle(mech.pos.x + 30 * Math.cos(mech.angle), mech.pos.y + 30 * Math.sin(mech.angle), 17 * b.modBulletSize, 5 * b.modBulletSize, b.fireAttributes(dir)); + b.fireProps(mech.crouch ? 11 : 5, mech.crouch ? 44 : 36, dir, me); //cd , speed + bullet[me].endCycle = game.cycle + Math.floor(65 * b.modBulletsLastLonger); + bullet[me].frictionAir = 0.01; + bullet[me].do = function () { + this.force.y += this.mass * 0.0005; + }; + } + }, + { + name: "wave beam", + description: "fire a stream of oscillating particles
propagates through solids
effective at close range", + ammo: 0, + ammoPack: 85, + have: false, + fire() { + const me = bullet.length; + const DIR = mech.angle + const SCALE = (mech.crouch ? 0.963 : 0.95) + const wiggleMag = ((mech.flipLegs === 1) ? 1 : -1) * ((mech.crouch) ? 0.004 : 0.005) + bullet[me] = Bodies.circle(mech.pos.x + 25 * Math.cos(DIR), mech.pos.y + 25 * Math.sin(DIR), 10 * b.modBulletSize, { + angle: DIR, + cycle: -0.43, //adjust this number until the bullets line up with the cross hairs + endCycle: game.cycle + Math.floor((mech.crouch ? 155 : 120) * b.modBulletsLastLonger), + inertia: Infinity, + frictionAir: 0, + minDmgSpeed: 0, + dmg: 0.13, //damage done in addition to the damage from momentum + classType: "bullet", + collisionFilter: { + category: 0x000100, + mask: 0x000010 + }, + onDmg() {}, + onEnd() {}, + do() { + if (!mech.isBodiesAsleep) { + this.cycle++ + const THRUST = wiggleMag * Math.cos(this.cycle * 0.3) + this.force = Matter.Vector.mult(Matter.Vector.normalise(this.direction), this.mass * THRUST) //wiggle + + if (this.cycle > 0 && !(Math.floor(this.cycle) % 6)) Matter.Body.scale(this, SCALE, SCALE); //shrink + } + } + }); + World.add(engine.world, bullet[me]); //add bullet to world + mech.fireCDcycle = mech.cycle + Math.floor((mech.crouch ? 8 : 4) * b.modFireRate); // cool down + const SPEED = mech.crouch ? 5.2 : 4.5; + Matter.Body.setVelocity(bullet[me], { + x: SPEED * Math.cos(DIR), + y: SPEED * Math.sin(DIR) + }); + bullet[me].direction = Matter.Vector.perp(bullet[me].velocity) + // if (mech.angle + Math.PI / 2 > 0) { + // bullet[me].direction = Matter.Vector.perp(bullet[me].velocity, true) + // } else { + // bullet[me].direction = Matter.Vector.perp(bullet[me].velocity) + // } + + World.add(engine.world, bullet[me]); //add bullet to world + } + }, + { + name: "super balls", + description: "fire 3 very bouncy balls", + ammo: 0, + ammoPack: 11, + have: false, + fire() { + b.muzzleFlash(20); + // mobs.alert(450); + const SPREAD = mech.crouch ? 0.04 : 0.14 + let dir = mech.angle - SPREAD; + for (let i = 0; i < 3; i++) { + const me = bullet.length; + bullet[me] = Bodies.circle(mech.pos.x + 30 * Math.cos(mech.angle), mech.pos.y + 30 * Math.sin(mech.angle), 7 * b.modBulletSize, b.fireAttributes(dir, false)); + b.fireProps(mech.crouch ? 40 : 20, mech.crouch ? 34 : 26, dir, me); //cd , speed + Matter.Body.setDensity(bullet[me], 0.0001); + bullet[me].endCycle = game.cycle + Math.floor(360 * b.modBulletsLastLonger); + bullet[me].dmg = 0.5; + bullet[me].minDmgSpeed = 0; + bullet[me].restitution = 0.96; + bullet[me].friction = 0; + bullet[me].do = function () { + this.force.y += this.mass * 0.001; + }; + dir += SPREAD; + } + } + }, + { + name: "spray", + description: "fire a burst of bullets with high recoil
more effective at close range", + ammo: 0, + ammoPack: 8, + have: false, + fire() { + b.muzzleFlash(35); + // mobs.alert(650); + const side = 11 * b.modBulletSize + for (let i = 0; i < 9; i++) { + const me = bullet.length; + const dir = mech.angle + (Math.random() - 0.5) * (mech.crouch ? 0.22 : 0.7) + bullet[me] = Bodies.rectangle(mech.pos.x + 35 * Math.cos(mech.angle) + 15 * (Math.random() - 0.5), mech.pos.y + 35 * Math.sin(mech.angle) + 15 * (Math.random() - 0.5), side, side, b.fireAttributes(dir)); + b.fireProps(mech.crouch ? 60 : 30, 40 + Math.random() * 11, dir, me); //cd , speed + bullet[me].endCycle = game.cycle + Math.floor(55 * b.modBulletsLastLonger); + bullet[me].frictionAir = 0.03; + bullet[me].do = function () { + this.force.y += this.mass * 0.001; + }; + } + + //knock back + const KNOCK = ((mech.crouch) ? 0.015 : 0.15) * b.modBulletSize * b.modBulletSize + player.force.x -= KNOCK * Math.cos(mech.angle) + player.force.y -= KNOCK * Math.sin(mech.angle) * 0.5 //reduce knock back in vertical direction to stop super jumps + } + }, + { + name: "needles", + description: "fire a narrow projectile
effective at long distances", + ammo: 0, + ammoPack: 19, + have: false, + fire() { + const me = bullet.length; + const dir = mech.angle; + if (mech.crouch) { + bullet[me] = Bodies.rectangle(mech.pos.x + 40 * Math.cos(mech.angle), mech.pos.y + 40 * Math.sin(mech.angle), 40 * b.modBulletSize, 3 * b.modBulletSize, b.fireAttributes(dir)); + } else { + bullet[me] = Bodies.rectangle(mech.pos.x + 40 * Math.cos(mech.angle), mech.pos.y + 40 * Math.sin(mech.angle), 31 * b.modBulletSize, 2 * b.modBulletSize, b.fireAttributes(dir)); + } + b.fireProps(mech.crouch ? 40 : 20, mech.crouch ? 45 : 37, dir, me); //cd , speed + bullet[me].endCycle = game.cycle + Math.floor(180 * b.modBulletsLastLonger); + bullet[me].dmg = mech.crouch ? 1.4 : 1; + b.drawOneBullet(bullet[me].vertices); + bullet[me].do = function () { + //low gravity + this.force.y += this.mass * 0.0002; + }; + } + }, + { + name: "missiles", + description: "fire a missile that accelerates towards nearby targets
explodes when near target", + ammo: 0, + ammoPack: 9, + have: false, + fireCycle: 0, + ammoLoaded: 0, + fire() { + const thrust = 0.0003; + let dir = mech.angle + (0.5 - Math.random()) * (mech.crouch ? 0 : 0.2); + const me = bullet.length; + bullet[me] = Bodies.rectangle(mech.pos.x + 40 * Math.cos(mech.angle), mech.pos.y + 40 * Math.sin(mech.angle) - 3, 30 * b.modBulletSize, 4 * b.modBulletSize, b.fireAttributes(dir)); + b.fireProps(mech.crouch ? 70 : 30, -3 * (0.5 - Math.random()) + (mech.crouch ? 25 : -8), dir, me); //cd , speed + + b.drawOneBullet(bullet[me].vertices); + // Matter.Body.setDensity(bullet[me], 0.01) //doesn't help with reducing explosion knock backs + bullet[me].force.y += 0.00045; //a small push down at first to make it seem like the missile is briefly falling + bullet[me].frictionAir = 0 + bullet[me].endCycle = game.cycle + Math.floor((265 + Math.random() * 20) * b.modBulletsLastLonger); + bullet[me].explodeRad = 150 + 40 * Math.random(); + bullet[me].lookFrequency = Math.floor(8 + Math.random() * 7); + bullet[me].onEnd = b.explode; //makes bullet do explosive damage at end + bullet[me].onDmg = function () { + this.endCycle = 0; //bullet ends cycle after doing damage // also triggers explosion + }; + bullet[me].lockedOn = null; + bullet[me].do = function () { + if (!mech.isBodiesAsleep) { + if (!(mech.cycle % this.lookFrequency)) { + this.closestTarget = null; + this.lockedOn = null; + let closeDist = Infinity; + + //look for targets + for (let i = 0, len = mob.length; i < len; ++i) { + if ( + mob[i].alive && + mob[i].dropPowerUp && + Matter.Query.ray(map, this.position, mob[i].position).length === 0 && + Matter.Query.ray(body, this.position, mob[i].position).length === 0 + ) { + const dist = Matter.Vector.magnitude(Matter.Vector.sub(this.position, mob[i].position)); + if (dist < closeDist) { + this.closestTarget = mob[i].position; + closeDist = dist; + this.lockedOn = mob[i]; + } + } + } + //explode when bullet is close enough to target + if (this.closestTarget && closeDist < this.explodeRad * 0.7) { + this.endCycle = 0; //bullet ends cycle after doing damage //this also triggers explosion + } + + if (this.lockedOn) { + this.frictionAir = 0.04; //extra friction + + //draw locked on targeting + ctx.beginPath(); + const vertices = this.lockedOn.vertices; + ctx.moveTo(this.position.x, this.position.y); + const mod = Math.floor((game.cycle / 3) % vertices.length); + ctx.lineTo(vertices[mod].x, vertices[mod].y); + ctx.strokeStyle = "rgba(0,0,155,0.35)"; //"#2f6"; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + + //rotate missile towards the target + if (this.closestTarget) { + const face = { + x: Math.cos(this.angle), + y: Math.sin(this.angle) + }; + const target = Matter.Vector.normalise(Matter.Vector.sub(this.position, this.closestTarget)); + if (Matter.Vector.dot(target, face) > -0.98) { + if (Matter.Vector.cross(target, face) > 0) { + Matter.Body.rotate(this, 0.08); + } else { + Matter.Body.rotate(this, -0.08); + } + } + } + //accelerate in direction bullet is facing + const dir = this.angle; // + (Math.random() - 0.5); + this.force.x += Math.cos(dir) * thrust; + this.force.y += Math.sin(dir) * thrust; + + //draw rocket + ctx.beginPath(); + ctx.arc(this.position.x - Math.cos(this.angle) * 27 + (Math.random() - 0.5) * 4, this.position.y - Math.sin(this.angle) * 27 + (Math.random() - 0.5) * 4, 11, 0, 2 * Math.PI); + ctx.fillStyle = "rgba(255,155,0,0.5)"; + ctx.fill(); + } else { + //draw rocket with time stop + ctx.beginPath(); + ctx.arc(this.position.x - Math.cos(this.angle) * 27, this.position.y - Math.sin(this.angle) * 27, 11, 0, 2 * Math.PI); + ctx.fillStyle = "rgba(255,155,0,0.5)"; + ctx.fill(); + } + } + } + }, + { + name: "flak", + description: "fire a cluster of explosive projectiles
explode on contact or after half a second", + ammo: 0, + ammoPack: 18, + have: false, + fire() { + b.muzzleFlash(30); + const totalBullets = 5 + const angleStep = (mech.crouch ? 0.06 : 0.15) / totalBullets + const SPEED = mech.crouch ? 27 : 20 + const CD = mech.crouch ? 50 : 20 + const END = Math.floor((mech.crouch ? 27 : 18) * b.modBulletsLastLonger); + let dir = mech.angle - angleStep * totalBullets / 2; + const side1 = 17 * b.modBulletSize + const side2 = 4 * b.modBulletSize + + for (let i = 0; i < totalBullets; i++) { //5 -> 7 + dir += angleStep + const me = bullet.length; + bullet[me] = Bodies.rectangle(mech.pos.x + 50 * Math.cos(mech.angle), mech.pos.y + 50 * Math.sin(mech.angle), side1, side2, b.fireAttributes(dir)); + b.fireProps(CD, SPEED + 25 * Math.random() - i, dir, me); //cd , speed + + //Matter.Body.setDensity(bullet[me], 0.00001); + bullet[me].endCycle = i + game.cycle + END + bullet[me].restitution = 0; + bullet[me].friction = 1; + bullet[me].explodeRad = 75 + (Math.random() - 0.5) * 50; + bullet[me].onEnd = b.explode; + bullet[me].onDmg = function () { + this.endCycle = 0; //bullet ends cycle after hitting a mob and triggers explosion + }; + bullet[me].do = function () { + this.force.y += this.mass * 0.0004; + // if (this.speed < 10) { //if slow explode + // for (let i = 0, len = bullet.length; i < len; i++) { + // bullet[i].endCycle = 0 //all other bullets explode + // } + // } + } + } + } + }, + { + name: "M80", + description: "rapidly fire small bouncy bombs
explodes on contact or after 2 seconds", + ammo: 0, + ammoPack: 45, + have: false, + fire() { + const me = bullet.length; + const dir = mech.angle; // + Math.random() * 0.05; + bullet[me] = Bodies.circle(mech.pos.x + 30 * Math.cos(mech.angle), mech.pos.y + 30 * Math.sin(mech.angle), 10 * b.modBulletSize, b.fireAttributes(dir, false)); + b.fireProps(mech.crouch ? 15 : 8, mech.crouch ? 32 : 24, dir, me); //cd , speed + b.drawOneBullet(bullet[me].vertices); + Matter.Body.setDensity(bullet[me], 0.000001); + bullet[me].totalCycles = 120; + bullet[me].endCycle = game.cycle + Math.floor(120 * b.modBulletsLastLonger); + bullet[me].restitution = 0.6; + bullet[me].explodeRad = 130; + bullet[me].onEnd = b.explode; //makes bullet do explosive damage before despawn + bullet[me].minDmgSpeed = 1; + bullet[me].dmg = 0.25; + bullet[me].onDmg = function () { + this.endCycle = 0; //bullet ends cycle after doing damage //this triggers explosion + }; + bullet[me].do = function () { + //extra gravity for harder arcs + this.force.y += this.mass * 0.0025; + }; + } + }, + { + name: "grenades", + description: "fire a huge sticky bomb
explodes on contact or after 2 seconds", + ammo: 0, + ammoPack: 5, + have: false, + fire() { + const me = bullet.length; + const dir = mech.angle; + bullet[me] = Bodies.circle(mech.pos.x + 30 * Math.cos(mech.angle), mech.pos.y + 30 * Math.sin(mech.angle), 22 * b.modBulletSize, b.fireAttributes(dir, false)); + bullet[me].radius = 22; //used from drawing timer + b.fireProps(mech.crouch ? 60 : 40, mech.crouch ? 38 : 30, dir, me); //cd , speed + + b.drawOneBullet(bullet[me].vertices); + Matter.Body.setDensity(bullet[me], 0.000001); + bullet[me].endCycle = game.cycle + Math.floor(140 * b.modBulletsLastLonger); + bullet[me].endCycleLength = Math.floor(140 * b.modBulletsLastLonger); + // bullet[me].restitution = 0.3; + // bullet[me].frictionAir = 0.01; + // bullet[me].friction = 0.15; + bullet[me].inertia = Infinity; //prevents rotation + bullet[me].restitution = 0; + bullet[me].friction = 1; + + bullet[me].explodeRad = 380 + Math.floor(Math.random() * 60); + bullet[me].onEnd = b.explode; //makes bullet do explosive damage before despawn + bullet[me].minDmgSpeed = 1; + bullet[me].onDmg = function () { + this.endCycle = 0; //bullet ends cycle after doing damage //this triggers explosion + }; + bullet[me].do = function () { + //extra gravity for harder arcs + this.force.y += this.mass * 0.0022; + //draw timer + if (!(game.cycle % 10)) { + if (this.isFlashOn) { + this.isFlashOn = false; + } else { + this.isFlashOn = true; + } + } + if (this.isFlashOn) { + ctx.fillStyle = "#000"; + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, this.radius, 0, 2 * Math.PI); + ctx.fill(); + //draw clock on timer + ctx.fillStyle = "#f12"; + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, this.radius * (1 - (this.endCycle - game.cycle) / this.endCycleLength), 0, 2 * Math.PI); + ctx.fill(); + } + }; + } + }, + { + name: "spores", + description: "release an orb that discharges spores after 2 seconds
spores seek out targets
spores can pass through blocks", + ammo: 0, + ammoPack: 5, + have: false, + fire() { + const me = bullet.length; + const dir = mech.angle; + bullet[me] = Bodies.polygon(mech.pos.x + 30 * Math.cos(mech.angle), mech.pos.y + 30 * Math.sin(mech.angle), 20, 4.5, b.fireAttributes(dir, false)); + b.fireProps(mech.crouch ? 75 : 55, mech.crouch ? 25 : 14, dir, me); //cd , speed + b.drawOneBullet(bullet[me].vertices); + Matter.Body.setDensity(bullet[me], 0.000001); + bullet[me].endCycle = game.cycle + 100; + bullet[me].frictionAir = 0; + bullet[me].friction = 0.5; + bullet[me].restitution = 0.3; + bullet[me].minDmgSpeed = 0; + bullet[me].onDmg = function () {}; + bullet[me].do = function () { + if (!mech.isBodiesAsleep) { + const SCALE = 1.017 + Matter.Body.scale(this, SCALE, SCALE); + this.frictionAir += 0.00023; + } + + this.force.y += this.mass * 0.00045; + + //draw green glow + ctx.fillStyle = "rgba(0,200,125,0.16)"; + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, 26, 0, 2 * Math.PI); + ctx.fill(); + }; + + //spawn bullets on end + bullet[me].onEnd = function () { + const NUM = 9; + for (let i = 0; i < NUM; i++) { + const bIndex = bullet.length; + const RADIUS = (4 + 2 * Math.random()) * b.modBulletSize; + bullet[bIndex] = Bodies.circle(this.position.x, this.position.y, RADIUS, { + // density: 0.0015, //frictionAir: 0.01, + inertia: Infinity, + restitution: 0.9, + angle: dir, + friction: 0, + frictionAir: 0.01, + dmg: 1.65, //damage done in addition to the damage from momentum + classType: "bullet", + collisionFilter: { + category: 0x000100, + mask: 0x000011 //no collide with body + }, + endCycle: game.cycle + Math.floor((300 + Math.floor(Math.random() * 240)) * b.modBulletsLastLonger), + minDmgSpeed: 0, + onDmg() { + this.endCycle = 0; //bullet ends cycle after doing damage + }, + onEnd() {}, + lookFrequency: 67 + Math.floor(47 * Math.random()), + do() { + //find mob targets + if (!(game.cycle % this.lookFrequency)) { + this.closestTarget = null; + this.lockedOn = null; + let closeDist = Infinity; + for (let i = 0, len = mob.length; i < len; ++i) { + if (Matter.Query.ray(map, this.position, mob[i].position).length === 0) { + // Matter.Query.ray(body, this.position, mob[i].position).length === 0 + const targetVector = Matter.Vector.sub(this.position, mob[i].position) + const dist = Matter.Vector.magnitude(targetVector); + if (dist < closeDist) { + this.closestTarget = mob[i].position; + closeDist = dist; + this.lockedOn = Matter.Vector.normalise(targetVector); + if (0.3 > Math.random()) break //doesn't always target the closest mob + } + } + } + } + //accelerate towards mobs + const THRUST = this.mass * 0.0008 + if (this.lockedOn) { + this.force.x -= THRUST * this.lockedOn.x + this.force.y -= THRUST * this.lockedOn.y + } else { + this.force.y += this.mass * 0.00027; //gravity + } + }, + }); + const SPEED = 9; + const ANGLE = 2 * Math.PI * Math.random() + Matter.Body.setVelocity(bullet[bIndex], { + x: SPEED * Math.cos(ANGLE), + y: SPEED * Math.sin(ANGLE) + }); + World.add(engine.world, bullet[bIndex]); //add bullet to world + } + } + + } + }, + { + name: "drones", + description: "release drones that seek out targets
if no targets, drones move to mouse
", + ammo: 0, + ammoPack: 20, + have: false, + fire() { + const THRUST = 0.0015 + const dir = mech.angle + (Math.random() - 0.5) * 0.7; + const me = bullet.length; + const RADIUS = (4 + 4 * Math.random()) * b.modBulletSize + bullet[me] = Bodies.circle(mech.pos.x + 30 * Math.cos(mech.angle), mech.pos.y + 30 * Math.sin(mech.angle), RADIUS, { + angle: dir, + inertia: Infinity, + friction: 0, + frictionAir: 0.0005, + restitution: 1, + dmg: 0.14, //damage done in addition to the damage from momentum + lookFrequency: 79 + Math.floor(37 * Math.random()), + endCycle: game.cycle + Math.floor((780 + 360 * Math.random()) * b.modBulletsLastLonger), + classType: "bullet", + collisionFilter: { + category: 0x000100, + mask: 0x010111 //self collide + }, + minDmgSpeed: 0, + lockedOn: null, + isFollowMouse: true, + onDmg() { + this.lockedOn = null + }, + onEnd() {}, + do() { + this.force.y += this.mass * 0.0002; + //find mob targets + if (!(game.cycle % this.lookFrequency)) { + this.lockedOn = null; + let closeDist = Infinity; + for (let i = 0, len = mob.length; i < len; ++i) { + if ( + Matter.Query.ray(map, this.position, mob[i].position).length === 0 && + Matter.Query.ray(body, this.position, mob[i].position).length === 0 + ) { + const TARGET_VECTOR = Matter.Vector.sub(this.position, mob[i].position) + const DIST = Matter.Vector.magnitude(TARGET_VECTOR); + if (DIST < closeDist) { + closeDist = DIST; + this.lockedOn = mob[i] + } + } + } + if (!this.lockedOn) { + //grab a power up if it is (ammo) or (a heal when player is low) + let closeDist = Infinity; + for (let i = 0, len = powerUp.length; i < len; ++i) { + if ( + ((powerUp[i].name !== "field" && powerUp[i].name !== "heal") || (powerUp[i].name === "heal" && mech.health < 0.8)) && + Matter.Query.ray(map, this.position, powerUp[i].position).length === 0 && + Matter.Query.ray(body, this.position, powerUp[i].position).length === 0 + ) { + const TARGET_VECTOR = Matter.Vector.sub(this.position, powerUp[i].position) + const DIST = Matter.Vector.magnitude(TARGET_VECTOR); + if (DIST < closeDist) { + if (DIST < 50) { //eat the power up if close enough + powerUp[i].effect(); + Matter.World.remove(engine.world, powerUp[i]); + powerUp.splice(i, 1); + break; + } + closeDist = DIST; + this.lockedOn = powerUp[i] + } + } + } + } + } + if (this.lockedOn) { //accelerate towards mobs + this.force = Matter.Vector.mult(Matter.Vector.normalise(Matter.Vector.sub(this.position, this.lockedOn.position)), -this.mass * THRUST) + } else { //accelerate towards mouse + this.force = Matter.Vector.mult(Matter.Vector.normalise(Matter.Vector.sub(this.position, game.mouseInGame)), -this.mass * THRUST) + } + // speed cap instead of friction to give more agility + if (this.speed > 6) { + Matter.Body.setVelocity(this, { + x: this.velocity.x * 0.97, + y: this.velocity.y * 0.97 + }); + } + } + }) + b.fireProps(mech.crouch ? 22 : 15, mech.crouch ? 26 : 1, dir, me); //cd , speed + b.drawOneBullet(bullet[me].vertices); + } + }, + ] +}; \ No newline at end of file diff --git a/js/engine.js b/js/engine.js new file mode 100644 index 0000000..d7f3cd4 --- /dev/null +++ b/js/engine.js @@ -0,0 +1,174 @@ +//matter.js *********************************************************** +// module aliases +const Engine = Matter.Engine, + World = Matter.World, + Events = Matter.Events, + Composites = Matter.Composites, + Composite = Matter.Composite, + Constraint = Matter.Constraint, + Vertices = Matter.Vertices, + Query = Matter.Query, + Body = Matter.Body, + Bodies = Matter.Bodies; + +// create an engine +const engine = Engine.create(); +engine.world.gravity.scale = 0; //turn off gravity (it's added back in later) +// engine.velocityIterations = 100 +// engine.positionIterations = 100 +// engine.enableSleeping = true + +// matter events ********************************************************* +//************************************************************************ +//************************************************************************ +//************************************************************************ + +function playerOnGroundCheck(event) { + //runs on collisions events + function enter() { + mech.numTouching++; + if (!mech.onGround) mech.enterLand(); + } + const pairs = event.pairs; + for (let i = 0, j = pairs.length; i != j; ++i) { + let pair = pairs[i]; + if (pair.bodyA === jumpSensor) { + mech.standingOn = pair.bodyB; //keeping track to correctly provide recoil on jump + enter(); + } else if (pair.bodyB === jumpSensor) { + mech.standingOn = pair.bodyA; //keeping track to correctly provide recoil on jump + enter(); + } + } + mech.numTouching = 0; +} + +function playerOffGroundCheck(event) { + //runs on collisions events + function enter() { + if (mech.onGround && mech.numTouching === 0) mech.enterAir(); + } + const pairs = event.pairs; + for (let i = 0, j = pairs.length; i != j; ++i) { + if (pairs[i].bodyA === jumpSensor) { + enter(); + } else if (pairs[i].bodyB === jumpSensor) { + enter(); + } + } +} + +function playerHeadCheck(event) { + //runs on collisions events + if (mech.crouch) { + mech.isHeadClear = true; + const pairs = event.pairs; + for (let i = 0, j = pairs.length; i != j; ++i) { + if (pairs[i].bodyA === headSensor) { + mech.isHeadClear = false; + } else if (pairs[i].bodyB === headSensor) { + mech.isHeadClear = false; + } + } + } +} + +function mobCollisionChecks(event) { + const pairs = event.pairs; + for (let i = 0, j = pairs.length; i != j; i++) { + for (let k = 0; k < mob.length; k++) { + if (mob[k].alive && mech.alive) { + if (pairs[i].bodyA === mob[k]) { + collide(pairs[i].bodyB); + break; + } else if (pairs[i].bodyB === mob[k]) { + collide(pairs[i].bodyA); + break; + } + + function collide(obj) { + //player and mob collision + if (obj === playerBody || obj === playerHead) { + if (mech.damageImmune < mech.cycle) { + //player is immune to mob collision damage for 30 cycles + mech.damageImmune = mech.cycle + 30; + mob[k].foundPlayer(); + let dmg = Math.min(Math.max(0.025 * Math.sqrt(mob[k].mass), 0.05), 0.3) * game.dmgScale; //player damage is capped at 0.3*dmgScale of 1.0 + mech.damage(dmg); + if (mob[k].onHit) mob[k].onHit(k); + game.drawList.push({ + //add dmg to draw queue + x: pairs[i].activeContacts[0].vertex.x, + y: pairs[i].activeContacts[0].vertex.y, + radius: dmg * 500, + color: game.mobDmgColor, + time: game.drawTime + }); + } + //extra kick between player and mob + //this section would be better with forces but they don't work... + let angle = Math.atan2(player.position.y - mob[k].position.y, player.position.x - mob[k].position.x); + Matter.Body.setVelocity(player, { + x: player.velocity.x + 8 * Math.cos(angle), + y: player.velocity.y + 8 * Math.sin(angle) + }); + Matter.Body.setVelocity(mob[k], { + x: mob[k].velocity.x - 8 * Math.cos(angle), + y: mob[k].velocity.y - 8 * Math.sin(angle) + }); + return; + } + //bullet mob collisions + if (obj.classType === "bullet" && obj.speed > obj.minDmgSpeed) { + mob[k].foundPlayer(); + const dmg = b.dmgScale * (obj.dmg + 0.15 * obj.mass * Matter.Vector.magnitude(Matter.Vector.sub(mob[k].velocity, obj.velocity))); + // console.log(dmg) + mob[k].damage(dmg); + obj.onDmg(); //some bullets do actions when they hits things, like despawn + game.drawList.push({ + //add dmg to draw queue + x: pairs[i].activeContacts[0].vertex.x, + y: pairs[i].activeContacts[0].vertex.y, + radius: Math.sqrt(dmg) * 40, + color: game.playerDmgColor, + time: game.drawTime + }); + return; + } + //mob and body collisions + if (obj.classType === "body" && obj.speed > 5) { + const v = Matter.Vector.magnitude(Matter.Vector.sub(mob[k].velocity, obj.velocity)); + if (v > 8) { + let dmg = b.dmgScale * v * Math.sqrt(obj.mass) * 0.05; + mob[k].damage(dmg); + if (mob[k].distanceToPlayer2() < 1000000) mob[k].foundPlayer(); + game.drawList.push({ + //add dmg to draw queue + x: pairs[i].activeContacts[0].vertex.x, + y: pairs[i].activeContacts[0].vertex.y, + radius: Math.sqrt(dmg) * 40, + color: game.playerDmgColor, + time: game.drawTime + }); + return; + } + } + } + } + } + } +} + +//determine if player is on the ground +Events.on(engine, "collisionStart", function (event) { + playerOnGroundCheck(event); + playerHeadCheck(event); + mobCollisionChecks(event); +}); +Events.on(engine, "collisionActive", function (event) { + playerOnGroundCheck(event); + playerHeadCheck(event); +}); +Events.on(engine, "collisionEnd", function (event) { + playerOffGroundCheck(event); +}); \ No newline at end of file diff --git a/js/game.js b/js/game.js new file mode 100644 index 0000000..666e0cb --- /dev/null +++ b/js/game.js @@ -0,0 +1,1028 @@ +// game Object ******************************************************** +//********************************************************************* +const game = { + loop() { + game.cycle++; //tracks game cycles + mech.cycle++; //tracks player cycles //used to alow time to stop for everything, but the player + if (game.clearNow) { + game.clearNow = false; + game.clearMap(); + level.start(); + } + game.gravity(); + Engine.update(engine, game.delta); + game.wipe(); + game.textLog(); + mech.keyMove(); + level.checkZones(); + level.checkQuery(); + mech.move(); + mech.look(); + game.fallChecks(); + ctx.save(); + game.camera(); + if (game.testing) { + mech.draw(); + game.draw.wireFrame(); + game.draw.cons(); + game.draw.testing(); + game.drawCircle(); + ctx.restore(); + game.getCoords.out(); + game.testingOutput(); + } else { + level.drawFillBGs(); + level.exit.draw(); + level.enter.draw(); + game.draw.powerUp(); + mobs.draw(); + game.draw.cons(); + game.draw.body(); + mobs.loop(); + mech.draw(); + mech.hold(); + level.drawFills(); + game.draw.drawMapPath(); + b.draw(); + b.fire(); + game.drawCircle(); + ctx.restore(); + } + game.drawCursor(); + }, + mouse: { + x: canvas.width / 2, + y: canvas.height / 2 + }, + mouseInGame: { + x: 0, + y: 0 + }, + levelsCleared: 0, + g: 0.001, + dmgScale: 1, + accelScale: 1, + CDScale: 1, + lookFreqScale: 1, + onTitlePage: true, + paused: false, + testing: false, //testing mode: shows wireframe and some variables + cycle: 0, //total cycles, 60 per second + fpsCap: 72, //limits frames per second to 144/2=72, on most monitors the fps is capped at 60fps by the hardware + fpsCapDefault: 72, //use to change fpsCap back to normal after a hit from a mob + cyclePaused: 0, + fallHeight: 3000, //below this y position the player dies + lastTimeStamp: 0, //tracks time stamps for measuring delta + delta: 1000 / 60, //speed of game engine //looks like it has to be 16 to match player input + buttonCD: 0, + // dropFPS(cap = 40, time = 15) { + // game.fpsCap = cap + // game.fpsInterval = 1000 / game.fpsCap; + // game.defaultFPSCycle = game.cycle + time + // const normalFPS = function () { + // if (game.defaultFPSCycle < game.cycle) { + // game.fpsCap = 72 + // game.fpsInterval = 1000 / game.fpsCap; + // } else { + // requestAnimationFrame(normalFPS); + // } + // }; + // requestAnimationFrame(normalFPS); + // }, + drawCursor() { + const size = 10; + ctx.beginPath(); + ctx.moveTo(game.mouse.x - size, game.mouse.y); + ctx.lineTo(game.mouse.x + size, game.mouse.y); + ctx.moveTo(game.mouse.x, game.mouse.y - size); + ctx.lineTo(game.mouse.x, game.mouse.y + size); + ctx.lineWidth = 2; + ctx.strokeStyle = "#000"; //'rgba(0,0,0,0.4)' + ctx.stroke(); // Draw it + }, + drawList: [], //so you can draw a first frame of explosions.. I know this is bad + drawTime: 8, //how long circles are drawn. use to push into drawlist.time + mobDmgColor: "rgba(255,0,0,0.7)", //used top push into drawList.color + playerDmgColor: "rgba(0,0,0,0.7)", //used top push into drawList.color + drawCircle() { + //draws a circle for two cycles, used for showing damage mostly + let i = this.drawList.length; + while (i--) { + ctx.beginPath(); //draw circle + ctx.arc(this.drawList[i].x, this.drawList[i].y, this.drawList[i].radius, 0, 2 * Math.PI); + ctx.fillStyle = this.drawList[i].color; + ctx.fill(); + if (this.drawList[i].time) { + //remove when timer runs out + this.drawList[i].time--; + } else { + this.drawList.splice(i, 1); + } + } + }, + lastLogTime: 0, + lastLogTimeBig: 0, + boldActiveGunHUD() { + if (b.inventory.length > 0) { + for (let i = 0, len = b.inventory.length; i < len; ++i) { + // document.getElementById(b.inventory[i]).style.fontSize = "25px"; + document.getElementById(b.inventory[i]).style.opacity = "0.3"; + } + // document.getElementById(b.activeGun).style.fontSize = "30px"; + if (document.getElementById(b.activeGun)) document.getElementById(b.activeGun).style.opacity = "1"; + } + }, + updateGunHUD() { + for (let i = 0, len = b.inventory.length; i < len; ++i) { + document.getElementById(b.inventory[i]).innerHTML = b.guns[b.inventory[i]].name + " - " + b.guns[b.inventory[i]].ammo; + } + }, + makeGunHUD() { + //remove all nodes + const myNode = document.getElementById("guns"); + while (myNode.firstChild) { + myNode.removeChild(myNode.firstChild); + } + //add nodes + for (let i = 0, len = b.inventory.length; i < len; ++i) { + const node = document.createElement("div"); + node.setAttribute("id", b.inventory[i]); + let textnode = document.createTextNode(b.guns[b.inventory[i]].name + " - " + b.guns[b.inventory[i]].ammo); + node.appendChild(textnode); + document.getElementById("guns").appendChild(node); + } + game.boldActiveGunHUD(); + }, + updateModHUD() { + let text = "" + for (let i = 0, len = b.mods.length; i < len; i++) { //add mods + if (b.mods[i].have) { + if (text) text += "
" //add a new line, but not on the first line + text += b.mods[i].name + } + } + document.getElementById("mods").innerHTML = text + }, + makeTextLog(text, time = 180) { + document.getElementById("text-log").innerHTML = text; + document.getElementById("text-log").style.opacity = 1; + game.lastLogTime = mech.cycle + time; + }, + textLog() { + if (game.lastLogTime && game.lastLogTime < mech.cycle) { + game.lastLogTime = 0; + // document.getElementById("text-log").innerHTML = " "; + document.getElementById("text-log").style.opacity = 0; + } + }, + // timing: function() { + // this.cycle++; //tracks game cycles + // //delta is used to adjust forces on game slow down; + // this.delta = (engine.timing.timestamp - this.lastTimeStamp) / 16.666666666666; + // this.lastTimeStamp = engine.timing.timestamp; //track last engine timestamp + // }, + nextGun() { + if (b.inventory.length > 0) { + b.inventoryGun++; + if (b.inventoryGun > b.inventory.length - 1) b.inventoryGun = 0; + game.switchGun(); + } + }, + previousGun() { + if (b.inventory.length > 0) { + b.inventoryGun--; + if (b.inventoryGun < 0) b.inventoryGun = b.inventory.length - 1; + game.switchGun(); + } + }, + switchGun() { + b.activeGun = b.inventory[b.inventoryGun]; + game.updateGunHUD(); + game.boldActiveGunHUD(); + // mech.drop(); + }, + // tips = [ + // "You can throw blocks at dangerous speeds by holding the right mouse or spacebar.", + // "You can use your field to block damage. (right mouse or spacebar)", + // "Explosive weapons, like the grenade are good at clearing groups.", + // "Larger and faster bullets do more damage.", + // "You take more damage from colliding with larger baddies", + // "Holding large blocks slows down the player.", + // "holding blocks prevents the field energy from regenerating.", + // `There are ${mech.fieldUpgrades.length-1} possible field upgrades.`, + // `There are ${b.guns.length} different guns.`, + // "You keep field upgrades after you die.", + // "Unique level bosses always drop a field upgrade if you don't have one.", + // "You jump higher if you hold down the jump button.", + // "Crouching while firing makes bullets go faster, but slows the rate of fire.", + // ] + keyPress() { + if (keys[189]) { + // - key + game.zoomScale /= 0.9; + game.setZoom(); + } else if (keys[187]) { + // = key + game.zoomScale *= 0.9; + game.setZoom(); + } + + //full screen toggle + // if (keys[13]) { + // //enter key + // var doc = window.document; + // var docEl = doc.documentElement; + + // var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen; + // var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen; + + // if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) { + // requestFullScreen.call(docEl); + // } else { + // cancelFullScreen.call(doc); + // } + // setupCanvas(); + // } + + + if (keys[69]) { + // e swap to next active gun + game.nextGun(); + } else if (keys[81]) { + //q swap to previous active gun + game.previousGun(); + } + + if (keys[80]) { + //p for pause + if (game.paused) { + game.paused = false; + requestAnimationFrame(cycle); + } else { + game.paused = true; + game.makeTextLog("

PAUSED

", 1); + // let text = "

PAUSED


" + // //output current mod, field, and gun info when paused + // if (mech.fieldMode !== 0) text += "

" + mech.fieldUpgrades[mech.fieldMode].name + "
" + mech.fieldUpgrades[mech.fieldMode].description + "

" + // if (b.mod !== null) text += "

" + b.mods[b.mod].name + "
" + b.mods[b.mod].description + "

" + // if (b.activeGun !== null) text += "

" + b.guns[b.activeGun].name + "
" + b.guns[b.activeGun].description + "

" + // text += "
" + // game.makeTextLog(text, 1); + } + } + + //toggle testing mode + if (keys[84]) { + // 84 = t + if (this.testing) { + this.testing = false; + } else { + this.testing = true; + } + } else if (this.testing) { + //only in testing mode + + if (keys[70]) { //cycle fields with F + if (mech.fieldMode === mech.fieldUpgrades.length - 1) { + mech.fieldUpgrades[0].effect() + } else { + mech.fieldUpgrades[mech.fieldMode + 1].effect() + } + } + if (keys[71]) { // give all guns with G + b.giveGuns("all", 1000) + } + if (keys[72]) { // power ups with H + powerUps.spawn(game.mouseInGame.x, game.mouseInGame.y, "gun"); + powerUps.spawn(game.mouseInGame.x, game.mouseInGame.y, "gun"); + powerUps.spawn(game.mouseInGame.x, game.mouseInGame.y, "ammo"); + powerUps.spawn(game.mouseInGame.x, game.mouseInGame.y, "field"); + powerUps.spawn(game.mouseInGame.x, game.mouseInGame.y, "mod"); + powerUps.spawn(game.mouseInGame.x, game.mouseInGame.y, "heal"); + powerUps.spawn(game.mouseInGame.x, game.mouseInGame.y, "heal"); + } + if (keys[89]) { //add all mods with y + for (let i = 0; i < b.mods.length; i++) { + if (!b.mods[i].have) { + b.mods[i].effect() + b.mods[i].have = true + } + } + game.updateModHUD(); + } + if (keys[82]) { // teleport to mouse with R + Matter.Body.setPosition(player, this.mouseInGame); + Matter.Body.setVelocity(player, { + x: 0, + y: 0 + }); + } + } + }, + zoom: null, + zoomScale: 1000, + setZoom(zoomScale = game.zoomScale) { //use in window resize in index.js + game.zoomScale = zoomScale + game.zoom = canvas.height / zoomScale; //sets starting zoom scale + }, + zoomTransition(newZoomScale, step = 2) { + const isBigger = (newZoomScale - game.zoomScale > 0) ? true : false; + requestAnimationFrame(zLoop); + const currentLevel = level.onLevel + + function zLoop() { + if (currentLevel != level.onLevel) return //stop the zoom if player goes to a new level + + if (isBigger) { + game.zoomScale += step + if (game.zoomScale >= newZoomScale) { + game.setZoom(newZoomScale); + return + } + } else { + game.zoomScale -= step + if (game.zoomScale <= newZoomScale) { + game.setZoom(newZoomScale); + return + } + } + + game.setZoom(); + requestAnimationFrame(zLoop); + } + }, + camera() { + ctx.translate(canvas.width2, canvas.height2); //center + ctx.scale(game.zoom, game.zoom); //zoom in once centered + ctx.translate(-canvas.width2 + mech.transX, -canvas.height2 + mech.transY); //translate + //calculate in game mouse position by undoing the zoom and translations + game.mouseInGame.x = (game.mouse.x - canvas.width2) / game.zoom + canvas.width2 - mech.transX; + game.mouseInGame.y = (game.mouse.y - canvas.height2) / game.zoom + canvas.height2 - mech.transY; + }, + zoomInFactor: 0, + startZoomIn(time = 180) { + game.zoom = 0; + let count = 0; + requestAnimationFrame(zLoop); + + function zLoop() { + game.zoom += canvas.height / game.zoomScale / time; + count++; + if (count < time) { + requestAnimationFrame(zLoop); + } else { + game.setZoom(); + } + } + }, + wipe() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // ctx.fillStyle = "rgba(255,255,255,1)"; + // ctx.globalCompositeOperation = "difference"; + // ctx.fillRect(0, 0, canvas.width, canvas.height); + // ctx.globalCompositeOperation = "source-over"; + + // ctx.globalAlpha = (mech.health < 0.7) ? (mech.health+0.3)*(mech.health+0.3) : 1 + // if (mech.health < 0.7) { + // ctx.globalAlpha= 0.3 + mech.health + // ctx.fillStyle = document.body.style.backgroundColor + // ctx.fillRect(0, 0, canvas.width, canvas.height); + // ctx.globalAlpha=1; + // } else { + // ctx.clearRect(0, 0, canvas.width, canvas.height); + // } + //ctx.fillStyle = "rgba(255,255,255," + (1 - Math.sqrt(player.speed)*0.1) + ")"; + //ctx.fillStyle = "rgba(255,255,255,0.4)"; + //ctx.fillRect(0, 0, canvas.width, canvas.height); + }, + gravity() { + function addGravity(bodies, magnitude) { + for (var i = 0; i < bodies.length; i++) { + bodies[i].force.y += bodies[i].mass * magnitude; + } + } + addGravity(powerUp, game.g); + addGravity(body, game.g); + player.force.y += player.mass * mech.gravity; + }, + reset() { + 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; + } + b.activeGun = null; + b.setModDefaults(); //remove mods + game.updateModHUD(); + mech.fieldUpgrades[0].effect(); //set to default field + game.paused = false; + engine.timing.timeScale = 1; + game.dmgScale = 1; + b.dmgScale = 0.7; + game.makeGunHUD(); + mech.drop(); + mech.addHealth(1); + mech.alive = true; + level.onLevel = 0; + game.levelsCleared = 0; + game.clearNow = true; + document.getElementById("text-log").style.opacity = 0; + document.getElementById("fade-out").style.opacity = 0; + document.title = "n-gon"; + if (!mech.fieldMode) mech.fieldUpgrades[0].effect(); //reset to starting field? or let them keep the field + }, + firstRun: true, + splashReturn() { + game.onTitlePage = true; + // document.getElementById('splash').onclick = 'run(this)'; + document.getElementById("splash").onclick = function () { + game.startGame(); + }; + document.getElementById("controls").style.display = "inline"; + document.getElementById("splash").style.display = "inline"; + document.getElementById("dmg").style.display = "none"; + document.getElementById("health-bg").style.display = "none"; + document.body.style.cursor = "auto"; + }, + fpsInterval: 0, //set in startGame + then: null, + startGame() { + game.onTitlePage = false; + document.getElementById("controls").style.display = "none"; + document.getElementById("splash").onclick = null; //removes the onclick effect so the function only runs once + document.getElementById("splash").style.display = "none"; //hides the element that spawned the function + document.getElementById("dmg").style.display = "inline"; + document.getElementById("health-bg").style.display = "inline"; + + // window.onmousedown = function (e) { + // //mouse up event in set in index.js + + // // game.mouseDown = true; + // if (e.which === 3) { + // game.mouseDownRight = true; + // } else { + // game.mouseDown = true; + // } + // // keep this disabled unless building maps + // // if (!game.mouseDown){ + // // game.getCoords.pos1.x = Math.round(game.mouseInGame.x / 25) * 25; + // // game.getCoords.pos1.y = Math.round(game.mouseInGame.y / 25) * 25; + // // } + + // // mech.throw(); + // }; + + document.body.style.cursor = "none"; + if (this.firstRun) { + mech.spawn(); //spawns the player + b.setModDefaults(); //doesn't run on reset so that gun mods carry over to new runs + level.levels = shuffle(level.levels); //shuffles order of maps + level.levels.unshift("bosses"); //add bosses level to the end of the randomized levels list + } + game.reset(); + game.firstRun = false; + + + //setup FPS cap + game.fpsInterval = 1000 / game.fpsCap; + game.then = Date.now(); + requestAnimationFrame(cycle); //starts game loop + }, + clearNow: false, + clearMap() { + //if player is holding something this remembers it before it gets deleted + let holdTarget; + if (mech.holdingTarget) { + holdTarget = mech.holdingTarget; + } + mech.drop(); + level.fill = []; + level.fillBG = []; + level.zones = []; + level.queryList = []; + this.drawList = []; + + function removeAll(array) { + for (let i = 0; i < array.length; ++i) Matter.World.remove(engine.world, array[i]); + } + removeAll(map); + map = []; + removeAll(body); + body = []; + removeAll(mob); + mob = []; + removeAll(powerUp); + powerUp = []; + removeAll(cons); + cons = []; + removeAll(consBB); + consBB = []; + removeAll(bullet); + bullet = []; + // if player was holding something this makes a new copy to hold + if (holdTarget) { + len = body.length; + body[len] = Matter.Bodies.fromVertices(0, 0, holdTarget.vertices, { + friction: holdTarget.friction, + frictionAir: holdTarget.frictionAir, + frictionStatic: holdTarget.frictionStatic + }); + mech.holdingTarget = body[len]; + } + }, + getCoords: { + //used when building maps, outputs a draw rect command to console, only works in testing mode + pos1: { + x: 0, + y: 0 + }, + pos2: { + x: 0, + y: 0 + }, + out() { + if (keys[49]) { + this.pos1.x = Math.round(game.mouseInGame.x / 25) * 25; + this.pos1.y = Math.round(game.mouseInGame.y / 25) * 25; + } + if (keys[50]) { + //press 1 in the top left; press 2 in the bottom right;copy command from console + this.pos2.x = Math.round(game.mouseInGame.x / 25) * 25; + this.pos2.y = Math.round(game.mouseInGame.y / 25) * 25; + window.getSelection().removeAllRanges(); + var range = document.createRange(); + range.selectNode(document.getElementById("test")); + window.getSelection().addRange(range); + document.execCommand("copy"); + window.getSelection().removeAllRanges(); + console.log(`spawn.mapRect(${this.pos1.x}, ${this.pos1.y}, ${this.pos2.x - this.pos1.x}, ${this.pos2.y - this.pos1.y}); //`); + } + } + }, + fallChecks() { + // if 4000px deep + if (mech.pos.y > game.fallHeight) { + mech.death(); + // if (b.modNonEuclidean) { + // Matter.Body.setPosition(player, { + // x: player.position.x, + // y: player.position.y - 12000 + // }); + // Matter.Body.setVelocity(player, { + // x: player.velocity.x, + // y: player.velocity.y * 0 + // }); + + // mech.pos.x = player.position.x; + // mech.pos.y = playerBody.position.y - mech.yOff; + + // //smoothed mouse look translations + // const scale = 10; + // mech.transSmoothX = canvas.width2 - mech.pos.x - (game.mouse.x - canvas.width2) * scale; + // mech.transSmoothY = canvas.height2 - mech.pos.y - (game.mouse.y - canvas.height2) * scale; + + // mech.transX = mech.transSmoothX + // mech.transY = mech.transSmoothY + + // } else { + // mech.death(); + // } + } + + if (!(mech.cycle % 420)) { + remove = function (who) { + let i = who.length; + while (i--) { + if (who[i].position.y > game.fallHeight) { + Matter.World.remove(engine.world, who[i]); + who.splice(i, 1); + } + } + }; + remove(mob); + remove(body); + remove(powerUp); + } + }, + testingOutput() { + ctx.textAlign = "right"; + ctx.fillStyle = "#000"; + let line = 140; + const x = canvas.width - 5; + ctx.fillText("T: exit testing mode", x, line); + line += 20; + ctx.fillText("Y: give all mods", x, line); + line += 20; + ctx.fillText("R: teleport to mouse", x, line); + line += 20; + ctx.fillText("F: cycle field", x, line); + line += 20; + ctx.fillText("G: give all guns", x, line); + line += 20; + ctx.fillText("H: spawn power ups", x, line); + line += 30; + + ctx.fillText("cycle: " + game.cycle, x, line); + line += 20; + ctx.fillText("player cycle: " + mech.cycle, x, line); + line += 20; + ctx.fillText("x: " + player.position.x.toFixed(0), x, line); + line += 20; + ctx.fillText("y: " + player.position.y.toFixed(0), x, line); + line += 20; + ctx.fillText("Vx: " + mech.Vx.toFixed(2), x, line); + line += 20; + ctx.fillText("Vy: " + mech.Vy.toFixed(2), x, line); + line += 20; + ctx.fillText("Fx: " + player.force.x.toFixed(3), x, line); + line += 20; + ctx.fillText("Fy: " + player.force.y.toFixed(3), x, line); + line += 20; + ctx.fillText("yOff: " + mech.yOff.toFixed(1), x, line); + line += 20; + ctx.fillText("mass: " + player.mass.toFixed(1), x, line); + line += 20; + ctx.fillText("onGround: " + mech.onGround, x, line); + line += 20; + ctx.fillText("crouch: " + mech.crouch, x, line); + line += 20; + ctx.fillText("isHeadClear: " + mech.isHeadClear, x, line); + line += 20; + ctx.fillText("frictionAir: " + player.frictionAir.toFixed(3), x, line); + line += 20; + ctx.fillText("stepSize: " + mech.stepSize.toFixed(2), x, line); + line += 20; + ctx.fillText("zoom: " + this.zoom.toFixed(4), x, line); + line += 20; + ctx.textAlign = "center"; + ctx.fillText(`(${this.mouseInGame.x.toFixed(1)}, ${this.mouseInGame.y.toFixed(1)})`, this.mouse.x, this.mouse.y - 20); + }, + draw: { + powerUp() { + // draw power up + // ctx.globalAlpha = 0.4 * Math.sin(mech.cycle * 0.15) + 0.6; + // for (let i = 0, len = powerUp.length; i < len; ++i) { + // let vertices = powerUp[i].vertices; + // ctx.beginPath(); + // 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 = powerUp[i].color; + // ctx.fill(); + // } + // ctx.globalAlpha = 1; + ctx.globalAlpha = 0.4 * Math.sin(mech.cycle * 0.15) + 0.6; + for (let i = 0, len = powerUp.length; i < len; ++i) { + ctx.beginPath(); + ctx.arc(powerUp[i].position.x, powerUp[i].position.y, powerUp[i].size, 0, 2 * Math.PI); + ctx.fillStyle = powerUp[i].color; + ctx.fill(); + } + ctx.globalAlpha = 1; + }, + // map: function() { + // ctx.beginPath(); + // for (let i = 0, len = map.length; i < len; ++i) { + // let vertices = map[i].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 = "#444"; + // ctx.fill(); + // }, + mapPath: null, //holds the path for the map to speed up drawing + setPaths() { + //runs at each new level to store the path for the map since the map doesn't change + this.mapPath = new Path2D(); + for (let i = 0, len = map.length; i < len; ++i) { + let vertices = map[i].vertices; + this.mapPath.moveTo(vertices[0].x, vertices[0].y); + for (let j = 1; j < vertices.length; j += 1) { + this.mapPath.lineTo(vertices[j].x, vertices[j].y); + } + this.mapPath.lineTo(vertices[0].x, vertices[0].y); + } + }, + mapFill: "#444", + bodyFill: "#999", + bodyStroke: "#222", + drawMapPath() { + ctx.fillStyle = this.mapFill; + ctx.fill(this.mapPath); + }, + + seeEdges() { + const eye = { + x: mech.pos.x + 20 * Math.cos(mech.angle), + y: mech.pos.y + 20 * Math.sin(mech.angle) + }; + //find all vertex nodes in range and in LOS + findNodes = function (domain, center) { + let nodes = []; + for (let i = 0; i < domain.length; ++i) { + let vertices = domain[i].vertices; + + for (let j = 0, len = vertices.length; j < len; j++) { + //calculate distance to player + const dx = vertices[j].x - center.x; + const dy = vertices[j].y - center.y; + if (dx * dx + dy * dy < 800 * 800 && Matter.Query.ray(domain, center, vertices[j]).length === 0) { + nodes.push(vertices[j]); + } + } + } + return nodes; + }; + let nodes = findNodes(map, eye); + //sort node list by angle to player + nodes.sort(function (a, b) { + //sub artan2 from player loc + const dx = a.x - eye.x; + const dy = a.y - eye.y; + return Math.atan2(dy, dx) - Math.atan2(dy, dx); + }); + //draw nodes + ctx.lineWidth = 2; + ctx.strokeStyle = "#000"; + ctx.beginPath(); + for (let i = 0; i < nodes.length; ++i) { + ctx.lineTo(nodes[i].x, nodes[i].y); + } + ctx.stroke(); + }, + see() { + const vertexCollision = function ( + v1, + v1End, + domain, + best = { + x: null, + y: null, + dist2: Infinity, + who: null, + v1: null, + v2: null + } + ) { + 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 && (!domain[i].mob || domain[i].alive)) { + best = { + x: results.x, + y: results.y, + dist2: dist2, + who: domain[i], + v1: vertices[0], + v2: vertices[len] + }; + } + } + } + return best; + }; + const range = 3000; + ctx.beginPath(); + for (let i = 0; i < Math.PI * 2; i += Math.PI / 2 / 100) { + const cosAngle = Math.cos(mech.angle + i); + const sinAngle = Math.sin(mech.angle + i); + + const start = { + x: mech.pos.x + 20 * cosAngle, + y: mech.pos.y + 20 * sinAngle + }; + const end = { + x: mech.pos.x + range * cosAngle, + y: mech.pos.y + range * sinAngle + }; + let result = vertexCollision(start, end, map); + result = vertexCollision(start, end, body, result); + result = vertexCollision(start, end, mob, result); + + if (result.dist2 < range * range) { + // ctx.arc(result.x, result.y, 2, 0, 2 * Math.PI); + ctx.lineTo(result.x, result.y); + } else { + // ctx.arc(end.x, end.y, 2, 0, 2 * Math.PI); + ctx.lineTo(end.x, end.y); + } + } + // ctx.lineWidth = 1; + // ctx.strokeStyle = "#000"; + // ctx.stroke(); + ctx.fillStyle = "rgba(0,0,0,0.3)"; + ctx.fillStyle = "#fff"; + ctx.fill(); + ctx.clip(); + }, + body() { + ctx.beginPath(); + for (let i = 0, len = body.length; i < len; ++i) { + let vertices = body[i].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.lineWidth = 2; + ctx.fillStyle = this.bodyFill; + ctx.fill(); + ctx.strokeStyle = this.bodyStroke; + ctx.stroke(); + }, + cons() { + ctx.beginPath(); + for (let i = 0, len = cons.length; i < len; ++i) { + ctx.moveTo(cons[i].pointA.x, cons[i].pointA.y); + ctx.lineTo(cons[i].bodyB.position.x, cons[i].bodyB.position.y); + } + for (let i = 0, len = consBB.length; i < len; ++i) { + ctx.moveTo(consBB[i].bodyA.position.x, consBB[i].bodyA.position.y); + ctx.lineTo(consBB[i].bodyB.position.x, consBB[i].bodyB.position.y); + } + ctx.lineWidth = 2; + // ctx.strokeStyle = "#999"; + ctx.strokeStyle = "rgba(0,0,0,0.15)"; + ctx.stroke(); + }, + wireFrame() { + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = "#999"; + const bodies = Composite.allBodies(engine.world); + ctx.beginPath(); + for (let i = 0; i < bodies.length; ++i) { + //ctx.fillText(bodies[i].id,bodies[i].position.x,bodies[i].position.y); //shows the id of every body + let vertices = bodies[i].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.lineWidth = 1; + ctx.strokeStyle = "#000"; + ctx.stroke(); + }, + testing() { + //zones + ctx.beginPath(); + for (let i = 0, len = level.zones.length; i < len; ++i) { + ctx.rect(level.zones[i].x1, level.zones[i].y1 + 70, level.zones[i].x2 - level.zones[i].x1, level.zones[i].y2 - level.zones[i].y1); + } + ctx.fillStyle = "rgba(0, 255, 0, 0.3)"; + ctx.fill(); + //query zones + ctx.beginPath(); + for (let i = 0, len = level.queryList.length; i < len; ++i) { + ctx.rect( + level.queryList[i].bounds.max.x, + level.queryList[i].bounds.max.y, + level.queryList[i].bounds.min.x - level.queryList[i].bounds.max.x, + level.queryList[i].bounds.min.y - level.queryList[i].bounds.max.y + ); + } + ctx.fillStyle = "rgba(0, 0, 255, 0.2)"; + ctx.fill(); + //jump + ctx.beginPath(); + let bodyDraw = jumpSensor.vertices; + ctx.moveTo(bodyDraw[0].x, bodyDraw[0].y); + for (let j = 1; j < bodyDraw.length; ++j) { + ctx.lineTo(bodyDraw[j].x, bodyDraw[j].y); + } + ctx.lineTo(bodyDraw[0].x, bodyDraw[0].y); + ctx.fillStyle = "rgba(255, 0, 0, 0.3)"; + ctx.fill(); + ctx.strokeStyle = "#000"; + ctx.stroke(); + //main body + ctx.beginPath(); + bodyDraw = playerBody.vertices; + ctx.moveTo(bodyDraw[0].x, bodyDraw[0].y); + for (let j = 1; j < bodyDraw.length; ++j) { + ctx.lineTo(bodyDraw[j].x, bodyDraw[j].y); + } + ctx.lineTo(bodyDraw[0].x, bodyDraw[0].y); + ctx.fillStyle = "rgba(0, 255, 255, 0.3)"; + ctx.fill(); + ctx.stroke(); + //head + ctx.beginPath(); + bodyDraw = playerHead.vertices; + ctx.moveTo(bodyDraw[0].x, bodyDraw[0].y); + for (let j = 1; j < bodyDraw.length; ++j) { + ctx.lineTo(bodyDraw[j].x, bodyDraw[j].y); + } + ctx.lineTo(bodyDraw[0].x, bodyDraw[0].y); + ctx.fillStyle = "rgba(255, 255, 0, 0.3)"; + ctx.fill(); + ctx.stroke(); + //head sensor + ctx.beginPath(); + bodyDraw = headSensor.vertices; + ctx.moveTo(bodyDraw[0].x, bodyDraw[0].y); + for (let j = 1; j < bodyDraw.length; ++j) { + ctx.lineTo(bodyDraw[j].x, bodyDraw[j].y); + } + ctx.lineTo(bodyDraw[0].x, bodyDraw[0].y); + ctx.fillStyle = "rgba(0, 0, 255, 0.3)"; + ctx.fill(); + ctx.stroke(); + } + }, + checkLineIntersection(v1, v1End, v2, v2End) { + // if the lines intersect, the result contains the x and y of the intersection (treating the lines as infinite) and booleans for whether line segment 1 or line segment 2 contain the point + let denominator, a, b, numerator1, numerator2; + let result = { + x: null, + y: null, + onLine1: false, + onLine2: false + }; + denominator = (v2End.y - v2.y) * (v1End.x - v1.x) - (v2End.x - v2.x) * (v1End.y - v1.y); + if (denominator == 0) { + return result; + } + a = v1.y - v2.y; + b = v1.x - v2.x; + numerator1 = (v2End.x - v2.x) * a - (v2End.y - v2.y) * b; + numerator2 = (v1End.x - v1.x) * a - (v1End.y - v1.y) * b; + a = numerator1 / denominator; + b = numerator2 / denominator; + + // if we cast these lines infinitely in both directions, they intersect here: + result.x = v1.x + a * (v1End.x - v1.x); + result.y = v1.y + a * (v1End.y - v1.y); + // if line1 is a segment and line2 is infinite, they intersect if: + if (a > 0 && a < 1) result.onLine1 = true; + // if line2 is a segment and line1 is infinite, they intersect if: + if (b > 0 && b < 1) result.onLine2 = true; + // if line1 and line2 are segments, they intersect if both of the above are true + return result; + }, + //was used in level design + buildingUp(e) { + if (game.mouseDown) { + game.getCoords.pos2.x = Math.round(game.mouseInGame.x / 25) * 25; + game.getCoords.pos2.y = Math.round(game.mouseInGame.y / 25) * 25; + let out; + + //body rect mode + out = `spawn.mapRect(${game.getCoords.pos1.x}, ${game.getCoords.pos1.y}, ${game.getCoords.pos2.x - game.getCoords.pos1.x}, ${game.getCoords.pos2.y - + game.getCoords.pos1.y});`; + + //mob spawn + //out = `spawn.randomMob(${game.getCoords.pos1.x}, ${game.getCoords.pos1.y}, 0.3);` + + //draw foreground + //out = `level.fill.push({ x: ${game.getCoords.pos1.x}, y: ${game.getCoords.pos1.y}, width: ${game.getCoords.pos2.x-game.getCoords.pos1.x}, height: ${game.getCoords.pos2.y-game.getCoords.pos1.y}, color: "rgba(0,0,0,0.1)"});`; + + //draw background fill + //out = `level.fillBG.push({ x: ${game.getCoords.pos1.x}, y: ${game.getCoords.pos1.y}, width: ${game.getCoords.pos2.x-game.getCoords.pos1.x}, height: ${game.getCoords.pos2.y-game.getCoords.pos1.y}, color: "#ccc"});`; + + //svg mode + //out = 'rect x="'+game.getCoords.pos1.x+'" y="'+ game.getCoords.pos1.y+'" width="'+(game.getCoords.pos2.x-game.getCoords.pos1.x)+'" height="'+(game.getCoords.pos2.y-game.getCoords.pos1.y)+'"'; + + console.log(out); + // document.getElementById("copy-this").innerHTML = out + // + // window.getSelection().removeAllRanges(); + // var range = document.createRange(); + // range.selectNode(document.getElementById('copy-this')); + // window.getSelection().addRange(range); + // document.execCommand('copy') + // window.getSelection().removeAllRanges(); + } + } +}; \ No newline at end of file diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..c5e0044 --- /dev/null +++ b/js/index.js @@ -0,0 +1,197 @@ +"use strict"; +/* TODO: ******************************************* +***************************************************** + +make power ups keep moving to player if the field is turned off + +levels spawn by having the map aspects randomly fly into place + +new map with repeating endlessness + get ideas from Manifold Garden game + if falling, get teleported above the map + I tried it, but had trouble getting the camera to adjust to the teleportation + this can apply to blocks mobs, and power ups as well + +Find a diegetic way to see player damage (and or field meter too) + a health meter, like the field meter above player? (doesn't work with the field meter) + +field power up effects + field allows player to hold and throw living mobs + +mod power ups ideas + bullet on mob damage effects + add to the array mob.do new mob behaviors + add a damage over time + add a freeze +give mobs more animal-like behaviors + like rainworld + give mobs something to do when they don't see player + explore map + eat power ups + drop power up (if killed after eating one) + mobs some times aren't aggressive + when low on life or after taking a large hit + mobs can fight each other + this might be hard to code + isolated mobs try to group up. + +game mechanics + mechanics that support the physics engine + add rope/constraint + get ideas from game: limbo / inside + environmental hazards + laser + lava + button / switch + door + fizzler + moving platform + map zones + water + low friction ground + bouncy ground + +track foot positions with velocity better as the player walks/crouch/runs + +Boss ideas + boss grows and spilt, if you don't kill it fast + sensor that locks you in after you enter the boss room + boss that eats other mobs and gains stats from them + chance to spawn on any level (past level 5) + boss that knows how to shoot (player) bullets that collide with player + overwrite custom engine collision bullet mob function. + + + +// collision info: + category mask +powerUp: 0x100000 0x100001 +body: 0x010000 0x011111 +player: 0x001000 0x010011 +bullet: 0x000100 0x010011 +mob: 0x000010 0x011111 +mobBullet: 0x000010 0x011101 +mobShield: 0x000010 0x001100 +map: 0x000001 0x111111 + + + + +*/ + +//set up canvas +var canvas = document.getElementById("canvas"); +//using "const" causes problems in safari when an ID shares the same name. +const ctx = canvas.getContext("2d"); +document.body.style.backgroundColor = "#fff"; + +//disable pop up menu on right click +document.oncontextmenu = function () { + return false; +} + +function setupCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + canvas.width2 = canvas.width / 2; //precalculated because I use this often (in mouse look) + canvas.height2 = canvas.height / 2; + canvas.diagonal = Math.sqrt(canvas.width2 * canvas.width2 + canvas.height2 * canvas.height2); + ctx.font = "15px Arial"; + ctx.lineJoin = "round"; + ctx.lineCap = "round"; + // ctx.lineCap='square'; + game.setZoom(); +} +setupCanvas(); +window.onresize = () => { + setupCanvas(); +}; + +//mouse move input +document.body.addEventListener("mousemove", (e) => { + game.mouse.x = e.clientX; + game.mouse.y = e.clientY; +}); + +document.body.addEventListener("mouseup", (e) => { + // game.buildingUp(e); //uncomment when building levels + game.mouseDown = false; + // console.log(e) + if (e.which === 3) { + game.mouseDownRight = false; + } else { + game.mouseDown = false; + } +}); + +document.body.addEventListener("mousedown", (e) => { + if (e.which === 3) { + game.mouseDownRight = true; + } else { + game.mouseDown = true; + } +}); + +//keyboard input +const keys = []; +document.body.addEventListener("keydown", (e) => { + keys[e.keyCode] = true; + game.keyPress(); +}); + +document.body.addEventListener("keyup", (e) => { + keys[e.keyCode] = false; +}); + +document.body.addEventListener("wheel", (e) => { + if (e.deltaY > 0) { + game.nextGun(); + } else { + game.previousGun(); + } +}, { + passive: true +}); + + +// function playSound(id) { +// //play sound +// if (false) { +// //sounds are turned off for now +// // if (document.getElementById(id)) { +// var sound = document.getElementById(id); //setup audio +// sound.currentTime = 0; //reset position of playback to zero //sound.load(); +// sound.play(); +// } +// } + +function shuffle(array) { + var currentIndex = array.length, + temporaryValue, + randomIndex; + // While there remain elements to shuffle... + while (0 !== currentIndex) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + // And swap it with the current element. + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + return array; +} + + + +//main loop ************************************************************ +//********************************************************************** +function cycle() { + if (!game.paused) requestAnimationFrame(cycle); + const now = Date.now(); + const elapsed = now - game.then; // calc elapsed time since last loop + if (elapsed > game.fpsInterval) { // if enough time has elapsed, draw the next frame + game.then = now - (elapsed % game.fpsInterval); // Get ready for next frame by setting then=now. Also, adjust for fpsInterval not being multiple of 16.67 + game.loop(); + } +} \ No newline at end of file diff --git a/js/level.js b/js/level.js new file mode 100644 index 0000000..f02934c --- /dev/null +++ b/js/level.js @@ -0,0 +1,1593 @@ +//global game variables +let body = []; //non static bodies +let map = []; //all static bodies +let cons = []; //all constraints between a point and a body +let consBB = []; //all constraints between two bodies +//main object for spawning levels +const level = { + maxJump: 390, + defaultZoom: 1400, + boostScale: 0.000023, + levels: ["skyscrapers", "rooftops", "warehouse", "highrise", "office", "aerie"], + onLevel: 0, + start() { + if (game.levelsCleared === 0) { + // game.levelsCleared = 16; //for testing to simulate possible mobs spawns + // b.giveGuns("all", 1000) + // b.giveGuns(3) // set a starting gun for testing + // mech.fieldUpgrades[2].effect(); //give a field power up for testing + b.giveMod(8) + + this.intro(); //starting level + // this.testingMap(); + // this.bosses(); + // this.aerie(); + // this.rooftops(); + // this.warehouse(); + // this.highrise(); + // this.office(); + } else { + spawn.setSpawnList(); //picks a couple mobs types for a themed random mob spawns + level[level.levels[level.onLevel]](); //picks the current map from the the levels array + level.levelAnnounce(); + } + game.setZoom(); + level.addToWorld(); //add bodies to game engine + game.draw.setPaths(); + }, + difficultyIncrease() { + game.dmgScale += 0.35; //damage done by mobs increases each level + b.dmgScale *= 0.92; //damage done by player decreases each level + game.accelScale *= 1.05 //mob acceleration increases each level + game.lookFreqScale *= 0.95 //mob cycles between looks decreases each level + game.CDScale *= 0.95 //mob CD time decreases each level + }, + //****************************************************************************************************************** + //****************************************************************************************************************** + testingMap() { + //start with all guns + b.giveGuns("all", 1000) + game.zoomScale = 1400 //1400 is normal + spawn.setSpawnList(); + game.levelsCleared = 3; //for testing to simulate all possible mobs spawns + for (let i = 0; i < game.levelsCleared; i++) { + level.difficultyIncrease() + } + mech.setPosToSpawn(-75, -60); //normal spawn + level.enter.x = mech.spawnPos.x - 50; + level.enter.y = mech.spawnPos.y + 20; + + level.exit.x = 3500; + level.exit.y = -870; + this.addZone(level.exit.x, level.exit.y, 100, 30, "nextLevel"); + document.body.style.backgroundColor = "#dcdcde"; + + + + + // this.addZone(250, -1000, 500, 1500, "laser"); + //spawn.debris(0, -900, 4500, 10); //15 debris per level + // setTimeout(function() { + // document.body.style.backgroundColor = "#eee"; + // }, 1); + // this.addQueryRegion(550, -25, 100, 50, "bounce", { Vx: 0, Vy: -25 }); + // level.fillBG.push({ x: 550, y: -25, width: 100, height: 50, color: "#ff0" }); + + spawn.mapRect(-1200, 0, 2200, 300); //left ground + spawn.mapRect(3500, -860, 100, 50); //ground bump wall + spawn.mapVertex(1250, 0, "0 0 0 300 -500 600 -500 300"); + spawn.mapRect(1500, -300, 2000, 300); //upper ground + spawn.mapVertex(3750, 0, "0 600 0 300 -500 0 -500 300"); + spawn.mapRect(4000, 0, 1000, 300); //right lower ground + spawn.mapRect(2200, -600, 600, 50); //center platform + spawn.mapRect(1300, -850, 700, 50); //center platform + spawn.mapRect(3000, -850, 700, 50); //center platform + // spawn.mapRect(0, -2000, 3000, 50); //center platform + spawn.spawnBuilding(-200, -250, 275, 240, false, true, "left"); //far left; player spawns in side + // spawn.boost(350, 0, -1000); + // for (let i = 0; i < 10; i++) { + // powerUps.spawn(950, -425, "gun", false); + // } + // for (let i = 0; i < 5; i++) { + // powerUps.spawn(2500 + i * 20, -1300, "gun", false); + // powerUps.spawn(2500 + i * 20, -1100, "ammo", false); + // } + // spawn.nodeBoss(-500, -600, spawn.allowedBossList[Math.floor(Math.random() * spawn.allowedBossList.length)]); + // spawn.lineBoss(-500, -600, spawn.allowedBossList[Math.floor(Math.random() * spawn.allowedBossList.length)]); + // spawn.bodyRect(-135, -50, 50, 50); + // spawn.bodyRect(-140, -100, 50, 50); + // spawn.bodyRect(-145, -150, 60, 50); + // spawn.bodyRect(-140, -200, 50, 50); + // spawn.bodyRect(-95, -50, 40, 50); + // spawn.bodyRect(-90, -100, 60, 50); + // spawn.bodyRect(300, -150, 140, 50); + // spawn.bodyRect(300, -150, 30, 30); + // spawn.bodyRect(300, -150, 20, 20); + // spawn.bodyRect(300, -150, 40, 100); + // spawn.bodyRect(300, -150, 40, 90); + // spawn.bodyRect(300, -150, 30, 60); + // spawn.bodyRect(300, -150, 40, 70); + // spawn.bodyRect(300, -150, 40, 60); + // spawn.bodyRect(300, -150, 20, 20); + // spawn.bodyRect(500, -150, 140, 110); + // spawn.bodyRect(600, -150, 140, 100); + // spawn.bodyRect(400, -150, 140, 160); + // spawn.bodyRect(500, -150, 110, 110); + // powerUps.spawn(340, -400, "heal", false); + // powerUps.spawn(370, -400, "gun", false); + // powerUps.spawn(400, -400, "field", false, 2); + // powerUps.spawn(420, -400, "ammo", false); + powerUps.spawn(450, -400, "mod", false, 6); + // powerUps.spawn(450, -400, "mod", false); + // spawn.bodyRect(-45, -100, 40, 50); + // spawn.focuser(800, -1150); + // spawn.groupBoss(-600, -550); + spawn.starter(800, -150, 100); + // spawn.grower(800, -250); + // spawn.blinker(800, -250, 40); + // spawn.groupBoss(900, -1070); + // for (let i = 0; i < 20; i++) { + // spawn.randomBoss(-100, -1470); + // } + }, + bosses() { + level.defaultZoom = 1500 + game.zoomTransition(level.defaultZoom) + + // spawn.setSpawnList(); + // spawn.setSpawnList(); + // game.levelsCleared = 7; //for testing to simulate all possible mobs spawns + // for (let i = 0; i < game.levelsCleared; i++) { + // game.dmgScale += 0.4; //damage done by mobs increases each level + // b.dmgScale *= 0.9; //damage done by player decreases each level + // } + + document.body.style.backgroundColor = "#ddd"; + + // level.fillBG.push({ + // x: -150, + // y: -1150, + // width: 7000, + // height: 1200, + // color: "#eee" + // }); + + level.fill.push({ + x: 6400, + y: -550, + width: 300, + height: 350, + color: "rgba(0,255,255,0.1)" + }); + + mech.setPosToSpawn(0, -750); //normal spawn + level.enter.x = mech.spawnPos.x - 50; + level.enter.y = mech.spawnPos.y + 20; + level.exit.x = 6500; + level.exit.y = -230; + this.addZone(level.exit.x, level.exit.y, 100, 30, "nextLevel"); + + spawn.mapRect(-950, 0, 8200, 800); //ground + spawn.mapRect(-950, -1200, 800, 1400); //left wall + spawn.mapRect(-950, -1800, 8200, 800); //roof + spawn.mapRect(-250, -700, 1000, 900); // shelf + spawn.mapRect(-250, -1200, 1000, 250); // shelf roof + powerUps.spawnStartingPowerUps(600, -800); + + function blockDoor(x, y, blockSize = 58) { + spawn.mapRect(x, y - 290, 40, 60); // door lip + spawn.mapRect(x, y, 40, 50); // door lip + for (let i = 0; i < 4; ++i) { + spawn.bodyRect(x + 5, y - 260 + i * blockSize, 30, blockSize); + } + } + blockDoor(710, -710); + + spawn[spawn.pickList[0]](1500, -200, 100 + game.levelsCleared * 8); + spawn.mapRect(2500, -1200, 200, 750); //right wall + blockDoor(2585, -210) + spawn.mapRect(2500, -200, 200, 300); //right wall + + spawn.nodeBoss(3500, -200, spawn.allowedBossList[Math.floor(Math.random() * spawn.allowedBossList.length)]); + spawn.mapRect(4500, -1200, 200, 750); //right wall + blockDoor(4585, -210) + spawn.mapRect(4500, -200, 200, 300); //right wall + + spawn.lineBoss(5000, -200, spawn.allowedBossList[Math.floor(Math.random() * spawn.allowedBossList.length)]); + spawn.mapRect(6400, -1200, 400, 750); //right wall + spawn.mapRect(6400, -200, 400, 300); //right wall + spawn.mapRect(6700, -1800, 800, 2600); //right wall + spawn.mapRect(level.exit.x, level.exit.y + 20, 100, 100); //exit bump + + for (let i = 0; i < 5; ++i) { + if (game.levelsCleared * Math.random() > 3 * i) { + spawn.randomBoss(2000 + 500 * (Math.random() - 0.5), -800 + 200 * (Math.random() - 0.5), Infinity); + } + if (game.levelsCleared * Math.random() > 2.6 * i) { + spawn.randomBoss(3500 + 500 * (Math.random() - 0.5), -800 + 200 * (Math.random() - 0.5), Infinity); + } + if (game.levelsCleared * Math.random() > 2.4 * i) { + spawn.randomBoss(5000 + 500 * (Math.random() - 0.5), -800 + 200 * (Math.random() - 0.5), Infinity); + } + } + }, + //empty map for testing mobs + intro() { + // b.giveGuns(0, 1000) + game.zoomScale = 1000 //1400 is normal + level.defaultZoom = 1600 + game.zoomTransition(level.defaultZoom, 1) + + mech.setPosToSpawn(460, -100); //normal spawn + level.enter.x = -1000000; //offscreen + level.enter.y = -400; + level.exit.x = 2800; + level.exit.y = -335; + this.addZone(level.exit.x, level.exit.y, 100, 30, "nextLevel"); + document.body.style.backgroundColor = "#fff"; + level.fillBG.push({ + x: 2600, + y: -600, + width: 400, + height: 500, + color: "#edf9f9" + }); + + level.fillBG.push({ + x: 1600, + y: -500, + width: 100, + height: 100, + color: "#eee" + }); + + level.fillBG.push({ + x: -55, + y: -283, + width: 12, + height: 100, + color: "#eee" + }); + + //faster way to draw a wire + function wallWire(x, y, width, height, front = false) { + if (front) { + level.fill.push({ + x: x, + y: y, + width: width, + height: height, + color: "#aaa" + }); + } else { + level.fillBG.push({ + x: x, + y: y, + width: width, + height: height, + color: "#eee" + }); + } + } + + for (let i = 0; i < 3; i++) { + wallWire(100 - 10 * i, -1050 - 10 * i, 5, 800); + wallWire(100 - 10 * i, -255 - 10 * i, -300, 5); + } + for (let i = 0; i < 5; i++) { + wallWire(1000 + 10 * i, -1050 - 10 * i, 5, 600); + wallWire(1000 + 10 * i, -450 - 10 * i, 150, 5); + wallWire(1150 + 10 * i, -450 - 10 * i, 5, 500); + } + for (let i = 0; i < 3; i++) { + wallWire(2650 - 10 * i, -700 - 10 * i, -300, 5); + wallWire(2350 - 10 * i, -700 - 10 * i, 5, 800); + } + for (let i = 0; i < 5; i++) { + wallWire(1625 + 10 * i, -1050, 5, 1200); + } + for (let i = 0; i < 4; i++) { + wallWire(1650, -470 + i * 10, 670 - i * 10, 5); + wallWire(1650 + 670 - i * 10, -470 + i * 10, 5, 600); + } + for (let i = 0; i < 3; i++) { + wallWire(-200 - i * 10, -245 + i * 10, 1340, 5); + wallWire(1140 - i * 10, -245 + i * 10, 5, 300); + wallWire(-200 - i * 10, -215 + i * 10, 660, 5); + wallWire(460 - i * 10, -215 + i * 10, 5, 300); + } + spawn.mapRect(-250, 0, 3600, 1800); //ground + spawn.mapRect(-2750, -2800, 2600, 4600); //left wall + spawn.mapRect(3000, -2800, 2600, 4600); //right wall + spawn.mapRect(-250, -2800, 3600, 1800); //roof + spawn.mapRect(2600, -300, 500, 500); //exit shelf + spawn.mapRect(2600, -1200, 500, 600); //exit roof + spawn.mapRect(level.exit.x, level.exit.y + 20, 100, 100); //exit bump + spawn.mapRect(-95, -1100, 80, 110); //wire source + spawn.mapRect(410, -10, 90, 20); //small platform for player + + // spawn.bodyRect(-35, -50, 50, 50); + // spawn.bodyRect(-40, -100, 50, 50); + // spawn.bodyRect(-45, -150, 60, 50); + // spawn.bodyRect(-40, -200, 50, 50); + // spawn.bodyRect(5, -50, 40, 50); + // spawn.bodyRect(10, -100, 60, 50); + // spawn.bodyRect(-10, -150, 40, 50); + // spawn.bodyRect(55, -100, 40, 50); + // spawn.bodyRect(-150, -300, 100, 100); + // spawn.bodyRect(-150, -200, 100, 100); + // spawn.bodyRect(-150, -100, 100, 100); + + // spawn.bodyRect(1790, -50, 40, 50); + // spawn.bodyRect(1875, -100, 200, 90); + spawn.bodyRect(2425, -120, 70, 50); + spawn.bodyRect(2400, -100, 100, 60); + spawn.bodyRect(2500, -150, 100, 150); //exit step + + mech.health = 0.25; + mech.displayHealth(); + powerUps.spawn(-100, 0, "heal", false); //starting gun + powerUps.spawn(1900, -150, "heal", false); //starting gun + powerUps.spawn(2050, -150, "heal", false); //starting gun + // powerUps.spawn(2050, -150, "field", false); //starting gun + powerUps.spawn(2300, -150, "gun", false); //starting gun + + spawn.wireFoot(); + spawn.wireFootLeft(); + spawn.wireKnee(); + spawn.wireKneeLeft(); + spawn.wireHead(); + // spawn.mapRect(1400, -700, 50, 300); //ground + // spawn.healer(1600, -500) + // spawn.healer(1600, -500) + // spawn.healer(1900, -500) + // spawn.healer(1000, -500) + // spawn.healer(1000, -400) + }, + rooftops() { + level.defaultZoom = 1700 + game.zoomTransition(level.defaultZoom) + document.body.style.backgroundColor = "#dcdcde"; + + if (Math.random() < 0.75) { + //normal direction start in top left + mech.setPosToSpawn(-450, -2050); + level.exit.x = 3600; + level.exit.y = -300; + spawn.mapRect(3600, -285, 100, 50); //ground bump wall + //mobs that spawn in exit room + spawn.randomSmallMob(4100, -100); + spawn.randomSmallMob(4600, -100); + spawn.randomMob(3765, -450, 0.3); + level.fill.push({ + x: -650, + y: -2300, + width: 450, + height: 300, + color: "rgba(0,0,0,0.15)" + }); + } else { + //reverse direction, start in bottom right + mech.setPosToSpawn(3650, -310); + level.exit.x = -550; + level.exit.y = -2030; + spawn.mapRect(-550, -2015, 100, 50); //ground bump wall + spawn.boost(4950, 0, 1600); + level.fillBG.push({ + x: -650, + y: -2300, + width: 450, + height: 300, + color: "#d4f4f4" + }); + } + level.enter.x = mech.spawnPos.x - 50; + level.enter.y = mech.spawnPos.y + 20; + this.addZone(level.exit.x, level.exit.y, 100, 30, "nextLevel"); + + spawn.debris(1650, -1800, 3800, 20); //20 debris per level + powerUps.spawnStartingPowerUps(2450, -1675); + + //foreground + + level.fill.push({ + x: 3450, + y: -1250, + width: 1100, + height: 1250, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 4550, + y: -725, + width: 900, + height: 725, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 3400, + y: 100, + width: 2150, + height: 900, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: -700, + y: -1950, + width: 2100, + height: 2950, + color: "rgba(0,0,0,0.1)" + }); + + level.fill.push({ + x: 1950, + y: -1950, + width: 600, + height: 350, + color: "rgba(0,0,0,0.1)" + }); + + level.fill.push({ + x: 1950, + y: -1550, + width: 1025, + height: 550, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 1600, + y: -900, + width: 1650, + height: 1900, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 3450, + y: -1550, + width: 350, + height: 300, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 700, + y: -2225, + width: 700, + height: 225, + color: "rgba(0,0,0,0.1)" + }); + + //spawn.mapRect(-700, 0, 6250, 100); //ground + spawn.mapRect(3400, 0, 2150, 100); //ground + spawn.mapRect(-700, -2000, 2100, 50); //Top left ledge + spawn.bodyRect(1300, -2125, 50, 125, 0.8); + spawn.bodyRect(1307, -2225, 50, 100, 0.8); + spawn.mapRect(-700, -2350, 50, 400); //far left starting left wall + spawn.mapRect(-700, -2010, 500, 50); //far left starting ground + spawn.mapRect(-700, -2350, 500, 50); //far left starting ceiling + spawn.mapRect(-250, -2350, 50, 200); //far left starting right part of wall + spawn.bodyRect(-240, -2150, 30, 36); //door to starting room + spawn.bodyRect(-240, -2115, 30, 36); //door to starting room + spawn.bodyRect(-240, -2080, 30, 35); //door to starting room + spawn.bodyRect(-240, -2045, 30, 35); //door to starting room + spawn.mapRect(1950, -2000, 600, 50); + spawn.bodyRect(200, -2150, 200, 220, 0.8); + spawn.mapRect(700, -2275, 700, 50); + spawn.bodyRect(1050, -2350, 30, 30, 0.8); + spawn.boost(1800, -1000, 1200); + spawn.bodyRect(1625, -1100, 100, 75); + spawn.bodyRect(1350, -1025, 400, 25); // ground plank + spawn.mapRect(-725, -1000, 2150, 100); //lower left ledge + spawn.bodyRect(350, -1100, 200, 100, 0.8); + spawn.bodyRect(370, -1200, 100, 100, 0.8); + spawn.bodyRect(360, -1300, 100, 100, 0.8); + spawn.bodyRect(950, -1050, 300, 50, 0.8); + spawn.bodyRect(-600, -1250, 400, 250, 0.8); + spawn.mapRect(1575, -1000, 1700, 100); //middle ledge + spawn.mapRect(3400, -1000, 75, 25); + spawn.bodyRect(2600, -1950, 100, 250, 0.8); + spawn.bodyRect(2700, -1125, 125, 125, 0.8); + spawn.bodyRect(2710, -1250, 125, 125, 0.8); + spawn.bodyRect(2705, -1350, 75, 100, 0.8); + spawn.mapRect(3450, -1600, 350, 50); + spawn.mapRect(1950, -1600, 1025, 50); + spawn.bodyRect(3100, -1015, 375, 15); + spawn.bodyRect(3500, -850, 75, 125, 0.8); + spawn.mapRect(3450, -1000, 50, 580); //left building wall + spawn.bodyRect(3460, -420, 30, 144); + spawn.mapRect(5450, -775, 100, 875); //right building wall + spawn.bodyRect(4850, -750, 300, 25, 0.8); + spawn.bodyRect(3925, -1400, 100, 150, 0.8); + spawn.mapRect(3450, -1250, 1100, 50); + spawn.mapRect(3450, -1225, 50, 75); + spawn.mapRect(4500, -1225, 50, 390); + spawn.mapRect(3450, -725, 1500, 50); + spawn.mapRect(5100, -725, 400, 50); + spawn.mapRect(4500, -735, 50, 635); + spawn.bodyRect(4510, -100, 30, 100, 0.8); + spawn.mapRect(4500, -885, 100, 50); + spawn.spawnStairs(3800, 0, 3, 150, 206); //stairs top exit + spawn.mapRect(3400, -275, 450, 275); //exit platform + + spawn.randomSmallMob(2200, -1775); + spawn.randomSmallMob(4000, -825); + spawn.randomSmallMob(-350, -2400); + spawn.randomMob(4250, -1350, 0.8); + spawn.randomMob(2550, -1350, 0.8); + spawn.randomMob(1225, -2400, 0.3); + spawn.randomMob(1120, -1200, 0.3); + spawn.randomMob(3000, -1150, 0.2); + spawn.randomMob(3200, -1150, 0.3); + spawn.randomMob(3300, -1750, 0.3); + spawn.randomMob(3650, -1350, 0.3); + spawn.randomMob(3600, -1800, 0.1); + spawn.randomMob(5200, -100, 0.3); + spawn.randomMob(5275, -900, 0.2); + spawn.randomMob(900, -2125, 0.3); + spawn.randomBoss(600, -1575, 0); + spawn.randomBoss(2225, -1325, 0.4); + spawn.randomBoss(4900, -1200, 0); + //spawn.randomBoss(4850, -1250,0.7); + if (game.levelsCleared > 4) spawn.bomber(2500, -2400, 100); + }, + aerie() { + // game.setZoom(3000); + // game.levelsCleared = 4; //for testing to simulate possible mobs spawns + level.defaultZoom = 2100 + game.zoomTransition(level.defaultZoom) + + const backwards = (Math.random() < 0.75) ? false : true; + if (backwards) { + mech.setPosToSpawn(4000, -3300); //normal spawn + level.exit.x = -100; + level.exit.y = -1025; + } else { + mech.setPosToSpawn(-50, -1050); //normal spawn + level.exit.x = 3950; + level.exit.y = -3275; + } + // mech.setPosToSpawn(2250, -900); + // game.zoomTransition(1500) //1400 is normal + + level.enter.x = mech.spawnPos.x - 50; + level.enter.y = mech.spawnPos.y + 20; + this.addZone(level.exit.x, level.exit.y, 100, 30, "nextLevel"); + powerUps.spawnStartingPowerUps(1075, -550); + spawn.debris(-250, 50, 1650, 2); //20 debris per level + spawn.debris(2475, 0, 750, 2); //20 debris per level + spawn.debris(3450, 0, 2000, 20); //20 debris per level + spawn.debris(3500, -2350, 1500, 2); //20 debris per level + document.body.style.backgroundColor = "#dcdcde"; + + //foreground + level.fill.push({ + x: -100, + y: -1000, + width: 1450, + height: 1400, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 2000, + y: -1110, + width: 450, + height: 1550, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 3700, + y: -3150, + width: 1100, + height: 900, + color: "rgba(0,0,0,0.1)" + }); + + //background + level.fillBG.push({ + x: 4200, + y: -2250, + width: 100, + height: 2600, + color: "#c7c7ca" + }); + if (!backwards) { + level.fillBG.push({ + x: 3750, + y: -3650, + width: 550, + height: 400, + color: "#d4f4f4" + }); + level.fill.push({ + x: -275, + y: -1275, + width: 425, + height: 300, + color: "rgba(0,0,0,0.1)" + }); + } else { + level.fill.push({ + x: 3750, + y: -3650, + width: 550, + height: 400, + color: "rgba(0,0,0,0.1)" + }); + level.fillBG.push({ + x: -275, + y: -1275, + width: 425, + height: 300, + color: "#d4f4f4" + }); + } + + // starting room + spawn.mapRect(-100, -1010, 100, 30); + spawn.mapRect(-300, -1000, 600, 50); + spawn.mapRect(-300, -1300, 450, 50); + spawn.mapRect(-300, -1300, 50, 350); + if (!backwards) spawn.bodyRect(100, -1250, 200, 240); //remove on backwards + //left building + spawn.mapRect(-100, -975, 100, 975); + spawn.mapRect(-500, 100, 1950, 400); + spawn.boost(-425, 100, 1400); + spawn.mapRect(600, -1000, 750, 50); + spawn.mapRect(900, -500, 550, 50); + spawn.mapRect(1250, -975, 100, 375); + spawn.bodyRect(1250, -600, 100, 100, 0.7); + spawn.mapRect(1250, -450, 100, 450); + if (!backwards) spawn.bodyRect(1250, -1225, 100, 200); //remove on backwards + if (!backwards) spawn.bodyRect(1200, -1025, 350, 25); //remove on backwards + //middle super tower + if (backwards) { + spawn.bodyRect(2000, -800, 700, 35); + } else { + spawn.bodyRect(1750, -800, 700, 35); + } + spawn.mapVertex(2225, -2100, "0 0 450 0 300 -2500 150 -2500") + spawn.mapRect(2000, -700, 450, 300); + spawn.bodyRect(2360, -450, 100, 300, 0.6); + spawn.mapRect(2000, -75, 450, 275); + spawn.bodyRect(2450, 150, 150, 150, 0.4); + spawn.mapRect(1550, 300, 4600, 200); //ground + //floor below right tall tower + spawn.bodyRect(3000, 50, 150, 250, 0.9); + spawn.bodyRect(4500, -500, 300, 250, 0.7); + spawn.bodyRect(3500, -100, 100, 150, 0.7); + spawn.bodyRect(4200, -500, 110, 30, 0.7); + spawn.bodyRect(3800, -500, 150, 130, 0.7); + spawn.bodyRect(4000, 50, 200, 150, 0.9); + spawn.bodyRect(4500, 50, 300, 200, 0.9); + spawn.bodyRect(4200, -350, 200, 50, 0.9); + spawn.bodyRect(4700, -350, 50, 200, 0.9); + spawn.bodyRect(4900, -100, 300, 300, 0.7); + spawn.boost(5350, 275, 2850); + spawn.mapRect(6050, -700, 600, 1200); + //right tall tower + spawn.mapRect(3700, -3200, 100, 800); + spawn.mapRect(4700, -2910, 100, 510); + spawn.mapRect(3700, -2600, 300, 50); + spawn.mapRect(4100, -2900, 900, 50); + spawn.mapRect(3450, -2300, 1650, 100); + //exit room on top of tower + spawn.mapRect(3700, -3700, 600, 50); + spawn.mapRect(3700, -3700, 50, 500); + spawn.mapRect(4250, -3700, 50, 300); + spawn.mapRect(3700, -3250, 1100, 100); + spawn.mapRect(3950, -3260, 100, 30); + + spawn.randomSmallMob(-225, 25); + spawn.randomSmallMob(1000, -1100); + spawn.randomSmallMob(4000, -250); + spawn.randomSmallMob(4450, -3000); + spawn.randomSmallMob(5600, 100); + spawn.randomMob(4275, -2600, 0.8); + spawn.randomMob(1050, -700, 0.8) + spawn.randomMob(6050, -850, 0.7); + spawn.randomMob(2150, -300, 0.6) + spawn.randomMob(3900, -2700, 0.8); + spawn.randomMob(3600, -500, 0.8); + spawn.randomMob(3400, -200, 0.8); + spawn.randomMob(1650, -1300, 0.7) + spawn.randomMob(-4100, -50, 0.7); + spawn.randomMob(4100, -50, 0.5); + spawn.randomMob(1700, -50, 0.3) + spawn.randomMob(2350, -900, 0.3) + spawn.randomMob(4700, -150, 0.2); + spawn.randomBoss(350, -500, 1) + spawn.randomBoss(4000, -350, 0.6); + spawn.randomBoss(2750, -550, 0.1); + if (game.levelsCleared > 2) spawn.suckerBoss(4500, -400); + + //add mini boss, giant hopper? or a black hole that spawns hoppers? + }, + skyscrapers() { + level.defaultZoom = 2000 + game.zoomTransition(level.defaultZoom) + + mech.setPosToSpawn(-50, -50); //normal spawn + //mech.setPosToSpawn(1550, -1200); //spawn left high + //mech.setPosToSpawn(1800, -2000); //spawn near exit + level.enter.x = mech.spawnPos.x - 50; + level.enter.y = mech.spawnPos.y + 20; + level.exit.x = 1500; + level.exit.y = -1875; + this.addZone(level.exit.x, level.exit.y, 100, 30, "nextLevel"); + + powerUps.spawnStartingPowerUps(1475, -1175); + spawn.debris(0, -2200, 4500, 20); //20 debris per level + document.body.style.backgroundColor = "#dcdcde"; + + //foreground + level.fill.push({ + x: 2500, + y: -1100, + width: 450, + height: 250, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 2400, + y: -550, + width: 600, + height: 150, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 2550, + y: -1650, + width: 250, + height: 200, + color: "rgba(0,0,0,0.1)" + }); + //level.fill.push({ x: 1350, y: -2100, width: 400, height: 250, color: "rgba(0,255,255,0.1)" }); + level.fill.push({ + x: 700, + y: -110, + width: 400, + height: 110, + color: "rgba(0,0,0,0.2)" + }); + level.fill.push({ + x: 3600, + y: -110, + width: 400, + height: 110, + color: "rgba(0,0,0,0.2)" + }); + level.fill.push({ + x: -250, + y: -300, + width: 450, + height: 300, + color: "rgba(0,0,0,0.15)" + }); + + //background + level.fillBG.push({ + x: 1300, + y: -1800, + width: 750, + height: 1800, + color: "#d4d4d7" + }); + level.fillBG.push({ + x: 3350, + y: -1325, + width: 50, + height: 1325, + color: "#d4d4d7" + }); + level.fillBG.push({ + x: 1350, + y: -2100, + width: 400, + height: 250, + color: "#d4f4f4" + }); + + spawn.mapRect(-300, 0, 5000, 300); //***********ground + spawn.mapRect(-300, -350, 50, 400); //far left starting left wall + spawn.mapRect(-300, -10, 500, 50); //far left starting ground + spawn.mapRect(-300, -350, 500, 50); //far left starting ceiling + spawn.mapRect(150, -350, 50, 200); //far left starting right part of wall + spawn.bodyRect(170, -130, 14, 140, 1, spawn.propsFriction); //door to starting room + spawn.boost(475, 0, 1300); + spawn.mapRect(700, -1100, 400, 990); //far left building + spawn.mapRect(1600, -400, 1500, 500); //long center building + spawn.mapRect(1345, -1100, 250, 25); //left platform + spawn.mapRect(1755, -1100, 250, 25); //right platform + spawn.mapRect(1300, -1850, 750, 50); //left higher platform + spawn.mapRect(1300, -2150, 50, 350); //left higher platform left edge wall + spawn.mapRect(1300, -2150, 450, 50); //left higher platform roof + spawn.mapRect(1500, -1860, 100, 50); //ground bump wall + spawn.mapRect(2400, -850, 600, 300); //center floating large square + //spawn.bodyRect(2500, -1100, 25, 250); //wall before chasers + spawn.mapRect(2500, -1450, 450, 350); //higher center floating large square + spawn.mapRect(2500, -1675, 50, 300); //left wall on higher center floating large square + spawn.mapRect(2500, -1700, 300, 50); //roof on higher center floating large square + spawn.mapRect(3300, -850, 150, 25); //ledge by far right building + spawn.mapRect(3300, -1350, 150, 25); //higher ledge by far right building + spawn.mapRect(3600, -1100, 400, 990); //far right building + spawn.boost(4150, 0, 1300); + + spawn.bodyRect(3200, -1375, 300, 25, 0.9); + spawn.bodyRect(1825, -1875, 400, 25, 0.9); + // spawn.bodyRect(1800, -575, 250, 150, 0.8); + spawn.bodyRect(1800, -600, 250, 200, 0.8); + spawn.bodyRect(2557, -450, 35, 55, 0.7); + spawn.bodyRect(2957, -450, 30, 15, 0.7); + spawn.bodyRect(2900, -450, 60, 45, 0.7); + spawn.bodyRect(915, -1200, 60, 100, 0.95); + spawn.bodyRect(925, -1300, 50, 100, 0.95); + if (Math.random() < 0.9) { + spawn.bodyRect(2300, -1720, 400, 20); + spawn.bodyRect(2590, -1780, 80, 80); + } + spawn.bodyRect(2925, -1100, 25, 250, 0.8); + spawn.bodyRect(3325, -1550, 50, 200, 0.3); + if (Math.random() < 0.8) { + spawn.bodyRect(1400, -75, 200, 75); //block to get up ledge from ground + spawn.bodyRect(1525, -125, 50, 50); //block to get up ledge from ground + } + spawn.bodyRect(1025, -1110, 400, 25, 0.9); //block on far left building + spawn.bodyRect(1425, -1110, 115, 25, 0.9); //block on far left building + spawn.bodyRect(1540, -1110, 300, 25, 0.9); //block on far left building + + if (game.levelsCleared > 2) spawn.shooterBoss(2200, -1300); + spawn.randomSmallMob(1300, -70); + spawn.randomSmallMob(3200, -100); + spawn.randomSmallMob(4450, -100); + spawn.randomSmallMob(2700, -475); + spawn.randomMob(2650, -975, 0.8); + spawn.randomMob(2650, -1550, 0.8); + spawn.randomMob(4150, -200, 0.15); + spawn.randomMob(1700, -1300, 0.2); + spawn.randomMob(1850, -1950, 0.25); + spawn.randomMob(2610, -1880, 0.25); + spawn.randomMob(3350, -950, 0.25); + spawn.randomMob(1690, -2250, 0.25); + spawn.randomMob(2200, -600, 0.2); + spawn.randomMob(850, -1300, 0.25); + spawn.randomMob(-100, -900, -0.2); + spawn.randomBoss(3700, -1500, 0.4); + spawn.randomBoss(1700, -900, 0.4); + }, + highrise() { + level.defaultZoom = 1500 + game.zoomTransition(level.defaultZoom) + + document.body.style.backgroundColor = "#dcdcde" //"#fafcff"; + mech.setPosToSpawn(0, -700); //normal spawn + //mech.setPosToSpawn(-2000, -1700); // left ledge spawn + level.enter.x = mech.spawnPos.x - 50; + level.enter.y = mech.spawnPos.y + 20; + level.exit.x = -4275; + level.exit.y = -2805; + this.addZone(level.exit.x, level.exit.y, 100, 30, "nextLevel"); + powerUps.spawnStartingPowerUps(-2550, -700); + + // spawn.laserZone(-550, -350, 10, 400, 0.3) + // spawn.deathQuery(-550, -350, 50, 400) + + // spawn.debris(-3950, -2575, 1050, 4); //20 debris per level + spawn.debris(-2325, -1825, 2400); //20 debris per level + spawn.debris(-2625, -600, 600, 6); //20 debris per level + spawn.debris(-2000, -60, 1200, 6); //20 debris per level + // if (!game.levelsCleared) powerUps.spawn(2450, -1675, "gun", false); + //background + level.fillBG.push({ + x: -4425, + y: -3050, + width: 425, + height: 275, + color: "#cff" + }); + //foreground + level.fill.push({ + x: -1650, + y: -1575, + width: 550, + height: 425, + color: "rgba(0,0,0,0.12)" + }); + level.fill.push({ + x: -2600, + y: -2400, + width: 450, + height: 1800, + color: "rgba(0,0,0,0.12)" + }); + level.fill.push({ + x: -3425, + y: -2150, + width: 525, + height: 1550, + color: "rgba(0,0,0,0.12)" + }); + level.fill.push({ + x: -1850, + y: -1150, + width: 2025, + height: 1150, + color: "rgba(0,0,0,0.12)" + }); + + //hidden zone + level.fill.push({ + x: -4450, + y: -955, + width: 1025, + height: 360, + color: "rgba(64,64,64,0.97)" + }); + // level.fill.push({ + // x: -4050, + // y: -955, + // width: 625, + // height: 360, + // color: "#444" + // }); + powerUps.spawn(-4300, -700, "heal"); + powerUps.spawn(-4200, -700, "ammo"); + powerUps.spawn(-4100, -700, "gun"); + spawn.mapRect(-4450, -1000, 100, 500); + spawn.bodyRect(-3576, -750, 150, 150); + + + //building 1 + spawn.bodyRect(-1000, -675, 25, 25); + spawn.mapRect(-2225, 0, 2475, 150); + spawn.mapRect(175, -1000, 75, 1100); + + spawn.mapRect(-175, -985, 25, 175); + spawn.bodyRect(-170, -810, 14, 160, 1, spawn.propsFriction); //door to starting room + spawn.mapRect(-600, -650, 825, 50); + spawn.mapRect(-1300, -650, 500, 50); + spawn.mapRect(-175, -250, 425, 300); + spawn.bodyRect(-75, -300, 50, 50); + + // spawn.boost(-750, 0, 0, -0.01); + spawn.boost(-750, 0, 1700); + spawn.bodyRect(-425, -1375, 400, 225); + spawn.mapRect(-1125, -1575, 50, 475); + spawn.bodyRect(-1475, -1275, 250, 125); + spawn.bodyRect(-825, -1160, 250, 10); + + spawn.mapRect(-1650, -1575, 400, 50); + spawn.mapRect(-600, -1150, 850, 175); + + spawn.mapRect(-1850, -1150, 1050, 175); + spawn.bodyRect(-1907, -1600, 550, 25); + if (game.levelsCleared < 4) { + spawn.bodyRect(-1600, -125, 125, 125); + spawn.bodyRect(-1560, -200, 75, 75); + } else { + spawn.bodyRect(-1200, -125, 125, 125); + spawn.bodyRect(-1160, -200, 75, 75); + } + // spawn.bodyRect(-1100, -125, 150, 125); + + // spawn.bodyRect(-1200, -75, 75, 75); + + //building 2 + spawn.mapRect(-4450, -600, 2300, 750); + spawn.mapRect(-2225, -500, 175, 550); + spawn.boost(-2800, -600, 1150); + spawn.mapRect(-3450, -1325, 550, 50); + spawn.mapRect(-3425, -2200, 525, 50); + spawn.mapRect(-2600, -1750, 450, 50); + spawn.mapRect(-2600, -2450, 450, 50); + spawn.bodyRect(-2275, -2700, 50, 60); + spawn.bodyRect(-2600, -1975, 250, 225); + spawn.bodyRect(-3415, -1425, 100, 100); + spawn.bodyRect(-3400, -1525, 100, 100); + spawn.bodyRect(-3305, -1425, 100, 100); + + //building 3 + spawn.mapRect(-4450, -1750, 1025, 1000); + spawn.mapRect(-3750, -2000, 175, 275); + spawn.mapRect(-4000, -2350, 275, 675); + // spawn.mapRect(-4450, -2650, 475, 1000); + spawn.mapRect(-4450, -2775, 475, 1125); + spawn.bodyRect(-3715, -2050, 50, 50); + spawn.bodyRect(-3570, -1800, 50, 50); + spawn.bodyRect(-2970, -2250, 50, 50); + + if (game.levelsCleared < 4) spawn.bodyRect(-3760, -2400, 50, 50); + + spawn.bodyRect(-3080, -2250, 40, 40); + spawn.bodyRect(-3420, -650, 50, 50); + + //exit + spawn.mapRect(-4450, -3075, 25, 300); + spawn.mapRect(-4450, -3075, 450, 25); + spawn.mapRect(-4025, -3075, 25, 100); + spawn.mapRect(-4275, -2785, 100, 25); + + //mobs + spawn.randomMob(-2500, -2700, 1); + spawn.randomMob(-3200, -750, 1); + spawn.randomMob(-1875, -775, 0.2); + spawn.randomMob(-950, -1675, 0.2); + spawn.randomMob(-1525, -1750, 0.2); + spawn.randomMob(-1375, -1400, 0.2); + spawn.randomMob(-1625, -1275, 0.2); + spawn.randomMob(-1900, -1250, 0.2); + spawn.randomMob(-2250, -1850, 0.2); + spawn.randomMob(-2475, -2200, 0.2); + spawn.randomMob(-3000, -1475, 0.2); + spawn.randomMob(-3850, -2500, 0.2); + spawn.randomMob(-3650, -2125, 0.2); + spawn.randomMob(-4010, -3200, 0.2); + spawn.randomMob(-3500, -1825, 0.2); + spawn.randomMob(-975, -100, 0); + spawn.randomMob(-1050, -725, 0.2); + spawn.randomMob(-1525, -100, 0); + spawn.randomMob(-525, -1700, -0.1); + spawn.randomMob(-125, -1500, -0.1); + spawn.randomMob(-325, -1900, -0.1); + spawn.randomMob(-550, -100, -0.1); + spawn.randomBoss(-3250, -2700, 0.2); + spawn.randomBoss(-2450, -1100, 0); + }, + warehouse() { + level.defaultZoom = 1300 + game.zoomTransition(level.defaultZoom) + + document.body.style.backgroundColor = "#f2f5f3"; + mech.setPosToSpawn(25, -60); //normal spawn + //mech.setPosToSpawn(-2000, -1700); // left ledge spawn + level.enter.x = mech.spawnPos.x - 50; + level.enter.y = mech.spawnPos.y + 20; + level.exit.x = 425; + level.exit.y = -35; + this.addZone(level.exit.x, level.exit.y, 100, 30, "nextLevel"); + //level.addQueryRegion(-600, -250, 180, 420, "death", [[player]],{}); + + spawn.debris(-2250, 1330, 3000, 7); //20 debris per level + spawn.debris(-3000, -800, 3280, 7); //20 debris per level + spawn.debris(-1400, 410, 2300, 6); //20 debris per level + powerUps.spawnStartingPowerUps(25, 500); + + //foreground + // level.fill.push({ x: -3025, y: 50, width: 4125, height: 1350, color: "rgba(0,0,0,0.05)"}); + // level.fill.push({ x: -1800, y: -500, width: 1975, height: 550, color: "rgba(0,0,0,0.05)"}); + // level.fill.push({ x: -2600, y: -150, width: 700, height: 200, color: "rgba(0,0,0,0.05)"}); + //background + const BGColor = "rgba(0,0,0,0.1)"; + level.fill.push({ + x: -3025, + y: 50, + width: 4125, + height: 1350, + color: BGColor + }); + level.fill.push({ + x: -1800, + y: -500, + width: 1625, + height: 550, + color: BGColor + }); + level.fill.push({ + x: -175, + y: -250, + width: 350, + height: 300, + color: BGColor + }); + level.fill.push({ + x: -2600, + y: -150, + width: 700, + height: 200, + color: BGColor + }); + level.fillBG.push({ + x: 300, + y: -250, + width: 350, + height: 250, + color: "#cff" + }); + spawn.mapRect(-1500, 0, 2750, 100); + spawn.mapRect(175, -270, 125, 300); + spawn.mapRect(-1900, -600, 1775, 100); + spawn.mapRect(-1900, -600, 100, 1300); + //house + spawn.mapRect(-175, -550, 50, 400); + spawn.mapRect(-175, -15, 350, 50); + spawn.mapRect(-25, -25, 100, 50); + // spawn.mapRect(-175, -275, 350, 25); + // spawn.mapRect(-175, -250, 25, 75); + // spawn.bodyRect(-170, -175, 14, 160, 1, spawn.propsFriction); //door to starting room + //exit house + spawn.mapRect(300, -15, 350, 50); + spawn.mapRect(-150, -300, 800, 50); + spawn.mapRect(600, -275, 50, 75); + spawn.mapRect(425, -25, 100, 25); + // spawn.mapRect(-1900, 600, 2700, 100); + spawn.mapRect(1100, 0, 150, 1500); + spawn.mapRect(-2850, 1400, 4100, 100); + spawn.mapRect(-2375, 875, 1775, 75); + spawn.mapRect(-1450, 865, 75, 435); + spawn.mapRect(-1450, 662, 75, 100); + spawn.bodyRect(-1418, 773, 11, 102, 1, spawn.propsFriction); //blocking path + spawn.mapRect(-2950, 1250, 175, 250); + spawn.mapRect(-3050, 1100, 150, 400); + spawn.mapRect(-3150, 50, 125, 1450); + spawn.mapRect(-2375, 600, 3175, 100); + spawn.mapRect(-2125, 400, 250, 275); + // spawn.mapRect(-1950, -400, 100, 25); + spawn.mapRect(-3150, 50, 775, 100); + spawn.mapRect(-2600, -250, 775, 100); + spawn.bodyRect(-1350, -200, 200, 200, 1, spawn.propsSlide); //weight + spawn.bodyRect(-1800, 0, 300, 100, 1, spawn.propsHoist); //hoist + cons[cons.length] = Constraint.create({ + pointA: { + x: -1650, + y: -500 + }, + bodyB: body[body.length - 1], + stiffness: 0.000076, + length: 1 + }); + + spawn.bodyRect(400, 400, 200, 200, 1, spawn.propsSlide); //weight + spawn.bodyRect(800, 600, 300, 100, 1, spawn.propsHoist); //hoist + cons[cons.length] = Constraint.create({ + pointA: { + x: 950, + y: 100 + }, + bodyB: body[body.length - 1], + stiffness: 0.000076, + length: 1 + }); + + spawn.bodyRect(-2700, 1150, 100, 160, 1, spawn.propsSlide); //weight + spawn.bodyRect(-2550, 1150, 200, 100, 1, spawn.propsSlide); //weight + + spawn.bodyRect(-2775, 1300, 400, 100, 1, spawn.propsHoist); //hoist + cons[cons.length] = Constraint.create({ + pointA: { + x: -2575, + y: 150 + }, + bodyB: body[body.length - 1], + stiffness: 0.0002, + length: 566 + }); + + //blocks + //spawn.bodyRect(-155, -150, 10, 140, 1, spawn.propsFriction); + spawn.bodyRect(-165, -150, 30, 35, 1); + spawn.bodyRect(-165, -115, 30, 35, 1); + spawn.bodyRect(-165, -80, 30, 35, 1); + spawn.bodyRect(-165, -45, 30, 35, 1); + + spawn.bodyRect(-750, 400, 150, 150, 0.5); + spawn.bodyRect(-400, 1175, 100, 250, 1); //block to get to top path on bottom level + // spawn.bodyRect(-1450, 737, 75, 103, 0.5); //blocking path + + spawn.bodyRect(-2525, -50, 145, 100, 0.5); + spawn.bodyRect(-2325, -300, 150, 100, 0.5); + spawn.bodyRect(-1275, -750, 200, 150, 0.5); //roof block + spawn.bodyRect(-525, -700, 125, 100, 0.5); //roof block + + //mobs + spawn.randomSmallMob(-1125, 550); + spawn.randomSmallMob(-2325, 800); + spawn.randomSmallMob(-2950, -50); + spawn.randomSmallMob(825, 300); + spawn.randomSmallMob(-900, 825); + spawn.randomMob(-2025, 175, 0.6); + spawn.randomMob(-2325, 450, 0.6); + spawn.randomMob(-2925, 675, 0.5); + spawn.randomMob(-2700, 300, 0.2); + spawn.randomMob(-2500, 300, 0.2); + spawn.randomMob(-2075, -425, 0.2); + spawn.randomMob(-1550, -725, 0.2); + spawn.randomMob(375, 1100, 0.1); + spawn.randomMob(-1425, -100, 0.1); + spawn.randomMob(-800, -750, 0); + spawn.randomMob(400, -350, 0); + spawn.randomMob(650, 1300, 0); + spawn.randomMob(-750, -150, 0); + spawn.randomMob(475, 300, 0); + spawn.randomMob(-75, -700, 0); + spawn.randomMob(900, -200, -0.1); + spawn.randomBoss(-125, 275, -0.2); + spawn.randomBoss(-825, 1000, 0.2); + spawn.randomBoss(-1300, -1100, -0.3); + //spawn.randomBoss(600, -1575, 0); + //spawn.randomMob(1120, -1200, 0.3); + //spawn.randomSmallMob(2200, -1775); + + if (game.levelsCleared > 2) spawn.snaker(-1300 + Math.random() * 2000, -2200); //boss snake with head + }, + office() { + level.defaultZoom = 1400 + game.zoomTransition(level.defaultZoom) + + if (Math.random() < 0.75) { + //normal direction start in top left + mech.setPosToSpawn(1375, -1550); //normal spawn + level.exit.x = 3250; + level.exit.y = -530; + // spawn.randomSmallMob(3550, -550); + } else { + //reverse direction, start in bottom right + mech.setPosToSpawn(3250, -530); //normal spawn + level.exit.x = 1375; + level.exit.y = -1530; + spawn.bodyRect(3655, -650, 40, 150); //door + } + spawn.mapRect(level.exit.x, level.exit.y + 20, 100, 50); //ground bump wall + level.enter.x = mech.spawnPos.x - 50; + level.enter.y = mech.spawnPos.y + 20; + this.addZone(level.exit.x, level.exit.y, 100, 30, "nextLevel"); + + document.body.style.backgroundColor = "#e0e5e0"; + //foreground + level.fill.push({ + x: -550, + y: -1700, + width: 1300, + height: 1700, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 750, + y: -1450, + width: 650, + height: 1450, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 750, + y: -1950, + width: 800, + height: 450, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 3000, + y: -1000, + width: 650, + height: 1000, + color: "rgba(0,0,0,0.1)" + }); + level.fill.push({ + x: 3650, + y: -1300, + width: 1300, + height: 1300, + color: "rgba(0,0,0,0.1)" + }); + + //mech.setPosToSpawn(600, -1200); //normal spawn + //mech.setPosToSpawn(525, -150); //ground first building + //mech.setPosToSpawn(3150, -700); //near exit spawn + spawn.debris(-300, -200, 1000, 5); //ground debris //20 debris per level + spawn.debris(3500, -200, 800, 5); //ground debris //20 debris per level + spawn.debris(-300, -650, 1200, 5); //1st floor debris //20 debris per level + spawn.debris(3500, -650, 800, 5); //1st floor debris //20 debris per level + powerUps.spawnStartingPowerUps(-525, -700); + + spawn.mapRect(-600, 25, 5600, 300); //ground + spawn.mapRect(-600, 0, 2000, 50); //ground + spawn.mapRect(-600, -1700, 50, 2000 - 100); //left wall + spawn.bodyRect(-295, -1540, 40, 40); //center block under wall + spawn.bodyRect(-298, -1580, 40, 40); //center block under wall + spawn.bodyRect(1500, -1540, 30, 30); //left of entrance + + spawn.mapRect(1550, -2000, 50, 550); //right wall + spawn.mapRect(1350, -2000 + 505, 50, 1295); //right wall + spawn.mapRect(-600, -2000 + 250, 2000 - 700, 50); //roof left + spawn.mapRect(-600 + 1300, -2000, 50, 300); //right roof wall + spawn.mapRect(-600 + 1300, -2000, 900, 50); //center wall + map[map.length] = Bodies.polygon(425, -1700, 0, 15); //circle above door + spawn.bodyRect(420, -1675, 15, 170, 1, spawn.propsDoor); // door + body[body.length - 1].isNotHoldable = true; + //makes door swing + consBB[consBB.length] = Constraint.create({ + bodyA: body[body.length - 1], + pointA: { + x: 0, + y: -90 + }, + bodyB: map[map.length - 1], + stiffness: 1 + }); + spawn.mapRect(-600 + 300, -2000 * 0.75, 1900, 50); //3rd floor + spawn.mapRect(-600 + 2000 * 0.7, -2000 * 0.74, 50, 375); //center wall + spawn.bodyRect(-600 + 2000 * 0.7, -2000 * 0.5 - 106, 50, 106); //center block under wall + spawn.mapRect(-600, -1000, 1100, 50); //2nd floor + spawn.mapRect(600, -1000, 500, 50); //2nd floor + spawn.spawnStairs(-600, -1000, 4, 250, 350); //stairs 2nd + spawn.mapRect(350, -600, 350, 150); //center table + spawn.mapRect(-600 + 300, -2000 * 0.25, 2000 - 300, 50); //1st floor + spawn.spawnStairs(-600 + 2000 - 50, -500, 4, 250, 350, true); //stairs 1st + spawn.spawnStairs(-600, 0, 4, 250, 350); //stairs ground + spawn.bodyRect(700, -200, 100, 100); //center block under wall + spawn.bodyRect(700, -300, 100, 100); //center block under wall + spawn.bodyRect(700, -400, 100, 100); //center block under wall + spawn.mapRect(1390, 13, 30, 20); //step left + spawn.mapRect(2980, 13, 30, 20); //step right + spawn.mapRect(3000, 0, 2000, 50); //ground + spawn.bodyRect(4250, -700, 50, 100); + spawn.bodyRect(3000, -200, 50, 200); //door + spawn.mapRect(3000, -1000, 50, 800); //left wall + spawn.mapRect(3000 + 2000 - 50, -1300, 50, 1100); //right wall + spawn.mapRect(4150, -600, 350, 150); //table + spawn.mapRect(3650, -1300, 50, 650); //exit wall + spawn.mapRect(3650, -1300, 1350, 50); //exit wall + + spawn.mapRect(3000, -2000 * 0.5, 700, 50); //exit roof + spawn.mapRect(3000, -2000 * 0.25, 2000 - 300, 50); //1st floor + spawn.spawnStairs(3000 + 2000 - 50, 0, 4, 250, 350, true); //stairs ground + + // tether ball + if (game.levelsCleared > 2) { + level.fillBG.push({ + x: 2495, + y: -500, + width: 10, + height: 525, + color: "#ccc" + }); + spawn.tether(2850, -80) + cons[cons.length] = Constraint.create({ + pointA: { + x: 2500, + y: -500 + }, + bodyB: mob[mob.length - 1], + stiffness: 0.00012 + }); + //chance to spawn a ring of exploding mobs around this boss + if (game.levelsCleared > 4) spawn.nodeBoss(2850, -80, "spawns", 8, 20, 105); + } + + spawn.randomSmallMob(4575, -560, 1); + spawn.randomSmallMob(1315, -880, 1); + spawn.randomSmallMob(800, -600); + spawn.randomSmallMob(-100, -1600); + spawn.randomMob(4100, -225, 0.8); + spawn.randomMob(-250, -700, 0.8); + spawn.randomMob(4500, -225, 0.15); + spawn.randomMob(3250, -225, 0.15); + spawn.randomMob(-100, -225, 0.1); + spawn.randomMob(1150, -225, 0.15); + spawn.randomMob(2000, -225, 0.15); + spawn.randomMob(450, -225, 0.15); + spawn.randomMob(100, -1200, 1); + spawn.randomMob(950, -1150, -0.1); + spawn.randomBoss(1800, -800, -0.2); + spawn.randomBoss(4150, -1000, 0.6); + }, + //***************************************************************************************************************** + //***************************************************************************************************************** + //***************************************************************************************************************** + //***************************************************************************************************************** + //***************************************************************************************************************** + //***************************************************************************************************************** + //***************************************************************************************************************** + enter: { + x: 0, + y: 0, + draw() { + ctx.beginPath(); + ctx.moveTo(this.x, this.y + 30); + ctx.lineTo(this.x, this.y - 80); + ctx.bezierCurveTo(this.x, this.y - 170, this.x + 100, this.y - 170, this.x + 100, this.y - 80); + ctx.lineTo(this.x + 100, this.y + 30); + ctx.lineTo(this.x, this.y + 30); + ctx.fillStyle = "#ccc"; + ctx.fill(); + } + }, + exit: { + x: 0, + y: 0, + draw() { + ctx.beginPath(); + ctx.moveTo(this.x, this.y + 30); + ctx.lineTo(this.x, this.y - 80); + ctx.bezierCurveTo(this.x, this.y - 170, this.x + 100, this.y - 170, this.x + 100, this.y - 80); + ctx.lineTo(this.x + 100, this.y + 30); + ctx.lineTo(this.x, this.y + 30); + ctx.fillStyle = "#0ff"; + ctx.fill(); + } + }, + fillBG: [], + drawFillBGs() { + for (let i = 0, len = level.fillBG.length; i < len; ++i) { + const f = level.fillBG[i]; + ctx.fillStyle = f.color; + ctx.fillRect(f.x, f.y, f.width, f.height); + } + }, + + fill: [], + drawFills() { + for (let i = 0, len = level.fill.length; i < len; ++i) { + const f = level.fill[i]; + ctx.fillStyle = f.color; + ctx.fillRect(f.x, f.y, f.width, f.height); + } + }, + zones: [], //zone do actions when player is in a region // to effect everything use a query + checkZones() { + for (let i = 0, len = this.zones.length; i < len; ++i) { + if ( + player.position.x > this.zones[i].x1 && + player.position.x < this.zones[i].x2 && + player.position.y > this.zones[i].y1 && + player.position.y < this.zones[i].y2 + ) { + this.zoneActions[this.zones[i].action](i); + break; + } + } + }, + addZone(x, y, width, height, action, info) { + this.zones[this.zones.length] = { + x1: x, + y1: y - 150, + x2: x + width, + y2: y + height - 70, //-70 to adjust for player height + action: action, + info: info + }; + }, + zoneActions: { + fling(i) { + Matter.Body.setVelocity(player, { + x: level.zones[i].info.Vx, + y: level.zones[i].info.Vy + }); + }, + nextLevel() { + //enter when player isn't falling + if (player.velocity.y < 0.1) { + game.levelsCleared++; + if (game.levelsCleared > 1) level.difficultyIncrease() + //cycles map to next level + level.onLevel++; + if (level.onLevel > level.levels.length - 1) level.onLevel = 0; + + game.clearNow = true; //triggers in the physics engine to remove all physics bodies + } + }, + death() { + mech.death(); + }, + laser(i) { + //draw these in game with spawn.background + mech.damage(level.zones[i].info.dmg); + }, + slow() { + Matter.Body.setVelocity(player, { + x: player.velocity.x * 0.5, + y: player.velocity.y * 0.5 + }); + } + }, + queryList: [], //queries do actions on many objects in regions (for only player use a zone) + checkQuery() { + let bounds, action, info; + + function isInZone(targetArray) { + let results = Matter.Query.region(targetArray, bounds); + for (let i = 0, len = results.length; i < len; ++i) { + level.queryActions[action](results[i], info); + } + } + for (let i = 0, len = level.queryList.length; i < len; ++i) { + bounds = level.queryList[i].bounds; + action = level.queryList[i].action; + info = level.queryList[i].info; + for (let j = 0, l = level.queryList[i].groups.length; j < l; ++j) { + isInZone(level.queryList[i].groups[j]); + } + } + }, + //oddly query regions can't get smaller than 50 width? + addQueryRegion(x, y, width, height, action, groups = [ + [player], body, mob, powerUp, bullet + ], info) { + this.queryList[this.queryList.length] = { + bounds: { + min: { + x: x, + y: y + }, + max: { + x: x + width, + y: y + height + } + }, + action: action, + groups: groups, + info: info + }; + }, + queryActions: { + bounce(target, info) { + //jerky fling upwards + Matter.Body.setVelocity(target, { + x: info.Vx + (Math.random() - 0.5) * 6, + y: info.Vy + }); + target.torque = (Math.random() - 0.5) * 2 * target.mass; + }, + boost(target, info) { + // if (target.velocity.y < 0) { + // mech.undoCrouch(); + // mech.enterAir(); + mech.buttonCD_jump = 0; // reset short jump counter to prevent short jumps on boosts + mech.hardLandCD = 0 // disable hard landing + Matter.Body.setVelocity(target, { + x: target.velocity.x + (Math.random() - 0.5) * 2, + y: info + }); + }, + force(target, info) { + if (target.velocity.y < 0) { + //gently force up if already on the way up + target.force.x += info.Vx * target.mass; + target.force.y += info.Vy * target.mass; + } else { + target.force.y -= 0.0007 * target.mass; //gently fall in on the way down + } + }, + antiGrav(target) { + target.force.y -= 0.0011 * target.mass; + }, + death(target) { + target.death(); + } + }, + levelAnnounce() { + document.title = "n-gon: L" + (game.levelsCleared) + " " + level.levels[level.onLevel]; + game.makeTextLog(`
level ${game.levelsCleared}
${level.levels[level.onLevel]}
`, 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)

${b.guns[newGun].name}
(left click)

${b.guns[newGun].description}

`, 1000); + // } else { + // game.makeTextLog(`${b.guns[newGun].name}
(left click)

${b.guns[newGun].description}

`, 1000); + // } + b.guns[newGun].have = true; + b.inventory.push(newGun); + b.guns[newGun].ammo += b.guns[newGun].ammoPack * 2; + game.makeGunHUD(); + } else { + //if you have all guns then get ammo + const ammoTarget = Math.floor(Math.random() * (b.guns.length)); + const ammo = Math.ceil(b.guns[ammoTarget].ammoPack * 2); + b.guns[ammoTarget].ammo += ammo; + game.updateGunHUD(); + game.makeTextLog("+" + ammo + " ammo: " + b.guns[ammoTarget].name, 180); + } + } + }, + spawnRandomPowerUp(x, y) { //mostly used after mob dies + if (Math.random() * Math.random() - 0.25 > Math.sqrt(mech.health) || Math.random() < 0.04) { //spawn heal chance is higher at low health + powerUps.spawn(x, y, "heal"); + return; + } + if (Math.random() < 0.19) { + if (b.inventory.length > 0) powerUps.spawn(x, y, "ammo"); + return; + } + if (Math.random() < 0.004 * (5 - b.inventory.length)) { //a new gun has a low chance for each not acquired gun to drop + powerUps.spawn(x, y, "gun"); + return; + } + if (Math.random() < 0.008) { + powerUps.spawn(x, y, "mod"); + return; + } + if (Math.random() < 0.005) { + powerUps.spawn(x, y, "field"); + return; + } + }, + spawnBossPowerUp(x, y) { //boss spawns field and gun mod upgrades + if (mech.fieldMode === 0) { + powerUps.spawn(x, y, "field") + } else if (Math.random() < 0.35) { + powerUps.spawn(x, y, "mod") + } else if (Math.random() < 0.27) { + powerUps.spawn(x, y, "field"); + } else if (Math.random() < 0.04 * (7 - b.inventory.length)) { //a new gun has a low chance for each not acquired gun to drop + powerUps.spawn(x, y, "gun") + } else if (mech.health < 0.5) { + powerUps.spawn(x, y, "heal"); + } else { + powerUps.spawn(x, y, "ammo"); + } + }, + chooseRandomPowerUp(x, y) { //100% chance to drop a random power up //used in spawn.debris + if (Math.random() < 0.5) { + powerUps.spawn(x, y, "heal", false); + } else { + powerUps.spawn(x, y, "ammo", false); + } + }, + spawnStartingPowerUps(x, y) { //used for map specific power ups, mostly to give player a starting gun + if (b.inventory.length < 2) { + powerUps.spawn(x, y, "gun", false); //starting gun + } else { + powerUps.spawnRandomPowerUp(x, y); + powerUps.spawnRandomPowerUp(x, y); + powerUps.spawnRandomPowerUp(x, y); + powerUps.spawnRandomPowerUp(x, y); + } + }, + spawn(x, y, target, moving = true, mode = null) { + let i = powerUp.length; + target = powerUps[target]; + size = target.size(); + powerUp[i] = Matter.Bodies.polygon(x, y, 0, size, { + density: 0.001, + frictionAir: 0.01, + restitution: 0.8, + inertia: Infinity, //prevents rotation + collisionFilter: { + group: 0, + category: 0x100000, + mask: 0x100001 + }, + color: target.color, + effect: target.effect, + mode: mode, + name: target.name, + size: size + }); + if (moving) { + Matter.Body.setVelocity(powerUp[i], { + x: (Math.random() - 0.5) * 15, + y: Math.random() * -9 - 3 + }); + } + World.add(engine.world, powerUp[i]); //add to world + }, +}; \ No newline at end of file diff --git a/js/spawn.js b/js/spawn.js new file mode 100644 index 0000000..6ab1a6e --- /dev/null +++ b/js/spawn.js @@ -0,0 +1,1722 @@ +//main object for spawning things in a level +const spawn = { + pickList: ["starter", "starter"], + fullPickList: [ + "chaser", "chaser", "chaser", + "striker", "striker", + "spinner", + "hopper", "hopper", "hopper", "hopper", + "grower", + "springer", + "shooter", "shooter", "shooter", "shooter", "shooter", + "beamer", + "focuser", + "laser", "laser", + // "blinker", //make blinker a boss + "sucker", + "exploder", "exploder", "exploder", + "spawner", + "ghoster", + "sneaker", + ], + allowedBossList: ["chaser", "spinner", "striker", "springer", "laser", "focuser", "beamer", "exploder", "spawner", "shooter"], //"zoomer", + setSpawnList() { + //this is run at the start of each new level to determine the possible mobs for the level + //each level has 2 mobs: one new mob and one from the last level + spawn.pickList.splice(0, 1); + spawn.pickList.push(spawn.fullPickList[Math.floor(Math.random() * spawn.fullPickList.length)]); + }, + randomMob(x, y, chance = 1) { + if (Math.random() < chance + 0.09 * (game.levelsCleared - 1) && mob.length < 4 + game.levelsCleared * 1.7) { + const pick = this.pickList[Math.floor(Math.random() * this.pickList.length)]; + this[pick](x, y); + } + }, + randomSmallMob(x, y, + num = Math.max(Math.min(Math.round(Math.random() * (game.levelsCleared - 1) * 0.45 - 0.4), 4), 0), + size = 16 + Math.ceil(Math.random() * 15), + chance = 1) { + if (Math.random() < chance + (game.levelsCleared - 1) * 0.03 && mob.length < 4 + game.levelsCleared * 1.7) { + for (let i = 0; i < num; ++i) { + const pick = this.pickList[Math.floor(Math.random() * this.pickList.length)]; + this[pick](x + Math.round((Math.random() - 0.5) * 20) + i * size * 2.5, y + Math.round((Math.random() - 0.5) * 20), size); + } + } + }, + randomBoss(x, y, chance = 1) { + if (Math.random() < chance + (game.levelsCleared - 1) * 0.14 && game.levelsCleared !== 1 && mob.length < 4 + game.levelsCleared * 2 || chance == Infinity) { + //choose from the possible picklist + let pick = this.pickList[Math.floor(Math.random() * this.pickList.length)]; + //is the pick able to be a boss? + let canBeBoss = false; + for (let i = 0, len = this.allowedBossList.length; i < len; ++i) { + if (this.allowedBossList[i] === pick) { + canBeBoss = true; + break; + } + } + if (canBeBoss) { + if (Math.random() < 0.55) { + this.nodeBoss(x, y, pick); + } else { + this.lineBoss(x, y, pick); + } + } else { + if (Math.random() < 0.07) { + this[pick](x, y, 90 + Math.random() * 40); //one extra large mob + } else if (Math.random() < 0.35) { + this.groupBoss(x, y) //hidden grouping blocks + } else { + pick = (Math.random() < 0.5) ? "randomList" : "random"; + if (Math.random() < 0.55) { + this.nodeBoss(x, y, pick); + } else { + this.lineBoss(x, y, pick); + } + } + } + } + }, + + //mob templates ********************************************************************************************* + //*********************************************************************************************************** + groupBoss(x, y, num = 5 + Math.random() * 8) { + for (let i = 0; i < num; i++) { + const radius = 25 + Math.floor(Math.random() * 20) + spawn.grouper(x + Math.random() * radius, y + Math.random() * radius, radius); + } + }, + grouper(x, y, radius = 27 + Math.floor(Math.random() * 10)) { + mobs.spawn(x, y, 4, radius, "#777"); + let me = mob[mob.length - 1]; + me.g = 0.0002; //required if using 'gravity' + me.accelMag = 0.0007 * game.accelScale; + me.groupingRangeMax = 250000 + Math.random() * 100000; + me.groupingRangeMin = (radius * 8) * (radius * 8); + me.groupingStrength = 0.0005 + me.memory = 200; + + me.do = function () { + this.gravity(); + if (this.seePlayer.recall) { + this.seePlayerByDistAndLOS(); + this.healthBar(); + this.attraction(); + //tether to other blocks + ctx.beginPath(); + for (let i = 0, len = mob.length; i < len; i++) { + if (mob[i] != this && mob[i].dropPowerUp) { //don't tether to self, bullets, shields, ... + const distance2 = Matter.Vector.magnitudeSquared(Matter.Vector.sub(this.position, mob[i].position)) + if (distance2 < this.groupingRangeMax) { + if (!mob[i].seePlayer.recall) mob[i].seePlayerByDistAndLOS(); //wake up sleepy mobs + if (distance2 > this.groupingRangeMin) { + const angle = Math.atan2(mob[i].position.y - this.position.y, mob[i].position.x - this.position.x); + const forceMag = this.groupingStrength * mob[i].mass; + mob[i].force.x -= forceMag * Math.cos(angle); + mob[i].force.y -= forceMag * Math.sin(angle); + } + ctx.moveTo(this.position.x, this.position.y); + ctx.lineTo(mob[i].position.x, mob[i].position.y); + } + } + } + ctx.strokeStyle = "#000"; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + }, + starter(x, y, radius = 30) { + //easy mob for on level 1 + mobs.spawn(x, y, 8, radius, "#9ccdc6"); + let me = mob[mob.length - 1]; + me.accelMag = 0.00055 * game.accelScale; + me.memory = 60; + Matter.Body.setDensity(me, 0.0005) // normal density is 0.001 // this reduces life by half and decreases knockback + + me.do = function () { + this.healthBar(); + this.seePlayerByLookingAt(); + this.attraction(); + }; + }, + healer(x, y, radius = 20) { + //easy mob for on level 1 + mobs.spawn(x, y, 3, radius, "rgba(50,255,200,0.4)"); + let me = mob[mob.length - 1]; + me.frictionAir = 0.02; + me.accelMag = 0.0004 * game.accelScale; + if (map.length) me.searchTarget = map[Math.floor(Math.random() * (map.length - 1))].position; //required for search + me.lookFrequency = 160 + Math.floor(57 * Math.random()) + me.lockedOn = null; + Matter.Body.setDensity(me, 0.003) // normal density is 0.001 + + me.do = function () { + this.healthBar(); + + if (!(game.cycle % this.lookFrequency)) { + //slow self heal + this.health += 0.02; + if (this.health > 1) this.health = 1; + + //target mobs with low health + let closeDist = Infinity; + for (let i = 0; i < mob.length; i++) { + if (mob[i] != this && Matter.Query.ray(map, this.position, mob[i].position).length === 0) { + const TARGET_VECTOR = Matter.Vector.sub(this.position, mob[i].position) + const DIST = Matter.Vector.magnitude(TARGET_VECTOR) * mob[i].health * mob[i].health * mob[i].health; //distance is multiplied by mob health to prioritize low health mobs + if (DIST < closeDist) { + closeDist = DIST; + this.lockedOn = mob[i] + } + } + } + } + + //move away from player if too close + if (this.distanceToPlayer2() < 400000) { + const TARGET_VECTOR = Matter.Vector.sub(this.position, player.position) + this.force = Matter.Vector.mult(Matter.Vector.normalise(TARGET_VECTOR), this.mass * this.accelMag * 1.4) + if (this.lockedOn) this.lockedOn = null + } else if (this.lockedOn && this.lockedOn.alive) { + //move towards and heal locked on target + const TARGET_VECTOR = Matter.Vector.sub(this.position, this.lockedOn.position) + const DIST = Matter.Vector.magnitude(TARGET_VECTOR); + if (DIST > 250) { + this.force = Matter.Vector.mult(Matter.Vector.normalise(TARGET_VECTOR), -this.mass * this.accelMag) + } else { + if (this.lockedOn.health < 1) { + this.lockedOn.health += 0.002; + if (this.lockedOn.health > 1) this.lockedOn.health = 1; + //spin when healing + this.torque = 0.000005 * this.inertia; + //draw heal + ctx.beginPath(); + ctx.moveTo(this.position.x, this.position.y); + ctx.lineTo(this.lockedOn.position.x, this.lockedOn.position.y); + ctx.lineWidth = 10 + ctx.strokeStyle = "rgba(50,255,200,0.4)" + ctx.stroke(); + } + } + } else { + //wander if no heal targets visible + //be sure to declare searchTarget in mob spawn + const newTarget = function (that) { + that.searchTarget = mob[Math.floor(Math.random() * (mob.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.6 of normal acceleration + this.force = Matter.Vector.mult(Matter.Vector.normalise(sub), this.accelMag * this.mass * 0.6); + } else { + //after reaching random target switch to new target + newTarget(this); + } + //switch to a new target after a while + if (!(game.cycle % (this.lookFrequency * 15))) { + newTarget(this); + } + + } + }; + }, + chaser(x, y, radius = 35 + Math.ceil(Math.random() * 40)) { + mobs.spawn(x, y, 8, radius, "#2c9790"); + let me = mob[mob.length - 1]; + // Matter.Body.setDensity(me, 0.0007); //extra dense //normal is 0.001 //makes effective life much lower + me.friction = 0; + me.frictionAir = 0; + me.accelMag = 0.001 * game.accelScale;; + me.g = me.accelMag * 0.6; //required if using 'gravity' + me.memory = 50; + if (Math.random() < Math.min((game.levelsCleared - 1) * 0.1, 0.7)) spawn.shield(me, x, y); + me.do = function () { + this.healthBar(); + this.gravity(); + this.seePlayerCheck(); + this.attraction(); + }; + }, + grower(x, y, radius = 15) { + mobs.spawn(x, y, 7, radius, "hsl(144, 15%, 50%)"); + let me = mob[mob.length - 1]; + me.big = false; //required for grow + me.accelMag = 0.00045 * game.accelScale; + me.do = function () { + this.healthBar(); + this.seePlayerByLookingAt(); + this.attraction(); + this.grow(); + }; + }, + springer(x, y, radius = 20 + Math.ceil(Math.random() * 35)) { + mobs.spawn(x, y, 8, radius, "#b386e8"); + let me = mob[mob.length - 1]; + me.friction = 0; + me.frictionAir = 0.1; + me.lookTorque = 0.000005; + me.g = 0.0002; //required if using 'gravity' + me.seePlayerFreq = Math.round((40 + 25 * Math.random()) * game.lookFreqScale); + const springStiffness = 0.002; + const springDampening = 0.1; + + me.springTarget = { + x: me.position.x, + y: me.position.y + }; + const len = cons.length; + cons[len] = Constraint.create({ + pointA: me.springTarget, + bodyB: me, + stiffness: springStiffness, + damping: springDampening + }); + cons[len].length = 100 + 1.5 * radius; + me.cons = cons[len]; + + me.springTarget2 = { + x: me.position.x, + y: me.position.y + }; + const len2 = cons.length; + cons[len2] = Constraint.create({ + pointA: me.springTarget2, + bodyB: me, + stiffness: springStiffness, + damping: springDampening + }); + cons[len2].length = 100 + 1.5 * radius; + me.cons2 = cons[len2]; + + me.onDeath = function () { + this.removeCons(); + }; + if (Math.random() < Math.min((game.levelsCleared - 1) * 0.1, 0.7)) spawn.shield(me, x, y); + me.do = function () { + this.healthBar(); + this.gravity(); + this.searchSpring(); + }; + }, + zoomer(x, y, radius = 20 + Math.ceil(Math.random() * 30)) { + mobs.spawn(x, y, 6, radius, "#ffe2fd"); + let me = mob[mob.length - 1]; + me.trailLength = 20; //required for trails + me.setupTrail(); //fill trails array up with the current position of mob + me.trailFill = "#ff00f0"; + me.g = 0.001; //required if using 'gravity' + me.frictionAir = 0.02; + me.accelMag = 0.004 * game.accelScale; + me.memory = 30; + me.zoomMode = 150; + me.onHit = function () { + this.zoomMode = 150; + }; + me.do = function () { + this.healthBar(); + this.seePlayerByDistAndLOS(); + this.zoom(); + this.gravity(); + }; + }, + hopper(x, y, radius = 30 + Math.ceil(Math.random() * 30)) { + mobs.spawn(x, y, 5, radius, "rgb(0,200,180)"); + let me = mob[mob.length - 1]; + me.accelMag = 0.04 * game.accelScale; + me.g = 0.0015; //required if using 'gravity' + me.frictionAir = 0.018; + me.restitution = 0; + me.delay = 110; + me.randomHopFrequency = 50 + Math.floor(Math.random() * 1000); + me.randomHopCD = game.cycle + me.randomHopFrequency; + me.do = function () { + this.healthBar(); + this.gravity(); + this.seePlayerCheck(); + this.hop(); + //randomly hob if not aware of player + if (this.randomHopCD < game.cycle && this.speed < 1 && !this.seePlayer.recall) { + this.randomHopCD = game.cycle + this.randomHopFrequency; + //slowly change randomHopFrequency after each hop + this.randomHopFrequency = Math.max(100, this.randomHopFrequency + (0.5 - Math.random()) * 200); + const forceMag = (this.accelMag + this.accelMag * Math.random()) * this.mass * (0.1 + Math.random() * 0.3); + const angle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI; + this.force.x += forceMag * Math.cos(angle); + this.force.y += forceMag * Math.sin(angle) - 0.04 * this.mass; //antigravity + } + }; + }, + spinner(x, y, radius = 30 + Math.ceil(Math.random() * 35)) { + mobs.spawn(x, y, 5, radius, "#000000"); + let me = mob[mob.length - 1]; + me.fill = "#28b"; + me.rememberFill = me.fill; + me.cdBurst1 = 0; //must add for burstAttraction + me.cdBurst2 = 0; //must add for burstAttraction + me.delay = 0; + me.burstDir = { + x: 0, + y: 0 + }; + me.accelMag = 0.16 * game.accelScale; + me.frictionAir = 0.022; + me.lookTorque = 0.0000014; + me.restitution = 0; + me.do = function () { + this.healthBar(); + this.seePlayerByLookingAt(); + //accelerate towards the player after a delay + if (this.seePlayer.recall) { + if (this.cdBurst2 < game.cycle && this.angularSpeed < 0.01) { + this.cdBurst2 = Infinity; + this.cdBurst1 = game.cycle + 40; + this.burstDir = Matter.Vector.normalise(Matter.Vector.sub(this.seePlayer.position, this.position)); + } else if (this.cdBurst1 < game.cycle) { + this.cdBurst2 = game.cycle + this.delay; + this.cdBurst1 = Infinity; + this.force = Matter.Vector.mult(this.burstDir, this.mass * 0.25); + this.fill = this.rememberFill; + } else if (this.cdBurst1 != Infinity) { + this.torque += 0.000035 * this.inertia; + this.fill = randomColor({ + hue: "blue" + }); + //draw attack vector + const mag = this.radius * 2 + 200; + const gradient = ctx.createRadialGradient(this.position.x, this.position.y, 0, this.position.x, this.position.y, mag); + gradient.addColorStop(0, "rgba(0,0,0,0.2)"); + gradient.addColorStop(1, "transparent"); + ctx.strokeStyle = gradient; + ctx.lineWidth = 5; + ctx.setLineDash([10, 20]); //30 + const dir = Matter.Vector.add(this.position, Matter.Vector.mult(this.burstDir, mag)); + ctx.beginPath(); + ctx.moveTo(this.position.x, this.position.y); + ctx.lineTo(dir.x, dir.y); + ctx.stroke(); + ctx.setLineDash([]); + } else { + this.fill = this.rememberFill; + } + } else { + this.cdBurst2 = 0; + } + }; + }, + sucker(x, y, radius = 30 + Math.ceil(Math.random() * 70)) { + radius = 9 + radius / 8; //extra small + mobs.spawn(x, y, 6, radius, "#000"); + let me = mob[mob.length - 1]; + me.stroke = "transparent"; //used for drawSneaker + me.eventHorizon = radius * 23; //required for blackhole + me.seeAtDistance2 = (me.eventHorizon + 500) * (me.eventHorizon + 500); //vision limit is event horizon + me.accelMag = 0.00009 * game.accelScale; + // me.frictionAir = 0.005; + me.memory = 600; + Matter.Body.setDensity(me, 0.004); //extra dense //normal is 0.001 //makes effective life much larger + me.do = function () { + //keep it slow, to stop issues from explosion knock backs + if (this.speed > 5) { + Matter.Body.setVelocity(this, { + x: this.velocity.x * 0.99, + y: this.velocity.y * 0.99 + }); + } + this.seePlayerByDistOrLOS(); + if (this.seePlayer.recall) { + //eventHorizon waves in and out + eventHorizon = this.eventHorizon * (0.93 + 0.17 * Math.sin(game.cycle * 0.011)) + + //accelerate towards the player + 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); + + //draw darkness + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, eventHorizon * 0.25, 0, 2 * Math.PI); + ctx.fillStyle = "rgba(0,0,0,0.9)"; + ctx.fill(); + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, eventHorizon * 0.55, 0, 2 * Math.PI); + ctx.fillStyle = "rgba(0,0,0,0.5)"; + ctx.fill(); + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, eventHorizon, 0, 2 * Math.PI); + ctx.fillStyle = "rgba(0,0,0,0.1)"; + ctx.fill(); + + this.healthBar(); + //when player is inside event horizon + if (Matter.Vector.magnitude(Matter.Vector.sub(this.position, player.position)) < eventHorizon) { + mech.damage(0.00015 * game.dmgScale); + if (mech.fieldMeter > 0.1) mech.fieldMeter -= 0.01 + const angle = Math.atan2(player.position.y - this.position.y, player.position.x - this.position.x); + player.force.x -= 1.25 * Math.cos(angle) * player.mass * game.g * (mech.onGround ? 1.8 : 1); + player.force.y -= 0.96 * player.mass * game.g * Math.sin(angle); + //draw line to player + 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(); + } + } + } + }, + suckerBoss(x, y, radius = 20) { + mobs.spawn(x, y, 12, radius, "#000"); + let me = mob[mob.length - 1]; + me.stroke = "transparent"; //used for drawSneaker + me.eventHorizon = 1100; //required for black hole + me.seeAtDistance2 = (me.eventHorizon + 1000) * (me.eventHorizon + 1000); //vision limit is event horizon + me.accelMag = 0.00003 * game.accelScale; + me.collisionFilter.mask = 0x001100 + // me.frictionAir = 0.005; + me.memory = 1600; + Matter.Body.setDensity(me, 0.05); //extra dense //normal is 0.001 //makes effective life much larger + me.onDeath = function () { + //applying forces to player doesn't seem to work inside this method, not sure why + powerUps.spawnBossPowerUp(this.position.x, this.position.y) + if (game.levelsCleared > 5) { + for (let i = 0; i < (game.levelsCleared - 3); ++i) { + spawn.sucker(this.position.x + (Math.random() - 0.5) * radius * 2, this.position.y + (Math.random() - 0.5) * radius * 2, 70 * Math.random()); + Matter.Body.setVelocity(mob[mob.length - 1], { + x: (Math.random() - 0.5) * 70, + y: (Math.random() - 0.5) * 70 + }); + } + } + }; + me.do = function () { + //keep it slow, to stop issues from explosion knock backs + if (this.speed > 1) { + Matter.Body.setVelocity(this, { + x: this.velocity.x * 0.95, + y: this.velocity.y * 0.95 + }); + } + this.seePlayerByDistOrLOS(); + if (this.seePlayer.recall) { + //accelerate towards the player + 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); + + //eventHorizon waves in and out + eventHorizon = this.eventHorizon * (1 + 0.2 * Math.sin(game.cycle * 0.008)) + // zoom camera in and out with the event horizon + + //draw darkness + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, eventHorizon * 0.2, 0, 2 * Math.PI); + ctx.fillStyle = "rgba(0,20,40,0.6)"; + ctx.fill(); + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, eventHorizon * 0.4, 0, 2 * Math.PI); + ctx.fillStyle = "rgba(0,20,40,0.4)"; + ctx.fill(); + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, eventHorizon * 0.6, 0, 2 * Math.PI); + ctx.fillStyle = "rgba(0,20,40,0.3)"; + ctx.fill(); + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, eventHorizon * 0.8, 0, 2 * Math.PI); + ctx.fillStyle = "rgba(0,20,40,0.2)"; + ctx.fill(); + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, eventHorizon, 0, 2 * Math.PI); + ctx.fillStyle = "rgba(0,0,0,0.05)"; + ctx.fill(); + //when player is inside event horizon + if (Matter.Vector.magnitude(Matter.Vector.sub(this.position, player.position)) < eventHorizon) { + mech.damage(0.00015 * game.dmgScale); + if (mech.fieldMeter > 0.1) mech.fieldMeter -= 0.01 + const angle = Math.atan2(player.position.y - this.position.y, player.position.x - this.position.x); + player.force.x -= 1.3 * Math.cos(angle) * player.mass * game.g * (mech.onGround ? 1.7 : 1); + player.force.y -= 1.2 * Math.sin(angle) * player.mass * game.g; + //draw line to player + 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(); + } + this.healthBar(); + this.curl(eventHorizon); + } + } + }, + beamer(x, y, radius = 15 + Math.ceil(Math.random() * 15)) { + mobs.spawn(x, y, 4, radius, "rgb(255,0,190)"); + let me = mob[mob.length - 1]; + me.repulsionRange = 73000; //squared + me.laserRange = 370; + me.accelMag = 0.0005 * game.accelScale; + me.frictionStatic = 0; + me.friction = 0; + if (Math.random() < Math.min(0.2 + (game.levelsCleared - 1) * 0.1, 0.7)) spawn.shield(me, x, y); + me.do = function () { + this.healthBar(); + this.seePlayerByLookingAt(); + this.attraction(); + this.repulsion(); + //laser beam + this.laserBeam(); + }; + }, + focuser(x, y, radius = 30 + Math.ceil(Math.random() * 10)) { + radius = Math.ceil(radius * 0.7); + mobs.spawn(x, y, 4, radius, "rgb(0,0,255)"); + let me = mob[mob.length - 1]; + Matter.Body.setDensity(me, 0.003); //extra dense //normal is 0.001 + me.restitution = 0; + me.laserPos = me.position; //required for laserTracking + me.repulsionRange = 1200000; //squared + me.accelMag = 0.0002 * game.accelScale; + me.frictionStatic = 0; + me.friction = 0; + me.onDamage = function () { + this.laserPos = this.position; + }; + // if (Math.random() < Math.min(0.2 + game.levelsCleared * 0.1, 0.7)) spawn.shield(me, x, y); + me.do = function () { + this.healthBar(); + if (!mech.isBodiesAsleep) { + this.seePlayerByLookingAt(); + const dist2 = this.distanceToPlayer2(); + //laser Tracking + if (this.seePlayer.yes && dist2 < 4000000 && !mech.isStealth) { + this.attraction(); + const rangeWidth = 2000; //this is sqrt of 4000000 from above if() + //targeting laser will slowly move from the mob to the player's position + this.laserPos = Matter.Vector.add(this.laserPos, Matter.Vector.mult(Matter.Vector.sub(player.position, this.laserPos), 0.1)); + let targetDist = Matter.Vector.magnitude(Matter.Vector.sub(this.laserPos, mech.pos)); + const r = 10; + + // ctx.setLineDash([15, 200]); + // ctx.lineDashOffset = 20*(game.cycle % 215); + ctx.beginPath(); + ctx.moveTo(this.position.x, this.position.y); + if (targetDist < r + 15) { + // || dist2 < 80000 + targetDist = r + 10; + //charge at player + const forceMag = this.accelMag * 40 * 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); + } else { + //high friction if can't lock onto player + Matter.Body.setVelocity(this, { + x: this.velocity.x * 0.96, + y: this.velocity.y * 0.96 + }); + } + if (dist2 > 80000) { + const laserWidth = 0.002; + let laserOffR = Matter.Vector.rotateAbout(this.laserPos, (targetDist - r) * laserWidth, this.position); + let sub = Matter.Vector.normalise(Matter.Vector.sub(laserOffR, this.position)); + laserOffR = Matter.Vector.add(laserOffR, Matter.Vector.mult(sub, rangeWidth)); + ctx.lineTo(laserOffR.x, laserOffR.y); + + let laserOffL = Matter.Vector.rotateAbout(this.laserPos, (targetDist - r) * -laserWidth, this.position); + sub = Matter.Vector.normalise(Matter.Vector.sub(laserOffL, this.position)); + laserOffL = Matter.Vector.add(laserOffL, Matter.Vector.mult(sub, rangeWidth)); + ctx.lineTo(laserOffL.x, laserOffL.y); + // ctx.fillStyle = "rgba(0,0,255,0.15)"; + var gradient = ctx.createRadialGradient(this.position.x, this.position.y, 0, this.position.x, this.position.y, rangeWidth); + gradient.addColorStop(0, `rgba(0,0,255,${((r + 5) * (r + 5)) / (targetDist * targetDist)})`); + gradient.addColorStop(1, "transparent"); + ctx.fillStyle = gradient; + ctx.fill(); + } + } else { + this.laserPos = this.position; + } + }; + } + }, + laser(x, y, radius = 30) { + //only on level 1 + mobs.spawn(x, y, 3, radius, "#f00"); + let me = mob[mob.length - 1]; + me.vertices = Matter.Vertices.rotate(me.vertices, Math.PI, me.position); //make the pointy side of triangle the front + Matter.Body.rotate(me, Math.random() * Math.PI * 2); + me.accelMag = 0.00007 * game.accelScale; + me.onHit = function () { + //run this function on hitting player + this.explode(); + }; + me.do = function () { + this.healthBar(); + this.seePlayerByLookingAt(); + this.attraction(); + this.laser(); + }; + }, + striker(x, y, radius = 15 + Math.ceil(Math.random() * 25)) { + mobs.spawn(x, y, 5, radius, "rgb(221,102,119)"); + let me = mob[mob.length - 1]; + me.accelMag = 0.0004 * game.accelScale; + me.g = 0.0002; //required if using 'gravity' + me.frictionStatic = 0; + me.friction = 0; + me.delay = 60; + Matter.Body.rotate(me, Math.PI * 0.1); + me.onDamage = function () { + this.cd = game.cycle + this.delay; + }; + me.do = function () { + this.healthBar(); + this.seePlayerCheck(); + this.attraction(); + this.gravity(); + this.strike(); + }; + }, + sneaker(x, y, radius = 15 + Math.ceil(Math.random() * 25)) { + let me; + mobs.spawn(x, y, 5, radius, "transparent"); + me = mob[mob.length - 1]; + me.accelMag = 0.0007 * game.accelScale; + me.g = 0.0002; //required if using 'gravity' + me.stroke = "transparent"; //used for drawSneaker + me.alpha = 1; //used in drawSneaker + // me.leaveBody = false; + me.canTouchPlayer = false; //used in drawSneaker + me.collisionFilter.mask = 0x010111; //can't touch player + // me.memory = 420; + me.do = function () { + + this.seePlayerCheck(); + this.attraction(); + this.gravity(); + //draw + if (!mech.isBodiesAsleep) { + if (this.seePlayer.yes) { + if (this.alpha < 1) this.alpha += 0.01; + } else { + if (this.alpha > 0) this.alpha -= 0.03; + } + } + if (this.alpha > 0) { + if (this.alpha > 0.95) { + this.healthBar(); + if (!this.canTouchPlayer) { + this.canTouchPlayer = true; + this.collisionFilter.mask = 0x011111; //can touch player + } + } + //draw body + ctx.beginPath(); + const vertices = this.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 = `rgba(0,0,0,${this.alpha * this.alpha})`; + ctx.fill(); + } else if (this.canTouchPlayer) { + this.canTouchPlayer = false; + this.collisionFilter.mask = 0x000111; //can't touch player + } + }; + }, + ghoster(x, y, radius = 40 + Math.ceil(Math.random() * 100)) { + let me; + mobs.spawn(x, y, 7, radius, "transparent"); + me = mob[mob.length - 1]; + me.seeAtDistance2 = 1000000; + me.accelMag = 0.00014 * game.accelScale; + if (map.length) me.searchTarget = map[Math.floor(Math.random() * (map.length - 1))].position; //required for search + Matter.Body.setDensity(me, 0.00065); //normal is 0.001 //makes effective life much lower + me.stroke = "transparent"; //used for drawGhost + me.alpha = 1; //used in drawGhost + me.canTouchPlayer = false; //used in drawGhost + // me.leaveBody = false; + me.collisionFilter.mask = 0x000100; //move through walls and player + me.memory = 480; + me.do = function () { + //cap max speed + if (this.speed > 5) { + Matter.Body.setVelocity(mob[mob.length - 1], { + x: this.velocity.x * 0.8, + y: this.velocity.y * 0.8 + }); + } + this.seePlayerCheckByDistance(); + this.attraction(); + this.search(); + //draw + if (this.distanceToPlayer2() - this.seeAtDistance2 < 0) { + if (this.alpha < 1) this.alpha += 0.004; + } else { + if (this.alpha > 0) this.alpha -= 0.03; + } + if (this.alpha > 0) { + if (this.alpha > 0.9) { + this.healthBar(); + if (!this.canTouchPlayer) { + this.canTouchPlayer = true; + this.collisionFilter.mask = 0x001100; //can touch player but not walls + } + } + //draw body + ctx.beginPath(); + const vertices = this.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.lineWidth = 1; + ctx.strokeStyle = `rgba(0,0,0,${this.alpha * this.alpha})`; + ctx.stroke(); + } else if (this.canTouchPlayer) { + this.canTouchPlayer = false; + this.collisionFilter.mask = 0x000100; //can't touch player or walls + } + }; + }, + blinker(x, y, radius = 45 + Math.ceil(Math.random() * 70)) { + mobs.spawn(x, y, 6, radius, "transparent"); + let me = mob[mob.length - 1]; + Matter.Body.setDensity(me, 0.0005); //normal is 0.001 //makes effective life much lower + me.stroke = "rgb(0,200,255)"; //used for drawGhost + Matter.Body.rotate(me, Math.random() * 2 * Math.PI); + me.blinkRate = 40 + Math.round(Math.random() * 60); //required for blink + me.blinkLength = 150 + Math.round(Math.random() * 200); //required for blink + me.isStatic = true; + me.memory = 360; + me.seePlayerFreq = Math.round((40 + 30 * Math.random()) * game.lookFreqScale); + me.isBig = false; + me.scaleMag = Math.max(5 - me.mass, 1.75); + me.onDeath = function () { + if (this.isBig) { + Matter.Body.scale(this, 1 / this.scaleMag, 1 / this.scaleMag); + this.isBig = false; + } + }; + me.do = function () { + this.healthBar(); + this.seePlayerCheck(); + this.blink(); + //strike by expanding + if (this.isBig) { + if (this.cd - this.delay + 15 < game.cycle) { + Matter.Body.scale(this, 1 / this.scaleMag, 1 / this.scaleMag); + this.isBig = false; + } + } else if (this.seePlayer.yes && this.cd < game.cycle) { + const dist = Matter.Vector.sub(this.seePlayer.position, this.position); + const distMag2 = Matter.Vector.magnitudeSquared(dist); + if (distMag2 < 80000) { + this.cd = game.cycle + this.delay; + Matter.Body.scale(this, this.scaleMag, this.scaleMag); + this.isBig = true; + } + } + }; + }, + bomber(x, y, radius = 120 + Math.ceil(Math.random() * 70)) { + //boss that drops bombs from above and holds a set distance from player + mobs.spawn(x, y, 3, radius, "transparent"); + let me = mob[mob.length - 1]; + Matter.Body.setDensity(me, 0.0015 + 0.0005 * Math.sqrt(game.levelsCleared)); //extra dense //normal is 0.001 //makes effective life much larger + + me.stroke = "rgba(255,0,200)"; //used for drawGhost + me.seeAtDistance2 = 2000000; + me.fireFreq = Math.ceil(30 + 2000 / radius); + me.searchTarget = map[Math.floor(Math.random() * (map.length - 1))].position; //required for search + me.hoverElevation = 400 + (Math.random() - 0.5) * 200; //squared + me.hoverXOff = (Math.random() - 0.5) * 100; + me.accelMag = Math.floor(10 * (Math.random() + 5)) * 0.00001 * game.accelScale; + me.g = 0.0002; //required if using 'gravity' // gravity called in hoverOverPlayer + me.frictionStatic = 0; + me.friction = 0; + me.frictionAir = 0.01; + // me.memory = 300; + // Matter.Body.setDensity(me, 0.0015); //extra dense //normal is 0.001 + me.collisionFilter.mask = 0x001100; //move through walls + spawn.shield(me, x, y); + me.onDeath = function () { + powerUps.spawnBossPowerUp(this.position.x, this.position.y) + }; + me.do = function () { + this.healthBar(); + this.seePlayerCheckByDistance(); + this.hoverOverPlayer(); + this.bomb(); + this.search(); + }; + }, + shooter(x, y, radius = 25 + Math.ceil(Math.random() * 50)) { + mobs.spawn(x, y, 3, radius, "rgb(255,100,150)"); + let me = mob[mob.length - 1]; + me.vertices = Matter.Vertices.rotate(me.vertices, Math.PI, me.position); //make the pointy side of triangle the front + me.memory = 120; + me.fireFreq = 0.007 + Math.random() * 0.005; + me.noseLength = 0; + me.fireAngle = 0; + me.accelMag = 0.0005 * game.accelScale; + me.frictionAir = 0.05; + me.lookTorque = 0.0000025 * (Math.random() > 0.5 ? -1 : 1); + me.fireDir = { + x: 0, + y: 0 + }; + if (Math.random() < Math.min(0.15 + (game.levelsCleared - 1) * 0.1, 0.7)) spawn.shield(me, x, y); + me.do = function () { + this.healthBar(); + this.seePlayerByLookingAt(); + this.fire(); + }; + }, + shooterBoss(x, y, radius = 85 + Math.ceil(Math.random() * 50)) { + //boss spawns on skyscraper level + mobs.spawn(x, y, 3, radius, "rgb(255,70,180)"); + let me = mob[mob.length - 1]; + me.vertices = Matter.Vertices.rotate(me.vertices, Math.PI, me.position); //make the pointy side of triangle the front + me.memory = 240; + me.fireFreq = 0.02; + me.noseLength = 0; + me.fireAngle = 0; + me.accelMag = 0.005 * game.accelScale; + me.frictionAir = 0.1; + me.lookTorque = 0.000005 * (Math.random() > 0.5 ? -1 : 1); + me.fireDir = { + x: 0, + y: 0 + }; + Matter.Body.setDensity(me, 0.001 + 0.0005 * Math.sqrt(game.levelsCleared)); //extra dense //normal is 0.001 //makes effective life much larger + spawn.shield(me, x, y); + me.onDeath = function () { + powerUps.spawnBossPowerUp(this.position.x, this.position.y) + }; + me.do = function () { + this.healthBar(); + this.seePlayerByLookingAt(); + this.fire(); + }; + }, + bullet(x, y, radius = 6, sides = 0) { + //bullets + mobs.spawn(x, y, sides, radius, "rgb(255,0,0)"); + let me = mob[mob.length - 1]; + me.stroke = "transparent"; + me.onHit = function () { + this.explode(); + }; + Matter.Body.setDensity(me, 0.001); //normal is 0.001 + me.timeLeft = 240; + me.g = 0.001; //required if using 'gravity' + me.frictionAir = 0; + me.restitution = 0.8; + me.leaveBody = false; + me.dropPowerUp = false; + me.collisionFilter.category = 0x000010; + me.collisionFilter.mask = 0x011101; + me.do = function () { + this.gravity(); + this.timeLimit(); + }; + }, + spawner(x, y, radius = 55 + Math.ceil(Math.random() * 50)) { + mobs.spawn(x, y, 4, radius, "rgb(255,150,0)"); + let me = mob[mob.length - 1]; + me.g = 0.0004; //required if using 'gravity' + me.leaveBody = false; + // me.dropPowerUp = false; + me.onDeath = function () { //run this function on death + for (let i = 0; i < Math.ceil(this.mass * 0.2 + Math.random() * 3); ++i) { + spawn.spawns(this.position.x + (Math.random() - 0.5) * radius * 2, this.position.y + (Math.random() - 0.5) * radius * 2); + Matter.Body.setVelocity(mob[mob.length - 1], { + x: (Math.random() - 0.5) * 25, + y: (Math.random() - 0.5) * 25 + }); + } + }; + if (Math.random() < Math.min((game.levelsCleared - 1) * 0.1, 0.5)) spawn.shield(me, x, y); + me.do = function () { + this.healthBar(); + this.gravity(); + this.seePlayerCheck(); + this.attraction(); + }; + }, + spawns(x, y, radius = 15 + Math.ceil(Math.random() * 5)) { + mobs.spawn(x, y, 4, radius, "rgb(255,0,0)"); + let me = mob[mob.length - 1]; + me.onHit = function () { + //run this function on hitting player + this.explode(); + }; + me.g = 0.0001; //required if using 'gravity' + me.accelMag = 0.0003 * game.accelScale; + me.memory = 30; + me.leaveBody = false; + me.seePlayerFreq = Math.round((80 + 50 * Math.random()) * game.lookFreqScale); + me.frictionAir = 0.002; + me.do = function () { + this.healthBar(); + this.gravity(); + this.seePlayerCheck(); + this.attraction(); + }; + }, + exploder(x, y, radius = 25 + Math.ceil(Math.random() * 50)) { + mobs.spawn(x, y, 4, radius, "rgb(255,0,0)"); + let me = mob[mob.length - 1]; + me.onHit = function () { + //run this function on hitting player + this.explode(); + }; + me.g = 0.0004; //required if using 'gravity' + me.do = function () { + this.healthBar(); + this.gravity(); + this.seePlayerCheck(); + this.attraction(); + }; + }, + snaker(x, y, radius = 80) { + //snake boss with a laser head + mobs.spawn(x, y, 8, radius, "rgb(255,50,130)"); + let me = mob[mob.length - 1]; + me.accelMag = 0.0012 * game.accelScale; + me.memory = 200; + me.laserRange = 500; + Matter.Body.setDensity(me, 0.001 + 0.0005 * Math.sqrt(game.levelsCleared)); //extra dense //normal is 0.001 //makes effective life much larger + spawn.shield(me, x, y); + if (Math.random() < Math.min((game.levelsCleared - 1) * 0.1, 0.7)) spawn.shield(me, x, y); + me.onDeath = function () { + powerUps.spawnBossPowerUp(this.position.x, this.position.y) + }; + me.do = function () { + this.healthBar(); + this.seePlayerCheck(); + this.attraction(); + this.laserBeam(); + }; + + //snake tail + const nodes = Math.min(3 + Math.ceil(Math.random() * game.levelsCleared + 2), 8) + spawn.lineBoss(x + 105, y, "spawns", nodes); + //constraint boss with first 3 mobs in lineboss + consBB[consBB.length] = Constraint.create({ + bodyA: mob[mob.length - nodes], + bodyB: mob[mob.length - 1 - nodes], + stiffness: 0.05 + }); + consBB[consBB.length] = Constraint.create({ + bodyA: mob[mob.length - nodes + 1], + bodyB: mob[mob.length - 1 - nodes], + stiffness: 0.05 + }); + consBB[consBB.length] = Constraint.create({ + bodyA: mob[mob.length - nodes + 2], + bodyB: mob[mob.length - 1 - nodes], + stiffness: 0.05 + }); + + }, + tether(x, y, radius = 90) { + // constrained mob boss for the towers level + // often has a ring of mobs around it + mobs.spawn(x, y, 8, radius, "rgb(0,60,80)"); + let me = mob[mob.length - 1]; + me.g = 0.0001; //required if using 'gravity' + me.accelMag = 0.002 * game.accelScale; + me.memory = 20; + Matter.Body.setDensity(me, 0.001 + 0.0005 * Math.sqrt(game.levelsCleared)); //extra dense //normal is 0.001 //makes effective life much larger + spawn.shield(me, x, y); + if (Math.random() < Math.min((game.levelsCleared - 1) * 0.1, 0.7)) spawn.shield(me, x, y); + + me.onDeath = function () { + powerUps.spawnBossPowerUp(this.position.x, this.position.y) + this.removeCons(); //remove constraint + }; + me.do = function () { + this.healthBar(); + this.gravity(); + this.seePlayerCheck(); + this.attraction(); + }; + }, + shield(target, x, y, stiffness = 0.4) { + if (this.allowShields) { + mobs.spawn(x, y, 9, target.radius + 20, "rgba(220,220,255,0.6)"); + let me = mob[mob.length - 1]; + me.stroke = "rgb(220,220,255)"; + Matter.Body.setDensity(me, 0.0001) //very low density to not mess with the original mob's motion + me.shield = true; + me.collisionFilter.mask = 0x000100; //don't collide with bodies, map, player, and mobs, only bullets + consBB[consBB.length] = Constraint.create({ + //attach shield to last spawned mob + bodyA: me, + bodyB: target, + stiffness: stiffness, + damping: 0.1 + }); + me.onDamage = function () { + //make sure the mob that owns the shield can tell when damage is done + this.alertNearByMobs(); + }; + me.leaveBody = false; + me.dropPowerUp = false; + //swap order of shield and mob, so that mob is behind shield graphically + mob[mob.length - 1] = mob[mob.length - 2]; + mob[mob.length - 2] = me; + me.do = function () {}; + } + }, + bossShield(nodes, x, y, radius) { + mobs.spawn(x, y, 9, radius, "rgba(220,220,255,0.65)"); + let me = mob[mob.length - 1]; + me.stroke = "rgb(220,220,255)"; + Matter.Body.setDensity(me, 0.00005) //very low density to not mess with the original mob's motion + me.frictionAir = 0; + me.shield = true; + me.collisionFilter.mask = 0x000100; //don't collide with bodies, map, and mobs, only bullets and player + //constrain to all mob nodes in boss + for (let i = 0; i < nodes; ++i) { + consBB[consBB.length] = Constraint.create({ + bodyA: me, + bodyB: mob[mob.length - i - 2], + stiffness: 0.4, + damping: 0.1 + }); + } + me.onDamage = function () { + //make sure the mob that owns the shield can tell when damage is done + this.alertNearByMobs(); + }; + me.leaveBody = false; + me.dropPowerUp = false; + mob[mob.length - 1] = mob[mob.length - 1 - nodes]; + mob[mob.length - 1 - nodes] = me; + me.do = function () {}; + }, + //complex constrained mob templates********************************************************************** + //******************************************************************************************************* + allowShields: true, + nodeBoss( + x, + y, + spawn = "striker", + nodes = Math.min(2 + Math.ceil(Math.random() * (game.levelsCleared + 2)), 8), + //Math.ceil(Math.random() * 3) + Math.min(4,Math.ceil(game.levelsCleared/2)), + radius = Math.ceil(Math.random() * 10) + 17, // radius of each node mob + sideLength = Math.ceil(Math.random() * 100) + 70, // distance between each node mob + stiffness = Math.random() * 0.03 + 0.005 + ) { + this.allowShields = false; //don't want shields on boss mobs + const angle = 2 * Math.PI / nodes + for (let i = 0; i < nodes; ++i) { + let whoSpawn = spawn; + if (spawn === "random") { + whoSpawn = this.fullPickList[Math.floor(Math.random() * this.fullPickList.length)]; + } else if (spawn === "randomList") { + whoSpawn = this.pickList[Math.floor(Math.random() * this.pickList.length)]; + } + this[whoSpawn](x + sideLength * Math.sin(i * angle), y + sideLength * Math.cos(i * angle), radius); + } + if (Math.random() < 0.3) { + this.constrain2AdjacentMobs(nodes, stiffness * 2, true); + } else { + this.constrainAllMobCombos(nodes, stiffness); + } + //spawn shield for entire boss + if (nodes > 2 && Math.random() < 0.998) { + this.bossShield(nodes, x, y, sideLength + 2.5 * radius + nodes * 6 - 25); + // this.bossShield(nodes, x, y, sideLength / (2 * Math.sin(Math.PI / nodes))); + } + this.allowShields = true; + }, + // nodeBoss( + // x, + // y, + // spawn = "striker", + // nodes = Math.min(2 + Math.round(Math.random() * game.levelsCleared), 8), + // //Math.ceil(Math.random() * 3) + Math.min(4,Math.ceil(game.levelsCleared/2)), + // radius = Math.ceil(Math.random() * 10) + 17, // radius of each node mob + // l = Math.ceil(Math.random() * 100) + 70, // distance between each node mob + // stiffness = Math.random() * 0.03 + 0.005 + // ) { + // this.allowShields = false; //dont' want shields on boss mobs + // let px = 0; + // let py = 0; + // let a = (2 * Math.PI) / nodes; + // for (let i = 0; i < nodes; ++i) { + // px += l * Math.cos(a * i); + // py += l * Math.sin(a * i); + // let whoSpawn = spawn; + // if (spawn === "random") { + // whoSpawn = this.fullPickList[Math.floor(Math.random() * this.fullPickList.length)]; + // } else if (spawn === "randomList") { + // whoSpawn = this.pickList[Math.floor(Math.random() * this.pickList.length)]; + // } + // this[whoSpawn](x + px, y + py, radius); + // } + // if (Math.random() < 0.3) { + // this.constrain2AdjacentMobs(nodes, stiffness * 2, true); + // } else { + // this.constrainAllMobCombos(nodes, stiffness); + // } + // this.allowShields = true; + // }, + lineBoss( + x, + y, + spawn = "striker", + nodes = Math.min(3 + Math.ceil(Math.random() * game.levelsCleared + 2), 8), + //Math.ceil(Math.random() * 3) + Math.min(4,Math.ceil(game.levelsCleared/2)), + radius = Math.ceil(Math.random() * 10) + 17, + l = Math.ceil(Math.random() * 80) + 30, + stiffness = Math.random() * 0.06 + 0.01 + ) { + this.allowShields = false; //dont' want shields on boss mobs + for (let i = 0; i < nodes; ++i) { + let whoSpawn = spawn; + if (spawn === "random") { + whoSpawn = this.fullPickList[Math.floor(Math.random() * this.fullPickList.length)]; + } else if (spawn === "randomList") { + whoSpawn = this.pickList[Math.floor(Math.random() * this.pickList.length)]; + } + this[whoSpawn](x + i * radius + i * l, y, radius); + } + this.constrain2AdjacentMobs(nodes, stiffness); + this.allowShields = true; + }, + //constraints ************************************************************************************************ + //************************************************************************************************************* + constrainAllMobCombos(nodes, stiffness) { + //runs through every combination of last 'num' bodies and constrains them + for (let i = 1; i < nodes + 1; ++i) { + for (let j = i + 1; j < nodes + 1; ++j) { + consBB[consBB.length] = Constraint.create({ + bodyA: mob[mob.length - i], + bodyB: mob[mob.length - j], + stiffness: stiffness + }); + } + } + }, + constrain2AdjacentMobs(nodes, stiffness, loop = false) { + //runs through every combination of last 'num' bodies and constrains them + for (let i = 0; i < nodes - 1; ++i) { + consBB[consBB.length] = Constraint.create({ + bodyA: mob[mob.length - i - 1], + bodyB: mob[mob.length - i - 2], + stiffness: stiffness + }); + } + if (nodes > 2) { + for (let i = 0; i < nodes - 2; ++i) { + consBB[consBB.length] = Constraint.create({ + bodyA: mob[mob.length - i - 1], + bodyB: mob[mob.length - i - 3], + stiffness: stiffness + }); + } + } + //optional connect the tail to head + if (loop && nodes > 3) { + consBB[consBB.length] = Constraint.create({ + bodyA: mob[mob.length - 1], + bodyB: mob[mob.length - nodes], + stiffness: stiffness + }); + consBB[consBB.length] = Constraint.create({ + bodyA: mob[mob.length - 2], + bodyB: mob[mob.length - nodes], + stiffness: stiffness + }); + consBB[consBB.length] = Constraint.create({ + bodyA: mob[mob.length - 1], + bodyB: mob[mob.length - nodes + 1], + stiffness: stiffness + }); + } + }, + constraintPB(x, y, bodyIndex, stiffness) { + cons[cons.length] = Constraint.create({ + pointA: { + x: x, + y: y + }, + bodyB: body[bodyIndex], + stiffness: stiffness + }); + }, + constraintBB(bodyIndexA, bodyIndexB, stiffness) { + consBB[consBB.length] = Constraint.create({ + bodyA: body[bodyIndexA], + bodyB: body[bodyIndexB], + stiffness: stiffness + }); + }, + // body and map spawns ****************************************************************************** + //********************************************************************************************** + wireHead() { + //not a mob, just a graphic for level 1 + const breakingPoint = 1300 + mobs.spawn(breakingPoint, -100, 0, 7.5, "transparent"); + let me = mob[mob.length - 1]; + //touch nothing + me.collisionFilter.category = 0x010000; //act like a body + me.collisionFilter.mask = 0x000101; //only collide with map + me.inertia = Infinity; + me.g = 0.0004; //required for gravity + me.restitution = 0; + me.stroke = "transparent" + me.freeOfWires = false; + me.frictionStatic = 1; + me.friction = 1; + me.frictionAir = 0.01; + + me.do = function () { + let wireX = -50; + let wireY = -1000; + if (this.freeOfWires) { + this.gravity(); + } else { + if (mech.pos.x > breakingPoint) { + this.freeOfWires = true; + this.fill = "#000" + this.force.x += -0.003; + player.force.x += 0.06; + // player.force.y -= 0.15; + } + + //player is extra heavy from wires + Matter.Body.setVelocity(player, { + x: player.velocity.x, + y: player.velocity.y + 0.3 + }) + + //player friction from the wires + if (mech.pos.x > 700 && player.velocity.x > -2) { + let wireFriction = 0.75 * Math.min(0.6, Math.max(0, 100 / (breakingPoint - mech.pos.x))); + if (!mech.onGround) wireFriction *= 3 + Matter.Body.setVelocity(player, { + x: player.velocity.x - wireFriction, + y: player.velocity.y + }) + } + //move to player + Matter.Body.setPosition(this, { + x: mech.pos.x + (42 * Math.cos(mech.angle + Math.PI)), + y: mech.pos.y + (42 * Math.sin(mech.angle + Math.PI)) + }) + } + //draw wire + ctx.beginPath(); + ctx.moveTo(wireX, wireY); + ctx.quadraticCurveTo(wireX, 0, this.position.x, this.position.y); + if (!this.freeOfWires) ctx.lineTo(mech.pos.x + (30 * Math.cos(mech.angle + Math.PI)), mech.pos.y + (30 * Math.sin(mech.angle + Math.PI))); + ctx.lineCap = "butt"; + ctx.lineWidth = 15; + ctx.strokeStyle = "#000"; + ctx.stroke(); + ctx.lineCap = "round"; + }; + }, + wireKnee() { + //not a mob, just a graphic for level 1 + const breakingPoint = 1425 + mobs.spawn(breakingPoint, -100, 0, 2, "transparent"); + let me = mob[mob.length - 1]; + //touch nothing + me.collisionFilter.category = 0x010000; //act like a body + me.collisionFilter.mask = 0x000101; //only collide with map + me.g = 0.0003; //required for gravity + // me.restitution = 0; + me.stroke = "transparent" + // me.inertia = Infinity; + me.restitution = 0; + me.freeOfWires = false; + me.frictionStatic = 1; + me.friction = 1; + me.frictionAir = 0.01; + + me.do = function () { + let wireX = -50 - 20; + let wireY = -1000; + + if (this.freeOfWires) { + this.gravity(); + } else { + if (mech.pos.x > breakingPoint) { + this.freeOfWires = true; + this.force.x -= 0.0004; + this.fill = "#222"; + } + //move mob to player + mech.calcLeg(0, 0); + Matter.Body.setPosition(this, { + x: mech.pos.x + mech.flipLegs * mech.knee.x - 5, + y: mech.pos.y + mech.knee.y + }) + } + //draw wire + ctx.beginPath(); + ctx.moveTo(wireX, wireY); + ctx.quadraticCurveTo(wireX, 0, this.position.x, this.position.y); + ctx.lineWidth = 5; + ctx.strokeStyle = "#222"; + ctx.lineCap = "butt"; + ctx.stroke(); + ctx.lineCap = "round"; + }; + }, + wireKneeLeft() { + //not a mob, just a graphic for level 1 + const breakingPoint = 1400 + mobs.spawn(breakingPoint, -100, 0, 2, "transparent"); + let me = mob[mob.length - 1]; + //touch nothing + me.collisionFilter.category = 0x010000; //act like a body + me.collisionFilter.mask = 0x000101; //only collide with map + me.g = 0.0003; //required for gravity + // me.restitution = 0; + me.stroke = "transparent" + // me.inertia = Infinity; + me.restitution = 0; + me.freeOfWires = false; + me.frictionStatic = 1; + me.friction = 1; + me.frictionAir = 0.01; + + me.do = function () { + let wireX = -50 - 35; + let wireY = -1000; + + if (this.freeOfWires) { + this.gravity(); + } else { + if (mech.pos.x > breakingPoint) { + this.freeOfWires = true; + this.force.x += -0.0003; + this.fill = "#333"; + } + //move mob to player + mech.calcLeg(Math.PI, -3); + Matter.Body.setPosition(this, { + x: mech.pos.x + mech.flipLegs * mech.knee.x - 5, + y: mech.pos.y + mech.knee.y + }) + } + //draw wire + ctx.beginPath(); + ctx.moveTo(wireX, wireY); + ctx.quadraticCurveTo(wireX, 0, this.position.x, this.position.y); + ctx.lineWidth = 5; + ctx.lineCap = "butt"; + ctx.strokeStyle = "#333"; + ctx.stroke(); + ctx.lineCap = "round"; + }; + }, + wireFoot() { + //not a mob, just a graphic for level 1 + const breakingPoint = 1350 + mobs.spawn(breakingPoint, -100, 0, 2, "transparent"); + let me = mob[mob.length - 1]; + //touch nothing + me.collisionFilter.category = 0x010000; //act like a body + me.collisionFilter.mask = 0x000101; //only collide with map + me.g = 0.0003; //required for gravity + me.restitution = 0; + me.stroke = "transparent" + // me.inertia = Infinity; + me.freeOfWires = false; + // me.frictionStatic = 1; + // me.friction = 1; + me.frictionAir = 0.01; + + me.do = function () { + let wireX = -50 + 16; + let wireY = -1000; + + if (this.freeOfWires) { + this.gravity(); + } else { + if (mech.pos.x > breakingPoint) { + this.freeOfWires = true; + this.force.x += -0.0006; + this.fill = "#222"; + } + //move mob to player + mech.calcLeg(0, 0); + Matter.Body.setPosition(this, { + x: mech.pos.x + mech.flipLegs * mech.foot.x - 5, + y: mech.pos.y + mech.foot.y - 1 + }) + } + //draw wire + ctx.beginPath(); + ctx.moveTo(wireX, wireY); + ctx.quadraticCurveTo(wireX, 0, this.position.x, this.position.y); + ctx.lineWidth = 5; + ctx.lineCap = "butt"; + ctx.strokeStyle = "#222"; + ctx.stroke(); + ctx.lineCap = "round"; + }; + }, + wireFootLeft() { + //not a mob, just a graphic for level 1 + const breakingPoint = 1325 + mobs.spawn(breakingPoint, -100, 0, 2, "transparent"); + let me = mob[mob.length - 1]; + //touch nothing + me.collisionFilter.category = 0x010000; //act like a body + me.collisionFilter.mask = 0x000101; //only collide with map + me.g = 0.0003; //required for gravity + me.restitution = 0; + me.stroke = "transparent" + // me.inertia = Infinity; + me.freeOfWires = false; + // me.frictionStatic = 1; + // me.friction = 1; + me.frictionAir = 0.01; + + me.do = function () { + let wireX = -50 + 26; + let wireY = -1000; + + if (this.freeOfWires) { + this.gravity(); + } else { + if (mech.pos.x > breakingPoint) { + this.freeOfWires = true; + this.force.x += -0.0005; + this.fill = "#333"; + } + //move mob to player + mech.calcLeg(Math.PI, -3); + Matter.Body.setPosition(this, { + x: mech.pos.x + mech.flipLegs * mech.foot.x - 5, + y: mech.pos.y + mech.foot.y - 1 + }) + } + //draw wire + ctx.beginPath(); + ctx.moveTo(wireX, wireY); + ctx.quadraticCurveTo(wireX, 0, this.position.x, this.position.y); + ctx.lineWidth = 5; + ctx.strokeStyle = "#333"; + ctx.lineCap = "butt"; + ctx.stroke(); + ctx.lineCap = "round"; + }; + }, + boost(x, y, height = 1000) { + spawn.mapVertex(x + 50, y + 35, "120 40 -120 40 -50 -40 50 -40"); + // level.addZone(x, y, 100, 30, "fling", {Vx:Vx, Vy: Vy}); + level.addQueryRegion(x, y - 20, 100, 20, "boost", [ + [player], body, mob, powerUp, bullet + ], -1.1 * Math.sqrt(Math.abs(height))); + let color = "rgba(200,0,255,"; + level.fillBG.push({ + x: x, + y: y - 25, + width: 100, + height: 25, + color: color + "0.2)" + }); + level.fillBG.push({ + x: x, + y: y - 55, + width: 100, + height: 55, + color: color + "0.1)" + }); + level.fillBG.push({ + x: x, + y: y - 120, + width: 100, + height: 120, + color: color + "0.05)" + }); + }, + laserZone(x, y, width, height, dmg) { + level.addZone(x, y, width, height, "laser", { + dmg + }); + level.fill.push({ + x: x, + y: y, + width: width, + height: height, + color: "#f00" + }); + }, + deathQuery(x, y, width, height) { + level.addQueryRegion(x, y, width, height, "death", [ + [player], mob + ]); + level.fill.push({ + x: x, + y: y, + width: width, + height: height, + color: "#f00" + }); + }, + platform(x, y, width, height) { + const size = 20; + spawn.mapRect(x, y + height, width, 30); + level.fillBG.push({ + x: x + width / 2 - size / 2, + y: y, + width: size, + height: height, + color: "#f0f0f3" + }); + }, + debris(x, y, width, number = Math.floor(3 + Math.random() * 11)) { + for (let i = 0; i < number; ++i) { + if (Math.random() < 0.15) { + powerUps.chooseRandomPowerUp(x + Math.random() * width, y); + } else { + const size = 18 + Math.random() * 25; + spawn.bodyRect(x + Math.random() * width, y, size * (0.6 + Math.random()), size * (0.6 + Math.random()), 1); + // body[body.length] = Bodies.rectangle(x + Math.random() * width, y, size * (0.6 + Math.random()), size * (0.6 + Math.random())); + } + } + }, + bodyRect(x, y, width, height, chance = 1, properties = { + friction: 0.05, + frictionAir: 0.01 + }) { + if (Math.random() < chance) body[body.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height, properties); + }, + bodyVertex(x, y, vector, properties) { //adds shape to body array + body[body.length] = Matter.Bodies.fromVertices(x, y, Vertices.fromPath(vector), properties); + }, + mapRect(x, y, width, height, properties) { //adds rectangle to map array + map[map.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height, properties); + }, + mapVertex(x, y, vector, properties) { //adds shape to map array + map[map.length] = Matter.Bodies.fromVertices(x, y, Vertices.fromPath(vector), properties); + }, + //complex map templates + spawnBuilding(x, y, w, h, leftDoor, rightDoor, walledSide) { + this.mapRect(x, y, w, 25); //roof + this.mapRect(x, y + h, w, 35); //ground + if (walledSide === "left") { + this.mapRect(x, y, 25, h); //wall left + } else { + this.mapRect(x, y, 25, h - 150); //wall left + if (leftDoor) { + this.bodyRect(x + 5, y + h - 150, 15, 150, this.propsFriction); //door left + } + } + if (walledSide === "right") { + this.mapRect(x - 25 + w, y, 25, h); //wall right + } else { + this.mapRect(x - 25 + w, y, 25, h - 150); //wall right + if (rightDoor) { + this.bodyRect(x + w - 20, y + h - 150, 15, 150, this.propsFriction); //door right + } + } + }, + spawnStairs(x, y, num, w, h, stepRight) { + w += 50; + if (stepRight) { + for (let i = 0; i < num; i++) { + this.mapRect(x - (w / num) * (1 + i), y - h + (i * h) / num, w / num + 50, h - (i * h) / num + 50); + } + } else { + for (let i = 0; i < num; i++) { + this.mapRect(x + (i * w) / num, y - h + (i * h) / num, w / num + 50, h - (i * h) / num + 50); + } + } + }, + //pre-made property options************************************************************************************* + //************************************************************************************************************* + //Object.assign({}, propsHeavy, propsBouncy, propsNoRotation) //will combine properties into a new object + propsFriction: { + friction: 0.5, + frictionAir: 0.02, + frictionStatic: 1 + }, + propsFrictionMedium: { + friction: 0.15, + frictionStatic: 1 + }, + propsBouncy: { + friction: 0, + frictionAir: 0, + frictionStatic: 0, + restitution: 1 + }, + propsSlide: { + friction: 0.003, + frictionStatic: 0.4, + restitution: 0, + density: 0.002 + }, + propsLight: { + density: 0.001 + }, + propsOverBouncy: { + friction: 0, + frictionAir: 0, + frictionStatic: 0, + restitution: 1.05 + }, + propsHeavy: { + density: 0.01 //default density is 0.001 + }, + propsIsNotHoldable: { + isNotHoldable: true + }, + propsNoRotation: { + inertia: Infinity //prevents rotation + }, + propsHoist: { + inertia: Infinity, //prevents rotation + frictionAir: 0.001, + friction: 0.0001, + frictionStatic: 0, + restitution: 0, + isNotHoldable: true + // density: 0.0001 + }, + propsDoor: { + density: 0.001, //default density is 0.001 + friction: 0, + frictionAir: 0.03, + frictionStatic: 0, + restitution: 0 + }, + sandPaper: { + friction: 1, + frictionStatic: 1, + restitution: 0 + } +}; \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..bd4e6e5 --- /dev/null +++ b/style.css @@ -0,0 +1,255 @@ +body { + font-family: "Helvetica", "Arial", sans-serif; + margin: 0; + overflow: hidden; + background-color: #fff; + user-select: none; + /*cursor: crosshair;*/ +} + +canvas { + position: absolute; + top: 0; + left: 0; + z-index: 0; +} + +#splash { + user-select: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2; +} + +*:focus { + outline: none; +} + +table { + border-collapse: collapse; + /* border: 1px solid #eee; */ + width: 360px; + /* background-color: #ddd; */ +} + +summary { + font-size: 1.2em; +} + +#controls { + position: absolute; + bottom: 0px; + left: 1px; + z-index: 12; + font-size: 1.3em; + /* background-color: #ccc; */ + /* border-radius: 5px; */ +} + +#controls-div { + padding: 10px; + border-radius: 8px; + background-color: #ddd; +} + +#dmg { + position: absolute; + z-index: 2; + width: 100%; + height: 100%; + display: none; + background-color: #f67; + opacity: 0; + transition: opacity 1s; +} + +#health-bg { + position: absolute; + top: 15px; + left: 15px; + height: 20px; + width: 300px; + background-color: #000; + opacity: 0.1; + z-index: 1; + pointer-events: none; + display: none; +} + +#health { + position: absolute; + top: 15px; + left: 15px; + height: 20px; + width: 0px; + transition: width 1s ease-out; + opacity: 0.6; + z-index: 2; + pointer-events: none; + background-color: #f00; +} + +.low-health { + animation: blink 250ms infinite alternate; +} + +@keyframes blink { + from { + opacity: 0.9; + } + + to { + opacity: 0.2; + } +} + +#fade-out { + position: absolute; + z-index: 2; + width: 100%; + height: 100%; + background-color: #fff; + opacity: 0; + transition: opacity 5s; + pointer-events: none; +} + +#guns { + position: absolute; + top: 40px; + left: 15px; + z-index: 2; + font-size: 23px; + color: #111; + background-color: rgba(255, 255, 255, 0.4); + user-select: none; + pointer-events: none; + padding: 0px 5px 0px 5px; + border-radius: 5px; + /*border: 2px solid rgba(0, 0, 0, 0.4);*/ +} + +#field { + position: absolute; + top: 15px; + right: 15px; + z-index: 2; + font-size: 23px; + color: #000; + text-align: right; + opacity: 0.7; + line-height: 140%; + background-color: rgba(190, 210, 245, 0.25); + user-select: none; + pointer-events: none; + padding: 0px 5px 0px 5px; + border-radius: 5px; + /*border: 2px solid rgba(0, 0, 0, 0.4);*/ +} + +#mods { + position: absolute; + top: 60px; + right: 15px; + z-index: 2; + font-size: 20px; + color: #000; + text-align: right; + opacity: 0.35; + line-height: 140%; + background-color: rgba(255, 255, 255, 0.4); + user-select: none; + pointer-events: none; + padding: 0px 5px 0px 5px; + border-radius: 5px; + /*border: 2px solid rgba(0, 0, 0, 0.4);*/ +} + +#text-log { + position: absolute; + top: 20px; + left: 0; + width: 100%; + line-height: 150%; + text-align: center; + z-index: 2; + font-size: 1.25em; + color: #000; + opacity: 0; + transition: opacity 0.5s; + pointer-events: none; + user-select: none; +} + +.box { + padding: 3px 8px 3px 8px; + border: 2px solid #444; + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.5); +} + +.faded { + opacity: 0.5; + font-size: 85%; +} + +.wrapper { + display: grid; + grid-template-columns: 360px 10px; + align-self: center; + justify-content: center; +} + +.grid-box { + align-self: center; + justify-self: center; +} + +.mouse { + color: #ccc; + position: relative; + padding: 37px 30px 37px 30px; + border-radius: 25px; + border: 2px solid #444; + background-color: rgba(255, 255, 255, 0.5); +} + +.mouse:after { + content: ""; + position: absolute; + z-index: 1; + top: 4px; + left: 26px; + border-radius: 25px; + /* background: #444; */ + border: 2px solid #444; + + width: 4px; + height: 20px; + border-radius: 10px / 25px; +} + +.mouse-line { + position: relative; + top: 30px; + left: 0px; +} + +.mouse-line:after { + content: ""; + position: absolute; + z-index: 1; + top: -35px; + left: -30px; + width: 60px; + height: 2px; + border-radius: 8px; + background: #444; +} + +.right { + text-align: right; +} \ No newline at end of file