rewarding facility capture through cep; not fully tested yet; math sucks

This commit is contained in:
Fate-JH 2023-07-04 04:24:52 -04:00
parent d103671647
commit a23d8d4cb8
21 changed files with 723 additions and 260 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -152,6 +152,7 @@ case class GameConfig(
amenityAutorepairDrainRate: Float,
bepRate: Double,
cepRate: Double,
maximumCepPerSquadSize: Seq[Int],
newAvatar: NewAvatar,
hart: HartConfig,
sharedMaxCooldown: Boolean,