diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
index 346dbc309..e06299a48 100644
--- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala
+++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
@@ -4,10 +4,14 @@ package net.psforever.actors.session
import akka.actor.Cancellable
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
+
import java.util.concurrent.atomic.AtomicInteger
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill}
+import net.psforever.objects.serverobject.affinity.FactionAffinity
+import net.psforever.objects.vital.InGameHistory
import org.joda.time.{LocalDateTime, Seconds}
+
import scala.collection.mutable
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
import scala.util.{Failure, Success}
@@ -36,7 +40,7 @@ import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, VehicleLoadout}
import net.psforever.objects.locker.LockerContainer
import net.psforever.objects.sourcing.{PlayerSource,SourceWithHealthEntry}
import net.psforever.objects.vital.projectile.ProjectileReason
-import net.psforever.objects.vital.{DamagingActivity, HealFromImplant, HealingActivity, SpawningActivity, Vitality}
+import net.psforever.objects.vital.{DamagingActivity, HealFromImplant, HealingActivity, SpawningActivity}
import net.psforever.packet.game.objectcreate.{BasicCharacterData, ObjectClass, RibbonBars}
import net.psforever.packet.game.{Friend => GameFriend, _}
import net.psforever.persistence
@@ -2949,10 +2953,10 @@ class AvatarActor(
case pr: ProjectileReason => pr.projectile.mounted_in.map { a => zone.GUID(a._1) }
case _ => None
}).collect {
- case Some(obj: Vitality) =>
- zone.actor ! ZoneActor.RewardOurSupporters(playerSource, obj.History, kill, exp)
+ case Some(mount: PlanetSideGameObject with FactionAffinity with InGameHistory) =>
+ player.ContributionFrom(mount)
}
- zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.History, kill, exp)
+ zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.HistoryAndContributions(), kill, exp)
case _: Assist =>
()
case _: Death =>
diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
index 702dc6e02..bb913edbc 100644
--- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
@@ -2614,10 +2614,11 @@ class ZoningOperations(
LoadZoneAsPlayer(newPlayer, zoneId)
} else {
avatarActor ! AvatarActor.DeactivateActiveImplants()
+ val betterSpawnPoint = physSpawnPoint.collect { case o: PlanetSideGameObject with FactionAffinity with InGameHistory => o }
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case Some(vehicle: Vehicle) => // driver or passenger in vehicle using a warp gate, or a droppod
- InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, toSpawnPoint)
- InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
+ InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, betterSpawnPoint)
+ InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneInVehicle(vehicle, pos, ori, zoneId)
case _ if player.HasGUID => // player is deconstructing self or instant action
@@ -2629,13 +2630,13 @@ class ZoningOperations(
)
player.Position = pos
player.Orientation = ori
- InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
+ InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneAsPlayer(player, zoneId)
case _ => //player is logging in
player.Position = pos
player.Orientation = ori
- InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
+ InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneAsPlayer(player, zoneId)
}
}
diff --git a/src/main/scala/net/psforever/login/WorldSession.scala b/src/main/scala/net/psforever/login/WorldSession.scala
index 4a03211b4..f49d54d65 100644
--- a/src/main/scala/net/psforever/login/WorldSession.scala
+++ b/src/main/scala/net/psforever/login/WorldSession.scala
@@ -942,8 +942,9 @@ object WorldSession {
result: Boolean
): Unit = {
if (result) {
- player.Zone.GUID(guid).collect {
- case term: Terminal => player.LogActivity(TerminalUsedActivity(AmenitySource(term), transaction))
+ player.Zone.GUID(guid).collect { case term: Terminal =>
+ player.LogActivity(TerminalUsedActivity(AmenitySource(term), transaction))
+ player.ContributionFrom(term)
}
}
player.Zone.AvatarEvents ! AvatarServiceMessage(
diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala
index 343092dad..18efd8417 100644
--- a/src/main/scala/net/psforever/objects/Player.scala
+++ b/src/main/scala/net/psforever/objects/Player.scala
@@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.aura.AuraContainer
import net.psforever.objects.serverobject.environment.InteractWithEnvironment
import net.psforever.objects.serverobject.mount.MountableEntity
import net.psforever.objects.vital.resistance.ResistanceProfile
-import net.psforever.objects.vital.Vitality
+import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFromEquipment, Vitality}
import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.resolution.DamageResistanceModel
@@ -422,12 +422,13 @@ class Player(var avatar: Avatar)
def UsingSpecial_=(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
usingSpecial(state)
+ //noinspection ScalaUnusedSymbol
private def DefaultUsingSpecial(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
SpecialExoSuitDefinition.Mode.Normal
private def UsingAnchorsOrOverdrive(
- state: SpecialExoSuitDefinition.Mode.Value
- ): SpecialExoSuitDefinition.Mode.Value = {
+ state: SpecialExoSuitDefinition.Mode.Value
+ ): SpecialExoSuitDefinition.Mode.Value = {
import SpecialExoSuitDefinition.Mode._
val curr = UsingSpecial
val next = if (curr == Normal) {
@@ -532,11 +533,14 @@ class Player(var avatar: Avatar)
def Carrying: Option[SpecialCarry] = carrying
+ //noinspection ScalaUnusedSymbol
def Carrying_=(item: SpecialCarry): Option[SpecialCarry] = {
- Carrying
+ Carrying_=(Some(item))
}
+ //noinspection ScalaUnusedSymbol
def Carrying_=(item: Option[SpecialCarry]): Option[SpecialCarry] = {
+ carrying = item
Carrying
}
@@ -555,6 +559,14 @@ class Player(var avatar: Avatar)
def canEqual(other: Any): Boolean = other.isInstanceOf[Player]
+ override def GetContributionDuringPeriod(ending: Long, duration: Long): List[InGameActivity] = {
+ val start = ending - duration
+ History.collect {
+ case heal: HealFromEquipment if heal.time <= ending && heal.time > start => heal
+ case repair: RepairFromEquipment if repair.time <= ending && repair.time > start => repair
+ }
+ }
+
override def equals(other: Any): Boolean =
other match {
case that: Player =>
@@ -620,6 +632,7 @@ object Player {
}
}
+ //noinspection ScalaUnusedSymbol
def neverRestrict(player: Player, slot: Int): Boolean = {
false
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala
index b2af1fe73..3ab229af5 100644
--- a/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala
@@ -17,7 +17,7 @@ import net.psforever.objects.serverobject.damage.DamageableAmenity
import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior}
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity}
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
-import net.psforever.objects.vital.{HealFromTerm, RepairFromTerm, Vitality}
+import net.psforever.objects.vital.{HealFromTerminal, RepairFromTerminal, Vitality}
import net.psforever.objects.zones.ZoneAware
import net.psforever.packet.game.InventoryStateMessage
import net.psforever.services.Service
@@ -291,7 +291,7 @@ object ProximityTerminalControl {
healAmount
}
target.Health = health + finalHealthAmount
- target.LogActivity(HealFromTerm(AmenitySource(terminal), finalHealthAmount))
+ target.LogActivity(HealFromTerminal(AmenitySource(terminal), finalHealthAmount))
updateFunc(target)
target.Health == maxHealth
} else {
@@ -338,7 +338,7 @@ object ProximityTerminalControl {
repairAmount
}
target.Armor = armor + finalRepairAmount
- target.LogActivity(RepairFromTerm(AmenitySource(terminal), finalRepairAmount))
+ target.LogActivity(RepairFromTerminal(AmenitySource(terminal), finalRepairAmount))
val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
index b31f0716d..d4836b221 100644
--- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
+++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
@@ -4,13 +4,15 @@ package net.psforever.objects.vital
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity
-import net.psforever.objects.sourcing.{AmenitySource, ObjectSource, PlayerSource, SourceEntry, SourceWithHealthEntry, VehicleSource}
+import net.psforever.objects.sourcing.{AmenitySource, ObjectSource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource}
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
+import scala.collection.mutable
+
/* root */
/**
@@ -48,6 +50,11 @@ final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
extends GeneralActivity
+final case class Contribution(unique: SourceEntry, entries: List[InGameActivity])
+ extends GeneralActivity {
+ override val time: Long = entries.maxBy(_.time).time
+}
+
/* vitals history */
/**
@@ -77,9 +84,8 @@ trait DamagingActivity extends VitalsActivity {
def health: Int = {
(data.targetBefore, data.targetAfter) match {
- case (pb: PlayerSource, pa: PlayerSource) if pb.ExoSuit == ExoSuitType.MAX => pb.total - pa.total
- case (pb: SourceWithHealthEntry, pa: SourceWithHealthEntry) => pb.health - pa.health
- case _ => 0
+ case (pb: SourceWithHealthEntry, pa: SourceWithHealthEntry) => pb.health - pa.health
+ case _ => 0
}
}
}
@@ -90,7 +96,7 @@ final case class HealFromKit(kit_def: KitDefinition, amount: Int)
final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends HealingActivity with SupportActivityCausedByAnother
-final case class HealFromTerm(term: AmenitySource, amount: Int)
+final case class HealFromTerminal(term: AmenitySource, amount: Int)
extends HealingActivity
final case class HealFromImplant(implant: ImplantType, amount: Int)
@@ -105,7 +111,7 @@ final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends RepairingActivity with SupportActivityCausedByAnother
-final case class RepairFromTerm(term: AmenitySource, amount: Int) extends RepairingActivity
+final case class RepairFromTerminal(term: AmenitySource, amount: Int) extends RepairingActivity
final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, vehicle: VehicleSource, amount: Int)
extends RepairingActivity
@@ -221,6 +227,66 @@ trait InGameHistory {
}
}
+ private val contributionInheritance: mutable.HashMap[SourceUniqueness, Seq[Contribution]] =
+ mutable.HashMap[SourceUniqueness, Seq[Contribution]]()
+
+ def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Boolean = {
+ if (target ne this) {
+ val events = target.GetContribution()
+ val nonEmpty = events.nonEmpty
+ if (nonEmpty) {
+ val source = SourceEntry(target)
+ contributionInheritance.get(source.unique) match {
+ case Some(previousContributions) =>
+ val uniqueEvents = for {
+ curr <- events
+ if !previousContributions.filter(_ == curr).exists(_.time == curr.time)
+ } yield curr
+ contributionInheritance.put(source.unique, previousContributions :+ Contribution(source, uniqueEvents))
+ case None =>
+ contributionInheritance.put(source.unique, Seq(Contribution(source, events)))
+ }
+ }
+ nonEmpty
+ } else {
+ false
+ }
+ }
+
+ def RemoveContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Iterable[Contribution] = {
+ contributionInheritance.remove(SourceEntry(target).unique).getOrElse(Nil)
+ }
+
+ def GetContribution(): List[InGameActivity] = {
+ GetContributionDuringPeriod(System.currentTimeMillis(), duration = 600000)
+ }
+
+ def GetContribution(ending: Long): List[InGameActivity] = {
+ GetContributionDuringPeriod(ending, duration = 600000)
+ }
+
+ def GetContributionDuringPeriod(ending: Long, duration: Long): List[InGameActivity] = {
+ val start = ending - duration
+ History.collect { case repair: RepairFromEquipment
+ if repair.time <= ending && repair.time > start => repair
+ }
+ }
+
+ def HistoryAndContributions(): List[InGameActivity] = {
+ val ending = System.currentTimeMillis()
+ val start = ending - 600000
+ contributionInheritance.foreach { case (obj, list) =>
+ val filtered = list.filter { event => event.time <= ending && event.time > start }
+ if (filtered.isEmpty) {
+ contributionInheritance.remove(obj)
+ } else if (filtered.size != list.size) {
+ contributionInheritance.update(obj, filtered)
+ }
+ }
+ val contributions = contributionInheritance.flatMap { case (_, list) => list }
+ (History ++ contributions).sortBy(_.time)
+ }
+
def ClearHistory(): List[InGameActivity] = {
lastDamage = None
val out = history
@@ -233,14 +299,15 @@ object InGameHistory {
def SpawnReconstructionActivity(
obj: PlanetSideGameObject with FactionAffinity with InGameHistory,
zoneNumber: Int,
- unit: Option[SourceEntry]
+ unit: Option[PlanetSideGameObject with FactionAffinity with InGameHistory]
): Unit = {
+ val toUnitSource = unit.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }
val event: GeneralActivity = if (obj.History.nonEmpty || obj.History.headOption.exists {
_.isInstanceOf[SpawningActivity]
}) {
- ReconstructionActivity(ObjectSource(obj), zoneNumber, unit)
+ ReconstructionActivity(ObjectSource(obj), zoneNumber, toUnitSource)
} else {
- SpawningActivity(ObjectSource(obj), zoneNumber, unit)
+ SpawningActivity(ObjectSource(obj), zoneNumber, toUnitSource)
}
if (obj.History.lastOption match {
case Some(evt: SpawningActivity) => evt != event
@@ -248,6 +315,7 @@ object InGameHistory {
case _ => true
}) {
obj.LogActivity(event)
+ unit.foreach { o => obj.ContributionFrom(o) }
}
}
}
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 9f34f86b8..1f1541c7a 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala
@@ -1,20 +1,15 @@
// 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.{Default, PlanetSideGameObject}
-import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
+import net.psforever.objects.PlanetSideGameObject
+import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.affinity.FactionAffinity
-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.sourcing.{PlayerSource, SourceEntry}
+import net.psforever.objects.vital.{InGameActivity, InGameHistory}
+import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.Zone
-import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
-
-import scala.collection.mutable
-import scala.concurrent.duration._
object ExperienceCalculator {
def apply(zone: Zone): Behavior[Command] =
@@ -41,8 +36,6 @@ object ExperienceCalculator {
}
}
- private case object AssistDecay extends Command
-
def calculateExperience(
victim: PlayerSource,
history: Iterable[InGameActivity]
@@ -80,197 +73,16 @@ 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 RewardThisDeath(victim: SourceWithHealthEntry, lastDamage, history) =>
- sortThisContributionForLater(victim, lastDamage, history)
+ KillAssists.rewardThisPlayerDeath(victim, lastDamage, history, zone.AvatarEvents)
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()
+ KillContributions.rewardTheseSupporters(target, history, kill, bep, zone.AvatarEvents)
case _ => ()
}
Behaviors.same
}
-
- 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)
- val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
- case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
- }.toSeq
- (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
-
- case _ =>
- val assists = collectAssistsForPlayer(victim, shortHistory, None)
- val fullBep = ExperienceCalculator.calculateExperience(victim, shortHistory)
- val hitSquad = assists.map {
- case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
- }.toSeq
- (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
- }
- val events = zone.AvatarEvents
- everyone.foreach { case (p, kda) =>
- events ! AvatarServiceMessage(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda))
- }
- }
-
- private[exp] def collectAssistsForPlayer(
- victim: PlayerSource,
- history: List[InGameActivity],
- killerOpt: Option[PlayerSource]
- ): Iterable[ContributionStatsOutput] = {
- val healthAssists = Support.collectHealthAssists(
- victim,
- history,
- 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 rewardTheseSupporters(
- target: SourceEntry,
- history: List[InGameActivity],
- kill: Kill,
- bep: Long
- ): Unit = {
- val time = kill.time
- val events = zone.AvatarEvents
- 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/KillAssists.scala b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
new file mode 100644
index 000000000..7410cbd6d
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
@@ -0,0 +1,310 @@
+// Copyright (c) 2023 PSForever
+package net.psforever.objects.zones.exp
+
+import akka.actor.ActorRef
+import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
+import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
+import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
+import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairingActivity, RevivingActivity}
+import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+import net.psforever.types.PlanetSideEmpire
+
+import scala.collection.mutable
+import scala.concurrent.duration._
+
+object KillAssists {
+ 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 = {
+ //base value (the kill experience before modifiers)
+ val base = ExperienceCalculator.calculateExperience(victim, history)
+ if (base > 1) {
+ //battle rank disparity modifiers
+ val battleRankDisparity = {
+ import net.psforever.objects.avatar.BattleRank
+ val killerLevel = BattleRank.withExperience(killer.bep).value
+ val victimLevel = BattleRank.withExperience(victim.bep).value
+ if (victimLevel > killerLevel || killerLevel - victimLevel < 6) {
+ if (killerLevel < 7) {
+ 6 * victimLevel + 10
+ } else if (killerLevel < 12) {
+ (12 - killerLevel) * victimLevel + 10
+ } else if (killerLevel < 25) {
+ 25 + victimLevel - killerLevel
+ } else {
+ 25
+ }
+ } else {
+ math.floor(-0.15f * base - killerLevel + victimLevel).toLong
+ }
+ }
+ math.max(1, base + battleRankDisparity)
+ } else {
+ base
+ }
+ }
+
+ private[exp] def rewardThisPlayerDeath(
+ victim: PlayerSource,
+ lastDamage: Option[DamageResult],
+ history: Iterable[InGameActivity],
+ eventBus: ActorRef
+ ): Unit = {
+ val shortHistory = Support.limitHistoryToThisLife(history.toList)
+ val everyone = determineKiller(lastDamage, shortHistory) match {
+ case Some((result, killer: PlayerSource)) =>
+ val assists = collectKillAssistsForPlayer(victim, shortHistory, Some(killer))
+ val fullBep = calculateExperience(killer, victim, shortHistory)
+ val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
+ case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
+ }.toSeq
+ (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
+
+ case _ =>
+ val assists = collectKillAssistsForPlayer(victim, shortHistory, None)
+ val fullBep = ExperienceCalculator.calculateExperience(victim, shortHistory)
+ val hitSquad = assists.map {
+ case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
+ }.toSeq
+ (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
+ }
+ everyone.foreach { case (p, kda) =>
+ eventBus ! AvatarServiceMessage(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda))
+ }
+ }
+
+ private def collectKillAssistsForPlayer(
+ victim: PlayerSource,
+ history: List[InGameActivity],
+ killerOpt: Option[PlayerSource]
+ ): Iterable[ContributionStatsOutput] = {
+ val healthAssists = collectKillAssists(
+ victim,
+ history,
+ Support.allocateContributors(healthDamageContributors)
+ )
+ healthAssists.remove(0L)
+ healthAssists.remove(victim.CharId)
+ killerOpt.map { killer => healthAssists.remove(killer.CharId) }
+ if (Support.wasEverAMax(victim, history)) { //a cardinal sin
+ val armorAssists = collectKillAssists(
+ victim,
+ history,
+ Support.allocateContributors(armorDamageContributors)
+ )
+ armorAssists.remove(0L)
+ armorAssists.remove(victim.CharId)
+ killerOpt.map { killer => armorAssists.remove(killer.CharId) }
+ Support.onlyOriginalAssistEntries(healthAssists, armorAssists)
+ } else {
+ healthAssists.values
+ }
+ }
+
+ private def collectKillAssists(
+ victim: SourceEntry,
+ history: List[InGameActivity],
+ func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
+ ): mutable.LongMap[ContributionStatsOutput] = {
+ val assists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
+ val total = assists.values.foldLeft(0f)(_ + _.total)
+ val output = assists.map { case (id, kda) =>
+ (id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / total))
+ }
+ output.remove(victim.CharId)
+ output
+ }
+
+ 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.foreach {
+ case d: DamagingActivity if d.health > 0 =>
+ inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
+ case r: RevivingActivity =>
+ inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
+ case h: HealingActivity =>
+ inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder)
+ case _ => ()
+ }
+ inOrder
+ }
+
+ private def armorDamageContributors(
+ 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.foreach {
+ case d: DamagingActivity if d.amount - d.health > 0 =>
+ inOrder = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, inOrder)
+ case r: RepairingActivity =>
+ inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
+ case _ => ()
+ }
+ inOrder
+ }
+
+ private def contributeWithDamagingActivity(
+ activity: DamagingActivity,
+ faction: PlanetSideEmpire.Value,
+ amount: Int,
+ participants: mutable.LongMap[ContributionStats],
+ order: Seq[(Long, Int)]
+ ): Seq[(Long, Int)] = {
+ val data = activity.data
+ val playerOpt = data.adversarial.collect { case Adversarial(p: PlayerSource, _,_) => p }
+ contributeWithDamagingActivity(
+ playerOpt,
+ data.interaction.cause.attribution,
+ faction,
+ amount,
+ activity.time,
+ participants,
+ order
+ )
+ }
+
+ private[exp] def contributeWithDamagingActivity(
+ userOpt: Option[PlayerSource],
+ wepid: Int,
+ faction: PlanetSideEmpire.Value,
+ amount: Int,
+ time: Long,
+ participants: mutable.LongMap[ContributionStats],
+ order: Seq[(Long, Int)]
+ ): Seq[(Long, Int)] = {
+ userOpt match {
+ case Some(user)
+ if user.Faction != faction =>
+ val whoId = user.CharId
+ val percentage = amount / user.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 = time
+ )
+ case None =>
+ //new attacker, new entry
+ ContributionStats(
+ user,
+ 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[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)) ++
+ pweapons.slice(0, index).map(_.copy(amount = 0))
+ }
+ 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/KillContributions.scala b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala
new file mode 100644
index 000000000..a546793e7
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala
@@ -0,0 +1,498 @@
+// Copyright (c) 2023 PSForever
+package net.psforever.objects.zones.exp
+
+import akka.actor.ActorRef
+
+import java.util.Date
+import net.psforever.objects.avatar.scoring.{Assist, Kill}
+import net.psforever.objects.sourcing.{PlayerSource, SourceUniqueness}
+import net.psforever.objects.vital.{Contribution, DamagingActivity, HealFromEquipment, HealFromTerminal, HealingActivity, InGameActivity, RepairFromEquipment, RepairFromExoSuitChange, RepairFromTerminal, RepairingActivity, RevivingActivity, SupportActivityCausedByAnother}
+import net.psforever.objects.vital.interaction.Adversarial
+import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+import net.psforever.types.PlanetSideEmpire
+import org.joda.time.Instant
+import org.joda.time.{LocalDateTime => JodaLocalDateTime}
+
+import scala.collection.mutable
+
+object KillContributions {
+ private lazy val recoveryItems: Seq[Int] = {
+ import net.psforever.objects.GlobalDefinitions._
+ Seq(
+ bank.ObjectId,
+ nano_dispenser.ObjectId,
+ medicalapplicator.ObjectId,
+ medical_terminal.ObjectId,
+ adv_med_terminal.ObjectId
+ )
+ }
+
+ private[exp] def rewardTheseSupporters(
+ target: PlayerSource,
+ history: Iterable[InGameActivity],
+ kill: Kill,
+ bep: Long,
+ eventBus: ActorRef
+ ): Unit = {
+ val killTime = kill.time
+ val faction = target.Faction
+ val shortHistory = Support.limitHistoryToThisLife(history.toList)
+ //direct heal assists and repair assists
+ /*
+ can be simplified by ignoring resolved damage and countered healing
+ and just extracting the HealingActivity and DamagingActivity events,
+ but the contribution percentage calculations will need to be adjusted
+ */
+ val healthAssists = cullContributorImplements(allocateContributors(healthRecoveryContributors)(shortHistory, faction))
+ val armorAssists = cullContributorImplements(allocateContributors(armorRecoveryContributors)(shortHistory, faction))
+ //combined
+ val allAssists = healthAssists.map { case out @ (id, healthEntry) =>
+ armorAssists.get(id) match {
+ case Some(armorEntry) =>
+ (id, healthEntry.copy(weapons = healthEntry.weapons ++ armorEntry.weapons))
+ case None =>
+ out
+ }
+ }
+ //revival
+ contributeWithRevivalActivity(shortHistory, allAssists)
+ allAssists.remove(0)
+ allAssists.remove(target.CharId)
+ //divide by applicable time periods (long=10minutes, short=5minutes)
+ val longEvents = findTimeApplicableActivities(allAssists.values.toSeq, killTime, timeOffset = 600)
+ val shortEvents = findTimeApplicableActivities(longEvents, killTime, timeOffset = 300)
+ val victim = kill.victim
+ //convert to output format
+ longEvents
+ .map { stats =>
+ val (_, ContributionStatsOutput(p, w, r)) = mapContributionPointsByPercentage(total = 100f, shortEvents)(stats.player.CharId, stats)
+ (p, Assist(victim, w, r, (bep * r).toLong))
+ }
+ .foreach { case (player, kda) =>
+ eventBus ! AvatarServiceMessage(player.Name, AvatarAction.UpdateKillsDeathsAssists(player.CharId, kda))
+ }
+ }
+
+ private def allocateContributors(
+ tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Seq[(Long, Int)]
+ )
+ (
+ 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 cullContributorImplements(
+ input: mutable.LongMap[ContributionStats]
+ ): mutable.LongMap[ContributionStats] = {
+ input.collect { case (id, entry) =>
+ (id, entry.copy(weapons = entry.weapons.filter { stats => recoveryItems.contains(stats.weapon_id) }))
+ }.filter { case (_, entry) =>
+ entry.weapons.nonEmpty
+ }
+ }
+
+ private def healthRecoveryContributors(
+ 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 damageInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
+ var recoveryInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
+ val contributions = history
+ .collect { case Contribution(unique, entries) => (unique.unique, entries) }
+ .toMap[SourceUniqueness, List[InGameActivity]]
+ val contributionsBy: mutable.LongMap[List[InGameActivity]] =
+ mutable.LongMap[List[InGameActivity]]()
+ history.foreach {
+ case d: DamagingActivity if d.health > 0 =>
+ val (damage, recovery) = contributeWithDamagingActivity(d, faction, d.health, participants, damageInOrder, recoveryInOrder)
+ damageInOrder = damage
+ recoveryInOrder = recovery
+ case h: HealFromEquipment =>
+ val (damage, recovery) = contributeWithRecoveryActivity(h.user, h.equipment_def.ObjectId, faction, h.amount, h.time, participants, damageInOrder, recoveryInOrder)
+ damageInOrder = damage
+ recoveryInOrder = recovery
+ case ht: HealFromTerminal =>
+ val time = ht.time
+ val users = cacheContributionsForEntityByUser(ht.term.unique, contributions, contributionsBy)
+ .collect { case entry: SupportActivityCausedByAnother
+ if entry.time <= time && entry.time > time - 300000 => entry //short support activity time
+ }
+ .groupBy(_.user.unique)
+ .map(_._2.head.user)
+ .toSeq
+ val (damage, recovery) = contributeWithSupportRecoveryActivity(users, ht.term.Definition.ObjectId, faction, ht.amount, time, participants, damageInOrder, recoveryInOrder)
+ damageInOrder = damage
+ recoveryInOrder = recovery
+ case h: HealingActivity =>
+ val (damage, recovery) = contributeWithRecoveryActivity(wepid = 0, faction, h.amount, h.time, participants, damageInOrder, recoveryInOrder)
+ damageInOrder = damage
+ recoveryInOrder = recovery
+ case r: RevivingActivity =>
+ val (damage, recovery) = contributeWithRecoveryActivity(r.equipment.ObjectId, faction, r.amount, r.time, participants, damageInOrder, recoveryInOrder)
+ damageInOrder = damage
+ recoveryInOrder = recovery
+ case _ => ()
+ }
+ recoveryInOrder
+ }
+
+ private def armorRecoveryContributors(
+ 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 damageInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
+ var recoveryInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
+ val contributions = history
+ .collect { case Contribution(unique, entries) => (unique.unique, entries) }
+ .toMap[SourceUniqueness, List[InGameActivity]]
+ val contributionsBy: mutable.LongMap[List[InGameActivity]] =
+ mutable.LongMap[List[InGameActivity]]()
+ history.foreach {
+ case d: DamagingActivity if d.amount - d.health > 0 =>
+ val (damage, recovery) = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, damageInOrder, recoveryInOrder)
+ damageInOrder = damage
+ recoveryInOrder = recovery
+ case r: RepairFromEquipment =>
+ val (damage, recovery) = contributeWithRecoveryActivity(r.user, r.equipment_def.ObjectId, faction, r.amount, r.time, participants, damageInOrder, recoveryInOrder)
+ damageInOrder = damage
+ recoveryInOrder = recovery
+ case rt: RepairFromTerminal => ()
+ val time = rt.time
+ val users = cacheContributionsForEntityByUser(rt.term.unique, contributions, contributionsBy)
+ .collect { case entry: SupportActivityCausedByAnother
+ if entry.time <= time && entry.time > time - 300000 => entry //short support activity time
+ }
+ .groupBy(_.user.unique)
+ .map(_._2.head.user)
+ .toSeq
+ val (damage, recovery) = contributeWithSupportRecoveryActivity(users, rt.term.Definition.ObjectId, faction, rt.amount, time, participants, damageInOrder, recoveryInOrder)
+ damageInOrder = damage
+ recoveryInOrder = recovery
+ case rxc: RepairFromExoSuitChange =>
+ //TODO use cacheContributionsForEntityByUser; what out what order terminal was used
+ val (damage, recovery) = contributeWithRecoveryActivity(wepid = 0, faction, rxc.amount, rxc.time, participants, damageInOrder, recoveryInOrder)
+ damageInOrder = damage
+ recoveryInOrder = recovery
+ case r: RepairingActivity =>
+ val (damage, recovery) = contributeWithRecoveryActivity(wepid = 0, faction, r.amount, r.time, participants, damageInOrder,recoveryInOrder)
+ damageInOrder = damage
+ recoveryInOrder = recovery
+ case _ => ()
+ }
+ recoveryInOrder
+ }
+
+ def cacheContributionsForEntityByUser(
+ target: SourceUniqueness,
+ contributions: Map[SourceUniqueness, List[InGameActivity]],
+ contributionsBy: mutable.LongMap[List[InGameActivity]]
+ ): List[InGameActivity] = {
+ contributions.get(target) match {
+ case Some(list) if list.nonEmpty =>
+ list
+ .collect { case r: RepairFromEquipment => r }
+ .groupBy(_.user.unique)
+ .flatMap { case (_, activities) =>
+ val charId = activities.head.user.CharId
+ contributionsBy.get(charId) match {
+ case Some(outList) =>
+ outList
+ case None =>
+ contributionsBy.put(charId, activities).getOrElse(Nil)
+ }
+ }.toList
+ case _ =>
+ Nil
+ }
+ }
+
+ private def contributeWithDamagingActivity(
+ activity: DamagingActivity,
+ faction: PlanetSideEmpire.Value,
+ amount: Int,
+ participants: mutable.LongMap[ContributionStats],
+ damageOrder: Seq[(Long, Int)],
+ recoveryOrder: Seq[(Long, Int)]
+ ): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
+ //mark entries from the recovery list to truncate
+ val data = activity.data
+ val time = activity.time
+ val playerOpt = data.adversarial.collect { case Adversarial(p: PlayerSource, _, _) => p }
+ var lastCharId = 0L
+ var lastValue = 0
+ var ramt = amount
+ var rindex = -1
+ val riter = recoveryOrder.iterator
+ while (riter.hasNext && ramt > 0) {
+ val (id, value) = riter.next()
+ if (value > 0) {
+ val entry = participants(id)
+ val weapons = entry.weapons
+ lastCharId = id
+ lastValue = value
+ if (value > ramt) {
+ participants.put(
+ id,
+ entry.copy(
+ weapons = weapons.head.copy(amount = weapons.head.amount - ramt, time = time) +: weapons.tail,
+ amount = entry.total - ramt,
+ time = time
+ )
+ )
+ ramt = 0
+ lastValue = lastValue - value
+ } else {
+ participants.put(
+ id,
+ entry.copy(
+ weapons = weapons.tail :+ weapons.head.copy(amount = 0, time = time),
+ amount = entry.total - ramt,
+ time = time
+ )
+ )
+ ramt = ramt - value
+ rindex += 1
+ lastValue = 0
+ }
+ }
+ rindex += 1
+ }
+ //damage calculations
+ val newDamageOrder = KillAssists.contributeWithDamagingActivity(
+ playerOpt,
+ data.interaction.cause.attribution,
+ faction,
+ amount,
+ time,
+ participants,
+ damageOrder
+ )
+ //re-combine output list(s)
+ val leftovers = if (lastValue > 0) {
+ Seq((lastCharId, lastValue))
+ } else {
+ Nil
+ }
+ (newDamageOrder, leftovers ++ recoveryOrder.slice(rindex, recoveryOrder.size) ++ recoveryOrder.take(rindex).map { case (id, _) => (id, 0) })
+ }
+
+ private def contributeWithRecoveryActivity(
+ user: PlayerSource,
+ wepid: Int,
+ faction: PlanetSideEmpire.Value,
+ amount: Int,
+ time: Long,
+ participants: mutable.LongMap[ContributionStats],
+ damageOrder: Seq[(Long, Int)],
+ recoveryOrder: Seq[(Long, Int)]
+ ): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
+ contributeWithRecoveryActivity(user, user.CharId, wepid, faction, amount, time, participants, damageOrder, recoveryOrder)
+ }
+
+ private def contributeWithRecoveryActivity(
+ wepid: Int,
+ faction: PlanetSideEmpire.Value,
+ amount: Int,
+ time: Long,
+ participants: mutable.LongMap[ContributionStats],
+ damageOrder: Seq[(Long, Int)],
+ recoveryOrder: Seq[(Long, Int)]
+ ): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
+ contributeWithRecoveryActivity(PlayerSource.Nobody, charId = 0, wepid, faction, amount, time, participants, damageOrder, recoveryOrder)
+ }
+
+ private def contributeWithRecoveryActivity(
+ user: PlayerSource,
+ charId: Long,
+ wepid: Int,
+ faction: PlanetSideEmpire.Value,
+ amount: Int,
+ time: Long,
+ participants: mutable.LongMap[ContributionStats],
+ damageOrder: Seq[(Long, Int)],
+ recoveryOrder: Seq[(Long, Int)]
+ ): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
+ //recovery calculations
+ val newOrder = KillAssists.contributeWithRecoveryActivity(amount, participants, damageOrder)
+ //detect potential instances of and remove approximate value for friendly-fire recovery (that doesn't count)
+ val qualityRecoveryAmount = {
+ val (entries, count) = {
+ val value = damageOrder.size - newOrder.size
+ if (value > 0) {
+ if (newOrder.head == damageOrder.lift(value).getOrElse(newOrder.head)) {
+ (damageOrder.take(value), value)
+ } else {
+ (damageOrder.take(value + 1), value + 1)
+ }
+ } else {
+ if (newOrder.headOption != damageOrder.headOption) {
+ (damageOrder.headOption.toList, 1)
+ } else {
+ (Nil, 1)
+ }
+ }
+ }
+ if (entries.nonEmpty) {
+ //approximate the value as a percentage
+ amount * entries.count { case (id, _) => participants.get(id).collect { _.player.Faction != faction }.getOrElse(false) } / count
+ } else {
+ amount
+ }
+ }
+ if (qualityRecoveryAmount > 0) {
+ val updatedEntry = participants.get(charId) match {
+ case Some(entry) =>
+ //existing entry
+ entry.copy(
+ weapons = WeaponStats(wepid, qualityRecoveryAmount, 1, time, 1f) +: entry.weapons,
+ amount = entry.amount + qualityRecoveryAmount,
+ total = entry.total + qualityRecoveryAmount,
+ time = time
+ )
+ case None =>
+ //new entry
+ ContributionStats(user, Seq(WeaponStats(wepid, qualityRecoveryAmount, 1, time, 1f)), qualityRecoveryAmount, qualityRecoveryAmount, 1, time)
+ }
+ participants.put(charId, updatedEntry)
+ }
+ //re-combine output list(s) (bridge entry for output recovery list)
+ (newOrder, (charId, amount) +: recoveryOrder)
+ }
+
+ private def contributeWithSupportRecoveryActivity(
+ users: Seq[PlayerSource],
+ wepid: Int,
+ faction: PlanetSideEmpire.Value,
+ amount: Int,
+ time: Long,
+ participants: mutable.LongMap[ContributionStats],
+ damageOrder: Seq[(Long, Int)],
+ recoveryOrder: Seq[(Long, Int)]
+ ): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
+ var outputDamageOrder = damageOrder
+ var outputRecoveryOrder = recoveryOrder
+ users.zip {
+ val numberOfUsers = users.size
+ val out = Array.fill(numberOfUsers)(numberOfUsers / amount)
+ (0 to numberOfUsers % amount).foreach {
+ out(_) += 1
+ }
+ out
+ }.foreach { case (user, subAmount) =>
+ val (a, b) = contributeWithRecoveryActivity(user, user.CharId, wepid, faction, subAmount, time, participants, outputDamageOrder, outputRecoveryOrder)
+ outputDamageOrder = a
+ outputRecoveryOrder = b
+ }
+ (outputDamageOrder, outputRecoveryOrder)
+ }
+
+ private def contributeWithRevivalActivity(
+ history: List[InGameActivity],
+ participants: mutable.LongMap[ContributionStats]
+ ): Unit = {
+ history
+ .collect { case rev: RevivingActivity => rev }
+ .groupBy(_.user.CharId)
+ .foreach { case (id, revivesByThisPlayer) =>
+ val numberOfRevives = revivesByThisPlayer.size
+ val latestTime = revivesByThisPlayer.maxBy(_.time).time
+ val newStats = revivesByThisPlayer.map { stat => WeaponStats(stat.equipment.ObjectId, 100, 1, stat.time, 0.25f) }
+ val newStat = WeaponStats(revivesByThisPlayer.head.equipment.ObjectId, 100 * numberOfRevives, numberOfRevives, latestTime, 0.25f)
+ participants.get(id) match {
+ case Some(stats) =>
+ participants.put(id, stats.copy(weapons = stats.weapons ++ newStats))
+ case None =>
+ participants.put(id, ContributionStats(
+ revivesByThisPlayer.head.user,
+ newStats,
+ newStat.amount,
+ newStat.shots,
+ revivesByThisPlayer.size,
+ latestTime
+ ))
+ }
+ }
+ }
+
+ private def findTimeApplicableActivities(
+ activity: Seq[ContributionStats],
+ killLastTime: JodaLocalDateTime,
+ timeOffset: Int //s
+ ): Seq[ContributionStats] = {
+ val dateTimeConverter: Date=>JodaLocalDateTime = JodaLocalDateTime.fromDateFields
+ val milliToInstant: Long=>org.joda.time.Instant = Instant.ofEpochMilli
+ val targetTime = killLastTime.minusSeconds(timeOffset)
+ //find all activities that occurred before the kill time but after the offset
+ activity.collect { entry =>
+ val weapons = entry.weapons.filter { stat =>
+ val activityTime = dateTimeConverter(milliToInstant(stat.time).toDate)
+ (activityTime.isBefore(killLastTime) || activityTime.equals(killLastTime)) && targetTime.isAfter(activityTime)
+ }
+ .groupBy(_.weapon_id)
+ .collect { case (id, stats) =>
+ WeaponStats(
+ id,
+ stats.foldLeft(0)(_ + _.amount),
+ stats.foldLeft(0)(_ + _.shots),
+ stats.maxBy(_.time).time,
+ stats.foldLeft(0f)(_ + _.contributions)
+ )
+ }
+ if (weapons.nonEmpty) {
+ val totalMod = entry.total / entry.amount.toFloat
+ val recoveryAmount = weapons.foldLeft(0)(_ + _.amount)
+ val recoveryShots = weapons.foldLeft(0)(_ + _.shots)
+ val recoveryTime = weapons.map { _.time }.max
+ Some(ContributionStats(entry.player, weapons.toSeq, recoveryAmount, (recoveryAmount * totalMod).toInt, recoveryShots, recoveryTime))
+ } else {
+ None
+ }
+ }.flatten
+ }
+
+ private def mapContributionPointsByPercentage(
+ total: Float,
+ compareList: Iterable[ContributionStats]
+ )
+ (
+ charId: Long,
+ contribution: ContributionStats,
+ ): (Long, ContributionStatsOutput) = {
+ val user = contribution.player
+ val unique = user.unique
+ val points = contribution.amount
+ val value = if (points < 75) {
+ //a small contribution means the lower time limit
+ if (compareList.exists { a => a.player.unique == unique }) {
+ math.max(0.2f, points / total)
+ } else {
+ 0f
+ }
+ } else {
+ //large contribution is always okay
+ if (points > 299) {
+ 1.0f
+ } else if (points > 100) {
+ 0.75f
+ } else {
+ 0.5f
+ }
+ }
+ (charId, ContributionStatsOutput(user, contribution.weapons.map { _.weapon_id }, value))
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala b/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala
deleted file mode 100644
index 3799af0c7..000000000
--- a/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala
+++ /dev/null
@@ -1,399 +0,0 @@
-// Copyright (c) 2023 PSForever
-package net.psforever.objects.zones.exp
-
-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 = {
- //base value (the kill experience before modifiers)
- val base = ExperienceCalculator.calculateExperience(victim, history)
- if (base > 1) {
- //battle rank disparity modifiers
- val battleRankDisparity = {
- import net.psforever.objects.avatar.BattleRank
- val killerLevel = BattleRank.withExperience(killer.bep).value
- val victimLevel = BattleRank.withExperience(victim.bep).value
- if (victimLevel > killerLevel || killerLevel - victimLevel < 6) {
- if (killerLevel < 7) {
- 6 * victimLevel + 10
- } else if (killerLevel < 12) {
- (12 - killerLevel) * victimLevel + 10
- } else if (killerLevel < 25) {
- 25 + victimLevel - killerLevel
- } else {
- 25
- }
- } else {
- math.floor(-0.15f * base - killerLevel + victimLevel).toLong
- }
- }
- math.max(1, base + battleRankDisparity)
- } else {
- 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/KillDeathAssists2.scala b/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists2.scala
new file mode 100644
index 000000000..4b2b4ad9f
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists2.scala
@@ -0,0 +1,295 @@
+// Copyright (c) NOT VERSIONED PSForever
+package net.psforever.objects.zones.exp
+
+import net.psforever.objects.definition.WithShields
+import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceWithHealthEntry, SourceWithShieldsEntry}
+import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairingActivity, ShieldCharge, SpawningActivity}
+import net.psforever.objects.vital.interaction.Adversarial
+import net.psforever.types.PlanetSideEmpire
+
+import scala.collection.mutable
+
+object KillDeathAssists2 {
+ private def collectKillAssists(
+ victim: SourceEntry,
+ history: List[InGameActivity],
+ func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
+ ): mutable.LongMap[ContributionStatsOutput] = {
+ val assists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
+ val total = assists.values.foldLeft(0f)(_ + _.total)
+ val output = assists.map { case (id, kda) =>
+ (id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / total))
+ }
+ output.remove(victim.CharId)
+ output
+ }
+
+ private[exp] def collectAssistsForEntity(
+ target: SourceWithHealthEntry,
+ history: List[InGameActivity],
+ killerOpt: Option[PlayerSource]
+ ): Iterable[ContributionStatsOutput] = {
+ val (_, maxShields) = maximumEntityHealthAndShields(history)
+ val healthAssists = collectKillAssists(
+ 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 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
+ }
+
+ 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 def armorDamageContributors(
+ 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.foreach {
+ case d: DamagingActivity if d.amount - d.health > 0 =>
+ inOrder = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, inOrder)
+ case r: RepairingActivity =>
+ inOrder = contributeWithRecoveryActivity(r.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)] = {
+ val data = activity.data
+ val playerOpt = data.adversarial.collect { case Adversarial(p: PlayerSource, _,_) => p }
+ contributeWithDamagingActivity(
+ playerOpt,
+ data.interaction.cause.attribution,
+ faction,
+ amount,
+ activity.time,
+ participants,
+ order
+ )
+ }
+
+ private[exp] def contributeWithDamagingActivity(
+ userOpt: Option[PlayerSource],
+ wepid: Int,
+ faction: PlanetSideEmpire.Value,
+ amount: Int,
+ time: Long,
+ participants: mutable.LongMap[ContributionStats],
+ order: Seq[(Long, Int)]
+ ): Seq[(Long, Int)] = {
+ userOpt match {
+ case Some(user)
+ if user.Faction != faction =>
+ val whoId = user.CharId
+ val percentage = amount / user.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 = time
+ )
+ case None =>
+ //new attacker, new entry
+ ContributionStats(
+ user,
+ 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/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
index d6cfe4da9..4d518a504 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
@@ -42,14 +42,18 @@ object Support {
private[exp] def onlyOriginalAssistEntries(
first: mutable.LongMap[ContributionStatsOutput],
- second: mutable.LongMap[ContributionStatsOutput]
+ second: mutable.LongMap[ContributionStatsOutput],
+ combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
+ defaultAdditiveOutputCombiner(multiplier = 0.05f)
): Iterable[ContributionStatsOutput] = {
- onlyOriginalAssistEntriesIterable(first.values, second.values)
+ onlyOriginalAssistEntriesIterable(first.values, second.values, combiner)
}
private[exp] def onlyOriginalAssistEntriesIterable(
first: Iterable[ContributionStatsOutput],
- second: Iterable[ContributionStatsOutput]
+ second: Iterable[ContributionStatsOutput],
+ combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
+ defaultAdditiveOutputCombiner(multiplier = 0.05f)
): Iterable[ContributionStatsOutput] = {
if (second.isEmpty) {
first
@@ -59,12 +63,10 @@ object Support {
//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))
+ h @ ContributionStatsOutput(hid, _, _) <- first
+ a @ ContributionStatsOutput(aid, _, _) <- second
+ out = combiner(h, a)
+ id = out.player.CharId
if hid == aid && shared.put(id, out).isEmpty
} yield ()
val sharedKeys = shared.keys
@@ -72,20 +74,39 @@ object Support {
}
}
- 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 def defaultAdditiveOutputCombiner(
+ multiplier: Float
+ )
+ (
+ first: ContributionStatsOutput,
+ second: ContributionStatsOutput
+ ): ContributionStatsOutput = {
+ if (first.percentage < second.percentage)
+ second.copy(
+ implements = (second.implements ++ first.implements).distinct,
+ percentage = first.percentage + second.implements.size * multiplier
+ )
+ else
+ first.copy(
+ implements = (first.implements ++ second.implements).distinct,
+ percentage = second.percentage + second.implements.size * multiplier
+ )
}
+// private[exp] def collectKillAssists(
+// 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,
@@ -199,7 +220,7 @@ object Support {
}
}
- private def findTimeApplicableActivities(
+ private[exp] def findTimeApplicableActivities(
activity: Seq[SupportActivity],
killLastTime: JodaLocalDateTime,
timeOffset: Int //s
diff --git a/src/test/scala/objects/VitalityTest.scala b/src/test/scala/objects/VitalityTest.scala
index 782cdac00..a7f740f74 100644
--- a/src/test/scala/objects/VitalityTest.scala
+++ b/src/test/scala/objects/VitalityTest.scala
@@ -40,10 +40,10 @@ class VitalityTest extends Specification {
player.LogActivity(result) //DamageResult, straight-up
player.LogActivity(DamageFromProjectile(result))
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
- player.LogActivity(HealFromTerm(term, 10))
+ player.LogActivity(HealFromTerminal(term, 10))
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
player.LogActivity(RepairFromExoSuitChange(ExoSuitType.Standard, 10))
- player.LogActivity(RepairFromTerm(term, 10))
+ player.LogActivity(RepairFromTerminal(term, 10))
player.LogActivity(ShieldCharge(10, Some(vSource)))
player.LogActivity(PlayerSuicide(PlayerSource(player)))
ok
@@ -55,10 +55,10 @@ class VitalityTest extends Specification {
val term = AmenitySource(new Terminal(GlobalDefinitions.order_terminal) { GUID = PlanetSideGUID(1) })
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
- player.LogActivity(HealFromTerm(term, 10))
+ player.LogActivity(HealFromTerminal(term, 10))
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
player.LogActivity(RepairFromExoSuitChange(ExoSuitType.Standard, 10))
- player.LogActivity(RepairFromTerm(term, 10))
+ player.LogActivity(RepairFromTerminal(term, 10))
player.LogActivity(ShieldCharge(10, Some(vSource)))
player.LogActivity(PlayerSuicide(PlayerSource(player)))
player.History.size mustEqual 7
@@ -67,10 +67,10 @@ class VitalityTest extends Specification {
player.History.size mustEqual 0
list.head.isInstanceOf[PlayerSuicide] mustEqual true
list(1).isInstanceOf[ShieldCharge] mustEqual true
- list(2).isInstanceOf[RepairFromTerm] mustEqual true
+ list(2).isInstanceOf[RepairFromTerminal] mustEqual true
list(3).isInstanceOf[RepairFromExoSuitChange] mustEqual true
list(4).isInstanceOf[HealFromImplant] mustEqual true
- list(5).isInstanceOf[HealFromTerm] mustEqual true
+ list(5).isInstanceOf[HealFromTerminal] mustEqual true
list(6).isInstanceOf[HealFromKit] mustEqual true
}
@@ -92,10 +92,10 @@ class VitalityTest extends Specification {
player.LogActivity(DamageFromProjectile(result))
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
- player.LogActivity(HealFromTerm(term, 10))
+ player.LogActivity(HealFromTerminal(term, 10))
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
player.LogActivity(RepairFromExoSuitChange(ExoSuitType.Standard, 10))
- player.LogActivity(RepairFromTerm(term, 10))
+ player.LogActivity(RepairFromTerminal(term, 10))
player.LogActivity(ShieldCharge(10, Some(vSource)))
player.LogActivity(PlayerSuicide(PlayerSource(player)))