augmented the calculations for bep

This commit is contained in:
Fate-JH 2023-12-28 01:20:16 -05:00
parent 2be0b875e0
commit 5b0ffdb5aa
9 changed files with 612 additions and 223 deletions

View file

@ -231,7 +231,7 @@ game {
# Battle experience points
# BEP is to be calculated in relation to how valuable a kill is worth.
bep = {
# After all calculations are complete, multiple the result by this value
# After all calculations are complete, multiply the result by this value
rate = 1.0
# These numbers are to determine the starting value for a particular kill
base = {
@ -240,12 +240,103 @@ game {
# If the player who died ever utilized a mechanized assault exo-suit
as-max = 250
# The player who died got at least one kill
with-kills = 100
with-kills = 150
# The player who died was mounted in a vehicle at the time of death
as-mounted = 100
# The player who died after having been in the game world for a while after spawning.
# Dying before this is often called a "spawn kill".
mature = 50
# How long it normally takes for a player who has respawned to naturally lose the status of "green" when being inactive.
# See `base.nature`.
maturity-time = 30000
}
life-span = {
# The experience value of a player's lifespan is measured in intervals.
# Per interval, after all calculations are complete, multiply the result by this value
life-span-threat-rate = 1.0
# The experience value of using certain equipment per interval of time during playtime (consider to be per second).
# (key, value) where key is technically the index of an ExoSuitType or an object class id and value is the growth
threat-assessment-of = [
{
id = 0
value = 1.0
},
{
id = 1
value = 1.25
},
{
id = 2
value = 2.0
},
{
id = 3
value = 1.0
},
{
id = 4
value = 0.75
},
{
id = 258
value = 0
},
{
id = 410
value = 0
},
{
id = 608
value = 0
}
]
# The maximum experience ceiling during playtime based on the use of certain equipment.
# (key, value) where key is technically the index of an ExoSuitType or an object class id and value is the maximum
max-threat-level = [
{
id = 0
level = 900
},
{
id = 1
level = 1200
},
{
id = 2
level = 5000
},
{
id = 3
level = 1000
},
{
id = 4
level = 20000
},
{
id = 258
level = 0
},
{
id = 410
level = 0
},
{
id = 608
level = 0
}
]
}
revenge = {
# If player A kills another player B who killed player A just previously, this is the percentage of experience to deposit.
# The kill event must have been the exact previous life's death after revive or respawn.
# This only applies if experience is set to 0.
# Set to zero and experience = 0 to ignore revenge.
rate = 0.15
# If player A kills another player B who killed player A just previously, deposit this experience.
# The kill event must have been the exact previous life's death after revive or respawn.
# Set to zero to reuse the experience value from the previous kill event.
experience = 0
}
}
@ -253,7 +344,7 @@ game {
# The events from which support experience rises are numerous.
# Calculation is determined by the selection of an "event" that decides how the values are combined.
sep = {
# After all calculations are complete, multiple the result by this value
# After all calculations are complete, multiply the result by this value
rate = 1.0
# When using an advanced nanite transport to deposit into the resource silo of a major facility,
# for reaching the maximum amount of a single deposit,
@ -376,6 +467,8 @@ game {
# When the cep has to be capped for squad size, calculate a small amount to add to the capped value
squad-size-limit-overflow-multiplier = 0.2
}
# When summing bep to produce facility capture base rewards, multiply the result by this value
facility-capture-rate = 0.5
}
# The game's official maximum battle rank is 40.

View file

@ -643,7 +643,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit
val toMaxArmor = player.MaxArmor
val toArmor = toMaxArmor
if (originalArmor != toMaxArmor) {
if (originalSuit != exosuit || originalArmor != toMaxArmor) {
player.LogActivity(RepairFromExoSuitChange(exosuit, toMaxArmor - originalArmor))
}
player.Armor = toMaxArmor

View file

@ -110,10 +110,10 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
)
//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(
val baseExperienceFromFacilityCapture: Long = (FacilityHackParticipation.calculateExperienceFromKills(
killsByPlayersNotInTower,
contributionOpposingSize
)
) * Config.app.game.experience.facilityCaptureRate).toLong
val events = building.Zone.AvatarEvents
val buildingId = building.GUID.guid
val zoneNumber = building.Zone.Number

View file

@ -50,7 +50,7 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
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(
val baseExperienceFromFacilityCapture: Long = (FacilityHackParticipation.calculateExperienceFromKills(
FacilityHackParticipation.allocateKillsByPlayers(
building.Position,
building.Definition.SOIRadius.toFloat,
@ -60,7 +60,7 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
contributionVictor
),
contributionOpposingSize
)
) * Config.app.game.experience.facilityCaptureRate).toLong
//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) {

View file

@ -50,13 +50,28 @@ trait SupportActivityCausedByAnother {
def amount: Int
}
trait ExoSuitChange {
def exosuit: ExoSuitType.Value
}
trait CommonExoSuitChange extends ExoSuitChange {
def src: SourceEntry
def exosuit: ExoSuitType.Value = {
src match {
case p: PlayerSource => p.ExoSuit
case _ => ExoSuitType.Standard
}
}
}
trait IncarnationActivity extends GeneralActivity
final case class SpawningActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
extends IncarnationActivity
extends IncarnationActivity with CommonExoSuitChange
final case class ReconstructionActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
extends IncarnationActivity
extends IncarnationActivity with CommonExoSuitChange
final case class RevivingActivity(target: SourceEntry, user: PlayerSource, amount: Int, equipment: EquipmentDefinition)
extends IncarnationActivity with SupportActivityCausedByAnother
@ -154,7 +169,7 @@ final case class HealFromImplant(implant: ImplantType, amount: Int)
extends HealingActivity
final case class RepairFromExoSuitChange(exosuit: ExoSuitType.Value, amount: Int)
extends RepairingActivity
extends RepairingActivity with ExoSuitChange
final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
extends RepairingActivity()

View file

@ -8,8 +8,8 @@ import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairingActivity, RevivingActivity, SpawningActivity}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire
import net.psforever.util.Config
import scala.annotation.tailrec
import scala.collection.mutable
import scala.concurrent.duration._
@ -150,154 +150,6 @@ object KillAssists {
}
}
/**
* "Menace" is a crude measurement of how much consistent destructive power a player has been demonstrating.
* Within the last ten kills, the rate of the player's killing speed is measured.
* The measurement - a "streak" in modern lingo - is transformed into the form of an `Integer` for simplicity.
* @param player the player
* @param mercy a time value that can be used to continue a missed streak
* @return an integer between 0 and 7;
* 0 is no kills,
* 1 is some kills,
* 2-7 is a menace score;
* there is no particular meaning behind different menace scores ascribed by this function
* but the range allows for progressive distinction
* @see `qualifiedTimeDifferences`
* @see `takeWhileLess`
*/
private[exp] def calculateMenace(player: PlayerSource, mercy: Long = 5000L): Int = {
val maxDelayDiff: Long = 45000L
val minDelayDiff: Long = 20000L
val allKills = player.progress.kills
//the very first kill must have been within the max delay (but does not count towards menace)
if (allKills.headOption.exists { System.currentTimeMillis() - _.time.toDate.getTime < maxDelayDiff}) {
allKills match {
case _ :: kills if kills.size > 3 =>
val (continuations, restsBetweenKills) =
qualifiedTimeDifferences(
kills.map(_.time.toDate.getTime).iterator,
maxValidDiffCount = 10,
maxDelayDiff,
minDelayDiff
)
.partition(_ > minDelayDiff)
math.max(
1,
math.floor(math.sqrt(
math.max(0, takeWhileLess(restsBetweenKills, testValue = 20000L, mercy).size - 1) + /*max=8*/
math.max(0, takeWhileLess(restsBetweenKills, testValue = 10000L, mercy).size - 5) * 3 + /*max=12*/
math.max(0, takeWhileLess(restsBetweenKills, testValue = 5000L, mercy = 1000L).size - 4) * 7 /*max=35*/
) - continuations.size)
).toInt
case _ =>
1
}
} else {
0
}
}
/**
* Take a list of times
* and produce a list of delays between those entries less than a maximum time delay.
* These are considered "qualifying".
* Count a certain number of time delays that fall within a minimum threshold
* and stop when that minimum count is achieved.
* These are considered "valid".
* The final product should be a new list of the successive delays from the first list
* containing both qualified and valid entries,
* stopping at either the first unqualified delay or the last valid delay or at exhaustion of the original list.
* @param iter unfiltered list of times (ms)
* @param maxValidDiffCount maximum number of valid entries in the final list of time differences;
* see `validTimeEntryCount`
* @param maxDiff exclusive amount of time allowed between qualifying entries;
* include any time difference within this delay;
* these entries are "qualifying" but are not "valid"
* @param minDiff inclusive amount of time difference allowed between valid entries;
* include time differences in this delay
* these entries are "valid" and should increment the counter `validTimeEntryCount`
* @return list of qualifying time differences (ms)
*/
/*
Parameters governed by recursion:
@param diffList ongoing list of qualifying time differences (ms)
@param diffExtensionList accumulation of entries greater than the `minTimeEntryDiff`
but less that the `minTimeEntryDiff`;
holds qualifying time differences
that will be included before the next valid time difference
@param validDiffCount currently number of valid time entries in the qualified time list;
see `maxValidTimeEntryCount`
@param previousTime previous qualifying entry time;
by default, current time (ms)
*/
@tailrec
private def qualifiedTimeDifferences(
iter: Iterator[Long],
maxValidDiffCount: Int,
maxDiff: Long,
minDiff: Long,
diffList: Seq[Long] = Nil,
diffExtensionList: Seq[Long] = Nil,
validDiffCount: Int = 0,
previousTime: Long = System.currentTimeMillis()
): Iterable[Long] = {
if (iter.hasNext && validDiffCount < maxValidDiffCount) {
val nextTime = iter.next()
val delay = previousTime - nextTime
if (delay < maxDiff) {
if (delay <= minDiff) {
qualifiedTimeDifferences(
iter,
maxValidDiffCount,
maxDiff,
minDiff,
diffList ++ (diffExtensionList :+ delay),
Nil,
validDiffCount + 1,
nextTime
)
} else {
qualifiedTimeDifferences(
iter,
maxValidDiffCount,
maxDiff,
minDiff,
diffList,
diffExtensionList :+ delay,
validDiffCount,
nextTime
)
}
} else {
diffList
}
} else {
diffList
}
}
/**
* From a list of values, isolate all values less than than a test value.
* @param list list of values
* @param testValue test value that all valid values must be less than
* @param mercy initial mercy value that values may be tested for being less than the test value
* @return list of values less than the test value, including mercy
*/
private def takeWhileLess(list: Iterable[Long], testValue: Long, mercy: Long): Iterable[Long] = {
var onGoingMercy: Long = mercy
list.filter { value =>
if (value < testValue) {
true
} else if (value - onGoingMercy < testValue) {
//mercy is reduced every time it is utilized to find a valid value
onGoingMercy = math.ceil(onGoingMercy * 0.8f).toLong
true
} else {
false
}
}
}
/**
* Modify a base experience value to consider additional reasons for points.
* @param killer player that delivers the interaction that reduces health to zero
@ -306,39 +158,50 @@ object KillAssists {
* @return the value of the kill in what the game called "battle experience points"
* @see `BattleRank.withExperience`
* @see `Support.baseExperience`
* @see `Support.calculateMenace`
*/
private def calculateExperience(
killer: PlayerSource,
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
//base value (the kill experience before modifiers)
lazy val base = Support.baseExperience(victim, history)
if (killer.Faction == victim.Faction || killer.unique == victim.unique) {
val killerUnique = killer.unique
if (killer.Faction == victim.Faction || killerUnique == victim.unique) {
0L
} else if (base > 1) {
//include battle rank disparity modifier
val battleRankDisparity: Long = {
import net.psforever.objects.avatar.BattleRank
val killerLevel = BattleRank.withExperience(killer.bep).value
val victimLevel = BattleRank.withExperience(victim.bep).value
val victimMinusKiller = victimLevel - killerLevel
if (victimMinusKiller > -1) {
victimMinusKiller * 10 + victimLevel
} else {
val bothLevels = killerLevel + victimLevel
val pointFive = (base.toFloat * 0.25f).toInt
-1 * (if (bothLevels >= base) {
pointFive
} else {
math.min(bothLevels, pointFive)
})
}
}.toLong
//include menace modifier
base + battleRankDisparity + (victim.progress.kills.size.toFloat * (1f + calculateMenace(victim).toFloat / 10f)).toLong
} else {
base
val base = Support.baseExperience(victim, history)
if (base > Support.TheShortestLifeIsWorth) {
val bep = Config.app.game.experience.bep
//battle rank disparity modifier
val battleRankDisparity: Long = {
import net.psforever.objects.avatar.BattleRank
val killerLevel = BattleRank.withExperience(killer.bep).value
val victimLevel = BattleRank.withExperience(victim.bep).value
val victimMinusKiller = victimLevel - killerLevel
if (victimMinusKiller > -1) {
victimMinusKiller * 5 + victimLevel // 5 x (y - x) + y -> min = 1 @ (1, 1), max = 235 @ (40, 1)
} else {
victimMinusKiller //min = -39, max = -1
}
}.toLong
//revenge modifier
val revengeBonus: Long = {
val revenge = bep.revenge
val victimUnique = victim.unique
val lastDeath = killer.progress.prior.flatMap(_.death)
val sameLastKiller = lastDeath.map(_.assailant.map(_.unique)).flatMap(_.headOption).contains(victimUnique)
if (revenge.experience != 0 && sameLastKiller) {
revenge.experience
} else if (sameLastKiller) {
(lastDeath.map(_.experienceEarned).get * revenge.rate).toLong
} else {
0L
}
}
base + battleRankDisparity + revengeBonus
} else {
base
}
}
}

View file

@ -107,7 +107,7 @@ object KillContributions {
* @see `CombinedHealthAndArmorContributionProcess`
* @see `composeContributionOutput`
* @see `initialScoring`
* @see `KillAssists.calculateMenace`
* @see `Support.calculateMenace`
* @see `limitHistoryToThisLife`
* @see `rewardTheseSupporters`
* @see `SupportActivity`
@ -140,7 +140,7 @@ object KillContributions {
val empty = mutable.ListBuffer[SourceUniqueness]()
empty.addOne(target.unique)
val otherContributionCalculations = additionalContributionSources(faction, kill, contributions)(_, _, _)
if (longHistory.nonEmpty && KillAssists.calculateMenace(target) > 3) {
if (longHistory.nonEmpty && Support.calculateMenace(target) > 3) {
//long and short history
val longContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
val shortContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Seq(longContributionProcess))

View file

@ -2,49 +2,444 @@
package net.psforever.objects.zones.exp
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.{InGameActivity, ReconstructionActivity, RepairFromExoSuitChange, SpawningActivity}
import net.psforever.objects.vital.{ExoSuitChange, InGameActivity, RevivingActivity, TerminalUsedActivity, VehicleDismountActivity, VehicleMountActivity, VehicleMountChange, VitalityDefinition}
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
import net.psforever.util.Config
import net.psforever.util.{Config, DefinitionUtil, ThreatAssessment, ThreatLevel}
import scala.annotation.tailrec
import scala.collection.mutable
/**
* Functions to assist experience calculation and history manipulation and analysis.
*/
object Support {
private val sep = Config.app.game.experience.sep
/** Almost nothing! */
final val TheShortestLifeIsWorth: Long = 1L
/**
* Calculate a base experience value to consider additional reasons for points.
* Calculate the experience value to reflect the value of a player's lifespan.
* @param victim player to which a final interaction has reduced health to zero
* @param history chronology of activity the game considers noteworthy
* @return the value of the kill in what the game called "battle experience points"
* @see `Support.wasEverAMax`
*/
private[exp] def baseExperience(
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
val lifespan = (history.headOption, history.lastOption) match {
//setup
val historyList = history.toList
val withKills = victim.progress.kills.nonEmpty
val fullLifespan = (historyList.headOption, historyList.lastOption) match {
case (Some(spawn), Some(death)) => death.time - spawn.time
case _ => 0L
}
val base = if (Support.wasEverAMax(victim, history)) {
Config.app.game.experience.bep.base.asMax
} else if (victim.progress.kills.nonEmpty) {
Config.app.game.experience.bep.base.withKills
} else if (victim.Seated) {
Config.app.game.experience.bep.base.asMounted
} else if (lifespan > 15000L) {
Config.app.game.experience.bep.base.mature
val recordOfWornTimes = countTimeWhileExoSuitOrMounted(historyList)
.map { case (id, time) => (id, (time * 0.001f).toLong) } // turn milliseconds into seconds
//short life factors
val shortLifeBonus = baseExperienceShortLifeFactors(
victim,
historyList,
recordOfWornTimes,
withKills,
fullLifespan
)
if (shortLifeBonus > TheShortestLifeIsWorth) {
val longLifeBonus: Long = {
val threat = baseExperienceLongLifeFactors(victim, recordOfWornTimes)
if (withKills) {
threat
} else {
(threat * 0.85f).toLong
}
}
//long life factors
shortLifeBonus + longLifeBonus
} else {
1L
//the shortest life is afforded no additional bonuses
shortLifeBonus
}
if (base > 1) {
//black ops modifier
base// * Config.app.game.experience.bep.base.bopsMultiplier
}
/**
* Assuming a chronological history of player actions and interactions,
* allocate every exo-suit use and mountable use to a time interval
* and accumulates the sum of those time intervals.
* The end result is a map association between exo-suits and vehicles and time that equipment has been used.
* @param history chronology of activity the game considers noteworthy
* @param initialExosuit start with this exo-suit type
* @return mapping between equipment (object class ids) and the time that equipment has been used (ms);
* the "equipment" includes exo-suits and all noted mountable entities
*/
private def countTimeWhileExoSuitOrMounted(
history: List[InGameActivity],
initialExosuit: ExoSuitType.Value = ExoSuitType.Standard
): Map[Int, Long] = {
val wornTime: mutable.HashMap[Int, Long] = mutable.HashMap[Int, Long]()
var currentSuit: Int = initialExosuit.id
var lastSuitAct: Long = history.head.time
var lastDismountAct: Option[VehicleMountChange] = None
var lastMountAct: Option[VehicleMountChange] = None
//collect history events that encompass changes to exo-suits and to mounting conditions
history.collect {
case suitChange: ExoSuitChange =>
//use previous vehicle dismount to distinguish between infantry exo-suit use and pilot exo-suit use
val timePassed = lastDismountAct.map(_.time).getOrElse(lastSuitAct) - suitChange.time
wornTime.get(currentSuit) match {
case None => wornTime.update(currentSuit, timePassed)
case Some(oldTime) => wornTime.update(currentSuit, oldTime + timePassed)
}
currentSuit = suitChange.exosuit.id
lastSuitAct = suitChange.time
lastDismountAct = None
case mount: VehicleMountActivity =>
wornTime.getOrElseUpdate(mount.vehicle.Definition.ObjectId, 0L)
lastDismountAct = None
lastMountAct = Some(mount)
case dismount: VehicleDismountActivity
if dismount.pairedEvent.isEmpty =>
//though we have reference to a previous mount activity, only care about the dismount activity's knowledge
wornTime.getOrElseUpdate(dismount.vehicle.Definition.ObjectId, 0L)
lastDismountAct = Some(dismount)
lastMountAct = None
case dismount: VehicleDismountActivity =>
val timePassed = dismount.time - dismount.pairedEvent.get.time
val input = dismount.vehicle.Definition.ObjectId
wornTime.get(input) match {
case None => wornTime.update(input, timePassed)
case Some(oldTime) => wornTime.update(input, oldTime + timePassed)
}
lastDismountAct = Some(dismount)
lastMountAct = None
}
//no more changes; add remaining time from unresolved activity
val lastTime = history.last.time
lastMountAct
.collect { mount =>
//dying in a vehicle is a reason to care about the last mount activity
val input = mount.vehicle.Definition.ObjectId
val lastMountTime = lastTime - mount.time
wornTime.get(input) match {
case None => wornTime.update(input, lastMountTime)
case Some(oldTime) => wornTime.update(input, oldTime + lastMountTime)
}
Some(mount)
}
.orElse {
//dying while on foot
val lastSuitTime = lastTime - lastDismountAct.map(_.time).getOrElse(lastSuitAct)
wornTime.get(currentSuit) match {
case None => wornTime.update(currentSuit, lastSuitTime)
case Some(oldTime) => wornTime.update(currentSuit, oldTime + lastSuitTime)
}
None
}
wornTime.toMap
}
/**
* Calculate the experience value to reflect the value of a player's short term lifespan.
* In effect, determine a token experience value for short unproductive lives.
* Four main conditions are outlined.
* In order of elimination traversal:
* was the player ever using a mechanized assault exo-suit,
* did the player kill anything,
* was the player mounted in a vehicle of turret for long enough for it to be considered,
* and has the player been alive long enough?
* @param player player to which a final interaction has reduced health to zero
* @param history chronology of activity the game considers noteworthy
* @param recordOfWornTimes between equipment (object class ids) and the time that equipment has been used (ms)
* @param withKills consider that the victim killed an opponent in this past life
* @param fullLifespan for how long this last life spanned
* @return the value of the kill in what the game called "battle experience points"
* @see `Config.app.game.experience.bep.base`
*/
private def baseExperienceShortLifeFactors(
player: PlayerSource,
history: List[InGameActivity],
recordOfWornTimes: Map[Int, Long],
withKills: Boolean,
fullLifespan: Long
): Long = {
val bep = Config.app.game.experience.bep.base
//TODO bops
if (recordOfWornTimes.getOrElse(ExoSuitType.MAX.id, 0L) > 0L) { //see: Support.wasEverAMax
bep.asMax
} else if (withKills) {
bep.withKills
} else if (player.Seated || {
val mountTime = recordOfWornTimes.collect { case (id, value) if id > 10 => value }.sum
mountTime * 3L >= fullLifespan
}) {
bep.asMounted
} else {
base
val validMaturityTime = if (
!history.head.isInstanceOf[RevivingActivity] ||
history.exists(_.isInstanceOf[TerminalUsedActivity])
) {
bep.maturityTime
} else {
0L
}
if (fullLifespan > validMaturityTime) {
bep.mature
} else {
TheShortestLifeIsWorth
}
}
}
/**
* Calculate the experience value to reflect the value of a player's full lifespan.
* A lifespan is associated with conditions and states that can each be assigned a weight or value.
* Summing up all of these conditions and states produces a reward value.
* @param player player to which a final interaction has reduced health to zero
* @param recordOfWornTimes between equipment (object class ids) and the time that equipment has been used (ms)
* @return the value of the kill in what the game called "battle experience points"
* @see `Config.app.game.experience.bep.lifeSpanThreatRate`
* @see `Config.app.game.experience.bep.threatAssessmentOf`
*/
private def baseExperienceLongLifeFactors(
player: PlayerSource,
recordOfWornTimes: Map[Int, Long]
): Long = {
val bep = Config.app.game.experience.bep
//awarded value for a target's lifespan based on the distribution of their tactical choices
val threatEstimate = (recordOfWornTimes.foldLeft(0L) {
case (sum, (key, amount)) =>
if (key > 10) sum + amount
else sum + (amount * bep.lifeSpan.threatAssessmentOf.find { case ThreatAssessment(a, _) => a == key }.map(_.value).getOrElse(1.0f)).toLong
} * bep.lifeSpan.lifeSpanThreatRate).toLong
//maximum award for a target's lifespan based on the greatest potential of their tactical choices
val maxThreatLevel : Long = estimateMaxThreatLevel(
recordOfWornTimes,
recordOfWornTimes.maxBy { case (_, value) => value }._1
)
//menace modifier -> min = kills, max = 8 x kills
val menace = (player.progress.kills.size.toFloat * (1f + Support.calculateMenace(player).toFloat)).toLong
//cap
math.min(threatEstimate + menace, maxThreatLevel)
}
/**
* Calculate the maximum possible reward available based on tactical options.
* @param recordOfWornTimes between equipment (object class ids) and the time that equipment has been used (ms)
* @param testKey equipment id used to estimate one sample for the ceiling value
* @param defaultThreatLevel what to use for an unresolved ceiling value;
* defaults to 0
* @return maximum value of the kill in what the game called "battle experience points"
*/
private def estimateMaxThreatLevel(
recordOfWornTimes: Map[Int, Long],
testKey: Int,
defaultThreatLevel: Long = 0L
): Long = {
val exoSuitMaxThreatId = recordOfWornTimes
.filter { case (key, _) => key < 10 }
.maxBy { case (_, value) => value }
._1
val estimatedExosuitThreatLevel = estimateMaxThreatLevelBasedOnKey(exoSuitMaxThreatId, defaultThreatLevel)
math.max(
estimateMaxThreatLevelBasedOnKey(testKey, estimatedExosuitThreatLevel),
estimatedExosuitThreatLevel
)
}
/**
* Calculate the maximum possible reward available based on a tactical option by id.
* @param refId equipment id used to collect the ceiling value
* @param defaultThreatLevel what to use for an unresolved ceiling value
* @return maximum value of the kill in what the game called "battle experience points"
* @see `Config.app.game.experience.bep.maxThreatLevel`
* @see `DefinitionUtil.idToDefinition`
* @see `VitalityDefinition.MaxHealth`
*/
private def estimateMaxThreatLevelBasedOnKey(
refId: Int,
defaultThreatLevel: Long
): Long = {
Config.app.game.experience.bep.lifeSpan.maxThreatLevel
.find { case ThreatLevel(key, _) => key == refId }
.map(_.level)
.getOrElse(
try {
DefinitionUtil.idToDefinition(refId) match {
case o: VitalityDefinition => (o.MaxHealth * 1.5f).toLong
case _ => defaultThreatLevel
}
} catch {
case _: Exception => defaultThreatLevel
}
)
}
/**
* "Menace" is a crude measurement of how much consistent destructive power a player has been demonstrating.
* Within the last few kills, the rate of the player's killing speed is measured.
* The measurement - a "streak" in modern lingo - is transformed into the form of an `Integer` for simplicity.
* @param player the player
* @param minimumKills number of kills needed before menace is considered
* @param testValues time values to determine allowable delay between kills to qualify for a score rating;
* three score ratings, so three values;
* defaults to 20s, 10s, 5s (in ms)
* @param maxDelayDiff time until the previous kill disqualifies menace;
* exclusive amount of time allowed between qualifying entries;
* default is 45s (in ms)
* @param minDelayDiff inclusive amount of time difference allowed between valid entries;
* default is 20s (in ms)
* @param mercy a time value that can be used to continue a missed streak;
* defaults to 5s (in ms)
* @return an integer between 0 and 7;
* 0 is no kills,
* 1 is some kills,
* 2-7 is a menace score;
* there is no particular meaning behind different menace scores ascribed by this function
* but the range allows for progressive distinction
* @see `qualifiedTimeDifferences`
* @see `takeWhileLess`
*/
private[exp] def calculateMenace(
player: PlayerSource,
minimumKills: Int = 3,
testValues: Seq[Long] = Seq(20000L, 10000L, 5000L),
maxDelayDiff: Long = 45000L,
minDelayDiff: Long = 20000L,
mercy: Long = 5000L
): Int = {
//init
val (minDiff, maxDiff) = (math.min(maxDelayDiff, maxDelayDiff), math.max(maxDelayDiff, maxDelayDiff))
val valuesForTesting = testValues.padTo(3, ((maxDiff + minDiff) * 0.5f).toLong)
//func
val allKills = player.progress.kills
//the very first kill must have been within the max delay (but does not count towards menace)
if (allKills.headOption.exists { System.currentTimeMillis() - _.time.toDate.getTime < maxDiff}) {
allKills match {
case _ :: kills if kills.size > minimumKills =>
val (continuations, restsBetweenKills) =
qualifiedTimeDifferences(
kills.map(_.time.toDate.getTime).iterator,
maxValidDiffCount = 10,
maxDiff,
minDiff
)
.partition(_ > minDiff)
math.max(
1,
math.floor(math.sqrt(
math.max(0, takeWhileLess(restsBetweenKills, valuesForTesting.head, mercy).size - 1) + /*max=8*/
math.max(0, takeWhileLess(restsBetweenKills, valuesForTesting(1), mercy).size - 5) * 3 + /*max=12*/
math.max(0, takeWhileLess(restsBetweenKills, valuesForTesting(2), mercy = 1000L).size - 4) * 7 /*max=35*/
) - continuations.size)
).toInt
case _ =>
1
}
} else {
0
}
}
/**
* Take a list of times
* and produce a list of delays between those entries less than a maximum time delay.
* These are considered "qualifying".
* Count a certain number of time delays that fall within a minimum threshold
* and stop when that minimum count is achieved.
* These are considered "valid".
* The final product should be a new list of the successive delays from the first list
* containing both qualified and valid entries,
* stopping at either the first unqualified delay or the last valid delay or at exhaustion of the original list.
* @param iter unfiltered list of times (ms)
* @param maxValidDiffCount maximum number of valid entries in the final list of time differences;
* see `validTimeEntryCount`
* @param maxDiff exclusive amount of time allowed between qualifying entries;
* include any time difference within this delay;
* these entries are "qualifying" but are not "valid"
* @param minDiff inclusive amount of time difference allowed between valid entries;
* include time differences in this delay
* these entries are "valid" and should increment the counter `validTimeEntryCount`
* @return list of qualifying time differences (ms)
*/
/*
Parameters governed by recursion:
@param diffList ongoing list of qualifying time differences (ms)
@param diffExtensionList accumulation of entries greater than the `minTimeEntryDiff`
but less that the `minTimeEntryDiff`;
holds qualifying time differences
that will be included before the next valid time difference
@param validDiffCount currently number of valid time entries in the qualified time list;
see `maxValidTimeEntryCount`
@param previousTime previous qualifying entry time;
by default, current time (ms)
*/
@tailrec
private def qualifiedTimeDifferences(
iter: Iterator[Long],
maxValidDiffCount: Int,
maxDiff: Long,
minDiff: Long,
diffList: Seq[Long] = Nil,
diffExtensionList: Seq[Long] = Nil,
validDiffCount: Int = 0,
previousTime: Long = System.currentTimeMillis()
): Iterable[Long] = {
if (iter.hasNext && validDiffCount < maxValidDiffCount) {
val nextTime = iter.next()
val delay = previousTime - nextTime
if (delay < maxDiff) {
if (delay <= minDiff) {
qualifiedTimeDifferences(
iter,
maxValidDiffCount,
maxDiff,
minDiff,
diffList ++ (diffExtensionList :+ delay),
Nil,
validDiffCount + 1,
nextTime
)
} else {
qualifiedTimeDifferences(
iter,
maxValidDiffCount,
maxDiff,
minDiff,
diffList,
diffExtensionList :+ delay,
validDiffCount,
nextTime
)
}
} else {
diffList
}
} else {
diffList
}
}
/**
* From a list of values, isolate all values less than than a test value.
* @param list list of values
* @param testValue test value that all valid values must be less than
* @param mercy initial mercy value that values may be tested against being less than the test value
* @return list of values less than the test value, including mercy
*/
private def takeWhileLess(
list: Iterable[Long],
testValue: Long,
mercy: Long
): Iterable[Long] = {
var onGoingMercy: Long = mercy
list.filter { value =>
if (value < testValue) {
true
} else if (value - onGoingMercy < testValue) {
//mercy is reduced every time it is utilized to find a valid value
onGoingMercy = math.ceil(onGoingMercy * 0.8f).toLong
true
} else {
false
}
}
}
@ -185,10 +580,8 @@ object Support {
*/
private[exp] def wasEverAMax(player: PlayerSource, history: Iterable[InGameActivity]): Boolean = {
player.ExoSuit == ExoSuitType.MAX || history.exists {
case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
case ReconstructionActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
case _ => false
case suitChange: ExoSuitChange => suitChange.exosuit == ExoSuitType.MAX
case _ => false
}
}
@ -211,7 +604,7 @@ object Support {
weaponStat: WeaponStats,
canNotFindEventDefaultValue: Option[Float] = None
): WeaponStats = {
val rewards: Float = sep.events
val rewards: Float = Config.app.game.experience.sep.events
.find(evt => event.equals(evt.name))
.map { event =>
val shots = weaponStat.shots
@ -228,7 +621,7 @@ object Support {
}
}
.getOrElse(
canNotFindEventDefaultValue.getOrElse(sep.canNotFindEventDefaultValue.toFloat)
canNotFindEventDefaultValue.getOrElse(Config.app.game.experience.sep.canNotFindEventDefaultValue.toFloat)
)
weaponStat.copy(contributions = rewards)
}

View file

@ -244,14 +244,27 @@ case class Experience(
longContributionTime: Long,
bep: BattleExperiencePoints,
sep: SupportExperiencePoints,
cep: CommandExperiencePoints
cep: CommandExperiencePoints,
facilityCaptureRate: Float
) {
assert(shortContributionTime < longContributionTime)
}
case class ThreatAssessment(
id: Int,
value: Float
)
case class ThreatLevel(
id: Int,
level: Long
)
case class BattleExperiencePoints(
base: BattleExperiencePointsBase,
rate: Float
rate: Float,
base: BattleExperiencePointsBase,
lifeSpan: BattleExperiencePointsLifespan,
revenge: BattleExperiencePointsRevenge
)
case class BattleExperiencePointsBase(
@ -259,7 +272,19 @@ case class BattleExperiencePointsBase(
asMax: Long,
withKills: Long,
asMounted: Long,
mature: Long
mature: Long,
maturityTime: Long
)
case class BattleExperiencePointsLifespan(
lifeSpanThreatRate: Float,
threatAssessmentOf: List[ThreatAssessment],
maxThreatLevel: List[ThreatLevel]
)
case class BattleExperiencePointsRevenge(
rate: Float,
experience: Long
)
case class SupportExperiencePoints(
@ -289,11 +314,11 @@ case class CommandExperiencePoints(
)
case class PromotionSystem(
active: Boolean,
broadcastBattleRank: Int,
resetBattleRank: Int,
maxBattleRank: Int,
battleExperiencePointsModifier: Float,
supportExperiencePointsModifier: Float,
captureExperiencePointsModifier: Float
active: Boolean,
broadcastBattleRank: Int,
resetBattleRank: Int,
maxBattleRank: Int,
battleExperiencePointsModifier: Float,
supportExperiencePointsModifier: Float,
captureExperiencePointsModifier: Float
)