mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-07-16 08:55:18 +00:00
augmented the calculations for bep
This commit is contained in:
parent
2be0b875e0
commit
5b0ffdb5aa
9 changed files with 612 additions and 223 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue