From d3392ecab22a7999784f3bb5f8a1b9b4aa47bb9a Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 30 Oct 2023 23:53:43 -0400 Subject: [PATCH] QoL changes; event chat messages for exp when in debt; different calculations for sep; timestamps for progress system start and clear; hopefully proper cleanup for progress system --- .../db/migration/V011__ScoringPatch2.sql | 75 ++- src/main/resources/application.conf | 31 +- .../actors/session/AvatarActor.scala | 616 ++++++++++-------- .../psforever/actors/session/ChatActor.scala | 58 +- .../session/support/ZoningOperations.scala | 2 +- .../FacilityHackParticipation.scala | 2 +- .../MajorFacilityHackParticipation.scala | 19 +- .../psforever/objects/zones/exp/Support.scala | 16 +- .../packet/game/ExperienceAddedMessage.scala | 33 +- .../persistence/Progressiondebt.scala | 7 +- .../scala/net/psforever/util/Config.scala | 23 +- 11 files changed, 535 insertions(+), 347 deletions(-) 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 )