PSF-BotServer/src/main/scala/net/psforever/objects/Deployables.scala
Fate-JH 93a544c07c
Collisions (#932)
* pattern for applying damage to player avatar and player-controlled vehicle collisions

* pattern for applying damage to targets due to collisions, falling damage and crashing damage individually; fields to support these calculations are provided

* modifiers to translate 'small step velocity' to real game velocity, as reported by the HUD; corrections for velocity; corrections for velocity in other packets

* fall damage calculations moved to function

* basic two-body collisions between GUID-identified game entities and a ward against too many collisions in a short amount of time

* bailing mechanics

* vssm for non-driven vehicles

* comment about vehicle state message field

* comments and minor refactoring for current collision damage calc; tank_traps modifier; potential fix for blockmap indexing issue

* fixed cargo/carrier vehicle ops

* corrections to initialization of ce construction items; adjustments to handling of modifiers for collision damage

* modifier change, protection against flight speed and spectator crashes; submerged status is once again known only to the actor

* appeasing the automated tests

* hopefully paced collisions better; re-did how Infantry collisions are calculated, incorporating mass and exo-suit data; kill feed reporting should be better

* adjusted damage values again, focusing on the lesser of or middling results; collision killfeed attribution attempt

* kicking offers bail protection; lowered the artificial modifier for one kind of collision damage calculation

* correction to the local reference map functions

* fixed tests; attempt to zero fall damage distance based on velocity; attempt to block mine damage when spectating
2021-10-05 09:59:49 -04:00

267 lines
12 KiB
Scala

// Copyright (c) 2017 PSForever
package net.psforever.objects
import akka.actor.ActorRef
import net.psforever.objects.avatar.{Avatar, Certification}
import scala.concurrent.duration._
import net.psforever.objects.ce.{Deployable, DeployedItem}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game._
import net.psforever.types.PlanetSideGUID
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
object Deployables {
//private val log = org.log4s.getLogger("Deployables")
object Make {
def apply(item: DeployedItem.Value): () => Deployable = cemap(item)
private val cemap: Map[DeployedItem.Value, () => Deployable] = Map(
DeployedItem.boomer -> { () => new BoomerDeployable(GlobalDefinitions.boomer) },
DeployedItem.he_mine -> { () => new ExplosiveDeployable(GlobalDefinitions.he_mine) },
DeployedItem.jammer_mine -> { () => new ExplosiveDeployable(GlobalDefinitions.jammer_mine) },
DeployedItem.spitfire_turret -> { () => new TurretDeployable(GlobalDefinitions.spitfire_turret) },
DeployedItem.spitfire_cloaked -> { () => new TurretDeployable(GlobalDefinitions.spitfire_cloaked) },
DeployedItem.spitfire_aa -> { () => new TurretDeployable(GlobalDefinitions.spitfire_aa) },
DeployedItem.motionalarmsensor -> { () => new SensorDeployable(GlobalDefinitions.motionalarmsensor) },
DeployedItem.sensor_shield -> { () => new SensorDeployable(GlobalDefinitions.sensor_shield) },
DeployedItem.tank_traps -> { () => new TrapDeployable(GlobalDefinitions.tank_traps) },
DeployedItem.portable_manned_turret -> { () => new TurretDeployable(GlobalDefinitions.portable_manned_turret) },
DeployedItem.portable_manned_turret -> { () => new TurretDeployable(GlobalDefinitions.portable_manned_turret) },
DeployedItem.portable_manned_turret_nc -> { () =>
new TurretDeployable(GlobalDefinitions.portable_manned_turret_nc)
},
DeployedItem.portable_manned_turret_tr -> { () =>
new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr)
},
DeployedItem.portable_manned_turret_vs -> { () =>
new TurretDeployable(GlobalDefinitions.portable_manned_turret_vs)
},
DeployedItem.deployable_shield_generator -> { () =>
new ShieldGeneratorDeployable(GlobalDefinitions.deployable_shield_generator)
},
DeployedItem.router_telepad_deployable -> { () =>
new TelepadDeployable(GlobalDefinitions.router_telepad_deployable)
}
).withDefaultValue({ () => new ExplosiveDeployable(GlobalDefinitions.boomer) })
}
/**
* Distribute information that a deployable has been destroyed.
* Additionally, since the player who destroyed the deployable isn't necessarily the owner,
* and the real owner will still be aware of the existence of the deployable,
* that player must be informed of the loss of the deployable directly.
* @see `AnnounceDestroyDeployable(Deployable)`
* @see `Deployable.Deconstruct`
* @param target the deployable that is destroyed
* @param time length of time that the deployable is allowed to exist in the game world;
* `None` indicates the normal un-owned existence time (180 seconds)
*/
def AnnounceDestroyDeployable(target: Deployable, time: Option[FiniteDuration]): Unit = {
AnnounceDestroyDeployable(target)
target.Actor ! Deployable.Deconstruct(time)
}
/**
* Distribute information that a deployable has been destroyed.
* The deployable may not have yet been eliminated from the game world (client or server),
* but its health is zero and it has entered the conditions where it is nearly irrelevant.<br>
* <br>
* The typical use case of this function involves destruction via weapon fire, attributed to a particular player.
* Contrast this to simply destroying a deployable by being the deployable's owner and using the map icon controls.
* This function eventually invokes the same routine
* but mainly goes into effect when the deployable has been destroyed
* and may still leave a physical component in the game world to be cleaned up later.
* @see `DeployableInfo`
* @see `DeploymentAction`
* @see `LocalAction.DeployableMapIcon`
* @param target the deployable that is destroyed
**/
def AnnounceDestroyDeployable(target: Deployable): Unit = {
val zone = target.Zone
val events = zone.LocalEvents
val item = target.Definition.Item
target.OwnerName match {
case Some(owner) =>
zone.Players.find { p => owner.equals(p.name) } match {
case Some(p) =>
if (p.deployables.Remove(target)) {
events ! LocalServiceMessage(owner, LocalAction.DeployableUIFor(item))
}
case None => ;
}
target.Owner = None
target.OwnerName = None
case None => ;
}
events ! LocalServiceMessage(
s"${target.Faction}",
LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(target.GUID, Deployable.Icon(item), target.Position, PlanetSideGUID(0))
)
)
}
/**
* Collect all deployables previously owned by the player,
* dissociate the avatar's globally unique identifier to remove turnover ownership,
* and, on top of performing the above manipulations, dispose of any boomers discovered.
* (`BoomerTrigger` objects, the companions of the boomers, should be handled by an external implementation
* if they had not already been handled by the time this function is executed.)
* @return all previously-owned deployables after they have been processed;
* boomers are listed before all other deployable types
*/
def Disown(zone: Zone, avatar: Avatar, replyTo: ActorRef): List[Deployable] = {
avatar.deployables
.Clear()
.map(zone.GUID)
.collect {
case Some(obj: Deployable) =>
obj.Actor ! Deployable.Ownership(None)
obj.Owner = None //fast-forward the effect
obj
}
}
/**
* Initialize the deployables backend information.
* @param avatar the player's core
*/
def InitializeDeployableQuantities(avatar: Avatar): Boolean = {
avatar.deployables.Initialize(avatar.certifications)
}
/**
* Initialize the UI elements for deployables.
* @param avatar the player's core
*/
def InitializeDeployableUIElements(avatar: Avatar): List[(Int, Int, Int, Int)] = {
avatar.deployables.UpdateUI()
}
/**
* If the default ammunition mode for the `ConstructionTool` is not supported by the given certifications,
* find a suitable ammunition mode and switch to it internally.
* No special complaint is raised if the `ConstructionItem` itself is completely unsupported.
* The search function will explore every ammo option for every fire mode option
* and will stop when it finds either a valid option or when arrives back at the original fire mode.
* @param certs the certification baseline being compared against
* @param obj the `ConstructionItem` entity
* @return `true`, if the firemode and ammunition mode of the item is valid;
* `false`, otherwise
*/
def initializeConstructionItem(
certs: Set[Certification],
obj: ConstructionItem
): Boolean = {
val initialFireModeIndex = obj.FireModeIndex
if (!Deployables.constructionItemPermissionComparison(certs, obj.ModePermissions)) {
while (!Deployables.constructionItemPermissionComparison(certs, obj.ModePermissions) &&
!Deployables.performConstructionItemAmmoChange(certs, obj, obj.AmmoTypeIndex) &&
{
obj.NextFireMode
initialFireModeIndex != obj.FireModeIndex
}) {
/* change in fire mode occurs in conditional */
}
Deployables.constructionItemPermissionComparison(certs, obj.ModePermissions)
} else {
true
}
}
/**
* The custom behavior responding to the packet `ChangeAmmoMessage` for `ConstructionItem` game objects.
* Iterate through sub-modes corresponding to a type of "deployable" as ammunition for this fire mode
* and check each of these sub-modes for their certification requirements to be met before they can be used.
* Additional effort is exerted to ensure that the requirements for the given ammunition are satisfied.
* If no satisfactory combination is achieved, the original state will be restored.
* @see `Certification`
* @see `ChangeAmmoMessage`
* @see `ConstructionItem.ModePermissions`
* @see `Deployables.constructionItemPermissionComparison`
* @param certs the certification baseline being compared against
* @param obj the `ConstructionItem` entity
* @param originalAmmoIndex the starting point ammunition type mode index
* @return `true`, if the ammunition mode of the item has been changed;
* `false`, otherwise
*/
def performConstructionItemAmmoChange(
certs: Set[Certification],
obj: ConstructionItem,
originalAmmoIndex: Int
): Boolean = {
do {
obj.NextAmmoType
} while (
!Deployables.constructionItemPermissionComparison(certs, obj.ModePermissions) &&
originalAmmoIndex != obj.AmmoTypeIndex
)
obj.AmmoTypeIndex != originalAmmoIndex
}
/**
* The custom behavior responding to the message `ChangeFireModeMessage` for `ConstructionItem` game objects.
* Each fire mode has sub-modes corresponding to a type of "deployable" as ammunition
* and each of these sub-modes have certification requirements that must be met before they can be used.
* Additional effort is exerted to ensure that the requirements for the given mode and given sub-mode are satisfied.
* If no satisfactory combination is achieved, the original state will be restored.
* @see `Deployables.constructionItemPermissionComparison`
* @see `Deployables.performConstructionItemAmmoChange`
* @see `FireModeSwitch.NextFireMode`
* @param certs the certification baseline being compared against
* @param obj the `ConstructionItem` entity
* @param originalModeIndex the starting point fire mode index
* @return `true`, if the ammunition mode of the item has been changed;
* `false`, otherwise
*/
def performConstructionItemFireModeChange(
certs: Set[Certification],
obj: ConstructionItem,
originalModeIndex: Int
): Boolean = {
/*
if any of the fire modes possess an initial option that is not valid for a given set of certifications,
but a subsequent option is valid, the do...while loop has to be modified to traverse and compare each option
*/
do {
obj.NextFireMode
} while (
!Deployables.constructionItemPermissionComparison(certs, obj.ModePermissions) &&
originalModeIndex != obj.FireModeIndex
)
originalModeIndex != obj.FireModeIndex
}
/**
* Compare sets of certifications to determine if
* the requested `Engineering`-like certification requirements of the one group can be found in a another group.
* @see `CertificationType`
* @param sample the certifications to be compared against
* @param test the desired certifications
* @return `true`, if the desired certification requirements are met; `false`, otherwise
*/
def constructionItemPermissionComparison(
sample: Set[Certification],
test: Set[Certification]
): Boolean = {
import Certification._
val engineeringCerts: Set[Certification] = Set(AssaultEngineering, FortificationEngineering)
val testDiff: Set[Certification] = test diff (engineeringCerts ++ Set(AdvancedEngineering))
//substitute `AssaultEngineering` and `FortificationEngineering` for `AdvancedEngineering`
val sampleIntersect = if (sample contains AdvancedEngineering) {
engineeringCerts
} else {
sample intersect engineeringCerts
}
val testIntersect = if (test contains AdvancedEngineering) {
engineeringCerts
} else {
test intersect engineeringCerts
}
(sample intersect testDiff equals testDiff) && (sampleIntersect intersect testIntersect equals testIntersect)
}
}