From 45d2ea544c87728a2374b3b2eaac74e76285edf1 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 11 Apr 2023 13:56:05 -0400 Subject: [PATCH] kill contributions should work; even if they don't, need to put this away for now --- .../actors/session/AvatarActor.scala | 12 +- .../session/support/ZoningOperations.scala | 9 +- .../net/psforever/login/WorldSession.scala | 5 +- .../scala/net/psforever/objects/Player.scala | 21 +- .../terminals/ProximityTerminalControl.scala | 6 +- .../objects/vital/InGameHistory.scala | 86 ++- .../zones/exp/ExperienceCalculator.scala | 202 +------ .../objects/zones/exp/KillAssists.scala | 310 +++++++++++ .../objects/zones/exp/KillContributions.scala | 498 ++++++++++++++++++ .../objects/zones/exp/KillDeathAssists.scala | 399 -------------- .../objects/zones/exp/KillDeathAssists2.scala | 295 +++++++++++ .../psforever/objects/zones/exp/Support.scala | 65 ++- src/test/scala/objects/VitalityTest.scala | 16 +- 13 files changed, 1274 insertions(+), 650 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala create mode 100644 src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala delete mode 100644 src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala create mode 100644 src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists2.scala 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)))