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

This commit is contained in:
Fate-JH 2023-10-30 23:53:43 -04:00
parent e9dbd5f259
commit d3392ecab2
11 changed files with 535 additions and 347 deletions

View file

@ -19,4 +19,77 @@ BEGIN
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
$$ 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();

View file

@ -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.
}
}

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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. " ++

View file

@ -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
}
}

View file

@ -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]]) }
}
)

View file

@ -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
}

View file

@ -2,7 +2,8 @@
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.Codec
import scodec.bits.BitVector
import scodec.{Attempt, Codec}
import scodec.codecs._
/**
@ -12,21 +13,33 @@ import scodec.codecs._
* It merely generates the message:<br>
* `"You have been awarded x experience points."`<br>
* ... where `x` is the number of experience points that have been promised.
* If the `Boolean` parameter is `true`, `x` will be equal to the number provided followed by the word "Command."
* If the `Boolean` parameter is `false`, `x` will be represented as an obvious blank space character.
* (Yes, it prints to the events chat like that.)
* @param exp the number of (Command) experience points earned
* @param unk defaults to `true` for effect;
* if `false`, the number of experience points in the message will be blanked
* @param cmd if `true`, the message will be tailored for "Command" experience;
* if `false`, the number of experience points and the "Command" flair will be blanked
*/
final case class ExperienceAddedMessage(exp: Int, unk: Boolean = true) extends PlanetSideGamePacket {
final case class ExperienceAddedMessage(exp: Int, cmd: Boolean) extends PlanetSideGamePacket {
type Packet = ExperienceAddedMessage
def opcode = GamePacketOpcode.ExperienceAddedMessage
def encode = ExperienceAddedMessage.encode(this)
def opcode: GamePacketOpcode.Value = GamePacketOpcode.ExperienceAddedMessage
def encode: Attempt[BitVector] = ExperienceAddedMessage.encode(this)
}
object ExperienceAddedMessage extends Marshallable[ExperienceAddedMessage] {
/**
* Produce a packet whose message to the event chat is
* "You have been awarded experience points."
* @return `ExperienceAddedMessage` packet
*/
def apply(): ExperienceAddedMessage = ExperienceAddedMessage(0, cmd = false)
/**
* Produce a packet whose message to the event chat is
* "You have been awarded 'exp' Command experience points."
* @param exp the number of Command experience points earned
* @return `ExperienceAddedMessage` packet
*/
def apply(exp: Int): ExperienceAddedMessage = ExperienceAddedMessage(exp, cmd = true)
implicit val codec: Codec[ExperienceAddedMessage] = (
("exp" | uintL(15)) :: ("unk" | bool)
("exp" | uintL(bits = 15)) :: ("unk" | bool)
).as[ExperienceAddedMessage]
}

View file

@ -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()
)

View file

@ -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
)