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:
Fate-JH 2024-01-08 12:59:58 -05:00 committed by GitHub
parent ba7adee547
commit 85957670ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 737 additions and 259 deletions

View file

@ -231,7 +231,7 @@ game {
# Battle experience points
# BEP is to be calculated in relation to how valuable a kill is worth.
bep = {
# After all calculations are complete, multiple the result by this value
# After all calculations are complete, multiply the result by this value
rate = 1.0
# These numbers are to determine the starting value for a particular kill
base = {
@ -240,12 +240,103 @@ game {
# If the player who died ever utilized a mechanized assault exo-suit
as-max = 250
# The player who died got at least one kill
with-kills = 100
with-kills = 150
# The player who died was mounted in a vehicle at the time of death
as-mounted = 100
# The player who died after having been in the game world for a while after spawning.
# Dying before this is often called a "spawn kill".
mature = 50
# How long it normally takes for a player who has respawned to naturally lose the status of "green" when being inactive.
# See `base.nature`.
maturity-time = 30000
}
life-span = {
# The experience value of a player's lifespan is measured in intervals.
# Per interval, after all calculations are complete, multiply the result by this value
life-span-threat-rate = 1.0
# The experience value of using certain equipment per interval of time during playtime (consider to be per second).
# (key, value) where key is technically the index of an ExoSuitType or an object class id and value is the growth
threat-assessment-of = [
{
id = 0
value = 1.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.
# Calculation is determined by the selection of an "event" that decides how the values are combined.
sep = {
# After all calculations are complete, multiple the result by this value
# After all calculations are complete, multiply the result by this value
rate = 1.0
# When using an advanced nanite transport to deposit into the resource silo of a major facility,
# for reaching the maximum amount of a single deposit,
@ -376,6 +467,8 @@ game {
# When the cep has to be capped for squad size, calculate a small amount to add to the capped value
squad-size-limit-overflow-multiplier = 0.2
}
# When summing bep to produce facility capture base rewards, multiply the result by this value
facility-capture-rate = 0.5
}
# The game's official maximum battle rank is 40.

View file

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

View file

@ -191,6 +191,7 @@ object ExoSuitDefinition {
case PlanetSideEmpire.TR => GlobalDefinitions.TRMAX.Use
case PlanetSideEmpire.NC => GlobalDefinitions.NCMAX.Use
case PlanetSideEmpire.VS => GlobalDefinitions.VSMAX.Use
case _ => GlobalDefinitions.Standard.Use
}
case _ => GlobalDefinitions.Standard.Use
}

View file

@ -110,10 +110,10 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
)
//1) experience from killing opposingFaction across duration of hack
//The kills that occurred in the facility's attached field tower's sphere of influence have been eliminated from consideration.
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
val baseExperienceFromFacilityCapture: Long = (FacilityHackParticipation.calculateExperienceFromKills(
killsByPlayersNotInTower,
contributionOpposingSize
)
) * Config.app.game.experience.facilityCaptureRate).toLong
val events = building.Zone.AvatarEvents
val buildingId = building.GUID.guid
val zoneNumber = building.Zone.Number

View file

@ -50,7 +50,7 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
val hackerId = hacker.CharId
//1) experience from killing opposingFaction
//Because the hack duration of towers is instantaneous, the prior period of five minutes is artificially selected.
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
val baseExperienceFromFacilityCapture: Long = (FacilityHackParticipation.calculateExperienceFromKills(
FacilityHackParticipation.allocateKillsByPlayers(
building.Position,
building.Definition.SOIRadius.toFloat,
@ -60,7 +60,7 @@ final case class TowerHackParticipation(building: Building) extends FacilityHack
contributionVictor
),
contributionOpposingSize
)
) * Config.app.game.experience.facilityCaptureRate).toLong
//based on this math, the optimal number of enemy for experience gain is 20
//max value of: 1000 * pop * max(0, (40 - pop)) * 0.1
if (baseExperienceFromFacilityCapture > 0) {

View file

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

View file

@ -8,8 +8,8 @@ import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairingActivity, RevivingActivity, SpawningActivity}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire
import net.psforever.util.Config
import scala.annotation.tailrec
import scala.collection.mutable
import scala.concurrent.duration._
@ -145,161 +145,11 @@ object KillAssists {
.orElse {
limitHistoryToThisLife(history)
.lastOption
.collect { case dam: DamagingActivity =>
val res = dam.data
(res, res.adversarial.get.attacker)
}
.collect { case dam: DamagingActivity if dam.data.adversarial.nonEmpty => dam.data }
.map { data => (data, data.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.
* @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"
* @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
}
}
}
@ -600,37 +461,49 @@ object KillAssists {
): Seq[(Long, Int)] = {
var amt = amount
var count = 0
var newOrder: Seq[(Long, Int)] = Nil
var newOrderPos: Seq[(Long, Int)] = Nil
var newOrderNeg: Seq[(Long, Int)] = Nil
order.takeWhile { entry =>
val (id, total) = entry
if (id > 0 && total > 0) {
val part = participants(id)
if (amount > total) {
//drop this entry
participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case
amt = amt - total
} else {
//edit around the inclusion of this entry
val newTotal = total - amt
val trimmedWeapons = {
var index = -1
var weaponSum = 0
val pweapons = part.weapons
while (weaponSum < amt) {
index += 1
weaponSum = weaponSum + pweapons(index).amount
participants.get(id) match {
case Some(part) =>
val reduceByValue = math.min(amt, total)
val trimmedWeapons = {
var index = 0
var weaponSum = 0
val pweapons = part.weapons
val pwepiter = pweapons.iterator
while (pwepiter.hasNext && weaponSum < reduceByValue) {
weaponSum = weaponSum + pwepiter.next().amount
index += 1
}
//output list(s)
(if (weaponSum == reduceByValue) {
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)) ++
pweapons.slice(0, index).map(_.copy(amount = 0))
}
newOrder = (id, newTotal) +: newOrder
participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
amt = 0
participants.put(id, part.copy(amount = part.amount - reduceByValue, weapons = trimmedWeapons.toSeq))
amt = amt - reduceByValue
case _ =>
//we do not have contribution stat data for this id
//perform no calculations; devalue the entry
newOrderNeg = newOrderNeg :+ (id, 0)
}
}
count += 1
amt > 0
}
newOrder ++ order.drop(count)
newOrderPos ++ order.drop(count) ++ newOrderNeg//.reverse
}
}

View file

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

View file

@ -2,49 +2,510 @@
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, defaultValue = 100f * shortLifeBonus.toFloat)
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 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 {
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 = {
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 +670,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 +687,7 @@ object Support {
}
}
.getOrElse(
canNotFindEventDefaultValue.getOrElse(sep.canNotFindEventDefaultValue.toFloat)
canNotFindEventDefaultValue.getOrElse(Config.app.game.experience.sep.canNotFindEventDefaultValue.toFloat)
)
weaponStat.copy(contributions = rewards)
}

View file

@ -54,7 +54,7 @@ object ToDatabase {
position: Vector3,
exp: Long
): Unit = {
ctx.run(query[persistence.Killactivity]
ctx.run(query[persistence.Assistactivity]
.insert(
_.killerId -> lift(avatarId),
_.victimId -> lift(victimId),
@ -227,6 +227,12 @@ object ToDatabase {
avatarIdAndExp.map { case (avatarId, 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
)))
}
}

View file

@ -89,7 +89,10 @@ class HackCaptureActor extends Actor {
val faction = GetHackingFaction(target).getOrElse(target.Faction)
target.HackedBy = None
hackedObjects = remainder
val now: Long = System.currentTimeMillis()
val facilityHackTime: Long = target.Definition.FacilityHackTime.toMillis
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
building.GetFlag.collect {
case flag: CaptureFlag => target.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.Resecured)
@ -99,15 +102,15 @@ class HackCaptureActor extends Actor {
target.Faction,
faction,
hacker,
target.Definition.FacilityHackTime.toMillis,
System.currentTimeMillis() - results.head.hack_timestamp,
facilityHackTime,
hackTime,
isResecured = true
)
// Restart the timer in case the object we just removed was the next one scheduled
RestartTimer()
case HackCaptureActor.FlagCaptured(flag) =>
log.warn(hackedObjects.toString())
log.debug(hackedObjects.toString())
val building = flag.Owner.asInstanceOf[Building]
val bguid = building.CaptureTerminal.map { _.GUID }
hackedObjects.find(entry => bguid.contains(entry.target.GUID)) match {
@ -160,13 +163,16 @@ class HackCaptureActor extends Actor {
RestartTimer()
spawnCaptureFlag(neighbours, terminal, hackingFaction)
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?")
false
case Some((owner, _, _)) =>
log.error(s"TrySpawnCaptureFlag: couldn't find any neighbouring $hackingFaction facilities of ${owner.Name} for LLU hack")
case Some((owner, Some(flag), _)) =>
log.warn(s"TrySpawnCaptureFlag: couldn't find any neighbouring $hackingFaction facilities of ${owner.Name} for LLU hack")
owner.GetFlagSocket.foreach { _.clearOldFlagData() }
terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(owner.GetFlag.get, CaptureFlagLostReasonEnum.Ended)
terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.Ended)
false
case _ =>
log.error(s"TrySpawnCaptureFlag: expecting a terminal ${terminal.GUID.guid} with the ctf owning facility")

View file

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