events for mounting and dismounting of both passenger and cargo; id'ing the installation of an amenity (vehicle or facility); separation of kill/assist experience and support experience calculations; retention of kill record which allows for the calculation of menace

This commit is contained in:
Fate-JH 2023-06-20 22:20:42 -04:00
parent da6ea21147
commit 4b87526e4d
16 changed files with 367 additions and 140 deletions

View file

@ -2473,7 +2473,11 @@ class SessionData(
continent.id,
LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid)
)
player.LogActivity(VehicleDismountActivity(VehicleSource(router), PlayerSource(player)))
val vSource = VehicleSource(router)
val zoneNumber = continent.Number
player.LogActivity(VehicleMountActivity(vSource, PlayerSource(player), zoneNumber))
player.Position = dest.Position
player.LogActivity(VehicleDismountActivity(vSource, PlayerSource(player), zoneNumber))
} else {
log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
}

View file

@ -15,7 +15,7 @@ import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.zones.exp.ExperienceCalculator
import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator}
import net.psforever.util.Database._
import net.psforever.persistence
@ -87,6 +87,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
private[this] val log = org.log4s.getLogger
private val players: mutable.ListBuffer[Player] = mutable.ListBuffer()
private val experience: ActorRef[ExperienceCalculator.Command] = context.spawnAnonymous(ExperienceCalculator(zone))
private val supportExperience: ActorRef[SupportExperienceCalculator.Command] = context.spawnAnonymous(SupportExperienceCalculator(zone))
zone.actor = context.self
zone.init(context.toClassic)
@ -154,7 +155,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
experience ! ExperienceCalculator.RewardThisDeath(entity)
case RewardOurSupporters(target, history, kill, bep) =>
experience ! ExperienceCalculator.RewardOurSupporters(target, history, kill, bep)
supportExperience ! SupportExperienceCalculator.RewardOurSupporters(target, history, kill, bep)
case ZoneMapUpdate() =>
zone.Buildings

View file

@ -348,7 +348,7 @@ object MineDeployableControl {
jumping = false,
ExoSuitDefinition.Select(exosuit, faction),
bep = 0,
kills = Nil,
progress = PlayerSource.Nobody.progress,
UniquePlayer(charId, name, CharacterSex.Male, mine.Faction)
)
case None =>

View file

