eliminated multiple timers for a single aura effect; added comments; added tests; fixed tests

This commit is contained in:
FateJH 2020-08-19 20:43:50 -04:00
parent fc89355acf
commit f627571f0e
11 changed files with 616 additions and 185 deletions

View file

@ -4,20 +4,54 @@ import net.psforever.objects.equipment.TargetValidation
import net.psforever.objects.serverobject.aura.Aura
import net.psforever.objects.vital.DamageType
/**
* In what manner of pacing the aggravated damage ticks are applied.
* @param duration for how long the over-all effect is applied
* @param ticks a custom number of damage applications,
* as opposed to whatever calculations normally estimate the number of applications
*/
final case class AggravatedTiming(duration: Long, ticks: Option[Int])
object AggravatedTiming {
/**
* Overloaded constructor that only defines the duration.
* @param duration for how long the over-all effect lasts
* @return an `AggravatedTiming` object
*/
def apply(duration: Long): AggravatedTiming = AggravatedTiming(duration, None)
/**
* Overloaded constructor.
* @param duration for how long the over-all effect lasts
* @param ticks a custom number of damage applications
* @return an `AggravatedTiming` object
*/
def apply(duration: Long, ticks: Int): AggravatedTiming = AggravatedTiming(duration, Some(ticks))
}
/**
* Aggravation damage has components that are mainly divided by the `DamageType` they inflict.
* Only `Direct` and `Splash` are valid damage types, however.
* @param damage_type the type of damage
* @param degradation_percentage by how much the damage is degraded
* @param infliction_rate how often the damage is inflicted (ms)
*/
final case class AggravatedInfo(damage_type: DamageType.Value,
degradation_percentage: Float,
infliction_rate: Long) {
assert(damage_type == DamageType.Direct || damage_type == DamageType.Splash, s"aggravated damage is an unsupported type - $damage_type")
}
/**
* Information related to the aggravated damage.
* @param info the specific kinds of aggravation damage available
* @param effect_type what effect is exhibited by this aggravated damage
* @param timing the timing for the damage application
* @param max_factor na (if the target is a mechanized assault exo-suit?)
* @param cumulative_damage_degrade na (can multiple instances of this type of aggravated damage apply to the same target at once?)
* @param vanu_aggravated na (search me)
* @param targets validation information indicating whether a certain entity is applicable for aggravation
*/
final case class AggravatedDamage(info: List[AggravatedInfo],
effect_type: Aura,
timing: AggravatedTiming,
@ -27,6 +61,14 @@ final case class AggravatedDamage(info: List[AggravatedInfo],
targets: List[TargetValidation])
object AggravatedDamage {
/**
* Overloaded constructor.
* @param info the specific kinds of aggravation damage available
* @param effect_type what effect is exhibited by this aggravated damage
* @param timing the timing for the damage application
* @param max_factor na
* @param targets validation information indicating whether a certain entity is applicable for aggravation
*/
def apply(info: AggravatedInfo,
effect_type: Aura,
timing: AggravatedTiming,
@ -42,6 +84,15 @@ object AggravatedDamage {
targets
)
/**
* Overloaded constructor.
* @param info the specific kinds of aggravation damage available
* @param effect_type what effect is exhibited by this aggravated damage
* @param timing the timing for the damage application
* @param max_factor na
* @param vanu_aggravated na
* @param targets validation information indicating whether a certain entity is applicable for aggravation
*/
def apply(info: AggravatedInfo,
effect_type: Aura,
timing: AggravatedTiming,
@ -58,6 +109,14 @@ object AggravatedDamage {
targets
)
/**
* Overloaded constructor.
* @param info the specific kinds of aggravation damage available
* @param effect_type what effect is exhibited by this aggravated damage
* @param duration for how long the over-all effect is applied
* @param max_factor na
* @param targets validation information indicating whether a certain entity is applicable for aggravation
*/
def apply(info: AggravatedInfo,
effect_type: Aura,
duration: Long,
@ -73,6 +132,15 @@ object AggravatedDamage {
targets
)
/**
* Overloaded constructor.
* @param info the specific kinds of aggravation damage available
* @param effect_type what effect is exhibited by this aggravated damage
* @param duration for how long the over-all effect is applied
* @param max_factor na
* @param vanu_aggravated na
* @param targets validation information indicating whether a certain entity is applicable for aggravation
*/
def apply(info: AggravatedInfo,
effect_type: Aura,
duration: Long,

View file

@ -15,7 +15,7 @@ sealed trait ProjectileQuality {
}
/**
* Implement the numeric modifier with as one.
* Implement the numeric modifier with the value as one.
*/
sealed trait SameAsQuality extends ProjectileQuality {
def mod: Float = 1f
@ -25,7 +25,7 @@ object ProjectileQuality {
/** Standard projectile quality. More of a flag than a modifier. */
case object Normal extends SameAsQuality
/** Quality that flags the first stage of aggravation (setup). */
/** Quality that flags the first stage of aggravation (initial damage). */
case object AggravatesTarget extends SameAsQuality
/** The complete lack of quality. Even the numeric modifier is zeroed. */

View file

@ -2,19 +2,28 @@
package net.psforever.objects.serverobject.aura
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.Default
import net.psforever.objects.serverobject.PlanetSideServerObject
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
/**
* A mixin that governs the addition, display, and removal of aura particle effects
* on a target with control agency.
* @see `Aura`
* @see `AuraContainer`
* @see `PlayerControl`
*/
trait AuraEffectBehavior {
_ : Actor =>
private var activeEffectIndex: Long = 0
private val effectToEntryId: mutable.HashMap[Aura, List[Long]] =
mutable.HashMap.empty[Aura, List[Long]]
private val effectIdToTimer: mutable.LongMap[Cancellable] =
mutable.LongMap.empty[Cancellable]
/** active aura effects are monotonic, but the timer will be updated for continuing and cancelling effects as well<br>
* only effects that are initialized to this mapping are approved for display on this target<br>
* key - aura effect; value - the timer for that effect
* @see `ApplicableEffect`
*/
private val effectToTimer: mutable.HashMap[Aura, Cancellable] = mutable.HashMap.empty[Aura, Cancellable]
def AuraTargetObject: AuraEffectBehavior.Target
@ -22,144 +31,142 @@ trait AuraEffectBehavior {
case AuraEffectBehavior.StartEffect(effect, duration) =>
StartAuraEffect(effect, duration)
case AuraEffectBehavior.EndEffect(Some(id), None) =>
EndAuraEffect(id)
case AuraEffectBehavior.EndEffect(None, Some(effect)) =>
EndAuraEffect(effect)
case AuraEffectBehavior.EndEffect(effect) =>
EndAuraEffectAndUpdate(effect)
case AuraEffectBehavior.EndAllEffects() =>
EndAllEffectsAndUpdate()
}
final def GetUnusedEffectId: Long = {
val id = activeEffectIndex
activeEffectIndex += 1
id
/**
* Only pre-apporved aura effects will be emitted by this target.
* @param effect the aura effect
*/
def ApplicableEffect(effect: Aura): Unit = {
//create entry
effectToTimer += effect -> Default.Cancellable
}
def StartAuraEffect(effect: Aura, duration: Long): Option[Long] = {
/**
* An aura particle effect is to be emitted by the target.
* If the effect was not previously applied to the target in an ongoing manner,
* animate it appropriately.
* @param effect the effect to be emitted
* @param duration for how long the effect will be emitted
* @return the active effect index number
*/
def StartAuraEffect(effect: Aura, duration: Long): Unit = {
val obj = AuraTargetObject
val auraEffects = obj.Aura
if (obj.Aura.contains(effect)) {
effectToEntryId.getOrElse(effect, List[Long](AuraEffectBehavior.InvalidEffectId)).headOption //grab an available active effect id
}
else if(obj.AddEffectToAura(effect).diff(auraEffects).contains(effect)) {
Some(StartAuraEffect(GetUnusedEffectId, effect, duration))
}
else {
None
val auraEffectsBefore = obj.Aura.size
if(StartAuraTimer(effect, duration) && obj.AddEffectToAura(effect).size > auraEffectsBefore) {
//new effect; update visuals
UpdateAuraEffect(AuraTargetObject)
}
}
def StartAuraEffect(id: Long, effect: Aura, duration: Long): Long = {
/**
* As long as the effect has been approved for this target,
* the timer will either start if it is stopped or has never been started,
* or the timer will stop and be recreated with the new duration if is currently running.
* @param effect the effect to be emitted
* @param duration for how long the effect will be emitted
* @return `true`, if the timer was started or restarted;
* `false`, otherwise
*/
private def StartAuraTimer(effect: Aura, duration: Long): Boolean = {
//pair aura effect with id
effectToEntryId.get(effect) match {
case None | Some(Nil) => effectToEntryId += effect -> List(id)
case Some(list) => effectToEntryId -> (list :+ id)
}
//pair id with timer
effectIdToTimer += id -> context.system.scheduler.scheduleOnce(duration milliseconds, self, AuraEffectBehavior.EndEffect(id))
//update visuals
UpdateAuraEffect(AuraTargetObject)
id
}
def EndAuraEffect(id: Long): Unit = {
EndActiveEffect(id) match {
case Aura.Nothing => ;
case effect =>
CancelEffectTimer(id)
val obj = AuraTargetObject
obj.RemoveEffectFromAura(effect)
UpdateAuraEffect(obj)
}
}
def EndActiveEffect(id: Long): Aura = {
effectToEntryId.find { case (_, ids) => ids.contains(id) } match {
case Some((effect, ids)) if ids.size == 1 =>
effectToEntryId.remove(effect)
effect
case Some((effect, ids)) =>
effectToEntryId += effect -> ids.filterNot(_ == id)
Aura.Nothing
(effectToTimer.get(effect) match {
case None =>
Aura.Nothing
None
case Some(timer) =>
timer.cancel()
Some(effect)
}) match {
case None =>
false
case Some(_) =>
//paired id with timer; retime
effectToTimer(effect) =
context.system.scheduler.scheduleOnce(duration milliseconds, self, AuraEffectBehavior.EndEffect(effect))
true
}
}
def CancelEffectTimer(id: Long) : Unit = {
effectIdToTimer.remove(id) match {
case Some(timer) => timer.cancel
case _ => ;
}
}
def EndAuraEffect(effect: Aura): Unit = {
effectToEntryId.remove(effect) match {
case Some(idList) =>
idList.foreach { id =>
val obj = AuraTargetObject
CancelEffectTimer(id)
obj.RemoveEffectFromAura(effect)
UpdateAuraEffect(obj)
}
case _ => ;
/**
* Stop the target entity from emitting the aura particle effect, if it currently is.
* @param effect the target effect
* @return `true`, if the effect was being emitted but has been stopped
* `false`, if the effect was not approved or is not being emitted
*/
def EndAuraEffect(effect: Aura): Boolean = {
effectToTimer.get(effect) match {
case Some(timer) if !timer.isCancelled =>
timer.cancel()
effectToTimer(effect) = Default.Cancellable
AuraTargetObject.RemoveEffectFromAura(effect)
true
case _ =>
false
}
}
/**
* Stop the target entity from emitting all aura particle effects.
*/
def EndAllEffects() : Unit = {
effectIdToTimer.values.foreach { _.cancel }
effectIdToTimer.clear
effectToEntryId.clear
effectToTimer.keysIterator.foreach { effect =>
effectToTimer(effect).cancel()
effectToTimer(effect) = Default.Cancellable
}
val obj = AuraTargetObject
obj.Aura.foreach { obj.RemoveEffectFromAura }
}
/**
* Stop the target entity from emitting the aura particle effect, if it currently is.
* If the effect has been stopped, animate the new particle effect state.
*/
def EndAuraEffectAndUpdate(effect: Aura) : Unit = {
if(EndAuraEffect(effect)) {
UpdateAuraEffect(AuraTargetObject)
}
}
/**
* Stop the target entity from emitting all aura particle effects.
* Animate the new particle effect state.
*/
def EndAllEffectsAndUpdate() : Unit = {
EndAllEffects()
UpdateAuraEffect(AuraTargetObject)
}
def UpdateAuraEffect(target: AuraEffectBehavior.Target) : Unit = {
import services.avatar.{AvatarAction, AvatarServiceMessage}
val zone = target.Zone
val value = target.Aura.foldLeft(0)(_ + AuraEffectBehavior.effectToAttributeValue(_))
zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttributeToAll(target.GUID, 54, value))
}
def TestForEffect(id: Long): Aura = {
effectToEntryId.find { case (_, ids) => ids.contains(id) } match {
case Some((effect, _)) => effect
case _ => Aura.Nothing
/**
* Is the target entity emitting the aura effect?
* @param effect the effect being tested
* @return `true`, if the effect is currently being emitted;
* `false`, otherwise
*/
def TestForEffect(effect: Aura): Boolean = {
effectToTimer.get(effect) match {
case None => false
case Some(timer) => timer.isCancelled
}
}
/**
* An override callback to display aura effects emitted.
* @param target the entity from which the aura effects are being emitted
*/
def UpdateAuraEffect(target: AuraEffectBehavior.Target) : Unit
}
object AuraEffectBehavior {
type Target = PlanetSideServerObject with AuraContainer
final val InvalidEffectId = -1
final case class StartEffect(effect: Aura, duration: Long)
final case class EndEffect(id: Option[Long], aura: Option[Aura])
object EndEffect {
def apply(id: Long): EndEffect = EndEffect(Some(id), None)
def apply(aura: Aura): EndEffect = EndEffect(None, Some(aura))
}
final case class EndEffect(aura: Aura)
final case class EndAllEffects()
private def effectToAttributeValue(effect: Aura): Int = effect match {
case Aura.None => 0
case Aura.Plasma => 1
case Aura.Comet => 2
case Aura.Napalm => 4
case Aura.Fire => 8
case _ => Int.MinValue
}
}

View file

@ -7,12 +7,17 @@ import scodec.Codec
import scodec.codecs._
/**
* na
* @param guid na
* @param unk na
* Dispatched from the server to cause a damage reaction from a specific target.
* Infantry targets should be the primary target of this packet, as indicated by their identifier.
* Infantry targets display their flinch animation.
* All targets yelp in agony.
* Infantry targets use their assigned voice.
* Non-infantry targets use the "grizzled"(?) voice.
* @param guid the target entity's global unique identifier
* @param damage the amount of damsge being simulated
*/
final case class AggravatedDamageMessage(guid : PlanetSideGUID,
unk : Long)
damage : Long)
extends PlanetSideGamePacket {
type Packet = AggravatedDamageMessage
def opcode = GamePacketOpcode.AggravatedDamageMessage
@ -22,6 +27,6 @@ final case class AggravatedDamageMessage(guid : PlanetSideGUID,
object AggravatedDamageMessage extends Marshallable[AggravatedDamageMessage] {
implicit val codec : Codec[AggravatedDamageMessage] = (
("guid" | PlanetSideGUID.codec) ::
("unk" | uint32L)
("damage" | uint32L)
).as[AggravatedDamageMessage]
}

View file

@ -0,0 +1,237 @@
// Copyright (c) 2020 PSForever
package objects
import akka.actor.{Actor, ActorRef, Props}
import akka.testkit.TestProbe
import base.ActorTest
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.aura.AuraEffectBehavior.Target
import net.psforever.objects.serverobject.aura.{Aura, AuraContainer, AuraEffectBehavior}
import net.psforever.types.PlanetSideEmpire
import org.specs2.mutable.Specification
import scala.concurrent.duration._
class AuraContainerTest extends Specification {
"AuraContainer" should {
"have no default effects" in {
new AuraTest.Entity().Aura.isEmpty mustEqual true
}
"add effects" in {
val obj = new AuraTest.Entity()
obj.Aura.size mustEqual 0
obj.Aura.contains(Aura.Plasma) mustEqual false
obj.AddEffectToAura(Aura.Plasma)
obj.Aura.size mustEqual 1
obj.Aura.contains(Aura.Plasma) mustEqual true
}
"do nothing if adding repeated effects" in {
val obj = new AuraTest.Entity()
obj.Aura.size mustEqual 0
obj.AddEffectToAura(Aura.Plasma)
obj.Aura.size mustEqual 1
obj.AddEffectToAura(Aura.Plasma)
obj.Aura.size mustEqual 1
}
"remove effects" in {
val obj = new AuraTest.Entity()
obj.Aura.size mustEqual 0
obj.Aura.contains(Aura.Plasma) mustEqual false
obj.AddEffectToAura(Aura.Plasma)
obj.Aura.size mustEqual 1
obj.Aura.contains(Aura.Plasma) mustEqual true
obj.RemoveEffectFromAura(Aura.Plasma)
obj.Aura.size mustEqual 0
obj.Aura.contains(Aura.Plasma) mustEqual false
}
"do nothing if no effects" in {
val obj = new AuraTest.Entity()
obj.Aura.size mustEqual 0
obj.Aura.contains(Aura.Plasma) mustEqual false
obj.RemoveEffectFromAura(Aura.Plasma)
obj.Aura.size mustEqual 0
obj.Aura.contains(Aura.Plasma) mustEqual false
}
"do nothing if trying to remove wrong effect" in {
val obj = new AuraTest.Entity()
obj.Aura.size mustEqual 0
obj.Aura.contains(Aura.Plasma) mustEqual false
obj.Aura.contains(Aura.Fire) mustEqual false
obj.AddEffectToAura(Aura.Plasma)
obj.Aura.size mustEqual 1
obj.Aura.contains(Aura.Plasma) mustEqual true
obj.Aura.contains(Aura.Fire) mustEqual false
obj.RemoveEffectFromAura(Aura.Fire)
obj.Aura.size mustEqual 1
obj.Aura.contains(Aura.Plasma) mustEqual true
obj.Aura.contains(Aura.Fire) mustEqual false
}
}
}
class AuraEffectBehaviorInitTest extends ActorTest {
val obj = new AuraTest.Entity()
"AuraEffectBehavior" should {
"init" in {
obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, ActorRef.noSender), "aura-test-actor")
expectNoMessage(500 milliseconds)
}
}
}
class AuraEffectBehaviorStartEffectTest extends ActorTest {
val obj = new AuraTest.Entity()
val updateProbe = new TestProbe(system)
obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor")
"AuraEffectBehavior" should {
"start effect (ends naturally)" in {
assert(obj.Aura.isEmpty)
obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500)
val msg1 = updateProbe.receiveOne(100 milliseconds)
assert(
msg1 match {
case AuraTest.DoUpdateAuraEffect() => true
case _ => false
}
)
assert(obj.Aura.contains(Aura.Plasma))
expectNoMessage(2000 milliseconds)
assert(obj.Aura.contains(Aura.Plasma))
val msg2 = updateProbe.receiveOne(750 milliseconds)
assert(
msg2 match {
case AuraTest.DoUpdateAuraEffect() => true
case _ => false
}
)
assert(obj.Aura.isEmpty)
}
}
}
class AuraEffectBehaviorNoRedundantStartEffectTest extends ActorTest {
val obj = new AuraTest.Entity()
val updateProbe = new TestProbe(system)
obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor")
"AuraEffectBehavior" should {
"not start an effect if already active" in {
assert(obj.Aura.isEmpty)
obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500)
val msg1 = updateProbe.receiveOne(100 milliseconds)
assert(
msg1 match {
case AuraTest.DoUpdateAuraEffect() => true
case _ => false
}
)
assert(obj.Aura.contains(Aura.Plasma))
expectNoMessage(1000 milliseconds) //wait for half of the effect's duration
assert(obj.Aura.contains(Aura.Plasma))
obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500)
updateProbe.expectNoMessage(1500 milliseconds)
//effect has not ended naturally
assert(obj.Aura.contains(Aura.Plasma))
}
}
}
class AuraEffectBehaviorNoStartUnsupportedEffectTest extends ActorTest {
val obj = new AuraTest.Entity()
val updateProbe = new TestProbe(system)
obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor") //supports Plasma only
"AuraEffectBehavior" should {
"not start an effect that is not approved" in {
assert(obj.Aura.isEmpty)
obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Fire, 2500)
assert(obj.Aura.isEmpty)
updateProbe.expectNoMessage(2000 milliseconds)
}
}
}
class AuraEffectBehaviorEndEarlyTest extends ActorTest {
val obj = new AuraTest.Entity()
val updateProbe = new TestProbe(system)
obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor")
"AuraEffectBehavior" should {
"start effect (ends early)" in {
assert(obj.Aura.isEmpty)
obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500)
val msg1 = updateProbe.receiveOne(100 milliseconds)
assert(
msg1 match {
case AuraTest.DoUpdateAuraEffect() => true
case _ => false
}
)
assert(obj.Aura.contains(Aura.Plasma))
obj.Actor ! AuraEffectBehavior.EndEffect(Aura.Plasma)
val msg2 = updateProbe.receiveOne(100 milliseconds)
assert(
msg2 match {
case AuraTest.DoUpdateAuraEffect() => true
case _ => false
}
)
assert(obj.Aura.isEmpty)
}
}
}
class AuraEffectBehaviorEndNothingTest extends ActorTest {
val obj = new AuraTest.Entity()
val updateProbe = new TestProbe(system)
obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor")
"AuraEffectBehavior" should {
"can not end an effect that is not supported (hence, not started)" in {
assert(obj.Aura.isEmpty)
obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500)
val msg1 = updateProbe.receiveOne(100 milliseconds)
assert(
msg1 match {
case AuraTest.DoUpdateAuraEffect() => true
case _ => false
}
)
assert(obj.Aura.size == 1)
obj.Actor ! AuraEffectBehavior.EndEffect(Aura.Fire)
updateProbe.expectNoMessage(1000 milliseconds)
assert(obj.Aura.size == 1)
}
}
}
object AuraTest {
class Agency(obj: AuraEffectBehavior.Target, updateRef: ActorRef) extends Actor with AuraEffectBehavior {
def AuraTargetObject : Target = obj
ApplicableEffect(Aura.Plasma)
def receive: Receive = auraBehavior.orElse {
case _ => ;
}
def UpdateAuraEffect(target : Target) : Unit = {
updateRef ! DoUpdateAuraEffect()
}
}
class Entity extends PlanetSideServerObject with AuraContainer {
def Faction = PlanetSideEmpire.NEUTRAL
def Definition = null
}
final case class DoUpdateAuraEffect()
}

View file

@ -8,7 +8,7 @@ import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile}
import net.psforever.objects.equipment._
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
import net.psforever.objects.loadouts.Loadout
import net.psforever.objects.serverobject.aura.AuraEffectBehavior
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
@ -36,13 +36,18 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
with ContainableBehavior
with AggravatedBehavior
with AuraEffectBehavior {
def JammableObject = player
def JammableObject = player
def DamageableObject = player
def ContainerObject = player
def AggravatedObject = player
ApplicableEffect(Aura.Plasma)
ApplicableEffect(Aura.Napalm)
ApplicableEffect(Aura.Comet)
ApplicableEffect(Aura.Fire)
def AuraTargetObject = player
@ -922,4 +927,28 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID)
)
}
def UpdateAuraEffect(target: AuraEffectBehavior.Target) : Unit = {
import services.avatar.{AvatarAction, AvatarServiceMessage}
val zone = target.Zone
val value = target.Aura.foldLeft(0)(_ + PlayerControl.auraEffectToAttributeValue(_))
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttributeToAll(target.GUID, 54, value))
}
}
object PlayerControl {
/**
* Transform an applicable Aura effect into its `PlanetsideAttributeMessage` value.
* @see `Aura`
* @see `PlanetsideAttributeMessage`
* @param effect the aura effect
* @return the attribute value for that effect
*/
private def auraEffectToAttributeValue(effect: Aura): Int = effect match {
case Aura.Plasma => 1
case Aura.Comet => 2
case Aura.Napalm => 4
case Aura.Fire => 8
case _ => 0
}
}

View file

@ -1,7 +1,6 @@
// 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.DamageType
import net.psforever.types.{ExoSuitType, Vector3}
@ -135,26 +134,68 @@ object DamageModifiers {
}
}
/*
Below this point are the calculations for sources of 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 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
@ -186,6 +227,24 @@ object DamageModifiers {
}
}
/**
* 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
@ -207,16 +266,11 @@ object DamageModifiers {
(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 {
val degradedDamage = damage * degradation
if (degradedDamage > resist) {
degradedDamage toInt
}
else {
damage
}
(damage * degradation).toInt + resist
}
}
case _ =>
@ -227,14 +281,21 @@ object DamageModifiers {
}
}
/**
* 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, data.target) match {
case (Some(aggravation), v : VehicleSource) if GlobalDefinitions.isFlightVehicle(v.Definition) =>
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
@ -250,13 +311,21 @@ object DamageModifiers {
}
}
/**
* 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, data.target) match {
case (Some(aggravation), v : VehicleSource) if GlobalDefinitions.isFlightVehicle(v.Definition) =>
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
@ -272,6 +341,13 @@ object DamageModifiers {
}
}
/**
* 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
@ -295,6 +371,14 @@ object DamageModifiers {
}
}
/**
* 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

View file

@ -19,7 +19,7 @@ import shapeless.{::, HNil}
* @param unk3b if no global unique identifier (above), the name of the entity absorbing the damage
* @param unk3c if no global unique identifier (above), the object type of the entity absorbing the damage
* @param unk3d na
* @param unk4 na
* @param unk4 an indicator for the target-specific vital statistic being affected
* @param unk5 the amount of damage
* @param unk6 na
*/
@ -66,6 +66,13 @@ final case class DamageFeedbackMessage(
}
object DamageFeedbackMessage extends Marshallable[DamageFeedbackMessage] {
def apply(unk1: Int,
unk2: PlanetSideGUID,
unk3: PlanetSideGUID,
unk4: Int,
unk5: Long): DamageFeedbackMessage =
DamageFeedbackMessage(unk1, true, Some(unk2), None, None, true, Some(unk3), None, None, None, unk4, unk5, 0)
implicit val codec: Codec[DamageFeedbackMessage] = (
("unk1" | uint4) ::
(bool >>:~ { u2 =>

View file

@ -33,7 +33,7 @@ object DamageWithPositionMessage extends Marshallable[DamageWithPositionMessage]
).xmap[DamageWithPositionMessage] (
{
case unk :: pos :: HNil =>
DamageWithPositionMessage(math.min(0, math.max(unk, 255)), pos)
DamageWithPositionMessage(math.max(0, math.min(unk, 255)), pos)
},
{
case DamageWithPositionMessage(unk, pos) =>

View file

@ -626,7 +626,7 @@ class DamageableMountableDamageTest extends ActorTest {
msg1_3(1) match {
case AvatarServiceMessage(
"TestCharacter2",
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 2, 2)))
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(_, Vector3(2, 2, 2)))
) =>
true
case _ => false
@ -737,8 +737,10 @@ class DamageableWeaponTurretDamageTest extends ActorTest {
}
val activityProbe = TestProbe()
val avatarProbe = TestProbe()
val vehicleProbe = TestProbe()
zone.Activity = activityProbe.ref
zone.AvatarEvents = avatarProbe.ref
zone.VehicleEvents = vehicleProbe.ref
val turret = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) //2
turret.Actor = system.actorOf(Props(classOf[TurretControl], turret), "turret-control")
turret.Zone = zone
@ -787,16 +789,17 @@ class DamageableWeaponTurretDamageTest extends ActorTest {
assert(turret.Health == turret.Definition.DefaultHealth)
turret.Actor ! Vitality.Damage(applyDamageTo)
val msg1_3 = avatarProbe.receiveN(2, 500 milliseconds)
val msg2 = activityProbe.receiveOne(500 milliseconds)
val msg12 = vehicleProbe.receiveOne(500 milliseconds)
val msg3 = activityProbe.receiveOne(500 milliseconds)
val msg4 = avatarProbe.receiveOne(500 milliseconds)
assert(
msg1_3.head match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case _ => false
msg12 match {
case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(2), 0, _)) => true
case _ => false
}
)
assert(
msg2 match {
msg3 match {
case activity: Zone.HotSpot.Activity =>
activity.attacker == PlayerSource(player1) &&
activity.defender == turretSource &&
@ -805,10 +808,10 @@ class DamageableWeaponTurretDamageTest extends ActorTest {
}
)
assert(
msg1_3(1) match {
msg4 match {
case AvatarServiceMessage(
"TestCharacter2",
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 2, 2)))
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(_, Vector3(2, 2, 2)))
) =>
true
case _ => false
@ -1130,40 +1133,36 @@ class DamageableVehicleDamageTest extends ActorTest {
assert(atv.Shields == 1)
atv.Actor ! Vitality.Damage(applyDamageTo)
val msg1_3 = avatarProbe.receiveN(2, 500 milliseconds)
val msg2 = activityProbe.receiveOne(200 milliseconds)
val msg4 = vehicleProbe.receiveOne(200 milliseconds)
val msg12 = vehicleProbe.receiveN(2, 200 milliseconds)
val msg3 = activityProbe.receiveOne(200 milliseconds)
val msg4 = avatarProbe.receiveOne(200 milliseconds)
assert(
msg1_3.head match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true
case _ => false
msg12.head match {
case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)) => true
case _ => false
}
)
assert(
msg2 match {
msg12(1) match {
case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 0, _)) => true
case _ => false
}
)
assert(
msg3 match {
case activity: Zone.HotSpot.Activity =>
activity.attacker == PlayerSource(player1) &&
activity.defender == vehicleSource &&
activity.location == Vector3(1, 0, 0)
case _ => false
}
)
assert(
msg1_3(1) match {
case AvatarServiceMessage(
"TestCharacter2",
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0)))
) =>
true
activity.defender == vehicleSource &&
activity.location == Vector3(1, 0, 0)
case _ => false
}
)
assert(
msg4 match {
case VehicleServiceMessage(
channel,
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)
) if channel.equals(atv.Actor.toString) =>
case AvatarServiceMessage(
"TestCharacter2",
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(9, Vector3(2, 0, 0)))
) =>
true
case _ => false
}
@ -1264,17 +1263,23 @@ class DamageableVehicleDamageMountedTest extends ActorTest {
assert(atv.Shields == 1)
lodestar.Actor ! Vitality.Damage(applyDamageTo)
val msg1_35 = avatarProbe.receiveN(3, 500 milliseconds)
val msg2 = activityProbe.receiveOne(200 milliseconds)
val msg4 = vehicleProbe.receiveOne(200 milliseconds)
val msg12 = vehicleProbe.receiveN(2, 200 milliseconds)
val msg3 = activityProbe.receiveOne(200 milliseconds)
val msg45 = avatarProbe.receiveN(2,200 milliseconds)
assert(
msg1_35.head match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true
case _ => false
msg12.head match {
case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)) => true
case _ => false
}
)
assert(
msg2 match {
msg12(1) match {
case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 0, _)) => true
case _ => false
}
)
assert(
msg3 match {
case activity: Zone.HotSpot.Activity =>
activity.attacker == PlayerSource(player1) &&
activity.defender == vehicleSource &&
@ -1283,30 +1288,20 @@ class DamageableVehicleDamageMountedTest extends ActorTest {
}
)
assert(
msg1_35(1) match {
msg45.head match {
case AvatarServiceMessage(
"TestCharacter2",
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0)))
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(400, Vector3(2, 0, 0)))
) =>
true
case _ => false
}
)
assert(
msg4 match {
case VehicleServiceMessage(
channel,
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)
) if channel.equals(lodestar.Actor.toString) =>
true
case _ => false
}
)
assert(
msg1_35(2) match {
msg45(1) match {
case AvatarServiceMessage(
"TestCharacter3",
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0)))
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(0, Vector3(2, 0, 0)))
) =>
true
case _ => false

View file

@ -413,14 +413,13 @@ class PlayerControlDamageTest extends ActorTest {
)
assert(
msg_avatar(1) match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => true
case _ => false
}
)
assert(
msg_avatar(2) match {
case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) =>
true
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case _ => false
}
)
@ -437,7 +436,7 @@ class PlayerControlDamageTest extends ActorTest {
msg_avatar(3) match {
case AvatarServiceMessage(
"TestCharacter2",
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0)))
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(17, Vector3(2, 0, 0)))
) =>
true
case _ => false