mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-07-12 06:55:10 +00:00
eliminated multiple timers for a single aura effect; added comments; added tests; fixed tests
This commit is contained in:
parent
fc89355acf
commit
f627571f0e
11 changed files with 616 additions and 185 deletions
|
|
@ -4,20 +4,54 @@ import net.psforever.objects.equipment.TargetValidation
|
||||||
import net.psforever.objects.serverobject.aura.Aura
|
import net.psforever.objects.serverobject.aura.Aura
|
||||||
import net.psforever.objects.vital.DamageType
|
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])
|
final case class AggravatedTiming(duration: Long, ticks: Option[Int])
|
||||||
|
|
||||||
object AggravatedTiming {
|
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)
|
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))
|
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,
|
final case class AggravatedInfo(damage_type: DamageType.Value,
|
||||||
degradation_percentage: Float,
|
degradation_percentage: Float,
|
||||||
infliction_rate: Long) {
|
infliction_rate: Long) {
|
||||||
assert(damage_type == DamageType.Direct || damage_type == DamageType.Splash, s"aggravated damage is an unsupported type - $damage_type")
|
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],
|
final case class AggravatedDamage(info: List[AggravatedInfo],
|
||||||
effect_type: Aura,
|
effect_type: Aura,
|
||||||
timing: AggravatedTiming,
|
timing: AggravatedTiming,
|
||||||
|
|
@ -27,6 +61,14 @@ final case class AggravatedDamage(info: List[AggravatedInfo],
|
||||||
targets: List[TargetValidation])
|
targets: List[TargetValidation])
|
||||||
|
|
||||||
object AggravatedDamage {
|
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,
|
def apply(info: AggravatedInfo,
|
||||||
effect_type: Aura,
|
effect_type: Aura,
|
||||||
timing: AggravatedTiming,
|
timing: AggravatedTiming,
|
||||||
|
|
@ -42,6 +84,15 @@ object AggravatedDamage {
|
||||||
targets
|
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,
|
def apply(info: AggravatedInfo,
|
||||||
effect_type: Aura,
|
effect_type: Aura,
|
||||||
timing: AggravatedTiming,
|
timing: AggravatedTiming,
|
||||||
|
|
@ -58,6 +109,14 @@ object AggravatedDamage {
|
||||||
targets
|
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,
|
def apply(info: AggravatedInfo,
|
||||||
effect_type: Aura,
|
effect_type: Aura,
|
||||||
duration: Long,
|
duration: Long,
|
||||||
|
|
@ -73,6 +132,15 @@ object AggravatedDamage {
|
||||||
targets
|
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,
|
def apply(info: AggravatedInfo,
|
||||||
effect_type: Aura,
|
effect_type: Aura,
|
||||||
duration: Long,
|
duration: Long,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
sealed trait SameAsQuality extends ProjectileQuality {
|
||||||
def mod: Float = 1f
|
def mod: Float = 1f
|
||||||
|
|
@ -25,7 +25,7 @@ object ProjectileQuality {
|
||||||
/** Standard projectile quality. More of a flag than a modifier. */
|
/** Standard projectile quality. More of a flag than a modifier. */
|
||||||
case object Normal extends SameAsQuality
|
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
|
case object AggravatesTarget extends SameAsQuality
|
||||||
|
|
||||||
/** The complete lack of quality. Even the numeric modifier is zeroed. */
|
/** The complete lack of quality. Even the numeric modifier is zeroed. */
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,28 @@
|
||||||
package net.psforever.objects.serverobject.aura
|
package net.psforever.objects.serverobject.aura
|
||||||
|
|
||||||
import akka.actor.{Actor, Cancellable}
|
import akka.actor.{Actor, Cancellable}
|
||||||
|
import net.psforever.objects.Default
|
||||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
import scala.concurrent.duration._
|
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 {
|
trait AuraEffectBehavior {
|
||||||
_ : Actor =>
|
_ : Actor =>
|
||||||
private var activeEffectIndex: Long = 0
|
/** active aura effects are monotonic, but the timer will be updated for continuing and cancelling effects as well<br>
|
||||||
private val effectToEntryId: mutable.HashMap[Aura, List[Long]] =
|
* only effects that are initialized to this mapping are approved for display on this target<br>
|
||||||
mutable.HashMap.empty[Aura, List[Long]]
|
* key - aura effect; value - the timer for that effect
|
||||||
private val effectIdToTimer: mutable.LongMap[Cancellable] =
|
* @see `ApplicableEffect`
|
||||||
mutable.LongMap.empty[Cancellable]
|
*/
|
||||||
|
private val effectToTimer: mutable.HashMap[Aura, Cancellable] = mutable.HashMap.empty[Aura, Cancellable]
|
||||||
|
|
||||||
def AuraTargetObject: AuraEffectBehavior.Target
|
def AuraTargetObject: AuraEffectBehavior.Target
|
||||||
|
|
||||||
|
|
@ -22,144 +31,142 @@ trait AuraEffectBehavior {
|
||||||
case AuraEffectBehavior.StartEffect(effect, duration) =>
|
case AuraEffectBehavior.StartEffect(effect, duration) =>
|
||||||
StartAuraEffect(effect, duration)
|
StartAuraEffect(effect, duration)
|
||||||
|
|
||||||
case AuraEffectBehavior.EndEffect(Some(id), None) =>
|
case AuraEffectBehavior.EndEffect(effect) =>
|
||||||
EndAuraEffect(id)
|
EndAuraEffectAndUpdate(effect)
|
||||||
|
|
||||||
case AuraEffectBehavior.EndEffect(None, Some(effect)) =>
|
|
||||||
EndAuraEffect(effect)
|
|
||||||
|
|
||||||
case AuraEffectBehavior.EndAllEffects() =>
|
case AuraEffectBehavior.EndAllEffects() =>
|
||||||
EndAllEffectsAndUpdate()
|
EndAllEffectsAndUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
final def GetUnusedEffectId: Long = {
|
/**
|
||||||
val id = activeEffectIndex
|
* Only pre-apporved aura effects will be emitted by this target.
|
||||||
activeEffectIndex += 1
|
* @param effect the aura effect
|
||||||
id
|
*/
|
||||||
|
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 obj = AuraTargetObject
|
||||||
val auraEffects = obj.Aura
|
val auraEffectsBefore = obj.Aura.size
|
||||||
if (obj.Aura.contains(effect)) {
|
if(StartAuraTimer(effect, duration) && obj.AddEffectToAura(effect).size > auraEffectsBefore) {
|
||||||
effectToEntryId.getOrElse(effect, List[Long](AuraEffectBehavior.InvalidEffectId)).headOption //grab an available active effect id
|
//new effect; update visuals
|
||||||
}
|
UpdateAuraEffect(AuraTargetObject)
|
||||||
else if(obj.AddEffectToAura(effect).diff(auraEffects).contains(effect)) {
|
|
||||||
Some(StartAuraEffect(GetUnusedEffectId, effect, duration))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
//pair aura effect with id
|
||||||
effectToEntryId.get(effect) match {
|
(effectToTimer.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
|
|
||||||
case None =>
|
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 {
|
* Stop the target entity from emitting the aura particle effect, if it currently is.
|
||||||
case Some(timer) => timer.cancel
|
* @param effect the target effect
|
||||||
case _ => ;
|
* @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 = {
|
||||||
def EndAuraEffect(effect: Aura): Unit = {
|
effectToTimer.get(effect) match {
|
||||||
effectToEntryId.remove(effect) match {
|
case Some(timer) if !timer.isCancelled =>
|
||||||
case Some(idList) =>
|
timer.cancel()
|
||||||
idList.foreach { id =>
|
effectToTimer(effect) = Default.Cancellable
|
||||||
val obj = AuraTargetObject
|
AuraTargetObject.RemoveEffectFromAura(effect)
|
||||||
CancelEffectTimer(id)
|
true
|
||||||
obj.RemoveEffectFromAura(effect)
|
case _ =>
|
||||||
UpdateAuraEffect(obj)
|
false
|
||||||
}
|
|
||||||
case _ => ;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the target entity from emitting all aura particle effects.
|
||||||
|
*/
|
||||||
def EndAllEffects() : Unit = {
|
def EndAllEffects() : Unit = {
|
||||||
effectIdToTimer.values.foreach { _.cancel }
|
effectToTimer.keysIterator.foreach { effect =>
|
||||||
effectIdToTimer.clear
|
effectToTimer(effect).cancel()
|
||||||
effectToEntryId.clear
|
effectToTimer(effect) = Default.Cancellable
|
||||||
|
}
|
||||||
val obj = AuraTargetObject
|
val obj = AuraTargetObject
|
||||||
obj.Aura.foreach { obj.RemoveEffectFromAura }
|
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 = {
|
def EndAllEffectsAndUpdate() : Unit = {
|
||||||
EndAllEffects()
|
EndAllEffects()
|
||||||
UpdateAuraEffect(AuraTargetObject)
|
UpdateAuraEffect(AuraTargetObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
def UpdateAuraEffect(target: AuraEffectBehavior.Target) : Unit = {
|
/**
|
||||||
import services.avatar.{AvatarAction, AvatarServiceMessage}
|
* Is the target entity emitting the aura effect?
|
||||||
val zone = target.Zone
|
* @param effect the effect being tested
|
||||||
val value = target.Aura.foldLeft(0)(_ + AuraEffectBehavior.effectToAttributeValue(_))
|
* @return `true`, if the effect is currently being emitted;
|
||||||
zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttributeToAll(target.GUID, 54, value))
|
* `false`, otherwise
|
||||||
}
|
*/
|
||||||
|
def TestForEffect(effect: Aura): Boolean = {
|
||||||
def TestForEffect(id: Long): Aura = {
|
effectToTimer.get(effect) match {
|
||||||
effectToEntryId.find { case (_, ids) => ids.contains(id) } match {
|
case None => false
|
||||||
case Some((effect, _)) => effect
|
case Some(timer) => timer.isCancelled
|
||||||
case _ => Aura.Nothing
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
object AuraEffectBehavior {
|
||||||
type Target = PlanetSideServerObject with AuraContainer
|
type Target = PlanetSideServerObject with AuraContainer
|
||||||
|
|
||||||
final val InvalidEffectId = -1
|
|
||||||
|
|
||||||
final case class StartEffect(effect: Aura, duration: Long)
|
final case class StartEffect(effect: Aura, duration: Long)
|
||||||
|
|
||||||
final case class EndEffect(id: Option[Long], aura: Option[Aura])
|
final case class EndEffect(aura: Aura)
|
||||||
|
|
||||||
object EndEffect {
|
|
||||||
def apply(id: Long): EndEffect = EndEffect(Some(id), None)
|
|
||||||
|
|
||||||
def apply(aura: Aura): EndEffect = EndEffect(None, Some(aura))
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class EndAllEffects()
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,17 @@ import scodec.Codec
|
||||||
import scodec.codecs._
|
import scodec.codecs._
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* na
|
* Dispatched from the server to cause a damage reaction from a specific target.
|
||||||
* @param guid na
|
* Infantry targets should be the primary target of this packet, as indicated by their identifier.
|
||||||
* @param unk na
|
* 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,
|
final case class AggravatedDamageMessage(guid : PlanetSideGUID,
|
||||||
unk : Long)
|
damage : Long)
|
||||||
extends PlanetSideGamePacket {
|
extends PlanetSideGamePacket {
|
||||||
type Packet = AggravatedDamageMessage
|
type Packet = AggravatedDamageMessage
|
||||||
def opcode = GamePacketOpcode.AggravatedDamageMessage
|
def opcode = GamePacketOpcode.AggravatedDamageMessage
|
||||||
|
|
@ -22,6 +27,6 @@ final case class AggravatedDamageMessage(guid : PlanetSideGUID,
|
||||||
object AggravatedDamageMessage extends Marshallable[AggravatedDamageMessage] {
|
object AggravatedDamageMessage extends Marshallable[AggravatedDamageMessage] {
|
||||||
implicit val codec : Codec[AggravatedDamageMessage] = (
|
implicit val codec : Codec[AggravatedDamageMessage] = (
|
||||||
("guid" | PlanetSideGUID.codec) ::
|
("guid" | PlanetSideGUID.codec) ::
|
||||||
("unk" | uint32L)
|
("damage" | uint32L)
|
||||||
).as[AggravatedDamageMessage]
|
).as[AggravatedDamageMessage]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
237
common/src/test/scala/objects/AuraTest.scala
Normal file
237
common/src/test/scala/objects/AuraTest.scala
Normal 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()
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile}
|
||||||
import net.psforever.objects.equipment._
|
import net.psforever.objects.equipment._
|
||||||
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
|
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
|
||||||
import net.psforever.objects.loadouts.Loadout
|
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.containable.{Containable, ContainableBehavior}
|
||||||
import net.psforever.objects.serverobject.damage.Damageable.Target
|
import net.psforever.objects.serverobject.damage.Damageable.Target
|
||||||
import net.psforever.objects.vital.PlayerSuicide
|
import net.psforever.objects.vital.PlayerSuicide
|
||||||
|
|
@ -36,13 +36,18 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
|
||||||
with ContainableBehavior
|
with ContainableBehavior
|
||||||
with AggravatedBehavior
|
with AggravatedBehavior
|
||||||
with AuraEffectBehavior {
|
with AuraEffectBehavior {
|
||||||
def JammableObject = player
|
|
||||||
|
def JammableObject = player
|
||||||
|
|
||||||
def DamageableObject = player
|
def DamageableObject = player
|
||||||
|
|
||||||
def ContainerObject = player
|
def ContainerObject = player
|
||||||
|
|
||||||
def AggravatedObject = player
|
def AggravatedObject = player
|
||||||
|
ApplicableEffect(Aura.Plasma)
|
||||||
|
ApplicableEffect(Aura.Napalm)
|
||||||
|
ApplicableEffect(Aura.Comet)
|
||||||
|
ApplicableEffect(Aura.Fire)
|
||||||
|
|
||||||
def AuraTargetObject = player
|
def AuraTargetObject = player
|
||||||
|
|
||||||
|
|
@ -922,4 +927,28 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
|
||||||
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright (c) 2020 PSForever
|
// Copyright (c) 2020 PSForever
|
||||||
package net.psforever.objects.vital.damage
|
package net.psforever.objects.vital.damage
|
||||||
|
|
||||||
import net.psforever.objects.GlobalDefinitions
|
|
||||||
import net.psforever.objects.ballistics._
|
import net.psforever.objects.ballistics._
|
||||||
import net.psforever.objects.vital.DamageType
|
import net.psforever.objects.vital.DamageType
|
||||||
import net.psforever.types.{ExoSuitType, Vector3}
|
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 {
|
case object InfantryAggravatedDirect extends Mod {
|
||||||
def Calculate: DamageModifiers.Format =
|
def Calculate: DamageModifiers.Format =
|
||||||
BaseAggravatedFormula(ProjectileResolution.AggravatedDirect, DamageType.Direct)
|
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 {
|
case object InfantryAggravatedSplash extends Mod {
|
||||||
def Calculate: DamageModifiers.Format =
|
def Calculate: DamageModifiers.Format =
|
||||||
BaseAggravatedFormula(ProjectileResolution.AggravatedSplash, DamageType.Splash)
|
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 {
|
case object InfantryAggravatedDirectBurn extends Mod {
|
||||||
def Calculate: DamageModifiers.Format =
|
def Calculate: DamageModifiers.Format =
|
||||||
BaseAggravatedBurnFormula(ProjectileResolution.AggravatedDirectBurn, DamageType.Direct)
|
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 {
|
case object InfantryAggravatedSplashBurn extends Mod {
|
||||||
def Calculate: DamageModifiers.Format =
|
def Calculate: DamageModifiers.Format =
|
||||||
BaseAggravatedBurnFormula(ProjectileResolution.AggravatedSplashBurn, DamageType.Splash)
|
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(
|
private def BaseAggravatedFormula(
|
||||||
resolution: ProjectileResolution.Value,
|
resolution: ProjectileResolution.Value,
|
||||||
damageType : DamageType.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(
|
private def BaseAggravatedBurnFormula(
|
||||||
resolution: ProjectileResolution.Value,
|
resolution: ProjectileResolution.Value,
|
||||||
damageType : DamageType.Value
|
damageType : DamageType.Value
|
||||||
|
|
@ -207,16 +266,11 @@ object DamageModifiers {
|
||||||
(damage * degradation * aggravation.max_factor) toInt
|
(damage * degradation * aggravation.max_factor) toInt
|
||||||
} else {
|
} else {
|
||||||
val resist = data.damage_model.ResistUsing(data)(data)
|
val resist = data.damage_model.ResistUsing(data)(data)
|
||||||
|
//add resist to offset resist subtraction later
|
||||||
if (damage > resist) {
|
if (damage > resist) {
|
||||||
((damage - resist) * degradation).toInt + resist
|
((damage - resist) * degradation).toInt + resist
|
||||||
} else {
|
} else {
|
||||||
val degradedDamage = damage * degradation
|
(damage * degradation).toInt + resist
|
||||||
if (degradedDamage > resist) {
|
|
||||||
degradedDamage toInt
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
damage
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case _ =>
|
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 {
|
case object StarfireAggravated extends Mod {
|
||||||
def Calculate: DamageModifiers.Format = formula
|
def Calculate: DamageModifiers.Format = formula
|
||||||
|
|
||||||
private def formula(damage: Int, data: ResolvedProjectile): Int = {
|
private def formula(damage: Int, data: ResolvedProjectile): Int = {
|
||||||
if (data.resolution == ProjectileResolution.AggravatedDirect &&
|
if (data.resolution == ProjectileResolution.AggravatedDirect &&
|
||||||
data.projectile.quality == ProjectileQuality.AggravatesTarget) {
|
data.projectile.quality == ProjectileQuality.AggravatesTarget) {
|
||||||
(data.projectile.profile.Aggravated, data.target) match {
|
data.projectile.profile.Aggravated match {
|
||||||
case (Some(aggravation), v : VehicleSource) if GlobalDefinitions.isFlightVehicle(v.Definition) =>
|
case Some(aggravation) =>
|
||||||
aggravation.info.find(_.damage_type == DamageType.Direct) match {
|
aggravation.info.find(_.damage_type == DamageType.Direct) match {
|
||||||
case Some(infos) =>
|
case Some(infos) =>
|
||||||
(damage * infos.degradation_percentage + damage) toInt
|
(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 {
|
case object StarfireAggravatedBurn extends Mod {
|
||||||
def Calculate: DamageModifiers.Format = formula
|
def Calculate: DamageModifiers.Format = formula
|
||||||
|
|
||||||
private def formula(damage: Int, data: ResolvedProjectile): Int = {
|
private def formula(damage: Int, data: ResolvedProjectile): Int = {
|
||||||
if (data.resolution == ProjectileResolution.AggravatedDirectBurn) {
|
if (data.resolution == ProjectileResolution.AggravatedDirectBurn) {
|
||||||
(data.projectile.profile.Aggravated, data.target) match {
|
data.projectile.profile.Aggravated match {
|
||||||
case (Some(aggravation), v : VehicleSource) if GlobalDefinitions.isFlightVehicle(v.Definition) =>
|
case Some(aggravation) =>
|
||||||
aggravation.info.find(_.damage_type == DamageType.Direct) match {
|
aggravation.info.find(_.damage_type == DamageType.Direct) match {
|
||||||
case Some(infos) =>
|
case Some(infos) =>
|
||||||
(math.floor(damage * infos.degradation_percentage) * data.projectile.quality.mod) toInt
|
(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 {
|
case object CometAggravated extends Mod {
|
||||||
def Calculate: DamageModifiers.Format = formula
|
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 {
|
case object CometAggravatedBurn extends Mod {
|
||||||
def Calculate: DamageModifiers.Format = formula
|
def Calculate: DamageModifiers.Format = formula
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import shapeless.{::, HNil}
|
||||||
* @param unk3b if no global unique identifier (above), the name of the entity absorbing the damage
|
* @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 unk3c if no global unique identifier (above), the object type of the entity absorbing the damage
|
||||||
* @param unk3d na
|
* @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 unk5 the amount of damage
|
||||||
* @param unk6 na
|
* @param unk6 na
|
||||||
*/
|
*/
|
||||||
|
|
@ -66,6 +66,13 @@ final case class DamageFeedbackMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
object DamageFeedbackMessage extends Marshallable[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] = (
|
implicit val codec: Codec[DamageFeedbackMessage] = (
|
||||||
("unk1" | uint4) ::
|
("unk1" | uint4) ::
|
||||||
(bool >>:~ { u2 =>
|
(bool >>:~ { u2 =>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ object DamageWithPositionMessage extends Marshallable[DamageWithPositionMessage]
|
||||||
).xmap[DamageWithPositionMessage] (
|
).xmap[DamageWithPositionMessage] (
|
||||||
{
|
{
|
||||||
case unk :: pos :: HNil =>
|
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) =>
|
case DamageWithPositionMessage(unk, pos) =>
|
||||||
|
|
|
||||||
|
|
@ -626,7 +626,7 @@ class DamageableMountableDamageTest extends ActorTest {
|
||||||
msg1_3(1) match {
|
msg1_3(1) match {
|
||||||
case AvatarServiceMessage(
|
case AvatarServiceMessage(
|
||||||
"TestCharacter2",
|
"TestCharacter2",
|
||||||
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 2, 2)))
|
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(_, Vector3(2, 2, 2)))
|
||||||
) =>
|
) =>
|
||||||
true
|
true
|
||||||
case _ => false
|
case _ => false
|
||||||
|
|
@ -737,8 +737,10 @@ class DamageableWeaponTurretDamageTest extends ActorTest {
|
||||||
}
|
}
|
||||||
val activityProbe = TestProbe()
|
val activityProbe = TestProbe()
|
||||||
val avatarProbe = TestProbe()
|
val avatarProbe = TestProbe()
|
||||||
|
val vehicleProbe = TestProbe()
|
||||||
zone.Activity = activityProbe.ref
|
zone.Activity = activityProbe.ref
|
||||||
zone.AvatarEvents = avatarProbe.ref
|
zone.AvatarEvents = avatarProbe.ref
|
||||||
|
zone.VehicleEvents = vehicleProbe.ref
|
||||||
val turret = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) //2
|
val turret = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) //2
|
||||||
turret.Actor = system.actorOf(Props(classOf[TurretControl], turret), "turret-control")
|
turret.Actor = system.actorOf(Props(classOf[TurretControl], turret), "turret-control")
|
||||||
turret.Zone = zone
|
turret.Zone = zone
|
||||||
|
|
@ -787,16 +789,17 @@ class DamageableWeaponTurretDamageTest extends ActorTest {
|
||||||
assert(turret.Health == turret.Definition.DefaultHealth)
|
assert(turret.Health == turret.Definition.DefaultHealth)
|
||||||
|
|
||||||
turret.Actor ! Vitality.Damage(applyDamageTo)
|
turret.Actor ! Vitality.Damage(applyDamageTo)
|
||||||
val msg1_3 = avatarProbe.receiveN(2, 500 milliseconds)
|
val msg12 = vehicleProbe.receiveOne(500 milliseconds)
|
||||||
val msg2 = activityProbe.receiveOne(500 milliseconds)
|
val msg3 = activityProbe.receiveOne(500 milliseconds)
|
||||||
|
val msg4 = avatarProbe.receiveOne(500 milliseconds)
|
||||||
assert(
|
assert(
|
||||||
msg1_3.head match {
|
msg12 match {
|
||||||
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
|
case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(2), 0, _)) => true
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
msg2 match {
|
msg3 match {
|
||||||
case activity: Zone.HotSpot.Activity =>
|
case activity: Zone.HotSpot.Activity =>
|
||||||
activity.attacker == PlayerSource(player1) &&
|
activity.attacker == PlayerSource(player1) &&
|
||||||
activity.defender == turretSource &&
|
activity.defender == turretSource &&
|
||||||
|
|
@ -805,10 +808,10 @@ class DamageableWeaponTurretDamageTest extends ActorTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
msg1_3(1) match {
|
msg4 match {
|
||||||
case AvatarServiceMessage(
|
case AvatarServiceMessage(
|
||||||
"TestCharacter2",
|
"TestCharacter2",
|
||||||
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 2, 2)))
|
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(_, Vector3(2, 2, 2)))
|
||||||
) =>
|
) =>
|
||||||
true
|
true
|
||||||
case _ => false
|
case _ => false
|
||||||
|
|
@ -1130,40 +1133,36 @@ class DamageableVehicleDamageTest extends ActorTest {
|
||||||
assert(atv.Shields == 1)
|
assert(atv.Shields == 1)
|
||||||
|
|
||||||
atv.Actor ! Vitality.Damage(applyDamageTo)
|
atv.Actor ! Vitality.Damage(applyDamageTo)
|
||||||
val msg1_3 = avatarProbe.receiveN(2, 500 milliseconds)
|
val msg12 = vehicleProbe.receiveN(2, 200 milliseconds)
|
||||||
val msg2 = activityProbe.receiveOne(200 milliseconds)
|
val msg3 = activityProbe.receiveOne(200 milliseconds)
|
||||||
val msg4 = vehicleProbe.receiveOne(200 milliseconds)
|
val msg4 = avatarProbe.receiveOne(200 milliseconds)
|
||||||
assert(
|
assert(
|
||||||
msg1_3.head match {
|
msg12.head match {
|
||||||
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true
|
case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)) => true
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert(
|
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 =>
|
case activity: Zone.HotSpot.Activity =>
|
||||||
activity.attacker == PlayerSource(player1) &&
|
activity.attacker == PlayerSource(player1) &&
|
||||||
activity.defender == vehicleSource &&
|
activity.defender == vehicleSource &&
|
||||||
activity.location == Vector3(1, 0, 0)
|
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
|
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
msg4 match {
|
msg4 match {
|
||||||
case VehicleServiceMessage(
|
case AvatarServiceMessage(
|
||||||
channel,
|
"TestCharacter2",
|
||||||
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)
|
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(9, Vector3(2, 0, 0)))
|
||||||
) if channel.equals(atv.Actor.toString) =>
|
) =>
|
||||||
true
|
true
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
|
|
@ -1264,17 +1263,23 @@ class DamageableVehicleDamageMountedTest extends ActorTest {
|
||||||
assert(atv.Shields == 1)
|
assert(atv.Shields == 1)
|
||||||
|
|
||||||
lodestar.Actor ! Vitality.Damage(applyDamageTo)
|
lodestar.Actor ! Vitality.Damage(applyDamageTo)
|
||||||
val msg1_35 = avatarProbe.receiveN(3, 500 milliseconds)
|
val msg12 = vehicleProbe.receiveN(2, 200 milliseconds)
|
||||||
val msg2 = activityProbe.receiveOne(200 milliseconds)
|
val msg3 = activityProbe.receiveOne(200 milliseconds)
|
||||||
val msg4 = vehicleProbe.receiveOne(200 milliseconds)
|
val msg45 = avatarProbe.receiveN(2,200 milliseconds)
|
||||||
assert(
|
assert(
|
||||||
msg1_35.head match {
|
msg12.head match {
|
||||||
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true
|
case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)) => true
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert(
|
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 =>
|
case activity: Zone.HotSpot.Activity =>
|
||||||
activity.attacker == PlayerSource(player1) &&
|
activity.attacker == PlayerSource(player1) &&
|
||||||
activity.defender == vehicleSource &&
|
activity.defender == vehicleSource &&
|
||||||
|
|
@ -1283,30 +1288,20 @@ class DamageableVehicleDamageMountedTest extends ActorTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
msg1_35(1) match {
|
msg45.head match {
|
||||||
case AvatarServiceMessage(
|
case AvatarServiceMessage(
|
||||||
"TestCharacter2",
|
"TestCharacter2",
|
||||||
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0)))
|
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(400, Vector3(2, 0, 0)))
|
||||||
) =>
|
) =>
|
||||||
true
|
true
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
msg4 match {
|
msg45(1) 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 {
|
|
||||||
case AvatarServiceMessage(
|
case AvatarServiceMessage(
|
||||||
"TestCharacter3",
|
"TestCharacter3",
|
||||||
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0)))
|
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(0, Vector3(2, 0, 0)))
|
||||||
) =>
|
) =>
|
||||||
true
|
true
|
||||||
case _ => false
|
case _ => false
|
||||||
|
|
|
||||||
|
|
@ -413,14 +413,13 @@ class PlayerControlDamageTest extends ActorTest {
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
msg_avatar(1) match {
|
msg_avatar(1) match {
|
||||||
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
|
case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => true
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
msg_avatar(2) match {
|
msg_avatar(2) match {
|
||||||
case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) =>
|
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
|
||||||
true
|
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -437,7 +436,7 @@ class PlayerControlDamageTest extends ActorTest {
|
||||||
msg_avatar(3) match {
|
msg_avatar(3) match {
|
||||||
case AvatarServiceMessage(
|
case AvatarServiceMessage(
|
||||||
"TestCharacter2",
|
"TestCharacter2",
|
||||||
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0)))
|
AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(17, Vector3(2, 0, 0)))
|
||||||
) =>
|
) =>
|
||||||
true
|
true
|
||||||
case _ => false
|
case _ => false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue