all rewarded support events are accounted for

This commit is contained in:
Fate-JH 2023-09-11 02:07:18 -04:00
parent d6a37e9421
commit 8bdb215d58
19 changed files with 1393 additions and 913 deletions

View file

@ -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

View file

@ -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(

View file

@ -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)

View file

@ -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 _ => ()
}
}

View file

@ -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.<br>
* 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()
}

View file

@ -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
})
)
}
}

View file

@ -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)
}
}
}
}

View file

@ -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

View file

@ -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)

View file

@ -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)<br>
* 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)<br>
key - character identifier,
value - current damage contribution
*/
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.foreach {
case d: DamagingActivity if d.health > 0 =>
@ -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)<br>
* 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)<br>
key - character identifier,
value - current damage contribution
*/
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.foreach {
case d: DamagingActivity if d.amount - d.health > 0 =>
@ -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],

View file

@ -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<br>
* key - character identifier,
* value - (player, damage, total damage, number of shots) */
/*
players who have contributed to this death, and how much they have contributed<br>
key - character identifier,
value - (player, damage, total damage, number of shots)
*/
val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
tallyFunc(history, faction, participants)
participants
}
private def mapContributionPointsByPercentage(
total: Float,
compareList: Iterable[SupportActivity]
)
(
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

View file

@ -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)
)
)
}
)
}
/**

View file

@ -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,

View file

@ -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 = {

View file

@ -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,

View file

@ -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))
}
}
}

View file

@ -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)
}

View file

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