mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
importing controlled implementation changes from original exp-for-kda branch; assist kill experience rewarded
importing controlled implementation changes from original exp-for-kda branch; code for contributions from prior activity, but will be adjusting to new contribution methods kill contributions should work; even if they don't, need to put this away for now extensivwe changes to the way OwnableByPlayer manages owner user information due to uniqueness, that changes a lot of vehicle and deployable code; fleshing out experience calculation procedure for future testing events for mounting and dismounting of both passenger and cargo; id'ing the installation of an amenity (vehicle or facility); separation of kill/assist experience and support experience calculations; retention of kill record which allows for the calculation of menace support experience accumulates and is given to the user in gradual provisions rewarding facility capture through cep; not fully tested yet; math sucks sort of cep to bep consditions for squad facility capture; bep deposit for ntu silo activity early reivision for v010; recording ongoing shots fired and landed restored bep from ntu deposits into resource silos; updating statistics in the database regarding kills and related stats including weapons; updated history management; basic experience calculation changes all rewarded support events are accounted for command experience calculations upon facility capture or resecure corrected database migrations most of the code for the play or progress system statistics window updates for exosuits to report kills; killing an llu runner gives cep; moving play or progress functionality to a bang command rather than piggybacking setbr; bep is no longer too high by error
This commit is contained in:
parent
b866aa8a30
commit
e9dbd5f259
|
|
@ -0,0 +1,22 @@
|
|||
/* Original: V008__Scoring.sql */
|
||||
CREATE OR REPLACE FUNCTION fn_assistactivity_updateRelatedStats()
|
||||
RETURNS TRIGGER
|
||||
AS
|
||||
$$
|
||||
DECLARE killerSessionId Int;
|
||||
DECLARE killerId Int;
|
||||
DECLARE weaponId Int;
|
||||
DECLARE out integer;
|
||||
BEGIN
|
||||
killerId := NEW.killer_id;
|
||||
weaponId := NEW.weapon_id;
|
||||
killerSessionId := proc_sessionnumber_get(killerId);
|
||||
out := proc_weaponstatsession_addEntryIfNoneWithSessionId(killerId, killerSessionId, weaponId);
|
||||
BEGIN
|
||||
UPDATE weaponstatsession
|
||||
SET assists = assists + 1
|
||||
WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId;
|
||||
END;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
|
@ -231,17 +231,17 @@ object VehicleSpawnPadControlTest {
|
|||
override def SetupNumberPools(): Unit = {}
|
||||
}
|
||||
zone.GUID(guid)
|
||||
zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.nanoTime()}")
|
||||
zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.currentTimeMillis()}")
|
||||
|
||||
// Hack: Wait for the Zone to finish booting, otherwise later tests will fail randomly due to race conditions
|
||||
// with actor probe setting
|
||||
// TODO(chord): Remove when Zone supports notification of booting being complete
|
||||
Thread.sleep(5000)
|
||||
|
||||
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), s"vehicle-control-${System.nanoTime()}")
|
||||
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), s"vehicle-control-${System.currentTimeMillis()}")
|
||||
|
||||
val pad = VehicleSpawnPad(GlobalDefinitions.mb_pad_creation)
|
||||
pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), s"test-pad-${System.nanoTime()}")
|
||||
pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), s"test-pad-${System.currentTimeMillis()}")
|
||||
pad.Owner =
|
||||
new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building)
|
||||
pad.Owner.Faction = faction
|
||||
|
|
|
|||
|
|
@ -75,12 +75,6 @@ game {
|
|||
third-party = no
|
||||
}
|
||||
|
||||
# Battle experience rate
|
||||
bep-rate = 1.0
|
||||
|
||||
# Command experience rate
|
||||
cep-rate = 1.0
|
||||
|
||||
# Modify the amount of mending per autorepair tick for facility amenities
|
||||
amenity-autorepair-rate = 1.0
|
||||
|
||||
|
|
@ -223,6 +217,190 @@ game {
|
|||
|
||||
# Don't ask.
|
||||
doors-can-be-opened-by-med-app-from-this-distance = 5.05
|
||||
|
||||
# How the experience calculates
|
||||
experience {
|
||||
# The short contribution time when events are collected and evaluated.
|
||||
short-contribution-time = 300000
|
||||
# The long contribution time when events are collected and evaluated
|
||||
# even factoring the same events from the short contribution time.
|
||||
# As a result, when comparing the two event lists, similar actors may appear
|
||||
# but their contributions may be different.
|
||||
long-contribution-time = 600000
|
||||
|
||||
# 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
|
||||
rate = 1.0
|
||||
# These numbers are to determine the starting value for a particular kill
|
||||
base = {
|
||||
# Black Ops multiplies the base value by this much
|
||||
bops-multiplier = 10.0
|
||||
# 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
|
||||
# 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
|
||||
}
|
||||
}
|
||||
|
||||
# Support experience points
|
||||
# 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
|
||||
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,
|
||||
# reward the user with this amount of support experience points.
|
||||
# Small deposits reward only a percentage of this value.
|
||||
ntu-silo-deposit-reward = 100
|
||||
# When the event can not be found, this flat sum is rewarded.
|
||||
# This should not be treated as a feature.
|
||||
# It is a bug.
|
||||
# Check your event label calls.
|
||||
can-not-find-event-default-value = 15
|
||||
# The events by which support experience calculation occurs.
|
||||
# Events can be composed of three parts: a base value, a per-use (shots) value, and an active amount value.
|
||||
# "Per-use" relies on knowledge from the server about the number of times this exact action occurred before the event.
|
||||
# "Active amount" relies on knowledge from the server about how much of the changes for this event are still valid.
|
||||
# Some changes can be undone by other events or other behavior.
|
||||
#
|
||||
# name - label by which this event is organized
|
||||
# base - whole number value
|
||||
# shots-multiplier - whether use count matters for this event
|
||||
# - when set to 0.0 (default), it does not
|
||||
# shots-limit - upper limit of use count
|
||||
# - cap the count here, if higher
|
||||
# shots-cutoff - if the use count exceeds this number, the event no longer applies
|
||||
# - a hard limit that should zero the contribution reward
|
||||
# - the *-cutoff should probably apply before *-limit, maybe
|
||||
# shots-nat-log - when set, may the use count to a natural logarithmic curve
|
||||
# - actually the exponent on the use count before the logarithm
|
||||
# - similar to shots-limit, but the curve plateaus quickly
|
||||
# amount-multiplier - whether active amount matters for this event
|
||||
# - when set to 0.0 (default), it does not
|
||||
events = [
|
||||
{
|
||||
name = "support-heal"
|
||||
base = 10
|
||||
shots-multiplier = 5.0
|
||||
shots-limit = 100
|
||||
amount-multiplier = 2.0
|
||||
}
|
||||
{
|
||||
name = "support-repair"
|
||||
base = 10
|
||||
shots-multiplier = 5.0
|
||||
shots-limit = 100
|
||||
}
|
||||
{
|
||||
name = "support-repair-terminal"
|
||||
base = 10
|
||||
shots-multiplier = 5.0
|
||||
shots-limit = 100
|
||||
}
|
||||
{
|
||||
name = "support-repair-turret"
|
||||
base = 10
|
||||
shots-multiplier = 5.0
|
||||
shots-limit = 100
|
||||
}
|
||||
{
|
||||
name = "mounted-kill"
|
||||
base = 25
|
||||
}
|
||||
{
|
||||
name = "router"
|
||||
base = 15
|
||||
}
|
||||
{
|
||||
name = "hotdrop"
|
||||
base = 25
|
||||
}
|
||||
{
|
||||
name = "hack"
|
||||
base = 5
|
||||
amount-multiplier = 5.0
|
||||
}
|
||||
{
|
||||
name = "ams-resupply"
|
||||
base = 15
|
||||
shots-multiplier = 1.0
|
||||
shots-nat-log = 5.0
|
||||
}
|
||||
{
|
||||
name = "lodestar-repair"
|
||||
base = 10
|
||||
shots-multiplier = 1.0
|
||||
shots-nat-log = 5.0
|
||||
shots-limit = 100
|
||||
amount-multiplier = 1.0
|
||||
}
|
||||
{
|
||||
name = "lodestar-rearm"
|
||||
base = 10
|
||||
shots-multiplier = 1.0
|
||||
shots-nat-log = 5.0
|
||||
}
|
||||
{
|
||||
name = "revival"
|
||||
base = 0
|
||||
shots-multiplier = 15.0
|
||||
shots-nat-log = 5.0
|
||||
shots-cutoff = 10
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Support experience points
|
||||
cep = {
|
||||
# After all calculations are complete, multiple the result by this value
|
||||
rate = 1.0
|
||||
# When command experience points are rewarded to the lattice link unit carrier,
|
||||
# modify the original value by this modifier.
|
||||
llu-carrier-modifier = 0.5
|
||||
# If a player died while carrying an lattice logic unit,
|
||||
# award the player who is accredited with the kill command experience as long as the time it had been carried longer than this duration.
|
||||
# Can set to Duration.Inf to never pass.
|
||||
llu-slayer-credit-duration = 1 minute
|
||||
# If a player died while carrying an lattice logic unit,
|
||||
# and satisfies the carrying duration,
|
||||
# award the player who is accredited with the kill command experience.
|
||||
llu-slayer-credit = 200
|
||||
# The maximum command experience that can be earned in a facility capture based on squad size
|
||||
maximum-per-squad-size = [990, 1980, 3466, 4950, 6436, 7920, 9406, 10890, 12376, 13860]
|
||||
# When the cep has to be capped for squad size, add a small value to the capped value
|
||||
# -1 reuses the cep before being capped
|
||||
squad-size-limit-overflow = -1
|
||||
# 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
|
||||
}
|
||||
}
|
||||
|
||||
# The game's official maximum battle rank is 40.
|
||||
# This is an artificial cap that attempts to stop advancement long before that.
|
||||
# After becoming this battle rank, battle experience points gain will be locked.
|
||||
# In our case, we're imposing this because character features can be unstable when above BR24.
|
||||
max-battle-rank = 24
|
||||
|
||||
promotion {
|
||||
# Whether promotion versus play is offered at battle rank 1.
|
||||
# Anyone who is currently enrolled in the promotion system remains enrolled during normal game play.
|
||||
# Relenting on the promotion debt back to battle rank 2 is still possible.
|
||||
active = true
|
||||
# This is the maximum battle rank that can be set as part of the promotion system.
|
||||
max-battle-rank = 13
|
||||
# How much direct combat contributes to paying back promotion debt.
|
||||
# Typically, it does not contribute.
|
||||
battle-experience-points-modifier = 0f
|
||||
# Don't forget to pay back that debt.
|
||||
}
|
||||
}
|
||||
|
||||
anti-cheat {
|
||||
|
|
|
|||
|
|
@ -137,12 +137,10 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
|
|||
bcryptedPassword
|
||||
}
|
||||
|
||||
def getAccountLogin(username: String, password: Option[String], token: Option[String]): Unit = {
|
||||
|
||||
if (token.isDefined) {
|
||||
accountLoginWithToken(token.getOrElse(""))
|
||||
} else {
|
||||
accountLogin(username, password.getOrElse(""))
|
||||
def getAccountLogin(username: String, passwordOpt: Option[String], tokenOpt: Option[String]): Unit = {
|
||||
tokenOpt match {
|
||||
case Some(token) => accountLoginWithToken(token)
|
||||
case None => accountLogin(username, passwordOpt.getOrElse(""))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +162,8 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
|
|||
accountOption <- accountsExact.headOption orElse accountsLower.headOption match {
|
||||
|
||||
// account found
|
||||
case Some(account) => Future.successful(Some(account))
|
||||
case Some(account) =>
|
||||
Future.successful(Some(account))
|
||||
|
||||
// create new account
|
||||
case None =>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,17 @@ package net.psforever.actors.session
|
|||
import akka.actor.Cancellable
|
||||
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
||||
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.avatar.scoring.{Death, EquipmentStat, KDAStat, Kill}
|
||||
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill, Life, ScoreCard, SupportActivity}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.sourcing.VehicleSource
|
||||
import net.psforever.objects.vital.InGameHistory
|
||||
import net.psforever.objects.vehicles.MountedWeapons
|
||||
import net.psforever.types.{ChatMessageType, StatisticalCategory, StatisticalElement}
|
||||
import org.joda.time.{LocalDateTime, Seconds}
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
|
||||
import scala.util.{Failure, Success}
|
||||
|
|
@ -36,7 +43,7 @@ import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, VehicleLoadout}
|
|||
import net.psforever.objects.locker.LockerContainer
|
||||
import net.psforever.objects.sourcing.{PlayerSource,SourceWithHealthEntry}
|
||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
||||
import net.psforever.objects.vital.{DamagingActivity, HealFromImplant, HealingActivity, SpawningActivity, Vitality}
|
||||
import net.psforever.objects.vital.{DamagingActivity, HealFromImplant, HealingActivity, SpawningActivity}
|
||||
import net.psforever.packet.game.objectcreate.{BasicCharacterData, ObjectClass, RibbonBars}
|
||||
import net.psforever.packet.game.{Friend => GameFriend, _}
|
||||
import net.psforever.persistence
|
||||
|
|
@ -199,11 +206,17 @@ object AvatarActor {
|
|||
/** Set total battle experience points */
|
||||
final case class SetBep(bep: Long) extends Command
|
||||
|
||||
/** Advance total battle experience points */
|
||||
final case class Progress(bep: Long) extends Command
|
||||
|
||||
/** Award command experience points */
|
||||
final case class AwardCep(bep: Long) extends Command
|
||||
final case class AwardFacilityCaptureBep(bep: Long) extends Command
|
||||
|
||||
/** Award command experience points */
|
||||
final case class AwardCep(cep: Long) extends Command
|
||||
|
||||
/** Set total command experience points */
|
||||
final case class SetCep(bep: Long) extends Command
|
||||
final case class SetCep(cep: Long) extends Command
|
||||
|
||||
/** Set cosmetics. Only allowed for BR24 or higher. */
|
||||
final case class SetCosmetics(personalStyles: Set[Cosmetic]) extends Command
|
||||
|
|
@ -226,6 +239,8 @@ object AvatarActor {
|
|||
|
||||
final case class AvatarLoginResponse(avatar: Avatar)
|
||||
|
||||
final case class SupportExperienceDeposit(bep: Long, delay: Long) extends Command
|
||||
|
||||
/**
|
||||
* A player loadout represents all of the items in the player's hands (equipment slots)
|
||||
* and all of the items in the player's backpack (inventory)
|
||||
|
|
@ -822,6 +837,110 @@ object AvatarActor {
|
|||
out.future
|
||||
}
|
||||
|
||||
def setBepOnly(avatarId: Long, bep: Long): Future[Long] = {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val out: Promise[Long] = Promise()
|
||||
val result = ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId)).update(_.bep -> lift(bep)))
|
||||
result.onComplete { _ =>
|
||||
out.completeWith(Future(bep))
|
||||
}
|
||||
out.future
|
||||
}
|
||||
|
||||
def loadExperienceDebt(avatarId: Long): Future[Long] = {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val out: Promise[Long] = Promise()
|
||||
val result = ctx.run(query[persistence.Progressiondebt].filter(_.avatarId == lift(avatarId)))
|
||||
result.onComplete {
|
||||
case Success(debt) if debt.nonEmpty =>
|
||||
out.completeWith(Future(debt.head.experience))
|
||||
case _ =>
|
||||
out.completeWith(Future(0L))
|
||||
}
|
||||
out.future
|
||||
}
|
||||
|
||||
def saveExperienceDebt(avatarId: Long, exp: Long): Future[Int] = {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val out: Promise[Int] = Promise()
|
||||
val result = ctx.run(query[persistence.Progressiondebt].filter(_.avatarId == lift(avatarId)))
|
||||
result.onComplete {
|
||||
case Success(debt) if debt.nonEmpty =>
|
||||
ctx.run(
|
||||
query[persistence.Progressiondebt]
|
||||
.filter(_.avatarId == lift(avatarId))
|
||||
.update(_.experience -> lift(exp))
|
||||
)
|
||||
out.completeWith(Future(1))
|
||||
case _ =>
|
||||
out.completeWith(Future(0))
|
||||
}
|
||||
out.future
|
||||
}
|
||||
|
||||
def avatarNoLongerLoggedIn(accountId: Long): Unit = {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global //linter says unused but compiler says otherwise
|
||||
ctx.run(
|
||||
query[persistence.Account]
|
||||
.filter(_.id == lift(accountId))
|
||||
.update(_.avatarLoggedIn -> lift(0L))
|
||||
)
|
||||
}
|
||||
|
||||
def updateToolDischargeFor(avatar: Avatar): Unit = {
|
||||
updateToolDischargeFor(avatar.id, avatar.scorecard.CurrentLife)
|
||||
}
|
||||
|
||||
def updateToolDischargeFor(avatarId: Long, life: Life): Unit = {
|
||||
updateToolDischargeFor(avatarId, life.equipmentStats)
|
||||
}
|
||||
|
||||
def updateToolDischargeFor(avatarId: Long, stats: Seq[EquipmentStat]): Unit = {
|
||||
stats.foreach { stat =>
|
||||
zones.exp.ToDatabase.reportToolDischarge(avatarId, stat)
|
||||
}
|
||||
}
|
||||
|
||||
def loadCampaignKdaData(avatarId: Long): Future[ScoreCard] = {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val out: Promise[ScoreCard] = Promise()
|
||||
val result = ctx.run(quote {
|
||||
for {
|
||||
kdaOut <- query[persistence.Killactivity]
|
||||
.filter(c =>
|
||||
(c.killerId == lift(avatarId) || c.victimId == lift(avatarId)) && c.killerId != c.victimId
|
||||
)
|
||||
.join(query[persistence.Avatar])
|
||||
.on({ case (a, b) =>
|
||||
b.id == a.victimId
|
||||
})
|
||||
.map({ case (a, b) =>
|
||||
(a.killerId, a.victimId, a.victimExosuit, b.factionId)
|
||||
})
|
||||
} yield kdaOut
|
||||
})
|
||||
result.onComplete {
|
||||
case Success(res) =>
|
||||
val card = new ScoreCard()
|
||||
val (killerEntries, _) = res.partition {
|
||||
case (killer, _, _, _) => avatarId == killer
|
||||
}
|
||||
killerEntries.foreach { case (_, _, exosuit, faction) =>
|
||||
val statId = StatisticalElement.relatedElement(ExoSuitType(exosuit)).value
|
||||
card.initStatisticForKill(statId, PlanetSideEmpire(faction))
|
||||
}
|
||||
out.completeWith(Future(card))
|
||||
case _ =>
|
||||
out.completeWith(Future(new ScoreCard()))
|
||||
}
|
||||
out.future
|
||||
}
|
||||
|
||||
def toAvatar(avatar: persistence.Avatar): Avatar = {
|
||||
val bep = avatar.bep
|
||||
val convertedCosmetics = if (BattleRank.showCosmetics(bep)) {
|
||||
|
|
@ -863,6 +982,9 @@ class AvatarActor(
|
|||
var _avatar: Option[Avatar] = None
|
||||
var saveLockerFunc: () => Unit = storeNewLocker
|
||||
//val topic: ActorRef[Topic.Command[Avatar]] = context.spawnAnonymous(Topic[Avatar]("avatar"))
|
||||
var supportExperiencePool: Long = 0
|
||||
var supportExperienceTimer: Cancellable = Default.Cancellable
|
||||
var experienceDebt: Long = 0L
|
||||
|
||||
def avatar: Avatar = _avatar.get
|
||||
|
||||
|
|
@ -1024,13 +1146,13 @@ class AvatarActor(
|
|||
} yield true
|
||||
inits.onComplete {
|
||||
case Success(_) =>
|
||||
performAvatarLogin(avatarId, account.id, replyTo)
|
||||
performAvatarLoginTest(avatarId, account.id, replyTo)
|
||||
case Failure(e) =>
|
||||
log.error(e)("db failure")
|
||||
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6))
|
||||
}
|
||||
} else {
|
||||
performAvatarLogin(avatarId, account.id, replyTo)
|
||||
performAvatarLoginTest(avatarId, account.id, replyTo)
|
||||
}
|
||||
case Success(_) =>
|
||||
//TODO this may not be an actual failure, but don't know what to do
|
||||
|
|
@ -1049,6 +1171,11 @@ class AvatarActor(
|
|||
buffer.stash(other)
|
||||
Behaviors.same
|
||||
}
|
||||
.receiveSignal {
|
||||
case (_, PostStop) =>
|
||||
AvatarActor.avatarNoLongerLoggedIn(account.id)
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
||||
def postCharacterSelectBehaviour(): Behavior[Command] = {
|
||||
|
|
@ -1646,24 +1773,95 @@ class AvatarActor(
|
|||
updateToolDischarge(stats)
|
||||
Behaviors.same
|
||||
|
||||
case UpdateKillsDeathsAssists(stat) =>
|
||||
updateKillsDeathsAssists(stat)
|
||||
case UpdateKillsDeathsAssists(stat: Kill) =>
|
||||
updateKills(stat)
|
||||
Behaviors.same
|
||||
|
||||
case UpdateKillsDeathsAssists(stat: Assist) =>
|
||||
updateAssists(stat)
|
||||
Behaviors.same
|
||||
|
||||
case UpdateKillsDeathsAssists(stat: Death) =>
|
||||
updateDeaths(stat)
|
||||
Behaviors.same
|
||||
|
||||
case UpdateKillsDeathsAssists(stat: SupportActivity) =>
|
||||
updateSupport(stat)
|
||||
Behaviors.same
|
||||
|
||||
case AwardBep(bep, ExperienceType.Support) =>
|
||||
val gain = bep - experienceDebt
|
||||
if (gain > 0L) {
|
||||
awardSupportExperience(gain, previousDelay = 0L)
|
||||
} else {
|
||||
experienceDebt = experienceDebt - bep
|
||||
}
|
||||
Behaviors.same
|
||||
|
||||
case AwardBep(bep, modifier) =>
|
||||
setBep(avatar.bep + bep, modifier)
|
||||
val mod = Config.app.game.promotion.battleExperiencePointsModifier
|
||||
if (experienceDebt == 0L) {
|
||||
setBep(avatar.bep + bep, modifier)
|
||||
} else if (mod > 0f) {
|
||||
val modifiedBep = (bep.toFloat * Config.app.game.promotion.battleExperiencePointsModifier).toLong
|
||||
val gain = modifiedBep - experienceDebt
|
||||
if (gain > 0L) {
|
||||
setBep(avatar.bep + gain, modifier)
|
||||
} else {
|
||||
experienceDebt = experienceDebt - modifiedBep
|
||||
}
|
||||
}
|
||||
Behaviors.same
|
||||
|
||||
case AwardFacilityCaptureBep(bep) =>
|
||||
val gain = bep - experienceDebt
|
||||
if (gain > 0L) {
|
||||
setBep(gain, ExperienceType.Normal)
|
||||
} else {
|
||||
experienceDebt = experienceDebt - bep
|
||||
}
|
||||
Behaviors.same
|
||||
|
||||
case SupportExperienceDeposit(bep, delayBy) =>
|
||||
actuallyAwardSupportExperience(bep, delayBy)
|
||||
Behaviors.same
|
||||
|
||||
case SetBep(bep) =>
|
||||
setBep(bep, ExperienceType.Normal)
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_229, "@AckSuccessSetBattleRank"))
|
||||
Behaviors.same
|
||||
|
||||
case Progress(bep) =>
|
||||
if ({
|
||||
val oldBr = BattleRank.withExperience(avatar.bep).value
|
||||
val newBr = BattleRank.withExperience(bep).value
|
||||
if (Config.app.game.promotion.active && oldBr == 1 && newBr > 1 && newBr < Config.app.game.promotion.maxBattleRank + 1) {
|
||||
experienceDebt = bep
|
||||
if (avatar.cep > 0) {
|
||||
setCep(0L)
|
||||
}
|
||||
true
|
||||
} else if (experienceDebt > 0 && newBr == 2) {
|
||||
experienceDebt = 0
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
setBep(bep, ExperienceType.Normal)
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_229, "@AckSuccessSetBattleRank"))
|
||||
}
|
||||
Behaviors.same
|
||||
|
||||
case AwardCep(cep) =>
|
||||
setCep(avatar.cep + cep)
|
||||
if (experienceDebt > 0L) {
|
||||
setCep(avatar.cep + cep)
|
||||
}
|
||||
Behaviors.same
|
||||
|
||||
case SetCep(cep) =>
|
||||
setCep(cep)
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_229, "@AckSuccessSetCommandRank"))
|
||||
Behaviors.same
|
||||
|
||||
case SetCosmetics(cosmetics) =>
|
||||
|
|
@ -1789,10 +1987,17 @@ class AvatarActor(
|
|||
}
|
||||
.receiveSignal {
|
||||
case (_, PostStop) =>
|
||||
AvatarActor.saveAvatarData(avatar)
|
||||
staminaRegenTimer.cancel()
|
||||
implantTimers.values.foreach(_.cancel())
|
||||
supportExperienceTimer.cancel()
|
||||
if (supportExperiencePool > 0) {
|
||||
AvatarActor.setBepOnly(avatar.id, avatar.bep + supportExperiencePool)
|
||||
}
|
||||
AvatarActor.saveAvatarData(avatar)
|
||||
saveLockerFunc()
|
||||
AvatarActor.updateToolDischargeFor(avatar)
|
||||
AvatarActor.saveExperienceDebt(avatar.id, experienceDebt)
|
||||
AvatarActor.avatarNoLongerLoggedIn(account.get.id)
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
|
@ -1805,9 +2010,26 @@ class AvatarActor(
|
|||
Future.failed(ex).asInstanceOf[Future[Loadout]]
|
||||
}
|
||||
|
||||
def performAvatarLoginTest(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
|
||||
import ctx._
|
||||
val blockLogInIfNot = for {
|
||||
out <- ctx.run(query[persistence.Account].filter(_.id == lift(accountId)))
|
||||
} yield out
|
||||
blockLogInIfNot.onComplete {
|
||||
case Success(account)
|
||||
//TODO test against acceptable player factions
|
||||
if account.exists { _.avatarLoggedIn == 0 } =>
|
||||
//accept
|
||||
performAvatarLogin(avatarId, accountId, replyTo)
|
||||
case _ =>
|
||||
//refuse
|
||||
//TODO refuse?
|
||||
sessionActor ! SessionActor.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
def performAvatarLogin(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
|
||||
import ctx._
|
||||
|
||||
val result = for {
|
||||
//log this login
|
||||
_ <- ctx.run(
|
||||
|
|
@ -1819,7 +2041,10 @@ class AvatarActor(
|
|||
_ <- ctx.run(
|
||||
query[persistence.Account]
|
||||
.filter(_.id == lift(accountId))
|
||||
.update(_.lastFactionId -> lift(avatar.faction.id))
|
||||
.update(
|
||||
_.lastFactionId -> lift(avatar.faction.id),
|
||||
_.avatarLoggedIn -> lift(avatarId)
|
||||
)
|
||||
)
|
||||
//retrieve avatar data
|
||||
loadouts <- initializeAllLoadouts()
|
||||
|
|
@ -1830,9 +2055,11 @@ class AvatarActor(
|
|||
ignored <- loadIgnoredList(avatarId)
|
||||
shortcuts <- loadShortcuts(avatarId)
|
||||
saved <- AvatarActor.loadSavedAvatarData(avatarId)
|
||||
} yield (loadouts, implants, certs, locker, friends, ignored, shortcuts, saved)
|
||||
debt <- AvatarActor.loadExperienceDebt(avatarId)
|
||||
card <- AvatarActor.loadCampaignKdaData(avatarId)
|
||||
} yield (loadouts, implants, certs, locker, friends, ignored, shortcuts, saved, debt, card)
|
||||
result.onComplete {
|
||||
case Success((_loadouts, implants, certs, lockerInv, friendsList, ignoredList, shortcutList, saved)) =>
|
||||
case Success((_loadouts, implants, certs, lockerInv, friendsList, ignoredList, shortcutList, saved, debt, card)) =>
|
||||
//shortcuts must have a hotbar option for each implant
|
||||
// val implantShortcuts = shortcutList.filter {
|
||||
// case Some(e) => e.purpose == 0
|
||||
|
|
@ -1866,11 +2093,13 @@ class AvatarActor(
|
|||
cooldowns = Cooldowns(
|
||||
purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log),
|
||||
use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log)
|
||||
)
|
||||
),
|
||||
scorecard = card
|
||||
)
|
||||
)
|
||||
// if we need to start stamina regeneration
|
||||
tryRestoreStaminaForSession(stamina = 1).collect { _ => defaultStaminaRegen(initialDelay = 0.5f seconds) }
|
||||
experienceDebt = debt
|
||||
replyTo ! AvatarLoginResponse(avatar)
|
||||
case Failure(e) =>
|
||||
log.error(e)("db failure")
|
||||
|
|
@ -2865,13 +3094,7 @@ class AvatarActor(
|
|||
|
||||
def setBep(bep: Long, modifier: ExperienceType): Unit = {
|
||||
import ctx._
|
||||
val current = BattleRank.withExperience(avatar.bep).value
|
||||
val next = BattleRank.withExperience(bep).value
|
||||
lazy val br24 = BattleRank.BR24.value
|
||||
val result = for {
|
||||
r <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatar.id)).update(_.bep -> lift(bep)))
|
||||
} yield r
|
||||
result.onComplete {
|
||||
AvatarActor.setBepOnly(avatar.id, bep).onComplete {
|
||||
case Success(_) =>
|
||||
val sess = session.get
|
||||
val zone = sess.zone
|
||||
|
|
@ -2880,6 +3103,9 @@ class AvatarActor(
|
|||
val player = sess.player
|
||||
val pguid = player.GUID
|
||||
val localModifier = modifier
|
||||
val current = BattleRank.withExperience(avatar.bep).value
|
||||
val next = BattleRank.withExperience(bep).value
|
||||
val br24 = BattleRank.BR24.value
|
||||
sessionActor ! SessionActor.SendResponse(BattleExperienceMessage(pguid, bep, localModifier))
|
||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(pguid, 17, bep))
|
||||
if (current < br24 && next >= br24 || current >= br24 && next < br24) {
|
||||
|
|
@ -2936,41 +3162,142 @@ class AvatarActor(
|
|||
}
|
||||
}
|
||||
|
||||
def updateKillsDeathsAssists(kdaStat: KDAStat): Unit = {
|
||||
avatar.scorecard.rate(kdaStat)
|
||||
val exp = kdaStat.experienceEarned
|
||||
def awardSupportExperience(bep: Long, previousDelay: Long): Unit = {
|
||||
setBep(avatar.bep + bep, ExperienceType.Support) //todo simplify support testing
|
||||
// supportExperiencePool = supportExperiencePool + bep
|
||||
// avatar.scorecard.rate(bep)
|
||||
// if (supportExperienceTimer.isCancelled) {
|
||||
// resetSupportExperienceTimer(previousBep = 0, previousDelay = 0)
|
||||
// }
|
||||
}
|
||||
|
||||
def actuallyAwardSupportExperience(bep: Long, delayBy: Long): Unit = {
|
||||
setBep(avatar.bep + bep, ExperienceType.Support)
|
||||
supportExperiencePool = supportExperiencePool - bep
|
||||
if (supportExperiencePool > 0) {
|
||||
resetSupportExperienceTimer(bep, delayBy)
|
||||
} else {
|
||||
supportExperiencePool = 0
|
||||
supportExperienceTimer.cancel()
|
||||
supportExperienceTimer = Default.Cancellable
|
||||
}
|
||||
}
|
||||
|
||||
def updateKills(killStat: Kill): Unit = {
|
||||
val exp = killStat.experienceEarned
|
||||
val (modifiedExp, msg) = updateExperienceAndType(killStat.experienceEarned)
|
||||
val output = avatar.scorecard.rate(killStat.copy(experienceEarned = modifiedExp))
|
||||
val _session = session.get
|
||||
val zone = _session.zone
|
||||
val player = _session.player
|
||||
val playerSource = PlayerSource(player)
|
||||
val historyTranscript = {
|
||||
(killStat.info.interaction.cause match {
|
||||
case pr: ProjectileReason => pr.projectile.mounted_in.flatMap { a => zone.GUID(a._1) } //what fired the projectile
|
||||
case _ => None
|
||||
}).collect {
|
||||
case mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons =>
|
||||
player.ContributionFrom(mount)
|
||||
}
|
||||
player.HistoryAndContributions()
|
||||
}
|
||||
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, killStat, exp)
|
||||
val target = killStat.info.targetAfter.asInstanceOf[PlayerSource]
|
||||
val targetMounted = target.seatedIn.map { case (v: VehicleSource, seat) =>
|
||||
val definition = v.Definition
|
||||
definition.ObjectId * 10 + Vehicles.SeatPermissionGroup(definition, seat).map { _.id }.getOrElse(0)
|
||||
}.getOrElse(0)
|
||||
zones.exp.ToDatabase.reportKillBy(
|
||||
avatar.id.toLong,
|
||||
target.CharId,
|
||||
target.ExoSuit.id,
|
||||
targetMounted,
|
||||
killStat.info.interaction.cause.attribution,
|
||||
player.Zone.Number,
|
||||
target.Position,
|
||||
modifiedExp
|
||||
)
|
||||
output.foreach { case (id, entry) =>
|
||||
val elem = StatisticalElement.fromId(id)
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
AvatarStatisticsMessage(SessionStatistic(
|
||||
StatisticalCategory.Destroyed,
|
||||
elem,
|
||||
entry.tr_s,
|
||||
entry.nc_s,
|
||||
entry.vs_s,
|
||||
entry.ps_s
|
||||
))
|
||||
)
|
||||
}
|
||||
if (exp > 0L) {
|
||||
setBep(avatar.bep + exp, msg)
|
||||
}
|
||||
}
|
||||
|
||||
def updateDeaths(deathStat: Death): Unit = {
|
||||
AvatarActor.updateToolDischargeFor(avatar)
|
||||
avatar.scorecard.rate(deathStat)
|
||||
val _session = session.get
|
||||
val zone = _session.zone
|
||||
val player = _session.player
|
||||
kdaStat match {
|
||||
case kill: Kill =>
|
||||
val playerSource = PlayerSource(player)
|
||||
(kill.info.interaction.cause match {
|
||||
case pr: ProjectileReason => pr.projectile.mounted_in.map { a => zone.GUID(a._1) }
|
||||
case _ => None
|
||||
}).collect {
|
||||
case Some(obj: Vitality) =>
|
||||
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, obj.History, kill, exp)
|
||||
}
|
||||
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.History, kill, exp)
|
||||
case _: Death =>
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
player.Name,
|
||||
AvatarAction.SendResponse(
|
||||
Service.defaultPlayerGUID,
|
||||
AvatarStatisticsMessage(DeathStatistic(avatar.scorecard.Lives.size))
|
||||
)
|
||||
)
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
player.Name,
|
||||
AvatarAction.SendResponse(
|
||||
Service.defaultPlayerGUID,
|
||||
AvatarStatisticsMessage(DeathStatistic(ScoreCard.deathCount(avatar.scorecard)))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def updateAssists(assistStat: Assist): Unit = {
|
||||
avatar.scorecard.rate(assistStat)
|
||||
val exp = assistStat.experienceEarned
|
||||
val _session = session.get
|
||||
val avatarId = avatar.id.toLong
|
||||
assistStat.weapons.foreach { wrapper =>
|
||||
zones.exp.ToDatabase.reportKillAssistBy(
|
||||
avatarId,
|
||||
assistStat.victim.CharId,
|
||||
wrapper.equipment,
|
||||
_session.zone.Number,
|
||||
assistStat.victim.Position,
|
||||
exp
|
||||
)
|
||||
}
|
||||
if (exp > 0L) {
|
||||
val gameOpts = Config.app.game
|
||||
val (msg, modifier): (ExperienceType, Float) = if (player.Carrying.contains(SpecialCarry.RabbitBall)) {
|
||||
(ExperienceType.RabbitBall, 1.25f)
|
||||
} else {
|
||||
(ExperienceType.Normal, 1f)
|
||||
}
|
||||
setBep(avatar.bep + (exp * modifier * gameOpts.bepRate).toLong, msg)
|
||||
awardSupportExperience(exp, previousDelay = 0L)
|
||||
}
|
||||
|
||||
def updateSupport(supportStat: SupportActivity): Unit = {
|
||||
val avatarId = avatar.id.toLong
|
||||
val target = supportStat.target
|
||||
val targetId = target.CharId
|
||||
val targetExosuit = target.ExoSuit.id
|
||||
val exp = supportStat.experienceEarned
|
||||
supportStat.weapons.foreach { entry =>
|
||||
zones.exp.ToDatabase.reportSupportBy(
|
||||
avatarId,
|
||||
targetId,
|
||||
targetExosuit,
|
||||
entry.value,
|
||||
entry.intermediate,
|
||||
entry.equipment,
|
||||
exp
|
||||
)
|
||||
}
|
||||
awardSupportExperience(exp, previousDelay = 0L)
|
||||
}
|
||||
|
||||
def updateExperienceAndType(exp: Long): (Long, ExperienceType) = {
|
||||
val _session = session.get
|
||||
val player = _session.player
|
||||
val gameOpts = Config.app.game.experience.bep
|
||||
val (modifier, msg) = if (player.Carrying.contains(SpecialCarry.RabbitBall)) {
|
||||
(1.25f, ExperienceType.RabbitBall)
|
||||
} else {
|
||||
(1f, ExperienceType.Normal)
|
||||
}
|
||||
((exp * modifier * gameOpts.rate).toLong, msg)
|
||||
}
|
||||
|
||||
def updateToolDischarge(stats: EquipmentStat): Unit = {
|
||||
|
|
@ -3103,4 +3430,41 @@ class AvatarActor(
|
|||
}
|
||||
output.future
|
||||
}
|
||||
|
||||
def resetSupportExperienceTimer(previousBep: Long, previousDelay: Long): Unit = {
|
||||
val bep: Long = if (supportExperiencePool < 10L) {
|
||||
supportExperiencePool
|
||||
} else {
|
||||
val rand = math.random()
|
||||
val range: Long = if (previousBep < 30L) {
|
||||
if (rand < 0.3d) {
|
||||
75L
|
||||
} else {
|
||||
215L
|
||||
}
|
||||
} else {
|
||||
if (rand < 0.1d || (previousDelay > 35000L && previousBep > 150L)) {
|
||||
75L
|
||||
} else if (rand > 0.9d) {
|
||||
520L
|
||||
} else {
|
||||
125L
|
||||
}
|
||||
}
|
||||
math.min((range * math.random()).toLong, supportExperiencePool)
|
||||
}
|
||||
val delay: Long = {
|
||||
val rand = math.random()
|
||||
if ((previousBep > 190L || previousDelay > 35000L) && bep < 51L) {
|
||||
(1000d * rand).toLong
|
||||
} else {
|
||||
10000L + (rand * 35000d).toLong
|
||||
}
|
||||
}
|
||||
supportExperienceTimer = context.scheduleOnce(
|
||||
delay.milliseconds,
|
||||
context.self,
|
||||
SupportExperienceDeposit(bep, delay)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
|
|||
import akka.actor.typed.receptionist.Receptionist
|
||||
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.ExecutionContextExecutor
|
||||
import scala.concurrent.duration._
|
||||
|
|
@ -76,7 +77,7 @@ object ChatActor {
|
|||
): Unit = {
|
||||
if (silos.isEmpty) {
|
||||
session ! SessionActor.SendResponse(
|
||||
ChatMsg(UNK_229, true, "Server", s"no targets for ntu found with parameters $debugContent", None)
|
||||
ChatMsg(UNK_229, wideContents=true, "Server", s"no targets for ntu found with parameters $debugContent", None)
|
||||
)
|
||||
}
|
||||
resources match {
|
||||
|
|
@ -225,7 +226,7 @@ class ChatActor(
|
|||
}
|
||||
sessionActor ! SessionActor.SetFlying(flying)
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(CMT_FLY, false, recipient, if (flying) "on" else "off", None)
|
||||
ChatMsg(CMT_FLY, wideContents=false, recipient, if (flying) "on" else "off", None)
|
||||
)
|
||||
|
||||
case (CMT_ANONYMOUS, _, _) =>
|
||||
|
|
@ -282,69 +283,43 @@ class ChatActor(
|
|||
}
|
||||
errorMessage match {
|
||||
case Some(errorMessage) =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(
|
||||
CMT_QUIT,
|
||||
false,
|
||||
"",
|
||||
errorMessage,
|
||||
None
|
||||
)
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, errorMessage))
|
||||
case None =>
|
||||
sessionActor ! SessionActor.Recall()
|
||||
}
|
||||
|
||||
case (CMT_INSTANTACTION, _, _) =>
|
||||
if (session.zoningType == Zoning.Method.Quit) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(CMT_QUIT, false, "", "You can't instant action while quitting.", None)
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "You can't instant action while quitting."))
|
||||
} else if (session.zoningType == Zoning.Method.InstantAction) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_instantactionting", None)
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_instantactionting"))
|
||||
} else if (session.zoningType == Zoning.Method.Recall) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(
|
||||
CMT_QUIT,
|
||||
false,
|
||||
"",
|
||||
"You won't instant action. You already requested to recall to your sanctuary continent",
|
||||
None
|
||||
)
|
||||
ChatMsg(CMT_QUIT, "You won't instant action. You already requested to recall to your sanctuary continent")
|
||||
)
|
||||
} else if (!session.player.isAlive || session.deadState != DeadState.Alive) {
|
||||
if (session.player.isAlive) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_deconstructing", None)
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_deconstructing"))
|
||||
} else {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_dead", None)
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_dead"))
|
||||
}
|
||||
} else if (session.player.VehicleSeated.nonEmpty) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_invehicle", None)
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_invehicle"))
|
||||
} else {
|
||||
sessionActor ! SessionActor.InstantAction()
|
||||
}
|
||||
|
||||
case (CMT_QUIT, _, _) =>
|
||||
if (session.zoningType == Zoning.Method.Quit) {
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_quitting", None))
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_quitting"))
|
||||
} else if (!session.player.isAlive || session.deadState != DeadState.Alive) {
|
||||
if (session.player.isAlive) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(CMT_QUIT, false, "", "@noquit_deconstructing", None)
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_deconstructing"))
|
||||
} else {
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_dead", None))
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_dead"))
|
||||
}
|
||||
} else if (session.player.VehicleSeated.nonEmpty) {
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_invehicle", None))
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_invehicle"))
|
||||
} else {
|
||||
sessionActor ! SessionActor.Quit()
|
||||
}
|
||||
|
|
@ -514,7 +489,7 @@ class ChatActor(
|
|||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(
|
||||
UNK_229,
|
||||
true,
|
||||
wideContents=true,
|
||||
"",
|
||||
s"\\#FF4040ERROR - \'${args(0)}\' is not a valid building name.",
|
||||
None
|
||||
|
|
@ -524,7 +499,7 @@ class ChatActor(
|
|||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(
|
||||
UNK_229,
|
||||
true,
|
||||
wideContents=true,
|
||||
"",
|
||||
s"\\#FF4040ERROR - \'${args(timerPos.get)}\' is not a valid timer value.",
|
||||
None
|
||||
|
|
@ -610,11 +585,11 @@ class ChatActor(
|
|||
)
|
||||
} else if (AvatarActor.getLiveAvatarForFunc(message.recipient, (_,_,_)=>{}).isEmpty) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.UNK_45, false, "none", "@notell_target", None)
|
||||
ChatMsg(ChatMessageType.UNK_45, wideContents=false, "none", "@notell_target", None)
|
||||
)
|
||||
} else {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.UNK_45, false, "none", "@notell_ignore", None)
|
||||
ChatMsg(ChatMessageType.UNK_45, wideContents=false, "none", "@notell_ignore", None)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -663,22 +638,20 @@ class ChatActor(
|
|||
val popVS = players.count(_.faction == PlanetSideEmpire.VS)
|
||||
|
||||
if (popNC + popTR + popVS == 0) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.CMT_WHO, false, "", "@Nomatches", None)
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.CMT_WHO, "@Nomatches"))
|
||||
} else {
|
||||
val contName = session.zone.map.name
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None)
|
||||
ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "That command doesn't work for now, but : ", None)
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None)
|
||||
ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "NC online : " + popNC + " on " + contName, None)
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None)
|
||||
ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "TR online : " + popTR + " on " + contName, None)
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None)
|
||||
ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "VS online : " + popVS + " on " + contName, None)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -702,16 +675,16 @@ class ChatActor(
|
|||
}
|
||||
(zone, gate, list) match {
|
||||
case (None, None, true) =>
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, true, "", PointOfInterest.list, None))
|
||||
sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.list, None))
|
||||
case (Some(zone), None, true) =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(UNK_229, true, "", PointOfInterest.listWarpgates(zone), None)
|
||||
ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.listWarpgates(zone), None)
|
||||
)
|
||||
case (Some(zone), Some(gate), false) =>
|
||||
sessionActor ! SessionActor.SetZone(zone.zonename, gate)
|
||||
case (_, None, false) =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(UNK_229, true, "", "Gate id not defined (use '/zone <zone> -list')", None)
|
||||
ChatMsg(UNK_229, wideContents=true, "", "Gate id not defined (use '/zone <zone> -list')", None)
|
||||
)
|
||||
case (_, _, _) if buffer.isEmpty || buffer(0).equals("-help") =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
|
|
@ -740,16 +713,16 @@ class ChatActor(
|
|||
zone match {
|
||||
case Some(zone: PointOfInterest) =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(UNK_229, true, "", PointOfInterest.listAll(zone), None)
|
||||
ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.listAll(zone), None)
|
||||
)
|
||||
case _ => ChatMsg(UNK_229, true, "", s"unknown player zone '${session.player.Zone.id}'", None)
|
||||
case _ => ChatMsg(UNK_229, wideContents=true, "", s"unknown player zone '${session.player.Zone.id}'", None)
|
||||
}
|
||||
case (None, Some(waypoint)) if waypoint != "-help" =>
|
||||
PointOfInterest.getWarpLocation(session.zone.id, waypoint) match {
|
||||
case Some(location) => sessionActor ! SessionActor.SetPosition(location)
|
||||
case None =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(UNK_229, true, "", s"unknown location '$waypoint'", None)
|
||||
ChatMsg(UNK_229, wideContents=true, "", s"unknown location '$waypoint'", None)
|
||||
)
|
||||
}
|
||||
case _ =>
|
||||
|
|
@ -759,60 +732,10 @@ class ChatActor(
|
|||
}
|
||||
|
||||
case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed =>
|
||||
val buffer = contents.toLowerCase.split("\\s+")
|
||||
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
|
||||
case (Some(target), Some(rank)) if target == session.avatar.name =>
|
||||
rank.toIntOption match {
|
||||
case Some(rank) => (None, BattleRank.withValueOpt(rank))
|
||||
case None => (None, None)
|
||||
}
|
||||
case (Some(target), Some(rank)) =>
|
||||
// picking other targets is not supported for now
|
||||
(None, None)
|
||||
case (Some(rank), None) =>
|
||||
rank.toIntOption match {
|
||||
case Some(rank) => (None, BattleRank.withValueOpt(rank))
|
||||
case None => (None, None)
|
||||
}
|
||||
case _ => (None, None)
|
||||
}
|
||||
(target, rank) match {
|
||||
case (_, Some(rank)) =>
|
||||
avatarActor ! AvatarActor.SetBep(rank.experience)
|
||||
sessionActor ! SessionActor.SendResponse(message.copy(contents = "@AckSuccessSetBattleRank"))
|
||||
case _ =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
|
||||
)
|
||||
}
|
||||
setBattleRank(message, contents, session, AvatarActor.SetBep)
|
||||
|
||||
case (CMT_SETCOMMANDRANK, _, contents) if gmCommandAllowed =>
|
||||
val buffer = contents.toLowerCase.split("\\s+")
|
||||
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
|
||||
case (Some(target), Some(rank)) if target == session.avatar.name =>
|
||||
rank.toIntOption match {
|
||||
case Some(rank) => (None, CommandRank.withValueOpt(rank))
|
||||
case None => (None, None)
|
||||
}
|
||||
case (Some(target), Some(rank)) =>
|
||||
// picking other targets is not supported for now
|
||||
(None, None)
|
||||
case (Some(rank), None) =>
|
||||
rank.toIntOption match {
|
||||
case Some(rank) => (None, CommandRank.withValueOpt(rank))
|
||||
case None => (None, None)
|
||||
}
|
||||
case _ => (None, None)
|
||||
}
|
||||
(target, rank) match {
|
||||
case (_, Some(rank)) =>
|
||||
avatarActor ! AvatarActor.SetCep(rank.experience)
|
||||
sessionActor ! SessionActor.SendResponse(message.copy(contents = "@AckSuccessSetCommandRank"))
|
||||
case _ =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
|
||||
)
|
||||
}
|
||||
setCommandRank(message, contents, session)
|
||||
|
||||
case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
|
||||
contents.toIntOption match {
|
||||
|
|
@ -1031,20 +954,20 @@ class ChatActor(
|
|||
if (session.player.silenced) {
|
||||
sessionActor ! SessionActor.SetSilenced(false)
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.UNK_229, true, "", "@silence_off", None)
|
||||
ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_off", None)
|
||||
)
|
||||
if (!silenceTimer.isCancelled) silenceTimer.cancel()
|
||||
} else {
|
||||
sessionActor ! SessionActor.SetSilenced(true)
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.UNK_229, true, "", "@silence_on", None)
|
||||
ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_on", None)
|
||||
)
|
||||
silenceTimer = context.system.scheduler.scheduleOnce(
|
||||
time minutes,
|
||||
() => {
|
||||
sessionActor ! SessionActor.SetSilenced(false)
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.UNK_229, true, "", "@silence_timeout", None)
|
||||
ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_timeout", None)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -1186,7 +1109,7 @@ class ChatActor(
|
|||
if (contents.startsWith("!whitetext ") && gmCommandAllowed) {
|
||||
chatService ! ChatService.Message(
|
||||
session,
|
||||
ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None),
|
||||
ChatMsg(UNK_227, wideContents=true, "", contents.replace("!whitetext ", ""), None),
|
||||
ChatChannel.Default()
|
||||
)
|
||||
true
|
||||
|
|
@ -1350,6 +1273,13 @@ class ChatActor(
|
|||
case _ =>
|
||||
false
|
||||
}
|
||||
} else if (contents.startsWith("!progress")) {
|
||||
if (!session.account.gm && BattleRank.withExperience(session.avatar.bep).value < Config.app.game.promotion.maxBattleRank + 1) {
|
||||
setBattleRank(message, contents, session, AvatarActor.Progress)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false // unknown ! commands are ignored
|
||||
}
|
||||
|
|
@ -1357,4 +1287,66 @@ class ChatActor(
|
|||
false // unknown ! commands are ignored
|
||||
}
|
||||
}
|
||||
|
||||
def setBattleRank(
|
||||
message: ChatMsg,
|
||||
contents: String,
|
||||
session: Session,
|
||||
msgFunc: Long => AvatarActor.Command
|
||||
): Unit = {
|
||||
val buffer = contents.toLowerCase.split("\\s+")
|
||||
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
|
||||
case (Some(target), Some(rank)) if target == session.avatar.name =>
|
||||
rank.toIntOption match {
|
||||
case Some(rank) => (None, BattleRank.withValueOpt(rank))
|
||||
case None => (None, None)
|
||||
}
|
||||
case (Some(_), Some(_)) =>
|
||||
// picking other targets is not supported for now
|
||||
(None, None)
|
||||
case (Some(rank), None) =>
|
||||
rank.toIntOption match {
|
||||
case Some(rank) => (None, BattleRank.withValueOpt(rank))
|
||||
case None => (None, None)
|
||||
}
|
||||
case _ => (None, None)
|
||||
}
|
||||
(target, rank) match {
|
||||
case (_, Some(rank)) if rank.value <= Config.app.game.maxBattleRank =>
|
||||
avatarActor ! msgFunc(rank.experience)
|
||||
case _ =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def setCommandRank(message: ChatMsg, contents: String, session: Session): Unit = {
|
||||
val buffer = contents.toLowerCase.split("\\s+")
|
||||
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
|
||||
case (Some(target), Some(rank)) if target == session.avatar.name =>
|
||||
rank.toIntOption match {
|
||||
case Some(rank) => (None, CommandRank.withValueOpt(rank))
|
||||
case None => (None, None)
|
||||
}
|
||||
case (Some(_), Some(_)) =>
|
||||
// picking other targets is not supported for now
|
||||
(None, None)
|
||||
case (Some(rank), None) =>
|
||||
rank.toIntOption match {
|
||||
case Some(rank) => (None, CommandRank.withValueOpt(rank))
|
||||
case None => (None, None)
|
||||
}
|
||||
case _ => (None, None)
|
||||
}
|
||||
(target, rank) match {
|
||||
case (_, Some(rank)) =>
|
||||
avatarActor ! AvatarActor.SetCep(rank.experience)
|
||||
//sessionActor ! SessionActor.SendResponse(message.copy(contents = "@AckSuccessSetCommandRank"))
|
||||
case _ =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ package net.psforever.actors.session.support
|
|||
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.objects.avatar.SpecialCarry
|
||||
import net.psforever.objects.serverobject.llu.CaptureFlag
|
||||
import net.psforever.packet.game.objectcreate.ConstructorData
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.objects.zones.exp
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
|
@ -225,8 +228,6 @@ class SessionAvatarHandlers(
|
|||
case AvatarResponse.DestroyDisplay(killer, victim, method, unk)
|
||||
if killer.CharId == avatar.id && killer.Faction != victim.Faction =>
|
||||
sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk))
|
||||
//TODO Temporary thing that should go somewhere else and use proper xp values
|
||||
// avatarActor ! AvatarActor.AwardCep((100 * Config.app.game.cepRate).toLong)
|
||||
|
||||
case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
|
||||
// guid = victim // killer = killer
|
||||
|
|
@ -398,6 +399,80 @@ class SessionAvatarHandlers(
|
|||
case AvatarResponse.UpdateKillsDeathsAssists(_, kda) =>
|
||||
avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
|
||||
|
||||
case AvatarResponse.AwardBep(charId, bep, expType) =>
|
||||
if (charId == player.CharId) {
|
||||
avatarActor ! AvatarActor.AwardBep(bep, expType)
|
||||
}
|
||||
|
||||
case AvatarResponse.AwardCep(charId, cep) =>
|
||||
//if the target player, always award (some) CEP
|
||||
if (charId == player.CharId) {
|
||||
avatarActor ! AvatarActor.AwardCep(cep)
|
||||
}
|
||||
|
||||
case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) =>
|
||||
//must be in a squad to earn experience
|
||||
val cepConfig = Config.app.game.experience.cep
|
||||
val charId = player.CharId
|
||||
val squadUI = sessionData.squad.squadUI
|
||||
val participation = continent
|
||||
.Building(buildingId)
|
||||
.map { building =>
|
||||
building.Participation.PlayerContribution()
|
||||
}
|
||||
squadUI
|
||||
.find { _._1 == charId }
|
||||
.collect {
|
||||
case (_, elem) if elem.index == 0 =>
|
||||
//squad leader earns CEP, modified by squad effort, capped by squad size present during the capture
|
||||
val squadParticipation = participation match {
|
||||
case Some(map) => map.filter { case (id, _) => squadUI.contains(id) }
|
||||
case _ => Map.empty[Long, Float]
|
||||
}
|
||||
val maxCepBySquadSize: Long = {
|
||||
val maxCepList = cepConfig.maximumPerSquadSize
|
||||
val squadSize: Int = squadParticipation.size
|
||||
maxCepList.lift(squadSize - 1).getOrElse(squadSize * maxCepList.head).toLong
|
||||
}
|
||||
val groupContribution: Float = squadUI
|
||||
.map { case (id, _) => (id, squadParticipation.getOrElse(id, 0f) / 10f) }
|
||||
.values
|
||||
.max
|
||||
val modifiedExp: Long = (cep.toFloat * groupContribution).toLong
|
||||
val cappedModifiedExp: Long = math.min(modifiedExp, maxCepBySquadSize)
|
||||
val finalExp: Long = if (modifiedExp > cappedModifiedExp) {
|
||||
val overLimitOverflow = if (cepConfig.squadSizeLimitOverflow == -1) {
|
||||
cep.toFloat
|
||||
} else {
|
||||
cepConfig.squadSizeLimitOverflow.toFloat
|
||||
}
|
||||
cappedModifiedExp + (overLimitOverflow * cepConfig.squadSizeLimitOverflowMultiplier).toLong
|
||||
} else {
|
||||
cappedModifiedExp
|
||||
}
|
||||
exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, finalExp, expType="cep")
|
||||
avatarActor ! AvatarActor.AwardCep(finalExp)
|
||||
Some(finalExp)
|
||||
|
||||
case _ =>
|
||||
//squad member earns BEP based on CEP, modified by personal effort
|
||||
val individualContribution = {
|
||||
val contributionList = for {
|
||||
facilityMap <- participation
|
||||
if facilityMap.contains(charId)
|
||||
} yield facilityMap(charId)
|
||||
if (contributionList.nonEmpty) {
|
||||
contributionList.max
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
val modifiedExp = (cep * individualContribution).toLong
|
||||
exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, modifiedExp, expType="bep")
|
||||
avatarActor ! AvatarActor.AwardFacilityCaptureBep(modifiedExp)
|
||||
Some(modifiedExp)
|
||||
}
|
||||
|
||||
case AvatarResponse.SendResponse(msg) =>
|
||||
sendResponse(msg)
|
||||
|
||||
|
|
@ -412,16 +487,35 @@ class SessionAvatarHandlers(
|
|||
case AvatarResponse.Killed(mount) =>
|
||||
//log and chat messages
|
||||
val cause = player.LastDamage.flatMap { damage =>
|
||||
damage.interaction.cause match {
|
||||
case cause: ExplodingEntityReason if cause.entity.isInstanceOf[VehicleSpawnPad] =>
|
||||
val interaction = damage.interaction
|
||||
val reason = interaction.cause
|
||||
val adversarial = interaction.adversarial.map { _.attacker }
|
||||
reason match {
|
||||
case r: ExplodingEntityReason if r.entity.isInstanceOf[VehicleSpawnPad] =>
|
||||
//also, @SVCP_Killed_TooCloseToPadOnCreate^n~ or "... within n meters of pad ..."
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SVCP_Killed_OnPadOnCreate"))
|
||||
case _ => ()
|
||||
}
|
||||
damage match {
|
||||
case damage if damage.adversarial.nonEmpty => Some(damage.adversarial.get.attacker.Name)
|
||||
case damage => Some(s"a ${damage.interaction.cause.getClass.getSimpleName}")
|
||||
adversarial.collect {
|
||||
case attacker
|
||||
if player.Carrying.contains(SpecialCarry.CaptureFlag) &&
|
||||
attacker.Faction != player.Faction &&
|
||||
sessionData
|
||||
.specialItemSlotGuid
|
||||
.flatMap { continent.GUID }
|
||||
.collect {
|
||||
case llu: CaptureFlag =>
|
||||
System.currentTimeMillis() - llu.LastCollectionTime > Config.app.game.experience.cep.lluSlayerCreditDuration.toMillis
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
.contains(true) =>
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
attacker.Name,
|
||||
AvatarAction.AwardCep(attacker.CharId, Config.app.game.experience.cep.lluSlayerCredit)
|
||||
)
|
||||
}
|
||||
adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") }
|
||||
}.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" }
|
||||
log.info(s"${player.Name} has died, killed by $cause")
|
||||
if (sessionData.shooting.shotsWhileDead > 0) {
|
||||
|
|
@ -434,6 +528,7 @@ class SessionAvatarHandlers(
|
|||
sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
|
||||
|
||||
//player state changes
|
||||
AvatarActor.updateToolDischargeFor(avatar)
|
||||
player.FreeHand.Equipment.foreach { item =>
|
||||
DropEquipmentFromInventory(player)(item)
|
||||
}
|
||||
|
|
@ -448,7 +543,6 @@ class SessionAvatarHandlers(
|
|||
}
|
||||
sessionData.playerActionsToCancel()
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sessionData.zoning
|
||||
AvatarActor.savePlayerLocation(player)
|
||||
sessionData.zoning.spawn.shiftPosition = Some(player.Position)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package net.psforever.actors.session.support
|
|||
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
|
||||
import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation}
|
||||
|
||||
import scala.collection.mutable
|
||||
|
|
@ -411,10 +411,10 @@ class SessionData(
|
|||
/* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */
|
||||
if (
|
||||
(session.account.gm ||
|
||||
(player.avatar.vehicle.contains(objectGuid) && vehicle.Owner.contains(player.GUID)) ||
|
||||
(player.avatar.vehicle.contains(objectGuid) && vehicle.OwnerGuid.contains(player.GUID)) ||
|
||||
(player.Faction == vehicle.Faction &&
|
||||
(vehicle.Definition.CanBeOwned.nonEmpty &&
|
||||
(vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Destroyed))) &&
|
||||
(vehicle.OwnerGuid.isEmpty || continent.GUID(vehicle.OwnerGuid.get).isEmpty) || vehicle.Destroyed))) &&
|
||||
(vehicle.MountedIn.isEmpty || !vehicle.Seats.values.exists(_.isOccupied))
|
||||
) {
|
||||
vehicle.Actor ! Vehicle.Deconstruct()
|
||||
|
|
@ -442,7 +442,7 @@ class SessionData(
|
|||
}
|
||||
|
||||
case Some(obj: Deployable) =>
|
||||
if (session.account.gm || obj.Owner.isEmpty || obj.Owner.contains(player.GUID) || obj.Destroyed) {
|
||||
if (session.account.gm || obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) {
|
||||
obj.Actor ! Deployable.Deconstruct()
|
||||
} else {
|
||||
log.warn(s"RequestDestroy: ${player.Name} must own the deployable in order to deconstruct it")
|
||||
|
|
@ -1377,7 +1377,7 @@ class SessionData(
|
|||
//access to trunk
|
||||
if (
|
||||
obj.AccessingTrunk.isEmpty &&
|
||||
(!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.Owner
|
||||
(!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid
|
||||
.contains(player.GUID))
|
||||
) {
|
||||
log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk")
|
||||
|
|
@ -2458,9 +2458,9 @@ class SessionData(
|
|||
src: PlanetSideGameObject with TelepadLike,
|
||||
dest: PlanetSideGameObject with TelepadLike
|
||||
): Unit = {
|
||||
val time = System.nanoTime
|
||||
val time = System.currentTimeMillis()
|
||||
if (
|
||||
time - recentTeleportAttempt > (2 seconds).toNanos && router.DeploymentState == DriveState.Deployed &&
|
||||
time - recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
|
||||
internalTelepad.Active &&
|
||||
remoteTelepad.Active
|
||||
) {
|
||||
|
|
@ -2473,6 +2473,11 @@ class SessionData(
|
|||
continent.id,
|
||||
LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid)
|
||||
)
|
||||
val vSource = VehicleSource(router)
|
||||
val zoneNumber = continent.Number
|
||||
player.LogActivity(VehicleMountActivity(vSource, PlayerSource(player), zoneNumber))
|
||||
player.Position = dest.Position
|
||||
player.LogActivity(VehicleDismountActivity(vSource, PlayerSource(player), zoneNumber))
|
||||
} else {
|
||||
log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
package net.psforever.actors.session.support
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.vital.InGameHistory
|
||||
|
||||
import scala.concurrent.duration._
|
||||
//
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
|
|
@ -160,7 +163,7 @@ class SessionMountHandlers(
|
|||
s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating"
|
||||
)
|
||||
|
||||
case Mountable.CanMount(obj: PlanetSideGameObject with WeaponTurret, seatNumber, _) =>
|
||||
case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}")
|
||||
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
|
||||
|
|
@ -246,7 +249,7 @@ class SessionMountHandlers(
|
|||
VehicleAction.KickPassenger(tplayer.GUID, seat_num, unk2=true, obj.GUID)
|
||||
)
|
||||
|
||||
case Mountable.CanDismount(obj: PlanetSideGameObject with WeaponTurret, seatNum, _) =>
|
||||
case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) =>
|
||||
log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}")
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
|
||||
|
|
@ -276,7 +279,7 @@ class SessionMountHandlers(
|
|||
* @param obj the mountable object
|
||||
* @param seatNum the mount into which the player is mounting
|
||||
*/
|
||||
def MountingAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = {
|
||||
def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
val playerGuid: PlanetSideGUID = tplayer.GUID
|
||||
val objGuid: PlanetSideGUID = obj.GUID
|
||||
sessionData.playerActionsToCancel()
|
||||
|
|
@ -295,7 +298,7 @@ class SessionMountHandlers(
|
|||
* @param obj the mountable object
|
||||
* @param seatNum the mount out of which which the player is disembarking
|
||||
*/
|
||||
def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = {
|
||||
def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
//until vehicles maintain synchronized momentum without a driver
|
||||
obj match {
|
||||
|
|
@ -332,8 +335,9 @@ class SessionMountHandlers(
|
|||
* @param obj the mountable object
|
||||
* @param seatNum the mount out of which which the player is disembarking
|
||||
*/
|
||||
def DismountAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = {
|
||||
def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
val playerGuid: PlanetSideGUID = tplayer.GUID
|
||||
tplayer.ContributionFrom(obj)
|
||||
sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
|
||||
val bailType = if (tplayer.BailProtection) {
|
||||
BailType.Bailed
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
package net.psforever.actors.session.support
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.objects.sourcing.AmenitySource
|
||||
import net.psforever.objects.vital.TerminalUsedActivity
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
//
|
||||
|
|
@ -31,7 +34,8 @@ class SessionTerminalHandlers(
|
|||
val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt
|
||||
continent.GUID(terminalGuid) match {
|
||||
case Some(term: Terminal) if lastTerminalOrderFulfillment =>
|
||||
log.info(s"${player.Name} is submitting an order - $transactionType of $itemName")
|
||||
val msg: String = if (itemName.nonEmpty) s" of $itemName" else ""
|
||||
log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg")
|
||||
lastTerminalOrderFulfillment = false
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||
term.Actor ! Terminal.Request(player, pkt)
|
||||
|
|
@ -68,8 +72,8 @@ class SessionTerminalHandlers(
|
|||
order match {
|
||||
case Terminal.BuyEquipment(item)
|
||||
if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty =>
|
||||
lastTerminalOrderFulfillment = true
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
|
||||
lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.BuyEquipment(item) =>
|
||||
avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
|
||||
|
|
@ -141,6 +145,7 @@ class SessionTerminalHandlers(
|
|||
if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
|
||||
sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid))
|
||||
}
|
||||
player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type))
|
||||
}.orElse {
|
||||
log.error(
|
||||
s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
package net.psforever.actors.session.support
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.objects.zones.exp.ToDatabase
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
|
|
@ -43,7 +45,8 @@ private[support] class WeaponAndProjectileOperations(
|
|||
var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start
|
||||
private[support] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]()
|
||||
private[support] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]()
|
||||
private var ongoingShotsFired: Int = 0
|
||||
private val shotsFired: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]()
|
||||
private val shotsLanded: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]()
|
||||
private[support] var shotsWhileDead: Int = 0
|
||||
private val projectiles: Array[Option[Projectile]] =
|
||||
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
|
||||
|
|
@ -157,7 +160,7 @@ private[support] class WeaponAndProjectileOperations(
|
|||
fireStateStopPlayerMessages(item_guid)
|
||||
case Some(_) =>
|
||||
fireStateStopMountedMessages(item_guid)
|
||||
case _ => ()
|
||||
case _ =>
|
||||
log.warn(s"ChangeFireState_Stop: can not find $item_guid")
|
||||
}
|
||||
sessionData.progressBarUpdate.cancel()
|
||||
|
|
@ -275,8 +278,8 @@ private[support] class WeaponAndProjectileOperations(
|
|||
val LongRangeProjectileInfoMessage(guid, _, _) = pkt
|
||||
FindContainedWeapon match {
|
||||
case (Some(_: Vehicle), weapons)
|
||||
if weapons.exists { _.GUID == guid } => ; //now what?
|
||||
case _ => ;
|
||||
if weapons.exists { _.GUID == guid } => () //now what?
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,13 +332,11 @@ private[support] class WeaponAndProjectileOperations(
|
|||
_: Vector3,
|
||||
hitPos: Vector3
|
||||
) =>
|
||||
ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos) match {
|
||||
case Some(resprojectile) =>
|
||||
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
|
||||
sessionData.handleDealingDamage(target, resprojectile)
|
||||
case None => ;
|
||||
ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
|
||||
addShotsLanded(resprojectile.cause.attribution, shots = 1)
|
||||
sessionData.handleDealingDamage(target, resprojectile)
|
||||
}
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
case None =>
|
||||
log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found")
|
||||
|
|
@ -368,26 +369,22 @@ private[support] class WeaponAndProjectileOperations(
|
|||
sessionData.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match {
|
||||
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
|
||||
CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target)
|
||||
ResolveProjectileInteraction(projectile, resolution1, target, target.Position) match {
|
||||
case Some(resprojectile) =>
|
||||
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
|
||||
sessionData.handleDealingDamage(target, resprojectile)
|
||||
case None => ;
|
||||
ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile =>
|
||||
addShotsLanded(resprojectile.cause.attribution, shots = 1)
|
||||
sessionData.handleDealingDamage(target, resprojectile)
|
||||
}
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
//other victims
|
||||
targets.foreach(elem => {
|
||||
sessionData.validObject(elem.uid, decorator = "SplashHit/other_victims") match {
|
||||
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
|
||||
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
|
||||
ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos) match {
|
||||
case Some(resprojectile) =>
|
||||
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
|
||||
sessionData.handleDealingDamage(target, resprojectile)
|
||||
case None => ;
|
||||
ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile =>
|
||||
addShotsLanded(resprojectile.cause.attribution, shots = 1)
|
||||
sessionData.handleDealingDamage(target, resprojectile)
|
||||
}
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
})
|
||||
//...
|
||||
|
|
@ -413,7 +410,7 @@ private[support] class WeaponAndProjectileOperations(
|
|||
continent.Projectile ! ZoneProjectile.Remove(projectile.GUID)
|
||||
}
|
||||
}
|
||||
case None => ;
|
||||
case None => ()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -422,13 +419,12 @@ private[support] class WeaponAndProjectileOperations(
|
|||
sessionData.validObject(victim_guid, decorator = "Lash") match {
|
||||
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
|
||||
CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target)
|
||||
ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos) match {
|
||||
case Some(resprojectile) =>
|
||||
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
|
||||
ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos).foreach {
|
||||
resprojectile =>
|
||||
addShotsLanded(resprojectile.cause.attribution, shots = 1)
|
||||
sessionData.handleDealingDamage(target, resprojectile)
|
||||
case None => ;
|
||||
}
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -524,7 +520,7 @@ private[support] class WeaponAndProjectileOperations(
|
|||
obj match {
|
||||
case turret: FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret =>
|
||||
turret.Actor ! FacilityTurret.WeaponDischarged()
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
|
|
@ -532,7 +528,7 @@ private[support] class WeaponAndProjectileOperations(
|
|||
)
|
||||
}
|
||||
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -552,7 +548,7 @@ private[support] class WeaponAndProjectileOperations(
|
|||
case Some(v: Vehicle) =>
|
||||
//assert subsystem states
|
||||
v.SubsystemMessages().foreach { sendResponse }
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
if (enabledTools.nonEmpty) {
|
||||
|
|
@ -577,8 +573,9 @@ private[support] class WeaponAndProjectileOperations(
|
|||
avatarActor ! AvatarActor.ConsumeStamina(avatar.stamina)
|
||||
}
|
||||
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
|
||||
tool.Discharge()
|
||||
prefire += weaponGUID
|
||||
ongoingShotsFired = ongoingShotsFired + tool.Discharge()
|
||||
addShotsFired(tool.Definition.ObjectId, tool.AmmoSlot.Chamber)
|
||||
}
|
||||
(o, Some(tool))
|
||||
}
|
||||
|
|
@ -635,7 +632,7 @@ private[support] class WeaponAndProjectileOperations(
|
|||
continent.GUID(weapon_guid) match {
|
||||
case Some(tool: Tool) =>
|
||||
EmptyMagazine(weapon_guid, tool)
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -719,19 +716,17 @@ private[support] class WeaponAndProjectileOperations(
|
|||
*/
|
||||
def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = {
|
||||
ModifyAmmunition(obj)(box, reloadValue)
|
||||
obj.Find(box) match {
|
||||
case Some(index) =>
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
s"${obj.Actor}",
|
||||
VehicleAction.InventoryState(
|
||||
player.GUID,
|
||||
box,
|
||||
obj.GUID,
|
||||
index,
|
||||
box.Definition.Packet.DetailedConstructorData(box).get
|
||||
)
|
||||
obj.Find(box).collect { index =>
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
s"${obj.Actor}",
|
||||
VehicleAction.InventoryState(
|
||||
player.GUID,
|
||||
box,
|
||||
obj.GUID,
|
||||
index,
|
||||
box.Definition.Packet.DetailedConstructorData(box).get
|
||||
)
|
||||
case None => ;
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -849,7 +844,7 @@ private[support] class WeaponAndProjectileOperations(
|
|||
sessionData.normalItemDrop(player, continent)(previousBox)
|
||||
}
|
||||
AmmoBox.Split(previousBox) match {
|
||||
case Nil | List(_) => ; //done (the former case is technically not possible)
|
||||
case Nil | List(_) => () //done (the former case is technically not possible)
|
||||
case _ :: toUpdate =>
|
||||
modifyFunc(previousBox, 0) //update to changed capacity value
|
||||
toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) })
|
||||
|
|
@ -1151,7 +1146,6 @@ private[support] class WeaponAndProjectileOperations(
|
|||
prefire -= itemGuid
|
||||
shooting += itemGuid
|
||||
shootingStart += itemGuid -> System.currentTimeMillis()
|
||||
ongoingShotsFired = 0
|
||||
}
|
||||
|
||||
private def fireStateStartChargeMode(tool: Tool): Unit = {
|
||||
|
|
@ -1220,11 +1214,10 @@ private[support] class WeaponAndProjectileOperations(
|
|||
used by ChangeFireStateMessage_Stop handling
|
||||
*/
|
||||
private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = {
|
||||
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(tool.Definition.ObjectId, ongoingShotsFired, 0, 0))
|
||||
tool.FireMode match {
|
||||
case _: ChargeFireModeDefinition =>
|
||||
sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine))
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
if (tool.Magazine == 0) {
|
||||
FireCycleCleanup(tool)
|
||||
|
|
@ -1365,6 +1358,49 @@ private[support] class WeaponAndProjectileOperations(
|
|||
)
|
||||
}
|
||||
|
||||
private def addShotsFired(weaponId: Int, shots: Int): Unit = {
|
||||
addShotsToMap(shotsFired, weaponId, shots)
|
||||
}
|
||||
|
||||
private def addShotsLanded(weaponId: Int, shots: Int): Unit = {
|
||||
addShotsToMap(shotsLanded, weaponId, shots)
|
||||
}
|
||||
|
||||
private def addShotsToMap(map: mutable.HashMap[Int, Int], weaponId: Int, shots: Int): Unit = {
|
||||
map.put(
|
||||
weaponId,
|
||||
map.get(weaponId) match {
|
||||
case Some(previousShots) => previousShots + shots
|
||||
case None => shots
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private[support] def reportOngoingShots(reportFunc: (Long, Int, Int, Int) => Unit): Unit = {
|
||||
reportOngoingShots(player.CharId, reportFunc)
|
||||
}
|
||||
|
||||
private[support] def reportOngoingShots(avatarId: Long, reportFunc: (Long, Int, Int, Int) => Unit): Unit = {
|
||||
//only shots that have been reported as fired count
|
||||
//if somehow shots had reported as landed but never reported as fired, they are ignored
|
||||
//these are just raw counts; there's only numeric connection between the entries of fired and of landed
|
||||
shotsFired.foreach { case (weaponId, fired) =>
|
||||
val landed = math.min(shotsLanded.getOrElse(weaponId, 0), fired)
|
||||
reportFunc(avatarId, weaponId, fired, landed)
|
||||
}
|
||||
shotsFired.clear()
|
||||
shotsLanded.clear()
|
||||
}
|
||||
|
||||
//noinspection ScalaUnusedSymbol
|
||||
private[support] def reportOngoingShotsToAvatar(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = {
|
||||
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(weaponId, fired, landed, 0, 0))
|
||||
}
|
||||
|
||||
private[support] def reportOngoingShotsToDatabase(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = {
|
||||
ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0))
|
||||
}
|
||||
|
||||
override protected[session] def stop(): Unit = {
|
||||
if (player != null && player.HasGUID) {
|
||||
(prefire ++ shooting).foreach { guid =>
|
||||
|
|
@ -1373,6 +1409,7 @@ private[support] class WeaponAndProjectileOperations(
|
|||
fireStateStopMountedMessages(guid)
|
||||
}
|
||||
projectiles.indices.foreach { projectiles.update(_, None) }
|
||||
reportOngoingShots(reportOngoingShotsToDatabase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@ import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
|
|||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import net.psforever.login.WorldSession
|
||||
import net.psforever.objects.avatar.BattleRank
|
||||
import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, SessionStatistics}
|
||||
import net.psforever.objects.inventory.InventoryItem
|
||||
import net.psforever.objects.serverobject.mount.Seat
|
||||
import net.psforever.objects.serverobject.tube.SpawnTube
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
|
||||
import net.psforever.objects.vital.{InGameHistory, ReconstructionActivity, SpawningActivity}
|
||||
import net.psforever.packet.game.{CampaignStatistic, MailMessage, SessionStatistic}
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
|
|
@ -69,6 +72,30 @@ import net.psforever.zones.Zones
|
|||
|
||||
object ZoningOperations {
|
||||
private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20)
|
||||
|
||||
def reportProgressionSystem(sessionActor: ActorRef): Unit = {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
MailMessage(
|
||||
"High Command",
|
||||
"Progress versus Promotion",
|
||||
"If you consider yourself as a veteran soldier, despite looking so green, please read this.\n" ++
|
||||
"You only have this opportunity while you are battle rank 1." ++
|
||||
"\n\n" ++
|
||||
"The normal method of rank advancement comes from progress on the battlefield - fighting enemies, helping allies, and capturing facilities. " ++
|
||||
"\n\n" ++
|
||||
s"You may, however, rapidly promote yourself to at most battle rank ${Config.app.game.promotion.maxBattleRank}. " ++
|
||||
"You have access to all of the normal benefits, certification points, implants, etc., of your chosen rank. " ++
|
||||
"However, that experience that you have skipped will count as PROMOTION DEBT. " ++
|
||||
"You will not advance any further until you earn that experience back through support activity and engaging in facility capture. " ++
|
||||
"The amount of experience required and your own effort will determine how long it takes. " ++
|
||||
"In addition, you will be ineligible of having your command experience be recognized during this time." ++
|
||||
"\n\n" ++
|
||||
"If you wish to continue, set your desired battle rank now - use '!progress' followed by a battle rank index. " ++
|
||||
"If you accept, but it becomes too much of burden, you may ask to revert to battle rank 2 at any time. " ++
|
||||
"Your normal sense of progress will be restored."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ZoningOperations(
|
||||
|
|
@ -258,7 +285,7 @@ class ZoningOperations(
|
|||
obj.GUID,
|
||||
Deployable.Icon(obj.Definition.Item),
|
||||
obj.Position,
|
||||
obj.Owner.getOrElse(PlanetSideGUID(0))
|
||||
obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
|
||||
)
|
||||
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
|
||||
})
|
||||
|
|
@ -1657,6 +1684,8 @@ class ZoningOperations(
|
|||
private[support] var reviveTimer: Cancellable = Default.Cancellable
|
||||
private[support] var respawnTimer: Cancellable = Default.Cancellable
|
||||
|
||||
private var statisticsPacketFunc: () => Unit = loginAvatarStatisticsFields
|
||||
|
||||
/* packets */
|
||||
|
||||
def handleReleaseAvatarRequest(pkt: ReleaseAvatarRequestMessage): Unit = {
|
||||
|
|
@ -1803,6 +1832,7 @@ class ZoningOperations(
|
|||
sessionData.persistFunc = UpdatePersistence(from)
|
||||
//tell the old WorldSessionActor to kill itself by using its own subscriptions against itself
|
||||
inZone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.TeardownConnection())
|
||||
spawn.switchAvatarStatisticsFieldToRefreshAfterRespawn()
|
||||
//find and reload previous player
|
||||
(
|
||||
inZone.Players.find(p => p.name.equals(name)),
|
||||
|
|
@ -2342,6 +2372,7 @@ class ZoningOperations(
|
|||
player.avatar = player.avatar.copy(stamina = avatar.maxStamina)
|
||||
avatarActor ! AvatarActor.RestoreStamina(avatar.maxStamina)
|
||||
avatarActor ! AvatarActor.ResetImplants()
|
||||
zones.exp.ToDatabase.reportRespawns(tplayer.CharId, ScoreCard.reviveCount(player.avatar.scorecard.CurrentLife))
|
||||
val obj = Player.Respawn(tplayer)
|
||||
DefinitionUtil.applyDefaultLoadout(obj)
|
||||
obj.death_by = tplayer.death_by
|
||||
|
|
@ -2614,10 +2645,11 @@ class ZoningOperations(
|
|||
LoadZoneAsPlayer(newPlayer, zoneId)
|
||||
} else {
|
||||
avatarActor ! AvatarActor.DeactivateActiveImplants()
|
||||
val betterSpawnPoint = physSpawnPoint.collect { case o: PlanetSideGameObject with FactionAffinity with InGameHistory => o }
|
||||
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
|
||||
case Some(vehicle: Vehicle) => // driver or passenger in vehicle using a warp gate, or a droppod
|
||||
InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, toSpawnPoint)
|
||||
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
|
||||
InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, betterSpawnPoint)
|
||||
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
|
||||
LoadZoneInVehicle(vehicle, pos, ori, zoneId)
|
||||
|
||||
case _ if player.HasGUID => // player is deconstructing self or instant action
|
||||
|
|
@ -2629,13 +2661,13 @@ class ZoningOperations(
|
|||
)
|
||||
player.Position = pos
|
||||
player.Orientation = ori
|
||||
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
|
||||
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
|
||||
LoadZoneAsPlayer(player, zoneId)
|
||||
|
||||
case _ => //player is logging in
|
||||
player.Position = pos
|
||||
player.Orientation = ori
|
||||
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
|
||||
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
|
||||
LoadZoneAsPlayer(player, zoneId)
|
||||
}
|
||||
}
|
||||
|
|
@ -2803,10 +2835,7 @@ class ZoningOperations(
|
|||
.foreach { case (_, building) =>
|
||||
sendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0 /*building.BuildingType == StructureType.Facility*/))
|
||||
}
|
||||
(0 to 30).foreach(_ => {
|
||||
//TODO 30 for a new character only?
|
||||
sendResponse(AvatarStatisticsMessage(DeathStatistic(0L)))
|
||||
})
|
||||
statisticsPacketFunc()
|
||||
if (tplayer.ExoSuit == ExoSuitType.MAX) {
|
||||
sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong))
|
||||
}
|
||||
|
|
@ -2823,7 +2852,7 @@ class ZoningOperations(
|
|||
continent.DeployableList
|
||||
.filter(_.OwnerName.contains(name))
|
||||
.foreach(obj => {
|
||||
obj.Owner = guid
|
||||
obj.OwnerGuid = guid
|
||||
drawDeloyableIcon(obj)
|
||||
})
|
||||
drawDeloyableIcon = DontRedrawIcons
|
||||
|
|
@ -2831,7 +2860,7 @@ class ZoningOperations(
|
|||
//assert or transfer vehicle ownership
|
||||
continent.GUID(player.avatar.vehicle) match {
|
||||
case Some(vehicle: Vehicle) if vehicle.OwnerName.contains(tplayer.Name) =>
|
||||
vehicle.Owner = guid
|
||||
vehicle.OwnerGuid = guid
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
s"${tplayer.Faction}",
|
||||
VehicleAction.Ownership(guid, vehicle.GUID)
|
||||
|
|
@ -2906,7 +2935,7 @@ class ZoningOperations(
|
|||
val effortBy = nextSpawnPoint
|
||||
.collect { case sp: SpawnTube => (sp, continent.GUID(sp.Owner.GUID)) }
|
||||
.collect {
|
||||
case (_, Some(v: Vehicle)) => continent.GUID(v.Owner)
|
||||
case (_, Some(v: Vehicle)) => continent.GUID(v.OwnerGuid)
|
||||
case (sp, Some(_: Building)) => Some(sp)
|
||||
}
|
||||
.collect { case Some(thing: PlanetSideGameObject with FactionAffinity) => Some(SourceEntry(thing)) }
|
||||
|
|
@ -2918,11 +2947,15 @@ class ZoningOperations(
|
|||
SpawningActivity(PlayerSource(player), continent.Number, effortBy)
|
||||
}
|
||||
})
|
||||
//ride
|
||||
|
||||
}
|
||||
upstreamMessageCount = 0
|
||||
setAvatar = true
|
||||
if (
|
||||
BattleRank.withExperience(tplayer.avatar.bep).value == 1 &&
|
||||
Config.app.game.promotion.active &&
|
||||
!account.gm) {
|
||||
ZoningOperations.reportProgressionSystem(context.self)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2961,7 +2994,7 @@ class ZoningOperations(
|
|||
obj.GUID,
|
||||
Deployable.Icon(obj.Definition.Item),
|
||||
obj.Position,
|
||||
obj.Owner.getOrElse(PlanetSideGUID(0))
|
||||
obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
|
||||
)
|
||||
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
|
||||
}
|
||||
|
|
@ -3164,6 +3197,64 @@ class ZoningOperations(
|
|||
sendResponse(mountPointStatusMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the Character Info window's statistics page during login.
|
||||
* Always send campaign (historical, total) statistics.
|
||||
* Set to refresh the statistics fields after each respawn from now on.
|
||||
*/
|
||||
private def loginAvatarStatisticsFields(): Unit = {
|
||||
avatar.scorecard.KillStatistics.foreach { case (id, stat) =>
|
||||
val campaign = CampaignStatistics(stat)
|
||||
val elem = StatisticalElement.fromId(id)
|
||||
sendResponse(AvatarStatisticsMessage(
|
||||
CampaignStatistic(StatisticalCategory.Destroyed, elem, campaign.tr, campaign.nc, campaign.vs, campaign.ps)
|
||||
))
|
||||
}
|
||||
//originally the client sent a death statistic update in between each change of statistic categories, about 30 times
|
||||
sendResponse(AvatarStatisticsMessage(DeathStatistic(ScoreCard.deathCount(avatar.scorecard))))
|
||||
statisticsPacketFunc = respawnAvatarStatisticsFields
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the Character Info window's statistics page after each respawn.
|
||||
* Check whether to send session-related data, or campaign-related data, or both.
|
||||
*/
|
||||
private def respawnAvatarStatisticsFields(): Unit = {
|
||||
avatar
|
||||
.scorecard
|
||||
.KillStatistics
|
||||
.flatMap { case (id, stat) =>
|
||||
val campaign = CampaignStatistics(stat)
|
||||
val session = SessionStatistics(stat)
|
||||
(StatisticalElement.fromId(id), campaign.total, campaign, session.total, session) match {
|
||||
case (elem, 0, _, _, session) =>
|
||||
Seq(SessionStatistic(StatisticalCategory.Destroyed, elem, session.tr, session.nc, session.vs, session.ps))
|
||||
case (elem, _, campaign, 0, _) =>
|
||||
Seq(CampaignStatistic(StatisticalCategory.Destroyed, elem, campaign.tr, campaign.nc, campaign.vs, campaign.ps))
|
||||
case (elem, _, campaign, _, session) =>
|
||||
Seq(
|
||||
CampaignStatistic(StatisticalCategory.Destroyed, elem, campaign.tr, campaign.nc, campaign.vs, campaign.ps),
|
||||
SessionStatistic(StatisticalCategory.Destroyed, elem, session.tr, session.nc, session.vs, session.ps)
|
||||
)
|
||||
}
|
||||
}
|
||||
.foreach { statistics =>
|
||||
sendResponse(AvatarStatisticsMessage(statistics))
|
||||
}
|
||||
//originally the client sent a death statistic update in between each change of statistic categories, about 30 times
|
||||
sendResponse(AvatarStatisticsMessage(DeathStatistic(ScoreCard.deathCount(avatar.scorecard))))
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessible method to switch population of the Character Info window's statistics page
|
||||
* from whatever it currently js to after each respawn.
|
||||
* At the time of "login", only campaign (total, historical) deaths are reported for convenience.
|
||||
* At the time of "respawn", all fields - campaign and session - should be reported if applicable.
|
||||
*/
|
||||
def switchAvatarStatisticsFieldToRefreshAfterRespawn(): Unit = {
|
||||
statisticsPacketFunc = respawnAvatarStatisticsFields
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't extract the award advancement information from a player character upon respawning or zoning.
|
||||
* You only need to perform that population once at login.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import net.psforever.objects.avatar.scoring.Kill
|
|||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.sourcing.SourceEntry
|
||||
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
|
||||
import net.psforever.objects.zones.exp.ExperienceCalculator
|
||||
import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator}
|
||||
import net.psforever.util.Database._
|
||||
import net.psforever.persistence
|
||||
|
||||
|
|
@ -87,6 +87,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
|||
private[this] val log = org.log4s.getLogger
|
||||
private val players: mutable.ListBuffer[Player] = mutable.ListBuffer()
|
||||
private val experience: ActorRef[ExperienceCalculator.Command] = context.spawnAnonymous(ExperienceCalculator(zone))
|
||||
private val supportExperience: ActorRef[SupportExperienceCalculator.Command] = context.spawnAnonymous(SupportExperienceCalculator(zone))
|
||||
|
||||
zone.actor = context.self
|
||||
zone.init(context.toClassic)
|
||||
|
|
@ -154,7 +155,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
|||
experience ! ExperienceCalculator.RewardThisDeath(entity)
|
||||
|
||||
case RewardOurSupporters(target, history, kill, bep) =>
|
||||
()
|
||||
supportExperience ! SupportExperienceCalculator.RewardOurSupporters(target, history, kill, bep)
|
||||
|
||||
case ZoneMapUpdate() =>
|
||||
zone.Buildings
|
||||
|
|
@ -162,7 +163,6 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
|||
.values
|
||||
.foreach(_.Actor ! BuildingActor.MapUpdate())
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -942,8 +942,9 @@ object WorldSession {
|
|||
result: Boolean
|
||||
): Unit = {
|
||||
if (result) {
|
||||
player.Zone.GUID(guid).collect {
|
||||
case term: Terminal => player.LogActivity(TerminalUsedActivity(AmenitySource(term), transaction))
|
||||
player.Zone.GUID(guid).collect { case term: Terminal =>
|
||||
player.LogActivity(TerminalUsedActivity(AmenitySource(term), transaction))
|
||||
player.ContributionFrom(term)
|
||||
}
|
||||
}
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ class BoomerDeployableControl(mine: BoomerDeployable)
|
|||
|
||||
override def loseOwnership(faction: PlanetSideEmpire.Value): Unit = {
|
||||
super.loseOwnership(PlanetSideEmpire.NEUTRAL)
|
||||
mine.OwnerName = None
|
||||
val guid = mine.OwnerGuid
|
||||
mine.AssignOwnership(None)
|
||||
mine.OwnerGuid = guid
|
||||
}
|
||||
|
||||
override def gainOwnership(player: Player): Unit = {
|
||||
|
|
|
|||
|
|
@ -89,8 +89,7 @@ object Deployables {
|
|||
.foreach { p =>
|
||||
p.Actor ! Player.LoseDeployable(target)
|
||||
}
|
||||
target.Owner = None
|
||||
target.OwnerName = None
|
||||
target.AssignOwnership(None)
|
||||
}
|
||||
events ! LocalServiceMessage(
|
||||
s"${target.Faction}",
|
||||
|
|
@ -119,7 +118,7 @@ object Deployables {
|
|||
.collect {
|
||||
case Some(obj: Deployable) =>
|
||||
obj.Actor ! Deployable.Ownership(None)
|
||||
obj.Owner = None //fast-forward the effect
|
||||
obj.OwnerGuid = None //fast-forward the effect
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ object MineDeployableControl {
|
|||
jumping = false,
|
||||
ExoSuitDefinition.Select(exosuit, faction),
|
||||
bep = 0,
|
||||
kills = Nil,
|
||||
progress = PlayerSource.Nobody.progress,
|
||||
UniquePlayer(charId, name, CharacterSex.Male, mine.Faction)
|
||||
)
|
||||
case None =>
|
||||
|
|
|
|||
|
|
@ -9847,6 +9847,7 @@ object GlobalDefinitions {
|
|||
resource_silo.Damageable = false
|
||||
resource_silo.Repairable = false
|
||||
resource_silo.MaxNtuCapacitor = 1000
|
||||
resource_silo.ChargeTime = 105.seconds //from 0-100% in roughly 105s on live (~20%-100% https://youtu.be/veOWToR2nSk?t=1402)
|
||||
|
||||
capture_terminal.Name = "capture_terminal"
|
||||
capture_terminal.Damageable = false
|
||||
|
|
@ -9856,7 +9857,7 @@ object GlobalDefinitions {
|
|||
secondary_capture.Name = "secondary_capture"
|
||||
secondary_capture.Damageable = false
|
||||
secondary_capture.Repairable = false
|
||||
secondary_capture.FacilityHackTime = 1.nanosecond
|
||||
secondary_capture.FacilityHackTime = 1.millisecond
|
||||
|
||||
vanu_control_console.Name = "vanu_control_console"
|
||||
vanu_control_console.Damageable = false
|
||||
|
|
|
|||
|
|
@ -1,45 +1,33 @@
|
|||
// Copyright (c) 2019 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
trait OwnableByPlayer {
|
||||
private var owner: Option[PlanetSideGUID] = None
|
||||
private var ownerName: Option[String] = None
|
||||
private var originalOwnerName: Option[String] = None
|
||||
private var owner: Option[UniquePlayer] = None
|
||||
private var ownerGuid: Option[PlanetSideGUID] = None
|
||||
private var originalOwnerName: Option[String] = None
|
||||
|
||||
def Owner: Option[PlanetSideGUID] = owner
|
||||
def Owners: Option[UniquePlayer] = owner
|
||||
|
||||
def Owner_=(owner: PlanetSideGUID): Option[PlanetSideGUID] = Owner_=(Some(owner))
|
||||
def OwnerGuid: Option[PlanetSideGUID] = ownerGuid
|
||||
|
||||
def Owner_=(owner: Player): Option[PlanetSideGUID] = Owner_=(Some(owner.GUID))
|
||||
def OwnerGuid_=(owner: PlanetSideGUID): Option[PlanetSideGUID] = OwnerGuid_=(Some(owner))
|
||||
|
||||
def Owner_=(owner: Option[PlanetSideGUID]): Option[PlanetSideGUID] = {
|
||||
def OwnerGuid_=(owner: Player): Option[PlanetSideGUID] = OwnerGuid_=(Some(owner.GUID))
|
||||
|
||||
def OwnerGuid_=(owner: Option[PlanetSideGUID]): Option[PlanetSideGUID] = {
|
||||
owner match {
|
||||
case Some(_) =>
|
||||
this.owner = owner
|
||||
ownerGuid = owner
|
||||
case None =>
|
||||
this.owner = None
|
||||
ownerGuid = None
|
||||
}
|
||||
Owner
|
||||
OwnerGuid
|
||||
}
|
||||
|
||||
def OwnerName: Option[String] = ownerName
|
||||
|
||||
def OwnerName_=(owner: String): Option[String] = OwnerName_=(Some(owner))
|
||||
|
||||
def OwnerName_=(owner: Player): Option[String] = OwnerName_=(Some(owner.Name))
|
||||
|
||||
def OwnerName_=(owner: Option[String]): Option[String] = {
|
||||
owner match {
|
||||
case Some(_) =>
|
||||
ownerName = owner
|
||||
originalOwnerName = originalOwnerName.orElse(owner)
|
||||
case None =>
|
||||
ownerName = None
|
||||
}
|
||||
OwnerName
|
||||
}
|
||||
def OwnerName: Option[String] = owner.map { _.name }
|
||||
|
||||
def OriginalOwnerName: Option[String] = originalOwnerName
|
||||
|
||||
|
|
@ -56,14 +44,30 @@ trait OwnableByPlayer {
|
|||
* @return na
|
||||
*/
|
||||
def AssignOwnership(playerOpt: Option[Player]): OwnableByPlayer = {
|
||||
playerOpt match {
|
||||
case Some(player) =>
|
||||
Owner = player
|
||||
OwnerName = player
|
||||
case None =>
|
||||
Owner = None
|
||||
OwnerName = None
|
||||
(originalOwnerName, playerOpt) match {
|
||||
case (None, Some(player)) =>
|
||||
owner = Some(PlayerSource(player).unique)
|
||||
originalOwnerName = originalOwnerName.orElse { Some(player.Name) }
|
||||
OwnerGuid = player
|
||||
case (_, Some(player)) =>
|
||||
owner = Some(PlayerSource(player).unique)
|
||||
OwnerGuid = player
|
||||
case (_, None) =>
|
||||
owner = None
|
||||
OwnerGuid = None
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param ownable na
|
||||
* @return na
|
||||
*/
|
||||
def AssignOwnership(ownable: OwnableByPlayer): OwnableByPlayer = {
|
||||
owner = ownable.owner
|
||||
originalOwnerName = originalOwnerName.orElse { ownable.originalOwnerName }
|
||||
OwnerGuid = ownable.OwnerGuid
|
||||
this
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.aura.AuraContainer
|
|||
import net.psforever.objects.serverobject.environment.InteractWithEnvironment
|
||||
import net.psforever.objects.serverobject.mount.MountableEntity
|
||||
import net.psforever.objects.vital.resistance.ResistanceProfile
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFromEquipment, Vitality}
|
||||
import net.psforever.objects.vital.damage.DamageProfile
|
||||
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
||||
|
|
@ -110,6 +110,7 @@ class Player(var avatar: Avatar)
|
|||
Health = Definition.DefaultHealth
|
||||
Armor = MaxArmor
|
||||
Capacitor = 0
|
||||
avatar.scorecard.respawn()
|
||||
released = false
|
||||
}
|
||||
isAlive
|
||||
|
|
@ -124,13 +125,16 @@ class Player(var avatar: Avatar)
|
|||
def Revive: Boolean = {
|
||||
Destroyed = false
|
||||
Health = Definition.DefaultHealth
|
||||
avatar.scorecard.revive()
|
||||
released = false
|
||||
true
|
||||
}
|
||||
|
||||
def Release: Boolean = {
|
||||
released = true
|
||||
backpack = !isAlive
|
||||
if (!released) {
|
||||
released = true
|
||||
backpack = !isAlive
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
|
@ -422,12 +426,13 @@ class Player(var avatar: Avatar)
|
|||
def UsingSpecial_=(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
|
||||
usingSpecial(state)
|
||||
|
||||
//noinspection ScalaUnusedSymbol
|
||||
private def DefaultUsingSpecial(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
|
||||
SpecialExoSuitDefinition.Mode.Normal
|
||||
|
||||
private def UsingAnchorsOrOverdrive(
|
||||
state: SpecialExoSuitDefinition.Mode.Value
|
||||
): SpecialExoSuitDefinition.Mode.Value = {
|
||||
state: SpecialExoSuitDefinition.Mode.Value
|
||||
): SpecialExoSuitDefinition.Mode.Value = {
|
||||
import SpecialExoSuitDefinition.Mode._
|
||||
val curr = UsingSpecial
|
||||
val next = if (curr == Normal) {
|
||||
|
|
@ -532,11 +537,14 @@ class Player(var avatar: Avatar)
|
|||
|
||||
def Carrying: Option[SpecialCarry] = carrying
|
||||
|
||||
//noinspection ScalaUnusedSymbol
|
||||
def Carrying_=(item: SpecialCarry): Option[SpecialCarry] = {
|
||||
Carrying
|
||||
Carrying_=(Some(item))
|
||||
}
|
||||
|
||||
//noinspection ScalaUnusedSymbol
|
||||
def Carrying_=(item: Option[SpecialCarry]): Option[SpecialCarry] = {
|
||||
carrying = item
|
||||
Carrying
|
||||
}
|
||||
|
||||
|
|
@ -553,6 +561,14 @@ class Player(var avatar: Avatar)
|
|||
|
||||
def DamageModel: DamageResistanceModel = exosuit.asInstanceOf[DamageResistanceModel]
|
||||
|
||||
override def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = {
|
||||
val earliestEndTime = System.currentTimeMillis() - duration
|
||||
History.collect {
|
||||
case heal: HealFromEquipment if heal.amount > 0 && heal.time > earliestEndTime => heal
|
||||
case repair: RepairFromEquipment if repair.amount > 0 && repair.time > earliestEndTime => repair
|
||||
}
|
||||
}
|
||||
|
||||
def canEqual(other: Any): Boolean = other.isInstanceOf[Player]
|
||||
|
||||
override def equals(other: Any): Boolean =
|
||||
|
|
@ -614,12 +630,14 @@ object Player {
|
|||
if (player.Release) {
|
||||
val obj = new Player(player.avatar)
|
||||
obj.Continent = player.Continent
|
||||
obj.avatar.scorecard.respawn()
|
||||
obj
|
||||
} else {
|
||||
player
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection ScalaUnusedSymbol
|
||||
def neverRestrict(player: Player, slot: Int): Boolean = {
|
||||
false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import net.psforever.objects.equipment.EquipmentSlot
|
|||
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
||||
import net.psforever.objects.inventory.InventoryItem
|
||||
import net.psforever.objects.loadouts.InfantryLoadout
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.vital.RevivingActivity
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3}
|
||||
|
|
@ -64,7 +66,7 @@ object Players {
|
|||
val name = target.Name
|
||||
val medicName = medic.Name
|
||||
log.info(s"$medicName had revived $name")
|
||||
//target.History(PlayerRespawn(PlayerSource(target), target.Zone, target.Position, Some(PlayerSource(medic))))
|
||||
target.LogActivity(RevivingActivity(PlayerSource(target), PlayerSource(medic), target.MaxHealth, item.Definition))
|
||||
val magazine = item.Discharge(Some(25))
|
||||
target.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||
medicName,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package net.psforever.objects
|
|||
|
||||
import akka.actor.{Actor, ActorContext, Props}
|
||||
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployableCategory}
|
||||
import net.psforever.objects.definition.DeployableDefinition
|
||||
import net.psforever.objects.definition.{DeployableDefinition, WithShields}
|
||||
import net.psforever.objects.definition.converter.ShieldGeneratorConverter
|
||||
import net.psforever.objects.equipment.{JammableBehavior, JammableUnit}
|
||||
import net.psforever.objects.serverobject.damage.Damageable.Target
|
||||
|
|
@ -22,7 +22,8 @@ class ShieldGeneratorDeployable(cdef: ShieldGeneratorDefinition)
|
|||
with Hackable
|
||||
with JammableUnit
|
||||
|
||||
class ShieldGeneratorDefinition extends DeployableDefinition(240) {
|
||||
class ShieldGeneratorDefinition extends DeployableDefinition(240)
|
||||
with WithShields {
|
||||
Packet = new ShieldGeneratorConverter
|
||||
DeployCategory = DeployableCategory.ShieldGenerators
|
||||
|
||||
|
|
|
|||
|
|
@ -112,11 +112,10 @@ object SpecialEmp {
|
|||
faction: PlanetSideEmpire.Value
|
||||
): (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = {
|
||||
distanceCheck(new PlanetSideServerObject with OwnableByPlayer {
|
||||
Owner = Some(owner.GUID)
|
||||
OwnerName = owner match {
|
||||
case p: Player => p.Name
|
||||
case o: OwnableByPlayer => o.OwnerName.getOrElse("")
|
||||
case _ => ""
|
||||
owner match {
|
||||
case p: Player => AssignOwnership(p)
|
||||
case o: OwnableByPlayer => AssignOwnership(o)
|
||||
case _ => OwnerGuid_=(Some(owner.GUID))
|
||||
}
|
||||
Position = position
|
||||
def Faction = faction
|
||||
|
|
|
|||
|
|
@ -103,8 +103,7 @@ class TelepadDeployableControl(tpad: TelepadDeployable)
|
|||
override def startOwnerlessDecay(): Unit = {
|
||||
//telepads do not decay when they become ownerless
|
||||
//telepad decay is tied to their lifecycle with routers
|
||||
tpad.Owner = None
|
||||
tpad.OwnerName = None
|
||||
tpad.AssignOwnership(None)
|
||||
}
|
||||
|
||||
override def finalizeDeployable(callback: ActorRef): Unit = {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class Tool(private val toolDef: ToolDefinition)
|
|||
}
|
||||
|
||||
def Discharge(rounds: Option[Int] = None): Int = {
|
||||
lastDischarge = System.nanoTime()
|
||||
lastDischarge = System.currentTimeMillis()
|
||||
Magazine = FireMode.Discharge(this, rounds)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
|
|||
interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20))
|
||||
|
||||
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
private var previousFaction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
private var shields: Int = 0
|
||||
private var decal: Int = 0
|
||||
private var trunkAccess: Option[PlanetSideGUID] = None
|
||||
|
|
@ -128,14 +129,18 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
|
|||
}
|
||||
|
||||
def Faction: PlanetSideEmpire.Value = {
|
||||
this.faction
|
||||
}
|
||||
|
||||
override def Faction_=(faction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
|
||||
this.faction = faction
|
||||
faction
|
||||
}
|
||||
|
||||
override def Faction_=(toFaction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
|
||||
//TODO in the future, this may create an issue when the vehicle is originally or is hacked from Black Ops
|
||||
previousFaction = faction
|
||||
faction = toFaction
|
||||
toFaction
|
||||
}
|
||||
|
||||
def PreviousFaction: PlanetSideEmpire.Value = previousFaction
|
||||
|
||||
/** How long it takes to jack the vehicle in seconds, based on the hacker's certification level */
|
||||
def JackingDuration: Array[Int] = Definition.JackingDuration
|
||||
|
||||
|
|
@ -267,30 +272,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
|
|||
}
|
||||
|
||||
def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = {
|
||||
if (seatNumber == 0) { //valid in almost all cases
|
||||
Some(AccessPermissionGroup.Driver)
|
||||
} else {
|
||||
Seat(seatNumber) match {
|
||||
case Some(_) =>
|
||||
Definition.controlledWeapons().get(seatNumber) match {
|
||||
case Some(_) =>
|
||||
Some(AccessPermissionGroup.Gunner)
|
||||
case None =>
|
||||
Some(AccessPermissionGroup.Passenger)
|
||||
}
|
||||
case None =>
|
||||
CargoHold(seatNumber) match {
|
||||
case Some(_) =>
|
||||
Some(AccessPermissionGroup.Passenger) //TODO confirm this
|
||||
case None =>
|
||||
if (seatNumber >= trunk.Offset && seatNumber < trunk.Offset + trunk.TotalCapacity) {
|
||||
Some(AccessPermissionGroup.Trunk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vehicles.SeatPermissionGroup(this.Definition, seatNumber)
|
||||
}
|
||||
|
||||
def Utilities: Map[Int, Utility] = utilities
|
||||
|
|
@ -358,9 +340,9 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
|
|||
}
|
||||
}
|
||||
|
||||
override def DeployTime = Definition.DeployTime
|
||||
override def DeployTime: Int = Definition.DeployTime
|
||||
|
||||
override def UndeployTime = Definition.UndeployTime
|
||||
override def UndeployTime: Int = Definition.UndeployTime
|
||||
|
||||
def Inventory: GridInventory = trunk
|
||||
|
||||
|
|
@ -476,7 +458,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
|
|||
if (trunkAccess.isEmpty || trunkAccess.contains(player.GUID)) {
|
||||
groupPermissions(3) match {
|
||||
case VehicleLockState.Locked => //only the owner
|
||||
Owner.isEmpty || (Owner.isDefined && player.GUID == Owner.get)
|
||||
OwnerGuid.isEmpty || (OwnerGuid.isDefined && player.GUID == OwnerGuid.get)
|
||||
case VehicleLockState.Group => //anyone in the owner's squad or platoon
|
||||
faction == player.Faction //TODO this is not correct
|
||||
case VehicleLockState.Empire => //anyone of the owner's faction
|
||||
|
|
@ -518,7 +500,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
|
|||
|
||||
def PreviousGatingManifest(): Option[VehicleManifest] = previousVehicleGatingManifest
|
||||
|
||||
def DamageModel = Definition.asInstanceOf[DamageResistanceModel]
|
||||
def DamageModel: DamageResistanceModel = Definition.asInstanceOf[DamageResistanceModel]
|
||||
|
||||
override def BailProtection_=(protect: Boolean): Boolean = {
|
||||
!Definition.CanFly && super.BailProtection_=(protect)
|
||||
|
|
@ -681,6 +663,6 @@ object Vehicle {
|
|||
*/
|
||||
def toString(obj: Vehicle): String = {
|
||||
val occupancy = obj.Seats.values.count(seat => seat.isOccupied)
|
||||
s"${obj.Definition.Name}, owned by ${obj.Owner}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)"
|
||||
s"${obj.Definition.Name}, owned by ${obj.OwnerGuid}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.ce.TelepadLike
|
||||
import net.psforever.objects.definition.VehicleDefinition
|
||||
import net.psforever.objects.serverobject.CommonMessages
|
||||
import net.psforever.objects.serverobject.deploy.Deployment
|
||||
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
||||
|
|
@ -61,7 +62,7 @@ object Vehicles {
|
|||
* `None`, otherwise
|
||||
*/
|
||||
def Disown(guid: PlanetSideGUID, vehicle: Vehicle): Option[Vehicle] =
|
||||
vehicle.Zone.GUID(vehicle.Owner) match {
|
||||
vehicle.Zone.GUID(vehicle.OwnerGuid) match {
|
||||
case Some(player: Player) =>
|
||||
if (player.avatar.vehicle.contains(guid)) {
|
||||
player.avatar.vehicle = None
|
||||
|
|
@ -127,7 +128,7 @@ object Vehicles {
|
|||
*/
|
||||
def Disown(player: Player, vehicle: Vehicle): Option[Vehicle] = {
|
||||
val pguid = player.GUID
|
||||
if (vehicle.Owner.contains(pguid)) {
|
||||
if (vehicle.OwnerGuid.contains(pguid)) {
|
||||
vehicle.AssignOwnership(None)
|
||||
//vehicle.Zone.VehicleEvents ! VehicleServiceMessage(player.Name, VehicleAction.Ownership(pguid, PlanetSideGUID(0)))
|
||||
val vguid = vehicle.GUID
|
||||
|
|
@ -236,16 +237,14 @@ object Vehicles {
|
|||
val zone = target.Zone
|
||||
// Forcefully dismount any cargo
|
||||
target.CargoHolds.foreach { case (_, cargoHold) =>
|
||||
cargoHold.occupant match {
|
||||
case Some(cargo: Vehicle) =>
|
||||
cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false)
|
||||
case None => ;
|
||||
cargoHold.occupant.collect {
|
||||
cargo: Vehicle => cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false)
|
||||
}
|
||||
}
|
||||
// Forcefully dismount all seated occupants from the vehicle
|
||||
target.Seats.values.foreach(seat => {
|
||||
seat.occupant match {
|
||||
case Some(tplayer: Player) =>
|
||||
seat.occupant.collect {
|
||||
tplayer: Player =>
|
||||
seat.unmount(tplayer)
|
||||
tplayer.VehicleSeated = None
|
||||
if (tplayer.HasGUID) {
|
||||
|
|
@ -254,7 +253,6 @@ object Vehicles {
|
|||
VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, target.GUID)
|
||||
)
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
})
|
||||
// If the vehicle can fly and is flying deconstruct it, and well played to whomever managed to hack a plane in mid air. I'm impressed.
|
||||
|
|
@ -263,25 +261,17 @@ object Vehicles {
|
|||
target.Actor ! Vehicle.Deconstruct()
|
||||
} else { // Otherwise handle ownership transfer as normal
|
||||
// Remove ownership of our current vehicle, if we have one
|
||||
hacker.avatar.vehicle match {
|
||||
case Some(guid: PlanetSideGUID) =>
|
||||
zone.GUID(guid) match {
|
||||
case Some(vehicle: Vehicle) =>
|
||||
Vehicles.Disown(hacker, vehicle)
|
||||
case _ => ;
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
target.Owner match {
|
||||
case Some(previousOwnerGuid: PlanetSideGUID) =>
|
||||
// Remove ownership of the vehicle from the previous player
|
||||
zone.GUID(previousOwnerGuid) match {
|
||||
case Some(tplayer: Player) =>
|
||||
Vehicles.Disown(tplayer, target)
|
||||
case _ => ; // Vehicle already has no owner
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
hacker.avatar.vehicle
|
||||
.flatMap { guid => zone.GUID(guid) }
|
||||
.collect { case vehicle: Vehicle =>
|
||||
Vehicles.Disown(hacker, vehicle)
|
||||
}
|
||||
// Remove ownership of the vehicle from the previous player
|
||||
target.OwnerGuid
|
||||
.flatMap { guid => zone.GUID(guid) }
|
||||
.collect { case tplayer: Player =>
|
||||
Vehicles.Disown(tplayer, target)
|
||||
}
|
||||
// Now take ownership of the jacked vehicle
|
||||
target.Actor ! CommonMessages.Hack(hacker, target)
|
||||
target.Faction = hacker.Faction
|
||||
|
|
@ -301,16 +291,15 @@ object Vehicles {
|
|||
// If AMS is deployed, swap it to the new faction
|
||||
target.Definition match {
|
||||
case GlobalDefinitions.router =>
|
||||
target.Utility(UtilityType.internal_router_telepad_deployable) match {
|
||||
case Some(util: Utility.InternalTelepad) =>
|
||||
target.Utility(UtilityType.internal_router_telepad_deployable).collect {
|
||||
case util: Utility.InternalTelepad =>
|
||||
//"power cycle"
|
||||
util.Actor ! TelepadLike.Deactivate(util)
|
||||
util.Actor ! TelepadLike.Activate(util)
|
||||
case _ => ;
|
||||
}
|
||||
case GlobalDefinitions.ams if target.DeploymentState == DriveState.Deployed =>
|
||||
zone.VehicleEvents ! VehicleServiceMessage.AMSDeploymentChange(zone)
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -411,6 +400,7 @@ object Vehicles {
|
|||
*
|
||||
* @param vehicle the vehicle
|
||||
*/
|
||||
//noinspection ScalaUnusedSymbol
|
||||
def BeforeUnloadVehicle(vehicle: Vehicle, zone: Zone): Unit = {
|
||||
vehicle.Definition match {
|
||||
case GlobalDefinitions.ams =>
|
||||
|
|
@ -419,7 +409,7 @@ object Vehicles {
|
|||
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
|
||||
case GlobalDefinitions.router =>
|
||||
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -439,4 +429,49 @@ object Vehicles {
|
|||
val turnAway = if (offset.x >= 0) -90f else 90f
|
||||
(obj.Position + offset, (shuttleAngle + turnAway) % 360f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on a mounting index, for a certain mount, to what mounting group does this seat belong?
|
||||
* @param vehicle the vehicle
|
||||
* @param seatNumber specific seat index
|
||||
* @return the seat group designation
|
||||
*/
|
||||
def SeatPermissionGroup(vehicle: Vehicle, seatNumber: Int): Option[AccessPermissionGroup.Value] = {
|
||||
SeatPermissionGroup(vehicle.Definition, seatNumber)
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on a mounting index, for a certain mount, to what mounting group does this seat belong?
|
||||
* @param definition global vehicle specification
|
||||
* @param seatNumber specific seat index
|
||||
* @return the seat group designation
|
||||
*/
|
||||
def SeatPermissionGroup(definition: VehicleDefinition, seatNumber: Int): Option[AccessPermissionGroup.Value] = {
|
||||
if (seatNumber == 0) { //valid in almost all cases
|
||||
Some(AccessPermissionGroup.Driver)
|
||||
} else {
|
||||
definition.Seats
|
||||
.get(seatNumber)
|
||||
.map { _ =>
|
||||
definition.controlledWeapons()
|
||||
.get(seatNumber)
|
||||
.map { _ => AccessPermissionGroup.Gunner }
|
||||
.getOrElse { AccessPermissionGroup.Passenger }
|
||||
}
|
||||
.orElse {
|
||||
definition.Cargo
|
||||
.get(seatNumber)
|
||||
.map { _ => AccessPermissionGroup.Passenger }
|
||||
.orElse {
|
||||
val offset = definition.TrunkOffset
|
||||
val size = definition.TrunkSize
|
||||
if (seatNumber >= offset && seatNumber < offset + size.Width * size.Height) {
|
||||
Some(AccessPermissionGroup.Trunk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import net.psforever.objects.locker.LockerContainerControl
|
|||
import net.psforever.objects.serverobject.environment._
|
||||
import net.psforever.objects.serverobject.repair.Repairable
|
||||
import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource}
|
||||
import net.psforever.objects.vital.collision.CollisionReason
|
||||
import net.psforever.objects.vital.environment.EnvironmentReason
|
||||
import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason}
|
||||
|
|
@ -356,6 +356,12 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
|
|||
}
|
||||
|
||||
case Terminal.TerminalMessage(_, msg, order) =>
|
||||
lazy val terminalUsedAction = {
|
||||
player.Zone.GUID(msg.terminal_guid).collect {
|
||||
case t: Terminal =>
|
||||
player.LogActivity(TerminalUsedActivity(AmenitySource(t), msg.transaction_type))
|
||||
}
|
||||
}
|
||||
order match {
|
||||
case Terminal.BuyExosuit(exosuit, subtype) =>
|
||||
val result = setExoSuit(exosuit, subtype)
|
||||
|
|
@ -366,6 +372,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
|
|||
player.Name,
|
||||
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result)
|
||||
)
|
||||
terminalUsedAction
|
||||
|
||||
case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) =>
|
||||
log.info(s"${player.Name} wants to change equipment loadout to their option #${msg.unk1 + 1}")
|
||||
|
|
@ -508,7 +515,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
|
|||
player.Name,
|
||||
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result=true)
|
||||
)
|
||||
case _ => assert(assertion=false, msg.toString)
|
||||
terminalUsedAction
|
||||
case _ =>
|
||||
assert(assertion=false, msg.toString)
|
||||
}
|
||||
|
||||
case Zone.Ground.ItemOnGround(item, _, _) =>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.avatar.scoring
|
||||
|
||||
final case class EquipmentStat(objectId: Int, shotsFired: Int, shotsLanded: Int, kills: Int)
|
||||
final case class EquipmentStat(objectId: Int, shotsFired: Int, shotsLanded: Int, kills: Int, assists: Int)
|
||||
|
||||
object EquipmentStat {
|
||||
def apply(objectId: Int): EquipmentStat = EquipmentStat(objectId, 0, 1, 0)
|
||||
def apply(objectId: Int): EquipmentStat = EquipmentStat(objectId, 0, 1, 0, 0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package net.psforever.objects.avatar.scoring
|
|||
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.vital.interaction.DamageResult
|
||||
import net.psforever.objects.zones.exp.EquipmentUseContextWrapper
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
trait KDAStat {
|
||||
|
|
@ -10,10 +11,29 @@ trait KDAStat {
|
|||
val time: LocalDateTime = LocalDateTime.now()
|
||||
}
|
||||
|
||||
final case class Kill(victim: PlayerSource, info: DamageResult, experienceEarned: Long) extends KDAStat
|
||||
final case class Kill(
|
||||
victim: PlayerSource,
|
||||
info: DamageResult,
|
||||
experienceEarned: Long
|
||||
) extends KDAStat
|
||||
|
||||
final case class Assist(victim: PlayerSource, weapons: Seq[Int], damageInflictedPercentage: Float, experienceEarned: Long) extends KDAStat
|
||||
final case class Assist(
|
||||
victim: PlayerSource,
|
||||
weapons: Seq[EquipmentUseContextWrapper],
|
||||
damageInflictedPercentage: Float,
|
||||
experienceEarned: Long
|
||||
) extends KDAStat
|
||||
|
||||
final case class Death(assailant: Seq[PlayerSource], timeAlive: Long, bep: Long) extends KDAStat {
|
||||
final case class Death(
|
||||
assailant: Seq[PlayerSource],
|
||||
timeAlive: Long,
|
||||
bep: Long
|
||||
) extends KDAStat {
|
||||
def experienceEarned: Long = 0
|
||||
}
|
||||
|
||||
final case class SupportActivity(
|
||||
target: PlayerSource,
|
||||
weapons: Seq[EquipmentUseContextWrapper],
|
||||
experienceEarned: Long
|
||||
) extends KDAStat
|
||||
|
|
|
|||
|
|
@ -5,11 +5,15 @@ final case class Life(
|
|||
kills: Seq[Kill],
|
||||
assists: Seq[Assist],
|
||||
death: Option[Death],
|
||||
equipmentStats: Seq[EquipmentStat]
|
||||
equipmentStats: Seq[EquipmentStat],
|
||||
supportExperience: Long,
|
||||
prior: Option[Life]
|
||||
)
|
||||
|
||||
object Life {
|
||||
def apply(): Life = Life(Nil, Nil, None, Nil)
|
||||
def apply(): Life = Life(Nil, Nil, None, Nil, 0, None)
|
||||
|
||||
def revive(prior: Life): Life = Life(Nil, Nil, None, Nil, 0, Some(prior))
|
||||
|
||||
def bep(life: Life): Long = {
|
||||
life.kills.foldLeft(0L)(_ + _.experienceEarned) + life.assists.foldLeft(0L)(_ + _.experienceEarned)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package net.psforever.objects.avatar.scoring
|
|||
|
||||
import net.psforever.objects.GlobalDefinitions
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
|
||||
import net.psforever.types.{PlanetSideEmpire, StatisticalCategory}
|
||||
import net.psforever.types.{PlanetSideEmpire, StatisticalCategory, StatisticalElement}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.mutable
|
||||
|
|
@ -18,43 +18,97 @@ class ScoreCard() {
|
|||
|
||||
def Lives: Seq[Life] = lives
|
||||
|
||||
def AllLives: Seq[Life] = curr +: lives
|
||||
|
||||
def Kills: Seq[Kill] = lives.flatMap { _.kills } ++ curr.kills
|
||||
|
||||
def KillStatistics: Map[Int, Statistic] = killStatistics.toMap
|
||||
|
||||
def AssistStatistics: Map[Int, Statistic] = assistStatistics.toMap
|
||||
|
||||
def rate(msg: Any): Unit = {
|
||||
def revive(): Unit = {
|
||||
curr = Life.revive(curr)
|
||||
}
|
||||
|
||||
def respawn(): Unit = {
|
||||
val death = curr
|
||||
curr = Life()
|
||||
lives = death +: lives
|
||||
}
|
||||
|
||||
def initStatisticForKill(targetId: Int, victimFaction: PlanetSideEmpire.Value): Statistic = {
|
||||
ScoreCard.initStatisticsFor(killStatistics, targetId, victimFaction)
|
||||
}
|
||||
|
||||
def rate(msg: Any): Seq[(Int, Statistic)] = {
|
||||
msg match {
|
||||
case e: EquipmentStat =>
|
||||
curr = ScoreCard.updateEquipmentStat(curr, e)
|
||||
Nil
|
||||
case k: Kill =>
|
||||
curr = curr.copy(kills = k +: curr.kills)
|
||||
curr = ScoreCard.updateEquipmentStat(curr, EquipmentStat(k.info.interaction.cause.attribution, 0, 0, 1))
|
||||
ScoreCard.updateStatisticsFor(killStatistics, k.info.interaction.cause.attribution, k.victim.Faction)
|
||||
//TODO may need to expand these to include other fields later
|
||||
curr = ScoreCard.updateEquipmentStat(curr, EquipmentStat(k.info.interaction.cause.attribution, 0, 0, 1, 0))
|
||||
val wid = StatisticalElement.relatedElement(k.victim.ExoSuit).value
|
||||
Seq((wid, ScoreCard.updateStatisticsFor(killStatistics, wid, k.victim.Faction)))
|
||||
case a: Assist =>
|
||||
curr = curr.copy(assists = a +: curr.assists)
|
||||
val faction = a.victim.Faction
|
||||
a.weapons.foreach { wid =>
|
||||
ScoreCard.updateStatisticsFor(assistStatistics, wid, faction)
|
||||
//TODO may need to expand these to include other fields later
|
||||
a.weapons.map { weq =>
|
||||
val wid = weq.equipment
|
||||
(wid, ScoreCard.updateStatisticsFor(assistStatistics, wid, faction))
|
||||
}
|
||||
case d: Death =>
|
||||
val expired = curr
|
||||
curr = Life()
|
||||
lives = expired.copy(death = Some(d)) +: lives
|
||||
case _ => ;
|
||||
curr = curr.copy(death = Some(d))
|
||||
Nil
|
||||
case value: Long =>
|
||||
curr = curr.copy(supportExperience = curr.supportExperience + value)
|
||||
Nil
|
||||
case _ =>
|
||||
Nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ScoreCard {
|
||||
def reviveCount(card: ScoreCard): Int = {
|
||||
reviveCount(card.CurrentLife)
|
||||
}
|
||||
|
||||
def reviveCount(life: Life): Int = {
|
||||
recursiveReviveCount(life, count = 0)
|
||||
}
|
||||
|
||||
def deathCount(card: ScoreCard): Int = {
|
||||
card.AllLives.foldLeft(0)(_ + deathCount(_))
|
||||
}
|
||||
|
||||
private def deathCount(life: Life): Int = {
|
||||
life.prior match {
|
||||
case None => if (life.death.nonEmpty) 1 else 0
|
||||
case Some(previousLife) => recursiveReviveCount(previousLife, count = 1)
|
||||
}
|
||||
}
|
||||
|
||||
@tailrec
|
||||
private def recursiveReviveCount(life: Life, count: Int): Int = {
|
||||
life.prior match {
|
||||
case None => count + 1
|
||||
case Some(previousLife) => recursiveReviveCount(previousLife, count + 1)
|
||||
}
|
||||
}
|
||||
|
||||
private def updateEquipmentStat(curr: Life, entry: EquipmentStat): Life = {
|
||||
updateEquipmentStat(curr, entry, entry.objectId, entry.kills)
|
||||
updateEquipmentStat(curr, entry, entry.objectId, entry.kills, entry.assists)
|
||||
}
|
||||
|
||||
private def updateEquipmentStat(
|
||||
curr: Life,
|
||||
entry: EquipmentStat,
|
||||
objectId: Int,
|
||||
killCount: Int
|
||||
killCount: Int,
|
||||
assists: Int
|
||||
): Life = {
|
||||
curr.equipmentStats.indexWhere { a => a.objectId == objectId } match {
|
||||
case -1 =>
|
||||
|
|
@ -72,6 +126,29 @@ object ScoreCard {
|
|||
}
|
||||
}
|
||||
|
||||
@tailrec
|
||||
private def initStatisticsFor(
|
||||
statisticMap: mutable.HashMap[Int, Statistic],
|
||||
objectId: Int,
|
||||
victimFaction: PlanetSideEmpire.Value
|
||||
): Statistic = {
|
||||
statisticMap.get(objectId) match {
|
||||
case Some(fields) =>
|
||||
val outEntry = victimFaction match {
|
||||
case PlanetSideEmpire.TR => fields.copy(tr_c = fields.tr_c + 1)
|
||||
case PlanetSideEmpire.NC => fields.copy(nc_c = fields.nc_c + 1)
|
||||
case PlanetSideEmpire.VS => fields.copy(vs_c = fields.vs_c + 1)
|
||||
case PlanetSideEmpire.NEUTRAL => fields.copy(ps_c = fields.ps_c + 1)
|
||||
}
|
||||
statisticMap.put(objectId, outEntry)
|
||||
outEntry
|
||||
case _ =>
|
||||
val out = Statistic(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
statisticMap.put(objectId, out)
|
||||
initStatisticsFor(statisticMap, objectId, victimFaction)
|
||||
}
|
||||
}
|
||||
|
||||
@tailrec
|
||||
private def updateStatisticsFor(
|
||||
statisticMap: mutable.HashMap[Int, Statistic],
|
||||
|
|
@ -81,10 +158,10 @@ object ScoreCard {
|
|||
statisticMap.get(objectId) match {
|
||||
case Some(fields) =>
|
||||
val outEntry = victimFaction match {
|
||||
case PlanetSideEmpire.TR => fields.copy(tr_b = fields.tr_b + 1)
|
||||
case PlanetSideEmpire.NC => fields.copy(nc_b = fields.nc_b + 1)
|
||||
case PlanetSideEmpire.VS => fields.copy(vs_b = fields.vs_b + 1)
|
||||
case PlanetSideEmpire.NEUTRAL => fields.copy(ps_b = fields.ps_b + 1)
|
||||
case PlanetSideEmpire.TR => fields.copy(tr_s = fields.tr_s + 1)
|
||||
case PlanetSideEmpire.NC => fields.copy(nc_s = fields.nc_s + 1)
|
||||
case PlanetSideEmpire.VS => fields.copy(vs_s = fields.vs_s + 1)
|
||||
case PlanetSideEmpire.NEUTRAL => fields.copy(ps_s = fields.ps_s + 1)
|
||||
}
|
||||
outEntry
|
||||
case _ =>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,35 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.avatar.scoring
|
||||
|
||||
final case class Statistic(tr_a: Int, tr_b: Int, nc_a: Int, nc_b: Int, vs_a: Int, vs_b: Int, ps_a: Int, ps_b: Int)
|
||||
/**
|
||||
* Organizes the eight fields one would find in an `AvatarServiceMessage` statistic field.
|
||||
* The `_c` fields and the `_s` fields are paired when the values populate the packet
|
||||
* where `c` stands for "campaign" and `s` stands for "session".
|
||||
* "Session" values reflect on the UI as the K in K/D
|
||||
* while "campaign" values reflect on the Character Info window, stats section.
|
||||
* @param tr_c terran republic campaign stat
|
||||
* @param tr_s terran republic session stat
|
||||
* @param nc_c new conglomerate campaign stat
|
||||
* @param nc_s new conglomerate session stat
|
||||
* @param vs_c vanu sovereignty campaign stat
|
||||
* @param vs_s vanu sovereignty session stat
|
||||
* @param ps_c generic faction campaign stat
|
||||
* @param ps_s generic faction session stat
|
||||
*/
|
||||
final case class Statistic(tr_c: Int, tr_s: Int, nc_c: Int, nc_s: Int, vs_c: Int, vs_s: Int, ps_c: Int, ps_s: Int)
|
||||
|
||||
final case class StatisticByContext(tr: Int, nc: Int, vs: Int, ps: Int) {
|
||||
def total: Int = tr + nc + vs + ps
|
||||
}
|
||||
|
||||
object CampaignStatistics {
|
||||
def apply(stat: Statistic): StatisticByContext = {
|
||||
StatisticByContext(stat.tr_c, stat.nc_c, stat.vs_c, stat.ps_c)
|
||||
}
|
||||
}
|
||||
|
||||
object SessionStatistics {
|
||||
def apply(stat: Statistic): StatisticByContext = {
|
||||
StatisticByContext(stat.tr_s, stat.nc_s, stat.vs_s, stat.ps_s)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,16 +66,16 @@ trait DeployableBehavior {
|
|||
finalizeDeployable(callback)
|
||||
|
||||
case Deployable.Ownership(None)
|
||||
if DeployableObject.Owner.nonEmpty =>
|
||||
if DeployableObject.OwnerGuid.nonEmpty =>
|
||||
val obj = DeployableObject
|
||||
if (constructed.contains(true)) {
|
||||
loseOwnership(obj.Faction)
|
||||
} else {
|
||||
obj.Owner = None
|
||||
obj.OwnerGuid = None
|
||||
}
|
||||
|
||||
case Deployable.Ownership(Some(player))
|
||||
if !DeployableObject.Destroyed && DeployableObject.Owner.isEmpty =>
|
||||
if !DeployableObject.Destroyed && DeployableObject.OwnerGuid.isEmpty =>
|
||||
if (constructed.contains(true)) {
|
||||
gainOwnership(player)
|
||||
} else {
|
||||
|
|
@ -132,12 +132,12 @@ trait DeployableBehavior {
|
|||
|
||||
def startOwnerlessDecay(): Unit = {
|
||||
val obj = DeployableObject
|
||||
if (obj.Owner.nonEmpty && decay.isCancelled) {
|
||||
if (obj.OwnerGuid.nonEmpty && decay.isCancelled) {
|
||||
//without an owner, this deployable should begin to decay and will deconstruct later
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
decay = context.system.scheduler.scheduleOnce(Deployable.decay, self, Deployable.Deconstruct())
|
||||
}
|
||||
obj.Owner = None //OwnerName should remain set
|
||||
obj.OwnerGuid = None //OwnerName should remain set
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -163,7 +163,7 @@ trait DeployableBehavior {
|
|||
val guid = obj.GUID
|
||||
val zone = obj.Zone
|
||||
val originalFaction = obj.Faction
|
||||
val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, obj.Owner.get)
|
||||
val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, obj.OwnerGuid.get)
|
||||
if (originalFaction != toFaction) {
|
||||
obj.Faction = toFaction
|
||||
val localEvents = zone.LocalEvents
|
||||
|
|
@ -199,7 +199,7 @@ trait DeployableBehavior {
|
|||
zone.LocalEvents ! LocalServiceMessage(
|
||||
zone.id,
|
||||
LocalAction.TriggerEffectLocation(
|
||||
obj.Owner.getOrElse(Service.defaultPlayerGUID),
|
||||
obj.OwnerGuid.getOrElse(Service.defaultPlayerGUID),
|
||||
"spawn_object_effect",
|
||||
obj.Position,
|
||||
obj.Orientation
|
||||
|
|
@ -234,7 +234,7 @@ trait DeployableBehavior {
|
|||
val obj = DeployableObject
|
||||
val zone = obj.Zone
|
||||
val localEvents = zone.LocalEvents
|
||||
val owner = obj.Owner.getOrElse(Service.defaultPlayerGUID)
|
||||
val owner = obj.OwnerGuid.getOrElse(Service.defaultPlayerGUID)
|
||||
obj.OwnerName match {
|
||||
case Some(_) =>
|
||||
case None =>
|
||||
|
|
@ -306,7 +306,9 @@ trait DeployableBehavior {
|
|||
if (!obj.Destroyed) {
|
||||
Deployables.AnnounceDestroyDeployable(obj)
|
||||
}
|
||||
obj.OwnerName = None
|
||||
val guid = obj.OwnerGuid
|
||||
obj.AssignOwnership(None)
|
||||
obj.OwnerGuid = guid
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,27 +26,8 @@ class VehicleDefinition(objectId: Int)
|
|||
with VitalityDefinition
|
||||
with NtuContainerDefinition
|
||||
with ResistanceProfileMutators
|
||||
with DamageResistanceModel {
|
||||
/** ... */
|
||||
var shieldUiAttribute: Int = 68
|
||||
/** how many points of shield the vehicle starts with (should default to 0 if unset through the accessor) */
|
||||
private var defaultShields : Option[Int] = None
|
||||
/** maximum vehicle shields (generally: 20% of health)
|
||||
* for normal vehicles, offered through amp station facility benefits
|
||||
* for BFR's, it charges naturally
|
||||
**/
|
||||
private var maxShields: Int = 0
|
||||
/** the minimum amount of time that must elapse in between damage and shield charge activities (ms) */
|
||||
private var shieldChargeDamageCooldown : Long = 5000L
|
||||
/** the minimum amount of time that must elapse in between distinct shield charge activities (ms) */
|
||||
private var shieldChargePeriodicCooldown : Long = 1000L
|
||||
/** if the shield recharges on its own, this value will be non-`None` and indicate by how much */
|
||||
private var autoShieldRecharge : Option[Int] = None
|
||||
private var autoShieldRechargeSpecial : Option[Int] = None
|
||||
/** shield drain is what happens to the shield under special conditions, e.g., bfr flight;
|
||||
* the drain interval is 250ms which is convenient for us
|
||||
* we can skip needing to define is explicitly */
|
||||
private var shieldDrain : Option[Int] = None
|
||||
with DamageResistanceModel
|
||||
with WithShields {
|
||||
private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]()
|
||||
private var deployment: Boolean = false
|
||||
private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap()
|
||||
|
|
@ -92,63 +73,6 @@ class VehicleDefinition(objectId: Int)
|
|||
RepairRestoresAt = 1
|
||||
registerAs = "vehicles"
|
||||
|
||||
def DefaultShields: Int = defaultShields.getOrElse(0)
|
||||
|
||||
def DefaultShields_=(shield: Int): Int = DefaultShields_=(Some(shield))
|
||||
|
||||
def DefaultShields_=(shield: Option[Int]): Int = {
|
||||
defaultShields = shield
|
||||
DefaultShields
|
||||
}
|
||||
|
||||
def MaxShields: Int = maxShields
|
||||
|
||||
def MaxShields_=(shields: Int): Int = {
|
||||
maxShields = shields
|
||||
MaxShields
|
||||
}
|
||||
|
||||
def ShieldPeriodicDelay : Long = shieldChargePeriodicCooldown
|
||||
|
||||
def ShieldPeriodicDelay_=(cooldown: Long): Long = {
|
||||
shieldChargePeriodicCooldown = cooldown
|
||||
ShieldPeriodicDelay
|
||||
}
|
||||
|
||||
def ShieldDamageDelay: Long = shieldChargeDamageCooldown
|
||||
|
||||
def ShieldDamageDelay_=(cooldown: Long): Long = {
|
||||
shieldChargeDamageCooldown = cooldown
|
||||
ShieldDamageDelay
|
||||
}
|
||||
|
||||
def ShieldAutoRecharge: Option[Int] = autoShieldRecharge
|
||||
|
||||
def ShieldAutoRecharge_=(charge: Int): Option[Int] = ShieldAutoRecharge_=(Some(charge))
|
||||
|
||||
def ShieldAutoRecharge_=(charge: Option[Int]): Option[Int] = {
|
||||
autoShieldRecharge = charge
|
||||
ShieldAutoRecharge
|
||||
}
|
||||
|
||||
def ShieldAutoRechargeSpecial: Option[Int] = autoShieldRechargeSpecial.orElse(ShieldAutoRecharge)
|
||||
|
||||
def ShieldAutoRechargeSpecial_=(charge: Int): Option[Int] = ShieldAutoRechargeSpecial_=(Some(charge))
|
||||
|
||||
def ShieldAutoRechargeSpecial_=(charge: Option[Int]): Option[Int] = {
|
||||
autoShieldRechargeSpecial = charge
|
||||
ShieldAutoRechargeSpecial
|
||||
}
|
||||
|
||||
def ShieldDrain: Option[Int] = shieldDrain
|
||||
|
||||
def ShieldDrain_=(drain: Int): Option[Int] = ShieldDrain_=(Some(drain))
|
||||
|
||||
def ShieldDrain_=(drain: Option[Int]): Option[Int] = {
|
||||
shieldDrain = drain
|
||||
ShieldDrain
|
||||
}
|
||||
|
||||
def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo
|
||||
|
||||
def CanBeOwned: Option[Boolean] = canBeOwned
|
||||
|
|
@ -300,6 +224,7 @@ class VehicleDefinition(objectId: Int)
|
|||
)
|
||||
}
|
||||
|
||||
//noinspection ScalaUnusedSymbol
|
||||
def Uninitialize(obj: Vehicle, context: ActorContext): Unit = {
|
||||
obj.Actor ! akka.actor.PoisonPill
|
||||
obj.Actor = Default.Actor
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package net.psforever.objects.definition
|
||||
|
||||
trait WithShields {
|
||||
/** ... */
|
||||
var shieldUiAttribute: Int = 68
|
||||
/** how many points of shield the vehicle starts with (should default to 0 if unset through the accessor) */
|
||||
private var defaultShields : Option[Int] = None
|
||||
/** maximum vehicle shields (generally: 20% of health)
|
||||
* for normal vehicles, offered through amp station facility benefits
|
||||
* for BFR's, it charges naturally
|
||||
**/
|
||||
private var maxShields: Int = 0
|
||||
/** the minimum amount of time that must elapse in between damage and shield charge activities (ms) */
|
||||
private var shieldChargeDamageCooldown : Long = 5000L
|
||||
/** the minimum amount of time that must elapse in between distinct shield charge activities (ms) */
|
||||
private var shieldChargePeriodicCooldown : Long = 1000L
|
||||
/** if the shield recharges on its own, this value will be non-`None` and indicate by how much */
|
||||
private var autoShieldRecharge : Option[Int] = None
|
||||
private var autoShieldRechargeSpecial : Option[Int] = None
|
||||
/** shield drain is what happens to the shield under special conditions, e.g., bfr flight;
|
||||
* the drain interval is 250ms which is convenient for us
|
||||
* we can skip needing to define is explicitly */
|
||||
private var shieldDrain : Option[Int] = None
|
||||
|
||||
def DefaultShields: Int = defaultShields.getOrElse(0)
|
||||
|
||||
def DefaultShields_=(shield: Int): Int = DefaultShields_=(Some(shield))
|
||||
|
||||
def DefaultShields_=(shield: Option[Int]): Int = {
|
||||
defaultShields = shield
|
||||
DefaultShields
|
||||
}
|
||||
|
||||
def MaxShields: Int = maxShields
|
||||
|
||||
def MaxShields_=(shields: Int): Int = {
|
||||
maxShields = shields
|
||||
MaxShields
|
||||
}
|
||||
|
||||
def ShieldPeriodicDelay : Long = shieldChargePeriodicCooldown
|
||||
|
||||
def ShieldPeriodicDelay_=(cooldown: Long): Long = {
|
||||
shieldChargePeriodicCooldown = cooldown
|
||||
ShieldPeriodicDelay
|
||||
}
|
||||
|
||||
def ShieldDamageDelay: Long = shieldChargeDamageCooldown
|
||||
|
||||
def ShieldDamageDelay_=(cooldown: Long): Long = {
|
||||
shieldChargeDamageCooldown = cooldown
|
||||
ShieldDamageDelay
|
||||
}
|
||||
|
||||
def ShieldAutoRecharge: Option[Int] = autoShieldRecharge
|
||||
|
||||
def ShieldAutoRecharge_=(charge: Int): Option[Int] = ShieldAutoRecharge_=(Some(charge))
|
||||
|
||||
def ShieldAutoRecharge_=(charge: Option[Int]): Option[Int] = {
|
||||
autoShieldRecharge = charge
|
||||
ShieldAutoRecharge
|
||||
}
|
||||
|
||||
def ShieldAutoRechargeSpecial: Option[Int] = autoShieldRechargeSpecial.orElse(ShieldAutoRecharge)
|
||||
|
||||
def ShieldAutoRechargeSpecial_=(charge: Int): Option[Int] = ShieldAutoRechargeSpecial_=(Some(charge))
|
||||
|
||||
def ShieldAutoRechargeSpecial_=(charge: Option[Int]): Option[Int] = {
|
||||
autoShieldRechargeSpecial = charge
|
||||
ShieldAutoRechargeSpecial
|
||||
}
|
||||
|
||||
def ShieldDrain: Option[Int] = shieldDrain
|
||||
|
||||
def ShieldDrain_=(drain: Int): Option[Int] = ShieldDrain_=(Some(drain))
|
||||
|
||||
def ShieldDrain_=(drain: Option[Int]): Option[Int] = {
|
||||
shieldDrain = drain
|
||||
ShieldDrain
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ class BattleFrameFlightConverter extends ObjectCreateConverter[Vehicle]() {
|
|||
jammered = false,
|
||||
v4 = None,
|
||||
v5 = None,
|
||||
obj.Owner match {
|
||||
obj.OwnerGuid match {
|
||||
case Some(owner) => owner
|
||||
case None => PlanetSideGUID(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class BattleFrameRoboticsConverter extends ObjectCreateConverter[Vehicle]() {
|
|||
jammered = obj.Jammed,
|
||||
v4 = None,
|
||||
v5 = None,
|
||||
obj.Owner match {
|
||||
obj.OwnerGuid match {
|
||||
case Some(owner) => owner
|
||||
case None => PlanetSideGUID(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class CaptureFlagConverter extends ObjectCreateConverter[CaptureFlag]() {
|
|||
case _ => Hackable.HackInfo(PlayerSource("", PlanetSideEmpire.NEUTRAL, Vector3.Zero), PlanetSideGUID(0), 0L, 0L)
|
||||
}
|
||||
|
||||
val millisecondsRemaining = TimeUnit.MILLISECONDS.convert(math.max(0, hackInfo.hackStartTime + hackInfo.hackDuration - System.nanoTime), TimeUnit.NANOSECONDS)
|
||||
val millisecondsRemaining = math.max(0, hackInfo.hackStartTime + hackInfo.hackDuration - System.currentTimeMillis())
|
||||
|
||||
Success(
|
||||
CaptureFlagData(
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class DroppodConverter extends ObjectCreateConverter[Vehicle]() {
|
|||
jammered = obj.Jammed,
|
||||
v4 = Some(false),
|
||||
v5 = None,
|
||||
obj.Owner match {
|
||||
obj.OwnerGuid match {
|
||||
case Some(owner) => owner
|
||||
case None => PlanetSideGUID(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class FieldTurretConverter extends ObjectCreateConverter[TurretDeployable]() {
|
|||
jammered = obj.Jammed,
|
||||
Some(false),
|
||||
None,
|
||||
obj.Owner match {
|
||||
obj.OwnerGuid match {
|
||||
case Some(owner) => owner
|
||||
case None => PlanetSideGUID(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class ShieldGeneratorConverter extends ObjectCreateConverter[ShieldGeneratorDepl
|
|||
jammered = obj.Jammed,
|
||||
None,
|
||||
None,
|
||||
obj.Owner match {
|
||||
obj.OwnerGuid match {
|
||||
case Some(owner) => owner
|
||||
case None => PlanetSideGUID(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class SmallDeployableConverter extends ObjectCreateConverter[Deployable]() {
|
|||
},
|
||||
Some(false),
|
||||
None,
|
||||
obj.Owner match {
|
||||
obj.OwnerGuid match {
|
||||
case Some(owner) => owner
|
||||
case None => PlanetSideGUID(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class SmallTurretConverter extends ObjectCreateConverter[TurretDeployable]() {
|
|||
jammered = obj.Jammed,
|
||||
Some(true),
|
||||
None,
|
||||
obj.Owner match {
|
||||
obj.OwnerGuid match {
|
||||
case Some(owner) => owner
|
||||
case None => PlanetSideGUID(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class TRAPConverter extends ObjectCreateConverter[TrapDeployable]() {
|
|||
false,
|
||||
Some(true),
|
||||
None,
|
||||
obj.Owner match {
|
||||
obj.OwnerGuid match {
|
||||
case Some(owner) => owner
|
||||
case None => PlanetSideGUID(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class TelepadDeployableConverter extends ObjectCreateConverter[TelepadDeployable
|
|||
false,
|
||||
None,
|
||||
Some(router.guid),
|
||||
obj.Owner.getOrElse(PlanetSideGUID(0))
|
||||
obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
|
||||
),
|
||||
unk1 = 87,
|
||||
unk2 = 12
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
|
|||
jammered = obj.Jammed,
|
||||
v4 = Some(false),
|
||||
v5 = None,
|
||||
obj.Owner match {
|
||||
obj.OwnerGuid match {
|
||||
case Some(owner) => owner
|
||||
case None => PlanetSideGUID(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,15 @@ import akka.actor.{Actor, Cancellable}
|
|||
import net.psforever.objects.{Vehicle, Vehicles}
|
||||
import net.psforever.objects.equipment.JammableUnit
|
||||
import net.psforever.objects.serverobject.damage.Damageable.Target
|
||||
import net.psforever.objects.sourcing.VehicleSource
|
||||
import net.psforever.objects.vital.base.DamageResolution
|
||||
import net.psforever.objects.vital.interaction.DamageResult
|
||||
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
|
||||
import net.psforever.objects.vital.resolution.ResolutionCalculations
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects.zones.exp.ToDatabase
|
||||
import net.psforever.packet.game.DamageWithPositionMessage
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game.DamageWithPositionMessage
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
|
@ -213,6 +215,18 @@ trait DamageableVehicle
|
|||
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, obj.Definition.shieldUiAttribute, 0)
|
||||
)
|
||||
}
|
||||
//database entry
|
||||
cause.adversarial.collect {
|
||||
case Adversarial(attacker, victim: VehicleSource, implement) =>
|
||||
ToDatabase.reportMachineDestruction(
|
||||
attacker.CharId,
|
||||
victim,
|
||||
DamageableObject.HackedBy,
|
||||
DamageableObject.MountedIn.nonEmpty,
|
||||
implement,
|
||||
obj.Zone.Number
|
||||
)
|
||||
}
|
||||
//clean up
|
||||
target.Actor ! Vehicle.Deconstruct(Some(1 minute))
|
||||
DamageableWeaponTurret.DestructionAwareness(obj, cause)
|
||||
|
|
|
|||
|
|
@ -25,14 +25,14 @@ trait Hackable {
|
|||
def HackedBy_=(agent: Option[Player]): Option[HackInfo] = {
|
||||
(hackedBy, agent) match {
|
||||
case (None, Some(actor)) =>
|
||||
hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.nanoTime, 0L))
|
||||
hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.currentTimeMillis(), 0L))
|
||||
case (Some(info), Some(actor)) =>
|
||||
if (actor.Faction == this.Faction) {
|
||||
//hack cleared
|
||||
hackedBy = None
|
||||
} else if (actor.Faction != info.hackerFaction) {
|
||||
//override the hack state with a new hack state if the new user has different faction affiliation
|
||||
hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.nanoTime, 0L))
|
||||
hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.currentTimeMillis(), 0L))
|
||||
}
|
||||
case (_, None) =>
|
||||
hackedBy = None
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import net.psforever.types.{PlanetSideEmpire, Vector3}
|
|||
/**
|
||||
* Represent a special entity that is carried by the player in certain circumstances.
|
||||
* The entity is not a piece of `Equipment` so it does not go into the holsters,
|
||||
* doe not into the player's inventory,
|
||||
* does not into the player's inventory,
|
||||
* and is not carried in or manipulated by the player's hands.
|
||||
* The different game elements it simulates are:
|
||||
* a facility's lattice logic unit (LLU),
|
||||
|
|
@ -33,6 +33,7 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity {
|
|||
private var target: Building = Building.NoBuilding
|
||||
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
private var carrier: Option[Player] = None
|
||||
private var lastTimeCollected: Long = System.currentTimeMillis()
|
||||
|
||||
def Target: Building = target
|
||||
def Target_=(newTarget: Building): Building = {
|
||||
|
|
@ -64,8 +65,11 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity {
|
|||
def Carrier: Option[Player] = carrier
|
||||
def Carrier_=(newCarrier: Option[Player]) : Option[Player] = {
|
||||
carrier = newCarrier
|
||||
lastTimeCollected = System.currentTimeMillis()
|
||||
carrier
|
||||
}
|
||||
|
||||
def LastCollectionTime: Long = carrier.map { _ => lastTimeCollected }.getOrElse { System.currentTimeMillis() }
|
||||
}
|
||||
|
||||
object CaptureFlag {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.pad
|
||||
|
||||
import akka.actor.{Cancellable, Props}
|
||||
import akka.actor.{ActorRef, Cancellable, OneForOneStrategy, Props}
|
||||
import net.psforever.objects.avatar.SpecialCarry
|
||||
import net.psforever.objects.entity.WorldEntity
|
||||
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
||||
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
|
||||
import net.psforever.objects.serverobject.pad.process.{VehicleSpawnControlBase, VehicleSpawnControlConcealPlayer}
|
||||
import net.psforever.objects.sourcing.AmenitySource
|
||||
import net.psforever.objects.vital.TerminalUsedActivity
|
||||
import net.psforever.objects.zones.{Zone, ZoneAware, Zoning}
|
||||
import net.psforever.objects.{Default, PlanetSideGameObject, Player, Vehicle}
|
||||
import net.psforever.types.Vector3
|
||||
import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
|
@ -38,38 +40,39 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
with FactionAffinityBehavior.Check {
|
||||
|
||||
/** a reminder sent to future customers */
|
||||
var periodicReminder: Cancellable = Default.Cancellable
|
||||
private var periodicReminder: Cancellable = Default.Cancellable
|
||||
|
||||
/** repeatedly test whether queued orders are valid */
|
||||
var queueManagement: Cancellable = Default.Cancellable
|
||||
private var queueManagement: Cancellable = Default.Cancellable
|
||||
|
||||
/** a list of vehicle orders that have been submitted for this spawn pad */
|
||||
var orders: List[VehicleSpawnPad.VehicleOrder] = List.empty[VehicleSpawnPad.VehicleOrder]
|
||||
private var orders: List[VehicleSpawnPad.VehicleOrder] = List.empty[VehicleSpawnPad.VehicleOrder]
|
||||
|
||||
/** the current vehicle order being acted upon;
|
||||
* used as a guard condition to control order processing rate
|
||||
*/
|
||||
var trackedOrder: Option[VehicleSpawnControl.Order] = None
|
||||
private var trackedOrder: Option[VehicleSpawnControl.Order] = None
|
||||
|
||||
/** how to process either the first order or every subsequent order */
|
||||
var handleOrderFunc: VehicleSpawnPad.VehicleOrder => Unit = NewTasking
|
||||
private var handleOrderFunc: VehicleSpawnPad.VehicleOrder => Unit = NewTasking
|
||||
|
||||
def LogId = ""
|
||||
|
||||
/**
|
||||
* The first chained action of the vehicle spawning process.
|
||||
*/
|
||||
val concealPlayer =
|
||||
private val concealPlayer: ActorRef =
|
||||
context.actorOf(Props(classOf[VehicleSpawnControlConcealPlayer], pad), s"${context.parent.path.name}-conceal")
|
||||
|
||||
def FactionObject: FactionAffinity = pad
|
||||
|
||||
import akka.actor.SupervisorStrategy._
|
||||
override val supervisorStrategy = {
|
||||
import akka.actor.OneForOneStrategy
|
||||
|
||||
override val supervisorStrategy: OneForOneStrategy = {
|
||||
OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 10 seconds) {
|
||||
case _: akka.actor.ActorKilledException => Restart
|
||||
case _ => Resume
|
||||
case _ =>
|
||||
log.warn(s"vehicle spawn pad restarted${trackedOrder.map { o => s"; an unfulfilled order for ${o.driver.Name} will be expunged" }.getOrElse("")}")
|
||||
Restart
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,21 +88,18 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
try {
|
||||
handleOrderFunc(msg)
|
||||
} catch {
|
||||
case _: AssertionError => ; //ehhh
|
||||
case e: Exception => //something unexpected
|
||||
e.printStackTrace()
|
||||
case _: AssertionError => () //ehhh
|
||||
case e: Exception => e.printStackTrace() //something unexpected
|
||||
}
|
||||
|
||||
case VehicleSpawnControl.ProcessControl.OrderCancelled =>
|
||||
trackedOrder match {
|
||||
case Some(entry)
|
||||
if sender() == concealPlayer =>
|
||||
trackedOrder.collect {
|
||||
case entry if sender() == concealPlayer =>
|
||||
CancelOrder(
|
||||
entry,
|
||||
VehicleSpawnControl.validateOrderCredentials(pad, entry.driver, entry.vehicle)
|
||||
.orElse(Some("@SVCP_RemovedFromVehicleQueue_Generic"))
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
trackedOrder = None //guard off
|
||||
SelectOrder()
|
||||
|
|
@ -120,37 +120,40 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
During this time, a periodic message about the spawn pad being blocked will be broadcast to the order queue.
|
||||
*/
|
||||
case VehicleSpawnControl.ProcessControl.Reminder =>
|
||||
trackedOrder match {
|
||||
case Some(entry) =>
|
||||
if (periodicReminder.isCancelled) {
|
||||
trace(s"the pad has become blocked by a ${entry.vehicle.Definition.Name} in its current order")
|
||||
periodicReminder = context.system.scheduler.scheduleWithFixedDelay(
|
||||
VehicleSpawnControl.periodicReminderTestDelay,
|
||||
VehicleSpawnControl.periodicReminderTestDelay,
|
||||
self,
|
||||
VehicleSpawnControl.ProcessControl.Reminder
|
||||
)
|
||||
} else {
|
||||
BlockedReminder(entry, orders)
|
||||
}
|
||||
case None => ;
|
||||
trackedOrder
|
||||
.collect {
|
||||
case entry =>
|
||||
if (periodicReminder.isCancelled) {
|
||||
trace(s"the pad has become blocked by a ${entry.vehicle.Definition.Name} in its current order")
|
||||
periodicReminder = context.system.scheduler.scheduleWithFixedDelay(
|
||||
VehicleSpawnControl.periodicReminderTestDelay,
|
||||
VehicleSpawnControl.periodicReminderTestDelay,
|
||||
self,
|
||||
VehicleSpawnControl.ProcessControl.Reminder
|
||||
)
|
||||
} else {
|
||||
BlockedReminder(entry, orders)
|
||||
}
|
||||
trackedOrder
|
||||
}
|
||||
.orElse {
|
||||
periodicReminder.cancel()
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
case VehicleSpawnControl.ProcessControl.Flush =>
|
||||
periodicReminder.cancel()
|
||||
orders.foreach { CancelOrder(_, Some("@SVCP_RemovedFromVehicleQueue_Generic")) }
|
||||
orders = Nil
|
||||
trackedOrder match {
|
||||
case Some(entry) => CancelOrder(entry, Some("@SVCP_RemovedFromVehicleQueue_Generic"))
|
||||
case None => ;
|
||||
trackedOrder.foreach {
|
||||
entry => CancelOrder(entry, Some("@SVCP_RemovedFromVehicleQueue_Generic"))
|
||||
}
|
||||
trackedOrder = None
|
||||
handleOrderFunc = NewTasking
|
||||
pad.Zone.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad) //cautious animation reset
|
||||
concealPlayer ! akka.actor.Kill //should cause the actor to restart, which will abort any trapped messages
|
||||
self ! akka.actor.Kill //should cause the actor to restart, which will abort any trapped messages
|
||||
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -158,7 +161,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
* All orders accepted in the meantime will be queued and a note about priority will be issued.
|
||||
* @param order the order being accepted
|
||||
*/
|
||||
def NewTasking(order: VehicleSpawnPad.VehicleOrder): Unit = {
|
||||
private def NewTasking(order: VehicleSpawnPad.VehicleOrder): Unit = {
|
||||
handleOrderFunc = QueuedTasking
|
||||
ProcessOrder(Some(order))
|
||||
}
|
||||
|
|
@ -168,7 +171,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
* all orders accepted in the meantime will be queued and a note about priority will be issued.
|
||||
* @param order the order being accepted
|
||||
*/
|
||||
def QueuedTasking(order: VehicleSpawnPad.VehicleOrder): Unit = {
|
||||
private def QueuedTasking(order: VehicleSpawnPad.VehicleOrder): Unit = {
|
||||
val name = order.player.Name
|
||||
if (trackedOrder match {
|
||||
case Some(tracked) =>
|
||||
|
|
@ -219,14 +222,14 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
/**
|
||||
* Select the next available queued order and begin processing it.
|
||||
*/
|
||||
def SelectOrder(): Unit = ProcessOrder(SelectFirstOrder())
|
||||
private def SelectOrder(): Unit = ProcessOrder(SelectFirstOrder())
|
||||
|
||||
/**
|
||||
* Select the next-available queued order if there is no current order being fulfilled.
|
||||
* If the queue has been exhausted, set functionality to prepare to accept the next order as a "first order."
|
||||
* @return the next-available order
|
||||
*/
|
||||
def SelectFirstOrder(): Option[VehicleSpawnPad.VehicleOrder] = {
|
||||
private def SelectFirstOrder(): Option[VehicleSpawnPad.VehicleOrder] = {
|
||||
trackedOrder match {
|
||||
case None =>
|
||||
val (completeOrder, remainingOrders): (Option[VehicleSpawnPad.VehicleOrder], List[VehicleSpawnPad.VehicleOrder]) =
|
||||
|
|
@ -255,14 +258,12 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
* @param order the order being accepted;
|
||||
* `None`, if no order found or submitted
|
||||
*/
|
||||
def ProcessOrder(order: Option[VehicleSpawnPad.VehicleOrder]): Unit = {
|
||||
private def ProcessOrder(order: Option[VehicleSpawnPad.VehicleOrder]): Unit = {
|
||||
periodicReminder.cancel()
|
||||
order match {
|
||||
case Some(_order) =>
|
||||
order.collect {
|
||||
case VehicleSpawnPad.VehicleOrder(driver, vehicle, terminal) =>
|
||||
val size = orders.size + 1
|
||||
val driver = _order.player
|
||||
val name = driver.Name
|
||||
val vehicle = _order.vehicle
|
||||
val newOrder = VehicleSpawnControl.Order(driver, vehicle)
|
||||
recursiveOrderReminder(orders.iterator, size)
|
||||
trace(s"processing next order - a ${vehicle.Definition.Name} for $name")
|
||||
|
|
@ -273,7 +274,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
)
|
||||
trackedOrder = Some(newOrder) //guard on
|
||||
context.system.scheduler.scheduleOnce(2000 milliseconds, concealPlayer, newOrder)
|
||||
case None => ;
|
||||
driver.LogActivity(TerminalUsedActivity(AmenitySource(terminal), TransactionType.Buy))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -282,7 +283,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
* either start a periodic examination of those credentials until the queue has been emptied or
|
||||
* cancel a running periodic examination if the queue is already empty.
|
||||
*/
|
||||
def queueManagementTask(): Unit = {
|
||||
private def queueManagementTask(): Unit = {
|
||||
if (orders.nonEmpty) {
|
||||
orders = orderCredentialsCheck(orders).toList
|
||||
if (queueManagement.isCancelled) {
|
||||
|
|
@ -306,7 +307,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
* @param recipients the original list of orders
|
||||
* @return the list of still-acceptable orders
|
||||
*/
|
||||
def orderCredentialsCheck(recipients: Iterable[VehicleSpawnPad.VehicleOrder]): Iterable[VehicleSpawnPad.VehicleOrder] = {
|
||||
private def orderCredentialsCheck(recipients: Iterable[VehicleSpawnPad.VehicleOrder]): Iterable[VehicleSpawnPad.VehicleOrder] = {
|
||||
recipients
|
||||
.map { order =>
|
||||
(order, VehicleSpawnControl.validateOrderCredentials(order.terminal, order.player, order.vehicle))
|
||||
|
|
@ -328,10 +329,10 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
* @param blockedOrder the previous order whose vehicle is blocking the spawn pad from operating
|
||||
* @param recipients all of the other customers who will be receiving the message
|
||||
*/
|
||||
def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnPad.VehicleOrder]): Unit = {
|
||||
private def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnPad.VehicleOrder]): Unit = {
|
||||
val user = blockedOrder.vehicle
|
||||
.Seats(0).occupant
|
||||
.orElse(pad.Zone.GUID(blockedOrder.vehicle.Owner))
|
||||
.orElse(pad.Zone.GUID(blockedOrder.vehicle.OwnerGuid))
|
||||
.orElse(pad.Zone.GUID(blockedOrder.DriverGUID))
|
||||
val relevantRecipients: Iterator[VehicleSpawnPad.VehicleOrder] = user match {
|
||||
case Some(p: Player) if !p.HasGUID =>
|
||||
|
|
@ -358,14 +359,14 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
* Cancel this vehicle order and inform the person who made it, if possible.
|
||||
* @param entry the order being cancelled
|
||||
*/
|
||||
def CancelOrder(entry: VehicleSpawnControl.Order, msg: Option[String]): Unit = {
|
||||
private def CancelOrder(entry: VehicleSpawnControl.Order, msg: Option[String]): Unit = {
|
||||
CancelOrder(entry.vehicle, entry.driver, msg)
|
||||
}
|
||||
/**
|
||||
* Cancel this vehicle order and inform the person who made it, if possible.
|
||||
* @param entry the order being cancelled
|
||||
*/
|
||||
def CancelOrder(entry: VehicleSpawnPad.VehicleOrder, msg: Option[String]): Unit = {
|
||||
private def CancelOrder(entry: VehicleSpawnPad.VehicleOrder, msg: Option[String]): Unit = {
|
||||
CancelOrder(entry.vehicle, entry.player, msg)
|
||||
}
|
||||
/**
|
||||
|
|
@ -373,7 +374,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
|
|||
* @param vehicle the vehicle from the order being cancelled
|
||||
* @param player the player who would driver the vehicle from the order being cancelled
|
||||
*/
|
||||
def CancelOrder(vehicle: Vehicle, player: Player, msg: Option[String]): Unit = {
|
||||
private def CancelOrder(vehicle: Vehicle, player: Player, msg: Option[String]): Unit = {
|
||||
if (vehicle.Seats.values.count(_.isOccupied) == 0) {
|
||||
VehicleSpawnControl.DisposeSpawnedVehicle(vehicle, player, pad.Zone)
|
||||
pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(player.Name, VehicleSpawnPad.Reminders.Cancelled, msg)
|
||||
|
|
@ -436,8 +437,8 @@ object VehicleSpawnControl {
|
|||
final case class Order(driver: Player, vehicle: Vehicle) {
|
||||
assert(driver.HasGUID, s"when ordering a vehicle, the prospective driver ${driver.Name} does not have a GUID")
|
||||
assert(vehicle.HasGUID, s"when ordering a vehicle, the ${vehicle.Definition.Name} does not have a GUID")
|
||||
val DriverGUID = driver.GUID
|
||||
val time = System.currentTimeMillis()
|
||||
val DriverGUID: PlanetSideGUID = driver.GUID
|
||||
val time: Long = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -502,7 +503,7 @@ object VehicleSpawnControl {
|
|||
* @param player the player who would own the vehicle being disposed
|
||||
* @param zone the zone in which the vehicle is registered (should be located)
|
||||
*/
|
||||
def DisposeSpawnedVehicle(vehicle: Vehicle, player: Player, zone: Zone): Unit = {
|
||||
private def DisposeSpawnedVehicle(vehicle: Vehicle, player: Player, zone: Zone): Unit = {
|
||||
DisposeVehicle(vehicle, zone)
|
||||
zone.VehicleEvents ! VehicleSpawnPad.RevealPlayer(player.GUID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ import net.psforever.actors.zone.BuildingActor
|
|||
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
|
||||
import net.psforever.objects.serverobject.transfer.TransferBehavior
|
||||
import net.psforever.objects.serverobject.structures.Building
|
||||
import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
import net.psforever.objects.zones
|
||||
import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior, Vehicle}
|
||||
import net.psforever.types.{ExperienceType, PlanetSideEmpire}
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.util.Config
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
|
|
@ -181,6 +183,24 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
|
|||
if (amount != 0) {
|
||||
panelAnimationFunc(sender, amount)
|
||||
panelAnimationFunc = SkipPanelAnimation
|
||||
(src match {
|
||||
case v: Vehicle => Some(v)
|
||||
case _ => None
|
||||
})
|
||||
.map { v => (v, v.Owners) }
|
||||
.collect { case (vehicle, Some(owner)) =>
|
||||
//experience is reported as normal
|
||||
val deposit: Long =
|
||||
(Config.app.game.experience.sep.ntuSiloDepositReward.toFloat *
|
||||
math.floor(amount).toFloat /
|
||||
math.floor(resourceSilo.MaxNtuCapacitor / resourceSilo.Definition.ChargeTime.toMillis.toFloat)
|
||||
).toLong
|
||||
vehicle.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||
owner.name,
|
||||
AvatarAction.AwardBep(0, deposit, ExperienceType.Normal)
|
||||
)
|
||||
zones.exp.ToDatabase.reportNtuActivity(owner.charId, resourceSilo.Zone.Number, resourceSilo.Owner.GUID.guid, deposit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +212,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
|
|||
* @param trigger if positive, activate the animation;
|
||||
* if negative or zero, disable the animation
|
||||
*/
|
||||
//noinspection ScalaUnusedSymbol
|
||||
def PanelAnimation(source: ActorRef, trigger: Float): Unit = {
|
||||
val currentlyHas = resourceSilo.NtuCapacitor
|
||||
// do not let the trigger charge go to waste, but also do not let the silo be filled
|
||||
|
|
|
|||
|
|
@ -4,12 +4,16 @@ package net.psforever.objects.serverobject.resourcesilo
|
|||
import net.psforever.objects.NtuContainerDefinition
|
||||
import net.psforever.objects.serverobject.structures.AmenityDefinition
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* The definition for any `Resource Silo`.
|
||||
* Object Id 731.
|
||||
*/
|
||||
class ResourceSiloDefinition extends AmenityDefinition(731)
|
||||
with NtuContainerDefinition {
|
||||
var ChargeTime: FiniteDuration = 0.seconds
|
||||
|
||||
Name = "resource_silo"
|
||||
MaxNtuCapacitor = 1000
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@ import net.psforever.types._
|
|||
import scalax.collection.{Graph, GraphEdge}
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
|
||||
import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation}
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
|
||||
|
||||
import scala.collection.mutable
|
||||
import java.util.concurrent.TimeUnit
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class Building(
|
||||
private val name: String,
|
||||
|
|
@ -34,9 +33,8 @@ class Building(
|
|||
|
||||
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
private var playersInSOI: List[Player] = List.empty
|
||||
private val capitols = List("Thoth", "Voltan", "Neit", "Anguta", "Eisa", "Verica")
|
||||
private var forceDomeActive: Boolean = false
|
||||
private var participationFunc: Building.ParticipationLogic = Building.NoParticipation
|
||||
private var participationFunc: ParticipationLogic = NoParticipation
|
||||
super.Zone_=(zone)
|
||||
super.GUID_=(PlanetSideGUID(building_guid)) //set
|
||||
Invalidate() //unset; guid can be used during setup, but does not stop being registered properly later
|
||||
|
|
@ -51,10 +49,11 @@ class Building(
|
|||
*/
|
||||
def MapId: Int = map_id
|
||||
|
||||
def IsCapitol: Boolean = capitols.contains(name)
|
||||
def IsCapitol: Boolean = Building.Capitols.contains(name)
|
||||
|
||||
def IsSubCapitol: Boolean = {
|
||||
Neighbours match {
|
||||
case Some(buildings: Set[Building]) => buildings.exists(x => capitols.contains(x.name))
|
||||
case Some(buildings: Set[Building]) => buildings.exists(x => Building.Capitols.contains(x.name))
|
||||
case None => false
|
||||
}
|
||||
}
|
||||
|
|
@ -85,13 +84,11 @@ class Building(
|
|||
box.Actor ! Painbox.Stop()
|
||||
}
|
||||
}
|
||||
participationFunc.Players(building = this, list)
|
||||
playersInSOI = list
|
||||
participationFunc.TryUpdate()
|
||||
playersInSOI
|
||||
}
|
||||
|
||||
def PlayerContribution: Map[Player, Long] = participationFunc.Contribution()
|
||||
|
||||
// Get all lattice neighbours
|
||||
def AllNeighbours: Option[Set[Building]] = {
|
||||
zone.Lattice find this match {
|
||||
|
|
@ -186,8 +183,7 @@ class Building(
|
|||
case Some(obj: CaptureTerminal with Hackable) =>
|
||||
obj.HackedBy match {
|
||||
case Some(Hackable.HackInfo(p, _, start, length)) =>
|
||||
val hack_time_remaining_ms =
|
||||
TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
|
||||
val hack_time_remaining_ms = math.max(0, start + length - System.currentTimeMillis())
|
||||
(true, p.Faction, hack_time_remaining_ms)
|
||||
case _ =>
|
||||
(false, PlanetSideEmpire.NEUTRAL, 0L)
|
||||
|
|
@ -360,43 +356,24 @@ class Building(
|
|||
|
||||
override def Amenities_=(obj: Amenity): List[Amenity] = {
|
||||
obj match {
|
||||
case _: CaptureTerminal => participationFunc = Building.FacilityHackParticipation
|
||||
case _ => ;
|
||||
case _: CaptureTerminal =>
|
||||
if (buildingType == StructureType.Facility) {
|
||||
participationFunc = MajorFacilityHackParticipation(this)
|
||||
} else if (buildingType == StructureType.Tower) {
|
||||
participationFunc = TowerHackParticipation(this)
|
||||
}
|
||||
case _ => ()
|
||||
}
|
||||
super.Amenities_=(obj)
|
||||
}
|
||||
|
||||
def Participation: ParticipationLogic = participationFunc
|
||||
|
||||
def Definition: BuildingDefinition = buildingDefinition
|
||||
}
|
||||
|
||||
object Building {
|
||||
trait ParticipationLogic {
|
||||
def Players(building: Building, list: List[Player]): Unit = { }
|
||||
def Contribution(): Map[Player, Long]
|
||||
}
|
||||
|
||||
final case object NoParticipation extends ParticipationLogic {
|
||||
def Contribution(): Map[Player, Long] = Map.empty[Player, Long]
|
||||
}
|
||||
|
||||
final case object FacilityHackParticipation extends ParticipationLogic {
|
||||
private var playerContribution: mutable.HashMap[Player, Long] = mutable.HashMap[Player, Long]()
|
||||
|
||||
override def Players(building: Building, list: List[Player]): Unit = {
|
||||
if (list.isEmpty) {
|
||||
playerContribution.clear()
|
||||
} else {
|
||||
val hackTime = (building.CaptureTerminal.get.Definition.FacilityHackTime + 10.minutes).toMillis
|
||||
val curr = System.currentTimeMillis()
|
||||
val list2 = list.map { p => (p, curr) }
|
||||
playerContribution = playerContribution.filterNot { case (p, t) =>
|
||||
list2.contains(p) || curr - t > hackTime
|
||||
} ++ list2
|
||||
}
|
||||
}
|
||||
|
||||
def Contribution(): Map[Player, Long] = playerContribution.toMap
|
||||
}
|
||||
final val Capitols = List("Thoth", "Voltan", "Neit", "Anguta", "Eisa", "Verica")
|
||||
|
||||
final val NoBuilding: Building =
|
||||
new Building(name = "", 0, map_id = 0, Zone.Nowhere, StructureType.Platform, GlobalDefinitions.building) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,272 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.serverobject.structures.participation
|
||||
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.avatar.scoring.Kill
|
||||
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
|
||||
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
trait FacilityHackParticipation extends ParticipationLogic {
|
||||
protected var lastInfoRequest: Long = 0L
|
||||
protected var infoRequestOverTime: Seq[Long] = Seq[Long]()
|
||||
/**
|
||||
key: unique player identifier<br>
|
||||
values: last player entry, number of times updated, time of last update (POSIX time)
|
||||
*/
|
||||
protected var playerContribution: mutable.LongMap[(Player, Int, Long)] = mutable.LongMap[(Player, Int, Long)]()
|
||||
protected var playerPopulationOverTime: Seq[Map[PlanetSideEmpire.Value, Int]] = Seq[Map[PlanetSideEmpire.Value, Int]]()
|
||||
|
||||
def PlayerContribution(timeDelay: Long): Map[Long, Float] = {
|
||||
playerContribution
|
||||
.collect {
|
||||
case (id, (_, d, _)) if d < timeDelay => (id, d.toFloat / timeDelay.toFloat)
|
||||
case (id, (_, _, _)) => (id, 1.0f)
|
||||
}
|
||||
.toMap[Long, Float]
|
||||
}
|
||||
|
||||
protected def updatePlayers(list: List[Player]): Unit = {
|
||||
val hackTime = building.CaptureTerminal.get.Definition.FacilityHackTime.toMillis
|
||||
val curr = System.currentTimeMillis()
|
||||
if (list.isEmpty) {
|
||||
playerContribution = playerContribution.filterNot { case (_, (_, _, t)) => curr - t > hackTime }
|
||||
} else {
|
||||
val (vanguardParticipants, missingParticipants) = {
|
||||
val uniqueList2 = list.map { _.CharId }
|
||||
playerContribution
|
||||
.filterNot { case (_, (_, _, t)) => curr - t > hackTime }
|
||||
.partition { case (p, _) => uniqueList2.contains(p) }
|
||||
}
|
||||
val newParticipaants = list
|
||||
.filterNot { p =>
|
||||
playerContribution.exists { case (u, _) => p.CharId == u }
|
||||
}
|
||||
playerContribution =
|
||||
vanguardParticipants.map { case (u, (p, d, _)) => (u, (p, d + 1, curr)) } ++
|
||||
newParticipaants.map { p => (p.CharId, (p, 1, curr)) } ++
|
||||
missingParticipants
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminate participation for players who have no submitted updates within the time period.
|
||||
* @param list current list of players
|
||||
* @param now current time (ms)
|
||||
* @param before how long before the current time beyond which players should be eliminated (ms)
|
||||
* @see `timeSensitiveFilterAndAppend`
|
||||
*/
|
||||
protected def updatePopulationOverTime(list: List[Player], now: Long, before: Long): Unit = {
|
||||
var populationList = list
|
||||
val layer = PlanetSideEmpire.values.map { faction =>
|
||||
val (isFaction, everyoneElse) = populationList.partition(_.Faction == faction)
|
||||
populationList = everyoneElse
|
||||
(faction, isFaction.size)
|
||||
}.toMap[PlanetSideEmpire.Value, Int]
|
||||
playerPopulationOverTime = timeSensitiveFilterAndAppend(playerPopulationOverTime, layer, now - before)
|
||||
}
|
||||
|
||||
protected def updateTime(now: Long): Unit = {
|
||||
infoRequestOverTime = timeSensitiveFilterAndAppend(infoRequestOverTime, now, now - 900000L)
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminate entries from the primary input list based on time entries in a secondary time record list.
|
||||
* The time record list must be updated independently.
|
||||
* @param list input list whose entries are edited against time and then is appended
|
||||
* @param newEntry new entry of the appropriate type to append to the end of the output list
|
||||
* @param beforeTime how long before the current time beyond which entries in the input list should be eliminated (ms)
|
||||
* @tparam T it does not matter what the type is
|
||||
* @return the modified list
|
||||
*/
|
||||
protected def timeSensitiveFilterAndAppend[T](
|
||||
list: Seq[T],
|
||||
newEntry: T,
|
||||
beforeTime: Long
|
||||
): Seq[T] = {
|
||||
infoRequestOverTime match {
|
||||
case Nil => Seq(newEntry)
|
||||
case _ =>
|
||||
(infoRequestOverTime.indexWhere { _ >= beforeTime } match {
|
||||
case -1 => list
|
||||
case cutOffIndex => list.drop(cutOffIndex)
|
||||
}) :+ newEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object FacilityHackParticipation {
|
||||
private[participation] def allocateKillsByPlayers(
|
||||
center: Vector3,
|
||||
radius: Float,
|
||||
hackStart: Long,
|
||||
completionTime: Long,
|
||||
opposingFaction: PlanetSideEmpire.Value,
|
||||
contributionVictor: Iterable[(Player, Int, Long)],
|
||||
): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
|
||||
val killMapFunc: Iterable[(Player, Int, Long)] => Iterable[(UniquePlayer, Float, Seq[Kill])] = {
|
||||
killsEarnedPerPlayerDuringHack(center.xy, radius * radius, hackStart, hackStart + completionTime, opposingFaction)
|
||||
}
|
||||
killMapFunc(contributionVictor)
|
||||
}
|
||||
|
||||
private[participation] def calculateExperienceFromKills(
|
||||
killMapValues: Iterable[(UniquePlayer, Float, Seq[Kill])],
|
||||
contributionOpposingSize: Int
|
||||
): Long = {
|
||||
val totalExperienceFromKills = killMapValues
|
||||
.flatMap { _._3.map { _.experienceEarned } }
|
||||
.sum
|
||||
.toFloat
|
||||
(totalExperienceFromKills * contributionOpposingSize.toFloat * 0.1d).toLong
|
||||
}
|
||||
|
||||
private[participation] def killsEarnedPerPlayerDuringHack(
|
||||
centerXY: Vector3,
|
||||
distanceSq: Float,
|
||||
start: Long,
|
||||
end: Long,
|
||||
faction: PlanetSideEmpire.Value
|
||||
)
|
||||
(
|
||||
list: Iterable[(Player, Int, Long)]
|
||||
): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
|
||||
val duration = end - start
|
||||
list.map { case (p, d, _) =>
|
||||
val killList = p.avatar.scorecard.Kills.filter { k =>
|
||||
val killTime = k.info.interaction.hitTime
|
||||
k.victim.Faction == faction &&
|
||||
start < killTime &&
|
||||
killTime <= end &&
|
||||
Vector3.DistanceSquared(centerXY, k.info.interaction.hitPos.xy) < distanceSq
|
||||
}
|
||||
(PlayerSource(p).unique, math.min(d, duration).toFloat / duration.toFloat, killList)
|
||||
}
|
||||
}
|
||||
|
||||
private[participation] def diffHeatForFactionMap(
|
||||
data: mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]],
|
||||
faction: PlanetSideEmpire.Value
|
||||
): Map[Vector3, Seq[Long]] = {
|
||||
var lastHeatAmount: Long = 0
|
||||
var outList: Seq[Long] = Seq[Long]()
|
||||
data.map { case (key, map) =>
|
||||
map(faction) match {
|
||||
case Nil => ()
|
||||
case value :: Nil =>
|
||||
outList = outList :+ value
|
||||
case value :: list =>
|
||||
lastHeatAmount = value
|
||||
list.foreach { heat =>
|
||||
if (heat < lastHeatAmount) {
|
||||
lastHeatAmount = heat
|
||||
outList = outList :+ heat
|
||||
} else {
|
||||
outList = outList :+ (heat - lastHeatAmount)
|
||||
lastHeatAmount = heat
|
||||
}
|
||||
}
|
||||
}
|
||||
(key, outList)
|
||||
}.toMap[Vector3, Seq[Long]]
|
||||
}
|
||||
|
||||
private[participation] def heatMapComparison(
|
||||
victorData: Iterable[Seq[Long]],
|
||||
opposedData: Iterable[Seq[Long]]
|
||||
): Float = {
|
||||
var dataCount: Int = 0
|
||||
var dataSum: Float = 0
|
||||
if (victorData.size == opposedData.size) {
|
||||
val seq1 = victorData.toSeq
|
||||
val seq2 = opposedData.toSeq
|
||||
seq1.indices.foreach { outerIndex =>
|
||||
val list1 = seq1(outerIndex)
|
||||
val list2 = seq2(outerIndex)
|
||||
if (list1.size == list2.size) {
|
||||
val indices1 = list1.indices
|
||||
dataCount = dataCount + indices1.size
|
||||
indices1.foreach { innerIndex =>
|
||||
val value1 = list1(innerIndex)
|
||||
val value2 = list2(innerIndex)
|
||||
if (value1 * value2 == 0) {
|
||||
dataCount -= 1
|
||||
} else if (value1 > value2) {
|
||||
dataSum = dataSum - value2.toFloat / value1.toFloat
|
||||
} else {
|
||||
dataSum = dataSum + value2.toFloat / value1.toFloat
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dataSum != 0) {
|
||||
math.max(0.15f, math.min(2f, dataSum / dataCount.toFloat))
|
||||
} else {
|
||||
1f //can't do anything; multiplier should not affect values
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param populationNumbers list of the population updates
|
||||
* @param gradingRule the rule whereby population numbers are transformed into percentage bonus
|
||||
* @param layers from largest groupings of percentages from applying the above rule, average the values from this many groups
|
||||
* @return the modifier value
|
||||
*/
|
||||
private[participation] def populationProgressModifier(
|
||||
populationNumbers: Seq[Int],
|
||||
gradingRule: Int=>Float,
|
||||
layers: Int
|
||||
): Float = {
|
||||
val gradedPopulation = populationNumbers
|
||||
.map { gradingRule }
|
||||
.groupBy(x => x)
|
||||
.values
|
||||
.toSeq
|
||||
.sortBy(_.size)
|
||||
.take(layers)
|
||||
.flatten
|
||||
gradedPopulation.sum / gradedPopulation.size.toFloat
|
||||
}
|
||||
|
||||
private[participation] def populationBalanceModifier(
|
||||
victorPopulationNumbers: Seq[Int],
|
||||
opposingPopulationNumbers: Seq[Int],
|
||||
healthyPercentage: Float
|
||||
): Float = {
|
||||
val rate = for {
|
||||
victorPop <- victorPopulationNumbers
|
||||
opposePop <- opposingPopulationNumbers
|
||||
out = if (
|
||||
(opposePop + victorPop < 8) ||
|
||||
(opposePop < victorPop && opposePop * healthyPercentage > victorPop) ||
|
||||
(opposePop > victorPop && victorPop * healthyPercentage > opposePop)
|
||||
) {
|
||||
1f //balanced enough population
|
||||
} else {
|
||||
opposePop / victorPop.toFloat
|
||||
}
|
||||
if true
|
||||
} yield out
|
||||
rate.sum / rate.size.toFloat
|
||||
}
|
||||
|
||||
private[participation] def competitionBonus(
|
||||
victorSize: Long,
|
||||
opposingSize: Long,
|
||||
steamrollPercentage: Float,
|
||||
steamrollBonus: Long,
|
||||
overwhelmingOddsPercentage: Float,
|
||||
overwhelmingOddsBonus: Long
|
||||
): Long = {
|
||||
if (opposingSize * steamrollPercentage < victorSize.toFloat) {
|
||||
0L //steamroll by the victor
|
||||
} else if (victorSize * overwhelmingOddsPercentage <= opposingSize.toFloat) {
|
||||
overwhelmingOddsBonus + opposingSize + victorSize //victory against overwhelming odds
|
||||
} else {
|
||||
steamrollBonus * opposingSize //still a battle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.serverobject.structures.participation
|
||||
|
||||
import net.psforever.objects.serverobject.structures.{Building, StructureType}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
|
||||
import net.psforever.objects.zones.{HotSpotInfo, ZoneHotSpotProjector}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState, Vector3}
|
||||
import net.psforever.util.Config
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import net.psforever.objects.avatar.scoring.Kill
|
||||
import net.psforever.objects.zones.exp.ToDatabase
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
|
||||
final case class MajorFacilityHackParticipation(building: Building) extends FacilityHackParticipation {
|
||||
private implicit val timeout: Timeout = 10.seconds
|
||||
|
||||
private var hotSpotLayersOverTime: Seq[List[HotSpotInfo]] = Seq[List[HotSpotInfo]]()
|
||||
|
||||
def TryUpdate(): Unit = {
|
||||
val list = building.PlayersInSOI
|
||||
updatePlayers(list)
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastInfoRequest > 60000L) {
|
||||
updatePopulationOverTime(list, now, before = 900000L)
|
||||
updateHotSpotInfoOverTime()
|
||||
updateTime(now)
|
||||
}
|
||||
lastInfoRequest = now
|
||||
}
|
||||
|
||||
private def updateHotSpotInfoOnly(): Future[ZoneHotSpotProjector.ExposedHeat] = {
|
||||
ask(
|
||||
building.Zone.Activity,
|
||||
ZoneHotSpotProjector.ExposeHeatForRegion(building.Position, building.Definition.SOIRadius.toFloat)
|
||||
).mapTo[ZoneHotSpotProjector.ExposedHeat]
|
||||
}
|
||||
|
||||
private def updateHotSpotInfoOverTime(): Future[ZoneHotSpotProjector.ExposedHeat] = {
|
||||
import net.psforever.objects.zones.ZoneHotSpotProjector
|
||||
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.Success
|
||||
val requestLayers: Promise[ZoneHotSpotProjector.ExposedHeat] = Promise[ZoneHotSpotProjector.ExposedHeat]()
|
||||
val request = updateHotSpotInfoOnly()
|
||||
requestLayers.completeWith(request)
|
||||
request.onComplete {
|
||||
case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) =>
|
||||
hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis() - 900000L)
|
||||
case _ =>
|
||||
requestLayers.completeWith(Future(ZoneHotSpotProjector.ExposedHeat(Vector3.Zero, 0, Nil)))
|
||||
}
|
||||
requestLayers.future
|
||||
}
|
||||
|
||||
def RewardFacilityCapture(
|
||||
defenderFaction: PlanetSideEmpire.Value,
|
||||
attackingFaction: PlanetSideEmpire.Value,
|
||||
hacker: PlayerSource,
|
||||
hackTime: Long,
|
||||
completionTime: Long,
|
||||
isResecured: Boolean
|
||||
): Unit = {
|
||||
val curr = System.currentTimeMillis()
|
||||
val hackStart = curr - completionTime
|
||||
val socketOpt = building.GetFlagSocket
|
||||
val (victorFaction, opposingFaction, hasFlag, flagCarrier) = if (!isResecured) {
|
||||
val carrier = socketOpt.flatMap(_.previousFlag).flatMap(_.Carrier)
|
||||
(attackingFaction, defenderFaction, socketOpt.nonEmpty, carrier)
|
||||
} else {
|
||||
(defenderFaction, attackingFaction, socketOpt.nonEmpty, None)
|
||||
}
|
||||
val (contributionVictor, contributionOpposing, _) = {
|
||||
val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction }
|
||||
val (b, c) = b1.partition { case (_, (p, _, _)) => p.Faction == opposingFaction }
|
||||
(a.values, b.values, c.values)
|
||||
}
|
||||
val contributionVictorSize = contributionVictor.size
|
||||
if (contributionVictorSize > 0) {
|
||||
//setup for ...
|
||||
val populationIndices = playerPopulationOverTime.indices
|
||||
val allFactions = PlanetSideEmpire.values.filterNot { _ == PlanetSideEmpire.NEUTRAL }.toSeq
|
||||
val (victorPopulationByLayer, opposingPopulationByLayer) = {
|
||||
val individualPopulationByLayer = allFactions.map { f =>
|
||||
(f, populationIndices.indices.map { i => playerPopulationOverTime(i)(f) })
|
||||
}.toMap[PlanetSideEmpire.Value, Seq[Int]]
|
||||
(individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction))
|
||||
}
|
||||
val contributionOpposingSize = contributionOpposing.size
|
||||
val killsByPlayersNotInTower = eliminateClosestTowerFromParticipating(
|
||||
building,
|
||||
FacilityHackParticipation.allocateKillsByPlayers(
|
||||
building.Position,
|
||||
building.Definition.SOIRadius.toFloat,
|
||||
hackStart,
|
||||
completionTime,
|
||||
opposingFaction,
|
||||
contributionOpposing
|
||||
)
|
||||
)
|
||||
//1) experience from killing opposingFaction across duration of hack
|
||||
//The kills that occurred in the facility's attached field tower's sphere of influence have been eliminated from consideration.
|
||||
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
|
||||
killsByPlayersNotInTower,
|
||||
contributionOpposingSize
|
||||
)
|
||||
//2) peak population modifier
|
||||
//Large facility battles should be well-rewarded.
|
||||
val populationModifier = FacilityHackParticipation.populationProgressModifier(
|
||||
opposingPopulationByLayer,
|
||||
{ pop =>
|
||||
if (pop > 75) 0.9f
|
||||
else if (pop > 59) 0.6f
|
||||
else if (pop > 29) 0.55f
|
||||
else if (pop > 25) 0.5f
|
||||
else 0.45f
|
||||
},
|
||||
4
|
||||
)
|
||||
//3) competition multiplier
|
||||
val competitionMultiplier: Float = {
|
||||
val populationBalanceModifier: Float = FacilityHackParticipation.populationBalanceModifier(
|
||||
victorPopulationByLayer,
|
||||
opposingPopulationByLayer,
|
||||
healthyPercentage = 1.5f
|
||||
)
|
||||
//compensate for heat
|
||||
val regionHeatMapProgression = {
|
||||
/*
|
||||
transform the different layers of the facility heat map timeline into a progressing timeline of regional hotspot information;
|
||||
where the grouping are of simultaneous hotspots,
|
||||
the letter indicates a unique hotspot,
|
||||
and the number an identifier between related hotspots:
|
||||
((A-1, B-2, C-3), (D-1, E-2, F-3), (G-1, H-2, I-3)) ... (1->(A, D, G), 2->(B, E, H), 3->(C, F, I))
|
||||
*/
|
||||
val finalMap = mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]]()
|
||||
.addAll(
|
||||
hotSpotLayersOverTime.take(1).flatMap { entry =>
|
||||
entry.map { f => (f.DisplayLocation, Map.empty[PlanetSideEmpire.Value, Seq[Long]]) }
|
||||
}
|
||||
)
|
||||
//note: this pre-seeding of keys allows us to skip a getOrElse call in the foldLeft
|
||||
hotSpotLayersOverTime.foldLeft(finalMap) { (map, list) =>
|
||||
list.foreach { entry =>
|
||||
val key = entry.DisplayLocation
|
||||
val newValues = entry.Activity.map { case (f, e) => (f, e.Heat.toLong) }
|
||||
val combinedValues = map(key).map { case (f, e) => (f, e :+ newValues(f)) }
|
||||
map.put(key, combinedValues)
|
||||
}
|
||||
map
|
||||
}.toMap
|
||||
finalMap //explicit for no good reason
|
||||
}
|
||||
val heatMapModifier = FacilityHackParticipation.heatMapComparison(
|
||||
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values,
|
||||
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values
|
||||
)
|
||||
heatMapModifier * populationBalanceModifier
|
||||
}
|
||||
//4) hack time modifier
|
||||
//Captured major facilities without a lattice link unit and resecured major facilities with a lattice link unit
|
||||
// incur the full hack time if the module is not transported to a friendly facility
|
||||
//Captured major facilities with a lattice link unit and resecure major facilities without a lattice link uit
|
||||
// will incur an abbreviated duration
|
||||
val overallTimeMultiplier: Float = {
|
||||
if (
|
||||
building.Faction == PlanetSideEmpire.NEUTRAL ||
|
||||
building.NtuLevel == 0 ||
|
||||
building.Generator.map { _.Condition }.contains(PlanetSideGeneratorState.Destroyed)
|
||||
) { //the facility ran out of nanites or power during the hack or became neutral
|
||||
0f
|
||||
} else if (hasFlag) {
|
||||
if (completionTime >= hackTime) { //hack timed out without llu delivery
|
||||
0.25f
|
||||
} else if (isResecured) {
|
||||
0.5f + (if (hackTime <= completionTime * 0.3f) {
|
||||
completionTime.toFloat / hackTime.toFloat
|
||||
} else if (hackTime >= completionTime * 0.6f) {
|
||||
(hackTime - completionTime).toFloat / hackTime.toFloat
|
||||
} else {
|
||||
0f
|
||||
})
|
||||
} else {
|
||||
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
|
||||
}
|
||||
} else {
|
||||
if (isResecured) {
|
||||
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
|
||||
} else {
|
||||
0.5f
|
||||
}
|
||||
}
|
||||
}
|
||||
//5. individual contribution factors - by time
|
||||
val contributionPerPlayerByTime = playerContribution.collect {
|
||||
case (a, (_, d, t)) if d >= 600000 && math.abs(completionTime - t) < 5000 =>
|
||||
(a, 0.45f)
|
||||
case (a, (_, d, _)) if d >= 600000 =>
|
||||
(a, 0.25f)
|
||||
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
|
||||
(a, 0.25f * (0.5f + (d.toFloat / 600000f)))
|
||||
case (a, (_, _, _)) =>
|
||||
(a, 0.15f)
|
||||
}
|
||||
//6. competition bonus
|
||||
//This value will probably suck, and that's fine.
|
||||
val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
|
||||
contributionVictorSize,
|
||||
contributionOpposingSize,
|
||||
steamrollPercentage = 1.25f,
|
||||
steamrollBonus = 5L,
|
||||
overwhelmingOddsPercentage = 0.5f,
|
||||
overwhelmingOddsBonus = 15L
|
||||
)
|
||||
//7. calculate overall command experience points
|
||||
val finalCep: Long = math.ceil(
|
||||
math.max(0L, baseExperienceFromFacilityCapture) *
|
||||
populationModifier *
|
||||
competitionMultiplier *
|
||||
overallTimeMultiplier *
|
||||
Config.app.game.experience.cep.rate + competitionBonus
|
||||
).toLong
|
||||
//8. reward participants
|
||||
//Classically, only players in the SOI are rewarded, and the llu runner too
|
||||
val hackerId = hacker.CharId
|
||||
val events = building.Zone.AvatarEvents
|
||||
val playersInSoi = building.PlayersInSOI
|
||||
if (playersInSoi.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) {
|
||||
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep))
|
||||
}
|
||||
playersInSoi
|
||||
.filter { player => player.Faction == victorFaction && player.CharId != hackerId }
|
||||
.foreach { player =>
|
||||
val charId = player.CharId
|
||||
val contributionMultiplier = contributionPerPlayerByTime.getOrElse(charId, 1f)
|
||||
val outputValue = (finalCep * contributionMultiplier).toLong
|
||||
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, outputValue))
|
||||
}
|
||||
flagCarrier.collect {
|
||||
case player if !isResecured =>
|
||||
val charId: Long = player.CharId
|
||||
val finalModifiedCep: Long = {
|
||||
val durationPoints: Long = (hackTime - completionTime) / 1500L
|
||||
val betterDurationPoints: Long = if (durationPoints >= 200L) {
|
||||
durationPoints
|
||||
} else {
|
||||
200L + durationPoints
|
||||
}
|
||||
math.min(
|
||||
betterDurationPoints,
|
||||
(finalCep * Config.app.game.experience.cep.lluCarrierModifier).toLong
|
||||
)
|
||||
}
|
||||
ToDatabase.reportFacilityCapture(
|
||||
charId,
|
||||
building.Zone.Number,
|
||||
building.GUID.guid,
|
||||
finalModifiedCep,
|
||||
expType="llu"
|
||||
)
|
||||
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(charId, finalModifiedCep))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def eliminateClosestTowerFromParticipating(
|
||||
building: Building,
|
||||
list: Iterable[(UniquePlayer, Float, Seq[Kill])]
|
||||
): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
|
||||
val buildingPosition = building.Position.xy
|
||||
building
|
||||
.Zone
|
||||
.Buildings
|
||||
.values
|
||||
.filter { building => building.BuildingType == StructureType.Tower }
|
||||
.minByOption { tower => Vector3.DistanceSquared(buildingPosition, tower.Position.xy) }
|
||||
.map { tower =>
|
||||
val towerPosition = tower.Position.xy
|
||||
val towerRadius = math.pow(tower.Definition.SOIRadius.toDouble * 0.7d, 2d).toFloat
|
||||
list
|
||||
.map { case (p, f, kills) =>
|
||||
val filteredKills = kills.filter { kill => Vector3.DistanceSquared(kill.victim.Position.xy, towerPosition) <= towerRadius }
|
||||
(p, f, filteredKills)
|
||||
}
|
||||
.filter { case (_, _, kills) => kills.nonEmpty }
|
||||
}
|
||||
.getOrElse(list)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.serverobject.structures.participation
|
||||
|
||||
import net.psforever.objects.serverobject.structures.Building
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
case object NoParticipation extends ParticipationLogic {
|
||||
def building: Building = Building.NoBuilding
|
||||
def TryUpdate(): Unit = { /* nothing here */ }
|
||||
def RewardFacilityCapture(
|
||||
defenderFaction: PlanetSideEmpire.Value,
|
||||
attackingFaction: PlanetSideEmpire.Value,
|
||||
hacker: PlayerSource,
|
||||
hackTime: Long,
|
||||
completionTime: Long,
|
||||
isResecured: Boolean
|
||||
): Unit = { /* nothing here */ }
|
||||
override def PlayerContribution(timeDelay: Long): Map[Long, Float] = Map.empty[Long, Float]
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.serverobject.structures.participation
|
||||
|
||||
import net.psforever.objects.serverobject.structures.Building
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
//noinspection ScalaUnusedSymbol
|
||||
trait ParticipationLogic {
|
||||
def building: Building
|
||||
def TryUpdate(): Unit
|
||||
/**
|
||||
* na
|
||||
* @param defenderFaction those attempting to stop the hack
|
||||
* the `terminal` (above) and facility originally belonged to this empire
|
||||
* @param attackingFaction those attempting to progress the hack;
|
||||
* the `hacker` (below) belongs to this empire
|
||||
* @param hacker the player who hacked the capture terminal (above)
|
||||
* @param hackTime how long the over-all facility hack allows or requires
|
||||
* @param completionTime how long the facility hacking process lasted
|
||||
* @param isResecured whether `defendingFaction` or the `attackingFaction` succeeded;
|
||||
* the latter is called a "capture",
|
||||
* while the former is a "resecure"
|
||||
*/
|
||||
def RewardFacilityCapture(
|
||||
defenderFaction: PlanetSideEmpire.Value,
|
||||
attackingFaction: PlanetSideEmpire.Value,
|
||||
hacker: PlayerSource,
|
||||
hackTime: Long,
|
||||
completionTime: Long,
|
||||
isResecured: Boolean
|
||||
): Unit
|
||||
|
||||
def PlayerContribution(timeDelay: Long = 600): Map[Long, Float]
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.serverobject.structures.participation
|
||||
|
||||
import net.psforever.objects.serverobject.structures.Building
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
||||
import net.psforever.util.Config
|
||||
|
||||
final case class TowerHackParticipation(building: Building) extends FacilityHackParticipation {
|
||||
def TryUpdate(): Unit = {
|
||||
val list = building.PlayersInSOI
|
||||
updatePlayers(building.PlayersInSOI)
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastInfoRequest > 60000L) {
|
||||
updatePopulationOverTime(list, now, before = 300000L)
|
||||
}
|
||||
}
|
||||
|
||||
def RewardFacilityCapture(
|
||||
defenderFaction: PlanetSideEmpire.Value,
|
||||
attackingFaction: PlanetSideEmpire.Value,
|
||||
hacker: PlayerSource,
|
||||
hackTime: Long,
|
||||
completionTime: Long,
|
||||
isResecured: Boolean
|
||||
): Unit = {
|
||||
val (victorFaction, opposingFaction) = if (!isResecured) {
|
||||
(attackingFaction, defenderFaction)
|
||||
} else {
|
||||
(defenderFaction, attackingFaction)
|
||||
}
|
||||
val (contributionVictor, contributionOpposing, _) = {
|
||||
//TODO this is only to preserve a semblance of the original return type; fix this output
|
||||
val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction }
|
||||
val (b, c) = b1.partition { case (_, (p, _, _)) => p.Faction == opposingFaction }
|
||||
(a.values, b.values, c.values)
|
||||
}
|
||||
val contributionVictorSize = contributionVictor.size
|
||||
if (contributionVictorSize > 0) {
|
||||
//setup for ...
|
||||
import scala.concurrent.duration._
|
||||
val curr = System.currentTimeMillis()
|
||||
val contributionOpposingSize = contributionOpposing.size
|
||||
val populationIndices = playerPopulationOverTime.indices
|
||||
val allFactions = PlanetSideEmpire.values.filterNot {
|
||||
_ == PlanetSideEmpire.NEUTRAL
|
||||
}.toSeq
|
||||
val (victorPopulationByLayer, opposingPopulationByLayer) = {
|
||||
val individualPopulationByLayer = allFactions.map { f =>
|
||||
(f, populationIndices.indices.map { i => playerPopulationOverTime(i)(f) })
|
||||
}.toMap[PlanetSideEmpire.Value, Seq[Int]]
|
||||
(individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction))
|
||||
}
|
||||
val soiPlayers = building.PlayersInSOI
|
||||
//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(
|
||||
FacilityHackParticipation.allocateKillsByPlayers(
|
||||
building.Position,
|
||||
building.Definition.SOIRadius.toFloat,
|
||||
curr - 5.minutes.toMillis,
|
||||
curr,
|
||||
opposingFaction,
|
||||
contributionVictor
|
||||
),
|
||||
contributionOpposingSize
|
||||
)
|
||||
//2) peak population modifier
|
||||
//Towers should not be regarded as major battles.
|
||||
//As the population rises, the rewards decrease (dramatically).
|
||||
val populationModifier = FacilityHackParticipation.populationProgressModifier(
|
||||
victorPopulationByLayer,
|
||||
{ pop =>
|
||||
if (pop > 80) 0f
|
||||
else if (pop > 39) (80 - pop).toFloat * 0.01f
|
||||
else if (pop > 25) 0.5f
|
||||
else if (pop > 19) 0.55f
|
||||
else if (pop > 9) 0.6f
|
||||
else if (pop > 5) 0.75f
|
||||
else 1f
|
||||
},
|
||||
2
|
||||
)
|
||||
//3) competition multiplier
|
||||
val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier(
|
||||
victorPopulationByLayer,
|
||||
opposingPopulationByLayer,
|
||||
healthyPercentage = 1.25f
|
||||
)
|
||||
//4a. individual contribution factors - by time
|
||||
//Once again, an arbitrary five minute period.
|
||||
val contributionPerPlayerByTime = playerContribution.collect {
|
||||
case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 =>
|
||||
(a, 0.45f)
|
||||
case (a, (_, d, _)) if d >= 300000 =>
|
||||
(a, 0.25f)
|
||||
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
|
||||
(a, 0.25f * (0.5f + (d.toFloat / 300000f)))
|
||||
case (a, (_, _, _)) =>
|
||||
(a, 0.15f)
|
||||
}
|
||||
//4b. individual contribution factors - by distance to goal (secondary_capture)
|
||||
//Because the hack duration of towers is instantaneous, distance from terminal is a more important factor
|
||||
val contributionPerPlayerByDistanceFromGoal = {
|
||||
var minDistance: Float = Float.PositiveInfinity
|
||||
val location = building
|
||||
.CaptureTerminal
|
||||
.map { terminal => terminal.Position }
|
||||
.getOrElse { hacker.Position }
|
||||
soiPlayers
|
||||
.map { p =>
|
||||
val distance = Vector3.Distance(p.Position, location)
|
||||
minDistance = math.min(minDistance, distance)
|
||||
(p.CharId, distance)
|
||||
}
|
||||
.map { case (id, distance) =>
|
||||
(id, math.max(0.15f, minDistance / distance))
|
||||
}
|
||||
}.toMap[Long, Float]
|
||||
//5) token competition bonus
|
||||
//This value will probably suck, and that's fine.
|
||||
val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
|
||||
contributionVictorSize,
|
||||
contributionOpposingSize,
|
||||
steamrollPercentage = 1.25f,
|
||||
steamrollBonus = 2L,
|
||||
overwhelmingOddsPercentage = 0.5f,
|
||||
overwhelmingOddsBonus = 30L
|
||||
)
|
||||
//6. calculate overall command experience points
|
||||
val finalCep: Long = math.ceil(
|
||||
baseExperienceFromFacilityCapture *
|
||||
populationModifier *
|
||||
competitionMultiplier *
|
||||
Config.app.game.experience.cep.rate + competitionBonus
|
||||
).toLong
|
||||
//7. reward participants
|
||||
//Classically, only players in the SOI are rewarded
|
||||
val events = building.Zone.AvatarEvents
|
||||
soiPlayers
|
||||
.filter { player =>
|
||||
player.Faction == victorFaction && player.CharId != hacker.CharId
|
||||
}
|
||||
.foreach { player =>
|
||||
val charId = player.CharId
|
||||
val contributionTimeMultiplier = contributionPerPlayerByTime.getOrElse(charId, 0.5f)
|
||||
val contributionDistanceMultiplier = contributionPerPlayerByDistanceFromGoal.getOrElse(charId, 0.5f)
|
||||
val outputValue = (finalCep * contributionTimeMultiplier * contributionDistanceMultiplier).toLong
|
||||
events ! AvatarServiceMessage(
|
||||
player.Name,
|
||||
AvatarAction.AwardCep(0, outputValue)
|
||||
)
|
||||
}
|
||||
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
|
||||
}
|
||||
|
||||
playerContribution.clear()
|
||||
playerPopulationOverTime.reverse match {
|
||||
case entry :: _ => playerPopulationOverTime = Seq(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ import net.psforever.objects.serverobject.damage.DamageableAmenity
|
|||
import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior}
|
||||
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity}
|
||||
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
|
||||
import net.psforever.objects.vital.{HealFromTerm, RepairFromTerm, Vitality}
|
||||
import net.psforever.objects.vital.{HealFromTerminal, RepairFromTerminal, Vitality}
|
||||
import net.psforever.objects.zones.ZoneAware
|
||||
import net.psforever.packet.game.InventoryStateMessage
|
||||
import net.psforever.services.Service
|
||||
|
|
@ -291,7 +291,7 @@ object ProximityTerminalControl {
|
|||
healAmount
|
||||
}
|
||||
target.Health = health + finalHealthAmount
|
||||
target.LogActivity(HealFromTerm(AmenitySource(terminal), finalHealthAmount))
|
||||
target.LogActivity(HealFromTerminal(AmenitySource(terminal), finalHealthAmount))
|
||||
updateFunc(target)
|
||||
target.Health == maxHealth
|
||||
} else {
|
||||
|
|
@ -338,7 +338,7 @@ object ProximityTerminalControl {
|
|||
repairAmount
|
||||
}
|
||||
target.Armor = armor + finalRepairAmount
|
||||
target.LogActivity(RepairFromTerm(AmenitySource(terminal), finalRepairAmount))
|
||||
target.LogActivity(RepairFromTerminal(AmenitySource(terminal), finalRepairAmount))
|
||||
val zone = target.Zone
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class FacilityTurretControl(turret: FacilityTurret)
|
|||
turret.ControlledWeapon(wepNumber = 1).foreach {
|
||||
case weapon: Tool =>
|
||||
// recharge when last shot fired 3s delay, +1, 200ms interval
|
||||
if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) {
|
||||
if (weapon.Magazine < weapon.MaxMagazine && System.currentTimeMillis() - weapon.LastDischarge > 3000L) {
|
||||
weapon.Magazine += 1
|
||||
val seat = turret.Seat(0).get
|
||||
seat.occupant match {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.serverobject.turret
|
||||
|
||||
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition}
|
||||
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition, WithShields}
|
||||
import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets}
|
||||
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
|
||||
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
||||
|
|
@ -14,7 +14,8 @@ import scala.collection.mutable
|
|||
trait TurretDefinition
|
||||
extends MountableWeaponsDefinition
|
||||
with ResistanceProfileMutators
|
||||
with DamageResistanceModel {
|
||||
with DamageResistanceModel
|
||||
with WithShields {
|
||||
odef: ObjectDefinition =>
|
||||
Turrets(odef.ObjectId) //let throw NoSuchElementException
|
||||
/* key - upgrade, value - weapon definition */
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ final case class AmenitySource(
|
|||
health: Int,
|
||||
Orientation: Vector3,
|
||||
occupants: List[SourceEntry],
|
||||
installation: SourceEntry,
|
||||
hacked: Option[HackInfo],
|
||||
unique: UniqueAmenity
|
||||
) extends SourceWithHealthEntry {
|
||||
|
|
@ -54,6 +55,7 @@ object AmenitySource {
|
|||
health,
|
||||
obj.Orientation,
|
||||
Nil,
|
||||
SourceEntry(obj.Owner),
|
||||
hackData,
|
||||
sourcing.UniqueAmenity(obj.Zone.Number, obj.GUID, obj.Position)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.sourcing
|
||||
|
||||
import net.psforever.objects.avatar.scoring.Life
|
||||
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
|
|
@ -28,7 +29,7 @@ final case class PlayerSource(
|
|||
jumping: Boolean,
|
||||
Modifiers: ResistanceProfile,
|
||||
bep: Long,
|
||||
kills: Seq[Any],
|
||||
progress: Life,
|
||||
unique: UniquePlayer
|
||||
) extends SourceWithHealthEntry {
|
||||
override def Name: String = unique.name
|
||||
|
|
@ -46,6 +47,7 @@ object PlayerSource {
|
|||
val exosuit = p.ExoSuit
|
||||
val faction = p.Faction
|
||||
val seatedEntity = mountableAndSeat(p)
|
||||
val avatar = p.avatar
|
||||
PlayerSource(
|
||||
p.Definition,
|
||||
exosuit,
|
||||
|
|
@ -58,13 +60,17 @@ object PlayerSource {
|
|||
p.Crouching,
|
||||
p.Jumping,
|
||||
ExoSuitDefinition.Select(exosuit, faction),
|
||||
p.avatar.bep,
|
||||
kills = Nil,
|
||||
avatar.bep,
|
||||
progress = avatar.scorecard.CurrentLife,
|
||||
UniquePlayer(p.CharId, p.Name, p.Sex, faction)
|
||||
)
|
||||
}
|
||||
|
||||
def apply(name: String, faction: PlanetSideEmpire.Value, position: Vector3): PlayerSource = {
|
||||
this(UniquePlayer(0L, name, CharacterSex.Male, faction), position)
|
||||
}
|
||||
|
||||
def apply(unique: UniquePlayer, position: Vector3): PlayerSource = {
|
||||
new PlayerSource(
|
||||
GlobalDefinitions.avatar,
|
||||
ExoSuitType.Standard,
|
||||
|
|
@ -78,8 +84,8 @@ object PlayerSource {
|
|||
jumping = false,
|
||||
GlobalDefinitions.Standard,
|
||||
bep = 0L,
|
||||
kills = Nil,
|
||||
UniquePlayer(0L, name, CharacterSex.Male, faction)
|
||||
progress = tokenLife,
|
||||
unique
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -116,6 +122,7 @@ object PlayerSource {
|
|||
def inSeat(player: Player, source: SourceEntry, seatNumber: Int): PlayerSource = {
|
||||
val exosuit = player.ExoSuit
|
||||
val faction = player.Faction
|
||||
val avatar = player.avatar
|
||||
PlayerSource(
|
||||
player.Definition,
|
||||
exosuit,
|
||||
|
|
@ -128,12 +135,25 @@ object PlayerSource {
|
|||
player.Crouching,
|
||||
player.Jumping,
|
||||
ExoSuitDefinition.Select(exosuit, faction),
|
||||
player.avatar.bep,
|
||||
kills = Nil,
|
||||
avatar.bep,
|
||||
progress = tokenLife,
|
||||
UniquePlayer(player.CharId, player.Name, player.Sex, faction)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a copy of a normal player source entity
|
||||
* but the `seatedIn` field is overrode to point at the specified vehicle and seat number.<br>
|
||||
* Don't think too much about it.
|
||||
* @param player `SourceEntry` for a player
|
||||
* @param source `SourceEntry` for the aforementioned mountable entity
|
||||
* @param seatNumber the attributed seating index in which the player is mounted in `source`
|
||||
* @return a `PlayerSource` entity
|
||||
*/
|
||||
def inSeat(player: PlayerSource, source: SourceEntry, seatNumber: Int): PlayerSource = {
|
||||
player.copy(seatedIn = Some((source, seatNumber)))
|
||||
}
|
||||
|
||||
/**
|
||||
* "Nobody is my name: Nobody they call me –
|
||||
* my mother and my father and all my other companions”
|
||||
|
|
@ -142,4 +162,9 @@ object PlayerSource {
|
|||
* the others first: this will be my guest-gift to you.”
|
||||
*/
|
||||
final val Nobody = PlayerSource("Nobody", PlanetSideEmpire.NEUTRAL, Vector3.Zero)
|
||||
|
||||
/**
|
||||
* Used to dummy the statistics value for shallow player source entities.
|
||||
*/
|
||||
private val tokenLife: Life = Life()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,15 @@ final case class VehicleSource(
|
|||
Orientation: Vector3,
|
||||
Velocity: Option[Vector3],
|
||||
deployed: DriveState.Value,
|
||||
owner: Option[UniquePlayer],
|
||||
occupants: List[SourceEntry],
|
||||
Modifiers: ResistanceProfile,
|
||||
unique: UniqueVehicle
|
||||
) extends SourceWithHealthEntry with SourceWithShieldsEntry {
|
||||
def Name: String = SourceEntry.NameFormat(Definition.Name)
|
||||
def Health: Int = health
|
||||
def Shields: Int = shields
|
||||
def total: Int = health + shields
|
||||
def Name: String = SourceEntry.NameFormat(Definition.Name)
|
||||
def Health: Int = health
|
||||
def Shields: Int = shields
|
||||
def total: Int = health + shields
|
||||
}
|
||||
|
||||
object VehicleSource {
|
||||
|
|
@ -42,6 +43,7 @@ object VehicleSource {
|
|||
obj.Orientation,
|
||||
obj.Velocity,
|
||||
obj.DeploymentState,
|
||||
None,
|
||||
Nil,
|
||||
obj.Definition.asInstanceOf[ResistanceProfile],
|
||||
UniqueVehicle(
|
||||
|
|
@ -52,13 +54,15 @@ object VehicleSource {
|
|||
obj.OriginalOwnerName.getOrElse("none")
|
||||
)
|
||||
)
|
||||
vehicle.copy(occupants = {
|
||||
obj.Seats.map { case (seatNumber, seat) =>
|
||||
//shallow information that references the existing source entry
|
||||
vehicle.copy(
|
||||
owner = obj.Owners,
|
||||
occupants = obj.Seats.map { case (seatNumber, seat) =>
|
||||
seat.occupant match {
|
||||
case Some(p) => PlayerSource.inSeat(p, vehicle, seatNumber) //shallow
|
||||
case Some(p) => PlayerSource.inSeat(p, vehicle, seatNumber)
|
||||
case _ => PlayerSource.Nobody
|
||||
}
|
||||
}.toList
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.objects.vehicles
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import akka.actor.{ActorRef, Cancellable}
|
||||
import net.psforever.actors.commands.NtuCommand
|
||||
import net.psforever.actors.zone.BuildingActor
|
||||
import net.psforever.objects.serverobject.deploy.Deployment
|
||||
|
|
@ -13,17 +13,18 @@ import net.psforever.types.DriveState
|
|||
import net.psforever.services.Service
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.objects.serverobject.transfer.TransferContainer.TransferMaterial
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
|
||||
trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
|
||||
var panelAnimationFunc: () => Unit = NoCharge
|
||||
var ntuChargingTick = Default.Cancellable
|
||||
var ntuChargingTick: Cancellable = Default.Cancellable
|
||||
findChargeTargetFunc = Vehicles.FindANTChargingSource
|
||||
findDischargeTargetFunc = Vehicles.FindANTDischargingTarget
|
||||
|
||||
def TransferMaterial = Ntu.Nanites
|
||||
def TransferMaterial: TransferMaterial = Ntu.Nanites
|
||||
|
||||
def ChargeTransferObject: Vehicle with NtuContainer
|
||||
|
||||
|
|
@ -186,8 +187,10 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
|
|||
val chargeToDeposit = if (min == 0) {
|
||||
transferTarget match {
|
||||
case Some(silo: ResourceSilo) =>
|
||||
// Silos would charge from 0-100% in roughly 105s on live (~20%-100% https://youtu.be/veOWToR2nSk?t=1402)
|
||||
scala.math.min(scala.math.min(silo.MaxNtuCapacitor / 105, chargeable.NtuCapacitor), max)
|
||||
scala.math.min(
|
||||
scala.math.min(silo.MaxNtuCapacitor / silo.Definition.ChargeTime.toMillis.toFloat, chargeable.NtuCapacitor),
|
||||
max
|
||||
)
|
||||
case _ =>
|
||||
0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,9 @@ import akka.actor.{Actor, Cancellable}
|
|||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects._
|
||||
import net.psforever.packet.game.{
|
||||
CargoMountPointStatusMessage,
|
||||
ObjectAttachMessage,
|
||||
ObjectDetachMessage,
|
||||
PlanetsideAttributeMessage
|
||||
}
|
||||
import net.psforever.objects.sourcing.VehicleSource
|
||||
import net.psforever.objects.vital.VehicleCargoMountActivity
|
||||
import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage}
|
||||
import net.psforever.types.{BailType, CargoStatus, PlanetSideGUID, Vector3}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.Service
|
||||
|
|
@ -35,14 +32,12 @@ trait CarrierBehavior {
|
|||
cargoDismountTimer.cancel()
|
||||
val obj = CarrierObject
|
||||
val zone = obj.Zone
|
||||
zone.GUID(isMounting) match {
|
||||
case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
|
||||
case _ => ;
|
||||
zone.GUID(isMounting).collect {
|
||||
case v : Vehicle => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
|
||||
}
|
||||
isMounting = None
|
||||
zone.GUID(isDismounting) match {
|
||||
case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID)
|
||||
case _ => ;
|
||||
zone.GUID(isDismounting).collect {
|
||||
case v : Vehicle => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID)
|
||||
}
|
||||
isDismounting = None
|
||||
}
|
||||
|
|
@ -89,9 +84,8 @@ trait CarrierBehavior {
|
|||
)
|
||||
}
|
||||
else {
|
||||
obj.Zone.GUID(isMounting) match {
|
||||
case Some(v: Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
|
||||
case _ => ;
|
||||
obj.Zone.GUID(isMounting).collect {
|
||||
case v: Vehicle => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
|
||||
}
|
||||
isMounting = None
|
||||
}
|
||||
|
|
@ -115,10 +109,9 @@ trait CarrierBehavior {
|
|||
kicked = false
|
||||
)
|
||||
case _ =>
|
||||
obj.CargoHold(mountPoint) match {
|
||||
case Some(hold) if hold.isOccupied && hold.occupant.get.GUID == cargo_guid =>
|
||||
hold.unmount(hold.occupant.get)
|
||||
case _ => ;
|
||||
obj.CargoHold(mountPoint).collect {
|
||||
case hold if hold.isOccupied && hold.occupant.get.GUID == cargo_guid =>
|
||||
CarrierBehavior.CargoDismountAction(obj, hold.occupant.get, hold, BailType.Normal)
|
||||
}
|
||||
false
|
||||
}
|
||||
|
|
@ -135,17 +128,15 @@ trait CarrierBehavior {
|
|||
CarrierBehavior.CheckCargoDismount(cargo_guid, mountPoint, iteration + 1, bailed)
|
||||
)
|
||||
} else {
|
||||
zone.GUID(isDismounting.getOrElse(cargo_guid)) match {
|
||||
case Some(cargo: Vehicle) =>
|
||||
zone.GUID(isDismounting.getOrElse(cargo_guid)).collect {
|
||||
case cargo: Vehicle =>
|
||||
cargo.Actor ! CargoBehavior.EndCargoDismounting(guid)
|
||||
case _ => ;
|
||||
}
|
||||
isDismounting = None
|
||||
}
|
||||
} else {
|
||||
zone.GUID(isDismounting.getOrElse(cargo_guid)) match {
|
||||
case Some(cargo: Vehicle) => cargo.Actor ! CargoBehavior.EndCargoDismounting(guid)
|
||||
case _ => ;
|
||||
zone.GUID(isDismounting.getOrElse(cargo_guid)).collect {
|
||||
case cargo: Vehicle => cargo.Actor ! CargoBehavior.EndCargoDismounting(guid)
|
||||
}
|
||||
isDismounting = None
|
||||
}
|
||||
|
|
@ -213,10 +204,8 @@ object CarrierBehavior {
|
|||
if (distance <= 64) {
|
||||
//cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it
|
||||
log.debug(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance")
|
||||
hold.mount(cargo)
|
||||
cargo.MountedIn = carrierGUID
|
||||
CargoMountAction(carrier, cargo, hold, carrierGUID)
|
||||
cargo.Velocity = None
|
||||
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID)
|
||||
zone.VehicleEvents ! VehicleServiceMessage(
|
||||
s"${cargo.Actor}",
|
||||
VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health))
|
||||
|
|
@ -358,9 +347,7 @@ object CarrierBehavior {
|
|||
//obviously, don't do this
|
||||
} else if (iteration > 40) {
|
||||
//cargo vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold
|
||||
hold.mount(cargo)
|
||||
cargo.MountedIn = carrierGUID
|
||||
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID)
|
||||
CargoMountAction(carrier, cargo, hold, carrierGUID)
|
||||
CargoMountBehaviorForAll(carrier, cargo, mountPoint)
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(cargo)
|
||||
false
|
||||
|
|
@ -441,9 +428,10 @@ object CarrierBehavior {
|
|||
val zone = carrier.Zone
|
||||
carrier.CargoHolds.find({ case (_, hold) => hold.occupant.contains(cargo) }) match {
|
||||
case Some((mountPoint, hold)) =>
|
||||
cargo.MountedIn = None
|
||||
hold.unmount(
|
||||
CarrierBehavior.CargoDismountAction(
|
||||
carrier,
|
||||
cargo,
|
||||
hold,
|
||||
if (bailed) BailType.Bailed else if (kicked) BailType.Kicked else BailType.Normal
|
||||
)
|
||||
val driverOpt = cargo.Seats(0).occupant
|
||||
|
|
@ -532,7 +520,7 @@ object CarrierBehavior {
|
|||
targetGUID: PlanetSideGUID
|
||||
): Unit = {
|
||||
target match {
|
||||
case Some(_: Vehicle) => ;
|
||||
case Some(_: Vehicle) => ()
|
||||
case Some(_) => log.error(s"$decorator target $targetGUID no longer identifies as a vehicle")
|
||||
case None => log.error(s"$decorator target $targetGUID has gone missing")
|
||||
}
|
||||
|
|
@ -653,4 +641,62 @@ object CarrierBehavior {
|
|||
)
|
||||
msgs
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param carrier the ferrying vehicle
|
||||
* @param cargo the ferried vehicle
|
||||
* @param hold na
|
||||
* @param carrierGuid the ferrying vehicle's unique identifier
|
||||
*/
|
||||
private def CargoMountAction(
|
||||
carrier: Vehicle,
|
||||
cargo: Vehicle,
|
||||
hold: Cargo,
|
||||
carrierGuid: PlanetSideGUID): Unit = {
|
||||
hold.mount(cargo)
|
||||
cargo.MountedIn = carrierGuid
|
||||
val event = VehicleCargoMountActivity(VehicleSource(carrier), VehicleSource(cargo), carrier.Zone.Number)
|
||||
cargo.LogActivity(event)
|
||||
cargo.Seats
|
||||
.filterNot(_._1 == 0) /*ignore driver*/
|
||||
.values
|
||||
.collect {
|
||||
case seat if seat.isOccupied =>
|
||||
seat.occupants.foreach { player =>
|
||||
player.LogActivity(event)
|
||||
}
|
||||
}
|
||||
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGuid)
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param carrier the ferrying vehicle
|
||||
* @param cargo the ferried vehicle
|
||||
* @param hold na
|
||||
* @param bailType na
|
||||
*/
|
||||
private def CargoDismountAction(
|
||||
carrier: Vehicle,
|
||||
cargo: Vehicle,
|
||||
hold: Cargo,
|
||||
bailType: BailType.Value
|
||||
): Unit = {
|
||||
cargo.MountedIn = None
|
||||
hold.unmount(cargo, bailType)
|
||||
val event = VehicleCargoMountActivity(VehicleSource(carrier), VehicleSource(cargo), carrier.Zone.Number)
|
||||
cargo.LogActivity(event)
|
||||
cargo.Seats
|
||||
.filterNot(_._1 == 0) /*ignore driver*/
|
||||
.values
|
||||
.collect {
|
||||
case seat if seat.isOccupied =>
|
||||
seat.occupants.foreach { player =>
|
||||
player.LogActivity(event)
|
||||
player.ContributionFrom(cargo)
|
||||
player.ContributionFrom(carrier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,8 +170,8 @@ class BfrControl(vehicle: Vehicle)
|
|||
specialArmWeaponEquipManagement(item, slot, handiness)
|
||||
}
|
||||
|
||||
override def dismountCleanup(seatBeingDismounted: Int): Unit = {
|
||||
super.dismountCleanup(seatBeingDismounted)
|
||||
override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = {
|
||||
super.dismountCleanup(seatBeingDismounted, player)
|
||||
if (!vehicle.Seats.values.exists(_.isOccupied)) {
|
||||
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
|
||||
case Some(subsys) =>
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ class DeployingVehicleControl(vehicle: Vehicle)
|
|||
case msg : Deployment.TryUndeploy =>
|
||||
deployBehavior.apply(msg)
|
||||
|
||||
case msg @ Mountable.TryDismount(_, seat_num, _) =>
|
||||
case msg @ Mountable.TryDismount(player, seat_num, _) =>
|
||||
dismountBehavior.apply(msg)
|
||||
dismountCleanup(seat_num)
|
||||
dismountCleanup(seat_num, player)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ import net.psforever.objects.serverobject.hackable.GenericHackables
|
|||
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
|
||||
import net.psforever.objects.serverobject.repair.RepairableVehicle
|
||||
import net.psforever.objects.serverobject.terminals.Terminal
|
||||
import net.psforever.objects.sourcing.{SourceEntry, VehicleSource}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
|
||||
import net.psforever.objects.vehicles._
|
||||
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
|
||||
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge}
|
||||
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, VehicleDismountActivity, VehicleMountActivity}
|
||||
import net.psforever.objects.vital.environment.EnvironmentReason
|
||||
import net.psforever.objects.vital.etc.SuicideReason
|
||||
import net.psforever.objects.zones._
|
||||
|
|
@ -128,9 +128,9 @@ class VehicleControl(vehicle: Vehicle)
|
|||
mountBehavior.apply(msg)
|
||||
mountCleanup(mount_point, player)
|
||||
|
||||
case msg @ Mountable.TryDismount(_, seat_num, _) =>
|
||||
case msg @ Mountable.TryDismount(player, seat_num, _) =>
|
||||
dismountBehavior.apply(msg)
|
||||
dismountCleanup(seat_num)
|
||||
dismountCleanup(seat_num, player)
|
||||
|
||||
case CommonMessages.ChargeShields(amount, motivator) =>
|
||||
chargeShields(amount, motivator.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) })
|
||||
|
|
@ -237,9 +237,9 @@ class VehicleControl(vehicle: Vehicle)
|
|||
|
||||
def commonDisabledBehavior: Receive = checkBehavior
|
||||
.orElse {
|
||||
case msg @ Mountable.TryDismount(_, seat_num, _) =>
|
||||
case msg @ Mountable.TryDismount(user, seat_num, _) =>
|
||||
dismountBehavior.apply(msg)
|
||||
dismountCleanup(seat_num)
|
||||
dismountCleanup(seat_num, user)
|
||||
|
||||
case Vehicle.Deconstruct(time) =>
|
||||
time match {
|
||||
|
|
@ -286,7 +286,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
val seatGroup = vehicle.SeatPermissionGroup(seatNumber).getOrElse(AccessPermissionGroup.Passenger)
|
||||
val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire)
|
||||
(if (seatGroup == AccessPermissionGroup.Driver) {
|
||||
vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked
|
||||
vehicle.OwnerGuid.contains(user.GUID) || vehicle.OwnerGuid.isEmpty || permission != VehicleLockState.Locked
|
||||
} else {
|
||||
permission != VehicleLockState.Locked
|
||||
}) &&
|
||||
|
|
@ -297,6 +297,8 @@ class VehicleControl(vehicle: Vehicle)
|
|||
val obj = MountableObject
|
||||
obj.PassengerInSeat(user) match {
|
||||
case Some(seatNumber) =>
|
||||
val vsrc = VehicleSource(vehicle)
|
||||
user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number))
|
||||
//if the driver mount, change ownership if that is permissible for this vehicle
|
||||
if (seatNumber == 0 && !obj.OwnerName.contains(user.Name) && obj.Definition.CanBeOwned.nonEmpty) {
|
||||
//whatever vehicle was previously owned
|
||||
|
|
@ -325,7 +327,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
vehicle.DeploymentState == DriveState.Deployed || super.dismountTest(obj, seatNumber, user)
|
||||
}
|
||||
|
||||
def dismountCleanup(seatBeingDismounted: Int): Unit = {
|
||||
def dismountCleanup(seatBeingDismounted: Int, user: Player): Unit = {
|
||||
val obj = MountableObject
|
||||
// Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount
|
||||
if (!obj.Seats(0).isOccupied) {
|
||||
|
|
@ -340,6 +342,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
)
|
||||
}
|
||||
if (!obj.Seats(seatBeingDismounted).isOccupied) { //seat was vacated
|
||||
user.LogActivity(VehicleDismountActivity(VehicleSource(vehicle), PlayerSource(user), vehicle.Zone.Number))
|
||||
//we were only owning the vehicle while we sat in its driver seat
|
||||
val canBeOwned = obj.Definition.CanBeOwned
|
||||
if (canBeOwned.contains(false) && seatBeingDismounted == 0) {
|
||||
|
|
@ -348,7 +351,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
//are we already decaying? are we unowned? is no one seated anywhere?
|
||||
if (!decaying &&
|
||||
obj.Definition.undergoesDecay &&
|
||||
obj.Owner.isEmpty &&
|
||||
obj.OwnerGuid.isEmpty &&
|
||||
obj.Seats.values.forall(!_.isOccupied)) {
|
||||
decaying = true
|
||||
decayTimer = context.system.scheduler.scheduleOnce(
|
||||
|
|
@ -418,7 +421,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
Vehicles.Disown(obj.GUID, obj)
|
||||
if (!decaying &&
|
||||
obj.Definition.undergoesDecay &&
|
||||
obj.Owner.isEmpty &&
|
||||
obj.OwnerGuid.isEmpty &&
|
||||
obj.Seats.values.forall(!_.isOccupied)) {
|
||||
decaying = true
|
||||
decayTimer = context.system.scheduler.scheduleOnce(
|
||||
|
|
@ -890,7 +893,7 @@ object VehicleControl {
|
|||
|
||||
/**
|
||||
* Determine if a given activity entry would invalidate the act of charging vehicle shields this tick.
|
||||
* @param now the current time (in nanoseconds)
|
||||
* @param now the current time (in milliseconds)
|
||||
* @param act a `VitalsActivity` entry to test
|
||||
* @return `true`, if the shield charge would be blocked;
|
||||
* `false`, otherwise
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ package net.psforever.objects.vital
|
|||
import net.psforever.objects.PlanetSideGameObject
|
||||
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.sourcing.{AmenitySource, ObjectSource, PlayerSource, SourceEntry, SourceWithHealthEntry, VehicleSource}
|
||||
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource}
|
||||
import net.psforever.objects.vital.environment.EnvironmentReason
|
||||
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
|
||||
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
|
||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
||||
import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
|
||||
import net.psforever.util.Config
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
/* root */
|
||||
|
||||
|
|
@ -18,7 +21,21 @@ import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
|
|||
* Must keep track of the time (ms) the activity occurred.
|
||||
*/
|
||||
trait InGameActivity {
|
||||
val time: Long = System.currentTimeMillis()
|
||||
private var _time: Long = System.currentTimeMillis()
|
||||
|
||||
def time: Long = _time
|
||||
}
|
||||
|
||||
object InGameActivity {
|
||||
def ShareTime(benefactor: InGameActivity, donor: InGameActivity): InGameActivity = {
|
||||
benefactor._time = donor.time
|
||||
benefactor
|
||||
}
|
||||
|
||||
def SetTime(benefactor: InGameActivity, time: Long): InGameActivity = {
|
||||
benefactor._time = time
|
||||
benefactor
|
||||
}
|
||||
}
|
||||
|
||||
/* normal history */
|
||||
|
|
@ -28,13 +45,64 @@ trait InGameActivity {
|
|||
*/
|
||||
trait GeneralActivity extends InGameActivity
|
||||
|
||||
final case class SpawningActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry]) extends GeneralActivity
|
||||
trait SupportActivityCausedByAnother {
|
||||
def user: PlayerSource
|
||||
def amount: Int
|
||||
}
|
||||
|
||||
final case class ReconstructionActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry]) extends GeneralActivity
|
||||
final case class SpawningActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
|
||||
extends GeneralActivity
|
||||
|
||||
final case class ShieldCharge(amount: Int, cause: Option[SourceEntry]) extends GeneralActivity
|
||||
final case class ReconstructionActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
|
||||
extends GeneralActivity
|
||||
|
||||
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value) extends GeneralActivity
|
||||
final case class RevivingActivity(target: SourceEntry, user: PlayerSource, amount: Int, equipment: EquipmentDefinition)
|
||||
extends GeneralActivity with SupportActivityCausedByAnother
|
||||
|
||||
final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
|
||||
extends GeneralActivity
|
||||
|
||||
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
|
||||
extends GeneralActivity
|
||||
|
||||
sealed trait VehicleMountChange extends GeneralActivity {
|
||||
def vehicle: VehicleSource
|
||||
def zoneNumber: Int
|
||||
}
|
||||
|
||||
sealed trait VehiclePassengerMountChange extends VehicleMountChange {
|
||||
def player: PlayerSource
|
||||
}
|
||||
|
||||
sealed trait VehicleCargoMountChange extends VehicleMountChange {
|
||||
def cargo: VehicleSource
|
||||
}
|
||||
|
||||
final case class VehicleMountActivity(vehicle: VehicleSource, player: PlayerSource, zoneNumber: Int)
|
||||
extends VehiclePassengerMountChange
|
||||
|
||||
final case class VehicleDismountActivity(
|
||||
vehicle: VehicleSource,
|
||||
player: PlayerSource,
|
||||
zoneNumber: Int,
|
||||
pairedEvent: Option[VehicleMountActivity] = None
|
||||
) extends VehiclePassengerMountChange
|
||||
|
||||
final case class VehicleCargoMountActivity(vehicle: VehicleSource, cargo: VehicleSource, zoneNumber: Int)
|
||||
extends VehicleCargoMountChange
|
||||
|
||||
final case class VehicleCargoDismountActivity(
|
||||
vehicle: VehicleSource,
|
||||
cargo: VehicleSource,
|
||||
zoneNumber: Int,
|
||||
pairedEvent: Option[VehicleCargoMountActivity] = None
|
||||
) extends VehicleCargoMountChange
|
||||
|
||||
final case class Contribution(src: SourceUniqueness, entries: List[InGameActivity])
|
||||
extends GeneralActivity {
|
||||
val start: Long = entries.headOption.map { _.time }.getOrElse(System.currentTimeMillis())
|
||||
val end: Long = entries.lastOption.map { _.time }.getOrElse(start)
|
||||
}
|
||||
|
||||
/* vitals history */
|
||||
|
||||
|
|
@ -65,9 +133,8 @@ trait DamagingActivity extends VitalsActivity {
|
|||
|
||||
def health: Int = {
|
||||
(data.targetBefore, data.targetAfter) match {
|
||||
case (pb: PlayerSource, pa: PlayerSource) if pb.ExoSuit == ExoSuitType.MAX => pb.total - pa.total
|
||||
case (pb: SourceWithHealthEntry, pa: SourceWithHealthEntry) => pb.health - pa.health
|
||||
case _ => 0
|
||||
case (pb: SourceWithHealthEntry, pa: SourceWithHealthEntry) => pb.health - pa.health
|
||||
case _ => 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,9 +143,9 @@ final case class HealFromKit(kit_def: KitDefinition, amount: Int)
|
|||
extends HealingActivity
|
||||
|
||||
final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
|
||||
extends HealingActivity
|
||||
extends HealingActivity with SupportActivityCausedByAnother
|
||||
|
||||
final case class HealFromTerm(term: AmenitySource, amount: Int)
|
||||
final case class HealFromTerminal(term: AmenitySource, amount: Int)
|
||||
extends HealingActivity
|
||||
|
||||
final case class HealFromImplant(implant: ImplantType, amount: Int)
|
||||
|
|
@ -91,9 +158,9 @@ final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
|
|||
extends RepairingActivity()
|
||||
|
||||
final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
|
||||
extends RepairingActivity
|
||||
extends RepairingActivity with SupportActivityCausedByAnother
|
||||
|
||||
final case class RepairFromTerm(term: AmenitySource, amount: Int) extends RepairingActivity
|
||||
final case class RepairFromTerminal(term: AmenitySource, amount: Int) extends RepairingActivity
|
||||
|
||||
final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, vehicle: VehicleSource, amount: Int)
|
||||
extends RepairingActivity
|
||||
|
|
@ -158,11 +225,34 @@ trait InGameHistory {
|
|||
/**
|
||||
* An in-game event must be recorded.
|
||||
* Add new entry to the list (for recent activity).
|
||||
* Special handling must be conducted for certain events.
|
||||
* @param action the fully-informed entry
|
||||
* @return the list of previous changes to this entity
|
||||
*/
|
||||
def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = {
|
||||
action match {
|
||||
case Some(act: VehicleDismountActivity) =>
|
||||
history
|
||||
.findLast(_.isInstanceOf[VehicleMountActivity])
|
||||
.collect {
|
||||
case event: VehicleMountActivity if event.vehicle.unique == act.vehicle.unique =>
|
||||
history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
|
||||
}
|
||||
.orElse {
|
||||
history = history :+ act
|
||||
None
|
||||
}
|
||||
case Some(act: VehicleCargoDismountActivity) =>
|
||||
history
|
||||
.findLast(_.isInstanceOf[VehicleCargoMountActivity])
|
||||
.collect {
|
||||
case event: VehicleCargoMountActivity if event.vehicle.unique == act.vehicle.unique =>
|
||||
history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
|
||||
}
|
||||
.orElse {
|
||||
history = history :+ act
|
||||
None
|
||||
}
|
||||
case Some(act) =>
|
||||
history = history :+ act
|
||||
case None => ()
|
||||
|
|
@ -188,7 +278,7 @@ trait InGameHistory {
|
|||
LogActivity(DamageFromPainbox(result))
|
||||
case _: EnvironmentReason =>
|
||||
LogActivity(DamageFromEnvironment(result))
|
||||
case _ => ;
|
||||
case _ =>
|
||||
LogActivity(DamageFrom(result))
|
||||
if(result.adversarial.nonEmpty) {
|
||||
lastDamage = Some(result)
|
||||
|
|
@ -209,10 +299,55 @@ trait InGameHistory {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* activity that comes from another entity used for scoring;<br>
|
||||
* key - unique reference to that entity; value - history from that entity
|
||||
*/
|
||||
private val contributionInheritance: mutable.HashMap[SourceUniqueness, Contribution] =
|
||||
mutable.HashMap[SourceUniqueness, Contribution]()
|
||||
|
||||
def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Option[Contribution] = {
|
||||
if (target eq this) {
|
||||
None
|
||||
} else {
|
||||
val uniqueTarget = SourceEntry(target).unique
|
||||
(target.GetContribution(), contributionInheritance.get(uniqueTarget)) match {
|
||||
case (Some(in), Some(curr)) =>
|
||||
val end = curr.end
|
||||
val contribution = Contribution(uniqueTarget, curr.entries ++ in.filter(_.time > end))
|
||||
contributionInheritance.put(uniqueTarget, contribution)
|
||||
Some(contribution)
|
||||
case (Some(in), _) =>
|
||||
val contribution = Contribution(uniqueTarget, in)
|
||||
contributionInheritance.put(uniqueTarget, contribution)
|
||||
Some(contribution)
|
||||
case (None, _) =>
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def GetContribution(): Option[List[InGameActivity]] = {
|
||||
Option(GetContributionDuringPeriod(History, duration = Config.app.game.experience.longContributionTime))
|
||||
}
|
||||
|
||||
def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = {
|
||||
val earliestEndTime = System.currentTimeMillis() - duration
|
||||
list.collect {
|
||||
case event: DamagingActivity if event.health > 0 && event.time > earliestEndTime => event
|
||||
case event: RepairingActivity if event.amount > 0 && event.time > earliestEndTime => event
|
||||
}
|
||||
}
|
||||
|
||||
def HistoryAndContributions(): List[InGameActivity] = {
|
||||
History ++ contributionInheritance.values.toList
|
||||
}
|
||||
|
||||
def ClearHistory(): List[InGameActivity] = {
|
||||
lastDamage = None
|
||||
val out = history
|
||||
history = List.empty
|
||||
contributionInheritance.clear()
|
||||
out
|
||||
}
|
||||
}
|
||||
|
|
@ -221,14 +356,14 @@ object InGameHistory {
|
|||
def SpawnReconstructionActivity(
|
||||
obj: PlanetSideGameObject with FactionAffinity with InGameHistory,
|
||||
zoneNumber: Int,
|
||||
unit: Option[SourceEntry]
|
||||
unit: Option[PlanetSideGameObject with FactionAffinity with InGameHistory]
|
||||
): Unit = {
|
||||
val event: GeneralActivity = if (obj.History.nonEmpty || obj.History.headOption.exists {
|
||||
_.isInstanceOf[SpawningActivity]
|
||||
}) {
|
||||
ReconstructionActivity(ObjectSource(obj), zoneNumber, unit)
|
||||
val toUnitSource = unit.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }
|
||||
|
||||
val event: GeneralActivity = if (obj.History.isEmpty) {
|
||||
SpawningActivity(SourceEntry(obj), zoneNumber, toUnitSource)
|
||||
} else {
|
||||
SpawningActivity(ObjectSource(obj), zoneNumber, unit)
|
||||
ReconstructionActivity(SourceEntry(obj), zoneNumber, toUnitSource)
|
||||
}
|
||||
if (obj.History.lastOption match {
|
||||
case Some(evt: SpawningActivity) => evt != event
|
||||
|
|
@ -236,6 +371,13 @@ object InGameHistory {
|
|||
case _ => true
|
||||
}) {
|
||||
obj.LogActivity(event)
|
||||
unit.foreach { o => obj.ContributionFrom(o) }
|
||||
}
|
||||
}
|
||||
|
||||
def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Option[Contribution] = {
|
||||
target
|
||||
.GetContribution()
|
||||
.collect { case events => Contribution(SourceEntry(target).unique, events) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ class ActivityReport {
|
|||
* @return the time
|
||||
*/
|
||||
def Duration_=(time: FiniteDuration): FiniteDuration = {
|
||||
Duration_=(time.toNanos)
|
||||
Duration_=(time.toMillis)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -112,8 +112,8 @@ class ActivityReport {
|
|||
* @return the time
|
||||
*/
|
||||
def Duration_=(time: Long): FiniteDuration = {
|
||||
if (time > duration.toNanos) {
|
||||
duration = FiniteDuration(time, "nanoseconds")
|
||||
if (time > duration.toMillis) {
|
||||
duration = FiniteDuration(time, "milliseconds")
|
||||
Renew
|
||||
}
|
||||
Duration
|
||||
|
|
@ -177,7 +177,7 @@ class ActivityReport {
|
|||
* @return the current time
|
||||
*/
|
||||
def Renew: Long = {
|
||||
val t = System.nanoTime
|
||||
val t = System.currentTimeMillis()
|
||||
firstReport = firstReport.orElse(Some(t))
|
||||
lastReport = Some(t)
|
||||
t
|
||||
|
|
@ -191,6 +191,6 @@ class ActivityReport {
|
|||
heat = 0
|
||||
firstReport = None
|
||||
lastReport = None
|
||||
duration = FiniteDuration(0, "nanoseconds")
|
||||
duration = FiniteDuration(0, "milliseconds")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ class ZoneHotSpotProjector(zone: Zone, hotspots: ListBuffer[HotSpotInfo], blanki
|
|||
val attackerFaction = attacker.Faction
|
||||
val noPriorHotSpots = hotspots.isEmpty
|
||||
val duration = zone.HotSpotTimeFunction(defender, attacker)
|
||||
if (duration.toNanos > 0) {
|
||||
if (duration.toMillis > 0) {
|
||||
val hotspot = TryHotSpot(zone.HotSpotCoordinateFunction(location))
|
||||
trace(
|
||||
s"updating activity status for ${zone.id} hotspot x=${hotspot.DisplayLocation.x} y=${hotspot.DisplayLocation.y}"
|
||||
|
|
@ -191,11 +191,11 @@ class ZoneHotSpotProjector(zone: Zone, hotspots: ListBuffer[HotSpotInfo], blanki
|
|||
|
||||
case ZoneHotSpotProjector.BlankingPhase() | Zone.HotSpot.Cleanup() =>
|
||||
blanking.cancel()
|
||||
val curr: Long = System.nanoTime
|
||||
val curr: Long = System.currentTimeMillis()
|
||||
//blanking dated activity reports
|
||||
val changed = hotspots.flatMap(spot => {
|
||||
spot.Activity.collect {
|
||||
case (b, a: ActivityReport) if a.LastReport + a.Duration.toNanos <= curr =>
|
||||
case (b, a: ActivityReport) if a.LastReport + a.Duration.toMillis <= curr =>
|
||||
a.Clear() //this faction has no more activity in this sector
|
||||
(b, spot)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp
|
||||
|
||||
import enumeratum.values.IntEnumEntry
|
||||
|
||||
sealed abstract class EquipmentUseContextWrapper(val value: Int) extends IntEnumEntry {
|
||||
def equipment: Int
|
||||
def intermediate: Int
|
||||
}
|
||||
|
||||
sealed abstract class NoIntermediateUseContextWrapper(override val value: Int)
|
||||
extends EquipmentUseContextWrapper(value) {
|
||||
def intermediate: Int = 0
|
||||
}
|
||||
|
||||
final case class NoUse() extends NoIntermediateUseContextWrapper(value = -1) {
|
||||
def equipment: Int = 0
|
||||
}
|
||||
|
||||
final case class DamageWith(equipment: Int) extends NoIntermediateUseContextWrapper(value = 0)
|
||||
|
||||
final case class Destroyed(equipment: Int) extends NoIntermediateUseContextWrapper(value = 1)
|
||||
final case class ReviveAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 4)
|
||||
final case class AmenityDestroyed(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 10)
|
||||
final case class DriverKilled(equipment: Int) extends NoIntermediateUseContextWrapper(value = 12)
|
||||
final case class GunnerKilled(equipment: Int) extends NoIntermediateUseContextWrapper(value = 13)
|
||||
final case class PassengerKilled(equipment: Int) extends NoIntermediateUseContextWrapper(value = 14)
|
||||
final case class CargoDestroyed(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 15)
|
||||
final case class DriverAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 18)
|
||||
final case class HealKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 20)
|
||||
final case class ReviveKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 21)
|
||||
final case class RepairKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 22)
|
||||
final case class AmsRespawnKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 23)
|
||||
final case class HotDropKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 24)
|
||||
final case class HackKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 25)
|
||||
final case class LodestarRearmKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 26)
|
||||
final case class AmsResupplyKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 27)
|
||||
final case class RouterKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 28)
|
||||
|
|
@ -4,17 +4,11 @@ package net.psforever.objects.zones.exp
|
|||
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||
import akka.actor.typed.{Behavior, SupervisorStrategy}
|
||||
import net.psforever.objects.PlanetSideGameObject
|
||||
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, InGameHistory, ReconstructionActivity, RepairFromExoSuitChange, RepairingActivity, SpawningActivity}
|
||||
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
|
||||
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
|
||||
import net.psforever.objects.vital.interaction.DamageResult
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object ExperienceCalculator {
|
||||
def apply(zone: Zone): Behavior[Command] =
|
||||
|
|
@ -32,38 +26,6 @@ object ExperienceCalculator {
|
|||
RewardThisDeath(SourceEntry(obj), obj.LastDamage, obj.History)
|
||||
}
|
||||
}
|
||||
|
||||
def calculateExperience(
|
||||
victim: PlayerSource,
|
||||
history: Iterable[InGameActivity]
|
||||
): Long = {
|
||||
val lifespan = (history.headOption, history.lastOption) match {
|
||||
case (Some(spawn), Some(death)) => death.time - spawn.time
|
||||
case _ => 0L
|
||||
}
|
||||
val wasEverAMax = victim.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
|
||||
}
|
||||
val base = if (wasEverAMax) { //shamed
|
||||
250L
|
||||
} else if (victim.Seated || victim.kills.nonEmpty) {
|
||||
100L
|
||||
} else if (lifespan > 15000L) {
|
||||
50L
|
||||
} else {
|
||||
1L
|
||||
}
|
||||
if (base > 1) {
|
||||
//black ops modifier
|
||||
//TODO x10
|
||||
base
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone)
|
||||
|
|
@ -74,263 +36,10 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
|
|||
def onMessage(msg: Command): Behavior[Command] = {
|
||||
msg match {
|
||||
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
|
||||
rewardThisPlayerDeath(
|
||||
victim,
|
||||
lastDamage,
|
||||
limitHistoryToThisLife(history.toList)
|
||||
)
|
||||
case _ =>
|
||||
()
|
||||
KillAssists.rewardThisPlayerDeath(victim, lastDamage, history, zone.AvatarEvents)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
def rewardThisPlayerDeath(
|
||||
victim: PlayerSource,
|
||||
lastDamage: Option[DamageResult],
|
||||
history: List[InGameActivity]
|
||||
): Unit = {
|
||||
val everyone = determineKiller(lastDamage, history) match {
|
||||
case Some((result, killer: PlayerSource)) =>
|
||||
val assists = collectAssistsForPlayer(victim, history, Some(killer))
|
||||
val fullBep = KillDeathAssists.calculateExperience(killer, victim, history)
|
||||
val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
|
||||
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
|
||||
}.toSeq
|
||||
(victim, Death(hitSquad.map { _._1 }, history.last.time - history.head.time, fullBep)) +: hitSquad
|
||||
|
||||
case _ =>
|
||||
val assists = collectAssistsForPlayer(victim, history, None)
|
||||
val fullBep = ExperienceCalculator.calculateExperience(victim, history)
|
||||
val hitSquad = assists.map {
|
||||
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
|
||||
}.toSeq
|
||||
(victim, Death(hitSquad.map { _._1 }, history.last.time - history.head.time, fullBep)) +: hitSquad
|
||||
}
|
||||
val events = zone.AvatarEvents
|
||||
everyone.foreach { case (p, kda) =>
|
||||
events ! AvatarServiceMessage(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda))
|
||||
}
|
||||
}
|
||||
|
||||
def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
|
||||
val spawnIndex = history.indexWhere {
|
||||
case SpawningActivity(_, _, _) => true
|
||||
case _ => false
|
||||
}
|
||||
val endIndex = history.lastIndexWhere {
|
||||
case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
|
||||
case _ => false
|
||||
}
|
||||
if (spawnIndex == -1 || endIndex == -1) {
|
||||
Nil //throw VitalsHistoryException(history.head, "vitals history does not contain expected conditions")
|
||||
// } else
|
||||
// if (spawnIndex == -1) {
|
||||
// Nil //throw VitalsHistoryException(history.head, "vitals history does not contain initial spawn conditions")
|
||||
// } else if (endIndex == -1) {
|
||||
// Nil //throw VitalsHistoryException(history.last, "vitals history does not contain end of life conditions")
|
||||
} else {
|
||||
history.slice(spawnIndex, endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
def determineKiller(lastDamageActivity: Option[DamageResult], history: List[InGameActivity]): Option[(DamageResult, SourceEntry)] = {
|
||||
val now = System.currentTimeMillis()
|
||||
val compareTimeMillis = 10.seconds.toMillis
|
||||
lastDamageActivity
|
||||
.collect { case dam if now - dam.interaction.hitTime < compareTimeMillis => dam }
|
||||
.flatMap { dam => Some(dam, dam.adversarial) }
|
||||
.orElse {
|
||||
history.collect { case damage: DamagingActivity
|
||||
if now - damage.time < compareTimeMillis && damage.data.adversarial.nonEmpty =>
|
||||
damage.data
|
||||
}
|
||||
.flatMap { dam => Some(dam, dam.adversarial) }.lastOption
|
||||
}
|
||||
.collect { case (dam, Some(adv)) => (dam, adv.attacker) }
|
||||
}
|
||||
|
||||
private[exp] def collectAssistsForPlayer(
|
||||
victim: PlayerSource,
|
||||
history: List[InGameActivity],
|
||||
killerOpt: Option[PlayerSource]
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
// val cardinalSin = victim.ExoSuit == ExoSuitType.MAX || history.exists {
|
||||
// case SpawningActivity(p: PlayerSource,_,_) => p.ExoSuit == ExoSuitType.MAX
|
||||
// case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
|
||||
// case _ => false
|
||||
// }
|
||||
val initialHealth = history
|
||||
.headOption
|
||||
.collect { case SpawningActivity(p: PlayerSource,_,_) => p.health } match {
|
||||
case Some(value) => value.toFloat
|
||||
case _ => 100f
|
||||
}
|
||||
val healthAssists = collectHealthAssists(
|
||||
victim,
|
||||
history,
|
||||
initialHealth,
|
||||
allocateContributors(healthDamageContributors)
|
||||
)
|
||||
healthAssists.remove(0L)
|
||||
killerOpt.map { killer => healthAssists.remove(killer.CharId) }
|
||||
healthAssists.values
|
||||
}
|
||||
|
||||
private def allocateContributors(
|
||||
tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any
|
||||
)
|
||||
(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
/** players who have contributed to this death, and how much they have contributed<br>
|
||||
* key - character identifier,
|
||||
* value - (player, damage, total damage, number of shots) */
|
||||
val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
|
||||
tallyFunc(history, faction, participants)
|
||||
participants
|
||||
}
|
||||
|
||||
|
||||
|
||||
private def healthDamageContributors(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
participants: mutable.LongMap[ContributionStats]
|
||||
): Seq[(Long, Int)] = {
|
||||
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
|
||||
* key - character identifier,
|
||||
* value - current damage contribution */
|
||||
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
|
||||
history.tail.foreach {
|
||||
case d: DamagingActivity if d.health > 0 =>
|
||||
inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
|
||||
case _: RepairingActivity => ()
|
||||
case h: HealingActivity =>
|
||||
inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder)
|
||||
case _ => ()
|
||||
}
|
||||
inOrder
|
||||
}
|
||||
|
||||
private def collectHealthAssists(
|
||||
victim: SourceEntry,
|
||||
history: List[InGameActivity],
|
||||
topHealth: Float,
|
||||
func: (List[InGameActivity], PlanetSideEmpire.Value)=>mutable.LongMap[ContributionStats]
|
||||
): mutable.LongMap[ContributionStatsOutput] = {
|
||||
val healthAssists = func(history, victim.Faction)
|
||||
.filterNot { case (_, kda) => kda.amount <= 0 }
|
||||
.map { case (id, kda) =>
|
||||
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / topHealth))
|
||||
}
|
||||
healthAssists.remove(victim.CharId)
|
||||
healthAssists
|
||||
}
|
||||
|
||||
private def contributeWithDamagingActivity(
|
||||
activity: DamagingActivity,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
amount: Int,
|
||||
participants: mutable.LongMap[ContributionStats],
|
||||
order: Seq[(Long, Int)]
|
||||
): Seq[(Long, Int)] = {
|
||||
activity.data.adversarial match {
|
||||
case Some(Adversarial(attacker: PlayerSource, _, _))
|
||||
if attacker.Faction != faction =>
|
||||
val whoId = attacker.CharId
|
||||
val wepid = activity.data.interaction.cause.attribution
|
||||
val time = activity.time
|
||||
val updatedEntry = participants.get(whoId) match {
|
||||
case Some(mod) =>
|
||||
//previous attacker, just add to entry
|
||||
val firstWeapon = mod.weapons.head
|
||||
val weapons = if (firstWeapon.weapon_id == wepid) {
|
||||
firstWeapon.copy(amount = firstWeapon.amount + amount, shots = firstWeapon.shots + 1, time = time) +: mod.weapons.tail
|
||||
} else {
|
||||
WeaponStats(wepid, amount, 1, time) +: mod.weapons
|
||||
}
|
||||
mod.copy(
|
||||
amount = mod.amount + amount,
|
||||
weapons = weapons,
|
||||
totalDamage = mod.totalDamage + amount,
|
||||
shots = mod.shots + 1,
|
||||
time = activity.time
|
||||
)
|
||||
case None =>
|
||||
//new attacker, new entry
|
||||
ContributionStats(
|
||||
attacker,
|
||||
Seq(WeaponStats(wepid, amount, 1, time)),
|
||||
amount,
|
||||
amount,
|
||||
1,
|
||||
time
|
||||
)
|
||||
}
|
||||
participants.put(whoId, updatedEntry)
|
||||
order.indexWhere({ case (id, _) => id == whoId }) match {
|
||||
case 0 =>
|
||||
//ongoing attack by same player
|
||||
val entry = order.head
|
||||
(entry._1, entry._2 + amount) +: order.tail
|
||||
case _ =>
|
||||
//different player than immediate prior attacker
|
||||
(whoId, amount) +: order
|
||||
}
|
||||
case _ =>
|
||||
//damage that does not lead to contribution
|
||||
order.headOption match {
|
||||
case Some((id, dam)) =>
|
||||
if (id == 0L) {
|
||||
(0L, dam + amount) +: order.tail //pool
|
||||
} else {
|
||||
(0L, amount) +: order //new
|
||||
}
|
||||
case None =>
|
||||
order
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def contributeWithRecoveryActivity(
|
||||
amount: Int,
|
||||
participants: mutable.LongMap[ContributionStats],
|
||||
order: Seq[(Long, Int)]
|
||||
): Seq[(Long, Int)] = {
|
||||
var amt = amount
|
||||
var count = 0
|
||||
var newOrder: 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
|
||||
}
|
||||
pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size)
|
||||
}
|
||||
newOrder = (id, newTotal) +: newOrder
|
||||
participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
|
||||
amt = 0
|
||||
}
|
||||
}
|
||||
count += 1
|
||||
amt > 0
|
||||
}
|
||||
newOrder ++ order.drop(count)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
642
src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
Normal file
642
src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
Normal file
|
|
@ -0,0 +1,642 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import net.psforever.objects.avatar.scoring.{Assist, Death, KDAStat, Kill}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||
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 scala.annotation.tailrec
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* One player will interact using any number of weapons they possess
|
||||
* that will affect a different player - the target.
|
||||
* A kill is counted as the last interaction that affects a target so as to drop their health to zero.
|
||||
* An assist is counted as every other interaction that affects the target up until the kill interaction
|
||||
* in a similar way to the kill interaction.
|
||||
* @see `ContributionStats`
|
||||
* @see `ContributionStatsOutput`
|
||||
* @see `DamagingActivity`
|
||||
* @see `HealingActivity`
|
||||
* @see `InGameActivity`
|
||||
* @see `InGameHistory`
|
||||
* @see `PlayerSource`
|
||||
* @see `RepairingActivity`
|
||||
* @see `SourceEntry`
|
||||
*/
|
||||
object KillAssists {
|
||||
/**
|
||||
* Primary landing point for calculating the rewards given for player death.
|
||||
* Rewards in the form of "battle experience points" are given:
|
||||
* to the player held responsible for the other player's death - the killer;
|
||||
* all players whose efforts managed to deal damage to the player who died prior to the killer - assists.
|
||||
* @param victim player that died
|
||||
* @param lastDamage purported as the in-game activity that resulted in the player dying
|
||||
* @param history chronology of activity the game considers noteworthy;
|
||||
* `lastDamage` should be within this chronology
|
||||
* @param eventBus where to send the results of the experience determination(s)
|
||||
* @see `ActorRef`
|
||||
* @see `AvatarAction.UpdateKillsDeathsAssists`
|
||||
* @see `AvatarServiceMessage`
|
||||
* @see `DamageResult`
|
||||
* @see `rewardThisPlayerDeath`
|
||||
*/
|
||||
private[exp] def rewardThisPlayerDeath(
|
||||
victim: PlayerSource,
|
||||
lastDamage: Option[DamageResult],
|
||||
history: Iterable[InGameActivity],
|
||||
eventBus: ActorRef
|
||||
): Unit = {
|
||||
rewardThisPlayerDeath(victim, lastDamage, history).foreach { case (p, kda) =>
|
||||
eventBus ! AvatarServiceMessage(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary innards of the functionality of calculating the rewards given for player death.
|
||||
* @param victim player that died
|
||||
* @param lastDamage purported as the in-game activity that resulted in the player dying
|
||||
* @param history chronology of activity the game considers noteworthy;
|
||||
* `lastDamage` should be within this chronology
|
||||
* @return na
|
||||
* @see `Assist`
|
||||
* @see `calculateExperience`
|
||||
* @see `collectKillAssistsForPlayer`
|
||||
* @see `DamageResult`
|
||||
* @see `Death`
|
||||
* @see `KDAStat`
|
||||
* @see `limitHistoryToThisLife`
|
||||
* @see `Support.baseExperience`
|
||||
*/
|
||||
private def rewardThisPlayerDeath(
|
||||
victim: PlayerSource,
|
||||
lastDamage: Option[DamageResult],
|
||||
history: Iterable[InGameActivity],
|
||||
): Seq[(PlayerSource, KDAStat)] = {
|
||||
val truncatedHistory = limitHistoryToThisLife(history.toList)
|
||||
determineKiller(lastDamage, truncatedHistory) match {
|
||||
case Some((result, killer: PlayerSource)) =>
|
||||
val assists = collectKillAssistsForPlayer(victim, truncatedHistory, Some(killer))
|
||||
val fullBep = calculateExperience(killer, victim, truncatedHistory)
|
||||
val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
|
||||
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
|
||||
}.toSeq
|
||||
(victim, Death(hitSquad.map { _._1 }, truncatedHistory.last.time - truncatedHistory.head.time, fullBep)) +: hitSquad
|
||||
|
||||
case _ =>
|
||||
val assists = collectKillAssistsForPlayer(victim, truncatedHistory, None)
|
||||
val fullBep = Support.baseExperience(victim, truncatedHistory)
|
||||
val hitSquad = assists.map {
|
||||
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
|
||||
}.toSeq
|
||||
(victim, Death(hitSquad.map { _._1 }, truncatedHistory.last.time - truncatedHistory.head.time, fullBep)) +: hitSquad
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the chronology of in-game activity between an starting activity and a concluding activity for a player character.
|
||||
* The starting activity is signalled by one or two particular events.
|
||||
* The concluding activity is a condition of one of many common events.
|
||||
* All of the activities logged in between count.
|
||||
* @param history chronology of activity the game considers noteworthy
|
||||
* @return chronology of activity the game considers noteworthy, but truncated
|
||||
*/
|
||||
private def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
|
||||
val spawnIndex = history.lastIndexWhere {
|
||||
case _: SpawningActivity => true
|
||||
case _: RevivingActivity => true
|
||||
case _ => false
|
||||
}
|
||||
val endIndex = history.lastIndexWhere {
|
||||
case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
|
||||
case _ => false
|
||||
}
|
||||
if (spawnIndex == -1 || endIndex == -1 || spawnIndex > endIndex) {
|
||||
Nil
|
||||
} else {
|
||||
history.slice(spawnIndex, endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the player who is the origin/owner of the bullet that reduced health to zero.
|
||||
* @param lastDamageActivity damage result that purports the player who is the killer
|
||||
* @param history chronology of activity the game considers noteworthy;
|
||||
* referenced in the case that the suggested `DamageResult` is not suitable to determine a player
|
||||
* @return player associated
|
||||
* @see `limitHistoryToThisLife`
|
||||
*/
|
||||
private[exp] def determineKiller(
|
||||
lastDamageActivity: Option[DamageResult],
|
||||
history: List[InGameActivity]
|
||||
): Option[(DamageResult, SourceEntry)] = {
|
||||
val now = System.currentTimeMillis()
|
||||
val compareTimeMillis = 10.seconds.toMillis
|
||||
lastDamageActivity
|
||||
.collect { case dam
|
||||
if now - dam.interaction.hitTime < compareTimeMillis && dam.adversarial.nonEmpty =>
|
||||
(dam, dam.adversarial.get.attacker)
|
||||
}
|
||||
.orElse {
|
||||
limitHistoryToThisLife(history)
|
||||
.lastOption
|
||||
.collect { case dam: DamagingActivity =>
|
||||
val res = dam.data
|
||||
(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.
|
||||
* @param killer player that delivers the interaction that reduces health to zero
|
||||
* @param victim player to which the 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 `BattleRank.withExperience`
|
||||
* @see `Support.baseExperience`
|
||||
*/
|
||||
private def calculateExperience(
|
||||
killer: PlayerSource,
|
||||
victim: PlayerSource,
|
||||
history: Iterable[InGameActivity]
|
||||
): Long = {
|
||||
//base value (the kill experience before modifiers)
|
||||
val base = Support.baseExperience(victim, history)
|
||||
if (base > 1) {
|
||||
//include battle rank disparity modifier
|
||||
val battleRankDisparity = {
|
||||
import net.psforever.objects.avatar.BattleRank
|
||||
val killerLevel = BattleRank.withExperience(killer.bep).value
|
||||
val victimLevel = BattleRank.withExperience(victim.bep).value
|
||||
if (victimLevel > killerLevel || killerLevel - victimLevel < 6) {
|
||||
if (killerLevel < 7) {
|
||||
6 * victimLevel + 10
|
||||
} else if (killerLevel < 12) {
|
||||
(12 - killerLevel) * victimLevel + 10
|
||||
} else if (killerLevel < 25) {
|
||||
25 + victimLevel - killerLevel
|
||||
} else {
|
||||
25
|
||||
}
|
||||
} else {
|
||||
math.floor(-0.15f * base - killerLevel + victimLevel).toLong
|
||||
}
|
||||
}
|
||||
val baseWithDisparity: Long = base + battleRankDisparity
|
||||
val killCount: Long = victim.progress.kills.size
|
||||
if (battleRankDisparity > 0) {
|
||||
//include menace modifier
|
||||
val pureMenace = calculateMenace(victim)
|
||||
baseWithDisparity + (killCount * (1f + pureMenace.toFloat / 10f)).toLong
|
||||
} else {
|
||||
math.max(baseWithDisparity, killCount)
|
||||
}
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate chronological in-game activity within a scope of history and
|
||||
* isolate the interactions that lead to one player dying.
|
||||
* Factor in interactions that would have the dying player attempt to resist death, if only for a short while longer.
|
||||
* @param victim player to which the final interaction has reduced health to zero
|
||||
* @param history chronology of activity the game considers noteworthy
|
||||
* @param killerOpt optional player that delivers the interaction that reduces the `victim's` health to zero
|
||||
* @return summary of the interaction in terms of players, equipment activity, and experience
|
||||
* @see `armorDamageContributors`
|
||||
* @see `collectKillAssists`
|
||||
* @see `healthDamageContributors`
|
||||
* @see `Support.allocateContributors`
|
||||
* @see `Support.onlyOriginalAssistEntries`
|
||||
*/
|
||||
private def collectKillAssistsForPlayer(
|
||||
victim: PlayerSource,
|
||||
history: List[InGameActivity],
|
||||
killerOpt: Option[PlayerSource]
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
val healthAssists = collectKillAssists(
|
||||
victim,
|
||||
history,
|
||||
Support.allocateContributors(healthDamageContributors)
|
||||
)
|
||||
healthAssists.remove(0L)
|
||||
healthAssists.remove(victim.CharId)
|
||||
killerOpt.map { killer => healthAssists.remove(killer.CharId) }
|
||||
if (Support.wasEverAMax(victim, history)) {
|
||||
val armorAssists = collectKillAssists(
|
||||
victim,
|
||||
history,
|
||||
Support.allocateContributors(armorDamageContributors)
|
||||
)
|
||||
armorAssists.remove(0L)
|
||||
armorAssists.remove(victim.CharId)
|
||||
killerOpt.map { killer => armorAssists.remove(killer.CharId) }
|
||||
Support.onlyOriginalAssistEntries(healthAssists, armorAssists)
|
||||
} else {
|
||||
healthAssists.values
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze history based on a discriminating function and format the output.
|
||||
* @param victim player to which the final interaction has reduced health to zero
|
||||
* @param history chronology of activity the game considers noteworthy
|
||||
* @param func mechanism for discerning particular interactions and building a narrative around their history;
|
||||
* tallies all activity by a certain player using certain equipment
|
||||
* @return summary of the interaction in terms of players, equipment activity, and experience
|
||||
*/
|
||||
private def collectKillAssists(
|
||||
victim: SourceEntry,
|
||||
history: List[InGameActivity],
|
||||
func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
|
||||
): mutable.LongMap[ContributionStatsOutput] = {
|
||||
val assists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
|
||||
val total = assists.values.foldLeft(0f)(_ + _.total)
|
||||
val output = assists.map { case (id, kda) =>
|
||||
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.equipment }, kda.amount / total))
|
||||
}
|
||||
output.remove(victim.CharId)
|
||||
output
|
||||
}
|
||||
|
||||
/**
|
||||
* In relation to a target player's health,
|
||||
* build a secondary chronology of how the health value is affected per interaction and
|
||||
* maintain a quantitative record of that activity in relation to the other players and their equipment.
|
||||
* @param history chronology of activity the game considers noteworthy
|
||||
* @param faction empire to target
|
||||
* @param participants quantitative record of activity in relation to the other players and their equipment
|
||||
* @return chronology of how the health value is affected per interaction
|
||||
* @see `contributeWithDamagingActivity`
|
||||
* @see `contributeWithRecoveryActivity`
|
||||
* @see `RevivingActivity`
|
||||
*/
|
||||
private def healthDamageContributors(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
participants: mutable.LongMap[ContributionStats]
|
||||
): Seq[(Long, Int)] = {
|
||||
/*
|
||||
damage as it is measured in order (with heal-countered damage eliminated)<br>
|
||||
key - character identifier,
|
||||
value - current damage contribution
|
||||
*/
|
||||
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
|
||||
history.foreach {
|
||||
case d: DamagingActivity if d.health > 0 =>
|
||||
inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
|
||||
case r: RevivingActivity =>
|
||||
inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
|
||||
case h: HealingActivity =>
|
||||
inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder)
|
||||
case _ => ()
|
||||
}
|
||||
inOrder
|
||||
}
|
||||
|
||||
/**
|
||||
* In relation to a target player's armor,
|
||||
* build a secondary chronology of how the armor value is affected per interaction and
|
||||
* maintain a quantitative record of that activity in relation to the other players and their equipment.
|
||||
* @param history chronology of activity the game considers noteworthy
|
||||
* @param faction empire to target
|
||||
* @param participants quantitative record of activity in relation to the other players and their equipment
|
||||
* @return chronology of how the armor value is affected per interaction
|
||||
* @see `contributeWithDamagingActivity`
|
||||
* @see `contributeWithRecoveryActivity`
|
||||
*/
|
||||
private def armorDamageContributors(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
participants: mutable.LongMap[ContributionStats]
|
||||
): Seq[(Long, Int)] = {
|
||||
/*
|
||||
damage as it is measured in order (with heal-countered damage eliminated)<br>
|
||||
key - character identifier,
|
||||
value - current damage contribution
|
||||
*/
|
||||
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
|
||||
history.foreach {
|
||||
case d: DamagingActivity if d.amount - d.health > 0 =>
|
||||
inOrder = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, inOrder)
|
||||
case r: RepairingActivity =>
|
||||
inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
|
||||
case _ => ()
|
||||
}
|
||||
inOrder
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze damaging activity for quantitative records.
|
||||
* @param activity a particular in-game activity that negative affects a player's health
|
||||
* @param faction empire to target
|
||||
* @param amount value
|
||||
* @param participants quantitative record of activity in relation to the other players and their equipment
|
||||
* @param order chronology of how the armor value is affected per interaction
|
||||
* @return chronology of how the armor value is affected per interaction
|
||||
*/
|
||||
private def contributeWithDamagingActivity(
|
||||
activity: DamagingActivity,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
amount: Int,
|
||||
participants: mutable.LongMap[ContributionStats],
|
||||
order: Seq[(Long, Int)]
|
||||
): Seq[(Long, Int)] = {
|
||||
val data = activity.data
|
||||
val playerOpt = data.adversarial.collect { case Adversarial(p: PlayerSource, _,_) => p }
|
||||
contributeWithDamagingActivity(
|
||||
playerOpt,
|
||||
data.interaction.cause.attribution,
|
||||
faction,
|
||||
amount,
|
||||
activity.time,
|
||||
participants,
|
||||
order
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze damaging activity for quantitative records.
|
||||
* @param userOpt optional player for the quantitative record
|
||||
* @param wepid weapon for the quantitative record
|
||||
* @param faction empire to target
|
||||
* @param amount value
|
||||
* @param participants quantitative record of activity in relation to the other players and their equipment
|
||||
* @param order chronology of how the armor value is affected per interaction
|
||||
* @return chronology of how the armor value is affected per interaction
|
||||
*/
|
||||
private[exp] def contributeWithDamagingActivity(
|
||||
userOpt: Option[PlayerSource],
|
||||
wepid: Int,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
amount: Int,
|
||||
time: Long,
|
||||
participants: mutable.LongMap[ContributionStats],
|
||||
order: Seq[(Long, Int)]
|
||||
): Seq[(Long, Int)] = {
|
||||
userOpt match {
|
||||
case Some(user)
|
||||
if user.Faction != faction =>
|
||||
val whoId = user.CharId
|
||||
val percentage = amount / user.Definition.MaxHealth.toFloat
|
||||
val updatedEntry = participants.get(whoId) match {
|
||||
case Some(mod) =>
|
||||
//previous attacker, just add to entry
|
||||
val firstWeapon = mod.weapons.head
|
||||
val newEntry = DamageWith(wepid)
|
||||
val weapons = if (firstWeapon.equipment == newEntry) {
|
||||
firstWeapon.copy(
|
||||
amount = firstWeapon.amount + amount,
|
||||
shots = firstWeapon.shots + 1,
|
||||
time = time,
|
||||
contributions = firstWeapon.contributions + percentage
|
||||
) +: mod.weapons.tail
|
||||
} else {
|
||||
WeaponStats(newEntry, amount, 1, time, percentage) +: mod.weapons
|
||||
}
|
||||
mod.copy(
|
||||
amount = mod.amount + amount,
|
||||
weapons = weapons,
|
||||
total = mod.total + amount,
|
||||
shots = mod.shots + 1,
|
||||
time = time
|
||||
)
|
||||
case None =>
|
||||
//new attacker, new entry
|
||||
ContributionStats(
|
||||
user,
|
||||
Seq(WeaponStats(DamageWith(wepid), amount, 1, time, percentage)),
|
||||
amount,
|
||||
amount,
|
||||
1,
|
||||
time
|
||||
)
|
||||
}
|
||||
participants.put(whoId, updatedEntry)
|
||||
order.indexWhere({ case (id, _) => id == whoId }) match {
|
||||
case 0 =>
|
||||
//ongoing attack by same player
|
||||
val entry = order.head
|
||||
(entry._1, entry._2 + amount) +: order.tail
|
||||
case _ =>
|
||||
//different player than immediate prior attacker
|
||||
(whoId, amount) +: order
|
||||
}
|
||||
case _ =>
|
||||
//damage that does not lead to contribution
|
||||
order.headOption match {
|
||||
case Some((id, dam)) =>
|
||||
if (id == 0L) {
|
||||
(0L, dam + amount) +: order.tail //pool
|
||||
} else {
|
||||
(0L, amount) +: order //new
|
||||
}
|
||||
case None =>
|
||||
order
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze recovery activity for quantitative records.
|
||||
* @param amount value
|
||||
* @param participants quantitative record of activity in relation to the other players and their equipment
|
||||
* @param order chronology of how the armor value is affected per interaction
|
||||
* @return chronology of how the armor value is affected per interaction
|
||||
*/
|
||||
private[exp] def contributeWithRecoveryActivity(
|
||||
amount: Int,
|
||||
participants: mutable.LongMap[ContributionStats],
|
||||
order: Seq[(Long, Int)]
|
||||
): Seq[(Long, Int)] = {
|
||||
var amt = amount
|
||||
var count = 0
|
||||
var newOrder: 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
|
||||
}
|
||||
(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
|
||||
}
|
||||
}
|
||||
count += 1
|
||||
amt > 0
|
||||
}
|
||||
newOrder ++ order.drop(count)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,930 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import net.psforever.objects.GlobalDefinitions
|
||||
import net.psforever.objects.avatar.scoring.{Kill, SupportActivity}
|
||||
import net.psforever.objects.sourcing.{BuildingSource, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, VehicleSource}
|
||||
import net.psforever.objects.vital.{Contribution, HealFromTerminal, InGameActivity, RepairFromTerminal, RevivingActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleDismountActivity, VehicleMountActivity}
|
||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
||||
import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
||||
import net.psforever.util.Config
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
/**
|
||||
* Kills and assists consider the target, in an exchange of projectiles from the weapons of players towards the target.
|
||||
* Contributions consider actions of other allied players towards the player who is the source of the projectiles.
|
||||
* These actions are generally positive for the player.
|
||||
* @see `Contribution`
|
||||
* @see `ContributionStats`
|
||||
* @see `ContributionStatsOutput`
|
||||
* @see `DamagingActivity`
|
||||
* @see `GlobalDefinitions`
|
||||
* @see `HealingActivity`
|
||||
* @see `InGameActivity`
|
||||
* @see `InGameHistory`
|
||||
* @see `Kill`
|
||||
* @see `PlayerSource`
|
||||
* @see `RepairingActivity`
|
||||
* @see `SourceEntry`
|
||||
* @see `SourceUniqueness`
|
||||
* @see `VehicleSource`
|
||||
*/
|
||||
object KillContributions {
|
||||
/** the object type ids of various game elements that are recognized for "stat recovery" */
|
||||
final val RecoveryItems: Seq[Int] = {
|
||||
import net.psforever.objects.GlobalDefinitions._
|
||||
Seq(
|
||||
bank,
|
||||
nano_dispenser,
|
||||
medicalapplicator,
|
||||
order_terminal,
|
||||
order_terminala,
|
||||
order_terminalb,
|
||||
medical_terminal,
|
||||
adv_med_terminal,
|
||||
bfr_rearm_terminal,
|
||||
multivehicle_rearm_terminal,
|
||||
lodestar_repair_terminal
|
||||
).collect { _.ObjectId }
|
||||
} //TODO currently includes things that are not typical equipment but things that express contribution
|
||||
|
||||
/** cached for empty collection returns; please do not add anything to it */
|
||||
private val emptyMap: mutable.LongMap[ContributionStats] = mutable.LongMap.empty[ContributionStats]
|
||||
|
||||
/**
|
||||
* Primary landing point for calculating the rewards given for helping one player kill another player.
|
||||
* Rewards in the form of "support experience points" are given
|
||||
* to all allied players that have somehow been involved with the player who killed another player.
|
||||
* @param target player that delivers the interaction that killed another player;
|
||||
* history is purportedly composed of events that have happened to this player within a time frame
|
||||
* @param history chronology of activity the game considers noteworthy
|
||||
* @param kill the in-game event that maintains information about the other player's death;
|
||||
* originates from prior statistical management normally
|
||||
* @param bep battle experience points to be referenced for support experience points conversion
|
||||
* @param eventBus where to send the results of the experience determination(s)
|
||||
* @see `ActorRef`
|
||||
* @see `AvatarAction.UpdateKillsDeathsAssists`
|
||||
* @see `AvatarServiceMessage`
|
||||
* @see `rewardTheseSupporters`
|
||||
* @see `SupportActivity`
|
||||
*/
|
||||
private[exp] def rewardTheseSupporters(
|
||||
target: PlayerSource,
|
||||
history: Iterable[InGameActivity],
|
||||
kill: Kill,
|
||||
bep: Long,
|
||||
eventBus: ActorRef
|
||||
): Unit = {
|
||||
val victim = kill.victim
|
||||
//take the output and transform that into contribution distribution data
|
||||
rewardTheseSupporters(target, history, kill, bep)
|
||||
.foreach { case (charId, ContributionStatsOutput(player, weapons, exp)) =>
|
||||
eventBus ! AvatarServiceMessage(
|
||||
player.Name,
|
||||
AvatarAction.UpdateKillsDeathsAssists(charId, SupportActivity(victim, weapons, exp.toLong))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary innards for calculating the rewards given for helping one player kill another player.
|
||||
* @param target player that delivers the interaction that killed another player;
|
||||
* history is purportedly composed of events that have happened to this player within a time frame
|
||||
* @param history chronology of activity the game considers noteworthy
|
||||
* @param kill the in-game event that maintains information about the other player's death;
|
||||
* originates from prior statistical management normally
|
||||
* @param bep battle experience points to be referenced for support experience points conversion
|
||||
* returns list of user unique identifiers and
|
||||
* a summary of the interaction in terms of players, equipment activity, and experience
|
||||
* @see `ActorRef`
|
||||
* @see `additionalContributionSources`
|
||||
* @see `AvatarAction.UpdateKillsDeathsAssists`
|
||||
* @see `AvatarServiceMessage`
|
||||
* @see `CombinedHealthAndArmorContributionProcess`
|
||||
* @see `composeContributionOutput`
|
||||
* @see `initialScoring`
|
||||
* @see `KillAssists.calculateMenace`
|
||||
* @see `limitHistoryToThisLife`
|
||||
* @see `rewardTheseSupporters`
|
||||
* @see `SupportActivity`
|
||||
*/
|
||||
private[exp] def rewardTheseSupporters(
|
||||
target: PlayerSource,
|
||||
history: Iterable[InGameActivity],
|
||||
kill: Kill,
|
||||
bep: Long
|
||||
): Iterable[(Long, ContributionStatsOutput)] = {
|
||||
val faction = target.Faction
|
||||
/*
|
||||
divide into applicable time periods;
|
||||
these two periods represent passes over the in-game history to evaluate statistic modification events;
|
||||
the short time period should stand on its own, but should also be represented in the long time period;
|
||||
more players should be rewarded if one qualifies for the longer time period's evaluation
|
||||
*/
|
||||
val (contributions, (longHistory, shortHistory)) = {
|
||||
val killTime = kill.time.toDate.getTime
|
||||
val shortPeriod = killTime - Config.app.game.experience.shortContributionTime
|
||||
val (contrib, onlyHistory) = history.partition { _.isInstanceOf[Contribution] }
|
||||
(
|
||||
contrib
|
||||
.collect { case Contribution(unique, entries) => (unique, entries) }
|
||||
.toMap[SourceUniqueness, List[InGameActivity]],
|
||||
limitHistoryToThisLife(onlyHistory.toList, killTime).partition { _.time < shortPeriod }
|
||||
)
|
||||
}
|
||||
//events that are older than 5 minutes are enough to prove one has been alive that long
|
||||
val empty = mutable.ListBuffer[SourceUniqueness]()
|
||||
empty.addOne(target.unique)
|
||||
val otherContributionCalculations = additionalContributionSources(faction, kill, contributions)(_, _, _)
|
||||
if (longHistory.nonEmpty && KillAssists.calculateMenace(target) > 3) {
|
||||
//long and short history
|
||||
val longContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
|
||||
val shortContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Seq(longContributionProcess))
|
||||
longContributionProcess.submit(longHistory)
|
||||
shortContributionProcess.submit(shortHistory)
|
||||
val longContributionEntries = otherContributionCalculations(
|
||||
longHistory,
|
||||
initialScoring(longContributionProcess.output(), bep.toFloat),
|
||||
empty
|
||||
)
|
||||
val shortContributionEntries = otherContributionCalculations(
|
||||
shortHistory,
|
||||
initialScoring(shortContributionProcess.output(), bep.toFloat),
|
||||
empty
|
||||
)
|
||||
longContributionEntries.remove(target.CharId)
|
||||
longContributionEntries.remove(kill.victim.CharId)
|
||||
shortContributionEntries.remove(target.CharId)
|
||||
shortContributionEntries.remove(kill.victim.CharId)
|
||||
//combine
|
||||
(longContributionEntries ++ shortContributionEntries)
|
||||
.toSeq
|
||||
.distinctBy(_._2.player.unique)
|
||||
.flatMap { case (_, stats) =>
|
||||
composeContributionOutput(stats.player.CharId, shortContributionEntries, longContributionEntries, bep)
|
||||
}
|
||||
} else {
|
||||
//short history only
|
||||
val contributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
|
||||
contributionProcess.submit(shortHistory)
|
||||
val contributionEntries = otherContributionCalculations(
|
||||
shortHistory,
|
||||
initialScoring(contributionProcess.output(), bep.toFloat),
|
||||
empty
|
||||
)
|
||||
contributionEntries.remove(target.CharId)
|
||||
contributionEntries.remove(kill.victim.CharId)
|
||||
contributionEntries
|
||||
.flatMap { case (_, stats) =>
|
||||
composeContributionOutput(stats.player.CharId, contributionEntries, contributionEntries, bep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only historical activity that falls within the valid period matters.<br>
|
||||
* Unlike an expected case where the history would be bound by being spawned and being killed, respectively,
|
||||
* this imposes only the long contribution time limit on events since the latest entry;
|
||||
* and, it may stop some time after the otherwise closest activity for being spawned.
|
||||
* @param history the original history
|
||||
* @param eventTime from which time to start counting backwards
|
||||
* @return the potentially truncated history
|
||||
*/
|
||||
private def limitHistoryToThisLife(history: List[InGameActivity], eventTime: Long): List[InGameActivity] = {
|
||||
limitHistoryToThisLife(history, eventTime, eventTime - Config.app.game.experience.longContributionTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* Only historical activity that falls within the valid period matters.
|
||||
* @param history the original history
|
||||
* @param eventTime from which time to start counting backwards
|
||||
* @param startTime after which time to start counting forwards
|
||||
* @return the potentially truncated history
|
||||
*/
|
||||
private def limitHistoryToThisLife(
|
||||
history: List[InGameActivity],
|
||||
eventTime: Long,
|
||||
startTime: Long
|
||||
): List[InGameActivity] = {
|
||||
history.filter { event => event.time <= eventTime && event.time >= startTime }
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulate contribution scores that have been evaluated up to this point
|
||||
* for a fixed combination of users and different implements
|
||||
* by replacing the score using a flat predictable numerical evaluation.
|
||||
* @param existingParticipants quantitative record of activity in relation to the other players and their equipment
|
||||
* @param bep battle experience point
|
||||
* @return quantitative record of activity in relation to the other players and their equipment
|
||||
*/
|
||||
private def initialScoring(
|
||||
existingParticipants: mutable.LongMap[ContributionStats],
|
||||
bep: Float
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
//the scoring up to this point should be rate based, but is not perfectly useful for us
|
||||
existingParticipants.map { case (id, stat) =>
|
||||
val newWeaponStats = stat.weapons.map { weaponStat =>
|
||||
weaponStat.copy(contributions = 10f + weaponStat.shots.toFloat + 0.05f * bep)
|
||||
}
|
||||
existingParticipants.put(id, stat.copy(weapons = newWeaponStats))
|
||||
}
|
||||
existingParticipants
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param faction empire to target
|
||||
* @param kill the in-game event that maintains information about the other player's death;
|
||||
* originates from prior statistical management normally
|
||||
* @param contributions na
|
||||
* @param history chronology of activity the game considers noteworthy
|
||||
* @param existingParticipants quantitative record of activity in relation to the other players and their equipment
|
||||
* @param excludedTargets do not repeat analysis on entities associated with these tokens
|
||||
* @return quantitative record of activity in relation to the other players and their equipment
|
||||
* @see `contributeWithRevivalActivity`
|
||||
* @see `contributeWithTerminalActivity`
|
||||
* @see `contributeWithVehicleTransportActivity`
|
||||
* @see `contributeWithVehicleCargoTransportActivity`
|
||||
* @see `contributeWithKillWhileMountedActivity`
|
||||
*/
|
||||
private def additionalContributionSources(
|
||||
faction: PlanetSideEmpire.Value,
|
||||
kill: Kill,
|
||||
contributions: Map[SourceUniqueness, List[InGameActivity]]
|
||||
)
|
||||
(
|
||||
history: List[InGameActivity],
|
||||
existingParticipants: mutable.LongMap[ContributionStats],
|
||||
excludedTargets: mutable.ListBuffer[SourceUniqueness]
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
contributeWithRevivalActivity(history, existingParticipants)
|
||||
contributeWithTerminalActivity(history, faction, contributions, excludedTargets, existingParticipants)
|
||||
contributeWithVehicleTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants)
|
||||
contributeWithVehicleCargoTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants)
|
||||
contributeWithKillWhileMountedActivity(kill, faction, contributions, excludedTargets, existingParticipants)
|
||||
existingParticipants.remove(0)
|
||||
existingParticipants
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather and reward specific in-game equipment use activity.<br>
|
||||
* If the player who performed the killing interaction is mounted in something,
|
||||
* determine if the mount is has been effected by previous in-game interactions
|
||||
* that resulted in positive stat maintenance or development.
|
||||
* Also, reward the owner, if an owner exists, for providing the mount.
|
||||
* @param kill the in-game event that maintains information about the other player's death
|
||||
* @param faction empire to target
|
||||
* @param contributions mapping between external entities
|
||||
* the target has interacted with in the form of in-game activity
|
||||
* and history related to the time period in which the interaction ocurred
|
||||
* @param excludedTargets if a potential target is listed here already, skip processing it
|
||||
* @param out quantitative record of activity in relation to the other players and their equipment
|
||||
* @see `combineStatsInto`
|
||||
* @see `extractContributionsForMachineByTarget`
|
||||
*/
|
||||
private def contributeWithKillWhileMountedActivity(
|
||||
kill: Kill,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
contributions: Map[SourceUniqueness, List[InGameActivity]],
|
||||
excludedTargets: mutable.ListBuffer[SourceUniqueness],
|
||||
out: mutable.LongMap[ContributionStats]
|
||||
): Unit = {
|
||||
val eventTime = kill.time.toDate.getTime
|
||||
(kill
|
||||
.info
|
||||
.interaction
|
||||
.cause match {
|
||||
case p: ProjectileReason => p.projectile.mounted_in.map { case (_, src) => Some((src, p.projectile.owner)) }
|
||||
case _ => None
|
||||
})
|
||||
.collect {
|
||||
case Some((mount: VehicleSource, attacker: PlayerSource)) if !excludedTargets.contains(mount.unique) =>
|
||||
mount.owner
|
||||
.collect {
|
||||
case owner if owner == attacker.unique =>
|
||||
//owner is gunner; reward only repairs
|
||||
excludedTargets.addOne(owner)
|
||||
owner
|
||||
case owner =>
|
||||
//gunner is different from owner; reward driver and repairs
|
||||
excludedTargets.addOne(owner)
|
||||
excludedTargets.addOne(attacker.unique)
|
||||
val time = kill.time.toDate.getTime
|
||||
val weaponStat = Support.calculateSupportExperience(
|
||||
event = "mounted-kill",
|
||||
WeaponStats(DriverAssist(mount.Definition.ObjectId), 1, 1, time, 1f)
|
||||
)
|
||||
combineStatsInto(
|
||||
out,
|
||||
(
|
||||
owner.charId,
|
||||
ContributionStats(
|
||||
PlayerSource(owner, mount.Position),
|
||||
Seq(weaponStat),
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
time
|
||||
)
|
||||
)
|
||||
)
|
||||
owner
|
||||
}
|
||||
combineStatsInto(
|
||||
out,
|
||||
extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets, eventOutputType="support-repair")
|
||||
)
|
||||
case Some((mount: TurretSource, _: PlayerSource)) if !excludedTargets.contains(mount.unique) =>
|
||||
combineStatsInto(
|
||||
out,
|
||||
extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets, eventOutputType="support-repair-turret")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather and reward specific in-game equipment use activity.<br>
|
||||
* na
|
||||
* @param kill the in-game event that maintains information about the other player's death
|
||||
* @param history chronology of activity the game considers noteworthy
|
||||
* @param faction empire to target
|
||||
* @param contributions mapping between external entities
|
||||
* the target has interacted with in the form of in-game activity
|
||||
* and history related to the time period in which the interaction ocurred
|
||||
* @param excludedTargets if a potential target is listed here already, skip processing it
|
||||
* @param out quantitative record of activity in relation to the other players and their equipment
|
||||
* @see `combineStatsInto`
|
||||
* @see `extractContributionsForMachineByTarget`
|
||||
*/
|
||||
private def contributeWithVehicleTransportActivity(
|
||||
kill: Kill,
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
contributions: Map[SourceUniqueness, List[InGameActivity]],
|
||||
excludedTargets: mutable.ListBuffer[SourceUniqueness],
|
||||
out: mutable.LongMap[ContributionStats]
|
||||
): Unit = {
|
||||
/*
|
||||
collect the dismount activity of all vehicles from which this player is not the owner
|
||||
make certain all dismount activity can be paired with a mounting activity
|
||||
certain other qualifications of the prior mounting must be met before the support bonus applies
|
||||
*/
|
||||
val dismountActivity = history
|
||||
.collect {
|
||||
case out: VehicleDismountActivity
|
||||
if !out.vehicle.owner.contains(out.player.unique) && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
|
||||
}
|
||||
.collect {
|
||||
case (in: VehicleMountActivity, out: VehicleDismountActivity)
|
||||
if in.vehicle.unique == out.vehicle.unique &&
|
||||
out.vehicle.Faction == out.player.Faction &&
|
||||
(in.vehicle.Definition == GlobalDefinitions.router || {
|
||||
val inTime = in.time
|
||||
val outTime = out.time
|
||||
out.player.progress.kills.exists { death =>
|
||||
val deathTime = death.info.interaction.hitTime
|
||||
inTime < deathTime && deathTime <= outTime
|
||||
}
|
||||
} || {
|
||||
val sameZone = in.zoneNumber == out.zoneNumber
|
||||
val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
|
||||
val distanceMoved = {
|
||||
val killLocation = kill.info.adversarial
|
||||
.collect { adversarial => adversarial.attacker.Position.xy }
|
||||
.getOrElse(Vector3.Zero)
|
||||
Vector3.DistanceSquared(killLocation, out.player.Position.xy)
|
||||
}
|
||||
val timeSpent = out.time - in.time
|
||||
distanceMoved < 5625f /* 75m */ &&
|
||||
(timeSpent >= 210000L /* 3:30 */ ||
|
||||
(sameZone && (distanceTransported > 160000f /* 400m */ ||
|
||||
distanceTransported > 10000f /* 100m */ && timeSpent >= 60000L /* 1:00m */)) ||
|
||||
(!sameZone && (distanceTransported > 10000f /* 100m */ || timeSpent >= 120000L /* 2:00 */ )))
|
||||
}) =>
|
||||
out
|
||||
}
|
||||
//apply
|
||||
dismountActivity
|
||||
.groupBy { _.vehicle }
|
||||
.collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty =>
|
||||
val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
|
||||
val (equipmentUseContext, equipmentUseEvent) = mount.Definition match {
|
||||
case v @ GlobalDefinitions.router =>
|
||||
(RouterKillAssist(v.ObjectId), "router")
|
||||
case v =>
|
||||
(HotDropKillAssist(v.ObjectId, 0), "hotdrop")
|
||||
}
|
||||
val size = dismountsFromVehicle.size
|
||||
val time = dismountsFromVehicle.maxBy(_.time).time
|
||||
val weaponStat = Support.calculateSupportExperience(
|
||||
equipmentUseEvent,
|
||||
WeaponStats(equipmentUseContext, size, size, time, 1f)
|
||||
)
|
||||
combineStatsInto(
|
||||
out,
|
||||
(
|
||||
promotedOwner.CharId,
|
||||
ContributionStats(promotedOwner, Seq(weaponStat), size, size, size, time)
|
||||
)
|
||||
)
|
||||
contributions.get(mount.unique).collect {
|
||||
case list =>
|
||||
val mountHistory = dismountsFromVehicle
|
||||
.flatMap { event =>
|
||||
val eventTime = event.time
|
||||
val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
|
||||
limitHistoryToThisLife(list, eventTime, startTime)
|
||||
}
|
||||
.distinctBy(_.time)
|
||||
combineStatsInto(
|
||||
out,
|
||||
extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets, eventOutputType="support-repair")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather and reward specific in-game equipment use activity.<br>
|
||||
* na
|
||||
* @param kill the in-game event that maintains information about the other player's death
|
||||
* @param faction empire to target
|
||||
* @param contributions mapping between external entities
|
||||
* the target has interacted with in the form of in-game activity
|
||||
* and history related to the time period in which the interaction ocurred
|
||||
* @param excludedTargets if a potential target is listed here already, skip processing it
|
||||
* @param out quantitative record of activity in relation to the other players and their equipment
|
||||
* @see `combineStatsInto`
|
||||
* @see `extractContributionsForMachineByTarget`
|
||||
*/
|
||||
private def contributeWithVehicleCargoTransportActivity(
|
||||
kill: Kill,
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
contributions: Map[SourceUniqueness, List[InGameActivity]],
|
||||
excludedTargets: mutable.ListBuffer[SourceUniqueness],
|
||||
out: mutable.LongMap[ContributionStats]
|
||||
): Unit = {
|
||||
/*
|
||||
collect the dismount activity of all vehicles from which this player is not the owner
|
||||
make certain all dismount activity can be paired with a mounting activity
|
||||
certain other qualifications of the prior mounting must be met before the support bonus applies
|
||||
*/
|
||||
val dismountActivity = history
|
||||
.collect {
|
||||
case out: VehicleCargoDismountActivity
|
||||
if out.vehicle.owner.nonEmpty && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
|
||||
}
|
||||
.collect {
|
||||
case (in: VehicleCargoMountActivity, out: VehicleCargoDismountActivity)
|
||||
if in.vehicle.unique == out.vehicle.unique &&
|
||||
out.vehicle.Faction == out.cargo.Faction &&
|
||||
(in.vehicle.Definition == GlobalDefinitions.router || {
|
||||
val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
|
||||
val distanceMoved = {
|
||||
val killLocation = kill.info.adversarial
|
||||
.collect { adversarial => adversarial.attacker.Position.xy }
|
||||
.getOrElse(Vector3.Zero)
|
||||
Vector3.DistanceSquared(killLocation, out.cargo.Position.xy)
|
||||
}
|
||||
val timeSpent = out.time - in.time
|
||||
distanceMoved < 5625f /* 75m */ &&
|
||||
(timeSpent >= 210000 /* 3:30 */ || distanceTransported > 360000f /* 600m */)
|
||||
}) =>
|
||||
out
|
||||
}
|
||||
//apply
|
||||
dismountActivity
|
||||
.groupBy { _.cargo }
|
||||
.collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty =>
|
||||
val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
|
||||
val mountId = mount.Definition.ObjectId
|
||||
dismountsFromVehicle
|
||||
.groupBy(_.vehicle)
|
||||
.map { case (vehicle, events) =>
|
||||
val size = events.size
|
||||
val time = events.maxBy(_.time).time
|
||||
val weaponStat = Support.calculateSupportExperience(
|
||||
event = "hotdrop",
|
||||
WeaponStats(HotDropKillAssist(vehicle.Definition.ObjectId, mountId), size, size, time, 1f)
|
||||
)
|
||||
(vehicle, vehicle.owner, Seq(weaponStat))
|
||||
}
|
||||
.collect { case (vehicle, Some(owner), statContext) =>
|
||||
combineStatsInto(
|
||||
out,
|
||||
(
|
||||
owner.charId,
|
||||
ContributionStats(promotedOwner, statContext, 1, 1, 1, statContext.head.time)
|
||||
)
|
||||
)
|
||||
contributions.get(mount.unique).collect {
|
||||
case list =>
|
||||
val mountHistory = dismountsFromVehicle
|
||||
.flatMap { event =>
|
||||
val eventTime = event.time
|
||||
val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
|
||||
limitHistoryToThisLife(list, eventTime, startTime)
|
||||
}
|
||||
.distinctBy(_.time)
|
||||
combineStatsInto(
|
||||
out,
|
||||
extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets, eventOutputType="support-repair")
|
||||
)
|
||||
}
|
||||
contributions.get(vehicle.unique).collect {
|
||||
case list =>
|
||||
val carrierHistory = dismountsFromVehicle
|
||||
.flatMap { event =>
|
||||
val eventTime = event.time
|
||||
val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
|
||||
limitHistoryToThisLife(list, eventTime, startTime)
|
||||
}
|
||||
.distinctBy(_.time)
|
||||
combineStatsInto(
|
||||
out,
|
||||
extractContributionsForMachineByTarget(vehicle, faction, carrierHistory, contributions, excludedTargets, eventOutputType="support-repair")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather and reward specific in-game equipment use activity.<br>
|
||||
* na
|
||||
* @param faction empire to target
|
||||
* @param contributions mapping between external entities
|
||||
* the target has interacted with in the form of in-game activity
|
||||
* and history related to the time period in which the interaction ocurred
|
||||
* @param excludedTargets if a potential target is listed here already, skip processing it
|
||||
* @param out quantitative record of activity in relation to the other players and their equipment
|
||||
* @see `AmsResupplyKillAssist`
|
||||
* @see `BuildingSource`
|
||||
* @see `combineStatsInto`
|
||||
* @see `contributeWithTerminalActivity`
|
||||
* @see `extractContributionsForMachineByTarget`
|
||||
* @see `HackKillAssist`
|
||||
* @see `HealFromTerminal`
|
||||
* @see `LodestarRearmKillAssist`
|
||||
* @see `RepairFromTerminal`
|
||||
* @see `RepairKillAssist`
|
||||
* @see `TerminalUsedActivity`
|
||||
*/
|
||||
private def contributeWithTerminalActivity(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value,
|
||||
contributions: Map[SourceUniqueness, List[InGameActivity]],
|
||||
excludedTargets: mutable.ListBuffer[SourceUniqueness],
|
||||
out: mutable.LongMap[ContributionStats]
|
||||
): Unit = {
|
||||
history
|
||||
.collect {
|
||||
case h: HealFromTerminal => (h.term, h)
|
||||
case r: RepairFromTerminal => (r.term, r)
|
||||
case t: TerminalUsedActivity => (t.terminal, t)
|
||||
}
|
||||
.groupBy(_._1.unique)
|
||||
.map {
|
||||
case (_, events1) =>
|
||||
val (termThings1, _) = events1.unzip
|
||||
val hackContext = HackKillAssist(GlobalDefinitions.remote_electronics_kit.ObjectId, termThings1.head.Definition.ObjectId)
|
||||
if (termThings1.exists(t => t.Faction != faction && t.hacked.nonEmpty)) {
|
||||
/*
|
||||
if the terminal has been hacked,
|
||||
and the original terminal does not align with our own faction,
|
||||
then the support must be reported as a hack;
|
||||
if we are the same faction as the terminal, then the hacked condition is irrelevant
|
||||
*/
|
||||
events1
|
||||
.collect { case out @ (t, _) if t.hacked.nonEmpty => out }
|
||||
.groupBy { case (t, _) => t.hacked.get.player.unique }
|
||||
.foreach { case (_, events2) =>
|
||||
val (termThings2, events3) = events2.unzip
|
||||
val hacker = termThings2.head.hacked.get.player
|
||||
val size = events3.size
|
||||
val time = events3.maxBy(_.time).time
|
||||
val weaponStats = Support.calculateSupportExperience(
|
||||
event = "hack",
|
||||
WeaponStats(hackContext, size, size, time, 1f)
|
||||
)
|
||||
combineStatsInto(
|
||||
out,
|
||||
(
|
||||
hacker.CharId,
|
||||
ContributionStats(
|
||||
hacker,
|
||||
Seq(weaponStats),
|
||||
size,
|
||||
size,
|
||||
size,
|
||||
time
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else if (termThings1.exists(_.Faction == faction)) {
|
||||
//faction-aligned terminal
|
||||
val (_, events2) = events1.unzip
|
||||
val eventTime = events2.maxBy(_.time).time
|
||||
val startTime = events2.minBy(_.time).time - Config.app.game.experience.longContributionTime
|
||||
val termThingsHead = termThings1.head
|
||||
val (equipmentUseContext, equipmentUseEvent, installationEvent, target) = termThingsHead.installation match {
|
||||
case v: VehicleSource =>
|
||||
termThingsHead.Definition match {
|
||||
case GlobalDefinitions.order_terminala =>
|
||||
(AmsResupplyKillAssist(GlobalDefinitions.order_terminala.ObjectId), "ams-resupply", "support-repair", Some(v))
|
||||
case GlobalDefinitions.order_terminalb =>
|
||||
(AmsResupplyKillAssist(GlobalDefinitions.order_terminalb.ObjectId), "ams-resupply", "support-repair", Some(v))
|
||||
case GlobalDefinitions.lodestar_repair_terminal =>
|
||||
(RepairKillAssist(GlobalDefinitions.lodestar_repair_terminal.ObjectId, v.Definition.ObjectId), "lodestar-repair", "support-repair", Some(v))
|
||||
case GlobalDefinitions.bfr_rearm_terminal =>
|
||||
(LodestarRearmKillAssist(GlobalDefinitions.bfr_rearm_terminal.ObjectId), "lodestar-rearm", "support-repair", Some(v))
|
||||
case GlobalDefinitions.multivehicle_rearm_terminal =>
|
||||
(LodestarRearmKillAssist(GlobalDefinitions.multivehicle_rearm_terminal.ObjectId), "lodestar-rearm", "support-repair", Some(v))
|
||||
case _ =>
|
||||
(NoUse(), "", "", None)
|
||||
}
|
||||
case _: BuildingSource =>
|
||||
(NoUse(), "", "support-repair-terminal", Some(termThingsHead))
|
||||
case _ =>
|
||||
(NoUse(), "", "", None)
|
||||
}
|
||||
target.map { src =>
|
||||
combineStatsInto(
|
||||
out,
|
||||
extractContributionsForMachineByTarget(src, faction, eventTime, startTime, contributions, excludedTargets, installationEvent)
|
||||
)
|
||||
}
|
||||
events1
|
||||
.map { case (a, b) => (a.installation, b) }
|
||||
.collect { case (installation: VehicleSource, evt) if installation.owner.nonEmpty => (installation, evt) }
|
||||
.groupBy(_._1.owner.get)
|
||||
.collect { case (owner, list) =>
|
||||
val (installations, events2) = list.unzip
|
||||
val size = events2.size
|
||||
val time = events2.maxBy(_.time).time
|
||||
val weaponStats = Support.calculateSupportExperience(
|
||||
equipmentUseEvent,
|
||||
WeaponStats(equipmentUseContext, size, size, time, 1f)
|
||||
)
|
||||
combineStatsInto(
|
||||
out,
|
||||
(
|
||||
owner.charId,
|
||||
ContributionStats(
|
||||
PlayerSource(owner, installations.head.Position),
|
||||
Seq(weaponStats),
|
||||
size,
|
||||
size,
|
||||
size,
|
||||
time
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather and reward specific in-game equipment use activity.<br>
|
||||
* na
|
||||
* @param history chronology of activity the game considers noteworthy
|
||||
* @param out quantitative record of activity in relation to the other players and their equipment
|
||||
* @see `combineStatsInto`
|
||||
* @see `ReviveKillAssist`
|
||||
*/
|
||||
private def contributeWithRevivalActivity(
|
||||
history: List[InGameActivity],
|
||||
out: mutable.LongMap[ContributionStats]
|
||||
): Unit = {
|
||||
history
|
||||
.collect { case rev: RevivingActivity => rev }
|
||||
.groupBy(_.user.CharId)
|
||||
.map { case (id, revivesByThisPlayer) =>
|
||||
val user = revivesByThisPlayer.head.user
|
||||
revivesByThisPlayer
|
||||
.groupBy(_.equipment)
|
||||
.map { case (definition, events) =>
|
||||
val eventSize = events.size
|
||||
val objectId = definition.ObjectId
|
||||
val time = events.maxBy(_.time).time
|
||||
combineStatsInto(
|
||||
out,
|
||||
(
|
||||
id,
|
||||
ContributionStats(
|
||||
user,
|
||||
Seq({
|
||||
Support.calculateSupportExperience(
|
||||
event = "revival",
|
||||
WeaponStats(ReviveKillAssist(objectId), 1, eventSize, time, 1f)
|
||||
)
|
||||
}),
|
||||
eventSize,
|
||||
eventSize,
|
||||
eventSize,
|
||||
time
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* Mainly produces repair events.
|
||||
* @param target entity external to the subject of the kill
|
||||
* @param faction empire to target
|
||||
* @param time na
|
||||
* @param contributions mapping between external entities
|
||||
* the target has interacted with in the form of in-game activity
|
||||
* and history related to the time period in which the interaction ocurred
|
||||
* @param excludedTargets if a potential target is listed here already, skip processing it
|
||||
* @return quantitative record of activity in relation to the other players and their equipment
|
||||
*/
|
||||
private def extractContributionsForMachineByTarget(
|
||||
target: SourceEntry,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
time: Long,
|
||||
contributions: Map[SourceUniqueness, List[InGameActivity]],
|
||||
excludedTargets: mutable.ListBuffer[SourceUniqueness],
|
||||
eventOutputType: String
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
val start: Long = time - Config.app.game.experience.longContributionTime
|
||||
extractContributionsForMachineByTarget(target, faction, time, start, contributions, excludedTargets, eventOutputType)
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* Mainly produces repair events.
|
||||
* @param target entity external to the subject of the kill
|
||||
* @param faction empire to target
|
||||
* @param eventTime na
|
||||
* @param startTime na
|
||||
* @param contributions mapping between external entities
|
||||
* the target has interacted with in the form of in-game activity
|
||||
* and history related to the time period in which the interaction ocurred
|
||||
* @param excludedTargets if a potential target is listed here already, skip processing it
|
||||
* @return quantitative record of activity in relation to the other players and their equipment
|
||||
* @see `limitHistoryToThisLife`
|
||||
*/
|
||||
private def extractContributionsForMachineByTarget(
|
||||
target: SourceEntry,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
eventTime: Long,
|
||||
startTime: Long,
|
||||
contributions: Map[SourceUniqueness, List[InGameActivity]],
|
||||
excludedTargets: mutable.ListBuffer[SourceUniqueness],
|
||||
eventOutputType: String
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
val unique = target.unique
|
||||
val history = limitHistoryToThisLife(contributions.getOrElse(unique, List()), eventTime, startTime)
|
||||
extractContributionsForMachineByTarget(target, faction, history, contributions, excludedTargets, eventOutputType)
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* Mainly produces repair events.
|
||||
* @param target entity external to the subject of the kill
|
||||
* @param faction empire to target
|
||||
* @param history na
|
||||
* @param contributions mapping between external entities
|
||||
* the target has interacted with in the form of in-game activity
|
||||
* and history related to the time period in which the interaction ocurred
|
||||
* @param excludedTargets if a potential target is listed here already, skip processing it
|
||||
* @return quantitative record of activity in relation to the other players and their equipment
|
||||
* @see `cullContributorImplements`
|
||||
* @see `emptyMap`
|
||||
* @see `MachineRecoveryExperienceContributionProcess`
|
||||
*/
|
||||
private def extractContributionsForMachineByTarget(
|
||||
target: SourceEntry,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
history: List[InGameActivity],
|
||||
contributions: Map[SourceUniqueness, List[InGameActivity]],
|
||||
excludedTargets: mutable.ListBuffer[SourceUniqueness],
|
||||
eventOutputType: String
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
val unique = target.unique
|
||||
if (!excludedTargets.contains(unique) && history.nonEmpty) {
|
||||
excludedTargets.addOne(unique)
|
||||
val process = new MachineRecoveryExperienceContributionProcess(faction, contributions, eventOutputType, excludedTargets)
|
||||
process.submit(history)
|
||||
cullContributorImplements(process.output())
|
||||
} else {
|
||||
emptyMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param main quantitative record of activity in relation to the other players and their equipment
|
||||
* @param transferFrom quantitative record of activity in relation to the other players and their equipment
|
||||
* @return quantitative record of activity in relation to the other players and their equipment
|
||||
* @see `combineStatsInto`
|
||||
*/
|
||||
private def combineStatsInto(
|
||||
main: mutable.LongMap[ContributionStats],
|
||||
transferFrom: mutable.LongMap[ContributionStats]
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
transferFrom.foreach { (entry: (Long, ContributionStats)) => combineStatsInto(main, entry) }
|
||||
main
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param main quantitative record of activity in relation to the other players and their equipment
|
||||
* @param entry two value tuple representing:
|
||||
* a player's unique identifier,
|
||||
* and a quantitative record of activity in relation to the other players and their equipment
|
||||
* @see `Support.combineWeaponStats`
|
||||
*/
|
||||
private def combineStatsInto(main: mutable.LongMap[ContributionStats], entry: (Long, ContributionStats)): Unit = {
|
||||
val (id, sampleStats) = entry
|
||||
main.get(id) match {
|
||||
case Some(foundStats) =>
|
||||
main.put(id, foundStats.copy(weapons = Support.combineWeaponStats(foundStats.weapons, sampleStats.weapons)))
|
||||
case None =>
|
||||
main.put(id, sampleStats)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter quantitative records based on the presence of specific equipment used for statistic recovery.
|
||||
* @param input quantitative record of activity in relation to the other players and their equipment
|
||||
* @return quantitative record of activity in relation to the other players and their equipment
|
||||
* @see `RecoveryItems`
|
||||
*/
|
||||
private[exp] def cullContributorImplements(
|
||||
input: mutable.LongMap[ContributionStats]
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
input.collect { case (id, entry) =>
|
||||
(id, entry.copy(weapons = entry.weapons.filter { stats => RecoveryItems.contains(stats.equipment.equipment) }))
|
||||
}.filter { case (_, entry) =>
|
||||
entry.weapons.nonEmpty
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param charId the unique identifier being targeted
|
||||
* @param shortPeriod quantitative record of activity in relation to the other players and their equipment
|
||||
* @param longPeriod quantitative record of activity in relation to the other players and their equipment
|
||||
* @param max maximum value for the third output value
|
||||
* @return two value tuple representing:
|
||||
* a player's unique identifier,
|
||||
* and a summary of the interaction in terms of players, equipment activity, and experience
|
||||
* @see `composeContributionOutput`
|
||||
*/
|
||||
private def composeContributionOutput(
|
||||
charId: Long,
|
||||
shortPeriod: mutable.LongMap[ContributionStats],
|
||||
longPeriod: mutable.LongMap[ContributionStats],
|
||||
max: Long
|
||||
): Option[(Long, ContributionStatsOutput)] = {
|
||||
composeContributionOutput(charId, longPeriod, modifier=0.8f, max)
|
||||
.orElse { composeContributionOutput(charId, shortPeriod, modifier=1f, max) }
|
||||
.collect {
|
||||
case (player, weaponIds, experience) =>
|
||||
(charId, ContributionStatsOutput(player, weaponIds, experience))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param charId the unique identifier being targeted
|
||||
* @param stats quantitative record of activity in relation to the other players and their equipment
|
||||
* @param modifier modifier value for the potential third output value
|
||||
* @param max maximum value for the third output value
|
||||
* @return three value tuple representing:
|
||||
* player,
|
||||
* the context in which certain equipment is being used,
|
||||
* and a final value for the awarded support experience points
|
||||
*/
|
||||
private def composeContributionOutput(
|
||||
charId: Long,
|
||||
stats: mutable.LongMap[ContributionStats],
|
||||
modifier: Float,
|
||||
max: Long
|
||||
): Option[(PlayerSource, Seq[EquipmentUseContextWrapper], Float)] = {
|
||||
stats
|
||||
.get(charId)
|
||||
.collect {
|
||||
case entry =>
|
||||
val (weapons, contributions) = entry.weapons.map { entry => (entry.equipment, entry.contributions) }.unzip
|
||||
(
|
||||
entry.player,
|
||||
weapons.distinct,
|
||||
modifier * math.floor(math.min(contributions.foldLeft(0f)(_ + _), max.toFloat)).toFloat
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp
|
||||
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.vital.InGameActivity
|
||||
|
||||
object KillDeathAssists {
|
||||
private[exp] def calculateExperience(
|
||||
killer: PlayerSource,
|
||||
victim: PlayerSource,
|
||||
history: Iterable[InGameActivity]
|
||||
): Long = {
|
||||
//base value (the kill experience before modifiers)
|
||||
val base = ExperienceCalculator.calculateExperience(victim, history)
|
||||
if (base > 1) {
|
||||
//battle rank disparity modifiers
|
||||
val battleRankDisparity = {
|
||||
import net.psforever.objects.avatar.BattleRank
|
||||
val killerLevel = BattleRank.withExperience(killer.bep).value
|
||||
val victimLevel = BattleRank.withExperience(victim.bep).value
|
||||
if (victimLevel > killerLevel || killerLevel - victimLevel < 6) {
|
||||
if (killerLevel < 7) {
|
||||
6 * victimLevel + 10
|
||||
} else if (killerLevel < 12) {
|
||||
(12 - killerLevel) * victimLevel + 10
|
||||
} else if (killerLevel < 25) {
|
||||
25 + victimLevel - killerLevel
|
||||
} else {
|
||||
25
|
||||
}
|
||||
} else {
|
||||
math.floor(-0.15f * base - killerLevel + victimLevel).toLong
|
||||
}
|
||||
}
|
||||
math.max(1, base + battleRankDisparity)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,24 +3,39 @@ package net.psforever.objects.zones.exp
|
|||
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
|
||||
sealed trait ItemUseStats {
|
||||
def equipment: EquipmentUseContextWrapper
|
||||
def shots: Int
|
||||
def time: Long
|
||||
def contributions: Float
|
||||
}
|
||||
|
||||
private case class WeaponStats(
|
||||
weapon_id: Int,
|
||||
equipment: EquipmentUseContextWrapper,
|
||||
amount: Int,
|
||||
shots: Int,
|
||||
time: Long
|
||||
)
|
||||
time: Long,
|
||||
contributions: Float
|
||||
) extends ItemUseStats
|
||||
|
||||
private case class ContributionStats(
|
||||
player: PlayerSource,
|
||||
weapons: Seq[WeaponStats],
|
||||
amount: Int,
|
||||
totalDamage: Int,
|
||||
shots: Int,
|
||||
time: Long
|
||||
)
|
||||
private case class EquipmentStats(
|
||||
equipment: EquipmentUseContextWrapper,
|
||||
shots: Int,
|
||||
time: Long,
|
||||
contributions: Float
|
||||
) extends ItemUseStats
|
||||
|
||||
private[exp] case class ContributionStats(
|
||||
player: PlayerSource,
|
||||
weapons: Seq[WeaponStats],
|
||||
amount: Int,
|
||||
total: Int,
|
||||
shots: Int,
|
||||
time: Long
|
||||
)
|
||||
|
||||
sealed case class ContributionStatsOutput(
|
||||
player: PlayerSource,
|
||||
implements: Seq[Int],
|
||||
implements: Seq[EquipmentUseContextWrapper],
|
||||
percentage: Float
|
||||
)
|
||||
|
|
|
|||
239
src/main/scala/net/psforever/objects/zones/exp/Support.scala
Normal file
239
src/main/scala/net/psforever/objects/zones/exp/Support.scala
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp
|
||||
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.vital.{InGameActivity, ReconstructionActivity, RepairFromExoSuitChange, SpawningActivity}
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
|
||||
import net.psforever.util.Config
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
/**
|
||||
* Functions to assist experience calculation and history manipulation and analysis.
|
||||
*/
|
||||
object Support {
|
||||
private val sep = Config.app.game.experience.sep
|
||||
|
||||
/**
|
||||
* Calculate a base experience value to consider additional reasons for points.
|
||||
* @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 {
|
||||
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
|
||||
} else {
|
||||
1L
|
||||
}
|
||||
if (base > 1) {
|
||||
//black ops modifier
|
||||
base// * Config.app.game.experience.bep.base.bopsMultiplier
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine two quantitative records into one, maintaining only the original entries.
|
||||
* @param first one quantitative record
|
||||
* @param second another quantitative record
|
||||
* @param combiner mechanism for determining how to combine quantitative records;
|
||||
* defaults to an additive combiner with a small multiplier value
|
||||
* @return the combined quantitative records
|
||||
* @see `defaultAdditiveOutputCombiner`
|
||||
* @see `onlyOriginalAssistEntriesIterable`
|
||||
*/
|
||||
private[exp] def onlyOriginalAssistEntries(
|
||||
first: mutable.LongMap[ContributionStatsOutput],
|
||||
second: mutable.LongMap[ContributionStatsOutput],
|
||||
combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
|
||||
defaultAdditiveOutputCombiner(multiplier = 0.05f)
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
onlyOriginalAssistEntriesIterable(first.values, second.values, combiner)
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine two quantitative records into one, maintaining only the original entries.
|
||||
* @param first one quantitative record
|
||||
* @param second another quantitative record
|
||||
* @param combiner mechanism for determining how to combine quantitative records;
|
||||
* defaults to an additive combiner with a small multiplier value
|
||||
* @return the combined quantitative records
|
||||
* @see `defaultAdditiveOutputCombiner`
|
||||
*/
|
||||
private[exp] def onlyOriginalAssistEntriesIterable(
|
||||
first: Iterable[ContributionStatsOutput],
|
||||
second: Iterable[ContributionStatsOutput],
|
||||
combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
|
||||
defaultAdditiveOutputCombiner(multiplier = 0.05f)
|
||||
): Iterable[ContributionStatsOutput] = {
|
||||
if (second.isEmpty) {
|
||||
first
|
||||
} else if (first.isEmpty) {
|
||||
second
|
||||
} else {
|
||||
//overlap discriminated by percentage
|
||||
val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
|
||||
for {
|
||||
h @ ContributionStatsOutput(hid, _, _) <- first
|
||||
a @ ContributionStatsOutput(aid, _, _) <- second
|
||||
out = combiner(h, a)
|
||||
id = out.player.CharId
|
||||
if hid == aid && shared.put(id, out).isEmpty
|
||||
} yield ()
|
||||
val sharedKeys = shared.keys
|
||||
(first ++ second).filterNot { case ContributionStatsOutput(id, _, _) => sharedKeys.exists(_ == id.CharId) } ++ shared.values
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine two quantitative records into one, maintaining only the original entries.
|
||||
* @param multiplier adjust the combined
|
||||
* @param first one quantitative record
|
||||
* @param second another quantitative record
|
||||
* @return the combined quantitative records
|
||||
*/
|
||||
private def defaultAdditiveOutputCombiner(
|
||||
multiplier: Float
|
||||
)
|
||||
(
|
||||
first: ContributionStatsOutput,
|
||||
second: ContributionStatsOutput
|
||||
): ContributionStatsOutput = {
|
||||
if (first.percentage < second.percentage)
|
||||
second.copy(implements = (second.implements ++ first.implements).distinct, percentage = first.percentage + second.implements.size * multiplier)
|
||||
else
|
||||
first.copy(implements = (first.implements ++ second.implements).distinct, percentage = second.percentage + second.implements.size * multiplier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Take two sequences of equipment statistics
|
||||
* and combine both lists where overlap of the same equipment use is added together per field.
|
||||
* If one sequence comtains more elements of the same type of equipment use,
|
||||
* the additional entries may become lost.
|
||||
* @param first statistics in relation to equipment
|
||||
* @param second statistics in relation to equipment
|
||||
* @return statistics in relation to equipment
|
||||
*/
|
||||
private[exp] def combineWeaponStats(
|
||||
first: Seq[WeaponStats],
|
||||
second: Seq[WeaponStats]
|
||||
): Seq[WeaponStats] = {
|
||||
val (firstInSecond, firstAlone) = first.partition(firstStat => second.exists(_.equipment == firstStat.equipment))
|
||||
val (secondInFirst, secondAlone) = second.partition(secondStat => firstInSecond.exists(_.equipment == secondStat.equipment))
|
||||
val combined = firstInSecond.flatMap { firstStat =>
|
||||
secondInFirst
|
||||
.filter(_.equipment == firstStat.equipment)
|
||||
.map { secondStat =>
|
||||
firstStat.copy(
|
||||
shots = firstStat.shots + secondStat.shots,
|
||||
amount = firstStat.amount + secondStat.amount,
|
||||
contributions = firstStat.contributions + secondStat.contributions,
|
||||
time = math.max(firstStat.time, secondStat.time)
|
||||
)
|
||||
}
|
||||
}
|
||||
firstAlone ++ secondAlone ++ combined
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a function against history, targeting a certain faction.
|
||||
* @param tallyFunc the history analysis function
|
||||
* @param history chronology of activity the game considers noteworthy
|
||||
* @param faction empire to target
|
||||
* @return quantitative record of activity in relation to the other players and their equipment
|
||||
*/
|
||||
private[exp] def allocateContributors(
|
||||
tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any
|
||||
)
|
||||
(
|
||||
history: List[InGameActivity],
|
||||
faction: PlanetSideEmpire.Value
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
/*
|
||||
players who have contributed to this death, and how much they have contributed<br>
|
||||
key - character identifier,
|
||||
value - (player, damage, total damage, number of shots)
|
||||
*/
|
||||
val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
|
||||
tallyFunc(history, faction, participants)
|
||||
participants
|
||||
}
|
||||
|
||||
/**
|
||||
* You better not fail this purity test.
|
||||
* @param player player being tested
|
||||
* @param history chronology of activity the game considers noteworthy;
|
||||
* allegedly associated with this player
|
||||
* @return `true`, if the player has ever committed a great shame;
|
||||
* `false`, otherwise ... and it better be
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a weapon statistics entry and calculate the support experience value resulting from this support event.
|
||||
* The complete formula is:<br><br>
|
||||
* `base + shots-multplier * ln(shots^exp + 2) + amount-multiplier * amount`<br><br>
|
||||
* ... where the middle field can be truncated into:<br><br>
|
||||
* `shots-multplier * shots`<br><br>
|
||||
* ... without the natural logarithm exponent defined.
|
||||
* Limits can be applied to the number of shots and/or to the amount,
|
||||
* which will either zero the calculations or cap the results.
|
||||
* @param event identification for the event calculation parameters
|
||||
* @param weaponStat base weapon stat entry to be modified
|
||||
* @param canNotFindEventDefaultValue custom default value
|
||||
* @return weapon stat entry with a modified for the experience
|
||||
*/
|
||||
private[exp] def calculateSupportExperience(
|
||||
event: String,
|
||||
weaponStat: WeaponStats,
|
||||
canNotFindEventDefaultValue: Option[Float] = None
|
||||
): WeaponStats = {
|
||||
val rewards: Float = sep.events
|
||||
.find(evt => event.equals(evt.name))
|
||||
.map { event =>
|
||||
val shots = weaponStat.shots
|
||||
val shotsMultiplier = event.shotsMultiplier
|
||||
if (shotsMultiplier > 0f && shots < event.shotsCutoff) {
|
||||
val modifiedShotsReward: Float = {
|
||||
val partialShots = math.min(event.shotsLimit, shots).toFloat
|
||||
shotsMultiplier * (if (event.shotsNatLog > 0f) {
|
||||
math.log(math.pow(partialShots, event.shotsNatLog) + 2d).toFloat
|
||||
} else {
|
||||
partialShots
|
||||
})
|
||||
}
|
||||
val modifiedAmountReward: Float = event.amountMultiplier * weaponStat.amount.toFloat
|
||||
event.base + modifiedShotsReward + modifiedAmountReward
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
.getOrElse(
|
||||
canNotFindEventDefaultValue.getOrElse(sep.canNotFindEventDefaultValue.toFloat)
|
||||
)
|
||||
weaponStat.copy(contributions = rewards)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp
|
||||
|
||||
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||
import akka.actor.typed.{Behavior, SupervisorStrategy}
|
||||
import net.psforever.objects.PlanetSideGameObject
|
||||
import net.psforever.objects.avatar.scoring.Kill
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
|
||||
import net.psforever.objects.zones.Zone
|
||||
|
||||
object SupportExperienceCalculator {
|
||||
def apply(zone: Zone): Behavior[Command] =
|
||||
Behaviors.supervise[Command] {
|
||||
Behaviors.setup(context => new SupportExperienceCalculator(context, zone))
|
||||
}.onFailure[Exception](SupervisorStrategy.restart)
|
||||
|
||||
sealed trait Command
|
||||
|
||||
final case class RewardOurSupporters(target: SourceEntry, history: Iterable[InGameActivity], kill: Kill, bep: Long) extends Command
|
||||
|
||||
object RewardOurSupporters {
|
||||
def apply(obj: PlanetSideGameObject with FactionAffinity with InGameHistory, kill: Kill): RewardOurSupporters = {
|
||||
RewardOurSupporters(SourceEntry(obj), obj.History, kill, kill.experienceEarned)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SupportExperienceCalculator(context: ActorContext[SupportExperienceCalculator.Command], zone: Zone)
|
||||
extends AbstractBehavior[SupportExperienceCalculator.Command](context) {
|
||||
|
||||
import SupportExperienceCalculator._
|
||||
|
||||
def onMessage(msg: Command): Behavior[Command] = {
|
||||
msg match {
|
||||
case RewardOurSupporters(target: PlayerSource, history, kill, bep) =>
|
||||
KillContributions.rewardTheseSupporters(target, history, kill, bep, zone.AvatarEvents)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
217
src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
Normal file
217
src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
// Copyright (c) 2022 PSForever
|
||||
package net.psforever.objects.zones.exp
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import net.psforever.objects.avatar.scoring.EquipmentStat
|
||||
import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
|
||||
import net.psforever.objects.sourcing.VehicleSource
|
||||
import net.psforever.persistence
|
||||
import net.psforever.types.Vector3
|
||||
import net.psforever.util.Database.ctx
|
||||
import net.psforever.util.Database.ctx._
|
||||
|
||||
object ToDatabase {
|
||||
/**
|
||||
* Insert an entry into the database's `killactivity` table.
|
||||
* One player just died and some other player is at fault.
|
||||
*/
|
||||
def reportKillBy(
|
||||
killerId: Long,
|
||||
victimId: Long,
|
||||
victimExoSuitId: Int,
|
||||
victimMounted: Int,
|
||||
weaponId: Int,
|
||||
zoneId: Int,
|
||||
position: Vector3,
|
||||
exp: Long
|
||||
): Unit = {
|
||||
ctx.run(query[persistence.Killactivity]
|
||||
.insert(
|
||||
_.victimId -> lift(victimId),
|
||||
_.killerId -> lift(killerId),
|
||||
_.victimExosuit -> lift(victimExoSuitId),
|
||||
_.victimMounted -> lift(victimMounted),
|
||||
_.weaponId -> lift(weaponId),
|
||||
_.zoneId -> lift(zoneId),
|
||||
_.px -> lift((position.x * 1000).toInt),
|
||||
_.py -> lift((position.y * 1000).toInt),
|
||||
_.pz -> lift((position.z * 1000).toInt),
|
||||
_.exp -> lift(exp)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an entry into the database's `assistactivity` table.
|
||||
* One player just died and some other player tried to take credit.
|
||||
* (They are actually an accomplice.)
|
||||
*/
|
||||
def reportKillAssistBy(
|
||||
avatarId: Long,
|
||||
victimId: Long,
|
||||
weaponId: Int,
|
||||
zoneId: Int,
|
||||
position: Vector3,
|
||||
exp: Long
|
||||
): Unit = {
|
||||
ctx.run(query[persistence.Killactivity]
|
||||
.insert(
|
||||
_.killerId -> lift(avatarId),
|
||||
_.victimId -> lift(victimId),
|
||||
_.weaponId -> lift(weaponId),
|
||||
_.zoneId -> lift(zoneId),
|
||||
_.px -> lift((position.x * 1000).toInt),
|
||||
_.py -> lift((position.y * 1000).toInt),
|
||||
_.pz -> lift((position.z * 1000).toInt),
|
||||
_.exp -> lift(exp)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an entry into the database's `supportactivity` table.
|
||||
* One player did something for some other player and
|
||||
* that other player was able to kill a third player.
|
||||
*/
|
||||
def reportSupportBy(
|
||||
user: Long,
|
||||
target: Long,
|
||||
exosuit: Int,
|
||||
interaction: Int,
|
||||
intermediate: Int,
|
||||
implement: Int,
|
||||
experience: Long
|
||||
): Unit = {
|
||||
ctx.run(query[persistence.Supportactivity]
|
||||
.insert(
|
||||
_.userId -> lift(user),
|
||||
_.targetId -> lift(target),
|
||||
_.targetExosuit -> lift(exosuit),
|
||||
_.interactionType -> lift(interaction),
|
||||
_.implementType -> lift(implement),
|
||||
_.intermediateType -> lift(intermediate),
|
||||
_.exp -> lift(experience)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to update the database's `weaponstatsession` table and,
|
||||
* if no existing entries can be found,
|
||||
* insert a new entry into the table.
|
||||
* Shots fired.
|
||||
*/
|
||||
def reportToolDischarge(avatarId: Long, stats: EquipmentStat): Unit = {
|
||||
ctx.run(query[persistence.Weaponstatsession]
|
||||
.insert(
|
||||
_.avatarId -> lift(avatarId),
|
||||
_.weaponId -> lift(stats.objectId),
|
||||
_.shotsFired -> lift(stats.shotsFired),
|
||||
_.shotsLanded -> lift(stats.shotsLanded),
|
||||
_.kills -> lift(0),
|
||||
_.assists -> lift(0),
|
||||
_.sessionId -> lift(-1L)
|
||||
)
|
||||
.onConflictUpdate(_.avatarId, _.weaponId, _.sessionId)(
|
||||
(t, e) => t.shotsFired -> (t.shotsFired + e.shotsFired),
|
||||
(t, e) => t.shotsLanded -> (t.shotsLanded + e.shotsLanded)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an entry into the database's `machinedestroyed` table.
|
||||
* Just as stated, something that was not a player was destroyed.
|
||||
* Valid entity types include: vehicles, amenities, and various turrets.
|
||||
*/
|
||||
def reportMachineDestruction(
|
||||
avatarId: Long,
|
||||
machine: VehicleSource,
|
||||
hackState: Option[HackInfo],
|
||||
isCargo: Boolean,
|
||||
weaponId: Int,
|
||||
zoneNumber: Int
|
||||
): Unit = {
|
||||
import net.psforever.util.Database.ctx
|
||||
import net.psforever.util.Database.ctx._
|
||||
val normalFaction = machine.Faction.id
|
||||
val hackedToFaction = hackState.map { _.player.Faction.id }.getOrElse(normalFaction)
|
||||
val machinePosition = machine.Position
|
||||
ctx.run(query[persistence.Machinedestroyed]
|
||||
.insert(
|
||||
_.avatarId -> lift(avatarId),
|
||||
_.weaponId -> lift(weaponId),
|
||||
_.machineType -> lift(machine.Definition.ObjectId),
|
||||
_.machineFaction -> lift(normalFaction),
|
||||
_.hackedFaction -> lift(hackedToFaction),
|
||||
_.asCargo -> lift(isCargo),
|
||||
_.zoneNum -> lift(zoneNumber),
|
||||
_.px -> lift((machinePosition.x * 1000).toInt),
|
||||
_.py -> lift((machinePosition.y * 1000).toInt),
|
||||
_.pz -> lift((machinePosition.z * 1000).toInt)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an entry into the database's `ntuactivity` table.
|
||||
* This table monitors experience earned through NTU silo operations and
|
||||
* first time event entity interactions (zone and building set to 0).
|
||||
*/
|
||||
def reportNtuActivity(
|
||||
avatarId: Long,
|
||||
zoneId: Int,
|
||||
buildingId: Int,
|
||||
experience: Long
|
||||
): Unit = {
|
||||
ctx.run(query[persistence.Ntuactivity]
|
||||
.insert(
|
||||
_.avatarId -> lift(avatarId),
|
||||
_.zoneId -> lift(zoneId),
|
||||
_.buildingId -> lift(buildingId),
|
||||
_.exp -> lift(experience)
|
||||
)
|
||||
.onConflictUpdate(_.avatarId, _.zoneId, _.buildingId)(
|
||||
(t, e) => t.exp -> (t.exp + e.exp)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an entry into the database's `kdasession` table
|
||||
* to specifically update the revive counter for the current session.
|
||||
*/
|
||||
def reportRespawns(
|
||||
avatarId: Long,
|
||||
reviveCount: Int
|
||||
): Unit = {
|
||||
ctx.run(query[persistence.Kdasession]
|
||||
.insert(
|
||||
_.avatarId -> lift(avatarId),
|
||||
_.revives -> lift(reviveCount),
|
||||
_.sessionId -> lift(-1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an entry into the database's `buildingCapture` table.
|
||||
*/
|
||||
def reportFacilityCapture(
|
||||
avatarId: Long,
|
||||
zoneId: Int,
|
||||
buildingId: Int,
|
||||
exp: Long,
|
||||
expType: String
|
||||
): Unit = {
|
||||
ctx.run(query[persistence.Buildingcapture]
|
||||
.insert(
|
||||
_.avatarId -> lift(avatarId),
|
||||
_.zoneId -> lift(zoneId),
|
||||
_.buildingId -> lift(buildingId),
|
||||
_.exp -> lift(exp),
|
||||
_.expType -> lift(expType)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp
|
||||
|
||||
import net.psforever.objects.vital.InGameActivity
|
||||
|
||||
final case class VitalsHistoryException(
|
||||
head: InGameActivity, //InGameActivity might be more suitable?
|
||||
private val message: String = "",
|
||||
private val cause: Throwable = None.orNull
|
||||
) extends Exception(message, cause)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp.rec
|
||||
|
||||
import net.psforever.objects.sourcing.SourceUniqueness
|
||||
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairFromEquipment, RepairingActivity}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
class ArmorRecoveryExperienceContributionProcess(
|
||||
private val faction : PlanetSideEmpire.Value,
|
||||
private val contributions: Map[SourceUniqueness, List[InGameActivity]]
|
||||
) extends RecoveryExperienceContributionProcess(faction, contributions) {
|
||||
def submit(history: List[InGameActivity]): Unit = {
|
||||
history.foreach {
|
||||
case d: DamagingActivity if d.amount - d.health > 0 =>
|
||||
val (damage, recovery) = RecoveryExperienceContribution.contributeWithDamagingActivity(
|
||||
d,
|
||||
d.amount - d.health,
|
||||
damageParticipants,
|
||||
participants,
|
||||
damageInOrder,
|
||||
recoveryInOrder
|
||||
)
|
||||
damageInOrder = damage
|
||||
recoveryInOrder = recovery
|
||||
case r: RepairFromEquipment =>
|
||||
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(
|
||||
r.user,
|
||||
r.equipment_def.ObjectId,
|
||||
faction,
|
||||
r.amount,
|
||||
r.time,
|
||||
damageParticipants,
|
||||
participants,
|
||||
damageInOrder,
|
||||
recoveryInOrder
|
||||
)
|
||||
damageInOrder = damage
|
||||
recoveryInOrder = recovery
|
||||
case r: RepairingActivity =>
|
||||
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(
|
||||
wepid = 0,
|
||||
faction,
|
||||
r.amount,
|
||||
r.time,
|
||||
damageParticipants,
|
||||
participants,
|
||||
damageInOrder,
|
||||
recoveryInOrder
|
||||
)
|
||||
damageInOrder = damage
|
||||
recoveryInOrder = recovery
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp.rec
|
||||
|
||||
import net.psforever.objects.sourcing.SourceUniqueness
|
||||
import net.psforever.objects.vital.InGameActivity
|
||||
import net.psforever.objects.zones.exp.{ContributionStats, KillContributions, Support, WeaponStats}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
class CombinedHealthAndArmorContributionProcess(
|
||||
private val faction : PlanetSideEmpire.Value,
|
||||
private val contributions: Map[SourceUniqueness, List[InGameActivity]],
|
||||
otherSubmissions: Seq[RecoveryExperienceContribution]
|
||||
) extends RecoveryExperienceContribution {
|
||||
private val process: Seq[RecoveryExperienceContributionProcess] = Seq(
|
||||
new HealthRecoveryExperienceContributionProcess(faction, contributions),
|
||||
new ArmorRecoveryExperienceContributionProcess(faction, contributions)
|
||||
)
|
||||
|
||||
def submit(history: List[InGameActivity]): Unit = {
|
||||
for (elem <- process ++ otherSubmissions) { elem.submit(history) }
|
||||
}
|
||||
|
||||
def output(): mutable.LongMap[ContributionStats] = {
|
||||
val output = combineRecoveryContributions(
|
||||
KillContributions.cullContributorImplements(process.head.output()),
|
||||
KillContributions.cullContributorImplements(process(1).output())
|
||||
)
|
||||
clear()
|
||||
output
|
||||
}
|
||||
|
||||
def clear(): Unit = {
|
||||
process.foreach ( _.clear() )
|
||||
}
|
||||
|
||||
private def combineRecoveryContributions(
|
||||
healthAssists: mutable.LongMap[ContributionStats],
|
||||
armorAssists: mutable.LongMap[ContributionStats]
|
||||
): mutable.LongMap[ContributionStats] = {
|
||||
healthAssists
|
||||
.map {
|
||||
case out@(id, healthEntry) =>
|
||||
armorAssists.get(id) match {
|
||||
case Some(armorEntry) =>
|
||||
//healthAssists && armorAssists
|
||||
(id, healthEntry.copy(weapons = healthEntry.weapons ++ armorEntry.weapons))
|
||||
case None =>
|
||||
//healthAssists only
|
||||
out
|
||||
}
|
||||
}
|
||||
.addAll {
|
||||
//armorAssists only
|
||||
val healthKeys = healthAssists.keys.toSeq
|
||||
armorAssists.filter { case (id, _) => !healthKeys.contains(id) }
|
||||
}
|
||||
.map {
|
||||
case (id, entry) =>
|
||||
var totalShots: Int = 0
|
||||
var totalAmount: Int = 0
|
||||
var mostRecentTime: Long = 0
|
||||
val groupedWeapons = entry.weapons
|
||||
.groupBy(_.equipment)
|
||||
.map {
|
||||
case (weaponContext, weaponEntries) =>
|
||||
val specificEntries = weaponEntries.filter(_.equipment == weaponContext)
|
||||
val amount = specificEntries.foldLeft(0)(_ + _.amount)
|
||||
totalAmount = totalAmount + amount
|
||||
val shots = specificEntries.foldLeft(0)(_ + _.shots)
|
||||
totalShots = totalShots + shots
|
||||
val time = specificEntries.maxBy(_.time).time
|
||||
mostRecentTime = math.max(mostRecentTime, time)
|
||||
Support.calculateSupportExperience(
|
||||
event = "support-heal",
|
||||
WeaponStats(weaponContext, amount, shots, time, 1f)
|
||||
)
|
||||
}
|
||||
.toSeq
|
||||
(id, entry.copy(
|
||||
weapons = groupedWeapons,
|
||||
amount = totalAmount,
|
||||
total = math.max(entry.total, totalAmount),
|
||||
shots = totalShots,
|
||||
time = mostRecentTime
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp.rec
|
||||
|
||||
import net.psforever.objects.sourcing.SourceUniqueness
|
||||
import net.psforever.objects.vital.{DamagingActivity, HealFromEquipment, HealingActivity, InGameActivity, RevivingActivity}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
private class HealthRecoveryExperienceContributionProcess(
|
||||
private val faction : PlanetSideEmpire.Value,
|
||||
private val contributions: Map[SourceUniqueness, List[InGameActivity]]
|
||||
) extends RecoveryExperienceContributionProcess(faction, contributions) {
|
||||
def submit(history: List[InGameActivity]): Unit = {
|
||||
history.foreach {
|
||||
case d: DamagingActivity if d.health > 0 =>
|
||||
val (damage, recovery) = RecoveryExperienceContribution.contributeWithDamagingActivity(
|
||||
d,
|
||||
d.health,
|
||||
damageParticipants,
|
||||
participants,
|
||||
damageInOrder,
|
||||
recoveryInOrder
|
||||
)
|
||||
damageInOrder = damage
|
||||
recoveryInOrder = recovery
|
||||
case h: HealFromEquipment =>
|
||||
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(
|
||||
h.user,
|
||||
h.equipment_def.ObjectId,
|
||||
faction,
|
||||
h.amount,
|
||||
h.time,
|
||||
damageParticipants,
|
||||
participants,
|
||||
damageInOrder,
|
||||
recoveryInOrder
|
||||
)
|
||||
damageInOrder = damage
|
||||
recoveryInOrder = recovery
|
||||
case h: HealingActivity =>
|
||||
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(
|
||||
wepid = 0,
|
||||
faction,
|
||||
h.amount,
|
||||
h.time,
|
||||
damageParticipants,
|
||||
participants,
|
||||
damageInOrder,
|
||||
recoveryInOrder
|
||||
)
|
||||
damageInOrder = damage
|
||||
recoveryInOrder = recovery
|
||||
case r: RevivingActivity =>
|
||||
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(
|
||||
r.equipment.ObjectId,
|
||||
faction,
|
||||
r.amount,
|
||||
r.time,
|
||||
damageParticipants,
|
||||
participants,
|
||||
damageInOrder,
|
||||
recoveryInOrder
|
||||
)
|
||||
damageInOrder = damage
|
||||
recoveryInOrder = recovery
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp.rec
|
||||
|
||||
import net.psforever.objects.sourcing.SourceUniqueness
|
||||
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairFromEquipment, RepairingActivity}
|
||||
import net.psforever.objects.zones.exp.{ContributionStats, Support, WeaponStats}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
class MachineRecoveryExperienceContributionProcess(
|
||||
private val faction : PlanetSideEmpire.Value,
|
||||
private val contributions: Map[SourceUniqueness, List[InGameActivity]],
|
||||
eventOutputType: String,
|
||||
private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer()
|
||||
) extends RecoveryExperienceContributionProcess(faction, contributions) {
|
||||
def submit(history: List[InGameActivity]): Unit = {
|
||||
history.foreach {
|
||||
case d: DamagingActivity if d.amount == d.health =>
|
||||
val (damage, recovery) = RecoveryExperienceContribution.contributeWithDamagingActivity(
|
||||
d,
|
||||
d.health,
|
||||
damageParticipants,
|
||||
participants,
|
||||
damageInOrder,
|
||||
recoveryInOrder
|
||||
)
|
||||
damageInOrder = damage
|
||||
recoveryInOrder = recovery
|
||||
case r: RepairFromEquipment if !excludedTargets.contains(r.user.unique) =>
|
||||
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(
|
||||
r.user,
|
||||
r.equipment_def.ObjectId,
|
||||
faction,
|
||||
r.amount,
|
||||
r.time,
|
||||
damageParticipants,
|
||||
participants,
|
||||
damageInOrder,
|
||||
recoveryInOrder
|
||||
)
|
||||
damageInOrder = damage
|
||||
recoveryInOrder = recovery
|
||||
case r: RepairingActivity =>
|
||||
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(
|
||||
wepid = 0,
|
||||
faction,
|
||||
r.amount,
|
||||
r.time,
|
||||
damageParticipants,
|
||||
participants,
|
||||
damageInOrder,
|
||||
recoveryInOrder
|
||||
)
|
||||
damageInOrder = damage
|
||||
recoveryInOrder = recovery
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
override def output(): mutable.LongMap[ContributionStats] = {
|
||||
super.output().map { case (id, stats) =>
|
||||
val weps = stats.weapons
|
||||
.groupBy(_.equipment)
|
||||
.map { case (wrapper, entries) =>
|
||||
val size = entries.size
|
||||
val newTime = entries.maxBy(_.time).time
|
||||
Support.calculateSupportExperience(
|
||||
eventOutputType,
|
||||
WeaponStats(wrapper, size, entries.foldLeft(0)(_ + _.amount), newTime, 1f)
|
||||
)
|
||||
}
|
||||
.toSeq
|
||||
(id, stats.copy(weapons = weps))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp.rec
|
||||
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.vital._
|
||||
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
|
||||
import net.psforever.objects.zones.exp.{ContributionStats, HealKillAssist, WeaponStats}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
trait RecoveryExperienceContribution {
|
||||
def submit(history: List[InGameActivity]): Unit
|
||||
def output(): mutable.LongMap[ContributionStats]
|
||||
def clear(): Unit
|
||||
}
|
||||
|
||||
object RecoveryExperienceContribution {
|
||||
private[exp] def contributeWithDamagingActivity(
|
||||
activity: DamagingActivity,
|
||||
amount: Int,
|
||||
damageParticipants: mutable.LongMap[PlayerSource],
|
||||
recoveryParticipants: mutable.LongMap[ContributionStats],
|
||||
damageOrder: Seq[(Long, Int)],
|
||||
recoveryOrder: Seq[(Long, Int)]
|
||||
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
|
||||
//mark entries from the ordered recovery list to truncate
|
||||
val data: DamageResult = activity.data
|
||||
val time: Long = activity.time
|
||||
var lastCharId: Long = 0L
|
||||
var lastValue: Int = 0
|
||||
var ramt: Int = amount
|
||||
var rindex: Int = 0
|
||||
val riter = recoveryOrder.iterator
|
||||
while (riter.hasNext && ramt > 0) {
|
||||
val (id, value) = riter.next()
|
||||
if (value > 0) {
|
||||
/*
|
||||
if the amount on the previous recovery node is positive, reduce it by the damage value for that user's last used equipment
|
||||
keep traversing recovery nodes, and lobbing them off, until the recovery amount is zero
|
||||
if the user can not be found having an entry, skip the update but lob off the recovery progress node all the same
|
||||
if the amount is zero, do not check any further recovery progress nodes
|
||||
*/
|
||||
recoveryParticipants
|
||||
.get(id)
|
||||
.foreach { entry =>
|
||||
val weapons = entry.weapons
|
||||
lastCharId = id
|
||||
lastValue = value
|
||||
if (value > ramt) {
|
||||
//take from the value on the last-used equipment, at the front of the list
|
||||
recoveryParticipants.put(
|
||||
id,
|
||||
entry.copy(
|
||||
weapons = weapons.head.copy(amount = math.max(0, weapons.head.amount - ramt), time = time) +: weapons.tail,
|
||||
amount = math.max(0, entry.amount - ramt),
|
||||
time = time
|
||||
)
|
||||
)
|
||||
ramt = 0
|
||||
lastValue = lastValue - value
|
||||
} else {
|
||||
//take from the value on the last-used equipment, at the front of the list
|
||||
//move that entry to the end of the list
|
||||
recoveryParticipants.put(
|
||||
id,
|
||||
entry.copy(
|
||||
weapons = weapons.tail :+ weapons.head.copy(amount = 0, time = time),
|
||||
amount = math.max(0, entry.amount - ramt),
|
||||
time = time
|
||||
)
|
||||
)
|
||||
ramt = ramt - value
|
||||
rindex += 1
|
||||
lastValue = 0
|
||||
}
|
||||
}
|
||||
rindex += 1
|
||||
}
|
||||
}
|
||||
//damage order and damage contribution entry
|
||||
val newDamageEntry = data
|
||||
.adversarial
|
||||
.collect { case Adversarial(p: PlayerSource, _, _) => (p, damageParticipants.get(p.CharId)) }
|
||||
.collect {
|
||||
case (player, Some(PlayerSource.Nobody)) =>
|
||||
damageParticipants.put(player.CharId, player)
|
||||
Some(player)
|
||||
case (player, Some(_)) =>
|
||||
damageParticipants.getOrElseUpdate(player.CharId, player)
|
||||
Some(player)
|
||||
}
|
||||
.collect {
|
||||
case Some(player) => (player.CharId, amount) //for damageOrder
|
||||
}
|
||||
.orElse {
|
||||
Some((0L, amount)) //for damageOrder
|
||||
}
|
||||
//re-combine output list(s)
|
||||
val leftovers = if (lastValue > 0) {
|
||||
Seq((lastCharId, lastValue))
|
||||
} else {
|
||||
Nil
|
||||
}
|
||||
(newDamageEntry.toList ++ damageOrder, leftovers ++ recoveryOrder.slice(rindex, recoveryOrder.size) ++ recoveryOrder.take(rindex).map { case (id, _) => (id, 0) })
|
||||
}
|
||||
|
||||
private[exp] def contributeWithRecoveryActivity(
|
||||
user: PlayerSource,
|
||||
wepid: Int,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
amount: Int,
|
||||
time: Long,
|
||||
damageParticipants: mutable.LongMap[PlayerSource],
|
||||
recoveryParticipants: mutable.LongMap[ContributionStats],
|
||||
damageOrder: Seq[(Long, Int)],
|
||||
recoveryOrder: Seq[(Long, Int)]
|
||||
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
|
||||
contributeWithRecoveryActivity(user, user.CharId, wepid, faction, amount, time, damageParticipants, recoveryParticipants, damageOrder, recoveryOrder)
|
||||
}
|
||||
|
||||
private[exp] def contributeWithRecoveryActivity(
|
||||
wepid: Int,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
amount: Int,
|
||||
time: Long,
|
||||
damageParticipants: mutable.LongMap[PlayerSource],
|
||||
recoveryParticipants: mutable.LongMap[ContributionStats],
|
||||
damageOrder: Seq[(Long, Int)],
|
||||
recoveryOrder: Seq[(Long, Int)]
|
||||
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
|
||||
contributeWithRecoveryActivity(PlayerSource.Nobody, charId = 0, wepid, faction, amount, time, damageParticipants, recoveryParticipants, damageOrder, recoveryOrder)
|
||||
}
|
||||
|
||||
private[exp] def contributeWithRecoveryActivity(
|
||||
user: PlayerSource,
|
||||
charId: Long,
|
||||
wepid: Int,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
amount: Int,
|
||||
time: Long,
|
||||
damageParticipants: mutable.LongMap[PlayerSource],
|
||||
recoveryParticipants: mutable.LongMap[ContributionStats],
|
||||
damageOrder: Seq[(Long, Int)],
|
||||
recoveryOrder: Seq[(Long, Int)]
|
||||
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
|
||||
//mark entries from the ordered damage list to truncate
|
||||
val damageEntries = damageOrder.iterator
|
||||
var amtToReduce: Int = amount
|
||||
var amtToGain: Int = 0
|
||||
var lastValue: Int = -1
|
||||
var damageRemoveCount: Int = 0
|
||||
var damageRemainder: Seq[(Long, Int)] = Nil
|
||||
//keep reducing previous damage until recovery amount is depleted, or no more damage entries remain, or the last damage entry was depleted already
|
||||
while (damageEntries.hasNext && amtToReduce > 0 && lastValue != 0) {
|
||||
val (id, value) = damageEntries.next()
|
||||
lastValue = value
|
||||
if (value > 0) {
|
||||
damageParticipants
|
||||
.get(id)
|
||||
.collect {
|
||||
case player if player.Faction != faction =>
|
||||
//if previous attacker was an enemy, the recovery counts towards contribution
|
||||
if (value > amtToReduce) {
|
||||
damageRemainder = Seq((id, value - amtToReduce))
|
||||
amtToGain = amtToGain + amtToReduce
|
||||
amtToReduce = 0
|
||||
} else {
|
||||
amtToGain = amtToGain + value
|
||||
amtToReduce = amtToReduce - value
|
||||
}
|
||||
Some(player)
|
||||
case player =>
|
||||
//if the previous attacker was friendly fire, the recovery doesn't count towards contribution
|
||||
if (value > amtToReduce) {
|
||||
damageRemainder = Seq((id, value - amtToReduce))
|
||||
amtToReduce = 0
|
||||
} else {
|
||||
amtToReduce = amtToReduce - value
|
||||
}
|
||||
Some(player)
|
||||
}
|
||||
.orElse {
|
||||
//if we couldn't find an entry, just give the contribution to the user anyway
|
||||
damageParticipants.put(id, user)
|
||||
if (value > amtToReduce) {
|
||||
damageRemainder = Seq((id, value - amtToReduce))
|
||||
amtToGain = amtToGain + amtToReduce
|
||||
amtToReduce = 0
|
||||
} else {
|
||||
amtToGain = amtToGain + value
|
||||
amtToReduce = amtToReduce - value
|
||||
}
|
||||
None
|
||||
}
|
||||
//keep track of entries whose damage was depleted
|
||||
damageRemoveCount += 1
|
||||
}
|
||||
}
|
||||
amtToGain = amtToGain + amtToReduce //if early termination, gives leftovers as gain
|
||||
if (amtToGain > 0) {
|
||||
val newWeaponStats = WeaponStats(HealKillAssist(wepid), amtToGain, 1, time, 1f)
|
||||
//try: add first contribution entry
|
||||
//then: add accumulation of last weapon entry to contribution entry
|
||||
//last: add new weapon entry to contribution entry
|
||||
recoveryParticipants
|
||||
.getOrElseUpdate(
|
||||
charId,
|
||||
ContributionStats(user, Seq(newWeaponStats), amtToGain, amtToGain, 1, time)
|
||||
) match {
|
||||
case entry if entry.weapons.size > 1 =>
|
||||
if (entry.weapons.head.equipment.equipment == wepid) {
|
||||
val head = entry.weapons.head
|
||||
recoveryParticipants.put(
|
||||
charId,
|
||||
entry.copy(
|
||||
weapons = head.copy(amount = head.amount + amtToGain, shots = head.shots + 1, time = time) +: entry.weapons.tail,
|
||||
amount = entry.amount + amtToGain,
|
||||
total = entry.total + amtToGain,
|
||||
shots = entry.shots + 1,
|
||||
time = time
|
||||
)
|
||||
)
|
||||
} else {
|
||||
recoveryParticipants.put(
|
||||
charId,
|
||||
entry.copy(
|
||||
weapons = newWeaponStats +: entry.weapons,
|
||||
amount = entry.amount + amtToGain,
|
||||
total = entry.total + amtToGain,
|
||||
shots = entry.shots + 1,
|
||||
time = time
|
||||
)
|
||||
)
|
||||
}
|
||||
case _ => ()
|
||||
//not technically possible
|
||||
}
|
||||
}
|
||||
val newRecoveryEntry = if (amtToGain == 0) {
|
||||
Seq((0L, amount))
|
||||
} else if (amtToGain < amount) {
|
||||
Seq((0L, amount - amtToGain), (charId, amtToGain))
|
||||
} else {
|
||||
Seq((charId, amount))
|
||||
}
|
||||
(
|
||||
damageRemainder ++ damageOrder.drop(damageRemoveCount) ++ damageOrder.take(damageRemoveCount).map { case (id, _) => (id, 0) },
|
||||
newRecoveryEntry ++ recoveryOrder
|
||||
)
|
||||
}
|
||||
|
||||
private[exp] def contributeWithSupportRecoveryActivity(
|
||||
users: Seq[PlayerSource],
|
||||
wepid: Int,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
amount: Int,
|
||||
time: Long,
|
||||
participants: mutable.LongMap[ContributionStats],
|
||||
damageOrder: Seq[(Long, Int)],
|
||||
recoveryOrder: Seq[(Long, Int)]
|
||||
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
|
||||
var outputDamageOrder = damageOrder
|
||||
var outputRecoveryOrder = recoveryOrder
|
||||
if (users.nonEmpty) {
|
||||
val damageParticipants: mutable.LongMap[PlayerSource] = mutable.LongMap[PlayerSource]()
|
||||
users.zip {
|
||||
val numberOfUsers = users.size
|
||||
val out = Array.fill(numberOfUsers)(numberOfUsers / amount)
|
||||
(0 to numberOfUsers % amount).foreach {
|
||||
out(_) += 1
|
||||
}
|
||||
out
|
||||
}.foreach { case (user, subAmount) =>
|
||||
val (a, b) = contributeWithRecoveryActivity(user, user.CharId, wepid, faction, subAmount, time, damageParticipants, participants, outputDamageOrder, outputRecoveryOrder)
|
||||
outputDamageOrder = a
|
||||
outputRecoveryOrder = b
|
||||
}
|
||||
}
|
||||
(outputDamageOrder, outputRecoveryOrder)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.zones.exp.rec
|
||||
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceUniqueness}
|
||||
import net.psforever.objects.vital.InGameActivity
|
||||
import net.psforever.objects.zones.exp.ContributionStats
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
//noinspection ScalaUnusedSymbol
|
||||
abstract class RecoveryExperienceContributionProcess(
|
||||
faction : PlanetSideEmpire.Value,
|
||||
contributions: Map[SourceUniqueness, List[InGameActivity]]
|
||||
) extends RecoveryExperienceContribution {
|
||||
protected var damageInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
|
||||
protected var recoveryInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
|
||||
protected val contributionsBy: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
|
||||
protected val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
|
||||
protected val damageParticipants: mutable.LongMap[PlayerSource] = mutable.LongMap[PlayerSource]()
|
||||
|
||||
def submit(history: List[InGameActivity]): Unit
|
||||
|
||||
def output(): mutable.LongMap[ContributionStats] = {
|
||||
val output = participants.map { a => a }
|
||||
clear()
|
||||
output
|
||||
}
|
||||
|
||||
def clear(): Unit = {
|
||||
damageInOrder = Nil
|
||||
recoveryInOrder = Nil
|
||||
contributionsBy.clear()
|
||||
participants.clear()
|
||||
damageParticipants.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import scala.annotation.switch
|
|||
* na
|
||||
*/
|
||||
sealed abstract class Statistic(val code: Int)
|
||||
|
||||
/**
|
||||
* na
|
||||
*/
|
||||
|
|
@ -38,12 +39,18 @@ sealed case class IntermediateStatistic(
|
|||
* @param fields four pairs of values that add together to produce the first columns on the statistics spreadsheet;
|
||||
* organized as TR, NC, VS, BO (PS)
|
||||
*/
|
||||
final case class InitStatistic(
|
||||
final case class CampaignStatistic(
|
||||
category: StatisticalCategory,
|
||||
unk: StatisticalElement,
|
||||
fields: List[Long]
|
||||
) extends Statistic(code = 0) with ComplexStatistic
|
||||
|
||||
object CampaignStatistic {
|
||||
def apply(cat: StatisticalCategory, stat: StatisticalElement, tr: Long, nc: Long, vs: Long, bo: Long): CampaignStatistic = {
|
||||
CampaignStatistic(cat, stat, List(tr, 0, nc, 0, vs, 0, bo, 0))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param category na
|
||||
|
|
@ -51,12 +58,18 @@ final case class InitStatistic(
|
|||
* @param fields four pairs of values that add together to produce the first column(s) on the statistics spreadsheet;
|
||||
* organized as TR, NC, VS, BO (PS)
|
||||
*/
|
||||
final case class UpdateStatistic(
|
||||
final case class SessionStatistic(
|
||||
category: StatisticalCategory,
|
||||
unk: StatisticalElement,
|
||||
fields: List[Long]
|
||||
) extends Statistic(code = 1) with ComplexStatistic
|
||||
|
||||
object SessionStatistic {
|
||||
def apply(cat: StatisticalCategory, stat: StatisticalElement, tr: Long, nc: Long, vs: Long, bo: Long): SessionStatistic = {
|
||||
SessionStatistic(cat, stat, List(0, tr, 0, nc, 0, vs, 0, bo))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param deaths how badly you suck, quantitatively analyzed
|
||||
|
|
@ -103,10 +116,10 @@ object AvatarStatisticsMessage extends Marshallable[AvatarStatisticsMessage] {
|
|||
*/
|
||||
private val initCodec: Codec[Statistic] = complexCodec.exmap[Statistic](
|
||||
{
|
||||
case IntermediateStatistic(a, b, c) => Successful(InitStatistic(a, b, c))
|
||||
case IntermediateStatistic(a, b, c) => Successful(CampaignStatistic(a, b, c))
|
||||
},
|
||||
{
|
||||
case InitStatistic(a, b, c) => Successful(IntermediateStatistic(a, b, c))
|
||||
case CampaignStatistic(a, b, c) => Successful(IntermediateStatistic(a, b, c))
|
||||
case _ => Failure(Err("expected initializing statistic, but found something else"))
|
||||
}
|
||||
)
|
||||
|
|
@ -115,10 +128,10 @@ object AvatarStatisticsMessage extends Marshallable[AvatarStatisticsMessage] {
|
|||
*/
|
||||
private val updateCodec: Codec[Statistic] = complexCodec.exmap[Statistic](
|
||||
{
|
||||
case IntermediateStatistic(a, b, c) => Successful(UpdateStatistic(a, b, c))
|
||||
case IntermediateStatistic(a, b, c) => Successful(SessionStatistic(a, b, c))
|
||||
},
|
||||
{
|
||||
case UpdateStatistic(a, b, c) => Successful(IntermediateStatistic(a, b, c))
|
||||
case SessionStatistic(a, b, c) => Successful(IntermediateStatistic(a, b, c))
|
||||
case _ => Failure(Err("expected updating statistic, but found something else"))
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ import scodec.codecs._
|
|||
* At the moment, it only seems possible to receive and read mail from the server.
|
||||
* @param sender the name of the player who sent the mail
|
||||
* @param subject the subject
|
||||
* @param message the message
|
||||
* @param message the message;
|
||||
* line breaks use `\n`
|
||||
*/
|
||||
final case class MailMessage(sender: String, subject: String, message: String) extends PlanetSideGamePacket {
|
||||
type Packet = MailMessage
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ case class Account(
|
|||
inactive: Boolean = false,
|
||||
gm: Boolean = false,
|
||||
lastFactionId: Int = 3,
|
||||
avatarLoggedIn: Long,
|
||||
sessionId: Long,
|
||||
token: Option[String],
|
||||
tokenCreated: Option[LocalDateTime]
|
||||
)
|
||||
|
|
|
|||
95
src/main/scala/net/psforever/persistence/KdaExp.scala
Normal file
95
src/main/scala/net/psforever/persistence/KdaExp.scala
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.persistence
|
||||
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
case class Assistactivity(
|
||||
index: Int,
|
||||
killerId: Long,
|
||||
victimId: Long,
|
||||
weaponId: Int,
|
||||
zoneId: Int,
|
||||
px: Int, //Position.x * 1000
|
||||
py: Int, //Position.y * 1000
|
||||
pz: Int, //Position.z * 1000
|
||||
exp: Long,
|
||||
timestamp: LocalDateTime = LocalDateTime.now()
|
||||
)
|
||||
|
||||
case class Buildingcapture(
|
||||
index: Int,
|
||||
avatarId: Long,
|
||||
zoneId: Int,
|
||||
buildingId: Int,
|
||||
exp: Long,
|
||||
expType: String,
|
||||
timestamp: LocalDateTime = LocalDateTime.now()
|
||||
)
|
||||
|
||||
case class Kdasession (
|
||||
avatarId: Long,
|
||||
sessionId: Int,
|
||||
kills: Int,
|
||||
deaths: Int,
|
||||
assists: Int,
|
||||
revives: Int
|
||||
)
|
||||
|
||||
case class Killactivity(
|
||||
index: Int,
|
||||
killerId: Long,
|
||||
victimId: Long,
|
||||
victimExosuit: Int,
|
||||
victimMounted: Int, //object type id * 10 + seat type
|
||||
weaponId: Int,
|
||||
zoneId: Int,
|
||||
px: Int, //Position.x * 1000
|
||||
py: Int, //Position.y * 1000
|
||||
pz: Int, //Position.z * 1000
|
||||
exp: Long,
|
||||
timestamp: LocalDateTime = LocalDateTime.now()
|
||||
)
|
||||
|
||||
case class Machinedestroyed(
|
||||
index: Int,
|
||||
avatarId: Long,
|
||||
weaponId: Int,
|
||||
machineType: Int,
|
||||
machineFaction: Int,
|
||||
hackedFaction: Int,
|
||||
asCargo: Boolean,
|
||||
zoneNum: Int,
|
||||
px: Int, //Position.x * 1000
|
||||
py: Int, //Position.y * 1000
|
||||
pz: Int, //Position.z * 1000
|
||||
timestamp: LocalDateTime = LocalDateTime.now()
|
||||
)
|
||||
|
||||
case class Ntuactivity (
|
||||
avatarId: Long,
|
||||
zoneId: Int,
|
||||
buildingId: Int,
|
||||
exp: Long
|
||||
)
|
||||
|
||||
case class Supportactivity(
|
||||
index: Int,
|
||||
userId: Long,
|
||||
targetId: Long,
|
||||
targetExosuit: Int,
|
||||
interactionType: Int,
|
||||
implementType: Int,
|
||||
intermediateType: Int,
|
||||
exp: Long,
|
||||
timestamp: LocalDateTime = LocalDateTime.now()
|
||||
)
|
||||
|
||||
case class Weaponstatsession(
|
||||
avatarId: Long,
|
||||
weaponId: Int,
|
||||
shotsFired: Int,
|
||||
shotsLanded: Int,
|
||||
kills: Int,
|
||||
assists: Int,
|
||||
sessionId: Long
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.persistence
|
||||
|
||||
case class Progressiondebt(
|
||||
avatarId:Long,
|
||||
experience: Long
|
||||
)
|
||||
|
|
@ -82,7 +82,7 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
|
|||
entryManagementBehaviors
|
||||
.orElse {
|
||||
case RemoverActor.AddTask(obj, zone, duration) =>
|
||||
val entry = RemoverActor.Entry(obj, zone, duration.getOrElse(FirstStandardDuration).toNanos)
|
||||
val entry = RemoverActor.Entry(obj, zone, duration.getOrElse(FirstStandardDuration).toMillis)
|
||||
if (InclusionTest(entry) && !secondHeap.exists(test => sameEntryComparator.Test(test, entry))) {
|
||||
InitialJob(entry)
|
||||
if (entry.duration == 0) {
|
||||
|
|
@ -120,7 +120,7 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
|
|||
case RemoverActor.StartDelete() =>
|
||||
firstTask.cancel()
|
||||
secondTask.cancel()
|
||||
val now: Long = System.nanoTime
|
||||
val now: Long = System.currentTimeMillis()
|
||||
val (in, out) = firstHeap.partition(entry => {
|
||||
now - entry.time >= entry.duration
|
||||
})
|
||||
|
|
@ -229,19 +229,19 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
|
|||
* this new entry is always set to last for the duration of the second pool
|
||||
*/
|
||||
private def RepackageEntry(entry: RemoverActor.Entry): RemoverActor.Entry = {
|
||||
RemoverActor.Entry(entry.obj, entry.zone, SecondStandardDuration.toNanos)
|
||||
RemoverActor.Entry(entry.obj, entry.zone, SecondStandardDuration.toMillis)
|
||||
}
|
||||
|
||||
/**
|
||||
* Common function to reset the first task's delayed execution.
|
||||
* Cancels the scheduled timer and will only restart the timer if there is at least one entry in the first pool.
|
||||
* @param now the time (in nanoseconds);
|
||||
* defaults to the current time (in nanoseconds)
|
||||
* @param now the time (in milliseconds);
|
||||
* defaults to the current time (in milliseconds)
|
||||
*/
|
||||
def RetimeFirstTask(now: Long = System.nanoTime): Unit = {
|
||||
def RetimeFirstTask(now: Long = System.currentTimeMillis()): Unit = {
|
||||
firstTask.cancel()
|
||||
if (firstHeap.nonEmpty) {
|
||||
val short_timeout: FiniteDuration = math.max(1, firstHeap.head.duration - (now - firstHeap.head.time)) nanoseconds
|
||||
val short_timeout: FiniteDuration = math.max(1, firstHeap.head.duration - (now - firstHeap.head.time)).milliseconds
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
firstTask = context.system.scheduler.scheduleOnce(short_timeout, self, RemoverActor.StartDelete())
|
||||
}
|
||||
|
|
@ -274,14 +274,14 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
|
|||
/**
|
||||
* Default time for entries waiting in the first list.
|
||||
* Override.
|
||||
* @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds)
|
||||
* @return the time as a `FiniteDuration` object (to be later transformed into milliseconds)
|
||||
*/
|
||||
def FirstStandardDuration: FiniteDuration
|
||||
|
||||
/**
|
||||
* Default time for entries waiting in the second list.
|
||||
* Override.
|
||||
* @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds)
|
||||
* @return the time as a `FiniteDuration` object (to be later transformed into milliseconds)
|
||||
*/
|
||||
def SecondStandardDuration: FiniteDuration
|
||||
|
||||
|
|
@ -322,7 +322,7 @@ object RemoverActor extends SupportActorCaseConversions {
|
|||
* Internally, all entries have a "time created" field.
|
||||
* @param _obj the target
|
||||
* @param _zone the zone in which this target is registered
|
||||
* @param _duration how much longer the target will exist in its current state (in nanoseconds)
|
||||
* @param _duration how much longer the target will exist in its current state (in milliseconds)
|
||||
*/
|
||||
case class Entry(_obj: PlanetSideGameObject, _zone: Zone, _duration: Long)
|
||||
extends SupportActor.Entry(_obj, _zone, _duration)
|
||||
|
|
@ -332,7 +332,7 @@ object RemoverActor extends SupportActorCaseConversions {
|
|||
* @see `FirstStandardDuration`
|
||||
* @param obj the target
|
||||
* @param zone the zone in which this target is registered
|
||||
* @param duration how much longer the target will exist in its current state (in nanoseconds);
|
||||
* @param duration how much longer the target will exist in its current state (in milliseconds);
|
||||
* a default time duration is provided by implementation
|
||||
*/
|
||||
case class AddTask(obj: PlanetSideGameObject, zone: Zone, duration: Option[FiniteDuration] = None)
|
||||
|
|
|
|||
|
|
@ -438,7 +438,34 @@ class AvatarService(zone: Zone) extends Actor {
|
|||
)
|
||||
)
|
||||
|
||||
case _ => ;
|
||||
case AvatarAction.AwardBep(charId, bep, expType) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(
|
||||
s"/$forChannel/Avatar",
|
||||
Service.defaultPlayerGUID,
|
||||
AvatarResponse.AwardBep(charId, bep, expType)
|
||||
)
|
||||
)
|
||||
|
||||
case AvatarAction.AwardCep(charId, bep) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(
|
||||
s"/$forChannel/Avatar",
|
||||
Service.defaultPlayerGUID,
|
||||
AvatarResponse.AwardCep(charId, bep)
|
||||
)
|
||||
)
|
||||
|
||||
case AvatarAction.FacilityCaptureRewards(building_id, zone_number, exp) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(
|
||||
s"/$forChannel/Avatar",
|
||||
Service.defaultPlayerGUID,
|
||||
AvatarResponse.FacilityCaptureRewards(building_id, zone_number, exp)
|
||||
)
|
||||
)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
//message to Undertaker
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import net.psforever.objects.sourcing.SourceEntry
|
|||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.PlanetSideGamePacket
|
||||
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent}
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
|
||||
import net.psforever.types.{ExoSuitType, ExperienceType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
|
|
@ -156,6 +156,9 @@ object AvatarAction {
|
|||
final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Action
|
||||
|
||||
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action
|
||||
final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Action
|
||||
final case class AwardCep(charId: Long, bep: Long) extends Action
|
||||
final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Action
|
||||
|
||||
final case class TeardownConnection() extends Action
|
||||
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import net.psforever.objects.sourcing.SourceEntry
|
|||
import net.psforever.packet.PlanetSideGamePacket
|
||||
import net.psforever.packet.game.objectcreate.ConstructorData
|
||||
import net.psforever.packet.game.ObjectCreateMessage
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
|
||||
import net.psforever.types.{ExoSuitType, ExperienceType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
|
||||
import net.psforever.services.GenericEventBusMsg
|
||||
|
||||
final case class AvatarServiceResponse(
|
||||
|
|
@ -127,4 +127,7 @@ object AvatarResponse {
|
|||
final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Response
|
||||
|
||||
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response
|
||||
final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Response
|
||||
final case class AwardCep(charId: Long, bep: Long) extends Response
|
||||
final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Response
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,7 @@ class DoorCloseActor() extends Actor {
|
|||
})
|
||||
|
||||
if (openDoors.nonEmpty) {
|
||||
val short_timeout: FiniteDuration =
|
||||
math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)) nanoseconds
|
||||
val short_timeout: FiniteDuration = math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)).milliseconds
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors())
|
||||
}
|
||||
|
|
@ -67,7 +66,7 @@ class DoorCloseActor() extends Actor {
|
|||
* and newer entries are always added to the end of the main `List`,
|
||||
* processing in order is always correct.
|
||||
* @param list the `List` of entries to divide
|
||||
* @param now the time right now (in nanoseconds)
|
||||
* @param now the time right now (in milliseconds)
|
||||
* @see `List.partition`
|
||||
* @return a `Tuple` of two `Lists`, whose qualifications are explained above
|
||||
*/
|
||||
|
|
@ -84,7 +83,7 @@ class DoorCloseActor() extends Actor {
|
|||
* a `List` of elements that have exceeded the time limit,
|
||||
* and a `List` of elements that still satisfy the time limit.
|
||||
* @param iter the `Iterator` of entries to divide
|
||||
* @param now the time right now (in nanoseconds)
|
||||
* @param now the time right now (in milliseconds)
|
||||
* @param index a persistent record of the index where list division should occur;
|
||||
* defaults to 0
|
||||
* @return the index where division will occur
|
||||
|
|
|
|||
|
|
@ -9,19 +9,16 @@ import net.psforever.objects.serverobject.llu.CaptureFlag
|
|||
import net.psforever.objects.serverobject.structures.Building
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects.{Default, Player}
|
||||
import net.psforever.objects.Default
|
||||
import net.psforever.packet.game.{GenericAction, PlanetsideAttributeEnum}
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.zones.ZoneHotSpotProjector
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.local.support.HackCaptureActor.GetHackingFaction
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
|
||||
import net.psforever.util.Config
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration.{FiniteDuration, _}
|
||||
import scala.util.{Random, Success}
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* Responsible for handling the aspects related to hacking control consoles and capturing bases.
|
||||
|
|
@ -40,7 +37,7 @@ class HackCaptureActor extends Actor {
|
|||
val duration = target.Definition.FacilityHackTime
|
||||
target.HackedBy match {
|
||||
case Some(hackInfo) =>
|
||||
target.HackedBy = hackInfo.Duration(duration.toNanos)
|
||||
target.HackedBy = hackInfo.Duration(duration.toMillis)
|
||||
case None =>
|
||||
log.error(s"Initial $target hack information is missing")
|
||||
}
|
||||
|
|
@ -59,8 +56,8 @@ class HackCaptureActor extends Actor {
|
|||
case HackCaptureActor.ProcessCompleteHacks() =>
|
||||
log.trace("Processing complete hacks")
|
||||
clearTrigger.cancel()
|
||||
val now: Long = System.nanoTime
|
||||
val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toNanos)
|
||||
val now: Long = System.currentTimeMillis()
|
||||
val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toMillis)
|
||||
hackedObjects = stillHacked
|
||||
finishedHacks.foreach { entry =>
|
||||
val terminal = entry.target
|
||||
|
|
@ -79,14 +76,9 @@ class HackCaptureActor extends Actor {
|
|||
|
||||
case _ =>
|
||||
// Timed hack finished (or neutral LLU base with no neighbour as timed hack), capture the base
|
||||
val hackTime = terminal.Definition.FacilityHackTime.toMillis
|
||||
HackCompleted(terminal, hackedByFaction)
|
||||
HackCaptureActor.RewardFacilityCaptureParticipants(
|
||||
building,
|
||||
terminal,
|
||||
hacker,
|
||||
now - entry.hack_timestamp,
|
||||
isResecured = false
|
||||
)
|
||||
building.Participation.RewardFacilityCapture(terminal.Faction, hackedByFaction, hacker, hackTime, hackTime, isResecured = false)
|
||||
}
|
||||
}
|
||||
// If there's hacked objects left in the list restart the timer with the shortest hack time left
|
||||
|
|
@ -94,6 +86,7 @@ class HackCaptureActor extends Actor {
|
|||
|
||||
case HackCaptureActor.ResecureCaptureTerminal(target, _, hacker) =>
|
||||
val (results, remainder) = hackedObjects.partition(x => x.target eq target)
|
||||
val faction = GetHackingFaction(target).getOrElse(target.Faction)
|
||||
target.HackedBy = None
|
||||
hackedObjects = remainder
|
||||
val building = target.Owner.asInstanceOf[Building]
|
||||
|
|
@ -102,13 +95,7 @@ class HackCaptureActor extends Actor {
|
|||
case flag: CaptureFlag => target.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.Resecured)
|
||||
}
|
||||
NotifyHackStateChange(target, isResecured = true)
|
||||
// HackCaptureActor.RewardFacilityCaptureParticipants(
|
||||
// building,
|
||||
// target,
|
||||
// hacker,
|
||||
// System.currentTimeMillis() - results.head.hack_timestamp,
|
||||
// isResecured = true
|
||||
// )
|
||||
building.Participation.RewardFacilityCapture(target.Faction, faction, hacker, target.Definition.FacilityHackTime.toMillis, System.currentTimeMillis() - results.head.hack_timestamp, isResecured = true)
|
||||
// Restart the timer in case the object we just removed was the next one scheduled
|
||||
RestartTimer()
|
||||
|
||||
|
|
@ -124,13 +111,7 @@ class HackCaptureActor extends Actor {
|
|||
val hackedByFaction = hackInfo.hackerFaction
|
||||
hackedObjects = hackedObjects.filterNot(x => x == entry)
|
||||
HackCompleted(terminal, hackedByFaction)
|
||||
// HackCaptureActor.RewardFacilityCaptureParticipants(
|
||||
// building,
|
||||
// terminal,
|
||||
// hacker,
|
||||
// System.currentTimeMillis() - entry.hack_timestamp,
|
||||
// isResecured = false
|
||||
// )
|
||||
building.Participation.RewardFacilityCapture(terminal.Faction, hacker.Faction, hacker, terminal.Definition.FacilityHackTime.toMillis, System.currentTimeMillis() - entry.hack_timestamp, isResecured = false)
|
||||
entry.target.Actor ! CommonMessages.ClearHack()
|
||||
flag.Zone.LocalEvents ! CaptureFlagManager.Captured(flag)
|
||||
// If there's hacked objects left in the list restart the timer with the shortest hack time left
|
||||
|
|
@ -151,6 +132,9 @@ class HackCaptureActor extends Actor {
|
|||
case owner: Building if owner.IsCtfBase => Some((owner, owner.GetFlag, owner.Neighbours(hackingFaction)))
|
||||
case _ => None
|
||||
}) match {
|
||||
case None =>
|
||||
//not an error; this is just not a ctf facility
|
||||
false
|
||||
case Some((owner, None, Some(neighbours))) if neighbours.nonEmpty =>
|
||||
log.info(s"An LLU is being spawned for facility ${owner.Name} by $hackingFaction")
|
||||
spawnCaptureFlag(neighbours, terminal, hackingFaction)
|
||||
|
|
@ -231,9 +215,8 @@ class HackCaptureActor extends Actor {
|
|||
|
||||
private def RestartTimer(): Unit = {
|
||||
if (hackedObjects.nonEmpty) {
|
||||
val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.nanoTime()))
|
||||
val short_timeout: FiniteDuration =
|
||||
math.max(1, hackEntry.duration.toNanos - (System.nanoTime - hackEntry.hack_timestamp)).nanoseconds
|
||||
val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.currentTimeMillis()))
|
||||
val short_timeout: FiniteDuration = math.max(1, hackEntry.duration.toMillis - (System.currentTimeMillis() - hackEntry.hack_timestamp)).milliseconds
|
||||
log.trace(s"RestartTimer: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds")
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackCaptureActor.ProcessCompleteHacks())
|
||||
|
|
@ -247,7 +230,7 @@ object HackCaptureActor {
|
|||
zone: Zone,
|
||||
unk1: Long,
|
||||
unk2: Long,
|
||||
startTime: Long = System.nanoTime()
|
||||
startTime: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource)
|
||||
|
|
@ -274,8 +257,7 @@ object HackCaptureActor {
|
|||
17039360L
|
||||
case Some(Hackable.HackInfo(p, _, start, length)) =>
|
||||
// See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated
|
||||
val hackTimeRemainingMS =
|
||||
TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
|
||||
val hackTimeRemainingMS = math.max(0, start + length - System.currentTimeMillis())
|
||||
val startNum = p.Faction match {
|
||||
case PlanetSideEmpire.TR => 0x10000
|
||||
case PlanetSideEmpire.NC => 0x20000
|
||||
|
|
@ -291,149 +273,12 @@ object HackCaptureActor {
|
|||
entry1: HackCaptureActor.HackEntry,
|
||||
entry2: HackCaptureActor.HackEntry
|
||||
): HackCaptureActor.HackEntry = {
|
||||
val entry1TimeLeft = entry1.duration.toNanos - (now - entry1.hack_timestamp)
|
||||
val entry2TimeLeft = entry2.duration.toNanos - (now - entry2.hack_timestamp)
|
||||
val entry1TimeLeft = entry1.duration.toMillis - (now - entry1.hack_timestamp)
|
||||
val entry2TimeLeft = entry2.duration.toMillis - (now - entry2.hack_timestamp)
|
||||
if (entry1TimeLeft < entry2TimeLeft) {
|
||||
entry1
|
||||
} else {
|
||||
entry2
|
||||
}
|
||||
}
|
||||
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
private implicit val timeout: Timeout = Timeout(5.seconds)
|
||||
|
||||
private def RewardFacilityCaptureParticipants(
|
||||
building: Building,
|
||||
terminal: CaptureTerminal,
|
||||
hacker: PlayerSource,
|
||||
time: Long,
|
||||
isResecured: Boolean
|
||||
): Unit = {
|
||||
val faction: PlanetSideEmpire.Value = terminal.Faction
|
||||
val (contributionVictor, contributionAgainst) = building.PlayerContribution.keys.partition { _.Faction == faction }
|
||||
val contributionVictorSize = contributionVictor.size
|
||||
val flagCarrier = if (!isResecured) {
|
||||
building.GetFlagSocket.flatMap(_.previousFlag).flatMap(_.Carrier)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
val request = ask(building.Zone.Activity, ZoneHotSpotProjector.ExposeHeatForRegion(building.Position, building.Definition.SOIRadius.toFloat))
|
||||
request.onComplete {
|
||||
case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) =>
|
||||
val (heatVictor, heatAgainst) = {
|
||||
val reports = activity.map { _.Activity }
|
||||
val allHeat: List[Long] = reports.map { a => a.values.foldLeft(0L)(_ + _.Heat) }
|
||||
val _rewardedHeat: List[Long] = reports.flatMap { rep => rep.get(faction).map { _.Heat.toLong } }
|
||||
val _enemyHeat = allHeat.indices.map { index =>
|
||||
val allHeatValue = allHeat(index)
|
||||
val rewardedHeatValue = _rewardedHeat(index)
|
||||
allHeatValue - rewardedHeatValue
|
||||
}
|
||||
(_rewardedHeat, _enemyHeat.toList)
|
||||
}
|
||||
val heatVictorSum: Long = heatVictor.sum[Long]
|
||||
val heatAgainstSum: Long = heatAgainst.sum[Long]
|
||||
if (contributionVictorSize > 0) {
|
||||
val contributionRate = if (heatVictorSum * heatAgainstSum != 0) {
|
||||
math.log(heatVictorSum * contributionVictorSize / heatAgainstSum.toFloat).toFloat
|
||||
} else {
|
||||
contributionAgainst.size / contributionVictorSize.toFloat
|
||||
}
|
||||
RewardFacilityCaptureParticipants(building, terminal, faction, hacker, building.PlayersInSOI, flagCarrier, isResecured, time, contributionRate)
|
||||
}
|
||||
case _ =>
|
||||
RewardFacilityCaptureParticipants(building, terminal, faction, hacker, building.PlayersInSOI, flagCarrier, isResecured, time, victorContributionRate = 1.0f)
|
||||
}
|
||||
request.recover {
|
||||
_ => RewardFacilityCaptureParticipants(building, terminal, faction, hacker, building.PlayersInSOI, flagCarrier, isResecured, time, victorContributionRate = 1.0f)
|
||||
}
|
||||
}
|
||||
|
||||
private def RewardFacilityCaptureParticipants(
|
||||
building: Building,
|
||||
terminal: CaptureTerminal,
|
||||
faction: PlanetSideEmpire.Value,
|
||||
hacker: PlayerSource,
|
||||
targets: List[Player],
|
||||
flagCarrier: Option[Player],
|
||||
isResecured: Boolean,
|
||||
hackTime: Long,
|
||||
victorContributionRate: Float
|
||||
): Unit = {
|
||||
val contribution = building.PlayerContribution
|
||||
val (contributionVictor, contributionAgainst) = contribution.keys.partition { _.Faction == faction }
|
||||
val contributionVictorSize = contributionVictor.size
|
||||
val contributionAgainstSize = contributionAgainst.size
|
||||
val (contributionByTime, contributionByTimePartitioned) = {
|
||||
val curr = System.currentTimeMillis()
|
||||
val interval = 300000
|
||||
val range: Seq[Long] = {
|
||||
val htime = hackTime.toInt
|
||||
(
|
||||
if (htime < 60000) {
|
||||
Seq(htime, interval + htime, 2 * interval + htime)
|
||||
} else if (htime <= interval) {
|
||||
Seq(60000, htime, interval + htime, 2 * interval + htime)
|
||||
} else {
|
||||
(60000 +: (interval to htime by interval)) ++ Seq(interval + htime, 2 * interval + htime)
|
||||
}
|
||||
).map { _.toLong }
|
||||
}
|
||||
val playerMap = Array.fill[mutable.ListBuffer[Player]](range.size)(mutable.ListBuffer.empty)
|
||||
contribution.foreach { case (p, t) =>
|
||||
playerMap(range.lastIndexWhere(time => curr - t <= time)).addOne(p)
|
||||
}
|
||||
(playerMap, playerMap.map { _.partition(_.Faction == faction) })
|
||||
}
|
||||
val contributionByTimeSize = contributionByTime.length
|
||||
|
||||
val base: Long = 50L
|
||||
val overallPopulationBonus = {
|
||||
contributionByTime.map { _.size }.sum * contributionByTimeSize +
|
||||
contributionByTime.zipWithIndex.map { case (lst, index) =>
|
||||
((contributionByTimeSize - index) * lst.size *
|
||||
{
|
||||
val lists = contributionByTimePartitioned(index)
|
||||
lists._2.size / math.max(lists._1.size, 1).toFloat
|
||||
}).toLong
|
||||
}.sum
|
||||
}
|
||||
val competitionBonus: Long = if (contributionAgainstSize * 1.5f < contributionVictorSize.toFloat) {
|
||||
//steamroll by the victor
|
||||
25L * (contributionVictorSize - contributionAgainstSize)
|
||||
} else if (contributionVictorSize * 1.5f <= contributionAgainstSize.toFloat) {
|
||||
//victory against overwhelming odds
|
||||
500L + 50L * contribution.keys.size
|
||||
} else {
|
||||
//still a battle
|
||||
10L * math.min(contributionAgainstSize, contributionVictorSize)
|
||||
}
|
||||
val timeMultiplier: Float = {
|
||||
val buildingHackTimeMilli = terminal.Definition.FacilityHackTime.toMillis.toFloat
|
||||
1f + (if (isResecured) {
|
||||
(buildingHackTimeMilli - hackTime) / buildingHackTimeMilli
|
||||
} else {
|
||||
0f
|
||||
})
|
||||
}
|
||||
val finalCep: Long = ((base + overallPopulationBonus + competitionBonus) * timeMultiplier * Config.app.game.cepRate).toLong
|
||||
//reward participant(s)
|
||||
// targets
|
||||
// .filter { player =>
|
||||
// player.Faction == faction && !player.Name.equals(hacker.Name)
|
||||
// }
|
||||
// .foreach { player =>
|
||||
// events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, finalCep))
|
||||
// }
|
||||
// events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
|
||||
// flagCarrier match {
|
||||
// case Some(player) => events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(player.CharId, finalCep / 2))
|
||||
// case None => ;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue