redid (cleaned-up) implant logic

This commit is contained in:
Fate-JH 2024-06-10 15:13:32 -04:00
parent 38f25f5bcc
commit 40c93d8105
12 changed files with 425 additions and 461 deletions

View file

@ -11,7 +11,8 @@ import net.psforever.objects.Session
import net.psforever.objects.avatar.ModePermissions
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill, Life, ScoreCard, SupportActivity}
import net.psforever.objects.sourcing.{TurretSource, VehicleSource}
import net.psforever.objects.vital.ReconstructionActivity
import net.psforever.packet.game.ImplantAction
import net.psforever.services.avatar.AvatarServiceResponse
import net.psforever.types.{ChatMessageType, StatisticalCategory, StatisticalElement}
import org.joda.time.{LocalDateTime, Seconds}
@ -117,9 +118,6 @@ object AvatarActor {
/** Log in the currently selected avatar. Must have first sent SelectAvatar. */
final case class LoginAvatar(replyTo: ActorRef[AvatarLoginResponse]) extends Command
/** Send implants to client */
final case class CreateImplants() extends Command
/** Replace avatar instance with the provided one */
final case class ReplaceAvatar(avatar: Avatar) extends Command
@ -173,23 +171,23 @@ object AvatarActor {
/** Activate an implant (must already be initialized) */
final case class ActivateImplant(implantType: ImplantType) extends Command
/** Deactivate an implant */
/** Deactivate an implant (must already be activated) */
final case class DeactivateImplant(implantType: ImplantType) extends Command
/** Deactivate all non-passive implants that are in use */
final case class DeactivateActiveImplants() extends Command
/** Deactivate all non-passive implants that have been activated */
final case object DeactivateActiveImplants extends Command
/** Start implant initialization timers (after zoning or respawn) */
final case class InitializeImplants() extends Command
/** Start all implant initialization timers (this will also hard restart all active timers) */
final case object InitializeImplants extends Command
/** Deinitialize implants (before zoning or respawning) */
final case class DeinitializeImplants() extends Command
/** Set all implants to deactivated and deinitialized; do not restart the initialization process */
final case object DeinitializeImplants extends Command
/** Deinitialize a certain implant, then initialize it again */
/** Set a certain implant to deactivated and deinitialized; restart the initialization process */
final case class ResetImplant(implant: ImplantType) extends Command
/** Shorthand for DeinitializeImplants and InitializeImplants */
final case class ResetImplants() extends Command
/** Set all active non-passive implants to deactivated and restart the initialization process all un-initialized implants */
final case object SoftResetImplants extends Command
/** Set the avatar's lookingForSquad */
final case class SetLookingForSquad(lfs: Boolean) extends Command
@ -234,7 +232,7 @@ object AvatarActor {
final case class SetStamina(stamina: Int) extends Command
final case class SetImplantInitialized(implantType: ImplantType) extends Command
private case class SetImplantInitialized(implantType: ImplantType) extends Command
final case class MemberListRequest(action: MemberAction.Value, name: String) extends Command
@ -1015,7 +1013,7 @@ class AvatarActor(
private[this] val log = org.log4s.getLogger
var account: Option[Account] = None
var session: Option[Session] = None
val implantTimers: mutable.Map[Int, Cancellable] = mutable.Map()
val implantTimers: Array[Cancellable] = Array.fill(3)(Default.Cancellable)
var staminaRegenTimer: Cancellable = Default.Cancellable
var _avatar: Option[Avatar] = None
var saveLockerFunc: () => Unit = storeNewLocker
@ -1292,22 +1290,6 @@ class AvatarActor(
Behaviors.same
case CreateImplants() =>
avatar.implants.zipWithIndex.foreach {
case (Some(implant), index) =>
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(
session.get.player.GUID,
ImplantAction.Add,
index,
implant.definition.implantType.value
)
)
case _ => ()
}
deinitializeImplants()
Behaviors.same
case LearnImplant(terminalGuid, definition) =>
// TODO there used to be a terminal check here, do we really need it?
buyImplantAction(terminalGuid, definition)
@ -1466,113 +1448,36 @@ class AvatarActor(
avatarCopy(avatar.copy(vehicle = vehicle))
Behaviors.same
case ActivateImplant(implantType) =>
avatar.implants.zipWithIndex.collectFirst {
case (Some(implant), index) if implant.definition.implantType == implantType => (implant, index)
} match {
case Some((implant, slot)) =>
if (!implant.initialized) {
log.warn(s"requested activation of uninitialized implant $implantType")
} else if (
!consumeThisMuchStamina(implant.definition.ActivationStaminaCost) ||
avatar.stamina < implant.definition.StaminaCost
) {
// not enough stamina to activate
} else if (implant.definition.implantType.disabledFor.contains(session.get.player.ExoSuit)) {
// TODO can this really happen? can we prevent it?
} else {
avatarCopy(
avatar.copy(
implants = avatar.implants.updated(slot, Some(implant.copy(active = true)))
)
)
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Activation, slot, 1)
)
// Activation sound / effect
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
AvatarAction.PlanetsideAttribute(
session.get.player.GUID,
28,
implant.definition.implantType.value * 2 + 1
)
)
implantTimers.get(slot).foreach(_.cancel())
val interval = implant.definition.GetCostIntervalByExoSuit(session.get.player.ExoSuit).milliseconds
// TODO costInterval should be an option ^
if (interval.toMillis > 0) {
implantTimers(slot) = context.system.scheduler.scheduleWithFixedDelay(interval, interval)(() => {
val player = session.get.player
if (
implantType match {
case ImplantType.AdvancedRegen =>
// for every 1hp: 2sp (running), 1.5sp (standing), 1sp (crouched)
// to simulate '1.5sp (standing)', find if 0.0...1.0 * 100 is an even number
val cost = implant.definition.StaminaCost -
(if (player.Crouching || (!player.isMoving && (math.random() * 100) % 2 == 1)) 1 else 0)
val aliveAndWounded = player.isAlive && player.Health < player.MaxHealth
if (aliveAndWounded && consumeThisMuchStamina(cost)) {
//heal
val originalHealth = player.Health
val zone = player.Zone
val events = zone.AvatarEvents
val guid = player.GUID
val newHealth = player.Health = originalHealth + 1
player.LogActivity(HealFromImplant(implantType, 1))
events ! AvatarServiceMessage(
zone.id,
AvatarAction.PlanetsideAttributeToAll(guid, 0, newHealth)
)
false
} else {
!aliveAndWounded
}
case _ =>
!player.isAlive || !consumeThisMuchStamina(implant.definition.StaminaCost)
}
) {
context.self ! DeactivateImplant(implantType)
}
})
}
}
case None => log.error(s"requested activation of unknown implant $implantType")
}
case SetImplantInitialized(implantType) =>
setImplantInitialized(implantType)
Behaviors.same
case SetImplantInitialized(implantType) =>
avatar.implants.zipWithIndex.collectFirst {
case (Some(implant), index) if implant.definition.implantType == implantType => index
} match {
case Some(index) =>
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, index, 1)
)
avatarCopy(avatar.copy(implants = avatar.implants.map {
case Some(implant) if implant.definition.implantType == implantType =>
Some(implant.copy(initialized = true))
case other => other
}))
case None => log.error(s"set initialized called for unknown implant $implantType")
}
case ActivateImplant(implantType) =>
activateImplant(implantType)
Behaviors.same
case DeactivateImplant(implantType) =>
deactivateImplant(implantType)
Behaviors.same
case DeactivateActiveImplants() =>
avatar.implants.indices.foreach { index =>
avatar.implants(index).foreach { implant =>
if (implant.active && implant.definition.GetCostIntervalByExoSuit(session.get.player.ExoSuit) > 0) {
deactivateImplant(implant.definition.implantType)
}
}
}
case DeactivateActiveImplants =>
deactivateActiveImplants()
Behaviors.same
case InitializeImplants =>
initializeImplants()
Behaviors.same
case DeinitializeImplants =>
deinitializeImplants()
Behaviors.same
case ResetImplant(implantType) =>
reinitializeImplant(implantType)
Behaviors.same
case SoftResetImplants =>
softResetImplants()
Behaviors.same
case RestoreStamina(stamina) =>
@ -1596,83 +1501,6 @@ class AvatarActor(
defaultStaminaRegen(duration)
Behaviors.same
case InitializeImplants() =>
initializeImplants()
Behaviors.same
case DeinitializeImplants() =>
deinitializeImplants()
Behaviors.same
case ResetImplant(implantType) =>
resetAnImplant(implantType)
Behaviors.same
case ResetImplants() =>
val player = session.get.player
// Get time of when you spawned after a deconstruction or zoning activity.
val lastDecon: Long = player.History.findLast {entry => entry.isInstanceOf[ReconstructionActivity]} match {
case Some(entry) => entry.time
case _ => 0L
}
// Get time of when you entered the world or respawned after death.
val lastRespawn: Long = player.History.findLast {entry => entry.isInstanceOf[SpawningActivity]} match {
case Some(entry) => entry.time
case _ => 0L
}
// You didn't die. You deconstructed or changed zones via warpgate/IA/recall.
// When you respawn after death, it does both recon and spawn activities, hence the minus 3000 to make sure
// this doesn't happen at respawn after death.
if (lastDecon - 3000 > lastRespawn) {
deinitializeImplants()
val implants = avatar.implants
implants.zipWithIndex.foreach {
case (Some(implant), slot) =>
sessionActor ! SessionActor.SendResponse(
CreateShortcutMessage(
session.get.player.GUID,
slot + 2,
Some(implant.definition.implantType.shortcut)
)
)
// If the amount of time that has passed since you entered the world or died is > how long it takes to
// initialize this implant, initialize it after 1 second.
if ((System.currentTimeMillis() / 1000) - (lastRespawn / 1000) > implant.definition.InitializationDuration) {
implantTimers.get(slot).foreach(_.cancel())
implantTimers(slot) = context.scheduleOnce(
1.seconds,
context.self,
SetImplantInitialized(implant.definition.implantType)
)
session.get.zone.AvatarEvents ! AvatarServiceMessage(
avatar.name,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 0))
)
}
// If the implant initialization timer hasn't quite finished, calculate a reduced timer based on last spawn activity
else {
val remainingTime = (lastRespawn / 1000).seconds - (System.currentTimeMillis() / 1000).seconds + implant.definition.InitializationDuration.seconds
implantTimers.get(slot).foreach(_.cancel())
implantTimers(slot) = context.scheduleOnce(
remainingTime,
context.self,
SetImplantInitialized(implant.definition.implantType)
)
session.get.zone.AvatarEvents ! AvatarServiceMessage(
avatar.name,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 0))
)
}
case (None, _) =>
}
}
// You just entered the world or died. Implants reset and timers start from scratch
else {
deinitializeImplants()
initializeImplants()
}
Behaviors.same
case UpdateToolDischarge(stats) =>
updateToolDischarge(stats)
Behaviors.same
@ -1898,7 +1726,7 @@ class AvatarActor(
.receiveSignal {
case (_, PostStop) =>
staminaRegenTimer.cancel()
implantTimers.values.foreach(_.cancel())
implantTimers.foreach(_.cancel())
supportExperienceTimer.cancel()
if (supportExperiencePool > 0) {
AvatarActor.setBepOnly(avatar.id, avatar.bep + supportExperiencePool)
@ -1940,63 +1768,6 @@ class AvatarActor(
def performAvatarLogin(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
performAvatarLogin0(avatarId, accountId, replyTo)
/*import ctx._
val result = for {
//log this login
_ <- ctx.run(
query[persistence.Avatar]
.filter(_.id == lift(avatarId))
.update(_.lastLogin -> lift(LocalDateTime.now()))
)
//log this choice of faction (no empire switching)
_ <- ctx.run(
query[persistence.Account]
.filter(_.id == lift(accountId))
.update(
_.lastFactionId -> lift(avatar.faction.id),
_.avatarLoggedIn -> lift(avatarId)
)
)
//retrieve avatar data
loadouts <- initializeAllLoadouts()
implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatarId)))
certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatarId)))
locker <- loadLocker(avatarId)
friends <- loadFriendList(avatarId)
ignored <- loadIgnoredList(avatarId)
shortcuts <- loadShortcuts(avatarId)
saved <- AvatarActor.loadSavedAvatarData(avatarId)
debt <- AvatarActor.loadExperienceDebt(avatarId)
card <- AvatarActor.loadCampaignKdaData(avatarId)
} yield (loadouts, implants, certs, locker, friends, ignored, shortcuts, saved, debt, card)
result.onComplete {
case Success((_loadouts, implants, certs, lockerInv, friendsList, ignoredList, shortcutList, saved, debt, card)) =>
avatarCopy(
avatar.copy(
loadouts = avatar.loadouts.copy(suit = _loadouts),
certifications =
certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications,
implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None),
shortcuts = shortcutList,
locker = lockerInv,
people = MemberLists(
friend = friendsList,
ignored = ignoredList
),
cooldowns = Cooldowns(
purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log),
use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log)
),
scorecard = card
)
)
// if we need to start stamina regeneration
tryRestoreStaminaForSession(stamina = 1).collect { _ => defaultStaminaRegen(initialDelay = 0.5f seconds) }
experienceDebt = debt
replyTo ! AvatarLoginResponse(avatar)
case Failure(e) =>
log.error(e)("db failure")
}*/
}
def performAvatarLogin0(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
@ -2163,7 +1934,7 @@ class AvatarActor(
if (originalFatigued && !isFatigued) {
avatar.implants.zipWithIndex.foreach {
case (Some(_), slot) =>
sessionActor ! SessionActor.SendResponse(AvatarImplantMessage(guid, ImplantAction.OutOfStamina, slot, 0))
sendAvatarImplantMessageToSelf(guid, ImplantAction.OutOfStamina, slot, value = 0)
case _ => ()
}
}
@ -2183,8 +1954,8 @@ class AvatarActor(
* 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.
* @param stamina an amount to drain
* @return `true`, as long as the requested amount of stamina can be drained in total;
* `false`, otherwise
* @return `false`, as long as the requested amount of stamina can be drained in total, or tif stamina equals zero;
* `true`, otherwise
*/
def consumeThisMuchStamina(stamina: Int): Boolean = {
if (stamina < 1) {
@ -2204,9 +1975,7 @@ class AvatarActor(
if (implant.active) {
deactivateImplant(implant.definition.implantType)
}
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 1)
)
sendAvatarImplantMessageToSelf(player.GUID, ImplantAction.OutOfStamina, slot, value = 1)
case _ => ()
}
}
@ -2214,7 +1983,8 @@ class AvatarActor(
} else if (becomeFatigued) {
avatarCopy(avatar.copy(implants = avatar.implants.zipWithIndex.collect {
case (Some(implant), slot) if implant.active =>
implantTimers.get(slot).foreach(_.cancel())
implantTimers.lift(slot).foreach(_.cancel())
implantTimers.update(slot, Default.Cancellable)
Some(implant.copy(active = false))
case (out, _) =>
out
@ -2224,119 +1994,6 @@ class AvatarActor(
}
}
def initializeImplants(): Unit = {
avatar.implants.zipWithIndex.foreach {
case (Some(implant), slot) =>
// TODO if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63
// for now, just write into slots 2, 3 and 4
sessionActor ! SessionActor.SendResponse(
CreateShortcutMessage(
session.get.player.GUID,
slot + 2,
Some(implant.definition.implantType.shortcut)
)
)
implantTimers.get(slot).foreach(_.cancel())
implantTimers(slot) = context.scheduleOnce(
implant.definition.InitializationDuration.seconds,
context.self,
SetImplantInitialized(implant.definition.implantType)
)
// Start client side initialization timer, visible on the character screen
// Progress accumulates according to the client's knowledge of the implant initialization time
// What is normally a 60s timer that is set to 120s on the server will still visually update as if 60s\
session.get.zone.AvatarEvents ! AvatarServiceMessage(
avatar.name,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 0))
)
case (None, _) => ()
}
}
def deinitializeImplants(): Unit = {
avatarCopy(avatar.copy(implants = avatar.implants.zipWithIndex.map {
case (Some(implant), slot) =>
if (implant.active) {
deactivateImplant(implant.definition.implantType)
}
if (implant.initialized) {
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, slot, 0)
)
)
}
Some(implant.copy(initialized = false, active = false))
case (None, _) => None
}))
}
def resetAnImplant(implantType: ImplantType): Unit = {
avatar.implants.zipWithIndex.find {
case (Some(imp), _) => imp.definition.implantType == implantType
case (None, _) => false
} match {
case Some((Some(imp), index)) =>
//deactivate
if (imp.active) {
deactivateImplant(implantType)
}
//deinitialize
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, index, 0)
)
)
avatarCopy(
avatar.copy(
implants = avatar.implants.updated(index, Some(imp.copy(initialized = false, active = false)))
)
)
//restart initialization process
implantTimers.get(index).foreach(_.cancel())
implantTimers(index) = context.scheduleOnce(
imp.definition.InitializationDuration.seconds,
context.self,
SetImplantInitialized(implantType)
)
session.get.zone.AvatarEvents ! AvatarServiceMessage(
avatar.name,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(index + 6, 0))
)
case _ => ()
}
}
def deactivateImplant(implantType: ImplantType): Unit = {
avatar.implants.zipWithIndex.collectFirst {
case (Some(implant), index) if implant.definition.implantType == implantType => (implant, index)
} match {
case Some((implant, slot)) =>
implantTimers.get(slot).foreach(_.cancel())
avatarCopy(
avatar.copy(
implants = avatar.implants.updated(slot, Some(implant.copy(active = false)))
)
)
// Deactivation sound / effect
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
AvatarAction.PlanetsideAttribute(session.get.player.GUID, 28, implant.definition.implantType.value * 2)
)
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Activation, slot, 0)
)
case None => log.error(s"requested deactivation of unknown implant $implantType")
}
}
/** Send list of avatars to client (show character selection screen) */
def sendAvatars(account: Account): Unit = {
import ctx._
@ -3115,9 +2772,7 @@ class AvatarActor(
)
.onComplete {
case Success(_) =>
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(pguid, ImplantAction.Remove, index, 0)
)
sendAvatarImplantMessageToSelf(pguid, ImplantAction.Remove, index, value = 0)
case Failure(exception) =>
log.error(exception)("db failure")
}
@ -3573,16 +3228,29 @@ class AvatarActor(
AvatarActor.basicLoginCertifications.diff(certs).foreach { learnCertificationInTheFuture }
}
def buyImplantAction(
terminalGuid: PlanetSideGUID,
definition: ImplantDefinition
): Unit = {
private def sendAvatarImplantMessageToSelf(
guid: PlanetSideGUID,
action: ImplantAction.Value,
index: Int,
value: Int
): Unit = {
import akka.actor.typed.scaladsl.adapter.TypedActorRefOps
import net.psforever.services.avatar.{AvatarResponse => RESP}
sessionActor.toClassic ! AvatarServiceResponse("", guid, RESP.AvatarImplant(action, index, value))
}
private 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)
findImplantByType(definition.implantType).foreach {
case (implant, slot) => updateAvatarForImplant(implant, slot, initializeImplant(implant.definition.InitializationDuration.seconds))
}
sessionActor ! SessionActor.CharSaved
case _ =>
sessionActor ! SessionActor.SendResponse(
@ -3591,7 +3259,7 @@ class AvatarActor(
}
}
def buyImplantInTheFuture(definition: ImplantDefinition): Future[Boolean] = {
private 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
@ -3604,14 +3272,7 @@ class AvatarActor(
.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
)
)
sendAvatarImplantMessageToSelf(session.get.player.GUID, ImplantAction.Add, index, definition.implantType.value)
out.completeWith(Future(true))
case Failure(exception) =>
log.error(exception)("db failure")
@ -3624,10 +3285,10 @@ class AvatarActor(
out.future
}
def sellImplantAction(
terminalGuid: PlanetSideGUID,
definition: ImplantDefinition
): Unit = {
private def sellImplantAction(
terminalGuid: PlanetSideGUID,
definition: ImplantDefinition
): Unit = {
sellImplantInTheFuture(definition).onComplete {
case Success(true) =>
sessionActor ! SessionActor.SendResponse(
@ -3641,7 +3302,7 @@ class AvatarActor(
}
}
def sellImplantInTheFuture(definition: ImplantDefinition): Future[Boolean] = {
private 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
@ -3657,10 +3318,8 @@ class AvatarActor(
)
.onComplete {
case Success(_) =>
replaceAvatar(avatar.copy(implants = avatar.implants.updated(index, None)))
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, index, 0)
)
updateAvatarForImplant(index)
sendAvatarImplantMessageToSelf(session.get.player.GUID, ImplantAction.Remove, index, value = 0)
out.completeWith(Future(true))
case Failure(exception) =>
log.error(exception)("db failure")
@ -3673,9 +3332,275 @@ class AvatarActor(
out.future
}
def removeAllImplants(): Unit = {
avatar.implants.collect { case Some(imp) => imp.definition }.foreach { sellImplantInTheFuture }
context.self ! ResetImplants()
private def findImplantByType(implantType: ImplantType): Option[(Implant, Int)] = {
avatar
.implants
.zipWithIndex
.collectFirst {
case (Some(implant), index) if implant.definition.implantType == implantType => (implant, index)
}
}
private def updateAvatarForImplant(
implant: Implant,
slot: Int,
implantFunc: (Implant, Int) => Implant
): Unit = {
avatarCopy(avatar.copy(implants = avatar.implants.updated(slot, Some(implantFunc(implant, slot)))))
}
private def updateAvatarForImplant(slot: Int): Unit = {
avatarCopy(avatar.copy(implants = avatar.implants.updated(slot, None)))
}
private def initializeImplants(): Unit = {
avatar.implants.zipWithIndex.foreach {
case (Some(implant), slot) =>
// TODO if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63
// for now, just write into slots 2, 3 and 4
sessionActor ! SessionActor.SendResponse(
CreateShortcutMessage(
session.get.player.GUID,
slot + 2,
Some(implant.definition.implantType.shortcut)
)
)
initializeImplant(implant.definition.InitializationDuration.seconds)(implant, slot)
case (None, _) => ()
}
}
private def reinitializeImplant(implantType: ImplantType): Unit = {
findImplantByType(implantType).collect {
case (implant, slot) if implant.active =>
updateAvatarForImplant(deactivateImplant(implant, slot), slot, reinitializeImplant)
case (implant, slot) =>
updateAvatarForImplant(implant, slot, reinitializeImplant)
}
}
private def reinitializeImplant(implant: Implant, slot: Int): Implant = {
//deinitialize
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
AvatarAction.AvatarImplant(session.get.player.GUID, ImplantAction.Initialization, slot, 0)
)
initializeImplant(implant.definition.InitializationDuration.seconds)(implant, slot)
}
private def initializeImplant(delay: FiniteDuration)(implant: Implant, slot: Int): Implant = {
//start initialization process
setImplantInitializedTimer(implant, slot, delay)
// Start client side initialization timer, visible on the character screen
// Progress accumulates according to the client's knowledge of the implant initialization time
// What is normally a 60s timer that is set to 120s on the server will still visually update as if 60s\
session.get.zone.AvatarEvents ! AvatarServiceMessage(
avatar.name,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 0))
)
implant.copy(initialized = false, active = false)
}
private def deinitializeImplants(): Unit = {
avatarCopy(avatar.copy(implants = avatar
.implants
.zipWithIndex
.collect {
case (Some(implant), slot) if implant.active =>
Some(deinitializeImplant(deactivateImplant(implant, slot), slot))
case (Some(implant), slot) if implant.initialized =>
Some(deinitializeImplant(implant, slot))
case (implantOpt, _) =>
implantOpt
}
))
}
private def deinitializeImplant(implant: Implant, slot: Int): Implant = {
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
AvatarAction.AvatarImplant(session.get.player.GUID, ImplantAction.Initialization, slot, 0)
)
implant.copy(initialized = false, active = false)
}
private def setImplantInitialized(implantType: ImplantType): Unit = {
findImplantByType(implantType)
.collect {
case (implant, slot) =>
sendAvatarImplantMessageToSelf(session.get.player.GUID, ImplantAction.Initialization, slot, value = 1)
avatarCopy(avatar.copy(implants = avatar.implants.map {
case Some(implant)
if implant.definition.implantType == implantType && implant.definition.Passive =>
activateImplantPackets(implant, slot)
Some(implant.copy(initialized = true, active = true))
case Some(implant)
if implant.definition.implantType == implantType =>
Some(implant.copy(initialized = true))
case other => other
}))
Some(implant)
}
.orElse {
log.error(s"set initialized called for unknown implant $implantType")
None
}
}
private def setImplantInitializedTimer(implant: Implant, slot: Int, delay: FiniteDuration): Unit = {
implantTimers.lift(slot).foreach(_.cancel())
implantTimers.update(slot, context.scheduleOnce(
delay,
context.self,
SetImplantInitialized(implant.definition.implantType)
))
}
private def deactivateImplant(implantType: ImplantType): Unit = {
avatar.implants.zipWithIndex.collectFirst {
case (Some(implant), index) if implant.definition.implantType == implantType => (implant, index)
} match {
case Some((implant, slot)) =>
updateAvatarForImplant(implant, slot, deactivateImplant)
case None =>
log.error(s"requested deactivation of unknown implant $implantType")
}
}
private def deactivateActiveImplants(): Unit = {
avatar
.implants
.zipWithIndex
.collect {
case (Some(implant), slot) if implant.active && !implant.definition.Passive =>
updateAvatarForImplant(implant, slot, deactivateImplant)
}
}
private def deactivateImplant(implant: Implant, slot: Int): Implant = {
implantTimers.lift(slot).foreach(_.cancel())
implantTimers.update(slot, Default.Cancellable)
// Deactivation sound / effect
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
AvatarAction.PlanetsideAttribute(session.get.player.GUID, 28, implant.definition.implantType.value * 2)
)
sendAvatarImplantMessageToSelf(session.get.player.GUID, ImplantAction.Activation, slot, value = 0)
implant.copy(active = false)
}
private def activateImplant(implantType: ImplantType): Unit = {
findImplantByType(implantType)
.collect { case (implant, slot) =>
activateImplant(implant, slot)
Some(true)
}
.orElse {
log.error(s"requested activation of unknown implant $implantType")
None
}
}
private def activateImplant(implant: Implant, slot: Int): Unit = {
if (!implant.initialized) {
log.warn(s"requested activation of uninitialized implant ${implant.definition.implantType}")
} else if (
!consumeThisMuchStamina(implant.definition.ActivationStaminaCost) ||
avatar.stamina < implant.definition.StaminaCost
) {
// not enough stamina to activate
} else if (implant.definition.implantType.disabledFor.contains(session.get.player.ExoSuit)) {
// TODO can this really happen? can we prevent it?
} else {
avatarCopy(
avatar.copy(
implants = avatar.implants.updated(slot, Some(implant.copy(active = true)))
)
)
activateImplantPackets(implant, slot)
implantTimers.lift(slot).foreach(_.cancel())
val interval = implant.definition.GetCostIntervalByExoSuit(session.get.player.ExoSuit).milliseconds
if (interval.toMillis > 0) {
val stopConditionTest: (Implant, Player) => Boolean = implant.definition.implantType match {
case ImplantType.AdvancedRegen => staminaDrainByIntervalAdvancedRegen
case _ => staminaDrainByIntervalSomeImplant
}
val stopConditionFunc: () => Unit = staminaDrainByIntervalOngoing(implant, slot, session.get.player, stopConditionTest)
implantTimers.update(slot, context.system.scheduler.scheduleWithFixedDelay(interval, interval)(() => stopConditionFunc()))
}
}
}
private def staminaDrainByIntervalOngoing(
implant: Implant,
slot: Int,
player: Player,
func: (Implant, Player) => Boolean
)(): Unit = {
if (func(implant, player)) {
updateAvatarForImplant(implant, slot, deactivateImplant)
}
}
private def staminaDrainByIntervalSomeImplant(implant: Implant, player: Player): Boolean = {
!player.isAlive || !consumeThisMuchStamina(implant.definition.StaminaCost)
}
private def staminaDrainByIntervalAdvancedRegen(implant: Implant, player: Player): Boolean = {
// for every 1hp: 2sp (running), 1.5sp (standing), 1sp (crouched)
// to simulate '1.5sp (standing)', find if 0.0...1.0 * 100 is an even number
val cost = implant.definition.StaminaCost -
(if (player.Crouching || (!player.isMoving && (math.random() * 100) % 2 == 1)) 1 else 0)
val aliveAndWounded = player.isAlive && player.Health < player.MaxHealth
if (aliveAndWounded && consumeThisMuchStamina(cost)) {
//heal
val originalHealth = player.Health
val zone = player.Zone
val guid = player.GUID
val newHealth = player.Health = originalHealth + 1
val events = zone.AvatarEvents
player.LogActivity(HealFromImplant(implant.definition.implantType, 1))
events ! AvatarServiceMessage(
zone.id,
AvatarAction.PlanetsideAttributeToAll(guid, 0, newHealth)
)
false
} else {
!aliveAndWounded
}
}
private def activateImplantPackets(implant: Implant, slot: Int): Unit = {
sendAvatarImplantMessageToSelf(session.get.player.GUID, ImplantAction.Activation, slot, value = 1)
// Activation sound / effect
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
AvatarAction.PlanetsideAttribute(
session.get.player.GUID,
28,
implant.definition.implantType.value * 2 + 1
)
)
}
private def softResetImplants() : Unit = {
avatarCopy(
avatar.copy(implants = avatar
.implants
.zipWithIndex
.map {
case (Some(implant), slot) if implant.active && !implant.definition.Passive =>
//deactivate active non-passive implant
Some(deactivateImplant(implant, slot))
case (Some(implant), slot) if !implant.initialized && implantTimers(slot).isCancelled =>
//restart stopped/unstarted initialization process
Some(reinitializeImplant(implant, slot))
case (implantOpt, _) =>
//fine as is
implantOpt
}
)
)
}
def resetSupportExperienceTimer(previousBep: Long, previousDelay: Long): Unit = {

View file

@ -3,6 +3,8 @@ package net.psforever.actors.session.normal
import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.support.AvatarHandlerFunctions
import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction}
import net.psforever.types.ImplantType
import scala.concurrent.duration._
//
@ -155,6 +157,37 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
}
}
case AvatarResponse.AvatarImplant(ImplantAction.Add, implant_slot, value)
if value == ImplantType.SecondWind.value =>
sendResponse(AvatarImplantMessage(resolvedPlayerGuid, ImplantAction.Add, implant_slot, 7))
//second wind does not normally load its icon into the shortcut hotbar
avatar
.shortcuts
.zipWithIndex
.find { case (s, _) => s.isEmpty}
.foreach { case (_, index) =>
sendResponse(CreateShortcutMessage(resolvedPlayerGuid, index + 1, Some(ImplantType.SecondWind.shortcut)))
}
case AvatarResponse.AvatarImplant(ImplantAction.Remove, implant_slot, value)
if value == ImplantType.SecondWind.value =>
sendResponse(AvatarImplantMessage(resolvedPlayerGuid, ImplantAction.Remove, implant_slot, value))
//second wind does not normally unload its icon from the shortcut hotbar
val shortcut = {
val imp = ImplantType.SecondWind.shortcut
net.psforever.objects.avatar.Shortcut(imp.code, imp.tile) //case class
}
avatar
.shortcuts
.zipWithIndex
.find { case (s, _) => s.contains(shortcut) }
.foreach { case (_, index) =>
sendResponse(CreateShortcutMessage(resolvedPlayerGuid, index + 1, None))
}
case AvatarResponse.AvatarImplant(action, implant_slot, value) =>
sendResponse(AvatarImplantMessage(resolvedPlayerGuid, action, implant_slot, value))
case AvatarResponse.ObjectHeld(slot, _)
if isSameTarget && player.VisibleSlots.contains(slot) =>
sendResponse(ObjectHeldMessage(guid, slot, unk1=true))

View file

@ -40,7 +40,7 @@ import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones.{Zone, ZoneProjectile, Zoning}
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.objectcreate.ObjectClass
import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BindStatus, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, ItemTransactionMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BindStatus, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, ItemTransactionMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, Shortcut, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.RemoverActor
import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@ -117,9 +117,10 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
}
}
ops.fallHeightTracker(pos.z)
// if (isCrouching && !player.Crouching) {
// //dev stuff goes here
// }
if (isCrouching && !player.Crouching) {
//dev stuff goes here
sendResponse(CreateShortcutMessage(player.GUID, 2, Some(Shortcut.Implant("second_wind"))))
}
player.Position = pos
player.Velocity = vel
player.Orientation = Vector3(player.Orientation.x, pitch, yaw)

View file

@ -450,7 +450,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
val playerGuid: PlanetSideGUID = tplayer.GUID
val objGuid: PlanetSideGUID = obj.GUID
sessionLogic.actionsToCancel()
avatarActor ! AvatarActor.DeactivateActiveImplants()
avatarActor ! AvatarActor.DeactivateActiveImplants
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum))
continent.VehicleEvents ! VehicleServiceMessage(

View file

@ -17,7 +17,7 @@ import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity}
import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, MailMessage, ObjectDetectedMessage, SessionStatistic}
import net.psforever.packet.game.{AvatarImplantMessage, CampaignStatistic, ChangeFireStateMessage_Start, ImplantAction, MailMessage, ObjectDetectedMessage, SessionStatistic}
import net.psforever.services.chat.DefaultChannel
import scala.collection.mutable
@ -918,7 +918,7 @@ class ZoningOperations(
def beginZoningCountdown(runnable: Runnable): Unit = {
val descriptor = zoningType.toString.toLowerCase
if (zoningStatus == Zoning.Status.Request) {
avatarActor ! AvatarActor.DeactivateActiveImplants()
avatarActor ! AvatarActor.DeactivateActiveImplants
zoningStatus = Zoning.Status.Countdown
val (time, origin) = ZoningStartInitialMessageAndTimer()
zoningCounter = time
@ -2047,8 +2047,6 @@ class ZoningOperations(
sessionLogic.persist = UpdatePersistenceAndRefs
tplayer.avatar = avatar
session = session.copy(player = tplayer)
avatarActor ! AvatarActor.CreateImplants()
avatarActor ! AvatarActor.InitializeImplants()
//LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar
val weaponsEnabled = !(mapName.equals("map11") || mapName.equals("map12") || mapName.equals("map13"))
sendResponse(LoadMapMessage(mapName, id, 40100, 25, weaponsEnabled, map.checksum))
@ -2264,7 +2262,7 @@ class ZoningOperations(
val armor = player.Armor
val events = continent.VehicleEvents
val zoneid = continent.id
avatarActor ! AvatarActor.ResetImplants()
avatarActor ! AvatarActor.SoftResetImplants
player.Spawn()
if (health != 0) {
player.Health = health
@ -2492,7 +2490,7 @@ class ZoningOperations(
// workaround to make sure player is spawned with full stamina
player.avatar = player.avatar.copy(stamina = avatar.maxStamina)
avatarActor ! AvatarActor.RestoreStamina(avatar.maxStamina)
avatarActor ! AvatarActor.ResetImplants()
avatarActor ! AvatarActor.DeinitializeImplants
zones.exp.ToDatabase.reportRespawns(tplayer.CharId, ScoreCard.reviveCount(player.avatar.scorecard.CurrentLife))
val obj = Player.Respawn(tplayer)
DefinitionUtil.applyDefaultLoadout(obj)
@ -2794,9 +2792,9 @@ class ZoningOperations(
// new player is spawning
val newPlayer = RespawnClone(player)
newPlayer.LogActivity(SpawningActivity(PlayerSource(newPlayer), toZoneNumber, toSpawnPoint))
LoadZoneAsPlayUsing(newPlayer, pos, ori, toSide, zoneId)
LoadZoneAsPlayerUsing(newPlayer, pos, ori, toSide, zoneId)
} else {
avatarActor ! AvatarActor.DeactivateActiveImplants()
avatarActor ! AvatarActor.DeactivateActiveImplants
val betterSpawnPoint = physSpawnPoint.collect { case o: PlanetSideGameObject with FactionAffinity with InGameHistory => o }
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case Some(vehicle: Vehicle) => // driver or passenger in vehicle using a warp gate, or a droppod
@ -2813,11 +2811,11 @@ class ZoningOperations(
AvatarAction.ObjectDelete(player_guid, player_guid, 4)
)
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneAsPlayUsing(player, pos, ori, toSide, zoneId)
LoadZoneAsPlayerUsing(player, pos, ori, toSide, zoneId)
case _ => //player is logging in
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneAsPlayUsing(player, pos, ori, toSide, zoneId)
LoadZoneAsPlayerUsing(player, pos, ori, toSide, zoneId)
}
}
}
@ -2831,7 +2829,7 @@ class ZoningOperations(
* @param onThisSide description of the containing environment
* @param goingToZone common designation for the zone
*/
private def LoadZoneAsPlayUsing(
private def LoadZoneAsPlayerUsing(
target: Player,
position: Vector3,
orientation: Vector3,
@ -2958,9 +2956,22 @@ class ZoningOperations(
tplayer.Actor ! JammableUnit.ClearJammeredStatus()
tplayer.Actor ! JammableUnit.ClearJammeredSound()
}
avatarActor ! AvatarActor.SoftResetImplants
tavatar.implants.zipWithIndex.collect {
case (Some(implant), slot) if !implant.initialized =>
sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 0))
}
// tavatar.implants.zipWithIndex.collect {
// case (Some(implant), slot) if implant.active && implant.definition.Passive =>
// sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1))
// sendResponse(AvatarImplantMessage(guid, ImplantAction.Activation, slot, 1))
// case (Some(implant), slot) if implant.initialized =>
// sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1))
// case (Some(implant), _) =>
// () //avatarActor ! AvatarActor.ResetImplant(implant.definition.implantType)
// }
val originalDeadState = deadState
deadState = DeadState.Alive
avatarActor ! AvatarActor.ResetImplants()
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
initializeShortcutsAndBank(guid, tavatar.shortcuts)
//Favorites lists
@ -3588,10 +3599,7 @@ class ZoningOperations(
def startDeconstructing(obj: SpawnTube): Unit = {
log.info(s"${player.Name} is deconstructing at the ${obj.Owner.Definition.Name}'s spawns")
avatar.implants.collect {
case Some(implant) if implant.active && !implant.definition.Passive =>
avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
}
avatarActor ! AvatarActor.DeactivateActiveImplants
if (player.ExoSuit != ExoSuitType.MAX) {
player.Actor ! PlayerControl.ObjectHeld(Player.HandsDownSlot, updateMyHolsterArm = true)
}

View file

@ -476,7 +476,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
Deployables.initializeConstructionItem(player.avatar.certifications, citem)
}
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants()
avatarActor ! AvatarActor.DeactivateActiveImplants
val zone = player.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
@ -659,7 +659,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
afterHolsters.foreach(elem => player.Slot(elem.start).Equipment = elem.obj)
afterInventory.foreach(elem => player.Inventory.InsertQuickly(elem.start, elem.obj))
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants()
avatarActor ! AvatarActor.DeactivateActiveImplants
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Zone.id,
AvatarAction.ChangeExosuit(
@ -944,7 +944,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
CancelJammeredSound(target)
super.CancelJammeredStatus(target)
//uninitialize implants
avatarActor ! AvatarActor.DeinitializeImplants()
avatarActor ! AvatarActor.DeinitializeImplants
//log historical event
target.LogActivity(cause)
@ -1073,13 +1073,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
* @param dur the duration of the timer, in milliseconds
*/
override def StartJammeredStatus(target: Any, dur: Int): Unit = {
avatarActor ! AvatarActor.DeinitializeImplants()
avatarActor ! AvatarActor.DeinitializeImplants
avatarActor ! AvatarActor.SuspendStaminaRegeneration(5 seconds)
super.StartJammeredStatus(target, dur)
}
override def CancelJammeredStatus(target: Any): Unit = {
avatarActor ! AvatarActor.InitializeImplants()
avatarActor ! AvatarActor.SoftResetImplants
super.CancelJammeredStatus(target)
}

View file

@ -83,5 +83,6 @@ class ImplantDefinition(val implantType: ImplantType) extends BasicDefinition {
def GetCostIntervalByExoSuit(exosuit: ExoSuitType.Value): Int =
costIntervalByExoSuit.getOrElse(exosuit, CostIntervalDefault)
def CostIntervalByExoSuitHashMap: mutable.Map[ExoSuitType.Value, Int] = costIntervalByExoSuit
}

View file

@ -13,25 +13,14 @@ import shapeless.{::, HNil}
* The parameters `purpose` and `tile` are closely related.
* These two fields are consistent for all shortcuts of the same type.
* `purpose` indicates the purpose of the shortcut.
* The medkit icon is 0, chat shortcuts are 1, and implants are 2.
* `tile` is related to what kind of graphic is displayed in this shortcut's slot on the hotbar based on its purpose.
* The parameters `effect1` and `effect2` are exclusive to text macro shortcuts and are defaulted to empty `String`s.<br>
* The medkit tile use "medkit", chat shortcuts use "shortcut_macro", and implants are the internal name of the implant.<br>
* <br>
* The parameters `effect1` and `effect2` are exclusive to text macro shortcuts and are defaulted to empty `String`s.
* The `shortcut_macro` setting displays a word bubble superimposed by the (first three letters of) `effect1` text.<br>
* Implants and the medkit should have self-explanatory graphics.
* <br>
* Tile - Code<br>
* `advanced_regen` (regeneration) - 2<br>
* `audio_amplifier` - 2<br>
* `darklight_vision` - 2<br>
* `medkit` - 0<br>
* `melee_booster` - 2<br>
* `personal_shield` - 2<br>
* `range_magnifier` - 2<br>
* `second_wind` - 2<br>
* `shortcut_macro` - 1<br>
* `silent_run` (sensor shield) - 2<br>
* `surge` - 2<br>
* `targeting` (enhanced targeting) - 2
* The implant second wind does not have a graphic shortcut icon.
* @param code the primary use of this shortcut
*/
abstract class Shortcut(val code: Int) {

View file

@ -39,6 +39,10 @@ class AvatarService(zone: Zone) extends Actor {
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ArmorChanged(suit, subtype))
)
case AvatarAction.AvatarImplant(player_guid, action, implantSlot, status) =>
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.AvatarImplant(action, implantSlot, status))
)
case AvatarAction.ChangeAmmo(
player_guid,
weapon_guid,

View file

@ -11,6 +11,7 @@ import net.psforever.objects.serverobject.environment.interaction.common.Watery.
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.zones.Zone
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.ImplantAction
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent}
import net.psforever.types.{ExoSuitType, ExperienceType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
@ -27,6 +28,7 @@ object AvatarAction {
sealed trait Action
final case class ArmorChanged(player_guid: PlanetSideGUID, suit: ExoSuitType.Value, subtype: Int) extends Action
final case class AvatarImplant(player_guid: PlanetSideGUID, action: ImplantAction.Value, implantSlot: Int, status: Int) extends Action
final case class ChangeAmmo(
player_guid: PlanetSideGUID,
weapon_guid: PlanetSideGUID,

View file

@ -10,7 +10,7 @@ import net.psforever.objects.serverobject.environment.interaction.common.Watery.
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.objectcreate.ConstructorData
import net.psforever.packet.game.ObjectCreateMessage
import net.psforever.packet.game.{ImplantAction, ObjectCreateMessage}
import net.psforever.types.{ExoSuitType, ExperienceType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
import net.psforever.services.GenericEventBusMsg
@ -24,6 +24,7 @@ object AvatarResponse {
sealed trait Response
final case class ArmorChanged(suit: ExoSuitType.Value, subtype: Int) extends Response
final case class AvatarImplant(action: ImplantAction.Value, implantSlot: Int, status: Int) extends Response
final case class ChangeAmmo(
weapon_guid: PlanetSideGUID,
weapon_slot: Int,

View file

@ -544,8 +544,8 @@ class PlayerControlDeathStandingTest extends ActorTest {
)
assert(
msg_stamina match {
case AvatarActor.DeinitializeImplants() => true
case _ => false
case AvatarActor.DeinitializeImplants => true
case _ => false
}
)
assert(
@ -685,8 +685,8 @@ class PlayerControlDeathStandingTest extends ActorTest {
// activityProbe.expectNoMessage(200 milliseconds)
// assert(
// msg_stamina match {
// case AvatarActor.DeinitializeImplants() => true
// case _ => false
// case AvatarActor.DeinitializeImplants => true
// case _ => false
// }
// )
// assert(