Stamina / Implants (#485)

* removing stamina update business logic from the Player class

* I really need to sort this out

* implant changes: deactivate when changing armor or loadouts and when zoning; extra details for state management when being jammed or fatigued upon loading

* merge with master

* pull rebase on master; moved implant learning/forgetting to PlayerControl, but not yet completely tested

* unhandled case of no implant in a slot during avatar setup

* complete implant deactivation optional?

* moved reference to player control agency for matters concerning stamina updates on damage taken

* - > +

* crouching makes nothing better

* PlayerControl now handles stamina regeneration; handling a case where being fatigued because activation charge is too much leaves the drain timer running

* no more stamina drain message; moving functionality into the class (out of the object) for expediency; handling implant uninitialization differently upon death

* test repairs; redundant messages in player damage and player death logic

* no jumping; riders get a free pass

* making the code uglier
This commit is contained in:
Fate-JH 2020-06-10 09:27:12 -04:00 committed by GitHub
parent 3ea51d404e
commit 181fdb9c84
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 737 additions and 536 deletions

View file

@ -202,7 +202,9 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet
implants.foreach(slot => {
slot.Installed match {
case Some(_) =>
slot.Active = false
slot.Initialized = false
slot.InitializeTime = 0L
case None => ;
}
})
@ -302,7 +304,7 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet
def ObjectTypeNameReference(id : Long) : String = {
objectTypeNameReference.get(id) match {
case Some(name) => name
case Some(objectName) => objectName
case None => ""
}
}

View file

@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import akka.actor.Cancellable
import net.psforever.objects.definition.ImplantDefinition
import net.psforever.types.{ExoSuitType, ImplantType}
@ -19,19 +18,18 @@ class ImplantSlot {
private var unlocked : Boolean = false
/** whether this implant is ready for use */
private var initialized : Boolean = false
/** a cancellable timer that can be used to set an implant as initialized once complete */
private var initializeTimer: Cancellable = Default.Cancellable
/** */
private var initializeTime : Long = 0L
/** is this implant active */
private var active : Boolean = false
/** what implant is currently installed in this slot; None if there is no implant currently installed */
private var implant : Option[ImplantDefinition] = None
def InitializeTimer : Cancellable = initializeTimer
def InitializeTime : Long = initializeTime
def InitializeTimer_=(timer : Cancellable) : Cancellable = {
initializeTimer = timer
initializeTimer
def InitializeTime_=(time : Long) : Long = {
initializeTime = time
InitializeTime
}
def Unlocked : Boolean = unlocked

View file

@ -1,9 +1,8 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import akka.actor.ActorRef
import net.psforever.objects.avatar.LoadoutManager
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, ImplantDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
import net.psforever.objects.serverobject.PlanetSideServerObject
@ -124,11 +123,6 @@ class Player(private val core : Avatar) extends PlanetSideServerObject
def Stamina_=(assignStamina : Int) : Int = {
stamina = if(isAlive) { math.min(math.max(0, assignStamina), MaxStamina) } else { 0 }
if(Actor != Default.Actor) {
Actor ! Player.StaminaChanged(Stamina)
}
Stamina
}
@ -374,6 +368,10 @@ class Player(private val core : Avatar) extends PlanetSideServerObject
core.Implants.takeWhile(_.Unlocked).map( implant => { (implant.Implant, implant.MaxTimer, implant.Active) })
}
def InstallImplant(implant : ImplantDefinition) : Option[Int] = core.InstallImplant(implant)
def UninstallImplant(implant : ImplantType.Value) : Option[Int] = core.UninstallImplant(implant)
def ResetAllImplants() : Unit = core.ResetAllImplants()
def FacingYawUpper : Float = facingYawUpper
@ -674,8 +672,12 @@ object Player {
final case class ImplantInitializationStart(slot : Int)
final case class UninitializeImplant(slot : Int)
final case class ImplantInitializationComplete(slot : Int)
final case class DrainStamina(amount : Int)
final case class StaminaChanged(currentStamina : Int)
final case class StaminaRegen()
final case class StaminaChanged(currentStamina : Option[Int] = None)
object StaminaChanged {
def apply(amount : Int) : StaminaChanged = StaminaChanged(Some(amount))
}
def apply(core : Avatar) : Player = {
new Player(core)

View file

@ -1,10 +1,9 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.avatar
import akka.actor.{Actor, ActorRef, Props}
import net.psforever.objects._
import akka.actor.{Actor, ActorRef, Cancellable, Props}
import net.psforever.objects.{Player, _}
import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile}
import net.psforever.objects.definition.ImplantDefinition
import net.psforever.objects.equipment._
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
import net.psforever.objects.loadouts.Loadout
@ -39,9 +38,14 @@ class PlayerControl(player : Player) extends Actor
private [this] val log = org.log4s.getLogger(player.Name)
private [this] val damageLog = org.log4s.getLogger(Damageable.LogChannel)
// A collection of timers for each slot to trigger stamina drain on an interval
val implantSlotStaminaDrainTimers = mutable.HashMap(0 -> Default.Cancellable, 1 -> Default.Cancellable, 2 -> Default.Cancellable)
// control agency for the player's locker container (dedicated inventory slot #5)
/** Stamina will be used. Stamina will be restored. */
var staminaRegen : Cancellable = Default.Cancellable
/**
* A collection of timers indexed for the implant in each slot.
* Before an implant is ready, it serves as the initialization timer.
* After being initialized, it is used as the stamina drain interval when the implant is active. */
val implantSlotTimers = mutable.HashMap(0 -> Default.Cancellable, 1 -> Default.Cancellable, 2 -> Default.Cancellable)
/** control agency for the player's locker container (dedicated inventory slot #5) */
val lockerControlAgent : ActorRef = {
val locker = player.Locker
locker.Zone = player.Zone
@ -51,106 +55,55 @@ class PlayerControl(player : Player) extends Actor
override def postStop() : Unit = {
lockerControlAgent ! akka.actor.PoisonPill
player.Locker.Actor = Default.Actor
implantSlotStaminaDrainTimers.values.foreach { _.cancel }
staminaRegen.cancel
implantSlotTimers.values.foreach { _.cancel }
}
def receive : Receive = jammableBehavior
.orElse(takesDamage)
.orElse(containerBehavior)
.orElse {
case Player.ImplantActivation(slot: Int, status : Int) =>
// todo: disable implants with stamina cost when changing armour type
val implantSlot = player.ImplantSlot(slot)
case Player.ImplantActivation(slot : Int, status : Int) =>
ImplantActivation(slot, status)
// Allow uninitialized implants to be deactivated in case they're stuck in a state where they are no longer active or initialized but still draining stamina (e.g. by EMP)
if(status == 0 && (implantSlot.Active || !implantSlot.Initialized)) {
// Cancel stamina drain timer
implantSlotStaminaDrainTimers(slot).cancel()
implantSlotStaminaDrainTimers(slot) = Default.Cancellable
case Player.UninitializeImplant(slot : Int) =>
UninitializeImplant(slot)
implantSlot.Active = false
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.DeactivateImplantSlot(player.GUID, slot))
} else if (status == 1 && implantSlot.Initialized && !player.Fatigued) {
implantSlot.Installed match {
case Some(implant: ImplantDefinition) =>
if(implantSlot.Active) {
// Some events such as zoning will reset the implant on the client side without sending a deactivation packet
// But the implant will remain in an active state server side. For now, allow reactivation of the implant.
// todo: Deactivate implants server side when actions like zoning happen. (Other actions?)
log.warn(s"Implant $slot is already active, but activating again")
implantSlotStaminaDrainTimers(slot).cancel()
implantSlotStaminaDrainTimers(slot) = Default.Cancellable
}
implantSlot.Active = true
case Player.ImplantInitializationStart(slot : Int) =>
ImplantInitializationStart(slot)
if (implant.ActivationStaminaCost >= 0) {
player.Stamina -= implant.ActivationStaminaCost // Activation stamina drain
}
case Player.ImplantInitializationComplete(slot : Int) =>
ImplantInitializationComplete(slot)
if(implant.StaminaCost > 0 && implant.GetCostIntervalByExoSuit(player.ExoSuit) > 0) { // Ongoing stamina drain, if applicable
implantSlotStaminaDrainTimers(slot) = context.system.scheduler.scheduleWithFixedDelay(0 seconds, implant.GetCostIntervalByExoSuit(player.ExoSuit) milliseconds, self, Player.DrainStamina(implant.StaminaCost))
}
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2 + 1)) // Activation sound / effect
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.ActivateImplantSlot(player.GUID, slot))
case _ => ;
}
}
else {
log.warn(s"Can't handle ImplantActivation: Player GUID: ${player.GUID} Slot $slot Status: $status Initialized: ${implantSlot.Initialized} Active: ${implantSlot.Active} Fatigued: ${player.Fatigued}")
case Player.StaminaRegen() =>
if(staminaRegen == Default.Cancellable) {
staminaRegen.cancel
staminaRegen = context.system.scheduler.scheduleOnce(delay = 500 milliseconds, self, PlayerControl.StaminaRegen())
}
case Player.UninitializeImplant(slot: Int) =>
PlayerControl.UninitializeImplant(player, slot)
case Player.ImplantInitializationStart(slot: Int) =>
val implantSlot = player.ImplantSlot(slot)
if(implantSlot.Installed.isDefined) {
if(implantSlot.Initialized) {
PlayerControl.UninitializeImplant(player, slot)
case PlayerControl.StaminaRegen() =>
staminaRegen.cancel
if (player.isAlive) {
if (player.skipStaminaRegenForTurns > 0) {
// Do not renew stamina for a while
player.skipStaminaRegenForTurns -= 1
}
// Start client side initialization timer
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(player.GUID, ActionProgressMessage(slot + 6, 0)))
// Callback after initialization timer to complete initialization
implantSlot.InitializeTimer = context.system.scheduler.scheduleOnce(implantSlot.MaxTimer seconds, self, Player.ImplantInitializationComplete(slot))
}
case Player.ImplantInitializationComplete(slot: Int) =>
val implantSlot = player.ImplantSlot(slot)
if(implantSlot.Installed.isDefined) {
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.Initialization, slot, 1)))
implantSlot.Initialized = true
if(implantSlot.InitializeTimer != Default.Cancellable) {
implantSlot.InitializeTimer.cancel()
implantSlot.InitializeTimer = Default.Cancellable
else if ((player.VehicleSeated.nonEmpty || !player.isMoving && !player.Jumping) && player.Stamina < player.MaxStamina) {
// Regen stamina roughly every 500ms
StaminaChanged(changeInStamina = 1)
}
}
staminaRegen = context.system.scheduler.scheduleOnce(delay = 500 milliseconds, self, PlayerControl.StaminaRegen())
case Player.DrainStamina(amount : Int) =>
player.Stamina -= amount
case Player.StaminaChanged(Some(changeInStamina)) =>
StaminaChanged(changeInStamina)
case Player.StaminaChanged(currentStamina : Int) =>
if(currentStamina == 0) {
player.Fatigued = true
player.skipStaminaRegenForTurns += 4
player.Implants.indices.foreach { slot => // Disable all implants
self ! Player.ImplantActivation(slot, 0)
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponseTargeted(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 1)))
}
} else if (player.Fatigued && currentStamina >= 20) {
player.Fatigued = false
player.Implants.indices.foreach { slot => // Re-enable all implants
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponseTargeted(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 0)))
}
}
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttributeSelf(player.GUID, 2, player.Stamina))
case Player.StaminaChanged(None) =>
UpdateStamina()
case Player.Die() =>
if(player.isAlive) {
PlayerControl.DestructionAwareness(player, None)
DestructionAwareness(player, None)
}
case CommonMessages.Use(user, Some(item : Tool)) if item.Definition == GlobalDefinitions.medicalapplicator && player.isAlive =>
@ -308,6 +261,13 @@ class PlayerControl(player : Player) extends Actor
stow.foreach { elem =>
player.Inventory.InsertQuickly(elem.start, elem.obj)
}
//deactivate non-passive implants
implantSlotTimers.keys.foreach { index =>
val implantSlot = player.ImplantSlot(index)
if(implantSlot.Installed.nonEmpty && implantSlot.Active && (implantSlot.Charge(originalSuit) > 0 || implantSlot.Charge(exosuit) > 0)) {
ImplantActivation(index, status = 0)
}
}
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id,
AvatarAction.ChangeExosuit(player.GUID, exosuit, subtype, player.LastDrawnSlot, exosuit == ExoSuitType.MAX && requestToChangeArmor,
beforeHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterHolsters,
@ -431,6 +391,13 @@ class PlayerControl(player : Player) extends Actor
}
(afterHolsters ++ afterInventory).foreach { entry => entry.obj.Faction = player.Faction }
toDeleteOrDrop.foreach { entry => entry.obj.Faction = PlanetSideEmpire.NEUTRAL }
//deactivate non-passive implants
implantSlotTimers.keys.foreach { index =>
val implantSlot = player.ImplantSlot(index)
if(implantSlot.Installed.nonEmpty && implantSlot.Active && (implantSlot.Charge(originalSuit) > 0 || implantSlot.Charge(nextSuit) > 0)) {
ImplantActivation(index, status = 0)
}
}
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id,
AvatarAction.ChangeLoadout(player.GUID, nextSuit, nextSubtype, player.LastDrawnSlot, exosuit == ExoSuitType.MAX,
oldHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterHolsters,
@ -438,6 +405,92 @@ class PlayerControl(player : Player) extends Actor
)
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true))
case Terminal.LearnImplant(implant) =>
val zone = player.Zone
val events = zone.AvatarEvents
val playerChannel = player.Name
val terminal_guid = msg.terminal_guid
val implant_type = implant.Type
val message = s"wants to learn $implant_type"
val (interface, slotNumber) = player.VehicleSeated match {
case Some(mech_guid) =>
(
zone.Map.TerminalToInterface.get(mech_guid.guid),
if(!player.Implants.exists({ case (implantType, _, _) => implantType == implant_type })) {
//no duplicates
player.InstallImplant(implant)
}
else {
None
}
)
case _ =>
(None, None)
}
val result = if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) {
val slot = slotNumber.get
log.info(s"$message - put in slot $slot")
events ! AvatarServiceMessage(playerChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID, AvatarImplantMessage(player.GUID, ImplantAction.Add, slot, implant_type.id)))
ImplantInitializationStart(slot)
true
}
else {
if(interface.isEmpty) {
log.warn(s"$message - not interacting with a terminal")
}
else if(!interface.contains(terminal_guid.guid)) {
log.warn(s"$message - interacting with the wrong terminal, ${interface.get}")
}
else if(slotNumber.isEmpty) {
log.warn(s"$message - already knows that implant")
}
else {
log.warn(s"$message - forgot to sit at a terminal")
}
false
}
events ! AvatarServiceMessage(playerChannel, AvatarAction.TerminalOrderResult(terminal_guid, msg.transaction_type, result))
case Terminal.SellImplant(implant) =>
val zone = player.Zone
val events = zone.AvatarEvents
val playerChannel = player.Name
val terminal_guid = msg.terminal_guid
val implant_type = implant.Type
val (interface, slotNumber) = player.VehicleSeated match {
case Some(mech_guid) =>
(
zone.Map.TerminalToInterface.get(mech_guid.guid),
player.UninstallImplant(implant_type)
)
case None =>
(None, None)
}
val result = if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) {
val slot = slotNumber.get
log.info(s"is uninstalling $implant_type - take from slot $slot")
UninitializeImplant(slot)
events ! AvatarServiceMessage(playerChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID, AvatarImplantMessage(player.GUID, ImplantAction.Remove, slot, 0)))
true
}
else {
val message = s"${player.Name} can not sell $implant_type"
if(interface.isEmpty) {
log.warn(s"$message - not interacting with a terminal")
}
else if(!interface.contains(terminal_guid.guid)) {
log.warn(s"$message - interacting with the wrong terminal, ${interface.get}")
}
else if(slotNumber.isEmpty) {
log.warn(s"$message - does not know that implant")
}
else {
log.warn(s"$message - forgot to sit at a terminal")
}
false
}
events ! AvatarServiceMessage(playerChannel, AvatarAction.TerminalOrderResult(terminal_guid, msg.transaction_type, result))
case _ => ; //terminal messages not handled here
}
@ -490,21 +543,189 @@ class PlayerControl(player : Player) extends Actor
if(player.isAlive) {
val originalHealth = player.Health
val originalArmor = player.Armor
val originalStamina = player.Stamina
val originalCapacitor = player.Capacitor.toInt
val cause = applyDamageTo(player)
val health = player.Health
val armor = player.Armor
val stamina = player.Stamina
val capacitor = player.Capacitor.toInt
val damageToHealth = originalHealth - health
val damageToArmor = originalArmor - armor
val damageToStamina = originalStamina - stamina
val damageToCapacitor = originalCapacitor - capacitor
PlayerControl.HandleDamage(player, cause, damageToHealth, damageToArmor, damageToCapacitor)
if(damageToHealth > 0 || damageToArmor > 0 || damageToCapacitor > 0) {
damageLog.info(s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalCapacitor, AFTER=$health/$armor/$capacitor, CHANGE=$damageToHealth/$damageToArmor/$damageToCapacitor")
HandleDamage(player, cause, damageToHealth, damageToArmor, damageToStamina, damageToCapacitor)
if(damageToHealth > 0 || damageToArmor > 0 || damageToStamina > 0 || damageToCapacitor > 0) {
damageLog.info(s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalStamina/$originalCapacitor, AFTER=$health/$armor/$stamina/$capacitor, CHANGE=$damageToHealth/$damageToArmor/$damageToStamina/$damageToCapacitor")
}
}
}
/**
* na
* @param target na
*/
def HandleDamage(target : Player, cause : ResolvedProjectile, damageToHealth : Int, damageToArmor : Int, damageToStamina : Int, damageToCapacitor : Int) : Unit = {
val targetGUID = target.GUID
val zone = target.Zone
val zoneId = zone.Id
val events = zone.AvatarEvents
val health = target.Health
if(damageToArmor > 0) {
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 4, target.Armor))
}
if(health > 0) {
if(damageToCapacitor > 0) {
events ! AvatarServiceMessage(target.Name, AvatarAction.PlanetsideAttributeSelf(targetGUID, 7, target.Capacitor.toLong))
}
if(damageToHealth > 0 || damageToStamina > 0) {
target.History(cause)
if(damageToHealth > 0) {
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 0, health))
}
if(damageToStamina > 0) {
UpdateStamina()
}
//activity on map
zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos)
//alert damage source
DamageAwareness(target, cause)
}
if(Damageable.CanJammer(target, cause)) {
target.Actor ! JammableUnit.Jammered(cause)
}
}
else {
DestructionAwareness(target, Some(cause))
}
}
/**
* na
* @param target na
* @param cause na
*/
def DamageAwareness(target : Player, cause : ResolvedProjectile) : Unit = {
val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage(
target.Name,
cause.projectile.owner match {
case pSource : PlayerSource => //player damage
val name = pSource.Name
zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match {
case Some(tplayer) => AvatarAction.HitHint(tplayer.GUID, target.GUID)
case None => AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, pSource.Position))
}
case source => AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, source.Position))
}
)
}
/**
* The player has lost all his vitality and must be killed.<br>
* <br>
* Shift directly into a state of being dead on the client by setting health to zero points,
* whereupon the player will perform a dramatic death animation.
* Stamina is also set to zero points.
* If the player was in a vehicle at the time of demise, special conditions apply and
* the model must be manipulated so it behaves correctly.
* Do not move or completely destroy the `Player` object as its coordinates of death will be important.<br>
* <br>
* A maximum revive waiting timer is started.
* When this timer reaches zero, the avatar will attempt to spawn back on its faction-specific sanctuary continent.
* @param target na
* @param cause na
*/
def DestructionAwareness(target : Player, cause : Option[ResolvedProjectile]) : Unit = {
val player_guid = target.GUID
val pos = target.Position
val respawnTimer = 300000 //milliseconds
val zone = target.Zone
val events = zone.AvatarEvents
val nameChannel = target.Name
val zoneChannel = zone.Id
target.Die
//unjam
CancelJammeredSound(target)
CancelJammeredStatus(target)
//implants off
target.Stamina = 0
UpdateStamina() //turn off implants / OutOfStamina
//uninitialize implants
target.Implants.indices.foreach { case slot if target.Implant(slot) != ImplantType.None =>
UninitializeImplant(slot)
}
target.ResetAllImplants() //anything else specific to the backend
events ! AvatarServiceMessage(nameChannel, AvatarAction.Killed(player_guid, target.VehicleSeated)) //align client interface fields with state
zone.GUID(target.VehicleSeated) match {
case Some(obj : Mountable) =>
//boot cadaver from seat internally (vehicle perspective)
obj.PassengerInSeat(target) match {
case Some(index) =>
obj.Seats(index).Occupant = None
case _ => ;
}
//boot cadaver from seat on client
events ! AvatarServiceMessage(nameChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID,
ObjectDetachMessage(obj.GUID, player_guid, target.Position, Vector3.Zero))
)
//make player invisible on client
events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 29, 1))
//only the dead player should "see" their own body, so that the death camera has something to focus on
events ! AvatarServiceMessage(zoneChannel, AvatarAction.ObjectDelete(player_guid, player_guid))
case _ => ;
}
events ! AvatarServiceMessage(zoneChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 0, 0)) //health
if(target.Capacitor > 0) {
target.Capacitor = 0
events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 7, 0)) // capacitor
}
val attribute = cause match {
case Some(resolved) =>
resolved.projectile.owner match {
case pSource : PlayerSource =>
val name = pSource.Name
zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match {
case Some(tplayer) => tplayer.GUID
case None => player_guid
}
case _ => player_guid
}
case _ => player_guid
}
events ! AvatarServiceMessage(
nameChannel,
AvatarAction.SendResponse(Service.defaultPlayerGUID, DestroyMessage(player_guid, attribute, Service.defaultPlayerGUID, pos)) //how many players get this message?
)
events ! AvatarServiceMessage(
nameChannel,
AvatarAction.SendResponse(Service.defaultPlayerGUID, AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, target.Faction, true))
)
//TODO other methods of death?
val pentry = PlayerSource(target)
(target.History.find({p => p.isInstanceOf[PlayerSuicide]}) match {
case Some(PlayerSuicide(_)) =>
None
case _ =>
cause.orElse { target.LastShot } match {
case out @ Some(shot) =>
if(System.nanoTime - shot.hit_time < (10 seconds).toNanos) {
out
}
else {
None //suicide
}
case None =>
None //suicide
}
}) match {
case Some(shot) =>
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(shot.projectile.owner, pentry, shot.projectile.attribute_to))
case None =>
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0))
}
}
/**
* Start the jammered buzzing.
* Although, as a rule, the jammering sound effect should last as long as the jammering status,
@ -530,29 +751,23 @@ class PlayerControl(player : Player) extends Actor
* @param target an object that can be affected by the jammered status
* @param dur the duration of the timer, in milliseconds
*/
override def StartJammeredStatus(target : Any, dur : Int) : Unit = target match {
case obj : Player =>
//TODO these features
// val guid = obj.GUID
// val zone = obj.Zone
// val zoneId = zone.Id
// val events = zone.AvatarEvents
player.Implants.indices.foreach { slot => // Deactivate & uninitialize all implants
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
self ! Player.ImplantActivation(slot, 0)
PlayerControl.UninitializeImplant(player, slot)
}
obj.skipStaminaRegenForTurns = math.max(obj.skipStaminaRegenForTurns, 10)
super.StartJammeredStatus(target, dur)
case _ => ;
override def StartJammeredStatus(target : Any, dur : Int) : Unit = {
//TODO these features
val zone = player.Zone
player.Implants.indices.foreach { slot => // Deactivate & uninitialize all implants
zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
ImplantActivation(slot, status = 0)
UninitializeImplant(slot)
}
player.skipStaminaRegenForTurns = math.max(player.skipStaminaRegenForTurns, 10)
super.StartJammeredStatus(target, dur)
}
override def CancelJammeredStatus(target: Any): Unit = {
player.Implants.indices.foreach { slot => // Start reinitializing all implants
self ! Player.ImplantInitializationStart(slot)
player.ImplantSlot(slot).InitializeTime = 0 //setting time to 0 will restart implant initialization (eventually)
ImplantInitializationStart(slot)
}
super.CancelJammeredStatus(target)
}
@ -664,175 +879,200 @@ class PlayerControl(player : Player) extends Actor
val zone = obj.Zone
zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectDetachMessage(obj.GUID, item.GUID, Vector3.Zero, 0f)))
}
/**
* na
* @param changeInStamina na
*/
def StaminaChanged(changeInStamina : Int) : Unit = {
val beforeStamina = player.Stamina
val afterStamina = player.Stamina += changeInStamina
if(beforeStamina != afterStamina) {
UpdateStamina()
}
}
/**
* Determine whether the current stamina value for this player requires a greater change in player states.
* Losing all stamina and not yet being fatigued deactivates implants.
* Having stamina of 20 points or greater and having previously been fatigued
* allows implants to operate once again.
* Initialization must be restarted manually for any implant that had not previously finished initializing.
*/
def UpdateStamina() : Unit = {
val currentStamina = player.Stamina
if(currentStamina == 0 && !player.Fatigued) { // Only be fatigued once even if loses all stamina again
player.Fatigued = true
player.skipStaminaRegenForTurns = math.max(player.skipStaminaRegenForTurns, 6)
player.Implants.indices.foreach { slot => // Disable all implants
ImplantActivation(slot, status = 0)
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(Service.defaultPlayerGUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 1)))
}
}
else if(currentStamina >= 20) {
val wasFatigued = player.Fatigued
player.Fatigued = false
if(wasFatigued) { //reactivate only if we were fatigued
player.Implants.indices.foreach { slot => // Re-enable all implants
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(Service.defaultPlayerGUID, AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 0)))
if(!player.ImplantSlot(slot).Initialized) {
ImplantInitializationStart(slot)
}
}
}
}
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(player.GUID, 2, currentStamina))
}
/**
* The process of starting an implant so that it can be activated is one that requires a matter of time.
* If the implant should already have been started, then just switch to the proper state.
* Always (check to) initialize implants when setting up an avatar or becoming fatigued or when revived.
* @param slot the slot in which this implant is found
*/
def ImplantInitializationStart(slot : Int) : Unit = {
val implantSlot = player.ImplantSlot(slot)
if(implantSlot.Installed.isDefined) {
if(!implantSlot.Initialized) {
val time = System.currentTimeMillis
val initializationTime = if(implantSlot.InitializeTime == 0L) {
implantSlot.InitializeTime = time
time
}
else {
implantSlot.InitializeTime
}
val maxInitializationTime = implantSlot.MaxTimer * 1000
if (time - initializationTime > maxInitializationTime) {
//this implant should have already been initialized
ImplantInitializationComplete(slot)
}
else {
// Start client side initialization timer
// Check this along the bottom of the character information window
//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
val percent = (100 * (time - initializationTime) / maxInitializationTime.toFloat ).toInt
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, percent)))
// Callback after initialization timer to complete initialization
implantSlotTimers(slot).cancel
implantSlotTimers(slot) = context.system.scheduler.scheduleOnce((maxInitializationTime - (time - initializationTime)) milliseconds, self, Player.ImplantInitializationComplete(slot))
}
}
else {
ImplantInitializationComplete(slot)
}
}
}
/**
* The implant is ready to be made available and active on selection.
* The end result of a timed process, occasionally an implant will become "already active".
* @param slot the slot in which this implant is found
*/
def ImplantInitializationComplete(slot : Int) : Unit = {
val implantSlot = player.ImplantSlot(slot)
if(implantSlot.Installed.isDefined) {
implantSlot.Initialized = true
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.SendResponse(Service.defaultPlayerGUID, AvatarImplantMessage(player.GUID, ImplantAction.Initialization, slot, 1)))
implantSlotTimers(slot).cancel
implantSlotTimers(slot) = Default.Cancellable
}
}
/**
* Whether or not the implant is being used by the player who installed it.
* If the implant has no business having its activation state changed yet, it (re)starts its initialization phase.
* @param slot the slot in which this implant is found
* @param status `1`, if the implant should become active;
* `0`, if it should be deactivated
*/
def ImplantActivation(slot : Int, status : Int) : Unit = {
val implantSlot = player.ImplantSlot(slot)
if(!implantSlot.Initialized && !player.Fatigued) {
log.warn(s"implant in slot $slot is trying to (de)activate when not even initialized!")
//we should not be activating or deactivataing, but initializing
implantSlotTimers(slot).cancel
implantSlotTimers(slot) = Default.Cancellable
implantSlot.Active = false
//normal deactivation
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.DeactivateImplantSlot(player.GUID, slot))
//initialization process (from scratch)
implantSlot.InitializeTime = 0
ImplantInitializationStart(slot)
}
else if(status == 0 && implantSlot.Active) {
implantSlotTimers(slot).cancel
implantSlotTimers(slot) = Default.Cancellable
implantSlot.Active = false
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.DeactivateImplantSlot(player.GUID, slot))
}
else if(status == 1 && implantSlot.Initialized && !player.Fatigued) {
implantSlot.Installed match {
case Some(implant)
if(implant.Type == ImplantType.PersonalShield && player.ExoSuit == ExoSuitType.Infiltration) ||
(implant.Type == ImplantType.Surge && player.ExoSuit == ExoSuitType.MAX) =>
//TODO STILL NOT ALLOWED (but make it look normal)
case Some(implant) =>
if (implantSlot.Active) {
// Some events such as zoning will reset the implant on the client side without sending a deactivation packet
// But the implant will remain in an active state server side. For now, allow reactivation of the implant.
log.warn(s"implant $slot is already active, but activating again")
implantSlotTimers(slot).cancel
implantSlotTimers(slot) = Default.Cancellable
}
val activationStaminaCost = implant.ActivationStaminaCost
if (activationStaminaCost > 0) {
player.Stamina -= activationStaminaCost // Activation stamina drain
UpdateStamina()
}
if (!player.Fatigued) {
implantSlot.Active = true
val zone = player.Zone
val drainInterval = implant.GetCostIntervalByExoSuit(player.ExoSuit)
if (drainInterval > 0) { // Ongoing stamina drain, if applicable
implantSlotTimers(slot).cancel
implantSlotTimers(slot) = context.system.scheduler.scheduleWithFixedDelay(initialDelay = 0 seconds, drainInterval milliseconds, self, Player.StaminaChanged(-implant.StaminaCost))
}
zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, player.Implant(slot).id * 2 + 1)) // Activation sound / effect
zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.ActivateImplantSlot(player.GUID, slot))
}
case _ =>
//there should have been an implant here ...
implantSlot.Active = false
implantSlot.Initialized = false
implantSlot.InitializeTime = 0L
//todo: AvatarImplantMessage(tplayer.GUID, ImplantAction.Remove, slot, 0)?
}
}
}
/**
* The implant in this slot is no longer active and is no longer considered ready to activate.
* @param slot the slot in which an implant could be found
*/
def UninitializeImplant(slot: Int): Unit = {
implantSlotTimers(slot).cancel
implantSlotTimers(slot) = Default.Cancellable
val zone = player.Zone
val guid = player.GUID
val playerChannel = player.Name
val zoneChannel = zone.Id
val implantSlot = player.ImplantSlot(slot)
// if(implantSlot.Active) {
// zone.AvatarEvents ! AvatarServiceMessage(zoneChannel, AvatarAction.PlanetsideAttribute(guid, 28, player.Implant(slot).id * 2)) // Deactivation sound / effect
// zone.AvatarEvents ! AvatarServiceMessage(playerChannel, AvatarAction.DeactivateImplantSlot(guid, slot))
// }
implantSlot.Active = false
implantSlot.Initialized = false
implantSlot.InitializeTime = 0L
zone.AvatarEvents ! AvatarServiceMessage(playerChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 100)))
zone.AvatarEvents ! AvatarServiceMessage(zoneChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID, AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 0)))
}
}
object PlayerControl {
/**
* na
* @param target na
*/
def HandleDamage(target : Player, cause : ResolvedProjectile, damageToHealth : Int, damageToArmor : Int, damageToCapacitor : Int) : Unit = {
val targetGUID = target.GUID
val zone = target.Zone
val zoneId = zone.Id
val events = zone.AvatarEvents
val health = target.Health
if(health > 0) {
if(damageToCapacitor > 0) {
events ! AvatarServiceMessage(target.Name, AvatarAction.PlanetsideAttributeSelf(targetGUID, 7, target.Capacitor.toLong))
}
if(damageToHealth > 0 || damageToArmor > 0) {
target.History(cause)
if(damageToHealth > 0) {
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 0, health))
}
if(damageToArmor > 0) {
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 4, target.Armor))
}
//activity on map
zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos)
//alert damage source
DamageAwareness(target, cause)
}
if(Damageable.CanJammer(target, cause)) {
target.Actor ! JammableUnit.Jammered(cause)
}
}
else {
if(damageToArmor > 0) {
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 4, target.Armor))
}
DestructionAwareness(target, Some(cause))
}
}
/**
* na
* @param target na
* @param cause na
*/
def DamageAwareness(target : Player, cause : ResolvedProjectile) : Unit = {
val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage(
target.Name,
cause.projectile.owner match {
case pSource : PlayerSource => //player damage
val name = pSource.Name
zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match {
case Some(player) => AvatarAction.HitHint(player.GUID, target.GUID)
case None => AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, pSource.Position))
}
case source => AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, source.Position))
}
)
}
/**
* The player has lost all his vitality and must be killed.<br>
* <br>
* Shift directly into a state of being dead on the client by setting health to zero points,
* whereupon the player will perform a dramatic death animation.
* Stamina is also set to zero points.
* If the player was in a vehicle at the time of demise, special conditions apply and
* the model must be manipulated so it behaves correctly.
* Do not move or completely destroy the `Player` object as its coordinates of death will be important.<br>
* <br>
* A maximum revive waiting timer is started.
* When this timer reaches zero, the avatar will attempt to spawn back on its faction-specific sanctuary continent.
* @param target na
* @param cause na
*/
def DestructionAwareness(target : Player, cause : Option[ResolvedProjectile]) : Unit = {
val player_guid = target.GUID
val pos = target.Position
val respawnTimer = 300000 //milliseconds
val zone = target.Zone
val events = zone.AvatarEvents
val nameChannel = target.Name
val zoneChannel = zone.Id
target.Die
//unjam
target.Actor ! JammableUnit.ClearJammeredSound()
target.Actor ! JammableUnit.ClearJammeredStatus()
events ! AvatarServiceMessage(nameChannel, AvatarAction.Killed(player_guid, target.VehicleSeated)) //align client interface fields with state
zone.GUID(target.VehicleSeated) match {
case Some(obj : Mountable) =>
//boot cadaver from seat internally (vehicle perspective)
obj.PassengerInSeat(target) match {
case Some(index) =>
obj.Seats(index).Occupant = None
case _ => ;
}
//boot cadaver from seat on client
events ! AvatarServiceMessage(nameChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID,
ObjectDetachMessage(obj.GUID, player_guid, target.Position, Vector3.Zero))
)
//make player invisible on client
events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 29, 1))
//only the dead player should "see" their own body, so that the death camera has something to focus on
events ! AvatarServiceMessage(zoneChannel, AvatarAction.ObjectDelete(player_guid, player_guid))
case _ => ;
}
events ! AvatarServiceMessage(zoneChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 0, 0)) //health
events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 2, 0)) //stamina
if(target.Capacitor > 0) {
target.Capacitor = 0
events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 7, 0)) // capacitor
}
val attribute = cause match {
case Some(resolved) =>
resolved.projectile.owner match {
case pSource : PlayerSource =>
val name = pSource.Name
zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match {
case Some(player) => player.GUID
case None => player_guid
}
case _ => player_guid
}
case _ => player_guid
}
events ! AvatarServiceMessage(
nameChannel,
AvatarAction.SendResponse(Service.defaultPlayerGUID, DestroyMessage(player_guid, attribute, Service.defaultPlayerGUID, pos)) //how many players get this message?
)
events ! AvatarServiceMessage(
nameChannel,
AvatarAction.SendResponse(Service.defaultPlayerGUID, AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, target.Faction, true))
)
//TODO other methods of death?
val pentry = PlayerSource(target)
(target.History.find({p => p.isInstanceOf[PlayerSuicide]}) match {
case Some(PlayerSuicide(_)) =>
None
case _ =>
cause.orElse { target.LastShot } match {
case out @ Some(shot) =>
if(System.nanoTime - shot.hit_time < (10 seconds).toNanos) {
out
}
else {
None //suicide
}
case None =>
None //suicide
}
}) match {
case Some(shot) =>
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(shot.projectile.owner, pentry, shot.projectile.attribute_to))
case None =>
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0))
}
}
def UninitializeImplant(player: Player, slot: Int): Unit = {
val implantSlot = player.ImplantSlot(slot)
implantSlot.Initialized = false
if(implantSlot.InitializeTimer != Default.Cancellable) {
implantSlot.InitializeTimer.cancel()
implantSlot.InitializeTimer = Default.Cancellable
}
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponse(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.Initialization, slot, 0)))
}
/** */
private case class StaminaRegen()
}

View file

@ -245,7 +245,7 @@ object OrderTerminalDefinition {
}
def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = {
sender ! msg
msg.player.Actor ! msg
}
}

View file

@ -184,7 +184,7 @@ object ResolutionCalculations {
// If any health damage was applied also drain an amount of stamina equal to half the health damage
if(player.Health < originalHealth) {
val delta = originalHealth - player.Health
player.Stamina = player.Stamina - math.floor(delta / 2).toInt
player.Stamina -= math.floor(delta / 2).toInt
}
}
case _ =>

View file

@ -89,52 +89,41 @@ object Shortcut extends Marshallable[Shortcut] {
/**
A map to convert between ImplantTypes and Implant Shortcuts
*/
final lazy val ImplantsMap = Map(
ImplantType.AdvancedRegen->REGENERATION,
ImplantType.Targeting->ENHANCED_TARGETING,
ImplantType.AudioAmplifier->AUDIO_AMPLIFIER,
ImplantType.DarklightVision->DARKLIGHT_VISION,
ImplantType.MeleeBooster->MELEE_BOOSTER,
ImplantType.PersonalShield->PERSONAL_SHIELD,
ImplantType.RangeMagnifier->RANGE_MAGNIFIER,
ImplantType.SecondWind->SECOND_WIND,
ImplantType.SilentRun->SENSOR_SHIELD,
ImplantType.Surge->SURGE
)
final lazy val ImplantsMap : Map[ImplantType.Value, Option[Shortcut]] = Map(
ImplantType.AdvancedRegen->Regeneration,
ImplantType.Targeting->EnhancedTargeting,
ImplantType.AudioAmplifier->AudioAmplifier,
ImplantType.DarklightVision->DartklightVision,
ImplantType.MeleeBooster->MeleeBooster,
ImplantType.PersonalShield->PersonalShield,
ImplantType.RangeMagnifier->RangeMagnifier,
ImplantType.SecondWind->SecondWind,
ImplantType.SilentRun->SensorShield,
ImplantType.Surge->Surge
).withDefaultValue(None)
/**
* Preset for the Audio Amplifier implant. */
final val AUDIO_AMPLIFIER : Some[Shortcut] = Some(Shortcut(2, "audio_amplifier"))
/**
* Preset for the Darklight Vision implant. */
final val DARKLIGHT_VISION : Some[Shortcut] = Some(Shortcut(2, "darklight_vision"))
/**
* Preset for the Enhanced Targeting implant. */
final val ENHANCED_TARGETING : Some[Shortcut] = Some(Shortcut(2, "targeting"))
/**
* Preset for the medkit quick-use option. */
final val MEDKIT : Some[Shortcut] = Some(Shortcut(0, "medkit"))
/**
* Preset for the Melee Booster implant. */
final val MELEE_BOOSTER : Some[Shortcut] = Some(Shortcut(2, "melee_booster"))
/**
* Preset for the Personal Shield implant. */
final val PERSONAL_SHIELD : Some[Shortcut] = Some(Shortcut(2, "personal_shield"))
/**
* Preset for the Range Magnifier implant. */
final val RANGE_MAGNIFIER : Some[Shortcut] = Some(Shortcut(2, "range_magnifier"))
/**
* Preset for the Regeneration implant. */
final val REGENERATION : Some[Shortcut] = Some(Shortcut(2, "advanced_regen"))
/**
* Preset for the Second Wind implant. */
final val SECOND_WIND : Some[Shortcut] = Some(Shortcut(2, "second_wind"))
/**
* Preset for the Sensor Shield implant. */
final val SENSOR_SHIELD : Some[Shortcut] = Some(Shortcut(2, "silent_run"))
/**
* Preset for the Surge implant. */
final val SURGE : Some[Shortcut] = Some(Shortcut(2, "surge"))
/** Preset for the Audio Amplifier implant. */
final val AudioAmplifier : Some[Shortcut] = Some(Shortcut(2, "audio_amplifier"))
/** Preset for the Darklight Vision implant. */
final val DartklightVision : Some[Shortcut] = Some(Shortcut(2, "darklight_vision"))
/** Preset for the Enhanced Targeting implant. */
final val EnhancedTargeting : Some[Shortcut] = Some(Shortcut(2, "targeting"))
/** Preset for the medkit quick-use option. */
final val Medkit : Some[Shortcut] = Some(Shortcut(0, "medkit"))
/** Preset for the Melee Booster implant. */
final val MeleeBooster : Some[Shortcut] = Some(Shortcut(2, "melee_booster"))
/** Preset for the Personal Shield implant. */
final val PersonalShield : Some[Shortcut] = Some(Shortcut(2, "personal_shield"))
/** Preset for the Range Magnifier implant. */
final val RangeMagnifier : Some[Shortcut] = Some(Shortcut(2, "range_magnifier"))
/** Preset for the Regeneration implant. */
final val Regeneration : Some[Shortcut] = Some(Shortcut(2, "advanced_regen"))
/** Preset for the Second Wind implant. */
final val SecondWind : Some[Shortcut] = Some(Shortcut(2, "second_wind"))
/** Preset for the Sensor Shield implant. */
final val SensorShield : Some[Shortcut] = Some(Shortcut(2, "silent_run"))
/** Preset for the Surge implant. */
final val Surge : Some[Shortcut] = Some(Shortcut(2, "surge"))
/**
* Converter for text macro parameters that acts like a preset.
* @param effect1 a three letter acronym displayed in the hotbar

View file

@ -89,27 +89,27 @@ class CreateShortcutMessageTest extends Specification {
}
"presets" in {
Shortcut.AUDIO_AMPLIFIER.get.purpose mustEqual 2
Shortcut.AUDIO_AMPLIFIER.get.tile mustEqual "audio_amplifier"
Shortcut.DARKLIGHT_VISION.get.purpose mustEqual 2
Shortcut.DARKLIGHT_VISION.get.tile mustEqual "darklight_vision"
Shortcut.ENHANCED_TARGETING.get.purpose mustEqual 2
Shortcut.ENHANCED_TARGETING.get.tile mustEqual "targeting"
Shortcut.MEDKIT.get.purpose mustEqual 0
Shortcut.MEDKIT.get.tile mustEqual "medkit"
Shortcut.MELEE_BOOSTER.get.purpose mustEqual 2
Shortcut.MELEE_BOOSTER.get.tile mustEqual "melee_booster"
Shortcut.PERSONAL_SHIELD.get.purpose mustEqual 2
Shortcut.PERSONAL_SHIELD.get.tile mustEqual "personal_shield"
Shortcut.RANGE_MAGNIFIER.get.purpose mustEqual 2
Shortcut.RANGE_MAGNIFIER.get.tile mustEqual "range_magnifier"
Shortcut.REGENERATION.get.purpose mustEqual 2
Shortcut.REGENERATION.get.tile mustEqual "advanced_regen"
Shortcut.SECOND_WIND.get.purpose mustEqual 2
Shortcut.SECOND_WIND.get.tile mustEqual "second_wind"
Shortcut.SENSOR_SHIELD.get.purpose mustEqual 2
Shortcut.SENSOR_SHIELD.get.tile mustEqual "silent_run"
Shortcut.SURGE.get.purpose mustEqual 2
Shortcut.SURGE.get.tile mustEqual "surge"
Shortcut.AudioAmplifier.get.purpose mustEqual 2
Shortcut.AudioAmplifier.get.tile mustEqual "audio_amplifier"
Shortcut.DartklightVision.get.purpose mustEqual 2
Shortcut.DartklightVision.get.tile mustEqual "darklight_vision"
Shortcut.EnhancedTargeting.get.purpose mustEqual 2
Shortcut.EnhancedTargeting.get.tile mustEqual "targeting"
Shortcut.Medkit.get.purpose mustEqual 0
Shortcut.Medkit.get.tile mustEqual "medkit"
Shortcut.MeleeBooster.get.purpose mustEqual 2
Shortcut.MeleeBooster.get.tile mustEqual "melee_booster"
Shortcut.PersonalShield.get.purpose mustEqual 2
Shortcut.PersonalShield.get.tile mustEqual "personal_shield"
Shortcut.RangeMagnifier.get.purpose mustEqual 2
Shortcut.RangeMagnifier.get.tile mustEqual "range_magnifier"
Shortcut.Regeneration.get.purpose mustEqual 2
Shortcut.Regeneration.get.tile mustEqual "advanced_regen"
Shortcut.SecondWind.get.purpose mustEqual 2
Shortcut.SecondWind.get.tile mustEqual "second_wind"
Shortcut.SensorShield.get.purpose mustEqual 2
Shortcut.SensorShield.get.tile mustEqual "silent_run"
Shortcut.Surge.get.purpose mustEqual 2
Shortcut.Surge.get.tile mustEqual "surge"
}
}

View file

@ -343,17 +343,23 @@ class PlayerControlDamageTest extends ActorTest {
assert(player2.Health == player2.Definition.DefaultHealth)
assert(player2.Armor == player2.MaxArmor)
player2.Actor ! Vitality.Damage(applyDamageTo)
val msg_avatar = avatarProbe.receiveN(3, 500 milliseconds)
val msg_avatar = avatarProbe.receiveN(4, 500 milliseconds)
val msg_activity = activityProbe.receiveOne(200 milliseconds)
assert(
msg_avatar.head match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 4, _)) => true
case _ => false
}
)
assert(
msg_avatar(1) match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 4, _)) => true
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case _ => false
}
)
assert(
msg_avatar(2) match {
case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => true
case _ => false
}
)
@ -367,7 +373,7 @@ class PlayerControlDamageTest extends ActorTest {
}
)
assert(
msg_avatar(2) match {
msg_avatar(3) match {
case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0)))) => true
case _ => false
}
@ -440,19 +446,19 @@ class PlayerControlDeathStandingTest extends ActorTest {
)
assert(
msg_avatar(1) match {
case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), None)) => true
case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => true
case _ => false
}
)
assert(
msg_avatar(2) match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), None)) => true
case _ => false
}
)
assert(
msg_avatar(3) match {
case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => true
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case _ => false
}
)
@ -546,12 +552,18 @@ class PlayerControlDeathSeatedTest extends ActorTest {
activityProbe.expectNoMessage(200 milliseconds)
assert(
msg_avatar.head match {
case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), Some(PlanetSideGUID(5)))) => true
case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => true
case _ => false
}
)
assert(
msg_avatar(1) match {
case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), Some(PlanetSideGUID(5)))) => true
case _ => false
}
)
assert(
msg_avatar(2) match {
case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(_,
ObjectDetachMessage(PlanetSideGUID(5), PlanetSideGUID(2), _, _, _, _))
) => true
@ -559,26 +571,20 @@ class PlayerControlDeathSeatedTest extends ActorTest {
}
)
assert(
msg_avatar(2) match {
msg_avatar(3) match {
case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 29, 1)) => true
case _ => false
}
)
assert(
msg_avatar(3) match {
msg_avatar(4) match {
case AvatarServiceMessage("test", AvatarAction.ObjectDelete(PlanetSideGUID(2), PlanetSideGUID(2), _)) => true
case _ => false
}
)
assert(
msg_avatar(4) match {
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case _ => false
}
)
assert(
msg_avatar(5) match {
case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => true
case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true
case _ => false
}
)

View file

@ -2584,86 +2584,6 @@ class WorldSessionActor extends Actor
}
lastTerminalOrderFulfillment = true
case Terminal.LearnImplant(implant) =>
val terminal_guid = msg.terminal_guid
val implant_type = implant.Type
val message = s"Implants: ${tplayer.Name} wants to learn $implant_type"
val (interface, slotNumber) = tplayer.VehicleSeated match {
case Some(mech_guid) =>
(
continent.Map.TerminalToInterface.get(mech_guid.guid),
if(!avatar.Implants.exists({ slot => slot.Implant == implant_type })) {
//no duplicates
avatar.InstallImplant(implant)
}
else {
None
}
)
case _ =>
(None, None)
}
if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) {
val slot = slotNumber.get
log.info(s"$message - put in slot $slot")
sendResponse(AvatarImplantMessage(tplayer.GUID, ImplantAction.Add, slot, implant_type.id))
sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Learn, true))
player.Actor ! Player.ImplantInitializationStart(slot)
}
else {
if(interface.isEmpty) {
log.warn(s"$message - not interacting with a terminal")
}
else if(!interface.contains(terminal_guid.guid)) {
log.warn(s"$message - interacting with the wrong terminal, ${interface.get}")
}
else if(slotNumber.isEmpty) {
log.warn(s"$message - already knows that implant")
}
else {
log.warn(s"$message - forgot to sit at a terminal")
}
sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Learn, false))
}
lastTerminalOrderFulfillment = true
case Terminal.SellImplant(implant) =>
val terminal_guid = msg.terminal_guid
val implant_type = implant.Type
val (interface, slotNumber) = tplayer.VehicleSeated match {
case Some(mech_guid) =>
(
continent.Map.TerminalToInterface.get(mech_guid.guid),
avatar.UninstallImplant(implant_type)
)
case None =>
(None, None)
}
if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) {
val slot = slotNumber.get
log.info(s"${tplayer.Name} is selling $implant_type - take from slot $slot")
player.Actor ! Player.UninitializeImplant(slot)
sendResponse(AvatarImplantMessage(tplayer.GUID, ImplantAction.Remove, slot, 0))
sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Sell, true))
}
else {
val message = s"${tplayer.Name} can not sell $implant_type"
if(interface.isEmpty) {
log.warn(s"$message - not interacting with a terminal")
}
else if(!interface.contains(terminal_guid.guid)) {
log.warn(s"$message - interacting with the wrong terminal, ${interface.get}")
}
else if(slotNumber.isEmpty) {
log.warn(s"$message - does not know that implant")
}
else {
log.warn(s"$message - forgot to sit at a terminal")
}
sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Sell, false))
}
lastTerminalOrderFulfillment = true
case Terminal.BuyVehicle(vehicle, weapons, trunk) =>
continent.Map.TerminalToSpawnPad.get(msg.terminal_guid.guid) match {
case Some(pad_guid) =>
@ -2740,8 +2660,7 @@ class WorldSessionActor extends Actor
* @param reply na
*/
def HandleVehicleServiceResponse(toChannel : String, guid : PlanetSideGUID, reply : VehicleResponse.Response) : Unit = {
val tplayer_guid = if(player.HasGUID) player.GUID
else PlanetSideGUID(0)
val tplayer_guid = if(player.HasGUID) player.GUID else PlanetSideGUID(0)
reply match {
case VehicleResponse.AttachToRails(vehicle_guid, pad_guid) =>
sendResponse(ObjectAttachMessage(pad_guid, vehicle_guid, 3))
@ -2868,6 +2787,17 @@ class WorldSessionActor extends Actor
sendResponse(VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6))
if(player.VehicleSeated.contains(vehicle_guid)) {
player.Position = pos
GetVehicleAndSeat() match {
case (Some(_), Some(0)) => ;
case (Some(_), Some(_)) =>
turnCounter(guid)
if (player.death_by == -1) {
sendResponse(ChatMsg(ChatMessageType.UNK_71, true, "", "Your account has been logged out by a Customer Service Representative.", None))
Thread.sleep(300)
sendResponse(DropSession(sessionId, "kick by GM"))
}
case _ => ;
}
}
}
case VehicleResponse.SendResponse(msg) =>
@ -3173,26 +3103,42 @@ class WorldSessionActor extends Actor
if(player.spectator) {
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None))
}
(0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => {
if(player.Jammed) {
//TODO something better than just canceling?
player.Actor ! JammableUnit.ClearJammeredStatus()
player.Actor ! JammableUnit.ClearJammeredSound()
}
val fatigued = player.Fatigued
(0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach {slot =>
val implantSlot = player.ImplantSlot(slot)
if(implantSlot.Initialized) {
sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1))
implantSlot.Installed match {
case Some(_) =>
if (implantSlot.Initialized) {
sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1))
if (fatigued) {
sendResponse(AvatarImplantMessage(guid, ImplantAction.OutOfStamina, slot, 1))
}
}
else if (!fatigued) {
player.Actor ! Player.ImplantInitializationStart(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
val implant = implantSlot.Implant
Shortcut.ImplantsMap(implant) match {
case shortcut @ Some(_) =>
sendResponse(CreateShortcutMessage(guid, slot + 2, 0, addShortcut = true, shortcut))
case None if implant != ImplantType.None =>
log.warn(s"could not find shortcut for implant $implant")
case _ => ;
}
case _ => ;
}
else {
player.Actor ! Player.ImplantInitializationStart(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
Shortcut.ImplantsMap(implantSlot.Implant) match {
case Some(shortcut : Shortcut) =>
sendResponse(CreateShortcutMessage(guid, slot + 2, 0, addShortcut = true, Some(shortcut)))
case _ => log.warn(s"Could not find shortcut for implant ${implantSlot.Implant.toString()}")
}
})
}
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
//TODO if Medkit does not have shortcut, add to a free slot or write over slot 64
sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT))
sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.Medkit))
sendResponse(ChangeShortcutBankMessage(guid, 0))
//Favorites lists
val (inf, veh) = avatar.EquipmentLoadouts.Loadouts.partition { case (index, _) => index < 10 }
@ -3265,6 +3211,11 @@ class WorldSessionActor extends Actor
))
case (Some(vehicle), Some(0)) =>
//summon any passengers and cargo vehicles left behind on previous continent
if(vehicle.Jammed) {
//TODO something better than just canceling?
vehicle.Actor ! JammableUnit.ClearJammeredStatus()
vehicle.Actor ! JammableUnit.ClearJammeredSound()
}
LoadZoneTransferPassengerMessages(
guid,
continent.Id,
@ -3284,6 +3235,9 @@ class WorldSessionActor extends Actor
//killed during spawn setup or possibly a relog into a corpse (by accident?)
player.Actor ! Player.Die()
}
else {
tplayer.Actor ! Player.StaminaRegen()
}
upstreamMessageCount = 0
}
@ -3870,27 +3824,13 @@ class WorldSessionActor extends Actor
zoneLoaded = Some(true)
case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, jump_thrust, is_cloaking, unk5, unk6) =>
if (player.death_by == -1) {
sendResponse(ChatMsg(ChatMessageType.UNK_71, true, "", "Your account has been logged out by a Customer Service Representative.", None))
Thread.sleep(300)
sendResponse(DropSession(sessionId, "kick by GM"))
}
//log.info(s"$msg")
turnCounter(avatar_guid)
val isMoving = WorldEntity.isMoving(vel)
val isMovingPlus = isMoving || is_jumping || jump_thrust
if(isMovingPlus) {
CancelZoningProcessWithDescriptiveReason("cancel_motion")
}
if(deadState == DeadState.Alive && upstreamMessageCount % 2 == 0) { // Regen stamina roughly every 500ms
if(player.skipStaminaRegenForTurns > 0) {
//do not renew stamina for a while
player.skipStaminaRegenForTurns -= 1
}
else if(!isMovingPlus && player.Stamina != player.MaxStamina) {
player.Stamina += 1
}
}
player.Position = pos
player.Velocity = vel
player.Orientation = Vector3(player.Orientation.x, pitch, yaw)
@ -3937,8 +3877,14 @@ class WorldSessionActor extends Actor
}
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, player.Position, player.Velocity, yaw, pitch, yaw_upper, seq_time, is_crouching, is_jumping, jump_thrust, is_cloaking, player.spectator, wepInHand))
updateSquad()
if(player.death_by == -1) {
sendResponse(ChatMsg(ChatMessageType.UNK_71, true, "", "Your account has been logged out by a Customer Service Representative.", None))
Thread.sleep(300)
sendResponse(DropSession(sessionId, "kick by GM"))
}
case msg@ChildObjectStateMessage(object_guid, pitch, yaw) =>
//log.info(s"$msg")
//the majority of the following check retrieves information to determine if we are in control of the child
FindContainedWeapon match {
case (Some(o), Some(tool)) =>
@ -3971,49 +3917,47 @@ class WorldSessionActor extends Actor
}
case msg@VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, flying, unk6, unk7, wheels, is_decelerating, is_cloaked) =>
if(deadState == DeadState.Alive) {
GetVehicleAndSeat() match {
case (Some(obj), Some(0)) =>
//we're driving the vehicle
turnCounter(player.GUID)
val seat = obj.Seats(0)
player.Position = pos //convenient
if(seat.ControlledWeapon.isEmpty) {
player.Orientation = Vector3.z(ang.z) //convenient
//log.info(s"$msg")
GetVehicleAndSeat() match {
case (Some(obj), Some(0)) =>
//we're driving the vehicle
turnCounter(player.GUID)
val seat = obj.Seats(0)
player.Position = pos //convenient
if(seat.ControlledWeapon.isEmpty) {
player.Orientation = Vector3.z(ang.z) //convenient
}
obj.Position = pos
obj.Orientation = ang
if(obj.MountedIn.isEmpty) {
if(obj.DeploymentState != DriveState.Deployed) {
obj.Velocity = vel
} else {
obj.Velocity = Some(Vector3.Zero)
}
obj.Position = pos
obj.Orientation = ang
if(obj.MountedIn.isEmpty) {
if(obj.DeploymentState != DriveState.Deployed) {
obj.Velocity = vel
} else {
obj.Velocity = Some(Vector3.Zero)
}
if(obj.Definition.CanFly) {
obj.Flying = flying.nonEmpty //usually Some(7)
}
obj.Cloaked = obj.Definition.CanCloak && is_cloaked
if(obj.Definition.CanFly) {
obj.Flying = flying.nonEmpty //usually Some(7)
}
else {
obj.Velocity = None
obj.Flying = false
}
continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, obj.Position, ang, obj.Velocity, if(obj.Flying) {
flying
}
else {
None
}, unk6, unk7, wheels, is_decelerating, obj.Cloaked))
updateSquad()
case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
case (_, Some(index)) =>
log.error(s"VehicleState: player should not be dispatching this kind of packet from vehicle#$vehicle_guid when not the driver ($index)")
case _ => ;
}
obj.Cloaked = obj.Definition.CanCloak && is_cloaked
}
else {
obj.Velocity = None
obj.Flying = false
}
continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, obj.Position, ang, obj.Velocity, if(obj.Flying) {
flying
}
else {
None
}, unk6, unk7, wheels, is_decelerating, obj.Cloaked))
updateSquad()
case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
case (_, Some(index)) =>
log.error(s"VehicleState: player should not be dispatching this kind of packet from vehicle#$vehicle_guid when not the driver ($index)")
case _ => ;
}
//log.info(s"VehicleState: $msg")
if (player.death_by == -1) {
sendResponse(ChatMsg(ChatMessageType.UNK_71, true, "", "Your account has been logged out by a Customer Service Representative.", None))
Thread.sleep(300)
@ -4825,7 +4769,7 @@ class WorldSessionActor extends Actor
case msg@AvatarJumpMessage(state) =>
//log.info("AvatarJump: " + msg)
player.Stamina = player.Stamina - 10
player.Actor ! Player.StaminaChanged(-10)
player.skipStaminaRegenForTurns = math.max(player.skipStaminaRegenForTurns, 5)
case msg@ZipLineMessage(player_guid, forwards, action, path_id, pos) =>
@ -5123,7 +5067,7 @@ class WorldSessionActor extends Actor
false
}
else {
player.Stamina = player.Stamina + 100
player.Actor ! Player.StaminaChanged(100)
sendResponse(PlanetsideAttributeMessage(avatar_guid, 2, player.Stamina))
true
}
@ -5614,7 +5558,8 @@ class WorldSessionActor extends Actor
}
else { //shooting
if (tool.FireModeIndex == 1 && (tool.Definition.Name == "anniversary_guna" || tool.Definition.Name == "anniversary_gun" || tool.Definition.Name == "anniversary_gunb")) {
player.Stamina = 0
player.Actor ! Player.StaminaChanged(-player.Stamina)
player.skipStaminaRegenForTurns = math.max(player.skipStaminaRegenForTurns, 3)
}
prefire = shooting.orElse(Some(weapon_guid))
@ -8165,6 +8110,15 @@ class WorldSessionActor extends Actor
val player_guid : PlanetSideGUID = tplayer.GUID
val obj_guid : PlanetSideGUID = obj.GUID
PlayerActionsToCancel()
//deactivate non-passive implants
tplayer.Implants.indices.foreach { index =>
val implantSlot = tplayer.ImplantSlot(index)
if(implantSlot.Active && implantSlot.Charge(tplayer.ExoSuit) > 0) {
tplayer.Actor ! Player.ImplantActivation(index, 0)
}
}
//delay regen
player.skipStaminaRegenForTurns = math.max(player.skipStaminaRegenForTurns, 6)
log.info(s"MountVehicleMsg: ${player.Name}_guid mounts $obj @ $seatNum")
sendResponse(ObjectAttachMessage(obj_guid, player_guid, seatNum))
continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.MountVehicle(player_guid, obj_guid, seatNum))
@ -8765,6 +8719,13 @@ class WorldSessionActor extends Actor
LoadZoneAsPlayer(newPlayer, zone_id)
}
else {
//deactivate non-passive implants
player.Implants.indices.foreach { index =>
val implantSlot = player.ImplantSlot(index)
if(implantSlot.Active && implantSlot.Charge(player.ExoSuit) > 0) {
player.Actor ! Player.ImplantActivation(index, 0)
}
}
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case Some(vehicle : Vehicle) => //driver or passenger in vehicle using a warp gate, or a droppod
LoadZoneInVehicle(vehicle, pos, ori, zone_id)
@ -10150,8 +10111,11 @@ class WorldSessionActor extends Actor
}
def DeactivateImplants() : Unit = {
for(slot <- 0 to player.Implants.length - 1) {
player.Actor ! Player.ImplantActivation(slot, 0)
//TODO 3 implant slots?
player.Implants.indices.foreach { slot =>
if(player.ImplantSlot(slot).Active) {
player.Actor ! Player.ImplantActivation(slot, 0)
}
}
}