mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-07-16 08:55:18 +00:00
importing controlled implementation changes from original exp-for-kda branch; assist kill experience rewarded
This commit is contained in:
parent
2f787a467f
commit
6774edf5b6
11 changed files with 403 additions and 49 deletions
|
|
@ -6,7 +6,7 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
||||||
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
|
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import net.psforever.actors.zone.ZoneActor
|
import net.psforever.actors.zone.ZoneActor
|
||||||
import net.psforever.objects.avatar.scoring.{Death, EquipmentStat, KDAStat, Kill}
|
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill}
|
||||||
import org.joda.time.{LocalDateTime, Seconds}
|
import org.joda.time.{LocalDateTime, Seconds}
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
|
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
|
||||||
|
|
@ -2953,6 +2953,8 @@ class AvatarActor(
|
||||||
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, obj.History, kill, exp)
|
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 _: Assist =>
|
||||||
|
()
|
||||||
case _: Death =>
|
case _: Death =>
|
||||||
zone.AvatarEvents ! AvatarServiceMessage(
|
zone.AvatarEvents ! AvatarServiceMessage(
|
||||||
player.Name,
|
player.Name,
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,9 @@ class SessionAvatarHandlers(
|
||||||
case AvatarResponse.UpdateKillsDeathsAssists(_, kda) =>
|
case AvatarResponse.UpdateKillsDeathsAssists(_, kda) =>
|
||||||
avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
|
avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
|
||||||
|
|
||||||
|
case AvatarResponse.AwardSupportBep(_, bep) =>
|
||||||
|
avatarActor ! AvatarActor.AwardBep(bep, ExperienceType.Support)
|
||||||
|
|
||||||
case AvatarResponse.SendResponse(msg) =>
|
case AvatarResponse.SendResponse(msg) =>
|
||||||
sendResponse(msg)
|
sendResponse(msg)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
||||||
experience ! ExperienceCalculator.RewardThisDeath(entity)
|
experience ! ExperienceCalculator.RewardThisDeath(entity)
|
||||||
|
|
||||||
case RewardOurSupporters(target, history, kill, bep) =>
|
case RewardOurSupporters(target, history, kill, bep) =>
|
||||||
()
|
experience ! ExperienceCalculator.RewardOurSupporters(target, history, kill, bep)
|
||||||
|
|
||||||
case ZoneMapUpdate() =>
|
case ZoneMapUpdate() =>
|
||||||
zone.Buildings
|
zone.Buildings
|
||||||
|
|
@ -162,7 +162,6 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
||||||
.values
|
.values
|
||||||
.foreach(_.Actor ! BuildingActor.MapUpdate())
|
.foreach(_.Actor ! BuildingActor.MapUpdate())
|
||||||
}
|
}
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import net.psforever.objects.equipment.EquipmentSlot
|
||||||
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
||||||
import net.psforever.objects.inventory.InventoryItem
|
import net.psforever.objects.inventory.InventoryItem
|
||||||
import net.psforever.objects.loadouts.InfantryLoadout
|
import net.psforever.objects.loadouts.InfantryLoadout
|
||||||
|
import net.psforever.objects.sourcing.PlayerSource
|
||||||
|
import net.psforever.objects.vital.RevivingActivity
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.packet.game._
|
import net.psforever.packet.game._
|
||||||
import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3}
|
import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3}
|
||||||
|
|
@ -64,7 +66,7 @@ object Players {
|
||||||
val name = target.Name
|
val name = target.Name
|
||||||
val medicName = medic.Name
|
val medicName = medic.Name
|
||||||
log.info(s"$medicName had revived $name")
|
log.info(s"$medicName had revived $name")
|
||||||
//target.History(PlayerRespawn(PlayerSource(target), target.Zone, target.Position, Some(PlayerSource(medic))))
|
target.LogActivity(RevivingActivity(PlayerSource(target), PlayerSource(medic), target.MaxHealth, item.Definition))
|
||||||
val magazine = item.Discharge(Some(25))
|
val magazine = item.Discharge(Some(25))
|
||||||
target.Zone.AvatarEvents ! AvatarServiceMessage(
|
target.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||||
medicName,
|
medicName,
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,25 @@ trait InGameActivity {
|
||||||
*/
|
*/
|
||||||
trait GeneralActivity extends InGameActivity
|
trait GeneralActivity extends InGameActivity
|
||||||
|
|
||||||
final case class SpawningActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry]) extends GeneralActivity
|
trait SupportActivityCausedByAnother {
|
||||||
|
def user: PlayerSource
|
||||||
|
def amount: Int
|
||||||
|
}
|
||||||
|
|
||||||
final case class ReconstructionActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry]) extends GeneralActivity
|
final case class SpawningActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
|
||||||
|
extends GeneralActivity
|
||||||
|
|
||||||
final case class ShieldCharge(amount: Int, cause: Option[SourceEntry]) extends GeneralActivity
|
final case class ReconstructionActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
|
||||||
|
extends GeneralActivity
|
||||||
|
|
||||||
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value) extends GeneralActivity
|
final case class RevivingActivity(target: SourceEntry, user: PlayerSource, amount: Int, equipment: EquipmentDefinition)
|
||||||
|
extends GeneralActivity with SupportActivityCausedByAnother
|
||||||
|
|
||||||
|
final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
|
||||||
|
extends GeneralActivity
|
||||||
|
|
||||||
|
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
|
||||||
|
extends GeneralActivity
|
||||||
|
|
||||||
/* vitals history */
|
/* vitals history */
|
||||||
|
|
||||||
|
|
@ -76,7 +88,7 @@ final case class HealFromKit(kit_def: KitDefinition, amount: Int)
|
||||||
extends HealingActivity
|
extends HealingActivity
|
||||||
|
|
||||||
final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
|
final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
|
||||||
extends HealingActivity
|
extends HealingActivity with SupportActivityCausedByAnother
|
||||||
|
|
||||||
final case class HealFromTerm(term: AmenitySource, amount: Int)
|
final case class HealFromTerm(term: AmenitySource, amount: Int)
|
||||||
extends HealingActivity
|
extends HealingActivity
|
||||||
|
|
@ -91,7 +103,7 @@ final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
|
||||||
extends RepairingActivity()
|
extends RepairingActivity()
|
||||||
|
|
||||||
final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
|
final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
|
||||||
extends RepairingActivity
|
extends RepairingActivity with SupportActivityCausedByAnother
|
||||||
|
|
||||||
final case class RepairFromTerm(term: AmenitySource, amount: Int) extends RepairingActivity
|
final case class RepairFromTerm(term: AmenitySource, amount: Int) extends RepairingActivity
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import net.psforever.objects.PlanetSideGameObject
|
||||||
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
|
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
|
||||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
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.{DamagingActivity, HealingActivity, InGameActivity, InGameHistory, ReconstructionActivity, RepairFromExoSuitChange, RepairingActivity, RevivingActivity, SpawningActivity}
|
||||||
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
|
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
|
|
@ -33,6 +33,36 @@ object ExperienceCalculator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
|
||||||
|
val spawnIndex = history.lastIndexWhere {
|
||||||
|
case _: SpawningActivity => true
|
||||||
|
case _: RevivingActivity => 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 calculateExperience(
|
def calculateExperience(
|
||||||
victim: PlayerSource,
|
victim: PlayerSource,
|
||||||
history: Iterable[InGameActivity]
|
history: Iterable[InGameActivity]
|
||||||
|
|
@ -64,6 +94,38 @@ object ExperienceCalculator {
|
||||||
base
|
base
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def onlyOriginalAssistEntries(
|
||||||
|
first: mutable.LongMap[ContributionStatsOutput],
|
||||||
|
second: mutable.LongMap[ContributionStatsOutput]
|
||||||
|
): Iterable[ContributionStatsOutput] = {
|
||||||
|
onlyOriginalAssistEntriesIterable(first.values, second.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
def onlyOriginalAssistEntriesIterable(
|
||||||
|
first: Iterable[ContributionStatsOutput],
|
||||||
|
second: Iterable[ContributionStatsOutput]
|
||||||
|
): Iterable[ContributionStatsOutput] = {
|
||||||
|
if (second.isEmpty) {
|
||||||
|
first
|
||||||
|
} else if (first.isEmpty) {
|
||||||
|
second
|
||||||
|
} else {
|
||||||
|
//overlap discriminated by percentage
|
||||||
|
val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
|
||||||
|
for {
|
||||||
|
h @ ContributionStatsOutput(hid, hwep, hkda) <- first
|
||||||
|
a @ ContributionStatsOutput(aid, awep, akda) <- second
|
||||||
|
(id, out) = if (hkda < akda)
|
||||||
|
(aid.CharId, a.copy(implements = (a.implements ++ hwep).distinct))
|
||||||
|
else
|
||||||
|
(hid.CharId, h.copy(implements = (h.implements ++ awep).distinct))
|
||||||
|
if hid == aid && shared.put(id, out).isEmpty
|
||||||
|
} yield ()
|
||||||
|
val sharedKeys = shared.keys
|
||||||
|
(first ++ second).filterNot { case ContributionStatsOutput(id, _, _) => sharedKeys.exists(_ == id.CharId) } ++ shared.values
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone)
|
class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone)
|
||||||
|
|
@ -74,13 +136,10 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
|
||||||
def onMessage(msg: Command): Behavior[Command] = {
|
def onMessage(msg: Command): Behavior[Command] = {
|
||||||
msg match {
|
msg match {
|
||||||
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
|
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
|
||||||
rewardThisPlayerDeath(
|
rewardThisPlayerDeath(victim, lastDamage, history)
|
||||||
victim,
|
case RewardOurSupporters(target, history, kill, bep) =>
|
||||||
lastDamage,
|
rewardTheseSupporters(target, history.toList, kill, bep)
|
||||||
limitHistoryToThisLife(history.toList)
|
case _ => ()
|
||||||
)
|
|
||||||
case _ =>
|
|
||||||
()
|
|
||||||
}
|
}
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
}
|
}
|
||||||
|
|
@ -88,24 +147,25 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
|
||||||
def rewardThisPlayerDeath(
|
def rewardThisPlayerDeath(
|
||||||
victim: PlayerSource,
|
victim: PlayerSource,
|
||||||
lastDamage: Option[DamageResult],
|
lastDamage: Option[DamageResult],
|
||||||
history: List[InGameActivity]
|
history: Iterable[InGameActivity]
|
||||||
): Unit = {
|
): Unit = {
|
||||||
val everyone = determineKiller(lastDamage, history) match {
|
val shortHistory = ExperienceCalculator.limitHistoryToThisLife(history.toList)
|
||||||
|
val everyone = determineKiller(lastDamage, shortHistory) match {
|
||||||
case Some((result, killer: PlayerSource)) =>
|
case Some((result, killer: PlayerSource)) =>
|
||||||
val assists = collectAssistsForPlayer(victim, history, Some(killer))
|
val assists = collectAssistsForPlayer(victim, shortHistory, Some(killer))
|
||||||
val fullBep = KillDeathAssists.calculateExperience(killer, victim, history)
|
val fullBep = KillDeathAssists.calculateExperience(killer, victim, shortHistory)
|
||||||
val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
|
val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
|
||||||
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
|
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
|
||||||
}.toSeq
|
}.toSeq
|
||||||
(victim, Death(hitSquad.map { _._1 }, history.last.time - history.head.time, fullBep)) +: hitSquad
|
(victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
val assists = collectAssistsForPlayer(victim, history, None)
|
val assists = collectAssistsForPlayer(victim, shortHistory, None)
|
||||||
val fullBep = ExperienceCalculator.calculateExperience(victim, history)
|
val fullBep = ExperienceCalculator.calculateExperience(victim, shortHistory)
|
||||||
val hitSquad = assists.map {
|
val hitSquad = assists.map {
|
||||||
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
|
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
|
||||||
}.toSeq
|
}.toSeq
|
||||||
(victim, Death(hitSquad.map { _._1 }, history.last.time - history.head.time, fullBep)) +: hitSquad
|
(victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad
|
||||||
}
|
}
|
||||||
val events = zone.AvatarEvents
|
val events = zone.AvatarEvents
|
||||||
everyone.foreach { case (p, kda) =>
|
everyone.foreach { case (p, kda) =>
|
||||||
|
|
@ -113,27 +173,6 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)] = {
|
def determineKiller(lastDamageActivity: Option[DamageResult], history: List[InGameActivity]): Option[(DamageResult, SourceEntry)] = {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val compareTimeMillis = 10.seconds.toMillis
|
val compareTimeMillis = 10.seconds.toMillis
|
||||||
|
|
@ -254,7 +293,7 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
|
||||||
mod.copy(
|
mod.copy(
|
||||||
amount = mod.amount + amount,
|
amount = mod.amount + amount,
|
||||||
weapons = weapons,
|
weapons = weapons,
|
||||||
totalDamage = mod.totalDamage + amount,
|
total = mod.total + amount,
|
||||||
shots = mod.shots + 1,
|
shots = mod.shots + 1,
|
||||||
time = activity.time
|
time = activity.time
|
||||||
)
|
)
|
||||||
|
|
@ -333,4 +372,30 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
|
||||||
}
|
}
|
||||||
newOrder ++ order.drop(count)
|
newOrder ++ order.drop(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def rewardTheseSupporters(
|
||||||
|
target: SourceEntry,
|
||||||
|
history: List[InGameActivity],
|
||||||
|
kill: Kill,
|
||||||
|
bep: Long
|
||||||
|
): Unit = {
|
||||||
|
val time = kill.time
|
||||||
|
val normalAssists = ExperienceCalculator.onlyOriginalAssistEntries(
|
||||||
|
Support.collectHealingSupportAssists(target, time, history),
|
||||||
|
Support.collectRepairingSupportAssists(target, time, history)
|
||||||
|
)
|
||||||
|
// retainedSupportAssists.get(target.unique) match {
|
||||||
|
// case Some(support) =>
|
||||||
|
// ExperienceCalculator.onlyOriginalAssistEntriesIterable(normalAssists, support.assists)
|
||||||
|
// case None =>
|
||||||
|
// normalAssists
|
||||||
|
// }
|
||||||
|
val events = zone.AvatarEvents
|
||||||
|
normalAssists.foreach { case ContributionStatsOutput(p, _, ratio) =>
|
||||||
|
events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, (ratio * bep).toLong))
|
||||||
|
}
|
||||||
|
Support.collectTerminalSupportAssists(target, history).foreach { case ContributionStatsOutput(p, _, reward) =>
|
||||||
|
events ! AvatarServiceMessage(p.Name, AvatarAction.AwardSupportBep(p.CharId, reward.toLong))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ private case class ContributionStats(
|
||||||
player: PlayerSource,
|
player: PlayerSource,
|
||||||
weapons: Seq[WeaponStats],
|
weapons: Seq[WeaponStats],
|
||||||
amount: Int,
|
amount: Int,
|
||||||
totalDamage: Int,
|
total: Int,
|
||||||
shots: Int,
|
shots: Int,
|
||||||
time: Long
|
time: Long
|
||||||
)
|
)
|
||||||
|
|
|
||||||
260
src/main/scala/net/psforever/objects/zones/exp/Support.scala
Normal file
260
src/main/scala/net/psforever/objects/zones/exp/Support.scala
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
// Copyright (c) 2023 PSForever
|
||||||
|
package net.psforever.objects.zones.exp
|
||||||
|
|
||||||
|
import net.psforever.objects.GlobalDefinitions
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
import org.joda.time.{Instant, LocalDateTime => JodaLocalDateTime}
|
||||||
|
import net.psforever.objects.serverobject.hackable.Hackable
|
||||||
|
import net.psforever.objects.serverobject.terminals.Terminal
|
||||||
|
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry}
|
||||||
|
import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFromEquipment, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity}
|
||||||
|
import net.psforever.objects.zones.Zone
|
||||||
|
import net.psforever.types.TransactionType
|
||||||
|
import net.psforever.zones.Zones
|
||||||
|
|
||||||
|
import scala.collection.mutable
|
||||||
|
|
||||||
|
object Support {
|
||||||
|
private type SupportActivity = InGameActivity with SupportActivityCausedByAnother
|
||||||
|
|
||||||
|
private[exp] def collectHealingSupportAssists(
|
||||||
|
target: SourceEntry,
|
||||||
|
time: JodaLocalDateTime,
|
||||||
|
history: Iterable[InGameActivity]
|
||||||
|
): mutable.LongMap[ContributionStatsOutput] = {
|
||||||
|
//normal heals
|
||||||
|
val healInfo = collectSupportContributions(
|
||||||
|
target, time, ExperienceCalculator.limitHistoryToThisLife(history.toList).collect { case heals: HealFromEquipment
|
||||||
|
if heals.amount > 0 && heals.user.unique != target.unique => (heals, heals.equipment_def.ObjectId)
|
||||||
|
}, mapContributionPointsByPercentage
|
||||||
|
)
|
||||||
|
//revivals (count all revivals performed by the player of the last one, if any)
|
||||||
|
val reviveInfo = collectSupportContributions(
|
||||||
|
target, time, history.collect { case revive: RevivingActivity
|
||||||
|
if revive.user.unique != target.unique => (revive, revive.equipment.ObjectId)
|
||||||
|
}.lastOption.toSeq, mapContributionPointsByCount
|
||||||
|
)
|
||||||
|
//combine and output
|
||||||
|
reviveInfo.foreach { case (reviverId, contribution) =>
|
||||||
|
val result = healInfo.get(reviverId)
|
||||||
|
val percentage = result.map { a => a.percentage }.getOrElse { contribution.percentage }
|
||||||
|
val implements = result.map { a => (a.implements ++ contribution.implements).distinct }.getOrElse { contribution.implements }
|
||||||
|
healInfo.put(reviverId, ContributionStatsOutput(contribution.player, implements, percentage))
|
||||||
|
}
|
||||||
|
healInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
private[exp] def collectRepairingSupportAssists(
|
||||||
|
target: SourceEntry,
|
||||||
|
time: JodaLocalDateTime,
|
||||||
|
history: Iterable[InGameActivity]
|
||||||
|
): mutable.LongMap[ContributionStatsOutput] = {
|
||||||
|
collectSupportContributions(
|
||||||
|
target, time, ExperienceCalculator.limitHistoryToThisLife(history.toList).collect { case repairs: RepairFromEquipment
|
||||||
|
if repairs.amount > 0 && repairs.user.unique != target.unique => (repairs, repairs.equipment_def.ObjectId)
|
||||||
|
}, mapContributionPointsByPercentage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private[exp] def collectTerminalSupportAssists(
|
||||||
|
target: SourceEntry,
|
||||||
|
history: List[InGameActivity]
|
||||||
|
): Iterable[ContributionStatsOutput] = {
|
||||||
|
val delay: Long = 300000L
|
||||||
|
val curr = System.currentTimeMillis()
|
||||||
|
var zone: Zone = Zone.Nowhere
|
||||||
|
val termsUsed: mutable.LongMap[Option[AmenitySource]] = mutable.LongMap[Option[AmenitySource]]()
|
||||||
|
val credit: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
|
||||||
|
val faction = target.Faction
|
||||||
|
val terminalUseHistory = history.filter {
|
||||||
|
case _: SpawningActivity => true
|
||||||
|
case entry: TerminalUsedActivity => curr - entry.time < delay
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
terminalUseHistory.collect {
|
||||||
|
case entry: SpawningActivity =>
|
||||||
|
zone = Zones.zones(entry.zoneNumber)
|
||||||
|
case entry: TerminalUsedActivity
|
||||||
|
if !termsUsed.contains(entry.terminal.unique.guid.guid.toLong) =>
|
||||||
|
if (
|
||||||
|
entry.transaction == TransactionType.Loadout ||
|
||||||
|
entry.transaction == TransactionType.Buy ||
|
||||||
|
entry.transaction == TransactionType.Learn
|
||||||
|
) {
|
||||||
|
val terminal = entry.terminal
|
||||||
|
val guid = terminal.unique.guid.guid
|
||||||
|
terminal.hacked.collect {
|
||||||
|
case Hackable.HackInfo(player, _, _, _)
|
||||||
|
if target.CharId != player.CharId && faction == player.Faction =>
|
||||||
|
//accessed a hacked terminal
|
||||||
|
termsUsed.put(guid.toLong, Some(terminal))
|
||||||
|
addTerminalContributionEntry(credit, player, Seq(GlobalDefinitions.remote_electronics_kit.ObjectId), percentage = 1f)
|
||||||
|
case _
|
||||||
|
if terminal.Faction == faction =>
|
||||||
|
//accessed a faction-friendly terminal; check log for repair history
|
||||||
|
termsUsed.put(guid.toLong, Some(terminal))
|
||||||
|
zone.GUID(guid)
|
||||||
|
.asInstanceOf[Terminal]
|
||||||
|
.History
|
||||||
|
.collect { case a: RepairFromEquipment =>
|
||||||
|
val user = a.user
|
||||||
|
addTerminalContributionEntry(credit, user, Seq(a.equipment_def.ObjectId), percentage = 0.5f)
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
//what is this?
|
||||||
|
termsUsed.put(guid.toLong, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
credit.remove(target.CharId)
|
||||||
|
credit.values
|
||||||
|
}
|
||||||
|
|
||||||
|
private def addTerminalContributionEntry(
|
||||||
|
contributions: mutable.LongMap[ContributionStatsOutput],
|
||||||
|
player: PlayerSource,
|
||||||
|
implements: Seq[Int],
|
||||||
|
percentage: Float
|
||||||
|
): Unit = {
|
||||||
|
val charId = player.CharId
|
||||||
|
contributions.get(charId) match {
|
||||||
|
case Some(entry) if percentage > entry.percentage =>
|
||||||
|
contributions.put(charId, entry.copy(percentage = percentage))
|
||||||
|
case Some(entry) =>
|
||||||
|
contributions.put(charId, entry.copy(
|
||||||
|
implements = (entry.implements ++ implements).distinct,
|
||||||
|
percentage = entry.percentage + 0.05f
|
||||||
|
))
|
||||||
|
case None =>
|
||||||
|
contributions.put(charId, ContributionStatsOutput(player, implements, percentage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def findTimeApplicableActivities(
|
||||||
|
activity: Seq[SupportActivity],
|
||||||
|
killLastTime: JodaLocalDateTime,
|
||||||
|
timeOffset: Int //s
|
||||||
|
): Seq[SupportActivity] = {
|
||||||
|
val dateTimeConverter: Date=>JodaLocalDateTime = JodaLocalDateTime.fromDateFields
|
||||||
|
val milliToInstant: Long=>org.joda.time.Instant = Instant.ofEpochMilli
|
||||||
|
val targetTime = killLastTime.minusSeconds(timeOffset)
|
||||||
|
//find all activities that occurred after the kill time after the offset is considered
|
||||||
|
activity
|
||||||
|
.filter { a =>
|
||||||
|
val activityTime = dateTimeConverter(milliToInstant(a.time).toDate)
|
||||||
|
targetTime.isBefore(activityTime) || targetTime.equals(activityTime)
|
||||||
|
}
|
||||||
|
.sortBy(_.time)(Ordering.Long.reverse)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def allocateSupportAssists(
|
||||||
|
longTerm: Seq[SupportActivity],
|
||||||
|
shortTerm: Seq[SupportActivity],
|
||||||
|
defaultTool: Int
|
||||||
|
): mutable.LongMap[ContributionStats] = {
|
||||||
|
val contributions: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
|
||||||
|
(longTerm ++ shortTerm).foreach { h =>
|
||||||
|
val amount = h.amount
|
||||||
|
val user = h.user
|
||||||
|
val charId = user.CharId
|
||||||
|
contributions.get(charId) match {
|
||||||
|
case Some(entry) =>
|
||||||
|
contributions.update(charId, entry.copy(
|
||||||
|
amount = entry.amount + amount,
|
||||||
|
total = entry.total + amount,
|
||||||
|
shots = entry.shots + 1,
|
||||||
|
time = math.max(entry.time, h.time)
|
||||||
|
))
|
||||||
|
case None =>
|
||||||
|
contributions.put(charId, ContributionStats(user, Seq(WeaponStats(defaultTool, amount, 1, h.time)), amount, amount, 1, h.time))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contributions
|
||||||
|
}
|
||||||
|
|
||||||
|
private def collectSupportContributions(
|
||||||
|
target: SourceEntry,
|
||||||
|
time: JodaLocalDateTime,
|
||||||
|
pairs: Seq[(InGameActivity, Int)],
|
||||||
|
contributionPointsMap: (Float,Iterable[SupportActivity])=>(Long,ContributionStats)=>(Long,ContributionStatsOutput)
|
||||||
|
): mutable.LongMap[ContributionStatsOutput] = {
|
||||||
|
val (activity, tools) = pairs.unzip
|
||||||
|
//TODO this is not correct, but it will do for now
|
||||||
|
if (tools.isEmpty) {
|
||||||
|
mutable.LongMap[ContributionStatsOutput]()
|
||||||
|
} else if (tools.distinct.size == 1) {
|
||||||
|
collectSupportAssists(target, time, activity, tools.head, contributionPointsMap)
|
||||||
|
} else {
|
||||||
|
var output = Iterable[ContributionStatsOutput]()
|
||||||
|
tools.groupBy(identity).keys.foreach { implement =>
|
||||||
|
output = ExperienceCalculator.onlyOriginalAssistEntriesIterable(
|
||||||
|
output,
|
||||||
|
collectSupportAssists(target, time, activity, implement, contributionPointsMap).values
|
||||||
|
)
|
||||||
|
}
|
||||||
|
mutable.LongMap[ContributionStatsOutput]().addAll(output.map { entry => (entry.player.CharId, entry) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def collectSupportAssists(
|
||||||
|
target: SourceEntry,
|
||||||
|
killLastTime: JodaLocalDateTime,
|
||||||
|
activity: Seq[InGameActivity],
|
||||||
|
defaultImplement: Int,
|
||||||
|
contributionPointsMap: (Float,Iterable[SupportActivity])=>(Long,ContributionStats)=>(Long,ContributionStatsOutput)
|
||||||
|
): mutable.LongMap[ContributionStatsOutput] = {
|
||||||
|
val activityByAnother = activity.collect {
|
||||||
|
case activity: SupportActivity => activity
|
||||||
|
}
|
||||||
|
val longTermActivity = findTimeApplicableActivities(activityByAnother, killLastTime, timeOffset = 600)
|
||||||
|
val shortTermActivity = findTimeApplicableActivities(activityByAnother, killLastTime, timeOffset = 300)
|
||||||
|
val contributions = allocateSupportAssists(longTermActivity, shortTermActivity, defaultImplement)
|
||||||
|
contributions.remove(target.CharId)
|
||||||
|
val mapFunc = contributionPointsMap(contributions.values.foldLeft(0)(_ + _.total).toFloat, shortTermActivity)
|
||||||
|
contributions.map { case (a, b) => mapFunc(a, b) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private def mapContributionPointsByPercentage(
|
||||||
|
total: Float,
|
||||||
|
compareList: Iterable[SupportActivity]
|
||||||
|
)
|
||||||
|
(
|
||||||
|
charId: Long,
|
||||||
|
contribution: ContributionStats,
|
||||||
|
): (Long, ContributionStatsOutput) = {
|
||||||
|
val user = contribution.player
|
||||||
|
val unique = user.unique
|
||||||
|
val points = contribution.amount
|
||||||
|
val value = if (points < 75) {
|
||||||
|
//a small contribution means the lower time limit
|
||||||
|
if (compareList.exists { a => a.user.unique == unique }) {
|
||||||
|
math.max(0.2f, points / total)
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//large contribution is always okay
|
||||||
|
if (points > 299) {
|
||||||
|
1.0f
|
||||||
|
} else if (points > 100) {
|
||||||
|
0.75f
|
||||||
|
} else {
|
||||||
|
0.5f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(charId, ContributionStatsOutput(user, contribution.weapons.map { _.weapon_id }, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
//noinspection ScalaUnusedSymbol
|
||||||
|
private def mapContributionPointsByCount(
|
||||||
|
total: Float,
|
||||||
|
compareList: Iterable[SupportActivity]
|
||||||
|
)
|
||||||
|
(
|
||||||
|
charId: Long,
|
||||||
|
contribution: ContributionStats,
|
||||||
|
): (Long, ContributionStatsOutput) = {
|
||||||
|
(charId, ContributionStatsOutput(contribution.player, contribution.weapons.map { _.weapon_id }, compareList.size.toFloat))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -438,6 +438,15 @@ class AvatarService(zone: Zone) extends Actor {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case AvatarAction.AwardSupportBep(charId, bep) =>
|
||||||
|
AvatarEvents.publish(
|
||||||
|
AvatarServiceResponse(
|
||||||
|
s"/$forChannel/Avatar",
|
||||||
|
Service.defaultPlayerGUID,
|
||||||
|
AvatarResponse.AwardSupportBep(charId, bep)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@ object AvatarAction {
|
||||||
final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) 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 UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action
|
||||||
|
final case class AwardSupportBep(charId: Long, bep: Long) extends Action
|
||||||
|
|
||||||
final case class TeardownConnection() extends Action
|
final case class TeardownConnection() extends Action
|
||||||
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||||
|
|
|
||||||
|
|
@ -127,4 +127,5 @@ object AvatarResponse {
|
||||||
final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Response
|
final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Response
|
||||||
|
|
||||||
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response
|
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response
|
||||||
|
final case class AwardSupportBep(charId: Long, bep: Long) extends Response
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue