forked from sent/waves
8509 lines
411 KiB
JavaScript
8509 lines
411 KiB
JavaScript
//main object for spawning things in a level
|
|
const spawn = {
|
|
nonCollideBossList: ["cellBossCulture", "bomberBoss", "powerUpBoss", "growBossCulture"],
|
|
// other bosses: suckerBoss, laserBoss, tetherBoss, bounceBoss, sprayBoss, mineBoss, hopMomBoss //these need a particular level to work so they are not included in the random pool
|
|
randomBossList: [
|
|
"orbitalBoss", "historyBoss", "shooterBoss", "cellBossCulture", "bomberBoss", "spiderBoss", "launcherBoss", "laserTargetingBoss",
|
|
"powerUpBoss", "powerUpBossBaby", "streamBoss", "pulsarBoss", "spawnerBossCulture", "grenadierBoss", "growBossCulture", "blinkBoss",
|
|
"snakeSpitBoss", "laserBombingBoss", "blockBoss", "revolutionBoss", "slashBoss", "shieldingBoss",
|
|
"timeSkipBoss", "dragonFlyBoss", "beetleBoss", "sneakBoss", "mantisBoss"
|
|
],
|
|
bossTypeSpawnOrder: [], //preset list of boss names calculated at the start of a run by the randomSeed
|
|
bossTypeSpawnIndex: 0, //increases as the boss type cycles
|
|
randomLevelBoss(x, y, options = []) {
|
|
if (options.length === 0) {
|
|
const boss = spawn.bossTypeSpawnOrder[spawn.bossTypeSpawnIndex++ % spawn.bossTypeSpawnOrder.length]
|
|
spawn[boss](x, y)
|
|
} else {
|
|
spawn[options[Math.floor(Math.random() * options.length)]](x, y)
|
|
}
|
|
},
|
|
pickList: ["starter", "starter"],
|
|
fullPickList: [
|
|
"slasher", "slasher", "slasher2", "slasher3",
|
|
"flutter", "flutter", "flutter",
|
|
"hopper", "hopper", "hopper",
|
|
"stabber", "stabber", "stabber",
|
|
"springer", "springer", "springer",
|
|
"shooter", "shooter",
|
|
"grenadier", "grenadier",
|
|
"striker", "striker",
|
|
"laser", "laser",
|
|
"pulsar", "pulsar",
|
|
"sneaker", "launcher", "launcherOne", "exploder", "sucker", "sniper", "spinner", "grower", "beamer", "spawner", "ghoster", "focuser"
|
|
],
|
|
mobTypeSpawnOrder: [], //preset list of mob names calculated at the start of a run by the randomSeed
|
|
mobTypeSpawnIndex: 0, //increases as the mob type cycles
|
|
allowedGroupList: ["spinner", "striker", "springer", "laser", "focuser", "beamer", "exploder", "spawner", "shooter", "launcher", "launcherOne", "stabber", "sniper", "pulsar", "grenadier", "slasher", "flutter"],
|
|
setSpawnList() { //this is run at the start of each new level to determine the possible mobs for the level
|
|
spawn.pickList.splice(0, 1);
|
|
const push = spawn.mobTypeSpawnOrder[spawn.mobTypeSpawnIndex++ % spawn.mobTypeSpawnOrder.length]
|
|
spawn.pickList.push(push);
|
|
// if (spawn.mobTypeSpawnIndex > spawn.mobTypeSpawnOrder.length) spawn.mobTypeSpawnIndex = 0
|
|
//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)]);
|
|
},
|
|
spawnChance(chance) {
|
|
return Math.random() < chance + 0.07 * simulation.difficulty && mob.length < -1 + 16 * Math.log10(simulation.difficulty + 1)
|
|
},
|
|
randomMob(x, y, chance = 1) {
|
|
if (spawn.spawnChance(chance) || chance === Infinity) {
|
|
const pick = spawn.pickList[Math.floor(Math.random() * spawn.pickList.length)];
|
|
spawn[pick](x, y);
|
|
}
|
|
if (tech.isDuplicateMobs && Math.random() < tech.duplicationChance()) {
|
|
const pick = spawn.pickList[Math.floor(Math.random() * spawn.pickList.length)];
|
|
spawn[pick](x, y);
|
|
}
|
|
},
|
|
randomSmallMob(x, y,
|
|
num = Math.max(Math.min(Math.round(Math.random() * simulation.difficulty * 0.2), 4), 0),
|
|
size = 16 + Math.ceil(Math.random() * 15),
|
|
chance = 1) {
|
|
if (spawn.spawnChance(chance)) {
|
|
for (let i = 0; i < num; ++i) {
|
|
const pick = spawn.pickList[Math.floor(Math.random() * spawn.pickList.length)];
|
|
spawn[pick](x + Math.round((Math.random() - 0.5) * 20) + i * size * 2.5, y + Math.round((Math.random() - 0.5) * 20), size);
|
|
}
|
|
}
|
|
if (tech.isDuplicateMobs && Math.random() < tech.duplicationChance()) {
|
|
for (let i = 0; i < num; ++i) {
|
|
const pick = spawn.pickList[Math.floor(Math.random() * spawn.pickList.length)];
|
|
spawn[pick](x + Math.round((Math.random() - 0.5) * 20) + i * size * 2.5, y + Math.round((Math.random() - 0.5) * 20), size);
|
|
}
|
|
}
|
|
},
|
|
randomGroup(x, y, chance = 1) {
|
|
if (spawn.spawnChance(chance) && simulation.difficulty > 2 || chance === Infinity) {
|
|
//choose from the possible picklist
|
|
let pick = spawn.pickList[Math.floor(Math.random() * spawn.pickList.length)];
|
|
//is the pick able to be a group?
|
|
let canBeGroup = false;
|
|
for (let i = 0, len = spawn.allowedGroupList.length; i < len; ++i) {
|
|
if (spawn.allowedGroupList[i] === pick) {
|
|
canBeGroup = true;
|
|
break;
|
|
}
|
|
}
|
|
if (canBeGroup) {
|
|
if (Math.random() < 0.55) {
|
|
spawn.nodeGroup(x, y, pick);
|
|
} else {
|
|
spawn.lineGroup(x, y, pick);
|
|
}
|
|
} else {
|
|
if (Math.random() < 0.1) {
|
|
spawn[pick](x, y, 90 + Math.random() * 40); //one extra large mob
|
|
spawn.spawnOrbitals(mob[mob.length - 1], mob[mob.length - 1].radius + 50 + 200 * Math.random(), 1)
|
|
// } else if (Math.random() < 0.35) {
|
|
// spawn.blockGroup(x, y) //hidden grouping blocks
|
|
} else {
|
|
pick = (Math.random() < 0.5) ? "randomList" : "random";
|
|
if (Math.random() < 0.55) {
|
|
spawn.nodeGroup(x, y, pick);
|
|
} else {
|
|
spawn.lineGroup(x, y, pick);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
secondaryBossThreshold: 22,
|
|
secondaryBossChance(x, y) {
|
|
// if (tech.isDuplicateMobs && Math.random() < tech.duplicationChance()) {
|
|
// tech.isScaleMobsWithDuplication = true
|
|
// spawn.randomLevelBoss(x, y);
|
|
// tech.isScaleMobsWithDuplication = false
|
|
// return true
|
|
// } else if (tech.isResearchBoss) {
|
|
// if (powerUps.research.count > 2) {
|
|
// powerUps.research.changeRerolls(-3)
|
|
// simulation.makeTextLog(`<span class='color-var'>m</span>.<span class='color-r'>research</span> <span class='color-symbol'>-=</span> 3<br>${powerUps.research.count}`)
|
|
// } else {
|
|
// tech.addJunkTechToPool(0.49)
|
|
// }
|
|
// spawn.randomLevelBoss(x, y);
|
|
// return true
|
|
// }
|
|
if (simulation.difficulty > spawn.secondaryBossThreshold) { //starts on hard mode level 6, level 12 on easy, level 4 on why?
|
|
spawn.randomLevelBoss(x, y);
|
|
} else {
|
|
return false
|
|
}
|
|
},
|
|
//mob templates *********************************************************************************************
|
|
//***********************************************************************************************************
|
|
MACHO(x = m.pos.x, y = m.pos.y) { //immortal mob that follows player //if you have the tech it spawns at start of every level at the player
|
|
mobs.spawn(x, y, 3, 0.1, "transparent");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent"
|
|
me.isShielded = true; //makes it immune to damage
|
|
me.leaveBody = false;
|
|
me.isBadTarget = true;
|
|
me.isUnblockable = true;
|
|
me.isDropPowerUp = false;
|
|
me.showHealthBar = false;
|
|
me.collisionFilter.category = 0;
|
|
me.collisionFilter.mask = 0; //cat.player //| cat.body
|
|
me.chaseSpeed = 3.3
|
|
me.isMACHO = true;
|
|
me.frictionAir = 0.006
|
|
me.onDeath = function () {
|
|
tech.isHarmMACHO = false;
|
|
}
|
|
me.do = function () {
|
|
if (!simulation.isTimeSkipping) {
|
|
const sine = Math.sin(simulation.cycle * 0.015)
|
|
this.radius = 55 * tech.isDarkStar + 370 * (1 + 0.1 * sine)
|
|
//chase player
|
|
const sub = Vector.sub(player.position, this.position)
|
|
const mag = Vector.magnitude(sub)
|
|
// follow physics
|
|
// Matter.Body.setVelocity(this, { x: 0, y: 0 });
|
|
// const where = Vector.add(this.position, Vector.mult(Vector.normalise(sub), this.chaseSpeed))
|
|
// if (mag > 10) Matter.Body.setPosition(this, { x: where.x, y: where.y });
|
|
|
|
//realistic physics
|
|
const force = Vector.mult(Vector.normalise(sub), 0.000000003)
|
|
this.force.x += force.x
|
|
this.force.y += force.y
|
|
|
|
|
|
if (mag < this.radius) { //buff to player when inside radius
|
|
tech.isHarmMACHO = true;
|
|
|
|
//draw halo
|
|
ctx.strokeStyle = "rgba(80,120,200,0.2)" //"rgba(255,255,0,0.2)" //ctx.strokeStyle = `rgba(0,0,255,${0.5+0.5*Math.random()})`
|
|
ctx.beginPath();
|
|
ctx.arc(m.pos.x, m.pos.y, 36, 0, 2 * Math.PI);
|
|
ctx.lineWidth = 10;
|
|
ctx.stroke();
|
|
// ctx.strokeStyle = "rgba(255,255,0,0.17)" //ctx.strokeStyle = `rgba(0,0,255,${0.5+0.5*Math.random()})`
|
|
// ctx.beginPath();
|
|
// ctx.arc(this.position.x, this.position.y, this.radius, 0, 2 * Math.PI);
|
|
// ctx.lineWidth = 30;
|
|
// ctx.stroke();
|
|
} else {
|
|
tech.isHarmMACHO = false;
|
|
}
|
|
//draw outline
|
|
ctx.beginPath();
|
|
ctx.arc(this.position.x, this.position.y, this.radius + 15, 0, 2 * Math.PI);
|
|
ctx.strokeStyle = "#000"
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
if (tech.isDarkStar && !m.isCloak) { //&& !m.isBodiesAsleep
|
|
ctx.fillStyle = "rgba(10,0,40,0.4)"
|
|
ctx.fill()
|
|
//damage mobs
|
|
for (let i = 0, len = mob.length; i < len; ++i) {
|
|
if (mob[i].alive && !mob[i].isShielded) {
|
|
if (Vector.magnitude(Vector.sub(this.position, mob[i].position)) - mob[i].radius < this.radius) {
|
|
mob[i].damage(0.02 * m.dmgScale);
|
|
// mob[i].locatePlayer();//
|
|
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: mob[i].position.x,
|
|
y: mob[i].position.y,
|
|
radius: mob[i].radius + 8,
|
|
color: `rgba(10,0,40,0.1)`, // random hue, but not red
|
|
time: 4
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//draw growing and fading out ring around the arc
|
|
ctx.beginPath();
|
|
const rate = 150
|
|
const r = simulation.cycle % rate
|
|
ctx.arc(this.position.x, this.position.y, 15 + this.radius + 0.3 * r, 0, 2 * Math.PI);
|
|
ctx.strokeStyle = `rgba(0,0,0,${0.5 * Math.max(0, 1 - 1.4 * r / rate)})`
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
},
|
|
WIMP(x = level.exit.x + tech.wimpCount * 200 * (Math.random() - 0.5), y = level.exit.y + tech.wimpCount * 200 * (Math.random() - 0.5)) { //immortal mob that follows player //if you have the tech it spawns at start of every level at the exit
|
|
mobs.spawn(x, y, 3, 0.1, "transparent");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent"
|
|
me.isShielded = true; //makes it immune to damage
|
|
me.leaveBody = false;
|
|
me.isBadTarget = true;
|
|
me.isUnblockable = true;
|
|
me.isDropPowerUp = false;
|
|
me.showHealthBar = false;
|
|
me.collisionFilter.category = 0;
|
|
me.collisionFilter.mask = 0; //cat.player //| cat.body
|
|
me.chaseSpeed = 1.2 + 2.3 * Math.random()
|
|
|
|
me.awake = function () {
|
|
//chase player
|
|
const sub = Vector.sub(player.position, this.position)
|
|
const where = Vector.add(this.position, Vector.mult(Vector.normalise(sub), this.chaseSpeed))
|
|
|
|
Matter.Body.setPosition(this, { //hold position
|
|
x: where.x,
|
|
y: where.y
|
|
});
|
|
Matter.Body.setVelocity(this, { x: 0, y: 0 });
|
|
|
|
//aoe damage to player
|
|
if (m.immuneCycle < m.cycle && Vector.magnitude(Vector.sub(player.position, this.position)) < this.radius) {
|
|
const DRAIN = tech.isRadioactiveResistance ? 0.05 * 0.25 : 0.05
|
|
if (m.energy > DRAIN) {
|
|
if (m.immuneCycle < m.cycle) m.energy -= DRAIN
|
|
} else {
|
|
m.energy = 0;
|
|
m.damage((tech.isRadioactiveResistance ? 0.005 * 0.25 : 0.005) * simulation.dmgScale)
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: this.position.x,
|
|
y: this.position.y,
|
|
radius: this.radius,
|
|
color: simulation.mobDmgColor,
|
|
time: simulation.drawTime
|
|
});
|
|
}
|
|
}
|
|
ctx.beginPath();
|
|
ctx.arc(this.position.x, this.position.y, this.radius, 0, 2 * Math.PI);
|
|
ctx.fillStyle = `rgba(25,139,170,${0.2 + 0.12 * Math.random()})`;
|
|
ctx.fill();
|
|
this.radius = 100 * (1 + 0.25 * Math.sin(simulation.cycle * 0.03))
|
|
}
|
|
me.do = function () { //wake up after the player moves
|
|
if (player.speed > 1 && !m.isCloak) {
|
|
if (this.distanceToPlayer() < 500) {
|
|
const unit = Vector.rotate({ x: 1, y: 0 }, Math.random() * 6.28)
|
|
Matter.Body.setPosition(this, Vector.add(player.position, Vector.mult(unit, 2000)))
|
|
}
|
|
setTimeout(() => {
|
|
this.do = this.awake;
|
|
}, 1000 * Math.random());
|
|
}
|
|
this.checkStatus();
|
|
};
|
|
},
|
|
finalBoss(x, y, radius = 300) {
|
|
mobs.spawn(x, y, 6, radius, "rgb(150,150,255)");
|
|
let me = mob[mob.length - 1];
|
|
setTimeout(() => { //fix mob in place, but allow rotation
|
|
me.constraint = Constraint.create({
|
|
pointA: { x: me.position.x, y: me.position.y },
|
|
bodyB: me,
|
|
stiffness: 1,
|
|
damping: 1
|
|
});
|
|
Composite.add(engine.world, me.constraint);
|
|
}, 1000); //add in a delay in case the level gets flipped left right
|
|
me.isBoss = true;
|
|
me.isFinalBoss = true;
|
|
me.frictionAir = 0.01;
|
|
me.memory = Infinity;
|
|
me.locatePlayer();
|
|
me.hasRunDeathScript = false
|
|
me.cycle = 1;
|
|
|
|
Matter.Body.setDensity(me, 0.2); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.damageReduction = 0.14
|
|
me.startingDamageReduction = me.damageReduction
|
|
me.nextHealthThreshold = 0.999
|
|
me.invulnerableCount = 0
|
|
me.isInvulnerable = false
|
|
me.totalModes = 0
|
|
me.lastDamageCycle = 0
|
|
me.onDamage = function () {
|
|
this.lastDamageCycle = this.cycle
|
|
if (this.health < this.nextHealthThreshold) {
|
|
if (this.health === 1) me.cycle = 1; //reset fight
|
|
this.health = this.nextHealthThreshold - 0.01
|
|
this.nextHealthThreshold = Math.floor(this.health * 4) / 4 //0.75,0.5,0.25
|
|
this.invulnerableCount = 220 + 10 * simulation.difficultyMode //how long does invulnerable time last
|
|
this.isInvulnerable = true
|
|
this.damageReduction = 0
|
|
}
|
|
};
|
|
me.invulnerable = function () {
|
|
if (this.isInvulnerable) {
|
|
this.invulnerableCount--
|
|
if (this.invulnerableCount < 0) {
|
|
this.isInvulnerable = false
|
|
this.damageReduction = this.startingDamageReduction
|
|
this.pushAway();
|
|
this.mode[this.totalModes].enter() //enter new mode
|
|
this.totalModes++
|
|
//spawn 6 mobs
|
|
me.mobType = spawn.fullPickList[Math.floor(Math.random() * spawn.fullPickList.length)]; //fire a bullet from each vertex
|
|
for (let i = 0; i < 6; i++) me.spawnMobs(i)
|
|
|
|
level.newLevelOrPhase() //run some new level tech effects
|
|
}
|
|
ctx.beginPath(); //draw invulnerable
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 15 + 6 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
ctx.fillStyle = `rgba(255,255,255,${Math.min(1, 120 / (this.invulnerableCount + 60))})`;
|
|
ctx.fill()
|
|
}
|
|
}
|
|
me.damageReductionDecay = function () { //slowly make the boss take more damage over time //damageReduction resets with each invulnerability phase
|
|
if (!(me.cycle % 60) && this.lastDamageCycle + 240 > this.cycle) this.damageReduction *= 1.04 //only decay once a second //only decay if the player has done damage in the last 4 seconds
|
|
}
|
|
me.mobType = spawn.fullPickList[Math.floor(Math.random() * spawn.fullPickList.length)]
|
|
me.spawnMobs = function (index = 0) {
|
|
const vertex = me.vertices[index]
|
|
const unit = Vector.normalise(Vector.sub(me.position, vertex))
|
|
const where = Vector.add(vertex, Vector.mult(unit, -30))
|
|
spawn[me.mobType](where.x + 50 * (Math.random() - 0.5), where.y + 50 * (Math.random() - 0.5));
|
|
const velocity = Vector.mult(Vector.perp(unit), -10) //give the mob a rotational velocity as if they were attached to a vertex
|
|
Matter.Body.setVelocity(mob[mob.length - 1], { x: me.velocity.x + velocity.x, y: me.velocity.y + velocity.y });
|
|
}
|
|
me.maxMobs = 400
|
|
me.mode = [{
|
|
name: "boulders",
|
|
spawnRate: 170 - 6 * simulation.difficultyMode,
|
|
do() {
|
|
if (!(me.cycle % this.spawnRate) && mob.length < me.maxMobs) {
|
|
me.boulder(me.position.x, me.position.y + 250)
|
|
}
|
|
},
|
|
enter() { },
|
|
exit() { },
|
|
}, {
|
|
name: "mobs",
|
|
// whoSpawn: spawn.fullPickList[Math.floor(Math.random() * spawn.fullPickList.length)],
|
|
spawnRate: 280 - 20 * simulation.difficultyMode,
|
|
do() {
|
|
if (!(me.cycle % this.spawnRate) && mob.length < me.maxMobs) {
|
|
me.torque += 0.000015 * me.inertia; //spin
|
|
const index = Math.floor((me.cycle % (this.spawnRate * 6)) / this.spawnRate) //int from 0 to 5
|
|
if (index === 0) me.mobType = spawn.fullPickList[Math.floor(Math.random() * spawn.fullPickList.length)]; //fire a bullet from each vertex
|
|
me.spawnMobs(index)
|
|
// const vertex = me.vertices[index]
|
|
// const unit = Vector.normalise(Vector.sub(me.position, vertex))
|
|
// const where = Vector.add(vertex, Vector.mult(unit, -30))
|
|
// spawn[me.mobType](where.x + 50 * (Math.random() - 0.5), where.y + 50 * (Math.random() - 0.5));
|
|
// const velocity = Vector.mult(Vector.perp(unit), -18) //give the mob a rotational velocity as if they were attached to a vertex
|
|
// Matter.Body.setVelocity(mob[mob.length - 1], { x: me.velocity.x + velocity.x, y: me.velocity.y + velocity.y });
|
|
}
|
|
},
|
|
enter() { },
|
|
exit() { },
|
|
},
|
|
{
|
|
name: "hoppers",
|
|
spawnRate: 480 - 16 * simulation.difficultyMode,
|
|
do() {
|
|
if (!(me.cycle % this.spawnRate) && mob.length < me.maxMobs) {
|
|
me.torque += 0.00002 * me.inertia; //spin
|
|
for (let i = 0; i < 6; i++) {
|
|
const vertex = me.vertices[i]
|
|
spawn.hopBullet(vertex.x + 50 * (Math.random() - 0.5), vertex.y + 50 * (Math.random() - 0.5), 13 + Math.ceil(Math.random() * 8)); //hopBullet(x, y, radius = 10 + Math.ceil(Math.random() * 8))
|
|
Matter.Body.setDensity(mob[mob.length - 1], 0.002); //normal is 0.001
|
|
const velocity = Vector.mult(Vector.perp(Vector.normalise(Vector.sub(me.position, vertex))), -18) //give the mob a rotational velocity as if they were attached to a vertex
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: me.velocity.x + velocity.x,
|
|
y: me.velocity.y + velocity.y
|
|
});
|
|
}
|
|
let where = { x: 600 - Math.random() * 100, y: -225 }
|
|
if (simulation.isHorizontalFlipped) where.x = -600 + Math.random() * 100
|
|
spawn.hopBullet(where.x, where.y, 13 + Math.ceil(Math.random() * 8)); //hopBullet(x, y, radius = 10 + Math.ceil(Math.random() * 8))
|
|
Matter.Body.setDensity(mob[mob.length - 1], 0.002); //normal is 0.001
|
|
}
|
|
},
|
|
enter() { },
|
|
exit() { },
|
|
},
|
|
{
|
|
name: "seekers",
|
|
spawnRate: 100 - 3 * simulation.difficultyMode,
|
|
do() {
|
|
if (!(me.cycle % this.spawnRate) && mob.length < me.maxMobs) { //spawn seeker
|
|
const index = Math.floor((me.cycle % 360) / 60)
|
|
spawn.seeker(me.vertices[index].x, me.vertices[index].y, 18 * (0.5 + Math.random())); //seeker(x, y, radius = 8, sides = 6)
|
|
const who = mob[mob.length - 1]
|
|
Matter.Body.setDensity(who, 0.00003); //normal is 0.001
|
|
who.timeLeft = 720 + 30 * simulation.difficulty //* (0.8 + 0.4 * Math.random());
|
|
who.accelMag = 0.0004 * simulation.accelScale; //* (0.8 + 0.4 * Math.random())
|
|
who.frictionAir = 0.01 //* (0.8 + 0.4 * Math.random());
|
|
}
|
|
},
|
|
enter() { },
|
|
exit() { },
|
|
},
|
|
{
|
|
name: "mines",
|
|
bombCycle: 0,
|
|
bombInterval: 10 - simulation.difficultyMode,
|
|
do() {
|
|
const yOff = 120
|
|
this.bombCycle++
|
|
if (!(this.bombCycle % this.bombInterval) && (this.bombCycle % 660) > 330) { //mines above player
|
|
if (simulation.isHorizontalFlipped) {
|
|
const x = m.pos.x + 200 * (Math.random() - 0.5)
|
|
if (x > -750) { //mines above player IN tunnel
|
|
spawn.mine(Math.min(Math.max(-730, x), 100), -450 - yOff * Math.random()) //player in main room
|
|
mob[mob.length - 1].fallHeight = -209
|
|
} else { //mines above player NOT in tunnel
|
|
spawn.mine(Math.min(Math.max(-5375, x), -765), -1500 - yOff * Math.random()) //player in tunnel
|
|
mob[mob.length - 1].fallHeight = -9
|
|
}
|
|
if (Math.random() < 0.5) {
|
|
spawn.mine(-5350 + 4550 * Math.random(), -1500 - yOff * Math.random()) //random mines
|
|
mob[mob.length - 1].fallHeight = -9
|
|
}
|
|
} else {
|
|
const x = m.pos.x + 200 * (Math.random() - 0.5)
|
|
if (x < 750) { //mines above player IN tunnel
|
|
spawn.mine(Math.min(Math.max(-100, x), 735), -450 - yOff * Math.random()) //player in main room
|
|
mob[mob.length - 1].fallHeight = -209
|
|
} else { //mines above player NOT in tunnel
|
|
spawn.mine(Math.min(Math.max(760, x), 5375), -1500 - yOff * Math.random()) //player in tunnel
|
|
mob[mob.length - 1].fallHeight = -9
|
|
}
|
|
if (Math.random() < 0.5) { //random mines, but not in tunnel
|
|
spawn.mine(800 + 4550 * Math.random(), -1500 - yOff * Math.random()) //random mines
|
|
mob[mob.length - 1].fallHeight = -9
|
|
}
|
|
}
|
|
}
|
|
for (let i = 0; i < mob.length; i++) { //mines fall
|
|
if (mob[i].isMine) {
|
|
if (mob[i].position.y < mob[i].fallHeight) {
|
|
mob[i].force.y += mob[i].mass * 0.03;
|
|
} else if (!mob[i].isOnGround) {
|
|
mob[i].isOnGround = true
|
|
Matter.Body.setPosition(mob[i], {
|
|
x: mob[i].position.x,
|
|
y: mob[i].fallHeight
|
|
})
|
|
}
|
|
}
|
|
}
|
|
},
|
|
enter() {
|
|
this.bombCycle = 0;
|
|
},
|
|
exit() {
|
|
for (let i = 0; i < mob.length; i++) {
|
|
if (mob[i].isMine) mob[i].isExploding = true //explode the mines at the start of new round
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "orbiters",
|
|
spawnRate: Math.ceil(4 - 0.25 * simulation.difficultyMode),
|
|
orbitersCycle: 0,
|
|
do() {
|
|
this.orbitersCycle++
|
|
if (!(this.orbitersCycle % this.spawnRate) && (this.orbitersCycle % 660) > 600 && mob.length < me.maxMobs) {
|
|
const speed = (0.01 + 0.0005 * simulation.difficultyMode) * ((Math.random() < 0.5) ? 0.85 : -1.15)
|
|
const phase = 0 //Math.floor(2 * Math.random()) * Math.PI
|
|
//find distance to play and set orbs at that range
|
|
const dist = me.distanceToPlayer()
|
|
//360 + 2150 * Math.random()
|
|
me.orbitalNoVelocity(me, dist + 900 * (Math.random() - 0.5), 0.1 * Math.random() + phase, speed) // orbital(who, radius, phase, speed)
|
|
}
|
|
},
|
|
enter() { },
|
|
exit() { },
|
|
},
|
|
{
|
|
name: "laser",
|
|
spinForce: 0.00000008, // * (Math.random() < 0.5 ? -1 : 1),
|
|
fadeCycle: 0, //fades in over 4 seconds
|
|
do() {
|
|
this.fadeCycle++
|
|
if (this.fadeCycle > 0) {
|
|
me.torque += this.spinForce * me.inertia; //spin //0.00000015
|
|
if (this.fadeCycle > 360) this.fadeCycle = -150 + 2 * simulation.difficultyMode * simulation.difficultyMode //turn laser off and reset
|
|
ctx.strokeStyle = "#50f";
|
|
ctx.setLineDash([70 + 300 * Math.random(), 55 * Math.random()]);
|
|
ctx.lineWidth = 1.5;
|
|
ctx.beginPath();
|
|
if (this.fadeCycle < 120) { //damage scales up over 2 seconds to give player time to move as it fades in
|
|
const scale = this.fadeCycle / 120
|
|
const dmg = this.fadeCycle < 60 ? 0 : 0.1 * simulation.dmgScale * scale
|
|
me.lasers(me.vertices[0], me.angle + Math.PI / 6, dmg);
|
|
me.lasers(me.vertices[1], me.angle + 3 * Math.PI / 6, dmg);
|
|
me.lasers(me.vertices[2], me.angle + 5 * Math.PI / 6, dmg);
|
|
me.lasers(me.vertices[3], me.angle + 7 * Math.PI / 6, dmg);
|
|
me.lasers(me.vertices[4], me.angle + 9 * Math.PI / 6, dmg);
|
|
me.lasers(me.vertices[5], me.angle + 11 * Math.PI / 6, dmg);
|
|
ctx.strokeStyle = `rgba(85, 0, 255,${scale})`;
|
|
ctx.stroke();
|
|
ctx.strokeStyle = `rgba(80, 0, 255,${0.07 * scale})`
|
|
} else if (this.fadeCycle > 0) {
|
|
me.lasers(me.vertices[0], me.angle + Math.PI / 6);
|
|
me.lasers(me.vertices[1], me.angle + 3 * Math.PI / 6);
|
|
me.lasers(me.vertices[2], me.angle + 5 * Math.PI / 6);
|
|
me.lasers(me.vertices[3], me.angle + 7 * Math.PI / 6);
|
|
me.lasers(me.vertices[4], me.angle + 9 * Math.PI / 6);
|
|
me.lasers(me.vertices[5], me.angle + 11 * Math.PI / 6);
|
|
ctx.strokeStyle = "#50f";
|
|
ctx.stroke();
|
|
ctx.strokeStyle = "rgba(80,0,255,0.07)";
|
|
}
|
|
ctx.setLineDash([]);
|
|
ctx.lineWidth = 20;
|
|
ctx.stroke();
|
|
}
|
|
},
|
|
enter() { this.fadeCycle = 0 },
|
|
exit() { },
|
|
},
|
|
{
|
|
name: "black hole",
|
|
eventHorizon: 0,
|
|
eventHorizonRadius: 1900,
|
|
eventHorizonCycle: 0,
|
|
do() {
|
|
this.eventHorizonCycle++
|
|
this.eventHorizon = Math.max(0, this.eventHorizonRadius * Math.sin(this.eventHorizonCycle * 0.007)) //eventHorizon waves in and out
|
|
//draw darkness
|
|
ctx.beginPath();
|
|
ctx.arc(me.position.x, me.position.y, this.eventHorizon * 0.2, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "rgba(0,20,40,0.3)";
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(me.position.x, me.position.y, this.eventHorizon * 0.4, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "rgba(0,20,40,0.25)";
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(me.position.x, me.position.y, this.eventHorizon * 0.6, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "rgba(0,20,40,0.2)";
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(me.position.x, me.position.y, this.eventHorizon * 0.8, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "rgba(0,20,40,0.15)";
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(me.position.x, me.position.y, this.eventHorizon, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "rgba(0,0,0,0.1)";
|
|
ctx.fill();
|
|
//when player is inside event horizon
|
|
if (Vector.magnitude(Vector.sub(me.position, player.position)) < this.eventHorizon) {
|
|
if (m.immuneCycle < m.cycle) {
|
|
if (m.energy > 0) m.energy -= 0.018
|
|
if (m.energy < 0.05 && m.immuneCycle < m.cycle) m.damage(0.0003 * simulation.dmgScale);
|
|
}
|
|
const angle = Math.atan2(player.position.y - me.position.y, player.position.x - me.position.x);
|
|
player.force.x -= 0.0017 * Math.cos(angle) * player.mass * (m.onGround ? 1.7 : 1);
|
|
player.force.y -= 0.0017 * Math.sin(angle) * player.mass;
|
|
//draw line to player
|
|
ctx.beginPath();
|
|
ctx.moveTo(me.position.x, me.position.y);
|
|
ctx.lineTo(m.pos.x, m.pos.y);
|
|
ctx.lineWidth = Math.min(60, me.radius * 2);
|
|
ctx.strokeStyle = "rgba(0,0,0,0.5)";
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.arc(m.pos.x, m.pos.y, 40, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "rgba(0,0,0,0.3)";
|
|
ctx.fill();
|
|
}
|
|
me.curl(this.eventHorizon);
|
|
},
|
|
enter() { this.eventHorizonCycle = 0 },
|
|
exit() { },
|
|
},
|
|
{
|
|
name: "oscillation",
|
|
waveCycle: 0,
|
|
whereX: simulation.isHorizontalFlipped ? -3000 : 3000,
|
|
do() {
|
|
this.waveCycle += !me.isStunned + !me.isSlowed
|
|
// if (!me.isShielded && (!(this.waveCycle % 1800) || !(this.waveCycle % 1801))) spawn.shield(me, me.position.x, me.position.y, 1);
|
|
me.constraint.pointA = {
|
|
x: this.whereX + 600 * Math.sin(this.waveCycle * 0.005),
|
|
y: me.constraint.pointA.y
|
|
}
|
|
},
|
|
enter() {
|
|
spawn.shield(me, me.position.x, me.position.y, 1);
|
|
},
|
|
exit() { this.waveCycle = 0 },
|
|
},
|
|
// {
|
|
// name: "__",
|
|
// do() {},
|
|
// enter() {},
|
|
// exit() {},
|
|
// },
|
|
]
|
|
shuffle(me.mode); //THIS SHOULDN'T BE COMMENTED OUT!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
me.do = function () {
|
|
this.fill = `hsl(${360 * Math.sin(this.cycle * 0.011)},${80 + 20 * Math.sin(this.cycle * 0.004)}%,${60 + 20 * Math.sin(this.cycle * 0.009)}%)`
|
|
if (this.health < 1) {
|
|
this.cycle++;
|
|
this.checkStatus();
|
|
this.invulnerable();
|
|
this.spawnBoss();
|
|
this.damageReductionDecay();
|
|
for (let i = 0; i < this.totalModes; i++) this.mode[i].do()
|
|
}
|
|
// this.mode[5].do() //deelete this
|
|
// this.cycle++;
|
|
// this.mode[4].do()
|
|
// this.mode[7].do()
|
|
};
|
|
me.spawnRate = 5800 - 30 * simulation.difficultyMode * simulation.difficultyMode
|
|
me.spawnBoss = function () { //if the fight lasts too long start spawning bosses
|
|
if (!(me.cycle % this.spawnRate) && this.health < 1) {
|
|
this.spawnRate = Math.max(300, this.spawnRate - 10 * simulation.difficultyMode * simulation.difficultyMode) //reduce the timer each time a boss spawns
|
|
spawn.randomLevelBoss(3000 * (simulation.isHorizontalFlipped ? -1 : 1) + 2000 * (Math.random() - 0.5), -1100 + 200 * (Math.random() - 0.5))
|
|
}
|
|
}
|
|
me.pushAway = function (magX = 0.13, magY = 0.05) {
|
|
for (let i = 0, len = body.length; i < len; ++i) { //push blocks away horizontally
|
|
body[i].force.x += magX * body[i].mass * (body[i].position.x > this.position.x ? 1 : -1)
|
|
body[i].force.y -= magY * body[i].mass
|
|
}
|
|
for (let i = 0, len = bullet.length; i < len; ++i) { //push blocks away horizontally
|
|
bullet[i].force.x += magX * bullet[i].mass * (bullet[i].position.x > this.position.x ? 1 : -1)
|
|
bullet[i].force.y -= magY * bullet[i].mass
|
|
}
|
|
for (let i = 0, len = powerUp.length; i < len; ++i) { //push blocks away horizontally
|
|
powerUp[i].force.x += magX * powerUp[i].mass * (powerUp[i].position.x > this.position.x ? 1 : -1)
|
|
powerUp[i].force.y -= magY * powerUp[i].mass
|
|
}
|
|
player.force.x += magX * player.mass * (player.position.x > this.position.x ? 1 : -1)
|
|
player.force.y -= magY * player.mass
|
|
}
|
|
me.boulder = function (x, y) {
|
|
mobs.spawn(x, y, 6, Math.floor(50 + 50 * Math.random()), this.fill);
|
|
let boss = this
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent";
|
|
me.onHit = function () {
|
|
this.timeLeft = 0
|
|
};
|
|
me.explodeRange = 500
|
|
me.onDeath = function () { //explode
|
|
simulation.drawList.push({ //draw explosion
|
|
x: this.position.x,
|
|
y: this.position.y,
|
|
radius: this.explodeRange,
|
|
color: this.fill,
|
|
time: 8
|
|
});
|
|
let sub, knock
|
|
sub = Vector.sub(player.position, this.position);
|
|
if (Vector.magnitude(sub) < this.explodeRange) { //player knock back
|
|
m.damage(0.05);
|
|
knock = Vector.mult(Vector.normalise(sub), player.mass * 0.1);
|
|
player.force.x += knock.x;
|
|
player.force.y += knock.y;
|
|
}
|
|
for (let i = 0, len = powerUp.length; i < len; ++i) { //power up knock backs
|
|
sub = Vector.sub(powerUp[i].position, this.position);
|
|
if (Vector.magnitude(sub) < this.explodeRange) {
|
|
knock = Vector.mult(Vector.normalise(sub), powerUp[i].mass * 0.1);
|
|
powerUp[i].force.x += knock.x;
|
|
powerUp[i].force.y += knock.y;
|
|
}
|
|
}
|
|
for (let i = 0, len = body.length; i < len; ++i) { //block knock backs
|
|
sub = Vector.sub(body[i].position, this.position);
|
|
if (Vector.magnitude(sub) < this.explodeRange) {
|
|
knock = Vector.mult(Vector.normalise(sub), body[i].mass * 0.1);
|
|
body[i].force.x += knock.x;
|
|
body[i].force.y += knock.y;
|
|
}
|
|
}
|
|
}
|
|
Matter.Body.setDensity(me, 0.003); //normal is 0.001
|
|
me.timeLeft = 360;
|
|
me.g = 0.0005; //required if using this.gravity
|
|
me.frictionAir = 0.005;
|
|
me.friction = 1;
|
|
me.frictionStatic = 1
|
|
me.restitution = 0;
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.isBadTarget = true;
|
|
me.isMobBullet = true;
|
|
me.showHealthBar = false;
|
|
me.collisionFilter.category = cat.mobBullet;
|
|
me.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet;
|
|
me.spin = me.inertia * 0.000005 * (1 + Math.random()) * (m.pos.x > me.position.x ? 1 : -1)
|
|
Matter.Body.setAngularVelocity(me, 0.1 * (1 + 0.3 * Math.random()) * (m.pos.x > me.position.x ? 1 : -1));
|
|
Matter.Body.setVelocity(me, { x: 0, y: 10 });
|
|
me.do = function () {
|
|
this.fill = boss.fill
|
|
this.torque += this.spin;
|
|
this.gravity();
|
|
this.timeLimit();
|
|
};
|
|
}
|
|
me.orbitalNoVelocity = function (who, radius, phase, speed) { //orbitals that don't include their host velocity //specifically for finalBoss
|
|
let boss = this
|
|
mobs.spawn(who.position.x, who.position.y, 6, 20, "rgb(255,0,150)");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent";
|
|
Matter.Body.setDensity(me, 0.001); //normal is 0.001
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.isBadTarget = true;
|
|
me.isUnstable = true; //dies when blocked
|
|
me.showHealthBar = false;
|
|
me.isOrbital = true;
|
|
me.collisionFilter.category = cat.mobBullet;
|
|
me.collisionFilter.mask = cat.bullet; //cat.player | cat.map | cat.body
|
|
me.do = function () {
|
|
this.fill = boss.fill
|
|
const time = simulation.cycle * speed + phase
|
|
const orbit = { x: Math.cos(time), y: Math.sin(time) }
|
|
Matter.Body.setPosition(this, Vector.add(who.position, Vector.mult(orbit, radius)))
|
|
if (Matter.Query.collides(this, [player]).length > 0 && !(m.isCloak && tech.isIntangible) && m.immuneCycle < m.cycle) {
|
|
m.immuneCycle = m.cycle + m.collisionImmuneCycles; //player is immune to damage for 30 cycles
|
|
const dmg = 0.13 * simulation.dmgScale
|
|
m.damage(dmg);
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: this.position.x,
|
|
y: this.position.y,
|
|
radius: Math.sqrt(dmg) * 200,
|
|
color: simulation.mobDmgColor,
|
|
time: simulation.drawTime
|
|
});
|
|
this.death();
|
|
}
|
|
};
|
|
}
|
|
me.lasers = function (where, angle, dmg = 0.1 * simulation.dmgScale) {
|
|
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 = simulation.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 = simulation.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 = 7000;
|
|
best = {
|
|
x: null,
|
|
y: null,
|
|
dist2: Infinity,
|
|
who: null,
|
|
v1: null,
|
|
v2: null
|
|
};
|
|
const look = {
|
|
x: where.x + seeRange * Math.cos(angle),
|
|
y: where.y + seeRange * Math.sin(angle)
|
|
};
|
|
// vertexCollision(where, look, mob);
|
|
vertexCollision(where, look, map);
|
|
vertexCollision(where, look, body);
|
|
if (!m.isCloak) vertexCollision(where, look, [playerBody, playerHead]);
|
|
if (best.who && (best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
|
|
if (m.immuneCycle < m.cycle + 60 + m.collisionImmuneCycles) m.immuneCycle = m.cycle + 60 + m.collisionImmuneCycles; //player is immune to damage extra time
|
|
m.damage(dmg);
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: best.x,
|
|
y: best.y,
|
|
radius: dmg * 1500,
|
|
color: "rgba(80,0,255,0.5)",
|
|
time: 20
|
|
});
|
|
}
|
|
//draw beam
|
|
if (best.dist2 === Infinity) best = look;
|
|
ctx.moveTo(where.x, where.y);
|
|
ctx.lineTo(best.x, best.y);
|
|
}
|
|
me.onDeath = function () {
|
|
if (!this.hasRunDeathScript) {
|
|
this.hasRunDeathScript = true
|
|
//make a block body to replace this one
|
|
//this body is too big to leave behind in the normal way mobs.replace()
|
|
const len = body.length;
|
|
const v = Matter.Vertices.hull(Matter.Vertices.clockwiseSort(this.vertices)) //might help with vertex collision issue, not sure
|
|
body[len] = Matter.Bodies.fromVertices(this.position.x, this.position.y, v);
|
|
Matter.Body.setVelocity(body[len], { x: 0, y: -3 });
|
|
Matter.Body.setAngularVelocity(body[len], this.angularVelocity);
|
|
body[len].collisionFilter.category = cat.body;
|
|
body[len].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet;
|
|
body[len].classType = "body";
|
|
Composite.add(engine.world, body[len]); //add to world
|
|
const expand = function (that, massLimit) {
|
|
const scale = 1.05;
|
|
Matter.Body.scale(that, scale, scale);
|
|
if (that.mass < massLimit) setTimeout(expand, 20, that, massLimit);
|
|
};
|
|
expand(body[len], 200)
|
|
|
|
function unlockExit() {
|
|
if (simulation.isHorizontalFlipped) {
|
|
level.exit.x = -5500 - 100;
|
|
} else {
|
|
level.exit.x = 5500;
|
|
}
|
|
level.exit.y = -330;
|
|
Matter.Composite.remove(engine.world, map[map.length - 1]);
|
|
map.splice(map.length - 1, 1);
|
|
simulation.draw.setPaths(); //redraw map draw path
|
|
// level.levels.push("null")
|
|
}
|
|
|
|
//add lore level as next level if player took lore tech earlier in the game
|
|
if (lore.techCount > (lore.techGoal - 1) && !simulation.isCheating) {
|
|
simulation.makeTextLog(`<span class="lore-text">undefined</span> <span class='color-symbol'>=</span> ${lore.techCount}/${lore.techGoal}`, 360);
|
|
setTimeout(function () {
|
|
simulation.makeTextLog(`level.levels.push("<span class='lore-text'>null</span>")`, 720);
|
|
unlockExit()
|
|
level.levels.push("null")
|
|
}, 4000);
|
|
//remove block map element so exit is clear
|
|
} else { //reset game
|
|
let count = 0
|
|
|
|
function loop() {
|
|
if (!simulation.paused && !simulation.onTitlePage) {
|
|
count++
|
|
if (count < 660) {
|
|
if (count === 1) simulation.makeTextLog(`<em>//enter testing mode to set level.levels.length to <strong>Infinite</strong></em>`);
|
|
if (!(count % 60)) simulation.makeTextLog(`simulation.analysis <span class='color-symbol'>=</span> ${((count / 60 - Math.random()) * 0.1).toFixed(3)}`);
|
|
} else if (count === 660) {
|
|
simulation.makeTextLog(`simulation.analysis <span class='color-symbol'>=</span> 1 <em>//analysis complete</em>`);
|
|
} else if (count === 780) {
|
|
simulation.makeTextLog(`<span class="lore-text">undefined</span> <span class='color-symbol'>=</span> ${lore.techCount}/${lore.techGoal}`)
|
|
} else if (count === 1020) {
|
|
simulation.makeTextLog(`Engine.clear(engine) <em>//simulation successful</em>`);
|
|
} else if (count === 1260) {
|
|
// tech.isImmortal = false;
|
|
// m.death()
|
|
// m.alive = false;
|
|
// simulation.paused = true;
|
|
// m.health = 0;
|
|
// m.displayHealth();
|
|
document.getElementById("health").style.display = "none"
|
|
document.getElementById("health-bg").style.display = "none"
|
|
document.getElementById("defense-bar").style.display = "none"
|
|
document.getElementById("damage-bar").style.display = "none"
|
|
document.getElementById("text-log").style.display = "none"
|
|
document.getElementById("fade-out").style.opacity = 1; //slowly fades out
|
|
// build.shareURL(false)
|
|
setTimeout(function () {
|
|
if (!simulation.onTitlePage) {
|
|
m.alive = false
|
|
simulation.paused = true;
|
|
// simulation.clearMap();
|
|
// Matter.Composite.clear(composite, keepStatic, [deep = false])
|
|
// Composite.clear(engine.composite);
|
|
engine.world.bodies.forEach((body) => { Matter.Composite.remove(engine.world, body) })
|
|
Engine.clear(engine);
|
|
simulation.splashReturn();
|
|
}
|
|
}, 6000);
|
|
return
|
|
}
|
|
}
|
|
if (simulation.testing) {
|
|
unlockExit()
|
|
setTimeout(function () {
|
|
simulation.makeTextLog(`level.levels.length <span class='color-symbol'>=</span> <strong>Infinite</strong>`);
|
|
}, 1500);
|
|
} else {
|
|
if (!simulation.onTitlePage) requestAnimationFrame(loop);
|
|
}
|
|
}
|
|
requestAnimationFrame(loop);
|
|
}
|
|
// for (let i = 0; i < 3; i++)
|
|
level.difficultyIncrease(simulation.difficultyMode) //ramp up damage
|
|
//remove power Ups, to avoid spamming console
|
|
function removeAll(array) {
|
|
for (let i = 0; i < array.length; ++i) Matter.Composite.remove(engine.world, array[i]);
|
|
}
|
|
removeAll(powerUp);
|
|
powerUp = [];
|
|
|
|
//pull in particles
|
|
for (let i = 0, len = body.length; i < len; ++i) {
|
|
const velocity = Vector.mult(Vector.normalise(Vector.sub(this.position, body[i].position)), 65)
|
|
const pushUp = Vector.add(velocity, { x: 0, y: -0.5 })
|
|
Matter.Body.setVelocity(body[i], Vector.add(body[i].velocity, pushUp));
|
|
}
|
|
//damage all mobs
|
|
for (let j = 0; j < 8; j++) { //in case some mobs leave things after they die
|
|
for (let i = 0, len = mob.length; i < len; ++i) {
|
|
if (mob[i] !== this) {
|
|
if (mob[i].isInvulnerable) { //disable invulnerability
|
|
mob[i].isInvulnerable = false
|
|
mob[i].damageReduction = 1
|
|
}
|
|
mob[i].damage(Infinity, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
//draw stuff
|
|
for (let i = 0, len = 22; i < len; i++) {
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: this.position.x,
|
|
y: this.position.y,
|
|
radius: (i + 1) * 150,
|
|
color: `rgba(255,255,255,0.17)`,
|
|
time: 5 * (len - i + 1)
|
|
});
|
|
}
|
|
}
|
|
};
|
|
},
|
|
// finalBoss(x, y, radius = 300) {
|
|
// mobs.spawn(x, y, 6, radius, "rgb(150,150,255)");
|
|
// let me = mob[mob.length - 1];
|
|
// setTimeout(() => { //fix mob in place, but allow rotation
|
|
// me.constraint = Constraint.create({
|
|
// pointA: {
|
|
// x: me.position.x,
|
|
// y: me.position.y
|
|
// },
|
|
// bodyB: me,
|
|
// stiffness: 1,
|
|
// damping: 1
|
|
// });
|
|
// Composite.add(engine.world, me.constraint);
|
|
// }, 2000); //add in a delay in case the level gets flipped left right
|
|
// me.isBoss = true;
|
|
// me.isFinalBoss = true;
|
|
// me.frictionAir = 0.01;
|
|
// me.memory = Infinity;
|
|
// me.hasRunDeathScript = false
|
|
// me.locatePlayer();
|
|
// // const density = 0.2
|
|
// Matter.Body.setDensity(me, 0.2); //extra dense //normal is 0.001 //makes effective life much larger
|
|
// // spawn.shield(me, x, y, 1);
|
|
// me.onDamage = function() {};
|
|
// me.cycle = 660;
|
|
// me.endCycle = 780;
|
|
// me.totalCycles = 0
|
|
// me.mode = 0;
|
|
// me.damageReduction = 0.25 //reset on each new mode
|
|
// me.pushAway = function(magX = 0.13, magY = 0.05) {
|
|
// for (let i = 0, len = body.length; i < len; ++i) { //push blocks away horizontally
|
|
// body[i].force.x += magX * body[i].mass * (body[i].position.x > this.position.x ? 1 : -1)
|
|
// body[i].force.y -= magY * body[i].mass
|
|
// }
|
|
// for (let i = 0, len = bullet.length; i < len; ++i) { //push blocks away horizontally
|
|
// bullet[i].force.x += magX * bullet[i].mass * (bullet[i].position.x > this.position.x ? 1 : -1)
|
|
// bullet[i].force.y -= magY * bullet[i].mass
|
|
// }
|
|
// for (let i = 0, len = powerUp.length; i < len; ++i) { //push blocks away horizontally
|
|
// powerUp[i].force.x += magX * powerUp[i].mass * (powerUp[i].position.x > this.position.x ? 1 : -1)
|
|
// powerUp[i].force.y -= magY * powerUp[i].mass
|
|
// }
|
|
// player.force.x += magX * player.mass * (player.position.x > this.position.x ? 1 : -1)
|
|
// player.force.y -= magY * player.mass
|
|
// }
|
|
// me.do = function() {
|
|
// this.modeDo(); //this does different things based on the mode
|
|
// this.checkStatus();
|
|
// this.cycle++; //switch modes÷ if time isn't paused
|
|
// this.totalCycles++;
|
|
// if (this.health > 0.3) {
|
|
// if (this.cycle > this.endCycle) {
|
|
// this.showHealthBar = true
|
|
// this.cycle = 0;
|
|
// this.mode++
|
|
// this.damageReduction = 0.25
|
|
// if (this.totalCycles > 180) this.pushAway();
|
|
// if (this.mode > 3) {
|
|
// this.mode = 0;
|
|
// this.fill = "#50f";
|
|
// this.rotateVelocity = Math.abs(this.rotateVelocity) * (player.position.x > this.position.x ? 1 : -1) //rotate so that the player can get away
|
|
// this.modeDo = this.modeLasers
|
|
// //push blocks and player away, since this is the end of suck, and suck causes blocks to fall on the boss and stun it
|
|
// Matter.Body.scale(this, 1000, 1000);
|
|
// if (!this.isShielded) spawn.shield(this, this.position.x, this.position.y, 1); // regen shield to also prevent stun
|
|
// } else if (this.mode === 1) {
|
|
// this.fill = "#50f"; // this.fill = "rgb(150,150,255)";
|
|
// this.modeDo = this.modeSpawns
|
|
// } else if (this.mode === 2) {
|
|
// this.fill = "#50f";
|
|
// this.modeDo = this.modeBombs
|
|
// } else if (this.mode === 3) {
|
|
// for (let i = 0; i < mob.length; i++) {
|
|
// if (mob[i].isMine) mob[i].isExploding = true //explode the mines at the start of new round
|
|
// }
|
|
// this.fill = "#000";
|
|
// this.modeDo = this.modeSuck
|
|
// Matter.Body.scale(this, 0.001, 0.001);
|
|
// this.damageReduction = 0.000025
|
|
// this.showHealthBar = false
|
|
// }
|
|
|
|
// if (tech.isGunCycle) {
|
|
// b.inventoryGun++;
|
|
// if (b.inventoryGun > b.inventory.length - 1) b.inventoryGun = 0;
|
|
// simulation.switchGun();
|
|
// }
|
|
// }
|
|
// } else if (this.mode !== 3) { //all three modes at once , this runs once
|
|
// this.showHealthBar = true
|
|
// this.pushAway();
|
|
// this.cycle = 0;
|
|
// this.endCycle = Infinity
|
|
// this.damageReduction = 0.15
|
|
// if (this.mode === 2) {
|
|
// Matter.Body.scale(this, 500, 500);
|
|
// } else {
|
|
// Matter.Body.scale(this, 0.5, 0.5);
|
|
// }
|
|
// this.mode = 3
|
|
// this.fill = "#000";
|
|
// this.eventHorizon = 750
|
|
// this.spawnInterval = 600
|
|
// this.rotateVelocity = 0.001 * (player.position.x > this.position.x ? 1 : -1) //rotate so that the player can get away
|
|
// // if (!this.isShielded) spawn.shield(this, x, y, 1); //regen shield here ?
|
|
// this.modeDo = this.modeAll
|
|
// this.eventHorizonRadius = 700
|
|
// if (tech.isGunCycle) {
|
|
// b.inventoryGun++;
|
|
// if (b.inventoryGun > b.inventory.length - 1) b.inventoryGun = 0;
|
|
// simulation.switchGun();
|
|
// }
|
|
// }
|
|
// // }
|
|
// };
|
|
// me.modeDo = function() {}
|
|
// me.modeAll = function() {
|
|
// this.modeBombs()
|
|
// this.modeSpawns()
|
|
// this.modeSuck()
|
|
// this.modeLasers()
|
|
// }
|
|
// me.bombInterval = 36 - 0.5 * simulation.difficultyMode * simulation.difficultyMode
|
|
// me.modeBombs = function() {
|
|
// if (!(this.cycle % 20)) {
|
|
// if (m.pos.x < 750) {
|
|
// spawn.mine(m.pos.x + 200 * (Math.random() - 0.5), -500)
|
|
// } else {
|
|
// spawn.mine(Math.min(Math.max(770, m.pos.x + 200 * (Math.random() - 0.5)), 5350), -1500)
|
|
// }
|
|
// }
|
|
// if (!(this.cycle % 10)) spawn.mine(800 + 4550 * Math.random(), -1500)
|
|
|
|
// //mines fall
|
|
// for (let i = 0; i < mob.length; i++) {
|
|
// // if (mob[i].isMine && mob[i].position.y < -5) Matter.Body.setPosition(mob[i], { x: mob[i].position.x, y: mob[i].position.y + 5 })
|
|
// if (mob[i].isMine && mob[i].position.y < -14) mob[i].force.y += mob[i].mass * 0.03;
|
|
// }
|
|
|
|
// }
|
|
// me.spawnInterval = 395
|
|
// me.modeSpawns = function() {
|
|
// if (!(this.cycle % this.spawnInterval) && mob.length < 40) {
|
|
// if (this.mode !== 3) Matter.Body.setAngularVelocity(this, 0.1)
|
|
// //fire a bullet from each vertex
|
|
// const whoSpawn = spawn.fullPickList[Math.floor(Math.random() * spawn.fullPickList.length)];
|
|
// for (let i = 0, len = 2 + this.totalCycles / 1000; i < len; i++) {
|
|
// const vertex = this.vertices[i % 6]
|
|
// spawn[whoSpawn](vertex.x + 50 * (Math.random() - 0.5), vertex.y + 50 * (Math.random() - 0.5));
|
|
// const velocity = Vector.mult(Vector.perp(Vector.normalise(Vector.sub(this.position, vertex))), -18) //give the mob a rotational velocity as if they were attached to a vertex
|
|
// Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
// x: this.velocity.x + velocity.x,
|
|
// y: this.velocity.y + velocity.y
|
|
// });
|
|
// }
|
|
// if (!(this.cycle % 2 * this.spawnInterval) && mob.length < 40) {
|
|
// const len = (this.totalCycles / 1000 + simulation.difficulty / 2 - 30) / 15
|
|
// for (let i = 0; i < len; i++) {
|
|
// spawn.randomLevelBoss(3000 * (simulation.isHorizontalFlipped ? -1 : 1) + 2000 * (Math.random() - 0.5), -1100 + 200 * (Math.random() - 0.5))
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// me.eventHorizon = 0
|
|
// me.eventHorizonRadius = 1300
|
|
// me.modeSuck = function() {
|
|
// if (!(this.cycle % 30)) {
|
|
// const index = Math.floor((this.cycle % 360) / 60)
|
|
// spawn.seeker(this.vertices[index].x, this.vertices[index].y, 20 * (0.5 + Math.random()), 9); //give the bullet a rotational velocity as if they were attached to a vertex
|
|
// const who = mob[mob.length - 1]
|
|
// Matter.Body.setDensity(who, 0.00003); //normal is 0.001
|
|
// who.timeLeft = 720 + 10 * simulation.difficulty //* (0.8 + 0.4 * Math.random());
|
|
// who.accelMag = 0.0003 * simulation.accelScale; //* (0.8 + 0.4 * Math.random())
|
|
// who.frictionAir = 0.01 //* (0.8 + 0.4 * Math.random());
|
|
// const velocity = Vector.mult(Vector.perp(Vector.normalise(Vector.sub(this.position, this.vertices[index]))), -7)
|
|
// Matter.Body.setVelocity(who, {
|
|
// x: this.velocity.x + velocity.x,
|
|
// y: this.velocity.y + velocity.y
|
|
// });
|
|
// }
|
|
|
|
// //eventHorizon waves in and out
|
|
// if (this.cycle + 30 > this.endCycle) { //shrink fast in last bit of cycle
|
|
// this.eventHorizon = 0.93 * this.eventHorizon
|
|
// } else {
|
|
// this.eventHorizon = 0.97 * this.eventHorizon + 0.03 * (this.eventHorizonRadius * (1 - 0.5 * Math.cos(this.cycle * 0.015)))
|
|
// }
|
|
// //draw darkness
|
|
// ctx.beginPath();
|
|
// ctx.arc(this.position.x, this.position.y, this.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, this.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, this.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, this.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, this.eventHorizon, 0, 2 * Math.PI);
|
|
// ctx.fillStyle = "rgba(0,0,0,0.05)";
|
|
// ctx.fill();
|
|
// //when player is inside event horizon
|
|
// if (Vector.magnitude(Vector.sub(this.position, player.position)) < this.eventHorizon) {
|
|
// if (m.immuneCycle < m.cycle) {
|
|
// if (m.energy > 0) m.energy -= 0.02
|
|
// if (m.energy < 0.05 && m.immuneCycle < m.cycle) m.damage(0.0004 * simulation.dmgScale);
|
|
// }
|
|
// const angle = Math.atan2(player.position.y - this.position.y, player.position.x - this.position.x);
|
|
// player.force.x -= 0.0017 * Math.cos(angle) * player.mass * (m.onGround ? 1.7 : 1);
|
|
// player.force.y -= 0.0017 * Math.sin(angle) * player.mass;
|
|
// //draw line to player
|
|
// ctx.beginPath();
|
|
// ctx.moveTo(this.position.x, this.position.y);
|
|
// ctx.lineTo(m.pos.x, m.pos.y);
|
|
// ctx.lineWidth = Math.min(60, this.radius * 2);
|
|
// ctx.strokeStyle = "rgba(0,0,0,0.5)";
|
|
// ctx.stroke();
|
|
// ctx.beginPath();
|
|
// ctx.arc(m.pos.x, m.pos.y, 40, 0, 2 * Math.PI);
|
|
// ctx.fillStyle = "rgba(0,0,0,0.3)";
|
|
// ctx.fill();
|
|
// }
|
|
// this.curl(this.eventHorizon);
|
|
// }
|
|
// me.rotateVelocity = 0.0025
|
|
// me.rotateCount = 0;
|
|
// me.lasers = function(where, angle, dmg = 0.14 * simulation.dmgScale) {
|
|
// 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 = simulation.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 = simulation.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 = 7000;
|
|
// best = {
|
|
// x: null,
|
|
// y: null,
|
|
// dist2: Infinity,
|
|
// who: null,
|
|
// v1: null,
|
|
// v2: null
|
|
// };
|
|
// const look = {
|
|
// x: where.x + seeRange * Math.cos(angle),
|
|
// y: where.y + seeRange * Math.sin(angle)
|
|
// };
|
|
// // vertexCollision(where, look, mob);
|
|
// vertexCollision(where, look, map);
|
|
// vertexCollision(where, look, body);
|
|
// if (!m.isCloak) vertexCollision(where, look, [playerBody, playerHead]);
|
|
// if (best.who && (best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
|
|
// if (m.immuneCycle < m.cycle + 60 + m.collisionImmuneCycles) m.immuneCycle = m.cycle + 60 + m.collisionImmuneCycles; //player is immune to damage extra time
|
|
// m.damage(dmg);
|
|
// simulation.drawList.push({ //add dmg to draw queue
|
|
// x: best.x,
|
|
// y: best.y,
|
|
// radius: dmg * 1500,
|
|
// color: "rgba(80,0,255,0.5)",
|
|
// time: 20
|
|
// });
|
|
// }
|
|
// //draw beam
|
|
// if (best.dist2 === Infinity) best = look;
|
|
// ctx.moveTo(where.x, where.y);
|
|
// ctx.lineTo(best.x, best.y);
|
|
// }
|
|
// me.modeLasers = function() {
|
|
// if (!this.isStunned) {
|
|
// let slowed = false //check if slowed
|
|
// for (let i = 0; i < this.status.length; i++) {
|
|
// if (this.status[i].type === "slow") {
|
|
// slowed = true
|
|
// break
|
|
// }
|
|
// }
|
|
// if (!slowed) {
|
|
// this.rotateCount++
|
|
// Matter.Body.setAngle(this, this.rotateCount * this.rotateVelocity)
|
|
// Matter.Body.setAngularVelocity(this, 0)
|
|
// Matter
|
|
// }
|
|
// }
|
|
// if (this.cycle < 240) { //damage scales up over 2 seconds to give player time to move
|
|
// const scale = this.cycle / 240
|
|
// const dmg = (this.cycle < 120) ? 0 : 0.14 * simulation.dmgScale * scale
|
|
// ctx.beginPath();
|
|
// this.lasers(this.vertices[0], this.angle + Math.PI / 6, dmg);
|
|
// this.lasers(this.vertices[1], this.angle + 3 * Math.PI / 6, dmg);
|
|
// this.lasers(this.vertices[2], this.angle + 5 * Math.PI / 6, dmg);
|
|
// this.lasers(this.vertices[3], this.angle + 7 * Math.PI / 6, dmg);
|
|
// this.lasers(this.vertices[4], this.angle + 9 * Math.PI / 6, dmg);
|
|
// this.lasers(this.vertices[5], this.angle + 11 * Math.PI / 6, dmg);
|
|
// ctx.strokeStyle = "#50f";
|
|
// ctx.lineWidth = 1.5 * scale;
|
|
// ctx.setLineDash([70 + 300 * Math.random(), 55 * Math.random()]);
|
|
// ctx.stroke(); // Draw it
|
|
// ctx.setLineDash([]);
|
|
// ctx.lineWidth = 20;
|
|
// ctx.strokeStyle = `rgba(80,0,255,${0.07 * scale})`;
|
|
// ctx.stroke(); // Draw it
|
|
// } else {
|
|
// ctx.beginPath();
|
|
// this.lasers(this.vertices[0], this.angle + Math.PI / 6);
|
|
// this.lasers(this.vertices[1], this.angle + 3 * Math.PI / 6);
|
|
// this.lasers(this.vertices[2], this.angle + 5 * Math.PI / 6);
|
|
// this.lasers(this.vertices[3], this.angle + 7 * Math.PI / 6);
|
|
// this.lasers(this.vertices[4], this.angle + 9 * Math.PI / 6);
|
|
// this.lasers(this.vertices[5], this.angle + 11 * Math.PI / 6);
|
|
// ctx.strokeStyle = "#50f";
|
|
// ctx.lineWidth = 1.5;
|
|
// ctx.setLineDash([70 + 300 * Math.random(), 55 * Math.random()]);
|
|
// ctx.stroke(); // Draw it
|
|
// ctx.setLineDash([]);
|
|
// ctx.lineWidth = 20;
|
|
// ctx.strokeStyle = "rgba(80,0,255,0.07)";
|
|
// ctx.stroke(); // Draw it
|
|
// }
|
|
// }
|
|
// me.onDeath = function() {
|
|
// if (!this.hasRunDeathScript) {
|
|
// this.hasRunDeathScript = true
|
|
// //make a block body to replace this one
|
|
// //this body is too big to leave behind in the normal way mobs.replace()
|
|
// const len = body.length;
|
|
// const v = Matter.Vertices.hull(Matter.Vertices.clockwiseSort(this.vertices)) //might help with vertex collision issue, not sure
|
|
// body[len] = Matter.Bodies.fromVertices(this.position.x, this.position.y, v);
|
|
// Matter.Body.setVelocity(body[len], { x: 0, y: -3 });
|
|
// Matter.Body.setAngularVelocity(body[len], this.angularVelocity);
|
|
// body[len].collisionFilter.category = cat.body;
|
|
// body[len].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet;
|
|
// body[len].classType = "body";
|
|
// Composite.add(engine.world, body[len]); //add to world
|
|
// const expand = function(that, massLimit) {
|
|
// const scale = 1.05;
|
|
// Matter.Body.scale(that, scale, scale);
|
|
// if (that.mass < massLimit) setTimeout(expand, 20, that, massLimit);
|
|
// };
|
|
// expand(body[len], 200)
|
|
|
|
// function unlockExit() {
|
|
// if (simulation.isHorizontalFlipped) {
|
|
// level.exit.x = -5500 - 100;
|
|
// } else {
|
|
// level.exit.x = 5500;
|
|
// }
|
|
// level.exit.y = -330;
|
|
// Matter.Composite.remove(engine.world, map[map.length - 1]);
|
|
// map.splice(map.length - 1, 1);
|
|
// simulation.draw.setPaths(); //redraw map draw path
|
|
// // level.levels.push("null")
|
|
// }
|
|
|
|
// //add lore level as next level if player took lore tech earlier in the game
|
|
// if (lore.techCount > (lore.techGoal - 1) && !simulation.isCheating) {
|
|
// simulation.makeTextLog(`<span class="lore-text">undefined</span> <span class='color-symbol'>=</span> ${lore.techCount}/${lore.techGoal}`, 360);
|
|
// setTimeout(function() {
|
|
// simulation.makeTextLog(`level.levels.push("<span class='lore-text'>null</span>")`, 720);
|
|
// unlockExit()
|
|
// level.levels.push("null")
|
|
// }, 4000);
|
|
// //remove block map element so exit is clear
|
|
// } else { //reset game
|
|
// let count = 0
|
|
|
|
// function loop() {
|
|
// if (!simulation.paused && !simulation.onTitlePage) {
|
|
// count++
|
|
// if (count < 660) {
|
|
// if (count === 1) simulation.makeTextLog(`<em>//enter testing mode to set level.levels.length to <strong>Infinite</strong></em>`);
|
|
// if (!(count % 60)) simulation.makeTextLog(`simulation.analysis <span class='color-symbol'>=</span> ${((count / 60 - Math.random()) * 0.1).toFixed(3)}`);
|
|
// } else if (count === 660) {
|
|
// simulation.makeTextLog(`simulation.analysis <span class='color-symbol'>=</span> 1 <em>//analysis complete</em>`);
|
|
// } else if (count === 780) {
|
|
// simulation.makeTextLog(`<span class="lore-text">undefined</span> <span class='color-symbol'>=</span> ${lore.techCount}/${lore.techGoal}`)
|
|
// } else if (count === 1020) {
|
|
// simulation.makeTextLog(`Engine.clear(engine) <em>//simulation successful</em>`);
|
|
// } else if (count === 1260) {
|
|
// // tech.isImmortal = false;
|
|
// // m.death()
|
|
// // m.alive = false;
|
|
// // simulation.paused = true;
|
|
// // m.health = 0;
|
|
// // m.displayHealth();
|
|
// document.getElementById("health").style.display = "none"
|
|
// document.getElementById("health-bg").style.display = "none"
|
|
// document.getElementById("text-log").style.opacity = 0; //fade out any active text logs
|
|
// document.getElementById("fade-out").style.opacity = 1; //slowly fades out
|
|
// // build.shareURL(false)
|
|
// setTimeout(function() {
|
|
// if (!simulation.onTitlePage) {
|
|
// simulation.paused = true;
|
|
// // simulation.clearMap();
|
|
// // Matter.Composite.clear(composite, keepStatic, [deep = false])
|
|
// // Composite.clear(engine.composite);
|
|
// engine.world.bodies.forEach((body) => { Matter.Composite.remove(engine.world, body) })
|
|
// Engine.clear(engine);
|
|
// simulation.splashReturn();
|
|
// }
|
|
// }, 6000);
|
|
// return
|
|
// }
|
|
// }
|
|
// if (simulation.testing) {
|
|
// unlockExit()
|
|
// setTimeout(function() {
|
|
// simulation.makeTextLog(`level.levels.length <span class='color-symbol'>=</span> <strong>Infinite</strong>`);
|
|
// }, 1500);
|
|
// } else {
|
|
// if (!simulation.onTitlePage) requestAnimationFrame(loop);
|
|
// }
|
|
// }
|
|
// requestAnimationFrame(loop);
|
|
// }
|
|
// // for (let i = 0; i < 3; i++)
|
|
// level.difficultyIncrease(simulation.difficultyMode) //ramp up damage
|
|
// //remove power Ups, to avoid spamming console
|
|
// function removeAll(array) {
|
|
// for (let i = 0; i < array.length; ++i) Matter.Composite.remove(engine.world, array[i]);
|
|
// }
|
|
// removeAll(powerUp);
|
|
// powerUp = [];
|
|
|
|
// //pull in particles
|
|
// for (let i = 0, len = body.length; i < len; ++i) {
|
|
// const velocity = Vector.mult(Vector.normalise(Vector.sub(this.position, body[i].position)), 65)
|
|
// const pushUp = Vector.add(velocity, { x: 0, y: -0.5 })
|
|
// Matter.Body.setVelocity(body[i], Vector.add(body[i].velocity, pushUp));
|
|
// }
|
|
// //damage all mobs
|
|
// for (let j = 0; j < 8; j++) { //in case some mobs leave things after they die
|
|
// for (let i = 0, len = mob.length; i < len; ++i) {
|
|
// if (mob[i] !== this) {
|
|
// if (mob[i].isInvulnerable) { //disable invulnerability
|
|
// mob[i].isInvulnerable = false
|
|
// mob[i].damageReduction = 1
|
|
// }
|
|
// mob[i].damage(Infinity, true);
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// //draw stuff
|
|
// for (let i = 0, len = 22; i < len; i++) {
|
|
// simulation.drawList.push({ //add dmg to draw queue
|
|
// x: this.position.x,
|
|
// y: this.position.y,
|
|
// radius: (i + 1) * 150,
|
|
// color: `rgba(255,255,255,0.17)`,
|
|
// time: 5 * (len - i + 1)
|
|
// });
|
|
// }
|
|
// }
|
|
// };
|
|
// },
|
|
zombie(x, y, radius, sides, color) { //mob that attacks other mobs
|
|
mobs.spawn(x, y, sides, radius, color);
|
|
let me = mob[mob.length - 1];
|
|
me.damageReduction = 0 //take NO damage, but also slowly lose health
|
|
Matter.Body.setDensity(me, 0.0001) // normal density is 0.001 // this reduces life by half and decreases knockback
|
|
me.isZombie = true
|
|
me.isBadTarget = true;
|
|
me.isDropPowerUp = false;
|
|
me.showHealthBar = false;
|
|
me.stroke = "#83a"
|
|
me.accelMag = 0.003
|
|
me.frictionAir = 0.005
|
|
me.collisionFilter.mask = cat.player | cat.map | cat.body | cat.mob
|
|
me.seeAtDistance2 = 1000000 //1000 vision range
|
|
// me.onDeath = function() {
|
|
// const amount = Math.min(10, Math.ceil(this.mass * 0.5))
|
|
// if (tech.isSporeFlea) {
|
|
// const len = amount / 2
|
|
// for (let i = 0; i < len; i++) {
|
|
// const speed = 10 + 5 * Math.random()
|
|
// const angle = 2 * Math.PI * Math.random()
|
|
// b.flea(this.position, { x: speed * Math.cos(angle), y: speed * Math.sin(angle) })
|
|
// }
|
|
// } else if (tech.isSporeWorm) {
|
|
// const len = amount / 2
|
|
// for (let i = 0; i < len; i++) b.worm(this.position)
|
|
// } else {
|
|
// for (let i = 0; i < amount; i++) b.spore(this.position)
|
|
// }
|
|
// }
|
|
me.do = function () {
|
|
this.zombieHealthBar();
|
|
this.lookForMobTargets();
|
|
this.attack();
|
|
this.checkStatus();
|
|
};
|
|
me.mobSearchIndex = 0;
|
|
me.target = null
|
|
me.lookForMobTargets = function () {
|
|
if (this.target === null && mob.length > 1 && !(simulation.cycle % this.seePlayerFreq)) { //find mob targets
|
|
let closeDist = Infinity;
|
|
for (let i = 0, len = mob.length; i < len; ++i) {
|
|
if (
|
|
!mob[i].isZombie &&
|
|
!mob[i].isUnblockable &&
|
|
!mob[i].isMobBullet &&
|
|
Matter.Query.ray(map, this.position, mob[i].position).length === 0 &&
|
|
Matter.Query.ray(body, this.position, mob[i].position).length === 0
|
|
// !mob[i].isBadTarget &&
|
|
// !mob[i].isInvulnerable &&
|
|
// (Vector.magnitudeSquared(Vector.sub(this.position, mob[this.mobSearchIndex].position)) < this.seeAtDistance2)
|
|
) {
|
|
const DIST = Vector.magnitude(Vector.sub(this.position, mob[i].position));
|
|
if (DIST < closeDist) {
|
|
closeDist = DIST;
|
|
this.target = mob[i]
|
|
}
|
|
}
|
|
}
|
|
} else if (
|
|
!(simulation.cycle % this.memory) &&
|
|
this.target &&
|
|
(!this.target.alive || Matter.Query.ray(map, this.position, this.target.position).length !== 0)
|
|
) {
|
|
this.target = null //chance to forget target
|
|
}
|
|
}
|
|
me.zombieHealthBar = function () {
|
|
this.health -= 0.0003 //decay
|
|
if ((this.health < 0.01 || isNaN(this.health)) && this.alive) this.death();
|
|
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(136, 51, 170,0.7)";
|
|
ctx.fillRect(x, y, w * this.health, h);
|
|
}
|
|
me.hitCD = 0
|
|
me.attack = function () { //hit non zombie mobs
|
|
if (this.hitCD < simulation.cycle) {
|
|
if (this.target) {
|
|
this.force = Vector.mult(Vector.normalise(Vector.sub(this.target.position, this.position)), this.accelMag * this.mass)
|
|
} else { //wonder around
|
|
this.torque += 0.0000003 * this.inertia;
|
|
const mag = 0.0003 * this.mass
|
|
this.force.x += mag * Math.cos(this.angle)
|
|
this.force.y += mag * Math.sin(this.angle)
|
|
}
|
|
if (this.speed > 15) { // speed cap instead of friction to give more agility
|
|
Matter.Body.setVelocity(this, { x: this.velocity.x * 0.96, y: this.velocity.y * 0.96 });
|
|
} else if (this.speed < 10) {
|
|
Matter.Body.setVelocity(this, { x: this.velocity.x * 0.98, y: this.velocity.y * 0.98 });
|
|
} else if (this.speed < 8) {
|
|
Matter.Body.setVelocity(this, { x: this.velocity.x * 0.99, y: this.velocity.y * 0.99 });
|
|
}
|
|
const hit = (who) => {
|
|
if (!who.isZombie && who.damageReduction) {
|
|
this.hitCD = simulation.cycle + 15
|
|
//knock back away from recently hit mob
|
|
const force = Vector.mult(Vector.normalise(Vector.sub(who.position, this.position)), 0.03 * this.mass)
|
|
this.force.x -= force.x;
|
|
this.force.y -= force.y;
|
|
this.target = null //look for a new target
|
|
|
|
// who.damage(dmg);
|
|
//by pass normal damage method
|
|
const dmg = 1.5 * who.damageReduction / Math.sqrt(who.mass)
|
|
who.health -= dmg
|
|
who.onDamage(dmg); //custom damage effects
|
|
who.locatePlayer();
|
|
if ((who.health < 0.01 || isNaN(who.health)) && who.alive) who.death();
|
|
|
|
simulation.drawList.push({
|
|
x: this.position.x,
|
|
y: this.position.y,
|
|
radius: Math.log(dmg + 1.1) * 40 * who.damageReduction + 3,
|
|
color: simulation.playerDmgColor,
|
|
time: simulation.drawTime
|
|
});
|
|
}
|
|
}
|
|
const collide = Matter.Query.collides(this, mob) //damage mob targets if nearby
|
|
if (collide.length > 1) { //don't count self collide
|
|
for (let i = 0, len = collide.length; i < len; i++) {
|
|
hit(collide[i].bodyA)
|
|
hit(collide[i].bodyB)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
starter(x, y, radius = Math.floor(15 + 20 * Math.random())) { //easy mob for on level 1
|
|
mobs.spawn(x, y, 8, radius, "#9ccdc6");
|
|
let me = mob[mob.length - 1];
|
|
// console.log(`mass=${me.mass}, radius = ${radius}`)
|
|
me.accelMag = 0.0002
|
|
me.repulsionRange = 400000 + radius * radius; //squared
|
|
// me.memory = 120;
|
|
me.seeAtDistance2 = 2000000 //1400 vision range
|
|
Matter.Body.setDensity(me, 0.0005) // normal density is 0.001 // this reduces life by half and decreases knockback
|
|
me.do = function () {
|
|
this.seePlayerByLookingAt();
|
|
this.attraction();
|
|
this.repulsion();
|
|
this.checkStatus();
|
|
};
|
|
},
|
|
blockGroup(x, y, num = 3 + Math.random() * 8) {
|
|
for (let i = 0; i < num; i++) {
|
|
const radius = 25 + Math.floor(Math.random() * 20)
|
|
spawn.blockGroupMob(x + Math.random() * radius, y + Math.random() * radius, radius);
|
|
}
|
|
},
|
|
blockGroupMob(x, y, radius = 25 + Math.floor(Math.random() * 20)) {
|
|
mobs.spawn(x, y, 4, radius, "#999");
|
|
let me = mob[mob.length - 1];
|
|
me.g = 0.00015; //required if using this.gravity
|
|
me.accelMag = 0.0008 * simulation.accelScale;
|
|
me.groupingRangeMax = 250000 + Math.random() * 100000;
|
|
me.groupingRangeMin = (radius * 8) * (radius * 8);
|
|
me.groupingStrength = 0.0005
|
|
me.memory = 200;
|
|
me.isGrouper = true;
|
|
me.seeAtDistance2 = 600 * 600
|
|
me.seePlayerFreq = Math.floor(50 + 50 * Math.random())
|
|
me.do = function () {
|
|
this.gravity();
|
|
this.checkStatus();
|
|
this.seePlayerCheck();
|
|
if (this.seePlayer.recall) {
|
|
this.attraction();
|
|
//tether to other blocks
|
|
ctx.beginPath();
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (mob[i].isGrouper && mob[i] != this && mob[i].isDropPowerUp) { //don't tether to self, bullets, shields, ...
|
|
const distance2 = Vector.magnitudeSquared(Vector.sub(this.position, mob[i].position))
|
|
if (distance2 < this.groupingRangeMax) {
|
|
if (!mob[i].seePlayer.recall) mob[i].seePlayerCheck(); //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 = "#0ff";
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
},
|
|
blockBoss(x, y, radius = 60) {
|
|
const activeBeams = []; // used to draw beams when converting
|
|
const beamTotalDuration = 60
|
|
mobs.spawn(x, y, 4, radius, "#999"); //#54291d
|
|
const me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
Matter.Body.setDensity(me, 0.002); //normal density even though its a boss
|
|
me.damageReduction = 0.04 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1); //extra reduction for a boss, because normal density
|
|
me.frictionAir = 0.01;
|
|
me.accelMag = 0.0002;
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y);
|
|
for (const who of mob) {
|
|
if (who.isNecroMob) { //blockMobs leave their body, and die
|
|
who.leaveBody = true
|
|
who.damage(Infinity)
|
|
}
|
|
}
|
|
}
|
|
me.target = player; // the target to lock on. Usually a block, but will be the player under certain conditions
|
|
me.do = function () {
|
|
this.checkStatus();
|
|
this.seePlayerCheck();
|
|
if (this.target) { //(this.target === player && this.seePlayer.yes) || this.target !== player
|
|
const force = Vector.mult(Vector.normalise(Vector.sub(this.target.position, this.position)), this.accelMag * this.mass)
|
|
this.force.x += force.x;
|
|
this.force.y += force.y;
|
|
}
|
|
|
|
if (!(simulation.cycle % 30)) {
|
|
//find blocks to turn into mobs
|
|
for (let i = 0; i < body.length; i++) {
|
|
if (Vector.magnitude(Vector.sub(this.position, body[i].position)) < 700 && !body[i].isNotHoldable) { // check distance for each block
|
|
Matter.Composite.remove(engine.world, body[i]);
|
|
this.target = null //player;
|
|
spawn.blockMob(body[i].position.x, body[i].position.y, body[i], 0);
|
|
body.splice(i, 1);
|
|
activeBeams.push([beamTotalDuration, mob[mob.length - 1]]);
|
|
}
|
|
}
|
|
|
|
// generally, the boss will tend to stay in the player's area but focus on blocks.
|
|
if (this.distanceToPlayer() > 1500 && this.target === null) {
|
|
this.target = player; // too far, attract to the player
|
|
} else {
|
|
if (body.length) { // look for a new target by finding the closest block
|
|
let min = Infinity;
|
|
let closestBlock = null;
|
|
for (const block of body) {
|
|
const dist = Vector.magnitudeSquared(Vector.sub(this.position, block.position))
|
|
if (dist < min && Matter.Query.ray(map, this.position, block.position).length === 0) {
|
|
min = dist;
|
|
closestBlock = block;
|
|
}
|
|
}
|
|
this.target = closestBlock;
|
|
}
|
|
}
|
|
|
|
//randomly spawn new mobs from nothing
|
|
if (!(simulation.cycle % 90)) {
|
|
let count = 0
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (mob[i].isNecroMob) count++
|
|
}
|
|
if (count < 20 * Math.random() * Math.random()) { //limit number of spawns if there are already too many blockMobs
|
|
const unit = Vector.normalise(Vector.sub(player.position, this.position))
|
|
for (let i = 0, len = 3 * Math.random(); i < len; i++) {
|
|
this.damageReduction += 0.001; //0.05 is starting value
|
|
const scale = 0.99; //if 120 use 1.02
|
|
Matter.Body.scale(this, scale, scale);
|
|
this.radius *= scale;
|
|
|
|
const where = Vector.add(Vector.mult(unit, radius + 200 * Math.random()), this.position)
|
|
spawn.blockMob(where.x + 100 * (Math.random() - 0.5), where.y + 100 * (Math.random() - 0.5), null);
|
|
this.torque += 0.000035 * this.inertia; //spin after spawning
|
|
activeBeams.push([beamTotalDuration, mob[mob.length - 1]]);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
for (let i = 0; i < activeBeams.length; i++) { // draw beams on new mobs
|
|
const [duration, newBlockMob] = activeBeams[i];
|
|
if (duration === 0) {
|
|
activeBeams.splice(i, 1);
|
|
continue;
|
|
}
|
|
if (newBlockMob.alive) {
|
|
const vertexIndex = Math.floor((newBlockMob.vertices.length - 1) * duration / beamTotalDuration)
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.position.x, this.position.y);
|
|
ctx.lineTo(newBlockMob.vertices[vertexIndex].x, newBlockMob.vertices[vertexIndex].y);
|
|
|
|
//outline mob
|
|
ctx.moveTo(newBlockMob.vertices[0].x, newBlockMob.vertices[0].y);
|
|
for (let j = 1; j < newBlockMob.vertices.length; j++) {
|
|
ctx.lineTo(newBlockMob.vertices[j].x, newBlockMob.vertices[j].y);
|
|
}
|
|
ctx.lineTo(newBlockMob.vertices[0].x, newBlockMob.vertices[0].y);
|
|
|
|
ctx.strokeStyle = "#0ff";
|
|
ctx.lineWidth = 3;
|
|
ctx.stroke();
|
|
}
|
|
activeBeams[i][0]--; // shorten duration
|
|
}
|
|
}
|
|
},
|
|
blockMob(x, y, host, growCycles = 60) {
|
|
if (host === null) {
|
|
mobs.spawn(x, y, 4, 1.25 + 3.5 * Math.random(), "#999");
|
|
} else {
|
|
const sideLength = Vector.magnitude(Vector.sub(host.vertices[0], host.vertices[1])) + Vector.magnitude(Vector.sub(host.vertices[1], host.vertices[2])) / 2 //average of first 2 sides
|
|
mobs.spawn(x, y, 4, Math.min(70, sideLength), "#999");
|
|
if (host.bounds.max.x - host.bounds.min.x < 150 && host.bounds.max.y - host.bounds.min.y < 150) {
|
|
Matter.Body.setVertices(mob[mob.length - 1], host.vertices) //if not too big match vertices of host exactly
|
|
// mob[mob.length - 1].radius =
|
|
}
|
|
}
|
|
const me = mob[mob.length - 1];
|
|
me.damageReduction = 0.5; //only until done growing
|
|
me.isNecroMob = true
|
|
me.g = 0.00012; //required if using this.gravity
|
|
me.accelMag = 0.0003 * Math.sqrt(simulation.accelScale);
|
|
me.memory = 120;
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
// me.showHealthBar = false;
|
|
me.cycle = 0
|
|
me.do = function () { //grow phase only occurs for growCycles
|
|
this.checkStatus();
|
|
this.seePlayerCheck();
|
|
this.cycle++
|
|
if (this.cycle > growCycles) {
|
|
this.damageReduction = 1.8 //take extra damage
|
|
this.do = this.normalDo
|
|
} else {
|
|
const scale = 1.04; //if 120 use 1.02
|
|
Matter.Body.scale(this, scale, scale);
|
|
this.radius *= scale;
|
|
}
|
|
}
|
|
me.normalDo = function () {
|
|
this.gravity();
|
|
this.checkStatus();
|
|
this.seePlayerCheck();
|
|
this.attraction();
|
|
}
|
|
},
|
|
cellBossCulture(x, y, radius = 20, num = 5) {
|
|
const cellID = Math.random()
|
|
for (let i = 0; i < num; i++) {
|
|
spawn.cellBoss(x, y, radius, cellID)
|
|
}
|
|
},
|
|
cellBoss(x, y, radius = 20, cellID) {
|
|
mobs.spawn(x + Math.random(), y + Math.random(), 20, radius * (1 + 1.2 * Math.random()), "rgba(0,80,125,0.3)");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent"
|
|
me.isBoss = true;
|
|
me.isCell = true;
|
|
me.cellID = cellID
|
|
me.accelMag = 0.000165 * simulation.accelScale;
|
|
me.memory = Infinity;
|
|
me.leaveBody = false;
|
|
me.isVerticesChange = true
|
|
me.frictionAir = 0.012
|
|
me.seePlayerFreq = Math.floor(11 + 7 * Math.random())
|
|
me.seeAtDistance2 = 1400000;
|
|
me.cellMassMax = 70
|
|
me.collisionFilter.mask = cat.player | cat.bullet //| cat.body | cat.map
|
|
Matter.Body.setDensity(me, 0.0001 + 0.00002 * simulation.difficulty) // normal density is 0.001
|
|
me.damageReduction = 0.17 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1); //me.damageReductionGoal
|
|
|
|
const k = 642 //k=r^2/m
|
|
me.split = function () {
|
|
const scale = 0.45
|
|
Matter.Body.scale(this, scale, scale);
|
|
this.radius *= scale;
|
|
spawn.cellBoss(this.position.x, this.position.y, this.radius, this.cellID);
|
|
mob[mob.length - 1].health = this.health
|
|
}
|
|
me.onHit = function () { //run this function on hitting player
|
|
this.health = 1;
|
|
this.split();
|
|
};
|
|
me.onDamage = function (dmg) {
|
|
if (Math.random() < 0.34 * dmg * Math.sqrt(this.mass) && this.health > dmg) this.split();
|
|
}
|
|
me.do = function () {
|
|
this.seePlayerByDistOrLOS();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
|
|
if (this.seePlayer.recall && this.mass < this.cellMassMax) { //grow cell radius
|
|
const scale = 1 + 0.0002 * this.cellMassMax / this.mass;
|
|
Matter.Body.scale(this, scale, scale);
|
|
this.radius *= scale;
|
|
}
|
|
if (!(simulation.cycle % this.seePlayerFreq)) { //move away from other mobs
|
|
const repelRange = 150
|
|
const attractRange = 700
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (mob[i].isCell && mob[i].id !== this.id) {
|
|
const sub = Vector.sub(this.position, mob[i].position)
|
|
const dist = Vector.magnitude(sub)
|
|
if (dist < repelRange) {
|
|
this.force = Vector.mult(Vector.normalise(sub), this.mass * 0.002)
|
|
} else if (dist > attractRange) {
|
|
this.force = Vector.mult(Vector.normalise(sub), -this.mass * 0.003)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
me.onDeath = function () {
|
|
this.isCell = false;
|
|
let count = 0 //count other cells by id
|
|
// console.log(this.cellID)
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (mob[i].isCell && mob[i].cellID === this.cellID) count++
|
|
}
|
|
if (count < 1) { //only drop a power up if this is the last cell
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
} else {
|
|
this.isDropPowerUp = false;
|
|
}
|
|
}
|
|
},
|
|
spawnerBossCulture(x, y, radius = 50, num = 8 + Math.min(20, simulation.difficulty * 0.4)) {
|
|
tech.deathSpawnsFromBoss += 0.4
|
|
const spawnID = Math.random()
|
|
for (let i = 0; i < num; i++) spawn.spawnerBoss(x, y, radius, spawnID)
|
|
},
|
|
spawnerBoss(x, y, radius, spawnID) {
|
|
mobs.spawn(x + Math.random(), y + Math.random(), 4, radius, "rgba(255,60,0,0.3)") //);
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
|
|
me.isSpawnBoss = true;
|
|
me.spawnID = spawnID
|
|
me.accelMag = 0.00022 * simulation.accelScale;
|
|
me.memory = Infinity;
|
|
me.showHealthBar = false;
|
|
me.isVerticesChange = true
|
|
me.frictionAir = 0.011
|
|
me.seePlayerFreq = Math.floor(14 + 7 * Math.random())
|
|
me.seeAtDistance2 = 200000 //1400000;
|
|
me.stroke = "transparent"
|
|
me.collisionFilter.mask = cat.player | cat.bullet | cat.body | cat.map //"rgba(255,60,0,0.3)"
|
|
// Matter.Body.setDensity(me, 0.0014) // normal density is 0.001
|
|
Matter.Body.setAngularVelocity(me, 0.12 * (Math.random() - 0.5))
|
|
// spawn.shield(me, x, y, 1);
|
|
|
|
me.onHit = function () { //run this function on hitting player
|
|
this.explode();
|
|
};
|
|
me.damageReduction = 0.14 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1);
|
|
me.doAwake = function () {
|
|
this.alwaysSeePlayer();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
|
|
if (!(simulation.cycle % this.seePlayerFreq)) { //move away from other mobs
|
|
const repelRange = 40
|
|
const attractRange = 240
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (mob[i].isSpawnBoss && mob[i].id !== this.id) {
|
|
const sub = Vector.sub(this.position, mob[i].position)
|
|
const dist = Vector.magnitude(sub)
|
|
if (dist < repelRange) {
|
|
this.force = Vector.mult(Vector.normalise(sub), this.mass * 0.002)
|
|
} else if (dist > attractRange) {
|
|
this.force = Vector.mult(Vector.normalise(sub), -this.mass * 0.002)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
me.do = function () {
|
|
this.checkStatus();
|
|
if (this.seePlayer.recall) {
|
|
this.do = this.doAwake
|
|
//awaken other spawnBosses
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (mob[i].isSpawnBoss && mob[i].spawnID === this.spawnID) mob[i].seePlayer.recall = 1
|
|
}
|
|
}
|
|
};
|
|
me.onDeath = function () {
|
|
this.isSpawnBoss = false;
|
|
let count = 0 //count other cells by id
|
|
// console.log(this.spawnID)
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (mob[i].isSpawnBoss && mob[i].spawnID === this.spawnID) count++
|
|
}
|
|
if (count < 1) { //only drop a power up if this is the last cell
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
tech.deathSpawnsFromBoss -= 0.4
|
|
} else {
|
|
this.leaveBody = false;
|
|
this.isDropPowerUp = false;
|
|
}
|
|
|
|
const spawns = tech.deathSpawns + tech.deathSpawnsFromBoss
|
|
const len = Math.min(12, spawns * Math.ceil(Math.random() * simulation.difficulty * spawns))
|
|
for (let i = 0; i < len; i++) {
|
|
spawn.spawns(this.position.x + (Math.random() - 0.5) * radius * 2.5, this.position.y + (Math.random() - 0.5) * radius * 2.5);
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: this.velocity.x + (Math.random() - 0.5) * 10,
|
|
y: this.velocity.x + (Math.random() - 0.5) * 10
|
|
});
|
|
}
|
|
|
|
}
|
|
},
|
|
growBossCulture(x, y, radius = 17, nodes = 12 + Math.min(10, simulation.difficulty * 0.25)) {
|
|
const buffID = Math.random()
|
|
const sideLength = 200 + 50 * Math.sqrt(nodes) // distance between each node mob
|
|
for (let i = 0; i < nodes; ++i) {
|
|
const angle = 2 * Math.PI * Math.random()
|
|
const mag = Math.max(radius, sideLength * (1 - Math.pow(Math.random(), 1.5))) //working on a distribution that is circular, random, but not too focused in the center
|
|
spawn.growBoss(x + mag * Math.cos(angle), y + mag * Math.sin(angle), radius, buffID);
|
|
}
|
|
spawn.constrain2AdjacentMobs(nodes, 0.0001, false); //loop mobs together
|
|
},
|
|
growBoss(x, y, radius, buffID) {
|
|
mobs.spawn(x + Math.random(), y + Math.random(), 6, radius, "hsl(144, 15%, 50%)") //);
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
me.isBuffBoss = true;
|
|
me.buffID = buffID
|
|
me.memory = Infinity;
|
|
me.isVerticesChange = true
|
|
me.frictionAir = 0.012
|
|
me.seePlayerFreq = Math.floor(11 + 7 * Math.random())
|
|
me.seeAtDistance2 = 200000 //1400000;
|
|
me.stroke = "transparent"
|
|
me.collisionFilter.mask = cat.player | cat.bullet //| cat.body //| cat.map //"rgba(255,60,0,0.3)"
|
|
|
|
me.buffCount = 0
|
|
me.accelMag = 0.00005 //* simulation.accelScale;
|
|
me.setBuffed = function () {
|
|
this.buffCount++
|
|
this.accelMag += 0.000024 //* Math.sqrt(simulation.accelScale)
|
|
this.fill = `hsl(144, ${5 + 10 * this.buffCount}%, 50%)`
|
|
const scale = 1.135;
|
|
Matter.Body.scale(this, scale, scale);
|
|
this.radius *= scale;
|
|
|
|
// this.isInvulnerable = true
|
|
// if (this.damageReduction) this.startingDamageReduction = this.damageReduction
|
|
// this.damageReduction = 0
|
|
// this.invulnerabilityCountDown = simulation.difficulty
|
|
}
|
|
me.onDeath = function () {
|
|
this.isBuffBoss = false;
|
|
let count = 0 //count other cells by id
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (mob[i].isBuffBoss && mob[i].buffID === this.buffID) {
|
|
count++
|
|
mob[i].setBuffed()
|
|
}
|
|
}
|
|
if (count < 1) { //only drop a power up if this is the last cell
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
} else {
|
|
this.leaveBody = false;
|
|
this.isDropPowerUp = false;
|
|
powerUps.spawnRandomPowerUp(this.position.x, this.position.y) // manual power up spawn to avoid spawning too many tech with "symbiosis"
|
|
}
|
|
}
|
|
me.damageReduction = 0.2 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
//required setup for invulnerable
|
|
// me.isInvulnerable = false
|
|
me.invulnerabilityCountDown = 0
|
|
me.do = function () {
|
|
// if (this.isInvulnerable) {
|
|
// if (this.invulnerabilityCountDown > 0) {
|
|
// this.invulnerabilityCountDown--
|
|
// ctx.beginPath();
|
|
// let vertices = this.vertices;
|
|
// ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
// for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
// ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
// ctx.lineWidth = 20;
|
|
// ctx.strokeStyle = "rgba(255,255,255,0.7)";
|
|
// ctx.stroke();
|
|
// } else {
|
|
// this.isInvulnerable = false
|
|
// this.damageReduction = this.startingDamageReduction
|
|
// }
|
|
// }
|
|
this.alwaysSeePlayer();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
// if (!(simulation.cycle % this.seePlayerFreq)) { //move away from other mobs
|
|
// const repelRange = 100 + 4 * this.radius
|
|
// const attractRange = 240
|
|
// for (let i = 0, len = mob.length; i < len; i++) {
|
|
// if (mob[i].isBuffBoss && mob[i].id !== this.id) {
|
|
// const sub = Vector.sub(this.position, mob[i].position)
|
|
// const dist = Vector.magnitude(sub)
|
|
// if (dist < repelRange) {
|
|
// this.force = Vector.mult(Vector.normalise(sub), this.mass * 0.002)
|
|
// } else if (dist > attractRange) {
|
|
// this.force = Vector.mult(Vector.normalise(sub), -this.mass * 0.002)
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
}
|
|
},
|
|
powerUpBossBaby(x, y, vertices = 9, radius = 60) {
|
|
mobs.spawn(x, y, vertices, radius, "rgba(225,240,245,0.4)"); //"rgba(120,140,150,0.4)"
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
me.frictionAir = 0.006
|
|
me.seeAtDistance2 = 1000000;
|
|
me.accelMag = 0.0004 + 0.0003 * simulation.accelScale;
|
|
// Matter.Body.setDensity(me, 0.001); //normal is 0.001
|
|
me.collisionFilter.mask = cat.bullet | cat.player | cat.body | cat.map
|
|
me.memory = Infinity;
|
|
me.seePlayerFreq = 17
|
|
me.lockedOn = null;
|
|
if (vertices === 9) {
|
|
//on primary spawn
|
|
powerUps.spawnBossPowerUp(me.position.x, me.position.y)
|
|
powerUps.spawn(me.position.x, me.position.y, "heal");
|
|
powerUps.spawn(me.position.x, me.position.y, "ammo");
|
|
} else if (!m.isCloak) {
|
|
me.foundPlayer();
|
|
}
|
|
me.damageReduction = 0.2 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.isInvulnerable = true
|
|
me.startingDamageReduction = me.damageReduction
|
|
me.damageReduction = 0
|
|
me.invulnerabilityCountDown = 40 + simulation.difficulty
|
|
me.onHit = function () { //run this function on hitting player
|
|
if (powerUps.ejectTech()) {
|
|
powerUps.ejectGraphic("150, 138, 255");
|
|
// powerUps.spawn(m.pos.x + 60 * (Math.random() - 0.5), m.pos.y + 60 * (Math.random() - 0.5), "ammo");
|
|
// powerUps.spawn(m.pos.x + 60 * (Math.random() - 0.5), m.pos.y + 60 * (Math.random() - 0.5), "research");
|
|
this.accelMag *= 1.4
|
|
Matter.Body.setDensity(this, this.density * 1.4); //normal is 0.001
|
|
}
|
|
};
|
|
me.onDeath = function () {
|
|
this.leaveBody = false;
|
|
if (vertices > 3) {
|
|
this.isDropPowerUp = false;
|
|
spawn.powerUpBossBaby(this.position.x, this.position.y, vertices - 1)
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: this.velocity.x,
|
|
y: this.velocity.y
|
|
})
|
|
}
|
|
for (let i = 0; i < powerUp.length; i++) powerUp[i].collisionFilter.mask = cat.map | cat.powerUp
|
|
};
|
|
me.do = function () {
|
|
if (this.isInvulnerable) {
|
|
if (this.invulnerabilityCountDown > 0) {
|
|
this.invulnerabilityCountDown--
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 13 + 5 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
} else {
|
|
this.isInvulnerable = false
|
|
this.damageReduction = this.startingDamageReduction
|
|
}
|
|
}
|
|
// this.stroke = `hsl(0,0%,${80 + 25 * Math.sin(simulation.cycle * 0.01)}%)`
|
|
// this.fill = `hsla(0,0%,${80 + 25 * Math.sin(simulation.cycle * 0.01)}%,0.3)`
|
|
|
|
//steal all power ups
|
|
if (this.alive) {
|
|
for (let i = 0; i < Math.min(powerUp.length, this.vertices.length); i++) {
|
|
powerUp[i].collisionFilter.mask = 0
|
|
Matter.Body.setPosition(powerUp[i], this.vertices[i])
|
|
Matter.Body.setVelocity(powerUp[i], {
|
|
x: 0,
|
|
y: 0
|
|
})
|
|
}
|
|
}
|
|
this.seePlayerByHistory(50);
|
|
this.attraction();
|
|
this.checkStatus();
|
|
};
|
|
},
|
|
powerUpBoss(x, y, vertices = 9, radius = 130) {
|
|
mobs.spawn(x, y, vertices, radius, "transparent");
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
me.frictionAir = 0.01
|
|
me.seeAtDistance2 = 1000000;
|
|
me.accelMag = 0.0002 + 0.0004 * simulation.accelScale;
|
|
Matter.Body.setDensity(me, 0.00035); //normal is 0.001
|
|
me.collisionFilter.mask = cat.bullet | cat.player //| cat.body
|
|
me.memory = Infinity;
|
|
me.seePlayerFreq = 30
|
|
me.lockedOn = null;
|
|
if (vertices === 9) {
|
|
//on primary spawn
|
|
powerUps.spawnBossPowerUp(me.position.x, me.position.y)
|
|
powerUps.spawn(me.position.x, me.position.y, "heal");
|
|
powerUps.spawn(me.position.x, me.position.y, "ammo");
|
|
} else if (!m.isCloak) {
|
|
me.foundPlayer();
|
|
}
|
|
|
|
me.damageReduction = 0.16 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
// me.isInvulnerable = true
|
|
// me.startingDamageReduction = me.damageReduction
|
|
// me.damageReduction = 0
|
|
// me.invulnerabilityCountDown = 60 + simulation.difficulty * 2
|
|
|
|
me.onHit = function () { //run this function on hitting player
|
|
if (powerUps.ejectTech()) {
|
|
powerUps.ejectGraphic("150, 138, 255");
|
|
// powerUps.spawn(m.pos.x + 60 * (Math.random() - 0.5), m.pos.y + 60 * (Math.random() - 0.5), "ammo");
|
|
// powerUps.spawn(m.pos.x + 60 * (Math.random() - 0.5), m.pos.y + 60 * (Math.random() - 0.5), "research");
|
|
this.accelMag *= 1.4
|
|
Matter.Body.setDensity(this, this.density * 1.4); //normal is 0.001
|
|
}
|
|
};
|
|
me.onDeath = function () {
|
|
this.leaveBody = false;
|
|
if (vertices > 3) {
|
|
this.isDropPowerUp = false;
|
|
spawn.powerUpBoss(this.position.x, this.position.y, vertices - 1)
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: this.velocity.x,
|
|
y: this.velocity.y
|
|
})
|
|
}
|
|
for (let i = 0; i < powerUp.length; i++) powerUp[i].collisionFilter.mask = cat.map | cat.powerUp
|
|
};
|
|
|
|
//steal all power ups
|
|
// for (let i = 0; i < Math.min(powerUp.length, this.vertices.length); i++) {
|
|
// powerUp[i].collisionFilter.mask = 0
|
|
// Matter.Body.setPosition(powerUp[i], this.vertices[i])
|
|
// Matter.Body.setVelocity(powerUp[i], {
|
|
// x: 0,
|
|
// y: 0
|
|
// })
|
|
// }
|
|
// me.powerUpList = []
|
|
// me.constrainPowerUps = function() {
|
|
// for (let i = 0; i < Math.min(powerUp.length, this.vertices.length); i++) {
|
|
// //remove other constraints on power up
|
|
// for (let i = 0, len = cons.length; i < len; ++i) {
|
|
// if (cons[i].bodyB === powerUp[i] || cons[i].bodyA === powerUp[i]) {
|
|
// Matter.Composite.remove(engine.world, cons[i]);
|
|
// cons.splice(i, 1);
|
|
// break;
|
|
// }
|
|
// }
|
|
|
|
// //add to list
|
|
// this.powerUpList.push(powerUp[i])
|
|
// //position and stop
|
|
// powerUp[i].collisionFilter.mask = 0
|
|
// Matter.Body.setPosition(powerUp[i], this.vertices[i])
|
|
// Matter.Body.setVelocity(powerUp[i], { x: 0, y: 0 })
|
|
// //add constraint
|
|
// cons[cons.length] = Constraint.create({
|
|
// pointA: this.vertices[i],
|
|
// bodyB: powerUp[i],
|
|
// stiffness: 1,
|
|
// damping: 1
|
|
// });
|
|
// Composite.add(engine.world, cons[cons.length - 1]);
|
|
// }
|
|
// for (let i = 0; i < this.powerUpList.length; i++) {}
|
|
// }
|
|
// me.constrainPowerUps()
|
|
me.do = function () {
|
|
this.stroke = `hsl(0,0%,${80 + 25 * Math.sin(simulation.cycle * 0.01)}%)`
|
|
// if (this.isInvulnerable) {
|
|
// if (this.invulnerabilityCountDown > 0) {
|
|
// this.invulnerabilityCountDown--
|
|
// ctx.beginPath();
|
|
// let vertices = this.vertices;
|
|
// ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
// for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
// ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
// ctx.lineWidth = 20;
|
|
// ctx.strokeStyle = "rgba(255,255,255,0.7)";
|
|
// ctx.stroke();
|
|
// } else {
|
|
// this.isInvulnerable = false
|
|
// this.damageReduction = this.startingDamageReduction
|
|
// }
|
|
// }
|
|
//steal all power ups
|
|
// for (let i = 0; i < Math.min(powerUp.length, this.vertices.length); i++) {
|
|
// powerUp[i].collisionFilter.mask = 0
|
|
// Matter.Body.setPosition(powerUp[i], this.vertices[i])
|
|
// Matter.Body.setVelocity(powerUp[i], { x: 0, y: 0 })
|
|
// }
|
|
if (this.alive) {
|
|
for (let i = 0; i < Math.min(powerUp.length, this.vertices.length); i++) {
|
|
powerUp[i].collisionFilter.mask = 0
|
|
Matter.Body.setPosition(powerUp[i], this.vertices[i])
|
|
Matter.Body.setVelocity(powerUp[i], { x: 0, y: 0 })
|
|
}
|
|
}
|
|
|
|
this.seePlayerCheckByDistance();
|
|
this.attraction();
|
|
this.checkStatus();
|
|
};
|
|
},
|
|
|
|
// chaser(x, y, radius = 35 + Math.ceil(Math.random() * 40)) {
|
|
// mobs.spawn(x, y, 8, radius, "rgb(255,150,100)"); //"#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.1;
|
|
// me.frictionAir = 0;
|
|
// me.accelMag = 0.001 * Math.sqrt(simulation.accelScale);
|
|
// me.g = me.accelMag * 0.6; //required if using this.gravity
|
|
// me.memory = 180;
|
|
// spawn.shield(me, x, y);
|
|
// me.do = function() {
|
|
// this.gravity();
|
|
// this.seePlayerByHistory(15);
|
|
// this.checkStatus();
|
|
// this.attraction();
|
|
// };
|
|
// },
|
|
grower(x, y, radius = 15) {
|
|
mobs.spawn(x, y, 7, radius, "hsl(144, 15%, 50%)");
|
|
let me = mob[mob.length - 1];
|
|
me.isVerticesChange = true
|
|
me.big = false; //required for grow
|
|
me.accelMag = 0.00045 * simulation.accelScale;
|
|
me.collisionFilter.mask = cat.map | cat.body | cat.bullet | cat.player //can't touch other mobs
|
|
// me.onDeath = function () { //helps collisions functions work better after vertex have been changed
|
|
// this.vertices = Matter.Vertices.hull(Matter.Vertices.clockwiseSort(this.vertices))
|
|
// }
|
|
me.do = function () {
|
|
this.seePlayerByLookingAt();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
this.grow();
|
|
};
|
|
},
|
|
springer(x, y, radius = 20 + Math.ceil(Math.random() * 35)) {
|
|
mobs.spawn(x, y, 10, radius, "#b386e8");
|
|
let me = mob[mob.length - 1];
|
|
me.friction = 0;
|
|
me.frictionAir = 0.006;
|
|
me.lookTorque = 0.0000008; //controls spin while looking for player
|
|
me.g = 0.0002; //required if using this.gravity
|
|
me.seePlayerFreq = Math.floor((40 + 25 * Math.random()));
|
|
const springStiffness = 0.00014;
|
|
const springDampening = 0.0005;
|
|
|
|
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
|
|
});
|
|
Composite.add(engine.world, cons[cons.length - 1]);
|
|
|
|
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
|
|
});
|
|
Composite.add(engine.world, cons[cons.length - 1]);
|
|
cons[len2].length = 100 + 1.5 * radius;
|
|
me.cons2 = cons[len2];
|
|
me.do = function () {
|
|
this.gravity();
|
|
this.searchSpring();
|
|
this.checkStatus();
|
|
this.springAttack();
|
|
};
|
|
|
|
me.onDeath = function () {
|
|
this.removeCons();
|
|
};
|
|
spawn.shield(me, x, y);
|
|
},
|
|
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.05;
|
|
me.g = 0.0032; //required if using this.gravity
|
|
me.frictionAir = 0.01;
|
|
me.friction = 1
|
|
me.frictionStatic = 1
|
|
me.restitution = 0;
|
|
me.delay = 120 * simulation.CDScale;
|
|
me.randomHopFrequency = 200 + Math.floor(Math.random() * 150);
|
|
me.randomHopCD = simulation.cycle + me.randomHopFrequency;
|
|
Matter.Body.rotate(me, Math.random() * Math.PI);
|
|
spawn.shield(me, x, y);
|
|
me.do = function () {
|
|
this.gravity();
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
if (this.seePlayer.recall) {
|
|
if (this.cd < simulation.cycle && (Matter.Query.collides(this, map).length || Matter.Query.collides(this, body).length)) {
|
|
this.cd = simulation.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) - (Math.random() * 0.06 + 0.1) * this.mass; //antigravity
|
|
}
|
|
} else {
|
|
//randomly hob if not aware of player
|
|
if (this.randomHopCD < simulation.cycle && (Matter.Query.collides(this, map).length || Matter.Query.collides(this, body).length)) {
|
|
this.randomHopCD = simulation.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.07 * this.mass; //antigravity
|
|
}
|
|
}
|
|
};
|
|
},
|
|
hopBullet(x, y, radius = 10 + Math.ceil(Math.random() * 8)) {
|
|
mobs.spawn(x, y, 5, radius, "rgb(0,200,180)");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent";
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
// me.isBadTarget = true;
|
|
me.isMobBullet = true;
|
|
me.showHealthBar = false;
|
|
me.timeLeft = 1200 + Math.floor(600 * Math.random());
|
|
|
|
me.isRandomMove = Math.random() < 0.3 //most chase player, some don't
|
|
me.accelMag = 0.01; //jump height
|
|
me.g = 0.0015; //required if using this.gravity
|
|
me.frictionAir = 0.01;
|
|
me.friction = 1
|
|
me.frictionStatic = 1
|
|
me.restitution = 0;
|
|
me.delay = 130 + 60 * simulation.CDScale;
|
|
// Matter.Body.rotate(me, Math.random() * Math.PI);
|
|
me.collisionFilter.category = cat.mobBullet;
|
|
me.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet;
|
|
me.onHit = function () {
|
|
this.explode(this.mass);
|
|
};
|
|
me.do = function () {
|
|
this.gravity();
|
|
this.checkStatus();
|
|
if (this.cd < simulation.cycle && (Matter.Query.collides(this, map).length || Matter.Query.collides(this, body).length)) {
|
|
this.cd = simulation.cycle + this.delay;
|
|
if (this.isRandomMove || Math.random() < 0.2) {
|
|
this.force.x += (0.01 + 0.03 * Math.random()) * this.mass * (Math.random() < 0.5 ? 1 : -1); //random move
|
|
} else {
|
|
this.force.x += (0.01 + 0.03 * Math.random()) * this.mass * (player.position.x > this.position.x ? 1 : -1); //chase player
|
|
}
|
|
this.force.y -= (0.04 + 0.04 * Math.random()) * this.mass
|
|
}
|
|
this.timeLimit();
|
|
};
|
|
},
|
|
hopMomBoss(x, y, radius = 120) {
|
|
mobs.spawn(x, y, 5, radius, "rgb(0,200,180)");
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
me.damageReduction = 0.08 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.accelMag = 0.05; //jump height
|
|
me.g = 0.003; //required if using this.gravity
|
|
me.frictionAir = 0.01;
|
|
me.friction = 1
|
|
me.frictionStatic = 1
|
|
me.restitution = 0;
|
|
me.delay = 120 + 40 * simulation.CDScale;
|
|
Matter.Body.rotate(me, Math.random() * Math.PI);
|
|
spawn.shield(me, x, y, 1);
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
// for (let i = 0, len = 3 + 0.1 * simulation.difficulty; i < len; ++i) spawn.hopBullet(this.position.x + 100 * (Math.random() - 0.5), this.position.y + 100 * (Math.random() - 0.5))
|
|
};
|
|
me.do = function () {
|
|
this.gravity();
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
if (this.cd < simulation.cycle && (Matter.Query.collides(this, map).length || Matter.Query.collides(this, body).length)) {
|
|
this.cd = simulation.cycle + this.delay;
|
|
//spawn hopBullets after each jump
|
|
for (let i = 0, len = 1 + 0.05 * simulation.difficulty; i < len; ++i) spawn.hopBullet(this.position.x + 100 * (Math.random() - 0.5), this.position.y + 100 * (Math.random() - 0.5))
|
|
|
|
this.force.x += (0.02 + 0.06 * Math.random()) * this.mass * (player.position.x > this.position.x ? 1 : -1);
|
|
this.force.y -= (0.08 + 0.08 * Math.random()) * this.mass
|
|
}
|
|
};
|
|
},
|
|
// hopBoss(x, y, radius = 90) {
|
|
// mobs.spawn(x, y, 5, radius, "rgb(0,200,180)");
|
|
// let me = mob[mob.length - 1];
|
|
// me.isBoss = true;
|
|
// me.g = 0.005; //required if using this.gravity
|
|
// me.frictionAir = 0.01;
|
|
// me.friction = 1
|
|
// me.frictionStatic = 1
|
|
// me.restitution = 0;
|
|
// me.accelMag = 0.07;
|
|
// me.delay = 120 * simulation.CDScale;
|
|
// me.randomHopFrequency = 200
|
|
// me.randomHopCD = simulation.cycle + me.randomHopFrequency;
|
|
// // me.memory = 420;
|
|
// me.isInAir = false
|
|
// Matter.Body.setDensity(me, 0.03); //extra dense //normal is 0.001 //makes effective life much larger
|
|
// spawn.shield(me, x, y, 1);
|
|
// spawn.spawnOrbitals(me, radius + 60, 1)
|
|
// me.onDeath = function() {
|
|
// powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
// };
|
|
// me.lastSpeed = me.speed
|
|
// me.damageReduction = 0.25 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
// me.do = function() {
|
|
// // this.armor();
|
|
// this.gravity();
|
|
// this.seePlayerCheck();
|
|
// this.checkStatus();
|
|
// if (this.seePlayer.recall) {
|
|
// const deltaSpeed = this.lastSpeed - this.speed
|
|
// this.lastSpeed = this.speed
|
|
// if (deltaSpeed > 13 && this.speed < 5) { //if the player slows down greatly in one cycle
|
|
// //damage and push player away, push away blocks
|
|
// const range = 800 //Math.min(800, 50 * deltaSpeed)
|
|
// for (let i = body.length - 1; i > -1; i--) {
|
|
// if (!body[i].isNotHoldable) {
|
|
// sub = Vector.sub(body[i].position, this.position);
|
|
// dist = Vector.magnitude(sub);
|
|
// if (dist < range) {
|
|
// knock = Vector.mult(Vector.normalise(sub), Math.min(20, 50 * body[i].mass / dist));
|
|
// body[i].force.x += knock.x;
|
|
// body[i].force.y += knock.y;
|
|
// }
|
|
// }
|
|
// }
|
|
// simulation.drawList.push({ //draw radius
|
|
// x: this.position.x,
|
|
// y: this.position.y,
|
|
// radius: range,
|
|
// color: "rgba(0,200,180,0.6)",
|
|
// time: 4
|
|
// });
|
|
// }
|
|
// if (this.isInAir) {
|
|
// if (this.velocity.y > -0.01 && Matter.Query.collides(this, map).length || Matter.Query.collides(this, body).length) { //not moving up, and has hit the map or a body
|
|
// this.isInAir = false //landing
|
|
// this.cd = simulation.cycle + this.delay
|
|
// }
|
|
// } else { //on ground
|
|
// if (this.cd < simulation.cycle && (Matter.Query.collides(this, map).length || Matter.Query.collides(this, body).length)) { //jump
|
|
// this.isInAir = true
|
|
// 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) - (Math.random() * 0.05 + 0.04) * this.mass; //antigravity
|
|
// }
|
|
// }
|
|
// // if (this.cd < simulation.cycle && (Matter.Query.collides(this, map).length || Matter.Query.collides(this, body).length)) {
|
|
// // this.cd = simulation.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) - (Math.random() * 0.05 + 0.04) * this.mass; //antigravity
|
|
// // }
|
|
// } else {
|
|
// //randomly hob if not aware of player
|
|
// if (this.randomHopCD < simulation.cycle && (Matter.Query.collides(this, map).length || Matter.Query.collides(this, body).length)) {
|
|
// this.randomHopCD = simulation.cycle + this.randomHopFrequency;
|
|
// //slowly change randomHopFrequency after each hop
|
|
// this.randomHopFrequency = Math.max(100, this.randomHopFrequency + 200 * (0.5 - Math.random()));
|
|
// const forceMag = (this.accelMag + this.accelMag * Math.random()) * this.mass * (0.5 + Math.random() * 0.2);
|
|
// 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.1 + 0.08 * Math.random()) * 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.cd = 0;
|
|
me.burstDir = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
me.frictionAir = 0.022;
|
|
me.lookTorque = 0.0000014;
|
|
me.restitution = 0;
|
|
spawn.shield(me, x, y);
|
|
me.look = function () {
|
|
this.seePlayerByLookingAt();
|
|
this.checkStatus();
|
|
if (this.seePlayer.recall && this.cd < simulation.cycle) {
|
|
this.burstDir = Vector.normalise(Vector.sub(this.seePlayer.position, this.position));
|
|
this.cd = simulation.cycle + 40;
|
|
this.do = this.spin
|
|
}
|
|
}
|
|
me.do = me.look
|
|
me.spin = function () {
|
|
this.checkStatus();
|
|
this.torque += 0.000035 * this.inertia;
|
|
//draw attack vector
|
|
const mag = this.radius * 2.5 + 50;
|
|
ctx.strokeStyle = "rgba(0,0,0,0.2)";
|
|
ctx.lineWidth = 3;
|
|
ctx.setLineDash([10, 20]); //30
|
|
const dir = Vector.add(this.position, 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([]);
|
|
if (this.cd < simulation.cycle) {
|
|
this.fill = this.rememberFill;
|
|
this.cd = simulation.cycle + 180 * simulation.CDScale
|
|
this.do = this.look
|
|
this.force = Vector.mult(this.burstDir, this.mass * 0.25);
|
|
}
|
|
}
|
|
},
|
|
sucker(x, y, radius = 30 + Math.ceil(Math.random() * 25)) {
|
|
radius = 9 + radius / 8; //extra small
|
|
mobs.spawn(x, y, 6, radius, "transparent");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent"; //used for drawSneaker
|
|
me.eventHorizon = radius * 30; //required for blackhole
|
|
me.seeAtDistance2 = (me.eventHorizon + 400) * (me.eventHorizon + 400); //vision limit is event horizon
|
|
me.accelMag = 0.00012 * simulation.accelScale;
|
|
me.frictionAir = 0.025;
|
|
me.collisionFilter.mask = cat.player | cat.bullet //| cat.body
|
|
me.memory = Infinity;
|
|
Matter.Body.setDensity(me, 0.015); //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.seePlayerCheckByDistance()
|
|
this.checkStatus();
|
|
//accelerate towards the player
|
|
if (this.seePlayer.recall) {
|
|
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
|
|
const eventHorizon = this.eventHorizon * (0.93 + 0.17 * Math.sin(simulation.cycle * 0.011))
|
|
//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();
|
|
|
|
//when player is inside event horizon
|
|
if (Vector.magnitude(Vector.sub(this.position, player.position)) < eventHorizon) {
|
|
if (m.immuneCycle < m.cycle) {
|
|
if (m.energy > 0) m.energy -= 0.005
|
|
if (m.energy < 0.1) m.damage(0.0001 * simulation.dmgScale);
|
|
}
|
|
const angle = Math.atan2(player.position.y - this.position.y, player.position.x - this.position.x);
|
|
player.force.x -= 0.00125 * player.mass * Math.cos(angle) * (m.onGround ? 1.8 : 1);
|
|
player.force.y -= 0.0001 * player.mass * Math.sin(angle);
|
|
//draw line to player
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.position.x, this.position.y);
|
|
ctx.lineTo(m.pos.x, m.pos.y);
|
|
ctx.lineWidth = Math.min(60, this.radius * 2);
|
|
ctx.strokeStyle = "rgba(0,0,0,0.5)";
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.arc(m.pos.x, m.pos.y, 40, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "rgba(0,0,0,0.3)";
|
|
ctx.fill();
|
|
}
|
|
}
|
|
},
|
|
// timeBoss(x, y, radius = 25) {
|
|
// mobs.spawn(x, y, 12, radius, "#000");
|
|
// let me = mob[mob.length - 1];
|
|
// me.isBoss = true;
|
|
// me.stroke = "transparent"; //used for drawSneaker
|
|
// me.eventHorizon = 1100; //required for black hole
|
|
// me.eventHorizonGoal = 1100; //required for black hole
|
|
// me.seeAtDistance2 = (me.eventHorizon + 1200) * (me.eventHorizon + 1200); //vision limit is event horizon
|
|
// me.accelMag = 0.00006 * simulation.accelScale;
|
|
// // me.collisionFilter.mask = cat.player | cat.bullet //| cat.body
|
|
// // me.frictionAir = 0.005;
|
|
// me.memory = Infinity;
|
|
// Matter.Body.setDensity(me, 0.03); //extra dense //normal is 0.001 //makes effective life much larger
|
|
// me.onDeath = function() {
|
|
// powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
// };
|
|
// me.damageReduction = 0.23 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
// me.do = function() {
|
|
// //keep it slow, to stop issues from explosion knock backs
|
|
// if (!(simulation.cycle % this.seePlayerFreq)) {
|
|
// if (this.distanceToPlayer2() < this.seeAtDistance2) { //&& !m.isCloak ignore cloak for black holes
|
|
// this.locatePlayer();
|
|
// if (!this.seePlayer.yes) this.seePlayer.yes = true;
|
|
// } else if (this.seePlayer.recall) {
|
|
// this.lostPlayer();
|
|
// }
|
|
// }
|
|
// this.checkStatus();
|
|
// if (this.seePlayer.recall) {
|
|
// //accelerate towards the player
|
|
// const forceMag = this.accelMag * this.mass;
|
|
// const dx = this.seePlayer.position.x - this.position.x
|
|
// const dy = this.seePlayer.position.y - this.position.y
|
|
// const mag = Math.sqrt(dx * dx + dy * dy)
|
|
// this.force.x += forceMag * dx / mag;
|
|
// this.force.y += forceMag * dy / mag;
|
|
|
|
// //eventHorizon waves in and out
|
|
// const eventHorizon = this.eventHorizon * (1 + 0.2 * Math.sin(simulation.cycle * 0.008))
|
|
// // zoom camera in and out with the event horizon
|
|
|
|
// //draw darkness
|
|
// if (!simulation.isTimeSkipping) {
|
|
// 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 (Vector.magnitude(Vector.sub(this.position, player.position)) < eventHorizon) {
|
|
// // if (!(simulation.cycle % 10)) simulation.timePlayerSkip(5)
|
|
// // if (!(simulation.cycle % 2)) simulation.timePlayerSkip(1)
|
|
// simulation.timePlayerSkip(2)
|
|
|
|
// // if (m.immuneCycle < m.cycle) {
|
|
// // if (m.energy > 0) m.energy -= 0.004
|
|
// // if (m.energy < 0.1) m.damage(0.00017 * simulation.dmgScale);
|
|
// // }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// },
|
|
suckerBoss(x, y, radius = 25) {
|
|
mobs.spawn(x, y, 12, radius, "#000");
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
|
|
me.stroke = "transparent"; //used for drawSneaker
|
|
me.eventHorizon = 1100; //required for black hole
|
|
me.seeAtDistance2 = (me.eventHorizon + 3000) * (me.eventHorizon + 3000); //vision limit is event horizon
|
|
me.accelMag = 0.00004 * simulation.accelScale;
|
|
me.collisionFilter.mask = cat.player | cat.bullet //| cat.body
|
|
// me.frictionAir = 0.005;
|
|
me.memory = 1600;
|
|
Matter.Body.setDensity(me, 0.06); //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 (simulation.difficulty > 5) {
|
|
//teleport everything to center
|
|
function toMe(who, where, range) {
|
|
for (let i = 0, len = who.length; i < len; i++) {
|
|
if (!who[i].isNotHoldable) {
|
|
const SUB = Vector.sub(who[i].position, where)
|
|
const DISTANCE = Vector.magnitude(SUB)
|
|
if (DISTANCE < range) {
|
|
Matter.Body.setPosition(who[i], where)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
toMe(body, this.position, this.eventHorizon)
|
|
toMe(mob, this.position, this.eventHorizon)
|
|
// toMe(bullet, this.position, this.eventHorizon))
|
|
}
|
|
};
|
|
me.damageReduction = 0.25 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
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
|
|
});
|
|
}
|
|
if (!(simulation.cycle % this.seePlayerFreq)) {
|
|
if (this.distanceToPlayer2() < this.seeAtDistance2) { //&& !m.isCloak ignore cloak for black holes
|
|
this.locatePlayer();
|
|
if (!this.seePlayer.yes) this.seePlayer.yes = true;
|
|
} else if (this.seePlayer.recall) {
|
|
this.lostPlayer();
|
|
}
|
|
}
|
|
this.checkStatus();
|
|
if (this.seePlayer.recall) {
|
|
//throw large seekers
|
|
if (!(simulation.cycle % 90)) {
|
|
spawn.seeker(this.position.x, this.position.y, 15 * (0.7 + 0.5 * Math.random()), 7); //give the bullet a rotational velocity as if they were attached to a vertex
|
|
const who = mob[mob.length - 1]
|
|
Matter.Body.setDensity(who, 0.00001); //normal is 0.001
|
|
who.timeLeft = 600
|
|
who.accelMag = 0.0002 * simulation.accelScale; //* (0.8 + 0.4 * Math.random())
|
|
who.frictionAir = 0.01 //* (0.
|
|
const velocity = Vector.mult(Vector.normalise(Vector.sub(m.pos, this.position)), -20); //set direction to turn to fire //Vector.mult(Vector.perp(Vector.normalise(Vector.sub(this.position, this.vertices[index]))), -35)
|
|
Matter.Body.setVelocity(who, {
|
|
x: this.velocity.x + velocity.x,
|
|
y: this.velocity.y + velocity.y
|
|
});
|
|
}
|
|
//accelerate towards the player
|
|
const forceMag = this.accelMag * this.mass;
|
|
const dx = this.seePlayer.position.x - this.position.x
|
|
const dy = this.seePlayer.position.y - this.position.y
|
|
const mag = Math.sqrt(dx * dx + dy * dy)
|
|
this.force.x += forceMag * dx / mag;
|
|
this.force.y += forceMag * dy / mag;
|
|
//eventHorizon waves in and out
|
|
const eventHorizon = this.eventHorizon * (1 + 0.2 * Math.sin(simulation.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 (Vector.magnitude(Vector.sub(this.position, player.position)) < eventHorizon) {
|
|
if (m.immuneCycle < m.cycle) {
|
|
if (m.energy > 0) m.energy -= 0.008
|
|
if (m.energy < 0.1) m.damage(0.00015 * simulation.dmgScale);
|
|
}
|
|
const angle = Math.atan2(player.position.y - this.position.y, player.position.x - this.position.x);
|
|
player.force.x -= 0.0013 * Math.cos(angle) * player.mass * (m.onGround ? 1.7 : 1);
|
|
player.force.y -= 0.0013 * Math.sin(angle) * player.mass;
|
|
//draw line to player
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.position.x, this.position.y);
|
|
ctx.lineTo(m.pos.x, m.pos.y);
|
|
ctx.lineWidth = Math.min(60, this.radius * 2);
|
|
ctx.strokeStyle = "rgba(0,0,0,0.5)";
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.arc(m.pos.x, m.pos.y, 40, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "rgba(0,0,0,0.3)";
|
|
ctx.fill();
|
|
}
|
|
this.curl(eventHorizon);
|
|
//attract other power ups
|
|
for (let i = 0; i < powerUp.length; i++) { //attract heal power ups
|
|
const sub = Vector.sub(this.position, powerUp[i].position)
|
|
const mag = 0.0015 * Math.min(1, (Vector.magnitude(sub) - 200) / this.eventHorizon)
|
|
const attract = Vector.mult(Vector.normalise(sub), mag * powerUp[i].mass)
|
|
powerUp[i].force.x += attract.x;
|
|
powerUp[i].force.y += attract.y - powerUp[i].mass * simulation.g; //negate gravity
|
|
// Matter.Body.setVelocity(powerUp[i], Vector.mult(powerUp[i].velocity, 0.7));
|
|
}
|
|
}
|
|
}
|
|
},
|
|
spiderBoss(x, y, radius = 60 + Math.ceil(Math.random() * 10)) {
|
|
let targets = [] //track who is in the node boss, for shields
|
|
mobs.spawn(x, y, 6, radius, "#b386e8");
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.setDensity(me, 0.003); //extra dense //normal is 0.001 //makes effective life much larger and damage on collision
|
|
me.isBoss = true;
|
|
me.damageReduction = 0.13 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1) //normal is 1, most bosses have 0.25
|
|
|
|
targets.push(me.id) //add to shield protection
|
|
me.friction = 0;
|
|
me.frictionAir = 0.0067;
|
|
me.lookTorque = 0.0000008; //controls spin while looking for player
|
|
me.g = 0.0002; //required if using this.gravity
|
|
me.seePlayerFreq = Math.floor((30 + 20 * Math.random()));
|
|
const springStiffness = 0.00014;
|
|
const springDampening = 0.0005;
|
|
|
|
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
|
|
});
|
|
Composite.add(engine.world, cons[cons.length - 1]);
|
|
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,
|
|
length: 0
|
|
});
|
|
Composite.add(engine.world, cons[cons.length - 1]);
|
|
cons[len2].length = 100 + 1.5 * radius;
|
|
me.cons2 = cons[len2];
|
|
me.do = function () {
|
|
// this.armor();
|
|
this.gravity();
|
|
this.searchSpring();
|
|
this.checkStatus();
|
|
this.springAttack();
|
|
};
|
|
|
|
me.onDeath = function () {
|
|
this.removeCons();
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
};
|
|
|
|
radius = 22 // radius of each node mob
|
|
const sideLength = 100 // distance between each node mob
|
|
const nodes = 6
|
|
const angle = 2 * Math.PI / nodes
|
|
|
|
spawn.allowShields = false; //don't want shields on individual mobs
|
|
|
|
for (let i = 0; i < nodes; ++i) {
|
|
spawn.stabber(x + sideLength * Math.sin(i * angle), y + sideLength * Math.cos(i * angle), radius, 12);
|
|
Matter.Body.setDensity(mob[mob.length - 1], 0.003); //extra dense //normal is 0.001 //makes effective life much larger
|
|
mob[mob.length - 1].damageReduction = 0.12
|
|
targets.push(mob[mob.length - 1].id) //track who is in the node boss, for shields
|
|
}
|
|
|
|
const attachmentStiffness = 0.02
|
|
spawn.constrain2AdjacentMobs(nodes, attachmentStiffness, true); //loop mobs together
|
|
|
|
for (let i = 0; i < nodes; ++i) { //attach to center mob
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: me,
|
|
bodyB: mob[mob.length - i - 1],
|
|
stiffness: attachmentStiffness,
|
|
damping: 0.03
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
}
|
|
//spawn shield around all nodes
|
|
spawn.groupShield(targets, x, y, sideLength + 1 * radius + nodes * 5 - 25);
|
|
spawn.allowShields = true;
|
|
},
|
|
mantisBoss(x, y, radius = 35, isSpawnBossPowerUp = true) {
|
|
mobs.spawn(x, y, 5, radius, "#6ba");
|
|
let me = mob[mob.length - 1];
|
|
me.babyList = [] //list of mobs that are apart of this boss
|
|
Matter.Body.setDensity(me, 0.0015); //extra dense //normal is 0.001 //makes effective life much larger and damage on collision
|
|
me.damageReduction = 0.13 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1) //normal is 1, most bosses have 0.25
|
|
me.isBoss = true;
|
|
|
|
me.friction = 0;
|
|
me.frictionAir = 0.006;
|
|
me.g = 0.0002; //required if using this.gravity
|
|
me.seePlayerFreq = 31;
|
|
const springStiffness = 0.00003; //simulation.difficulty
|
|
const springDampening = 0.0002;
|
|
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
|
|
});
|
|
Composite.add(engine.world, cons[cons.length - 1]);
|
|
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,
|
|
length: 0
|
|
});
|
|
Composite.add(engine.world, cons[cons.length - 1]);
|
|
cons[len2].length = 100 + 1.5 * radius;
|
|
me.cons2 = cons[len2];
|
|
me.startingDamageReduction = me.damageReduction
|
|
me.isInvulnerable = false
|
|
me.invulnerabilityCountDown = 0
|
|
me.do = function () {
|
|
this.checkStatus();
|
|
this.gravity();
|
|
//draw the two dots on the end of the springs
|
|
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.fillStyle = "#222";
|
|
ctx.fill();
|
|
// this.seePlayerCheck()
|
|
this.seePlayerByHistory()
|
|
this.invulnerabilityCountDown--
|
|
if (this.isInvulnerable) {
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
for (let i = 0; i < this.babyList.length; i++) {
|
|
if (this.babyList[i].alive) {
|
|
let vertices = this.babyList[i].vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
}
|
|
}
|
|
ctx.lineWidth = 13 + 5 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
if (this.invulnerabilityCountDown < 0) {
|
|
this.invulnerabilityCountDown = 110
|
|
this.isInvulnerable = false
|
|
this.damageReduction = this.startingDamageReduction
|
|
for (let i = 0; i < this.babyList.length; i++) {
|
|
if (this.babyList[i].alive) this.babyList[i].damageReduction = this.startingDamageReduction
|
|
}
|
|
}
|
|
} else if (this.invulnerabilityCountDown < 0) {
|
|
this.invulnerabilityCountDown = 120 + 9 * simulation.difficulty
|
|
this.isInvulnerable = true
|
|
if (this.damageReduction) this.startingDamageReduction = this.damageReduction
|
|
this.damageReduction = 0
|
|
for (let i = 0; i < this.babyList.length; i++) {
|
|
if (this.babyList[i].alive) {
|
|
this.babyList[i].isInvulnerable = true
|
|
this.babyList[i].damageReduction = 0
|
|
}
|
|
}
|
|
}
|
|
// set new values of the ends of the spring constraints
|
|
const stepRange = 700
|
|
if (this.seePlayer.recall && Matter.Query.ray(map, this.position, this.seePlayer.position).length === 0) {
|
|
if (!(simulation.cycle % (this.seePlayerFreq * 2))) {
|
|
const unit = Vector.normalise(Vector.sub(this.seePlayer.position, this.position))
|
|
const goal = Vector.add(this.position, Vector.mult(unit, stepRange))
|
|
this.springTarget.x = goal.x;
|
|
this.springTarget.y = goal.y;
|
|
this.cons.length = -200;
|
|
this.cons2.length = 100 + 1.5 * this.radius;
|
|
} else if (!(simulation.cycle % this.seePlayerFreq)) {
|
|
const unit = Vector.normalise(Vector.sub(this.seePlayer.position, this.position))
|
|
const goal = Vector.add(this.position, Vector.mult(unit, stepRange))
|
|
this.springTarget2.x = goal.x;
|
|
this.springTarget2.y = goal.y;
|
|
this.cons.length = 100 + 1.5 * this.radius;
|
|
this.cons2.length = -200;
|
|
}
|
|
} else {
|
|
this.torque = this.lookTorque * this.inertia;
|
|
//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 = simulation.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 = simulation.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]
|
|
};
|
|
}
|
|
}
|
|
}
|
|
};
|
|
//move to a random location
|
|
if (!(simulation.cycle % (this.seePlayerFreq))) {
|
|
best = {
|
|
x: null,
|
|
y: null,
|
|
dist2: Infinity,
|
|
who: null,
|
|
v1: null,
|
|
v2: null
|
|
};
|
|
const seeRange = 3000;
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
me.onDeath = function () {
|
|
this.removeCons();
|
|
if (isSpawnBossPowerUp) powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
for (let i = 0; i < this.babyList.length; i++) {
|
|
if (this.babyList[i].alive) {
|
|
this.babyList[i].collisionFilter.mask = cat.map | cat.bullet | cat.player
|
|
this.babyList[i].isInvulnerable = false
|
|
this.babyList[i].damageReduction = this.startingDamageReduction
|
|
this.babyList[i].collisionFilter.mask = cat.bullet | cat.player | cat.map | cat.body
|
|
}
|
|
}
|
|
};
|
|
const sideLength = 80 // distance between each node mob
|
|
const nodes = 3
|
|
const angle = 2 * Math.PI / nodes
|
|
spawn.allowShields = false; //don't want shields on individual mobs, it messes with the constraints
|
|
for (let i = 0; i < nodes; ++i) {
|
|
spawn.striker(x + sideLength * Math.sin(i * angle), y + sideLength * Math.cos(i * angle), 20, 12);
|
|
const babyMob = mob[mob.length - 1]
|
|
me.babyList.push(babyMob)
|
|
babyMob.fill = "rgb(68, 102, 119)"
|
|
babyMob.isBoss = true;
|
|
// Matter.Body.setDensity(babyMob, 0.001); //extra dense //normal is 0.001 //makes effective life much larger and increases damage
|
|
babyMob.damageReduction = this.startingDamageReduction * 0.8
|
|
babyMob.collisionFilter.mask = cat.bullet | cat.player //can't touch other mobs //cat.map | cat.body |
|
|
babyMob.delay = 60 + 55 * simulation.CDScale + Math.floor(Math.random() * 20);
|
|
babyMob.strikeRange = 400
|
|
babyMob.onHit = function () {
|
|
this.cd = simulation.cycle + this.delay;
|
|
//dislodge ammo
|
|
if (b.inventory.length) {
|
|
let isRemovedAmmo = false
|
|
const numRemoved = 3
|
|
for (let j = 0; j < numRemoved; j++) {
|
|
for (let i = 0; i < b.inventory.length; i++) {
|
|
const gun = b.guns[b.inventory[i]]
|
|
if (gun.ammo > 0 && gun.ammo !== Infinity) {
|
|
gun.ammo -= Math.ceil((0.45 * Math.random() + 0.45 * Math.random()) * gun.ammoPack) //Math.ceil(Math.random() * target.ammoPack)
|
|
if (gun.ammo < 0) gun.ammo = 0
|
|
isRemovedAmmo = true
|
|
}
|
|
}
|
|
}
|
|
if (isRemovedAmmo) {
|
|
simulation.updateGunHUD();
|
|
for (let j = 0; j < numRemoved; j++) powerUps.directSpawn(this.position.x + 10 * Math.random(), this.position.y + 10 * Math.random(), "ammo");
|
|
powerUps.ejectGraphic();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
const stiffness = 0.01
|
|
const damping = 0.1
|
|
for (let i = 1; i < nodes; ++i) { //attach to center mob
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: mob[mob.length - i],
|
|
bodyB: mob[mob.length - i - 1],
|
|
stiffness: stiffness,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
}
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: mob[mob.length - 1],
|
|
bodyB: mob[mob.length - nodes],
|
|
stiffness: stiffness,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
|
|
|
|
for (let i = 0; i < nodes; ++i) { //attach to center mob
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: me,
|
|
bodyB: mob[mob.length - i - 1],
|
|
stiffness: stiffness,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
}
|
|
spawn.allowShields = true;
|
|
},
|
|
// timeSkipBoss(x, y, radius = 55) {
|
|
// mobs.spawn(x, y, 6, radius, '#000');
|
|
// let me = mob[mob.length - 1];
|
|
// me.isBoss = true;
|
|
// // me.stroke = "transparent"; //used for drawSneaker
|
|
// me.timeSkipLastCycle = 0
|
|
// me.eventHorizon = 1800; //required for black hole
|
|
// me.seeAtDistance2 = (me.eventHorizon + 2000) * (me.eventHorizon + 2000); //vision limit is event horizon + 2000
|
|
// me.accelMag = 0.0004 * simulation.accelScale;
|
|
// // me.frictionAir = 0.005;
|
|
// // me.memory = 1600;
|
|
// // Matter.Body.setDensity(me, 0.02); //extra dense //normal is 0.001 //makes effective life much larger
|
|
// Matter.Body.setDensity(me, 0.0005 + 0.00018 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
// spawn.shield(me, x, y, 1);
|
|
// 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)
|
|
// };
|
|
// me.do = function() {
|
|
// //keep it slow, to stop issues from explosion knock backs
|
|
// if (this.speed > 8) {
|
|
// Matter.Body.setVelocity(this, {
|
|
// x: this.velocity.x * 0.99,
|
|
// y: this.velocity.y * 0.99
|
|
// });
|
|
// }
|
|
// this.seePlayerCheck();
|
|
// this.checkStatus();
|
|
// this.attraction()
|
|
// if (!simulation.isTimeSkipping) {
|
|
// const compress = 1
|
|
// if (this.timeSkipLastCycle < simulation.cycle - compress &&
|
|
// Vector.magnitude(Vector.sub(this.position, player.position)) < this.eventHorizon) {
|
|
// this.timeSkipLastCycle = simulation.cycle
|
|
// simulation.timeSkip(compress)
|
|
|
|
// this.fill = `rgba(0,0,0,${0.4+0.6*Math.random()})`
|
|
// this.stroke = "#014"
|
|
// this.isShielded = false;
|
|
// this.isDropPowerUp = true;
|
|
// this.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob; //can't touch bullets
|
|
|
|
// ctx.beginPath();
|
|
// ctx.arc(this.position.x, this.position.y, this.eventHorizon, 0, 2 * Math.PI);
|
|
// ctx.fillStyle = "#fff";
|
|
// ctx.globalCompositeOperation = "destination-in"; //in or atop
|
|
// ctx.fill();
|
|
// ctx.globalCompositeOperation = "source-over";
|
|
// ctx.beginPath();
|
|
// ctx.arc(this.position.x, this.position.y, this.eventHorizon, 0, 2 * Math.PI);
|
|
// ctx.clip();
|
|
|
|
// // ctx.beginPath();
|
|
// // ctx.arc(this.position.x, this.position.y, 9999, 0, 2 * Math.PI);
|
|
// // ctx.fillStyle = "#000";
|
|
// // ctx.fill();
|
|
// // ctx.strokeStyle = "#000";
|
|
// // ctx.stroke();
|
|
|
|
// // ctx.beginPath();
|
|
// // ctx.arc(this.position.x, this.position.y, this.eventHorizon, 0, 2 * Math.PI);
|
|
// // ctx.fillStyle = `rgba(0,0,0,${0.05*Math.random()})`;
|
|
// // ctx.fill();
|
|
// // ctx.strokeStyle = "#000";
|
|
// // ctx.stroke();
|
|
// } else {
|
|
// this.isShielded = true;
|
|
// this.isDropPowerUp = false;
|
|
// this.seePlayer.recall = false
|
|
// this.fill = "transparent"
|
|
// this.stroke = "transparent"
|
|
// this.collisionFilter.mask = cat.player | cat.map | cat.body | cat.mob; //can't touch bullets
|
|
// ctx.beginPath();
|
|
// ctx.arc(this.position.x, this.position.y, this.eventHorizon, 0, 2 * Math.PI);
|
|
// ctx.fillStyle = `rgba(0,0,0,${0.05*Math.random()})`;
|
|
// ctx.fill();
|
|
// }
|
|
// }
|
|
// }
|
|
// },
|
|
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 * simulation.accelScale;
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
spawn.shield(me, x, y);
|
|
me.do = function () {
|
|
this.seePlayerByLookingAt();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
this.repulsion();
|
|
this.harmZone();
|
|
};
|
|
},
|
|
historyBoss(x, y, radius = 30) {
|
|
if (tech.dynamoBotCount > 0) {
|
|
spawn.randomLevelBoss(x, y, spawn.nonCollideBossList)
|
|
return
|
|
}
|
|
mobs.spawn(x, y, 0, radius, "transparent");
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.setDensity(me, 0.21); //extra dense //normal is 0.001
|
|
me.laserRange = 350;
|
|
me.seeAtDistance2 = 2000000;
|
|
me.isBoss = true;
|
|
me.damageReduction = 0.35 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1) // me.damageReductionGoal
|
|
|
|
me.showHealthBar = false; //drawn in this.awake
|
|
me.delayLimit = 60 + Math.floor(30 * Math.random());
|
|
me.followDelay = 600 - Math.floor(90 * Math.random())
|
|
me.stroke = "transparent"; //used for drawGhost
|
|
me.collisionFilter.mask = cat.bullet | cat.body
|
|
me.memory = Infinity
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
ctx.setTransform(1, 0, 0, 1, 0, 0); //reset warp effect
|
|
ctx.setLineDash([]) //reset stroke dash effect
|
|
})
|
|
})
|
|
};
|
|
me.warpIntensity = 0
|
|
me.awake = function () {
|
|
this.checkStatus();
|
|
//health bar needs to be here because the position is being set
|
|
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(150,0,255,0.7)";
|
|
ctx.fillRect(x, y, w * this.health, h);
|
|
|
|
//draw eye
|
|
const unit = Vector.normalise(Vector.sub(m.pos, this.position))
|
|
const eye = Vector.add(Vector.mult(unit, 15), this.position)
|
|
ctx.beginPath();
|
|
ctx.arc(eye.x, eye.y, 4, 0, 2 * Math.PI);
|
|
ctx.moveTo(this.position.x + 20 * unit.x, this.position.y + 20 * unit.y);
|
|
ctx.lineTo(this.position.x + 30 * unit.x, this.position.y + 30 * unit.y);
|
|
ctx.strokeStyle = this.stroke;
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
|
|
ctx.setLineDash([125 * Math.random(), 125 * Math.random()]); //the dashed effect is not set back to normal, because it looks neat for how the player is drawn
|
|
// ctx.lineDashOffset = 6*(simulation.cycle % 215);
|
|
if (this.distanceToPlayer() < this.laserRange) {
|
|
if (m.immuneCycle < m.cycle) {
|
|
if (m.energy > 0.002) {
|
|
m.energy -= 0.004
|
|
} else {
|
|
m.damage(0.0004 * simulation.dmgScale)
|
|
}
|
|
}
|
|
this.warpIntensity += 0.0004
|
|
requestAnimationFrame(() => {
|
|
if (!simulation.paused && m.alive) {
|
|
ctx.transform(1, this.warpIntensity * (Math.random() - 0.5), this.warpIntensity * (Math.random() - 0.5), 1, 0, 0); //ctx.transform(Horizontal scaling. A value of 1 results in no scaling, Vertical skewing, Horizontal skewing, Vertical scaling. A value of 1 results in no scaling, Horizontal translation (moving), Vertical translation (moving)) //https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setTransform
|
|
// ctx.transform(1 - scale * (Math.random() - 0.5), skew * (Math.random() - 0.5), skew * (Math.random() - 0.5), 1 - scale * (Math.random() - 0.5), translation * (Math.random() - 0.5), translation * (Math.random() - 0.5)); //ctx.transform(Horizontal scaling. A value of 1 results in no scaling, Vertical skewing, Horizontal skewing, Vertical scaling. A value of 1 results in no scaling, Horizontal translation (moving), Vertical translation (moving)) //https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setTransform
|
|
}
|
|
})
|
|
ctx.beginPath();
|
|
ctx.moveTo(eye.x, eye.y);
|
|
ctx.lineTo(m.pos.x, m.pos.y);
|
|
ctx.lineTo(m.pos.x + (Math.random() - 0.5) * 3000, m.pos.y + (Math.random() - 0.5) * 3000);
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeStyle = "rgb(150,0,255)";
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.arc(m.pos.x, m.pos.y, 40, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "rgba(150,0,255,0.1)";
|
|
ctx.fill();
|
|
} else {
|
|
this.warpIntensity = 0;
|
|
}
|
|
|
|
//several ellipses spinning about the same axis
|
|
const rotation = simulation.cycle * 0.015
|
|
const phase = simulation.cycle * 0.021
|
|
ctx.lineWidth = 1;
|
|
ctx.fillStyle = "rgba(150,0,255,0.05)"
|
|
ctx.strokeStyle = "#70f"
|
|
for (let i = 0, len = 6; i < len; i++) {
|
|
ctx.beginPath();
|
|
ctx.ellipse(this.position.x, this.position.y, this.laserRange * Math.abs(Math.sin(phase + i / len * Math.PI)), this.laserRange, rotation, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
|
|
if (!this.isStunned && !this.isSlowed) {
|
|
if (this.followDelay > this.delayLimit) this.followDelay -= 0.15;
|
|
let history = m.history[(m.cycle - Math.floor(this.followDelay)) % 600]
|
|
Matter.Body.setPosition(this, { x: history.position.x, y: history.position.y - history.yOff + 24.2859 }) //bullets move with player
|
|
}
|
|
}
|
|
me.do = function () {
|
|
if (this.seePlayer.recall || (!(simulation.cycle % this.seePlayerFreq) && this.distanceToPlayer2() < this.seeAtDistance2 && !m.isCloak)) {
|
|
setTimeout(() => {
|
|
this.do = this.awake
|
|
this.stroke = "rgba(205,0,255,0.5)"
|
|
this.fill = "rgba(205,0,255,0.1)"
|
|
this.seePlayer.yes = true
|
|
}, 2000);
|
|
}
|
|
this.checkStatus();
|
|
};
|
|
},
|
|
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.00009 * simulation.accelScale;
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.onDamage = function () {
|
|
this.laserPos = this.position;
|
|
};
|
|
spawn.shield(me, x, y);
|
|
me.do = function () {
|
|
this.seePlayerByLookingAt();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
const dist2 = this.distanceToPlayer2();
|
|
//laser Tracking
|
|
if (this.seePlayer.yes && dist2 < 4000000) {
|
|
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 = Vector.add(this.laserPos, Vector.mult(Vector.sub(player.position, this.laserPos), 0.1));
|
|
let targetDist = Vector.magnitude(Vector.sub(this.laserPos, m.pos));
|
|
const r = 12;
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.position.x, this.position.y);
|
|
if (targetDist < r + 16) {
|
|
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.98,
|
|
// y: this.velocity.y * 0.98
|
|
// });
|
|
// }
|
|
if (dist2 > 80000) {
|
|
const laserWidth = 0.002;
|
|
let laserOffR = Vector.rotateAbout(this.laserPos, (targetDist - r) * laserWidth, this.position);
|
|
let sub = Vector.normalise(Vector.sub(laserOffR, this.position));
|
|
laserOffR = Vector.add(laserOffR, Vector.mult(sub, rangeWidth));
|
|
ctx.lineTo(laserOffR.x, laserOffR.y);
|
|
|
|
let laserOffL = Vector.rotateAbout(this.laserPos, (targetDist - r) * -laserWidth, this.position);
|
|
sub = Vector.normalise(Vector.sub(laserOffL, this.position));
|
|
laserOffL = Vector.add(laserOffL, Vector.mult(sub, rangeWidth));
|
|
ctx.lineTo(laserOffL.x, laserOffL.y);
|
|
ctx.fillStyle = `rgba(0,0,255,${Math.max(0, 0.3 * r / targetDist)})`
|
|
ctx.fill();
|
|
}
|
|
} else {
|
|
this.laserPos = this.position;
|
|
}
|
|
}
|
|
},
|
|
flutter(x, y, radius = 20 + 6 * Math.random()) {
|
|
mobs.spawn(x, y, 7, radius, '#16576b');
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.setDensity(me, 0.002); //extra dense //normal is 0.001 //makes effective life much larger
|
|
// me.damageReduction = 0.04 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 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.0006 + 0.0007 * Math.sqrt(simulation.accelScale);
|
|
me.frictionAir = 0.04;
|
|
// me.seePlayerFreq = 40 + Math.floor(13 * Math.random())
|
|
me.memory = 240;
|
|
me.restitution = 1;
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.lookTorque = 0.000001 * (Math.random() > 0.5 ? -1 : 1);
|
|
me.fireDir = { x: 0, y: 0 }
|
|
spawn.shield(me, x, y);
|
|
|
|
// me.onDeath = function() {};
|
|
me.flapRate = 0.3 + Math.floor(3 * Math.random()) / 10 + 100 * me.accelMag
|
|
me.flapRadius = 75 + radius * 3
|
|
me.do = function () {
|
|
this.seePlayerByHistory()
|
|
this.checkStatus();
|
|
if (this.seePlayer.recall) {
|
|
this.force.x += Math.cos(this.angle) * this.accelMag * this.mass
|
|
this.force.y += Math.sin(this.angle) * this.accelMag * this.mass
|
|
|
|
//set direction to turn to fire
|
|
if (!(simulation.cycle % this.seePlayerFreq)) {
|
|
this.fireDir = Vector.normalise(Vector.sub(this.seePlayer.position, this.position));
|
|
|
|
//dot product can't tell if mob is facing directly away or directly towards, so check if pointed directly away from player every few cycles
|
|
const mod = (a, n) => {
|
|
return a - Math.floor(a / n) * n
|
|
}
|
|
const sub = Vector.sub(m.pos, this.position) //check by comparing different between angles. Give this a nudge if angles are 180 degree different
|
|
const diff = mod(Math.atan2(sub.y, sub.x) - this.angle + Math.PI, 2 * Math.PI) - Math.PI
|
|
if (Math.abs(diff) > 2.8) this.torque += 0.0002 * this.inertia * Math.random();
|
|
}
|
|
|
|
//rotate towards fireDir
|
|
const angle = this.angle + Math.PI / 2;
|
|
c = Math.cos(angle) * this.fireDir.x + Math.sin(angle) * this.fireDir.y;
|
|
const threshold = 0.4;
|
|
const turn = 0.000025 * this.inertia
|
|
if (c > threshold) {
|
|
this.torque += turn;
|
|
} else if (c < -threshold) {
|
|
this.torque -= turn;
|
|
}
|
|
const flapArc = 0.7 //don't go past 1.57 for normal flaps
|
|
|
|
ctx.fillStyle = `hsla(${160 + 40 * Math.random()}, 100%, ${25 + 25 * Math.random() * Math.random()}%, 0.2)`; //"rgba(0,235,255,0.3)"; // ctx.fillStyle = `hsla(44, 79%, 31%,0.4)`; //"rgba(0,235,255,0.3)";
|
|
this.wing(this.angle + Math.PI / 2 + flapArc * Math.sin(simulation.cycle * this.flapRate), this.flapRadius)
|
|
this.wing(this.angle - Math.PI / 2 - flapArc * Math.sin(simulation.cycle * this.flapRate), this.flapRadius)
|
|
}
|
|
};
|
|
},
|
|
beetleBoss(x, y, radius = 50) {
|
|
mobs.spawn(x, y, 7, radius, '#16576b');
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
Matter.Body.setDensity(me, 0.005); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.damageReduction = 0.07 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.startingDamageReduction = me.damageReduction
|
|
me.isInvulnerable = false
|
|
me.nextHealthThreshold = 0.75
|
|
me.invulnerableCount = 0
|
|
|
|
me.flapRate = 0.2
|
|
me.wingSize = 0
|
|
me.wingGoal = 250 + simulation.difficulty
|
|
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.00045 + 0.0005 * Math.sqrt(simulation.accelScale);
|
|
me.frictionAir = 0.05;
|
|
me.seePlayerFreq = 13
|
|
me.memory = 420;
|
|
me.restitution = 1;
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.lookTorque = 0.000001 * (Math.random() > 0.5 ? -1 : 1);
|
|
me.fireDir = { x: 0, y: 0 }
|
|
spawn.shield(me, x, y);
|
|
|
|
// len = 0.3 * simulation.difficulty //spawn some baby flutters
|
|
// if (len > 3) {
|
|
// for (let i = 0; i < len; ++i) {
|
|
// const phase = i / len * 2 * Math.PI
|
|
// const where = Vector.add(me.position, Vector.mult({ x: Math.cos(phase), y: Math.sin(phase) }, radius * 1.5))
|
|
// spawn.flutter(where.x, where.y)
|
|
// }
|
|
// }
|
|
|
|
// if (Math.random() < 0.3) {
|
|
// const len = 0.1 * simulation.difficulty //spawn some baby flutters
|
|
// let i = 0
|
|
// let spawnFlutters = () => {
|
|
// if (i < len) {
|
|
// if (!(simulation.cycle % 30) && !simulation.paused && !simulation.isChoosing) {
|
|
// const phase = i / len * 2 * Math.PI
|
|
// const where = Vector.add(me.position, Vector.mult({ x: Math.cos(phase), y: Math.sin(phase) }, radius * 1.5))
|
|
// spawn.flutter(where.x, where.y)
|
|
// i++
|
|
// }
|
|
// requestAnimationFrame(spawnFlutters);
|
|
// }
|
|
// }
|
|
// requestAnimationFrame(spawnFlutters);
|
|
// me.isAlreadyHadBabies = true
|
|
// }
|
|
|
|
me.pushAway = function (magX = 0.13, magY = 0.05) {
|
|
for (let i = 0, len = body.length; i < len; ++i) { //push blocks away horizontally
|
|
if (Vector.magnitudeSquared(Vector.sub(body[i].position, this.position)) < 4000000) { //2000
|
|
body[i].force.x += magX * body[i].mass * (body[i].position.x > this.position.x ? 1 : -1)
|
|
body[i].force.y -= magY * body[i].mass
|
|
}
|
|
}
|
|
for (let i = 0, len = bullet.length; i < len; ++i) { //push blocks away horizontally
|
|
if (Vector.magnitudeSquared(Vector.sub(bullet[i].position, this.position)) < 4000000) { //2000
|
|
bullet[i].force.x += magX * bullet[i].mass * (bullet[i].position.x > this.position.x ? 1 : -1)
|
|
bullet[i].force.y -= magY * bullet[i].mass
|
|
}
|
|
}
|
|
for (let i = 0, len = powerUp.length; i < len; ++i) { //push blocks away horizontally
|
|
if (Vector.magnitudeSquared(Vector.sub(powerUp[i].position, this.position)) < 4000000) { //2000
|
|
powerUp[i].force.x += magX * powerUp[i].mass * (powerUp[i].position.x > this.position.x ? 1 : -1)
|
|
powerUp[i].force.y -= magY * powerUp[i].mass
|
|
}
|
|
}
|
|
if (Vector.magnitudeSquared(Vector.sub(player.position, this.position)) < 4000000) { //2000
|
|
player.force.x += magX * player.mass * (player.position.x > this.position.x ? 1 : -1)
|
|
player.force.y -= magY * player.mass
|
|
}
|
|
}
|
|
|
|
me.babies = function (len) {
|
|
const delay = Math.max(3, Math.floor(15 - len / 2))
|
|
let i = 0
|
|
let spawnFlutters = () => {
|
|
if (i < len) {
|
|
if (!(simulation.cycle % delay) && !simulation.paused && !simulation.isChoosing && m.alive) {
|
|
// const phase = i / len * 2 * Math.PI
|
|
// const where = Vector.add(this.position, Vector.mult({ x: Math.cos(phase), y: Math.sin(phase) }, radius * 1.5))
|
|
const unit = Vector.normalise(Vector.sub(player.position, this.position))
|
|
const velocity = Vector.mult(unit, 10 + 10 * Math.random())
|
|
const where = Vector.add(this.position, Vector.mult(unit, radius * 1.2))
|
|
spawn.allowShields = false
|
|
spawn.flutter(where.x, where.y, Math.floor(7 + 8 * Math.random()))
|
|
const who = mob[mob.length - 1]
|
|
Matter.Body.setDensity(who, 0.01); //extra dense //normal is 0.001 //makes effective life much larger
|
|
Matter.Body.setVelocity(who, velocity);
|
|
Matter.Body.setAngle(who, Math.atan2(velocity.y, velocity.x))
|
|
|
|
this.alertNearByMobs();
|
|
spawn.allowShields = true
|
|
i++
|
|
}
|
|
requestAnimationFrame(spawnFlutters);
|
|
}
|
|
}
|
|
requestAnimationFrame(spawnFlutters);
|
|
}
|
|
// me.babies(0.05 * simulation.difficulty + 1)
|
|
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
me.babies(0.05 * simulation.difficulty + 1)
|
|
};
|
|
me.onDamage = function () {
|
|
if (this.health < this.nextHealthThreshold && this.alive) {
|
|
this.health = this.nextHealthThreshold - 0.01
|
|
this.nextHealthThreshold = Math.floor(this.health * 4) / 4
|
|
this.invulnerableCount = 90 + Math.floor(30 * Math.random())
|
|
this.isInvulnerable = true
|
|
this.damageReduction = 0
|
|
this.frictionAir = 0
|
|
this.wingGoal = 0
|
|
this.wingSize = 0
|
|
this.flapRate += 0.13
|
|
this.accelMag *= 1.4
|
|
}
|
|
};
|
|
me.do = function () {
|
|
this.seePlayerByHistory(50)
|
|
this.checkStatus();
|
|
if (this.isInvulnerable) {
|
|
this.invulnerableCount--
|
|
if (this.invulnerableCount < 0) {
|
|
this.isInvulnerable = false
|
|
this.damageReduction = this.startingDamageReduction
|
|
this.frictionAir = 0.05
|
|
this.wingGoal = 250
|
|
this.pushAway(Math.sqrt(this.flapRate) * 0.13, Math.sqrt(this.flapRate) * 0.06) //this.flapRate = 0.2, +0.13x3 -> 0.6
|
|
me.babies(0.05 * simulation.difficulty + 1)
|
|
}
|
|
//draw invulnerable
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 13 + 5 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
} else if (this.seePlayer.recall) {
|
|
// const force = Vector.mult(Vector.normalise(Vector.sub(this.seePlayer.position, this.position)), this.accelMag * this.mass)
|
|
// const force = Vector.mult({ x: Math.cos(this.angle), y: Math.sin(this.angle) }, this.accelMag * this.mass)
|
|
// this.force.x += force.x;
|
|
// this.force.y += force.y;
|
|
this.force.x += Math.cos(this.angle) * this.accelMag * this.mass
|
|
this.force.y += Math.sin(this.angle) * this.accelMag * this.mass
|
|
|
|
//set direction to turn to fire
|
|
if (!(simulation.cycle % this.seePlayerFreq)) {
|
|
this.fireDir = Vector.normalise(Vector.sub(this.seePlayer.position, this.position));
|
|
|
|
//dot product can't tell if mob is facing directly away or directly towards, so check if pointed directly away from player every few cycles
|
|
//check by comparing different between angles. Give this a nudge if angles are 180 degree different
|
|
const mod = (a, n) => {
|
|
return a - Math.floor(a / n) * n
|
|
}
|
|
const sub = Vector.sub(m.pos, this.position)
|
|
const diff = mod(Math.atan2(sub.y, sub.x) - this.angle + Math.PI, 2 * Math.PI) - Math.PI
|
|
if (Math.abs(diff) > 2.8) this.torque += 0.0002 * this.inertia * Math.random();
|
|
}
|
|
|
|
//rotate towards fireDir
|
|
const angle = this.angle + Math.PI / 2;
|
|
c = Math.cos(angle) * this.fireDir.x + Math.sin(angle) * this.fireDir.y;
|
|
const threshold = 0.4;
|
|
const turn = 0.00003 * this.inertia
|
|
if (c > threshold) {
|
|
this.torque += turn;
|
|
} else if (c < -threshold) {
|
|
this.torque -= turn;
|
|
}
|
|
const flapArc = 0.7 //don't go past 1.57 for normal flaps
|
|
this.wingSize = 0.97 * this.wingSize + 0.03 * this.wingGoal
|
|
ctx.fillStyle = this.fill = `hsla(${160 + 40 * Math.random()}, 100%, ${25 + 25 * Math.random() * Math.random()}%, 0.9)`; //"rgba(0,235,255,0.3)"; // ctx.fillStyle = `hsla(44, 79%, 31%,0.4)`; //"rgba(0,235,255,0.3)";
|
|
this.wing(this.angle + Math.PI / 2 + flapArc * Math.sin(simulation.cycle * this.flapRate), this.wingSize, 0.5, 0.0012)
|
|
this.wing(this.angle - Math.PI / 2 - flapArc * Math.sin(simulation.cycle * this.flapRate), this.wingSize, 0.5, 0.0012)
|
|
} else {
|
|
this.wingSize = 0.96 * this.wingSize + 0 //shrink while stunned
|
|
}
|
|
};
|
|
},
|
|
laserTargetingBoss(x, y, radius = 80) {
|
|
const color = "#07f"
|
|
mobs.spawn(x, y, 3, radius, color);
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
Matter.Body.setDensity(me, 0.004); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.damageReduction = 0.1 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 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.00018 * Math.sqrt(simulation.accelScale);
|
|
me.seePlayerFreq = 30
|
|
me.memory = 420;
|
|
me.restitution = 1;
|
|
me.frictionAir = 0.01;
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.lookTorque = 0.000001 * (Math.random() > 0.5 ? -1 : 1);
|
|
me.fireDir = { x: 0, y: 0 }
|
|
spawn.shield(me, x, y, 1);
|
|
// spawn.spawnOrbitals(me, radius + 50 + 100 * Math.random())
|
|
for (let i = 0, len = 2 + 0.3 * Math.sqrt(simulation.difficulty); i < len; i++) spawn.spawnOrbitals(me, radius + 40 + 10 * i, 1);
|
|
// me.onHit = function() { };
|
|
// spawn.shield(me, x, y, 1); //not working, not sure why
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
};
|
|
me.laserInterval = 100
|
|
me.do = function () {
|
|
// this.armor();
|
|
this.seePlayerByLookingAt();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
|
|
if (this.seePlayer.recall) {
|
|
//set direction to turn to fire
|
|
if (!(simulation.cycle % this.seePlayerFreq)) this.fireDir = Vector.normalise(Vector.sub(this.seePlayer.position, this.position));
|
|
|
|
//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.4;
|
|
if (c > threshold) {
|
|
this.torque += 0.000004 * this.inertia;
|
|
} else if (c < -threshold) {
|
|
this.torque -= 0.000004 * this.inertia;
|
|
}
|
|
if (simulation.cycle % this.laserInterval > this.laserInterval / 2) {
|
|
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 = simulation.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 = simulation.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 = 8000;
|
|
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);
|
|
if (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]);
|
|
|
|
// hitting player
|
|
if ((best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
|
|
const dmg = 0.006 * simulation.dmgScale;
|
|
m.damage(dmg);
|
|
//draw damage
|
|
ctx.fillStyle = color;
|
|
ctx.beginPath();
|
|
ctx.arc(best.x, best.y, dmg * 1500, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
}
|
|
//draw beam
|
|
if (best.dist2 === Infinity) best = look;
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.vertices[1].x, this.vertices[1].y);
|
|
ctx.lineTo(best.x, best.y);
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 3;
|
|
ctx.setLineDash([50 + 120 * Math.random(), 50 * Math.random()]);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(this.vertices[1].x, this.vertices[1].y, 1 + 0.3 * (this.laserInterval - simulation.cycle % this.laserInterval), 0, 2 * Math.PI); //* this.fireCycle / this.fireDelay
|
|
ctx.fillStyle = color;
|
|
ctx.fill();
|
|
} else {
|
|
ctx.beginPath();
|
|
ctx.arc(this.vertices[1].x, this.vertices[1].y, 1 + 0.3 * (simulation.cycle % this.laserInterval), 0, 2 * Math.PI); //* this.fireCycle / this.fireDelay
|
|
ctx.fillStyle = color;
|
|
ctx.fill();
|
|
}
|
|
}
|
|
};
|
|
},
|
|
laserBombingBoss(x, y, radius = 80) {
|
|
mobs.spawn(x, y, 3, radius, "rgb(0,235,255)");
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
me.damageReduction = 0.25 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 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.00055 * Math.sqrt(simulation.accelScale);
|
|
me.seePlayerFreq = 30;
|
|
me.memory = 420;
|
|
me.restitution = 1;
|
|
me.frictionAir = 0.05;
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.lookTorque = 0.0000055 * (Math.random() > 0.5 ? -1 : 1) * (1 + 0.1 * Math.sqrt(simulation.difficulty))
|
|
me.fireDir = { x: 0, y: 0 }
|
|
Matter.Body.setDensity(me, 0.01); //extra dense //normal is 0.001 //makes effective life much larger
|
|
spawn.shield(me, x, y, 1);
|
|
spawn.spawnOrbitals(me, radius + 200 + 300 * Math.random())
|
|
me.onHit = function () { };
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
};
|
|
me.targetingCount = 0;
|
|
me.targetingTime = 60 - Math.min(58, 3 * simulation.difficulty)
|
|
me.do = function () {
|
|
|
|
// //wings
|
|
// const wing = (simulation.cycle % 9) > 4 ? this.vertices[0] : this.vertices[2] //Vector.add(this.position, { x: 100, y: 0 })
|
|
// const radius = 200
|
|
// //draw
|
|
// ctx.beginPath();
|
|
// ctx.arc(wing.x, wing.y, radius, 0, 2 * Math.PI); //* this.fireCycle / this.fireDelay
|
|
// ctx.fillStyle = "rgba(0,235,255,0.3)";
|
|
// ctx.fill();
|
|
// //check damage
|
|
// const hitPlayer = Matter.Query.ray([player], this.position, wing, radius)
|
|
// if (hitPlayer.length && m.immuneCycle < m.cycle) {
|
|
// m.immuneCycle = m.cycle + m.collisionImmuneCycles; //player is immune to damage
|
|
// m.damage(0.02 * simulation.dmgScale);
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
this.seePlayerByLookingAt();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
|
|
if (this.seePlayer.recall) {
|
|
//set direction to turn to fire
|
|
if (!(simulation.cycle % this.seePlayerFreq)) this.fireDir = Vector.normalise(Vector.sub(this.seePlayer.position, this.position));
|
|
|
|
//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.02;
|
|
if (c > threshold) {
|
|
this.torque += 0.000004 * this.inertia;
|
|
} else if (c < -threshold) {
|
|
this.torque -= 0.000004 * this.inertia;
|
|
}
|
|
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 = simulation.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 = simulation.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 = 8000;
|
|
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 (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]);
|
|
|
|
// hitting player
|
|
if (best.who === playerBody || best.who === playerHead) {
|
|
this.targetingCount++
|
|
if (this.targetingCount > this.targetingTime) {
|
|
this.targetingCount -= 10;
|
|
const sub = Vector.sub(player.position, this.position)
|
|
const dist = Vector.magnitude(sub)
|
|
const speed = Math.min(55, 5 + 20 * simulation.accelScale)
|
|
const velocity = Vector.mult(Vector.normalise(sub), speed)
|
|
spawn.grenade(this.vertices[1].x, this.vertices[1].y, dist / speed, Math.min(550, 250 + simulation.difficulty * 3), 6); // grenade(x, y, lifeSpan = 90 + Math.ceil(60 / simulation.accelScale), pulseRadius = Math.min(550, 250 + simulation.difficulty * 3), size = 4) {
|
|
Matter.Body.setVelocity(mob[mob.length - 1], velocity);
|
|
}
|
|
} else if (this.targetingCount > 0) {
|
|
this.targetingCount--
|
|
}
|
|
//draw beam
|
|
if (best.dist2 === Infinity) best = look;
|
|
// ctx.beginPath();
|
|
// ctx.moveTo(this.vertices[1].x, this.vertices[1].y);
|
|
// ctx.lineTo(best.x, best.y);
|
|
// ctx.strokeStyle = "rgba(0,235,255,0.5)";
|
|
// ctx.lineWidth = 3// + 0.1 * this.targetingCount;
|
|
// ctx.setLineDash([50 + 120 * Math.random(), 50 * Math.random()]);
|
|
// ctx.stroke();
|
|
// ctx.setLineDash([]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.vertices[1].x, this.vertices[1].y);
|
|
ctx.lineTo(best.x, best.y);
|
|
ctx.strokeStyle = "rgba(0,235,255,1)";
|
|
ctx.lineWidth = 3
|
|
ctx.stroke();
|
|
if (this.targetingCount / this.targetingTime > 0.33) {
|
|
ctx.strokeStyle = "rgba(0,235,255,0.45)";
|
|
ctx.lineWidth = 10
|
|
ctx.stroke();
|
|
if (this.targetingCount / this.targetingTime > 0.66) {
|
|
ctx.strokeStyle = "rgba(0,235,255,0.25)";
|
|
ctx.lineWidth = 30
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
// ctx.setLineDash([50 + 120 * Math.random(), 50 * Math.random()]);
|
|
// ctx.setLineDash([]);
|
|
}
|
|
};
|
|
},
|
|
blinkBoss(x, y) {
|
|
mobs.spawn(x, y, 5, 50, "rgb(0,235,255)"); //"rgb(221,102,119)"
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.rotate(me, Math.PI * 0.1);
|
|
Matter.Body.setDensity(me, 0.002); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.isBoss = true;
|
|
me.damageReduction = 0.034 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.memory = 240
|
|
me.seePlayerFreq = 55
|
|
me.blinkRange = 250
|
|
if (0.5 < Math.random()) {
|
|
me.grenadeDelay = 260
|
|
me.blinkRange *= 1.5
|
|
} else {
|
|
me.grenadeDelay = 100
|
|
}
|
|
me.pulseRadius = 1.5 * Math.min(550, 200 + simulation.difficulty * 2)
|
|
me.delay = 55 + 35 * simulation.CDScale;
|
|
me.nextBlinkCycle = me.delay;
|
|
spawn.shield(me, x, y, 1);
|
|
me.onDamage = function () {
|
|
// this.cd = simulation.cycle + this.delay;
|
|
};
|
|
me.onDeath = function () {
|
|
const offAngle = Math.PI * Math.random()
|
|
for (let i = 0, len = 3; i < len; i++) {
|
|
spawn.grenade(this.position.x, this.position.y, this.grenadeDelay);
|
|
const who = mob[mob.length - 1]
|
|
const speed = 5 * simulation.accelScale;
|
|
const angle = 2 * Math.PI * i / len + offAngle
|
|
Matter.Body.setVelocity(who, {
|
|
x: speed * Math.cos(angle),
|
|
y: speed * Math.sin(angle)
|
|
});
|
|
}
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
}
|
|
me.do = function () {
|
|
this.seePlayerByHistory(40)
|
|
if (this.nextBlinkCycle < simulation.cycle && this.seePlayer.yes) { //teleport towards the player
|
|
this.nextBlinkCycle = simulation.cycle + this.delay;
|
|
const dist = Vector.sub(this.seePlayer.position, this.position);
|
|
const distMag = Vector.magnitude(dist);
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.position.x, this.position.y);
|
|
if (distMag < this.blinkRange) { //if player is inside teleport range
|
|
Matter.Body.setPosition(this, this.seePlayer.position);
|
|
} else {
|
|
Matter.Body.translate(this, Vector.mult(Vector.normalise(dist), this.blinkRange));
|
|
}
|
|
spawn.grenade(this.position.x, this.position.y, this.grenadeDelay, this.pulseRadius); //spawn at new location
|
|
ctx.lineTo(this.position.x, this.position.y);
|
|
ctx.lineWidth = this.radius * 2.1;
|
|
ctx.strokeStyle = this.fill; //"rgba(0,0,0,0.5)"; //'#000'
|
|
ctx.stroke();
|
|
Matter.Body.setVelocity(this, { x: 0, y: 0 });
|
|
this.torque += (0.00004 + 0.00003 * Math.random()) * this.inertia * (Math.round(Math.random()) * 2 - 1) //randomly spin around after firing
|
|
}
|
|
this.checkStatus();
|
|
};
|
|
},
|
|
pulsarBoss(x, y, radius = 90, isNonCollide = false) {
|
|
mobs.spawn(x, y, 3, radius, "#a0f");
|
|
let me = mob[mob.length - 1];
|
|
if (isNonCollide) me.collisionFilter.mask = cat.bullet | cat.player
|
|
setTimeout(() => { //fix mob in place, but allow rotation
|
|
me.constraint = Constraint.create({
|
|
pointA: {
|
|
x: me.position.x,
|
|
y: me.position.y
|
|
},
|
|
bodyB: me,
|
|
stiffness: 0.0001,
|
|
damping: 0.3
|
|
});
|
|
Composite.add(engine.world, me.constraint);
|
|
}, 2000); //add in a delay in case the level gets flipped left right
|
|
|
|
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.radius *= 1.5
|
|
me.vertices[1].x = me.position.x + Math.cos(me.angle) * me.radius; //make one end of the triangle longer
|
|
me.vertices[1].y = me.position.y + Math.sin(me.angle) * me.radius;
|
|
// me.homePosition = { x: x, y: y };
|
|
me.fireCycle = 0
|
|
me.fireTarget = { x: 0, y: 0 }
|
|
me.pulseRadius = Math.min(500, 230 + simulation.difficulty * 3)
|
|
me.fireDelay = Math.max(60, 150 - simulation.difficulty * 2)
|
|
me.isFiring = false
|
|
Matter.Body.setDensity(me, 0.01); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.isBoss = true;
|
|
|
|
spawn.shield(me, x, y, 1);
|
|
spawn.spawnOrbitals(me, radius + 200 + 300 * Math.random(), 1)
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
};
|
|
me.onHit = function () { };
|
|
me.do = function () {
|
|
if (player.speed > 5) this.do = this.fire //don't attack until player moves
|
|
}
|
|
me.damageReduction = 0.25 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.fire = function () {
|
|
// this.armor();
|
|
this.checkStatus();
|
|
if (!m.isCloak && !this.isStunned) {
|
|
if (this.isFiring) {
|
|
if (this.fireCycle > this.fireDelay) { //fire
|
|
this.isFiring = false
|
|
this.fireCycle = 0
|
|
this.torque += (0.00008 + 0.00007 * Math.random()) * this.inertia * (Math.round(Math.random()) * 2 - 1) //randomly spin around after firing
|
|
//is player in beam path
|
|
if (Matter.Query.ray([player], this.fireTarget, this.position).length) {
|
|
unit = Vector.mult(Vector.normalise(Vector.sub(this.vertices[1], this.position)), this.distanceToPlayer() - 100)
|
|
this.fireTarget = Vector.add(this.vertices[1], unit)
|
|
}
|
|
//damage player if in range
|
|
if (Vector.magnitude(Vector.sub(player.position, this.fireTarget)) < this.pulseRadius && m.immuneCycle < m.cycle) {
|
|
m.immuneCycle = m.cycle + m.collisionImmuneCycles; //player is immune to damage
|
|
m.damage(0.045 * simulation.dmgScale);
|
|
}
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: this.fireTarget.x,
|
|
y: this.fireTarget.y,
|
|
radius: this.pulseRadius,
|
|
color: "rgba(120,0,255,0.6)",
|
|
time: simulation.drawTime
|
|
});
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.vertices[1].x, this.vertices[1].y)
|
|
ctx.lineTo(this.fireTarget.x, this.fireTarget.y)
|
|
ctx.lineWidth = 20;
|
|
ctx.strokeStyle = "rgba(120,0,255,0.3)";
|
|
ctx.stroke();
|
|
ctx.lineWidth = 5;
|
|
ctx.strokeStyle = "rgba(120,0,255,1)";
|
|
ctx.stroke();
|
|
} else { //delay before firing
|
|
this.fireCycle++
|
|
//draw explosion outline
|
|
ctx.beginPath();
|
|
ctx.arc(this.fireTarget.x, this.fireTarget.y, this.pulseRadius, 0, 2 * Math.PI); //* this.fireCycle / this.fireDelay
|
|
ctx.fillStyle = "rgba(120,0,255,0.07)";
|
|
ctx.fill();
|
|
//draw path from mob to explosion
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.vertices[1].x, this.vertices[1].y)
|
|
ctx.lineTo(this.fireTarget.x, this.fireTarget.y)
|
|
ctx.setLineDash([40 * Math.random(), 200 * Math.random()]);
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeStyle = "rgba(120,0,255,0.3)";
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
}
|
|
} else { //aim at player
|
|
this.fireCycle++
|
|
this.fireDir = Vector.normalise(Vector.sub(m.pos, this.position)); //set direction to turn to fire
|
|
//rotate towards fireAngle
|
|
const angle = this.angle + Math.PI / 2;
|
|
const c = Math.cos(angle) * this.fireDir.x + Math.sin(angle) * this.fireDir.y;
|
|
const threshold = 0.04;
|
|
if (c > threshold) {
|
|
this.torque += 0.0000015 * this.inertia;
|
|
} else if (c < -threshold) {
|
|
this.torque -= 0.0000015 * this.inertia;
|
|
} else if (this.fireCycle > 45) { //fire
|
|
unit = Vector.mult(Vector.normalise(Vector.sub(this.vertices[1], this.position)), this.distanceToPlayer() - 100)
|
|
this.fireTarget = Vector.add(this.vertices[1], unit)
|
|
if (Vector.magnitude(Vector.sub(m.pos, this.fireTarget)) < 1000) { //if's possible for this to be facing 180 degrees away from the player, this makes sure that doesn't occur
|
|
Matter.Body.setAngularVelocity(this, 0)
|
|
this.fireLockCount = 0
|
|
this.isFiring = true
|
|
this.fireCycle = 0
|
|
}
|
|
}
|
|
}
|
|
//gently return to starting location
|
|
// const sub = Vector.sub(this.homePosition, this.position)
|
|
// const dist = Vector.magnitude(sub)
|
|
// if (dist > 250) this.force = Vector.mult(Vector.normalise(sub), this.mass * 0.0002)
|
|
} else {
|
|
this.isFiring = false
|
|
}
|
|
};
|
|
},
|
|
pulsar(x, y, radius = 40) {
|
|
mobs.spawn(x, y, 3, radius, "#f08");
|
|
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.radius *= 2
|
|
// me.frictionAir = 0.02
|
|
|
|
me.vertices[1].x = me.position.x + Math.cos(me.angle) * me.radius; //make one end of the triangle longer
|
|
me.vertices[1].y = me.position.y + Math.sin(me.angle) * me.radius;
|
|
// me.homePosition = { x: x, y: y };
|
|
Matter.Body.setDensity(me, 0.002); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.fireCycle = Infinity
|
|
me.fireTarget = { x: 0, y: 0 }
|
|
me.pulseRadius = Math.min(400, 170 + simulation.difficulty * 3)
|
|
me.fireDelay = Math.max(75, 140 - simulation.difficulty * 0.5)
|
|
me.isFiring = false
|
|
me.onHit = function () { };
|
|
me.canSeeTarget = function () {
|
|
const angle = this.angle + Math.PI / 2;
|
|
const dot = Vector.dot({
|
|
x: Math.cos(angle),
|
|
y: Math.sin(angle)
|
|
}, Vector.normalise(Vector.sub(this.fireTarget, this.position)));
|
|
//distance between the target and the player's location
|
|
if (
|
|
m.isCloak ||
|
|
dot > 0.03 || // not looking at target
|
|
Matter.Query.ray(map, this.fireTarget, this.position).length || Matter.Query.ray(body, this.fireTarget, this.position).length || //something blocking line of sight
|
|
Vector.magnitude(Vector.sub(m.pos, this.fireTarget)) > 1000 // distance from player to target is very far, (this is because dot product can't tell if facing 180 degrees away)
|
|
) {
|
|
this.isFiring = false
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
me.do = function () {
|
|
this.seePlayerByLookingAt();
|
|
this.checkStatus();
|
|
if (this.seePlayer.recall) {
|
|
if (this.isFiring) {
|
|
if (this.fireCycle > this.fireDelay) { //fire
|
|
if (!this.canSeeTarget()) return
|
|
this.isFiring = false
|
|
this.fireCycle = 0
|
|
this.torque += (0.00002 + 0.0002 * Math.random()) * this.inertia * (Math.round(Math.random()) * 2 - 1) //randomly spin around after firing
|
|
//is player in beam path
|
|
if (Matter.Query.ray([player], this.fireTarget, this.position).length) {
|
|
unit = Vector.mult(Vector.normalise(Vector.sub(this.vertices[1], this.position)), this.distanceToPlayer() - 100)
|
|
this.fireTarget = Vector.add(this.vertices[1], unit)
|
|
}
|
|
//damage player if in range
|
|
if (Vector.magnitude(Vector.sub(player.position, this.fireTarget)) < this.pulseRadius && m.immuneCycle < m.cycle) {
|
|
m.immuneCycle = m.cycle + m.collisionImmuneCycles; //player is immune to damage
|
|
m.damage(0.03 * simulation.dmgScale);
|
|
}
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: this.fireTarget.x,
|
|
y: this.fireTarget.y,
|
|
radius: this.pulseRadius,
|
|
color: "rgba(255,0,100,0.6)",
|
|
time: simulation.drawTime
|
|
});
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.vertices[1].x, this.vertices[1].y)
|
|
ctx.lineTo(this.fireTarget.x, this.fireTarget.y)
|
|
ctx.lineWidth = 20;
|
|
ctx.strokeStyle = "rgba(255,0,100,0.3)";
|
|
ctx.stroke();
|
|
ctx.lineWidth = 5;
|
|
ctx.strokeStyle = "rgba(255,0,100,1)";
|
|
ctx.stroke();
|
|
} else { //delay before firing
|
|
this.fireCycle++
|
|
if (!(simulation.cycle % 3)) {
|
|
if (!this.canSeeTarget()) return //if can't see stop firing
|
|
}
|
|
//draw explosion outline
|
|
ctx.beginPath();
|
|
ctx.arc(this.fireTarget.x, this.fireTarget.y, this.pulseRadius, 0, 2 * Math.PI); //* this.fireCycle / this.fireDelay
|
|
ctx.fillStyle = "rgba(255,0,100,0.07)";
|
|
ctx.fill();
|
|
//draw path from mob to explosion
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.vertices[1].x, this.vertices[1].y)
|
|
ctx.lineTo(this.fireTarget.x, this.fireTarget.y)
|
|
ctx.setLineDash([40 * Math.random(), 200 * Math.random()]);
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeStyle = "rgba(255,0,100,0.3)";
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
}
|
|
} else { //aim at player
|
|
this.fireCycle++
|
|
// this.fireDir = ; //set direction to turn to fire
|
|
const angle = this.angle + Math.PI / 2;
|
|
const dot = Vector.dot({
|
|
x: Math.cos(angle),
|
|
y: Math.sin(angle)
|
|
}, Vector.normalise(Vector.sub(this.seePlayer.position, this.position)))
|
|
const threshold = 0.04;
|
|
if (dot > threshold) { //rotate towards fireAngle
|
|
this.torque += 0.0000015 * this.inertia;
|
|
} else if (dot < -threshold) {
|
|
this.torque -= 0.0000015 * this.inertia;
|
|
} else if (this.fireCycle > 60) { // aim
|
|
unit = Vector.mult(Vector.normalise(Vector.sub(this.vertices[1], this.position)), this.distanceToPlayer() - 100)
|
|
this.fireTarget = Vector.add(this.vertices[1], unit)
|
|
if (!this.canSeeTarget()) return
|
|
Matter.Body.setAngularVelocity(this, 0)
|
|
this.fireLockCount = 0
|
|
this.isFiring = true
|
|
this.fireCycle = 0
|
|
}
|
|
}
|
|
//gently return to starting location
|
|
// const sub = Vector.sub(this.homePosition, this.position)
|
|
// const dist = Vector.magnitude(sub)
|
|
// if (dist > 350) this.force = Vector.mult(Vector.normalise(sub), this.mass * 0.0002)
|
|
} else {
|
|
this.isFiring = false
|
|
}
|
|
};
|
|
},
|
|
laser(x, y, radius = 30) {
|
|
const color = "#f00"
|
|
mobs.spawn(x, y, 3, radius, color);
|
|
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.0001 * simulation.accelScale;
|
|
me.laserInterval = 100
|
|
me.onHit = function () {
|
|
//run this function on hitting player
|
|
this.explode();
|
|
};
|
|
me.do = function () {
|
|
this.torque = this.lookTorque * this.inertia * 0.5;
|
|
this.seePlayerByLookingAt();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
if (this.seePlayer.recall) {
|
|
//set direction to turn to fire
|
|
if (!(simulation.cycle % this.seePlayerFreq)) this.fireDir = Vector.normalise(Vector.sub(this.seePlayer.position, this.position));
|
|
|
|
if (simulation.cycle % this.laserInterval > this.laserInterval / 2) {
|
|
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 = simulation.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 = simulation.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 = 8000;
|
|
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);
|
|
if (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]);
|
|
|
|
// hitting player
|
|
if ((best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
|
|
const dmg = 0.003 * simulation.dmgScale;
|
|
m.damage(dmg);
|
|
//draw damage
|
|
ctx.fillStyle = color;
|
|
ctx.beginPath();
|
|
ctx.arc(best.x, best.y, dmg * 1500, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
}
|
|
//draw beam
|
|
if (best.dist2 === Infinity) best = look;
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.vertices[1].x, this.vertices[1].y);
|
|
ctx.lineTo(best.x, best.y);
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 3;
|
|
ctx.setLineDash([50 + 120 * Math.random(), 50 * Math.random()]);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(this.vertices[1].x, this.vertices[1].y, 1 + 0.3 * (this.laserInterval - simulation.cycle % this.laserInterval), 0, 2 * Math.PI); //* this.fireCycle / this.fireDelay
|
|
ctx.fillStyle = color;
|
|
ctx.fill();
|
|
} else {
|
|
ctx.beginPath();
|
|
ctx.arc(this.vertices[1].x, this.vertices[1].y, 1 + 0.3 * (simulation.cycle % this.laserInterval), 0, 2 * Math.PI); //* this.fireCycle / this.fireDelay
|
|
ctx.fillStyle = color;
|
|
ctx.fill();
|
|
}
|
|
}
|
|
};
|
|
},
|
|
laserBoss(x, y, radius = 30) {
|
|
mobs.spawn(x, y, 3, radius, "#f00");
|
|
let me = mob[mob.length - 1];
|
|
|
|
setTimeout(() => { //fix mob in place, but allow rotation
|
|
me.constraint = Constraint.create({
|
|
pointA: {
|
|
x: me.position.x,
|
|
y: me.position.y
|
|
},
|
|
bodyB: me,
|
|
stiffness: 1,
|
|
damping: 1
|
|
});
|
|
Composite.add(engine.world, me.constraint);
|
|
}, 2000); //add in a delay in case the level gets flipped left right
|
|
me.count = 0;
|
|
me.frictionAir = 0.03;
|
|
// me.torque -= me.inertia * 0.002
|
|
spawn.spawnOrbitals(me, radius + 50 + 200 * Math.random())
|
|
Matter.Body.setDensity(me, 0.03); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.damageReduction = 0.25 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.isBoss = true;
|
|
// spawn.shield(me, x, y, 1); //not working, not sure why
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
};
|
|
me.rotateVelocity = Math.min(0.0045, 0.0015 * simulation.accelScale * simulation.accelScale) * (level.levelsCleared > 8 ? 1 : -1) * (simulation.isHorizontalFlipped ? -1 : 1)
|
|
me.do = function () {
|
|
this.fill = '#' + Math.random().toString(16).substr(-6); //flash colors
|
|
this.checkStatus();
|
|
|
|
if (!this.isStunned) {
|
|
//check if slowed
|
|
let slowed = false
|
|
for (let i = 0; i < this.status.length; i++) {
|
|
if (this.status[i].type === "slow") {
|
|
slowed = true
|
|
break
|
|
}
|
|
}
|
|
if (!slowed) {
|
|
this.count++
|
|
Matter.Body.setAngle(this, this.count * this.rotateVelocity)
|
|
Matter.Body.setAngularVelocity(this, 0)
|
|
}
|
|
|
|
ctx.beginPath();
|
|
this.lasers(this.vertices[0], this.angle + Math.PI / 3);
|
|
this.lasers(this.vertices[1], this.angle + Math.PI);
|
|
this.lasers(this.vertices[2], this.angle - Math.PI / 3);
|
|
ctx.strokeStyle = "#50f";
|
|
ctx.lineWidth = 1.5;
|
|
ctx.setLineDash([70 + 300 * Math.random(), 55 * Math.random()]);
|
|
ctx.stroke(); // Draw it
|
|
ctx.setLineDash([]);
|
|
ctx.lineWidth = 20;
|
|
ctx.strokeStyle = "rgba(80,0,255,0.07)";
|
|
ctx.stroke(); // Draw it
|
|
}
|
|
|
|
|
|
// Matter.Body.setVelocity(this, {
|
|
// x: 0,
|
|
// y: 0
|
|
// });
|
|
// Matter.Body.setPosition(this, this.startingPosition);
|
|
|
|
};
|
|
me.lasers = function (where, 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 = simulation.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 = simulation.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 = 7000;
|
|
best = {
|
|
x: null,
|
|
y: null,
|
|
dist2: Infinity,
|
|
who: null,
|
|
v1: null,
|
|
v2: null
|
|
};
|
|
const look = {
|
|
x: where.x + seeRange * Math.cos(angle),
|
|
y: where.y + seeRange * Math.sin(angle)
|
|
};
|
|
// vertexCollision(where, look, mob);
|
|
vertexCollision(where, look, map);
|
|
vertexCollision(where, look, body);
|
|
if (!m.isCloak) vertexCollision(where, look, [playerBody, playerHead]);
|
|
if (best.who && (best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
|
|
m.immuneCycle = m.cycle + m.collisionImmuneCycles + 60; //player is immune to damage for an extra second
|
|
const dmg = 0.14 * simulation.dmgScale;
|
|
m.damage(dmg);
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: best.x,
|
|
y: best.y,
|
|
radius: dmg * 1500,
|
|
color: "rgba(80,0,255,0.5)",
|
|
time: 20
|
|
});
|
|
}
|
|
//draw beam
|
|
if (best.dist2 === Infinity) best = look;
|
|
ctx.moveTo(where.x, where.y);
|
|
ctx.lineTo(best.x, best.y);
|
|
}
|
|
},
|
|
stabber(x, y, radius = 25 + Math.ceil(Math.random() * 12), spikeMax = 7) {
|
|
if (radius > 80) radius = 65;
|
|
mobs.spawn(x, y, 6, radius, "rgb(220,50,205)"); //can't have sides above 6 or collision events don't work (probably because of a convex problem)
|
|
let me = mob[mob.length - 1];
|
|
me.isVerticesChange = true
|
|
me.accelMag = 0.0006 * simulation.accelScale;
|
|
// me.g = 0.0002; //required if using this.gravity
|
|
me.delay = 360 * simulation.CDScale;
|
|
me.spikeVertex = 0;
|
|
me.spikeLength = 0;
|
|
me.isSpikeGrowing = false;
|
|
me.spikeGrowth = 0;
|
|
me.isSpikeReset = true;
|
|
me.collisionFilter.mask = cat.map | cat.body | cat.bullet | cat.player //can't touch other mobs
|
|
Matter.Body.rotate(me, Math.PI * 0.1);
|
|
spawn.shield(me, x, y);
|
|
// me.onDamage = function () {};
|
|
// me.onHit = function() { //run this function on hitting player
|
|
// };
|
|
me.onDeath = function () {
|
|
if (this.spikeLength > 4) {
|
|
this.spikeLength = 4
|
|
const spike = Vector.mult(Vector.normalise(Vector.sub(this.vertices[this.spikeVertex], this.position)), this.radius * this.spikeLength)
|
|
this.vertices[this.spikeVertex].x = this.position.x + spike.x
|
|
this.vertices[this.spikeVertex].y = this.position.y + spike.y
|
|
// this.vertices = Matter.Vertices.hull(Matter.Vertices.clockwiseSort(this.vertices))
|
|
}
|
|
};
|
|
me.do = function () {
|
|
this.seePlayerByLookingAt();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
if (this.isSpikeReset) {
|
|
if (this.seePlayer.recall) {
|
|
const dist = Vector.sub(this.seePlayer.position, this.position);
|
|
const distMag = Vector.magnitude(dist);
|
|
if (distMag < radius * spikeMax) {
|
|
//find nearest vertex
|
|
let nearestDistance = Infinity
|
|
for (let i = 0, len = this.vertices.length; i < len; i++) {
|
|
//find distance to player for each vertex
|
|
const dist = Vector.sub(this.seePlayer.position, this.vertices[i]);
|
|
const distMag = Vector.magnitude(dist);
|
|
//save the closest distance
|
|
if (distMag < nearestDistance) {
|
|
this.spikeVertex = i
|
|
nearestDistance = distMag
|
|
}
|
|
}
|
|
this.spikeLength = 1
|
|
this.isSpikeGrowing = true;
|
|
this.isSpikeReset = false;
|
|
Matter.Body.setAngularVelocity(this, 0)
|
|
}
|
|
}
|
|
} else {
|
|
if (this.isSpikeGrowing) {
|
|
this.spikeLength += Math.pow(this.spikeGrowth += 0.02, 8)
|
|
// if (this.spikeLength < 2) {
|
|
// this.spikeLength += 0.035
|
|
// } else {
|
|
// this.spikeLength += 1
|
|
// }
|
|
if (this.spikeLength > spikeMax) {
|
|
this.isSpikeGrowing = false;
|
|
this.spikeGrowth = 0
|
|
}
|
|
} else {
|
|
Matter.Body.setAngularVelocity(this, this.angularVelocity * 0.8) //reduce rotation
|
|
this.spikeLength -= 0.3
|
|
if (this.spikeLength < 1) {
|
|
this.spikeLength = 1
|
|
this.isSpikeReset = true
|
|
this.radius = radius
|
|
}
|
|
}
|
|
const spike = Vector.mult(Vector.normalise(Vector.sub(this.vertices[this.spikeVertex], this.position)), radius * this.spikeLength)
|
|
this.vertices[this.spikeVertex].x = this.position.x + spike.x
|
|
this.vertices[this.spikeVertex].y = this.position.y + spike.y
|
|
// this.radius = radius * this.spikeLength;
|
|
}
|
|
};
|
|
},
|
|
|
|
striker(x, y, radius = 14 + Math.ceil(Math.random() * 25)) {
|
|
mobs.spawn(x, y, 5, radius, "rgb(221,102,119)");
|
|
let me = mob[mob.length - 1];
|
|
me.accelMag = 0.00034 * simulation.accelScale;
|
|
me.g = 0.00015; //required if using this.gravity
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.delay = 30 + 60 * simulation.CDScale;
|
|
me.cd = Infinity;
|
|
me.strikeRange = 300
|
|
Matter.Body.rotate(me, Math.PI * 0.1);
|
|
spawn.shield(me, x, y);
|
|
me.onDamage = function () {
|
|
this.cd = simulation.cycle + this.delay;
|
|
};
|
|
me.do = function () {
|
|
this.gravity();
|
|
if (!(simulation.cycle % this.seePlayerFreq)) { // this.seePlayerCheck(); from mobs
|
|
if (
|
|
this.distanceToPlayer2() < this.seeAtDistance2 &&
|
|
Matter.Query.ray(map, this.position, this.playerPosRandomY()).length === 0 &&
|
|
// Matter.Query.ray(body, this.position, this.playerPosRandomY()).length === 0 &&
|
|
!m.isCloak
|
|
) {
|
|
this.foundPlayer();
|
|
if (this.cd === Infinity) this.cd = simulation.cycle + this.delay * 0.7;
|
|
} else if (this.seePlayer.recall) {
|
|
this.lostPlayer();
|
|
this.cd = Infinity
|
|
}
|
|
}
|
|
this.checkStatus();
|
|
this.attraction();
|
|
if (this.cd < simulation.cycle && this.seePlayer.recall) {
|
|
const dist = Vector.sub(this.seePlayer.position, this.position);
|
|
const distMag = Vector.magnitude(dist);
|
|
this.cd = simulation.cycle + this.delay;
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.position.x, this.position.y);
|
|
if (distMag < 400) {
|
|
Matter.Body.translate(this, Vector.mult(Vector.normalise(dist), distMag - 20 - radius));
|
|
} else {
|
|
Matter.Body.translate(this, Vector.mult(Vector.normalise(dist), this.strikeRange));
|
|
}
|
|
ctx.lineTo(this.position.x, this.position.y);
|
|
ctx.lineWidth = radius * 2.1;
|
|
ctx.strokeStyle = this.fill; //"rgba(0,0,0,0.5)"; //'#000'
|
|
ctx.stroke();
|
|
Matter.Body.setVelocity(this, {
|
|
x: this.velocity.x * 0.5,
|
|
y: this.velocity.y * 0.5
|
|
});
|
|
}
|
|
};
|
|
},
|
|
revolutionBoss(x, y, radius = 70) {
|
|
const sides = 9 + Math.floor(Math.min(12, 0.2 * simulation.difficulty))
|
|
const coolBends = [-1.8, 0, 0, 0.9, 1.2]
|
|
const bendFactor = coolBends[Math.floor(Math.random() * coolBends.length)];
|
|
mobs.spawn(x, y, sides, radius, "rgb(201,202,225)");
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.rotate(me, 2 * Math.PI * Math.random());
|
|
me.accelMag = 0.00018 + 0.00018 * Math.sqrt(simulation.accelScale);
|
|
me.frictionAir = 0.01;
|
|
me.swordRadiusMax = 400 + 10 * simulation.difficulty;
|
|
me.laserAngle = 0;
|
|
me.swordDamage = 0.025 * simulation.dmgScale //0.033 * simulation.dmgScale; previous //0.14 * simulation.dmgScale;laserBoss
|
|
|
|
// spawn.shield(me, x, y, 1);
|
|
Matter.Body.setDensity(me, 0.005); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.damageReduction = 0.12 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.isBoss = true;
|
|
me.onDamage = function () { };
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
};
|
|
|
|
//invulnerability every 1/4 fraction of life lost
|
|
//required setup for invulnerable
|
|
me.isInvulnerable = false
|
|
me.isNextInvulnerability = 0.75
|
|
me.invulnerabilityCountDown = 0
|
|
me.invulnerable = function () {
|
|
if (this.health < this.isNextInvulnerability) {
|
|
this.isNextInvulnerability = Math.floor(this.health * 4) / 4 //0.75,0.5,0.25
|
|
this.isInvulnerable = true
|
|
this.startingDamageReduction = this.damageReduction
|
|
this.damageReduction = 0
|
|
this.invulnerabilityCountDown = 106
|
|
}
|
|
if (this.isInvulnerable) {
|
|
if (this.invulnerabilityCountDown > 0) {
|
|
this.invulnerabilityCountDown--
|
|
//draw invulnerability
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 13 + 5 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
} else {
|
|
this.isInvulnerable = false
|
|
this.damageReduction = this.startingDamageReduction
|
|
}
|
|
}
|
|
}
|
|
me.do = function () {
|
|
this.invulnerable();
|
|
this.checkStatus();
|
|
this.seePlayerByHistory(60);
|
|
this.attraction();
|
|
//traveling laser
|
|
this.laserAngle += this.isInvulnerable ? 0.025 : 0.006
|
|
for (let i = 0, len = this.vertices.length; i < len; i++) {
|
|
// this.laserSword(this.vertices[1], this.angle + laserAngle);
|
|
const bend = bendFactor * Math.cos(this.laserAngle + 2 * Math.PI * i / len)
|
|
const long = this.swordRadiusMax * Math.sin(this.laserAngle + 2 * Math.PI * i / len)
|
|
if (long > 0) this.laserSword(this.vertices[i], bend + this.angle + (i + 0.5) / sides * 2 * Math.PI, Math.abs(long));
|
|
}
|
|
};
|
|
me.laserSword = function (where, angle, length) {
|
|
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 = simulation.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 = simulation.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] };
|
|
}
|
|
}
|
|
};
|
|
best = { x: null, y: null, dist2: Infinity, who: null, v1: null, v2: null };
|
|
const look = { x: where.x + length * Math.cos(angle), y: where.y + length * Math.sin(angle) };
|
|
// vertexCollision(where, look, body); // vertexCollision(where, look, mob);
|
|
vertexCollision(where, look, map);
|
|
if (!m.isCloak) vertexCollision(where, look, [playerBody, playerHead]);
|
|
if (best.who && (best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
|
|
m.immuneCycle = m.cycle + m.collisionImmuneCycles; //player is immune to damage for an extra second
|
|
m.damage(this.swordDamage);
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: best.x,
|
|
y: best.y,
|
|
radius: this.swordDamage * 1500,
|
|
color: "rgba(80,0,255,0.5)",
|
|
time: 20
|
|
});
|
|
}
|
|
if (best.dist2 === Infinity) best = look;
|
|
ctx.beginPath(); //draw beam
|
|
ctx.moveTo(where.x, where.y);
|
|
ctx.lineTo(best.x, best.y);
|
|
ctx.strokeStyle = "rgba(100,100,255,0.1)"; // Purple path
|
|
ctx.lineWidth = 10;
|
|
ctx.stroke();
|
|
ctx.strokeStyle = "rgba(100,100,255,0.5)"; // Purple path
|
|
ctx.lineWidth = 2;
|
|
// ctx.stroke();
|
|
ctx.setLineDash([70 + 300 * Math.random(), 55 * Math.random()]);
|
|
ctx.stroke(); // Draw it
|
|
ctx.setLineDash([]);
|
|
}
|
|
},
|
|
sprayBoss(x, y, radius = 40, isSpawnBossPowerUp = true) {
|
|
mobs.spawn(x, y, 16, radius, "rgb(255,255,255)");
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
me.isReactorBoss = true;
|
|
me.inertia = Infinity; //no rotation
|
|
me.burstFireFreq = 22 + Math.floor(13 * simulation.CDScale)
|
|
me.burstTotalPhases = 3 + Math.floor(1.4 / simulation.CDScale)
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0;
|
|
me.restitution = 1
|
|
// spawn.spawnOrbitals(me, radius + 50 + 125 * Math.random(), 1)
|
|
Matter.Body.setDensity(me, 0.002 + 0.0001 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.damageReduction = 0.09 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.startingDamageReduction = me.damageReduction
|
|
me.isInvulnerable = false
|
|
me.nextHealthThreshold = 0.75
|
|
me.onDeath = function () {
|
|
if (isSpawnBossPowerUp) powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
};
|
|
me.onDamage = function () {
|
|
if (this.health < this.nextHealthThreshold) {
|
|
this.health = this.nextHealthThreshold - 0.01
|
|
this.nextHealthThreshold = Math.floor(this.health * 4) / 4 //0.75,0.5,0.25
|
|
|
|
this.phaseCycle = -2
|
|
this.do = this.burstFire
|
|
this.frictionAir = 1
|
|
this.isInvulnerable = true
|
|
this.damageReduction = 0
|
|
}
|
|
};
|
|
|
|
//draw radial lines from verticies showing future bullet paths?
|
|
me.radialLines = function () {
|
|
ctx.beginPath();
|
|
for (let i = 0, len = this.vertices.length; i < len; i++) {
|
|
ctx.moveTo(this.vertices[i].x, this.vertices[i].y)
|
|
const unit = Vector.add(Vector.mult(Vector.normalise(Vector.sub(this.vertices[i], this.position)), 1000), this.vertices[i])
|
|
ctx.lineTo(unit.x, unit.y)
|
|
}
|
|
ctx.lineWidth = 10
|
|
ctx.strokeStyle = "rgb(200,0,200,0.03)"
|
|
ctx.stroke();
|
|
}
|
|
|
|
me.phaseCycle = 0
|
|
me.normalDoStuff = function () {
|
|
this.checkStatus();
|
|
me.seePlayer.recall = 1
|
|
//maintain speed //faster in the vertical to help avoid repeating patterns
|
|
if (this.speed < 0.01) {
|
|
Matter.Body.setVelocity(this, Vector.mult(Vector.normalise(Vector.sub(player.position, this.position)), 0.1));
|
|
} else {
|
|
if (Math.abs(this.velocity.y) < 9) Matter.Body.setVelocity(this, { x: this.velocity.x, y: this.velocity.y * 1.03 });
|
|
if (Math.abs(this.velocity.x) < 7) Matter.Body.setVelocity(this, { x: this.velocity.x * 1.03, y: this.velocity.y });
|
|
}
|
|
}
|
|
me.burstFire = function () {
|
|
this.normalDoStuff();
|
|
this.radialLines()
|
|
//draw invulnerable
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 13 + 5 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
|
|
if (!(simulation.cycle % this.burstFireFreq)) {
|
|
this.phaseCycle++
|
|
if (this.phaseCycle > this.burstTotalPhases) { //start spiral fire mode
|
|
// this.phaseCycle = -7
|
|
this.do = this.normalDoStuff
|
|
this.frictionAir = 0;
|
|
this.isInvulnerable = false
|
|
this.damageReduction = this.startingDamageReduction
|
|
Matter.Body.setVelocity(this, Vector.rotate({ x: 20, y: 0 }, 2 * Math.PI * Math.random()));
|
|
if (this.isShielded) { //remove shield
|
|
for (let i = 0; i < mob.length; i++) {
|
|
if (mob[i].shield) mob[i].death()
|
|
}
|
|
}
|
|
}
|
|
if (this.phaseCycle > -1) {
|
|
Matter.Body.rotate(this, 0.02)
|
|
for (let i = 0, len = this.vertices.length; i < len; i++) { //fire a bullet from each vertex
|
|
spawn.sniperBullet(this.vertices[i].x, this.vertices[i].y, 3, 4);
|
|
const velocity = Vector.mult(Vector.normalise(Vector.sub(this.position, this.vertices[i])), -15)
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: velocity.x,
|
|
y: velocity.y
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
me.do = me.normalDoStuff
|
|
Matter.Body.setVelocity(me, { x: 10 * (Math.random() - 0.5), y: 10 * (Math.random() - 0.5) });
|
|
},
|
|
mineBoss(x, y, radius = 120, isSpawnBossPowerUp = true) {
|
|
mobs.spawn(x, y, 0, radius, "rgba(255,255,255,0.5)") // "rgb(201,202,225)");
|
|
let me = mob[mob.length - 1];
|
|
// Matter.Body.rotate(me, 2 * Math.PI * Math.random());
|
|
me.isBoss = true;
|
|
me.isReactorBoss = true;
|
|
Matter.Body.setDensity(me, 0.001); //normal is 0.001
|
|
me.damageReduction = 0.04 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.startingDamageReduction = me.damageReduction
|
|
me.isInvulnerable = false
|
|
me.nextHealthThreshold = 0.75
|
|
me.invulnerableCount = 0
|
|
|
|
me.cycle = 0
|
|
me.inertia = Infinity;
|
|
me.frictionAir = 0.01
|
|
me.restitution = 1
|
|
me.friction = 0
|
|
me.collisionFilter.mask = cat.bullet | cat.player | cat.body | cat.map | cat.mob
|
|
me.explodeRange = 400
|
|
Matter.Body.setVelocity(me, { x: 10 * (Math.random() - 0.5), y: 10 * (Math.random() - 0.5) });
|
|
me.seePlayer.recall = 1;
|
|
// spawn.shield(me, x, y, 1);
|
|
me.onDamage = function () {
|
|
if (this.health < this.nextHealthThreshold) {
|
|
this.health = this.nextHealthThreshold - 0.01
|
|
this.nextHealthThreshold = Math.floor(this.health * 4) / 4
|
|
this.invulnerableCount = 60 + simulation.difficulty * 1.5
|
|
this.isInvulnerable = true
|
|
this.damageReduction = 0
|
|
|
|
//slow time to look cool
|
|
// simulation.fpsCap = 10 //new fps
|
|
// simulation.fpsInterval = 1000 / simulation.fpsCap;
|
|
//how long to wait to return to normal fps
|
|
// m.defaultFPSCycle = m.cycle + 20 + 90
|
|
|
|
|
|
for (let i = 0, len = mob.length; i < len; ++i) { //trigger nearby mines
|
|
if (mob[i].isMine && Vector.magnitude(Vector.sub(this.position, mob[i].position)) < this.explodeRange) mob[i].isExploding = true
|
|
}
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: this.position.x,
|
|
y: this.position.y,
|
|
radius: this.explodeRange,
|
|
color: "rgba(255,25,0,0.6)",
|
|
time: simulation.drawTime * 2
|
|
});
|
|
|
|
}
|
|
};
|
|
me.onDeath = function () {
|
|
if (isSpawnBossPowerUp) powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
for (let i = 0, len = mob.length; i < len; ++i) { //trigger nearby mines
|
|
if (mob[i].isMine && Vector.magnitude(Vector.sub(this.position, mob[i].position)) < this.explodeRange) mob[i].isExploding = true
|
|
}
|
|
};
|
|
// console.log(me.mass) //100
|
|
me.do = function () {
|
|
me.seePlayer.recall = 1
|
|
//maintain speed //faster in the vertical to help avoid repeating patterns
|
|
if (this.speed < 0.01) {
|
|
const unit = Vector.sub(player.position, this.position)
|
|
Matter.Body.setVelocity(this, Vector.mult(Vector.normalise(unit), 0.1));
|
|
// this.invulnerableCount = 10 + simulation.difficulty * 0.5
|
|
// this.isInvulnerable = true
|
|
// this.damageReduction = 0
|
|
} else {
|
|
if (Math.abs(this.velocity.y) < 10) {
|
|
Matter.Body.setVelocity(this, { x: this.velocity.x, y: this.velocity.y * 1.03 });
|
|
}
|
|
if (Math.abs(this.velocity.x) < 7) {
|
|
Matter.Body.setVelocity(this, { x: this.velocity.x * 1.03, y: this.velocity.y });
|
|
}
|
|
}
|
|
if (this.isInvulnerable) {
|
|
this.invulnerableCount--
|
|
if (this.invulnerableCount < 0) {
|
|
this.isInvulnerable = false
|
|
this.damageReduction = this.startingDamageReduction
|
|
}
|
|
//draw invulnerable
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 13 + 5 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
}
|
|
this.checkStatus();
|
|
if (!(simulation.cycle % 15) && mob.length < 360) spawn.mine(this.position.x, this.position.y)
|
|
};
|
|
},
|
|
mine(x, y) {
|
|
mobs.spawn(x, y, 8, 10, "rgb(255, 255, 255)"); //"rgb(100,170,150)" //"rgb(61, 125, 121)"
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent";
|
|
Matter.Body.setDensity(me, 0.0001); //normal is 0.001
|
|
// Matter.Body.setStatic(me, true); //make static (disables taking damage)
|
|
me.frictionAir = 1
|
|
me.damageReduction = 2
|
|
me.collisionFilter.mask = cat.bullet //| cat.body
|
|
// me.collisionFilter.category = cat.mobBullet;
|
|
// me.collisionFilter.mask = cat.bullet | cat.body // | cat.player
|
|
me.isMine = true
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.isBadTarget = true;
|
|
me.isMobBullet = true;
|
|
me.isUnstable = true; //dies when blocked
|
|
me.showHealthBar = false;
|
|
me.explodeRange = 210 + 140 * Math.random()
|
|
me.isExploding = false
|
|
me.countDown = Math.ceil(4 * Math.random())
|
|
me.isInvulnerable = true //not actually invulnerable, just prevents block + ice-9 interaction
|
|
|
|
// me.onHit = function() {
|
|
// this.isExploding = true
|
|
// };
|
|
// me.onDamage = function() {
|
|
// this.health = 1
|
|
// this.isExploding = true
|
|
// };
|
|
me.do = function () {
|
|
this.checkStatus();
|
|
|
|
if (Matter.Query.collides(this, [player]).length > 0) {
|
|
this.isExploding = true
|
|
}
|
|
|
|
if (this.isExploding) {
|
|
if (this.countDown-- < 0) { //explode
|
|
this.death();
|
|
//hit player
|
|
if (Vector.magnitude(Vector.sub(this.position, player.position)) < this.explodeRange && m.immuneCycle < m.cycle) {
|
|
m.damage(0.02 * simulation.dmgScale * (tech.isRadioactiveResistance ? 0.25 : 1));
|
|
m.energy -= 0.2 * (tech.isRadioactiveResistance ? 0.25 : 1)
|
|
if (m.energy < 0) m.energy = 0
|
|
}
|
|
// mob[i].isInvulnerable = false //make mineBoss not invulnerable ?
|
|
const range = this.explodeRange + 50 //mines get a slightly larger range to explode
|
|
for (let i = 0, len = mob.length; i < len; ++i) {
|
|
if (mob[i].alive && Vector.magnitude(Vector.sub(this.position, mob[i].position)) < range) {
|
|
if (mob[i].isMine) mob[i].isExploding = true //explode other mines
|
|
}
|
|
}
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: this.position.x,
|
|
y: this.position.y,
|
|
radius: this.explodeRange,
|
|
color: "rgba(80,220,190,0.45)",
|
|
time: 16
|
|
});
|
|
}
|
|
}
|
|
};
|
|
},
|
|
bounceBoss(x, y, radius = 80, isSpawnBossPowerUp = true) {
|
|
mobs.spawn(x, y, 0, radius, "rgb(255,255,255)") // "rgb(201,202,225)");
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.rotate(me, 2 * Math.PI * Math.random());
|
|
me.isBoss = true;
|
|
me.isReactorBoss = true;
|
|
Matter.Body.setDensity(me, 0.003); //normal is 0.001
|
|
me.damageReduction = 0.1 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.startingDamageReduction = me.damageReduction
|
|
me.inertia = Infinity;
|
|
me.isInvulnerable = false
|
|
me.frictionAir = 0.01
|
|
me.restitution = 1
|
|
me.friction = 0
|
|
me.collisionFilter.mask = cat.bullet | cat.player | cat.body | cat.map | cat.mob
|
|
Matter.Body.setVelocity(me, { x: 10 * (Math.random() - 0.5), y: 10 * (Math.random() - 0.5) });
|
|
me.seePlayer.recall = 1;
|
|
// spawn.shield(me, x, y, 1);
|
|
me.onDamage = function () {
|
|
if (this.health < this.nextHealthThreshold) {
|
|
this.health = this.nextHealthThreshold - 0.01
|
|
this.nextHealthThreshold = Math.floor(this.health * 4) / 4 //0.75,0.5,0.25
|
|
this.fireCount = 60 + simulation.difficulty * 1.5
|
|
this.isInvulnerable = true
|
|
this.damageReduction = 0
|
|
}
|
|
};
|
|
if (isSpawnBossPowerUp) me.onDeath = function () { powerUps.spawnBossPowerUp(this.position.x, this.position.y) };
|
|
me.cycle = 0
|
|
me.nextHealthThreshold = 0.75
|
|
me.fireCount = 0
|
|
// console.log(me.mass) //100
|
|
me.do = function () {
|
|
me.seePlayer.recall = 1
|
|
//maintain speed //faster in the vertical to help avoid repeating patterns
|
|
if (this.speed < 0.01) {
|
|
const unit = Vector.sub(player.position, this.position)
|
|
Matter.Body.setVelocity(this, Vector.mult(Vector.normalise(unit), 0.1));
|
|
// this.fireCount = 10 + simulation.difficulty * 0.5
|
|
// this.isInvulnerable = true
|
|
// this.damageReduction = 0
|
|
} else {
|
|
if (Math.abs(this.velocity.y) < 15) Matter.Body.setVelocity(this, { x: this.velocity.x, y: this.velocity.y * 1.03 });
|
|
if (Math.abs(this.velocity.x) < 11) Matter.Body.setVelocity(this, { x: this.velocity.x * 1.03, y: this.velocity.y });
|
|
}
|
|
|
|
if (this.isInvulnerable) {
|
|
this.fireCount--
|
|
if (this.fireCount < 0) {
|
|
this.isInvulnerable = false
|
|
this.damageReduction = this.startingDamageReduction
|
|
}
|
|
|
|
if (this.mass > 10) Matter.Body.scale(this, 0.99, 0.99);
|
|
|
|
// for (let i = 0; i < 1; i++) {
|
|
const velocity = Vector.rotate(Vector.mult(Vector.normalise(this.velocity), -5 - 10 * Math.random()), 0.5 * (Math.random() - 0.5))
|
|
spawn.bounceBullet(this.position.x, this.position.y, velocity)
|
|
// }
|
|
//draw invulnerable
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 13 + 5 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
} else if (this.mass < 100) {
|
|
Matter.Body.scale(this, 1.01, 1.01); //grow back to normal size
|
|
}
|
|
|
|
this.checkStatus();
|
|
//horizontal attraction
|
|
// const xMag = 0.0005
|
|
// if (player.position.x > this.position.x + 200) {
|
|
// this.force.x += xMag * this.mass;
|
|
// } else if (player.position.x < this.position.x - 200) {
|
|
// this.force.x -= xMag * this.mass;
|
|
// }
|
|
};
|
|
},
|
|
timeBoss(x, y, radius = 50, isSpawnBossPowerUp = true) {
|
|
mobs.spawn(x, y, 0, radius, `hsl(0, 100%, 50%)`) // "rgb(201,202,225)");
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.rotate(me, 2 * Math.PI * Math.random());
|
|
me.isBoss = true;
|
|
me.isReactorBoss = true;
|
|
me.inertia = Infinity;
|
|
me.frictionAir = 0.01
|
|
me.restitution = 1
|
|
me.friction = 0
|
|
me.collisionFilter.mask = cat.bullet | cat.player | cat.body | cat.map | cat.mob
|
|
Matter.Body.setVelocity(me, { x: 10 * (Math.random() - 0.5), y: 10 * (Math.random() - 0.5) });
|
|
me.seePlayer.recall = 1;
|
|
// for (let i = 0; i < 4; i++) spawn.spawnOrbitals(me, radius + 25, 1)
|
|
for (let i = 0, len = 2 + 0.3 * Math.sqrt(simulation.difficulty); i < len; i++) spawn.spawnOrbitals(me, radius + 10 + 10 * i, 1);
|
|
// me.skipRate = 1 + Math.floor(simulation.difficulty*0.02)
|
|
// spawn.shield(me, x, y, 1);
|
|
Matter.Body.setDensity(me, 0.001); //normal is 0.001
|
|
me.damageReduction = 0.05 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.startingDamageReduction = me.damageReduction
|
|
me.isInvulnerable = false
|
|
me.onDamage = function () {
|
|
if (this.health < this.nextHealthThreshold) {
|
|
this.health = this.nextHealthThreshold - 0.01
|
|
this.nextHealthThreshold = Math.floor(this.health * 4) / 4 //0.75,0.5,0.25
|
|
this.invulnerableCount = 420 + simulation.difficulty * 5 //how long does invulnerable time last
|
|
this.isInvulnerable = true
|
|
this.damageReduction = 0
|
|
// requestAnimationFrame(() => { simulation.timePlayerSkip(300) }); //wrapping in animation frame prevents errors, probably
|
|
}
|
|
};
|
|
me.onDeath = function () {
|
|
if (isSpawnBossPowerUp) powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
requestAnimationFrame(() => { simulation.timePlayerSkip(60) }); //wrapping in animation frame prevents errors, probably
|
|
};
|
|
|
|
me.cycle = Math.floor(360 * Math.random())
|
|
me.nextHealthThreshold = 0.75
|
|
me.invulnerableCount = 0
|
|
// console.log(me.mass) //100
|
|
me.do = function () {
|
|
this.cycle++
|
|
this.fill = `hsl(${this.cycle * 0.5}, 100%, 80%)`;
|
|
// this.fill = `hsl(${270 + 50*Math.sin(this.cycle*0.02)}, 100%, 60%)`;
|
|
this.seePlayer.recall = 1
|
|
//maintain speed //faster in the vertical to help avoid repeating patterns
|
|
if (this.speed < 0.01) {
|
|
const unit = Vector.sub(player.position, this.position)
|
|
Matter.Body.setVelocity(this, Vector.mult(Vector.normalise(unit), 0.1));
|
|
} else {
|
|
if (Math.abs(this.velocity.y) < 10) Matter.Body.setVelocity(this, { x: this.velocity.x, y: this.velocity.y * 1.02 });
|
|
if (Math.abs(this.velocity.x) < 7) Matter.Body.setVelocity(this, { x: this.velocity.x * 1.02, y: this.velocity.y });
|
|
}
|
|
|
|
if (this.isInvulnerable) {
|
|
this.invulnerableCount--
|
|
if (this.invulnerableCount < 0) {
|
|
this.isInvulnerable = false
|
|
this.damageReduction = this.startingDamageReduction
|
|
}
|
|
//time dilation
|
|
if (!simulation.isTimeSkipping) {
|
|
requestAnimationFrame(() => {
|
|
simulation.timePlayerSkip(2)
|
|
m.walk_cycle += m.flipLegs * m.Vx //makes the legs look like they are moving fast
|
|
}); //wrapping in animation frame prevents errors, probably
|
|
|
|
// simulation.timePlayerSkip(2)
|
|
// m.walk_cycle += m.flipLegs * m.Vx //makes the legs look like they are moving fast
|
|
|
|
//draw invulnerable
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 15 + 6 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
this.checkStatus();
|
|
//horizontal attraction
|
|
// const xMag = 0.0005
|
|
// if (player.position.x > this.position.x + 200) {
|
|
// this.force.x += xMag * this.mass;
|
|
// } else if (player.position.x < this.position.x - 200) {
|
|
// this.force.x -= xMag * this.mass;
|
|
// }
|
|
};
|
|
},
|
|
bounceBullet(x, y, velocity = { x: 0, y: 0 }, radius = 11, sides = 6) {
|
|
//bullets
|
|
mobs.spawn(x, y, sides, radius, "rgb(255,0,155)");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent";
|
|
Matter.Body.setDensity(me, 0.00001); //normal is 0.001
|
|
me.timeLeft = 360 + Math.floor(180 * Math.random())
|
|
me.inertia = Infinity;
|
|
me.damageReduction = 1
|
|
me.frictionAir = 0
|
|
me.friction = 0
|
|
me.restitution = 1
|
|
me.collisionFilter.category = cat.mobBullet;
|
|
me.collisionFilter.mask = cat.bullet | cat.player | cat.body | cat.map | cat.mob
|
|
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.isBadTarget = true;
|
|
me.isMobBullet = true;
|
|
me.showHealthBar = false;
|
|
me.onHit = function () {
|
|
this.explode(this.mass * 12);
|
|
};
|
|
me.do = function () {
|
|
this.timeLimit();
|
|
};
|
|
Matter.Body.setVelocity(me, velocity);
|
|
},
|
|
slashBoss(x, y, radius = 80) {
|
|
mobs.spawn(x, y, 5, radius, "rgb(201,202,225)");
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.rotate(me, 2 * Math.PI * Math.random());
|
|
me.isBoss = true;
|
|
me.damageReduction = 0.1 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.startingDamageReduction = me.damageReduction
|
|
me.isInvulnerable = false
|
|
me.frictionAir = 0.02
|
|
me.seeAtDistance2 = 1000000;
|
|
me.accelMag = 0.0004 + 0.00015 * simulation.accelScale;
|
|
Matter.Body.setDensity(me, 0.0005); //normal is 0.001
|
|
me.collisionFilter.mask = cat.bullet | cat.player | cat.body | cat.map
|
|
me.memory = Infinity;
|
|
me.seePlayerFreq = 20
|
|
me.lockedOn = null;
|
|
|
|
me.torqueMagnitude = 0.00024 * me.inertia * (Math.random() > 0.5 ? -1 : 1);
|
|
me.delay = 70 + 70 * simulation.CDScale;
|
|
me.cd = 0;
|
|
me.swordRadius = 0;
|
|
me.swordVertex = 1
|
|
me.swordRadiusMax = 1100 + 20 * simulation.difficulty;
|
|
me.swordRadiusGrowRate = me.swordRadiusMax * (0.005 + 0.0003 * simulation.difficulty)
|
|
me.isSlashing = false;
|
|
me.swordDamage = 0.07 * simulation.dmgScale
|
|
me.laserAngle = 3 * Math.PI / 5
|
|
const seeDistance2 = 200000
|
|
spawn.shield(me, x, y);
|
|
me.onDamage = function () { };
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
};
|
|
me.do = function () {
|
|
this.seePlayerByHistory(40);
|
|
this.attraction();
|
|
this.checkStatus();
|
|
this.sword() //does various things depending on what stage of the sword swing
|
|
|
|
// ctx.beginPath(); //hide map
|
|
// ctx.arc(this.position.x, this.position.y, 3000, 0, 2 * Math.PI); //* this.fireCycle / this.fireDelay
|
|
// ctx.fillStyle = "#444";
|
|
// ctx.fill();
|
|
};
|
|
me.swordWaiting = function () {
|
|
if (
|
|
this.seePlayer.recall &&
|
|
this.cd < simulation.cycle &&
|
|
this.distanceToPlayer2() < seeDistance2 &&
|
|
!m.isCloak &&
|
|
Matter.Query.ray(map, this.position, this.playerPosRandomY()).length === 0 &&
|
|
Matter.Query.ray(body, this.position, this.playerPosRandomY()).length === 0
|
|
) {
|
|
//find vertex farthest away from player
|
|
let dist = 0
|
|
for (let i = 0, len = this.vertices.length; i < len; i++) {
|
|
const D = Vector.magnitudeSquared(Vector.sub({ x: this.vertices[i].x, y: this.vertices[i].y }, m.pos))
|
|
if (D > dist) {
|
|
dist = D
|
|
this.swordVertex = i
|
|
}
|
|
}
|
|
this.laserAngle = this.swordVertex / 5 * 2 * Math.PI + 0.6283
|
|
this.sword = this.swordGrow
|
|
Matter.Body.setVelocity(this, { x: 0, y: 0 });
|
|
Matter.Body.setAngularVelocity(this, 0)
|
|
this.accelMag = 0
|
|
this.damageReduction = 0
|
|
this.isInvulnerable = true
|
|
this.frictionAir = 1
|
|
}
|
|
}
|
|
me.sword = me.swordWaiting //base function that changes during different aspects of the sword swing
|
|
me.swordGrow = function () {
|
|
this.laserSword(this.vertices[this.swordVertex], this.angle + this.laserAngle);
|
|
this.swordRadius += this.swordRadiusGrowRate
|
|
if (this.swordRadius > this.swordRadiusMax) {
|
|
this.sword = this.swordSlash
|
|
this.spinCount = 0
|
|
}
|
|
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 13 + 5 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
}
|
|
me.swordSlash = function () {
|
|
this.laserSword(this.vertices[this.swordVertex], this.angle + this.laserAngle);
|
|
this.torque += this.torqueMagnitude;
|
|
this.spinCount++
|
|
if (this.spinCount > 80) {
|
|
this.sword = this.swordWaiting
|
|
this.swordRadius = 0
|
|
this.accelMag = 0.001 * simulation.accelScale;
|
|
this.cd = simulation.cycle + this.delay;
|
|
this.damageReduction = this.startingDamageReduction
|
|
this.isInvulnerable = false
|
|
this.frictionAir = 0.01
|
|
}
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 13 + 5 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
}
|
|
me.laserSword = function (where, angle) {
|
|
const vertexCollision = function (v1, v1End, domain) {
|
|
for (let i = 0; i < domain.length; ++i) {
|
|
let v = domain[i].vertices;
|
|
const len = v.length - 1;
|
|
for (let j = 0; j < len; j++) {
|
|
results = simulation.checkLineIntersection(v1, v1End, v[j], v[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: v[j], v2: v[j + 1] };
|
|
}
|
|
}
|
|
results = simulation.checkLineIntersection(v1, v1End, v[0], v[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: v[0], v2: v[len] };
|
|
}
|
|
}
|
|
};
|
|
best = { x: null, y: null, dist2: Infinity, who: null, v1: null, v2: null };
|
|
const look = { x: where.x + this.swordRadius * Math.cos(angle), y: where.y + this.swordRadius * Math.sin(angle) };
|
|
vertexCollision(where, look, body); // vertexCollision(where, look, mob);
|
|
vertexCollision(where, look, map);
|
|
if (!m.isCloak) vertexCollision(where, look, [playerBody, playerHead]);
|
|
if (best.who && (best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
|
|
m.immuneCycle = m.cycle + m.collisionImmuneCycles + 60; //player is immune to damage for an extra second
|
|
m.damage(this.swordDamage);
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: best.x,
|
|
y: best.y,
|
|
radius: this.swordDamage * 1500,
|
|
color: "rgba(80,0,255,0.5)",
|
|
time: 20
|
|
});
|
|
}
|
|
if (best.dist2 === Infinity) best = look;
|
|
ctx.beginPath(); //draw beam
|
|
ctx.moveTo(where.x, where.y);
|
|
ctx.lineTo(best.x, best.y);
|
|
ctx.strokeStyle = "rgba(100,100,255,0.1)"; // Purple path
|
|
ctx.lineWidth = 25;
|
|
ctx.stroke();
|
|
ctx.strokeStyle = "rgba(100,100,255,0.5)"; // Purple path
|
|
ctx.lineWidth = 5;
|
|
ctx.setLineDash([70 + 300 * Math.random(), 55 * Math.random()]);
|
|
ctx.stroke(); // Draw it
|
|
ctx.setLineDash([]);
|
|
}
|
|
},
|
|
slasher(x, y, radius = 33 + Math.ceil(Math.random() * 30)) {
|
|
mobs.spawn(x, y, 5, radius, "rgb(201,202,225)");
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.rotate(me, 2 * Math.PI * Math.random());
|
|
me.accelMag = 0.0008 * simulation.accelScale;
|
|
me.torqueMagnitude = 0.00002 * me.inertia * (Math.random() > 0.5 ? -1 : 1);
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0.035;
|
|
me.delay = 120 * simulation.CDScale;
|
|
me.cd = 0;
|
|
me.swordRadius = 0;
|
|
me.swordVertex = 1
|
|
me.swordRadiusMax = 350 + 5 * simulation.difficulty;
|
|
me.swordRadiusGrowRate = me.swordRadiusMax * (0.018 + 0.0006 * simulation.difficulty)
|
|
me.isSlashing = false;
|
|
me.swordDamage = 0.04 * simulation.dmgScale
|
|
me.laserAngle = 3 * Math.PI / 5
|
|
const seeDistance2 = 200000
|
|
spawn.shield(me, x, y);
|
|
me.onDamage = function () { };
|
|
me.do = function () {
|
|
this.checkStatus();
|
|
this.seePlayerByHistory(15);
|
|
this.attraction();
|
|
this.sword() //does various things depending on what stage of the sword swing
|
|
};
|
|
me.swordWaiting = function () {
|
|
if (
|
|
this.seePlayer.recall &&
|
|
this.cd < simulation.cycle &&
|
|
this.distanceToPlayer2() < seeDistance2 &&
|
|
Matter.Query.ray(map, this.position, this.playerPosRandomY()).length === 0 &&
|
|
Matter.Query.ray(body, this.position, this.playerPosRandomY()).length === 0
|
|
) {
|
|
//find vertex farthest away from player
|
|
let dist = 0
|
|
for (let i = 0, len = this.vertices.length; i < len; i++) {
|
|
const D = Vector.magnitudeSquared(Vector.sub({ x: this.vertices[i].x, y: this.vertices[i].y }, m.pos))
|
|
if (D > dist) {
|
|
dist = D
|
|
this.swordVertex = i
|
|
}
|
|
}
|
|
this.laserAngle = this.swordVertex / 5 * 2 * Math.PI + 0.6283
|
|
this.sword = this.swordGrow
|
|
this.accelMag = 0
|
|
}
|
|
}
|
|
me.sword = me.swordWaiting //base function that changes during different aspects of the sword swing
|
|
me.swordGrow = function () {
|
|
this.laserSword(this.vertices[this.swordVertex], this.angle + this.laserAngle);
|
|
this.swordRadius += this.swordRadiusGrowRate
|
|
if (this.swordRadius > this.swordRadiusMax || this.isStunned) {
|
|
this.sword = this.swordSlash
|
|
this.spinCount = 0
|
|
}
|
|
}
|
|
me.swordSlash = function () {
|
|
this.laserSword(this.vertices[this.swordVertex], this.angle + this.laserAngle);
|
|
this.torque += this.torqueMagnitude;
|
|
this.spinCount++
|
|
if (this.spinCount > 60 || this.isStunned) {
|
|
this.sword = this.swordWaiting
|
|
this.swordRadius = 0
|
|
this.accelMag = 0.001 * simulation.accelScale;
|
|
this.cd = simulation.cycle + this.delay;
|
|
}
|
|
}
|
|
me.laserSword = function (where, angle) {
|
|
const vertexCollision = function (v1, v1End, domain) {
|
|
for (let i = 0; i < domain.length; ++i) {
|
|
let v = domain[i].vertices;
|
|
const len = v.length - 1;
|
|
for (let j = 0; j < len; j++) {
|
|
results = simulation.checkLineIntersection(v1, v1End, v[j], v[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: v[j], v2: v[j + 1] };
|
|
}
|
|
}
|
|
results = simulation.checkLineIntersection(v1, v1End, v[0], v[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: v[0], v2: v[len] };
|
|
}
|
|
}
|
|
};
|
|
best = { x: null, y: null, dist2: Infinity, who: null, v1: null, v2: null };
|
|
const look = { x: where.x + this.swordRadius * Math.cos(angle), y: where.y + this.swordRadius * Math.sin(angle) };
|
|
vertexCollision(where, look, body); // vertexCollision(where, look, mob);
|
|
vertexCollision(where, look, map);
|
|
if (!m.isCloak) vertexCollision(where, look, [playerBody, playerHead]);
|
|
if (best.who && (best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
|
|
m.immuneCycle = m.cycle + m.collisionImmuneCycles + 60; //player is immune to damage for an extra second
|
|
m.damage(this.swordDamage);
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: best.x,
|
|
y: best.y,
|
|
radius: this.swordDamage * 1500,
|
|
color: "rgba(80,0,255,0.5)",
|
|
time: 20
|
|
});
|
|
}
|
|
if (best.dist2 === Infinity) best = look;
|
|
ctx.beginPath(); //draw beam
|
|
ctx.moveTo(where.x, where.y);
|
|
ctx.lineTo(best.x, best.y);
|
|
ctx.strokeStyle = "rgba(100,100,255,0.1)"; // Purple path
|
|
ctx.lineWidth = 15;
|
|
ctx.stroke();
|
|
ctx.strokeStyle = "rgba(100,100,255,0.5)"; // Purple path
|
|
ctx.lineWidth = 4;
|
|
ctx.setLineDash([70 + 300 * Math.random(), 55 * Math.random()]);
|
|
ctx.stroke(); // Draw it
|
|
ctx.setLineDash([]);
|
|
}
|
|
},
|
|
slasher2(x, y, radius = 33 + Math.ceil(Math.random() * 30)) {
|
|
mobs.spawn(x, y, 6, radius, "rgb(180,199,245)");
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.rotate(me, 2 * Math.PI * Math.random());
|
|
me.accelMag = 0.001 * simulation.accelScale;
|
|
me.torqueMagnitude = -0.000012 * me.inertia //* (Math.random() > 0.5 ? -1 : 1);
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0.035;
|
|
me.delay = 140 * simulation.CDScale;
|
|
me.cd = 0;
|
|
me.swordRadius = 0;
|
|
me.swordVertex = 1
|
|
me.swordRadiusMax = 320 + 3.6 * simulation.difficulty;
|
|
me.swordRadiusGrowRate = me.swordRadiusMax * (0.011 + 0.0002 * simulation.difficulty)
|
|
me.isSlashing = false;
|
|
me.swordDamage = 0.03 * simulation.dmgScale
|
|
me.laserAngle = 3 * Math.PI / 5
|
|
const seeDistance2 = 200000
|
|
spawn.shield(me, x, y);
|
|
me.onDamage = function () { };
|
|
me.do = function () {
|
|
this.checkStatus();
|
|
this.seePlayerByHistory(15);
|
|
this.attraction();
|
|
this.sword() //does various things depending on what stage of the sword swing
|
|
};
|
|
me.swordWaiting = function () {
|
|
if (
|
|
this.seePlayer.recall &&
|
|
this.cd < simulation.cycle &&
|
|
this.distanceToPlayer2() < seeDistance2 &&
|
|
Matter.Query.ray(map, this.position, this.playerPosRandomY()).length === 0 &&
|
|
Matter.Query.ray(body, this.position, this.playerPosRandomY()).length === 0
|
|
) {
|
|
this.laserAngle = -Math.PI / 6
|
|
this.sword = this.swordGrow
|
|
this.accelMag = 0
|
|
}
|
|
}
|
|
me.sword = me.swordWaiting //base function that changes during different aspects of the sword swing
|
|
me.swordGrow = function () {
|
|
this.laserSword(this.vertices[0], this.angle + this.laserAngle);
|
|
this.laserSword(this.vertices[3], this.angle + this.laserAngle + Math.PI);
|
|
this.swordRadius += this.swordRadiusGrowRate
|
|
if (this.swordRadius > this.swordRadiusMax || this.isStunned) {
|
|
this.sword = this.swordSlash
|
|
this.spinCount = 0
|
|
}
|
|
}
|
|
me.swordSlash = function () {
|
|
this.laserSword(this.vertices[0], this.angle + this.laserAngle);
|
|
this.laserSword(this.vertices[3], this.angle + this.laserAngle + Math.PI);
|
|
|
|
this.torque += this.torqueMagnitude;
|
|
this.spinCount++
|
|
if (this.spinCount > 100 || this.isStunned) {
|
|
this.sword = this.swordWaiting
|
|
this.swordRadius = 0
|
|
this.accelMag = 0.001 * simulation.accelScale;
|
|
this.cd = simulation.cycle + this.delay;
|
|
}
|
|
}
|
|
me.laserSword = function (where, angle) {
|
|
const vertexCollision = function (v1, v1End, domain) {
|
|
for (let i = 0; i < domain.length; ++i) {
|
|
let v = domain[i].vertices;
|
|
const len = v.length - 1;
|
|
for (let j = 0; j < len; j++) {
|
|
results = simulation.checkLineIntersection(v1, v1End, v[j], v[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: v[j], v2: v[j + 1] };
|
|
}
|
|
}
|
|
results = simulation.checkLineIntersection(v1, v1End, v[0], v[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: v[0], v2: v[len] };
|
|
}
|
|
}
|
|
};
|
|
best = { x: null, y: null, dist2: Infinity, who: null, v1: null, v2: null };
|
|
const look = { x: where.x + this.swordRadius * Math.cos(angle), y: where.y + this.swordRadius * Math.sin(angle) };
|
|
vertexCollision(where, look, body); // vertexCollision(where, look, mob);
|
|
vertexCollision(where, look, map);
|
|
if (!m.isCloak) vertexCollision(where, look, [playerBody, playerHead]);
|
|
if (best.who && (best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
|
|
m.immuneCycle = m.cycle + m.collisionImmuneCycles + 60; //player is immune to damage for an extra second
|
|
m.damage(this.swordDamage);
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: best.x,
|
|
y: best.y,
|
|
radius: this.swordDamage * 1500,
|
|
color: "rgba(80,0,255,0.5)",
|
|
time: 20
|
|
});
|
|
}
|
|
if (best.dist2 === Infinity) best = look;
|
|
ctx.beginPath(); //draw beam
|
|
ctx.moveTo(where.x, where.y);
|
|
ctx.lineTo(best.x, best.y);
|
|
ctx.strokeStyle = "rgba(100,100,255,0.1)"; // Purple path
|
|
ctx.lineWidth = 15;
|
|
ctx.stroke();
|
|
ctx.strokeStyle = "rgba(100,100,255,0.5)"; // Purple path
|
|
ctx.lineWidth = 4;
|
|
ctx.setLineDash([70 + 300 * Math.random(), 55 * Math.random()]);
|
|
ctx.stroke(); // Draw it
|
|
ctx.setLineDash([]);
|
|
}
|
|
},
|
|
slasher3(x, y, radius = 33 + Math.ceil(Math.random() * 30)) {
|
|
const sides = 6
|
|
mobs.spawn(x, y, sides, radius, "rgb(180,215,235)");
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.rotate(me, 2 * Math.PI * Math.random());
|
|
me.accelMag = 0.00055 * simulation.accelScale;
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0.02;
|
|
me.delay = 150 * simulation.CDScale;
|
|
me.cd = 0;
|
|
me.cycle = 0;
|
|
me.swordVertex = 1
|
|
me.swordRadiusInitial = radius / 2;
|
|
me.swordRadius = me.swordRadiusInitial;
|
|
me.swordRadiusMax = 800 + 6 * simulation.difficulty;
|
|
me.swordRadiusGrowRateInitial = 1.08
|
|
me.swordRadiusGrowRate = me.swordRadiusGrowRateInitial//me.swordRadiusMax * (0.009 + 0.0002 * simulation.difficulty)
|
|
me.isSlashing = false;
|
|
me.swordDamage = 0.03 * simulation.dmgScale
|
|
me.laserAngle = 3 * Math.PI / 5
|
|
const seeDistance2 = me.swordRadiusMax * me.swordRadiusMax
|
|
spawn.shield(me, x, y);
|
|
me.onDamage = function () { };
|
|
me.do = function () {
|
|
this.checkStatus();
|
|
this.seePlayerByHistory(15);
|
|
this.sword() //does various things depending on what stage of the sword swing
|
|
};
|
|
me.swordWaiting = function () {
|
|
this.attraction();
|
|
if (
|
|
this.seePlayer.recall &&
|
|
this.cd < simulation.cycle &&
|
|
this.distanceToPlayer2() < seeDistance2 &&
|
|
Matter.Query.ray(map, this.position, this.playerPosRandomY()).length === 0 &&
|
|
Matter.Query.ray(body, this.position, this.playerPosRandomY()).length === 0
|
|
) {
|
|
//find vertex closest to the player
|
|
let dist = Infinity
|
|
for (let i = 0, len = this.vertices.length; i < len; i++) {
|
|
const D = Vector.magnitudeSquared(Vector.sub({ x: this.vertices[i].x, y: this.vertices[i].y }, m.pos))
|
|
if (D < dist) {
|
|
dist = D
|
|
this.swordVertex = i
|
|
}
|
|
}
|
|
this.laserAngle = this.swordVertex / sides * 2 * Math.PI + Math.PI / sides
|
|
this.sword = this.swordGrow
|
|
this.cycle = 0
|
|
this.swordRadius = this.swordRadiusInitial
|
|
//slow velocity but don't stop
|
|
Matter.Body.setVelocity(this, Vector.mult(this.velocity, 0.5))
|
|
//set angular velocity to 50%
|
|
// Matter.Body.setAngularVelocity(this, this.angularVelocity * 0.5)
|
|
//gently rotate towards the player with a torque, use cross product to decided clockwise or counterclockwise
|
|
const laserStartVector = Vector.sub(this.position, this.vertices[this.swordVertex])
|
|
const playerVector = Vector.sub(this.position, m.pos)
|
|
const cross = Matter.Vector.cross(laserStartVector, playerVector)
|
|
this.torque = 0.00002 * this.inertia * (cross > 0 ? 1 : -1)
|
|
}
|
|
}
|
|
me.sword = me.swordWaiting //base function that changes during different aspects of the sword swing
|
|
me.swordGrow = function () {
|
|
this.laserSpear(this.vertices[this.swordVertex], this.angle + this.laserAngle);
|
|
Matter.Body.setVelocity(this, Vector.mult(this.velocity, 0.9))
|
|
// this.swordRadius += this.swordRadiusGrowRate
|
|
this.cycle++
|
|
// this.swordRadius = this.swordRadiusMax * Math.sin(this.cycle * 0.03)
|
|
this.swordRadius *= this.swordRadiusGrowRate
|
|
|
|
if (this.swordRadius > this.swordRadiusMax) this.swordRadiusGrowRate = 1 / this.swordRadiusGrowRateInitial
|
|
// if (this.swordRadius > this.swordRadiusMax) this.swordRadiusGrowRate = -Math.abs(this.swordRadiusGrowRate)
|
|
if (this.swordRadius < this.swordRadiusInitial || this.isStunned) {
|
|
// this.swordRadiusGrowRate = Math.abs(this.swordRadiusGrowRate)
|
|
this.swordRadiusGrowRate = this.swordRadiusGrowRateInitial
|
|
this.sword = this.swordWaiting
|
|
this.swordRadius = 0
|
|
this.cd = simulation.cycle + this.delay;
|
|
}
|
|
}
|
|
me.laserSpear = function (where, angle) {
|
|
const vertexCollision = function (v1, v1End, domain) {
|
|
for (let i = 0; i < domain.length; ++i) {
|
|
let v = domain[i].vertices;
|
|
const len = v.length - 1;
|
|
for (let j = 0; j < len; j++) {
|
|
results = simulation.checkLineIntersection(v1, v1End, v[j], v[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: v[j], v2: v[j + 1] };
|
|
}
|
|
}
|
|
results = simulation.checkLineIntersection(v1, v1End, v[0], v[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: v[0], v2: v[len] };
|
|
}
|
|
}
|
|
};
|
|
best = { x: null, y: null, dist2: Infinity, who: null, v1: null, v2: null };
|
|
const look = { x: where.x + this.swordRadius * Math.cos(angle), y: where.y + this.swordRadius * Math.sin(angle) };
|
|
vertexCollision(where, look, body); // vertexCollision(where, look, mob);
|
|
vertexCollision(where, look, map);
|
|
if (!m.isCloak) vertexCollision(where, look, [playerBody, playerHead]);
|
|
if (best.who && (best.who === playerBody || best.who === playerHead)) {
|
|
this.swordRadiusGrowRate = 1 / this.swordRadiusGrowRateInitial //!!!! this retracts the sword if it hits the player
|
|
|
|
if (m.immuneCycle < m.cycle) {
|
|
m.immuneCycle = m.cycle + m.collisionImmuneCycles + 60; //player is immune to damage for an extra second
|
|
m.damage(this.swordDamage);
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: best.x,
|
|
y: best.y,
|
|
radius: this.swordDamage * 1500,
|
|
color: "rgba(80,0,255,0.5)",
|
|
time: 20
|
|
});
|
|
}
|
|
}
|
|
if (best.dist2 === Infinity) best = look;
|
|
ctx.beginPath(); //draw beam
|
|
ctx.moveTo(where.x, where.y);
|
|
ctx.lineTo(best.x, best.y);
|
|
ctx.strokeStyle = "rgba(100,100,255,0.1)"; // Purple path
|
|
ctx.lineWidth = 15;
|
|
ctx.stroke();
|
|
ctx.strokeStyle = "rgba(100,100,255,0.5)"; // Purple path
|
|
ctx.lineWidth = 4;
|
|
ctx.setLineDash([70 + 300 * Math.random(), 55 * Math.random()]);
|
|
ctx.stroke(); // Draw it
|
|
ctx.setLineDash([]);
|
|
}
|
|
},
|
|
sneakBoss(x, y, radius = 70) {
|
|
mobs.spawn(x, y, 5, radius, "transparent");
|
|
let me = mob[mob.length - 1];
|
|
Matter.Body.setDensity(me, 0.002); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.isBoss = true;
|
|
me.damageReduction = 0.2 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
|
|
me.accelMag = 0.0017 * Math.sqrt(simulation.accelScale);
|
|
me.frictionAir = 0.01;
|
|
me.g = 0.0001; //required if using this.gravity
|
|
me.stroke = "transparent"; //used for drawSneaker
|
|
me.alpha = 1; //used in drawSneaker
|
|
me.isCloaked = true; //used in drawSneaker
|
|
me.isBadTarget = true;
|
|
me.collisionFilter.mask = cat.map | cat.body | cat.bullet | cat.mob //can't touch player
|
|
me.showHealthBar = false;
|
|
me.memory = 30;
|
|
me.vanishesLeft = Math.ceil(1 + simulation.difficultyMode * 0.5)
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
};
|
|
me.onDamage = function () {
|
|
if (this.vanishesLeft > 0 && this.health < 0.1) { //if health is below 10% teleport to a random spot on player history, heal, and cloak
|
|
this.vanishesLeft--
|
|
|
|
for (let i = 0; i < 8; i++) { //flash screen to hide vanish
|
|
simulation.drawList.push({
|
|
x: this.position.x,
|
|
y: this.position.y,
|
|
radius: 3000,
|
|
color: `rgba(0, 0, 0,${1 - 0.1 * i})`,
|
|
time: (i + 2) * 4
|
|
});
|
|
}
|
|
//teleport to near the end of player history
|
|
const index = Math.floor((m.history.length - 1) * (0.66 + 0.2 * Math.random()))
|
|
let history = m.history[(m.cycle - index) % 600]
|
|
Matter.Body.setPosition(this, history.position)
|
|
Matter.Body.setVelocity(this, { x: 0, y: 0 });
|
|
|
|
this.damageReduction = 0 //immune to harm for the rest of this game cycle
|
|
this.seePlayer.recall = 0
|
|
this.cloak();
|
|
this.health = 1;
|
|
}
|
|
};
|
|
me.cloak = function () {
|
|
if (!this.isCloaked) { //stealth
|
|
this.alpha = 0;
|
|
this.isCloaked = true;
|
|
this.isBadTarget = true;
|
|
this.collisionFilter.mask = cat.map | cat.body | cat.bullet | cat.mob //can't touch player
|
|
this.damageReduction = 0.04 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
}
|
|
}
|
|
me.deCloak = function () {
|
|
if (this.isCloaked) {
|
|
this.damageReduction = 0.4 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
this.isCloaked = false;
|
|
this.isBadTarget = false;
|
|
this.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob; //can touch player
|
|
}
|
|
}
|
|
me.do = function () {
|
|
if (this.damageReduction === 0) {
|
|
this.damageReduction = 0.04 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
let i = this.status.length //clear bad status effects
|
|
while (i--) {
|
|
if (this.status[i].type === "stun" || this.status[i].type === "dot") this.status.splice(i, 1);
|
|
}
|
|
this.isStunned = false;
|
|
}
|
|
this.gravity();
|
|
this.seePlayerByHistory(55);
|
|
this.checkStatus();
|
|
this.attraction();
|
|
//draw
|
|
if (this.seePlayer.recall) {
|
|
if (this.alpha < 1) this.alpha += 0.005 + 0.003 / simulation.CDScale;
|
|
} else {
|
|
if (this.alpha > 0) this.alpha -= 0.04;
|
|
}
|
|
if (this.alpha > 0) {
|
|
if (this.alpha > 0.7) {
|
|
this.healthBar();
|
|
this.deCloak()
|
|
}
|
|
//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 {
|
|
this.cloak()
|
|
}
|
|
};
|
|
},
|
|
sneaker(x, y, radius = 15 + Math.ceil(Math.random() * 10)) {
|
|
mobs.spawn(x, y, 5, radius, "transparent");
|
|
let me = mob[mob.length - 1];
|
|
// Matter.Body.setDensity(me, 0.001); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.accelMag = 0.001 * Math.sqrt(simulation.accelScale);
|
|
me.frictionAir = 0.01;
|
|
me.g = 0.0002; //required if using this.gravity
|
|
me.stroke = "transparent"; //used for drawSneaker
|
|
me.alpha = 1; //used in drawSneaker
|
|
me.isNotCloaked = false; //used in drawSneaker
|
|
me.isBadTarget = true;
|
|
me.collisionFilter.mask = cat.map | cat.body | cat.bullet | cat.mob //can't touch player
|
|
me.showHealthBar = false;
|
|
me.memory = 240;
|
|
me.isVanished = false;
|
|
me.onDamage = function () {
|
|
if (!this.isVanished && this.health < 0.1 && !this.isStunned && !this.isSlowed) { //if health is below 10% teleport to a random spot on player history, heal, and cloak
|
|
this.health = 1;
|
|
this.isVanished = true
|
|
this.cloak();
|
|
//teleport to near the end of player history
|
|
Matter.Body.setPosition(this, m.history[Math.floor((m.history.length - 1) * (0.66 + 0.33 * Math.random()))].position)
|
|
Matter.Body.setVelocity(this, { x: 0, y: 0 });
|
|
this.damageReduction = 0 //immune to harm for the rest of this game cycle
|
|
}
|
|
};
|
|
me.cloak = function () {
|
|
if (this.isNotCloaked) { //stealth
|
|
this.alpha = 0;
|
|
this.isNotCloaked = false;
|
|
this.isBadTarget = true;
|
|
this.collisionFilter.mask = cat.map | cat.body | cat.bullet | cat.mob //can't touch player
|
|
}
|
|
}
|
|
me.do = function () {
|
|
if (this.damageReduction === 0) {
|
|
this.damageReduction = 1 //stop being immune to harm immediately
|
|
let i = this.status.length //clear bad status effects
|
|
while (i--) {
|
|
if (this.status[i].type === "stun" || this.status[i].type === "dot") this.status.splice(i, 1);
|
|
}
|
|
this.isStunned = false;
|
|
}
|
|
this.gravity();
|
|
this.seePlayerByHistory(25);
|
|
this.checkStatus();
|
|
this.attraction();
|
|
//draw
|
|
if (this.seePlayer.recall) {
|
|
if (this.alpha < 1) this.alpha += 0.003 + 0.003 / simulation.CDScale;
|
|
} else {
|
|
if (this.alpha > 0) this.alpha -= 0.03;
|
|
}
|
|
if (this.alpha > 0) {
|
|
if (this.alpha > 0.7) {
|
|
this.healthBar();
|
|
if (!this.isNotCloaked) {
|
|
this.isNotCloaked = true;
|
|
this.isBadTarget = false;
|
|
this.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob; //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 {
|
|
this.cloak()
|
|
}
|
|
};
|
|
},
|
|
ghoster(x, y, radius = 50 + Math.ceil(Math.random() * 90)) {
|
|
mobs.spawn(x, y, 7, radius, "transparent");
|
|
let me = mob[mob.length - 1];
|
|
me.seeAtDistance2 = 300000;
|
|
me.accelMag = 0.00004 + 0.00015 * simulation.accelScale;
|
|
if (map.length) me.searchTarget = map[Math.floor(Math.random() * (map.length - 1))].position; //required for search
|
|
// Matter.Body.setDensity(me, 0.0015); //normal is 0.001
|
|
me.damageReduction = 0.5
|
|
me.stroke = "transparent"; //used for drawGhost
|
|
me.alpha = 1; //used in drawGhost
|
|
me.isNotCloaked = false; //used in drawGhost
|
|
me.isBadTarget = true;
|
|
// me.leaveBody = false;
|
|
me.collisionFilter.mask = cat.bullet //| cat.body
|
|
me.showHealthBar = false;
|
|
me.memory = 600;
|
|
me.do = function () {
|
|
//cap max speed to avoid getting launched by deflection, explosion
|
|
if (this.speed > 7) {
|
|
Matter.Body.setVelocity(this, {
|
|
x: this.velocity.x * 0.8,
|
|
y: this.velocity.y * 0.8
|
|
});
|
|
}
|
|
this.seePlayerCheckByDistance();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
this.search();
|
|
//draw
|
|
if (this.distanceToPlayer2() < this.seeAtDistance2) {
|
|
if (this.alpha < 1) this.alpha += 0.011 * simulation.CDScale; //near player go solid
|
|
} else {
|
|
if (this.alpha > 0) this.alpha -= 0.05; ///away from player, hide
|
|
}
|
|
if (this.alpha > 0) {
|
|
if (this.alpha > 0.7 && this.seePlayer.recall) {
|
|
this.healthBar();
|
|
if (!this.isNotCloaked) {
|
|
this.isNotCloaked = true;
|
|
this.isBadTarget = false;
|
|
this.collisionFilter.mask = cat.player | cat.bullet
|
|
}
|
|
}
|
|
//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.fillStyle = `rgba(255,255,255,${this.alpha * this.alpha})`;
|
|
ctx.fill();
|
|
} else if (this.isNotCloaked) {
|
|
this.isNotCloaked = false;
|
|
this.isBadTarget = true;
|
|
this.collisionFilter.mask = cat.bullet; //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()));
|
|
// // 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.onHit = function () {
|
|
// simulation.timeSkip(120)
|
|
// };
|
|
// me.do = function () {
|
|
// this.seePlayerCheck();
|
|
// this.blink();
|
|
// //strike by expanding
|
|
// // if (this.isBig) {
|
|
// // if (this.cd - this.delay + 15 < simulation.cycle) {
|
|
// // Matter.Body.scale(this, 1 / this.scaleMag, 1 / this.scaleMag);
|
|
// // this.isBig = false;
|
|
// // }
|
|
// // } else
|
|
// if (this.seePlayer.yes && this.cd < simulation.cycle) {
|
|
// const dist = Vector.sub(this.seePlayer.position, this.position);
|
|
// const distMag2 = Vector.magnitudeSquared(dist);
|
|
// if (distMag2 < 80000) {
|
|
// this.cd = simulation.cycle + this.delay;
|
|
|
|
// // Matter.Body.scale(this, this.scaleMag, this.scaleMag);
|
|
// // this.isBig = true;
|
|
// }
|
|
// }
|
|
// };
|
|
// },
|
|
bomberBoss(x, y, radius = 88) {
|
|
//boss that drops bombs from above and holds a set distance from player
|
|
mobs.spawn(x, y, 3, radius, "rgba(255,0,200,0.5)");
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
|
|
Matter.Body.setDensity(me, 0.0025 + 0.00013 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
|
|
me.stroke = "transparent"; //used for drawGhost
|
|
me.seeAtDistance2 = 1500000;
|
|
me.fireFreq = 10 + Math.floor(70 * simulation.CDScale);
|
|
me.searchTarget = map[Math.floor(Math.random() * (map.length - 1))].position; //required for search
|
|
me.hoverElevation = 460 + (Math.random() - 0.5) * 200; //squared
|
|
me.hoverXOff = (Math.random() - 0.5) * 100;
|
|
me.accelMag = Math.floor(10 * (Math.random() + 4.5)) * 0.00001 * simulation.accelScale;
|
|
me.g = 0.0002; //required if using this.gravity // gravity called in hoverOverPlayer
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0.01;
|
|
me.memory = Infinity;
|
|
me.collisionFilter.mask = cat.player | cat.bullet //| cat.body
|
|
spawn.shield(me, x, y, 1);
|
|
|
|
const len = Math.floor(Math.min(15, 3 + Math.sqrt(simulation.difficulty))) // simulation.difficulty = 40 on hard mode level 10
|
|
const speed = (0.007 + 0.003 * Math.random() + 0.004 * Math.sqrt(simulation.difficulty))
|
|
let radiusOrbitals = radius + 125 + 350 * Math.random()
|
|
for (let i = 0; i < len; i++) spawn.orbital(me, radiusOrbitals, i / len * 2 * Math.PI, speed)
|
|
radiusOrbitals = radius + 125 + 350 * Math.random()
|
|
for (let i = 0; i < len; i++) spawn.orbital(me, radiusOrbitals, i / len * 2 * Math.PI, -speed)
|
|
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
};
|
|
me.damageReduction = 0.2 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.do = function () {
|
|
// this.armor();
|
|
this.seePlayerCheckByDistance();
|
|
this.checkStatus();
|
|
if (this.seePlayer.recall) {
|
|
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.clockwiseSort(Matter.Vertices.rotate(me.vertices, Math.PI, me.position)); //make the pointy side of triangle the front
|
|
me.vertices = Matter.Vertices.rotate(me.vertices, Math.PI, me.position); //make the pointy side of triangle the front
|
|
me.isVerticesChange = true
|
|
// Matter.Body.rotate(me, Math.PI)
|
|
|
|
me.memory = 120;
|
|
me.fireFreq = 0.007 + Math.random() * 0.005;
|
|
me.noseLength = 0;
|
|
me.fireAngle = 0;
|
|
me.accelMag = 0.0005 * simulation.accelScale;
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0.05;
|
|
me.lookTorque = 0.0000025 * (Math.random() > 0.5 ? -1 : 1);
|
|
me.fireDir = { x: 0, y: 0 };
|
|
me.onDeath = function () { //helps collisions functions work better after vertex have been changed
|
|
// this.vertices = Matter.Vertices.hull(Matter.Vertices.clockwiseSort(this.vertices))
|
|
}
|
|
// spawn.shield(me, x, y);
|
|
me.do = function () {
|
|
this.seePlayerByLookingAt();
|
|
this.checkStatus();
|
|
this.fire();
|
|
};
|
|
},
|
|
shooterBoss(x, y, radius = 110, isSpawnBossPowerUp = true) {
|
|
mobs.spawn(x, y, 3, radius, "rgb(255,70,180)");
|
|
let me = mob[mob.length - 1];
|
|
setTimeout(() => { //fix mob in place, but allow rotation
|
|
me.constraint = Constraint.create({
|
|
pointA: {
|
|
x: me.position.x,
|
|
y: me.position.y
|
|
},
|
|
bodyB: me,
|
|
stiffness: 0.00004,
|
|
damping: 0.2
|
|
});
|
|
Composite.add(engine.world, me.constraint);
|
|
}, 2000); //add in a delay in case the level gets flipped left right
|
|
|
|
me.isBoss = true;
|
|
Matter.Body.setDensity(me, 0.01 + 0.0004 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.damageReduction = 0.2 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
|
|
me.vertices = Matter.Vertices.rotate(me.vertices, Math.PI, me.position); //make the pointy side of triangle the front
|
|
me.isVerticesChange = true
|
|
me.memory = 240;
|
|
me.fireFreq = 0.01 + 0.0005 * Math.min(40, simulation.difficulty); //bigger number means more shots per second
|
|
me.noseLength = 0;
|
|
me.fireAngle = 0;
|
|
me.accelMag = 0.005 * simulation.accelScale;
|
|
me.frictionAir = 0.05;
|
|
me.lookTorque = 0.000006 * (Math.random() > 0.5 ? -1 : 1);
|
|
me.fireDir = { x: 0, y: 0 };
|
|
setTimeout(() => {
|
|
for (let i = 0, len = 3 + 0.5 * Math.sqrt(simulation.difficulty); i < len; i++) spawn.spawnOrbitals(me, radius + 40 + 10 * i, 1);
|
|
}, 100); //have to wait a sec so the tether constraint doesn't attach to an orbital
|
|
me.onDeath = function () {
|
|
if (isSpawnBossPowerUp) powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
// this.vertices = Matter.Vertices.hull(Matter.Vertices.clockwiseSort(this.vertices)) //helps collisions functions work better after vertex have been changed
|
|
};
|
|
me.do = function () {
|
|
// this.armor();
|
|
this.seePlayerByLookingAt();
|
|
this.checkStatus();
|
|
// this.fire();
|
|
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 (!(simulation.cycle % this.seePlayerFreq)) {
|
|
this.fireDir = Vector.normalise(Vector.sub(this.seePlayer.position, this.position));
|
|
this.fireDir.y -= Math.abs(this.seePlayer.position.x - this.position.x) / 5000; //gives the bullet an arc //was / 1600
|
|
}
|
|
//rotate towards fireAngle
|
|
const angle = this.angle + Math.PI / 2;
|
|
const dot = Vector.dot({ x: Math.cos(angle), y: Math.sin(angle) }, this.fireDir)
|
|
// c = Math.cos(angle) * this.fireDir.x + Math.sin(angle) * this.fireDir.y;
|
|
const threshold = 0.1;
|
|
if (dot > threshold) {
|
|
this.torque += 0.000004 * this.inertia;
|
|
} else if (dot < -threshold) {
|
|
this.torque -= 0.000004 * this.inertia;
|
|
} else if (this.noseLength > 1.5 && dot > -0.2 && dot < 0.2) {
|
|
//fire
|
|
for (let i = 0, len = 2 + 0.07 * simulation.difficulty; i < len; i++) {
|
|
spawn.bullet(this.vertices[1].x, this.vertices[1].y, 10 + Math.ceil(this.radius / 25));
|
|
const spread = Vector.rotate({ x: Math.sqrt(len) + 4, y: 0 }, 2 * Math.PI * Math.random())
|
|
const dir = Vector.add(Vector.mult(this.fireDir, 25), spread)
|
|
Matter.Body.setVelocity(mob[mob.length - 1], dir);
|
|
}
|
|
this.noseLength = 0;
|
|
}
|
|
if (this.noseLength < 1.5) this.noseLength += this.fireFreq;
|
|
setNoseShape();
|
|
} else if (this.noseLength > 0.1) {
|
|
this.noseLength -= this.fireFreq / 2;
|
|
setNoseShape();
|
|
}
|
|
};
|
|
},
|
|
bullet(x, y, radius = 9, 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(this.mass * 15);
|
|
};
|
|
Matter.Body.setDensity(me, 0.00004); //normal is 0.001
|
|
me.timeLeft = 220;
|
|
me.g = 0.001; //required if using this.gravity
|
|
me.frictionAir = 0;
|
|
me.restitution = 0.8;
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.isBadTarget = true;
|
|
me.isMobBullet = true;
|
|
me.showHealthBar = false;
|
|
me.collisionFilter.category = cat.mobBullet;
|
|
me.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet;
|
|
me.do = function () {
|
|
this.gravity();
|
|
this.timeLimit();
|
|
};
|
|
},
|
|
bomb(x, y, radius = 9, sides = 5) {
|
|
mobs.spawn(x, y, sides, radius, "rgb(255,0,0)");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent";
|
|
me.onHit = function () {
|
|
this.explode(this.mass * 120);
|
|
};
|
|
me.onDeath = function () {
|
|
spawn.bullet(this.position.x, this.position.y, this.radius / 3, 5);
|
|
spawn.bullet(this.position.x, this.position.y, this.radius / 3, 5);
|
|
spawn.bullet(this.position.x, this.position.y, this.radius / 3, 5);
|
|
const mag = 8
|
|
const v1 = Vector.rotate({
|
|
x: 1,
|
|
y: 1
|
|
}, 2 * Math.PI * Math.random())
|
|
const v2 = Vector.rotate({
|
|
x: 1,
|
|
y: 1
|
|
}, 2 * Math.PI * Math.random())
|
|
const v3 = Vector.normalise(Vector.add(v1, v2)) //last vector is opposite the sum of the other two to look a bit like momentum is conserved
|
|
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: mag * v1.x,
|
|
y: mag * v1.y
|
|
});
|
|
Matter.Body.setVelocity(mob[mob.length - 2], {
|
|
x: mag * v2.x,
|
|
y: mag * v2.y
|
|
});
|
|
Matter.Body.setVelocity(mob[mob.length - 3], {
|
|
x: -mag * v3.x,
|
|
y: -mag * v3.y
|
|
});
|
|
}
|
|
Matter.Body.setDensity(me, 0.00005); //normal is 0.001
|
|
me.timeLeft = 140 + Math.floor(Math.random() * 30);
|
|
me.g = 0.001; //required if using this.gravity
|
|
me.frictionAir = 0;
|
|
me.restitution = 1;
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.isBadTarget = true;
|
|
me.isMobBullet = true;
|
|
me.showHealthBar = false;
|
|
me.collisionFilter.category = cat.mobBullet;
|
|
me.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet;
|
|
me.do = function () {
|
|
this.gravity();
|
|
this.timeLimit();
|
|
};
|
|
},
|
|
sniper(x, y, radius = 35 + Math.ceil(Math.random() * 30)) {
|
|
mobs.spawn(x, y, 3, radius, "transparent"); //"rgb(25,0,50)")
|
|
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.isVerticesChange = true
|
|
// Matter.Body.rotate(me, Math.PI)
|
|
me.stroke = "transparent"; //used for drawSneaker
|
|
me.alpha = 1; //used in drawSneaker
|
|
me.showHealthBar = false;
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.isNotCloaked = false; //used in drawSneaker
|
|
me.isBadTarget = true;
|
|
me.collisionFilter.mask = cat.map | cat.body | cat.bullet | cat.mob //can't touch player
|
|
|
|
me.memory = 30 //140;
|
|
me.fireFreq = 0.005 + Math.random() * 0.002 + 0.0005 * simulation.difficulty; //larger = fire more often
|
|
me.noseLength = 0;
|
|
me.fireAngle = 0;
|
|
me.accelMag = 0.0005 * simulation.accelScale;
|
|
me.frictionAir = 0.05;
|
|
me.torque = 0.0001 * me.inertia;
|
|
me.fireDir = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
me.onDeath = function () { //helps collisions functions work better after vertex have been changed
|
|
// this.vertices = Matter.Vertices.hull(Matter.Vertices.clockwiseSort(this.vertices))
|
|
}
|
|
// spawn.shield(me, x, y);
|
|
me.do = function () {
|
|
// this.seePlayerByLookingAt();
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
|
|
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 (!(simulation.cycle % this.seePlayerFreq)) {
|
|
this.fireDir = Vector.normalise(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
|
|
}
|
|
//rotate towards fireAngle
|
|
const angle = this.angle + Math.PI / 2;
|
|
// c = Math.cos(angle) * this.fireDir.x + Math.sin(angle) * this.fireDir.y;
|
|
//rotate towards fireAngle
|
|
const dot = Vector.dot({
|
|
x: Math.cos(angle),
|
|
y: Math.sin(angle)
|
|
}, this.fireDir)
|
|
const threshold = 0.03;
|
|
if (dot > threshold) {
|
|
this.torque += 0.000004 * this.inertia;
|
|
} else if (dot < -threshold) {
|
|
this.torque -= 0.000004 * this.inertia;
|
|
} else if (this.noseLength > 1.5 && dot > -0.2 && dot < 0.2) {
|
|
//fire
|
|
spawn.sniperBullet(this.vertices[1].x, this.vertices[1].y, 7 + Math.ceil(this.radius / 15), 5);
|
|
const v = 10 + 8 * simulation.accelScale;
|
|
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();
|
|
// }
|
|
|
|
if (this.seePlayer.recall) {
|
|
if (this.alpha < 1) this.alpha += 0.01;
|
|
} else {
|
|
if (this.alpha > 0) this.alpha -= 0.03;
|
|
}
|
|
//draw
|
|
if (this.alpha > 0) {
|
|
if (this.alpha > 0.95) {
|
|
this.healthBar();
|
|
if (!this.isNotCloaked) {
|
|
this.isNotCloaked = true;
|
|
this.isBadTarget = false;
|
|
this.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob; //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(25,0,50,${this.alpha * this.alpha})`;
|
|
ctx.fill();
|
|
} else if (this.isNotCloaked) {
|
|
this.isNotCloaked = false;
|
|
this.isBadTarget = true
|
|
this.collisionFilter.mask = cat.map | cat.body | cat.bullet | cat.mob //can't touch player
|
|
}
|
|
};
|
|
},
|
|
sniperBullet(x, y, radius = 9, sides = 5) { //bullets
|
|
mobs.spawn(x, y, sides, radius, "rgb(255,0,155)");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent";
|
|
me.onHit = function () {
|
|
this.explode(this.mass * 20);
|
|
};
|
|
Matter.Body.setDensity(me, 0.00005); //normal is 0.001
|
|
me.timeLeft = 120;
|
|
// me.g = 0.0005; //required if using this.gravity
|
|
me.frictionAir = 0;
|
|
me.restitution = 0;
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.isBadTarget = true;
|
|
me.isMobBullet = true;
|
|
me.showHealthBar = false;
|
|
me.collisionFilter.category = cat.mobBullet;
|
|
me.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet;
|
|
me.onDeath = function () {
|
|
if (simulation.difficulty > 11) { //explode AoE
|
|
const radius = 100 + 0.5 * simulation.difficulty + 50 * Math.random()
|
|
if (m.immuneCycle < m.cycle && Vector.magnitude(Vector.sub(this.position, player.position)) < radius) m.damage(0.0003 * radius * simulation.dmgScale);
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: this.position.x,
|
|
y: this.position.y,
|
|
radius: radius,
|
|
color: "rgba(255,0,155,0.5)",
|
|
time: simulation.drawTime
|
|
});
|
|
}
|
|
};
|
|
me.do = function () {
|
|
// this.gravity();
|
|
this.timeLimit();
|
|
if (Matter.Query.collides(this, map).length > 0 || Matter.Query.collides(this, body).length > 0 && this.speed < 10) {
|
|
this.isDropPowerUp = false;
|
|
this.death(); //death with no power up
|
|
}
|
|
};
|
|
},
|
|
launcherOne(x, y, radius = 30 + Math.ceil(Math.random() * 40)) {
|
|
mobs.spawn(x, y, 3, radius, "rgb(150,150,255)");
|
|
let me = mob[mob.length - 1];
|
|
me.accelMag = 0.00004 * simulation.accelScale;
|
|
me.fireFreq = Math.floor(420 + 90 * Math.random() * simulation.CDScale)
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0.015;
|
|
spawn.shield(me, x, y);
|
|
me.onDamage = function () { };
|
|
me.do = function () {
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
if (this.seePlayer.recall && !(simulation.cycle % this.fireFreq)) {
|
|
Matter.Body.setAngularVelocity(this, 0.14)
|
|
spawn.seeker(this.vertices[0].x, this.vertices[0].y, 20, 9); //give the bullet a rotational velocity as if they were attached to a vertex
|
|
const who = mob[mob.length - 1]
|
|
Matter.Body.setDensity(who, 0.00003); //normal is 0.001
|
|
who.timeLeft = 840 //* (0.8 + 0.4 * Math.random());
|
|
who.accelMag = 0.00035 * simulation.accelScale; //* (0.8 + 0.4 * Math.random())
|
|
who.frictionAir = 0.01 //* (0.8 + 0.4 * Math.random());
|
|
const velocity = Vector.mult(Vector.perp(Vector.normalise(Vector.sub(this.position, this.vertices[0]))), -6)
|
|
Matter.Body.setVelocity(who, {
|
|
x: this.velocity.x + velocity.x,
|
|
y: this.velocity.y + velocity.y
|
|
});
|
|
}
|
|
};
|
|
},
|
|
launcher(x, y, radius = 30 + Math.ceil(Math.random() * 40)) {
|
|
mobs.spawn(x, y, 3, radius, "rgb(150,150,255)");
|
|
let me = mob[mob.length - 1];
|
|
me.accelMag = 0.00004 * simulation.accelScale;
|
|
me.fireFreq = Math.floor(420 + 90 * Math.random() * simulation.CDScale)
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0.02;
|
|
spawn.shield(me, x, y);
|
|
me.onDamage = function () { };
|
|
me.do = function () {
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
if (this.seePlayer.recall && !(simulation.cycle % this.fireFreq)) {
|
|
Matter.Body.setAngularVelocity(this, 0.14)
|
|
//fire a bullet from each vertex
|
|
for (let i = 0, len = this.vertices.length; i < len; i++) {
|
|
spawn.seeker(this.vertices[i].x, this.vertices[i].y, 7)
|
|
//give the bullet a rotational velocity as if they were attached to a vertex
|
|
const velocity = Vector.mult(Vector.perp(Vector.normalise(Vector.sub(this.position, this.vertices[i]))), -8)
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: this.velocity.x + velocity.x,
|
|
y: this.velocity.y + velocity.y
|
|
});
|
|
}
|
|
}
|
|
};
|
|
},
|
|
launcherBoss(x, y, radius = 90, isSpawnBossPowerUp = true) {
|
|
mobs.spawn(x, y, 6, radius, "rgb(150,150,255)");
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
Matter.Body.setDensity(me, 0.0022 + 0.0002 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.damageReduction = 0.2 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
|
|
me.accelMag = 0.0001 * simulation.accelScale;
|
|
me.fireFreq = Math.floor(330 * simulation.CDScale)
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0.02;
|
|
me.memory = 420;
|
|
me.repulsionRange = 1000000; //squared
|
|
spawn.shield(me, x, y, 1);
|
|
spawn.spawnOrbitals(me, radius + 50 + 200 * Math.random())
|
|
|
|
me.onDeath = function () {
|
|
if (isSpawnBossPowerUp) powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
// this.vertices = Matter.Vertices.hull(Matter.Vertices.clockwiseSort(this.vertices)) //helps collisions functions work better after vertex have been changed
|
|
};
|
|
me.onDamage = function () { };
|
|
me.do = function () {
|
|
// this.armor();
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
this.repulsion();
|
|
if (this.seePlayer.recall && !(simulation.cycle % this.fireFreq)) {
|
|
Matter.Body.setAngularVelocity(this, 0.11)
|
|
//fire a bullet from each vertex
|
|
for (let i = 0, len = this.vertices.length; i < len; i++) {
|
|
spawn.seeker(this.vertices[i].x, this.vertices[i].y, 8)
|
|
//give the bullet a rotational velocity as if they were attached to a vertex
|
|
const velocity = Vector.mult(Vector.perp(Vector.normalise(Vector.sub(this.position, this.vertices[i]))), -10)
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: this.velocity.x + velocity.x,
|
|
y: this.velocity.y + velocity.y
|
|
});
|
|
}
|
|
}
|
|
};
|
|
},
|
|
// blowSuckBoss(x, y, radius = 80) {
|
|
// mobs.spawn(x, y, 10, radius, "transparent");
|
|
// let me = mob[mob.length - 1];
|
|
// me.isBoss = true;
|
|
// Matter.Body.setDensity(me, 0.0022 + 0.0002 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
// me.damageReduction = 0.2 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
|
|
// me.fireFreq = Math.floor(60 * simulation.CDScale)
|
|
// me.seePlayerFreq = 15
|
|
// me.seeAtDistance2 = 300000;
|
|
// me.accelMag = 0.0003 * simulation.accelScale;
|
|
// if (map.length) me.searchTarget = map[Math.floor(Math.random() * (map.length - 1))].position; //required for search
|
|
// // Matter.Body.setDensity(me, 0.001); //normal is 0.001 //makes effective life much lower
|
|
// me.stroke = "transparent"; //used for drawGhost
|
|
// me.alpha = 1; //used in drawGhost
|
|
// me.isNotCloaked = false; //used in drawGhost
|
|
// me.isBadTarget = true;
|
|
// // me.leaveBody = false;
|
|
// me.collisionFilter.mask = cat.bullet //| cat.body
|
|
// me.showHealthBar = false;
|
|
// me.memory = 480;
|
|
// me.onDeath = function() {
|
|
// powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
// };
|
|
// me.onDamage = function() {};
|
|
// me.suck = function() {
|
|
// for (let i = 0; i < mob.length; i++) {
|
|
// if (mob[i].isBlowSuckBullet) {
|
|
// const unit = Vector.normalise(Vector.sub(this.position, mob[i].position))
|
|
// const mag = Vector.mult(unit, 0.0008 * mob[i].mass)
|
|
// mob[i].force.x += mag.x
|
|
// mob[i].force.y += mag.y
|
|
// }
|
|
// }
|
|
// }
|
|
// me.do = function() {
|
|
// //cap max speed
|
|
// if (this.speed > 8) {
|
|
// Matter.Body.setVelocity(this, {
|
|
// x: this.velocity.x * 0.8,
|
|
// y: this.velocity.y * 0.8
|
|
// });
|
|
// }
|
|
// this.seePlayerCheckByDistance();
|
|
// this.checkStatus();
|
|
// this.attraction();
|
|
// this.search();
|
|
// //draw
|
|
// if (this.distanceToPlayer2() < this.seeAtDistance2) {
|
|
// if (this.alpha < 1) this.alpha += 0.005 * simulation.CDScale; //near player go solid
|
|
// } else {
|
|
// if (this.alpha > 0) this.alpha -= 0.05; ///away from player, hide
|
|
// }
|
|
// if (this.alpha > 0) {
|
|
// if (this.alpha > 0.8 && this.seePlayer.recall) {
|
|
// this.healthBar();
|
|
// if (!this.isNotCloaked) {
|
|
// this.isNotCloaked = true;
|
|
// this.isBadTarget = false;
|
|
// this.collisionFilter.mask = cat.player | cat.bullet
|
|
// }
|
|
// }
|
|
// //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.fillStyle = `rgba(255,255,255,${this.alpha * this.alpha})`;
|
|
// ctx.fill();
|
|
|
|
|
|
// this.suck();
|
|
// if (this.seePlayer.recall && !(simulation.cycle % this.fireFreq)) {
|
|
// this.suckCount = 0;
|
|
// Matter.Body.setAngularVelocity(this, 0.11)
|
|
// //fire a bullet from each vertex
|
|
// for (let i = 0, len = this.vertices.length; i < len; i++) {
|
|
// spawn.bounceBullet(this.vertices[i].x, this.vertices[i].y, 8)
|
|
// //give the bullet a rotational velocity as if they were attached to a vertex
|
|
// const velocity = Vector.mult(Vector.perp(Vector.normalise(Vector.sub(this.position, this.vertices[i]))), -16)
|
|
// const who = mob[mob.length - 1]
|
|
// who.isBlowSuckBullet = true
|
|
// Matter.Body.setVelocity(who, {
|
|
// x: this.velocity.x + velocity.x,
|
|
// y: this.velocity.y + velocity.y
|
|
// });
|
|
// }
|
|
// }
|
|
|
|
|
|
// } else if (this.isNotCloaked) {
|
|
// this.isNotCloaked = false;
|
|
// this.isBadTarget = true;
|
|
// this.collisionFilter.mask = cat.bullet; //can't touch player or walls
|
|
// }
|
|
// };
|
|
// },
|
|
// blowSuckBoss(x, y, radius = 80, isSpawnBossPowerUp = true) {
|
|
// mobs.spawn(x, y, 10, radius, "transparent"); //rgb(60,60,85)
|
|
// let me = mob[mob.length - 1];
|
|
// me.isBoss = true;
|
|
// Matter.Body.setDensity(me, 0.0022 + 0.0002 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
// me.damageReduction = 0.2 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
|
|
// me.accelMag = 0.0001 * simulation.accelScale;
|
|
// me.fireFreq = Math.floor(180 * simulation.CDScale)
|
|
// me.frictionStatic = 0;
|
|
// me.friction = 0;
|
|
// me.frictionAir = 0.005;
|
|
// me.memory = 420;
|
|
// me.repulsionRange = 1000000; //squared
|
|
// // spawn.shield(me, x, y, 1);
|
|
// // spawn.spawnOrbitals(me, radius + 50 + 200 * Math.random())
|
|
|
|
// me.onDeath = function() {
|
|
// if (isSpawnBossPowerUp) powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
// };
|
|
// me.onDamage = function() {};
|
|
// me.suck = function() {
|
|
// for (let i = 0; i < mob.length; i++) {
|
|
// if (mob[i].isBlowSuckBullet) {
|
|
// const unit = Vector.normalise(Vector.sub(this.position, mob[i].position))
|
|
// const mag = Vector.mult(unit, 0.0008 * mob[i].mass)
|
|
// mob[i].force.x += mag.x
|
|
// mob[i].force.y += mag.y
|
|
// }
|
|
// }
|
|
// }
|
|
// me.do = function() {
|
|
// this.seePlayerCheck();
|
|
// this.checkStatus();
|
|
// this.attraction();
|
|
// // this.repulsion();
|
|
// this.suck();
|
|
|
|
// if (this.seePlayer.recall && !(simulation.cycle % this.fireFreq)) {
|
|
// this.suckCount = 0;
|
|
// Matter.Body.setAngularVelocity(this, 0.11)
|
|
// //fire a bullet from each vertex
|
|
// for (let i = 0, len = this.vertices.length; i < len; i++) {
|
|
// spawn.bounceBullet(this.vertices[i].x, this.vertices[i].y, 8)
|
|
// //give the bullet a rotational velocity as if they were attached to a vertex
|
|
// const velocity = Vector.mult(Vector.perp(Vector.normalise(Vector.sub(this.position, this.vertices[i]))), -16)
|
|
// const who = mob[mob.length - 1]
|
|
// who.isBlowSuckBullet = true
|
|
// Matter.Body.setVelocity(who, {
|
|
// x: this.velocity.x + velocity.x,
|
|
// y: this.velocity.y + velocity.y
|
|
// });
|
|
// }
|
|
// }
|
|
// };
|
|
// },
|
|
grenadierBoss(x, y, radius = 95) {
|
|
mobs.spawn(x, y, 6, radius, "rgb(0,235,255)");
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
|
|
me.accelMag = 0.0001 * simulation.accelScale;
|
|
me.fireFreq = Math.floor(360 * simulation.CDScale)
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0.035;
|
|
me.memory = 420;
|
|
me.repulsionRange = 1200000; //squared
|
|
// spawn.shield(me, x, y, 1);
|
|
spawn.spawnOrbitals(me, radius + 50, 1);
|
|
spawn.spawnOrbitals(me, radius + 125, 1);
|
|
spawn.spawnOrbitals(me, radius + 200, 1);
|
|
Matter.Body.setDensity(me, 0.004 + 0.0002 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.onDeath = function () { //helps collisions functions work better after vertex have been changed
|
|
setTimeout(() => { //fix mob in place, but allow rotation
|
|
for (let i = 0, len = 6; i < len; i++) {
|
|
const speed = 2.25 * simulation.accelScale;
|
|
const angle = 2 * Math.PI * i / len
|
|
spawn.grenade(this.position.x, this.position.y, 170 * simulation.CDScale);
|
|
const who = mob[mob.length - 1]
|
|
Matter.Body.setVelocity(who, {
|
|
x: speed * Math.cos(angle),
|
|
y: speed * Math.sin(angle)
|
|
});
|
|
}
|
|
}, 200);
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
}
|
|
me.grenadeLimiter = 0
|
|
me.onDamage = function () {
|
|
if (this.grenadeLimiter < 240 && this.health > 0) {
|
|
this.grenadeLimiter += 60
|
|
spawn.grenade(this.position.x, this.position.y, 80 + Math.floor(60 * Math.random()));
|
|
who = mob[mob.length - 1]
|
|
const velocity = Vector.mult(Vector.normalise(Vector.sub(player.position, who.position)), 3 * Math.sqrt(simulation.accelScale) + 4 * Math.random())
|
|
Matter.Body.setVelocity(who, {
|
|
x: this.velocity.x + velocity.x,
|
|
y: this.velocity.y + velocity.y
|
|
});
|
|
}
|
|
};
|
|
me.damageReduction = 0.25 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.do = function () {
|
|
// this.armor();
|
|
if (this.grenadeLimiter > 1) this.grenadeLimiter--
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
};
|
|
},
|
|
grenadier(x, y, radius = 35 + Math.ceil(Math.random() * 20)) {
|
|
mobs.spawn(x, y, 3, radius, "rgb(0,235,255)"); //rgb(255,100,200)
|
|
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.isVerticesChange = true
|
|
// Matter.Body.rotate(me, Math.PI)
|
|
// me.stroke = "transparent"; //used for drawSneaker
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.memory = 60 //140;
|
|
me.fireFreq = 0.0055 + Math.random() * 0.0015;
|
|
me.noseLength = 0;
|
|
me.fireAngle = 0;
|
|
me.accelMag = 0.0006 * simulation.accelScale;
|
|
me.frictionAir = 0.05;
|
|
me.torque = 0.0001 * me.inertia * (Math.random() > 0.5 ? -1 : 1)
|
|
me.fireDir = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
me.onDeath = function () { //helps collisions functions work better after vertex have been changed
|
|
spawn.grenade(this.position.x, this.position.y, 200 * simulation.CDScale);
|
|
// mob[mob.length - 1].collisionFilter.category = 0
|
|
mob[mob.length - 1].collisionFilter.mask = cat.player | cat.map;
|
|
}
|
|
// spawn.shield(me, x, y);
|
|
me.do = function () {
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
|
|
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 (!(simulation.cycle % this.seePlayerFreq)) {
|
|
this.fireDir = Vector.normalise(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
|
|
}
|
|
//rotate towards fireAngle
|
|
const angle = this.angle + Math.PI / 2;
|
|
// c = Math.cos(angle) * this.fireDir.x + Math.sin(angle) * this.fireDir.y;
|
|
//rotate towards fireAngle
|
|
const dot = Vector.dot({
|
|
x: Math.cos(angle),
|
|
y: Math.sin(angle)
|
|
}, this.fireDir)
|
|
const threshold = 0.03;
|
|
if (dot > threshold) {
|
|
this.torque += 0.000004 * this.inertia;
|
|
} else if (dot < -threshold) {
|
|
this.torque -= 0.000004 * this.inertia;
|
|
} else if (this.noseLength > 1.5 && dot > -0.2 && dot < 0.2) {
|
|
//fire
|
|
spawn.grenade(this.vertices[1].x, this.vertices[1].y);
|
|
const v = 5 * simulation.accelScale;
|
|
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();
|
|
}
|
|
};
|
|
},
|
|
grenade(x, y, lifeSpan = 90 + Math.ceil(60 / simulation.accelScale), pulseRadius = Math.min(550, 250 + simulation.difficulty * 3), size = 3) {
|
|
mobs.spawn(x, y, 4, size, "rgb(215,0,190)"); //rgb(215,80,190)
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent";
|
|
me.onHit = function () {
|
|
this.explode(this.mass);
|
|
};
|
|
Matter.Body.setDensity(me, 0.00004); //normal is 0.001
|
|
|
|
me.lifeSpan = lifeSpan;
|
|
me.timeLeft = me.lifeSpan;
|
|
// me.g = 0.0002; //required if using this.gravity
|
|
me.frictionAir = 0;
|
|
me.restitution = 0.8;
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.isBadTarget = true;
|
|
me.isMobBullet = true;
|
|
me.onDeath = function () {
|
|
//damage player if in range
|
|
if (Vector.magnitude(Vector.sub(player.position, this.position)) < pulseRadius && m.immuneCycle < m.cycle) {
|
|
m.immuneCycle = m.cycle + m.collisionImmuneCycles; //player is immune to damage
|
|
m.damage(0.015 * simulation.dmgScale);
|
|
}
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: this.position.x,
|
|
y: this.position.y,
|
|
radius: pulseRadius,
|
|
color: "rgba(255,0,220,0.3)",
|
|
time: simulation.drawTime
|
|
});
|
|
};
|
|
me.showHealthBar = false;
|
|
me.collisionFilter.category = cat.mobBullet;
|
|
me.collisionFilter.mask = cat.map | cat.body | cat.player
|
|
// me.collisionFilter.mask = 0
|
|
me.do = function () {
|
|
this.timeLimit();
|
|
ctx.beginPath(); //draw explosion outline
|
|
ctx.arc(this.position.x, this.position.y, pulseRadius * (1.01 - this.timeLeft / this.lifeSpan), 0, 2 * Math.PI); //* this.fireCycle / this.fireDelay
|
|
ctx.fillStyle = "rgba(255,0,220,0.05)";
|
|
ctx.fill();
|
|
};
|
|
},
|
|
shieldingBoss(x, y, radius = 200) {
|
|
mobs.spawn(x, y, 9, radius, "rgb(150, 150, 255)");
|
|
let me = mob[mob.length - 1];
|
|
setTimeout(() => { //fix mob in place, but allow rotation
|
|
me.constraint = Constraint.create({
|
|
pointA: {
|
|
x: me.position.x,
|
|
y: me.position.y
|
|
},
|
|
bodyB: me,
|
|
stiffness: 0.0001,
|
|
damping: 1
|
|
});
|
|
Composite.add(engine.world, me.constraint);
|
|
}, 2000); //add in a delay in case the level gets flipped left right
|
|
|
|
Matter.Body.rotate(me, Math.random() * 2 * Math.PI)
|
|
// me.stroke = "rgb(220,220,255)"
|
|
me.isBoss = true;
|
|
me.cycle = 0
|
|
me.maxCycles = 110;
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 1;
|
|
// me.homePosition = { x: x, y: y };
|
|
spawn.shield(me, x, y, 1);
|
|
spawn.spawnOrbitals(me, radius + 50 + 200 * Math.random())
|
|
|
|
Matter.Body.setDensity(me, 0.0045); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
// this.vertices = Matter.Vertices.hull(Matter.Vertices.clockwiseSort(this.vertices)) //helps collisions functions work better after vertex have been changed
|
|
};
|
|
me.onDamage = function () {
|
|
this.cycle = 0
|
|
};
|
|
me.damageReduction = 0.35 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.do = function () {
|
|
Matter.Body.rotate(this, 0.003) //gently spin around
|
|
this.checkStatus();
|
|
ctx.beginPath(); //draw cycle timer
|
|
ctx.moveTo(this.vertices[this.vertices.length - 1].x, this.vertices[this.vertices.length - 1].y)
|
|
const phase = (this.vertices.length + 1) * this.cycle / this.maxCycles
|
|
if (phase > 1) ctx.lineTo(this.vertices[0].x, this.vertices[0].y)
|
|
for (let i = 1; i < phase - 1; i++) {
|
|
ctx.lineTo(this.vertices[i].x, this.vertices[i].y)
|
|
}
|
|
ctx.lineWidth = 5
|
|
ctx.strokeStyle = "rgb(255,255,255)"
|
|
ctx.stroke();
|
|
|
|
this.cycle++
|
|
if (this.cycle > this.maxCycles) {
|
|
this.cycle = 0
|
|
ctx.beginPath();
|
|
for (let i = 0; i < mob.length; i++) {
|
|
if (!mob[i].isShielded && !mob[i].shield && mob[i].isDropPowerUp && mob[i].alive && !mob[i].isBoss) {
|
|
ctx.moveTo(this.position.x, this.position.y)
|
|
ctx.lineTo(mob[i].position.x, mob[i].position.y)
|
|
spawn.shield(mob[i], mob[i].position.x, mob[i].position.y, 1, true);
|
|
// me.damageReduction = 0.075 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
mob[mob.length - 1].damageReduction = 0.5 * 0.075 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1) //shields are extra strong
|
|
}
|
|
}
|
|
if (!this.isShielded && this.alive) spawn.shield(this, this.position.x, this.position.y, 1, true);
|
|
ctx.lineWidth = 20
|
|
ctx.strokeStyle = "rgb(200,200,255)"
|
|
ctx.stroke();
|
|
}
|
|
};
|
|
},
|
|
timeSkipBoss(x, y, radius = 50) {
|
|
mobs.spawn(x, y, 15, radius, "transparent");
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
me.eventHorizon = 0; //set in mob loop
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0.005;
|
|
me.accelMag = 0.00008 + 0.00002 * simulation.accelScale
|
|
spawn.shield(me, x, y, 1);
|
|
spawn.spawnOrbitals(me, radius + 50 + 100 * Math.random())
|
|
|
|
Matter.Body.setDensity(me, 0.0025); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.damageReduction = 0.07 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.startingDamageReduction = me.damageReduction
|
|
me.isInvulnerable = false
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
// requestAnimationFrame(() => { simulation.timePlayerSkip(120) }); //wrapping in animation frame prevents errors, probably
|
|
};
|
|
me.onDamage = function () {
|
|
//find side of mob closest to player
|
|
//causes lag for foam,laser too many seekers //maybe scale chance with dmg
|
|
// const where = Vector.add(this.position, Vector.mult(Vector.normalise(Vector.sub(m.pos, this.position)), this.radius + 10))
|
|
// spawn.seeker(where.x, where.y); //give the bullet a rotational velocity as if they were attached to a vertex
|
|
};
|
|
me.do = function () {
|
|
this.seePlayerByHistory(60);
|
|
this.attraction();
|
|
this.checkStatus();
|
|
this.eventHorizon = 950 + 250 * Math.sin(simulation.cycle * 0.005)
|
|
if (!simulation.isTimeSkipping) {
|
|
if (Vector.magnitude(Vector.sub(this.position, m.pos)) < this.eventHorizon) {
|
|
this.attraction();
|
|
this.damageReduction = this.startingDamageReduction
|
|
this.isInvulnerable = false
|
|
// if (!(simulation.cycle % 15)) requestAnimationFrame(() => {
|
|
// simulation.timePlayerSkip(5)
|
|
// // simulation.loop(); //ending with a wipe and normal loop fixes some very minor graphical issues where things are draw in the wrong locations
|
|
// }); //wrapping in animation frame prevents errors, probably
|
|
|
|
//
|
|
// simulation.timePlayerSkip(1)
|
|
// m.walk_cycle += m.flipLegs * m.Vx //makes the legs look like they are moving fast
|
|
// }); //wrapping in animation frame prevents errors, probably
|
|
requestAnimationFrame(() => {
|
|
simulation.timePlayerSkip(1)
|
|
}); //wrapping in animation frame prevents errors, probably
|
|
|
|
m.walk_cycle += m.flipLegs * m.Vx //makes the legs look like they are moving fast
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(this.position.x, this.position.y, this.eventHorizon, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "#fff";
|
|
ctx.globalCompositeOperation = "destination-in"; //in or atop
|
|
ctx.fill();
|
|
ctx.globalCompositeOperation = "source-over";
|
|
ctx.beginPath();
|
|
ctx.arc(this.position.x, this.position.y, this.eventHorizon, 0, 2 * Math.PI);
|
|
// ctx.stroke();
|
|
ctx.clip();
|
|
} else {
|
|
this.damageReduction = 0
|
|
this.isInvulnerable = true
|
|
//prevents other things from being drawn later on in the draw cycle
|
|
requestAnimationFrame(() => {
|
|
simulation.camera();
|
|
ctx.beginPath(); //gets rid of already draw shapes
|
|
ctx.arc(this.position.x, this.position.y, this.eventHorizon, 0, 2 * Math.PI, false); //part you can't see
|
|
ctx.fillStyle = document.body.style.backgroundColor;
|
|
ctx.fill();
|
|
ctx.restore();
|
|
})
|
|
}
|
|
}
|
|
};
|
|
},
|
|
streamBoss(x, y, radius = 110) {
|
|
mobs.spawn(x, y, 5, radius, "rgb(245,180,255)");
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
// me.accelMag = 0.00023 * simulation.accelScale;
|
|
me.accelMag = 0.00008 * simulation.accelScale;
|
|
// me.fireFreq = Math.floor(30 * simulation.CDScale)
|
|
me.canFire = false;
|
|
me.closestVertex1 = 0;
|
|
me.closestVertex2 = 1;
|
|
me.cycle = 0
|
|
me.frictionStatic = 0;
|
|
me.friction = 0;
|
|
me.frictionAir = 0.022;
|
|
me.memory = 240;
|
|
me.repulsionRange = 1200000; //squared
|
|
spawn.shield(me, x, y, 1);
|
|
spawn.spawnOrbitals(me, radius + 50 + 200 * Math.random())
|
|
|
|
Matter.Body.setDensity(me, 0.01); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
// this.vertices = Matter.Vertices.hull(Matter.Vertices.clockwiseSort(this.vertices)) //helps collisions functions work better after vertex have been changed
|
|
};
|
|
me.onDamage = function () { };
|
|
me.damageReduction = 0.25 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.do = function () {
|
|
// this.armor();
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
this.repulsion();
|
|
|
|
this.cycle++
|
|
if (this.seePlayer.recall && ((this.cycle % 15) === 0)) {
|
|
if (this.canFire) {
|
|
if (this.cycle > 100) {
|
|
this.cycle = 0
|
|
this.canFire = false
|
|
// Matter.Body.setAngularVelocity(this, 0.1)
|
|
// const forceMag = 0.01 * 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
|
|
}
|
|
spawn.seeker(this.vertices[this.closestVertex1].x, this.vertices[this.closestVertex1].y, 6)
|
|
Matter.Body.setDensity(mob[mob.length - 1], 0.000001); //normal is 0.001
|
|
const velocity = Vector.mult(Vector.normalise(Vector.sub(this.position, this.vertices[this.closestVertex1])), -10)
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: this.velocity.x + velocity.x,
|
|
y: this.velocity.y + velocity.y
|
|
});
|
|
spawn.seeker(this.vertices[this.closestVertex2].x, this.vertices[this.closestVertex2].y, 6)
|
|
Matter.Body.setDensity(mob[mob.length - 1], 0.000001); //normal is 0.001
|
|
const velocity2 = Vector.mult(Vector.normalise(Vector.sub(this.position, this.vertices[this.closestVertex2])), -10)
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: this.velocity.x + velocity2.x,
|
|
y: this.velocity.y + velocity2.y
|
|
});
|
|
} else if (this.cycle > 210) {
|
|
this.cycle = 0
|
|
this.canFire = true
|
|
|
|
//find closest 2 vertexes
|
|
let distance2 = Infinity
|
|
for (let i = 0; i < this.vertices.length; i++) {
|
|
const d = Vector.magnitudeSquared(Vector.sub(this.vertices[i], player.position))
|
|
if (d < distance2) {
|
|
distance2 = d
|
|
this.closestVertex2 = this.closestVertex1
|
|
this.closestVertex1 = i
|
|
}
|
|
}
|
|
if (this.closestVertex2 === this.closestVertex1) {
|
|
this.closestVertex2++
|
|
if (this.closestVertex2 === this.vertices.length) this.closestVertex2 = 0
|
|
}
|
|
}
|
|
}
|
|
};
|
|
},
|
|
seeker(x, y, radius = 8, sides = 6) {
|
|
//bullets
|
|
mobs.spawn(x, y, sides, radius, "rgb(255,0,255)");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent";
|
|
me.onHit = function () {
|
|
this.explode(this.mass * 20);
|
|
};
|
|
Matter.Body.setDensity(me, 0.000015); //normal is 0.001
|
|
me.timeLeft = 420 //* (0.8 + 0.4 * Math.random());
|
|
me.accelMag = 0.00017 * simulation.accelScale; //* (0.8 + 0.4 * Math.random())
|
|
me.frictionAir = 0.01 //* (0.8 + 0.4 * Math.random());
|
|
me.restitution = 0.5;
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.isBadTarget = true;
|
|
me.isMobBullet = true;
|
|
me.showHealthBar = false;
|
|
me.collisionFilter.category = cat.mobBullet;
|
|
me.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet;
|
|
me.do = function () {
|
|
// this.seePlayer.yes = false;
|
|
this.alwaysSeePlayer()
|
|
this.attraction();
|
|
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 this.gravity
|
|
me.leaveBody = false;
|
|
// me.isDropPowerUp = false;
|
|
me.onDeath = function () { //run this function on death
|
|
for (let i = 0; i < Math.ceil(this.mass * 0.15 + Math.random() * 2.5); ++i) {
|
|
spawn.spawns(this.position.x + (Math.random() - 0.5) * radius * 2.5, this.position.y + (Math.random() - 0.5) * radius * 2.5);
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: this.velocity.x + (Math.random() - 0.5) * 15,
|
|
y: this.velocity.x + (Math.random() - 0.5) * 15
|
|
});
|
|
}
|
|
};
|
|
spawn.shield(me, x, y);
|
|
me.do = function () {
|
|
this.gravity();
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
};
|
|
},
|
|
spawns(x, y, radius = 15) {
|
|
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.stroke = "transparent"
|
|
me.collisionFilter.mask = cat.player | cat.bullet | cat.body | cat.map | cat.mob
|
|
me.showHealthBar = false;
|
|
Matter.Body.setDensity(me, 0.0001); //normal is 0.001
|
|
me.g = 0.00002; //required if using this.gravity
|
|
me.accelMag = 0.00012 * simulation.accelScale;
|
|
// me.memory = 30;
|
|
me.isDropPowerUp = false
|
|
me.leaveBody = false;
|
|
me.seePlayerFreq = Math.floor((80 + 50 * Math.random()));
|
|
me.frictionAir = 0.004;
|
|
me.do = function () {
|
|
this.gravity();
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
|
|
// this.alwaysSeePlayer();
|
|
// this.checkStatus();
|
|
// this.attraction();
|
|
};
|
|
},
|
|
// exploder(x, y, radius = 40 + 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.0003; //required if using this.gravity
|
|
// me.seePlayerFreq = 50 + Math.floor(Math.random() * 20)
|
|
// me.do = function() {
|
|
// this.gravity();
|
|
// if (!(simulation.cycle % this.seePlayerFreq)) {
|
|
// if (
|
|
// this.distanceToPlayer2() < this.seeAtDistance2 &&
|
|
// Matter.Query.ray(map, this.position, this.playerPosRandomY()).length === 0 &&
|
|
// Matter.Query.ray(body, this.position, this.playerPosRandomY()).length === 0 &&
|
|
// !m.isCloak
|
|
// ) {
|
|
// this.foundPlayer();
|
|
// } else if (this.seePlayer.recall) {
|
|
// for (let i = 0; i < 20; i++) {
|
|
// let history = m.history[(m.cycle - 30 * i) % 600]
|
|
// if (Matter.Query.ray(map, this.position, history.position).length === 0) {
|
|
// this.seePlayer.recall = this.memory + Math.round(this.memory * Math.random()); //seconds before mob falls a sleep
|
|
// this.seePlayer.position.x = history.position.x;
|
|
// this.seePlayer.position.y = history.position.y;
|
|
|
|
// ctx.beginPath();
|
|
// ctx.moveTo(this.position.x, this.position.y);
|
|
// ctx.lineTo(history.position.x, history.position.y);
|
|
// ctx.lineWidth = 5;
|
|
// ctx.strokeStyle = "#000";
|
|
// ctx.stroke();
|
|
|
|
// break
|
|
// }
|
|
// }
|
|
// this.lostPlayer();
|
|
// }
|
|
// }
|
|
// this.checkStatus();
|
|
// this.attraction();
|
|
// };
|
|
// },
|
|
exploder(x, y, radius = 40 + 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 this.gravity
|
|
me.do = function () {
|
|
this.gravity();
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
};
|
|
},
|
|
snakeSpitBoss(x, y, radius = 50) {
|
|
let angle = Math.PI
|
|
const tailRadius = 300
|
|
|
|
const color1 = "rgb(235,180,255)"
|
|
mobs.spawn(x + tailRadius * Math.cos(angle), y + tailRadius * Math.sin(angle), 8, radius, color1); //"rgb(55,170,170)"
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
me.accelMag = 0.0001 + 0.0004 * Math.sqrt(simulation.accelScale)
|
|
// me.accelMag = 0.0004 + 0.0002 * Math.sqrt(simulation.accelScale)
|
|
me.memory = 250;
|
|
me.laserRange = 500;
|
|
Matter.Body.setDensity(me, 0.0022 + 0.00022 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.startingDamageReduction = 0.14 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.damageReduction = 0
|
|
me.isInvulnerable = true
|
|
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (this.id === mob[i].snakeHeadID && mob[i].alive) mob[i].death()
|
|
}
|
|
};
|
|
};
|
|
me.canFire = false;
|
|
me.closestVertex1 = 0;
|
|
me.cycle = 0
|
|
me.do = function () {
|
|
// this.armor();
|
|
this.seePlayerByHistory(40)
|
|
this.checkStatus();
|
|
this.attraction();
|
|
this.cycle++
|
|
if (this.seePlayer.recall && ((this.cycle % 10) === 0)) {
|
|
if (this.canFire) {
|
|
if (this.cycle > 120) {
|
|
this.cycle = 0
|
|
this.canFire = false
|
|
// Matter.Body.setAngularVelocity(this, 0.1)
|
|
// const forceMag = 0.01 * 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
|
|
}
|
|
spawn.seeker(this.vertices[this.closestVertex1].x, this.vertices[this.closestVertex1].y, 6)
|
|
Matter.Body.setDensity(mob[mob.length - 1], 0.000001); //normal is 0.001
|
|
const velocity = Vector.mult(Vector.normalise(Vector.sub(this.vertices[this.closestVertex1], this.position)), 15)
|
|
Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
x: this.velocity.x + velocity.x,
|
|
y: this.velocity.y + velocity.y
|
|
});
|
|
// spawn.seeker(this.vertices[this.closestVertex2].x, this.vertices[this.closestVertex2].y, 6)
|
|
// Matter.Body.setDensity(mob[mob.length - 1], 0.000001); //normal is 0.001
|
|
// const velocity2 = Vector.mult(Vector.normalise(Vector.sub(this.position, this.vertices[this.closestVertex2])), -10)
|
|
// Matter.Body.setVelocity(mob[mob.length - 1], {
|
|
// x: this.velocity.x + velocity2.x,
|
|
// y: this.velocity.y + velocity2.y
|
|
// });
|
|
} else if (this.cycle > 200) {
|
|
this.cycle = 0
|
|
this.canFire = true
|
|
|
|
//find closest 2 vertexes
|
|
let distance2 = Infinity
|
|
for (let i = 0; i < this.vertices.length; i++) {
|
|
const d = Vector.magnitudeSquared(Vector.sub(this.vertices[i], player.position))
|
|
if (d < distance2) {
|
|
distance2 = d
|
|
// this.closestVertex2 = this.closestVertex1
|
|
this.closestVertex1 = i
|
|
}
|
|
}
|
|
// if (this.closestVertex2 === this.closestVertex1) {
|
|
// this.closestVertex2++
|
|
// if (this.closestVertex2 === this.vertices.length) this.closestVertex2 = 0
|
|
// }
|
|
}
|
|
}
|
|
if (this.isInvulnerable) {
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 20;
|
|
ctx.strokeStyle = "rgba(255,255,255,0.7)";
|
|
ctx.stroke();
|
|
}
|
|
};
|
|
//extra space to give head room
|
|
angle -= 0.07
|
|
let previousTailID = 0
|
|
const nodes = Math.min(10 + Math.ceil(0.6 * simulation.difficulty), 60)
|
|
for (let i = 0; i < nodes; ++i) {
|
|
angle -= 0.1
|
|
spawn.snakeBody(x + tailRadius * Math.cos(angle), y + tailRadius * Math.sin(angle), i === 0 ? 25 : 20);
|
|
if (i < 4) mob[mob.length - 1].snakeHeadID = me.id
|
|
mob[mob.length - 1].previousTailID = previousTailID
|
|
previousTailID = mob[mob.length - 1].id
|
|
}
|
|
const damping = 1
|
|
const stiffness = 1
|
|
this.constrain2AdjacentMobs(nodes, stiffness, false, damping);
|
|
for (let i = mob.length - 1, len = i - nodes; i > len; i--) { //set alternating colors
|
|
if (i % 2) {
|
|
mob[i].fill = "#778"
|
|
} else {
|
|
mob[i].fill = color1
|
|
}
|
|
}
|
|
//constraint with first 3 mobs in line
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: mob[mob.length - nodes],
|
|
bodyB: mob[mob.length - 1 - nodes],
|
|
stiffness: stiffness,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: mob[mob.length - nodes + 1],
|
|
bodyB: mob[mob.length - 1 - nodes],
|
|
stiffness: stiffness,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: mob[mob.length - nodes + 2],
|
|
bodyB: mob[mob.length - 1 - nodes],
|
|
stiffness: stiffness,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
},
|
|
dragonFlyBoss(x, y, radius = 42) { //snake boss with a laser head
|
|
let angle = Math.PI
|
|
const tailRadius = 300
|
|
mobs.spawn(x + tailRadius * Math.cos(angle), y + tailRadius * Math.sin(angle), 8, radius, "#000"); //"rgb(55,170,170)"
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
Matter.Body.setDensity(me, 0.00165 + 0.00011 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.startingDamageReduction = 0.14 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.damageReduction = 0
|
|
me.isInvulnerable = true
|
|
|
|
me.accelMag = 0.00008 + 0.00045 * Math.sqrt(simulation.accelScale)
|
|
me.memory = 250;
|
|
me.seePlayerFreq = 13
|
|
me.flapRate = 0.4
|
|
me.flapArc = 0.2 //don't go past 1.57 for normal flaps
|
|
me.wingLength = 150
|
|
me.ellipticity = 0.3
|
|
me.angleOff = 0.4
|
|
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (this.id === mob[i].snakeHeadID && mob[i].alive) mob[i].death()
|
|
}
|
|
};
|
|
me.do = function () {
|
|
this.seePlayerByHistory(40)
|
|
this.checkStatus();
|
|
this.attraction();
|
|
|
|
let a //used to set the angle of wings
|
|
if (this.isInvulnerable) {
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 12;
|
|
ctx.strokeStyle = "rgba(255,255,255,0.9)";
|
|
ctx.stroke();
|
|
const sub = Vector.sub(this.position, this.snakeBody1.position)
|
|
a = Math.atan2(sub.y, sub.x)
|
|
} else {
|
|
a = Math.atan2(this.velocity.y, this.velocity.x)
|
|
}
|
|
|
|
ctx.fillStyle = `hsla(${160 + 40 * Math.random()}, 100%, ${25 + 25 * Math.random() * Math.random()}%, 0.9)`; //"rgba(0,235,255,0.3)"; // ctx.fillStyle = `hsla(44, 79%, 31%,0.4)`; //"rgba(0,235,255,0.3)";
|
|
this.wing(a + Math.PI / 2 + this.angleOff + this.flapArc * Math.sin(simulation.cycle * this.flapRate), this.wingLength, this.ellipticity)
|
|
this.wing(a - Math.PI / 2 - this.angleOff - this.flapArc * Math.sin(simulation.cycle * this.flapRate), this.wingLength, this.ellipticity)
|
|
this.wing(a - Math.PI / 2 + this.angleOff + this.flapArc * Math.sin(simulation.cycle * this.flapRate), this.wingLength, this.ellipticity)
|
|
this.wing(a + Math.PI / 2 - this.angleOff - this.flapArc * Math.sin(simulation.cycle * this.flapRate), this.wingLength, this.ellipticity)
|
|
};
|
|
|
|
angle -= 0.07
|
|
let previousTailID = 0
|
|
const nodes = Math.min(10 + Math.ceil(0.6 * simulation.difficulty), 60)
|
|
for (let i = 0; i < nodes; ++i) {
|
|
angle -= 0.1
|
|
spawn.snakeBody(x + tailRadius * Math.cos(angle), y + tailRadius * Math.sin(angle), i === 0 ? 25 : 20);
|
|
const who = mob[mob.length - 1]
|
|
who.fill = `hsl(${160 + 40 * Math.random()}, 100%, ${5 + 25 * Math.random() * Math.random()}%)`
|
|
if (i < 4) who.snakeHeadID = me.id
|
|
if (i === 0) me.snakeBody1 = who //track this segment, so the difference in position between this segment and the head can be used to angle the wings
|
|
who.previousTailID = previousTailID
|
|
previousTailID = who.id
|
|
}
|
|
const damping = 1
|
|
const stiffness = 1
|
|
this.constrain2AdjacentMobs(nodes, stiffness, false, damping);
|
|
//constraint with first few mobs in tail
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: mob[mob.length - nodes],
|
|
bodyB: mob[mob.length - 1 - nodes],
|
|
stiffness: stiffness,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: mob[mob.length - nodes + 1],
|
|
bodyB: mob[mob.length - 1 - nodes],
|
|
stiffness: stiffness,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: mob[mob.length - nodes + 2],
|
|
bodyB: mob[mob.length - 1 - nodes],
|
|
stiffness: stiffness,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
},
|
|
snakeBody(x, y, radius = 10) {
|
|
mobs.spawn(x, y, 8, radius, "rgba(0,180,180,0.4)");
|
|
let me = mob[mob.length - 1];
|
|
me.collisionFilter.mask = cat.bullet | cat.player //| cat.mob //| cat.body
|
|
me.damageReduction = 0.028
|
|
Matter.Body.setDensity(me, 0.0001); //normal is 0.001
|
|
|
|
// me.accelMag = 0.0007 * simulation.accelScale;
|
|
me.leaveBody = Math.random() < 0.33 ? true : false;
|
|
me.showHealthBar = false;
|
|
me.isDropPowerUp = false;
|
|
me.frictionAir = 0;
|
|
me.isSnakeTail = true;
|
|
me.stroke = "transparent"
|
|
me.onDeath = function () {
|
|
setTimeout(() => {
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (this.id === mob[i].previousTailID && mob[i].alive) mob[i].death()
|
|
if (this.snakeHeadID === mob[i].id) {
|
|
mob[i].isInvulnerable = false
|
|
mob[i].damageReduction = mob[i].startingDamageReduction
|
|
}
|
|
}
|
|
}, 500);
|
|
};
|
|
me.do = function () {
|
|
this.checkStatus();
|
|
};
|
|
// me.doActive = function() {
|
|
// this.checkStatus();
|
|
// this.alwaysSeePlayer();
|
|
// this.attraction();
|
|
// };
|
|
},
|
|
tetherBoss(x, y, constraint, 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.isBoss = true;
|
|
Matter.Body.setDensity(me, 0.0005 + 0.0002 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.damageReduction = 0.25 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.startingDamageReduction = me.damageReduction
|
|
me.isInvulnerable = false
|
|
me.nextHealthThreshold = 0.75
|
|
me.invulnerableCount = 0
|
|
me.g = 0.0001; //required if using this.gravity
|
|
me.accelMag = 0.0012 + 0.0013 * simulation.accelScale;
|
|
me.memory = 20;
|
|
me.repulsionRange = 1800 * 1800
|
|
|
|
cons[cons.length] = Constraint.create({
|
|
pointA: { x: constraint.x, y: constraint.y },
|
|
bodyB: me,
|
|
stiffness: 0.00012
|
|
});
|
|
Composite.add(engine.world, cons[cons.length - 1]);
|
|
|
|
spawn.shield(me, x, y, 1);
|
|
setTimeout(() => { spawn.spawnOrbitals(me, radius + 50 + 200 * Math.random()) }, 100); //have to wait a sec so the tether constraint doesn't attach to an orbital
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
this.removeCons(); //remove constraint
|
|
me.babies(0.05 * simulation.difficulty + 1)
|
|
};
|
|
me.babies = function (len) {
|
|
const delay = Math.max(3, Math.floor(15 - len / 2))
|
|
let i = 0
|
|
let spawnFlutters = () => {
|
|
if (i < len) {
|
|
if (!(simulation.cycle % delay) && !simulation.paused && !simulation.isChoosing && m.alive) {
|
|
// const phase = i / len * 2 * Math.PI
|
|
// const where = Vector.add(this.position, Vector.mult({ x: Math.cos(phase), y: Math.sin(phase) }, radius * 1.5))
|
|
const unit = Vector.normalise(Vector.sub(player.position, this.position))
|
|
const velocity = Vector.mult(unit, 10 + 10 * Math.random())
|
|
const where = Vector.add(this.position, Vector.mult(unit, radius * 1.2))
|
|
spawn.allowShields = false
|
|
spawn.flutter(where.x, where.y, Math.floor(9 + 8 * Math.random()))
|
|
const who = mob[mob.length - 1]
|
|
Matter.Body.setDensity(who, 0.01); //extra dense //normal is 0.001 //makes effective life much larger
|
|
Matter.Body.setVelocity(who, velocity);
|
|
Matter.Body.setAngle(who, Math.atan2(velocity.y, velocity.x))
|
|
|
|
this.alertNearByMobs();
|
|
spawn.allowShields = true
|
|
i++
|
|
}
|
|
requestAnimationFrame(spawnFlutters);
|
|
}
|
|
}
|
|
requestAnimationFrame(spawnFlutters);
|
|
}
|
|
me.onDamage = function () {
|
|
if (this.health < this.nextHealthThreshold && this.alive) {
|
|
this.health = this.nextHealthThreshold - 0.01
|
|
this.nextHealthThreshold = Math.floor(this.health * 4) / 4
|
|
this.invulnerableCount = 90 + Math.floor(30 * Math.random())
|
|
this.isInvulnerable = true
|
|
this.damageReduction = 0
|
|
}
|
|
};
|
|
me.do = function () {
|
|
this.gravity();
|
|
if (this.isInvulnerable) {
|
|
this.repulsion();
|
|
this.invulnerableCount--
|
|
if (this.invulnerableCount < 0) {
|
|
this.isInvulnerable = false
|
|
this.damageReduction = this.startingDamageReduction
|
|
this.frictionAir = 0.05
|
|
me.babies(0.07 * simulation.difficulty + 2)
|
|
if (this.radius > 15) {
|
|
const scale = 0.88;
|
|
Matter.Body.scale(this, scale, scale);
|
|
this.radius *= scale;
|
|
}
|
|
}
|
|
//draw invulnerable
|
|
ctx.beginPath();
|
|
let vertices = this.vertices;
|
|
ctx.moveTo(vertices[0].x, vertices[0].y);
|
|
for (let j = 1; j < vertices.length; j++) ctx.lineTo(vertices[j].x, vertices[j].y);
|
|
ctx.lineTo(vertices[0].x, vertices[0].y);
|
|
ctx.lineWidth = 13 + 5 * Math.random();
|
|
ctx.strokeStyle = `rgba(255,255,255,${0.5 + 0.2 * Math.random()})`;
|
|
ctx.stroke();
|
|
} else {
|
|
this.seePlayerCheck();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
}
|
|
};
|
|
},
|
|
shield(target, x, y, chance = Math.min(0.02 + simulation.difficulty * 0.005, 0.2) + tech.duplicationChance(), isExtraShield = false) {
|
|
if (this.allowShields && Math.random() < chance) {
|
|
mobs.spawn(x, y, 9, target.radius + 30, "rgba(220,220,255,0.9)");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "rgb(220,220,255)";
|
|
Matter.Body.setDensity(me, 0.00001) //very low density to not mess with the original mob's motion
|
|
me.shield = true;
|
|
me.damageReduction = 0.05 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.isUnblockable = true
|
|
me.isExtraShield = isExtraShield //this prevents spamming with tech.isShieldAmmo
|
|
me.collisionFilter.category = cat.mobShield
|
|
me.collisionFilter.mask = cat.bullet;
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: me,
|
|
bodyB: target, //attach shield to target
|
|
stiffness: 0.4,
|
|
damping: 0.1
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
|
|
me.onDamage = function () {
|
|
//make sure the mob that owns the shield can tell when damage is done
|
|
this.alertNearByMobs();
|
|
this.fill = `rgba(220,220,255,${0.3 + 0.6 * this.health})`
|
|
};
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.showHealthBar = false;
|
|
|
|
me.shieldTargetID = target.id
|
|
target.isShielded = true;
|
|
target.shieldID = me.id
|
|
me.onDeath = function () {
|
|
//clear isShielded status from target
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (mob[i].id === this.shieldTargetID) mob[i].isShielded = false;
|
|
}
|
|
};
|
|
me.do = function () {
|
|
this.checkStatus();
|
|
};
|
|
|
|
mob.unshift(me); //move shield to the front of the array, so that mob is behind shield graphically
|
|
|
|
//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;
|
|
}
|
|
},
|
|
groupShield(targets, x, y, radius, stiffness = 0.4) {
|
|
const nodes = targets.length
|
|
mobs.spawn(x, y, 9, radius, "rgba(220,220,255,0.9)");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "rgb(220,220,255)";
|
|
Matter.Body.setDensity(me, 0.00001) //very low density to not mess with the original mob's motion
|
|
me.frictionAir = 0;
|
|
me.shield = true;
|
|
me.damageReduction = 0.075 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
me.collisionFilter.category = cat.mobShield
|
|
me.collisionFilter.mask = cat.bullet;
|
|
for (let i = 0; i < nodes; ++i) {
|
|
mob[mob.length - i - 2].isShielded = true;
|
|
//constrain to all mob nodes in group
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: me,
|
|
bodyB: mob[mob.length - i - 2],
|
|
stiffness: stiffness,
|
|
damping: 0.1
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
}
|
|
me.onDamage = function () {
|
|
this.alertNearByMobs(); //makes sure the mob that owns the shield can tell when damage is done
|
|
this.fill = `rgba(220,220,255,${0.3 + 0.6 * this.health})`
|
|
};
|
|
me.onDeath = function () {
|
|
//clear isShielded status from target
|
|
for (let j = 0; j < targets.length; j++) {
|
|
for (let i = 0, len = mob.length; i < len; i++) {
|
|
if (mob[i].id === targets[j]) mob[i].isShielded = false;
|
|
}
|
|
}
|
|
};
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.showHealthBar = false;
|
|
mob[mob.length - 1] = mob[mob.length - 1 - nodes];
|
|
mob[mob.length - 1 - nodes] = me;
|
|
me.do = function () {
|
|
this.checkStatus();
|
|
};
|
|
},
|
|
spawnOrbitals(who, radius, chance = Math.min(0.25 + simulation.difficulty * 0.005)) {
|
|
if (Math.random() < chance) {
|
|
// simulation.difficulty = 50
|
|
const len = Math.floor(Math.min(15, 3 + Math.sqrt(simulation.difficulty))) // simulation.difficulty = 40 on hard mode level 10
|
|
const speed = (0.003 + 0.004 * Math.random() + 0.002 * Math.sqrt(simulation.difficulty)) * ((Math.random() < 0.5) ? 1 : -1)
|
|
const offSet = 6.28 * Math.random()
|
|
for (let i = 0; i < len; i++) spawn.orbital(who, radius, i / len * 2 * Math.PI + offSet, speed)
|
|
}
|
|
},
|
|
orbital(who, radius, phase, speed) {
|
|
// for (let i = 0, len = 7; i < len; i++) spawn.orbital(me, radius + 250, 2 * Math.PI / len * i)
|
|
mobs.spawn(who.position.x, who.position.y, 8, 12, "rgb(255,0,150)");
|
|
let me = mob[mob.length - 1];
|
|
me.stroke = "transparent";
|
|
Matter.Body.setDensity(me, 0.01); //normal is 0.001
|
|
me.leaveBody = false;
|
|
me.isDropPowerUp = false;
|
|
me.isBadTarget = true;
|
|
me.isUnstable = true; //dies when blocked
|
|
me.showHealthBar = false;
|
|
me.isOrbital = true;
|
|
// me.isShielded = true
|
|
me.collisionFilter.category = cat.mobBullet;
|
|
me.collisionFilter.mask = cat.bullet; //cat.player | cat.map | cat.body
|
|
me.do = function () {
|
|
//if host is gone
|
|
if (!who || !who.alive) {
|
|
this.death();
|
|
return
|
|
}
|
|
//set orbit
|
|
const time = simulation.cycle * speed + phase
|
|
const orbit = {
|
|
x: Math.cos(time),
|
|
y: Math.sin(time)
|
|
}
|
|
Matter.Body.setPosition(this, Vector.add(Vector.add(who.position, who.velocity), Vector.mult(orbit, radius)))
|
|
//damage player
|
|
if (Matter.Query.collides(this, [player]).length > 0 && !(m.isCloak && tech.isIntangible) && m.immuneCycle < m.cycle) {
|
|
m.immuneCycle = m.cycle + m.collisionImmuneCycles; //player is immune to damage for 30 cycles
|
|
const dmg = 0.03 * simulation.dmgScale
|
|
m.damage(dmg);
|
|
simulation.drawList.push({ //add dmg to draw queue
|
|
x: this.position.x,
|
|
y: this.position.y,
|
|
radius: Math.sqrt(dmg) * 200,
|
|
color: simulation.mobDmgColor,
|
|
time: simulation.drawTime
|
|
});
|
|
this.death();
|
|
}
|
|
};
|
|
},
|
|
orbitalBoss(x, y, radius = 70) {
|
|
const nodeBalance = Math.random()
|
|
const nodes = Math.min(15, Math.floor(2 + 4 * nodeBalance + 0.75 * Math.sqrt(simulation.difficulty)))
|
|
mobs.spawn(x, y, nodes, radius, "rgb(255,0,150)");
|
|
let me = mob[mob.length - 1];
|
|
me.isBoss = true;
|
|
Matter.Body.setDensity(me, 0.0017 + 0.0002 * Math.sqrt(simulation.difficulty)); //extra dense //normal is 0.001 //makes effective life much larger
|
|
me.damageReduction = 0.1 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1)
|
|
|
|
me.stroke = "transparent"; //used for drawGhost
|
|
me.seeAtDistance2 = 2000000;
|
|
me.collisionFilter.mask = cat.bullet | cat.player | cat.body | cat.map
|
|
me.memory = Infinity;
|
|
me.frictionAir = 0.04;
|
|
me.accelMag = 0.0002 + 0.00015 * simulation.accelScale
|
|
spawn.shield(me, x, y, 1);
|
|
|
|
const rangeInnerVsOuter = Math.random()
|
|
let speed = (0.006 + 0.001 * Math.sqrt(simulation.difficulty)) * ((Math.random() < 0.5) ? 1 : -1)
|
|
let range = radius + 350 + 200 * rangeInnerVsOuter + nodes * 7
|
|
for (let i = 0; i < nodes; i++) spawn.orbital(me, range, i / nodes * 2 * Math.PI, speed)
|
|
const orbitalIndexes = [] //find indexes for all the current nodes
|
|
for (let i = 0; i < nodes; i++) orbitalIndexes.push(mob.length - 1 - i)
|
|
// add orbitals for each orbital
|
|
range = Math.max(60, 100 + 100 * Math.random() - nodes * 3 - rangeInnerVsOuter * 80)
|
|
speed = speed * (1.25 + 2 * Math.random())
|
|
const subNodes = Math.max(2, Math.floor(6 - 5 * nodeBalance + 0.5 * Math.sqrt(simulation.difficulty)))
|
|
for (let j = 0; j < nodes; j++) {
|
|
for (let i = 0, len = subNodes; i < len; i++) spawn.orbital(mob[orbitalIndexes[j]], range, i / len * 2 * Math.PI, speed)
|
|
}
|
|
for (let i = 0, len = 3 + 0.5 * Math.sqrt(simulation.difficulty); i < len; i++) spawn.spawnOrbitals(me, radius + 40 + 10 * i, 1);
|
|
|
|
me.onDeath = function () {
|
|
powerUps.spawnBossPowerUp(this.position.x, this.position.y)
|
|
};
|
|
me.do = function () {
|
|
this.seePlayerByHistory();
|
|
this.checkStatus();
|
|
this.attraction();
|
|
};
|
|
},
|
|
//complex constrained mob templates**********************************************************************
|
|
//*******************************************************************************************************
|
|
allowShields: true,
|
|
nodeGroup(
|
|
x,
|
|
y,
|
|
spawn = "striker",
|
|
nodes = Math.min(2 + Math.ceil(Math.random() * (simulation.difficulty + 2)), 8),
|
|
//Math.ceil(Math.random() * 3) + Math.min(4,Math.ceil(simulation.difficulty/2)),
|
|
radius = Math.ceil(Math.random() * 10) + 18, // 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 individual group mobs
|
|
const angle = 2 * Math.PI / nodes
|
|
let targets = []
|
|
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);
|
|
targets.push(mob[mob.length - 1].id) //track who is in the group, for shields
|
|
}
|
|
if (Math.random() < 0.3) {
|
|
this.constrain2AdjacentMobs(nodes, stiffness * 2, true);
|
|
} else {
|
|
this.constrainAllMobCombos(nodes, stiffness);
|
|
}
|
|
//spawn shield for entire group
|
|
if (nodes > 2 && Math.random() < 0.998) {
|
|
this.groupShield(targets, x, y, sideLength + 2.5 * radius + nodes * 6 - 25);
|
|
}
|
|
this.allowShields = true;
|
|
},
|
|
lineGroup(
|
|
x,
|
|
y,
|
|
spawn = "striker",
|
|
nodes = Math.min(3 + Math.ceil(Math.random() * simulation.difficulty + 2), 8),
|
|
//Math.ceil(Math.random() * 3) + Math.min(4,Math.ceil(simulation.difficulty/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; //don't want shields on individual group 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
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
}
|
|
}
|
|
},
|
|
constrain2AdjacentMobs(nodes, stiffness, loop = false, damping = 0) {
|
|
//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,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
}
|
|
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,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
}
|
|
}
|
|
//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,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: mob[mob.length - 2],
|
|
bodyB: mob[mob.length - nodes],
|
|
stiffness: stiffness,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: mob[mob.length - 1],
|
|
bodyB: mob[mob.length - nodes + 1],
|
|
stiffness: stiffness,
|
|
damping: damping
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
}
|
|
},
|
|
constraintPB(x, y, bodyIndex, stiffness) {
|
|
cons[cons.length] = Constraint.create({
|
|
pointA: {
|
|
x: x,
|
|
y: y
|
|
},
|
|
bodyB: body[bodyIndex],
|
|
stiffness: stiffness
|
|
});
|
|
Composite.add(engine.world, cons[cons.length - 1]);
|
|
},
|
|
constraintBB(bodyIndexA, bodyIndexB, stiffness) {
|
|
consBB[consBB.length] = Constraint.create({
|
|
bodyA: body[bodyIndexA],
|
|
bodyB: body[bodyIndexB],
|
|
stiffness: stiffness
|
|
});
|
|
Composite.add(engine.world, consBB[consBB.length - 1]);
|
|
},
|
|
// 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];
|
|
me.collisionFilter.category = cat.body;
|
|
me.collisionFilter.mask = cat.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.isDropPowerUp = false;
|
|
me.showHealthBar = false;
|
|
me.isBadTarget = true;
|
|
me.isUnblockable = true;
|
|
|
|
me.do = function () {
|
|
let wireX = -50;
|
|
let wireY = -1000;
|
|
if (this.freeOfWires) {
|
|
this.gravity();
|
|
} else {
|
|
if (m.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 (m.pos.x > 700 && player.velocity.x > -2) {
|
|
let wireFriction = 0.75 * Math.min(0.6, Math.max(0, 100 / (breakingPoint - m.pos.x)));
|
|
if (!m.onGround) wireFriction *= 3
|
|
Matter.Body.setVelocity(player, {
|
|
x: player.velocity.x - wireFriction,
|
|
y: player.velocity.y
|
|
})
|
|
}
|
|
//move to player
|
|
Matter.Body.setPosition(this, {
|
|
x: m.pos.x + (42 * Math.cos(m.angle + Math.PI)),
|
|
y: m.pos.y + (42 * Math.sin(m.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(m.pos.x + (30 * Math.cos(m.angle + Math.PI)), m.pos.y + (30 * Math.sin(m.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 = cat.body;
|
|
me.collisionFilter.mask = cat.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.isDropPowerUp = false;
|
|
me.showHealthBar = false;
|
|
me.isBadTarget = true;
|
|
me.isUnblockable = true;
|
|
|
|
me.do = function () {
|
|
let wireX = -50 - 20;
|
|
let wireY = -1000;
|
|
|
|
if (this.freeOfWires) {
|
|
this.gravity();
|
|
} else {
|
|
if (m.pos.x > breakingPoint) {
|
|
this.freeOfWires = true;
|
|
this.force.x -= 0.0004;
|
|
this.fill = "#222";
|
|
}
|
|
//move mob to player
|
|
m.calcLeg(0, 0);
|
|
Matter.Body.setPosition(this, {
|
|
x: m.pos.x + m.flipLegs * m.knee.x - 5,
|
|
y: m.pos.y + m.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 = cat.body;
|
|
me.collisionFilter.mask = cat.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.isDropPowerUp = false;
|
|
me.showHealthBar = false;
|
|
me.isBadTarget = true;
|
|
me.isUnblockable = true;
|
|
|
|
me.do = function () {
|
|
let wireX = -50 - 35;
|
|
let wireY = -1000;
|
|
|
|
if (this.freeOfWires) {
|
|
this.gravity();
|
|
} else {
|
|
if (m.pos.x > breakingPoint) {
|
|
this.freeOfWires = true;
|
|
this.force.x += -0.0003;
|
|
this.fill = "#333";
|
|
}
|
|
//move mob to player
|
|
m.calcLeg(Math.PI, -3);
|
|
Matter.Body.setPosition(this, {
|
|
x: m.pos.x + m.flipLegs * m.knee.x - 5,
|
|
y: m.pos.y + m.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 = cat.body;
|
|
me.collisionFilter.mask = cat.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.isDropPowerUp = false;
|
|
me.showHealthBar = false;
|
|
me.isBadTarget = true;
|
|
me.isUnblockable = true;
|
|
|
|
me.do = function () {
|
|
let wireX = -50 + 16;
|
|
let wireY = -1000;
|
|
|
|
if (this.freeOfWires) {
|
|
this.gravity();
|
|
} else {
|
|
if (m.pos.x > breakingPoint) {
|
|
this.freeOfWires = true;
|
|
this.force.x += -0.0006;
|
|
this.fill = "#111";
|
|
}
|
|
//move mob to player
|
|
m.calcLeg(0, 0);
|
|
Matter.Body.setPosition(this, {
|
|
x: m.pos.x + m.flipLegs * m.foot.x - 5,
|
|
y: m.pos.y + m.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 = "#111";
|
|
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 = cat.body;
|
|
me.collisionFilter.mask = cat.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.isDropPowerUp = false;
|
|
me.showHealthBar = false;
|
|
me.isBadTarget = true;
|
|
me.isUnblockable = true;
|
|
|
|
me.do = function () {
|
|
let wireX = -50 + 26;
|
|
let wireY = -1000;
|
|
|
|
if (this.freeOfWires) {
|
|
this.gravity();
|
|
} else {
|
|
if (m.pos.x > breakingPoint) {
|
|
this.freeOfWires = true;
|
|
this.force.x += -0.0005;
|
|
this.fill = "#222";
|
|
}
|
|
//move mob to player
|
|
m.calcLeg(Math.PI, -3);
|
|
Matter.Body.setPosition(this, {
|
|
x: m.pos.x + m.flipLegs * m.foot.x - 5,
|
|
y: m.pos.y + m.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 = "#222";
|
|
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.addQueryRegion(x, y - 20, 100, 20, "boost", [
|
|
[player], body, mob, powerUp, bullet
|
|
], -1.21 * Math.sqrt(Math.abs(height)));
|
|
},
|
|
blockDoor(x, y, blockSize = 60) {
|
|
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 + i * 3, 30, blockSize);
|
|
}
|
|
},
|
|
debris(x, y, width, number = Math.floor(2 + Math.random() * 9)) {
|
|
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.001 }) {
|
|
// 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);
|
|
// },
|
|
bodyRect(x, y, width, height, chance = 1, properties = { friction: 0.05, frictionAir: 0.001 }) { //this is the command that adds blocks to the world in the middle of a level
|
|
if (Math.random() < chance) {
|
|
body[body.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height, properties);
|
|
const who = body[body.length - 1]
|
|
who.collisionFilter.category = cat.body;
|
|
who.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet
|
|
Composite.add(engine.world, who); //add to world
|
|
who.classType = "body"
|
|
}
|
|
},
|
|
bodyVertex(x, y, vector, properties) { //this is the command that adds blocks to the world in the middle of a level
|
|
body[body.length] = Matter.Bodies.fromVertices(x, y, Vertices.fromPath(vector), properties);
|
|
const who = body[body.length - 1]
|
|
who.collisionFilter.category = cat.body;
|
|
who.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet
|
|
Composite.add(engine.world, who); //add to world
|
|
who.classType = "body"
|
|
},
|
|
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);
|
|
},
|
|
mapRectNow(x, y, width, height, properties, isRedrawMap = true) { //adds rectangle to map array in the middle of a level
|
|
map[map.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height, properties);
|
|
const who = map[map.length - 1]
|
|
who.collisionFilter.category = cat.map;
|
|
who.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.powerUp | cat.mob | cat.mobBullet;
|
|
Matter.Body.setStatic(who, true); //make static
|
|
Composite.add(engine.world, who); //add to world
|
|
if (isRedrawMap) simulation.draw.setPaths()
|
|
},
|
|
mapVertexNow(x, y, vector, properties, isRedrawMap = true) { //adds shape to map array in the middle of a level
|
|
map[map.length] = Matter.Bodies.fromVertices(x, y, Vertices.fromPath(vector), properties);
|
|
const who = map[map.length - 1]
|
|
who.collisionFilter.category = cat.map;
|
|
who.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.powerUp | cat.mob | cat.mobBullet;
|
|
Matter.Body.setStatic(who, true); //make static
|
|
Composite.add(engine.world, who); //add to world
|
|
if (isRedrawMap) simulation.draw.setPaths() //this is a bit slow on processing so maybe it's better to run after you spawn several different shapes
|
|
},
|
|
//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
|
|
}
|
|
}; |