making specific target validation conditions for different auto turrets, also target blanking, and clarification of how the self-reporting mode cleansup after itself; wrote function documentation to make it all make sense (it doesn't)

This commit is contained in:
Fate-JH 2024-01-23 17:45:20 -05:00
parent 02ad42743c
commit 1ff0577db7
14 changed files with 416 additions and 157 deletions

View file

@ -3,8 +3,9 @@ package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
import net.psforever.objects.definition.ProjectileDefinition
import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.zones.Zoning
import net.psforever.objects.serverobject.turret.{AutomatedTurret, AutomatedTurretBehavior, VanuSentry}
import net.psforever.objects.serverobject.turret.VanuSentry
import net.psforever.objects.zones.exp.ToDatabase
import scala.collection.mutable
@ -470,7 +471,7 @@ private[support] class WeaponAndProjectileOperations(
val hitPos = target.Position + Vector3.z(value = 1f)
ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
addShotsLanded(resprojectile.cause.attribution, shots = 1)
//sessionData.handleDealingDamage(target, resprojectile)
sessionData.handleDealingDamage(target, resprojectile)
}
}
}

View file

@ -24,7 +24,7 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSiloDefinition
import net.psforever.objects.serverobject.structures.{AmenityDefinition, AutoRepairStats, BuildingDefinition, WarpGateDefinition}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalDefinition
import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalDefinition, ImplantTerminalMechDefinition}
import net.psforever.objects.serverobject.turret.{Automation, FacilityTurretDefinition, TurretUpgrade}
import net.psforever.objects.serverobject.turret.{Automation, AutoChecks, FacilityTurretDefinition, AutoRanges, TurretUpgrade}
import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, UtilityType, VehicleSubsystemEntry}
import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.damage._
@ -9074,11 +9074,19 @@ object GlobalDefinitions {
spitfire_turret.Model = ComplexDeployableResolutions.calculate
spitfire_turret.deployAnimation = DeployAnimation.Standard
spitfire_turret.AutoFire = Automation(
targetDetectionRange = 60f,
targetTriggerRange = 50f,
targetEscapeRange = 50f,
targetValidation = List(EffectTarget.Validation.PlayerOnRadar, EffectTarget.Validation.Vehicle),
retaliatoryDuration = 8000L,
AutoRanges(
detection = 75f,
trigger = 50f,
escape = 50f
),
AutoChecks(
validation = List(
EffectTarget.Validation.PlayerDetectedBySpitfireTurret,
EffectTarget.Validation.GroundVehicleDetectedByAutoTurret,
EffectTarget.Validation.AircraftDetectedByAutoTurret
)
),
retaliatoryDelay = 2000L, //8000L
refireTime = 200.milliseconds //150.milliseconds
)
spitfire_turret.innateDamage = new DamageWithPosition {
@ -9108,11 +9116,19 @@ object GlobalDefinitions {
spitfire_cloaked.deployAnimation = DeployAnimation.Standard
spitfire_cloaked.Model = ComplexDeployableResolutions.calculate
spitfire_cloaked.AutoFire = Automation(
targetDetectionRange = 50f,
targetTriggerRange = 30f,
targetEscapeRange = 50f,
targetValidation = List(EffectTarget.Validation.PlayerOnRadar, EffectTarget.Validation.VehiclesOnRadar),
retaliatoryDuration = 8000L,
AutoRanges(
detection = 75f,
trigger = 50f,
escape = 75f
),
AutoChecks(
validation = List(
EffectTarget.Validation.PlayerDetectedBySpitfireTurret,
EffectTarget.Validation.GroundVehicleDetectedByAutoTurret,
EffectTarget.Validation.AircraftDetectedByAutoTurret
)
),
retaliatoryDelay = 1L, //8000L
refireTime = 200.milliseconds //150.milliseconds
)
spitfire_cloaked.innateDamage = new DamageWithPosition {
@ -9142,15 +9158,19 @@ object GlobalDefinitions {
spitfire_aa.deployAnimation = DeployAnimation.Standard
spitfire_aa.Model = ComplexDeployableResolutions.calculate
spitfire_aa.AutoFire = Automation(
targetDetectionRange = 100f,
targetTriggerRange = 90f,
targetEscapeRange = 200f,
targetValidation = List(EffectTarget.Validation.AircraftOnRadar),
retaliatoryDuration = 2000L,
AutoRanges(
detection = 125f,
trigger = 100f,
escape = 200f
),
AutoChecks(
validation = List(EffectTarget.Validation.AircraftDetectedByAutoTurret)
),
retaliatoryDelay = 2000L, //8000L
retaliationOverridesTarget = false,
refireTime = 350.milliseconds, //300.milliseconds
cylindricalCheck = true,
cylindricalHeight = 25f
refireTime = 0.seconds, //300.milliseconds
cylindrical = true,
cylindricalExtraHeight = 50f
)
spitfire_aa.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
@ -10054,14 +10074,22 @@ object GlobalDefinitions {
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,
detectionSpeed = 2.seconds,
AutoRanges(
detection = 125f,
trigger = 100f,
escape = 200f
),
AutoChecks(
validation = List(
EffectTarget.Validation.MaxDetectedByAutoTurret,
EffectTarget.Validation.GroundVehicleDetectedByAutoTurret,
EffectTarget.Validation.AircraftDetectedByAutoTurret
)
),
retaliatoryDelay = 4000L, //8000L
cylindrical = true,
cylindricalExtraHeight = 50f,
detectionSweepTime = 2.seconds,
refireTime = 362.milliseconds //312.milliseconds
)
manned_turret.innateDamage = new DamageWithPosition {

View file

@ -12,8 +12,9 @@ import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
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.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.serverobject.turret.{MountableTurretControl, TurretDefinition, WeaponTurret}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance}
@ -31,10 +32,11 @@ class TurretDeployable(tdef: TurretDeployableDefinition)
def TurretOwner: SourceEntry = {
Seats
.values
.headOption
.collect { case (_, a) => a }
.flatMap(_.occupant)
.map(SourceEntry(_))
.map(p => PlayerSource.inSeat(PlayerSource(p), SourceEntry(this), seatNumber=0))
.orElse(Owners.map(PlayerSource(_, Position)))
.getOrElse(SourceEntry(this))
}
@ -86,6 +88,7 @@ class TurretControl(turret: TurretDeployable)
override def postStop(): Unit = {
super.postStop()
deployableBehaviorPostStop()
selfReportingDatabaseUpdate()
automaticTurretPostStop()
}
@ -111,7 +114,7 @@ class TurretControl(turret: TurretDeployable)
override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
val startsUnjammed = !JammableObject.Jammed
super.TryJammerEffectActivate(target, cause)
if (startsUnjammed && JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDuration > 0)) {
if (startsUnjammed && JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) {
AutomaticOperation = false
//look in direction of cause of jamming
val zone = JammableObject.Zone
@ -172,7 +175,7 @@ class TurretControl(turret: TurretDeployable)
override def finalizeDeployable(callback: ActorRef): Unit = {
super.finalizeDeployable(callback)
AutomaticOperation = true
AutomaticOperation = AutomaticOperationFunctionalityChecks
}
override def unregisterDeployable(obj: Deployable): Unit = {

View file

@ -3,7 +3,7 @@ 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.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.zones.blockmap.SectorPopulation
import net.psforever.objects.zones.{InteractsWithZone, ZoneInteraction, ZoneInteractionType}
import net.psforever.objects.sourcing.SourceEntry
@ -80,7 +80,7 @@ object InteractWithTurrets {
GlobalDefinitions.manned_turret
)
.flatMap(_.AutoFire)
.map(_.targetDetectionRange)
.map(_.ranges.detection)
.max
}
}

View file

@ -2,10 +2,11 @@
package net.psforever.objects.equipment
import net.psforever.objects._
import net.psforever.objects.ce.DeployableCategory
import net.psforever.objects.ce.{DeployableCategory, DeployedItem}
import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.vital.DamagingActivity
import net.psforever.types.{ExoSuitType, ImplantType}
import net.psforever.objects.zones.blockmap.SectorPopulation
import net.psforever.types.{DriveState, ExoSuitType, ImplantType, LatticeBenefit, PlanetSideEmpire, Vector3}
final case class TargetValidation(category: EffectTarget.Category.Value, test: EffectTarget.Validation.Value)
@ -188,47 +189,169 @@ object EffectTarget {
false
}
def PlayerOnRadar(target: PlanetSideGameObject): Boolean =
def PlayerDetectedBySpitfireTurret(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.Cloaked)) ||
p.Jumping
val now = System.currentTimeMillis()
val pos = p.Position
val faction = p.Faction
val sector = p.Zone.blockMap.sector(p.Position, range = 51f)
lazy val tookDamage = p.LastDamage.exists(dam => dam.adversarial.nonEmpty && now - dam.interaction.hitTime < 2000L)
//todo equipment-use usually a violation for any equipment type
lazy val usedEquipment = (p.Holsters().flatMap(_.Equipment) ++ p.Inventory.Items.map(_.obj))
.collect {
case t: Tool
if !(t.Projectile == GlobalDefinitions.no_projectile || t.Projectile.GrenadeProjectile || t.Size == EquipmentSize.Melee) =>
now - t.LastDischarge
}
.exists(_ < 2000L)
lazy val silentRunActive = p.avatar.implants.flatten.find(a => a.definition.implantType == ImplantType.SilentRun).exists(_.active)
lazy val movingFast = p.isMoving(test = 17d) || p.Jumping
p.VehicleSeated.isEmpty &&
(if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
else if (radarCloakedSensor(sector, pos, faction)) tookDamage || usedEquipment
else if (radarEnhancedInterlink(sector, pos, faction) || radarEnhancedSensor(sector, pos, faction)) true
else tookDamage || usedEquipment || !silentRunActive && movingFast)
case _ =>
false
})
def MaxOnRadar(target: PlanetSideGameObject): Boolean =
!target.Destroyed && (target match {
case p: Player =>
p.ExoSuit == ExoSuitType.MAX && p.isMoving(test = 17d)
case _ =>
false
})
def VehiclesOnRadar(target: PlanetSideGameObject): Boolean =
!target.Destroyed && (target match {
case v: Vehicle =>
val vdef = v.Definition
!(v.MountedIn.nonEmpty ||
v.Cloaked ||
GlobalDefinitions.isAtvVehicle(vdef) ||
vdef == GlobalDefinitions.two_man_assault_buggy ||
vdef == GlobalDefinitions.skyguard)
case _ =>
false
})
def AircraftOnRadar(target: PlanetSideGameObject): Boolean =
def PlayerUndetectedByAutoTurret(target: PlanetSideGameObject): Boolean =
target match {
case v: Vehicle =>
GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 && !v.Cloaked
case p: Player =>
val pos = p.Position
val sector = p.Zone.blockMap.sector(p.Position, range = 51f)
p.VehicleSeated.nonEmpty || radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)
case _ =>
false
}
def MaxDetectedByAutoTurret(target: PlanetSideGameObject): Boolean =
!target.Destroyed && (target match {
case p: Player =>
val now = System.currentTimeMillis()
val pos = p.Position
val faction = p.Faction
val sector = p.Zone.blockMap.sector(p.Position, range = 51f)
lazy val tookDamage = p.LastDamage.exists(dam => dam.adversarial.nonEmpty && now - dam.interaction.hitTime < 2000L)
lazy val usedEquipment = p.Holsters().flatMap(_.Equipment)
.collect { case t: Tool => now - t.LastDischarge }
.exists(_ < 2000L)
lazy val movingFast = p.isMoving(test = 17d)
p.ExoSuit == ExoSuitType.MAX &&
p.VehicleSeated.isEmpty &&
(if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
else if (radarCloakedSensor(sector, pos, faction)) tookDamage || usedEquipment
else if (radarEnhancedInterlink(sector, pos, faction) || radarEnhancedSensor(sector, pos, faction)) true
else tookDamage || usedEquipment || movingFast)
case _ =>
false
})
def GroundVehicleDetectedByAutoTurret(target: PlanetSideGameObject): Boolean =
!target.Destroyed && (target match {
case v: Vehicle =>
val now = System.currentTimeMillis()
val vdef = v.Definition
lazy val tookDamage = v.LastDamage.exists(dam => dam.adversarial.nonEmpty && now - dam.interaction.hitTime< 2000L)
lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
.collect { case t: Tool => now - t.LastDischarge }
.exists(_ < 2000L)
!GlobalDefinitions.isFlightVehicle(vdef) && v.MountedIn.isEmpty && (
if (vdef == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) false
else if (
v.Cloaked ||
GlobalDefinitions.isAtvVehicle(vdef) ||
vdef == GlobalDefinitions.two_man_assault_buggy ||
vdef == GlobalDefinitions.skyguard
) tookDamage || usedEquipment
else true)
case _ =>
false
})
def VehicleUndetectedByAutoTurret(target: PlanetSideGameObject): Boolean =
target match {
case v: Vehicle =>
(v.Definition == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) || v.MountedIn.nonEmpty || v.Cloaked
case _ =>
false
}
def AircraftDetectedByAutoTurret(target: PlanetSideGameObject): Boolean =
!target.Destroyed && (target match {
case v: Vehicle =>
GlobalDefinitions.isFlightVehicle(v.Definition) && !v.Cloaked
case _ =>
false
})
}
private def radarEnhancedInterlink(
sector: SectorPopulation,
position: Vector3,
faction: PlanetSideEmpire.Value
): Boolean = {
sector.buildingList.collect {
case b =>
b.Faction != faction &&
b.hasLatticeBenefit(LatticeBenefit.InterlinkFacility) &&
Vector3.DistanceSquared(b.Position, position).toDouble < math.pow(b.Definition.SOIRadius.toDouble, 2d)
}.contains(true)
}
private def radarEnhancedSensor(
sector: SectorPopulation,
position: Vector3,
faction: PlanetSideEmpire.Value
): Boolean = {
sector.deployableList.collect {
case d: SensorDeployable =>
!d.Destroyed &&
d.Definition.Item == DeployedItem.motionalarmsensor &&
d.Faction != faction &&
!d.Jammed && Vector3.DistanceSquared(d.Position, position) < 2500f
}.contains(true)
}
private def radarCloakedAms(
sector: SectorPopulation,
position: Vector3
): Boolean = {
sector.vehicleList.collect {
case v =>
!v.Destroyed &&
v.Definition == GlobalDefinitions.ams &&
v.DeploymentState == DriveState.Deployed &&
!v.Jammed &&
Vector3.DistanceSquared(v.Position, position) < 144f
}.contains(true)
}
private def radarCloakedAegis(
sector: SectorPopulation,
position: Vector3
): Boolean = {
sector.deployableList.collect {
case d: ShieldGeneratorDeployable =>
!d.Destroyed &&
!d.Jammed &&
Vector3.DistanceSquared(d.Position, position) < 100f
}.contains(true)
}
private def radarCloakedSensor(
sector: SectorPopulation,
position: Vector3,
faction: PlanetSideEmpire.Value
): Boolean = {
sector.deployableList.collect {
case d: SensorDeployable =>
!d.Destroyed &&
d.Definition.Item == DeployedItem.sensor_shield &&
d.Faction == faction &&
!d.Jammed &&
Vector3.DistanceSquared(d.Position, position) < 900f
}.contains(true)
}
}

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.serverobject.turret.auto.AutomatedTurret
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.types.Vector3

