kill contributions should work; even if they don't, need to put this away for now

This commit is contained in:
Fate-JH 2023-04-11 13:56:05 -04:00
parent 5afa1b2166
commit 45d2ea544c
13 changed files with 1274 additions and 650 deletions

View file

@ -4,10 +4,14 @@ package net.psforever.actors.session
import akka.actor.Cancellable
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import java.util.concurrent.atomic.AtomicInteger
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.InGameHistory
import org.joda.time.{LocalDateTime, Seconds}
import scala.collection.mutable
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
import scala.util.{Failure, Success}
@ -36,7 +40,7 @@ import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, VehicleLoadout}
import net.psforever.objects.locker.LockerContainer
import net.psforever.objects.sourcing.{PlayerSource,SourceWithHealthEntry}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.vital.{DamagingActivity, HealFromImplant, HealingActivity, SpawningActivity, Vitality}
import net.psforever.objects.vital.{DamagingActivity, HealFromImplant, HealingActivity, SpawningActivity}
import net.psforever.packet.game.objectcreate.{BasicCharacterData, ObjectClass, RibbonBars}
import net.psforever.packet.game.{Friend => GameFriend, _}
import net.psforever.persistence
@ -2949,10 +2953,10 @@ class AvatarActor(
case pr: ProjectileReason => pr.projectile.mounted_in.map { a => zone.GUID(a._1) }
case _ => None
}).collect {
case Some(obj: Vitality) =>
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, obj.History, kill, exp)
case Some(mount: PlanetSideGameObject with FactionAffinity with InGameHistory) =>
player.ContributionFrom(mount)
}
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.History, kill, exp)
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.HistoryAndContributions(), kill, exp)
case _: Assist =>
()
case _: Death =>

View file

@ -2614,10 +2614,11 @@ class ZoningOperations(
LoadZoneAsPlayer(newPlayer, zoneId)
} else {
avatarActor ! AvatarActor.DeactivateActiveImplants()
val betterSpawnPoint = physSpawnPoint.collect { case o: PlanetSideGameObject with FactionAffinity with InGameHistory => o }
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case Some(vehicle: Vehicle) => // driver or passenger in vehicle using a warp gate, or a droppod
InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, toSpawnPoint)
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, betterSpawnPoint)
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneInVehicle(vehicle, pos, ori, zoneId)
case _ if player.HasGUID => // player is deconstructing self or instant action
@ -2629,13 +2630,13 @@ class ZoningOperations(
)
player.Position = pos
player.Orientation = ori
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneAsPlayer(player, zoneId)
case _ => //player is logging in
player.Position = pos
player.Orientation = ori
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneAsPlayer(player, zoneId)
}
}

View file

