mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-07-15 16:34:39 +00:00
Log-Related Fixes (2023-12-5) (#1149)
* tighened up the iterative processing aspects of kill assist calculations; wrong database query for assists; assumption of flag being acquired when it really wasn't; assumption of facility capture start when no longer represented * mainly, the spacing * augmented the calculations for bep * adjustment to calculations for the long life bonus experience and to the lifespan experience limits
This commit is contained in:
parent
ba7adee547
commit
85957670ba
12 changed files with 737 additions and 259 deletions
|
|
@ -231,7 +231,7 @@ game {
|
||||||
# Battle experience points
|
# Battle experience points
|
||||||
# BEP is to be calculated in relation to how valuable a kill is worth.
|
# BEP is to be calculated in relation to how valuable a kill is worth.
|
||||||
bep = {
|
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
|
rate = 1.0
|
||||||
# These numbers are to determine the starting value for a particular kill
|
# These numbers are to determine the starting value for a particular kill
|
||||||
base = {
|
base = {
|
||||||
|
|
@ -240,12 +240,103 @@ game {
|
||||||
# If the player who died ever utilized a mechanized assault exo-suit
|
# If the player who died ever utilized a mechanized assault exo-suit
|
||||||
as-max = 250
|
as-max = 250
|
||||||
# The player who died got at least one kill
|
# 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
|
# The player who died was mounted in a vehicle at the time of death
|
||||||
as-mounted = 100
|
as-mounted = 100
|
||||||
# The player who died after having been in the game world for a while after spawning.
|
# 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".
|
# Dying before this is often called a "spawn kill".
|
||||||
mature = 50
|
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.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = 1
|
||||||
|
value = 1.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = 2
|
||||||
|
value = 2.15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = 3
|
||||||
|
value = 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = 4
|
||||||
|
value = 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = 258
|
||||||
|
value = 10.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 = 2000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = 1
|
||||||
|
level = 2000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = 2
|
||||||
|
level = 5000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = 3
|
||||||
|
level = 2000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = 4
|
||||||
|
level = 900
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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.
|
# 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.
|
# Calculation is determined by the selection of an "event" that decides how the values are combined.
|
||||||
sep = {
|
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
|
rate = 1.0
|
||||||
# When using an advanced nanite transport to deposit into the resource silo of a major facility,
|
# 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,
|
# 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
|
# 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
|
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.
|
# 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
|
player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit
|
||||||
val toMaxArmor = player.MaxArmor
|
val toMaxArmor = player.MaxArmor
|
||||||
val toArmor = toMaxArmor
|
val toArmor = toMaxArmor
|
||||||
if (originalArmor != toMaxArmor) {
|
if (originalSuit != exosuit || originalArmor != toMaxArmor) {
|
||||||
player.LogActivity(RepairFromExoSuitChange(exosuit, toMaxArmor - originalArmor))
|
player.LogActivity(RepairFromExoSuitChange(exosuit, toMaxArmor - originalArmor))
|
||||||
}
|
}
|
||||||
player.Armor = toMaxArmor
|
player.Armor = toMaxArmor
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@ object ExoSuitDefinition {
|
||||||
case PlanetSideEmpire.TR => GlobalDefinitions.TRMAX.Use
|
case PlanetSideEmpire.TR => GlobalDefinitions.TRMAX.Use
|
||||||
case PlanetSideEmpire.NC => GlobalDefinitions.NCMAX.Use
|
case PlanetSideEmpire.NC => GlobalDefinitions.NCMAX.Use
|
||||||
case PlanetSideEmpire.VS => GlobalDefinitions.VSMAX.Use
|
case PlanetSideEmpire.VS => GlobalDefinitions.VSMAX.Use
|
||||||
|
case _ => GlobalDefinitions.Standard.Use
|
||||||
}
|
}
|
||||||
case _ => GlobalDefinitions.Standard.Use
|
case _ => GlobalDefinitions.Standard.Use
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,10 +110,10 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
|
||||||
)
|
)
|
||||||
//1) experience from killing opposingFaction across duration of hack
|
//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.
|
//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,
|
killsByPlayersNotInTower,
|
||||||
contributionOpposingSize
|
contributionOpposingSize
|
||||||
)
|
) * Config.app.game.experience.facilityCaptureRate).toLong
|
||||||
val events = building.Zone.AvatarEvents
|
val events = building.Zone.AvatarEvents
|
||||||
val buildingId = building.GUID.guid
|
val buildingId = building.GUID.guid
|
||||||
val zoneNumber = building.Zone.Number
|
val zoneNumber = building.Zone.Number
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
|
||||||
val hackerId = hacker.CharId
|
val hackerId = hacker.CharId
|
||||||
//1) experience from killing opposingFaction
|
//1) experience from killing opposingFaction
|
||||||
//Because the hack duration of towers is instantaneous, the prior period of five minutes is artificially selected.
|
//Because the hack duration of towers is instantaneous, the prior period of five minutes is artificially selected.
|
||||||
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
|
val baseExperienceFromFacilityCapture: Long = (FacilityHackParticipation.calculateExperienceFromKills(
|
||||||
FacilityHackParticipation.allocateKillsByPlayers(
|
FacilityHackParticipation.allocateKillsByPlayers(
|
||||||
building.Position,
|
building.Position,
|
||||||
building.Definition.SOIRadius.toFloat,
|
building.Definition.SOIRadius.toFloat,
|
||||||
|
|
@ -60,7 +60,7 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
|
||||||
contributionVictor
|
contributionVictor
|
||||||
),
|
),
|
||||||
contributionOpposingSize
|
contributionOpposingSize
|
||||||
)
|
) * Config.app.game.experience.facilityCaptureRate).toLong
|
||||||
//based on this math, the optimal number of enemy for experience gain is 20
|
//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
|
//max value of: 1000 * pop * max(0, (40 - pop)) * 0.1
|
||||||
if (baseExperienceFromFacilityCapture > 0) {
|
if (baseExperienceFromFacilityCapture > 0) {
|
||||||
|
|
|
||||||
|
|
@ -50,13 +50,28 @@ trait SupportActivityCausedByAnother {
|
||||||
def amount: Int
|
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
|
trait IncarnationActivity extends GeneralActivity
|
||||||
|
|
||||||
final case class SpawningActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
|
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])
|
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)
|
final case class RevivingActivity(target: SourceEntry, user: PlayerSource, amount: Int, equipment: EquipmentDefinition)
|
||||||
extends IncarnationActivity with SupportActivityCausedByAnother
|
extends IncarnationActivity with SupportActivityCausedByAnother
|
||||||
|
|
@ -154,7 +169,7 @@ final case class HealFromImplant(implant: ImplantType, amount: Int)
|
||||||
extends HealingActivity
|
extends HealingActivity
|
||||||
|
|
||||||
final case class RepairFromExoSuitChange(exosuit: ExoSuitType.Value, amount: Int)
|
final case class RepairFromExoSuitChange(exosuit: ExoSuitType.Value, amount: Int)
|
||||||
extends RepairingActivity
|
extends RepairingActivity with ExoSuitChange
|
||||||
|
|
||||||
final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
|
final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
|
||||||
extends RepairingActivity()
|
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.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairingActivity, RevivingActivity, SpawningActivity}
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
import net.psforever.types.PlanetSideEmpire
|
import net.psforever.types.PlanetSideEmpire
|
||||||
|
import net.psforever.util.Config
|
||||||
|
|
||||||
import scala.annotation.tailrec
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
|
|
@ -145,161 +145,11 @@ object KillAssists {
|
||||||
.orElse {
|
.orElse {
|
||||||
limitHistoryToThisLife(history)
|
limitHistoryToThisLife(history)
|
||||||
.lastOption
|
.lastOption
|
||||||
.collect { case dam: DamagingActivity =>
|
.collect { case dam: DamagingActivity if dam.data.adversarial.nonEmpty => dam.data }
|
||||||
val res = dam.data
|
.map { data => (data, data.adversarial.get.attacker) }
|
||||||
(res, res.adversarial.get.attacker)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* "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.
|
* Modify a base experience value to consider additional reasons for points.
|
||||||
* @param killer player that delivers the interaction that reduces health to zero
|
* @param killer player that delivers the interaction that reduces health to zero
|
||||||
|
|
@ -308,39 +158,50 @@ object KillAssists {
|
||||||
* @return the value of the kill in what the game called "battle experience points"
|
* @return the value of the kill in what the game called "battle experience points"
|
||||||
* @see `BattleRank.withExperience`
|
* @see `BattleRank.withExperience`
|
||||||
* @see `Support.baseExperience`
|
* @see `Support.baseExperience`
|
||||||
|
* @see `Support.calculateMenace`
|
||||||
*/
|
*/
|
||||||
private def calculateExperience(
|
private def calculateExperience(
|
||||||
killer: PlayerSource,
|
killer: PlayerSource,
|
||||||
victim: PlayerSource,
|
victim: PlayerSource,
|
||||||
history: Iterable[InGameActivity]
|
history: Iterable[InGameActivity]
|
||||||
): Long = {
|
): Long = {
|
||||||
//base value (the kill experience before modifiers)
|
val killerUnique = killer.unique
|
||||||
lazy val base = Support.baseExperience(victim, history)
|
if (killer.Faction == victim.Faction || killerUnique == victim.unique) {
|
||||||
if (killer.Faction == victim.Faction || killer.unique == victim.unique) {
|
|
||||||
0L
|
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 {
|
} 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -600,37 +461,49 @@ object KillAssists {
|
||||||
): Seq[(Long, Int)] = {
|
): Seq[(Long, Int)] = {
|
||||||
var amt = amount
|
var amt = amount
|
||||||
var count = 0
|
var count = 0
|
||||||
var newOrder: Seq[(Long, Int)] = Nil
|
var newOrderPos: Seq[(Long, Int)] = Nil
|
||||||
|
var newOrderNeg: Seq[(Long, Int)] = Nil
|
||||||
order.takeWhile { entry =>
|
order.takeWhile { entry =>
|
||||||
val (id, total) = entry
|
val (id, total) = entry
|
||||||
if (id > 0 && total > 0) {
|
if (id > 0 && total > 0) {
|
||||||
val part = participants(id)
|
participants.get(id) match {
|
||||||
if (amount > total) {
|
case Some(part) =>
|
||||||
//drop this entry
|
val reduceByValue = math.min(amt, total)
|
||||||
participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case
|
val trimmedWeapons = {
|
||||||
amt = amt - total
|
var index = 0
|
||||||
} else {
|
var weaponSum = 0
|
||||||
//edit around the inclusion of this entry
|
val pweapons = part.weapons
|
||||||
val newTotal = total - amt
|
val pwepiter = pweapons.iterator
|
||||||
val trimmedWeapons = {
|
while (pwepiter.hasNext && weaponSum < reduceByValue) {
|
||||||
var index = -1
|
weaponSum = weaponSum + pwepiter.next().amount
|
||||||
var weaponSum = 0
|
index += 1
|
||||||
val pweapons = part.weapons
|
}
|
||||||
while (weaponSum < amt) {
|
//output list(s)
|
||||||
index += 1
|
(if (weaponSum == reduceByValue) {
|
||||||
weaponSum = weaponSum + pweapons(index).amount
|
newOrderNeg = newOrderNeg :+ (id, 0)
|
||||||
|
index += 1
|
||||||
|
pweapons.drop(index)
|
||||||
|
} else if (weaponSum > reduceByValue) {
|
||||||
|
newOrderPos = (id, total - amt) +: newOrderPos
|
||||||
|
val remainder = pweapons.drop(index)
|
||||||
|
remainder.headOption.map(_.copy(amount = weaponSum - reduceByValue)) ++ remainder.tail
|
||||||
|
} else {
|
||||||
|
newOrderPos = (id, total - amt) +: newOrderPos
|
||||||
|
val remainder = pweapons.drop(index)
|
||||||
|
remainder.headOption.map(_.copy(amount = reduceByValue - weaponSum)) ++ remainder.tail
|
||||||
|
}) ++ pweapons.take(index).map(_.copy(amount = 0))
|
||||||
}
|
}
|
||||||
(pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size)) ++
|
participants.put(id, part.copy(amount = part.amount - reduceByValue, weapons = trimmedWeapons.toSeq))
|
||||||
pweapons.slice(0, index).map(_.copy(amount = 0))
|
amt = amt - reduceByValue
|
||||||
}
|
case _ =>
|
||||||
newOrder = (id, newTotal) +: newOrder
|
//we do not have contribution stat data for this id
|
||||||
participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
|
//perform no calculations; devalue the entry
|
||||||
amt = 0
|
newOrderNeg = newOrderNeg :+ (id, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
count += 1
|
count += 1
|
||||||
amt > 0
|
amt > 0
|
||||||
}
|
}
|
||||||
newOrder ++ order.drop(count)
|
newOrderPos ++ order.drop(count) ++ newOrderNeg//.reverse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ object KillContributions {
|
||||||
* @see `CombinedHealthAndArmorContributionProcess`
|
* @see `CombinedHealthAndArmorContributionProcess`
|
||||||
* @see `composeContributionOutput`
|
* @see `composeContributionOutput`
|
||||||
* @see `initialScoring`
|
* @see `initialScoring`
|
||||||
* @see `KillAssists.calculateMenace`
|
* @see `Support.calculateMenace`
|
||||||
* @see `limitHistoryToThisLife`
|
* @see `limitHistoryToThisLife`
|
||||||
* @see `rewardTheseSupporters`
|
* @see `rewardTheseSupporters`
|
||||||
* @see `SupportActivity`
|
* @see `SupportActivity`
|
||||||
|
|
@ -140,7 +140,7 @@ object KillContributions {
|
||||||
val empty = mutable.ListBuffer[SourceUniqueness]()
|
val empty = mutable.ListBuffer[SourceUniqueness]()
|
||||||
empty.addOne(target.unique)
|
empty.addOne(target.unique)
|
||||||
val otherContributionCalculations = additionalContributionSources(faction, kill, contributions)(_, _, _)
|
val otherContributionCalculations = additionalContributionSources(faction, kill, contributions)(_, _, _)
|
||||||
if (longHistory.nonEmpty && KillAssists.calculateMenace(target) > 3) {
|
if (longHistory.nonEmpty && Support.calculateMenace(target) > 3) {
|
||||||
//long and short history
|
//long and short history
|
||||||
val longContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
|
val longContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
|
||||||
val shortContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Seq(longContributionProcess))
|
val shortContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Seq(longContributionProcess))
|
||||||
|
|
|
||||||
|
|
@ -2,49 +2,510 @@
|
||||||
package net.psforever.objects.zones.exp
|
package net.psforever.objects.zones.exp
|
||||||
|
|
||||||
import net.psforever.objects.sourcing.PlayerSource
|
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.types.{ExoSuitType, PlanetSideEmpire}
|
||||||
import net.psforever.util.Config
|
import net.psforever.util.{Config, DefinitionUtil, ThreatAssessment, ThreatLevel}
|
||||||
|
|
||||||
|
import scala.annotation.tailrec
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functions to assist experience calculation and history manipulation and analysis.
|
* Functions to assist experience calculation and history manipulation and analysis.
|
||||||
*/
|
*/
|
||||||
object Support {
|
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 victim player to which a final interaction has reduced health to zero
|
||||||
* @param history chronology of activity the game considers noteworthy
|
* @param history chronology of activity the game considers noteworthy
|
||||||
* @return the value of the kill in what the game called "battle experience points"
|
* @return the value of the kill in what the game called "battle experience points"
|
||||||
* @see `Support.wasEverAMax`
|
|
||||||
*/
|
*/
|
||||||
private[exp] def baseExperience(
|
private[exp] def baseExperience(
|
||||||
victim: PlayerSource,
|
victim: PlayerSource,
|
||||||
history: Iterable[InGameActivity]
|
history: Iterable[InGameActivity]
|
||||||
): Long = {
|
): 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 (Some(spawn), Some(death)) => death.time - spawn.time
|
||||||
case _ => 0L
|
case _ => 0L
|
||||||
}
|
}
|
||||||
val base = if (Support.wasEverAMax(victim, history)) {
|
val recordOfWornTimes = countTimeWhileExoSuitOrMounted(historyList)
|
||||||
Config.app.game.experience.bep.base.asMax
|
.map { case (id, time) => (id, (time * 0.001f).toLong) } // turn milliseconds into seconds
|
||||||
} else if (victim.progress.kills.nonEmpty) {
|
//short life factors
|
||||||
Config.app.game.experience.bep.base.withKills
|
val shortLifeBonus = baseExperienceShortLifeFactors(
|
||||||
} else if (victim.Seated) {
|
victim,
|
||||||
Config.app.game.experience.bep.base.asMounted
|
historyList,
|
||||||
} else if (lifespan > 15000L) {
|
recordOfWornTimes,
|
||||||
Config.app.game.experience.bep.base.mature
|
withKills,
|
||||||
|
fullLifespan
|
||||||
|
)
|
||||||
|
if (shortLifeBonus > TheShortestLifeIsWorth) {
|
||||||
|
val longLifeBonus: Long = {
|
||||||
|
val threat = baseExperienceLongLifeFactors(victim, recordOfWornTimes, defaultValue = 100f * shortLifeBonus.toFloat)
|
||||||
|
if (withKills) {
|
||||||
|
threat
|
||||||
|
} else {
|
||||||
|
(threat * 0.85f).toLong
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//long life factors
|
||||||
|
shortLifeBonus + longLifeBonus
|
||||||
} else {
|
} 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 lastActTime: Long = history.head.time
|
||||||
|
var lastMountAct: Option[VehicleMountChange] = None
|
||||||
|
//collect history events that encompass changes to exo-suits and to mounting conditions
|
||||||
|
history.collect {
|
||||||
|
case suitChange: ExoSuitChange =>
|
||||||
|
updateEquippedEntry(
|
||||||
|
currentSuit,
|
||||||
|
suitChange.time - lastActTime,
|
||||||
|
wornTime
|
||||||
|
)
|
||||||
|
currentSuit = suitChange.exosuit.id
|
||||||
|
lastActTime = suitChange.time
|
||||||
|
case mount: VehicleMountActivity =>
|
||||||
|
updateEquippedEntry(
|
||||||
|
currentSuit,
|
||||||
|
mount.time - lastActTime,
|
||||||
|
wornTime
|
||||||
|
)
|
||||||
|
lastActTime = mount.time
|
||||||
|
lastMountAct = Some(mount)
|
||||||
|
case dismount: VehicleDismountActivity
|
||||||
|
if dismount.pairedEvent.isEmpty =>
|
||||||
|
updateEquippedEntry(
|
||||||
|
dismount.vehicle.Definition.ObjectId,
|
||||||
|
dismount.time - lastActTime,
|
||||||
|
wornTime
|
||||||
|
)
|
||||||
|
lastActTime = dismount.time
|
||||||
|
lastMountAct = None
|
||||||
|
case dismount: VehicleDismountActivity =>
|
||||||
|
updateEquippedEntry(
|
||||||
|
dismount.vehicle.Definition.ObjectId,
|
||||||
|
dismount.time - dismount.pairedEvent.get.time,
|
||||||
|
wornTime
|
||||||
|
)
|
||||||
|
lastActTime = dismount.time
|
||||||
|
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
|
||||||
|
updateEquippedEntry(
|
||||||
|
mount.vehicle.Definition.ObjectId,
|
||||||
|
lastTime - mount.time,
|
||||||
|
wornTime
|
||||||
|
)
|
||||||
|
Some(mount)
|
||||||
|
}
|
||||||
|
.orElse {
|
||||||
|
//dying while on foot
|
||||||
|
updateEquippedEntry(
|
||||||
|
currentSuit,
|
||||||
|
lastTime - lastActTime,
|
||||||
|
wornTime
|
||||||
|
)
|
||||||
|
None
|
||||||
|
}
|
||||||
|
wornTime.toMap
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ...
|
||||||
|
* @param equipmentId the equipment
|
||||||
|
* @param timePassed how long it was in use
|
||||||
|
* @param wornTime mapping between equipment (object class ids) and the time that equipment has been used (ms)
|
||||||
|
* @return the length of time the equipment was used
|
||||||
|
*/
|
||||||
|
private def updateEquippedEntry(
|
||||||
|
equipmentId: Int,
|
||||||
|
timePassed: Long,
|
||||||
|
wornTime: mutable.HashMap[Int, Long]
|
||||||
|
): Long = {
|
||||||
|
wornTime
|
||||||
|
.get(equipmentId)
|
||||||
|
.collect {
|
||||||
|
oldTime =>
|
||||||
|
val time = oldTime + timePassed
|
||||||
|
wornTime.update(equipmentId, time)
|
||||||
|
time
|
||||||
|
}
|
||||||
|
.orElse {
|
||||||
|
wornTime.update(equipmentId, timePassed)
|
||||||
|
Some(timePassed)
|
||||||
|
}
|
||||||
|
.get
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
} 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],
|
||||||
|
defaultValue: Float
|
||||||
|
): Long = {
|
||||||
|
//awarded values for a target's lifespan based on the distribution of their tactical choices
|
||||||
|
val individualThreatEstimates: Map[Int, Float] = calculateThreatEstimatesPerEntry(recordOfWornTimes)
|
||||||
|
val totalThreatEstimate: Float = individualThreatEstimates.values.sum
|
||||||
|
val maxThreatCapacity: Float = {
|
||||||
|
val (exosuitTimes, otherTimes) = recordOfWornTimes.partition(_._1 < 10)
|
||||||
|
calculateMaxThreatCapacityPerEntry(
|
||||||
|
(if (exosuitTimes.values.sum > otherTimes.values.sum) {
|
||||||
|
individualThreatEstimates.filter(_._1 < 10)
|
||||||
|
} else {
|
||||||
|
individualThreatEstimates.filter(_._1 > 10)
|
||||||
|
}).maxBy(_._2)._1,
|
||||||
|
defaultValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//menace modifier -> min = kills, max = 8 x kills
|
||||||
|
val menace = (player.progress.kills.size.toFloat * (1f + Support.calculateMenace(player).toFloat)).toLong
|
||||||
|
//last kill experience
|
||||||
|
val lastKillExperience = player.progress.kills
|
||||||
|
.lastOption
|
||||||
|
.collect { kill =>
|
||||||
|
val reduce = ((System.currentTimeMillis() - kill.time.toDate.getTime).toFloat * 0.001f).toLong
|
||||||
|
math.max(0L, kill.experienceEarned - reduce)
|
||||||
|
}
|
||||||
|
.getOrElse(0L)
|
||||||
|
//cap lifespan then add extra
|
||||||
|
math.min(totalThreatEstimate, maxThreatCapacity).toLong + menace + lastKillExperience
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the reward available based on a tactical option by id.
|
||||||
|
* @param recordOfWornTimes between equipment (object class ids) and the time that equipment has been used (ms)
|
||||||
|
* @return value of the equipment
|
||||||
|
*/
|
||||||
|
private def calculateThreatEstimatesPerEntry(recordOfWornTimes: Map[Int, Long]): Map[Int, Float] = {
|
||||||
|
recordOfWornTimes.map {
|
||||||
|
case (key, amount) => (key, amount * calculateThreatEstimatesPerEntry(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the reward available based on a tactical option by id.
|
||||||
|
* If not listed in a previous table of values,
|
||||||
|
* obtain the definition associated with the equipment id and test use the mass of the entity.
|
||||||
|
* The default value is 0.
|
||||||
|
* @param key equipment id used to collect the ceiling value
|
||||||
|
* @return value of the equipment
|
||||||
|
* @see `Config.app.game.experience.bep.threatAssessmentOf`
|
||||||
|
* @see `VitalityDefinition.mass`
|
||||||
|
*/
|
||||||
|
private def calculateThreatEstimatesPerEntry(key: Int): Float = {
|
||||||
|
Config.app.game.experience.bep.lifeSpan.threatAssessmentOf
|
||||||
|
.find { case ThreatAssessment(a, _) => a == key }
|
||||||
|
.map(_.value)
|
||||||
|
.getOrElse {
|
||||||
|
getDefinitionById(key)
|
||||||
|
.map(o => 2f + math.log10(o.mass.toDouble).toFloat)
|
||||||
|
.getOrElse(0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the maximum possible reward available based on tactical options.
|
||||||
|
* If not listed in a previous table of values,
|
||||||
|
* obtain the definition associated with the equipment id and test use the maximum health of the entity.
|
||||||
|
* @param key equipment id used to estimate one sample for the ceiling value
|
||||||
|
* @param defaultValue what to use for an unresolved ceiling value;
|
||||||
|
* defaults to 0
|
||||||
|
* @return maximum value for this equipment
|
||||||
|
* @see `Config.app.game.experience.bep.maxThreatLevel`
|
||||||
|
* @see `VitalityDefinition.MaxHealth`
|
||||||
|
*/
|
||||||
|
private def calculateMaxThreatCapacityPerEntry(
|
||||||
|
key: Int,
|
||||||
|
defaultValue: Float
|
||||||
|
): Float = {
|
||||||
|
Config.app.game.experience.bep.lifeSpan.maxThreatLevel
|
||||||
|
.find { case ThreatLevel(a, _) => a == key }
|
||||||
|
.map(_.level.toFloat)
|
||||||
|
.getOrElse {
|
||||||
|
getDefinitionById(key)
|
||||||
|
.map(_.MaxHealth.toFloat * 1.2f)
|
||||||
|
.getOrElse(defaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ...
|
||||||
|
* @param key equipment id
|
||||||
|
* @return the definition if the definition can be found;
|
||||||
|
* `None`, otherwise
|
||||||
|
* @see `DefinitionUtil.idToDefinition`
|
||||||
|
* @see `GlobalDefinitions`
|
||||||
|
* @see `VitalityDefinition`
|
||||||
|
*/
|
||||||
|
private def getDefinitionById(key: Int): Option[VitalityDefinition] = {
|
||||||
|
try {
|
||||||
|
DefinitionUtil.idToDefinition(key) match {
|
||||||
|
case o: VitalityDefinition => Some(o)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case _: Exception => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "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 +646,8 @@ object Support {
|
||||||
*/
|
*/
|
||||||
private[exp] def wasEverAMax(player: PlayerSource, history: Iterable[InGameActivity]): Boolean = {
|
private[exp] def wasEverAMax(player: PlayerSource, history: Iterable[InGameActivity]): Boolean = {
|
||||||
player.ExoSuit == ExoSuitType.MAX || history.exists {
|
player.ExoSuit == ExoSuitType.MAX || history.exists {
|
||||||
case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
|
case suitChange: ExoSuitChange => suitChange.exosuit == ExoSuitType.MAX
|
||||||
case ReconstructionActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
|
case _ => false
|
||||||
case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
|
|
||||||
case _ => false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +670,7 @@ object Support {
|
||||||
weaponStat: WeaponStats,
|
weaponStat: WeaponStats,
|
||||||
canNotFindEventDefaultValue: Option[Float] = None
|
canNotFindEventDefaultValue: Option[Float] = None
|
||||||
): WeaponStats = {
|
): WeaponStats = {
|
||||||
val rewards: Float = sep.events
|
val rewards: Float = Config.app.game.experience.sep.events
|
||||||
.find(evt => event.equals(evt.name))
|
.find(evt => event.equals(evt.name))
|
||||||
.map { event =>
|
.map { event =>
|
||||||
val shots = weaponStat.shots
|
val shots = weaponStat.shots
|
||||||
|
|
@ -228,7 +687,7 @@ object Support {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.getOrElse(
|
.getOrElse(
|
||||||
canNotFindEventDefaultValue.getOrElse(sep.canNotFindEventDefaultValue.toFloat)
|
canNotFindEventDefaultValue.getOrElse(Config.app.game.experience.sep.canNotFindEventDefaultValue.toFloat)
|
||||||
)
|
)
|
||||||
weaponStat.copy(contributions = rewards)
|
weaponStat.copy(contributions = rewards)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ object ToDatabase {
|
||||||
position: Vector3,
|
position: Vector3,
|
||||||
exp: Long
|
exp: Long
|
||||||
): Unit = {
|
): Unit = {
|
||||||
ctx.run(query[persistence.Killactivity]
|
ctx.run(query[persistence.Assistactivity]
|
||||||
.insert(
|
.insert(
|
||||||
_.killerId -> lift(avatarId),
|
_.killerId -> lift(avatarId),
|
||||||
_.victimId -> lift(victimId),
|
_.victimId -> lift(victimId),
|
||||||
|
|
@ -227,6 +227,12 @@ object ToDatabase {
|
||||||
avatarIdAndExp.map { case (avatarId, exp, expType) =>
|
avatarIdAndExp.map { case (avatarId, exp, expType) =>
|
||||||
persistence.Buildingcapture(-1, avatarId, zoneId, buildingId, exp, expType)
|
persistence.Buildingcapture(-1, avatarId, zoneId, buildingId, exp, expType)
|
||||||
}
|
}
|
||||||
)}.foreach(e => query[persistence.Buildingcapture].insertValue(e)))
|
)}.foreach(e => query[persistence.Buildingcapture].insert(
|
||||||
|
_.avatarId -> e.avatarId,
|
||||||
|
_.zoneId -> e.zoneId,
|
||||||
|
_.buildingId -> e.buildingId,
|
||||||
|
_.exp -> e.exp,
|
||||||
|
_.expType -> e.expType
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,10 @@ class HackCaptureActor extends Actor {
|
||||||
val faction = GetHackingFaction(target).getOrElse(target.Faction)
|
val faction = GetHackingFaction(target).getOrElse(target.Faction)
|
||||||
target.HackedBy = None
|
target.HackedBy = None
|
||||||
hackedObjects = remainder
|
hackedObjects = remainder
|
||||||
|
val now: Long = System.currentTimeMillis()
|
||||||
|
val facilityHackTime: Long = target.Definition.FacilityHackTime.toMillis
|
||||||
val building = target.Owner.asInstanceOf[Building]
|
val building = target.Owner.asInstanceOf[Building]
|
||||||
|
val hackTime = results.headOption.map { now - _.hack_timestamp }.getOrElse(facilityHackTime)
|
||||||
// If LLU exists it was not delivered. Send resecured notifications
|
// If LLU exists it was not delivered. Send resecured notifications
|
||||||
building.GetFlag.collect {
|
building.GetFlag.collect {
|
||||||
case flag: CaptureFlag => target.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.Resecured)
|
case flag: CaptureFlag => target.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.Resecured)
|
||||||
|
|
@ -99,15 +102,15 @@ class HackCaptureActor extends Actor {
|
||||||
target.Faction,
|
target.Faction,
|
||||||
faction,
|
faction,
|
||||||
hacker,
|
hacker,
|
||||||
target.Definition.FacilityHackTime.toMillis,
|
facilityHackTime,
|
||||||
System.currentTimeMillis() - results.head.hack_timestamp,
|
hackTime,
|
||||||
isResecured = true
|
isResecured = true
|
||||||
)
|
)
|
||||||
// Restart the timer in case the object we just removed was the next one scheduled
|
// Restart the timer in case the object we just removed was the next one scheduled
|
||||||
RestartTimer()
|
RestartTimer()
|
||||||
|
|
||||||
case HackCaptureActor.FlagCaptured(flag) =>
|
case HackCaptureActor.FlagCaptured(flag) =>
|
||||||
log.warn(hackedObjects.toString())
|
log.debug(hackedObjects.toString())
|
||||||
val building = flag.Owner.asInstanceOf[Building]
|
val building = flag.Owner.asInstanceOf[Building]
|
||||||
val bguid = building.CaptureTerminal.map { _.GUID }
|
val bguid = building.CaptureTerminal.map { _.GUID }
|
||||||
hackedObjects.find(entry => bguid.contains(entry.target.GUID)) match {
|
hackedObjects.find(entry => bguid.contains(entry.target.GUID)) match {
|
||||||
|
|
@ -160,13 +163,16 @@ class HackCaptureActor extends Actor {
|
||||||
RestartTimer()
|
RestartTimer()
|
||||||
spawnCaptureFlag(neighbours, terminal, hackingFaction)
|
spawnCaptureFlag(neighbours, terminal, hackingFaction)
|
||||||
true
|
true
|
||||||
case Some((owner, Some(flag), _)) if hackingFaction == flag.Faction =>
|
case Some((_, Some(flag), _)) if hackingFaction == flag.Faction =>
|
||||||
|
log.error(s"TrySpawnCaptureFlag: flag hacked facility can not be hacked twice by $hackingFaction")
|
||||||
|
false
|
||||||
|
case Some((owner, _, _)) if hackingFaction == terminal.Faction =>
|
||||||
log.error(s"TrySpawnCaptureFlag: owning faction and hacking faction match for facility ${owner.Name}; should we be resecuring instead?")
|
log.error(s"TrySpawnCaptureFlag: owning faction and hacking faction match for facility ${owner.Name}; should we be resecuring instead?")
|
||||||
false
|
false
|
||||||
case Some((owner, _, _)) =>
|
case Some((owner, Some(flag), _)) =>
|
||||||
log.error(s"TrySpawnCaptureFlag: couldn't find any neighbouring $hackingFaction facilities of ${owner.Name} for LLU hack")
|
log.warn(s"TrySpawnCaptureFlag: couldn't find any neighbouring $hackingFaction facilities of ${owner.Name} for LLU hack")
|
||||||
owner.GetFlagSocket.foreach { _.clearOldFlagData() }
|
owner.GetFlagSocket.foreach { _.clearOldFlagData() }
|
||||||
terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(owner.GetFlag.get, CaptureFlagLostReasonEnum.Ended)
|
terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.Ended)
|
||||||
false
|
false
|
||||||
case _ =>
|
case _ =>
|
||||||
log.error(s"TrySpawnCaptureFlag: expecting a terminal ${terminal.GUID.guid} with the ctf owning facility")
|
log.error(s"TrySpawnCaptureFlag: expecting a terminal ${terminal.GUID.guid} with the ctf owning facility")
|
||||||
|
|
|
||||||
|
|
@ -244,14 +244,27 @@ case class Experience(
|
||||||
longContributionTime: Long,
|
longContributionTime: Long,
|
||||||
bep: BattleExperiencePoints,
|
bep: BattleExperiencePoints,
|
||||||
sep: SupportExperiencePoints,
|
sep: SupportExperiencePoints,
|
||||||
cep: CommandExperiencePoints
|
cep: CommandExperiencePoints,
|
||||||
|
facilityCaptureRate: Float
|
||||||
) {
|
) {
|
||||||
assert(shortContributionTime < longContributionTime)
|
assert(shortContributionTime < longContributionTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class ThreatAssessment(
|
||||||
|
id: Int,
|
||||||
|
value: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
case class ThreatLevel(
|
||||||
|
id: Int,
|
||||||
|
level: Long
|
||||||
|
)
|
||||||
|
|
||||||
case class BattleExperiencePoints(
|
case class BattleExperiencePoints(
|
||||||
base: BattleExperiencePointsBase,
|
rate: Float,
|
||||||
rate: Float
|
base: BattleExperiencePointsBase,
|
||||||
|
lifeSpan: BattleExperiencePointsLifespan,
|
||||||
|
revenge: BattleExperiencePointsRevenge
|
||||||
)
|
)
|
||||||
|
|
||||||
case class BattleExperiencePointsBase(
|
case class BattleExperiencePointsBase(
|
||||||
|
|
@ -259,7 +272,19 @@ case class BattleExperiencePointsBase(
|
||||||
asMax: Long,
|
asMax: Long,
|
||||||
withKills: Long,
|
withKills: Long,
|
||||||
asMounted: 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(
|
case class SupportExperiencePoints(
|
||||||
|
|
@ -289,11 +314,11 @@ case class CommandExperiencePoints(
|
||||||
)
|
)
|
||||||
|
|
||||||
case class PromotionSystem(
|
case class PromotionSystem(
|
||||||
active: Boolean,
|
active: Boolean,
|
||||||
broadcastBattleRank: Int,
|
broadcastBattleRank: Int,
|
||||||
resetBattleRank: Int,
|
resetBattleRank: Int,
|
||||||
maxBattleRank: Int,
|
maxBattleRank: Int,
|
||||||
battleExperiencePointsModifier: Float,
|
battleExperiencePointsModifier: Float,
|
||||||
supportExperiencePointsModifier: Float,
|
supportExperiencePointsModifier: Float,
|
||||||
captureExperiencePointsModifier: Float
|
captureExperiencePointsModifier: Float
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue