From b866aa8a309c88e791a770a602f7b1a3a53ac7a2 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 20 Mar 2023 23:49:46 -0400 Subject: [PATCH] importing controlled implementation changes from original exp-for-kda branch; primary kill experience rewarded --- .../actors/session/AvatarActor.scala | 25 +- .../support/SessionAvatarHandlers.scala | 8 +- .../net/psforever/actors/zone/ZoneActor.scala | 19 +- .../objects/avatar/PlayerControl.scala | 2 + .../objects/vital/InGameHistory.scala | 6 +- .../zones/exp/ExperienceCalculator.scala | 336 ++++++++++++++++++ .../objects/zones/exp/KillDeathAssists.scala | 40 +++ .../psforever/objects/zones/exp/Stats.scala | 26 ++ .../zones/exp/VitalsHistoryException.scala | 10 + .../services/avatar/AvatarService.scala | 9 + .../avatar/AvatarServiceMessage.scala | 3 + .../avatar/AvatarServiceResponse.scala | 3 + 12 files changed, 469 insertions(+), 18 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala create mode 100644 src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala create mode 100644 src/main/scala/net/psforever/objects/zones/exp/Stats.scala create mode 100644 src/main/scala/net/psforever/objects/zones/exp/VitalsHistoryException.scala diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 719a54c6..3bd05f35 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -5,6 +5,8 @@ import akka.actor.Cancellable import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} 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.{Death, EquipmentStat, KDAStat, Kill} import org.joda.time.{LocalDateTime, Seconds} import scala.collection.mutable import scala.concurrent.{ExecutionContextExecutor, Future, Promise} @@ -26,7 +28,6 @@ import net.psforever.objects.avatar.{ Shortcut => AvatarShortcut, SpecialCarry } -import net.psforever.objects.avatar.scoring.{Death, EquipmentStat, KDAStat, Kill} import net.psforever.objects.definition._ import net.psforever.objects.definition.converter.CharacterSelectConverter import net.psforever.objects.equipment.{Equipment, EquipmentSlot} @@ -192,6 +193,9 @@ object AvatarActor { /** Award battle experience points */ final case class AwardBep(bep: Long, modifier: ExperienceType) extends Command + /** ... */ + final case class UpdateKillsDeathsAssists(stat: KDAStat) extends Command + /** Set total battle experience points */ final case class SetBep(bep: Long) extends Command @@ -1642,6 +1646,10 @@ class AvatarActor( updateToolDischarge(stats) Behaviors.same + case UpdateKillsDeathsAssists(stat) => + updateKillsDeathsAssists(stat) + Behaviors.same + case AwardBep(bep, modifier) => setBep(avatar.bep + bep, modifier) Behaviors.same @@ -2936,18 +2944,17 @@ class AvatarActor( val player = _session.player kdaStat match { case kill: Kill => - val _ = PlayerSource(player) + val playerSource = PlayerSource(player) (kill.info.interaction.cause match { case pr: ProjectileReason => pr.projectile.mounted_in.map { a => zone.GUID(a._1) } - case _ => None - }) match { - case Some(Some(_: Vitality)) => - //zone.actor ! ZoneActor.RewardOurSupporters(playerSource, obj.History, kill, exp) - case _ => ; + case _ => None + }).collect { + case Some(obj: Vitality) => + zone.actor ! ZoneActor.RewardOurSupporters(playerSource, obj.History, kill, exp) } - //zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.History, kill, exp) + zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.History, kill, exp) case _: Death => - player.Zone.AvatarEvents ! AvatarServiceMessage( + zone.AvatarEvents ! AvatarServiceMessage( player.Name, AvatarAction.SendResponse( Service.defaultPlayerGUID, diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index 0f83c48b..dd08f21e 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -224,10 +224,9 @@ class SessionAvatarHandlers( case AvatarResponse.DestroyDisplay(killer, victim, method, unk) if killer.CharId == avatar.id && killer.Faction != victim.Faction => - // TODO Temporary thing that should go somewhere else and use proper xp values sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk)) - avatarActor ! AvatarActor.AwardBep((1000 * Config.app.game.bepRate).toLong, ExperienceType.Normal) - avatarActor ! AvatarActor.AwardCep((100 * Config.app.game.cepRate).toLong) + //TODO Temporary thing that should go somewhere else and use proper xp values +// avatarActor ! AvatarActor.AwardCep((100 * Config.app.game.cepRate).toLong) case AvatarResponse.Destroy(victim, killer, weapon, pos) => // guid = victim // killer = killer @@ -396,6 +395,9 @@ class SessionAvatarHandlers( sessionData.kitToBeUsed = None sendResponse(ChatMsg(ChatMessageType.UNK_225, msg)) + case AvatarResponse.UpdateKillsDeathsAssists(_, kda) => + avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda) + case AvatarResponse.SendResponse(msg) => sendResponse(msg) diff --git a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala index 39f9756c..e355b1b7 100644 --- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala +++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala @@ -7,12 +7,15 @@ import net.psforever.objects.equipment.Equipment import net.psforever.objects.serverobject.structures.{StructureType, WarpGate} import net.psforever.objects.zones.Zone import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup} -import net.psforever.objects.{ConstructionItem, Player, Vehicle} +import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} - import akka.actor.typed.scaladsl.adapter._ import net.psforever.actors.zone.building.MajorFacilityLogic +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.util.Database._ import net.psforever.persistence @@ -70,6 +73,9 @@ object ZoneActor { // Once they do, we won't need this anymore final case class ZoneMapUpdate() extends Command + final case class RewardThisDeath(entity: PlanetSideGameObject with FactionAffinity with InGameHistory) extends Command + + final case class RewardOurSupporters(target: SourceEntry, history: Iterable[InGameActivity], kill: Kill, bep: Long) extends Command } class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone) @@ -79,7 +85,8 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone) import ctx._ private[this] val log = org.log4s.getLogger - val players: mutable.ListBuffer[Player] = mutable.ListBuffer() + private val players: mutable.ListBuffer[Player] = mutable.ListBuffer() + private val experience: ActorRef[ExperienceCalculator.Command] = context.spawnAnonymous(ExperienceCalculator(zone)) zone.actor = context.self zone.init(context.toClassic) @@ -143,6 +150,12 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone) case HotSpotActivity(defender, attacker, location) => zone.Activity ! Zone.HotSpot.Activity(defender, attacker, location) + case RewardThisDeath(entity) => + experience ! ExperienceCalculator.RewardThisDeath(entity) + + case RewardOurSupporters(target, history, kill, bep) => + () + case ZoneMapUpdate() => zone.Buildings .filter(_._2.BuildingType == StructureType.Facility) diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 3bc22033..bb2dfc6b 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -3,6 +3,7 @@ package net.psforever.objects.avatar import akka.actor.{Actor, ActorRef, Props, typed} import net.psforever.actors.session.AvatarActor +import net.psforever.actors.zone.ZoneActor import net.psforever.login.WorldSession.{DropEquipmentFromInventory, HoldNewEquipmentUp, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} import net.psforever.objects._ import net.psforever.objects.ce.Deployable @@ -1052,6 +1053,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm case _ => events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0)) } + zone.actor ! ZoneActor.RewardThisDeath(player) } def suicide() : Unit = { diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala index ae832624..2cc7f603 100644 --- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala +++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala @@ -148,7 +148,7 @@ trait InGameHistory { /** * An in-game event must be recorded. - * Add new entry to the front of the list (for recent activity). + * Add new entry to the list (for recent activity). * @param action the fully-informed entry * @return the list of previous changes to this entity */ @@ -157,14 +157,14 @@ trait InGameHistory { /** * An in-game event must be recorded. - * Add new entry to the front of the list (for recent activity). + * Add new entry to the list (for recent activity). * @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) => - history = act +: history + history = history :+ act case None => () } history diff --git a/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala b/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala new file mode 100644 index 00000000..15f23698 --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala @@ -0,0 +1,336 @@ +// 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.{Assist, Death, Kill} +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} +import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, InGameHistory, ReconstructionActivity, RepairFromExoSuitChange, RepairingActivity, SpawningActivity} +import net.psforever.objects.vital.interaction.{Adversarial, DamageResult} +import net.psforever.objects.zones.Zone +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.types.{ExoSuitType, PlanetSideEmpire} + +import scala.collection.mutable +import scala.concurrent.duration._ + +object ExperienceCalculator { + def apply(zone: Zone): Behavior[Command] = + Behaviors.supervise[Command] { + Behaviors.setup(context => new ExperienceCalculator(context, zone)) + }.onFailure[Exception](SupervisorStrategy.restart) + + sealed trait Command + + final case class RewardThisDeath(victim: SourceEntry, lastDamage: Option[DamageResult], history: Iterable[InGameActivity]) + extends ExperienceCalculator.Command + + object RewardThisDeath { + def apply(obj: PlanetSideGameObject with FactionAffinity with InGameHistory): RewardThisDeath = { + RewardThisDeath(SourceEntry(obj), obj.LastDamage, obj.History) + } + } + + 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 = victim.ExoSuit == ExoSuitType.MAX || history.exists { + case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX + case ReconstructionActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX + case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX + case _ => false + } + 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 + } + } +} + +class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone) + extends AbstractBehavior[ExperienceCalculator.Command](context) { + + import ExperienceCalculator._ + + def onMessage(msg: Command): Behavior[Command] = { + msg match { + case RewardThisDeath(victim: PlayerSource, lastDamage, history) => + rewardThisPlayerDeath( + victim, + lastDamage, + limitHistoryToThisLife(history.toList) + ) + case _ => + () + } + Behaviors.same + } + + def rewardThisPlayerDeath( + victim: PlayerSource, + lastDamage: Option[DamageResult], + history: List[InGameActivity] + ): Unit = { + val everyone = determineKiller(lastDamage, history) match { + case Some((result, killer: PlayerSource)) => + val assists = collectAssistsForPlayer(victim, history, Some(killer)) + val fullBep = KillDeathAssists.calculateExperience(killer, victim, history) + 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 }, history.last.time - history.head.time, fullBep)) +: hitSquad + + case _ => + val assists = collectAssistsForPlayer(victim, history, None) + val fullBep = ExperienceCalculator.calculateExperience(victim, history) + val hitSquad = assists.map { + case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong)) + }.toSeq + (victim, Death(hitSquad.map { _._1 }, history.last.time - history.head.time, fullBep)) +: hitSquad + } + val events = zone.AvatarEvents + everyone.foreach { case (p, kda) => + events ! AvatarServiceMessage(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda)) + } + } + + def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = { + val spawnIndex = history.indexWhere { + case SpawningActivity(_, _, _) => true + case _ => false + } + val endIndex = history.lastIndexWhere { + case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0 + 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") + } else { + history.slice(spawnIndex, endIndex) + } + } + + def determineKiller(lastDamageActivity: Option[DamageResult], history: List[InGameActivity]): Option[(DamageResult, SourceEntry)] = { + 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, Some(adv)) => (dam, adv.attacker) } + } + + private[exp] def collectAssistsForPlayer( + victim: PlayerSource, + history: List[InGameActivity], + killerOpt: Option[PlayerSource] + ): Iterable[ContributionStatsOutput] = { + // val cardinalSin = victim.ExoSuit == ExoSuitType.MAX || history.exists { + // case SpawningActivity(p: PlayerSource,_,_) => p.ExoSuit == ExoSuitType.MAX + // case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX + // case _ => false + // } + val initialHealth = history + .headOption + .collect { case SpawningActivity(p: PlayerSource,_,_) => p.health } match { + case Some(value) => value.toFloat + case _ => 100f + } + val healthAssists = collectHealthAssists( + victim, + history, + initialHealth, + allocateContributors(healthDamageContributors) + ) + healthAssists.remove(0L) + killerOpt.map { killer => healthAssists.remove(killer.CharId) } + healthAssists.values + } + + private def allocateContributors( + tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any + ) + ( + history: List[InGameActivity], + faction: PlanetSideEmpire.Value + ): mutable.LongMap[ContributionStats] = { + /** players who have contributed to this death, and how much they have contributed
+ * key - character identifier, + * value - (player, damage, total damage, number of shots) */ + val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]() + tallyFunc(history, faction, participants) + participants + } + + + + private def healthDamageContributors( + history: List[InGameActivity], + faction: PlanetSideEmpire.Value, + participants: mutable.LongMap[ContributionStats] + ): Seq[(Long, Int)] = { + /** damage as it is measured in order (with heal-countered damage eliminated)
+ * key - character identifier, + * value - current damage contribution */ + var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]() + history.tail.foreach { + case d: DamagingActivity if d.health > 0 => + inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder) + case _: RepairingActivity => () + case h: HealingActivity => + inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder) + case _ => () + } + inOrder + } + + private def collectHealthAssists( + victim: SourceEntry, + history: List[InGameActivity], + topHealth: Float, + func: (List[InGameActivity], PlanetSideEmpire.Value)=>mutable.LongMap[ContributionStats] + ): mutable.LongMap[ContributionStatsOutput] = { + val healthAssists = func(history, victim.Faction) + .filterNot { case (_, kda) => kda.amount <= 0 } + .map { case (id, kda) => + (id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / topHealth)) + } + healthAssists.remove(victim.CharId) + healthAssists + } + + private def contributeWithDamagingActivity( + activity: DamagingActivity, + faction: PlanetSideEmpire.Value, + amount: Int, + participants: mutable.LongMap[ContributionStats], + order: Seq[(Long, Int)] + ): Seq[(Long, Int)] = { + activity.data.adversarial match { + case Some(Adversarial(attacker: PlayerSource, _, _)) + if attacker.Faction != faction => + val whoId = attacker.CharId + val wepid = activity.data.interaction.cause.attribution + val time = activity.time + val updatedEntry = participants.get(whoId) match { + case Some(mod) => + //previous attacker, just add to entry + val firstWeapon = mod.weapons.head + val weapons = if (firstWeapon.weapon_id == wepid) { + firstWeapon.copy(amount = firstWeapon.amount + amount, shots = firstWeapon.shots + 1, time = time) +: mod.weapons.tail + } else { + WeaponStats(wepid, amount, 1, time) +: mod.weapons + } + mod.copy( + amount = mod.amount + amount, + weapons = weapons, + totalDamage = mod.totalDamage + amount, + shots = mod.shots + 1, + time = activity.time + ) + case None => + //new attacker, new entry + ContributionStats( + attacker, + Seq(WeaponStats(wepid, amount, 1, time)), + amount, + amount, + 1, + time + ) + } + participants.put(whoId, updatedEntry) + order.indexWhere({ case (id, _) => id == whoId }) match { + case 0 => + //ongoing attack by same player + val entry = order.head + (entry._1, entry._2 + amount) +: order.tail + case _ => + //different player than immediate prior attacker + (whoId, amount) +: order + } + case _ => + //damage that does not lead to contribution + order.headOption match { + case Some((id, dam)) => + if (id == 0L) { + (0L, dam + amount) +: order.tail //pool + } else { + (0L, amount) +: order //new + } + case None => + order + } + } + } + + private def contributeWithRecoveryActivity( + amount: Int, + participants: mutable.LongMap[ContributionStats], + order: Seq[(Long, Int)] + ): Seq[(Long, Int)] = { + var amt = amount + var count = 0 + var newOrder: Seq[(Long, Int)] = Nil + order.takeWhile { entry => + val (id, total) = entry + if (id > 0 && total > 0) { + val part = participants(id) + if (amount > total) { + //drop this entry + participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case + amt = amt - total + } else { + //edit around the inclusion of this entry + val newTotal = total - amt + val trimmedWeapons = { + var index = -1 + var weaponSum = 0 + val pweapons = part.weapons + while (weaponSum < amt) { + index += 1 + weaponSum = weaponSum + pweapons(index).amount + } + pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size) + } + newOrder = (id, newTotal) +: newOrder + participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons)) + amt = 0 + } + } + count += 1 + amt > 0 + } + newOrder ++ order.drop(count) + } +} diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala b/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala new file mode 100644 index 00000000..4560b7a8 --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/KillDeathAssists.scala @@ -0,0 +1,40 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.zones.exp + +import net.psforever.objects.sourcing.PlayerSource +import net.psforever.objects.vital.InGameActivity + +object KillDeathAssists { + private[exp] def calculateExperience( + killer: PlayerSource, + victim: PlayerSource, + history: Iterable[InGameActivity] + ): Long = { + //base value (the kill experience before modifiers) + val base = ExperienceCalculator.calculateExperience(victim, history) + if (base > 1) { + //battle rank disparity modifiers + val battleRankDisparity = { + import net.psforever.objects.avatar.BattleRank + val killerLevel = BattleRank.withExperience(killer.bep).value + val victimLevel = BattleRank.withExperience(victim.bep).value + if (victimLevel > killerLevel || killerLevel - victimLevel < 6) { + if (killerLevel < 7) { + 6 * victimLevel + 10 + } else if (killerLevel < 12) { + (12 - killerLevel) * victimLevel + 10 + } else if (killerLevel < 25) { + 25 + victimLevel - killerLevel + } else { + 25 + } + } else { + math.floor(-0.15f * base - killerLevel + victimLevel).toLong + } + } + math.max(1, base + battleRankDisparity) + } else { + base + } + } +} diff --git a/src/main/scala/net/psforever/objects/zones/exp/Stats.scala b/src/main/scala/net/psforever/objects/zones/exp/Stats.scala new file mode 100644 index 00000000..a27b6e4a --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/Stats.scala @@ -0,0 +1,26 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.zones.exp + +import net.psforever.objects.sourcing.PlayerSource + +private case class WeaponStats( + weapon_id: Int, + amount: Int, + shots: Int, + time: Long + ) + +private case class ContributionStats( + player: PlayerSource, + weapons: Seq[WeaponStats], + amount: Int, + totalDamage: Int, + shots: Int, + time: Long + ) + +sealed case class ContributionStatsOutput( + player: PlayerSource, + implements: Seq[Int], + percentage: Float + ) diff --git a/src/main/scala/net/psforever/objects/zones/exp/VitalsHistoryException.scala b/src/main/scala/net/psforever/objects/zones/exp/VitalsHistoryException.scala new file mode 100644 index 00000000..e5fd7e67 --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/VitalsHistoryException.scala @@ -0,0 +1,10 @@ +// 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) diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index 6fb9d8cb..c560723a 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -429,6 +429,15 @@ class AvatarService(zone: Zone) extends Actor { ) ) + case AvatarAction.UpdateKillsDeathsAssists(charId, stat) => + AvatarEvents.publish( + AvatarServiceResponse( + s"/$forChannel/Avatar", + Service.defaultPlayerGUID, + AvatarResponse.UpdateKillsDeathsAssists(charId, stat) + ) + ) + case _ => ; } diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index feaef692..70574ef7 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -2,6 +2,7 @@ package net.psforever.services.avatar import net.psforever.objects.Player +import net.psforever.objects.avatar.scoring.KDAStat import net.psforever.objects.ballistics.Projectile import net.psforever.objects.ce.Deployable import net.psforever.objects.equipment.Equipment @@ -154,6 +155,8 @@ object AvatarAction { final case class UseKit(kit_guid: PlanetSideGUID, kit_objid: Int) extends Action final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Action + final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action + final case class TeardownConnection() extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala index 4064cec4..e3a98ee9 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -2,6 +2,7 @@ package net.psforever.services.avatar import net.psforever.objects.Player +import net.psforever.objects.avatar.scoring.KDAStat import net.psforever.objects.ballistics.Projectile import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem @@ -124,4 +125,6 @@ object AvatarResponse { // final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response final case class UseKit(kit_guid: PlanetSideGUID, kit_objid: Int) extends Response final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Response + + final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response }