diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
index 101c3445f..6e9f7df92 100644
--- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
@@ -2,6 +2,8 @@
package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
+import net.psforever.objects.definition.ProjectileDefinition
+import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.zones.Zoning
import net.psforever.objects.serverobject.turret.{AutomatedTurret, AutomatedTurretBehavior, VanuSentry}
import net.psforever.objects.zones.exp.ToDatabase
@@ -443,21 +445,19 @@ private[support] class WeaponAndProjectileOperations(
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
None
- case turret: AutomatedTurret with OwnableByPlayer =>
+ case turret: AutomatedTurret with OwnableByPlayer => //most likely a deployable
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
val owner = continent.GUID(turret.OwnerGuid)
.collect { case obj: PlanetSideGameObject with FactionAffinity => SourceEntry(obj) }
- .getOrElse { SourceEntry.None }
- turret.Weapons
- .values
- .flatMap { _.Equipment }
- .collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
- .find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
+ .getOrElse(SourceEntry(turret))
+ CompileAutomatedTurretDamageData(turret, owner, projectileTypeId)
+ case turret: Amenity with AutomatedTurret => //most likely a facility turret
+ turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
+ CompileAutomatedTurretDamageData(turret, SourceEntry(turret.Owner), projectileTypeId)
}
.collect {
case Some((obj, tool, owner, projectileInfo)) =>
-
val angle = Vector3.Unit(target.Position - obj.Position)
val proj = new Projectile(
projectileInfo,
@@ -1458,6 +1458,18 @@ private[support] class WeaponAndProjectileOperations(
ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0))
}
+ private def CompileAutomatedTurretDamageData(
+ turret: AutomatedTurret,
+ owner: SourceEntry,
+ projectileTypeId: Long
+ ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = {
+ turret.Weapons
+ .values
+ .flatMap { _.Equipment }
+ .collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
+ .find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
+ }
+
override protected[session] def stop(): Unit = {
if (player != null && player.HasGUID) {
(prefire ++ shooting).foreach { guid =>
diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
index 398d2fcb0..745a40bc3 100644
--- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
+++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
@@ -9074,8 +9074,10 @@ object GlobalDefinitions {
spitfire_turret.Model = ComplexDeployableResolutions.calculate
spitfire_turret.deployAnimation = DeployAnimation.Standard
spitfire_turret.AutoFire = Automation(
- targetingRange = 40f,
- targetValidation = List(EffectTarget.Validation.ObviousPlayer, EffectTarget.Validation.Vehicle),
+ targetDetectionRange = 60f,
+ targetTriggerRange = 50f,
+ targetEscapeRange = 50f,
+ targetValidation = List(EffectTarget.Validation.PlayerOnRadar, EffectTarget.Validation.Vehicle),
retaliatoryDuration = 8000L
)
spitfire_turret.innateDamage = new DamageWithPosition {
@@ -9105,8 +9107,10 @@ object GlobalDefinitions {
spitfire_cloaked.deployAnimation = DeployAnimation.Standard
spitfire_cloaked.Model = ComplexDeployableResolutions.calculate
spitfire_cloaked.AutoFire = Automation(
- targetingRange = 40f,
- targetValidation = List(EffectTarget.Validation.ObviousPlayer, EffectTarget.Validation.Vehicle),
+ targetDetectionRange = 50f,
+ targetTriggerRange = 30f,
+ targetEscapeRange = 50f,
+ targetValidation = List(EffectTarget.Validation.PlayerOnRadar, EffectTarget.Validation.VehiclesOnRadar),
retaliatoryDuration = 8000L
)
spitfire_cloaked.innateDamage = new DamageWithPosition {
@@ -9136,9 +9140,14 @@ object GlobalDefinitions {
spitfire_aa.deployAnimation = DeployAnimation.Standard
spitfire_aa.Model = ComplexDeployableResolutions.calculate
spitfire_aa.AutoFire = Automation(
- targetingRange = 80f,
- targetValidation = List(EffectTarget.Validation.Aircraft),
- retaliatoryDuration = 8000L
+ targetDetectionRange = 100f,
+ targetTriggerRange = 90f,
+ targetEscapeRange = 200f,
+ targetValidation = List(EffectTarget.Validation.AircraftOnRadar),
+ retaliatoryDuration = 2000L,
+ retaliationOverridesTarget = false,
+ cylindricalCheck = true,
+ cylindricalHeight = 25f
)
spitfire_aa.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
@@ -10041,6 +10050,15 @@ object GlobalDefinitions {
manned_turret.FactionLocked = true
manned_turret.ReserveAmmunition = false
manned_turret.RadiationShielding = 0.5f
+ manned_turret.AutoFire = Automation(
+ targetDetectionRange = 100f,
+ targetTriggerRange = 90f,
+ targetEscapeRange = 200f,
+ targetValidation = List(EffectTarget.Validation.MaxOnRadar, EffectTarget.Validation.VehiclesOnRadar),
+ retaliatoryDuration = 8000L,
+ cylindricalCheck = true,
+ cylindricalHeight = 25f
+ )
manned_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 150
diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala
index 73e6ee266..52b65fcc0 100644
--- a/src/main/scala/net/psforever/objects/Player.scala
+++ b/src/main/scala/net/psforever/objects/Player.scala
@@ -38,7 +38,7 @@ class Player(var avatar: Avatar)
with MountableEntity {
interaction(new InteractWithEnvironment())
interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10))
- interaction(new InteractWithTurrets(range = 100f))
+ interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))
private var backpack: Boolean = false
diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala
index af6bcd069..75289bc48 100644
--- a/src/main/scala/net/psforever/objects/TurretDeployable.scala
+++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala
@@ -89,6 +89,8 @@ class TurretControl(turret: TurretDeployable)
case _ => ()
}
+ protected def AutomaticOperationFunctionalityChecks: Boolean = true
+
override protected def mountTest(
obj: PlanetSideServerObject with Mountable,
seatNumber: Int,
@@ -120,13 +122,7 @@ class TurretControl(turret: TurretDeployable)
}
override protected def DamageAwareness(target: Target, cause: DamageResult, amount: Any): Unit = {
- if (AutomaticOperation && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDuration > 0)) {
- //turret retribution
- AutomatedTurretBehavior.getAttackerFromCause(target.Zone, cause).collect {
- case attacker if attacker.Faction != target.Faction =>
- engageNewDetectedTarget(attacker)
- }
- }
+ attemptRetaliation(target, cause)
super.DamageAwareness(target, cause, amount)
}
@@ -147,15 +143,14 @@ class TurretControl(turret: TurretDeployable)
//it's possible to request deconstruction of one's own field turret while seated in it
val wasKickedByDriver = false
seats.foreach { seat =>
- seat.occupant match {
- case Some(tplayer) =>
- seat.unmount(tplayer)
- tplayer.VehicleSeated = None
+ seat.occupant.collect {
+ case player: Player =>
+ seat.unmount(player)
+ player.VehicleSeated = None
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
- VehicleAction.KickPassenger(tplayer.GUID, 4, wasKickedByDriver, turret.GUID)
+ VehicleAction.KickPassenger(player.GUID, 4, wasKickedByDriver, turret.GUID)
)
- case None => ()
}
}
Some(time.getOrElse(Deployable.cleanup) + Deployable.cleanup)
diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala
index 962e19cff..d6bbd19d1 100644
--- a/src/main/scala/net/psforever/objects/Vehicle.scala
+++ b/src/main/scala/net/psforever/objects/Vehicle.scala
@@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
-import net.psforever.objects.ce.InteractWithMines
+import net.psforever.objects.ce.{InteractWithMines, InteractWithTurrets}
import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile}
@@ -92,6 +92,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
with MountableEntity {
interaction(new InteractWithEnvironment())
interaction(new InteractWithMines(range = 20))
+ interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20))
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
diff --git a/src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala b/src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala
index 82301ce7b..2732bc6fd 100644
--- a/src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala
+++ b/src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala
@@ -1,6 +1,7 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.ce
+import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.turret.{AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.zones.blockmap.SectorPopulation
@@ -13,8 +14,10 @@ case object TurretInteraction extends ZoneInteractionType
/**
* ...
*/
-class InteractWithTurrets(val range: Float)
+class InteractWithTurrets()
extends ZoneInteraction {
+ def range: Float = InteractWithTurrets.Range
+
def Type: TurretInteraction.type = TurretInteraction
/**
@@ -34,8 +37,8 @@ class InteractWithTurrets(val range: Float)
private def getTurretTargets(
sector: SectorPopulation,
position: Vector3
- ): List[PlanetSideServerObject with AutomatedTurret] = {
- (sector
+ ): Iterable[PlanetSideServerObject with AutomatedTurret] = {
+ val list: Iterable[AutomatedTurret] = sector
.deployableList
.collect {
case turret: AutomatedTurret => turret
@@ -43,8 +46,15 @@ class InteractWithTurrets(val range: Float)
.amenityList
.collect {
case turret: AutomatedTurret => turret
- })
- .filter { turret => Vector3.DistanceSquared(turret.Position.xy, position) < 625 }
+ }
+ list.collect {
+ case turret: AutomatedTurret
+ if {
+ val stats = turret.Definition.AutoFire
+ stats.nonEmpty &&
+ AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(stats, turret.Position, position, range, result = -1)
+ } => turret
+ }
}
/**
@@ -60,3 +70,17 @@ class InteractWithTurrets(val range: Float)
}
}
}
+
+object InteractWithTurrets {
+ private val Range: Float = {
+ Seq(
+ GlobalDefinitions.spitfire_turret,
+ GlobalDefinitions.spitfire_cloaked,
+ GlobalDefinitions.spitfire_aa,
+ GlobalDefinitions.manned_turret
+ )
+ .flatMap(_.AutoFire)
+ .map(_.targetDetectionRange)
+ .max
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala
index c326b5761..b552661c9 100644
--- a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala
+++ b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala
@@ -188,19 +188,19 @@ object EffectTarget {
false
}
- def ObviousPlayer(target: PlanetSideGameObject): Boolean =
+ def PlayerOnRadar(target: PlanetSideGameObject): Boolean =
!target.Destroyed && (target match {
case p: Player =>
//TODO attacking breaks stealth
p.LastDamage.map(_.interaction.hitTime).exists(System.currentTimeMillis() - _ < 3000L) ||
p.avatar.implants.flatten.find(a => a.definition.implantType == ImplantType.SilentRun).exists(_.active) ||
- (p.isMoving(test = 17d) && !p.Crouching) ||
+ (p.isMoving(test = 17d) && !(p.Crouching || p.Cloaked)) ||
p.Jumping
case _ =>
false
})
- def ObviousMax(target: PlanetSideGameObject): Boolean =
+ def MaxOnRadar(target: PlanetSideGameObject): Boolean =
!target.Destroyed && (target match {
case p: Player =>
p.ExoSuit == ExoSuitType.MAX && p.isMoving(test = 17d)
@@ -212,11 +212,22 @@ object EffectTarget {
!target.Destroyed && (target match {
case v: Vehicle =>
val vdef = v.Definition
- !(GlobalDefinitions.isAtvVehicle(vdef) ||
+ !(v.Cloaked ||
+ GlobalDefinitions.isAtvVehicle(vdef) ||
vdef == GlobalDefinitions.two_man_assault_buggy ||
vdef == GlobalDefinitions.skyguard)
case _ =>
false
})
+
+
+
+ def AircraftOnRadar(target: PlanetSideGameObject): Boolean =
+ target match {
+ case v: Vehicle =>
+ GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 && !v.Cloaked
+ case _ =>
+ false
+ }
}
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
index 05628c740..1c60938c6 100644
--- a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
@@ -65,7 +65,7 @@ trait MountableBehavior {
!obj.Destroyed
}
- private def tryMount(
+ protected def tryMount(
obj: PlanetSideServerObject with Mountable,
seatNumber: Int,
player: Player
@@ -110,7 +110,7 @@ trait MountableBehavior {
})
}
- private def tryDismount(
+ protected def tryDismount(
obj: Mountable,
seatNumber: Int,
user: Player,
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala
index 585a43062..8953dfd46 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala
@@ -2,9 +2,8 @@
package net.psforever.objects.serverobject.turret
import akka.actor.{Actor, Cancellable}
-import net.psforever.objects.ce.TurretInteraction
import net.psforever.objects.definition.ObjectDefinition
-import net.psforever.objects.{Default, Player}
+import net.psforever.objects.{Default, Player, Vehicle}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.DamageableEntity
import net.psforever.objects.sourcing.{SourceEntry, SourceUniqueness}
@@ -16,7 +15,7 @@ import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{PlanetSideGUID, Vector3}
import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.duration.FiniteDuration
+import scala.concurrent.duration._
trait AutomatedTurret
extends PlanetSideServerObject
@@ -80,8 +79,6 @@ trait AutomatedTurretBehavior {
private var currentTargetToken: Option[SourceUniqueness] = None
- private var currentTarget: Option[Target] = None
-
private var currentTargetLastShotReported: Long = 0L
private var periodicValidationTest: Cancellable = Default.Cancellable
@@ -118,7 +115,7 @@ trait AutomatedTurretBehavior {
if (!previousState && state) {
trySelectNewTarget()
} else if (previousState && !state) {
- currentTarget.foreach {
+ AutomatedTurretObject.Target.foreach {
noLongerEngageDetectedTarget
}
}
@@ -128,6 +125,8 @@ trait AutomatedTurretBehavior {
}
}
+ protected def AutomaticOperationFunctionalityChecks: Boolean
+
private def bringAttentionToTarget(target: Target): Unit = {
val targets = AutomatedTurretObject.Targets
val size = targets.size
@@ -166,7 +165,7 @@ trait AutomatedTurretBehavior {
}
private def resetAlerts(): Unit = {
- currentTarget.foreach { noLongerEngageDetectedTarget }
+ AutomatedTurretObject.Target.foreach { noLongerEngageDetectedTarget }
AutomatedTurretObject.Clear()
testTargetListQualifications(beforeSize = 1)
}
@@ -175,14 +174,15 @@ trait AutomatedTurretBehavior {
val zone = target.Zone
AutomatedTurretBehavior.startTrackingTargets(zone, channel, AutomatedTurretObject.GUID, List(target.GUID))
AutomatedTurretBehavior.startShooting(zone, channel, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
- AutomatedTurretBehavior.stopShooting(zone, channel, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
+ context.system.scheduler.scheduleOnce(100.milliseconds, (() => {
+ AutomatedTurretBehavior.stopShooting(zone, channel, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
+ }).asInstanceOf[Runnable])
}
protected def engageNewDetectedTarget(target: Target): Unit = {
val zone = target.Zone
val zoneid = zone.id
AutomatedTurretObject.Target = target
- currentTarget = Some(target)
currentTargetToken = Some(SourceEntry(target).unique)
AutomatedTurretBehavior.startTrackingTargets(zone, zoneid, AutomatedTurretObject.GUID, List(target.GUID))
AutomatedTurretBehavior.startShooting(zone, zoneid, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
@@ -192,7 +192,7 @@ trait AutomatedTurretBehavior {
if (currentTargetToken.contains(SourceEntry(target).unique)) {
noLongerEngageDetectedTarget(target)
} else {
- currentTarget
+ AutomatedTurretObject.Target
}
}
@@ -200,7 +200,6 @@ trait AutomatedTurretBehavior {
val zone = target.Zone
val zoneid = zone.id
AutomatedTurretObject.Target = None
- currentTarget = None
currentTargetToken = None
AutomatedTurretBehavior.stopTrackingTargets(zone, zoneid, AutomatedTurretObject.GUID)
AutomatedTurretBehavior.stopShooting(zone, zoneid, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
@@ -208,28 +207,32 @@ trait AutomatedTurretBehavior {
}
protected def trySelectNewTarget(): Option[Target] = {
- currentTarget.orElse {
+ AutomatedTurretObject.Target.orElse {
val turretPosition = AutomatedTurretObject.Position
- val radiusSquared = autoStats.get.targetingRange * autoStats.get.targetingRange
+ val radius = autoStats.get.targetTriggerRange
val validation = autoStats.get.targetValidation
val faction = AutomatedTurretObject.Faction
AutomatedTurretObject
.Targets
- .map { target =>
- (target, Vector3.DistanceSquared(target.Position, turretPosition))
- }
- .collect { case out @ (target, distance)
+ .collect { case target
if /*target.Faction != faction &&*/
- distance < radiusSquared &&
+ AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, target.Position, turretPosition, radius, result = -1) &&
validation.exists(func => func(target)) =>
- out
+ target
}
- .sortBy(_._2)
- .flatMap { case (target: Player, _) =>
- testNewDetectedTarget(target, target.Name)
- Some(target)
+ .sortBy(target => Vector3.DistanceSquared(target.Position, turretPosition))
+ .collectFirst {
+ case target: Player =>
+ testNewDetectedTarget(target, target.Name)
+ target
+ case target: Vehicle =>
+ target.Seats.values
+ .flatMap(_.occupants)
+ .foreach { target =>
+ testNewDetectedTarget(target, target.Name)
+ }
+ target
}
- .headOption
}
}
@@ -244,13 +247,11 @@ trait AutomatedTurretBehavior {
private def performDistanceCheck(): List[Target] = {
//cull targets
val pos = AutomatedTurretObject.Position
+ val range = autoStats.map(_.targetDetectionRange).getOrElse(0f)
val removedTargets = AutomatedTurretObject.Targets
.collect {
case t: InteractsWithZone
- if t.Destroyed || {
- val range = t.interaction().find(_.Type == TurretInteraction).map(_.range).getOrElse(100f)
- Vector3.DistanceSquared(t.Position, pos) > range * range
- } =>
+ if t.Destroyed || AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, t.Position, pos, range) =>
AutomatedTurretObject.RemoveTarget(t)
t
}
@@ -259,54 +260,73 @@ trait AutomatedTurretBehavior {
private def performCurrentTargetDecayCheck(): Unit = {
val now = System.currentTimeMillis()
- val delay = autoStats.map(_.missedShotCooldown).getOrElse(3000L)
- currentTarget
+ val selectDelay = autoStats.map(_.targetSelectCooldown).getOrElse(3000L)
+ AutomatedTurretObject.Target
.collect { target =>
- if (target.Destroyed) {
- //if the target died while we were shooting at it, immediately switch to the next target
- noLongerEngageDetectedTarget(target)
- currentTargetLastShotReported = now - delay
- None
- } else if (System.currentTimeMillis() - currentTargetLastShotReported >= delay) {
- //if the target goes mia, evaluate a possible cooldown phase before selecting next target
- noLongerEngageDetectedTarget(target)
- None
- } else {
- //continue shooting
- Some(target)
- }
+ //test target
+ generalDecayCheck(
+ now,
+ autoStats.map(_.targetEscapeRange).getOrElse(400f),
+ selectDelay,
+ autoStats.map(_.missedShotCooldown).getOrElse(3000L),
+ autoStats.map(_.targetEliminationCooldown).getOrElse(0L)
+ )(target)
}
.orElse {
//no target; unless we are deactivated or have any unfinished delays, search for new target
- if (automaticOperation && now - currentTargetLastShotReported >= delay) {
+ if (automaticOperation && now - currentTargetLastShotReported >= selectDelay) {
trySelectNewTarget()
}
None
}
}
+ private def generalDecayCheck(
+ now: Long,
+ escapeRange: Float,
+ selectDelay: Long,
+ cooldownDelay: Long,
+ eliminationDelay: Long
+ )(target: Target): Option[Target] = {
+ if (target.Destroyed) {
+ //if the target died while we were shooting at it
+ noLongerEngageDetectedTarget(target)
+ currentTargetLastShotReported = now - eliminationDelay
+ None
+ } else if (AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, target.Position, AutomatedTurretObject.Position, escapeRange)) {
+ //if the target made sufficient distance from the turret
+ noLongerEngageDetectedTarget(target)
+ currentTargetLastShotReported = now - cooldownDelay
+ None
+ } else if (now - currentTargetLastShotReported >= cooldownDelay) {
+ //if the target goes mia through lack of response
+ noLongerEngageDetectedTarget(target)
+ currentTargetLastShotReported = now - selectDelay
+ None
+ } else {
+ //continue shooting
+ Some(target)
+ }
+ }
+
private def testTargetListQualifications(beforeSize: Int): Boolean = {
beforeSize > 0 && AutomatedTurretObject.Targets.isEmpty && periodicValidationTest.cancel()
}
private def retimePeriodicTargetChecks(beforeSize: Int): Boolean = {
if (beforeSize == 0 && AutomatedTurretObject.Targets.nonEmpty && autoStats.isDefined) {
- val (initial, repeated) = autoStats
- .map {
- ta => (ta.initialDetectionSpeed, ta.detectionSpeed)
- }
- .get
- retimePeriodicTargetChecks(initial, repeated)
+ val repeated = autoStats.map(_.detectionSpeed).getOrElse(0.seconds)
+ retimePeriodicTargetChecks(repeated)
true
} else {
false
}
}
- private def retimePeriodicTargetChecks(initial: FiniteDuration, repeated: FiniteDuration): Unit = {
+ private def retimePeriodicTargetChecks(repeated: FiniteDuration): Unit = {
periodicValidationTest.cancel()
periodicValidationTest = context.system.scheduler.scheduleWithFixedDelay(
- initial,
+ 0.seconds,
repeated,
self,
AutomatedTurretBehavior.PeriodicCheck
@@ -315,9 +335,34 @@ trait AutomatedTurretBehavior {
def automaticTurretPostStop(): Unit = {
periodicValidationTest.cancel()
- currentTarget.foreach { noLongerEngageDetectedTarget }
+ AutomatedTurretObject.Target.foreach { noLongerEngageDetectedTarget }
AutomatedTurretObject.Targets.foreach { AutomatedTurretObject.RemoveTarget }
}
+
+ protected def attemptRetaliation(target: Target, cause: DamageResult): Option[Target] = {
+ if (automaticOperation && autoStats.exists(_.retaliatoryDuration > 0)) {
+ AutomatedTurretBehavior.getAttackerFromCause(target.Zone, cause).collect {
+ case attacker if attacker.Faction != target.Faction =>
+ performRetaliation(attacker)
+ attacker
+ }
+ } else {
+ None
+ }
+ }
+
+ private def performRetaliation(target: Target): Option[Target] = {
+ AutomatedTurretObject.Target
+ .collect {
+ case _ if autoStats.exists(_.retaliationOverridesTarget) =>
+ engageNewDetectedTarget(target)
+ target
+ }
+ .orElse {
+ engageNewDetectedTarget(target)
+ Some(target)
+ }
+ }
}
object AutomatedTurretBehavior {
@@ -394,4 +439,21 @@ object AutomatedTurretBehavior {
case out: PlanetSideServerObject with Vitality => out
}
}
+
+ def shapedDistanceCheckAgainstValue(
+ stats: Option[Automation],
+ position: Vector3,
+ testPosition: Vector3,
+ testRange: Float,
+ result: Int = 1 //by default, calculation > testRange^2
+ ): Boolean = {
+ val testRangeSq = testRange * testRange
+ if (stats.exists(_.cylindricalCheck)) {
+ val height = testRange + stats.map(_.cylindricalHeight).getOrElse(0f)
+ math.abs(position.z - testPosition.z).compareTo(height) == result &&
+ Vector3.DistanceSquared(position.xy, testPosition.xy).compareTo(testRangeSq) == result
+ } else {
+ Vector3.DistanceSquared(position, testPosition).compareTo(testRangeSq) == result
+ }
+ }
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
index b41b61190..30ca857a3 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
@@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret
-import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.{GlobalDefinitions, Player, Tool}
import net.psforever.objects.equipment.Ammo
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
@@ -15,16 +14,13 @@ import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.packet.game.ChangeFireModeMessage
import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import net.psforever.types.BailType
/**
- * An `Actor` that handles messages being dispatched to a specific `MannedTurret`.
- *
- * Mounted turrets have only slightly different entry requirements than a normal vehicle
- * because they encompass both faction-specific facility turrets
- * and faction-blind cavern sentry turrets.
- *
- * @param turret the `MannedTurret` object being governed
- */
+ * A control agency that handles messages being dispatched to a specific `FacilityTurret`.
+ * These turrets are attached specifically to surface-level facilities and field towers.
+ * @param turret the `FacilityTurret` object being governed
+ */
class FacilityTurretControl(turret: FacilityTurret)
extends PoweredAmenityControl
with AmenityAutoRepair
@@ -92,27 +88,31 @@ class FacilityTurretControl(turret: FacilityTurret)
seatNumber: Int,
player: Player): Boolean = {
super.mountTest(obj, seatNumber, player) &&
- (!TurretObject.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L) &&
- AutomaticOperation_=(state = false)
+ (!TurretObject.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L)
}
- override protected def dismountTest(obj: Mountable with WorldEntity, seatNumber: Int, user: Player): Boolean = {
- super.dismountTest(obj, seatNumber, user) &&
- AutomaticOperation_=(autoTurretFunctionalityChecks)
+ override protected def tryMount(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = {
+ val result = super.tryMount(obj, seatNumber, player)
+ if (result) {
+ AutomaticOperation = false
+ }
+ result
+ }
+
+ override protected def tryDismount(obj: Mountable, seatNumber: Int, player: Player, bailType: BailType.Value): Boolean = {
+ val result = super.tryDismount(obj, seatNumber, player, bailType)
+ if (result) {
+ AutomaticOperation = AutomaticOperationFunctionalityChecks
+ }
+ result
}
override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any) : Unit = {
tryAutoRepair()
- if (AutomaticOperation) {
- if (TurretObject.Health < TurretObject.Definition.DamageDisablesAt) {
- AutomaticOperation = false
- } else if (AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDuration > 0)) {
- //turret retribution
- AutomatedTurretBehavior.getAttackerFromCause(target.Zone, cause).collect {
- case attacker if attacker.Faction != target.Faction =>
- engageNewDetectedTarget(attacker)
- }
- }
+ if (TurretObject.Health < TurretObject.Definition.DamageDisablesAt) {
+ AutomaticOperation = false
+ } else {
+ attemptRetaliation(target, cause)
}
super.DamageAwareness(target, cause, amount)
}
@@ -133,7 +133,7 @@ class FacilityTurretControl(turret: FacilityTurret)
override def Restoration(obj: Damageable.Target): Unit = {
super.Restoration(obj)
- AutomaticOperation = autoTurretFunctionalityChecks
+ AutomaticOperation = AutomaticOperationFunctionalityChecks
}
override def tryAutoRepair() : Boolean = {
@@ -163,7 +163,7 @@ class FacilityTurretControl(turret: FacilityTurret)
def powerTurnOnCallback(): Unit = {
tryAutoRepair()
- AutomaticOperation = autoTurretFunctionalityChecks
+ AutomaticOperation = AutomaticOperationFunctionalityChecks
}
override def AutomaticOperation_=(state: Boolean): Boolean = {
@@ -172,7 +172,7 @@ class FacilityTurretControl(turret: FacilityTurret)
result
}
- private def autoTurretFunctionalityChecks: Boolean = {
+ protected def AutomaticOperationFunctionalityChecks: Boolean = {
isPowered &&
!JammableObject.Jammed &&
TurretObject.Health > TurretObject.Definition.DamageDisablesAt &&
@@ -198,7 +198,7 @@ class FacilityTurretControl(turret: FacilityTurret)
}
override protected def trySelectNewTarget(): Option[AutomatedTurret.Target] = {
- if (autoTurretFunctionalityChecks) {
+ if (AutomaticOperationFunctionalityChecks) {
primaryWeaponFireModeOnly()
super.trySelectNewTarget()
} else {
@@ -207,7 +207,7 @@ class FacilityTurretControl(turret: FacilityTurret)
}
override def engageNewDetectedTarget(target: AutomatedTurret.Target): Unit = {
- if (autoTurretFunctionalityChecks) {
+ if (AutomaticOperationFunctionalityChecks) {
primaryWeaponFireModeOnly()
super.engageNewDetectedTarget(target)
}
@@ -233,6 +233,6 @@ class FacilityTurretControl(turret: FacilityTurret)
override def CancelJammeredStatus(target: Any): Unit = {
val startsJammed = JammableObject.Jammed
super.CancelJammeredStatus(target)
- startsJammed && AutomaticOperation_=(autoTurretFunctionalityChecks)
+ startsJammed && AutomaticOperation_=(AutomaticOperationFunctionalityChecks)
}
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala
index f677798d6..046bccb95 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala
@@ -13,13 +13,13 @@ import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
trait MountableTurretControl
- extends FactionAffinityBehavior.Check
+ extends Actor
+ with FactionAffinityBehavior.Check
with MountableBehavior
with DamageableWeaponTurret
with RepairableWeaponTurret
- with JammableMountedWeapons
- with Actor { /* note: jammable status is reported as vehicle events, not local events */
- def TurretObject: PlanetSideServerObject with WeaponTurret
+ with JammableMountedWeapons { /* note: jammable status is reported as vehicle events, not local events */
+ def TurretObject: PlanetSideServerObject with WeaponTurret with Mountable
override def postStop(): Unit = {
super.postStop()
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala
index 77c4dffba..cf6bdf33c 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala
@@ -11,16 +11,23 @@ import scala.collection.mutable
import scala.concurrent.duration._
final case class Automation(
- targetingRange: Float,
+ targetDetectionRange: Float,
+ targetTriggerRange: Float,
+ targetEscapeRange: Float,
targetValidation: List[PlanetSideGameObject => Boolean],
+ cylindricalCheck: Boolean = false,
+ cylindricalHeight: Float = 0,
retaliatoryDuration: Long = 0,
+ retaliationOverridesTarget: Boolean = true,
initialDetectionSpeed: FiniteDuration = Duration.Zero,
detectionSpeed: FiniteDuration = 1.seconds,
targetSelectCooldown: Long = 1500L, //ms
missedShotCooldown: Long = 3000L, //ms
targetEliminationCooldown: Long = 0L, //ms
revertToDefaultFireMode: Boolean = true
- )
+ ) {
+ assert(targetDetectionRange > targetTriggerRange, "trigger range must be less or equal to detection range")
+}
/**
* The definition for any `WeaponTurret`.
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/VanuSentryControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/VanuSentryControl.scala
index 6f0f32d36..4a6cb991c 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/VanuSentryControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/VanuSentryControl.scala
@@ -10,15 +10,20 @@ import net.psforever.types.Vector3
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
+/**
+ * A control agency that handles messages being dispatched to a specific `FacilityTurret`.
+ * These turrets are installed tangential to cavern facilities but are independent of the facility.
+ * @param turret the `FacilityTurret` object being governed
+ */
class VanuSentryControl(turret: FacilityTurret)
extends ServerObjectControl
with MountableTurretControl {
- def TurretObject: FacilityTurret = turret
- def FactionObject: FacilityTurret = turret
- def MountableObject: FacilityTurret = turret
- def JammableObject: FacilityTurret = turret
- def DamageableObject: FacilityTurret = turret
- def RepairableObject: FacilityTurret = turret
+ def TurretObject: FacilityTurret = turret
+ def FactionObject: FacilityTurret = turret
+ def MountableObject: FacilityTurret = turret
+ def JammableObject: FacilityTurret = turret
+ def DamageableObject: FacilityTurret = turret
+ def RepairableObject: FacilityTurret = turret
// Used for timing ammo recharge for vanu turrets in caves
private var weaponAmmoRechargeTimer: Cancellable = Default.Cancellable