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()
}