self-reported firing mode for targets that go stationary and then use a 'clever trick' to avoid taking damage while in full exposure to the automated turret; documentation on the automated turret operations (it needs it!)

This commit is contained in:
Fate-JH 2024-01-15 18:37:02 -05:00
parent 3ffb817f4a
commit 02ad42743c
11 changed files with 753 additions and 232 deletions

View file

@ -3,7 +3,6 @@ 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
@ -450,16 +449,9 @@ private[support] class WeaponAndProjectileOperations(
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
None
case turret: AutomatedTurret with OwnableByPlayer => //most likely a deployable
case turret: AutomatedTurret =>
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
val owner = continent.GUID(turret.OwnerGuid)
.collect { case obj: PlanetSideGameObject with FactionAffinity => SourceEntry(obj) }
.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)
CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId)
}
.collect {
case Some((obj, tool, owner, projectileInfo)) =>

View file

@ -9078,7 +9078,8 @@ object GlobalDefinitions {
targetTriggerRange = 50f,
targetEscapeRange = 50f,
targetValidation = List(EffectTarget.Validation.PlayerOnRadar, EffectTarget.Validation.Vehicle),
retaliatoryDuration = 8000L
retaliatoryDuration = 8000L,
refireTime = 200.milliseconds //150.milliseconds
)
spitfire_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
@ -9111,7 +9112,8 @@ object GlobalDefinitions {
targetTriggerRange = 30f,
targetEscapeRange = 50f,
targetValidation = List(EffectTarget.Validation.PlayerOnRadar, EffectTarget.Validation.VehiclesOnRadar),
retaliatoryDuration = 8000L
retaliatoryDuration = 8000L,
refireTime = 200.milliseconds //150.milliseconds
)
spitfire_cloaked.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
@ -9146,6 +9148,7 @@ object GlobalDefinitions {
targetValidation = List(EffectTarget.Validation.AircraftOnRadar),
retaliatoryDuration = 2000L,
retaliationOverridesTarget = false,
refireTime = 350.milliseconds, //300.milliseconds
cylindricalCheck = true,
cylindricalHeight = 25f
)
@ -10058,7 +10061,8 @@ object GlobalDefinitions {
retaliatoryDuration = 8000L,
cylindricalCheck = true,
cylindricalHeight = 25f,
detectionSpeed = 2.seconds
detectionSpeed = 2.seconds,
refireTime = 362.milliseconds //312.milliseconds
)
manned_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One

View file

@ -13,6 +13,7 @@ import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.turret.{AutomatedTurret, AutomatedTurretBehavior, MountableTurretControl, TurretDefinition, WeaponTurret}
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance}
@ -28,6 +29,15 @@ class TurretDeployable(tdef: TurretDeployableDefinition)
with AutomatedTurret {
WeaponTurret.LoadDefinition(turret = this)
def TurretOwner: SourceEntry = {
Seats
.headOption
.collect { case (_, a) => a }
.flatMap(_.occupant)
.map(SourceEntry(_))
.getOrElse(SourceEntry(this))
}
override def Definition: TurretDeployableDefinition = tdef
}
@ -105,7 +115,7 @@ class TurretControl(turret: TurretDeployable)
AutomaticOperation = false
//look in direction of cause of jamming
val zone = JammableObject.Zone
AutomatedTurretBehavior.getAttackerFromCause(zone, cause).foreach {
AutomatedTurretBehavior.getAttackVectorFromCause(zone, cause).foreach {
attacker =>
val channel = zone.id
val guid = AutomatedTurretObject.GUID

View file

@ -0,0 +1,69 @@
// Copyright (c) 2024 PSForever
package net.psforever.objects.serverobject.turret
import akka.actor.Actor
import net.psforever.objects.Tool
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.types.Vector3
/**
* With a timed messaging cycle from `AutomatedTurretBehavior`,
* an implementation of this trait should be able to simulate being damaged by a source of automated weapon's fire
* without needing a player character to experience the damage directly as is usual for a client's user.
* As a drawback, however, it's not possible to validate against collision detection of any sort
* so damage could be applied through trees and rocks and walls and other users.
*/
trait AffectedByAutomaticTurretFire extends Damageable {
_: Actor =>
def AffectedObject: AutomatedTurret.Target
val takeAutomatedDamage: Receive = {
case AffectedByAutomaticTurretFire.AiDamage(turret) =>
performAutomatedDamage(turret)
}
private def performAutomatedDamage(turret: AutomatedTurret): Unit = {
val target = AffectedObject
val tool = turret.Weapons.values.head.Equipment.collect { case t : Tool => t }.get
val projectileInfo = tool.Projectile
val targetPos = target.Position
val turretPos = turret.Position
val correctedTargetPosition = targetPos + Vector3.z(value = 1f)
val angle = Vector3.Unit(targetPos - turretPos)
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target, Some(SourceEntry(target)))
val projectile = new Projectile(
projectileInfo,
tool.Definition,
tool.FireMode,
None,
turret.TurretOwner,
turret.Definition.ObjectId,
turretPos + Vector3.z(value = 1f),
angle,
Some(angle * projectileInfo.FinalVelocity)
)
val modProjectile = ProjectileQuality.modifiers(
projectile,
DamageResolution.Hit,
target,
correctedTargetPosition,
None
)
val resolvedProjectile = DamageInteraction(
SourceEntry(target),
ProjectileReason(DamageResolution.Hit, modProjectile, target.DamageModel),
correctedTargetPosition
)
PerformDamage(target, resolvedProjectile.calculate())
}
}
object AffectedByAutomaticTurretFire {
case class AiDamage(turret: AutomatedTurret)
}

View file

@ -0,0 +1,63 @@
// Copyright (c) 2024 PSForever
package net.psforever.objects.serverobject.turret
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.sourcing.{SourceEntry, SourceUniqueness}
import net.psforever.objects.vital.Vitality
trait AutomatedTurret
extends PlanetSideServerObject
with WeaponTurret {
import AutomatedTurret.Target
private var currentTarget: Option[Target] = None
private var targets: List[Target] = List[Target]()
def TurretOwner: SourceEntry
def Target: Option[Target] = currentTarget
def Target_=(newTarget: Target): Option[Target] = {
Target_=(Some(newTarget))
}
def Target_=(newTarget: Option[Target]): Option[Target] = {
if (newTarget.isDefined != currentTarget.isDefined) {
currentTarget = newTarget
}
currentTarget
}
def Targets: List[Target] = targets
def Detected(target: Target): Option[Target] = {
val unique = SourceEntry(target).unique
targets.find(SourceEntry(_).unique == unique)
}
def Detected(target: SourceUniqueness): Option[Target] = {
targets.find(SourceEntry(_).unique == target)
}
def AddTarget(target: Target): Unit = {
targets = targets :+ target
}
def RemoveTarget(target: Target): Unit = {
val unique = SourceEntry(target).unique
targets = targets.filterNot(SourceEntry(_).unique == unique)
}
def Clear(): List[Target] = {
val oldTargets = targets
targets = Nil
oldTargets
}
def Definition: ObjectDefinition with TurretDefinition
}
object AutomatedTurret {
type Target = PlanetSideServerObject with Vitality
}

View file

@ -1,8 +1,7 @@
// Copyright (c) 2023 PSForever
// Copyright (c) 2024 PSForever
package net.psforever.objects.serverobject.turret
import akka.actor.{Actor, ActorRef, Cancellable}
import net.psforever.objects.definition.ObjectDefinition
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.{Default, Player, Vehicle}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.DamageableEntity
@ -10,85 +9,34 @@ import net.psforever.objects.sourcing.{SourceEntry, SourceUniqueness}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.{InteractsWithZone, Zone}
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ObjectDetectedMessage}
import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{PlanetSideGUID, Vector3}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
trait AutomatedTurret
extends PlanetSideServerObject
with WeaponTurret {
import AutomatedTurret.Target
private var currentTarget: Option[Target] = None
private var targets: List[Target] = List[Target]()
def Target: Option[Target] = currentTarget
def Target_=(newTarget: Target): Option[Target] = {
Target_=(Some(newTarget))
}
def Target_=(newTarget: Option[Target]): Option[Target] = {
if (newTarget.isDefined != currentTarget.isDefined) {
currentTarget = newTarget
}
currentTarget
}
def Targets: List[Target] = targets
def Detected(target: Target): Option[Target] = {
val unique = SourceEntry(target).unique
targets.find(SourceEntry(_).unique == unique)
}
def Detected(target: SourceUniqueness): Option[Target] = {
targets.find(SourceEntry(_).unique == target)
}
def AddTarget(target: Target): Unit = {
targets = targets :+ target
}
def RemoveTarget(target: Target): Unit = {
val unique = SourceEntry(target).unique
targets = targets.filterNot(SourceEntry(_).unique == unique)
}
def Clear(): List[Target] = {
val oldTargets = targets
targets = Nil
oldTargets
}
def Definition: ObjectDefinition with TurretDefinition
}
object AutomatedTurret {
type Target = PlanetSideServerObject with Vitality
}
trait AutomatedTurretBehavior {
_: Actor with DamageableEntity =>
import AutomatedTurret.Target
private var automaticOperation: Boolean = false
private var currentTargetToken: Option[SourceUniqueness] = None
private var currentTargetLastShotReported: Long = 0L
private var periodicValidationTest: Cancellable = Default.Cancellable
/** a local reference to the automated turret data on the entity's definition */
private lazy val autoStats: Option[Automation] = AutomatedTurretObject.Definition.AutoFire
/** whether the automated turret is functional or if anything is blocking its operation */
private var automaticOperation: Boolean = false
/** quick reference of the current target, if any */
private var currentTargetToken: Option[SourceUniqueness] = None
/** time of the last confirmed shot hitting the target */
private var currentTargetLastShotTime: Long = 0L
/** game world position when the last shot's confirmation was recorded */
private var currentTargetLocation: Option[Vector3] = None
/** targets queued during evaluation of the "next test", and targets to be being confirmed during "this test" */
private var previousTestedTargets: Set[Target] = Set()
/** timer managing the available target qualifications test
* whether or not a previously valid target is still a valid target */
private var periodicValidationTest: Cancellable = Default.Cancellable
/** timer managing the trailing target qualifications self test
* where the a source will shot directly at some target */
private var selfReportedRefire: Cancellable = Default.Cancellable
private var confirmShotFunc: Target=>Unit = normalConfirmShot
def AutomatedTurretObject: AutomatedTurret
@ -96,8 +44,11 @@ trait AutomatedTurretBehavior {
case AutomatedTurretBehavior.Alert(target) =>
bringAttentionToTarget(target)
case AutomatedTurretBehavior.ConfirmShot(target) =>
confirmShot(target)
case AutomatedTurretBehavior.ConfirmShot(target, None) =>
confirmShotFunc(target)
case AutomatedTurretBehavior.ConfirmShot(target, _) =>
confirmShotFunc(target)
case AutomatedTurretBehavior.Unalert(target) =>
disregardTarget(target)
@ -113,6 +64,15 @@ trait AutomatedTurretBehavior {
def AutomaticOperation: Boolean = automaticOperation
/**
* In relation to whether the automated turret is operational,
* set the value of a flag to record this condition.
* Additionally, perform actions relevant to the state changes:
* turning on when previously inactive;
* and, turning off when previously active.
* @param state new state
* @return state that results from this action
*/
def AutomaticOperation_=(state: Boolean): Boolean = {
val previousState = automaticOperation
if (autoStats.isDefined) {
@ -120,25 +80,50 @@ trait AutomatedTurretBehavior {
if (!previousState && state) {
trySelectNewTarget()
} else if (previousState && !state) {
AutomatedTurretObject.Target.foreach {
noLongerEngageDetectedTarget
}
cancelSelfReportedAutoFire()
AutomatedTurretObject.Target.foreach(noLongerEngageDetectedTarget)
}
state
} else {
false
previousState
}
}
/**
* A checklist of conditions that must be met before automatic operation of the turret should be possible.
* Should not actually change the current activation state of the turret.
* @return `true`, if it would be possible for automated behavior to become operational;
* `false`, otherwise
*/
protected def AutomaticOperationFunctionalityChecks: Boolean
protected def CurrentTargetLastShotReported: Long = currentTargetLastShotReported
/**
* The last time weapons fire from the turret was confirmed by this control agency.
* Exists for subclass access.
* @return the time
*/
protected def CurrentTargetLastShotReported: Long = currentTargetLastShotTime
/**
* Set a new last time weapons fire from the turret was confirmed by this control agency.
* Exists for subclass access.
* @param value the new time
* @return the time
*/
protected def CurrentTargetLastShotReported_=(value: Long): Long = {
currentTargetLastShotReported = value
currentTargetLastShotTime = value
CurrentTargetLastShotReported
}
/* Actor level functions */
/**
* Add a new potential target to the turret's list of known targets
* only if this is a new potential target.
* If the provided target is the first potential target known to the turret,
* begin the timer that determines when or if that target is no longer considered qualified.
* @param target something the turret can potentially shoot at
*/
private def bringAttentionToTarget(target: Target): Unit = {
val targets = AutomatedTurretObject.Targets
val size = targets.size
@ -150,18 +135,13 @@ trait AutomatedTurretBehavior {
}
}
private def confirmShot(target: Target): Unit = {
val now = System.currentTimeMillis()
if (currentTargetToken.isEmpty) {
currentTargetLastShotReported = now
engageNewDetectedTarget(target)
} else if (
currentTargetToken.contains(SourceEntry(target).unique) &&
now - currentTargetLastShotReported < autoStats.map(_.missedShotCooldown).getOrElse(0L)) {
currentTargetLastShotReported = now
}
}
/**
* Remove a target from the turret's list of known targets.
* If the provided target is the last potential target known to the turret,
* cancel the timer that determines when or if targets are to be considered qualified.
* If we are shooting at the target, stop shooting at it.
* @param target something the turret can potentially shoot at
*/
private def disregardTarget(target: Target): Unit = {
val targets = AutomatedTurretObject.Targets
val size = targets.size
@ -176,42 +156,103 @@ trait AutomatedTurretBehavior {
}
}
/**
* Undo all the things.
* It's like nothing ever happened.
*/
private def resetAlerts(): Unit = {
cancelPeriodicTargetChecks()
cancelSelfReportedAutoFire()
AutomatedTurretObject.Target.foreach(noLongerEngageDetectedTarget)
AutomatedTurretObject.Target = None
AutomatedTurretObject.Clear()
testTargetListQualifications(beforeSize = 1)
previousTestedTargets.foreach(noLongerEngageTestedTarget)
previousTestedTargets = Set()
currentTargetToken = None
currentTargetLocation = None
}
/* Normal automated turret behavior */
/**
* Process feedback from automatic turret weapon fire.
* The most common situation in which this is encountered is when the turret is instructed to shoot at something
* and that something reports being hit with the resulting projectile
* and, as a result, a message is sent to the turret to encourage it to continue to shoot.
* If there is no primary target yet, this target becomes primary.
* @param target something the turret can potentially shoot at
*/
private def normalConfirmShot(target: Target): Unit = {
val now = System.currentTimeMillis()
if (currentTargetToken.isEmpty) {
currentTargetLastShotTime = now
currentTargetLocation = Some(target.Position)
engageNewDetectedTarget(target)
} else if (
currentTargetToken.contains(SourceEntry(target).unique) &&
now - currentTargetLastShotTime < autoStats.map(_.missedShotCooldown).getOrElse(0L)) {
currentTargetLastShotTime = now
currentTargetLocation = Some(target.Position)
}
}
/**
* Point the business end of the turret's weapon at a provided target
* and begin shooting at that target.
* The turret will rotate to follow the target's movements in the game world.
* Perform some cleanup of potential targets and
* perform setup of variables useful to maintain firepower against the target.
* @param target something the turret can potentially shoot at
*/
protected def engageNewDetectedTarget(target: Target): Unit = {
val zone = target.Zone
val zoneid = zone.id
previousTestedTargets.filterNot(_ == target).foreach(noLongerEngageTestedTarget)
previousTestedTargets = Set(target)
currentTargetToken = Some(SourceEntry(target).unique)
currentTargetLocation = Some(target.Position)
AutomatedTurretObject.Target = target
AutomatedTurretBehavior.startTracking(target, zoneid, AutomatedTurretObject.GUID, List(target.GUID))
AutomatedTurretBehavior.startShooting(target, zoneid, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
}
/**
* If the provided target is the current target:
* Stop pointing the business end of the turret's weapon at a provided target.
* Stop shooting at the target.
* @param target something the turret can potentially shoot at
* @return something the turret was potentially shoot at
*/
protected def noLongerDetectTargetIfCurrent(target: Target): Option[Target] = {
if (currentTargetToken.contains(SourceEntry(target).unique)) {
cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(target)
} else {
AutomatedTurretObject.Target
}
}
/**
* Stop pointing the business end of the turret's weapon at a provided target.
* Stop shooting at the target.
* Adjust some local values to disengage from the target.
* @param target something the turret can potentially shoot at
* @return something the turret was potentially shoot at
*/
protected def noLongerEngageDetectedTarget(target: Target): Option[Target] = {
AutomatedTurretObject.Target = None
currentTargetToken = None
currentTargetLocation = None
noLongerEngageTestedTarget(target)
None
}
/**
* Stop pointing the business end of the turret's weapon at a provided target.
* Stop shooting at the target.
* @param target something the turret can potentially shoot at
* @return something the turret was potentially shoot at
*/
private def noLongerEngageTestedTarget(target: Target): Option[Target] = {
val zone = target.Zone
val zoneid = zone.id
@ -220,6 +261,20 @@ trait AutomatedTurretBehavior {
None
}
/**
* While the automated turret is operational and active,
* and while the turret does not have a current target to point towards and shoot at,
* collect all of the potential targets known to the turret
* and perform test shots that would only be visible to certain client perspectives.
* If those perspectives report back about those test shots being confirmed hits,
* the first reported confirmed test shot will be the chosen target.
* We will potentially have an old list of targets that were tested the previous pass
* and can be compared against a fresher list of targets.
* Explicitly order certain unrepresented targets to stop being tested
* in case the packets between the server and the client do not get transmitted properly
* or the turret is not assembled correctly in its automatic fire definition.
* @return something the turret can potentially shoot at
*/
protected def trySelectNewTarget(): Option[Target] = {
AutomatedTurretObject.Target.orElse {
val turretPosition = AutomatedTurretObject.Position
@ -238,19 +293,7 @@ trait AutomatedTurretBehavior {
target
}
.sortBy(target => Vector3.DistanceSquared(target.Position, turretPosition))
.collect {
case target: Player =>
AutomatedTurretBehavior.Generic.testNewDetected(target, target.Name, turretGuid, weaponGuid)
Seq((target, target))
case target: Vehicle =>
target.Seats.values
.flatMap(_.occupants)
.collectFirst { passenger =>
AutomatedTurretBehavior.Generic.testNewDetected(passenger, passenger.Name, turretGuid, weaponGuid)
(target, passenger)
}
}
.flatten
.flatMap { processForTestingDetectedTarget(_, turretGuid, weaponGuid) }
.unzip
//call an explicit stop for these targets
(for {
@ -264,6 +307,45 @@ trait AutomatedTurretBehavior {
}
}
/**
* Dispatch packets in the direction of a client perspective
* to determine if this target can be reliably struck with a projectile from the turret's weapon.
* This resolves to a player avatar entity usually and is communicated on that player's personal name channel.
* @param target something the turret can potentially shoot at
* @param turretGuid turret
* @param weaponGuid turret's weapon
* @return a tuple composed of:
* something the turret can potentially shoot at
* something that will report whether the test shot struck the target
*/
private def processForTestingDetectedTarget(
target: Target,
turretGuid: PlanetSideGUID,
weaponGuid: PlanetSideGUID
): Option[(Target, Target)] = {
target match {
case target: Player =>
AutomatedTurretDispatch.Generic.testNewDetected(target, target.Name, turretGuid, weaponGuid)
Some((target, target))
case target: Vehicle =>
target.Seats.values
.flatMap(_.occupants)
.collectFirst { passenger =>
AutomatedTurretDispatch.Generic.testNewDetected(passenger, passenger.Name, turretGuid, weaponGuid)
(target, passenger)
}
case _ =>
None
}
}
/**
* Cull all targets that have been detected by this turret at some point
* by determining which targets are either destroyed
* or by determining which targets are too far away to be detected anymore.
* If there are no more available targets, cancel the timer that governs this evaluation.
* @return a list of somethings the turret can potentially shoot at that were removed
*/
private def performPeriodicTargetValidation(): List[Target] = {
val size = AutomatedTurretObject.Targets.size
val list = performDistanceCheck()
@ -272,6 +354,12 @@ trait AutomatedTurretBehavior {
list
}
/**
* Cull all targets that have been detected by this turret at some point
* by determining which targets are either destroyed
* or by determining which targets are too far away to be detected anymore.
* @return a list of somethings the turret can potentially shoot at that were removed
*/
private def performDistanceCheck(): List[Target] = {
//cull targets
val pos = AutomatedTurretObject.Position
@ -286,50 +374,73 @@ trait AutomatedTurretBehavior {
removedTargets
}
/**
* An important process loop in the target engagement and target management of an automated turret.
* If a target has been selected, perform a test to determine whether it remains the selected ("current") target.
* If there is no target selected, or the previous selected target was demoted from being selected,
* determine if enough time has passed before testing all available targets to find a new selected target.
*/
private def performCurrentTargetDecayCheck(): Unit = {
val now = System.currentTimeMillis()
val selectDelay = autoStats.map(_.targetSelectCooldown).getOrElse(3000L)
AutomatedTurretObject.Target
.collect { target =>
//test target
generalDecayCheck(
target,
now,
autoStats.map(_.targetEscapeRange).getOrElse(400f),
selectDelay,
autoStats.map(_.targetSelectCooldown).getOrElse(3000L),
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 >= 0) {
cancelSelfReportedAutoFire()
currentTargetLocation = None
if (automaticOperation && now - currentTargetLastShotTime >= 0) {
trySelectNewTarget()
}
None
}
}
/**
* An important process loop in the target engagement and target management of an automated turret.
* If a target has been selected, perform a test to determine whether it remains the selected ("current") target.
* If the target has been destroyed,
* moved beyond the turret's maximum engagement range,
* or has been missing for a certain amount of time,
* declare the the turret should no longer be shooting at (whatever) it (was).
* Apply appropriate cooldown 6to instruct the turret to wait before attempting to select a new current target.
* @param target something the turret can potentially shoot at
* @return something the turret can potentially shoot at
*/
private def generalDecayCheck(
target: Target,
now: Long,
escapeRange: Float,
selectDelay: Long,
cooldownDelay: Long,
eliminationDelay: Long
)(target: Target): Option[Target] = {
): Option[Target] = {
if (target.Destroyed) {
//if the target died while we were shooting at it
cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(target)
currentTargetLastShotReported = now + eliminationDelay
currentTargetLastShotTime = now + eliminationDelay
None
} else if (AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, target.Position, AutomatedTurretObject.Position, escapeRange)) {
//if the target made sufficient distance from the turret
cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(target)
currentTargetLastShotReported = now + cooldownDelay
currentTargetLastShotTime = now + cooldownDelay
None
} else if (now - currentTargetLastShotReported >= cooldownDelay) {
} else if (now - currentTargetLastShotTime >= cooldownDelay) {
//if the target goes mia through lack of response
trySelfReportedAutofireIfStationary() //certain targets can go "unresponsive" even though they should still be reachable
noLongerEngageDetectedTarget(target)
currentTargetLastShotReported = now + selectDelay
currentTargetLastShotTime = now + selectDelay
None
} else {
//continue shooting
@ -337,10 +448,25 @@ trait AutomatedTurretBehavior {
}
}
/**
* If there are no available targets,
* and no current target,
* stop the evaluation of available targets.
* @param beforeSize size of the list of available targets before some operation took place
* @return `true`, if the evaluation of available targets was stopped;
* `false`, otherwise
*/
private def testTargetListQualifications(beforeSize: Int): Boolean = {
beforeSize > 0 && AutomatedTurretObject.Targets.isEmpty && periodicValidationTest.cancel()
beforeSize > 0 && AutomatedTurretObject.Targets.isEmpty && cancelPeriodicTargetChecks()
}
/**
* If there is no current target,
* start or restart the evaluation of available targets.
* @param beforeSize size of the list of available targets before some operation took place
* @return `true`, if the evaluation of available targets was stopped;
* `false`, otherwise
*/
private def retimePeriodicTargetChecks(beforeSize: Int): Boolean = {
if (beforeSize == 0 && AutomatedTurretObject.Targets.nonEmpty && autoStats.isDefined) {
val repeated = autoStats.map(_.detectionSpeed).getOrElse(0.seconds)
@ -351,6 +477,10 @@ trait AutomatedTurretBehavior {
}
}
/**
* Start or restart the evaluation of available targets immediately.
* @param repeated delay in between evaluation periods
*/
private def retimePeriodicTargetChecks(repeated: FiniteDuration): Unit = {
periodicValidationTest.cancel()
periodicValidationTest = context.system.scheduler.scheduleWithFixedDelay(
@ -361,15 +491,39 @@ trait AutomatedTurretBehavior {
)
}
def automaticTurretPostStop(): Unit = {
/**
* Stop evaluation of available targets,
* including tests for targets being removed from selection for the current target,
* and tests whether the current target should remain a valid target.
* @return `true`, because we can not fail
* @see `Default.Cancellable`
*/
private def cancelPeriodicTargetChecks(): Boolean = {
periodicValidationTest.cancel()
AutomatedTurretObject.Target.foreach { noLongerEngageDetectedTarget }
periodicValidationTest = Default.Cancellable
true
}
/**
* Undo all the things, even the turret's knowledge of available targets.
* It's like nothing ever happened.
* @see `Actor.postStop`
*/
def automaticTurretPostStop(): Unit = {
resetAlerts()
AutomatedTurretObject.Targets.foreach { AutomatedTurretObject.RemoveTarget }
}
/**
* Retaliation is when a turret returns fire on a potential target that had just previously dealt damage to it.
* Occasionally, the turret will drop its current target for the retaliatory target.
* @param target something the turret can potentially shoot at
* @param cause information about the damaging incident that caused the turret to consider retaliation
* @return something the turret can potentially shoot at
*/
protected def attemptRetaliation(target: Target, cause: DamageResult): Option[Target] = {
if (automaticOperation && autoStats.exists(_.retaliatoryDuration > 0)) {
AutomatedTurretBehavior.getAttackerFromCause(target.Zone, cause).collect {
AutomatedTurretBehavior.getAttackVectorFromCause(target.Zone, cause).collect {
case attacker if attacker.Faction != target.Faction =>
performRetaliation(attacker)
attacker
@ -379,18 +533,120 @@ trait AutomatedTurretBehavior {
}
}
/**
* Retaliation is when a turret returns fire on a potential target that had just previously dealt damage to it.
* Occasionally, the turret will drop its current target for the retaliatory target.
* @param target something the turret can potentially shoot at
* @return something the turret can potentially shoot at
*/
private def performRetaliation(target: Target): Option[Target] = {
AutomatedTurretObject.Target
.collect {
case _ if autoStats.exists(_.retaliationOverridesTarget) =>
case existingTarget if autoStats.exists(_.retaliationOverridesTarget) =>
cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(existingTarget)
engageNewDetectedTarget(target)
target
case existingTarget =>
existingTarget
}
.orElse {
engageNewDetectedTarget(target)
Some(target)
}
}
/* Self-reporting automatic turret behavior */
/**
* Process confirmation shot feedback from self-reported automatic turret weapon fire.
* If the target has moved from the last time reported, cancel self-reported fire and revert to standard turret operation.
* Fire a normal test shot specifically at that target to determine if it is yet out of range.
* @param target something the turret can potentially shoot at
*/
private def movementCancelSelfReportingFireConfirmShot(target: Target): Unit = {
normalConfirmShot(target)
if (currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, target.Position) > 1f)) {
cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(target)
processForTestingDetectedTarget(
target,
AutomatedTurretObject.GUID,
AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID
)
} else {
tryStartSelfReportedAutofire(target)
}
}
/**
* If the target still is known to the turret,
* and if the target has not moved recently,
* but if none of the turret's projectiles have been confirmed shoots,
* it may still be reachable with weapons fire.
* Directly engage the target to simulate client perspective weapons fire damage.
* If you enter this mode, and the target can be damaged this way, the target needs to move in the game world to switch back.
* @return `true`, if the self-reporting test shot was discharged;
* `false`, otherwise
*/
private def trySelfReportedAutofireIfStationary(): Boolean = {
AutomatedTurretObject.Target
.collect {
case target if currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, target.Position) > 1f) =>
trySelfReportedAutofireTest(target)
}
.getOrElse(false)
}
/**
* Directly engage the target to simulate client perspective weapons fire damage.
* If you enter this mode, and the target can be damaged this way, the target needs to move in the game world to switch back.
* @return `true`, if the self-reporting test shot was discharged;
* `false`, otherwise
*/
private def trySelfReportedAutofireTest(target: Target): Boolean = {
if (selfReportedRefire.isCancelled) {
confirmShotFunc = movementCancelSelfReportingFireConfirmShot
target.Actor ! AffectedByAutomaticTurretFire.AiDamage(AutomatedTurretObject)
true
} else {
false
}
}
/**
* Directly engage the target to simulate client perspective weapons fire damage.
* If you enter this mode, and the target can be damaged this way, the target needs to move in the game world to switch out.
* @param target something the turret can potentially shoot at
* @return `true`, if the self-reporting operation was initiated;
* `false`, otherwise
*/
private def tryStartSelfReportedAutofire(target: Target): Boolean = {
if (selfReportedRefire.isCancelled) {
confirmShotFunc = movementCancelSelfReportingFireConfirmShot
selfReportedRefire = context.system.scheduler.scheduleWithFixedDelay(
0.seconds,
autoStats.map(_.refireTime).getOrElse(1.second),
target.Actor,
AffectedByAutomaticTurretFire.AiDamage(AutomatedTurretObject)
)
true
} else {
false
}
}
/**
* Stop directly communicating with a target to simulate weapons fire damage.
* @return `true`, because we can not fail
* @see `Default.Cancellable`
*/
private def cancelSelfReportedAutoFire(): Boolean = {
selfReportedRefire.cancel()
selfReportedRefire = Default.Cancellable
confirmShotFunc = normalConfirmShot
true
}
}
object AutomatedTurretBehavior {
@ -399,114 +655,97 @@ object AutomatedTurretBehavior {
final case class Unalert(target: Target)
final case class ConfirmShot(target: Target)
final case class ConfirmShot(target: Target, reporter: Option[SourceEntry] = None)
final case object Reset
private case object PeriodicCheck
trait AutomatedTurretDispatch {
private val noTargets :List[PlanetSideGUID] = List(Service.defaultPlayerGUID)
def getEventBus(target: Target): ActorRef
def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any
def startTracking(target: Target, channel: String, turretGuid: PlanetSideGUID, list: List[PlanetSideGUID]): Unit = {
getEventBus(target) ! composeMessageEnvelope(channel, startTrackingMsg(turretGuid, list))
}
def stopTracking(target: Target, channel: String, turretGuid: PlanetSideGUID): Unit = {
getEventBus(target) ! composeMessageEnvelope(channel, stopTrackingMsg(turretGuid))
}
def startShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
getEventBus(target) ! composeMessageEnvelope(channel, startShootingMsg(weaponGuid))
}
def stopShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
getEventBus(target) ! composeMessageEnvelope(channel, stopShootingMsg(weaponGuid))
}
def testNewDetected(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit = {
startTracking(target, channel, turretGuid, List(target.GUID))
startShooting(target, channel, weaponGuid)
stopShooting(target, channel, weaponGuid)
}
private def startTrackingMsg(guid: PlanetSideGUID, list: List[PlanetSideGUID]): PlanetSideGamePacket = {
ObjectDetectedMessage(guid, guid, 0, list)
}
private def stopTrackingMsg(turretGuid: PlanetSideGUID): PlanetSideGamePacket = {
ObjectDetectedMessage(turretGuid, turretGuid, 0, noTargets)
}
private def startShootingMsg(weaponGuid: PlanetSideGUID): PlanetSideGamePacket = {
ChangeFireStateMessage_Start(weaponGuid)
}
private def stopShootingMsg(weaponGuid: PlanetSideGUID): PlanetSideGamePacket = {
ChangeFireStateMessage_Stop(weaponGuid)
}
}
object Generic extends AutomatedTurretDispatch {
def getEventBus(target: Target): ActorRef = {
target.Zone.LocalEvents
}
def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any = {
LocalServiceMessage(channel, LocalAction.SendResponse(msg))
}
}
object Vehicle extends AutomatedTurretDispatch {
def getEventBus(target: Target): ActorRef = {
target.Zone.VehicleEvents
}
def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any = {
VehicleServiceMessage(channel, VehicleAction.SendResponse(Service.defaultPlayerGUID, msg))
}
}
/**
* Are we tracking a `Vehicle` entity?
* or, is it some other kind of entity?
* @param target something a turret can potentially shoot at
* @param channel scope of the message
* @param turretGuid turret
* @param list target's globally unique identifier, in list form
*/
def startTracking(target: Target, channel: String, turretGuid: PlanetSideGUID, list: List[PlanetSideGUID]): Unit = {
target match {
case v: Vehicle => Vehicle.startTracking(v, channel, turretGuid, list)
case _ => Generic.startTracking(target, channel, turretGuid, list)
case v: Vehicle => AutomatedTurretDispatch.Vehicle.startTracking(v, channel, turretGuid, list)
case _ => AutomatedTurretDispatch.Generic.startTracking(target, channel, turretGuid, list)
}
}
/**
* Are we no longer tracking a `Vehicle` entity?
* or, was it some other kind of entity?
* @param target something a turret can potentially shoot at
* @param channel scope of the message
* @param turretGuid turret
*/
def stopTracking(target: Target, channel: String, turretGuid: PlanetSideGUID): Unit = {
target match {
case v: Vehicle => Vehicle.stopTracking(v, channel, turretGuid)
case _ => Generic.stopTracking(target, channel, turretGuid)
case v: Vehicle => AutomatedTurretDispatch.Vehicle.stopTracking(v, channel, turretGuid)
case _ => AutomatedTurretDispatch.Generic.stopTracking(target, channel, turretGuid)
}
}
/**
* Are we shooting at a `Vehicle` entity?
* or, is it some other kind of entity?
* @param target something a turret can potentially shoot at
* @param channel scope of the message
* @param weaponGuid turret's weapon
*/
def startShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
target match {
case v: Vehicle => Vehicle.startShooting(v, channel, weaponGuid)
case _ => Generic.startShooting(target, channel, weaponGuid)
case v: Vehicle => AutomatedTurretDispatch.Vehicle.startShooting(v, channel, weaponGuid)
case _ => AutomatedTurretDispatch.Generic.startShooting(target, channel, weaponGuid)
}
}
/**
* Are we no longer shooting at a `Vehicle` entity?
* or, was it some other kind of entity?
* @param target something a turret can potentially shoot at
* @param channel scope of the message
* @param weaponGuid turret's weapon
*/
def stopShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
target match {
case v: Vehicle => Vehicle.stopShooting(v, channel, weaponGuid)
case _ => Generic.stopShooting(target, channel, weaponGuid)
case v: Vehicle => AutomatedTurretDispatch.Vehicle.stopShooting(v, channel, weaponGuid)
case _ => AutomatedTurretDispatch.Generic.stopShooting(target, channel, weaponGuid)
}
}
/**
* Will we be shooting at a `Vehicle` entity?
* or, will it be some other kind of entity?
* @param target something a turret can potentially shoot at
* @param channel scope of the message
* @param turretGuid turret
* @param weaponGuid turret's weapon
*/
def testNewDetected(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit = {
target match {
case v: Vehicle => Vehicle.testNewDetected(v, channel, turretGuid, weaponGuid)
case _ => Generic.testNewDetected(target, channel, turretGuid, weaponGuid)
case v: Vehicle => AutomatedTurretDispatch.Vehicle.testNewDetected(v, channel, turretGuid, weaponGuid)
case _ => AutomatedTurretDispatch.Generic.testNewDetected(target, channel, turretGuid, weaponGuid)
}
}
def getAttackerFromCause(zone: Zone, cause: DamageResult): Option[PlanetSideServerObject with Vitality] = {
/**
* Provided damage information and a zone in which the damage occurred,
* find a reference to the entity that caused the damage.
* The entity that caused the damage should also be damageable itself.<br>
* Very important: do not return the owner of the entity that caused the damage;
* return the cause of the damage.<br>
* Very important: does not properly trace damage from automatic weapons fire.
* @param zone where the damage occurred
* @param cause damage information
* @return entity that caused the damage
* @see `Vitality`
*/
def getAttackVectorFromCause(zone: Zone, cause: DamageResult): Option[PlanetSideServerObject with Vitality] = {
import net.psforever.objects.sourcing._
cause
.interaction
@ -541,20 +780,33 @@ object AutomatedTurretBehavior {
}
}
/**
* Perform special distance checks that are either spherical or cylindrical.
* Spherical distance checks are the default.
* @param stats check if doing cylindrical tests
* @param positionA one position in the game world
* @param positionB another position in the game world
* @param range input distance to test against
* @param result complies with standard `compareTo` operations;
* `foo.compareTo(bar)`,
* where "foo" is calculated using `Vector3.DistanceSquared` or the absolute value of the vertical distance,
* and "bar" is `range`-squared
* @return
*/
def shapedDistanceCheckAgainstValue(
stats: Option[Automation],
position: Vector3,
testPosition: Vector3,
testRange: Float,
result: Int = 1 //by default, calculation > input
): Boolean = {
val testRangeSq = testRange * testRange
stats: Option[Automation],
positionA: Vector3,
positionB: Vector3,
range: Float,
result: Int = 1 //by default, calculation > input
): Boolean = {
val testRangeSq = range * range
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
val height = range + stats.map(_.cylindricalHeight).getOrElse(0f)
math.abs(positionA.z - positionB.z).compareTo(height) == result &&
Vector3.DistanceSquared(positionA.xy, positionB.xy).compareTo(testRangeSq) == result
} else {
Vector3.DistanceSquared(position, testPosition).compareTo(testRangeSq) == result
Vector3.DistanceSquared(positionA, positionB).compareTo(testRangeSq) == result
}
}
}

View file

@ -0,0 +1,116 @@
// Copyright (c) 2024 PSForever
package net.psforever.objects.serverobject.turret
import akka.actor.ActorRef
import net.psforever.objects.serverobject.turret.AutomatedTurret.Target
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ObjectDetectedMessage}
import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.PlanetSideGUID
/**
* Dispatch messages from an `AutomatedTurret` entity's control agency
* with respects to the kind of entity which is the target.
* The main sticking point is that the message bus destination matches the type of message envelope.
* The packet messages utilized are the same either way and are tied to the action rather than the transmission process.
* @see `ChangeFireStateMessage_Start`
* @see `ChangeFireStateMessage_Stop`
* @see `ObjectDetectedMessage`
* @see `PlanetSideGamePacket`
* @see `Zone`
*/
trait AutomatedTurretDispatch {
/**
* The event bus should be accessible from the target's knowledge of their zone.
* @param target something the turret can potentially shoot at
* @return event bus to use
*/
def getEventBus(target: Target): ActorRef
/**
* The event bus should be accessible from the target's knowledge of their zone.
* @param channel the scope of the message transmission
* @param msg the packet to be dispatched
* @return messaging envelope to use
*/
def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any
/**
* Are we tracking an entity?
*/
def startTracking(target: Target, channel: String, turretGuid: PlanetSideGUID, list: List[PlanetSideGUID]): Unit = {
getEventBus(target) ! composeMessageEnvelope(channel, startTrackingMsg(turretGuid, list))
}
/**
* Are we no longer tracking an entity?
*/
def stopTracking(target: Target, channel: String, turretGuid: PlanetSideGUID): Unit = {
getEventBus(target) ! composeMessageEnvelope(channel, stopTrackingMsg(turretGuid))
}
/**
* Are we shooting at an entity?
*/
def startShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
getEventBus(target) ! composeMessageEnvelope(channel, startShootingMsg(weaponGuid))
}
/**
* Are we no longer shooting at an entity?
*/
def stopShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
getEventBus(target) ! composeMessageEnvelope(channel, stopShootingMsg(weaponGuid))
}
/**
* Will we be shooting at an entity?
*/
def testNewDetected(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit = {
startTracking(target, channel, turretGuid, List(target.GUID))
startShooting(target, channel, weaponGuid)
stopShooting(target, channel, weaponGuid)
}
private def startTrackingMsg(guid: PlanetSideGUID, list: List[PlanetSideGUID]): PlanetSideGamePacket = {
ObjectDetectedMessage(guid, guid, 0, list)
}
private def stopTrackingMsg(turretGuid: PlanetSideGUID): PlanetSideGamePacket = {
ObjectDetectedMessage(turretGuid, turretGuid, 0, AutomatedTurretDispatch.noTargets)
}
private def startShootingMsg(weaponGuid: PlanetSideGUID): PlanetSideGamePacket = {
ChangeFireStateMessage_Start(weaponGuid)
}
private def stopShootingMsg(weaponGuid: PlanetSideGUID): PlanetSideGamePacket = {
ChangeFireStateMessage_Stop(weaponGuid)
}
}
object AutomatedTurretDispatch {
private val noTargets: List[PlanetSideGUID] = List(Service.defaultPlayerGUID)
object Generic extends AutomatedTurretDispatch {
def getEventBus(target: Target): ActorRef = {
target.Zone.LocalEvents
}
def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any = {
LocalServiceMessage(channel, LocalAction.SendResponse(msg))
}
}
object Vehicle extends AutomatedTurretDispatch {
def getEventBus(target: Target): ActorRef = {
target.Zone.VehicleEvents
}
def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any = {
VehicleServiceMessage(channel, VehicleAction.SendResponse(Service.defaultPlayerGUID, msg))
}
}
}

View file

@ -4,6 +4,7 @@ package net.psforever.objects.serverobject.turret
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.structures.{Amenity, AmenityOwner, Building}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.types.Vector3
class FacilityTurret(tDef: FacilityTurretDefinition)
@ -11,7 +12,16 @@ class FacilityTurret(tDef: FacilityTurretDefinition)
with AutomatedTurret
with JammableUnit
with CaptureTerminalAware {
WeaponTurret.LoadDefinition(this)
WeaponTurret.LoadDefinition(turret = this)
def TurretOwner: SourceEntry = {
Seats
.headOption
.collect { case (_, a) => a }
.flatMap(_.occupant)
.map(SourceEntry(_))
.getOrElse(SourceEntry(Owner))
}
override def Owner: AmenityOwner = {
if (Zone.map.cavern) {

View file

@ -228,7 +228,7 @@ class FacilityTurretControl(turret: FacilityTurret)
AutomaticOperation = false
//look in direction of cause of jamming
val zone = JammableObject.Zone
AutomatedTurretBehavior.getAttackerFromCause(zone, cause).foreach {
AutomatedTurretBehavior.getAttackVectorFromCause(zone, cause).foreach {
attacker =>
val channel = zone.id
val guid = AutomatedTurretObject.GUID

View file

@ -11,20 +11,21 @@ import scala.collection.mutable
import scala.concurrent.duration._
final case class Automation(
targetDetectionRange: Float,
targetTriggerRange: Float,
targetEscapeRange: Float,
targetDetectionRange: Float, //m
targetTriggerRange: Float, //m
targetEscapeRange: Float, //m
targetValidation: List[PlanetSideGameObject => Boolean],
cylindricalCheck: Boolean = false,
cylindricalHeight: Float = 0,
retaliatoryDuration: Long = 0,
cylindricalHeight: Float = 0, //m
retaliatoryDuration: Long = 0, //ms
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
revertToDefaultFireMode: Boolean = true,
refireTime: FiniteDuration = 1.seconds //60rpm
) {
assert(targetDetectionRange > targetTriggerRange, "trigger range must be less than detection range")
}

View file

@ -20,6 +20,7 @@ import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.repair.RepairableVehicle
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.serverobject.turret.AffectedByAutomaticTurretFire
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
@ -57,7 +58,8 @@ class VehicleControl(vehicle: Vehicle)
with ContainableBehavior
with AggravatedBehavior
with RespondsToZoneEnvironment
with CargoBehavior {
with CargoBehavior
with AffectedByAutomaticTurretFire {
//make control actors belonging to utilities when making control actor belonging to vehicle
vehicle.Utilities.foreach { case (_, util) => util.Setup }
@ -70,6 +72,7 @@ class VehicleControl(vehicle: Vehicle)
def ContainerObject: Vehicle = vehicle
def InteractiveObject: Vehicle = vehicle
def CargoObject: Vehicle = vehicle
def AffectedObject: Vehicle = vehicle
SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater)
SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava)
@ -114,6 +117,7 @@ class VehicleControl(vehicle: Vehicle)
.orElse(containerBehavior)
.orElse(environmentBehavior)
.orElse(cargoBehavior)
.orElse(takeAutomatedDamage)
.orElse {
case Vehicle.Ownership(None) =>
LoseOwnership()