tech: cache - ammo power ups give 11x ammo, but you can't hold over 11x ammo

harpoon
  grabs 1 power up on the way out, or in
  harpooned power ups are predictable
    they attach to the harpoon instead of using physics to move towards player

bugs fixes
  lasers were broke, but I fixed them
This commit is contained in:
landgreen
2021-09-16 06:25:33 -07:00
parent 8cf4197219
commit 4eed719d10
8 changed files with 134 additions and 97 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -115,6 +115,7 @@ const b = {
} }
}, },
giveGuns(gun = "random", ammoPacks = 10) { giveGuns(gun = "random", ammoPacks = 10) {
if (tech.ammoCap) ammoPacks = 0.45 * tech.ammoCap
if (tech.isOneGun) b.removeAllGuns(); if (tech.isOneGun) b.removeAllGuns();
if (gun === "random") { if (gun === "random") {
//find what guns player doesn't have //find what guns player doesn't have
@@ -131,7 +132,7 @@ const b = {
for (let i = 0; i < b.guns.length; i++) { for (let i = 0; i < b.guns.length; i++) {
b.inventory[i] = i; b.inventory[i] = i;
b.guns[i].have = true; b.guns[i].have = true;
b.guns[i].ammo = Math.floor(b.guns[i].ammoPack * ammoPacks); b.guns[i].ammo = Math.ceil(b.guns[i].ammoPack * ammoPacks);
} }
b.activeGun = 0; b.activeGun = 0;
} else { } else {
@@ -148,7 +149,7 @@ const b = {
} }
if (!b.guns[gun].have) b.inventory.push(gun); if (!b.guns[gun].have) b.inventory.push(gun);
b.guns[gun].have = true; b.guns[gun].have = true;
b.guns[gun].ammo = Math.floor(b.guns[gun].ammoPack * ammoPacks); b.guns[gun].ammo = Math.ceil(b.guns[gun].ammoPack * ammoPacks);
if (b.activeGun === null) { if (b.activeGun === null) {
b.activeGun = gun //if no active gun switch to new gun b.activeGun = gun //if no active gun switch to new gun
if (b.guns[b.activeGun].charge) b.guns[b.activeGun].charge = 0; //set foam charge to zero if foam is a new gun if (b.guns[b.activeGun].charge) b.guns[b.activeGun].charge = 0; //set foam charge to zero if foam is a new gun
@@ -1148,7 +1149,32 @@ const b = {
} }
}, },
onEnd() {}, caughtPowerUp: null,
dropCaughtPowerUp() {
if (this.caughtPowerUp) {
this.caughtPowerUp.collisionFilter.category = cat.powerUp
this.caughtPowerUp.collisionFilter.mask = cat.map | cat.powerUp
this.caughtPowerUp = null
}
},
onEnd() {
if (this.caughtPowerUp && !simulation.isChoosing && (this.caughtPowerUp.name !== "heal" || m.health !== m.maxHealth || tech.isOverHeal)) {
let index = null //find index
for (let i = 0, len = powerUp.length; i < len; ++i) {
if (powerUp[i] === this.caughtPowerUp) index = i
}
if (index !== null) {
powerUps.onPickUp(this.caughtPowerUp);
this.caughtPowerUp.effect();
Matter.Composite.remove(engine.world, this.caughtPowerUp);
powerUp.splice(index, 1);
} else {
this.dropCaughtPowerUp()
}
} else {
this.dropCaughtPowerUp()
}
},
drawString() { drawString() {
if (isReturn) { if (isReturn) {
const where = { const where = {
@@ -1183,50 +1209,37 @@ const b = {
break; break;
} }
} }
// if you grabbed a power up, stop it near the player
// for (let i = 0, len = powerUp.length; i < len; ++i) { //near power up
// if (Vector.magnitudeSquared(Vector.sub(this.position, powerUp[i].position)) < 6000) {
// Matter.Body.setVelocity(powerUp[i], { x: 0, y: -2 })
// break
// }
// }
for (let i = 0, len = powerUp.length; i < len; ++i) {
if ( //use power up if it is close enough
Vector.magnitudeSquared(Vector.sub(this.position, powerUp[i].position)) < 6000 &&
!simulation.isChoosing &&
(powerUp[i].name !== "heal" || m.health !== m.maxHealth || tech.isOverHeal)
) {
powerUps.onPickUp(powerUp[i]);
Matter.Body.setVelocity(player, { //player knock back, after grabbing power up
x: player.velocity.x + powerUp[i].velocity.x / player.mass * 1,
y: player.velocity.y + powerUp[i].velocity.y / player.mass * 1
});
powerUp[i].effect();
Matter.Composite.remove(engine.world, powerUp[i]);
powerUp.splice(i, 1);
break; //because the array order is messed up after splice
}
}
} else { } else {
let isPulling = false
for (let i = 0, len = powerUp.length; i < len; ++i) { //near power up
if (Vector.magnitudeSquared(Vector.sub(this.vertices[2], powerUp[i].position)) < 3000) {
Matter.Body.setVelocity(powerUp[i], this.velocity)
Matter.Body.setPosition(powerUp[i], this.vertices[2])
isPulling = true
this.endCycle += 0.5 //it pulls back slower, so this prevents it from ending early
break //just pull 1 power up if possible
}
}
if (m.energy > 0.005) m.energy -= 0.005 if (m.energy > 0.005) m.energy -= 0.005
const sub = Vector.sub(this.position, m.pos) const sub = Vector.sub(this.position, m.pos)
const rangeScale = 1 + 0.000001 * Vector.magnitude(sub) * Vector.magnitude(sub) //return faster when far from player const rangeScale = 1 + 0.000001 * Vector.magnitude(sub) * Vector.magnitude(sub) //return faster when far from player
const returnForce = Vector.mult(Vector.normalise(sub), rangeScale * this.thrustMag * this.mass * (isPulling ? 0.6 : 1)) const returnForce = Vector.mult(Vector.normalise(sub), rangeScale * this.thrustMag * this.mass)
this.force.x -= returnForce.x this.force.x -= returnForce.x
this.force.y -= returnForce.y this.force.y -= returnForce.y
this.drawString() this.drawString()
this.grabPowerUp()
}
},
grabPowerUp() { //grab power ups near the tip of the harpoon
if (this.caughtPowerUp) {
Matter.Body.setPosition(this.caughtPowerUp, Vector.add(this.vertices[2], this.velocity))
Matter.Body.setVelocity(this.caughtPowerUp, { x: 0, y: 0 })
} else { //&& simulation.cycle % 2
for (let i = 0, len = powerUp.length; i < len; ++i) {
const radius = powerUp[i].circleRadius + 25
if (Vector.magnitudeSquared(Vector.sub(this.vertices[2], powerUp[i].position)) < radius * radius) {
if (powerUp[i].name !== "heal" || m.health !== m.maxHealth || tech.isOverHeal) {
this.caughtPowerUp = powerUp[i]
Matter.Body.setVelocity(powerUp[i], { x: 0, y: 0 })
Matter.Body.setPosition(powerUp[i], this.vertices[2])
powerUp[i].collisionFilter.category = 0
powerUp[i].collisionFilter.mask = 0
this.thrustMag *= 0.6
this.endCycle += 0.5 //it pulls back slower, so this prevents it from ending early
break //just pull 1 power up if possible
}
}
}
} }
}, },
do() { do() {
@@ -1240,11 +1253,14 @@ const b = {
this.force.y -= returnForce.y this.force.y -= returnForce.y
this.frictionAir = 0.002 this.frictionAir = 0.002
this.do = () => { this.force.y += this.mass * 0.001; } this.do = () => { this.force.y += this.mass * 0.001; }
this.dropCaughtPowerUp()
} else { //return to player } else { //return to player
this.do = this.returnToPlayer this.do = this.returnToPlayer
if (this.angularSpeed < 0.5) this.torque += this.inertia * 0.001 * (Math.random() - 0.5) //(Math.round(Math.random()) ? 1 : -1) if (this.angularSpeed < 0.5) this.torque += this.inertia * 0.001 * (Math.random() - 0.5) //(Math.round(Math.random()) ? 1 : -1)
this.collisionFilter.mask = cat.map | cat.mob | cat.mobBullet | cat.mobShield // | cat.body this.collisionFilter.mask = cat.map | cat.mob | cat.mobBullet | cat.mobShield // | cat.body
} }
} else {
this.grabPowerUp()
} }
} else if (this.cycle > 30) { } else if (this.cycle > 30) {
this.frictionAir = 0.003 this.frictionAir = 0.003
@@ -4730,7 +4746,7 @@ const b = {
name: "mine", name: "mine",
description: "toss a <strong>proximity</strong> mine that <strong>sticks</strong> to walls<br>refund <strong>undetonated</strong> mines on <strong>exiting</strong> a level", //fires <strong>nails</strong> at mobs within range description: "toss a <strong>proximity</strong> mine that <strong>sticks</strong> to walls<br>refund <strong>undetonated</strong> mines on <strong>exiting</strong> a level", //fires <strong>nails</strong> at mobs within range
ammo: 0, ammo: 0,
ammoPack: 1.1, ammoPack: 1.25,
have: false, have: false,
do() {}, do() {},
fire() { fire() {

View File

@@ -2292,18 +2292,14 @@ const level = {
// spawn.focuser(1600, -500) // spawn.focuser(1600, -500)
// spawn.laserTargetingBoss(1700, -120) // spawn.laserTargetingBoss(1700, -120)
// spawn.bomberBoss(1400, -500) // spawn.bomberBoss(1400, -500)
// spawn.beamer(1800, -120) // spawn.laser(1800, -120)
// spawn.orbitalBoss(1600, -500) // spawn.laserBombingBoss(1600, -500)
// spawn.powerUpBoss(1600, -500)
// spawn.cellBossCulture(1600, -500)
// spawn.laserTargetingBoss(1600, -500) // spawn.laserTargetingBoss(1600, -500)
// spawn.laser(1200, -500) // spawn.laserBoss(1600, -500)
// spawn.cellBossCulture(1600, -500)
spawn.nodeGroup(1200, -500, "grenadier") spawn.nodeGroup(1200, -500, "grenadier")
spawn.nodeGroup(1800, -500, "grenadier") spawn.nodeGroup(1800, -500, "grenadier")
spawn.nodeGroup(1200, 0, "grenadier") spawn.nodeGroup(1200, 0, "grenadier")
// spawn.snakeBoss(1200, -500) // spawn.snakeBoss(1200, -500)
// spawn.suckerBoss(2900, -500) // spawn.suckerBoss(2900, -500)
// spawn.randomMob(1600, -500) // spawn.randomMob(1600, -500)

View File

@@ -532,7 +532,7 @@ const mobs = {
vertexCollision(this.position, look, body); vertexCollision(this.position, look, body);
if (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]); if (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]);
// hitting player // hitting player
if (best.who === player) { if (best.who === playerBody || best.who === playerHead) {
if (m.immuneCycle < m.cycle) { if (m.immuneCycle < m.cycle) {
const dmg = 0.0012 * simulation.dmgScale; const dmg = 0.0012 * simulation.dmgScale;
m.damage(dmg); m.damage(dmg);

View File

@@ -455,23 +455,34 @@ const powerUps = {
return 17; return 17;
}, },
effect() { effect() {
if (tech.isAmmoForGun && b.inventory.length > 0 && b.activeGun) { if (tech.isAmmoForGun && b.inventory.length > 0 && b.activeGun) { //give extra ammo to one gun only with tech logistics
const target = b.guns[b.activeGun] const target = b.guns[b.activeGun]
if (target.ammo !== Infinity) { if (target.ammo !== Infinity) {
const ammoAdded = Math.ceil((0.7 * Math.random() + 0.7 * Math.random()) * target.ammoPack) if (tech.ammoCap) {
target.ammo += ammoAdded const ammoAdded = Math.ceil(target.ammoPack * 0.7 * tech.ammoCap) //0.7 is average
simulation.makeTextLog(`${target.name}.<span class='color-g'>ammo</span> <span class='color-symbol'>+=</span> ${ammoAdded}`) target.ammo = ammoAdded
simulation.makeTextLog(`${target.name}.<span class='color-g'>ammo</span> <span class='color-symbol'>=</span> ${ammoAdded}`)
} else {
const ammoAdded = Math.ceil((0.7 * Math.random() + 0.7 * Math.random()) * target.ammoPack)
target.ammo += ammoAdded
simulation.makeTextLog(`${target.name}.<span class='color-g'>ammo</span> <span class='color-symbol'>+=</span> ${ammoAdded}`)
}
} }
} else { //give ammo to all guns in inventory } else { //give ammo to all guns in inventory
for (let i = 0, len = b.inventory.length; i < len; i++) { for (let i = 0, len = b.inventory.length; i < len; i++) {
const target = b.guns[b.inventory[i]] const target = b.guns[b.inventory[i]]
if (target.ammo !== Infinity) { if (target.ammo !== Infinity) {
const ammoAdded = Math.ceil((0.5 * Math.random() + 0.4 * Math.random()) * target.ammoPack) //Math.ceil(Math.random() * target.ammoPack) if (tech.ammoCap) {
target.ammo += ammoAdded const ammoAdded = Math.ceil(target.ammoPack * 0.45 * tech.ammoCap) //0.45 is average
simulation.makeTextLog(`${target.name}.<span class='color-g'>ammo</span> <span class='color-symbol'>+=</span> ${ammoAdded}`) target.ammo = ammoAdded
simulation.makeTextLog(`${target.name}.<span class='color-g'>ammo</span> <span class='color-symbol'>=</span> ${ammoAdded}`)
} else {
const ammoAdded = Math.ceil((0.45 * Math.random() + 0.45 * Math.random()) * target.ammoPack) //Math.ceil(Math.random() * target.ammoPack)
target.ammo += ammoAdded
simulation.makeTextLog(`${target.name}.<span class='color-g'>ammo</span> <span class='color-symbol'>+=</span> ${ammoAdded}`)
}
} }
} }
} }
simulation.updateGunHUD(); simulation.updateGunHUD();
} }

View File

@@ -645,8 +645,8 @@ const spawn = {
// vertexCollision(where, look, mob); // vertexCollision(where, look, mob);
vertexCollision(where, look, map); vertexCollision(where, look, map);
vertexCollision(where, look, body); vertexCollision(where, look, body);
if (!m.isCloak) vertexCollision(where, look, [player]); if (!m.isCloak) vertexCollision(where, look, [playerBody, playerHead]);
if (best.who && best.who === player && m.immuneCycle < m.cycle) { if (best.who && (best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
if (m.immuneCycle < m.cycle + 60 + tech.collisionImmuneCycles) m.immuneCycle = m.cycle + 60 + tech.collisionImmuneCycles; //player is immune to damage extra time if (m.immuneCycle < m.cycle + 60 + tech.collisionImmuneCycles) m.immuneCycle = m.cycle + 60 + tech.collisionImmuneCycles; //player is immune to damage extra time
m.damage(dmg); m.damage(dmg);
simulation.drawList.push({ //add dmg to draw queue simulation.drawList.push({ //add dmg to draw queue
@@ -2014,7 +2014,6 @@ const spawn = {
mobs.spawn(x, y, 3, radius, color); mobs.spawn(x, y, 3, radius, color);
let me = mob[mob.length - 1]; let me = mob[mob.length - 1];
me.isBoss = true; me.isBoss = true;
me.vertices = Matter.Vertices.rotate(me.vertices, Math.PI, me.position); //make the pointy side of triangle the front me.vertices = Matter.Vertices.rotate(me.vertices, Math.PI, me.position); //make the pointy side of triangle the front
Matter.Body.rotate(me, Math.random() * Math.PI * 2); Matter.Body.rotate(me, Math.random() * Math.PI * 2);
me.accelMag = 0.00018 * Math.sqrt(simulation.accelScale); me.accelMag = 0.00018 * Math.sqrt(simulation.accelScale);
@@ -2025,10 +2024,7 @@ const spawn = {
me.frictionStatic = 0; me.frictionStatic = 0;
me.friction = 0; me.friction = 0;
me.lookTorque = 0.000001 * (Math.random() > 0.5 ? -1 : 1); me.lookTorque = 0.000001 * (Math.random() > 0.5 ? -1 : 1);
me.fireDir = { me.fireDir = { x: 0, y: 0 }
x: 0,
y: 0
}
Matter.Body.setDensity(me, 0.008); //extra dense //normal is 0.001 //makes effective life much larger Matter.Body.setDensity(me, 0.008); //extra dense //normal is 0.001 //makes effective life much larger
spawn.shield(me, x, y, 1); spawn.shield(me, x, y, 1);
spawn.spawnOrbitals(me, radius + 200 + 300 * Math.random()) spawn.spawnOrbitals(me, radius + 200 + 300 * Math.random())
@@ -2128,16 +2124,14 @@ const spawn = {
if (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]); if (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]);
// hitting player // hitting player
if (best.who === player) { if ((best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
if (m.immuneCycle < m.cycle) { const dmg = 0.002 * simulation.dmgScale;
const dmg = 0.002 * simulation.dmgScale; m.damage(dmg);
m.damage(dmg); //draw damage
//draw damage ctx.fillStyle = color;
ctx.fillStyle = color; ctx.beginPath();
ctx.beginPath(); ctx.arc(best.x, best.y, dmg * 10000, 0, 2 * Math.PI);
ctx.arc(best.x, best.y, dmg * 10000, 0, 2 * Math.PI); ctx.fill();
ctx.fill();
}
} }
//draw beam //draw beam
if (best.dist2 === Infinity) best = look; if (best.dist2 === Infinity) best = look;
@@ -2258,7 +2252,7 @@ const spawn = {
if (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]); if (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]);
// hitting player // hitting player
if (best.who === player) { if (best.who === playerBody || best.who === playerHead) {
this.targetingCount++ this.targetingCount++
if (this.targetingCount > this.targetingTime) { if (this.targetingCount > this.targetingTime) {
this.targetingCount -= 10; this.targetingCount -= 10;
@@ -2763,8 +2757,8 @@ const spawn = {
// vertexCollision(where, look, mob); // vertexCollision(where, look, mob);
vertexCollision(where, look, map); vertexCollision(where, look, map);
vertexCollision(where, look, body); vertexCollision(where, look, body);
if (!m.isCloak) vertexCollision(where, look, [player]); if (!m.isCloak) vertexCollision(where, look, [playerBody, playerHead]);
if (best.who && best.who === player && m.immuneCycle < m.cycle) { if (best.who && (best.who === playerBody || best.who === playerHead) && m.immuneCycle < m.cycle) {
m.immuneCycle = m.cycle + tech.collisionImmuneCycles + 60; //player is immune to damage for an extra second m.immuneCycle = m.cycle + tech.collisionImmuneCycles + 60; //player is immune to damage for an extra second
const dmg = 0.14 * simulation.dmgScale; const dmg = 0.14 * simulation.dmgScale;
m.damage(dmg); m.damage(dmg);

View File

@@ -416,8 +416,8 @@
description: `${powerUps.orb.ammo()} give <strong>80%</strong> more <strong class='color-ammo'>ammo</strong><br>but it's only added to your current <strong class='color-g'>gun</strong>`, description: `${powerUps.orb.ammo()} give <strong>80%</strong> more <strong class='color-ammo'>ammo</strong><br>but it's only added to your current <strong class='color-g'>gun</strong>`,
maxCount: 1, maxCount: 1,
count: 0, count: 0,
frequency: 2, frequency: 1,
frequencyDefault: 2, frequencyDefault: 1,
allowed() { allowed() {
return !tech.isEnergyNoAmmo return !tech.isEnergyNoAmmo
}, },
@@ -450,6 +450,25 @@
}, },
remove() {} remove() {}
}, },
{
name: "cache",
description: `${powerUps.orb.ammo()} gives <strong>11x</strong> more <strong class='color-ammo'>ammo</strong>, but<br>you can't <strong>store</strong> any more <strong class='color-ammo'>ammo</strong> than that`,
maxCount: 1,
count: 0,
frequency: 1,
frequencyDefault: 1,
allowed() {
return !tech.isEnergyNoAmmo
},
requires: "not exciton-lattice",
effect() {
tech.ammoCap = 11;
powerUps.ammo.effect()
},
remove() {
tech.ammoCap = 0;
}
},
{ {
name: "catabolism", name: "catabolism",
description: `firing while <strong>out</strong> of <strong class='color-ammo'>ammo</strong> spawns ${powerUps.orb.ammo(4)}<br>and reduces your <strong>maximum</strong> <strong class='color-h'>health</strong> by <strong>1</strong>`, description: `firing while <strong>out</strong> of <strong class='color-ammo'>ammo</strong> spawns ${powerUps.orb.ammo(4)}<br>and reduces your <strong>maximum</strong> <strong class='color-h'>health</strong> by <strong>1</strong>`,
@@ -473,8 +492,8 @@
description: "every other <strong>crouched</strong> shot uses no <strong class='color-ammo'>ammo</strong><br><strong>+6</strong> <strong class='color-j'>JUNK</strong> to the potential <strong class='color-m'>tech</strong> pool", description: "every other <strong>crouched</strong> shot uses no <strong class='color-ammo'>ammo</strong><br><strong>+6</strong> <strong class='color-j'>JUNK</strong> to the potential <strong class='color-m'>tech</strong> pool",
maxCount: 1, maxCount: 1,
count: 0, count: 0,
frequency: 2, frequency: 1,
frequencyDefault: 2, frequencyDefault: 1,
allowed() { allowed() {
return true return true
}, },
@@ -8120,5 +8139,6 @@
isFilament: null, isFilament: null,
// isSpear: null, // isSpear: null,
isLargeHarpoon: null, isLargeHarpoon: null,
extraHarpoons: null extraHarpoons: null,
ammoCap: null
} }

View File

@@ -1,17 +1,14 @@
******************************************************** NEXT PATCH ************************************************** ******************************************************** NEXT PATCH **************************************************
gun alt fire is determined by the down key not the player crouch state tech: cache - ammo power ups give 11x ammo, but you can't hold over 11x ammo
(so you can control alt fire when in the air or stuck in a tunnel)
I did this with text replace, so it could produce some bugs
harpoon harpoon
automatically uses power ups that return to player grabs 1 power up on the way out, or in
will aim at harder to hit targets, and possible miss harpooned power ups are predictable
returns extra fast if it is far from the player they attach to the harpoon instead of using physics to move towards player
bullets last a bit longer so they don't despawn early
cd on miss fire lowered to 1.5s (was 3s)
bugs fixes bugs fixes
lasers were broke, but I fixed them
******************************************************** TODO ******************************************************** ******************************************************** TODO ********************************************************
@@ -19,9 +16,15 @@ bugs fixes
disable zoom progress when paused disable zoom progress when paused
gun: harpoon harpoon
return to player is slower for heavier harpoons post launch tracking: more airFriction, more thrust, harder turning
if no target found slow down and aim much better?
harpoon tech harpoon tech
tech that buffs alt fire:
remove the string, all shots are alt fire
alt fire has a 50% chance to not use ammo?
ammo power ups are 10% more likely to spawn from dead mobs
ammo power ups give the harpoon 2x more ammo
holding down fire lets the string extend farther, holding down fire lets the string extend farther,
this can overwrite crouch mode this can overwrite crouch mode
can't have 2+ harpoons can't have 2+ harpoons
@@ -32,15 +35,11 @@ harpoon tech
grappling hook? grappling hook?
remove string in all modes, why? remove string in all modes, why?
increase ammo increase ammo
post launch tracking: more airFriction, more thrust, harder turning
if no target found slow down and aim much better?
tracking so good harpoon can hit a target, circle around and hit it again tracking so good harpoon can hit a target, circle around and hit it again
doesn't seem to be good physics doesn't seem to be good physics
level:lab too much walking around and too much platforming level:lab too much walking around and too much platforming
set blockBoss frequency to 1x not 2x
tech - explode after getting hit, but while you are immune to harm tech - explode after getting hit, but while you are immune to harm
on mouse down wormhole shows a possible wormhole on mouse down wormhole shows a possible wormhole
@@ -475,6 +474,7 @@ possible names for tech
hypergraph hypergraph
gnarl gnarl
SQUID (for superconducting quantum interference device) is a very sensitive magnetometer used to measure extremely subtle magnetic fields, based on superconducting loops containing Josephson junctions. SQUID (for superconducting quantum interference device) is a very sensitive magnetometer used to measure extremely subtle magnetic fields, based on superconducting loops containing Josephson junctions.
nuclear pasta - hard matter in neutron star
a tutorial / lore intro a tutorial / lore intro
needs to be optional so it doesn't slow experienced players needs to be optional so it doesn't slow experienced players