let bullet = [];
const b = {
dmgScale: null, //scales all gun damage from momentum, but not raw .dmg //set in levels.setDifficulty
gravity: 0.0006, //most other bodies have gravity = 0.001
//variables use for gun mod upgrades
modCount: null,
modFireRate: null,
modExplosionRadius: null,
modBulletSize: null,
modEnergySiphon: null,
modHealthDrain: null,
modNoAmmo: null,
isModBulletsLastLonger: null,
isModImmortal: null,
modSpores: null,
isModImmuneExplosion: null,
isModDroneOnDamage: null,
modAcidDmg: null,
isModAcidDmg: null,
annihilation: null,
modRecursiveHealing: null,
modSquirrelFx: null,
isModCrit: null,
isModBayesian: null,
isModLowHealthDmg: null,
isModFarAwayDmg: null,
isModEntanglement: null,
isModMassEnergy: null,
isModFourOptions: null,
modLaserBotCount: null,
modNailBotCount: null,
modCollisionImmuneCycles: null,
modBlockDmg: null,
isModPiezo: null,
isModDroneCollide: null,
isModFastSpores: null,
isModStomp: null,
modSuperBallNumber: null,
modLaserReflections: null,
modLaserDamage: null,
modLaserFieldDrain: null,
isModNoAmmo: null,
isModAmmoFromHealth: null,
modMobDieAtHealth: null,
isModEnergyRecovery: null,
isModHealthRecovery: null,
isModEnergyLoss: null,
modOnHealthChange() { //used with acid mod
if (b.isModAcidDmg && mech.health > 0.8) {
game.playerDmgColor = "rgba(0,80,80,0.9)"
b.modAcidDmg = 0.9
} else {
game.playerDmgColor = "rgba(0,0,0,0.7)"
b.modAcidDmg = 0
}
},
mods: [{
name: "depleted uranium rounds",
description: `your bullets are +13% larger
increased mass and physical damage`,
count: 0,
maxCount: 9,
allowed() {
return true
},
effect() {
b.modBulletSize += 0.13
},
remove() {
b.modBulletSize = 1;
}
},
{
name: "fluoroantimonic acid",
description: "each bullet does extra chemical damage
only active when you are above 80% health",
maxCount: 1,
count: 0,
allowed() {
return mech.health > 0.8 || level.isBuildRun
},
effect() {
b.isModAcidDmg = true;
b.modOnHealthChange();
},
remove() {
b.modAcidDmg = 0;
b.isModAcidDmg = false;
game.playerDmgColor = "rgba(0,0,0,0.7)"
}
},
{
name: "fracture analysis",
description: "5x physical damage to unaware enemies
unaware enemies don't have a health bar",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect() {
b.isModCrit = true;
},
remove() {
b.isModCrit = false;
}
},
{
name: "kinetic bombardment",
description: "do up to 33% more damage at a distance
increase maxes out at about 40 steps away",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect() {
b.isModFarAwayDmg = true; //used in mob.damage()
},
remove() {
b.isModFarAwayDmg = false;
}
},
{
name: "quasistatic equilibrium",
description: "do extra damage at low health
up to 50% increase when near death",
maxCount: 1,
count: 0,
allowed() {
return mech.health < 0.75 || level.isBuildRun
},
effect() {
b.isModLowHealthDmg = true; //used in mob.damage()
},
remove() {
b.isModLowHealthDmg = false;
}
},
{
name: "high explosives",
description: "explosions do +20% more damage
explosive area is +44% larger",
maxCount: 3,
count: 0,
allowed() {
return b.haveGunCheck("missiles") || b.haveGunCheck("flak") || b.haveGunCheck("grenades") || b.haveGunCheck("vacuum bomb") || b.haveGunCheck("pulse");
},
effect: () => {
b.modExplosionRadius += 0.2;
},
remove() {
b.modExplosionRadius = 1;
}
},
{
name: "electric reactive armour",
description: "explosions do no harm
explosions drain energy",
maxCount: 1,
count: 0,
allowed() {
return (b.modExplosionRadius > 1)
},
effect: () => {
b.isModImmuneExplosion = true;
},
remove() {
b.isModImmuneExplosion = false;
}
},
{
name: "auto-loading heuristics",
description: "your delay after firing is +14% shorter",
maxCount: 9,
count: 0,
allowed() {
return true
},
effect() {
b.modFireRate *= 0.86
},
remove() {
b.modFireRate = 1;
}
},
{
name: "desublimated ammunition",
description: "use 50% less ammo when crouching",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect() {
b.modNoAmmo = 1
},
remove() {
b.modNoAmmo = 0;
}
},
{
name: "Lorentzian topology",
description: "your bullets last +33% longer",
maxCount: 3,
count: 0,
allowed() {
return mech.fieldUpgrades[mech.fieldMode].name === "nano-scale manufacturing" || b.haveGunCheck("spores") || b.haveGunCheck("drones") || b.haveGunCheck("super balls") || b.haveGunCheck("foam")
},
effect() {
b.isModBulletsLastLonger += 0.33
},
remove() {
b.isModBulletsLastLonger = 1;
}
},
{
name: "reaction inhibitor",
description: "mobs die if their life goes below 12%",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect: () => {
b.modMobDieAtHealth = 0.15
},
remove() {
b.modMobDieAtHealth = 0.05;
}
},
{
name: "zoospore vector",
description: "enemies discharge spores on death
+11% chance",
maxCount: 9,
count: 0,
allowed() {
return true
},
effect() {
b.modSpores += 0.11;
for (let i = 0; i < 10; i++) {
b.spore(player)
}
},
remove() {
b.modSpores = 0;
}
},
{
name: "laser-bot",
description: "a bot defends the space around you
uses a short range laser that drains energy",
maxCount: 9,
count: 0,
allowed() {
return true
},
effect() {
b.modLaserBotCount++;
b.laserBot();
},
remove() {
b.modLaserBotCount = 0;
}
},
{
name: "nail-bot",
description: "a bot fires nails at targets in line of sight",
maxCount: 9,
count: 0,
allowed() {
return true
},
effect() {
b.modNailBotCount++;
b.nailBot();
},
remove() {
b.modNailBotCount = 0;
}
},
{
name: "ablative synthesis",
description: "rebuild your broken parts as drones
chance to occur after being harmed",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect() {
b.isModDroneOnDamage = true;
for (let i = 0; i < 4; i++) {
b.drone() //spawn drone
}
},
remove() {
b.isModDroneOnDamage = false;
}
},
{
name: "bremsstrahlung radiation",
description: "when your field blocks it also does damage",
maxCount: 9,
count: 0,
allowed() {
return mech.fieldUpgrades[mech.fieldMode].name !== "time dilation field" && mech.fieldUpgrades[mech.fieldMode].name !== "phase decoherence field"
},
effect() {
b.modBlockDmg += 0.7 //if you change this value also update the for loop in the electricity graphics in mech.pushMass
},
remove() {
b.modBlockDmg = 0;
}
},
{
name: "field superposition",
description: "increase your field radius by 40%",
maxCount: 1,
count: 0,
allowed() {
return mech.fieldUpgrades[mech.fieldMode].name !== "time dilation field" && mech.fieldUpgrades[mech.fieldMode].name !== "phase decoherence field"
},
effect() {
mech.fieldRange = 175 * 1.4
},
remove() {
mech.fieldRange = 175;
}
},
{
name: "entanglement",
description: "only when your first gun is equipped
reduce harm by 10% for each gun you have",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect() {
b.isModEntanglement = true
},
remove() {
b.isModEntanglement = false;
}
},
{
name: "waste energy recovery",
description: "regen 7% of max energy every second
active for 5 seconds after a mob dies",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect() {
b.isModEnergyRecovery = true;
},
remove() {
b.isModEnergyRecovery = false;
}
},
{
name: "scrap recycling",
description: "heal up to 1% of max health every second
active for 5 seconds after a mob dies",
maxCount: 1,
count: 0,
allowed() {
return b.isModEnergyRecovery
},
effect() {
b.isModHealthRecovery = true;
},
remove() {
b.isModHealthRecovery = false;
}
},
{
name: "acute stress response",
description: "increase damage by 50%
no energy for 5 seconds after a mob dies",
maxCount: 1,
count: 0,
allowed() {
return b.isModEnergyRecovery
},
effect() {
b.isModEnergyLoss = true;
},
remove() {
b.isModEnergyLoss = false;
}
},
{
name: "squirrel-cage rotor",
description: "jump higher and move faster
reduced harm from falling ",
maxCount: 9,
count: 0,
allowed() {
return true
},
effect() { // good with melee builds, content skipping builds
b.modSquirrelFx += 0.2;
mech.Fx = 0.016 * b.modSquirrelFx;
mech.jumpForce += 0.038;
},
remove() {
b.modSquirrelFx = 1;
mech.Fx = 0.016; //if this changes update the values in definePlayerMass
mech.jumpForce = 0.42; //was 0.38 at 0.0019 gravity
}
},
{
name: "basidio-stomp",
description: "hard landings disrupt spores from the ground
immune to harm from falling",
maxCount: 1,
count: 0,
allowed() {
return b.modSquirrelFx > 1
},
effect() {
b.isModStomp = true
},
remove() {
b.isModStomp = false;
}
},
{
name: "Pauli exclusion",
description: `unable to collide with enemies for +2 seconds
activates after being harmed from a collision`,
maxCount: 9,
count: 0,
allowed() {
return true
},
effect() {
b.modCollisionImmuneCycles += 120;
mech.collisionImmune = mech.cycle + b.modCollisionImmuneCycles; //player is immune to collision damage for 30 cycles
},
remove() {
b.modCollisionImmuneCycles = 30;
}
},
{
name: "annihilation",
description: "after touching enemies, they are annihilated",
maxCount: 1,
count: 0,
allowed() {
return b.modCollisionImmuneCycles > 120 || b.isModPiezo
},
effect() {
b.isModAnnihilation = true
},
remove() {
b.isModAnnihilation = false;
}
},
{
name: "piezoelectricity",
description: "colliding with enemies charges your energy",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect() {
b.isModPiezo = true;
mech.fieldMeter = mech.fieldEnergyMax;
},
remove() {
b.isModPiezo = false;
}
},
{
name: "energy conservation",
description: "gain energy proportional to damage done",
maxCount: 9,
count: 0,
allowed() {
return true
},
effect() {
b.modEnergySiphon += 0.15;
mech.fieldMeter = mech.fieldEnergyMax
},
remove() {
b.modEnergySiphon = 0;
}
},
{
name: "entropy exchange",
description: "heal proportional to damage done",
maxCount: 9,
count: 0,
allowed() {
return true
},
effect() {
b.modHealthDrain += 0.015;
},
remove() {
b.modHealthDrain = 0;
}
},
{
name: "overcharge",
description: "increase your maximum energy by +50%",
maxCount: 9,
count: 0,
allowed() {
return true
},
effect() {
mech.fieldEnergyMax += 0.5
mech.fieldMeter += 0.5
},
remove() {
mech.fieldEnergyMax = 1;
}
},
{
name: "supersaturation",
description: "heal +50% beyond your max health",
maxCount: 9,
count: 0,
allowed() {
return true
},
effect() {
mech.maxHealth += 0.50
mech.addHealth(0.50)
},
remove() {
mech.maxHealth = 1;
}
},
{
name: "recursive healing",
description: "healing power ups trigger one extra time.",
maxCount: 9,
count: 0,
allowed() {
return mech.health < 0.7 || level.isBuildRun
},
effect() {
b.modRecursiveHealing += 1
},
remove() {
b.modRecursiveHealing = 1;
}
},
{
name: "mass-energy equivalence",
description: "power ups overcharge your energy
temporarily gain 150% of maximum",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect: () => {
b.isModMassEnergy = true // used in mech.grabPowerUp
mech.fieldMeter = mech.fieldEnergyMax * 2
},
remove() {
b.isModMassEnergy = false;
}
},
{
name: "quantum immortality",
description: "after dying, continue in an alternate reality
guns, ammo, field, and mods are randomized",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect() {
b.isModImmortal = true;
},
remove() {
b.isModImmortal = false;
}
},
{
name: "Bayesian inference",
description: "20% chance for double power ups to drop
one fewer choice when selecting power ups",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect: () => {
b.isModBayesian = 0.20;
},
remove() {
b.isModBayesian = 0;
}
},
{
name: "catabolism",
description: "when you fire while out of ammo
convert 3% of current health into ammo",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect: () => {
b.isModAmmoFromHealth = 0.03;
},
remove() {
b.isModAmmoFromHealth = 0;
}
},
{
name: "leveraged investment",
description: "remove all future ammo power ups
spawn 6 mods and 3 healing power ups",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect: () => {
b.isModNoAmmo = true;
for (let i = 0; i < 6; i++) { //if you change the six also change it in Born rule
powerUps.spawn(mech.pos.x, mech.pos.y, "mod");
if (Math.random() < b.isModBayesian) powerUps.spawn(mech.pos.x, mech.pos.y, "mod");
}
for (let i = 0; i < 3; i++) { // spawn new mods
powerUps.spawn(mech.pos.x, mech.pos.y, "heal");
if (Math.random() < b.isModBayesian) powerUps.spawn(mech.pos.x, mech.pos.y, "heal");
}
},
remove() {
b.isModNoAmmo = false;
}
},
{
name: "+1 cardinality",
description: "one extra choice when selecting power ups",
maxCount: 1,
count: 0,
allowed() {
return true
},
effect: () => {
b.isModFourOptions = true;
},
remove() {
b.isModFourOptions = false;
}
},
{
name: "Born rule",
description: "remove all current mods
spawn new mods to replace them",
maxCount: 1,
count: 0,
allowed() {
return (b.modCount > 6) && !level.isBuildRun
},
effect: () => {
let count = b.modCount
if (b.isModNoAmmo) count - 6 //remove the 6 bonus mods when getting rid of leveraged investment
for (let i = 0; i < count; i++) { // spawn new mods
powerUps.spawn(mech.pos.x, mech.pos.y, "mod");
}
b.setupAllMods(); // remove all mods
//have state is checked in mech.death()
},
remove() {
//nothing to undo
}
},
{
name: "redundant systems",
description: "drone collisions no longer reduce their lifespan",
maxCount: 1,
count: 0,
allowed() {
return b.haveGunCheck("drones") || mech.fieldUpgrades[mech.fieldMode].name === "nano-scale manufacturing"
},
effect() {
b.isModDroneCollide = true
},
remove() {
b.isModDroneCollide = true;
}
},
{
name: "tinsellated flagella",
description: "your spores accelerate 33% faster",
maxCount: 1,
count: 0,
allowed() {
return b.haveGunCheck("spores") || b.modSpores > 0 || b.isModStomp
},
effect() {
b.isModFastSpores = true
},
remove() {
b.isModFastSpores = false
}
},
{
name: "super duper",
description: "you fire +1 additional super ball",
maxCount: 9,
count: 0,
allowed() {
return b.haveGunCheck("super balls")
},
effect() {
b.modSuperBallNumber++
},
remove() {
b.modSuperBallNumber = 4;
}
},
{
name: "specular reflection",
description: "your laser gains +1 reflection
+30% laser damage and energy drain",
maxCount: 9,
count: 0,
allowed() {
return b.haveGunCheck("laser")
},
effect() {
b.modLaserReflections++;
b.modLaserDamage += 0.015; //base is 0.05
b.modLaserFieldDrain += 0.0006 //base is 0.002
},
remove() {
b.modLaserReflections = 2;
b.modLaserDamage = 0.05;
b.modLaserFieldDrain = 0.002;
}
},
{
name: "optimized shell packing",
description: "flak ammo drops contain 2x more shells",
maxCount: 3,
count: 0,
allowed() {
return b.haveGunCheck("flak")
},
effect() {
for (i = 0, len = b.guns.length; i < len; i++) { //find which gun is flak
if (b.guns[i].name === "flak") b.guns[i].ammoPack = b.guns[i].defaultAmmoPack * (2 + this.count);
}
},
remove() {
for (i = 0, len = b.guns.length; i < len; i++) { //find which gun is flak
if (b.guns[i].name === "flak") b.guns[i].ammoPack = b.guns[i].defaultAmmoPack;
}
}
},
// {
// name: "super mines",
// description: "mines fire super balls when triggered",
// maxCount: 1,
// count: 0,
// allowed() {
// return b.haveGunCheck("mines")
// },
// effect() {
// }
// },
],
removeMod(index) {
b.mods[index].remove();
b.mods[index].count = 0;
game.updateModHUD();
},
setupAllMods() {
for (let i = 0, len = b.mods.length; i < len; i++) {
b.mods[i].remove();
b.mods[i].count = 0
}
b.modCount = 0;
game.updateModHUD();
},
// setupAllMods() {
// for (let i = 0, len = b.mods.length; i < len; i++) {
// if (b.mods[i].count) b.mods[i].remove();
// b.mods[i].count = 0
// }
// b.modCount = 0;
// game.updateModHUD();
// },
giveMod(index = 'random') {
if (index === 'random') {
let options = [];
for (let i = 0; i < b.mods.length; i++) {
if (b.mods[i].count < b.mods[i].maxCount && b.mods[i].allowed())
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)
}
} else {
if (isNaN(index)) { //find index by name
for (let i = 0; i < b.mods.length; i++) {
if (index === b.mods[i].name) index = i
}
}
b.mods[index].effect(); //give specific mod
b.mods[index].count++
b.modCount++ //used in power up randomization
game.updateModHUD();
}
},
haveGunCheck(name) {
for (i = 0, len = b.inventory.length; i < len; i++) {
if (b.guns[b.inventory[i]].name === name) return true
}
return false
},
activeGun: null, //current gun in use by player
inventoryGun: 0,
inventory: [], //list of what guns player has // 0 starts with basic gun
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 {
if (b.isModAmmoFromHealth && mech.health > b.isModAmmoFromHealth) {
mech.damage(b.isModAmmoFromHealth * mech.health);
powerUps.spawn(mech.pos.x, mech.pos.y, "ammo");
if (Math.random() < b.isModBayesian) powerUps.spawn(mech.pos.x, mech.pos.y, "ammo");
}
mech.fireCDcycle = mech.cycle + 30; //fire cooldown
// game.makeTextLog("
Q, E, and mouse wheel change weapons
", 200); } if (mech.holdingTarget) { mech.drop(); } } }, bulletActions() { //run in main loop //remove bullet if at end cycle for that bullet let i = bullet.length; while (i--) { 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 } } } //draw ctx.beginPath(); for (let i = 0, len = bullet.length; i < len; i++) { 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); } ctx.fillStyle = "#000"; ctx.fill(); //do bullet 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: cat.bullet, mask: cat.map | cat.body | cat.mob | cat.mobBullet | cat.mobShield }, 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: cat.bullet, mask: cat.map | cat.body | cat.mob | cat.mobBullet | cat.mobShield }, 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(); }, 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; } } }, onCollision(event) { const pairs = event.pairs; for (let i = 0, j = pairs.length; i != j; i++) { //map + bullet collisions if (pairs[i].bodyA.collisionFilter.category === cat.map && pairs[i].bodyB.collisionFilter.category === cat.bullet) { collideBulletStatic(pairs[i].bodyB) } else if (pairs[i].bodyB.collisionFilter.category === cat.map && pairs[i].bodyA.collisionFilter.category === cat.bullet) { collideBulletStatic(pairs[i].bodyA) } function collideBulletStatic(obj) { if (obj.onWallHit) obj.onWallHit(); } } }, explosion(where, radius) { radius *= b.modExplosionRadius // typically explode is used for some bullets with .onEnd //add dmg to draw queue game.drawList.push({ x: where.x, y: where.y, radius: radius, color: "rgba(255,25,0,0.6)", time: game.drawTime }); let dist, sub, knock; let dmg = b.dmgScale * radius * 0.009; const alertRange = 100 + radius * 2; //alert range //add alert to draw queue game.drawList.push({ x: where.x, y: where.y, radius: alertRange, color: "rgba(100,20,0,0.03)", time: game.drawTime }); //player damage and knock back sub = Vector.sub(where, player.position); dist = Vector.magnitude(sub); if (dist < radius) { if (!b.isModImmuneExplosion && mech.fieldMeter > 0.1) { mech.damage(radius * 0.0002); } else { mech.fieldMeter -= Math.max(radius * 0.0006, 0.1) } knock = Vector.mult(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 = Vector.mult(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 = Vector.sub(where, body[i].position); dist = Vector.magnitude(sub); if (dist < radius) { knock = Vector.mult(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 = Vector.mult(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 = Vector.sub(where, powerUp[i].position); dist = Vector.magnitude(sub); if (dist < radius) { knock = Vector.mult(Vector.normalise(sub), (-Math.sqrt(dmg) * powerUp[i].mass) / 30); powerUp[i].force.x += knock.x; powerUp[i].force.y += knock.y; } else if (dist < alertRange) { knock = Vector.mult(Vector.normalise(sub), (-Math.sqrt(dmg) * powerUp[i].mass) / 45); powerUp[i].force.x += knock.x; powerUp[i].force.y += knock.y; } } //mob damage and knock back with alert let damageScale = 1.5; // 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 && !mob[i].isShielded) { sub = Vector.sub(where, mob[i].position); dist = Vector.magnitude(sub) - mob[i].radius; if (dist < radius) { if (mob[i].shield) dmg *= 3 //balancing explosion dmg to shields mob[i].damage(dmg * damageScale); mob[i].locatePlayer(); knock = Vector.mult(Vector.normalise(sub), (-Math.sqrt(dmg * damageScale) * mob[i].mass) / 50); mob[i].force.x += knock.x; mob[i].force.y += knock.y; radius *= 0.93 //reduced range for each additional explosion target damageScale *= 0.8 //reduced damage for each additional explosion target } else if (!mob[i].seePlayer.recall && dist < alertRange) { mob[i].locatePlayer(); knock = Vector.mult(Vector.normalise(sub), (-Math.sqrt(dmg * damageScale) * mob[i].mass) / 80); mob[i].force.x += knock.x; mob[i].force.y += knock.y; } } } }, mine(where, velocity, angle = 0) { const bIndex = bullet.length; bullet[bIndex] = Bodies.rectangle(where.x, where.y, 45 * b.modBulletSize, 16 * b.modBulletSize, { angle: angle, friction: 1, frictionStatic: 1, frictionAir: 0, restitution: 0, dmg: 0, //damage done in addition to the damage from momentum classType: "bullet", collisionFilter: { category: cat.bullet, mask: cat.map | cat.body | cat.mob | cat.mobBullet | cat.mobShield | cat.bullet }, minDmgSpeed: 5, stillCount: 0, isArmed: false, endCycle: game.cycle + 2000 + 360 * Math.random(), lookFrequency: 41 + Math.floor(23 * Math.random()), range: 700, onDmg() {}, do() { this.force.y += this.mass * 0.002; //extra gravity let collide = Matter.Query.collides(this, map) //check if collides with map if (collide.length > 0) { for (let i = 0; i < collide.length; i++) { if (collide[i].bodyA.collisionFilter.category === cat.map || collide[i].bodyB.collisionFilter.category === cat.map) { const angle = Matter.Vector.angle(collide[i].normal, { x: 1, y: 0 }) Matter.Body.setAngle(this, Math.atan2(collide[i].tangent.y, collide[i].tangent.x)) //move until touching map again after rotation for (let j = 0; j < 10; j++) { if (Matter.Query.collides(this, map).length > 0) { if (angle > -0.2 || angle < -1.5) { //don't stick to level ground Matter.Body.setStatic(this, true) //don't set to static if not touching map } else { Matter.Body.setVelocity(this, { x: 0, y: 0 }); Matter.Body.setAngularVelocity(this, 0) } this.arm(); //sometimes the mine can't attach to map and it just needs to be reset const that = this setTimeout(function () { if (Matter.Query.collides(that, map).length === 0) { that.endCycle = 0 // if not touching map explode that.isArmed = false b.mine(that.position, that.velocity, that.angle) } }, 100, that); break } //move until you are touching the wall Matter.Body.setPosition(this, Vector.add(this.position, Vector.mult(collide[i].normal, 2))) } } } } else { if (this.speed < 1 && this.angularSpeed < 0.01 && !mech.isBodiesAsleep) { this.stillCount++ } } if (this.stillCount > 25) this.arm(); }, arm() { this.isArmed = true game.drawList.push({ //add dmg to draw queue x: this.position.x, y: this.position.y, radius: 10, color: "#f00", time: 4 }); this.do = function () { //overwrite the do method for this bullet this.force.y += this.mass * 0.002; //extra gravity if (!(game.cycle % this.lookFrequency)) { //find mob targets for (let i = 0, len = mob.length; i < len; ++i) { if (Vector.magnitudeSquared(Vector.sub(this.position, mob[i].position)) < 500000 && 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) { this.endCycle = 0 //end life if mob is near and visible } } } } }, onEnd() { if (this.isArmed) { const targets = [] //target nearby mobs for (let i = 0, len = mob.length; i < len; i++) { if (mob[i].dropPowerUp) { const dist = Vector.magnitudeSquared(Vector.sub(this.position, mob[i].position)); if (dist < 1440000 && //1200*1200 Matter.Query.ray(map, this.position, mob[i].position).length === 0 && Matter.Query.ray(body, this.position, mob[i].position).length === 0) { targets.push(Vector.add(mob[i].position, Vector.mult(mob[i].velocity, Math.sqrt(dist) / 60))) //predict where the mob will be in a few cycles } } } for (let i = 0; i < 16; i++) { const speed = 53 + 10 * Math.random() if (targets.length > 0) { // aim near a random target in array const index = Math.floor(Math.random() * targets.length) const SPREAD = 150 / targets.length const WHERE = { x: targets[index].x + SPREAD * (Math.random() - 0.5), y: targets[index].y + SPREAD * (Math.random() - 0.5) } b.nail(this.position, Vector.mult(Vector.normalise(Vector.sub(WHERE, this.position)), speed), 1.1) } else { // aim in random direction const ANGLE = 2 * Math.PI * Math.random() b.nail(this.position, { x: speed * Math.cos(ANGLE), y: speed * Math.sin(ANGLE) }) } } } } }); bullet[bIndex].torque += bullet[bIndex].inertia * 0.0002 * (0.5 - Math.random()) Matter.Body.setVelocity(bullet[bIndex], velocity); World.add(engine.world, bullet[bIndex]); //add bullet to world }, spore(who) { //used with the mod upgrade in mob.death() const bIndex = bullet.length; const side = 4 * b.modBulletSize; bullet[bIndex] = Bodies.polygon(who.position.x, who.position.y, 5, side, { // density: 0.0015, //frictionAir: 0.01, inertia: Infinity, restitution: 0.5, angle: Math.random() * 2 * Math.PI, friction: 0, frictionAir: 0.025, thrust: b.isModFastSpores ? 0.0008 : 0.0004, dmg: 2.2, //damage done in addition to the damage from momentum classType: "bullet", collisionFilter: { category: cat.bullet, mask: cat.map | cat.mob | cat.mobBullet | cat.mobShield //no collide with body }, endCycle: game.cycle + Math.floor((660 + Math.floor(Math.random() * 240)) * b.isModBulletsLastLonger), minDmgSpeed: 0, onDmg() { this.endCycle = 0; //bullet ends cycle after doing damage }, onEnd() {}, lookFrequency: 97 + Math.floor(77 * 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 (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 targetVector = Vector.sub(this.position, mob[i].position) const dist = Vector.magnitude(targetVector); if (dist < closeDist) { this.closestTarget = mob[i].position; closeDist = dist; this.lockedOn = mob[i] //Vector.normalise(targetVector); if (0.3 > Math.random()) break //doesn't always target the closest mob } } } } //accelerate towards mobs if (this.lockedOn && this.lockedOn.alive) { this.force = Vector.mult(Vector.normalise(Vector.sub(this.position, this.lockedOn.position)), -this.mass * this.thrust) // this.force.x -= THRUST * this.lockedOn.x // this.force.y -= THRUST * this.lockedOn.y } else { this.force.y += this.mass * 0.0001; //gravity } }, }); const SPEED = 4 + 8 * Math.random(); 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 }, drone(speed = 1) { const me = bullet.length; const THRUST = 0.0015 const dir = mech.angle + 0.2 * (Math.random() - 0.5); const RADIUS = (4.5 + 3 * Math.random()) * b.modBulletSize bullet[me] = Bodies.polygon(mech.pos.x + 30 * Math.cos(mech.angle), mech.pos.y + 30 * Math.sin(mech.angle), 8, RADIUS, { angle: dir, inertia: Infinity, friction: 0.05, frictionAir: 0.0005, restitution: 1, dmg: 0.13, //damage done in addition to the damage from momentum lookFrequency: 83 + Math.floor(41 * Math.random()), endCycle: game.cycle + Math.floor((1200 + 420 * Math.random()) * b.isModBulletsLastLonger), classType: "bullet", collisionFilter: { category: cat.bullet, mask: cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet | cat.mobShield //self collide }, minDmgSpeed: 0, lockedOn: null, isFollowMouse: true, onDmg() { this.lockedOn = null if (this.endCycle > game.cycle + 180 && b.isModDroneCollide) { this.endCycle -= 60 if (game.cycle + 180 > this.endCycle) this.endCycle = game.cycle + 180 } }, onEnd() {}, do() { if (game.cycle + 180 > this.endCycle) { //fall and die this.force.y += this.mass * 0.0012; this.restitution = 0.2; } else { 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 ( 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 TARGET_VECTOR = Vector.sub(this.position, mob[i].position) const DIST = 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 = Vector.sub(this.position, powerUp[i].position) const DIST = 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 = Vector.mult(Vector.normalise(Vector.sub(this.position, this.lockedOn.position)), -this.mass * THRUST) } else { //accelerate towards mouse this.force = Vector.mult(Vector.normalise(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 }); } } } }) World.add(engine.world, bullet[me]); //add bullet to world Matter.Body.setVelocity(bullet[me], { x: speed * Math.cos(dir), y: speed * Math.sin(dir) }); }, nail(pos, velocity, dmg = 0) { const me = bullet.length; bullet[me] = Bodies.rectangle(pos.x, pos.y, 25 * b.modBulletSize, 2 * b.modBulletSize, b.fireAttributes(Math.atan2(velocity.y, velocity.x))); Matter.Body.setVelocity(bullet[me], velocity); World.add(engine.world, bullet[me]); //add bullet to world bullet[me].endCycle = game.cycle + 60 + 18 * Math.random(); bullet[me].dmg = dmg bullet[me].do = function () {}; }, nailBot(speed = 1) { const me = bullet.length; const dir = mech.angle; const RADIUS = (10 + 5 * Math.random()) * b.modBulletSize bullet[me] = Bodies.polygon(mech.pos.x + 30 * Math.cos(mech.angle), mech.pos.y + 30 * Math.sin(mech.angle), 4, RADIUS, { angle: dir, friction: 0, frictionStatic: 0, frictionAir: 0.05, restitution: 0.6 * (1 + 0.5 * Math.random()), dmg: 0, // 0.14 //damage done in addition to the damage from momentum minDmgSpeed: 2, lookFrequency: 56 + Math.floor(17 * Math.random()), acceleration: 0.005 * (1 + 0.5 * Math.random()), range: 70 * (1 + 0.3 * Math.random()), endCycle: Infinity, classType: "bullet", collisionFilter: { category: cat.bullet, mask: cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet | cat.mobShield }, lockedOn: null, onDmg() { this.lockedOn = null }, onEnd() {}, do() { if (!(game.cycle % this.lookFrequency) && !mech.isStealth) { let target for (let i = 0, len = mob.length; i < len; i++) { const dist = Vector.magnitudeSquared(Vector.sub(this.position, mob[i].position)); if (dist < 3000000 && //1400*1400 Matter.Query.ray(map, this.position, mob[i].position).length === 0 && Matter.Query.ray(body, this.position, mob[i].position).length === 0) { target = Vector.add(mob[i].position, Vector.mult(mob[i].velocity, Math.sqrt(dist) / 60)) const SPEED = 50 b.nail(this.position, Vector.mult(Vector.normalise(Vector.sub(target, this.position)), SPEED), 0.4) break; } } } const distanceToPlayer = Vector.magnitude(Vector.sub(this.position, mech.pos)) if (distanceToPlayer > this.range) { //if far away move towards player this.force = Vector.mult(Vector.normalise(Vector.sub(mech.pos, this.position)), this.mass * this.acceleration) // this.frictionAir = 0.1 } else { //close to player // this.frictionAir = 0 //add player's velocity Matter.Body.setVelocity(this, Vector.add(Vector.mult(this.velocity, 0.90), Vector.mult(player.velocity, 0.17))); } } }) World.add(engine.world, bullet[me]); //add bullet to world Matter.Body.setVelocity(bullet[me], { x: speed * Math.cos(dir), y: speed * Math.sin(dir) }); }, laserBot(speed = 1) { const me = bullet.length; const dir = mech.angle; const RADIUS = (14 + 6 * Math.random()) * b.modBulletSize bullet[me] = Bodies.polygon(mech.pos.x + 30 * Math.cos(mech.angle), mech.pos.y + 30 * Math.sin(mech.angle), 3, RADIUS, { angle: dir, friction: 0, frictionStatic: 0, frictionAir: 0.008 * (1 + 0.3 * Math.random()), restitution: 0.5 * (1 + 0.5 * Math.random()), dmg: 0, // 0.14 //damage done in addition to the damage from momentum minDmgSpeed: 2, lookFrequency: 27 + Math.floor(17 * Math.random()), acceleration: 0.0015 * (1 + 0.3 * Math.random()), range: 600 * (1 + 0.2 * Math.random()), followRange: 150 + Math.floor(30 * Math.random()), offPlayer: { x: 0, y: 0, }, endCycle: Infinity, classType: "bullet", collisionFilter: { category: cat.bullet, mask: cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet | cat.mobShield }, lockedOn: null, onDmg() { this.lockedOn = null }, onEnd() {}, do() { //move in a circle // const radius = 1.5 // this.offPlayer.x -= radius * Math.cos(game.cycle * 0.02) // this.offPlayer.y -= radius * Math.sin(game.cycle * 0.02) const playerPos = Vector.add(Vector.add(this.offPlayer, mech.pos), Vector.mult(player.velocity, 20)) //also include an offset unique to this bot to keep many bots spread out const farAway = Math.max(0, (Vector.magnitude(Vector.sub(this.position, playerPos))) / this.followRange) //linear bounding well const mag = Math.min(farAway, 4) * this.mass * this.acceleration this.force = Vector.mult(Vector.normalise(Vector.sub(playerPos, this.position)), mag) //manual friction to not lose rotational velocity Matter.Body.setVelocity(this, { x: this.velocity.x * 0.95, y: this.velocity.y * 0.95 }); //find targets if (!(game.cycle % this.lookFrequency) && !mech.isStealth) { this.lockedOn = null; let closeDist = this.range; for (let i = 0, len = mob.length; i < len; ++i) { const DIST = Vector.magnitude(Vector.sub(this.vertices[0], mob[i].position)); if (DIST - mob[i].radius < closeDist && !mob[i].isShielded && Matter.Query.ray(map, this.vertices[0], mob[i].position).length === 0 && Matter.Query.ray(body, this.vertices[0], mob[i].position).length === 0) { closeDist = DIST; this.lockedOn = mob[i] } } //randomize position relative to player if (Math.random() < 0.15) { this.offPlayer = { x: 100 * (Math.random() - 0.5), y: 90 * (Math.random() - 0.5), } } } //hit target with laser if (this.lockedOn && this.lockedOn.alive && mech.fieldMeter > 0.15) { mech.fieldMeter -= 0.0014 //make sure you can still see vertex const DIST = Vector.magnitude(Vector.sub(this.vertices[0], this.lockedOn.position)); if (DIST - this.lockedOn.radius < this.range + 150 && Matter.Query.ray(map, this.vertices[0], this.lockedOn.position).length === 0 && Matter.Query.ray(body, this.vertices[0], this.lockedOn.position).length === 0) { //move towards the target this.force = Vector.add(this.force, Vector.mult(Vector.normalise(Vector.sub(this.lockedOn.position, this.position)), 0.0013)) //find the closest vertex let bestVertexDistance = Infinity let bestVertex = null for (let i = 0; i < this.lockedOn.vertices.length; i++) { const dist = Vector.magnitude(Vector.sub(this.vertices[0], this.lockedOn.vertices[i])); if (dist < bestVertexDistance) { bestVertex = i bestVertexDistance = dist } } const dmg = b.dmgScale * 0.05; this.lockedOn.damage(dmg); this.lockedOn.locatePlayer(); ctx.beginPath(); //draw laser ctx.moveTo(this.vertices[0].x, this.vertices[0].y); ctx.lineTo(this.lockedOn.vertices[bestVertex].x, this.lockedOn.vertices[bestVertex].y); ctx.strokeStyle = "#f00"; ctx.lineWidth = "2" ctx.lineDashOffset = 300 * Math.random() ctx.setLineDash([50 + 100 * Math.random(), 100 * Math.random()]); ctx.stroke(); ctx.setLineDash([0, 0]); ctx.beginPath(); ctx.arc(this.lockedOn.vertices[bestVertex].x, this.lockedOn.vertices[bestVertex].y, Math.sqrt(dmg) * 100, 0, 2 * Math.PI); ctx.fillStyle = "#f00"; ctx.fill(); } } } }) World.add(engine.world, bullet[me]); //add bullet to world Matter.Body.setVelocity(bullet[me], { x: speed * Math.cos(dir), y: speed * Math.sin(dir) }); }, giveGuns(gun = "random", ammoPacks = 6) { if (gun === "random") { //find what guns player doesn't have options = [] for (let i = 0, len = b.guns.length; i < len; i++) { if (!b.guns[i].have) options.push(i) } if (options.length === 0) return //randomly pick from list of possible guns gun = options[Math.floor(Math.random() * options.length)] } if (gun === "all") { b.activeGun = 0; b.inventoryGun = 0; for (let i = 0; i < b.guns.length; i++) { b.inventory[i] = i; b.guns[i].have = true; b.guns[i].ammo = Math.floor(b.guns[i].ammoPack * ammoPacks); } } else { if (isNaN(gun)) { //find gun by name for (let i = 0; i < b.guns.length; i++) { if (gun === b.guns[i].name) gun = i } } if (!b.guns[gun].have) b.inventory.push(gun); if (b.activeGun === null) b.activeGun = gun //if no active gun switch to new gun b.guns[gun].have = true; b.guns[gun].ammo = Math.floor(b.guns[gun].ammoPack * ammoPacks); } game.makeGunHUD(); }, guns: [{ name: "minigun", //0 description: "rapidly fire a stream of small bullets", ammo: 0, ammoPack: 55, have: false, isStarterGun: true, 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.03 : 0.1); bullet[me] = Bodies.rectangle(mech.pos.x + 30 * Math.cos(mech.angle), mech.pos.y + 30 * Math.sin(mech.angle), 20 * b.modBulletSize, 6 * b.modBulletSize, b.fireAttributes(dir)); b.fireProps(mech.crouch ? 8 : 4, mech.crouch ? 52 : 38, dir, me); //cd , speed bullet[me].endCycle = game.cycle + 70; bullet[me].dmg = 0.07; bullet[me].frictionAir = mech.crouch ? 0.007 : 0.01; bullet[me].do = function () { this.force.y += this.mass * 0.0005; }; } }, { name: "shotgun", //1 description: "fire a burst of short range bullets