introduced definition properties to configure auto fire; interspersed properties into relevant files; non-squared velocity check for isMoving

This commit is contained in:
Fate-JH 2023-12-13 23:21:28 -05:00
parent 2e84b33a47
commit 18c3162dfe
10 changed files with 199 additions and 66 deletions

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.{FacilityTurretDefinition, TurretUpgrade}
import net.psforever.objects.serverobject.turret.{Automation, FacilityTurretDefinition, TurretUpgrade}
import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, UtilityType, VehicleSubsystemEntry}
import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.damage._
@ -1913,6 +1913,20 @@ object GlobalDefinitions {
}
}
/**
* Using the definition for a `Vehicle` determine whether it is an all-terrain vehicle type.
* @param vdef the `VehicleDefinition` of the vehicle
* @return `true`, if it is; `false`, otherwise
*/
def isAtvVehicle(vdef: VehicleDefinition): Boolean = {
vdef match {
case `quadassault` | `fury` | `quadstealth` =>
true
case _ =>
false
}
}
/**
* Using the definition for a `Vehicle` determine whether it can fly.
* Does not count the flying battleframe robotics vehicles.
@ -9059,6 +9073,11 @@ object GlobalDefinitions {
spitfire_turret.DeployTime = Duration.create(5000, "ms")
spitfire_turret.Model = ComplexDeployableResolutions.calculate
spitfire_turret.deployAnimation = DeployAnimation.Standard
spitfire_turret.AutoFire = Automation(
targetingRange = 40f,
targetValidation = List(EffectTarget.Validation.ObviousPlayer, EffectTarget.Validation.Vehicle),
retaliatoryDuration = 8000L
)
spitfire_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 200
@ -9085,6 +9104,11 @@ object GlobalDefinitions {
spitfire_cloaked.DeployTime = Duration.create(5000, "ms")
spitfire_cloaked.deployAnimation = DeployAnimation.Standard
spitfire_cloaked.Model = ComplexDeployableResolutions.calculate
spitfire_cloaked.AutoFire = Automation(
targetingRange = 40f,
targetValidation = List(EffectTarget.Validation.ObviousPlayer, EffectTarget.Validation.Vehicle),
retaliatoryDuration = 8000L
)
spitfire_cloaked.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 50
@ -9111,6 +9135,11 @@ object GlobalDefinitions {
spitfire_aa.DeployTime = Duration.create(5000, "ms")
spitfire_aa.deployAnimation = DeployAnimation.Standard
spitfire_aa.Model = ComplexDeployableResolutions.calculate
spitfire_aa.AutoFire = Automation(
targetingRange = 80f,
targetValidation = List(EffectTarget.Validation.Aircraft),
retaliatoryDuration = 8000L
)
spitfire_aa.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 200

View file

@ -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 = 25f))
interaction(new InteractWithTurrets(range = 100f))
interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))
private var backpack: Boolean = false

View file

@ -108,9 +108,9 @@ class TurretControl(turret: TurretDeployable)
override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
val startsUnjammed = !JammableObject.Jammed
super.TryJammerEffectActivate(target, cause)
if (startsUnjammed && JammableObject.Jammed) {
if (startsUnjammed && JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDuration > 0)) {
AutomaticOperation = false
//look in direction of source of jamming
//look in direction of cause of jamming
val zone = JammableObject.Zone
TurretControl.getAttackerFromCause(zone, cause).foreach {
attacker =>
@ -129,10 +129,10 @@ class TurretControl(turret: TurretDeployable)
}
override protected def DamageAwareness(target: Target, cause: DamageResult, amount: Any): Unit = {
//turret retribution
if (AutomaticOperation) {
TurretControl.getAttackerFromCause(target.Zone, cause).foreach {
attacker =>
if (AutomaticOperation && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDuration > 0)) {
//turret retribution
TurretControl.getAttackerFromCause(target.Zone, cause).collect {
case attacker if attacker.Faction != target.Faction =>
engageNewDetectedTarget(attacker)
}
}
@ -153,7 +153,7 @@ class TurretControl(turret: TurretDeployable)
val seats = turret.Seats.values
//either we have no seats or no one gets to sit
val retime = if (seats.count(_.isOccupied) > 0) {
//unlike with vehicles, it's possible to request deconstruction of one's own field turret while seated in it
//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 {

View file

@ -25,7 +25,7 @@ class InteractWithTurrets(val range: Float)
case clarifiedTarget: AutomatedTurret.Target =>
val posxy = clarifiedTarget.Position.xy
val unique = SourceEntry(clarifiedTarget).unique
val targets = getTurretTargets(sector, posxy).filter { turret => turret.Detected(unique).isEmpty }
val targets = getTurretTargets(sector, posxy).filter { turret => turret.Definition.AutoFire.nonEmpty && turret.Detected(unique).isEmpty }
targets.foreach { t => t.Actor ! AutomatedTurretBehavior.Alert(clarifiedTarget) }
case _ => ()
}

View file

@ -32,10 +32,16 @@ trait WorldEntity {
def isMoving(test: Vector3): Boolean = WorldEntity.isMoving(Velocity, test)
/**
* This object is not considered moving unless it is moving at least as fast as a certain velocity.
* @param test the (squared) velocity to test against
* @return `true`, if we are moving; `false`, otherwise
*/
* This object is not considered moving unless it is moving at least as fast as a certain velocity.
* @param test the velocity to test against
* @return `true`, if we are moving; `false`, otherwise
*/
def isMoving(test: Double): Boolean = WorldEntity.isMoving(Velocity, (test * test).toFloat)
/**
* This object is not considered moving unless it is moving at least as fast as a certain velocity.
* @param test the (squared) velocity to test against
* @return `true`, if we are moving; `false`, otherwise
*/
def isMoving(test: Float): Boolean = WorldEntity.isMoving(Velocity, test)
}