@ -942,8 +942,9 @@ object WorldSession {
result: Boolean
): Unit = {
if (result) {
player.Zone.GUID(guid).collect {
case term: Terminal => player.LogActivity(TerminalUsedActivity(AmenitySource(term), transaction))
player.Zone.GUID(guid).collect { case term: Terminal =>
player.LogActivity(TerminalUsedActivity(AmenitySource(term), transaction))
player.ContributionFrom(term)
}
}
player.Zone.AvatarEvents ! AvatarServiceMessage(

View file

@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.aura.AuraContainer
import net.psforever.objects.serverobject.environment.InteractWithEnvironment
import net.psforever.objects.serverobject.mount.MountableEntity
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFromEquipment, Vitality}
import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.resolution.DamageResistanceModel
@ -422,12 +422,13 @@ class Player(var avatar: Avatar)
def UsingSpecial_=(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
usingSpecial(state)
//noinspection ScalaUnusedSymbol
private def DefaultUsingSpecial(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
SpecialExoSuitDefinition.Mode.Normal
private def UsingAnchorsOrOverdrive(
state: SpecialExoSuitDefinition.Mode.Value
): SpecialExoSuitDefinition.Mode.Value = {
state: SpecialExoSuitDefinition.Mode.Value
): SpecialExoSuitDefinition.Mode.Value = {
import SpecialExoSuitDefinition.Mode._
val curr = UsingSpecial
val next = if (curr == Normal) {
@ -532,11 +533,14 @@ class Player(var avatar: Avatar)
def Carrying: Option[SpecialCarry] = carrying
//noinspection ScalaUnusedSymbol
def Carrying_=(item: SpecialCarry): Option[SpecialCarry] = {
Carrying
Carrying_=(Some(item))
}
//noinspection ScalaUnusedSymbol
def Carrying_=(item: Option[SpecialCarry]): Option[SpecialCarry] = {
carrying = item
Carrying
}
@ -555,6 +559,14 @@ class Player(var avatar: Avatar)
def canEqual(other: Any): Boolean = other.isInstanceOf[Player]
override def GetContributionDuringPeriod(ending: Long, duration: Long): List[InGameActivity] = {
val start = ending - duration
History.collect {
case heal: HealFromEquipment if heal.time <= ending && heal.time > start => heal
case repair: RepairFromEquipment if repair.time <= ending && repair.time > start => repair
}
}
override def equals(other: Any): Boolean =
other match {
case that: Player =>
@ -620,6 +632,7 @@ object Player {
}
}
//noinspection ScalaUnusedSymbol
def neverRestrict(player: Player, slot: Int): Boolean = {
false
}

View file

@ -17,7 +17,7 @@ import net.psforever.objects.serverobject.damage.DamageableAmenity
import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior}
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity}
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
import net.psforever.objects.vital.{HealFromTerm, RepairFromTerm, Vitality}
import net.psforever.objects.vital.{HealFromTerminal, RepairFromTerminal, Vitality}
import net.psforever.objects.zones.ZoneAware
import net.psforever.packet.game.InventoryStateMessage
import net.psforever.services.Service
@ -291,7 +291,7 @@ object ProximityTerminalControl {
healAmount
}
target.Health = health + finalHealthAmount
target.LogActivity(HealFromTerm(AmenitySource(terminal), finalHealthAmount))
target.LogActivity(HealFromTerminal(AmenitySource(terminal), finalHealthAmount))
updateFunc(target)
target.Health == maxHealth
} else {
@ -338,7 +338,7 @@ object ProximityTerminalControl {
repairAmount
}
target.Armor = armor + finalRepairAmount
target.LogActivity(RepairFromTerm(AmenitySource(terminal), finalRepairAmount))
target.LogActivity(RepairFromTerminal(AmenitySource(terminal), finalRepairAmount))
val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,

View file

@ -4,13 +4,15 @@ package net.psforever.objects.vital
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.{AmenitySource, ObjectSource, PlayerSource, SourceEntry, SourceWithHealthEntry, VehicleSource}
import net.psforever.objects.sourcing.{AmenitySource, ObjectSource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource}
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
import scala.collection.mutable
/* root */
/**
@ -48,6 +50,11 @@ final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
extends GeneralActivity
final case class Contribution(unique: SourceEntry, entries: List[InGameActivity])
extends GeneralActivity {
override val time: Long = entries.maxBy(_.time).time
}
/* vitals history */
/**
@ -77,9 +84,8 @@ trait DamagingActivity extends VitalsActivity {
def health: Int = {
(data.targetBefore, data.targetAfter) match {
case (pb: PlayerSource, pa: PlayerSource) if pb.ExoSuit == ExoSuitType.MAX => pb.total - pa.total
case (pb: SourceWithHealthEntry, pa: SourceWithHealthEntry) => pb.health - pa.health
case _ => 0
case (pb: SourceWithHealthEntry, pa: SourceWithHealthEntry) => pb.health - pa.health
case _ => 0
}
}
}
@ -90,7 +96,7 @@ final case class HealFromKit(kit_def: KitDefinition, amount: Int)
final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends HealingActivity with SupportActivityCausedByAnother
final case class HealFromTerm(term: AmenitySource, amount: Int)
final case class HealFromTerminal(term: AmenitySource, amount: Int)
extends HealingActivity
final case class HealFromImplant(implant: ImplantType, amount: Int)
@ -105,7 +111,7 @@ final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends RepairingActivity with SupportActivityCausedByAnother
final case class RepairFromTerm(term: AmenitySource, amount: Int) extends RepairingActivity
final case class RepairFromTerminal(term: AmenitySource, amount: Int) extends RepairingActivity
final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, vehicle: VehicleSource, amount: Int)
extends RepairingActivity
@ -221,6 +227,66 @@ trait InGameHistory {
}
}
private val contributionInheritance: mutable.HashMap[SourceUniqueness, Seq[Contribution]] =
mutable.HashMap[SourceUniqueness, Seq[Contribution]]()
def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Boolean = {
if (target ne this) {
val events = target.GetContribution()
val nonEmpty = events.nonEmpty
if (nonEmpty) {
val source = SourceEntry(target)
contributionInheritance.get(source.unique) match {
case Some(previousContributions) =>
val uniqueEvents = for {
curr <- events
if !previousContributions.filter(_ == curr).exists(_.time == curr.time)
} yield curr
contributionInheritance.put(source.unique, previousContributions :+ Contribution(source, uniqueEvents))
case None =>
contributionInheritance.put(source.unique, Seq(Contribution(source, events)))
}
}
nonEmpty
} else {
false
}
}
def RemoveContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Iterable[Contribution] = {
contributionInheritance.remove(SourceEntry(target).unique).getOrElse(Nil)
}
def GetContribution(): List[InGameActivity] = {
GetContributionDuringPeriod(System.currentTimeMillis(), duration = 600000)
}
def GetContribution(ending: Long): List[InGameActivity] = {
GetContributionDuringPeriod(ending, duration = 600000)
}
def GetContributionDuringPeriod(ending: Long, duration: Long): List[InGameActivity] = {
val start = ending - duration
History.collect { case repair: RepairFromEquipment
if repair.time <= ending && repair.time > start => repair
}
}
def HistoryAndContributions(): List[InGameActivity] = {
val ending = System.currentTimeMillis()
val start = ending - 600000
contributionInheritance.foreach { case (obj, list) =>
val filtered = list.filter { event => event.time <= ending && event.time > start }
if (filtered.isEmpty) {
contributionInheritance.remove(obj)
} else if (filtered.size != list.size) {
contributionInheritance.update(obj, filtered)
}
}
val contributions = contributionInheritance.flatMap { case (_, list) => list }
(History ++ contributions).sortBy(_.time)
}
def ClearHistory(): List[InGameActivity] = {
lastDamage = None
val out = history
@ -233,14 +299,15 @@ object InGameHistory {
def SpawnReconstructionActivity(
obj: PlanetSideGameObject with FactionAffinity with InGameHistory,
zoneNumber: Int,
unit: Option[SourceEntry]
unit: Option[PlanetSideGameObject with FactionAffinity with InGameHistory]
): Unit = {
val toUnitSource = unit.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }
val event: GeneralActivity = if (obj.History.nonEmpty || obj.History.headOption.exists {
_.isInstanceOf[SpawningActivity]
}) {
ReconstructionActivity(ObjectSource(obj), zoneNumber, unit)
ReconstructionActivity(ObjectSource(obj), zoneNumber, toUnitSource)
} else {
SpawningActivity(ObjectSource(obj), zoneNumber, unit)
SpawningActivity(ObjectSource(obj), zoneNumber, toUnitSource)
}
if (obj.History.lastOption match {
case Some(evt: SpawningActivity) => evt != event
@ -248,6 +315,7 @@ object InGameHistory {
case _ => true
}) {
obj.LogActivity(event)
unit.foreach { o => obj.ContributionFrom(o) }
}
}
}

View file

@ -1,20 +1,15 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import akka.actor.Cancellable
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.actor.typed.{Behavior, SupervisorStrategy}
import net.psforever.objects.{Default, PlanetSideGameObject}
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry}
import net.psforever.objects.vital.{DamageFromExplodingEntity, InGameActivity, InGameHistory}
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.Zone
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import scala.collection.mutable
import scala.concurrent.duration._
object ExperienceCalculator {
def apply(zone: Zone): Behavior[Command] =
@ -41,8 +36,6 @@ object ExperienceCalculator {
}
}
private case object AssistDecay extends Command
def calculateExperience(
victim: PlayerSource,
history: Iterable[InGameActivity]
@ -80,197 +73,16 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
import ExperienceCalculator._
private val retainedKillAssists: mutable.HashMap[SourceUniqueness, InheritedAssistEntry] =
mutable.HashMap[SourceUniqueness, InheritedAssistEntry]()
private val retainedSupportAssists: mutable.HashMap[SourceUniqueness, InheritedAssistEntry] =
mutable.HashMap[SourceUniqueness, InheritedAssistEntry]()
private var inheritedAssistsDecayTimer: Cancellable = Default.Cancellable
def onMessage(msg: Command): Behavior[Command] = {
msg match {
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
rewardThisPlayerDeath(victim, lastDamage, history)
case RewardThisDeath(victim: SourceWithHealthEntry, lastDamage, history) =>
sortThisContributionForLater(victim, lastDamage, history)
KillAssists.rewardThisPlayerDeath(victim, lastDamage, history, zone.AvatarEvents)
case RewardOurSupporters(target: PlayerSource, history, kill, bep) =>
rewardTheseSupporters(target, history.toList, kill, bep)
case RewardOurSupporters(target: SourceWithHealthEntry, history, kill, _) =>
sortTheseSupportersForLater(target, history, kill)
case AssistDecay =>
performAssistDecay()
KillContributions.rewardTheseSupporters(target, history, kill, bep, zone.AvatarEvents)
case _ => ()
}
Behaviors.same
}
private def rewardThisPlayerDeath(
victim: PlayerSource,
lastDamage: Option[DamageResult],
history: Iterable[InGameActivity]
): Unit = {
val shortHistory = Support.limitHistoryToThisLife(history.toList)
val everyone = KillDeathAssists.determineKiller(lastDamage, shortHistory) match {
case Some((result, killer: PlayerSource)) =>
val assists = collectAssistsForPlayer(victim, shortHistory, Some(killer))
val fullBep = KillDeathAssists.calculateExperience(killer, victim, shortHistory)
val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq
(victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
case _ =>
val assists = collectAssistsForPlayer(victim, shortHistory, None)
val fullBep = ExperienceCalculator.calculateExperience(victim, shortHistory)
val hitSquad = assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq
(victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
}
val events = zone.AvatarEvents
everyone.foreach { case (p, kda) =>
events ! AvatarServiceMessage(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda))
}
}
private[exp] def collectAssistsForPlayer(
victim: PlayerSource,
history: List[InGameActivity],
killerOpt: Option[PlayerSource]
): Iterable[ContributionStatsOutput] = {
val healthAssists = Support.collectHealthAssists(
victim,
history,
Support.allocateContributors(KillDeathAssists.healthDamageContributors)
)
healthAssists.remove(0L)
// if (armor > 0) {
// val armorAssists = collectMaxArmorAssists(victim, history, armor.toFloat)
// }
// if (Support.wasEverAMax(victim, history)) {
//
// } else {
//
// }
killerOpt.map { killer => healthAssists.remove(killer.CharId) }
healthAssists.values
}
private def rewardTheseSupporters(
target: SourceEntry,
history: List[InGameActivity],
kill: Kill,
bep: Long
): Unit = {
val time = kill.time
val events = zone.AvatarEvents
val trimmedHistory = Support.limitHistoryToThisLife(history)
val normalAssists = Support.onlyOriginalAssistEntries(
Support.collectHealingSupportAssists(target, time, trimmedHistory),
Support.collectRepairingSupportAssists(target, time, trimmedHistory)
)
(retainedSupportAssists.get(target.unique) match {
case Some(support) =>
Support.onlyOriginalAssistEntriesIterable(normalAssists, support.assists)
case None =>
normalAssists
})
.foreach { case ContributionStatsOutput(p, _, ratio) =>
events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, (ratio * bep).toLong))
}
Support.collectTerminalSupportAssists(target, history).foreach { case ContributionStatsOutput(p, _, reward) =>
events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, reward.toLong))
}
}
/* */
private def sortTheseSupportersForLater(
target: SourceEntry,
history: Iterable[InGameActivity],
kill: Kill
): Unit = {
val time = kill.time
val trimmedHistory = Support.limitHistoryToThisLife(history.toList)
val assists = Support.collectRepairingSupportAssists(target, time, trimmedHistory).values
retainAssistsAndScheduleBlanking(target.unique, assists)
}
private def sortThisContributionForLater(
target: SourceWithHealthEntry,
lastDamage: Option[DamageResult],
history: Iterable[InGameActivity]
): Unit = {
val shortHistory = Support.limitHistoryToThisLife(history.toList)
val assists = KillDeathAssists.determineKiller(lastDamage, shortHistory) match {
case Some((damage, killer: PlayerSource)) =>
ContributionStatsOutput(killer, Seq(damage.interaction.cause.attribution), 1f) +:
collectRetainableAssistsForEntity(target, shortHistory, Some(killer)).toSeq
case _ =>
collectRetainableAssistsForEntity(target, shortHistory, None)
}
retainAssistsAndScheduleBlanking(target.unique, assists)
}
private def collectRetainableAssistsForEntity(
entity: SourceWithHealthEntry,
history: List[InGameActivity],
killerOpt: Option[PlayerSource]
): Iterable[ContributionStatsOutput] = {
val assists = KillDeathAssists.collectAssistsForEntity(entity, history, killerOpt)
Support.onlyOriginalAssistEntriesIterable(
assists,
recoverRetainedKillAssists(assists, history)
)
}
private def recoverRetainedKillAssists(
assists: Iterable[ContributionStatsOutput],
history: Iterable[InGameActivity]
): Iterable[ContributionStatsOutput] = {
history.collect { case ex: DamageFromExplodingEntity =>
ex.data
.adversarial
.flatMap {
case Adversarial(attacker: PlayerSource, _, _) =>
Some((
assists.find { case ContributionStatsOutput(p, _, _) => p == attacker },
retainedKillAssists.get(ex.data.targetAfter.unique)
))
}
.map {
case (Some(_), Some(retained)) => retained.assists
}
}.flatten.flatten
}
private[this] def retainAssistsAndScheduleBlanking(
target: SourceUniqueness,
assists: Iterable[ContributionStatsOutput]
): Boolean = {
val retime = retainedKillAssists.isEmpty && inheritedAssistsDecayTimer.isCancelled
retainedKillAssists.put(target, InheritedAssistEntry(assists))
if (retime) {
inheritedAssistsDecayTimer = context.scheduleOnce(3.minutes, context.self, AssistDecay)
}
retime
}
private[this] def performAssistDecay(): Unit = {
val curr = System.currentTimeMillis()
val dur = 5.minutes.toMillis
retainedKillAssists.filterInPlace { case (_, entry) => curr - entry.time < dur }
retainedSupportAssists.filterInPlace { case (_, entry) => curr - entry.time < dur }
if (retainedKillAssists.nonEmpty || retainedSupportAssists.nonEmpty) {
inheritedAssistsDecayTimer = context.scheduleOnce(3.minutes, context.self, AssistDecay)
} else {
inheritedAssistsDecayTimer = Default.Cancellable
}
}
}

View file

@ -0,0 +1,310 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import akka.actor.ActorRef
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairingActivity, RevivingActivity}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire
import scala.collection.mutable
import scala.concurrent.duration._
object KillAssists {
private[exp] def determineKiller(
lastDamageActivity: Option[DamageResult],
history: List[InGameActivity]
): Option[(DamageResult, SourceEntry)] = {
val now = System.currentTimeMillis()
val compareTimeMillis = 10.seconds.toMillis
lastDamageActivity
.collect { case dam if now - dam.interaction.hitTime < compareTimeMillis => dam }
.flatMap { dam => Some(dam, dam.adversarial) }
.orElse {
history.collect { case damage: DamagingActivity
if now - damage.time < compareTimeMillis && damage.data.adversarial.nonEmpty =>
damage.data
}
.flatMap { dam => Some(dam, dam.adversarial) }.lastOption
}
.collect { case (dam, Some(adv)) => (dam, adv.attacker) }
}
private[exp] def calculateExperience(
killer: PlayerSource,
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
//base value (the kill experience before modifiers)
val base = ExperienceCalculator.calculateExperience(victim, history)
if (base > 1) {
//battle rank disparity modifiers
val battleRankDisparity = {
import net.psforever.objects.avatar.BattleRank
val killerLevel = BattleRank.withExperience(killer.bep).value
val victimLevel = BattleRank.withExperience(victim.bep).value
if (victimLevel > killerLevel || killerLevel - victimLevel < 6) {
if (killerLevel < 7) {
6 * victimLevel + 10
} else if (killerLevel < 12) {
(12 - killerLevel) * victimLevel + 10
} else if (killerLevel < 25) {
25 + victimLevel - killerLevel
} else {
25
}
} else {
math.floor(-0.15f * base - killerLevel + victimLevel).toLong
}
}
math.max(1, base + battleRankDisparity)
} else {
base
}
}
private[exp] def rewardThisPlayerDeath(
victim: PlayerSource,
lastDamage: Option[DamageResult],
history: Iterable[InGameActivity],
eventBus: ActorRef
): Unit = {
val shortHistory = Support.limitHistoryToThisLife(history.toList)
val everyone = determineKiller(lastDamage, shortHistory) match {
case Some((result, killer: PlayerSource)) =>
val assists = collectKillAssistsForPlayer(victim, shortHistory, Some(killer))
val fullBep = calculateExperience(killer, victim, shortHistory)
val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq
(victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
case _ =>
val assists = collectKillAssistsForPlayer(victim, shortHistory, None)
val fullBep = ExperienceCalculator.calculateExperience(victim, shortHistory)
val hitSquad = assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq
(victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
}
everyone.foreach { case (p, kda) =>
eventBus ! AvatarServiceMessage(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda))
}
}
private def collectKillAssistsForPlayer(
victim: PlayerSource,
history: List[InGameActivity],
killerOpt: Option[PlayerSource]
): Iterable[ContributionStatsOutput] = {
val healthAssists = collectKillAssists(
victim,
history,
Support.allocateContributors(healthDamageContributors)
)
healthAssists.remove(0L)
healthAssists.remove(victim.CharId)
killerOpt.map { killer => healthAssists.remove(killer.CharId) }
if (Support.wasEverAMax(victim, history)) { //a cardinal sin
val armorAssists = collectKillAssists(
victim,
history,
Support.allocateContributors(armorDamageContributors)
)
armorAssists.remove(0L)
armorAssists.remove(victim.CharId)
killerOpt.map { killer => armorAssists.remove(killer.CharId) }
Support.onlyOriginalAssistEntries(healthAssists, armorAssists)
} else {
healthAssists.values
}
}
private def collectKillAssists(
victim: SourceEntry,
history: List[InGameActivity],
func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStatsOutput] = {
val assists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
val total = assists.values.foldLeft(0f)(_ + _.total)
val output = assists.map { case (id, kda) =>
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / total))
}
output.remove(victim.CharId)
output
}
private def healthDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
* key - character identifier,
* value - current damage contribution */
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.foreach {
case d: DamagingActivity if d.health > 0 =>
inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
case r: RevivingActivity =>
inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
case h: HealingActivity =>
inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder)
case _ => ()
}
inOrder
}
private def armorDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
* key - character identifier,
* value - current damage contribution */
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.foreach {
case d: DamagingActivity if d.amount - d.health > 0 =>
inOrder = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, inOrder)
case r: RepairingActivity =>
inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
case _ => ()
}
inOrder
}
private def contributeWithDamagingActivity(
activity: DamagingActivity,
faction: PlanetSideEmpire.Value,
amount: Int,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
val data = activity.data
val playerOpt = data.adversarial.collect { case Adversarial(p: PlayerSource, _,_) => p }
contributeWithDamagingActivity(
playerOpt,
data.interaction.cause.attribution,
faction,
amount,
activity.time,
participants,
order
)
}
private[exp] def contributeWithDamagingActivity(
userOpt: Option[PlayerSource],
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
userOpt match {
case Some(user)
if user.Faction != faction =>
val whoId = user.CharId
val percentage = amount / user.Definition.MaxHealth.toFloat
val updatedEntry = participants.get(whoId) match {
case Some(mod) =>
//previous attacker, just add to entry
val firstWeapon = mod.weapons.head
val weapons = if (firstWeapon.weapon_id == wepid) {
firstWeapon.copy(
amount = firstWeapon.amount + amount,
shots = firstWeapon.shots + 1,
time = time,
contributions = firstWeapon.contributions + percentage
) +: mod.weapons.tail
} else {
WeaponStats(wepid, amount, 1, time, percentage) +: mod.weapons
}
mod.copy(
amount = mod.amount + amount,
weapons = weapons,
total = mod.total + amount,
shots = mod.shots + 1,
time = time
)
case None =>
//new attacker, new entry
ContributionStats(
user,
Seq(WeaponStats(wepid, amount, 1, time, percentage)),
amount,
amount,
1,
time
)
}
participants.put(whoId, updatedEntry)
order.indexWhere({ case (id, _) => id == whoId }) match {
case 0 =>
//ongoing attack by same player
val entry = order.head
(entry._1, entry._2 + amount) +: order.tail
case _ =>
//different player than immediate prior attacker
(whoId, amount) +: order
}
case _ =>
//damage that does not lead to contribution
order.headOption match {
case Some((id, dam)) =>
if (id == 0L) {
(0L, dam + amount) +: order.tail //pool
} else {
(0L, amount) +: order //new
}
case None =>
order
}
}
}
private[exp] def contributeWithRecoveryActivity(
amount: Int,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
var amt = amount
var count = 0
var newOrder: Seq[(Long, Int)] = Nil
order.takeWhile { entry =>
val (id, total) = entry
if (id > 0 && total > 0) {
val part = participants(id)
if (amount > total) {
//drop this entry
participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case
amt = amt - total
} else {
//edit around the inclusion of this entry
val newTotal = total - amt
val trimmedWeapons = {
var index = -1
var weaponSum = 0
val pweapons = part.weapons
while (weaponSum < amt) {
index += 1
weaponSum = weaponSum + pweapons(index).amount
}
(pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size)) ++
pweapons.slice(0, index).map(_.copy(amount = 0))
}
newOrder = (id, newTotal) +: newOrder
participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
amt = 0
}
}
count += 1
amt > 0
}
newOrder ++ order.drop(count)
}
}

