diff --git a/server/src/main/resources/db/migration/V010__Scoring2.sql b/server/src/main/resources/db/migration/V010__Scoring2.sql
index 14f013560..56e82eba9 100644
--- a/server/src/main/resources/db/migration/V010__Scoring2.sql
+++ b/server/src/main/resources/db/migration/V010__Scoring2.sql
@@ -363,7 +363,7 @@ DECLARE weaponId Int;
BEGIN
killerId := NEW.killer_id;
weaponId := NEW.weapon_id;
- SELECT proc_sessionnumber_get(killerId, killerSessionId);
+ CALL proc_sessionnumber_get(killerId, killerSessionId);
BEGIN
UPDATE weaponstatsession
SET assists = assists + 1
diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
index b1ce43916..26221e1c7 100644
--- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala
+++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
@@ -7,7 +7,7 @@ import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import java.util.concurrent.atomic.AtomicInteger
import net.psforever.actors.zone.ZoneActor
-import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill, SupportActivity}
+import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill, Life, SupportActivity}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.VehicleSource
import net.psforever.objects.vital.InGameHistory
@@ -851,6 +851,14 @@ object AvatarActor {
)
}
+ def updateToolDischargeFor(avatarId: Long, lives: Seq[Life]): Unit = {
+ lives
+ .flatMap { _.equipmentStats }
+ .foreach { stat =>
+ zones.exp.ToDatabase.reportToolDischarge(avatarId, stat)
+ }
+ }
+
def toAvatar(avatar: persistence.Avatar): Avatar = {
val bep = avatar.bep
val convertedCosmetics = if (BattleRank.showCosmetics(bep)) {
@@ -1083,6 +1091,7 @@ class AvatarActor(
.receiveSignal {
case (_, PostStop) =>
AvatarActor.avatarNoLongerLoggedIn(account.id)
+ AvatarActor.updateToolDischargeFor(avatar.id.toLong, avatar.scorecard.CurrentLife +: avatar.scorecard.Lives)
Behaviors.same
}
}
@@ -1707,14 +1716,7 @@ class AvatarActor(
Behaviors.same
case SupportExperienceDeposit(bep, delayBy) =>
- setBep(avatar.bep + bep, ExperienceType.Support)
- supportExperiencePool = supportExperiencePool - bep
- if (supportExperiencePool > 0) {
- resetSupportExperienceTimer(bep, delayBy)
- } else {
- supportExperienceTimer.cancel()
- supportExperienceTimer = Default.Cancellable
- }
+ actuallyAwardSupportExperience(bep, delayBy)
Behaviors.same
case SetBep(bep) =>
@@ -3022,10 +3024,23 @@ class AvatarActor(
}
def awardSupportExperience(bep: Long, previousDelay: Long): Unit = {
- supportExperiencePool = supportExperiencePool + bep
- avatar.scorecard.rate(bep)
- if (supportExperienceTimer.isCancelled) {
- resetSupportExperienceTimer(previousBep = 0, previousDelay = 0)
+ setBep(avatar.bep + bep, ExperienceType.Support) //todo simplify support testing
+// supportExperiencePool = supportExperiencePool + bep
+// avatar.scorecard.rate(bep)
+// if (supportExperienceTimer.isCancelled) {
+// resetSupportExperienceTimer(previousBep = 0, previousDelay = 0)
+// }
+ }
+
+ def actuallyAwardSupportExperience(bep: Long, delayBy: Long): Unit = {
+ setBep(avatar.bep + bep, ExperienceType.Support)
+ supportExperiencePool = supportExperiencePool - bep
+ if (supportExperiencePool > 0) {
+ resetSupportExperienceTimer(bep, delayBy)
+ } else {
+ supportExperiencePool = 0
+ supportExperienceTimer.cancel()
+ supportExperienceTimer = Default.Cancellable
}
}
@@ -3037,14 +3052,15 @@ class AvatarActor(
val zone = _session.zone
val player = _session.player
val playerSource = PlayerSource(player)
- val historyTranscript = (killStat.info.interaction.cause match {
- case pr: ProjectileReason => pr.projectile.mounted_in.flatMap { a => zone.GUID(a._1) } //what fired the projectile
- case _ => None
- }) match {
- case Some(mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons) =>
- player.HistoryAndContributions() ++ InGameHistory.ContributionFrom(mount).toList
- case _ =>
- player.HistoryAndContributions()
+ val historyTranscript = {
+ (killStat.info.interaction.cause match {
+ case pr: ProjectileReason => pr.projectile.mounted_in.flatMap { a => zone.GUID(a._1) } //what fired the projectile
+ case _ => None
+ }).collect {
+ case mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons =>
+ player.ContributionFrom(mount)
+ }
+ player.HistoryAndContributions()
}
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, killStat, exp)
val target = killStat.info.targetAfter.asInstanceOf[PlayerSource]
@@ -3133,7 +3149,6 @@ class AvatarActor(
def updateToolDischarge(stats: EquipmentStat): Unit = {
avatar.scorecard.rate(stats)
- zones.exp.ToDatabase.reportToolDischarge(avatar.id.toLong, stats)
}
def createAvatar(
diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala
index df62c7596..962e19cff 100644
--- a/src/main/scala/net/psforever/objects/Vehicle.scala
+++ b/src/main/scala/net/psforever/objects/Vehicle.scala
@@ -95,6 +95,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20))
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
+ private var previousFaction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var shields: Int = 0
private var decal: Int = 0
private var trunkAccess: Option[PlanetSideGUID] = None
@@ -128,14 +129,18 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
}
def Faction: PlanetSideEmpire.Value = {
- this.faction
- }
-
- override def Faction_=(faction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
- this.faction = faction
faction
}
+ override def Faction_=(toFaction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
+ //TODO in the future, this may create an issue when the vehicle is originally or is hacked from Black Ops
+ previousFaction = faction
+ faction = toFaction
+ toFaction
+ }
+
+ def PreviousFaction: PlanetSideEmpire.Value = previousFaction
+
/** How long it takes to jack the vehicle in seconds, based on the hacker's certification level */
def JackingDuration: Array[Int] = Definition.JackingDuration
@@ -335,9 +340,9 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
}
}
- override def DeployTime = Definition.DeployTime
+ override def DeployTime: Int = Definition.DeployTime
- override def UndeployTime = Definition.UndeployTime
+ override def UndeployTime: Int = Definition.UndeployTime
def Inventory: GridInventory = trunk
@@ -495,7 +500,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
def PreviousGatingManifest(): Option[VehicleManifest] = previousVehicleGatingManifest
- def DamageModel = Definition.asInstanceOf[DamageResistanceModel]
+ def DamageModel: DamageResistanceModel = Definition.asInstanceOf[DamageResistanceModel]
override def BailProtection_=(protect: Boolean): Boolean = {
!Definition.CanFly && super.BailProtection_=(protect)
diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala
index 37d98cc60..ed661d16a 100644
--- a/src/main/scala/net/psforever/objects/Vehicles.scala
+++ b/src/main/scala/net/psforever/objects/Vehicles.scala
@@ -237,16 +237,14 @@ object Vehicles {
val zone = target.Zone
// Forcefully dismount any cargo
target.CargoHolds.foreach { case (_, cargoHold) =>
- cargoHold.occupant match {
- case Some(cargo: Vehicle) =>
- cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false)
- case None => ;
+ cargoHold.occupant.collect {
+ cargo: Vehicle => cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false)
}
}
// Forcefully dismount all seated occupants from the vehicle
target.Seats.values.foreach(seat => {
- seat.occupant match {
- case Some(tplayer: Player) =>
+ seat.occupant.collect {
+ tplayer: Player =>
seat.unmount(tplayer)
tplayer.VehicleSeated = None
if (tplayer.HasGUID) {
@@ -255,7 +253,6 @@ object Vehicles {
VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, target.GUID)
)
}
- case _ => ;
}
})
// If the vehicle can fly and is flying deconstruct it, and well played to whomever managed to hack a plane in mid air. I'm impressed.
@@ -264,25 +261,17 @@ object Vehicles {
target.Actor ! Vehicle.Deconstruct()
} else { // Otherwise handle ownership transfer as normal
// Remove ownership of our current vehicle, if we have one
- hacker.avatar.vehicle match {
- case Some(guid: PlanetSideGUID) =>
- zone.GUID(guid) match {
- case Some(vehicle: Vehicle) =>
- Vehicles.Disown(hacker, vehicle)
- case _ => ;
- }
- case _ => ;
- }
- target.OwnerGuid match {
- case Some(previousOwnerGuid: PlanetSideGUID) =>
- // Remove ownership of the vehicle from the previous player
- zone.GUID(previousOwnerGuid) match {
- case Some(tplayer: Player) =>
- Vehicles.Disown(tplayer, target)
- case _ => ; // Vehicle already has no owner
- }
- case _ => ;
- }
+ hacker.avatar.vehicle
+ .flatMap { guid => zone.GUID(guid) }
+ .collect { case vehicle: Vehicle =>
+ Vehicles.Disown(hacker, vehicle)
+ }
+ // Remove ownership of the vehicle from the previous player
+ target.OwnerGuid
+ .flatMap { guid => zone.GUID(guid) }
+ .collect { case tplayer: Player =>
+ Vehicles.Disown(tplayer, target)
+ }
// Now take ownership of the jacked vehicle
target.Actor ! CommonMessages.Hack(hacker, target)
target.Faction = hacker.Faction
@@ -302,16 +291,15 @@ object Vehicles {
// If AMS is deployed, swap it to the new faction
target.Definition match {
case GlobalDefinitions.router =>
- target.Utility(UtilityType.internal_router_telepad_deployable) match {
- case Some(util: Utility.InternalTelepad) =>
+ target.Utility(UtilityType.internal_router_telepad_deployable).collect {
+ case util: Utility.InternalTelepad =>
//"power cycle"
util.Actor ! TelepadLike.Deactivate(util)
util.Actor ! TelepadLike.Activate(util)
- case _ => ;
}
case GlobalDefinitions.ams if target.DeploymentState == DriveState.Deployed =>
zone.VehicleEvents ! VehicleServiceMessage.AMSDeploymentChange(zone)
- case _ => ;
+ case _ => ()
}
}
@@ -412,6 +400,7 @@ object Vehicles {
*
* @param vehicle the vehicle
*/
+ //noinspection ScalaUnusedSymbol
def BeforeUnloadVehicle(vehicle: Vehicle, zone: Zone): Unit = {
vehicle.Definition match {
case GlobalDefinitions.ams =>
@@ -420,7 +409,7 @@ object Vehicles {
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
case GlobalDefinitions.router =>
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
- case _ => ;
+ case _ => ()
}
}
diff --git a/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala b/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala
index 49e88c074..a291d7b8d 100644
--- a/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala
@@ -67,6 +67,10 @@ object PlayerSource {
}
def apply(name: String, faction: PlanetSideEmpire.Value, position: Vector3): PlayerSource = {
+ this(UniquePlayer(0L, name, CharacterSex.Male, faction), position)
+ }
+
+ def apply(unique: UniquePlayer, position: Vector3): PlayerSource = {
new PlayerSource(
GlobalDefinitions.avatar,
ExoSuitType.Standard,
@@ -81,7 +85,7 @@ object PlayerSource {
GlobalDefinitions.Standard,
bep = 0L,
progress = tokenLife,
- UniquePlayer(0L, name, CharacterSex.Male, faction)
+ unique
)
}
@@ -137,6 +141,19 @@ object PlayerSource {
)
}
+ /**
+ * Produce a copy of a normal player source entity
+ * but the `seatedIn` field is overrode to point at the specified vehicle and seat number.
+ * Don't think too much about it.
+ * @param player `SourceEntry` for a player
+ * @param source `SourceEntry` for the aforementioned mountable entity
+ * @param seatNumber the attributed seating index in which the player is mounted in `source`
+ * @return a `PlayerSource` entity
+ */
+ def inSeat(player: PlayerSource, source: SourceEntry, seatNumber: Int): PlayerSource = {
+ player.copy(seatedIn = Some((source, seatNumber)))
+ }
+
/**
* "Nobody is my name: Nobody they call me –
* my mother and my father and all my other companions”
@@ -146,5 +163,8 @@ object PlayerSource {
*/
final val Nobody = PlayerSource("Nobody", PlanetSideEmpire.NEUTRAL, Vector3.Zero)
+ /**
+ * Used to dummy the statistics value for shallow player source entities.
+ */
private val tokenLife: Life = Life()
}
diff --git a/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala b/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala
index f3d4d8c46..7ff45cfd4 100644
--- a/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala
@@ -17,15 +17,15 @@ final case class VehicleSource(
Orientation: Vector3,
Velocity: Option[Vector3],
deployed: DriveState.Value,
- owner: Option[PlayerSource],
+ owner: Option[UniquePlayer],
occupants: List[SourceEntry],
Modifiers: ResistanceProfile,
unique: UniqueVehicle
) extends SourceWithHealthEntry with SourceWithShieldsEntry {
- def Name: String = SourceEntry.NameFormat(Definition.Name)
- def Health: Int = health
- def Shields: Int = shields
- def total: Int = health + shields
+ def Name: String = SourceEntry.NameFormat(Definition.Name)
+ def Health: Int = health
+ def Shields: Int = shields
+ def total: Int = health + shields
}
object VehicleSource {
@@ -54,13 +54,15 @@ object VehicleSource {
obj.OriginalOwnerName.getOrElse("none")
)
)
- vehicle.copy(occupants = {
- obj.Seats.map { case (seatNumber, seat) =>
+ //shallow information that references the existing source entry
+ vehicle.copy(
+ owner = obj.Owners,
+ occupants = obj.Seats.map { case (seatNumber, seat) =>
seat.occupant match {
- case Some(p) => PlayerSource.inSeat(p, vehicle, seatNumber) //shallow
+ case Some(p) => PlayerSource.inSeat(p, vehicle, seatNumber)
case _ => PlayerSource.Nobody
}
}.toList
- })
+ )
}
}
diff --git a/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala
index 90e580bf4..a621917fc 100644
--- a/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala
@@ -32,14 +32,12 @@ trait CarrierBehavior {
cargoDismountTimer.cancel()
val obj = CarrierObject
val zone = obj.Zone
- zone.GUID(isMounting) match {
- case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
- case _ => ;
+ zone.GUID(isMounting).collect {
+ case v : Vehicle => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
}
isMounting = None
- zone.GUID(isDismounting) match {
- case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID)
- case _ => ;
+ zone.GUID(isDismounting).collect {
+ case v : Vehicle => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID)
}
isDismounting = None
}
@@ -86,9 +84,8 @@ trait CarrierBehavior {
)
}
else {
- obj.Zone.GUID(isMounting) match {
- case Some(v: Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
- case _ => ;
+ obj.Zone.GUID(isMounting).collect {
+ case v: Vehicle => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
}
isMounting = None
}
@@ -112,10 +109,9 @@ trait CarrierBehavior {
kicked = false
)
case _ =>
- obj.CargoHold(mountPoint) match {
- case Some(hold) if hold.isOccupied && hold.occupant.get.GUID == cargo_guid =>
+ obj.CargoHold(mountPoint).collect {
+ case hold if hold.isOccupied && hold.occupant.get.GUID == cargo_guid =>
CarrierBehavior.CargoDismountAction(obj, hold.occupant.get, hold, BailType.Normal)
- case _ => ;
}
false
}
@@ -132,17 +128,15 @@ trait CarrierBehavior {
CarrierBehavior.CheckCargoDismount(cargo_guid, mountPoint, iteration + 1, bailed)
)
} else {
- zone.GUID(isDismounting.getOrElse(cargo_guid)) match {
- case Some(cargo: Vehicle) =>
+ zone.GUID(isDismounting.getOrElse(cargo_guid)).collect {
+ case cargo: Vehicle =>
cargo.Actor ! CargoBehavior.EndCargoDismounting(guid)
- case _ => ;
}
isDismounting = None
}
} else {
- zone.GUID(isDismounting.getOrElse(cargo_guid)) match {
- case Some(cargo: Vehicle) => cargo.Actor ! CargoBehavior.EndCargoDismounting(guid)
- case _ => ;
+ zone.GUID(isDismounting.getOrElse(cargo_guid)).collect {
+ case cargo: Vehicle => cargo.Actor ! CargoBehavior.EndCargoDismounting(guid)
}
isDismounting = None
}
@@ -526,7 +520,7 @@ object CarrierBehavior {
targetGUID: PlanetSideGUID
): Unit = {
target match {
- case Some(_: Vehicle) => ;
+ case Some(_: Vehicle) => ()
case Some(_) => log.error(s"$decorator target $targetGUID no longer identifies as a vehicle")
case None => log.error(s"$decorator target $targetGUID has gone missing")
}
@@ -662,7 +656,17 @@ object CarrierBehavior {
carrierGuid: PlanetSideGUID): Unit = {
hold.mount(cargo)
cargo.MountedIn = carrierGuid
- cargo.LogActivity(VehicleCargoMountActivity(VehicleSource(carrier), VehicleSource(cargo), carrier.Zone.Number))
+ val event = VehicleCargoMountActivity(VehicleSource(carrier), VehicleSource(cargo), carrier.Zone.Number)
+ cargo.LogActivity(event)
+ cargo.Seats
+ .filterNot(_._1 == 0) /*ignore driver*/
+ .values
+ .collect {
+ case seat if seat.isOccupied =>
+ seat.occupants.foreach { player =>
+ player.LogActivity(event)
+ }
+ }
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGuid)
}
@@ -681,6 +685,18 @@ object CarrierBehavior {
): Unit = {
cargo.MountedIn = None
hold.unmount(cargo, bailType)
- cargo.LogActivity(VehicleCargoMountActivity(VehicleSource(carrier), VehicleSource(cargo), carrier.Zone.Number))
+ val event = VehicleCargoMountActivity(VehicleSource(carrier), VehicleSource(cargo), carrier.Zone.Number)
+ cargo.LogActivity(event)
+ cargo.Seats
+ .filterNot(_._1 == 0) /*ignore driver*/
+ .values
+ .collect {
+ case seat if seat.isOccupied =>
+ seat.occupants.foreach { player =>
+ player.LogActivity(event)
+ player.ContributionFrom(cargo)
+ player.ContributionFrom(carrier)
+ }
+ }
}
}
diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
index 320415bd8..cd5177b2b 100644
--- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
+++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
@@ -4,7 +4,7 @@ package net.psforever.objects.vital
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity
-import net.psforever.objects.sourcing.{AmenitySource, ObjectSource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource}
+import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource}
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
@@ -20,7 +20,21 @@ import scala.collection.mutable
* Must keep track of the time (ms) the activity occurred.
*/
trait InGameActivity {
- val time: Long = System.currentTimeMillis()
+ private var _time: Long = System.currentTimeMillis()
+
+ def time: Long = _time
+}
+
+object InGameActivity {
+ def ShareTime(benefactor: InGameActivity, donor: InGameActivity): InGameActivity = {
+ benefactor._time = donor.time
+ benefactor
+ }
+
+ def SetTime(benefactor: InGameActivity, time: Long): InGameActivity = {
+ benefactor._time = time
+ benefactor
+ }
}
/* normal history */
@@ -66,14 +80,22 @@ sealed trait VehicleCargoMountChange extends VehicleMountChange {
final case class VehicleMountActivity(vehicle: VehicleSource, player: PlayerSource, zoneNumber: Int)
extends VehiclePassengerMountChange
-final case class VehicleDismountActivity(vehicle: VehicleSource, player: PlayerSource, zoneNumber: Int)
- extends VehiclePassengerMountChange
+final case class VehicleDismountActivity(
+ vehicle: VehicleSource,
+ player: PlayerSource,
+ zoneNumber: Int,
+ pairedEvent: Option[VehicleMountActivity] = None
+ ) extends VehiclePassengerMountChange
final case class VehicleCargoMountActivity(vehicle: VehicleSource, cargo: VehicleSource, zoneNumber: Int)
extends VehicleCargoMountChange
-final case class VehicleCargoDismountActivity(vehicle: VehicleSource, cargo: VehicleSource, zoneNumber: Int)
- extends VehicleCargoMountChange
+final case class VehicleCargoDismountActivity(
+ vehicle: VehicleSource,
+ cargo: VehicleSource,
+ zoneNumber: Int,
+ pairedEvent: Option[VehicleCargoMountActivity] = None
+ ) extends VehicleCargoMountChange
final case class Contribution(src: SourceUniqueness, entries: List[InGameActivity])
extends GeneralActivity {
@@ -202,11 +224,34 @@ trait InGameHistory {
/**
* An in-game event must be recorded.
* Add new entry to the list (for recent activity).
+ * Special handling must be conducted for certain events.
* @param action the fully-informed entry
* @return the list of previous changes to this entity
*/
def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = {
action match {
+ case Some(act: VehicleDismountActivity) =>
+ history
+ .findLast(_.isInstanceOf[VehicleMountActivity])
+ .collect {
+ case event: VehicleMountActivity if event.vehicle.unique == act.vehicle.unique =>
+ history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
+ }
+ .orElse {
+ history = history :+ act
+ None
+ }
+ case Some(act: VehicleCargoDismountActivity) =>
+ history
+ .findLast(_.isInstanceOf[VehicleCargoMountActivity])
+ .collect {
+ case event: VehicleCargoMountActivity if event.vehicle.unique == act.vehicle.unique =>
+ history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
+ }
+ .orElse {
+ history = history :+ act
+ None
+ }
case Some(act) =>
history = history :+ act
case None => ()
@@ -264,13 +309,18 @@ trait InGameHistory {
if (target eq this) {
None
} else {
- target.GetContribution() match {
- case Some(in) =>
- val contribution = Contribution(SourceEntry(target).unique, in)
- contributionInheritance.put(contribution.src, contribution)
+ val uniqueTarget = SourceEntry(target).unique
+ (target.GetContribution(), contributionInheritance.get(uniqueTarget)) match {
+ case (Some(in), Some(curr)) =>
+ val end = curr.end
+ val contribution = Contribution(uniqueTarget, curr.entries ++ in.filter(_.time > end))
+ contributionInheritance.put(uniqueTarget, contribution)
Some(contribution)
- case None =>
- contributionInheritance.remove(SourceEntry(target).unique)
+ case (Some(in), _) =>
+ val contribution = Contribution(uniqueTarget, in)
+ contributionInheritance.put(uniqueTarget, contribution)
+ Some(contribution)
+ case (None, _) =>
None
}
}
@@ -310,9 +360,9 @@ object InGameHistory {
val toUnitSource = unit.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }
val event: GeneralActivity = if (obj.History.isEmpty) {
- SpawningActivity(ObjectSource(obj), zoneNumber, toUnitSource)
+ SpawningActivity(SourceEntry(obj), zoneNumber, toUnitSource)
} else {
- ReconstructionActivity(ObjectSource(obj), zoneNumber, toUnitSource)
+ ReconstructionActivity(SourceEntry(obj), zoneNumber, toUnitSource)
}
if (obj.History.lastOption match {
case Some(evt: SpawningActivity) => evt != event
diff --git a/src/main/scala/net/psforever/objects/zones/exp/EquipmentUseContextWrapper.scala b/src/main/scala/net/psforever/objects/zones/exp/EquipmentUseContextWrapper.scala
index cb7bedfce..d52c620c7 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/EquipmentUseContextWrapper.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/EquipmentUseContextWrapper.scala
@@ -5,27 +5,34 @@ import enumeratum.values.IntEnumEntry
sealed abstract class EquipmentUseContextWrapper(val value: Int) extends IntEnumEntry {
def equipment: Int
+ def intermediate: Int
+}
+
+sealed abstract class NoIntermediateUseContextWrapper(override val value: Int)
+ extends EquipmentUseContextWrapper(value) {
def intermediate: Int = 0
}
-final case class NoUse(equipment: Int) extends EquipmentUseContextWrapper(value = -1)
+final case class NoUse() extends NoIntermediateUseContextWrapper(value = -1) {
+ def equipment: Int = 0
+}
-final case class DamageWith(equipment: Int) extends EquipmentUseContextWrapper(value = 0)
+final case class DamageWith(equipment: Int) extends NoIntermediateUseContextWrapper(value = 0)
-final case class Destroyed(equipment: Int) extends EquipmentUseContextWrapper(value = 1)
-final case class ReviveAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 4)
-final case class AmenityDestroyed(equipment: Int) extends EquipmentUseContextWrapper(value = 10)
-final case class DriverKilled(equipment: Int) extends EquipmentUseContextWrapper(value = 12)
-final case class GunnerKilled(equipment: Int) extends EquipmentUseContextWrapper(value = 13)
-final case class PassengerKilled(equipment: Int) extends EquipmentUseContextWrapper(value = 14)
-final case class CargoDestroyed(equipment: Int) extends EquipmentUseContextWrapper(value = 15)
-final case class DriverAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 18)
-final case class HealKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 20)
-final case class ReviveKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 21)
-final case class RepairKillAssist(equipment: Int, override val intermediate: Int) extends EquipmentUseContextWrapper(value = 22)
-final case class AmsRespawnKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 23)
-final case class HotDropKillAssist(equipment: Int, override val intermediate: Int) extends EquipmentUseContextWrapper(value = 24)
-final case class HackKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 25)
-final case class LodestarRearmKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 26)
-final case class AmsResupplyKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 27)
-final case class RouterKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 28)
+final case class Destroyed(equipment: Int) extends NoIntermediateUseContextWrapper(value = 1)
+final case class ReviveAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 4)
+final case class AmenityDestroyed(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 10)
+final case class DriverKilled(equipment: Int) extends NoIntermediateUseContextWrapper(value = 12)
+final case class GunnerKilled(equipment: Int) extends NoIntermediateUseContextWrapper(value = 13)
+final case class PassengerKilled(equipment: Int) extends NoIntermediateUseContextWrapper(value = 14)
+final case class CargoDestroyed(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 15)
+final case class DriverAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 18)
+final case class HealKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 20)
+final case class ReviveKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 21)
+final case class RepairKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 22)
+final case class AmsRespawnKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 23)
+final case class HotDropKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 24)
+final case class HackKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 25)
+final case class LodestarRearmKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 26)
+final case class AmsResupplyKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 27)
+final case class RouterKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 28)
diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
index 4ba367fc3..ada240c1b 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
@@ -2,17 +2,110 @@
package net.psforever.objects.zones.exp
import akka.actor.ActorRef
-import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
+import net.psforever.objects.avatar.scoring.{Assist, Death, KDAStat, Kill}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairingActivity, RevivingActivity, SpawningActivity}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire
+import scala.annotation.tailrec
import scala.collection.mutable
import scala.concurrent.duration._
+/**
+ * One player will interact using any number of weapons they possess
+ * that will affect a different player - the target.
+ * A kill is counted as the last interaction that affects a target so as to drop their health to zero.
+ * An assist is counted as every other interaction that affects the target up until the kill interaction
+ * in a similar way to the kill interaction.
+ * @see `ContributionStats`
+ * @see `ContributionStatsOutput`
+ * @see `DamagingActivity`
+ * @see `HealingActivity`
+ * @see `InGameActivity`
+ * @see `InGameHistory`
+ * @see `PlayerSource`
+ * @see `RepairingActivity`
+ * @see `SourceEntry`
+ */
object KillAssists {
+ /**
+ * Primary landing point for calculating the rewards given for player death.
+ * Rewards in the form of "battle experience points" are given:
+ * to the player held responsible for the other player's death - the killer;
+ * all players whose efforts managed to deal damage to the player who died prior to the killer - assists.
+ * @param victim player that died
+ * @param lastDamage purported as the in-game activity that resulted in the player dying
+ * @param history chronology of activity the game considers noteworthy;
+ * `lastDamage` should be within this chronology
+ * @param eventBus where to send the results of the experience determination(s)
+ * @see `ActorRef`
+ * @see `AvatarAction.UpdateKillsDeathsAssists`
+ * @see `AvatarServiceMessage`
+ * @see `DamageResult`
+ * @see `rewardThisPlayerDeath`
+ */
+ private[exp] def rewardThisPlayerDeath(
+ victim: PlayerSource,
+ lastDamage: Option[DamageResult],
+ history: Iterable[InGameActivity],
+ eventBus: ActorRef
+ ): Unit = {
+ rewardThisPlayerDeath(victim, lastDamage, history).foreach { case (p, kda) =>
+ eventBus ! AvatarServiceMessage(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda))
+ }
+ }
+
+ /**
+ * Primary innards of the functionality of calculating the rewards given for player death.
+ * @param victim player that died
+ * @param lastDamage purported as the in-game activity that resulted in the player dying
+ * @param history chronology of activity the game considers noteworthy;
+ * `lastDamage` should be within this chronology
+ * @return na
+ * @see `Assist`
+ * @see `calculateExperience`
+ * @see `collectKillAssistsForPlayer`
+ * @see `DamageResult`
+ * @see `Death`
+ * @see `KDAStat`
+ * @see `limitHistoryToThisLife`
+ * @see `Support.baseExperience`
+ */
+ private def rewardThisPlayerDeath(
+ victim: PlayerSource,
+ lastDamage: Option[DamageResult],
+ history: Iterable[InGameActivity],
+ ): Seq[(PlayerSource, KDAStat)] = {
+ val shortHistory = limitHistoryToThisLife(history.toList)
+ determineKiller(lastDamage, shortHistory) match {
+ case Some((result, killer: PlayerSource)) =>
+ val assists = collectKillAssistsForPlayer(victim, shortHistory, Some(killer))
+ val fullBep = calculateExperience(killer, victim, shortHistory)
+ val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
+ case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
+ }.toSeq
+ (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
+
+ case _ =>
+ val assists = collectKillAssistsForPlayer(victim, shortHistory, None)
+ val fullBep = Support.baseExperience(victim, shortHistory)
+ val hitSquad = assists.map {
+ case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
+ }.toSeq
+ (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
+ }
+ }
+
+ /**
+ * Limit the chronology of in-game activity between an starting activity and a concluding activity for a player character.
+ * The starting activity is signalled by one or two particular events.
+ * The concluding activity is a condition of one of many common events.
+ * All of the activities logged in between count.
+ * @param history chronology of activity the game considers noteworthy
+ * @return chronology of activity the game considers noteworthy, but truncated
+ */
private def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
val spawnIndex = history.lastIndexWhere {
case _: SpawningActivity => true
@@ -30,6 +123,14 @@ object KillAssists {
}
}
+ /**
+ * Determine the player who is the origin/owner of the bullet that reduced health to zero.
+ * @param lastDamageActivity damage result that purports the player who is the killer
+ * @param history chronology of activity the game considers noteworthy;
+ * referenced in the case that the suggested `DamageResult` is not suitable to determine a player
+ * @return player associated
+ * @see `limitHistoryToThisLife`
+ */
private[exp] def determineKiller(
lastDamageActivity: Option[DamageResult],
history: List[InGameActivity]
@@ -37,16 +138,18 @@ object KillAssists {
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
+ if now - dam.interaction.hitTime < compareTimeMillis && dam.adversarial.nonEmpty =>
+ (dam, dam.adversarial.get.attacker)
+ }
+ .orElse {
+ limitHistoryToThisLife(history)
+ .lastOption
+ .collect { case dam: DamagingActivity =>
+ val res = dam.data
+ (res, res.adversarial.get.attacker)
+ }
}
- .collect { case (dam, Some(adv)) => (dam, adv.attacker) }
}
/**
@@ -55,37 +158,140 @@ object KillAssists {
* The measurement - a "streak" in modern lingo - is transformed into the form of an `Integer` for simplicity.
* @param player the player
* @param mercy a time value that can be used to continue a missed streak
- * @return an `Integer` between 0 and 7
+ * @return an integer between 0 and 7;
+ * 0 is no kills,
+ * 1 is some kills,
+ * 2-7 is a menace score;
+ * there is no particular meaning behind different menace scores ascribed by this function
+ * @see `qualifiedTimeDifferences`
+ * @see `takeWhileLess`
*/
- private[exp] def calculateMenace(player: PlayerSource, mercy: Long = 2500L): Int = {
- val allKills = player.progress.kills.reverse
- val restBetweenKills = allKills.take(10) match {
- case firstKill :: kills if kills.size == 9 =>
- var xTime = firstKill.time.toDate.getTime
- kills.map { kill =>
- val time = kill.time.toDate.getTime
- val timeOut = time - xTime
- xTime = time
- timeOut
- }
- case _ =>
- Nil
+ private[exp] def calculateMenace(player: PlayerSource, mercy: Long = 5000L): Int = {
+ val maxDelayDiff: Long = 45000L
+ val minDelayDiff: Long = 20000L
+ val allKills = player.progress.kills
+ //the very first kill must have been within the max delay (but does not count towards menace)
+ if (allKills.headOption.exists { System.currentTimeMillis() - _.time.toDate.getTime < maxDelayDiff}) {
+ allKills match {
+ case _ :: kills if kills.size > 3 =>
+ val (continuations, restsBetweenKills) =
+ qualifiedTimeDifferences(
+ kills.map(_.time.toDate.getTime).iterator,
+ maxValidDiffCount = 10,
+ maxDelayDiff,
+ minDelayDiff
+ )
+ .partition(_ > minDelayDiff)
+ math.max(
+ 1,
+ math.floor(math.sqrt(
+ math.max(0, takeWhileLess(restsBetweenKills, testValue = 20000L, mercy).size - 1) + /*max=8*/
+ math.max(0, takeWhileLess(restsBetweenKills, testValue = 10000L, mercy).size - 5) * 3 + /*max=12*/
+ math.max(0, takeWhileLess(restsBetweenKills, testValue = 5000L, mercy = 1000L).size - 4) * 7 /*max=35*/
+ ) - continuations.size)
+ ).toInt
+ case _ =>
+ 1
+ }
+ } else {
+ 0
}
- //TODO the math here is not very meaningful
- math.floor(math.sqrt(
- math.max(0, takeWhileDelay(restBetweenKills, testValue = 20000L, mercy).size - 1) +
- math.max(0, takeWhileDelay(restBetweenKills, testValue = 10000L, mercy).size - 5) * 3 +
- math.max(0, takeWhileDelay(restBetweenKills, testValue = 5000L, mercy = 1000L).size - 4) * 5
- )).toInt
}
- private def takeWhileDelay(list: Iterable[Long], testValue: Long, mercy: Long = 2500L): Iterable[Long] = {
+ /**
+ * Take a list of times
+ * and produce a list of delays between those entries less than a maximum time delay.
+ * These are considered "qualifying".
+ * Count a certain number of time delays that fall within a minimum threshold
+ * and stop when that minimum count is achieved.
+ * These are considered "valid".
+ * The final product should be a new list of the successive delays from the first list
+ * containing both qualified and valid entries,
+ * stopping at either the first unqualified delay or the last valid delay or at exhaustion of the original list.
+ * @param iter unfiltered list of times (ms)
+ * @param maxValidDiffCount maximum number of valid entries in the final list of time differences;
+ * see `validTimeEntryCount`
+ * @param maxDiff exclusive amount of time allowed between qualifying entries;
+ * include any time difference within this delay;
+ * these entries are "qualifying" but are not "valid"
+ * @param minDiff inclusive amount of time difference allowed between valid entries;
+ * include time differences in this delay
+ * these entries are "valid" and should increment the counter `validTimeEntryCount`
+ * @return list of qualifying time differences (ms)
+ */
+ /*
+ Parameters governed by recursion:
+ @param diffList ongoing list of qualifying time differences (ms)
+ @param diffExtensionList accumulation of entries greater than the `minTimeEntryDiff`
+ but less that the `minTimeEntryDiff`;
+ holds qualifying time differences
+ that will be included before the next valid time difference
+ @param validDiffCount currently number of valid time entries in the qualified time list;
+ see `maxValidTimeEntryCount`
+ @param previousTime previous qualifying entry time;
+ by default, current time (ms)
+ */
+ @tailrec
+ private def qualifiedTimeDifferences(
+ iter: Iterator[Long],
+ maxValidDiffCount: Int,
+ maxDiff: Long,
+ minDiff: Long,
+ diffList: Seq[Long] = Nil,
+ diffExtensionList: Seq[Long] = Nil,
+ validDiffCount: Int = 0,
+ previousTime: Long = System.currentTimeMillis()
+ ): Iterable[Long] = {
+ if (iter.hasNext && validDiffCount < maxValidDiffCount) {
+ val nextTime = iter.next()
+ val delay = previousTime - nextTime
+ if (delay < maxDiff) {
+ if (delay <= minDiff) {
+ qualifiedTimeDifferences(
+ iter,
+ maxValidDiffCount,
+ maxDiff,
+ minDiff,
+ diffList ++ (diffExtensionList :+ delay),
+ Nil,
+ validDiffCount + 1,
+ nextTime
+ )
+ } else {
+ qualifiedTimeDifferences(
+ iter,
+ maxValidDiffCount,
+ maxDiff,
+ minDiff,
+ diffList,
+ diffExtensionList :+ delay,
+ validDiffCount,
+ nextTime
+ )
+ }
+ } else {
+ diffList
+ }
+ } else {
+ diffList
+ }
+ }
+
+ /**
+ * From a list of values, isolate all values less than than a test value.
+ * @param list list of values
+ * @param testValue test value that all valid values must be less than
+ * @param mercy initial mercy value that values may be tested for being less than the test value
+ * @return list of values less than the test value, including mercy
+ */
+ private def takeWhileLess(list: Iterable[Long], testValue: Long, mercy: Long): Iterable[Long] = {
var onGoingMercy: Long = mercy
- list.takeWhile { time =>
- if (time < testValue) {
+ list.filter { value =>
+ if (value < testValue) {
true
- } else if (time - onGoingMercy - 1 < testValue) {
- onGoingMercy = math.ceil(onGoingMercy * 0.65).toLong
+ } else if (value - onGoingMercy < testValue) {
+ //mercy is reduced every time it is utilized to find a valid value
+ onGoingMercy = math.ceil(onGoingMercy * 0.8f).toLong
true
} else {
false
@@ -93,6 +299,15 @@ object KillAssists {
}
}
+ /**
+ * Modify a base experience value to consider additional reasons for points.
+ * @param killer player that delivers the interaction that reduces health to zero
+ * @param victim player to which the final interaction has reduced health to zero
+ * @param history chronology of activity the game considers noteworthy
+ * @return the value of the kill in what the game called "battle experience points"
+ * @see `BattleRank.withExperience`
+ * @see `Support.baseExperience`
+ */
private def calculateExperience(
killer: PlayerSource,
victim: PlayerSource,
@@ -128,35 +343,20 @@ object KillAssists {
}
}
- private[exp] def rewardThisPlayerDeath(
- victim: PlayerSource,
- lastDamage: Option[DamageResult],
- history: Iterable[InGameActivity],
- eventBus: ActorRef
- ): Unit = {
- val shortHistory = limitHistoryToThisLife(history.toList)
- val everyone = determineKiller(lastDamage, shortHistory) match {
- case Some((result, killer: PlayerSource)) =>
- val assists = collectKillAssistsForPlayer(victim, shortHistory, Some(killer))
- val fullBep = calculateExperience(killer, victim, shortHistory)
- val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
- case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
- }.toSeq
- (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
-
- case _ =>
- val assists = collectKillAssistsForPlayer(victim, shortHistory, None)
- val fullBep = Support.baseExperience(victim, shortHistory)
- val hitSquad = assists.map {
- case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
- }.toSeq
- (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
- }
- everyone.foreach { case (p, kda) =>
- eventBus ! AvatarServiceMessage(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda))
- }
- }
-
+ /**
+ * Evaluate chronologic in-game activity within a scope of history and
+ * isolate the interactions that lead to one player dying.
+ * Factor in interactions that would have the dying player attempt to resist death, if only for a short while longer.
+ * @param victim player to which the final interaction has reduced health to zero
+ * @param history chronology of activity the game considers noteworthy
+ * @param killerOpt optional player that delivers the interaction that reduces the `victim's` health to zero
+ * @return summary of the interaction in terms of players, equipment activity, and experience
+ * @see `armorDamageContributors`
+ * @see `collectKillAssists`
+ * @see `healthDamageContributors`
+ * @see `Support.allocateContributors`
+ * @see `Support.onlyOriginalAssistEntries`
+ */
private def collectKillAssistsForPlayer(
victim: PlayerSource,
history: List[InGameActivity],
@@ -170,7 +370,7 @@ object KillAssists {
healthAssists.remove(0L)
healthAssists.remove(victim.CharId)
killerOpt.map { killer => healthAssists.remove(killer.CharId) }
- if (Support.wasEverAMax(victim, history)) { //a cardinal sin
+ if (Support.wasEverAMax(victim, history)) {
val armorAssists = collectKillAssists(
victim,
history,
@@ -185,11 +385,19 @@ object KillAssists {
}
}
+ /**
+ * Analyze history based on a discriminating function and format the output.
+ * @param victim player to which the final interaction has reduced health to zero
+ * @param history chronology of activity the game considers noteworthy
+ * @param func mechanism for discerning particular interactions and building a narrative around their history;
+ * tallies all activity by a certain player using certain equipment
+ * @return summary of the interaction in terms of players, equipment activity, and experience
+ */
private def collectKillAssists(
- victim: SourceEntry,
- history: List[InGameActivity],
- func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
- ): mutable.LongMap[ContributionStatsOutput] = {
+ victim: SourceEntry,
+ history: List[InGameActivity],
+ func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
+ ): mutable.LongMap[ContributionStatsOutput] = {
val assists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
val total = assists.values.foldLeft(0f)(_ + _.total)
val output = assists.map { case (id, kda) =>
@@ -199,14 +407,28 @@ object KillAssists {
output
}
+ /**
+ * In relation to a target player's health,
+ * build a secondary chronology of how the health value is affected per interaction and
+ * maintain a quantitative record of that activity in relation to the other players and their equipment.
+ * @param history chronology of activity the game considers noteworthy
+ * @param faction empire to target
+ * @param participants quantitative record of activity in relation to the other players and their equipment
+ * @return chronology of how the health value is affected per interaction
+ * @see `contributeWithDamagingActivity`
+ * @see `contributeWithRecoveryActivity`
+ * @see `RevivingActivity`
+ */
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)
- * key - character identifier,
- * value - current damage contribution */
+ 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)
+ key - character identifier,
+ value - current damage contribution
+ */
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.foreach {
case d: DamagingActivity if d.health > 0 =>
@@ -220,14 +442,27 @@ object KillAssists {
inOrder
}
+ /**
+ * In relation to a target player's armor,
+ * build a secondary chronology of how the armor value is affected per interaction and
+ * maintain a quantitative record of that activity in relation to the other players and their equipment.
+ * @param history chronology of activity the game considers noteworthy
+ * @param faction empire to target
+ * @param participants quantitative record of activity in relation to the other players and their equipment
+ * @return chronology of how the armor value is affected per interaction
+ * @see `contributeWithDamagingActivity`
+ * @see `contributeWithRecoveryActivity`
+ */
private def armorDamageContributors(
- history: List[InGameActivity],
- faction: PlanetSideEmpire.Value,
- participants: mutable.LongMap[ContributionStats]
- ): Seq[(Long, Int)] = {
- /** damage as it is measured in order (with heal-countered damage eliminated)
- * key - character identifier,
- * value - current damage contribution */
+ 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)
+ key - character identifier,
+ value - current damage contribution
+ */
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.foreach {
case d: DamagingActivity if d.amount - d.health > 0 =>
@@ -239,6 +474,15 @@ object KillAssists {
inOrder
}
+ /**
+ * Analyze damaging activity for quantitative records.
+ * @param activity a particular in-game activity that negative affects a player's health
+ * @param faction empire to target
+ * @param amount value
+ * @param participants quantitative record of activity in relation to the other players and their equipment
+ * @param order chronology of how the armor value is affected per interaction
+ * @return chronology of how the armor value is affected per interaction
+ */
private def contributeWithDamagingActivity(
activity: DamagingActivity,
faction: PlanetSideEmpire.Value,
@@ -259,6 +503,16 @@ object KillAssists {
)
}
+ /**
+ * Analyze damaging activity for quantitative records.
+ * @param userOpt optional player for the quantitative record
+ * @param wepid weapon for the quantitative record
+ * @param faction empire to target
+ * @param amount value
+ * @param participants quantitative record of activity in relation to the other players and their equipment
+ * @param order chronology of how the armor value is affected per interaction
+ * @return chronology of how the armor value is affected per interaction
+ */
private[exp] def contributeWithDamagingActivity(
userOpt: Option[PlayerSource],
wepid: Int,
@@ -331,6 +585,13 @@ object KillAssists {
}
}
+ /**
+ * Analyze recovery activity for quantitative records.
+ * @param amount value
+ * @param participants quantitative record of activity in relation to the other players and their equipment
+ * @param order chronology of how the armor value is affected per interaction
+ * @return chronology of how the armor value is affected per interaction
+ */
private[exp] def contributeWithRecoveryActivity(
amount: Int,
participants: mutable.LongMap[ContributionStats],
diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala
index 748c3f520..c315429f6 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala
@@ -4,17 +4,36 @@ package net.psforever.objects.zones.exp
import akka.actor.ActorRef
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.avatar.scoring.{Kill, SupportActivity}
-import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
-import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry, SourceUniqueness, VehicleSource}
-import net.psforever.objects.vital.{Contribution, InGameActivity, RevivingActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleCargoMountChange, VehicleDismountActivity, VehicleMountActivity, VehiclePassengerMountChange}
+import net.psforever.objects.sourcing.{BuildingSource, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, VehicleSource}
+import net.psforever.objects.vital.{Contribution, HealFromTerminal, InGameActivity, RepairFromTerminal, RevivingActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleDismountActivity, VehicleMountActivity}
import net.psforever.objects.vital.projectile.ProjectileReason
-import net.psforever.objects.zones.exp.rec.{ArmorRecoveryExperienceContributionProcess, CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess}
+import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideEmpire, Vector3}
import scala.collection.mutable
+/**
+ * Kills and assists consider the target, in an exchange of projectiles from the weapons of players towards the target.
+ * Contributions consider actions of other allied players towards the player who is the source of the projectiles.
+ * These actions are generally positive for the player.
+ * @see `Contribution`
+ * @see `ContributionStats`
+ * @see `ContributionStatsOutput`
+ * @see `DamagingActivity`
+ * @see `GlobalDefinitions`
+ * @see `HealingActivity`
+ * @see `InGameActivity`
+ * @see `InGameHistory`
+ * @see `Kill`
+ * @see `PlayerSource`
+ * @see `RepairingActivity`
+ * @see `SourceEntry`
+ * @see `SourceUniqueness`
+ * @see `VehicleSource`
+ */
object KillContributions {
+ /** the object type ids of various game elements that are recognized for "stat recovery" */
final val RecoveryItems: Seq[Int] = {
import net.psforever.objects.GlobalDefinitions._
Seq(
@@ -32,6 +51,26 @@ object KillContributions {
).collect { _.ObjectId }
} //TODO currently includes things that are not typical items but are used for expressing contribution implements
+ /** cached for empty collection returns; please do not add anything to it */
+ private val emptyMap: mutable.LongMap[ContributionStats] = mutable.LongMap.empty[ContributionStats]
+
+ /**
+ * Primary landing point for calculating the rewards given for helping one player kill another player.
+ * Rewards in the form of "support experience points" are given
+ * to all allied players that have somehow been involved with the player who killed another player.
+ * @param target player that delivers the interaction that killed another player;
+ * history is purportedly composed of events that have happened to this player within a time frame
+ * @param history chronology of activity the game considers noteworthy
+ * @param kill the in-game event that maintains information about the other player's death;
+ * originates from prior statistical management normally
+ * @param bep battle experience points to be referenced for support experience points conversion
+ * @param eventBus where to send the results of the experience determination(s)
+ * @see `ActorRef`
+ * @see `AvatarAction.UpdateKillsDeathsAssists`
+ * @see `AvatarServiceMessage`
+ * @see `rewardTheseSupporters`
+ * @see `SupportActivity`
+ */
private[exp] def rewardTheseSupporters(
target: PlayerSource,
history: Iterable[InGameActivity],
@@ -39,12 +78,56 @@ object KillContributions {
bep: Long,
eventBus: ActorRef
): Unit = {
- //setup
- val killTime = kill.time.toDate.getTime
+ val victim = kill.victim
+ //take the output and transform that into contribution distribution data
+ rewardTheseSupporters(target, history, kill, bep)
+ .foreach { case (charId, ContributionStatsOutput(player, weapons, exp)) =>
+ eventBus ! AvatarServiceMessage(
+ player.Name,
+ AvatarAction.UpdateKillsDeathsAssists(charId, SupportActivity(victim, weapons, exp.toLong))
+ )
+ }
+ }
+
+ /**
+ * Primary innards for calculating the rewards given for helping one player kill another player.
+ * @param target player that delivers the interaction that killed another player;
+ * history is purportedly composed of events that have happened to this player within a time frame
+ * @param history chronology of activity the game considers noteworthy
+ * @param kill the in-game event that maintains information about the other player's death;
+ * originates from prior statistical management normally
+ * @param bep battle experience points to be referenced for support experience points conversion
+ * returns list of user unique identifiers and
+ * a summary of the interaction in terms of players, equipment activity, and experience
+ * @see `ActorRef`
+ * @see `additionalContributionSources`
+ * @see `AvatarAction.UpdateKillsDeathsAssists`
+ * @see `AvatarServiceMessage`
+ * @see `CombinedHealthAndArmorContributionProcess`
+ * @see `composeContributionOutput`
+ * @see `initialScoring`
+ * @see `KillAssists.calculateMenace`
+ * @see `limitHistoryToThisLife`
+ * @see `rewardTheseSupporters`
+ * @see `SupportActivity`
+ */
+ private[exp] def rewardTheseSupporters(
+ target: PlayerSource,
+ history: Iterable[InGameActivity],
+ kill: Kill,
+ bep: Long
+ ): Iterable[(Long, ContributionStatsOutput)] = {
val faction = target.Faction
+ /*
+ divide into applicable time periods - long for 10 minutes and short 5 minutes;
+ these two periods represent passes over the in-game history to evaluate statistic modification events;
+ the short time period should stand on its own, but should also be represented in the long time period;
+ more players should be rewarded if one qualifies for the longer time period's evaluation
+ */
//divide by applicable time periods (long=10minutes, short=5minutes)
- val shortPeriod = killTime - 300000L
- val (contributions, (longHistory, shortHistory)) = {
+ val (contributions, (shortHistory, longHistory)) = {
+ val killTime = kill.time.toDate.getTime
+ val shortPeriod = killTime - 300000L
val (contrib, onlyHistory) = history.partition { _.isInstanceOf[Contribution] }
(
contrib
@@ -53,45 +136,53 @@ object KillContributions {
limitHistoryToThisLife(onlyHistory.toList, killTime).partition { _.time > shortPeriod }
)
}
- //sort by applicable time periods, as long as the longer period is represented by activity
+ //events that are older than 5 minutes are enough to prove one has been alive that long
val empty = mutable.ListBuffer[SourceUniqueness]()
- val otherContributionCalculations = contributionScoringAndCulling(faction, kill, contributions, bep)(_, _, _)
- val finalContributions = if (longHistory.nonEmpty && KillAssists.calculateMenace(target) > 2) {
+ empty.addOne(target.unique)
+ val otherContributionCalculations = additionalContributionSources(faction, kill, contributions)(_, _, _)
+ if (longHistory.nonEmpty && KillAssists.calculateMenace(target) > 3) {
+ //long and short history
val longContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
val shortContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Seq(longContributionProcess))
longContributionProcess.submit(longHistory)
shortContributionProcess.submit(shortHistory)
- val longContributionEntries = otherContributionCalculations(longHistory, longContributionProcess.output(), empty)
- val shortContributionEntries = otherContributionCalculations(shortHistory, shortContributionProcess.output(), empty)
+ val longContributionEntries = otherContributionCalculations(
+ longHistory,
+ initialScoring(longContributionProcess.output(), bep.toFloat),
+ empty
+ )
+ val shortContributionEntries = otherContributionCalculations(
+ shortHistory,
+ initialScoring(shortContributionProcess.output(), bep.toFloat),
+ empty
+ )
longContributionEntries.remove(target.CharId)
longContributionEntries.remove(kill.victim.CharId)
shortContributionEntries.remove(target.CharId)
shortContributionEntries.remove(kill.victim.CharId)
+ //combine
(longContributionEntries ++ shortContributionEntries)
.toSeq
.distinctBy(_._2.player.unique)
- .map { case (_, stats) =>
- composeContributionOutput(stats.player, shortContributionEntries, longContributionEntries, bep)
+ .flatMap { case (_, stats) =>
+ composeContributionOutput(stats.player.CharId, shortContributionEntries, longContributionEntries, bep)
}
} else {
+ //short history only
val contributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
contributionProcess.submit(shortHistory)
- val contributionEntries = otherContributionCalculations(shortHistory, contributionProcess.output(), empty)
+ val contributionEntries = otherContributionCalculations(
+ shortHistory,
+ initialScoring(contributionProcess.output(), bep.toFloat),
+ empty
+ )
contributionEntries.remove(target.CharId)
contributionEntries.remove(kill.victim.CharId)
contributionEntries
- .map { case (_, stats) =>
- composeContributionOutput(stats.player, contributionEntries, contributionEntries, bep)
+ .flatMap { case (_, stats) =>
+ composeContributionOutput(stats.player.CharId, contributionEntries, contributionEntries, bep)
}
}
- //take the output and transform that into contribution distribution data
- val victim = kill.victim
- finalContributions.foreach { case (charId, ContributionStatsOutput(player, weapons, exp)) =>
- eventBus ! AvatarServiceMessage(
- player.Name,
- AvatarAction.UpdateKillsDeathsAssists(charId, SupportActivity(victim, weapons, exp.toLong))
- )
- }
}
/**
@@ -104,110 +195,173 @@ object KillContributions {
* @return the potentially truncated history
*/
private def limitHistoryToThisLife(history: List[InGameActivity], eventTime: Long): List[InGameActivity] = {
- val longLimit: Long = eventTime - 600000L
- history.collect {
- case event if event.time < eventTime && event.time >= longLimit => event
- }
+ limitHistoryToThisLife(history, eventTime, eventTime - 600000L)
}
- private def contributionScoringAndCulling(
+ /**
+ * Only historical activity that falls within the valid period matters.
+ * @param history the original history
+ * @param eventTime from which time to start counting backwards
+ * @param startTime after which time to start counting forwards
+ * @return the potentially truncated history
+ */
+ private def limitHistoryToThisLife(
+ history: List[InGameActivity],
+ eventTime: Long,
+ startTime: Long
+ ): List[InGameActivity] = {
+ history.filter { event => event.time <= eventTime && event.time >= startTime }
+ }
+
+ /**
+ * Manipulate contribution scores that have been evaluated up to this point
+ * for a fixed combination of users and different implements
+ * by replacing the score using a flat predictable numerical evaluation.
+ * @param existingParticipants quantitative record of activity in relation to the other players and their equipment
+ * @param bep battle experience point
+ * @return quantitative record of activity in relation to the other players and their equipment
+ */
+ private def initialScoring(
+ existingParticipants: mutable.LongMap[ContributionStats],
+ bep: Float
+ ): mutable.LongMap[ContributionStats] = {
+ //the scoring up to this point should be rate based, but is not perfectly useful for us
+ existingParticipants.map { case (id, stat) =>
+ val newWeaponStats = stat.weapons.map { weaponStat =>
+ weaponStat.copy(contributions = 10f + weaponStat.shots.toFloat + 0.05f * bep)
+ }
+ existingParticipants.put(id, stat.copy(weapons = newWeaponStats))
+ }
+ existingParticipants
+ }
+
+ /**
+ * na
+ * @param faction empire to target
+ * @param kill the in-game event that maintains information about the other player's death;
+ * originates from prior statistical management normally
+ * @param contributions na
+ * @param history chronology of activity the game considers noteworthy
+ * @param existingParticipants quantitative record of activity in relation to the other players and their equipment
+ * @param excludedTargets do not repeat analysis on entities associated with these tokens
+ * @return quantitative record of activity in relation to the other players and their equipment
+ * @see `contributeWithRevivalActivity`
+ * @see `contributeWithTerminalActivity`
+ * @see `contributeWithVehicleTransportActivity`
+ * @see `contributeWithVehicleCargoTransportActivity`
+ * @see `contributeWithKillWhileMountedActivity`
+ */
+ private def additionalContributionSources(
faction: PlanetSideEmpire.Value,
kill: Kill,
- contributions: Map[SourceUniqueness, List[InGameActivity]],
- bep: Long
+ contributions: Map[SourceUniqueness, List[InGameActivity]]
)
(
history: List[InGameActivity],
- contributionEntries: mutable.LongMap[ContributionStats],
+ existingParticipants: mutable.LongMap[ContributionStats],
excludedTargets: mutable.ListBuffer[SourceUniqueness]
): mutable.LongMap[ContributionStats] = {
- contributionEntries.map { case (id, stat) =>
- val newWeaponStats = stat.weapons.map { weaponStat =>
- weaponStat.copy(contributions = (weaponStat.shots + (bep * weaponStat.amount / stat.total)).toFloat)
- }
- contributionEntries.put(id, stat.copy(weapons = newWeaponStats))
- }
- contributeWithKillWhileMountedActivity(faction, kill, history, contributionEntries, excludedTargets)
- contributeWithRevivalActivity(history, contributionEntries)
- contributeWithVehicleTransportActivity(history, contributionEntries)
- contributeWithVehicleCargoTransportActivity(history, contributionEntries)
- contributeWithTerminalActivity(faction, history, contributions, contributionEntries, excludedTargets)
- contributionEntries.remove(0)
- contributionEntries
+ contributeWithRevivalActivity(history, existingParticipants)
+ contributeWithTerminalActivity(faction, history, contributions, excludedTargets, existingParticipants)
+ contributeWithVehicleTransportActivity(history, faction, contributions, excludedTargets, existingParticipants)
+ contributeWithVehicleCargoTransportActivity(history, faction, contributions, excludedTargets, existingParticipants)
+ contributeWithKillWhileMountedActivity(kill, faction, contributions, excludedTargets, existingParticipants)
+ existingParticipants.remove(0)
+ existingParticipants
}
+ /**
+ * Gather and reward specific in-game equipment use activity.
+ * If the player who performed the killing interaction is mounted in something,
+ * determine if the mount is has been effected by previous in-game interactions
+ * that resulted in positive stat maintenance or development.
+ * Also, reward the owner, if an owner exists, for providing the mount.
+ * @param kill the in-game event that maintains information about the other player's death
+ * @param faction empire to target
+ * @param contributions mapping between external entities
+ * the target has interacted with in the form of in-game activity
+ * and history related to the time period in which the interaction ocurred
+ * @param excludedTargets if a potential target is listed here already, skip processing it
+ * @param out quantitative record of activity in relation to the other players and their equipment
+ * @see `combineStatsInto`
+ * @see `extractContributionsForMachineByTarget`
+ */
private def contributeWithKillWhileMountedActivity(
- faction: PlanetSideEmpire.Value,
kill: Kill,
- history: List[InGameActivity],
- participants: mutable.LongMap[ContributionStats],
- excludedTargets: mutable.ListBuffer[SourceUniqueness]
- ): List[InGameActivity] = {
+ faction: PlanetSideEmpire.Value,
+ contributions: Map[SourceUniqueness, List[InGameActivity]],
+ excludedTargets: mutable.ListBuffer[SourceUniqueness],
+ out: mutable.LongMap[ContributionStats]
+ ): Unit = {
+ val eventTime = kill.time.toDate.getTime
(kill
.info
.interaction
.cause match {
- case p: ProjectileReason => p.projectile.mounted_in.map { _._2 }
+ case p: ProjectileReason => p.projectile.mounted_in.map { case (_, src) => Some((src, p.projectile.owner)) }
case _ => None
})
.collect {
- case mount: VehicleSource if !excludedTargets.contains(mount.unique) =>
- //repairs
- val contributions = history
- .collect { case Contribution(unique, entries) if mount.unique == unique => (unique, entries) }
- .toMap[SourceUniqueness, List[InGameActivity]]
- contributions
- .foreach {
- case (_, localHistory) =>
- val process = new ArmorRecoveryExperienceContributionProcess(faction, contributions, excludedTargets :+ mount.unique)
- process.submit(localHistory)
- cullContributorImplements(process.output()).foreach {
- case (id, stat) =>
- val killExperience = kill.experienceEarned
- val newWeapons = stat.weapons.map { weapon =>
- val shots = weapon.shots
- val amount = weapon.amount
- val exp = math.min(0.5d * killExperience, shots + math.ceil(math.log(amount))).toFloat
- weapon.copy(contributions = exp)
- }.toList
- contributeWithCombinedActivity(
- id,
- newWeapons,
- stat.player,
- stat.amount,
- stat.total,
- stat.shots,
- stat.time,
- participants
- )
- }
- }
- //vehicle owner
- extractContributionsForEntityByUser(faction, mount, contributions, participants, excludedTargets)
+ case Some((mount: VehicleSource, attacker: PlayerSource)) if !excludedTargets.contains(mount.unique) =>
mount.owner
.collect {
+ case owner if owner == attacker.unique =>
+ //owner is gunner; reward only repairs
+ excludedTargets.addOne(owner)
+ owner
case owner =>
+ //gunner is different from owner; reward driver and repairs
+ excludedTargets.addOne(owner)
+ excludedTargets.addOne(attacker.unique)
val time = kill.time.toDate.getTime
- participants.getOrElseUpdate(
- owner.CharId,
- ContributionStats(
- owner,
- Seq(WeaponStats(DriverAssist(mount.Definition.ObjectId), 1, 1, time, 10f)),
- 1,
- 1,
- 1,
- time
+ combineStatsInto(
+ out,
+ (
+ owner.charId,
+ ContributionStats(
+ PlayerSource(owner, mount.Position),
+ Seq(WeaponStats(DriverAssist(mount.Definition.ObjectId), 1, 1, time, 10f)),
+ 1,
+ 1,
+ 1,
+ time
+ )
)
)
+ owner
}
+ combineStatsInto(
+ out,
+ extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets)
+ )
+ case Some((mount: TurretSource, _: PlayerSource)) if !excludedTargets.contains(mount.unique) =>
+ combineStatsInto(
+ out,
+ extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets)
+ )
}
- history
}
+ /**
+ * Gather and reward specific in-game equipment use activity.
+ * na
+ * @param history chronology of activity the game considers noteworthy
+ * @param faction empire to target
+ * @param contributions mapping between external entities
+ * the target has interacted with in the form of in-game activity
+ * and history related to the time period in which the interaction ocurred
+ * @param excludedTargets if a potential target is listed here already, skip processing it
+ * @param out quantitative record of activity in relation to the other players and their equipment
+ * @see `combineStatsInto`
+ * @see `extractContributionsForMachineByTarget`
+ */
private def contributeWithVehicleTransportActivity(
history: List[InGameActivity],
- participants: mutable.LongMap[ContributionStats]
- ): List[InGameActivity] = {
+ faction: PlanetSideEmpire.Value,
+ contributions: Map[SourceUniqueness, List[InGameActivity]],
+ excludedTargets: mutable.ListBuffer[SourceUniqueness],
+ out: mutable.LongMap[ContributionStats]
+ ): Unit = {
/*
collect the dismount activity of all vehicles from which this player is not the owner
make certain all dismount activity can be paired with a mounting activity
@@ -215,12 +369,11 @@ object KillContributions {
*/
val dismountActivity = history
.collect {
- case inAndOut: VehiclePassengerMountChange
- if !inAndOut.vehicle.owner.contains(inAndOut.player) => inAndOut
+ case out: VehicleDismountActivity
+ if !out.vehicle.owner.contains(out.player.unique) && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
}
- .grouped(2)
.collect {
- case List(in: VehicleMountActivity, out: VehicleDismountActivity)
+ case (in: VehicleMountActivity, out: VehicleDismountActivity)
if in.vehicle.unique == out.vehicle.unique &&
out.vehicle.Faction == out.player.Faction &&
(in.vehicle.Definition == GlobalDefinitions.router || {
@@ -239,38 +392,62 @@ object KillContributions {
(!sameZone && (distanceMoved > 10000f || timeSpent >= 120000))
}) =>
out
- }.toList
+ }
//apply
dismountActivity
- .groupBy { a => a.vehicle.owner }
- .collect { case (Some(owner), dismountsFromVehicle) =>
- val numberOfDismounts = dismountsFromVehicle.size
- contributeWithCombinedActivity(
- owner.CharId,
- dismountsFromVehicle.map { act =>
- val statContext = act.vehicle.Definition match {
- case v @ GlobalDefinitions.router =>
- RouterKillAssist(v.ObjectId)
- case v =>
- HotDropKillAssist(v.ObjectId, 0)
- }
- WeaponStats(statContext, 0, numberOfDismounts, act.time, 15f)
- },
- dismountsFromVehicle.head.vehicle.owner.get,
- amount = 0,
- total = 0,
- numberOfDismounts,
- dismountsFromVehicle.maxBy(_.time).time,
- participants
+ .groupBy { _.vehicle }
+ .collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty =>
+ val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
+ val equipmentUseContext = mount.Definition match {
+ case v @ GlobalDefinitions.router =>
+ RouterKillAssist(v.ObjectId)
+ case v =>
+ HotDropKillAssist(v.ObjectId, 0)
+ }
+ val statContext = Seq(WeaponStats(equipmentUseContext, 0, dismountsFromVehicle.size, dismountsFromVehicle.maxBy(_.time).time, 15f))
+ combineStatsInto(
+ out,
+ (
+ promotedOwner.CharId,
+ ContributionStats(promotedOwner, statContext, 1, 1, 1, statContext.head.time)
+ )
)
+ contributions.get(mount.unique).collect {
+ case list =>
+ val mountHistory = dismountsFromVehicle
+ .flatMap { event =>
+ val eventTime = event.time
+ val startTime = event.pairedEvent.get.time - 600000L
+ limitHistoryToThisLife(list, eventTime, startTime)
+ }
+ .distinctBy(_.time)
+ combineStatsInto(
+ out,
+ extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets)
+ )
+ }
}
- dismountActivity
}
+ /**
+ * Gather and reward specific in-game equipment use activity.
+ * na
+ * @param faction empire to target
+ * @param contributions mapping between external entities
+ * the target has interacted with in the form of in-game activity
+ * and history related to the time period in which the interaction ocurred
+ * @param excludedTargets if a potential target is listed here already, skip processing it
+ * @param out quantitative record of activity in relation to the other players and their equipment
+ * @see `combineStatsInto`
+ * @see `extractContributionsForMachineByTarget`
+ */
private def contributeWithVehicleCargoTransportActivity(
history: List[InGameActivity],
- participants: mutable.LongMap[ContributionStats]
- ): List[InGameActivity] = {
+ faction: PlanetSideEmpire.Value,
+ contributions: Map[SourceUniqueness, List[InGameActivity]],
+ excludedTargets: mutable.ListBuffer[SourceUniqueness],
+ out: mutable.LongMap[ContributionStats]
+ ): Unit = {
/*
collect the dismount activity of all vehicles from which this player is not the owner
make certain all dismount activity can be paired with a mounting activity
@@ -278,11 +455,11 @@ object KillContributions {
*/
val dismountActivity = history
.collect {
- case inAndOut: VehicleCargoMountChange if inAndOut.vehicle.owner.nonEmpty => inAndOut
+ case out: VehicleCargoDismountActivity
+ if out.vehicle.owner.nonEmpty && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
}
- .grouped(2)
.collect {
- case List(in: VehicleCargoMountActivity, out: VehicleCargoDismountActivity)
+ case (in: VehicleCargoMountActivity, out: VehicleCargoDismountActivity)
if in.vehicle.unique == out.vehicle.unique &&
out.vehicle.Faction == out.cargo.Faction &&
(in.vehicle.Definition == GlobalDefinitions.router || {
@@ -291,244 +468,405 @@ object KillContributions {
timeSpent >= 210000 /* 3:30 */ || distanceMoved > 640000f
}) =>
out
- }.toList
+ }
//apply
dismountActivity
- .groupBy { a => a.vehicle.owner }
- .collect { case (Some(owner), dismountsFromVehicle) =>
- val numberOfDismounts = dismountsFromVehicle.size
- contributeWithCombinedActivity(
- owner.CharId,
- dismountsFromVehicle.map { act =>
- WeaponStats(
- HotDropKillAssist(act.vehicle.Definition.ObjectId, act.cargo.Definition.ObjectId),
- 0,
- numberOfDismounts,
- act.time,
- 15f
+ .groupBy { _.cargo }
+ .collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty =>
+ val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
+ val mountId = mount.Definition.ObjectId
+ dismountsFromVehicle
+ .groupBy(_.vehicle)
+ .map { case (vehicle, events) =>
+ (vehicle, vehicle.owner, Seq(WeaponStats(HotDropKillAssist(vehicle.Definition.ObjectId, mountId), 0, events.size, events.maxBy(_.time).time, 15f)))
+ }
+ .collect { case (vehicle, Some(owner), statContext) =>
+ combineStatsInto(
+ out,
+ (
+ owner.charId,
+ ContributionStats(promotedOwner, statContext, 1, 1, 1, statContext.head.time)
+ )
)
- },
- dismountsFromVehicle.head.vehicle.owner.get,
- amount = 0,
- total = 0,
- numberOfDismounts,
- dismountsFromVehicle.maxBy(_.time).time,
- participants
- )
+ contributions.get(mount.unique).collect {
+ case list =>
+ val mountHistory = dismountsFromVehicle
+ .flatMap { event =>
+ val eventTime = event.time
+ val startTime = event.pairedEvent.get.time - 600000L
+ limitHistoryToThisLife(list, eventTime, startTime)
+ }
+ .distinctBy(_.time)
+ combineStatsInto(
+ out,
+ extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets)
+ )
+ }
+ contributions.get(vehicle.unique).collect {
+ case list =>
+ val carrierHistory = dismountsFromVehicle
+ .flatMap { event =>
+ val eventTime = event.time
+ val startTime = event.pairedEvent.get.time - 600000L
+ limitHistoryToThisLife(list, eventTime, startTime)
+ }
+ .distinctBy(_.time)
+ combineStatsInto(
+ out,
+ extractContributionsForMachineByTarget(vehicle, faction, carrierHistory, contributions, excludedTargets)
+ )
+ }
+ }
}
- dismountActivity
- }
-
- private def contributeWithRevivalActivity(
- history: List[InGameActivity],
- participants: mutable.LongMap[ContributionStats]
- ): List[InGameActivity] = {
- val shortHistory = history.collect { case rev: RevivingActivity => rev }
- shortHistory
- .groupBy(_.user.CharId)
- .foreach { case (id, revivesByThisPlayer) =>
- val numberOfRevives = revivesByThisPlayer.size
- contributeWithCombinedActivity(
- id,
- revivesByThisPlayer.map { stat =>
- WeaponStats(ReviveKillAssist(stat.equipment.ObjectId), 100, 1, stat.time, 25f)
- },
- revivesByThisPlayer.head.user,
- amount = 100 * numberOfRevives,
- total = 100 * numberOfRevives,
- numberOfRevives,
- revivesByThisPlayer.maxBy(_.time).time,
- participants
- )
- }
- shortHistory
}
+ /**
+ * Gather and reward specific in-game equipment use activity.
+ * na
+ * @param faction empire to target
+ * @param contributions mapping between external entities
+ * the target has interacted with in the form of in-game activity
+ * and history related to the time period in which the interaction ocurred
+ * @param excludedTargets if a potential target is listed here already, skip processing it
+ * @param out quantitative record of activity in relation to the other players and their equipment
+ * @see `AmsResupplyKillAssist`
+ * @see `BuildingSource`
+ * @see `combineStatsInto`
+ * @see `contributeWithTerminalActivity`
+ * @see `extractContributionsForMachineByTarget`
+ * @see `HackKillAssist`
+ * @see `HealFromTerminal`
+ * @see `LodestarRearmKillAssist`
+ * @see `RepairFromTerminal`
+ * @see `RepairKillAssist`
+ * @see `TerminalUsedActivity`
+ */
private def contributeWithTerminalActivity(
faction: PlanetSideEmpire.Value,
history: List[InGameActivity],
contributions: Map[SourceUniqueness, List[InGameActivity]],
- participants: mutable.LongMap[ContributionStats],
- excludedTargets: mutable.ListBuffer[SourceUniqueness]
- ): List[InGameActivity] = {
- contributeWithTerminalActivity(
- history.collect { case t: TerminalUsedActivity => (t, t.terminal, t.terminal.hacked) },
- faction,
- contributions,
- participants,
- excludedTargets
- )
- }
-
- private[exp] def contributeWithTerminalActivity(
- data: Seq[(InGameActivity, AmenitySource, Option[HackInfo])],
- faction: PlanetSideEmpire.Value,
- contributions: Map[SourceUniqueness, List[InGameActivity]],
- participants: mutable.LongMap[ContributionStats],
- excludedTargets: mutable.ListBuffer[SourceUniqueness]
- ): List[InGameActivity] = {
- data.collect {
- case (t, terminal, Some(info)) if terminal.Faction != faction =>
- /*
- if the terminal has been hacked,
- and the original terminal does not align with our own faction,
- then the support must be reported as a hack;
- if we are the same faction as the terminal, then the hacked condition is irrelevant
- */
- participants.getOrElseUpdate(
- info.player.CharId,
- ContributionStats(
- info.player,
- Seq(WeaponStats(HackKillAssist(terminal.Definition.ObjectId), 0, 1, t.time, 10f)),
- 0,
- 0,
- 1,
- t.time
- )
- )
- t
- case (t, terminal, _) =>
- val (equipmentUseContext, ownerOpt) = terminal.installation match {
- case v: VehicleSource =>
- terminal.Definition match {
- case GlobalDefinitions.order_terminala =>
- extractContributionsForEntityByUser(faction, v, contributions, participants, excludedTargets)
- (AmsResupplyKillAssist(terminal.Definition.ObjectId), v.owner)
- case GlobalDefinitions.order_terminalb =>
- extractContributionsForEntityByUser(faction, v, contributions, participants, excludedTargets)
- (AmsResupplyKillAssist(terminal.Definition.ObjectId), v.owner)
- case GlobalDefinitions.lodestar_repair_terminal =>
- extractContributionsForEntityByUser(faction, v, contributions, participants, excludedTargets)
- (RepairKillAssist(terminal.Definition.ObjectId, v.Definition.ObjectId), v.owner)
- case GlobalDefinitions.bfr_rearm_terminal =>
- extractContributionsForEntityByUser(faction, v, contributions, participants, excludedTargets)
- (LodestarRearmKillAssist(terminal.Definition.ObjectId), v.owner)
- case GlobalDefinitions.multivehicle_rearm_terminal =>
- extractContributionsForEntityByUser(faction, v, contributions, participants, excludedTargets)
- (LodestarRearmKillAssist(terminal.Definition.ObjectId), v.owner)
- case _ =>
- (NoUse(0), None)
- }
- case _ =>
- (NoUse(0), None)
- }
- ownerOpt.collect {
- owner =>
- participants.getOrElseUpdate(
- owner.CharId,
- ContributionStats(
- owner,
- Seq(WeaponStats(equipmentUseContext, 0, 1, t.time, 10f)),
- 0,
- 0,
- 1,
- t.time
- )
- )
- }
- t
- }.toList
- }
-
- private def extractContributionsForEntityByUser(
- faction: PlanetSideEmpire.Value,
- target: SourceEntry,
- contributions: Map[SourceUniqueness, List[InGameActivity]],
- contributionsBy: mutable.LongMap[ContributionStats],
- excludedTargets: mutable.ListBuffer[SourceUniqueness]
- ): List[InGameActivity] = {
- if (!excludedTargets.contains(target.unique)) {
- val shortHistory = contributions.getOrElse(target.unique, Nil)
- val process = new MachineRecoveryExperienceContributionProcess(faction, contributions, excludedTargets :+ target.unique)
- process.submit(shortHistory)
- cullContributorImplements(process.output()).foreach {
- case (id, stats) => contributionsBy.getOrElseUpdate(id, stats)
+ excludedTargets: mutable.ListBuffer[SourceUniqueness],
+ out: mutable.LongMap[ContributionStats]
+ ): Unit = {
+ val data = history
+ .collect {
+ case h: HealFromTerminal => (h.term, (h, h.term.hacked))
+ case r: RepairFromTerminal => (r.term, (r, r.term.hacked))
+ case t: TerminalUsedActivity => (t.terminal, (t, t.terminal.hacked))
}
- shortHistory
- } else {
- Nil
+ .groupBy(_._1.unique)
+ .map { case (_, list) => (list.head._1, list.map { _._2 }) }
+ data.flatMap {
+ case (terminal, events) =>
+ val (activity, hackState) = events.unzip
+ val terminalFaction = terminal.Faction
+ if (terminalFaction != faction && hackState.exists { _.nonEmpty }) {
+ /*
+ if the terminal has been hacked,
+ and the original terminal does not align with our own faction,
+ then the support must be reported as a hack;
+ if we are the same faction as the terminal, then the hacked condition is irrelevant
+ */
+ val hackContext = HackKillAssist(GlobalDefinitions.remote_electronics_kit.ObjectId, terminal.Definition.ObjectId)
+ hackState
+ .groupBy(_.get.player)
+ .collect {
+ case (player, _) if player.Faction == faction => //only reward allied hacking
+ val time = activity.maxBy(_.time).time
+ combineStatsInto(
+ out,
+ (
+ player.CharId,
+ ContributionStats(
+ player,
+ Seq(WeaponStats(hackContext, 0, 1, time, 10f)),
+ 0,
+ 0,
+ 1,
+ time
+ )
+ )
+ )
+ }
+ activity
+ } else if (terminalFaction == faction) {
+ val eventTime = activity.maxBy(_.time).time
+ val startTime = activity.minBy(_.time).time - 600000L
+ val (equipmentUseContext, ownerOpt) = terminal.installation match {
+ case v: VehicleSource =>
+ terminal.Definition match {
+ case GlobalDefinitions.order_terminala =>
+ combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
+ (AmsResupplyKillAssist(terminal.Definition.ObjectId), v.owner)
+ case GlobalDefinitions.order_terminalb =>
+ combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
+ (AmsResupplyKillAssist(terminal.Definition.ObjectId), v.owner)
+ case GlobalDefinitions.lodestar_repair_terminal =>
+ combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
+ (RepairKillAssist(terminal.Definition.ObjectId, v.Definition.ObjectId), v.owner)
+ case GlobalDefinitions.bfr_rearm_terminal =>
+ combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
+ (LodestarRearmKillAssist(terminal.Definition.ObjectId), v.owner)
+ case GlobalDefinitions.multivehicle_rearm_terminal =>
+ combineStatsInto(out, extractContributionsForMachineByTarget(v, faction, eventTime, startTime, contributions, excludedTargets))
+ (LodestarRearmKillAssist(terminal.Definition.ObjectId), v.owner)
+ case _ =>
+ (NoUse(), None)
+ }
+ case _: BuildingSource =>
+ combineStatsInto(out, extractContributionsForMachineByTarget(terminal, faction, eventTime, startTime, contributions, excludedTargets))
+ (NoUse(), None) //general terminal use
+ case _ =>
+ (NoUse(), None)
+ }
+ ownerOpt.collect { owner =>
+ val time = activity.maxBy(_.time).time
+ combineStatsInto(
+ out,
+ (
+ owner.charId,
+ ContributionStats(
+ PlayerSource(owner, terminal.installation.Position),
+ Seq(WeaponStats(equipmentUseContext, 0, 1, time, 10f)),
+ 0,
+ 0,
+ 1,
+ time
+ )
+ ))
+ }
+ activity
+ } else {
+ Nil
+ }
}
}
+ /**
+ * Gather and reward specific in-game equipment use activity.
+ * na
+ * @param history chronology of activity the game considers noteworthy
+ * @param out quantitative record of activity in relation to the other players and their equipment
+ * @see `combineStatsInto`
+ * @see `ReviveKillAssist`
+ */
+ private def contributeWithRevivalActivity(
+ history: List[InGameActivity],
+ out: mutable.LongMap[ContributionStats]
+ ): Unit = {
+ history
+ .collect { case rev: RevivingActivity => rev }
+ .groupBy(_.user.CharId)
+ .map { case (id, revivesByThisPlayer) =>
+ val user = revivesByThisPlayer.head.user
+ revivesByThisPlayer
+ .groupBy(_.equipment)
+ .map { case (definition, events) =>
+ val eventSize = events.size
+ val objectId = definition.ObjectId
+ val time = events.maxBy(_.time).time
+ combineStatsInto(
+ out,
+ (
+ id,
+ ContributionStats(
+ user,
+ Seq(WeaponStats(ReviveKillAssist(objectId), 100, 1, time, math.log(eventSize + 1).toFloat * 15f)),
+ eventSize,
+ eventSize,
+ eventSize,
+ time
+ )
+ )
+ )
+ }
+ }
+ }
+
+ /**
+ * na
+ * Mainly produces repair events.
+ * @param target entity external to the subject of the kill
+ * @param faction empire to target
+ * @param time na
+ * @param contributions mapping between external entities
+ * the target has interacted with in the form of in-game activity
+ * and history related to the time period in which the interaction ocurred
+ * @param excludedTargets if a potential target is listed here already, skip processing it
+ * @return quantitative record of activity in relation to the other players and their equipment
+ */
+ private def extractContributionsForMachineByTarget(
+ target: SourceEntry,
+ faction: PlanetSideEmpire.Value,
+ time: Long,
+ contributions: Map[SourceUniqueness, List[InGameActivity]],
+ excludedTargets: mutable.ListBuffer[SourceUniqueness]
+ ): mutable.LongMap[ContributionStats] = {
+ val start: Long = time - 600000L
+ extractContributionsForMachineByTarget(target, faction, time, start, contributions, excludedTargets)
+ }
+
+ /**
+ * na
+ * Mainly produces repair events.
+ * @param target entity external to the subject of the kill
+ * @param faction empire to target
+ * @param eventTime na
+ * @param startTime na
+ * @param contributions mapping between external entities
+ * the target has interacted with in the form of in-game activity
+ * and history related to the time period in which the interaction ocurred
+ * @param excludedTargets if a potential target is listed here already, skip processing it
+ * @return quantitative record of activity in relation to the other players and their equipment
+ * @see `limitHistoryToThisLife`
+ */
+ private def extractContributionsForMachineByTarget(
+ target: SourceEntry,
+ faction: PlanetSideEmpire.Value,
+ eventTime: Long,
+ startTime: Long,
+ contributions: Map[SourceUniqueness, List[InGameActivity]],
+ excludedTargets: mutable.ListBuffer[SourceUniqueness]
+ ): mutable.LongMap[ContributionStats] = {
+ val unique = target.unique
+ val history = limitHistoryToThisLife(contributions.getOrElse(unique, List()), eventTime, startTime)
+ extractContributionsForMachineByTarget(target, faction, history, contributions, excludedTargets)
+ }
+
+ /**
+ * na
+ * Mainly produces repair events.
+ * @param target entity external to the subject of the kill
+ * @param faction empire to target
+ * @param history na
+ * @param contributions mapping between external entities
+ * the target has interacted with in the form of in-game activity
+ * and history related to the time period in which the interaction ocurred
+ * @param excludedTargets if a potential target is listed here already, skip processing it
+ * @return quantitative record of activity in relation to the other players and their equipment
+ * @see `cullContributorImplements`
+ * @see `emptyMap`
+ * @see `MachineRecoveryExperienceContributionProcess`
+ */
+ private def extractContributionsForMachineByTarget(
+ target: SourceEntry,
+ faction: PlanetSideEmpire.Value,
+ history: List[InGameActivity],
+ contributions: Map[SourceUniqueness, List[InGameActivity]],
+ excludedTargets: mutable.ListBuffer[SourceUniqueness]
+ ): mutable.LongMap[ContributionStats] = {
+ val unique = target.unique
+ if (!excludedTargets.contains(unique) && history.nonEmpty) {
+ excludedTargets.addOne(unique)
+ val process = new MachineRecoveryExperienceContributionProcess(faction, contributions, excludedTargets)
+ process.submit(history)
+ cullContributorImplements(process.output())
+ } else {
+ emptyMap
+ }
+ }
+
+ /**
+ * na
+ * @param main quantitative record of activity in relation to the other players and their equipment
+ * @param transferFrom quantitative record of activity in relation to the other players and their equipment
+ * @return quantitative record of activity in relation to the other players and their equipment
+ * @see `combineStatsInto`
+ */
+ private def combineStatsInto(
+ main: mutable.LongMap[ContributionStats],
+ transferFrom: mutable.LongMap[ContributionStats]
+ ): mutable.LongMap[ContributionStats] = {
+ transferFrom.foreach { (entry: (Long, ContributionStats)) => combineStatsInto(main, entry) }
+ main
+ }
+
+ /**
+ * na
+ * @param main quantitative record of activity in relation to the other players and their equipment
+ * @param entry two value tuple representing:
+ * a player's unique identifier,
+ * and a quantitative record of activity in relation to the other players and their equipment
+ * @see `Support.combineWeaponStats`
+ */
+ private def combineStatsInto(main: mutable.LongMap[ContributionStats], entry: (Long, ContributionStats)): Unit = {
+ val (id, sampleStats) = entry
+ main.get(id) match {
+ case Some(foundStats) =>
+ main.put(id, foundStats.copy(weapons = Support.combineWeaponStats(foundStats.weapons, sampleStats.weapons)))
+ case None =>
+ main.put(id, sampleStats)
+ }
+ }
+
+ /**
+ * Filter quantitative records based on the presence of specific equipment used for statistic recovery.
+ * @param input quantitative record of activity in relation to the other players and their equipment
+ * @return quantitative record of activity in relation to the other players and their equipment
+ * @see `RecoveryItems`
+ */
private[exp] def cullContributorImplements(
input: mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStats] = {
input.collect { case (id, entry) =>
- (id, entry.copy(weapons = entry.weapons.filter { stats => KillContributions.RecoveryItems.contains(stats.equipment.equipment) })) //TODO bad test; fix later
+ (id, entry.copy(weapons = entry.weapons.filter { stats => RecoveryItems.contains(stats.equipment.equipment) }))
}.filter { case (_, entry) =>
entry.weapons.nonEmpty
}
}
- private def contributeWithCombinedActivity(
- id: Long,
- newStats: List[WeaponStats],
- user: PlayerSource,
- amount: Int,
- total: Int,
- shots: Int,
- time: Long,
- participants: mutable.LongMap[ContributionStats]
- ): Unit = {
- participants.get(id) match {
- case Some(stats) =>
- val oldWeapons = stats.weapons
- val newWeapons = newStats.map { weapon =>
- oldWeapons.find { _.equipment == weapon.equipment } match {
- case Some(foundEntry) =>
- weapon.copy(
- amount = weapon.amount + foundEntry.amount,
- shots = weapon.shots + foundEntry.shots,
- contributions = weapon.contributions + foundEntry.contributions
- )
- case None =>
- weapon
- }
- }
- participants.put(id, stats.copy(weapons = newWeapons))
- case None =>
- participants.put(id, ContributionStats(user, newStats, amount, total, shots, time))
- }
- }
-
+ /**
+ * na
+ * @param charId the unique identifier being targeted
+ * @param shortPeriod quantitative record of activity in relation to the other players and their equipment
+ * @param longPeriod quantitative record of activity in relation to the other players and their equipment
+ * @param max maximum value for the third output value
+ * @return two value tuple representing:
+ * a player's unique identifier,
+ * and a summary of the interaction in terms of players, equipment activity, and experience
+ * @see `composeContributionOutput`
+ */
private def composeContributionOutput(
- player: PlayerSource,
+ charId: Long,
shortPeriod: mutable.LongMap[ContributionStats],
longPeriod: mutable.LongMap[ContributionStats],
- bep: Long
- ): (Long, ContributionStatsOutput) = {
- val charId = player.CharId
- longPeriod
+ max: Long
+ ): Option[(Long, ContributionStatsOutput)] = {
+ composeContributionOutput(charId, longPeriod, modifier=0.8f, max)
+ .orElse { composeContributionOutput(charId, shortPeriod, modifier=1f, max) }
+ .collect {
+ case (player, weaponIds, experience) =>
+ (charId, ContributionStatsOutput(player, weaponIds, experience))
+ }
+ }
+
+ /**
+ * na
+ * @param charId the unique identifier being targeted
+ * @param stats quantitative record of activity in relation to the other players and their equipment
+ * @param modifier modifier value for the potential third output value
+ * @param max maximum value for the third output value
+ * @return three value tuple representing:
+ * player,
+ * the context in which certain equipment is being used,
+ * and a final value for the awarded support experience points
+ */
+ private def composeContributionOutput(
+ charId: Long,
+ stats: mutable.LongMap[ContributionStats],
+ modifier: Float,
+ max: Long
+ ): Option[(PlayerSource, Seq[EquipmentUseContextWrapper], Float)] = {
+ stats
.get(charId)
.collect {
case entry =>
- val weapons = entry.weapons
+ val (weapons, contributions) = entry.weapons.map { entry => (entry.equipment, entry.contributions) }.unzip
(
entry.player,
- weapons.filter { _.amount == 0 }.map { _.equipment },
- (0.9f * math.min(weapons.foldLeft(0f)(_ + _.contributions), bep.toFloat)).toLong
+ weapons.distinct,
+ modifier * math.floor(math.min(contributions.foldLeft(0f)(_ + _), max.toFloat)).toFloat
)
}
- .orElse {
- shortPeriod
- .get(charId)
- .collect {
- case entry =>
- val weapons = entry.weapons
- (
- entry.player,
- weapons.map { _.equipment },
- math.min(weapons.foldLeft(0f)(_ + _.contributions).toLong, bep)
- )
- }
- }
- .orElse {
- Some((PlayerSource.Nobody, Seq(), 0L))
- }
- .collect {
- case (player, weaponIds, experience) if experience > 0 =>
- (charId, ContributionStatsOutput(player, weaponIds, experience.toFloat))
- }
- .get
}
}
diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
index 3f6f82fb6..08fa1dbf4 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
@@ -1,23 +1,23 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
-import net.psforever.objects.GlobalDefinitions
-
-import java.util.Date
-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, ReconstructionActivity, RepairFromEquipment, RepairFromExoSuitChange, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity}
-import net.psforever.objects.zones.Zone
-import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
-import net.psforever.zones.Zones
+import net.psforever.objects.sourcing.PlayerSource
+import net.psforever.objects.vital.{InGameActivity, ReconstructionActivity, RepairFromExoSuitChange, SpawningActivity}
+import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
import scala.collection.mutable
+/**
+ * Functions to assist experience calculation and history manipulation and analysis.
+ */
object Support {
- private type SupportActivity = InGameActivity with SupportActivityCausedByAnother
-
+ /**
+ * Calculate a base experience value to consider additional reasons for points.
+ * @param victim player to which a final interaction has reduced health to zero
+ * @param history chronology of activity the game considers noteworthy
+ * @return the value of the kill in what the game called "battle experience points"
+ * @see `Support.wasEverAMax`
+ */
private[exp] def baseExperience(
victim: PlayerSource,
history: Iterable[InGameActivity]
@@ -26,8 +26,7 @@ object Support {
case (Some(spawn), Some(death)) => death.time - spawn.time
case _ => 0L
}
- val wasEverAMax = Support.wasEverAMax(victim, history)
- val base = if (wasEverAMax) { //shamed
+ val base = if (Support.wasEverAMax(victim, history)) {
250L
} else if (victim.Seated || victim.progress.kills.nonEmpty) {
100L
@@ -45,20 +44,39 @@ object Support {
}
}
+ /**
+ * Combine two quantitative records into one, maintaining only the original entries.
+ * @param first one quantitative record
+ * @param second another quantitative record
+ * @param combiner mechanism for determining how to combine quantitative records;
+ * defaults to an additive combiner with a small multiplier value
+ * @return the combined quantitative records
+ * @see `defaultAdditiveOutputCombiner`
+ * @see `onlyOriginalAssistEntriesIterable`
+ */
private[exp] def onlyOriginalAssistEntries(
first: mutable.LongMap[ContributionStatsOutput],
second: mutable.LongMap[ContributionStatsOutput],
combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
- defaultAdditiveOutputCombiner(multiplier = 0.05f)
+ defaultAdditiveOutputCombiner(multiplier = 0.05f)
): Iterable[ContributionStatsOutput] = {
onlyOriginalAssistEntriesIterable(first.values, second.values, combiner)
}
+ /**
+ * Combine two quantitative records into one, maintaining only the original entries.
+ * @param first one quantitative record
+ * @param second another quantitative record
+ * @param combiner mechanism for determining how to combine quantitative records;
+ * defaults to an additive combiner with a small multiplier value
+ * @return the combined quantitative records
+ * @see `defaultAdditiveOutputCombiner`
+ */
private[exp] def onlyOriginalAssistEntriesIterable(
first: Iterable[ContributionStatsOutput],
second: Iterable[ContributionStatsOutput],
combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
- defaultAdditiveOutputCombiner(multiplier = 0.05f)
+ defaultAdditiveOutputCombiner(multiplier = 0.05f)
): Iterable[ContributionStatsOutput] = {
if (second.isEmpty) {
first
@@ -79,6 +97,13 @@ object Support {
}
}
+ /**
+ * Combine two quantitative records into one, maintaining only the original entries.
+ * @param multiplier adjust the combined
+ * @param first one quantitative record
+ * @param second another quantitative record
+ * @return the combined quantitative records
+ */
private def defaultAdditiveOutputCombiner(
multiplier: Float
)
@@ -92,206 +117,43 @@ object Support {
first.copy(implements = (first.implements ++ second.implements).distinct, percentage = second.percentage + second.implements.size * multiplier)
}
- private[exp] def collectHealingSupportAssists(
- target: SourceEntry,
- time: JodaLocalDateTime,
- history: List[InGameActivity]
- ): mutable.LongMap[ContributionStatsOutput] = {
- //normal heals
- val healInfo = collectSupportContributions(
- target, time, history.collect { case heals: HealFromEquipment
- if heals.amount > 0 && heals.user.unique != target.unique => (heals, heals.equipment_def.ObjectId)
- }, mapContributionPointsByPercentage
- )
- //revivals (count all revivals performed by the player of the last one, if any)
- val reviveInfo = collectSupportContributions(
- target, time, history.collect { case revive: RevivingActivity
- if revive.user.unique != target.unique => (revive, revive.equipment.ObjectId)
- }.lastOption.toSeq, mapContributionPointsByCount
- )
- //combine and output
- reviveInfo.foreach { case (reviverId, contribution) =>
- val result = healInfo.get(reviverId)
- val percentage = result.map { a => a.percentage }.getOrElse { contribution.percentage }
- val implements = result.map { a => (a.implements ++ contribution.implements).distinct }.getOrElse { contribution.implements }
- healInfo.put(reviverId, ContributionStatsOutput(contribution.player, implements, percentage))
- }
- healInfo
- }
-
- private[exp] def collectRepairingSupportAssists(
- target: SourceEntry,
- time: JodaLocalDateTime,
- history: List[InGameActivity]
- ): mutable.LongMap[ContributionStatsOutput] = {
- collectSupportContributions(
- target,
- time,
- history.collect { case repairs: RepairFromEquipment
- if repairs.amount > 0 && repairs.user.unique != target.unique => (repairs, repairs.equipment_def.ObjectId)
- },
- mapContributionPointsByPercentage
- )
- }
-
- private[exp] def collectTerminalSupportAssists(
- target: SourceEntry,
- history: List[InGameActivity]
- ): Iterable[ContributionStatsOutput] = {
- val delay: Long = 300000L
- val curr = System.currentTimeMillis()
- var zone: Zone = Zone.Nowhere
- val termsUsed: mutable.LongMap[Option[AmenitySource]] = mutable.LongMap[Option[AmenitySource]]()
- val credit: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
- val faction = target.Faction
- val terminalUseHistory = history.filter {
- case _: SpawningActivity => true
- case entry: TerminalUsedActivity => curr - entry.time < delay
- case _ => false
- }
- terminalUseHistory.collect {
- case entry: SpawningActivity =>
- zone = Zones.zones(entry.zoneNumber)
- case entry: TerminalUsedActivity
- if !termsUsed.contains(entry.terminal.unique.guid.guid.toLong) =>
- if (
- entry.transaction == TransactionType.Loadout ||
- entry.transaction == TransactionType.Buy ||
- entry.transaction == TransactionType.Learn
- ) {
- val terminal = entry.terminal
- val guid = terminal.unique.guid.guid
- terminal.hacked.collect {
- case Hackable.HackInfo(player, _, _, _)
- if target.CharId != player.CharId && faction == player.Faction =>
- //accessed a hacked terminal
- termsUsed.put(guid.toLong, Some(terminal))
- addTerminalContributionEntry(credit, player, Seq(HackKillAssist(GlobalDefinitions.remote_electronics_kit.ObjectId)), percentage = 1f)
- case _
- if terminal.Faction == faction =>
- //accessed a faction-friendly terminal; check log for repair history
- termsUsed.put(guid.toLong, Some(terminal))
- zone.GUID(guid)
- .asInstanceOf[Terminal]
- .History
- .collect { case a: RepairFromEquipment =>
- val user = a.user
- //TODO might be wrong intermediate
- addTerminalContributionEntry(credit, user, Seq(RepairKillAssist(a.equipment_def.ObjectId, target.Definition.ObjectId)), percentage = 0.5f)
- }
- case _ =>
- //what is this?
- termsUsed.put(guid.toLong, None)
- }
- }
- }
- credit.remove(target.CharId)
- credit.values
- }
-
- private def addTerminalContributionEntry(
- contributions: mutable.LongMap[ContributionStatsOutput],
- player: PlayerSource,
- implements: Seq[EquipmentUseContextWrapper],
- percentage: Float
- ): Unit = {
- val charId = player.CharId
- contributions.get(charId) match {
- case Some(entry) if percentage > entry.percentage =>
- contributions.put(charId, entry.copy(percentage = percentage))
- case Some(entry) =>
- contributions.put(charId, entry.copy(implements = (entry.implements ++ implements).distinct, percentage = entry.percentage + 0.05f))
- case None =>
- contributions.put(charId, ContributionStatsOutput(player, implements, percentage))
- }
- }
-
- private[exp] def findTimeApplicableActivities(
- activity: Seq[SupportActivity],
- killLastTime: JodaLocalDateTime,
- timeOffset: Int //s
- ): Seq[SupportActivity] = {
- val dateTimeConverter: Date=>JodaLocalDateTime = JodaLocalDateTime.fromDateFields
- val milliToInstant: Long=>org.joda.time.Instant = Instant.ofEpochMilli
- val targetTime = killLastTime.minusSeconds(timeOffset)
- //find all activities that occurred after the kill time after the offset is considered
- activity
- .filter { a =>
- val activityTime = dateTimeConverter(milliToInstant(a.time).toDate)
- targetTime.isBefore(activityTime) || targetTime.equals(activityTime)
- }
- .sortBy(_.time)(Ordering.Long.reverse)
- }
-
- 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
- val user = h.user
- val charId = user.CharId
- contributions.get(charId) match {
- case Some(entry) =>
- contributions.update(charId, entry.copy(
- amount = entry.amount + amount,
- total = entry.total + amount,
- shots = entry.shots + 1,
- time = math.max(entry.time, h.time)
- ))
- case None =>
- //the contribution percentage allocated will always be 1.0f and should be overwritten later
- contributions.put(charId, ContributionStats(user, Seq(WeaponStats(NoUse(defaultTool), amount, 1, h.time, 1f)), amount, amount, 1, h.time))
- }
- }
- contributions
- }
-
- private def collectSupportContributions(
- target: SourceEntry,
- time: JodaLocalDateTime,
- pairs: Seq[(InGameActivity, Int)],
- contributionPointsMap: (Float,Iterable[SupportActivity])=>(Long,ContributionStats)=>(Long,ContributionStatsOutput)
- ): mutable.LongMap[ContributionStatsOutput] = {
- val (activity, tools) = pairs.unzip
- //TODO this is not correct, but it will do for now
- if (tools.isEmpty) {
- mutable.LongMap[ContributionStatsOutput]()
- } else {
- 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
+ /**
+ * Take two sequences of equipment statistics
+ * and combine both lists where overlap of the same equipment use is added together per field.
+ * If one sequence comtains more elements of the same type of equipment use,
+ * the additional entries may become lost.
+ * @param first statistics in relation to equipment
+ * @param second statistics in relation to equipment
+ * @return statistics in relation to equipment
+ */
+ private[exp] def combineWeaponStats(
+ first: Seq[WeaponStats],
+ second: Seq[WeaponStats]
+ ): Seq[WeaponStats] = {
+ val (firstInSecond, firstAlone) = first.partition(firstStat => second.exists(_.equipment == firstStat.equipment))
+ val (secondInFirst, secondAlone) = second.partition(secondStat => firstInSecond.exists(_.equipment == secondStat.equipment))
+ val combined = firstInSecond.flatMap { firstStat =>
+ secondInFirst
+ .filter(_.equipment == firstStat.equipment)
+ .map { secondStat =>
+ firstStat.copy(
+ shots = firstStat.shots + secondStat.shots,
+ amount = firstStat.amount + secondStat.amount,
+ contributions = firstStat.contributions + secondStat.contributions,
+ time = math.max(firstStat.time, secondStat.time)
)
}
- mutable.LongMap[ContributionStatsOutput]().addAll(output.map { entry => (entry.player.CharId, entry) })
- }
}
+ firstAlone ++ secondAlone ++ combined
}
- 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) }
- }
-
+ /**
+ * Run a function against history, targeting a certain faction.
+ * @param tallyFunc the history analysis function
+ * @param history chronology of activity the game considers noteworthy
+ * @param faction empire to target
+ * @return quantitative record of activity in relation to the other players and their equipment
+ */
private[exp] def allocateContributors(
tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any
)
@@ -299,57 +161,24 @@ object Support {
history: List[InGameActivity],
faction: PlanetSideEmpire.Value
): mutable.LongMap[ContributionStats] = {
- /** players who have contributed to this death, and how much they have contributed
- * key - character identifier,
- * value - (player, damage, total damage, number of shots) */
+ /*
+ players who have contributed to this death, and how much they have contributed
+ 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]
- )
- (
- charId: Long,
- contribution: ContributionStats,
- ): (Long, ContributionStatsOutput) = {
- val user = contribution.player
- val unique = user.unique
- val points = contribution.amount
- val value = if (points < 75) {
- //a small contribution means the lower time limit
- if (compareList.exists { a => a.user.unique == unique }) {
- math.max(0.2f, points / total)
- } else {
- 0f
- }
- } else {
- //large contribution is always okay
- if (points > 299) {
- 1.0f
- } else if (points > 100) {
- 0.75f
- } else {
- 0.5f
- }
- }
- (charId, ContributionStatsOutput(user, contribution.weapons.map { _.equipment }, value))
- }
-
- //noinspection ScalaUnusedSymbol
- private def mapContributionPointsByCount(
- total: Float,
- compareList: Iterable[SupportActivity]
- )
- (
- charId: Long,
- contribution: ContributionStats,
- ): (Long, ContributionStatsOutput) = {
- (charId, ContributionStatsOutput(contribution.player, contribution.weapons.map { _.equipment }, compareList.size.toFloat))
- }
-
+ /**
+ * You better not fail this purity test.
+ * @param player player being tested
+ * @param history chronology of activity the game considers noteworthy;
+ * allegedly associated with this player
+ * @return `true`, if the player has ever committed a great shame;
+ * `false`, otherwise ... and it better be
+ */
private[exp] def wasEverAMax(player: PlayerSource, history: Iterable[InGameActivity]): Boolean = {
player.ExoSuit == ExoSuitType.MAX || history.exists {
case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
diff --git a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
index 9252de9fc..62b76a04e 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
@@ -105,32 +105,17 @@ object ToDatabase {
* Shots fired.
*/
def reportToolDischarge(avatarId: Long, stats: EquipmentStat): Unit = {
- val result = for {
- res <- ctx.run(
- query[persistence.Weaponstatsession]
- .filter(_.avatarId == lift(avatarId))
- .filter(_.weaponId == lift(stats.objectId))
- .update(
- _.shotsFired -> lift(stats.shotsFired),
- _.shotsLanded -> lift(stats.shotsLanded)
- )
+ ctx.run(query[persistence.Weaponstatsession]
+ .insert(
+ _.avatarId -> lift(avatarId),
+ _.weaponId -> lift(stats.objectId),
+ _.shotsFired -> lift(stats.shotsFired),
+ _.shotsLanded -> lift(stats.shotsLanded),
+ _.kills -> lift(0),
+ _.assists -> lift(0),
+ _.sessionId -> lift(-1L)
)
- } yield res
- result.onComplete {
- case Success(rowCount) if rowCount.longValue > 0 => ()
- case _ =>
- ctx.run(query[persistence.Weaponstatsession]
- .insert(
- _.avatarId -> lift(avatarId),
- _.weaponId -> lift(stats.objectId),
- _.shotsFired -> lift(stats.shotsFired),
- _.shotsLanded -> lift(stats.shotsLanded),
- _.kills -> lift(0),
- _.assists -> lift(0),
- _.sessionId -> lift(-1L)
- )
- )
- }
+ )
}
/**
diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/ArmorRecoveryExperienceContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/ArmorRecoveryExperienceContributionProcess.scala
index cf4034255..4927b9863 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/rec/ArmorRecoveryExperienceContributionProcess.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/rec/ArmorRecoveryExperienceContributionProcess.scala
@@ -2,16 +2,12 @@
package net.psforever.objects.zones.exp.rec
import net.psforever.objects.sourcing.SourceUniqueness
-import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairFromEquipment, RepairFromTerminal, RepairingActivity, SupportActivityCausedByAnother}
-import net.psforever.objects.zones.exp.KillContributions
+import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairFromEquipment, RepairingActivity}
import net.psforever.types.PlanetSideEmpire
-import scala.collection.mutable
-
class ArmorRecoveryExperienceContributionProcess(
private val faction : PlanetSideEmpire.Value,
- private val contributions: Map[SourceUniqueness, List[InGameActivity]],
- private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer()
+ private val contributions: Map[SourceUniqueness, List[InGameActivity]]
) extends RecoveryExperienceContributionProcess(faction, contributions) {
def submit(history: List[InGameActivity]): Unit = {
history.foreach {
@@ -26,31 +22,6 @@ class ArmorRecoveryExperienceContributionProcess(
)
damageInOrder = damage
recoveryInOrder = recovery
- case rt: RepairFromTerminal =>
- val time = rt.time
- val users = KillContributions.contributeWithTerminalActivity(
- Seq((rt, rt.term, rt.term.hacked)),
- faction,
- contributions,
- contributionsBy,
- excludedTargets
- )
- .collect { case entry: SupportActivityCausedByAnother => entry }
- .groupBy(_.user.unique)
- .map(_._2.head.user)
- .toSeq
- val (damage, recovery) = RecoveryExperienceContribution.contributeWithSupportRecoveryActivity(
- users,
- rt.term.Definition.ObjectId,
- faction,
- rt.amount,
- time,
- participants,
- damageInOrder,
- recoveryInOrder
- )
- damageInOrder = damage
- recoveryInOrder = recovery
case r: RepairFromEquipment =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(
r.user,
diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala
index 3ec36dacd..8e0685f06 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala
@@ -13,10 +13,9 @@ class CombinedHealthAndArmorContributionProcess(
private val contributions: Map[SourceUniqueness, List[InGameActivity]],
otherSubmissions: Seq[RecoveryExperienceContribution]
) extends RecoveryExperienceContribution {
- private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer()
private val process: Seq[RecoveryExperienceContributionProcess] = Seq(
- new HealthRecoveryExperienceContributionProcess(faction, contributions, excludedTargets),
- new ArmorRecoveryExperienceContributionProcess(faction, contributions, excludedTargets)
+ new HealthRecoveryExperienceContributionProcess(faction, contributions),
+ new ArmorRecoveryExperienceContributionProcess(faction, contributions)
)
def submit(history: List[InGameActivity]): Unit = {
diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/HealthRecoveryExperienceContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/HealthRecoveryExperienceContributionProcess.scala
index 4b5edcd67..e639d7625 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/rec/HealthRecoveryExperienceContributionProcess.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/rec/HealthRecoveryExperienceContributionProcess.scala
@@ -2,16 +2,12 @@
package net.psforever.objects.zones.exp.rec
import net.psforever.objects.sourcing.SourceUniqueness
-import net.psforever.objects.vital.{DamagingActivity, HealFromEquipment, HealFromTerminal, HealingActivity, InGameActivity, RevivingActivity, SupportActivityCausedByAnother}
-import net.psforever.objects.zones.exp.KillContributions
+import net.psforever.objects.vital.{DamagingActivity, HealFromEquipment, HealingActivity, InGameActivity, RevivingActivity}
import net.psforever.types.PlanetSideEmpire
-import scala.collection.mutable
-
private class HealthRecoveryExperienceContributionProcess(
private val faction : PlanetSideEmpire.Value,
- private val contributions: Map[SourceUniqueness, List[InGameActivity]],
- private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer()
+ private val contributions: Map[SourceUniqueness, List[InGameActivity]]
) extends RecoveryExperienceContributionProcess(faction, contributions) {
def submit(history: List[InGameActivity]): Unit = {
history.foreach {
@@ -26,31 +22,6 @@ private class HealthRecoveryExperienceContributionProcess(
)
damageInOrder = damage
recoveryInOrder = recovery
- case ht: HealFromTerminal =>
- val time = ht.time
- val users = KillContributions.contributeWithTerminalActivity(
- Seq((ht, ht.term, ht.term.hacked)),
- faction,
- contributions,
- contributionsBy,
- excludedTargets
- )
- .collect { case entry: SupportActivityCausedByAnother => entry }
- .groupBy(_.user.unique)
- .map(_._2.head.user)
- .toSeq
- val (damage, recovery) = RecoveryExperienceContribution.contributeWithSupportRecoveryActivity(
- users,
- ht.term.Definition.ObjectId,
- faction,
- ht.amount,
- time,
- participants,
- damageInOrder,
- recoveryInOrder
- )
- damageInOrder = damage
- recoveryInOrder = recovery
case h: HealFromEquipment =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(
h.user,
diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala
index 0b211ddfe..ce0404888 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala
@@ -3,22 +3,23 @@ package net.psforever.objects.zones.exp.rec
import net.psforever.objects.sourcing.SourceUniqueness
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairFromEquipment, RepairingActivity}
+import net.psforever.objects.zones.exp.ContributionStats
import net.psforever.types.PlanetSideEmpire
import scala.collection.mutable
//noinspection ScalaUnusedSymbol
class MachineRecoveryExperienceContributionProcess(
- private val faction : PlanetSideEmpire.Value,
- private val contributions: Map[SourceUniqueness, List[InGameActivity]],
- private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer()
- ) extends RecoveryExperienceContributionProcess(faction, contributions) {
+ private val faction : PlanetSideEmpire.Value,
+ private val contributions: Map[SourceUniqueness, List[InGameActivity]],
+ private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer()
+ ) extends RecoveryExperienceContributionProcess(faction, contributions) {
def submit(history: List[InGameActivity]): Unit = {
history.foreach {
- case d: DamagingActivity if d.amount - d.health > 0 =>
+ case d: DamagingActivity if d.amount == d.health =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithDamagingActivity(
d,
- d.amount - d.health,
+ d.health,
damageParticipants,
participants,
damageInOrder,
@@ -26,7 +27,7 @@ class MachineRecoveryExperienceContributionProcess(
)
damageInOrder = damage
recoveryInOrder = recovery
- case r: RepairFromEquipment =>
+ case r: RepairFromEquipment if !excludedTargets.contains(r.user.unique) =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(
r.user,
r.equipment_def.ObjectId,
@@ -56,4 +57,23 @@ class MachineRecoveryExperienceContributionProcess(
case _ => ()
}
}
+
+ override def output(): mutable.LongMap[ContributionStats] = {
+ super.output().map { case (id, stats) =>
+ val weps = stats.weapons
+ .groupBy(_.equipment)
+ .map { case (wrapper, entries) =>
+ val size = entries.size
+ val newTime = entries.maxBy(_.time).time
+ entries.head.copy(
+ shots = size,
+ amount = entries.foldLeft(0)(_ + _.amount),
+ contributions = (10 + size).toFloat,
+ time = newTime
+ )
+ }
+ .toSeq
+ (id, stats.copy(weapons = weps))
+ }
+ }
}
diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContribution.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContribution.scala
index bc932856d..affab6e76 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContribution.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContribution.scala
@@ -262,18 +262,20 @@ object RecoveryExperienceContribution {
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
var outputDamageOrder = damageOrder
var outputRecoveryOrder = recoveryOrder
- val damageParticipants: mutable.LongMap[PlayerSource] = mutable.LongMap[PlayerSource]()
- users.zip {
- val numberOfUsers = users.size
- val out = Array.fill(numberOfUsers)(numberOfUsers / amount)
- (0 to numberOfUsers % amount).foreach {
- out(_) += 1
+ if (users.nonEmpty) {
+ val damageParticipants: mutable.LongMap[PlayerSource] = mutable.LongMap[PlayerSource]()
+ users.zip {
+ val numberOfUsers = users.size
+ val out = Array.fill(numberOfUsers)(numberOfUsers / amount)
+ (0 to numberOfUsers % amount).foreach {
+ out(_) += 1
+ }
+ out
+ }.foreach { case (user, subAmount) =>
+ val (a, b) = contributeWithRecoveryActivity(user, user.CharId, wepid, faction, subAmount, time, damageParticipants, participants, outputDamageOrder, outputRecoveryOrder)
+ outputDamageOrder = a
+ outputRecoveryOrder = b
}
- out
- }.foreach { case (user, subAmount) =>
- val (a, b) = contributeWithRecoveryActivity(user, user.CharId, wepid, faction, subAmount, time, damageParticipants, participants, outputDamageOrder, outputRecoveryOrder)
- outputDamageOrder = a
- outputRecoveryOrder = b
}
(outputDamageOrder, outputRecoveryOrder)
}
diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContributionProcess.scala
index 7394bba68..e6f3ecc82 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContributionProcess.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContributionProcess.scala
@@ -10,9 +10,9 @@ import scala.collection.mutable
//noinspection ScalaUnusedSymbol
abstract class RecoveryExperienceContributionProcess(
- faction : PlanetSideEmpire.Value,
- contributions: Map[SourceUniqueness, List[InGameActivity]]
- ) extends RecoveryExperienceContribution {
+ faction : PlanetSideEmpire.Value,
+ contributions: Map[SourceUniqueness, List[InGameActivity]]
+ ) extends RecoveryExperienceContribution {
protected var damageInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
protected var recoveryInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
protected val contributionsBy: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()