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.Cancellable
|
||||||
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
||||||
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
|
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import net.psforever.actors.zone.ZoneActor
|
import net.psforever.actors.zone.ZoneActor
|
||||||
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill}
|
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 org.joda.time.{LocalDateTime, Seconds}
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
|
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
|
||||||
import scala.util.{Failure, Success}
|
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.locker.LockerContainer
|
||||||
import net.psforever.objects.sourcing.{PlayerSource,SourceWithHealthEntry}
|
import net.psforever.objects.sourcing.{PlayerSource,SourceWithHealthEntry}
|
||||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
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.objectcreate.{BasicCharacterData, ObjectClass, RibbonBars}
|
||||||
import net.psforever.packet.game.{Friend => GameFriend, _}
|
import net.psforever.packet.game.{Friend => GameFriend, _}
|
||||||
import net.psforever.persistence
|
import net.psforever.persistence
|
||||||
|
|
@ -2949,10 +2953,10 @@ class AvatarActor(
|
||||||
case pr: ProjectileReason => pr.projectile.mounted_in.map { a => zone.GUID(a._1) }
|
case pr: ProjectileReason => pr.projectile.mounted_in.map { a => zone.GUID(a._1) }
|
||||||
case _ => None
|
case _ => None
|
||||||
}).collect {
|
}).collect {
|
||||||
case Some(obj: Vitality) =>
|
case Some(mount: PlanetSideGameObject with FactionAffinity with InGameHistory) =>
|
||||||
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, obj.History, kill, exp)
|
player.ContributionFrom(mount)
|
||||||
}
|
}
|
||||||
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.History, kill, exp)
|
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.HistoryAndContributions(), kill, exp)
|
||||||
case _: Assist =>
|
case _: Assist =>
|
||||||
()
|
()
|
||||||
case _: Death =>
|
case _: Death =>
|
||||||
|
|
|
||||||
|
|
@ -2614,10 +2614,11 @@ class ZoningOperations(
|
||||||
LoadZoneAsPlayer(newPlayer, zoneId)
|
LoadZoneAsPlayer(newPlayer, zoneId)
|
||||||
} else {
|
} else {
|
||||||
avatarActor ! AvatarActor.DeactivateActiveImplants()
|
avatarActor ! AvatarActor.DeactivateActiveImplants()
|
||||||
|
val betterSpawnPoint = physSpawnPoint.collect { case o: PlanetSideGameObject with FactionAffinity with InGameHistory => o }
|
||||||
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
|
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
|
||||||
case Some(vehicle: Vehicle) => // driver or passenger in vehicle using a warp gate, or a droppod
|
case Some(vehicle: Vehicle) => // driver or passenger in vehicle using a warp gate, or a droppod
|
||||||
InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, toSpawnPoint)
|
InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, betterSpawnPoint)
|
||||||
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
|
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
|
||||||
LoadZoneInVehicle(vehicle, pos, ori, zoneId)
|
LoadZoneInVehicle(vehicle, pos, ori, zoneId)
|
||||||
|
|
||||||
case _ if player.HasGUID => // player is deconstructing self or instant action
|
case _ if player.HasGUID => // player is deconstructing self or instant action
|
||||||
|
|
@ -2629,13 +2630,13 @@ class ZoningOperations(
|
||||||
)
|
)
|
||||||
player.Position = pos
|
player.Position = pos
|
||||||
player.Orientation = ori
|
player.Orientation = ori
|
||||||
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
|
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
|
||||||
LoadZoneAsPlayer(player, zoneId)
|
LoadZoneAsPlayer(player, zoneId)
|
||||||
|
|
||||||
case _ => //player is logging in
|
case _ => //player is logging in
|
||||||
player.Position = pos
|
player.Position = pos
|
||||||
player.Orientation = ori
|
player.Orientation = ori
|
||||||
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
|
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
|
||||||
LoadZoneAsPlayer(player, zoneId)
|
LoadZoneAsPlayer(player, zoneId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -942,8 +942,9 @@ object WorldSession {
|
||||||
result: Boolean
|
result: Boolean
|
||||||
): Unit = {
|
): Unit = {
|
||||||
if (result) {
|
if (result) {
|
||||||
player.Zone.GUID(guid).collect {
|
player.Zone.GUID(guid).collect { case term: Terminal =>
|
||||||
case term: Terminal => player.LogActivity(TerminalUsedActivity(AmenitySource(term), transaction))
|
player.LogActivity(TerminalUsedActivity(AmenitySource(term), transaction))
|
||||||
|
player.ContributionFrom(term)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
player.Zone.AvatarEvents ! AvatarServiceMessage(
|
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.environment.InteractWithEnvironment
|
||||||
import net.psforever.objects.serverobject.mount.MountableEntity
|
import net.psforever.objects.serverobject.mount.MountableEntity
|
||||||
import net.psforever.objects.vital.resistance.ResistanceProfile
|
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.damage.DamageProfile
|
||||||
import net.psforever.objects.vital.interaction.DamageInteraction
|
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||||
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
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 =
|
def UsingSpecial_=(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
|
||||||
usingSpecial(state)
|
usingSpecial(state)
|
||||||
|
|
||||||
|
//noinspection ScalaUnusedSymbol
|
||||||
private def DefaultUsingSpecial(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
|
private def DefaultUsingSpecial(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
|
||||||
SpecialExoSuitDefinition.Mode.Normal
|
SpecialExoSuitDefinition.Mode.Normal
|
||||||
|
|
||||||
private def UsingAnchorsOrOverdrive(
|
private def UsingAnchorsOrOverdrive(
|
||||||
state: SpecialExoSuitDefinition.Mode.Value
|
state: SpecialExoSuitDefinition.Mode.Value
|
||||||
): SpecialExoSuitDefinition.Mode.Value = {
|
): SpecialExoSuitDefinition.Mode.Value = {
|
||||||
import SpecialExoSuitDefinition.Mode._
|
import SpecialExoSuitDefinition.Mode._
|
||||||
val curr = UsingSpecial
|
val curr = UsingSpecial
|
||||||
val next = if (curr == Normal) {
|
val next = if (curr == Normal) {
|
||||||
|
|
@ -532,11 +533,14 @@ class Player(var avatar: Avatar)
|
||||||
|
|
||||||
def Carrying: Option[SpecialCarry] = carrying
|
def Carrying: Option[SpecialCarry] = carrying
|
||||||
|
|
||||||
|
//noinspection ScalaUnusedSymbol
|
||||||
def Carrying_=(item: SpecialCarry): Option[SpecialCarry] = {
|
def Carrying_=(item: SpecialCarry): Option[SpecialCarry] = {
|
||||||
Carrying
|
Carrying_=(Some(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//noinspection ScalaUnusedSymbol
|
||||||
def Carrying_=(item: Option[SpecialCarry]): Option[SpecialCarry] = {
|
def Carrying_=(item: Option[SpecialCarry]): Option[SpecialCarry] = {
|
||||||
|
carrying = item
|
||||||
Carrying
|
Carrying
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -555,6 +559,14 @@ class Player(var avatar: Avatar)
|
||||||
|
|
||||||
def canEqual(other: Any): Boolean = other.isInstanceOf[Player]
|
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 =
|
override def equals(other: Any): Boolean =
|
||||||
other match {
|
other match {
|
||||||
case that: Player =>
|
case that: Player =>
|
||||||
|
|
@ -620,6 +632,7 @@ object Player {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//noinspection ScalaUnusedSymbol
|
||||||
def neverRestrict(player: Player, slot: Int): Boolean = {
|
def neverRestrict(player: Player, slot: Int): Boolean = {
|
||||||
false
|
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.hackable.{GenericHackables, HackableBehavior}
|
||||||
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity}
|
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity}
|
||||||
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
|
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.objects.zones.ZoneAware
|
||||||
import net.psforever.packet.game.InventoryStateMessage
|
import net.psforever.packet.game.InventoryStateMessage
|
||||||
import net.psforever.services.Service
|
import net.psforever.services.Service
|
||||||
|
|
@ -291,7 +291,7 @@ object ProximityTerminalControl {
|
||||||
healAmount
|
healAmount
|
||||||
}
|
}
|
||||||
target.Health = health + finalHealthAmount
|
target.Health = health + finalHealthAmount
|
||||||
target.LogActivity(HealFromTerm(AmenitySource(terminal), finalHealthAmount))
|
target.LogActivity(HealFromTerminal(AmenitySource(terminal), finalHealthAmount))
|
||||||
updateFunc(target)
|
updateFunc(target)
|
||||||
target.Health == maxHealth
|
target.Health == maxHealth
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -338,7 +338,7 @@ object ProximityTerminalControl {
|
||||||
repairAmount
|
repairAmount
|
||||||
}
|
}
|
||||||
target.Armor = armor + finalRepairAmount
|
target.Armor = armor + finalRepairAmount
|
||||||
target.LogActivity(RepairFromTerm(AmenitySource(terminal), finalRepairAmount))
|
target.LogActivity(RepairFromTerminal(AmenitySource(terminal), finalRepairAmount))
|
||||||
val zone = target.Zone
|
val zone = target.Zone
|
||||||
zone.AvatarEvents ! AvatarServiceMessage(
|
zone.AvatarEvents ! AvatarServiceMessage(
|
||||||
zone.id,
|
zone.id,
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,15 @@ package net.psforever.objects.vital
|
||||||
import net.psforever.objects.PlanetSideGameObject
|
import net.psforever.objects.PlanetSideGameObject
|
||||||
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
|
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
|
||||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
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.environment.EnvironmentReason
|
||||||
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
|
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
|
||||||
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
|
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
|
||||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
import net.psforever.objects.vital.projectile.ProjectileReason
|
||||||
import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
|
import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
|
||||||
|
|
||||||
|
import scala.collection.mutable
|
||||||
|
|
||||||
/* root */
|
/* root */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -48,6 +50,11 @@ final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
|
||||||
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
|
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
|
||||||
extends GeneralActivity
|
extends GeneralActivity
|
||||||
|
|
||||||
|
final case class Contribution(unique: SourceEntry, entries: List[InGameActivity])
|
||||||
|
extends GeneralActivity {
|
||||||
|
override val time: Long = entries.maxBy(_.time).time
|
||||||
|
}
|
||||||
|
|
||||||
/* vitals history */
|
/* vitals history */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -77,9 +84,8 @@ trait DamagingActivity extends VitalsActivity {
|
||||||
|
|
||||||
def health: Int = {
|
def health: Int = {
|
||||||
(data.targetBefore, data.targetAfter) match {
|
(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 (pb: SourceWithHealthEntry, pa: SourceWithHealthEntry) => pb.health - pa.health
|
case _ => 0
|
||||||
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)
|
final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
|
||||||
extends HealingActivity with SupportActivityCausedByAnother
|
extends HealingActivity with SupportActivityCausedByAnother
|
||||||
|
|
||||||
final case class HealFromTerm(term: AmenitySource, amount: Int)
|
final case class HealFromTerminal(term: AmenitySource, amount: Int)
|
||||||
extends HealingActivity
|
extends HealingActivity
|
||||||
|
|
||||||
final case class HealFromImplant(implant: ImplantType, amount: Int)
|
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)
|
final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
|
||||||
extends RepairingActivity with SupportActivityCausedByAnother
|
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)
|
final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, vehicle: VehicleSource, amount: Int)
|
||||||
extends RepairingActivity
|
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] = {
|
def ClearHistory(): List[InGameActivity] = {
|
||||||
lastDamage = None
|
lastDamage = None
|
||||||
val out = history
|
val out = history
|
||||||
|
|
@ -233,14 +299,15 @@ object InGameHistory {
|
||||||
def SpawnReconstructionActivity(
|
def SpawnReconstructionActivity(
|
||||||
obj: PlanetSideGameObject with FactionAffinity with InGameHistory,
|
obj: PlanetSideGameObject with FactionAffinity with InGameHistory,
|
||||||
zoneNumber: Int,
|
zoneNumber: Int,
|
||||||
unit: Option[SourceEntry]
|
unit: Option[PlanetSideGameObject with FactionAffinity with InGameHistory]
|
||||||
): Unit = {
|
): Unit = {
|
||||||
|
val toUnitSource = unit.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }
|
||||||
val event: GeneralActivity = if (obj.History.nonEmpty || obj.History.headOption.exists {
|
val event: GeneralActivity = if (obj.History.nonEmpty || obj.History.headOption.exists {
|
||||||
_.isInstanceOf[SpawningActivity]
|
_.isInstanceOf[SpawningActivity]
|
||||||
}) {
|
}) {
|
||||||
ReconstructionActivity(ObjectSource(obj), zoneNumber, unit)
|
ReconstructionActivity(ObjectSource(obj), zoneNumber, toUnitSource)
|
||||||
} else {
|
} else {
|
||||||
SpawningActivity(ObjectSource(obj), zoneNumber, unit)
|
SpawningActivity(ObjectSource(obj), zoneNumber, toUnitSource)
|
||||||
}
|
}
|
||||||
if (obj.History.lastOption match {
|
if (obj.History.lastOption match {
|
||||||
case Some(evt: SpawningActivity) => evt != event
|
case Some(evt: SpawningActivity) => evt != event
|
||||||
|
|
@ -248,6 +315,7 @@ object InGameHistory {
|
||||||
case _ => true
|
case _ => true
|
||||||
}) {
|
}) {
|
||||||
obj.LogActivity(event)
|
obj.LogActivity(event)
|
||||||
|
unit.foreach { o => obj.ContributionFrom(o) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,15 @@
|
||||||
// Copyright (c) 2023 PSForever
|
// Copyright (c) 2023 PSForever
|
||||||
package net.psforever.objects.zones.exp
|
package net.psforever.objects.zones.exp
|
||||||
|
|
||||||
import akka.actor.Cancellable
|
|
||||||
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||||
import akka.actor.typed.{Behavior, SupervisorStrategy}
|
import akka.actor.typed.{Behavior, SupervisorStrategy}
|
||||||
import net.psforever.objects.{Default, PlanetSideGameObject}
|
import net.psforever.objects.PlanetSideGameObject
|
||||||
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
|
import net.psforever.objects.avatar.scoring.Kill
|
||||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry}
|
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||||
import net.psforever.objects.vital.{DamageFromExplodingEntity, InGameActivity, InGameHistory}
|
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
|
||||||
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
|
import net.psforever.objects.vital.interaction.DamageResult
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
|
||||||
|
|
||||||
import scala.collection.mutable
|
|
||||||
import scala.concurrent.duration._
|
|
||||||
|
|
||||||
object ExperienceCalculator {
|
object ExperienceCalculator {
|
||||||
def apply(zone: Zone): Behavior[Command] =
|
def apply(zone: Zone): Behavior[Command] =
|
||||||
|
|
@ -41,8 +36,6 @@ object ExperienceCalculator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private case object AssistDecay extends Command
|
|
||||||
|
|
||||||
def calculateExperience(
|
def calculateExperience(
|
||||||
victim: PlayerSource,
|
victim: PlayerSource,
|
||||||
history: Iterable[InGameActivity]
|
history: Iterable[InGameActivity]
|
||||||
|
|
@ -80,197 +73,16 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
|
||||||
|
|
||||||
import ExperienceCalculator._
|
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] = {
|
def onMessage(msg: Command): Behavior[Command] = {
|
||||||
msg match {
|
msg match {
|
||||||
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
|
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
|
||||||
rewardThisPlayerDeath(victim, lastDamage, history)
|
KillAssists.rewardThisPlayerDeath(victim, lastDamage, history, zone.AvatarEvents)
|
||||||
|
|
||||||
case RewardThisDeath(victim: SourceWithHealthEntry, lastDamage, history) =>
|
|
||||||
sortThisContributionForLater(victim, lastDamage, history)
|
|
||||||
|
|
||||||
case RewardOurSupporters(target: PlayerSource, history, kill, bep) =>
|
case RewardOurSupporters(target: PlayerSource, history, kill, bep) =>
|
||||||
rewardTheseSupporters(target, history.toList, kill, bep)
|
KillContributions.rewardTheseSupporters(target, history, kill, bep, zone.AvatarEvents)
|
||||||
|
|
||||||
case RewardOurSupporters(target: SourceWithHealthEntry, history, kill, _) =>
|
|
||||||
sortTheseSupportersForLater(target, history, kill)
|
|
||||||
|
|
||||||
case AssistDecay =>
|
|
||||||
performAssistDecay()
|
|
||||||
|
|
||||||
case _ => ()
|
case _ => ()
|
||||||
}
|
}
|
||||||
Behaviors.same
|
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(
|
private[exp] def onlyOriginalAssistEntries(
|
||||||
first: mutable.LongMap[ContributionStatsOutput],
|
first: mutable.LongMap[ContributionStatsOutput],
|
||||||
second: mutable.LongMap[ContributionStatsOutput]
|
second: mutable.LongMap[ContributionStatsOutput],
|
||||||
|
combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
|
||||||
|
defaultAdditiveOutputCombiner(multiplier = 0.05f)
|
||||||
): Iterable[ContributionStatsOutput] = {
|
): Iterable[ContributionStatsOutput] = {
|
||||||
onlyOriginalAssistEntriesIterable(first.values, second.values)
|
onlyOriginalAssistEntriesIterable(first.values, second.values, combiner)
|
||||||
}
|
}
|
||||||
|
|
||||||
private[exp] def onlyOriginalAssistEntriesIterable(
|
private[exp] def onlyOriginalAssistEntriesIterable(
|
||||||
first: Iterable[ContributionStatsOutput],
|
first: Iterable[ContributionStatsOutput],
|
||||||
second: Iterable[ContributionStatsOutput]
|
second: Iterable[ContributionStatsOutput],
|
||||||
|
combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
|
||||||
|
defaultAdditiveOutputCombiner(multiplier = 0.05f)
|
||||||
): Iterable[ContributionStatsOutput] = {
|
): Iterable[ContributionStatsOutput] = {
|
||||||
if (second.isEmpty) {
|
if (second.isEmpty) {
|
||||||
first
|
first
|
||||||
|
|
@ -59,12 +63,10 @@ object Support {
|
||||||
//overlap discriminated by percentage
|
//overlap discriminated by percentage
|
||||||
val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
|
val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
|
||||||
for {
|
for {
|
||||||
h @ ContributionStatsOutput(hid, hwep, hkda) <- first
|
h @ ContributionStatsOutput(hid, _, _) <- first
|
||||||
a @ ContributionStatsOutput(aid, awep, akda) <- second
|
a @ ContributionStatsOutput(aid, _, _) <- second
|
||||||
(id, out) = if (hkda < akda)
|
out = combiner(h, a)
|
||||||
(aid.CharId, a.copy(implements = (a.implements ++ hwep).distinct))
|
id = out.player.CharId
|
||||||
else
|
|
||||||
(hid.CharId, h.copy(implements = (h.implements ++ awep).distinct))
|
|
||||||
if hid == aid && shared.put(id, out).isEmpty
|
if hid == aid && shared.put(id, out).isEmpty
|
||||||
} yield ()
|
} yield ()
|
||||||
val sharedKeys = shared.keys
|
val sharedKeys = shared.keys
|
||||||
|
|
@ -72,20 +74,39 @@ object Support {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private[exp] def collectHealthAssists(
|
private def defaultAdditiveOutputCombiner(
|
||||||
victim: SourceEntry,
|
multiplier: Float
|
||||||
history: List[InGameActivity],
|
)
|
||||||
func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
|
(
|
||||||
): mutable.LongMap[ContributionStatsOutput] = {
|
first: ContributionStatsOutput,
|
||||||
val healthAssists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
|
second: ContributionStatsOutput
|
||||||
val qualifiedHealing = healthAssists.values.foldLeft(0)(_ + _.total)
|
): ContributionStatsOutput = {
|
||||||
val healthAssistsOutput = healthAssists.map { case (id, kda) =>
|
if (first.percentage < second.percentage)
|
||||||
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / qualifiedHealing))
|
second.copy(
|
||||||
}
|
implements = (second.implements ++ first.implements).distinct,
|
||||||
healthAssistsOutput.remove(victim.CharId)
|
percentage = first.percentage + second.implements.size * multiplier
|
||||||
healthAssistsOutput
|
)
|
||||||
|
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(
|
private[exp] def collectHealingSupportAssists(
|
||||||
target: SourceEntry,
|
target: SourceEntry,
|
||||||
time: JodaLocalDateTime,
|
time: JodaLocalDateTime,
|
||||||
|
|
@ -199,7 +220,7 @@ object Support {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def findTimeApplicableActivities(
|
private[exp] def findTimeApplicableActivities(
|
||||||
activity: Seq[SupportActivity],
|
activity: Seq[SupportActivity],
|
||||||
killLastTime: JodaLocalDateTime,
|
killLastTime: JodaLocalDateTime,
|
||||||
timeOffset: Int //s
|
timeOffset: Int //s
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,10 @@ class VitalityTest extends Specification {
|
||||||
player.LogActivity(result) //DamageResult, straight-up
|
player.LogActivity(result) //DamageResult, straight-up
|
||||||
player.LogActivity(DamageFromProjectile(result))
|
player.LogActivity(DamageFromProjectile(result))
|
||||||
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
|
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
|
||||||
player.LogActivity(HealFromTerm(term, 10))
|
player.LogActivity(HealFromTerminal(term, 10))
|
||||||
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
|
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
|
||||||
player.LogActivity(RepairFromExoSuitChange(ExoSuitType.Standard, 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(ShieldCharge(10, Some(vSource)))
|
||||||
player.LogActivity(PlayerSuicide(PlayerSource(player)))
|
player.LogActivity(PlayerSuicide(PlayerSource(player)))
|
||||||
ok
|
ok
|
||||||
|
|
@ -55,10 +55,10 @@ class VitalityTest extends Specification {
|
||||||
val term = AmenitySource(new Terminal(GlobalDefinitions.order_terminal) { GUID = PlanetSideGUID(1) })
|
val term = AmenitySource(new Terminal(GlobalDefinitions.order_terminal) { GUID = PlanetSideGUID(1) })
|
||||||
|
|
||||||
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
|
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
|
||||||
player.LogActivity(HealFromTerm(term, 10))
|
player.LogActivity(HealFromTerminal(term, 10))
|
||||||
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
|
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
|
||||||
player.LogActivity(RepairFromExoSuitChange(ExoSuitType.Standard, 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(ShieldCharge(10, Some(vSource)))
|
||||||
player.LogActivity(PlayerSuicide(PlayerSource(player)))
|
player.LogActivity(PlayerSuicide(PlayerSource(player)))
|
||||||
player.History.size mustEqual 7
|
player.History.size mustEqual 7
|
||||||
|
|
@ -67,10 +67,10 @@ class VitalityTest extends Specification {
|
||||||
player.History.size mustEqual 0
|
player.History.size mustEqual 0
|
||||||
list.head.isInstanceOf[PlayerSuicide] mustEqual true
|
list.head.isInstanceOf[PlayerSuicide] mustEqual true
|
||||||
list(1).isInstanceOf[ShieldCharge] 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(3).isInstanceOf[RepairFromExoSuitChange] mustEqual true
|
||||||
list(4).isInstanceOf[HealFromImplant] 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
|
list(6).isInstanceOf[HealFromKit] mustEqual true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,10 +92,10 @@ class VitalityTest extends Specification {
|
||||||
|
|
||||||
player.LogActivity(DamageFromProjectile(result))
|
player.LogActivity(DamageFromProjectile(result))
|
||||||
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
|
player.LogActivity(HealFromKit(GlobalDefinitions.medkit, 10))
|
||||||
player.LogActivity(HealFromTerm(term, 10))
|
player.LogActivity(HealFromTerminal(term, 10))
|
||||||
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
|
player.LogActivity(HealFromImplant(ImplantType.AdvancedRegen, 10))
|
||||||
player.LogActivity(RepairFromExoSuitChange(ExoSuitType.Standard, 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(ShieldCharge(10, Some(vSource)))
|
||||||
player.LogActivity(PlayerSuicide(PlayerSource(player)))
|
player.LogActivity(PlayerSuicide(PlayerSource(player)))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue