importing controlled implementation changes from original exp-for-kda branch; primary kill experience rewarded

This commit is contained in:
Fate-JH 2023-03-20 23:49:46 -04:00
parent 0485c759e7
commit b866aa8a30
12 changed files with 469 additions and 18 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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