Stamkit Fixed, Maybe, Sorta, Not Really, Take Two (#966)

* fixed stamina kit, maybe

* ui field updates for stamina on kit use
This commit is contained in:
Fate-JH 2021-11-11 09:03:08 -05:00 committed by GitHub
parent a8630e11ab
commit 46763b7877
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 113 additions and 70 deletions

View file

@ -151,7 +151,10 @@ object AvatarActor {
/** Set the avatar's lookingForSquad */ /** Set the avatar's lookingForSquad */
final case class SetLookingForSquad(lfs: Boolean) extends Command final case class SetLookingForSquad(lfs: Boolean) extends Command
/** Restore up to the given stamina amount */ /** Restore up to the given stamina amount due to natural recharge */
private case class RestoreStaminaPeriodically(stamina: Int) extends Command
/** Restore up to the given stamina amount for some reason */
final case class RestoreStamina(stamina: Int) extends Command final case class RestoreStamina(stamina: Int) extends Command
/** Consume up to the given stamina amount */ /** Consume up to the given stamina amount */
@ -201,7 +204,7 @@ class AvatarActor(
var account: Option[Account] = None var account: Option[Account] = None
var session: Option[Session] = None var session: Option[Session] = None
val implantTimers: mutable.Map[Int, Cancellable] = mutable.Map() val implantTimers: mutable.Map[Int, Cancellable] = mutable.Map()
var staminaRegenTimer: Cancellable = Cancellable.alreadyCancelled var staminaRegenTimer: Cancellable = Default.Cancellable
var _avatar: Option[Avatar] = None var _avatar: Option[Avatar] = None
var saveLockerFunc: () => Unit = storeNewLocker var saveLockerFunc: () => Unit = storeNewLocker
//val topic: ActorRef[Topic.Command[Avatar]] = context.spawnAnonymous(Topic[Avatar]("avatar")) //val topic: ActorRef[Topic.Command[Avatar]] = context.spawnAnonymous(Topic[Avatar]("avatar"))
@ -257,7 +260,7 @@ class AvatarActor(
Behaviors.same Behaviors.same
case SetLookingForSquad(lfs) => case SetLookingForSquad(lfs) =>
avatar = avatar.copy(lookingForSquad = lfs) avatarCopy(avatar.copy(lookingForSquad = lfs))
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 53, 0)) sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 53, 0))
session.get.zone.AvatarEvents ! AvatarServiceMessage( session.get.zone.AvatarEvents ! AvatarServiceMessage(
avatar.faction.toString, avatar.faction.toString,
@ -366,16 +369,15 @@ class AvatarActor(
result.onComplete { result.onComplete {
case Success((loadouts, implants, certs, locker)) => case Success((loadouts, implants, certs, locker)) =>
avatar = avatar.copy( avatarCopy(avatar.copy(
loadouts = loadouts, loadouts = loadouts,
// make sure we always have the base certifications // make sure we always have the base certifications
certifications = certifications =
certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications, certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications,
implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None), implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None),
locker = locker locker = locker
) ))
staminaRegenTimer.cancel() defaultStaminaRegen(initialDelay = 0.5f seconds)
staminaRegenTimer = defaultStaminaRegen()
replyTo ! AvatarLoginResponse(avatar) replyTo ! AvatarLoginResponse(avatar)
case Failure(e) => case Failure(e) =>
log.error(e)("db failure") log.error(e)("db failure")
@ -385,11 +387,11 @@ class AvatarActor(
case ReplaceAvatar(newAvatar) => case ReplaceAvatar(newAvatar) =>
replaceAvatar(newAvatar) replaceAvatar(newAvatar)
staminaRegenTimer.cancel() staminaRegenTimer.cancel()
staminaRegenTimer = defaultStaminaRegen() defaultStaminaRegen(initialDelay = 0.5f seconds)
Behaviors.same Behaviors.same
case AddFirstTimeEvent(event) => case AddFirstTimeEvent(event) =>
avatar = avatar.copy(firstTimeEvents = avatar.firstTimeEvents ++ Set(event)) avatarCopy(avatar.copy(firstTimeEvents = avatar.firstTimeEvents ++ Set(event)))
Behaviors.same Behaviors.same
case LearnCertification(terminalGuid, certification) => case LearnCertification(terminalGuid, certification) =>
@ -710,7 +712,7 @@ class AvatarActor(
} }
result.onComplete { result.onComplete {
case Success(_) => case Success(_) =>
replaceAvatar(avatar.copy(loadouts = avatar.loadouts.updated(lineNo, None))) avatarCopy(avatar.copy(loadouts = avatar.loadouts.updated(lineNo, None)))
sessionActor ! SessionActor.SendResponse(FavoritesMessage(loadoutType, player.GUID, number, "")) sessionActor ! SessionActor.SendResponse(FavoritesMessage(loadoutType, player.GUID, number, ""))
case Failure(exception) => case Failure(exception) =>
log.error(exception)("db failure (?)") log.error(exception)("db failure (?)")
@ -742,14 +744,14 @@ class AvatarActor(
case _ => ; case _ => ;
} }
} }
avatar = avatar.copy(purchaseTimes = newTimes) avatarCopy(avatar.copy(purchaseTimes = newTimes))
Behaviors.same Behaviors.same
case UpdateUseTime(definition, time) => case UpdateUseTime(definition, time) =>
if (!Avatar.useCooldowns.contains(definition)) { if (!Avatar.useCooldowns.contains(definition)) {
log.warn(s"${avatar.name} is updating a use time for item '${definition.Name}' that has no cooldown") log.warn(s"${avatar.name} is updating a use time for item '${definition.Name}' that has no cooldown")
} }
avatar = avatar.copy(useTimes = avatar.useTimes.updated(definition.Name, time)) avatarCopy(avatar.copy(useTimes = avatar.useTimes.updated(definition.Name, time)))
sessionActor ! SessionActor.UseCooldownRenewed(definition, time) sessionActor ! SessionActor.UseCooldownRenewed(definition, time)
Behaviors.same Behaviors.same
@ -758,7 +760,7 @@ class AvatarActor(
Behaviors.same Behaviors.same
case SetVehicle(vehicle) => case SetVehicle(vehicle) =>
avatar = avatar.copy(vehicle = vehicle) avatarCopy(avatar.copy(vehicle = vehicle))
Behaviors.same Behaviors.same
case ActivateImplant(implantType) => case ActivateImplant(implantType) =>
@ -776,9 +778,9 @@ class AvatarActor(
} else if (implant.definition.implantType.disabledFor.contains(session.get.player.ExoSuit)) { } else if (implant.definition.implantType.disabledFor.contains(session.get.player.ExoSuit)) {
// TODO can this really happen? can we prevent it? // TODO can this really happen? can we prevent it?
} else { } else {
avatar = avatar.copy( avatarCopy(avatar.copy(
implants = avatar.implants.updated(slot, Some(implant.copy(active = true))) implants = avatar.implants.updated(slot, Some(implant.copy(active = true)))
) ))
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage( AvatarImplantMessage(
session.get.player.GUID, session.get.player.GUID,
@ -849,11 +851,11 @@ class AvatarActor(
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, index, 1) AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, index, 1)
) )
avatar = avatar.copy(implants = avatar.implants.map { avatarCopy(avatar.copy(implants = avatar.implants.map {
case Some(implant) if implant.definition.implantType == implantType => case Some(implant) if implant.definition.implantType == implantType =>
Some(implant.copy(initialized = true)) Some(implant.copy(initialized = true))
case other => other case other => other
}) }))
case None => log.error(s"set initialized called for unknown implant $implantType") case None => log.error(s"set initialized called for unknown implant $implantType")
} }
@ -875,30 +877,29 @@ class AvatarActor(
Behaviors.same Behaviors.same
case RestoreStamina(stamina) => case RestoreStamina(stamina) =>
if (stamina > 0 && session.get.player.HasGUID) { tryRestoreStaminaForSession(stamina) match {
val totalStamina = math.min(avatar.maxStamina, avatar.stamina + stamina) case Some(sess) =>
val fatigued = if (avatar.fatigued && totalStamina >= 20) { actuallyRestoreStamina(stamina, sess)
avatar.implants.zipWithIndex.foreach { defaultStaminaRegen(initialDelay = 0.5f seconds)
case (Some(_), slot) => case _ => ;
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(session.get.player.GUID, ImplantAction.OutOfStamina, slot, 0)
)
case _ => ()
}
false
} else {
avatar.fatigued
}
avatar = avatar.copy(stamina = totalStamina, fatigued = fatigued)
sessionActor ! SessionActor.SendResponse(
PlanetsideAttributeMessage(session.get.player.GUID, 2, avatar.stamina)
)
} }
Behaviors.same Behaviors.same
case RestoreStaminaPeriodically(stamina) =>
tryRestoreStaminaForSession(stamina) match {
case Some(sess) =>
actuallyRestoreStaminaIfStationary(stamina, sess)
case _ => ;
}
defaultStaminaRegen(initialDelay = 0.5f seconds)
Behaviors.same
case ConsumeStamina(stamina) => case ConsumeStamina(stamina) =>
if (stamina > 0) { if (stamina > 0) {
consumeThisMuchStamina(stamina) consumeThisMuchStamina(stamina)
if(staminaRegenTimer.isCancelled) {
defaultStaminaRegen(initialDelay = 0.5f seconds)
}
} else { } else {
log.warn(s"consumed stamina must be larger than 0, but is: $stamina") log.warn(s"consumed stamina must be larger than 0, but is: $stamina")
} }
@ -906,13 +907,7 @@ class AvatarActor(
case SuspendStaminaRegeneration(duration) => case SuspendStaminaRegeneration(duration) =>
// TODO suspensions can overwrite each other with different durations // TODO suspensions can overwrite each other with different durations
staminaRegenTimer.cancel() defaultStaminaRegen(duration)
staminaRegenTimer = context.system.scheduler.scheduleOnce(
duration,
() => {
staminaRegenTimer = defaultStaminaRegen()
}
)
Behaviors.same Behaviors.same
case InitializeImplants() => case InitializeImplants() =>
@ -1018,8 +1013,13 @@ class AvatarActor(
Future.failed(ex).asInstanceOf[Future[Loadout]] Future.failed(ex).asInstanceOf[Future[Loadout]]
} }
/**
* na
* @see `avatarCopy(Avatar)`
* @param newAvatar na
*/
def replaceAvatar(newAvatar: Avatar): Unit = { def replaceAvatar(newAvatar: Avatar): Unit = {
avatar = newAvatar avatarCopy(newAvatar)
avatar.deployables.UpdateMaxCounts(avatar.certifications) avatar.deployables.UpdateMaxCounts(avatar.certifications)
updateDeployableUIElements( updateDeployableUIElements(
avatar.deployables.UpdateUI() avatar.deployables.UpdateUI()
@ -1038,7 +1038,7 @@ class AvatarActor(
) )
.onComplete { .onComplete {
case Success(_) => case Success(_) =>
avatar = avatar.copy(cosmetics = Some(cosmetics)) avatarCopy(avatar.copy(cosmetics = Some(cosmetics)))
session.get.zone.AvatarEvents ! AvatarServiceMessage( session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id, session.get.zone.id,
AvatarAction AvatarAction
@ -1052,13 +1052,45 @@ class AvatarActor(
p.future p.future
} }
def tryRestoreStaminaForSession(stamina: Int): Option[Session] = {
session match {
case out @ Some(_) if !avatar.staminaFull && stamina > 0 => out
case _ => None
}
}
def actuallyRestoreStaminaIfStationary(stamina: Int, session: Session): Unit = {
if (session.player.VehicleSeated.nonEmpty || !(session.player.isMoving || session.player.Jumping)) {
actuallyRestoreStamina(stamina, session)
}
}
def actuallyRestoreStamina(stamina: Int, session: Session): Unit = {
val totalStamina = math.min(avatar.maxStamina, avatar.stamina + stamina)
val isFatigued = if (avatar.fatigued && totalStamina >= 20) {
val pguid = session.player.GUID
avatar.implants.zipWithIndex.foreach {
case (Some(_), slot) =>
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(pguid, ImplantAction.OutOfStamina, slot, 0)
)
case _ => ()
}
false
} else {
avatar.fatigued
}
avatarCopy(avatar.copy(stamina = totalStamina, fatigued = isFatigued))
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.player.GUID, 2, totalStamina))
}
/** /**
* Drain at most a given amount of stamina from the player's pool of stamina. * Drain at most a given amount of stamina from the player's pool of stamina.
* If the player's reserves become zero in the act, inform the player that he is fatigued * If the player's reserves become zero in the act, inform the player that he is fatigued
* meaning that he will only be able to walk, all implants will deactivate, * meaning that he will only be able to walk, all implants will deactivate,
* and all exertion that require stamina use will become impossible until a threshold of stamina is regained. * and all exertion that require stamina use will become impossible until a threshold of stamina is regained.
* @param stamina an amount to drain * @param stamina an amount to drain
* @return `true`, as long as the amount of stamina can be drained in total; * @return `true`, as long as the requested amount of stamina can be drained in total;
* `false`, otherwise * `false`, otherwise
*/ */
def consumeThisMuchStamina(stamina: Int): Boolean = { def consumeThisMuchStamina(stamina: Int): Boolean = {
@ -1069,7 +1101,7 @@ class AvatarActor(
val totalStamina = math.max(0, resultingStamina) val totalStamina = math.max(0, resultingStamina)
val alreadyFatigued = avatar.fatigued val alreadyFatigued = avatar.fatigued
val becomeFatigued = !alreadyFatigued && totalStamina == 0 val becomeFatigued = !alreadyFatigued && totalStamina == 0
avatar = avatar.copy(stamina = totalStamina, fatigued = alreadyFatigued || becomeFatigued) avatarCopy(avatar.copy(stamina = totalStamina, fatigued = alreadyFatigued || becomeFatigued))
val player = session.get.player val player = session.get.player
if (player.HasGUID) { if (player.HasGUID) {
if (becomeFatigued) { if (becomeFatigued) {
@ -1084,15 +1116,15 @@ class AvatarActor(
case _ => ; case _ => ;
} }
} }
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(player.GUID, 2, avatar.stamina)) sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(player.GUID, 2, totalStamina))
} else if (becomeFatigued) { } else if (becomeFatigued) {
avatar = avatar.copy(implants = avatar.implants.zipWithIndex.collect { avatarCopy(avatar.copy(implants = avatar.implants.zipWithIndex.collect {
case (Some(implant), slot) if implant.active => case (Some(implant), slot) if implant.active =>
implantTimers(slot).cancel() implantTimers(slot).cancel()
Some(implant.copy(active = false)) Some(implant.copy(active = false))
case (out, _) => case (out, _) =>
out out
}) }))
} }
resultingStamina >= 0 resultingStamina >= 0
} }
@ -1133,7 +1165,7 @@ class AvatarActor(
} }
def deinitializeImplants(): Unit = { def deinitializeImplants(): Unit = {
avatar = avatar.copy(implants = avatar.implants.zipWithIndex.map { avatarCopy(avatar.copy(implants = avatar.implants.zipWithIndex.map {
case (Some(implant), slot) => case (Some(implant), slot) =>
if (implant.active) { if (implant.active) {
deactivateImplant(implant.definition.implantType) deactivateImplant(implant.definition.implantType)
@ -1147,7 +1179,7 @@ class AvatarActor(
) )
Some(implant.copy(initialized = false, active = false)) Some(implant.copy(initialized = false, active = false))
case (None, _) => None case (None, _) => None
}) }))
} }
def resetAnImplant(implantType: ImplantType): Unit = { def resetAnImplant(implantType: ImplantType): Unit = {
@ -1168,9 +1200,9 @@ class AvatarActor(
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, index, 0) AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, index, 0)
) )
) )
avatar = avatar.copy( avatarCopy(avatar.copy(
implants = avatar.implants.updated(index, Some(imp.copy(initialized = false, active = false))) implants = avatar.implants.updated(index, Some(imp.copy(initialized = false, active = false)))
) ))
//restart initialization process //restart initialization process
implantTimers(index).cancel() implantTimers(index).cancel()
implantTimers(index) = context.scheduleOnce( implantTimers(index) = context.scheduleOnce(
@ -1192,9 +1224,9 @@ class AvatarActor(
} match { } match {
case Some((implant, slot)) => case Some((implant, slot)) =>
implantTimers(slot).cancel() implantTimers(slot).cancel()
avatar = avatar.copy( avatarCopy(avatar.copy(
implants = avatar.implants.updated(slot, Some(implant.copy(active = false))) implants = avatar.implants.updated(slot, Some(implant.copy(active = false)))
) ))
// Deactivation sound / effect // Deactivation sound / effect
session.get.zone.AvatarEvents ! AvatarServiceMessage( session.get.zone.AvatarEvents ! AvatarServiceMessage(
@ -1647,18 +1679,15 @@ class AvatarActor(
} }
} }
def defaultStaminaRegen(): Cancellable = { def defaultStaminaRegen(initialDelay: FiniteDuration): Unit = {
context.system.scheduler.scheduleWithFixedDelay(0.5 seconds, 0.5 seconds)(() => { staminaRegenTimer.cancel()
(session, _avatar) match { staminaRegenTimer = if (!avatar.staminaFull) {
case (Some(_session), Some(_)) => context.system.scheduler.scheduleWithFixedDelay(initialDelay, 0.5 seconds)(() => {
if ( context.self ! RestoreStaminaPeriodically(1)
!avatar.staminaFull && (_session.player.VehicleSeated.nonEmpty || !_session.player.isMoving && !_session.player.Jumping) })
) { } else {
context.self ! RestoreStamina(1) Default.Cancellable
} }
case _ => ;
}
})
} }
// same as in SA, this really doesn't belong here // same as in SA, this really doesn't belong here
@ -1731,7 +1760,7 @@ class AvatarActor(
} }
} }
if (keysToDrop.nonEmpty) { if (keysToDrop.nonEmpty) {
avatar = avatar.copy(purchaseTimes = avatar.purchaseTimes.removedAll(keysToDrop)) avatarCopy(avatar.copy(purchaseTimes = avatar.purchaseTimes.removedAll(keysToDrop)))
} }
} }
@ -1741,4 +1770,18 @@ class AvatarActor(
AvatarVehicleTimerMessage(session.get.player.GUID, name, time, unk1 = true) AvatarVehicleTimerMessage(session.get.player.GUID, name, time, unk1 = true)
) )
} }
/**
* na
* @see `replaceCopy(Avatar)`
* @param copyAvatar na
*/
def avatarCopy(copyAvatar: Avatar): Unit = {
avatar = copyAvatar
session match {
case Some(sess) if sess.player != null =>
sess.player.avatar = copyAvatar
case _ => ;
}
}
} }

View file

@ -265,8 +265,8 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
(None, 0, 0, "Stamina at maximum - No recharge required.") (None, 0, 0, "Stamina at maximum - No recharge required.")
} else { } else {
avatarActor ! AvatarActor.RestoreStamina(100) avatarActor ! AvatarActor.RestoreStamina(100)
//(Some(index), 2, player.avatar.stamina, "") //proper stamina update will occur due to above message; update something relatively harmless instead
(None, 0, 0, "") (Some(index), 3, player.avatar.maxStamina, "")
} }
} else { } else {
log.warn(s"UseItem: Your $kit behavior is not supported, ${player.Name}") log.warn(s"UseItem: Your $kit behavior is not supported, ${player.Name}")