View file

@ -0,0 +1,498 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import akka.actor.ActorRef
import java.util.Date
import net.psforever.objects.avatar.scoring.{Assist, Kill}
import net.psforever.objects.sourcing.{PlayerSource, SourceUniqueness}
import net.psforever.objects.vital.{Contribution, DamagingActivity, HealFromEquipment, HealFromTerminal, HealingActivity, InGameActivity, RepairFromEquipment, RepairFromExoSuitChange, RepairFromTerminal, RepairingActivity, RevivingActivity, SupportActivityCausedByAnother}
import net.psforever.objects.vital.interaction.Adversarial
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire
import org.joda.time.Instant
import org.joda.time.{LocalDateTime => JodaLocalDateTime}
import scala.collection.mutable
object KillContributions {
private lazy val recoveryItems: Seq[Int] = {
import net.psforever.objects.GlobalDefinitions._
Seq(
bank.ObjectId,
nano_dispenser.ObjectId,
medicalapplicator.ObjectId,
medical_terminal.ObjectId,
adv_med_terminal.ObjectId
)
}
private[exp] def rewardTheseSupporters(
target: PlayerSource,
history: Iterable[InGameActivity],
kill: Kill,
bep: Long,
eventBus: ActorRef
): Unit = {
val killTime = kill.time
val faction = target.Faction
val shortHistory = Support.limitHistoryToThisLife(history.toList)
//direct heal assists and repair assists
/*
can be simplified by ignoring resolved damage and countered healing
and just extracting the HealingActivity and DamagingActivity events,
but the contribution percentage calculations will need to be adjusted
*/
val healthAssists = cullContributorImplements(allocateContributors(healthRecoveryContributors)(shortHistory, faction))
val armorAssists = cullContributorImplements(allocateContributors(armorRecoveryContributors)(shortHistory, faction))
//combined
val allAssists = healthAssists.map { case out @ (id, healthEntry) =>
armorAssists.get(id) match {
case Some(armorEntry) =>
(id, healthEntry.copy(weapons = healthEntry.weapons ++ armorEntry.weapons))
case None =>
out
}
}
//revival
contributeWithRevivalActivity(shortHistory, allAssists)
allAssists.remove(0)
allAssists.remove(target.CharId)
//divide by applicable time periods (long=10minutes, short=5minutes)
val longEvents = findTimeApplicableActivities(allAssists.values.toSeq, killTime, timeOffset = 600)
val shortEvents = findTimeApplicableActivities(longEvents, killTime, timeOffset = 300)
val victim = kill.victim
//convert to output format
longEvents
.map { stats =>
val (_, ContributionStatsOutput(p, w, r)) = mapContributionPointsByPercentage(total = 100f, shortEvents)(stats.player.CharId, stats)
(p, Assist(victim, w, r, (bep * r).toLong))
}
.foreach { case (player, kda) =>
eventBus ! AvatarServiceMessage(player.Name, AvatarAction.UpdateKillsDeathsAssists(player.CharId, kda))
}
}
private def allocateContributors(
tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Seq[(Long, Int)]
)
(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value
): mutable.LongMap[ContributionStats] = {
/** players who have contributed to this death, and how much they have contributed<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 cullContributorImplements(
input: mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStats] = {
input.collect { case (id, entry) =>
(id, entry.copy(weapons = entry.weapons.filter { stats => recoveryItems.contains(stats.weapon_id) }))
}.filter { case (_, entry) =>
entry.weapons.nonEmpty
}
}
private def healthRecoveryContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
* key - character identifier,
* value - current damage contribution */
var damageInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
var recoveryInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
val contributions = history
.collect { case Contribution(unique, entries) => (unique.unique, entries) }
.toMap[SourceUniqueness, List[InGameActivity]]
val contributionsBy: mutable.LongMap[List[InGameActivity]] =
mutable.LongMap[List[InGameActivity]]()
history.foreach {
case d: DamagingActivity if d.health > 0 =>
val (damage, recovery) = contributeWithDamagingActivity(d, faction, d.health, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case h: HealFromEquipment =>
val (damage, recovery) = contributeWithRecoveryActivity(h.user, h.equipment_def.ObjectId, faction, h.amount, h.time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case ht: HealFromTerminal =>
val time = ht.time
val users = cacheContributionsForEntityByUser(ht.term.unique, contributions, contributionsBy)
.collect { case entry: SupportActivityCausedByAnother
if entry.time <= time && entry.time > time - 300000 => entry //short support activity time
}
.groupBy(_.user.unique)
.map(_._2.head.user)
.toSeq
val (damage, recovery) = contributeWithSupportRecoveryActivity(users, ht.term.Definition.ObjectId, faction, ht.amount, time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case h: HealingActivity =>
val (damage, recovery) = contributeWithRecoveryActivity(wepid = 0, faction, h.amount, h.time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case r: RevivingActivity =>
val (damage, recovery) = contributeWithRecoveryActivity(r.equipment.ObjectId, faction, r.amount, r.time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case _ => ()
}
recoveryInOrder
}
private def armorRecoveryContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
* key - character identifier,
* value - current damage contribution */
var damageInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
var recoveryInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
val contributions = history
.collect { case Contribution(unique, entries) => (unique.unique, entries) }
.toMap[SourceUniqueness, List[InGameActivity]]
val contributionsBy: mutable.LongMap[List[InGameActivity]] =
mutable.LongMap[List[InGameActivity]]()
history.foreach {
case d: DamagingActivity if d.amount - d.health > 0 =>
val (damage, recovery) = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case r: RepairFromEquipment =>
val (damage, recovery) = contributeWithRecoveryActivity(r.user, r.equipment_def.ObjectId, faction, r.amount, r.time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case rt: RepairFromTerminal => ()
val time = rt.time
val users = cacheContributionsForEntityByUser(rt.term.unique, contributions, contributionsBy)
.collect { case entry: SupportActivityCausedByAnother
if entry.time <= time && entry.time > time - 300000 => entry //short support activity time
}
.groupBy(_.user.unique)
.map(_._2.head.user)
.toSeq
val (damage, recovery) = contributeWithSupportRecoveryActivity(users, rt.term.Definition.ObjectId, faction, rt.amount, time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case rxc: RepairFromExoSuitChange =>
//TODO use cacheContributionsForEntityByUser; what out what order terminal was used
val (damage, recovery) = contributeWithRecoveryActivity(wepid = 0, faction, rxc.amount, rxc.time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case r: RepairingActivity =>
val (damage, recovery) = contributeWithRecoveryActivity(wepid = 0, faction, r.amount, r.time, participants, damageInOrder,recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case _ => ()
}
recoveryInOrder
}
def cacheContributionsForEntityByUser(
target: SourceUniqueness,
contributions: Map[SourceUniqueness, List[InGameActivity]],
contributionsBy: mutable.LongMap[List[InGameActivity]]
): List[InGameActivity] = {
contributions.get(target) match {
case Some(list) if list.nonEmpty =>
list
.collect { case r: RepairFromEquipment => r }
.groupBy(_.user.unique)
.flatMap { case (_, activities) =>
val charId = activities.head.user.CharId
contributionsBy.get(charId) match {
case Some(outList) =>
outList
case None =>
contributionsBy.put(charId, activities).getOrElse(Nil)
}
}.toList
case _ =>
Nil
}
}
private def contributeWithDamagingActivity(
activity: DamagingActivity,
faction: PlanetSideEmpire.Value,
amount: Int,
participants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
//mark entries from the recovery list to truncate
val data = activity.data
val time = activity.time
val playerOpt = data.adversarial.collect { case Adversarial(p: PlayerSource, _, _) => p }
var lastCharId = 0L
var lastValue = 0
var ramt = amount
var rindex = -1
val riter = recoveryOrder.iterator
while (riter.hasNext && ramt > 0) {
val (id, value) = riter.next()
if (value > 0) {
val entry = participants(id)
val weapons = entry.weapons
lastCharId = id
lastValue = value
if (value > ramt) {
participants.put(
id,
entry.copy(
weapons = weapons.head.copy(amount = weapons.head.amount - ramt, time = time) +: weapons.tail,
amount = entry.total - ramt,
time = time
)
)
ramt = 0
lastValue = lastValue - value
} else {
participants.put(
id,
entry.copy(
weapons = weapons.tail :+ weapons.head.copy(amount = 0, time = time),
amount = entry.total - ramt,
time = time
)
)
ramt = ramt - value
rindex += 1
lastValue = 0
}
}
rindex += 1
}
//damage calculations
val newDamageOrder = KillAssists.contributeWithDamagingActivity(
playerOpt,
data.interaction.cause.attribution,
faction,
amount,
time,
participants,
damageOrder
)
//re-combine output list(s)
val leftovers = if (lastValue > 0) {
Seq((lastCharId, lastValue))
} else {
Nil
}
(newDamageOrder, leftovers ++ recoveryOrder.slice(rindex, recoveryOrder.size) ++ recoveryOrder.take(rindex).map { case (id, _) => (id, 0) })
}
private def contributeWithRecoveryActivity(
user: PlayerSource,
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
contributeWithRecoveryActivity(user, user.CharId, wepid, faction, amount, time, participants, damageOrder, recoveryOrder)
}
private def contributeWithRecoveryActivity(
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
contributeWithRecoveryActivity(PlayerSource.Nobody, charId = 0, wepid, faction, amount, time, participants, damageOrder, recoveryOrder)
}
private def contributeWithRecoveryActivity(
user: PlayerSource,
charId: Long,
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
//recovery calculations
val newOrder = KillAssists.contributeWithRecoveryActivity(amount, participants, damageOrder)
//detect potential instances of and remove approximate value for friendly-fire recovery (that doesn't count)
val qualityRecoveryAmount = {
val (entries, count) = {
val value = damageOrder.size - newOrder.size
if (value > 0) {
if (newOrder.head == damageOrder.lift(value).getOrElse(newOrder.head)) {
(damageOrder.take(value), value)
} else {
(damageOrder.take(value + 1), value + 1)
}
} else {
if (newOrder.headOption != damageOrder.headOption) {
(damageOrder.headOption.toList, 1)
} else {
(Nil, 1)
}
}
}
if (entries.nonEmpty) {
//approximate the value as a percentage
amount * entries.count { case (id, _) => participants.get(id).collect { _.player.Faction != faction }.getOrElse(false) } / count
} else {
amount
}
}
if (qualityRecoveryAmount > 0) {
val updatedEntry = participants.get(charId) match {
case Some(entry) =>
//existing entry
entry.copy(
weapons = WeaponStats(wepid, qualityRecoveryAmount, 1, time, 1f) +: entry.weapons,
amount = entry.amount + qualityRecoveryAmount,
total = entry.total + qualityRecoveryAmount,
time = time
)
case None =>
//new entry
ContributionStats(user, Seq(WeaponStats(wepid, qualityRecoveryAmount, 1, time, 1f)), qualityRecoveryAmount, qualityRecoveryAmount, 1, time)
}
participants.put(charId, updatedEntry)
}
//re-combine output list(s) (bridge entry for output recovery list)
(newOrder, (charId, amount) +: recoveryOrder)
}
private def contributeWithSupportRecoveryActivity(
users: Seq[PlayerSource],
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
var outputDamageOrder = damageOrder
var outputRecoveryOrder = recoveryOrder
users.zip {
val numberOfUsers = users.size
val out = Array.fill(numberOfUsers)(numberOfUsers / amount)
(0 to numberOfUsers % amount).foreach {
out(_) += 1
}
out
}.foreach { case (user, subAmount) =>
val (a, b) = contributeWithRecoveryActivity(user, user.CharId, wepid, faction, subAmount, time, participants, outputDamageOrder, outputRecoveryOrder)
outputDamageOrder = a
outputRecoveryOrder = b
}
(outputDamageOrder, outputRecoveryOrder)
}
private def contributeWithRevivalActivity(
history: List[InGameActivity],
participants: mutable.LongMap[ContributionStats]
): Unit = {
history
.collect { case rev: RevivingActivity => rev }
.groupBy(_.user.CharId)
.foreach { case (id, revivesByThisPlayer) =>
val numberOfRevives = revivesByThisPlayer.size
val latestTime = revivesByThisPlayer.maxBy(_.time).time
val newStats = revivesByThisPlayer.map { stat => WeaponStats(stat.equipment.ObjectId, 100, 1, stat.time, 0.25f) }
val newStat = WeaponStats(revivesByThisPlayer.head.equipment.ObjectId, 100 * numberOfRevives, numberOfRevives, latestTime, 0.25f)
participants.get(id) match {
case Some(stats) =>
participants.put(id, stats.copy(weapons = stats.weapons ++ newStats))
case None =>
participants.put(id, ContributionStats(
revivesByThisPlayer.head.user,
newStats,
newStat.amount,
newStat.shots,
revivesByThisPlayer.size,
latestTime
))
}
}
}
private def findTimeApplicableActivities(
activity: Seq[ContributionStats],
killLastTime: JodaLocalDateTime,
timeOffset: Int //s
): Seq[ContributionStats] = {
val dateTimeConverter: Date=>JodaLocalDateTime = JodaLocalDateTime.fromDateFields
val milliToInstant: Long=>org.joda.time.Instant = Instant.ofEpochMilli
val targetTime = killLastTime.minusSeconds(timeOffset)
//find all activities that occurred before the kill time but after the offset
activity.collect { entry =>
val weapons = entry.weapons.filter { stat =>
val activityTime = dateTimeConverter(milliToInstant(stat.time).toDate)
(activityTime.isBefore(killLastTime) || activityTime.equals(killLastTime)) && targetTime.isAfter(activityTime)
}
.groupBy(_.weapon_id)
.collect { case (id, stats) =>
WeaponStats(
id,
stats.foldLeft(0)(_ + _.amount),
stats.foldLeft(0)(_ + _.shots),
stats.maxBy(_.time).time,
stats.foldLeft(0f)(_ + _.contributions)
)
}
if (weapons.nonEmpty) {
val totalMod = entry.total / entry.amount.toFloat
val recoveryAmount = weapons.foldLeft(0)(_ + _.amount)
val recoveryShots = weapons.foldLeft(0)(_ + _.shots)
val recoveryTime = weapons.map { _.time }.max
Some(ContributionStats(entry.player, weapons.toSeq, recoveryAmount, (recoveryAmount * totalMod).toInt, recoveryShots, recoveryTime))
} else {
None
}
}.flatten
}
private def mapContributionPointsByPercentage(
total: Float,
compareList: Iterable[ContributionStats]
)
(
charId: Long,
contribution: ContributionStats,
): (Long, ContributionStatsOutput) = {
val user = contribution.player
val unique = user.unique
val points = contribution.amount
val value = if (points < 75) {
//a small contribution means the lower time limit
if (compareList.exists { a => a.player.unique == unique }) {
math.max(0.2f, points / total)
} else {
0f
}
} else {
//large contribution is always okay
if (points > 299) {
1.0f
} else if (points > 100) {
0.75f
} else {
0.5f
}
}
(charId, ContributionStatsOutput(user, contribution.weapons.map { _.weapon_id }, value))
}
}

View file

@ -1,399 +0,0 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import net.psforever.objects.definition.{ExoSuitDefinition, WithShields}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceWithHealthEntry, SourceWithShieldsEntry}
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairFromExoSuitChange, RepairingActivity, ShieldCharge, SpawningActivity}
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
import scala.collection.mutable
import scala.concurrent.duration._
object KillDeathAssists {
private [exp] def determineKiller(
lastDamageActivity: Option[DamageResult],
history: List[InGameActivity]
): Option[(DamageResult, SourceEntry)] = {
val now = System.currentTimeMillis()
val compareTimeMillis = 10.seconds.toMillis
lastDamageActivity
.collect { case dam if now - dam.interaction.hitTime < compareTimeMillis => dam }
.flatMap { dam => Some(dam, dam.adversarial) }
.orElse {
history.collect { case damage: DamagingActivity
if now - damage.time < compareTimeMillis && damage.data.adversarial.nonEmpty =>
damage.data
}
.flatMap { dam => Some(dam, dam.adversarial) }.lastOption
}
.collect { case (dam, Some(adv)) => (dam, adv.attacker) }
}
private[exp] def calculateExperience(
killer: PlayerSource,
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
//base value (the kill experience before modifiers)
val base = ExperienceCalculator.calculateExperience(victim, history)
if (base > 1) {
//battle rank disparity modifiers
val battleRankDisparity = {
import net.psforever.objects.avatar.BattleRank
val killerLevel = BattleRank.withExperience(killer.bep).value
val victimLevel = BattleRank.withExperience(victim.bep).value
if (victimLevel > killerLevel || killerLevel - victimLevel < 6) {
if (killerLevel < 7) {
6 * victimLevel + 10
} else if (killerLevel < 12) {
(12 - killerLevel) * victimLevel + 10
} else if (killerLevel < 25) {
25 + victimLevel - killerLevel
} else {
25
}
} else {
math.floor(-0.15f * base - killerLevel + victimLevel).toLong
}
}
math.max(1, base + battleRankDisparity)
} else {
base
}
}
private[exp] def collectAssistsForEntity(
target: SourceWithHealthEntry,
history: List[InGameActivity],
killerOpt: Option[PlayerSource]
): Iterable[ContributionStatsOutput] = {
val (_, maxShields) = maximumEntityHealthAndShields(history)
val healthAssists = Support.collectHealthAssists(
target,
history,
Support.allocateContributors(entityHealthDamageContributors)
)
if (maxShields > 0) {
val shieldAssists = collectShieldAssists(target.asInstanceOf[SourceWithShieldsEntry], history)
killerOpt.collect {
case killer =>
healthAssists.remove(killer.CharId)
shieldAssists.remove(killer.CharId)
}
Support.onlyOriginalAssistEntries(healthAssists, shieldAssists)
} else {
killerOpt.collect {
case killer =>
healthAssists.remove(killer.CharId)
}
healthAssists.values
}
}
private[exp] def collectMaxArmorAssists(
victim: PlayerSource,
history: List[InGameActivity],
fullArmor: Float
): mutable.LongMap[ContributionStatsOutput] = {
val initialArmorAssists = Support.allocateContributors(maxArmorDamageContributors)(history, victim.Faction)
val allArmorDamage = initialArmorAssists.values.foldLeft(0f)(_ + _.total)
val flatComparativeRate = if (allArmorDamage > fullArmor * 1.35f) {
0.4f //this max has been damaged and repaired a lot
} else {
0.65f
}
val armorAssists = initialArmorAssists.map { case (id, kda) =>
val averageRateOfAllWeapons = kda.weapons.map { stat => stat.contributions / stat.shots }.sum / kda.weapons.size
val finalRate = math.max(flatComparativeRate, (flatComparativeRate + averageRateOfAllWeapons) * 0.5f)
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, finalRate))
}
armorAssists.remove(victim.CharId)
armorAssists
}
private def maxArmorDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
var isMax = history.head match {
case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
case _ => false
}
/** damage as it is measured in order (with repair-countered damage eliminated)<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

@ -0,0 +1,295 @@
// Copyright (c) NOT VERSIONED PSForever
package net.psforever.objects.zones.exp
import net.psforever.objects.definition.WithShields
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceWithHealthEntry, SourceWithShieldsEntry}
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairingActivity, ShieldCharge, SpawningActivity}
import net.psforever.objects.vital.interaction.Adversarial
import net.psforever.types.PlanetSideEmpire
import scala.collection.mutable
object KillDeathAssists2 {
private def collectKillAssists(
victim: SourceEntry,
history: List[InGameActivity],
func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStatsOutput] = {
val assists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
val total = assists.values.foldLeft(0f)(_ + _.total)
val output = assists.map { case (id, kda) =>
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / total))
}
output.remove(victim.CharId)
output
}
private[exp] def collectAssistsForEntity(
target: SourceWithHealthEntry,
history: List[InGameActivity],
killerOpt: Option[PlayerSource]
): Iterable[ContributionStatsOutput] = {
val (_, maxShields) = maximumEntityHealthAndShields(history)
val healthAssists = collectKillAssists(
target,
history,
Support.allocateContributors(entityHealthDamageContributors)
)
if (maxShields > 0) {
val shieldAssists = collectShieldAssists(target.asInstanceOf[SourceWithShieldsEntry], history)
killerOpt.collect {
case killer =>
healthAssists.remove(killer.CharId)
shieldAssists.remove(killer.CharId)
}
Support.onlyOriginalAssistEntries(healthAssists, shieldAssists)
} else {
killerOpt.collect {
case killer =>
healthAssists.remove(killer.CharId)
}
healthAssists.values
}
}
private[exp] def collectShieldAssists(
victim: SourceWithShieldsEntry,
history: List[InGameActivity],
): mutable.LongMap[ContributionStatsOutput] = {
val initialShieldAssists = Support.allocateContributors(entityShieldDamageContributors)(history, victim.Faction)
.filterNot { case (_, kda) => kda.amount <= 0 }
val fullFloatShield = initialShieldAssists.values.foldLeft(0)(_ + _.total).toFloat
val shieldAssists = initialShieldAssists.map { case (id, kda) =>
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, math.min(kda.total / fullFloatShield, 0.4f)))
}
shieldAssists.remove(victim.CharId)
shieldAssists
}
private def entityShieldDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<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
}
private def maximumEntityHealthAndShields(history: List[InGameActivity]): (Int, Int) = {
var (health, shields) = (0, 0)
history.collect {
case SpawningActivity(target: SourceWithHealthEntry with SourceWithShieldsEntry, _, _) =>
health = math.max(target.Definition.MaxHealth, health)
shields = math.max(target.Definition.asInstanceOf[WithShields].MaxShields, shields)
case SpawningActivity(target: SourceWithHealthEntry, _, _) =>
health = math.max(target.Definition.MaxHealth, health)
case d: DamagingActivity =>
d.data.targetBefore match {
case target: SourceWithHealthEntry with SourceWithShieldsEntry =>
health = math.max(target.Definition.MaxHealth, health)
shields = math.max(target.Definition.asInstanceOf[WithShields].MaxShields, shields)
case target: SourceWithHealthEntry =>
health = math.max(target.health, health)
case _ => ()
}
}
(health, shields)
}
// private def initialEntityHealthAndShields(history: List[InGameActivity]): (Int, Int, PlanetSideEmpire.Value) = {
// history.collectFirst {
// case SpawningActivity(target: SourceWithHealthEntry with SourceWithShieldsEntry, _, _) =>
// (target.health, target.shields, target.Faction)
// case SpawningActivity(target: SourceWithHealthEntry, _, _) =>
// (target.health, 0, target.Faction)
// case d: DamagingActivity =>
// d.data.targetBefore match {
// case target: SourceWithHealthEntry with SourceWithShieldsEntry =>
// (target.health, target.shields, target.Faction)
// case target: SourceWithHealthEntry =>
// (target.health, 0, target.Faction)
// case _ =>
// (0, 0, PlanetSideEmpire.NEUTRAL)
// }
// }.getOrElse((0, 0, PlanetSideEmpire.NEUTRAL))
// }
private def armorDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
* key - character identifier,
* value - current damage contribution */
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.foreach {
case d: DamagingActivity if d.amount - d.health > 0 =>
inOrder = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, inOrder)
case r: RepairingActivity =>
inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
case _ => ()
}
inOrder
}
private[exp] def contributeWithDamagingActivity(
activity: DamagingActivity,
faction: PlanetSideEmpire.Value,
amount: Int,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
val data = activity.data
val playerOpt = data.adversarial.collect { case Adversarial(p: PlayerSource, _,_) => p }
contributeWithDamagingActivity(
playerOpt,
data.interaction.cause.attribution,
faction,
amount,
activity.time,
participants,
order
)
}
private[exp] def contributeWithDamagingActivity(
userOpt: Option[PlayerSource],
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
userOpt match {
case Some(user)
if user.Faction != faction =>
val whoId = user.CharId
val percentage = amount / user.Definition.MaxHealth.toFloat
val updatedEntry = participants.get(whoId) match {
case Some(mod) =>
//previous attacker, just add to entry
val firstWeapon = mod.weapons.head
val weapons = if (firstWeapon.weapon_id == wepid) {
firstWeapon.copy(
amount = firstWeapon.amount + amount,
shots = firstWeapon.shots + 1,
time = time,
contributions = firstWeapon.contributions + percentage
) +: mod.weapons.tail
} else {
WeaponStats(wepid, amount, 1, time, percentage) +: mod.weapons
}
mod.copy(
amount = mod.amount + amount,
weapons = weapons,
total = mod.total + amount,
shots = mod.shots + 1,
time = time
)
case None =>
//new attacker, new entry
ContributionStats(
user,
Seq(WeaponStats(wepid, amount, 1, time, percentage)),
amount,
amount,
1,
time
)
}
participants.put(whoId, updatedEntry)
order.indexWhere({ case (id, _) => id == whoId }) match {
case 0 =>
//ongoing attack by same player
val entry = order.head
(entry._1, entry._2 + amount) +: order.tail
case _ =>
//different player than immediate prior attacker
(whoId, amount) +: order
}
case _ =>
//damage that does not lead to contribution
order.headOption match {
case Some((id, dam)) =>
if (id == 0L) {
(0L, dam + amount) +: order.tail //pool
} else {
(0L, amount) +: order //new
}
case None =>
order
}
}
}
private def entityHealthDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<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

@ -42,14 +42,18 @@ object Support {
private[exp] def onlyOriginalAssistEntries(
first: mutable.LongMap[ContributionStatsOutput],
second: mutable.LongMap[ContributionStatsOutput]
second: mutable.LongMap[ContributionStatsOutput],
combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
defaultAdditiveOutputCombiner(multiplier = 0.05f)
): Iterable[ContributionStatsOutput] = {
onlyOriginalAssistEntriesIterable(first.values, second.values)
onlyOriginalAssistEntriesIterable(first.values, second.values, combiner)
}
private[exp] def onlyOriginalAssistEntriesIterable(
first: Iterable[ContributionStatsOutput],
second: Iterable[ContributionStatsOutput]
second: Iterable[ContributionStatsOutput],
combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
defaultAdditiveOutputCombiner(multiplier = 0.05f)
): Iterable[ContributionStatsOutput] = {
if (second.isEmpty) {
first
@ -59,12 +63,10 @@ object Support {
//overlap discriminated by percentage
val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
for {
h @ ContributionStatsOutput(hid, hwep, hkda) <- first
a @ ContributionStatsOutput(aid, awep, akda) <- second
(id, out) = if (hkda < akda)
(aid.CharId, a.copy(implements = (a.implements ++ hwep).distinct))
else
(hid.CharId, h.copy(implements = (h.implements ++ awep).distinct))
h @ ContributionStatsOutput(hid, _, _) <- first
a @ ContributionStatsOutput(aid, _, _) <- second
out = combiner(h, a)
id = out.player.CharId
if hid == aid && shared.put(id, out).isEmpty
} yield ()
val sharedKeys = shared.keys
@ -72,20 +74,39 @@ object Support {
}
}
private[exp] def collectHealthAssists(
victim: SourceEntry,
history: List[InGameActivity],
func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStatsOutput] = {
val healthAssists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
val qualifiedHealing = healthAssists.values.foldLeft(0)(_ + _.total)
val healthAssistsOutput = healthAssists.map { case (id, kda) =>
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / qualifiedHealing))
}
healthAssistsOutput.remove(victim.CharId)
healthAssistsOutput
private def defaultAdditiveOutputCombiner(
multiplier: Float
)
(
first: ContributionStatsOutput,
second: ContributionStatsOutput
): ContributionStatsOutput = {
if (first.percentage < second.percentage)
second.copy(
implements = (second.implements ++ first.implements).distinct,
percentage = first.percentage + second.implements.size * multiplier
)
else
first.copy(
implements = (first.implements ++ second.implements).distinct,
percentage = second.percentage + second.implements.size * multiplier
)
}
// private[exp] def collectKillAssists(
// victim: SourceEntry,
// history: List[InGameActivity],
// func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
// ): mutable.LongMap[ContributionStatsOutput] = {
// val healthAssists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
// val qualifiedHealing = healthAssists.values.foldLeft(0)(_ + _.total)
// val healthAssistsOutput = healthAssists.map { case (id, kda) =>
// (id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / qualifiedHealing))
// }
// healthAssistsOutput.remove(victim.CharId)
// healthAssistsOutput
// }
private[exp] def collectHealingSupportAssists(
target: SourceEntry,
time: JodaLocalDateTime,
@ -199,7 +220,7 @@ object Support {
}
}
private def findTimeApplicableActivities(
private[exp] def findTimeApplicableActivities(
activity: Seq[SupportActivity],
killLastTime: JodaLocalDateTime,
timeOffset: Int //s

View file

@ -40,10 +40,10 @@ class VitalityTest extends Specification {
player.LogActivity(result) //DamageResult, straight-up
player.LogActivity(DamageFromProjectile(result))
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
player.LogActivity(HealFromTerm(term, 10))
player.LogActivity(HealFromTerminal(term, 10))
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
player.LogActivity(RepairFromExoSuitChange(ExoSuitType.Standard, 10))
player.LogActivity(RepairFromTerm(term, 10))
player.LogActivity(RepairFromTerminal(term, 10))
player.LogActivity(ShieldCharge(10, Some(vSource)))
player.LogActivity(PlayerSuicide(PlayerSource(player)))
ok
@ -55,10 +55,10 @@ class VitalityTest extends Specification {
val term = AmenitySource(new Terminal(GlobalDefinitions.order_terminal) { GUID = PlanetSideGUID(1) })
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
player.LogActivity(HealFromTerm(term, 10))
player.LogActivity(HealFromTerminal(term, 10))
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
player.LogActivity(RepairFromExoSuitChange(ExoSuitType.Standard, 10))
player.LogActivity(RepairFromTerm(term, 10))
player.LogActivity(RepairFromTerminal(term, 10))
player.LogActivity(ShieldCharge(10, Some(vSource)))
player.LogActivity(PlayerSuicide(PlayerSource(player)))
player.History.size mustEqual 7
@ -67,10 +67,10 @@ class VitalityTest extends Specification {
player.History.size mustEqual 0
list.head.isInstanceOf[PlayerSuicide] mustEqual true
list(1).isInstanceOf[ShieldCharge] mustEqual true
list(2).isInstanceOf[RepairFromTerm] mustEqual true
list(2).isInstanceOf[RepairFromTerminal] mustEqual true
list(3).isInstanceOf[RepairFromExoSuitChange] mustEqual true
list(4).isInstanceOf[HealFromImplant] mustEqual true
list(5).isInstanceOf[HealFromTerm] mustEqual true
list(5).isInstanceOf[HealFromTerminal] mustEqual true
list(6).isInstanceOf[HealFromKit] mustEqual true
}
@ -92,10 +92,10 @@ class VitalityTest extends Specification {
player.LogActivity(DamageFromProjectile(result))
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
player.LogActivity(HealFromTerm(term, 10))
player.LogActivity(HealFromTerminal(term, 10))
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
player.LogActivity(RepairFromExoSuitChange(ExoSuitType.Standard, 10))
player.LogActivity(RepairFromTerm(term, 10))
player.LogActivity(RepairFromTerminal(term, 10))
player.LogActivity(ShieldCharge(10, Some(vSource)))
player.LogActivity(PlayerSuicide(PlayerSource(player)))