diff --git a/server/src/main/resources/db/migration/V011__ScoringPatch2.sql b/server/src/main/resources/db/migration/V011__ScoringPatch2.sql
index 7a303720f..a78cf7088 100644
--- a/server/src/main/resources/db/migration/V011__ScoringPatch2.sql
+++ b/server/src/main/resources/db/migration/V011__ScoringPatch2.sql
@@ -19,4 +19,77 @@ BEGIN
END;
RETURN NEW;
END;
-$$ LANGUAGE plpgsql;
\ No newline at end of file
+$$ 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();
diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf
index de91cf966..df36b5d88 100644
--- a/src/main/resources/application.conf
+++ b/src/main/resources/application.conf
@@ -273,16 +273,14 @@ game {
#
# name - label by which this event is organized
# base - whole number value
- # shots-multiplier - whether use count matters for this event
- # - when set to 0.0 (default), it does not
- # shots-limit - upper limit of use count
- # - cap the count here, if higher
+ # shots-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
- # - the *-cutoff should probably apply before *-limit, maybe
- # shots-nat-log - when set, may the use count to a natural logarithmic curve
- # - actually the exponent on the use count before the logarithm
- # - similar to shots-limit, but the curve plateaus quickly
+ # 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 = [
@@ -290,26 +288,26 @@ game {
name = "support-heal"
base = 10
shots-multiplier = 5.0
- shots-limit = 100
+ shots-max = 100
amount-multiplier = 2.0
}
{
name = "support-repair"
base = 10
shots-multiplier = 5.0
- shots-limit = 100
+ shots-max = 100
}
{
name = "support-repair-terminal"
base = 10
shots-multiplier = 5.0
- shots-limit = 100
+ shots-max = 100
}
{
name = "support-repair-turret"
base = 10
shots-multiplier = 5.0
- shots-limit = 100
+ shots-max = 100
}
{
name = "mounted-kill"
@@ -332,27 +330,23 @@ game {
name = "ams-resupply"
base = 15
shots-multiplier = 1.0
- shots-nat-log = 5.0
}
{
name = "lodestar-repair"
base = 10
shots-multiplier = 1.0
- shots-nat-log = 5.0
- shots-limit = 100
+ shots-max = 100
amount-multiplier = 1.0
}
{
name = "lodestar-rearm"
base = 10
shots-multiplier = 1.0
- shots-nat-log = 5.0
}
{
name = "revival"
base = 0
shots-multiplier = 15.0
- shots-nat-log = 5.0
shots-cutoff = 10
}
]
@@ -376,6 +370,7 @@ game {
# 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
@@ -399,6 +394,8 @@ game {
# How much direct combat contributes to paying back promotion debt.
# Typically, it does not contribute.
battle-experience-points-modifier = 0f
+ support-experience-points-modifier = 2f
+ capture-experience-points-modifier = 1f
# Don't forget to pay back that debt.
}
}
diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
index 4d4702951..9370c9819 100644
--- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala
+++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
@@ -67,6 +67,16 @@ import net.psforever.util.Database._
import net.psforever.util.{Config, Database, DefinitionUtil}
object AvatarActor {
+ private val basicLoginCertifications: Set[Certification] = Set(
+ Certification.StandardExoSuit,
+ Certification.AgileExoSuit,
+ Certification.ReinforcedExoSuit,
+ Certification.StandardAssault,
+ Certification.MediumAssault,
+ Certification.ATV,
+ Certification.Harasser
+ )
+
def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] =
Behaviors
.supervise[Command] {
@@ -336,7 +346,7 @@ object AvatarActor {
case "Kit" =>
container.Slot(objectIndex).Equipment =
Kit(DefinitionUtil.idToDefinition(objectId).asInstanceOf[KitDefinition])
- case "Telepad" | "BoomerTrigger" => ;
+ case "Telepad" | "BoomerTrigger" => ()
//special types of equipment that are not actually loaded
case name =>
log.error(s"failing to add unknown equipment to a container - $name")
@@ -368,10 +378,10 @@ object AvatarActor {
cooldownDurations.get(DefinitionUtil.fromString(name)) match {
case Some(duration) if now.compareTo(cooldown.plusMillis(duration.toMillis.toInt)) == -1 =>
cooldowns.put(name, cooldown)
- case _ => ;
+ case _ => ()
}
} catch {
- case _: Exception => ;
+ case _: Exception => ()
}
case _ =>
log.warn(s"ignoring invalid cooldown string: '$value'")
@@ -534,9 +544,9 @@ object AvatarActor {
otherAvatar.headOption match {
case Some(a) =>
func(a.id, a.name, a.factionId)
- case _ => ;
+ case _ => ()
}
- case _ => ;
+ case _ => ()
}
}
None //satisfy the orElse
@@ -857,23 +867,30 @@ object AvatarActor {
case Success(debt) if debt.nonEmpty =>
out.completeWith(Future(debt.head.experience))
case _ =>
+ ctx.run(
+ query[persistence.Progressiondebt]
+ .filter(_.avatarId == lift(avatarId))
+ .update(_.experience -> lift(0L))
+ )
out.completeWith(Future(0L))
}
out.future
}
- def saveExperienceDebt(avatarId: Long, exp: Long): Future[Int] = {
+ def saveExperienceDebt(avatarId: Long, exp: Long, max: Long): Future[Int] = {
import ctx._
import scala.concurrent.ExecutionContext.Implicits.global
val out: Promise[Int] = Promise()
- val result = ctx.run(query[persistence.Progressiondebt].filter(_.avatarId == lift(avatarId)))
- result.onComplete {
- case Success(debt) if debt.nonEmpty =>
- ctx.run(
- query[persistence.Progressiondebt]
- .filter(_.avatarId == lift(avatarId))
- .update(_.experience -> lift(exp))
+ val result = ctx.run(
+ query[persistence.Progressiondebt]
+ .filter(_.avatarId == lift(avatarId))
+ .update(
+ _.experience -> lift(exp),
+ _.maxExperience -> lift(max)
)
+ )
+ result.onComplete {
+ case Success(debt) if debt.toInt > 0 =>
out.completeWith(Future(1))
case _ =>
out.completeWith(Future(0))
@@ -1127,15 +1144,7 @@ class AvatarActor(
val inits = for {
_ <- ctx.run(
liftQuery(
- List(
- persistence.Certification(Certification.StandardExoSuit.value, avatarId),
- persistence.Certification(Certification.AgileExoSuit.value, avatarId),
- persistence.Certification(Certification.ReinforcedExoSuit.value, avatarId),
- persistence.Certification(Certification.StandardAssault.value, avatarId),
- persistence.Certification(Certification.MediumAssault.value, avatarId),
- persistence.Certification(Certification.ATV.value, avatarId),
- persistence.Certification(Certification.Harasser.value, avatarId)
- )
+ basicLoginCertifications.map { cert => persistence.Certification(cert.value, avatarId) }.toList
).foreach(c => query[persistence.Certification].insertValue(c))
)
_ <- ctx.run(
@@ -1212,135 +1221,11 @@ class AvatarActor(
Behaviors.same
case LearnCertification(terminalGuid, certification) =>
- import ctx._
-
- if (avatar.certifications.contains(certification)) {
- sessionActor ! SessionActor.SendResponse(
- ItemTransactionResultMessage(terminalGuid, TransactionType.Learn, success = false)
- )
- } else {
- val replace = certification.replaces.intersect(avatar.certifications)
- Future
- .sequence(replace.map(cert => {
- ctx
- .run(
- query[persistence.Certification]
- .filter(_.avatarId == lift(avatar.id))
- .filter(_.id == lift(cert.value))
- .delete
- )
- .map(_ => cert)
- }))
- .onComplete {
- case Failure(exception) =>
- log.error(exception)("db failure")
- sessionActor ! SessionActor.SendResponse(
- ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = false)
- )
- case Success(_replace) =>
- _replace.foreach { cert =>
- sessionActor ! SessionActor.SendResponse(
- PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value)
- )
- }
- ctx
- .run(
- query[persistence.Certification]
- .insert(_.id -> lift(certification.value), _.avatarId -> lift(avatar.id))
- )
- .onComplete {
- case Failure(exception) =>
- log.error(exception)("db failure")
- sessionActor ! SessionActor.SendResponse(
- ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = false)
- )
-
- case Success(_) =>
- sessionActor ! SessionActor.SendResponse(
- PlanetsideAttributeMessage(session.get.player.GUID, 24, certification.value)
- )
- replaceAvatar(
- avatar.copy(certifications = avatar.certifications.diff(replace) + certification)
- )
- sessionActor ! SessionActor.SendResponse(
- ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
- )
- sessionActor ! SessionActor.CharSaved
- }
-
- }
- }
+ performCertificationAction(terminalGuid, certification, learnCertificationInTheFuture, TransactionType.Buy)
Behaviors.same
case SellCertification(terminalGuid, certification) =>
- import ctx._
-
- if (!avatar.certifications.contains(certification)) {
- sessionActor ! SessionActor.SendResponse(
- ItemTransactionResultMessage(terminalGuid, TransactionType.Learn, success = false)
- )
- } else {
- var requiredByCert: Set[Certification] = Set(certification)
- var removeThese: Set[Certification] = Set(certification)
- val allCerts: Set[Certification] = Certification.values.toSet
- do {
- removeThese = allCerts.filter { testingCert =>
- testingCert.requires.intersect(removeThese).nonEmpty
- }
- requiredByCert = requiredByCert ++ removeThese
- } while (removeThese.nonEmpty)
-
- Future
- .sequence(
- avatar.certifications
- .intersect(requiredByCert)
- .map(cert => {
- ctx
- .run(
- query[persistence.Certification]
- .filter(_.avatarId == lift(avatar.id))
- .filter(_.id == lift(cert.value))
- .delete
- )
- .map(_ => cert)
- })
- )
- .onComplete {
- case Failure(exception) =>
- log.error(exception)("db failure")
- sessionActor ! SessionActor.SendResponse(
- ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = false)
- )
- case Success(certs) =>
- val player = session.get.player
- replaceAvatar(avatar.copy(certifications = avatar.certifications.diff(certs)))
- certs.foreach { cert =>
- sessionActor ! SessionActor.SendResponse(
- PlanetsideAttributeMessage(player.GUID, 25, cert.value)
- )
- }
- sessionActor ! SessionActor.SendResponse(
- ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
- )
- sessionActor ! SessionActor.CharSaved
- //wearing invalid armor?
- if (
- if (certification == Certification.ReinforcedExoSuit) player.ExoSuit == ExoSuitType.Reinforced
- else if (certification == Certification.InfiltrationSuit) player.ExoSuit == ExoSuitType.Infiltration
- else if (player.ExoSuit == ExoSuitType.MAX) {
- lazy val subtype =
- InfantryLoadout.DetermineSubtypeA(ExoSuitType.MAX, player.Slot(slot = 0).Equipment)
- if (certification == Certification.UniMAX) true
- else if (certification == Certification.AAMAX) subtype == 1
- else if (certification == Certification.AIMAX) subtype == 2
- else if (certification == Certification.AVMAX) subtype == 3
- else false
- } else false
- ) {
- player.Actor ! PlayerControl.SetExoSuit(ExoSuitType.Standard, 0)
- }
- }
- }
+ performCertificationAction(terminalGuid, certification, sellCertificationInTheFuture, TransactionType.Sell)
Behaviors.same
case SetCertifications(certifications) =>
@@ -1394,84 +1279,19 @@ class AvatarActor(
implant.definition.implantType.value
)
)
- case _ => ;
+ case _ => ()
}
deinitializeImplants()
Behaviors.same
case LearnImplant(terminalGuid, definition) =>
// TODO there used to be a terminal check here, do we really need it?
- val index = avatar.implants.zipWithIndex.collectFirst {
- case (Some(implant), _index) if implant.definition.implantType == definition.implantType => _index
- case (None, _index) if _index < avatar.br.implantSlots => _index
- }
- index match {
- case Some(_index) =>
- import ctx._
- ctx
- .run(query[persistence.Implant].insert(_.name -> lift(definition.Name), _.avatarId -> lift(avatar.id)))
- .onComplete {
- case Success(_) =>
- replaceAvatar(avatar.copy(implants = avatar.implants.updated(_index, Some(Implant(definition)))))
- sessionActor ! SessionActor.SendResponse(
- AvatarImplantMessage(
- session.get.player.GUID,
- ImplantAction.Add,
- _index,
- definition.implantType.value
- )
- )
- sessionActor ! SessionActor.SendResponse(
- ItemTransactionResultMessage(terminalGuid, TransactionType.Learn, success = true)
- )
- context.self ! ResetImplants()
- sessionActor ! SessionActor.CharSaved
- case Failure(exception) => log.error(exception)("db failure")
- }
-
- case None =>
- log.warn("attempted to learn implant but could not find slot")
- sessionActor ! SessionActor.SendResponse(
- ItemTransactionResultMessage(terminalGuid, TransactionType.Learn, success = false)
- )
- }
+ buyImplantAction(terminalGuid, definition)
Behaviors.same
case SellImplant(terminalGuid, definition) =>
// TODO there used to be a terminal check here, do we really need it?
- val index = avatar.implants.zipWithIndex.collectFirst {
- case (Some(implant), _index) if implant.definition.implantType == definition.implantType => _index
- }
- index match {
- case Some(_index) =>
- import ctx._
- ctx
- .run(
- query[persistence.Implant]
- .filter(_.name == lift(definition.Name))
- .filter(_.avatarId == lift(avatar.id))
- .delete
- )
- .onComplete {
- case Success(_) =>
- replaceAvatar(avatar.copy(implants = avatar.implants.updated(_index, None)))
- sessionActor ! SessionActor.SendResponse(
- AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, _index, 0)
- )
- sessionActor ! SessionActor.SendResponse(
- ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
- )
- context.self ! ResetImplants()
- sessionActor ! SessionActor.CharSaved
- case Failure(exception) => log.error(exception)("db failure")
- }
-
- case None =>
- log.warn("attempted to sell implant but could not find slot")
- sessionActor ! SessionActor.SendResponse(
- ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = false)
- )
- }
+ sellImplantAction(terminalGuid, definition)
Behaviors.same
case SaveLoadout(player, loadoutType, label, number) =>
@@ -1597,7 +1417,7 @@ class AvatarActor(
case _ => true
}
)
- case _ => ;
+ case _ => ()
}
}
if (updateTheTimes) {
@@ -1790,36 +1610,27 @@ class AvatarActor(
Behaviors.same
case AwardBep(bep, ExperienceType.Support) =>
- val gain = bep - experienceDebt
- if (gain > 0L) {
- awardSupportExperience(gain, previousDelay = 0L)
- } else {
- experienceDebt = experienceDebt - bep
- }
+ awardProgressionOrExperience(
+ setSupportAction,
+ bep,
+ Config.app.game.promotion.supportExperiencePointsModifier
+ )
Behaviors.same
case AwardBep(bep, modifier) =>
- val mod = Config.app.game.promotion.battleExperiencePointsModifier
- if (experienceDebt == 0L) {
- setBep(avatar.bep + bep, modifier)
- } else if (mod > 0f) {
- val modifiedBep = (bep.toFloat * Config.app.game.promotion.battleExperiencePointsModifier).toLong
- val gain = modifiedBep - experienceDebt
- if (gain > 0L) {
- setBep(avatar.bep + gain, modifier)
- } else {
- experienceDebt = experienceDebt - modifiedBep
- }
- }
+ awardProgressionOrExperience(
+ setBepAction(modifier),
+ avatar.bep + bep,
+ Config.app.game.promotion.battleExperiencePointsModifier
+ )
Behaviors.same
case AwardFacilityCaptureBep(bep) =>
- val gain = bep - experienceDebt
- if (gain > 0L) {
- setBep(gain, ExperienceType.Normal)
- } else {
- experienceDebt = experienceDebt - bep
- }
+ awardProgressionOrExperience(
+ setBepAction(ExperienceType.Normal),
+ avatar.bep + bep,
+ Config.app.game.promotion.captureExperiencePointsModifier
+ )
Behaviors.same
case SupportExperienceDeposit(bep, delayBy) =>
@@ -1837,25 +1648,36 @@ class AvatarActor(
val newBr = BattleRank.withExperience(bep).value
if (Config.app.game.promotion.active && oldBr == 1 && newBr > 1 && newBr < Config.app.game.promotion.maxBattleRank + 1) {
experienceDebt = bep
- if (avatar.cep > 0) {
- setCep(0L)
- }
+ AvatarActor.saveExperienceDebt(avatar.id, bep, bep)
true
} else if (experienceDebt > 0 && newBr == 2) {
experienceDebt = 0
+ AvatarActor.saveExperienceDebt(avatar.id, exp = 0, bep)
true
} else {
false
}
}) {
setBep(bep, ExperienceType.Normal)
+ if (avatar.cep > 0) {
+ setCep(0L)
+ }
+ restoreBasicCerts()
+ removeAllImplants()
+ sessionActor ! SessionActor.CharSaved
sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_229, "@AckSuccessSetBattleRank"))
+ } else if (experienceDebt > 0) {
+ sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.CMT_QUIT, s"You already must earn back $experienceDebt."))
+ } else {
+ sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.CMT_QUIT, "You may not suffer this debt."))
}
Behaviors.same
case AwardCep(cep) =>
if (experienceDebt > 0L) {
setCep(avatar.cep + cep)
+ } else {
+ sessionActor ! SessionActor.SendResponse(ExperienceAddedMessage(0))
}
Behaviors.same
@@ -1973,7 +1795,7 @@ class AvatarActor(
case RemoveShortcut(slot) =>
import ctx._
avatar.shortcuts.lift(slot).flatten match {
- case None => ;
+ case None => ()
case Some(_) =>
ctx.run(
query[persistence.Shortcut]
@@ -1996,7 +1818,7 @@ class AvatarActor(
AvatarActor.saveAvatarData(avatar)
saveLockerFunc()
AvatarActor.updateToolDischargeFor(avatar)
- AvatarActor.saveExperienceDebt(avatar.id, experienceDebt)
+ AvatarActor.saveExperienceDebt(avatar.id, experienceDebt, avatar.bep)
AvatarActor.avatarNoLongerLoggedIn(account.get.id)
Behaviors.same
}
@@ -2072,7 +1894,7 @@ class AvatarActor(
// }
// }.foreach { c =>
// shortcutList.indexWhere { _.isEmpty } match {
-// case -1 => ;
+// case -1 => ()
// case index =>
// shortcutList.update(index, Some(AvatarShortcut(2, c.name)))
// }
@@ -2180,7 +2002,7 @@ class AvatarActor(
avatar.implants.zipWithIndex.foreach {
case (Some(_), slot) =>
sessionActor ! SessionActor.SendResponse(AvatarImplantMessage(guid, ImplantAction.OutOfStamina, slot, 0))
- case _ => ;
+ case _ => ()
}
}
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(guid, 2, totalStamina))
@@ -2223,7 +2045,7 @@ class AvatarActor(
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 1)
)
- case _ => ;
+ case _ => ()
}
}
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(player.GUID, 2, totalStamina))
@@ -2268,7 +2090,7 @@ class AvatarActor(
AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 0))
)
- case (None, _) => ;
+ case (None, _) => ()
}
}
@@ -2324,7 +2146,7 @@ class AvatarActor(
avatar.name,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(index + 6, 0))
)
- case _ => ;
+ case _ => ()
}
}
@@ -2388,7 +2210,7 @@ class AvatarActor(
tool.GUID = PlanetSideGUID(gen.getAndIncrement)
case Some(item: Equipment) =>
item.GUID = PlanetSideGUID(gen.getAndIncrement)
- case _ => ;
+ case _ => ()
}
)
player.GUID = PlanetSideGUID(gen.getAndIncrement)
@@ -2426,7 +2248,7 @@ class AvatarActor(
item.Invalidate()
case Some(item: Equipment) =>
item.Invalidate()
- case _ => ;
+ case _ => ()
}
)
player.Invalidate()
@@ -2696,7 +2518,7 @@ class AvatarActor(
subtype
)
)
- case _ => ;
+ case _ => ()
}
}
@@ -2884,7 +2706,7 @@ class AvatarActor(
session match {
case Some(sess) if sess.player != null =>
sess.player.avatar = copyAvatar
- case _ => ;
+ case _ => ()
}
}
@@ -2903,7 +2725,7 @@ class AvatarActor(
case MemberAction.RemoveFriend => getAvatarForFunc(name, formatForOtherFunc(memberActionRemoveFriend))
case MemberAction.AddIgnoredPlayer => getAvatarForFunc(name, memberActionAddIgnored)
case MemberAction.RemoveIgnoredPlayer => getAvatarForFunc(name, formatForOtherFunc(memberActionRemoveIgnored))
- case _ => ;
+ case _ => ()
}
}
}
@@ -2947,7 +2769,7 @@ class AvatarActor(
def memberActionAddFriend(charId: Long, name: String, faction: Int): Unit = {
val people = avatar.people
people.friend.find { _.name.equals(name) } match {
- case Some(_) => ;
+ case Some(_) => ()
case None =>
import ctx._
ctx.run(
@@ -2983,7 +2805,7 @@ class AvatarActor(
replaceAvatar(
avatar.copy(people = people.copy(friend = people.friend.filterNot { _.charId == charId }))
)
- case None => ;
+ case None => ()
}
ctx.run(
query[persistence.Friend]
@@ -3042,7 +2864,7 @@ class AvatarActor(
def memberActionAddIgnored(charId: Long, name: String, faction: Int): Unit = {
val people = avatar.people
people.ignored.find { _.name.equals(name) } match {
- case Some(_) => ;
+ case Some(_) => ()
case None =>
import ctx._
ctx.run(
@@ -3078,7 +2900,7 @@ class AvatarActor(
replaceAvatar(
avatar.copy(people = people.copy(ignored = people.ignored.filterNot { _.charId == charId }))
)
- case None => ;
+ case None => ()
}
ctx.run(
query[persistence.Ignored]
@@ -3163,12 +2985,7 @@ class AvatarActor(
}
def awardSupportExperience(bep: Long, previousDelay: Long): Unit = {
- setBep(avatar.bep + bep, ExperienceType.Support) //todo simplify support testing
-// supportExperiencePool = supportExperiencePool + bep
-// avatar.scorecard.rate(bep)
-// if (supportExperienceTimer.isCancelled) {
-// resetSupportExperienceTimer(previousBep = 0, previousDelay = 0)
-// }
+ setBep(avatar.bep + bep, ExperienceType.Support)
}
def actuallyAwardSupportExperience(bep: Long, delayBy: Long): Unit = {
@@ -3183,6 +3000,34 @@ class AvatarActor(
}
}
+ private def awardProgressionOrExperience(
+ awardAction: Long => Unit,
+ experience: Long,
+ modifier: Float
+ ): Unit = {
+ if (experienceDebt == 0L) {
+ awardAction(experience)
+ } else if (modifier > 0f) {
+ val modifiedBep = (experience.toFloat * modifier).toLong
+ val gain = modifiedBep - experienceDebt
+ if (gain > 0L) {
+ experienceDebt = 0L
+ awardAction(experience)
+ } else {
+ experienceDebt = experienceDebt - modifiedBep
+ sessionActor ! SessionActor.SendResponse(ExperienceAddedMessage())
+ }
+ }
+ }
+
+ private def setBepAction(modifier: ExperienceType)(value: Long): Unit = {
+ setBep(value, modifier)
+ }
+
+ private def setSupportAction(value: Long): Unit = {
+ awardSupportExperience(value, previousDelay = 0L)
+ }
+
def updateKills(killStat: Kill): Unit = {
val exp = killStat.experienceEarned
val (modifiedExp, msg) = updateExperienceAndType(killStat.experienceEarned)
@@ -3431,6 +3276,245 @@ class AvatarActor(
output.future
}
+ def performCertificationAction(
+ terminalGuid: PlanetSideGUID,
+ certification: Certification,
+ action: Certification => Future[Boolean],
+ transaction: TransactionType.Value
+ ): Unit = {
+ action(certification).onComplete {
+ case Success(true) =>
+ sessionActor ! SessionActor.SendResponse(
+ ItemTransactionResultMessage(terminalGuid, transaction, success = true)
+ )
+ sessionActor ! SessionActor.CharSaved
+ case _ =>
+ sessionActor ! SessionActor.SendResponse(
+ ItemTransactionResultMessage(terminalGuid, transaction, success = false)
+ )
+ }
+ }
+
+ def learnCertificationInTheFuture(certification: Certification): Future[Boolean] = {
+ import ctx._
+ val out: Promise[Boolean] = Promise()
+ val replace = certification.replaces.intersect(avatar.certifications)
+ Future
+ .sequence(replace.map(cert => {
+ ctx
+ .run(
+ query[persistence.Certification]
+ .filter(_.avatarId == lift(avatar.id))
+ .filter(_.id == lift(cert.value))
+ .delete
+ )
+ .map(_ => cert)
+ }))
+ .onComplete {
+ case Failure(exception) =>
+ log.error(exception)("db failure")
+ case Success(_replace) =>
+ _replace.foreach { cert =>
+ sessionActor ! SessionActor.SendResponse(
+ PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value)
+ )
+ }
+ ctx
+ .run(
+ query[persistence.Certification]
+ .insert(_.id -> lift(certification.value), _.avatarId -> lift(avatar.id))
+ )
+ .onComplete {
+ case Failure(exception) =>
+ log.error(exception)("db failure")
+ out.completeWith(Future(false))
+ case Success(_) =>
+ sessionActor ! SessionActor.SendResponse(
+ PlanetsideAttributeMessage(session.get.player.GUID, 24, certification.value)
+ )
+ replaceAvatar(
+ avatar.copy(certifications = avatar.certifications.diff(replace) + certification)
+ )
+ out.completeWith(Future(true))
+ }
+ }
+ out.future
+ }
+
+ def sellCertificationInTheFuture(certification: Certification): Future[Boolean] = {
+ import ctx._
+ val out: Promise[Boolean] = Promise()
+ var requiredByCert: Set[Certification] = Set(certification)
+ var removeThese: Set[Certification] = Set(certification)
+ val allCerts: Set[Certification] = Certification.values.toSet
+ do {
+ removeThese = allCerts.filter { testingCert =>
+ testingCert.requires.intersect(removeThese).nonEmpty
+ }
+ requiredByCert = requiredByCert ++ removeThese
+ } while (removeThese.nonEmpty)
+
+ Future
+ .sequence(
+ avatar.certifications
+ .intersect(requiredByCert)
+ .map(cert => {
+ ctx
+ .run(
+ query[persistence.Certification]
+ .filter(_.avatarId == lift(avatar.id))
+ .filter(_.id == lift(cert.value))
+ .delete
+ )
+ .map(_ => cert)
+ })
+ )
+ .onComplete {
+ case Failure(exception) =>
+ log.error(exception)("db failure")
+ out.complete(Success(false))
+ case Success(certs) if certs.isEmpty =>
+ out.complete(Success(false))
+ case Success(certs) =>
+ val player = session.get.player
+ replaceAvatar(avatar.copy(certifications = avatar.certifications.diff(certs)))
+ certs.foreach { cert =>
+ sessionActor ! SessionActor.SendResponse(
+ PlanetsideAttributeMessage(player.GUID, 25, cert.value)
+ )
+ }
+ //wearing invalid armor?
+ if (
+ if (certification == Certification.ReinforcedExoSuit) player.ExoSuit == ExoSuitType.Reinforced
+ else if (certification == Certification.InfiltrationSuit) player.ExoSuit == ExoSuitType.Infiltration
+ else if (player.ExoSuit == ExoSuitType.MAX) {
+ lazy val subtype =
+ InfantryLoadout.DetermineSubtypeA(ExoSuitType.MAX, player.Slot(slot = 0).Equipment)
+ if (certification == Certification.UniMAX) true
+ else if (certification == Certification.AAMAX) subtype == 1
+ else if (certification == Certification.AIMAX) subtype == 2
+ else if (certification == Certification.AVMAX) subtype == 3
+ else false
+ } else false
+ ) {
+ player.Actor ! PlayerControl.SetExoSuit(ExoSuitType.Standard, 0)
+ }
+ out.complete(Success(true))
+ }
+ out.future
+ }
+
+ def restoreBasicCerts(): Unit = {
+ val certs = avatar.certifications
+ certs.diff(AvatarActor.basicLoginCertifications).foreach { sellCertificationInTheFuture }
+ AvatarActor.basicLoginCertifications.diff(certs).foreach { learnCertificationInTheFuture }
+ }
+
+ def buyImplantAction(
+ terminalGuid: PlanetSideGUID,
+ definition: ImplantDefinition
+ ): Unit = {
+ buyImplantInTheFuture(definition).onComplete {
+ case Success(true) =>
+ sessionActor ! SessionActor.SendResponse(
+ ItemTransactionResultMessage(terminalGuid, TransactionType.Buy, success = true)
+ )
+ resetAnImplant(definition.implantType)
+ sessionActor ! SessionActor.CharSaved
+ case _ =>
+ sessionActor ! SessionActor.SendResponse(
+ ItemTransactionResultMessage(terminalGuid, TransactionType.Buy, success = false)
+ )
+ }
+ }
+
+ def buyImplantInTheFuture(definition: ImplantDefinition): Future[Boolean] = {
+ val out: Promise[Boolean] = Promise()
+ avatar.implants.zipWithIndex.collectFirst {
+ case (Some(implant), _) if implant.definition.implantType == definition.implantType => None
+ case (None, index) if index < avatar.br.implantSlots => Some(index)
+ }.flatten match {
+ case Some(index) =>
+ import ctx._
+ ctx
+ .run(query[persistence.Implant].insert(_.name -> lift(definition.Name), _.avatarId -> lift(avatar.id)))
+ .onComplete {
+ case Success(_) =>
+ replaceAvatar(avatar.copy(implants = avatar.implants.updated(index, Some(Implant(definition)))))
+ sessionActor ! SessionActor.SendResponse(
+ AvatarImplantMessage(
+ session.get.player.GUID,
+ ImplantAction.Add,
+ index,
+ definition.implantType.value
+ )
+ )
+ out.completeWith(Future(true))
+ case Failure(exception) =>
+ log.error(exception)("db failure")
+ out.completeWith(Future(false))
+ }
+ case None =>
+ log.warn("attempted to learn implant but could not find slot")
+ out.completeWith(Future(false))
+ }
+ out.future
+ }
+
+ def sellImplantAction(
+ terminalGuid: PlanetSideGUID,
+ definition: ImplantDefinition
+ ): Unit = {
+ sellImplantInTheFuture(definition).onComplete {
+ case Success(true) =>
+ sessionActor ! SessionActor.SendResponse(
+ ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
+ )
+ sessionActor ! SessionActor.CharSaved
+ case _ =>
+ sessionActor ! SessionActor.SendResponse(
+ ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = false)
+ )
+ }
+ }
+
+ def sellImplantInTheFuture(definition: ImplantDefinition): Future[Boolean] = {
+ val out: Promise[Boolean] = Promise()
+ avatar.implants.zipWithIndex.collectFirst {
+ case (Some(implant), index) if implant.definition.implantType == definition.implantType => index
+ } match {
+ case Some(index) =>
+ import ctx._
+ ctx
+ .run(
+ query[persistence.Implant]
+ .filter(_.name == lift(definition.Name))
+ .filter(_.avatarId == lift(avatar.id))
+ .delete
+ )
+ .onComplete {
+ case Success(_) =>
+ replaceAvatar(avatar.copy(implants = avatar.implants.updated(index, None)))
+ sessionActor ! SessionActor.SendResponse(
+ AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, index, 0)
+ )
+ out.completeWith(Future(true))
+ case Failure(exception) =>
+ log.error(exception)("db failure")
+ out.completeWith(Future(false))
+ }
+ case None =>
+ log.warn("attempted to sell implant but could not find slot")
+ out.completeWith(Future(false))
+ }
+ out.future
+ }
+
+ def removeAllImplants(): Unit = {
+ avatar.implants.collect { case Some(imp) => imp.definition }.foreach { sellImplantInTheFuture }
+ context.self ! ResetImplants()
+ }
+
def resetSupportExperienceTimer(previousBep: Long, previousDelay: Long): Unit = {
val bep: Long = if (supportExperiencePool < 10L) {
supportExperiencePool
diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala
index d97b512c3..44f82f5c4 100644
--- a/src/main/scala/net/psforever/actors/session/ChatActor.scala
+++ b/src/main/scala/net/psforever/actors/session/ChatActor.scala
@@ -732,10 +732,18 @@ class ChatActor(
}
case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed =>
- setBattleRank(message, contents, session, AvatarActor.SetBep)
+ if (!setBattleRank(contents, session, AvatarActor.SetBep)) {
+ sessionActor ! SessionActor.SendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
+ )
+ }
case (CMT_SETCOMMANDRANK, _, contents) if gmCommandAllowed =>
- setCommandRank(message, contents, session)
+ if (!setCommandRank(contents, session)) {
+ sessionActor ! SessionActor.SendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
+ )
+ }
case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
contents.toIntOption match {
@@ -1124,7 +1132,7 @@ class ChatActor(
true
} else if (contents.startsWith("!list")) {
- val zone = contents.split(" ").lift(1) match {
+ val zone = dropFirstWord(contents).split(" ").headOption match {
case None =>
Some(session.zone)
case Some(id) =>
@@ -1170,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).toLowerCase.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)
@@ -1202,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).toLowerCase.split("\\s+")
+ cluster ! InterstellarClusterService.CavernRotation(buffer.headOption match {
case Some("-list") | Some("-l") =>
CavernRotationService.ReportRotationOrder(sessionActor.toClassic)
case _ =>
@@ -1224,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" =>
@@ -1275,9 +1283,10 @@ class ChatActor(
}
} else if (contents.startsWith("!progress")) {
if (!session.account.gm && BattleRank.withExperience(session.avatar.bep).value < Config.app.game.promotion.maxBattleRank + 1) {
- setBattleRank(message, contents, session, AvatarActor.Progress)
+ setBattleRank(dropFirstWord(contents), session, AvatarActor.Progress)
true
} else {
+ setBattleRank(contents="1", session, AvatarActor.Progress)
false
}
} else {
@@ -1288,12 +1297,19 @@ class ChatActor(
}
}
+ private def dropFirstWord(str: String): String = {
+ val noExtraSpaces = str.replaceAll("\\s+", " ").toLowerCase.trim
+ noExtraSpaces.indexOf({ char: String => char.equals(" ") }) match {
+ case -1 => ""
+ case beforeFirstBlank => noExtraSpaces.drop(beforeFirstBlank + 1)
+ }
+ }
+
def setBattleRank(
- message: ChatMsg,
contents: String,
session: Session,
msgFunc: Long => AvatarActor.Command
- ): Unit = {
+ ): 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 =>
@@ -1301,6 +1317,8 @@ class ChatActor(
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)
@@ -1314,14 +1332,16 @@ class ChatActor(
(target, rank) match {
case (_, Some(rank)) if rank.value <= Config.app.game.maxBattleRank =>
avatarActor ! msgFunc(rank.experience)
+ true
case _ =>
- sessionActor ! SessionActor.SendResponse(
- message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
- )
+ false
}
}
- def setCommandRank(message: ChatMsg, contents: String, session: Session): Unit = {
+ 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 =>
@@ -1342,11 +1362,9 @@ class ChatActor(
(target, rank) match {
case (_, Some(rank)) =>
avatarActor ! AvatarActor.SetCep(rank.experience)
- //sessionActor ! SessionActor.SendResponse(message.copy(contents = "@AckSuccessSetCommandRank"))
+ true
case _ =>
- sessionActor ! SessionActor.SendResponse(
- message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
- )
+ false
}
}
}
diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
index 9da4cc00c..d0a4f4083 100644
--- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
@@ -81,7 +81,7 @@ object ZoningOperations {
"If you consider yourself as a veteran soldier, despite looking so green, please read this.\n" ++
"You only have this opportunity while you are battle rank 1." ++
"\n\n" ++
- "The normal method of rank advancement comes from progress on the battlefield - fighting enemies, helping allies, and capturing facilities. " ++
+ "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. " ++
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
index c64c31156..72c527e52 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
@@ -204,7 +204,7 @@ object FacilityHackParticipation {
if (dataSum != 0) {
math.max(0.15f, math.min(2f, dataSum / dataCount.toFloat))
} else {
- 1f //can't do anything; multiplier should not affect values
+ 1f
}
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala
index 8d2d1e099..f8b07ed64 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala
@@ -47,14 +47,15 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
import scala.concurrent.Promise
import scala.util.Success
val requestLayers: Promise[ZoneHotSpotProjector.ExposedHeat] = Promise[ZoneHotSpotProjector.ExposedHeat]()
- val request = updateHotSpotInfoOnly()
- requestLayers.completeWith(request)
- request.onComplete {
- case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) =>
- hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis() - 900000L)
- case _ =>
- requestLayers.completeWith(Future(ZoneHotSpotProjector.ExposedHeat(Vector3.Zero, 0, Nil)))
- }
+// 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, Nil)))
requestLayers.future
}
@@ -140,7 +141,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
*/
val finalMap = mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]]()
.addAll(
- hotSpotLayersOverTime.take(1).flatMap { entry =>
+ hotSpotLayersOverTime.flatMap { entry =>
entry.map { f => (f.DisplayLocation, Map.empty[PlanetSideEmpire.Value, Seq[Long]]) }
}
)
diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
index 0ac83a35c..a4fd65a77 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
@@ -215,18 +215,14 @@ object Support {
.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 = {
- val partialShots = math.min(event.shotsLimit, shots).toFloat
- shotsMultiplier * (if (event.shotsNatLog > 0f) {
- math.log(math.pow(partialShots, event.shotsNatLog) + 2d).toFloat
- } else {
- partialShots
- })
- }
- val modifiedAmountReward: Float = event.amountMultiplier * weaponStat.amount.toFloat
- event.base + modifiedShotsReward + modifiedAmountReward
+ 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
}
diff --git a/src/main/scala/net/psforever/packet/game/ExperienceAddedMessage.scala b/src/main/scala/net/psforever/packet/game/ExperienceAddedMessage.scala
index 2117e39b3..d13a14e73 100644
--- a/src/main/scala/net/psforever/packet/game/ExperienceAddedMessage.scala
+++ b/src/main/scala/net/psforever/packet/game/ExperienceAddedMessage.scala
@@ -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:
* `"You have been awarded x experience points."`
* ... 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]
}
diff --git a/src/main/scala/net/psforever/persistence/Progressiondebt.scala b/src/main/scala/net/psforever/persistence/Progressiondebt.scala
index e81aebc4d..8a4c94ac4 100644
--- a/src/main/scala/net/psforever/persistence/Progressiondebt.scala
+++ b/src/main/scala/net/psforever/persistence/Progressiondebt.scala
@@ -1,7 +1,12 @@
// Copyright (c) 2023 PSForever
package net.psforever.persistence
+import org.joda.time.LocalDateTime
+
case class Progressiondebt(
avatarId:Long,
- experience: Long
+ experience: Long,
+ maxExperience: Long = -1,
+ enrollTime: LocalDateTime = LocalDateTime.now(),
+ clearTime: LocalDateTime = LocalDateTime.now()
)
diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala
index 89150b514..7ddcd0f47 100644
--- a/src/main/scala/net/psforever/util/Config.scala
+++ b/src/main/scala/net/psforever/util/Config.scala
@@ -272,25 +272,26 @@ case class SupportExperiencePoints(
case class SupportExperienceEvent(
name: String,
base: Long,
- shotsMultiplier: Float = 0f,
- shotsNatLog: Double = 0f,
- shotsLimit: Int = 50,
+ shotsMax: Int = 50,
shotsCutoff: Int = 50,
+ shotsMultiplier: Float = 0f,
amountMultiplier: Float = 0f
)
case class CommandExperiencePoints(
- rate: Float,
- lluCarrierModifier: Float,
- lluSlayerCreditDuration: Duration,
- lluSlayerCredit: Long,
- maximumPerSquadSize: Seq[Int],
- squadSizeLimitOverflow: Int,
- squadSizeLimitOverflowMultiplier: Float
+ rate: Float,
+ lluCarrierModifier: Float,
+ lluSlayerCreditDuration: Duration,
+ lluSlayerCredit: Long,
+ maximumPerSquadSize: Seq[Int],
+ squadSizeLimitOverflow: Int,
+ squadSizeLimitOverflowMultiplier: Float
)
case class PromotionSystem(
active: Boolean,
maxBattleRank: Int,
- battleExperiencePointsModifier: Float
+ battleExperiencePointsModifier: Float,
+ supportExperiencePointsModifier: Float,
+ captureExperiencePointsModifier: Float
)