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]()