From fcbbf2937559b875b05c6e365e4474728a5c965d Mon Sep 17 00:00:00 2001 From: landgreen Date: Fri, 22 Apr 2022 19:51:39 -0700 Subject: [PATCH] temple new fan level temple by Scar1337 is now add to community maps! you have to try it out! 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% negative mass field neutronium: move 33->25% slower --- .DS_Store | Bin 6148 -> 6148 bytes js/level.js | 1397 +++++++++++++++++++++++++++++++++++++++++++++++++- js/player.js | 8 +- js/spawn.js | 12 +- js/tech.js | 69 ++- todo.txt | 23 +- 6 files changed, 1449 insertions(+), 60 deletions(-) diff --git a/.DS_Store b/.DS_Store index 65fe6c87ee9a7cf21ec7f75ee4c0d29f3ea1b78e..307961e1a135f79bc071b1de80a998d06423d11e 100644 GIT binary patch delta 23 ecmZoMXffEJ$;_;@=h$Q&W?v@d3!AH%J4FCpY6rdm delta 23 ecmZoMXffEJ$;_;{NoBGQvoDj9!scq`P7wf5ZU$fg 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