diff --git a/common/src/main/scala/net/psforever/objects/ballistics/AggravatedDamage.scala b/common/src/main/scala/net/psforever/objects/ballistics/AggravatedDamage.scala index cf445115..7fad9e15 100644 --- a/common/src/main/scala/net/psforever/objects/ballistics/AggravatedDamage.scala +++ b/common/src/main/scala/net/psforever/objects/ballistics/AggravatedDamage.scala @@ -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, diff --git a/common/src/main/scala/net/psforever/objects/ballistics/ProjectileQuality.scala b/common/src/main/scala/net/psforever/objects/ballistics/ProjectileQuality.scala index 8fe9d38c..9a630845 100644 --- a/common/src/main/scala/net/psforever/objects/ballistics/ProjectileQuality.scala +++ b/common/src/main/scala/net/psforever/objects/ballistics/ProjectileQuality.scala @@ -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. */ diff --git a/common/src/main/scala/net/psforever/objects/serverobject/aura/AuraEffectBehavior.scala b/common/src/main/scala/net/psforever/objects/serverobject/aura/AuraEffectBehavior.scala index 6dc233a5..51683d18 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/aura/AuraEffectBehavior.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/aura/AuraEffectBehavior.scala @@ -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
+ * only effects that are initialized to this mapping are approved for display on this target
+ * 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 - } } diff --git a/common/src/main/scala/net/psforever/packet/game/AggravatedDamageMessage.scala b/common/src/main/scala/net/psforever/packet/game/AggravatedDamageMessage.scala index faa48528..2456bee2 100644 --- a/common/src/main/scala/net/psforever/packet/game/AggravatedDamageMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/AggravatedDamageMessage.scala @@ -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] } diff --git a/common/src/test/scala/objects/AuraTest.scala b/common/src/test/scala/objects/AuraTest.scala new file mode 100644 index 00000000..a4af3983 --- /dev/null +++ b/common/src/test/scala/objects/AuraTest.scala @@ -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() +} diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index e9a3b956..bed0afbb 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -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 + } } diff --git a/src/main/scala/net/psforever/objects/vital/damage/DamageModifiers.scala b/src/main/scala/net/psforever/objects/vital/damage/DamageModifiers.scala index e1be38de..e0009f1e 100644 --- a/src/main/scala/net/psforever/objects/vital/damage/DamageModifiers.scala +++ b/src/main/scala/net/psforever/objects/vital/damage/DamageModifiers.scala @@ -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 diff --git a/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala b/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala index 181a67b2..f88bcf03 100644 --- a/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala +++ b/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala @@ -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 => diff --git a/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala b/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala index 3e6406b4..d6e4a2df 100644 --- a/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala @@ -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) => diff --git a/src/test/scala/objects/DamageableTest.scala b/src/test/scala/objects/DamageableTest.scala index 63c59290..d5973874 100644 --- a/src/test/scala/objects/DamageableTest.scala +++ b/src/test/scala/objects/DamageableTest.scala @@ -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 diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala index 56b34460..9b261bdf 100644 --- a/src/test/scala/objects/PlayerControlTest.scala +++ b/src/test/scala/objects/PlayerControlTest.scala @@ -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