From a23d8d4cb8829d70a672f798f7ef3fd9b66e655d Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 4 Jul 2023 04:24:52 -0400 Subject: [PATCH] rewarding facility capture through cep; not fully tested yet; math sucks --- src/main/resources/application.conf | 3 + .../actors/session/AvatarActor.scala | 6 +- .../support/SessionAvatarHandlers.scala | 30 +++ .../support/SessionMountHandlers.scala | 15 +- .../objects/avatar/scoring/Life.scala | 5 +- .../objects/avatar/scoring/ScoreCard.scala | 6 +- .../serverobject/structures/Building.scala | 54 ++-- .../FacilityHackParticipation.scala | 242 ++++++++++++++++++ .../MajorFacilityHackParticipation.scala | 200 +++++++++++++++ .../participation/NoParticipation.scala | 19 ++ .../participation/ParticipationLogic.scala | 33 +++ .../TowerHackParticipation.scala | 125 +++++++++ .../objects/vital/InGameHistory.scala | 43 +--- .../psforever/objects/zones/HotSpotInfo.scala | 10 +- .../objects/zones/ZoneHotSpotProjector.scala | 6 +- .../psforever/objects/zones/exp/Support.scala | 1 - .../services/avatar/AvatarService.scala | 11 +- .../avatar/AvatarServiceMessage.scala | 1 + .../avatar/AvatarServiceResponse.scala | 1 + .../local/support/HackCaptureActor.scala | 171 +------------ .../scala/net/psforever/util/Config.scala | 1 + 21 files changed, 723 insertions(+), 260 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/structures/participation/NoParticipation.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/structures/participation/ParticipationLogic.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 2396ea92c..4e89670d5 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -81,6 +81,9 @@ game { # Command experience rate cep-rate = 1.0 + # The maximum command experience that can be earned in a facility capture based on squad size + maximum-cep-per-squad-size = [990, 1980, 3466, 4950, 6436, 7920, 9406, 10890, 12376, 13860] + # Modify the amount of mending per autorepair tick for facility amenities amenity-autorepair-rate = 1.0 diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index ddedad4f4..91306e806 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -1671,8 +1671,8 @@ class AvatarActor( Behaviors.same case AwardBep(bep, ExperienceType.Support) => - //TODO cache experience for slow payouts supportExperiencePool = supportExperiencePool + bep + avatar.scorecard.rate(bep) if (supportExperienceTimer.isCancelled) { resetSupportExperienceTimer(previousBep = 0, previousDelay = 0) } @@ -2990,8 +2990,8 @@ class AvatarActor( case _ => None }) match { case Some(mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons) => - player.HistoryAndContributions() ++ InGameHistory.ContributionFrom(mount) - case None => + player.HistoryAndContributions() ++ InGameHistory.ContributionFrom(mount).toList + case _ => player.HistoryAndContributions() } zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, kill, exp) 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 55e5f1b79..951fbb467 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -401,6 +401,36 @@ class SessionAvatarHandlers( case AvatarResponse.AwardSupportBep(_, bep) => avatarActor ! AvatarActor.AwardBep(bep, ExperienceType.Support) + case AvatarResponse.AwardCep(0, cep) => + //must lead a squad to be awarded CEP + val squadUI = sessionData.squad.squadUI + squadUI + .find { _._1 == avatar.id } + .collect { + case (_, elem) if elem.index == 0 => + val thisZone = continent.Number + val squadSize = squadUI.count { case (_, e) => e.zone == thisZone } -1 + val maxCepList = Config.app.game.maximumCepPerSquadSize + val maxRate = maxCepList.lift(squadSize).getOrElse(squadSize * maxCepList.head).toLong + avatarActor ! AvatarActor.AwardCep(math.min(cep, maxRate)) + } + + case AvatarResponse.AwardCep(charId, cep) => + //if the target player, always award (some) CEP + val squadUI = sessionData.squad.squadUI + if (charId == player.CharId) { + val maxRate: Long = squadUI.find { _._1 == avatar.id } match { + case Some((_, elem)) if elem.index == 0 => + val thisZone = continent.Number + val squadSize = squadUI.count { case (_, e) => e.zone == thisZone } - 1 + val maxCepList = Config.app.game.maximumCepPerSquadSize + maxCepList.lift(squadSize).getOrElse(squadSize * maxCepList.head).toLong + case _ => + Config.app.game.maximumCepPerSquadSize.head.toLong + } + avatarActor ! AvatarActor.AwardCep(math.min(cep, maxRate)) + } + case AvatarResponse.SendResponse(msg) => sendResponse(msg) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala index 261b1fed9..d1584ff42 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala @@ -2,6 +2,9 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.vital.InGameHistory + import scala.concurrent.duration._ // import net.psforever.actors.session.AvatarActor @@ -160,7 +163,7 @@ class SessionMountHandlers( s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating" ) - case Mountable.CanMount(obj: PlanetSideGameObject with WeaponTurret, seatNumber, _) => + case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) => sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}") sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) @@ -246,7 +249,7 @@ class SessionMountHandlers( VehicleAction.KickPassenger(tplayer.GUID, seat_num, unk2=true, obj.GUID) ) - case Mountable.CanDismount(obj: PlanetSideGameObject with WeaponTurret, seatNum, _) => + case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) => log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}") DismountAction(tplayer, obj, seatNum) @@ -276,9 +279,10 @@ class SessionMountHandlers( * @param obj the mountable object * @param seatNum the mount into which the player is mounting */ - def MountingAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = { + def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { val playerGuid: PlanetSideGUID = tplayer.GUID val objGuid: PlanetSideGUID = obj.GUID + tplayer.ContributionFrom(obj) sessionData.playerActionsToCancel() avatarActor ! AvatarActor.DeactivateActiveImplants() avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds) @@ -295,7 +299,7 @@ class SessionMountHandlers( * @param obj the mountable object * @param seatNum the mount out of which which the player is disembarking */ - def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = { + def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { DismountAction(tplayer, obj, seatNum) //until vehicles maintain synchronized momentum without a driver obj match { @@ -332,8 +336,9 @@ class SessionMountHandlers( * @param obj the mountable object * @param seatNum the mount out of which which the player is disembarking */ - def DismountAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = { + def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { val playerGuid: PlanetSideGUID = tplayer.GUID + tplayer.ContributionFrom(obj) sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive val bailType = if (tplayer.BailProtection) { BailType.Bailed diff --git a/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala b/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala index 105f72a68..05ce6f846 100644 --- a/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala +++ b/src/main/scala/net/psforever/objects/avatar/scoring/Life.scala @@ -5,11 +5,12 @@ final case class Life( kills: Seq[Kill], assists: Seq[Assist], death: Option[Death], - equipmentStats: Seq[EquipmentStat] + equipmentStats: Seq[EquipmentStat], + supportExperience: Long ) object Life { - def apply(): Life = Life(Nil, Nil, None, Nil) + def apply(): Life = Life(Nil, Nil, None, Nil, 0) def bep(life: Life): Long = { life.kills.foldLeft(0L)(_ + _.experienceEarned) + life.assists.foldLeft(0L)(_ + _.experienceEarned) diff --git a/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala b/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala index 43db48efe..23b8f7720 100644 --- a/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala +++ b/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala @@ -18,6 +18,8 @@ class ScoreCard() { def Lives: Seq[Life] = lives + def Kills: Seq[Kill] = lives.flatMap { _.kills } ++ curr.kills + def KillStatistics: Map[Int, Statistic] = killStatistics.toMap def AssistStatistics: Map[Int, Statistic] = assistStatistics.toMap @@ -40,7 +42,9 @@ class ScoreCard() { val expired = curr curr = Life() lives = expired.copy(death = Some(d)) +: lives - case _ => ; + case value: Long => + curr = curr.copy(supportExperience = curr.supportExperience + value) + case _ => () } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index ab1bf430b..dbf6336de 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -16,11 +16,10 @@ import net.psforever.types._ import scalax.collection.{Graph, GraphEdge} import akka.actor.typed.scaladsl.adapter._ import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket} +import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation} import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal -import scala.collection.mutable import java.util.concurrent.TimeUnit -import scala.concurrent.duration._ class Building( private val name: String, @@ -34,9 +33,8 @@ class Building( private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var playersInSOI: List[Player] = List.empty - private val capitols = List("Thoth", "Voltan", "Neit", "Anguta", "Eisa", "Verica") private var forceDomeActive: Boolean = false - private var participationFunc: Building.ParticipationLogic = Building.NoParticipation + private var participationFunc: ParticipationLogic = NoParticipation super.Zone_=(zone) super.GUID_=(PlanetSideGUID(building_guid)) //set Invalidate() //unset; guid can be used during setup, but does not stop being registered properly later @@ -51,10 +49,11 @@ class Building( */ def MapId: Int = map_id - def IsCapitol: Boolean = capitols.contains(name) + def IsCapitol: Boolean = Building.Capitols.contains(name) + def IsSubCapitol: Boolean = { Neighbours match { - case Some(buildings: Set[Building]) => buildings.exists(x => capitols.contains(x.name)) + case Some(buildings: Set[Building]) => buildings.exists(x => Building.Capitols.contains(x.name)) case None => false } } @@ -85,13 +84,11 @@ class Building( box.Actor ! Painbox.Stop() } } - participationFunc.Players(building = this, list) playersInSOI = list + participationFunc.TryUpdate() playersInSOI } - def PlayerContribution: Map[Player, Long] = participationFunc.Contribution() - // Get all lattice neighbours def AllNeighbours: Option[Set[Building]] = { zone.Lattice find this match { @@ -360,43 +357,24 @@ class Building( override def Amenities_=(obj: Amenity): List[Amenity] = { obj match { - case _: CaptureTerminal => participationFunc = Building.FacilityHackParticipation - case _ => ; + case _: CaptureTerminal => + if (buildingType == StructureType.Facility) { + participationFunc = MajorFacilityHackParticipation(this) + } else if (buildingType == StructureType.Tower) { + participationFunc = TowerHackParticipation(this) + } + case _ => () } super.Amenities_=(obj) } + def Participation: ParticipationLogic = participationFunc + def Definition: BuildingDefinition = buildingDefinition } object Building { - trait ParticipationLogic { - def Players(building: Building, list: List[Player]): Unit = { } - def Contribution(): Map[Player, Long] - } - - final case object NoParticipation extends ParticipationLogic { - def Contribution(): Map[Player, Long] = Map.empty[Player, Long] - } - - final case object FacilityHackParticipation extends ParticipationLogic { - private var playerContribution: mutable.HashMap[Player, Long] = mutable.HashMap[Player, Long]() - - override def Players(building: Building, list: List[Player]): Unit = { - if (list.isEmpty) { - playerContribution.clear() - } else { - val hackTime = (building.CaptureTerminal.get.Definition.FacilityHackTime + 10.minutes).toMillis - val curr = System.currentTimeMillis() - val list2 = list.map { p => (p, curr) } - playerContribution = playerContribution.filterNot { case (p, t) => - list2.contains(p) || curr - t > hackTime - } ++ list2 - } - } - - def Contribution(): Map[Player, Long] = playerContribution.toMap - } + final val Capitols = List("Thoth", "Voltan", "Neit", "Anguta", "Eisa", "Verica") final val NoBuilding: Building = new Building(name = "", 0, map_id = 0, Zone.Nowhere, StructureType.Platform, GlobalDefinitions.building) { diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala new file mode 100644 index 000000000..ea03fb043 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala @@ -0,0 +1,242 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.serverobject.structures.participation + +import net.psforever.objects.Player +import net.psforever.objects.avatar.scoring.Kill +import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer} +import net.psforever.types.{PlanetSideEmpire, Vector3} + +import scala.collection.mutable + +trait FacilityHackParticipation extends ParticipationLogic { + protected var lastInfoRequest: Long = 0L + protected var infoRequestOverTime: Seq[Long] = Seq[Long]() + /* + key: unique player identifier + */ + protected var playerContribution: mutable.LongMap[(Player, Int, Long)] = mutable.LongMap[(Player, Int, Long)]() + protected var playerPopulationOverTime: Seq[Map[PlanetSideEmpire.Value, Int]] = Seq[ Map[PlanetSideEmpire.Value, Int]]() + + protected def updatePlayers(list: List[Player]): Unit = { + val hackTime = building.CaptureTerminal.get.Definition.FacilityHackTime.toMillis + val curr = System.currentTimeMillis() + if (list.isEmpty) { + playerContribution = playerContribution.filterNot { case (_, (_, _, t)) => curr - t > hackTime } + } else { + val (vanguardParticipants, missingParticipants) = { + val uniqueList2 = list.map { _.CharId } + playerContribution + .filterNot { case (_, (_, _, t)) => curr - t > hackTime } + .partition { case (p, _) => uniqueList2.contains(p) } + } + val newParticipaants = list + .filterNot { p => + playerContribution.exists { case (u, _) => p.CharId == u } + } + playerContribution = + vanguardParticipants.map { case (u, (p, d, _)) => (u, (p, d + 1, curr)) } ++ + newParticipaants.map { p => (p.CharId, (p, 1, curr)) } ++ + missingParticipants + } + } + + protected def updatePopulationOverTime(list: List[Player], now: Long, before: Long): Unit = { + var populationList = list + val layer = PlanetSideEmpire.values.map { faction => + val (isFaction, everyoneElse) = populationList.partition(_.Faction == faction) + populationList = everyoneElse + (faction, isFaction.size) + }.toMap[PlanetSideEmpire.Value, Int] + playerPopulationOverTime = timeSensitiveFilterAndAppend(playerPopulationOverTime, layer, now, before) + } + + protected def timeSensitiveFilterAndAppend[T]( + list: Seq[T], + newEntry: T, + now: Long = System.currentTimeMillis(), + before: Long + ): Seq[T] = { + infoRequestOverTime match { + case Nil => + Seq(newEntry) + case _ => + val beforeTime = now - before + (infoRequestOverTime.indexWhere { _ >= beforeTime } match { + case -1 => + list + case cutOffIndex => + list.drop(cutOffIndex) + }) :+ newEntry + } + } +} + +object FacilityHackParticipation { + private[participation] def calculateExperienceFromKills( + center: Vector3, + radius: Float, + hackStart: Long, + completionTime: Long, + opposingFaction: PlanetSideEmpire.Value, + contributionVictor: Iterable[(Player, Int, Long)], + contributionOpposingSize: Int + ): Long = { + val killMapFunc: Iterable[(Player, Int, Long)] => Iterable[(UniquePlayer, Float, Seq[Kill])] = { + killsEarnedPerPlayerDuringHack(center.xy, radius * radius, hackStart, hackStart + completionTime, opposingFaction) + } + val killMapValues = killMapFunc(contributionVictor) + val totalExperienceFromKills = killMapValues.flatMap { _._3.map { _.experienceEarned } }.sum + val experienceModifier = { + if (contributionOpposingSize > 0 && contributionOpposingSize < 10) { + contributionOpposingSize * 0.1f + math.random() + } else { + contributionOpposingSize * 0.1f + } + } + (totalExperienceFromKills * experienceModifier).toLong + } + + private def killsEarnedPerPlayerDuringHack( + centerXY: Vector3, + distanceSq: Float, + start: Long, + end: Long, + faction: PlanetSideEmpire.Value + ) + ( + list: Iterable[(Player, Int, Long)] + ): Iterable[(UniquePlayer, Float, Seq[Kill])] = { + val duration = end - start + list.map { case (p, d, _) => + val killList = p.avatar.scorecard.Kills.filter { k => + val killTime = k.info.interaction.hitTime + k.victim.Faction == faction && start < killTime && killTime < end && Vector3.DistanceSquared(centerXY, k.info.interaction.hitPos.xy) < distanceSq + } + (PlayerSource(p).unique, math.min(d, duration).toFloat / duration.toFloat, killList) + } + } + + private[participation] def diffHeatForFactionMap( + data: mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]], + faction: PlanetSideEmpire.Value + ): Map[Vector3, Seq[Long]] = { + var lastHeatAmount: Long = 0 + var outList: Seq[Long] = Seq[Long]() + data.map { case (key, map) => + map(faction) match { + case Nil => () + case value :: Nil => + outList = outList :+ value + case value :: list => + lastHeatAmount = value + list.foreach { heat => + if (heat < lastHeatAmount) { + lastHeatAmount = heat + outList = outList :+ heat + } else { + outList = outList :+ (heat - lastHeatAmount) + lastHeatAmount = heat + } + } + } + (key, outList) + }.toMap[Vector3, Seq[Long]] + } + + private[participation] def heatMapComparison( + victorData: Iterable[Seq[Long]], + opposedData: Iterable[Seq[Long]] + ): Float = { + var dataCount: Int = 0 + var dataSum: Float = 0 + if (victorData.size == opposedData.size) { + val seq1 = victorData.toSeq + val seq2 = opposedData.toSeq + seq1.indices.foreach { outerIndex => + val list1 = seq1(outerIndex) + val list2 = seq2(outerIndex) + if (list1.size == list2.size) { + val indices1 = list1.indices + dataCount = dataCount + indices1.size + indices1.foreach { innerIndex => + val value1 = list1(innerIndex) + val value2 = list2(innerIndex) + if (value1 * value2 == 0) { + dataCount -= 1 + } else if (value1 > value2) { + dataSum = dataSum - value2.toFloat / value1.toFloat + } else { + dataSum = dataSum + value2.toFloat / value1.toFloat + } + } + } + } + } + if (dataSum != 0) { + math.max(0.15f, math.min(2f, dataSum / dataCount.toFloat)) + } else { + 1f //can't do anything; multiplier should not affect values + } + } + + /** + * na + * @param populationNumbers list of the population updates + * @param gradingRule the rule whereby population numbers are transformed into percentage bonus + * @param layers from largest groupings of percentages from applying the above rule, average the values from this many groups + * @return the modifier value + */ + private[participation] def populationProgressModifier( + populationNumbers: Seq[Int], + gradingRule: Int=>Float, + layers: Int + ): Float = { + val gradedPopulation = populationNumbers + .map { gradingRule } + .groupBy(x => x) + .values + .toSeq + .sortBy(_.size) + .take(layers) + .flatten + gradedPopulation.sum / gradedPopulation.size.toFloat + } + + private[participation] def populationBalanceModifier( + victorPopulationNumbers: Seq[Int], + opposingPopulationNumbers: Seq[Int], + healthyPercentage: Float + ): Float = { + val rate = for { + victorPop <- victorPopulationNumbers + opposePop <- opposingPopulationNumbers + out = if ( + (opposePop < victorPop && opposePop * healthyPercentage > victorPop) || + (opposePop > victorPop && victorPop * healthyPercentage > opposePop) + ) { + 1f //balanced enough population + } else { + opposePop / victorPop.toFloat + } + if true + } yield out + rate.sum / rate.size.toFloat + } + + private[participation] def competitionBonus( + victorSize: Long, + opposingSize: Long, + steamrollPercentage: Float, + steamrollBonus: Long, + overwhelmingOddsPercentage: Float, + overwhelmingOddsBonus: Long + ): Long = { + if (opposingSize * steamrollPercentage < victorSize.toFloat) { + -steamrollBonus * (victorSize - opposingSize) //steamroll by the victor + } else if (victorSize * overwhelmingOddsPercentage <= opposingSize.toFloat) { + overwhelmingOddsBonus + opposingSize + victorSize //victory against overwhelming odds + } else { + steamrollBonus * opposingSize //still a battle + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala new file mode 100644 index 000000000..6800ebfbb --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala @@ -0,0 +1,200 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.serverobject.structures.participation + +import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.sourcing.PlayerSource +import net.psforever.objects.zones.{HotSpotInfo, ZoneHotSpotProjector} +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.types.{PlanetSideEmpire, Vector3} +import net.psforever.util.Config + +import akka.pattern.ask +import akka.util.Timeout +import scala.collection.mutable +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +final case class MajorFacilityHackParticipation(building: Building) extends FacilityHackParticipation { + private implicit val timeout: Timeout = 10.seconds + + private var hotSpotLayersOverTime: Seq[List[HotSpotInfo]] = Seq[List[HotSpotInfo]]() + + def TryUpdate(): Unit = { + val list = building.PlayersInSOI + updatePlayers(list) + val now = System.currentTimeMillis() + if (now - lastInfoRequest > 60000L) { + updatePopulationOverTime(list, now, before = 900000L) + updateHotSpotInfoOverTime() + updateTime(now) + } + lastInfoRequest = now + } + + private def updateHotSpotInfoOnly(): Future[ZoneHotSpotProjector.ExposedHeat] = { + ask( + building.Zone.Activity, + ZoneHotSpotProjector.ExposeHeatForRegion(building.Position, building.Definition.SOIRadius.toFloat) + ).mapTo[ZoneHotSpotProjector.ExposedHeat] + } + + private def updateHotSpotInfoOverTime(): Future[ZoneHotSpotProjector.ExposedHeat] = { + import net.psforever.objects.zones.ZoneHotSpotProjector + + import scala.concurrent.Promise + import scala.util.Success + val requestLayers: Promise[ZoneHotSpotProjector.ExposedHeat] = Promise[ZoneHotSpotProjector.ExposedHeat]() + val request = updateHotSpotInfoOnly() + requestLayers.completeWith(request) + request.onComplete { + case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) => + hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis(), before = 900000L) + case _ => + requestLayers.completeWith(Future(ZoneHotSpotProjector.ExposedHeat(Vector3.Zero, 0, Nil))) + } + requestLayers.future + } + + private def updateTime(now: Long): Unit = { + infoRequestOverTime = timeSensitiveFilterAndAppend(infoRequestOverTime, now, now, before = 900000L) + } + + def RewardFacilityCapture( + defenderFaction: PlanetSideEmpire.Value, + attackingFaction: PlanetSideEmpire.Value, + hacker: PlayerSource, + hackTime: Long, + completionTime: Long, + isResecured: Boolean + ): Unit = { + val curr = System.currentTimeMillis() + val hackStart = curr - completionTime + val (victorFaction, opposingFaction, flagCarrier) = if (!isResecured) { + val carrier = building.GetFlagSocket.flatMap(_.previousFlag).flatMap(_.Carrier) + (attackingFaction, defenderFaction, carrier) + } else { + (defenderFaction, attackingFaction, None) + } + val (contributionVictor, contributionOpposing, _) = { + val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction } + val (b, c) = b1.partition { case (_, (p, _, _)) => p.Faction == opposingFaction } + (a.values, b.values, c.values) + } + val contributionVictorSize = contributionVictor.size + if (contributionVictorSize > 0) { + val contributionOpposingSize = contributionOpposing.size + //1) experience from killing opposingFaction across duration of hack + val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills( + building.Position, + building.Definition.SOIRadius - 50f, + hackStart, + completionTime, + opposingFaction, + contributionVictor, + contributionOpposingSize + ) + //setup for ... + val populationIndices = playerPopulationOverTime.indices + val allFactions = PlanetSideEmpire.values.filterNot { _ == PlanetSideEmpire.NEUTRAL }.toSeq + val (victorPopulationByLayer, opposingPopulationByLayer) = { + val individualPopulationByLayer = allFactions.map { f => + (f, populationIndices.indices.map { i => playerPopulationOverTime(i)(f) }) + }.toMap[PlanetSideEmpire.Value, Seq[Int]] + (individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction)) + } + //2) peak population modifier + val populationModifier = FacilityHackParticipation.populationProgressModifier( + opposingPopulationByLayer, + { pop => + if (pop > 59) 0.5f + else if (pop > 29) 0.4f + else if (pop > 25) 0.3f + else 0.25f + }, + 3 + ) + //3) competition bonus + val competitionBonus: Long = FacilityHackParticipation.competitionBonus( + contributionVictorSize, + contributionOpposingSize, + steamrollPercentage = 1.25f, + steamrollBonus = 5L, + overwhelmingOddsPercentage = 0.5f, + overwhelmingOddsBonus = 100L + ) + //4) competition multiplier + val competitionMultiplier: Float = { + val populationBalanceModifier: Float = FacilityHackParticipation.populationBalanceModifier( + victorPopulationByLayer, + opposingPopulationByLayer, + healthyPercentage = 1.25f + ) + //compensate for heat + val regionHeatMapProgression = { + /* + transform the different layers of the facility heat map timeline into a progressing timeline of regional hotspot information; + where the grouping are of simultaneous hotspots, + the letter indicates a unique hotspot, + and the number an identifier between related hotspots: + ((A-1, B-2, C-3), (D-1, E-2, F-3), (G-1, H-2, I-3)) ... (1->(A, D, G), 2->(B, E, H), 3->(C, F, I)) + */ + val finalMap = mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]]() + .addAll(hotSpotLayersOverTime.head.map { entry => (entry.DisplayLocation, Map.empty) }) + //note: this pre-seeding of keys allows us to skip a getOrElse call in the foldLeft + hotSpotLayersOverTime.foldLeft(finalMap) { (map, list) => + list.foreach { entry => + val key = entry.DisplayLocation + val newValues = entry.Activity.map { case (f, e) => (f, e.Heat.toLong) } + val combinedValues = map(key).map { case (f, e) => (f, e :+ newValues(f)) } + map.put(key, combinedValues) + } + map + }.toMap + finalMap //explicit for no good reason + } + val heatVictorMap = FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values + val heatAgainstMap = FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values + val heatMapModifier = FacilityHackParticipation.heatMapComparison(heatVictorMap, heatAgainstMap) + heatMapModifier * populationBalanceModifier + } + //5) hack time modifier + val overallTimeMultiplier: Float = if (isResecured) { + math.max(0.5f + (hackTime - completionTime) / hackTime, 1f) + } else { + 1f + } + //calculate overall command experience points and the individual player multiplier + val finalCep: Long = math.ceil( + math.max(1L, baseExperienceFromFacilityCapture + competitionBonus) * + populationModifier * + competitionMultiplier * + overallTimeMultiplier * + Config.app.game.cepRate + ).toLong + val contributionPerPlayerByTime = playerContribution.collect { + case (a, (_, d, t)) if d >= 600000 && math.abs(completionTime - t) < 5000 => + (a, 1f) + case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 => + (a, d.toFloat / 6000000f) + case (a, (_, d, t)) => + (a, math.max(0, d - completionTime + t).toFloat / 6000000f) + } + //reward participant(s) + // classically, only players in the SOI are rewarded + val events = building.Zone.AvatarEvents + building.PlayersInSOI + .filter { player => + player.Faction == victorFaction && player.CharId != hacker.CharId && !flagCarrier.contains(player) + } + .foreach { player => + val contributionMultiplier = contributionPerPlayerByTime.getOrElse(player.CharId, 1f) + events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, (finalCep * contributionMultiplier).toLong)) + } + events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep)) + flagCarrier.collect { + player => events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(player.CharId, (finalCep * 0.5f).toLong)) + } + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/NoParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/NoParticipation.scala new file mode 100644 index 000000000..e1a565e95 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/NoParticipation.scala @@ -0,0 +1,19 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.serverobject.structures.participation + +import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.sourcing.PlayerSource +import net.psforever.types.PlanetSideEmpire + +case object NoParticipation extends ParticipationLogic { + def building: Building = Building.NoBuilding + def TryUpdate(): Unit = { /* nothing here */ } + def RewardFacilityCapture( + defenderFaction: PlanetSideEmpire.Value, + attackingFaction: PlanetSideEmpire.Value, + hacker: PlayerSource, + hackTime: Long, + completionTime: Long, + isResecured: Boolean + ): Unit = { /* nothing here */ } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/ParticipationLogic.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/ParticipationLogic.scala new file mode 100644 index 000000000..0c9b57d32 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/ParticipationLogic.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.serverobject.structures.participation + +import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.sourcing.PlayerSource +import net.psforever.types.PlanetSideEmpire + +//noinspection ScalaUnusedSymbol +trait ParticipationLogic { + def building: Building + def TryUpdate(): Unit + /** + * na + * @param defenderFaction those attempting to stop the hack + * the `terminal` (above) and facility originally belonged to this empire + * @param attackingFaction those attempting to progress the hack; + * the `hacker` (below) belongs to this empire + * @param hacker the player who hacked the capture terminal (above) + * @param hackTime how long the over-all facility hack allows or requires + * @param completionTime how long the facility hacking process lasted + * @param isResecured whether `defendingFaction` or the `attackingFaction` succeeded; + * the latter is called a "capture", + * while the former is a "resecure" + */ + def RewardFacilityCapture( + defenderFaction: PlanetSideEmpire.Value, + attackingFaction: PlanetSideEmpire.Value, + hacker: PlayerSource, + hackTime: Long, + completionTime: Long, + isResecured: Boolean + ): Unit +} diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala new file mode 100644 index 000000000..410ddf4d4 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala @@ -0,0 +1,125 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.serverobject.structures.participation + +import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.sourcing.PlayerSource +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.types.PlanetSideEmpire +import net.psforever.util.Config + +final case class TowerHackParticipation(building: Building) extends FacilityHackParticipation { + def TryUpdate(): Unit = { + val list = building.PlayersInSOI + updatePlayers(building.PlayersInSOI) + val now = System.currentTimeMillis() + if (now - lastInfoRequest > 60000L) { + updatePopulationOverTime(list, now, before = 300000L) + } + } + + def RewardFacilityCapture( + defenderFaction: PlanetSideEmpire.Value, + attackingFaction: PlanetSideEmpire.Value, + hacker: PlayerSource, + hackTime: Long, + completionTime: Long, + isResecured: Boolean + ): Unit = { + val curr = System.currentTimeMillis() + val hackStart = curr - completionTime + val (victorFaction, opposingFaction) = if (!isResecured) { + (attackingFaction, defenderFaction) + } else { + (defenderFaction, attackingFaction) + } + val (contributionVictor, contributionOpposing, _) = { + //TODO this is only to preserve a semblance of the original return type; fix this output + val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction } + val (b, c) = b1.partition { case (_, (p, _, _)) => p.Faction == opposingFaction } + (a.values, b.values, c.values) + } + val contributionVictorSize = contributionVictor.size + if (contributionVictorSize > 0) { + val contributionOpposingSize = contributionOpposing.size + //1) experience from killing opposingFaction across duration of hack + val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills( + building.Position, + building.Definition.SOIRadius.toFloat, + hackStart, + completionTime, + opposingFaction, + contributionVictor, + contributionOpposingSize + ) + //setup for ... + val populationIndices = playerPopulationOverTime.indices + val allFactions = PlanetSideEmpire.values.filterNot { + _ == PlanetSideEmpire.NEUTRAL + }.toSeq + val (victorPopulationByLayer, opposingPopulationByLayer) = { + val individualPopulationByLayer = allFactions.map { f => + (f, populationIndices.indices.map { i => playerPopulationOverTime(i)(f) }) + }.toMap[PlanetSideEmpire.Value, Seq[Int]] + (individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction)) + } + //2) peak population modifier + val populationModifier = FacilityHackParticipation.populationProgressModifier( + opposingPopulationByLayer, + { pop => + if (pop > 59) 0.75f + else if (pop > 29) 0.675f + else if (pop > 25) 0.55f + else 0.5f + }, + 2 + ) + //3) competition bonus + val competitionBonus: Long = FacilityHackParticipation.competitionBonus( + contributionVictorSize, + contributionOpposingSize, + steamrollPercentage = 1.25f, + steamrollBonus = 2L, + overwhelmingOddsPercentage = 0.5f, + overwhelmingOddsBonus = 30L + ) + //4) competition multiplier + val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier( + victorPopulationByLayer, + opposingPopulationByLayer, + healthyPercentage = 1.25f + ) + //calculate overall command experience points and the individual player multiplier + val finalCep: Long = math.ceil( + math.max(1L, baseExperienceFromFacilityCapture + competitionBonus) * + populationModifier * + competitionMultiplier * + Config.app.game.cepRate + ).toLong + val contributionPerPlayerByTime = playerContribution.collect { + case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 => + (a, 0.5f) + case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 => + (a, 0.25f * (1f + (d.toFloat / 3000000f))) + case (a, (_, d, t)) => + (a, 0.25f * (1f + (math.max(0, d - completionTime + t).toFloat / 3000000f))) + } + //reward participant(s) + // classically, only players in the SOI are rewarded + val events = building.Zone.AvatarEvents + building.PlayersInSOI + .filter { player => + player.Faction == victorFaction && player.CharId != hacker.CharId + } + .foreach { player => + val contributionMultiplier = contributionPerPlayerByTime.getOrElse(player.CharId, 1f) + events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, (finalCep * contributionMultiplier).toLong)) + } + events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep)) + } + + playerContribution.clear() + playerPopulationOverTime.reverse match { + case entry :: _ => playerPopulationOverTime = Seq(entry) + } + } +} diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala index 5f713a784..2ba1f801d 100644 --- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala +++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala @@ -209,9 +209,6 @@ trait InGameHistory { action match { case Some(act) => history = history :+ act - if (IsContributionEvent(act)) { - previousContributionInheritance = None - } case None => () } history @@ -256,8 +253,10 @@ trait InGameHistory { } } - private var previousContributionInheritance: Option[List[InGameActivity]] = None - + /** + * activity that comes from another entity used for scoring;
+ * key - unique reference to that entity; value - history from that entity + */ private val contributionInheritance: mutable.HashMap[SourceUniqueness, Contribution] = mutable.HashMap[SourceUniqueness, Contribution]() @@ -277,30 +276,7 @@ trait InGameHistory { } def GetContribution(): Option[List[InGameActivity]] = { - previousContributionInheritance - .collect { - case out @ events if events.head.time > System.currentTimeMillis() - 600000L => - Some(out) - case events => - val newEvents = GetContributionDuringPeriod(events, duration = 600000L) - if (newEvents.isEmpty) { - previousContributionInheritance = None - None - } else { - previousContributionInheritance = Some(newEvents) - Some(newEvents) - } - } - .flatten - .orElse { - val events = GetContributionDuringPeriod(History, duration = 600000L) - if (events.nonEmpty) { - previousContributionInheritance = Some(events) - Some(events) - } else { - None - } - } + Option(GetContributionDuringPeriod(History, duration = 600000L)) } def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = { @@ -311,14 +287,6 @@ trait InGameHistory { } } - def IsContributionEvent(event: InGameActivity): Boolean = { - event match { - case _: RepairingActivity => true - case _: DamagingActivity => true - case _ => false - } - } - def HistoryAndContributions(): List[InGameActivity] = { History ++ contributionInheritance.values.toList } @@ -327,7 +295,6 @@ trait InGameHistory { lastDamage = None val out = history history = List.empty - previousContributionInheritance = None contributionInheritance.clear() out } diff --git a/src/main/scala/net/psforever/objects/zones/HotSpotInfo.scala b/src/main/scala/net/psforever/objects/zones/HotSpotInfo.scala index 23ac2b116..20b0eacbd 100644 --- a/src/main/scala/net/psforever/objects/zones/HotSpotInfo.scala +++ b/src/main/scala/net/psforever/objects/zones/HotSpotInfo.scala @@ -102,7 +102,7 @@ class ActivityReport { * @return the time */ def Duration_=(time: FiniteDuration): FiniteDuration = { - Duration_=(time.toNanos) + Duration_=(time.toMillis) } /** @@ -112,8 +112,8 @@ class ActivityReport { * @return the time */ def Duration_=(time: Long): FiniteDuration = { - if (time > duration.toNanos) { - duration = FiniteDuration(time, "nanoseconds") + if (time > duration.toMillis) { + duration = FiniteDuration(time, "milliseconds") Renew } Duration @@ -177,7 +177,7 @@ class ActivityReport { * @return the current time */ def Renew: Long = { - val t = System.nanoTime + val t = System.currentTimeMillis() firstReport = firstReport.orElse(Some(t)) lastReport = Some(t) t @@ -191,6 +191,6 @@ class ActivityReport { heat = 0 firstReport = None lastReport = None - duration = FiniteDuration(0, "nanoseconds") + duration = FiniteDuration(0, "milliseconds") } } diff --git a/src/main/scala/net/psforever/objects/zones/ZoneHotSpotProjector.scala b/src/main/scala/net/psforever/objects/zones/ZoneHotSpotProjector.scala index a730afe66..a22b32314 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneHotSpotProjector.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneHotSpotProjector.scala @@ -165,7 +165,7 @@ class ZoneHotSpotProjector(zone: Zone, hotspots: ListBuffer[HotSpotInfo], blanki val attackerFaction = attacker.Faction val noPriorHotSpots = hotspots.isEmpty val duration = zone.HotSpotTimeFunction(defender, attacker) - if (duration.toNanos > 0) { + if (duration.toMillis > 0) { val hotspot = TryHotSpot(zone.HotSpotCoordinateFunction(location)) trace( s"updating activity status for ${zone.id} hotspot x=${hotspot.DisplayLocation.x} y=${hotspot.DisplayLocation.y}" @@ -191,11 +191,11 @@ class ZoneHotSpotProjector(zone: Zone, hotspots: ListBuffer[HotSpotInfo], blanki case ZoneHotSpotProjector.BlankingPhase() | Zone.HotSpot.Cleanup() => blanking.cancel() - val curr: Long = System.nanoTime + val curr: Long = System.currentTimeMillis() //blanking dated activity reports val changed = hotspots.flatMap(spot => { spot.Activity.collect { - case (b, a: ActivityReport) if a.LastReport + a.Duration.toNanos <= curr => + case (b, a: ActivityReport) if a.LastReport + a.Duration.toMillis <= curr => a.Clear() //this faction has no more activity in this sector (b, spot) } diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala index 975784381..2f1b7bd99 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala @@ -10,7 +10,6 @@ import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry} import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, ReconstructionActivity, RepairFromEquipment, RepairFromExoSuitChange, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity} import net.psforever.objects.zones.Zone -import net.psforever.objects.zones.exp.KillAssists.calculateMenace import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType} import net.psforever.zones.Zones diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index 8a495cb31..68ae6f12c 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -447,7 +447,16 @@ class AvatarService(zone: Zone) extends Actor { ) ) - case _ => ; + case AvatarAction.AwardCep(charId, bep) => + AvatarEvents.publish( + AvatarServiceResponse( + s"/$forChannel/Avatar", + Service.defaultPlayerGUID, + AvatarResponse.AwardCep(charId, bep) + ) + ) + + case _ => () } //message to Undertaker diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index 6ebf7fcc2..6775b8005 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -157,6 +157,7 @@ object AvatarAction { final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action final case class AwardSupportBep(charId: Long, bep: Long) extends Action + final case class AwardCep(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 7f16889c6..9c98c3622 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -128,4 +128,5 @@ object AvatarResponse { final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response final case class AwardSupportBep(charId: Long, bep: Long) extends Response + final case class AwardCep(charId: Long, bep: Long) extends Response } diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index c8cf0579a..500fc63da 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -9,19 +9,17 @@ import net.psforever.objects.serverobject.llu.CaptureFlag import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.zones.Zone -import net.psforever.objects.{Default, Player} +import net.psforever.objects.Default import net.psforever.packet.game.{GenericAction, PlanetsideAttributeEnum} import net.psforever.objects.sourcing.PlayerSource -import net.psforever.objects.zones.ZoneHotSpotProjector import net.psforever.services.Service +import net.psforever.services.local.support.HackCaptureActor.GetHackingFaction import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} -import net.psforever.util.Config import java.util.concurrent.TimeUnit -import scala.collection.mutable import scala.concurrent.duration.{FiniteDuration, _} -import scala.util.{Random, Success} +import scala.util.Random /** * Responsible for handling the aspects related to hacking control consoles and capturing bases. @@ -79,14 +77,9 @@ class HackCaptureActor extends Actor { case _ => // Timed hack finished (or neutral LLU base with no neighbour as timed hack), capture the base + val hackTime = terminal.Definition.FacilityHackTime.toMillis HackCompleted(terminal, hackedByFaction) - HackCaptureActor.RewardFacilityCaptureParticipants( - building, - terminal, - hacker, - now - entry.hack_timestamp, - isResecured = false - ) + building.Participation.RewardFacilityCapture(terminal.Faction, hackedByFaction, hacker, hackTime, hackTime, isResecured = false) } } // If there's hacked objects left in the list restart the timer with the shortest hack time left @@ -94,6 +87,7 @@ class HackCaptureActor extends Actor { case HackCaptureActor.ResecureCaptureTerminal(target, _, hacker) => val (results, remainder) = hackedObjects.partition(x => x.target eq target) + val faction = GetHackingFaction(target).getOrElse(target.Faction) target.HackedBy = None hackedObjects = remainder val building = target.Owner.asInstanceOf[Building] @@ -102,13 +96,7 @@ class HackCaptureActor extends Actor { case flag: CaptureFlag => target.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.Resecured) } NotifyHackStateChange(target, isResecured = true) -// HackCaptureActor.RewardFacilityCaptureParticipants( -// building, -// target, -// hacker, -// System.currentTimeMillis() - results.head.hack_timestamp, -// isResecured = true -// ) + building.Participation.RewardFacilityCapture(target.Faction, faction, hacker, target.Definition.FacilityHackTime.toMillis, System.currentTimeMillis() - results.head.hack_timestamp, isResecured = true) // Restart the timer in case the object we just removed was the next one scheduled RestartTimer() @@ -124,13 +112,7 @@ class HackCaptureActor extends Actor { val hackedByFaction = hackInfo.hackerFaction hackedObjects = hackedObjects.filterNot(x => x == entry) HackCompleted(terminal, hackedByFaction) -// HackCaptureActor.RewardFacilityCaptureParticipants( -// building, -// terminal, -// hacker, -// System.currentTimeMillis() - entry.hack_timestamp, -// isResecured = false -// ) + building.Participation.RewardFacilityCapture(terminal.Faction, hacker.Faction, hacker, terminal.Definition.FacilityHackTime.toMillis, System.currentTimeMillis() - entry.hack_timestamp, isResecured = false) entry.target.Actor ! CommonMessages.ClearHack() flag.Zone.LocalEvents ! CaptureFlagManager.Captured(flag) // If there's hacked objects left in the list restart the timer with the shortest hack time left @@ -299,141 +281,4 @@ object HackCaptureActor { entry2 } } - - import akka.pattern.ask - import akka.util.Timeout - import scala.concurrent.duration._ - import scala.concurrent.ExecutionContext.Implicits.global - - private implicit val timeout: Timeout = Timeout(5.seconds) - - private def RewardFacilityCaptureParticipants( - building: Building, - terminal: CaptureTerminal, - hacker: PlayerSource, - time: Long, - isResecured: Boolean - ): Unit = { - val faction: PlanetSideEmpire.Value = terminal.Faction - val (contributionVictor, contributionAgainst) = building.PlayerContribution.keys.partition { _.Faction == faction } - val contributionVictorSize = contributionVictor.size - val flagCarrier = if (!isResecured) { - building.GetFlagSocket.flatMap(_.previousFlag).flatMap(_.Carrier) - } else { - None - } - val request = ask(building.Zone.Activity, ZoneHotSpotProjector.ExposeHeatForRegion(building.Position, building.Definition.SOIRadius.toFloat)) - request.onComplete { - case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) => - val (heatVictor, heatAgainst) = { - val reports = activity.map { _.Activity } - val allHeat: List[Long] = reports.map { a => a.values.foldLeft(0L)(_ + _.Heat) } - val _rewardedHeat: List[Long] = reports.flatMap { rep => rep.get(faction).map { _.Heat.toLong } } - val _enemyHeat = allHeat.indices.map { index => - val allHeatValue = allHeat(index) - val rewardedHeatValue = _rewardedHeat(index) - allHeatValue - rewardedHeatValue - } - (_rewardedHeat, _enemyHeat.toList) - } - val heatVictorSum: Long = heatVictor.sum[Long] - val heatAgainstSum: Long = heatAgainst.sum[Long] - if (contributionVictorSize > 0) { - val contributionRate = if (heatVictorSum * heatAgainstSum != 0) { - math.log(heatVictorSum * contributionVictorSize / heatAgainstSum.toFloat).toFloat - } else { - contributionAgainst.size / contributionVictorSize.toFloat - } - RewardFacilityCaptureParticipants(building, terminal, faction, hacker, building.PlayersInSOI, flagCarrier, isResecured, time, contributionRate) - } - case _ => - RewardFacilityCaptureParticipants(building, terminal, faction, hacker, building.PlayersInSOI, flagCarrier, isResecured, time, victorContributionRate = 1.0f) - } - request.recover { - _ => RewardFacilityCaptureParticipants(building, terminal, faction, hacker, building.PlayersInSOI, flagCarrier, isResecured, time, victorContributionRate = 1.0f) - } - } - - private def RewardFacilityCaptureParticipants( - building: Building, - terminal: CaptureTerminal, - faction: PlanetSideEmpire.Value, - hacker: PlayerSource, - targets: List[Player], - flagCarrier: Option[Player], - isResecured: Boolean, - hackTime: Long, - victorContributionRate: Float - ): Unit = { - val contribution = building.PlayerContribution - val (contributionVictor, contributionAgainst) = contribution.keys.partition { _.Faction == faction } - val contributionVictorSize = contributionVictor.size - val contributionAgainstSize = contributionAgainst.size - val (contributionByTime, contributionByTimePartitioned) = { - val curr = System.currentTimeMillis() - val interval = 300000 - val range: Seq[Long] = { - val htime = hackTime.toInt - ( - if (htime < 60000) { - Seq(htime, interval + htime, 2 * interval + htime) - } else if (htime <= interval) { - Seq(60000, htime, interval + htime, 2 * interval + htime) - } else { - (60000 +: (interval to htime by interval)) ++ Seq(interval + htime, 2 * interval + htime) - } - ).map { _.toLong } - } - val playerMap = Array.fill[mutable.ListBuffer[Player]](range.size)(mutable.ListBuffer.empty) - contribution.foreach { case (p, t) => - playerMap(range.lastIndexWhere(time => curr - t <= time)).addOne(p) - } - (playerMap, playerMap.map { _.partition(_.Faction == faction) }) - } - val contributionByTimeSize = contributionByTime.length - - val base: Long = 50L - val overallPopulationBonus = { - contributionByTime.map { _.size }.sum * contributionByTimeSize + - contributionByTime.zipWithIndex.map { case (lst, index) => - ((contributionByTimeSize - index) * lst.size * - { - val lists = contributionByTimePartitioned(index) - lists._2.size / math.max(lists._1.size, 1).toFloat - }).toLong - }.sum - } - val competitionBonus: Long = if (contributionAgainstSize * 1.5f < contributionVictorSize.toFloat) { - //steamroll by the victor - 25L * (contributionVictorSize - contributionAgainstSize) - } else if (contributionVictorSize * 1.5f <= contributionAgainstSize.toFloat) { - //victory against overwhelming odds - 500L + 50L * contribution.keys.size - } else { - //still a battle - 10L * math.min(contributionAgainstSize, contributionVictorSize) - } - val timeMultiplier: Float = { - val buildingHackTimeMilli = terminal.Definition.FacilityHackTime.toMillis.toFloat - 1f + (if (isResecured) { - (buildingHackTimeMilli - hackTime) / buildingHackTimeMilli - } else { - 0f - }) - } - val finalCep: Long = ((base + overallPopulationBonus + competitionBonus) * timeMultiplier * Config.app.game.cepRate).toLong - //reward participant(s) -// targets -// .filter { player => -// player.Faction == faction && !player.Name.equals(hacker.Name) -// } -// .foreach { player => -// events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, finalCep)) -// } -// events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep)) -// flagCarrier match { -// case Some(player) => events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(player.CharId, finalCep / 2)) -// case None => ; -// } - } } diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index fa5c742fb..6c3b8e9de 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -152,6 +152,7 @@ case class GameConfig( amenityAutorepairDrainRate: Float, bepRate: Double, cepRate: Double, + maximumCepPerSquadSize: Seq[Int], newAvatar: NewAvatar, hart: HartConfig, sharedMaxCooldown: Boolean,