thoroughly reorganized code in behavior; added code for turret-specific interactions for deployable construction, deployable destruction, jamming, and for weaponfire retribution; killing is currently disable for testing turnaround

This commit is contained in:
Fate-JH 2023-12-11 11:24:36 -05:00
parent 41462ce540
commit 2e84b33a47
5 changed files with 359 additions and 151 deletions

View file

@ -3,7 +3,7 @@ package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
import net.psforever.objects.zones.Zoning
import net.psforever.objects.serverobject.turret.AutomatedTurret
import net.psforever.objects.serverobject.turret.{AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.zones.exp.ToDatabase
import scala.collection.mutable
@ -432,44 +432,52 @@ private[support] class WeaponAndProjectileOperations(
}
def handleAIDamage(pkt: AIDamage): Unit = {
val AIDamage(_, attackerGuid, projectileTypeId, _, _) = pkt
sessionData.validObject(attackerGuid, decorator = "AIDamage/Entity")
.collect {
case turret: AutomatedTurret with OwnableByPlayer =>
val owner = sessionData.validObject(turret.OwnerGuid, decorator = "AIDamage/Owner")
.collect { case obj: PlanetSideGameObject with FactionAffinity => SourceEntry(obj) }
.getOrElse { SourceEntry.None }
turret.Weapons
.values
.flatMap { _.Equipment }
.collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
.find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt
(continent.GUID(player.VehicleSeated) match {
case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality) if tobj.GUID == targetGuid => Some(tobj)
case _ if player.GUID == targetGuid => Some(player)
case _ => None
}).foreach { target =>
sessionData.validObject(attackerGuid, decorator = "AIDamage/Attacker")
.collect {
case turret: AutomatedTurret if turret.Target.isEmpty =>
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
None
}
.collect {
case Some((obj, tool, owner, projectileInfo)) =>
val target = sessionData.validObject(player.VehicleSeated, decorator = "AIDamage/Entity") match {
case Some(tobj: PlanetSideGameObject with FactionAffinity with Vitality) => tobj
case _ => player
}
val angle = Vector3.Unit(target.Position - obj.Position)
val proj = new Projectile(
projectileInfo,
tool.Definition,
tool.FireMode,
None,
owner,
obj.Definition.ObjectId,
obj.Position + Vector3.z(value = 1f),
angle,
Some(angle * projectileInfo.FinalVelocity)
)
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)
}
}
case turret: AutomatedTurret with OwnableByPlayer =>
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
val owner = continent.GUID(turret.OwnerGuid)
.collect { case obj: PlanetSideGameObject with FactionAffinity => SourceEntry(obj) }
.getOrElse { SourceEntry.None }
turret.Weapons
.values
.flatMap { _.Equipment }
.collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
.find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
}
.collect {
case Some((obj, tool, owner, projectileInfo)) =>
val angle = Vector3.Unit(target.Position - obj.Position)
val proj = new Projectile(
projectileInfo,
tool.Definition,
tool.FireMode,
None,
owner,
obj.Definition.ObjectId,
obj.Position + Vector3.z(value = 1f),
angle,
Some(angle * projectileInfo.FinalVelocity)
)
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)
}
}
}
}
/* support code */

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import akka.actor.{Actor, ActorContext, ActorRef, Props}
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem}
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.definition.converter.SmallTurretConverter
@ -17,7 +17,8 @@ import net.psforever.objects.serverobject.repair.RepairableWeaponTurret
import net.psforever.objects.serverobject.turret.{AutomatedTurret, AutomatedTurretBehavior, TurretDefinition, WeaponTurret}
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance}
import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance, Vitality}
import net.psforever.objects.zones.Zone
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.duration.FiniteDuration
@ -28,7 +29,7 @@ class TurretDeployable(tdef: TurretDeployableDefinition)
with JammableUnit
with Hackable
with AutomatedTurret {
WeaponTurret.LoadDefinition(this)
WeaponTurret.LoadDefinition(turret = this)
override def Definition: TurretDeployableDefinition = tdef
}
@ -81,6 +82,7 @@ class TurretControl(turret: TurretDeployable)
super.postStop()
deployableBehaviorPostStop()
damageableWeaponTurretPostStop()
automaticTurretPostStop()
}
def receive: Receive =
@ -103,7 +105,42 @@ class TurretControl(turret: TurretDeployable)
(!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed
}
override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
val startsUnjammed = !JammableObject.Jammed
super.TryJammerEffectActivate(target, cause)
if (startsUnjammed && JammableObject.Jammed) {
AutomaticOperation = false
//look in direction of source of jamming
val zone = JammableObject.Zone
TurretControl.getAttackerFromCause(zone, cause).foreach {
attacker =>
val channel = zone.id
val guid = AutomatedTurretObject.GUID
AutomatedTurretBehavior.startTrackingTargets(zone, channel, guid, List(attacker.GUID))
AutomatedTurretBehavior.stopTrackingTargets(zone, channel, guid) //TODO delay by a few milliseconds?
}
}
}
override def CancelJammeredStatus(target: Any): Unit = {
val startsJammed = JammableObject.Jammed
super.CancelJammeredStatus(target)
startsJammed && AutomaticOperation_=(state = true)
}
override protected def DamageAwareness(target: Target, cause: DamageResult, amount: Any): Unit = {
//turret retribution
if (AutomaticOperation) {
TurretControl.getAttackerFromCause(target.Zone, cause).foreach {
attacker =>
engageNewDetectedTarget(attacker)
}
}
super.DamageAwareness(target, cause, amount)
}
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
AutomaticOperation = false
super.DestructionAwareness(target, cause)
CancelJammeredSound(target)
CancelJammeredStatus(target)
@ -111,6 +148,7 @@ class TurretControl(turret: TurretDeployable)
}
override def deconstructDeployable(time: Option[FiniteDuration]) : Unit = {
AutomaticOperation = false
val zone = turret.Zone
val seats = turret.Seats.values
//either we have no seats or no one gets to sit
@ -136,8 +174,50 @@ class TurretControl(turret: TurretDeployable)
super.deconstructDeployable(retime)
}
override def finalizeDeployable(callback: ActorRef): Unit = {
super.finalizeDeployable(callback)
AutomaticOperation = true
}
override def unregisterDeployable(obj: Deployable): Unit = {
val zone = obj.Zone
TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(zone.GUID, turret))
}
}
object TurretControl {
private def getAttackerFromCause(zone: Zone, cause: DamageResult): Option[PlanetSideServerObject with Vitality] = {
import net.psforever.objects.sourcing._
cause
.interaction
.adversarial
.collect { adversarial =>
adversarial.attacker match {
case p: PlayerSource =>
p.seatedIn
.map { _._1.unique }
.collect {
case v: UniqueVehicle => zone.Vehicles.find(SourceEntry(_).unique == v)
case a: UniqueAmenity => zone.GUID(a.guid)
case d: UniqueDeployable => zone.DeployableList.find(SourceEntry(_).unique == d)
}
.flatten
.orElse {
val name = p.Name
zone.LivePlayers.find(_.Name.equals(name))
}
case o =>
o.unique match {
case v: UniqueVehicle => zone.Vehicles.find(SourceEntry(_).unique == v)
case a: UniqueAmenity => zone.GUID(a.guid)
case d: UniqueDeployable => zone.DeployableList.find(SourceEntry(_).unique == d)
case _ => None
}
}
}
.flatten
.collect {
case out: PlanetSideServerObject with Vitality => out
}
}
}

