diff --git a/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala b/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala
index 18dca79df..afce1d3fc 100644
--- a/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala
+++ b/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala
@@ -3,7 +3,7 @@ package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployableCategory}
-import net.psforever.objects.definition.DeployableDefinition
+import net.psforever.objects.definition.{DeployableDefinition, WithShields}
import net.psforever.objects.definition.converter.ShieldGeneratorConverter
import net.psforever.objects.equipment.{JammableBehavior, JammableUnit}
import net.psforever.objects.serverobject.damage.Damageable.Target
@@ -22,7 +22,8 @@ class ShieldGeneratorDeployable(cdef: ShieldGeneratorDefinition)
with Hackable
with JammableUnit
-class ShieldGeneratorDefinition extends DeployableDefinition(240) {
+class ShieldGeneratorDefinition extends DeployableDefinition(240)
+ with WithShields {
Packet = new ShieldGeneratorConverter
DeployCategory = DeployableCategory.ShieldGenerators
diff --git a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala
index c89fd5b6e..306fc1cbf 100644
--- a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala
+++ b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala
@@ -26,27 +26,8 @@ class VehicleDefinition(objectId: Int)
with VitalityDefinition
with NtuContainerDefinition
with ResistanceProfileMutators
- with DamageResistanceModel {
- /** ... */
- var shieldUiAttribute: Int = 68
- /** how many points of shield the vehicle starts with (should default to 0 if unset through the accessor) */
- private var defaultShields : Option[Int] = None
- /** maximum vehicle shields (generally: 20% of health)
- * for normal vehicles, offered through amp station facility benefits
- * for BFR's, it charges naturally
- **/
- private var maxShields: Int = 0
- /** the minimum amount of time that must elapse in between damage and shield charge activities (ms) */
- private var shieldChargeDamageCooldown : Long = 5000L
- /** the minimum amount of time that must elapse in between distinct shield charge activities (ms) */
- private var shieldChargePeriodicCooldown : Long = 1000L
- /** if the shield recharges on its own, this value will be non-`None` and indicate by how much */
- private var autoShieldRecharge : Option[Int] = None
- private var autoShieldRechargeSpecial : Option[Int] = None
- /** shield drain is what happens to the shield under special conditions, e.g., bfr flight;
- * the drain interval is 250ms which is convenient for us
- * we can skip needing to define is explicitly */
- private var shieldDrain : Option[Int] = None
+ with DamageResistanceModel
+ with WithShields {
private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]()
private var deployment: Boolean = false
private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap()
@@ -92,63 +73,6 @@ class VehicleDefinition(objectId: Int)
RepairRestoresAt = 1
registerAs = "vehicles"
- def DefaultShields: Int = defaultShields.getOrElse(0)
-
- def DefaultShields_=(shield: Int): Int = DefaultShields_=(Some(shield))
-
- def DefaultShields_=(shield: Option[Int]): Int = {
- defaultShields = shield
- DefaultShields
- }
-
- def MaxShields: Int = maxShields
-
- def MaxShields_=(shields: Int): Int = {
- maxShields = shields
- MaxShields
- }
-
- def ShieldPeriodicDelay : Long = shieldChargePeriodicCooldown
-
- def ShieldPeriodicDelay_=(cooldown: Long): Long = {
- shieldChargePeriodicCooldown = cooldown
- ShieldPeriodicDelay
- }
-
- def ShieldDamageDelay: Long = shieldChargeDamageCooldown
-
- def ShieldDamageDelay_=(cooldown: Long): Long = {
- shieldChargeDamageCooldown = cooldown
- ShieldDamageDelay
- }
-
- def ShieldAutoRecharge: Option[Int] = autoShieldRecharge
-
- def ShieldAutoRecharge_=(charge: Int): Option[Int] = ShieldAutoRecharge_=(Some(charge))
-
- def ShieldAutoRecharge_=(charge: Option[Int]): Option[Int] = {
- autoShieldRecharge = charge
- ShieldAutoRecharge
- }
-
- def ShieldAutoRechargeSpecial: Option[Int] = autoShieldRechargeSpecial.orElse(ShieldAutoRecharge)
-
- def ShieldAutoRechargeSpecial_=(charge: Int): Option[Int] = ShieldAutoRechargeSpecial_=(Some(charge))
-
- def ShieldAutoRechargeSpecial_=(charge: Option[Int]): Option[Int] = {
- autoShieldRechargeSpecial = charge
- ShieldAutoRechargeSpecial
- }
-
- def ShieldDrain: Option[Int] = shieldDrain
-
- def ShieldDrain_=(drain: Int): Option[Int] = ShieldDrain_=(Some(drain))
-
- def ShieldDrain_=(drain: Option[Int]): Option[Int] = {
- shieldDrain = drain
- ShieldDrain
- }
-
def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo
def CanBeOwned: Option[Boolean] = canBeOwned
@@ -300,6 +224,7 @@ class VehicleDefinition(objectId: Int)
)
}
+ //noinspection ScalaUnusedSymbol
def Uninitialize(obj: Vehicle, context: ActorContext): Unit = {
obj.Actor ! akka.actor.PoisonPill
obj.Actor = Default.Actor
diff --git a/src/main/scala/net/psforever/objects/definition/WithShields.scala b/src/main/scala/net/psforever/objects/definition/WithShields.scala
new file mode 100644
index 000000000..0d5b1b378
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/definition/WithShields.scala
@@ -0,0 +1,81 @@
+package net.psforever.objects.definition
+
+trait WithShields {
+ /** ... */
+ var shieldUiAttribute: Int = 68
+ /** how many points of shield the vehicle starts with (should default to 0 if unset through the accessor) */
+ private var defaultShields : Option[Int] = None
+ /** maximum vehicle shields (generally: 20% of health)
+ * for normal vehicles, offered through amp station facility benefits
+ * for BFR's, it charges naturally
+ **/
+ private var maxShields: Int = 0
+ /** the minimum amount of time that must elapse in between damage and shield charge activities (ms) */
+ private var shieldChargeDamageCooldown : Long = 5000L
+ /** the minimum amount of time that must elapse in between distinct shield charge activities (ms) */
+ private var shieldChargePeriodicCooldown : Long = 1000L
+ /** if the shield recharges on its own, this value will be non-`None` and indicate by how much */
+ private var autoShieldRecharge : Option[Int] = None
+ private var autoShieldRechargeSpecial : Option[Int] = None
+ /** shield drain is what happens to the shield under special conditions, e.g., bfr flight;
+ * the drain interval is 250ms which is convenient for us
+ * we can skip needing to define is explicitly */
+ private var shieldDrain : Option[Int] = None
+
+ def DefaultShields: Int = defaultShields.getOrElse(0)
+
+ def DefaultShields_=(shield: Int): Int = DefaultShields_=(Some(shield))
+
+ def DefaultShields_=(shield: Option[Int]): Int = {
+ defaultShields = shield
+ DefaultShields
+ }
+
+ def MaxShields: Int = maxShields
+
+ def MaxShields_=(shields: Int): Int = {
+ maxShields = shields
+ MaxShields
+ }
+
+ def ShieldPeriodicDelay : Long = shieldChargePeriodicCooldown
+
+ def ShieldPeriodicDelay_=(cooldown: Long): Long = {
+ shieldChargePeriodicCooldown = cooldown
+ ShieldPeriodicDelay
+ }
+
+ def ShieldDamageDelay: Long = shieldChargeDamageCooldown
+
+ def ShieldDamageDelay_=(cooldown: Long): Long = {
+ shieldChargeDamageCooldown = cooldown
+ ShieldDamageDelay
+ }
+
+ def ShieldAutoRecharge: Option[Int] = autoShieldRecharge
+
+ def ShieldAutoRecharge_=(charge: Int): Option[Int] = ShieldAutoRecharge_=(Some(charge))
+
+ def ShieldAutoRecharge_=(charge: Option[Int]): Option[Int] = {
+ autoShieldRecharge = charge
+ ShieldAutoRecharge
+ }
+
+ def ShieldAutoRechargeSpecial: Option[Int] = autoShieldRechargeSpecial.orElse(ShieldAutoRecharge)
+
+ def ShieldAutoRechargeSpecial_=(charge: Int): Option[Int] = ShieldAutoRechargeSpecial_=(Some(charge))
+
+ def ShieldAutoRechargeSpecial_=(charge: Option[Int]): Option[Int] = {
+ autoShieldRechargeSpecial = charge
+ ShieldAutoRechargeSpecial
+ }
+
+ def ShieldDrain: Option[Int] = shieldDrain
+
+ def ShieldDrain_=(drain: Int): Option[Int] = ShieldDrain_=(Some(drain))
+
+ def ShieldDrain_=(drain: Option[Int]): Option[Int] = {
+ shieldDrain = drain
+ ShieldDrain
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala
index 9936abab3..ecb84ac5e 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala
@@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret
-import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition}
+import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition, WithShields}
import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets}
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel
@@ -14,7 +14,8 @@ import scala.collection.mutable
trait TurretDefinition
extends MountableWeaponsDefinition
with ResistanceProfileMutators
- with DamageResistanceModel {
+ with DamageResistanceModel
+ with WithShields {
odef: ObjectDefinition =>
Turrets(odef.ObjectId) //let throw NoSuchElementException
/* key - upgrade, value - weapon definition */
diff --git a/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala b/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala
index e43dd9136..9f34f86b8 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala
@@ -1,17 +1,17 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
+import akka.actor.Cancellable
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.actor.typed.{Behavior, SupervisorStrategy}
-import net.psforever.objects.PlanetSideGameObject
+import net.psforever.objects.{Default, PlanetSideGameObject}
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
import net.psforever.objects.serverobject.affinity.FactionAffinity
-import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
-import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, InGameHistory, ReconstructionActivity, RepairFromExoSuitChange, RepairingActivity, RevivingActivity, SpawningActivity}
+import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry}
+import net.psforever.objects.vital.{DamageFromExplodingEntity, InGameActivity, InGameHistory}
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.zones.Zone
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
-import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
import scala.collection.mutable
import scala.concurrent.duration._
@@ -41,27 +41,7 @@ object ExperienceCalculator {
}
}
- def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
- val spawnIndex = history.lastIndexWhere {
- case _: SpawningActivity => true
- case _: RevivingActivity => true
- case _ => false
- }
- val endIndex = history.lastIndexWhere {
- case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
- case _ => false
- }
- if (spawnIndex == -1 || endIndex == -1) {
- Nil //throw VitalsHistoryException(history.head, "vitals history does not contain expected conditions")
- // } else
- // if (spawnIndex == -1) {
- // Nil //throw VitalsHistoryException(history.head, "vitals history does not contain initial spawn conditions")
- // } else if (endIndex == -1) {
- // Nil //throw VitalsHistoryException(history.last, "vitals history does not contain end of life conditions")
- } else {
- history.slice(spawnIndex, endIndex)
- }
- }
+ private case object AssistDecay extends Command
def calculateExperience(
victim: PlayerSource,
@@ -71,12 +51,7 @@ object ExperienceCalculator {
case (Some(spawn), Some(death)) => death.time - spawn.time
case _ => 0L
}
- val wasEverAMax = victim.ExoSuit == ExoSuitType.MAX || history.exists {
- case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
- case ReconstructionActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
- case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
- case _ => false
- }
+ val wasEverAMax = Support.wasEverAMax(victim, history)
val base = if (wasEverAMax) { //shamed
250L
} else if (victim.Seated || victim.kills.nonEmpty) {
@@ -94,38 +69,10 @@ object ExperienceCalculator {
base
}
}
+}
- def onlyOriginalAssistEntries(
- first: mutable.LongMap[ContributionStatsOutput],
- second: mutable.LongMap[ContributionStatsOutput]
- ): Iterable[ContributionStatsOutput] = {
- onlyOriginalAssistEntriesIterable(first.values, second.values)
- }
-
- def onlyOriginalAssistEntriesIterable(
- first: Iterable[ContributionStatsOutput],
- second: Iterable[ContributionStatsOutput]
- ): Iterable[ContributionStatsOutput] = {
- if (second.isEmpty) {
- first
- } else if (first.isEmpty) {
- second
- } else {
- //overlap discriminated by percentage
- val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
- for {
- h @ ContributionStatsOutput(hid, hwep, hkda) <- first
- a @ ContributionStatsOutput(aid, awep, akda) <- second
- (id, out) = if (hkda < akda)
- (aid.CharId, a.copy(implements = (a.implements ++ hwep).distinct))
- else
- (hid.CharId, h.copy(implements = (h.implements ++ awep).distinct))
- if hid == aid && shared.put(id, out).isEmpty
- } yield ()
- val sharedKeys = shared.keys
- (first ++ second).filterNot { case ContributionStatsOutput(id, _, _) => sharedKeys.exists(_ == id.CharId) } ++ shared.values
- }
- }
+sealed case class InheritedAssistEntry(assists: Iterable[ContributionStatsOutput]) {
+ val time: Long = System.currentTimeMillis()
}
class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone)
@@ -133,24 +80,43 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
import ExperienceCalculator._
+ private val retainedKillAssists: mutable.HashMap[SourceUniqueness, InheritedAssistEntry] =
+ mutable.HashMap[SourceUniqueness, InheritedAssistEntry]()
+
+ private val retainedSupportAssists: mutable.HashMap[SourceUniqueness, InheritedAssistEntry] =
+ mutable.HashMap[SourceUniqueness, InheritedAssistEntry]()
+
+ private var inheritedAssistsDecayTimer: Cancellable = Default.Cancellable
+
def onMessage(msg: Command): Behavior[Command] = {
msg match {
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
rewardThisPlayerDeath(victim, lastDamage, history)
- case RewardOurSupporters(target, history, kill, bep) =>
+
+ case RewardThisDeath(victim: SourceWithHealthEntry, lastDamage, history) =>
+ sortThisContributionForLater(victim, lastDamage, history)
+
+ case RewardOurSupporters(target: PlayerSource, history, kill, bep) =>
rewardTheseSupporters(target, history.toList, kill, bep)
+
+ case RewardOurSupporters(target: SourceWithHealthEntry, history, kill, _) =>
+ sortTheseSupportersForLater(target, history, kill)
+
+ case AssistDecay =>
+ performAssistDecay()
+
case _ => ()
}
Behaviors.same
}
- def rewardThisPlayerDeath(
- victim: PlayerSource,
- lastDamage: Option[DamageResult],
- history: Iterable[InGameActivity]
- ): Unit = {
- val shortHistory = ExperienceCalculator.limitHistoryToThisLife(history.toList)
- val everyone = determineKiller(lastDamage, shortHistory) match {
+ private def rewardThisPlayerDeath(
+ victim: PlayerSource,
+ lastDamage: Option[DamageResult],
+ history: Iterable[InGameActivity]
+ ): Unit = {
+ val shortHistory = Support.limitHistoryToThisLife(history.toList)
+ val everyone = KillDeathAssists.determineKiller(lastDamage, shortHistory) match {
case Some((result, killer: PlayerSource)) =>
val assists = collectAssistsForPlayer(victim, shortHistory, Some(killer))
val fullBep = KillDeathAssists.calculateExperience(killer, victim, shortHistory)
@@ -173,229 +139,138 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
}
}
- def determineKiller(lastDamageActivity: Option[DamageResult], history: List[InGameActivity]): Option[(DamageResult, SourceEntry)] = {
- val now = System.currentTimeMillis()
- val compareTimeMillis = 10.seconds.toMillis
- lastDamageActivity
- .collect { case dam if now - dam.interaction.hitTime < compareTimeMillis => dam }
- .flatMap { dam => Some(dam, dam.adversarial) }
- .orElse {
- history.collect { case damage: DamagingActivity
- if now - damage.time < compareTimeMillis && damage.data.adversarial.nonEmpty =>
- damage.data
- }
- .flatMap { dam => Some(dam, dam.adversarial) }.lastOption
- }
- .collect { case (dam, Some(adv)) => (dam, adv.attacker) }
- }
-
private[exp] def collectAssistsForPlayer(
victim: PlayerSource,
history: List[InGameActivity],
killerOpt: Option[PlayerSource]
): Iterable[ContributionStatsOutput] = {
- // val cardinalSin = victim.ExoSuit == ExoSuitType.MAX || history.exists {
- // case SpawningActivity(p: PlayerSource,_,_) => p.ExoSuit == ExoSuitType.MAX
- // case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
- // case _ => false
- // }
- val initialHealth = history
- .headOption
- .collect { case SpawningActivity(p: PlayerSource,_,_) => p.health } match {
- case Some(value) => value.toFloat
- case _ => 100f
- }
- val healthAssists = collectHealthAssists(
+ val healthAssists = Support.collectHealthAssists(
victim,
history,
- initialHealth,
- allocateContributors(healthDamageContributors)
+ Support.allocateContributors(KillDeathAssists.healthDamageContributors)
)
healthAssists.remove(0L)
+// if (armor > 0) {
+// val armorAssists = collectMaxArmorAssists(victim, history, armor.toFloat)
+// }
+// if (Support.wasEverAMax(victim, history)) {
+//
+// } else {
+//
+// }
killerOpt.map { killer => healthAssists.remove(killer.CharId) }
healthAssists.values
}
- private def allocateContributors(
- tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any
- )
- (
- history: List[InGameActivity],
- faction: PlanetSideEmpire.Value
- ): mutable.LongMap[ContributionStats] = {
- /** players who have contributed to this death, and how much they have contributed
- * key - character identifier,
- * value - (player, damage, total damage, number of shots) */
- val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
- tallyFunc(history, faction, participants)
- participants
- }
-
-
-
- private def healthDamageContributors(
- history: List[InGameActivity],
- faction: PlanetSideEmpire.Value,
- participants: mutable.LongMap[ContributionStats]
- ): Seq[(Long, Int)] = {
- /** damage as it is measured in order (with heal-countered damage eliminated)
- * key - character identifier,
- * value - current damage contribution */
- var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
- history.tail.foreach {
- case d: DamagingActivity if d.health > 0 =>
- inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
- case _: RepairingActivity => ()
- case h: HealingActivity =>
- inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder)
- case _ => ()
- }
- inOrder
- }
-
- private def collectHealthAssists(
- victim: SourceEntry,
- history: List[InGameActivity],
- topHealth: Float,
- func: (List[InGameActivity], PlanetSideEmpire.Value)=>mutable.LongMap[ContributionStats]
- ): mutable.LongMap[ContributionStatsOutput] = {
- val healthAssists = func(history, victim.Faction)
- .filterNot { case (_, kda) => kda.amount <= 0 }
- .map { case (id, kda) =>
- (id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / topHealth))
- }
- healthAssists.remove(victim.CharId)
- healthAssists
- }
-
- private def contributeWithDamagingActivity(
- activity: DamagingActivity,
- faction: PlanetSideEmpire.Value,
- amount: Int,
- participants: mutable.LongMap[ContributionStats],
- order: Seq[(Long, Int)]
- ): Seq[(Long, Int)] = {
- activity.data.adversarial match {
- case Some(Adversarial(attacker: PlayerSource, _, _))
- if attacker.Faction != faction =>
- val whoId = attacker.CharId
- val wepid = activity.data.interaction.cause.attribution
- val time = activity.time
- val updatedEntry = participants.get(whoId) match {
- case Some(mod) =>
- //previous attacker, just add to entry
- val firstWeapon = mod.weapons.head
- val weapons = if (firstWeapon.weapon_id == wepid) {
- firstWeapon.copy(amount = firstWeapon.amount + amount, shots = firstWeapon.shots + 1, time = time) +: mod.weapons.tail
- } else {
- WeaponStats(wepid, amount, 1, time) +: mod.weapons
- }
- mod.copy(
- amount = mod.amount + amount,
- weapons = weapons,
- total = mod.total + amount,
- shots = mod.shots + 1,
- time = activity.time
- )
- case None =>
- //new attacker, new entry
- ContributionStats(
- attacker,
- Seq(WeaponStats(wepid, amount, 1, time)),
- amount,
- amount,
- 1,
- time
- )
- }
- participants.put(whoId, updatedEntry)
- order.indexWhere({ case (id, _) => id == whoId }) match {
- case 0 =>
- //ongoing attack by same player
- val entry = order.head
- (entry._1, entry._2 + amount) +: order.tail
- case _ =>
- //different player than immediate prior attacker
- (whoId, amount) +: order
- }
- case _ =>
- //damage that does not lead to contribution
- order.headOption match {
- case Some((id, dam)) =>
- if (id == 0L) {
- (0L, dam + amount) +: order.tail //pool
- } else {
- (0L, amount) +: order //new
- }
- case None =>
- order
- }
- }
- }
-
- private def contributeWithRecoveryActivity(
- amount: Int,
- participants: mutable.LongMap[ContributionStats],
- order: Seq[(Long, Int)]
- ): Seq[(Long, Int)] = {
- var amt = amount
- var count = 0
- var newOrder: Seq[(Long, Int)] = Nil
- order.takeWhile { entry =>
- val (id, total) = entry
- if (id > 0 && total > 0) {
- val part = participants(id)
- if (amount > total) {
- //drop this entry
- participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case
- amt = amt - total
- } else {
- //edit around the inclusion of this entry
- val newTotal = total - amt
- val trimmedWeapons = {
- var index = -1
- var weaponSum = 0
- val pweapons = part.weapons
- while (weaponSum < amt) {
- index += 1
- weaponSum = weaponSum + pweapons(index).amount
- }
- pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size)
- }
- newOrder = (id, newTotal) +: newOrder
- participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
- amt = 0
- }
- }
- count += 1
- amt > 0
- }
- newOrder ++ order.drop(count)
- }
-
private def rewardTheseSupporters(
- target: SourceEntry,
- history: List[InGameActivity],
- kill: Kill,
- bep: Long
- ): Unit = {
+ target: SourceEntry,
+ history: List[InGameActivity],
+ kill: Kill,
+ bep: Long
+ ): Unit = {
val time = kill.time
- val normalAssists = ExperienceCalculator.onlyOriginalAssistEntries(
- Support.collectHealingSupportAssists(target, time, history),
- Support.collectRepairingSupportAssists(target, time, history)
- )
-// retainedSupportAssists.get(target.unique) match {
-// case Some(support) =>
-// ExperienceCalculator.onlyOriginalAssistEntriesIterable(normalAssists, support.assists)
-// case None =>
-// normalAssists
-// }
val events = zone.AvatarEvents
- normalAssists.foreach { case ContributionStatsOutput(p, _, ratio) =>
- events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, (ratio * bep).toLong))
- }
+ val trimmedHistory = Support.limitHistoryToThisLife(history)
+ val normalAssists = Support.onlyOriginalAssistEntries(
+ Support.collectHealingSupportAssists(target, time, trimmedHistory),
+ Support.collectRepairingSupportAssists(target, time, trimmedHistory)
+ )
+ (retainedSupportAssists.get(target.unique) match {
+ case Some(support) =>
+ Support.onlyOriginalAssistEntriesIterable(normalAssists, support.assists)
+ case None =>
+ normalAssists
+ })
+ .foreach { case ContributionStatsOutput(p, _, ratio) =>
+ events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, (ratio * bep).toLong))
+ }
Support.collectTerminalSupportAssists(target, history).foreach { case ContributionStatsOutput(p, _, reward) =>
events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, reward.toLong))
}
}
+
+ /* */
+
+ private def sortTheseSupportersForLater(
+ target: SourceEntry,
+ history: Iterable[InGameActivity],
+ kill: Kill
+ ): Unit = {
+ val time = kill.time
+ val trimmedHistory = Support.limitHistoryToThisLife(history.toList)
+ val assists = Support.collectRepairingSupportAssists(target, time, trimmedHistory).values
+ retainAssistsAndScheduleBlanking(target.unique, assists)
+ }
+
+ private def sortThisContributionForLater(
+ target: SourceWithHealthEntry,
+ lastDamage: Option[DamageResult],
+ history: Iterable[InGameActivity]
+ ): Unit = {
+ val shortHistory = Support.limitHistoryToThisLife(history.toList)
+ val assists = KillDeathAssists.determineKiller(lastDamage, shortHistory) match {
+ case Some((damage, killer: PlayerSource)) =>
+ ContributionStatsOutput(killer, Seq(damage.interaction.cause.attribution), 1f) +:
+ collectRetainableAssistsForEntity(target, shortHistory, Some(killer)).toSeq
+ case _ =>
+ collectRetainableAssistsForEntity(target, shortHistory, None)
+ }
+ retainAssistsAndScheduleBlanking(target.unique, assists)
+ }
+
+ private def collectRetainableAssistsForEntity(
+ entity: SourceWithHealthEntry,
+ history: List[InGameActivity],
+ killerOpt: Option[PlayerSource]
+ ): Iterable[ContributionStatsOutput] = {
+ val assists = KillDeathAssists.collectAssistsForEntity(entity, history, killerOpt)
+ Support.onlyOriginalAssistEntriesIterable(
+ assists,
+ recoverRetainedKillAssists(assists, history)
+ )
+ }
+
+ private def recoverRetainedKillAssists(
+ assists: Iterable[ContributionStatsOutput],
+ history: Iterable[InGameActivity]
+ ): Iterable[ContributionStatsOutput] = {
+ history.collect { case ex: DamageFromExplodingEntity =>
+ ex.data
+ .adversarial
+ .flatMap {
+ case Adversarial(attacker: PlayerSource, _, _) =>
+ Some((
+ assists.find { case ContributionStatsOutput(p, _, _) => p == attacker },
+ retainedKillAssists.get(ex.data.targetAfter.unique)
+ ))
+ }
+ .map {
+ case (Some(_), Some(retained)) => retained.assists
+ }
+ }.flatten.flatten
+ }
+
+ private[this] def retainAssistsAndScheduleBlanking(
+ target: SourceUniqueness,
+ assists: Iterable[ContributionStatsOutput]
+ ): Boolean = {
+ val retime = retainedKillAssists.isEmpty && inheritedAssistsDecayTimer.isCancelled
+ retainedKillAssists.put(target, InheritedAssistEntry(assists))
+ if (retime) {
+ inheritedAssistsDecayTimer = context.scheduleOnce(3.minutes, context.self, AssistDecay)
+ }
+ retime
+ }
+
+ private[this] def performAssistDecay(): Unit = {
+ val curr = System.currentTimeMillis()
+ val dur = 5.minutes.toMillis
+ retainedKillAssists.filterInPlace { case (_, entry) => curr - entry.time < dur }
+ retainedSupportAssists.filterInPlace { case (_, entry) => curr - entry.time < dur }
+ if (retainedKillAssists.nonEmpty || retainedSupportAssists.nonEmpty) {
+ inheritedAssistsDecayTimer = context.scheduleOnce(3.minutes, context.self, AssistDecay)
+ } else {
+ inheritedAssistsDecayTimer = Default.Cancellable
+ }
+ }
}
diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala b/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala
index 4560b7a80..3799af0c7 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala
@@ -1,15 +1,40 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
-import net.psforever.objects.sourcing.PlayerSource
-import net.psforever.objects.vital.InGameActivity
+import net.psforever.objects.definition.{ExoSuitDefinition, WithShields}
+import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceWithHealthEntry, SourceWithShieldsEntry}
+import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
+import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairFromExoSuitChange, RepairingActivity, ShieldCharge, SpawningActivity}
+import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
+
+import scala.collection.mutable
+import scala.concurrent.duration._
object KillDeathAssists {
+ private [exp] def determineKiller(
+ lastDamageActivity: Option[DamageResult],
+ history: List[InGameActivity]
+ ): Option[(DamageResult, SourceEntry)] = {
+ val now = System.currentTimeMillis()
+ val compareTimeMillis = 10.seconds.toMillis
+ lastDamageActivity
+ .collect { case dam if now - dam.interaction.hitTime < compareTimeMillis => dam }
+ .flatMap { dam => Some(dam, dam.adversarial) }
+ .orElse {
+ history.collect { case damage: DamagingActivity
+ if now - damage.time < compareTimeMillis && damage.data.adversarial.nonEmpty =>
+ damage.data
+ }
+ .flatMap { dam => Some(dam, dam.adversarial) }.lastOption
+ }
+ .collect { case (dam, Some(adv)) => (dam, adv.attacker) }
+ }
+
private[exp] def calculateExperience(
- killer: PlayerSource,
- victim: PlayerSource,
- history: Iterable[InGameActivity]
- ): Long = {
+ killer: PlayerSource,
+ victim: PlayerSource,
+ history: Iterable[InGameActivity]
+ ): Long = {
//base value (the kill experience before modifiers)
val base = ExperienceCalculator.calculateExperience(victim, history)
if (base > 1) {
@@ -37,4 +62,338 @@ object KillDeathAssists {
base
}
}
+
+ private[exp] def collectAssistsForEntity(
+ target: SourceWithHealthEntry,
+ history: List[InGameActivity],
+ killerOpt: Option[PlayerSource]
+ ): Iterable[ContributionStatsOutput] = {
+ val (_, maxShields) = maximumEntityHealthAndShields(history)
+ val healthAssists = Support.collectHealthAssists(
+ target,
+ history,
+ Support.allocateContributors(entityHealthDamageContributors)
+ )
+ if (maxShields > 0) {
+ val shieldAssists = collectShieldAssists(target.asInstanceOf[SourceWithShieldsEntry], history)
+ killerOpt.collect {
+ case killer =>
+ healthAssists.remove(killer.CharId)
+ shieldAssists.remove(killer.CharId)
+ }
+ Support.onlyOriginalAssistEntries(healthAssists, shieldAssists)
+ } else {
+ killerOpt.collect {
+ case killer =>
+ healthAssists.remove(killer.CharId)
+ }
+ healthAssists.values
+ }
+ }
+
+ private[exp] def collectMaxArmorAssists(
+ victim: PlayerSource,
+ history: List[InGameActivity],
+ fullArmor: Float
+ ): mutable.LongMap[ContributionStatsOutput] = {
+ val initialArmorAssists = Support.allocateContributors(maxArmorDamageContributors)(history, victim.Faction)
+ val allArmorDamage = initialArmorAssists.values.foldLeft(0f)(_ + _.total)
+ val flatComparativeRate = if (allArmorDamage > fullArmor * 1.35f) {
+ 0.4f //this max has been damaged and repaired a lot
+ } else {
+ 0.65f
+ }
+ val armorAssists = initialArmorAssists.map { case (id, kda) =>
+ val averageRateOfAllWeapons = kda.weapons.map { stat => stat.contributions / stat.shots }.sum / kda.weapons.size
+ val finalRate = math.max(flatComparativeRate, (flatComparativeRate + averageRateOfAllWeapons) * 0.5f)
+ (id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, finalRate))
+ }
+ armorAssists.remove(victim.CharId)
+ armorAssists
+ }
+
+ private def maxArmorDamageContributors(
+ history: List[InGameActivity],
+ faction: PlanetSideEmpire.Value,
+ participants: mutable.LongMap[ContributionStats]
+ ): Seq[(Long, Int)] = {
+ var isMax = history.head match {
+ case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
+ case _ => false
+ }
+ /** damage as it is measured in order (with repair-countered damage eliminated)
+ * key - character identifier,
+ * value - current damage contribution */
+ var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
+ history.tail.collect {
+ case d: DamagingActivity if isMax || d.amount - d.health == d.amount =>
+ inOrder = contributeWithDamagingActivity(d, faction, d.amount, participants, inOrder)
+
+ case RepairFromExoSuitChange(suit, _) =>
+ isMax = suit == ExoSuitType.MAX
+
+ case r: RepairingActivity =>
+ inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
+ }
+ inOrder
+ }
+
+ private[exp] def collectShieldAssists(
+ victim: SourceWithShieldsEntry,
+ history: List[InGameActivity],
+ ): mutable.LongMap[ContributionStatsOutput] = {
+ val initialShieldAssists = Support.allocateContributors(entityShieldDamageContributors)(history, victim.Faction)
+ .filterNot { case (_, kda) => kda.amount <= 0 }
+ val fullFloatShield = initialShieldAssists.values.foldLeft(0)(_ + _.total).toFloat
+ val shieldAssists = initialShieldAssists.map { case (id, kda) =>
+ (id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, math.min(kda.total / fullFloatShield, 0.4f)))
+ }
+ shieldAssists.remove(victim.CharId)
+ shieldAssists
+ }
+
+ private def entityShieldDamageContributors(
+ history: List[InGameActivity],
+ faction: PlanetSideEmpire.Value,
+ participants: mutable.LongMap[ContributionStats]
+ ): Seq[(Long, Int)] = {
+ /** damage as it is measured in order (with heal-countered damage eliminated)
+ * key - character identifier,
+ * value - current damage contribution */
+ var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
+ history.tail.foreach {
+ case d: DamagingActivity if d.amount - d.health > 0 =>
+ inOrder = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, inOrder)
+ case v: ShieldCharge if v.amount > 0 =>
+ inOrder = contributeWithRecoveryActivity(v.amount, participants, inOrder)
+ case _ => ()
+ }
+ inOrder
+ }
+
+ /**
+ * Determine the maximum amount of health and armor that a player character had
+ * before their event and time of death.
+ *
+ * For health value, the number is determined by starting with their spawn health and tracking heals and damage.
+ * For armor value, the number is determined in the same way at first,
+ * by starting with their spawn armor and tracking repairs and damage.
+ * If and only after being bulk set by an exosuit change event once, however,
+ * the only things that matter are exosuit change events.
+ * @param history na
+ * @return a pair of the aforementioned integers, the maximum health value and the maximum armor value
+ */
+ private[exp] def maximumPlayerHealthAndArmor(history: List[InGameActivity]): (Int, Int) = {
+ var (health, armor, faction) = (0, 0, PlanetSideEmpire.NEUTRAL)
+ history.collect {
+ case SpawningActivity(target: PlayerSource, _, _) =>
+ health = math.max(target.Definition.MaxHealth, health)
+ armor = math.max(ExoSuitDefinition.Select(target.ExoSuit, target.Faction).MaxArmor, armor)
+ faction = target.Faction
+ case d: DamagingActivity =>
+ val target = d.data.targetBefore.asInstanceOf[PlayerSource]
+ health = math.max(target.Definition.MaxHealth, health)
+ armor = math.max(ExoSuitDefinition.Select(target.ExoSuit, target.Faction).MaxArmor, armor)
+ }
+ (health, armor)
+ }
+
+// private def initialPlayerHealthAndArmor(history: List[InGameActivity]): (Int, Int, PlanetSideEmpire.Value) = {
+// history.collectFirst {
+// case SpawningActivity(target: PlayerSource, _, _) =>
+// (target.health, target.armor, target.Faction)
+// case d: DamagingActivity =>
+// val target = d.data.targetBefore.asInstanceOf[PlayerSource]
+// (target.health, target.armor, target.Faction)
+// }.getOrElse((0, 0, PlanetSideEmpire.NEUTRAL))
+// }
+
+ private def maximumEntityHealthAndShields(history: List[InGameActivity]): (Int, Int) = {
+ var (health, shields) = (0, 0)
+ history.collect {
+ case SpawningActivity(target: SourceWithHealthEntry with SourceWithShieldsEntry, _, _) =>
+ health = math.max(target.Definition.MaxHealth, health)
+ shields = math.max(target.Definition.asInstanceOf[WithShields].MaxShields, shields)
+ case SpawningActivity(target: SourceWithHealthEntry, _, _) =>
+ health = math.max(target.Definition.MaxHealth, health)
+ case d: DamagingActivity =>
+ d.data.targetBefore match {
+ case target: SourceWithHealthEntry with SourceWithShieldsEntry =>
+ health = math.max(target.Definition.MaxHealth, health)
+ shields = math.max(target.Definition.asInstanceOf[WithShields].MaxShields, shields)
+ case target: SourceWithHealthEntry =>
+ health = math.max(target.health, health)
+ case _ => ()
+ }
+ }
+ (health, shields)
+ }
+
+// private def initialEntityHealthAndShields(history: List[InGameActivity]): (Int, Int, PlanetSideEmpire.Value) = {
+// history.collectFirst {
+// case SpawningActivity(target: SourceWithHealthEntry with SourceWithShieldsEntry, _, _) =>
+// (target.health, target.shields, target.Faction)
+// case SpawningActivity(target: SourceWithHealthEntry, _, _) =>
+// (target.health, 0, target.Faction)
+// case d: DamagingActivity =>
+// d.data.targetBefore match {
+// case target: SourceWithHealthEntry with SourceWithShieldsEntry =>
+// (target.health, target.shields, target.Faction)
+// case target: SourceWithHealthEntry =>
+// (target.health, 0, target.Faction)
+// case _ =>
+// (0, 0, PlanetSideEmpire.NEUTRAL)
+// }
+// }.getOrElse((0, 0, PlanetSideEmpire.NEUTRAL))
+// }
+
+ private[exp] def healthDamageContributors(
+ history: List[InGameActivity],
+ faction: PlanetSideEmpire.Value,
+ participants: mutable.LongMap[ContributionStats]
+ ): Seq[(Long, Int)] = {
+ /** damage as it is measured in order (with heal-countered damage eliminated)
+ * key - character identifier,
+ * value - current damage contribution */
+ var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
+ history.tail.foreach {
+ case d: DamagingActivity if d.health > 0 =>
+ inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
+ case _: RepairingActivity => ()
+ case h: HealingActivity =>
+ inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder)
+ case _ => ()
+ }
+ inOrder
+ }
+
+ private[exp] def contributeWithDamagingActivity(
+ activity: DamagingActivity,
+ faction: PlanetSideEmpire.Value,
+ amount: Int,
+ participants: mutable.LongMap[ContributionStats],
+ order: Seq[(Long, Int)]
+ ): Seq[(Long, Int)] = {
+ activity.data.adversarial match {
+ case Some(Adversarial(attacker: PlayerSource, _, _))
+ if attacker.Faction != faction =>
+ val whoId = attacker.CharId
+ val wepid = activity.data.interaction.cause.attribution
+ val time = activity.time
+ val percentage = amount / attacker.Definition.MaxHealth.toFloat
+ val updatedEntry = participants.get(whoId) match {
+ case Some(mod) =>
+ //previous attacker, just add to entry
+ val firstWeapon = mod.weapons.head
+ val weapons = if (firstWeapon.weapon_id == wepid) {
+ firstWeapon.copy(
+ amount = firstWeapon.amount + amount,
+ shots = firstWeapon.shots + 1,
+ time = time,
+ contributions = firstWeapon.contributions + percentage
+ ) +: mod.weapons.tail
+ } else {
+ WeaponStats(wepid, amount, 1, time, percentage) +: mod.weapons
+ }
+ mod.copy(
+ amount = mod.amount + amount,
+ weapons = weapons,
+ total = mod.total + amount,
+ shots = mod.shots + 1,
+ time = activity.time
+ )
+ case None =>
+ //new attacker, new entry
+ ContributionStats(
+ attacker,
+ Seq(WeaponStats(wepid, amount, 1, time, percentage)),
+ amount,
+ amount,
+ 1,
+ time
+ )
+ }
+ participants.put(whoId, updatedEntry)
+ order.indexWhere({ case (id, _) => id == whoId }) match {
+ case 0 =>
+ //ongoing attack by same player
+ val entry = order.head
+ (entry._1, entry._2 + amount) +: order.tail
+ case _ =>
+ //different player than immediate prior attacker
+ (whoId, amount) +: order
+ }
+ case _ =>
+ //damage that does not lead to contribution
+ order.headOption match {
+ case Some((id, dam)) =>
+ if (id == 0L) {
+ (0L, dam + amount) +: order.tail //pool
+ } else {
+ (0L, amount) +: order //new
+ }
+ case None =>
+ order
+ }
+ }
+ }
+
+ private def entityHealthDamageContributors(
+ history: List[InGameActivity],
+ faction: PlanetSideEmpire.Value,
+ participants: mutable.LongMap[ContributionStats]
+ ): Seq[(Long, Int)] = {
+ /** damage as it is measured in order (with heal-countered damage eliminated)
+ * key - character identifier,
+ * value - current damage contribution */
+ var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
+ history.collect {
+ case d: DamagingActivity if d.health > 0 =>
+ inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
+
+ case r: RepairingActivity if r.amount > 0 =>
+ inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
+ }
+ inOrder
+ }
+
+ private[exp] def contributeWithRecoveryActivity(
+ amount: Int,
+ participants: mutable.LongMap[ContributionStats],
+ order: Seq[(Long, Int)]
+ ): Seq[(Long, Int)] = {
+ var amt = amount
+ var count = 0
+ var newOrder: Seq[(Long, Int)] = Nil
+ order.takeWhile { entry =>
+ val (id, total) = entry
+ if (id > 0 && total > 0) {
+ val part = participants(id)
+ if (amount > total) {
+ //drop this entry
+ participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case
+ amt = amt - total
+ } else {
+ //edit around the inclusion of this entry
+ val newTotal = total - amt
+ val trimmedWeapons = {
+ var index = -1
+ var weaponSum = 0
+ val pweapons = part.weapons
+ while (weaponSum < amt) {
+ index += 1
+ weaponSum = weaponSum + pweapons(index).amount
+ }
+ pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size)
+ }
+ newOrder = (id, newTotal) +: newOrder
+ participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
+ amt = 0
+ }
+ }
+ count += 1
+ amt > 0
+ }
+ newOrder ++ order.drop(count)
+ }
}
diff --git a/src/main/scala/net/psforever/objects/zones/exp/Stats.scala b/src/main/scala/net/psforever/objects/zones/exp/Stats.scala
index 9af4ba3fd..103b5110e 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/Stats.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/Stats.scala
@@ -7,7 +7,8 @@ private case class WeaponStats(
weapon_id: Int,
amount: Int,
shots: Int,
- time: Long
+ time: Long,
+ contributions: Float
)
private case class ContributionStats(
diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
index b648e3671..d6cfe4da9 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
@@ -8,9 +8,9 @@ import org.joda.time.{Instant, LocalDateTime => JodaLocalDateTime}
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry}
-import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFromEquipment, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity}
+import net.psforever.objects.vital.{DamagingActivity, HealFromEquipment, InGameActivity, ReconstructionActivity, RepairFromEquipment, RepairFromExoSuitChange, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity}
import net.psforever.objects.zones.Zone
-import net.psforever.types.TransactionType
+import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
import net.psforever.zones.Zones
import scala.collection.mutable
@@ -18,14 +18,82 @@ import scala.collection.mutable
object Support {
private type SupportActivity = InGameActivity with SupportActivityCausedByAnother
+ private[exp] def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
+ val spawnIndex = history.lastIndexWhere {
+ case _: SpawningActivity => true
+ case _: RevivingActivity => true
+ case _ => false
+ }
+ val endIndex = history.lastIndexWhere {
+ case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
+ case _ => false
+ }
+ if (spawnIndex == -1 || endIndex == -1) {
+ Nil //throw VitalsHistoryException(history.head, "vitals history does not contain expected conditions")
+ // } else
+ // if (spawnIndex == -1) {
+ // Nil //throw VitalsHistoryException(history.head, "vitals history does not contain initial spawn conditions")
+ // } else if (endIndex == -1) {
+ // Nil //throw VitalsHistoryException(history.last, "vitals history does not contain end of life conditions")
+ } else {
+ history.slice(spawnIndex, endIndex)
+ }
+ }
+
+ private[exp] def onlyOriginalAssistEntries(
+ first: mutable.LongMap[ContributionStatsOutput],
+ second: mutable.LongMap[ContributionStatsOutput]
+ ): Iterable[ContributionStatsOutput] = {
+ onlyOriginalAssistEntriesIterable(first.values, second.values)
+ }
+
+ private[exp] def onlyOriginalAssistEntriesIterable(
+ first: Iterable[ContributionStatsOutput],
+ second: Iterable[ContributionStatsOutput]
+ ): Iterable[ContributionStatsOutput] = {
+ if (second.isEmpty) {
+ first
+ } else if (first.isEmpty) {
+ second
+ } else {
+ //overlap discriminated by percentage
+ val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
+ for {
+ h @ ContributionStatsOutput(hid, hwep, hkda) <- first
+ a @ ContributionStatsOutput(aid, awep, akda) <- second
+ (id, out) = if (hkda < akda)
+ (aid.CharId, a.copy(implements = (a.implements ++ hwep).distinct))
+ else
+ (hid.CharId, h.copy(implements = (h.implements ++ awep).distinct))
+ if hid == aid && shared.put(id, out).isEmpty
+ } yield ()
+ val sharedKeys = shared.keys
+ (first ++ second).filterNot { case ContributionStatsOutput(id, _, _) => sharedKeys.exists(_ == id.CharId) } ++ shared.values
+ }
+ }
+
+ private[exp] def collectHealthAssists(
+ victim: SourceEntry,
+ history: List[InGameActivity],
+ func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
+ ): mutable.LongMap[ContributionStatsOutput] = {
+ val healthAssists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
+ val qualifiedHealing = healthAssists.values.foldLeft(0)(_ + _.total)
+ val healthAssistsOutput = healthAssists.map { case (id, kda) =>
+ (id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / qualifiedHealing))
+ }
+ healthAssistsOutput.remove(victim.CharId)
+ healthAssistsOutput
+ }
+
private[exp] def collectHealingSupportAssists(
target: SourceEntry,
time: JodaLocalDateTime,
- history: Iterable[InGameActivity]
+ history: List[InGameActivity]
): mutable.LongMap[ContributionStatsOutput] = {
//normal heals
val healInfo = collectSupportContributions(
- target, time, ExperienceCalculator.limitHistoryToThisLife(history.toList).collect { case heals: HealFromEquipment
+ target, time, history.collect { case heals: HealFromEquipment
if heals.amount > 0 && heals.user.unique != target.unique => (heals, heals.equipment_def.ObjectId)
}, mapContributionPointsByPercentage
)
@@ -48,10 +116,10 @@ object Support {
private[exp] def collectRepairingSupportAssists(
target: SourceEntry,
time: JodaLocalDateTime,
- history: Iterable[InGameActivity]
+ history: List[InGameActivity]
): mutable.LongMap[ContributionStatsOutput] = {
collectSupportContributions(
- target, time, ExperienceCalculator.limitHistoryToThisLife(history.toList).collect { case repairs: RepairFromEquipment
+ target, time, history.collect { case repairs: RepairFromEquipment
if repairs.amount > 0 && repairs.user.unique != target.unique => (repairs, repairs.equipment_def.ObjectId)
}, mapContributionPointsByPercentage
)
@@ -148,11 +216,11 @@ object Support {
.sortBy(_.time)(Ordering.Long.reverse)
}
- private def allocateSupportAssists(
- longTerm: Seq[SupportActivity],
- shortTerm: Seq[SupportActivity],
- defaultTool: Int
- ): mutable.LongMap[ContributionStats] = {
+ private def combineTimeApplicableActivitiesForContribution(
+ longTerm: Seq[SupportActivity],
+ shortTerm: Seq[SupportActivity],
+ defaultTool: Int
+ ): mutable.LongMap[ContributionStats] = {
val contributions: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
(longTerm ++ shortTerm).foreach { h =>
val amount = h.amount
@@ -167,7 +235,8 @@ object Support {
time = math.max(entry.time, h.time)
))
case None =>
- contributions.put(charId, ContributionStats(user, Seq(WeaponStats(defaultTool, amount, 1, h.time)), amount, amount, 1, h.time))
+ //the contribution percentage allocated will always be 1.0f and should be overwritten later
+ contributions.put(charId, ContributionStats(user, Seq(WeaponStats(defaultTool, amount, 1, h.time, 1f)), amount, amount, 1, h.time))
}
}
contributions
@@ -183,38 +252,54 @@ object Support {
//TODO this is not correct, but it will do for now
if (tools.isEmpty) {
mutable.LongMap[ContributionStatsOutput]()
- } else if (tools.distinct.size == 1) {
- collectSupportAssists(target, time, activity, tools.head, contributionPointsMap)
} else {
- var output = Iterable[ContributionStatsOutput]()
- tools.groupBy(identity).keys.foreach { implement =>
- output = ExperienceCalculator.onlyOriginalAssistEntriesIterable(
- output,
- collectSupportAssists(target, time, activity, implement, contributionPointsMap).values
- )
+ val distinctTools = tools.distinct
+ if (distinctTools.size == 1) {
+ compileTimedSupportContributions(target, time, activity, distinctTools.head, contributionPointsMap)
+ } else {
+ var output = Iterable[ContributionStatsOutput]()
+ distinctTools.foreach { implement =>
+ output = onlyOriginalAssistEntriesIterable(
+ output,
+ compileTimedSupportContributions(target, time, activity, implement, contributionPointsMap).values
+ )
+ }
+ mutable.LongMap[ContributionStatsOutput]().addAll(output.map { entry => (entry.player.CharId, entry) })
}
- mutable.LongMap[ContributionStatsOutput]().addAll(output.map { entry => (entry.player.CharId, entry) })
}
}
- private def collectSupportAssists(
- target: SourceEntry,
- killLastTime: JodaLocalDateTime,
- activity: Seq[InGameActivity],
- defaultImplement: Int,
- contributionPointsMap: (Float,Iterable[SupportActivity])=>(Long,ContributionStats)=>(Long,ContributionStatsOutput)
- ): mutable.LongMap[ContributionStatsOutput] = {
- val activityByAnother = activity.collect {
- case activity: SupportActivity => activity
- }
- val longTermActivity = findTimeApplicableActivities(activityByAnother, killLastTime, timeOffset = 600)
- val shortTermActivity = findTimeApplicableActivities(activityByAnother, killLastTime, timeOffset = 300)
- val contributions = allocateSupportAssists(longTermActivity, shortTermActivity, defaultImplement)
+ private def compileTimedSupportContributions(
+ target: SourceEntry,
+ timeLimit: JodaLocalDateTime,
+ activity: Seq[InGameActivity],
+ defaultImplement: Int,
+ contributionPointsMap: (Float,Iterable[SupportActivity])=>(Long,ContributionStats)=>(Long,ContributionStatsOutput)
+ ): mutable.LongMap[ContributionStatsOutput] = {
+ val activityByAnother = activity.collect { case theActivity: SupportActivity => theActivity }
+ val longTermActivity = findTimeApplicableActivities(activityByAnother, timeLimit, timeOffset = 600)
+ val shortTermActivity = findTimeApplicableActivities(activityByAnother, timeLimit, timeOffset = 300)
+ val contributions = combineTimeApplicableActivitiesForContribution(longTermActivity, shortTermActivity, defaultImplement)
contributions.remove(target.CharId)
val mapFunc = contributionPointsMap(contributions.values.foldLeft(0)(_ + _.total).toFloat, shortTermActivity)
contributions.map { case (a, b) => mapFunc(a, b) }
}
+ private[exp] def allocateContributors(
+ tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any
+ )
+ (
+ history: List[InGameActivity],
+ faction: PlanetSideEmpire.Value
+ ): mutable.LongMap[ContributionStats] = {
+ /** players who have contributed to this death, and how much they have contributed
+ * key - character identifier,
+ * value - (player, damage, total damage, number of shots) */
+ val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
+ tallyFunc(history, faction, participants)
+ participants
+ }
+
private def mapContributionPointsByPercentage(
total: Float,
compareList: Iterable[SupportActivity]
@@ -257,4 +342,13 @@ object Support {
): (Long, ContributionStatsOutput) = {
(charId, ContributionStatsOutput(contribution.player, contribution.weapons.map { _.weapon_id }, compareList.size.toFloat))
}
+
+ private[exp] def wasEverAMax(player: PlayerSource, history: Iterable[InGameActivity]): Boolean = {
+ player.ExoSuit == ExoSuitType.MAX || history.exists {
+ case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
+ case ReconstructionActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
+ case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
+ case _ => false
+ }
+ }
}