diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 3bd05f353..346dbc309 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -6,7 +6,7 @@ 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 net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill} import org.joda.time.{LocalDateTime, Seconds} import scala.collection.mutable 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, player.History, kill, exp) + case _: Assist => + () case _: Death => zone.AvatarEvents ! AvatarServiceMessage( player.Name, 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 dd08f21ed..55e5f1b79 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -398,6 +398,9 @@ class SessionAvatarHandlers( case AvatarResponse.UpdateKillsDeathsAssists(_, kda) => avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda) + case AvatarResponse.AwardSupportBep(_, bep) => + avatarActor ! AvatarActor.AwardBep(bep, ExperienceType.Support) + 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 e355b1b71..54be39a72 100644 --- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala +++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala @@ -154,7 +154,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone) experience ! ExperienceCalculator.RewardThisDeath(entity) case RewardOurSupporters(target, history, kill, bep) => - () + experience ! ExperienceCalculator.RewardOurSupporters(target, history, kill, bep) case ZoneMapUpdate() => zone.Buildings @@ -162,7 +162,6 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone) .values .foreach(_.Actor ! BuildingActor.MapUpdate()) } - this } } diff --git a/src/main/scala/net/psforever/objects/Players.scala b/src/main/scala/net/psforever/objects/Players.scala index 63f623af7..9a0993ef1 100644 --- a/src/main/scala/net/psforever/objects/Players.scala +++ b/src/main/scala/net/psforever/objects/Players.scala @@ -10,6 +10,8 @@ import net.psforever.objects.equipment.EquipmentSlot import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.inventory.InventoryItem 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.packet.game._ import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3} @@ -64,7 +66,7 @@ object Players { val name = target.Name val medicName = medic.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)) target.Zone.AvatarEvents ! AvatarServiceMessage( medicName, diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala index 2cc7f603d..b31f0716d 100644 --- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala +++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala @@ -28,13 +28,25 @@ trait 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 */ @@ -76,7 +88,7 @@ final case class HealFromKit(kit_def: KitDefinition, amount: Int) extends HealingActivity 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) extends HealingActivity @@ -91,7 +103,7 @@ final case class RepairFromKit(kit_def: KitDefinition, amount: Int) extends RepairingActivity() 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 diff --git a/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala b/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala index 15f236982..e43dd9136 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/ExperienceCalculator.scala @@ -7,7 +7,7 @@ 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.{DamagingActivity, HealingActivity, InGameActivity, InGameHistory, ReconstructionActivity, RepairFromExoSuitChange, RepairingActivity, RevivingActivity, SpawningActivity} import net.psforever.objects.vital.interaction.{Adversarial, DamageResult} import net.psforever.objects.zones.Zone 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( victim: PlayerSource, history: Iterable[InGameActivity] @@ -64,6 +94,38 @@ object ExperienceCalculator { 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) @@ -74,13 +136,10 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], def onMessage(msg: Command): Behavior[Command] = { msg match { case RewardThisDeath(victim: PlayerSource, lastDamage, history) => - rewardThisPlayerDeath( - victim, - lastDamage, - limitHistoryToThisLife(history.toList) - ) - case _ => - () + rewardThisPlayerDeath(victim, lastDamage, history) + case RewardOurSupporters(target, history, kill, bep) => + rewardTheseSupporters(target, history.toList, kill, bep) + case _ => () } Behaviors.same } @@ -88,24 +147,25 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], def rewardThisPlayerDeath( victim: PlayerSource, lastDamage: Option[DamageResult], - history: List[InGameActivity] + history: Iterable[InGameActivity] ): Unit = { - val everyone = determineKiller(lastDamage, history) match { + val shortHistory = ExperienceCalculator.limitHistoryToThisLife(history.toList) + val everyone = determineKiller(lastDamage, shortHistory) match { case Some((result, killer: PlayerSource)) => - val assists = collectAssistsForPlayer(victim, history, Some(killer)) - val fullBep = KillDeathAssists.calculateExperience(killer, victim, history) + val assists = collectAssistsForPlayer(victim, shortHistory, Some(killer)) + val fullBep = KillDeathAssists.calculateExperience(killer, victim, shortHistory) 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 + (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad case _ => - val assists = collectAssistsForPlayer(victim, history, None) - val fullBep = ExperienceCalculator.calculateExperience(victim, history) + val assists = collectAssistsForPlayer(victim, shortHistory, None) + val fullBep = ExperienceCalculator.calculateExperience(victim, shortHistory) 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 + (victim, Death(hitSquad.map { _._1 }, shortHistory.last.time - shortHistory.head.time, fullBep)) +: hitSquad } val events = zone.AvatarEvents 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)] = { val now = System.currentTimeMillis() val compareTimeMillis = 10.seconds.toMillis @@ -254,7 +293,7 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], mod.copy( amount = mod.amount + amount, weapons = weapons, - totalDamage = mod.totalDamage + amount, + total = mod.total + amount, shots = mod.shots + 1, time = activity.time ) @@ -333,4 +372,30 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], } 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)) + } + } } diff --git a/src/main/scala/net/psforever/objects/zones/exp/Stats.scala b/src/main/scala/net/psforever/objects/zones/exp/Stats.scala index a27b6e4a1..9af4ba3fd 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/Stats.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/Stats.scala @@ -14,7 +14,7 @@ private case class ContributionStats( player: PlayerSource, weapons: Seq[WeaponStats], amount: Int, - totalDamage: Int, + total: Int, shots: Int, time: Long ) diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala new file mode 100644 index 000000000..b648e3671 --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala @@ -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)) + } +} diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index c560723a7..8a495cb31 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -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 _ => ; } diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index 70574ef78..6ebf7fcc2 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -156,6 +156,7 @@ object AvatarAction { final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) 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 PlayerStateShift(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 e3a98ee92..7f16889c6 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -127,4 +127,5 @@ object AvatarResponse { final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Response final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response + final case class AwardSupportBep(charId: Long, bep: Long) extends Response }