From 09c9e93fcffd08c4a4b72add69ac63f89afe5beb Mon Sep 17 00:00:00 2001 From: landgreen Date: Sat, 3 Jun 2023 13:56:18 -0700 Subject: [PATCH] subway level: subway replaces gauntlet just before the final boss gauntlet moved to community map pool subway todo: add a few more stations balance difficulty find bugs surface plasmons does 50% more damage elasticity renamed nitinol because I bought some nitinol wire and it's neat entanglement power up no longer shows guns or fields you already have disabled minimal HUD for training levels reaction inhibitor 12->11% mob max health reduction spawn.bodyRect() now can add blocks mid level without any extra code I think I found all the bugs this causes, but let me know if any blocks added mid game aren't colliding community map - clock update: visual overhaul,live lighting, remove pendulum overlap, move with pendulum if you stand in it, debris, trap exit only opens after completing the fight, moving elements now freeze while using time dilation --- img/{elasticity.webp => nitinol.webp} | Bin js/index.js | 17 +- js/level.js | 1162 ++++++++++++++++++++++--- js/mob.js | 5 +- js/player.js | 8 +- js/powerup.js | 19 +- js/simulation.js | 489 ++++++----- js/spawn.js | 61 +- js/tech.js | 26 +- todo.txt | 86 +- 10 files changed, 1428 insertions(+), 445 deletions(-) rename img/{elasticity.webp => nitinol.webp} (100%) diff --git a/img/elasticity.webp b/img/nitinol.webp similarity index 100% rename from img/elasticity.webp rename to img/nitinol.webp diff --git a/js/index.js b/js/index.js index d81eb6e..e1cc1eb 100644 --- a/js/index.js +++ b/js/index.js @@ -303,7 +303,13 @@ const build = { // console.log(localSettings.isHideImages, from) }, hideHUD() { - localSettings.isHideHUD = !localSettings.isHideHUD + + if (simulation.isTraining) { + localSettings.isHideHUD = false + } else { + localSettings.isHideHUD = !localSettings.isHideHUD + } + if (localSettings.isAllowed) localStorage.setItem("localSettings", JSON.stringify(localSettings)); //update local storage document.getElementById("hide-hud").checked = localSettings.isHideHUD document.getElementById("hide-hud").classList.toggle("ticked") @@ -1299,12 +1305,7 @@ window.addEventListener("keydown", function (event) { powerUps.directSpawn(simulation.mouseInGame.x, simulation.mouseInGame.y, "tech"); break case "6": - const index = body.length spawn.bodyRect(simulation.mouseInGame.x, simulation.mouseInGame.y, 50, 50); - body[index].collisionFilter.category = cat.body; - body[index].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet - body[index].classType = "body"; - Composite.add(engine.world, body[index]); //add to world break case "7": const pick = spawn.fullPickList[Math.floor(Math.random() * spawn.fullPickList.length)]; @@ -1452,7 +1453,7 @@ document.body.addEventListener("wheel", (e) => { //********************************************************************** let localSettings -function localstorageCheck() { +function localStorageCheck() { try { return 'localStorage' in window && window['localStorage'] !== null; } catch (e) { @@ -1460,7 +1461,7 @@ function localstorageCheck() { } } -if (localstorageCheck()) { +if (localStorageCheck()) { localSettings = JSON.parse(localStorage.getItem("localSettings")) if (localSettings) { console.log('localStorage is enabled') diff --git a/js/level.js b/js/level.js index fac1148..791add1 100644 --- a/js/level.js +++ b/js/level.js @@ -10,7 +10,7 @@ const level = { // playableLevels: ["pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion", "pavilion"], //see level.populateLevels: (intro, ... , reservoir or factory, reactor, ... , gauntlet, final) added later playableLevels: ["labs", "rooftops", "skyscrapers", "warehouse", "highrise", "office", "aerie", "satellite", "sewers", "testChamber", "pavilion", "lock"], - communityLevels: ["stronghold", "basement", "crossfire", "vats", "run", "ngon", "house", "perplex", "coliseum", "tunnel", "islands", "temple", "dripp", "biohazard", "stereoMadness", "yingYang", "staircase", "fortress", "commandeer", "clock", "buttonbutton", "downpour", "superNgonBros", "underpass"], + communityLevels: ["gauntlet", "stronghold", "basement", "crossfire", "vats", "run", "ngon", "house", "perplex", "coliseum", "tunnel", "islands", "temple", "dripp", "biohazard", "stereoMadness", "yingYang", "staircase", "fortress", "commandeer", "clock", "buttonbutton", "downpour", "superNgonBros", "underpass"], trainingLevels: ["walk", "crouch", "jump", "hold", "throw", "throwAt", "deflect", "heal", "fire", "nailGun", "shotGun", "superBall", "matterWave", "missile", "stack", "mine", "grenades", "harpoon"], levels: [], start() { @@ -18,25 +18,25 @@ const level = { // simulation.enableConstructMode() //tech.giveTech('motion sickness') //used to build maps in testing mode // simulation.isHorizontalFlipped = true // tech.giveTech("performance") - // level.difficultyIncrease(4 * 4) //30 is near max on hard //60 is near max on why + // level.difficultyIncrease(8 * 4) //30 is near max on hard //60 is near max on why // spawn.setSpawnList(); // spawn.setSpawnList(); // m.maxHealth = m.health = 100 // tech.isRerollDamage = true - // powerUps.research.changeRerolls(7) + // powerUps.research.changeRerolls(99999) // m.immuneCycle = Infinity //you can't take damage // tech.tech[297].frequency = 100 // m.couplingChange(10) - // m.setField("time dilation") //1 standing wave 2 perfect diamagnetism 3 negative mass 4 molecular assembler 5 plasma torch 6 time dilation 7 metamaterial cloaking 8 pilot wave 9 wormhole + // m.setField("negative mass") //1 standing wave 2 perfect diamagnetism 3 negative mass 4 molecular assembler 5 plasma torch 6 time dilation 7 metamaterial cloaking 8 pilot wave 9 wormhole // m.energy = 0 // simulation.molecularMode = 2 // m.damage(0.1); - // b.giveGuns("harpoon") //0 nail gun 1 shotgun 2 super balls 3 wave 4 missiles 5 grenades 6 spores 7 drones 8 foam 9 harpoon 10 mine 11 laser + // b.giveGuns("nail gun") //0 nail gun 1 shotgun 2 super balls 3 wave 4 missiles 5 grenades 6 spores 7 drones 8 foam 9 harpoon 10 mine 11 laser // b.giveGuns("shotgun") //0 nail gun 1 shotgun 2 super balls 3 wave 4 missiles 5 grenades 6 spores 7 drones 8 foam 9 harpoon 10 mine 11 laser // b.guns[3].ammo = 100000000 - // tech.giveTech("propagator") - // tech.giveTech("missile-bot") - // for (let i = 0; i < 6; ++i) tech.giveTech("replication") + // tech.giveTech("nitinol") + // tech.giveTech("surface plasmons") + // for (let i = 0; i < 6; ++i) tech.giveTech("Lorentz transformation") // for (let i = 0; i < 1; ++i) tech.giveTech("waste heat recovery") // requestAnimationFrame(() => { for (let i = 0; i < 1; i++) tech.giveTech("foam-bot") }); // for (let i = 0; i < 1; i++) tech.giveTech("foam-bot upgrade") @@ -44,7 +44,7 @@ const level = { // for (let i = 0; i < 3; i++) powerUps.directSpawn(450, -50, "tech"); // for (let i = 0; i < 10; i++) powerUps.directSpawn(1750, -500, "research"); // for (let i = 0; i < 10; i++) powerUps.directSpawn(1750, -500, "coupling"); - // level.testing(); + // level.subway(); // spawn.nodeGroup(3200, -300, "sniper") // spawn.nodeGroup(2200, -300, "sniper") // spawn.nodeGroup(2200, -300, "sniper") @@ -65,7 +65,7 @@ const level = { // simulation.isAutoZoom = false; //look in close // simulation.zoomScale *= 0.5; // simulation.setZoom(); - // for (let i = 0; i < 4; ++i) powerUps.directSpawn(m.pos.x + 50 * Math.random(), m.pos.y + 50 * Math.random(), "tech"); + // for (let i = 0; i < 2; ++i) powerUps.directSpawn(m.pos.x + 50 * Math.random(), m.pos.y + 50 * Math.random(), "tech"); // for (let i = 0; i < 2; ++i) powerUps.directSpawn(m.pos.x + 50 * Math.random(), m.pos.y + 50 * Math.random(), "boost"); // for (let i = 0; i < 100; ++i) powerUps.directSpawn(m.pos.x + 50 * Math.random(), m.pos.y + 50 * Math.random(), "heal"); // for (let i = 0; i < 2; i++) powerUps.spawn(player.position.x + Math.random() * 50, player.position.y - Math.random() * 50, "field", false); @@ -111,23 +111,16 @@ const level = { if (tech.isForeverDrones) { if (tech.isDroneRadioactive) { for (let i = 0; i < tech.isForeverDrones * 0.25; i++) { - b.droneRadioactive({ - x: m.pos.x + 30 * (Math.random() - 0.5), - y: m.pos.y + 30 * (Math.random() - 0.5) - }, 5) + b.droneRadioactive({ x: m.pos.x + 30 * (Math.random() - 0.5), y: m.pos.y + 30 * (Math.random() - 0.5) }, 5) bullet[bullet.length - 1].endCycle = Infinity } } else { for (let i = 0; i < tech.isForeverDrones; i++) { - b.drone({ - x: m.pos.x + 30 * (Math.random() - 0.5), - y: m.pos.y + 30 * (Math.random() - 0.5) - }, 5) + b.drone({ x: m.pos.x + 30 * (Math.random() - 0.5), y: m.pos.y + 30 * (Math.random() - 0.5) }, 5) bullet[bullet.length - 1].endCycle = Infinity } } } - if (tech.isMACHO) spawn.MACHO() for (let i = 0; i < tech.wimpCount; i++) { spawn.WIMP() @@ -338,7 +331,7 @@ const level = { level.levels.splice(Math.floor(Math.seededRandom(level.levels.length * 0.6, level.levels.length)), 0, "reactor"); //add level to the back half of the randomized levels list if (!build.isExperimentSelection || (build.hasExperimentalMode && !simulation.isCheating)) { //experimental mode is endless, unless you only have an experiment Tech level.levels.unshift("intro"); //add level to the start of the randomized levels list - level.levels.push("gauntlet"); //add level to the end of the randomized levels list + level.levels.push("subway"); //add level to the end of the randomized levels list level.levels.push("final"); //add level to the end of the randomized levels list } } @@ -480,14 +473,6 @@ const level = { // } }, addToWorld() { //needs to be run to put bodies into the world - for (let i = 0; i < body.length; i++) { - if (body[i] !== m.holdingTarget && !body[i].isNoSetCollision) { - body[i].collisionFilter.category = cat.body; - body[i].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet - } - body[i].classType = "body"; - Composite.add(engine.world, body[i]); //add to world - } for (let i = 0; i < map.length; i++) { map[i].collisionFilter.category = cat.map; map[i].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.powerUp | cat.mob | cat.mobBullet; @@ -512,9 +497,10 @@ const level = { }); Matter.Body.setAngle(who, angle) Matter.Body.setAngularVelocity(who, angularVelocity); - - Matter.Body.setDensity(who, density) + Composite.add(engine.world, who); //add to world + who.classType = "body" + const constraint = Constraint.create({ //fix rotor in place, but allow rotation pointA: { x: who.position.x, @@ -590,6 +576,8 @@ const level = { // } // } + Composite.add(engine.world, who); //add to world + who.classType = "body" return who }, @@ -756,6 +744,8 @@ const level = { } }); Matter.Body.setDensity(who, 0.01) //10x density for added stability + Composite.add(engine.world, who); //add to world + who.classType = "body" return who }, spring(x, y, v = "-100 0 100 0 70 40 0 50 -70 40", force = 0.01, distance = 300, angle = 0) { @@ -899,15 +889,16 @@ const level = { toggle(x, y, isOn = false, isLockOn = false) { spawn.mapVertex(x + 65, y + 2, "70 10 -70 10 -40 -10 40 -10"); //toggle platform map[map.length - 1].restitution = 0; - map[map.length - 1].friction = 1; + map[map.length - 1].friction = 1; // map[map.length - 1].frictionStatic = 1; - spawn.bodyRect(x, y - 5, 120, 15) //toggle body called flip + const width = 120 + const height = 15 + body[body.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height, { friction: 0.05, frictionAir: 0.01 }); let flip = body[body.length - 1]; flip.isNoSetCollision = true //prevents collision from being rewritten in level.addToWorld flip.collisionFilter.category = cat.body flip.collisionFilter.mask = cat.player | cat.body flip.isNotHoldable = true - flip.frictionAir = 0.01 flip.restitution = 0 Matter.Body.setDensity(flip, 0.003) if (isOn) { @@ -925,7 +916,8 @@ const level = { length: 0 }); Composite.add(engine.world, [cons[cons.length - 1]]); - + Composite.add(engine.world, flip); //add to world + flip.classType = "body" return { flip: flip, isOn: isOn, @@ -1185,7 +1177,8 @@ const level = { }, }); Matter.Body.setStatic(block, true); //make static - // Composite.add(engine.world, block); //add to world + Composite.add(engine.world, block); //add to world + // who.classType = "body" if (simulation.isHorizontalFlipped) x *= -1 return block }, @@ -1266,6 +1259,8 @@ const level = { } }); Matter.Body.setStatic(doorBlock, true); //make static + Composite.add(engine.world, doorBlock); //add to world + doorBlock.classType = "body" return doorBlock }, portal(centerA, angleA, centerB, angleB) { @@ -1673,21 +1668,9 @@ const level = { isMover: true, VxGoal: VxGoal, force: force, - changeDirection(isRight) { - if (isRight) { - this.VxGoal = Math.abs(this.VxGoal) - this.force = Math.abs(this.force) - if (Matter.Query.collides(this, [jumpSensor]).length) player.force.x += 12 * Math.abs(this.VxGoal) * this.force * player.mass - } else { - this.VxGoal = -Math.abs(this.VxGoal) - this.force = -Math.abs(this.force) - if (Matter.Query.collides(this, [jumpSensor]).length) player.force.x += 12 * Math.abs(this.VxGoal) * this.force * player.mass - } - }, move() { if (!m.isBodiesAsleep) { Matter.Body.setPosition(this, { x: this.position.x + this.VxGoal, y: this.position.y }); //horizontal movement - const touchingPlayer = Matter.Query.collides(this, [jumpSensor]) if (touchingPlayer.length) { m.moverX = this.VxGoal @@ -1721,9 +1704,64 @@ const level = { ctx.lineTo(v[0].x, v[0].y); ctx.fillStyle = "#586370" ctx.fill(); - } + }, + changeDirection(isRight) { + if (isRight) { + this.VxGoal = Math.abs(this.VxGoal) + this.force = Math.abs(this.force) + if (Matter.Query.collides(this, [jumpSensor]).length) player.force.x += this.trainKickPlayer * this.force * player.mass + } else { + this.VxGoal = -Math.abs(this.VxGoal) + this.force = -Math.abs(this.force) + if (Matter.Query.collides(this, [jumpSensor]).length) player.force.x += this.trainKickPlayer * this.force * player.mass + } + }, + trainSpeed: Math.abs(VxGoal), + trainKickPlayer: 12 * Math.abs(force), + isSensing: false, + stops: { left: x, right: x + 1000 }, //this should probably be reset in the level code for the actual train stops + trainStop() { + if (this.isMoving) { + this.move(); + //oscillate back and forth + if (this.position.x < this.stops.left) {//stop + this.VxGoal = this.trainSpeed + this.force = 0.0005 + this.isMoving = false + this.isSensing = false + if (Matter.Query.collides(this, [jumpSensor]).length) player.force.x += this.trainKickPlayer * player.mass * (this.VxGoal > 0 ? 1 : -1)//give player a kick so they don't fall off + } else if (this.position.x > this.stops.right) {//stop + this.VxGoal = -this.trainSpeed + this.force = -0.0005 + this.isMoving = false + this.isSensing = false + if (Matter.Query.collides(this, [jumpSensor]).length) player.force.x += this.trainKickPlayer * player.mass * (this.VxGoal > 0 ? 1 : -1)//give player a kick so they don't fall off + } + } else if (this.isSensing) { + if (Matter.Query.collides(this, [jumpSensor]).length) { + this.isMoving = true + this.move(); //needs to move out of the stop range + // if (Matter.Query.collides(this, [jumpSensor]).length) player.force.x += trainKickPlayer * player.mass * (this.VxGoal > 0 ? 1 : -1)//give player a kick so they don't fall off + if (Matter.Query.collides(this, [jumpSensor]).length) { + Matter.Body.setVelocity(player, { x: this.VxGoal, y: player.velocity.y }); + } + } else if (this.position.x > this.stops.right && player.position.x < this.stops.left + 500) {//head to other stop if the player is far away + this.changeDirection(false) //go left + this.isMoving = true + this.move(); //needs to move out of the stop range + } else if (this.position.x < this.stops.left && player.position.x > this.stops.right - 500) {//head to other stop if the player is far away + this.changeDirection(true) //go right + this.isMoving = true + this.move(); //needs to move out of the stop range + } + } else if (!Matter.Query.collides(this, [jumpSensor]).length) {//wait until player is off the train to start sensing + this.isSensing = true + } + }, }); Matter.Body.setStatic(rect, true); //make static + Composite.add(engine.world, rect); //add to world + rect.classType = "body" return rect }, chain(x, y, angle = 0, isAttached = true, len = 15, radius = 20, stiffness = 1, damping = 1) { @@ -1737,6 +1775,11 @@ const level = { inertia: Infinity, isNotHoldable: true }); + const who = body[body.length - 1] + who.collisionFilter.category = cat.body; + who.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet + Composite.add(engine.world, who); //add to world + who.classType = "body" } for (let i = 1; i < len; i++) { //attach blocks to each other consBB[consBB.length] = Constraint.create({ @@ -2561,9 +2604,11 @@ const level = { if (simulation.difficulty * Math.random() > 7 * i) spawn.randomGroup(5000 + 500 * (Math.random() - 0.5), -800 + 200 * (Math.random() - 0.5), Infinity); } } - + if (simulation.difficulty > 1) { + spawn.randomLevelBoss(5750, -600); + spawn.secondaryBossChance(4125, -350) + } powerUps.addResearchToLevel() //needs to run after mobs are spawned - spawn.secondaryBossChance(4125, -350) if (simulation.isHorizontalFlipped) { //flip the map horizontally level.flipHorizontal(); //only flips map,body,mob,powerUp,cons,consBB, exit @@ -2581,6 +2626,477 @@ const level = { }; } }, + subway() { + // simulation.enableConstructMode() //tech.giveTech('motion sickness') //used to build maps in testing mode + // level.difficultyIncrease(10 * 4); + // spawn.setSpawnList(); //picks a couple mobs types for a themed random mob spawns + m.maxHealth = m.health = 100 + + const mobSpawnCap = mobs.mobDeaths + 100 + level.isProcedural = true //used in generating text for the level builder + simulation.draw.drawMapPath = simulation.draw.drawMapSight + + // color.map = "#333" //custom map color + document.body.style.backgroundColor = "#e3e3e3"//"#e3e3e3"//color.map//"#333"//"#000" + level.defaultZoom = 1400 + simulation.zoomTransition(level.defaultZoom) + level.setPosToSpawn(1400 * (Math.random() < 0.5 ? 1 : -1), -250); //normal spawn + // spawn.mapRect(level.enter.x, level.enter.y + 20, 100, 20); //entrance bump disabled for performance + level.exit.x = 0; + level.exit.y = -9000; + // spawn.mapRect(level.exit.x, level.exit.y + 20, 100, 100); //exit bump disabled for performance + + + const stationWidth = 9000 + let stationNumber = 0; + let stationCustom = () => { } + let stationCustomTopLayer = () => { } + const train = [] + train.push(level.transport(1475, -200, 500, 25, 30)) + train[train.length - 1].isMoving = false + train[train.length - 1].stops = { left: 1725, right: 7225 } + train.push(level.transport(-1475 - 500, -200, 500, 25, -30)) + train[train.length - 1].isMoving = false + train[train.length - 1].stops = { left: -7225, right: -1725 } + + const stationList = [] //organize the possible stations into a random order, with one station removed + for (let i = 0, totalNumberOfStations = 4; i < totalNumberOfStations; ++i) stationList.push(i) + shuffle(stationList); + stationList.splice(Math.floor(Math.random() * stationList.length), 1); //remove one random element from array + let isExitOpen = false + const infrastructure = (x, isInProgress = true) => { + if (isInProgress) { + spawn.setSpawnList(); //picks a couple mobs types for a themed random mob spawns + + const mobsLeft = Math.floor(mobSpawnCap - mobs.mobDeaths - 50 / simulation.difficultyMode) + if (mobsLeft > 0) { + simulation.makeTextLog(`${mobsLeft} mobs left before exit opens`); + } else { + isExitOpen = true + simulation.makeTextLog(`exit opened`); + } + + function removeAll(array) { + for (let i = 0; i < array.length; ++i) Matter.Composite.remove(engine.world, array[i]); + } + removeAll(map); + map = []; + // removeAll(body); //can't remove bodies because it removes the train + // body = []; + // removeAll(mob); + // mob = []; + // removeAll(bullet); + // bullet = []; + // removeAll(composite); + // composite = []; + removeAll(powerUp); + powerUp = []; + // removeAll(cons); // don't allow constraints that don't come from a mob (like elevators, rotors?) + // cons = []; + // removeAll(consBB); // don't allow constraints that don't come from a mob (like elevators, rotors?) + // consBB = []; + + //remove any mob that is too far from player + for (let i = 0; i < mob.length; ++i) { + if (Vector.magnitudeSquared(Vector.sub(player.position, mob[i].position)) > 4000000) { //remove any mob farther then 2000 pixels from player + mob[i].removeConsBB() + mob[i].removeCons() + mob[i].leaveBody = false + mob[i].alive = false + Matter.Composite.remove(engine.world, mob[i]); + mob.splice(i--, 1) + } + } + } else { + for (let i = 0; i < 8; ++i) powerUps.chooseRandomPowerUp(100 * (Math.random() - 0.5), -200 - 100 * Math.random())//only spawn heal or ammo once at the first station + } + const stations = [ //update totalNumberOfStations as you add move stations + () => { //portals + if (isExitOpen) { + level.exit.x = x + 950; + level.exit.y = -1725; + } + + spawn.mapVertex(x + 400, -180, "-300 0 -300 -100 300 -100 400 0"); + spawn.mapVertex(x - 400, -180, "300 0 300 -100 -300 -100 -400 0"); + spawn.mapRect(x + -1500, -210, 1425, 350); //station floor left + spawn.mapRect(x + 75, -210, 1425, 350); //station floor right + spawn.mapRect(x + 75, -950, 50, 450); + spawn.mapRect(x + 125, -700, 1225, 200); + spawn.mapRect(x + -1325, -1775, 775, 175); + spawn.mapVertex(x + 445, -800, "-200 0 -200 -300 100 -300 185 0"); + spawn.mapVertex(x - 350, -1850, "-185 0 -100 -400 400 -400 400 0"); + spawn.mapVertex(x + -675, -725, "325 0 250 80 -250 80 -325 0 -250 -80 250 -80"); + spawn.mapRect(x + 650, -1700, 725, 550); + spawn.mapRect(x + 1500, -2375, 275, 450); + + if (mobs.mobDeaths < mobSpawnCap) { + spawn.randomMob(x + -750, -1925, 0); + spawn.randomMob(x + -425, -2300, 0); + spawn.randomMob(x + -350, -2200, 0); + spawn.randomMob(x + -275, -2175, 0); + spawn.randomMob(x + -375, -2175, 0); + spawn.randomMob(x + 1075, -1850, 0); + spawn.randomMob(x + 925, -1775, 0); + spawn.randomMob(x + 1150, -1800, 0); + spawn.randomMob(x + 1400, -2150, 0); + spawn.randomMob(x + 925, -850, 0); + spawn.randomMob(x + 800, -800, 0); + spawn.randomMob(x + 875, -825, 0); + spawn.randomMob(x + 1050, -900, 0); + spawn.randomMob(x + 19050, -2925, 0); + spawn.randomMob(x + 17150, -3150, 0); + spawn.randomMob(x + 17700, -3300, 0); + } + + if (!isInProgress) spawn.secondaryBossChance(420, -2200) // spawn.randomLevelBoss(5750, -600); + + const portal1 = level.portal({ + x: x + 0, + y: -200 + }, -Math.PI / 2, { //right + x: x + 200, + y: -900 + }, -Math.PI / 2) //right + const portal2 = level.portal({ + x: x + 1275, + y: -800 + }, Math.PI, { //right + x: x + -1275, + y: -1875 + }, 2 * Math.PI) //right + + stationCustom = () => { + portal1[2].query(true) + portal1[3].query(true) + portal2[2].query() + portal2[3].query() + } + stationCustomTopLayer = () => { + portal1[0].draw(); + portal1[1].draw(); + portal1[2].draw(); + portal1[3].draw(); + portal2[0].draw(); + portal2[1].draw(); + portal2[2].draw(); + portal2[3].draw(); + } + }, + () => { //tower levels and squares + if (isExitOpen) { + level.exit.x = x - 450; + level.exit.y = -3150; + } + spawn.mapRect(x + -1500, -210, 3000, 400);//station floor + spawn.mapRect(x + -1625, -3950, 3225, 350);//roof + spawn.mapRect(x + 1300, -3850, 300, 2150); //roof wall + spawn.mapRect(x + -1625, -3950, 325, 2250); //roof wall + spawn.mapRect(x + -1050, -575, 1000, 75); + spawn.mapRect(x + 175, -575, 975, 75); + spawn.mapRect(x + -1050, -825, 150, 275); + spawn.mapRect(x + -900, -1200, 2275, 75); + spawn.mapRect(x + 125, -1425, 1250, 300); + spawn.mapRect(x + -925, -1775, 2100, 75); + spawn.mapRect(x + -100, -2050, 950, 350); + spawn.mapRect(x + -925, -2100, 100, 400); + spawn.mapRect(x + -700, -2375, 1225, 75); + spawn.mapRect(x + 650, -2375, 575, 75); + spawn.mapRect(x + -25, -2750, 350, 269); + spawn.mapRect(x + -950, -3125, 975, 75); + spawn.mapRect(x + 325, -3025, 900, 75); + spawn.bodyRect(x + -125, -1325, 225, 125, 0.3); + spawn.bodyRect(x + -225, -2100, 300, 50, 0.3); + spawn.bodyRect(x + -225, -2575, 100, 200, 0.3); + spawn.bodyRect(x + 850, -2575, 150, 200, 0.3); + spawn.bodyRect(x + 850, -1875, 75, 100, 0.3); + spawn.bodyRect(x + 500, -725, 175, 150, 0.3); + spawn.bodyRect(x + -925, -2250, 100, 150, 0.3); + spawn.bodyRect(x + -1050, -950, 150, 125, 0.3); + + const mobPlacement = [ + () => { //1st floor + spawn.randomMob(x + -775, -725, 0); + spawn.randomMob(x + -575, -700, 0); + spawn.randomMob(x + -275, -700, 0); + spawn.randomMob(x + -125, -650, 0); + spawn.randomMob(x + 250, -675, 0); + spawn.randomMob(x + 425, -650, 0); + spawn.randomMob(x + 775, -650, 0); + spawn.randomMob(x + 1050, -675, 0); + spawn.randomMob(x + 675, -950, 0); + spawn.randomMob(x + -625, -900, 0); + spawn.randomMob(x + -750, -1400, 0); + spawn.randomMob(x + -500, -2025, 0); + spawn.randomMob(x + -125, -3225, 0); + }, + () => { //2nd floor + spawn.randomMob(x + -950, -925, 0); + spawn.randomMob(x + -775, -1325, 0); + spawn.randomMob(x + -450, -1500, 0); + spawn.randomMob(x + -325, -1250, 0); + spawn.randomMob(x + 0, -1500, 0); + spawn.randomMob(x + 375, -1525, 0); + spawn.randomMob(x + 750, -1550, 0); + spawn.randomMob(x + 1175, -1550, 0); + spawn.randomMob(x + -875, -1350, 0); + spawn.randomMob(x + -875, -2375, 0); + spawn.randomMob(x + 175, -2850, 0); + spawn.randomMob(x + 750, -2475, 0); + }, + () => {//3rd floor + spawn.randomMob(x + 1075, -2000, 0); + spawn.randomMob(x + 725, -2125, 0); + spawn.randomMob(x + 350, -2125, 0); + spawn.randomMob(x + -325, -2000, 0); + spawn.randomMob(x + -675, -1875, 0); + spawn.randomMob(x + -725, -2200, 0); + spawn.randomMob(x + -675, -2575, 0); + spawn.randomMob(x + -425, -2675, 0); + spawn.randomMob(x + -50, -2875, 0); + spawn.randomMob(x + 425, -2725, 0); + spawn.randomMob(x + 1150, -2550, 0); + spawn.randomMob(x + 1150, -2175, 0); + spawn.randomMob(x + 1000, -1900, 0); + spawn.randomMob(x + 500, -2550, 0); + spawn.randomMob(x + 125, -2900, 0); + }, + () => {//all floors + spawn.randomMob(x + 1000, -850, 0); + spawn.randomMob(x + 300, -850, 0); + spawn.randomMob(x + -450, -825, 0); + spawn.randomMob(x + -1025, -1125, 0); + spawn.randomMob(x + -750, -1375, 0); + spawn.randomMob(x + -225, -1375, 0); + spawn.randomMob(x + 625, -1525, 0); + spawn.randomMob(x + 1025, -1925, 0); + spawn.randomMob(x + -425, -2100, 0); + spawn.randomMob(x + -400, -2650, 0); + spawn.randomMob(x + 150, -3000, 0); + spawn.randomMob(x + 675, -3200, 0); + spawn.randomMob(x + -550, -3300, 0); + + }, + ] + if (mobs.mobDeaths < mobSpawnCap) mobPlacement[Math.floor(Math.random() * mobPlacement.length)]()//different random mob placements, with mobs clustered to surprise player + if (!isInProgress) spawn.secondaryBossChance(900, -2700) // spawn.randomLevelBoss(5750, -600); + stationCustom = () => { } + stationCustomTopLayer = () => { } + }, + () => { //jump pads and 6 sided platforms + if (isExitOpen) { + if (Math.random() < 0.5) { + level.exit.x = x - 2075; + level.exit.y = -2450; + } else { + level.exit.x = x + 2250; + level.exit.y = -2020; + } + } + spawn.mapRect(x + -1500, -210, 3000, 400);//station floor + spawn.mapRect(x + -3200, -3200, 300, 1400); //roof left wall + spawn.mapRect(x + 2600, -3200, 300, 1400);//roof right wall + spawn.mapRect(x + -3175, -3200, 6175, 225);//roof + if (Math.random() < 0.3) spawn.mapRect(x + -1350, -550, 150, 50); //wall ledge + if (Math.random() < 0.3) spawn.mapRect(x + 1175, -550, 200, 50); //wall ledge + spawn.mapVertex(x + 600, -900, "490 0 350 80 -350 80 -490 0 -350 -80 350 -80"); //hexagon but wide + spawn.mapVertex(x - 600, -750, "490 0 350 80 -350 80 -490 0 -350 -80 350 -80"); //hexagon but wide + spawn.mapVertex(x - 100, -1850, "-100 -300 0 -350 100 -300 100 300 0 350 -100 300"); //hexagon but tall + spawn.mapVertex(x + -600, -2000, "-150 -450 150 -450 150 450 0 525 -150 450"); //hexagon but big and tall and flat + spawn.mapVertex(x + 350, -1575, "-150 0 150 0 150 450 0 525 -150 450"); //hexagon but tall and flat top + spawn.mapVertex(x + 850, -1575, "-150 0 150 0 150 450 0 525 -150 450"); //hexagon but tall and flat top + spawn.mapVertex(x + -2050, -2350, "490 0 350 80 -350 80 -490 0 -350 -80 350 -80"); //left top corner hexagon but wide + spawn.mapVertex(x + 1700, -2450, "-100 -300 0 -350 100 -300 100 300 0 350 -100 300"); //hexagon but tall + + const mobPlacement = [ + () => { //rightish + spawn.randomMob(x + 2250, -2375, 0); + spawn.randomMob(x + 1950, -2825, 0); + spawn.randomMob(x + 1275, -2775, 0); + spawn.randomMob(x + 1450, -2200, 0); + spawn.randomMob(x + 825, -1950, 0); + spawn.randomMob(x + 400, -1875, 0); + spawn.randomMob(x + -75, -2275, 0); + spawn.randomMob(x + -650, -2550, 0); + spawn.randomMob(x + 1500, -2075, 0); + spawn.randomMob(x + 2125, -2650, 0); + spawn.randomMob(x + 2075, -2250, 0); + spawn.randomMob(x + 1000, -2850, 0); + spawn.randomMob(x + 750, -950, 0); + spawn.randomMob(x + -750, -1125, 0); + spawn.randomMob(x + -1575, -2050, 0); + }, + () => { //leftish + spawn.randomMob(x + -2225, -2125, 0); + spawn.randomMob(x + -2675, -2125, 0); + spawn.randomMob(x + -2600, -2600, 0); + spawn.randomMob(x + -2100, -2725, 0); + spawn.randomMob(x + -1425, -2600, 0); + spawn.randomMob(x + -1375, -2050, 0); + spawn.randomMob(x + -575, -2575, 0); + spawn.randomMob(x + -125, -2300, 0); + spawn.randomMob(x + 350, -1925, 0); + spawn.randomMob(x + -350, -1050, 0); + spawn.randomMob(x + -1000, -1000, 0); + spawn.randomMob(x + -700, -1300, 0); + spawn.randomMob(x + 350, -1150, 0); + spawn.randomMob(x + -575, -2525, 0); + spawn.randomMob(x + -1075, -2525, 0); + }, + () => {//centerish + spawn.randomMob(x + 25, -2275, 0); + spawn.randomMob(x + 300, -1975, 0); + spawn.randomMob(x + 700, -1950, 0); + spawn.randomMob(x + 325, -1200, 0); + spawn.randomMob(x + -225, -950, 0); + spawn.randomMob(x + -925, -975, 0); + spawn.randomMob(x + -675, -2575, 0); + spawn.randomMob(x + -1425, -2175, 0); + spawn.randomMob(x + 1575, -2075, 0); + spawn.randomMob(x + 2300, -2075, 0); + spawn.randomMob(x + 425, -1925, 0); + spawn.randomMob(x + 125, -2175, 0); + spawn.randomMob(x + -325, -2150, 0); + spawn.randomMob(x + -350, -950, 0); + spawn.randomMob(x + 600, -325, 0); + spawn.randomMob(x + -600, -375, 0); + }, + ] + if (mobs.mobDeaths < mobSpawnCap) mobPlacement[Math.floor(Math.random() * mobPlacement.length)]()//different random mob placements, with mobs clustered to surprise player + if (!isInProgress) spawn.secondaryBossChance(600, -2500) // spawn.randomLevelBoss(5750, -600); + const boost1 = level.boost(x - 50, -225, 790) + const boost2 = level.boost(x + 550, -985, 900) + const boost3 = level.boost(x + -850, -835, 1900) + stationCustom = () => { } + stationCustomTopLayer = () => { + boost1.query(); + boost2.query(); + boost3.query(); + } + }, + () => { //crouch tunnels + if (isExitOpen) { + level.exit.x = x + 0; + level.exit.y = -1620; + } + spawn.mapRect(x + -1500, -210, 3000, 400);//station floor + spawn.mapRect(x + -1575, -2200, 3025, 300); //roof + spawn.mapRect(x + -1100, -925, 100, 425); + spawn.mapRect(x + -1100, -575, 375, 75); + spawn.mapRect(x + -925, -1300, 375, 125); + spawn.mapRect(x + -300, -1300, 620, 125); + spawn.mapRect(x + 450, -1400, 500, 225); + spawn.mapRect(x + 900, -550, 500, 50); + spawn.mapRect(x + 950, -925, 400, 270); + spawn.mapRect(x + 1250, -1250, 150, 75); + spawn.mapRect(x + -225, -525, 800, 210); + spawn.mapRect(x + -100, -1600, 300, 193); + spawn.mapRect(x + 925, -1250, 75, 75); + spawn.bodyRect(x + 625, -1550, 150, 150, 0.3); + spawn.bodyRect(x + 200, -1475, 75, 175, 0.3); + spawn.bodyRect(x + -25, -625, 225, 100, 0.3); + spawn.bodyRect(x + -1000, -750, 125, 175, 0.3); + spawn.bodyRect(x + -625, -1450, 75, 150, 0.3); + spawn.bodyRect(x + -650, -300, 300, 75, 0.3); + if (mobs.mobDeaths < mobSpawnCap) { + spawn.randomMob(x + -750, -1425, 0); + spawn.randomMob(x + -1050, -1100, 0); + spawn.randomMob(x + -825, -750, 0); + spawn.randomMob(x + -500, -400, 0); + spawn.randomMob(x + 450, -650, 0); + spawn.randomMob(x + 0, -725, 0); + spawn.randomMob(x + 300, -1350, 0); + spawn.randomMob(x + 550, -1500, 0); + spawn.randomMob(x + 725, -1650, 0); + spawn.randomMob(x + 900, -1550, 0); + spawn.randomMob(x + 1100, -1300, 0); + spawn.randomMob(x + -1050, -1050, 0); + spawn.randomMob(x + -925, -350, 0); + spawn.randomMob(x + 75, -1750, 0); + spawn.randomMob(x + 1000, -375, 0); + } + if (!isInProgress) spawn.secondaryBossChance(100, -850) // spawn.randomLevelBoss(5750, -600); + stationCustom = () => { } + stationCustomTopLayer = () => { + ctx.fillStyle = "rgba(0,0,0,0.08)" + ctx.fillRect(x + -225, -325, 800, 125); + ctx.fillRect(x + -100, -1425, 300, 125); + ctx.fillRect(x + 950, -675, 400, 125); + } + }, + ] + // stations[3]() //for testing a specific station + stations[stationList[Math.abs((stationNumber + Math.floor(stationList.length / 2)) % stationList.length)]]() //*************** run this one when uploading + //add in standard station map infrastructure + spawn.mapRect(x + -8000, 0, 16000, 800);//tunnel floor + spawn.mapRect(x + 1500 - 200, -2000, 6400, 1500); //tunnel roof + spawn.mapRect(x + -1500 - 6400 + 200, -2000, 6400, 1500); //tunnel roof + + // add debris so you can see how fast the train moves + const debrisCount = 4 + const size = 18 + Math.random() * 25; + const wide = 6400 + if (isInProgress) { + //adds new map elements to the level while the level is already running + for (let i = 0; i < map.length; ++i) { + map[i].collisionFilter.category = cat.map; + map[i].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.powerUp | cat.mob | cat.mobBullet; + Matter.Body.setStatic(map[i], true); //make static + Composite.add(engine.world, map[i]); //add to world + } + simulation.draw.setPaths() + + //shift trains left/right, as you move left or right a train will jump over and become the train needed at the next station + let repositionTrain + let playerOnTrain + if (Math.abs(train[0].position.x - m.pos.x) > Math.abs(train[1].position.x - m.pos.x)) { //figure out which train the player is farthest from and move it + repositionTrain = train[0] + playerOnTrain = train[1] + } else { + repositionTrain = train[1] + playerOnTrain = train[0] + } + repositionTrain.isMoving = false + if (repositionTrain.position.x > playerOnTrain.position.x) { //decide if the train is moving left or right + Matter.Body.setPosition(repositionTrain, { x: -1725 + x, y: repositionTrain.position.y }); + repositionTrain.changeDirection(false) //go left + repositionTrain.stops = { right: -1725 + x, left: -7225 + x } + for (let i = 0; i < debrisCount; ++i) spawn.bodyRect(x + -1500 - 6400 + 200 + Math.random() * wide, -35, size * (0.6 + Math.random()), size * (0.6 + Math.random()), 1); + } else { + Matter.Body.setPosition(repositionTrain, { x: 1725 + x, y: repositionTrain.position.y }); + repositionTrain.changeDirection(true) //go right + repositionTrain.stops = { left: 1725 + x, right: 7225 + x } + for (let i = 0; i < debrisCount; ++i) spawn.bodyRect(x + 1500 - 200 + Math.random() * wide, -35, size * (0.6 + Math.random()), size * (0.6 + Math.random()), 1); + } + } else { + for (let i = 0; i < debrisCount; ++i) spawn.bodyRect(x + -1500 - 6400 + 200 + Math.random() * wide, -35, size * (0.6 + Math.random()), size * (0.6 + Math.random()), 1); + for (let i = 0; i < debrisCount; ++i) spawn.bodyRect(x + 1500 - 200 + Math.random() * wide, -35, size * (0.6 + Math.random()), size * (0.6 + Math.random()), 1); + } + } + infrastructure(0, false) //if this is run before the level starts, it needs a false + + level.custom = () => { + for (let i = 0; i < train.length; i++) train[i].trainStop() + ctx.fillStyle = "rgba(0,0,0,0.1)"//"#ddd" + ctx.fillRect(m.pos.x - 4000, m.pos.y - 4000, 8000, 8000) + level.exit.drawAndCheck(); + // level.enter.draw(); + + //track what station the player is in + if (m.pos.x > 0.55 * stationWidth + stationNumber * stationWidth) { + stationNumber++ + infrastructure(stationNumber * stationWidth) + } else if (m.pos.x < -0.55 * stationWidth + stationNumber * stationWidth) { + stationNumber-- + infrastructure(stationNumber * stationWidth) + } + stationCustom() + }; + level.customTopLayer = () => { + for (let i = 0; i < train.length; i++) train[i].draw() + stationCustomTopLayer() + }; + }, reservoir() { level.exit.x = 1700; level.exit.y = -4510; @@ -3106,9 +3622,7 @@ const level = { simulation.zoomTransition(level.defaultZoom) document.body.style.backgroundColor = "#d0d2d4s"; // color.map = "#262a2f" - let isPowerLeft = true - const movers = [] //left side movers.push(level.mover(125, -140, 925, 35, -5)) @@ -3120,7 +3634,6 @@ const level = { movers.push(level.mover(7175, -215, 2275, 50, 3)) movers.push(level.mover(6475, -215, 275, 100, -3)) movers.push(level.mover(6725, -500, 500, 375, 3)) - movers.push(level.mover(7675, -725, 500, 410, 0)) movers.push(level.mover(6775, -1075, 375, 50, 0)) movers.push(level.mover(5525, -1075, 450, 50, 0)) @@ -3128,9 +3641,7 @@ const level = { movers.push(level.mover(5450, -1900, 525, 50, 0)) function setMoverDirection(VxGoal) { - for (let i = 7; i < movers.length; i++) { - movers[i].VxGoal = VxGoal - } + for (let i = 7; i < movers.length; i++) movers[i].VxGoal = VxGoal } setMoverDirection(0) @@ -3152,18 +3663,9 @@ const level = { const button1 = level.button(2235, -200) button1.isUp = true let bonusAmmoCount = 0 - spawnBlock = (x, y) => { - const index = body.length - spawn.bodyRect(x, y, 50, 50); // spawn.bodyRect(x, y, 40 + Math.floor(30 * Math.random()), 40 + Math.floor(30 * Math.random())); - body[index].collisionFilter.category = cat.body; - body[index].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet - body[index].classType = "body"; - Composite.add(engine.world, body[index]); //add to world - } - level.custom = () => { if (isPowerLeft) { - if (!(simulation.cycle % 90)) spawnBlock(2730, -1600); + if (!(simulation.cycle % 90)) spawn.bodyRect(2730, -1600, 50, 50); } else { // for (let i = 0; i < trains.length; i++) { // //oscillate back and forth @@ -3187,7 +3689,7 @@ const level = { movers[3].force = 0 movers[2].VxGoal = 0; movers[2].force = 0 - spawnBlock(2730, -1600); //3315, -1600); + spawn.bodyRect(2730, -1600, 50, 50); if ((simulation.cycle % (rate * 3)) === 0) { if (bonusAmmoCount < 3 && Math.random() < 0.5) { //some extra ammo because of all the extra mobs that don't drop ammo bonusAmmoCount++ @@ -3390,7 +3892,7 @@ const level = { powerUps.spawn(5200, -1300, "ammo"); }, labs() { - level.isProcedural = true //used in generating text it the level builder + level.isProcedural = true //used in generating text for the level builder level.defaultZoom = 1700 simulation.zoomTransition(level.defaultZoom) document.body.style.backgroundColor = "#d9d9de" //"#d3d3db" //"#dcdcdf"; @@ -3658,10 +4160,7 @@ const level = { button.isReadyToFire = true } else if (button.isReadyToFire && !button.isUp) { button.isReadyToFire = false - b.pulse(90, 0, { - x: x + 560, - y: y - 150 - }) + b.pulse(90, 0, { x: x + 560, y: y - 150 }) } } ) @@ -3699,11 +4198,7 @@ const level = { spawn.bodyRect(xPos, yPos, 35 + 50 * Math.random(), 35 + 50 * Math.random()); const bodyBullet = body[body.length - 1] Matter.Body.setVelocity(body[index], { x: -120, y: -5 }); - body[index].collisionFilter.category = cat.body; - body[index].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet - body[index].classType = "body"; body[index].isAboutToBeRemoved = true; - Composite.add(engine.world, body[index]); //add to world setTimeout(() => { //remove block for (let i = 0; i < body.length; i++) { if (body[i] === bodyBullet) { @@ -3752,14 +4247,9 @@ const level = { const index = body.length spawn.bodyRect(xPos, yPos, 35 + 50 * Math.random(), 35 + 50 * Math.random()); const bodyBullet = body[body.length - 1] - Matter.Body.setVelocity(body[index], { - x: 120, - y: -5 - }); - body[index].collisionFilter.category = cat.body; - body[index].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet - body[index].classType = "body"; - Composite.add(engine.world, body[index]); //add to world + Matter.Body.setVelocity(body[index], { x: 120, y: -5 }); + body[index].isAboutToBeRemoved = true; + setTimeout(() => { //remove block for (let i = 0; i < body.length; i++) { if (body[i] === bodyBullet) { @@ -4121,18 +4611,9 @@ const level = { simulation.draw.setPaths() //update map graphics //blocks that ride the movers and portals - const addBody = (index) => { - body[index].collisionFilter.category = cat.body; - body[index].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet - body[index].classType = "body"; - Composite.add(engine.world, body[index]); //add to world - } spawn.bodyRect(x + 175, y + -2525, 50, 75); - addBody(body.length - 1) spawn.bodyRect(x + 300, y + -2525, 50, 50); - addBody(body.length - 1) spawn.bodyRect(x + 500, y + -2525, 80, 75); - addBody(body.length - 1) //mobs go here spawn.randomMob(x + 175, y + -125, 0); @@ -16408,7 +16889,7 @@ const level = { simulation.makeTextLog(`clock by Cornbread 2100`); function drawBackgroundGear(x, y, r1, r2, rot, color, speed, numTeeth = 5, toothWidth = 75, linew = 2) { - var vertices = getGearVertices(x, y, r1, r2, numTeeth, m.cycle * speed + rot, toothWidth / 100); + var vertices = getGearVertices(x, y, r1, r2, numTeeth, simulation.cycle * speed + rot, toothWidth / 100); // draw gear ctx.beginPath(); @@ -16434,7 +16915,7 @@ const level = { for (var i = 0; i <= numTeeth; i++) { var gear_r = r2; if (i % 2 == 1) gear_r = r1; - ctx.arc(x, y + (m.cycle - startCycle) * fallSpeed, gear_r, (i * gearInc) + rot, ((i + 1) * gearInc) + rot); + ctx.arc(x, y + (simulation.cycle - startCycle) * fallSpeed, gear_r, (i * gearInc) + rot, ((i + 1) * gearInc) + rot); } ctx.fillStyle = color; ctx.fill(); @@ -16627,7 +17108,7 @@ const level = { who.center = { x: x, y: y } who.rotate = function () { - var rotation = m.cycle * speed + rot; + var rotation = simulation.cycle * speed + rot; Matter.Body.setAngle(who, rotation); } @@ -16685,13 +17166,13 @@ const level = { who.center = { x: who.position.x, y: who.position.y } who.rotate = function () { - if (m.cycle % 60 == 0) { + if (simulation.cycle % 60 == 0) { who.handRotation += speed; if (Math.abs(who.handRotation % (Math.PI * 2) - Math.PI) < 0.2) { // spawn random mob at exit door const pick = spawn.fullPickList[Math.floor(Math.random() * spawn.fullPickList.length)]; spawn[pick](300, 600); - }; + } if (Matter.Query.collides(player, [this]).length != 0) { var playerAngle = Math.atan((m.pos.y - y) / (m.pos.x - x)); if (m.pos.x - x < 0) playerAngle += Math.PI; @@ -16764,7 +17245,18 @@ const level = { who.center = { x: x, y: y + height / 2 } who.rotate = function () { - var rotation = Math.sin(m.cycle / swingTime) * swingDistanceMultiplier; + var rotation = Math.sin(simulation.cycle / swingTime) * swingDistanceMultiplier; + + if (Matter.Query.collides(player, [this]).length != 0) { + var playerAngle = Math.atan((player.position.y - y) / (player.position.x - x)) + rotation - Math.sin((simulation.cycle - 1) / swingTime) * swingDistanceMultiplier; + if (player.position.x - x < 0) playerAngle += Math.PI; + const playerDistance = Math.sqrt((player.position.x - x) ** 2 + (player.position.y - y) ** 2); + Matter.Body.setPosition(player, { + x: x + Math.cos(playerAngle) * playerDistance, + y: y + Math.sin(playerAngle) * playerDistance + }) + } + Matter.Body.setAngle(who, rotation); } @@ -16874,6 +17366,8 @@ const level = { } }); Matter.Body.setStatic(doorBlock, true); //make static + Composite.add(engine.world, doorBlock); //add to world + doorBlock.classType = "body" return doorBlock } @@ -16935,6 +17429,8 @@ const level = { } }); Matter.Body.setStatic(doorBlock, true); //make static + Composite.add(engine.world, doorBlock); //add to world + doorBlock.classType = "body" return doorBlock } @@ -17028,6 +17524,312 @@ const level = { ctx.stroke(); } + function getIntersection(v1, v1End, domain) { + const intersections = getIntersections(v1, v1End, domain); + + var best = { + x: v1End.x, + y: v1End.y, + dist: Math.sqrt((v1End.x - v1.x) ** 2 + (v1End.y - v1.y) ** 2) + } + for (const intersection of intersections) { + const dist = Math.sqrt((intersection.x - v1.x) ** 2 + (intersection.y - v1.y) ** 2); + if (dist < best.dist) { + best = { + x: intersection.x, + y: intersection.y, + dist: dist + }; + } + } + + return best; + } + + function getIntersections(v1, v1End, domain) { + const intersections = []; + + for (const obj of domain) { + for (var i = 0; i < obj.vertices.length - 1; i++) { + results = simulation.checkLineIntersection(v1, v1End, obj.vertices[i], obj.vertices[i + 1]); + if (results.onLine1 && results.onLine2) intersections.push({ x: results.x, y: results.y }); + } + results = simulation.checkLineIntersection(v1, v1End, obj.vertices[obj.vertices.length - 1], obj.vertices[0]); + if (results.onLine1 && results.onLine2) intersections.push({ x: results.x, y: results.y }); + } + + return intersections; + } + + function circleLoS(pos, radius, domain) { + + function allCircleLineCollisions(c, radius, domain) { + var lines = []; + for (const obj of domain) { + //const obj = domain[0] + for (var i = 0; i < obj.vertices.length - 1; i++) { + lines.push(circleLineCollisions(obj.vertices[i], obj.vertices[i + 1], c, radius)); + } + lines.push(circleLineCollisions(obj.vertices[obj.vertices.length - 1], obj.vertices[0], c, radius)); + } + + const collisionLines = []; + for (const line of lines) { + if (line.length == 2) { + const distance1 = Math.sqrt((line[0].x - c.x) ** 2 + (line[0].y - c.y) ** 2) + const angle1 = Math.atan2(line[0].y - c.y, line[0].x - c.x); + const queryPoint1 = { + x: Math.cos(angle1) * (distance1 - 1) + c.x, + y: Math.sin(angle1) * (distance1 - 1) + c.y + } + const distance2 = Math.sqrt((line[1].x - c.x) ** 2 + (line[1].y - c.y) ** 2) + const angle2 = Math.atan2(line[1].y - c.y, line[1].x - c.x); + const queryPoint2 = { + x: Math.cos(angle2) * (distance2 - 1) + c.x, + y: Math.sin(angle2) * (distance2 - 1) + c.y + } + + collisionLines.push(line) + } + } + + return collisionLines; + } + + function circleLineCollisions(a, b, c, radius) { + // calculate distances + const angleOffset = Math.atan2(b.y - a.y, b.x - a.x); + const sideB = Math.sqrt((a.x - c.x) ** 2 + (a.y - c.y) ** 2); + const sideC = Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2); + const sideA = Math.sqrt((c.x - b.x) ** 2 + (c.y - b.y) ** 2); + + // calculate the closest point on line AB to point C + const angleA = Math.acos((sideB ** 2 + sideC ** 2 - sideA ** 2) / (2 * sideB * sideC)) * (a.x - c.x) / -Math.abs(a.x - c.x) + const sideAD = Math.cos(angleA) * sideB; + const d = { // closest point + x: Math.cos(angleOffset) * sideAD + a.x, + y: Math.sin(angleOffset) * sideAD + a.y + } + const distance = Math.sqrt((d.x - c.x) ** 2 + (d.y - c.y) ** 2); + if (distance == radius) { + // tangent + return [d]; + } else if (distance < radius) { + // secant + const angleOffset = Math.atan2(d.y - c.y, d.x - c.x); + const innerAngle = Math.acos(distance / radius); + const intersection1 = { + x: Math.cos(angleOffset + innerAngle) * radius + c.x, + y: Math.sin(angleOffset + innerAngle) * radius + c.y + } + + const intersection2 = { + x: Math.cos(angleOffset - innerAngle) * radius + c.x, + y: Math.sin(angleOffset - innerAngle) * radius + c.y + } + + const distance1 = { + a: Math.sqrt((intersection1.x - a.x) ** 2 + (intersection1.y - a.y) ** 2), + b: Math.sqrt((intersection1.x - b.x) ** 2 + (intersection1.y - b.y) ** 2) + } + const distance2 = { + a: Math.sqrt((intersection2.x - a.x) ** 2 + (intersection2.y - a.y) ** 2), + b: Math.sqrt((intersection2.x - b.x) ** 2 + (intersection2.y - b.y) ** 2) + } + const result = []; + if (Math.abs(sideC - (distance1.a + distance1.b)) < 0.01) { + result.push(intersection1); + } else { + if (distance1.a < distance1.b) { + if (sideB <= radius) result.push(a); + } else { + if (sideA <= radius) result.push(b) + } + } + if (Math.abs(sideC - (distance2.a + distance2.b)) < 0.01) { + result.push(intersection2); + } else { + if (distance2.a <= distance2.b) { + if (sideB <= radius) result.push(a); + } else { + if (sideA <= radius) result.push(b) + } + } + + return result; + } else { + // no intersection + return []; + } + } + + var vertices = []; + for (const obj of losDomain) { + for (var i = 0; i < obj.vertices.length; i++) { + const vertex = obj.vertices[i]; + const angleToVertex = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + const queryPoint = { + x: Math.cos(angleToVertex + Math.PI) + vertex.x, + y: Math.sin(angleToVertex + Math.PI) + vertex.y + } + + if (Matter.Query.ray(domain, pos, queryPoint).length == 0) { + var distance = Math.sqrt((vertex.x - pos.x) ** 2 + (vertex.y - pos.y) ** 2); + var endPoint = { + x: vertex.x, + y: vertex.y + } + + if (distance > radius) { + const angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + endPoint = { + x: Math.cos(angle) * radius + pos.x, + y: Math.sin(angle) * radius + pos.y + } + + distance = radius + } + + var best = getIntersection(pos, endPoint, domain); + + if (best.dist >= distance) { + best = { + x: endPoint.x, + y: endPoint.y, + dist: distance + } + } + vertices.push(best) + + + var angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + endPoint = { + x: Math.cos(angle + 0.001) * radius + pos.x, + y: Math.sin(angle + 0.001) * radius + pos.y + } + + best = getIntersection(pos, endPoint, domain); + + if (best.dist >= radius) { + best = { + x: endPoint.x, + y: endPoint.y, + dist: radius + } + } + vertices.push(best) + + + angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + endPoint = { + x: Math.cos(angle - 0.001) * radius + pos.x, + y: Math.sin(angle - 0.001) * radius + pos.y + } + + best = getIntersection(pos, endPoint, domain); + + if (best.dist >= radius) { + best = { + x: endPoint.x, + y: endPoint.y, + dist: radius + } + } + vertices.push(best) + } + } + } + + const outerCollisions = allCircleLineCollisions(pos, radius, domain); + const circleCollisions = []; + for (const line of outerCollisions) { + for (const vertex of line) { + const distance = Math.sqrt((vertex.x - pos.x) ** 2 + (vertex.y - pos.y) ** 2) + const angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + const queryPoint = { + x: Math.cos(angle) * (distance - 1) + pos.x, + y: Math.sin(angle) * (distance - 1) + pos.y + } + if (Math.abs(distance - radius) < 1 && Matter.Query.ray(domain, pos, queryPoint).length == 0) circleCollisions.push(vertex) + } + } + + for (var i = 0; i < circleCollisions.length; i++) { + const vertex = circleCollisions[i]; + var nextIndex = i + 1; + if (nextIndex == circleCollisions.length) nextIndex = 0; + const nextVertex = circleCollisions[nextIndex]; + const angle1 = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + const angle2 = Math.atan2(nextVertex.y - pos.y, nextVertex.x - pos.x); + var newAngle; + if (Math.abs(angle1) > Math.PI / 2 && Math.abs(angle2) > Math.PI / 2 && angle1 / Math.abs(angle1) != angle2 / Math.abs(angle2)) { + // if the arc between the to points crosses over the left side (+/- pi radians) + const newAngle1 = (Math.PI - Math.abs(angle1)) * (angle1 / Math.abs(angle1)); + const newAngle2 = (Math.PI - Math.abs(angle2)) * (angle2 / Math.abs(angle2)); + newAngle = (newAngle1 + newAngle2) / 2; + var multiplier; + if (newAngle == 0) { + multiplier = 1; + } else { + multiplier = newAngle / Math.abs(newAngle); + } + newAngle = Math.PI * multiplier - newAngle * multiplier; + test = true; + } else { + newAngle = (angle1 + angle2) / 2; + } + + // shoot ray between them + var endPoint = { + x: Math.cos(newAngle) * radius + pos.x, + y: Math.sin(newAngle) * radius + pos.y + } + + var best = getIntersection(pos, endPoint, domain); + + vertices.push(vertex); + + if (best.dist <= radius) vertices.push({ x: best.x, y: best.y }) + } + + vertices.sort((a, b) => Math.atan2(a.y - pos.y, a.x - pos.x) - Math.atan2(b.y - pos.y, b.x - pos.x)); + return vertices; + } + + function compareArrays(array1, array2) { + for (var i = 0; i < array1.length; i++) { + if (array1[i] != array2[i]) return false; + } + + return true; + } + + function generateIntersectMap() { + // include intersections in map elements to avoid LoS issues with overlapping + const intersectMap = []; + for (var i = 0; i < map.length; i++) { + const obj = map[i]; + const newVertices = []; + const restOfMap = [...map].slice(0, i).concat([...map].slice(i + 1)) + for (var j = 0; j < obj.vertices.length - 1; j++) { + var intersections = getIntersections(obj.vertices[j], obj.vertices[j + 1], restOfMap); + newVertices.push(obj.vertices[j]); + for (const vertex of intersections) { + newVertices.push({ x: vertex.x, y: vertex.y }); + } + } + intersections = getIntersections(obj.vertices[obj.vertices.length - 1], obj.vertices[0], restOfMap); + newVertices.push(obj.vertices[obj.vertices.length - 1]); + for (const vertex of intersections) { + newVertices.push({ x: vertex.x, y: vertex.y }); + } + + intersectMap.push({ vertices: newVertices }); + } + + return intersectMap; + } + function addPartToMap(len) { // from "run" map map[len].collisionFilter.category = cat.map; map[len].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.powerUp | cat.mob | cat.mobBullet; @@ -17043,9 +17845,6 @@ const level = { level.defaultZoom = 1800; simulation.zoomTransition(level.defaultZoom); document.body.style.backgroundColor = "#d8dadf"; - //spawn.debris(750, -2200, 3700, 16); //16 debris per level - - //spawn.bodyVertex(100, -200, verticesString) spawn.mapRect(-925, 0, 2650, 100); @@ -17067,9 +17866,11 @@ const level = { spawn.mapRect(-600, -2900, 3550, 100); spawn.mapRect(2850, -2900, 100, 700); + spawn.mapRect(2850, -2200, 100, 350); + map[map.length - 1].fallsOff2 = true; // this piece will fall off in the middle of cutscene spawn.mapRect(2300, -1900, 500, 50); - map[map.length - 1].fallsOff = true; // this piece wall fall off in cutscene + map[map.length - 1].fallsOff = true; // this piece wall fall off at the start of cutscene spawn.mapRect(2800, -1900, 200, 50); spawn.mapRect(2900, -1900, 50, 450); @@ -17099,13 +17900,16 @@ const level = { const hand1 = clockHand(400, -3700, 75, 600); const elevator1 = level.elevator(3200, 0, 150, 50, -1750, 0.0025, { up: 0.05, down: 0.2 }); + spawn.debris(-300, 0, 1300, 6); + spawn.debris(0, -2900, 2500, 8); + spawn.randomSmallMob(-500, -500, 1); spawn.randomMob(190, -1300, 1); spawn.randomMob(200, -320, 0.3); spawn.randomMob(1000, -1100, 1); spawn.randomMob(-160, -2050, 1); spawn.randomMob(-1100, -2900, 0.5); - spawn.randomLevelBoss(1900, -3800); + spawn.randomLevelBoss(1900, -3800, spawn.randomBossList.splice(0, spawn.randomBossList.indexOf("shieldingBoss"), 1).concat(spawn.randomBossList.splice(spawn.randomBossList.indexOf("shieldingBoss") + 1))); // shieldingBoss lags out the lighting system for some reason spawn.randomMob(2500, -3500, 0.3); spawn.randomMob(1300, -4100, 0.5); spawn.randomMob(3400, -2450, 1); @@ -17117,11 +17921,6 @@ const level = { spawn.randomMob(2700, -850, 0.4); spawn.randomMob(2500, -50, 0.4); - // spawn.randomSmallMob(1300, -70); - // spawn.randomMob(2650, -975, 0.8); - // spawn.randomGroup(1700, -900, 0.4); - // if (simulation.difficulty > 1) spawn.randomLevelBoss(2200, -1300); - powerUps.addResearchToLevel() // needs to run after mobs are spawned var dealtPiston1Damage = false; @@ -17134,7 +17933,7 @@ const level = { var roofReadyToFall = false; var roofFallCycle = 0; var drawGear = false; - var gearCycle = m.cycle; + var gearCycle = simulation.cycle; var gearPositions = []; var pistonUnlockCycle = 0; @@ -17155,6 +17954,10 @@ const level = { }) } + var circleHead = Matter.Bodies.polygon(m.pos.x, m.pos.y, 0, 31); + var losDomain = generateIntersectMap().concat(mob.filter((obj) => { return obj.isNotCloaked == null && (obj.isBoss || obj.label != 'Circle Body') }), [pendulum1, gear1, gear2, player, circleHead]); + var oldMap = [...map]; + var oldMob = [...mob]; var spawnGearMobCycle = 0; var gearsSpawned = 0; var lastSmallGearRot = 0; @@ -17163,9 +17966,64 @@ const level = { var bigGearRot = 0; var finalGearRot; var lastFinalGearRot; - var startCycle = m.cycle; // used to offset m.cycle to avoid the swing starting halfway through at the start of the level and messing up syncronization + var startCycle = simulation.cycle; // used to offset simulation.cycle to avoid the swing starting halfway through at the start of the level and messing up syncronization level.custom = () => { + Matter.Body.setPosition(circleHead, m.pos) + if (!(compareArrays(oldMap, map) && compareArrays(oldMob, mob))) { + losDomain = generateIntersectMap().concat(mob.filter((obj) => { return obj.isNotCloaked == null && (obj.isBoss || obj.label != 'Circle Body') }), [pendulum1, gear1, gear2, player, circleHead]); + } + oldMap = [...map]; + oldMob = [...mob]; + ctx.fillStyle = "#b0b0b2"; + ctx.fillRect(-600, -1700, 2000, 1700); + ctx.fillRect(1350, -1851, 1550, 350); + ctx.fillRect(-1400, -2950, 4250, 1450); + ctx.fillRect(-1400, -4400, 4350, 1500); + ctx.fillRect(-450, -4600, 650, 250); + ctx.fillRect(2750, -3200, 200, 1300); + ctx.fillRect(2750, -3200, 200, 1300); + ctx.fillStyle = "#000"; + ctx.fillRect(350, -2800, 100, 25); + // light + var lightPos = { x: 400, y: -2775 }; + var lightRadius = 2950; + const vertices = circleLoS(lightPos, lightRadius, map.concat(mob.filter((obj) => { return obj.isNotCloaked == null && (obj.isBoss || obj.label != 'Circle Body') }), [pendulum1, gear1, gear2, player, circleHead])); + if (vertices.length > 0 && vertices[0].x) { + ctx.beginPath(); + ctx.moveTo(vertices[0].x, vertices[0].y); + for (var i = 1; i < vertices.length; i++) { + var currentDistance = Math.sqrt((vertices[i - 1].x - lightPos.x) ** 2 + (vertices[i - 1].y - lightPos.y) ** 2); + var newDistance = Math.sqrt((vertices[i].x - lightPos.x) ** 2 + (vertices[i].y - lightPos.y) ** 2); + if (Math.abs(currentDistance - lightRadius) < 1 && Math.abs(newDistance - lightRadius) < 1) { + const currentAngle = Math.atan2(vertices[i - 1].y - lightPos.y, vertices[i - 1].x - lightPos.x); + const newAngle = Math.atan2(vertices[i].y - lightPos.y, vertices[i].x - lightPos.x); + ctx.arc(lightPos.x, lightPos.y, lightRadius, currentAngle, newAngle); + } else { + ctx.lineTo(vertices[i].x, vertices[i].y) + } + } + newDistance = Math.sqrt((vertices[0].x - lightPos.x) ** 2 + (vertices[0].y - lightPos.y) ** 2); + currentDistance = Math.sqrt((vertices[vertices.length - 1].x - lightPos.x) ** 2 + (vertices[vertices.length - 1].y - lightPos.y) ** 2); + if (Math.abs(currentDistance - lightRadius) < 1 && Math.abs(newDistance - lightRadius) < 1) { + const currentAngle = Math.atan2(vertices[vertices.length - 1].y - lightPos.y, vertices[vertices.length - 1].x - lightPos.x); + const newAngle = Math.atan2(vertices[0].y - lightPos.y, vertices[0].x - lightPos.x); + ctx.arc(lightPos.x, lightPos.y, lightRadius, currentAngle, newAngle); + } else { + ctx.lineTo(vertices[0].x, vertices[0].y) + } + ctx.fillStyle = "rgba(216, 218, 223, 0.5)"; + ctx.fill(); + } + + ctx.beginPath(); + ctx.moveTo(425, -2775); + ctx.arc(400, -2775, 25, 0, Math.PI); + ctx.fillStyle = "#c6aa12"; + ctx.fill(); + ctx.strokeStyle = "#000000"; + ctx.lineWidth = 1; + ctx.stroke(); pendulum1.rotate(); gear1.rotate(); gear2.rotate(); @@ -17175,11 +18033,11 @@ const level = { drawBackgroundGear(-1010, -2380, 30, 100, -0.1, "#ccc", 0.05); // pendulum gears - smallGearPosRot += Math.sin((m.cycle - startCycle) / 50) * 0.3 - Math.sin((m.cycle - startCycle - 1) / 50) * 0.3; + if (!m.isBodiesAsleep) smallGearPosRot += Math.sin((simulation.cycle - startCycle) / 50) * 0.3 - Math.sin((simulation.cycle - startCycle - 1) / 50) * 0.3; if (smallGearPosRot > 0.1) smallGearPosRot = 0.1; if (smallGearPosRot < -0.1) smallGearPosRot = -0.1; var circ = 2 * Math.PI * 150; - var arcLength = ((smallGearPosRot - Math.sin((m.cycle - startCycle) / 50) * 0.2) / (Math.PI * 2)) * circ; + var arcLength = ((smallGearPosRot - Math.sin((simulation.cycle - startCycle) / 50) * 0.2) / (Math.PI * 2)) * circ; lastSmallGearRot = smallGearRot; smallGearRot = arcLength / (2 * Math.PI * 50) * Math.PI * -2 + 0.6; @@ -17189,7 +18047,7 @@ const level = { drawBackgroundGear(740, -2625, 270, 330, bigGearRot, "#d2d3d4", 0, 15, 20); // the big one in the background - drawBackgroundGear(400, -2500, 100, 150, Math.sin((m.cycle - startCycle) / 50) * -0.3, "#ccc", 0, 8, 20); // attached to pendulum + drawBackgroundGear(400, -2500, 100, 150, Math.sin((simulation.cycle - startCycle) / 50) * -0.3, "#ccc", 0, 8, 20); // attached to pendulum drawBackgroundGear(400 + Math.cos(smallGearPosRot) * 200, -2500 + Math.sin(smallGearPosRot) * 200, 50, 75, smallGearRot, "#ccc", 0, 7, 20); ctx.beginPath(); @@ -17273,11 +18131,11 @@ const level = { if (!finishedGearFight && !pistonsLocked && m.pos.x > 2100 && m.pos.x < 2900 && m.pos.y > -1850 && m.pos.y < -1500) { pistonsLocked = true; - roofFallCycle = m.cycle + 250; + roofFallCycle = simulation.cycle + 250; roofReadyToFall = true; } - if (roofReadyToFall && m.cycle >= roofFallCycle) { + if (roofReadyToFall && simulation.cycle >= roofFallCycle) { // section of roof is deleted for (var i = 0; i < map.length; i++) { if (map[i].fallsOff) { @@ -17288,49 +18146,54 @@ const level = { // replace it with a block spawn.bodyRect(2310, -1900, 480, 50); - body[body.length - 1].collisionFilter.category = cat.body; - body[body.length - 1].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet - body[body.length - 1].classType = "body"; - Composite.add(engine.world, body[body.length - 1]); //add to world - roofReadyToFall = false; drawGear = true; - gearCycle = m.cycle + 100; + gearCycle = simulation.cycle + 100; } //draw some background gears falling when roof falls - if (drawGear && m.cycle >= gearCycle) { + if (drawGear && simulation.cycle >= gearCycle) { for (var i = 0; i < 15; i++) { - drawFallingBackgroundGear(gearPositions[i].x, gearPositions[i].y, gearSizes[i].r1, gearSizes[i].r2, m.cycle, "#ccc", 0.1, 25, gearCycle); + drawFallingBackgroundGear(gearPositions[i].x, gearPositions[i].y, gearSizes[i].r1, gearSizes[i].r2, simulation.cycle, "#ccc", 0.1, 25, gearCycle); } if (spawnGearMobCycle == 0) { - spawnGearMobCycle = m.cycle + 100; + spawnGearMobCycle = simulation.cycle + 100; } } - if (spawnGearMobCycle > 0 && m.cycle >= spawnGearMobCycle) { + if (spawnGearMobCycle > 0 && simulation.cycle >= spawnGearMobCycle) { if (gearsSpawned < 40) { - gearMob(2400 + Math.random() * 200, -2300 - Math.random() * 200, false, true); + gearMob(1600 + Math.random() * 1000, -2300 - Math.random() * 300, false, true); gearsSpawned++; - spawnGearMobCycle = m.cycle + 25 - (simulation.difficulty - simulation.difficultyMode) / 2; + spawnGearMobCycle = simulation.cycle + 25 - (simulation.difficulty - simulation.difficultyMode) / 2; } else if (pistonUnlockCycle == 0) { - pistonUnlockCycle = m.cycle + 50; + pistonUnlockCycle = simulation.cycle + 50; } } - if (pistonUnlockCycle > 0 && m.cycle > pistonUnlockCycle) { + if (!finishedGearFight && pistonUnlockCycle > 0 && simulation.cycle > pistonUnlockCycle) { pistonsLocked = false; finishedGearFight = true; + + for (var i = 0; i < map.length; i++) { + if (map[i].fallsOff2) { + Matter.Composite.remove(engine.world, map[i]); + map.splice(i, 1); + } + } + + spawn.bodyRect(2850, -2180, 100, 280); + Matter.Body.setAngularVelocity(body[body.length - 1], 0.025); } - if (Math.sin((m.cycle + 15) / 25) < 0 && !lastPistonDirection) { // 15 cycles early to line up better with pendulum + if (Math.sin((simulation.cycle + 15) / 25) < 0 && !lastPistonDirection) { // 15 cycles early to line up better with pendulum piston3.isClosing = true; piston4.isClosing = false; piston5.isClosing = true; piston6.isClosing = false; piston7.isClosing = true; - } else if (Math.sin((m.cycle + 15) / 25) > 0 && lastPistonDirection) { + } else if (Math.sin((simulation.cycle + 15) / 25) > 0 && lastPistonDirection) { piston3.isClosing = false; piston4.isClosing = true; piston5.isClosing = false; @@ -17462,7 +18325,7 @@ const level = { ctx.lineWidth = 10; ctx.stroke(); - lastPistonDirection = Math.sin((m.cycle + 15) / 25) < 0; + lastPistonDirection = Math.sin((simulation.cycle + 15) / 25) < 0; if (Matter.Query.ray([player], Matter.Vector.create(piston1.position.x - 50, piston1.position.y + 175), Matter.Vector.create(piston1.position.x + 50, piston1.position.y + 175), 5).length > 0 && !dealtPiston1Damage && Math.sin(-finalGearRot) - Math.sin(-lastFinalGearRot) > 0.01) { m.damage(0.1); @@ -17685,17 +18548,41 @@ const level = { level.enter.draw(); } - var lastBlock = Math.sin(m.cycle / 50) < 0; + var lastBlock = Math.sin(simulation.cycle / 50) < 0; level.customTopLayer = () => { elevator1.move(); + ctx.beginPath(); + ctx.moveTo(pendulum1.parts[2].vertices[0].x, pendulum1.parts[2].vertices[0].y); + for (var i = 0; i < pendulum1.parts[2].vertices.length; i++) { + ctx.lineTo(pendulum1.parts[2].vertices[i].x, pendulum1.parts[2].vertices[i].y); + } + ctx.lineTo(pendulum1.parts[2].vertices[0].x, pendulum1.parts[2].vertices[0].y); + ctx.fillStyle = "#999"; + ctx.fill(); + ctx.lineWidth = 2 + ctx.strokeStyle = color.blockS; + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(gear3.parts[1].vertices[0].x, gear3.parts[1].vertices[0].y); + for (var i = 0; i < gear3.parts[1].vertices.length; i++) { + ctx.lineTo(gear3.parts[1].vertices[i].x, gear3.parts[1].vertices[i].y); + } + ctx.lineTo(gear3.parts[1].vertices[0].x, gear3.parts[1].vertices[0].y); + ctx.fillStyle = "#999"; + ctx.fill(); + ctx.lineWidth = 2 + ctx.strokeStyle = color.blockS; + ctx.stroke(); + ctx.fillStyle = "#444"; ctx.fillRect(3275, -1750, 1, 1750); ctx.fillStyle = "#888"; - if (Math.sin(m.cycle / 50) < 0 && !lastBlock) { + if (Math.sin(simulation.cycle / 50) < 0 && !lastBlock) { // remove old elements for (var i = 0; i < map.length; i++) { if (map[i].isRemove) { @@ -17708,7 +18595,7 @@ const level = { spawn.mapRect(-200, -600, 275, 50); addPartToMap(map.length - 1); map[map.length - 1].isRemove = true; - } else if (Math.sin(m.cycle / 50) * 0.3 >= 0 && lastBlock) { + } else if (Math.sin(simulation.cycle / 50) * 0.3 >= 0 && lastBlock) { for (var i = 0; i < map.length; i++) { if (map[i].isRemove) { Matter.Composite.remove(engine.world, map[i]); @@ -17722,7 +18609,7 @@ const level = { } simulation.draw.setPaths(); - lastBlock = Math.sin(m.cycle / 50) * 0.3 < 0; + lastBlock = Math.sin(simulation.cycle / 50) * 0.3 < 0; } }, buttonbutton() { @@ -24313,6 +25200,7 @@ const level = { // ******************************************************************************************************** // ******************************************************************************************************** walk() { //learn to walk + if (localSettings.isHideHUD) localSettings.isHideHUD = false m.addHealth(Infinity) document.getElementById("health").style.display = "none" //hide your health bar document.getElementById("health-bg").style.display = "none" diff --git a/js/mob.js b/js/mob.js index 20047eb..e736aed 100644 --- a/js/mob.js +++ b/js/mob.js @@ -239,7 +239,7 @@ const mobs = { deathCount: 0, mobSpawnWithHealth: 1, setMobSpawnHealth() { - mobs.mobSpawnWithHealth = 0.88 ** (tech.mobSpawnWithHealth) + mobs.mobSpawnWithHealth = 0.89 ** (tech.mobSpawnWithHealth) }, //********************************************************************************************** //********************************************************************************************** @@ -1397,9 +1397,6 @@ const mobs = { Matter.Body.setAngularVelocity(body[len2], this.angularVelocity); body[len2].collisionFilter.category = cat.body; body[len2].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet; - // if (body[len].mass > 10 || 45 + 10 * Math.random() < body.length) { - // body[len].collisionFilter.mask = cat.player | cat.bullet | cat.mob | cat.mobBullet; - // } body[len2].classType = "body"; Composite.add(engine.world, body[len2]); //add to world diff --git a/js/player.js b/js/player.js index 3394f81..012736f 100644 --- a/js/player.js +++ b/js/player.js @@ -2282,13 +2282,13 @@ const m = { if (tech.isLaserField) { simulation.ephemera.push({ name: "laser field", //used to find this array element in simulation.removeEphemera() - count: 15 + Math.floor(m.maxEnergy * 30 * 0.0018 / tech.laserDrain), //how many cycles the ephemera lasts, scales with max energy + count: 20 + Math.floor(m.maxEnergy * 30 * 0.0018 / tech.laserDrain), //how many cycles the ephemera lasts, scales with max energy do() { this.count-- if (this.count < 0) simulation.removeEphemera(this.name) - for (let i = 0, num = 20; i < num; i++) { //draw random lasers + for (let i = 0, num = 12; i < num; i++) { //draw random lasers const angle = 6.28 * i / num + m.cycle * 0.04 - b.laser({ x: m.pos.x + 30 * Math.cos(angle), y: m.pos.y + 30 * Math.sin(angle) }, { x: m.pos.x + 3000 * Math.cos(angle), y: m.pos.y + 3000 * Math.sin(angle) })//dmg = tech.laserDamage, reflections = tech.laserReflections, isThickBeam = false, push = 1 + b.laser({ x: m.pos.x + 30 * Math.cos(angle), y: m.pos.y + 30 * Math.sin(angle) }, { x: m.pos.x + 3000 * Math.cos(angle), y: m.pos.y + 3000 * Math.sin(angle) }, tech.laserDamage * 2.5)//dmg = tech.laserDamage, reflections = tech.laserReflections, isThickBeam = false, push = 1 } }, }) @@ -2470,7 +2470,7 @@ const m = { // return `deflecting condenses +${couple.toFixed(1)} ice IX` return `+${(couple * 5).toFixed(0)} maximum energy` case 2: //perfect diamagnetism - return `deflecting condenses ${0.1 * couple.toFixed(2)} ice IX` + return `deflecting condenses ${(0.1 * couple).toFixed(2)} ice IX` // return `invulnerable +${2*couple} seconds post collision` case 3: //negative mass return `+${(100 * (1 - 0.973 ** couple)).toFixed(1)}% defense` diff --git a/js/powerup.js b/js/powerup.js index 6bf7f02..faa2d90 100644 --- a/js/powerup.js +++ b/js/powerup.js @@ -664,7 +664,7 @@ const powerUps = { researchText(type) { let text = "" if (type === "entanglement") { - text += `
entanglement
` + text += `
entanglement
` } else if (tech.isJunkResearch && powerUps.research.currentRerollCount < 3) { text += `
` // style = "margin-left: 192px; margin-right: -192px;" tech.junkResearchNumber = Math.ceil(4 * Math.random()) @@ -686,7 +686,7 @@ const powerUps = { researchAndCancelText(type) { let text = `
` if (type === "entanglement") { - text += `entanglement` //‌ + text += `entanglement` //‌ } else if (tech.isJunkResearch && powerUps.research.currentRerollCount < 3) { text += `` // style = "margin-left: 192px; margin-right: -192px;" tech.junkResearchNumber = Math.ceil(4 * Math.random()) @@ -1140,7 +1140,7 @@ const powerUps = { const cycle = (timestamp) => { // if (timeStart === undefined) timeStart = timestamp // console.log(timestamp, timeStart) - if (timestamp - timeStart > tech.brainStormDelay * count) { + if (timestamp - timeStart > tech.brainStormDelay * count && simulation.isChoosing) { count++ powerUps.tech.effect(); document.getElementById("choose-grid").style.pointerEvents = "auto"; //turn off the normal 500ms delay @@ -1201,18 +1201,19 @@ const powerUps = { // text += "
" // text += "
entanglement
" // text += `
cancel
` //powerUps.cancelText('tech') - if (localSettings.entanglement.fieldIndex) { + if (localSettings.entanglement.fieldIndex && localSettings.entanglement.fieldIndex !== m.fieldMode) { const choose = localSettings.entanglement.fieldIndex //add field text += powerUps.fieldText(choose, `powerUps.choose('field',${choose})`) } for (let i = 0; i < localSettings.entanglement.gunIndexes.length; i++) { //add guns const choose = localSettings.entanglement.gunIndexes[i] + //check if you always have this gun + let alreadyHasGun = false + for (let j = 0; j < b.inventory.length; j++) { + if (b.inventory[j] === choose) alreadyHasGun = true + } // text += `
  ${b.guns[gun].name}
${b.guns[gun].description}
` - text += powerUps.gunText(choose, `powerUps.choose('gun',${choose})`) - - //consider not adding guns the player currently has? - - + if (!alreadyHasGun) text += powerUps.gunText(choose, `powerUps.choose('gun',${choose})`) } for (let i = 0; i < localSettings.entanglement.techIndexes.length; i++) { //add tech let choose = localSettings.entanglement.techIndexes[i] diff --git a/js/simulation.js b/js/simulation.js index 0f34c86..c563300 100644 --- a/js/simulation.js +++ b/js/simulation.js @@ -388,213 +388,6 @@ const simulation = { } requestAnimationFrame(loop) }, - - sight: { //credit to Cornbread for adding this algorithm to n-gon - intersectMap: [], //this is precalculated in simulation.draw.setPaths() when the map changes - getIntersection(v1, v1End, domain) { - const intersections = simulation.sight.getIntersections(v1, v1End, domain); - var best = { x: v1End.x, y: v1End.y, dist: Math.sqrt((v1End.x - v1.x) ** 2 + (v1End.y - v1.y) ** 2) } - for (const intersection of intersections) { - const dist = Math.sqrt((intersection.x - v1.x) ** 2 + (intersection.y - v1.y) ** 2); - if (dist < best.dist) best = { x: intersection.x, y: intersection.y, dist: dist } - } - return best; - }, - getIntersections(v1, v1End, domain) { - const intersections = []; - for (const obj of domain) { - for (var i = 0; i < obj.vertices.length - 1; i++) { - results = simulation.checkLineIntersection(v1, v1End, obj.vertices[i], obj.vertices[i + 1]); - if (results.onLine1 && results.onLine2) intersections.push({ x: results.x, y: results.y }); - } - results = simulation.checkLineIntersection(v1, v1End, obj.vertices[obj.vertices.length - 1], obj.vertices[0]); - if (results.onLine1 && results.onLine2) intersections.push({ x: results.x, y: results.y }); - } - return intersections; - }, - circleLoS(pos, radius) { - function allCircleLineCollisions(c, radius, domain) { - var lines = []; - for (const obj of domain) { - for (var i = 0; i < obj.vertices.length - 1; i++) lines.push(circleLineCollisions(obj.vertices[i], obj.vertices[i + 1], c, radius)); - lines.push(circleLineCollisions(obj.vertices[obj.vertices.length - 1], obj.vertices[0], c, radius)); - } - const collisionLines = []; - for (const line of lines) { - if (line.length == 2) { - // const distance1 = Math.sqrt((line[0].x - c.x) ** 2 + (line[0].y - c.y) ** 2) - // const angle1 = Math.atan2(line[0].y - c.y, line[0].x - c.x); - // const queryPoint1 = { - // x: Math.cos(angle1) * (distance1 - 1) + c.x, - // y: Math.sin(angle1) * (distance1 - 1) + c.y - // } - // const distance2 = Math.sqrt((line[1].x - c.x) ** 2 + (line[1].y - c.y) ** 2) - // const angle2 = Math.atan2(line[1].y - c.y, line[1].x - c.x); - // const queryPoint2 = { - // x: Math.cos(angle2) * (distance2 - 1) + c.x, - // y: Math.sin(angle2) * (distance2 - 1) + c.y - // } - collisionLines.push(line) - } - } - - return collisionLines; - } - - function circleLineCollisions(a, b, c, radius) { - // calculate distances - const angleOffset = Math.atan2(b.y - a.y, b.x - a.x); - const sideB = Math.sqrt((a.x - c.x) ** 2 + (a.y - c.y) ** 2); - const sideC = Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2); - const sideA = Math.sqrt((c.x - b.x) ** 2 + (c.y - b.y) ** 2); - - // calculate the closest point on line AB to point C - const angleA = Math.acos((sideB ** 2 + sideC ** 2 - sideA ** 2) / (2 * sideB * sideC)) * (a.x - c.x) / -Math.abs(a.x - c.x) - const sideAD = Math.cos(angleA) * sideB; - const d = { // closest point - x: Math.cos(angleOffset) * sideAD + a.x, - y: Math.sin(angleOffset) * sideAD + a.y - } - const distance = Math.sqrt((d.x - c.x) ** 2 + (d.y - c.y) ** 2); - if (distance == radius) { - // tangent - return [d]; - } else if (distance < radius) { - // secant - const angleOffset = Math.atan2(d.y - c.y, d.x - c.x); - const innerAngle = Math.acos(distance / radius); - const intersection1 = { - x: Math.cos(angleOffset + innerAngle) * radius + c.x, - y: Math.sin(angleOffset + innerAngle) * radius + c.y - } - - const intersection2 = { - x: Math.cos(angleOffset - innerAngle) * radius + c.x, - y: Math.sin(angleOffset - innerAngle) * radius + c.y - } - - const distance1 = { - a: Math.sqrt((intersection1.x - a.x) ** 2 + (intersection1.y - a.y) ** 2), - b: Math.sqrt((intersection1.x - b.x) ** 2 + (intersection1.y - b.y) ** 2) - } - const distance2 = { - a: Math.sqrt((intersection2.x - a.x) ** 2 + (intersection2.y - a.y) ** 2), - b: Math.sqrt((intersection2.x - b.x) ** 2 + (intersection2.y - b.y) ** 2) - } - const result = []; - if (Math.abs(sideC - (distance1.a + distance1.b)) < 0.01) { - result.push(intersection1); - } else { - if (distance1.a < distance1.b) { - if (sideB <= radius) result.push(a); - } else { - if (sideA <= radius) result.push(b) - } - } - if (Math.abs(sideC - (distance2.a + distance2.b)) < 0.01) { - result.push(intersection2); - } else { - if (distance2.a <= distance2.b) { - if (sideB <= radius) result.push(a); - } else { - if (sideA <= radius) result.push(b) - } - } - - return result; - } else { - // no intersection - return []; - } - } - - var vertices = []; - for (const obj of simulation.sight.intersectMap) { - for (var i = 0; i < obj.vertices.length; i++) { - const vertex = obj.vertices[i]; - const angleToVertex = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); - const distanceToVertex = Math.sqrt((vertex.x - pos.x) ** 2 + (vertex.y - pos.y) ** 2); - const queryPoint = { x: Math.cos(angleToVertex) * (distanceToVertex - 1) + pos.x, y: Math.sin(angleToVertex) * (distanceToVertex - 1) + pos.y } - - if (Matter.Query.ray(map, pos, queryPoint).length == 0) { - var distance = Math.sqrt((vertex.x - pos.x) ** 2 + (vertex.y - pos.y) ** 2); - var endPoint = { x: vertex.x, y: vertex.y } - - if (distance > radius) { - const angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); - endPoint = { x: Math.cos(angle) * radius + pos.x, y: Math.sin(angle) * radius + pos.y } - distance = radius - } - - var best = simulation.sight.getIntersection(pos, endPoint, map); - if (best.dist >= distance) best = { x: endPoint.x, y: endPoint.y, dist: distance } - vertices.push(best) - - var angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); - endPoint = { x: Math.cos(angle + 0.001) * radius + pos.x, y: Math.sin(angle + 0.001) * radius + pos.y } - best = simulation.sight.getIntersection(pos, endPoint, map); - - if (best.dist >= radius) best = { x: endPoint.x, y: endPoint.y, dist: radius } - vertices.push(best) - - angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); - endPoint = { x: Math.cos(angle - 0.001) * radius + pos.x, y: Math.sin(angle - 0.001) * radius + pos.y } - - best = simulation.sight.getIntersection(pos, endPoint, map); - if (best.dist >= radius) best = { x: endPoint.x, y: endPoint.y, dist: radius } - vertices.push(best) - } - } - } - - const outerCollisions = allCircleLineCollisions(pos, radius, map); - const circleCollisions = []; - for (const line of outerCollisions) { - for (const vertex of line) { - const distance = Math.sqrt((vertex.x - pos.x) ** 2 + (vertex.y - pos.y) ** 2) - const angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); - const queryPoint = { - x: Math.cos(angle) * (distance - 1) + pos.x, - y: Math.sin(angle) * (distance - 1) + pos.y - } - if (Math.abs(distance - radius) < 1 && Matter.Query.ray(map, pos, queryPoint).length == 0) circleCollisions.push(vertex) - } - } - for (var i = 0; i < circleCollisions.length; i++) { - const vertex = circleCollisions[i]; - var nextIndex = i + 1; - if (nextIndex == circleCollisions.length) nextIndex = 0; - const nextVertex = circleCollisions[nextIndex]; - const angle1 = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); - const angle2 = Math.atan2(nextVertex.y - pos.y, nextVertex.x - pos.x); - var newAngle; - if (Math.abs(angle1) > Math.PI / 2 && Math.abs(angle2) > Math.PI / 2 && angle1 / Math.abs(angle1) != angle2 / Math.abs(angle2)) { - // if the arc between the to points crosses over the left side (+/- pi radians) - const newAngle1 = (Math.PI - Math.abs(angle1)) * (angle1 / Math.abs(angle1)); - const newAngle2 = (Math.PI - Math.abs(angle2)) * (angle2 / Math.abs(angle2)); - newAngle = (newAngle1 + newAngle2) / 2; - var multiplier; - if (newAngle == 0) { - multiplier = 1; - } else { - multiplier = newAngle / Math.abs(newAngle); - } - newAngle = Math.PI * multiplier - newAngle * multiplier; - test = true; - } else { - newAngle = (angle1 + angle2) / 2; - } - - // shoot ray between them - var endPoint = { x: Math.cos(newAngle) * radius + pos.x, y: Math.sin(newAngle) * radius + pos.y } - var best = simulation.sight.getIntersection(pos, endPoint, map); - vertices.push(vertex); - if (best.dist <= radius) vertices.push({ x: best.x, y: best.y }) - } - vertices.sort((a, b) => Math.atan2(a.y - pos.y, a.x - pos.x) - Math.atan2(b.y - pos.y, b.x - pos.x)); - return vertices; - }, - }, boldActiveGunHUD() { if (b.inventory.length > 0) { for (let i = 0, len = b.inventory.length; i < len; ++i) { @@ -1287,6 +1080,7 @@ const simulation = { map: "#444", bullet: "#000" } + simulation.draw.drawMapPath = simulation.draw.drawMapPathDefault m.fireCDcycle = 0 m.drop(); m.hole.isOn = false; @@ -1507,6 +1301,218 @@ const simulation = { ctx.textAlign = "center"; ctx.fillText(`(${simulation.mouseInGame.x.toFixed(1)}, ${simulation.mouseInGame.y.toFixed(1)})`, simulation.mouse.x, simulation.mouse.y - 20); }, + sight: { //credit to Cornbread for adding this algorithm to n-gon + // square: 0, + intersectMap: [], //this is precalculated in simulation.draw.setPaths() when the map changes + getIntersection(v1, v1End, domain) { + const intersections = simulation.sight.getIntersections(v1, v1End, domain); + var best = { x: v1End.x, y: v1End.y, dist: (v1End.x - v1.x) ** 2 + (v1End.y - v1.y) ** 2 } + for (const intersection of intersections) { + const dist = (intersection.x - v1.x) ** 2 + (intersection.y - v1.y) ** 2; + if (dist < best.dist) best = { x: intersection.x, y: intersection.y, dist: dist } + } + best.dist = Math.sqrt(best.dist) + return best; + }, + getIntersections(v1, v1End, domain) { + const intersections = []; + for (const obj of domain) { + for (var i = 0; i < obj.vertices.length - 1; i++) { + results = simulation.checkLineIntersection(v1, v1End, obj.vertices[i], obj.vertices[i + 1]); + if (results.onLine1 && results.onLine2) intersections.push({ x: results.x, y: results.y }); + } + results = simulation.checkLineIntersection(v1, v1End, obj.vertices[obj.vertices.length - 1], obj.vertices[0]); + if (results.onLine1 && results.onLine2) intersections.push({ x: results.x, y: results.y }); + } + return intersections; + }, + circleLoS(pos, radius) { + function allCircleLineCollisions(c, radius, domain) { + var lines = []; + for (const obj of domain) { + for (var i = 0; i < obj.vertices.length - 1; i++) lines.push(circleLineCollisions(obj.vertices[i], obj.vertices[i + 1], c, radius)); + lines.push(circleLineCollisions(obj.vertices[obj.vertices.length - 1], obj.vertices[0], c, radius)); + } + const collisionLines = []; + for (const line of lines) { + if (line.length == 2) { + // const distance1 = Math.sqrt((line[0].x - c.x) ** 2 + (line[0].y - c.y) ** 2) + // const angle1 = Math.atan2(line[0].y - c.y, line[0].x - c.x); + // const queryPoint1 = { + // x: Math.cos(angle1) * (distance1 - 1) + c.x, + // y: Math.sin(angle1) * (distance1 - 1) + c.y + // } + // const distance2 = Math.sqrt((line[1].x - c.x) ** 2 + (line[1].y - c.y) ** 2) + // const angle2 = Math.atan2(line[1].y - c.y, line[1].x - c.x); + // const queryPoint2 = { + // x: Math.cos(angle2) * (distance2 - 1) + c.x, + // y: Math.sin(angle2) * (distance2 - 1) + c.y + // } + collisionLines.push(line) + } + } + + return collisionLines; + } + + function circleLineCollisions(a, b, c, radius) { + // calculate distances + const angleOffset = Math.atan2(b.y - a.y, b.x - a.x); + const sideB = Math.sqrt((a.x - c.x) ** 2 + (a.y - c.y) ** 2); + const sideC = Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2); + const sideA = Math.sqrt((c.x - b.x) ** 2 + (c.y - b.y) ** 2); + + // calculate the closest point on line AB to point C + const angleA = Math.acos((sideB ** 2 + sideC ** 2 - sideA ** 2) / (2 * sideB * sideC)) * (a.x - c.x) / -Math.abs(a.x - c.x) + const sideAD = Math.cos(angleA) * sideB; + const d = { // closest point + x: Math.cos(angleOffset) * sideAD + a.x, + y: Math.sin(angleOffset) * sideAD + a.y + } + const distance = Math.sqrt((d.x - c.x) ** 2 + (d.y - c.y) ** 2); + if (distance == radius) { + // tangent + return [d]; + } else if (distance < radius) { + // secant + const angleOffset = Math.atan2(d.y - c.y, d.x - c.x); + const innerAngle = Math.acos(distance / radius); + const intersection1 = { + x: Math.cos(angleOffset + innerAngle) * radius + c.x, + y: Math.sin(angleOffset + innerAngle) * radius + c.y + } + + const intersection2 = { + x: Math.cos(angleOffset - innerAngle) * radius + c.x, + y: Math.sin(angleOffset - innerAngle) * radius + c.y + } + + const distance1 = { + a: Math.sqrt((intersection1.x - a.x) ** 2 + (intersection1.y - a.y) ** 2), + b: Math.sqrt((intersection1.x - b.x) ** 2 + (intersection1.y - b.y) ** 2) + } + const distance2 = { + a: Math.sqrt((intersection2.x - a.x) ** 2 + (intersection2.y - a.y) ** 2), + b: Math.sqrt((intersection2.x - b.x) ** 2 + (intersection2.y - b.y) ** 2) + } + const result = []; + if (Math.abs(sideC - (distance1.a + distance1.b)) < 0.01) { + result.push(intersection1); + } else { + if (distance1.a < distance1.b) { + if (sideB <= radius) result.push(a); + } else { + if (sideA <= radius) result.push(b) + } + } + if (Math.abs(sideC - (distance2.a + distance2.b)) < 0.01) { + result.push(intersection2); + } else { + if (distance2.a <= distance2.b) { + if (sideB <= radius) result.push(a); + } else { + if (sideA <= radius) result.push(b) + } + } + + return result; + } else { + // no intersection + return []; + } + } + + var vertices = []; + for (const obj of simulation.sight.intersectMap) { + for (var i = 0; i < obj.vertices.length; i++) { + const vertex = obj.vertices[i]; + const angleToVertex = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + // const distanceToVertex = Math.sqrt((vertex.x - pos.x) ** 2 + (vertex.y - pos.y) ** 2); + // const queryPoint = { x: Math.cos(angleToVertex) * (distanceToVertex - 1) + pos.x, y: Math.sin(angleToVertex) * (distanceToVertex - 1) + pos.y } + const queryPoint = { x: Math.cos(angleToVertex + Math.PI) + vertex.x, y: Math.sin(angleToVertex + Math.PI) + vertex.y } + + if (Matter.Query.ray(map, pos, queryPoint).length == 0) { + var distance = Math.sqrt((vertex.x - pos.x) ** 2 + (vertex.y - pos.y) ** 2); + var endPoint = { x: vertex.x, y: vertex.y } + + if (distance > radius) { + const angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + endPoint = { x: Math.cos(angle) * radius + pos.x, y: Math.sin(angle) * radius + pos.y } + distance = radius + } + + var best = simulation.sight.getIntersection(pos, endPoint, map); + if (best.dist >= distance) best = { x: endPoint.x, y: endPoint.y, dist: distance } + vertices.push(best) + + var angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + endPoint = { x: Math.cos(angle + 0.001) * radius + pos.x, y: Math.sin(angle + 0.001) * radius + pos.y } + best = simulation.sight.getIntersection(pos, endPoint, map); + + if (best.dist >= radius) best = { x: endPoint.x, y: endPoint.y, dist: radius } + vertices.push(best) + + angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + endPoint = { x: Math.cos(angle - 0.001) * radius + pos.x, y: Math.sin(angle - 0.001) * radius + pos.y } + + best = simulation.sight.getIntersection(pos, endPoint, map); + if (best.dist >= radius) best = { x: endPoint.x, y: endPoint.y, dist: radius } + vertices.push(best) + } + } + } + + const outerCollisions = allCircleLineCollisions(pos, radius, map); + const circleCollisions = []; + for (const line of outerCollisions) { + for (const vertex of line) { + // console.log('hi') + const distance = Math.sqrt((vertex.x - pos.x) ** 2 + (vertex.y - pos.y) ** 2) + const angle = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + // const queryPoint = { + // x: Math.cos(angle) * (distance - 1) + pos.x, + // y: Math.sin(angle) * (distance - 1) + pos.y + // } + const queryPoint = { x: Math.cos(angle + Math.PI) + vertex.x, y: Math.sin(angle + Math.PI) + vertex.y } + + if (Math.abs(distance - radius) < 1 && Matter.Query.ray(map, pos, queryPoint).length == 0) circleCollisions.push(vertex) + } + } + for (var i = 0; i < circleCollisions.length; i++) { + const vertex = circleCollisions[i]; + var nextIndex = i + 1; + if (nextIndex == circleCollisions.length) nextIndex = 0; + const nextVertex = circleCollisions[nextIndex]; + const angle1 = Math.atan2(vertex.y - pos.y, vertex.x - pos.x); + const angle2 = Math.atan2(nextVertex.y - pos.y, nextVertex.x - pos.x); + var newAngle; + if (Math.abs(angle1) > Math.PI / 2 && Math.abs(angle2) > Math.PI / 2 && angle1 / Math.abs(angle1) != angle2 / Math.abs(angle2)) { + // if the arc between the to points crosses over the left side (+/- pi radians) + const newAngle1 = (Math.PI - Math.abs(angle1)) * (angle1 / Math.abs(angle1)); + const newAngle2 = (Math.PI - Math.abs(angle2)) * (angle2 / Math.abs(angle2)); + newAngle = (newAngle1 + newAngle2) / 2; + var multiplier; + if (newAngle == 0) { + multiplier = 1; + } else { + multiplier = newAngle / Math.abs(newAngle); + } + newAngle = Math.PI * multiplier - newAngle * multiplier; + test = true; + } else { + newAngle = (angle1 + angle2) / 2; + } + + // shoot ray between them + var endPoint = { x: Math.cos(newAngle) * radius + pos.x, y: Math.sin(newAngle) * radius + pos.y } + var best = simulation.sight.getIntersection(pos, endPoint, map); + vertices.push(vertex); + if (best.dist <= radius) vertices.push({ x: best.x, y: best.y }) + } + vertices.sort((a, b) => Math.atan2(a.y - pos.y, a.x - pos.x) - Math.atan2(b.y - pos.y, b.x - pos.x)); + return vertices; + }, + }, draw: { // powerUp() { //is set by Bayesian tech // // ctx.globalAlpha = 0.4 * Math.sin(m.cycle * 0.15) + 0.6; @@ -1590,9 +1596,6 @@ const simulation = { } - - - //store data for line of sight precalculation simulation.sight.intersectMap = []; for (var i = 0; i < map.length; i++) { @@ -1617,14 +1620,59 @@ const simulation = { // } simulation.sight.intersectMap.push({ vertices: newVertices }); } - - - }, - drawMapPath() { + drawMapPath() { }, + drawMapPathDefault() { ctx.fillStyle = color.map; ctx.fill(simulation.draw.mapPath); }, + drawMapSight() { + if (!simulation.isTimeSkipping) { + const pos = m.pos + const radius = 4000 + const vertices = simulation.sight.circleLoS(pos, radius); + if (vertices.length) { + ctx.beginPath(); + ctx.moveTo(vertices[0].x, vertices[0].y); + for (var i = 1; i < vertices.length; i++) { + var currentDistance = Math.sqrt((vertices[i - 1].x - pos.x) ** 2 + (vertices[i - 1].y - pos.y) ** 2); + var newDistance = Math.sqrt((vertices[i].x - pos.x) ** 2 + (vertices[i].y - pos.y) ** 2); + if (Math.abs(currentDistance - radius) < 1 && Math.abs(newDistance - radius) < 1) { + const currentAngle = Math.atan2(vertices[i - 1].y - pos.y, vertices[i - 1].x - pos.x); + const newAngle = Math.atan2(vertices[i].y - pos.y, vertices[i].x - pos.x); + ctx.arc(pos.x, pos.y, radius, currentAngle, newAngle); + } else { + ctx.lineTo(vertices[i].x, vertices[i].y) + } + } + newDistance = Math.sqrt((vertices[0].x - pos.x) ** 2 + (vertices[0].y - pos.y) ** 2); + currentDistance = Math.sqrt((vertices[vertices.length - 1].x - pos.x) ** 2 + (vertices[vertices.length - 1].y - pos.y) ** 2); + if (Math.abs(currentDistance - radius) < 1 && Math.abs(newDistance - radius) < 1) { + const currentAngle = Math.atan2(vertices[vertices.length - 1].y - pos.y, vertices[vertices.length - 1].x - pos.x); + const newAngle = Math.atan2(vertices[0].y - pos.y, vertices[0].x - pos.x); + ctx.arc(pos.x, pos.y, radius, currentAngle, newAngle); + } else { + ctx.lineTo(vertices[0].x, vertices[0].y) + } + + // outline map edges, best with lighter colored document.body.style.backgroundColor + ctx.strokeStyle = "#000"; + ctx.lineWidth = 5; + ctx.stroke(simulation.draw.mapPath); + + ctx.globalCompositeOperation = "destination-in"; + ctx.fillStyle = "#000"; + ctx.fill(); + ctx.globalCompositeOperation = "source-over"; + + // make map visible + // ctx.fill(simulation.draw.mapPath); + // ctx.fillStyle = "#000"; + + ctx.clip(); //this doesn't seem to be required, but it helps with performance, probably stops the canvas context from drawing the whole map + } + } + }, body() { ctx.beginPath(); for (let i = 0, len = body.length; i < len; ++i) { @@ -1792,10 +1840,10 @@ const simulation = { const y = round(simulation.constructMouseDownPosition.y) const dx = Math.max(25, round(simulation.mouseInGame.x) - x) const dy = Math.max(25, round(simulation.mouseInGame.y) - y) - console.log(e.button) + // console.log(e.button) if (e.button === 1) { if (level.isProcedural) { - simulation.outputMapString(`spawn.randomMob(x+${x}, y+${y}, 0);\n`); + simulation.outputMapString(`spawn.randomMob(x+${x}, ${y}, 0);\n`); } else { simulation.outputMapString(`spawn.randomMob(${x}, ${y}, 0);\n`); } @@ -1804,7 +1852,7 @@ const simulation = { } else if (simulation.mouseInGame.x > simulation.constructMouseDownPosition.x && simulation.mouseInGame.y > simulation.constructMouseDownPosition.y) { //make sure that the width and height are positive if (e.button === 0) { //add map if (level.isProcedural) { - simulation.outputMapString(`spawn.mapRect(x+${x}, y+${y}, ${dx}, ${dy});\n`); + simulation.outputMapString(`spawn.mapRect(x+${x}, ${y}, ${dx}, ${dy});\n`); } else { simulation.outputMapString(`spawn.mapRect(${x}, ${y}, ${dx}, ${dy});\n`); } @@ -1818,17 +1866,12 @@ const simulation = { simulation.draw.setPaths() //update map graphics } else if (e.button === 2) { //add body if (level.isProcedural) { - simulation.outputMapString(`spawn.bodyRect(x+${x}, y+${y}, ${dx}, ${dy});\n`); + simulation.outputMapString(`spawn.bodyRect(x+${x}, ${y}, ${dx}, ${dy});\n`); } else { simulation.outputMapString(`spawn.bodyRect(${x}, ${y}, ${dx}, ${dy});\n`); } //see map in world spawn.bodyRect(x, y, dx, dy); - len = body.length - 1 - body[len].collisionFilter.category = cat.body; - body[len].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet - Composite.add(engine.world, body[len]); //add to world - body[len].classType = "body" } } } diff --git a/js/spawn.js b/js/spawn.js index 4ee0056..5601b1c 100644 --- a/js/spawn.js +++ b/js/spawn.js @@ -32,8 +32,7 @@ const spawn = { "laser", "laser", "pulsar", "pulsar", "sneaker", "sneaker", - "launcher", "launcherOne", "exploder", "sucker", "sniper", "spinner", "grower", "beamer", "spawner", "ghoster", - //, "focuser" + "launcher", "launcherOne", "exploder", "sucker", "sniper", "spinner", "grower", "beamer", "spawner", "ghoster", "focuser" ], mobTypeSpawnOrder: [], //preset list of mob names calculated at the start of a run by the randomSeed mobTypeSpawnIndex: 0, //increases as the mob type cycles @@ -5883,9 +5882,10 @@ const spawn = { mobs.spawn(x, y, 7, radius, "transparent"); let me = mob[mob.length - 1]; me.seeAtDistance2 = 300000; - me.accelMag = 0.00015 * simulation.accelScale; + me.accelMag = 0.00004 + 0.00015 * simulation.accelScale; if (map.length) me.searchTarget = map[Math.floor(Math.random() * (map.length - 1))].position; //required for search - Matter.Body.setDensity(me, 0.0015); //normal is 0.001 //makes effective life much lower + // Matter.Body.setDensity(me, 0.0015); //normal is 0.001 + me.damageReduction = 0.5 me.stroke = "transparent"; //used for drawGhost me.alpha = 1; //used in drawGhost me.isNotCloaked = false; //used in drawGhost @@ -5893,9 +5893,9 @@ const spawn = { // me.leaveBody = false; me.collisionFilter.mask = cat.bullet //| cat.body me.showHealthBar = false; - me.memory = 480; + me.memory = 600; me.do = function () { - //cap max speed + //cap max speed to avoid getting launched by deflection, explosion if (this.speed > 7) { Matter.Body.setVelocity(this, { x: this.velocity.x * 0.8, @@ -5908,12 +5908,12 @@ const spawn = { this.search(); //draw if (this.distanceToPlayer2() < this.seeAtDistance2) { - if (this.alpha < 1) this.alpha += 0.01 * simulation.CDScale; //near player go solid + if (this.alpha < 1) this.alpha += 0.011 * simulation.CDScale; //near player go solid } else { if (this.alpha > 0) this.alpha -= 0.05; ///away from player, hide } if (this.alpha > 0) { - if (this.alpha > 0.8 && this.seePlayer.recall) { + if (this.alpha > 0.7 && this.seePlayer.recall) { this.healthBar(); if (!this.isNotCloaked) { this.isNotCloaked = true; @@ -8165,14 +8165,29 @@ const spawn = { } } }, - bodyRect(x, y, width, height, chance = 1, properties = { - friction: 0.05, - frictionAir: 0.001, - }) { - if (Math.random() < chance) body[body.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height, properties); + // bodyRect(x, y, width, height, chance = 1, properties = { friction: 0.05, frictionAir: 0.001 }) { + // if (Math.random() < chance) body[body.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height, properties); + // }, + // bodyVertex(x, y, vector, properties) { //adds shape to body array + // body[body.length] = Matter.Bodies.fromVertices(x, y, Vertices.fromPath(vector), properties); + // }, + bodyRect(x, y, width, height, chance = 1, properties = { friction: 0.05, frictionAir: 0.001 }) { //this is the command that adds blocks to the world in the middle of a level + if (Math.random() < chance) { + body[body.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height, properties); + const who = body[body.length - 1] + who.collisionFilter.category = cat.body; + who.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet + Composite.add(engine.world, who); //add to world + who.classType = "body" + } }, - bodyVertex(x, y, vector, properties) { //adds shape to body array + bodyVertex(x, y, vector, properties) { //this is the command that adds blocks to the world in the middle of a level body[body.length] = Matter.Bodies.fromVertices(x, y, Vertices.fromPath(vector), properties); + const who = body[body.length - 1] + who.collisionFilter.category = cat.body; + who.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet + Composite.add(engine.world, who); //add to world + who.classType = "body" }, mapRect(x, y, width, height, properties) { //adds rectangle to map array map[map.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height, properties); @@ -8180,6 +8195,24 @@ const spawn = { mapVertex(x, y, vector, properties) { //adds shape to map array map[map.length] = Matter.Bodies.fromVertices(x, y, Vertices.fromPath(vector), properties); }, + mapRectNow(x, y, width, height, properties, isRedrawMap = true) { //adds rectangle to map array in the middle of a level + map[map.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height, properties); + const who = map[map.length - 1] + who.collisionFilter.category = cat.map; + who.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.powerUp | cat.mob | cat.mobBullet; + Matter.Body.setStatic(who, true); //make static + Composite.add(engine.world, who); //add to world + if (isRedrawMap) simulation.draw.setPaths() + }, + mapVertexNow(x, y, vector, properties) { //adds shape to map array in the middle of a level + map[map.length] = Matter.Bodies.fromVertices(x, y, Vertices.fromPath(vector), properties); + const who = map[map.length - 1] + who.collisionFilter.category = cat.map; + who.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.powerUp | cat.mob | cat.mobBullet; + Matter.Body.setStatic(who, true); //make static + Composite.add(engine.world, who); //add to world + if (isRedrawMap) simulation.draw.setPaths() //this is a bit slow on processing so maybe it's better to run after you spawn several different shapes + }, //complex map templates spawnBuilding(x, y, w, h, leftDoor, rightDoor, walledSide) { this.mapRect(x, y, w, 25); //roof diff --git a/js/tech.js b/js/tech.js index efa9d9f..8d3939c 100644 --- a/js/tech.js +++ b/js/tech.js @@ -33,6 +33,7 @@ const tech = { tech.totalCount = 0; tech.junkCount = 0 //tech.countJunkTech(); simulation.updateTechHUD(); + simulation.updateGunHUD(); }, removeTech(index = 'random') { if (index === 'random') { @@ -335,7 +336,7 @@ const tech = { } }, { - name: "elasticity", + name: "nitinol", description: "+33% movement and jumping
+30% defense", maxCount: 3, count: 0, @@ -1288,7 +1289,7 @@ const tech = { }, { name: "reaction inhibitor", - description: "-12% maximum mob health", //health + description: "-11% maximum mob health", //health maxCount: 3, count: 0, frequency: 1, @@ -3489,7 +3490,7 @@ const tech = { for (let i = 0; i < 5; i++) powerUps.spawn(m.pos.x + 60 * (Math.random() - 0.5), m.pos.y + 60 * (Math.random() - 0.5), "tech"); }, remove() { - if (!this.count) tech.isDeterminism = false; + tech.isDeterminism = false; } }, { @@ -8936,10 +8937,6 @@ const tech = { y: 10 * (Math.random() - 0.5) }); bodyBullet.isAboutToBeRemoved = true - bodyBullet.collisionFilter.category = cat.body; - bodyBullet.collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet - bodyBullet.classType = "body"; - Composite.add(engine.world, bodyBullet); //add to world setTimeout(() => { //remove block for (let i = 0; i < body.length; i++) { if (body[i] === bodyBullet) { @@ -10095,7 +10092,7 @@ const tech = { simulation.ephemera.push({ name: "LoS", count: 0, do() { const pos = m.pos - const radius = 5000 + const radius = 3000 if (!simulation.isTimeSkipping) { const vertices = simulation.sight.circleLoS(pos, radius); if (vertices.length) { @@ -10121,10 +10118,11 @@ const tech = { } else { ctx.lineTo(vertices[0].x, vertices[0].y) } + //stroke the map, so it looks different form the line of sight ctx.strokeStyle = "#234"; ctx.lineWidth = 9; - ctx.stroke(simulation.draw.mapPath); + ctx.stroke(simulation.draw.mapPath); //this has a pretty large impact on performance, maybe 5% worse performance ctx.globalCompositeOperation = "destination-in"; ctx.fillStyle = "#000"; @@ -10235,16 +10233,8 @@ const tech = { for (let i = 0, len = 40; i < len; i++) { setTimeout(() => { m.energy -= 1 / len - const index = body.length - where = Vector.add(m.pos, { - x: 400 * (Math.random() - 0.5), - y: 400 * (Math.random() - 0.5) - }) + where = Vector.add(m.pos, { x: 400 * (Math.random() - 0.5), y: 400 * (Math.random() - 0.5) }) spawn.bodyRect(where.x, where.y, Math.floor(15 + 100 * Math.random()), Math.floor(15 + 100 * Math.random())); - body[index].collisionFilter.category = cat.body; - body[index].collisionFilter.mask = cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet - body[index].classType = "body"; - Composite.add(engine.world, body[index]); //add to world }, i * 100); } diff --git a/todo.txt b/todo.txt index 79b9761..03929e0 100644 --- a/todo.txt +++ b/todo.txt @@ -1,34 +1,65 @@ ******************************************************** NEXT PATCH ************************************************** -JUNK tech: flatland - draw line of sight - credit to Cornbread for line of sight algorithm - a preview of future line of site content - try it out in console: tech.giveTech("flatland") +level: subway + replaces gauntlet just before the final boss + gauntlet moved to community map pool + subway todo: + add a few more stations + balance difficulty + find bugs -new images -bug fixes +surface plasmons does 50% more damage +elasticity renamed nitinol because I bought some nitinol wire and it's neat +entanglement power up no longer shows guns or fields you already have +disabled minimal HUD for training levels +reaction inhibitor 12->11% mob max health reduction + +spawn.bodyRect() now can add blocks mid level without any extra code + I think I found all the bugs this causes, but let me know if any blocks added mid game aren't colliding + +community map - clock update: + visual overhaul,live lighting, remove pendulum overlap, move with pendulum if you stand in it, debris, trap exit only opens after completing the fight, moving elements now freeze while using time dilation *********************************************************** TODO ***************************************************** -LoS - performance - calculate things that don't change when the map ctx is done - like the intersections? - rewrite code for infinite range - how to use this? - JUNK tech? - custom level? - give it a dark back ground for contrast? - game setting? -applications - level that is dark, and you can only see LoS - boss that you need to avoid, probably requires a custom level - boss is a source of light - cloaking tech, or just replace cloaking effect - explosion graphic - mobs area of effect damage - maybe make a shared mob AoE damage function - standing wave graphic +consider increasing the base player horizontal movement + maybe only increase ground movement, air control seems fine + +level: subway - a map that uses the train level element and line of sight graphics + stations + station theme ideas: + portals + teleport to far away rooms + slime + map elements that alternate between positions + buttons and doors + boss + spawn at the exit station? or at a random station? or at the station before the exit? + spawn on the station after enough mobs have been killed? + type of boss? + might need to make a new boss designed for this map: los and stations + small, quick, sneaky + do random bosses work? + I think + looks good with line of sight + background lighting for each room drawn in level.custom + no outdoors, no fall off the edge + slime + no small bumps + starting in a small room with a hole to the right with a short drop, like highrise or aerie + floating hexagons, like in reservoir, labs + ramping walls to jump over, like satellite + +tech stubs should be a tech unlocked by skins + nitinol, tungsten? + maybe give another benefit? + defense? + make a shared variable for skin defense, since you can only have one skin + +make a lemming that walks until it hits a wall and then turns around robotically + body or mob? + can't be killed? + Also another thing I made that could fit in-game: https://kgurchiek.github.io/universal-n-gon-loader/ by default it just plays a random version of n-gon downloaded from past github commits @@ -37,9 +68,6 @@ also you can play any version with https://kgurchiek.github.io/universal-n-gon-l where setting "NUM" to 0 is the very first commit here's the code if you want to check it out: https://github.com/kgurchiek/universal-n-gon-loader/blob/main/script.js -level - funicular - The system is characterized by two counterbalanced carriages (also called cars or trains) permanently attached to opposite ends of a haulage cable, which is looped over a pulley at the upper end of the track.[2][3] The result of such a configuration is that the two carriages move synchronously: as one ascends, the other descends at an equal speed. - missile bot and plasma bot don't get converted by bot upgrade tech? is this more confusing because it contradicts text? @@ -66,6 +94,8 @@ rework quantum eraser test bremsstrahlung damage and make sure it actually does more damage with the dot tech +tech circular polarization - wave gun bullets move in a circle + tech: choose next map by name after exiting current map use modified tech selection code? this might be too much work without much reward