Damage Changes/Explosions (#644)

* created base damage interaction classes and replaced various projectile-based damage that utilized ResolvedProjectile; not refined, maintains redundancy and overloads, but should work

* continuing to reduce the exposure of ResolvedProjectile and replacing it with applications of DamageInteraction, DamageResult, and DamageReason

* removed ResolvedProjectile from the project; adjusted remaining code paths to work around it

* vitals.test became vital.base; no one liked this

* lots of inheritance, polymorphism, and other chicanery; moved around files, so it also looks like more files have changed when they have not (even if they did)

* codecov file correction

* master rebase; vital directory structure changed, so file imports have been modified in several other files; ResolutionSelection has been removed, requiring direct function literal assignment; tests repaired, where necessary; no actual functional change

* code comments

* DamageResult is its own case class now, wrapping around a before/after target and the interaction used in its calaculations; tests have been corrected

* adjusted Player.Die() to demonstrate a damage-based suicide approach

* resolved circular inheritance in projectile damage modifiers; better employed explosion reason, damages players around exploding vehicle as example

* expanded explosions to other object types; exploding is now a flag and the damage is an innate property of the object type; removed advanced references to properties on the damage source, since the damage source is easily accessible; wrote comments; fixed tests

* overhaul to painbox damage to align with normal player damage handling, thus assimilating it properly into the damage system

* future development; normal vector from euler angles; custom proximity test

* where 'innateDamage' should have not replaced 'explosion'

* moved the hitPos for the generator test; attempting to imrpove the reliability of the auto-repair integration tests (didn't ...)

* spelling and private val
This commit is contained in:
Fate-JH 2020-12-08 14:32:42 -05:00 committed by GitHub
parent b3101d9a8d
commit 6c93746767
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
99 changed files with 4083 additions and 2490 deletions

View file

@ -4,7 +4,7 @@ comment: off
ignore:
- "src/main/scala/net/psforever/objects/ObjectType.scala"
- "src/main/scala/net/psforever/objects/avatar/Avatars.scala"
- "src/main/scala/net/psforever/objects/ballistics/ProjectileResolution.scala"
- "src/main/scala/net/psforever/objects/ballistics/DamageResolution.scala"
- "src/main/scala/net/psforever/objects/ballistics/Projectiles.scala"
- "src/main/scala/net/psforever/objects/equipment/Ammo.scala"
- "src/main/scala/net/psforever/objects/equipment/CItem.scala"
@ -22,15 +22,23 @@ ignore:
- "src/main/scala/net/psforever/objects/vehicles/SeatArmoRestriction.scala"
- "src/main/scala/net/psforever/objects/vehicles/Turrets.scala"
- "src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala"
- "src/main/scala/net/psforever/objects/vital/base"
- "src/main/scala/net/psforever/objects/vital/collision"
- "src/main/scala/net/psforever/objects/vital/damage/DamageProfile.scala"
- "src/main/scala/net/psforever/objects/vital/projectile/ProjectileCalculations.scala"
- "src/main/scala/net/psforever/objects/vital/damage/SpecificDamageProfile.scala"
- "src/main/scala/net/psforever/objects/vital/etc"
- "src/main/scala/net/psforever/objects/vital/interaction"
- "src/main/scala/net/psforever/objects/vital/projectile/ProjectileReason.scala"
- "src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifiers.scala"
- "src/main/scala/net/psforever/objects/vital/prop"
- "src/main/scala/net/psforever/objects/vital/resistance/ResistanceProfile.scala"
- "src/main/scala/net/psforever/objects/vital/resistance/ResistanceSelection.scala"
- "src/main/scala/net/psforever/objects/vital/resolution/DamageResistanceCalculations.scala"
- "src/main/scala/net/psforever/objects/vital/resolution/DamageResistanceModel.scala"
- "src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala"
- "src/main/scala/net/psforever/objects/vital/DamageType.scala"
- "src/main/scala/net/psforever/objects/vital/StandardDamages.scala"
- "src/main/scala/net/psforever/objects/vital/StandardResistanceProfile.scala"
- "src/main/scala/net/psforever/objects/vital/StandardResistances.scala"
- "src/main/scala/net/psforever/objects/vital/StandardResolutions.scala"
- "src/main/scala/net/psforever/objects/vital/VitalityDefinition.scala"
- "src/main/scala/net/psforever/packet/crypto"
- "src/main/scala/net/psforever/packet/game/objectcreate/DrawnSlot.scala"
- "src/main/scala/net/psforever/packet/game/objectcreate/DriveState.scala"

View file

@ -7,10 +7,12 @@ import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
import akka.pattern.ask
import akka.util.Timeout
import java.util.concurrent.TimeUnit
import net.psforever.actors.net.MiddlewareActor
import net.psforever.services.ServiceManager.Lookup
import net.psforever.objects.locker.LockerContainer
import org.log4s.MDC
import scala.collection.mutable
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
@ -24,7 +26,7 @@ import net.psforever.objects.ce._
import net.psforever.objects.definition._
import net.psforever.objects.definition.converter.{CorpseConverter, DestroyedVehicleConverter}
import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity}
import net.psforever.objects.equipment.{ChargeFireModeDefinition, EffectTarget, Equipment, FireModeSwitch, JammableUnit}
import net.psforever.objects.equipment._
import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver}
import net.psforever.objects.inventory.{Container, InventoryItem}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
@ -40,7 +42,6 @@ import net.psforever.objects.serverobject.locks.IFFLock
import net.psforever.objects.serverobject.mblocker.Locker
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
import net.psforever.objects.serverobject.painbox.Painbox
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate}
import net.psforever.objects.serverobject.terminals._
@ -51,6 +52,9 @@ import net.psforever.objects.teamwork.Squad
import net.psforever.objects.vehicles._
import net.psforever.objects.vehicles.Utility.InternalTelepad
import net.psforever.objects.vital._
import net.psforever.objects.vital.base._
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning}
import net.psforever.packet._
import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo, _}
@ -64,12 +68,7 @@ import net.psforever.services.local.support.RouterTelepadActivation
import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse}
import net.psforever.services.properties.PropertyOverrideManager
import net.psforever.services.support.SupportActor
import net.psforever.services.teamwork.{
SquadResponse,
SquadServiceMessage,
SquadServiceResponse,
SquadAction => SquadServiceAction
}
import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction}
import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
import net.psforever.services.{InterstellarClusterService, RemoverActor, Service, ServiceManager}
import net.psforever.types._
@ -1783,31 +1782,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(GenericObjectActionMessage(guid, 9))
case AvatarResponse.EnvironmentalDamage(target, source, amount) =>
if (player.isAlive && amount > 0) {
val playerGUID = player.GUID
val armor = player.Armor
val capacitor = player.Capacitor
val originalHealth = player.Health
//history
continent.GUID(source) match {
case Some(obj: Painbox) =>
player.History(DamageFromPainbox(PlayerSource(player), obj, amount))
case _ => ;
}
CancelZoningProcessWithDescriptiveReason("cancel_dmg")
player.Health = originalHealth - amount
sendResponse(PlanetsideAttributeMessage(target, 0, player.Health))
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.PlanetsideAttribute(target, 0, player.Health)
)
damageLog.info(
s"${player.Name}-infantry: BEFORE=$originalHealth/$armor/$capacitor, AFTER=${player.Health}/$armor/$capacitor, CHANGE=$amount/0/0"
)
if (player.Health == 0 && player.isAlive) {
player.Actor ! Player.Die()
}
}
CancelZoningProcessWithDescriptiveReason("cancel_dmg")
//TODO damage marker?
case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
// guid = victim // killer = killer ;)
@ -5215,7 +5191,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
shotOrigin: Vector3,
hitPos: Vector3
) =>
ResolveProjectileEntry(projectile, ProjectileResolution.Hit, target, hitPos) match {
ResolveProjectileInteraction(projectile, DamageResolution.Hit, target, hitPos) match {
case Some(resprojectile) =>
HandleDealingDamage(target, resprojectile)
case None => ;
@ -5244,15 +5220,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
projectile.Velocity = projectile_vel
val (resolution1, resolution2) = profile.Aggravated match {
case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) =>
(ProjectileResolution.AggravatedDirect, ProjectileResolution.AggravatedSplash)
(DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash)
case _ =>
(ProjectileResolution.Splash, ProjectileResolution.Splash)
(DamageResolution.Splash, DamageResolution.Splash)
}
//direct_victim_uid
ValidObject(direct_victim_uid) match {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target)
ResolveProjectileEntry(projectile, resolution1, target, target.Position) match {
ResolveProjectileInteraction(projectile, resolution1, target, target.Position) match {
case Some(projectile) =>
HandleDealingDamage(target, projectile)
case None => ;
@ -5264,7 +5240,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
ValidObject(elem.uid) match {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
ResolveProjectileEntry(projectile, resolution2, target, explosion_pos) match {
ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos) match {
case Some(projectile) =>
HandleDealingDamage(target, projectile)
case None => ;
@ -5289,7 +5265,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
ValidObject(victim_guid) match {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target)
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Lash, target, hit_pos) match {
ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos) match {
case Some(projectile) =>
HandleDealingDamage(target, projectile)
case None => ;
@ -7648,15 +7624,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* @param resolution the resolution status to promote the projectile
* @return the projectile
*/
def ResolveProjectileEntry(
projectile_guid: PlanetSideGUID,
resolution: ProjectileResolution.Value,
target: PlanetSideGameObject with FactionAffinity with Vitality,
pos: Vector3
): Option[ResolvedProjectile] = {
def ResolveProjectileInteraction(
projectile_guid: PlanetSideGUID,
resolution: DamageResolution.Value,
target: PlanetSideGameObject with FactionAffinity with Vitality,
pos: Vector3
): Option[DamageInteraction] = {
FindProjectileEntry(projectile_guid) match {
case Some(projectile) =>
ResolveProjectileEntry(projectile, resolution, target, pos)
ResolveProjectileInteraction(projectile, resolution, target, pos)
case None =>
log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found")
None
@ -7670,18 +7646,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* @param resolution the resolution status to promote the projectile
* @return a copy of the projectile
*/
def ResolveProjectileEntry(
projectile: Projectile,
index: Int,
resolution: ProjectileResolution.Value,
target: PlanetSideGameObject with FactionAffinity with Vitality,
pos: Vector3
): Option[ResolvedProjectile] = {
def ResolveProjectileInteraction(
projectile: Projectile,
index: Int,
resolution: DamageResolution.Value,
target: PlanetSideGameObject with FactionAffinity with Vitality,
pos: Vector3
): Option[DamageInteraction] = {
if (!projectiles(index).contains(projectile)) {
log.error(s"expected projectile could not be found at $index; can not resolve")
None
} else {
ResolveProjectileEntry(projectile, resolution, target, pos)
ResolveProjectileInteraction(projectile, resolution, target, pos)
}
}
@ -7691,12 +7667,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* @param resolution the resolution status to promote the projectile
* @return a copy of the projectile
*/
def ResolveProjectileEntry(
projectile: Projectile,
resolution: ProjectileResolution.Value,
target: PlanetSideGameObject with FactionAffinity with Vitality,
pos: Vector3
): Option[ResolvedProjectile] = {
def ResolveProjectileInteraction(
projectile: Projectile,
resolution: DamageResolution.Value,
target: PlanetSideGameObject with FactionAffinity with Vitality,
pos: Vector3
): Option[DamageInteraction] = {
if (projectile.isMiss) {
log.error("expected projectile was already counted as a missed shot; can not resolve any further")
None
@ -7715,7 +7691,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
} else {
projectile
}
Some(ResolvedProjectile(resolution, outProjectile, SourceEntry(target), target.DamageModel, pos))
Some(DamageInteraction(SourceEntry(target), ProjectileReason(resolution, outProjectile, target.DamageModel), pos))
}
}
@ -7760,7 +7736,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* Calculate the amount of damage to be dealt to an active `target`
* using the information reconstructed from a `Resolvedprojectile`
* and affect the `target` in a synchronized manner.
* The active `target` and the target of the `ResolvedProjectile` do not have be the same.
* 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`,
* only specific `Vitality` entity types are being screened for sustaining damage.
* @see `DamageResistanceModel`
@ -7768,8 +7744,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* @param target a valid game object that is known to the server
* @param data a projectile that will affect the target
*/
def HandleDealingDamage(target: PlanetSideGameObject with Vitality, data: ResolvedProjectile): Unit = {
val func = data.damage_model.Calculate(data)
def HandleDealingDamage(target: PlanetSideGameObject with Vitality, data: DamageInteraction): Unit = {
val func = data.calculate()
target match {
case obj: Player if obj.CanDamage && obj.Actor != Default.Actor =>
// auto kick players damaging spectators
@ -7847,7 +7823,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
def UpdateDeployableUIElements(list: List[(Int, Int, Int, Int)]): Unit = {
val guid = PlanetSideGUID(0)
list.foreach({
case ((currElem, curr, maxElem, max)) =>
case (currElem, curr, maxElem, max) =>
//fields must update in ordered pairs: max, curr
sendResponse(PlanetsideAttributeMessage(guid, maxElem, max))
sendResponse(PlanetsideAttributeMessage(guid, currElem, curr))

View file

@ -440,7 +440,7 @@ class BuildingActor(
case Request(amount, replyTo) =>
building match {
case b: WarpGate =>
//warp gates are an infiite source of nanites
//warp gates are an infinite source of nanites
replyTo ! Grant(b, if (b.Active) amount else 0)
Behaviors.same
case _ if building.BuildingType == StructureType.Tower || building.Zone.map.cavern =>

View file

@ -2,18 +2,19 @@
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.ce._
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.definition.converter.SmallDeployableConverter
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.vital.resolution.ResolutionCalculations.Output
import net.psforever.objects.vital.StandardResolutions
import net.psforever.objects.vital.SimpleResolutions
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.Zone
import net.psforever.types.{PlanetSideGUID, Vector3}
import net.psforever.types.Vector3
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
@ -28,7 +29,7 @@ class ExplosiveDeployable(cdef: ExplosiveDeployableDefinition) extends ComplexDe
class ExplosiveDeployableDefinition(private val objectId: Int) extends ComplexDeployableDefinition(objectId) {
Name = "explosive_deployable"
DeployCategory = DeployableCategory.Mines
Model = StandardResolutions.SimpleDeployables
Model = SimpleResolutions.calculate
Packet = new SmallDeployableConverter
private var detonateOnJamming: Boolean = true
@ -73,7 +74,7 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D
val originalHealth = mine.Health
val cause = applyDamageTo(mine)
val damage = originalHealth - mine.Health
if (Damageable.CanDamageOrJammer(mine, damage, cause)) {
if (Damageable.CanDamageOrJammer(mine, damage, cause.interaction)) {
ExplosiveDeployableControl.DamageResolution(mine, cause, damage)
} else {
mine.Health = originalHealth
@ -83,21 +84,26 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D
}
object ExplosiveDeployableControl {
def DamageResolution(target: ExplosiveDeployable, cause: ResolvedProjectile, damage: Int): Unit = {
def DamageResolution(target: ExplosiveDeployable, cause: DamageResult, damage: Int): Unit = {
target.History(cause)
if (target.Health == 0) {
DestructionAwareness(target, cause)
} else if (!target.Jammed && Damageable.CanJammer(target, cause)) {
if (
target.Jammed = {
val radius = cause.projectile.profile.DamageRadius
Vector3.DistanceSquared(cause.hit_pos, cause.target.Position) < radius * radius
} else if (!target.Jammed && Damageable.CanJammer(target, cause.interaction)) {
if ( {
target.Jammed = cause.interaction.cause match {
case o: ProjectileReason =>
val radius = o.projectile.profile.DamageRadius
Vector3.DistanceSquared(cause.interaction.hitPos, cause.interaction.target.Position) < radius * radius
case _ =>
true
}
}
) {
if (target.Definition.DetonateOnJamming) {
if (cause.interaction.cause.source.SympatheticExplosion || target.Definition.DetonateOnJamming) {
val zone = target.Zone
zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos)
zone.Activity ! Zone.HotSpot.Activity(cause)
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.Detonate(target.GUID, target))
Zone.causeExplosion(zone, target, Some(cause))
}
DestructionAwareness(target, cause)
}
@ -109,12 +115,9 @@ object ExplosiveDeployableControl {
* @param target na
* @param cause na
*/
def DestructionAwareness(target: ExplosiveDeployable, cause: ResolvedProjectile): Unit = {
def DestructionAwareness(target: ExplosiveDeployable, cause: DamageResult): Unit = {
val zone = target.Zone
val attribution = zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match {
case Some(player) => player.GUID
case _ => PlanetSideGUID(0)
}
val attribution = DamageableEntity.attributionTo(cause, target.Zone)
target.Destroyed = true
Deployables.AnnounceDestroyDeployable(target, Some(if (target.Jammed) 0 seconds else 500 milliseconds))
zone.AvatarEvents ! AvatarServiceMessage(

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,17 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.avatar.{
Avatar,
LoadoutManager
}
import net.psforever.objects.definition.{
AvatarDefinition,
ExoSuitDefinition,
SpecialExoSuitDefinition
}
import net.psforever.objects.avatar.{Avatar, LoadoutManager}
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}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.aura.AuraContainer
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.{DamageResistanceModel, Vitality}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.ZoneAware
import net.psforever.types.{PlanetSideGUID, _}
@ -532,7 +527,15 @@ object Player {
final val FreeHandSlot: Int = 250
final val HandsDownSlot: Int = 255
final case class Die()
final case class Die(reason: Option[DamageInteraction])
object Die {
def apply(): Die = Die(None)
def apply(reason: DamageInteraction): Die = {
Die(Some(reason))
}
}
def apply(core: Avatar): Player = {
new Player(core)

View file

@ -2,7 +2,6 @@
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.ce._
import net.psforever.objects.definition.converter.SmallDeployableConverter
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
@ -11,7 +10,8 @@ import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.repair.RepairableEntity
import net.psforever.objects.vital.StandardResolutions
import net.psforever.objects.vital.SimpleResolutions
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.types.{PlanetSideGUID, Vector3}
import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
@ -24,7 +24,7 @@ class SensorDeployable(cdef: SensorDeployableDefinition) extends ComplexDeployab
class SensorDeployableDefinition(private val objectId: Int) extends ComplexDeployableDefinition(objectId) {
Name = "sensor_deployable"
DeployCategory = DeployableCategory.Sensors
Model = StandardResolutions.SimpleDeployables
Model = SimpleResolutions.calculate
Packet = new SmallDeployableConverter
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
@ -62,7 +62,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
override protected def DamageLog(msg: String): Unit = {}
override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
SensorDeployableControl.DestructionAwareness(sensor, PlanetSideGUID(0))
}

View file

@ -2,7 +2,6 @@
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployableCategory}
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.definition.converter.ShieldGeneratorConverter
@ -12,6 +11,7 @@ import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.repair.RepairableEntity
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.resolution.ResolutionCalculations
import net.psforever.types.PlanetSideGUID
import net.psforever.services.Service
@ -96,7 +96,7 @@ class ShieldGeneratorControl(gen: ShieldGeneratorDeployable)
}
}
override protected def DamageAwareness(target: Damageable.Target, cause: ResolvedProjectile, amount: Any): Unit = {
override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any): Unit = {
val (damageToHealth, damageToShields) = amount match {
case (a: Int, b: Int) => (a, b)
case _ => (0, 0)
@ -105,7 +105,7 @@ class ShieldGeneratorControl(gen: ShieldGeneratorDeployable)
ShieldGeneratorControl.DamageAwareness(gen, cause, damageToShields > 0)
}
override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = {
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
ShieldGeneratorControl.DestructionAwareness(gen, PlanetSideGUID(0))
}
@ -150,7 +150,7 @@ object ShieldGeneratorControl {
* @param cause na
* @param damageToShields na
*/
def DamageAwareness(target: ShieldGeneratorDeployable, cause: ResolvedProjectile, damageToShields: Boolean): Unit = {
def DamageAwareness(target: ShieldGeneratorDeployable, cause: DamageResult, damageToShields: Boolean): Unit = {
//shields
if (damageToShields) {
val zone = target.Zone

View file

@ -2,19 +2,20 @@
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployedItem}
import net.psforever.objects.definition.converter.TRAPConverter
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.serverobject.repair.RepairableEntity
import net.psforever.objects.vital.StandardResolutions
import net.psforever.objects.vital.SimpleResolutions
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.Zone
class TrapDeployable(cdef: TrapDeployableDefinition) extends ComplexDeployable(cdef)
class TrapDeployableDefinition(objectId: Int) extends ComplexDeployableDefinition(objectId) {
Model = StandardResolutions.SimpleDeployables
Model = SimpleResolutions.calculate
Packet = new TRAPConverter
override def Initialize(obj: PlanetSideServerObject with Deployable, context: ActorContext) = {
@ -43,8 +44,9 @@ class TrapDeployableControl(trap: TrapDeployable) extends Actor with DamageableE
case _ =>
}
override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
Deployables.AnnounceDestroyDeployable(trap, None)
Zone.causeExplosion(target.Zone, target, Some(cause))
}
}

View file

@ -2,7 +2,6 @@
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployedItem}
import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition}
import net.psforever.objects.definition.converter.SmallTurretConverter
@ -16,7 +15,8 @@ import net.psforever.objects.serverobject.mount.MountableBehavior
import net.psforever.objects.serverobject.repair.RepairableWeaponTurret
import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret}
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.{StandardResolutions, StandardVehicleResistance}
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance}
class TurretDeployable(tdef: TurretDeployableDefinition)
extends ComplexDeployable(tdef)
@ -37,7 +37,7 @@ class TurretDeployableDefinition(private val objectId: Int)
Packet = new SmallTurretConverter
DamageUsing = DamageCalculations.AgainstVehicle
ResistUsing = StandardVehicleResistance
Model = StandardResolutions.FacilityTurrets
Model = SimpleResolutions.calculate
//override to clarify inheritance conflict
override def MaxHealth: Int = super[ComplexDeployableDefinition].MaxHealth
@ -91,7 +91,7 @@ class TurretControl(turret: TurretDeployable)
case _ => ;
}
override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = {
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
Deployables.AnnounceDestroyDeployable(turret, None)
}

View file

@ -12,7 +12,9 @@ import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.structures.AmenityOwner
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.{DamageResistanceModel, StandardResistanceProfile, Vitality}
import net.psforever.objects.vital.resistance.StandardResistanceProfile
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import scala.annotation.tailrec
@ -531,7 +533,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
def PrepareGatingManifest(): VehicleManifest = {
val manifest = VehicleManifest(this)
seats.collect { case (index, seat) if index > 0 => seat.Occupant = None }
seats.collect { case (index: Int, seat: Seat) if index > 0 => seat.Occupant = None }
vehicleGatingManifest = Some(manifest)
previousVehicleGatingManifest = None
manifest

View file

@ -4,16 +4,15 @@ package net.psforever.objects.avatar
import akka.actor.{Actor, ActorRef, Props}
import net.psforever.actors.session.AvatarActor
import net.psforever.objects.{Player, _}
import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile}
import net.psforever.objects.ballistics.{ObjectSource, PlayerSource}
import net.psforever.objects.equipment._
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
import net.psforever.objects.loadouts.Loadout
import net.psforever.objects.serverobject.aura.{Aura, AuraEffectBehavior}
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.vital.PlayerSuicide
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, Damageable}
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, Damageable, DamageableEntity}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.repair.Repairable
import net.psforever.objects.serverobject.terminals.Terminal
@ -28,6 +27,10 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import akka.actor.typed
import net.psforever.objects.locker.LockerContainerControl
import net.psforever.objects.serverobject.painbox.Painbox
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import scala.concurrent.duration._
@ -80,9 +83,28 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
.orElse(auraBehavior)
.orElse(containerBehavior)
.orElse {
case Player.Die() =>
case Player.Die(Some(reason)) =>
if (player.isAlive) {
DestructionAwareness(player, None)
//primary death
PerformDamage(player, reason.calculate())
if(player.Health > 0 || player.isAlive) {
//that wasn't good enough
DestructionAwareness(player, None)
}
}
case Player.Die(None) =>
if (player.isAlive) {
//suicide
PerformDamage(
player,
DamageInteraction(
DamageResolution.Resolved,
PlayerSource(player),
SuicideReason(),
player.Position
).calculate()
)
}
case CommonMessages.Use(user, Some(item: Tool))
@ -524,7 +546,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
*/
def HandleDamage(
target: Player,
cause: ResolvedProjectile,
cause: DamageResult,
damageToHealth: Int,
damageToArmor: Int,
damageToStamina: Int,
@ -548,7 +570,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
def DamageAwareness(
target: Player,
cause: ResolvedProjectile,
cause: DamageResult,
damageToHealth: Int,
damageToArmor: Int,
damageToStamina: Int,
@ -561,7 +583,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
val health = target.Health
var announceConfrontation = damageToArmor > 0
//special effects
if (Damageable.CanJammer(target, cause)) {
if (Damageable.CanJammer(target, cause.interaction)) {
TryJammerEffectActivate(target, cause)
}
val aggravated: Boolean = TryAggravationEffectActivate(cause) match {
@ -571,7 +593,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
//initial damage for aggravation, but never treat as "aggravated"
false
case _ =>
cause.projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated)
cause.interaction.cause.source.Aggravated.nonEmpty
}
//log historical event
target.History(cause)
@ -595,23 +617,44 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
if(announceConfrontation) {
if (!aggravated) {
//activity on map
zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos)
zone.Activity ! Zone.HotSpot.Activity(cause)
//alert to damage source
zone.AvatarEvents ! AvatarServiceMessage(
target.Name,
cause.projectile.owner match {
case pSource: PlayerSource => //player damage
val name = pSource.Name
zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match {
case Some(tplayer) =>
AvatarAction.HitHint(tplayer.GUID, target.GUID)
case None =>
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(countableDamage, pSource.Position))
}
case source =>
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(countableDamage, source.Position))
}
)
cause.adversarial match {
case Some(adversarial) =>
adversarial.attacker match {
case pSource : PlayerSource => //player damage
val name = pSource.Name
zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match {
case Some(tplayer) =>
zone.AvatarEvents ! AvatarServiceMessage(
target.Name,
AvatarAction.HitHint(tplayer.GUID, target.GUID)
)
case None =>
zone.AvatarEvents ! AvatarServiceMessage(
target.Name,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
DamageWithPositionMessage(countableDamage, pSource.Position)
)
)
}
case source: ObjectSource if source.obj.isInstanceOf[Painbox] =>
zone.AvatarEvents ! AvatarServiceMessage(
target.Name,
AvatarAction.EnvironmentalDamage(target.GUID, source.obj.GUID, countableDamage)
)
case source =>
zone.AvatarEvents ! AvatarServiceMessage(
target.Name,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
DamageWithPositionMessage(countableDamage, source.Position)
)
)
}
case None =>
}
}
else {
//general alert
@ -644,7 +687,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
* @param target na
* @param cause na
*/
def DestructionAwareness(target: Player, cause: Option[ResolvedProjectile]): Unit = {
def DestructionAwareness(target: Player, cause: Option[DamageResult]): Unit = {
val player_guid = target.GUID
val pos = target.Position
val respawnTimer = 300000 //milliseconds
@ -694,17 +737,8 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 7, 0)) // capacitor
}
val attribute = cause match {
case Some(resolved) =>
resolved.projectile.owner match {
case pSource: PlayerSource =>
val name = pSource.Name
zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match {
case Some(tplayer) => tplayer.GUID
case None => player_guid
}
case _ => player_guid
}
case _ => player_guid
case Some(reason) => DamageableEntity.attributionTo(reason, target.Zone, player_guid)
case None => player_guid
}
events ! AvatarServiceMessage(
nameChannel,
@ -722,25 +756,21 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
)
//TODO other methods of death?
val pentry = PlayerSource(target)
(target.History.find({ p => p.isInstanceOf[PlayerSuicide] }) match {
case Some(PlayerSuicide(_)) =>
None
case _ =>
cause.orElse { target.LastShot } match {
case out @ Some(shot) =>
if (System.nanoTime - shot.hit_time < (10 seconds).toNanos) {
out
} else {
None //suicide
}
(cause match {
case Some(result) =>
result.adversarial
case None =>
target.LastDamage match {
case Some(attack) if System.currentTimeMillis() - attack.interaction.hitTime < (10 seconds).toMillis =>
attack.adversarial
case None =>
None //suicide
None
}
}) match {
case Some(shot) =>
case Some(adversarial) =>
events ! AvatarServiceMessage(
zoneChannel,
AvatarAction.DestroyDisplay(shot.projectile.owner, pentry, shot.projectile.attribute_to)
AvatarAction.DestroyDisplay(adversarial.attacker, pentry, adversarial.implement)
)
case None =>
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0))

View file

@ -2,7 +2,7 @@
package net.psforever.objects.ballistics
import net.psforever.objects.equipment.TargetValidation
import net.psforever.objects.serverobject.aura.Aura
import net.psforever.objects.vital.DamageType
import net.psforever.objects.vital.base.{DamageResolution, DamageType}
/**
* In what manner of pacing the aggravated damage ticks are applied.
@ -157,19 +157,19 @@ object AggravatedDamage {
targets
)
def burning(resolution: ProjectileResolution.Value): ProjectileResolution.Value = {
def burning(resolution: DamageResolution.Value): DamageResolution.Value = {
resolution match {
case ProjectileResolution.AggravatedDirect => ProjectileResolution.AggravatedDirectBurn
case ProjectileResolution.AggravatedSplash => ProjectileResolution.AggravatedSplashBurn
case DamageResolution.AggravatedDirect => DamageResolution.AggravatedDirectBurn
case DamageResolution.AggravatedSplash => DamageResolution.AggravatedSplashBurn
case _ => resolution
}
}
def basicDamageType(resolution: ProjectileResolution.Value): DamageType.Value = {
def basicDamageType(resolution: DamageResolution.Value): DamageType.Value = {
resolution match {
case ProjectileResolution.AggravatedDirect | ProjectileResolution.AggravatedDirectBurn =>
case DamageResolution.AggravatedDirect | DamageResolution.AggravatedDirectBurn =>
DamageType.Direct
case ProjectileResolution.AggravatedSplash | ProjectileResolution.AggravatedSplashBurn =>
case DamageResolution.AggravatedSplash | DamageResolution.AggravatedSplashBurn =>
DamageType.Splash
case _ =>
DamageType.None

View file

@ -1,7 +1,7 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.vital.StandardDamageProfile
import net.psforever.objects.vital.damage.StandardDamageProfile
final case class ChargeDamage(
effect_count: Int,

View file

@ -8,6 +8,7 @@ import net.psforever.objects.definition.{ProjectileDefinition, ToolDefinition}
import net.psforever.objects.entity.SimpleWorldEntity
import net.psforever.objects.equipment.FireModeDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.types.Vector3
/**
@ -31,7 +32,7 @@ import net.psforever.types.Vector3
* @param id an exclusive identifier for this projectile;
* normally generated internally, but can be manually set
* @param fire_time when the weapon discharged was recorded;
* defaults to `System.nanoTime`
* defaults to `System.currentTimeMillis()`
*/
final case class Projectile(
profile: ProjectileDefinition,
@ -57,7 +58,7 @@ final case class Projectile(
/** Information about the current world coordinates and orientation of the projectile */
val current: SimpleWorldEntity = new SimpleWorldEntity()
private var resolved: ProjectileResolution.Value = ProjectileResolution.Unresolved
private var resolved: DamageResolution.Value = DamageResolution.Unresolved
/**
* Create a copy of this projectile with all the same information
@ -89,16 +90,16 @@ final case class Projectile(
* Mark the projectile as being "encountered" or "managed" at least once.
*/
def Resolve(): Unit = {
resolved = ProjectileResolution.Resolved
resolved = DamageResolution.Resolved
}
def Miss(): Unit = {
resolved = ProjectileResolution.MissedShot
resolved = DamageResolution.Missed
}
def isResolved: Boolean = resolved == ProjectileResolution.Resolved || resolved == ProjectileResolution.MissedShot
def isResolved: Boolean = resolved == DamageResolution.Resolved || resolved == DamageResolution.Missed
def isMiss: Boolean = resolved == ProjectileResolution.MissedShot
def isMiss: Boolean = resolved == DamageResolution.Missed
def Definition = profile
}

View file

@ -1,25 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.vital.DamageResistanceModel
import net.psforever.types.Vector3
/**
* An encapsulation of a projectile event that records sufficient historical information
* about the interaction of weapons discharge and a target
* to the point that the original event might be reconstructed.
* Reenacting the calculations of this entry should always produce the same values.
* @param projectile the original projectile
* @param target what the projectile hit
* @param damage_model the kind of damage model to which the `target` is/was subject
* @param hit_pos where the projectile hit
*/
final case class ResolvedProjectile(
resolution : ProjectileResolution.Value,
projectile: Projectile,
target: SourceEntry,
damage_model: DamageResistanceModel,
hit_pos: Vector3
) {
val hit_time: Long = System.nanoTime
}

View file

@ -4,7 +4,8 @@ package net.psforever.objects.ce
import net.psforever.objects._
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.{DamageResistanceModel, Vitality}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.ZoneAware
import net.psforever.packet.game.DeployableIcon
import net.psforever.types.PlanetSideEmpire

View file

@ -8,6 +8,7 @@ import net.psforever.objects.inventory.InventoryTile
import net.psforever.objects.vital._
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
/**
@ -31,7 +32,7 @@ class ExoSuitDefinition(private val suitType: ExoSuitType.Value)
Name = "exo-suit"
DamageUsing = DamageCalculations.AgainstExoSuit
ResistUsing = StandardInfantryResistance
Model = StandardResolutions.Infantry
Model = InfantryResolutions.calculate
def SuitType: ExoSuitType.Value = suitType

View file

@ -1,10 +1,10 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition
import net.psforever.objects.ballistics.{AggravatedDamage, ChargeDamage, Projectiles}
import net.psforever.objects.equipment.JammingUnit
import net.psforever.objects.vital.damage.DamageModifiers
import net.psforever.objects.vital.{DamageType, StandardDamageProfile}
import net.psforever.objects.ballistics.Projectiles
import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.projectile.DistanceDegrade
import net.psforever.objects.vital.prop.DamageWithPosition
/**
* The definition that outlines the damage-dealing characteristics of any projectile.
@ -12,22 +12,14 @@ import net.psforever.objects.vital.{DamageType, StandardDamageProfile}
* @param objectId the object's identifier number
*/
class ProjectileDefinition(objectId: Int)
extends ObjectDefinition(objectId)
with JammingUnit
with StandardDamageProfile
with DamageModifiers {
extends ObjectDefinition(objectId)
with DamageWithPosition {
/** ascertain that this object is a valid projectile type */
private val projectileType: Projectiles.Value = Projectiles(objectId) //let throw NoSuchElementException
/** how much faster (or slower) the projectile moves (m/s^2^) */
private var acceleration: Int = 0
/** when the acceleration stops being applied (s) */
private var accelerationUntil: Float = 0f
/** the type of damage that the projectile causes */
private var damageType: DamageType.Value = DamageType.None
/** an auxillary type of damage that the projectile causes */
private var damageTypeSecondary: DamageType.Value = DamageType.None
/** against Infantry targets, this projectile does not do armor damage */
private var damageToHealthOnly: Boolean = false
/** number of seconds before an airborne projectile's damage begins to degrade (s) */
private var degradeDelay: Float = 1f
/** the rate of degrade of projectile damage after the degrade delay */
@ -36,16 +28,8 @@ class ProjectileDefinition(objectId: Int)
private var initialVelocity: Int = 1
/** for how long the projectile exists (s) */
private var lifespan: Float = 1f
/** for radial damage, how much damage has been lost the further away from the impact point (m) */
private var damageAtEdge: Float = 1f
/** for radial damage, the distance of the explosion effect (m) */
private var damageRadius: Float = 0f
/** for radial damage, the distance before degradation of the explosion effect (m) */
private var damageRadiusMin: Float = 1f
/** for lashing damage, how far away a target will be affected by the projectile (m) */
private var lashRadius : Float = 0f
/** use a specific modifier as a part of damage calculations */
private var useDamage1Subtract: Boolean = false
/** the projectile is represented by a server-side entity
* that is updated by the projectile owner
* and transmitted to all projectile observers;
@ -55,23 +39,13 @@ class ProjectileDefinition(objectId: Int)
* `0, 0` are artificial values;
* the oicw_little_buddy is undefined for these values */
private var remoteClientData: (Int, Int) = (0, 0)
/** some other entity confers projectile damage;
* a set value should not `None` and not `0` but is preferred to be the damager's uid */
private var damageProxy: Option[Int] = None
/** this projectile follows its target, after a fashion */
private var autoLock: Boolean = false
/** na;
* currently used with jammer properties only */
private var additionalEffect: Boolean = false
/** the projectile tries to confer the jammered status effect to its target(s) */
private var jammerProjectile: Boolean = false
/** projectile takes the form of a type of "grenade";
* grenades arc with gravity rather than travel in a relatively straight path */
private var grenade_projectile: Boolean = false
/** projectile tries to confers aggravated damage burn to its target */
private var aggravated_damage: Option[AggravatedDamage] = None
/** */
private var charging: Option[ChargeDamage] = None
//derived calculations
/** the calculated distance at which the projectile have traveled far enough to despawn (m);
* typically handled as the projectile no longer performing damage;
@ -85,17 +59,10 @@ class ProjectileDefinition(objectId: Int)
/** after acceleration, if any, what is the final speed of the projectile (m/s) */
private var finalVelocity: Float = 0f
Name = "projectile"
Modifiers = DamageModifiers.DistanceDegrade
Modifiers = DistanceDegrade
def ProjectileType: Projectiles.Value = projectileType
def UseDamage1Subtract: Boolean = useDamage1Subtract
def UseDamage1Subtract_=(useDamage1Subtract: Boolean): Boolean = {
this.useDamage1Subtract = useDamage1Subtract
UseDamage1Subtract
}
def Acceleration: Int = acceleration
def Acceleration_=(accel: Int): Int = {
@ -110,30 +77,21 @@ class ProjectileDefinition(objectId: Int)
AccelerationUntil
}
def ProjectileDamageType: DamageType.Value = damageType
def ProjectileDamageType: DamageType.Value = CausesDamageType
def ProjectileDamageType_=(damageType1: DamageType.Value): DamageType.Value = {
damageType = damageType1
CausesDamageType = damageType1
ProjectileDamageType
}
def ProjectileDamageTypeSecondary: DamageType.Value = damageTypeSecondary
def ProjectileDamageTypeSecondary: DamageType.Value = CausesDamageTypeSecondary
def ProjectileDamageTypeSecondary_=(damageTypeSecondary1: DamageType.Value): DamageType.Value = {
damageTypeSecondary = damageTypeSecondary1
CausesDamageTypeSecondary = damageTypeSecondary1
ProjectileDamageTypeSecondary
}
def ProjectileDamageTypes : Set[DamageType.Value] = {
Set(damageType, damageTypeSecondary).filterNot(_ == DamageType.None)
}
def DamageToHealthOnly : Boolean = damageToHealthOnly
def DamageToHealthOnly_=(healthOnly: Boolean) : Boolean = {
damageToHealthOnly = healthOnly
DamageToHealthOnly
}
def ProjectileDamageTypes : Set[DamageType.Value] = AllDamageTypes
def DegradeDelay: Float = degradeDelay
@ -163,27 +121,6 @@ class ProjectileDefinition(objectId: Int)
Lifespan
}
def DamageAtEdge: Float = damageAtEdge
def DamageAtEdge_=(damageAtEdge: Float): Float = {
this.damageAtEdge = damageAtEdge
DamageAtEdge
}
def DamageRadius: Float = damageRadius
def DamageRadius_=(damageRadius: Float): Float = {
this.damageRadius = damageRadius
DamageRadius
}
def DamageRadiusMin: Float = damageRadiusMin
def DamageRadiusMin_=(damageRadius: Float): Float = {
this.damageRadiusMin = damageRadius
DamageRadiusMin
}
def LashRadius: Float = lashRadius
def LashRadius_=(radius: Float): Float = {
@ -205,15 +142,6 @@ class ProjectileDefinition(objectId: Int)
RemoteClientData
}
def DamageProxy : Option[Int] = damageProxy
def DamageProxy_=(proxyObjectId : Int) : Option[Int] = DamageProxy_=(Some(proxyObjectId))
def DamageProxy_=(proxyObjectId : Option[Int]) : Option[Int] = {
damageProxy = proxyObjectId
DamageProxy
}
def AutoLock: Boolean = autoLock
def AutoLock_=(lockState: Boolean): Boolean = {
@ -221,13 +149,6 @@ class ProjectileDefinition(objectId: Int)
AutoLock
}
def AdditionalEffect: Boolean = additionalEffect
def AdditionalEffect_=(effect: Boolean): Boolean = {
additionalEffect = effect
AdditionalEffect
}
def JammerProjectile: Boolean = jammerProjectile
def JammerProjectile_=(effect: Boolean): Boolean = {
@ -242,24 +163,6 @@ class ProjectileDefinition(objectId: Int)
GrenadeProjectile
}
def Aggravated : Option[AggravatedDamage] = aggravated_damage
def Aggravated_=(damage : AggravatedDamage) : Option[AggravatedDamage] = Aggravated_=(Some(damage))
def Aggravated_=(damage : Option[AggravatedDamage]) : Option[AggravatedDamage] = {
aggravated_damage = damage
Aggravated
}
def Charging : Option[ChargeDamage] = charging
def Charging_=(damage : ChargeDamage) : Option[ChargeDamage] = Charging_=(Some(damage))
def Charging_=(damage : Option[ChargeDamage]) : Option[ChargeDamage] = {
charging = damage
Charging
}
def DistanceMax : Float = distanceMax //accessor only
def DistanceFromAcceleration: Float = distanceFromAcceleration //accessor only

View file

@ -8,7 +8,8 @@ import net.psforever.objects.definition.converter.SmallDeployableConverter
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.{DamageResistanceModel, NoResistanceSelection, VitalityDefinition}
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.vital.{NoResistanceSelection, VitalityDefinition}
import scala.concurrent.duration._

View file

@ -8,6 +8,7 @@ import net.psforever.objects.vehicles.{DestroyedVehicle, UtilityType}
import net.psforever.objects.vital._
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.types.Vector3
import scala.collection.mutable
@ -39,8 +40,7 @@ class VehicleDefinition(objectId: Int)
private var deploymentTime_Undeploy: Int = 0 //ms
private var trunkSize: InventoryTile = InventoryTile.None
private var trunkOffset: Int = 0
/** The position offset of the trunk, orientation as East = 0 */
/* The position offset of the trunk, orientation as East = 0 */
private var trunkLocation: Vector3 = Vector3.Zero
private var canCloak: Boolean = false
private var canFly: Boolean = false
@ -53,7 +53,7 @@ class VehicleDefinition(objectId: Int)
Packet = VehicleDefinition.converter
DamageUsing = DamageCalculations.AgainstVehicle
ResistUsing = StandardVehicleResistance
Model = StandardResolutions.Vehicle
Model = VehicleResolutions.calculate
RepairDistance = 10
RepairRestoresAt = 1

View file

@ -2,8 +2,8 @@
package net.psforever.objects.equipment
import net.psforever.objects.Tool
import net.psforever.objects.vital.SpecificDamageProfile
import net.psforever.objects.vital.damage.DamageModifiers
import net.psforever.objects.vital.base.DamageModifiers
import net.psforever.objects.vital.damage.SpecificDamageProfile
import scala.collection.mutable

View file

@ -3,9 +3,10 @@ package net.psforever.objects.equipment
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.{Default, PlanetSideGameObject, Tool}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.vehicles.MountedWeapons
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.ZoneAware
import net.psforever.types.Vector3
import net.psforever.services.Service
@ -43,7 +44,7 @@ object JammableUnit {
* A message for jammering due to a projectile.
* @param cause information pertaining to the projectile
*/
final case class Jammered(cause: ResolvedProjectile)
final case class Jammered(cause: DamageResult)
/**
* Stop the auditory aspect of being jammered.
@ -68,7 +69,7 @@ trait JammingUnit {
*/
private val jammedEffectDuration: mutable.ListBuffer[(TargetValidation, Int)] = new mutable.ListBuffer()
def HasJammedEffectDuration: Boolean = jammedEffectDuration.isEmpty
def HasJammedEffectDuration: Boolean = jammedEffectDuration.nonEmpty
def JammedEffectDuration: mutable.ListBuffer[(TargetValidation, Int)] = jammedEffectDuration
}
@ -85,7 +86,11 @@ object JammingUnit {
* @return the duration to be jammered, if any, in milliseconds
*/
def FindJammerDuration(jammer: JammingUnit, target: PlanetSideGameObject): Option[Int] = {
jammer.JammedEffectDuration
FindJammerDuration(jammer.JammedEffectDuration.toList, target)
}
def FindJammerDuration(durations: Iterable[(TargetValidation, Int)], target: PlanetSideGameObject): Option[Int] = {
durations
.collect { case (TargetValidation(_, test), duration) if test(target) => duration }
.toList
.sortWith(_ > _)
@ -134,15 +139,23 @@ 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: ResolvedProjectile): Unit =
def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit =
target match {
case obj: PlanetSideServerObject =>
val radius = cause.projectile.profile.DamageRadius
JammingUnit.FindJammerDuration(cause.projectile.profile, obj) match {
case Some(dur) if Vector3.DistanceSquared(cause.hit_pos, cause.target.Position) < radius * radius =>
StartJammeredSound(obj, dur)
StartJammeredStatus(obj, dur)
case _ => ;
val interaction = cause.interaction
JammingUnit.FindJammerDuration(interaction.cause.source.JammedEffectDuration.toList, obj) match {
case Some(dur) =>
if(interaction.cause match {
case reason: ProjectileReason =>
val radius = reason.projectile.profile.DamageRadius
Vector3.DistanceSquared(interaction.hitPos, interaction.target.Position) < radius * radius
case _ =>
true
}) {
StartJammeredSound(obj, dur)
StartJammeredStatus(obj, dur)
}
case None =>
}
case _ => ;
}

View file

@ -10,7 +10,10 @@ import net.psforever.objects.zones.ZoneAware
* An object layered on top of the standard game object class that maintains an internal `ActorRef`.
* A measure of synchronization can be managed using this `Actor` as a "controlling agent."
*/
abstract class PlanetSideServerObject extends PlanetSideGameObject with FactionAffinity with ZoneAware {
abstract class PlanetSideServerObject
extends PlanetSideGameObject
with FactionAffinity
with ZoneAware {
private var actor: ActorRef = ActorRef.noSender
private var getActorFunc: () => ActorRef = PlanetSideServerObject.getDefaultActor

View file

@ -4,7 +4,10 @@ package net.psforever.objects.serverobject.damage
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.ballistics._
import net.psforever.objects.serverobject.aura.Aura
import net.psforever.objects.vital.{DamageType, Vitality}
import net.psforever.objects.vital.base._
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.projectile.ProjectileReason
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
@ -21,16 +24,15 @@ trait AggravatedBehavior {
def AggravatedObject: AggravatedBehavior.Target
def TryAggravationEffectActivate(data: ResolvedProjectile): Option[AggravatedDamage] = {
val projectile = data.projectile
projectile.profile.Aggravated match {
case Some(damage)
if projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated) &&
damage.info.exists(_.damage_type == AggravatedDamage.basicDamageType(data.resolution)) &&
def TryAggravationEffectActivate(data: DamageResult): Option[AggravatedDamage] = {
(data.interaction.cause, data.interaction.cause.source.Aggravated) match {
case (o: ProjectileReason, Some(damage))
if data.interaction.cause.source.AllDamageTypes.contains(DamageType.Aggravated) &&
damage.info.exists(_.damage_type == AggravatedDamage.basicDamageType(data.interaction.resolution)) &&
damage.effect_type != Aura.Nothing &&
(projectile.quality == ProjectileQuality.AggravatesTarget ||
damage.targets.exists(validation => validation.test(AggravatedObject))) =>
TryAggravationEffectActivate(damage, data)
(o.projectile.quality == ProjectileQuality.AggravatesTarget ||
damage.targets.exists(validation => validation.test(AggravatedObject))) =>
TryAggravationEffectActivate(damage, data.interaction)
case _ =>
None
}
@ -38,10 +40,10 @@ trait AggravatedBehavior {
private def TryAggravationEffectActivate(
aggravation: AggravatedDamage,
data: ResolvedProjectile
data: DamageInteraction
): Option[AggravatedDamage] = {
val effect = aggravation.effect_type
if(CheckForUniqueUnqueuedProjectile(data.projectile)) {
if(CheckForUniqueUnqueuedCause(data.cause)) {
val sameEffect = entryIdToEntry.values.filter(entry => entry.effect == effect)
if(sameEffect.isEmpty || sameEffect.nonEmpty && aggravation.cumulative_damage_degrade) {
SetupAggravationEntry(aggravation, data)
@ -56,18 +58,21 @@ trait AggravatedBehavior {
}
}
private def CheckForUniqueUnqueuedProjectile(projectile : Projectile): Boolean = {
!entryIdToEntry.values.exists { entry => entry.data.projectile.id == projectile.id }
private def CheckForUniqueUnqueuedCause(cause : DamageReason): Boolean = {
!entryIdToEntry.values.exists { entry => entry.data.cause.same(cause) }
}
private def SetupAggravationEntry(aggravation: AggravatedDamage, data: ResolvedProjectile): Boolean = {
private def SetupAggravationEntry(aggravation: AggravatedDamage, data: DamageInteraction): Boolean = {
val effect = aggravation.effect_type
aggravation.info.find(_.damage_type == AggravatedDamage.basicDamageType(data.resolution)) match {
case Some(info) =>
//setup effect
val timing = aggravation.timing
val duration = timing.duration
//setup effect
val id = data.projectile.id
val id = data.cause match {
case o: ProjectileReason => o.projectile.id
case _ => data.hitTime
}
//setup timer data
val (tick: Long, iterations: Int) = timing.ticks match {
case Some(n) if n < 1 =>
@ -103,16 +108,16 @@ trait AggravatedBehavior {
id: Long,
effect: Aura,
retime: Long,
data: ResolvedProjectile,
data: DamageInteraction,
target: SourceEntry,
powerOffset: List[Float]
): AggravatedBehavior.Entry = {
val aggravatedDamageInfo = ResolvedProjectile(
AggravatedDamage.burning(data.resolution),
data.projectile,
val cause = data.cause
val aggravatedDamageInfo = DamageInteraction(
AggravatedDamage.burning(cause.resolution),
target,
data.damage_model,
data.hit_pos
cause,
data.hitPos
)
val entry = AggravatedBehavior.Entry(id, effect, retime, aggravatedDamageInfo, powerOffset)
entryIdToEntry += id -> entry
@ -159,7 +164,7 @@ trait AggravatedBehavior {
entryIdToEntry.remove(id) match {
case Some(entry) =>
ongoingAggravated = entryIdToEntry.nonEmpty
entry.data.projectile.profile.Aggravated.get.effect_type
entry.data.cause.source.Aggravated.get.effect_type
case _ =>
Aura.Nothing
}
@ -187,23 +192,31 @@ trait AggravatedBehavior {
def AggravatedReaction: Boolean = ongoingAggravated
private def PerformAggravation(entry: AggravatedBehavior.Entry, tick: Int = 0): Unit = {
val data = entry.data
val model = data.damage_model
val aggravatedProjectileData = ResolvedProjectile(
data.resolution,
data.projectile.quality(ProjectileQuality.Modified(entry.qualityPerTick(tick))),
data.target,
model,
data.hit_pos
)
takesDamage.apply(Vitality.Damage(model.Calculate(aggravatedProjectileData)))
entry.data.cause match {
case o: ProjectileReason =>
val aggravatedProjectileData = DamageInteraction(
entry.data.resolution,
entry.data.target,
ProjectileReason(
o.resolution,
o.projectile.quality(ProjectileQuality.Modified(entry.qualityPerTick(tick))),
o.damageModel
),
entry.data.hitPos
)
takesDamage.apply(Vitality.Damage(aggravatedProjectileData.calculate()))
case _ =>
//TODO how to apply tick damage degradation
takesDamage.apply(Vitality.Damage(entry.data.calculate()))
}
}
}
object AggravatedBehavior {
type Target = Damageable.Target
private case class Entry(id: Long, effect: Aura, retime: Long, data: ResolvedProjectile, qualityPerTick: List[Float])
private case class Entry(id: Long, effect: Aura, retime: Long, data: DamageInteraction, qualityPerTick: List[Float])
private case class Aggravate(id: Long, iterations: Int)
}

View file

@ -2,12 +2,12 @@
package net.psforever.objects.serverobject.damage
import akka.actor.Actor.Receive
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.resolution.ResolutionCalculations
/**
@ -77,16 +77,11 @@ object Damageable {
* @return `true`, if the target can be affected;
* `false`, otherwise
*/
def CanDamage(obj: Vitality with FactionAffinity, damage: Int, data: ResolvedProjectile): Boolean = {
def CanDamage(obj: Vitality with FactionAffinity, damage: Int, data: DamageInteraction): Boolean = {
val definition = obj.Definition
(damage > 0 || data.projectile.profile.Aggravated.nonEmpty) &&
(damage > 0 || data.cause.source.Aggravated.nonEmpty) &&
definition.Damageable &&
(definition.DamageableByFriendlyFire ||
(data.projectile.owner.Faction != obj.Faction ||
(obj match {
case hobj: Hackable => hobj.HackedBy.nonEmpty
case _ => false
})))
(definition.DamageableByFriendlyFire || adversarialOrHackableChecks(obj, data))
}
/**
@ -98,15 +93,21 @@ object Damageable {
* @return `true`, if the target can be affected;
* `false`, otherwise
*/
def CanJammer(obj: Vitality with FactionAffinity, data: ResolvedProjectile): Boolean = {
val projectile = data.projectile
projectile.profile.JammerProjectile &&
def CanJammer(obj: Vitality with FactionAffinity, data: DamageInteraction): Boolean = {
data.cause.source.HasJammedEffectDuration &&
obj.isInstanceOf[JammableUnit] &&
(projectile.owner.Faction != obj.Faction ||
(obj match {
case hobj: Hackable => hobj.HackedBy.nonEmpty
case _ => false
}))
adversarialOrHackableChecks(obj, data)
}
private def adversarialOrHackableChecks(obj: Vitality with FactionAffinity, data: DamageInteraction): Boolean = {
(data.adversarial match {
case Some(adversarial) => adversarial.attacker.Faction != adversarial.defender.Faction
case None => true
}) ||
(obj match {
case hobj: Hackable => hobj.HackedBy.nonEmpty
case _ => false
})
}
/**
@ -117,7 +118,7 @@ object Damageable {
* @return `true`, if the target can be affected;
* `false`, otherwise
*/
def CanDamageOrJammer(obj: Vitality with FactionAffinity, damage: Int, data: ResolvedProjectile): Boolean = {
def CanDamageOrJammer(obj: Vitality with FactionAffinity, damage: Int, data: DamageInteraction): Boolean = {
CanDamage(obj, damage, data) || CanJammer(obj, data)
}
@ -126,7 +127,7 @@ object Damageable {
* @param target the entity being damaged
* @param cause historical information about the damage
*/
def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
target.Destroyed = true
}
}

View file

@ -1,8 +1,8 @@
//Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.damage
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
/**
@ -13,7 +13,7 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
trait DamageableAmenity extends DamageableEntity {
def DamageableObject: Amenity
override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
DamageableAmenity.DestructionAwareness(target, cause)
target.ClearHistory()
@ -32,7 +32,7 @@ object DamageableAmenity {
* @param target the entity being destroyed
* @param cause historical information about the damage
*/
def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
val zone = target.Zone
val zoneId = zone.id
val events = zone.AvatarEvents

View file

@ -1,8 +1,8 @@
//Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.damage
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.resolution.ResolutionCalculations
import net.psforever.objects.zones.Zone
import net.psforever.types.PlanetSideGUID
@ -78,8 +78,8 @@ trait DamageableEntity extends Damageable {
* @return `true`, if damage resolution is to be evaluated;
* `false`, otherwise
*/
protected def WillAffectTarget(target: Damageable.Target, damage: Int, cause: ResolvedProjectile): Boolean = {
Damageable.CanDamageOrJammer(target, damage, cause)
protected def WillAffectTarget(target: Damageable.Target, damage: Int, cause: DamageResult): Boolean = {
Damageable.CanDamageOrJammer(target, damage, cause.interaction)
}
/**
@ -89,7 +89,7 @@ trait DamageableEntity extends Damageable {
* @param cause historical information about the damage
* @param damage the amount of damage
*/
protected def HandleDamage(target: Damageable.Target, cause: ResolvedProjectile, damage: Any): Unit = {
protected def HandleDamage(target: Damageable.Target, cause: DamageResult, damage: Any): Unit = {
if (!target.Destroyed && target.Health <= target.Definition.DamageDestroysAt) {
DestructionAwareness(target, cause)
} else {
@ -103,7 +103,7 @@ trait DamageableEntity extends Damageable {
* @param cause historical information about the damage
* @param amount the amount of damage
*/
protected def DamageAwareness(target: Damageable.Target, cause: ResolvedProjectile, amount: Any): Unit = {
protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any): Unit = {
amount match {
case value: Int =>
DamageableEntity.DamageAwareness(target, cause, value)
@ -117,13 +117,22 @@ trait DamageableEntity extends Damageable {
* @param target the entity being destroyed
* @param cause historical information about the damage
*/
protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
Damageable.DestructionAwareness(target, cause)
DamageableEntity.DestructionAwareness(target, cause)
}
}
object DamageableEntity {
def attributionTo(cause: DamageResult, zone: Zone, default: PlanetSideGUID = PlanetSideGUID(0)): PlanetSideGUID = {
(cause.adversarial match {
case Some(adversarial) => zone.LivePlayers.find { p => adversarial.attacker.Name.equals(p.Name) }
case None => None
}) match {
case Some(player) => player.GUID
case None => default
}
}
/**
* A damaged target dispatches messages to:
@ -143,16 +152,16 @@ object DamageableEntity {
* @param target the entity being damaged
* @param cause historical information about the damage
*/
def DamageAwareness(target: Damageable.Target, cause: ResolvedProjectile, amount: Int): Unit = {
if (Damageable.CanJammer(target, cause)) {
def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Int): Unit = {
if (Damageable.CanJammer(target, cause.interaction)) {
target.Actor ! JammableUnit.Jammered(cause)
}
if (DamageToHealth(target, cause, amount)) {
target.Zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos)
target.Zone.Activity ! Zone.HotSpot.Activity(cause)
}
}
def DamageToHealth(target: Damageable.Target, cause: ResolvedProjectile, amount: Int): Boolean = {
def DamageToHealth(target: Damageable.Target, cause: DamageResult, amount: Int): Boolean = {
if (amount > 0 && !target.Destroyed) {
val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage(
@ -180,7 +189,7 @@ object DamageableEntity {
* @param target the entity being destroyed
* @param cause historical information about the damage
*/
def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
//un-jam
target.Actor ! JammableUnit.ClearJammeredSound()
target.Actor ! JammableUnit.ClearJammeredStatus()
@ -188,10 +197,7 @@ object DamageableEntity {
val zone = target.Zone
val zoneId = zone.id
val tguid = target.GUID
val attribution = target.Zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match {
case Some(player) => player.GUID
case _ => PlanetSideGUID(0)
}
val attribution = attributionTo(cause, target.Zone)
zone.AvatarEvents ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 0, target.Health))
zone.AvatarEvents ! AvatarServiceMessage(
zoneId,

View file

@ -2,8 +2,9 @@
package net.psforever.objects.serverobject.damage
import net.psforever.objects.Player
import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile}
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.packet.game.DamageWithPositionMessage
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@ -30,7 +31,7 @@ object DamageableMountable {
*/
def DamageAwareness(
target: Damageable.Target with Mountable,
cause: ResolvedProjectile,
cause: DamageResult,
countableDamage: Int
): Unit = {
val zone = target.Zone
@ -39,8 +40,11 @@ object DamageableMountable {
case seat if seat.isOccupied && seat.Occupant.get.isAlive =>
seat.Occupant.get
}
(cause.projectile.owner match {
case pSource: PlayerSource => //player damage
((cause.adversarial match {
case Some(adversarial) => Some(adversarial.attacker)
case None => None
}) match {
case Some(pSource: PlayerSource) => //player damage
val name = pSource.Name
(zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match {
case Some(player) =>
@ -53,9 +57,11 @@ object DamageableMountable {
case msg =>
occupants.map { tplayer => (tplayer.Name, msg) }
}
case source => //object damage
case Some(source) => //object damage
val msg = AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(countableDamage, source.Position))
occupants.map { tplayer => (tplayer.Name, msg) }
case None =>
List.empty
}).foreach {
case (channel, msg) =>
events ! AvatarServiceMessage(channel, msg)
@ -70,15 +76,18 @@ object DamageableMountable {
* @param target the entity being destroyed
* @param cause historical information about the damage
*/
def DestructionAwareness(target: Damageable.Target with Mountable, cause: ResolvedProjectile): Unit = {
def DestructionAwareness(target: Damageable.Target with Mountable, cause: DamageResult): Unit = {
val interaction = cause.interaction
target.Seats.values
.filter(seat => {
seat.isOccupied && seat.Occupant.get.isAlive
})
.foreach(seat => {
val tplayer = seat.Occupant.get
tplayer.History(cause)
tplayer.Actor ! Player.Die()
//tplayer.History(cause)
tplayer.Actor ! Player.Die(
DamageInteraction(interaction.resolution, SourceEntry(tplayer), interaction.cause, interaction.hitPos)
)
})
}
}

View file

@ -3,10 +3,9 @@ package net.psforever.objects.serverobject.damage
import akka.actor.Actor
import net.psforever.objects.{Vehicle, Vehicles}
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.vital.DamageType
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.resolution.ResolutionCalculations
import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
@ -17,7 +16,7 @@ import net.psforever.types.Vector3
import scala.concurrent.duration._
/**
* The "control" `Actor` mixin for damage-handling code for `Vehicle` objects.
* The mixin for damage-handling code for `Vehicle` entities.
*/
trait DamageableVehicle
extends DamageableEntity
@ -95,7 +94,7 @@ trait DamageableVehicle
* @param cause historical information about the damage
* @param amount how much damage was performed
*/
override protected def DamageAwareness(target: Target, cause: ResolvedProjectile, amount: Any): Unit = {
override protected def DamageAwareness(target: Target, cause: DamageResult, amount: Any): Unit = {
val obj = DamageableObject
val zone = target.Zone
val events = zone.VehicleEvents
@ -112,16 +111,16 @@ trait DamageableVehicle
announceConfrontation = true
false
case _ =>
cause.projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated)
cause.interaction.cause.source.Aggravated.nonEmpty
}
reportDamageToVehicle = false
//log historical event
target.History(cause)
//damage
if (Damageable.CanDamageOrJammer(target, totalDamage, cause)) {
if (Damageable.CanDamageOrJammer(target, totalDamage, cause.interaction)) {
//jammering
if (Damageable.CanJammer(target, cause)) {
if (Damageable.CanJammer(target, cause.interaction)) {
target.Actor ! JammableUnit.Jammered(cause)
}
//stat changes
@ -151,7 +150,7 @@ trait DamageableVehicle
}
else {
//activity on map
zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos)
zone.Activity ! Zone.HotSpot.Activity(cause)
//alert to damage source
DamageableMountable.DamageAwareness(obj, cause, totalDamage)
}
@ -187,13 +186,14 @@ trait DamageableVehicle
* @param target the entity being destroyed
* @param cause historical information about the damage
*/
override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = {
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
val obj = DamageableObject
DamageableMountable.DestructionAwareness(obj, cause)
val zone = target.Zone
//aggravation cancel
EndAllAggravation()
//passengers die with us
DamageableMountable.DestructionAwareness(obj, cause)
//cargo vehicles die with us
obj.CargoHolds.values.foreach(hold => {
hold.Occupant match {
@ -202,6 +202,8 @@ trait DamageableVehicle
case None => ;
}
})
//things positioned around us can get hurt from us
Zone.causeExplosion(obj.Zone, obj, Some(cause))
//special considerations for certain vehicles
Vehicles.BeforeUnloadVehicle(obj, zone)
//shields
@ -212,6 +214,7 @@ trait DamageableVehicle
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 68, 0)
)
}
//clean up
target.Actor ! Vehicle.Deconstruct(Some(1 minute))
target.ClearHistory()
DamageableWeaponTurret.DestructionAwareness(obj, cause)
@ -224,12 +227,12 @@ object DamageableVehicle {
* Message for instructing the target's cargo vehicles about a damage source affecting their carrier.
* @param cause historical information about damage
*/
private case class Damage(cause: ResolvedProjectile, amount: Int)
private case class Damage(cause: DamageResult, amount: Int)
/**
* Message for instructing the target's cargo vehicles that their carrier is destroyed,
* and they should be destroyed too.
* @param cause historical information about damage
*/
private case class Destruction(cause: ResolvedProjectile)
private case class Destruction(cause: DamageResult)
}

View file

@ -2,11 +2,10 @@
package net.psforever.objects.serverobject.damage
import akka.actor.Actor
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.turret.{TurretUpgrade, WeaponTurret}
import net.psforever.objects.vehicles.MountedWeapons
import net.psforever.objects.vital.DamageType
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.DamageWithPositionMessage
import net.psforever.types.Vector3
@ -32,7 +31,7 @@ trait DamageableWeaponTurret
override val takesDamage: Receive = originalTakesDamage.orElse(aggravatedBehavior)
override protected def DamageAwareness(target: Damageable.Target, cause: ResolvedProjectile, amount: Any): Unit = {
override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any): Unit = {
val obj = DamageableObject
val zone = target.Zone
val events = zone.VehicleEvents
@ -48,15 +47,15 @@ trait DamageableWeaponTurret
announceConfrontation = true
false
case _ =>
cause.projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated)
cause.interaction.cause.source.Aggravated.nonEmpty
}
//log historical event
target.History(cause)
//damage
if (Damageable.CanDamageOrJammer(target, damageToHealth, cause)) {
if (Damageable.CanDamageOrJammer(target, damageToHealth, cause.interaction)) {
//jammering
if (Damageable.CanJammer(target, cause)) {
if (Damageable.CanJammer(target, cause.interaction)) {
target.Actor ! JammableUnit.Jammered(cause)
}
//stat changes
@ -81,19 +80,20 @@ trait DamageableWeaponTurret
}
else {
//activity on map
zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos)
zone.Activity ! Zone.HotSpot.Activity(cause)
//alert to damage source
DamageableMountable.DamageAwareness(obj, cause, damageToHealth)
}
}
}
override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
val obj = DamageableObject
EndAllAggravation()
DamageableWeaponTurret.DestructionAwareness(obj, cause)
DamageableMountable.DestructionAwareness(obj, cause)
Zone.causeExplosion(target.Zone, target, Some(cause))
}
}
@ -120,7 +120,7 @@ object DamageableWeaponTurret {
* but the handling code closely associates with the former
* @param cause historical information about the damage
*/
def DestructionAwareness(target: Damageable.Target with MountedWeapons, cause: ResolvedProjectile): Unit = {
def DestructionAwareness(target: Damageable.Target with MountedWeapons, cause: DamageResult): Unit = {
//wreckage has no (visible) mounted weapons
val zone = target.Zone
val zoneId = zone.id

View file

@ -4,15 +4,14 @@ package net.psforever.objects.serverobject.generator
import akka.actor.{Actor, Cancellable}
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.{Default, Player, Tool}
import net.psforever.objects.ballistics._
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.damage.DamageableEntity
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, Repairable, RepairableEntity}
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.vital.DamageFromExplosion
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.TriggerEffectMessage
import net.psforever.types.{PlanetSideGeneratorState, Vector3}
import net.psforever.types.PlanetSideGeneratorState
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@ -103,17 +102,8 @@ class GeneratorControl(gen: Generator)
queuedExplosion.cancel()
queuedExplosion = Default.Cancellable
imminentExplosion = false
//kill everyone within 14m
gen.Owner match {
case b: Building =>
val genDef = gen.Definition
b.PlayersInSOI.collect {
case player if player.isAlive && Vector3.DistanceSquared(player.Position, gen.Position) < 196 =>
player.History(DamageFromExplosion(PlayerSource(player), genDef))
player.Actor ! Player.Die()
}
case _ => ;
}
//hate on everything nearby
Zone.causeExplosion(gen.Zone, gen, gen.LastDamage)
gen.ClearHistory()
case GeneratorControl.Restored() =>
@ -154,12 +144,12 @@ class GeneratorControl(gen: Generator)
!imminentExplosion && super.CanPerformRepairs(obj, player, item)
}
override protected def WillAffectTarget(target: Target, damage: Int, cause: ResolvedProjectile): Boolean = {
override protected def WillAffectTarget(target: Target, damage: Int, cause: DamageResult): Boolean = {
//if an explosion is queued, disallow further damage
!imminentExplosion && super.WillAffectTarget(target, damage, cause)
}
override protected def DamageAwareness(target: Target, cause: ResolvedProjectile, amount: Any): Unit = {
override protected def DamageAwareness(target: Target, cause: DamageResult, amount: Any): Unit = {
tryAutoRepair()
super.DamageAwareness(target, cause, amount)
val damageTo = amount match {
@ -169,7 +159,7 @@ class GeneratorControl(gen: Generator)
GeneratorControl.DamageAwareness(gen, cause, damageTo)
}
override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = {
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
tryAutoRepair()
//if the target is already destroyed, do not let it be destroyed again
if (!target.Destroyed) {
@ -211,7 +201,7 @@ class GeneratorControl(gen: Generator)
if(!gen.Destroyed) {
GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Offline))
}
//can any explosion (see withoutNtu->GenweratorControl.Destabilized)
//quit any explosion (see withoutNtu->GeneratorControl.Destabilized)
if(!queuedExplosion.isCancelled) {
queuedExplosion.cancel()
self ! GeneratorControl.Destabilized()
@ -275,7 +265,7 @@ object GeneratorControl {
* @param cause historical information about the damage
* @param amount the amount of damage
*/
def DamageAwareness(target: Generator, cause: ResolvedProjectile, amount: Int): Unit = {
def DamageAwareness(target: Generator, cause: DamageResult, amount: Int): Unit = {
if (!target.Destroyed) {
val health: Float = target.Health.toFloat
val max: Float = target.MaxHealth.toFloat

View file

@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.implantmech
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.{GlobalDefinitions, Player, SimpleItem}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
@ -11,6 +10,7 @@ import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity,
import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior}
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableEntity}
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
/**
@ -83,7 +83,7 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech)
}
}
override protected def DamageAwareness(target: Target, cause: ResolvedProjectile, amount: Any): Unit = {
override protected def DamageAwareness(target: Target, cause: DamageResult, amount: Any): Unit = {
tryAutoRepair()
super.DamageAwareness(target, cause, amount)
val damageTo = amount match {
@ -93,7 +93,7 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech)
DamageableMountable.DamageAwareness(DamageableObject, cause, damageTo)
}
override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
DamageableMountable.DestructionAwareness(DamageableObject, cause)
target.ClearHistory()

View file

@ -1,12 +1,14 @@
package net.psforever.objects.serverobject.painbox
import akka.actor.Cancellable
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
import net.psforever.objects.{Default, GlobalDefinitions}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.etc.PainboxReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.{Default, GlobalDefinitions, Player}
import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
@ -15,87 +17,49 @@ class PainboxControl(painbox: Painbox) extends PoweredAmenityControl {
private[this] val log = org.log4s.getLogger(s"Painbox")
var painboxTick: Cancellable = Default.Cancellable
var nearestDoor: Option[Door] = None
var bBoxMinCorner = Vector3.Zero
var bBoxMaxCorner = Vector3.Zero
var bBoxMidPoint = Vector3.Zero
var disabled = false // Temporary to disable cavern non-radius fields
var domain: PainboxControl.Shape = PainboxControl.Unshaped()
var disabled = false
def initialStartup(): Unit = {
if (painbox.Definition.HasNearestDoorDependency) {
(painbox.Owner match {
case obj : Building =>
obj.Amenities
.collect { case door : Door => door }
.sortBy(door => Vector3.DistanceSquared(painbox.Position, door.Position))
.headOption
case _ =>
None
}) match {
case door@Some(_) =>
nearestDoor = door
case _ =>
log.error(
s"Painbox ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position} can not find a door that it is dependent on"
)
}
}
else {
if (painbox.Definition.Radius == 0f) {
if (painbox.Owner.Continent.matches("c[0-9]")) {
// todo: handle non-radius painboxes in caverns properly
log.warn(s"Skipping initialization of ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position}")
disabled = true
}
else {
painbox.Owner match {
case obj : Building =>
val planarRange = 16.5f
val aboveRange = 5
val belowRange = 5
// Find amenities within the specified range
val nearbyAmenities = obj.Amenities
.filter(amenity =>
amenity.Position != Vector3.Zero
&& (amenity.Definition == GlobalDefinitions.mb_locker
|| amenity.Definition == GlobalDefinitions.respawn_tube
|| amenity.Definition == GlobalDefinitions.spawn_terminal
|| amenity.Definition == GlobalDefinitions.order_terminal
|| amenity.Definition == GlobalDefinitions.door)
&& amenity.Position.x > painbox.Position.x - planarRange && amenity.Position.x < painbox.Position.x + planarRange
&& amenity.Position.y > painbox.Position.y - planarRange && amenity.Position.y < painbox.Position.y + planarRange
&& amenity.Position.z > painbox.Position.z - belowRange && amenity.Position.z < painbox.Position.z + aboveRange
)
// Calculate bounding box of amenities
bBoxMinCorner = Vector3(
nearbyAmenities.minBy(amenity => amenity.Position.x).Position.x,
nearbyAmenities.minBy(amenity => amenity.Position.y).Position.y,
nearbyAmenities.minBy(x => x.Position.z).Position.z
)
bBoxMaxCorner = Vector3(
nearbyAmenities.maxBy(amenity => amenity.Position.x).Position.x,
nearbyAmenities.maxBy(amenity => amenity.Position.y).Position.y,
painbox.Position.z
)
bBoxMidPoint = Vector3(
(bBoxMinCorner.x + bBoxMaxCorner.x) / 2,
(bBoxMinCorner.y + bBoxMaxCorner.y) / 2,
(bBoxMinCorner.z + bBoxMaxCorner.z) / 2
)
case _ => None
}
if (painbox.Owner.Continent.matches("c[0-9]")) {
//are we in a safe zone?
// todo: handle non-radius painboxes in caverns properly
log.warn(s"Skipping initialization of ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position}")
disabled = true
} else {
if (painbox.Definition.HasNearestDoorDependency) {
//whether an open door summons the pain
(painbox.Owner match {
case obj : Building =>
obj.Amenities
.collect { case door : Door => door }
.sortBy(door => Vector3.DistanceSquared(painbox.Position, door.Position))
.headOption
case _ =>
None
}) match {
case door@Some(_) =>
nearestDoor = door
case _ =>
log.error(
s"Painbox ${painbox.GUID} on ${painbox.Owner.Continent} - ${painbox.Position} can not find a door that it is dependent on"
)
disabled = true
}
}
}
if (!disabled) {
self ! BuildingActor.PowerOff()
//the region the painbox endangers
domain = painbox.Definition.innateDamage match {
case Some(properties) if properties.DamageRadius > 0f =>
PainboxControl.Spherical(painbox.Position + painbox.Definition.sphereOffset, properties.DamageRadius)
case _ =>
PainboxControl.Box(painbox)
}
}
}
var commonBehavior: Receive = {
case "startup" =>
if (bBoxMidPoint == Vector3.Zero) {
if (!disabled && domain.midpoint == Vector3.Zero) {
initialStartup()
}
@ -107,7 +71,7 @@ class PainboxControl(painbox: Painbox) extends PoweredAmenityControl {
def poweredStateLogic: Receive =
commonBehavior
.orElse {
case Painbox.Start() if isPowered =>
case Painbox.Start() if isPowered && !disabled =>
painboxTick.cancel()
painboxTick = context.system.scheduler.scheduleWithFixedDelay(0 seconds, 1 second, self, Painbox.Tick())
@ -115,53 +79,26 @@ class PainboxControl(painbox: Painbox) extends PoweredAmenityControl {
//todo: Account for overlapping pain fields
//todo: Pain module
//todo: REK boosting
val guid = painbox.GUID
val owner = painbox.Owner.asInstanceOf[Building]
val faction = owner.Faction
val faction = painbox.Faction
if (
isPowered && faction != PlanetSideEmpire.NEUTRAL && (nearestDoor match {
case Some(door) => door.Open.nonEmpty;
case _ => true
})
) {
val events = painbox.Zone.AvatarEvents
val damage = painbox.Definition.Damage
val radius = painbox.Definition.Radius * painbox.Definition.Radius
val position = painbox.Position
if (painbox.Definition.Radius != 0f) {
// Spherical pain field
owner.PlayersInSOI
.collect {
case p
if p.Faction != faction
&& p.Health > 0
&& Vector3.DistanceSquared(p.Position, position) < radius =>
events ! AvatarServiceMessage(p.Name, AvatarAction.EnvironmentalDamage(p.GUID, guid, damage))
val pain = PainboxReason(painbox)
domain
.filterTargets(
(painbox.Owner match {
case b: Building => b.PlayersInSOI
case _ => painbox.Zone.LivePlayers
}).filter { p =>
p.Faction != faction && p.Health > 0
}
} else {
// Bounding box pain field
owner.PlayersInSOI
.collect {
case p
if p.Faction != faction
&& p.Health > 0 =>
/*
This may be cpu intensive with a large number of players in SOI. Further performance tweaking may be required
The bounding box is calculated aligned to the world XY axis, instead of rotating the painbox corners to match the base rotation
we instead rotate the player's current coordinates to match the base rotation, which allows for much simplified checking of if the player is
within the bounding box
*/
val playerRot =
Vector3.PlanarRotateAroundPoint(p.Position, bBoxMidPoint, painbox.Owner.Orientation.z.toRadians)
if (
bBoxMinCorner.x <= playerRot.x && playerRot.x <= bBoxMaxCorner.x && bBoxMinCorner.y <= playerRot.y && playerRot.y <= bBoxMaxCorner.y
&& playerRot.z >= bBoxMinCorner.z && playerRot.z <= bBoxMaxCorner.z
) {
events ! AvatarServiceMessage(p.Name, AvatarAction.EnvironmentalDamage(p.GUID, guid, damage))
}
}
}
)
.foreach { p =>
p.Actor ! Vitality.Damage(DamageInteraction(SourceEntry(p), pain, p.Position).calculate())
}
}
case _ => ;
@ -179,9 +116,96 @@ class PainboxControl(painbox: Painbox) extends PoweredAmenityControl {
def powerTurnOnCallback(): Unit = {
painbox.Owner match {
case b: Building if b.PlayersInSOI.nonEmpty =>
case b: Building if b.PlayersInSOI.nonEmpty && !disabled =>
self ! Painbox.Start()
case _ => ;
}
}
}
object PainboxControl {
sealed trait Shape {
def midpoint: Vector3
def filterTargets(available : List[Player]): List[Player]
}
final case class Unshaped() extends Shape {
def midpoint: Vector3 = Vector3.Zero
def filterTargets(available: List[Player]) : List[Player] = Nil
}
final case class Passthrough(midpoint: Vector3) extends Shape {
def filterTargets(available: List[Player]) : List[Player] = available
}
final case class Spherical(midpoint: Vector3, radius: Float) extends Shape {
def filterTargets(available: List[Player]): List[Player] = {
available.filter { p =>
Vector3.DistanceSquared(p.Position, midpoint) < radius
}
}
}
final case class Box(painbox: Painbox) extends Shape {
private val (bBoxMinCorner, bBoxMaxCorner, bBoxMidPoint): (Vector3, Vector3, Vector3) = {
painbox.Owner match {
case obj : Building =>
val planarRange = 16.5f
val aboveRange = 5
val belowRange = 5
// Find amenities within the specified range
val nearbyAmenities = obj.Amenities
.filter(amenity =>
amenity.Position != Vector3.Zero
&& (amenity.Definition == GlobalDefinitions.mb_locker
|| amenity.Definition == GlobalDefinitions.respawn_tube
|| amenity.Definition == GlobalDefinitions.spawn_terminal
|| amenity.Definition == GlobalDefinitions.order_terminal
|| amenity.Definition == GlobalDefinitions.door)
&& amenity.Position.x > painbox.Position.x - planarRange && amenity.Position.x < painbox.Position.x + planarRange
&& amenity.Position.y > painbox.Position.y - planarRange && amenity.Position.y < painbox.Position.y + planarRange
&& amenity.Position.z > painbox.Position.z - belowRange && amenity.Position.z < painbox.Position.z + aboveRange
)
// Calculate bounding box of amenities
//0.5 is added/removed to ensure entirety of valid amenities were encompassed by field
val min = Vector3(
nearbyAmenities.minBy(_.Position.x).Position.x - 0.5f,
nearbyAmenities.minBy(_.Position.y).Position.y - 0.5f,
nearbyAmenities.minBy(_.Position.z).Position.z
)
val max = Vector3(
nearbyAmenities.maxBy(_.Position.x).Position.x + 0.5f,
nearbyAmenities.maxBy(_.Position.y).Position.y + 0.5f,
painbox.Position.z
)
(min, max, Vector3.midpoint(min, max))
case _ =>
(Vector3.Zero, Vector3.Zero, painbox.Position)
}
}
private val ownerRotZRadians = painbox.Owner.Orientation.z.toRadians
def midpoint: Vector3 = bBoxMidPoint
def filterTargets(available : List[Player]) : List[Player] = {
available.filter { p =>
/*
This may be cpu intensive with a large number of players in SOI. Further performance tweaking may be required
The bounding box is calculated aligned to the world XY axis, instead of rotating the painbox corners to match the base rotation
we instead rotate the player's current coordinates to match the base rotation,
allowing for much simplified checking of if the player is within the bounding box
*/
val playerRot = Vector3.PlanarRotateAroundPoint(
p.Position,
bBoxMidPoint,
ownerRotZRadians
)
bBoxMinCorner.x <= playerRot.x && playerRot.x <= bBoxMaxCorner.x &&
bBoxMinCorner.y <= playerRot.y && playerRot.y <= bBoxMaxCorner.y &&
bBoxMinCorner.z <= playerRot.z && playerRot.z <= bBoxMaxCorner.z
}
}
}
}

View file

@ -4,45 +4,11 @@ import net.psforever.objects.serverobject.structures.AmenityDefinition
import net.psforever.types.Vector3
class PainboxDefinition(objectId: Int) extends AmenityDefinition(objectId) {
private var alwaysOn: Boolean = true
private var radius: Float = 0f
private var damage: Int = 5
private var sphereOffset = Vector3(0f, 0f, -0.4f)
private var hasNearestDoorDependency = false
var alwaysOn: Boolean = true
var sphereOffset = Vector3.Zero
var hasNearestDoorDependency = false
objectId match {
case 622 =>
Name = "painbox"
alwaysOn = false
damage = 0
case 623 =>
Name = "painbox_continuous"
case 624 =>
Name = "painbox_door_radius"
alwaysOn = false
radius = 10f * 0.6928f
hasNearestDoorDependency = true
damage = 0
case 625 =>
Name = "painbox_door_radius_continuous"
radius = 10f * 0.6928f
hasNearestDoorDependency = true
case 626 =>
Name = "painbox_radius"
alwaysOn = false
radius = 10f * 0.6928f
damage = 0
case 627 =>
Name = "painbox_radius_continuous"
radius = 8.55f
sphereOffset = Vector3.Zero
case _ =>
throw new IllegalArgumentException(s"$objectId is not a valid painbox object id")
}
def Radius: Float = radius
def AlwaysOn: Boolean = alwaysOn
def Damage: Int = damage
def SphereOffset: Vector3 = sphereOffset
def HasNearestDoorDependency: Boolean = hasNearestDoorDependency
}

View file

@ -2,8 +2,10 @@
package net.psforever.objects.serverobject.structures
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.vital.{DamageResistanceModel, StandardResistanceProfile, Vitality}
import net.psforever.objects.zones.{Zone, ZoneAware}
import net.psforever.objects.vital.resistance.StandardResistanceProfile
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageAndResistance
import net.psforever.objects.zones.Zone
import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.objects.zones.{Zone => World}
@ -18,7 +20,10 @@ import net.psforever.objects.zones.{Zone => World}
* @see `AmenityOwner`
* @see `FactionAffinity`
*/
abstract class Amenity extends PlanetSideServerObject with Vitality with ZoneAware with StandardResistanceProfile {
abstract class Amenity
extends PlanetSideServerObject
with Vitality
with StandardResistanceProfile {
private[this] val log = org.log4s.getLogger("Amenity")
/** what other entity has authority over this amenity; usually either a building or a vehicle */
@ -76,7 +81,7 @@ abstract class Amenity extends PlanetSideServerObject with Vitality with ZoneAwa
LocationOffset
}
def DamageModel = Definition.asInstanceOf[DamageResistanceModel]
def DamageModel: DamageAndResistance = Definition.asInstanceOf[DamageAndResistance]
def Definition: AmenityDefinition
}

View file

@ -3,8 +3,9 @@ package net.psforever.objects.serverobject.structures
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.{DamageResistanceModel, StandardAmenityResistance, StandardResolutions, VitalityDefinition}
import net.psforever.objects.vital._
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel
final case class AutoRepairStats(amount: Int, start: Long, repeat: Long, drain: Float)
@ -16,7 +17,7 @@ abstract class AmenityDefinition(objectId: Int)
Name = "amenity"
DamageUsing = DamageCalculations.AgainstVehicle
ResistUsing = StandardAmenityResistance
Model = StandardResolutions.Amenities
Model = SimpleResolutions.calculate
var autoRepair: Option[AutoRepairStats] = None

View file

@ -2,7 +2,6 @@
package net.psforever.objects.serverobject.terminals
import akka.actor.ActorRef
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.{GlobalDefinitions, SimpleItem}
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
@ -11,6 +10,7 @@ import net.psforever.objects.serverobject.damage.{Damageable, DamageableAmenity}
import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior}
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity}
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
@ -67,12 +67,12 @@ class TerminalControl(term: Terminal)
case _ => ;
}
override protected def DamageAwareness(target : Target, cause : ResolvedProjectile, amount : Any) : Unit = {
override protected def DamageAwareness(target: Target, cause: DamageResult, amount: Any) : Unit = {
tryAutoRepair()
super.DamageAwareness(target, cause, amount)
}
override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile) : Unit = {
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult) : Unit = {
tryAutoRepair()
if (term.HackedBy.nonEmpty) {
val zone = term.Zone

View file

@ -2,12 +2,12 @@
package net.psforever.objects.serverobject.tube
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.damage.DamageableAmenity
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, Repairable, RepairableAmenity}
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
import net.psforever.objects.vital.interaction.DamageResult
/**
* An `Actor` that handles messages being dispatched to a specific `SpawnTube`.
@ -41,12 +41,12 @@ class SpawnTubeControl(tube: SpawnTube)
case _ => ;
}
override protected def DamageAwareness(target : Target, cause : ResolvedProjectile, amount : Any) : Unit = {
override protected def DamageAwareness(target : Target, cause : DamageResult, amount : Any) : Unit = {
tryAutoRepair()
super.DamageAwareness(target, cause, amount)
}
override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = {
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
tryAutoRepair()
super.DestructionAwareness(target, cause)
tube.Owner match {

View file

@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool}
import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons}
import net.psforever.objects.serverobject.CommonMessages
@ -11,6 +10,7 @@ import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTu
import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableWeaponTurret}
import net.psforever.objects.serverobject.structures.PoweredAmenityControl
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
@ -123,12 +123,12 @@ class FacilityTurretControl(turret: FacilityTurret)
case _ => ;
}
override protected def DamageAwareness(target : Damageable.Target, cause : ResolvedProjectile, amount : Any) : Unit = {
override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any) : Unit = {
tryAutoRepair()
super.DamageAwareness(target, cause, amount)
}
override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = {
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
tryAutoRepair()
super.DestructionAwareness(target, cause)
val zone = target.Zone

View file

@ -3,7 +3,7 @@ package net.psforever.objects.serverobject.turret
import net.psforever.objects.serverobject.structures.AmenityDefinition
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.{StandardResolutions, StandardVehicleResistance}
import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance}
/**
* The definition for any `FacilityTurret`.
@ -12,5 +12,5 @@ import net.psforever.objects.vital.{StandardResolutions, StandardVehicleResistan
class FacilityTurretDefinition(private val objectId: Int) extends AmenityDefinition(objectId) with TurretDefinition {
DamageUsing = DamageCalculations.AgainstVehicle
ResistUsing = StandardVehicleResistance
Model = StandardResolutions.FacilityTurrets
Model = SimpleResolutions.calculate
}

View file

@ -3,8 +3,8 @@ package net.psforever.objects.serverobject.turret
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition}
import net.psforever.objects.vehicles.Turrets
import net.psforever.objects.vital.DamageResistanceModel
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel
import scala.collection.mutable

View file

@ -3,7 +3,7 @@ package net.psforever.objects.vehicles
import akka.actor.{Actor, Cancellable}
import net.psforever.objects._
import net.psforever.objects.ballistics.{ResolvedProjectile, VehicleSource}
import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.ce.TelepadLike
import net.psforever.objects.equipment.{Equipment, EquipmentSlot, JammableMountedWeapons}
import net.psforever.objects.guid.GUIDTask
@ -19,6 +19,7 @@ import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.transfer.TransferBehavior
import net.psforever.objects.serverobject.repair.RepairableVehicle
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.VehicleShieldCharge
import net.psforever.objects.zones.Zone
import net.psforever.packet.game._
@ -156,7 +157,7 @@ class VehicleControl(vehicle: Vehicle)
}
case Vehicle.ChargeShields(amount) =>
val now: Long = System.nanoTime
val now: Long = System.currentTimeMillis()
//make certain vehicle doesn't charge shields too quickly
if (
vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields &&
@ -361,7 +362,7 @@ class VehicleControl(vehicle: Vehicle)
case _ =>
}
override def TryJammerEffectActivate(target: Any, cause: ResolvedProjectile): Unit = {
override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
if (vehicle.MountedIn.isEmpty) {
super.TryJammerEffectActivate(target, cause)
}
@ -601,8 +602,8 @@ object VehicleControl {
*/
def LastShieldChargeOrDamage(now: Long)(act: VitalsActivity): Boolean = {
act match {
case DamageFromProjectile(data) => now - data.hit_time < (5 seconds).toNanos //damage delays next charge by 5s
case vsc: VehicleShieldCharge => now - vsc.time < (1 seconds).toNanos //previous charge delays next by 1s
case DamageFromProjectile(data) => now - data.interaction.hitTime < (5 seconds).toMillis //damage delays next charge by 5s
case vsc: VehicleShieldCharge => now - vsc.time < (1 seconds).toMillis //previous charge delays next by 1s
case _ => false
}
}

View file

@ -1,13 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
/**
* An `Enumeration` of the damage type.
* These types are not necessarily representative of the kind of projectile being used;
* for example, the bolt driver's `bolt` is considered "splash."
*/
object DamageType extends Enumeration(1) {
type Type = Value
final val Direct, Splash, Lash, Radiation, Aggravated, Plasma, Comet, None = Value
}

View file

@ -1,100 +1,98 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
import net.psforever.objects.ballistics.{ObjectSource, PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.projectile.ProjectileCalculations
import net.psforever.objects.vital.resistance.{ResistanceCalculations, ResistanceSelection}
object NoResistance
extends ResistanceCalculations[SourceEntry](
ResistanceCalculations.ValidInfantryTarget,
extends ResistanceCalculations(
ResistanceCalculations.AlwaysValidTarget,
ResistanceCalculations.NoResistExtractor
)
object InfantryHitResistance
extends ResistanceCalculations[PlayerSource](
extends ResistanceCalculations(
ResistanceCalculations.ValidInfantryTarget,
ResistanceCalculations.ExoSuitDirectExtractor
)
object InfantrySplashResistance
extends ResistanceCalculations[PlayerSource](
extends ResistanceCalculations(
ResistanceCalculations.ValidInfantryTarget,
ResistanceCalculations.ExoSuitSplashExtractor
)
object InfantryLashResistance
extends ResistanceCalculations[PlayerSource](
extends ResistanceCalculations(
ResistanceCalculations.ValidInfantryTarget,
ResistanceCalculations.MaximumResistance
)
object InfantryAggravatedResistance
extends ResistanceCalculations[PlayerSource](
extends ResistanceCalculations(
ResistanceCalculations.ValidInfantryTarget,
ResistanceCalculations.ExoSuitAggravatedExtractor
)
object VehicleHitResistance
extends ResistanceCalculations[VehicleSource](
extends ResistanceCalculations(
ResistanceCalculations.ValidVehicleTarget,
ResistanceCalculations.VehicleDirectExtractor
)
object VehicleSplashResistance
extends ResistanceCalculations[VehicleSource](
extends ResistanceCalculations(
ResistanceCalculations.ValidVehicleTarget,
ResistanceCalculations.VehicleSplashExtractor
)
object VehicleLashResistance
extends ResistanceCalculations[VehicleSource](
extends ResistanceCalculations(
ResistanceCalculations.ValidVehicleTarget,
ResistanceCalculations.NoResistExtractor
)
object VehicleAggravatedResistance
extends ResistanceCalculations[VehicleSource](
extends ResistanceCalculations(
ResistanceCalculations.ValidVehicleTarget,
ResistanceCalculations.VehicleAggravatedExtractor
)
object AmenityHitResistance
extends ResistanceCalculations[ObjectSource](
extends ResistanceCalculations(
ResistanceCalculations.ValidAmenityTarget,
ResistanceCalculations.OtherDirectExtractor
)
object AmenitySplashResistance
extends ResistanceCalculations[ObjectSource](
extends ResistanceCalculations(
ResistanceCalculations.ValidAmenityTarget,
ResistanceCalculations.OtherSplashExtractor
)
object NoResistanceSelection extends ResistanceSelection {
def Direct: ProjectileCalculations.Form = None
def Splash: ProjectileCalculations.Form = None
def Lash: ProjectileCalculations.Form = None
def Aggravated: ProjectileCalculations.Form = None
def Direct: ResistanceSelection.Format = NoResistance.Calculate
def Splash: ResistanceSelection.Format = NoResistance.Calculate
def Lash: ResistanceSelection.Format = NoResistance.Calculate
def Aggravated: ResistanceSelection.Format = NoResistance.Calculate
}
object StandardInfantryResistance extends ResistanceSelection {
def Direct: ProjectileCalculations.Form = InfantryHitResistance.Calculate
def Splash: ProjectileCalculations.Form = InfantrySplashResistance.Calculate
def Lash: ProjectileCalculations.Form = InfantryLashResistance.Calculate
def Aggravated: ProjectileCalculations.Form = InfantryAggravatedResistance.Calculate
def Direct: ResistanceSelection.Format = InfantryHitResistance.Calculate
def Splash: ResistanceSelection.Format = InfantrySplashResistance.Calculate
def Lash: ResistanceSelection.Format = InfantryLashResistance.Calculate
def Aggravated: ResistanceSelection.Format = InfantryAggravatedResistance.Calculate
}
object StandardVehicleResistance extends ResistanceSelection {
def Direct: ProjectileCalculations.Form = VehicleHitResistance.Calculate
def Splash: ProjectileCalculations.Form = VehicleSplashResistance.Calculate
def Lash: ProjectileCalculations.Form = VehicleLashResistance.Calculate
def Aggravated: ProjectileCalculations.Form = VehicleAggravatedResistance.Calculate
def Direct: ResistanceSelection.Format = VehicleHitResistance.Calculate
def Splash: ResistanceSelection.Format = VehicleSplashResistance.Calculate
def Lash: ResistanceSelection.Format = VehicleLashResistance.Calculate
def Aggravated: ResistanceSelection.Format = VehicleAggravatedResistance.Calculate
}
object StandardAmenityResistance extends ResistanceSelection {
def Direct: ProjectileCalculations.Form = AmenityHitResistance.Calculate
def Splash: ProjectileCalculations.Form = AmenityHitResistance.Calculate
def Lash: ProjectileCalculations.Form = None
def Aggravated: ProjectileCalculations.Form = None
def Direct: ResistanceSelection.Format = AmenityHitResistance.Calculate
def Splash: ResistanceSelection.Format = AmenityHitResistance.Calculate
def Lash: ResistanceSelection.Format = ResistanceSelection.None
def Aggravated: ResistanceSelection.Format = ResistanceSelection.None
}

View file

@ -4,48 +4,43 @@ package net.psforever.objects.vital
import net.psforever.objects.vital.resolution._
object NoResolutions
extends DamageResistCalculations(
extends DamageResistanceCalculations(
ResolutionCalculations.NoDamage,
ResolutionCalculations.NoApplication
)
object AnyResolutions
extends DamageResistanceCalculations(
ResolutionCalculations.WildcardCalculations,
ResolutionCalculations.WildcardApplication
)
object InfantryResolutions
extends DamageResistCalculations(
extends DamageResistanceCalculations(
ResolutionCalculations.InfantryDamage,
ResolutionCalculations.InfantryApplication
)
object MaxResolutions
extends DamageResistCalculations(
extends DamageResistanceCalculations(
ResolutionCalculations.MaxDamage,
ResolutionCalculations.InfantryApplication
)
object VehicleResolutions
extends DamageResistCalculations(
extends DamageResistanceCalculations(
ResolutionCalculations.VehicleDamageAfterResist,
ResolutionCalculations.VehicleApplication
)
object SimpleResolutions
extends DamageResistCalculations(
extends DamageResistanceCalculations(
ResolutionCalculations.VehicleDamageAfterResist,
ResolutionCalculations.SimpleApplication
)
object ComplexDeployableResolutions
extends DamageResistCalculations(
extends DamageResistanceCalculations(
ResolutionCalculations.VehicleDamageAfterResist,
ResolutionCalculations.ComplexDeployableApplication
)
object StandardResolutions extends ResolutionSelection {
def Infantry: ResolutionCalculations.Form = InfantryResolutions.Calculate
def Max: ResolutionCalculations.Form = MaxResolutions.Calculate
def Vehicle: ResolutionCalculations.Form = VehicleResolutions.Calculate
def Aircraft: ResolutionCalculations.Form = VehicleResolutions.Calculate
def SimpleDeployables: ResolutionCalculations.Form = SimpleResolutions.Calculate
def ComplexDeployables: ResolutionCalculations.Form = ComplexDeployableResolutions.Calculate
def FacilityTurrets: ResolutionCalculations.Form = SimpleResolutions.Calculate
def Amenities: ResolutionCalculations.Form = SimpleResolutions.Calculate
}

View file

@ -1,8 +1,8 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.vital.resolution.ResolutionCalculations
import net.psforever.objects.vital.resolution.{DamageAndResistance, ResolutionCalculations}
import net.psforever.objects.vital.interaction.DamageResult
/**
* A vital object can be hurt or damaged or healed or repaired (HDHR).
@ -40,7 +40,7 @@ trait Vitality extends VitalsHistory {
Definition.Repairable && Health < MaxHealth && (Health > 0 || Definition.RepairIfDestroyed)
}
def DamageModel: DamageResistanceModel
def DamageModel: DamageAndResistance
def Definition: VitalityDefinition
}
@ -61,5 +61,5 @@ object Vitality {
* Report that a vitals object must be updated due to damage.
* @param obj the vital object
*/
final case class DamageResolution(obj: Vitality, cause: ResolvedProjectile)
final case class DamageResolution(obj: Vitality, cause: DamageResult)
}

View file

@ -1,6 +1,8 @@
//Copyright (c) 2020 PSForever
package net.psforever.objects.vital
import net.psforever.objects.vital.damage.DamageModifiers
import net.psforever.objects.vital.base.DamageModifiers
import net.psforever.objects.vital.prop.DamageWithPosition
/**
* na<br>
@ -148,4 +150,22 @@ trait VitalityDefinition extends DamageModifiers {
repairMod = mod
RepairMod
}
/**
* Characteristics of the objects dealing area of effect damage
* under guidance of the server motivated by client actions.
* Although "vitality" has nothing to do with explosions directly,
* exploding objects tend to be entities with `Vitality` (lowest common denominator inheritance).
*/
var explodes: Boolean = false
/**
* damage that is inherent to the object, used for explosions and collisions, mainly
*/
var innateDamage: Option[DamageWithPosition] = None
def innateDamage_=(combustion: DamageWithPosition): Option[DamageWithPosition] = {
innateDamage = Some(combustion)
innateDamage
}
}

View file

@ -1,15 +1,17 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital
import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile, SourceEntry, VehicleSource}
import net.psforever.objects.ballistics._
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ObjectDefinition}
import net.psforever.objects.serverobject.painbox.Painbox
import net.psforever.objects.serverobject.terminals.TerminalDefinition
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason}
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.types.{ExoSuitType, ImplantType}
abstract class VitalsActivity(target: SourceEntry) {
def Target: SourceEntry = target
val t: Long = System.nanoTime //???
val t: Long = System.currentTimeMillis() //???
def time: Long = t
}
@ -50,14 +52,16 @@ final case class RepairFromTerm(target: VehicleSource, amount: Int, term_def: Te
final case class VehicleShieldCharge(target: VehicleSource, amount: Int) extends HealingActivity(target) //TODO facility
final case class DamageFromProjectile(data: ResolvedProjectile) extends DamagingActivity(data.target)
final case class DamageFromProjectile(data: DamageResult) extends DamagingActivity(data.targetBefore)
final case class DamageFromPainbox(target: PlayerSource, painbox: Painbox, damage: Int) extends DamagingActivity(target)
final case class DamageFromPainbox(data: DamageResult) extends DamagingActivity(data.targetBefore)
final case class PlayerSuicide(target: PlayerSource) extends DamagingActivity(target)
final case class DamageFromExplosion(target: PlayerSource, cause: ObjectDefinition) extends DamagingActivity(target)
final case class DamageFromExplodingEntity(data: DamageResult) extends DamagingActivity(data.targetBefore)
/**
* A vital object can be hurt or damaged or healed or repaired (HDHR).
* A history of the previous changes in vital statistics of the underlying object is recorded
@ -68,6 +72,8 @@ trait VitalsHistory {
/** a reverse-order list of chronological events that have occurred to these vital statistics */
private var vitalsHistory: List[VitalsActivity] = List.empty[VitalsActivity]
private var lastDamage: Option[DamageResult] = None
def History: List[VitalsActivity] = vitalsHistory
/**
@ -94,20 +100,32 @@ trait VitalsHistory {
}
/**
* Very common example of a `VitalsActivity` event involving weapon discharge.
* @param projectile the fully-informed entry of discharge of a weapon
* Very common example of a `VitalsActivity` event involving damage.
* @param result the fully-informed entry
* @return the list of previous changes to this object's vital statistics
*/
def History(projectile: ResolvedProjectile): List[VitalsActivity] = {
vitalsHistory = DamageFromProjectile(projectile) +: vitalsHistory
def History(result: DamageResult): List[VitalsActivity] = {
result.interaction.cause match {
case _: ProjectileReason =>
vitalsHistory = DamageFromProjectile(result) +: vitalsHistory
lastDamage = Some(result)
case _: ExplodingEntityReason =>
vitalsHistory = DamageFromExplodingEntity(result) +: vitalsHistory
lastDamage = Some(result)
case _: PainboxReason =>
vitalsHistory = DamageFromPainbox(result) +: vitalsHistory
case _ => ;
}
vitalsHistory
}
def LastDamage: Option[DamageResult] = lastDamage
/**
* Find, specifically, the last instance of a weapon discharge vital statistics change.
* @return information about the discharge
*/
def LastShot: Option[ResolvedProjectile] = {
def LastShot: Option[DamageResult] = {
vitalsHistory.find({ p => p.isInstanceOf[DamageFromProjectile] }) match {
case Some(entry: DamageFromProjectile) =>
Some(entry.data)

View file

@ -0,0 +1,41 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.base
import net.psforever.objects.vital.interaction.DamageInteraction
/**
* Adjustments performed on the subsequent manipulations of the "base damage" value of an attack vector
* (like a projectile).<br>
* <br>
* Unlike static damage modifications which are structured like other `DamageProfiles`
* and offer purely additive or subtractive effects on the base damage,
* these modifiers should focus on unstructured, scaled manipulation of the value.
* The most common modifiers change the damage value based on distance between two points, called "degrading".
* The list of modifiers must be allocated in a single attempt, overriding previously-set modifiers.
* @see `DamageCalculations.WithModifiers`
* @see `DamageModifiers.Mod`
* @see `DamageProfile`
*/
trait DamageModifiers {
private var mods: List[DamageModifiers.Mod] = Nil
def Modifiers: List[DamageModifiers.Mod] = mods
def Modifiers_=(modifier: DamageModifiers.Mod): List[DamageModifiers.Mod] = Modifiers_=(List(modifier))
def Modifiers_=(modifiers: List[DamageModifiers.Mod]): List[DamageModifiers.Mod] = {
mods = modifiers
Modifiers
}
}
object DamageModifiers {
trait Mod {
/** Perform the underlying calculations, returning a modified value from the input value. */
final def calculate(damage : Int, data : DamageInteraction) : Int = {
calculate(damage, data, data.cause)
}
def calculate(damage : Int, data : DamageInteraction, cause : DamageReason) : Int
}
}

View file

@ -0,0 +1,83 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.base
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.vital.resolution.{DamageAndResistance, ResolutionCalculations}
/**
* A wrapper for ambiguity of the "damage source" in damage calculations.
* The base reason does not convey any specific requirements in regards to the interaction being described.
*/
trait DamageReason {
/**
* An indication about how the damage was or will be processed.
*/
def resolution: DamageResolution.Value
/**
* A direct connection to the damage information, numbers and properties.
*/
def source: DamageProperties
/**
* Determine whether two damage sources are equivalent.
* @param test the damage source to compare against
* @return `true`, if equivalent;
* `false`, otherwise
*/
def same(test: DamageReason): Boolean
/**
* The functionality that is necessary for interaction of a vital game object with the rest of the hostile game world.
*/
def damageModel: DamageAndResistance
/**
* Modifiers to the raw/modified damage value that are additive in nature.
* These modifiers use a selector function to extract the damage value from the profile,
* a process required to acquire the raw damage value, outlined elsewhere.
* @return a list of modifications to apply (in order)
*/
def staticModifiers: List[DamageProfile] = Nil
/**
* Modifiers to the raw/modified damage value that are multiplicative or provide disjoint modification.
* @return a list of modifications to apply (in order)
*/
def unstructuredModifiers: List[DamageModifiers.Mod] = Nil
/**
* The person to be blamed for this.
*/
def adversary: Option[SourceEntry]
/**
* Specifics about the method of damage, expected as an object class's unique identifier.
* @return defaults to 0
*/
def attribution: Int = 0
/**
* Perform the modified damage value and the basic resistance value allocations
* to be used against a given valid target.
* @param data the damaging interaction to be evaluated
* @return an application function that takes a target and returns a result
*/
def calculate(data: DamageInteraction): ResolutionCalculations.Output = {
damageModel.calculate(data)
}
/**
* Perform the modified damage value and the basic resistance value allocations
* to be used against a given valid target.
* @param data the damaging interaction to be evaluated
* @param dtype custom damage property for resistance allocation
* @return an application function that takes a target and returns a result
*/
def calculate(data: DamageInteraction, dtype: DamageType.Value): ResolutionCalculations.Output = {
damageModel.calculate(data, dtype)
}
}

View file

@ -1,12 +1,11 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
package net.psforever.objects.vital.base
/**
* An `Enumeration` of outcomes regarding what actually happened to the projectile,
* An `Enumeration` of outcomes regarding what actually happened to the damage,
* complementing normal damage type distinction in directing damage calculations.<br>
* <br>
* Although the latter states reflect what sort of damage the projectile might perform - `Hit`, `Splash`, etc. -
* the state is more a communication about how that damage is interpreted by the server.
* Although some of the earlier states reflect what sort of damage might perform - `Hit`, `Splash`, etc. -
* this state is more a communication about how that damage is interpreted by the server.
* For example, some projectiles:
* perform `Direct` damage, are reported by `HitMessage` packets, and resolve as `Hit`;
* or, perform `Direct` damage, are reported by `LashDamage` packets, and resolve as `Lash`.
@ -15,19 +14,20 @@ package net.psforever.objects.ballistics
* or, perform `Aggravated` damage, are reported by `SplashHitMessage` packets
* and resolve either as `AggravatedDirect` or as `AggravatedSplash`.
*/
object ProjectileResolution extends Enumeration {
object DamageResolution extends Enumeration {
type Type = Value
val
Unresolved, //original basic non-resolution
MissedShot, //projectile did not encounter any collision object and was despawned
Resolved, //a general "projectile encountered something" status with a more specific resolution
Missed, //did not interact with anything and was neutralized
Resolved, //a general "interacted with something" status, begging for a more specific resolution
Hit, //direct hit, one target
Splash, //area of effect damage, potentially multiple targets
Lash, //lashing damage, potentially multiple targets
AggravatedDirect, //direct hit aggravated damage
AggravatedDirectBurn, //continuous direct hit aggravated damage
AggravatedSplash, //splashed aggravated damage
AggravatedSplashBurn //continuous splashed aggravated damage
AggravatedSplashBurn, //continuous splashed aggravated damage
Explosion //area of effect damage caused by an internal mechanism; unrelated to Splash
= Value
}

View file

@ -0,0 +1,14 @@
package net.psforever.objects.vital.base
/**
* An `Enumeration` of the damage types
* not only distinguishing damage being inflicted
* but, more importantly, what kind of resistance is brought to bare against that damage.
* For additional types exclusive to aggravation, refer to `Aura`.
*/
object DamageType extends Enumeration(1) {
type Type = Value
//"one" (numerical 1 in the ADB) corresponds to objects that explode
final val Direct, Splash, Lash, Radiation, Aggravated, One, None = Value
}

View file

@ -0,0 +1,38 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.collision
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.vital.resolution.DamageAndResistance
/**
* A wrapper for a "damage source" in damage calculations
* that parameterizes information necessary to explain a collision.
* Being "adversarial" requires that the damage be performed as an aggressive action between individuals.
* @param source na
*/
final case class AdversarialCollisionReason(source: DamageProperties) extends DamageReason {
def resolution: DamageResolution.Value = DamageResolution.Unresolved
def same(test: DamageReason): Boolean = false
def damageModel: DamageAndResistance = null
override def adversary : Option[SourceEntry] = None
}
/**
* A wrapper for a "damage source" in damage calculations
* that parameterizes information necessary to explain a collision.
* @param source na
*/
final case class CollisionReason(source: DamageProperties) extends DamageReason {
def resolution: DamageResolution.Value = DamageResolution.Unresolved
def same(test: DamageReason): Boolean = false
def damageModel: DamageAndResistance = null
override def adversary : Option[SourceEntry] = None
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.damage
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.vital.interaction.DamageInteraction
/**
* A series of methods for extraction of the base damage against a given target type
@ -11,41 +11,48 @@ object DamageCalculations {
type Selector = DamageProfile => Int
//raw damage selectors
def AgainstNothing(profile: DamageProfile): Int = 0
def AgainstNothing(profile : DamageProfile) : Int = 0
def AgainstExoSuit(profile: DamageProfile): Int = profile.Damage0
def AgainstExoSuit(profile : DamageProfile) : Int = profile.Damage0
def AgainstVehicle(profile: DamageProfile): Int = profile.Damage1
def AgainstVehicle(profile : DamageProfile) : Int = profile.Damage1
def AgainstAircraft(profile: DamageProfile): Int = profile.Damage2
def AgainstAircraft(profile : DamageProfile) : Int = profile.Damage2
def AgainstMaxSuit(profile: DamageProfile): Int = profile.Damage3
def AgainstMaxSuit(profile : DamageProfile) : Int = profile.Damage3
def AgainstBFR(profile: DamageProfile): Int = profile.Damage4
def AgainstBFR(profile : DamageProfile) : Int = profile.Damage4
/**
* Get damage information from a series of profiles related to the weapon discharge.
* Get the damage value.
* @param selector the function that recovers the damage value
* @param data na
* @return the accumulated damage value
* @param data na
* @return the raw damage value
*/
def DamageWithModifiers(selector: DamageProfile => Int, data: ResolvedProjectile): Int = {
val projectile = data.projectile
val profile = projectile.profile
val fireMode = projectile.fire_mode
//static (additive and subtractive) modifiers
val staticModifiers = if (profile.UseDamage1Subtract) {
List(fireMode.Add, data.target.Modifiers.Subtract)
} else {
List(fireMode.Add)
}
def Raw(selector: DamageProfile => Int, data: DamageInteraction) : Int = {
selector(data.cause.source)
}
/**
* Get the damage value after it has been modified by context-related operations.
* Used as the default modifier function for `DamageResistanceCalculations`.
* @param selector the function that recovers the damage value
* @param data the interaction being processed
* @return the accumulated damage value
*/
def WithModifiers(selector: DamageProfile => Int, data: DamageInteraction) : Int = {
val cause = data.cause
val source = cause.source
val target = data.target
//base damage + static modifiers
var damage = selector(profile) + staticModifiers.foldLeft(0)(_ + selector(_))
//unstructured modifiers (the order is intentional, however)
(fireMode.Modifiers ++
profile.Modifiers ++
data.target.Definition.Modifiers)
.foreach { mod => damage = mod.Calculate(damage, data) }
val staticModifiers = cause.staticModifiers ++
(if (source.UseDamage1Subtract) List(target.Modifiers.Subtract) else Nil)
//unstructured modifiers (their ordering is intentional)
val unstructuredModifiers = cause.unstructuredModifiers ++
source.Modifiers ++ target.Definition.Modifiers
//apply
var damage = selector(source) + staticModifiers.foldLeft(0)(_ + selector(_))
unstructuredModifiers.foreach { mod => damage = mod.calculate(damage, data) }
damage
}
}

View file

@ -0,0 +1,72 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.damage
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.ballistics._
import net.psforever.objects.vital.base.{DamageModifiers, DamageReason}
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.prop.DamageWithPosition
import net.psforever.types.Vector3
/** The input value is the same as the output value. */
case object SameHit extends DamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: DamageReason): Int = damage
}
/**
* The input value degrades (lessens)
* the further the distance between the point of origin (target position)
* and the point of encounter (`hitPos`) of its vector.
* If the value is encountered beyond its maximum radial distance, the value is zero'd.
*/
case object RadialDegrade extends DamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: DamageReason): Int =
DamageModifierFunctions.radialDegradeFunction(damage, data, cause)
}
/**
* The input value degrades (lessens)
* to the percentage of its original value
* if the target is a vehicle with no shields.
* Specifically used for the `galaxy_gunship`.
*/
final case class GalaxyGunshipReduction(multiplier: Float) extends DamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: DamageReason): Int = {
data.target match {
case v: VehicleSource
if v.Definition == GlobalDefinitions.galaxy_gunship && v.Shields == 0 =>
(damage * multiplier).toInt
case _ =>
damage
}
}
}
object DamageModifierFunctions {
/**
* The input value degrades (lessens)
* the further the distance between the point of origin (target position)
* and the point of encounter (`hitPos`) of its vector (projectile).
* If the value is encountered beyond its maximum radial distance, the value is zero'd.
*/
def radialDegradeFunction(damage: Int, data: DamageInteraction, cause: DamageReason): Int = {
cause.source match {
case withPosition: DamageWithPosition =>
val distance = Vector3.Distance(data.hitPos, data.target.Position)
val radius = withPosition.DamageRadius
val radiusMin = withPosition.DamageRadiusMin
if (distance <= radiusMin) {
damage
} else if (distance <= radius) {
//damage - (damage * profile.DamageAtEdge * (distance - radiusMin) / (radius - radiusMin)).toInt
val base = withPosition.DamageAtEdge
val radi = radius - radiusMin
(damage * ((1 - base) * ((radi - (distance - radiusMin)) / radi) + base)).toInt
} else {
0
}
case _ =>
damage
}
}
}

View file

@ -1,545 +0,0 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.damage
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.ballistics._
import net.psforever.objects.equipment.ChargeFireModeDefinition
import net.psforever.objects.vital.DamageType
import net.psforever.types.{ExoSuitType, Vector3}
/**
* Adjustments performed on the subsequent manipulations of the "base damage" value of an attack vector
* (like a projectile).<br>
* <br>
* Unlike static damage modifications which are structured like other `DamageProfiles`
* and offer purely additive or subtractive effects on the base damage,
* these modifiers should focus on unstructured, scaled manipulation of the value.
* The most common modifiers change the damage value based on distance between two points, called "degrading".
* The list of modifiers must be allocated in a single attempt, overriding previously-set modifiers.
* @see `DamageCalculations.DamageWithModifiers`
* @see `DamageProfile`
* @see `ResolvedProjectile`
*/
trait DamageModifiers {
private var mods: List[DamageModifiers.Mod] = Nil
def Modifiers: List[DamageModifiers.Mod] = mods
def Modifiers_=(modifier: DamageModifiers.Mod): List[DamageModifiers.Mod] = Modifiers_=(List(modifier))
def Modifiers_=(modifiers: List[DamageModifiers.Mod]): List[DamageModifiers.Mod] = {
mods = modifiers
Modifiers
}
}
object DamageModifiers {
type Format = (Int, ResolvedProjectile) => Int
trait Mod {
/** Perform the underlying calculations, returning a modified value from the input value. */
def Calculate: DamageModifiers.Format
}
/** The input value is the same as the output value. */
case object SameHit extends Mod {
def Calculate: DamageModifiers.Format = function
private def function(damage: Int, data: ResolvedProjectile): Int = damage
}
/** If the calculated distance is greater than the maximum distance of the projectile, damage is zero'd. */
case object MaxDistanceCutoff extends Mod {
def Calculate: DamageModifiers.Format = function
private def function(damage: Int, data: ResolvedProjectile): Int = {
val projectile = data.projectile
val profile = projectile.profile
val distance = Vector3.Distance(data.hit_pos, projectile.shot_origin)
if (distance <= profile.DistanceMax) {
damage
} else {
0
}
}
}
/** If the calculated distance is greater than a custom distance, damage is zero'd. */
case class CustomDistanceCutoff(cutoff: Float) extends Mod {
def Calculate: DamageModifiers.Format = function
private def function(damage: Int, data: ResolvedProjectile): Int = {
val projectile = data.projectile
val distance = Vector3.Distance(data.hit_pos, projectile.shot_origin)
if (distance <= cutoff) {
damage
} else {
0
}
}
}
/**
* The input value degrades (lessens)
* the further the distance between the point of origin (`shot_origin`)
* and the point of encounter (`hit_pos`) of its vector (projectile).
* If the value is not set to degrade over any distance within its maximum distance, the value goes unmodified.
* If the value is encountered beyond its maximum distance, the value is zero'd.
*/
case object DistanceDegrade extends Mod {
def Calculate: DamageModifiers.Format = distanceDegradeFunction
}
/**
* The input value degrades (lessens)
* the further the distance between the point of origin (target position)
* and the point of encounter (`hit_pos`) of its vector (projectile).
* If the value is encountered beyond its maximum radial distance, the value is zero'd.
*/
case object RadialDegrade extends Mod {
def Calculate: DamageModifiers.Format = radialDegradeFunction
}
/**
* Lashing is the property of a projectile affecting nearby targets without coming into direct contact with them.
* The effect only activates after 5m from the point of origin (`shot_origin`) before the maximum distance.
* If lashing does not apply, the value goes unmodified.
* If lashing is valid but the value is encountered beyond its maximum radial distance, the value is zero'd.
*/
case object Lash extends Mod {
def Calculate: DamageModifiers.Format = function
private def function(damage: Int, data: ResolvedProjectile): Int = {
if (data.resolution == ProjectileResolution.Lash) {
val distance = Vector3.Distance(data.hit_pos, data.projectile.shot_origin)
if (distance > 5 && distance <= data.projectile.profile.DistanceMax) {
(damage * 0.2f) toInt
} else {
0
}
} else {
damage
}
}
}
/*
Aggravated damage.
For the most part, these calculations are individualistic and arbitrary.
They exist in their current form to satisfy observed shots to kill (STK) of specific weapon systems
according to 2012 standards of the Youtube video series by TheLegendaryNarwhal.
*/
/**
* The initial application of aggravated damage against an infantry target
* where the specific damage component is `Direct`.
*/
case object InfantryAggravatedDirect extends Mod {
def Calculate: DamageModifiers.Format =
baseAggravatedFormula(ProjectileResolution.AggravatedDirect, DamageType.Direct)
}
/**
* The initial application of aggravated damage against an infantry target
* where the specific damage component is `Splash`.
*/
case object InfantryAggravatedSplash extends Mod {
def Calculate: DamageModifiers.Format =
baseAggravatedFormula(ProjectileResolution.AggravatedSplash, DamageType.Splash)
}
/**
* The ongoing application of aggravated damage ticks against an infantry target
* where the specific damage component is `Direct`.
* This is called "burning" regardless of what the active aura effect actually is.
*/
case object InfantryAggravatedDirectBurn extends Mod {
def Calculate: DamageModifiers.Format =
baseAggravatedBurnFormula(ProjectileResolution.AggravatedDirectBurn, DamageType.Direct)
}
/**
* The ongoing application of aggravated damage ticks against an infantry target
* where the specific damage component is `Splash`.
* This is called "burning" regardless of what the active aura effect actually is.
*/
case object InfantryAggravatedSplashBurn extends Mod {
def Calculate: DamageModifiers.Format =
baseAggravatedBurnFormula(ProjectileResolution.AggravatedSplashBurn, DamageType.Splash)
}
/**
* For damage application that involves aggravation of a fireball (Dragon secondary fire mode),
* perform 1 damage.
* @see `ResolvedProjectile`
*/
case object FireballAggravatedBurn extends Mod {
def Calculate: DamageModifiers.Format = formula
private def formula(damage: Int, data: ResolvedProjectile): Int = {
if (damage > 0 &&
(data.resolution == ProjectileResolution.AggravatedDirectBurn ||
data.resolution == ProjectileResolution.AggravatedSplashBurn)) {
//add resist to offset resist subtraction later
1 + data.damage_model.ResistUsing(data)(data)
} else {
damage
}
}
}
/**
* The initial application of aggravated damage against an aircraft target.
* Primarily for use in the starfire weapon system.
* @see `AggravatedDamage`
* @see `ProjectileQuality.AggravatesTarget`
* @see `ResolvedProjectile`
*/
case object StarfireAggravated extends Mod {
def Calculate: DamageModifiers.Format = formula
private def formula(damage: Int, data: ResolvedProjectile): Int = {
if (data.resolution == ProjectileResolution.AggravatedDirect &&
data.projectile.quality == ProjectileQuality.AggravatesTarget) {
data.projectile.profile.Aggravated match {
case Some(aggravation) =>
aggravation.info.find(_.damage_type == DamageType.Direct) match {
case Some(infos) =>
(damage * infos.degradation_percentage + damage) toInt
case _ =>
damage
}
case _ =>
damage
}
} else {
damage
}
}
}
/**
* The ongoing application of aggravated damage ticks against an aircraft target.
* Primarily for use in the starfire weapon system.
* This is called "burning" regardless of what the active aura effect actually is.
* @see `AggravatedDamage`
* @see `ProjectileQuality`
* @see `ResolvedProjectile`
*/
case object StarfireAggravatedBurn extends Mod {
def Calculate: DamageModifiers.Format = formula
private def formula(damage: Int, data: ResolvedProjectile): Int = {
if (data.resolution == ProjectileResolution.AggravatedDirectBurn) {
data.projectile.profile.Aggravated match {
case Some(aggravation) =>
aggravation.info.find(_.damage_type == DamageType.Direct) match {
case Some(infos) =>
(math.floor(damage * infos.degradation_percentage) * data.projectile.quality.mod) toInt
case _ =>
damage
}
case _ =>
0
}
} else {
damage
}
}
}
/**
* The initial application of aggravated damage against a target.
* Primarily for use in the comet weapon system.
* @see `AggravatedDamage`
* @see `ProjectileQuality.AggravatesTarget`
* @see `ResolvedProjectile`
*/
case object CometAggravated extends Mod {
def Calculate: DamageModifiers.Format = formula
private def formula(damage: Int, data: ResolvedProjectile): Int = {
if (data.resolution == ProjectileResolution.AggravatedDirect &&
data.projectile.quality == ProjectileQuality.AggravatesTarget) {
data.projectile.profile.Aggravated match {
case Some(aggravation) =>
aggravation.info.find(_.damage_type == DamageType.Direct) match {
case Some(infos) =>
damage - (damage * infos.degradation_percentage) toInt
case _ =>
damage
}
case _ =>
damage
}
} else {
damage
}
}
}
/**
* The ongoing application of aggravated damage ticks against a target.
* Primarily for use in the comet weapon system.
* This is called "burning" regardless of what the active aura effect actually is.
* @see `AggravatedDamage`
* @see `ProjectileQuality`
* @see `ResolvedProjectile`
*/
case object CometAggravatedBurn extends Mod {
def Calculate: DamageModifiers.Format = formula
private def formula(damage: Int, data: ResolvedProjectile): Int = {
if (data.resolution == ProjectileResolution.AggravatedDirectBurn) {
data.projectile.profile.Aggravated match {
case Some(aggravation) =>
aggravation.info.find(_.damage_type == DamageType.Direct) match {
case Some(infos) =>
damage - (damage * infos.degradation_percentage) toInt
case _ =>
damage
}
case _ =>
0
}
} else {
damage
}
}
}
/**
* If the projectile has charging properties,
* and the weapon that produced the projectile has charging mechanics,
* calculate the current value of the damage as a sum
* of some minimum damage and scaled normal damage.
* The projectile quality has information about the "factor" of damage scaling.
* @see `ChargeDamage`
* @see `ChargeFireModeDefinition`
* @see `ProjectileQuality`
* @see `ResolvedProjectile`
*/
case object SpikerChargeDamage extends Mod {
def Calculate: DamageModifiers.Format = formula
private def formula(damage: Int, data: ResolvedProjectile): Int = {
val projectile = data.projectile
(projectile.fire_mode, projectile.profile.Charging) match {
case (_: ChargeFireModeDefinition, Some(info: ChargeDamage)) =>
val chargeQuality = math.max(0f, math.min(projectile.quality.mod, 1f))
data.damage_model.DamageUsing(info.min) + (damage * chargeQuality).toInt
case _ =>
damage
}
}
}
/**
* If the damage is resolved through a `HitDamage` packet,
* calculate the damage as a function of its degrading value over distance traveled by its carrier projectile.
* @see `distanceDegradeFunction`
* @see `ProjectileQuality`
* @see `ResolvedProjectile`
*/
case object FlakHit extends Mod {
def Calculate: DamageModifiers.Format = formula
private def formula(damage: Int, data: ResolvedProjectile): Int = {
if(data.resolution == ProjectileResolution.Hit) {
distanceDegradeFunction(damage, data)
} else {
damage
}
}
}
/**
* If the damage is resolved through a `SplashHitDamage` packet,
* calculate the damage as a function of its degrading value over distance
* between the hit position of the projectile and the position of the target.
* @see `radialDegradeFunction`
* @see `ProjectileQuality`
* @see `ResolvedProjectile`
*/
case object FlakBurst extends Mod {
def Calculate: DamageModifiers.Format = formula
private def formula(damage: Int, data: ResolvedProjectile): Int = {
if(data.resolution == ProjectileResolution.Splash) {
radialDegradeFunction(damage, data)
} else {
damage
}
}
}
/**
* The input value degrades (lessens)
* to the percentage of its original value
* if the target is a vehicle with no shields.
* Mainly used for the `galaxy_gunship` vehicle.
* @see `ResolvedProjectile`
*/
case class GalaxyGunshipReduction(multiplier: Float) extends Mod {
def Calculate: DamageModifiers.Format = formula
private def formula(damage: Int, data: ResolvedProjectile): Int = {
data.target match {
case v: VehicleSource
if v.Definition == GlobalDefinitions.galaxy_gunship && v.Shields == 0 =>
(damage * multiplier).toInt
case _ =>
damage
}
}
}
/* Functions */
/**
* The input value degrades (lessens)
* the further the distance between the point of origin (`shot_origin`)
* and the point of encounter (`hit_pos`) of its vector (projectile).
* If the value is not set to degrade over any distance within its maximum distance, the value goes unmodified.
* If the value is encountered beyond its maximum distance, the value is zero'd.
*/
private def distanceDegradeFunction(damage: Int, data: ResolvedProjectile): Int = {
val projectile = data.projectile
val profile = projectile.profile
val distance = Vector3.Distance(data.hit_pos, projectile.shot_origin)
if (distance <= profile.DistanceMax) {
if (profile.DistanceNoDegrade == profile.DistanceMax || distance <= profile.DistanceNoDegrade) {
damage
} else {
damage - ((damage - profile.DegradeMultiplier * damage) * ((distance - profile.DistanceNoDegrade) / (profile.DistanceMax - profile.DistanceNoDegrade))).toInt
}
} else {
0
}
}
/**
* The input value degrades (lessens)
* the further the distance between the point of origin (target position)
* and the point of encounter (`hit_pos`) of its vector (projectile).
* If the value is encountered beyond its maximum radial distance, the value is zero'd.
*/
private def radialDegradeFunction(damage: Int, data: ResolvedProjectile): Int = {
val profile = data.projectile.profile
val distance = Vector3.Distance(data.hit_pos, data.target.Position)
val radius = profile.DamageRadius
val radiusMin = profile.DamageRadiusMin
if (distance <= radiusMin) {
damage
} else if (distance <= radius) {
//damage - (damage * profile.DamageAtEdge * (distance - radiusMin) / (radius - radiusMin)).toInt
val base = profile.DamageAtEdge
val radi = radius - radiusMin
(damage * ((1 - base) * ((radi - (distance - radiusMin)) / radi) + base)).toInt
} else {
0
}
}
/**
* For damage application that involves aggravation of a particular damage type,
* calculate that initial damage application for infantry targets
* and produce the modified damage value.
* Infantry wearing mechanized assault exo-suits (MAX) incorporate an additional modifier.
* @see `AggravatedDamage`
* @see `ExoSuitType`
* @see `InfantryAggravatedDirect`
* @see `InfantryAggravatedSplash`
* @see `PlayerSource`
* @see `ProjectileTarget.AggravatesTarget`
* @see `ResolvedProjectile`
* @param resolution the projectile resolution to match against
* @param damageType the damage type to find in as a component of aggravated information
* @param damage the base damage value
* @param data historical information related to the damage interaction
* @return the modified damage
*/
private def baseAggravatedFormula(
resolution: ProjectileResolution.Value,
damageType : DamageType.Value
)
(
damage: Int,
data: ResolvedProjectile
): Int = {
if (data.resolution == resolution &&
data.projectile.quality == ProjectileQuality.AggravatesTarget) {
(data.projectile.profile.Aggravated, data.target) match {
case (Some(aggravation), p: PlayerSource) =>
val aggravatedDamage = aggravation.info.find(_.damage_type == damageType) match {
case Some(infos) =>
damage * infos.degradation_percentage + damage
case _ =>
damage toFloat
}
if(p.ExoSuit == ExoSuitType.MAX) {
(aggravatedDamage * aggravation.max_factor) toInt
} else {
aggravatedDamage toInt
}
case _ =>
damage
}
} else {
damage
}
}
/**
* For damage application that involves aggravation of a particular damage type,
* calculate that damage application burn for each tick for infantry targets
* and produce the modified damage value.
* Infantry wearing mechanized assault exo-suits (MAX) incorporate an additional modifier.
* Vanilla infantry incorporate their resistance value into a slightly different calculation than usual.
* @see `AggravatedDamage`
* @see `ExoSuitType`
* @see `InfantryAggravatedDirectBurn`
* @see `InfantryAggravatedSplashBurn`
* @see `PlayerSource`
* @see `ResolvedProjectile`
* @param resolution the projectile resolution to match against
* @param damageType the damage type to find in as a component of aggravated information
* @param damage the base damage value
* @param data historical information related to the damage interaction
* @return the modified damage
*/
private def baseAggravatedBurnFormula(
resolution: ProjectileResolution.Value,
damageType : DamageType.Value
)
(
damage: Int,
data: ResolvedProjectile
): Int = {
if (data.resolution == resolution) {
(data.projectile.profile.Aggravated, data.target) match {
case (Some(aggravation), p: PlayerSource) =>
val degradation = aggravation.info.find(_.damage_type == damageType) match {
case Some(info) =>
info.degradation_percentage
case _ =>
1f
}
if (p.exosuit == ExoSuitType.MAX) {
(damage * degradation * aggravation.max_factor) toInt
} else {
val resist = data.damage_model.ResistUsing(data)(data)
//add resist to offset resist subtraction later
if (damage > resist) {
((damage - resist) * degradation).toInt + resist
} else {
(damage * degradation).toInt + resist
}
}
case _ =>
0
}
} else {
damage
}
}
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2017 PSForever
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.damage
/**
@ -19,10 +19,10 @@ trait DamageProfile {
/** `damage1` is for armor, amenities, deployables, etc. */
def Damage1_=(damage: Int): Int
/** `damage2` if for aircraft */
/** `damage2` is for aircraft */
def Damage2: Int
/** `damage2` if for aircraft */
/** `damage2` is for aircraft */
def Damage2_=(damage: Int): Int
/** `damage3` is for mechanized infantry */

View file

@ -1,8 +1,9 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital
import net.psforever.objects.vital.damage.DamageProfile
package net.psforever.objects.vital.damage
/**
* A static modifier for damage.
*/
class SpecificDamageProfile extends DamageProfile {
private var damage0: Int = 0
private var damage1: Int = 0

View file

@ -1,10 +1,15 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
import net.psforever.objects.vital.damage.DamageProfile
package net.psforever.objects.vital.damage
/**
* A static modifier for damage.
* Typically considered the "raw damage" when a part of the "damage source".
* The value determination cascades down to the first defined one.
* Requesting `Damage3` returns the value for `Damage1`
* if `Damage3 == None` and `Damage2 == None` but `Damage1` is defined.
*/
trait StandardDamageProfile extends DamageProfile {
private var damage0: Int = 0
private var damage0: Int = 0 //always considered as a defined value
private var damage1: Option[Int] = None
private var damage2: Option[Int] = None
private var damage3: Option[Int] = None

View file

@ -0,0 +1,50 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.vital.{Vitality, VitalityDefinition}
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.prop.DamageWithPosition
import net.psforever.objects.vital.resolution.DamageAndResistance
/**
* A wrapper for a "damage source" in damage calculations
* that parameterizes information necessary to explain a server-driven explosion occurring.
* Some game objects cause area-of-effect damage upon being destroyed.
* @see `VitalityDefinition.explodes`
* @see `VitalityDefinition.innateDamage`
* @see `Zone.causesExplosion`
* @param entity the source of the explosive yield
* @param damageModel the model to be utilized in these calculations;
* typically, but not always, defined by the target
* @param instigation what previous event happened, if any, that caused this explosion
*/
final case class ExplodingEntityReason(
entity: PlanetSideGameObject with Vitality,
damageModel: DamageAndResistance,
instigation: Option[DamageResult]
) extends DamageReason {
private val definition = entity.Definition.asInstanceOf[ObjectDefinition with VitalityDefinition]
assert(definition.explodes && definition.innateDamage.nonEmpty, "causal entity does not explode")
def source: DamageWithPosition = definition.innateDamage.get
def resolution: DamageResolution.Value = DamageResolution.Explosion
def same(test: DamageReason): Boolean = test match {
case eer: ExplodingEntityReason => eer.entity eq entity
case _ => false
}
/** lay the blame on that which caused this explosion to occur */
def adversary: Option[SourceEntry] = instigation match {
case Some(prior) => prior.interaction.cause.adversary
case None => None
}
/** the entity that exploded is the source of the damage */
override def attribution: Int = definition.ObjectId
}

View file

@ -0,0 +1,22 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.vital.resolution.DamageAndResistance
final case class EnvironmentReason(body: Any, source: DamageProperties) extends DamageReason {
def resolution: DamageResolution.Value = DamageResolution.Unresolved
def same(test: DamageReason): Boolean = {
test match {
case o : EnvironmentReason => body == o.body //TODO eq
case _ => false
}
}
def adversary: Option[SourceEntry] = None
def damageModel: DamageAndResistance = null
}

View file

@ -0,0 +1,37 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.serverobject.painbox.Painbox
import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions}
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.prop.DamageWithPosition
import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel}
final case class PainboxReason(entity: Painbox) extends DamageReason {
private val definition = entity.Definition
assert(definition.innateDamage.nonEmpty, "causal entity does not emit pain field")
def source: DamageWithPosition = definition.innateDamage.get
def resolution: DamageResolution.Value = DamageResolution.Resolved
def same(test: DamageReason): Boolean = test match {
case eer: PainboxReason => eer.entity eq entity
case _ => false
}
def adversary: Option[SourceEntry] = None
def damageModel : DamageAndResistance = PainboxReason.drm
}
object PainboxReason {
/** damage0, no resisting, quick and simple */
val drm = new DamageResistanceModel {
DamageUsing = DamageCalculations.AgainstExoSuit
ResistUsing = NoResistanceSelection
Model = SimpleResolutions.calculate
}
}

View file

@ -0,0 +1,62 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions}
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel}
/**
* A wrapper for a "damage source" in damage calculations
* that parameterizes information necessary to explain wanting to kill oneself.
*/
final case class SuicideReason()
extends DamageReason {
/*
In my head canon, there is a neverending termite eating into the Auraxian human empires.
There is no recruitment.
People are barely alive long enough to feel basic needs like hunger or thirst.
All that is still thrives on the zealous fervor to keep the army motivated.
But what do people do if they don't want to fight anymore?
Do they just never come back from being a speck of thought floating in the air
and, in frustration at life, endure in a disembodied, solitary limbo of the nanites?
But that doesn't stop the thoughts, does it?
Never able to go back to Earth;
becoming a lifeform between organic and information, wandering the endless void known as space for eternity;
being unable to die even though they wish for it;
eventually, they stop logging in.
Anyway, this has nothing to do with that.
Most playes probably just want to jump to the next base over.
*/
def source: DamageProperties = SuicideReason.damageProperties
def resolution: DamageResolution.Value = DamageResolution.Resolved
def same(test: DamageReason): Boolean = {
test.source eq source
}
def adversary: Option[SourceEntry] = None
def damageModel: DamageAndResistance = SuicideReason.drm
}
object SuicideReason {
/** one swift blow that guarantees death */
val damageProperties = new DamageProperties {
Damage0 = 99999
DamageToHealthOnly = true
DamageToVehicleOnly = true
DamageToBattleframeOnly = true
}
/** damage0, no resisting, quick and simple */
val drm = new DamageResistanceModel {
DamageUsing = DamageCalculations.AgainstExoSuit
ResistUsing = NoResistanceSelection
Model = SimpleResolutions.calculate
}
}

View file

@ -0,0 +1,100 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.interaction
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.base.{DamageReason, DamageResolution, DamageType}
import net.psforever.objects.vital.resolution.{DamageAndResistance, ResolutionCalculations}
import net.psforever.types.Vector3
/**
* The recorded encounter of a damage source and a damageable target.
* @param target the original affected target;
* not necessarily the currently affected target
* @param hitPos the coordinate location where the damage was inflicted
* @param cause the method by which the damage was produced
* @param resolution how the damage is being processed
* @param hitTime when the interaction originally occurred;
* defaults to `System.currentTimeMills()` at object creation
*/
final case class DamageInteraction(
target: SourceEntry,
hitPos: Vector3,
cause: DamageReason,
resolution: DamageResolution.Value,
hitTime: Long = System.currentTimeMillis()
) {
/**
* If the cause of the original interaction can be attributed to some agency.
* @return a connection between offender, victim, and method
*/
def adversarial: Option[Adversarial] = cause.adversary match {
case Some(adversity) => Some(Adversarial(adversity, target, cause.attribution))
case None => None
}
/**
* Process the primary parameters from the interaction
* and produce the application function literal that can have a target entity applied to it.
* @return the function that applies changes to a target entity
*/
def calculate(): ResolutionCalculations.Output = cause.calculate(data = this)
/**
* Process the primary parameters from the interaction
* including a custom category of damage by which to temporarily reframe the interaction
* and produce the application function literal that can have a target entity applied to it.
* @return the function that applies changes to a target entity
*/
def calculate(dtype: DamageType.Value): ResolutionCalculations.Output = cause.calculate(data = this, dtype)
/**
* Process the primary parameters from the interaction
* in the context a custom damage processing method by which to temporarily reframe the interaction
* and produce a application function literal where the specified target entity can be applied to it.
* @param model the custom processing method
* @param target the target entity
* @return the outcome of the interaction under the given re-framing
*/
def calculate(model: DamageAndResistance)(target: PlanetSideGameObject with FactionAffinity): DamageResult = {
model.calculate(data = this)(target)
}
}
object DamageInteraction {
/**
* Overloaded constructor for an interaction.
* Shuffle the order of parameters; let time default.
* @param resolution how the damage is being processed
* @param target the original affected target
* @param cause the method by which the damage was produced
* @param hitPos the coordinate location where the damage was inflicted
* @return a `DamageInteraction` object
*/
def apply(resolution: DamageResolution.Value, target: SourceEntry, cause: DamageReason, hitPos: Vector3): DamageInteraction = {
DamageInteraction(
target,
hitPos,
cause,
resolution
)
}
/**
* Overloaded constructor for an interaction.
* Use the resolution from the reason for the damage, shuffle the parameters, and let time default.
* @param target the original affected target
* @param cause the method by which the damage was produced
* @param hitPos the coordinate location where the damage was inflicted
* @return a `DamageInteraction` object
*/
def apply(target: SourceEntry, cause: DamageReason, hitPos: Vector3): DamageInteraction = {
DamageInteraction(
target,
hitPos,
cause,
cause.resolution
)
}
}

View file

@ -0,0 +1,25 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.interaction
import net.psforever.objects.ballistics.SourceEntry
/**
* But one thing's sure. The player is hurt, attacked, and somebody's responsible.
* @param attacker the source of the damage
* @param defender the recipient of the damage
* @param implement how the damage was invoked;
* the object id of the method of punishment, used for reporting
*/
final case class Adversarial(attacker: SourceEntry, defender: SourceEntry, implement: Int)
/**
* The outcome of the damage interaction, after all the numbers have been processed and properly applied.
*/
final case class DamageResult(targetBefore: SourceEntry, targetAfter: SourceEntry, interaction: DamageInteraction) {
def adversarial: Option[Adversarial] = {
interaction.adversarial match {
case Some(adversarial) => Some(Adversarial(adversarial.attacker, targetAfter, adversarial.implement))
case None => None
}
}
}

View file

@ -1,21 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.projectile
import net.psforever.objects.ballistics.ResolvedProjectile
/**
* The base for all projectile-induced damage calculation function literals.
*/
trait ProjectileCalculations {
/**
* The exposed entry for the calculation function literal defined by this base.
* @param data the historical `ResolvedProjectile` information
* @return the calculated value
*/
def Calculate(data: ResolvedProjectile): Int
}
object ProjectileCalculations {
type Form = (ResolvedProjectile) => Int
}

View file

@ -0,0 +1,422 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.projectile
import net.psforever.objects.ballistics.{ChargeDamage, PlayerSource, ProjectileQuality}
import net.psforever.objects.equipment.ChargeFireModeDefinition
import net.psforever.objects.vital.base._
import net.psforever.objects.vital.damage.DamageModifierFunctions
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.types.{ExoSuitType, Vector3}
/** If the calculated distance is greater than the maximum distance of the projectile, damage is zero'd. */
case object MaxDistanceCutoff extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
val projectile = cause.projectile
val profile = projectile.profile
val distance = Vector3.Distance(data.hitPos, projectile.shot_origin)
if (distance <= profile.DistanceMax) {
damage
} else {
0
}
}
}
/** If the calculated distance is greater than a custom distance, damage is zero'd. */
case class CustomDistanceCutoff(cutoff: Float) extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
val projectile = cause.projectile
val distance = Vector3.Distance(data.hitPos, projectile.shot_origin)
if (distance <= cutoff) {
damage
} else {
0
}
}
}
/**
* The input value degrades (lessens)
* the further the distance between the point of origin (`shot_origin`)
* and the point of encounter (`hitPos`) of its vector (projectile).
* If the value is not set to degrade over any distance within its maximum distance, the value goes unmodified.
* If the value is encountered beyond its maximum distance, the value is zero'd.
*/
case object DistanceDegrade extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int =
ProjectileDamageModifierFunctions.distanceDegradeFunction(damage, data, cause)
}
/**
* Lashing is the property of a projectile affecting nearby targets without coming into direct contact with them.
* The effect only activates after 5m from the point of origin (`shot_origin`) before the maximum distance.
* If lashing does not apply, the value goes unmodified.
* If lashing is valid but the value is encountered beyond its maximum radial distance, the value is zero'd.
*/
case object Lash extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
if (cause.resolution == DamageResolution.Lash) {
val distance = Vector3.Distance(data.hitPos, cause.projectile.shot_origin)
if (distance > 5 && distance <= cause.projectile.profile.DistanceMax) {
(damage * 0.2f) toInt
} else {
0
}
} else {
damage
}
}
}
/*
Aggravated damage.
For the most part, these calculations are individualistic and arbitrary.
They exist in their current form to satisfy observed shots to kill (STK) of specific weapon systems
according to 2012 standards of the Youtube video series by TheLegendaryNarwhal.
*/
/**
* The initial application of aggravated damage against an infantry target
* where the specific damage component is `Direct`.
*/
case object InfantryAggravatedDirect extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int =
ProjectileDamageModifierFunctions.baseAggravatedFormula(DamageResolution.AggravatedDirect, DamageType.Direct)(damage, data, cause)
}
/**
* The initial application of aggravated damage against an infantry target
* where the specific damage component is `Splash`.
*/
case object InfantryAggravatedSplash extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int =
ProjectileDamageModifierFunctions.baseAggravatedFormula(DamageResolution.AggravatedSplash, DamageType.Splash)(damage, data, cause)
}
/**
* The ongoing application of aggravated damage ticks against an infantry target
* where the specific damage component is `Direct`.
* This is called "burning" regardless of what the active aura effect actually is.
*/
case object InfantryAggravatedDirectBurn extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int =
ProjectileDamageModifierFunctions.baseAggravatedBurnFormula(DamageResolution.AggravatedDirectBurn, DamageType.Direct)(damage, data, cause)
}
/**
* The ongoing application of aggravated damage ticks against an infantry target
* where the specific damage component is `Splash`.
* This is called "burning" regardless of what the active aura effect actually is.
*/
case object InfantryAggravatedSplashBurn extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int =
ProjectileDamageModifierFunctions.baseAggravatedBurnFormula(DamageResolution.AggravatedSplashBurn, DamageType.Splash)(damage, data, cause)
}
/**
* For damage application that involves aggravation of a fireball (Dragon secondary fire mode),
* perform 1 damage.
*/
case object FireballAggravatedBurn extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
if (damage > 0 &&
(cause.resolution == DamageResolution.AggravatedDirectBurn ||
cause.resolution == DamageResolution.AggravatedSplashBurn)) {
//add resist to offset resist subtraction later
1 + cause.damageModel.ResistUsing(data)(data)
} else {
damage
}
}
}
/**
* The initial application of aggravated damage against an aircraft target.
* Primarily for use in the starfire weapon system.
* @see `AggravatedDamage`
* @see `ProjectileQuality.AggravatesTarget`
*/
case object StarfireAggravated extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
if (cause.resolution == DamageResolution.AggravatedDirect &&
cause.projectile.quality == ProjectileQuality.AggravatesTarget) {
data.cause.source.Aggravated match {
case Some(aggravation) =>
aggravation.info.find(_.damage_type == DamageType.Direct) match {
case Some(infos) =>
(damage * infos.degradation_percentage + damage) toInt
case _ =>
damage
}
case _ =>
damage
}
} else {
damage
}
}
}
/**
* The ongoing application of aggravated damage ticks against an aircraft target.
* Primarily for use in the starfire weapon system.
* This is called "burning" regardless of what the active aura effect actually is.
* @see `AggravatedDamage`
* @see `ProjectileQuality`
*/
case object StarfireAggravatedBurn extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
if (cause.resolution == DamageResolution.AggravatedDirectBurn) {
data.cause.source.Aggravated match {
case Some(aggravation) =>
aggravation.info.find(_.damage_type == DamageType.Direct) match {
case Some(infos) =>
(math.floor(damage * infos.degradation_percentage) * cause.projectile.quality.mod) toInt
case _ =>
damage
}
case _ =>
0
}
} else {
damage
}
}
}
/**
* The initial application of aggravated damage against a target.
* Primarily for use in the comet weapon system.
* @see `AggravatedDamage`
* @see `ProjectileQuality.AggravatesTarget`
*/
case object CometAggravated extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
if (cause.resolution == DamageResolution.AggravatedDirect &&
cause.projectile.quality == ProjectileQuality.AggravatesTarget) {
data.cause.source.Aggravated match {
case Some(aggravation) =>
aggravation.info.find(_.damage_type == DamageType.Direct) match {
case Some(infos) =>
damage - (damage * infos.degradation_percentage) toInt
case _ =>
damage
}
case _ =>
damage
}
} else {
damage
}
}
}
/**
* The ongoing application of aggravated damage ticks against a target.
* Primarily for use in the comet weapon system.
* This is called "burning" regardless of what the active aura effect actually is.
* @see `AggravatedDamage`
* @see `ProjectileQuality`
*/
case object CometAggravatedBurn extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
if (cause.resolution == DamageResolution.AggravatedDirectBurn) {
data.cause.source.Aggravated match {
case Some(aggravation) =>
aggravation.info.find(_.damage_type == DamageType.Direct) match {
case Some(infos) =>
damage - (damage * infos.degradation_percentage) toInt
case _ =>
damage
}
case _ =>
0
}
} else {
damage
}
}
}
/**
* If the projectile has charging properties,
* and the weapon that produced the projectile has charging mechanics,
* calculate the current value of the damage as a sum
* of some minimum damage and scaled normal damage.
* The projectile quality has information about the "factor" of damage scaling.
* @see `ChargeDamage`
* @see `ChargeFireModeDefinition`
* @see `ProjectileQuality`
*/
case object SpikerChargeDamage extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
val projectile = cause.projectile
(projectile.fire_mode, projectile.profile.Charging) match {
case (_: ChargeFireModeDefinition, Some(info: ChargeDamage)) =>
val chargeQuality = math.max(0f, math.min(projectile.quality.mod, 1f))
cause.damageModel.DamageUsing(info.min) + (damage * chargeQuality).toInt
case _ =>
damage
}
}
}
/**
* If the damage is resolved through a `HitDamage` packet,
* calculate the damage as a function of its degrading value over distance traveled by its carrier projectile.
* @see `distanceDegradeFunction`
* @see `ProjectileQuality`
*/
case object FlakHit extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
if(cause.resolution == DamageResolution.Hit) {
ProjectileDamageModifierFunctions.distanceDegradeFunction(damage, data, cause)
} else {
damage
}
}
}
/**
* If the damage is resolved through a `SplashHitDamage` packet,
* calculate the damage as a function of its degrading value over distance
* between the hit position of the projectile and the position of the target.
* @see `DamageModifierFunctions.radialDegradeFunction`
* @see `ProjectileQuality`
*/
case object FlakBurst extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
if(cause.resolution == DamageResolution.Splash) {
DamageModifierFunctions.radialDegradeFunction(damage, data, cause)
} else {
damage
}
}
}
/* Functions */
object ProjectileDamageModifierFunctions {
/**
* The input value degrades (lessens)
* the further the distance between the point of origin (`shot_origin`)
* and the point of encounter (`hitPos`) of its vector (projectile).
* If the value is not set to degrade over any distance within its maximum distance, the value goes unmodified.
* If the value is encountered beyond its maximum distance, the value is zero'd.
*/
def distanceDegradeFunction(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
val projectile = cause.projectile
val profile = projectile.profile
val distance = Vector3.Distance(data.hitPos, projectile.shot_origin)
if (distance <= profile.DistanceMax) {
if (profile.DistanceNoDegrade == profile.DistanceMax || distance <= profile.DistanceNoDegrade) {
damage
} else {
damage - ((damage - profile.DegradeMultiplier * damage) * ((distance - profile.DistanceNoDegrade) / (profile.DistanceMax - profile.DistanceNoDegrade))).toInt
}
} else {
0
}
}
/**
* For damage application that involves aggravation of a particular damage type,
* calculate that initial damage application for infantry targets
* and produce the modified damage value.
* Infantry wearing mechanized assault exo-suits (MAX) incorporate an additional modifier.
* @see `AggravatedDamage`
* @see `ExoSuitType`
* @see `InfantryAggravatedDirect`
* @see `InfantryAggravatedSplash`
* @see `PlayerSource`
* @see `ProjectileTarget.AggravatesTarget`
* @param resolution the projectile resolution to match against
* @param damageType the damage type to find in as a component of aggravated information
* @param damage the base damage value
* @param data historical information related to the damage interaction
* @return the modified damage
*/
def baseAggravatedFormula(
resolution: DamageResolution.Value,
damageType : DamageType.Value
)
(
damage: Int,
data: DamageInteraction,
cause: ProjectileReason
): Int = {
if (cause.resolution == resolution &&
cause.projectile.quality == ProjectileQuality.AggravatesTarget) {
(data.cause.source.Aggravated, data.target) match {
case (Some(aggravation), p: PlayerSource) =>
val aggravatedDamage = aggravation.info.find(_.damage_type == damageType) match {
case Some(infos) =>
damage * infos.degradation_percentage + damage
case _ =>
damage toFloat
}
if(p.ExoSuit == ExoSuitType.MAX) {
(aggravatedDamage * aggravation.max_factor) toInt
} else {
aggravatedDamage toInt
}
case _ =>
damage
}
} else {
damage
}
}
/**
* For damage application that involves aggravation of a particular damage type,
* calculate that damage application burn for each tick for infantry targets
* and produce the modified damage value.
* Infantry wearing mechanized assault exo-suits (MAX) incorporate an additional modifier.
* Vanilla infantry incorporate their resistance value into a slightly different calculation than usual.
* @see `AggravatedDamage`
* @see `ExoSuitType`
* @see `InfantryAggravatedDirectBurn`
* @see `InfantryAggravatedSplashBurn`
* @see `PlayerSource`
* @param resolution the projectile resolution to match against
* @param damageType the damage type to find in as a component of aggravated information
* @param damage the base damage value
* @param data historical information related to the damage interaction
* @return the modified damage
*/
def baseAggravatedBurnFormula(
resolution: DamageResolution.Value,
damageType : DamageType.Value
)
(
damage: Int,
data: DamageInteraction,
cause: ProjectileReason
): Int = {
if (cause.resolution == resolution) {
(data.cause.source.Aggravated, data.target) match {
case (Some(aggravation), p: PlayerSource) =>
val degradation = aggravation.info.find(_.damage_type == damageType) match {
case Some(info) =>
info.degradation_percentage
case _ =>
1f
}
if (p.exosuit == ExoSuitType.MAX) {
(damage * degradation * aggravation.max_factor) toInt
} else {
val resist = cause.damageModel.ResistUsing(data)(data)
//add resist to offset resist subtraction later
if (damage > resist) {
((damage - resist) * degradation).toInt + resist
} else {
(damage * degradation).toInt + resist
}
}
case _ =>
0
}
} else {
damage
}
}
}

View file

@ -0,0 +1,18 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.projectile
import net.psforever.objects.vital.base._
import net.psforever.objects.vital.interaction.DamageInteraction
object ProjectileDamageModifiers {
trait Mod extends DamageModifiers.Mod {
def calculate(damage : Int, data : DamageInteraction, cause : DamageReason) : Int = {
cause match {
case o : ProjectileReason => calculate(damage, data, o)
case _ => damage
}
}
def calculate(damage : Int, data : DamageInteraction, cause : ProjectileReason) : Int
}
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.projectile
import net.psforever.objects.ballistics.{SourceEntry, Projectile => ActualProjectile}
import net.psforever.objects.vital.base._
import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.vital.resolution.DamageAndResistance
/**
* A wrapper for a "damage source" in damage calculations
* that parameterizes information necessary to explain a projectile being used.
* @param resolution how the damage is processed
* @param projectile the projectile that caused the damage
* @param damageModel the model to be utilized in these calculations;
* typically, but not always, defined by the target
*/
final case class ProjectileReason(
resolution : DamageResolution.Value,
projectile: ActualProjectile,
damageModel: DamageAndResistance
) extends DamageReason {
def source: DamageProperties = projectile.profile
def same(test: DamageReason): Boolean = {
test match {
case o: ProjectileReason => o.projectile.id == projectile.id //can only be another projectile with the same uid
case _ => false
}
}
override def staticModifiers: List[DamageProfile] = List(projectile.fire_mode.Add)
override def unstructuredModifiers: List[DamageModifiers.Mod] = projectile.fire_mode.Modifiers
def adversary: Option[SourceEntry] = Some(projectile.owner)
override def attribution: Int = projectile.attribute_to
}

View file

@ -0,0 +1,130 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.prop
import net.psforever.objects.ballistics.{AggravatedDamage, ChargeDamage}
import net.psforever.objects.equipment.JammingUnit
import net.psforever.objects.vital.base.{DamageModifiers, DamageType}
import net.psforever.objects.vital.damage.StandardDamageProfile
/**
* Information that explains aspects of the the damage being performed that go beyond simple numbers.
* Activation of these "special effects" may or may not even require the damage to be countable
* which is the context in which it is formally normalized.
*/
trait DamageProperties
extends StandardDamageProfile
with JammingUnit
with DamageModifiers {
/** the type of damage cuased */
private var damageType: DamageType.Value = DamageType.None
/** an auxiliary type of damage caused */
private var damageTypeSecondary: DamageType.Value = DamageType.None
/** against Infantry targets, damage does not apply to armor damage */
private var damageToHealthOnly: Boolean = false
/** against Vehicle targets, damage does not apply to vehicle shield */
private var damageToVehicleOnly: Boolean = false
/** against battleframe targets, damage does not apply to battleframe robotics shield;
* this is equivalent to the property "bfr_permeate_shield" */
private var damageToBFROnly: Boolean = false
/** use a specific modifier as a part of damage calculations */
private var useDamage1Subtract: Boolean = false
/** some other entity confers damage;
* a set value should not `None` and not `0` but is preferred to be the damager's uid */
private var damageProxy: Option[Int] = None
/** na;
* currently used with jammer properties only */
private var additionalEffect: Boolean = false
/** confers aggravated damage burn to its target */
private var aggravatedDamage: Option[AggravatedDamage] = None
/** modifies based on some measure of time */
private var charging: Option[ChargeDamage] = None
/** a destroyed mine will detonate rather than fizzle-out */
private var sympathy: Boolean = false
def UseDamage1Subtract: Boolean = useDamage1Subtract
def UseDamage1Subtract_=(useDamage1Subtract: Boolean): Boolean = {
this.useDamage1Subtract = useDamage1Subtract
UseDamage1Subtract
}
def CausesDamageType: DamageType.Value = damageType
def CausesDamageType_=(damageType1: DamageType.Value): DamageType.Value = {
damageType = damageType1
CausesDamageType
}
def CausesDamageTypeSecondary: DamageType.Value = damageTypeSecondary
def CausesDamageTypeSecondary_=(damageTypeSecondary1: DamageType.Value): DamageType.Value = {
damageTypeSecondary = damageTypeSecondary1
CausesDamageTypeSecondary
}
def AllDamageTypes : Set[DamageType.Value] = {
Set(damageType, damageTypeSecondary).filterNot(_ == DamageType.None)
}
def DamageToHealthOnly : Boolean = damageToHealthOnly
def DamageToHealthOnly_=(healthOnly: Boolean) : Boolean = {
damageToHealthOnly = healthOnly
DamageToHealthOnly
}
def DamageToVehicleOnly : Boolean = damageToVehicleOnly
def DamageToVehicleOnly_=(vehicleOnly: Boolean) : Boolean = {
damageToVehicleOnly = vehicleOnly
DamageToVehicleOnly
}
def DamageToBattleframeOnly : Boolean = damageToBFROnly
def DamageToBattleframeOnly_=(bfrOnly: Boolean) : Boolean = {
damageToBFROnly = bfrOnly
DamageToBattleframeOnly
}
def DamageProxy : Option[Int] = damageProxy
def DamageProxy_=(proxyObjectId : Int) : Option[Int] = DamageProxy_=(Some(proxyObjectId))
def DamageProxy_=(proxyObjectId : Option[Int]) : Option[Int] = {
damageProxy = proxyObjectId
DamageProxy
}
def AdditionalEffect: Boolean = additionalEffect
def AdditionalEffect_=(effect: Boolean): Boolean = {
additionalEffect = effect
AdditionalEffect
}
def Aggravated : Option[AggravatedDamage] = aggravatedDamage
def Aggravated_=(damage : AggravatedDamage) : Option[AggravatedDamage] = Aggravated_=(Some(damage))
def Aggravated_=(damage : Option[AggravatedDamage]) : Option[AggravatedDamage] = {
aggravatedDamage = damage
Aggravated
}
def Charging : Option[ChargeDamage] = charging
def Charging_=(damage : ChargeDamage) : Option[ChargeDamage] = Charging_=(Some(damage))
def Charging_=(damage : Option[ChargeDamage]) : Option[ChargeDamage] = {
charging = damage
Charging
}
def SympatheticExplosion: Boolean = sympathy
def SympatheticExplosion_=(chain: Boolean): Boolean = {
sympathy = chain
SympatheticExplosion
}
}

View file

@ -0,0 +1,38 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.prop
/**
* Damage that has a sense of occurring in a place where the target is not
* but the target is still affected by the damage.
* The distance between the target and the point of activation can have a modifying effect.
*/
trait DamageWithPosition
extends DamageProperties {
/** for radial damage, how much damage has been lost the furthest away from the point of origin (m) */
private var damageAtEdge: Float = 1f
/** for radial damage, the distance of the effect (m) */
private var damageRadius: Float = 0f
/** for radial damage, the distance before initial degradation of the effect (m) */
private var damageRadiusMin: Float = 1f
def DamageAtEdge: Float = damageAtEdge
def DamageAtEdge_=(atEdge: Float): Float = {
damageAtEdge = atEdge
DamageAtEdge
}
def DamageRadius: Float = damageRadius
def DamageRadius_=(radius: Float): Float = {
damageRadius = radius
DamageRadius
}
def DamageRadiusMin: Float = damageRadiusMin
def DamageRadiusMin_=(radius: Float): Float = {
damageRadiusMin = radius
DamageRadiusMin
}
}

View file

@ -5,42 +5,43 @@ import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.ballistics._
import net.psforever.objects.definition.ExoSuitDefinition
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.vital.projectile.ProjectileCalculations
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.types.ExoSuitType
import scala.util.{Failure, Success, Try}
/**
* The base class for function literal description related to calculating resistance information.<br>
* <br>
* Implementing functionality of the children is the product of two user-defined processes
* and information for the calculation is extracted from the to-be-provided weapon discharge information.
* Specifically, the information is found as the `target` object which is a member of the said information.
* The specific functions passed into this object typically operate simultaneously normally
* and are related to the target and the kind of interaction the weapon discharge had with the target.
* The base for function literal description related to calculating resistance information.
* This glue connects target validation to value extraction
* to avoid the possibility of `NullPointerException` and `ClassCastException`.
* Some different types of vital objects store their resistance values in different places.
* @param validate determine if a more generic `target` object is actually an expected type;
* cast to and return that type of object
* @param extractor recover the resistance values from an approved type of object
* @param default if the target does not match the validator, this is the constant resistance to return;
* the code really needs to be examined in this case;
* defaults to 0
* @tparam TargetType an internal type that converts between `validate`'s output and `extractor`'s input;
* in essence, should match the type of object container to which these resistances belong;
* never has to be defined explicitly, but will be checked upon object definition
* never has to be defined explicitly but will be checked at compile time
*/
abstract class ResistanceCalculations[TargetType](
validate: ResolvedProjectile => Try[TargetType],
extractor: TargetType => Int
) extends ProjectileCalculations {
validate: DamageInteraction => Try[TargetType],
extractor: TargetType => Int,
default: Int = 0
) {
/**
* Get resistance valuess.
* @param data the historical `ResolvedProjectile` information
* Get resistance values.
* @param data the historical `DamageInteraction` information
* @return the damage value
*/
def Calculate(data: ResolvedProjectile): Int = {
def Calculate(data: DamageInteraction): Int = {
validate(data) match {
case Success(target) =>
extractor(target)
case _ =>
0
default
}
}
}
@ -49,9 +50,12 @@ object ResistanceCalculations {
private def failure(typeName: String) = Failure(new Exception(s"can not match expected target $typeName"))
//target identification
def InvalidTarget(data: ResolvedProjectile): Try[SourceEntry] = failure(s"invalid ${data.target.Definition.Name}")
def InvalidTarget(data: DamageInteraction): Try[SourceEntry] = failure(s"invalid ${data.target.Definition.Name}")
def ValidInfantryTarget(data: ResolvedProjectile): Try[PlayerSource] = {
//target is always considered valid
def AlwaysValidTarget(data: DamageInteraction): Try[SourceEntry] = Success(data.target)
def ValidInfantryTarget(data: DamageInteraction): Try[PlayerSource] = {
data.target match {
case target: PlayerSource =>
if (target.ExoSuit != ExoSuitType.MAX) { //max is not counted as an official infantry exo-suit type
@ -64,7 +68,7 @@ object ResistanceCalculations {
}
}
def ValidMaxTarget(data: ResolvedProjectile): Try[PlayerSource] = {
def ValidMaxTarget(data: DamageInteraction): Try[PlayerSource] = {
data.target match {
case target: PlayerSource =>
if (target.ExoSuit == ExoSuitType.MAX) {
@ -77,7 +81,7 @@ object ResistanceCalculations {
}
}
def ValidVehicleTarget(data: ResolvedProjectile): Try[VehicleSource] = {
def ValidVehicleTarget(data: DamageInteraction): Try[VehicleSource] = {
data.target match {
case target: VehicleSource =>
if (!GlobalDefinitions.isFlightVehicle(target.Definition)) {
@ -90,7 +94,7 @@ object ResistanceCalculations {
}
}
def ValidAircraftTarget(data: ResolvedProjectile): Try[VehicleSource] = {
def ValidAircraftTarget(data: DamageInteraction): Try[VehicleSource] = {
data.target match {
case target: VehicleSource =>
if (GlobalDefinitions.isFlightVehicle(target.Definition)) {
@ -103,16 +107,16 @@ object ResistanceCalculations {
}
}
def ValidAmenityTarget(data: ResolvedProjectile): Try[ObjectSource] = {
def ValidAmenityTarget(data: DamageInteraction): Try[ObjectSource] = {
data.target match {
case target: ObjectSource =>
if (target.obj.isInstanceOf[Amenity]) {
Success(target)
} else {
failure("something else")
failure(s"${target.Definition.Name} amenity")
}
case _ =>
failure("something else")
failure(s"amenity")
}
}

View file

@ -1,8 +1,8 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resistance
import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.{DamageType, StandardDamageProfile}
import net.psforever.objects.vital.damage.{DamageProfile, StandardDamageProfile}
import net.psforever.objects.vital.base.DamageType
/**
* The different values for four common methods of modifying incoming damage.

View file

@ -1,35 +1,39 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resistance
import net.psforever.objects.ballistics._
import net.psforever.objects.vital.{DamageType, NoResistance}
import net.psforever.objects.vital.projectile.ProjectileCalculations
import net.psforever.objects.vital.NoResistance
import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.interaction.DamageInteraction
/**
* Maintain information about four primary forms of resistance calculation
* and a means to test which calculation is valid in a given situation.
*/
trait ResistanceSelection {
final def None: ProjectileCalculations.Form = NoResistance.Calculate
def Direct: ResistanceSelection.Format
def Splash: ResistanceSelection.Format
def Lash: ResistanceSelection.Format
def Aggravated: ResistanceSelection.Format
def Direct: ProjectileCalculations.Form
def Splash: ProjectileCalculations.Form
def Lash: ProjectileCalculations.Form
def Aggravated: ProjectileCalculations.Form
def apply(data : ResolvedProjectile) : ProjectileCalculations.Form = data.projectile.profile.ProjectileDamageType match {
case DamageType.Direct => Direct
case DamageType.Splash => Splash
case DamageType.Lash => Lash
def apply(data: DamageInteraction) : ResistanceSelection.Format = data.cause.source.CausesDamageType match {
case DamageType.Direct => Direct
case DamageType.Splash => Splash
case DamageType.Lash => Lash
case DamageType.Aggravated => Aggravated
case _ => None
case _ => ResistanceSelection.None
}
def apply(res : DamageType.Value) : ProjectileCalculations.Form = res match {
case DamageType.Direct => Direct
case DamageType.Splash => Splash
case DamageType.Lash => Lash
def apply(res: DamageType.Value) : ResistanceSelection.Format = res match {
case DamageType.Direct => Direct
case DamageType.Splash => Splash
case DamageType.Lash => Lash
case DamageType.Aggravated => Aggravated
case _ => None
case _ => ResistanceSelection.None
}
}
object ResistanceSelection {
type Format = DamageInteraction => Int
final val None: ResistanceSelection.Format = NoResistance.Calculate
}

View file

@ -1,9 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
package net.psforever.objects.vital.resistance
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.resistance.ResistanceProfile
/**
* The different values for four common methods of modifying incoming damage.

View file

@ -1,50 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resolution
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.projectile.ProjectileCalculations
/**
* A specific implementation of `ResolutionCalculations` that deals with
* the damage value and the resistance value in a specific manner.
* (The input type of the function literal output of `calcFunc`.)
* @param calcFunc a function literal that retrieves the function
* that factors the affects of damage and resistance values
* @param applyFunc a function literal that applies the final modified values to a target object
* @tparam A an internal type that converts between `calcFunc`'s output and `applyFunc`'s input;
* never has to be defined explicitly, but will be checked upon object definition
*/
abstract class DamageResistCalculations[A](
calcFunc: ResolvedProjectile => (Int, Int) => A,
applyFunc: (A, ResolvedProjectile) => ResolutionCalculations.Output
) extends ResolutionCalculations {
def Calculate(
damages: DamageCalculations.Selector,
resistances: ProjectileCalculations.Form,
data: ResolvedProjectile
): ResolutionCalculations.Output = {
val modDam = Sample(damages, resistances, data)
applyFunc(modDam, data)
}
/**
* An intermediate step of the normal `Calculate` operation that retrieves the damage values in their transitory form.
* @param damages the function that calculations raw damage values
* @param resistances the function that calculates resistance values
* @param data a historical projectile interaction;
* the origin of the data used to extract damage and resistance values
* @return the transitory form of the modified damage(s);
* usually, a single `Int` value or a tuple of `Int` values
*/
def Sample(
damages: DamageCalculations.Selector,
resistances: ProjectileCalculations.Form,
data: ResolvedProjectile
): A = {
val dam = DamageCalculations.DamageWithModifiers(damages, data)
val res: Int = resistances(data)
val mod = calcFunc(data)
mod(dam, res)
}
}

View file

@ -0,0 +1,38 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resolution
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.resistance.ResistanceSelection
/**
* A specific implementation of `ResolutionCalculations` that deals with
* the damage value and the resistance value in a specific manner.
* (The input type of the function literal output of `calcFunc`.)
* @see `DamageCalculations.WithModifiers`
* @param calcFunc a function literal that retrieves the function
* that factors the affects of damage and resistance values
* @param applyFunc a function literal that applies the final modified values to a target object
* @param modifiersFunc a function literal that extracts and modifies a numeric damage value;
* even if no modifiers are to be used, the base damage value needs to be extracted;
* defaults to a function that utilizes all of the available information
* @tparam A an internal type that converts between `calcFunc`'s output and `applyFunc`'s input;
* never has to be defined explicitly but will be checked at compile time
*/
abstract class DamageResistanceCalculations[A](
calcFunc: DamageInteraction => (Int, Int) => A,
applyFunc: (A, DamageInteraction) => ResolutionCalculations.Output,
modifiersFunc: (DamageCalculations.Selector, DamageInteraction) => Int = DamageCalculations.WithModifiers
) extends ResolutionCalculations {
def calculate(
damages: DamageCalculations.Selector,
resistances: ResistanceSelection.Format,
data: DamageInteraction
): ResolutionCalculations.Output = {
val dam = modifiersFunc(damages, data)
val res = resistances(data)
val mod = calcFunc(data)
val modDam = mod(dam, res)
applyFunc(modDam, data)
}
}

View file

@ -1,14 +1,40 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
package net.psforever.objects.vital.resolution
import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.vital.projectile.ProjectileCalculations
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.resistance.ResistanceSelection
import net.psforever.objects.vital.resolution.ResolutionCalculations
import net.psforever.objects.vital.{NoResistanceSelection, NoResolutions}
/**
* The functionality that is necessary for interaction of a vital game object with the rest of the game world.<br>
* The functionality that is necessary for interaction of a vital game object with the rest of the hostile game world.
*/
trait DamageAndResistance {
def DamageUsing: DamageCalculations.Selector
def ResistUsing: ResistanceSelection
def Model: ResolutionCalculations.Form
def calculate(data: DamageInteraction): ResolutionCalculations.Output
def calculate(data: DamageInteraction, resolution: DamageType.Value): ResolutionCalculations.Output
}
object DamageAndResistance {
/**
* A pass-through function.
* @param data garbage in
* @return garbage out
*/
def doNothingFallback(data: DamageInteraction): ResolutionCalculations.Output = {
_: Any => DamageResult(data.target, data.target, data)
}
}
/**
* The functionality that is necessary for interaction of a vital game object with the rest of the hostile game world.<br>
* <br>
* A vital object can be hurt or damaged or healed or repaired (HDHR).
* The actual implementation of how that works is left to the specific object and its interfaces, however.
@ -24,7 +50,7 @@ import net.psforever.objects.vital.resolution.ResolutionCalculations
* By default, nothing should do anything of substance.
* @see `Vitality`
*/
trait DamageResistanceModel {
trait DamageResistanceModel extends DamageAndResistance {
/** the functionality that processes damage; required */
private var damageUsing: DamageCalculations.Selector = DamageCalculations.AgainstNothing
@ -33,7 +59,7 @@ trait DamageResistanceModel {
private var resistUsing: ResistanceSelection = NoResistanceSelection
/** the functionality that prepares for damage application actions; required */
private var model: ResolutionCalculations.Form = NoResolutions.Calculate
private var model: ResolutionCalculations.Form = NoResolutions.calculate
def DamageUsing: DamageCalculations.Selector = damageUsing
@ -58,22 +84,22 @@ trait DamageResistanceModel {
/**
* Magic stuff.
* @param data the historical `ResolvedProjectile` information
* @param data the historical damage information
* @return a function literal that encapsulates delayed modification instructions for certain objects
*/
def Calculate(data: ResolvedProjectile): ResolutionCalculations.Output = {
val res: ProjectileCalculations.Form = ResistUsing(data)
def calculate(data: DamageInteraction): ResolutionCalculations.Output = {
val res: ResistanceSelection.Format = ResistUsing(data)
Model(DamageUsing, res, data)
}
/**
* Magic stuff.
* @param data the historical `ResolvedProjectile` information
* @param resolution an explicit damage resolution overriding the one in the `ResolvedProjectile` object
* @param data the historical damage information
* @param resolution an explicit damage resolution overriding the one provided
* @return a function literal that encapsulates delayed modification instructions for certain objects
*/
def Calculate(data: ResolvedProjectile, resolution: DamageType.Value): ResolutionCalculations.Output = {
val res: ProjectileCalculations.Form = ResistUsing(resolution)
def calculate(data: DamageInteraction, resolution: DamageType.Value): ResolutionCalculations.Output = {
val res: ResistanceSelection.Format = ResistUsing(resolution)
Model(DamageUsing, res, data)
}
}

View file

@ -1,47 +1,45 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resolution
import net.psforever.objects.{Player, TurretDeployable, Vehicle}
import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile}
import net.psforever.objects.ce.{ComplexDeployable, Deployable}
import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle}
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry}
import net.psforever.objects.ce.ComplexDeployable
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.projectile.ProjectileCalculations
import net.psforever.types.ImplantType
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.resistance.ResistanceSelection
import net.psforever.types.{ExoSuitType, ImplantType}
/**
* The base for the combining step of all projectile-induced damage calculation function literals.
*/
trait ResolutionCalculations {
/**
* The exposed entry for the calculation function literal defined by this base.
* @param damages the function literal that accumulates and calculates damages
* @param resistances the function literal that collects resistance values
* @param data the historical `ResolvedProjectile` information
* @param data the historical damage information
* @return a function literal that encapsulates delayed modification instructions for certain objects
*/
def Calculate(
def calculate(
damages: DamageCalculations.Selector,
resistances: ProjectileCalculations.Form,
data: ResolvedProjectile
resistances: ResistanceSelection.Format,
data: DamageInteraction
): ResolutionCalculations.Output
}
object ResolutionCalculations {
type Output = Any => ResolvedProjectile
type Form = (DamageCalculations.Selector, ProjectileCalculations.Form, ResolvedProjectile) => Output
type Output = PlanetSideGameObject with FactionAffinity => DamageResult
type Form = (DamageCalculations.Selector, ResistanceSelection.Format, DamageInteraction) => Output
def NoDamage(data: ResolvedProjectile)(a: Int, b: Int): Int = 0
def NoDamage(data: DamageInteraction)(a: Int, b: Int): Int = 0
def InfantryDamage(data: ResolvedProjectile): (Int, Int) => (Int, Int) = {
def InfantryDamage(data: DamageInteraction): (Int, Int) => (Int, Int) = {
data.target match {
case target: PlayerSource =>
if(data.projectile.profile.DamageToHealthOnly) {
if(data.cause.source.DamageToHealthOnly) {
DamageToHealthOnly(target.health)
} else {
InfantryDamageAfterResist(target.health, target.armor)
@ -83,10 +81,10 @@ object ResolutionCalculations {
}
}
def MaxDamage(data: ResolvedProjectile): (Int, Int) => (Int, Int) = {
def MaxDamage(data: DamageInteraction): (Int, Int) => (Int, Int) = {
data.target match {
case target: PlayerSource =>
if(data.projectile.profile.DamageToHealthOnly) {
if(data.cause.source.DamageToHealthOnly) {
DamageToHealthOnly(target.health)
} else {
MaxDamageAfterResist(target.health, target.armor)
@ -115,10 +113,10 @@ object ResolutionCalculations {
* Unlike with `Infantry*` and with `Max*`'s,
* `VehicleDamageAfterResist` does not necessarily need to validate its target object.
* The required input is sufficient.
* @param data the historical `ResolvedProjectile` information
* @param data the historical damage information
* @return a function literal for dealing with damage values and resistance values together
*/
def VehicleDamageAfterResist(data: ResolvedProjectile): (Int, Int) => Int = {
def VehicleDamageAfterResist(data: DamageInteraction): (Int, Int) => Int = {
VehicleDamageAfterResist
}
@ -130,7 +128,10 @@ object ResolutionCalculations {
}
}
def NoApplication(damageValue: Int, data: ResolvedProjectile)(target: Any): ResolvedProjectile = data
def NoApplication(damageValue: Int, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = {
val sameTarget = SourceEntry(target)
DamageResult(sameTarget, sameTarget, data)
}
def SubtractWithRemainder(current: Int, damage: Int): (Int, Int) = {
val a = Math.max(0, current - damage)
@ -138,7 +139,7 @@ object ResolutionCalculations {
(a, remainingDamage)
}
private def CanDamage(obj: Vitality with FactionAffinity, damage: Int, data: ResolvedProjectile): Boolean = {
private def CanDamage(obj: Vitality with FactionAffinity, damage: Int, data: DamageInteraction): Boolean = {
obj.Health > 0 && Damageable.CanDamage(obj, damage, data)
}
@ -146,58 +147,61 @@ object ResolutionCalculations {
* The expanded `(Any)=>Unit` function for infantry.
* Apply the damage values to the capacitor (if shielded NC max), health field and personal armor field for an infantry target.
* @param damageValues a tuple containing damage values for: health, personal armor
* @param data the historical `ResolvedProjectile` information
* @param data the historical damage information
* @param target the `Player` object to be affected by these damage values (at some point)
*/
def InfantryApplication(damageValues: (Int, Int), data: ResolvedProjectile)(target: Any): ResolvedProjectile = {
def InfantryApplication(damageValues: (Int, Int), data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = {
val targetBefore = SourceEntry(target)
target match {
case player: Player =>
var (a, b) = damageValues
var result = (0, 0)
if (player.isAlive && !(a == 0 && b == 0)) {
if (player.Capacitor.toInt > 0 && player.isShielded) {
// Subtract armour damage from capacitor
result = SubtractWithRemainder(player.Capacitor.toInt, b)
player.Capacitor = result._1.toFloat
b = result._2
// Then follow up with health damage if any capacitor is left
result = SubtractWithRemainder(player.Capacitor.toInt, a)
player.Capacitor = result._1.toFloat
a = result._2
}
player.avatar.implants.flatten.find(x => x.definition.implantType == ImplantType.PersonalShield) match {
case Some(implant) if implant.active => {
// Subtract armour damage from stamina
result = SubtractWithRemainder(player.avatar.stamina, b)
player.avatar = player.avatar.copy(stamina = result._1)
val originalHealth = player.Health
if (data.cause.source.DamageToHealthOnly) {
player.Health = SubtractWithRemainder(player.Health, a)._1
} else {
var result = (0, 0)
if (player.Capacitor.toInt > 0 && player.isShielded) {
// Subtract armour damage from capacitor
result = SubtractWithRemainder(player.Capacitor.toInt, b)
player.Capacitor = result._1.toFloat
b = result._2
// Then follow up with health damage if any stamina is left
result = SubtractWithRemainder(player.avatar.stamina, a)
player.avatar = player.avatar.copy(stamina = result._1)
// Then follow up with health damage if any capacitor is left
result = SubtractWithRemainder(player.Capacitor.toInt, a)
player.Capacitor = result._1.toFloat
a = result._2
}
case _ => ;
player.avatar.implants.flatten.find(x => x.definition.implantType == ImplantType.PersonalShield) match {
case Some(implant) if implant.active =>
// Subtract armour damage from stamina
result = SubtractWithRemainder(player.avatar.stamina, b)
player.avatar = player.avatar.copy(stamina = result._1)
b = result._2
// Then follow up with health damage if any stamina is left
result = SubtractWithRemainder(player.avatar.stamina, a)
player.avatar = player.avatar.copy(stamina = result._1)
a = result._2
case _ => ;
}
// Subtract any remaining armour damage from armour
result = SubtractWithRemainder(player.Armor, b)
player.Armor = result._1
b = result._2
// Then bleed through to health if armour ran out
result = SubtractWithRemainder(player.Health, b)
player.Health = result._1
b = result._2
// Finally, apply health damage to health
result = SubtractWithRemainder(player.Health, a)
player.Health = result._1
//if b > 0 (armor) or result._2 > 0 (health), then we did the math wrong
}
// Subtract any remaining armour damage from armour
result = SubtractWithRemainder(player.Armor, b)
player.Armor = result._1
b = result._2
val originalHealth = player.Health
// Then bleed through to health if armour ran out
result = SubtractWithRemainder(player.Health, b)
player.Health = result._1
b = result._2
// Finally, apply health damage to health
result = SubtractWithRemainder(player.Health, a)
player.Health = result._1
a = result._2
// If any health damage was applied also drain an amount of stamina equal to half the health damage
if (player.Health < originalHealth) {
val delta = originalHealth - player.Health
@ -207,17 +211,18 @@ object ResolutionCalculations {
}
case _ =>
}
data
DamageResult(targetBefore, SourceEntry(target), data)
}
/**
* The expanded `(Any)=>Unit` function for vehicles.
* Apply the damage value to the shield field and then the health field (that order) for a vehicle target.
* @param damage the raw damage
* @param data the historical `ResolvedProjectile` information
* @param data the historical damage information
* @param target the `Vehicle` object to be affected by these damage values (at some point)
*/
def VehicleApplication(damage: Int, data: ResolvedProjectile)(target: Any): ResolvedProjectile = {
def VehicleApplication(damage: Int, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = {
val targetBefore = SourceEntry(target)
target match {
case vehicle: Vehicle if CanDamage(vehicle, damage, data) =>
val shields = vehicle.Shields
@ -231,36 +236,22 @@ object ResolutionCalculations {
}
case _ => ;
}
data
DamageResult(targetBefore, SourceEntry(target), data)
}
def SimpleApplication(damage: Int, data: ResolvedProjectile)(target: Any): ResolvedProjectile = {
def SimpleApplication(damage: Int, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = {
val targetBefore = SourceEntry(target)
target match {
case obj: Deployable if CanDamage(obj, damage, data) =>
obj.Health -= damage
case turret: FacilityTurret if CanDamage(turret, damage, data) =>
turret.Health -= damage
case amenity: Amenity if CanDamage(amenity, damage, data) =>
amenity.Health -= damage
case entity: Vitality if CanDamage(entity, damage, data) =>
entity.Health -= damage
case _ => ;
}
data
DamageResult(targetBefore, SourceEntry(target), data)
}
def ComplexDeployableApplication(damage: Int, data: ResolvedProjectile)(target: Any): ResolvedProjectile = {
def ComplexDeployableApplication(damage: Int, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = {
val targetBefore = SourceEntry(target)
target match {
case ce: ComplexDeployable if CanDamage(ce, damage, data) =>
if (ce.Shields > 0) {
if (damage > ce.Shields) {
ce.Health -= (damage - ce.Shields)
ce.Shields = 0
} else {
ce.Shields -= damage
}
} else {
ce.Health -= damage
}
case ce: TurretDeployable if CanDamage(ce, damage, data) =>
if (ce.Shields > 0) {
if (damage > ce.Shields) {
@ -273,8 +264,63 @@ object ResolutionCalculations {
ce.Health -= damage
}
case ce: ComplexDeployable if CanDamage(ce, damage, data) =>
if (ce.Shields > 0) {
if (damage > ce.Shields) {
ce.Health -= (damage - ce.Shields)
ce.Shields = 0
} else {
ce.Shields -= damage
}
} else {
ce.Health -= damage
}
case _ => ;
}
data
DamageResult(targetBefore, SourceEntry(target), data)
}
def WildcardCalculations(data: DamageInteraction): (Int, Int) => Any = {
data.target match {
case p: PlayerSource =>
if(p.ExoSuit == ExoSuitType.MAX) MaxDamage(data)
else InfantryDamage(data)
case _ =>
VehicleDamageAfterResist(data)
}
}
def WildcardApplication(damage: Any, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = {
target match {
case _: Player =>
val dam : (Int, Int) = damage match {
case (a: Int, b: Int) => (a, b)
case a: Int => (a, 0)
case _ => (0, 0)
}
InfantryApplication(dam, data)(target)
case _: Vehicle =>
val dam : Int = damage match {
case a: Int => a
case _ => 0
}
VehicleApplication(dam, data)(target)
case _: ComplexDeployable =>
val dam : Int = damage match {
case a: Int => a
case _ => 0
}
ComplexDeployableApplication(dam, data)(target)
case _ =>
val dam : Int = damage match {
case a: Int => a
case _ => 0
}
SimpleApplication(dam, data)(target)
}
}
}

View file

@ -1,16 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resolution
/**
* Maintain information about four target types as the entry points for damage calculation.
*/
trait ResolutionSelection {
def Infantry: ResolutionCalculations.Form
def Max: ResolutionCalculations.Form
def Vehicle: ResolutionCalculations.Form
def Aircraft: ResolutionCalculations.Form
def SimpleDeployables: ResolutionCalculations.Form
def ComplexDeployables: ResolutionCalculations.Form
def FacilityTurrets: ResolutionCalculations.Form
def Amenities: ResolutionCalculations.Form
}

View file

@ -5,7 +5,7 @@ import akka.actor.{ActorContext, ActorRef, Props}
import akka.routing.RandomPool
import net.psforever.objects.ballistics.{Projectile, SourceEntry}
import net.psforever.objects._
import net.psforever.objects.ce.Deployable
import net.psforever.objects.ce.{ComplexDeployable, Deployable, SimpleDeployable}
import net.psforever.objects.entity.IdentifiableEntity
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.guid.{NumberPoolHub, TaskResolver}
@ -38,8 +38,12 @@ import akka.actor.typed
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.vehicles.UtilityType
import net.psforever.objects.vital.etc.ExplodingEntityReason
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
/**
* A server object representing the one-landmass planets as well as the individual subterranean caverns.<br>
@ -923,7 +927,35 @@ object Zone {
}
object HotSpot {
final case class Activity(defender: SourceEntry, attacker: SourceEntry, location: Vector3)
trait Activity {
def defender: SourceEntry
def attacker: SourceEntry
def location: Vector3
}
final case class Conflict(defender: SourceEntry, attacker: SourceEntry, location: Vector3) extends Activity
final case class NonEvent() extends Activity {
def defender: SourceEntry = SourceEntry.None
def attacker: SourceEntry = SourceEntry.None
def location: Vector3 = Vector3.Zero
}
object Activity {
def apply(data: DamageResult): Activity = {
data.adversarial match {
case Some(adversity) => Conflict(adversity.defender, adversity.attacker, data.interaction.hitPos)
case None => NonEvent()
}
}
def apply(defender: SourceEntry, attacker: SourceEntry, location: Vector3): Activity =
Conflict(defender, attacker, location)
}
final case class Cleanup()
@ -1034,4 +1066,124 @@ object Zone {
}
}
}
/**
* Allocates `Damageable` targets within the radius of a server-prepared explosion
* and informs those entities that they have affected by the aforementioned explosion.
* @see `Amenity.Owner`
* @see `ComplexDeployable`
* @see `DamageInteraction`
* @see `DamageResult`
* @see `DamageWithPosition`
* @see `ExplodingEntityReason`
* @see `SimpleDeployable`
* @see `VitalityDefinition`
* @see `VitalityDefinition.innateDamage`
* @see `Zone.Buildings`
* @see `Zone.DeployableList`
* @see `Zone.LivePlayers`
* @see `Zone.LocalEvents`
* @see `Zone.Vehicles`
* @param zone the zone in which the explosion should occur
* @param obj the entity that embodies the explosion (information)
* @param instigation whatever prior action triggered the entity to explode, if anything
* @param detectionTest a custom test to determine if any given target is affected;
* defaults to an internal test for simple radial proximity
* @return a list of affected entities;
* only mostly complete due to the exclusion of objects whose damage resolution is different than usual
*/
def causeExplosion(
zone: Zone,
obj: PlanetSideGameObject with Vitality,
instigation: Option[DamageResult],
detectionTest: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck
): List[PlanetSideServerObject] = {
obj.Definition.innateDamage match {
case Some(explosion) if obj.Definition.explodes =>
//useful in this form
val sourcePosition = obj.Position
val sourcePositionXY = sourcePosition.xy
val radius = explosion.DamageRadius * explosion.DamageRadius
//collect all targets that can be damaged
//players
val playerTargets = zone.LivePlayers.filterNot { _.VehicleSeated.nonEmpty }
//vehicles
val vehicleTargets = zone.Vehicles.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
//deployables
val (simpleDeployableTargets, complexDeployableTargets) =
zone.DeployableList
.filterNot { _.Destroyed }
.foldRight((List.empty[SimpleDeployable], List.empty[ComplexDeployable])) { case (f, (simp, comp)) =>
f match {
case o: SimpleDeployable => (simp :+ o, comp)
case o: ComplexDeployable => (simp, comp :+ o)
case _ => (simp, comp)
}
}
//amenities
val soiTargets = obj match {
case o: Amenity =>
//fortunately, even where soi overlap, amenities in different buildings are never that close to each other
o.Owner.Amenities
case _ =>
zone.Buildings.values
.filter { b =>
val soiRadius = b.Definition.SOIRadius * b.Definition.SOIRadius
Vector3.DistanceSquared(sourcePositionXY, b.Position.xy) < soiRadius || soiRadius <= radius
}
.flatMap { _.Amenities }
.filter { _.Definition.Damageable }
}
//restrict to targets in the damage radius
val allAffectedTargets = (playerTargets ++ vehicleTargets ++ complexDeployableTargets ++ soiTargets)
.filter { target =>
(target ne obj) && detectionTest(obj, target, radius)
}
//inform remaining targets that they have suffered an explosion
allAffectedTargets
.foreach { target =>
target.Actor ! Vitality.Damage(
DamageInteraction(
SourceEntry(target),
ExplodingEntityReason(obj, target.DamageModel, instigation),
target.Position
).calculate()
)
}
//important note - these are not returned as targets that were affected
simpleDeployableTargets
.filter { target =>
(target ne obj) && detectionTest(obj, target, radius)
}
.foreach { target =>
zone.LocalEvents ! Vitality.DamageOn(
target,
DamageInteraction(
SourceEntry(target),
ExplodingEntityReason(obj, target.DamageModel, instigation),
target.Position
).calculate()
)
}
allAffectedTargets
case None =>
Nil
}
}
/**
* Two game entities are considered "near" each other if they are within a certain distance of one another.
* A default function literal mainly used for `causesExplosion`.
* @see `causeExplosion`
* @see `Vector3.DistanceSquare`
* @param obj1 a game entity
* @param obj2 a game entity
* @param maxDistance the square of the maximum distance permissible between game entities
* before they are no longer considered "near"
* @return `true`, if the target entities are near to each other;
* `false`, otherwise
*/
private def distanceCheck(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = {
Vector3.DistanceSquared(obj1.Position, obj2.Position) <= maxDistance
}
}

View file

@ -151,7 +151,7 @@ class ZoneHotSpotProjector(zone: Zone, hotspots: ListBuffer[HotSpotInfo], blanki
import scala.concurrent.ExecutionContext.Implicits.global
blanking = context.system.scheduler.scheduleOnce(blankingDelay, self, ZoneHotSpotProjector.BlankingPhase())
case Zone.HotSpot.Activity(defender, attacker, location) =>
case Zone.HotSpot.Conflict(defender, attacker, location) =>
log.trace(s"received information about activity in ${zone.id}@$location")
val defenderFaction = defender.Faction
val attackerFaction = attacker.Faction
@ -194,7 +194,7 @@ class ZoneHotSpotProjector(zone: Zone, hotspots: ListBuffer[HotSpotInfo], blanki
//blanking dated activity reports
val changed = hotspots.flatMap(spot => {
spot.Activity.collect {
case (b, a) if a.LastReport + a.Duration.toNanos <= curr =>
case (b, a: ActivityReport) if a.LastReport + a.Duration.toNanos <= curr =>
a.Clear() //this faction has no more activity in this sector
(b, spot)
}

View file

@ -16,7 +16,7 @@ import shapeless.{::, HNil}
* Instead, all maps use a 0 - 8192 coordinate overlay.
* @param x the x-coord of the center of the hotspot
* @param y the y-coord of the center of the hotspot
* @param scale how big the hotspot explosion icon appears
* @param scale how big the hotspot sunburst icon appears
*/
final case class HotSpotInfo(x: Float, y: Float, scale: Float)

View file

@ -82,14 +82,14 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
val zoneActors: mutable.Map[String, (ActorRef[ZoneActor.Command], Zone)] = mutable.Map(
_zones.map {
case zone =>
zone =>
val zoneActor = context.spawn(ZoneActor(zone), s"zone-${zone.id}")
(zone.id, (zoneActor, zone))
}.toSeq: _*
)
val zones = zoneActors.map {
case (id, (_, zone)) => zone
val zones: Iterable[Zone] = zoneActors.map {
case (_, (_, zone: Zone)) => zone
}
override def onMessage(msg: Command): Behavior[Command] = {
@ -154,7 +154,7 @@ class InterstellarClusterService(context: ActorContext[InterstellarClusterServic
case GetRandomSpawnPoint(zoneNumber, faction, spawnGroups, replyTo) =>
val response = zones.find(_.Number == zoneNumber) match {
case Some(zone) =>
case Some(zone: Zone) =>
/*
val location = math.abs(Random.nextInt() % 4) match {
case 0 => Vector3(sanctuary.map.Scale.width, sanctuary.map.Scale.height, 0) //NE

View file

@ -1,9 +1,8 @@
// Copyright (c) 2017 PSForever
package net.psforever.services
import akka.actor.{Actor, ActorIdentity, ActorRef, ActorSystem, Identify, Props}
import akka.actor.{Actor, ActorIdentity, ActorRef, ActorSystem, Identify, InvalidActorNameException, Props, typed}
import akka.actor.typed.scaladsl.adapter._
import akka.actor.typed
import akka.actor.typed.receptionist.Receptionist
import scala.collection.mutable
@ -35,6 +34,7 @@ class ServiceManager extends Actor {
var nextLookupId: Long = 0
val lookups: mutable.LongMap[RequestEntry] = mutable.LongMap()
val retainedRequests: mutable.HashMap[String, Set[ActorRef]] = mutable.HashMap()
override def preStart() = {
log.info("Starting...")
@ -43,7 +43,32 @@ class ServiceManager extends Actor {
def receive = {
case Register(props, name) =>
log.info(s"Registered $name service")
context.actorOf(props, name)
try {
val ref = context.actorOf(props, name)
val result = LookupResult(name, ref)
//handle logged premature requests
retainedRequests.remove(name) match {
case Some(oldRequests) =>
oldRequests.foreach {
_ ! result
}
case None => ;
}
//handle active requests that will probably miss
val poorlytTimedRequests = lookups.filter {
_._2.request.equals(name)
}
poorlytTimedRequests.foreach { case (id, entry) =>
entry.responder ! result
lookups.remove(id)
}
}
catch {
case e: InvalidActorNameException => //if an entry already exists, no harm, no foul, just don't do it again
log.warn(s"service manager says: ${e.getMessage}")
case _ => ;
}
case Lookup(name) =>
context.actorSelection(name) ! Identify(nextLookupId)
lookups += nextLookupId -> RequestEntry(name, sender())
@ -60,22 +85,21 @@ class ServiceManager extends Actor {
case Some(RequestEntry(name, sender)) =>
sender ! LookupResult(name, ref)
lookups.remove(idNumber)
case _ =>
//TODO something
case _ => ;
}
case ActorIdentity(id, None) =>
val idNumber = id.asInstanceOf[Long]
lookups.get(idNumber) match {
case Some(RequestEntry(name, _)) =>
log.error(s"request #$idNumber for service `$name` came back empty; it may not exist")
case Some(RequestEntry(name, sender)) =>
log.error(s"service manager says: request #$idNumber for service `$name` came back empty; it may not exist")
lookups.remove(idNumber)
case _ =>
//TODO something
retainedRequests(name) = retainedRequests.getOrElse(name, Set[ActorRef]()) ++ Set(sender)
case _ => ;
}
case default =>
log.error(s"invalid message received - $default")
log.error(s"service manager says: invalid message received - $default")
}
protected case class RequestEntry(request: String, responder: ActorRef)

View file

@ -378,7 +378,7 @@ class LocalService(zone: Zone) extends Actor {
}
//synchronized damage calculations
case Vitality.DamageOn(target: Deployable, damage_func) =>
case Vitality.DamageOn(target: PlanetSideGameObject with Deployable, damage_func) =>
val cause = damage_func(target)
sender() ! Vitality.DamageResolution(target, cause)

View file

@ -329,10 +329,68 @@ object Vector3 {
)
}
def PlanarRotateAroundPoint(point: Vector3, center: Vector3, radians: Float): Vector3 = {
val x = (Math.cos(radians) * (point.x - center.x) - Math.sin(radians) * (point.y - center.y) + center.x).toFloat
val y = (Math.sin(radians) * (point.x - center.x) + Math.cos(radians) * (point.y - center.y) + center.y).toFloat
/**
* Perform a standard z-axis rotation of a `Vector3` element representing a point in space
* around a `Vector3` element representing a point representing an axis of rotation
* where the angle of rotation is assumed in radians.
* This follows number circle rotation (counterclockwise) instead of compass rose rotation (clockwise).
* It can not be substituted with `(Rz(point - axis, radians) + center).xy + z(point.z)`.
* @see `Vector3.Rz(Vector3, Double)`
* @param point a mathematical vector representing a point in space
* @param axis a mathematical vector representing an axis of rotation
* @param radians a rotation angle, in radians
* @return the rotated point
*/
def PlanarRotateAroundPoint(point: Vector3, axis: Vector3, radians: Float): Vector3 = {
val cos = math.cos(radians).toFloat
val sin = math.sin(radians).toFloat
val dx = point.x - axis.x
val dy = point.y - axis.y
val x = cos * dx - sin * dy + axis.x
val y = sin * dx + cos * dy + axis.y
Vector3(
closeToInsignificance(x),
closeToInsignificance(y),
point.z
)
}
Vector3(x, y, point.z)
/**
* Find the center between two points.
* @param p1 the first point
* @param p2 the second point
* @return the point that is the mean position directly between the first point and the second point
*/
def midpoint(p1: Vector3, p2: Vector3): Vector3 = {
Vector3((p1.x + p2.x) / 2, (p1.y + p2.y) / 2, (p1.z + p2.z) / 2)
}
/**
* Given a `Vector3` element composed of Euler angles,
* find a standard unit vector that points in the direction of "up" after rotating by the Euler angles.
* Compass direction rules apply (North is 0 degrees, East is 90 degrees, etc.).
* @see `Vector3.relativeUp(Vector3, Vector3)`
* @param orient three Euler angles representing rotation
* @return a mathematical vector representing a relative "up" direction
*/
def relativeUp(orient: Vector3): Vector3 = {
relativeUp(orient, Vector3(0,0,1)) //world up
}
/**
* Given a `Vector3` element composed of Euler angles
* and a `Vector3` element in the direction of "up",
* find a standard unit vector that points in the direction of "up" after rotating by the Euler angles.
* Compass direction rules apply (North is 0 degrees, East is 90 degrees, etc.).
* @see `Vector3.Rx(Float)`
* @see `Vector3.Ry(Float)`
* @see `Vector3.Rz(Float)`
* @param orient three Euler angles representing rotation
* @param up a mathematical vector representing "up"
* @return a mathematical vector representing a relative "up" direction
*/
def relativeUp(orient: Vector3, up: Vector3): Vector3 = {
//TODO is the missing calculation before Rz(Rx(Ry(v, x), y), z) or after Rz(Ry(Rx(v, y), x), z)?
Rz(Rx(up, orient.y), (orient.z + 180) % 360f)
}
}

View file

@ -240,5 +240,35 @@ class Vector3Test extends Specification {
val A: Vector3 = Vector3(1, 0, 0)
A.Rz(45) mustEqual Vector3(0.70710677f, 0.70710677f, 0)
}
"find a relative up (identity)" in {
val euler: Vector3 = Vector3(0, 0, 0)
Vector3.relativeUp(euler) mustEqual Vector3(0,0,1)
}
"find a relative up (z-rot)" in {
val up = Vector3(0,0,1)
Vector3.relativeUp(Vector3(0, 0, 0)) mustEqual up
Vector3.relativeUp(Vector3(0, 0, 90)) mustEqual up
Vector3.relativeUp(Vector3(0, 0, 180)) mustEqual up
Vector3.relativeUp(Vector3(0, 0, 270)) mustEqual up
Vector3.relativeUp(Vector3(0, 0, 360)) mustEqual up
}
"find a relative up (y-rot)" in {
Vector3.relativeUp(Vector3(0, 0, 0)) mustEqual Vector3(0,0,1) //up
Vector3.relativeUp(Vector3(0, 90, 0)) mustEqual Vector3(0,-1,0) //north
Vector3.relativeUp(Vector3(0, 180, 0)) mustEqual Vector3(0,0,-1) //down
Vector3.relativeUp(Vector3(0, 270, 0)) mustEqual Vector3(0,1,0) //south
Vector3.relativeUp(Vector3(0, 360, 0)) mustEqual Vector3(0,0,1) //up
}
"find a relative up (combined y,z)" in {
Vector3.relativeUp(Vector3(0, 0, 90)) mustEqual Vector3(0,0,1) //up
Vector3.relativeUp(Vector3(0, 90, 90)) mustEqual Vector3(-1,0,0) //west
Vector3.relativeUp(Vector3(0, 180, 90)) mustEqual Vector3(0,0,-1) //down
Vector3.relativeUp(Vector3(0, 270, 90)) mustEqual Vector3(1,0,0) //east
Vector3.relativeUp(Vector3(0, 360, 90)) mustEqual Vector3(0,0,1) //up
}
}
}

View file

@ -6,14 +6,17 @@ import akka.testkit.TestProbe
import base.FreedContextActorTest
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.ballistics.{Projectile, ProjectileResolution, ResolvedProjectile, SourceEntry}
import net.psforever.objects.ballistics.{Projectile, SourceEntry}
import net.psforever.objects.guid.NumberPoolHub
import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.serverobject.resourcesilo.{ResourceSilo, ResourceSiloControl}
import net.psforever.objects.serverobject.structures.{AutoRepairStats, Building, StructureType}
import net.psforever.objects.serverobject.terminals.{OrderTerminalDefinition, Terminal, TerminalControl}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.{Zone, ZoneMap}
import net.psforever.objects.{GlobalDefinitions, Player, Tool}
import net.psforever.services.galaxy.GalaxyService
@ -26,53 +29,51 @@ class AutoRepairFacilityIntegrationTest extends FreedContextActorTest {
import akka.actor.typed.scaladsl.adapter._
system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id)
ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy")
expectNoMessage(200 milliseconds)
expectNoMessage(1000 milliseconds)
val guid = new NumberPoolHub(new MaxNumberSource(max = 10))
val avatarProbe = new TestProbe(system)
val zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
GUID(guid)
override def AvatarEvents = avatarProbe.ref
}
val building = Building.Structure(StructureType.Facility)(name = "integ-fac-test-building", guid = 6, map_id = 0, zone, context)
building.Invalidate()
val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
player.Spawn()
val weapon = new Tool(GlobalDefinitions.suppressor)
val terminal = new Terminal(AutoRepairIntegrationTest.terminal_definition)
val silo = new ResourceSilo()
val guid = new NumberPoolHub(new MaxNumberSource(max = 10))
val zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
GUID(guid)
}
val avatarProbe = new TestProbe(system)
zone.AvatarEvents = avatarProbe.ref
guid.register(player, number = 1)
guid.register(weapon, number = 2)
guid.register(weapon.AmmoSlot.Box, number = 3)
guid.register(terminal, number = 4)
guid.register(silo, number = 5)
val building = Building.Structure(StructureType.Facility)(name = "test-building", guid = 6, map_id = 0, zone, context)
building.Invalidate()
guid.register(building, number = 6)
building.Amenities = silo
building.Amenities = terminal
terminal.Actor = context.actorOf(Props(classOf[TerminalControl], terminal), name = "test-terminal")
silo.NtuCapacitor = 1000
silo.Actor = system.actorOf(Props(classOf[ResourceSiloControl], silo), "test-silo")
silo.Actor ! "startup"
building.Actor ! BuildingActor.PowerOn() //artificial
val wep_fmode = weapon.FireMode
val wep_prof = wep_fmode.Add
val proj = weapon.Projectile
val proj_prof = proj.asInstanceOf[DamageProfile]
val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resolved = DamageInteraction(
SourceEntry(terminal),
terminal.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
terminal.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
"AutoRepair" should {
"should activate on damage and trade NTU from the facility's resource silo for repairs" in {
@ -80,7 +81,7 @@ class AutoRepairFacilityIntegrationTest extends FreedContextActorTest {
assert(terminal.Health == terminal.MaxHealth)
terminal.Actor ! Vitality.Damage(applyDamageTo)
avatarProbe.receiveOne(max = 200 milliseconds) //health update event
avatarProbe.receiveOne(max = 1000 milliseconds) //health update event
assert(terminal.Health < terminal.MaxHealth)
var i = 0 //safety counter
while(terminal.Health < terminal.MaxHealth && i < 100) {
@ -97,45 +98,47 @@ class AutoRepairTowerIntegrationTest extends FreedContextActorTest {
import akka.actor.typed.scaladsl.adapter._
system.spawn(InterstellarClusterService(Nil), InterstellarClusterService.InterstellarClusterServiceKey.id)
ServiceManager.boot(system) ! ServiceManager.Register(Props[GalaxyService](), "galaxy")
expectNoMessage(200 milliseconds)
expectNoMessage(1000 milliseconds)
val guid = new NumberPoolHub(new MaxNumberSource(max = 10))
val avatarProbe = new TestProbe(system)
val zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
GUID(guid)
override def AvatarEvents = avatarProbe.ref
}
val building = Building.Structure(StructureType.Tower)(name = "integ-twr-test-building", guid = 6, map_id = 0, zone, context)
building.Invalidate()
val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
player.Spawn()
val weapon = new Tool(GlobalDefinitions.suppressor)
val terminal = new Terminal(AutoRepairIntegrationTest.terminal_definition)
val guid = new NumberPoolHub(new MaxNumberSource(max = 10))
val zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
GUID(guid)
}
val avatarProbe = new TestProbe(system)
zone.AvatarEvents = avatarProbe.ref
terminal.Actor = context.actorOf(Props(classOf[TerminalControl], terminal), name = "test-terminal")
guid.register(player, number = 1)
guid.register(weapon, number = 2)
guid.register(weapon.AmmoSlot.Box, number = 3)
guid.register(terminal, number = 4)
val building = Building.Structure(StructureType.Tower)(name = "test-building", guid = 6, map_id = 0, zone, context)
building.Invalidate()
guid.register(building, number = 6)
building.Amenities = terminal
terminal.Actor = context.actorOf(Props(classOf[TerminalControl], terminal), name = "test-terminal")
building.Amenities = terminal
building.Actor ! BuildingActor.SuppliedWithNtu() //artificial
building.Actor ! BuildingActor.PowerOn() //artificial
val wep_fmode = weapon.FireMode
val wep_prof = wep_fmode.Add
val proj = weapon.Projectile
val proj_prof = proj.asInstanceOf[DamageProfile]
val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resolved = DamageInteraction(
SourceEntry(terminal),
terminal.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
terminal.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
"AutoRepair" should {
"should activate on damage and trade NTU from the tower for repairs" in {

View file

@ -7,13 +7,16 @@ import base.FreedContextActorTest
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.ballistics.{Projectile, ProjectileResolution, ResolvedProjectile, SourceEntry}
import net.psforever.objects.ballistics.{Projectile, SourceEntry}
import net.psforever.objects.guid.NumberPoolHub
import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.serverobject.structures.{AutoRepairStats, Building, StructureType}
import net.psforever.objects.serverobject.terminals.{OrderTerminalDefinition, Terminal, TerminalControl}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.{Zone, ZoneMap}
import net.psforever.objects.{GlobalDefinitions, Player, Tool}
import net.psforever.services.ServiceManager
@ -54,14 +57,17 @@ class AutoRepairRequestNtuTest extends FreedContextActorTest {
val proj = weapon.Projectile
val proj_prof = proj.asInstanceOf[DamageProfile]
val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resolved = DamageInteraction(
DamageResolution.Hit,
SourceEntry(terminal),
terminal.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
terminal.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
"AutoRepair" should {
"asks owning building for NTU after damage" in {
@ -114,14 +120,17 @@ class AutoRepairRequestNtuRepeatTest extends FreedContextActorTest {
val proj = weapon.Projectile
val proj_prof = proj.asInstanceOf[DamageProfile]
val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resolved = DamageInteraction(
DamageResolution.Hit,
SourceEntry(terminal),
terminal.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
terminal.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
"AutoRepair" should {
"repeatedly asks owning building for NTU after damage" in {
@ -176,14 +185,17 @@ class AutoRepairNoRequestNtuTest extends FreedContextActorTest {
val proj = weapon.Projectile
val proj_prof = proj.asInstanceOf[DamageProfile]
val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resolved = DamageInteraction(
DamageResolution.Hit,
SourceEntry(terminal),
terminal.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
terminal.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
"AutoRepair" should {
"not ask for NTU after damage if it expects no NTU" in {
@ -231,14 +243,17 @@ class AutoRepairRestoreRequestNtuTest extends FreedContextActorTest {
val proj = weapon.Projectile
val proj_prof = proj.asInstanceOf[DamageProfile]
val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resolved = DamageInteraction(
DamageResolution.Hit,
SourceEntry(terminal),
terminal.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
terminal.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
"AutoRepair" should {
"ask for NTU after damage if its expectation of NTU is restored" in {
@ -295,14 +310,17 @@ class AutoRepairRepairWithNtuTest extends FreedContextActorTest {
val proj = weapon.Projectile
val proj_prof = proj.asInstanceOf[DamageProfile]
val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resolved = DamageInteraction(
DamageResolution.Hit,
SourceEntry(terminal),
terminal.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
terminal.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
"AutoRepair" should {
"repair some of the damage when it receives NTU" in {
@ -354,14 +372,17 @@ class AutoRepairRepairWithNtuUntilDoneTest extends FreedContextActorTest {
val proj = weapon.Projectile
val proj_prof = proj.asInstanceOf[DamageProfile]
val projectile = Projectile(proj, weapon.Definition, wep_fmode, player, Vector3(2, 0, 0), Vector3.Zero)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resolved = DamageInteraction(
DamageResolution.Hit,
SourceEntry(terminal),
terminal.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
terminal.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
"AutoRepair" should {
"ask for NTU after damage and repair some of the damage when it receives NTU, until fully-repaired" in {

View file

@ -2,7 +2,8 @@
package objects
import net.psforever.objects._
import net.psforever.objects.vital.damage.{DamageCalculations, DamageModifiers, DamageProfile}
import net.psforever.objects.vital.damage.{DamageProfile, _}
import net.psforever.objects.vital.projectile._
import DamageCalculations._
import net.psforever.objects.vital.resistance.ResistanceCalculations
import ResistanceCalculations._
@ -10,11 +11,13 @@ import net.psforever.objects.vital.resolution.ResolutionCalculations
import ResolutionCalculations._
import net.psforever.objects.ballistics._
import net.psforever.objects.definition.{ProjectileDefinition, VehicleDefinition}
import net.psforever.objects.vital.{DamageType, Vitality}
import net.psforever.objects.vital.Vitality
import net.psforever.packet.game.objectcreate.ObjectClass
import net.psforever.types._
import org.specs2.mutable.Specification
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.vital.base._
import net.psforever.objects.vital.interaction.DamageInteraction
class DamageCalculationsTests extends Specification {
"DamageCalculations" should {
@ -27,11 +30,13 @@ class DamageCalculationsTests extends Specification {
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
val target = Vehicle(GlobalDefinitions.fury)
target.Position = Vector3(10, 0, 0)
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target.DamageModel
),
Vector3(15, 0, 0)
)
@ -60,173 +65,198 @@ class DamageCalculationsTests extends Specification {
}
"no degrade damage modifier" in {
DamageModifiers.SameHit.Calculate(100, resprojectile) mustEqual 100
SameHit.calculate(100, resprojectile) mustEqual 100
}
"degrade over distance damage modifier (no degrade)" in {
DamageModifiers.DistanceDegrade.Calculate(100, resprojectile) == 100 mustEqual true
DistanceDegrade.calculate(100, resprojectile) == 100 mustEqual true
}
"cut off damage at max distance (no cutoff)" in {
DamageModifiers.MaxDistanceCutoff.Calculate(100, resprojectile) == 100 mustEqual true
MaxDistanceCutoff.calculate(100, resprojectile) == 100 mustEqual true
}
"cut off damage at max distance (cutoff)" in {
val cutoffprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val cutoffprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target.DamageModel
),
Vector3(1500, 0, 0)
)
DamageModifiers.MaxDistanceCutoff.Calculate(100, cutoffprojectile) == 0 mustEqual true
val damage = MaxDistanceCutoff.calculate(100, cutoffprojectile)
damage == 0 mustEqual true
}
"cut off damage at custom distance (no cutoff)" in {
DamageModifiers.CustomDistanceCutoff(10).Calculate(100, resprojectile) == 0 mustEqual true
CustomDistanceCutoff(10).calculate(100, resprojectile) == 0 mustEqual true
}
"cut off damage at custom distance (cutoff)" in {
val coffprojectile = ResolvedProjectile(
ProjectileResolution.Splash,
projectile,
val coffprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Splash,
projectile,
target.DamageModel
),
Vector3(10, 0, 0)
)
DamageModifiers.CustomDistanceCutoff(2).Calculate(100, coffprojectile) == 0 mustEqual true
CustomDistanceCutoff(2).calculate(100, coffprojectile) == 0 mustEqual true
}
"degrade over distance damage modifier (some degrade)" in {
val resprojectile2 = ResolvedProjectile(
ProjectileResolution.Splash,
projectile,
val resprojectile2 = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Splash,
projectile,
target.DamageModel
),
Vector3(100, 0, 0)
)
val damage = DamageModifiers.DistanceDegrade.Calculate(100, resprojectile2)
val damage = DistanceDegrade.calculate(100, resprojectile2)
damage < 100 && damage > 0 mustEqual true
}
"degrade over distance damage modifier (zero'd)" in {
val resprojectile2 = ResolvedProjectile(
ProjectileResolution.Splash,
projectile,
val resprojectile2 = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Splash,
projectile,
target.DamageModel
),
Vector3(1000, 0, 0)
)
DamageModifiers.DistanceDegrade.Calculate(100, resprojectile2) == 0 mustEqual true
DistanceDegrade.calculate(100, resprojectile2) == 0 mustEqual true
}
"degrade at radial distance damage modifier (no degrade)" in {
val resprojectile2 = ResolvedProjectile(
ProjectileResolution.Splash,
projectile,
val resprojectile2 = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Splash,
projectile,
target.DamageModel
),
Vector3(10, 0, 0)
)
DamageModifiers.RadialDegrade.Calculate(100, resprojectile2) == 100 mustEqual true
RadialDegrade.calculate(100, resprojectile2) == 100 mustEqual true
}
"degrade at radial distance damage modifier (some degrade)" in {
val resprojectile2 = ResolvedProjectile(
ProjectileResolution.Splash,
projectile,
val resprojectile2 = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Splash,
projectile,
target.DamageModel
),
Vector3(12, 0, 0)
)
val damage = DamageModifiers.RadialDegrade.Calculate(100, resprojectile2)
val damage = RadialDegrade.calculate(100, resprojectile2)
damage < 100 && damage > 0 mustEqual true
}
"degrade at radial distance damage modifier (zero'd)" in {
val resprojectile2 = ResolvedProjectile(
ProjectileResolution.Splash,
projectile,
val resprojectile2 = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Splash,
projectile,
target.DamageModel
),
Vector3(100, 0, 0)
)
DamageModifiers.RadialDegrade.Calculate(100, resprojectile2) == 0 mustEqual true
RadialDegrade.calculate(100, resprojectile2) == 0 mustEqual true
}
"lash degrade (no lash; too close)" in {
val resprojectile2 = ResolvedProjectile(
ProjectileResolution.Lash,
projectile,
val resprojectile2 = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Lash,
projectile,
target.DamageModel
),
Vector3(5, 0, 0) //compared to Vector3(2, 2, 0)
)
DamageModifiers.Lash.Calculate(100, resprojectile2) == 0 mustEqual true
Lash.calculate(100, resprojectile2) == 0 mustEqual true
}
"lash degrade (lash)" in {
val resprojectile2 = ResolvedProjectile(
ProjectileResolution.Lash,
projectile,
val resprojectile2 = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Lash,
projectile,
target.DamageModel
),
Vector3(20, 0, 0)
)
val damage = DamageModifiers.Lash.Calculate(100, resprojectile2)
val damage = Lash.calculate(100, resprojectile2)
damage < 100 && damage > 0 mustEqual true
}
"lash degrade (no lash; too far)" in {
val resprojectile2 = ResolvedProjectile(
ProjectileResolution.Lash,
projectile,
val resprojectile2 = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Lash,
projectile,
target.DamageModel
),
Vector3(1000, 0, 0)
)
DamageModifiers.Lash.Calculate(100, resprojectile2) == 0 mustEqual true
Lash.calculate(100, resprojectile2) == 0 mustEqual true
}
"fireball aggravated damage (aggravated splash burn" in {
val burnWeapon = Tool(GlobalDefinitions.flamethrower)
val burnProjectile = Projectile(
burnWeapon.Projectile,
burnWeapon.Definition,
burnWeapon.FireMode,
player,
Vector3(2, 2, 0),
Vector3.Zero
)
val burnRes = ResolvedProjectile(
ProjectileResolution.AggravatedSplashBurn,
projectile,
// val burnWeapon = Tool(GlobalDefinitions.flamethrower)
// val burnProjectile = Projectile(
// burnWeapon.Projectile,
// burnWeapon.Definition,
// burnWeapon.FireMode,
// player,
// Vector3(2, 2, 0),
// Vector3.Zero
// )
val burnRes = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.AggravatedSplashBurn,
projectile,
target.DamageModel
),
Vector3(15, 0, 0)
)
val resistance = burnRes.damage_model.ResistUsing(burnRes)(burnRes)
DamageModifiers.FireballAggravatedBurn.Calculate(100, burnRes) == (1 + resistance) mustEqual true
val resistance = target.DamageModel.ResistUsing(burnRes)(burnRes)
FireballAggravatedBurn.calculate(100, burnRes) == (1 + resistance) mustEqual true
}
"fireball aggravated damage (noral splash, no modification)" in {
val burnWeapon = Tool(GlobalDefinitions.flamethrower)
val burnProjectile = Projectile(
burnWeapon.Projectile,
burnWeapon.Definition,
burnWeapon.FireMode,
player,
Vector3(2, 2, 0),
Vector3.Zero
)
val burnRes = ResolvedProjectile(
ProjectileResolution.Splash,
projectile,
// val burnWeapon = Tool(GlobalDefinitions.flamethrower)
// val burnProjectile = Projectile(
// burnWeapon.Projectile,
// burnWeapon.Definition,
// burnWeapon.FireMode,
// player,
// Vector3(2, 2, 0),
// Vector3.Zero
// )
val burnRes = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Splash,
projectile,
target.DamageModel
),
Vector3(15, 0, 0)
)
DamageModifiers.FireballAggravatedBurn.Calculate(100, burnRes) == 100 mustEqual true
FireballAggravatedBurn.calculate(100, burnRes) == 100 mustEqual true
}
val charge_weapon = Tool(GlobalDefinitions.spiker)
@ -243,42 +273,52 @@ class DamageCalculationsTests extends Specification {
"charge (none)" in {
val cprojectile = charge_projectile.quality(ProjectileQuality.Modified(0))
val rescprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
cprojectile,
val rescprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
cprojectile,
target.DamageModel
),
Vector3(15, 0, 0)
)
val damage = DamageModifiers.SpikerChargeDamage.Calculate(chargeBaseDamage, rescprojectile)
val calcDam = minDamageBase + math.floor(chargeBaseDamage * rescprojectile.projectile.quality.mod)
val damage = SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
val calcDam = minDamageBase + math.floor(
chargeBaseDamage * rescprojectile.cause.asInstanceOf[ProjectileReason].projectile.quality.mod
)
damage mustEqual calcDam
}
"charge (half)" in {
val cprojectile = charge_projectile.quality(ProjectileQuality.Modified(0.5f))
val rescprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
cprojectile,
val rescprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
cprojectile,
target.DamageModel
),
Vector3(15, 0, 0)
)
val damage = DamageModifiers.SpikerChargeDamage.Calculate(chargeBaseDamage, rescprojectile)
val calcDam = minDamageBase + math.floor(chargeBaseDamage * rescprojectile.projectile.quality.mod)
val damage = SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
val calcDam = minDamageBase + math.floor(
chargeBaseDamage * rescprojectile.cause.asInstanceOf[ProjectileReason].projectile.quality.mod
)
damage mustEqual calcDam
}
"charge (full)" in {
val cprojectile = charge_projectile.quality(ProjectileQuality.Modified(1))
val rescprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
cprojectile,
val rescprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
cprojectile,
target.DamageModel
),
Vector3(15, 0, 0)
)
val damage = DamageModifiers.SpikerChargeDamage.Calculate(chargeBaseDamage, rescprojectile)
val damage = SpikerChargeDamage.calculate(chargeBaseDamage, rescprojectile)
val calcDam = minDamageBase + chargeBaseDamage
damage mustEqual calcDam
}
@ -294,124 +334,142 @@ class DamageCalculationsTests extends Specification {
)
"flak hit (resolution is splash, no degrade)" in {
val resfprojectile = ResolvedProjectile(
ProjectileResolution.Splash,
flak_projectile,
val resfprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Splash,
flak_projectile,
target.DamageModel
),
Vector3(10, 0, 0)
)
val damage = DamageModifiers.FlakHit.Calculate(100, resfprojectile)
val damage = FlakHit.calculate(100, resfprojectile)
damage == 100 mustEqual true
}
"flak hit (resolution is hit, no degrade)" in {
val resfprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
flak_projectile,
val resfprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
flak_projectile,
target.DamageModel
),
Vector3(10, 0, 0)
)
val damage = DamageModifiers.FlakHit.Calculate(100, resfprojectile)
val damage = FlakHit.calculate(100, resfprojectile)
damage == 100 mustEqual true
}
"flak burst (resolution is hit)" in {
val resfprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
flak_projectile,
val resfprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
flak_projectile,
target.DamageModel
),
Vector3(15, 0, 0)
)
val damage = DamageModifiers.FlakBurst.Calculate(100, resfprojectile)
val damage = FlakBurst.calculate(100, resfprojectile)
damage == 100 mustEqual true
}
"flak burst (resolution is splash, no degrade)" in {
val resfprojectile = ResolvedProjectile(
ProjectileResolution.Splash,
flak_projectile,
val resfprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Splash,
flak_projectile,
target.DamageModel
),
Vector3(10, 0, 0)
)
val damage = DamageModifiers.FlakBurst.Calculate(100, resfprojectile)
val damage = FlakBurst.calculate(100, resfprojectile)
damage == 100 mustEqual true
}
"flak burst (resolution is splash, some degrade)" in {
val resfprojectile = ResolvedProjectile(
ProjectileResolution.Splash,
flak_projectile,
val resfprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Splash,
flak_projectile,
target.DamageModel
),
Vector3(5, 0, 0)
)
val damage = DamageModifiers.FlakBurst.Calculate(100, resfprojectile)
val damage = FlakBurst.calculate(100, resfprojectile)
damage < 100 mustEqual true
}
"galaxy gunship reduction (target is galaxy_gunship, no shields)" in {
val vehicle = Vehicle(GlobalDefinitions.galaxy_gunship)
val resfprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resfprojectile = DamageInteraction(
SourceEntry(vehicle),
vehicle.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
vehicle.DamageModel
),
Vector3(5, 0, 0)
)
val damage = DamageModifiers.GalaxyGunshipReduction(0.63f).Calculate(100, resfprojectile)
val damage = GalaxyGunshipReduction(0.63f).calculate(100, resfprojectile)
damage == 63 mustEqual true
}
"galaxy gunship reduction (target is galaxy_gunship)" in {
val vehicle = Vehicle(GlobalDefinitions.galaxy_gunship)
vehicle.Shields = 1
val resfprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resfprojectile = DamageInteraction(
SourceEntry(vehicle),
vehicle.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
vehicle.DamageModel
),
Vector3(5, 0, 0)
)
val damage = DamageModifiers.GalaxyGunshipReduction(0.63f).Calculate(100, resfprojectile)
val damage = GalaxyGunshipReduction(0.63f).calculate(100, resfprojectile)
damage == 100 mustEqual true
}
"galaxy gunship reduction (target is vehicle, but not a galaxy_gunship)" in {
val resfprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resfprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target.DamageModel
),
Vector3(5, 0, 0)
)
val damage = DamageModifiers.GalaxyGunshipReduction(0.63f).Calculate(100, resfprojectile)
val damage = GalaxyGunshipReduction(0.63f).calculate(100, resfprojectile)
damage == 100 mustEqual true
}
"galaxy gunship reduction (target is not a vehicle)" in {
val tplayer =
Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
val resfprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resfprojectile = DamageInteraction(
SourceEntry(tplayer),
tplayer.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
tplayer.DamageModel
),
Vector3(5, 0, 0)
)
val damage = DamageModifiers.GalaxyGunshipReduction(0.63f).Calculate(100, resfprojectile)
val damage = GalaxyGunshipReduction(0.63f).calculate(100, resfprojectile)
damage == 100 mustEqual true
}
"extract a complete damage profile" in {
val result1 = DamageModifiers.RadialDegrade.Calculate(
val result1 = RadialDegrade.calculate(
AgainstVehicle(proj_prof) + AgainstVehicle(wep_prof),
resprojectile
)
val result2 = DamageCalculations.DamageWithModifiers(AgainstVehicle, resprojectile)
val result2 = DamageCalculations.WithModifiers(AgainstVehicle, resprojectile)
result1 mustEqual result2
}
}
@ -427,11 +485,13 @@ class ResistanceCalculationsTests extends Specification {
"ResistanceCalculations" should {
"ignore all targets" in {
val target = Vehicle(GlobalDefinitions.fury)
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target.DamageModel
),
Vector3.Zero
)
InvalidTarget(resprojectile).isFailure mustEqual true
@ -439,11 +499,13 @@ class ResistanceCalculationsTests extends Specification {
"discern standard infantry targets" in {
val target = player
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target.DamageModel
),
Vector3.Zero
)
ValidInfantryTarget(resprojectile).isSuccess mustEqual true
@ -455,11 +517,13 @@ class ResistanceCalculationsTests extends Specification {
"discern mechanized infantry targets" in {
val target = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
target.ExoSuit = ExoSuitType.MAX
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target.DamageModel
),
Vector3.Zero
)
ValidInfantryTarget(resprojectile).isSuccess mustEqual false
@ -470,11 +534,13 @@ class ResistanceCalculationsTests extends Specification {
"discern ground vehicle targets" in {
val target = Vehicle(GlobalDefinitions.fury)
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target.DamageModel
),
Vector3.Zero
)
ValidInfantryTarget(resprojectile).isSuccess mustEqual false
@ -485,11 +551,13 @@ class ResistanceCalculationsTests extends Specification {
"discern flying vehicle targets" in {
val target = Vehicle(GlobalDefinitions.mosquito)
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target.DamageModel
),
Vector3.Zero
)
ValidInfantryTarget(resprojectile).isSuccess mustEqual false
@ -531,11 +599,13 @@ class ResolutionCalculationsTests extends Specification {
"ResolutionCalculations" should {
"calculate no damage" in {
val target = player
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target.DamageModel
),
Vector3.Zero
)
ResolutionCalculations.NoDamage(resprojectile)(50, 50) mustEqual 0
@ -543,21 +613,25 @@ class ResolutionCalculationsTests extends Specification {
"calculate no infantry damage for vehicles" in {
val target1 = Vehicle(GlobalDefinitions.fury) //!
val resprojectile1 = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile1 = DamageInteraction(
SourceEntry(target1),
target1.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target1.DamageModel
),
Vector3.Zero
)
InfantryDamage(resprojectile1)(50, 10) mustEqual (0, 0)
val target2 = player
val resprojectile2 = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile2 = DamageInteraction(
SourceEntry(target2),
target2.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target2.DamageModel
),
Vector3.Zero
)
InfantryDamage(resprojectile2)(50, 10) mustEqual (40, 10)
@ -587,21 +661,25 @@ class ResolutionCalculationsTests extends Specification {
player2.Spawn()
"calculate no max damage for vehicles" in {
val target1 = Vehicle(GlobalDefinitions.fury) //!
val resprojectile1 = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile1 = DamageInteraction(
SourceEntry(target1),
target1.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target1.DamageModel
),
Vector3.Zero
)
MaxDamage(resprojectile1)(50, 10) mustEqual (0, 0)
val target2 = player2
val resprojectile2 = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile2 = DamageInteraction(
SourceEntry(target2),
target2.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target2.DamageModel
),
Vector3.Zero
)
MaxDamage(resprojectile2)(50, 10) mustEqual (0, 40)
@ -623,21 +701,25 @@ class ResolutionCalculationsTests extends Specification {
"do not care if target is infantry for vehicle calculations" in {
val target1 = player
val resprojectile1 = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile1 = DamageInteraction(
SourceEntry(target1),
target1.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target1.DamageModel
),
Vector3.Zero
)
VehicleDamageAfterResist(resprojectile1)(50, 10) mustEqual 40
val target2 = Vehicle(GlobalDefinitions.fury) //!
val resprojectile2 = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile2 = DamageInteraction(
SourceEntry(target2),
target2.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
target2.DamageModel
),
Vector3.Zero
)
VehicleDamageAfterResist(resprojectile2)(50, 10) mustEqual 40
@ -690,14 +772,16 @@ class DamageModelTests extends Specification {
tplayer.Health mustEqual 100
tplayer.Armor mustEqual 50
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(tplayer),
tplayer.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
tplayer.DamageModel
),
Vector3.Zero
)
val func: Any => ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile)
val func = resprojectile.calculate()
func(tplayer)
tplayer.Health mustEqual 65
tplayer.Armor mustEqual 35
@ -710,15 +794,16 @@ class DamageModelTests extends Specification {
tplayer.Health mustEqual 100
tplayer.Armor mustEqual 50
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(tplayer),
tplayer.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
tplayer.DamageModel
),
Vector3.Zero
)
val func: Any => ResolvedProjectile =
resprojectile.damage_model.Calculate(resprojectile, DamageType.Splash)
val func = resprojectile.calculate(DamageType.Splash)
func(tplayer)
tplayer.Health mustEqual 65
tplayer.Armor mustEqual 35
@ -731,14 +816,16 @@ class DamageModelTests extends Specification {
tplayer.Health mustEqual 100
tplayer.Armor mustEqual 50
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(tplayer),
tplayer.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
tplayer.DamageModel
),
Vector3.Zero
)
val func: Any => ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile)
val func = resprojectile.calculate()
tplayer.Armor = 0
func(tplayer)
@ -750,14 +837,16 @@ class DamageModelTests extends Specification {
val vehicle = Vehicle(DamageModelTests.vehicle)
vehicle.Health mustEqual 650
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(vehicle),
vehicle.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
vehicle.DamageModel
),
Vector3.Zero
)
val func: Any => ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile)
val func = resprojectile.calculate()
func(vehicle)
vehicle.Health mustEqual 518
@ -769,14 +858,16 @@ class DamageModelTests extends Specification {
vehicle.Health mustEqual 650
vehicle.Shields mustEqual 10
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(vehicle),
vehicle.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
vehicle.DamageModel
),
Vector3.Zero
)
val func: Any => ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile)
val func = resprojectile.calculate()
func(vehicle)
vehicle.Health mustEqual 528
@ -789,14 +880,16 @@ class DamageModelTests extends Specification {
vehicle.Health mustEqual 650
vehicle.Shields mustEqual 10
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(vehicle),
vehicle.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
vehicle.DamageModel
),
Vector3.Zero
)
val func: Any => ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile)
val func = resprojectile.calculate()
func(vehicle)
vehicle.Health mustEqual 528
@ -810,15 +903,16 @@ class DamageModelTests extends Specification {
val vehicle = Vehicle(DamageModelTests.vehicle)
vehicle.Health mustEqual 650
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(vehicle),
vehicle.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
vehicle.DamageModel
),
Vector3.Zero
)
val func: Any => ResolvedProjectile =
resprojectile.damage_model.Calculate(resprojectile, DamageType.Splash)
val func = resprojectile.calculate(DamageType.Splash)
func(vehicle)
vehicle.Health mustEqual 518
@ -841,7 +935,7 @@ object DamageModelTests {
InitialVelocity = 75
Lifespan = 5f
ProjectileDefinition.CalculateDerivedFields(pdef = this)
Modifiers = DamageModifiers.RadialDegrade
Modifiers = RadialDegrade
}
final val vehicle = new VehicleDefinition(ObjectClass.fury) {

View file

@ -27,8 +27,12 @@ import net.psforever.services.support.SupportActor
import net.psforever.services.vehicle.support.TurretUpgrader
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import org.specs2.mutable.Specification
import scala.concurrent.duration._
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.projectile.ProjectileReason
class DamageableTest extends Specification {
val player1 = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
@ -39,11 +43,13 @@ class DamageableTest extends Specification {
"Damageable" should {
"permit damage" in {
val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
val resolved = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
target.DamageModel
),
Vector3.Zero
)
@ -52,19 +58,13 @@ class DamageableTest extends Specification {
"ignore attempts at non-zero damage" in {
val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectileA,
weaponA.Definition,
weaponA.FireMode,
PlayerSource(player1),
0,
Vector3.Zero,
Vector3.Zero
),
val resolved = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
target.DamageModel
),
Vector3.Zero
)
@ -77,21 +77,32 @@ class DamageableTest extends Specification {
new Building("test-building", 0, 0, Zone.Nowhere, StructureType.Building, GlobalDefinitions.building) {
Faction = player1.Faction
}
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
val resolvedFF = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
target.DamageModel
),
Vector3.Zero
)
target.Definition.DamageableByFriendlyFire mustEqual false
target.Faction == player1.Faction mustEqual true
Damageable.CanDamage(target, projectileA.Damage0, resolved) mustEqual false
Damageable.CanDamage(target, projectileA.Damage0, resolvedFF) mustEqual false
target.Owner.Faction = PlanetSideEmpire.NC
val resolvedNonFF = DamageInteraction(
SourceEntry(target),
ProjectileReason(
DamageResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
target.DamageModel
),
Vector3.Zero
)
target.Faction != player1.Faction mustEqual true
Damageable.CanDamage(target, projectileA.Damage0, resolved) mustEqual true
Damageable.CanDamage(target, projectileA.Damage0, resolvedNonFF) mustEqual true
}
"ignore attempts at damaging a target that is not damageable" in {
@ -100,11 +111,13 @@ class DamageableTest extends Specification {
new Building("test-building", 0, 0, Zone.Nowhere, StructureType.Building, GlobalDefinitions.building) {
Faction = PlanetSideEmpire.NC
}
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
val resolved = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
target.DamageModel
),
Vector3.Zero
)
@ -127,11 +140,13 @@ class DamageableTest extends Specification {
new Building("test-building", 0, 0, Zone.Nowhere, StructureType.Building, GlobalDefinitions.building) {
Faction = player1.Faction
}
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
val resolved = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
target.DamageModel
),
Vector3.Zero
)
@ -151,60 +166,74 @@ class DamageableTest extends Specification {
"permit jamming" in {
val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
val resolved = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
target.DamageModel
),
Vector3.Zero
)
resolved.projectile.profile.JammerProjectile mustEqual true
resolved.cause.source.HasJammedEffectDuration mustEqual true
Damageable.CanJammer(target, resolved) mustEqual true
}
"ignore attempts at jamming if the projectile is does not cause the effect" in {
val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
val resolved = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
target.DamageModel
),
Vector3.Zero
) //decimator
resolved.projectile.profile.JammerProjectile mustEqual false
resolved.cause.source.HasJammedEffectDuration mustEqual false
Damageable.CanJammer(target, resolved) mustEqual false
}
"ignore attempts at jamming friendly targets" in {
val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor)
target.Faction = player1.Faction
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
val resolved = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
target.DamageModel
),
Vector3.Zero
)
resolved.projectile.profile.JammerProjectile mustEqual true
resolved.projectile.owner.Faction == target.Faction mustEqual true
resolved.cause.source.HasJammedEffectDuration mustEqual true
resolved.adversarial match {
case Some(adversarial) => adversarial.attacker.Faction mustEqual adversarial.defender.Faction
case None => ko
}
Damageable.CanJammer(target, resolved) mustEqual false
}
"ignore attempts at jamming targets that are not jammable" in {
val target = new TrapDeployable(GlobalDefinitions.tank_traps)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
val resolved = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
target.DamageModel
),
Vector3.Zero
)
resolved.projectile.profile.JammerProjectile mustEqual true
resolved.projectile.owner.Faction == target.Faction mustEqual false
resolved.cause.source.HasJammedEffectDuration mustEqual true
resolved.adversarial match {
case Some(adversarial) => adversarial.attacker.Faction mustNotEqual adversarial.defender.Faction
case None => ko
}
target.isInstanceOf[JammableUnit] mustEqual false
Damageable.CanJammer(target, resolved) mustEqual false
}
@ -215,16 +244,21 @@ class DamageableTest extends Specification {
player2.GUID = PlanetSideGUID(1)
val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor)
target.Faction = player1.Faction
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
val resolved = DamageInteraction(
SourceEntry(target),
target.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
target.DamageModel,
),
Vector3.Zero
)
resolved.projectile.profile.JammerProjectile mustEqual true
resolved.projectile.owner.Faction == target.Faction mustEqual true
resolved.cause.source.HasJammedEffectDuration mustEqual true
resolved.adversarial match {
case Some(adversarial) => adversarial.attacker.Faction mustEqual adversarial.defender.Faction
case None => ko
}
target.isInstanceOf[JammableUnit] mustEqual true
target.HackedBy.nonEmpty mustEqual false
Damageable.CanJammer(target, resolved) mustEqual false
@ -270,22 +304,16 @@ class DamageableEntityDamageTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(gen),
gen.DamageModel,
Vector3(1, 0, 0)
ProjectileReason(
DamageResolution.Hit,
Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)),
gen.DamageModel
),
Vector3(1,0,0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
"DamageableEntity" should {
@ -341,22 +369,24 @@ class DamageableEntityDestroyedTest extends ActorTest {
guid.register(player1, 3)
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(mech),
mech.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
mech.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -415,22 +445,24 @@ class DamageableEntityNotDestroyTwice extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(gen),
gen.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
gen.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -486,22 +518,24 @@ class DamageableAmenityTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(term),
term.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
term.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -579,22 +613,24 @@ class DamageableMountableDamageTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(mech),
mech.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
mech.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
mech.Seats(0).Occupant = player2 //seat the player
player2.VehicleSeated = Some(mech.GUID) //seat the player
expectNoMessage(200 milliseconds)
@ -674,22 +710,24 @@ class DamageableMountableDestroyTest extends ActorTest {
building.Actor = buildingProbe.ref
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(mech),
mech.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
mech.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
mech.Seats(0).Occupant = player2 //seat the player
player2.VehicleSeated = Some(mech.GUID) //seat the player
expectNoMessage(200 milliseconds)
@ -719,7 +757,7 @@ class DamageableMountableDestroyTest extends ActorTest {
)
assert(
msg3 match {
case Player.Die() => true
case Player.Die(_) => true
case _ => false
}
)
@ -764,23 +802,25 @@ class DamageableWeaponTurretDamageTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.suppressor)
val projectile = weapon.Projectile
val turretSource = SourceEntry(turret)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
val pSource = PlayerSource(player1)
val resolved = DamageInteraction(
SourceEntry(turret),
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
pSource,
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
turret.DamageModel
),
turretSource,
turret.DamageModel,
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -801,8 +841,8 @@ class DamageableWeaponTurretDamageTest extends ActorTest {
assert(
msg3 match {
case activity: Zone.HotSpot.Activity =>
activity.attacker == PlayerSource(player1) &&
activity.defender == turretSource &&
activity.attacker == pSource &&
activity.defender == SourceEntry(turret) &&
activity.location == Vector3(1, 0, 0)
case _ => false
}
@ -864,22 +904,24 @@ class DamageableWeaponTurretJammerTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.jammer_grenade)
val projectile = weapon.Projectile
val turretSource = SourceEntry(turret)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
turretSource,
turret.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
turret.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -966,41 +1008,46 @@ class DamageableWeaponTurretDestructionTest extends ActorTest {
val turretSource = SourceEntry(turret)
val weaponA = Tool(GlobalDefinitions.jammer_grenade)
val projectileA = weaponA.Projectile
val resolvedA = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectileA,
weaponA.Definition,
weaponA.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolvedA = DamageInteraction(
turretSource,
turret.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectileA,
weaponA.Definition,
weaponA.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
turret.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageToA = resolvedA.damage_model.Calculate(resolvedA)
val applyDamageToA = resolvedA.calculate()
val weaponB = Tool(GlobalDefinitions.phoenix) //decimator
val projectileB = weaponB.Projectile
val resolvedB = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectileB,
weaponB.Definition,
weaponB.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolvedB = DamageInteraction(
turretSource,
turret.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectileB,
weaponB.Definition,
weaponB.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
turret.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageToB = resolvedB.damage_model.Calculate(resolvedB)
val applyDamageToB = resolvedB.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -1037,7 +1084,7 @@ class DamageableWeaponTurretDestructionTest extends ActorTest {
)
assert(
msg3 match {
case Player.Die() => true
case Player.Die(_) => true
case _ => false
}
)
@ -1107,22 +1154,24 @@ class DamageableVehicleDamageTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.suppressor)
val projectile = weapon.Projectile
val vehicleSource = SourceEntry(atv)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
vehicleSource,
atv.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
atv.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -1152,7 +1201,7 @@ class DamageableVehicleDamageTest extends ActorTest {
msg3 match {
case activity: Zone.HotSpot.Activity =>
activity.attacker == PlayerSource(player1) &&
activity.defender == vehicleSource &&
activity.defender == VehicleSource(atv) &&
activity.location == Vector3(1, 0, 0)
case _ => false
}
@ -1234,23 +1283,25 @@ class DamageableVehicleDamageMountedTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val vehicleSource = SourceEntry(lodestar)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
val pSource = PlayerSource(player1)
val resolved = DamageInteraction(
SourceEntry(lodestar),
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
pSource,
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
lodestar.DamageModel
),
vehicleSource,
lodestar.DamageModel,
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -1281,8 +1332,8 @@ class DamageableVehicleDamageMountedTest extends ActorTest {
assert(
msg3 match {
case activity: Zone.HotSpot.Activity =>
activity.attacker == PlayerSource(player1) &&
activity.defender == vehicleSource &&
activity.attacker == pSource &&
activity.defender == SourceEntry(lodestar) &&
activity.location == Vector3(1, 0, 0)
case _ => false
}
@ -1379,22 +1430,24 @@ class DamageableVehicleJammeringMountedTest extends ActorTest {
val vehicleSource = SourceEntry(lodestar)
val weapon = Tool(GlobalDefinitions.jammer_grenade)
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
vehicleSource,
lodestar.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
lodestar.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -1469,22 +1522,24 @@ class DamageableVehicleDestroyTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.suppressor)
val projectile = weapon.Projectile
val vehicleSource = SourceEntry(atv)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
vehicleSource,
atv.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
atv.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -1513,7 +1568,7 @@ class DamageableVehicleDestroyTest extends ActorTest {
)
assert(
msg3 match {
case Player.Die() => true
case Player.Die(_) => true
case _ => false
}
)
@ -1531,18 +1586,6 @@ class DamageableVehicleDestroyTest extends ActorTest {
}
class DamageableVehicleDestroyMountedTest extends ActorTest {
val guid = new NumberPoolHub(new MaxNumberSource(15))
val zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
GUID(guid)
}
val activityProbe = TestProbe()
val avatarProbe = TestProbe()
val vehicleProbe = TestProbe()
zone.Activity = activityProbe.ref
zone.AvatarEvents = avatarProbe.ref
zone.VehicleEvents = vehicleProbe.ref
val atv = Vehicle(GlobalDefinitions.quadassault) //guid=1
atv.Actor = system.actorOf(Props(classOf[VehicleControl], atv), "atv-control")
atv.Position = Vector3(1, 0, 0)
@ -1568,6 +1611,20 @@ class DamageableVehicleDestroyMountedTest extends ActorTest {
val player3Probe = TestProbe()
player3.Actor = player3Probe.ref
val guid = new NumberPoolHub(new MaxNumberSource(15))
val zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
GUID(guid)
override def LivePlayers = List(player1, player2, player3)
}
val activityProbe = TestProbe()
val avatarProbe = TestProbe()
val vehicleProbe = TestProbe()
zone.Activity = activityProbe.ref
zone.AvatarEvents = avatarProbe.ref
zone.VehicleEvents = vehicleProbe.ref
guid.register(atv, 1)
guid.register(atvWeapon, 2)
guid.register(atvWeapon.AmmoSlot.Box, 3)
@ -1595,41 +1652,45 @@ class DamageableVehicleDestroyMountedTest extends ActorTest {
val vehicleSource = SourceEntry(lodestar)
val weaponA = Tool(GlobalDefinitions.jammer_grenade)
val projectileA = weaponA.Projectile
val resolvedA = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectileA,
weaponA.Definition,
weaponA.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolvedA = DamageInteraction(
vehicleSource,
lodestar.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectileA,
weaponA.Definition,
weaponA.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
lodestar.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageToA = resolvedA.damage_model.Calculate(resolvedA)
val applyDamageToA = resolvedA.calculate()
val weaponB = Tool(GlobalDefinitions.phoenix) //decimator
val projectileB = weaponB.Projectile
val resolvedB = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectileB,
weaponB.Definition,
weaponB.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolvedB = DamageInteraction(
vehicleSource,
lodestar.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectileB,
weaponB.Definition,
weaponB.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
lodestar.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageToB = resolvedB.damage_model.Calculate(resolvedB)
val applyDamageToB = resolvedB.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -1699,13 +1760,13 @@ class DamageableVehicleDestroyMountedTest extends ActorTest {
)
assert(
msg_player2 match {
case Player.Die() => true
case Player.Die(_) => true
case _ => false
}
)
assert(
msg_player3 match {
case Player.Die() => true
case Player.Die(_) => true
case _ => false
}
)

View file

@ -20,6 +20,9 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.support.SupportActor
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import scala.concurrent.duration._
@ -333,14 +336,16 @@ class ExplosiveDeployableJammerTest extends ActorTest {
val jMineSource = SourceEntry(j_mine)
val pSource = PlayerSource(player1)
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
val resolved = DamageInteraction(
jMineSource,
j_mine.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
j_mine.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageToJ = resolved.damage_model.Calculate(resolved)
val applyDamageToJ = resolved.calculate()
"ExplosiveDeployable" should {
"handle being jammered appropriately (no detonation)" in {
@ -428,17 +433,18 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
h_mine.Faction = PlanetSideEmpire.NC
h_mine.Actor = system.actorOf(Props(classOf[ExplosiveDeployableControl], h_mine), "h-mine-control")
val hMineSource = SourceEntry(h_mine)
val pSource = PlayerSource(player1)
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
hMineSource,
h_mine.DamageModel,
val resolved = DamageInteraction(
SourceEntry(h_mine),
ProjectileReason(
DamageResolution.Hit,
Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
h_mine.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageToH = resolved.damage_model.Calculate(resolved)
val applyDamageToH = resolved.calculate()
"ExplosiveDeployable" should {
"handle being jammered appropriately (detonation)" in {
@ -498,7 +504,7 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
)
assert(
msg_activity match {
case Zone.HotSpot.Activity(target, attacker, _) => (target eq hMineSource) && (attacker eq pSource)
case Zone.HotSpot.Conflict(target, attacker, _) => (target.Definition eq h_mine.Definition) && (attacker eq pSource)
case _ => false
}
)
@ -541,14 +547,16 @@ class ExplosiveDeployableDestructionTest extends ActorTest {
val hMineSource = SourceEntry(h_mine)
val pSource = PlayerSource(player1)
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
val resolved = DamageInteraction(
hMineSource,
h_mine.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero),
h_mine.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
"ExplosiveDeployable" should {
"handle being destroyed" in {

View file

@ -14,6 +14,10 @@ import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl, GeneratorDefinition}
import net.psforever.objects.serverobject.structures.{Building, StructureType}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.vital.prop.DamageWithPosition
import net.psforever.objects.zones.{Zone, ZoneMap}
import net.psforever.packet.game.{InventoryStateMessage, RepairMessage, TriggerEffectMessage}
import net.psforever.types._
@ -80,22 +84,24 @@ class GeneratorControlDamageTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(gen),
gen.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
gen.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -161,22 +167,24 @@ class GeneratorControlCriticalTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(gen),
gen.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
gen.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
val halfHealth = gen.Definition.MaxHealth / 2
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -245,22 +253,24 @@ class GeneratorControlDestroyedTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(gen),
gen.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
gen.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -328,25 +338,6 @@ class GeneratorControlDestroyedTest extends ActorTest {
}
class GeneratorControlKillsTest extends ActorTest {
/*
to perform this test, players need to be added to the SOI organization of the test base in proximity of the generator
under normal player scenario, this is an automatic process
extending from the act of players being in a zone
and players being within the SOI radius from the center of a facility on a periodic check
the test base being used has no established SOI region or automatic SOI check refresh,
but its SOI information can be loaded with the players manually
the players need something to catch the die message
*/
val guid = new NumberPoolHub(new MaxNumberSource(5))
val zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
GUID(guid)
}
val avatarProbe = TestProbe()
zone.AvatarEvents = avatarProbe.ref
val activityProbe = TestProbe()
zone.Activity = activityProbe.ref
val gen = Generator(GeneratorTest.generator_definition) //guid=2
gen.Position = Vector3(1, 0, 0)
gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control")
@ -359,16 +350,24 @@ class GeneratorControlKillsTest extends ActorTest {
player1.Actor = player1Probe.ref
val player2 =
Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Female, 1, CharacterVoice.Mute)) //guid=4
player2.Position = Vector3(15, 0, 0) //>14m from generator; lives
player2.Position = Vector3(25, 0, 0) //>14m from generator; lives
player2.Spawn()
val player2Probe = TestProbe()
player2.Actor = player2Probe.ref
val avatarProbe = TestProbe()
val activityProbe = TestProbe()
val guid = new NumberPoolHub(new MaxNumberSource(5))
val zone = new Zone("test", new ZoneMap("test"), 0) {
override def SetupNumberPools() = {}
GUID(guid)
override def LivePlayers = List(player1, player2)
override def AvatarEvents = avatarProbe.ref
override def Activity = activityProbe.ref
}
val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1
building.Position = Vector3(1, 0, 0)
building.Zone = zone
building.Amenities = gen
building.PlayersInSOI = List(player1, player2)
val buildingProbe = TestProbe()
building.Actor = buildingProbe.ref
@ -379,27 +378,29 @@ class GeneratorControlKillsTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(gen),
gen.DamageModel,
Vector3(1, 0, 0)
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(25, 0, 0),
Vector3(-1, 0, 0)
),
gen.DamageModel
),
Vector3(2, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
"GeneratorControl" should {
"kill players when the generator is destroyed" in {
"damages (kills) players when the generator is destroyed" in {
gen.Health = 1 //no matter what, the next shot destroys the generator
assert(gen.Health == 1)
assert(!gen.Destroyed)
@ -458,11 +459,11 @@ class GeneratorControlKillsTest extends ActorTest {
assert(gen.Destroyed)
assert(gen.Condition == PlanetSideGeneratorState.Destroyed)
val msg_player1 = player1Probe.receiveOne(100 milliseconds)
val msg_player1 = player1Probe.receiveOne(200 milliseconds)
player2Probe.expectNoMessage(200 milliseconds)
assert(
msg_player1 match {
case _ @Player.Die() => true
case _ @ Vitality.Damage(_) => true
case _ => false
}
)
@ -499,22 +500,24 @@ class GeneratorControlNotDestroyTwice extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(gen),
gen.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
gen.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -589,22 +592,24 @@ class GeneratorControlNotDamageIfExplodingTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(gen),
gen.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
gen.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
expectNoMessage(200 milliseconds)
//we're not testing that the math is correct
@ -686,22 +691,24 @@ class GeneratorControlNotRepairIfExplodingTest extends ActorTest {
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
val projectile = weapon.Projectile
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
SourceEntry(gen),
gen.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
weapon.Definition,
weapon.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
gen.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
val tool = Tool(GlobalDefinitions.nano_dispenser) //4 & 5
guid.register(tool, 4)
@ -851,6 +858,10 @@ object GeneratorTest {
Repairable = true
RepairDistance = 13.5f
RepairIfDestroyed = true
explodes = true
innateDamage = new DamageWithPosition {
DamageRadius = 14
}
//note: no auto-repair
}
}

View file

@ -377,22 +377,24 @@ class PlayerControlDamageTest extends ActorTest {
val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4
val projectile = tool.Projectile
val playerSource = SourceEntry(player2)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(
projectile,
tool.Definition,
tool.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
val resolved = DamageInteraction(
playerSource,
player1.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(
projectile,
tool.Definition,
tool.FireMode,
PlayerSource(player1),
0,
Vector3(2, 0, 0),
Vector3(-1, 0, 0)
),
player1.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
guid.register(player1, 1)
guid.register(player2, 2)
guid.register(tool, 3)
@ -476,14 +478,16 @@ class PlayerControlDeathStandingTest extends ActorTest {
val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4
val projectile = tool.Projectile
val player1Source = SourceEntry(player1)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectile, tool.Definition, tool.FireMode, player1Source, 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)),
val resolved = DamageInteraction(
SourceEntry(player2),
player2.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectile, tool.Definition, tool.FireMode, player1Source, 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)),
player2.DamageModel
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
guid.register(player1, 1)
guid.register(player2, 2)
guid.register(tool, 3)
@ -605,14 +609,16 @@ class PlayerControlDeathSeatedTest extends ActorTest {
val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4
val projectile = tool.Projectile
val player1Source = SourceEntry(player1)
val resolved = ResolvedProjectile(
ProjectileResolution.Hit,
Projectile(projectile, tool.Definition, tool.FireMode, player1Source, 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)),
val resolved = DamageInteraction(
SourceEntry(player2),
player2.DamageModel,
ProjectileReason(
DamageResolution.Hit,
Projectile(projectile, tool.Definition, tool.FireMode, player1Source, 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)),
player2.DamageMode
),
Vector3(1, 0, 0)
)
val applyDamageTo = resolved.damage_model.Calculate(resolved)
val applyDamageTo = resolved.calculate()
guid.register(player1, 1)
guid.register(player2, 2)
guid.register(tool, 3)

View file

@ -6,7 +6,9 @@ import net.psforever.objects.avatar.Avatar
import net.psforever.objects.ballistics._
import net.psforever.objects.definition.ProjectileDefinition
import net.psforever.objects.serverobject.mblocker.Locker
import net.psforever.objects.vital.DamageType
import net.psforever.objects.vital.base.{DamageResolution, DamageType}
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.types.{PlanetSideGUID, _}
import org.specs2.mutable.Specification
@ -328,7 +330,7 @@ class ProjectileTest extends Specification {
}
}
"ResolvedProjectile" should {
"Projectile DamageInteraction" should { //TODO wrong place for this test?
val beamer_wep = Tool(GlobalDefinitions.beamer)
val p_source = PlayerSource(player)
val player2 = Player(Avatar(0, "TestTarget", PlanetSideEmpire.NC, CharacterGender.Female, 1, CharacterVoice.Mute))
@ -345,17 +347,20 @@ class ProjectileTest extends Specification {
val fury_dm = fury.DamageModel
"construct" in {
val obj = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val obj = DamageInteraction(
DamageResolution.Hit,
PlayerSource(player2),
fury_dm,
ProjectileReason(
DamageResolution.Hit,
projectile,
fury_dm
),
Vector3(1.2f, 3.4f, 5.6f)
)
obj.projectile mustEqual projectile
obj.cause.asInstanceOf[ProjectileReason].projectile mustEqual projectile
obj.target mustEqual p2_source
obj.damage_model mustEqual fury.DamageModel
obj.hit_pos mustEqual Vector3(1.2f, 3.4f, 5.6f)
obj.cause.asInstanceOf[ProjectileReason].damageModel mustEqual fury.DamageModel
obj.hitPos mustEqual Vector3(1.2f, 3.4f, 5.6f)
}
}
}

View file

@ -962,7 +962,7 @@ class VehicleControlShieldsNotChargingTooEarlyTest extends ActorTest {
// val p_source = PlayerSource( Player(Avatar(0, "TestTarget", PlanetSideEmpire.NC, CharacterGender.Female, 1, CharacterVoice.Mute)) )
// val projectile = Projectile(beamer_wep.Projectile, GlobalDefinitions.beamer, beamer_wep.FireMode, p_source, GlobalDefinitions.beamer.ObjectId, Vector3.Zero, Vector3.Zero)
// val fury_dm = Vehicle(GlobalDefinitions.fury).DamageModel
// val obj = ResolvedProjectile(projectile, p_source, fury_dm, Vector3(1.2f, 3.4f, 5.6f))
// val obj = DamageInteraction(p_source, ProjectileReason(DamageResolution.Hit, projectile, fury_dm), Vector3(1.2f, 3.4f, 5.6f))
//
// "not charge vehicle shields if recently damaged" in {
// assert(vehicle.Shields == 0)

View file

@ -5,6 +5,9 @@ import net.psforever.objects.ballistics._
import net.psforever.objects._
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.vital._
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._
import org.specs2.mutable.Specification
@ -20,16 +23,19 @@ class VitalityTest extends Specification {
val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
val pSource = PlayerSource(player)
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(player),
player.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
player.DamageModel
),
Vector3(50, 50, 0)
)
val result = resprojectile.calculate()(player)
player.History(resprojectile) //ResolvedProjectile, straight-up
player.History(DamageFromProjectile(resprojectile))
player.History(result) //DamageResult, straight-up
player.History(DamageFromProjectile(result))
player.History(HealFromKit(pSource, 10, GlobalDefinitions.medkit))
player.History(HealFromTerm(pSource, 10, 0, GlobalDefinitions.order_terminal))
player.History(HealFromImplant(pSource, 10, ImplantType.AdvancedRegen))
@ -68,15 +74,18 @@ class VitalityTest extends Specification {
val player = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
val pSource = PlayerSource(player)
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
val resprojectile = ResolvedProjectile(
ProjectileResolution.Hit,
projectile,
val resprojectile = DamageInteraction(
SourceEntry(player),
player.DamageModel,
ProjectileReason(
DamageResolution.Hit,
projectile,
player.DamageModel
),
Vector3(50, 50, 0)
)
val result = resprojectile.calculate()(player)
player.History(DamageFromProjectile(resprojectile))
player.History(DamageFromProjectile(result))
player.History(HealFromKit(pSource, 10, GlobalDefinitions.medkit))
player.History(HealFromTerm(pSource, 10, 0, GlobalDefinitions.order_terminal))
player.History(HealFromImplant(pSource, 10, ImplantType.AdvancedRegen))
@ -87,7 +96,10 @@ class VitalityTest extends Specification {
player.LastShot match {
case Some(resolved_projectile) =>
resolved_projectile.projectile mustEqual projectile
resolved_projectile.interaction.cause match {
case o: ProjectileReason => o.projectile mustEqual projectile
case _ => ko
}
case None =>
ko
}