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
|
# Command experience rate
|
||||||
cep-rate = 1.0
|
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
|
# Modify the amount of mending per autorepair tick for facility amenities
|
||||||
amenity-autorepair-rate = 1.0
|
amenity-autorepair-rate = 1.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1671,8 +1671,8 @@ class AvatarActor(
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case AwardBep(bep, ExperienceType.Support) =>
|
case AwardBep(bep, ExperienceType.Support) =>
|
||||||
//TODO cache experience for slow payouts
|
|
||||||
supportExperiencePool = supportExperiencePool + bep
|
supportExperiencePool = supportExperiencePool + bep
|
||||||
|
avatar.scorecard.rate(bep)
|
||||||
if (supportExperienceTimer.isCancelled) {
|
if (supportExperienceTimer.isCancelled) {
|
||||||
resetSupportExperienceTimer(previousBep = 0, previousDelay = 0)
|
resetSupportExperienceTimer(previousBep = 0, previousDelay = 0)
|
||||||
}
|
}
|
||||||
|
|
@ -2990,8 +2990,8 @@ class AvatarActor(
|
||||||
case _ => None
|
case _ => None
|
||||||
}) match {
|
}) match {
|
||||||
case Some(mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons) =>
|
case Some(mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons) =>
|
||||||
player.HistoryAndContributions() ++ InGameHistory.ContributionFrom(mount)
|
player.HistoryAndContributions() ++ InGameHistory.ContributionFrom(mount).toList
|
||||||
case None =>
|
case _ =>
|
||||||
player.HistoryAndContributions()
|
player.HistoryAndContributions()
|
||||||
}
|
}
|
||||||
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, kill, exp)
|
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, kill, exp)
|
||||||
|
|
|
||||||
|
|
@ -401,6 +401,36 @@ class SessionAvatarHandlers(
|
||||||
case AvatarResponse.AwardSupportBep(_, bep) =>
|
case AvatarResponse.AwardSupportBep(_, bep) =>
|
||||||
avatarActor ! AvatarActor.AwardBep(bep, ExperienceType.Support)
|
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) =>
|
case AvatarResponse.SendResponse(msg) =>
|
||||||
sendResponse(msg)
|
sendResponse(msg)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
package net.psforever.actors.session.support
|
package net.psforever.actors.session.support
|
||||||
|
|
||||||
import akka.actor.{ActorContext, typed}
|
import akka.actor.{ActorContext, typed}
|
||||||
|
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||||
|
import net.psforever.objects.vital.InGameHistory
|
||||||
|
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
//
|
//
|
||||||
import net.psforever.actors.session.AvatarActor
|
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"
|
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")
|
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||||
log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}")
|
log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}")
|
||||||
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
|
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)
|
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}")
|
log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}")
|
||||||
DismountAction(tplayer, obj, seatNum)
|
DismountAction(tplayer, obj, seatNum)
|
||||||
|
|
||||||
|
|
@ -276,9 +279,10 @@ class SessionMountHandlers(
|
||||||
* @param obj the mountable object
|
* @param obj the mountable object
|
||||||
* @param seatNum the mount into which the player is mounting
|
* @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 playerGuid: PlanetSideGUID = tplayer.GUID
|
||||||
val objGuid: PlanetSideGUID = obj.GUID
|
val objGuid: PlanetSideGUID = obj.GUID
|
||||||
|
tplayer.ContributionFrom(obj)
|
||||||
sessionData.playerActionsToCancel()
|
sessionData.playerActionsToCancel()
|
||||||
avatarActor ! AvatarActor.DeactivateActiveImplants()
|
avatarActor ! AvatarActor.DeactivateActiveImplants()
|
||||||
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
|
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
|
||||||
|
|
@ -295,7 +299,7 @@ class SessionMountHandlers(
|
||||||
* @param obj the mountable object
|
* @param obj the mountable object
|
||||||
* @param seatNum the mount out of which which the player is disembarking
|
* @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)
|
DismountAction(tplayer, obj, seatNum)
|
||||||
//until vehicles maintain synchronized momentum without a driver
|
//until vehicles maintain synchronized momentum without a driver
|
||||||
obj match {
|
obj match {
|
||||||
|
|
@ -332,8 +336,9 @@ class SessionMountHandlers(
|
||||||
* @param obj the mountable object
|
* @param obj the mountable object
|
||||||
* @param seatNum the mount out of which which the player is disembarking
|
* @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
|
val playerGuid: PlanetSideGUID = tplayer.GUID
|
||||||
|
tplayer.ContributionFrom(obj)
|
||||||
sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
|
sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
|
||||||
val bailType = if (tplayer.BailProtection) {
|
val bailType = if (tplayer.BailProtection) {
|
||||||
BailType.Bailed
|
BailType.Bailed
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,12 @@ final case class Life(
|
||||||
kills: Seq[Kill],
|
kills: Seq[Kill],
|
||||||
assists: Seq[Assist],
|
assists: Seq[Assist],
|
||||||
death: Option[Death],
|
death: Option[Death],
|
||||||
equipmentStats: Seq[EquipmentStat]
|
equipmentStats: Seq[EquipmentStat],
|
||||||
|
supportExperience: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
object Life {
|
object Life {
|
||||||
def apply(): Life = Life(Nil, Nil, None, Nil)
|
def apply(): Life = Life(Nil, Nil, None, Nil, 0)
|
||||||
|
|
||||||
def bep(life: Life): Long = {
|
def bep(life: Life): Long = {
|
||||||
life.kills.foldLeft(0L)(_ + _.experienceEarned) + life.assists.foldLeft(0L)(_ + _.experienceEarned)
|
life.kills.foldLeft(0L)(_ + _.experienceEarned) + life.assists.foldLeft(0L)(_ + _.experienceEarned)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ class ScoreCard() {
|
||||||
|
|
||||||
def Lives: Seq[Life] = lives
|
def Lives: Seq[Life] = lives
|
||||||
|
|
||||||
|
def Kills: Seq[Kill] = lives.flatMap { _.kills } ++ curr.kills
|
||||||
|
|
||||||
def KillStatistics: Map[Int, Statistic] = killStatistics.toMap
|
def KillStatistics: Map[Int, Statistic] = killStatistics.toMap
|
||||||
|
|
||||||
def AssistStatistics: Map[Int, Statistic] = assistStatistics.toMap
|
def AssistStatistics: Map[Int, Statistic] = assistStatistics.toMap
|
||||||
|
|
@ -40,7 +42,9 @@ class ScoreCard() {
|
||||||
val expired = curr
|
val expired = curr
|
||||||
curr = Life()
|
curr = Life()
|
||||||
lives = expired.copy(death = Some(d)) +: lives
|
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 scalax.collection.{Graph, GraphEdge}
|
||||||
import akka.actor.typed.scaladsl.adapter._
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
|
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 net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
|
||||||
|
|
||||||
import scala.collection.mutable
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import scala.concurrent.duration._
|
|
||||||
|
|
||||||
class Building(
|
class Building(
|
||||||
private val name: String,
|
private val name: String,
|
||||||
|
|
@ -34,9 +33,8 @@ class Building(
|
||||||
|
|
||||||
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||||
private var playersInSOI: List[Player] = List.empty
|
private var playersInSOI: List[Player] = List.empty
|
||||||
private val capitols = List("Thoth", "Voltan", "Neit", "Anguta", "Eisa", "Verica")
|
|
||||||
private var forceDomeActive: Boolean = false
|
private var forceDomeActive: Boolean = false
|
||||||
private var participationFunc: Building.ParticipationLogic = Building.NoParticipation
|
private var participationFunc: ParticipationLogic = NoParticipation
|
||||||
super.Zone_=(zone)
|
super.Zone_=(zone)
|
||||||
super.GUID_=(PlanetSideGUID(building_guid)) //set
|
super.GUID_=(PlanetSideGUID(building_guid)) //set
|
||||||
Invalidate() //unset; guid can be used during setup, but does not stop being registered properly later
|
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 MapId: Int = map_id
|
||||||
|
|
||||||
def IsCapitol: Boolean = capitols.contains(name)
|
def IsCapitol: Boolean = Building.Capitols.contains(name)
|
||||||
|
|
||||||
def IsSubCapitol: Boolean = {
|
def IsSubCapitol: Boolean = {
|
||||||
Neighbours match {
|
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
|
case None => false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,13 +84,11 @@ class Building(
|
||||||
box.Actor ! Painbox.Stop()
|
box.Actor ! Painbox.Stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
participationFunc.Players(building = this, list)
|
|
||||||
playersInSOI = list
|
playersInSOI = list
|
||||||
|
participationFunc.TryUpdate()
|
||||||
playersInSOI
|
playersInSOI
|
||||||
}
|
}
|
||||||
|
|
||||||
def PlayerContribution: Map[Player, Long] = participationFunc.Contribution()
|
|
||||||
|
|
||||||
// Get all lattice neighbours
|
// Get all lattice neighbours
|
||||||
def AllNeighbours: Option[Set[Building]] = {
|
def AllNeighbours: Option[Set[Building]] = {
|
||||||
zone.Lattice find this match {
|
zone.Lattice find this match {
|
||||||
|
|
@ -360,43 +357,24 @@ class Building(
|
||||||
|
|
||||||
override def Amenities_=(obj: Amenity): List[Amenity] = {
|
override def Amenities_=(obj: Amenity): List[Amenity] = {
|
||||||
obj match {
|
obj match {
|
||||||
case _: CaptureTerminal => participationFunc = Building.FacilityHackParticipation
|
case _: CaptureTerminal =>
|
||||||
case _ => ;
|
if (buildingType == StructureType.Facility) {
|
||||||
|
participationFunc = MajorFacilityHackParticipation(this)
|
||||||
|
} else if (buildingType == StructureType.Tower) {
|
||||||
|
participationFunc = TowerHackParticipation(this)
|
||||||
|
}
|
||||||
|
case _ => ()
|
||||||
}
|
}
|
||||||
super.Amenities_=(obj)
|
super.Amenities_=(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def Participation: ParticipationLogic = participationFunc
|
||||||
|
|
||||||
def Definition: BuildingDefinition = buildingDefinition
|
def Definition: BuildingDefinition = buildingDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
object Building {
|
object Building {
|
||||||
trait ParticipationLogic {
|
final val Capitols = List("Thoth", "Voltan", "Neit", "Anguta", "Eisa", "Verica")
|
||||||
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 NoBuilding: Building =
|
final val NoBuilding: Building =
|
||||||
new Building(name = "", 0, map_id = 0, Zone.Nowhere, StructureType.Platform, GlobalDefinitions.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 {
|
action match {
|
||||||
case Some(act) =>
|
case Some(act) =>
|
||||||
history = history :+ act
|
history = history :+ act
|
||||||
if (IsContributionEvent(act)) {
|
|
||||||
previousContributionInheritance = None
|
|
||||||
}
|
|
||||||
case None => ()
|
case None => ()
|
||||||
}
|
}
|
||||||
history
|
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] =
|
private val contributionInheritance: mutable.HashMap[SourceUniqueness, Contribution] =
|
||||||
mutable.HashMap[SourceUniqueness, Contribution]()
|
mutable.HashMap[SourceUniqueness, Contribution]()
|
||||||
|
|
||||||
|
|
@ -277,30 +276,7 @@ trait InGameHistory {
|
||||||
}
|
}
|
||||||
|
|
||||||
def GetContribution(): Option[List[InGameActivity]] = {
|
def GetContribution(): Option[List[InGameActivity]] = {
|
||||||
previousContributionInheritance
|
Option(GetContributionDuringPeriod(History, duration = 600000L))
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = {
|
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] = {
|
def HistoryAndContributions(): List[InGameActivity] = {
|
||||||
History ++ contributionInheritance.values.toList
|
History ++ contributionInheritance.values.toList
|
||||||
}
|
}
|
||||||
|
|
@ -327,7 +295,6 @@ trait InGameHistory {
|
||||||
lastDamage = None
|
lastDamage = None
|
||||||
val out = history
|
val out = history
|
||||||
history = List.empty
|
history = List.empty
|
||||||
previousContributionInheritance = None
|
|
||||||
contributionInheritance.clear()
|
contributionInheritance.clear()
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ class ActivityReport {
|
||||||
* @return the time
|
* @return the time
|
||||||
*/
|
*/
|
||||||
def Duration_=(time: FiniteDuration): FiniteDuration = {
|
def Duration_=(time: FiniteDuration): FiniteDuration = {
|
||||||
Duration_=(time.toNanos)
|
Duration_=(time.toMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -112,8 +112,8 @@ class ActivityReport {
|
||||||
* @return the time
|
* @return the time
|
||||||
*/
|
*/
|
||||||
def Duration_=(time: Long): FiniteDuration = {
|
def Duration_=(time: Long): FiniteDuration = {
|
||||||
if (time > duration.toNanos) {
|
if (time > duration.toMillis) {
|
||||||
duration = FiniteDuration(time, "nanoseconds")
|
duration = FiniteDuration(time, "milliseconds")
|
||||||
Renew
|
Renew
|
||||||
}
|
}
|
||||||
Duration
|
Duration
|
||||||
|
|
@ -177,7 +177,7 @@ class ActivityReport {
|
||||||
* @return the current time
|
* @return the current time
|
||||||
*/
|
*/
|
||||||
def Renew: Long = {
|
def Renew: Long = {
|
||||||
val t = System.nanoTime
|
val t = System.currentTimeMillis()
|
||||||
firstReport = firstReport.orElse(Some(t))
|
firstReport = firstReport.orElse(Some(t))
|
||||||
lastReport = Some(t)
|
lastReport = Some(t)
|
||||||
t
|
t
|
||||||
|
|
@ -191,6 +191,6 @@ class ActivityReport {
|
||||||
heat = 0
|
heat = 0
|
||||||
firstReport = None
|
firstReport = None
|
||||||
lastReport = 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 attackerFaction = attacker.Faction
|
||||||
val noPriorHotSpots = hotspots.isEmpty
|
val noPriorHotSpots = hotspots.isEmpty
|
||||||
val duration = zone.HotSpotTimeFunction(defender, attacker)
|
val duration = zone.HotSpotTimeFunction(defender, attacker)
|
||||||
if (duration.toNanos > 0) {
|
if (duration.toMillis > 0) {
|
||||||
val hotspot = TryHotSpot(zone.HotSpotCoordinateFunction(location))
|
val hotspot = TryHotSpot(zone.HotSpotCoordinateFunction(location))
|
||||||
trace(
|
trace(
|
||||||
s"updating activity status for ${zone.id} hotspot x=${hotspot.DisplayLocation.x} y=${hotspot.DisplayLocation.y}"
|
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() =>
|
case ZoneHotSpotProjector.BlankingPhase() | Zone.HotSpot.Cleanup() =>
|
||||||
blanking.cancel()
|
blanking.cancel()
|
||||||
val curr: Long = System.nanoTime
|
val curr: Long = System.currentTimeMillis()
|
||||||
//blanking dated activity reports
|
//blanking dated activity reports
|
||||||
val changed = hotspots.flatMap(spot => {
|
val changed = hotspots.flatMap(spot => {
|
||||||
spot.Activity.collect {
|
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
|
a.Clear() //this faction has no more activity in this sector
|
||||||
(b, spot)
|
(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.sourcing.{AmenitySource, PlayerSource, SourceEntry}
|
||||||
import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, ReconstructionActivity, RepairFromEquipment, RepairFromExoSuitChange, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity}
|
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.Zone
|
||||||
import net.psforever.objects.zones.exp.KillAssists.calculateMenace
|
|
||||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
|
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
|
||||||
import net.psforever.zones.Zones
|
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
|
//message to Undertaker
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,7 @@ object AvatarAction {
|
||||||
|
|
||||||
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action
|
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action
|
||||||
final case class AwardSupportBep(charId: Long, bep: Long) extends Action
|
final case class 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 TeardownConnection() extends Action
|
||||||
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) 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 UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response
|
||||||
final case class AwardSupportBep(charId: Long, bep: Long) 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.structures.Building
|
||||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
|
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
|
||||||
import net.psforever.objects.zones.Zone
|
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.packet.game.{GenericAction, PlanetsideAttributeEnum}
|
||||||
import net.psforever.objects.sourcing.PlayerSource
|
import net.psforever.objects.sourcing.PlayerSource
|
||||||
import net.psforever.objects.zones.ZoneHotSpotProjector
|
|
||||||
import net.psforever.services.Service
|
import net.psforever.services.Service
|
||||||
|
import net.psforever.services.local.support.HackCaptureActor.GetHackingFaction
|
||||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
|
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
|
||||||
import net.psforever.util.Config
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import scala.collection.mutable
|
|
||||||
import scala.concurrent.duration.{FiniteDuration, _}
|
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.
|
* Responsible for handling the aspects related to hacking control consoles and capturing bases.
|
||||||
|
|
@ -79,14 +77,9 @@ class HackCaptureActor extends Actor {
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
// Timed hack finished (or neutral LLU base with no neighbour as timed hack), capture the base
|
// 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)
|
HackCompleted(terminal, hackedByFaction)
|
||||||
HackCaptureActor.RewardFacilityCaptureParticipants(
|
building.Participation.RewardFacilityCapture(terminal.Faction, hackedByFaction, hacker, hackTime, hackTime, isResecured = false)
|
||||||
building,
|
|
||||||
terminal,
|
|
||||||
hacker,
|
|
||||||
now - entry.hack_timestamp,
|
|
||||||
isResecured = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If there's hacked objects left in the list restart the timer with the shortest hack time left
|
// 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) =>
|
case HackCaptureActor.ResecureCaptureTerminal(target, _, hacker) =>
|
||||||
val (results, remainder) = hackedObjects.partition(x => x.target eq target)
|
val (results, remainder) = hackedObjects.partition(x => x.target eq target)
|
||||||
|
val faction = GetHackingFaction(target).getOrElse(target.Faction)
|
||||||
target.HackedBy = None
|
target.HackedBy = None
|
||||||
hackedObjects = remainder
|
hackedObjects = remainder
|
||||||
val building = target.Owner.asInstanceOf[Building]
|
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)
|
case flag: CaptureFlag => target.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.Resecured)
|
||||||
}
|
}
|
||||||
NotifyHackStateChange(target, isResecured = true)
|
NotifyHackStateChange(target, isResecured = true)
|
||||||
// HackCaptureActor.RewardFacilityCaptureParticipants(
|
building.Participation.RewardFacilityCapture(target.Faction, faction, hacker, target.Definition.FacilityHackTime.toMillis, System.currentTimeMillis() - results.head.hack_timestamp, isResecured = true)
|
||||||
// building,
|
|
||||||
// target,
|
|
||||||
// hacker,
|
|
||||||
// System.currentTimeMillis() - results.head.hack_timestamp,
|
|
||||||
// isResecured = true
|
|
||||||
// )
|
|
||||||
// Restart the timer in case the object we just removed was the next one scheduled
|
// Restart the timer in case the object we just removed was the next one scheduled
|
||||||
RestartTimer()
|
RestartTimer()
|
||||||
|
|
||||||
|
|
@ -124,13 +112,7 @@ class HackCaptureActor extends Actor {
|
||||||
val hackedByFaction = hackInfo.hackerFaction
|
val hackedByFaction = hackInfo.hackerFaction
|
||||||
hackedObjects = hackedObjects.filterNot(x => x == entry)
|
hackedObjects = hackedObjects.filterNot(x => x == entry)
|
||||||
HackCompleted(terminal, hackedByFaction)
|
HackCompleted(terminal, hackedByFaction)
|
||||||
// HackCaptureActor.RewardFacilityCaptureParticipants(
|
building.Participation.RewardFacilityCapture(terminal.Faction, hacker.Faction, hacker, terminal.Definition.FacilityHackTime.toMillis, System.currentTimeMillis() - entry.hack_timestamp, isResecured = false)
|
||||||
// building,
|
|
||||||
// terminal,
|
|
||||||
// hacker,
|
|
||||||
// System.currentTimeMillis() - entry.hack_timestamp,
|
|
||||||
// isResecured = false
|
|
||||||
// )
|
|
||||||
entry.target.Actor ! CommonMessages.ClearHack()
|
entry.target.Actor ! CommonMessages.ClearHack()
|
||||||
flag.Zone.LocalEvents ! CaptureFlagManager.Captured(flag)
|
flag.Zone.LocalEvents ! CaptureFlagManager.Captured(flag)
|
||||||
// If there's hacked objects left in the list restart the timer with the shortest hack time left
|
// 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
|
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,
|
amenityAutorepairDrainRate: Float,
|
||||||
bepRate: Double,
|
bepRate: Double,
|
||||||
cepRate: Double,
|
cepRate: Double,
|
||||||
|
maximumCepPerSquadSize: Seq[Int],
|
||||||
newAvatar: NewAvatar,
|
newAvatar: NewAvatar,
|
||||||
hart: HartConfig,
|
hart: HartConfig,
|
||||||
sharedMaxCooldown: Boolean,
|
sharedMaxCooldown: Boolean,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue