diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index f6026d8c..013ccd21 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -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 = { diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index 41588dcc..26276cbe 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -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 { diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala index 7d0569f8..88a8a82c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala @@ -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( diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala index 60910773..1cfaa2c7 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala @@ -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)) + } } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala index 2e2d459c..4f591680 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala @@ -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() diff --git a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala index fc9801c2..33162ea4 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala @@ -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))) + } }