diff --git a/img/collimator.webp b/img/collimator.webp
new file mode 100644
index 0000000..033cda4
Binary files /dev/null and b/img/collimator.webp differ
diff --git a/js/bullet.js b/js/bullet.js
index 9c00921..d152a93 100644
--- a/js/bullet.js
+++ b/js/bullet.js
@@ -7370,6 +7370,8 @@ const b = {
};
}
+ } else if (tech.beamCollimator) {
+ this.fire = this.fireSplitCollimator
} else if (tech.beamSplitter) {
this.fire = this.fireSplit
} else if (tech.historyLaser) {
@@ -7395,6 +7397,7 @@ const b = {
}, tech.laserDamage / b.fireCDscale * this.lensDamage);
}
},
+
firePulse() { },
fireSplit() {
const drain = tech.laserDrain / b.fireCDscale
@@ -7418,6 +7421,29 @@ const b = {
}
}
},
+ fireSplitCollimator() {
+ const drain = tech.laserDrain / b.fireCDscale
+ if (m.energy < drain) {
+ m.fireCDcycle = m.cycle + 100; // cool down if out of energy
+ } else {
+ m.fireCDcycle = m.cycle
+ m.energy -= drain
+ const freq = 0.037
+ const len = tech.beamSplitter + 1
+ const phase = 2 * Math.PI / len
+ for (let i = 0; i < len; i++) {
+ if (Math.sin(m.cycle * freq + phase * (i) + Math.PI / 2) > 0 || !(m.cycle % 3)) ctx.globalAlpha = 0.35
+
+ const whereSweep = m.angle + (m.crouch ? 0.4 : 1) * (Math.sin(m.cycle * freq + phase * (i)))
+ const where = { x: m.pos.x + 30 * Math.cos(whereSweep), y: m.pos.y + 30 * Math.sin(whereSweep) }
+ b.laser(where, {
+ x: where.x + 5000 * Math.cos(m.angle),
+ y: where.y + 5000 * Math.sin(m.angle)
+ }, tech.laserDamage / b.fireCDscale * this.lensDamage);
+ ctx.globalAlpha = 1
+ }
+ }
+ },
fireWideBeam() {
const drain = tech.laserDrain / b.fireCDscale
if (m.energy < drain) {
diff --git a/js/index.js b/js/index.js
index 768bd67..4bd63fc 100644
--- a/js/index.js
+++ b/js/index.js
@@ -1176,6 +1176,7 @@ function openExperimentMenu() {
document.body.style.overflowX = "hidden";
document.getElementById("info").style.display = 'none'
build.reset();
+
}
//record settings so they can be reproduced in the experimental menu
diff --git a/js/level.js b/js/level.js
index 7c70f51..37ac320 100644
--- a/js/level.js
+++ b/js/level.js
@@ -33,7 +33,7 @@ const level = {
// tech.tech[297].frequency = 100
// tech.addJunkTechToPool(0.5)
// m.couplingChange(10)
- // m.setField("wormhole") //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 10 grappling hook
+ // m.setField("pilot wave") //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 10 grappling hook
// m.energy = 0
// powerUps.research.count = 3
// tech.isHookWire = true
@@ -50,8 +50,8 @@ const level = {
// requestAnimationFrame(() => { tech.giveTech("non-renewables") });
// tech.giveTech("dark matter")
// tech.addJunkTechToPool(0.5)
- // for (let i = 0; i < 1; ++i) tech.giveTech("demineralization")
- // for (let i = 0; i < 1; ++i) tech.giveTech("remineralization")
+ // for (let i = 0; i < 1; ++i) tech.giveTech("paradigm shift")
+ // for (let i = 0; i < 1; ++i) tech.giveTech("Higgs mechanism")
// m.skin.egg();
// for (let i = 0; i < 1; ++i) tech.giveTech("many-worlds")
// requestAnimationFrame(() => { for (let i = 0; i < 1; i++) tech.giveTech("quasiparticles") });
@@ -67,7 +67,7 @@ const level = {
// for (let i = 0; i < 10; ++i) spawn.starter(1900, -500)
- // for (let i = 0; i < 1; i++) spawn.mantisBoss(1900, -500)
+ // for (let i = 0; i < 1; i++) spawn.softBoss(1900, -500)
// for (let i = 0; i < 1; ++i) powerUps.directSpawn(m.pos.x + 50 * Math.random(), m.pos.y + 50 * Math.random(), "entanglement");
// for (let i = 0; i < 2; ++i) powerUps.directSpawn(m.pos.x + 450, m.pos.y + 50 * Math.random(), "gun");
// for (let i = 0; i < 100; ++i) powerUps.directSpawn(m.pos.x + 50 * Math.random(), m.pos.y + 50 * Math.random(), "ammo");
@@ -175,7 +175,8 @@ const level = {
let rate = tech.interestRate
if (level.onLevel < level.levels.length - 1) {//make sure it's not on the lore level which has an undefined name
const levelName = level.levels[level.onLevel]
- if (levelName === "final" || levelName === "subway") rate *= 1 / 3
+ if (levelName === "final") rate *= 1 / 3
+ if (levelName === "subway") rate *= 1 / 5
}
let ammoSum = 0
@@ -2754,6 +2755,11 @@ const level = {
Composite.add(engine.world, cons[cons.length - 1]);
}
},
+ // softBody(x, y, angle = 0, isAttached = true, len = 15, radius = 20, stiffness = 1, damping = 1) {
+ // https://github.com/liabru/matter-js/blob/master/examples/softBody.js
+ // https://brm.io/matter-js/docs/classes/Composites.html
+ // https://codepen.io/Shokeen/pen/EmOLJO?editors=0010
+ // },
//******************************************************************************************************************
//******************************************************************************************************************
//******************************************************************************************************************
@@ -3534,7 +3540,7 @@ const level = {
const stationList = [] //use to randomize station order
for (let i = 1, totalNumberOfStations = 10; i < totalNumberOfStations; ++i) stationList.push(i) //!!!! update station number when you add a new station
stationList.sort(() => Math.random() - 0.5);
- stationList.splice(0, 3); //remove some stations to keep it to 4 stations
+ stationList.splice(0, simulation.difficultyMode > 4 ? 4 : 5); //remove some stations to keep it to 4 stations
stationList.unshift(0) //add index zero to the front of the array
let isExitOpen = false
diff --git a/js/lore.js b/js/lore.js
index 9278246..8c8b7c5 100644
--- a/js/lore.js
+++ b/js/lore.js
@@ -1088,7 +1088,7 @@ const lore = {
() => {
setTimeout(() => {
- lore.anand.text("How ever it thinks it can learn, and I think we showed it that nonviolence is an option,")
+ lore.anand.text("How ever it thinks, it can learn, and I think we showed it that violence isn't the only option,")
}, 1000);
},
() => {
diff --git a/js/player.js b/js/player.js
index 1484d82..d069673 100644
--- a/js/player.js
+++ b/js/player.js
@@ -3647,6 +3647,7 @@ const m = {
m.fieldUpgrades[index].effect();
simulation.inGameConsole(`
m.setField("${m.fieldUpgrades[m.fieldMode].name}")
input.key.field: ["MouseRight"]`);
if (m.fieldMode === 4) simulation.inGameConsole(`simulation.molecularMode = ${m.fieldUpgrades[4].modeText()} ↓↘→↓↙←↑↑↓`);
+ if (m.fieldMode === 8) simulation.inGameConsole(`Composite.add(engine.world, block) //↓↓→↘↓↙←↓↓`);
},
fieldEvent: null,
fieldUpgrades: [
@@ -4209,43 +4210,23 @@ const m = {
setDescription() {
return `use energy to deflect mobs
excess energy used to print ${simulation.molecularMode === 0 ? "spores" : simulation.molecularMode === 1 ? "missiles" : simulation.molecularMode === 2 ? "ice IX" : "drones"}
12 energy per second ↓↘→↓↙←↑↑↓`
},
- keyLog: [],
+ keyLog: [null, null, null, null, null, null, null],
effect: () => {
//store event function so it can be found and removed in m.setField()
m.fieldEvent = function (event) {
- m.fieldUpgrades[4].keyLog.push(event.code)
-
-
- // Helper function to compare arrays
- function arraysEqual(arr1, arr2) {
- if (arr1.length !== arr2.length) return false;
- for (let i = 0; i < arr1.length; i++) {
- if (arr1[i] !== arr2[i]) return false;
- }
- return true;
- }
-
- const pattern = [input.key.down, input.key.right, input.key.down, input.key.left, input.key.up, input.key.up, input.key.down]
- //check if the newest key press is correct
- if (event.code !== pattern[m.fieldUpgrades[4].keyLog.length - 1]) {
- m.fieldUpgrades[4].keyLog = [] //pattern is wrong, reset log
- } else if (arraysEqual(m.fieldUpgrades[4].keyLog, pattern)) { //pattern is complete
+ m.fieldUpgrades[4].keyLog.shift() //remove first element
+ m.fieldUpgrades[4].keyLog.push(event.code) //add new key to end
+ const patternA = ["ArrowDown", "ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "ArrowUp", "ArrowDown"]
+ const patternB = [input.key.down, input.key.right, input.key.down, input.key.left, input.key.up, input.key.up, input.key.down]
+ const arraysEqual = (a, b) => a.length === b.length && a.every((val, i) => val === b[i]);
+ if (arraysEqual(m.fieldUpgrades[4].keyLog, patternA) || arraysEqual(m.fieldUpgrades[4].keyLog, patternB)) {
//cycle to next molecular mode
- m.fieldUpgrades[4].keyLog = []
- const energy = m.energy //save current energy
- if (simulation.molecularMode < 3) {
- simulation.molecularMode++
- } else {
- simulation.molecularMode = 0
- }
- // m.setField((m.fieldMode === m.fieldUpgrades.length - 1) ? 1 : m.fieldMode + 1) //cycle to next field, skip field emitter
+ simulation.molecularMode = simulation.molecularMode < 3 ? simulation.molecularMode + 1 : 0
m.fieldUpgrades[4].description = m.fieldUpgrades[4].setDescription()
- m.energy = energy //return to current energy
-
const name = `${simulation.molecularMode === 0 ? "spores" : simulation.molecularMode === 1 ? "missiles" : simulation.molecularMode === 2 ? "ice IX" : "drones"}`
simulation.inGameConsole(`simulation.molecularMode = ${simulation.molecularMode} // ${name} ↓↘→↓↙←↑↑↓`);
}
- // console.log(m.fieldUpgrades[4].keyLog)
+ // console.log(event.code, m.fieldUpgrades[4].keyLog)
}
window.addEventListener("keydown", m.fieldEvent);
@@ -5019,7 +5000,7 @@ const m = {
},
{
name: "metamaterial cloaking",
- description: `0.3x damage taken while cloaked
after decloaking 4.5x damage for 2 s
6 energy per second`,
+ description: `0.4x damage taken while cloaked
after decloaking 4.5x damage for 2 s
6 energy per second`,
effect: () => {
m.fieldFire = true;
m.fieldMeterColor = "#333";
@@ -5070,7 +5051,7 @@ const m = {
if (!m.isCloak) { //&& m.energy > drain + 0.03
// m.energy -= drain
m.isCloak = true //enter cloak
- m.fieldHarmReduction = 0.3;
+ m.fieldHarmReduction = 0.4;
m.enterCloakCycle = m.cycle
if (tech.isCloakHealLastHit && m.lastHit > 0) {
const heal = Math.min(0.75 * m.lastHit, m.energy)
@@ -5151,8 +5132,8 @@ const m = {
if (inPlayer.length > 0) {
for (let i = 0; i < inPlayer.length; i++) {
if (m.energy > 0) {
- if (!inPlayer[i].isUnblockable) m.energy -= 0.003;
- if (inPlayer[i].shield) m.energy -= 0.011;
+ if (!inPlayer[i].isUnblockable) m.energy -= 0.004 + 0.0005 * simulation.difficultyMode;
+ if (inPlayer[i].shield) m.energy -= 0.015 + 0.001 * simulation.difficultyMode;
}
}
}
@@ -5180,8 +5161,65 @@ const m = {
},
{
name: "pilot wave",
- description: `use energy to guide blocks
, , and have +3 choices
10 energy per second`,
+ description: `use energy to guide blocks↓↓→↘↓↙←↓↓
, , and have +3 choices
10 energy per second`,
+ keyLog: [null, null, null, null, null, null, null],
effect: () => {
+ //store event function so it can be found and removed in m.setField()
+ m.fieldEvent = function (event) {
+ m.fieldUpgrades[4].keyLog.shift() //remove first element
+ m.fieldUpgrades[4].keyLog.push(event.code) //add new key to end
+ const patternA = ["ArrowDown", "ArrowDown", "ArrowRight", "ArrowDown", "ArrowLeft", "ArrowDown", "ArrowDown"]
+ const patternB = [input.key.down, input.key.down, input.key.right, input.key.down, input.key.left, input.key.down, input.key.down]
+ const arraysEqual = (a, b) => a.length === b.length && a.every((val, i) => val === b[i]);
+ const where = {
+ x: m.pos.x,
+ y: m.pos.y - 75
+ }
+ if (
+ (arraysEqual(m.fieldUpgrades[4].keyLog, patternA) || arraysEqual(m.fieldUpgrades[4].keyLog, patternB))
+ && !Matter.Query.point(map, where).length
+ ) {
+ //remove old blocks
+ // for (let i = 0; i < body.length; i++) {
+ // if (body[i].isPilotWave) {
+ // Matter.Composite.remove(engine.world, body[i]);
+ // body.splice(i, 1);
+ // break
+ // }
+ // }
+
+ //spawn a block
+ const radius = 25 + Math.floor(15 * Math.random())
+ // body[body.length] = Matter.Bodies.polygon(simulation.mouseInGame.x, simulation.mouseInGame.y, 4, radius, {
+ body[body.length] = Matter.Bodies.polygon(where.x, where.y, 4 + Math.floor(4 * Math.random()), radius, {
+ friction: 0.05,
+ frictionAir: 0.001,
+ collisionFilter: {
+ category: cat.body,
+ mask: cat.player | cat.map | cat.body | cat.bullet | cat.mob | cat.mobBullet
+ },
+ classType: "body",
+ isPilotWave: true,
+ });
+ const block = body[body.length - 1]
+ //mess with the block shape (this code is horrible)
+ Composite.add(engine.world, block); //add to world
+ const r1 = radius * (0.85 + 0.6 * Math.random())
+ const r2 = radius * (0.85 + 0.6 * Math.random())
+ let angle = Math.PI / 4
+ const vertices = []
+ for (let i = 0, len = block.vertices.length; i < len; i++) {
+ angle += 2 * Math.PI / len + 0.06 * Math.random()
+ vertices.push({ x: block.position.x + r1 * Math.cos(angle), y: block.position.y + r2 * Math.sin(angle) })
+ }
+ Matter.Body.setVertices(block, vertices)
+ /* ↓↘→↓↙←↑↑↓ */
+ simulation.inGameConsole(`Composite.add(engine.world, block) //↓↓→↘↓↙←↓↓`);
+ }
+ }
+ window.addEventListener("keydown", m.fieldEvent);
+
+
m.fieldMeterColor = "#333"
m.eyeFillColor = m.fieldMeterColor
diff --git a/js/tech.js b/js/tech.js
index 59d5ea4..3807f51 100644
--- a/js/tech.js
+++ b/js/tech.js
@@ -406,12 +406,13 @@ const tech = {
m.skin.strokeGap();
},
remove() {
- tech.isFireMoveLock = false
if (tech.isFireMoveLock) {
+ tech.isFireMoveLock = false
b.setFireCD();
b.setFireMethod();
- if (this.count) m.resetSkin();
+ m.resetSkin();
}
+ tech.isFireMoveLock = false
}
},
{
@@ -480,7 +481,10 @@ const tech = {
},
remove() {
tech.isDilate = false
- if (this.count) m.resetSkin();
+ if (this.count) {
+ m.resetSkin();
+ if (tech.isDiaphragm) m.skin.dilate2()
+ }
}
},
{
@@ -490,7 +494,7 @@ const tech = {
count: 0,
frequency: 2,
frequencyDefault: 2,
- isSkin: true,
+ // isSkin: true,
allowed() {
return tech.isDilate
},
@@ -502,7 +506,10 @@ const tech = {
},
remove() {
tech.isDiaphragm = false
- if (this.count) m.resetSkin();
+ if (this.count) {
+ m.resetSkin();
+ if (tech.isDilate) m.skin.dilate()
+ }
}
},
{
@@ -1063,7 +1070,7 @@ const tech = {
{
name: "cache",
link: `cache`,
- description: `17x ammo per ${powerUps.orb.ammo()}, but
you can't store additional ammo`,
+ description: `15x ammo per ${powerUps.orb.ammo()}, but
you can't store additional ammo`,
maxCount: 1,
count: 0,
frequency: 1,
@@ -1073,7 +1080,7 @@ const tech = {
},
requires: "not non-renewables",
effect() {
- tech.ammoCap = 17;
+ tech.ammoCap = 15;
powerUps.ammo.effect()
},
remove() {
@@ -7372,7 +7379,7 @@ const tech = {
requestAnimationFrame(() => {
let techGiven = 0
for (let j = 0; j < 3; j++) {
- const names = ["quasiparticles", "lens", "compound lens", "arc length", "infrared diode", "free-electron laser", "dye laser", "relativistic momentum", "specular reflection", "diffraction grating", "diffuse beam", "output coupler", "slow light", "laser-bot", "laser-bot upgrade"]
+ const names = ["quasiparticles", "lens", "compound lens", "arc length", "infrared diode", "free-electron laser", "dye laser", "relativistic momentum", "specular reflection", "diffraction grating", "diffuse beam", "output coupler", "slow light", "laser-bot", "laser-bot upgrade", "collimator"]
//convert names into indexes
const options = []
for (let i = 0; i < names.length; i++) {
@@ -7514,7 +7521,7 @@ const tech = {
allowed() {
return (tech.haveGunCheck("laser") || tech.isLaserMine || tech.isLaserBotUpgrade || tech.isLaserField) && !tech.isWideLaser && !tech.isPulseLaser && !tech.historyLaser
},
- requires: "laser, not diffuse beam, pulse, or slow light",
+ requires: "laser, not diffuse beam, pulse, slow light",
effect() {
tech.laserReflections += 2;
},
@@ -7533,7 +7540,7 @@ const tech = {
allowed() {
return tech.haveGunCheck("laser") && !tech.isWideLaser && !tech.historyLaser
},
- requires: "laser gun, diffuse beam, or slow light",
+ requires: "laser gun, not diffuse beam, slow light",
effect() {
tech.beamSplitter++
b.guns[11].chooseFireMethod()
@@ -7545,6 +7552,29 @@ const tech = {
}
}
},
+ {
+ name: "collimator",
+ description: `+1 laser beam
align your diverging laser beams to be parallel`,
+ isGunTech: true,
+ maxCount: 1,
+ count: 0,
+ frequency: 1,
+ frequencyDefault: 1,
+ allowed() {
+ return tech.haveGunCheck("laser") && !tech.isWideLaser && !tech.historyLaser && tech.beamSplitter > 0 && !tech.isPulseLaser
+ },
+ requires: "laser gun, diffraction, not diffuse beam, slow light, pulse",
+ effect() {
+ tech.beamSplitter++
+ tech.beamCollimator = true
+ b.guns[11].chooseFireMethod()
+ },
+ remove() {
+ tech.beamCollimator = false
+ if (tech.beamSplitter > 0) tech.beamSplitter--
+ b.guns[11].chooseFireMethod()
+ }
+ },
{
name: "diffuse beam",
link: `diffuse beam`,
@@ -7607,7 +7637,7 @@ const tech = {
allowed() {
return tech.haveGunCheck("laser") && !tech.beamSplitter && !tech.isWideLaser
},
- requires: "laser gun, diffraction grating, diffuse beam",
+ requires: "laser gun, not diffraction grating, diffuse beam",
effect() {
tech.historyLaser++
b.guns[11].chooseFireMethod()
@@ -7657,8 +7687,8 @@ const tech = {
effect() {
tech.laserDrain *= 0.75
tech.laserDamage *= 1.25
- tech.laserColor = "rgb(0, 11, 255)"
- tech.laserColorAlpha = "rgba(0, 11, 255,0.5)"
+ tech.laserColor = "rgb(0, 40, 255)"
+ tech.laserColorAlpha = "rgba(0, 40, 255,0.5)"
},
remove() {
tech.laserDrain = 0.003;
@@ -7701,9 +7731,9 @@ const tech = {
frequency: 2,
frequencyDefault: 2,
allowed() {
- return tech.haveGunCheck("laser") && tech.laserReflections < 3 && !tech.isWideLaser && tech.laserDrain === 0.003 && !tech.isStuckOn
+ return tech.haveGunCheck("laser") && tech.laserReflections < 3 && !tech.isWideLaser && tech.laserDrain === 0.003 && !tech.isStuckOn && !tech.beamCollimator
},
- requires: "laser gun, not specular reflection, diffuse, free-electron laser, optical amplifier",
+ requires: "laser gun, not specular reflection, diffuse, free-electron laser, optical amplifier, collimator",
effect() {
tech.isPulseLaser = true;
b.guns[11].chooseFireMethod()
@@ -12319,4 +12349,5 @@ const tech = {
isDemineralize: null,
mineralDamage: null,
negativeMassCost: null,
+ beamCollimator: null,
}
\ No newline at end of file
diff --git a/style.css b/style.css
index b72325c..ba7656e 100644
--- a/style.css
+++ b/style.css
@@ -9,6 +9,7 @@ body {
margin: 0;
overflow: hidden;
cursor: auto;
+ background-color: #f00;
/* filter: grayscale(1); */
/* transition: background-color 0.2s ease-in-out; */
}
diff --git a/todo.txt b/todo.txt
index 4dc0196..4c467bc 100644
--- a/todo.txt
+++ b/todo.txt
@@ -1,17 +1,22 @@
******************************************************** NEXT PATCH **************************************************
-mantisBoss flashes for a second before it drops invulnerability
-removed parasitism - it's too similar to invulnerability tech
-invariant no longer drains energy while wormhole time is paused
- added 1 research cost
-added secret combo to change molecular assembler mode
+tech: collimator - add 1 laser beam and align your diverging beams to be parallel
+ requires diffraction grating
+
+added secret pilot wave combo to make blocks
+rewrote combo test algorithm to be more forgiving with pattern matching
+ also extended combos test to arrow keys, not just WASD
+
+cache 17->15x ammo
+metamaterial cloaking 0.3->0.4x damage reduction while cloaked
+boson composite drains more energy when passing through mobs
+ scales with difficulty
+subway level has 6->4 (5 on hard difficulty) stations
+subway gives 1/3->1/5 interest per station
bug fixes
- issue with constraint: "mob death heals mobs"
- mob health was becoming NaN, this was infecting other values like player energy
- entering a seed in settings wasn't giving the same results as a randomly generated seeds
- also removed some random code that was using seeded shuffle, but didn't need to
- converted it to non seeded random shuffle with .sort(() => Math.random() - 0.5);
+ Higgs skin removal fixed
+ diaphragm skin removal fixed
******************************************************** BUGS ********************************************************
@@ -19,9 +24,10 @@ player can become crouched while not touching the ground if they exit the ground
*********************************************************** TODO *****************************************************
+soft body boss?
+ search softBody(x, y, angle = 0, isAttached = true, len = 15, radius = 20, stiffness = 1, damping = 1) {
use ←↑→↓↖↗↘↙ combos to allow fields to have special actions
- !!should this be wasd, arrows, or both?
how to limit spam?
on cooldown
timer or once per level
@@ -40,8 +46,7 @@ use ←↑→↓↖↗↘↙ combos to allow fields to have special actions
plasma torch
time dilation
metamaterial cloaking
- pilot wave
- spawn blocks
+ pilot wave - done
wormhole
shoot out all the blocks that were sucked in this level (maybe cap at like 10?, cap with energy spent to fire)
are block sizes stored properly? because they shrink before they get eaten...
@@ -52,6 +57,12 @@ use ←↑→↓↖↗↘↙ combos to allow fields to have special actions
fire from player (and draw a wormhole looking graphic)
grappling hook
+new level idea: large map sized blocks that can't be destroyed that the player walks on as a part of the level
+ eventually the blocks fall
+ after fall level progresses to a phase 2 to clean up the blocks or leave them
+ should bosses be killed by falling blocks??
+ how to avoid the large block vibrating/dancing on tiny block issue
+
new level idea: escort mission
player has to stay near something that moves slowly through the level
maybe only a zone around the escort is safe