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("
succeed...
"); + canvas.style.filter = "invert(0.4)"; + } + if (templePlayer.room2ToRoom3Anim > 660 && templePlayer.room2ToRoom3Anim <= 840) { + canvas.style.filter = `sepia(${(templePlayer.room2ToRoom3Anim - 660) / 180}) invert(${0.5 + (templePlayer.room2ToRoom3Anim - 660) / 360})`; + } + if (templePlayer.room2ToRoom3Anim === 960) { + makeLore("Do not interfere with me."); + templePlayer.stage = 3; + relocateTo(50, -13150); + simulation.zoomTransition(1800); + templePlayer.startAnim = -1; + // Might be a bit harsh to the player if the WIMPs are involved in the third level + for (let i = 0; i < mob.length; i++) { + while (mob[i] && !mob[i].isWIMP) { + mob[i].replace(i); + } + } + } + if (templePlayer.room2ToRoom3Anim > 960 && templePlayer.room2ToRoom3Anim <= 1140) { + canvas.style.filter = `sepia(${(1140 - templePlayer.room2ToRoom3Anim) / 180}) invert(${(1140 - templePlayer.room2ToRoom3Anim) / 180})`; + } + templePlayer.room2ToRoom3Anim++; + }, + room3() { + if (templePlayer.room3ToEndAnim <= 0) return; + if (templePlayer.room3ToEndAnim === 1) { + makeLore("No."); + } + if (templePlayer.room3ToEndAnim === 120) { + makeLore("This cannot be."); + } + if (templePlayer.room3ToEndAnim === 240) { + makeLore("Has my power failed me?"); + } + if (templePlayer.room3ToEndAnim === 360) { + makeLore("Was it worth it, destroying this place?"); + } + if (templePlayer.room3ToEndAnim === 600) { + makeLore("No one is greater than me."); + } + const text = "noone-"; + for (let i = 0; i < 12; i++) { + if (templePlayer.room3ToEndAnim === 720 + i * 20) { + name = name.slice(0, -1); + simulation.makeTextLog(`${name}:   ${text[i % 6]}`); + canvas.style.filter = `brightness(${1 - i / 22})`; + } + } + if (templePlayer.room3ToEndAnim === 1060) { + templePlayer.drawExit = true; + for (let i = 0; i < 5 * tech.wimpCount; i++) { + powerUps.spawn(level.exit.x + 100 * (Math.random() - 0.5), level.exit.y - 100 + 100 * (Math.random() - 0.5), "research", false); + } + canvas.style.filter = ""; + } + templePlayer.room3ToEndAnim++; + }, + end() { + if (!templePlayer.clearedCycle) return; + Promise.resolve().then(() => { + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = `rgba(0, 0, 0, ${(simulation.cycle - templePlayer.clearedCycle) / 300})`; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + }); + if (simulation.cycle - templePlayer.clearedCycle > 420) level.nextLevel(); + } + }; + const LogicHandler = { + bounds() { + let isInBounds = false; + for (const b of bounds) { + if (isInBound(b)) { + isInBounds = true; + break; + } + } + // UwU + // Will I get timed out for this + if (!isInBounds) { + m.damage(0.1 * simulation.difficultyMode); + trapPlayer(level.enter.x, level.enter.y); + simulation.makeTextLog("" + name + ":   You thought I could let you get away with that?"); + } + }, + room0() { + // Block the player from entering the first seemingly innocuous exit + if ((m.pos.x > 1000) && templePlayer.startAnim === 0) { + spawn.mapRect(1200, -500, 100, 600); + templePlayer.initialTrapY = Math.min(player.position.y, -75); + trapPlayer(1000, templePlayer.initialTrapY); + + addPartToMap(); + simulation.draw.setPaths(); + templePlayer.startAnim = 1; + } + }, + room1() { + WaveHandler.room1(); + Objects.altar.logic(); + }, + room2() { + room2SlimePit.logic(); + Objects.room2Initiator.logic(); + Objects.room2Lightning.logic(); + Objects.room2GeneratedPath.logic(); + WaveHandler.room2(); + }, + room3() { + Objects.room3Rotors.logic(); + Objects.room3SlimePits.logic(); + WaveHandler.room3(); + }, + exit() { + if (!templePlayer.drawExit) return; + if (player.position.x > level.exit.x && + player.position.x < level.exit.x + 100 && + player.position.y > level.exit.y - 150 && + player.position.y < level.exit.y - 40 && + player.velocity.y < 0.1 && + level.exitCount + (input.down ? 8 : 2) > 100) { + if (templePlayer.stage === 1) { + templePlayer.drawExit = false; + templePlayer.room1ToRoom2Anim = 1; + } else if (templePlayer.stage === 2) { + templePlayer.drawExit = false; + templePlayer.room2ToRoom3Anim = 1; + } else { + level.exitCount = 99 - (input.down ? 8 : 2); + if (!templePlayer.clearedCycle) templePlayer.clearedCycle = simulation.cycle; + } + } + } + }; + const DrawHandler = { + // Bottom layer + base() { + // Draw base red background + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = color.map; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + + // Draw the normal bg on the bounds + ctx.fillStyle = "#eab6af"; + for (const b of bounds) { + if (isInBound(b)) ctx.fillRect(b.pos.x + 2, b.pos.y + 2, b.width - 4, b.height - 4); + } + }, + entrance() { + ctx.beginPath(); + ctx.moveTo(level.enter.x, level.enter.y + 30); + ctx.lineTo(level.enter.x, level.enter.y - 80); + ctx.bezierCurveTo(level.enter.x, level.enter.y - 170, level.enter.x + 100, level.enter.y - 170, level.enter.x + 100, level.enter.y - 80); + ctx.lineTo(level.enter.x + 100, level.enter.y + 30); + ctx.lineTo(level.enter.x, level.enter.y + 30); + ctx.fillStyle = "#fca"; + ctx.fill(); + }, + room1() { + if (!isInBound(firstRoomBounds)) return; + + // Draw Cross + ctx.fillStyle = "#fed"; + ctx.fillRect(2200, -3300, 200, 800); + ctx.fillRect(2000, -3100, 600, 200); + + // Final boss-like spawn fire thing. Was it necessary? No! + const spawnFlameAngle = Math.min(Math.min(templePlayer.room1.cycles, 2520) % 600, 120) * Math.PI / 30 + Math.PI / 2; + DrawTools.flame([2300, -3000, 26, 4, "#f60", spawnFlameAngle], 7); + + Objects.altar.drawBottom(); + }, + room2() { + if (!isInBound(secondRoomBounds)) return; + + if (templePlayer.room2.cycles) { + ctx.fillStyle = "#0006"; + ctx.fillRect(secondRoomBounds.pos.x + 2, secondRoomBounds.pos.y + 2, secondRoomBounds.width - 4, secondRoomBounds.height - 4); + } + room2SlimePit.draw(); + }, + room3() { + if (!isInBound(thirdRoomBounds)) return; + ctx.fillStyle = "#0006"; + ctx.fillRect(thirdRoomBounds.pos.x + 2, thirdRoomBounds.pos.y + 2, thirdRoomBounds.width - 4, thirdRoomBounds.height - 4); + Objects.room3SlimePits.draw(); + }, + // Top layer + mobTrails() { + if (simulation.cycle % 4 === 0) { + let newMobPositions = []; + for (const i of mob) { + if (!(i.isMACHO || i.isWIMP)) newMobPositions.push({ x: i.position.x, y: i.position.y }); + } + mobPositionsQueue.shift(); + mobPositionsQueue.push(newMobPositions); + } + // Draw "holy" trails for mobs for no particular reason at all + ctx.strokeStyle = "#bae"; + ctx.lineWidth = 3; + for (let i = 0; i < 10; i++) { + const p = mobPositionsQueue[i]; + for (const m of p) { + DrawTools.holy(m.x, m.y, i / 2 + 10); + } + } + ctx.shadowBlur = 0; + }, + waveTimer() { + const roomConditions = [ + isInBound(firstRoomBounds) && templePlayer.room1.cycles < 2400, + isInBound(secondRoomBounds) && templePlayer.room2.cycles > 0 && templePlayer.room2.cycles < 2160, + isInBound(thirdRoomBounds) && templePlayer.room2ToRoom3Anim < 1320 + ]; + Promise.resolve(roomConditions).then(roomConditions => { + // First Room + if (roomConditions[0]) { + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = "#0004"; + ctx.fillRect(canvas.width2 - 288, 50, 576, 20); + ctx.fillStyle = "#0cf"; + ctx.fillRect(canvas.width2 - 288, 50, 0.8 * (600 - templePlayer.room1.cycles % 600), 20); + ctx.restore(); + } + // Second Room + if (roomConditions[1]) { + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = "#0004"; + ctx.fillRect(canvas.width2 - 288, 50, 576, 20); + ctx.fillStyle = (Math.ceil(templePlayer.room2.cycles / 720) & 1) ? "#000" : "#e1d7ff"; + ctx.fillRect(canvas.width2 - 288, 50, 0.8 * (720 - templePlayer.room2.cycles % 720), 20); + ctx.restore(); + } + // Third Room + if (roomConditions[2]) { + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = "#0004"; + ctx.fillRect(canvas.width2 - 288, 50, 576, 20); + ctx.fillStyle = "#000"; + ctx.fillRect(canvas.width2 - 288, 50, 1.6 * (1320 - templePlayer.room2ToRoom3Anim), 20); + ctx.restore(); + } + }); + }, + room2Top() { + if (!isInBound(secondRoomBounds)) return; + Objects.room2Lightning.draw(); + Objects.room2GeneratedPath.draw(); + Objects.room2Initiator.draw(); + } + }; + const WaveHandler = { + room1() { + if (!isInBound(firstRoomBounds)) return; + if (templePlayer.room1.cycles === 0) powerUps.spawnStartingPowerUps(0, -2050); + templePlayer.room1.cycles++; + if (templePlayer.room1.cycles === 2400) { + spawn.secondaryBossChance(2300, -2800); + powerUps.addResearchToLevel(); + } + if (templePlayer.room1.cycles % 600 === 0 && templePlayer.room1.cycles <= 2400) { + for (let i = 0; i < 3 + Math.pow(simulation.difficulty / 2, 0.7) + Math.floor(templePlayer.room1.cycles / 720); i++) { + if (Math.random() < 0.5 + 0.07 * simulation.difficulty) { + spawn.randomMob(800 + Math.random() * 3e3, -2400 - Math.random() * 600, Infinity); + } + } + spawn.randomMob(800 + Math.random() * 3e3, -2400 - Math.random() * 600, Infinity); + } + if (templePlayer.room1.cycles === 2520) { + templePlayer.drawExit = true; + } + }, + room2() { + if (!isInBound(secondRoomBounds)) return; + if (templePlayer.room2.spawnInitiatorCycles > Objects.room2Initiator.cap) { + if (templePlayer.room2.cycles % 720 === 0 && templePlayer.room2.cycles <= 2160) { + const isOdd = Math.floor(templePlayer.room2.cycles / 720) & 1; + secondRoomBoss(-600, -9800, 25, isOdd); + secondRoomBoss(600, -9800, 25, isOdd); + secondRoomBoss(0, -9800, 25, !isOdd); + } + templePlayer.room2.cycles++; + if (templePlayer.room2.cycles === 2400) { + templePlayer.drawExit = true; + templePlayer.room2.readyPathCycle = simulation.cycle; + } + } + }, + room3() { + if (templePlayer.room2ToRoom3Anim === 1320) { + thirdRoomBoss(1800, -13700); + for (let i = 0; i < 3; i++) { + powerUps.spawn(m.spawnPos.x, m.spawnPos.y, "heal"); + } + } + } + }; + const DrawTools = { + get randomColours() { + return `rgb(${this._randomColours.join(",")})` + }, + _randomColours: [Math.random() * 255, Math.random() * 255, Math.random() * 255], + updateRandomColours(x = 0.8) { + for (let i = 0; i < this._randomColours.length; i++) { + this._randomColours[i] = Math.max(Math.min(this._randomColours[i] + (this.randFact() * x * 2) - x, 255), 0); + } + }, + randFact() { + return Math.random() * 0.8 + Math.sin(Date.now() / 300) * 0.2; + }, + + line(vecs) { + ctx.beginPath(); + ctx.moveTo(vecs[0].x, vecs[0].y); + for (const v of vecs.slice(1)) ctx.lineTo(v.x, v.y); + ctx.stroke(); + }, + arc(...x) { + ctx.beginPath(); + ctx.arc(...x); + ctx.fill(); + }, + arcOut(...x) { + ctx.beginPath(); + ctx.arc(...x); + ctx.stroke(); + }, + flame(props, repeat) { + for (let i = 0; i < repeat; i++) this.singleFlame(...props); + }, + singleFlame(x, y, size = 10, repeat = 3, color = "#f00", angle = Math.PI / 2) { + ctx.strokeStyle = color; + ctx.lineWidth = 3; + const path = [{ x, y }]; + for (let i = 0; i < repeat; i++) { + const randAng = (Math.random() - 0.5) * 2 + angle; + const randLen = 2 * size + Math.random() * size; + + x += Math.cos(randAng) * randLen; + y -= Math.sin(randAng) * randLen; + path.push({ x, y }) + } + DrawTools.line(path); + }, + lightning(from, to, cycle, randomPRNGMult = 1, width = 8, color = [255, 240, 127]) { + const diff = simulation.cycle - cycle; + if (diff >= 15) return; + ctx.strokeStyle = `rgba(${color.join(",")},${(1 - diff / 15) * 255})`; + ctx.lineWidth = width * (1 - diff / 15); + ctx.shadowColor = `rgb(${color.join(",")})`; + ctx.shadowBlur = 20; + const path = [{ x: from.x, y: from.y }]; + let vector = { x: from.x, y: from.y }; + let distanceLeft = V.magnitude(V.sub(from, to)); + const d = distanceLeft > 800 ? distanceLeft / 40 : 20; + const normalised = V.normalise(V.sub(to, from)); + while (1) { + const randOffset = rotateVector({ y: RNG(Math.floor(cycle * randomPRNGMult + distanceLeft)) * 2 * d - d, x: 0 }, normalised); + const randLen = RNG(Math.floor(cycle * (randomPRNGMult + 1) + distanceLeft)) * d + d; + distanceLeft -= randLen; + if (distanceLeft <= 0) { + path.push({ x: to.x, y: to.y }); + break; + } + vector = V.add(vector, V.mult(normalised, randLen)); + path.push({ x: vector.x + randOffset.x, y: vector.y + randOffset.y }); + } + DrawTools.line(path); + ctx.shadowBlur = 0; + }, + holy(x, y, size = 12) { + this.line([{ x, y: y - size }, { x: x - size, y }, + { x, y: y + size }, { x: x + size, y }, { x, y: y - size } + ]); + } + }; + + function RNG(x) { + x += Math.seed; + let start = Math.pow(x % 97, 4.3) * 232344573; + const a = 15485863; + const b = 521791; + start = (start * a) % b; + for (let i = 0; i < (x * x) % 90 + 90; i++) { + start = (start * a) % b; + } + return start / b; + } + + function rotateVector(v, ang) { + const c = typeof ang === "number" ? { x: Math.cos(ang), y: Math.sin(ang) } : V.normalise(ang); + return { x: v.x * c.x - v.y * c.y, y: v.y * c.x + v.x * c.y }; + } + + function trapPlayer(x, y) { + setPosAndFreeze(player, { x, y }); + const bLen = bullet.length; + for (let i = 0; i < bLen; i++) { + if (bullet[i].botType) setPosAndFreeze(bullet[i], V.add(player.position, { x: 100 * (RNG(i) - 0.5), y: 100 * (RNG(i + bLen) - 0.5) })); + } + } + + function relocateTo(x, y) { + level.setPosToSpawn(x, y); + trapPlayer(x, y); + for (let i = 0; i < mob.length; i++) { + if (mob[i].isMACHO) { + setPos(mob[i], { x, y }); + break; + } + } + m.resetHistory(); + } + const distance = (a, b) => V.magnitude(V.sub(a, b)); + const angle = (a, b) => Math.atan2(b.y - a.y, a.x - b.x); + const setPos = (a, b) => Matter.Body.setPosition(a, b); + const setVel = (a, b) => Matter.Body.setVelocity(a, b); + const freeze = a => setVel(a, { x: 0, y: 0 }); + const setPosAndFreeze = (a, b) => { + setPos(a, b); + freeze(a); + }; + const makeLore = x => simulation.makeTextLog(`

${name}:

 

${x}

`); + // TODO: Remove this before merging + window.TempleHelper = { + skipInitial() { + templePlayer.startAnim = 360; + }, + skipWaves() { + this.skipInitial(); + templePlayer.room1.cycles = 2500; + }, + skipToRoom2() { + this.skipInitial(); + templePlayer.room1ToRoom2Anim = 1; + }, + skipBossWaves() { + this.skipToRoom2(); + templePlayer.room2.spawnInitiatorCycles = Objects.room2Initiator.cap; + templePlayer.room2.cycles = 2200; + setTimeout(() => trapPlayer(0, -9100), 500); + }, + skipToRoom3() { + this.skipToRoom2(); + requestAnimationFrame(() => templePlayer.room2ToRoom3Anim = 1); + }, + spawnSecondRoomBoss(x, y) { + secondRoomBoss(x, y); + } + }; + level.custom = () => { + // All the logic gets handled here. How nice! + for (const i in LogicHandler) { + LogicHandler[i](); + } + + // Animations and lore for things that seem like exits + for (const i in RoomTransitionHandler) { + RoomTransitionHandler[i](); + } + + // Bottom layer graphics + DrawHandler.base(); + DrawHandler.room1(); + DrawHandler.room2(); + DrawHandler.room3(); + DrawHandler.entrance(); + if (templePlayer.drawExit) level.exit.drawAndCheck(); + }; + level.customTopLayer = () => { + // Top layer graphics + DrawHandler.mobTrails(); + Objects.altar.drawTop(); + DrawHandler.waveTimer(); + DrawHandler.room2Top(); + }; + }, // ******************************************************************************************************** // ******************************************************************************************************** // ***************************************** training levels ********************************************** diff --git a/js/player.js b/js/player.js index 97b1985..3b898fc 100644 --- a/js/player.js +++ b/js/player.js @@ -1533,7 +1533,7 @@ const m = { m.fieldBlockCD = 0; m.blockingRecoil = 2 //4 is normal m.fieldRange = 175 - m.fieldShieldingScale = 1.3 * Math.pow(0.6, (tech.harmonics - 2)) + m.fieldShieldingScale = (tech.isStandingWaveExpand ? 0.9 : 1.3) * Math.pow(0.6, (tech.harmonics - 2)) m.harmonic3Phase = () => { //normal standard 3 different 2-d circles const fieldRange1 = (0.75 + 0.3 * Math.sin(m.cycle / 23)) * m.fieldRange * m.harmonicRadius @@ -1557,8 +1557,9 @@ const m = { if (this.drainCD > m.cycle) { m.pushMass(mob[i], 0); } else { + console.log(this.drainCD) m.pushMass(mob[i]); - this.drainCD = m.cycle + 10 + this.drainCD = m.cycle + 15 } if (mob[i].isShielded || mob[i].shield) m.fieldCDcycle = m.cycle + 20 } @@ -1587,9 +1588,8 @@ const m = { m.pushMass(mob[i], 0); } else { m.pushMass(mob[i]); - this.drainCD = m.cycle + 10 + this.drainCD = m.cycle + 15 } - if (mob[i].isShielded || mob[i].shield) m.fieldCDcycle = m.cycle + 20 } } } diff --git a/js/spawn.js b/js/spawn.js index 4cf6700..cc4669e 100644 --- a/js/spawn.js +++ b/js/spawn.js @@ -187,7 +187,7 @@ const spawn = { ctx.stroke(); } }, - WIMP(x = level.exit.x + 300 * (Math.random() - 0.5), y = level.exit.y + 300 * (Math.random() - 0.5)) { //immortal mob that follows player //if you have the tech it spawns at start of every level at the exit + 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" @@ -259,9 +259,15 @@ const spawn = { // ctx.fill(); // ctx.globalCompositeOperation = "source-over" } - me.do = function() { //wake up 2 seconds after the player moves + me.do = function() { //wake up after the player moves if (player.speed > 1 && !m.isCloak) { - setTimeout(() => { this.do = this.awake; }, 2000); + 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; + }, 500 + 2000 * Math.random()); } this.checkStatus(); }; diff --git a/js/tech.js b/js/tech.js index d2f32d7..ef5c2bd 100644 --- a/js/tech.js +++ b/js/tech.js @@ -3129,7 +3129,7 @@ const tech = { }, { name: "unified field theory", - description: `clicking the field box when paused cycles your field
triple the frequency of finding field tech
`, + description: `clicking the field box when paused cycles your field
triple the frequency of finding fieldtech
`, maxCount: 1, count: 0, frequency: 1, @@ -3771,7 +3771,7 @@ const tech = { }, { name: "needle gun", - description: "nail gun and shot gun fire mob piercing needles", + description: "nail gun and shotgun fire mob piercing needles", isGunTech: true, maxCount: 1, count: 0, @@ -3810,7 +3810,7 @@ const tech = { }, { name: "rivet gun", - description: "nail gun and shot gun slowly lob a heavy rivet", + description: "nail gun and shotgun slowly lob a heavy rivet", isGunTech: true, maxCount: 1, count: 0, @@ -3819,7 +3819,7 @@ const tech = { allowed() { return ((tech.haveGunCheck("nail gun") && !tech.nailInstantFireRate) || (tech.haveGunCheck("shotgun") && !tech.isNailShot && !tech.isFoamShot && !tech.isSporeWorm)) && !tech.isNeedles && !tech.isIceCrystals && !tech.isIceShot }, - requires: "nail gun, shot gun, not ice crystal, needles, or pneumatic actuator", + requires: "nail gun, shotgun, not ice crystal, needles, or pneumatic actuator", effect() { tech.isRivets = true for (i = 0, len = b.guns.length; i < len; i++) { //find which gun @@ -4250,7 +4250,7 @@ const tech = { frequency: 1, frequencyDefault: 1, allowed() { - return tech.haveGunCheck("shotgun") || tech.haveGunCheck("super balls") || (tech.isRivets && !tech.isNailCrit) || (m.fieldUpgrades[m.fieldMode].name === "molecular assembler" && !(tech.isDroneTeleport || tech.isDroneRadioactive || tech.isSporeField || tech.isMissileField || tech.isIceField)) || (tech.haveGunCheck("drones") && !tech.isForeverDrones && !tech.isDroneRadioactive && !tech.isDroneTeleport) + return (tech.haveGunCheck("shotgun") && !tech.isNailShot && !tech.isIceShot && !tech.isRivets && !tech.isFoamShot && !tech.isSporeWorm && !tech.isNeedles) || tech.haveGunCheck("super balls") || (tech.isRivets && !tech.isNailCrit) || (m.fieldUpgrades[m.fieldMode].name === "molecular assembler" && !(tech.isDroneTeleport || tech.isDroneRadioactive || tech.isSporeField || tech.isMissileField || tech.isIceField)) || (tech.haveGunCheck("drones") && !tech.isForeverDrones && !tech.isDroneRadioactive && !tech.isDroneTeleport) }, requires: "shotgun, super balls, rivets, drones, not irradiated drones or burst drones", effect() { @@ -6192,7 +6192,7 @@ const tech = { }, { name: "spherical harmonics", - description: "standing wave oscillates in a 3rd dimension
increase deflecting efficiency by 40%", + description: "standing wave deflects 40% more efficiently
no longer deactivates with mob shields", //standing wave oscillates in a 3rd dimension
isFieldTech: true, maxCount: 9, count: 0, @@ -6204,18 +6204,18 @@ const tech = { requires: "standing wave", effect() { tech.harmonics++ - m.fieldShieldingScale = (tech.isStandingWaveExpand ? 1.1 : 1.3) * Math.pow(0.6, (tech.harmonics - 2)) + m.fieldShieldingScale = (tech.isStandingWaveExpand ? 0.9 : 1.3) * Math.pow(0.6, (tech.harmonics - 2)) m.harmonicShield = m.harmonicAtomic }, remove() { tech.harmonics = 2 - m.fieldShieldingScale = (tech.isStandingWaveExpand ? 1.1 : 1.3) * Math.pow(0.6, (tech.harmonics - 2)) + m.fieldShieldingScale = (tech.isStandingWaveExpand ? 0.9 : 1.3) * Math.pow(0.6, (tech.harmonics - 2)) m.harmonicShield = m.harmonic3Phase } }, { name: "expansion", - description: "using standing wave field expands its radius
increase deflecting efficiency by 25%", + description: "standing wave deflects 40% more efficiently
using standing wave field expands its radius", // description: "use energy to expand standing wave
the field slowly contracts when not used", isFieldTech: true, maxCount: 1, @@ -6223,16 +6223,16 @@ const tech = { frequency: 2, frequencyDefault: 2, allowed() { - return m.fieldUpgrades[m.fieldMode].name === "standing wave" + return m.fieldUpgrades[m.fieldMode].name === "standing wave" && (tech.blockDmg || tech.blockingIce) }, - requires: "standing wave", + requires: "standing wave, bremsstrahlung, triple point", effect() { tech.isStandingWaveExpand = true - m.fieldShieldingScale = (tech.isStandingWaveExpand ? 1.1 : 1.3) * Math.pow(0.6, (tech.harmonics - 2)) + m.fieldShieldingScale = (tech.isStandingWaveExpand ? 0.9 : 1.3) * Math.pow(0.6, (tech.harmonics - 2)) }, remove() { tech.isStandingWaveExpand = false - m.fieldShieldingScale = (tech.isStandingWaveExpand ? 1.1 : 1.3) * Math.pow(0.6, (tech.harmonics - 2)) + m.fieldShieldingScale = (tech.isStandingWaveExpand ? 0.9 : 1.3) * Math.pow(0.6, (tech.harmonics - 2)) m.harmonicRadius = 1 } }, @@ -6376,7 +6376,7 @@ const tech = { }, { name: "neutronium", - description: `reduce harm by 90% when your field is active
move and jump 33% slower`, + description: `reduce harm by 90% when your field is active
move and jump 25% slower`, isFieldTech: true, maxCount: 1, count: 0, @@ -6388,8 +6388,8 @@ const tech = { requires: "negative mass, not mass-energy", effect() { tech.isNeutronium = true - tech.baseFx *= 0.66 - tech.baseJumpForce *= 0.66 + tech.baseFx *= 0.75 + tech.baseJumpForce *= 0.75 m.setMovement() }, //also removed in m.setHoldDefaults() if player switches into a bad field @@ -7631,25 +7631,24 @@ const tech = { }, remove() {} }, - { - name: "hi", - description: `spawn to seed`, - maxCount: 1, - count: 0, - frequency: 0, - isNonRefundable: true, - isJunk: true, - allowed() { - return true - }, - requires: "", - effect() { - document.getElementById("seed").placeholder = Math.initialSeed = String(616) - Math.seed = Math.abs(Math.hash(Math.initialSeed)) //update randomizer seed in case the player changed it - - }, - remove() {} - }, + // { + // name: "hi", + // description: `spawn to seed 616 `, + // maxCount: 1, + // count: 0, + // frequency: 0, + // isNonRefundable: true, + // isJunk: true, + // allowed() { + // return true + // }, + // requires: "", + // effect() { + // document.getElementById("seed").placeholder = Math.initialSeed = String(616) + // Math.seed = Math.abs(Math.hash(Math.initialSeed)) //update randomizer seed in case the player changed it + // }, + // remove() {} + // }, { name: "meteor shower", description: "take a shower, but meteors instead of water", diff --git a/todo.txt b/todo.txt index bebd6b4..ef8532c 100644 --- a/todo.txt +++ b/todo.txt @@ -1,28 +1,21 @@ ******************************************************** NEXT PATCH ************************************************** -generalist now requires 2 guns to unlock - active cooling 18->20% fire speed increase per gun - arsenal 12->13% damage increase per gun -futures exchange 4.3->4.5% duplication on cancel -perimeter defense 7->6% harm reduction per bot -unified field theory no longer gets bonus rerolls, instead it triples the frequency of field tech - removed tensor field -meta-analysis gives 3->1 research after choosing JUNK +new fan level temple by Scar1337 is now add to community maps! + you have to try it out! -plasma ball moves faster 7->10 +standing wave buffs + standing wave deflecting, is more efficient for multiple blocks in a very short time (< 1s) + spherical harmonics no longer deactivates on contact with shielded mobs + expansion increases block efficiency by 25->40% -bug fixes -JUNK tech density removed - it was causing the NaN bug +negative mass field + neutronium: move 33->25% slower ******************************************************** TODO ******************************************************** - nonrefundable tech don't display, this is confusing maybe they can show up but greyed out or something -make player collisions with mobs do no harm while standing wave is active - bring back: the old phase decoherence field make cloak only active on input.field down