View file

@ -10,6 +10,7 @@ import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.repair.AmenityAutoRepair
import net.psforever.objects.serverobject.structures.PoweredAmenityControl
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior
import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.packet.game.ChangeFireModeMessage
import net.psforever.services.Service
@ -39,6 +40,8 @@ class FacilityTurretControl(turret: FacilityTurret)
private var testToResetToDefaultFireMode: Boolean = false
AutomaticOperation = AutomaticOperationFunctionalityChecks
override def postStop(): Unit = {
super.postStop()
damageableWeaponTurretPostStop()
@ -126,6 +129,7 @@ class FacilityTurretControl(turret: FacilityTurret)
super.DestructionAwareness(target, cause)
tryAutoRepair()
AutomaticOperation = false
selfReportingCleanUp()
}
override def PerformRepairs(target : Damageable.Target, amount : Int) : Int = {
@ -224,7 +228,7 @@ class FacilityTurretControl(turret: FacilityTurret)
override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
val startsUnjammed = !JammableObject.Jammed
super.TryJammerEffectActivate(target, cause)
if (startsUnjammed && JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDuration > 0)) {
if (startsUnjammed && JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) {
AutomaticOperation = false
//look in direction of cause of jamming
val zone = JammableObject.Zone

View file

@ -11,25 +11,60 @@ import scala.collection.mutable
import scala.concurrent.duration._
final case class Automation(
targetDetectionRange: Float, //m
targetTriggerRange: Float, //m
targetEscapeRange: Float, //m
targetValidation: List[PlanetSideGameObject => Boolean],
cylindricalCheck: Boolean = false,
cylindricalHeight: Float = 0, //m
retaliatoryDuration: Long = 0, //ms
ranges: AutoRanges,
checks: AutoChecks,
/** the boundary for target searching is typically a sphere of `ranges.detection` radius;
* instead, takes the shape of a cylinder of `ranges.detection` radius and height */
cylindrical: Boolean = false,
/** if target searching is performed in the shape of a cylinder,
* add height on top of the cylinder's normal height */
cylindricalExtraHeight: Float = 0, //m
/** how long after the last target engagement
* or how long into the current target engagement
* before the turret may counterattack damage;
* set to `0L` to never retaliate */
retaliatoryDelay: Long = 0, //ms
/** if the turret has a current target,
* allow for retaliation against a different target */
retaliationOverridesTarget: Boolean = true,
initialDetectionSpeed: FiniteDuration = Duration.Zero,
detectionSpeed: FiniteDuration = 1.seconds,
targetSelectCooldown: Long = 1500L, //ms
missedShotCooldown: Long = 3000L, //ms
targetEliminationCooldown: Long = 0L, //ms
/** frequency at which the turret will test target for reachability */
detectionSweepTime: FiniteDuration = 1.seconds,
cooldowns: AutoCooldowns = AutoCooldowns(),
/** if the turret weapon has multiple fire modes,
* revert to the base fire mode before engaging in target testing or other automatic operations */
revertToDefaultFireMode: Boolean = true,
/** the simulated weapon fire rate for self-reporting (internal damage loop) */
refireTime: FiniteDuration = 1.seconds //60rpm
)
final case class AutoRanges(
/** distance at which a target is first noticed */
detection: Float, //m
/** distance at which the target is tested */
trigger: Float, //m
/** distance away from the source of damage before the turret stops engaging */
escape: Float //m
) {
assert(targetDetectionRange > targetTriggerRange, "trigger range must be less than detection range")
assert(detection >= trigger, "detection range must be greater than or equal to trigger range")
assert(escape >= trigger, "escape range must be greater than or equal to trigger range")
}
final case class AutoChecks(
/** reasons why this target should be engaged */
validation: List[PlanetSideGameObject => Boolean],
/** reasons why an ongoing target engagement should be stopped */
blanking: List[PlanetSideGameObject => Boolean] = Nil
)
final case class AutoCooldowns(
/** when the target gets switched (generic) */
targetSelect: Long = 1500L, //ms
/** when the target escapes being damaged */
missedShot: Long = 3000L, //ms
/** when the target gets destroyed during an ongoing engagement */
targetElimination: Long = 0L //ms
)
/**
* The definition for any `WeaponTurret`.
*/

View file

@ -1,5 +1,5 @@
// Copyright (c) 2024 PSForever
package net.psforever.objects.serverobject.turret
package net.psforever.objects.serverobject.turret.auto
import akka.actor.Actor
import net.psforever.objects.Tool
@ -20,23 +20,22 @@ import net.psforever.types.Vector3
*/
trait AffectedByAutomaticTurretFire extends Damageable {
_: Actor =>
def AffectedObject: AutomatedTurret.Target
val takeAutomatedDamage: Receive = {
case AffectedByAutomaticTurretFire.AiDamage(turret) =>
case AiDamage(turret) =>
performAutomatedDamage(turret)
}
private def performAutomatedDamage(turret: AutomatedTurret): Unit = {
protected def performAutomatedDamage(turret: AutomatedTurret): Unit = {
val target = AffectedObject
val tool = turret.Weapons.values.head.Equipment.collect { case t : Tool => t }.get
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)))
turret.Actor ! SelfReportedConfirmShot(target)
val projectile = new Projectile(
projectileInfo,
tool.Definition,
@ -63,7 +62,3 @@ trait AffectedByAutomaticTurretFire extends Damageable {
PerformDamage(target, resolvedProjectile.calculate())
}
}
object AffectedByAutomaticTurretFire {
case class AiDamage(turret: AutomatedTurret)
}

View file

@ -1,8 +1,9 @@
// Copyright (c) 2024 PSForever
package net.psforever.objects.serverobject.turret
package net.psforever.objects.serverobject.turret.auto
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret}
import net.psforever.objects.sourcing.{SourceEntry, SourceUniqueness}
import net.psforever.objects.vital.Vitality
@ -14,6 +15,12 @@ trait AutomatedTurret
private var targets: List[Target] = List[Target]()
/**
* The entity that claims responsibility for the actions of the turret
* or has authoritative management over the turret.
* When no one else steps up to the challenge, the turret can be its own person.
* @return owner entity
*/
def TurretOwner: SourceEntry
def Target: Option[Target] = currentTarget

View file

@ -1,14 +1,19 @@
// Copyright (c) 2024 PSForever
package net.psforever.objects.serverobject.turret
package net.psforever.objects.serverobject.turret.auto
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.{Default, Player, Vehicle}
import net.psforever.objects.avatar.scoring.EquipmentStat
import net.psforever.objects.equipment.EffectTarget
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.DamageableEntity
import net.psforever.objects.sourcing.{SourceEntry, SourceUniqueness}
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.turret.Automation
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceUniqueness}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.exp.ToDatabase
import net.psforever.objects.zones.{InteractsWithZone, Zone}
import net.psforever.objects.{Default, PlanetSideGameObject, Player, Vehicle}
import net.psforever.types.{PlanetSideGUID, Vector3}
import scala.concurrent.ExecutionContext.Implicits.global
@ -23,20 +28,26 @@ trait AutomatedTurretBehavior {
private var automaticOperation: Boolean = false
/** quick reference of the current target, if any */
private var currentTargetToken: Option[SourceUniqueness] = None
/** time of the current target's selection or the last target's selection */
private var currentTargetSwitchTime: Long = 0L
/** 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
/** self-reported weapon fire produces projectiles that were shot;
* due to the call and response nature of this mode, they also count as shots that were landed */
private var shotsFired: Int = 0
/** self-reported weapon fire produces targets that were eliminated;
* due to the call and response nature of this mode, they also count as shots that were landed;
* this may duplicate information processed during some other database update call */
private var targetsDestroyed: Int = 0
def AutomatedTurretObject: AutomatedTurret
@ -44,11 +55,11 @@ trait AutomatedTurretBehavior {
case AutomatedTurretBehavior.Alert(target) =>
bringAttentionToTarget(target)
case AutomatedTurretBehavior.ConfirmShot(target, None) =>
confirmShotFunc(target)
case AutomatedTurretBehavior.ConfirmShot(target, _) =>
confirmShotFunc(target)
normalConfirmShot(target)
case SelfReportedConfirmShot(target) =>
movementCancelSelfReportingFireConfirmShot(target)
case AutomatedTurretBehavior.Unalert(target) =>
disregardTarget(target)
@ -166,8 +177,6 @@ trait AutomatedTurretBehavior {
AutomatedTurretObject.Target.foreach(noLongerEngageDetectedTarget)
AutomatedTurretObject.Target = None
AutomatedTurretObject.Clear()
previousTestedTargets.foreach(noLongerEngageTestedTarget)
previousTestedTargets = Set()
currentTargetToken = None
currentTargetLocation = None
}
@ -181,18 +190,26 @@ trait AutomatedTurretBehavior {
* 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
* @return `true`, if the target submitted was recognized by the turret;
* `false`, if the target can not be the current target
*/
private def normalConfirmShot(target: Target): Unit = {
private def normalConfirmShot(target: Target): Boolean = {
val now = System.currentTimeMillis()
if (currentTargetToken.isEmpty) {
currentTargetLastShotTime = now
currentTargetLocation = Some(target.Position)
cancelSelfReportedAutoFire()
engageNewDetectedTarget(target)
true
} else if (
currentTargetToken.contains(SourceEntry(target).unique) &&
now - currentTargetLastShotTime < autoStats.map(_.missedShotCooldown).getOrElse(0L)) {
now - currentTargetLastShotTime < autoStats.map(_.cooldowns.missedShot).getOrElse(0L)) {
currentTargetLastShotTime = now
currentTargetLocation = Some(target.Position)
cancelSelfReportedAutoFire()
true
} else {
false
}
}
@ -207,10 +224,9 @@ trait AutomatedTurretBehavior {
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)
currentTargetSwitchTime = System.currentTimeMillis()
AutomatedTurretObject.Target = target
AutomatedTurretBehavior.startTracking(target, zoneid, AutomatedTurretObject.GUID, List(target.GUID))
AutomatedTurretBehavior.startShooting(target, zoneid, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
@ -280,30 +296,20 @@ trait AutomatedTurretBehavior {
val turretPosition = AutomatedTurretObject.Position
val turretGuid = AutomatedTurretObject.GUID
val weaponGuid = AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID
val radius = autoStats.get.targetTriggerRange
val validation = autoStats.get.targetValidation
val radius = autoStats.get.ranges.trigger
val validation = autoStats.get.checks.validation
val faction = AutomatedTurretObject.Faction
//noinspection CollectHeadOption
val (targets, forValidation) = AutomatedTurretObject
val selectedTargets = AutomatedTurretObject
.Targets
.collect { case target
if /*target.Faction != faction &&*/
AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, target.Position, turretPosition, radius, result = -1) &&
validation.exists(func => func(target)) =>
validation.exists(func => func(target))=>
target
}
.sortBy(target => Vector3.DistanceSquared(target.Position, turretPosition))
.flatMap { processForTestingDetectedTarget(_, turretGuid, weaponGuid) }
.unzip
//call an explicit stop for these targets
(for {
a <- forValidation
b <- previousTestedTargets
if SourceEntry(a).unique != SourceEntry(b).unique
} yield b)
.foreach(noLongerEngageTestedTarget)
previousTestedTargets = forValidation.toSet
targets.headOption
selectedTargets.foreach(processForTestingDetectedTarget(_, turretGuid, weaponGuid))
selectedTargets.headOption
}
}
@ -363,7 +369,7 @@ trait AutomatedTurretBehavior {
private def performDistanceCheck(): List[Target] = {
//cull targets
val pos = AutomatedTurretObject.Position
val range = autoStats.map(_.targetDetectionRange).getOrElse(0f)
val range = autoStats.map(_.ranges.detection).getOrElse(0f)
val removedTargets = AutomatedTurretObject.Targets
.collect {
case t: InteractsWithZone
@ -388,10 +394,10 @@ trait AutomatedTurretBehavior {
generalDecayCheck(
target,
now,
autoStats.map(_.targetEscapeRange).getOrElse(400f),
autoStats.map(_.targetSelectCooldown).getOrElse(3000L),
autoStats.map(_.missedShotCooldown).getOrElse(3000L),
autoStats.map(_.targetEliminationCooldown).getOrElse(0L)
autoStats.map(_.ranges.escape).getOrElse(400f),
autoStats.map(_.cooldowns.targetSelect).getOrElse(3000L),
autoStats.map(_.cooldowns.missedShot).getOrElse(3000L),
autoStats.map(_.cooldowns.targetElimination).getOrElse(0L)
)
}
.orElse {
@ -409,10 +415,11 @@ trait AutomatedTurretBehavior {
* 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,
* no longer qualifies as a target due to an internal or external change,
* has 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.
* Apply appropriate cooldown to 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
*/
@ -425,11 +432,17 @@ trait AutomatedTurretBehavior {
eliminationDelay: Long
): Option[Target] = {
if (target.Destroyed) {
//if the target died while we were shooting at it
//if the target died or is no longer considered a valid target while we were shooting at it
cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(target)
currentTargetLastShotTime = now + eliminationDelay
None
} else if ((AutomatedTurretBehavior.commonBlanking ++ autoStats.map(_.checks.blanking).getOrElse(Nil)).exists(func => func(target))) {
//if the target, while being engaged, stops counting as a valid target
cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(target)
currentTargetLastShotTime = now + selectDelay
None
} else if (AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, target.Position, AutomatedTurretObject.Position, escapeRange)) {
//if the target made sufficient distance from the turret
cancelSelfReportedAutoFire()
@ -469,7 +482,7 @@ trait AutomatedTurretBehavior {
*/
private def retimePeriodicTargetChecks(beforeSize: Int): Boolean = {
if (beforeSize == 0 && AutomatedTurretObject.Targets.nonEmpty && autoStats.isDefined) {
val repeated = autoStats.map(_.detectionSpeed).getOrElse(0.seconds)
val repeated = autoStats.map(_.detectionSweepTime).getOrElse(1.seconds)
retimePeriodicTargetChecks(repeated)
true
} else {
@ -509,9 +522,32 @@ trait AutomatedTurretBehavior {
* It's like nothing ever happened.
* @see `Actor.postStop`
*/
def automaticTurretPostStop(): Unit = {
protected def automaticTurretPostStop(): Unit = {
resetAlerts()
AutomatedTurretObject.Targets.foreach { AutomatedTurretObject.RemoveTarget }
selfReportingCleanUp()
}
/**
* Cleanup for the variables involved in self-reporting.
* Set them to zero.
*/
protected def selfReportingCleanUp(): Unit = {
shotsFired = 0
targetsDestroyed = 0
}
/**
* The self-reporting mode for automatic turrets produces weapon fire data that should be sent to the database.
* The targets destroyed from self-reported fire are also logged to the database.
*/
protected def selfReportingDatabaseUpdate(): Unit = {
AutomatedTurretObject.TurretOwner match {
case p: PlayerSource =>
val weaponId = AutomatedTurretObject.Weapons.values.head.Equipment.map(_.Definition.ObjectId).getOrElse(0)
ToDatabase.reportToolDischarge(p.CharId, EquipmentStat(weaponId, shotsFired, shotsFired, targetsDestroyed, 0))
case _ => ()
}
}
/**
@ -522,7 +558,11 @@ trait AutomatedTurretBehavior {
* @return something the turret can potentially shoot at
*/
protected def attemptRetaliation(target: Target, cause: DamageResult): Option[Target] = {
if (automaticOperation && autoStats.exists(_.retaliatoryDuration > 0)) {
if (
automaticOperation &&
!currentTargetToken.contains(SourceEntry(target).unique) &&
autoStats.exists(_.retaliatoryDelay > 0)
) {
AutomatedTurretBehavior.getAttackVectorFromCause(target.Zone, cause).collect {
case attacker if attacker.Faction != target.Faction =>
performRetaliation(attacker)
@ -542,7 +582,11 @@ trait AutomatedTurretBehavior {
private def performRetaliation(target: Target): Option[Target] = {
AutomatedTurretObject.Target
.collect {
case existingTarget if autoStats.exists(_.retaliationOverridesTarget) =>
case existingTarget
if autoStats.exists { auto =>
auto.retaliationOverridesTarget &&
currentTargetSwitchTime + auto.retaliatoryDelay > System.currentTimeMillis()
} =>
cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(existingTarget)
engageNewDetectedTarget(target)
@ -565,7 +609,14 @@ trait AutomatedTurretBehavior {
* @param target something the turret can potentially shoot at
*/
private def movementCancelSelfReportingFireConfirmShot(target: Target): Unit = {
normalConfirmShot(target)
currentTargetLastShotTime = System.currentTimeMillis()
shotsFired += 1
target match {
case v: Damageable with Mountable
if v.Destroyed && v.Seats.values.exists(_.isOccupied) =>
targetsDestroyed += 1
case _ => ()
}
if (currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, target.Position) > 1f)) {
cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(target)
@ -575,7 +626,7 @@ trait AutomatedTurretBehavior {
AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID
)
} else {
tryStartSelfReportedAutofire(target)
tryPerformSelfReportedAutofire(target)
}
}
@ -592,7 +643,9 @@ trait AutomatedTurretBehavior {
private def trySelfReportedAutofireIfStationary(): Boolean = {
AutomatedTurretObject.Target
.collect {
case target if currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, target.Position) > 1f) =>
case target
if currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, target.Position) > 1f) &&
autoStats.exists(_.refireTime > 0.seconds) =>
trySelfReportedAutofireTest(target)
}
.getOrElse(false)
@ -606,8 +659,7 @@ trait AutomatedTurretBehavior {
*/
private def trySelfReportedAutofireTest(target: Target): Boolean = {
if (selfReportedRefire.isCancelled) {
confirmShotFunc = movementCancelSelfReportingFireConfirmShot
target.Actor ! AffectedByAutomaticTurretFire.AiDamage(AutomatedTurretObject)
target.Actor ! AiDamage(AutomatedTurretObject)
true
} else {
false
@ -621,14 +673,13 @@ trait AutomatedTurretBehavior {
* @return `true`, if the self-reporting operation was initiated;
* `false`, otherwise
*/
private def tryStartSelfReportedAutofire(target: Target): Boolean = {
private def tryPerformSelfReportedAutofire(target: Target): Boolean = {
if (selfReportedRefire.isCancelled) {
confirmShotFunc = movementCancelSelfReportingFireConfirmShot
selfReportedRefire = context.system.scheduler.scheduleWithFixedDelay(
0.seconds,
autoStats.map(_.refireTime).getOrElse(1.second),
autoStats.map(_.refireTime).getOrElse(1.seconds),
target.Actor,
AffectedByAutomaticTurretFire.AiDamage(AutomatedTurretObject)
AiDamage(AutomatedTurretObject)
)
true
} else {
@ -638,13 +689,13 @@ trait AutomatedTurretBehavior {
/**
* Stop directly communicating with a target to simulate weapons fire damage.
* Utilized as a p[art of the auto-fire reset process.
* @return `true`, because we can not fail
* @see `Default.Cancellable`
*/
private def cancelSelfReportedAutoFire(): Boolean = {
selfReportedRefire.cancel()
selfReportedRefire = Default.Cancellable
confirmShotFunc = normalConfirmShot
true
}
}
@ -661,6 +712,11 @@ object AutomatedTurretBehavior {
private case object PeriodicCheck
final val commonBlanking: List[PlanetSideGameObject => Boolean] = List(
EffectTarget.Validation.PlayerUndetectedByAutoTurret,
EffectTarget.Validation.VehicleUndetectedByAutoTurret
)
/**
* Are we tracking a `Vehicle` entity?
* or, is it some other kind of entity?
@ -801,8 +857,8 @@ object AutomatedTurretBehavior {
result: Int = 1 //by default, calculation > input
): Boolean = {
val testRangeSq = range * range
if (stats.exists(_.cylindricalCheck)) {
val height = range + stats.map(_.cylindricalHeight).getOrElse(0f)
if (stats.exists(_.cylindrical)) {
val height = range + stats.map(_.cylindricalExtraHeight).getOrElse(0f)
math.abs(positionA.z - positionB.z).compareTo(height) == result &&
Vector3.DistanceSquared(positionA.xy, positionB.xy).compareTo(testRangeSq) == result
} else {

View file

@ -1,8 +1,8 @@
// Copyright (c) 2024 PSForever
package net.psforever.objects.serverobject.turret
package net.psforever.objects.serverobject.turret.auto
import akka.actor.ActorRef
import net.psforever.objects.serverobject.turret.AutomatedTurret.Target
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret.Target
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ObjectDetectedMessage}
import net.psforever.services.Service

View file

@ -0,0 +1,6 @@
// Copyright (c) 2024 PSForever
package net.psforever.objects.serverobject.turret.auto
private[auto] case class AiDamage(turret: AutomatedTurret)
private[auto] case class SelfReportedConfirmShot(target: AutomatedTurret.Target)

View file

@ -20,7 +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.serverobject.turret.auto.AffectedByAutomaticTurretFire
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}