@ -17,6 +17,7 @@ final case class AmenitySource(
health: Int,
Orientation: Vector3,
occupants: List[SourceEntry],
installation: SourceEntry,
hacked: Option[HackInfo],
unique: UniqueAmenity
) extends SourceWithHealthEntry {
@ -54,6 +55,7 @@ object AmenitySource {
health,
obj.Orientation,
Nil,
SourceEntry(obj.Owner),
hackData,
sourcing.UniqueAmenity(obj.Zone.Number, obj.GUID, obj.Position)
)

View file

@ -1,6 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.avatar.scoring.Life
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.mount.Mountable
@ -28,7 +29,7 @@ final case class PlayerSource(
jumping: Boolean,
Modifiers: ResistanceProfile,
bep: Long,
kills: Seq[Any],
progress: Life,
unique: UniquePlayer
) extends SourceWithHealthEntry {
override def Name: String = unique.name
@ -46,6 +47,7 @@ object PlayerSource {
val exosuit = p.ExoSuit
val faction = p.Faction
val seatedEntity = mountableAndSeat(p)
val avatar = p.avatar
PlayerSource(
p.Definition,
exosuit,
@ -58,8 +60,8 @@ object PlayerSource {
p.Crouching,
p.Jumping,
ExoSuitDefinition.Select(exosuit, faction),
p.avatar.bep,
kills = Nil,
avatar.bep,
progress = avatar.scorecard.CurrentLife,
UniquePlayer(p.CharId, p.Name, p.Sex, faction)
)
}
@ -78,7 +80,7 @@ object PlayerSource {
jumping = false,
GlobalDefinitions.Standard,
bep = 0L,
kills = Nil,
progress = tokenLife,
UniquePlayer(0L, name, CharacterSex.Male, faction)
)
}
@ -116,6 +118,7 @@ object PlayerSource {
def inSeat(player: Player, source: SourceEntry, seatNumber: Int): PlayerSource = {
val exosuit = player.ExoSuit
val faction = player.Faction
val avatar = player.avatar
PlayerSource(
player.Definition,
exosuit,
@ -128,8 +131,8 @@ object PlayerSource {
player.Crouching,
player.Jumping,
ExoSuitDefinition.Select(exosuit, faction),
player.avatar.bep,
kills = Nil,
avatar.bep,
progress = tokenLife,
UniquePlayer(player.CharId, player.Name, player.Sex, faction)
)
}
@ -142,4 +145,6 @@ object PlayerSource {
* the others first: this will be my guest-gift to you.
*/
final val Nobody = PlayerSource("Nobody", PlanetSideEmpire.NEUTRAL, Vector3.Zero)
private val tokenLife: Life = Life()
}

View file

@ -5,12 +5,9 @@ import akka.actor.{Actor, Cancellable}
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.zones.Zone
import net.psforever.objects._
import net.psforever.packet.game.{
CargoMountPointStatusMessage,
ObjectAttachMessage,
ObjectDetachMessage,
PlanetsideAttributeMessage
}
import net.psforever.objects.sourcing.VehicleSource
import net.psforever.objects.vital.VehicleCargoMountActivity
import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage}
import net.psforever.types.{BailType, CargoStatus, PlanetSideGUID, Vector3}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.Service
@ -117,7 +114,7 @@ trait CarrierBehavior {
case _ =>
obj.CargoHold(mountPoint) match {
case Some(hold) if hold.isOccupied && hold.occupant.get.GUID == cargo_guid =>
hold.unmount(hold.occupant.get)
CarrierBehavior.CargoDismountAction(obj, hold.occupant.get, hold, BailType.Normal)
case _ => ;
}
false
@ -213,10 +210,8 @@ object CarrierBehavior {
if (distance <= 64) {
//cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it
log.debug(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance")
hold.mount(cargo)
cargo.MountedIn = carrierGUID
CargoMountAction(carrier, cargo, hold, carrierGUID)
cargo.Velocity = None
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID)
zone.VehicleEvents ! VehicleServiceMessage(
s"${cargo.Actor}",
VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health))
@ -358,9 +353,7 @@ object CarrierBehavior {
//obviously, don't do this
} else if (iteration > 40) {
//cargo vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold
hold.mount(cargo)
cargo.MountedIn = carrierGUID
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID)
CargoMountAction(carrier, cargo, hold, carrierGUID)
CargoMountBehaviorForAll(carrier, cargo, mountPoint)
zone.actor ! ZoneActor.RemoveFromBlockMap(cargo)
false
@ -441,9 +434,10 @@ object CarrierBehavior {
val zone = carrier.Zone
carrier.CargoHolds.find({ case (_, hold) => hold.occupant.contains(cargo) }) match {
case Some((mountPoint, hold)) =>
cargo.MountedIn = None
hold.unmount(
CarrierBehavior.CargoDismountAction(
carrier,
cargo,
hold,
if (bailed) BailType.Bailed else if (kicked) BailType.Kicked else BailType.Normal
)
val driverOpt = cargo.Seats(0).occupant
@ -653,4 +647,40 @@ object CarrierBehavior {
)
msgs
}
/**
* na
* @param carrier the ferrying vehicle
* @param cargo the ferried vehicle
* @param hold na
* @param carrierGuid the ferrying vehicle's unique identifier
*/
private def CargoMountAction(
carrier: Vehicle,
cargo: Vehicle,
hold: Cargo,
carrierGuid: PlanetSideGUID): Unit = {
hold.mount(cargo)
cargo.MountedIn = carrierGuid
cargo.LogActivity(VehicleCargoMountActivity(VehicleSource(carrier), VehicleSource(cargo), carrier.Zone.Number))
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGuid)
}
/**
* na
* @param carrier the ferrying vehicle
* @param cargo the ferried vehicle
* @param hold na
* @param bailType na
*/
private def CargoDismountAction(
carrier: Vehicle,
cargo: Vehicle,
hold: Cargo,
bailType: BailType.Value
): Unit = {
cargo.MountedIn = None
hold.unmount(cargo, bailType)
cargo.LogActivity(VehicleCargoMountActivity(VehicleSource(carrier), VehicleSource(cargo), carrier.Zone.Number))
}
}

View file

@ -170,8 +170,8 @@ class BfrControl(vehicle: Vehicle)
specialArmWeaponEquipManagement(item, slot, handiness)
}
override def dismountCleanup(seatBeingDismounted: Int): Unit = {
super.dismountCleanup(seatBeingDismounted)
override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = {
super.dismountCleanup(seatBeingDismounted, player)
if (!vehicle.Seats.values.exists(_.isOccupied)) {
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
case Some(subsys) =>

View file

@ -34,9 +34,9 @@ class DeployingVehicleControl(vehicle: Vehicle)
case msg : Deployment.TryUndeploy =>
deployBehavior.apply(msg)
case msg @ Mountable.TryDismount(_, seat_num, _) =>
case msg @ Mountable.TryDismount(player, seat_num, _) =>
dismountBehavior.apply(msg)
dismountCleanup(seat_num)
dismountCleanup(seat_num, player)
}
/**

View file

@ -20,10 +20,10 @@ import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.repair.RepairableVehicle
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.sourcing.{SourceEntry, VehicleSource}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge}
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, VehicleDismountActivity, VehicleMountActivity}
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.zones._
@ -128,9 +128,9 @@ class VehicleControl(vehicle: Vehicle)
mountBehavior.apply(msg)
mountCleanup(mount_point, player)
case msg @ Mountable.TryDismount(_, seat_num, _) =>
case msg @ Mountable.TryDismount(player, seat_num, _) =>
dismountBehavior.apply(msg)
dismountCleanup(seat_num)
dismountCleanup(seat_num, player)
case CommonMessages.ChargeShields(amount, motivator) =>
chargeShields(amount, motivator.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) })
@ -237,9 +237,9 @@ class VehicleControl(vehicle: Vehicle)
def commonDisabledBehavior: Receive = checkBehavior
.orElse {
case msg @ Mountable.TryDismount(_, seat_num, _) =>
case msg @ Mountable.TryDismount(user, seat_num, _) =>
dismountBehavior.apply(msg)
dismountCleanup(seat_num)
dismountCleanup(seat_num, user)
case Vehicle.Deconstruct(time) =>
time match {
@ -297,6 +297,8 @@ class VehicleControl(vehicle: Vehicle)
val obj = MountableObject
obj.PassengerInSeat(user) match {
case Some(seatNumber) =>
val vsrc = VehicleSource(vehicle)
user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vehicle, vsrc), vehicle.Zone.Number))
//if the driver mount, change ownership if that is permissible for this vehicle
if (seatNumber == 0 && !obj.OwnerName.contains(user.Name) && obj.Definition.CanBeOwned.nonEmpty) {
//whatever vehicle was previously owned
@ -325,7 +327,7 @@ class VehicleControl(vehicle: Vehicle)
vehicle.DeploymentState == DriveState.Deployed || super.dismountTest(obj, seatNumber, user)
}
def dismountCleanup(seatBeingDismounted: Int): Unit = {
def dismountCleanup(seatBeingDismounted: Int, user: Player): Unit = {
val obj = MountableObject
// Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount
if (!obj.Seats(0).isOccupied) {
@ -340,6 +342,7 @@ class VehicleControl(vehicle: Vehicle)
)
}
if (!obj.Seats(seatBeingDismounted).isOccupied) { //seat was vacated
user.LogActivity(VehicleDismountActivity(VehicleSource(vehicle), PlayerSource(user), vehicle.Zone.Number))
//we were only owning the vehicle while we sat in its driver seat
val canBeOwned = obj.Definition.CanBeOwned
if (canBeOwned.contains(false) && seatBeingDismounted == 0) {

View file

@ -50,8 +50,30 @@ final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
extends GeneralActivity
final case class VehicleDismountActivity(vehicle: VehicleSource, player: PlayerSource)
extends GeneralActivity
sealed trait VehicleMountChange extends GeneralActivity {
def vehicle: VehicleSource
def zoneNumber: Int
}
sealed trait VehiclePassengerMountChange extends VehicleMountChange {
def player: PlayerSource
}
sealed trait VehicleCargoMountChange extends VehicleMountChange {
def cargo: VehicleSource
}
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 VehicleCargoMountActivity(vehicle: VehicleSource, cargo: VehicleSource, zoneNumber: Int)
extends VehicleCargoMountChange
final case class VehicleCargoDismountActivity(vehicle: VehicleSource, cargo: VehicleSource, zoneNumber: Int)
extends VehicleCargoMountChange
final case class Contribution(src: SourceUniqueness, entries: List[InGameActivity])
extends GeneralActivity {

View file

@ -4,7 +4,6 @@ package net.psforever.objects.zones.exp
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.actor.typed.{Behavior, SupervisorStrategy}
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
@ -27,45 +26,6 @@ object ExperienceCalculator {
RewardThisDeath(SourceEntry(obj), obj.LastDamage, obj.History)
}
}
final case class RewardOurSupporters(target: SourceEntry, history: Iterable[InGameActivity], kill: Kill, bep: Long) extends Command
object RewardOurSupporters {
def apply(obj: PlanetSideGameObject with FactionAffinity with InGameHistory, kill: Kill): RewardOurSupporters = {
RewardOurSupporters(SourceEntry(obj), obj.History, kill, kill.experienceEarned)
}
}
def calculateExperience(
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
val lifespan = (history.headOption, history.lastOption) match {
case (Some(spawn), Some(death)) => death.time - spawn.time
case _ => 0L
}
val wasEverAMax = Support.wasEverAMax(victim, history)
val base = if (wasEverAMax) { //shamed
250L
} else if (victim.Seated || victim.kills.nonEmpty) {
100L
} else if (lifespan > 15000L) {
50L
} else {
1L
}
if (base > 1) {
//black ops modifier
//TODO x10
base
} else {
base
}
}
}
sealed case class InheritedAssistEntry(assists: Iterable[ContributionStatsOutput]) {
val time: Long = System.currentTimeMillis()
}
class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone)
@ -78,9 +38,6 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
KillAssists.rewardThisPlayerDeath(victim, lastDamage, history, zone.AvatarEvents)
case RewardOurSupporters(target: PlayerSource, history, kill, bep) =>
KillContributions.rewardTheseSupporters(target, history, kill, bep, zone.AvatarEvents)
case _ => ()
}
Behaviors.same

View file

@ -24,12 +24,7 @@ object KillAssists {
case _ => false
}
if (spawnIndex == -1 || endIndex == -1) {
Nil //throw VitalsHistoryException(history.head, "vitals history does not contain expected conditions")
// } else
// if (spawnIndex == -1) {
// Nil //throw VitalsHistoryException(history.head, "vitals history does not contain initial spawn conditions")
// } else if (endIndex == -1) {
// Nil //throw VitalsHistoryException(history.last, "vitals history does not contain end of life conditions")
Nil
} else {
history.slice(spawnIndex, endIndex)
}
@ -54,13 +49,57 @@ object KillAssists {
.collect { case (dam, Some(adv)) => (dam, adv.attacker) }
}
private[exp] def calculateExperience(
killer: PlayerSource,
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
/**
* "Menace" is a crude measurement of how much consistent destructive power a player has been demonstrating.
* Within the last ten kills, the rate of the player's killing speed is measured.
* 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
*/
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
}
//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] = {
var onGoingMercy: Long = mercy
list.takeWhile { time =>
if (time < testValue) {
true
} else if (time - onGoingMercy - 1 < testValue) {
onGoingMercy = math.ceil(onGoingMercy * 0.65).toLong
true
} else {
false
}
}
}
private def calculateExperience(
killer: PlayerSource,
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
//base value (the kill experience before modifiers)
val base = ExperienceCalculator.calculateExperience(victim, history)
val base = Support.baseExperience(victim, history)
if (base > 1) {
//battle rank disparity modifiers
val battleRankDisparity = {
@ -81,7 +120,9 @@ object KillAssists {
math.floor(-0.15f * base - killerLevel + victimLevel).toLong
}
}
math.max(1, base + battleRankDisparity)
//menace modifiers
val menace = 1f + calculateMenace(victim) * 0.14f
math.max(1, (base + battleRankDisparity) * menace).toLong
} else {
base
}
@ -105,7 +146,7 @@ object KillAssists {
case _ =>
val assists = collectKillAssistsForPlayer(victim, shortHistory, None)
val fullBep = ExperienceCalculator.calculateExperience(victim, shortHistory)
val fullBep = Support.baseExperience(victim, shortHistory)
val hitSquad = assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq

View file

@ -2,13 +2,14 @@
package net.psforever.objects.zones.exp
import akka.actor.ActorRef
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.avatar.scoring.{Assist, Kill}
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, VehicleDismountActivity}
import net.psforever.objects.vital.{Contribution, InGameActivity, RevivingActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleCargoMountChange, VehicleDismountActivity, VehicleMountActivity, VehiclePassengerMountChange}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire
import net.psforever.types.{PlanetSideEmpire, Vector3}
import scala.collection.mutable
@ -22,7 +23,10 @@ object KillContributions {
medical_terminal,
adv_med_terminal,
crystals_health_a,
crystals_health_b
crystals_health_b,
bfr_rearm_terminal,
multivehicle_rearm_terminal,
lodestar_repair_terminal
).collect { _.ObjectId }
}
@ -49,25 +53,34 @@ object KillContributions {
)
}
//sort by applicable time periods, as long as the longer period is represented by activity
val otherContributionCalculations = contributionScoringAndCulling(faction, kill, Seq(target.CharId), contributions, bep)(_, _)
val finalContributions = if (longHistory.isEmpty) {
val contributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
contributionProcess.submit(shortHistory)
val contributionEntries = otherContributionCalculations(shortHistory, contributionProcess.output())
contributionEntries
.keys
.map { composeContributionOutput(_, contributionEntries, contributionEntries, bep) }
} else {
val otherContributionCalculations = contributionScoringAndCulling(faction, kill, contributions, bep)(_, _)
val finalContributions = if (longHistory.nonEmpty && KillAssists.calculateMenace(target) > 2) {
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())
val shortContributionEntries = otherContributionCalculations(shortHistory, shortContributionProcess.output())
(longContributionEntries.keys ++ shortContributionEntries.keys)
longContributionEntries.remove(target.CharId)
longContributionEntries.remove(kill.victim.CharId)
shortContributionEntries.remove(target.CharId)
shortContributionEntries.remove(kill.victim.CharId)
(longContributionEntries ++ shortContributionEntries)
.toSeq
.distinct
.map { composeContributionOutput(_, shortContributionEntries, longContributionEntries, bep) }
.distinctBy(_._2.player.unique)
.map { case (_, stats) =>
composeContributionOutput(stats.player, shortContributionEntries, longContributionEntries, bep)
}
} else {
val contributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
contributionProcess.submit(shortHistory)
val contributionEntries = otherContributionCalculations(shortHistory, contributionProcess.output())
contributionEntries.remove(target.CharId)
contributionEntries.remove(kill.victim.CharId)
contributionEntries
.map { case (_, stats) =>
composeContributionOutput(stats.player, contributionEntries, contributionEntries, bep)
}
}
//take the output and transform that into contribution distribution data
val victim = kill.victim
@ -98,7 +111,6 @@ object KillContributions {
private def contributionScoringAndCulling(
faction: PlanetSideEmpire.Value,
kill: Kill,
excludeTargets: Seq[Long],
contributions: Map[SourceUniqueness, List[InGameActivity]],
bep: Long
)
@ -113,11 +125,11 @@ object KillContributions {
contributionEntries.put(id, stat.copy(weapons = newWeaponStats))
}
contributeWithKillWhileMountedActivity(faction, kill, history, contributionEntries)
contributeWithVehicleTransportActivity(history, contributionEntries)
contributeWithRevivalActivity(history, contributionEntries)
contributeWithVehicleTransportActivity(history, contributionEntries)
contributeWithVehicleCargoTransportActivity(history, contributionEntries)
contributeWithTerminalActivity(faction, history, contributions, contributionEntries)
contributionEntries.remove(0)
excludeTargets.foreach { contributionEntries.remove }
contributionEntries
}
@ -185,23 +197,99 @@ object KillContributions {
history: List[InGameActivity],
participants: mutable.LongMap[ContributionStats]
): List[InGameActivity] = {
val shortHistory = history.collect { case out: VehicleDismountActivity => out }
shortHistory
/*
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
certain other qualifications of the prior mounting must be met before the support bonus applies
*/
val dismountActivity = history
.collect {
case inAndOut: VehiclePassengerMountChange
if !inAndOut.vehicle.owner.contains(inAndOut.player) => inAndOut
}
.grouped(2)
.collect {
case List(in: VehicleMountActivity, out: VehicleDismountActivity)
if in.vehicle.unique == out.vehicle.unique &&
out.vehicle.Faction == out.player.Faction &&
(in.vehicle.Definition == GlobalDefinitions.router || {
val inTime = in.time
val outTime = out.time
out.player.progress.kills.exists { death =>
val deathTime = death.info.interaction.hitTime
inTime < deathTime && deathTime <= outTime
}
} || {
val sameZone = in.zoneNumber == out.zoneNumber
val distanceMoved = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
val timeSpent = out.time - in.time
timeSpent >= 210000 /* 3:30 */ ||
(sameZone && (distanceMoved > 160000f || distanceMoved > 10000f && timeSpent >= 60000)) |
(!sameZone && (distanceMoved > 10000f || timeSpent >= 120000))
}) =>
out
}.toList
//apply
dismountActivity
.groupBy { a => a.vehicle.owner }
.foreach { case (Some(owner), dismountsFromVehicle) =>
.collect { case (Some(owner), dismountsFromVehicle) =>
val numberOfDismounts = dismountsFromVehicle.size
contributeWithCombinedActivity(
owner.CharId,
dismountsFromVehicle.map { act => WeaponStats(act.vehicle.Definition.ObjectId, 1, 1, act.time, 15f) },
dismountsFromVehicle.map { act => WeaponStats(act.vehicle.Definition.ObjectId, 0, numberOfDismounts, act.time, 15f) },
dismountsFromVehicle.head.vehicle.owner.get,
numberOfDismounts,
numberOfDismounts,
amount = 0,
total = 0,
numberOfDismounts,
dismountsFromVehicle.maxBy(_.time).time,
participants
)
}
shortHistory
dismountActivity
}
private def contributeWithVehicleCargoTransportActivity(
history: List[InGameActivity],
participants: mutable.LongMap[ContributionStats]
): List[InGameActivity] = {
/*
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
certain other qualifications of the prior mounting must be met before the support bonus applies
*/
val dismountActivity = history
.collect {
case inAndOut: VehicleCargoMountChange if inAndOut.vehicle.owner.nonEmpty => inAndOut
}
.grouped(2)
.collect {
case List(in: VehicleCargoMountActivity, out: VehicleCargoDismountActivity)
if in.vehicle.unique == out.vehicle.unique &&
out.vehicle.Faction == out.cargo.Faction &&
(in.vehicle.Definition == GlobalDefinitions.router || {
val distanceMoved = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
val timeSpent = out.time - in.time
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(act.vehicle.Definition.ObjectId, 0, numberOfDismounts, act.time, 15f) },
dismountsFromVehicle.head.vehicle.owner.get,
amount = 0,
total = 0,
numberOfDismounts,
dismountsFromVehicle.maxBy(_.time).time,
participants
)
}
dismountActivity
}
private def contributeWithRevivalActivity(
@ -251,11 +339,22 @@ object KillContributions {
case (t, terminal, Some(info)) if terminal.Faction != faction =>
participants.getOrElseUpdate(
info.player.CharId,
ContributionStats(info.player, Seq(WeaponStats(0, 1, 1, t.time, 10f)), 1, 1, 1, t.time)
ContributionStats(info.player, Seq(WeaponStats(terminal.Definition.ObjectId, 0, 1, t.time, 10f)), 0, 0, 1, t.time)
)
t
case (t, terminal, _) =>
extractContributionsForEntityByUser(faction, terminal, contributions, participants)
terminal.installation match {
case v: VehicleSource =>
v.owner.collect {
owner =>
participants.getOrElseUpdate(
owner.CharId,
ContributionStats(owner, Seq(WeaponStats(terminal.Definition.ObjectId, 0, 1, t.time, 10f)), 0, 0, 1, t.time)
)
}
case _ =>
extractContributionsForEntityByUser(faction, terminal, contributions, participants)
}
t
}.toList
}
@ -317,32 +416,33 @@ object KillContributions {
}
private def composeContributionOutput(
charId: Long,
player: PlayerSource,
shortPeriod: mutable.LongMap[ContributionStats],
longPeriod: mutable.LongMap[ContributionStats],
bep: Long
): (Long, ContributionStatsOutput) = {
shortPeriod
val charId = player.CharId
longPeriod
.get(charId)
.collect {
case entry =>
val weapons = entry.weapons
(
entry.player,
weapons.map { _.equipment_id },
math.min(weapons.foldLeft(0f)(_ + _.contributions).toLong, bep)
weapons.filter { _.amount == 0 }.map { _.equipment_id },
(0.9f * math.min(weapons.foldLeft(0f)(_ + _.contributions), bep.toFloat)).toLong
)
}
.orElse {
longPeriod
shortPeriod
.get(charId)
.collect {
case entry =>
val weapons = entry.weapons
(
entry.player,
weapons.filter { _.amount == 0 }.map { _.equipment_id },
(0.9f * math.min(weapons.foldLeft(0f)(_ + _.contributions), bep.toFloat)).toLong
weapons.map { _.equipment_id },
math.min(weapons.foldLeft(0f)(_ + _.contributions).toLong, bep)
)
}
}
@ -350,7 +450,7 @@ object KillContributions {
Some((PlayerSource.Nobody, Seq(0), 0L))
}
.collect {
case (player, weaponIds, experience) =>
case (player, weaponIds, experience) if experience > 0 =>
(charId, ContributionStatsOutput(player, weaponIds, experience.toFloat))
}
.get

View file

@ -10,6 +10,7 @@ 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.objects.zones.exp.KillAssists.calculateMenace
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
import net.psforever.zones.Zones
@ -18,6 +19,33 @@ import scala.collection.mutable
object Support {
private type SupportActivity = InGameActivity with SupportActivityCausedByAnother
private[exp] def baseExperience(
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
val lifespan = (history.headOption, history.lastOption) match {
case (Some(spawn), Some(death)) => death.time - spawn.time
case _ => 0L
}
val wasEverAMax = Support.wasEverAMax(victim, history)
val base = if (wasEverAMax) { //shamed
250L
} else if (victim.Seated || victim.progress.kills.nonEmpty) {
100L
} else if (lifespan > 15000L) {
50L
} else {
1L
}
if (base > 1) {
//black ops modifier
//TODO x10
base
} else {
base
}
}
private[exp] def onlyOriginalAssistEntries(
first: mutable.LongMap[ContributionStatsOutput],
second: mutable.LongMap[ContributionStatsOutput],

View file

@ -0,0 +1,44 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.actor.typed.{Behavior, SupervisorStrategy}
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.zones.Zone
object SupportExperienceCalculator {
def apply(zone: Zone): Behavior[Command] =
Behaviors.supervise[Command] {
Behaviors.setup(context => new SupportExperienceCalculator(context, zone))
}.onFailure[Exception](SupervisorStrategy.restart)
sealed trait Command
final case class RewardOurSupporters(target: SourceEntry, history: Iterable[InGameActivity], kill: Kill, bep: Long) extends Command
object RewardOurSupporters {
def apply(obj: PlanetSideGameObject with FactionAffinity with InGameHistory, kill: Kill): RewardOurSupporters = {
RewardOurSupporters(SourceEntry(obj), obj.History, kill, kill.experienceEarned)
}
}
}
class SupportExperienceCalculator(context: ActorContext[SupportExperienceCalculator.Command], zone: Zone)
extends AbstractBehavior[SupportExperienceCalculator.Command](context) {
import SupportExperienceCalculator._
def onMessage(msg: Command): Behavior[Command] = {
msg match {
case RewardOurSupporters(target: PlayerSource, history, kill, bep) =>
KillContributions.rewardTheseSupporters(target, history, kill, bep, zone.AvatarEvents)
case _ => ()
}
Behaviors.same
}
}

View file

@ -1,10 +0,0 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import net.psforever.objects.vital.InGameActivity
final case class VitalsHistoryException(
head: InGameActivity, //InGameActivity might be more suitable?
private val message: String = "",
private val cause: Throwable = None.orNull
) extends Exception(message, cause)