diff --git a/.DS_Store b/.DS_Store index 711383d..7171361 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/js/bullet.js b/js/bullet.js index 4c459ce..f004239 100644 --- a/js/bullet.js +++ b/js/bullet.js @@ -726,7 +726,7 @@ const b = { bullet[me].explodeRad = 300 * size; bullet[me].onEnd = function() { b.explosion(this.position, this.explodeRad); //makes bullet do explosive damage at end - if (tech.fragments) b.targetedNail(this.position, tech.fragments * 4) + if (tech.fragments) b.targetedNail(this.position, tech.fragments * Math.floor(2 + 2 * Math.random())) } bullet[me].minDmgSpeed = 1; bullet[me].beforeDmg = function() { @@ -751,7 +751,7 @@ const b = { bullet[me].explodeRad = 305 * size; bullet[me].onEnd = function() { b.explosion(this.position, this.explodeRad); //makes bullet do explosive damage at end - if (tech.fragments) b.targetedNail(this.position, tech.fragments * 4) + if (tech.fragments) b.targetedNail(this.position, tech.fragments * Math.floor(2 + 2 * Math.random())) } bullet[me].minDmgSpeed = 1; bullet[me].beforeDmg = function() { @@ -786,7 +786,7 @@ const b = { bullet[me].explodeRad = 350 * size + Math.floor(Math.random() * 50) + tech.isBlockExplode * 110 bullet[me].onEnd = function() { b.explosion(this.position, this.explodeRad); //makes bullet do explosive damage at end - if (tech.fragments) b.targetedNail(this.position, tech.fragments * 4) + if (tech.fragments) b.targetedNail(this.position, tech.fragments * Math.floor(2 + 2 * Math.random())) } bullet[me].minDmgSpeed = 1; bullet[me].beforeDmg = function() { @@ -862,7 +862,7 @@ const b = { bullet[me].explodeRad = 350 * size + Math.floor(Math.random() * 50) + tech.isBlockExplode * 100 bullet[me].onEnd = function() { b.explosion(this.position, this.explodeRad); //makes bullet do explosive damage at end - if (tech.fragments) b.targetedNail(this.position, tech.fragments * 6) + if (tech.fragments) b.targetedNail(this.position, tech.fragments * 5) } bullet[me].beforeDmg = function() { this.endCycle = 0; //bullet ends cycle after doing damage //this also triggers explosion @@ -1108,14 +1108,13 @@ const b = { b.grenade = grenadeDefault } }, - harpoon(where, target, scale = 1, isReturn = false, ropeLength = 15, speed = 0) { + harpoon(where, target, angle = m.angle, scale = 1, isReturn = false, ropeLength = 15) { const me = bullet.length; - let vector = "-40 2 -40 -2 30 -2 50 0 30 2" - if (scale !== 1) vector = `${-40*scale} 2 ${-40*scale} -2 ${30*scale} -2 ${50*scale} 0 ${30*scale} 2` - bullet[me] = Bodies.fromVertices(where.x, where.y, Vertices.fromPath(vector), { - // bullet[me] = Bodies.rectangle(where.x, where.y, 70 * size, 4.5 * size, { + // let vector = "-40 2 -40 -2 50 -3 30 2" //"-40 2 -40 -2 30 -2 50 0 30 2" + // if (scale !== 1) vector = `${-40*scale} 2 ${-40*scale} -2 ${50*scale} -3 ${30*scale} 2` //`${-40*scale} 2 ${-40*scale} -2 ${30*scale} -2 ${50*scale} 0 ${30*scale} 2` + bullet[me] = Bodies.fromVertices(where.x, where.y, [{ x: -40 * scale, y: 2, index: 0, isInternal: false }, { x: -40 * scale, y: -2, index: 1, isInternal: false }, { x: 50 * scale, y: -3, index: 3, isInternal: false }, { x: 30 * scale, y: 2, index: 4, isInternal: false }], { cycle: 0, - angle: m.angle, + angle: angle, friction: 1, frictionAir: 0.4, thrustMag: 0.1, @@ -1124,18 +1123,23 @@ const b = { drawStringFlip: (Math.round(Math.random()) ? 1 : -1), dmg: 0, //damage done in addition to the damage from momentum classType: "bullet", - endCycle: simulation.cycle + 50, + endCycle: simulation.cycle + ropeLength * 2.5, collisionFilter: { category: cat.bullet, - mask: cat.map | cat.body | cat.mob | cat.mobBullet | cat.mobShield + mask: tech.isNeedleShieldPierce ? cat.map | cat.body | cat.mob | cat.mobBullet : cat.map | cat.body | cat.mob | cat.mobBullet | cat.mobShield, }, minDmgSpeed: 0, lookFrequency: Math.floor(7 + Math.random() * 3), density: 0.005, //0.001 is normal beforeDmg(who) { + // console.log(Vector.magnitude(Vector.sub(this.position, m.pos))) + if (tech.isNeedleShieldPierce && who.isShielded) { //disable shields + who.isShielded = false + requestAnimationFrame(() => { who.isShielded = true }); + } if (!who.isBadTarget) { if (tech.fragments) { - b.targetedNail(this.position, tech.fragments * 5) + b.targetedNail(this.vertices[2], tech.fragments * 4) } else if (isReturn) { this.do = this.returnToPlayer } else { @@ -1164,8 +1168,6 @@ const b = { ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, this.vertices[0].x, this.vertices[0].y) // ctx.lineTo(this.vertices[0].x, this.vertices[0].y); ctx.stroke(); - - if (m.energy > 0.003) m.energy -= 0.003 } }, returnToPlayer() { @@ -1187,20 +1189,24 @@ const b = { // 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: 0 }) + Matter.Body.setVelocity(powerUp[i], { x: 0, y: -2 }) // Matter.Body.setPosition(powerUp[i], this.position) break } } } else { + let isPulling = false for (let i = 0, len = powerUp.length; i < len; ++i) { //near power up - if (Vector.magnitudeSquared(Vector.sub(this.position, powerUp[i].position)) < 3000) { + 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.position) + 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 } } - const returnForce = Vector.mult(Vector.normalise(Vector.sub(this.position, m.pos)), this.thrustMag * this.mass) + if (m.energy > 0.005) m.energy -= 0.005 + const returnForce = Vector.mult(Vector.normalise(Vector.sub(this.position, m.pos)), this.thrustMag * this.mass * (isPulling ? 0.5 : 1)) this.force.x -= returnForce.x this.force.y -= returnForce.y this.drawString() @@ -1209,29 +1215,26 @@ const b = { do() { if (!m.isBodiesAsleep) { this.cycle++ - if (isReturn && this.cycle > ropeLength) { - if (m.energy < 0.05) { //snap rope if not enough energy - const returnForce = Vector.mult(Vector.normalise(Vector.sub(this.position, m.pos)), 3 * this.thrustMag * this.mass) - this.force.x -= returnForce.x - this.force.y -= returnForce.y - this.frictionAir = 0.003 - this.do = () => { - this.force.y += this.mass * 0.003; //gravity + if (isReturn) { + if (this.cycle > ropeLength) { + if (m.energy < 0.05) { //snap rope if not enough energy + const returnForce = Vector.mult(Vector.normalise(Vector.sub(this.position, m.pos)), 3 * this.thrustMag * this.mass) + this.force.x -= returnForce.x + this.force.y -= returnForce.y + this.frictionAir = 0.002 + this.do = () => { this.force.y += this.mass * 0.001; } + } else { //return to player + 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) + this.collisionFilter.mask = cat.map | cat.mob | cat.mobBullet | cat.mobShield // | cat.body } - } else { //return to player - 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) - this.collisionFilter.mask = cat.map | cat.mob | cat.mobBullet | cat.mobShield // | cat.body } - } - if (this.cycle > 30) { + } else if (this.cycle > 30) { this.frictionAir = 0.003 - this.do = () => { - this.force.y += this.mass * 0.003; //gravity - } + this.do = () => { this.force.y += this.mass * 0.003; } } - if (target) { //rotate missile towards the target + if (target) { //rotate towards the target const face = { x: Math.cos(this.angle), y: Math.sin(this.angle) @@ -1243,6 +1246,10 @@ const b = { Matter.Body.rotate(this, -this.turnRate); } } + if (isReturn || target) { + this.force.x += this.thrustMag * this.mass * Math.cos(this.angle); + this.force.y += this.thrustMag * this.mass * Math.sin(this.angle); + } // else if (!(this.cycle % 2)) { //look for a target if you don't have one // simulation.drawList.push({ //add dmg to draw queue // x: this.position.x, @@ -1275,16 +1282,20 @@ const b = { // this.frictionAir = 0.8 // } // } - this.force.x += this.thrustMag * this.mass * Math.cos(this.angle); - this.force.y += this.thrustMag * this.mass * Math.sin(this.angle); } this.drawString() }, }); - Matter.Body.setVelocity(bullet[me], { - x: m.Vx / 2 + speed * Math.cos(bullet[me].angle), - y: m.Vy / 2 + speed * Math.sin(bullet[me].angle) - }); + if (!isReturn && !target) { + Matter.Body.setVelocity(bullet[me], { + x: m.Vx / 2 + 60 * Math.cos(bullet[me].angle), + y: m.Vy / 2 + 60 * Math.sin(bullet[me].angle) + }); + bullet[me].frictionAir = 0.002 + bullet[me].do = function() { + this.force.y += this.mass * 0.001; //gravity + } + } Composite.add(engine.world, bullet[me]); //add bullet to world }, missile(where, angle, speed, size = 1) { @@ -1312,7 +1323,7 @@ const b = { }, onEnd() { b.explosion(this.position, this.explodeRad * size); //makes bullet do explosive damage at end - if (tech.fragments) b.targetedNail(this.position, tech.fragments * 4) + if (tech.fragments) b.targetedNail(this.position, tech.fragments * Math.floor(2 + 2 * Math.random())) }, lockedOn: null, tryToLockOn() { @@ -4057,7 +4068,7 @@ const b = { if (tech.fragments) { bullet[me].beforeDmg = function() { if (this.speed > 4) { - b.targetedNail(this.position, tech.fragments * 8) + b.targetedNail(this.position, tech.fragments * 7) this.endCycle = 0 //triggers despawn } } @@ -5007,7 +5018,6 @@ const b = { ammo: 0, ammoPack: 1, have: false, - fireCycle: 0, do() {}, fire() { const where = { @@ -5019,7 +5029,9 @@ const b = { target: null } //look for closest mob in player's LoS - const dir = { x: Math.cos(m.angle), y: Math.sin(m.angle) }; //make a vector for the player's direction of length 1 + const dir = { x: Math.cos(m.angle), y: Math.sin(m.angle) }; //make a vector for the player's direction of length 1; used in dot product + const length = tech.isLargeHarpoon ? 1 + 0.15 * Math.sqrt(this.ammo) : 1 + const totalCycles = 8 * (tech.isFilament ? 1 + Math.min(75, this.ammo) / 33 : 1) if (m.crouch) { for (let i = 0, len = mob.length; i < len; ++i) { if (mob[i].alive && !mob[i].isBadTarget && Matter.Query.ray(map, m.pos, mob[i].position).length === 0) { @@ -5031,8 +5043,32 @@ const b = { } } } - b.harpoon(where, closest.target, 1 + tech.isLargeHarpoon * this.ammo / 100, false) + b.harpoon(where, closest.target, m.angle, length, false) m.fireCDcycle = m.cycle + 50 * b.fireCDscale; // cool down + } else if (tech.extraHarpoons) { + const range = 560 * (tech.isFilament ? 1 + this.ammo / 33 : 1) + let targetCount = 0 + for (let i = 0, len = mob.length; i < len; ++i) { + if (mob[i].alive && !mob[i].isBadTarget && Matter.Query.ray(map, m.pos, mob[i].position).length === 0) { + const dot = Vector.dot(dir, Vector.normalise(Vector.sub(mob[i].position, m.pos))) //the dot product of diff and dir will return how much over lap between the vectors + const dist = Vector.magnitude(Vector.sub(where, mob[i].position)) + if (dist < range && dot > 0.9) { //target closest mob that player is looking at and isn't too close to target + if (this.ammo > 0) { + this.ammo-- + b.harpoon(where, mob[i], Vector.angle(Vector.sub(where, mob[i].position), { x: 0, y: 0 }), length, true, totalCycles) + targetCount++ + if (targetCount > tech.extraHarpoons) break + } + } + } + } + if (!targetCount) { + b.harpoon(where, null, m.angle, length, true, totalCycles) //if no target + } else if (targetCount > 0) { + this.ammo++ //make up for the ammo used up in fire() + simulation.updateGunHUD(); + } + m.fireCDcycle = m.cycle + 180 //Infinity; // cool down } else { for (let i = 0, len = mob.length; i < len; ++i) { if (mob[i].alive && !mob[i].isBadTarget && Matter.Query.ray(map, m.pos, mob[i].position).length === 0) { @@ -5044,7 +5080,7 @@ const b = { } } } - b.harpoon(where, closest.target, 1 + tech.isLargeHarpoon * this.ammo / 100, true, (m.crouch ? 10 : 8) * (tech.isFilament ? 1 + this.ammo / 100 : 1)) + b.harpoon(where, closest.target, m.angle, length, true, totalCycles) m.fireCDcycle = m.cycle + 180 //Infinity; // cool down } const recoil = Vector.mult(Vector.normalise(Vector.sub(where, m.pos)), m.crouch ? 0.015 : 0.035) @@ -5139,7 +5175,7 @@ const b = { // Matter.Body.setDensity(this, 0.001); } if (tech.fragments && this.speed > 10) { - b.targetedNail(this.position, tech.fragments * 15) + b.targetedNail(this.position, tech.fragments * 13) this.endCycle = 0 //triggers despawn } }, @@ -5223,7 +5259,7 @@ const b = { }); } if (tech.fragments && this.speed > 10) { - b.targetedNail(this.position, tech.fragments * 20) + b.targetedNail(this.position, tech.fragments * 17) this.endCycle = 0 //triggers despawn } }, diff --git a/js/engine.js b/js/engine.js index 9cb3e8d..b5ee26b 100644 --- a/js/engine.js +++ b/js/engine.js @@ -194,6 +194,7 @@ function collisionChecks(event) { if (v > 9) { let dmg = 0.075 * b.dmgScale * v * obj.mass * (tech.throwChargeRate) * (tech.isBlockHarm ? 2.5 : 1) * (tech.isMobBlockFling ? 2.5 : 1) * (tech.isBlockRestitution ? 2.5 : 1); if (mob[k].isShielded) dmg *= 0.7 + // console.log(dmg) mob[k].damage(dmg, true); if (tech.isBlockPowerUps && !mob[k].alive && mob[k].isDropPowerUp && m.throwCycle > m.cycle) { let type = tech.isEnergyNoAmmo ? "heal" : "ammo" diff --git a/js/index.js b/js/index.js index 9941215..6fd93dd 100644 --- a/js/index.js +++ b/js/index.js @@ -360,31 +360,31 @@ const build = { const isCount = tech.tech[i].count > 1 ? `(${tech.tech[i].count}x)` : ""; //
- // if (tech.tech[i].isFieldTech) { - // techID.classList.remove('experiment-grid-hide'); + if (tech.tech[i].isFieldTech) { + techID.classList.remove('experiment-grid-hide'); - // techID.innerHTML = ` - //
- // - //
- //
- //
- //         ${tech.tech[i].name} ${isCount}
${tech.tech[i].description} - // ` - // //
- // //
- // // border: #fff solid 0px; - // } else if (tech.tech[i].isGunTech) { - // techID.classList.remove('experiment-grid-hide'); - // techID.innerHTML = ` - //
- // - //
- //
- //
- //         ${tech.tech[i].name} ${isCount}
${tech.tech[i].description} - // ` - // } else + techID.innerHTML = ` +
+ +
+
+
+         ${tech.tech[i].name} ${isCount}
${tech.tech[i].description} + ` + //
+ //
+ // border: #fff solid 0px; + } else if (tech.tech[i].isGunTech) { + techID.classList.remove('experiment-grid-hide'); + techID.innerHTML = ` +
+ +
+
+
+         ${tech.tech[i].name} ${isCount}
${tech.tech[i].description} + ` + } else if (tech.tech[i].isJunk) { // text += `
  ${tech.tech[i].name} ${isCount}
${tech.tech[i].description}
` techID.innerHTML = `
  ${tech.tech[i].name} ${isCount}
${tech.tech[i].description}` diff --git a/js/level.js b/js/level.js index cf02a55..66b6bdf 100644 --- a/js/level.js +++ b/js/level.js @@ -17,7 +17,8 @@ const level = { // simulation.isHorizontalFlipped = true // m.setField("time dilation") // b.giveGuns("harpoon") - // tech.giveTech("nematodes") + // tech.giveTech("filament") + // tech.giveTech("reticulum") // tech.giveTech("necrophage") // for (let i = 0; i < 3; i++) tech.giveTech("super sized") // for (let i = 0; i < 9; i++) tech.giveTech("MIRV") @@ -2280,7 +2281,7 @@ const level = { // spawn.snakeSuckBoss(1900, -500) // spawn.grenadier(1900, -500) - // spawn.sneaker(1900, -500) + // spawn.sneaker(1900, -500, 200) // spawn.shield(mob[mob.length - 1], 1900, -500, 1); // mob[mob.length - 1].isShielded = true // spawn.historyBoss(1200, -500) @@ -2293,11 +2294,11 @@ const level = { // spawn.powerUpBoss(1600, -500) // spawn.cellBossCulture(1600, -500) // spawn.laserTargetingBoss(1600, -500) - // spawn.striker(1200, -500) + // spawn.laser(1200, -500) spawn.nodeGroup(1200, -500, "grenadier") - // spawn.nodeGroup(1800, -500, "grenadier") - // spawn.nodeGroup(1200, 0, "grenadier") + spawn.nodeGroup(1800, -500, "grenadier") + spawn.nodeGroup(1200, 0, "grenadier") // spawn.snakeBoss(1200, -500) diff --git a/js/mob.js b/js/mob.js index 843a8a8..0c00e86 100644 --- a/js/mob.js +++ b/js/mob.js @@ -530,7 +530,7 @@ const mobs = { }; vertexCollision(this.position, look, map); vertexCollision(this.position, look, body); - if (!m.isCloak) vertexCollision(this.position, look, [player]); + if (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]); // hitting player if (best.who === player) { if (m.immuneCycle < m.cycle) { diff --git a/js/spawn.js b/js/spawn.js index 59e8f9c..ffe9f41 100644 --- a/js/spawn.js +++ b/js/spawn.js @@ -2125,7 +2125,8 @@ const spawn = { }; vertexCollision(this.position, look, map); vertexCollision(this.position, look, body); - if (!m.isCloak) vertexCollision(this.position, look, [player]); + if (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]); + // hitting player if (best.who === player) { if (m.immuneCycle < m.cycle) { @@ -2254,8 +2255,8 @@ const spawn = { y: this.position.y + seeRange * Math.sin(this.angle) }; vertexCollision(this.position, look, map); - // vertexCollision(this.position, look, body); - if (!m.isCloak) vertexCollision(this.position, look, [player]); + if (!m.isCloak) vertexCollision(this.position, look, [playerBody, playerHead]); + // hitting player if (best.who === player) { this.targetingCount++ diff --git a/js/tech.js b/js/tech.js index b48895f..c47c97e 100644 --- a/js/tech.js +++ b/js/tech.js @@ -3645,17 +3645,17 @@ } }, { - name: "ceramic needles", - description: `needles pierce shields
directly damaging shielded mobs`, + name: "ceramics", + description: `needles and harpoons pierce shields
directly damaging shielded mobs`, isGunTech: true, maxCount: 1, count: 0, frequency: 2, frequencyDefault: 2, allowed() { - return (tech.isNeedles || tech.isNeedleShot) + return tech.haveGunCheck("harpoon") || (tech.isNeedles || tech.isNeedleShot) }, - requires: "needle gun, needle-shot", + requires: "needle gun, needle-shot, harpoon", effect() { tech.isNeedleShieldPierce = true }, @@ -4977,7 +4977,7 @@ }, { name: "filament", - description: "increase the length of your harpoon's rope
by 1% per harpoon ammo", + description: "increase the length of your harpoon's rope
by 3% per harpoon ammo", isGunTech: true, maxCount: 1, count: 0, @@ -4996,7 +4996,7 @@ }, { name: "unaaq", - description: "increase the length of your harpoon
by 1% per harpoon ammo", + description: "increase the length of your harpoon
by 15% of the square root of its ammo", isGunTech: true, maxCount: 1, count: 0, @@ -5013,6 +5013,25 @@ tech.isLargeHarpoon = false; } }, + { + name: "reticulum", + description: "fire +1 harpoon
when there are multiple targets in range", + isGunTech: true, + maxCount: 9, + count: 0, + frequency: 2, + frequencyDefault: 2, + allowed() { + return tech.haveGunCheck("harpoon") + }, + requires: "harpoon", + effect() { + tech.extraHarpoons++; + }, + remove() { + tech.extraHarpoons = 0; + } + }, // { // name: "spear", // description: "harpoons fired while crouched
have no rope and improved steering", @@ -8100,5 +8119,6 @@ isSmartRadius: null, isFilament: null, // isSpear: null, - isLargeHarpoon: null + isLargeHarpoon: null, + extraHarpoons: null } \ No newline at end of file diff --git a/todo.txt b/todo.txt index 9c5d577..7be8575 100644 --- a/todo.txt +++ b/todo.txt @@ -1,36 +1,48 @@ ******************************************************** NEXT PATCH ************************************************** -new gun harpoon -tech: filament - harpoon rope gets 1% longer for every harpoon ammo in your inventory -tech: unaaq - harpoon gets 1% longer for every harpoon ammo in your inventory - harpoon is pretty unbalanced, but I'm pushing it out so ya'll can let me know how I should balance it - more harpoon tech to come - 2+ harpoons out at once - hold fire to extend rope longer - improved targeting: so it can hit the same target a few times +harpoon gun + shaped more like a harpoon + grabs power ups slower, and from the tip of the harpoon + only uses energy on returning + doesn't have thrust if there is no mob target -tech: controlled explosion - explosions shrink to prevent them from hitting you, cost 3 research + tech: reticulum - make one more harpoon if there are mobs nearby that you are facing + I'm letting this stack to 9 even though it's silly cause that is n-gon's style + tech: ceramics now lets both harpoons and needles ignore shields + this was coded poorly so if shield bugs show up it might be from this + tech: fragmentation makes ~15% fewer nails for everything it affects + tech: filament gives 3% length per ammo (was 1%) + also length is capped at 75 ammo because after that it's just annoying + tech: unaaq is longer at low ammo but scales slower at high ammo + +bugs + fixed laser collisions on player head when crouching + returned experiment gun and field circles -JUNK tech: true colors - set all power ups to their real world colors (just makes random colors) ******************************************************** TODO ******************************************************** + "Interstellar Disturbance": Cosmic String applies to mobs who cross the wormhole's path, even after initial wormholing, but at reduced damage and stun time. disable zoom progress when paused gun: harpoon - if no target no thrust and no airfriction - stuck in walls, and don't return ammo return to player is slower for heavier harpoons + harpoons despawn before they have time to return with filament and lots o ammo harpoon tech holding down fire lets the string extend farther, + this can overwrite crouch mode can't have 2+ harpoons it uses up ammo as extends, and returns it as it contracts? will this effect performance? fire 2+ harpoons at the same time but different angles 2+ harpoons, with separate CDs can't have extended string? + grappling hook? + + remove string in all modes, why? + 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