diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala index 51fa82a6e..990ab35da 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -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") } diff --git a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala index 54be39a72..43ee1ffa4 100644 --- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala +++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala @@ -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 diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 8388008b6..201d39d8d 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -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 => diff --git a/src/main/scala/net/psforever/objects/sourcing/AmenitySource.scala b/src/main/scala/net/psforever/objects/sourcing/AmenitySource.scala index 56e9b600f..23893df77 100644 --- a/src/main/scala/net/psforever/objects/sourcing/AmenitySource.scala +++ b/src/main/scala/net/psforever/objects/sourcing/AmenitySource.scala @@ -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) ) diff --git a/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala b/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala index 1e2874201..49e88c074 100644 --- a/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala +++ b/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala @@ -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() } diff --git a/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala index ee71b68d4..90e580bf4 100644 --- a/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala +++ b/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala @@ -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)) + } } diff --git a/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala index 402f9cd32..69d60d4b1 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala @@ -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) => diff --git a/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala index 445782021..4c33ec918 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala @@ -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) } /** diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala index ae24739b1..210cc41c6 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -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) { diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala index be18371a8..5f713a784 100644 --- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala +++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala @@ -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 { diff --git a/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala b/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala index 1f1541c7a..15efb5f95 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala @@ -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 diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala index bd8ea8d71..028d1cf82 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala @@ -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 diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala index b02ace185..2e736a6b1 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala @@ -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 diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala index a5334b102..975784381 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala @@ -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], diff --git a/src/main/scala/net/psforever/objects/zones/exp/SupportExperienceCalculator.scala b/src/main/scala/net/psforever/objects/zones/exp/SupportExperienceCalculator.scala new file mode 100644 index 000000000..6dbda6ef2 --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/SupportExperienceCalculator.scala @@ -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 + } +} diff --git a/src/main/scala/net/psforever/objects/zones/exp/VitalsHistoryException.scala b/src/main/scala/net/psforever/objects/zones/exp/VitalsHistoryException.scala deleted file mode 100644 index e5fd7e67e..000000000 --- a/src/main/scala/net/psforever/objects/zones/exp/VitalsHistoryException.scala +++ /dev/null @@ -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)