corrected issue with multiplier for experience earned while in debt; poor separation between special case players in facility capture situation; missing database calls during facility capture situations

This commit is contained in:
Fate-JH 2023-11-21 02:38:44 -05:00
parent cc2089b513
commit 42d1422fc7
6 changed files with 503 additions and 428 deletions

View file

@ -1623,7 +1623,7 @@ class AvatarActor(
case AwardBep(bep, modifier) => case AwardBep(bep, modifier) =>
awardProgressionOrExperience( awardProgressionOrExperience(
setBepAction(modifier), setBepAction(modifier),
avatar.bep + bep, bep,
Config.app.game.promotion.battleExperiencePointsModifier Config.app.game.promotion.battleExperiencePointsModifier
) )
Behaviors.same Behaviors.same
@ -1631,7 +1631,7 @@ class AvatarActor(
case AwardFacilityCaptureBep(bep) => case AwardFacilityCaptureBep(bep) =>
awardProgressionOrExperience( awardProgressionOrExperience(
setBepAction(ExperienceType.Normal), setBepAction(ExperienceType.Normal),
avatar.bep + bep, bep,
Config.app.game.promotion.captureExperiencePointsModifier Config.app.game.promotion.captureExperiencePointsModifier
) )
Behaviors.same Behaviors.same
@ -1684,7 +1684,7 @@ class AvatarActor(
if (experienceDebt == 0L) { if (experienceDebt == 0L) {
setCep(avatar.cep + cep) setCep(avatar.cep + cep)
} else if (cep > 0) { } else if (cep > 0) {
sessionActor ! SessionActor.SendResponse(ExperienceAddedMessage(0)) sessionActor ! SessionActor.SendResponse(ExperienceAddedMessage())
} }
Behaviors.same Behaviors.same
@ -1859,63 +1859,63 @@ class AvatarActor(
def performAvatarLogin(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = { def performAvatarLogin(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
performAvatarLogin0(avatarId, accountId, replyTo) performAvatarLogin0(avatarId, accountId, replyTo)
// import ctx._ /*import ctx._
// val result = for { val result = for {
// //log this login //log this login
// _ <- ctx.run( _ <- ctx.run(
// query[persistence.Avatar] query[persistence.Avatar]
// .filter(_.id == lift(avatarId)) .filter(_.id == lift(avatarId))
// .update(_.lastLogin -> lift(LocalDateTime.now())) .update(_.lastLogin -> lift(LocalDateTime.now()))
// ) )
// //log this choice of faction (no empire switching) //log this choice of faction (no empire switching)
// _ <- ctx.run( _ <- ctx.run(
// query[persistence.Account] query[persistence.Account]
// .filter(_.id == lift(accountId)) .filter(_.id == lift(accountId))
// .update( .update(
// _.lastFactionId -> lift(avatar.faction.id), _.lastFactionId -> lift(avatar.faction.id),
// _.avatarLoggedIn -> lift(avatarId) _.avatarLoggedIn -> lift(avatarId)
// ) )
// ) )
// //retrieve avatar data //retrieve avatar data
// loadouts <- initializeAllLoadouts() loadouts <- initializeAllLoadouts()
// implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatarId))) implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatarId)))
// certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatarId))) certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatarId)))
// locker <- loadLocker(avatarId) locker <- loadLocker(avatarId)
// friends <- loadFriendList(avatarId) friends <- loadFriendList(avatarId)
// ignored <- loadIgnoredList(avatarId) ignored <- loadIgnoredList(avatarId)
// shortcuts <- loadShortcuts(avatarId) shortcuts <- loadShortcuts(avatarId)
// saved <- AvatarActor.loadSavedAvatarData(avatarId) saved <- AvatarActor.loadSavedAvatarData(avatarId)
// debt <- AvatarActor.loadExperienceDebt(avatarId) debt <- AvatarActor.loadExperienceDebt(avatarId)
// card <- AvatarActor.loadCampaignKdaData(avatarId) card <- AvatarActor.loadCampaignKdaData(avatarId)
// } yield (loadouts, implants, certs, locker, friends, ignored, shortcuts, saved, debt, card) } yield (loadouts, implants, certs, locker, friends, ignored, shortcuts, saved, debt, card)
// result.onComplete { result.onComplete {
// case Success((_loadouts, implants, certs, lockerInv, friendsList, ignoredList, shortcutList, saved, debt, card)) => case Success((_loadouts, implants, certs, lockerInv, friendsList, ignoredList, shortcutList, saved, debt, card)) =>
// avatarCopy( avatarCopy(
// avatar.copy( avatar.copy(
// loadouts = avatar.loadouts.copy(suit = _loadouts), loadouts = avatar.loadouts.copy(suit = _loadouts),
// certifications = certifications =
// certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications, certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications,
// implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None), implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None),
// shortcuts = shortcutList, shortcuts = shortcutList,
// locker = lockerInv, locker = lockerInv,
// people = MemberLists( people = MemberLists(
// friend = friendsList, friend = friendsList,
// ignored = ignoredList ignored = ignoredList
// ), ),
// cooldowns = Cooldowns( cooldowns = Cooldowns(
// purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log), purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log),
// use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log) use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log)
// ), ),
// scorecard = card scorecard = card
// ) )
// ) )
// // if we need to start stamina regeneration // if we need to start stamina regeneration
// tryRestoreStaminaForSession(stamina = 1).collect { _ => defaultStaminaRegen(initialDelay = 0.5f seconds) } tryRestoreStaminaForSession(stamina = 1).collect { _ => defaultStaminaRegen(initialDelay = 0.5f seconds) }
// experienceDebt = debt experienceDebt = debt
// replyTo ! AvatarLoginResponse(avatar) replyTo ! AvatarLoginResponse(avatar)
// case Failure(e) => case Failure(e) =>
// log.error(e)("db failure") log.error(e)("db failure")
// } }*/
} }
def performAvatarLogin0(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = { def performAvatarLogin0(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
@ -3084,23 +3084,25 @@ class AvatarActor(
experience: Long, experience: Long,
modifier: Float modifier: Float
): Unit = { ): Unit = {
if (experienceDebt == 0L) { if (experience > 0) {
awardAction(experience) if (experienceDebt == 0L) {
} else if (modifier > 0f) {
val modifiedBep = (experience.toFloat * modifier).toLong
val gain = modifiedBep - experienceDebt
if (gain > 0L) {
experienceDebt = 0L
awardAction(experience) awardAction(experience)
} else { } else if (modifier > 0f) {
experienceDebt = experienceDebt - modifiedBep val modifiedBep = (experience.toFloat * modifier).toLong
sessionActor ! SessionActor.SendResponse(ExperienceAddedMessage()) val gain = modifiedBep - experienceDebt
if (gain > 0L) {
experienceDebt = 0L
awardAction(experience)
} else {
experienceDebt = experienceDebt - modifiedBep
sessionActor ! SessionActor.SendResponse(ExperienceAddedMessage())
}
} }
} }
} }
private def setBepAction(modifier: ExperienceType)(value: Long): Unit = { private def setBepAction(modifier: ExperienceType)(value: Long): Unit = {
setBep(value, modifier) setBep(avatar.bep + value, modifier)
} }
private def setSupportAction(value: Long): Unit = { private def setSupportAction(value: Long): Unit = {

View file

@ -398,6 +398,7 @@ class SessionAvatarHandlers(
avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda) avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
case AvatarResponse.AwardBep(charId, bep, expType) => case AvatarResponse.AwardBep(charId, bep, expType) =>
//if the target player, always award (some) BEP
if (charId == player.CharId) { if (charId == player.CharId) {
avatarActor ! AvatarActor.AwardBep(bep, expType) avatarActor ! AvatarActor.AwardBep(bep, expType)
} }
@ -409,67 +410,7 @@ class SessionAvatarHandlers(
} }
case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) => case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) =>
//must be in a squad to earn experience facilityCaptureRewards(buildingId, zoneNumber, cep)
val cepConfig = Config.app.game.experience.cep
val charId = player.CharId
val squadUI = sessionData.squad.squadUI
val participation = continent
.Building(buildingId)
.map { building =>
building.Participation.PlayerContribution()
}
squadUI
.find { _._1 == charId }
.collect {
case (_, elem) if elem.index == 0 =>
//squad leader earns CEP, modified by squad effort, capped by squad size present during the capture
val squadParticipation = participation match {
case Some(map) => map.filter { case (id, _) => squadUI.contains(id) }
case _ => Map.empty[Long, Float]
}
val maxCepBySquadSize: Long = {
val maxCepList = cepConfig.maximumPerSquadSize
val squadSize: Int = squadParticipation.size
maxCepList.lift(squadSize - 1).getOrElse(squadSize * maxCepList.head).toLong
}
val groupContribution: Float = squadUI
.map { case (id, _) => (id, squadParticipation.getOrElse(id, 0f) / 10f) }
.values
.max
val modifiedExp: Long = (cep.toFloat * groupContribution).toLong
val cappedModifiedExp: Long = math.min(modifiedExp, maxCepBySquadSize)
val finalExp: Long = if (modifiedExp > cappedModifiedExp) {
val overLimitOverflow = if (cepConfig.squadSizeLimitOverflow == -1) {
cep.toFloat
} else {
cepConfig.squadSizeLimitOverflow.toFloat
}
cappedModifiedExp + (overLimitOverflow * cepConfig.squadSizeLimitOverflowMultiplier).toLong
} else {
cappedModifiedExp
}
exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, finalExp, expType="cep")
avatarActor ! AvatarActor.AwardCep(finalExp)
Some(finalExp)
case _ =>
//squad member earns BEP based on CEP, modified by personal effort
val individualContribution = {
val contributionList = for {
facilityMap <- participation
if facilityMap.contains(charId)
} yield facilityMap(charId)
if (contributionList.nonEmpty) {
contributionList.max
} else {
0f
}
}
val modifiedExp = (cep * individualContribution).toLong
exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, modifiedExp, expType="bep")
avatarActor ! AvatarActor.AwardFacilityCaptureBep(modifiedExp)
Some(modifiedExp)
}
case AvatarResponse.SendResponse(msg) => case AvatarResponse.SendResponse(msg) =>
sendResponse(msg) sendResponse(msg)
@ -665,6 +606,71 @@ class SessionAvatarHandlers(
) )
) )
} }
private def facilityCaptureRewards(buildingId: Int, zoneNumber: Int, cep: Long): Unit = {
//TODO squad services deactivated, participation trophy rewards for now - 11-20-2023
//must be in a squad to earn experience
val charId = player.CharId
val squadUI = sessionData.squad.squadUI
val participation = continent
.Building(buildingId)
.map { building =>
building.Participation.PlayerContribution()
}
squadUI
.find { _._1 == charId }
.collect {
case (_, elem) if elem.index == 0 =>
val cepConfig = Config.app.game.experience.cep
//squad leader earns CEP, modified by squad effort, capped by squad size present during the capture
val squadParticipation = participation match {
case Some(map) => map.filter { case (id, _) => squadUI.contains(id) }
case _ => Map.empty[Long, Float]
}
val maxCepBySquadSize: Long = {
val maxCepList = cepConfig.maximumPerSquadSize
val squadSize: Int = squadParticipation.size
maxCepList.lift(squadSize - 1).getOrElse(squadSize * maxCepList.head).toLong
}
val groupContribution: Float = squadUI
.map { case (id, _) => (id, squadParticipation.getOrElse(id, 0f) / 10f) }
.values
.max
val modifiedExp: Long = (cep.toFloat * groupContribution).toLong
val cappedModifiedExp: Long = math.min(modifiedExp, maxCepBySquadSize)
val finalExp: Long = if (modifiedExp > cappedModifiedExp) {
val overLimitOverflow = if (cepConfig.squadSizeLimitOverflow == -1) {
cep.toFloat
} else {
cepConfig.squadSizeLimitOverflow.toFloat
}
cappedModifiedExp + (overLimitOverflow * (math.random().toFloat % cepConfig.squadSizeLimitOverflowMultiplier)).toLong
} else {
cappedModifiedExp
}
exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, finalExp, expType="cep")
avatarActor ! AvatarActor.AwardCep(finalExp)
Some(finalExp)
case _ =>
//squad member earns BEP based on CEP, modified by personal effort
val individualContribution = {
val contributionList = for {
facilityMap <- participation
if facilityMap.contains(charId)
} yield facilityMap(charId)
if (contributionList.nonEmpty) {
contributionList.max
} else {
0f
}
}
val modifiedExp = (cep * individualContribution).toLong
exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, modifiedExp, expType="bep")
avatarActor ! AvatarActor.AwardFacilityCaptureBep(modifiedExp)
Some(modifiedExp)
}
}
} }
object SessionAvatarHandlers { object SessionAvatarHandlers {

View file

@ -203,10 +203,10 @@ object FacilityHackParticipation {
} }
} }
} }
if (dataSum != 0) { if (dataCount != 0) {
math.max(0.15f, math.min(2f, dataSum / dataCount.toFloat)) math.max(0.2f, math.min(2f, dataSum / dataCount.toFloat))
} else { } else {
1f 0.5f
} }
} }
@ -220,14 +220,15 @@ object FacilityHackParticipation {
private[participation] def populationProgressModifier( private[participation] def populationProgressModifier(
populationNumbers: Seq[Int], populationNumbers: Seq[Int],
gradingRule: Int=>Float, gradingRule: Int=>Float,
layers: Int layers: Int,
ordering: Ordering[Int] = Ordering[Int]
): Float = { ): Float = {
val gradedPopulation = populationNumbers val gradedPopulation = populationNumbers
.map { gradingRule } .map { gradingRule }
.groupBy(x => x) .groupBy(x => x)
.values .values
.toSeq .toSeq
.sortBy(_.size) .sortBy(_.size)(ordering)
.take(layers) .take(layers)
.flatten .flatten
gradedPopulation.sum / gradedPopulation.size.toFloat gradedPopulation.sum / gradedPopulation.size.toFloat
@ -236,7 +237,8 @@ object FacilityHackParticipation {
private[participation] def populationBalanceModifier( private[participation] def populationBalanceModifier(
victorPopulationNumbers: Seq[Int], victorPopulationNumbers: Seq[Int],
opposingPopulationNumbers: Seq[Int], opposingPopulationNumbers: Seq[Int],
healthyPercentage: Float healthyPercentage: Float,
maxRatio: Float = 1f
): Float = { ): Float = {
val rate = for { val rate = for {
victorPop <- victorPopulationNumbers victorPop <- victorPopulationNumbers
@ -252,7 +254,7 @@ object FacilityHackParticipation {
} }
if true if true
} yield out } yield out
rate.sum / rate.size.toFloat math.max(0f, math.min(rate.sum / rate.size.toFloat, maxRatio))
} }
private[participation] def competitionBonus( private[participation] def competitionBonus(

View file

@ -5,7 +5,7 @@ import net.psforever.objects.serverobject.structures.{Building, StructureType}
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer} import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
import net.psforever.objects.zones.{HotSpotInfo, ZoneHotSpotProjector} import net.psforever.objects.zones.{HotSpotInfo, ZoneHotSpotProjector}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState, Vector3} import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.util.Config import net.psforever.util.Config
import akka.pattern.ask import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
@ -45,7 +45,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
import net.psforever.objects.zones.ZoneHotSpotProjector import net.psforever.objects.zones.ZoneHotSpotProjector
import scala.concurrent.Promise import scala.concurrent.Promise
import scala.util.Success // import scala.util.Success
val requestLayers: Promise[ZoneHotSpotProjector.ExposedHeat] = Promise[ZoneHotSpotProjector.ExposedHeat]() val requestLayers: Promise[ZoneHotSpotProjector.ExposedHeat] = Promise[ZoneHotSpotProjector.ExposedHeat]()
// val request = updateHotSpotInfoOnly() // val request = updateHotSpotInfoOnly()
// requestLayers.completeWith(request) // requestLayers.completeWith(request)
@ -67,204 +67,237 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
completionTime: Long, completionTime: Long,
isResecured: Boolean isResecured: Boolean
): Unit = { ): Unit = {
val curr = System.currentTimeMillis() //has the facility ran out of nanites during the hack
val hackStart = curr - completionTime if (building.NtuLevel > 0) {
val socketOpt = building.GetFlagSocket val curr = System.currentTimeMillis()
val (victorFaction, opposingFaction, hasFlag, flagCarrier) = if (!isResecured) { val hackStart = curr - completionTime
val carrier = socketOpt.flatMap(_.previousFlag).flatMap(_.Carrier) val socketOpt = building.GetFlagSocket
(attackingFaction, defenderFaction, socketOpt.nonEmpty, carrier) val (victorFaction, opposingFaction, hasFlag, flagCarrier) = if (!isResecured) {
} else { val carrier = socketOpt.flatMap(_.previousFlag).flatMap(_.Carrier)
(defenderFaction, attackingFaction, socketOpt.nonEmpty, None) (attackingFaction, defenderFaction, socketOpt.nonEmpty, carrier)
} } else {
val (contributionVictor, contributionOpposing, _) = { (defenderFaction, attackingFaction, socketOpt.nonEmpty, None)
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) {
//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))
} }
val contributionOpposingSize = contributionOpposing.size val (contributionVictor, contributionOpposing, _) = {
val killsByPlayersNotInTower = eliminateClosestTowerFromParticipating( val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction }
building, val (b, c) = b1.partition { case (_, (p, _, _)) => p.Faction == opposingFaction }
FacilityHackParticipation.allocateKillsByPlayers( (a.values, b.values, c.values)
building.Position, }
building.Definition.SOIRadius.toFloat, val contributionVictorSize = contributionVictor.size
hackStart, if (contributionVictorSize > 0) {
completionTime, //setup for ...
opposingFaction, val populationIndices = playerPopulationOverTime.indices
contributionOpposing val allFactions = PlanetSideEmpire.values.filterNot {
) _ == PlanetSideEmpire.NEUTRAL
) }.toSeq
//1) experience from killing opposingFaction across duration of hack val (victorPopulationByLayer, opposingPopulationByLayer) = {
//The kills that occurred in the facility's attached field tower's sphere of influence have been eliminated from consideration. val individualPopulationByLayer = allFactions.map { f =>
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills( (f, populationIndices.indices.map { i => playerPopulationOverTime(i)(f) })
killsByPlayersNotInTower, }.toMap[PlanetSideEmpire.Value, Seq[Int]]
contributionOpposingSize (individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction))
)
//2) peak population modifier
//Large facility battles should be well-rewarded.
val populationModifier = FacilityHackParticipation.populationProgressModifier(
opposingPopulationByLayer,
{ pop =>
if (pop > 75) 0.9f
else if (pop > 59) 0.6f
else if (pop > 29) 0.55f
else if (pop > 25) 0.5f
else 0.45f
},
4
)
//3) competition multiplier
val competitionMultiplier: Float = {
val populationBalanceModifier: Float = FacilityHackParticipation.populationBalanceModifier(
victorPopulationByLayer,
opposingPopulationByLayer,
healthyPercentage = 1.5f
)
//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.flatMap { entry =>
entry.map { f => (f.DisplayLocation, Map.empty[PlanetSideEmpire.Value, Seq[Long]]) }
}
)
//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 heatMapModifier = FacilityHackParticipation.heatMapComparison( val contributionOpposingSize = contributionOpposing.size
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values, val killsByPlayersNotInTower = eliminateClosestTowerFromParticipating(
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values building,
FacilityHackParticipation.allocateKillsByPlayers(
building.Position,
building.Definition.SOIRadius.toFloat,
hackStart,
completionTime,
opposingFaction,
contributionOpposing
)
) )
heatMapModifier * populationBalanceModifier //1) experience from killing opposingFaction across duration of hack
} //The kills that occurred in the facility's attached field tower's sphere of influence have been eliminated from consideration.
//4) hack time modifier val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
//Captured major facilities without a lattice link unit and resecured major facilities with a lattice link unit killsByPlayersNotInTower,
// incur the full hack time if the module is not transported to a friendly facility contributionOpposingSize
//Captured major facilities with a lattice link unit and resecure major facilities without a lattice link uit )
// will incur an abbreviated duration val events = building.Zone.AvatarEvents
val overallTimeMultiplier: Float = { val buildingId = building.GUID.guid
if ( val zoneNumber = building.Zone.Number
building.Faction == PlanetSideEmpire.NEUTRAL || val playersInSoi = building.PlayersInSOI.filter {
building.NtuLevel == 0 || _.Faction == victorFaction
building.Generator.map { _.Condition }.contains(PlanetSideGeneratorState.Destroyed) }
) { //the facility ran out of nanites or power during the hack or became neutral if (baseExperienceFromFacilityCapture > 0) {
0f //2) population modifier
} else if (hasFlag) { //The value of the first should grow as population grows.
if (completionTime >= hackTime) { //hack timed out without llu delivery //This is an intentionally imperfect counterbalance to that growth.
0.25f val populationModifier = FacilityHackParticipation.populationProgressModifier(
} else if (isResecured) { opposingPopulationByLayer,
0.5f + (if (hackTime <= completionTime * 0.3f) { { pop =>
completionTime.toFloat / hackTime.toFloat if (pop > 75) 0.5f
} else if (hackTime >= completionTime * 0.6f) { else if (pop > 59) 0.6f
(hackTime - completionTime).toFloat / hackTime.toFloat else if (pop > 29) 0.7f
else if (pop > 19) 0.75f
else 0.8f
},
4
)
//3) competition multiplier
val competitionMultiplier: Float = {
val populationBalanceModifier: Float = FacilityHackParticipation.populationBalanceModifier(
victorPopulationByLayer,
opposingPopulationByLayer,
healthyPercentage = 1.5f,
maxRatio = 2.0f
)
//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.flatMap { entry =>
entry.map { f => (f.DisplayLocation, Map.empty[PlanetSideEmpire.Value, Seq[Long]]) }
}
)
//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 heatMapModifier = FacilityHackParticipation.heatMapComparison(
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values,
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values
)
heatMapModifier * populationBalanceModifier
}
//4) hack time modifier
//Captured major facilities without a lattice link unit and resecured major facilities with a lattice link unit
// incur the full hack time if the module is not transported to a friendly facility
//Captured major facilities with a lattice link unit and resecure major facilities without a lattice link unit
// will incur an abbreviated duration
val overallTimeMultiplier: Float = {
if (hasFlag) {
if (completionTime >= hackTime) { //hack timed out without llu delivery
0.5f
} else if (isResecured) {
0.5f + (if (hackTime <= completionTime * 0.3f) {
completionTime.toFloat / hackTime.toFloat
} else if (hackTime >= completionTime * 0.6f) {
(hackTime - completionTime).toFloat / hackTime.toFloat
} else {
0f
})
} else {
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
}
} else { } else {
0f if (isResecured) {
}) 0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
} else { } else {
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime) 0.5f
}
}
}
//5. individual contribution factors - by time
val contributionPerPlayerByTime = playerContribution.collect {
case (a, (_, d, t)) if d >= 600000 && math.abs(completionTime - t) < 5000 =>
(a, 0.65f)
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, 0.25f + (d.toFloat / 1800000f))
case (a, (_, _, _)) =>
(a, 0.25f)
}
//6. competition bonus
//This value will probably suck, and that's fine.
val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
contributionVictorSize,
contributionOpposingSize,
steamrollPercentage = 1.25f,
steamrollBonus = 5L,
overwhelmingOddsPercentage = 0.5f,
overwhelmingOddsBonus = 15L
)
//7. calculate overall command experience points
val finalCep: Long = math.ceil(
math.max(0L, baseExperienceFromFacilityCapture) *
populationModifier *
competitionMultiplier *
overallTimeMultiplier *
Config.app.game.experience.cep.rate + competitionBonus
).toLong
//8. reward participants
//Classically, only players in the SOI are rewarded, and the llu runner too
val hackerId = hacker.CharId
//terminal hacker (always cep)
if (playersInSoi.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) {
ToDatabase.reportFacilityCapture(
hackerId,
zoneNumber,
buildingId,
finalCep,
expType = "cep"
)
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep))
}
//bystanders (cep if squad leader, bep otherwise)
playersInSoi
.filterNot { _.CharId == hackerId }
.foreach { player =>
val charId = player.CharId
val contributionMultiplier = contributionPerPlayerByTime.getOrElse(charId, 1f)
val outputValue = (finalCep * contributionMultiplier).toLong
events ! AvatarServiceMessage(player.Name, AvatarAction.FacilityCaptureRewards(buildingId, zoneNumber, outputValue))
}
//flag carrier (won't be in soi, but earns cep from capture)
flagCarrier.collect {
case player if !isResecured =>
val charId: Long = player.CharId
val finalModifiedCep: Long = {
val durationPoints: Long = (hackTime - completionTime) / 1500L
val betterDurationPoints: Long = if (durationPoints >= 200L) {
durationPoints
} else {
200L + durationPoints
}
math.min(
betterDurationPoints,
(finalCep * Config.app.game.experience.cep.lluCarrierModifier).toLong
)
}
ToDatabase.reportFacilityCapture(
charId,
zoneNumber,
buildingId,
finalModifiedCep,
expType = "llu"
)
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(charId, finalModifiedCep))
} }
} else { } else {
if (isResecured) { //no need to calculate a fancy score
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime) val hackerId = hacker.CharId
} else { val hackerScore = List((hackerId, 0L, "cep"))
0.5f ToDatabase.reportFacilityCaptureInBulk(
} if (isResecured) {
} hackerScore
}
//5. individual contribution factors - by time
val contributionPerPlayerByTime = playerContribution.collect {
case (a, (_, d, t)) if d >= 600000 && math.abs(completionTime - t) < 5000 =>
(a, 0.45f)
case (a, (_, d, _)) if d >= 600000 =>
(a, 0.25f)
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, 0.25f * (0.5f + (d.toFloat / 600000f)))
case (a, (_, _, _)) =>
(a, 0.15f)
}
//6. competition bonus
//This value will probably suck, and that's fine.
val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
contributionVictorSize,
contributionOpposingSize,
steamrollPercentage = 1.25f,
steamrollBonus = 5L,
overwhelmingOddsPercentage = 0.5f,
overwhelmingOddsBonus = 15L
)
//7. calculate overall command experience points
val finalCep: Long = math.ceil(
math.max(0L, baseExperienceFromFacilityCapture) *
populationModifier *
competitionMultiplier *
overallTimeMultiplier *
Config.app.game.experience.cep.rate + competitionBonus
).toLong
//8. reward participants
//Classically, only players in the SOI are rewarded, and the llu runner too
val hackerId = hacker.CharId
val events = building.Zone.AvatarEvents
val playersInSoi = building.PlayersInSOI
if (playersInSoi.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) {
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep))
}
playersInSoi
.filter { player => player.Faction == victorFaction && player.CharId != hackerId }
.foreach { player =>
val charId = player.CharId
val contributionMultiplier = contributionPerPlayerByTime.getOrElse(charId, 1f)
val outputValue = (finalCep * contributionMultiplier).toLong
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, outputValue))
}
flagCarrier.collect {
case player if !isResecured =>
val charId: Long = player.CharId
val finalModifiedCep: Long = {
val durationPoints: Long = (hackTime - completionTime) / 1500L
val betterDurationPoints: Long = if (durationPoints >= 200L) {
durationPoints
} else { } else {
200L + durationPoints val flagCarrierScore = flagCarrier.map (p => List((p.CharId, 0L, "llu"))).getOrElse(Nil)
} if (playersInSoi.exists(_.CharId == hackerId) && !flagCarrierScore.exists { case (charId, _,_) => charId == hackerId }) {
math.min( hackerScore ++ flagCarrierScore
betterDurationPoints, } else {
(finalCep * Config.app.game.experience.cep.lluCarrierModifier).toLong flagCarrierScore
) }
} } ++ playersInSoi.filterNot { p => p.CharId == hackerId }.map(p => (p.CharId, 0L, "bep")),
ToDatabase.reportFacilityCapture( zoneNumber,
charId, buildingId
building.Zone.Number,
building.GUID.guid,
finalModifiedCep,
expType="llu"
) )
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(charId, finalModifiedCep)) }
} }
} }
} }

View file

@ -3,6 +3,7 @@ package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.zones.exp.ToDatabase
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideEmpire, Vector3} import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.util.Config import net.psforever.util.Config
@ -38,21 +39,15 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
} }
val contributionVictorSize = contributionVictor.size val contributionVictorSize = contributionVictor.size
if (contributionVictorSize > 0) { if (contributionVictorSize > 0) {
//setup for ... //early setup ...
import scala.concurrent.duration._ import scala.concurrent.duration._
val curr = System.currentTimeMillis() val curr = System.currentTimeMillis()
val soiPlayers = building.PlayersInSOI.filter { _.Faction == victorFaction }
val contributionOpposingSize = contributionOpposing.size val contributionOpposingSize = contributionOpposing.size
val populationIndices = playerPopulationOverTime.indices val events = building.Zone.AvatarEvents
val allFactions = PlanetSideEmpire.values.filterNot { val buildingId = building.GUID.guid
_ == PlanetSideEmpire.NEUTRAL val zoneNumber = building.Zone.Number
}.toSeq val hackerId = hacker.CharId
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))
}
val soiPlayers = building.PlayersInSOI
//1) experience from killing opposingFaction //1) experience from killing opposingFaction
//Because the hack duration of towers is instantaneous, the prior period of five minutes is artificially selected. //Because the hack duration of towers is instantaneous, the prior period of five minutes is artificially selected.
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills( val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
@ -66,93 +61,115 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
), ),
contributionOpposingSize contributionOpposingSize
) )
//2) peak population modifier //based on this math, the optimal number of enemy for experience gain is 20
//Towers should not be regarded as major battles. //max value of: 1000 * pop * max(0, (40 - pop)) * 0.1
//As the population rises, the rewards decrease (dramatically). if (baseExperienceFromFacilityCapture > 0) {
val populationModifier = FacilityHackParticipation.populationProgressModifier( //more setup ...
victorPopulationByLayer, val populationIndices = playerPopulationOverTime.indices
{ pop => val allFactions = PlanetSideEmpire.values.filterNot {
if (pop > 80) 0f _ == PlanetSideEmpire.NEUTRAL
else if (pop > 39) (80 - pop).toFloat * 0.01f }.toSeq
else if (pop > 25) 0.5f val (victorPopulationByLayer, opposingPopulationByLayer) = {
else if (pop > 19) 0.55f val individualPopulationByLayer = allFactions.map { f =>
else if (pop > 9) 0.6f (f, populationIndices.indices.map { i => playerPopulationOverTime(i)(f) })
else if (pop > 5) 0.75f }.toMap[PlanetSideEmpire.Value, Seq[Int]]
else 1f (individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction))
}, }
2 //2) peak population modifier
) //Towers should not be regarded as major battles.
//3) competition multiplier //As the population rises, the rewards decrease (dramatically).
val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier( val populationModifier = FacilityHackParticipation.populationProgressModifier(
victorPopulationByLayer, victorPopulationByLayer,
opposingPopulationByLayer, { pop =>
healthyPercentage = 1.25f if (pop > 40) 0.075f
) else if (pop > 8) (40 - pop).toFloat * 0.1f
//4a. individual contribution factors - by time else 1f
//Once again, an arbitrary five minute period. },
val contributionPerPlayerByTime = playerContribution.collect { 2
case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 => )
(a, 0.45f) //3) competition multiplier
case (a, (_, d, _)) if d >= 300000 => val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier(
(a, 0.25f) victorPopulationByLayer,
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 => opposingPopulationByLayer,
(a, 0.25f * (0.5f + (d.toFloat / 300000f))) healthyPercentage = 1.25f
case (a, (_, _, _)) => )
(a, 0.15f) //4a. individual contribution factors - by time
} //Once again, an arbitrary five minute period.
//4b. individual contribution factors - by distance to goal (secondary_capture) val contributionPerPlayerByTime = playerContribution.collect {
//Because the hack duration of towers is instantaneous, distance from terminal is a more important factor case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 =>
val contributionPerPlayerByDistanceFromGoal = { (a, 0.75f)
var minDistance: Float = Float.PositiveInfinity case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
val location = building (a, 0.15f + (d.toFloat / 600000f))
.CaptureTerminal case (a, (_, _, _)) =>
.map { terminal => terminal.Position } (a, 0.15f)
.getOrElse { hacker.Position } }
//4b. individual contribution factors - by distance to goal (secondary_capture)
//Because the hack duration of towers is instantaneous, distance from terminal is a more important factor
val contributionPerPlayerByDistanceFromGoal = {
var minDistance: Float = Float.PositiveInfinity
val location = building
.CaptureTerminal
.map { terminal => terminal.Position }
.getOrElse { hacker.Position }
soiPlayers
.map { p =>
val distance = Vector3.Distance(p.Position, location)
minDistance = math.min(minDistance, distance)
(p.CharId, distance)
}
.map { case (id, distance) =>
(id, math.max(0.25f, minDistance / distance))
}
}.toMap[Long, Float]
//5) token competition bonus
//This value will probably suck, and that's fine.
val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
contributionVictorSize,
contributionOpposingSize,
steamrollPercentage = 1.25f,
steamrollBonus = 2L,
overwhelmingOddsPercentage = 0.5f,
overwhelmingOddsBonus = 30L
)
//6. calculate overall command experience points
val finalCep: Long = math.ceil(
baseExperienceFromFacilityCapture *
populationModifier *
competitionMultiplier *
Config.app.game.experience.cep.rate + competitionBonus
).toLong
//7. reward participants
//Classically, only players in the SOI are rewarded
//terminal hacker (always cep)
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
ToDatabase.reportFacilityCapture(
hackerId,
zoneNumber,
buildingId,
finalCep,
expType = "cep"
)
//bystanders (cep if squad leader, bep otherwise)
soiPlayers soiPlayers
.map { p => .filterNot(_.CharId == hackerId)
val distance = Vector3.Distance(p.Position, location) .foreach { player =>
minDistance = math.min(minDistance, distance) val charId = player.CharId
(p.CharId, distance) val contributionTimeMultiplier = contributionPerPlayerByTime.getOrElse(charId, 0.5f)
val contributionDistanceMultiplier = contributionPerPlayerByDistanceFromGoal.getOrElse(charId, 0.5f)
val outputValue = (finalCep * contributionTimeMultiplier * contributionDistanceMultiplier).toLong
events ! AvatarServiceMessage(
player.Name,
AvatarAction.FacilityCaptureRewards(buildingId, zoneNumber, outputValue)
)
} }
.map { case (id, distance) => } else {
(id, math.max(0.15f, minDistance / distance)) //no need to calculate a fancy score
} ToDatabase.reportFacilityCaptureInBulk(
}.toMap[Long, Float] (hackerId, 0L, "cep") +: soiPlayers.filterNot(_.CharId == hackerId).map(p => (p.CharId, 0L, "bep")),
//5) token competition bonus zoneNumber,
//This value will probably suck, and that's fine. buildingId
val competitionBonus: Long = FacilityHackParticipation.competitionBonus( )
contributionVictorSize, }
contributionOpposingSize,
steamrollPercentage = 1.25f,
steamrollBonus = 2L,
overwhelmingOddsPercentage = 0.5f,
overwhelmingOddsBonus = 30L
)
//6. calculate overall command experience points
val finalCep: Long = math.ceil(
baseExperienceFromFacilityCapture *
populationModifier *
competitionMultiplier *
Config.app.game.experience.cep.rate + competitionBonus
).toLong
//7. reward participants
//Classically, only players in the SOI are rewarded
val events = building.Zone.AvatarEvents
soiPlayers
.filter { player =>
player.Faction == victorFaction && player.CharId != hacker.CharId
}
.foreach { player =>
val charId = player.CharId
val contributionTimeMultiplier = contributionPerPlayerByTime.getOrElse(charId, 0.5f)
val contributionDistanceMultiplier = contributionPerPlayerByDistanceFromGoal.getOrElse(charId, 0.5f)
val outputValue = (finalCep * contributionTimeMultiplier * contributionDistanceMultiplier).toLong
events ! AvatarServiceMessage(
player.Name,
AvatarAction.AwardCep(0, outputValue)
)
}
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
} }
playerContribution.clear() playerContribution.clear()

View file

@ -214,4 +214,19 @@ object ToDatabase {
) )
) )
} }
/**
* Insert multiple entries into the database's `buildingCapture` table as a single transaction.
*/
def reportFacilityCaptureInBulk(
avatarIdAndExp: List[(Long, Long, String)],
zoneId: Int,
buildingId: Int
): Unit = {
ctx.run(quote { liftQuery(
avatarIdAndExp.map { case (avatarId, exp, expType) =>
persistence.Buildingcapture(-1, avatarId, zoneId, buildingId, exp, expType)
}
)}.foreach(e => query[persistence.Buildingcapture].insertValue(e)))
}
} }