//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(`m.research -= 3
${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(`undefined = ${lore.techCount}/${lore.techGoal}`, 360); setTimeout(function () { simulation.makeTextLog(`level.levels.push("null")`, 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(`//enter testing mode to set level.levels.length to Infinite`); if (!(count % 60)) simulation.makeTextLog(`simulation.analysis = ${((count / 60 - Math.random()) * 0.1).toFixed(3)}`); } else if (count === 660) { simulation.makeTextLog(`simulation.analysis = 1 //analysis complete`); } else if (count === 780) { simulation.makeTextLog(`undefined = ${lore.techCount}/${lore.techGoal}`) } else if (count === 1020) { simulation.makeTextLog(`Engine.clear(engine) //simulation successful`); } 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 = Infinite`); }, 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(`undefined = ${lore.techCount}/${lore.techGoal}`, 360); // setTimeout(function() { // simulation.makeTextLog(`level.levels.push("null")`, 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(`//enter testing mode to set level.levels.length to Infinite`); // if (!(count % 60)) simulation.makeTextLog(`simulation.analysis = ${((count / 60 - Math.random()) * 0.1).toFixed(3)}`); // } else if (count === 660) { // simulation.makeTextLog(`simulation.analysis = 1 //analysis complete`); // } else if (count === 780) { // simulation.makeTextLog(`undefined = ${lore.techCount}/${lore.techGoal}`) // } else if (count === 1020) { // simulation.makeTextLog(`Engine.clear(engine) //simulation successful`); // } 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 = Infinite`); // }, 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 } };