mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-20 02:24:45 +00:00
Turret Automation (#1166)
* zone interaction for turret discovery, players only so far; minor field value change for small turret data; automated turret target recognition; grammatical and linter fixes * initial AIDamage packet and tests; wrote handling code for the AIDamage packet that transforms it into actionable projectile damage * 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 * introduced definition properties to configure auto fire; interspersed properties into relevant files; non-squared velocity check for isMoving * separated overworld facility turrets from cavern facility turrets, called vanu sentry turrets; tightened functionality inheritance between turret deployables and facility turrets; corrected issue where recharging vehicle weapons didn't work * adjust mounting code to betterhandle automation with the facility turrets; basic operation of automation has also been changed, adding a variety of ranges to test against, and cylindrical distance checks * attempted cleanup of previous test fire condition; division of turret callbacks between generic targets and vehicle targets; facility turret stops automatic fire when being mounted and resumes automatic mode when being dismounted * 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!) * 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) * secondary queue that keeps track of the previous test shot subjects when none have been tested, allowing for a packet to be skipped during subsequent test shots * reactivating turret deployable destruction; clarifying the validation and clearing conditions for different kinds of auto turrets; extending self-reporting auto turret behavior to other auto-turrets * overhaul of the auto turret target selection process; conditions for MAX detection; rewired self-reporting to address the its issue a bit more specifically; ATDispatch is no longer useless as differences between facility turrets and deployable turrets have been identified, shifting the method to implementing and overriding in subclass control agencies * turret detection methods accounting for specific targets and considerations such as silent running; various turret interactions with other turrets and radiation clouds; proper management of retaliation and jamming; facility turrets have play in the lifecycle in the power structure and capture mechanics of the facility * uniqueness can be generated without having to having to go through source entries; made certain turret upgrading cooperates with turret automation; other targets for turret misaimed aggression; turrets sychronize better on zone load; target validation and blankinghas changed again * starting the target validation timer when dealing with retaliation if it should come from beyond the maximum detection range * stop assuming mountable turrets have places to mount; AMS and AEGIS blocking detection of vehicles; deployable sensors and small robotics turrets are allergic to vehicles * AEGIS and AMS cloak bubbles more proactive
This commit is contained in:
parent
d049146b4f
commit
ea77d4728f
|
|
@ -518,6 +518,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
case packet: LashMessage =>
|
||||
sessionFuncs.shooting.handleLashHit(packet)
|
||||
|
||||
case packet: AIDamage =>
|
||||
sessionFuncs.shooting.handleAIDamage(packet)
|
||||
|
||||
case packet: AvatarFirstTimeEventMessage =>
|
||||
sessionFuncs.handleAvatarFirstTimeEvent(packet)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package net.psforever.actors.session.support
|
|||
import akka.actor.typed.scaladsl.adapter._
|
||||
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
|
||||
import net.psforever.objects.vital.etc.SuicideReason
|
||||
import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation}
|
||||
|
||||
import scala.collection.mutable
|
||||
|
|
@ -882,13 +883,7 @@ class SessionData(
|
|||
case (None, _, _) => ()
|
||||
|
||||
case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) =>
|
||||
if (collisionHistory.get(us.Actor) match {
|
||||
case Some(lastCollision) if curr - lastCollision <= 1000L =>
|
||||
false
|
||||
case _ =>
|
||||
collisionHistory.put(us.Actor, curr)
|
||||
true
|
||||
}) {
|
||||
if (updateCollisionHistoryForTarget(us, curr)) {
|
||||
if (!bailProtectStatus) {
|
||||
handleDealingDamage(
|
||||
us,
|
||||
|
|
@ -901,40 +896,26 @@ class SessionData(
|
|||
}
|
||||
}
|
||||
|
||||
case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) =>
|
||||
collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
|
||||
|
||||
case (Some(us: Vehicle), _, Some(victim: TurretDeployable)) if victim.Seats.isEmpty =>
|
||||
collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
|
||||
|
||||
case (
|
||||
Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _,
|
||||
Some(victim: PlanetSideServerObject with Vitality with FactionAffinity)
|
||||
) =>
|
||||
if (collisionHistory.get(victim.Actor) match {
|
||||
case Some(lastCollision) if curr - lastCollision <= 1000L =>
|
||||
false
|
||||
case _ =>
|
||||
collisionHistory.put(victim.Actor, curr)
|
||||
true
|
||||
}) {
|
||||
if (updateCollisionHistoryForTarget(victim, curr)) {
|
||||
val usSource = SourceEntry(us)
|
||||
val victimSource = SourceEntry(victim)
|
||||
//we take damage from the collision
|
||||
if (!bailProtectStatus) {
|
||||
handleDealingDamage(
|
||||
us,
|
||||
DamageInteraction(
|
||||
usSource,
|
||||
CollisionWithReason(CollisionReason(velocity - tv, fallHeight, us.DamageModel), victimSource),
|
||||
ppos
|
||||
)
|
||||
)
|
||||
performCollisionWithSomethingDamage(us, usSource, ppos, victimSource, fallHeight, velocity - tv)
|
||||
}
|
||||
//get dealt damage from our own collision (no protection)
|
||||
collisionHistory.put(us.Actor, curr)
|
||||
handleDealingDamage(
|
||||
victim,
|
||||
DamageInteraction(
|
||||
victimSource,
|
||||
CollisionWithReason(CollisionReason(tv - velocity, 0, victim.DamageModel), usSource),
|
||||
tpos
|
||||
)
|
||||
)
|
||||
performCollisionWithSomethingDamage(victim, victimSource, tpos, usSource, fallHeight = 0f, tv - velocity)
|
||||
}
|
||||
|
||||
case _ => ()
|
||||
|
|
@ -2202,7 +2183,7 @@ class SessionData(
|
|||
|
||||
/**
|
||||
* Calculate the amount of damage to be dealt to an active `target`
|
||||
* using the information reconstructed from a `Resolvedprojectile`
|
||||
* using the information reconstructed from a `ResolvedProjectile`
|
||||
* and affect the `target` in a synchronized manner.
|
||||
* The active `target` and the target of the `DamageResult` do not have be the same.
|
||||
* While the "tell" for being able to sustain damage is an entity of type `Vitality`,
|
||||
|
|
@ -2836,6 +2817,59 @@ class SessionData(
|
|||
}
|
||||
}
|
||||
|
||||
private def updateCollisionHistoryForTarget(
|
||||
target: PlanetSideServerObject with Vitality with FactionAffinity,
|
||||
curr: Long
|
||||
): Boolean = {
|
||||
collisionHistory.get(target.Actor) match {
|
||||
case Some(lastCollision) if curr - lastCollision <= 1000L =>
|
||||
false
|
||||
case _ =>
|
||||
collisionHistory.put(target.Actor, curr)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private def collisionBetweenVehicleAndFragileDeployable(
|
||||
vehicle: Vehicle,
|
||||
vehiclePosition: Vector3,
|
||||
smallDeployable: Deployable,
|
||||
smallDeployablePosition: Vector3,
|
||||
velocity: Vector3,
|
||||
fallHeight: Float,
|
||||
collisionTime: Long
|
||||
): Unit = {
|
||||
if (updateCollisionHistoryForTarget(smallDeployable, collisionTime)) {
|
||||
val smallDeployableSource = SourceEntry(smallDeployable)
|
||||
//vehicle takes damage from the collision (ignore bail protection in this case)
|
||||
performCollisionWithSomethingDamage(vehicle, SourceEntry(vehicle), vehiclePosition, smallDeployableSource, fallHeight, velocity)
|
||||
//deployable gets absolutely destroyed
|
||||
collisionHistory.put(vehicle.Actor, collisionTime)
|
||||
handleDealingDamage(
|
||||
smallDeployable,
|
||||
DamageInteraction(smallDeployableSource, SuicideReason(), smallDeployablePosition)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def performCollisionWithSomethingDamage(
|
||||
target: PlanetSideServerObject with Vitality with FactionAffinity,
|
||||
targetSource: SourceEntry,
|
||||
targetPosition: Vector3,
|
||||
victimSource: SourceEntry,
|
||||
fallHeight: Float,
|
||||
velocity: Vector3
|
||||
): Unit = {
|
||||
handleDealingDamage(
|
||||
target,
|
||||
DamageInteraction(
|
||||
targetSource,
|
||||
CollisionWithReason(CollisionReason(velocity, fallHeight, target.DamageModel), victimSource),
|
||||
targetPosition
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def failWithError(error: String): Unit = {
|
||||
log.error(error)
|
||||
middlewareActor ! MiddlewareActor.Teardown()
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ class SessionLocalHandlers(
|
|||
continent.GUID(vehicleGuid)
|
||||
.collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) }
|
||||
.collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) }
|
||||
.getOrElse(Set.empty)
|
||||
.collect { case weapon: Tool if weapon.GUID == weaponGuid =>
|
||||
sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
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.VanuSentry
|
||||
import net.psforever.objects.zones.exp.ToDatabase
|
||||
|
||||
import scala.collection.mutable
|
||||
|
|
@ -51,8 +54,7 @@ private[support] class WeaponAndProjectileOperations(
|
|||
private[support] var shotsWhileDead: Int = 0
|
||||
private val projectiles: Array[Option[Projectile]] =
|
||||
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
|
||||
private var zoningOpt: Option[ZoningOperations] = None
|
||||
def zoning: ZoningOperations = zoningOpt.orNull
|
||||
|
||||
/* packets */
|
||||
|
||||
def handleWeaponFire(pkt: WeaponFireMessage): Unit = {
|
||||
|
|
@ -430,6 +432,55 @@ private[support] class WeaponAndProjectileOperations(
|
|||
}
|
||||
}
|
||||
|
||||
def handleAIDamage(pkt: AIDamage): Unit = {
|
||||
val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt
|
||||
(continent.GUID(player.VehicleSeated) match {
|
||||
case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer)
|
||||
if tobj.GUID == targetGuid &&
|
||||
tobj.OwnerGuid.contains(player.GUID) =>
|
||||
//deployable turrets
|
||||
Some(tobj)
|
||||
case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable)
|
||||
if tobj.GUID == targetGuid &&
|
||||
tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) =>
|
||||
//facility turrets, etc.
|
||||
Some(tobj)
|
||||
case _
|
||||
if player.GUID == targetGuid =>
|
||||
//player avatars
|
||||
Some(player)
|
||||
case _ =>
|
||||
None
|
||||
}).collect {
|
||||
case target: AutomatedTurret.Target =>
|
||||
sessionData.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret")
|
||||
.collect {
|
||||
case turret: AutomatedTurret if turret.Target.isEmpty =>
|
||||
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
|
||||
Some(target)
|
||||
|
||||
case turret: AutomatedTurret =>
|
||||
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
|
||||
HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
|
||||
Some(target)
|
||||
}
|
||||
}
|
||||
.orElse {
|
||||
//occasionally, something that is not technically a turret's natural target may be attacked
|
||||
sessionData.validObject(targetGuid, decorator = "AIDamage/Target")
|
||||
.collect {
|
||||
case target: PlanetSideServerObject with FactionAffinity with Vitality =>
|
||||
sessionData.validObject(attackerGuid, decorator = "AIDamage/Attacker")
|
||||
.collect {
|
||||
case turret: AutomatedTurret if turret.Target.nonEmpty =>
|
||||
//the turret must be shooting at something (else) first
|
||||
HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
|
||||
}
|
||||
Some(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* support code */
|
||||
|
||||
def HandleWeaponFireOperations(
|
||||
|
|
@ -519,11 +570,6 @@ private[support] class WeaponAndProjectileOperations(
|
|||
)
|
||||
continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile)
|
||||
}
|
||||
obj match {
|
||||
case turret: FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret =>
|
||||
turret.Actor ! FacilityTurret.WeaponDischarged()
|
||||
case _ => ()
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
s"WeaponFireMessage: ${player.Name}'s ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect"
|
||||
|
|
@ -1174,6 +1220,10 @@ private[support] class WeaponAndProjectileOperations(
|
|||
}
|
||||
|
||||
private def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = {
|
||||
sessionData.findContainedEquipment()._1.collect {
|
||||
case turret: FacilityTurret if continent.map.cavern =>
|
||||
turret.Actor ! VanuSentry.ChangeFireStart
|
||||
}
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.ChangeFireState_Start(player.GUID, itemGuid)
|
||||
|
|
@ -1236,6 +1286,10 @@ private[support] class WeaponAndProjectileOperations(
|
|||
}
|
||||
|
||||
private def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = {
|
||||
sessionData.findContainedEquipment()._1.collect {
|
||||
case turret: FacilityTurret if continent.map.cavern =>
|
||||
turret.Actor ! VanuSentry.ChangeFireStop
|
||||
}
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.ChangeFireState_Stop(player.GUID, itemGuid)
|
||||
|
|
@ -1366,6 +1420,7 @@ private[support] class WeaponAndProjectileOperations(
|
|||
addShotsToMap(shotsFired, weaponId, shots)
|
||||
}
|
||||
|
||||
//noinspection SameParameterValue
|
||||
private def addShotsLanded(weaponId: Int, shots: Int): Unit = {
|
||||
addShotsToMap(shotsLanded, weaponId, shots)
|
||||
}
|
||||
|
|
@ -1405,6 +1460,44 @@ private[support] class WeaponAndProjectileOperations(
|
|||
ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0))
|
||||
}
|
||||
|
||||
private def CompileAutomatedTurretDamageData(
|
||||
turret: AutomatedTurret,
|
||||
owner: SourceEntry,
|
||||
projectileTypeId: Long
|
||||
): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = {
|
||||
turret.Weapons
|
||||
.values
|
||||
.flatMap { _.Equipment }
|
||||
.collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
|
||||
.find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
|
||||
}
|
||||
|
||||
private def HandleAIDamage(
|
||||
target: PlanetSideServerObject with FactionAffinity with Vitality,
|
||||
results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)]
|
||||
): Unit = {
|
||||
results.collect {
|
||||
case (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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override protected[session] def stop(): Unit = {
|
||||
if (player != null && player.HasGUID) {
|
||||
(prefire ++ shooting).foreach { guid =>
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, Sess
|
|||
import net.psforever.objects.inventory.InventoryItem
|
||||
import net.psforever.objects.serverobject.mount.Seat
|
||||
import net.psforever.objects.serverobject.tube.SpawnTube
|
||||
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
|
||||
import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity}
|
||||
import net.psforever.packet.game.{CampaignStatistic, MailMessage, SessionStatistic}
|
||||
import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, MailMessage, ObjectDetectedMessage, SessionStatistic}
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
|
|
@ -259,6 +260,19 @@ class ZoningOperations(
|
|||
)
|
||||
}
|
||||
}
|
||||
//auto turret behavior
|
||||
(obj match {
|
||||
case turret: AutomatedTurret with JammableUnit => turret.Target
|
||||
case _ => None
|
||||
}).collect {
|
||||
target =>
|
||||
val guid = obj.GUID
|
||||
val turret = obj.asInstanceOf[AutomatedTurret]
|
||||
sendResponse(ObjectDetectedMessage(guid, guid, 0, List(target.GUID)))
|
||||
if (!obj.asInstanceOf[JammableUnit].Jammed) {
|
||||
sendResponse(ChangeFireStateMessage_Start(turret.Weapons.values.head.Equipment.get.GUID))
|
||||
}
|
||||
}
|
||||
})
|
||||
//sensor animation
|
||||
normal
|
||||
|
|
@ -555,6 +569,14 @@ class ZoningOperations(
|
|||
)
|
||||
case _ => ;
|
||||
}
|
||||
turret.Target.collect {
|
||||
target =>
|
||||
val guid = turret.GUID
|
||||
sendResponse(ObjectDetectedMessage(guid, guid, 0, List(target.GUID)))
|
||||
if (!turret.Jammed) {
|
||||
sendResponse(ChangeFireStateMessage_Start(turret.Weapons.values.head.Equipment.get.GUID))
|
||||
}
|
||||
}
|
||||
}
|
||||
//remote projectiles and radiation clouds
|
||||
continent.Projectiles.foreach { projectile =>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import akka.actor.typed.scaladsl.adapter._
|
|||
import net.psforever.actors.zone.building.MajorFacilityLogic
|
||||
import net.psforever.objects.avatar.scoring.Kill
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior
|
||||
import net.psforever.objects.serverobject.turret.FacilityTurret
|
||||
import net.psforever.objects.sourcing.SourceEntry
|
||||
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
|
||||
import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator}
|
||||
|
|
@ -96,14 +98,18 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
|||
case Success(buildings) =>
|
||||
buildings.foreach { building =>
|
||||
zone.BuildingByMapId(building.localId) match {
|
||||
case Some(_: WarpGate) => ;
|
||||
case Some(_: WarpGate) => ()
|
||||
//warp gates are controlled by game logic and are better off not restored via the database
|
||||
case Some(b) =>
|
||||
if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) {
|
||||
b.ForceDomeActive = MajorFacilityLogic.checkForceDomeStatus(b).getOrElse(false)
|
||||
b.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(b) }
|
||||
b.Neighbours.getOrElse(Nil).foreach(_.Actor ! BuildingActor.AlertToFactionChange(b))
|
||||
b.CaptureTerminal.collect { terminal =>
|
||||
val msg = CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured = true)
|
||||
b.Amenities.collect { case turret: FacilityTurret => turret.Actor ! msg }
|
||||
}
|
||||
}
|
||||
case None => ;
|
||||
case None => ()
|
||||
// TODO this happens during testing, need a way to not always persist during tests
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.{AutoChecks, AutoCooldowns, AutoRanges, 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,24 @@ object GlobalDefinitions {
|
|||
spitfire_turret.DeployTime = Duration.create(5000, "ms")
|
||||
spitfire_turret.Model = ComplexDeployableResolutions.calculate
|
||||
spitfire_turret.deployAnimation = DeployAnimation.Standard
|
||||
spitfire_turret.AutoFire = Automation(
|
||||
AutoRanges(
|
||||
detection = 75f,
|
||||
trigger = 50f,
|
||||
escape = 50f
|
||||
),
|
||||
AutoChecks(
|
||||
validation = List(
|
||||
EffectTarget.Validation.SmallRoboticsTurretValidatePlayerTarget,
|
||||
EffectTarget.Validation.SmallRoboticsTurretValidateMaxTarget,
|
||||
EffectTarget.Validation.SmallRoboticsTurretValidateGroundVehicleTarget,
|
||||
EffectTarget.Validation.SmallRoboticsTurretValidateAircraftTarget,
|
||||
EffectTarget.Validation.AutoTurretValidateMountableEntityTarget
|
||||
)
|
||||
),
|
||||
retaliatoryDelay = 2000L, //8000L
|
||||
refireTime = 200.milliseconds //150.milliseconds
|
||||
)
|
||||
spitfire_turret.innateDamage = new DamageWithPosition {
|
||||
CausesDamageType = DamageType.One
|
||||
Damage0 = 200
|
||||
|
|
@ -9085,6 +9117,30 @@ object GlobalDefinitions {
|
|||
spitfire_cloaked.DeployTime = Duration.create(5000, "ms")
|
||||
spitfire_cloaked.deployAnimation = DeployAnimation.Standard
|
||||
spitfire_cloaked.Model = ComplexDeployableResolutions.calculate
|
||||
spitfire_cloaked.AutoFire = Automation(
|
||||
AutoRanges(
|
||||
detection = 75f,
|
||||
trigger = 50f,
|
||||
escape = 75f
|
||||
),
|
||||
AutoChecks(
|
||||
validation = List(
|
||||
EffectTarget.Validation.SmallRoboticsTurretValidatePlayerTarget,
|
||||
EffectTarget.Validation.SmallRoboticsTurretValidateMaxTarget,
|
||||
EffectTarget.Validation.SmallRoboticsTurretValidateGroundVehicleTarget,
|
||||
EffectTarget.Validation.SmallRoboticsTurretValidateAircraftTarget,
|
||||
EffectTarget.Validation.AutoTurretValidateMountableEntityTarget
|
||||
)
|
||||
),
|
||||
cooldowns = AutoCooldowns(
|
||||
targetSelect = 0L,
|
||||
missedShot = 0L
|
||||
),
|
||||
detectionSweepTime = 500.milliseconds,
|
||||
retaliatoryDelay = 1L, //8000L
|
||||
retaliationOverridesTarget = false,
|
||||
refireTime = 200.milliseconds //150.milliseconds
|
||||
)
|
||||
spitfire_cloaked.innateDamage = new DamageWithPosition {
|
||||
CausesDamageType = DamageType.One
|
||||
Damage0 = 50
|
||||
|
|
@ -9111,6 +9167,21 @@ object GlobalDefinitions {
|
|||
spitfire_aa.DeployTime = Duration.create(5000, "ms")
|
||||
spitfire_aa.deployAnimation = DeployAnimation.Standard
|
||||
spitfire_aa.Model = ComplexDeployableResolutions.calculate
|
||||
spitfire_aa.AutoFire = Automation(
|
||||
AutoRanges(
|
||||
detection = 125f,
|
||||
trigger = 100f,
|
||||
escape = 200f
|
||||
),
|
||||
AutoChecks(
|
||||
validation = List(EffectTarget.Validation.SmallRoboticsTurretValidateAircraftTarget)
|
||||
),
|
||||
retaliatoryDelay = 2000L, //8000L
|
||||
retaliationOverridesTarget = false,
|
||||
refireTime = 0.seconds, //300.milliseconds
|
||||
cylindrical = true,
|
||||
cylindricalExtraHeight = 50f
|
||||
)
|
||||
spitfire_aa.innateDamage = new DamageWithPosition {
|
||||
CausesDamageType = DamageType.One
|
||||
Damage0 = 200
|
||||
|
|
@ -9175,9 +9246,10 @@ object GlobalDefinitions {
|
|||
portable_manned_turret.Damageable = true
|
||||
portable_manned_turret.Repairable = true
|
||||
portable_manned_turret.RepairIfDestroyed = false
|
||||
portable_manned_turret.controlledWeapons(seat = 0, weapon = 1)
|
||||
portable_manned_turret.WeaponPaths += 1 -> new mutable.HashMap()
|
||||
portable_manned_turret.WeaponPaths(1) += TurretUpgrade.None -> energy_gun
|
||||
portable_manned_turret.Seats += 0 -> new SeatDefinition()
|
||||
portable_manned_turret.controlledWeapons(seat = 0, weapon = 1)
|
||||
portable_manned_turret.MountPoints += 1 -> MountInfo(0)
|
||||
portable_manned_turret.MountPoints += 2 -> MountInfo(0)
|
||||
portable_manned_turret.ReserveAmmunition = true
|
||||
|
|
@ -9209,6 +9281,7 @@ object GlobalDefinitions {
|
|||
portable_manned_turret_nc.RepairIfDestroyed = false
|
||||
portable_manned_turret_nc.WeaponPaths += 1 -> new mutable.HashMap()
|
||||
portable_manned_turret_nc.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_nc
|
||||
portable_manned_turret_nc.Seats += 0 -> new SeatDefinition()
|
||||
portable_manned_turret_nc.controlledWeapons(seat = 0, weapon = 1)
|
||||
portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0)
|
||||
portable_manned_turret_nc.MountPoints += 2 -> MountInfo(0)
|
||||
|
|
@ -9240,6 +9313,7 @@ object GlobalDefinitions {
|
|||
portable_manned_turret_tr.RepairIfDestroyed = false
|
||||
portable_manned_turret_tr.WeaponPaths += 1 -> new mutable.HashMap()
|
||||
portable_manned_turret_tr.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_tr
|
||||
portable_manned_turret_tr.Seats += 0 -> new SeatDefinition()
|
||||
portable_manned_turret_tr.controlledWeapons(seat = 0, weapon = 1)
|
||||
portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0)
|
||||
portable_manned_turret_tr.MountPoints += 2 -> MountInfo(0)
|
||||
|
|
@ -9271,6 +9345,7 @@ object GlobalDefinitions {
|
|||
portable_manned_turret_vs.RepairIfDestroyed = false
|
||||
portable_manned_turret_vs.WeaponPaths += 1 -> new mutable.HashMap()
|
||||
portable_manned_turret_vs.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_vs
|
||||
portable_manned_turret_vs.Seats += 0 -> new SeatDefinition()
|
||||
portable_manned_turret_vs.controlledWeapons(seat = 0, weapon = 1)
|
||||
portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0)
|
||||
portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0)
|
||||
|
|
@ -9999,7 +10074,7 @@ object GlobalDefinitions {
|
|||
manned_turret.Name = "manned_turret"
|
||||
manned_turret.MaxHealth = 3600
|
||||
manned_turret.Damageable = true
|
||||
manned_turret.DamageDisablesAt = 0
|
||||
manned_turret.DamageDisablesAt = 1800
|
||||
manned_turret.Repairable = true
|
||||
manned_turret.autoRepair = AutoRepairStats(1.0909f, 10000, 1600, 0.05f)
|
||||
manned_turret.RepairIfDestroyed = true
|
||||
|
|
@ -10007,11 +10082,32 @@ object GlobalDefinitions {
|
|||
manned_turret.WeaponPaths(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan
|
||||
manned_turret.WeaponPaths(1) += TurretUpgrade.AVCombo -> phalanx_avcombo
|
||||
manned_turret.WeaponPaths(1) += TurretUpgrade.FlakCombo -> phalanx_flakcombo
|
||||
manned_turret.Seats += 0 -> new SeatDefinition()
|
||||
manned_turret.controlledWeapons(seat = 0, weapon = 1)
|
||||
manned_turret.MountPoints += 1 -> MountInfo(0)
|
||||
manned_turret.FactionLocked = true
|
||||
manned_turret.ReserveAmmunition = false
|
||||
manned_turret.RadiationShielding = 0.5f
|
||||
manned_turret.AutoFire = Automation(
|
||||
AutoRanges(
|
||||
detection = 125f,
|
||||
trigger = 100f,
|
||||
escape = 200f
|
||||
),
|
||||
AutoChecks(
|
||||
validation = List(
|
||||
EffectTarget.Validation.FacilityTurretValidateMaxTarget,
|
||||
EffectTarget.Validation.FacilityTurretValidateGroundVehicleTarget,
|
||||
EffectTarget.Validation.FacilityTurretValidateAircraftTarget,
|
||||
EffectTarget.Validation.AutoTurretValidateMountableEntityTarget
|
||||
)
|
||||
),
|
||||
retaliatoryDelay = 4000L, //8000L
|
||||
cylindrical = true,
|
||||
cylindricalExtraHeight = 50f,
|
||||
detectionSweepTime = 2.seconds,
|
||||
refireTime = 362.milliseconds //312.milliseconds
|
||||
)
|
||||
manned_turret.innateDamage = new DamageWithPosition {
|
||||
CausesDamageType = DamageType.One
|
||||
Damage0 = 150
|
||||
|
|
@ -10031,6 +10127,7 @@ object GlobalDefinitions {
|
|||
vanu_sentry_turret.RepairIfDestroyed = true
|
||||
vanu_sentry_turret.WeaponPaths += 1 -> new mutable.HashMap()
|
||||
vanu_sentry_turret.WeaponPaths(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon
|
||||
vanu_sentry_turret.Seats += 0 -> new SeatDefinition()
|
||||
vanu_sentry_turret.controlledWeapons(seat = 0, weapon = 1)
|
||||
vanu_sentry_turret.MountPoints += 1 -> MountInfo(0)
|
||||
vanu_sentry_turret.MountPoints += 2 -> MountInfo(0)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2019 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
|
||||
import net.psforever.objects.sourcing.UniquePlayer
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
trait OwnableByPlayer {
|
||||
|
|
@ -46,11 +46,11 @@ trait OwnableByPlayer {
|
|||
def AssignOwnership(playerOpt: Option[Player]): OwnableByPlayer = {
|
||||
(originalOwnerName, playerOpt) match {
|
||||
case (None, Some(player)) =>
|
||||
owner = Some(PlayerSource(player).unique)
|
||||
owner = Some(UniquePlayer(player))
|
||||
originalOwnerName = originalOwnerName.orElse { Some(player.Name) }
|
||||
OwnerGuid = player
|
||||
case (_, Some(player)) =>
|
||||
owner = Some(PlayerSource(player).unique)
|
||||
owner = Some(UniquePlayer(player))
|
||||
OwnerGuid = player
|
||||
case (_, None) =>
|
||||
owner = None
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package net.psforever.objects
|
|||
|
||||
import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
|
||||
import net.psforever.objects.ballistics.InteractWithRadiationClouds
|
||||
import net.psforever.objects.ce.{Deployable, InteractWithMines}
|
||||
import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets}
|
||||
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
|
||||
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
|
||||
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
|
||||
|
|
@ -38,6 +38,7 @@ class Player(var avatar: Avatar)
|
|||
with MountableEntity {
|
||||
interaction(new InteractWithEnvironment())
|
||||
interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10))
|
||||
interaction(new InteractWithTurrets())
|
||||
interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))
|
||||
|
||||
private var backpack: Boolean = false
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import net.psforever.services.Service
|
|||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
|
||||
import scala.annotation.unused
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class SensorDeployable(cdef: SensorDeployableDefinition) extends Deployable(cdef) with Hackable with JammableUnit
|
||||
|
|
@ -27,7 +28,7 @@ class SensorDeployableDefinition(private val objectId: Int) extends DeployableDe
|
|||
Model = SimpleResolutions.calculate
|
||||
Packet = new SmallDeployableConverter
|
||||
|
||||
override def Initialize(obj: Deployable, context: ActorContext) = {
|
||||
override def Initialize(obj: Deployable, context: ActorContext): Unit = {
|
||||
obj.Actor =
|
||||
context.actorOf(Props(classOf[SensorDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
|
||||
}
|
||||
|
|
@ -45,10 +46,10 @@ class SensorDeployableControl(sensor: SensorDeployable)
|
|||
with JammableBehavior
|
||||
with DamageableEntity
|
||||
with RepairableEntity {
|
||||
def DeployableObject = sensor
|
||||
def JammableObject = sensor
|
||||
def DamageableObject = sensor
|
||||
def RepairableObject = sensor
|
||||
def DeployableObject: SensorDeployable = sensor
|
||||
def JammableObject: SensorDeployable = sensor
|
||||
def DamageableObject: SensorDeployable = sensor
|
||||
def RepairableObject: SensorDeployable = sensor
|
||||
|
||||
override def postStop(): Unit = {
|
||||
super.postStop()
|
||||
|
|
@ -64,7 +65,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
|
|||
case _ => ;
|
||||
}
|
||||
|
||||
override protected def DamageLog(msg: String): Unit = {}
|
||||
override protected def DamageLog(@unused msg: String): Unit = {}
|
||||
|
||||
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
|
||||
super.DestructionAwareness(target, cause)
|
||||
|
|
@ -88,7 +89,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
|
|||
val zone = obj.Zone
|
||||
zone.LocalEvents ! LocalServiceMessage(
|
||||
zone.id,
|
||||
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, false, 1000)
|
||||
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, unk1=false, 1000)
|
||||
)
|
||||
super.StartJammeredStatus(obj, dur)
|
||||
case _ => ;
|
||||
|
|
@ -113,7 +114,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
|
|||
val zone = sensor.Zone
|
||||
zone.LocalEvents ! LocalServiceMessage(
|
||||
zone.id,
|
||||
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, true, 1000)
|
||||
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, unk1=true, 1000)
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
|
|
@ -125,7 +126,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
|
|||
val zone = sensor.Zone
|
||||
zone.LocalEvents ! LocalServiceMessage(
|
||||
zone.id,
|
||||
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", sensor.GUID, true, 1000)
|
||||
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", sensor.GUID, unk1=true, 1000)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -142,7 +143,7 @@ object SensorDeployableControl {
|
|||
val zone = target.Zone
|
||||
zone.LocalEvents ! LocalServiceMessage(
|
||||
zone.id,
|
||||
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", target.GUID, false, 1000)
|
||||
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", target.GUID, unk1=false, 1000)
|
||||
)
|
||||
//position the explosion effect near the bulky area of the sensor stalk
|
||||
val ang = target.Orientation
|
||||
|
|
|
|||
|
|
@ -1,35 +1,56 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import akka.actor.{Actor, ActorContext, Props}
|
||||
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem}
|
||||
import akka.actor.{Actor, ActorContext, ActorRef, Props}
|
||||
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem, InteractWithTurrets}
|
||||
import net.psforever.objects.definition.DeployableDefinition
|
||||
import net.psforever.objects.definition.converter.SmallTurretConverter
|
||||
import net.psforever.objects.equipment.{JammableMountedWeapons, JammableUnit}
|
||||
import net.psforever.objects.equipment.JammableUnit
|
||||
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
|
||||
import net.psforever.objects.serverobject.damage.Damageable.Target
|
||||
import net.psforever.objects.serverobject.damage.DamageableWeaponTurret
|
||||
import net.psforever.objects.serverobject.damage.Damageable
|
||||
import net.psforever.objects.serverobject.hackable.Hackable
|
||||
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
|
||||
import net.psforever.objects.serverobject.repair.RepairableWeaponTurret
|
||||
import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret}
|
||||
import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, Mountable}
|
||||
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret.Target
|
||||
import net.psforever.objects.serverobject.turret.auto.{AffectedByAutomaticTurretFire, 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.resistance.StandardResistanceProfile
|
||||
import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance}
|
||||
import net.psforever.objects.zones.InteractsWithZone
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
class TurretDeployable(tdef: TurretDeployableDefinition)
|
||||
extends Deployable(tdef)
|
||||
extends Deployable(tdef)
|
||||
with AutomatedTurret
|
||||
with WeaponTurret
|
||||
with JammableUnit
|
||||
with InteractsWithZone
|
||||
with StandardResistanceProfile
|
||||
with Hackable {
|
||||
WeaponTurret.LoadDefinition(this)
|
||||
if (tdef.Seats.nonEmpty) {
|
||||
interaction(new InteractWithTurrets())
|
||||
interaction(new InteractWithRadiationCloudsSeatedInEntity(obj = this, range = 100f))
|
||||
}
|
||||
WeaponTurret.LoadDefinition(turret = this)
|
||||
|
||||
override def Definition = tdef
|
||||
def TurretOwner: SourceEntry = {
|
||||
Seats
|
||||
.values
|
||||
.headOption
|
||||
.flatMap(_.occupant)
|
||||
.map(p => PlayerSource.inSeat(PlayerSource(p), SourceEntry(this), seatNumber=0))
|
||||
.orElse(Owners.map(PlayerSource(_, Position)))
|
||||
.getOrElse(SourceEntry(this))
|
||||
}
|
||||
|
||||
override def Definition: TurretDeployableDefinition = tdef
|
||||
}
|
||||
|
||||
class TurretDeployableDefinition(private val objectId: Int)
|
||||
|
|
@ -46,7 +67,7 @@ class TurretDeployableDefinition(private val objectId: Int)
|
|||
//override to clarify inheritance conflict
|
||||
override def MaxHealth_=(max: Int): Int = super[DeployableDefinition].MaxHealth_=(max)
|
||||
|
||||
override def Initialize(obj: Deployable, context: ActorContext) = {
|
||||
override def Initialize(obj: Deployable, context: ActorContext): Unit = {
|
||||
obj.Actor = context.actorOf(Props(classOf[TurretControl], obj), PlanetSideServerObject.UniqueActorName(obj))
|
||||
}
|
||||
}
|
||||
|
|
@ -63,35 +84,92 @@ class TurretControl(turret: TurretDeployable)
|
|||
extends Actor
|
||||
with DeployableBehavior
|
||||
with FactionAffinityBehavior.Check
|
||||
with JammableMountedWeapons //note: jammable status is reported as vehicle events, not local events
|
||||
with MountableBehavior
|
||||
with DamageableWeaponTurret
|
||||
with RepairableWeaponTurret {
|
||||
def DeployableObject = turret
|
||||
def MountableObject = turret
|
||||
def JammableObject = turret
|
||||
def FactionObject = turret
|
||||
def DamageableObject = turret
|
||||
def RepairableObject = turret
|
||||
with MountableTurretControl
|
||||
with AutomatedTurretBehavior
|
||||
with AffectedByAutomaticTurretFire {
|
||||
def TurretObject: TurretDeployable = turret
|
||||
def DeployableObject: TurretDeployable = turret
|
||||
def MountableObject: TurretDeployable = turret
|
||||
def JammableObject: TurretDeployable = turret
|
||||
def FactionObject: TurretDeployable = turret
|
||||
def DamageableObject: TurretDeployable = turret
|
||||
def RepairableObject: TurretDeployable = turret
|
||||
def AutomatedTurretObject: TurretDeployable = turret
|
||||
def AffectedObject: TurretDeployable = turret
|
||||
|
||||
override def postStop(): Unit = {
|
||||
super.postStop()
|
||||
deployableBehaviorPostStop()
|
||||
damageableWeaponTurretPostStop()
|
||||
selfReportingDatabaseUpdate()
|
||||
automaticTurretPostStop()
|
||||
}
|
||||
|
||||
def receive: Receive =
|
||||
deployableBehavior
|
||||
commonBehavior
|
||||
.orElse(deployableBehavior)
|
||||
.orElse(checkBehavior)
|
||||
.orElse(jammableBehavior)
|
||||
.orElse(mountBehavior)
|
||||
.orElse(dismountBehavior)
|
||||
.orElse(takesDamage)
|
||||
.orElse(canBeRepairedByNanoDispenser)
|
||||
.orElse(automatedTurretBehavior)
|
||||
.orElse(takeAutomatedDamage)
|
||||
.orElse {
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
protected def engageNewDetectedTarget(
|
||||
target: AutomatedTurret.Target,
|
||||
channel: String,
|
||||
turretGuid: PlanetSideGUID,
|
||||
weaponGuid: PlanetSideGUID
|
||||
): Unit = {
|
||||
val zone = target.Zone
|
||||
AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
|
||||
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
|
||||
}
|
||||
|
||||
protected def noLongerEngageTarget(
|
||||
target: AutomatedTurret.Target,
|
||||
channel: String,
|
||||
turretGuid: PlanetSideGUID,
|
||||
weaponGuid: PlanetSideGUID
|
||||
): Option[AutomatedTurret.Target] = {
|
||||
val zone = target.Zone
|
||||
AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
|
||||
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
|
||||
None
|
||||
}
|
||||
|
||||
protected def testNewDetected(
|
||||
target: AutomatedTurret.Target,
|
||||
channel: String,
|
||||
turretGuid: PlanetSideGUID,
|
||||
weaponGuid: PlanetSideGUID
|
||||
): Unit = {
|
||||
val zone = target.Zone
|
||||
AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
|
||||
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
|
||||
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
|
||||
}
|
||||
|
||||
protected def testKnownDetected(
|
||||
target: AutomatedTurret.Target,
|
||||
channel: String,
|
||||
turretGuid: PlanetSideGUID,
|
||||
weaponGuid: PlanetSideGUID
|
||||
): Unit = {
|
||||
val zone = target.Zone
|
||||
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
|
||||
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
|
||||
}
|
||||
|
||||
override protected def suspendTargetTesting(
|
||||
target: Target,
|
||||
channel: String,
|
||||
turretGuid: PlanetSideGUID,
|
||||
weaponGuid: PlanetSideGUID
|
||||
): Unit = {
|
||||
AutomatedTurretBehavior.stopTracking(target.Zone, channel, turretGuid)
|
||||
}
|
||||
|
||||
override protected def mountTest(
|
||||
obj: PlanetSideServerObject with Mountable,
|
||||
seatNumber: Int,
|
||||
|
|
@ -99,7 +177,40 @@ class TurretControl(turret: TurretDeployable)
|
|||
(!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed
|
||||
}
|
||||
|
||||
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
|
||||
override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
|
||||
val startsUnjammed = !JammableObject.Jammed
|
||||
super.TryJammerEffectActivate(target, cause)
|
||||
if (JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) {
|
||||
if (startsUnjammed) {
|
||||
AutomaticOperation = false
|
||||
}
|
||||
//look in direction of cause of jamming
|
||||
val zone = JammableObject.Zone
|
||||
AutomatedTurretBehavior.getAttackVectorFromCause(zone, cause).foreach { attacker =>
|
||||
AutomatedTurretBehavior.startTracking(zone, zone.id, AutomatedTurretObject.GUID, List(attacker.GUID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def CancelJammeredStatus(target: Any): Unit = {
|
||||
val startsJammed = JammableObject.Jammed
|
||||
super.CancelJammeredStatus(target)
|
||||
if (startsJammed && AutomaticOperation_=(state = true)) {
|
||||
val zone = TurretObject.Zone
|
||||
AutomatedTurretBehavior.stopTracking(zone, zone.id, TurretObject.GUID)
|
||||
}
|
||||
}
|
||||
|
||||
override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any): Unit = {
|
||||
amount match {
|
||||
case 0 => ()
|
||||
case _ => attemptRetaliation(target, cause)
|
||||
}
|
||||
super.DamageAwareness(target, cause, amount)
|
||||
}
|
||||
|
||||
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
|
||||
AutomaticOperation = false
|
||||
super.DestructionAwareness(target, cause)
|
||||
CancelJammeredSound(target)
|
||||
CancelJammeredStatus(target)
|
||||
|
|
@ -107,22 +218,22 @@ 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
|
||||
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 {
|
||||
case Some(tplayer) =>
|
||||
seat.unmount(tplayer)
|
||||
tplayer.VehicleSeated = None
|
||||
seat.occupant.collect {
|
||||
case player: Player =>
|
||||
seat.unmount(player)
|
||||
player.VehicleSeated = None
|
||||
zone.VehicleEvents ! VehicleServiceMessage(
|
||||
zone.id,
|
||||
VehicleAction.KickPassenger(tplayer.GUID, 4, wasKickedByDriver, turret.GUID)
|
||||
VehicleAction.KickPassenger(player.GUID, 4, wasKickedByDriver, turret.GUID)
|
||||
)
|
||||
case None => ;
|
||||
}
|
||||
}
|
||||
Some(time.getOrElse(Deployable.cleanup) + Deployable.cleanup)
|
||||
|
|
@ -132,6 +243,11 @@ 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))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.ce.InteractWithMines
|
||||
import net.psforever.objects.ce.{InteractWithMines, InteractWithTurrets}
|
||||
import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition}
|
||||
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
|
||||
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile}
|
||||
|
|
@ -92,6 +92,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
|
|||
with MountableEntity {
|
||||
interaction(new InteractWithEnvironment())
|
||||
interaction(new InteractWithMines(range = 20))
|
||||
interaction(new InteractWithTurrets())
|
||||
interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20))
|
||||
|
||||
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
|
|
|
|||
|
|
@ -224,6 +224,10 @@ case class Avatar(
|
|||
false
|
||||
}
|
||||
|
||||
override def hashCode(): Int = {
|
||||
id
|
||||
}
|
||||
|
||||
/** Avatar assertions
|
||||
* These protect against programming errors by asserting avatar properties have correct values
|
||||
* They may or may not be disabled for live applications
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.ce
|
||||
|
||||
import net.psforever.objects.GlobalDefinitions
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
|
||||
import net.psforever.objects.zones.blockmap.SectorPopulation
|
||||
import net.psforever.objects.zones.{InteractsWithZone, ZoneInteraction, ZoneInteractionType}
|
||||
import net.psforever.objects.sourcing.SourceUniqueness
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
case object TurretInteraction extends ZoneInteractionType
|
||||
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
class InteractWithTurrets()
|
||||
extends ZoneInteraction {
|
||||
def range: Float = InteractWithTurrets.Range
|
||||
|
||||
def Type: TurretInteraction.type = TurretInteraction
|
||||
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
|
||||
target match {
|
||||
case clarifiedTarget: AutomatedTurret.Target =>
|
||||
val pos = clarifiedTarget.Position
|
||||
val unique = SourceUniqueness(clarifiedTarget)
|
||||
val targets = getTurretTargets(sector, pos).filter { turret => turret.Definition.AutoFire.nonEmpty && turret.Detected(unique).isEmpty }
|
||||
targets.foreach { t => t.Actor ! AutomatedTurretBehavior.Alert(clarifiedTarget) }
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
private def getTurretTargets(
|
||||
sector: SectorPopulation,
|
||||
position: Vector3
|
||||
): Iterable[PlanetSideServerObject with AutomatedTurret] = {
|
||||
val list: Iterable[AutomatedTurret] = sector
|
||||
.deployableList
|
||||
.collect {
|
||||
case turret: AutomatedTurret => turret
|
||||
} ++ sector
|
||||
.amenityList
|
||||
.collect {
|
||||
case turret: AutomatedTurret => turret
|
||||
}
|
||||
list.collect {
|
||||
case turret: AutomatedTurret
|
||||
if {
|
||||
val stats = turret.Definition.AutoFire
|
||||
stats.nonEmpty &&
|
||||
AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(stats, turret.Position, position, range, result = -1)
|
||||
} => turret
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param target na
|
||||
*/
|
||||
def resetInteraction(target: InteractsWithZone): Unit = {
|
||||
getTurretTargets(
|
||||
target.getInteractionSector(),
|
||||
target.Position.xy
|
||||
).foreach { turret =>
|
||||
turret.Actor ! AutomatedTurretBehavior.Reset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object InteractWithTurrets {
|
||||
private lazy val Range: Float = {
|
||||
Seq(
|
||||
GlobalDefinitions.spitfire_turret,
|
||||
GlobalDefinitions.spitfire_cloaked,
|
||||
GlobalDefinitions.spitfire_aa,
|
||||
GlobalDefinitions.manned_turret
|
||||
)
|
||||
.flatMap(_.AutoFire)
|
||||
.map(_.ranges.detection)
|
||||
.max
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import scala.util.{Success, Try}
|
|||
|
||||
class AmmoBoxConverter extends ObjectCreateConverter[AmmoBox] {
|
||||
override def ConstructorData(obj: AmmoBox): Try[CommonFieldData] = {
|
||||
Success(CommonFieldData()(false))
|
||||
Success(CommonFieldData()(flag = false))
|
||||
}
|
||||
|
||||
override def DetailedConstructorData(obj: AmmoBox): Try[DetailedAmmoBoxData] = {
|
||||
|
|
@ -19,9 +19,9 @@ class AmmoBoxConverter extends ObjectCreateConverter[AmmoBox] {
|
|||
PlanetSideEmpire.NEUTRAL,
|
||||
bops = false,
|
||||
alternate = false,
|
||||
true,
|
||||
v1 = true,
|
||||
None,
|
||||
false,
|
||||
jammered = false,
|
||||
None,
|
||||
None,
|
||||
PlanetSideGUID(0)
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ class SmallTurretConverter extends ObjectCreateConverter[TurretDeployable]() {
|
|||
obj.Faction,
|
||||
bops = false,
|
||||
alternate = false,
|
||||
false,
|
||||
v1 = true,
|
||||
None,
|
||||
jammered = obj.Jammed,
|
||||
obj.Jammed,
|
||||
Some(true),
|
||||
None,
|
||||
obj.OwnerGuid match {
|
||||
|
|
@ -45,9 +45,9 @@ class SmallTurretConverter extends ObjectCreateConverter[TurretDeployable]() {
|
|||
obj.Faction,
|
||||
bops = false,
|
||||
alternate = true,
|
||||
false,
|
||||
v1 = false,
|
||||
None,
|
||||
false,
|
||||
jammered = false,
|
||||
Some(false),
|
||||
None,
|
||||
PlanetSideGUID(0)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class ToolConverter extends ObjectCreateConverter[Tool]() {
|
|||
obj.Faction,
|
||||
bops = false,
|
||||
alternate = false,
|
||||
true,
|
||||
v1 = true,
|
||||
None,
|
||||
obj.Jammed,
|
||||
None,
|
||||
|
|
@ -47,7 +47,7 @@ class ToolConverter extends ObjectCreateConverter[Tool]() {
|
|||
obj.Faction,
|
||||
bops = false,
|
||||
alternate = false,
|
||||
true,
|
||||
v1 = true,
|
||||
None,
|
||||
obj.Jammed,
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
package net.psforever.objects.equipment
|
||||
|
||||
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.objects.ce.{DeployableCategory, DeployedItem}
|
||||
import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
|
||||
import net.psforever.objects.vital.{DamagingActivity, InGameHistory, Vitality}
|
||||
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)
|
||||
|
||||
|
|
@ -21,6 +23,7 @@ object EffectTarget {
|
|||
object Validation {
|
||||
type Value = PlanetSideGameObject => Boolean
|
||||
|
||||
//noinspection ScalaUnusedSymbol
|
||||
def Invalid(target: PlanetSideGameObject): Boolean = false
|
||||
|
||||
def Medical(target: PlanetSideGameObject): Boolean =
|
||||
|
|
@ -72,10 +75,10 @@ object EffectTarget {
|
|||
}
|
||||
|
||||
/**
|
||||
* To repair at this landing pad, the vehicle:
|
||||
* To repair at this landing pad, the vehicle must:
|
||||
* be a flight vehicle,
|
||||
* must have some health already, but does not have all its health,
|
||||
* and can not have taken damage in the last five seconds.
|
||||
* have some health already, but does not have all its health, and
|
||||
* have not taken damage in the last five seconds.
|
||||
*/
|
||||
def PadLanding(target: PlanetSideGameObject): Boolean =
|
||||
target match {
|
||||
|
|
@ -185,5 +188,269 @@ object EffectTarget {
|
|||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
def SmallRoboticsTurretValidatePlayerTarget(target: PlanetSideGameObject): Boolean =
|
||||
target match {
|
||||
case p: Player
|
||||
if p.ExoSuit != ExoSuitType.MAX && p.VehicleSeated.isEmpty =>
|
||||
val now = System.currentTimeMillis()
|
||||
val pos = p.Position
|
||||
val faction = p.Faction
|
||||
val sector = p.Zone.blockMap.sector(pos, range = 51f)
|
||||
//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 cloakedByInfiltrationSuit = p.ExoSuit == ExoSuitType.Infiltration && p.Cloaked
|
||||
lazy val silentRunActive = p.avatar.implants.flatten.find(a => a.definition.implantType == ImplantType.SilentRun).exists(_.active)
|
||||
lazy val movingFast = p.isMoving(test = 15.5d)
|
||||
lazy val isCrouched = p.Crouching
|
||||
lazy val isMoving = p.isMoving(test = 1d)
|
||||
lazy val isJumping = p.Jumping
|
||||
if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
|
||||
else if (entityTookDamage(p, now) || usedEquipment) true
|
||||
else if (radarCloakedSensor(sector, pos, faction) || silentRunActive) false
|
||||
else if (radarEnhancedInterlink(sector, pos, faction)) true
|
||||
else if (radarEnhancedSensor(sector, pos, faction)) !isCrouched && isMoving
|
||||
else if (cloakedByInfiltrationSuit) isJumping || movingFast
|
||||
else isJumping || movingFast
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
def SmallRoboticsTurretValidateMaxTarget(target: PlanetSideGameObject): Boolean =
|
||||
target match {
|
||||
case p: Player
|
||||
if p.ExoSuit == ExoSuitType.MAX && p.VehicleSeated.isEmpty =>
|
||||
val now = System.currentTimeMillis()
|
||||
val pos = p.Position
|
||||
val faction = p.Faction
|
||||
val sector = p.Zone.blockMap.sector(pos, range = 51f)
|
||||
lazy val usedEquipment = p.Holsters().flatMap(_.Equipment)
|
||||
.collect { case t: Tool => now - t.LastDischarge }
|
||||
.exists(_ < 2000L)
|
||||
lazy val isMoving = p.isMoving(test = 1d)
|
||||
if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
|
||||
else if (entityTookDamage(p, now) || usedEquipment) true
|
||||
else if (radarCloakedSensor(sector, pos, faction)) false
|
||||
else if (radarEnhancedInterlink(sector, pos, faction)) true
|
||||
else isMoving
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
def SmallRoboticsTurretValidateGroundVehicleTarget(target: PlanetSideGameObject): Boolean =
|
||||
target match {
|
||||
case v: Vehicle
|
||||
if !GlobalDefinitions.isFlightVehicle(v.Definition) && v.MountedIn.isEmpty && v.Seats.values.exists(_.isOccupied) =>
|
||||
val now = System.currentTimeMillis()
|
||||
val vdef = v.Definition
|
||||
val pos = v.Position
|
||||
lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
|
||||
lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
|
||||
.collect { case t: Tool => now - t.LastDischarge }
|
||||
.exists(_ < 2000L)
|
||||
if (
|
||||
(vdef == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) ||
|
||||
radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)
|
||||
) false
|
||||
else !v.Cloaked && v.isMoving(test = 1d) || entityTookDamage(v, now) || usedEquipment
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
def SmallRoboticsTurretValidateAircraftTarget(target: PlanetSideGameObject): Boolean =
|
||||
target match {
|
||||
case v: Vehicle
|
||||
if GlobalDefinitions.isFlightVehicle(v.Definition) && v.Seats.values.exists(_.isOccupied) =>
|
||||
val now = System.currentTimeMillis()
|
||||
val pos = v.Position
|
||||
val sector = v.Zone.blockMap.sector(pos, range = 51f)
|
||||
lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
|
||||
.collect { case t: Tool => now - t.LastDischarge }
|
||||
.exists(_ < 2000L)
|
||||
if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
|
||||
else !v.Cloaked && (v.isFlying || v.isMoving(test = 1d)) || entityTookDamage(v, now) || usedEquipment
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
def FacilityTurretValidateMaxTarget(target: PlanetSideGameObject): Boolean =
|
||||
target match {
|
||||
case p: Player
|
||||
if p.ExoSuit == ExoSuitType.MAX && p.VehicleSeated.isEmpty =>
|
||||
val now = System.currentTimeMillis()
|
||||
val pos = p.Position
|
||||
val faction = p.Faction
|
||||
val sector = p.Zone.blockMap.sector(p.Position, range = 51f)
|
||||
lazy val usedEquipment = p.Holsters().flatMap(_.Equipment)
|
||||
.collect { case t: Tool => now - t.LastDischarge }
|
||||
.exists(_ < 2000L)
|
||||
if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
|
||||
else if (radarCloakedSensor(sector, pos, faction)) entityTookDamage(p, now) || usedEquipment
|
||||
else if (radarEnhancedInterlink(sector, pos, faction)) true
|
||||
else p.isMoving(test = 15.5d)
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
def FacilityTurretValidateGroundVehicleTarget(target: PlanetSideGameObject): Boolean =
|
||||
target match {
|
||||
case v: Vehicle
|
||||
if !GlobalDefinitions.isFlightVehicle(v.Definition) && v.MountedIn.isEmpty && v.Seats.values.exists(_.isOccupied) =>
|
||||
val now = System.currentTimeMillis()
|
||||
val vdef = v.Definition
|
||||
val pos = v.Position
|
||||
lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
|
||||
lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
|
||||
.collect { case t: Tool => now - t.LastDischarge }
|
||||
.exists(_ < 2000L)
|
||||
if (
|
||||
(vdef == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) ||
|
||||
vdef == GlobalDefinitions.two_man_assault_buggy ||
|
||||
GlobalDefinitions.isAtvVehicle(vdef) || //todo should all ATV types get carte blanche treatment?
|
||||
radarCloakedAms(sector, pos) ||
|
||||
radarCloakedAegis(sector, pos)
|
||||
) false
|
||||
else v.isMoving(test = 1d) || entityTookDamage(v, now) || usedEquipment
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
def FacilityTurretValidateAircraftTarget(target: PlanetSideGameObject): Boolean =
|
||||
target match {
|
||||
case v: Vehicle
|
||||
if GlobalDefinitions.isFlightVehicle(v.Definition) && v.Seats.values.exists(_.isOccupied) =>
|
||||
val now = System.currentTimeMillis()
|
||||
val pos = v.Position
|
||||
lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
|
||||
lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
|
||||
.collect { case t: Tool => now - t.LastDischarge }
|
||||
.exists(_ < 2000L)
|
||||
// from the perspective of a mosquito, at 5th gauge, forward velocity is 59~60
|
||||
lazy val movingFast = Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero).xy) > 3721f //61
|
||||
lazy val isMoving = v.isMoving(test = 1d)
|
||||
if (v.Cloaked || radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
|
||||
else if (v.Definition == GlobalDefinitions.mosquito) movingFast
|
||||
else v.isFlying && (isMoving || entityTookDamage(v, now) || usedEquipment)
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
def AutoTurretValidateMountableEntityTarget(target: PlanetSideGameObject): Boolean =
|
||||
target match {
|
||||
case _: Vehicle =>
|
||||
false //strict vehicles are handled by other validations
|
||||
case t: WeaponTurret with Vitality =>
|
||||
t.Seats.values.exists(_.isOccupied)
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
def AutoTurretBlankPlayerTarget(target: PlanetSideGameObject): Boolean =
|
||||
target match {
|
||||
case p: Player =>
|
||||
val pos = p.Position
|
||||
lazy val sector = p.Zone.blockMap.sector(p.Position, range = 51f)
|
||||
p.VehicleSeated.nonEmpty || radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
def AutoTurretBlankVehicleTarget(target: PlanetSideGameObject): Boolean =
|
||||
target match {
|
||||
case v: Vehicle =>
|
||||
val pos = v.Position
|
||||
lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
|
||||
(v.Definition == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) ||
|
||||
v.MountedIn.nonEmpty ||
|
||||
v.Cloaked ||
|
||||
radarCloakedAms(sector, pos) ||
|
||||
radarCloakedAegis(sector, pos)
|
||||
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) < 169f //12+1m
|
||||
}.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) < 121f //10+1m
|
||||
}.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) < 961f //30+1m
|
||||
}.contains(true)
|
||||
}
|
||||
|
||||
private def entityTookDamage(
|
||||
obj: InGameHistory,
|
||||
now: Long = System.currentTimeMillis(),
|
||||
interval: Long = 2000L
|
||||
): Boolean = {
|
||||
obj.VitalsHistory()
|
||||
.findLast(_.isInstanceOf[DamagingActivity])
|
||||
.exists(dam => now - dam.time < interval)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.objects.serverobject.mount
|
||||
|
||||
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
|
||||
import net.psforever.objects.sourcing.SourceEntry
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.base.{DamageResolution, DamageType}
|
||||
import net.psforever.objects.vital.etc.RadiationReason
|
||||
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||
import net.psforever.objects.vital.resistance.StandardResistanceProfile
|
||||
import net.psforever.objects.zones.blockmap.SectorPopulation
|
||||
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
/**
|
||||
* This game entity may infrequently test whether it may interact with radiation cloud projectiles
|
||||
* that may be emitted in the game environment for a limited amount of time.
|
||||
* Since the entity in question is a vehicle, the occupants of the vehicle get tested their interaction.
|
||||
*/
|
||||
class InteractWithRadiationCloudsSeatedInEntity(
|
||||
private val obj: Mountable with StandardResistanceProfile,
|
||||
val range: Float
|
||||
) extends ZoneInteraction {
|
||||
/**
|
||||
* radiation clouds that, though detected, are skipped from affecting the target;
|
||||
* in between interaction tests, a memory of the clouds that were tested last are retained and
|
||||
* are excluded from being tested this next time;
|
||||
* clouds that are detected a second time are cleared from the list and are available to be tested next time
|
||||
*/
|
||||
private var skipTargets: List[PlanetSideGUID] = List()
|
||||
|
||||
def Type: RadiationInMountableInteraction.type = RadiationInMountableInteraction
|
||||
|
||||
/**
|
||||
* Drive into a radiation cloud and all the vehicle's occupants suffer the consequences.
|
||||
* @param sector the portion of the block map being tested
|
||||
* @param target the fixed element in this test
|
||||
*/
|
||||
override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
|
||||
val position = target.Position
|
||||
//collect all projectiles in sector/range
|
||||
val projectiles = sector
|
||||
.projectileList
|
||||
.filter { cloud =>
|
||||
val definition = cloud.Definition
|
||||
definition.radiation_cloud &&
|
||||
definition.AllDamageTypes.contains(DamageType.Radiation) &&
|
||||
{
|
||||
val radius = definition.DamageRadius
|
||||
Zone.distanceCheck(target, cloud, radius * radius)
|
||||
}
|
||||
}
|
||||
.distinct
|
||||
val notSkipped = projectiles.filterNot { t => skipTargets.contains(t.GUID) }
|
||||
skipTargets = notSkipped.map { _.GUID }
|
||||
if (notSkipped.nonEmpty) {
|
||||
(
|
||||
//isolate one of each type of projectile
|
||||
notSkipped
|
||||
.foldLeft(Nil: List[Projectile]) {
|
||||
(acc, next) => if (acc.exists { _.profile == next.profile }) acc else next :: acc
|
||||
},
|
||||
obj.Seats
|
||||
.values
|
||||
.collect { case seat => seat.occupant }
|
||||
.flatten
|
||||
) match {
|
||||
case (uniqueProjectiles, targets) if uniqueProjectiles.nonEmpty && targets.nonEmpty =>
|
||||
val shielding = obj.RadiationShielding
|
||||
targets.foreach { t =>
|
||||
uniqueProjectiles.foreach { p =>
|
||||
t.Actor ! Vitality.Damage(
|
||||
DamageInteraction(
|
||||
SourceEntry(t),
|
||||
RadiationReason(
|
||||
ProjectileQuality.modifiers(p, DamageResolution.Radiation, t, t.Position, None),
|
||||
t.DamageModel,
|
||||
shielding
|
||||
),
|
||||
position
|
||||
).calculate()
|
||||
)
|
||||
}
|
||||
}
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Any radiation clouds blocked from being tested should be cleared.
|
||||
* All that can be done is blanking our retained previous effect targets.
|
||||
* @param target the fixed element in this test
|
||||
*/
|
||||
def resetInteraction(target: InteractsWithZone): Unit = {
|
||||
skipTargets = List()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ trait MountableBehavior {
|
|||
!obj.Destroyed
|
||||
}
|
||||
|
||||
private def tryMount(
|
||||
protected def tryMount(
|
||||
obj: PlanetSideServerObject with Mountable,
|
||||
seatNumber: Int,
|
||||
player: Player
|
||||
|
|
@ -105,12 +105,12 @@ 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
|
||||
})
|
||||
}
|
||||
|
||||
private def tryDismount(
|
||||
protected def tryDismount(
|
||||
obj: Mountable,
|
||||
seatNumber: Int,
|
||||
user: Player,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package net.psforever.objects.serverobject.mount
|
||||
|
||||
import net.psforever.objects.zones.ZoneInteractionType
|
||||
|
||||
case object RadiationInMountableInteraction extends ZoneInteractionType
|
||||
|
|
@ -3,7 +3,7 @@ package net.psforever.objects.serverobject.structures.participation
|
|||
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.avatar.scoring.Kill
|
||||
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
|
||||
import net.psforever.objects.sourcing.UniquePlayer
|
||||
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
||||
|
||||
import scala.collection.mutable
|
||||
|
|
@ -143,7 +143,7 @@ object FacilityHackParticipation {
|
|||
killTime <= end &&
|
||||
Vector3.DistanceSquared(centerXY, k.info.interaction.hitPos.xy) < distanceSq
|
||||
}
|
||||
(PlayerSource(p).unique, math.min(d, duration).toFloat / duration.toFloat, killList)
|
||||
(UniquePlayer(p), math.min(d, duration).toFloat / duration.toFloat, killList)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,42 +5,46 @@ import net.psforever.objects.serverobject.mount.Mountable
|
|||
import net.psforever.objects.serverobject.structures.Amenity
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
/**
|
||||
* The behaviours corresponding to an Amenity that is marked as being CaptureTerminalAware
|
||||
* @see CaptureTerminalAware
|
||||
*/
|
||||
trait CaptureTerminalAwareBehavior {
|
||||
def CaptureTerminalAwareObject : Amenity with CaptureTerminalAware
|
||||
def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware
|
||||
|
||||
val captureTerminalAwareBehaviour: Receive = {
|
||||
case CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured) =>
|
||||
isResecured match {
|
||||
case true => ; // CC is resecured
|
||||
case false => // CC is hacked
|
||||
// Remove seated occupants for mountables
|
||||
CaptureTerminalAwareObject match {
|
||||
case mountable: Mountable =>
|
||||
case CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, true) =>
|
||||
captureTerminalIsResecured(terminal)
|
||||
|
||||
val guid = mountable.GUID
|
||||
val zone = mountable.Zone
|
||||
val zoneId = zone.id
|
||||
val events = zone.VehicleEvents
|
||||
case CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, _) =>
|
||||
captureTerminalIsHacked(terminal)
|
||||
}
|
||||
|
||||
mountable.Seats.values.zipWithIndex.foreach {
|
||||
case (seat, seat_num) =>
|
||||
seat.occupant match {
|
||||
case Some(player) =>
|
||||
seat.unmount(player)
|
||||
player.VehicleSeated = None
|
||||
if (player.HasGUID) {
|
||||
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, seat_num, true, guid))
|
||||
}
|
||||
case None => ;
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
protected def captureTerminalIsResecured(@unused terminal: CaptureTerminal): Unit = { /* intentionally blank */ }
|
||||
|
||||
protected def captureTerminalIsHacked(@unused terminal: CaptureTerminal): Unit = {
|
||||
// Remove seated occupants for mountables
|
||||
CaptureTerminalAwareObject match {
|
||||
case mountable: Mountable =>
|
||||
val guid = mountable.GUID
|
||||
val zone = mountable.Zone
|
||||
val zoneId = zone.id
|
||||
val events = zone.VehicleEvents
|
||||
mountable.Seats.values.zipWithIndex.foreach {
|
||||
case (seat, seat_num) =>
|
||||
seat.occupant.collect {
|
||||
case player =>
|
||||
seat.unmount(player)
|
||||
player.VehicleSeated = None
|
||||
if (player.HasGUID) {
|
||||
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, seat_num, unk2=true, guid))
|
||||
}
|
||||
}
|
||||
}
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,35 @@
|
|||
package net.psforever.objects.serverobject.turret
|
||||
|
||||
import net.psforever.objects.equipment.JammableUnit
|
||||
import net.psforever.objects.serverobject.structures.Amenity
|
||||
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
|
||||
|
||||
class FacilityTurret(tDef: FacilityTurretDefinition)
|
||||
extends Amenity
|
||||
with WeaponTurret
|
||||
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) {
|
||||
Building.NoBuilding
|
||||
} else {
|
||||
super.Owner
|
||||
}
|
||||
}
|
||||
|
||||
def Definition: FacilityTurretDefinition = tDef
|
||||
}
|
||||
|
|
@ -27,9 +46,6 @@ object FacilityTurret {
|
|||
new FacilityTurret(tDef)
|
||||
}
|
||||
|
||||
final case class RechargeAmmo()
|
||||
final case class WeaponDischarged()
|
||||
|
||||
import akka.actor.ActorContext
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,190 +1,175 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.turret
|
||||
|
||||
import akka.actor.Cancellable
|
||||
import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool}
|
||||
import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons}
|
||||
import net.psforever.objects.{GlobalDefinitions, Player, Tool}
|
||||
import net.psforever.objects.equipment.Ammo
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
|
||||
import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTurret}
|
||||
import net.psforever.objects.serverobject.damage.Damageable
|
||||
import net.psforever.objects.serverobject.hackable.GenericHackables
|
||||
import net.psforever.objects.serverobject.hackable.GenericHackables.getTurretUpgradeTime
|
||||
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableWeaponTurret}
|
||||
import net.psforever.objects.serverobject.structures.PoweredAmenityControl
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.repair.AmenityAutoRepair
|
||||
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
|
||||
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAwareBehavior}
|
||||
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret.Target
|
||||
import net.psforever.objects.serverobject.turret.auto.{AffectedByAutomaticTurretFire, AutomatedTurret, AutomatedTurretBehavior}
|
||||
import net.psforever.objects.vital.interaction.DamageResult
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.packet.game.ChangeFireModeMessage
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.vehicle.support.TurretUpgrader
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import net.psforever.types.{BailType, PlanetSideEmpire, PlanetSideGUID}
|
||||
|
||||
/**
|
||||
* An `Actor` that handles messages being dispatched to a specific `MannedTurret`.<br>
|
||||
* <br>
|
||||
* Mounted turrets have only slightly different entry requirements than a normal vehicle
|
||||
* because they encompass both faction-specific facility turrets
|
||||
* and faction-blind cavern sentry turrets.
|
||||
*
|
||||
* @param turret the `MannedTurret` object being governed
|
||||
*/
|
||||
* A control agency that handles messages being dispatched to a specific `FacilityTurret`.
|
||||
* These turrets are attached specifically to surface-level facilities and field towers.
|
||||
* @param turret the `FacilityTurret` object being governed
|
||||
*/
|
||||
class FacilityTurretControl(turret: FacilityTurret)
|
||||
extends PoweredAmenityControl
|
||||
with FactionAffinityBehavior.Check
|
||||
with MountableBehavior
|
||||
with DamageableWeaponTurret
|
||||
with RepairableWeaponTurret
|
||||
extends PoweredAmenityControl
|
||||
with AmenityAutoRepair
|
||||
with JammableMountedWeapons
|
||||
with MountableTurretControl
|
||||
with AutomatedTurretBehavior
|
||||
with AffectedByAutomaticTurretFire
|
||||
with CaptureTerminalAwareBehavior {
|
||||
def TurretObject: FacilityTurret = turret
|
||||
def FactionObject: FacilityTurret = turret
|
||||
def MountableObject: FacilityTurret = turret
|
||||
def JammableObject: FacilityTurret = turret
|
||||
def DamageableObject: FacilityTurret = turret
|
||||
def RepairableObject: FacilityTurret = turret
|
||||
def AutoRepairObject: FacilityTurret = turret
|
||||
def AutomatedTurretObject: FacilityTurret = turret
|
||||
def CaptureTerminalAwareObject: FacilityTurret = turret
|
||||
def AffectedObject: FacilityTurret = turret
|
||||
|
||||
// Used for timing ammo recharge for vanu turrets in caves
|
||||
var weaponAmmoRechargeTimer: Cancellable = Default.Cancellable
|
||||
private var testToResetToDefaultFireMode: Boolean = false
|
||||
|
||||
AutomaticOperation = true
|
||||
|
||||
override def postStop(): Unit = {
|
||||
super.postStop()
|
||||
damageableWeaponTurretPostStop()
|
||||
automaticTurretPostStop()
|
||||
stopAutoRepair()
|
||||
}
|
||||
|
||||
def commonBehavior: Receive =
|
||||
checkBehavior
|
||||
.orElse(jammableBehavior)
|
||||
.orElse(dismountBehavior)
|
||||
.orElse(takesDamage)
|
||||
.orElse(canBeRepairedByNanoDispenser)
|
||||
.orElse(autoRepairBehavior)
|
||||
.orElse(captureTerminalAwareBehaviour)
|
||||
private val upgradeableTurret: Receive = {
|
||||
case CommonMessages.Use(player, Some((item: Tool, upgradeValue: Int)))
|
||||
if player.Faction == TurretObject.Faction &&
|
||||
item.Definition == GlobalDefinitions.nano_dispenser && item.AmmoType == Ammo.upgrade_canister &&
|
||||
item.Magazine > 0 && TurretObject.Seats.values.forall(!_.isOccupied) =>
|
||||
TurretUpgrade.values.find(_.id == upgradeValue).foreach {
|
||||
case upgrade
|
||||
if TurretObject.Upgrade != upgrade && TurretObject.Definition.WeaponPaths.values
|
||||
.flatMap(_.keySet)
|
||||
.exists(_ == upgrade) =>
|
||||
AutomaticOperation = false
|
||||
sender() ! CommonMessages.Progress(
|
||||
1.25f,
|
||||
WeaponTurrets.FinishUpgradingMannedTurret(TurretObject, player, item, upgrade),
|
||||
GenericHackables.TurretUpgradingTickAction(progressType = 2, player, TurretObject, item.GUID)
|
||||
)
|
||||
}
|
||||
case TurretUpgrader.UpgradeCompleted(_) =>
|
||||
CurrentTargetLastShotReported = System.currentTimeMillis() + 2000L
|
||||
AutomaticOperation = true
|
||||
}
|
||||
|
||||
def poweredStateLogic: Receive =
|
||||
override def commonBehavior: Receive = super.commonBehavior
|
||||
.orElse(automatedTurretBehavior)
|
||||
.orElse(takeAutomatedDamage)
|
||||
.orElse(autoRepairBehavior)
|
||||
.orElse(captureTerminalAwareBehaviour)
|
||||
|
||||
override def poweredStateLogic: Receive =
|
||||
commonBehavior
|
||||
.orElse(mountBehavior)
|
||||
.orElse(upgradeableTurret)
|
||||
.orElse {
|
||||
case CommonMessages.Use(player, Some((item: Tool, upgradeValue: Int)))
|
||||
if player.Faction == turret.Faction &&
|
||||
item.Definition == GlobalDefinitions.nano_dispenser && item.AmmoType == Ammo.upgrade_canister &&
|
||||
item.Magazine > 0 && turret.Seats.values.forall(!_.isOccupied) =>
|
||||
TurretUpgrade.values.find(_.id == upgradeValue) match {
|
||||
case Some(upgrade)
|
||||
if turret.Upgrade != upgrade && turret.Definition.WeaponPaths.values
|
||||
.flatMap(_.keySet)
|
||||
.exists(_ == upgrade) =>
|
||||
turret.setMiddleOfUpgrade(true)
|
||||
sender() ! CommonMessages.Progress(
|
||||
1.25f,
|
||||
WeaponTurrets.FinishUpgradingMannedTurret(turret, player, item, upgrade),
|
||||
GenericHackables.TurretUpgradingTickAction(progressType = 2, player, turret, item.GUID)
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
case FacilityTurret.WeaponDischarged() =>
|
||||
if (weaponAmmoRechargeTimer != Default.Cancellable) {
|
||||
weaponAmmoRechargeTimer.cancel()
|
||||
}
|
||||
|
||||
weaponAmmoRechargeTimer = context.system.scheduler.scheduleWithFixedDelay(
|
||||
3 seconds,
|
||||
200 milliseconds,
|
||||
self,
|
||||
FacilityTurret.RechargeAmmo()
|
||||
)
|
||||
|
||||
case FacilityTurret.RechargeAmmo() =>
|
||||
turret.ControlledWeapon(wepNumber = 1).foreach {
|
||||
case weapon: Tool =>
|
||||
// recharge when last shot fired 3s delay, +1, 200ms interval
|
||||
if (weapon.Magazine < weapon.MaxMagazine && System.currentTimeMillis() - weapon.LastDischarge > 3000L) {
|
||||
weapon.Magazine += 1
|
||||
val seat = turret.Seat(0).get
|
||||
seat.occupant match {
|
||||
case Some(player : Player) =>
|
||||
turret.Zone.LocalEvents ! LocalServiceMessage(
|
||||
turret.Zone.id,
|
||||
LocalAction.RechargeVehicleWeapon(player.GUID, turret.GUID, weapon.GUID)
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
else if (weapon.Magazine == weapon.MaxMagazine && weaponAmmoRechargeTimer != Default.Cancellable) {
|
||||
weaponAmmoRechargeTimer.cancel()
|
||||
weaponAmmoRechargeTimer = Default.Cancellable
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
def unpoweredStateLogic: Receive =
|
||||
override def unpoweredStateLogic: Receive =
|
||||
commonBehavior
|
||||
.orElse {
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
override protected def mountTest(
|
||||
obj: PlanetSideServerObject with Mountable,
|
||||
seatNumber: Int,
|
||||
player: Player): Boolean = {
|
||||
(!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed && !turret.isUpgrading ||
|
||||
System.currentTimeMillis() - getTurretUpgradeTime >= 1500L
|
||||
super.mountTest(obj, seatNumber, player) &&
|
||||
(!TurretObject.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L)
|
||||
}
|
||||
|
||||
override protected def tryMount(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = {
|
||||
AutomaticOperation = false //turn off
|
||||
if (!super.tryMount(obj, seatNumber, player)) {
|
||||
AutomaticOperation = true //revert?
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override protected def tryDismount(obj: Mountable, seatNumber: Int, player: Player, bailType: BailType.Value): Boolean = {
|
||||
AutomaticOperation = AutomaticOperationFunctionalityChecksExceptMounting //turn on, if can turn on
|
||||
if (!super.tryDismount(obj, seatNumber, player, bailType)) {
|
||||
AutomaticOperation = false //revert
|
||||
false
|
||||
} else {
|
||||
CurrentTargetLastShotReported = System.currentTimeMillis() + 4000L
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any) : Unit = {
|
||||
tryAutoRepair()
|
||||
if (AutomaticOperation) {
|
||||
if (TurretObject.Health < TurretObject.Definition.DamageDisablesAt) {
|
||||
AutomaticOperation = false
|
||||
} else {
|
||||
amount match {
|
||||
case 0 => ()
|
||||
case _ => attemptRetaliation(target, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
super.DamageAwareness(target, cause, amount)
|
||||
}
|
||||
|
||||
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
|
||||
tryAutoRepair()
|
||||
super.DestructionAwareness(target, cause)
|
||||
val zone = target.Zone
|
||||
val zoneId = zone.id
|
||||
val events = zone.AvatarEvents
|
||||
val tguid = target.GUID
|
||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 1))
|
||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 1))
|
||||
tryAutoRepair()
|
||||
AutomaticOperation = false
|
||||
selfReportingCleanUp()
|
||||
}
|
||||
|
||||
override def PerformRepairs(target : Damageable.Target, amount : Int) : Int = {
|
||||
val newHealth = super.PerformRepairs(target, amount)
|
||||
if(newHealth == target.Definition.MaxHealth) {
|
||||
if (!AutomaticOperation && newHealth > target.Definition.DamageDisablesAt) {
|
||||
AutomaticOperation = true
|
||||
}
|
||||
if (newHealth == target.Definition.MaxHealth) {
|
||||
stopAutoRepair()
|
||||
}
|
||||
newHealth
|
||||
}
|
||||
|
||||
override def Restoration(obj: Damageable.Target): Unit = {
|
||||
super.Restoration(obj)
|
||||
val zone = turret.Zone
|
||||
val zoneId = zone.id
|
||||
val events = zone.AvatarEvents
|
||||
val tguid = turret.GUID
|
||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 0))
|
||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 0))
|
||||
}
|
||||
|
||||
override def tryAutoRepair() : Boolean = {
|
||||
isPowered && super.tryAutoRepair()
|
||||
}
|
||||
|
||||
def powerTurnOffCallback(): Unit = {
|
||||
stopAutoRepair()
|
||||
AutomaticOperation = false
|
||||
//kick all occupants
|
||||
val guid = turret.GUID
|
||||
val zone = turret.Zone
|
||||
val guid = TurretObject.GUID
|
||||
val zone = TurretObject.Zone
|
||||
val zoneId = zone.id
|
||||
val events = zone.VehicleEvents
|
||||
turret.Seats.values.foreach(seat =>
|
||||
TurretObject.Seats.values.foreach(seat =>
|
||||
seat.occupant match {
|
||||
case Some(player) =>
|
||||
seat.unmount(player)
|
||||
|
|
@ -192,12 +177,159 @@ class FacilityTurretControl(turret: FacilityTurret)
|
|||
if (player.HasGUID) {
|
||||
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2=false, guid))
|
||||
}
|
||||
case None => ;
|
||||
case None => ()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def powerTurnOnCallback(): Unit = {
|
||||
tryAutoRepair()
|
||||
AutomaticOperation = true
|
||||
}
|
||||
|
||||
override def AutomaticOperation_=(state: Boolean): Boolean = {
|
||||
val result = super.AutomaticOperation_=(state)
|
||||
testToResetToDefaultFireMode = result && TurretObject.Definition.AutoFire.exists(_.revertToDefaultFireMode)
|
||||
result
|
||||
}
|
||||
|
||||
override protected def AutomaticOperationFunctionalityChecks: Boolean = {
|
||||
AutomaticOperationFunctionalityChecksExceptMounting &&
|
||||
!TurretObject.Seats.values.exists(_.isOccupied)
|
||||
}
|
||||
|
||||
private def AutomaticOperationFunctionalityChecksExceptMounting: Boolean = {
|
||||
AutomaticOperationFunctionalityChecksExceptMountingAndHacking &&
|
||||
(TurretObject.Owner match {
|
||||
case b: Building => !b.CaptureTerminalIsHacked
|
||||
case _ => false
|
||||
})
|
||||
}
|
||||
|
||||
private def AutomaticOperationFunctionalityChecksExceptMountingAndHacking: Boolean = {
|
||||
super.AutomaticOperationFunctionalityChecks &&
|
||||
isPowered &&
|
||||
TurretObject.Owner.Faction != PlanetSideEmpire.NEUTRAL &&
|
||||
!JammableObject.Jammed &&
|
||||
TurretObject.Health >= TurretObject.Definition.DamageDisablesAt &&
|
||||
!TurretObject.isUpgrading
|
||||
}
|
||||
|
||||
private def primaryWeaponFireModeOnly(): Unit = {
|
||||
if (testToResetToDefaultFireMode) {
|
||||
val zone = TurretObject.Zone
|
||||
val zoneid = zone.id
|
||||
val events = zone.VehicleEvents
|
||||
TurretObject.Weapons.values
|
||||
.flatMap(_.Equipment)
|
||||
.collect { case weapon: Tool if weapon.FireModeIndex > 0 =>
|
||||
weapon.FireModeIndex = 0
|
||||
events ! VehicleServiceMessage(
|
||||
zoneid,
|
||||
VehicleAction.SendResponse(Service.defaultPlayerGUID, ChangeFireModeMessage(weapon.GUID, 0))
|
||||
)
|
||||
}
|
||||
}
|
||||
testToResetToDefaultFireMode = false
|
||||
}
|
||||
|
||||
override protected def trySelectNewTarget(): Option[AutomatedTurret.Target] = {
|
||||
primaryWeaponFireModeOnly()
|
||||
super.trySelectNewTarget()
|
||||
}
|
||||
|
||||
protected def engageNewDetectedTarget(
|
||||
target: Target,
|
||||
channel: String,
|
||||
turretGuid: PlanetSideGUID,
|
||||
weaponGuid: PlanetSideGUID
|
||||
): Unit = {
|
||||
val zone = target.Zone
|
||||
primaryWeaponFireModeOnly()
|
||||
AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
|
||||
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
|
||||
}
|
||||
|
||||
protected def noLongerEngageTarget(
|
||||
target: Target,
|
||||
channel: String,
|
||||
turretGuid: PlanetSideGUID,
|
||||
weaponGuid: PlanetSideGUID
|
||||
): Option[Target] = {
|
||||
val zone = target.Zone
|
||||
AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
|
||||
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
|
||||
None
|
||||
}
|
||||
|
||||
protected def testNewDetected(
|
||||
target: Target,
|
||||
channel: String,
|
||||
turretGuid: PlanetSideGUID,
|
||||
weaponGuid: PlanetSideGUID
|
||||
): Unit = {
|
||||
val zone = target.Zone
|
||||
AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
|
||||
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
|
||||
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
|
||||
AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
|
||||
}
|
||||
|
||||
protected def testKnownDetected(
|
||||
target: Target,
|
||||
channel: String,
|
||||
turretGuid: PlanetSideGUID,
|
||||
weaponGuid: PlanetSideGUID
|
||||
): Unit = {
|
||||
val zone = target.Zone
|
||||
AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
|
||||
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
|
||||
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
|
||||
AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
|
||||
}
|
||||
|
||||
override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
|
||||
val startsUnjammed = !JammableObject.Jammed
|
||||
super.TryJammerEffectActivate(target, cause)
|
||||
if (JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) {
|
||||
if (startsUnjammed) {
|
||||
AutomaticOperation = false
|
||||
}
|
||||
//look in direction of cause of jamming
|
||||
val zone = JammableObject.Zone
|
||||
AutomatedTurretBehavior.getAttackVectorFromCause(zone, cause).foreach { attacker =>
|
||||
AutomatedTurretBehavior.startTracking(zone, zone.id, JammableObject.GUID, List(attacker.GUID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def CancelJammeredStatus(target: Any): Unit = {
|
||||
val startsJammed = JammableObject.Jammed
|
||||
super.CancelJammeredStatus(target)
|
||||
if (startsJammed && AutomaticOperation_=(state = true)) {
|
||||
val zone = TurretObject.Zone
|
||||
AutomatedTurretBehavior.stopTracking(zone, zone.id, TurretObject.GUID)
|
||||
}
|
||||
}
|
||||
|
||||
override protected def captureTerminalIsResecured(terminal: CaptureTerminal): Unit = {
|
||||
captureTerminalChanges(terminal, super.captureTerminalIsResecured, actionDelays = 2000L)
|
||||
}
|
||||
|
||||
override protected def captureTerminalIsHacked(terminal: CaptureTerminal): Unit = {
|
||||
captureTerminalChanges(terminal, super.captureTerminalIsHacked, actionDelays = 3000L)
|
||||
}
|
||||
|
||||
private def captureTerminalChanges(
|
||||
terminal: CaptureTerminal,
|
||||
changeFunc: CaptureTerminal=>Unit,
|
||||
actionDelays: Long
|
||||
): Unit = {
|
||||
AutomaticOperation = false
|
||||
changeFunc(terminal)
|
||||
if (AutomaticOperationFunctionalityChecks) {
|
||||
CurrentTargetLastShotReported = System.currentTimeMillis() + actionDelays
|
||||
AutomaticOperation = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.serverobject.turret
|
||||
|
||||
import akka.actor.Actor
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.equipment.JammableMountedWeapons
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
|
||||
import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTurret}
|
||||
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
|
||||
import net.psforever.objects.serverobject.repair.RepairableWeaponTurret
|
||||
import net.psforever.objects.vital.interaction.DamageResult
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
|
||||
trait MountableTurretControl
|
||||
extends Actor
|
||||
with FactionAffinityBehavior.Check
|
||||
with MountableBehavior
|
||||
with DamageableWeaponTurret
|
||||
with RepairableWeaponTurret
|
||||
with JammableMountedWeapons { /* note: jammable status is reported as vehicle events, not local events */
|
||||
def TurretObject: PlanetSideServerObject with WeaponTurret with Mountable
|
||||
|
||||
override def postStop(): Unit = {
|
||||
super.postStop()
|
||||
damageableWeaponTurretPostStop()
|
||||
}
|
||||
|
||||
/** commonBehavior does not implement mountingBehavior; please do so when implementing */
|
||||
def commonBehavior: Receive =
|
||||
checkBehavior
|
||||
.orElse(jammableBehavior)
|
||||
.orElse(dismountBehavior)
|
||||
.orElse(takesDamage)
|
||||
.orElse(canBeRepairedByNanoDispenser)
|
||||
|
||||
override protected def mountTest(
|
||||
obj: PlanetSideServerObject with Mountable,
|
||||
seatNumber: Int,
|
||||
player: Player): Boolean = {
|
||||
(!TurretObject.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed
|
||||
}
|
||||
|
||||
/**
|
||||
* An override for `Restoration`, best for facility turrets.
|
||||
* @param obj the entity being restored
|
||||
*/
|
||||
override def Restoration(obj: Damageable.Target): Unit = {
|
||||
super.Restoration(obj)
|
||||
val zone = TurretObject.Zone
|
||||
val zoneId = zone.id
|
||||
val events = zone.AvatarEvents
|
||||
val tguid = TurretObject.GUID
|
||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 0))
|
||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* An override for `DamageAwareness`, best for facility turrets.
|
||||
* @param target the entity being destroyed
|
||||
* @param cause historical information about the damage
|
||||
*/
|
||||
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
|
||||
super.DestructionAwareness(target, cause)
|
||||
val zone = target.Zone
|
||||
val zoneId = zone.id
|
||||
val events = zone.AvatarEvents
|
||||
val tguid = target.GUID
|
||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 1))
|
||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 1))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,72 @@
|
|||
// 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(
|
||||
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,
|
||||
/** 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(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 `MannedTurret`.
|
||||
* The definition for any `WeaponTurret`.
|
||||
*/
|
||||
trait TurretDefinition
|
||||
extends MountableWeaponsDefinition
|
||||
|
|
@ -25,11 +82,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 +103,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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.serverobject.turret
|
||||
|
||||
import akka.actor.Cancellable
|
||||
import net.psforever.objects.serverobject.ServerObjectControl
|
||||
import net.psforever.objects.{Default, Player, Tool}
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* A control agency that handles messages being dispatched to a specific `FacilityTurret`.
|
||||
* These turrets are installed tangential to cavern facilities but are independent of the facility.
|
||||
* @param turret the `FacilityTurret` object being governed
|
||||
*/
|
||||
class VanuSentryControl(turret: FacilityTurret)
|
||||
extends ServerObjectControl
|
||||
with MountableTurretControl {
|
||||
def TurretObject: FacilityTurret = turret
|
||||
def FactionObject: FacilityTurret = turret
|
||||
def MountableObject: FacilityTurret = turret
|
||||
def JammableObject: FacilityTurret = turret
|
||||
def DamageableObject: FacilityTurret = turret
|
||||
def RepairableObject: FacilityTurret = turret
|
||||
|
||||
// Used for timing ammo recharge for vanu turrets in caves
|
||||
private var weaponAmmoRechargeTimer: Cancellable = Default.Cancellable
|
||||
|
||||
private val weaponAmmoRecharge: Receive = {
|
||||
case VanuSentry.ChangeFireStart =>
|
||||
weaponAmmoRechargeTimer.cancel()
|
||||
weaponAmmoRechargeTimer = Default.Cancellable
|
||||
|
||||
case VanuSentry.ChangeFireStop =>
|
||||
weaponAmmoRechargeTimer.cancel()
|
||||
weaponAmmoRechargeTimer = context.system.scheduler.scheduleWithFixedDelay(
|
||||
3 seconds,
|
||||
200 milliseconds,
|
||||
self,
|
||||
VanuSentry.RechargeAmmo
|
||||
)
|
||||
|
||||
case VanuSentry.RechargeAmmo =>
|
||||
TurretObject.ControlledWeapon(wepNumber = 1).collect {
|
||||
case weapon: Tool =>
|
||||
// recharge when last shot fired 3s delay, +1, 200ms interval
|
||||
if (weapon.Magazine < weapon.MaxMagazine && System.currentTimeMillis() - weapon.LastDischarge > 3000L) {
|
||||
weapon.Magazine += 1
|
||||
val seat = TurretObject.Seat(0).get
|
||||
seat.occupant.collect {
|
||||
case player: Player =>
|
||||
TurretObject.Zone.LocalEvents ! LocalServiceMessage(
|
||||
TurretObject.Zone.id,
|
||||
LocalAction.RechargeVehicleWeapon(player.GUID, TurretObject.GUID, weapon.GUID)
|
||||
)
|
||||
}
|
||||
}
|
||||
else if (weapon.Magazine == weapon.MaxMagazine && weaponAmmoRechargeTimer != Default.Cancellable) {
|
||||
weaponAmmoRechargeTimer.cancel()
|
||||
weaponAmmoRechargeTimer = Default.Cancellable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def postStop(): Unit = {
|
||||
super.postStop()
|
||||
weaponAmmoRechargeTimer.cancel()
|
||||
}
|
||||
|
||||
def receive: Receive =
|
||||
commonBehavior
|
||||
.orElse(mountBehavior)
|
||||
.orElse(weaponAmmoRecharge)
|
||||
.orElse {
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
override def parseAttribute(attribute: Int, value: Long, other: Option[Any]): Unit = { /*intentionally blank*/ }
|
||||
}
|
||||
|
||||
object VanuSentry {
|
||||
final case object RechargeAmmo
|
||||
final case object ChangeFireStart
|
||||
final case object ChangeFireStop
|
||||
|
||||
import akka.actor.ActorContext
|
||||
def Constructor(pos: Vector3, tdef: FacilityTurretDefinition)(id: Int, context: ActorContext): FacilityTurret = {
|
||||
import akka.actor.Props
|
||||
val obj = FacilityTurret(tdef)
|
||||
obj.Position = pos
|
||||
obj.Actor = context.actorOf(Props(classOf[VanuSentryControl], obj), s"${tdef.Name}_$id")
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import net.psforever.objects.definition.{AmmoBoxDefinition, ToolDefinition}
|
|||
import net.psforever.objects.equipment.EquipmentSlot
|
||||
import net.psforever.objects.inventory.{Container, GridInventory}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.mount.{SeatDefinition, Seat => Chair}
|
||||
import net.psforever.objects.serverobject.mount.{Seat => Chair}
|
||||
import net.psforever.objects.vehicles.MountableWeapons
|
||||
|
||||
trait WeaponTurret
|
||||
|
|
@ -14,10 +14,6 @@ trait WeaponTurret
|
|||
with MountableWeapons
|
||||
with Container {
|
||||
_: PlanetSideGameObject =>
|
||||
|
||||
/** manned turrets have just one mount; this is just standard interface */
|
||||
seats = Map(0 -> new Chair(new SeatDefinition()))
|
||||
|
||||
/** may or may not have inaccessible inventory space
|
||||
* see `ReserveAmmunition` in the definition
|
||||
*/
|
||||
|
|
@ -84,25 +80,27 @@ trait WeaponTurret
|
|||
}
|
||||
|
||||
object WeaponTurret {
|
||||
|
||||
/**
|
||||
* Use the `*Definition` that was provided to this object to initialize its fields and settings.
|
||||
* @see `{object}.LoadDefinition`
|
||||
* @param turret the `MannedTurret` being initialized
|
||||
* Use the definition that was provided to this object to initialize its fields and settings.
|
||||
* @see `WeaponTurret.LoadDefinition(WeaponTurret, TurretDefinition)`
|
||||
* @param turret turret being initialized
|
||||
*/
|
||||
def LoadDefinition(turret: WeaponTurret): WeaponTurret = {
|
||||
LoadDefinition(turret, turret.Definition)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the `*Definition` that was provided to this object to initialize its fields and settings.
|
||||
* A default definition is provided to be used.
|
||||
* @see `{object}.LoadDefinition`
|
||||
* @param turret the `MannedTurret` being initialized
|
||||
* @param tdef the object definition
|
||||
* Use the definition that was provided to this object to initialize its fields and settings.
|
||||
* @see `WeaponTurret.LoadDefinition(WeaponTurret)`
|
||||
* @param turret turret being initialized
|
||||
* @param tdef object's specific definition
|
||||
*/
|
||||
def LoadDefinition(turret: WeaponTurret, tdef: TurretDefinition): WeaponTurret = {
|
||||
import net.psforever.objects.equipment.EquipmentSize.BaseTurretWeapon
|
||||
//create seats, if any
|
||||
turret.seats = tdef.Seats.map {
|
||||
case (num, definition) => num -> new Chair(definition)
|
||||
}.toMap
|
||||
//create weapons; note the class
|
||||
turret.weapons = tdef.WeaponPaths
|
||||
.map({
|
||||
|
|
@ -160,17 +158,18 @@ class TurretWeapon(
|
|||
Upgrade
|
||||
}
|
||||
|
||||
override def Definition = udefs(Upgrade)
|
||||
override def Definition: ToolDefinition = udefs(Upgrade)
|
||||
}
|
||||
|
||||
/**
|
||||
* A special type of ammunition box contained within a `MannedTurret` for the purposes of infinite reloads.
|
||||
* A special type of ammunition box contained for the purposes of infinite reloads.
|
||||
* The original quantity of ammunition does not change.
|
||||
* @param adef ammunition definition
|
||||
*/
|
||||
class TurretAmmoBox(private val adef: AmmoBoxDefinition) extends AmmoBox(adef, Some(65535)) {
|
||||
class TurretAmmoBox(private val adef: AmmoBoxDefinition)
|
||||
extends AmmoBox(adef, Some(65535)) {
|
||||
import net.psforever.objects.inventory.InventoryTile
|
||||
override def Tile = InventoryTile.Tile11
|
||||
override def Tile: InventoryTile = InventoryTile.Tile11
|
||||
|
||||
override def Capacity_=(toCapacity: Int) = Capacity
|
||||
override def Capacity_=(toCapacity: Int): Int = Capacity
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.objects.serverobject.turret.auto
|
||||
|
||||
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 AiDamage(turret) =>
|
||||
performAutomatedDamage(turret)
|
||||
}
|
||||
|
||||
protected def performAutomatedDamage(turret: AutomatedTurret): Unit = {
|
||||
val target = AffectedObject
|
||||
if (!(target.Destroyed || target.isMoving(test = 1f))) {
|
||||
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 ! SelfReportedConfirmShot(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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
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
|
||||
|
||||
trait AutomatedTurret
|
||||
extends PlanetSideServerObject
|
||||
with WeaponTurret {
|
||||
import AutomatedTurret.Target
|
||||
private var currentTarget: Option[Target] = None
|
||||
|
||||
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
|
||||
|
||||
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 = SourceUniqueness(target)
|
||||
targets.find(SourceUniqueness(_) == unique)
|
||||
}
|
||||
|
||||
def Detected(target: SourceUniqueness): Option[Target] = {
|
||||
targets.find(SourceUniqueness(_) == target)
|
||||
}
|
||||
|
||||
def AddTarget(target: Target): Unit = {
|
||||
targets = targets :+ target
|
||||
}
|
||||
|
||||
def RemoveTarget(target: Target): Unit = {
|
||||
val unique = SourceUniqueness(target)
|
||||
targets = targets.filterNot(SourceUniqueness(_) == unique)
|
||||
}
|
||||
|
||||
def Clear(): List[Target] = {
|
||||
val oldTargets = targets
|
||||
targets = Nil
|
||||
oldTargets
|
||||
}
|
||||
|
||||
def Definition: ObjectDefinition with TurretDefinition
|
||||
}
|
||||
|
||||
object AutomatedTurret {
|
||||
type Target = PlanetSideServerObject with Vitality
|
||||
}
|
||||
|
|
@ -0,0 +1,998 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.objects.serverobject.turret.auto
|
||||
|
||||
import akka.actor.{Actor, Cancellable}
|
||||
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.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}
|
||||
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.types.{PlanetSideGUID, Vector3}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
|
||||
trait AutomatedTurretBehavior {
|
||||
_: Actor with DamageableEntity =>
|
||||
import AutomatedTurret.Target
|
||||
/** 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 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
|
||||
/** 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
|
||||
/** targets that have been the subject of test shots just recently;
|
||||
* emptied when switching from the test shot cycle to actually selecting a target */
|
||||
private var ongoingTestedTargets: Seq[Target] = Seq[Target]()
|
||||
|
||||
/** timer managing the trailing target qualifications self test
|
||||
* where the source will shoot directly at some target
|
||||
* expecting a response in return */
|
||||
private var selfReportedRefire: Cancellable = Default.Cancellable
|
||||
/** 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;
|
||||
* this may duplicate information processed during some other database update call */
|
||||
private var targetsDestroyed: Int = 0
|
||||
|
||||
def AutomatedTurretObject: AutomatedTurret
|
||||
|
||||
val automatedTurretBehavior: Actor.Receive = if (autoStats.isDefined) {
|
||||
case AutomatedTurretBehavior.Alert(target) =>
|
||||
bringAttentionToTarget(target)
|
||||
|
||||
case AutomatedTurretBehavior.ConfirmShot(target, _) =>
|
||||
normalConfirmShot(target)
|
||||
|
||||
case SelfReportedConfirmShot(target) =>
|
||||
movementCancelSelfReportingFireConfirmShot(target)
|
||||
|
||||
case AutomatedTurretBehavior.Unalert(target) =>
|
||||
disregardTarget(target)
|
||||
|
||||
case AutomatedTurretBehavior.Reset =>
|
||||
resetAlerts()
|
||||
|
||||
case AutomatedTurretBehavior.PeriodicCheck =>
|
||||
performPeriodicTargetValidation()
|
||||
} else {
|
||||
Actor.emptyBehavior
|
||||
}
|
||||
|
||||
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
|
||||
val newState = state && AutomaticOperationFunctionalityChecks
|
||||
automaticOperation = newState
|
||||
if (!previousState && newState) {
|
||||
trySelectNewTarget()
|
||||
} else if (previousState && !newState) {
|
||||
ongoingTestedTargets = Seq()
|
||||
cancelSelfReportedAutoFire()
|
||||
AutomatedTurretObject.Target.foreach(noLongerEngageDetectedTarget)
|
||||
}
|
||||
newState
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = { autoStats.isDefined }
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
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
|
||||
AutomatedTurretObject.Detected(target)
|
||||
.orElse {
|
||||
AutomatedTurretObject.AddTarget(target)
|
||||
retimePeriodicTargetChecks(size)
|
||||
Some(target)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
AutomatedTurretObject.Detected(target)
|
||||
.collect { out =>
|
||||
AutomatedTurretObject.RemoveTarget(target)
|
||||
testTargetQualificationsForOngoingChecks(size)
|
||||
out
|
||||
}
|
||||
.flatMap {
|
||||
noLongerDetectTargetIfCurrent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
currentTargetToken = None
|
||||
currentTargetLocation = None
|
||||
ongoingTestedTargets = Seq()
|
||||
}
|
||||
|
||||
/* 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
|
||||
* @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): Boolean = {
|
||||
val now = System.currentTimeMillis()
|
||||
if (
|
||||
currentTargetToken.isEmpty &&
|
||||
target.Faction != AutomatedTurretObject.Faction
|
||||
) {
|
||||
currentTargetLastShotTime = now
|
||||
currentTargetLocation = Some(target.Position)
|
||||
ongoingTestedTargets = Seq()
|
||||
cancelSelfReportedAutoFire()
|
||||
engageNewDetectedTarget(target)
|
||||
true
|
||||
} else if (
|
||||
currentTargetToken.contains(SourceUniqueness(target)) &&
|
||||
now - currentTargetLastShotTime < autoStats.map(_.cooldowns.missedShot).getOrElse(0L)) {
|
||||
currentTargetLastShotTime = now
|
||||
currentTargetLocation = Some(target.Position)
|
||||
cancelSelfReportedAutoFire()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private def engageNewDetectedTarget(target: Target): Unit = {
|
||||
val zone = target.Zone
|
||||
val zoneid = zone.id
|
||||
currentTargetToken = Some(SourceUniqueness(target))
|
||||
currentTargetLocation = Some(target.Position)
|
||||
currentTargetSwitchTime = System.currentTimeMillis()
|
||||
AutomatedTurretObject.Target = target
|
||||
engageNewDetectedTarget(
|
||||
target,
|
||||
zoneid,
|
||||
AutomatedTurretObject.GUID,
|
||||
AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.<br>
|
||||
* For implementing behavior.
|
||||
* Must be implemented.
|
||||
* @param target something the turret can potentially shoot at
|
||||
* @param channel scope of the message
|
||||
* @param turretGuid turret
|
||||
* @param weaponGuid turret's weapon
|
||||
*/
|
||||
protected def engageNewDetectedTarget(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit
|
||||
|
||||
/**
|
||||
* 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(SourceUniqueness(target))) {
|
||||
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
|
||||
*/
|
||||
private def noLongerEngageDetectedTarget(target: Target): Option[Target] = {
|
||||
AutomatedTurretObject.Target = None
|
||||
currentTargetToken = None
|
||||
currentTargetLocation = None
|
||||
noLongerEngageTarget(
|
||||
target,
|
||||
target.Zone.id,
|
||||
AutomatedTurretObject.GUID,
|
||||
AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID
|
||||
)
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop pointing the business end of the turret's weapon at a provided target.
|
||||
* Stop shooting at the target.<br>
|
||||
* For implementing behavior.
|
||||
* Must be implemented.
|
||||
* @param target something the turret can potentially shoot at
|
||||
* @param channel scope of the message
|
||||
* @param turretGuid turret
|
||||
* @param weaponGuid turret's weapon
|
||||
* @return something the turret was potentially shooting at
|
||||
*/
|
||||
protected def noLongerEngageTarget(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Option[Target]
|
||||
|
||||
/**
|
||||
* 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;
|
||||
* it doesn't really matter which something is returned but, rather, if anything is returned
|
||||
*/
|
||||
protected def trySelectNewTarget(): Option[Target] = {
|
||||
AutomatedTurretObject.Target.orElse {
|
||||
val turretPosition = AutomatedTurretObject.Position
|
||||
val turretGuid = AutomatedTurretObject.GUID
|
||||
val weaponGuid = AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID
|
||||
val radius = autoStats.get.ranges.trigger
|
||||
val validation = autoStats.get.checks.validation
|
||||
val disqualifiers = autoStats.get.checks.blanking
|
||||
val faction = AutomatedTurretObject.Faction
|
||||
//current targets
|
||||
val selectedTargets = AutomatedTurretObject
|
||||
.Targets
|
||||
.collect { case target
|
||||
if !target.Destroyed &&
|
||||
target.Faction != faction &&
|
||||
AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, target.Position, turretPosition, radius, result = -1) &&
|
||||
validation.exists(func => func(target)) &&
|
||||
disqualifiers.takeWhile(func => func(target)).isEmpty =>
|
||||
target
|
||||
}
|
||||
//sort targets into categories
|
||||
val (previousTargets, newTargets, staleTargets) = {
|
||||
val previouslyTestedTokens = ongoingTestedTargets.map(target => SourceUniqueness(target))
|
||||
val (previous_targets, new_targets) = selectedTargets.partition(target => previouslyTestedTokens.contains(SourceUniqueness(target)))
|
||||
val previousTargetTokens = previous_targets.map(target => (SourceUniqueness(target), target))
|
||||
val stale_targets = {
|
||||
for {
|
||||
(token, target) <- previousTargetTokens
|
||||
if !previouslyTestedTokens.contains(token)
|
||||
} yield target
|
||||
}
|
||||
(previous_targets, new_targets, stale_targets)
|
||||
}
|
||||
//associate with proper functionality and perform callbacks
|
||||
val newTargetsFunc: Iterable[(Target, (Target, String, PlanetSideGUID, PlanetSideGUID) => Unit)] =
|
||||
newTargets.map(target => (target, testNewDetected))
|
||||
val previousTargetsFunc: Iterable[(Target, (Target, String, PlanetSideGUID, PlanetSideGUID) => Unit)] =
|
||||
previousTargets.map(target => (target, testKnownDetected))
|
||||
ongoingTestedTargets = (newTargetsFunc ++ previousTargetsFunc)
|
||||
.toSeq
|
||||
.sortBy { case (target, _) => Vector3.DistanceSquared(target.Position, turretPosition) }
|
||||
.flatMap { case (target, func) => processForTestingTarget(target, turretGuid, weaponGuid, func) }
|
||||
.map { case (target, _) => target }
|
||||
staleTargets.foreach(target => processForTestingTarget(target, turretGuid, weaponGuid, suspendTargetTesting))
|
||||
selectedTargets.headOption
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param processFunc na
|
||||
* @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 processForTestingTarget(
|
||||
target: Target,
|
||||
turretGuid: PlanetSideGUID,
|
||||
weaponGuid: PlanetSideGUID,
|
||||
processFunc: (Target, String, PlanetSideGUID, PlanetSideGUID)=>Unit
|
||||
): Option[(Target, Target)] = {
|
||||
target match {
|
||||
case target: Player =>
|
||||
processFunc(target, target.Name, turretGuid, weaponGuid)
|
||||
Some((target, target))
|
||||
case target: Mountable =>
|
||||
target.Seats.values
|
||||
.flatMap(_.occupants)
|
||||
.collectFirst { passenger =>
|
||||
processFunc(target, passenger.Name, turretGuid, weaponGuid)
|
||||
(target, passenger)
|
||||
}
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.<br>
|
||||
* For implementing behavior.
|
||||
* Must be implemented.
|
||||
* @param target something the turret can potentially shoot at
|
||||
* @param channel scope of the message
|
||||
* @param turretGuid turret
|
||||
* @param weaponGuid turret's weapon
|
||||
*/
|
||||
protected def testNewDetected(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit
|
||||
|
||||
/**
|
||||
* 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.<br>
|
||||
* For implementing behavior.
|
||||
* Must be implemented.
|
||||
* @param target something the turret can potentially shoot at
|
||||
* @param channel scope of the message
|
||||
* @param turretGuid not used
|
||||
* @param weaponGuid turret's weapon
|
||||
*/
|
||||
protected def testKnownDetected(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit
|
||||
|
||||
/**
|
||||
* na<br>
|
||||
* For overriding behavior.
|
||||
* @param target something the turret can potentially shoot at
|
||||
* @param channel scope of the message
|
||||
* @param turretGuid not used
|
||||
* @param weaponGuid turret's weapon
|
||||
*/
|
||||
protected def suspendTargetTesting(
|
||||
target: Target,
|
||||
channel: String,
|
||||
turretGuid: PlanetSideGUID,
|
||||
weaponGuid: PlanetSideGUID
|
||||
): Unit = { /*do nothing*/ }
|
||||
|
||||
/**
|
||||
* 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()
|
||||
performCurrentTargetDecayCheck()
|
||||
testTargetQualificationsForOngoingChecks(size)
|
||||
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
|
||||
val range = autoStats.map(_.ranges.detection).getOrElse(0f)
|
||||
val removedTargets = AutomatedTurretObject.Targets
|
||||
.collect {
|
||||
case t: InteractsWithZone
|
||||
if t.Destroyed || AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, t.Position, pos, range) =>
|
||||
AutomatedTurretObject.RemoveTarget(t)
|
||||
t
|
||||
}
|
||||
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()
|
||||
AutomatedTurretObject.Target
|
||||
.collect { target =>
|
||||
//test target
|
||||
generalDecayCheck(
|
||||
target,
|
||||
now,
|
||||
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 {
|
||||
//no target; unless we are deactivated or have any unfinished delays, search for new target
|
||||
//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,
|
||||
* 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 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
|
||||
*/
|
||||
private def generalDecayCheck(
|
||||
target: Target,
|
||||
now: Long,
|
||||
escapeRange: Float,
|
||||
selectDelay: Long,
|
||||
cooldownDelay: Long,
|
||||
eliminationDelay: Long
|
||||
): Option[Target] = {
|
||||
if (target.Destroyed) {
|
||||
//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()
|
||||
noLongerEngageDetectedTarget(target)
|
||||
currentTargetLastShotTime = now + cooldownDelay
|
||||
None
|
||||
}
|
||||
else if ({
|
||||
target match {
|
||||
case mount: Mountable => !mount.Seats.values.exists(_.isOccupied)
|
||||
case _ => false
|
||||
}
|
||||
}) {
|
||||
//certain targets can go "unresponsive" even though they should still be reachable, otherwise the target is mia
|
||||
trySelfReportedAutofireIfStationary()
|
||||
noLongerEngageDetectedTarget(target)
|
||||
currentTargetLastShotTime = now + selectDelay
|
||||
None
|
||||
} else if (now - currentTargetLastShotTime >= cooldownDelay) {
|
||||
//if the target goes mia through lack of response
|
||||
noLongerEngageDetectedTarget(target)
|
||||
currentTargetLastShotTime = now + selectDelay
|
||||
None
|
||||
} else {
|
||||
//continue shooting
|
||||
Some(target)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are no available targets,
|
||||
* and no current target,
|
||||
* stop the evaluation of available targets.
|
||||
* @param beforeListSize 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 testTargetQualificationsForOngoingChecks(beforeListSize: Int): Boolean = {
|
||||
beforeListSize > 0 &&
|
||||
AutomatedTurretObject.Targets.isEmpty &&
|
||||
AutomatedTurretObject.Target.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(_.detectionSweepTime).getOrElse(1.seconds)
|
||||
retimePeriodicTargetChecks(repeated)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
0.seconds,
|
||||
repeated,
|
||||
self,
|
||||
AutomatedTurretBehavior.PeriodicCheck
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
ongoingTestedTargets = Seq()
|
||||
periodicValidationTest.cancel()
|
||||
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`
|
||||
*/
|
||||
protected def automaticTurretPostStop(): Unit = {
|
||||
resetAlerts()
|
||||
AutomatedTurretObject.Targets.foreach { AutomatedTurretObject.RemoveTarget }
|
||||
selfReportingCleanUp()
|
||||
}
|
||||
|
||||
/* Retaliation behavior */
|
||||
|
||||
/**
|
||||
* 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] = {
|
||||
val unique = SourceUniqueness(target)
|
||||
if (
|
||||
automaticOperation &&
|
||||
!currentTargetToken.contains(unique) &&
|
||||
autoStats.exists(_.retaliatoryDelay > 0)
|
||||
) {
|
||||
AutomatedTurretBehavior.getAttackVectorFromCause(target.Zone, cause).collect {
|
||||
case attacker
|
||||
if attacker.Faction != target.Faction &&
|
||||
performRetaliation(attacker).nonEmpty &&
|
||||
currentTargetToken.contains(unique) =>
|
||||
if (periodicValidationTest.isCancelled) {
|
||||
//timer may need to be started, for example if damaged by things outside of detection perimeter
|
||||
retimePeriodicTargetChecks(autoStats.map(_.detectionSweepTime).getOrElse(1.seconds))
|
||||
}
|
||||
attacker
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 existingTarget
|
||||
if autoStats.exists { auto =>
|
||||
auto.retaliationOverridesTarget &&
|
||||
currentTargetSwitchTime + auto.retaliatoryDelay > System.currentTimeMillis() &&
|
||||
auto.checks.blanking.takeWhile(func => func(target)).isEmpty
|
||||
} =>
|
||||
//conditions necessary for overriding the current target
|
||||
cancelSelfReportedAutoFire()
|
||||
noLongerEngageDetectedTarget(existingTarget)
|
||||
engageNewDetectedTarget(target)
|
||||
target
|
||||
|
||||
case existingTarget =>
|
||||
//stay with the current target
|
||||
existingTarget
|
||||
}
|
||||
.orElse {
|
||||
//no current target
|
||||
if (autoStats.exists(_.checks.blanking.takeWhile(func => func(target)).isEmpty)) {
|
||||
engageNewDetectedTarget(target)
|
||||
Some(target)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 = {
|
||||
currentTargetLastShotTime = System.currentTimeMillis()
|
||||
shotsFired += 1
|
||||
target match {
|
||||
case v: Mountable
|
||||
if v.Destroyed && !v.Seats.values.exists(_.isOccupied) =>
|
||||
targetsDestroyed += 1
|
||||
case _ => ()
|
||||
}
|
||||
AutomatedTurretObject.Target
|
||||
.collect { oldTarget =>
|
||||
if (currentTargetToken.contains(SourceUniqueness(oldTarget))) {
|
||||
//target already being handled
|
||||
if (oldTarget.Destroyed || currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, oldTarget.Position) > 1f)) {
|
||||
//stop (destroyed, or movement disqualification)
|
||||
cancelSelfReportedAutoFire()
|
||||
noLongerEngageDetectedTarget(oldTarget)
|
||||
processForTestingTarget(
|
||||
oldTarget,
|
||||
AutomatedTurretObject.GUID,
|
||||
AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID,
|
||||
testNewDetected
|
||||
)
|
||||
}
|
||||
} else {
|
||||
//stop (wrong target)
|
||||
cancelSelfReportedAutoFire()
|
||||
}
|
||||
}
|
||||
.orElse {
|
||||
//start new target
|
||||
engageNewDetectedTarget(target)
|
||||
tryPerformSelfReportedAutofire(target)
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) &&
|
||||
autoStats.exists(_.refireTime > 0.seconds) =>
|
||||
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) {
|
||||
target.Actor ! 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 tryPerformSelfReportedAutofire(target: Target): Boolean = {
|
||||
if (selfReportedRefire.isCancelled) {
|
||||
selfReportedRefire = context.system.scheduler.scheduleWithFixedDelay(
|
||||
0.seconds,
|
||||
autoStats.map(_.refireTime).getOrElse(1.seconds),
|
||||
target.Actor,
|
||||
AiDamage(AutomatedTurretObject)
|
||||
)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
* 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))
|
||||
selfReportingCleanUp()
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object AutomatedTurretBehavior {
|
||||
import AutomatedTurret.Target
|
||||
final case class Alert(target: Target)
|
||||
|
||||
final case class Unalert(target: Target)
|
||||
|
||||
final case class ConfirmShot(target: Target, reporter: Option[SourceEntry] = None)
|
||||
|
||||
final case object Reset
|
||||
|
||||
private case object PeriodicCheck
|
||||
|
||||
private val commonBlanking: List[PlanetSideGameObject => Boolean] = List(
|
||||
EffectTarget.Validation.AutoTurretBlankPlayerTarget,
|
||||
EffectTarget.Validation.AutoTurretBlankVehicleTarget
|
||||
)
|
||||
|
||||
private val noTargets: List[PlanetSideGUID] = List(Service.defaultPlayerGUID)
|
||||
|
||||
/**
|
||||
* Are we tracking a target entity?
|
||||
* @param zone the region in which the messages will be dispatched
|
||||
* @param channel scope of the message
|
||||
* @param turretGuid turret
|
||||
* @param list target's globally unique identifier, in list form
|
||||
*/
|
||||
def startTracking(zone: Zone, channel: String, turretGuid: PlanetSideGUID, list: List[PlanetSideGUID]): Unit = {
|
||||
zone.LocalEvents ! LocalServiceMessage(
|
||||
channel,
|
||||
LocalAction.SendResponse(ObjectDetectedMessage(turretGuid, turretGuid, 0, list))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we no longer tracking a target entity?
|
||||
* @param zone the region in which the messages will be dispatched
|
||||
* @param channel scope of the message
|
||||
* @param turretGuid turret
|
||||
*/
|
||||
def stopTracking(zone: Zone, channel: String, turretGuid: PlanetSideGUID): Unit = {
|
||||
zone.LocalEvents ! LocalServiceMessage(
|
||||
channel,
|
||||
LocalAction.SendResponse(ObjectDetectedMessage(turretGuid, turretGuid, 0, noTargets))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we shooting a weapon?
|
||||
* @param zone the region in which the messages will be dispatched
|
||||
* @param channel scope of the message
|
||||
* @param weaponGuid turret's weapon
|
||||
*/
|
||||
def startShooting(zone: Zone, channel: String, weaponGuid: PlanetSideGUID): Unit = {
|
||||
zone.LocalEvents ! LocalServiceMessage(
|
||||
channel,
|
||||
LocalAction.SendResponse(ChangeFireStateMessage_Start(weaponGuid))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we no longer shooting a weapon?
|
||||
* @param zone the region in which the messages will be dispatched
|
||||
* @param channel scope of the message
|
||||
* @param weaponGuid turret's weapon
|
||||
*/
|
||||
def stopShooting(zone: Zone, channel: String, weaponGuid: PlanetSideGUID): Unit = {
|
||||
zone.LocalEvents ! LocalServiceMessage(
|
||||
channel,
|
||||
LocalAction.SendResponse(ChangeFireStateMessage_Stop(weaponGuid))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
.adversarial
|
||||
.collect { adversarial =>
|
||||
adversarial.attacker match {
|
||||
case p: PlayerSource =>
|
||||
p.seatedIn
|
||||
.map { _._1.unique }
|
||||
.collect {
|
||||
case v: UniqueVehicle => zone.Vehicles.find(SourceUniqueness(_) == v)
|
||||
case a: UniqueAmenity => zone.GUID(a.guid)
|
||||
case d: UniqueDeployable => zone.DeployableList.find(SourceUniqueness(_) == d)
|
||||
}
|
||||
.flatten
|
||||
.orElse {
|
||||
val name = p.Name
|
||||
zone.LivePlayers.find(_.Name.equals(name))
|
||||
}
|
||||
case o =>
|
||||
o.unique match {
|
||||
case v: UniqueVehicle => zone.Vehicles.find(SourceUniqueness(_) == v)
|
||||
case a: UniqueAmenity => zone.GUID(a.guid)
|
||||
case d: UniqueDeployable => zone.DeployableList.find(SourceUniqueness(_) == d)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
.flatten
|
||||
.collect {
|
||||
case out: PlanetSideServerObject with Vitality => out
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 if the actual result of the comparison matches its anticipation `result`
|
||||
*/
|
||||
def shapedDistanceCheckAgainstValue(
|
||||
stats: Option[Automation],
|
||||
positionA: Vector3,
|
||||
positionB: Vector3,
|
||||
range: Float,
|
||||
result: Int = 1 //by default, calculation > input
|
||||
): Boolean = {
|
||||
val testRangeSq = range * range
|
||||
if (stats.exists(_.cylindrical)) {
|
||||
val height = range + stats.map(_.cylindricalExtraHeight).getOrElse(0f)
|
||||
(if (positionA.z > positionB.z) positionA.z - positionB.z else positionB.z - positionA.z).compareTo(height) == result &&
|
||||
Vector3.DistanceSquared(positionA.xy, positionB.xy).compareTo(testRangeSq) == result
|
||||
} else {
|
||||
Vector3.DistanceSquared(positionA, positionB).compareTo(testRangeSq) == result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -6,7 +6,6 @@ import net.psforever.objects.serverobject.hackable.Hackable
|
|||
import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.structures.Amenity
|
||||
import net.psforever.objects.sourcing
|
||||
import net.psforever.objects.vital.resistance.ResistanceProfile
|
||||
import net.psforever.objects.vital.{Vitality, VitalityDefinition}
|
||||
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
||||
|
|
@ -57,7 +56,7 @@ object AmenitySource {
|
|||
Nil,
|
||||
SourceEntry(obj.Owner),
|
||||
hackData,
|
||||
sourcing.UniqueAmenity(obj.Zone.Number, obj.GUID, obj.Position)
|
||||
UniqueAmenity(obj)
|
||||
)
|
||||
amenity.copy(occupants = obj match {
|
||||
case o: Mountable =>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ final case class UniqueBuilding(
|
|||
building_guid: PlanetSideGUID
|
||||
) extends SourceUniqueness
|
||||
|
||||
object UniqueBuilding {
|
||||
def apply(obj: Building): UniqueBuilding = {
|
||||
UniqueBuilding(obj.Zone.Number, obj.GUID)
|
||||
}
|
||||
}
|
||||
|
||||
final case class BuildingSource(
|
||||
private val obj_def: BuildingDefinition,
|
||||
Faction: PlanetSideEmpire.Value,
|
||||
|
|
@ -35,7 +41,7 @@ object BuildingSource {
|
|||
b.Position,
|
||||
b.Orientation,
|
||||
b.latticeConnectedFacilityBenefits(),
|
||||
UniqueBuilding(b.Zone.Number, b.GUID)
|
||||
UniqueBuilding(b)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,13 +52,7 @@ object DeployableSource {
|
|||
obj.Position,
|
||||
obj.Orientation,
|
||||
occupants,
|
||||
UniqueDeployable(
|
||||
obj.History.headOption match {
|
||||
case Some(entry) => entry.time
|
||||
case None => 0L
|
||||
},
|
||||
obj.OriginalOwnerName.getOrElse("none")
|
||||
)
|
||||
UniqueDeployable(obj)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ import net.psforever.types.{PlanetSideEmpire, Vector3}
|
|||
|
||||
final case class UniqueObject(objectId: Int) extends SourceUniqueness
|
||||
|
||||
object UniqueObject {
|
||||
def apply(obj: PlanetSideGameObject): UniqueObject = {
|
||||
UniqueObject(obj.Definition.ObjectId)
|
||||
}
|
||||
}
|
||||
|
||||
final case class ObjectSource(
|
||||
private val obj_def: ObjectDefinition,
|
||||
Faction: PlanetSideEmpire.Value,
|
||||
|
|
@ -44,7 +50,7 @@ object ObjectSource {
|
|||
obj.Position,
|
||||
obj.Orientation,
|
||||
obj.Velocity,
|
||||
UniqueObject(obj.Definition.ObjectId)
|
||||
UniqueObject(obj)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ final case class UniquePlayer(
|
|||
faction: PlanetSideEmpire.Value
|
||||
) extends SourceUniqueness
|
||||
|
||||
object UniquePlayer {
|
||||
def apply(obj: Player): UniquePlayer = {
|
||||
UniquePlayer(obj.CharId, obj.Name, obj.Sex, obj.Faction)
|
||||
}
|
||||
}
|
||||
|
||||
final case class PlayerSource(
|
||||
Definition: AvatarDefinition,
|
||||
ExoSuit: ExoSuitType.Value,
|
||||
|
|
@ -121,7 +127,6 @@ object PlayerSource {
|
|||
*/
|
||||
def inSeat(player: Player, source: SourceEntry, seatNumber: Int): PlayerSource = {
|
||||
val exosuit = player.ExoSuit
|
||||
val faction = player.Faction
|
||||
val avatar = player.avatar
|
||||
PlayerSource(
|
||||
player.Definition,
|
||||
|
|
@ -134,10 +139,10 @@ object PlayerSource {
|
|||
player.Velocity,
|
||||
player.Crouching,
|
||||
player.Jumping,
|
||||
ExoSuitDefinition.Select(exosuit, faction),
|
||||
ExoSuitDefinition.Select(exosuit, player.Faction),
|
||||
avatar.bep,
|
||||
progress = tokenLife,
|
||||
UniquePlayer(player.CharId, player.Name, player.Sex, faction)
|
||||
UniquePlayer(player)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ import net.psforever.objects.vital.resistance.ResistanceProfile
|
|||
import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle}
|
||||
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
||||
|
||||
trait SourceUniqueness
|
||||
|
||||
trait SourceEntry {
|
||||
def Name: String
|
||||
def Definition: ObjectDefinition with VitalityDefinition
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.objects.sourcing
|
||||
|
||||
import net.psforever.objects.ce.Deployable
|
||||
import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.structures.{Amenity, Building}
|
||||
import net.psforever.objects.serverobject.turret.FacilityTurret
|
||||
|
||||
trait SourceUniqueness
|
||||
|
||||
object SourceUniqueness {
|
||||
def apply(target: PlanetSideGameObject with FactionAffinity): SourceUniqueness = {
|
||||
target match {
|
||||
case obj: Player => UniquePlayer(obj)
|
||||
case obj: Vehicle => UniqueVehicle(obj)
|
||||
case obj: FacilityTurret => UniqueAmenity(obj)
|
||||
case obj: Amenity => UniqueAmenity(obj)
|
||||
case obj: TurretDeployable => UniqueDeployable(obj)
|
||||
case obj: Deployable => UniqueDeployable(obj)
|
||||
case obj: Building => UniqueBuilding(obj)
|
||||
case _ => UniqueObject(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,15 +33,9 @@ object TurretSource {
|
|||
val position = obj.Position
|
||||
val identifer = obj match {
|
||||
case o: TurretDeployable =>
|
||||
UniqueDeployable(
|
||||
o.History.headOption match {
|
||||
case Some(entry) => entry.time
|
||||
case None => 0L
|
||||
},
|
||||
o.OriginalOwnerName.getOrElse("none")
|
||||
)
|
||||
UniqueDeployable(o)
|
||||
case o: FacilityTurret =>
|
||||
UniqueAmenity(o.Zone.Number, o.GUID, position)
|
||||
UniqueAmenity(o)
|
||||
case o =>
|
||||
throw new IllegalArgumentException(s"was given ${o.Actor.toString()} when only wanted to model turrets")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.sourcing
|
||||
|
||||
import net.psforever.objects.serverobject.structures.Amenity
|
||||
import net.psforever.types.{PlanetSideGUID, Vector3}
|
||||
|
||||
final case class UniqueAmenity(
|
||||
|
|
@ -8,3 +9,9 @@ final case class UniqueAmenity(
|
|||
guid: PlanetSideGUID,
|
||||
position: Vector3
|
||||
) extends SourceUniqueness
|
||||
|
||||
object UniqueAmenity {
|
||||
def apply(obj: Amenity): UniqueAmenity = {
|
||||
UniqueAmenity(obj.Zone.Number, obj.GUID, obj.Position)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,21 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.sourcing
|
||||
|
||||
import net.psforever.objects.ce.Deployable
|
||||
|
||||
final case class UniqueDeployable(
|
||||
spawnTime: Long,
|
||||
originalOwnerName: String
|
||||
) extends SourceUniqueness
|
||||
|
||||
object UniqueDeployable {
|
||||
def apply(obj: Deployable): UniqueDeployable = {
|
||||
UniqueDeployable(
|
||||
obj.History.headOption match {
|
||||
case Some(entry) => entry.time
|
||||
case None => 0L
|
||||
},
|
||||
obj.OriginalOwnerName.getOrElse("none")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,18 @@ import net.psforever.types.{DriveState, PlanetSideEmpire, Vector3}
|
|||
|
||||
final case class UniqueVehicle(spawnTime: Long, originalOwnerName: String) extends SourceUniqueness
|
||||
|
||||
object UniqueVehicle {
|
||||
def apply(obj: Vehicle): UniqueVehicle = {
|
||||
UniqueVehicle(
|
||||
obj.History.headOption match {
|
||||
case Some(entry) => entry.time
|
||||
case None => 0L
|
||||
},
|
||||
obj.OriginalOwnerName.getOrElse("none")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final case class VehicleSource(
|
||||
Definition: VehicleDefinition,
|
||||
Faction: PlanetSideEmpire.Value,
|
||||
|
|
@ -46,13 +58,7 @@ object VehicleSource {
|
|||
None,
|
||||
Nil,
|
||||
obj.Definition.asInstanceOf[ResistanceProfile],
|
||||
UniqueVehicle(
|
||||
obj.History.headOption match {
|
||||
case Some(entry) => entry.time
|
||||
case None => 0L
|
||||
},
|
||||
obj.OriginalOwnerName.getOrElse("none")
|
||||
)
|
||||
UniqueVehicle(obj)
|
||||
)
|
||||
//shallow information that references the existing source entry
|
||||
vehicle.copy(
|
||||
|
|
|
|||
|
|
@ -2,17 +2,9 @@
|
|||
package net.psforever.objects.vehicles
|
||||
|
||||
import net.psforever.objects.Vehicle
|
||||
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
|
||||
import net.psforever.objects.sourcing.SourceEntry
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.base.{DamageResolution, DamageType}
|
||||
import net.psforever.objects.vital.etc.RadiationReason
|
||||
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||
import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, RadiationInMountableInteraction}
|
||||
import net.psforever.objects.zones.blockmap.SectorPopulation
|
||||
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
case object RadiationInVehicleInteraction extends ZoneInteractionType
|
||||
import net.psforever.objects.zones.InteractsWithZone
|
||||
|
||||
/**
|
||||
* This game entity may infrequently test whether it may interact with radiation cloud projectiles
|
||||
|
|
@ -21,90 +13,24 @@ case object RadiationInVehicleInteraction extends ZoneInteractionType
|
|||
*/
|
||||
class InteractWithRadiationCloudsSeatedInVehicle(
|
||||
private val obj: Vehicle,
|
||||
val range: Float
|
||||
) extends ZoneInteraction {
|
||||
/**
|
||||
* radiation clouds that, though detected, are skipped from affecting the target;
|
||||
* in between interaction tests, a memory of the clouds that were tested last are retained and
|
||||
* are excluded from being tested this next time;
|
||||
* clouds that are detected a second time are cleared from the list and are available to be tested next time
|
||||
*/
|
||||
private var skipTargets: List[PlanetSideGUID] = List()
|
||||
|
||||
def Type = RadiationInVehicleInteraction
|
||||
|
||||
override val range: Float
|
||||
) extends InteractWithRadiationCloudsSeatedInEntity(obj, range) {
|
||||
/**
|
||||
* Drive into a radiation cloud and all the vehicle's occupants suffer the consequences.
|
||||
* @param sector the portion of the block map being tested
|
||||
* @param target the fixed element in this test
|
||||
*/
|
||||
override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
|
||||
val position = target.Position
|
||||
//collect all projectiles in sector/range
|
||||
val projectiles = sector
|
||||
.projectileList
|
||||
.filter { cloud =>
|
||||
val definition = cloud.Definition
|
||||
definition.radiation_cloud &&
|
||||
definition.AllDamageTypes.contains(DamageType.Radiation) &&
|
||||
{
|
||||
val radius = definition.DamageRadius
|
||||
Zone.distanceCheck(target, cloud, radius * radius)
|
||||
}
|
||||
}
|
||||
.distinct
|
||||
val notSkipped = projectiles.filterNot { t => skipTargets.contains(t.GUID) }
|
||||
skipTargets = notSkipped.map { _.GUID }
|
||||
if (notSkipped.nonEmpty) {
|
||||
(
|
||||
//isolate one of each type of projectile
|
||||
notSkipped
|
||||
.foldLeft(Nil: List[Projectile]) {
|
||||
(acc, next) => if (acc.exists { _.profile == next.profile }) acc else next :: acc
|
||||
},
|
||||
obj.Seats
|
||||
.values
|
||||
.collect { case seat => seat.occupant }
|
||||
.flatten
|
||||
) match {
|
||||
case (uniqueProjectiles, targets) if uniqueProjectiles.nonEmpty && targets.nonEmpty =>
|
||||
val shielding = obj.Definition.RadiationShielding
|
||||
targets.foreach { t =>
|
||||
uniqueProjectiles.foreach { p =>
|
||||
t.Actor ! Vitality.Damage(
|
||||
DamageInteraction(
|
||||
SourceEntry(t),
|
||||
RadiationReason(
|
||||
ProjectileQuality.modifiers(p, DamageResolution.Radiation, t, t.Position, None),
|
||||
t.DamageModel,
|
||||
shielding
|
||||
),
|
||||
position
|
||||
).calculate()
|
||||
)
|
||||
}
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
super.interaction(sector, target)
|
||||
obj.CargoHolds
|
||||
.values
|
||||
.collect {
|
||||
case hold if hold.isOccupied =>
|
||||
val target = hold.occupant.get
|
||||
target.interaction().find { _.Type == RadiationInVehicleInteraction } match {
|
||||
case Some(func) => func.interaction(sector, target)
|
||||
case _ => ;
|
||||
target
|
||||
.interaction()
|
||||
.find(_.Type == RadiationInMountableInteraction)
|
||||
.foreach(func => func.interaction(sector, target))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Any radiation clouds blocked from being tested should be cleared.
|
||||
* All that can be done is blanking our retained previous effect targets.
|
||||
* @param target the fixed element in this test
|
||||
*/
|
||||
def resetInteraction(target: InteractsWithZone): Unit = {
|
||||
skipTargets = List()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ import net.psforever.objects.serverobject.damage.Damageable.Target
|
|||
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle}
|
||||
import net.psforever.objects.serverobject.environment._
|
||||
import net.psforever.objects.serverobject.hackable.GenericHackables
|
||||
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
|
||||
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior, RadiationInMountableInteraction}
|
||||
import net.psforever.objects.serverobject.repair.RepairableVehicle
|
||||
import net.psforever.objects.serverobject.terminals.Terminal
|
||||
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}
|
||||
|
|
@ -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()
|
||||
|
|
@ -243,7 +247,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
commonEnabledBehavior
|
||||
.orElse {
|
||||
case VehicleControl.RadiationTick =>
|
||||
vehicle.interaction().find { _.Type == RadiationInVehicleInteraction } match {
|
||||
vehicle.interaction().find { _.Type == RadiationInMountableInteraction } match {
|
||||
case Some(func) => func.interaction(vehicle.getInteractionSector(), vehicle)
|
||||
case _ => ;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ trait InGameHistory {
|
|||
if (target eq this) {
|
||||
None
|
||||
} else {
|
||||
val uniqueTarget = SourceEntry(target).unique
|
||||
val uniqueTarget = SourceUniqueness(target)
|
||||
(target.GetContribution(), contributionInheritance.get(uniqueTarget)) match {
|
||||
case (Some(in), Some(curr)) =>
|
||||
val end = curr.end
|
||||
|
|
@ -395,6 +395,6 @@ object InGameHistory {
|
|||
def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Option[Contribution] = {
|
||||
target
|
||||
.GetContribution()
|
||||
.collect { case events => Contribution(SourceEntry(target).unique, events) }
|
||||
.collect { case events => Contribution(SourceUniqueness(target), events) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -411,7 +411,7 @@ object GamePacketOpcode extends Enumeration {
|
|||
case 0x59 => noDecoder(UnknownMessage89)
|
||||
case 0x5a => game.DelayedPathMountMsg.decode
|
||||
case 0x5b => game.OrbitalShuttleTimeMsg.decode
|
||||
case 0x5c => noDecoder(AIDamage)
|
||||
case 0x5c => game.AIDamage.decode
|
||||
case 0x5d => game.DeployObjectMessage.decode
|
||||
case 0x5e => game.FavoritesRequest.decode
|
||||
case 0x5f => noDecoder(FavoritesResponse)
|
||||
|
|
|
|||
32
src/main/scala/net/psforever/packet/game/AIDamage.scala
Normal file
32
src/main/scala/net/psforever/packet/game/AIDamage.scala
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
final case class AIDamage(
|
||||
target_guid: PlanetSideGUID,
|
||||
attacker_guid: PlanetSideGUID,
|
||||
projectile_type: Long,
|
||||
unk1: Long,
|
||||
unk2: Long
|
||||
) extends PlanetSideGamePacket {
|
||||
type Packet = ActionResultMessage
|
||||
def opcode = GamePacketOpcode.AIDamage
|
||||
def encode = AIDamage.encode(this)
|
||||
}
|
||||
|
||||
object AIDamage extends Marshallable[AIDamage] {
|
||||
implicit val codec: Codec[AIDamage] = (
|
||||
("target_guid" | PlanetSideGUID.codec) ::
|
||||
("attacker_guid" | PlanetSideGUID.codec) ::
|
||||
("projectile_type" | ulongL(bits = 32)) ::
|
||||
("unk1" | ulongL(bits = 32)) ::
|
||||
("unk2" | ulongL(bits = 32))
|
||||
).as[AIDamage]
|
||||
}
|
||||
|
|
@ -81,13 +81,13 @@ object CommonFieldData extends Marshallable[CommonFieldData] {
|
|||
CommonFieldData(faction, false, false, false, None, false, None, None, PlanetSideGUID(0))
|
||||
|
||||
def apply(faction: PlanetSideEmpire.Value, unk: Int): CommonFieldData =
|
||||
CommonFieldData(faction, false, false, unk > 1, None, unk % 1 == 1, None, None, PlanetSideGUID(0))
|
||||
CommonFieldData(faction, false, false, unk > 1, None, unk > 0, None, None, PlanetSideGUID(0))
|
||||
|
||||
def apply(faction: PlanetSideEmpire.Value, unk: Int, player_guid: PlanetSideGUID): CommonFieldData =
|
||||
CommonFieldData(faction, false, false, unk > 1, None, unk % 1 == 1, None, None, player_guid)
|
||||
CommonFieldData(faction, false, false, unk > 1, None, unk > 0, None, None, player_guid)
|
||||
|
||||
def apply(faction: PlanetSideEmpire.Value, destroyed: Boolean, unk: Int): CommonFieldData =
|
||||
CommonFieldData(faction, false, destroyed, unk > 1, None, unk % 1 == 1, None, None, PlanetSideGUID(0))
|
||||
CommonFieldData(faction, false, destroyed, unk > 1, None, unk > 0, None, None, PlanetSideGUID(0))
|
||||
|
||||
def apply(
|
||||
faction: PlanetSideEmpire.Value,
|
||||
|
|
|
|||
|
|
@ -15,13 +15,14 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
|||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
|
||||
var task: Cancellable = Default.Cancellable
|
||||
|
||||
var list: List[TurretUpgrader.Entry] = List()
|
||||
|
||||
val sameEntryComparator = new SimilarityComparator[TurretUpgrader.Entry]() {
|
||||
val sameEntryComparator: SimilarityComparator[TurretUpgrader.Entry] = new SimilarityComparator[TurretUpgrader.Entry]() {
|
||||
def Test(entry1: TurretUpgrader.Entry, entry2: TurretUpgrader.Entry): Boolean = {
|
||||
entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID
|
||||
}
|
||||
|
|
@ -41,7 +42,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
|
|||
list = Nil
|
||||
}
|
||||
|
||||
def CreateEntry(obj: PlanetSideGameObject, zone: Zone, upgrade: TurretUpgrade.Value, duration: Long) =
|
||||
def CreateEntry(obj: PlanetSideGameObject, zone: Zone, upgrade: TurretUpgrade.Value, duration: Long): TurretUpgrader.Entry =
|
||||
TurretUpgrader.Entry(obj, zone, upgrade, duration)
|
||||
|
||||
def InclusionTest(entry: TurretUpgrader.Entry): Boolean = entry.obj.isInstanceOf[FacilityTurret]
|
||||
|
|
@ -89,7 +90,6 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
|
|||
task.cancel()
|
||||
if (list.nonEmpty) {
|
||||
val short_timeout: FiniteDuration = math.max(1, list.head.duration - (now - list.head.time)).milliseconds
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
task = context.system.scheduler.scheduleOnce(short_timeout, self, TurretUpgrader.Downgrade())
|
||||
}
|
||||
}
|
||||
|
|
@ -150,6 +150,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
|
|||
val upgrade = entry.upgrade
|
||||
val guid = zone.GUID
|
||||
val turretGUID = target.GUID
|
||||
target.setMiddleOfUpgrade(true)
|
||||
//kick all occupying players for duration of conversion
|
||||
target.Seats.values
|
||||
.filter { _.isOccupied }
|
||||
|
|
@ -160,7 +161,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
|
|||
if (tplayer.HasGUID) {
|
||||
context.parent ! VehicleServiceMessage(
|
||||
zoneId,
|
||||
VehicleAction.KickPassenger(tplayer.GUID, 4, false, turretGUID)
|
||||
VehicleAction.KickPassenger(tplayer.GUID, 4, unk2=false, turretGUID)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
@ -174,7 +175,6 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
|
|||
.filterNot { box => newBoxes.exists(_ eq box) }
|
||||
.map(box => GUIDTask.unregisterEquipment(guid, box))
|
||||
.toList
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val newBoxesTask = TaskBundle(
|
||||
new StraightforwardTask() {
|
||||
private val localFunc: () => Unit = FinishUpgradingTurret(entry)
|
||||
|
|
@ -189,22 +189,25 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
|
|||
.map(box => GUIDTask.registerEquipment(guid, box))
|
||||
.toList
|
||||
)
|
||||
TaskWorkflow.execute(TaskBundle(
|
||||
val mainTask = TaskWorkflow.execute(TaskBundle(
|
||||
new StraightforwardTask() {
|
||||
private val tasks = oldBoxesTask
|
||||
|
||||
def action(): Future[Any] = {
|
||||
tasks.foreach { TaskWorkflow.execute }
|
||||
tasks.foreach(TaskWorkflow.execute)
|
||||
Future(this)
|
||||
}
|
||||
},
|
||||
newBoxesTask
|
||||
))
|
||||
mainTask.recoverWith {
|
||||
case _: Exception => Finalize(target, upgrade); Future(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* From an object that has mounted weapons, parse all of the internal ammunition loaded into all of the weapons.
|
||||
* @param target the object with mounted weaponry
|
||||
* @param target entity with mounted weaponry
|
||||
* @return all of the internal ammunition objects
|
||||
*/
|
||||
def AllMountedWeaponMagazines(target: MountedWeapons): Iterable[AmmoBox] = {
|
||||
|
|
@ -224,11 +227,10 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
|
|||
val target = entry.obj.asInstanceOf[FacilityTurret]
|
||||
val zone = entry.zone
|
||||
trace(s"Wall turret finished ${target.Upgrade} upgrade")
|
||||
target.ConfirmUpgrade(entry.upgrade)
|
||||
val targetGUID = target.GUID
|
||||
if (target.Health > 0) {
|
||||
target.Weapons
|
||||
.map({ case (index: Int, slot: EquipmentSlot) => (index, slot.Equipment) })
|
||||
.map { case (index: Int, slot: EquipmentSlot) => (index, slot.Equipment) }
|
||||
.collect {
|
||||
case (index, Some(tool: Tool)) =>
|
||||
context.parent ! VehicleServiceMessage(
|
||||
|
|
@ -237,6 +239,17 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
|
|||
)
|
||||
}
|
||||
}
|
||||
Finalize(target, entry.upgrade)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch messages to report on the completion of this effort.
|
||||
* @param target the object with mounted weaponry
|
||||
* @param upgrade the path of the turret's progression
|
||||
*/
|
||||
def Finalize(target: FacilityTurret, upgrade: TurretUpgrade.Value): Unit = {
|
||||
target.ConfirmUpgrade(upgrade)
|
||||
target.Actor ! TurretUpgrader.UpgradeCompleted(target.GUID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -263,6 +276,8 @@ object TurretUpgrader extends SupportActorCaseConversions {
|
|||
|
||||
final case class Downgrade()
|
||||
|
||||
final case class UpgradeCompleted(targetGuid: PlanetSideGUID)
|
||||
|
||||
private def Similarity(entry1: TurretUpgrader.Entry, entry2: TurretUpgrader.Entry): Boolean = {
|
||||
entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import net.psforever.objects.serverobject.structures.{Building, BuildingDefiniti
|
|||
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalDefinition}
|
||||
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
|
||||
import net.psforever.objects.serverobject.tube.SpawnTube
|
||||
import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretDefinition}
|
||||
import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretDefinition, VanuSentry}
|
||||
import net.psforever.objects.serverobject.zipline.ZipLinePath
|
||||
import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, TurretSource, VehicleSource}
|
||||
import net.psforever.objects.zones.{MapInfo, Zone, ZoneInfo, ZoneMap}
|
||||
|
|
@ -585,7 +585,7 @@ object Zones {
|
|||
case _ => ;
|
||||
}
|
||||
|
||||
case "manned_turret" | "vanu_sentry_turret" =>
|
||||
case "manned_turret" =>
|
||||
zoneMap.addLocalObject(
|
||||
obj.guid,
|
||||
FacilityTurret.Constructor(
|
||||
|
|
@ -596,6 +596,17 @@ object Zones {
|
|||
)
|
||||
zoneMap.linkTurretToWeapon(obj.guid, turretWeaponGuid.getAndIncrement())
|
||||
|
||||
case "vanu_sentry_turret" =>
|
||||
zoneMap.addLocalObject(
|
||||
obj.guid,
|
||||
VanuSentry.Constructor(
|
||||
obj.position,
|
||||
obj.objectDefinition.asInstanceOf[FacilityTurretDefinition]
|
||||
),
|
||||
owningBuildingGuid = ownerGuid
|
||||
)
|
||||
zoneMap.linkTurretToWeapon(obj.guid, turretWeaponGuid.getAndIncrement())
|
||||
|
||||
case "implant_terminal_mech" =>
|
||||
zoneMap.addLocalObject(
|
||||
obj.guid,
|
||||
|
|
|
|||
32
src/test/scala/game/AIDamageTest.scala
Normal file
32
src/test/scala/game/AIDamageTest.scala
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package game
|
||||
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
import scodec.bits._
|
||||
|
||||
class AIDamageTest extends Specification {
|
||||
val string1 = hex"5c de10 89e8 38030000 00000000 04020000"
|
||||
|
||||
"decode" in {
|
||||
PacketCoding.decodePacket(string1).require match {
|
||||
case AIDamage(target_guid, attacker_guid, projectile_type, unk1, unk2) =>
|
||||
target_guid mustEqual PlanetSideGUID(4318)
|
||||
attacker_guid mustEqual PlanetSideGUID(59529)
|
||||
projectile_type mustEqual 824L
|
||||
unk1 mustEqual 0L
|
||||
unk2 mustEqual 516L
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode" in {
|
||||
val msg = AIDamage(PlanetSideGUID(4318), PlanetSideGUID(59529), 824L, 0L, 516L)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string1
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue