mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-02-23 00:23:36 +00:00
importing controlled implementation changes from original exp-for-kda branch; primary kill experience rewarded
This commit is contained in:
parent
0485c759e7
commit
b866aa8a30
12 changed files with 469 additions and 18 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<br>
|
||||
* key - character identifier,
|
||||
* value - (player, damage, total damage, number of shots) */
|
||||
val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
|
||||
tallyFunc(history, faction, participants)
|
||||
participants
|
||||
}
|
||||
|
||||
|
||||
|
||||
private def healthDamageContributors(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
participants: mutable.LongMap[ContributionStats]
|
||||
): Seq[(Long, Int)] = {
|
||||
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
|
||||
* key - character identifier,
|
||||
* value - current damage contribution */
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/main/scala/net/psforever/objects/zones/exp/Stats.scala
Normal file
26
src/main/scala/net/psforever/objects/zones/exp/Stats.scala
Normal file
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 _ => ;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue