importing controlled implementation changes from original exp-for-kda branch; code for contributions from prior activity, but will be adjusting to new contribution methods

This commit is contained in:
Fate-JH 2023-04-01 00:32:03 -04:00
parent 6774edf5b6
commit 5afa1b2166
8 changed files with 735 additions and 398 deletions

View file

@ -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

View file

@ -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

View file

@ -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
}
}

View file

@ -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 */

View file

@ -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<br>
* 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)<br>
* 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
}
}
}

View file

@ -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)<br>
* 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)<br>
* 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.<br>
* <br>
* 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)<br>
* 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)<br>
* 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)
}
}

View file

@ -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(

View file

@ -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<br>
* 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
}
}
}