mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-07-16 08:55:18 +00:00
rewarding facility capture through cep; not fully tested yet; math sucks
This commit is contained in:
parent
d103671647
commit
a23d8d4cb8
21 changed files with 723 additions and 260 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */ }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;<br>
|
||||
* 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 => ;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ case class GameConfig(
|
|||
amenityAutorepairDrainRate: Float,
|
||||
bepRate: Double,
|
||||
cepRate: Double,
|
||||
maximumCepPerSquadSize: Seq[Int],
|
||||
newAvatar: NewAvatar,
|
||||
hart: HartConfig,
|
||||
sharedMaxCooldown: Boolean,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue