mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-07-16 08:55:18 +00:00
importing controlled implementation changes from original exp-for-kda branch; code for contributions from prior activity, but will be adjusting to new contribution methods
This commit is contained in:
parent
6774edf5b6
commit
5afa1b2166
8 changed files with 735 additions and 398 deletions
|
|
@ -3,7 +3,7 @@ package net.psforever.objects
|
||||||
|
|
||||||
import akka.actor.{Actor, ActorContext, Props}
|
import akka.actor.{Actor, ActorContext, Props}
|
||||||
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployableCategory}
|
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployableCategory}
|
||||||
import net.psforever.objects.definition.DeployableDefinition
|
import net.psforever.objects.definition.{DeployableDefinition, WithShields}
|
||||||
import net.psforever.objects.definition.converter.ShieldGeneratorConverter
|
import net.psforever.objects.definition.converter.ShieldGeneratorConverter
|
||||||
import net.psforever.objects.equipment.{JammableBehavior, JammableUnit}
|
import net.psforever.objects.equipment.{JammableBehavior, JammableUnit}
|
||||||
import net.psforever.objects.serverobject.damage.Damageable.Target
|
import net.psforever.objects.serverobject.damage.Damageable.Target
|
||||||
|
|
@ -22,7 +22,8 @@ class ShieldGeneratorDeployable(cdef: ShieldGeneratorDefinition)
|
||||||
with Hackable
|
with Hackable
|
||||||
with JammableUnit
|
with JammableUnit
|
||||||
|
|
||||||
class ShieldGeneratorDefinition extends DeployableDefinition(240) {
|
class ShieldGeneratorDefinition extends DeployableDefinition(240)
|
||||||
|
with WithShields {
|
||||||
Packet = new ShieldGeneratorConverter
|
Packet = new ShieldGeneratorConverter
|
||||||
DeployCategory = DeployableCategory.ShieldGenerators
|
DeployCategory = DeployableCategory.ShieldGenerators
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,27 +26,8 @@ class VehicleDefinition(objectId: Int)
|
||||||
with VitalityDefinition
|
with VitalityDefinition
|
||||||
with NtuContainerDefinition
|
with NtuContainerDefinition
|
||||||
with ResistanceProfileMutators
|
with ResistanceProfileMutators
|
||||||
with DamageResistanceModel {
|
with DamageResistanceModel
|
||||||
/** ... */
|
with WithShields {
|
||||||
var shieldUiAttribute: Int = 68
|
|
||||||
/** how many points of shield the vehicle starts with (should default to 0 if unset through the accessor) */
|
|
||||||
private var defaultShields : Option[Int] = None
|
|
||||||
/** maximum vehicle shields (generally: 20% of health)
|
|
||||||
* for normal vehicles, offered through amp station facility benefits
|
|
||||||
* for BFR's, it charges naturally
|
|
||||||
**/
|
|
||||||
private var maxShields: Int = 0
|
|
||||||
/** the minimum amount of time that must elapse in between damage and shield charge activities (ms) */
|
|
||||||
private var shieldChargeDamageCooldown : Long = 5000L
|
|
||||||
/** the minimum amount of time that must elapse in between distinct shield charge activities (ms) */
|
|
||||||
private var shieldChargePeriodicCooldown : Long = 1000L
|
|
||||||
/** if the shield recharges on its own, this value will be non-`None` and indicate by how much */
|
|
||||||
private var autoShieldRecharge : Option[Int] = None
|
|
||||||
private var autoShieldRechargeSpecial : Option[Int] = None
|
|
||||||
/** shield drain is what happens to the shield under special conditions, e.g., bfr flight;
|
|
||||||
* the drain interval is 250ms which is convenient for us
|
|
||||||
* we can skip needing to define is explicitly */
|
|
||||||
private var shieldDrain : Option[Int] = None
|
|
||||||
private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]()
|
private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]()
|
||||||
private var deployment: Boolean = false
|
private var deployment: Boolean = false
|
||||||
private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap()
|
private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap()
|
||||||
|
|
@ -92,63 +73,6 @@ class VehicleDefinition(objectId: Int)
|
||||||
RepairRestoresAt = 1
|
RepairRestoresAt = 1
|
||||||
registerAs = "vehicles"
|
registerAs = "vehicles"
|
||||||
|
|
||||||
def DefaultShields: Int = defaultShields.getOrElse(0)
|
|
||||||
|
|
||||||
def DefaultShields_=(shield: Int): Int = DefaultShields_=(Some(shield))
|
|
||||||
|
|
||||||
def DefaultShields_=(shield: Option[Int]): Int = {
|
|
||||||
defaultShields = shield
|
|
||||||
DefaultShields
|
|
||||||
}
|
|
||||||
|
|
||||||
def MaxShields: Int = maxShields
|
|
||||||
|
|
||||||
def MaxShields_=(shields: Int): Int = {
|
|
||||||
maxShields = shields
|
|
||||||
MaxShields
|
|
||||||
}
|
|
||||||
|
|
||||||
def ShieldPeriodicDelay : Long = shieldChargePeriodicCooldown
|
|
||||||
|
|
||||||
def ShieldPeriodicDelay_=(cooldown: Long): Long = {
|
|
||||||
shieldChargePeriodicCooldown = cooldown
|
|
||||||
ShieldPeriodicDelay
|
|
||||||
}
|
|
||||||
|
|
||||||
def ShieldDamageDelay: Long = shieldChargeDamageCooldown
|
|
||||||
|
|
||||||
def ShieldDamageDelay_=(cooldown: Long): Long = {
|
|
||||||
shieldChargeDamageCooldown = cooldown
|
|
||||||
ShieldDamageDelay
|
|
||||||
}
|
|
||||||
|
|
||||||
def ShieldAutoRecharge: Option[Int] = autoShieldRecharge
|
|
||||||
|
|
||||||
def ShieldAutoRecharge_=(charge: Int): Option[Int] = ShieldAutoRecharge_=(Some(charge))
|
|
||||||
|
|
||||||
def ShieldAutoRecharge_=(charge: Option[Int]): Option[Int] = {
|
|
||||||
autoShieldRecharge = charge
|
|
||||||
ShieldAutoRecharge
|
|
||||||
}
|
|
||||||
|
|
||||||
def ShieldAutoRechargeSpecial: Option[Int] = autoShieldRechargeSpecial.orElse(ShieldAutoRecharge)
|
|
||||||
|
|
||||||
def ShieldAutoRechargeSpecial_=(charge: Int): Option[Int] = ShieldAutoRechargeSpecial_=(Some(charge))
|
|
||||||
|
|
||||||
def ShieldAutoRechargeSpecial_=(charge: Option[Int]): Option[Int] = {
|
|
||||||
autoShieldRechargeSpecial = charge
|
|
||||||
ShieldAutoRechargeSpecial
|
|
||||||
}
|
|
||||||
|
|
||||||
def ShieldDrain: Option[Int] = shieldDrain
|
|
||||||
|
|
||||||
def ShieldDrain_=(drain: Int): Option[Int] = ShieldDrain_=(Some(drain))
|
|
||||||
|
|
||||||
def ShieldDrain_=(drain: Option[Int]): Option[Int] = {
|
|
||||||
shieldDrain = drain
|
|
||||||
ShieldDrain
|
|
||||||
}
|
|
||||||
|
|
||||||
def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo
|
def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo
|
||||||
|
|
||||||
def CanBeOwned: Option[Boolean] = canBeOwned
|
def CanBeOwned: Option[Boolean] = canBeOwned
|
||||||
|
|
@ -300,6 +224,7 @@ class VehicleDefinition(objectId: Int)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//noinspection ScalaUnusedSymbol
|
||||||
def Uninitialize(obj: Vehicle, context: ActorContext): Unit = {
|
def Uninitialize(obj: Vehicle, context: ActorContext): Unit = {
|
||||||
obj.Actor ! akka.actor.PoisonPill
|
obj.Actor ! akka.actor.PoisonPill
|
||||||
obj.Actor = Default.Actor
|
obj.Actor = Default.Actor
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package net.psforever.objects.definition
|
||||||
|
|
||||||
|
trait WithShields {
|
||||||
|
/** ... */
|
||||||
|
var shieldUiAttribute: Int = 68
|
||||||
|
/** how many points of shield the vehicle starts with (should default to 0 if unset through the accessor) */
|
||||||
|
private var defaultShields : Option[Int] = None
|
||||||
|
/** maximum vehicle shields (generally: 20% of health)
|
||||||
|
* for normal vehicles, offered through amp station facility benefits
|
||||||
|
* for BFR's, it charges naturally
|
||||||
|
**/
|
||||||
|
private var maxShields: Int = 0
|
||||||
|
/** the minimum amount of time that must elapse in between damage and shield charge activities (ms) */
|
||||||
|
private var shieldChargeDamageCooldown : Long = 5000L
|
||||||
|
/** the minimum amount of time that must elapse in between distinct shield charge activities (ms) */
|
||||||
|
private var shieldChargePeriodicCooldown : Long = 1000L
|
||||||
|
/** if the shield recharges on its own, this value will be non-`None` and indicate by how much */
|
||||||
|
private var autoShieldRecharge : Option[Int] = None
|
||||||
|
private var autoShieldRechargeSpecial : Option[Int] = None
|
||||||
|
/** shield drain is what happens to the shield under special conditions, e.g., bfr flight;
|
||||||
|
* the drain interval is 250ms which is convenient for us
|
||||||
|
* we can skip needing to define is explicitly */
|
||||||
|
private var shieldDrain : Option[Int] = None
|
||||||
|
|
||||||
|
def DefaultShields: Int = defaultShields.getOrElse(0)
|
||||||
|
|
||||||
|
def DefaultShields_=(shield: Int): Int = DefaultShields_=(Some(shield))
|
||||||
|
|
||||||
|
def DefaultShields_=(shield: Option[Int]): Int = {
|
||||||
|
defaultShields = shield
|
||||||
|
DefaultShields
|
||||||
|
}
|
||||||
|
|
||||||
|
def MaxShields: Int = maxShields
|
||||||
|
|
||||||
|
def MaxShields_=(shields: Int): Int = {
|
||||||
|
maxShields = shields
|
||||||
|
MaxShields
|
||||||
|
}
|
||||||
|
|
||||||
|
def ShieldPeriodicDelay : Long = shieldChargePeriodicCooldown
|
||||||
|
|
||||||
|
def ShieldPeriodicDelay_=(cooldown: Long): Long = {
|
||||||
|
shieldChargePeriodicCooldown = cooldown
|
||||||
|
ShieldPeriodicDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
def ShieldDamageDelay: Long = shieldChargeDamageCooldown
|
||||||
|
|
||||||
|
def ShieldDamageDelay_=(cooldown: Long): Long = {
|
||||||
|
shieldChargeDamageCooldown = cooldown
|
||||||
|
ShieldDamageDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
def ShieldAutoRecharge: Option[Int] = autoShieldRecharge
|
||||||
|
|
||||||
|
def ShieldAutoRecharge_=(charge: Int): Option[Int] = ShieldAutoRecharge_=(Some(charge))
|
||||||
|
|
||||||
|
def ShieldAutoRecharge_=(charge: Option[Int]): Option[Int] = {
|
||||||
|
autoShieldRecharge = charge
|
||||||
|
ShieldAutoRecharge
|
||||||
|
}
|
||||||
|
|
||||||
|
def ShieldAutoRechargeSpecial: Option[Int] = autoShieldRechargeSpecial.orElse(ShieldAutoRecharge)
|
||||||
|
|
||||||
|
def ShieldAutoRechargeSpecial_=(charge: Int): Option[Int] = ShieldAutoRechargeSpecial_=(Some(charge))
|
||||||
|
|
||||||
|
def ShieldAutoRechargeSpecial_=(charge: Option[Int]): Option[Int] = {
|
||||||
|
autoShieldRechargeSpecial = charge
|
||||||
|
ShieldAutoRechargeSpecial
|
||||||
|
}
|
||||||
|
|
||||||
|
def ShieldDrain: Option[Int] = shieldDrain
|
||||||
|
|
||||||
|
def ShieldDrain_=(drain: Int): Option[Int] = ShieldDrain_=(Some(drain))
|
||||||
|
|
||||||
|
def ShieldDrain_=(drain: Option[Int]): Option[Int] = {
|
||||||
|
shieldDrain = drain
|
||||||
|
ShieldDrain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
package net.psforever.objects.serverobject.turret
|
package net.psforever.objects.serverobject.turret
|
||||||
|
|
||||||
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition}
|
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition, WithShields}
|
||||||
import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets}
|
import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets}
|
||||||
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
|
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
|
||||||
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
||||||
|
|
@ -14,7 +14,8 @@ import scala.collection.mutable
|
||||||
trait TurretDefinition
|
trait TurretDefinition
|
||||||
extends MountableWeaponsDefinition
|
extends MountableWeaponsDefinition
|
||||||
with ResistanceProfileMutators
|
with ResistanceProfileMutators
|
||||||
with DamageResistanceModel {
|
with DamageResistanceModel
|
||||||
|
with WithShields {
|
||||||
odef: ObjectDefinition =>
|
odef: ObjectDefinition =>
|
||||||
Turrets(odef.ObjectId) //let throw NoSuchElementException
|
Turrets(odef.ObjectId) //let throw NoSuchElementException
|
||||||
/* key - upgrade, value - weapon definition */
|
/* key - upgrade, value - weapon definition */
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
// 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.PlanetSideGameObject
|
import net.psforever.objects.{Default, PlanetSideGameObject}
|
||||||
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
|
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
|
||||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry}
|
||||||
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, InGameHistory, ReconstructionActivity, RepairFromExoSuitChange, RepairingActivity, RevivingActivity, SpawningActivity}
|
import net.psforever.objects.vital.{DamageFromExplodingEntity, InGameActivity, InGameHistory}
|
||||||
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
|
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
|
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
@ -41,27 +41,7 @@ object ExperienceCalculator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
|
private case object AssistDecay extends Command
|
||||||
val spawnIndex = history.lastIndexWhere {
|
|
||||||
case _: SpawningActivity => true
|
|
||||||
case _: RevivingActivity => true
|
|
||||||
case _ => false
|
|
||||||
}
|
|
||||||
val endIndex = history.lastIndexWhere {
|
|
||||||
case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
|
|
||||||
case _ => false
|
|
||||||
}
|
|
||||||
if (spawnIndex == -1 || endIndex == -1) {
|
|
||||||
Nil //throw VitalsHistoryException(history.head, "vitals history does not contain expected conditions")
|
|
||||||
// } else
|
|
||||||
// if (spawnIndex == -1) {
|
|
||||||
// Nil //throw VitalsHistoryException(history.head, "vitals history does not contain initial spawn conditions")
|
|
||||||
// } else if (endIndex == -1) {
|
|
||||||
// Nil //throw VitalsHistoryException(history.last, "vitals history does not contain end of life conditions")
|
|
||||||
} else {
|
|
||||||
history.slice(spawnIndex, endIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def calculateExperience(
|
def calculateExperience(
|
||||||
victim: PlayerSource,
|
victim: PlayerSource,
|
||||||
|
|
@ -71,12 +51,7 @@ object ExperienceCalculator {
|
||||||
case (Some(spawn), Some(death)) => death.time - spawn.time
|
case (Some(spawn), Some(death)) => death.time - spawn.time
|
||||||
case _ => 0L
|
case _ => 0L
|
||||||
}
|
}
|
||||||
val wasEverAMax = victim.ExoSuit == ExoSuitType.MAX || history.exists {
|
val wasEverAMax = Support.wasEverAMax(victim, history)
|
||||||
case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
|
|
||||||
case ReconstructionActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
|
|
||||||
case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
|
|
||||||
case _ => false
|
|
||||||
}
|
|
||||||
val base = if (wasEverAMax) { //shamed
|
val base = if (wasEverAMax) { //shamed
|
||||||
250L
|
250L
|
||||||
} else if (victim.Seated || victim.kills.nonEmpty) {
|
} else if (victim.Seated || victim.kills.nonEmpty) {
|
||||||
|
|
@ -94,38 +69,10 @@ object ExperienceCalculator {
|
||||||
base
|
base
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def onlyOriginalAssistEntries(
|
sealed case class InheritedAssistEntry(assists: Iterable[ContributionStatsOutput]) {
|
||||||
first: mutable.LongMap[ContributionStatsOutput],
|
val time: Long = System.currentTimeMillis()
|
||||||
second: mutable.LongMap[ContributionStatsOutput]
|
|
||||||
): Iterable[ContributionStatsOutput] = {
|
|
||||||
onlyOriginalAssistEntriesIterable(first.values, second.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
def onlyOriginalAssistEntriesIterable(
|
|
||||||
first: Iterable[ContributionStatsOutput],
|
|
||||||
second: Iterable[ContributionStatsOutput]
|
|
||||||
): Iterable[ContributionStatsOutput] = {
|
|
||||||
if (second.isEmpty) {
|
|
||||||
first
|
|
||||||
} else if (first.isEmpty) {
|
|
||||||
second
|
|
||||||
} else {
|
|
||||||
//overlap discriminated by percentage
|
|
||||||
val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
|
|
||||||
for {
|
|
||||||
h @ ContributionStatsOutput(hid, hwep, hkda) <- first
|
|
||||||
a @ ContributionStatsOutput(aid, awep, akda) <- second
|
|
||||||
(id, out) = if (hkda < akda)
|
|
||||||
(aid.CharId, a.copy(implements = (a.implements ++ hwep).distinct))
|
|
||||||
else
|
|
||||||
(hid.CharId, h.copy(implements = (h.implements ++ awep).distinct))
|
|
||||||
if hid == aid && shared.put(id, out).isEmpty
|
|
||||||
} yield ()
|
|
||||||
val sharedKeys = shared.keys
|
|
||||||
(first ++ second).filterNot { case ContributionStatsOutput(id, _, _) => sharedKeys.exists(_ == id.CharId) } ++ shared.values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone)
|
class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone)
|
||||||
|
|
@ -133,24 +80,43 @@ 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)
|
rewardThisPlayerDeath(victim, lastDamage, history)
|
||||||
case RewardOurSupporters(target, history, kill, bep) =>
|
|
||||||
|
case RewardThisDeath(victim: SourceWithHealthEntry, lastDamage, history) =>
|
||||||
|
sortThisContributionForLater(victim, lastDamage, history)
|
||||||
|
|
||||||
|
case RewardOurSupporters(target: PlayerSource, history, kill, bep) =>
|
||||||
rewardTheseSupporters(target, history.toList, kill, bep)
|
rewardTheseSupporters(target, history.toList, kill, bep)
|
||||||
|
|
||||||
|
case RewardOurSupporters(target: SourceWithHealthEntry, history, kill, _) =>
|
||||||
|
sortTheseSupportersForLater(target, history, kill)
|
||||||
|
|
||||||
|
case AssistDecay =>
|
||||||
|
performAssistDecay()
|
||||||
|
|
||||||
case _ => ()
|
case _ => ()
|
||||||
}
|
}
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
}
|
}
|
||||||
|
|
||||||
def rewardThisPlayerDeath(
|
private def rewardThisPlayerDeath(
|
||||||
victim: PlayerSource,
|
victim: PlayerSource,
|
||||||
lastDamage: Option[DamageResult],
|
lastDamage: Option[DamageResult],
|
||||||
history: Iterable[InGameActivity]
|
history: Iterable[InGameActivity]
|
||||||
): Unit = {
|
): Unit = {
|
||||||
val shortHistory = ExperienceCalculator.limitHistoryToThisLife(history.toList)
|
val shortHistory = Support.limitHistoryToThisLife(history.toList)
|
||||||
val everyone = determineKiller(lastDamage, shortHistory) match {
|
val everyone = KillDeathAssists.determineKiller(lastDamage, shortHistory) match {
|
||||||
case Some((result, killer: PlayerSource)) =>
|
case Some((result, killer: PlayerSource)) =>
|
||||||
val assists = collectAssistsForPlayer(victim, shortHistory, Some(killer))
|
val assists = collectAssistsForPlayer(victim, shortHistory, Some(killer))
|
||||||
val fullBep = KillDeathAssists.calculateExperience(killer, victim, shortHistory)
|
val fullBep = KillDeathAssists.calculateExperience(killer, victim, shortHistory)
|
||||||
|
|
@ -173,206 +139,29 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def determineKiller(lastDamageActivity: Option[DamageResult], history: List[InGameActivity]): Option[(DamageResult, SourceEntry)] = {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val compareTimeMillis = 10.seconds.toMillis
|
|
||||||
lastDamageActivity
|
|
||||||
.collect { case dam if now - dam.interaction.hitTime < compareTimeMillis => dam }
|
|
||||||
.flatMap { dam => Some(dam, dam.adversarial) }
|
|
||||||
.orElse {
|
|
||||||
history.collect { case damage: DamagingActivity
|
|
||||||
if now - damage.time < compareTimeMillis && damage.data.adversarial.nonEmpty =>
|
|
||||||
damage.data
|
|
||||||
}
|
|
||||||
.flatMap { dam => Some(dam, dam.adversarial) }.lastOption
|
|
||||||
}
|
|
||||||
.collect { case (dam, Some(adv)) => (dam, adv.attacker) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private[exp] def collectAssistsForPlayer(
|
private[exp] def collectAssistsForPlayer(
|
||||||
victim: PlayerSource,
|
victim: PlayerSource,
|
||||||
history: List[InGameActivity],
|
history: List[InGameActivity],
|
||||||
killerOpt: Option[PlayerSource]
|
killerOpt: Option[PlayerSource]
|
||||||
): Iterable[ContributionStatsOutput] = {
|
): Iterable[ContributionStatsOutput] = {
|
||||||
// val cardinalSin = victim.ExoSuit == ExoSuitType.MAX || history.exists {
|
val healthAssists = Support.collectHealthAssists(
|
||||||
// case SpawningActivity(p: PlayerSource,_,_) => p.ExoSuit == ExoSuitType.MAX
|
|
||||||
// case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
|
|
||||||
// case _ => false
|
|
||||||
// }
|
|
||||||
val initialHealth = history
|
|
||||||
.headOption
|
|
||||||
.collect { case SpawningActivity(p: PlayerSource,_,_) => p.health } match {
|
|
||||||
case Some(value) => value.toFloat
|
|
||||||
case _ => 100f
|
|
||||||
}
|
|
||||||
val healthAssists = collectHealthAssists(
|
|
||||||
victim,
|
victim,
|
||||||
history,
|
history,
|
||||||
initialHealth,
|
Support.allocateContributors(KillDeathAssists.healthDamageContributors)
|
||||||
allocateContributors(healthDamageContributors)
|
|
||||||
)
|
)
|
||||||
healthAssists.remove(0L)
|
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) }
|
killerOpt.map { killer => healthAssists.remove(killer.CharId) }
|
||||||
healthAssists.values
|
healthAssists.values
|
||||||
}
|
}
|
||||||
|
|
||||||
private def allocateContributors(
|
|
||||||
tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any
|
|
||||||
)
|
|
||||||
(
|
|
||||||
history: List[InGameActivity],
|
|
||||||
faction: PlanetSideEmpire.Value
|
|
||||||
): mutable.LongMap[ContributionStats] = {
|
|
||||||
/** players who have contributed to this death, and how much they have contributed<br>
|
|
||||||
* key - character identifier,
|
|
||||||
* value - (player, damage, total damage, number of shots) */
|
|
||||||
val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
|
|
||||||
tallyFunc(history, faction, participants)
|
|
||||||
participants
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private def healthDamageContributors(
|
|
||||||
history: List[InGameActivity],
|
|
||||||
faction: PlanetSideEmpire.Value,
|
|
||||||
participants: mutable.LongMap[ContributionStats]
|
|
||||||
): Seq[(Long, Int)] = {
|
|
||||||
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
|
|
||||||
* key - character identifier,
|
|
||||||
* value - current damage contribution */
|
|
||||||
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
|
|
||||||
history.tail.foreach {
|
|
||||||
case d: DamagingActivity if d.health > 0 =>
|
|
||||||
inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
|
|
||||||
case _: RepairingActivity => ()
|
|
||||||
case h: HealingActivity =>
|
|
||||||
inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder)
|
|
||||||
case _ => ()
|
|
||||||
}
|
|
||||||
inOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
private def collectHealthAssists(
|
|
||||||
victim: SourceEntry,
|
|
||||||
history: List[InGameActivity],
|
|
||||||
topHealth: Float,
|
|
||||||
func: (List[InGameActivity], PlanetSideEmpire.Value)=>mutable.LongMap[ContributionStats]
|
|
||||||
): mutable.LongMap[ContributionStatsOutput] = {
|
|
||||||
val healthAssists = func(history, victim.Faction)
|
|
||||||
.filterNot { case (_, kda) => kda.amount <= 0 }
|
|
||||||
.map { case (id, kda) =>
|
|
||||||
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / topHealth))
|
|
||||||
}
|
|
||||||
healthAssists.remove(victim.CharId)
|
|
||||||
healthAssists
|
|
||||||
}
|
|
||||||
|
|
||||||
private def contributeWithDamagingActivity(
|
|
||||||
activity: DamagingActivity,
|
|
||||||
faction: PlanetSideEmpire.Value,
|
|
||||||
amount: Int,
|
|
||||||
participants: mutable.LongMap[ContributionStats],
|
|
||||||
order: Seq[(Long, Int)]
|
|
||||||
): Seq[(Long, Int)] = {
|
|
||||||
activity.data.adversarial match {
|
|
||||||
case Some(Adversarial(attacker: PlayerSource, _, _))
|
|
||||||
if attacker.Faction != faction =>
|
|
||||||
val whoId = attacker.CharId
|
|
||||||
val wepid = activity.data.interaction.cause.attribution
|
|
||||||
val time = activity.time
|
|
||||||
val updatedEntry = participants.get(whoId) match {
|
|
||||||
case Some(mod) =>
|
|
||||||
//previous attacker, just add to entry
|
|
||||||
val firstWeapon = mod.weapons.head
|
|
||||||
val weapons = if (firstWeapon.weapon_id == wepid) {
|
|
||||||
firstWeapon.copy(amount = firstWeapon.amount + amount, shots = firstWeapon.shots + 1, time = time) +: mod.weapons.tail
|
|
||||||
} else {
|
|
||||||
WeaponStats(wepid, amount, 1, time) +: mod.weapons
|
|
||||||
}
|
|
||||||
mod.copy(
|
|
||||||
amount = mod.amount + amount,
|
|
||||||
weapons = weapons,
|
|
||||||
total = mod.total + amount,
|
|
||||||
shots = mod.shots + 1,
|
|
||||||
time = activity.time
|
|
||||||
)
|
|
||||||
case None =>
|
|
||||||
//new attacker, new entry
|
|
||||||
ContributionStats(
|
|
||||||
attacker,
|
|
||||||
Seq(WeaponStats(wepid, amount, 1, time)),
|
|
||||||
amount,
|
|
||||||
amount,
|
|
||||||
1,
|
|
||||||
time
|
|
||||||
)
|
|
||||||
}
|
|
||||||
participants.put(whoId, updatedEntry)
|
|
||||||
order.indexWhere({ case (id, _) => id == whoId }) match {
|
|
||||||
case 0 =>
|
|
||||||
//ongoing attack by same player
|
|
||||||
val entry = order.head
|
|
||||||
(entry._1, entry._2 + amount) +: order.tail
|
|
||||||
case _ =>
|
|
||||||
//different player than immediate prior attacker
|
|
||||||
(whoId, amount) +: order
|
|
||||||
}
|
|
||||||
case _ =>
|
|
||||||
//damage that does not lead to contribution
|
|
||||||
order.headOption match {
|
|
||||||
case Some((id, dam)) =>
|
|
||||||
if (id == 0L) {
|
|
||||||
(0L, dam + amount) +: order.tail //pool
|
|
||||||
} else {
|
|
||||||
(0L, amount) +: order //new
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
order
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def contributeWithRecoveryActivity(
|
|
||||||
amount: Int,
|
|
||||||
participants: mutable.LongMap[ContributionStats],
|
|
||||||
order: Seq[(Long, Int)]
|
|
||||||
): Seq[(Long, Int)] = {
|
|
||||||
var amt = amount
|
|
||||||
var count = 0
|
|
||||||
var newOrder: Seq[(Long, Int)] = Nil
|
|
||||||
order.takeWhile { entry =>
|
|
||||||
val (id, total) = entry
|
|
||||||
if (id > 0 && total > 0) {
|
|
||||||
val part = participants(id)
|
|
||||||
if (amount > total) {
|
|
||||||
//drop this entry
|
|
||||||
participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case
|
|
||||||
amt = amt - total
|
|
||||||
} else {
|
|
||||||
//edit around the inclusion of this entry
|
|
||||||
val newTotal = total - amt
|
|
||||||
val trimmedWeapons = {
|
|
||||||
var index = -1
|
|
||||||
var weaponSum = 0
|
|
||||||
val pweapons = part.weapons
|
|
||||||
while (weaponSum < amt) {
|
|
||||||
index += 1
|
|
||||||
weaponSum = weaponSum + pweapons(index).amount
|
|
||||||
}
|
|
||||||
pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size)
|
|
||||||
}
|
|
||||||
newOrder = (id, newTotal) +: newOrder
|
|
||||||
participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
|
|
||||||
amt = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
count += 1
|
|
||||||
amt > 0
|
|
||||||
}
|
|
||||||
newOrder ++ order.drop(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def rewardTheseSupporters(
|
private def rewardTheseSupporters(
|
||||||
target: SourceEntry,
|
target: SourceEntry,
|
||||||
history: List[InGameActivity],
|
history: List[InGameActivity],
|
||||||
|
|
@ -380,22 +169,108 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
|
||||||
bep: Long
|
bep: Long
|
||||||
): Unit = {
|
): Unit = {
|
||||||
val time = kill.time
|
val time = kill.time
|
||||||
val normalAssists = ExperienceCalculator.onlyOriginalAssistEntries(
|
|
||||||
Support.collectHealingSupportAssists(target, time, history),
|
|
||||||
Support.collectRepairingSupportAssists(target, time, history)
|
|
||||||
)
|
|
||||||
// retainedSupportAssists.get(target.unique) match {
|
|
||||||
// case Some(support) =>
|
|
||||||
// ExperienceCalculator.onlyOriginalAssistEntriesIterable(normalAssists, support.assists)
|
|
||||||
// case None =>
|
|
||||||
// normalAssists
|
|
||||||
// }
|
|
||||||
val events = zone.AvatarEvents
|
val events = zone.AvatarEvents
|
||||||
normalAssists.foreach { case ContributionStatsOutput(p, _, ratio) =>
|
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))
|
events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, (ratio * bep).toLong))
|
||||||
}
|
}
|
||||||
Support.collectTerminalSupportAssists(target, history).foreach { case ContributionStatsOutput(p, _, reward) =>
|
Support.collectTerminalSupportAssists(target, history).foreach { case ContributionStatsOutput(p, _, reward) =>
|
||||||
events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, reward.toLong))
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,35 @@
|
||||||
// Copyright (c) 2023 PSForever
|
// Copyright (c) 2023 PSForever
|
||||||
package net.psforever.objects.zones.exp
|
package net.psforever.objects.zones.exp
|
||||||
|
|
||||||
import net.psforever.objects.sourcing.PlayerSource
|
import net.psforever.objects.definition.{ExoSuitDefinition, WithShields}
|
||||||
import net.psforever.objects.vital.InGameActivity
|
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 {
|
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(
|
private[exp] def calculateExperience(
|
||||||
killer: PlayerSource,
|
killer: PlayerSource,
|
||||||
victim: PlayerSource,
|
victim: PlayerSource,
|
||||||
|
|
@ -37,4 +62,338 @@ object KillDeathAssists {
|
||||||
base
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ private case class WeaponStats(
|
||||||
weapon_id: Int,
|
weapon_id: Int,
|
||||||
amount: Int,
|
amount: Int,
|
||||||
shots: Int,
|
shots: Int,
|
||||||
time: Long
|
time: Long,
|
||||||
|
contributions: Float
|
||||||
)
|
)
|
||||||
|
|
||||||
private case class ContributionStats(
|
private case class ContributionStats(
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ import org.joda.time.{Instant, LocalDateTime => JodaLocalDateTime}
|
||||||
import net.psforever.objects.serverobject.hackable.Hackable
|
import net.psforever.objects.serverobject.hackable.Hackable
|
||||||
import net.psforever.objects.serverobject.terminals.Terminal
|
import net.psforever.objects.serverobject.terminals.Terminal
|
||||||
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry}
|
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry}
|
||||||
import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFromEquipment, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity}
|
import net.psforever.objects.vital.{DamagingActivity, HealFromEquipment, InGameActivity, ReconstructionActivity, RepairFromEquipment, RepairFromExoSuitChange, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity}
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.types.TransactionType
|
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
|
||||||
import net.psforever.zones.Zones
|
import net.psforever.zones.Zones
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
|
|
@ -18,14 +18,82 @@ import scala.collection.mutable
|
||||||
object Support {
|
object Support {
|
||||||
private type SupportActivity = InGameActivity with SupportActivityCausedByAnother
|
private type SupportActivity = InGameActivity with SupportActivityCausedByAnother
|
||||||
|
|
||||||
|
private[exp] def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
|
||||||
|
val spawnIndex = history.lastIndexWhere {
|
||||||
|
case _: SpawningActivity => true
|
||||||
|
case _: RevivingActivity => true
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
val endIndex = history.lastIndexWhere {
|
||||||
|
case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
if (spawnIndex == -1 || endIndex == -1) {
|
||||||
|
Nil //throw VitalsHistoryException(history.head, "vitals history does not contain expected conditions")
|
||||||
|
// } else
|
||||||
|
// if (spawnIndex == -1) {
|
||||||
|
// Nil //throw VitalsHistoryException(history.head, "vitals history does not contain initial spawn conditions")
|
||||||
|
// } else if (endIndex == -1) {
|
||||||
|
// Nil //throw VitalsHistoryException(history.last, "vitals history does not contain end of life conditions")
|
||||||
|
} else {
|
||||||
|
history.slice(spawnIndex, endIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private[exp] def onlyOriginalAssistEntries(
|
||||||
|
first: mutable.LongMap[ContributionStatsOutput],
|
||||||
|
second: mutable.LongMap[ContributionStatsOutput]
|
||||||
|
): Iterable[ContributionStatsOutput] = {
|
||||||
|
onlyOriginalAssistEntriesIterable(first.values, second.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
private[exp] def onlyOriginalAssistEntriesIterable(
|
||||||
|
first: Iterable[ContributionStatsOutput],
|
||||||
|
second: Iterable[ContributionStatsOutput]
|
||||||
|
): Iterable[ContributionStatsOutput] = {
|
||||||
|
if (second.isEmpty) {
|
||||||
|
first
|
||||||
|
} else if (first.isEmpty) {
|
||||||
|
second
|
||||||
|
} else {
|
||||||
|
//overlap discriminated by percentage
|
||||||
|
val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
|
||||||
|
for {
|
||||||
|
h @ ContributionStatsOutput(hid, hwep, hkda) <- first
|
||||||
|
a @ ContributionStatsOutput(aid, awep, akda) <- second
|
||||||
|
(id, out) = if (hkda < akda)
|
||||||
|
(aid.CharId, a.copy(implements = (a.implements ++ hwep).distinct))
|
||||||
|
else
|
||||||
|
(hid.CharId, h.copy(implements = (h.implements ++ awep).distinct))
|
||||||
|
if hid == aid && shared.put(id, out).isEmpty
|
||||||
|
} yield ()
|
||||||
|
val sharedKeys = shared.keys
|
||||||
|
(first ++ second).filterNot { case ContributionStatsOutput(id, _, _) => sharedKeys.exists(_ == id.CharId) } ++ shared.values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private[exp] def collectHealthAssists(
|
||||||
|
victim: SourceEntry,
|
||||||
|
history: List[InGameActivity],
|
||||||
|
func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
|
||||||
|
): mutable.LongMap[ContributionStatsOutput] = {
|
||||||
|
val healthAssists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
|
||||||
|
val qualifiedHealing = healthAssists.values.foldLeft(0)(_ + _.total)
|
||||||
|
val healthAssistsOutput = healthAssists.map { case (id, kda) =>
|
||||||
|
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / qualifiedHealing))
|
||||||
|
}
|
||||||
|
healthAssistsOutput.remove(victim.CharId)
|
||||||
|
healthAssistsOutput
|
||||||
|
}
|
||||||
|
|
||||||
private[exp] def collectHealingSupportAssists(
|
private[exp] def collectHealingSupportAssists(
|
||||||
target: SourceEntry,
|
target: SourceEntry,
|
||||||
time: JodaLocalDateTime,
|
time: JodaLocalDateTime,
|
||||||
history: Iterable[InGameActivity]
|
history: List[InGameActivity]
|
||||||
): mutable.LongMap[ContributionStatsOutput] = {
|
): mutable.LongMap[ContributionStatsOutput] = {
|
||||||
//normal heals
|
//normal heals
|
||||||
val healInfo = collectSupportContributions(
|
val healInfo = collectSupportContributions(
|
||||||
target, time, ExperienceCalculator.limitHistoryToThisLife(history.toList).collect { case heals: HealFromEquipment
|
target, time, history.collect { case heals: HealFromEquipment
|
||||||
if heals.amount > 0 && heals.user.unique != target.unique => (heals, heals.equipment_def.ObjectId)
|
if heals.amount > 0 && heals.user.unique != target.unique => (heals, heals.equipment_def.ObjectId)
|
||||||
}, mapContributionPointsByPercentage
|
}, mapContributionPointsByPercentage
|
||||||
)
|
)
|
||||||
|
|
@ -48,10 +116,10 @@ object Support {
|
||||||
private[exp] def collectRepairingSupportAssists(
|
private[exp] def collectRepairingSupportAssists(
|
||||||
target: SourceEntry,
|
target: SourceEntry,
|
||||||
time: JodaLocalDateTime,
|
time: JodaLocalDateTime,
|
||||||
history: Iterable[InGameActivity]
|
history: List[InGameActivity]
|
||||||
): mutable.LongMap[ContributionStatsOutput] = {
|
): mutable.LongMap[ContributionStatsOutput] = {
|
||||||
collectSupportContributions(
|
collectSupportContributions(
|
||||||
target, time, ExperienceCalculator.limitHistoryToThisLife(history.toList).collect { case repairs: RepairFromEquipment
|
target, time, history.collect { case repairs: RepairFromEquipment
|
||||||
if repairs.amount > 0 && repairs.user.unique != target.unique => (repairs, repairs.equipment_def.ObjectId)
|
if repairs.amount > 0 && repairs.user.unique != target.unique => (repairs, repairs.equipment_def.ObjectId)
|
||||||
}, mapContributionPointsByPercentage
|
}, mapContributionPointsByPercentage
|
||||||
)
|
)
|
||||||
|
|
@ -148,7 +216,7 @@ object Support {
|
||||||
.sortBy(_.time)(Ordering.Long.reverse)
|
.sortBy(_.time)(Ordering.Long.reverse)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def allocateSupportAssists(
|
private def combineTimeApplicableActivitiesForContribution(
|
||||||
longTerm: Seq[SupportActivity],
|
longTerm: Seq[SupportActivity],
|
||||||
shortTerm: Seq[SupportActivity],
|
shortTerm: Seq[SupportActivity],
|
||||||
defaultTool: Int
|
defaultTool: Int
|
||||||
|
|
@ -167,7 +235,8 @@ object Support {
|
||||||
time = math.max(entry.time, h.time)
|
time = math.max(entry.time, h.time)
|
||||||
))
|
))
|
||||||
case None =>
|
case None =>
|
||||||
contributions.put(charId, ContributionStats(user, Seq(WeaponStats(defaultTool, amount, 1, h.time)), amount, amount, 1, h.time))
|
//the contribution percentage allocated will always be 1.0f and should be overwritten later
|
||||||
|
contributions.put(charId, ContributionStats(user, Seq(WeaponStats(defaultTool, amount, 1, h.time, 1f)), amount, amount, 1, h.time))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contributions
|
contributions
|
||||||
|
|
@ -183,38 +252,54 @@ object Support {
|
||||||
//TODO this is not correct, but it will do for now
|
//TODO this is not correct, but it will do for now
|
||||||
if (tools.isEmpty) {
|
if (tools.isEmpty) {
|
||||||
mutable.LongMap[ContributionStatsOutput]()
|
mutable.LongMap[ContributionStatsOutput]()
|
||||||
} else if (tools.distinct.size == 1) {
|
} else {
|
||||||
collectSupportAssists(target, time, activity, tools.head, contributionPointsMap)
|
val distinctTools = tools.distinct
|
||||||
|
if (distinctTools.size == 1) {
|
||||||
|
compileTimedSupportContributions(target, time, activity, distinctTools.head, contributionPointsMap)
|
||||||
} else {
|
} else {
|
||||||
var output = Iterable[ContributionStatsOutput]()
|
var output = Iterable[ContributionStatsOutput]()
|
||||||
tools.groupBy(identity).keys.foreach { implement =>
|
distinctTools.foreach { implement =>
|
||||||
output = ExperienceCalculator.onlyOriginalAssistEntriesIterable(
|
output = onlyOriginalAssistEntriesIterable(
|
||||||
output,
|
output,
|
||||||
collectSupportAssists(target, time, activity, implement, contributionPointsMap).values
|
compileTimedSupportContributions(target, time, activity, implement, contributionPointsMap).values
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
mutable.LongMap[ContributionStatsOutput]().addAll(output.map { entry => (entry.player.CharId, entry) })
|
mutable.LongMap[ContributionStatsOutput]().addAll(output.map { entry => (entry.player.CharId, entry) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private def collectSupportAssists(
|
private def compileTimedSupportContributions(
|
||||||
target: SourceEntry,
|
target: SourceEntry,
|
||||||
killLastTime: JodaLocalDateTime,
|
timeLimit: JodaLocalDateTime,
|
||||||
activity: Seq[InGameActivity],
|
activity: Seq[InGameActivity],
|
||||||
defaultImplement: Int,
|
defaultImplement: Int,
|
||||||
contributionPointsMap: (Float,Iterable[SupportActivity])=>(Long,ContributionStats)=>(Long,ContributionStatsOutput)
|
contributionPointsMap: (Float,Iterable[SupportActivity])=>(Long,ContributionStats)=>(Long,ContributionStatsOutput)
|
||||||
): mutable.LongMap[ContributionStatsOutput] = {
|
): mutable.LongMap[ContributionStatsOutput] = {
|
||||||
val activityByAnother = activity.collect {
|
val activityByAnother = activity.collect { case theActivity: SupportActivity => theActivity }
|
||||||
case activity: SupportActivity => activity
|
val longTermActivity = findTimeApplicableActivities(activityByAnother, timeLimit, timeOffset = 600)
|
||||||
}
|
val shortTermActivity = findTimeApplicableActivities(activityByAnother, timeLimit, timeOffset = 300)
|
||||||
val longTermActivity = findTimeApplicableActivities(activityByAnother, killLastTime, timeOffset = 600)
|
val contributions = combineTimeApplicableActivitiesForContribution(longTermActivity, shortTermActivity, defaultImplement)
|
||||||
val shortTermActivity = findTimeApplicableActivities(activityByAnother, killLastTime, timeOffset = 300)
|
|
||||||
val contributions = allocateSupportAssists(longTermActivity, shortTermActivity, defaultImplement)
|
|
||||||
contributions.remove(target.CharId)
|
contributions.remove(target.CharId)
|
||||||
val mapFunc = contributionPointsMap(contributions.values.foldLeft(0)(_ + _.total).toFloat, shortTermActivity)
|
val mapFunc = contributionPointsMap(contributions.values.foldLeft(0)(_ + _.total).toFloat, shortTermActivity)
|
||||||
contributions.map { case (a, b) => mapFunc(a, b) }
|
contributions.map { case (a, b) => mapFunc(a, b) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private[exp] def allocateContributors(
|
||||||
|
tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any
|
||||||
|
)
|
||||||
|
(
|
||||||
|
history: List[InGameActivity],
|
||||||
|
faction: PlanetSideEmpire.Value
|
||||||
|
): mutable.LongMap[ContributionStats] = {
|
||||||
|
/** players who have contributed to this death, and how much they have contributed<br>
|
||||||
|
* key - character identifier,
|
||||||
|
* value - (player, damage, total damage, number of shots) */
|
||||||
|
val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
|
||||||
|
tallyFunc(history, faction, participants)
|
||||||
|
participants
|
||||||
|
}
|
||||||
|
|
||||||
private def mapContributionPointsByPercentage(
|
private def mapContributionPointsByPercentage(
|
||||||
total: Float,
|
total: Float,
|
||||||
compareList: Iterable[SupportActivity]
|
compareList: Iterable[SupportActivity]
|
||||||
|
|
@ -257,4 +342,13 @@ object Support {
|
||||||
): (Long, ContributionStatsOutput) = {
|
): (Long, ContributionStatsOutput) = {
|
||||||
(charId, ContributionStatsOutput(contribution.player, contribution.weapons.map { _.weapon_id }, compareList.size.toFloat))
|
(charId, ContributionStatsOutput(contribution.player, contribution.weapons.map { _.weapon_id }, compareList.size.toFloat))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private[exp] def wasEverAMax(player: PlayerSource, history: Iterable[InGameActivity]): Boolean = {
|
||||||
|
player.ExoSuit == ExoSuitType.MAX || history.exists {
|
||||||
|
case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
|
||||||
|
case ReconstructionActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
|
||||||
|
case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue