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

View file

@ -398,6 +398,7 @@ class SessionAvatarHandlers(
avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
case AvatarResponse.AwardBep(charId, bep, expType) =>
//if the target player, always award (some) BEP
if (charId == player.CharId) {
avatarActor ! AvatarActor.AwardBep(bep, expType)
}
@ -409,67 +410,7 @@ class SessionAvatarHandlers(
}
case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) =>
//must be in a squad to earn experience
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)
}
facilityCaptureRewards(buildingId, zoneNumber, cep)
case AvatarResponse.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 {

View file

@ -203,10 +203,10 @@ object FacilityHackParticipation {
}
}
}
if (dataSum != 0) {
math.max(0.15f, math.min(2f, dataSum / dataCount.toFloat))
if (dataCount != 0) {
math.max(0.2f, math.min(2f, dataSum / dataCount.toFloat))
} else {
1f
0.5f
}
}
@ -220,14 +220,15 @@ object FacilityHackParticipation {
private[participation] def populationProgressModifier(
populationNumbers: Seq[Int],
gradingRule: Int=>Float,
layers: Int
layers: Int,
ordering: Ordering[Int] = Ordering[Int]
): Float = {
val gradedPopulation = populationNumbers
.map { gradingRule }
.groupBy(x => x)
.values
.toSeq
.sortBy(_.size)
.sortBy(_.size)(ordering)
.take(layers)
.flatten
gradedPopulation.sum / gradedPopulation.size.toFloat
@ -236,7 +237,8 @@ object FacilityHackParticipation {
private[participation] def populationBalanceModifier(
victorPopulationNumbers: Seq[Int],
opposingPopulationNumbers: Seq[Int],
healthyPercentage: Float
healthyPercentage: Float,
maxRatio: Float = 1f
): Float = {
val rate = for {
victorPop <- victorPopulationNumbers
@ -252,7 +254,7 @@ object FacilityHackParticipation {
}
if true
} yield out
rate.sum / rate.size.toFloat
math.max(0f, math.min(rate.sum / rate.size.toFloat, maxRatio))
}
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.zones.{HotSpotInfo, ZoneHotSpotProjector}
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 akka.pattern.ask
import akka.util.Timeout
@ -45,7 +45,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
import net.psforever.objects.zones.ZoneHotSpotProjector
import scala.concurrent.Promise
import scala.util.Success
// import scala.util.Success
val requestLayers: Promise[ZoneHotSpotProjector.ExposedHeat] = Promise[ZoneHotSpotProjector.ExposedHeat]()
// val request = updateHotSpotInfoOnly()
// requestLayers.completeWith(request)
@ -67,204 +67,237 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
completionTime: Long,
isResecured: Boolean
): Unit = {
val curr = System.currentTimeMillis()
val hackStart = curr - completionTime
val socketOpt = building.GetFlagSocket
val (victorFaction, opposingFaction, hasFlag, flagCarrier) = if (!isResecured) {
val carrier = socketOpt.flatMap(_.previousFlag).flatMap(_.Carrier)
(attackingFaction, defenderFaction, socketOpt.nonEmpty, carrier)
} else {
(defenderFaction, attackingFaction, socketOpt.nonEmpty, 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) {
//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))
//has the facility ran out of nanites during the hack
if (building.NtuLevel > 0) {
val curr = System.currentTimeMillis()
val hackStart = curr - completionTime
val socketOpt = building.GetFlagSocket
val (victorFaction, opposingFaction, hasFlag, flagCarrier) = if (!isResecured) {
val carrier = socketOpt.flatMap(_.previousFlag).flatMap(_.Carrier)
(attackingFaction, defenderFaction, socketOpt.nonEmpty, carrier)
} else {
(defenderFaction, attackingFaction, socketOpt.nonEmpty, None)
}
val contributionOpposingSize = contributionOpposing.size
val killsByPlayersNotInTower = eliminateClosestTowerFromParticipating(
building,
FacilityHackParticipation.allocateKillsByPlayers(
building.Position,
building.Definition.SOIRadius.toFloat,
hackStart,
completionTime,
opposingFaction,
contributionOpposing
)
)
//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.
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
killsByPlayersNotInTower,
contributionOpposingSize
)
//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 (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) {
//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 heatMapModifier = FacilityHackParticipation.heatMapComparison(
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values,
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values
val contributionOpposingSize = contributionOpposing.size
val killsByPlayersNotInTower = eliminateClosestTowerFromParticipating(
building,
FacilityHackParticipation.allocateKillsByPlayers(
building.Position,
building.Definition.SOIRadius.toFloat,
hackStart,
completionTime,
opposingFaction,
contributionOpposing
)
)
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 uit
// will incur an abbreviated duration
val overallTimeMultiplier: Float = {
if (
building.Faction == PlanetSideEmpire.NEUTRAL ||
building.NtuLevel == 0 ||
building.Generator.map { _.Condition }.contains(PlanetSideGeneratorState.Destroyed)
) { //the facility ran out of nanites or power during the hack or became neutral
0f
} else if (hasFlag) {
if (completionTime >= hackTime) { //hack timed out without llu delivery
0.25f
} 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
//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.
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
killsByPlayersNotInTower,
contributionOpposingSize
)
val events = building.Zone.AvatarEvents
val buildingId = building.GUID.guid
val zoneNumber = building.Zone.Number
val playersInSoi = building.PlayersInSOI.filter {
_.Faction == victorFaction
}
if (baseExperienceFromFacilityCapture > 0) {
//2) population modifier
//The value of the first should grow as population grows.
//This is an intentionally imperfect counterbalance to that growth.
val populationModifier = FacilityHackParticipation.populationProgressModifier(
opposingPopulationByLayer,
{ pop =>
if (pop > 75) 0.5f
else if (pop > 59) 0.6f
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 {
0f
})
} else {
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
if (isResecured) {
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
} else {
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 {
if (isResecured) {
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
} else {
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.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
//no need to calculate a fancy score
val hackerId = hacker.CharId
val hackerScore = List((hackerId, 0L, "cep"))
ToDatabase.reportFacilityCaptureInBulk(
if (isResecured) {
hackerScore
} else {
200L + durationPoints
}
math.min(
betterDurationPoints,
(finalCep * Config.app.game.experience.cep.lluCarrierModifier).toLong
)
}
ToDatabase.reportFacilityCapture(
charId,
building.Zone.Number,
building.GUID.guid,
finalModifiedCep,
expType="llu"
val flagCarrierScore = flagCarrier.map (p => List((p.CharId, 0L, "llu"))).getOrElse(Nil)
if (playersInSoi.exists(_.CharId == hackerId) && !flagCarrierScore.exists { case (charId, _,_) => charId == hackerId }) {
hackerScore ++ flagCarrierScore
} else {
flagCarrierScore
}
} ++ playersInSoi.filterNot { p => p.CharId == hackerId }.map(p => (p.CharId, 0L, "bep")),
zoneNumber,
buildingId
)
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.sourcing.PlayerSource
import net.psforever.objects.zones.exp.ToDatabase
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.util.Config
@ -38,21 +39,15 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
}
val contributionVictorSize = contributionVictor.size
if (contributionVictorSize > 0) {
//setup for ...
//early setup ...
import scala.concurrent.duration._
val curr = System.currentTimeMillis()
val soiPlayers = building.PlayersInSOI.filter { _.Faction == victorFaction }
val contributionOpposingSize = contributionOpposing.size
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 soiPlayers = building.PlayersInSOI
val events = building.Zone.AvatarEvents
val buildingId = building.GUID.guid
val zoneNumber = building.Zone.Number
val hackerId = hacker.CharId
//1) experience from killing opposingFaction
//Because the hack duration of towers is instantaneous, the prior period of five minutes is artificially selected.
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
@ -66,93 +61,115 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
),
contributionOpposingSize
)
//2) peak population modifier
//Towers should not be regarded as major battles.
//As the population rises, the rewards decrease (dramatically).
val populationModifier = FacilityHackParticipation.populationProgressModifier(
victorPopulationByLayer,
{ pop =>
if (pop > 80) 0f
else if (pop > 39) (80 - pop).toFloat * 0.01f
else if (pop > 25) 0.5f
else if (pop > 19) 0.55f
else if (pop > 9) 0.6f
else if (pop > 5) 0.75f
else 1f
},
2
)
//3) competition multiplier
val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier(
victorPopulationByLayer,
opposingPopulationByLayer,
healthyPercentage = 1.25f
)
//4a. individual contribution factors - by time
//Once again, an arbitrary five minute period.
val contributionPerPlayerByTime = playerContribution.collect {
case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 =>
(a, 0.45f)
case (a, (_, d, _)) if d >= 300000 =>
(a, 0.25f)
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, 0.25f * (0.5f + (d.toFloat / 300000f)))
case (a, (_, _, _)) =>
(a, 0.15f)
}
//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 }
//based on this math, the optimal number of enemy for experience gain is 20
//max value of: 1000 * pop * max(0, (40 - pop)) * 0.1
if (baseExperienceFromFacilityCapture > 0) {
//more setup ...
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
//Towers should not be regarded as major battles.
//As the population rises, the rewards decrease (dramatically).
val populationModifier = FacilityHackParticipation.populationProgressModifier(
victorPopulationByLayer,
{ pop =>
if (pop > 40) 0.075f
else if (pop > 8) (40 - pop).toFloat * 0.1f
else 1f
},
2
)
//3) competition multiplier
val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier(
victorPopulationByLayer,
opposingPopulationByLayer,
healthyPercentage = 1.25f
)
//4a. individual contribution factors - by time
//Once again, an arbitrary five minute period.
val contributionPerPlayerByTime = playerContribution.collect {
case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 =>
(a, 0.75f)
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, 0.15f + (d.toFloat / 600000f))
case (a, (_, _, _)) =>
(a, 0.15f)
}
//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
.map { p =>
val distance = Vector3.Distance(p.Position, location)
minDistance = math.min(minDistance, distance)
(p.CharId, distance)
.filterNot(_.CharId == hackerId)
.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.FacilityCaptureRewards(buildingId, zoneNumber, outputValue)
)
}
.map { case (id, distance) =>
(id, math.max(0.15f, 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
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))
} else {
//no need to calculate a fancy score
ToDatabase.reportFacilityCaptureInBulk(
(hackerId, 0L, "cep") +: soiPlayers.filterNot(_.CharId == hackerId).map(p => (p.CharId, 0L, "bep")),
zoneNumber,
buildingId
)
}
}
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)))
}
}