mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-19 18:14:44 +00:00
commit
6bf58a7738
|
|
@ -0,0 +1,95 @@
|
|||
/* 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;
|
||||
|
||||
/* New */
|
||||
ALTER TABLE "progressiondebt"
|
||||
ADD COLUMN IF NOT EXISTS "max_experience" INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS "enroll_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN IF NOT EXISTS "clear_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
/*
|
||||
Upon indoctrinating a player into the progression system,
|
||||
update the peak experience for the battle rank for future reference
|
||||
and record when the player asked for this enhanced rank promotion.
|
||||
*/
|
||||
CREATE OR REPLACE FUNCTION fn_progressiondebt_updateEnrollment()
|
||||
RETURNS TRIGGER
|
||||
AS
|
||||
$$
|
||||
DECLARE avatarId Int;
|
||||
DECLARE oldExp Int;
|
||||
DECLARE newExp Int;
|
||||
BEGIN
|
||||
avatarId := NEW.avatar_id;
|
||||
newExp := NEW.experience;
|
||||
oldExp := OLD.experience;
|
||||
BEGIN
|
||||
IF (oldExp = 0 AND newExp > 0) THEN
|
||||
UPDATE progressiondebt
|
||||
SET experience = newExp, max_experience = newExp, enroll_time = CURRENT_TIMESTAMP
|
||||
WHERE avatar_id = avatarId;
|
||||
END IF;
|
||||
END;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER psf_progressiondebt_updateEnrollment
|
||||
BEFORE UPDATE
|
||||
ON progressiondebt
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE fn_progressiondebt_updateEnrollment();
|
||||
|
||||
/*
|
||||
Upon unlisting a player from the progression system,
|
||||
update the time when the player has completed his tensure.
|
||||
*/
|
||||
CREATE OR REPLACE FUNCTION fn_progressiondebt_updateClearTime()
|
||||
RETURNS TRIGGER
|
||||
AS
|
||||
$$
|
||||
DECLARE avatarId Int;
|
||||
DECLARE oldExp Int;
|
||||
DECLARE newExp Int;
|
||||
DECLARE newMaxExp Int;
|
||||
BEGIN
|
||||
avatarId := NEW.avatar_id;
|
||||
newExp := NEW.experience;
|
||||
oldExp := OLD.experience;
|
||||
newMaxExp := NEW.max_experience;
|
||||
BEGIN
|
||||
IF (oldExp > newExp AND newExp = 0) THEN
|
||||
UPDATE progressiondebt
|
||||
SET experience = 0, max_experience = newMaxExp, clear_time = CURRENT_TIMESTAMP
|
||||
WHERE avatar_id = avatarId;
|
||||
END IF;
|
||||
END;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER psf_progressiondebt_updateClearTime
|
||||
BEFORE UPDATE
|
||||
ON progressiondebt
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE fn_progressiondebt_updateClearTime();
|
||||
|
|
@ -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,192 @@ 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-min - lower limit of use count
|
||||
# - minimum amount of shots required before applying multiplier
|
||||
# shots-max - 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
|
||||
# shots-multiplier - whether use count matters for this event
|
||||
# - when set to 0.0 (default), it does not
|
||||
# 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-max = 100
|
||||
amount-multiplier = 2.0
|
||||
}
|
||||
{
|
||||
name = "support-repair"
|
||||
base = 10
|
||||
shots-multiplier = 5.0
|
||||
shots-max = 100
|
||||
}
|
||||
{
|
||||
name = "support-repair-terminal"
|
||||
base = 10
|
||||
shots-multiplier = 5.0
|
||||
shots-max = 100
|
||||
}
|
||||
{
|
||||
name = "support-repair-turret"
|
||||
base = 10
|
||||
shots-multiplier = 5.0
|
||||
shots-max = 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
|
||||
}
|
||||
{
|
||||
name = "lodestar-repair"
|
||||
base = 10
|
||||
shots-multiplier = 1.0
|
||||
shots-max = 100
|
||||
amount-multiplier = 1.0
|
||||
}
|
||||
{
|
||||
name = "lodestar-rearm"
|
||||
base = 10
|
||||
shots-multiplier = 1.0
|
||||
}
|
||||
{
|
||||
name = "revival"
|
||||
base = 0
|
||||
shots-multiplier = 15.0
|
||||
shots-cutoff = 10
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Command experience points
|
||||
cep = {
|
||||
# After all calculations are complete, multiply 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 = 30 seconds
|
||||
# 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
|
||||
# This is that 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 the reset battle rank is still possible.
|
||||
active = true
|
||||
# This battle rank and any battle ranks of ordinal decrement that allow opt-in to the progression system.
|
||||
broadcast-battle-rank = 1
|
||||
# This is the minimum battle rank that can be set as part of the promotion system.
|
||||
# Used to escape debt and return to normal play.
|
||||
reset-battle-rank = 5
|
||||
# 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
|
||||
support-experience-points-modifier = 3f
|
||||
capture-experience-points-modifier = 1f
|
||||
# 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 =>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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,59 +732,17 @@ 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")
|
||||
)
|
||||
if (!setBattleRank(contents, session, AvatarActor.SetBep)) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
|
||||
)
|
||||
}
|
||||
|
||||
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")
|
||||
)
|
||||
if (!setCommandRank(contents, session)) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
|
||||
)
|
||||
}
|
||||
|
||||
case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
|
||||
|
|
@ -1031,20 +962,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 +1117,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
|
||||
|
|
@ -1201,8 +1132,8 @@ class ChatActor(
|
|||
true
|
||||
|
||||
} else if (contents.startsWith("!list")) {
|
||||
val zone = contents.split(" ").lift(1) match {
|
||||
case None =>
|
||||
val zone = dropFirstWord(contents).split("\\s+").headOption match {
|
||||
case Some("") | None =>
|
||||
Some(session.zone)
|
||||
case Some(id) =>
|
||||
Zones.zones.find(_.id == id)
|
||||
|
|
@ -1247,8 +1178,8 @@ class ChatActor(
|
|||
true
|
||||
|
||||
} else if (contents.startsWith("!ntu") && gmCommandAllowed) {
|
||||
val buffer = contents.toLowerCase.split("\\s+")
|
||||
val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match {
|
||||
val buffer = dropFirstWord(contents).split("\\s+")
|
||||
val (facility, customNtuValue) = (buffer.headOption, buffer.lift(1)) match {
|
||||
case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
|
||||
case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
|
||||
case _ => (None, None)
|
||||
|
|
@ -1279,8 +1210,8 @@ class ChatActor(
|
|||
true
|
||||
|
||||
} else if (contents.startsWith("!zonerotate") && gmCommandAllowed) {
|
||||
val buffer = contents.toLowerCase.split("\\s+")
|
||||
cluster ! InterstellarClusterService.CavernRotation(buffer.lift(1) match {
|
||||
val buffer = dropFirstWord(contents).split("\\s+")
|
||||
cluster ! InterstellarClusterService.CavernRotation(buffer.headOption match {
|
||||
case Some("-list") | Some("-l") =>
|
||||
CavernRotationService.ReportRotationOrder(sessionActor.toClassic)
|
||||
case _ =>
|
||||
|
|
@ -1301,8 +1232,8 @@ class ChatActor(
|
|||
|
||||
} else if (contents.startsWith("!macro")) {
|
||||
val avatar = session.avatar
|
||||
val args = contents.split(" ").filter(_ != "")
|
||||
(args.lift(1), args.lift(2)) match {
|
||||
val args = dropFirstWord(contents).split(" ").filter(_ != "")
|
||||
(args.headOption, args.lift(1)) match {
|
||||
case (Some(cmd), other) =>
|
||||
cmd.toLowerCase() match {
|
||||
case "medkit" =>
|
||||
|
|
@ -1350,6 +1281,17 @@ class ChatActor(
|
|||
case _ =>
|
||||
false
|
||||
}
|
||||
} else if (contents.startsWith("!progress")) {
|
||||
val ourRank = BattleRank.withExperience(session.avatar.bep).value
|
||||
if (!session.account.gm &&
|
||||
(ourRank <= Config.app.game.promotion.broadcastBattleRank ||
|
||||
ourRank > Config.app.game.promotion.resetBattleRank && ourRank < Config.app.game.promotion.maxBattleRank + 1)) {
|
||||
setBattleRank(dropFirstWord(contents), session, AvatarActor.Progress)
|
||||
true
|
||||
} else {
|
||||
setBattleRank(contents="1", session, AvatarActor.Progress)
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false // unknown ! commands are ignored
|
||||
}
|
||||
|
|
@ -1357,4 +1299,75 @@ class ChatActor(
|
|||
false // unknown ! commands are ignored
|
||||
}
|
||||
}
|
||||
|
||||
private def dropFirstWord(str: String): String = {
|
||||
val noExtraSpaces = str.replaceAll("\\s+", " ").toLowerCase.trim
|
||||
noExtraSpaces.indexOf(" ") match {
|
||||
case -1 => ""
|
||||
case beforeFirstBlank => noExtraSpaces.drop(beforeFirstBlank + 1)
|
||||
}
|
||||
}
|
||||
|
||||
def setBattleRank(
|
||||
contents: String,
|
||||
session: Session,
|
||||
msgFunc: Long => AvatarActor.Command
|
||||
): Boolean = {
|
||||
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("-h"), _) | (Some("-help"), _) =>
|
||||
(None, Some(BattleRank.BR1))
|
||||
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)
|
||||
true
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def setCommandRank(
|
||||
contents: String,
|
||||
session: Session
|
||||
): Boolean = {
|
||||
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)
|
||||
true
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ object SessionActor {
|
|||
|
||||
final case class UpdateIgnoredPlayers(msg: FriendsResponse) extends Command
|
||||
|
||||
final case class AvatarLoadingSync(step: Int) extends Command
|
||||
|
||||
final case object CharSaved extends Command
|
||||
|
||||
private[session] case object CharSavedMsg extends Command
|
||||
|
|
@ -255,6 +257,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
case SessionActor.SetConnectionState(state) =>
|
||||
sessionFuncs.connectionState = state
|
||||
|
||||
case SessionActor.AvatarLoadingSync(state) =>
|
||||
sessionFuncs.zoning.spawn.handleAvatarLoadingSync(state)
|
||||
|
||||
/* uncommon messages (utility, or once in a while) */
|
||||
case SessionActor.AvatarAwardMessageBundle(pkts, delay) =>
|
||||
sessionFuncs.zoning.spawn.performAvatarAwardMessageDelivery(pkts, delay)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import akka.actor.typed.scaladsl.adapter._
|
|||
import akka.actor.{ActorContext, typed}
|
||||
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
|
||||
|
|
@ -224,10 +225,7 @@ class SessionAvatarHandlers(
|
|||
|
||||
case AvatarResponse.DestroyDisplay(killer, victim, method, unk)
|
||||
if killer.CharId == avatar.id && killer.Faction != victim.Faction =>
|
||||
// TODO Temporary thing that should go somewhere else and use proper xp values
|
||||
sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk))
|
||||
avatarActor ! AvatarActor.AwardBep((1000 * Config.app.game.bepRate).toLong, ExperienceType.Normal)
|
||||
avatarActor ! AvatarActor.AwardCep((100 * Config.app.game.cepRate).toLong)
|
||||
|
||||
case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
|
||||
// guid = victim // killer = killer
|
||||
|
|
@ -396,6 +394,24 @@ class SessionAvatarHandlers(
|
|||
sessionData.kitToBeUsed = None
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_225, msg))
|
||||
|
||||
case AvatarResponse.UpdateKillsDeathsAssists(_, kda) =>
|
||||
avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
|
||||
|
||||
case AvatarResponse.AwardBep(charId, bep, expType) =>
|
||||
//if the target player, always award (some) BEP
|
||||
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) =>
|
||||
facilityCaptureRewards(buildingId, zoneNumber, cep)
|
||||
|
||||
case AvatarResponse.SendResponse(msg) =>
|
||||
sendResponse(msg)
|
||||
|
||||
|
|
@ -410,16 +426,16 @@ 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.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) {
|
||||
|
|
@ -432,6 +448,7 @@ class SessionAvatarHandlers(
|
|||
sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
|
||||
|
||||
//player state changes
|
||||
AvatarActor.updateToolDischargeFor(avatar)
|
||||
player.FreeHand.Equipment.foreach { item =>
|
||||
DropEquipmentFromInventory(player)(item)
|
||||
}
|
||||
|
|
@ -446,7 +463,6 @@ class SessionAvatarHandlers(
|
|||
}
|
||||
sessionData.playerActionsToCancel()
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sessionData.zoning
|
||||
AvatarActor.savePlayerLocation(player)
|
||||
sessionData.zoning.spawn.shiftPosition = Some(player.Position)
|
||||
|
||||
|
|
@ -590,6 +606,71 @@ class SessionAvatarHandlers(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
private def facilityCaptureRewards(buildingId: Int, zoneNumber: Int, cep: Long): Unit = {
|
||||
//TODO squad services deactivated, participation trophy rewards for now - 11-20-2023
|
||||
//must be in a squad to earn experience
|
||||
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 =>
|
||||
val cepConfig = Config.app.game.experience.cep
|
||||
//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 * (math.random().toFloat % 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SessionAvatarHandlers {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -1379,7 +1379,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")
|
||||
|
|
@ -1582,6 +1582,24 @@ class SessionData(
|
|||
case Some(llu: CaptureFlag) => Some((llu, llu.Carrier))
|
||||
case _ => None
|
||||
}) match {
|
||||
case Some((llu, Some(carrier: Player)))
|
||||
if carrier.GUID == player.GUID && !player.isAlive =>
|
||||
player.LastDamage.foreach { damage =>
|
||||
damage
|
||||
.interaction
|
||||
.adversarial
|
||||
.map { _.attacker }
|
||||
.collect {
|
||||
case attacker
|
||||
if attacker.Faction != player.Faction &&
|
||||
System.currentTimeMillis() - llu.LastCollectionTime >= Config.app.game.experience.cep.lluSlayerCreditDuration.toMillis =>
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
attacker.Name,
|
||||
AvatarAction.AwardCep(attacker.CharId, Config.app.game.experience.cep.lluSlayerCredit)
|
||||
)
|
||||
}
|
||||
}
|
||||
continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
|
||||
case Some((llu, Some(carrier: Player))) if carrier.GUID == player.GUID =>
|
||||
continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
|
||||
case Some((_, Some(carrier: Player))) =>
|
||||
|
|
@ -2461,9 +2479,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
|
||||
) {
|
||||
|
|
@ -2476,6 +2494,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
|
||||
|
|
@ -162,7 +165,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))
|
||||
|
|
@ -248,7 +251,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)
|
||||
|
||||
|
|
@ -278,7 +281,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()
|
||||
|
|
@ -297,7 +300,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 {
|
||||
|
|
@ -334,8 +337,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.objects.vital.{InGameHistory, IncarnationActivity, 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" ++
|
||||
s"You only have this opportunity while you are less than or equal to battle rank ${Config.app.game.promotion.broadcastBattleRank}." ++
|
||||
"\n\n" ++
|
||||
"The normal method of rank advancement comes from 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. " ++
|
||||
s"If you accept, but it becomes too much of burden, you may ask to revert to battle rank ${Config.app.game.promotion.resetBattleRank} 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))
|
||||
})
|
||||
|
|
@ -602,22 +629,7 @@ class ZoningOperations(
|
|||
ServiceManager.serviceManager ! Lookup("propertyOverrideManager")
|
||||
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
|
||||
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
|
||||
(
|
||||
FriendsResponse.packetSequence(
|
||||
MemberAction.InitializeFriendList,
|
||||
avatar.people.friend
|
||||
.map { f =>
|
||||
game.Friend(f.name, AvatarActor.onlineIfNotIgnoredEitherWay(avatar, f.name))
|
||||
}
|
||||
) ++
|
||||
//ignored list (no one ever online)
|
||||
FriendsResponse.packetSequence(
|
||||
MemberAction.InitializeIgnoreList,
|
||||
avatar.people.ignored.map { f => game.Friend(f.name) }
|
||||
)
|
||||
).foreach {
|
||||
sendResponse
|
||||
}
|
||||
spawn.initializeFriendsAndIgnoredLists()
|
||||
//the following subscriptions last until character switch/logout
|
||||
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
|
||||
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots, etc.
|
||||
|
|
@ -1657,6 +1669,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 +1817,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 +2357,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 +2630,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 +2646,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)
|
||||
}
|
||||
}
|
||||
|
|
@ -2765,16 +2782,7 @@ class ZoningOperations(
|
|||
}
|
||||
|
||||
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
|
||||
avatar.shortcuts
|
||||
.zipWithIndex
|
||||
.collect { case (Some(shortcut), index) =>
|
||||
sendResponse(CreateShortcutMessage(
|
||||
guid,
|
||||
index + 1,
|
||||
Some(AvatarShortcut.convert(shortcut))
|
||||
))
|
||||
}
|
||||
sendResponse(ChangeShortcutBankMessage(guid, 0))
|
||||
initializeShortcutsAndBank(guid)
|
||||
//Favorites lists
|
||||
avatarActor ! AvatarActor.InitialRefreshLoadouts()
|
||||
|
||||
|
|
@ -2803,10 +2811,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 +2828,7 @@ class ZoningOperations(
|
|||
continent.DeployableList
|
||||
.filter(_.OwnerName.contains(name))
|
||||
.foreach(obj => {
|
||||
obj.Owner = guid
|
||||
obj.OwnerGuid = guid
|
||||
drawDeloyableIcon(obj)
|
||||
})
|
||||
drawDeloyableIcon = DontRedrawIcons
|
||||
|
|
@ -2831,7 +2836,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,23 +2911,35 @@ 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)) }
|
||||
.flatten
|
||||
player.LogActivity({
|
||||
if (player.History.headOption.exists { _.isInstanceOf[SpawningActivity] }) {
|
||||
ReconstructionActivity(PlayerSource(player), continent.Number, effortBy)
|
||||
} else {
|
||||
SpawningActivity(PlayerSource(player), continent.Number, effortBy)
|
||||
}
|
||||
})
|
||||
//ride
|
||||
|
||||
val lastEntryOpt = player.History.lastOption
|
||||
if (lastEntryOpt.exists { !_.isInstanceOf[IncarnationActivity] }) {
|
||||
player.LogActivity({
|
||||
lastEntryOpt match {
|
||||
case Some(_) =>
|
||||
ReconstructionActivity(PlayerSource(player), continent.Number, effortBy)
|
||||
case None =>
|
||||
SpawningActivity(PlayerSource(player), continent.Number, effortBy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
upstreamMessageCount = 0
|
||||
setAvatar = true
|
||||
if (
|
||||
!account.gm && /* gm's are excluded */
|
||||
Config.app.game.promotion.active && /* play versus progress system must be active */
|
||||
BattleRank.withExperience(tplayer.avatar.bep).value <= Config.app.game.promotion.broadcastBattleRank && /* must be below a certain battle rank */
|
||||
avatar.scorecard.Lives.isEmpty && /* first life after login */
|
||||
avatar.scorecard.CurrentLife.prior.isEmpty && /* no revives */
|
||||
player.History.size == 1 /* did nothing but come into existence */
|
||||
) {
|
||||
ZoningOperations.reportProgressionSystem(context.self)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2935,6 +2952,59 @@ class ZoningOperations(
|
|||
HandleSetCurrentAvatar(tplayer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to feedback of how the avatar's data is being handled
|
||||
* in a way that properly reflects the state of the server at the moment.
|
||||
* @param state indicator for the progress of the avatar
|
||||
*/
|
||||
def handleAvatarLoadingSync(state: Int): Unit = {
|
||||
if (state == 2 && zoneLoaded.contains(true)) {
|
||||
initializeFriendsAndIgnoredLists()
|
||||
initializeShortcutsAndBank(player.GUID)
|
||||
avatarActor ! AvatarActor.RefreshPurchaseTimes()
|
||||
loginAvatarStatisticsFields()
|
||||
avatarActor ! AvatarActor.InitialRefreshLoadouts()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up and dispatch a list of `FriendsResponse` packets related to both formal friends and ignored players.
|
||||
*/
|
||||
def initializeFriendsAndIgnoredLists(): Unit = {
|
||||
(
|
||||
FriendsResponse.packetSequence(
|
||||
MemberAction.InitializeFriendList,
|
||||
avatar.people.friend
|
||||
.map { f =>
|
||||
game.Friend(f.name, AvatarActor.onlineIfNotIgnoredEitherWay(avatar, f.name))
|
||||
}
|
||||
) ++
|
||||
//ignored list (no one ever online)
|
||||
FriendsResponse.packetSequence(
|
||||
MemberAction.InitializeIgnoreList,
|
||||
avatar.people.ignored.map { f => game.Friend(f.name) }
|
||||
)
|
||||
).foreach {
|
||||
sendResponse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up and dispatch a list of `CreateShortcutMessage` packets and a single `ChangeShortcutBankMessage` packet.
|
||||
*/
|
||||
def initializeShortcutsAndBank(guid: PlanetSideGUID): Unit = {
|
||||
avatar.shortcuts
|
||||
.zipWithIndex
|
||||
.collect { case (Some(shortcut), index) =>
|
||||
sendResponse(CreateShortcutMessage(
|
||||
guid,
|
||||
index + 1,
|
||||
Some(AvatarShortcut.convert(shortcut))
|
||||
))
|
||||
}
|
||||
sendResponse(ChangeShortcutBankMessage(guid, 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the icon for this deployable object.<br>
|
||||
* <br>
|
||||
|
|
@ -2961,7 +3031,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 +3234,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 is 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.
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import net.psforever.objects.equipment.Equipment
|
|||
import net.psforever.objects.serverobject.structures.{StructureType, WarpGate}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
|
||||
import net.psforever.objects.{ConstructionItem, Player, Vehicle}
|
||||
import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle}
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.actors.zone.building.MajorFacilityLogic
|
||||
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, SupportExperienceCalculator}
|
||||
import net.psforever.util.Database._
|
||||
import net.psforever.persistence
|
||||
|
||||
|
|
@ -70,6 +73,9 @@ object ZoneActor {
|
|||
// Once they do, we won't need this anymore
|
||||
final case class ZoneMapUpdate() extends Command
|
||||
|
||||
final case class RewardThisDeath(entity: PlanetSideGameObject with FactionAffinity with InGameHistory) extends Command
|
||||
|
||||
final case class RewardOurSupporters(target: SourceEntry, history: Iterable[InGameActivity], kill: Kill, bep: Long) extends Command
|
||||
}
|
||||
|
||||
class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
||||
|
|
@ -79,7 +85,9 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
|||
import ctx._
|
||||
|
||||
private[this] val log = org.log4s.getLogger
|
||||
val players: mutable.ListBuffer[Player] = mutable.ListBuffer()
|
||||
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)
|
||||
|
|
@ -143,13 +151,18 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
|||
case HotSpotActivity(defender, attacker, location) =>
|
||||
zone.Activity ! Zone.HotSpot.Activity(defender, attacker, location)
|
||||
|
||||
case RewardThisDeath(entity) =>
|
||||
experience ! ExperienceCalculator.RewardThisDeath(entity)
|
||||
|
||||
case RewardOurSupporters(target, history, kill, bep) =>
|
||||
supportExperience ! SupportExperienceCalculator.RewardOurSupporters(target, history, kill, bep)
|
||||
|
||||
case ZoneMapUpdate() =>
|
||||
zone.Buildings
|
||||
.filter(_._2.BuildingType == StructureType.Facility)
|
||||
.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 =>
|
||||
|
|
|
|||
|
|
@ -9918,6 +9918,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
|
||||
|
|
@ -9927,7 +9928,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package net.psforever.objects.avatar
|
|||
|
||||
import akka.actor.{Actor, ActorRef, Props, typed}
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.login.WorldSession.{DropEquipmentFromInventory, HoldNewEquipmentUp, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory}
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.ce.Deployable
|
||||
|
|
@ -32,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}
|
||||
|
|
@ -355,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)
|
||||
|
|
@ -365,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}")
|
||||
|
|
@ -507,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, _, _) =>
|
||||
|
|
@ -1052,6 +1062,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
|
|||
case _ =>
|
||||
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0))
|
||||
}
|
||||
zone.actor ! ZoneActor.RewardThisDeath(player)
|
||||
}
|
||||
|
||||
def suicide() : Unit = {
|
||||
|
|
|
|||
|
|
@ -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,11 +158,12 @@ 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)
|
||||
}
|
||||
statisticMap.put(objectId, outEntry)
|
||||
outEntry
|
||||
case _ =>
|
||||
val out = Statistic(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ class CaptureFlagConverter extends ObjectCreateConverter[CaptureFlag]() {
|
|||
override def ConstructorData(obj : CaptureFlag) : Try[CaptureFlagData] = {
|
||||
val hackInfo = obj.Owner.asInstanceOf[Building].CaptureTerminal.get.HackedBy match {
|
||||
case Some(hackInfo) => hackInfo
|
||||
case _ => Hackable.HackInfo(PlayerSource("", PlanetSideEmpire.NEUTRAL, Vector3.Zero), PlanetSideGUID(0), 0L, 0L)
|
||||
case _ => Hackable.HackInfo(PlayerSource("", PlanetSideEmpire.NEUTRAL, Vector3.Zero), PlanetSideGUID(0), 0L, 0L, obj.Faction)
|
||||
}
|
||||
|
||||
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, Faction))
|
||||
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, Faction))
|
||||
}
|
||||
case (_, None) =>
|
||||
hackedBy = None
|
||||
|
|
@ -73,14 +73,15 @@ trait Hackable {
|
|||
object Hackable {
|
||||
final case class HackInfo(
|
||||
player: PlayerSource,
|
||||
hackerGUID: PlanetSideGUID,
|
||||
hackStartTime: Long,
|
||||
hackDuration: Long
|
||||
hackerGUID: PlanetSideGUID,
|
||||
hackStartTime: Long,
|
||||
hackDuration: Long,
|
||||
originalFaction: PlanetSideEmpire.Value
|
||||
) {
|
||||
def hackerName: String = player.Name
|
||||
def hackerFaction: PlanetSideEmpire.Value = player.Faction
|
||||
def hackerPos: Vector3 = player.Position
|
||||
|
||||
def Duration(time: Long): HackInfo = HackInfo(player, hackerGUID, hackStartTime, time)
|
||||
def Duration(time: Long): HackInfo = HackInfo(player, hackerGUID, hackStartTime, time, originalFaction)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class IFFLockControl(lock: IFFLock)
|
|||
.orElse {
|
||||
case CommonMessages.Use(player, Some(item: SimpleItem))
|
||||
if item.Definition == GlobalDefinitions.remote_electronics_kit =>
|
||||
if (lock.Faction != player.Faction && lock.HackedBy.isEmpty) {
|
||||
if (lock.Faction != player.Faction) {
|
||||
sender() ! CommonMessages.Progress(
|
||||
GenericHackables.GetHackSpeed(player, lock),
|
||||
GenericHackables.FinishHacking(lock, player, 1114636288L),
|
||||
|
|
@ -42,8 +42,7 @@ class IFFLockControl(lock: IFFLock)
|
|||
} else {
|
||||
val log = org.log4s.getLogger
|
||||
log.warn(s"IFF lock is being hacked by ${player.Faction}, but don't know how to handle this state:")
|
||||
log.warn(s"Lock - Faction=${lock.Faction}, HackedBy=${lock.HackedBy}")
|
||||
log.warn(s"Player - Faction=${player.Faction}")
|
||||
log.warn(s"Lock - Faction=${lock.Faction}, HackedBy=${lock.HackedBy.map{_.player}}")
|
||||
}
|
||||
|
||||
case IFFLock.DoorOpenRequest(target, door, replyTo) =>
|
||||
|
|
|
|||
|
|
@ -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,23 @@ 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 *
|
||||
amount * resourceSilo.Definition.ChargeTime.toSeconds.toFloat / resourceSilo.MaxNtuCapacitor
|
||||
).toLong
|
||||
vehicle.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||
owner.name,
|
||||
AvatarAction.AwardBep(owner.charId, deposit, ExperienceType.Normal)
|
||||
)
|
||||
zones.exp.ToDatabase.reportNtuActivity(owner.charId, resourceSilo.Zone.Number, resourceSilo.Owner.GUID.guid, deposit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +211,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 {
|
||||
|
|
@ -185,9 +182,8 @@ class Building(
|
|||
val (hacking, hackingFaction, hackTime): (Boolean, PlanetSideEmpire.Value, Long) = CaptureTerminal match {
|
||||
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)
|
||||
case Some(Hackable.HackInfo(p, _, start, length, _)) =>
|
||||
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,276 @@
|
|||
// 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 PlayerContributionRaw: Map[Long, (Player, Int, Long)] = playerContribution.toMap
|
||||
|
||||
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 (dataCount != 0) {
|
||||
math.max(0.2f, math.min(2f, dataSum / dataCount.toFloat))
|
||||
} else {
|
||||
0.5f
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
ordering: Ordering[Int] = Ordering[Int]
|
||||
): Float = {
|
||||
val gradedPopulation = populationNumbers
|
||||
.map { gradingRule }
|
||||
.groupBy(x => x)
|
||||
.values
|
||||
.toSeq
|
||||
.sortBy(_.size)(ordering)
|
||||
.take(layers)
|
||||
.flatten
|
||||
gradedPopulation.sum / gradedPopulation.size.toFloat
|
||||
}
|
||||
|
||||
private[participation] def populationBalanceModifier(
|
||||
victorPopulationNumbers: Seq[Int],
|
||||
opposingPopulationNumbers: Seq[Int],
|
||||
healthyPercentage: Float,
|
||||
maxRatio: Float = 1f
|
||||
): 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
|
||||
math.max(0f, math.min(rate.sum / rate.size.toFloat, maxRatio))
|
||||
}
|
||||
|
||||
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,328 @@
|
|||
// 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, 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(building.Position.xy, building.Definition.SOIRadius, Nil)))
|
||||
// }
|
||||
requestLayers.completeWith(Future(ZoneHotSpotProjector.ExposedHeat(building.Position.xy, building.Definition.SOIRadius.toFloat, Nil)))
|
||||
requestLayers.future
|
||||
}
|
||||
|
||||
def RewardFacilityCapture(
|
||||
defenderFaction: PlanetSideEmpire.Value,
|
||||
attackingFaction: PlanetSideEmpire.Value,
|
||||
hacker: PlayerSource,
|
||||
hackTime: Long,
|
||||
completionTime: Long,
|
||||
isResecured: Boolean
|
||||
): Unit = {
|
||||
//has the facility ran out of nanites during the hack
|
||||
if (building.NtuLevel > 0) {
|
||||
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
|
||||
)
|
||||
val events = building.Zone.AvatarEvents
|
||||
val buildingId = building.GUID.guid
|
||||
val zoneNumber = building.Zone.Number
|
||||
val playersInSoi = building.PlayersInSOI.filter {
|
||||
_.Faction == victorFaction
|
||||
}
|
||||
if (baseExperienceFromFacilityCapture > 0) {
|
||||
//2) population modifier
|
||||
//The value of the first should grow as population grows.
|
||||
//This is an intentionally imperfect counterbalance to that growth.
|
||||
val populationModifier = FacilityHackParticipation.populationProgressModifier(
|
||||
opposingPopulationByLayer,
|
||||
{ pop =>
|
||||
if (pop > 75) 0.5f
|
||||
else if (pop > 59) 0.6f
|
||||
else if (pop > 29) 0.7f
|
||||
else if (pop > 19) 0.75f
|
||||
else 0.8f
|
||||
},
|
||||
4
|
||||
)
|
||||
//3) competition multiplier
|
||||
val competitionMultiplier: Float = {
|
||||
val populationBalanceModifier: Float = FacilityHackParticipation.populationBalanceModifier(
|
||||
victorPopulationByLayer,
|
||||
opposingPopulationByLayer,
|
||||
healthyPercentage = 1.5f,
|
||||
maxRatio = 2.0f
|
||||
)
|
||||
//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.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 unit
|
||||
// will incur an abbreviated duration
|
||||
val overallTimeMultiplier: Float = {
|
||||
if (hasFlag) {
|
||||
if (completionTime >= hackTime) { //hack timed out without llu delivery
|
||||
0.5f
|
||||
} 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.65f)
|
||||
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
|
||||
(a, 0.25f + (d.toFloat / 1800000f))
|
||||
case (a, (_, _, _)) =>
|
||||
(a, 0.25f)
|
||||
}
|
||||
//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
|
||||
//terminal hacker (always cep)
|
||||
if (playersInSoi.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) {
|
||||
ToDatabase.reportFacilityCapture(
|
||||
hackerId,
|
||||
zoneNumber,
|
||||
buildingId,
|
||||
finalCep,
|
||||
expType = "cep"
|
||||
)
|
||||
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep))
|
||||
}
|
||||
//bystanders (cep if squad leader, bep otherwise)
|
||||
playersInSoi
|
||||
.filterNot { _.CharId == hackerId }
|
||||
.foreach { player =>
|
||||
val charId = player.CharId
|
||||
val contributionMultiplier = contributionPerPlayerByTime.getOrElse(charId, 1f)
|
||||
val outputValue = (finalCep * contributionMultiplier).toLong
|
||||
events ! AvatarServiceMessage(player.Name, AvatarAction.FacilityCaptureRewards(buildingId, zoneNumber, outputValue))
|
||||
}
|
||||
//flag carrier (won't be in soi, but earns cep from capture)
|
||||
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,
|
||||
zoneNumber,
|
||||
buildingId,
|
||||
finalModifiedCep,
|
||||
expType = "llu"
|
||||
)
|
||||
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(charId, finalModifiedCep))
|
||||
}
|
||||
} else {
|
||||
//no need to calculate a fancy score
|
||||
val hackerId = hacker.CharId
|
||||
val hackerScore = List((hackerId, 0L, "cep"))
|
||||
ToDatabase.reportFacilityCaptureInBulk(
|
||||
if (isResecured) {
|
||||
hackerScore
|
||||
} else {
|
||||
val flagCarrierScore = flagCarrier.map (p => List((p.CharId, 0L, "llu"))).getOrElse(Nil)
|
||||
if (playersInSoi.exists(_.CharId == hackerId) && !flagCarrierScore.exists { case (charId, _,_) => charId == hackerId }) {
|
||||
hackerScore ++ flagCarrierScore
|
||||
} else {
|
||||
flagCarrierScore
|
||||
}
|
||||
} ++ playersInSoi.filterNot { p => p.CharId == hackerId }.map(p => (p.CharId, 0L, "bep")),
|
||||
zoneNumber,
|
||||
buildingId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,23 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.serverobject.structures.participation
|
||||
|
||||
import net.psforever.objects.Player
|
||||
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 PlayerContributionRaw: Map[Long, (Player, Int, Long)] = Map.empty[Long, (Player, Int, Long)]
|
||||
|
||||
override def PlayerContribution(timeDelay: Long): Map[Long, Float] = Map.empty[Long, Float]
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.objects.serverobject.structures.participation
|
||||
|
||||
import net.psforever.objects.Player
|
||||
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 PlayerContributionRaw: Map[Long, (Player, Int, Long)]
|
||||
|
||||
def PlayerContribution(timeDelay: Long = 600): Map[Long, Float]
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
// 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.objects.zones.exp.ToDatabase
|
||||
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) {
|
||||
//early setup ...
|
||||
import scala.concurrent.duration._
|
||||
val curr = System.currentTimeMillis()
|
||||
val soiPlayers = building.PlayersInSOI.filter { _.Faction == victorFaction }
|
||||
val contributionOpposingSize = contributionOpposing.size
|
||||
val events = building.Zone.AvatarEvents
|
||||
val buildingId = building.GUID.guid
|
||||
val zoneNumber = building.Zone.Number
|
||||
val hackerId = hacker.CharId
|
||||
//1) experience from killing opposingFaction
|
||||
//Because the hack duration of towers is instantaneous, the prior period of five minutes is artificially selected.
|
||||
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
|
||||
FacilityHackParticipation.allocateKillsByPlayers(
|
||||
building.Position,
|
||||
building.Definition.SOIRadius.toFloat,
|
||||
curr - 5.minutes.toMillis,
|
||||
curr,
|
||||
opposingFaction,
|
||||
contributionVictor
|
||||
),
|
||||
contributionOpposingSize
|
||||
)
|
||||
//based on this math, the optimal number of enemy for experience gain is 20
|
||||
//max value of: 1000 * pop * max(0, (40 - pop)) * 0.1
|
||||
if (baseExperienceFromFacilityCapture > 0) {
|
||||
//more setup ...
|
||||
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))
|
||||
}
|
||||
//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 > 40) 0.075f
|
||||
else if (pop > 8) (40 - pop).toFloat * 0.1f
|
||||
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.75f)
|
||||
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
|
||||
(a, 0.15f + (d.toFloat / 600000f))
|
||||
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.25f, 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
|
||||
//terminal hacker (always cep)
|
||||
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
|
||||
ToDatabase.reportFacilityCapture(
|
||||
hackerId,
|
||||
zoneNumber,
|
||||
buildingId,
|
||||
finalCep,
|
||||
expType = "cep"
|
||||
)
|
||||
//bystanders (cep if squad leader, bep otherwise)
|
||||
soiPlayers
|
||||
.filterNot(_.CharId == hackerId)
|
||||
.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.FacilityCaptureRewards(buildingId, zoneNumber, outputValue)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
//no need to calculate a fancy score
|
||||
ToDatabase.reportFacilityCaptureInBulk(
|
||||
(hackerId, 0L, "cep") +: soiPlayers.filterNot(_.CharId == hackerId).map(p => (p.CharId, 0L, "bep")),
|
||||
zoneNumber,
|
||||
buildingId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class CaptureTerminal(private val idef: CaptureTerminalDefinition) extends Ameni
|
|||
|
||||
override def toString: String = {
|
||||
val guid = if (HasGUID) {
|
||||
s" ${Continent}-${GUID.guid}"
|
||||
s" $Continent-${GUID.guid}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,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
|
||||
|
||||
|
|
@ -132,6 +133,7 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
|
|||
ActivatePanelsForChargingEvent(ChargeTransferObject)
|
||||
}
|
||||
|
||||
//noinspection ScalaUnusedSymbol
|
||||
def WithdrawAndTransmit(vehicle: Vehicle, maxRequested: Float): Any = {
|
||||
val chargeable = ChargeTransferObject
|
||||
var chargeToDeposit = Math.min(Math.min(chargeable.NtuCapacitor, 100), maxRequested)
|
||||
|
|
@ -186,8 +188,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.toSeconds.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, SpawningActivity}
|
||||
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity}
|
||||
import net.psforever.objects.vital.environment.EnvironmentReason
|
||||
import net.psforever.objects.vital.etc.SuicideReason
|
||||
import net.psforever.objects.zones._
|
||||
|
|
@ -143,9 +143,9 @@ class VehicleControl(vehicle: Vehicle)
|
|||
}) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num))
|
||||
|
||||
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) })
|
||||
|
|
@ -252,9 +252,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 {
|
||||
|
|
@ -301,7 +301,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
|
||||
}) &&
|
||||
|
|
@ -312,6 +312,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
|
||||
|
|
@ -340,7 +342,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) {
|
||||
|
|
@ -355,6 +357,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) {
|
||||
|
|
@ -363,7 +366,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(
|
||||
|
|
@ -433,7 +436,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(
|
||||
|
|
@ -905,7 +908,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,66 @@ 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
|
||||
trait IncarnationActivity extends GeneralActivity
|
||||
|
||||
final case class ShieldCharge(amount: Int, cause: Option[SourceEntry]) extends GeneralActivity
|
||||
final case class SpawningActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
|
||||
extends IncarnationActivity
|
||||
|
||||
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value) extends GeneralActivity
|
||||
final case class ReconstructionActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
|
||||
extends IncarnationActivity
|
||||
|
||||
final case class RevivingActivity(target: SourceEntry, user: PlayerSource, amount: Int, equipment: EquipmentDefinition)
|
||||
extends IncarnationActivity 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 +135,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 +145,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 +160,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
|
||||
|
|
@ -148,7 +217,7 @@ trait InGameHistory {
|
|||
|
||||
/**
|
||||
* An in-game event must be recorded.
|
||||
* Add new entry to the front of the list (for recent activity).
|
||||
* Add new entry to the list (for recent activity).
|
||||
* @param action the fully-informed entry
|
||||
* @return the list of previous changes to this entity
|
||||
*/
|
||||
|
|
@ -157,14 +226,37 @@ trait InGameHistory {
|
|||
|
||||
/**
|
||||
* An in-game event must be recorded.
|
||||
* Add new entry to the front of the list (for recent activity).
|
||||
* 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 = act +: history
|
||||
history = history :+ act
|
||||
case None => ()
|
||||
}
|
||||
history
|
||||
|
|
@ -188,7 +280,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 +301,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 +358,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 +373,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)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// 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.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
|
||||
import net.psforever.objects.vital.interaction.DamageResult
|
||||
import net.psforever.objects.zones.Zone
|
||||
|
||||
object ExperienceCalculator {
|
||||
def apply(zone: Zone): Behavior[Command] =
|
||||
Behaviors.supervise[Command] {
|
||||
Behaviors.setup(context => new ExperienceCalculator(context, zone))
|
||||
}.onFailure[Exception](SupervisorStrategy.restart)
|
||||
|
||||
sealed trait Command
|
||||
|
||||
final case class RewardThisDeath(victim: SourceEntry, lastDamage: Option[DamageResult], history: Iterable[InGameActivity])
|
||||
extends ExperienceCalculator.Command
|
||||
|
||||
object RewardThisDeath {
|
||||
def apply(obj: PlanetSideGameObject with FactionAffinity with InGameHistory): RewardThisDeath = {
|
||||
RewardThisDeath(SourceEntry(obj), obj.LastDamage, obj.History)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone)
|
||||
extends AbstractBehavior[ExperienceCalculator.Command](context) {
|
||||
|
||||
import ExperienceCalculator._
|
||||
|
||||
def onMessage(msg: Command): Behavior[Command] = {
|
||||
msg match {
|
||||
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
|
||||
KillAssists.rewardThisPlayerDeath(victim, lastDamage, history, zone.AvatarEvents)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
636
src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
Normal file
636
src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala
Normal file
|
|
@ -0,0 +1,636 @@
|
|||
// 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)
|
||||
lazy val base = Support.baseExperience(victim, history)
|
||||
if (killer.Faction == victim.Faction || killer.unique == victim.unique) {
|
||||
0L
|
||||
} else if (base > 1) {
|
||||
//include battle rank disparity modifier
|
||||
val battleRankDisparity: Long = {
|
||||
import net.psforever.objects.avatar.BattleRank
|
||||
val killerLevel = BattleRank.withExperience(killer.bep).value
|
||||
val victimLevel = BattleRank.withExperience(victim.bep).value
|
||||
val victimMinusKiller = victimLevel - killerLevel
|
||||
if (victimMinusKiller > -1) {
|
||||
victimMinusKiller * 10 + victimLevel
|
||||
} else {
|
||||
val bothLevels = killerLevel + victimLevel
|
||||
val pointFive = (base.toFloat * 0.25f).toInt
|
||||
-1 * (if (bothLevels >= base) {
|
||||
pointFive
|
||||
} else {
|
||||
math.min(bothLevels, pointFive)
|
||||
})
|
||||
}
|
||||
}.toLong
|
||||
//include menace modifier
|
||||
base + battleRankDisparity + (victim.progress.kills.size.toFloat * (1f + calculateMenace(victim).toFloat / 10f)).toLong
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/main/scala/net/psforever/objects/zones/exp/Stats.scala
Normal file
41
src/main/scala/net/psforever/objects/zones/exp/Stats.scala
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
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(
|
||||
equipment: EquipmentUseContextWrapper,
|
||||
amount: Int,
|
||||
shots: Int,
|
||||
time: Long,
|
||||
contributions: Float
|
||||
) extends ItemUseStats
|
||||
|
||||
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[EquipmentUseContextWrapper],
|
||||
percentage: Float
|
||||
)
|
||||
235
src/main/scala/net/psforever/objects/zones/exp/Support.scala
Normal file
235
src/main/scala/net/psforever/objects/zones/exp/Support.scala
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
// 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 shotsMax = event.shotsMax
|
||||
val shotsMultiplier = event.shotsMultiplier
|
||||
if (shotsMultiplier > 0f && shots < event.shotsCutoff) {
|
||||
val modifiedShotsReward: Float =
|
||||
shotsMultiplier * math.log(math.min(shotsMax, shots).toDouble + 2d).toFloat
|
||||
val modifiedAmountReward: Float =
|
||||
event.amountMultiplier * weaponStat.amount.toFloat
|
||||
event.base.toFloat + 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
|
||||
}
|
||||
}
|
||||
232
src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
Normal file
232
src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
// 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)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert multiple entries into the database's `buildingCapture` table as a single transaction.
|
||||
*/
|
||||
def reportFacilityCaptureInBulk(
|
||||
avatarIdAndExp: List[(Long, Long, String)],
|
||||
zoneId: Int,
|
||||
buildingId: Int
|
||||
): Unit = {
|
||||
ctx.run(quote { liftQuery(
|
||||
avatarIdAndExp.map { case (avatarId, exp, expType) =>
|
||||
persistence.Buildingcapture(-1, avatarId, zoneId, buildingId, exp, expType)
|
||||
}
|
||||
)}.foreach(e => query[persistence.Buildingcapture].insertValue(e)))
|
||||
}
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
||||
import scodec.Codec
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, Codec}
|
||||
import scodec.codecs._
|
||||
|
||||
/**
|
||||
|
|
@ -12,21 +13,33 @@ import scodec.codecs._
|
|||
* It merely generates the message:<br>
|
||||
* `"You have been awarded x experience points."`<br>
|
||||
* ... where `x` is the number of experience points that have been promised.
|
||||
* If the `Boolean` parameter is `true`, `x` will be equal to the number provided followed by the word "Command."
|
||||
* If the `Boolean` parameter is `false`, `x` will be represented as an obvious blank space character.
|
||||
* (Yes, it prints to the events chat like that.)
|
||||
* @param exp the number of (Command) experience points earned
|
||||
* @param unk defaults to `true` for effect;
|
||||
* if `false`, the number of experience points in the message will be blanked
|
||||
* @param cmd if `true`, the message will be tailored for "Command" experience;
|
||||
* if `false`, the number of experience points and the "Command" flair will be blanked
|
||||
*/
|
||||
final case class ExperienceAddedMessage(exp: Int, unk: Boolean = true) extends PlanetSideGamePacket {
|
||||
final case class ExperienceAddedMessage(exp: Int, cmd: Boolean) extends PlanetSideGamePacket {
|
||||
type Packet = ExperienceAddedMessage
|
||||
def opcode = GamePacketOpcode.ExperienceAddedMessage
|
||||
def encode = ExperienceAddedMessage.encode(this)
|
||||
def opcode: GamePacketOpcode.Value = GamePacketOpcode.ExperienceAddedMessage
|
||||
def encode: Attempt[BitVector] = ExperienceAddedMessage.encode(this)
|
||||
}
|
||||
|
||||
object ExperienceAddedMessage extends Marshallable[ExperienceAddedMessage] {
|
||||
/**
|
||||
* Produce a packet whose message to the event chat is
|
||||
* "You have been awarded experience points."
|
||||
* @return `ExperienceAddedMessage` packet
|
||||
*/
|
||||
def apply(): ExperienceAddedMessage = ExperienceAddedMessage(0, cmd = false)
|
||||
|
||||
/**
|
||||
* Produce a packet whose message to the event chat is
|
||||
* "You have been awarded 'exp' Command experience points."
|
||||
* @param exp the number of Command experience points earned
|
||||
* @return `ExperienceAddedMessage` packet
|
||||
*/
|
||||
def apply(exp: Int): ExperienceAddedMessage = ExperienceAddedMessage(exp, cmd = true)
|
||||
|
||||
implicit val codec: Codec[ExperienceAddedMessage] = (
|
||||
("exp" | uintL(15)) :: ("unk" | bool)
|
||||
("exp" | uintL(bits = 15)) :: ("unk" | bool)
|
||||
).as[ExperienceAddedMessage]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,12 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.persistence
|
||||
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
case class Progressiondebt(
|
||||
avatarId:Long,
|
||||
experience: Long,
|
||||
maxExperience: Long = -1,
|
||||
enrollTime: LocalDateTime = LocalDateTime.now(),
|
||||
clearTime: LocalDateTime = LocalDateTime.now()
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -429,7 +429,43 @@ class AvatarService(zone: Zone) extends Actor {
|
|||
)
|
||||
)
|
||||
|
||||
case _ => ;
|
||||
case AvatarAction.UpdateKillsDeathsAssists(charId, stat) =>
|
||||
AvatarEvents.publish(
|
||||
AvatarServiceResponse(
|
||||
s"/$forChannel/Avatar",
|
||||
Service.defaultPlayerGUID,
|
||||
AvatarResponse.UpdateKillsDeathsAssists(charId, stat)
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.services.avatar
|
||||
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.avatar.scoring.KDAStat
|
||||
import net.psforever.objects.ballistics.Projectile
|
||||
import net.psforever.objects.ce.Deployable
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
|
|
@ -11,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
|
||||
|
||||
|
|
@ -154,6 +155,11 @@ object AvatarAction {
|
|||
final case class UseKit(kit_guid: PlanetSideGUID, kit_objid: Int) extends Action
|
||||
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
|
||||
// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.services.avatar
|
||||
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.avatar.scoring.KDAStat
|
||||
import net.psforever.objects.ballistics.Projectile
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.objects.inventory.InventoryItem
|
||||
|
|
@ -10,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(
|
||||
|
|
@ -124,4 +125,9 @@ object AvatarResponse {
|
|||
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
|
||||
final case class UseKit(kit_guid: PlanetSideGUID, kit_objid: Int) extends Response
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue