diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 808b572d..275245c4 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -53,24 +53,51 @@ object GlobalDefinitions { Implants */ val advanced_regen = ImplantDefinition(0) + advanced_regen.InitializationDuration = 120 + advanced_regen.StaminaCost = 2 + advanced_regen.CostIntervalDefault = 500 val targeting = ImplantDefinition(1) + targeting.InitializationDuration = 60 val audio_amplifier = ImplantDefinition(2) + audio_amplifier.InitializationDuration = 60 + audio_amplifier.StaminaCost = 1 + audio_amplifier.CostIntervalDefault = 1000 val darklight_vision = ImplantDefinition(3) + darklight_vision.InitializationDuration = 60 + darklight_vision.ActivationStaminaCost = 3 + darklight_vision.StaminaCost = 1 + darklight_vision.CostIntervalDefault = 500 val melee_booster = ImplantDefinition(4) + melee_booster.InitializationDuration = 120 + melee_booster.StaminaCost = 10 val personal_shield = ImplantDefinition(5) + personal_shield.InitializationDuration = 120 + personal_shield.StaminaCost = 1 + personal_shield.CostIntervalDefault = 600 val range_magnifier = ImplantDefinition(6) + range_magnifier.InitializationDuration = 60 val second_wind = ImplantDefinition(7) + second_wind.InitializationDuration = 180 val silent_run = ImplantDefinition(8) + silent_run.InitializationDuration = 90 + silent_run.StaminaCost = 1 + silent_run.CostIntervalDefault = 333 + silent_run.CostIntervalByExoSuitHashMap(ExoSuitType.Agile) = 1000 val surge = ImplantDefinition(9) + surge.InitializationDuration = 90 + surge.StaminaCost = 1 + surge.CostIntervalDefault = 1000 + surge.CostIntervalByExoSuitHashMap(ExoSuitType.Agile) = 500 + surge.CostIntervalByExoSuitHashMap(ExoSuitType.Reinforced) = 333 /* Projectiles diff --git a/common/src/main/scala/net/psforever/objects/ImplantSlot.scala b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala index a8ddbbfb..5fed4a50 100644 --- a/common/src/main/scala/net/psforever/objects/ImplantSlot.scala +++ b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala @@ -1,7 +1,8 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.definition.{ImplantDefinition, Stance} +import akka.actor.Cancellable +import net.psforever.objects.definition.ImplantDefinition import net.psforever.types.{ExoSuitType, ImplantType} /** @@ -18,11 +19,21 @@ 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 = DefaultCancellable.obj + /** 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 InitializeTimer_=(timer : Cancellable) : Cancellable = { + initializeTimer = timer + initializeTimer + } + def Unlocked : Boolean = unlocked def Unlocked_=(lock : Boolean) : Boolean = { @@ -78,12 +89,12 @@ class ImplantSlot { case ImplantType.None => -1L case _ => - Installed.get.Initialization + Installed.get.InitializationDuration } def ActivationCharge : Int = { if(Active) { - Installed.get.ActivationCharge + Installed.get.ActivationStaminaCost } else { 0 @@ -92,15 +103,13 @@ class ImplantSlot { /** * Calculate the stamina consumption of the implant for any given moment of being active after its activation. - * As implant energy use can be influenced by both exo-suit worn and general stance held, both are considered. * @param suit the exo-suit being worn - * @param stance the player's stance * @return the amount of stamina (energy) that is consumed */ - def Charge(suit : ExoSuitType.Value, stance : Stance.Value) : Int = { + def Charge(suit : ExoSuitType.Value) : Int = { if(Active) { val inst = Installed.get - inst.DurationChargeBase + inst.DurationChargeByExoSuit(suit) + inst.DurationChargeByStance(stance) + inst.StaminaCost } else { 0 diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index d4527d61..71e7a1ad 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -1,6 +1,7 @@ // 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.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} @@ -48,6 +49,7 @@ class Player(private val core : Avatar) extends PlanetSideServerObject private var crouching : Boolean = false private var jumping : Boolean = false private var cloaked : Boolean = false + private var fatigued : Boolean = false // If stamina drops to 0, player is fatigued until regenerating at least 20 stamina private var vehicleSeated : Option[PlanetSideGUID] = None @@ -124,8 +126,13 @@ class Player(private val core : Avatar) extends PlanetSideServerObject def Stamina : Int = stamina - def Stamina_=(assignEnergy : Int) : Int = { - stamina = if(isAlive) { math.min(math.max(0, assignEnergy), MaxStamina) } else { 0 } + def Stamina_=(assignStamina : Int) : Int = { + stamina = if(isAlive) { math.min(math.max(0, assignStamina), MaxStamina) } else { 0 } + + if(Actor != ActorRef.noSender) { + Actor ! Player.StaminaChanged(Stamina) + } + Stamina } @@ -359,6 +366,8 @@ class Player(private val core : Avatar) extends PlanetSideServerObject */ def Implant(slot : Int) : ImplantType.Value = core.Implant(slot) + def ImplantSlot(slot: Int) : ImplantSlot = core.Implants(slot) + /** * A read-only `Array` of tuples representing important information about all unlocked implant slots. * @return a maximum of three implant types, initialization times, and active flags @@ -397,6 +406,12 @@ class Player(private val core : Avatar) extends PlanetSideServerObject Cloaked } + def Fatigued : Boolean = fatigued + def Fatigued_=(isFatigued : Boolean) : Boolean = { + fatigued = isFatigued + Fatigued + } + def PersonalStyleFeatures : Option[Cosmetics] = core.PersonalStyleFeatures def AddToPersonalStyle(value : PersonalStyle.Value) : (Option[Cosmetics], Option[Cosmetics]) = { @@ -623,6 +638,12 @@ object Player { final val HandsDownSlot : Int = 255 final case class Die() + final case class ImplantActivation(slot : Int, status : Int) + 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) 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 61b75262..d79f1a79 100644 --- a/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -2,6 +2,11 @@ package net.psforever.objects.avatar import akka.actor.Actor +import net.psforever.objects.{DefaultCancellable, ImplantSlot, Player} +import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile, SourceEntry} +import net.psforever.objects.definition.ImplantDefinition +import net.psforever.objects.equipment.{JammableBehavior, JammableUnit} +import net.psforever.objects.vital.{PlayerSuicide, Vitality} import net.psforever.objects.{GlobalDefinitions, Player, Tool} import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile} import net.psforever.objects.equipment.{Ammo, JammableBehavior, JammableUnit} @@ -12,11 +17,14 @@ import net.psforever.objects.serverobject.repair.Repairable import net.psforever.objects.vital._ import net.psforever.objects.zones.Zone import net.psforever.packet.game._ +import net.psforever.types.{ExoSuitType, ImplantType, PlanetSideGUID} import net.psforever.types.{ExoSuitType, Vector3} import services.Service import services.avatar.{AvatarAction, AvatarServiceMessage} import scala.concurrent.duration._ +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.global /** * na; @@ -28,11 +36,97 @@ class PlayerControl(player : Player) extends Actor def JammableObject = player def DamageableObject = player + 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 -> DefaultCancellable.obj, 1 -> DefaultCancellable.obj, 2 -> DefaultCancellable.obj) + def receive : Receive = jammableBehavior .orElse(takesDamage) .orElse { + case Player.ImplantActivation(slot: Int, status : Int) => + // todo: disable implants with stamina cost when changing armour type + val implantSlot = player.ImplantSlot(slot) + + if(status == 0 && implantSlot.Active) { + // Cancel stamina drain timer + implantSlotStaminaDrainTimers(slot).cancel() + implantSlotStaminaDrainTimers(slot) = DefaultCancellable.obj + + 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) { + log.warn(s"Implant ${slot} is already active, but activating again") + } + implantSlot.Active = true + + if (implant.ActivationStaminaCost >= 0) { + player.Stamina -= implant.ActivationStaminaCost // Activation stamina drain + } + + if(implant.StaminaCost > 0 && implant.GetCostIntervalByExoSuit(player.ExoSuit) > 0) { // Ongoing stamina drain, if applicable + implantSlotStaminaDrainTimers(slot) = context.system.scheduler.schedule(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.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) + } + + // Start client side initialization timer + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, 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.Zone.Id, AvatarAction.SendResponse(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.Initialization, slot, 1))) + implantSlot.Initialized = true + implantSlot.InitializeTimer = DefaultCancellable.obj + } + + case Player.DrainStamina(amount : Int) => + player.Stamina -= amount + + case Player.StaminaChanged(currentStamina : Int) => + if(currentStamina == 0) { + player.Fatigued = true + player.skipStaminaRegenForTurns += 4 + for(slot <- 0 to player.Implants.length - 1) { // 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 + for(slot <- 0 to player.Implants.length - 1) { // 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.Die() => if(player.isAlive) { PlayerControl.DestructionAwareness(player, None) @@ -343,4 +437,11 @@ object PlayerControl { events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0)) } } + + def UninitializeImplant(player: Player, slot: Int): Unit = { + val implantSlot = player.ImplantSlot(slot) + + implantSlot.Initialized = false + player.Zone.AvatarEvents ! AvatarServiceMessage(player.Zone.Id, AvatarAction.SendResponse(player.GUID, AvatarImplantMessage(player.GUID, ImplantAction.Initialization, slot, 0))) + } } diff --git a/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala index 7984b310..36302523 100644 --- a/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala @@ -5,19 +5,6 @@ import net.psforever.types.{ExoSuitType, ImplantType} import scala.collection.mutable -/** - * An `Enumeration` of a variety of poses or generalized movement. - */ -object Stance extends Enumeration { - val - Crouching, - CrouchWalking, //not used, but should still be defined - Standing, - Walking, //not used, but should still be defined - Running - = Value -} - /** * The definition for an installable player utility that grants a perk, usually in exchange for stamina (energy).
*
@@ -25,36 +12,40 @@ object Stance extends Enumeration { * When activated by the user, an `activationCharge` may be deducted form that user's stamina reserves. * This does not necessarily have to be a non-zero value. * Passive implants are always active and thus have no cost. - * After being activated, a non-passive implant consumes a specific amount of stamina each second. - * This cost is modified by how the user is standing and what type of exo-suit they are wearing. - * The `durationChargeBase` is the lowest cost for an implant. - * Modifiers for exo-suit type and stance type are then added onto this base cost. - * For example: wearing `Reinforced` costs 2 stamina but costs only 1 stamina in all other cases. - * Assuming that is the only cost, the definition would have a base charge of 1 and a `Reinforced` modifier of 1. + * After being activated, a non-passive implant consumes a specific amount of stamina at regular intervals + * Some implants will specify a different interval for consuming stamina based on the exo-suit the player is wearing * @param implantType the type of implant that is defined * @see `ImplantType` */ class ImplantDefinition(private val implantType : Int) extends BasicDefinition { ImplantType(implantType) - /** how long it takes the implant to spin-up; is milliseconds */ - private var initialization : Long = 0L + /** how long it takes the implant to become ready for activation; is milliseconds */ + private var initializationDuration : Long = 0L /** a passive certification is activated as soon as it is ready (or other condition) */ private var passive : Boolean = false /** how much turning on the implant costs */ - private var activationCharge : Int = 0 - /** how much energy does this implant cost to remain active per second*/ - private var durationChargeBase : Int = 0 - /** how much more energy does the implant cost for this exo-suit */ - private val durationChargeByExoSuit = mutable.HashMap[ExoSuitType.Value, Int]().withDefaultValue(0) - /** how much more energy does the implant cost for this stance */ - private val durationChargeByStance = mutable.HashMap[Stance.Value, Int]().withDefaultValue(0) + private var activationStaminaCost : Int = 0 + /** how much energy does this implant cost to remain activate per interval tick */ + private var staminaCost : Int = 0 + + /** + * How often in milliseconds the stamina cost will be applied, per exo-suit type + * in game_objects.adb.lst each armour type is listed as a numeric identifier + * stamina_consumption_interval = Standard + * stamina_consumption_interval1 = Infil + * stamina_consumption_interval2 = Agile + * stamina_consumption_interval3 = Rexo + * stamina_consumption_interval4 = MAX? + */ + private var costIntervalDefault : Int = 0 + private val costIntervalByExoSuit = mutable.HashMap[ExoSuitType.Value, Int]().withDefaultValue(CostIntervalDefault) Name = "implant" - def Initialization : Long = initialization + def InitializationDuration : Long = initializationDuration - def Initialization_=(time : Long) : Long = { - initialization = math.max(0, time) - Initialization + def InitializationDuration_=(time : Long) : Long = { + initializationDuration = math.max(0, time) + InitializationDuration } def Passive : Boolean = passive @@ -64,23 +55,31 @@ class ImplantDefinition(private val implantType : Int) extends BasicDefinition { Passive } - def ActivationCharge : Int = activationCharge + def ActivationStaminaCost : Int = activationStaminaCost - def ActivationCharge_=(charge : Int) : Int = { - activationCharge = math.max(0, charge) - ActivationCharge + def ActivationStaminaCost_=(charge : Int) : Int = { + activationStaminaCost = math.max(0, charge) + ActivationStaminaCost } - def DurationChargeBase : Int = durationChargeBase + def StaminaCost : Int = staminaCost - def DurationChargeBase_=(charge : Int) : Int = { - durationChargeBase = math.max(0, charge) - DurationChargeBase + def StaminaCost_=(charge : Int) : Int = { + staminaCost = math.max(0, charge) + StaminaCost } - def DurationChargeByExoSuit : mutable.Map[ExoSuitType.Value, Int] = durationChargeByExoSuit - def DurationChargeByStance : mutable.Map[Stance.Value, Int] = durationChargeByStance + def CostIntervalDefault : Int = { + costIntervalDefault + } + def CostIntervalDefault_=(interval : Int) : Int = { + costIntervalDefault = interval + CostIntervalDefault + } + + def GetCostIntervalByExoSuit(exosuit : ExoSuitType.Value) : Int = costIntervalByExoSuit.getOrElse(exosuit, CostIntervalDefault) + def CostIntervalByExoSuitHashMap : mutable.Map[ExoSuitType.Value, Int] = costIntervalByExoSuit def Type : ImplantType.Value = ImplantType(implantType) } 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 242a20b5..62af14af 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 @@ -154,6 +154,8 @@ object ResolutionCalculations { player.Armor = result._1 b = result._2 + + val originalHealth = player.Health // Then bleed through to health if armour ran out result = SubtractWithRemainder(player.Health, b) player.Health = result._1 @@ -163,6 +165,12 @@ object ResolutionCalculations { result = SubtractWithRemainder(player.Health, a) player.Health = result._1 a = result._2 + + // 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 + } } case _ => } diff --git a/common/src/main/scala/net/psforever/packet/game/ActionProgressMessage.scala b/common/src/main/scala/net/psforever/packet/game/ActionProgressMessage.scala index f9d3f6d9..64823204 100644 --- a/common/src/main/scala/net/psforever/packet/game/ActionProgressMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ActionProgressMessage.scala @@ -6,9 +6,9 @@ import scodec.Codec import scodec.codecs._ /** - * + * 6,7,8 - Start implant initialization timer for slots 0,1,2 respectively. Allowed values: 0-100 (50 will start timer at 50% complete) */ -final case class ActionProgressMessage(unk1 : Int, +final case class ActionProgressMessage(action : Int, unk2 : Long) extends PlanetSideGamePacket { type Packet = ActionProgressMessage @@ -18,7 +18,7 @@ final case class ActionProgressMessage(unk1 : Int, object ActionProgressMessage extends Marshallable[ActionProgressMessage] { implicit val codec : Codec[ActionProgressMessage] = ( - ("unk1" | uint4L) :: + ("action" | uint4L) :: ("unk2" | uint32L) ).as[ActionProgressMessage] } \ No newline at end of file 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 c1bfafea..9c1491a4 100644 --- a/common/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala @@ -2,7 +2,7 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import net.psforever.types.PlanetSideGUID +import net.psforever.types.{ImplantType, PlanetSideGUID} import scodec.Codec import scodec.codecs._ @@ -85,6 +85,23 @@ final case class CreateShortcutMessage(player_guid : PlanetSideGUID, object Shortcut extends Marshallable[Shortcut] { // Convenient predefined Shortcuts for the Medkit and Implants + + /** + 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 + ) + /** * Preset for the Audio Amplifier implant. */ final val AUDIO_AMPLIFIER : Some[Shortcut] = Some(Shortcut(2, "audio_amplifier")) diff --git a/common/src/main/scala/services/avatar/AvatarService.scala b/common/src/main/scala/services/avatar/AvatarService.scala index 0a338ad0..93faada0 100644 --- a/common/src/main/scala/services/avatar/AvatarService.scala +++ b/common/src/main/scala/services/avatar/AvatarService.scala @@ -74,6 +74,10 @@ class AvatarService(zone : Zone) extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.DeactivateImplantSlot(slot)) ) + case AvatarAction.ActivateImplantSlot(player_guid, slot) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ActivateImplantSlot(slot)) + ) case AvatarAction.DeployItem(player_guid, item) => val definition = item.Definition val objectData = definition.Packet.ConstructorData(item).get diff --git a/common/src/main/scala/services/avatar/AvatarServiceMessage.scala b/common/src/main/scala/services/avatar/AvatarServiceMessage.scala index 67188d97..e7fc9dab 100644 --- a/common/src/main/scala/services/avatar/AvatarServiceMessage.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceMessage.scala @@ -8,6 +8,7 @@ import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.Container import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket +import net.psforever.packet.game.ImplantAction import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent} import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} @@ -32,6 +33,7 @@ object AvatarAction { final case class EnvironmentalDamage(player_guid : PlanetSideGUID, source_guid : PlanetSideGUID, amount: Int) extends Action final case class DeployItem(player_guid : PlanetSideGUID, item : PlanetSideGameObject with Deployable) extends Action final case class DeactivateImplantSlot(player_guid : PlanetSideGUID, slot : Int) extends Action + final case class ActivateImplantSlot(player_guid : PlanetSideGUID, slot : Int) extends Action final case class Destroy(victim : PlanetSideGUID, killer : PlanetSideGUID, weapon : PlanetSideGUID, pos : Vector3) extends Action final case class DestroyDisplay(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int = 121) extends Action final case class DropItem(player_guid : PlanetSideGUID, item : Equipment, zone : Zone) extends Action diff --git a/common/src/main/scala/services/avatar/AvatarServiceResponse.scala b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala index 21519842..7920703f 100644 --- a/common/src/main/scala/services/avatar/AvatarServiceResponse.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala @@ -6,7 +6,7 @@ import net.psforever.objects.ballistics.{Projectile, SourceEntry} import net.psforever.objects.equipment.Equipment import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.objectcreate.ConstructorData -import net.psforever.packet.game.ObjectCreateMessage +import net.psforever.packet.game.{ImplantAction, ObjectCreateMessage} import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} import services.GenericEventBusMsg @@ -26,6 +26,7 @@ object AvatarResponse { final case class ConcealPlayer() extends Response final case class EnvironmentalDamage(target : PlanetSideGUID, source_guid : PlanetSideGUID, amount : Int) extends Response final case class DeactivateImplantSlot(slot : Int) extends Response + final case class ActivateImplantSlot(slot : Int) extends Response final case class Destroy(victim : PlanetSideGUID, killer : PlanetSideGUID, weapon : PlanetSideGUID, pos : Vector3) extends Response final case class DestroyDisplay(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int) extends Response final case class DropItem(pkt : ObjectCreateMessage) extends Response diff --git a/common/src/test/scala/objects/ImplantTest.scala b/common/src/test/scala/objects/ImplantTest.scala index e7bc2390..81b3d797 100644 --- a/common/src/test/scala/objects/ImplantTest.scala +++ b/common/src/test/scala/objects/ImplantTest.scala @@ -2,31 +2,25 @@ package objects import net.psforever.objects.ImplantSlot -import net.psforever.objects.definition.{ImplantDefinition, Stance} +import net.psforever.objects.definition.ImplantDefinition import net.psforever.types.{ExoSuitType, ImplantType} import org.specs2.mutable._ class ImplantTest extends Specification { val sample = new ImplantDefinition(8) //variant of sensor shield/silent run - sample.Initialization = 90000 //1:30 - sample.ActivationCharge = 3 - sample.DurationChargeBase = 1 - sample.DurationChargeByExoSuit += ExoSuitType.Agile -> 2 - sample.DurationChargeByExoSuit += ExoSuitType.Reinforced -> 2 - sample.DurationChargeByExoSuit += ExoSuitType.Standard -> 1 - sample.DurationChargeByStance += Stance.Running -> 1 + sample.InitializationDuration = 90 //1:30 + sample.ActivationStaminaCost = 3 + sample.StaminaCost = 1 + sample.CostIntervalDefault = 1000 + sample.CostIntervalByExoSuitHashMap += ExoSuitType.Agile -> 500 "ImplantDefinition" should { "define" in { - sample.Initialization mustEqual 90000 - sample.ActivationCharge mustEqual 3 - sample.DurationChargeBase mustEqual 1 - sample.DurationChargeByExoSuit(ExoSuitType.Agile) mustEqual 2 - sample.DurationChargeByExoSuit(ExoSuitType.Reinforced) mustEqual 2 - sample.DurationChargeByExoSuit(ExoSuitType.Standard) mustEqual 1 - sample.DurationChargeByExoSuit(ExoSuitType.Infiltration) mustEqual 0 //default value - sample.DurationChargeByStance(Stance.Running) mustEqual 1 - sample.DurationChargeByStance(Stance.Crouching) mustEqual 0 //default value + sample.InitializationDuration mustEqual 90 + sample.ActivationStaminaCost mustEqual 3 + sample.StaminaCost mustEqual 1 + sample.GetCostIntervalByExoSuit(ExoSuitType.Reinforced) mustEqual 1000 // Default value + sample.GetCostIntervalByExoSuit(ExoSuitType.Agile) mustEqual 500 // Overridden value sample.Type mustEqual ImplantType.SilentRun } } @@ -73,14 +67,14 @@ class ImplantTest extends Specification { obj.Unlocked mustEqual true } - "initialize without an implant" in { + "can not initialize without an implant" in { val obj = new ImplantSlot obj.Initialized mustEqual false obj.Initialized = true obj.Initialized mustEqual false } - "initialize an implant" in { + "can initialize an implant" in { val obj = new ImplantSlot obj.Initialized mustEqual false @@ -90,7 +84,7 @@ class ImplantTest extends Specification { obj.Initialized mustEqual true } - "activate an uninitialized implant" in { + "can not activate an uninitialized implant" in { val obj = new ImplantSlot obj.Unlocked = true obj.Implant = sample @@ -101,7 +95,7 @@ class ImplantTest extends Specification { obj.Active mustEqual false } - "activate an initialized implant" in { + "can activate an initialized implant" in { val obj = new ImplantSlot obj.Unlocked = true obj.Implant = sample @@ -120,7 +114,7 @@ class ImplantTest extends Specification { obj.Initialized = true obj.Active mustEqual false obj.ActivationCharge mustEqual 0 - obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 0 + obj.Charge(ExoSuitType.Reinforced) mustEqual 0 } "cost energy while active" in { @@ -131,7 +125,7 @@ class ImplantTest extends Specification { obj.Active = true obj.Active mustEqual true obj.ActivationCharge mustEqual 3 - obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 4 + obj.Charge(ExoSuitType.Reinforced) mustEqual 1 } } } diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 3136b480..597bb76f 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -164,11 +164,13 @@ class WorldSessionActor extends Actor var squadUpdateCounter : Int = 0 val queuedSquadActions : Seq[() => Unit] = Seq(SquadUpdates, NoSquadUpdates, NoSquadUpdates, NoSquadUpdates) - var timeDL : Long = 0 - var timeSurge : Long = 0 lazy val unsignedIntMaxValue : Long = Int.MaxValue.toLong * 2L + 1L var serverTime : Long = 0 + /** Keeps track of the number of PlayerStateMessageUpstream messages received by the client + * As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second */ + private var playerStateMessageUpstreamCount = 0 + var amsSpawnPoints : List[SpawnPoint] = Nil var clientKeepAlive : Cancellable = DefaultCancellable.obj var progressBarUpdate : Cancellable = DefaultCancellable.obj @@ -1382,7 +1384,15 @@ class WorldSessionActor extends Actor AwardBattleExperiencePoints(avatar, 20000000L) avatar.CEP = 600000 + avatar.Implants(0).Unlocked = true + avatar.Implants(0).Implant = GlobalDefinitions.darklight_vision + avatar.Implants(1).Unlocked = true + avatar.Implants(1).Implant = GlobalDefinitions.surge + avatar.Implants(2).Unlocked = true + avatar.Implants(2).Implant = GlobalDefinitions.targeting + player = new Player(avatar) + //xy-coordinates indicate sanctuary spawn bias: player.Position = math.abs(scala.util.Random.nextInt() % avatar.name.hashCode % 4) match { case 0 => Vector3(8192, 8192, 0) //NE @@ -1618,12 +1628,10 @@ class WorldSessionActor extends Actor } case AvatarResponse.DeactivateImplantSlot(slot) => - //temporary solution until implants are finalized - slot match { - case 1 => DeactivateImplantDarkLight() - case 2 => DeactivateImplantSurge() - case _ => ; - } + sendResponse(AvatarImplantMessage(PlanetSideGUID(player.GUID.guid), ImplantAction.Activation, slot, 0)) + + case AvatarResponse.ActivateImplantSlot(slot) => + sendResponse(AvatarImplantMessage(PlanetSideGUID(player.GUID.guid), ImplantAction.Activation, slot, 1)) case AvatarResponse.Destroy(victim, killer, weapon, pos) => // guid = victim // killer = killer ;) @@ -1656,8 +1664,6 @@ class WorldSessionActor extends Actor val respawnTimer = 300000 //milliseconds ToggleMaxSpecialState(enable = false) deadState = DeadState.Dead - timeDL = 0 - timeSurge = 0 continent.GUID(player.VehicleSeated) match { case Some(obj : Vehicle) => TotalDriverVehicleControl(obj) @@ -2643,6 +2649,7 @@ class WorldSessionActor extends Actor 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) { @@ -2676,6 +2683,7 @@ class WorldSessionActor extends Actor if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) { val slot = slotNumber.get log.info(s"$tplayer 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)) } @@ -3162,10 +3170,22 @@ class WorldSessionActor extends Actor sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None)) } (0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => { - sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1)) //init implant slot - sendResponse(AvatarImplantMessage(guid, ImplantAction.Activation, slot, 0)) //deactivate implant + val implantSlot = player.ImplantSlot(slot) + if(implantSlot.Initialized) { + sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1)) + } + 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)) @@ -3830,70 +3850,20 @@ class WorldSessionActor extends Actor beginZoningSetCurrentAvatarFunc(player) case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, jump_thrust, is_cloaking, unk5, unk6) => + playerStateMessageUpstreamCount += 1 val isMoving = WorldEntity.isMoving(vel) val isMovingPlus = isMoving || is_jumping || jump_thrust - //implants and stamina management start - val implantsAreActive = avatar.Implants(0).Active || avatar.Implants(1).Active - val staminaBefore = player.Stamina - val hadStaminaBefore = staminaBefore > 0 - val hasStaminaAfter = if(deadState == DeadState.Alive) { - if(implantsAreActive && hadStaminaBefore) { - val time = System.currentTimeMillis() - if(timeDL != 0) { - val duration = time - timeDL - if(duration > 500) { - val units = (duration / 500).toInt - player.Stamina = player.Stamina - units - timeDL += units * 500 - } - } - if(timeSurge != 0) { - val duration = time - timeSurge - val period = player.ExoSuit match { - case ExoSuitType.Agile => 500 - case ExoSuitType.Reinforced => 333 - case ExoSuitType.Infiltration => 1000 - case ExoSuitType.Standard => 1000 - case _ => 1 - } - if(duration > period) { - val units = (duration / period).toInt - player.Stamina = player.Stamina - units - timeSurge += period * units - } - } - } + + if(deadState == DeadState.Alive && playerStateMessageUpstreamCount % 2 == 0) { // Regen stamina roughly every 500ms if(player.skipStaminaRegenForTurns > 0) { //do not renew stamina for a while player.skipStaminaRegenForTurns -= 1 - player.Stamina > 0 } - else if(player.Stamina == 0 && hadStaminaBefore) { - //if the player lost all stamina this turn (had stamina at the start), do not renew stamina for a while - player.skipStaminaRegenForTurns = 4 - player.Stamina > 0 - } - else if(isMovingPlus || player.Stamina == player.MaxStamina) { - //ineligible for stamina regen - player.Stamina > 0 - } - else { - player.Stamina = player.Stamina + 1 - true + else if(!isMovingPlus && player.Stamina != player.MaxStamina) { + player.Stamina += 1 } } - else { - timeDL = 0 - timeSurge = 0 - false - } - if(staminaBefore != player.Stamina) { //stamina changed - sendResponse(PlanetsideAttributeMessage(player.GUID, 2, player.Stamina)) - } - if(implantsAreActive && !hasStaminaAfter) { //implants deactivated at 0 stamina - DeactivateImplants() - } - //implants and stamina management finish + player.Position = pos player.Velocity = vel player.Orientation = Vector3(player.Orientation.x, pitch, yaw) @@ -4487,7 +4457,6 @@ class WorldSessionActor extends Actor //log.info("AvatarJump: " + msg) player.Stamina = player.Stamina - 10 player.skipStaminaRegenForTurns = math.max(player.skipStaminaRegenForTurns, 5) - sendResponse(PlanetsideAttributeMessage(player.GUID, 2, player.Stamina)) case msg @ ZipLineMessage(player_guid,forwards,action,path_id,pos) => log.info("ZipLineMessage: " + msg) @@ -4706,25 +4675,10 @@ class WorldSessionActor extends Actor log.warn(s"LootItem: can not find where to put $item_guid") } - case msg @ AvatarImplantMessage(_, action, slot, status) => //(player_guid, unk1, unk2, implant) => + case msg @ AvatarImplantMessage(player_guid, action, slot, status) => log.info("AvatarImplantMessage: " + msg) - if (avatar.Implants(slot).Initialized) { - if(action == ImplantAction.Activation && status == 1) { // active - avatar.Implants(slot).Active = true - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, avatar.Implant(slot).id * 2 + 1)) - if (avatar.Implant(slot).id == 3) { - timeDL = System.currentTimeMillis() - player.Stamina = player.Stamina - 3 - sendResponse(PlanetsideAttributeMessage(player.GUID, 2, player.Stamina)) - } - if (avatar.Implant(slot).id == 9) timeSurge = System.currentTimeMillis() - } else if(action == ImplantAction.Activation && status == 0) { //desactive - avatar.Implants(slot).Active = false - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, avatar.Implant(slot).id * 2)) - if (avatar.Implant(slot).id == 3) timeDL = 0 - if (avatar.Implant(slot).id == 9) timeSurge = 0 - } - sendResponse(AvatarImplantMessage(PlanetSideGUID(player.GUID.guid),action,slot,status)) + if(action == ImplantAction.Activation) { + player.Actor ! Player.ImplantActivation(slot, status) } case msg @ UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType) => @@ -4870,7 +4824,6 @@ class WorldSessionActor extends Actor sendResponse(ObjectDeleteMessage(kit.GUID, 0)) taskResolver ! GUIDTask.UnregisterEquipment(kit)(continent.GUID) player.Stamina = player.Stamina + 100 - sendResponse(PlanetsideAttributeMessage(avatar_guid, 2, player.Stamina)) case None => log.error(s"UseItem: anticipated a $kit, but can't find it") } @@ -5326,7 +5279,6 @@ 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 - sendResponse(PlanetsideAttributeMessage(player.GUID, 2, 0)) } prefire = shooting.orElse(Some(weapon_guid)) @@ -5703,7 +5655,24 @@ class WorldSessionActor extends Actor case msg @ TargetingImplantRequest(list) => log.info("TargetingImplantRequest: "+msg) + val targetInfo: List[TargetInfo] = list.flatMap(x => { + continent.GUID(x.target_guid) match { + case Some(player: Player) => + val health = player.Health.toFloat / player.MaxHealth + val armor = if (player.MaxArmor > 0) { + player.Armor.toFloat / player.MaxArmor + } else { + 0 + } + Some(TargetInfo(player.GUID, health, armor)) + case _ => + log.warn(s"Target info requested for guid ${x.target_guid} but is not a player") + None + } + }) + + sendResponse(TargetingInfoMessage(targetInfo)) case msg @ ActionCancelMessage(u1, u2, u3) => log.info("Cancelled: "+msg) @@ -7902,6 +7871,7 @@ class WorldSessionActor extends Actor def RespawnClone(tplayer : Player) : Player = { val faction = tplayer.Faction val obj = Player.Respawn(tplayer) + obj.ResetAllImplants() LoadClassicDefault(obj) obj } @@ -10455,39 +10425,9 @@ class WorldSessionActor extends Actor projectilesToCleanUp(local_index) = false } - /** - * Deactivate all active implants. - * This method is intended to support only the current Live server implants that are functional, - * the darklight vision implant and the surge implant. - */ def DeactivateImplants() : Unit = { - DeactivateImplantDarkLight() - DeactivateImplantSurge() - } - - /** - * Deactivate the darklight vision implant. - * This method is intended to support only the current Live server implants. - */ - def DeactivateImplantDarkLight() : Unit = { - if(avatar.Implants(0).Active && avatar.Implants(0).Implant == ImplantType.DarklightVision) { - avatar.Implants(0).Active = false - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, avatar.Implant(0).id * 2)) - sendResponse(AvatarImplantMessage(PlanetSideGUID(player.GUID.guid), ImplantAction.Activation, 0, 0)) - timeDL = 0 - } - } - - /** - * Deactivate the surge implant. - * This method is intended to support only the current Live server implants. - */ - def DeactivateImplantSurge() : Unit = { - if(avatar.Implants(1).Active && avatar.Implants(0).Implant == ImplantType.Surge) { - avatar.Implants(1).Active = false - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 28, avatar.Implant(1).id * 2)) - sendResponse(AvatarImplantMessage(PlanetSideGUID(player.GUID.guid), ImplantAction.Activation, 1, 0)) - timeSurge = 0 + for(slot <- 0 to player.Implants.length - 1) { + player.Actor ! Player.ImplantActivation(slot, 0) } }