diff --git a/img/modified Newtonian dynamics.webp b/img/MOND.webp
similarity index 100%
rename from img/modified Newtonian dynamics.webp
rename to img/MOND.webp
diff --git a/img/hidden-variable theory.webp b/img/hidden-variable theory.webp
new file mode 100644
index 00000000..1db74f5d
Binary files /dev/null and b/img/hidden-variable theory.webp differ
diff --git a/img/polariton.webp b/img/polariton.webp
new file mode 100644
index 00000000..b512f02d
Binary files /dev/null and b/img/polariton.webp differ
diff --git a/js/bullet.js b/js/bullet.js
index b5ed3c1c..5459cac5 100644
--- a/js/bullet.js
+++ b/js/bullet.js
@@ -1498,7 +1498,7 @@ const b = {
if (this.pickUpTarget) {
if (tech.isReel && this.blockDist > 150) {
// console.log(0.0003 * Math.min(this.blockDist, 1000))
- m.energy += 0.00113 * Math.min(this.blockDist, 800) //max 0.352 energy
+ m.energy += 0.00113 * Math.min(this.blockDist, 800) * level.isReducedRegen //max 0.352 energy
simulation.drawList.push({ //add dmg to draw queue
x: m.pos.x,
y: m.pos.y,
@@ -2925,7 +2925,7 @@ const b = {
if (!who.isInvulnerable) {
if (tech.iceEnergy && !who.shield && !who.isShielded && who.isDropPowerUp && who.alive && m.immuneCycle < m.cycle) {
setTimeout(() => {
- if (!who.alive) m.energy += tech.iceEnergy * 0.8
+ if (!who.alive) m.energy += tech.iceEnergy * 0.8 * level.isReducedRegen
}, 10);
}
mobs.statusSlow(who, tech.iceIXFreezeTime)
@@ -4683,7 +4683,7 @@ const b = {
if (Vector.magnitude(Vector.sub(this.position, player.position)) < 250 && m.immuneCycle < m.cycle) { //give energy
Matter.Body.setAngularVelocity(this, this.spin)
if (this.isUpgraded) {
- m.energy += 0.12
+ m.energy += 0.12 * level.isReducedRegen
simulation.drawList.push({ //add dmg to draw queue
x: this.position.x,
y: this.position.y,
@@ -4692,7 +4692,7 @@ const b = {
time: simulation.drawTime
});
} else {
- m.energy += 0.04
+ m.energy += 0.04 * level.isReducedRegen
simulation.drawList.push({ //add dmg to draw queue
x: this.position.x,
y: this.position.y,
@@ -4705,7 +4705,7 @@ const b = {
}
if (!m.isCloak) { //if cloaking field isn't active
- const size = 33
+ const size = 33 - 6 * isKeep
q = Matter.Query.region(mob, {
min: {
x: this.position.x - size,
@@ -4760,7 +4760,7 @@ const b = {
minDmgSpeed: 2,
// lookFrequency: 56 + Math.floor(17 * Math.random()) - isUpgraded * 20,
lastLookCycle: simulation.cycle + 60 * Math.random(),
- delay: Math.floor((tech.isNailBotUpgrade ? 22 : 85)),
+ delay: Math.floor((tech.isNailBotUpgrade ? 22 : 85) + 10 * isKeep),
acceleration: (isKeep ? 0.005 : 0.001) * (1 + 0.5 * Math.random()),
range: 60 * (1 + 0.3 * Math.random()) + 3 * b.totalBots() + !isKeep * 100,
endCycle: Infinity,
@@ -4895,7 +4895,7 @@ const b = {
lookFrequency: 60 + Math.floor(17 * Math.random()) - 50 * tech.isFoamBotUpgrade,
cd: 0,
fireCount: 0,
- fireLimit: 5 + 2 * tech.isFoamBotUpgrade,
+ fireLimit: 5 + 2 * tech.isFoamBotUpgrade - isKeep,
delay: Math.floor((200 + (tech.isFoamBotUpgrade ? 0 : 200))),// + 30 - 20 * tech.isFoamBotUpgrade,//20 + Math.floor(85 * b.fireCDscale) - 20 * tech.isFoamBotUpgrade,
acceleration: (isKeep ? 0.005 : 0.001) * (1 + 0.5 * Math.random()),
range: 60 * (1 + 0.3 * Math.random()) + 3 * b.totalBots() + !isKeep * 100, //how far from the player the bot will move
@@ -4968,7 +4968,7 @@ const b = {
lookFrequency: 17 + Math.floor(7 * Math.random()) - 3 * tech.isSoundBotUpgrade,
cd: 0,
fireCount: 0,
- fireLimit: 5,
+ fireLimit: 5 - isKeep,
delay: Math.floor(140),// + 30 - 20 * tech.isFoamBotUpgrade,//20 + Math.floor(85 * b.fireCDscale) - 20 * tech.isFoamBotUpgrade,
acceleration: (isKeep ? 0.005 : 0.001) * (1 + 0.5 * Math.random()),
range: 60 * (1 + 0.3 * Math.random()) + 3 * b.totalBots() + !isKeep * 100, //how far from the player the bot will move
@@ -5136,7 +5136,7 @@ const b = {
lookFrequency: 20 + Math.floor(7 * Math.random()) - 13 * tech.isLaserBotUpgrade,
range: (600 + 375 * tech.isLaserBotUpgrade) * (1 + 0.12 * Math.random()),
drainThreshold: 0.15 + 0.5 * Math.random() + (tech.isEnergyHealth ? 0.3 : 0),// laser bot will not attack if the player is below this energy
- drain: (0.57 - 0.43 * tech.isLaserBotUpgrade) * tech.laserDrain,
+ drain: (0.57 - 0.43 * tech.isLaserBotUpgrade + isKeep * 0.08) * tech.laserDrain,
laserDamage: 0.75 + 0.75 * tech.isLaserBotUpgrade,
endCycle: Infinity,
classType: "bullet",
@@ -5313,7 +5313,7 @@ const b = {
restitution: 1,
dmg: 0,
minDmgSpeed: 0,
- lookFrequency: 43 + Math.floor(7 * Math.random()) - 13 * tech.isBoomBotUpgrade,
+ lookFrequency: 43 + Math.floor(7 * Math.random()) - 15 * tech.isBoomBotUpgrade,
acceleration: (isKeep ? 0.005 : 0.001) * (1 + 0.5 * Math.random()),
attackAcceleration: 0.012 + 0.005 * tech.isBoomBotUpgrade,
range: 500 * (1 + 0.1 * Math.random()) + 250 * tech.isBoomBotUpgrade + !isKeep * 100,
@@ -5443,11 +5443,7 @@ const b = {
const DIST = Vector.magnitude(sub);
const unit = Vector.normalise(sub)
if (DIST < tech.isPlasmaRange * 450 && m.energy > this.drainThreshold) {
- m.energy -= 0.0013 //0.004; //normal plasma field is 0.00008 + m.fieldRegen = 0.00108
- // if (m.energy < 0) {
- // m.fieldCDcycle = m.cycle + 120;
- // m.energy = 0;
- // }
+ m.energy -= 0.001
//calculate laser collision
let best;
let range = tech.isPlasmaRange * (120 + 300 * Math.sqrt(Math.random()))
@@ -5466,9 +5462,9 @@ const b = {
// Matter.Body.applyForce(best.who, path[1], force)
//push mobs away
if (best.who.speed > 3) {
- const force = Vector.mult(Vector.normalise(Vector.sub(m.pos, path[1])), -0.005 * Math.min(5, best.who.mass))
+ const force = Vector.mult(Vector.normalise(Vector.sub(m.pos, path[1])), -0.004 * Math.min(5, best.who.mass))
Matter.Body.applyForce(best.who, path[1], force)
- Matter.Body.setVelocity(best.who, { x: best.who.velocity.x * 0.4, y: best.who.velocity.y * 0.4 });
+ Matter.Body.setVelocity(best.who, { x: best.who.velocity.x * 0.5, y: best.who.velocity.y * 0.5 });
} else {
const force = Vector.mult(Vector.normalise(Vector.sub(m.pos, path[1])), -0.01 * Math.min(5, best.who.mass))
Matter.Body.applyForce(best.who, path[1], force)
@@ -5558,7 +5554,7 @@ const b = {
}
}
},
- range: 190 + 170 * tech.isOrbitBotUpgrade + !isKeep * 60 * (0.5 - Math.random()), //range is set in bot upgrade too!
+ range: 160 + 170 * tech.isOrbitBotUpgrade + !isKeep * 100 * (0.5 - Math.random()), //range is set in bot upgrade too!
orbitalSpeed: 0,
phase: 2 * Math.PI * Math.random(),
do() {
@@ -7211,8 +7207,6 @@ const b = {
const DRAIN = (tech.isRailEnergy ? 0 : 0.002)
//exit railgun charging without firing
if (m.energy < DRAIN) {
- // m.energy += 0.025 + this.charge * 22 * this.drain
- // m.energy -= this.drain
m.fireCDcycle = m.cycle + 120; // cool down if out of energy
this.endCycle = 0;
this.charge = 0
@@ -7292,7 +7286,7 @@ const b = {
const recoil = Vector.mult(Vector.normalise(Vector.sub(where, m.pos)), m.crouch ? 0.03 : 0.06)
player.force.x -= recoil.x
player.force.y -= recoil.y
- const harpoonSize = tech.isLargeHarpoon ? 1 + 0.1 * Math.sqrt(this.ammo) : 1
+ const harpoonSize = tech.isLargeHarpoon ? 1 + 0.07 * Math.sqrt(this.ammo) : 1
const thrust = 0.15 * (this.charge)
if (tech.extraHarpoons) {
let targetCount = 0
@@ -7452,10 +7446,7 @@ const b = {
if (tech.extraHarpoons && !m.crouch) { //multiple harpoons
const SPREAD = 0.2
let angle = m.angle - SPREAD * tech.extraHarpoons / 2;
- const dir = {
- x: Math.cos(angle),
- y: Math.sin(angle)
- }; //make a vector for the player's direction of length 1; used in dot product
+ const dir = { x: Math.cos(angle), y: Math.sin(angle) }; //make a vector for the player's direction of length 1; used in dot product
const range = 450 * (tech.isFilament ? 1 + 0.012 * Math.min(110, this.ammo) : 1)
let targetCount = 0
for (let i = 0, len = mob.length; i < len; ++i) {
diff --git a/js/engine.js b/js/engine.js
index 59427d0f..5a8f5442 100644
--- a/js/engine.js
+++ b/js/engine.js
@@ -120,7 +120,7 @@ function collisionChecks(event) {
simulation.trails(90)
simulation.inGameConsole(`simulation.amplitude = ${Math.random()}`);
}
- if (tech.isPiezo) m.energy += 20.48;
+ if (tech.isPiezo) m.energy += 20.48 * level.isReducedRegen;
if (tech.isCouplingNoHit && m.coupling > 0) {
m.couplingChange(-3)
@@ -162,10 +162,12 @@ function collisionChecks(event) {
const maxCount = 10 + 3 * tech.extraHarpoons //scale the number of hooks fired
let count = maxCount - 1
const angle = Math.atan2(mob[k].position.y - player.position.y, mob[k].position.x - player.position.x);
- b.harpoon(m.pos, mob[k], angle, 0.75, true, 7) // harpoon(where, target, angle = m.angle, harpoonSize = 1, isReturn = false, totalCycles = 35, isReturnAmmo = true, thrust = 0.1) {
+
+ const mass = 0.75 * (tech.isLargeHarpoon ? 1 + 0.05 * Math.sqrt(this.ammo) : 1)
+ b.harpoon(m.pos, mob[k], angle, mass, true, 7) // harpoon(where, target, angle = m.angle, harpoonSize = 1, isReturn = false, totalCycles = 35, isReturnAmmo = true, thrust = 0.1) {
bullet[bullet.length - 1].drain = 0
for (; count > 0; count--) {
- b.harpoon(m.pos, mob[k], angle + count * 2 * Math.PI / maxCount, 0.75, true, 7)
+ b.harpoon(m.pos, mob[k], angle + count * 2 * Math.PI / maxCount, mass, true, 7)
bullet[bullet.length - 1].drain = 0
}
}
diff --git a/js/index.js b/js/index.js
index e1f2b9a5..d36ff1b0 100644
--- a/js/index.js
+++ b/js/index.js
@@ -138,7 +138,8 @@ function beforeUnloadEventListener(event) {
event.preventDefault();
if (tech.isExitPrompt) {
tech.damage *= 1.25
- simulation.inGameConsole(`damage *= ${1.25}`)
+ // simulation.inGameConsole(`damage *= ${1.25}`)
+ simulation.inGameConsole(`tech.damage *= ${1.25} //beforeunload`);
if (Math.random() < 0.25) {
removeEventListener('beforeunload', beforeUnloadEventListener);
}
@@ -490,7 +491,7 @@ const build = {
level ${(simulation.dmgScale).toPrecision(4)}x
health (${(m.health * 100).toFixed(0)} / ${(m.maxHealth * 100).toFixed(0)})
${powerUps.research.count} ${powerUps.orb.research()}
-
energy (${(m.energy * 100).toFixed(0)} / ${(m.maxEnergy * 100).toFixed(0)}) + (${(m.fieldRegen * 6000).toFixed(0)}/s)
+
energy (${(m.energy * 100).toFixed(0)} / ${(m.maxEnergy * 100).toFixed(0)}) + (${(m.fieldRegen * 6000 * level.isReducedRegen).toFixed(0)}/s)
${tech.totalCount} ${powerUps.orb.tech()}
fire rate ${(1 / b.fireCDscale).toFixed(2)}x
mass ${player.mass.toFixed(1)}
diff --git a/js/level.js b/js/level.js
index 76420053..18ee2452 100644
--- a/js/level.js
+++ b/js/level.js
@@ -54,7 +54,7 @@ const level = {
// for (let i = 0; i < 1; i++) powerUps.directSpawn(450, -50, "tech");
// for (let i = 0; i < 3; i++) powerUps.directSpawn(m.pos.x + 200, m.pos.y - 50, "boost", false);
// spawn.bodyRect(575, -700, 150, 150); //block mob line of site on testing
- // level.satellite();
+ // level.heal();
level[simulation.isTraining ? "walk" : "initial"]() //normal starting level **************************************************
@@ -129,7 +129,9 @@ const level = {
powerUps.directSpawn(flip * localSettings.entanglement.position.x, localSettings.entanglement.position.y, "entanglement", false);
}
level.newLevelOrPhase()
- if (!simulation.isTraining) {
+ if (simulation.isTraining) {
+ simulation.difficultyMode = 2
+ } else {
simulation.inGameConsole(`level.onLevel = "${level.levels[level.onLevel]}"`);
document.title = "n-gon: " + level.levelAnnounce();
}
@@ -332,6 +334,15 @@ const level = {
constraintDescription1: "", //used in pause menu and console
constraintDescription2: "",
constraint: [
+ {
+ description: "0.5x energy regen",
+ effect() {
+ level.isReducedRegen = 0.5
+ },
+ remove() {
+ level.isReducedRegen = 1
+ }
+ },
{
description: "0.5x max health",
effect() {
@@ -351,7 +362,7 @@ const level = {
}
},
{
- description: "periodically spawn WIMPs",
+ description: "after 30 seconds spawn WIMPs",
effect() {
simulation.ephemera.push({
name: "WIMPS",
@@ -360,7 +371,7 @@ const level = {
do() {
this.time++
if (level.levels[level.onLevel] === this.levelName) {
- if (!(this.time % 900)) spawn.WIMP(level.enter.x, level.enter.y)
+ if (this.time > 1800 && !(this.time % 360)) spawn.WIMP(level.enter.x, level.enter.y)
} else {
simulation.removeEphemera(this.name);
}
@@ -383,7 +394,7 @@ const level = {
}
},
{
- description: "mobs heal for your lost health",
+ description: "mobs heal after you take damage",
effect() {
level.isMobHealPlayerDamage = true
},
@@ -400,16 +411,16 @@ const level = {
level.isMobDeathHeal = false
}
},
- {
- description: "full damage taken after boss dies",
- // description: "after boss dies damage taken = 1",
- effect() {
- level.noDefenseSetting = 1 //defense goes to zero once equal to 2
- },
- remove() {
- level.noDefenseSetting = 0
- }
- },
+ // {
+ // description: "full damage taken after boss dies",
+ // // description: "after boss dies damage taken = 1",
+ // effect() {
+ // level.noDefenseSetting = 1 //defense goes to zero once equal to 2
+ // },
+ // remove() {
+ // level.noDefenseSetting = 0
+ // }
+ // },
{
description: "4x shielded mobs",
effect() {
@@ -420,9 +431,9 @@ const level = {
}
},
{
- description: "50% JUNK chance",
+ description: "40% JUNK chance",
effect() {
- level.junkAdded = 0.5
+ level.junkAdded = 0.4
},
remove() {
level.junkAdded = 0
@@ -542,13 +553,14 @@ const level = {
is2xAmmo: false,
isReducedEnergy: false,
isSlowBots: false,
- noDefenseSetting: 0,
+ // noDefenseSetting: 0,
isMobDeathHeal: false,
isMobHealPlayerDamage: false,
isNoDamage: false,
noDamageCycle: 0,
reducedHealthLost: 0,
isReducedHealth: false,
+ isReducedRegen: 1,
levelAnnounce() {
const cheating = simulation.isCheating ? "(testing)" : ""
if (level.levelsCleared === 0) {
@@ -1239,7 +1251,7 @@ const level = {
ctx.moveTo(x, y + height / 2);
ctx.lineTo(x, maxHeight - height / 2);
ctx.strokeStyle = `rgba(0,0,0,0.2)`
- // ctx.lineWidth = "3"
+ ctx.lineWidth = "2"
ctx.stroke();
//draw body
@@ -35293,9 +35305,9 @@ const level = {
spawn.mapRect(1375, -16, 50, 50);
spawn.mapRect(1400, -8, 50, 25);
spawn.mapRect(750, -24, 650, 100);
- powerUps.directSpawn(875, -40, "heal", false, null, 15);
- powerUps.directSpawn(1075, -50, "heal", false, null, 25);
- powerUps.directSpawn(1275, -65, "heal", false, null, 35);
+ powerUps.directSpawn(875, -40, "heal", false, 15);
+ powerUps.directSpawn(1075, -50, "heal", false, 25);
+ powerUps.directSpawn(1275, -65, "heal", false, 35);
const door = level.door(1612.5, -175, 25, 190, 185, 3)
spawn.mapRect(1600, -1200, 500, 850); //exit roof
diff --git a/js/mob.js b/js/mob.js
index d4ecd86c..01a7f049 100644
--- a/js/mob.js
+++ b/js/mob.js
@@ -1085,7 +1085,7 @@ const mobs = {
if (tech.isFarAwayDmg) dmg *= 1 + Math.sqrt(Math.max(500, Math.min(3000, this.distanceToPlayer())) - 500) * 0.0067 //up to 33% dmg at max range of 3000
dmg *= this.damageReduction
//energy and heal drain should be calculated after damage boosts
- if (tech.energySiphon && dmg !== Infinity && this.isDropPowerUp && m.immuneCycle < m.cycle) m.energy += Math.min(this.health, dmg) * tech.energySiphon
+ if (tech.energySiphon && dmg !== Infinity && this.isDropPowerUp && m.immuneCycle < m.cycle) m.energy += Math.min(this.health, dmg) * tech.energySiphon * level.isReducedRegen
dmg /= Math.sqrt(this.mass)
}
@@ -1142,9 +1142,10 @@ const mobs = {
if (this.isDropPowerUp) {
if (level.isMobDeathHeal) {
for (let i = 0; i < mob.length; i++) {
- if (Vector.magnitudeSquared(Vector.sub(this.position, mob[i].position)) < 1000000 && mob[i].alive) { //1000
+ if (Vector.magnitudeSquared(Vector.sub(this.position, mob[i].position)) < 500000 && mob[i].alive) { //700
if (mob[i].health < 1) {
- mob[i].health = 1
+ mob[i].health += 0.33 + this.isBoss
+ if (mob[i].health > 1) mob[i].health = 1
simulation.drawList.push({
x: mob[i].position.x,
y: mob[i].position.y,
@@ -1304,9 +1305,6 @@ const mobs = {
tech.cloakDuplication -= 0.01
powerUps.setPowerUpMode(); //needed after adjusting duplication chance
}
- if (level.noDefenseSetting && this.isBoss) {
- level.noDefenseSetting = 2
- }
} else if (tech.isShieldAmmo && this.shield && this.shieldCount === 1) {
let type = tech.isEnergyNoAmmo ? "heal" : "ammo"
if (Math.random() < 0.4) {
diff --git a/js/player.js b/js/player.js
index 90fe921e..9bd7a427 100644
--- a/js/player.js
+++ b/js/player.js
@@ -542,6 +542,8 @@ const m = {
m.maxHealth *= 0.5
}
document.getElementById("health-bg").style.width = `${Math.floor(300 * m.maxHealth)}px`
+ document.getElementById("defense-bar").style.width = Math.floor(300 * m.maxHealth * (1 - m.defense())) + "px";
+
if (isMessage) simulation.inGameConsole(`m.maxHealth = ${m.maxHealth.toFixed(2)}`)
if (m.health > m.maxHealth) m.health = m.maxHealth;
m.displayHealth();
@@ -551,8 +553,8 @@ const m = {
lastCalculatedDamage: 0, //used to decided if damage bar needs to be redrawn (in simulation.checks)
lastCalculatedDefense: 0, //used to decided if defense bar needs to be redrawn (in simulation.checks)
defense() {
- if (level.noDefenseSetting === 2) return 1 //zero defense constraint
let dmg = 1
+ if (powerUps.boost.isDefense && powerUps.boost.endCycle > simulation.cycle) dmg *= 0.3
if (tech.isMaxHealthDefense && m.health === m.maxHealth) dmg *= 0.3
if (tech.isDiaphragm) dmg *= 0.55 + 0.35 * Math.sin(m.cycle * 0.0075);
if (tech.isZeno) dmg *= 0.15
@@ -794,7 +796,7 @@ const m = {
color: "rgba(0,255,100,0.5)",
time: 10
});
- mob[i].health += dmg * 10
+ mob[i].health += dmg * 7
if (mob[i].health > 1) mob[i].health = 1
}
}
@@ -2509,11 +2511,11 @@ const m = {
}
},
regenEnergy() { //used in drawRegenEnergy // rewritten by some tech
- if (m.immuneCycle < m.cycle && m.fieldCDcycle < m.cycle) m.energy += m.fieldRegen;
+ if (m.immuneCycle < m.cycle && m.fieldCDcycle < m.cycle) m.energy += m.fieldRegen * level.isReducedRegen;
if (m.energy < 0) m.energy = 0
},
regenEnergyDefault() {
- if (m.immuneCycle < m.cycle && m.fieldCDcycle < m.cycle) m.energy += m.fieldRegen;
+ if (m.immuneCycle < m.cycle && m.fieldCDcycle < m.cycle) m.energy += m.fieldRegen * level.isReducedRegen;
if (m.energy < 0) m.energy = 0
},
lookingAt(who) {
@@ -2696,7 +2698,7 @@ const m = {
if (tech.isTokamak && m.throwCharge > 4) { //remove the block body and pulse in the direction you are facing
//m.throwCharge > 5 seems to be when the field full colors in a block you are holding
m.throwCycle = m.cycle + 180 //used to detect if a block was thrown in the last 3 seconds
- if (m.immuneCycle < m.cycle) m.energy += 0.25 * Math.sqrt(m.holdingTarget.mass) * Math.min(5, m.throwCharge)
+ if (m.immuneCycle < m.cycle) m.energy += 0.25 * Math.sqrt(m.holdingTarget.mass) * Math.min(5, m.throwCharge) * level.isReducedRegen
m.throwCharge = 0;
m.definePlayerMass() //return to normal player mass
//remove block before pulse, so it doesn't get in the way
@@ -3051,7 +3053,7 @@ const m = {
m.pushMass(mob[i]);
if (tech.deflectEnergy && !mob[i].isInvulnerable && !mob[i].isShielded) {
- m.energy += tech.deflectEnergy
+ m.energy += tech.deflectEnergy * level.isReducedRegen
}
}
}
@@ -5049,7 +5051,7 @@ const m = {
Matter.Composite.remove(engine.world, body[i]);
body.splice(i, 1);
m.fieldRange *= 0.8
- if ((m.fieldMode === 0 || m.fieldMode === 9) && m.immuneCycle < m.cycle) m.energy += 0.02 * m.coupling
+ if ((m.fieldMode === 0 || m.fieldMode === 9) && m.immuneCycle < m.cycle) m.energy += 0.02 * m.coupling * level.isReducedRegen
if (tech.isWormholeWorms) { //pandimensional spermia
b.worm(Vector.add(m.hole.pos2, Vector.rotate({ x: m.fieldRange * 0.4, y: 0 }, 2 * Math.PI * Math.random())))
Matter.Body.setVelocity(bullet[bullet.length - 1], Vector.mult(Vector.rotate(m.hole.unit, Math.PI / 2), -10));
@@ -5071,10 +5073,8 @@ const m = {
Matter.Composite.remove(engine.world, body[i]);
body.splice(i, 1);
m.fieldRange *= 0.8
- // if (tech.isWormholeEnergy && m.energy < m.maxEnergy * 2) m.energy = m.maxEnergy * 2
- // if (tech.isWormholeEnergy && m.immuneCycle < m.cycle) m.energy += 0.5
- if ((m.fieldMode === 0 || m.fieldMode === 9) && m.immuneCycle < m.cycle) m.energy += 0.02 * m.coupling
- if (m.fieldMode === 0 || m.fieldMode === 9) m.energy += 0.02 * m.coupling
+ if ((m.fieldMode === 0 || m.fieldMode === 9) && m.immuneCycle < m.cycle) m.energy += 0.02 * m.coupling * level.isReducedRegen
+ if (m.fieldMode === 0 || m.fieldMode === 9) m.energy += 0.02 * m.coupling * level.isReducedRegen
if (tech.isWormholeWorms) { //pandimensional spermia
b.worm(Vector.add(m.hole.pos1, Vector.rotate({
x: m.fieldRange * 0.4,
@@ -5825,7 +5825,7 @@ const m = {
return
}
m.damage(dmg);
- if (tech.isPiezo) m.energy += 20.48;
+ if (tech.isPiezo) m.energy += 20.48 * level.isReducedRegen;
if (tech.isStimulatedEmission) powerUps.ejectTech()
if (mob[k].onHit) mob[k].onHit();
if (m.immuneCycle < m.cycle + m.collisionImmuneCycles) m.immuneCycle = m.cycle + m.collisionImmuneCycles; //player is immune to damage for 30 cycles
diff --git a/js/powerup.js b/js/powerup.js
index 3bc17185..4f3af638 100644
--- a/js/powerup.js
+++ b/js/powerup.js
@@ -503,6 +503,7 @@ const powerUps = {
endCycle: 0,
duration: null, //set by "tech: band gap"
damage: null, //set by "tech: band gap"
+ isDefense: false,
effect() {
powerUps.animatePowerUpGrab('rgba(255, 0, 0, 0.5)')
powerUps.boost.endCycle = simulation.cycle + Math.floor(Math.max(0, powerUps.boost.endCycle - simulation.cycle) * 0.6) + powerUps.boost.duration //duration+seconds plus 2/3 of current time left
@@ -605,7 +606,7 @@ const powerUps = {
}
if (tech.isResearchDamage) {
tech.damage *= 1.05
- simulation.inGameConsole(`1.05x damage`);
+ simulation.inGameConsole(`tech.damage *= ${1.05} //peer review`);
tech.addJunkTechToPool(0.01)
}
powerUps.research.currentRerollCount++
@@ -1448,7 +1449,7 @@ const powerUps = {
onPickUp(who) {
powerUps.research.currentRerollCount = 0
if (tech.isTechDamage && who.name === "tech") m.damage(0.1)
- if (tech.isMassEnergy) m.energy += 2;
+ if (tech.isMassEnergy) m.energy += 2 * level.isReducedRegen;
if (tech.isMineDrop && bullet.length < 150 && Math.random() < 0.5) {
if (tech.isLaserMine && input.down) {
b.laserMine(who.position)
diff --git a/js/simulation.js b/js/simulation.js
index c991faba..86dc5e99 100644
--- a/js/simulation.js
+++ b/js/simulation.js
@@ -1012,7 +1012,7 @@ const simulation = {
if (isNaN(player.position.x)) m.death();
if (m.lastKillCycle + 300 > m.cycle) { //effects active for 5 seconds after killing a mob
if (tech.isEnergyRecovery && m.immuneCycle < m.cycle) {
- m.energy += m.maxEnergy * 0.05
+ m.energy += m.maxEnergy * 0.05 * level.isReducedRegen
simulation.drawList.push({ //add dmg to draw queue
x: m.pos.x,
y: m.pos.y - 45,
@@ -1024,7 +1024,7 @@ const simulation = {
if (tech.isHealthRecovery) {
if (tech.isEnergyHealth) {
if (m.immuneCycle < m.cycle) {
- m.energy += m.maxEnergy * 0.005
+ m.energy += m.maxEnergy * 0.005 * level.isReducedRegen
simulation.drawList.push({ //add dmg to draw queue
x: m.pos.x,
y: m.pos.y,
diff --git a/js/spawn.js b/js/spawn.js
index 98f67757..b0f42072 100644
--- a/js/spawn.js
+++ b/js/spawn.js
@@ -144,7 +144,7 @@ const spawn = {
}
me.do = function () {
if (!simulation.isTimeSkipping) {
- const scale = (tech.isMoveDarkMatter || tech.isNotDarkMatter) ? 1.6 : 1
+ const scale = ((tech.isMoveDarkMatter || tech.isNotDarkMatter) ? 1.6 : 1) * level.isReducedRegen
const sine = Math.sin(simulation.cycle * 0.015)
this.radius = 111 * tech.isDarkStar + 370 * (1 + 0.1 * sine)
//chase player
@@ -7726,7 +7726,7 @@ const spawn = {
};
},
//chance = Math.min(0.02 + simulation.difficulty * 0.005, 0.2) + tech.duplicationChance()
- shield(target, x, y, chance = (level.isMobShields ? 3.25 : 1) * Math.min(0.02 + simulation.difficulty * 0.005, 0.2)) {
+ shield(target, x, y, chance = (level.isMobShields ? 4 : 1) * Math.min(0.02 + simulation.difficulty * 0.005, 0.2)) {
if (this.allowShields && Math.random() < chance) {
mobs.spawn(x, y, 9, target.radius + 30, "rgba(220,220,255,0.9)");
let me = mob[mob.length - 1];
diff --git a/js/tech.js b/js/tech.js
index 1b2c5264..fc7ca793 100644
--- a/js/tech.js
+++ b/js/tech.js
@@ -149,6 +149,11 @@ const tech = {
if (tech.tech[index].isLost) tech.tech[index].isLost = false; //give specific tech
if (tech.isBanish && tech.tech[index].isBanished) tech.tech[index].isBanished = false //stops the bug where you can't gets stacks of tech you take with decoherence, I think
+ if (tech.isDamageFieldTech && tech.tech[index].isFieldTech) {
+ tech.damage *= 1.15
+ // simulation.inGameConsole(`damage *= ${1.05}`)
+ simulation.inGameConsole(`tech.damage *= ${1.1} //hidden-variable theory`);
+ }
tech.tech[index].effect(); //give specific tech
tech.tech[index].count++
if (!tech.tech[index].isInstant) tech.totalCount++ //used in power up randomization
@@ -717,7 +722,7 @@ const tech = {
},
{
name: "ordnance",
- description: `spawn ${powerUps.orb.gun()} and get 2x frequency for ${powerUps.orb.gunTech()}
+6% JUNK chance`,
+ description: `spawn ${powerUps.orb.gun()} and get 2x frequency for ${powerUps.orb.gunTech()}
+6% JUNK choices`,
maxCount: 1,
count: 0,
frequency: 1,
@@ -1255,28 +1260,34 @@ const tech = {
let totalRate = 1
for (let i = 0; i < this.totalRate.length; i++) totalRate *= this.totalRate[i]
let currentRate = ""
- if (this.count) currentRate = `
(${(totalRate).toFixed(2)}x)`
- return `randomly gain between 1x and 1.5x fire rate` + currentRate
+ if (this.count) currentRate = `(${(totalRate).toFixed(2)}x)`
+ return `randomly gain between 1x and 2x fire rate
+5% JUNK choices` + currentRate
},
maxCount: 9,
count: 0,
frequency: 1,
frequencyDefault: 1,
allowed() {
- return true
+ return tech.junkChance < 1
},
requires: "",
totalRate: [], //tracks the random damage upgrades so it can be removed and in descriptionFunction
effect() {
- const rate = (Math.floor((Math.random() * 0.5 + 1) * 100)) / 100
+ const rate = (Math.floor((Math.random() + 1) * 100)) / 100
tech.fireRate /= rate
this.totalRate.push(rate)
b.setFireCD();
simulation.inGameConsole(`tech.fireRate *= ${rate} //heuristics`);
+ this.refundAmount += tech.addJunkTechToPool(0.05)
},
+ refundAmount: 0,
remove() {
if (this.count && m.alive) {
for (let i = 0; i < this.totalRate.length; i++) tech.fireRate *= this.totalRate[i]
+ if (this.refundAmount > 0) {
+ tech.removeJunkTechFromPool(this.refundAmount)
+ this.refundAmount = 0
+ }
}
this.totalRate.length = 0
b.setFireCD();
@@ -1460,7 +1471,9 @@ const tech = {
{
name: "band gap",
descriptionFunction() {
- return `${powerUps.orb.boost(1)} give 1.77x damage
but their duration is reduced by 1 second`
+ // return `${powerUps.orb.boost(1)} give ${(1 + powerUps.boost.damage).toFixed(2)}x ${(1 + powerUps.boost.damage + 0.77).toFixed(2)}x damage
but their duration is reduced by 1 second`
+ // const predict = this.count === 0 ? `${(1 + powerUps.boost.damage).toFixed(2)}x` : ``
+ return `${powerUps.orb.boost(1)} give an additional ${(1 + 0.75).toFixed(2)}x damage
but their duration is reduced by 1 second`
},
maxCount: 9,
count: 1,
@@ -1472,13 +1485,33 @@ const tech = {
requires: "exciton, quasiparticles",
effect() {
powerUps.boost.duration -= 60
- powerUps.boost.damage += 0.77
+ powerUps.boost.damage += 0.75
},
remove() {
powerUps.boost.duration = 600
powerUps.boost.damage = 1.25
}
},
+ {
+ name: "polariton",
+ descriptionFunction() {
+ return `${powerUps.orb.boost(1)} also give 0.3x damage taken
for ${(powerUps.boost.duration / 60).toFixed(0)} seconds`
+ },
+ maxCount: 9,
+ count: 1,
+ frequency: 2,
+ frequencyDefault: 2,
+ allowed() {
+ return tech.isBoostPowerUps || tech.isBoostReplaceAmmo
+ },
+ requires: "exciton, quasiparticles",
+ effect() {
+ powerUps.boost.isDefense = true
+ },
+ remove() {
+ powerUps.boost.isDefense = false
+ }
+ },
{
name: "collider",
descriptionFunction() {
@@ -2762,7 +2795,6 @@ const tech = {
requires: "not mass-energy",
effect() {
tech.isPiezo = true;
- // if (simulation.isTextLogOpen) m.energy += 20.48;
},
remove() {
tech.isPiezo = false;
@@ -2854,7 +2886,7 @@ const tech = {
},
{
name: "overcharge",
- description: "+88 maximum energy
+4% JUNK chance",
+ description: "+88 maximum energy
+4% JUNK choices",
maxCount: 9,
count: 0,
frequency: 1,
@@ -2880,7 +2912,7 @@ const tech = {
},
{
name: "Maxwells demon",
- description: "energy above maximum decays 30x slower
+5% JUNK chance",
+ description: "energy above maximum decays 30x slower
+5% JUNK choices",
maxCount: 1,
count: 0,
frequency: 2,
@@ -2916,7 +2948,7 @@ const tech = {
effect() {
tech.isCrouchRegen = true; //only used to check for requirements
m.regenEnergy = function () {
- if (m.immuneCycle < m.cycle && m.crouch && m.fieldCDcycle < m.cycle) m.energy += 7 * m.fieldRegen;
+ if (m.immuneCycle < m.cycle && m.crouch && m.fieldCDcycle < m.cycle) m.energy += 7 * m.fieldRegen * level.isReducedRegen;
if (m.energy < 0) m.energy = 0
}
},
@@ -2957,7 +2989,7 @@ const tech = {
effect() {
tech.isDamageAfterKillNoRegen = true;
m.regenEnergy = function () {
- if (m.immuneCycle < m.cycle && (m.lastKillCycle + 300 < m.cycle) && m.fieldCDcycle < m.cycle) m.energy += m.fieldRegen;
+ if (m.immuneCycle < m.cycle && (m.lastKillCycle + 300 < m.cycle) && m.fieldCDcycle < m.cycle) m.energy += m.fieldRegen * level.isReducedRegen;
if (m.energy < 0) m.energy = 0
}
},
@@ -3250,7 +3282,7 @@ const tech = {
{
name: "adiabatic healing",
descriptionFunction() {
- return `2x healing from ${powerUps.orb.heal()}
+4% JUNK chance`
+ return `2x healing from ${powerUps.orb.heal()}
+4% JUNK choices`
},
maxCount: 3,
count: 0,
@@ -3338,7 +3370,7 @@ const tech = {
{
name: "accretion disk",
descriptionFunction() {
- return `1.07x damage for each power up on this level
+5% JUNK chance (${(1 + 0.07 * powerUp.length).toFixed(2)}x)`
+ return `1.07x damage for each power up on this level
+5% JUNK choices (${(1 + 0.07 * powerUp.length).toFixed(2)}x)`
},
maxCount: 1,
count: 0,
@@ -3584,7 +3616,7 @@ const tech = {
},
{
name: "peer review",
- description: `after you research gain 1.05x damage
and +1% JUNK chance`,
+ description: `after you research gain 1.05x damage
and +1% JUNK choices`,
maxCount: 1,
count: 0,
frequency: 1,
@@ -3602,7 +3634,7 @@ const tech = {
},
{
name: "pseudoscience",
- description: "research 2 times for free, but
add 1% JUNK chance each time",
+ description: "research 2 times for free, but
+1% JUNK choices each time",
maxCount: 1,
count: 0,
frequency: 1,
@@ -3620,7 +3652,7 @@ const tech = {
},
{
name: "renormalization",
- description: `47% chance to spawn ${powerUps.orb.research(1)} after consuming ${powerUps.orb.research(1)}
+5% JUNK chance`,
+ description: `47% chance to spawn ${powerUps.orb.research(1)} after consuming ${powerUps.orb.research(1)}
+5% JUNK choices`,
maxCount: 1,
count: 0,
frequency: 2,
@@ -3817,7 +3849,7 @@ const tech = {
{
name: "path integral",
link: `path integral`,
- description: `your next ${powerUps.orb.tech()} has all possible choices
+4% JUNK chance`,
+ description: `your next ${powerUps.orb.tech()} has all possible choices
+4% JUNK choices`,
maxCount: 1,
count: 0,
frequency: 1,
@@ -3909,7 +3941,7 @@ const tech = {
},
{
name: "meta-analysis",
- description: `if you choose a JUNK you instead get a
random nonJUNK ${powerUps.orb.tech()} and ${powerUps.orb.research(2)}`,
+ description: `if you choose JUNK
you get a random choice and ${powerUps.orb.research(2)} instead`,
maxCount: 1,
count: 0,
frequency: 1,
@@ -3927,7 +3959,7 @@ const tech = {
},
{
name: "dark patterns",
- description: "1.3x damage
+15% JUNK chance",
+ description: "1.3x damage
+15% JUNK choices",
maxCount: 9,
count: 0,
frequency: 1,
@@ -4264,7 +4296,7 @@ const tech = {
},
{
name: "replication",
- description: "+10% chance to duplicate spawned power ups
+15% JUNK chance",
+ description: "+10% chance to duplicate spawned power ups
+15% JUNK choices",
maxCount: 9,
count: 0,
frequency: 1,
@@ -4565,7 +4597,7 @@ const tech = {
{
name: "paradigm shift",
descriptionFunction() {
- return `when paused clicking your ${powerUps.orb.tech()} ejects them
–${tech.pauseEjectTech.toFixed(1)} ${tech.isEnergyHealth ? "energy" : "health"} cost (1.3x cost each use)`
+ return `when paused clicking your ${powerUps.orb.tech()} ejects them
costs ${tech.pauseEjectTech.toFixed(1)} ${tech.isEnergyHealth ? "energy" : "health"} (1.3x cost each use)`
},
maxCount: 1,
count: 0,
@@ -4657,7 +4689,7 @@ const tech = {
for (let i = 0, len = pool.length * 0.5; i < len; i++) removeCount += tech.removeTech(pool[i])
this.damage = this.damagePerRemoved * removeCount
tech.damage *= (1 + this.damage)
- simulation.inGameConsole(`${(1 + this.damage).toFixed(2)}x damage //from Occam's razor`, 360)
+ simulation.inGameConsole(`tech.damage *= ${(1 + this.damage).toFixed(2)} //from Occam's razor`);
},
remove() {
if (this.count && m.alive) tech.damage /= (1 + this.damage)
@@ -4990,7 +5022,7 @@ const tech = {
{
name: "irradiated nails",
link: `irradiated nails`,
- description: "nails, needles, and rivets are radioactive
1.9x radioactive damage over 3 seconds",
+ description: "nails, needles, and rivets are radioactive
2x radioactive damage over 3 seconds",
isGunTech: true,
maxCount: 1,
count: 0,
@@ -6189,7 +6221,7 @@ const tech = {
},
{
name: "booby trap",
- description: "50% chance to drop a mine from power ups
+15% JUNK chance",
+ description: "50% chance to drop a mine from power ups
+15% JUNK choices",
isGunTech: true,
maxCount: 1,
count: 0,
@@ -7762,7 +7794,7 @@ const tech = {
},
{
name: "flux pinning",
- description: "after deflecting a mob
it is stunned for up to 4 seconds",
+ description: `mobs deflected by your ${powerUps.orb.field()}
are stunned for 4 seconds`,
isFieldTech: true,
maxCount: 9,
count: 0,
@@ -7988,7 +8020,7 @@ const tech = {
}
},
{
- name: "modified Newtonian dynamics",
+ name: "MOND",
descriptionFunction() {
return `your speed counts as +20 higher
(for Newton's 1st and 2nd laws)`
},
@@ -8122,7 +8154,7 @@ const tech = {
requires: "molecular assembler, pilot wave, standing wave",
effect() {
tech.isMassEnergy = true // used in m.grabPowerUp
- m.energy += 2
+ m.energy += 2 * level.isReducedRegen
},
remove() {
tech.isMassEnergy = false;
@@ -8643,6 +8675,25 @@ const tech = {
tech.isMobFullHealthCloak = false
}
},
+ {
+ name: "hidden-variable theory",
+ description: `1.15x damage each time you choose ${powerUps.orb.fieldTech()}`,
+ isFieldTech: true,
+ maxCount: 1,
+ count: 0,
+ frequency: 1,
+ frequencyDefault: 1,
+ allowed() {
+ return m.fieldMode === 8
+ },
+ requires: "pilot wave",
+ effect() {
+ tech.isDamageFieldTech = true
+ },
+ remove() {
+ tech.isDamageFieldTech = false
+ }
+ },
{
name: "WIMPs",
description: `at each level's exit, spawn ${powerUps.orb.research(4)}
and a dangerous particle that slowly chases you`,
@@ -9450,7 +9501,7 @@ const tech = {
requestAnimationFrame(() => {
if ((simulation.cycle % 1440) > 720) { //kinda alternate between each option
m.rewind(60)
- m.energy += 0.4 //to make up for lost energy
+ m.energy += 0.4 * level.isReducedRegen//to make up for lost energy
} else {
simulation.timePlayerSkip(60)
}
@@ -10645,7 +10696,7 @@ const tech = {
effect() {
setInterval(() => {
m.rewind(120)
- m.energy += 0.4
+ m.energy += 0.4 * level.isReducedRegen
}, 10000);
// for (let i = 0; i < 24; i++) {
// setTimeout(() => { m.rewind(120) }, i * 5000);
@@ -10668,7 +10719,7 @@ const tech = {
effect() {
setInterval(() => {
m.rewind(30)
- m.energy += 0.2
+ m.energy += 0.2 * level.isReducedRegen
}, 4000);
},
remove() { }
@@ -10742,7 +10793,7 @@ const tech = {
},
{
name: "expert system",
- description: `spawn ${powerUps.orb.tech()}
+50% JUNK chance`,
+ description: `spawn ${powerUps.orb.tech()}
+50% JUNK choices`,
maxCount: 9,
count: 0,
frequency: 0,
@@ -12076,4 +12127,5 @@ const tech = {
isEjectOld: null,
isWiki: null,
isStaticBlock: null,
+ isDamageFieldTech: null,
}
\ No newline at end of file
diff --git a/todo.txt b/todo.txt
index 5f1086c0..e90b56d7 100644
--- a/todo.txt
+++ b/todo.txt
@@ -1,54 +1,33 @@
******************************************************** NEXT PATCH **************************************************
-difficulty
- difficulty level 6 adds flat damage and damage taken
- bonus tech no longer spawns on level 2 and 3 on difficulty level 6
- at the end of subway you get 1 tech, but not on difficulty level 6
- difficulty level 3 and 5 add a random constraint that changes each level
-constraints are effects that only last until the level ends
- 50% JUNK chance
- 4x shielded mob chance
- power ups are sent to next level
- +33% chance for mobs to respawn
- -1 choice
- 2x ammo costs
- duplication is set to zero
- 50% max energy
- 50% max health
- bots follow slow
- full damage taken after boss dies
- 0.1x damage after a power up
- mob death heals mobs
- mobs heal for your lost health
- periodically spawn WIMPs
-
-exciton damage boost power up has a chance to spawn without the tech (~3%/mob)
- damage boost has a unique gel/hair aura for each skin
- damage boost timer no longer ticks with time dilation field
-JUNK tech: stationary - thrown blocks can't move, but they still have momentum
-added a classic n-gon link for the previous patch in settings
- but images are disabled to save space
-on levels where you can fall endlessly, power ups will also fall endlessly
- they no longer teleport to the exit, sorry
-
-Newton's 1st and 2nd laws are field tech, and they give twice the effect
-abelian group 4->3x damage while invulnerable
-bot fabrication price increases after 5->4 bots
-wikipedia 4->3 research per correct quiz
-upgraded sound bots fire fewer waves but do more damage per wave
- not much changed except improved performance, I think
-incendiary ammunition drones explode when they run out of durability not on the first hit
- this allows better synergy with other drone tech
-grappling hook retract momentum no longer scales with distance
- this should give you more control
-pressing the 3rd button in factory will remove blocks resting on the second block
- preventing an endless toggle
-
-bug fixes
- fleas no longer die early after hitting a high health target only once
- something with super ball density calculations for tech rebound
- grabbing a big block can make grappling hook go flying
- added 3 potential fixes, but the bug is too rare know if it's fixed
+tech: hidden-variable theory - after choosing a fieldtech gain 1.15x damage
+ for pilot wave only
+polariton - boosts also give 0.3x damage taken
+
+constraints
+ removed
+ full damage taken after boss dies
+ wording is too unclear
+ new
+ 0.5x energy regen from all sources
+ balanced
+ slow bots
+ bots have roughly 15% reduction in damage in addition to a slow follow speed
+ mob death heals mobs
+ has 1000->700 range and 1->0.33 healing
+ periodically spawn WIMPs
+ has a 30s delay and a 15->6s spawn rate
+ 50->40% JUNK chance
+
+
+heuristics gives (1-1.5x)->(1-2x) fire rate and +5% JUNK
+autonomous defense harpoon now scale from Bessemer process
+ but at half the rate since there are 6 harpoons
+Bessemer process and rail gun scale at 0.1->0.07
+
+bugs
+ crash with training level "heal" and power ups
+ set difficulty mode level 2 for training
******************************************************** BUGS ********************************************************
@@ -80,19 +59,36 @@ player can become crouched while not touching the ground if they exit the ground
*********************************************************** TODO *****************************************************
+procedural animation
+ https://www.youtube.com/watch?v=qlfh_rv6khY
+
+maybe no constraints on final boss and reactor?
+
+constraints balance
+ 50% JUNK chance
+ 4x shielded mob chance
+ power ups are sent to next level
+ +33% chance for mobs to respawn
+ -1 choice
+ 2x ammo costs
+ maybe nerf...
+ 0 duplication
+ 50% max energy
+ 50% max health
+ bots follow slow
+ 0.1x damage after a power up
+ mob death heals mobs
+ mobs heal for your lost health
+ periodically spawn WIMPs
+ 0.5x energy regen
+
each difficulty setting adds a chance for a random effect
make some effects only possible on certain levels, or with certain bosses?
not implemented random constraint ideas________________________
- if player takes too long on a level
- spawn mobs at the end of player's history
- give a warning before they spawn
- black holes, sneakers, bullets
mob death spawns something
- bullets
+ mob bullets
bosses heals nearby mobs
ammo power ups give 0.7x ammo
- 2x energy costs
- 0.5x energy regen from all sources
2x research costs
mobs slowly regen health
exit door takes 10x time to open,
@@ -108,6 +104,12 @@ each difficulty setting adds a chance for a random effect
explosions do 0.5x damage
freeze effects last 0.25x time
+tech: - lower damage taken over 10s to 0x but after taking damage increase damage taken to 1x
+ isn't this just CPT skin with less steps?
+ maybe skin
+
+tech: - freezing grenades/explosions
+
tech: - randomize constraints somehow
in pause interface or power up selection menu?
each time you research the current constraints also randomize?