mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-07-16 08:55:18 +00:00
kill contributions should work; even if they don't, need to put this away for now
This commit is contained in:
parent
5afa1b2166
commit
45d2ea544c
13 changed files with 1274 additions and 650 deletions
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
310
src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
Normal file
310
src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue