diff --git a/.DS_Store b/.DS_Store index 65fe6c8..307961e 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/js/level.js b/js/level.js index fc20023..a200d76 100644 --- a/js/level.js +++ b/js/level.js @@ -10,7 +10,7 @@ const level = { //see level.populateLevels: (intro, ... , reservoir, reactor, ... , gauntlet, final) added later playableLevels: ["labs", "rooftops", "skyscrapers", "warehouse", "highrise", "office", "aerie", "satellite", "sewers", "testChamber", "pavilion"], // playableLevels: ["pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion"], - communityLevels: ["stronghold", "basement", "crossfire", "vats", "run", "n-gon", "house", "perplex", "coliseum", "tunnel", "islands"], + communityLevels: ["stronghold", "basement", "crossfire", "vats", "run", "n-gon", "house", "perplex", "coliseum", "tunnel", "islands", "temple"], trainingLevels: ["walk", "crouch", "jump", "hold", "throw", "throwAt", "deflect", "heal", "fire", "nailGun", "shotGun", "superBall", "matterWave", "missile", "stack", "mine", "grenades", "harpoon"], levels: [], start() { @@ -25,7 +25,7 @@ const level = { // tech.giveTech("grappling hook") // tech.giveTech("annelids") // for (let i = 0; i < 10; i++) powerUps.directSpawn(0, 0, "tech"); - // for (let i = 0; i < 9; i++) tech.giveTech("annelids") + // for (let i = 0; i < 9; i++) tech.giveTech("WIMPs") // for (let i = 10; i < tech.tech.length; i++) { tech.tech[i].isBanished = true } // powerUps.research.changeRerolls(100000) // for (let i = 0; i < 5; i++) tech.giveTech("corona discharge") @@ -37,7 +37,7 @@ const level = { // m.immuneCycle = Infinity //you can't take damage // level.difficultyIncrease(15) //30 is near max on hard //60 is near max on why // simulation.enableConstructMode() //used to build maps in testing mode - // level.pavilion(); + // level.temple(); // level.testing(); //not in rotation, used for testing if (simulation.isTraining) { level.walk(); } else { level.intro(); } //normal starting level ************************************************ // powerUps.research.changeRerolls(3000) @@ -9396,6 +9396,1397 @@ const level = { powerUps.spawn(3000, -230, "heal"); // level.difficultyIncrease(60) }, + temple() { + simulation.makeTextLog(`temple by Scar1337`); + + const V = Vector; + const Equation = (function() { + function Equation(a, b, c) { + this.a = a; + this.b = b; + this.c = c; + } + Equation.prototype.getXfromY = function(y) { + return (-this.b * y - this.c) / this.a; + } + Equation.prototype.getYfromX = function(x) { + return (-this.a * x - this.c) / this.b; + } + Equation.fromPoints = function(v1, v2) { + if (v1.x === v2.x) return new Equation(1, 0, -v1.x); + if (v1.y === v2.y) return new Equation(0, 1, -v1.y); + const d = (v2.y - v1.y) / (v2.x - v1.x); + return new Equation(-d, 1, d * v1.x - v1.y); + }; + return Equation; + })(); + const Rect = (function() { + function Rect(x, y, w, h) { + this.pos = { x, y }; + this.width = w; + this.height = h; + } + Rect.prototype.has = function({ x, y }) { + return x >= this.pos.x && x <= this.pos.x + this.width && + y >= this.pos.y && y <= this.pos.y + this.height; + } + Rect.prototype.hasLine = function(eq) { + const leftInter = eq.getYfromX(this.pos.x); + const rightInter = eq.getYfromX(this.pos.x + this.width); + const topInter = eq.getXfromY(this.pos.y); + return (leftInter >= this.pos.y && leftInter <= this.pos.y + this.height) || + (rightInter >= this.pos.y && rightInter <= this.pos.y + this.height) || + (topInter >= this.pos.x && topInter <= this.pos.x + this.width); + } + Rect.prototype.addToMap = function() { + spawn.mapRect(this.pos.x, this.pos.y, this.width, this.height); + } + Object.defineProperty(Rect.prototype, "midPos", { + get() { + return V.add(this.pos, { x: this.width / 2, y: this.height / 2 }); + } + }); + Rect.fromBounds = function(min, max) { + return new Rect(min.x, min.y, max.x - min.x, max.y - min.y); + } + return Rect; + })(); + + function isInBound(bound) { + return bound.has(player.bounds.min) || bound.has(player.bounds.max); + } + + function addWIMP(x, y) { + spawn.WIMP(x, y); + const me = mob[mob.length - 1]; + me.isWIMP = true; + } + + function relocateWIMPs(x, y) { + for (const i of mob) { + if (i.isWIMP) { + setPos(i, { x: x + 300 * (Math.random() - 0.5), y: y + 300 * (Math.random() - 0.5) }); + } + } + } + + function secondRoomBoss(x, y, radius = 25, isDark = false) { + mobs.spawn(x, y, 12, radius, isDark ? "#000" : "#fff"); + let me = mob[mob.length - 1]; + me.isBoss = true; + me.isDark = isDark; + + me.stroke = "transparent"; + me.eventHorizon = 500; // How family friendly content much do I have to reduce this + me.seeAtDistance2 = 5e6; // Basically just see at all times, in the context it's given + me.accelMag = 0.00003 * simulation.accelScale; + me.collisionFilter.mask = cat.player | cat.bullet; + me.memory = 1600; + me.randomPRNGMult = Math.random() * 500; + + me.attackCycle = 0; + me.lastAttackCycle = 0; + Matter.Body.setDensity(me, 0.014); // 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.spawn(this.position.x, this.position.y, "ammo"); + powerUps.spawn(this.position.x, this.position.y, "ammo"); + if (Math.random() > 0.2) powerUps.spawn(this.position.x, this.position.y, "heal", true, null, + 30 * (simulation.healScale ** 0.25) * Math.sqrt(tech.largerHeals) * Math.sqrt(0.1 + Math.random() * 0.5)); + if (simulation.difficulty > 5) { + // fling player to center + const SUB = V.sub(this.position, player.position) + const DISTANCE = V.magnitude(SUB) + if (DISTANCE < this.eventHorizon) { + Matter.Body.setVelocity(player, V.mult(SUB, 5e4 / (100 + DISTANCE) / (100 + DISTANCE))) + } + } + }; + 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) { // 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)); + + // draw darkness + ctx.fillStyle = this.isDark ? "rgba(0,20,40,0.6)" : "rgba(225,215,255,0.6)"; + DrawTools.arc(this.position.x, this.position.y, eventHorizon * 0.2, 0, 2 * Math.PI); + ctx.fillStyle = this.isDark ? "rgba(0,20,40,0.4)" : "rgba(225,215,255,0.4)"; + DrawTools.arc(this.position.x, this.position.y, eventHorizon * 0.4, 0, 2 * Math.PI); + ctx.fillStyle = this.isDark ? "rgba(0,20,40,0.3)" : "rgba(225,215,255,0.3)"; + DrawTools.arc(this.position.x, this.position.y, eventHorizon * 0.6, 0, 2 * Math.PI); + ctx.fillStyle = this.isDark ? "rgba(0,20,40,0.2)" : "rgba(225,215,255,0.2)"; + DrawTools.arc(this.position.x, this.position.y, eventHorizon * 0.8, 0, 2 * Math.PI); + ctx.fillStyle = this.isDark ? "rgba(0,0,0,0.05)" : "rgba(255,255,255,0.05)"; + DrawTools.arc(this.position.x, this.position.y, eventHorizon, 0, 2 * Math.PI); + // when player is inside event horizon + if (distance(this.position, player.position) < eventHorizon) { + if (this.isDark) { + // Standard black hole stuff + if (m.immuneCycle < m.cycle) { + if (m.energy > 0) m.energy -= 0.003; + 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.0005 * Math.cos(angle) * player.mass * (m.onGround ? 1.7 : 1); + player.force.y -= 0.0005 * Math.sin(angle) * player.mass; + // draw line to player + ctx.lineWidth = Math.min(60, this.radius * 2); + ctx.strokeStyle = "rgba(0,0,0,0.5)"; + DrawTools.line([this.position, m.pos]); + ctx.fillStyle = "rgba(0,0,0,0.3)"; + DrawTools.arc(m.pos.x, m.pos.y, 40, 0, 2 * Math.PI); + } else { + // Lightning attacks + this.attackCycle++; + if (this.attackCycle >= 30) { + this.attackCycle = 0; + this.lastAttackCycle = simulation.cycle; + Matter.Body.setVelocity(player, V.add(player.velocity, { x: 0, y: -10 })); + if (m.immuneCycle < m.cycle) { + if (m.energy > 0) m.energy -= 0.03; + m.damage(0.005 * simulation.dmgScale); + } + } + DrawTools.lightning(this.position, m.pos, this.lastAttackCycle, this.randomPRNGMult); + ctx.fillStyle = `rgba(255,240,127,${0.12 * Math.max(15 - simulation.cycle + this.lastAttackCycle, 0)})`; + DrawTools.arc(m.pos.x, m.pos.y, 40, 0, 2 * Math.PI); + } + } + } + } + }; + + function mobGrenade(...args) { + spawn.grenade(...args); + const pulseRadius = args[3] || Math.min(550, 250 + simulation.difficulty * 3) + let me = mob[mob.length - 1]; + me.fill = "#ace"; + me.onDeath = function() { + //damage player if in range + if (distance(player.position, this.position) < pulseRadius && m.immuneCycle < m.cycle) { + m.immuneCycle = m.cycle + tech.collisionImmuneCycles; //player is immune to damage + m.damage(0.02 * simulation.dmgScale); + } + simulation.drawList.push({ //add dmg to draw queue + x: this.position.x, + y: this.position.y, + radius: pulseRadius, + color: "rgba(170,204,238,0.3)", + time: simulation.drawTime + }); + }; + 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(170,204,238,0.1)"; + ctx.fill(); + }; + } + // Todo: nerf ThirdRoomBoss a bit? + function thirdRoomBoss(x, y) { + mobs.spawn(x, y, 6, 60, "#000"); + let me = mob[mob.length - 1]; + // Fix in place + 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); + me.isBoss = true; + + me.stroke = "transparent"; + me.eventHorizon = 1000; + me.collisionFilter.mask = cat.player | cat.bullet | cat.body; + + me.memory = Infinity; + me.attackCycle = 0; + me.lastAttackCycle = 0; + Matter.Body.setDensity(me, 0.08); //extra dense //normal is 0.001 //makes effective life much larger + me.onDeath = function() { + 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); + } + } + } + // You earned it: One more tech + powerUps.spawn(this.position.x, this.position.y, "tech"); + powerUps.spawnBossPowerUp(this.position.x, this.position.y); + templePlayer.room3ToEndAnim = 1; + }; + me.nextHealthThreshold = 0.75; + me.trapCycle = 0; + 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.trapCycle = 1; + this.isInvulnerable = true; + this.damageReduction = 0; + } + }; + me.damageReduction = 0.25 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1); + me.rings = [{ + colour: "#65f", + radius: 300, + id: 0 + }, { + colour: "#0f0", + radius: 400, + id: 1 + }, { + colour: "#f00", + radius: 500, + id: 2 + }]; + me.ring = function() { + const rings = this.isInvulnerable ? [] : this.rings; + ctx.lineWidth = 10; + for (const ring of rings) { + const radius = ring.radius * (1 + 0.3 * Math.sin(simulation.cycle / 60 * (ring.id + 2))); + if (Math.abs(distance(player.position, this.position) - radius) < 60 && m.immuneCycle < simulation.cycle) { + m.damage(0.4 / radius); + } + ctx.strokeStyle = ring.colour; + DrawTools.arcOut(this.position.x, this.position.y, radius, 0, Math.PI * 2); + } + } + me.horizon = function() { + // eventHorizon waves in and out + const eventHorizon = this.eventHorizon * (1 + 0.2 * Math.sin(simulation.cycle * 0.008)); + + const charge = this.attackCycle / 90; + this.fill = this.isInvulnerable ? "#f00" : `rgb(${charge * 255},${charge * 255},${charge * 255})`; + // draw darkness + ctx.fillStyle = `rgba(${charge * 225},${20 + charge * 195},${40 + charge * 215},0.6)`; + DrawTools.arc(this.position.x, this.position.y, eventHorizon * 0.2, 0, 2 * Math.PI); + ctx.fillStyle = `rgba(${charge * 225},${20 + charge * 195},${40 + charge * 215},0.4)`; + DrawTools.arc(this.position.x, this.position.y, eventHorizon * 0.4, 0, 2 * Math.PI); + ctx.fillStyle = `rgba(${charge * 225},${20 + charge * 195},${40 + charge * 215},0.3)`; + DrawTools.arc(this.position.x, this.position.y, eventHorizon * 0.6, 0, 2 * Math.PI); + ctx.fillStyle = `rgba(${charge * 225},${20 + charge * 195},${40 + charge * 215},0.2)`; + DrawTools.arc(this.position.x, this.position.y, eventHorizon * 0.8, 0, 2 * Math.PI); + ctx.fillStyle = `rgba(${charge * 255},${charge * 255},${charge * 255},0.05)`; + DrawTools.arc(this.position.x, this.position.y, eventHorizon, 0, 2 * Math.PI); + + // when player is inside event horizon + if (V.magnitude(V.sub(this.position, player.position)) < eventHorizon) { + // Standard black hole stuff + if (m.immuneCycle < m.cycle) { + if (m.energy > 0) m.energy -= 0.004; + if (m.energy < 0.1) m.damage(0.0002 * simulation.dmgScale); + } + const angle = Math.atan2(player.position.y - this.position.y, player.position.x - this.position.x); + player.force.x -= 0.001 * Math.cos(angle) * player.mass * (m.onGround ? 1.7 : 1); + player.force.y -= 0.001 * Math.sin(angle) * player.mass; + // draw line to player + ctx.lineWidth = Math.min(60, this.radius * 2); + ctx.strokeStyle = "rgba(0,0,0,0.5)"; + DrawTools.line([this.position, m.pos]); + ctx.fillStyle = "rgba(0,0,0,0.3)"; + DrawTools.arc(m.pos.x, m.pos.y, 40, 0, 2 * Math.PI); + // Lightning attacks + this.attackCycle++; + if (this.attackCycle >= 90) { + this.attackCycle = 0; + this.lastAttackCycle = simulation.cycle; + Matter.Body.setVelocity(player, V.add(player.velocity, { x: 0, y: -20 })); + if (m.immuneCycle < m.cycle) { + m.damage(0.015 * simulation.dmgScale); + } + } + const lightningCycle = simulation.cycle * 2 / 3 + this.lastAttackCycle / 3; + DrawTools.lightning(this.position, m.pos, lightningCycle, 1, 12); + DrawTools.lightning(this.position, m.pos, lightningCycle, 2, 12); + ctx.fillStyle = `rgba(255,240,127,${0.12 * Math.max(15 - simulation.cycle + this.lastAttackCycle, 0)})`; + DrawTools.arc(m.pos.x, m.pos.y, 40, 0, 2 * Math.PI); + } + } + me.periodicSpawns = function() { + // Spawn annoying purple thing(s) that chases the player + if (!(simulation.cycle % 180)) { + spawn.seeker(this.position.x, this.position.y, 15 * (0.7 + 0.5 * Math.random()), 7); + spawn.seeker(this.position.x, this.position.y, 4 * (0.7 + 0.5 * Math.random()), 7); + spawn.seeker(this.position.x, this.position.y, 4 * (0.7 + 0.5 * Math.random()), 7); + } + // Spawn the annoying pink (now blue) exploder that doesn't chase the player + if (!(simulation.cycle % 300)) { + for (let i = 0; i < 3; i++) { + mobGrenade(1100 + 700 * i, -13030, undefined, Math.min(700, 300 + simulation.difficulty * 4), 10); + setVel(mob[mob.length - 1], { x: 0, y: -10 }); + mobGrenade(1100 + 700 * i, -14370, undefined, Math.min(700, 300 + simulation.difficulty * 4), 10); + setVel(mob[mob.length - 1], { x: 0, y: 10 }); + } + } + // Spawn a bunch of mobs + if (!(simulation.cycle % 600)) { + spawn.nodeGroup(this.position.x, this.position.y, "focuser", 3); + } + } + me.invulnerableTrap = function() { + if (this.trapCycle < 1) return; + this.trapCycle++; + // 32 is just an arbitrarily large number + const spawnCycles = Math.min(32, Math.max(8, 6 + Math.floor(simulation.difficulty / 3))); + // I have no idea how to balance at all, please help me + const spawnDelay = Math.floor(5 + 10 / (1 + Math.sqrt(simulation.difficulty) / 5)); + if (this.trapCycle >= 120) { + const cycle = this.trapCycle - 120; + if (!(cycle % spawnDelay)) { + const radius = Math.min(500, 200 + simulation.difficulty * 3); + mobGrenade(600, -13050, 30, radius); + Matter.Body.setVelocity(mob[mob.length - 1], { x: 35, y: 0 }); + mobGrenade(3000, -13050, 30, radius); + Matter.Body.setVelocity(mob[mob.length - 1], { x: -35, y: 0 }); + mobGrenade(600, -14350, 30, radius); + Matter.Body.setVelocity(mob[mob.length - 1], { x: 35, y: 0 }); + mobGrenade(3000, -14350, 30, radius); + Matter.Body.setVelocity(mob[mob.length - 1], { x: -35, y: 0 }); + if (Math.floor(cycle / spawnDelay) >= spawnCycles - 1) { + this.trapCycle = 0; + this.isInvulnerable = false; + this.damageReduction = 0.25 / (tech.isScaleMobsWithDuplication ? 1 + tech.duplicationChance() : 1); + } + } + } + ctx.font = "100px Arial"; + ctx.fillStyle = "#f00"; + ctx.shadowBlur = 10; + ctx.shadowColor = "#f00"; + ctx.textAlign = "center"; + ctx.textBaseLine = "middle"; + ctx.fillText("!", 900, -13050); + ctx.fillText("!", 900, -14350); + ctx.fillText("!", 1800, -13050); + ctx.fillText("!", 1800, -14350); + ctx.fillText("!", 2700, -13050); + ctx.fillText("!", 2700, -14350); + ctx.shadowBlur = 0; + } + me.do = function() { + this.checkStatus(); + this.horizon(); + this.ring(); + this.periodicSpawns(); + this.invulnerableTrap(); + } + }; + let oldNextLevel = level.nextLevel; + level.nextLevel = () => { + color.map = "#444"; + m.death = m.oldDeath; + canvas.style.filter = ""; + level.nextLevel = oldNextLevel; + oldNextLevel(); + } + let bounds = []; + let mobPositionsQueue = Array.from(Array(10), () => []); + m.oldDeath = m.death; + m.death = function() { + if (!tech.isImmortal) { + requestAnimationFrame(() => color.map = "#444"); + m.death = m.oldDeath; + canvas.style.filter = ""; + level.nextLevel = oldNextLevel; + } + m.oldDeath(); + } + let name = "⥟ᘊ5⪊Ыᳪៗⱕ␥ዘᑧ⍗"; + addPartToMap = (len = map.length - 1) => { + map[len].collisionFilter.category = cat.map; + map[len].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.powerUp | cat.mob | cat.mobBullet; + Matter.Body.setStatic(map[len], true); // make static + Composite.add(engine.world, map[len]); + } + level.setPosToSpawn(50, -50); // normal spawn + // Make the level exit really far away so WIMP powerups don't show up at all + level.exit.x = 1e6; + level.exit.y = -1e6; + Promise.resolve().then(() => { + // Clear all WIMPS and their research + for (let i = 0; i < mob.length; i++) { + while (mob[i] && !mob[i].isMACHO) { + mob[i].replace(i); + } + } + for (let i = 0; i < powerUp.length; i++) { + while (powerUp[i] && powerUp[i].name === "research") { + Matter.Composite.remove(engine.world, powerUp[i]); + powerUp.splice(i, 1); + } + } + level.exit.x = 1500; + level.exit.y = -30; + }); + spawn.mapRect(level.enter.x, level.enter.y + 20, 100, 20); + spawn.mapRect(1500, -10, 100, 20); + level.defaultZoom = 1800 + simulation.setZoom(1200); + document.body.style.backgroundColor = "#daa69f"; + color.map = "#600"; + + function box(x, y, w, h, s) { + spawn.mapRect(x, y, w, s); + spawn.mapRect(x, y, s, h); + spawn.mapRect(x + w - s, y, s, h); + spawn.mapRect(x, y + h - s, w, s); + } + + function diamond(x, y, s) { + spawn.mapVertex(x, y, `0 -${s} -${s} 0 0 ${s} ${s} 0`); + } + + // Fake level + bounds.push(new Rect(-200, -500, 2000, 600)); + box(-200, -500, 2000, 600, 100); + + // Actual level, Room 1 + const firstRoomBounds = new Rect(-200, -4000, 5000, 2100); + bounds.push(firstRoomBounds); + + box(-200, -4000, 5000, 2100, 100); + spawn.mapRect(-200, -2500, 1300, 100); + spawn.mapRect(3500, -2500, 1300, 100); + spawn.mapRect(-200, -4000, 1000, 1600); + spawn.mapRect(3800, -4000, 1000, 1600); + // Enter and Exit platforms + spawn.mapRect(0, -2010, 100, 20); + spawn.mapRect(4500, -2010, 100, 20); + + // Altar of Room 1 + spawn.mapRect(2100, -2200, 100, 300); + spawn.mapRect(2400, -2200, 100, 300); + spawn.mapRect(2070, -2200, 460, 20); + + spawn.debris(1700, -2100, 300, 10); + spawn.debris(2500, -2100, 300, 10); + + // Actual level, Room 2 + const secondRoomBounds = new Rect(-1500, -10500, 3000, 3600); + bounds.push(secondRoomBounds); + + box(-1500, -10500, 3000, 3600, 100); + spawn.mapRect(-2000, -8500, 1600, 1600); + spawn.mapRect(400, -8500, 1600, 1600); + // Enter and Exit platforms + spawn.mapRect(-50, -7010, 100, 20); + spawn.mapRect(-50, -10010, 100, 20); + + // Hazard traversing + spawn.mapRect(-300, -7320, 800, 20); + spawn.mapRect(175, -7600, 325, 20); + spawn.mapRect(200, -7775, 300, 20); + spawn.mapRect(-500, -7600, 325, 20); + spawn.mapRect(-500, -7775, 300, 20); + spawn.mapRect(-500, -7950, 800, 20); + spawn.mapRect(-300, -8100, 800, 20); + spawn.mapRect(-500, -8250, 800, 20); + for (let i = 0; i < 2; i++) spawn.mapRect(-250, -8400 + 150 * i, 500, 60); + const room2SlimePit = level.hazard(-400, -8410, 800, 1090); + room2SlimePit.logic = function() { + if (this.height > 0 && Matter.Query.region([player], this).length) { + if (m.immuneCycle < m.cycle) { + // Trolled + const hasCPT = tech.isRewindAvoidDeath; + tech.isRewindAvoidDeath = false; + const DRAIN = 0.002 * (tech.isRadioactiveResistance ? 0.25 : 1) + m.fieldRegen; + if (m.energy > DRAIN && !tech.isEnergyHealth) { + m.energy -= DRAIN; + } + m.damage(0.00015 * (tech.isRadioactiveResistance ? 0.25 : 1)); + if (tech.isEnergyHealth) { + const previousEnergy = m.energy; + m.regenEnergy(); + const energyRegenerated = m.energy - previousEnergy; + if (energyRegenerated > 0) { + m.energy = previousEnergy; + m.damage(energyRegenerated); + } + } + tech.isRewindAvoidDeath = hasCPT; + } + player.force.y -= 0.3 * player.mass * simulation.g; + setVel(player, Vector.sub(player.velocity, { x: 0, y: player.velocity.y * 0.02 })); + } + // Float power ups + powerUpCollide = Matter.Query.region(powerUp, this) + for (let i = 0, len = powerUpCollide.length; i < len; i++) { + const diameter = 2 * powerUpCollide[i].size + const buoyancy = 1 - 0.2 * Math.max(0, Math.min(diameter, this.min.y - powerUpCollide[i].position.y + powerUpCollide[i].size)) / diameter + powerUpCollide[i].force.y -= buoyancy * 1.14 * powerUpCollide[i].mass * simulation.g; + setVel(powerUpCollide[i], { x: powerUpCollide[i].velocity.x, y: 0.96 * powerUpCollide[i].velocity.y }); + } + } + room2SlimePit.draw = function() { + if (this.isOn) { + ctx.fillStyle = "hsla(160, 100%, 35%, 0.75)"; + ctx.fillRect(this.min.x, this.min.y, this.width, this.height); + } + } + // Room 2 spawning bounds + const secondRoomSpawnBounds = new Rect(-1500, -10500, 3000, 2000); + spawn.mapRect(-700, -8700, 150, 20); + spawn.mapRect(550, -8700, 150, 20); + spawn.mapRect(-400, -8900, 800, 20); + diamond(-600, -9800, 30); + diamond(0, -9800, 30); + diamond(600, -9800, 30); + + spawn.mapRect(-1000, -10000, 2000, 30); + + // Actual level, Room 3 (Final Boss?) + const thirdRoomBounds = new Rect(-200, -14500, 4000, 1600); + bounds.push(thirdRoomBounds); + box(-200, -14500, 4000, 1600, 100); + spawn.mapRect(-200, -14500, 800, 1100); + spawn.mapRect(3000, -14500, 800, 1100); + // Enter and Exit platforms + spawn.mapRect(0, -13110, 100, 20); + spawn.mapRect(-200, -13100, 800, 200); + spawn.mapRect(3500, -13110, 100, 20); + spawn.mapRect(3000, -13100, 800, 200); + for (let i = 0; i < 3; i++) spawn.bodyRect(500, -13400 + i * 100, 100, 100); + + for (let i = 0; i < 3; i++) { + diamond(1100 + 700 * i, -13000, 30, 30); + diamond(1100 + 700 * i, -14400, 30, 30); + } + + const Objects = { + altar: { + get isHeal() { + return simulation.cycle % 600 >= 300; + }, + pos: { + x: 2300, + y: -2200 + }, + get isActive() { + const roughPlayerCentre = V.add(m.pos, { x: 0, y: 40 }); + return distance(roughPlayerCentre, this.pos) < 240 && + (Math.abs(angle(roughPlayerCentre, this.pos) - Math.PI / 2) < 1); + }, + logic() { + if (!this.isActive) return; + if (this.isHeal) { + m.energy += 0.005; + } else { + m.energy = Math.max(m.energy - 0.007 - m.fieldRegen, 0); + if (m.energy <= 0.01 && m.immuneCycle < m.cycle) m.damage(0.002); + } + }, + drawTop() { + if (!isInBound(firstRoomBounds)) return; + const colour = this.isHeal ? m.fieldMeterColor : "#f00"; + DrawTools.flame([2300, -2200, 26, 4, colour], 7); + ctx.fillStyle = colour; + ctx.fillRect(2200, -2200, 200, 200); + }, + drawBottom() { + ctx.fillStyle = this.isHeal ? "#fff5" : "#0005"; + for (const radius of [230, 180, 130, 80, 30]) { + DrawTools.arc(2300, -2200, radius, 0, Math.PI, true); + } + } + }, + room2Initiator: { + pos: { + x: 0, + y: -9050 + }, + get distance() { + return distance(player.position, this.pos); + }, + range: 120, + rings: [{ + colour: [102, 85, 255], + radius: 200 + }, { + colour: [0, 255, 0], + radius: 300 + }, { + colour: [255, 0, 0], + radius: 400 + }], + get ringNumber() { + return this.rings.length; + }, + get cap() { + return (this.ringNumber + 1) * 90 + 240; + }, + get capped() { + return templePlayer.room2.spawnInitiatorCycles > this.cap; + }, + logic() { + if (this.distance < this.range) { + templePlayer.room2.spawnInitiatorCycles++; + } + }, + draw() { + Promise.resolve().then(() => { + const cycle = templePlayer.room2.spawnInitiatorCycles; + if (!this.capped && this.distance < 400) { + ctx.fillStyle = `rgba(0, 0, 0, ${Math.min(1, (400 - this.distance) / (400 - this.range)) * 0.9})`; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + ctx.save(); + simulation.camera(); + if (this.distance < this.range && !this.capped) { + DrawTools.lightning(V.sub(this.pos, { x: 300, y: 300 }), V.add(this.pos, { x: 300, y: 300 }), simulation.cycle - 5); + DrawTools.lightning(V.add(this.pos, { x: -300, y: 300 }), V.add(this.pos, { x: 300, y: -300 }), simulation.cycle - 5); + } + if (!this.capped && cycle >= this.cap - 200) { + const multCoeff = (cycle - this.cap + 200) * 0.4 + ctx.translate((Math.random() - 0.5) * multCoeff, (Math.random() - 0.5) * multCoeff); + } + ctx.shadowBlur = 20; + ctx.lineWidth = 12; + ctx.strokeStyle = (templePlayer.room2.cycles % 60 < 30) ? "#fff" : "#000"; + ctx.shadowColor = (templePlayer.room2.cycles % 60 < 30) ? "#fff" : "#000"; + DrawTools.arcOut(this.pos.x, this.pos.y, 100, 0, Math.PI * 2); + if (templePlayer.room2.cycles <= 100) { + for (let i = 0; i < this.ringNumber; i++) { + if (cycle < i * 90 + 90) break; + const ring = this.rings[i]; + ctx.shadowColor = `rgb(${ring.colour.join(",")})`; + const opacity = this.capped ? 1 - 0.01 * templePlayer.room2.cycles : (cycle / 180 - i / 2 - 0.5); + ctx.strokeStyle = `rgba(${ring.colour.join(",")}, ${Math.min(opacity, 1)})`; + const radius = (this.capped ? 1 + 0.07 * templePlayer.room2.cycles : Math.sin(Math.min(cycle - i * 90 - 90, 45) / 90 * Math.PI)) * ring.radius; + DrawTools.arcOut(this.pos.x, this.pos.y, radius, 0, Math.PI * 2); + } + } + ctx.restore(); + }); + } + }, + room2Lightning: { + one: [{ x: -1400, y: -10400 }, { x: 1400, y: -8500 }], + two: [{ x: -1400, y: -8500 }, { x: 1400, y: -10400 }], + get isHeal() { + return simulation.cycle % 360 < 180; + }, + get oneEq() { + return Equation.fromPoints(this.one[0], this.one[1]); + }, + get twoEq() { + return Equation.fromPoints(this.two[0], this.two[1]); + }, + logic() { + if (!isInBound(secondRoomSpawnBounds) || !templePlayer.room2.cycles) return; + + const playerbounds = Rect.fromBounds(player.bounds.min, player.bounds.max); + if (playerbounds.hasLine(this.oneEq) || playerbounds.hasLine(this.twoEq)) { + if (this.isHeal) { + m.energy += 0.003; + } else if (m.immuneCycle < m.cycle) { + m.energy -= 0.003; + } + } + }, + draw() { + if (!isInBound(secondRoomBounds) || !templePlayer.room2.cycles) return; + + const colour = this.isHeal ? undefined : [0, 0, 0]; + DrawTools.lightning(...this.one, Math.floor(simulation.cycle / 15) * 15, 1, 9, colour); + DrawTools.lightning(...this.two, Math.floor(simulation.cycle / 15) * 15, 2, 9, colour); + } + }, + room2GeneratedPath: { + rects: (function() { + const rects = []; + for (let i = 0; i < 4; i++) { + rects.push(new Rect(-1405 + (i & 1) * 200, -9700 + i * 300, 205, 30)); + rects.push(new Rect(1200 - (i & 1) * 200, -9700 + i * 300, 205, 30)); + } + return rects; + })(), + logic() { + if (templePlayer.room2.readyPathCycle && simulation.cycle - templePlayer.room2.readyPathCycle === 180) { + for (const r of this.rects) { + r.addToMap(); + addPartToMap(); + simulation.draw.setPaths(); + } + } + }, + draw() { + if (templePlayer.room2.readyPathCycle && simulation.cycle - templePlayer.room2.readyPathCycle < 180) { + ctx.fillStyle = "#fe79"; + for (const r of this.rects) { + ctx.fillRect(r.pos.x, r.pos.y, r.width, r.height); + } + } else if (simulation.cycle - templePlayer.room2.readyPathCycle < 195) { + for (const r of this.rects) { + DrawTools.lightning(Objects.room2Initiator.pos, r.midPos, templePlayer.room2.readyPathCycle + 180); + } + } + } + }, + room3Rotors: { + rotor1: (function() { + const rotor = level.spinner(900, -13700, 200, 30); + rotor.rotate = function() { + Matter.Body.setAngularVelocity(this.bodyB, (this.bodyB.angularVelocity + 0.01) * 0.9) + } + return rotor; + })(), + rotor2: (function() { + const rotor = level.spinner(2700, -13700, 200, 30); + rotor.rotate = function() { + Matter.Body.setAngularVelocity(this.bodyB, (this.bodyB.angularVelocity - 0.01) * 0.9) + } + return rotor; + })(), + logic() { + this.rotor1.rotate(); + this.rotor2.rotate(); + } + }, + room3SlimePits: { + pit1: level.hazard(-100, -13400, 0, 0, 0.004), + pit2: level.hazard(3700, -13400, 0, 0, 0.004), + logic() { + if (templePlayer.room2ToRoom3Anim >= 1320 && templePlayer.room2ToRoom3Anim <= 1570) { + this.pit1.height = this.pit2.height = 300; + this.pit1.max.y = this.pit2.max.y = -13100; + this.pit1.width = this.pit2.width = templePlayer.room2ToRoom3Anim * 2 - 2640; + this.pit1.max.x = this.pit1.min.x + this.pit1.width; + this.pit2.min.x = this.pit2.max.x - this.pit2.width; + } + if (templePlayer.room3ToEndAnim) { + this.pit1.height = this.pit1.width = 0; + this.pit2.height = this.pit2.width = 0; + } + + }, + draw() { + this.pit1.query(); + this.pit2.query(); + } + } + }; + let templePlayer = { + room1: { + cycles: 300 + }, + room2: { + spawnInitiatorCycles: 0, + cycles: 0, + readyPathCycle: 0 + }, + stage: 1, + startAnim: 0, + room1ToRoom2Anim: 0, + room2ToRoom3Anim: 0, + room3ToEndAnim: 0, + initialTrapY: 0, + clearedCycle: 0, + drawExit: true + }; + + const RoomTransitionHandler = { + room0() { + if (templePlayer.startAnim <= 0) return; + templePlayer.startAnim++; + if (templePlayer.startAnim == 120) { + makeLore("Not so fast."); + } + if (templePlayer.startAnim < 360) { + trapPlayer(1000, templePlayer.initialTrapY); + } else { + level.exit.x = 4500; + level.exit.y = -2030; + relocateTo(50, -2050); + simulation.setZoom(1800); + templePlayer.startAnim = -1; + for (let i = 0; i < tech.wimpCount + tech.wimpExperiment; i++) { + addWIMP(); + } + templePlayer.drawExit = false; + } + }, + room1() { + if (templePlayer.room1ToRoom2Anim <= 0) return; + if (templePlayer.room1ToRoom2Anim === 1) { + level.exit.x = -50; + level.exit.y = -10030; + makeLore("Pathetic."); + } + if (templePlayer.room1ToRoom2Anim === 241) { + makeLore("You will never succeed."); + } + if (templePlayer.room1ToRoom2Anim >= 480 && templePlayer.room1ToRoom2Anim <= 960) { + const factor = 200 - 200 * Math.cos((templePlayer.room1ToRoom2Anim / 120) * Math.PI); + ctx.translate(factor, factor); + Promise.resolve().then(() => { + ctx.save(); + ctx.globalCompositeOperation = "color-burn"; + ctx.fillStyle = DrawTools.randomColours; + DrawTools.updateRandomColours(5); + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + }); + } + if (templePlayer.room1ToRoom2Anim === 960) { + makeLore("You are trying too hard."); + relocateTo(0, -7050); + templePlayer.stage = 2; + } + if (templePlayer.room1ToRoom2Anim === 1200) { + makeLore("I have mastered the understandings of the universe."); + } + if (templePlayer.room1ToRoom2Anim === 1260) { + // Congrats, you discovered the actual words by looking at the source code. Are you happy now? + const x = ( + ["a speck of dust", "an insignificant hindrance", "a tiny obstacle"] + )[Math.floor(Math.random() * 3)].split(""); + for (let i = 0; i < x.length / 1.6; i++) { + const randomIndex = Math.floor(Math.random() * x.length); + if (x[randomIndex] !== " ") { + x[randomIndex] = String.fromCharCode(Math.floor(Math.random() * 50) + 192); + } + }; + makeLore(`You are no more than ${x.join("")} to me.`); + relocateWIMPs(0, -10030); + } + templePlayer.room1ToRoom2Anim++; + }, + room2() { + if (templePlayer.room2ToRoom3Anim <= 0) return; + if (templePlayer.room2ToRoom3Anim === 1) { + level.exit.x = 3500; + level.exit.y = -13130; + makeLore("Do not try me."); + } + if (templePlayer.room2ToRoom3Anim === 240) { + makeLore("I have absolute power over you."); + canvas.style.filter = "hue-rotate(90deg)"; + } + if (templePlayer.room2ToRoom3Anim === 480) { + makeLore("You will not succeed..."); + canvas.style.filter = "invert(0.2)"; + } + if (templePlayer.room2ToRoom3Anim === 600) { + makeLore("