View file

@ -56,7 +56,7 @@ class InteractWithTurrets(val range: Float)
target.getInteractionSector(),
target.Position.xy
).foreach { turret =>
turret.Actor ! AutomatedTurretBehavior.ResetAlerts
turret.Actor ! AutomatedTurretBehavior.Reset
}
}
}

View file

@ -139,7 +139,7 @@ trait JammableBehavior {
* @param target the objects to be determined if affected by the source's jammering
* @param cause the source of the "jammered" status
*/
def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit =
def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
target match {
case obj: PlanetSideServerObject =>
val interaction = cause.interaction
@ -157,8 +157,9 @@ trait JammableBehavior {
}
case None =>
}
case _ => ;
case _ => ()
}
}
/**
* Activate a distinctive buzzing sound effect.

View file

@ -5,14 +5,14 @@ import akka.actor.{Actor, Cancellable}
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.packet.game.{ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ObjectDetectedMessage}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{PlanetSideGUID, Vector3}
import scala.collection.mutable
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
@ -20,8 +20,23 @@ 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] = {
@ -56,125 +71,212 @@ object AutomatedTurret {
}
trait AutomatedTurretBehavior {
_: Actor =>
_: Actor with DamageableEntity =>
import AutomatedTurret.Target
import AutomatedTurretBehavior.TurretTargetEntry
private val targets: mutable.HashMap[SourceUniqueness, TurretTargetEntry] =
mutable.HashMap[SourceUniqueness, TurretTargetEntry]()
private var automaticOperation: Boolean = false
private var targetDistanceCheck: Cancellable = Default.Cancellable
private var currentTargetToken: Option[SourceUniqueness] = None
private var currentTarget: Option[Target] = None
private var currentTargetLastShotReported: Long = 0L
private var periodicValidationTest: Cancellable = Default.Cancellable
def AutomatedTurretObject: AutomatedTurret
val automatedTurretBehavior: Actor.Receive = {
case AutomatedTurretBehavior.Alert(target) =>
val targets = AutomatedTurretObject.Targets
val size = targets.size
AutomatedTurretObject.Detected(target)
.orElse {
AutomatedTurretObject.AddTarget(target)
retimeDistanceCheck(size)
Some(target)
}
.foreach { newDetectedTarget }
bringAttentionToTarget(target)
case AutomatedTurretBehavior.ConfirmShot(target) =>
confirmShot(target)
case AutomatedTurretBehavior.Unalert(target) =>
val targets = AutomatedTurretObject.Targets
val size = targets.size
AutomatedTurretObject.Detected(target)
.collect { out =>
AutomatedTurretObject.RemoveTarget(target)
testDistanceCheckQualifications(size)
out
disregardTarget(target)
case AutomatedTurretBehavior.Reset =>
resetAlerts()
case AutomatedTurretBehavior.PeriodicCheck =>
performPeriodicTargetValidation()
}
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 }
}
state
}
private def bringAttentionToTarget(target: Target): Unit = {
val targets = AutomatedTurretObject.Targets
val size = targets.size
AutomatedTurretObject.Detected(target)
.orElse {
AutomatedTurretObject.AddTarget(target)
retimePeriodicTargetChecks(size)
Some(target)
}
}
private def confirmShot(target: Target): Unit = {
val now = System.currentTimeMillis()
if (currentTargetToken.isEmpty || now - currentTargetLastShotReported > 1500L) {
currentTargetLastShotReported = now
engageNewDetectedTarget(target)
} else if (currentTargetToken.contains(SourceEntry(target).unique) && now - currentTargetLastShotReported < 3000L) {
currentTargetLastShotReported = now
}
}
private def disregardTarget(target: Target): Unit = {
val targets = AutomatedTurretObject.Targets
val size = targets.size
AutomatedTurretObject.Detected(target)
.collect { out =>
AutomatedTurretObject.RemoveTarget(target)
testTargetListQualifications(size)
out
}
.flatMap {
noLongerDetectTargetIfCurrent
}
}
private def resetAlerts(): Unit = {
currentTarget.foreach { noLongerEngageDetectedTarget }
AutomatedTurretObject.Clear()
testTargetListQualifications(beforeSize = 1)
}
private def testNewDetectedTarget(target: Target, channel: String): Unit = {
val zone = target.Zone
AutomatedTurretBehavior.startTrackingTargets(zone, channel, AutomatedTurretObject.GUID, List(target.GUID))
AutomatedTurretBehavior.startShooting(zone, channel, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
AutomatedTurretBehavior.stopShooting(zone, channel, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
}
protected def engageNewDetectedTarget(target: Target): Unit = {
val zone = target.Zone
val zoneid = zone.id
AutomatedTurretObject.Target = target
currentTarget = Some(target)
currentTargetToken = Some(SourceEntry(target).unique)
AutomatedTurretBehavior.startTrackingTargets(zone, zoneid, AutomatedTurretObject.GUID, List(target.GUID))
AutomatedTurretBehavior.startShooting(zone, zoneid, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
}
protected def noLongerDetectTargetIfCurrent(target: Target): Option[Target] = {
if (currentTargetToken.contains(SourceEntry(target).unique)) {
noLongerEngageDetectedTarget(target)
} else {
currentTarget
}
}
protected def noLongerEngageDetectedTarget(target: Target): Option[Target] = {
val zone = target.Zone
val zoneid = zone.id
AutomatedTurretObject.Target = None
currentTarget = None
currentTargetToken = None
AutomatedTurretBehavior.stopTrackingTargets(zone, zoneid, AutomatedTurretObject.GUID)
AutomatedTurretBehavior.stopShooting(zone, zoneid, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
None
}
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
})
}
.foreach { noLongerDetectedTarget }
case AutomatedTurretBehavior.ResetAlerts =>
AutomatedTurretObject.Clear().foreach { noLongerDetectedTarget }
testDistanceCheckQualifications(beforeSize = 1)
case AutomatedTurretBehavior.PeriodicDistanceCheck =>
performPeriodicDistanceCheck()
}
private def newDetectedTarget(target: Target): Unit = {
target match {
case target: Player =>
startTrackingTargets(target.Zone, target.Name, List(target.GUID))
startShooting(target.Zone, target.Name, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
case _ => ()
.sortBy {
target => Vector3.DistanceSquared(target.Position, turretPosition)
}
.flatMap { case target: Player =>
testNewDetectedTarget(target, target.Name)
Some(target)
}
.headOption
}
}
private def noLongerDetectedTarget(target: Target): Unit = {
target match {
case target: Player =>
stopTrackingTargets(target.Zone, target.Name)
stopShooting(target.Zone, target.Name, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
case _ => ()
}
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
})
}
def startTrackingTargets(zone: Zone, channel: String, list: List[PlanetSideGUID]): Unit = {
zone.AvatarEvents ! AvatarServiceMessage(
channel,
AvatarAction.SendResponse(PlanetSideGUID(0), ObjectDetectedMessage(AutomatedTurretObject.GUID, AutomatedTurretObject.GUID, 0, list))
)
private def performPeriodicTargetValidation(): List[Target] = {
val size = AutomatedTurretObject.Targets.size
val list = performDistanceCheck()
performCurrentTargetDecayCheck()
testTargetListQualifications(size)
list
}
def stopTrackingTargets(zone: Zone, channel: String): Unit = {
zone.AvatarEvents ! AvatarServiceMessage(
channel,
AvatarAction.SendResponse(PlanetSideGUID(0), ObjectDetectedMessage(AutomatedTurretObject.GUID, AutomatedTurretObject.GUID, 0, List(PlanetSideGUID(0))))
)
}
def startShooting(zone: Zone, channel: String, weaponGuid: PlanetSideGUID): Unit = {
zone.AvatarEvents ! AvatarServiceMessage(
channel,
AvatarAction.SendResponse(PlanetSideGUID(0), ChangeFireStateMessage_Start(weaponGuid))
)
}
def stopShooting(zone: Zone, channel: String, weaponGuid: PlanetSideGUID): Unit = {
zone.AvatarEvents ! AvatarServiceMessage(
channel,
AvatarAction.SendResponse(PlanetSideGUID(0), ChangeFireStateMessage_Stop(weaponGuid))
)
}
private def testDistanceCheckQualifications(beforeSize: Int): Unit = {
if (beforeSize > 0 && AutomatedTurretObject.Targets.isEmpty) {
targetDistanceCheck.cancel()
}
}
private def retimeDistanceCheck(beforeSize: Int): Unit = {
if (beforeSize == 0 && AutomatedTurretObject.Targets.nonEmpty) {
targetDistanceCheck = context.system.scheduler.scheduleAtFixedRate(
1.second,
1.second,
self,
AutomatedTurretBehavior.PeriodicDistanceCheck
)
}
}
private def performPeriodicDistanceCheck(): List[Target] = {
private def performDistanceCheck(): List[Target] = {
//cull targets
val pos = AutomatedTurretObject.Position
val earlyTargets = AutomatedTurretObject.Targets
val earlySize = earlyTargets.size
val removedTargets = earlyTargets
val removedTargets = AutomatedTurretObject.Targets
.collect {
case t if Vector3.DistanceSquared(t.Position, pos) > 625 =>
case t if t.Destroyed || Vector3.DistanceSquared(t.Position, pos) > 625 =>
AutomatedTurretObject.RemoveTarget(t)
t
}
removedTargets.foreach { noLongerDetectedTarget }
testDistanceCheckQualifications(earlySize)
removedTargets
}
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
)
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()
}
}
}
def automaticTurretPostStop(): Unit = {
periodicValidationTest.cancel()
currentTarget.foreach { noLongerEngageDetectedTarget }
AutomatedTurretObject.Targets.foreach { AutomatedTurretObject.RemoveTarget }
}
}
object AutomatedTurretBehavior {
@ -183,20 +285,37 @@ object AutomatedTurretBehavior {
final case class Unalert(target: Target)
final case object ResetAlerts
final case class ConfirmShot(target: Target)
private case object PeriodicDistanceCheck
final case object Reset
final case class TurretTargetEntry(target: Target, regard: RegardTargetAs.TurretOpinion)
private case object PeriodicCheck
object RegardTargetAs {
trait TurretOpinion
def startTrackingTargets(zone: Zone, channel: String, guid: PlanetSideGUID, list: List[PlanetSideGUID]): Unit = {
zone.LocalEvents ! LocalServiceMessage(
channel,
LocalAction.SendResponse(ObjectDetectedMessage(guid, guid, 0, list))
)
}
final case object Friendly extends TurretOpinion
final case object Testing extends TurretOpinion
final case object Unreachable extends TurretOpinion
final case object Blocked extends TurretOpinion
final case object Invalid extends TurretOpinion
final case object Attack extends TurretOpinion
def stopTrackingTargets(zone: Zone, channel: String, guid: PlanetSideGUID): Unit = {
zone.LocalEvents ! LocalServiceMessage(
channel,
LocalAction.SendResponse(ObjectDetectedMessage(guid, guid, 0, List(PlanetSideGUID(0))))
)
}
private def startShooting(zone: Zone, channel: String, weaponGuid: PlanetSideGUID): Unit = {
zone.LocalEvents ! LocalServiceMessage(
channel,
LocalAction.SendResponse(ChangeFireStateMessage_Start(weaponGuid))
)
}
private def stopShooting(zone: Zone, channel: String, weaponGuid: PlanetSideGUID): Unit = {
zone.LocalEvents ! LocalServiceMessage(
channel,
LocalAction.SendResponse(ChangeFireStateMessage_Stop(weaponGuid))
)
}
}