diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala index 451d85ffd..95a43ae7b 100644 --- a/common/src/main/scala/net/psforever/objects/Avatar.scala +++ b/common/src/main/scala/net/psforever/objects/Avatar.scala @@ -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 => "" } } diff --git a/common/src/main/scala/net/psforever/objects/ImplantSlot.scala b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala index 5cc624ea1..c77cf2309 100644 --- a/common/src/main/scala/net/psforever/objects/ImplantSlot.scala +++ b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala @@ -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 diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index 7da2c79d7..b29b19257 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -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) diff --git a/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index f864527b3..b1b397331 100644 --- a/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -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.
+ *
+ * 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.
+ *
+ * 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.
- *
- * 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.
- *
- * 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() } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala index dac5366f2..ff19e49a2 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala @@ -245,7 +245,7 @@ object OrderTerminalDefinition { } def Dispatch(sender : ActorRef, terminal : Terminal, msg : Terminal.TerminalMessage) : Unit = { - sender ! msg + msg.player.Actor ! msg } } diff --git a/common/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala b/common/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala index 518de2b3e..a0b566b73 100644 --- a/common/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala +++ b/common/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala @@ -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 _ => diff --git a/common/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala b/common/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala index 9c1491a4f..94904bc5f 100644 --- a/common/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala @@ -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 diff --git a/common/src/test/scala/game/CreateShortcutMessageTest.scala b/common/src/test/scala/game/CreateShortcutMessageTest.scala index f5a662bf3..6a4b3bde1 100644 --- a/common/src/test/scala/game/CreateShortcutMessageTest.scala +++ b/common/src/test/scala/game/CreateShortcutMessageTest.scala @@ -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" } } diff --git a/common/src/test/scala/objects/PlayerControlTest.scala b/common/src/test/scala/objects/PlayerControlTest.scala index a977c7c8b..7c4da5c48 100644 --- a/common/src/test/scala/objects/PlayerControlTest.scala +++ b/common/src/test/scala/objects/PlayerControlTest.scala @@ -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 } ) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 7392b05fc..1861be013 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -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) + } } }