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 net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployableCategory}
|
||||
import net.psforever.objects.definition.DeployableDefinition
|
||||
import net.psforever.objects.definition.{DeployableDefinition, WithShields}
|
||||
import net.psforever.objects.definition.converter.ShieldGeneratorConverter
|
||||
import net.psforever.objects.equipment.{JammableBehavior, JammableUnit}
|
||||
import net.psforever.objects.serverobject.damage.Damageable.Target
|
||||
|
|
@ -22,7 +22,8 @@ class ShieldGeneratorDeployable(cdef: ShieldGeneratorDefinition)
|
|||
with Hackable
|
||||
with JammableUnit
|
||||
|
||||
class ShieldGeneratorDefinition extends DeployableDefinition(240) {
|
||||
class ShieldGeneratorDefinition extends DeployableDefinition(240)
|
||||
with WithShields {
|
||||
Packet = new ShieldGeneratorConverter
|
||||
DeployCategory = DeployableCategory.ShieldGenerators
|
||||
|
||||
|
|
|
|||
|
|
@ -26,27 +26,8 @@ class VehicleDefinition(objectId: Int)
|
|||
with VitalityDefinition
|
||||
with NtuContainerDefinition
|
||||
with ResistanceProfileMutators
|
||||
with DamageResistanceModel {
|
||||
/** ... */
|
||||
var shieldUiAttribute: Int = 68
|
||||
/** how many points of shield the vehicle starts with (should default to 0 if unset through the accessor) */
|
||||
private var defaultShields : Option[Int] = None
|
||||
/** maximum vehicle shields (generally: 20% of health)
|
||||
* for normal vehicles, offered through amp station facility benefits
|
||||
* for BFR's, it charges naturally
|
||||
**/
|
||||
private var maxShields: Int = 0
|
||||
/** the minimum amount of time that must elapse in between damage and shield charge activities (ms) */
|
||||
private var shieldChargeDamageCooldown : Long = 5000L
|
||||
/** the minimum amount of time that must elapse in between distinct shield charge activities (ms) */
|
||||
private var shieldChargePeriodicCooldown : Long = 1000L
|
||||
/** if the shield recharges on its own, this value will be non-`None` and indicate by how much */
|
||||
private var autoShieldRecharge : Option[Int] = None
|
||||
private var autoShieldRechargeSpecial : Option[Int] = None
|
||||
/** shield drain is what happens to the shield under special conditions, e.g., bfr flight;
|
||||
* the drain interval is 250ms which is convenient for us
|
||||
* we can skip needing to define is explicitly */
|
||||
private var shieldDrain : Option[Int] = None
|
||||
with DamageResistanceModel
|
||||
with WithShields {
|
||||
private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]()
|
||||
private var deployment: Boolean = false
|
||||
private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap()
|
||||
|
|
@ -92,63 +73,6 @@ class VehicleDefinition(objectId: Int)
|
|||
RepairRestoresAt = 1
|
||||
registerAs = "vehicles"
|
||||
|
||||
def DefaultShields: Int = defaultShields.getOrElse(0)
|
||||
|
||||
def DefaultShields_=(shield: Int): Int = DefaultShields_=(Some(shield))
|
||||
|
||||
def DefaultShields_=(shield: Option[Int]): Int = {
|
||||
defaultShields = shield
|
||||
DefaultShields
|
||||
}
|
||||
|
||||
def MaxShields: Int = maxShields
|
||||
|
||||
def MaxShields_=(shields: Int): Int = {
|
||||
maxShields = shields
|
||||
MaxShields
|
||||
}
|
||||
|
||||
def ShieldPeriodicDelay : Long = shieldChargePeriodicCooldown
|
||||
|
||||
def ShieldPeriodicDelay_=(cooldown: Long): Long = {
|
||||
shieldChargePeriodicCooldown = cooldown
|
||||
ShieldPeriodicDelay
|
||||
}
|
||||
|
||||
def ShieldDamageDelay: Long = shieldChargeDamageCooldown
|
||||
|
||||
def ShieldDamageDelay_=(cooldown: Long): Long = {
|
||||
shieldChargeDamageCooldown = cooldown
|
||||
ShieldDamageDelay
|
||||
}
|
||||
|
||||
def ShieldAutoRecharge: Option[Int] = autoShieldRecharge
|
||||
|
||||
def ShieldAutoRecharge_=(charge: Int): Option[Int] = ShieldAutoRecharge_=(Some(charge))
|
||||
|
||||
def ShieldAutoRecharge_=(charge: Option[Int]): Option[Int] = {
|
||||
autoShieldRecharge = charge
|
||||
ShieldAutoRecharge
|
||||
}
|
||||
|
||||
def ShieldAutoRechargeSpecial: Option[Int] = autoShieldRechargeSpecial.orElse(ShieldAutoRecharge)
|
||||
|
||||
def ShieldAutoRechargeSpecial_=(charge: Int): Option[Int] = ShieldAutoRechargeSpecial_=(Some(charge))
|
||||
|
||||
def ShieldAutoRechargeSpecial_=(charge: Option[Int]): Option[Int] = {
|
||||
autoShieldRechargeSpecial = charge
|
||||
ShieldAutoRechargeSpecial
|
||||
}
|
||||
|
||||
def ShieldDrain: Option[Int] = shieldDrain
|
||||
|
||||
def ShieldDrain_=(drain: Int): Option[Int] = ShieldDrain_=(Some(drain))
|
||||
|
||||
def ShieldDrain_=(drain: Option[Int]): Option[Int] = {
|
||||
shieldDrain = drain
|
||||
ShieldDrain
|
||||
}
|
||||
|
||||
def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo
|
||||
|
||||
def CanBeOwned: Option[Boolean] = canBeOwned
|
||||
|
|
@ -300,6 +224,7 @@ class VehicleDefinition(objectId: Int)
|
|||
)
|
||||
}
|
||||
|
||||
//noinspection ScalaUnusedSymbol
|
||||
def Uninitialize(obj: Vehicle, context: ActorContext): Unit = {
|
||||
obj.Actor ! akka.actor.PoisonPill
|
||||
obj.Actor = Default.Actor
|
||||
|
|
|
|||
|
|
@ -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
|
||||
package net.psforever.objects.serverobject.turret
|
||||
|
||||
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition}
|
||||
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition, WithShields}
|
||||
import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets}
|
||||
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
|
||||
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
||||
|
|
@ -14,7 +14,8 @@ import scala.collection.mutable
|
|||
trait TurretDefinition
|
||||
extends MountableWeaponsDefinition
|
||||
with ResistanceProfileMutators
|
||||
with DamageResistanceModel {
|
||||
with DamageResistanceModel
|
||||
with WithShields {
|
||||
odef: ObjectDefinition =>
|
||||
Turrets(odef.ObjectId) //let throw NoSuchElementException
|
||||
/* key - upgrade, value - weapon definition */
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp
|
||||
|
||||
import akka.actor.Cancellable
|
||||
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||
import akka.actor.typed.{Behavior, SupervisorStrategy}
|
||||
import net.psforever.objects.PlanetSideGameObject
|
||||
import net.psforever.objects.{Default, PlanetSideGameObject}
|
||||
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, InGameHistory, ReconstructionActivity, RepairFromExoSuitChange, RepairingActivity, RevivingActivity, SpawningActivity}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry}
|
||||
import net.psforever.objects.vital.{DamageFromExplodingEntity, InGameActivity, InGameHistory}
|
||||
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
|
|
@ -41,27 +41,7 @@ object ExperienceCalculator {
|
|||
}
|
||||
}
|
||||
|
||||
def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
|
||||
val spawnIndex = history.lastIndexWhere {
|
||||
case _: SpawningActivity => true
|
||||
case _: RevivingActivity => true
|
||||
case _ => false
|
||||
}
|
||||
val endIndex = history.lastIndexWhere {
|
||||
case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
|
||||
case _ => false
|
||||
}
|
||||
if (spawnIndex == -1 || endIndex == -1) {
|
||||
Nil //throw VitalsHistoryException(history.head, "vitals history does not contain expected conditions")
|
||||
// } else
|
||||
// if (spawnIndex == -1) {
|
||||
// Nil //throw VitalsHistoryException(history.head, "vitals history does not contain initial spawn conditions")
|
||||
// } else if (endIndex == -1) {
|
||||
// Nil //throw VitalsHistoryException(history.last, "vitals history does not contain end of life conditions")
|
||||
} else {
|
||||
history.slice(spawnIndex, endIndex)
|
||||
}
|
||||
}
|
||||
private case object AssistDecay extends Command
|
||||
|
||||
def calculateExperience(
|
||||
victim: PlayerSource,
|
||||
|
|
@ -71,12 +51,7 @@ object ExperienceCalculator {
|
|||
case (Some(spawn), Some(death)) => death.time - spawn.time
|
||||
case _ => 0L
|
||||
}
|
||||
val wasEverAMax = victim.ExoSuit == ExoSuitType.MAX || history.exists {
|
||||
case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
|
||||
case ReconstructionActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
|
||||
case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
|
||||
case _ => false
|
||||
}
|
||||
val wasEverAMax = Support.wasEverAMax(victim, history)
|
||||
val base = if (wasEverAMax) { //shamed
|
||||
250L
|
||||
} else if (victim.Seated || victim.kills.nonEmpty) {
|
||||
|
|
@ -94,38 +69,10 @@ object ExperienceCalculator {
|
|||
base
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def onlyOriginalAssistEntries(
|
||||
first: mutable.LongMap[ContributionStatsOutput],
|
||||
second: mutable.LongMap[ContributionStatsOutput]
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
onlyOriginalAssistEntriesIterable(first.values, second.values)
|
||||
}
|
||||
|
||||
def onlyOriginalAssistEntriesIterable(
|
||||
first: Iterable[ContributionStatsOutput],
|
||||
second: Iterable[ContributionStatsOutput]
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
if (second.isEmpty) {
|
||||
first
|
||||
} else if (first.isEmpty) {
|
||||
second
|
||||
} else {
|
||||
//overlap discriminated by percentage
|
||||
val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
|
||||
for {
|
||||
h @ ContributionStatsOutput(hid, hwep, hkda) <- first
|
||||
a @ ContributionStatsOutput(aid, awep, akda) <- second
|
||||
(id, out) = if (hkda < akda)
|
||||
(aid.CharId, a.copy(implements = (a.implements ++ hwep).distinct))
|
||||
else
|
||||
(hid.CharId, h.copy(implements = (h.implements ++ awep).distinct))
|
||||
if hid == aid && shared.put(id, out).isEmpty
|
||||
} yield ()
|
||||
val sharedKeys = shared.keys
|
||||
(first ++ second).filterNot { case ContributionStatsOutput(id, _, _) => sharedKeys.exists(_ == id.CharId) } ++ shared.values
|
||||
}
|
||||
}
|
||||
sealed case class InheritedAssistEntry(assists: Iterable[ContributionStatsOutput]) {
|
||||
val time: Long = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone)
|
||||
|
|
@ -133,24 +80,43 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
|
|||
|
||||
import ExperienceCalculator._
|
||||
|
||||
private val retainedKillAssists: mutable.HashMap[SourceUniqueness, InheritedAssistEntry] =
|
||||
mutable.HashMap[SourceUniqueness, InheritedAssistEntry]()
|
||||
|
||||
private val retainedSupportAssists: mutable.HashMap[SourceUniqueness, InheritedAssistEntry] =
|
||||
mutable.HashMap[SourceUniqueness, InheritedAssistEntry]()
|
||||
|
||||
private var inheritedAssistsDecayTimer: Cancellable = Default.Cancellable
|
||||
|
||||
def onMessage(msg: Command): Behavior[Command] = {
|
||||
msg match {
|
||||
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
|
||||
rewardThisPlayerDeath(victim, lastDamage, history)
|
||||
case RewardOurSupporters(target, history, kill, bep) =>
|
||||
|
||||
case RewardThisDeath(victim: SourceWithHealthEntry, lastDamage, history) =>
|
||||
sortThisContributionForLater(victim, lastDamage, history)
|
||||
|
||||
case RewardOurSupporters(target: PlayerSource, history, kill, bep) =>
|
||||
rewardTheseSupporters(target, history.toList, kill, bep)
|
||||
|
||||
case RewardOurSupporters(target: SourceWithHealthEntry, history, kill, _) =>
|
||||
sortTheseSupportersForLater(target, history, kill)
|
||||
|
||||
case AssistDecay =>
|
||||
performAssistDecay()
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
def rewardThisPlayerDeath(
|
||||
victim: PlayerSource,
|
||||
lastDamage: Option[DamageResult],
|
||||
history: Iterable[InGameActivity]
|
||||
): Unit = {
|
||||
val shortHistory = ExperienceCalculator.limitHistoryToThisLife(history.toList)
|
||||
val everyone = determineKiller(lastDamage, shortHistory) match {
|
||||
private def rewardThisPlayerDeath(
|
||||
victim: PlayerSource,
|
||||
lastDamage: Option[DamageResult],
|
||||
history: Iterable[InGameActivity]
|
||||
): Unit = {
|
||||
val shortHistory = Support.limitHistoryToThisLife(history.toList)
|
||||
val everyone = KillDeathAssists.determineKiller(lastDamage, shortHistory) match {
|
||||
case Some((result, killer: PlayerSource)) =>
|
||||
val assists = collectAssistsForPlayer(victim, shortHistory, Some(killer))
|
||||
val fullBep = KillDeathAssists.calculateExperience(killer, victim, shortHistory)
|
||||
|
|
@ -173,229 +139,138 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
|
|||
}
|
||||
}
|
||||
|
||||
def determineKiller(lastDamageActivity: Option[DamageResult], history: List[InGameActivity]): Option[(DamageResult, SourceEntry)] = {
|
||||
val now = System.currentTimeMillis()
|
||||
val compareTimeMillis = 10.seconds.toMillis
|
||||
lastDamageActivity
|
||||
.collect { case dam if now - dam.interaction.hitTime < compareTimeMillis => dam }
|
||||
.flatMap { dam => Some(dam, dam.adversarial) }
|
||||
.orElse {
|
||||
history.collect { case damage: DamagingActivity
|
||||
if now - damage.time < compareTimeMillis && damage.data.adversarial.nonEmpty =>
|
||||
damage.data
|
||||
}
|
||||
.flatMap { dam => Some(dam, dam.adversarial) }.lastOption
|
||||
}
|
||||
.collect { case (dam, Some(adv)) => (dam, adv.attacker) }
|
||||
}
|
||||
|
||||
private[exp] def collectAssistsForPlayer(
|
||||
victim: PlayerSource,
|
||||
history: List[InGameActivity],
|
||||
killerOpt: Option[PlayerSource]
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
// val cardinalSin = victim.ExoSuit == ExoSuitType.MAX || history.exists {
|
||||
// case SpawningActivity(p: PlayerSource,_,_) => p.ExoSuit == ExoSuitType.MAX
|
||||
// case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
|
||||
// case _ => false
|
||||
// }
|
||||
val initialHealth = history
|
||||
.headOption
|
||||
.collect { case SpawningActivity(p: PlayerSource,_,_) => p.health } match {
|
||||
case Some(value) => value.toFloat
|
||||
case _ => 100f
|
||||
}
|
||||
val healthAssists = collectHealthAssists(
|
||||
val healthAssists = Support.collectHealthAssists(
|
||||
victim,
|
||||
history,
|
||||
initialHealth,
|
||||
allocateContributors(healthDamageContributors)
|
||||
Support.allocateContributors(KillDeathAssists.healthDamageContributors)
|
||||
)
|
||||
healthAssists.remove(0L)
|
||||
// if (armor > 0) {
|
||||
// val armorAssists = collectMaxArmorAssists(victim, history, armor.toFloat)
|
||||
// }
|
||||
// if (Support.wasEverAMax(victim, history)) {
|
||||
//
|
||||
// } else {
|
||||
//
|
||||
// }
|
||||
killerOpt.map { killer => healthAssists.remove(killer.CharId) }
|
||||
healthAssists.values
|
||||
}
|
||||
|
||||
private def allocateContributors(
|
||||
tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any
|
||||
)
|
||||
(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
/** players who have contributed to this death, and how much they have contributed<br>
|
||||
* key - character identifier,
|
||||
* value - (player, damage, total damage, number of shots) */
|
||||
val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
|
||||
tallyFunc(history, faction, participants)
|
||||
participants
|
||||
}
|
||||
|
||||
|
||||
|
||||
private def healthDamageContributors(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
participants: mutable.LongMap[ContributionStats]
|
||||
): Seq[(Long, Int)] = {
|
||||
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
|
||||
* key - character identifier,
|
||||
* value - current damage contribution */
|
||||
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
|
||||
history.tail.foreach {
|
||||
case d: DamagingActivity if d.health > 0 =>
|
||||
inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
|
||||
case _: RepairingActivity => ()
|
||||
case h: HealingActivity =>
|
||||
inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder)
|
||||
case _ => ()
|
||||
}
|
||||
inOrder
|
||||
}
|
||||
|
||||
private def collectHealthAssists(
|
||||
victim: SourceEntry,
|
||||
history: List[InGameActivity],
|
||||
topHealth: Float,
|
||||
func: (List[InGameActivity], PlanetSideEmpire.Value)=>mutable.LongMap[ContributionStats]
|
||||
): mutable.LongMap[ContributionStatsOutput] = {
|
||||
val healthAssists = func(history, victim.Faction)
|
||||
.filterNot { case (_, kda) => kda.amount <= 0 }
|
||||
.map { case (id, kda) =>
|
||||
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / topHealth))
|
||||
}
|
||||
healthAssists.remove(victim.CharId)
|
||||
healthAssists
|
||||
}
|
||||
|
||||
private def contributeWithDamagingActivity(
|
||||
activity: DamagingActivity,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
amount: Int,
|
||||
participants: mutable.LongMap[ContributionStats],
|
||||
order: Seq[(Long, Int)]
|
||||
): Seq[(Long, Int)] = {
|
||||
activity.data.adversarial match {
|
||||
case Some(Adversarial(attacker: PlayerSource, _, _))
|
||||
if attacker.Faction != faction =>
|
||||
val whoId = attacker.CharId
|
||||
val wepid = activity.data.interaction.cause.attribution
|
||||
val time = activity.time
|
||||
val updatedEntry = participants.get(whoId) match {
|
||||
case Some(mod) =>
|
||||
//previous attacker, just add to entry
|
||||
val firstWeapon = mod.weapons.head
|
||||
val weapons = if (firstWeapon.weapon_id == wepid) {
|
||||
firstWeapon.copy(amount = firstWeapon.amount + amount, shots = firstWeapon.shots + 1, time = time) +: mod.weapons.tail
|
||||
} else {
|
||||
WeaponStats(wepid, amount, 1, time) +: mod.weapons
|
||||
}
|
||||
mod.copy(
|
||||
amount = mod.amount + amount,
|
||||
weapons = weapons,
|
||||
total = mod.total + amount,
|
||||
shots = mod.shots + 1,
|
||||
time = activity.time
|
||||
)
|
||||
case None =>
|
||||
//new attacker, new entry
|
||||
ContributionStats(
|
||||
attacker,
|
||||
Seq(WeaponStats(wepid, amount, 1, time)),
|
||||
amount,
|
||||
amount,
|
||||
1,
|
||||
time
|
||||
)
|
||||
}
|
||||
participants.put(whoId, updatedEntry)
|
||||
order.indexWhere({ case (id, _) => id == whoId }) match {
|
||||
case 0 =>
|
||||
//ongoing attack by same player
|
||||
val entry = order.head
|
||||
(entry._1, entry._2 + amount) +: order.tail
|
||||
case _ =>
|
||||
//different player than immediate prior attacker
|
||||
(whoId, amount) +: order
|
||||
}
|
||||
case _ =>
|
||||
//damage that does not lead to contribution
|
||||
order.headOption match {
|
||||
case Some((id, dam)) =>
|
||||
if (id == 0L) {
|
||||
(0L, dam + amount) +: order.tail //pool
|
||||
} else {
|
||||
(0L, amount) +: order //new
|
||||
}
|
||||
case None =>
|
||||
order
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def contributeWithRecoveryActivity(
|
||||
amount: Int,
|
||||
participants: mutable.LongMap[ContributionStats],
|
||||
order: Seq[(Long, Int)]
|
||||
): Seq[(Long, Int)] = {
|
||||
var amt = amount
|
||||
var count = 0
|
||||
var newOrder: Seq[(Long, Int)] = Nil
|
||||
order.takeWhile { entry =>
|
||||
val (id, total) = entry
|
||||
if (id > 0 && total > 0) {
|
||||
val part = participants(id)
|
||||
if (amount > total) {
|
||||
//drop this entry
|
||||
participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case
|
||||
amt = amt - total
|
||||
} else {
|
||||
//edit around the inclusion of this entry
|
||||
val newTotal = total - amt
|
||||
val trimmedWeapons = {
|
||||
var index = -1
|
||||
var weaponSum = 0
|
||||
val pweapons = part.weapons
|
||||
while (weaponSum < amt) {
|
||||
index += 1
|
||||
weaponSum = weaponSum + pweapons(index).amount
|
||||
}
|
||||
pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size)
|
||||
}
|
||||
newOrder = (id, newTotal) +: newOrder
|
||||
participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
|
||||
amt = 0
|
||||
}
|
||||
}
|
||||
count += 1
|
||||
amt > 0
|
||||
}
|
||||
newOrder ++ order.drop(count)
|
||||
}
|
||||
|
||||
private def rewardTheseSupporters(
|
||||
target: SourceEntry,
|
||||
history: List[InGameActivity],
|
||||
kill: Kill,
|
||||
bep: Long
|
||||
): Unit = {
|
||||
target: SourceEntry,
|
||||
history: List[InGameActivity],
|
||||
kill: Kill,
|
||||
bep: Long
|
||||
): Unit = {
|
||||
val time = kill.time
|
||||
val normalAssists = ExperienceCalculator.onlyOriginalAssistEntries(
|
||||
Support.collectHealingSupportAssists(target, time, history),
|
||||
Support.collectRepairingSupportAssists(target, time, history)
|
||||
)
|
||||
// retainedSupportAssists.get(target.unique) match {
|
||||
// case Some(support) =>
|
||||
// ExperienceCalculator.onlyOriginalAssistEntriesIterable(normalAssists, support.assists)
|
||||
// case None =>
|
||||
// normalAssists
|
||||
// }
|
||||
val events = zone.AvatarEvents
|
||||
normalAssists.foreach { case ContributionStatsOutput(p, _, ratio) =>
|
||||
events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, (ratio * bep).toLong))
|
||||
}
|
||||
val trimmedHistory = Support.limitHistoryToThisLife(history)
|
||||
val normalAssists = Support.onlyOriginalAssistEntries(
|
||||
Support.collectHealingSupportAssists(target, time, trimmedHistory),
|
||||
Support.collectRepairingSupportAssists(target, time, trimmedHistory)
|
||||
)
|
||||
(retainedSupportAssists.get(target.unique) match {
|
||||
case Some(support) =>
|
||||
Support.onlyOriginalAssistEntriesIterable(normalAssists, support.assists)
|
||||
case None =>
|
||||
normalAssists
|
||||
})
|
||||
.foreach { case ContributionStatsOutput(p, _, ratio) =>
|
||||
events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, (ratio * bep).toLong))
|
||||
}
|
||||
Support.collectTerminalSupportAssists(target, history).foreach { case ContributionStatsOutput(p, _, reward) =>
|
||||
events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, reward.toLong))
|
||||
}
|
||||
}
|
||||
|
||||
/* */
|
||||
|
||||
private def sortTheseSupportersForLater(
|
||||
target: SourceEntry,
|
||||
history: Iterable[InGameActivity],
|
||||
kill: Kill
|
||||
): Unit = {
|
||||
val time = kill.time
|
||||
val trimmedHistory = Support.limitHistoryToThisLife(history.toList)
|
||||
val assists = Support.collectRepairingSupportAssists(target, time, trimmedHistory).values
|
||||
retainAssistsAndScheduleBlanking(target.unique, assists)
|
||||
}
|
||||
|
||||
private def sortThisContributionForLater(
|
||||
target: SourceWithHealthEntry,
|
||||
lastDamage: Option[DamageResult],
|
||||
history: Iterable[InGameActivity]
|
||||
): Unit = {
|
||||
val shortHistory = Support.limitHistoryToThisLife(history.toList)
|
||||
val assists = KillDeathAssists.determineKiller(lastDamage, shortHistory) match {
|
||||
case Some((damage, killer: PlayerSource)) =>
|
||||
ContributionStatsOutput(killer, Seq(damage.interaction.cause.attribution), 1f) +:
|
||||
collectRetainableAssistsForEntity(target, shortHistory, Some(killer)).toSeq
|
||||
case _ =>
|
||||
collectRetainableAssistsForEntity(target, shortHistory, None)
|
||||
}
|
||||
retainAssistsAndScheduleBlanking(target.unique, assists)
|
||||
}
|
||||
|
||||
private def collectRetainableAssistsForEntity(
|
||||
entity: SourceWithHealthEntry,
|
||||
history: List[InGameActivity],
|
||||
killerOpt: Option[PlayerSource]
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
val assists = KillDeathAssists.collectAssistsForEntity(entity, history, killerOpt)
|
||||
Support.onlyOriginalAssistEntriesIterable(
|
||||
assists,
|
||||
recoverRetainedKillAssists(assists, history)
|
||||
)
|
||||
}
|
||||
|
||||
private def recoverRetainedKillAssists(
|
||||
assists: Iterable[ContributionStatsOutput],
|
||||
history: Iterable[InGameActivity]
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
history.collect { case ex: DamageFromExplodingEntity =>
|
||||
ex.data
|
||||
.adversarial
|
||||
.flatMap {
|
||||
case Adversarial(attacker: PlayerSource, _, _) =>
|
||||
Some((
|
||||
assists.find { case ContributionStatsOutput(p, _, _) => p == attacker },
|
||||
retainedKillAssists.get(ex.data.targetAfter.unique)
|
||||
))
|
||||
}
|
||||
.map {
|
||||
case (Some(_), Some(retained)) => retained.assists
|
||||
}
|
||||
}.flatten.flatten
|
||||
}
|
||||
|
||||
private[this] def retainAssistsAndScheduleBlanking(
|
||||
target: SourceUniqueness,
|
||||
assists: Iterable[ContributionStatsOutput]
|
||||
): Boolean = {
|
||||
val retime = retainedKillAssists.isEmpty && inheritedAssistsDecayTimer.isCancelled
|
||||
retainedKillAssists.put(target, InheritedAssistEntry(assists))
|
||||
if (retime) {
|
||||
inheritedAssistsDecayTimer = context.scheduleOnce(3.minutes, context.self, AssistDecay)
|
||||
}
|
||||
retime
|
||||
}
|
||||
|
||||
private[this] def performAssistDecay(): Unit = {
|
||||
val curr = System.currentTimeMillis()
|
||||
val dur = 5.minutes.toMillis
|
||||
retainedKillAssists.filterInPlace { case (_, entry) => curr - entry.time < dur }
|
||||
retainedSupportAssists.filterInPlace { case (_, entry) => curr - entry.time < dur }
|
||||
if (retainedKillAssists.nonEmpty || retainedSupportAssists.nonEmpty) {
|
||||
inheritedAssistsDecayTimer = context.scheduleOnce(3.minutes, context.self, AssistDecay)
|
||||
} else {
|
||||
inheritedAssistsDecayTimer = Default.Cancellable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,40 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp
|
||||
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.vital.InGameActivity
|
||||
import net.psforever.objects.definition.{ExoSuitDefinition, WithShields}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceWithHealthEntry, SourceWithShieldsEntry}
|
||||
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
|
||||
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairFromExoSuitChange, RepairingActivity, ShieldCharge, SpawningActivity}
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object KillDeathAssists {
|
||||
private [exp] def determineKiller(
|
||||
lastDamageActivity: Option[DamageResult],
|
||||
history: List[InGameActivity]
|
||||
): Option[(DamageResult, SourceEntry)] = {
|
||||
val now = System.currentTimeMillis()
|
||||
val compareTimeMillis = 10.seconds.toMillis
|
||||
lastDamageActivity
|
||||
.collect { case dam if now - dam.interaction.hitTime < compareTimeMillis => dam }
|
||||
.flatMap { dam => Some(dam, dam.adversarial) }
|
||||
.orElse {
|
||||
history.collect { case damage: DamagingActivity
|
||||
if now - damage.time < compareTimeMillis && damage.data.adversarial.nonEmpty =>
|
||||
damage.data
|
||||
}
|
||||
.flatMap { dam => Some(dam, dam.adversarial) }.lastOption
|
||||
}
|
||||
.collect { case (dam, Some(adv)) => (dam, adv.attacker) }
|
||||
}
|
||||
|
||||
private[exp] def calculateExperience(
|
||||
killer: PlayerSource,
|
||||
victim: PlayerSource,
|
||||
history: Iterable[InGameActivity]
|
||||
): Long = {
|
||||
killer: PlayerSource,
|
||||
victim: PlayerSource,
|
||||
history: Iterable[InGameActivity]
|
||||
): Long = {
|
||||
//base value (the kill experience before modifiers)
|
||||
val base = ExperienceCalculator.calculateExperience(victim, history)
|
||||
if (base > 1) {
|
||||
|
|
@ -37,4 +62,338 @@ object KillDeathAssists {
|
|||
base
|
||||
}
|
||||
}
|
||||
|
||||
private[exp] def collectAssistsForEntity(
|
||||
target: SourceWithHealthEntry,
|
||||
history: List[InGameActivity],
|
||||
killerOpt: Option[PlayerSource]
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
val (_, maxShields) = maximumEntityHealthAndShields(history)
|
||||
val healthAssists = Support.collectHealthAssists(
|
||||
target,
|
||||
history,
|
||||
Support.allocateContributors(entityHealthDamageContributors)
|
||||
)
|
||||
if (maxShields > 0) {
|
||||
val shieldAssists = collectShieldAssists(target.asInstanceOf[SourceWithShieldsEntry], history)
|
||||
killerOpt.collect {
|
||||
case killer =>
|
||||
healthAssists.remove(killer.CharId)
|
||||
shieldAssists.remove(killer.CharId)
|
||||
}
|
||||
Support.onlyOriginalAssistEntries(healthAssists, shieldAssists)
|
||||
} else {
|
||||
killerOpt.collect {
|
||||
case killer =>
|
||||
healthAssists.remove(killer.CharId)
|
||||
}
|
||||
healthAssists.values
|
||||
}
|
||||
}
|
||||
|
||||
private[exp] def collectMaxArmorAssists(
|
||||
victim: PlayerSource,
|
||||
history: List[InGameActivity],
|
||||
fullArmor: Float
|
||||
): mutable.LongMap[ContributionStatsOutput] = {
|
||||
val initialArmorAssists = Support.allocateContributors(maxArmorDamageContributors)(history, victim.Faction)
|
||||
val allArmorDamage = initialArmorAssists.values.foldLeft(0f)(_ + _.total)
|
||||
val flatComparativeRate = if (allArmorDamage > fullArmor * 1.35f) {
|
||||
0.4f //this max has been damaged and repaired a lot
|
||||
} else {
|
||||
0.65f
|
||||
}
|
||||
val armorAssists = initialArmorAssists.map { case (id, kda) =>
|
||||
val averageRateOfAllWeapons = kda.weapons.map { stat => stat.contributions / stat.shots }.sum / kda.weapons.size
|
||||
val finalRate = math.max(flatComparativeRate, (flatComparativeRate + averageRateOfAllWeapons) * 0.5f)
|
||||
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, finalRate))
|
||||
}
|
||||
armorAssists.remove(victim.CharId)
|
||||
armorAssists
|
||||
}
|
||||
|
||||
private def maxArmorDamageContributors(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
participants: mutable.LongMap[ContributionStats]
|
||||
): Seq[(Long, Int)] = {
|
||||
var isMax = history.head match {
|
||||
case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
|
||||
case _ => false
|
||||
}
|
||||
/** damage as it is measured in order (with repair-countered damage eliminated)<br>
|
||||
* key - character identifier,
|
||||
* value - current damage contribution */
|
||||
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
|
||||
history.tail.collect {
|
||||
case d: DamagingActivity if isMax || d.amount - d.health == d.amount =>
|
||||
inOrder = contributeWithDamagingActivity(d, faction, d.amount, participants, inOrder)
|
||||
|
||||
case RepairFromExoSuitChange(suit, _) =>
|
||||
isMax = suit == ExoSuitType.MAX
|
||||
|
||||
case r: RepairingActivity =>
|
||||
inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
|
||||
}
|
||||
inOrder
|
||||
}
|
||||
|
||||
private[exp] def collectShieldAssists(
|
||||
victim: SourceWithShieldsEntry,
|
||||
history: List[InGameActivity],
|
||||
): mutable.LongMap[ContributionStatsOutput] = {
|
||||
val initialShieldAssists = Support.allocateContributors(entityShieldDamageContributors)(history, victim.Faction)
|
||||
.filterNot { case (_, kda) => kda.amount <= 0 }
|
||||
val fullFloatShield = initialShieldAssists.values.foldLeft(0)(_ + _.total).toFloat
|
||||
val shieldAssists = initialShieldAssists.map { case (id, kda) =>
|
||||
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, math.min(kda.total / fullFloatShield, 0.4f)))
|
||||
}
|
||||
shieldAssists.remove(victim.CharId)
|
||||
shieldAssists
|
||||
}
|
||||
|
||||
private def entityShieldDamageContributors(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
participants: mutable.LongMap[ContributionStats]
|
||||
): Seq[(Long, Int)] = {
|
||||
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
|
||||
* key - character identifier,
|
||||
* value - current damage contribution */
|
||||
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
|
||||
history.tail.foreach {
|
||||
case d: DamagingActivity if d.amount - d.health > 0 =>
|
||||
inOrder = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, inOrder)
|
||||
case v: ShieldCharge if v.amount > 0 =>
|
||||
inOrder = contributeWithRecoveryActivity(v.amount, participants, inOrder)
|
||||
case _ => ()
|
||||
}
|
||||
inOrder
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the maximum amount of health and armor that a player character had
|
||||
* before their event and time of death.<br>
|
||||
* <br>
|
||||
* For health value, the number is determined by starting with their spawn health and tracking heals and damage.
|
||||
* For armor value, the number is determined in the same way at first,
|
||||
* by starting with their spawn armor and tracking repairs and damage.
|
||||
* If and only after being bulk set by an exosuit change event once, however,
|
||||
* the only things that matter are exosuit change events.
|
||||
* @param history na
|
||||
* @return a pair of the aforementioned integers, the maximum health value and the maximum armor value
|
||||
*/
|
||||
private[exp] def maximumPlayerHealthAndArmor(history: List[InGameActivity]): (Int, Int) = {
|
||||
var (health, armor, faction) = (0, 0, PlanetSideEmpire.NEUTRAL)
|
||||
history.collect {
|
||||
case SpawningActivity(target: PlayerSource, _, _) =>
|
||||
health = math.max(target.Definition.MaxHealth, health)
|
||||
armor = math.max(ExoSuitDefinition.Select(target.ExoSuit, target.Faction).MaxArmor, armor)
|
||||
faction = target.Faction
|
||||
case d: DamagingActivity =>
|
||||
val target = d.data.targetBefore.asInstanceOf[PlayerSource]
|
||||
health = math.max(target.Definition.MaxHealth, health)
|
||||
armor = math.max(ExoSuitDefinition.Select(target.ExoSuit, target.Faction).MaxArmor, armor)
|
||||
}
|
||||
(health, armor)
|
||||
}
|
||||
|
||||
// private def initialPlayerHealthAndArmor(history: List[InGameActivity]): (Int, Int, PlanetSideEmpire.Value) = {
|
||||
// history.collectFirst {
|
||||
// case SpawningActivity(target: PlayerSource, _, _) =>
|
||||
// (target.health, target.armor, target.Faction)
|
||||
// case d: DamagingActivity =>
|
||||
// val target = d.data.targetBefore.asInstanceOf[PlayerSource]
|
||||
// (target.health, target.armor, target.Faction)
|
||||
// }.getOrElse((0, 0, PlanetSideEmpire.NEUTRAL))
|
||||
// }
|
||||
|
||||
private def maximumEntityHealthAndShields(history: List[InGameActivity]): (Int, Int) = {
|
||||
var (health, shields) = (0, 0)
|
||||
history.collect {
|
||||
case SpawningActivity(target: SourceWithHealthEntry with SourceWithShieldsEntry, _, _) =>
|
||||
health = math.max(target.Definition.MaxHealth, health)
|
||||
shields = math.max(target.Definition.asInstanceOf[WithShields].MaxShields, shields)
|
||||
case SpawningActivity(target: SourceWithHealthEntry, _, _) =>
|
||||
health = math.max(target.Definition.MaxHealth, health)
|
||||
case d: DamagingActivity =>
|
||||
d.data.targetBefore match {
|
||||
case target: SourceWithHealthEntry with SourceWithShieldsEntry =>
|
||||
health = math.max(target.Definition.MaxHealth, health)
|
||||
shields = math.max(target.Definition.asInstanceOf[WithShields].MaxShields, shields)
|
||||
case target: SourceWithHealthEntry =>
|
||||
health = math.max(target.health, health)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
(health, shields)
|
||||
}
|
||||
|
||||
// private def initialEntityHealthAndShields(history: List[InGameActivity]): (Int, Int, PlanetSideEmpire.Value) = {
|
||||
// history.collectFirst {
|
||||
// case SpawningActivity(target: SourceWithHealthEntry with SourceWithShieldsEntry, _, _) =>
|
||||
// (target.health, target.shields, target.Faction)
|
||||
// case SpawningActivity(target: SourceWithHealthEntry, _, _) =>
|
||||
// (target.health, 0, target.Faction)
|
||||
// case d: DamagingActivity =>
|
||||
// d.data.targetBefore match {
|
||||
// case target: SourceWithHealthEntry with SourceWithShieldsEntry =>
|
||||
// (target.health, target.shields, target.Faction)
|
||||
// case target: SourceWithHealthEntry =>
|
||||
// (target.health, 0, target.Faction)
|
||||
// case _ =>
|
||||
// (0, 0, PlanetSideEmpire.NEUTRAL)
|
||||
// }
|
||||
// }.getOrElse((0, 0, PlanetSideEmpire.NEUTRAL))
|
||||
// }
|
||||
|
||||
private[exp] def healthDamageContributors(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
participants: mutable.LongMap[ContributionStats]
|
||||
): Seq[(Long, Int)] = {
|
||||
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
|
||||
* key - character identifier,
|
||||
* value - current damage contribution */
|
||||
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
|
||||
history.tail.foreach {
|
||||
case d: DamagingActivity if d.health > 0 =>
|
||||
inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
|
||||
case _: RepairingActivity => ()
|
||||
case h: HealingActivity =>
|
||||
inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder)
|
||||
case _ => ()
|
||||
}
|
||||
inOrder
|
||||
}
|
||||
|
||||
private[exp] def contributeWithDamagingActivity(
|
||||
activity: DamagingActivity,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
amount: Int,
|
||||
participants: mutable.LongMap[ContributionStats],
|
||||
order: Seq[(Long, Int)]
|
||||
): Seq[(Long, Int)] = {
|
||||
activity.data.adversarial match {
|
||||
case Some(Adversarial(attacker: PlayerSource, _, _))
|
||||
if attacker.Faction != faction =>
|
||||
val whoId = attacker.CharId
|
||||
val wepid = activity.data.interaction.cause.attribution
|
||||
val time = activity.time
|
||||
val percentage = amount / attacker.Definition.MaxHealth.toFloat
|
||||
val updatedEntry = participants.get(whoId) match {
|
||||
case Some(mod) =>
|
||||
//previous attacker, just add to entry
|
||||
val firstWeapon = mod.weapons.head
|
||||
val weapons = if (firstWeapon.weapon_id == wepid) {
|
||||
firstWeapon.copy(
|
||||
amount = firstWeapon.amount + amount,
|
||||
shots = firstWeapon.shots + 1,
|
||||
time = time,
|
||||
contributions = firstWeapon.contributions + percentage
|
||||
) +: mod.weapons.tail
|
||||
} else {
|
||||
WeaponStats(wepid, amount, 1, time, percentage) +: mod.weapons
|
||||
}
|
||||
mod.copy(
|
||||
amount = mod.amount + amount,
|
||||
weapons = weapons,
|
||||
total = mod.total + amount,
|
||||
shots = mod.shots + 1,
|
||||
time = activity.time
|
||||
)
|
||||
case None =>
|
||||
//new attacker, new entry
|
||||
ContributionStats(
|
||||
attacker,
|
||||
Seq(WeaponStats(wepid, amount, 1, time, percentage)),
|
||||
amount,
|
||||
amount,
|
||||
1,
|
||||
time
|
||||
)
|
||||
}
|
||||
participants.put(whoId, updatedEntry)
|
||||
order.indexWhere({ case (id, _) => id == whoId }) match {
|
||||
case 0 =>
|
||||
//ongoing attack by same player
|
||||
val entry = order.head
|
||||
(entry._1, entry._2 + amount) +: order.tail
|
||||
case _ =>
|
||||
//different player than immediate prior attacker
|
||||
(whoId, amount) +: order
|
||||
}
|
||||
case _ =>
|
||||
//damage that does not lead to contribution
|
||||
order.headOption match {
|
||||
case Some((id, dam)) =>
|
||||
if (id == 0L) {
|
||||
(0L, dam + amount) +: order.tail //pool
|
||||
} else {
|
||||
(0L, amount) +: order //new
|
||||
}
|
||||
case None =>
|
||||
order
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def entityHealthDamageContributors(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
participants: mutable.LongMap[ContributionStats]
|
||||
): Seq[(Long, Int)] = {
|
||||
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
|
||||
* key - character identifier,
|
||||
* value - current damage contribution */
|
||||
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
|
||||
history.collect {
|
||||
case d: DamagingActivity if d.health > 0 =>
|
||||
inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
|
||||
|
||||
case r: RepairingActivity if r.amount > 0 =>
|
||||
inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
|
||||
}
|
||||
inOrder
|
||||
}
|
||||
|
||||
private[exp] def contributeWithRecoveryActivity(
|
||||
amount: Int,
|
||||
participants: mutable.LongMap[ContributionStats],
|
||||
order: Seq[(Long, Int)]
|
||||
): Seq[(Long, Int)] = {
|
||||
var amt = amount
|
||||
var count = 0
|
||||
var newOrder: Seq[(Long, Int)] = Nil
|
||||
order.takeWhile { entry =>
|
||||
val (id, total) = entry
|
||||
if (id > 0 && total > 0) {
|
||||
val part = participants(id)
|
||||
if (amount > total) {
|
||||
//drop this entry
|
||||
participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case
|
||||
amt = amt - total
|
||||
} else {
|
||||
//edit around the inclusion of this entry
|
||||
val newTotal = total - amt
|
||||
val trimmedWeapons = {
|
||||
var index = -1
|
||||
var weaponSum = 0
|
||||
val pweapons = part.weapons
|
||||
while (weaponSum < amt) {
|
||||
index += 1
|
||||
weaponSum = weaponSum + pweapons(index).amount
|
||||
}
|
||||
pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size)
|
||||
}
|
||||
newOrder = (id, newTotal) +: newOrder
|
||||
participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
|
||||
amt = 0
|
||||
}
|
||||
}
|
||||
count += 1
|
||||
amt > 0
|
||||
}
|
||||
newOrder ++ order.drop(count)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ private case class WeaponStats(
|
|||
weapon_id: Int,
|
||||
amount: Int,
|
||||
shots: Int,
|
||||
time: Long
|
||||
time: Long,
|
||||
contributions: Float
|
||||
)
|
||||
|
||||
private case class ContributionStats(
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import org.joda.time.{Instant, LocalDateTime => JodaLocalDateTime}
|
|||
import net.psforever.objects.serverobject.hackable.Hackable
|
||||
import net.psforever.objects.serverobject.terminals.Terminal
|
||||
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry}
|
||||
import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFromEquipment, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity}
|
||||
import net.psforever.objects.vital.{DamagingActivity, HealFromEquipment, InGameActivity, ReconstructionActivity, RepairFromEquipment, RepairFromExoSuitChange, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.types.TransactionType
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
|
||||
import net.psforever.zones.Zones
|
||||
|
||||
import scala.collection.mutable
|
||||
|
|
@ -18,14 +18,82 @@ import scala.collection.mutable
|
|||
object Support {
|
||||
private type SupportActivity = InGameActivity with SupportActivityCausedByAnother
|
||||
|
||||
private[exp] def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
|
||||
val spawnIndex = history.lastIndexWhere {
|
||||
case _: SpawningActivity => true
|
||||
case _: RevivingActivity => true
|
||||
case _ => false
|
||||
}
|
||||
val endIndex = history.lastIndexWhere {
|
||||
case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
|
||||
case _ => false
|
||||
}
|
||||
if (spawnIndex == -1 || endIndex == -1) {
|
||||
Nil //throw VitalsHistoryException(history.head, "vitals history does not contain expected conditions")
|
||||
// } else
|
||||
// if (spawnIndex == -1) {
|
||||
// Nil //throw VitalsHistoryException(history.head, "vitals history does not contain initial spawn conditions")
|
||||
// } else if (endIndex == -1) {
|
||||
// Nil //throw VitalsHistoryException(history.last, "vitals history does not contain end of life conditions")
|
||||
} else {
|
||||
history.slice(spawnIndex, endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private[exp] def onlyOriginalAssistEntries(
|
||||
first: mutable.LongMap[ContributionStatsOutput],
|
||||
second: mutable.LongMap[ContributionStatsOutput]
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
onlyOriginalAssistEntriesIterable(first.values, second.values)
|
||||
}
|
||||
|
||||
private[exp] def onlyOriginalAssistEntriesIterable(
|
||||
first: Iterable[ContributionStatsOutput],
|
||||
second: Iterable[ContributionStatsOutput]
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
if (second.isEmpty) {
|
||||
first
|
||||
} else if (first.isEmpty) {
|
||||
second
|
||||
} else {
|
||||
//overlap discriminated by percentage
|
||||
val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
|
||||
for {
|
||||
h @ ContributionStatsOutput(hid, hwep, hkda) <- first
|
||||
a @ ContributionStatsOutput(aid, awep, akda) <- second
|
||||
(id, out) = if (hkda < akda)
|
||||
(aid.CharId, a.copy(implements = (a.implements ++ hwep).distinct))
|
||||
else
|
||||
(hid.CharId, h.copy(implements = (h.implements ++ awep).distinct))
|
||||
if hid == aid && shared.put(id, out).isEmpty
|
||||
} yield ()
|
||||
val sharedKeys = shared.keys
|
||||
(first ++ second).filterNot { case ContributionStatsOutput(id, _, _) => sharedKeys.exists(_ == id.CharId) } ++ shared.values
|
||||
}
|
||||
}
|
||||
|
||||
private[exp] def collectHealthAssists(
|
||||
victim: SourceEntry,
|
||||
history: List[InGameActivity],
|
||||
func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
|
||||
): mutable.LongMap[ContributionStatsOutput] = {
|
||||
val healthAssists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
|
||||
val qualifiedHealing = healthAssists.values.foldLeft(0)(_ + _.total)
|
||||
val healthAssistsOutput = healthAssists.map { case (id, kda) =>
|
||||
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / qualifiedHealing))
|
||||
}
|
||||
healthAssistsOutput.remove(victim.CharId)
|
||||
healthAssistsOutput
|
||||
}
|
||||
|
||||
private[exp] def collectHealingSupportAssists(
|
||||
target: SourceEntry,
|
||||
time: JodaLocalDateTime,
|
||||
history: Iterable[InGameActivity]
|
||||
history: List[InGameActivity]
|
||||
): mutable.LongMap[ContributionStatsOutput] = {
|
||||
//normal heals
|
||||
val healInfo = collectSupportContributions(
|
||||
target, time, ExperienceCalculator.limitHistoryToThisLife(history.toList).collect { case heals: HealFromEquipment
|
||||
target, time, history.collect { case heals: HealFromEquipment
|
||||
if heals.amount > 0 && heals.user.unique != target.unique => (heals, heals.equipment_def.ObjectId)
|
||||
}, mapContributionPointsByPercentage
|
||||
)
|
||||
|
|
@ -48,10 +116,10 @@ object Support {
|
|||
private[exp] def collectRepairingSupportAssists(
|
||||
target: SourceEntry,
|
||||
time: JodaLocalDateTime,
|
||||
history: Iterable[InGameActivity]
|
||||
history: List[InGameActivity]
|
||||
): mutable.LongMap[ContributionStatsOutput] = {
|
||||
collectSupportContributions(
|
||||
target, time, ExperienceCalculator.limitHistoryToThisLife(history.toList).collect { case repairs: RepairFromEquipment
|
||||
target, time, history.collect { case repairs: RepairFromEquipment
|
||||
if repairs.amount > 0 && repairs.user.unique != target.unique => (repairs, repairs.equipment_def.ObjectId)
|
||||
}, mapContributionPointsByPercentage
|
||||
)
|
||||
|
|
@ -148,11 +216,11 @@ object Support {
|
|||
.sortBy(_.time)(Ordering.Long.reverse)
|
||||
}
|
||||
|
||||
private def allocateSupportAssists(
|
||||
longTerm: Seq[SupportActivity],
|
||||
shortTerm: Seq[SupportActivity],
|
||||
defaultTool: Int
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
private def combineTimeApplicableActivitiesForContribution(
|
||||
longTerm: Seq[SupportActivity],
|
||||
shortTerm: Seq[SupportActivity],
|
||||
defaultTool: Int
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
val contributions: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
|
||||
(longTerm ++ shortTerm).foreach { h =>
|
||||
val amount = h.amount
|
||||
|
|
@ -167,7 +235,8 @@ object Support {
|
|||
time = math.max(entry.time, h.time)
|
||||
))
|
||||
case None =>
|
||||
contributions.put(charId, ContributionStats(user, Seq(WeaponStats(defaultTool, amount, 1, h.time)), amount, amount, 1, h.time))
|
||||
//the contribution percentage allocated will always be 1.0f and should be overwritten later
|
||||
contributions.put(charId, ContributionStats(user, Seq(WeaponStats(defaultTool, amount, 1, h.time, 1f)), amount, amount, 1, h.time))
|
||||
}
|
||||
}
|
||||
contributions
|
||||
|
|
@ -183,38 +252,54 @@ object Support {
|
|||
//TODO this is not correct, but it will do for now
|
||||
if (tools.isEmpty) {
|
||||
mutable.LongMap[ContributionStatsOutput]()
|
||||
} else if (tools.distinct.size == 1) {
|
||||
collectSupportAssists(target, time, activity, tools.head, contributionPointsMap)
|
||||
} else {
|
||||
var output = Iterable[ContributionStatsOutput]()
|
||||
tools.groupBy(identity).keys.foreach { implement =>
|
||||
output = ExperienceCalculator.onlyOriginalAssistEntriesIterable(
|
||||
output,
|
||||
collectSupportAssists(target, time, activity, implement, contributionPointsMap).values
|
||||
)
|
||||
val distinctTools = tools.distinct
|
||||
if (distinctTools.size == 1) {
|
||||
compileTimedSupportContributions(target, time, activity, distinctTools.head, contributionPointsMap)
|
||||
} else {
|
||||
var output = Iterable[ContributionStatsOutput]()
|
||||
distinctTools.foreach { implement =>
|
||||
output = onlyOriginalAssistEntriesIterable(
|
||||
output,
|
||||
compileTimedSupportContributions(target, time, activity, implement, contributionPointsMap).values
|
||||
)
|
||||
}
|
||||
mutable.LongMap[ContributionStatsOutput]().addAll(output.map { entry => (entry.player.CharId, entry) })
|
||||
}
|
||||
mutable.LongMap[ContributionStatsOutput]().addAll(output.map { entry => (entry.player.CharId, entry) })
|
||||
}
|
||||
}
|
||||
|
||||
private def collectSupportAssists(
|
||||
target: SourceEntry,
|
||||
killLastTime: JodaLocalDateTime,
|
||||
activity: Seq[InGameActivity],
|
||||
defaultImplement: Int,
|
||||
contributionPointsMap: (Float,Iterable[SupportActivity])=>(Long,ContributionStats)=>(Long,ContributionStatsOutput)
|
||||
): mutable.LongMap[ContributionStatsOutput] = {
|
||||
val activityByAnother = activity.collect {
|
||||
case activity: SupportActivity => activity
|
||||
}
|
||||
val longTermActivity = findTimeApplicableActivities(activityByAnother, killLastTime, timeOffset = 600)
|
||||
val shortTermActivity = findTimeApplicableActivities(activityByAnother, killLastTime, timeOffset = 300)
|
||||
val contributions = allocateSupportAssists(longTermActivity, shortTermActivity, defaultImplement)
|
||||
private def compileTimedSupportContributions(
|
||||
target: SourceEntry,
|
||||
timeLimit: JodaLocalDateTime,
|
||||
activity: Seq[InGameActivity],
|
||||
defaultImplement: Int,
|
||||
contributionPointsMap: (Float,Iterable[SupportActivity])=>(Long,ContributionStats)=>(Long,ContributionStatsOutput)
|
||||
): mutable.LongMap[ContributionStatsOutput] = {
|
||||
val activityByAnother = activity.collect { case theActivity: SupportActivity => theActivity }
|
||||
val longTermActivity = findTimeApplicableActivities(activityByAnother, timeLimit, timeOffset = 600)
|
||||
val shortTermActivity = findTimeApplicableActivities(activityByAnother, timeLimit, timeOffset = 300)
|
||||
val contributions = combineTimeApplicableActivitiesForContribution(longTermActivity, shortTermActivity, defaultImplement)
|
||||
contributions.remove(target.CharId)
|
||||
val mapFunc = contributionPointsMap(contributions.values.foldLeft(0)(_ + _.total).toFloat, shortTermActivity)
|
||||
contributions.map { case (a, b) => mapFunc(a, b) }
|
||||
}
|
||||
|
||||
private[exp] def allocateContributors(
|
||||
tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any
|
||||
)
|
||||
(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
/** players who have contributed to this death, and how much they have contributed<br>
|
||||
* key - character identifier,
|
||||
* value - (player, damage, total damage, number of shots) */
|
||||
val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
|
||||
tallyFunc(history, faction, participants)
|
||||
participants
|
||||
}
|
||||
|
||||
private def mapContributionPointsByPercentage(
|
||||
total: Float,
|
||||
compareList: Iterable[SupportActivity]
|
||||
|
|
@ -257,4 +342,13 @@ object Support {
|
|||
): (Long, ContributionStatsOutput) = {
|
||||
(charId, ContributionStatsOutput(contribution.player, contribution.weapons.map { _.weapon_id }, compareList.size.toFloat))
|
||||
}
|
||||
|
||||
private[exp] def wasEverAMax(player: PlayerSource, history: Iterable[InGameActivity]): Boolean = {
|
||||
player.ExoSuit == ExoSuitType.MAX || history.exists {
|
||||
case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
|
||||
case ReconstructionActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
|
||||
case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue