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; END;
RETURN NEW; RETURN NEW;
END; 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 # name - label by which this event is organized
# base - whole number value # base - whole number value
# shots-multiplier - whether use count matters for this event # shots-min - lower limit of use count
# - when set to 0.0 (default), it does not # - minimum amount of shots required before applying multiplier
# shots-limit - upper limit of use count # shots-max - upper limit of use count
# - cap the count here, if higher # - cap the count here, if higher
# shots-cutoff - if the use count exceeds this number, the event no longer applies # shots-cutoff - if the use count exceeds this number, the event no longer applies
# - a hard limit that should zero the contribution reward # - a hard limit that should zero the contribution reward
# - the *-cutoff should probably apply before *-limit, maybe # shots-multiplier - whether use count matters for this event
# shots-nat-log - when set, may the use count to a natural logarithmic curve # - when set to 0.0 (default), it does not
# - actually the exponent on the use count before the logarithm
# - similar to shots-limit, but the curve plateaus quickly
# amount-multiplier - whether active amount matters for this event # amount-multiplier - whether active amount matters for this event
# - when set to 0.0 (default), it does not # - when set to 0.0 (default), it does not
events = [ events = [
@ -290,26 +288,26 @@ game {
name = "support-heal" name = "support-heal"
base = 10 base = 10
shots-multiplier = 5.0 shots-multiplier = 5.0
shots-limit = 100 shots-max = 100
amount-multiplier = 2.0 amount-multiplier = 2.0
} }
{ {
name = "support-repair" name = "support-repair"
base = 10 base = 10
shots-multiplier = 5.0 shots-multiplier = 5.0
shots-limit = 100 shots-max = 100
} }
{ {
name = "support-repair-terminal" name = "support-repair-terminal"
base = 10 base = 10
shots-multiplier = 5.0 shots-multiplier = 5.0
shots-limit = 100 shots-max = 100
} }
{ {
name = "support-repair-turret" name = "support-repair-turret"
base = 10 base = 10
shots-multiplier = 5.0 shots-multiplier = 5.0
shots-limit = 100 shots-max = 100
} }
{ {
name = "mounted-kill" name = "mounted-kill"
@ -332,27 +330,23 @@ game {
name = "ams-resupply" name = "ams-resupply"
base = 15 base = 15
shots-multiplier = 1.0 shots-multiplier = 1.0
shots-nat-log = 5.0
} }
{ {
name = "lodestar-repair" name = "lodestar-repair"
base = 10 base = 10
shots-multiplier = 1.0 shots-multiplier = 1.0
shots-nat-log = 5.0 shots-max = 100
shots-limit = 100
amount-multiplier = 1.0 amount-multiplier = 1.0
} }
{ {
name = "lodestar-rearm" name = "lodestar-rearm"
base = 10 base = 10
shots-multiplier = 1.0 shots-multiplier = 1.0
shots-nat-log = 5.0
} }
{ {
name = "revival" name = "revival"
base = 0 base = 0
shots-multiplier = 15.0 shots-multiplier = 15.0
shots-nat-log = 5.0
shots-cutoff = 10 shots-cutoff = 10
} }
] ]
@ -376,6 +370,7 @@ game {
# The maximum command experience that can be earned in a facility capture based on squad size # 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] 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 # 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 # -1 reuses the cep before being capped
squad-size-limit-overflow = -1 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 # 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. # How much direct combat contributes to paying back promotion debt.
# Typically, it does not contribute. # Typically, it does not contribute.
battle-experience-points-modifier = 0f battle-experience-points-modifier = 0f
support-experience-points-modifier = 2f
capture-experience-points-modifier = 1f
# Don't forget to pay back that debt. # 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} import net.psforever.util.{Config, Database, DefinitionUtil}
object AvatarActor { 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] = def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] =
Behaviors Behaviors
.supervise[Command] { .supervise[Command] {
@ -336,7 +346,7 @@ object AvatarActor {
case "Kit" => case "Kit" =>
container.Slot(objectIndex).Equipment = container.Slot(objectIndex).Equipment =
Kit(DefinitionUtil.idToDefinition(objectId).asInstanceOf[KitDefinition]) Kit(DefinitionUtil.idToDefinition(objectId).asInstanceOf[KitDefinition])
case "Telepad" | "BoomerTrigger" => ; case "Telepad" | "BoomerTrigger" => ()
//special types of equipment that are not actually loaded //special types of equipment that are not actually loaded
case name => case name =>
log.error(s"failing to add unknown equipment to a container - $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 { cooldownDurations.get(DefinitionUtil.fromString(name)) match {
case Some(duration) if now.compareTo(cooldown.plusMillis(duration.toMillis.toInt)) == -1 => case Some(duration) if now.compareTo(cooldown.plusMillis(duration.toMillis.toInt)) == -1 =>
cooldowns.put(name, cooldown) cooldowns.put(name, cooldown)
case _ => ; case _ => ()
} }
} catch { } catch {
case _: Exception => ; case _: Exception => ()
} }
case _ => case _ =>
log.warn(s"ignoring invalid cooldown string: '$value'") log.warn(s"ignoring invalid cooldown string: '$value'")
@ -534,9 +544,9 @@ object AvatarActor {
otherAvatar.headOption match { otherAvatar.headOption match {
case Some(a) => case Some(a) =>
func(a.id, a.name, a.factionId) func(a.id, a.name, a.factionId)
case _ => ; case _ => ()
} }
case _ => ; case _ => ()
} }
} }
None //satisfy the orElse None //satisfy the orElse
@ -857,23 +867,30 @@ object AvatarActor {
case Success(debt) if debt.nonEmpty => case Success(debt) if debt.nonEmpty =>
out.completeWith(Future(debt.head.experience)) out.completeWith(Future(debt.head.experience))
case _ => case _ =>
ctx.run(
query[persistence.Progressiondebt]
.filter(_.avatarId == lift(avatarId))
.update(_.experience -> lift(0L))
)
out.completeWith(Future(0L)) out.completeWith(Future(0L))
} }
out.future out.future
} }
def saveExperienceDebt(avatarId: Long, exp: Long): Future[Int] = { def saveExperienceDebt(avatarId: Long, exp: Long, max: Long): Future[Int] = {
import ctx._ import ctx._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
val out: Promise[Int] = Promise() val out: Promise[Int] = Promise()
val result = ctx.run(query[persistence.Progressiondebt].filter(_.avatarId == lift(avatarId))) val result = ctx.run(
result.onComplete { query[persistence.Progressiondebt]
case Success(debt) if debt.nonEmpty => .filter(_.avatarId == lift(avatarId))
ctx.run( .update(
query[persistence.Progressiondebt] _.experience -> lift(exp),
.filter(_.avatarId == lift(avatarId)) _.maxExperience -> lift(max)
.update(_.experience -> lift(exp))
) )
)
result.onComplete {
case Success(debt) if debt.toInt > 0 =>
out.completeWith(Future(1)) out.completeWith(Future(1))
case _ => case _ =>
out.completeWith(Future(0)) out.completeWith(Future(0))
@ -1127,15 +1144,7 @@ class AvatarActor(
val inits = for { val inits = for {
_ <- ctx.run( _ <- ctx.run(
liftQuery( liftQuery(
List( basicLoginCertifications.map { cert => persistence.Certification(cert.value, avatarId) }.toList
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)
)
).foreach(c => query[persistence.Certification].insertValue(c)) ).foreach(c => query[persistence.Certification].insertValue(c))
) )
_ <- ctx.run( _ <- ctx.run(
@ -1212,135 +1221,11 @@ class AvatarActor(
Behaviors.same Behaviors.same
case LearnCertification(terminalGuid, certification) => case LearnCertification(terminalGuid, certification) =>
import ctx._ performCertificationAction(terminalGuid, certification, learnCertificationInTheFuture, TransactionType.Buy)
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
}
}
}
Behaviors.same Behaviors.same
case SellCertification(terminalGuid, certification) => case SellCertification(terminalGuid, certification) =>
import ctx._ performCertificationAction(terminalGuid, certification, sellCertificationInTheFuture, TransactionType.Sell)
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)
}
}
}
Behaviors.same Behaviors.same
case SetCertifications(certifications) => case SetCertifications(certifications) =>
@ -1394,84 +1279,19 @@ class AvatarActor(
implant.definition.implantType.value implant.definition.implantType.value
) )
) )
case _ => ; case _ => ()
} }
deinitializeImplants() deinitializeImplants()
Behaviors.same Behaviors.same
case LearnImplant(terminalGuid, definition) => case LearnImplant(terminalGuid, definition) =>
// TODO there used to be a terminal check here, do we really need it? // TODO there used to be a terminal check here, do we really need it?
val index = avatar.implants.zipWithIndex.collectFirst { buyImplantAction(terminalGuid, definition)
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)
)
}
Behaviors.same Behaviors.same
case SellImplant(terminalGuid, definition) => case SellImplant(terminalGuid, definition) =>
// TODO there used to be a terminal check here, do we really need it? // TODO there used to be a terminal check here, do we really need it?
val index = avatar.implants.zipWithIndex.collectFirst { sellImplantAction(terminalGuid, definition)
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)
)
}
Behaviors.same Behaviors.same
case SaveLoadout(player, loadoutType, label, number) => case SaveLoadout(player, loadoutType, label, number) =>
@ -1597,7 +1417,7 @@ class AvatarActor(
case _ => true case _ => true
} }
) )
case _ => ; case _ => ()
} }
} }
if (updateTheTimes) { if (updateTheTimes) {
@ -1790,36 +1610,27 @@ class AvatarActor(
Behaviors.same Behaviors.same
case AwardBep(bep, ExperienceType.Support) => case AwardBep(bep, ExperienceType.Support) =>
val gain = bep - experienceDebt awardProgressionOrExperience(
if (gain > 0L) { setSupportAction,
awardSupportExperience(gain, previousDelay = 0L) bep,
} else { Config.app.game.promotion.supportExperiencePointsModifier
experienceDebt = experienceDebt - bep )
}
Behaviors.same Behaviors.same
case AwardBep(bep, modifier) => case AwardBep(bep, modifier) =>
val mod = Config.app.game.promotion.battleExperiencePointsModifier awardProgressionOrExperience(
if (experienceDebt == 0L) { setBepAction(modifier),
setBep(avatar.bep + bep, modifier) avatar.bep + bep,
} else if (mod > 0f) { Config.app.game.promotion.battleExperiencePointsModifier
val modifiedBep = (bep.toFloat * Config.app.game.promotion.battleExperiencePointsModifier).toLong )
val gain = modifiedBep - experienceDebt
if (gain > 0L) {
setBep(avatar.bep + gain, modifier)
} else {
experienceDebt = experienceDebt - modifiedBep
}
}
Behaviors.same Behaviors.same
case AwardFacilityCaptureBep(bep) => case AwardFacilityCaptureBep(bep) =>
val gain = bep - experienceDebt awardProgressionOrExperience(
if (gain > 0L) { setBepAction(ExperienceType.Normal),
setBep(gain, ExperienceType.Normal) avatar.bep + bep,
} else { Config.app.game.promotion.captureExperiencePointsModifier
experienceDebt = experienceDebt - bep )
}
Behaviors.same Behaviors.same
case SupportExperienceDeposit(bep, delayBy) => case SupportExperienceDeposit(bep, delayBy) =>
@ -1837,25 +1648,36 @@ class AvatarActor(
val newBr = BattleRank.withExperience(bep).value val newBr = BattleRank.withExperience(bep).value
if (Config.app.game.promotion.active && oldBr == 1 && newBr > 1 && newBr < Config.app.game.promotion.maxBattleRank + 1) { if (Config.app.game.promotion.active && oldBr == 1 && newBr > 1 && newBr < Config.app.game.promotion.maxBattleRank + 1) {
experienceDebt = bep experienceDebt = bep
if (avatar.cep > 0) { AvatarActor.saveExperienceDebt(avatar.id, bep, bep)
setCep(0L)
}
true true
} else if (experienceDebt > 0 && newBr == 2) { } else if (experienceDebt > 0 && newBr == 2) {
experienceDebt = 0 experienceDebt = 0
AvatarActor.saveExperienceDebt(avatar.id, exp = 0, bep)
true true
} else { } else {
false false
} }
}) { }) {
setBep(bep, ExperienceType.Normal) setBep(bep, ExperienceType.Normal)
if (avatar.cep > 0) {
setCep(0L)
}
restoreBasicCerts()
removeAllImplants()
sessionActor ! SessionActor.CharSaved
sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_229, "@AckSuccessSetBattleRank")) 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 Behaviors.same
case AwardCep(cep) => case AwardCep(cep) =>
if (experienceDebt > 0L) { if (experienceDebt > 0L) {
setCep(avatar.cep + cep) setCep(avatar.cep + cep)
} else {
sessionActor ! SessionActor.SendResponse(ExperienceAddedMessage(0))
} }
Behaviors.same Behaviors.same
@ -1973,7 +1795,7 @@ class AvatarActor(
case RemoveShortcut(slot) => case RemoveShortcut(slot) =>
import ctx._ import ctx._
avatar.shortcuts.lift(slot).flatten match { avatar.shortcuts.lift(slot).flatten match {
case None => ; case None => ()
case Some(_) => case Some(_) =>
ctx.run( ctx.run(
query[persistence.Shortcut] query[persistence.Shortcut]
@ -1996,7 +1818,7 @@ class AvatarActor(
AvatarActor.saveAvatarData(avatar) AvatarActor.saveAvatarData(avatar)
saveLockerFunc() saveLockerFunc()
AvatarActor.updateToolDischargeFor(avatar) AvatarActor.updateToolDischargeFor(avatar)
AvatarActor.saveExperienceDebt(avatar.id, experienceDebt) AvatarActor.saveExperienceDebt(avatar.id, experienceDebt, avatar.bep)
AvatarActor.avatarNoLongerLoggedIn(account.get.id) AvatarActor.avatarNoLongerLoggedIn(account.get.id)
Behaviors.same Behaviors.same
} }
@ -2072,7 +1894,7 @@ class AvatarActor(
// } // }
// }.foreach { c => // }.foreach { c =>
// shortcutList.indexWhere { _.isEmpty } match { // shortcutList.indexWhere { _.isEmpty } match {
// case -1 => ; // case -1 => ()
// case index => // case index =>
// shortcutList.update(index, Some(AvatarShortcut(2, c.name))) // shortcutList.update(index, Some(AvatarShortcut(2, c.name)))
// } // }
@ -2180,7 +2002,7 @@ class AvatarActor(
avatar.implants.zipWithIndex.foreach { avatar.implants.zipWithIndex.foreach {
case (Some(_), slot) => case (Some(_), slot) =>
sessionActor ! SessionActor.SendResponse(AvatarImplantMessage(guid, ImplantAction.OutOfStamina, slot, 0)) sessionActor ! SessionActor.SendResponse(AvatarImplantMessage(guid, ImplantAction.OutOfStamina, slot, 0))
case _ => ; case _ => ()
} }
} }
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(guid, 2, totalStamina)) sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(guid, 2, totalStamina))
@ -2223,7 +2045,7 @@ class AvatarActor(
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 1) AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 1)
) )
case _ => ; case _ => ()
} }
} }
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(player.GUID, 2, totalStamina)) sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(player.GUID, 2, totalStamina))
@ -2268,7 +2090,7 @@ class AvatarActor(
AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 0)) AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 0))
) )
case (None, _) => ; case (None, _) => ()
} }
} }
@ -2324,7 +2146,7 @@ class AvatarActor(
avatar.name, avatar.name,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(index + 6, 0)) AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(index + 6, 0))
) )
case _ => ; case _ => ()
} }
} }
@ -2388,7 +2210,7 @@ class AvatarActor(
tool.GUID = PlanetSideGUID(gen.getAndIncrement) tool.GUID = PlanetSideGUID(gen.getAndIncrement)
case Some(item: Equipment) => case Some(item: Equipment) =>
item.GUID = PlanetSideGUID(gen.getAndIncrement) item.GUID = PlanetSideGUID(gen.getAndIncrement)
case _ => ; case _ => ()
} }
) )
player.GUID = PlanetSideGUID(gen.getAndIncrement) player.GUID = PlanetSideGUID(gen.getAndIncrement)
@ -2426,7 +2248,7 @@ class AvatarActor(
item.Invalidate() item.Invalidate()
case Some(item: Equipment) => case Some(item: Equipment) =>
item.Invalidate() item.Invalidate()
case _ => ; case _ => ()
} }
) )
player.Invalidate() player.Invalidate()
@ -2696,7 +2518,7 @@ class AvatarActor(
subtype subtype
) )
) )
case _ => ; case _ => ()
} }
} }
@ -2884,7 +2706,7 @@ class AvatarActor(
session match { session match {
case Some(sess) if sess.player != null => case Some(sess) if sess.player != null =>
sess.player.avatar = copyAvatar sess.player.avatar = copyAvatar
case _ => ; case _ => ()
} }
} }
@ -2903,7 +2725,7 @@ class AvatarActor(
case MemberAction.RemoveFriend => getAvatarForFunc(name, formatForOtherFunc(memberActionRemoveFriend)) case MemberAction.RemoveFriend => getAvatarForFunc(name, formatForOtherFunc(memberActionRemoveFriend))
case MemberAction.AddIgnoredPlayer => getAvatarForFunc(name, memberActionAddIgnored) case MemberAction.AddIgnoredPlayer => getAvatarForFunc(name, memberActionAddIgnored)
case MemberAction.RemoveIgnoredPlayer => getAvatarForFunc(name, formatForOtherFunc(memberActionRemoveIgnored)) case MemberAction.RemoveIgnoredPlayer => getAvatarForFunc(name, formatForOtherFunc(memberActionRemoveIgnored))
case _ => ; case _ => ()
} }
} }
} }
@ -2947,7 +2769,7 @@ class AvatarActor(
def memberActionAddFriend(charId: Long, name: String, faction: Int): Unit = { def memberActionAddFriend(charId: Long, name: String, faction: Int): Unit = {
val people = avatar.people val people = avatar.people
people.friend.find { _.name.equals(name) } match { people.friend.find { _.name.equals(name) } match {
case Some(_) => ; case Some(_) => ()
case None => case None =>
import ctx._ import ctx._
ctx.run( ctx.run(
@ -2983,7 +2805,7 @@ class AvatarActor(
replaceAvatar( replaceAvatar(
avatar.copy(people = people.copy(friend = people.friend.filterNot { _.charId == charId })) avatar.copy(people = people.copy(friend = people.friend.filterNot { _.charId == charId }))
) )
case None => ; case None => ()
} }
ctx.run( ctx.run(
query[persistence.Friend] query[persistence.Friend]
@ -3042,7 +2864,7 @@ class AvatarActor(
def memberActionAddIgnored(charId: Long, name: String, faction: Int): Unit = { def memberActionAddIgnored(charId: Long, name: String, faction: Int): Unit = {
val people = avatar.people val people = avatar.people
people.ignored.find { _.name.equals(name) } match { people.ignored.find { _.name.equals(name) } match {
case Some(_) => ; case Some(_) => ()
case None => case None =>
import ctx._ import ctx._
ctx.run( ctx.run(
@ -3078,7 +2900,7 @@ class AvatarActor(
replaceAvatar( replaceAvatar(
avatar.copy(people = people.copy(ignored = people.ignored.filterNot { _.charId == charId })) avatar.copy(people = people.copy(ignored = people.ignored.filterNot { _.charId == charId }))
) )
case None => ; case None => ()
} }
ctx.run( ctx.run(
query[persistence.Ignored] query[persistence.Ignored]
@ -3163,12 +2985,7 @@ class AvatarActor(
} }
def awardSupportExperience(bep: Long, previousDelay: Long): Unit = { def awardSupportExperience(bep: Long, previousDelay: Long): Unit = {
setBep(avatar.bep + bep, ExperienceType.Support) //todo simplify support testing setBep(avatar.bep + bep, ExperienceType.Support)
// supportExperiencePool = supportExperiencePool + bep
// avatar.scorecard.rate(bep)
// if (supportExperienceTimer.isCancelled) {
// resetSupportExperienceTimer(previousBep = 0, previousDelay = 0)
// }
} }
def actuallyAwardSupportExperience(bep: Long, delayBy: Long): Unit = { 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 = { def updateKills(killStat: Kill): Unit = {
val exp = killStat.experienceEarned val exp = killStat.experienceEarned
val (modifiedExp, msg) = updateExperienceAndType(killStat.experienceEarned) val (modifiedExp, msg) = updateExperienceAndType(killStat.experienceEarned)
@ -3431,6 +3276,245 @@ class AvatarActor(
output.future 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 = { def resetSupportExperienceTimer(previousBep: Long, previousDelay: Long): Unit = {
val bep: Long = if (supportExperiencePool < 10L) { val bep: Long = if (supportExperiencePool < 10L) {
supportExperiencePool supportExperiencePool

View file

@ -732,10 +732,18 @@ class ChatActor(
} }
case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed => 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 => 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 => case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
contents.toIntOption match { contents.toIntOption match {
@ -1124,7 +1132,7 @@ class ChatActor(
true true
} else if (contents.startsWith("!list")) { } else if (contents.startsWith("!list")) {
val zone = contents.split(" ").lift(1) match { val zone = dropFirstWord(contents).split(" ").headOption match {
case None => case None =>
Some(session.zone) Some(session.zone)
case Some(id) => case Some(id) =>
@ -1170,8 +1178,8 @@ class ChatActor(
true true
} else if (contents.startsWith("!ntu") && gmCommandAllowed) { } else if (contents.startsWith("!ntu") && gmCommandAllowed) {
val buffer = contents.toLowerCase.split("\\s+") val buffer = dropFirstWord(contents).toLowerCase.split("\\s+")
val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match { 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), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt)) case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
case _ => (None, None) case _ => (None, None)
@ -1202,8 +1210,8 @@ class ChatActor(
true true
} else if (contents.startsWith("!zonerotate") && gmCommandAllowed) { } else if (contents.startsWith("!zonerotate") && gmCommandAllowed) {
val buffer = contents.toLowerCase.split("\\s+") val buffer = dropFirstWord(contents).toLowerCase.split("\\s+")
cluster ! InterstellarClusterService.CavernRotation(buffer.lift(1) match { cluster ! InterstellarClusterService.CavernRotation(buffer.headOption match {
case Some("-list") | Some("-l") => case Some("-list") | Some("-l") =>
CavernRotationService.ReportRotationOrder(sessionActor.toClassic) CavernRotationService.ReportRotationOrder(sessionActor.toClassic)
case _ => case _ =>
@ -1224,8 +1232,8 @@ class ChatActor(
} else if (contents.startsWith("!macro")) { } else if (contents.startsWith("!macro")) {
val avatar = session.avatar val avatar = session.avatar
val args = contents.split(" ").filter(_ != "") val args = dropFirstWord(contents).split(" ").filter(_ != "")
(args.lift(1), args.lift(2)) match { (args.headOption, args.lift(1)) match {
case (Some(cmd), other) => case (Some(cmd), other) =>
cmd.toLowerCase() match { cmd.toLowerCase() match {
case "medkit" => case "medkit" =>
@ -1275,9 +1283,10 @@ class ChatActor(
} }
} else if (contents.startsWith("!progress")) { } else if (contents.startsWith("!progress")) {
if (!session.account.gm && BattleRank.withExperience(session.avatar.bep).value < Config.app.game.promotion.maxBattleRank + 1) { 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 true
} else { } else {
setBattleRank(contents="1", session, AvatarActor.Progress)
false false
} }
} else { } 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( def setBattleRank(
message: ChatMsg,
contents: String, contents: String,
session: Session, session: Session,
msgFunc: Long => AvatarActor.Command msgFunc: Long => AvatarActor.Command
): Unit = { ): Boolean = {
val buffer = contents.toLowerCase.split("\\s+") val buffer = contents.toLowerCase.split("\\s+")
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match { val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
case (Some(target), Some(rank)) if target == session.avatar.name => case (Some(target), Some(rank)) if target == session.avatar.name =>
@ -1301,6 +1317,8 @@ class ChatActor(
case Some(rank) => (None, BattleRank.withValueOpt(rank)) case Some(rank) => (None, BattleRank.withValueOpt(rank))
case None => (None, None) case None => (None, None)
} }
case (Some("-h"), _) | (Some("-help"), _) =>
(None, Some(BattleRank.BR1))
case (Some(_), Some(_)) => case (Some(_), Some(_)) =>
// picking other targets is not supported for now // picking other targets is not supported for now
(None, None) (None, None)
@ -1314,14 +1332,16 @@ class ChatActor(
(target, rank) match { (target, rank) match {
case (_, Some(rank)) if rank.value <= Config.app.game.maxBattleRank => case (_, Some(rank)) if rank.value <= Config.app.game.maxBattleRank =>
avatarActor ! msgFunc(rank.experience) avatarActor ! msgFunc(rank.experience)
true
case _ => case _ =>
sessionActor ! SessionActor.SendResponse( false
message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
)
} }
} }
def setCommandRank(message: ChatMsg, contents: String, session: Session): Unit = { def setCommandRank(
contents: String,
session: Session
): Boolean = {
val buffer = contents.toLowerCase.split("\\s+") val buffer = contents.toLowerCase.split("\\s+")
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match { val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
case (Some(target), Some(rank)) if target == session.avatar.name => case (Some(target), Some(rank)) if target == session.avatar.name =>
@ -1342,11 +1362,9 @@ class ChatActor(
(target, rank) match { (target, rank) match {
case (_, Some(rank)) => case (_, Some(rank)) =>
avatarActor ! AvatarActor.SetCep(rank.experience) avatarActor ! AvatarActor.SetCep(rank.experience)
//sessionActor ! SessionActor.SendResponse(message.copy(contents = "@AckSuccessSetCommandRank")) true
case _ => case _ =>
sessionActor ! SessionActor.SendResponse( false
message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
)
} }
} }
} }

View file

@ -81,7 +81,7 @@ object ZoningOperations {
"If you consider yourself as a veteran soldier, despite looking so green, please read this.\n" ++ "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." ++ "You only have this opportunity while you are battle rank 1." ++
"\n\n" ++ "\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" ++ "\n\n" ++
s"You may, however, rapidly promote yourself to at most battle rank ${Config.app.game.promotion.maxBattleRank}. " ++ 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. " ++ "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) { if (dataSum != 0) {
math.max(0.15f, math.min(2f, dataSum / dataCount.toFloat)) math.max(0.15f, math.min(2f, dataSum / dataCount.toFloat))
} else { } 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.concurrent.Promise
import scala.util.Success import scala.util.Success
val requestLayers: Promise[ZoneHotSpotProjector.ExposedHeat] = Promise[ZoneHotSpotProjector.ExposedHeat]() val requestLayers: Promise[ZoneHotSpotProjector.ExposedHeat] = Promise[ZoneHotSpotProjector.ExposedHeat]()
val request = updateHotSpotInfoOnly() // val request = updateHotSpotInfoOnly()
requestLayers.completeWith(request) // requestLayers.completeWith(request)
request.onComplete { // request.onComplete {
case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) => // case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) =>
hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis() - 900000L) // hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis() - 900000L)
case _ => // case _ =>
requestLayers.completeWith(Future(ZoneHotSpotProjector.ExposedHeat(Vector3.Zero, 0, Nil))) // 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 requestLayers.future
} }
@ -140,7 +141,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
*/ */
val finalMap = mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]]() val finalMap = mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]]()
.addAll( .addAll(
hotSpotLayersOverTime.take(1).flatMap { entry => hotSpotLayersOverTime.flatMap { entry =>
entry.map { f => (f.DisplayLocation, Map.empty[PlanetSideEmpire.Value, Seq[Long]]) } 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)) .find(evt => event.equals(evt.name))
.map { event => .map { event =>
val shots = weaponStat.shots val shots = weaponStat.shots
val shotsMax = event.shotsMax
val shotsMultiplier = event.shotsMultiplier val shotsMultiplier = event.shotsMultiplier
if (shotsMultiplier > 0f && shots < event.shotsCutoff) { if (shotsMultiplier > 0f && shots < event.shotsCutoff) {
val modifiedShotsReward: Float = { val modifiedShotsReward: Float =
val partialShots = math.min(event.shotsLimit, shots).toFloat shotsMultiplier * math.log(math.min(shotsMax, shots).toDouble + 2d).toFloat
shotsMultiplier * (if (event.shotsNatLog > 0f) { val modifiedAmountReward: Float =
math.log(math.pow(partialShots, event.shotsNatLog) + 2d).toFloat event.amountMultiplier * weaponStat.amount.toFloat
} else { event.base.toFloat + modifiedShotsReward + modifiedAmountReward
partialShots
})
}
val modifiedAmountReward: Float = event.amountMultiplier * weaponStat.amount.toFloat
event.base + modifiedShotsReward + modifiedAmountReward
} else { } else {
0f 0f
} }

View file

@ -2,7 +2,8 @@
package net.psforever.packet.game package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.Codec import scodec.bits.BitVector
import scodec.{Attempt, Codec}
import scodec.codecs._ import scodec.codecs._
/** /**
@ -12,21 +13,33 @@ import scodec.codecs._
* It merely generates the message:<br> * It merely generates the message:<br>
* `"You have been awarded x experience points."`<br> * `"You have been awarded x experience points."`<br>
* ... where `x` is the number of experience points that have been promised. * ... 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 exp the number of (Command) experience points earned
* @param unk defaults to `true` for effect; * @param cmd if `true`, the message will be tailored for "Command" experience;
* if `false`, the number of experience points in the message will be blanked * 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 type Packet = ExperienceAddedMessage
def opcode = GamePacketOpcode.ExperienceAddedMessage def opcode: GamePacketOpcode.Value = GamePacketOpcode.ExperienceAddedMessage
def encode = ExperienceAddedMessage.encode(this) def encode: Attempt[BitVector] = ExperienceAddedMessage.encode(this)
} }
object ExperienceAddedMessage extends Marshallable[ExperienceAddedMessage] { 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] = ( implicit val codec: Codec[ExperienceAddedMessage] = (
("exp" | uintL(15)) :: ("unk" | bool) ("exp" | uintL(bits = 15)) :: ("unk" | bool)
).as[ExperienceAddedMessage] ).as[ExperienceAddedMessage]
} }

View file

@ -1,7 +1,12 @@
// Copyright (c) 2023 PSForever // Copyright (c) 2023 PSForever
package net.psforever.persistence package net.psforever.persistence
import org.joda.time.LocalDateTime
case class Progressiondebt( case class Progressiondebt(
avatarId:Long, 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( case class SupportExperienceEvent(
name: String, name: String,
base: Long, base: Long,
shotsMultiplier: Float = 0f, shotsMax: Int = 50,
shotsNatLog: Double = 0f,
shotsLimit: Int = 50,
shotsCutoff: Int = 50, shotsCutoff: Int = 50,
shotsMultiplier: Float = 0f,
amountMultiplier: Float = 0f amountMultiplier: Float = 0f
) )
case class CommandExperiencePoints( case class CommandExperiencePoints(
rate: Float, rate: Float,
lluCarrierModifier: Float, lluCarrierModifier: Float,
lluSlayerCreditDuration: Duration, lluSlayerCreditDuration: Duration,
lluSlayerCredit: Long, lluSlayerCredit: Long,
maximumPerSquadSize: Seq[Int], maximumPerSquadSize: Seq[Int],
squadSizeLimitOverflow: Int, squadSizeLimitOverflow: Int,
squadSizeLimitOverflowMultiplier: Float squadSizeLimitOverflowMultiplier: Float
) )
case class PromotionSystem( case class PromotionSystem(
active: Boolean, active: Boolean,
maxBattleRank: Int, maxBattleRank: Int,
battleExperiencePointsModifier: Float battleExperiencePointsModifier: Float,
supportExperiencePointsModifier: Float,
captureExperiencePointsModifier: Float
) )