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