View file

@ -5,6 +5,7 @@ import net.psforever.objects._
import net.psforever.objects.ce.DeployableCategory
import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.vital.DamagingActivity
import net.psforever.types.{ExoSuitType, ImplantType}
final case class TargetValidation(category: EffectTarget.Category.Value, test: EffectTarget.Validation.Value)
@ -21,6 +22,7 @@ object EffectTarget {
object Validation {
type Value = PlanetSideGameObject => Boolean
//noinspection ScalaUnusedSymbol
def Invalid(target: PlanetSideGameObject): Boolean = false
def Medical(target: PlanetSideGameObject): Boolean =
@ -185,5 +187,36 @@ object EffectTarget {
case _ =>
false
}
def ObviousPlayer(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.Jumping
case _ =>
false
})
def ObviousMax(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
!(GlobalDefinitions.isAtvVehicle(vdef) ||
vdef == GlobalDefinitions.two_man_assault_buggy ||
vdef == GlobalDefinitions.skyguard)
case _ =>
false
})
}
}

View file

@ -84,7 +84,7 @@ object GenericHackables {
HackState.Start
} else if (progress >= 100L) {
HackState.Finished
} else if (target.isMoving(1f)) {
} else if (target.isMoving(test = 1f)) {
// If the object is moving (more than slightly to account for things like magriders rotating, or the last velocity reported being the magrider dipping down on dismount) then cancel the hack
HackState.Cancelled
} else {

View file

@ -105,7 +105,7 @@ trait MountableBehavior {
): Boolean = {
obj.PassengerInSeat(user).contains(seatNumber) &&
(obj.Seats.get(seatNumber) match {
case Some(seat) => seat.bailable || !obj.isMoving(test = 1)
case Some(seat) => seat.bailable || !obj.isMoving(test = 1f)
case _ => false
})
}

View file

@ -2,19 +2,20 @@
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.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.DamageableEntity
import net.psforever.objects.sourcing.{SourceEntry, SourceUniqueness}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.{InteractsWithZone, Zone}
import net.psforever.packet.game.{ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ObjectDetectedMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{PlanetSideGUID, Vector3}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.FiniteDuration
trait AutomatedTurret
extends PlanetSideServerObject
@ -84,9 +85,11 @@ trait AutomatedTurretBehavior {
private var periodicValidationTest: Cancellable = Default.Cancellable
private lazy val autoStats: Option[Automation] = AutomatedTurretObject.Definition.AutoFire
def AutomatedTurretObject: AutomatedTurret
val automatedTurretBehavior: Actor.Receive = {
val automatedTurretBehavior: Actor.Receive = if (autoStats.isDefined) {
case AutomatedTurretBehavior.Alert(target) =>
bringAttentionToTarget(target)
@ -101,19 +104,27 @@ trait AutomatedTurretBehavior {
case AutomatedTurretBehavior.PeriodicCheck =>
performPeriodicTargetValidation()
} else {
Actor.emptyBehavior
}
def AutomaticOperation: Boolean = automaticOperation
def AutomaticOperation_=(state: Boolean): Boolean = {
val previousState = automaticOperation
automaticOperation = state
if (!previousState && state) {
trySelectNewTarget()
} else if (previousState && !state) {
currentTarget.foreach { noLongerEngageDetectedTarget }
if (autoStats.isDefined) {
automaticOperation = state
if (!previousState && state) {
trySelectNewTarget()
} else if (previousState && !state) {
currentTarget.foreach {
noLongerEngageDetectedTarget
}
}
state
} else {
false
}
state
}
private def bringAttentionToTarget(target: Target): Unit = {
@ -129,10 +140,12 @@ trait AutomatedTurretBehavior {
private def confirmShot(target: Target): Unit = {
val now = System.currentTimeMillis()
if (currentTargetToken.isEmpty || now - currentTargetLastShotReported > 1500L) {
if (currentTargetToken.isEmpty || now - currentTargetLastShotReported > autoStats.map { _.targetSelectCooldown }.get) {
currentTargetLastShotReported = now
engageNewDetectedTarget(target)
} else if (currentTargetToken.contains(SourceEntry(target).unique) && now - currentTargetLastShotReported < 3000L) {
} else if (
currentTargetToken.contains(SourceEntry(target).unique) &&
now - currentTargetLastShotReported < autoStats.map { _.missedShotCooldown }.get) {
currentTargetLastShotReported = now
}
}
@ -196,17 +209,22 @@ trait AutomatedTurretBehavior {
private def trySelectNewTarget(): Option[Target] = {
currentTarget.orElse {
val turretPosition = AutomatedTurretObject.Position
AutomatedTurretObject.Targets
.filter { target =>
!target.Destroyed && (target match {
case p: Player => validTargetCheck(p)
case _ => false
})
val radiusSquared = autoStats.get.targetingRange * autoStats.get.targetingRange
val validation = autoStats.get.targetValidation
val faction = AutomatedTurretObject.Faction
AutomatedTurretObject
.Targets
.map { target =>
(target, Vector3.DistanceSquared(target.Position, turretPosition))
}
.sortBy {
target => Vector3.DistanceSquared(target.Position, turretPosition)
.collect { case out @ (target, distance)
if /*target.Faction != faction &&*/
distance < radiusSquared &&
validation.exists(func => func(target)) =>
out
}
.flatMap { case target: Player =>
.sortBy(_._2)
.flatMap { case (target: Player, _) =>
testNewDetectedTarget(target, target.Name)
Some(target)
}
@ -214,16 +232,6 @@ trait AutomatedTurretBehavior {
}
}
private def validTargetCheck(target: Target): Boolean = {
!target.Destroyed && (target match {
case p: Player =>
if (p.Cloaked) false
else if (p.Crouching) false
else p.isMoving(test = 3f)
case _ => false
})
}
private def performPeriodicTargetValidation(): List[Target] = {
val size = AutomatedTurretObject.Targets.size
val list = performDistanceCheck()
@ -237,39 +245,71 @@ trait AutomatedTurretBehavior {
val pos = AutomatedTurretObject.Position
val removedTargets = AutomatedTurretObject.Targets
.collect {
case t if t.Destroyed || Vector3.DistanceSquared(t.Position, pos) > 625 =>
case t: InteractsWithZone
if t.Destroyed || {
val range = t.interaction().find(_.Type == TurretInteraction).map(_.range).getOrElse(100f)
Vector3.DistanceSquared(t.Position, pos) > range * range
} =>
AutomatedTurretObject.RemoveTarget(t)
t
}
removedTargets
}
private def performCurrentTargetDecayCheck(): Unit = {
val now = System.currentTimeMillis()
val delay = autoStats.map(_.missedShotCooldown).getOrElse(3000L)
currentTarget
.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)
}
}
.orElse {
//no target; unless we are deactivated or have any unfinished delays, search for new target
if (automaticOperation && now - currentTargetLastShotReported >= delay) {
trySelectNewTarget()
}
None
}
}
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) {
periodicValidationTest = context.system.scheduler.scheduleWithFixedDelay(
Duration.Zero,
1.second,
self,
AutomatedTurretBehavior.PeriodicCheck
)
if (beforeSize == 0 && AutomatedTurretObject.Targets.nonEmpty && autoStats.isDefined) {
val (initial, repeated) = autoStats
.map {
ta => (ta.initialDetectionSpeed, ta.detectionSpeed)
}
.get
retimePeriodicTargetChecks(initial, repeated)
true
} else {
false
}
}
private def performCurrentTargetDecayCheck(): Unit = {
//complete culling and/or check the current selected target
if (System.currentTimeMillis() - currentTargetLastShotReported > 3000L) {
currentTarget.foreach { noLongerEngageDetectedTarget }
if (automaticOperation) {
//trySelectNewTarget()
}
}
private def retimePeriodicTargetChecks(initial: FiniteDuration, repeated: FiniteDuration): Unit = {
periodicValidationTest.cancel()
periodicValidationTest = context.system.scheduler.scheduleWithFixedDelay(
initial,
repeated,
self,
AutomatedTurretBehavior.PeriodicCheck
)
}
def automaticTurretPostStop(): Unit = {

View file

@ -1,15 +1,28 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition, WithShields}
import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets}
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel
import scala.collection.mutable
import scala.concurrent.duration._
final case class Automation(
targetingRange: Float,
targetValidation: List[PlanetSideGameObject => Boolean],
retaliatoryDuration: Long = 0,
initialDetectionSpeed: FiniteDuration = Duration.Zero,
detectionSpeed: FiniteDuration = 1.seconds,
targetSelectCooldown: Long = 1500L, //ms
missedShotCooldown: Long = 3000L, //ms
targetEliminationCooldown: Long = 0L //ms
)
/**
* The definition for any `MannedTurret`.
* The definition for any `WeaponTurret`.
*/
trait TurretDefinition
extends MountableWeaponsDefinition
@ -25,11 +38,12 @@ trait TurretDefinition
/** can only be mounted by owning faction when `true` */
private var factionLocked: Boolean = true
/** creates internal ammunition reserves that can not become depleted
* see `MannedTurret.TurretAmmoBox` for details
*/
/** creates internal ammunition reserves that can not become depleted */
private var hasReserveAmmunition: Boolean = false
/** */
private var turretAutomation: Option[Automation] = None
def WeaponPaths: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weaponPaths
def FactionLocked: Boolean = factionLocked
@ -45,4 +59,15 @@ trait TurretDefinition
hasReserveAmmunition = reserved
ReserveAmmunition
}
def AutoFire: Option[Automation] = turretAutomation
def AutoFire_=(auto: Automation): Option[Automation] = {
AutoFire_=(Some(auto))
}
def AutoFire_=(auto: Option[Automation]): Option[Automation] = {
turretAutomation = auto
turretAutomation
}
}