From c2f6baf551bb0431bd4a311de64dc95d96bb21d0 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 14 Apr 2020 15:17:32 -0400 Subject: [PATCH] Destroy and repair (#346) * bog-standard order_terminal amenities now take damage up to the point of destruction and can be repaired from destruction to functional to the point of being fully repaired; this is mostly proof fo concept * restored proper destruction to FacilityTurrets; extended proper rrepairs to FacilityTurrets; co-opted terminal hacking into TerminalControl; started to expand on hacking protocol, but chose restraint * changes made thus that a clear Definition hierarchy is established; all of this is in line with making future changes to repair/destroy variables, and making generic the repair code * all meaningful facility amenities take damage and can be repaired; spawn tubes can be destroyed and the base will properly lose spawns (and show it on the map); some hack logic has been redistributed into the appropriate control objects, following in the wake of repair/damage logic * deployables are repairable; the TRAP has been converted into a ComplexDeployable; changed the nature of the Repairable traits * player bank repair and medapp heal has been moved out from WSA into PlayerControl * overhaul of Progress callback system and the inclusion of player revival as a Progress activity * begun relocating functionality for hacking outside of WSA; set up behavoir mixin for cargo operations, in order to move vehicle hack function, but did not yet integrate * integration of the actor behavior mixin for vehicle cargo operations to support the integration of vehicle hacking finalization * establishing inheritance/override potential of Damageable activity; Generator and SpawnTube map behavior behavior (currently inactive) * ImplantTerminalMech objects now have a 'with-coordinates' constructor and a deprecated 'no-coordinates' constructor; implants mechs and interfaces are now damageable and repairable, and their damage state can also block mounting * generators are destroyed and repaired properly, and even explode, killing a radius-worth of players * destroy and repair pass on deployables, except for explosive types * Damageable pass; client synchronization pass * helpful comments * some tests for damageable and repairable; refined output and repaired existing tests * enabled friendly fire check and recovery * handled friendly fire against allied mines; moved jammer code to common damageable behavior * tweaks to damageability, infantry heal and repair, and sensor and explosive animations * animations; framework for future vitals events; closing database connections * adding some deployable tests; fixing a bunch of other tests; History is back * testing for basic Damageable functions; removing a log message * finicky animation stuff * event messages to the Generator to represent health changes * damage against BFR's is now only used against mythical creatures * test fix --- .../net/psforever/objects/Deployables.scala | 27 +- .../objects/ExplosiveDeployable.scala | 77 +- .../psforever/objects/GlobalDefinitions.scala | 364 +++- .../objects/PlanetSideGameObject.scala | 8 + .../scala/net/psforever/objects/Player.scala | 45 +- .../scala/net/psforever/objects/Players.scala | 49 + .../psforever/objects/SensorDeployable.scala | 92 +- .../objects/ShieldGeneratorDeployable.scala | 119 +- .../net/psforever/objects/SpawnPoint.scala | 2 + .../psforever/objects/TrapDeployable.scala | 52 +- .../psforever/objects/TurretDeployable.scala | 136 +- .../scala/net/psforever/objects/Vehicle.scala | 34 +- .../net/psforever/objects/Vehicles.scala | 116 +- .../objects/avatar/PlayerControl.scala | 258 ++- .../ballistics/ResolvedProjectile.scala | 6 +- .../objects/ce/ComplexDeployable.scala | 4 - .../net/psforever/objects/ce/Deployable.scala | 12 +- .../objects/ce/SimpleDeployable.scala | 2 - .../objects/definition/AvatarDefinition.scala | 4 +- .../definition/ExoSuitDefinition.scala | 8 +- .../SimpleDeployableDefinition.scala | 38 +- .../definition/VehicleDefinition.scala | 15 +- .../converter/SmallDeployableConverter.scala | 2 +- .../objects/serverobject/CommonMessages.scala | 5 +- .../serverobject/damage/Damageable.scala | 114 ++ .../damage/DamageableAmenity.scala | 42 + .../damage/DamageableEntity.scala | 203 ++ .../damage/DamageableMountable.scala | 72 + .../damage/DamageableVehicle.scala | 187 ++ .../damage/DamageableWeaponTurret.scala | 78 + .../objects/serverobject/doors/Door.scala | 1 - .../serverobject/doors/DoorDefinition.scala | 4 +- .../serverobject/generator/Generator.scala | 20 +- .../generator/GeneratorControl.scala | 140 +- .../generator/GeneratorDefinition.scala | 4 +- .../hackable/GenericHackables.scala | 120 ++ .../serverobject/hackable/Hackable.scala | 17 +- .../hackable/HackableBehavior.scala | 7 +- .../hackable/HackableDefinition.scala | 21 + .../implantmech/ImplantTerminalMech.scala | 15 +- .../ImplantTerminalMechControl.scala | 61 +- .../ImplantTerminalMechDefinition.scala | 6 +- .../serverobject/locks/IFFLockControl.scala | 12 + .../locks/IFFLockDefinition.scala | 4 +- .../objects/serverobject/locks/IFFLocks.scala | 17 + .../serverobject/mblocker/LockerControl.scala | 7 + .../mblocker/LockerDefinition.scala | 4 +- .../mount/MountableBehavior.scala | 80 +- .../pad/VehicleSpawnPadDefinition.scala | 4 +- .../painbox/PainboxDefinition.scala | 6 +- .../serverobject/repair/Repairable.scala | 77 + .../repair/RepairableAmenity.scala | 37 + .../repair/RepairableEntity.scala | 108 ++ .../repair/RepairableVehicle.scala | 17 + .../repair/RepairableWeaponTurret.scala | 51 + .../resourcesilo/ResourceSilo.scala | 2 +- .../resourcesilo/ResourceSiloControl.scala | 9 +- .../resourcesilo/ResourceSiloDefinition.scala | 4 +- .../serverobject/structures/Amenity.scala | 14 +- .../structures/AmenityDefinition.scala | 16 + .../serverobject/structures/Building.scala | 24 +- .../structures/BuildingControl.scala | 91 +- .../serverobject/structures/WarpGate.scala | 4 +- .../terminals/CaptureTerminalControl.scala | 12 +- .../terminals/CaptureTerminalDefinition.scala | 4 +- .../terminals/CaptureTerminals.scala | 40 + .../terminals/ProximityTerminalControl.scala | 54 +- .../serverobject/terminals/Terminal.scala | 10 +- .../terminals/TerminalControl.scala | 29 +- .../terminals/TerminalDefinition.scala | 3 +- .../serverobject/tube/SpawnTubeControl.scala | 39 +- .../tube/SpawnTubeDefinition.scala | 5 +- .../serverobject/turret/FacilityTurret.scala | 45 +- .../turret/FacilityTurretControl.scala | 154 +- .../turret/FacilityTurretDefinition.scala | 8 +- .../turret/TurretDefinition.scala | 9 - .../serverobject/turret/WeaponTurret.scala | 63 +- .../serverobject/turret/WeaponTurrets.scala | 50 + .../objects/vehicles/CargoBehavior.scala | 423 ++++ .../psforever/objects/vehicles/Utility.scala | 41 +- .../objects/vehicles/VehicleControl.scala | 173 +- .../objects/vehicles/VehicleManifest.scala | 20 +- .../objects/vital/DamageResistanceModel.scala | 28 +- .../objects/vital/StandardDamages.scala | 18 + .../objects/vital/StandardResistances.scala | 19 +- .../objects/vital/StandardResolutions.scala | 1 + .../psforever/objects/vital/Vitality.scala | 103 +- .../objects/vital/VitalityDefinition.scala | 131 ++ .../objects/vital/VitalsHistory.scala | 109 ++ .../vital/damage/DamageCalculations.scala | 2 +- .../objects/vital/damage/DamageProfile.scala | 15 +- .../resistance/ResistanceCalculations.scala | 19 + .../resolution/ResolutionCalculations.scala | 27 +- .../net/psforever/objects/zones/Zone.scala | 24 +- .../psforever/objects/zones/ZoneActor.scala | 12 +- .../net/psforever/objects/zones/ZoneMap.scala | 4 +- .../objects/zones/ZoneVehicleActor.scala | 2 +- .../game/BuildingInfoUpdateMessage.scala | 16 +- .../packet/game/DamageFeedbackMessage.scala | 20 +- .../game/PlanetsideAttributeMessage.scala | 164 +- .../psforever/packet/game/RepairMessage.scala | 9 +- .../types/PlanetSideGeneratorState.scala | 19 + .../account/AccountIntermediaryService.scala | 12 +- .../services/teamwork/SquadService.scala | 124 +- .../vehicle/support/TurretUpgrader.scala | 5 +- .../game/BuildingInfoUpdateMessageTest.scala | 2 +- .../game/DamageFeedbackMessageTest.scala | 33 +- .../src/test/scala/objects/BuildingTest.scala | 6 +- .../test/scala/objects/DamageModelTests.scala | 2 +- .../test/scala/objects/DamageableTest.scala | 1560 +++++++++++++++ .../test/scala/objects/DeployableTest.scala | 438 ++++- .../scala/objects/FacilityTurretTest.scala | 113 +- .../test/scala/objects/GeneratorTest.scala | 766 ++++++++ .../src/test/scala/objects/IFFLockTest.scala | 4 +- .../scala/objects/PlayerControlTest.scala | 599 ++++++ .../src/test/scala/objects/PlayerTest.scala | 9 +- .../test/scala/objects/ProjectileTest.scala | 5 +- .../test/scala/objects/RepairableTest.scala | 400 ++++ .../test/scala/objects/ResourceSiloTest.scala | 39 +- .../src/test/scala/objects/UtilityTest.scala | 27 +- .../src/test/scala/objects/VehicleTest.scala | 38 +- .../terminal/ImplantTerminalMechTest.scala | 50 +- .../src/main/scala/WorldSessionActor.scala | 1700 ++++++----------- pslogin/src/main/scala/zonemaps/Map06.scala | 2 +- 124 files changed, 8530 insertions(+), 2503 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/Players.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableAmenity.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/hackable/HackableDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLocks.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/repair/Repairable.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableAmenity.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableVehicle.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableWeaponTurret.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/structures/AmenityDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminals.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala create mode 100644 common/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala create mode 100644 common/src/main/scala/net/psforever/objects/vital/VitalityDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala create mode 100644 common/src/main/scala/net/psforever/types/PlanetSideGeneratorState.scala create mode 100644 common/src/test/scala/objects/DamageableTest.scala create mode 100644 common/src/test/scala/objects/GeneratorTest.scala create mode 100644 common/src/test/scala/objects/PlayerControlTest.scala create mode 100644 common/src/test/scala/objects/RepairableTest.scala diff --git a/common/src/main/scala/net/psforever/objects/Deployables.scala b/common/src/main/scala/net/psforever/objects/Deployables.scala index 273fb88d0..a241e9782 100644 --- a/common/src/main/scala/net/psforever/objects/Deployables.scala +++ b/common/src/main/scala/net/psforever/objects/Deployables.scala @@ -2,10 +2,10 @@ package net.psforever.objects import akka.actor.ActorRef -import scala.concurrent.duration._ +import scala.concurrent.duration._ import net.psforever.objects.ce.{Deployable, DeployedItem} -import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.vehicles.{Utility, UtilityType} import net.psforever.objects.zones.Zone import net.psforever.packet.game.{DeployableInfo, DeploymentAction} import net.psforever.types.PlanetSideGUID @@ -13,6 +13,8 @@ import services.RemoverActor import services.local.{LocalAction, LocalServiceMessage} object Deployables { + private val log = org.log4s.getLogger("Deployables") + object Make { def apply(item : DeployedItem.Value) : ()=>PlanetSideGameObject with Deployable = cemap(item) @@ -58,7 +60,7 @@ object Deployables { * @param time length of time that the deployable is allowed to exist in the game world; * `None` indicates the normal un-owned existence time (180 seconds) */ - def AnnounceDestroyDeployable(target : PlanetSideServerObject with Deployable, time : Option[FiniteDuration]) : Unit = { + def AnnounceDestroyDeployable(target : PlanetSideGameObject with Deployable, time : Option[FiniteDuration]) : Unit = { val zone = target.Zone target.OwnerName match { case Some(owner) => @@ -102,4 +104,23 @@ object Deployables { }) boomers ++ deployables } + + def RemoveTelepad(vehicle: Vehicle) : Unit = { + val zone = vehicle.Zone + (vehicle.Utility(UtilityType.internal_router_telepad_deployable) match { + case Some(util : Utility.InternalTelepad) => + val telepad = util.Telepad + util.Telepad = None + zone.GUID(telepad) + case _ => + None + }) match { + case Some(telepad : TelepadDeployable) => + log.info(s"BeforeUnload: deconstructing telepad $telepad that was linked to router $vehicle ...") + telepad.Active = false + zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(telepad), zone)) + zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(telepad, zone, Some(0 seconds))) + case _ => ; + } + } } diff --git a/common/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/common/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 48f62514f..02c348384 100644 --- a/common/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/common/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2018 PSForever package net.psforever.objects import akka.actor.{Actor, ActorContext, Props} @@ -8,8 +8,11 @@ import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDepl import net.psforever.objects.definition.converter.SmallDeployableConverter import net.psforever.objects.equipment.JammableUnit import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.vital.{StandardResolutions, Vitality} +import net.psforever.objects.zones.Zone import net.psforever.types.{PlanetSideGUID, Vector3} +import services.Service import services.avatar.{AvatarAction, AvatarServiceMessage} import services.local.{LocalAction, LocalServiceMessage} @@ -17,14 +20,6 @@ import scala.concurrent.duration._ class ExplosiveDeployable(cdef : ExplosiveDeployableDefinition) extends ComplexDeployable(cdef) with JammableUnit { - private var exploded : Boolean = false - - def Exploded : Boolean = exploded - - def Exploded_=(fuse : Boolean) : Boolean = { - exploded = fuse - Exploded - } override def Definition : ExplosiveDeployableDefinition = cdef } @@ -59,38 +54,48 @@ object ExplosiveDeployableDefinition { } } -class ExplosiveDeployableControl(mine : ExplosiveDeployable) extends Actor { - def receive : Receive = { - case Vitality.Damage(damage_func) => - val originalHealth = mine.Health - if(originalHealth > 0) { - val cause = damage_func(mine) - ExplosiveDeployableControl.HandleDamageResolution(mine, cause, originalHealth - mine.Health) - } +class ExplosiveDeployableControl(mine : ExplosiveDeployable) extends Actor + with Damageable { + def DamageableObject = mine - case _ => ; + def receive : Receive = takesDamage + .orElse { + case _ => ; + } + + protected def TakesDamage : Receive = { + case Vitality.Damage(applyDamageTo) => + if(mine.CanDamage) { + val originalHealth = mine.Health + val cause = applyDamageTo(mine) + val damage = originalHealth - mine.Health + if(Damageable.CanDamageOrJammer(mine, damage, cause)) { + ExplosiveDeployableControl.DamageResolution(mine, cause, damage) + } + else { + mine.Health = originalHealth + } + } } } object ExplosiveDeployableControl { - def HandleDamageResolution(target : ExplosiveDeployable, cause : ResolvedProjectile, damage : Int) : Unit = { - val zone = target.Zone - val playerGUID = zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match { - case Some(player) => player.GUID - case _ => PlanetSideGUID(0) - } + def DamageResolution(target : ExplosiveDeployable, cause : ResolvedProjectile, damage : Int) : Unit = { + target.History(cause) if(target.Health == 0) { - HandleDestructionAwareness(target, playerGUID, cause) + DestructionAwareness(target, cause) } - else if(!target.Jammed && cause.projectile.profile.JammerProjectile) { + else if(!target.Jammed && Damageable.CanJammer(target, cause)) { if(target.Jammed = { val radius = cause.projectile.profile.DamageRadius Vector3.DistanceSquared(cause.hit_pos, cause.target.Position) < radius * radius }) { if(target.Definition.DetonateOnJamming) { - target.Zone.LocalEvents ! LocalServiceMessage(target.Zone.Id, LocalAction.Detonate(target.GUID, target)) + val zone = target.Zone + zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) + zone.LocalEvents ! LocalServiceMessage(zone.Id, LocalAction.Detonate(target.GUID, target)) } - HandleDestructionAwareness(target, playerGUID, cause) + DestructionAwareness(target, cause) } } } @@ -98,13 +103,19 @@ object ExplosiveDeployableControl { /** * na * @param target na - * @param attribution na - * @param lastShot na + * @param cause na */ - def HandleDestructionAwareness(target : ExplosiveDeployable, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = { + def DestructionAwareness(target : ExplosiveDeployable, cause : ResolvedProjectile) : Unit = { val zone = target.Zone + val attribution = zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match { + case Some(player) => player.GUID + case _ => PlanetSideGUID(0) + } + target.Destroyed = true Deployables.AnnounceDestroyDeployable(target, Some(if(target.Jammed) 0 seconds else 500 milliseconds)) - zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.Destroy(target.GUID, attribution, attribution, target.Position)) + zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.Destroy(target.GUID, attribution, Service.defaultPlayerGUID, target.Position)) + if(target.Health == 0) { + zone.LocalEvents ! LocalServiceMessage(zone.Id, LocalAction.TriggerEffect(Service.defaultPlayerGUID, "detonate_damaged_mine", target.GUID)) + } } } - diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index a3b2164c7..808b572d0 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -19,7 +19,7 @@ import net.psforever.objects.serverobject.tube.SpawnTubeDefinition import net.psforever.objects.serverobject.resourcesilo.ResourceSiloDefinition import net.psforever.objects.serverobject.structures.SphereOfInfluence import net.psforever.objects.serverobject.turret.{FacilityTurretDefinition, TurretUpgrade} -import net.psforever.objects.vehicles.{DestroyedVehicle, SeatArmorRestriction, UtilityType} +import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, SeatArmorRestriction, UtilityType} import net.psforever.objects.vital.{DamageType, StandardMaxDamage, StandardResolutions} import net.psforever.types.{CertificationType, ExoSuitType, PlanetSideEmpire, Vector3} @@ -29,6 +29,8 @@ import scala.concurrent.duration._ object GlobalDefinitions { // Characters val avatar = new AvatarDefinition(121) + avatar.MaxHealth = 100 + avatar.Damageable = true /* exo-suits */ @@ -889,7 +891,7 @@ object GlobalDefinitions { val sensor_shield = SensorDeployableDefinition(DeployedItem.sensor_shield) - val tank_traps = SimpleDeployableDefinition(DeployedItem.tank_traps) + val tank_traps = TrapDeployableDefinition(DeployedItem.tank_traps) val portable_manned_turret = TurretDeployableDefinition(DeployedItem.portable_manned_turret) @@ -904,7 +906,7 @@ object GlobalDefinitions { val router_telepad_deployable = SimpleDeployableDefinition(DeployedItem.router_telepad_deployable) //this is only treated like a deployable - val internal_router_telepad_deployable = SimpleDeployableDefinition(DeployedItem.router_telepad_deployable) + val internal_router_telepad_deployable = InternalTelepadDefinition() //objectId: 744 init_deployables() /* @@ -1626,7 +1628,7 @@ object GlobalDefinitions { max.ResistanceDirectHit = 6 max.ResistanceSplash = 35 max.ResistanceAggravated = 10 - max.Damage = StandardMaxDamage + max.DamageUsing = StandardMaxDamage max.Model = StandardResolutions.Max } @@ -4470,7 +4472,10 @@ object GlobalDefinitions { nano_dispenser.FireModes.head.AmmoSlotIndex = 0 nano_dispenser.FireModes.head.Magazine = 100 nano_dispenser.FireModes.head.CustomMagazine = Ammo.upgrade_canister -> 1 + nano_dispenser.FireModes.head.Modifiers.Damage0 = 0 nano_dispenser.FireModes.head.Modifiers.Damage1 = 20 + nano_dispenser.FireModes.head.Modifiers.Damage2 = 0 + nano_dispenser.FireModes.head.Modifiers.Damage3 = 0 nano_dispenser.FireModes.head.Modifiers.Damage4 = 20 nano_dispenser.Tile = InventoryTile.Tile63 @@ -5156,6 +5161,9 @@ object GlobalDefinitions { private def init_vehicles() : Unit = { fury.Name = "fury" fury.MaxHealth = 650 + fury.Damageable = true + fury.Repairable = true + fury.RepairIfDestroyed = false fury.MaxShields = 130 + 1 fury.Seats += 0 -> new SeatDefinition() fury.Seats(0).Bailable = true @@ -5171,6 +5179,9 @@ object GlobalDefinitions { quadassault.Name = "quadassault" // Basilisk quadassault.MaxHealth = 650 + quadassault.Damageable = true + quadassault.Repairable = true + quadassault.RepairIfDestroyed = false quadassault.MaxShields = 130 + 1 quadassault.Seats += 0 -> new SeatDefinition() quadassault.Seats(0).Bailable = true @@ -5186,6 +5197,9 @@ object GlobalDefinitions { quadstealth.Name = "quadstealth" // Wraith quadstealth.MaxHealth = 650 + quadstealth.Damageable = true + quadstealth.Repairable = true + quadstealth.RepairIfDestroyed = false quadstealth.MaxShields = 130 + 1 quadstealth.CanCloak = true quadstealth.Seats += 0 -> new SeatDefinition() @@ -5201,6 +5215,9 @@ object GlobalDefinitions { two_man_assault_buggy.Name = "two_man_assault_buggy" // Harasser two_man_assault_buggy.MaxHealth = 1250 + two_man_assault_buggy.Damageable = true + two_man_assault_buggy.Repairable = true + two_man_assault_buggy.RepairIfDestroyed = false two_man_assault_buggy.MaxShields = 250 + 1 two_man_assault_buggy.Seats += 0 -> new SeatDefinition() two_man_assault_buggy.Seats(0).Bailable = true @@ -5218,6 +5235,9 @@ object GlobalDefinitions { skyguard.Name = "skyguard" skyguard.MaxHealth = 1000 + skyguard.Damageable = true + skyguard.Repairable = true + skyguard.RepairIfDestroyed = false skyguard.MaxShields = 200 + 1 skyguard.Seats += 0 -> new SeatDefinition() skyguard.Seats(0).Bailable = true @@ -5236,6 +5256,9 @@ object GlobalDefinitions { threemanheavybuggy.Name = "threemanheavybuggy" // Marauder threemanheavybuggy.MaxHealth = 1700 + threemanheavybuggy.Damageable = true + threemanheavybuggy.Repairable = true + threemanheavybuggy.RepairIfDestroyed = false threemanheavybuggy.MaxShields = 340 + 1 threemanheavybuggy.Seats += 0 -> new SeatDefinition() threemanheavybuggy.Seats(0).Bailable = true @@ -5259,6 +5282,9 @@ object GlobalDefinitions { twomanheavybuggy.Name = "twomanheavybuggy" // Enforcer twomanheavybuggy.MaxHealth = 1800 + twomanheavybuggy.Damageable = true + twomanheavybuggy.Repairable = true + twomanheavybuggy.RepairIfDestroyed = false twomanheavybuggy.MaxShields = 360 + 1 twomanheavybuggy.Seats += 0 -> new SeatDefinition() twomanheavybuggy.Seats(0).Bailable = true @@ -5277,6 +5303,9 @@ object GlobalDefinitions { twomanhoverbuggy.Name = "twomanhoverbuggy" // Thresher twomanhoverbuggy.MaxHealth = 1600 + twomanhoverbuggy.Damageable = true + twomanhoverbuggy.Repairable = true + twomanhoverbuggy.RepairIfDestroyed = false twomanhoverbuggy.MaxShields = 320 + 1 twomanhoverbuggy.Seats += 0 -> new SeatDefinition() twomanhoverbuggy.Seats(0).Bailable = true @@ -5295,6 +5324,9 @@ object GlobalDefinitions { mediumtransport.Name = "mediumtransport" // Deliverer mediumtransport.MaxHealth = 2500 + mediumtransport.Damageable = true + mediumtransport.Repairable = true + mediumtransport.RepairIfDestroyed = false mediumtransport.MaxShields = 500 + 1 mediumtransport.Seats += 0 -> new SeatDefinition() mediumtransport.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5320,6 +5352,9 @@ object GlobalDefinitions { battlewagon.Name = "battlewagon" // Raider battlewagon.MaxHealth = 2500 + battlewagon.Damageable = true + battlewagon.Repairable = true + battlewagon.RepairIfDestroyed = false battlewagon.MaxShields = 500 + 1 battlewagon.Seats += 0 -> new SeatDefinition() battlewagon.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5348,6 +5383,9 @@ object GlobalDefinitions { thunderer.Name = "thunderer" thunderer.MaxHealth = 2500 + thunderer.Damageable = true + thunderer.Repairable = true + thunderer.RepairIfDestroyed = false thunderer.MaxShields = 500 + 1 thunderer.Seats += 0 -> new SeatDefinition() thunderer.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5373,6 +5411,9 @@ object GlobalDefinitions { aurora.Name = "aurora" aurora.MaxHealth = 2500 + aurora.Damageable = true + aurora.Repairable = true + aurora.RepairIfDestroyed = false aurora.MaxShields = 500 + 1 aurora.Seats += 0 -> new SeatDefinition() aurora.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5398,6 +5439,9 @@ object GlobalDefinitions { apc_tr.Name = "apc_tr" // Juggernaut apc_tr.MaxHealth = 6000 + apc_tr.Damageable = true + apc_tr.Repairable = true + apc_tr.RepairIfDestroyed = false apc_tr.MaxShields = 1200 + 1 apc_tr.Seats += 0 -> new SeatDefinition() apc_tr.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5445,6 +5489,9 @@ object GlobalDefinitions { apc_nc.Name = "apc_nc" // Vindicator apc_nc.MaxHealth = 6000 + apc_nc.Damageable = true + apc_nc.Repairable = true + apc_nc.RepairIfDestroyed = false apc_nc.MaxShields = 1200 + 1 apc_nc.Seats += 0 -> new SeatDefinition() apc_nc.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5492,6 +5539,9 @@ object GlobalDefinitions { apc_vs.Name = "apc_vs" // Leviathan apc_vs.MaxHealth = 6000 + apc_vs.Damageable = true + apc_vs.Repairable = true + apc_vs.RepairIfDestroyed = false apc_vs.MaxShields = 1200 + 1 apc_vs.Seats += 0 -> new SeatDefinition() apc_vs.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5539,6 +5589,9 @@ object GlobalDefinitions { lightning.Name = "lightning" lightning.MaxHealth = 2000 + lightning.Damageable = true + lightning.Repairable = true + lightning.RepairIfDestroyed = false lightning.MaxShields = 400 + 1 lightning.Seats += 0 -> new SeatDefinition() lightning.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5555,6 +5608,9 @@ object GlobalDefinitions { prowler.Name = "prowler" prowler.MaxHealth = 4800 + prowler.Damageable = true + prowler.Repairable = true + prowler.RepairIfDestroyed = false prowler.MaxShields = 960 + 1 prowler.Seats += 0 -> new SeatDefinition() prowler.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5576,6 +5632,9 @@ object GlobalDefinitions { vanguard.Name = "vanguard" vanguard.MaxHealth = 5400 + vanguard.Damageable = true + vanguard.Repairable = true + vanguard.RepairIfDestroyed = false vanguard.MaxShields = 1080 + 1 vanguard.Seats += 0 -> new SeatDefinition() vanguard.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5593,6 +5652,9 @@ object GlobalDefinitions { magrider.Name = "magrider" magrider.MaxHealth = 4200 + magrider.Damageable = true + magrider.Repairable = true + magrider.RepairIfDestroyed = false magrider.MaxShields = 840 + 1 magrider.Seats += 0 -> new SeatDefinition() magrider.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5613,6 +5675,9 @@ object GlobalDefinitions { val utilityConverter = new UtilityVehicleConverter ant.Name = "ant" ant.MaxHealth = 2000 + ant.Damageable = true + ant.Repairable = true + ant.RepairIfDestroyed = false ant.MaxShields = 400 + 1 ant.Seats += 0 -> new SeatDefinition() ant.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5630,6 +5695,9 @@ object GlobalDefinitions { ams.Name = "ams" ams.MaxHealth = 3000 + ams.Damageable = true + ams.Repairable = true + ams.RepairIfDestroyed = false ams.MaxShields = 600 + 1 ams.Seats += 0 -> new SeatDefinition() ams.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax @@ -5652,6 +5720,9 @@ object GlobalDefinitions { val variantConverter = new VariantVehicleConverter router.Name = "router" router.MaxHealth = 4000 + router.Damageable = true + router.Repairable = true + router.RepairIfDestroyed = false router.MaxShields = 800 + 1 router.Seats += 0 -> new SeatDefinition() router.MountPoints += 1 -> 0 @@ -5671,6 +5742,9 @@ object GlobalDefinitions { switchblade.Name = "switchblade" switchblade.MaxHealth = 1750 + switchblade.Damageable = true + switchblade.Repairable = true + switchblade.RepairIfDestroyed = false switchblade.MaxShields = 350 + 1 switchblade.Seats += 0 -> new SeatDefinition() switchblade.Seats(0).ControlledWeapon = 1 @@ -5691,6 +5765,9 @@ object GlobalDefinitions { flail.Name = "flail" flail.MaxHealth = 2400 + flail.Damageable = true + flail.Repairable = true + flail.RepairIfDestroyed = false flail.MaxShields = 480 + 1 flail.Seats += 0 -> new SeatDefinition() flail.Seats(0).ControlledWeapon = 1 @@ -5709,6 +5786,9 @@ object GlobalDefinitions { mosquito.Name = "mosquito" mosquito.MaxHealth = 665 + mosquito.Damageable = true + mosquito.Repairable = true + mosquito.RepairIfDestroyed = false mosquito.MaxShields = 133 + 1 mosquito.CanFly = true mosquito.Seats += 0 -> new SeatDefinition() @@ -5726,6 +5806,9 @@ object GlobalDefinitions { lightgunship.Name = "lightgunship" // Reaver lightgunship.MaxHealth = 1000 + lightgunship.Damageable = true + lightgunship.Repairable = true + lightgunship.RepairIfDestroyed = false lightgunship.MaxShields = 200 + 1 lightgunship.CanFly = true lightgunship.Seats += 0 -> new SeatDefinition() @@ -5744,6 +5827,9 @@ object GlobalDefinitions { wasp.Name = "wasp" wasp.MaxHealth = 515 + wasp.Damageable = true + wasp.Repairable = true + wasp.RepairIfDestroyed = false wasp.MaxShields = 103 + 1 wasp.CanFly = true wasp.Seats += 0 -> new SeatDefinition() @@ -5761,6 +5847,9 @@ object GlobalDefinitions { liberator.Name = "liberator" liberator.MaxHealth = 2500 + liberator.Damageable = true + liberator.Repairable = true + liberator.RepairIfDestroyed = false liberator.MaxShields = 500 + 1 liberator.CanFly = true liberator.Seats += 0 -> new SeatDefinition() @@ -5786,6 +5875,9 @@ object GlobalDefinitions { vulture.Name = "vulture" vulture.MaxHealth = 2500 + vulture.Damageable = true + vulture.Repairable = true + vulture.RepairIfDestroyed = false vulture.MaxShields = 500 + 1 vulture.CanFly = true vulture.Seats += 0 -> new SeatDefinition() @@ -5811,6 +5903,10 @@ object GlobalDefinitions { dropship.Name = "dropship" // Galaxy dropship.MaxHealth = 5000 + dropship.Damageable = true + dropship.Repairable = true + dropship.RepairDistance = 20 + dropship.RepairIfDestroyed = false dropship.MaxShields = 1000 + 1 dropship.CanFly = true dropship.Seats += 0 -> new SeatDefinition() @@ -5868,6 +5964,10 @@ object GlobalDefinitions { galaxy_gunship.Name = "galaxy_gunship" galaxy_gunship.MaxHealth = 6000 + galaxy_gunship.Damageable = true + galaxy_gunship.Repairable = true + galaxy_gunship.RepairDistance = 20 + galaxy_gunship.RepairIfDestroyed = false galaxy_gunship.MaxShields = 1200 + 1 galaxy_gunship.CanFly = true galaxy_gunship.Seats += 0 -> new SeatDefinition() @@ -5902,6 +6002,10 @@ object GlobalDefinitions { lodestar.Name = "lodestar" lodestar.MaxHealth = 5000 + lodestar.Damageable = true + lodestar.Repairable = true + lodestar.RepairDistance = 20 + lodestar.RepairIfDestroyed = false lodestar.MaxShields = 1000 + 1 lodestar.CanFly = true lodestar.Seats += 0 -> new SeatDefinition() @@ -5926,6 +6030,9 @@ object GlobalDefinitions { phantasm.Name = "phantasm" phantasm.MaxHealth = 2500 + phantasm.Damageable = true + phantasm.Repairable = true + phantasm.RepairIfDestroyed = false phantasm.MaxShields = 500 + 1 phantasm.CanCloak = true phantasm.CanFly = true @@ -5958,23 +6065,35 @@ object GlobalDefinitions { boomer.Name = "boomer" boomer.Descriptor = "Boomers" boomer.MaxHealth = 100 + boomer.Damageable = true + boomer.DamageableByFriendlyFire = false + boomer.Repairable = false boomer.DeployCategory = DeployableCategory.Boomers boomer.DeployTime = Duration.create(1000, "ms") he_mine.Name = "he_mine" he_mine.Descriptor = "Mines" he_mine.MaxHealth = 100 + he_mine.Damageable = true + he_mine.DamageableByFriendlyFire = false + he_mine.Repairable = false he_mine.DeployTime = Duration.create(1000, "ms") jammer_mine.Name = "jammer_mine" jammer_mine.Descriptor = "JammerMines" jammer_mine.MaxHealth = 100 + jammer_mine.Damageable = true + jammer_mine.DamageableByFriendlyFire = false + jammer_mine.Repairable = false jammer_mine.DeployTime = Duration.create(1000, "ms") jammer_mine.DetonateOnJamming = false spitfire_turret.Name = "spitfire_turret" spitfire_turret.Descriptor = "Spitfires" spitfire_turret.MaxHealth = 100 + spitfire_turret.Damageable = true + spitfire_turret.Repairable = true + spitfire_turret.RepairIfDestroyed = false spitfire_turret.Weapons += 1 -> new mutable.HashMap() spitfire_turret.Weapons(1) += TurretUpgrade.None -> spitfire_weapon spitfire_turret.ReserveAmmunition = false @@ -5985,6 +6104,9 @@ object GlobalDefinitions { spitfire_cloaked.Name = "spitfire_cloaked" spitfire_cloaked.Descriptor = "CloakingSpitfires" spitfire_cloaked.MaxHealth = 100 + spitfire_cloaked.Damageable = true + spitfire_cloaked.Repairable = true + spitfire_cloaked.RepairIfDestroyed = false spitfire_cloaked.Weapons += 1 -> new mutable.HashMap() spitfire_cloaked.Weapons(1) += TurretUpgrade.None -> spitfire_weapon spitfire_cloaked.ReserveAmmunition = false @@ -5995,6 +6117,9 @@ object GlobalDefinitions { spitfire_aa.Name = "spitfire_aa" spitfire_aa.Descriptor = "FlakSpitfires" spitfire_aa.MaxHealth = 100 + spitfire_aa.Damageable = true + spitfire_aa.Repairable = true + spitfire_aa.RepairIfDestroyed = false spitfire_aa.Weapons += 1 -> new mutable.HashMap() spitfire_aa.Weapons(1) += TurretUpgrade.None -> spitfire_aa_weapon spitfire_aa.ReserveAmmunition = false @@ -6005,17 +6130,25 @@ object GlobalDefinitions { motionalarmsensor.Name = "motionalarmsensor" motionalarmsensor.Descriptor = "MotionSensors" motionalarmsensor.MaxHealth = 100 + motionalarmsensor.Damageable = true + motionalarmsensor.Repairable = true + motionalarmsensor.RepairIfDestroyed = false motionalarmsensor.DeployTime = Duration.create(1000, "ms") sensor_shield.Name = "sensor_shield" sensor_shield.Descriptor = "SensorShields" sensor_shield.MaxHealth = 100 + sensor_shield.Damageable = true + sensor_shield.Repairable = true + sensor_shield.RepairIfDestroyed = false sensor_shield.DeployTime = Duration.create(5000, "ms") tank_traps.Name = "tank_traps" tank_traps.Descriptor = "TankTraps" tank_traps.MaxHealth = 5000 - tank_traps.Packet = new TRAPConverter + tank_traps.Damageable = true + tank_traps.Repairable = true + tank_traps.RepairIfDestroyed = false tank_traps.DeployCategory = DeployableCategory.TankTraps tank_traps.DeployTime = Duration.create(6000, "ms") tank_traps.Model = StandardResolutions.SimpleDeployables @@ -6024,6 +6157,9 @@ object GlobalDefinitions { portable_manned_turret.Name = "portable_manned_turret" portable_manned_turret.Descriptor = "FieldTurrets" portable_manned_turret.MaxHealth = 1000 + portable_manned_turret.Damageable = true + portable_manned_turret.Repairable = true + portable_manned_turret.RepairIfDestroyed = false portable_manned_turret.MountPoints += 1 -> 0 portable_manned_turret.MountPoints += 2 -> 0 portable_manned_turret.Weapons += 1 -> new mutable.HashMap() @@ -6038,6 +6174,9 @@ object GlobalDefinitions { portable_manned_turret_nc.Name = "portable_manned_turret_nc" portable_manned_turret_nc.Descriptor = "FieldTurrets" portable_manned_turret_nc.MaxHealth = 1000 + portable_manned_turret_nc.Damageable = true + portable_manned_turret_nc.Repairable = true + portable_manned_turret_nc.RepairIfDestroyed = false portable_manned_turret_nc.MountPoints += 1 -> 0 portable_manned_turret_nc.MountPoints += 2 -> 0 portable_manned_turret_nc.Weapons += 1 -> new mutable.HashMap() @@ -6052,6 +6191,9 @@ object GlobalDefinitions { portable_manned_turret_tr.Name = "portable_manned_turret_tr" portable_manned_turret_tr.Descriptor = "FieldTurrets" portable_manned_turret_tr.MaxHealth = 1000 + portable_manned_turret_tr.Damageable = true + portable_manned_turret_tr.Repairable = true + portable_manned_turret_tr.RepairIfDestroyed = false portable_manned_turret_tr.MountPoints += 1 -> 0 portable_manned_turret_tr.MountPoints += 2 -> 0 portable_manned_turret_tr.Weapons += 1 -> new mutable.HashMap() @@ -6066,6 +6208,9 @@ object GlobalDefinitions { portable_manned_turret_vs.Name = "portable_manned_turret_vs" portable_manned_turret_vs.Descriptor = "FieldTurrets" portable_manned_turret_vs.MaxHealth = 1000 + portable_manned_turret_vs.Damageable = true + portable_manned_turret_vs.Repairable = true + portable_manned_turret_vs.RepairIfDestroyed = false portable_manned_turret_vs.MountPoints += 1 -> 0 portable_manned_turret_vs.MountPoints += 2 -> 0 portable_manned_turret_vs.Weapons += 1 -> new mutable.HashMap() @@ -6080,18 +6225,27 @@ object GlobalDefinitions { deployable_shield_generator.Name = "deployable_shield_generator" deployable_shield_generator.Descriptor = "ShieldGenerators" deployable_shield_generator.MaxHealth = 1700 + deployable_shield_generator.Damageable = true + deployable_shield_generator.Repairable = true + deployable_shield_generator.RepairIfDestroyed = false deployable_shield_generator.DeployTime = Duration.create(6000, "ms") deployable_shield_generator.Model = StandardResolutions.ComplexDeployables router_telepad_deployable.Name = "router_telepad_deployable" router_telepad_deployable.MaxHealth = 100 + router_telepad_deployable.Damageable = true + router_telepad_deployable.Repairable = false router_telepad_deployable.DeployTime = Duration.create(1, "ms") + router_telepad_deployable.DeployCategory = DeployableCategory.Telepads router_telepad_deployable.Packet = new TelepadDeployableConverter router_telepad_deployable.Model = StandardResolutions.SimpleDeployables internal_router_telepad_deployable.Name = "router_telepad_deployable" internal_router_telepad_deployable.MaxHealth = 1 + internal_router_telepad_deployable.Damageable = false + internal_router_telepad_deployable.Repairable = false internal_router_telepad_deployable.DeployTime = Duration.create(1, "ms") + internal_router_telepad_deployable.DeployCategory = DeployableCategory.Telepads internal_router_telepad_deployable.Packet = new InternalTelepadDeployableConverter } @@ -6102,14 +6256,24 @@ object GlobalDefinitions { ams_respawn_tube.Name = "ams_respawn_tube" ams_respawn_tube.Delay = 5 ams_respawn_tube.SpecificPointFunc = SpawnPoint.AMS + ams_respawn_tube.Damageable = false + ams_respawn_tube.Repairable = false matrix_terminala.Name = "matrix_terminala" + matrix_terminala.Damageable = false + matrix_terminala.Repairable = false matrix_terminalb.Name = "matrix_terminalb" + matrix_terminalb.Damageable = false + matrix_terminalb.Repairable = false matrix_terminalc.Name = "matrix_terminalc" + matrix_terminalc.Damageable = false + matrix_terminalc.Repairable = false spawn_terminal.Name = "spawn_terminal" + spawn_terminal.Damageable = false + spawn_terminal.Repairable = false order_terminal.Name = "order_terminal" order_terminal.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons) @@ -6118,6 +6282,11 @@ object GlobalDefinitions { order_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) order_terminal.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage() order_terminal.SellEquipmentByDefault = true + order_terminal.MaxHealth = 500 + order_terminal.Damageable = true + order_terminal.Repairable = true + order_terminal.RepairIfDestroyed = true + order_terminal.Subtract.Damage1 = 8 order_terminala.Name = "order_terminala" order_terminala.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons) @@ -6127,6 +6296,8 @@ object GlobalDefinitions { order_terminala.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage() order_terminala.Tab(4).asInstanceOf[OrderTerminalDefinition.InfantryLoadoutPage].Exclude = ExoSuitType.MAX order_terminala.SellEquipmentByDefault = true + order_terminala.Damageable = false + order_terminala.Repairable = false order_terminalb.Name = "order_terminalb" order_terminalb.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons) @@ -6136,6 +6307,8 @@ object GlobalDefinitions { order_terminalb.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage() order_terminalb.Tab(4).asInstanceOf[OrderTerminalDefinition.InfantryLoadoutPage].Exclude = ExoSuitType.MAX order_terminalb.SellEquipmentByDefault = true + order_terminalb.Damageable = false + order_terminalb.Repairable = false vanu_equipment_term.Name = "vanu_equipment_term" vanu_equipment_term.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.infantryAmmunition ++ EquipmentTerminalDefinition.infantryWeapons) @@ -6144,55 +6317,124 @@ object GlobalDefinitions { vanu_equipment_term.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) vanu_equipment_term.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage() vanu_equipment_term.SellEquipmentByDefault = true + vanu_equipment_term.Damageable = false + vanu_equipment_term.Repairable = false cert_terminal.Name = "cert_terminal" cert_terminal.Tab += 0 -> OrderTerminalDefinition.CertificationPage(CertTerminalDefinition.certs) + cert_terminal.MaxHealth = 500 + cert_terminal.Damageable = true + cert_terminal.Repairable = true + cert_terminal.RepairIfDestroyed = true + cert_terminal.Subtract.Damage1 = 8 + + implant_terminal_mech.Name = "implant_terminal_mech" + implant_terminal_mech.MaxHealth = 1500 //TODO 1000; right now, 1000 (mech) + 500 (interface) + implant_terminal_mech.Damageable = true + implant_terminal_mech.Repairable = true + implant_terminal_mech.RepairIfDestroyed = true implant_terminal_interface.Name = "implant_terminal_interface" implant_terminal_interface.Tab += 0 -> OrderTerminalDefinition.ImplantPage(ImplantTerminalDefinition.implants) + implant_terminal_interface.MaxHealth = 500 + implant_terminal_interface.Damageable = false //TODO true + implant_terminal_interface.Repairable = true + implant_terminal_interface.RepairIfDestroyed = true ground_vehicle_terminal.Name = "ground_vehicle_terminal" ground_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(VehicleTerminalDefinition.groundVehicles, VehicleTerminalDefinition.trunk) ground_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + ground_vehicle_terminal.MaxHealth = 500 + ground_vehicle_terminal.Damageable = true + ground_vehicle_terminal.Repairable = true + ground_vehicle_terminal.RepairIfDestroyed = true + ground_vehicle_terminal.Subtract.Damage1 = 8 air_vehicle_terminal.Name = "air_vehicle_terminal" air_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(VehicleTerminalDefinition.flight1Vehicles, VehicleTerminalDefinition.trunk) air_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + air_vehicle_terminal.MaxHealth = 500 + air_vehicle_terminal.Damageable = true + air_vehicle_terminal.Repairable = true + air_vehicle_terminal.RepairIfDestroyed = true + air_vehicle_terminal.Subtract.Damage1 = 8 dropship_vehicle_terminal.Name = "dropship_vehicle_terminal" dropship_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.flight2Vehicles, VehicleTerminalDefinition.trunk) dropship_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + dropship_vehicle_terminal.MaxHealth = 500 + dropship_vehicle_terminal.Damageable = true + dropship_vehicle_terminal.Repairable = true + dropship_vehicle_terminal.RepairIfDestroyed = true + dropship_vehicle_terminal.Subtract.Damage1 = 8 vehicle_terminal_combined.Name = "vehicle_terminal_combined" vehicle_terminal_combined.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.groundVehicles, VehicleTerminalDefinition.trunk) vehicle_terminal_combined.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + vehicle_terminal_combined.MaxHealth = 500 + vehicle_terminal_combined.Damageable = true + vehicle_terminal_combined.Repairable = true + vehicle_terminal_combined.RepairIfDestroyed = true + vehicle_terminal_combined.Subtract.Damage1 = 8 vanu_air_vehicle_term.Name = "vanu_air_vehicle_term" vanu_air_vehicle_term.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(VehicleTerminalDefinition.flight1Vehicles, VehicleTerminalDefinition.trunk) vanu_air_vehicle_term.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + vanu_air_vehicle_term.MaxHealth = 500 + vanu_air_vehicle_term.Damageable = true + vanu_air_vehicle_term.Repairable = true + vanu_air_vehicle_term.RepairIfDestroyed = true + vanu_air_vehicle_term.Subtract.Damage1 = 8 vanu_vehicle_term.Name = "vanu_vehicle_term" vanu_vehicle_term.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(VehicleTerminalDefinition.groundVehicles, VehicleTerminalDefinition.trunk) vanu_vehicle_term.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + vanu_vehicle_term.MaxHealth = 500 + vanu_vehicle_term.Damageable = true + vanu_vehicle_term.Repairable = true + vanu_vehicle_term.RepairIfDestroyed = true + vanu_vehicle_term.Subtract.Damage1 = 8 bfr_terminal.Name = "bfr_terminal" bfr_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage(VehicleTerminalDefinition.bfrVehicles, VehicleTerminalDefinition.trunk) bfr_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + bfr_terminal.MaxHealth = 500 + bfr_terminal.Damageable = true + bfr_terminal.Repairable = true + bfr_terminal.RepairIfDestroyed = true + bfr_terminal.Subtract.Damage1 = 8 respawn_tube.Name = "respawn_tube" respawn_tube.Delay = 10 respawn_tube.SpecificPointFunc = SpawnPoint.Tube + respawn_tube.MaxHealth = 1000 + respawn_tube.Damageable = true + respawn_tube.DamageableByFriendlyFire = false + respawn_tube.Repairable = true + respawn_tube.RepairIfDestroyed = true + respawn_tube.Subtract.Damage1 = 8 respawn_tube_sanctuary.Name = "respawn_tube" respawn_tube_sanctuary.Delay = 10 respawn_tube_sanctuary.SpecificPointFunc = SpawnPoint.Default + respawn_tube_sanctuary.Damageable = false //true? + respawn_tube_sanctuary.DamageableByFriendlyFire = false + respawn_tube_sanctuary.Repairable = true respawn_tube_tower.Name = "respawn_tube_tower" respawn_tube_tower.Delay = 10 respawn_tube_tower.SpecificPointFunc = SpawnPoint.Tube + respawn_tube_tower.MaxHealth = 1000 + respawn_tube_tower.Damageable = true + respawn_tube_tower.DamageableByFriendlyFire = false + respawn_tube_tower.Repairable = true + respawn_tube_tower.RepairIfDestroyed = true + respawn_tube_tower.Subtract.Damage1 = 8 teleportpad_terminal.Name = "teleportpad_terminal" teleportpad_terminal.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.routerTerminal) + teleportpad_terminal.Damageable = false + teleportpad_terminal.Repairable = false medical_terminal.Name = "medical_terminal" medical_terminal.Interval = 500 @@ -6200,6 +6442,10 @@ object GlobalDefinitions { medical_terminal.ArmorAmount = 10 medical_terminal.UseRadius = 0.75f medical_terminal.TargetValidation += EffectTarget.Category.Player -> EffectTarget.Validation.Medical + medical_terminal.MaxHealth = 500 + medical_terminal.Damageable = true + medical_terminal.Repairable = true + medical_terminal.RepairIfDestroyed = true adv_med_terminal.Name = "adv_med_terminal" adv_med_terminal.Interval = 500 @@ -6207,18 +6453,26 @@ object GlobalDefinitions { adv_med_terminal.ArmorAmount = 15 adv_med_terminal.UseRadius = 0.75f adv_med_terminal.TargetValidation += EffectTarget.Category.Player -> EffectTarget.Validation.Medical + adv_med_terminal.MaxHealth = 750 + adv_med_terminal.Damageable = true + adv_med_terminal.Repairable = true + adv_med_terminal.RepairIfDestroyed = true crystals_health_a.Name = "crystals_health_a" crystals_health_a.Interval = 500 crystals_health_a.HealAmount = 4 crystals_health_a.UseRadius = 5 crystals_health_a.TargetValidation += EffectTarget.Category.Player -> EffectTarget.Validation.HealthCrystal + crystals_health_a.Damageable = false + crystals_health_a.Repairable = false crystals_health_b.Name = "crystals_health_b" crystals_health_b.Interval = 500 crystals_health_b.HealAmount = 4 crystals_health_b.UseRadius = 5 crystals_health_b.TargetValidation += EffectTarget.Category.Player -> EffectTarget.Validation.HealthCrystal + crystals_health_b.Damageable = false + crystals_health_b.Repairable = false portable_med_terminal.Name = "portable_med_terminal" portable_med_terminal.Interval = 500 @@ -6226,53 +6480,116 @@ object GlobalDefinitions { portable_med_terminal.ArmorAmount = 10 portable_med_terminal.UseRadius = 3 portable_med_terminal.TargetValidation += EffectTarget.Category.Player -> EffectTarget.Validation.Medical + portable_med_terminal.MaxHealth = 500 + portable_med_terminal.Damageable = false //TODO actually true + portable_med_terminal.Repairable = false pad_landing_frame.Name = "pad_landing_frame" pad_landing_frame.Interval = 1000 pad_landing_frame.HealAmount = 60 pad_landing_frame.UseRadius = 20 pad_landing_frame.TargetValidation += EffectTarget.Category.Aircraft -> EffectTarget.Validation.PadLanding + pad_landing_frame.Damageable = false + pad_landing_frame.Repairable = false pad_landing_tower_frame.Name = "pad_landing_tower_frame" pad_landing_tower_frame.Interval = 1000 pad_landing_tower_frame.HealAmount = 60 pad_landing_tower_frame.UseRadius = 20 pad_landing_tower_frame.TargetValidation += EffectTarget.Category.Aircraft -> EffectTarget.Validation.PadLanding + pad_landing_tower_frame.Damageable = false + pad_landing_tower_frame.Repairable = false repair_silo.Name = "repair_silo" repair_silo.Interval = 1000 repair_silo.HealAmount = 60 repair_silo.UseRadius = 20 repair_silo.TargetValidation += EffectTarget.Category.Vehicle -> EffectTarget.Validation.RepairSilo + repair_silo.Damageable = false + repair_silo.Repairable = false + + mb_pad_creation.Name = "mb_pad_creation" + mb_pad_creation.Damageable = false + mb_pad_creation.Repairable = false + + dropship_pad_doors.Name = "dropship_pad_doors" + dropship_pad_doors.Damageable = false + dropship_pad_doors.Repairable = false + + vanu_vehicle_creation_pad.Name = "vanu_vehicle_creation_pad" + vanu_vehicle_creation_pad.Damageable = false + vanu_vehicle_creation_pad.Repairable = false + + mb_locker.Name = "mb_locker" + mb_locker.Damageable = false + mb_locker.Repairable = false + + lock_external.Name = "lock_external" + lock_external.Damageable = false + lock_external.Repairable = false + + door.Name = "door" + door.Damageable = false + door.Repairable = false + + resource_silo.Name = "resource_silo" + resource_silo.Damageable = false + resource_silo.Repairable = false + + capture_terminal.Name = "capture_terminal" + capture_terminal.Damageable = false + capture_terminal.Repairable = false + + secondary_capture.Name = "secondary_capture" + secondary_capture.Damageable = false + secondary_capture.Repairable = false + + vanu_control_console.Name = "vanu_control_console" + vanu_control_console.Damageable = false + vanu_control_console.Repairable = false lodestar_repair_terminal.Name = "lodestar_repair_terminal" lodestar_repair_terminal.Interval = 1000 lodestar_repair_terminal.HealAmount = 60 lodestar_repair_terminal.UseRadius = 20 lodestar_repair_terminal.TargetValidation += EffectTarget.Category.Vehicle -> EffectTarget.Validation.RepairSilo + lodestar_repair_terminal.Damageable = false + lodestar_repair_terminal.Repairable = false multivehicle_rearm_terminal.Name = "multivehicle_rearm_terminal" multivehicle_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) multivehicle_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() multivehicle_rearm_terminal.SellEquipmentByDefault = true //TODO ? + multivehicle_rearm_terminal.Damageable = false + multivehicle_rearm_terminal.Repairable = false bfr_rearm_terminal.Name = "bfr_rearm_terminal" bfr_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(Map.empty[String, ()=>Equipment]) //TODO add stock to page bfr_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() bfr_rearm_terminal.SellEquipmentByDefault = true //TODO ? + bfr_rearm_terminal.Damageable = false + bfr_rearm_terminal.Repairable = false air_rearm_terminal.Name = "air_rearm_terminal" air_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) air_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() air_rearm_terminal.SellEquipmentByDefault = true //TODO ? + air_rearm_terminal.Damageable = false + air_rearm_terminal.Repairable = false ground_rearm_terminal.Name = "ground_rearm_terminal" ground_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) ground_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() ground_rearm_terminal.SellEquipmentByDefault = true //TODO ? + ground_rearm_terminal.Damageable = false + ground_rearm_terminal.Repairable = false manned_turret.Name = "manned_turret" manned_turret.MaxHealth = 3600 + manned_turret.Damageable = true + manned_turret.DamageDisablesAt = 0 + manned_turret.Repairable = true + manned_turret.RepairIfDestroyed = true manned_turret.Weapons += 1 -> new mutable.HashMap() manned_turret.Weapons(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan manned_turret.Weapons(1) += TurretUpgrade.AVCombo -> phalanx_avcombo @@ -6283,6 +6600,10 @@ object GlobalDefinitions { vanu_sentry_turret.Name = "vanu_sentry_turret" vanu_sentry_turret.MaxHealth = 1500 + vanu_sentry_turret.Damageable = true + vanu_sentry_turret.DamageDisablesAt = 0 + vanu_sentry_turret.Repairable = true + vanu_sentry_turret.RepairIfDestroyed = true vanu_sentry_turret.Weapons += 1 -> new mutable.HashMap() vanu_sentry_turret.Weapons(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon vanu_sentry_turret.MountPoints += 1 -> 0 @@ -6290,8 +6611,41 @@ object GlobalDefinitions { vanu_sentry_turret.FactionLocked = false vanu_sentry_turret.ReserveAmmunition = false + painbox.Name = "painbox" + painbox.Damageable = false + painbox.Repairable = false + + painbox_continuous.Name = "painbox_continuous" + painbox_continuous.Damageable = false + painbox_continuous.Repairable = false + + painbox_door_radius.Name = "painbox_door_radius" + painbox_door_radius.Damageable = false + painbox_door_radius.Repairable = false + + painbox_door_radius_continuous.Name = "painbox_door_radius_continuous" + painbox_door_radius_continuous.Damageable = false + painbox_door_radius_continuous.Repairable = false + + painbox_radius.Name = "painbox_radius" + painbox_radius.Damageable = false + painbox_radius.Repairable = false + + painbox_radius_continuous.Name = "painbox_radius_continuous" + painbox_radius_continuous.Damageable = false + painbox_radius_continuous.Repairable = false + gen_control.Name = "gen_control" + gen_control.Damageable = false + gen_control.Repairable = false generator.Name = "generator" + generator.MaxHealth = 4000 + generator.Damageable = true + generator.DamageableByFriendlyFire = false + generator.Repairable = true + generator.RepairDistance = 13.5f + generator.RepairIfDestroyed = true + generator.Subtract.Damage1 = 9 } } diff --git a/common/src/main/scala/net/psforever/objects/PlanetSideGameObject.scala b/common/src/main/scala/net/psforever/objects/PlanetSideGameObject.scala index 5b558b33e..b4943ef85 100644 --- a/common/src/main/scala/net/psforever/objects/PlanetSideGameObject.scala +++ b/common/src/main/scala/net/psforever/objects/PlanetSideGameObject.scala @@ -10,6 +10,7 @@ import net.psforever.types.Vector3 */ abstract class PlanetSideGameObject extends IdentifiableEntity with WorldEntity { private var entity : WorldEntity = new SimpleWorldEntity() + private var destroyed : Boolean = false def Entity : WorldEntity = entity @@ -35,6 +36,13 @@ abstract class PlanetSideGameObject extends IdentifiableEntity with WorldEntity Entity.Velocity = vec } + def Destroyed : Boolean = destroyed + + def Destroyed_=(state : Boolean) : Boolean = { + destroyed = state + Destroyed + } + def Definition : ObjectDefinition } diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index d00227f7a..d4527d619 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -23,9 +23,9 @@ class Player(private val core : Avatar) extends PlanetSideServerObject with Container with JammableUnit with ZoneAware { - private var alive : Boolean = false + Health = 0 //player health is artificially managed as a part of their lifecycle; start entity as dead + Destroyed = true //see isAlive private var backpack : Boolean = false - private var health : Int = 0 private var stamina : Int = 0 private var armor : Int = 0 @@ -34,7 +34,6 @@ class Player(private val core : Avatar) extends PlanetSideServerObject private var capacitorLastUsedMillis : Long = 0 private var capacitorLastChargedMillis : Long = 0 - private var maxHealth : Int = 100 //TODO affected by empire benefits, territory benefits, and bops private var maxStamina : Int = 100 //does anything affect this? private var exosuit : ExoSuitDefinition = GlobalDefinitions.Standard @@ -84,14 +83,14 @@ class Player(private val core : Avatar) extends PlanetSideServerObject def LFS : Boolean = core.LFS - def isAlive : Boolean = alive + def isAlive : Boolean = !Destroyed def isBackpack : Boolean = backpack def Spawn : Boolean = { if(!isAlive && !isBackpack) { - alive = true - Health = MaxHealth + Destroyed = false + Health = Definition.DefaultHealth Stamina = MaxStamina Armor = MaxArmor Capacitor = 0 @@ -101,14 +100,15 @@ class Player(private val core : Avatar) extends PlanetSideServerObject } def Die : Boolean = { - alive = false + Destroyed = true Health = 0 Stamina = 0 false } def Revive : Boolean = { - alive = true + Destroyed = false + Health = Definition.DefaultHealth true } @@ -122,20 +122,6 @@ class Player(private val core : Avatar) extends PlanetSideServerObject } } - def Health : Int = health - - def Health_=(assignHealth : Int) : Int = { - health = math.min(math.max(0, assignHealth), MaxHealth) - Health - } - - def MaxHealth : Int = maxHealth - - def MaxHealth_=(max : Int) : Int = { - maxHealth = math.min(math.max(0, max), 65535) - MaxHealth - } - def Stamina : Int = stamina def Stamina_=(assignEnergy : Int) : Int = { @@ -662,6 +648,21 @@ object Player { } } + def GetHackLevel(player : Player): Int = { + if(player.Certifications.contains(CertificationType.ExpertHacking) || player.Certifications.contains(CertificationType.ElectronicsExpert)) { + 3 + } + else if(player.Certifications.contains(CertificationType.AdvancedHacking)) { + 2 + } + else if (player.Certifications.contains(CertificationType.Hacking)) { + 1 + } + else { + 0 + } + } + def toString(obj : Player) : String = { val guid = if(obj.HasGUID) { s" ${obj.Continent}-${obj.GUID.guid}" } else { "" } s"${obj.core}$guid ${obj.Health}/${obj.MaxHealth} ${obj.Armor}/${obj.MaxArmor}" diff --git a/common/src/main/scala/net/psforever/objects/Players.scala b/common/src/main/scala/net/psforever/objects/Players.scala new file mode 100644 index 000000000..486265c73 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/Players.scala @@ -0,0 +1,49 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects + +import net.psforever.packet.game.{InventoryStateMessage, RepairMessage} +import services.Service +import services.avatar.{AvatarAction, AvatarServiceMessage} + +object Players { + private val log = org.log4s.getLogger("Players") + + /** + * Evaluate the progress of the user applying a tool to modify some other server object. + * This action is using the medical applicator to revive a fallen (dead but not released) ally. + * @param target the player being affected by the revive action + * @param user the player performing the revive action + * @param item the tool being used to revive the target player + * @param progress the current progress value + * @return `true`, if the next cycle of progress should occur; + * `false`, otherwise + */ + def RevivingTickAction(target : Player, user : Player, item : Tool)(progress : Float) : Boolean = { + if(!target.isAlive && !target.isBackpack && + user.isAlive && !user.isMoving && + user.Slot(user.DrawnSlot).Equipment.contains(item) && item.Magazine > 0) { + val magazine = item.Discharge + val events = target.Zone.AvatarEvents + val uname = user.Name + events ! AvatarServiceMessage(uname, AvatarAction.SendResponse(Service.defaultPlayerGUID, InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine))) + events ! AvatarServiceMessage(uname, AvatarAction.SendResponse(Service.defaultPlayerGUID, RepairMessage(target.GUID, progress.toInt))) + true + } + else { + false + } + } + + /** + * na + * @see `AvatarAction.Revive` + * @see `AvatarResponse.Revive` + * @param target the player being revived + * @param medic the name of the player doing the reviving + */ + def FinishRevivingPlayer(target : Player, medic : String)() : Unit = { + val name = target.Name + log.info(s"$medic had revived $name") + target.Zone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.Revive(target.GUID)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/SensorDeployable.scala b/common/src/main/scala/net/psforever/objects/SensorDeployable.scala index 22b35ab22..cb5106c31 100644 --- a/common/src/main/scala/net/psforever/objects/SensorDeployable.scala +++ b/common/src/main/scala/net/psforever/objects/SensorDeployable.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2019 PSForever package net.psforever.objects import akka.actor.{Actor, ActorContext, Props} @@ -8,12 +8,12 @@ import net.psforever.objects.definition.converter.SmallDeployableConverter import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition} import net.psforever.objects.equipment.{JammableBehavior, JammableUnit} import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity} import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.vital.{StandardResolutions, Vitality} -import net.psforever.objects.zones.Zone -import net.psforever.types.PlanetSideGUID +import net.psforever.objects.serverobject.repair.RepairableEntity +import net.psforever.objects.vital.StandardResolutions +import net.psforever.types.{PlanetSideGUID, Vector3} import services.Service -import services.avatar.{AvatarAction, AvatarServiceMessage} import services.local.{LocalAction, LocalServiceMessage} import services.vehicle.{VehicleAction, VehicleServiceMessage} @@ -45,19 +45,25 @@ object SensorDeployableDefinition { } class SensorDeployableControl(sensor : SensorDeployable) extends Actor - with JammableBehavior { - + with JammableBehavior + with DamageableEntity + with RepairableEntity { def JammableObject = sensor + def DamageableObject = sensor + def RepairableObject = sensor - def receive : Receive = jammableBehavior.orElse { - case Vitality.Damage(damage_func) => - val originalHealth = sensor.Health - if(originalHealth > 0) { - val cause = damage_func(sensor) - SensorDeployableControl.HandleDamageResolution(sensor, cause, originalHealth - sensor.Health) - } + def receive : Receive = jammableBehavior + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) + .orElse { + case _ => ; + } - case _ => ; + override protected def DamageLog(msg : String) : Unit = { } + + override protected def DestructionAwareness(target : Damageable.Target, cause : ResolvedProjectile) : Unit = { + super.DestructionAwareness(target, cause) + SensorDeployableControl.DestructionAwareness(sensor, PlanetSideGUID(0)) } override def StartJammeredSound(target : Any, dur : Int) : Unit = target match { @@ -69,7 +75,8 @@ class SensorDeployableControl(sensor : SensorDeployable) extends Actor override def StartJammeredStatus(target : Any, dur : Int) : Unit = target match { case obj : PlanetSideServerObject with JammableUnit if !obj.Jammed => - sensor.Zone.LocalEvents ! LocalServiceMessage(sensor.Zone.Id, LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, false, 1000)) + val zone = obj.Zone + zone.LocalEvents ! LocalServiceMessage(zone.Id, LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, false, 1000)) super.StartJammeredStatus(obj, dur) case _ => ; } @@ -77,7 +84,8 @@ class SensorDeployableControl(sensor : SensorDeployable) extends Actor override def CancelJammeredSound(target : Any) : Unit = { target match { case obj : PlanetSideServerObject if jammedSound => - obj.Zone.VehicleEvents ! VehicleServiceMessage(obj.Zone.Id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, obj.GUID, 54, 0)) + val zone = obj.Zone + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, obj.GUID, 54, 0)) case _ => ; } super.CancelJammeredSound(target) @@ -94,40 +102,32 @@ class SensorDeployableControl(sensor : SensorDeployable) extends Actor } object SensorDeployableControl { - def HandleDamageResolution(target : SensorDeployable, cause : ResolvedProjectile, damage : Int) : Unit = { - val zone = target.Zone - val targetGUID = target.GUID - val playerGUID = zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match { - case Some(player) => player.GUID - case _ => PlanetSideGUID(0) - } - if(target.Health > 0) { - //activity on map - if(damage > 0) { - zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) - } - if(cause.projectile.profile.JammerProjectile) { - target.Actor ! JammableUnit.Jammered(cause) - } - } - else { - HandleDestructionAwareness(target, playerGUID, cause) - } - zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttribute(targetGUID, 0, target.Health)) - } - /** * na * @param target na * @param attribution na - * @param lastShot na */ - def HandleDestructionAwareness(target : SensorDeployable, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = { - target.Actor ! JammableUnit.ClearJammeredSound() - target.Actor ! JammableUnit.ClearJammeredStatus() + def DestructionAwareness(target : Damageable.Target with Deployable, attribution : PlanetSideGUID) : Unit = { + Deployables.AnnounceDestroyDeployable(target, Some(1 seconds)) val zone = target.Zone - Deployables.AnnounceDestroyDeployable(target, None) - zone.LocalEvents ! LocalServiceMessage(zone.Id, LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", target.GUID, false, 1000)) - zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.Destroy(target.GUID, attribution, attribution, target.Position)) + zone.LocalEvents ! LocalServiceMessage(zone.Id, + LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", target.GUID, false, 1000) + ) + //position the explosion effect near the bulky area of the sensor stalk + val ang = target.Orientation + val explosionPos = { + val pos = target.Position + val yRadians = ang.y.toRadians + val d = Vector3.Rz(Vector3(0, 0.875f, 0), ang.z) * math.sin(yRadians).toFloat + Vector3( + pos.x + d.x, + pos.y + d.y, + pos.z + math.cos(yRadians).toFloat * 0.875f + ) + } + zone.LocalEvents ! LocalServiceMessage(zone.Id, + LocalAction.TriggerEffectLocation(Service.defaultPlayerGUID, "motion_sensor_destroyed", explosionPos, ang) + ) + //TODO replaced by an alternate model (charred stub)? } } diff --git a/common/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala b/common/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala index ff32b4193..8e4373310 100644 --- a/common/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala +++ b/common/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala @@ -7,12 +7,13 @@ import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployableCatego import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition} import net.psforever.objects.definition.converter.ShieldGeneratorConverter import net.psforever.objects.equipment.{JammableBehavior, JammableUnit} +import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.vital.Vitality -import net.psforever.objects.zones.Zone +import net.psforever.objects.serverobject.repair.RepairableEntity +import net.psforever.objects.vital.resolution.ResolutionCalculations import net.psforever.types.PlanetSideGUID -import services.avatar.{AvatarAction, AvatarServiceMessage} import services.Service import services.vehicle.{VehicleAction, VehicleServiceMessage} @@ -34,32 +35,75 @@ class ShieldGeneratorDefinition extends ComplexDeployableDefinition(240) { } class ShieldGeneratorControl(gen : ShieldGeneratorDeployable) extends Actor - with JammableBehavior { - + with JammableBehavior + with DamageableEntity + with RepairableEntity { def JammableObject = gen + def DamageableObject = gen + def RepairableObject = gen + private var handleDamageToShields : Boolean = false def receive : Receive = jammableBehavior + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) .orElse { - case Vitality.Damage(damage_func) => //note: damage status is reported as vehicle events, not local events - if(gen.Health > 0) { - val originalHealth = gen.Health - val cause = damage_func(gen) - val health = gen.Health - val damageToHealth = originalHealth - health - ShieldGeneratorControl.HandleDamageResolution(gen, cause, damageToHealth) - if(damageToHealth > 0) { - val name = gen.Actor.toString - val slashPoint = name.lastIndexOf("/") - org.log4s.getLogger("DamageResolution").info(s"${name.substring(slashPoint + 1, name.length - 1)}: BEFORE=$originalHealth, AFTER=$health, CHANGE=$damageToHealth") - } - } - case _ => ; } + + /** + * The shield generator has two upgrade paths - blocking projectiles, and providing ammunition like a terminal. + * Both upgrade paths are possible using the nano dispenser with an armor canister, + * and can only be started when the generator is undamaged. + * @see `PlanetSideGameObject.CanRepair` + * @see `RepairableEntity.CanPerformRepairs` + * @param player the user of the nano dispenser tool + * @param item the nano dispenser tool + */ + override def CanBeRepairedByNanoDispenser(player : Player, item : Tool) : Unit = { + if(gen.CanRepair) { + super.CanBeRepairedByNanoDispenser(player, item) + } + else if(!gen.Destroyed) { + //TODO reinforced shield upgrade not implemented yet + //TODO ammunition supply upgrade not implemented yet + } + } + + override protected def PerformDamage(target : Damageable.Target, applyDamageTo : ResolutionCalculations.Output) : Unit = { + val originalHealth = gen.Health + val originalShields = gen.Shields + val cause = applyDamageTo(target) + val health = gen.Health + val shields = gen.Shields + val damageToHealth = originalHealth - health + val damageToShields = originalShields - shields + val damage = damageToHealth + damageToShields + if(WillAffectTarget(target, damage, cause)) { + target.History(cause) + DamageLog(target,s"BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields") + handleDamageToShields = damageToShields > 0 + HandleDamage(target, cause, damageToHealth) + } + else { + gen.Health = originalHealth + gen.Shields = originalShields + } + } + + override protected def DamageAwareness(target : Damageable.Target, cause : ResolvedProjectile, amount : Int) : Unit = { + super.DamageAwareness(target, cause, amount) + ShieldGeneratorControl.DamageAwareness(gen, cause, handleDamageToShields) + handleDamageToShields = false + } + + override protected def DestructionAwareness(target : Target, cause : ResolvedProjectile) : Unit = { + super.DestructionAwareness(target, cause) + ShieldGeneratorControl.DestructionAwareness(gen, PlanetSideGUID(0)) + } + /* while the shield generator is technically a supported jammable target, how that works is currently unknown - electing to use a "status only, no sound" approach by overriding one with an empty function is not entirely arbitrary - the superclass of "status" calls also sets the jammed object property + check the object definition for proper feature activation */ override def StartJammeredSound(target : Any, dur : Int) : Unit = { } @@ -86,40 +130,23 @@ object ShieldGeneratorControl { /** * na * @param target na + * @param cause na + * @param damageToShields na */ - def HandleDamageResolution(target : ShieldGeneratorDeployable, cause : ResolvedProjectile, damage : Int) : Unit = { - val zone = target.Zone - val targetGUID = target.GUID - val playerGUID = zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match { - case Some(player) => player.GUID - case _ => PlanetSideGUID(0) + def DamageAwareness(target : ShieldGeneratorDeployable, cause : ResolvedProjectile, damageToShields : Boolean) : Unit = { + //shields + if(damageToShields) { + val zone = target.Zone + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 68, target.Shields)) } - if(target.Health > 0) { - //activity on map - if(damage > 0) { - zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) - } - if(cause.projectile.profile.JammerProjectile) { - target.Actor ! JammableUnit.Jammered(cause) - } - } - else { - HandleDestructionAwareness(target, playerGUID, cause) - } - zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttribute(targetGUID, 0, target.Health)) } /** * na * @param target na * @param attribution na - * @param lastShot na */ - def HandleDestructionAwareness(target : ShieldGeneratorDeployable, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = { - target.Actor ! JammableUnit.ClearJammeredSound() - target.Actor ! JammableUnit.ClearJammeredStatus() - val zone = target.Zone + def DestructionAwareness(target : Damageable.Target with Deployable, attribution : PlanetSideGUID) : Unit = { Deployables.AnnounceDestroyDeployable(target, None) - zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.Destroy(target.GUID, attribution, attribution, target.Position)) } } diff --git a/common/src/main/scala/net/psforever/objects/SpawnPoint.scala b/common/src/main/scala/net/psforever/objects/SpawnPoint.scala index f50cd135d..dc86b3738 100644 --- a/common/src/main/scala/net/psforever/objects/SpawnPoint.scala +++ b/common/src/main/scala/net/psforever/objects/SpawnPoint.scala @@ -45,6 +45,8 @@ trait SpawnPoint { */ def Definition : ObjectDefinition with SpawnPointDefinition + def Offline : Boolean = psso.Destroyed + /** * Determine a specific position and orientation in which to spawn the target. * @return a `Tuple` of `Vector3` objects; diff --git a/common/src/main/scala/net/psforever/objects/TrapDeployable.scala b/common/src/main/scala/net/psforever/objects/TrapDeployable.scala index e330e9998..6998f15c4 100644 --- a/common/src/main/scala/net/psforever/objects/TrapDeployable.scala +++ b/common/src/main/scala/net/psforever/objects/TrapDeployable.scala @@ -1,7 +1,51 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2020 PSForever package net.psforever.objects -import net.psforever.objects.ce.SimpleDeployable -import net.psforever.objects.definition.SimpleDeployableDefinition +import akka.actor.{Actor, ActorContext, Props} +import net.psforever.objects.ballistics.ResolvedProjectile +import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployedItem} +import net.psforever.objects.definition.converter.TRAPConverter +import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity} +import net.psforever.objects.serverobject.repair.RepairableEntity +import net.psforever.objects.vital.StandardResolutions -class TrapDeployable(cdef : SimpleDeployableDefinition) extends SimpleDeployable(cdef) +class TrapDeployable(cdef : TrapDeployableDefinition) extends ComplexDeployable(cdef) + +class TrapDeployableDefinition(objectId : Int) extends ComplexDeployableDefinition(objectId) { + Model = StandardResolutions.SimpleDeployables + Packet = new TRAPConverter + + override def Initialize(obj : PlanetSideServerObject with Deployable, context : ActorContext) = { + obj.Actor = context.actorOf(Props(classOf[TrapDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj)) + } + + override def Uninitialize(obj : PlanetSideServerObject with Deployable, context : ActorContext) = { + SimpleDeployableDefinition.SimpleUninitialize(obj, context) + } +} + +object TrapDeployableDefinition { + def apply(dtype : DeployedItem.Value) : TrapDeployableDefinition = { + new TrapDeployableDefinition(dtype.id) + } +} + +class TrapDeployableControl(trap : TrapDeployable) extends Actor + with DamageableEntity + with RepairableEntity { + def DamageableObject = trap + def RepairableObject = trap + + def receive : Receive = takesDamage + .orElse(canBeRepairedByNanoDispenser) + .orElse { + case _ => + } + + override protected def DestructionAwareness(target : Damageable.Target, cause : ResolvedProjectile) : Unit = { + super.DestructionAwareness(target, cause) + Deployables.AnnounceDestroyDeployable(trap, None) + } +} diff --git a/common/src/main/scala/net/psforever/objects/TurretDeployable.scala b/common/src/main/scala/net/psforever/objects/TurretDeployable.scala index 273acf40e..6f3ff3bc2 100644 --- a/common/src/main/scala/net/psforever/objects/TurretDeployable.scala +++ b/common/src/main/scala/net/psforever/objects/TurretDeployable.scala @@ -8,30 +8,23 @@ import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDepl import net.psforever.objects.definition.converter.SmallTurretConverter import net.psforever.objects.equipment.{JammableMountedWeapons, JammableUnit} import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior +import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.serverobject.damage.DamageableWeaponTurret import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.mount.MountableBehavior +import net.psforever.objects.serverobject.repair.RepairableWeaponTurret import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret} -import net.psforever.objects.vital.{StandardResolutions, StandardVehicleDamage, StandardVehicleResistance, Vitality} -import net.psforever.objects.zones.Zone -import net.psforever.types.PlanetSideGUID -import services.Service -import services.avatar.{AvatarAction, AvatarServiceMessage} -import services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.objects.vital.{StandardResolutions, StandardVehicleDamage, StandardVehicleResistance} class TurretDeployable(tdef : TurretDeployableDefinition) extends ComplexDeployable(tdef) with WeaponTurret with JammableUnit with Hackable { - WeaponTurret.LoadDefinition(this) //calls the equivalent of Health = Definition.MaxHealth + WeaponTurret.LoadDefinition(this) def MountPoints : Map[Int, Int] = Definition.MountPoints.toMap - //override to clarify inheritance conflict - override def Health : Int = super[ComplexDeployable].Health - //override to clarify inheritance conflict - override def Health_=(toHealth : Int) : Int = super[ComplexDeployable].Health_=(toHealth) - override def Definition = tdef } @@ -39,8 +32,8 @@ class TurretDeployableDefinition(private val objectId : Int) extends ComplexDepl with TurretDefinition { Name = "turret_deployable" Packet = new SmallTurretConverter - Damage = StandardVehicleDamage - Resistance = StandardVehicleResistance + DamageUsing = StandardVehicleDamage + ResistUsing = StandardVehicleResistance Model = StandardResolutions.FacilityTurrets //override to clarify inheritance conflict @@ -68,113 +61,28 @@ object TurretDeployableDefinition { class TurretControl(turret : TurretDeployable) extends Actor with FactionAffinityBehavior.Check with JammableMountedWeapons //note: jammable status is reported as vehicle events, not local events - with MountableBehavior.Mount - with MountableBehavior.Dismount { - def MountableObject = turret //do not add type! - + with MountableBehavior.TurretMount + with MountableBehavior.Dismount + with DamageableWeaponTurret + with RepairableWeaponTurret { + def MountableObject = turret def JammableObject = turret - - def FactionObject : FactionAffinity = turret + def FactionObject = turret + def DamageableObject = turret + def RepairableObject = turret def receive : Receive = checkBehavior .orElse(jammableBehavior) + .orElse(mountBehavior) .orElse(dismountBehavior) - .orElse(turretMountBehavior) + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) .orElse { - case Vitality.Damage(damage_func) => //note: damage status is reported as vehicle events, not local events - if(turret.Health > 0) { - val originalHealth = turret.Health - val cause = damage_func(turret) - val health = turret.Health - val damageToHealth = originalHealth - health - TurretControl.HandleDamageResolution(turret, cause, damageToHealth) - if(damageToHealth > 0) { - val name = turret.Actor.toString - val slashPoint = name.lastIndexOf("/") - org.log4s.getLogger("DamageResolution").info(s"${name.substring(slashPoint + 1, name.length - 1)}: BEFORE=$originalHealth, AFTER=$health, CHANGE=$damageToHealth") - } - } - case _ => ; } -} -object TurretControl { - /** - * na - * @param target na - */ - def HandleDamageResolution(target : TurretDeployable, cause : ResolvedProjectile, damage : Int) : Unit = { - val zone = target.Zone - val targetGUID = target.GUID - val playerGUID = zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match { - case Some(player) => player.GUID - case _ => PlanetSideGUID(0) - } - if(target.Health > 0) { - //activity on map - if(damage > 0) { - zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) - //alert occupants to damage source - HandleDamageAwareness(target, playerGUID, cause) - } - if(cause.projectile.profile.JammerProjectile) { - target.Actor ! JammableUnit.Jammered(cause) - } - } - else { - //alert to turret death (hence, occupants' deaths) - HandleDestructionAwareness(target, playerGUID, cause) - } - zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, target.Health)) - } - - /** - * na - * @param target na - * @param attribution na - * @param lastShot na - */ - def HandleDamageAwareness(target : TurretDeployable, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = { - val zone = target.Zone - //alert occupants to damage source - target.Seats.values.filter(seat => { - seat.isOccupied && seat.Occupant.get.isAlive - }).foreach(seat => { - val tplayer = seat.Occupant.get - zone.AvatarEvents ! AvatarServiceMessage(tplayer.Name, AvatarAction.HitHint(attribution, tplayer.GUID)) - }) - } - - /** - * na - * @param target na - * @param attribution na - * @param lastShot na - */ - def HandleDestructionAwareness(target : TurretDeployable, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = { - target.Actor ! JammableUnit.ClearJammeredSound() - target.Actor ! JammableUnit.ClearJammeredStatus() - val zone = target.Zone - val continentId = zone.Id - //alert to vehicle death (hence, occupants' deaths) - target.Seats.values.filter(seat => { - seat.isOccupied && seat.Occupant.get.isAlive - }).foreach(seat => { - val tplayer = seat.Occupant.get - tplayer.History(lastShot) - tplayer.Actor ! Player.Die() - }) - //vehicle wreckage has no weapons - target.Weapons.values - .filter { - _.Equipment.nonEmpty - } - .foreach(slot => { - val wep = slot.Equipment.get - zone.AvatarEvents ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, wep.GUID)) - }) - Deployables.AnnounceDestroyDeployable(target, None) - zone.AvatarEvents ! AvatarServiceMessage(continentId, AvatarAction.Destroy(target.GUID, attribution, attribution, target.Position)) + override protected def DestructionAwareness(target : Target, cause : ResolvedProjectile) : Unit = { + super.DestructionAwareness(target, cause) + Deployables.AnnounceDestroyDeployable(turret, None) } } diff --git a/common/src/main/scala/net/psforever/objects/Vehicle.scala b/common/src/main/scala/net/psforever/objects/Vehicle.scala index bd3a741ea..6ad779d7b 100644 --- a/common/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/common/src/main/scala/net/psforever/objects/Vehicle.scala @@ -9,6 +9,7 @@ import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.deploy.Deployment +import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.structures.AmenityOwner import net.psforever.objects.vehicles._ import net.psforever.objects.vital.{DamageResistanceModel, StandardResistanceProfile, Vitality} @@ -65,6 +66,7 @@ import scala.annotation.tailrec * used in the initialization process (`loadVehicleDefinition`) */ class Vehicle(private val vehicleDef : VehicleDefinition) extends AmenityOwner + with Hackable with FactionAffinity with Mountable with MountedWeapons @@ -74,10 +76,8 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends AmenityOwner with StandardResistanceProfile with JammableUnit with Container { - private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.TR - private var health : Int = 1 + private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var shields : Int = 0 - private var isDead : Boolean = false private var decal : Int = 0 private var trunkAccess : Option[PlanetSideGUID] = None private var jammered : Boolean = false @@ -144,28 +144,12 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends AmenityOwner MountedIn } - def IsDead : Boolean = { - isDead - } - - def Health : Int = { - health - } - - def Health_=(assignHealth : Int) : Int = { - if(!isDead) { - health = math.min(math.max(0, assignHealth), MaxHealth) + override def Health_=(assignHealth : Int) : Int = { + //TODO should vehicle class enforce this? + if(!Destroyed) { + super.Health_=(assignHealth) } - - if(health == 0) { - isDead = true - } - - health - } - - def MaxHealth : Int = { - Definition.MaxHealth + Health } def Shields : Int = { @@ -644,7 +628,7 @@ object Vehicle { def LoadDefinition(vehicle : Vehicle) : Vehicle = { val vdef : VehicleDefinition = vehicle.Definition //general stuff - vehicle.Health = vdef.MaxHealth + vehicle.Health = vdef.DefaultHealth //create weapons vehicle.weapons = vdef.Weapons.map({case (num, definition) => val slot = EquipmentSlot(EquipmentSize.VehicleWeapon) diff --git a/common/src/main/scala/net/psforever/objects/Vehicles.scala b/common/src/main/scala/net/psforever/objects/Vehicles.scala index 01635b13a..e4a699aa6 100644 --- a/common/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/common/src/main/scala/net/psforever/objects/Vehicles.scala @@ -1,20 +1,26 @@ // Copyright (c) 2020 PSForever package net.psforever.objects -import net.psforever.objects.vehicles.VehicleLockState +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.vehicles.{CargoBehavior, VehicleLockState} import net.psforever.objects.zones.Zone -import net.psforever.types.PlanetSideGUID +import net.psforever.packet.game.TriggeredSound +import net.psforever.types.{DriveState, PlanetSideGUID} +import services.RemoverActor import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.local.{LocalAction, LocalServiceMessage} import services.vehicle.{VehicleAction, VehicleServiceMessage} object Vehicles { + private val log = org.log4s.getLogger("Vehicles") + /** * na * @param vehicle na - * @param tplayer na + * @param player na * @return na */ - def Own(vehicle : Vehicle, tplayer : Player) : Option[Vehicle] = Own(vehicle, Some(tplayer)) + def Own(vehicle : Vehicle, player : Player) : Option[Vehicle] = Own(vehicle, Some(player)) /** * na @@ -151,4 +157,106 @@ object Vehicles { false } } + + /** + * The orientation of a cargo vehicle as it is being loaded into and contained by a carrier vehicle. + * The type of carrier is not an important consideration in determining the orientation, oddly enough. + * @param vehicle the cargo vehicle + * @return the orientation as an `Integer` value; + * `0` for almost all cases + */ + def CargoOrientation(vehicle : Vehicle) : Int = { + if(vehicle.Definition == GlobalDefinitions.router) { + 1 + } + else { + 0 + } + } + + /** + * The process of hacking/jacking a vehicle is complete. + * Change the faction of the vehicle to the hacker's faction and remove all occupants. + * @param target The `Vehicle` object that has been hacked/jacked + * @param hacker the one whoi performed the hack and will inherit ownership of the target vehicle + * @param unk na; used by `HackMessage` as `unk5` + */ + def FinishHackingVehicle(target : Vehicle, hacker : Player, unk : Long)(): Unit = { + log.info(s"Vehicle guid: ${target.GUID} has been jacked") + import scala.concurrent.duration._ + val zone = target.Zone + // Forcefully dismount any cargo + target.CargoHolds.values.foreach(cargoHold => { + cargoHold.Occupant match { + case Some(cargo : Vehicle) => { + cargo.Seats(0).Occupant match { + case Some(cargoDriver: Player) => + CargoBehavior.HandleVehicleCargoDismount(target.Zone, cargoDriver.GUID, cargo.GUID, bailed = target.Flying, requestedByPassenger = false, kicked = true ) + case None => + log.error("FinishHackingVehicle: vehicle in cargo hold missing driver") + CargoBehavior.HandleVehicleCargoDismount(hacker.GUID, cargo.GUID, cargo, target.GUID, target, false, false, true) + } + } + case None => ; + } + }) + // Forcefully dismount all seated occupants from the vehicle + target.Seats.values.foreach(seat => { + seat.Occupant match { + case Some(tplayer) => + seat.Occupant = None + tplayer.VehicleSeated = None + if(tplayer.HasGUID) { + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, target.GUID)) + } + case None => ; + } + }) + // If the vehicle can fly and is flying deconstruct it, and well played to whomever managed to hack a plane in mid air. I'm impressed. + if(target.Definition.CanFly && target.Flying) { + // todo: Should this force the vehicle to land in the same way as when a pilot bails with passengers on board? + zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), zone)) + zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, zone, Some(0 seconds))) + } else { // Otherwise handle ownership transfer as normal + // Remove ownership of our current vehicle, if we have one + hacker.VehicleOwned match { + case Some(guid : PlanetSideGUID) => + zone.GUID(guid) match { + case Some(vehicle: Vehicle) => + Vehicles.Disown(hacker, vehicle) + case _ => ; + } + case _ => ; + } + target.Owner match { + case Some(previousOwnerGuid: PlanetSideGUID) => + // Remove ownership of the vehicle from the previous player + zone.GUID(previousOwnerGuid) match { + case Some(tplayer: Player) => + Vehicles.Disown(tplayer, target) + case _ => ; // Vehicle already has no owner + } + case _ => ; + } + // Now take ownership of the jacked vehicle + target.Actor ! CommonMessages.Hack(hacker, target) + target.Faction = hacker.Faction + Vehicles.Own(target, hacker) + //todo: Send HackMessage -> HackCleared to vehicle? can be found in packet captures. Not sure if necessary. + // And broadcast the faction change to other clients + zone.AvatarEvents ! AvatarServiceMessage(hacker.Name, AvatarAction.SetEmpire(hacker.GUID, target.GUID, hacker.Faction)) + zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.SetEmpire(hacker.GUID, target.GUID, hacker.Faction)) + } + zone.LocalEvents ! LocalServiceMessage(zone.Id, LocalAction.TriggerSound(hacker.GUID, TriggeredSound.HackVehicle, target.Position, 30, 0.49803925f)) + // Clean up after specific vehicles, e.g. remove router telepads + // If AMS is deployed, swap it to the new faction + target.Definition match { + case GlobalDefinitions.router => + log.info("FinishHackingVehicle: cleaning up after a router ...") + Deployables.RemoveTelepad(target) + case GlobalDefinitions.ams if target.DeploymentState == DriveState.Deployed => + zone.VehicleEvents ! VehicleServiceMessage.AMSDeploymentChange(zone) + case _ => ; + } + } } 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 cffa60dfd..61b752629 100644 --- a/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -2,13 +2,17 @@ package net.psforever.objects.avatar import akka.actor.Actor -import net.psforever.objects.Player -import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile, SourceEntry} -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} +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.damage.Damageable +import net.psforever.objects.serverobject.mount.Mountable +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, PlanetSideGUID} +import net.psforever.types.{ExoSuitType, Vector3} import services.Service import services.avatar.{AvatarAction, AvatarServiceMessage} @@ -19,34 +23,108 @@ import scala.concurrent.duration._ * stub for future development */ class PlayerControl(player : Player) extends Actor - with JammableBehavior { + with JammableBehavior + with Damageable { def JammableObject = player + def DamageableObject = player - private [this] val log = org.log4s.getLogger(player.Name) - private [this] val damageLog = org.log4s.getLogger("DamageResolution") + private [this] val damageLog = org.log4s.getLogger(Damageable.LogChannel) - def receive : Receive = jammableBehavior.orElse { - case Player.Die() => - PlayerControl.HandleDestructionAwareness(player, player.GUID, None) + def receive : Receive = jammableBehavior + .orElse(takesDamage) + .orElse { + case Player.Die() => + if(player.isAlive) { + PlayerControl.DestructionAwareness(player, None) + } - case Vitality.Damage(resolution_function) => + case CommonMessages.Use(user, Some(item : Tool)) if item.Definition == GlobalDefinitions.medicalapplicator && player.isAlive => + //heal + val originalHealth = player.Health + val definition = player.Definition + if(player.MaxHealth > 0 && originalHealth < player.MaxHealth && + user.Faction == player.Faction && + item.Magazine > 0 && + Vector3.Distance(user.Position, player.Position) < definition.RepairDistance) { + val zone = player.Zone + val events = zone.AvatarEvents + val uname = user.Name + val guid = player.GUID + if(!(player.isMoving || user.isMoving)) { //only allow stationary heals + val newHealth = player.Health = originalHealth + 10 + val magazine = item.Discharge + events ! AvatarServiceMessage(uname, AvatarAction.SendResponse(Service.defaultPlayerGUID, InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong))) + events ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttributeToAll(guid, 0, newHealth)) + player.History(HealFromEquipment(PlayerSource(player), PlayerSource(user), newHealth - originalHealth, GlobalDefinitions.medicalapplicator)) + } + if(player != user) { + //"Someone is trying to heal you" + events ! AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(guid, 55, 1)) + //progress bar remains visible for all heal attempts + events ! AvatarServiceMessage(uname, AvatarAction.SendResponse(Service.defaultPlayerGUID, RepairMessage(guid, player.Health * 100 / definition.MaxHealth))) + } + } + + case CommonMessages.Use(user, Some(item : Tool)) if item.Definition == GlobalDefinitions.medicalapplicator && !player.isAlive => + //revive + if(user != player && user.isAlive && !user.isMoving && + !player.isBackpack && + item.Magazine >= 25) { + sender ! CommonMessages.Use(player, Some((item, user))) + } + + case CommonMessages.Use(user, Some(item : Tool)) if item.Definition == GlobalDefinitions.bank => + val originalArmor = player.Armor + val definition = player.Definition + if(player.MaxArmor > 0 && originalArmor < player.MaxArmor && + user.Faction == player.Faction && + item.AmmoType == Ammo.armor_canister && item.Magazine > 0 && + Vector3.Distance(user.Position, player.Position) < definition.RepairDistance) { + val zone = player.Zone + val events = zone.AvatarEvents + val uname = user.Name + val guid = player.GUID + if(!(player.isMoving || user.isMoving)) { //only allow stationary repairs + val newArmor = player.Armor = originalArmor + Repairable.Quality + RepairValue(item) + definition.RepairMod + val magazine = item.Discharge + events ! AvatarServiceMessage(uname, AvatarAction.SendResponse(Service.defaultPlayerGUID, InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong))) + events ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttributeToAll(guid, 4, player.Armor)) + player.History(RepairFromEquipment(PlayerSource(player), PlayerSource(user), newArmor - originalArmor, GlobalDefinitions.bank)) + } + if(player != user) { + if(player.isAlive) { + //"Someone is trying to repair you" gets strobed twice for visibility + val msg = AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(guid, 56, 1)) + events ! msg + import scala.concurrent.ExecutionContext.Implicits.global + context.system.scheduler.scheduleOnce(250 milliseconds, events, msg) + } + //progress bar remains visible for all repair attempts + events ! AvatarServiceMessage(uname, AvatarAction.SendResponse(Service.defaultPlayerGUID, RepairMessage(guid, player.Armor * 100 / player.MaxArmor))) + } + } + + case _ => ; + } + + protected def TakesDamage : Receive = { + case Vitality.Damage(applyDamageTo) => if(player.isAlive) { val originalHealth = player.Health val originalArmor = player.Armor val originalCapacitor = player.Capacitor.toInt - val cause = resolution_function(player) + val cause = applyDamageTo(player) val health = player.Health val armor = player.Armor val capacitor = player.Capacitor.toInt val damageToHealth = originalHealth - health val damageToArmor = originalArmor - armor val damageToCapacitor = originalCapacitor - capacitor - PlayerControl.HandleDamageResolution(player, cause, damageToHealth, damageToArmor, damageToCapacitor) - if(damageToHealth != 0 || damageToArmor != 0 || damageToCapacitor != 0) { + 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") } } - case _ => ; } /** @@ -77,8 +155,12 @@ class PlayerControl(player : Player) extends Actor override def StartJammeredStatus(target : Any, dur : Int) : Unit = target match { case obj : Player => //TODO these features - obj.Zone.AvatarEvents ! AvatarServiceMessage(obj.Zone.Id, AvatarAction.DeactivateImplantSlot(obj.GUID, 1)) - obj.Zone.AvatarEvents ! AvatarServiceMessage(obj.Zone.Id, AvatarAction.DeactivateImplantSlot(obj.GUID, 2)) + val guid = obj.GUID + val zone = obj.Zone + val zoneId = zone.Id + val events = zone.AvatarEvents + events ! AvatarServiceMessage(zoneId, AvatarAction.DeactivateImplantSlot(guid, 1)) + events ! AvatarServiceMessage(zoneId, AvatarAction.DeactivateImplantSlot(guid, 2)) obj.skipStaminaRegenForTurns = math.max(obj.skipStaminaRegenForTurns, 10) super.StartJammeredStatus(target, dur) case _ => ; @@ -95,6 +177,13 @@ class PlayerControl(player : Player) extends Actor super.CancelJammeredSound(obj) case _ => ; } + + def RepairValue(item : Tool) : Int = if(player.ExoSuit != ExoSuitType.MAX) { + item.FireMode.Modifiers.Damage0 + } + else { + item.FireMode.Modifiers.Damage3 + } } object PlayerControl { @@ -102,62 +191,60 @@ object PlayerControl { * na * @param target na */ - def HandleDamageResolution(target : Player, cause : ResolvedProjectile, damageToHealth : Int, damageToArmor : Int, damageToCapacitor : Int) : Unit = { + def HandleDamage(target : Player, cause : ResolvedProjectile, damageToHealth : Int, damageToArmor : Int, damageToCapacitor : Int) : Unit = { val targetGUID = target.GUID - val playerGUID = target.Zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match { - case Some(player) => player.GUID - case _ => PlanetSideGUID(0) - } - if(target.Health > 0) { - //activity on map - if(damageToHealth + damageToArmor > 0) { - target.Zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) - //alert damage source - HandleDamageAwareness(target, playerGUID, cause) + 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(cause.projectile.profile.JammerProjectile) { + 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 { - HandleDestructionAwareness(target, playerGUID, Some(cause)) - } - if(damageToHealth > 0) { - target.Zone.AvatarEvents ! AvatarServiceMessage(target.Zone.Id, AvatarAction.PlanetsideAttributeToAll(targetGUID, 0, target.Health)) - } - if(damageToArmor > 0) { - target.Zone.AvatarEvents ! AvatarServiceMessage(target.Zone.Id, AvatarAction.PlanetsideAttributeToAll(targetGUID, 4, target.Armor)) - } - if(damageToCapacitor > 0) { - target.Zone.AvatarEvents ! AvatarServiceMessage(target.Name, AvatarAction.PlanetsideAttributeSelf(targetGUID, 7, target.Capacitor.toLong)) + if(damageToArmor > 0) { + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 4, target.Armor)) + } + DestructionAwareness(target, Some(cause)) } } /** * na * @param target na - * @param attribution na - * @param lastShot na + * @param cause na */ - def HandleDamageAwareness(target : Player, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = { - val owner = lastShot.projectile.owner - owner match { - case pSource : PlayerSource => - target.Zone.LivePlayers.find(_.Name == pSource.Name) match { - case Some(tplayer) => - target.Zone.AvatarEvents ! AvatarServiceMessage( - target.Name, - AvatarAction.HitHint(tplayer.GUID, target.GUID) - ) - case None => ; - } - case vSource : SourceEntry => - target.Zone.AvatarEvents ! AvatarServiceMessage( - target.Name, - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, vSource.Position)) - ) - case _ => ; - } + 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)) + } + ) } /** @@ -173,10 +260,9 @@ object PlayerControl { * 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 attribution na - * @param lastShot na + * @param cause na */ - def HandleDestructionAwareness(target : Player, attribution : PlanetSideGUID, lastShot : Option[ResolvedProjectile]) : Unit = { + def DestructionAwareness(target : Player, cause : Option[ResolvedProjectile]) : Unit = { val player_guid = target.GUID val pos = target.Position val respawnTimer = 300000 //milliseconds @@ -185,24 +271,49 @@ object PlayerControl { 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)) //align client interface fields with state - if(target.VehicleSeated.nonEmpty) { - //make player invisible (if not, the cadaver sticks out the side in a seated position) - 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)) + zone.GUID(target.VehicleSeated) match { + case Some(obj : Mountable) => + //boot cadaver from seat + events ! AvatarServiceMessage(nameChannel, AvatarAction.SendResponse(Service.defaultPlayerGUID, + ObjectDetachMessage(obj.GUID, player_guid, target.Position, Vector3.Zero)) + ) + obj.PassengerInSeat(target) match { + case Some(index) => + obj.Seats(index).Occupant = None + case _ => ; + } + //make player invisible + 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 - events ! AvatarServiceMessage(zoneChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 4, target.Armor)) //armor - if(target.ExoSuit == ExoSuitType.MAX) { + 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, player_guid, Service.defaultPlayerGUID, pos)) //how many players get this message? + AvatarAction.SendResponse(Service.defaultPlayerGUID, DestroyMessage(player_guid, attribute, Service.defaultPlayerGUID, pos)) //how many players get this message? ) events ! AvatarServiceMessage( nameChannel, @@ -214,7 +325,7 @@ object PlayerControl { case Some(PlayerSuicide(_)) => None case _ => - lastShot.orElse { target.LastShot } match { + cause.orElse { target.LastShot } match { case out @ Some(shot) => if(System.nanoTime - shot.hit_time < (10 seconds).toNanos) { out @@ -227,7 +338,6 @@ object PlayerControl { } }) match { case Some(shot) => - zone.Activity ! Zone.HotSpot.Activity(pentry, shot.projectile.owner, shot.hit_pos) events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(shot.projectile.owner, pentry, shot.projectile.attribute_to)) case None => events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0)) diff --git a/common/src/main/scala/net/psforever/objects/ballistics/ResolvedProjectile.scala b/common/src/main/scala/net/psforever/objects/ballistics/ResolvedProjectile.scala index 79b24590a..63c5411de 100644 --- a/common/src/main/scala/net/psforever/objects/ballistics/ResolvedProjectile.scala +++ b/common/src/main/scala/net/psforever/objects/ballistics/ResolvedProjectile.scala @@ -14,11 +14,11 @@ import net.psforever.types.Vector3 * @param target what the projectile hit * @param damage_model the kind of damage model to which the `target` is/was subject * @param hit_pos where the projectile hit - * @param hit_time the sequence timing when the projectile hit the target */ final case class ResolvedProjectile(resolution : ProjectileResolution.Value, projectile : Projectile, target : SourceEntry, damage_model : DamageResistanceModel, - hit_pos : Vector3, - hit_time : Long = System.nanoTime) + hit_pos : Vector3) { + val hit_time : Long = System.nanoTime +} diff --git a/common/src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala b/common/src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala index 65665c9ea..b7eb91068 100644 --- a/common/src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala +++ b/common/src/main/scala/net/psforever/objects/ce/ComplexDeployable.scala @@ -6,10 +6,6 @@ import net.psforever.objects.serverobject.PlanetSideServerObject abstract class ComplexDeployable(cdef : ComplexDeployableDefinition) extends PlanetSideServerObject with Deployable { - Health = Definition.MaxHealth - - def MaxHealth : Int = Definition.MaxHealth - private var shields : Int = 0 def Shields : Int = shields diff --git a/common/src/main/scala/net/psforever/objects/ce/Deployable.scala b/common/src/main/scala/net/psforever/objects/ce/Deployable.scala index b552874e1..e77c00d8c 100644 --- a/common/src/main/scala/net/psforever/objects/ce/Deployable.scala +++ b/common/src/main/scala/net/psforever/objects/ce/Deployable.scala @@ -2,7 +2,7 @@ package net.psforever.objects.ce import net.psforever.objects._ -import net.psforever.objects.definition.{BaseDeployableDefinition, ObjectDefinition} +import net.psforever.objects.definition.DeployableDefinition import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.vital.{DamageResistanceModel, Vitality} import net.psforever.objects.zones.ZoneAware @@ -14,16 +14,8 @@ trait Deployable extends FactionAffinity with OwnableByPlayer with ZoneAware { this : PlanetSideGameObject => - private var health : Int = 1 private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL - def Health : Int = health - - def Health_=(toHealth : Int) : Int = { - health = math.min(math.max(0, toHealth), MaxHealth) - Health - } - def MaxHealth : Int def Faction : PlanetSideEmpire.Value = faction @@ -35,7 +27,7 @@ trait Deployable extends FactionAffinity def DamageModel : DamageResistanceModel = Definition.asInstanceOf[DamageResistanceModel] - def Definition : ObjectDefinition with BaseDeployableDefinition + def Definition : DeployableDefinition } object Deployable { diff --git a/common/src/main/scala/net/psforever/objects/ce/SimpleDeployable.scala b/common/src/main/scala/net/psforever/objects/ce/SimpleDeployable.scala index 76362ea90..814779da7 100644 --- a/common/src/main/scala/net/psforever/objects/ce/SimpleDeployable.scala +++ b/common/src/main/scala/net/psforever/objects/ce/SimpleDeployable.scala @@ -8,7 +8,5 @@ abstract class SimpleDeployable(cdef : SimpleDeployableDefinition) extends Plane with Deployable { Health = Definition.MaxHealth - def MaxHealth : Int = Definition.MaxHealth - def Definition = cdef } diff --git a/common/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala index 3d26b714f..164c69d55 100644 --- a/common/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala @@ -3,12 +3,14 @@ package net.psforever.objects.definition import net.psforever.objects.avatar.Avatars import net.psforever.objects.definition.converter.AvatarConverter +import net.psforever.objects.vital.VitalityDefinition /** * The definition for game objects that look like other people, and also for players. * @param objectId the object's identifier number */ -class AvatarDefinition(objectId : Int) extends ObjectDefinition(objectId) { +class AvatarDefinition(objectId : Int) extends ObjectDefinition(objectId) + with VitalityDefinition { Avatars(objectId) //let throw NoSuchElementException Packet = AvatarDefinition.converter } diff --git a/common/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala index 44e1175f3..da957db9a 100644 --- a/common/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala @@ -26,8 +26,8 @@ class ExoSuitDefinition(private val suitType : ExoSuitType.Value) extends BasicD protected var capacitorRechargePerSecond : Int = 0 protected var capacitorDrainPerSecond : Int = 0 Name = "exo-suit" - Damage = StandardInfantryDamage - Resistance = StandardInfantryResistance + DamageUsing = StandardInfantryDamage + ResistUsing = StandardInfantryResistance Model = StandardResolutions.Infantry def SuitType : ExoSuitType.Value = suitType @@ -142,8 +142,8 @@ class SpecialExoSuitDefinition(private val suitType : ExoSuitType.Value) extends obj.ResistanceDirectHit = ResistanceDirectHit obj.ResistanceSplash = ResistanceSplash obj.ResistanceAggravated = ResistanceAggravated - obj.Damage = Damage - obj.Resistance = Resistance + obj.DamageUsing = DamageUsing + obj.ResistUsing = ResistUsing obj.Model = Model (0 until 5).foreach(index => { obj.Holster(index, Holster(index)) }) obj diff --git a/common/src/main/scala/net/psforever/objects/definition/SimpleDeployableDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/SimpleDeployableDefinition.scala index fc9e0cbb8..07b9c4304 100644 --- a/common/src/main/scala/net/psforever/objects/definition/SimpleDeployableDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/SimpleDeployableDefinition.scala @@ -7,27 +7,16 @@ import net.psforever.objects.ce.{Deployable, DeployableCategory, DeployedItem} import net.psforever.objects.definition.converter.SmallDeployableConverter import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.vital.resistance.ResistanceProfileMutators -import net.psforever.objects.vital.{DamageResistanceModel, NoResistanceSelection, StandardDeployableDamage} +import net.psforever.objects.vital.{DamageResistanceModel, NoResistanceSelection, StandardDeployableDamage, VitalityDefinition} import scala.concurrent.duration._ -trait BaseDeployableDefinition extends DamageResistanceModel - with ResistanceProfileMutators { +trait BaseDeployableDefinition { private var category : DeployableCategory.Value = DeployableCategory.Boomers private var deployTime : Long = (1 second).toMillis //ms - private var maxHealth : Int = 1 - Damage = StandardDeployableDamage - Resistance = NoResistanceSelection def Item : DeployedItem.Value - def MaxHealth : Int = maxHealth - - def MaxHealth_=(toHealth : Int) : Int = { - maxHealth = toHealth - MaxHealth - } - def DeployCategory : DeployableCategory.Value = category def DeployCategory_=(cat : DeployableCategory.Value) : DeployableCategory.Value = { @@ -53,20 +42,23 @@ trait BaseDeployableDefinition extends DamageResistanceModel def Uninitialize(obj : PlanetSideServerObject with Deployable, context : ActorContext) : Unit = { } } -class SimpleDeployableDefinition(private val objectId : Int) extends ObjectDefinition(objectId) +abstract class DeployableDefinition(objectId : Int) extends ObjectDefinition(objectId) + with DamageResistanceModel + with ResistanceProfileMutators + with VitalityDefinition with BaseDeployableDefinition { private val item = DeployedItem(objectId) //let throw NoSuchElementException + DamageUsing = StandardDeployableDamage + ResistUsing = NoResistanceSelection + + def Item : DeployedItem.Value = item +} + +class SimpleDeployableDefinition(objectId : Int) extends DeployableDefinition(objectId) { Packet = new SmallDeployableConverter - - def Item : DeployedItem.Value = item } -abstract class ComplexDeployableDefinition(private val objectId : Int) extends ObjectDefinition(objectId) - with BaseDeployableDefinition { - private val item = DeployedItem(objectId) //let throw NoSuchElementException - - def Item : DeployedItem.Value = item -} +abstract class ComplexDeployableDefinition(objectId : Int) extends DeployableDefinition(objectId) object SimpleDeployableDefinition { def apply(item : DeployedItem.Value) : SimpleDeployableDefinition = @@ -75,7 +67,7 @@ object SimpleDeployableDefinition { def SimpleUninitialize(obj : PlanetSideGameObject, context : ActorContext) : Unit = { } def SimpleUninitialize(obj : PlanetSideServerObject, context : ActorContext) : Unit = { - obj.Actor ! akka.actor.PoisonPill + context.stop(obj.Actor) obj.Actor = ActorRef.noSender } } diff --git a/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala index bd5a13437..8fb5f0cc0 100644 --- a/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala @@ -16,9 +16,9 @@ import scala.concurrent.duration._ * @param objectId the object id that is associated with this sort of `Vehicle` */ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) + with VitalityDefinition with ResistanceProfileMutators with DamageResistanceModel { - private var maxHealth : Int = 100 /** vehicle shields offered through amp station facility benefits (generally: 20% of health + 1) */ private var maxShields : Int = 0 /* key - seat index, value - seat object */ @@ -45,16 +45,11 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) private var destroyedModel : Option[DestroyedVehicle.Value] = None Name = "vehicle" Packet = VehicleDefinition.converter - Damage = StandardVehicleDamage - Resistance = StandardVehicleResistance + DamageUsing = StandardVehicleDamage + ResistUsing = StandardVehicleResistance Model = StandardResolutions.Vehicle - - def MaxHealth : Int = maxHealth - - def MaxHealth_=(health : Int) : Int = { - maxHealth = health - MaxHealth - } + RepairDistance = 10 + RepairRestoresAt = 1 def MaxShields : Int = maxShields diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala index bfb04db8d..b80024b20 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/SmallDeployableConverter.scala @@ -17,7 +17,7 @@ class SmallDeployableConverter extends ObjectCreateConverter[PlanetSideGameObjec CommonFieldData( obj.Faction, bops = false, - alternate = false, + alternate = obj.Destroyed, false, None, jammered = obj match { diff --git a/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala b/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala index 4ec1a6e70..a21a69adc 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala @@ -1,12 +1,13 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject -import net.psforever.objects.{PlanetSideGameObject, Player} +import net.psforever.objects.Player +import net.psforever.objects.serverobject.hackable.Hackable //temporary location for these messages object CommonMessages { final case class Use(player : Player, data : Option[Any] = None) final case class Unuse(player : Player, data : Option[Any] = None) - final case class Hack(player : Player) + final case class Hack(player : Player, obj : PlanetSideServerObject with Hackable, data : Option[Any] = None) final case class ClearHack() } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala b/common/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala new file mode 100644 index 000000000..41a28d962 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala @@ -0,0 +1,114 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.damage + +import akka.actor.Actor.Receive +import net.psforever.objects.ballistics.ResolvedProjectile +import net.psforever.objects.equipment.JammableUnit +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.hackable.Hackable +import net.psforever.objects.vital.Vitality + +/** + * The base "control" `Actor` mixin for damage-handling code. + * A valid entity requires health points and + * may have additional obstructions to adjusting those health points such as armor and shields. + * All of these should be affected by the damage where applicable. + */ +trait Damageable { + /** + * Contextual access to the object being the target of this damage. + * Needs declaration in lowest implementing code. + * @return the entity controlled by this actor + */ + def DamageableObject : Damageable.Target + + /** the official mixin hook; `orElse` onto the "control" `Actor` `receive` */ + final val takesDamage : Receive = TakesDamage + + /** + * Implementation of the mixin hook will be provided by a child class. + * Override this method only when directly implementing. + * @see `takesDamage` + * @see `DamageableAmenity.PerformDamage` + */ + protected def TakesDamage : Receive +} + +object Damageable { + /* the type of all entities governed by this mixin; see Repairable.Target */ + final type Target = PlanetSideServerObject with Vitality + /* the master channel for logging damage resolution information + * the format of the channel is expected to follow: + * "[identifier]: BEFORE=[before1/before2/etc.] AFTER=[after1/after2/etc.] CHANGE=[change1/change2/etc.]" + * ... where before1 - change1 = after1, and so forth, for each field that matters + * the fields do not have to be labeled but the first (if not only) should always be Health + */ + final val LogChannel : String = "DamageResolution" + + /** + * Does the possibility exist that the designated target can be affected by this projectile's damage? + * @see `Hackable` + * @see `ObjectDefinition.DamageableByFriendlyFire` + * @param obj the entity being damaged + * @param damage the amount of damage + * @param data historical information about the damage + * @return `true`, if the target can be affected; + * `false`, otherwise + */ + def CanDamage(obj : Vitality with FactionAffinity, damage : Int, data : ResolvedProjectile) : Boolean = { + val definition = obj.Definition + damage > 0 && + definition.Damageable && + (definition.DamageableByFriendlyFire || + (data.projectile.owner.Faction != obj.Faction || + (obj match { + case hobj : Hackable => hobj.HackedBy.nonEmpty + case _ => false + }) + ) + ) + } + + /** + * Does the possibility exist that the designated target can be affected by this projectile's jammer effect? + * @see `Hackable` + * @see `ProjectileDefinition..JammerProjectile` + * @param obj the entity being damaged + * @param data historical information about the damage + * @return `true`, if the target can be affected; + * `false`, otherwise + */ + def CanJammer(obj : Vitality with FactionAffinity, data : ResolvedProjectile) : Boolean = { + val projectile = data.projectile + projectile.profile.JammerProjectile && + obj.isInstanceOf[JammableUnit] && + (projectile.owner.Faction != obj.Faction || + (obj match { + case hobj : Hackable => hobj.HackedBy.nonEmpty + case _ => false + }) + ) + } + + /** + * Does the possibility exist that the designated target can be affected by this projectile? + * @param obj the entity being damaged + * @param damage the amount of damage + * @param data historical information about the damage + * @return `true`, if the target can be affected; + * `false`, otherwise + */ + def CanDamageOrJammer(obj : Vitality with FactionAffinity, damage : Int, data : ResolvedProjectile) : Boolean = { + CanDamage(obj, damage, data) || CanJammer(obj, data) + } + + /** + * The entity has ben destroyed. + * @param target the entity being damaged + * @param cause historical information about the damage + */ + def DestructionAwareness(target : Damageable.Target, cause : ResolvedProjectile) : Unit = { + target.Destroyed = true + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableAmenity.scala b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableAmenity.scala new file mode 100644 index 000000000..fbe580524 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableAmenity.scala @@ -0,0 +1,42 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.damage + +import net.psforever.objects.ballistics.ResolvedProjectile +import net.psforever.objects.serverobject.structures.Amenity +import services.avatar.{AvatarAction, AvatarServiceMessage} + +/** + * The "control" `Actor` mixin for damage-handling code + * for the majority of `Damageable` `Amenity` objects installed in a facility or a field tower, + * with specific exceptions for the `ImplantTerminalMech` and the `Generator`. + */ +trait DamageableAmenity extends DamageableEntity { + def DamageableObject : Amenity + + override protected def DestructionAwareness(target : Damageable.Target, cause : ResolvedProjectile) : Unit = { + super.DestructionAwareness(target, cause) + DamageableAmenity.DestructionAwareness(target, cause) + target.ClearHistory() + } +} + +object DamageableAmenity { + /** + * A destroyed `Amenity` target dispatches two messages to chance its model and operational states. + * The common manifestation is a sparking entity that will no longer report being accessible. + * These `PlanetSideAttributeMessage` attributes are the same as reported during zone load client configuration. + * @see `AvatarAction.PlanetsideAttributeToAll` + * @see `AvatarServiceMessage` + * @see `Zone.AvatarEvents` + * @param target the entity being destroyed + * @param cause historical information about the damage + */ + def DestructionAwareness(target : Damageable.Target, cause : ResolvedProjectile) : Unit = { + val zone = target.Zone + val zoneId = zone.Id + val events = zone.AvatarEvents + val targetGUID = target.GUID + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 50, 1)) + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 51, 1)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala new file mode 100644 index 000000000..f4c46079e --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala @@ -0,0 +1,203 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.damage + +import akka.actor.Actor.Receive +import net.psforever.objects.ballistics.ResolvedProjectile +import net.psforever.objects.equipment.JammableUnit +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.resolution.ResolutionCalculations +import net.psforever.objects.zones.Zone +import net.psforever.types.PlanetSideGUID +import services.Service +import services.avatar.{AvatarAction, AvatarServiceMessage} + +/** + * The "control" `Actor` mixin for damage-handling code, + * for both expansion into other mixins and specific application on its own. + */ +trait DamageableEntity extends Damageable { + /** log specifically for damage events */ + private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel) + + /** + * Log a damage message. + * @param msg the message for the damage log + */ + protected def DamageLog(msg : String) : Unit = { + damageLog.info(msg) + } + /** + * Log a damage message with a decorator for this target. + * The decorator is constructed by the `Actor` name of the entity, sliced after the last forward/slash. + * For example, for "foo/bar/name", the decorator is just "name". + * @see `PlanetSideServerObject` + * @param target the entity to be used for the decorator + * @param msg the message for the damage log + */ + protected def DamageLog(target : Damageable.Target, msg : String) : Unit = { + val name = target.Actor.toString + val slashPoint = name.lastIndexOf("/") + DamageLog(s"${name.substring(slashPoint + 1, name.length - 1)}: $msg") + } + + /** + * Catch the expected damage message and apply checks to the target. + * If adding custom message handling in an future child implementation, + * override this method and call `super.TakesDamage.orElse { ... }`. + * @see `Damageable.TakesDamage` + * @see `ResolutionCalcultions.Output` + * @see `Vitality.CanDamage` + * @see `Vitality.Damage` + */ + protected def TakesDamage : Receive = { + case Vitality.Damage(damage_func) => + val obj = DamageableObject + if(obj.CanDamage) { + PerformDamage(obj, damage_func) + } + } + + /** + * Assess the vital statistics of the target, apply the damage, and determine if any of those statistics changed. + * By default, only take an interest in the change of "health". + * If implementing custom `DamageableAmenity` with no new message handling, choose to override this method. + * @see `DamageableAmenity.TakesDamage` + * @see `ResolutionCalculations.Output` + * @see `Vitality.Health` + * @param target the entity to be damaged + * @param applyDamageTo the function that applies the damage to the target in a target-tailored fashion + */ + protected def PerformDamage(target : Damageable.Target, applyDamageTo : ResolutionCalculations.Output) : Unit = { + val originalHealth = target.Health + val cause = applyDamageTo(target) + val health = target.Health + val damage = originalHealth - health + if(WillAffectTarget(target, damage, cause)) { + target.History(cause) + DamageLog(target, s"BEFORE=$originalHealth, AFTER=$health, CHANGE=$damage") + HandleDamage(target, cause, damage) + } + else { + target.Health = originalHealth + } + } + + /** + * Does the damage or the projectile that caused the damage offer any reason + * to execute the reminder of damage resolution considerations? + * The projectile causing additional affects, e.g., jamming, should be tested here, when applicable. + * Contrast with `Vitality.CanDamage`. + * The damage value tested against should be the total value of all meaningful vital statistics affected. + * @see `Damageable.CanDamageOrJammer` + * @see `PerformDamage` + * @param target the entity to be damaged + * @param damage the amount of damage + * @param cause historical information about the damage + * @return `true`, if damage resolution is to be evaluated; + * `false`, otherwise + */ + protected def WillAffectTarget(target : Damageable.Target, damage : Int, cause : ResolvedProjectile) : Boolean = { + Damageable.CanDamageOrJammer(target, damage, cause) + } + + /** + * Select between mere damage reception or target destruction. + * @see `VitalDefinition.DamageDestroysAt` + * @param target the entity being damaged + * @param cause historical information about the damage + * @param damage the amount of damage + */ + protected def HandleDamage(target : Damageable.Target, cause : ResolvedProjectile, damage : Int) : Unit = { + if(!target.Destroyed && target.Health <= target.Definition.DamageDestroysAt) { + DestructionAwareness(target, cause) + } + else { + DamageAwareness(target, cause, damage) + } + } + + /** + * What happens when damage is sustained but the target does not get destroyed. + * @param target the entity being damaged + * @param cause historical information about the damage + * @param amount the amount of damage + */ + protected def DamageAwareness(target : Damageable.Target, cause : ResolvedProjectile, amount : Int) : Unit = { + DamageableEntity.DamageAwareness(target, cause, amount) + } + + /** + * What happens when the target sustains too much damage and is destroyed. + * @see `Damageable.DestructionAwareness` + * @param target the entity being destroyed + * @param cause historical information about the damage + */ + protected def DestructionAwareness(target : Damageable.Target, cause : ResolvedProjectile) : Unit = { + Damageable.DestructionAwareness(target, cause) + DamageableEntity.DestructionAwareness(target, cause) + } +} + +object DamageableEntity { + /** + * A damaged target dispatches messages to: + * - reports its adjusted its health; + * - alert the activity monitor for that `Zone` about the damage; and, + * - provide a feedback message regarding the damage. + * @see `AvatarAction.PlanetsideAttributeToAll` + * @see `AvatarAction.SendResponse` + * @see `AvatarServiceMessage` + * @see `DamageFeedbackMessage` + * @see `JammableUnit.Jammered` + * @see `Service.defaultPlayerGUID` + * @see `Zone.Activity` + * @see `Zone.AvatarEvents` + * @see `Zone.HotSpot.Activity` + * @see `Zone.LivePlayers` + * @param target the entity being damaged + * @param cause historical information about the damage + */ + def DamageAwareness(target : Damageable.Target, cause : ResolvedProjectile, amount : Int) : Unit = { + if(Damageable.CanJammer(target, cause)) { + target.Actor ! JammableUnit.Jammered(cause) + } + if(amount > 0) { + val zone = target.Zone + if(!target.Destroyed) { + val tguid = target.GUID + zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.PlanetsideAttributeToAll(tguid, 0, target.Health)) + } + zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) + } + } + + /** + * A destroyed target dispatches messages to: + * - reports its adjusted its health; and, + * - report about its destruction. + * @see `AvatarAction.Destroy` + * @see `AvatarAction.PlanetsideAttribute` + * @see `AvatarServiceMessage` + * @see `DamageFeedbackMessage` + * @see `JammableUnit.ClearJammeredSound` + * @see `JammableUnit.ClearJammeredStatus` + * @see `Zone.AvatarEvents` + * @param target the entity being destroyed + * @param cause historical information about the damage + */ + def DestructionAwareness(target : Damageable.Target, cause : ResolvedProjectile) : Unit = { + //un-jam + target.Actor ! JammableUnit.ClearJammeredSound() + target.Actor ! JammableUnit.ClearJammeredStatus() + // + val zone = target.Zone + val zoneId = zone.Id + val tguid = target.GUID + val attribution = target.Zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match { + case Some(player) => player.GUID + case _ => PlanetSideGUID(0) + } + zone.AvatarEvents ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 0, target.Health)) + zone.AvatarEvents ! AvatarServiceMessage(zoneId, AvatarAction.Destroy(tguid, attribution, Service.defaultPlayerGUID, target.Position)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala new file mode 100644 index 000000000..986e40f9a --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala @@ -0,0 +1,72 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.damage + +import net.psforever.objects.Player +import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile} +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.packet.game.DamageWithPositionMessage +import services.Service +import services.avatar.{AvatarAction, AvatarServiceMessage} + +/** + * Functions to assist other damage-dealing code for objects that contain users. + */ +object DamageableMountable { + /** + * A damaged target alerts its occupants (as it is a `Mountable` object) of the source of the damage. + * @see `AvatarAction.HitHint` + * @see `AvatarAction.SendResponse` + * @see `AvatarServiceMessage` + * @see `DamageWithPositionMessage` + * @see `Mountable.Seats` + * @see `Service.defaultPlayerGUID` + * @see `Zone.AvatarEvents` + * @see `Zone.LivePlayers` + * @param target the entity being damaged + * @param cause historical information about the damage + */ + def DamageAwareness(target : Damageable.Target with Mountable, cause : ResolvedProjectile) : Unit = { + val zone = target.Zone + val events = zone.AvatarEvents + val occupants = target.Seats.values.collect { + case seat if seat.isOccupied && seat.Occupant.get.isAlive => + seat.Occupant.get + } + (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, player.GUID) + case None => AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, pSource.Position)) + }) match { + case AvatarAction.HitHint(_, guid) => + occupants.map { tplayer => (tplayer.Name, AvatarAction.HitHint(guid, tplayer.GUID)) } + case msg => + occupants.map { tplayer => (tplayer.Name, msg) } + } + case source => //object damage + val msg = AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, source.Position)) + occupants.map { tplayer => (tplayer.Name, msg) } + }).foreach { case (channel, msg) => + events ! AvatarServiceMessage(channel, msg) + } + } + + /** + * When the target dies, so do all of its occupants. + * @see `Mountable.Seats` + * @see `Player.Die` + * @see `VitalsHistory.History` + * @param target the entity being destroyed + * @param cause historical information about the damage + */ + def DestructionAwareness(target : Damageable.Target with Mountable, cause : ResolvedProjectile) : Unit = { + target.Seats.values.filter(seat => { + seat.isOccupied && seat.Occupant.get.isAlive + }).foreach(seat => { + val tplayer = seat.Occupant.get + tplayer.History(cause) + tplayer.Actor ! Player.Die() + }) + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala new file mode 100644 index 000000000..dce43d36a --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -0,0 +1,187 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.damage + +import akka.actor.Actor.Receive +import net.psforever.objects.{GlobalDefinitions, Vehicle} +import net.psforever.objects.ballistics.ResolvedProjectile +import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.serverobject.deploy.Deployment +import net.psforever.objects.vital.resolution.ResolutionCalculations +import net.psforever.types.{DriveState, PlanetSideGUID} +import services.{RemoverActor, Service} +import services.local.{LocalAction, LocalServiceMessage} +import services.vehicle.{VehicleAction, VehicleService, VehicleServiceMessage} + +import scala.concurrent.duration._ + +/** + * The "control" `Actor` mixin for damage-handling code for `Vehicle` objects. + */ +trait DamageableVehicle extends DamageableEntity { + /** vehicles (may) have shields; they need to be handled */ + private var handleDamageToShields : Boolean = false + /** whether or not the vehicle has been damaged directly, report that damage has occurred */ + private var reportDamageToVehicle : Boolean = false + + def DamageableObject : Vehicle + + override protected def TakesDamage : Receive = + super.TakesDamage.orElse { + case DamageableVehicle.Damage(cause, damage) => + //cargo vehicles inherit feedback from carrier + reportDamageToVehicle = damage > 0 + DamageAwareness(DamageableObject, cause, amount = 0) + + case DamageableVehicle.Destruction(cause) => + //cargo vehicles are destroyed when carrier is destroyed + val obj = DamageableObject + obj.Health = 0 + obj.History(cause) + DestructionAwareness(obj, cause) + } + + /** + * Vehicles may have charged shields that absorb damage before the vehicle's own health is affected. + * @param target the entity to be damaged + * @param applyDamageTo the function that applies the damage to the target in a target-tailored fashion + */ + override protected def PerformDamage(target : Damageable.Target, applyDamageTo : ResolutionCalculations.Output) : Unit = { + val obj = DamageableObject + val originalHealth = obj.Health + val originalShields = obj.Shields + val cause = applyDamageTo(obj) + val health = obj.Health + val shields = obj.Shields + val damageToHealth = originalHealth - health + val damageToShields = originalShields - shields + if(WillAffectTarget(target, damageToHealth + damageToShields, cause)) { + target.History(cause) + DamageLog(target, s"BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields") + handleDamageToShields = damageToShields > 0 + HandleDamage(target, cause, damageToHealth + damageToShields) + } + else { + obj.Health = originalHealth + obj.Shields = originalShields + } + } + + override protected def DamageAwareness(target : Target, cause : ResolvedProjectile, amount : Int) : Unit = { + val obj = DamageableObject + val handleShields = handleDamageToShields + handleDamageToShields = false + val handleReport = reportDamageToVehicle || amount > 0 + reportDamageToVehicle = false + if(Damageable.CanDamageOrJammer(target, amount, cause)) { + super.DamageAwareness(target, cause, amount) + } + if(handleReport) { + DamageableMountable.DamageAwareness(obj, cause) + } + DamageableVehicle.DamageAwareness(obj, cause, amount, handleShields) + } + + override protected def DestructionAwareness(target : Target, cause : ResolvedProjectile) : Unit = { + super.DestructionAwareness(target, cause) + val obj = DamageableObject + DamageableMountable.DestructionAwareness(obj, cause) + DamageableVehicle.DestructionAwareness(obj, cause) + DamageableWeaponTurret.DestructionAwareness(obj, cause) + } +} + +object DamageableVehicle { + /** + * Message for instructing the target's cargo vehicles about a damage source affecting their carrier. + * @param cause historical information about damage + */ + private case class Damage(cause : ResolvedProjectile, amount : Int) + /** + * Message for instructing the target's cargo vehicles that their carrier is destroyed, + * and they should be destroyed too. + * @param cause historical information about damage + */ + private case class Destruction(cause : ResolvedProjectile) + + /** + * Most all vehicles and the weapons mounted to them can jam + * if the projectile that strikes (near) them has jammering properties. + * A damaged carrier alerts its cargo vehicles of the source of the damage, + * but it will not be affected by the same jammering effect. + * If this vehicle has shields that were affected by previous damage, that is also reported to the clients. + * @see `Service.defaultPlayerGUID` + * @see `Vehicle.CargoHolds` + * @see `VehicleAction.PlanetsideAttribute` + * @see `VehicleServiceMessage` + * @param target the entity being destroyed + * @param cause historical information about the damage + * @param damage how much damage was performed + * @param damageToShields dispatch a shield strength update + */ + def DamageAwareness(target : Vehicle, cause : ResolvedProjectile, damage : Int, damageToShields : Boolean) : Unit = { + //alert cargo occupants to damage source + target.CargoHolds.values.foreach(hold => { + hold.Occupant match { + case Some(cargo) => + cargo.Actor ! DamageableVehicle.Damage(cause, damage + (if(damageToShields) 1 else 0)) + case None => ; + } + }) + //shields + if(damageToShields) { + val zone = target.Zone + zone.VehicleEvents ! VehicleServiceMessage(s"${target.Actor}", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 68, target.Shields)) + } + } + + /** + * A destroyed carrier informs its cargo vehicles that they should also be destroyed + * for reasons of the same cause being inherited as the source of damage. + * Regardless of the amount of damage they carrier takes or some other target would take, + * its cargo vehicles die immediately. + * The vehicle's shields are zero'd out if they were previously energized + * so that the vehicle's corpse does not act like it is still protected by vehicle shields. + * Finally, the vehicle is tasked for deconstruction. + * @see `Deployment.TryDeploymentChange` + * @see `DriveState.Undeploying` + * @see `Service.defaultPlayerGUID` + * @see `Vehicle.CargoHolds` + * @see `VehicleAction.PlanetsideAttribute` + * @see `RemoverActor.AddTask` + * @see `RemoverActor.ClearSpecific` + * @see `VehicleServiceMessage` + * @see `VehicleServiceMessage.Decon` + * @see `Zone.VehicleEvents` + * @param target the entity being destroyed + * @param cause historical information about the damage + */ + def DestructionAwareness(target : Vehicle, cause : ResolvedProjectile) : Unit = { + val zone = target.Zone + //cargo vehicles die with us + target.CargoHolds.values.foreach(hold => { + hold.Occupant match { + case Some(cargo) => + cargo.Actor ! DamageableVehicle.Destruction(cause) + case None => ; + } + }) + //special considerations for certain vehicles + target.Definition match { + case GlobalDefinitions.ams => + target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying) + case GlobalDefinitions.router => + target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying) + VehicleService.BeforeUnloadVehicle(target, zone) + zone.LocalEvents ! LocalServiceMessage(zone.Id, LocalAction.ToggleTeleportSystem(PlanetSideGUID(0), target, None)) + case _ => ; + } + //shields + if(target.Shields > 0) { + target.Shields = 0 + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 68, 0)) + } + zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), zone)) + zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, zone, Some(1 minute))) + target.ClearHistory() + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala new file mode 100644 index 000000000..6be219348 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala @@ -0,0 +1,78 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.damage + +import net.psforever.objects.ballistics.ResolvedProjectile +import net.psforever.objects.serverobject.turret.{TurretUpgrade, WeaponTurret} +import net.psforever.objects.vehicles.MountedWeapons +import services.Service +import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.vehicle.support.TurretUpgrader +import services.vehicle.VehicleServiceMessage + +/** + * The "control" `Actor` mixin for damage-handling code for `WeaponTurret` objects. + */ +trait DamageableWeaponTurret extends DamageableEntity { + def DamageableObject : Damageable.Target with WeaponTurret + + override protected def DamageAwareness(target : Damageable.Target, cause : ResolvedProjectile, amount : Int) : Unit = { + super.DamageAwareness(target, cause, amount) + if(amount > 0) { + DamageableMountable.DamageAwareness(DamageableObject, cause) + } + } + + override protected def DestructionAwareness(target : Damageable.Target, cause : ResolvedProjectile) : Unit = { + super.DestructionAwareness(target, cause) + val obj = DamageableObject + DamageableWeaponTurret.DestructionAwareness(obj, cause) + DamageableMountable.DestructionAwareness(obj, cause) + } +} + +object DamageableWeaponTurret { + /** + * A destroyed target dispatches a message to conceal (delete) its weapons from users. + * If affected by a jammer property, the jammer propoerty will be removed. + * If the type of entity is a `WeaponTurret`, the weapons are converted to their "normal" upgrade state. + * @see `AvatarAction.DeleteObject` + * @see `AvatarServiceMessage` + * @see `MountedWeapons` + * @see `MountedWeapons.Weapons` + * @see `Service.defaultPlayerGUID` + * @see `TurretUpgrade.None` + * @see `TurretUpgrader.AddTask` + * @see `TurretUpgrader.ClearSpecific` + * @see `WeaponTurret` + * @see `VehicleServiceMessage.TurretUpgrade` + * @see `Zone.AvatarEvents` + * @see `Zone.VehicleEvents` + * @param target the entity being destroyed; + * note: `MountedWeapons` is a parent of `WeaponTurret` + * but the handling code closely associates with the former + * @param cause historical information about the damage + */ + def DestructionAwareness(target : Damageable.Target with MountedWeapons, cause : ResolvedProjectile) : Unit = { + //wreckage has no (visible) mounted weapons + val zone = target.Zone + val zoneId = zone.Id + val avatarEvents = zone.AvatarEvents + target.Weapons.values + .filter { + _.Equipment.nonEmpty + } + .foreach(slot => { + val wep = slot.Equipment.get + avatarEvents ! AvatarServiceMessage(zoneId, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, wep.GUID)) + }) + target match { + case turret : WeaponTurret => + if(turret.Upgrade != TurretUpgrade.None) { + val vehicleEvents = zone.VehicleEvents + vehicleEvents ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.ClearSpecific(List(turret), zone)) + vehicleEvents ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.AddTask(turret, zone, TurretUpgrade.None)) + } + case _ => + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala index 127d1623b..27781b446 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala @@ -4,7 +4,6 @@ package net.psforever.objects.serverobject.doors import net.psforever.objects.Player import net.psforever.objects.serverobject.structures.Amenity import net.psforever.packet.game.UseItemMessage -import net.psforever.types.Vector3 /** * A structure-owned server object that is a "door" that can open and can close. diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala index 6a22670cd..6864f3a1a 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala @@ -1,12 +1,12 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.doors -import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.structures.AmenityDefinition /** * The definition for any `Door`. * Object Id 242 is a generic door. */ -class DoorDefinition extends ObjectDefinition(242) { +class DoorDefinition extends AmenityDefinition(242) { Name = "door" } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/generator/Generator.scala b/common/src/main/scala/net/psforever/objects/serverobject/generator/Generator.scala index e7838b573..d1309502d 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/generator/Generator.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/generator/Generator.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.generator import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.types.PlanetSideGeneratorState /** * The generator is a big feature of all major facilities. @@ -14,7 +15,15 @@ import net.psforever.objects.serverobject.structures.Amenity * @param gdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ class Generator(private val gdef : GeneratorDefinition) extends Amenity { - //TODO should have Vitality, to indicate damaged/destroyed property + private var condition : PlanetSideGeneratorState.Value = PlanetSideGeneratorState.Normal + + def Condition : PlanetSideGeneratorState.Value = condition + + def Condition_=(state : PlanetSideGeneratorState.Value) : PlanetSideGeneratorState.Value = { + condition = state + Condition + } + def Definition : GeneratorDefinition = gdef } @@ -24,15 +33,6 @@ object Generator { } import akka.actor.ActorContext - def Constructor(id : Int, context : ActorContext) : Generator = { - import akka.actor.Props - import net.psforever.objects.GlobalDefinitions - - val obj = Generator(GlobalDefinitions.generator) - obj.Actor = context.actorOf(Props(classOf[GeneratorControl], obj), s"${obj.Definition.Name}_$id") - obj - } - import net.psforever.types.Vector3 def Constructor(pos : Vector3)(id : Int, context : ActorContext) : Generator = { import akka.actor.Props diff --git a/common/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala index 19fceeb24..b7be5b051 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala @@ -2,17 +2,147 @@ package net.psforever.objects.serverobject.generator import akka.actor.Actor -import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.{Player, Tool} +import net.psforever.objects.ballistics._ +import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior +import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.serverobject.damage.DamageableEntity +import net.psforever.objects.serverobject.repair.{Repairable, RepairableEntity} +import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.vital.DamageFromExplosion +import net.psforever.packet.game.TriggerEffectMessage +import net.psforever.types.{PlanetSideGeneratorState, Vector3} +import services.Service +import services.avatar.{AvatarAction, AvatarServiceMessage} /** * An `Actor` that handles messages being dispatched to a specific `Generator`. * @param gen the `Generator` object being governed */ class GeneratorControl(gen : Generator) extends Actor - with FactionAffinityBehavior.Check { - def FactionObject : FactionAffinity = gen + with FactionAffinityBehavior.Check + with DamageableEntity + with RepairableEntity { + def FactionObject = gen + def DamageableObject = gen + def RepairableObject = gen + var imminentExplosion : Boolean = false - def receive : Receive = checkBehavior.orElse { - case _ => ; + def receive : Receive = checkBehavior + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) + .orElse { + case GeneratorControl.GeneratorExplodes() => //TODO this only works with projectiles right now! + val zone = gen.Zone + gen.Health = 0 + super.DestructionAwareness(gen, gen.LastShot.get) + gen.Condition = PlanetSideGeneratorState.Destroyed + GeneratorControl.UpdateOwner(gen) + //kaboom + zone.AvatarEvents ! AvatarServiceMessage( + zone.Id, AvatarAction.SendResponse( + Service.defaultPlayerGUID, TriggerEffectMessage(gen.GUID, "explosion_generator", None, None) + ) + ) + imminentExplosion = false + //kill everyone within 14m + gen.Owner match { + case b : Building => + val genDef = gen.Definition + b.PlayersInSOI.collect { + case player if player.isAlive && Vector3.DistanceSquared(player.Position, gen.Position) < 196 => + player.History(DamageFromExplosion(PlayerSource(player), genDef)) + player.Actor ! Player.Die() + } + case _ => ; + } + gen.ClearHistory() + + case _ => ; + } + + override protected def CanPerformRepairs(obj : Target, player : Player, item : Tool) : Boolean = { + !imminentExplosion && super.CanPerformRepairs(obj, player, item) + } + + override protected def WillAffectTarget(target : Target, damage : Int, cause : ResolvedProjectile) : Boolean = { + !imminentExplosion && super.WillAffectTarget(target, damage, cause) + } + + override protected def DamageAwareness(target : Target, cause : ResolvedProjectile, amount : Int) : Unit = { + super.DamageAwareness(target, cause, amount) + GeneratorControl.DamageAwareness(gen, cause, amount) + } + + override protected def DestructionAwareness(target : Target, cause : ResolvedProjectile) : Unit = { + if(!target.Destroyed) { + target.Health = 1 //temporary + imminentExplosion = true + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + context.system.scheduler.scheduleOnce(10 seconds, self, GeneratorControl.GeneratorExplodes()) + GeneratorControl.BroadcastGeneratorEvent(gen, 16) + } + } + + override def Restoration(obj : Repairable.Target) : Unit = { + super.Restoration(obj) + gen.Condition = PlanetSideGeneratorState.Normal + GeneratorControl.UpdateOwner(gen) + GeneratorControl.BroadcastGeneratorEvent(gen, 17) + } +} + +object GeneratorControl { + /** + * na + */ + private case class GeneratorExplodes() + + /** + * na + * @param obj na + */ + private def UpdateOwner(obj : Generator) : Unit = { + obj.Owner match { + case b : Building => b.Actor ! Building.AmenityStateChange(obj) + case _ => ; + } + } + + /** + * na + * @param target the generator + * @param event the action code for the event + */ + private def BroadcastGeneratorEvent(target : Generator, event : Int) : Unit = { + target.Owner match { + case b : Building => + val events = target.Zone.AvatarEvents + val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, target.Owner.GUID, event) + b.PlayersInSOI.foreach { player => + events ! AvatarServiceMessage(player.Name, msg) + } + case _ => ; + } + } + + /** + * If not destroyed, it will complain about being damaged. + * @param target the entity being damaged + * @param cause historical information about the damage + * @param amount the amount of damage + */ + def DamageAwareness(target : Generator, cause : ResolvedProjectile, amount : Int) : Unit = { + if(!target.Destroyed) { + val health : Float = target.Health + val max : Float = target.MaxHealth + if(target.Condition != PlanetSideGeneratorState.Critical && health / max < 0.51f) { //becoming critical + target.Condition = PlanetSideGeneratorState.Critical + GeneratorControl.UpdateOwner(target) + } + //the generator is under attack + GeneratorControl.BroadcastGeneratorEvent(target, 15) + } } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorDefinition.scala index a8e4a77fc..3e6236bdf 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorDefinition.scala @@ -1,11 +1,11 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.serverobject.generator -import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.structures.AmenityDefinition /** * The definition for a `Generator` object. */ -class GeneratorDefinition(objectId : Int) extends ObjectDefinition(objectId) { +class GeneratorDefinition(objectId : Int) extends AmenityDefinition(objectId) { Name = "generator" } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala b/common/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala new file mode 100644 index 000000000..b47e1d6a1 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala @@ -0,0 +1,120 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.hackable + +import net.psforever.objects.{Player, Vehicle} +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.packet.game.{HackMessage, HackState} +import net.psforever.types.PlanetSideGUID +import services.Service +import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.local.{LocalAction, LocalServiceMessage} + +import scala.util.{Failure, Success} + +object GenericHackables { + private val log = org.log4s.getLogger("HackableBehavior") + + /** + * na + * @param player the player doing the hacking + * @param obj the object being hacked + * @return the percentage amount of progress per tick + */ + def GetHackSpeed(player : Player, obj: PlanetSideServerObject): Float = { + val playerHackLevel = Player.GetHackLevel(player) + val timeToHack = obj match { + case vehicle : Vehicle => vehicle.JackingDuration(playerHackLevel) + case hackable : Hackable => hackable.HackDuration(playerHackLevel) + case _ => + log.warn(s"${player.Name} tried to hack an object that has no hack time defined - ${obj.Definition.Name}#${obj.GUID} on ${obj.Zone.Id}") + 0 + } + if(timeToHack == 0) { + log.warn(s"${player.Name} tried to hack an object that they don't have the correct hacking level for - ${obj.Definition.Name}#${obj.GUID} on ${obj.Zone.Id}") + 0f + } + else { + //timeToHack is in seconds; progress is measured in quarters of a second (250ms) + (100 / timeToHack) / 4 + } + } + + /** + * Evaluate the progress of the user applying a tool to modify some server object. + * This action is using the remote electronics kit to convert an enemy unit into an allied unit, primarily. + * The act of transforming allied units of one kind into allied units of another kind (facility turret upgrades) + * is also governed by this action per tick of progress. + * @see `HackMessage` + * @see `HackState` + * @param progressType 1 - remote electronics kit hack (various ...); + * 2 - nano dispenser (upgrade canister) turret upgrade + * @param tplayer the player performing the action + * @param target the object being affected + * @param tool_guid the tool being used to affest the object + * @param progress the current progress value + * @return `true`, if the next cycle of progress should occur; + * `false`, otherwise + */ + def HackingTickAction(progressType : Int, tplayer : Player, target : PlanetSideServerObject, tool_guid : PlanetSideGUID)(progress : Float) : Boolean = { + //hack state for progress bar visibility + val vis = if(progress <= 0L) { + HackState.Start + } + else if(progress >= 100L) { + HackState.Finished + } + else if(target.isMoving(1f)) { + // If the object is moving (more than slightly to account for things like magriders rotating, or the last velocity reported being the magrider dipping down on dismount) then cancel the hack + HackState.Cancelled + } + else { + HackState.Ongoing + } + target.Zone.AvatarEvents ! AvatarServiceMessage( + tplayer.Name, + AvatarAction.SendResponse(Service.defaultPlayerGUID, + if(!target.HasGUID) { + //cancel the hack (target is gone) + HackMessage(progressType, target.GUID, tplayer.GUID, 0, 0L, HackState.Cancelled, 8L) + } + else if(vis == HackState.Cancelled) { + //cancel the hack (e.g. vehicle drove away) + HackMessage(progressType, target.GUID, tplayer.GUID, 0, 0L, vis, 8L) + } + else { + HackMessage(progressType, target.GUID, tplayer.GUID, progress.toInt, 0L, vis, 8L) + } + ) + ) + vis != HackState.Cancelled + } + + /** + * The process of hacking an object is completed. + * Pass the message onto the hackable object and onto the local events system. + * @param target the `Hackable` object that has been hacked + * @param user the player that is performing this hacking task + * @param unk na; + * used by `HackMessage` as `unk5` + * @see `HackMessage` + */ + //TODO add params here depending on which params in HackMessage are important + def FinishHacking(target : PlanetSideServerObject with Hackable, user : Player, unk : Long)() : Unit = { + import akka.pattern.ask + import scala.concurrent.duration._ + log.info(s"Hacked a $target") + // Wait for the target actor to set the HackedBy property, otherwise LocalAction.HackTemporarily will not complete properly + import scala.concurrent.ExecutionContext.Implicits.global + val tplayer = user + ask(target.Actor, CommonMessages.Hack(tplayer, target))(1 second).mapTo[Boolean].onComplete { + case Success(_) => + val zone = target.Zone + val zoneId = zone.Id + val pguid = tplayer.GUID + zone.LocalEvents ! LocalServiceMessage(zoneId, LocalAction.TriggerSound(pguid, target.HackSound, tplayer.Position, 30, 0.49803925f)) + zone.LocalEvents ! LocalServiceMessage(zoneId, LocalAction.HackTemporarily(pguid, zone, target, unk, target.HackEffectDuration(Player.GetHackLevel(user)))) + case Failure(_) => log.warn(s"Hack message failed on target guid: ${target.GUID}") + } + } +} + diff --git a/common/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala b/common/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala index ace2fe5b0..cea75564c 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala @@ -2,11 +2,12 @@ package net.psforever.objects.serverobject.hackable import net.psforever.objects.Player import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.hackable.Hackable.HackInfo import net.psforever.packet.game.TriggeredSound import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} -trait Hackable extends FactionAffinity { - import Hackable._ +trait Hackable { + _ : FactionAffinity => /** inportant information regarding the hack and how it was started */ private var hackedBy : Option[HackInfo] = None def HackedBy : Option[HackInfo] = hackedBy @@ -66,6 +67,18 @@ trait Hackable extends FactionAffinity { hackDuration = arr arr } + +// private var hackable : Option[Boolean] = None +// def Hackable : Boolean = hackable.getOrElse(Definition.Hackable) +// +// def Hackable_=(state : Boolean) : Boolean = Hackable_=(Some(state)) +// +// def Hackable_=(state : Option[Boolean]) : Boolean = { +// hackable = state +// Hackable +// } +// +// def Definition : HackableDefinition } object Hackable { diff --git a/common/src/main/scala/net/psforever/objects/serverobject/hackable/HackableBehavior.scala b/common/src/main/scala/net/psforever/objects/serverobject/hackable/HackableBehavior.scala index 8203a8826..9a30fd8db 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/hackable/HackableBehavior.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/hackable/HackableBehavior.scala @@ -1,3 +1,4 @@ +// Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.hackable import akka.actor.Actor @@ -6,19 +7,19 @@ import net.psforever.objects.serverobject.CommonMessages object HackableBehavior { /** * The logic governing generic `Hackable` objects that use the `Hack` and `ClearHack` message. - * This is a mix-in trait for combining with existing Receive` logic. + * This is a mix-in trait for combining with existing `Receive` logic. * @see `Hackable` */ trait GenericHackable { this : Actor => - def HackableObject : Hackable val hackableBehavior : Receive = { - case CommonMessages.Hack(player) => + case CommonMessages.Hack(player, _, _) => val obj = HackableObject obj.HackedBy = player sender ! true + case CommonMessages.ClearHack() => val obj = HackableObject obj.HackedBy = None diff --git a/common/src/main/scala/net/psforever/objects/serverobject/hackable/HackableDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/hackable/HackableDefinition.scala new file mode 100644 index 000000000..f22f65c90 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/hackable/HackableDefinition.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.hackable + +class HackableDefinition { + private var hackable : Boolean = false + private var magicNumber : Long = 0 + + def Hackable : Boolean = hackable + + def Hackable_=(state : Boolean) : Boolean = { + hackable = state + Hackable + } + + def MagicNumber : Long = magicNumber + + def MagicNumber_=(magic : Long) : Long = { + magicNumber = magic + MagicNumber + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMech.scala b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMech.scala index 8e9b6f673..b6fed718d 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMech.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMech.scala @@ -2,12 +2,12 @@ package net.psforever.objects.serverobject.implantmech import net.psforever.objects.Player -import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.vehicles.Seat import net.psforever.packet.game.TriggeredSound +import net.psforever.types.Vector3 /** * A structure-owned server object that is the visible and `Mountable` component of an implant terminal. @@ -38,7 +38,7 @@ class ImplantTerminalMech(private val idef : ImplantTerminalMechDefinition) exte } } - def Definition : ObjectDefinition = idef + def Definition : ImplantTerminalMechDefinition = idef } object ImplantTerminalMech { @@ -53,10 +53,21 @@ object ImplantTerminalMech { import akka.actor.ActorContext /** * Instantiate an configure a `ImplantTerminalMech` object + * @param pos the position of the entity * @param id the unique id that will be assigned to this entity * @param context a context to allow the object to properly set up `ActorSystem` functionality * @return the `ImplantTerminalMech` object */ + def Constructor(pos : Vector3)(id : Int, context : ActorContext) : ImplantTerminalMech = { + import akka.actor.Props + import net.psforever.objects.GlobalDefinitions + + val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) + obj.Position = pos + obj.Actor = context.actorOf(Props(classOf[ImplantTerminalMechControl], obj), s"${GlobalDefinitions.implant_terminal_mech.Name}_$id") + obj + } + @deprecated("use implant terminal mechs that have position","destroyAndRepair") def Constructor(id : Int, context : ActorContext) : ImplantTerminalMech = { import akka.actor.Props import net.psforever.objects.GlobalDefinitions diff --git a/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala index ccf02782b..4824abed6 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala @@ -2,26 +2,73 @@ package net.psforever.objects.serverobject.implantmech import akka.actor.Actor -import net.psforever.objects.serverobject.mount.MountableBehavior -import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.ballistics.ResolvedProjectile +import net.psforever.objects.{GlobalDefinitions, Player, SimpleItem} +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} +import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior +import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity, DamageableMountable} import net.psforever.objects.serverobject.hackable.HackableBehavior +import net.psforever.objects.serverobject.repair.RepairableEntity +import net.psforever.objects.serverobject.structures.Building /** * An `Actor` that handles messages being dispatched to a specific `ImplantTerminalMech`. * @param mech the "mech" object being governed */ -class ImplantTerminalMechControl(mech : ImplantTerminalMech) extends Actor with FactionAffinityBehavior.Check - with MountableBehavior.Mount with MountableBehavior.Dismount with HackableBehavior.GenericHackable { - def MountableObject = mech //do not add type! +class ImplantTerminalMechControl(mech : ImplantTerminalMech) extends Actor + with FactionAffinityBehavior.Check + with MountableBehavior.Mount + with MountableBehavior.Dismount + with HackableBehavior.GenericHackable + with DamageableEntity + with RepairableEntity { + def MountableObject = mech def HackableObject = mech - - def FactionObject : FactionAffinity = mech + def FactionObject = mech + def DamageableObject = mech + def RepairableObject = mech def receive : Receive = checkBehavior .orElse(mountBehavior) .orElse(dismountBehavior) .orElse(hackableBehavior) + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) .orElse { + case CommonMessages.Use(player, Some(item : SimpleItem)) if item.Definition == GlobalDefinitions.remote_electronics_kit => + //TODO setup certifications check + mech.Owner match { + case b : Building if (b.Faction != player.Faction || b.CaptureConsoleIsHacked) && mech.HackedBy.isEmpty => + sender ! CommonMessages.Hack(player, mech, Some(item)) + case _ => ; + } case _ => ; } + + override protected def MountTest(obj : PlanetSideServerObject with Mountable, seatNumber : Int, player : Player) : Boolean = { + val zone = obj.Zone + zone.Map.TerminalToInterface.get(obj.GUID.guid) match { + case Some(interface_guid) => + (zone.GUID(interface_guid) match { + case Some(interface) => !interface.Destroyed + case None => false + }) && + super.MountTest(obj, seatNumber, player) + case None => + false + } + } + + override protected def DamageAwareness(target : Target, cause : ResolvedProjectile, amount : Int) : Unit = { + super.DamageAwareness(target, cause, amount) + DamageableMountable.DamageAwareness(DamageableObject, cause) + } + + override protected def DestructionAwareness(target : Damageable.Target, cause : ResolvedProjectile) : Unit = { + super.DestructionAwareness(target, cause) + DamageableMountable.DestructionAwareness(DamageableObject, cause) + target.ClearHistory() + } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechDefinition.scala index 554a19d31..b8fd76e51 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechDefinition.scala @@ -1,15 +1,15 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.implantmech -import net.psforever.objects.definition.{ObjectDefinition, SeatDefinition} -import net.psforever.objects.vehicles.SeatArmorRestriction +import net.psforever.objects.definition.SeatDefinition +import net.psforever.objects.serverobject.structures.AmenityDefinition /** * The `Definition` for any `Terminal` that is of a type "implant_terminal_interface." * Implant terminals are composed of two components. * This `Definition` constructs the visible mechanical tube component that can be mounted. */ -class ImplantTerminalMechDefinition extends ObjectDefinition(410) { +class ImplantTerminalMechDefinition extends AmenityDefinition(410) { /* key - seat index, value - seat object */ private val seats : Map[Int, SeatDefinition] = Map(0 -> new SeatDefinition) /* key - entry point index, value - seat index */ diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala index d722dc265..43785543c 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.locks import akka.actor.Actor +import net.psforever.objects.{GlobalDefinitions, SimpleItem} import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.hackable.HackableBehavior @@ -18,6 +19,17 @@ class IFFLockControl(lock : IFFLock) extends Actor with FactionAffinityBehavior. def receive : Receive = checkBehavior .orElse(hackableBehavior) .orElse { + case CommonMessages.Use(player, Some(item : SimpleItem)) if item.Definition == GlobalDefinitions.remote_electronics_kit => + if((lock.Faction != player.Faction && lock.HackedBy.isEmpty) || (lock.Faction == player.Faction && lock.HackedBy.nonEmpty)) { + sender ! CommonMessages.Hack(player, lock, Some(item)) + } + else { + val log = org.log4s.getLogger + log.warn("IFF lock is being hacked, but don't know how to handle this state:") + log.warn(s"Lock - Faction=${lock.Faction}, HackedBy=${lock.HackedBy}") + log.warn(s"Player - Faction=${player.Faction}") + } + case _ => ; //no default message } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala index d8c180d89..395123447 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala @@ -1,12 +1,12 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.locks -import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.structures.AmenityDefinition /** * The definition for any `IFFLock`. * Object Id 451 is a generic external lock. */ -class IFFLockDefinition extends ObjectDefinition(451) { +class IFFLockDefinition extends AmenityDefinition(451) { Name = "iff_lock" } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLocks.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLocks.scala new file mode 100644 index 000000000..a859ceeb2 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLocks.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.locks + +import services.Service +import services.local.{LocalAction, LocalServiceMessage} + +object IFFLocks { + /** + * The process of resecuring an IFF lock is finished + * Clear the hack state and send to clients + * @param lock the `IFFLock` object that has been resecured + */ + def FinishResecuringIFFLock(lock: IFFLock)() : Unit = { + val zone = lock.Zone + lock.Zone.LocalEvents ! LocalServiceMessage(zone.Id, LocalAction.ClearTemporaryHack(Service.defaultPlayerGUID, lock)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerControl.scala index e3a11e472..1a781c08f 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerControl.scala @@ -2,6 +2,8 @@ package net.psforever.objects.serverobject.mblocker import akka.actor.Actor +import net.psforever.objects.{GlobalDefinitions, SimpleItem} +import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.hackable.HackableBehavior @@ -16,6 +18,11 @@ class LockerControl(locker : Locker) extends Actor with FactionAffinityBehavior. def receive : Receive = checkBehavior .orElse(hackableBehavior) .orElse { + case CommonMessages.Use(player, Some(item : SimpleItem)) if item.Definition == GlobalDefinitions.remote_electronics_kit => + //TODO setup certifications check + if(locker.Faction != player.Faction && locker.HackedBy.isEmpty) { + sender ! CommonMessages.Hack(player, locker, Some(item)) + } case _ => ; } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerDefinition.scala index b73a9e533..d58c2ef31 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerDefinition.scala @@ -1,12 +1,12 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.mblocker -import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.structures.AmenityDefinition /** * The definition for any `Locker`. * Object Id 524. */ -class LockerDefinition extends ObjectDefinition(524) { +class LockerDefinition extends AmenityDefinition(524) { Name = "mb_locker" } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala b/common/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala index 26f3ca560..175294e57 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala @@ -2,11 +2,12 @@ package net.psforever.objects.serverobject.mount import akka.actor.Actor -import net.psforever.objects.{PlanetSideGameObject, Vehicle} +import net.psforever.objects.{Player, Vehicle} import net.psforever.objects.entity.{Identifiable, WorldEntity} +import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.serverobject.turret.TurretDefinition +import net.psforever.objects.serverobject.turret.WeaponTurret import net.psforever.types.DriveState object MountableBehavior { @@ -17,51 +18,50 @@ object MountableBehavior { * @see `Mountable` */ trait Mount { - this : Actor => - - def MountableObject : PlanetSideGameObject with Mountable with FactionAffinity + _ : Actor => + def MountableObject : PlanetSideServerObject with Mountable with FactionAffinity val mountBehavior : Receive = { case Mountable.TryMount(user, seat_num) => val obj = MountableObject - obj.Seat(seat_num) match { - case Some(seat) => - - var isHacked = false - if(MountableObject.isInstanceOf[Hackable]) { - // This is a special case for implant terminals, since they're both mountable and hackable, but not jackable. - isHacked = MountableObject.asInstanceOf[Hackable].HackedBy.isDefined - } - - if((user.Faction == obj.Faction || isHacked) && (seat.Occupant = user).contains(user)) { - user.VehicleSeated = obj.GUID - sender ! Mountable.MountMessages(user, Mountable.CanMount(obj, seat_num)) - } - else { - sender ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) - } - case None => - sender ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) + if(MountTest(MountableObject, seat_num, user)) { + user.VehicleSeated = obj.GUID + sender ! Mountable.MountMessages(user, Mountable.CanMount(obj, seat_num)) + } + else { + sender ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) } } - val turretMountBehavior : Receive = { - case Mountable.TryMount(user, seat_num) => - val obj = MountableObject - val definition = obj.Definition.asInstanceOf[TurretDefinition] - obj.Seat(seat_num) match { - case Some(seat) => - if((!definition.FactionLocked || user.Faction == obj.Faction) && - (seat.Occupant = user).contains(user)) { - user.VehicleSeated = obj.GUID - sender ! Mountable.MountMessages(user, Mountable.CanMount(obj, seat_num)) - } - else { - sender ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) - } - case None => - sender ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) - } + protected def MountTest(obj : PlanetSideServerObject with Mountable, seatNumber : Int, player : Player) : Boolean = { + (player.Faction == obj.Faction || + (obj match { + case o : Hackable => o.HackedBy.isDefined + case _ => false + })) && + !obj.Destroyed && + (obj.Seats.get(seatNumber) match { + case Some(seat) => (seat.Occupant = player).contains(player) + case _ => false + }) + } + } + + trait TurretMount extends Mount { + _ : Actor => + + override protected def MountTest(obj : PlanetSideServerObject with Mountable, seatNumber : Int, player : Player) : Boolean = { + obj match { + case wep : WeaponTurret => + (!wep.Definition.FactionLocked || player.Faction == obj.Faction) && + !obj.Destroyed && + (obj.Seats.get(seatNumber) match { + case Some(seat) => (seat.Occupant = player).contains(player) + case _ => false + }) + case _ => + super.MountTest(obj, seatNumber, player) + } } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala index af0a0d3ea..ebef3a146 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala @@ -1,12 +1,12 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.pad -import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.structures.AmenityDefinition /** * The definition for any `VehicleSpawnPad`. */ -class VehicleSpawnPadDefinition(objectId : Int) extends ObjectDefinition(objectId) { +class VehicleSpawnPadDefinition(objectId : Int) extends AmenityDefinition(objectId) { // Different pads require a Z offset to stop vehicles falling through the world after the pad rises from the floor, these values are found in game_objects.adb.lst private var vehicle_creation_z_offset = 0f diff --git a/common/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxDefinition.scala index 06ea10263..76bb9289f 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxDefinition.scala @@ -1,9 +1,9 @@ package net.psforever.objects.serverobject.painbox -import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.structures.AmenityDefinition import net.psforever.types.Vector3 -class PainboxDefinition(objectId : Int) extends ObjectDefinition(objectId) { +class PainboxDefinition(objectId : Int) extends AmenityDefinition(objectId) { private var alwaysOn : Boolean = true private var radius : Float = 0f private var damage : Int = 10 @@ -39,7 +39,7 @@ class PainboxDefinition(objectId : Int) extends ObjectDefinition(objectId) { radius = 8.55f sphereOffset = Vector3.Zero case _ => - throw new IllegalArgumentException(s"${objectId} is not a valid painbox object id") + throw new IllegalArgumentException(s"$objectId is not a valid painbox object id") } def Radius : Float = radius diff --git a/common/src/main/scala/net/psforever/objects/serverobject/repair/Repairable.scala b/common/src/main/scala/net/psforever/objects/serverobject/repair/Repairable.scala new file mode 100644 index 000000000..9c4c87477 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/repair/Repairable.scala @@ -0,0 +1,77 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.repair + +import akka.actor.Actor.Receive +import net.psforever.objects.equipment.Ammo +import net.psforever.objects.{GlobalDefinitions, Player, Tool} +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.vital.Vitality + +/** + * The base "control" `Actor` mixin for repair-handling code + * related to the nano dispenser tool loaded with an armor repair canister. + * Unlike the `Damageable` mixin, + * which should be extended to interact with all aspects of a target that impede access to its health points, + * shield, armor, etc., `Repairable` only affects `Vitality.Health`. + */ +trait Repairable { + /** + * Contextual access to the object being the target of this damage. + * Needs declaration in lowest implementing code. + * @return the entity controlled by this actor + */ + def RepairableObject : Repairable.Target + + /** + * The official mixin hook; `orElse` onto the "control" `Actor` `receive`; + * catch the expected repair message and apply initial checks to the item + * @see `Ammo` + * @see `CanBeRepairedByNanoDispenser` + * @see `CommonMessages.Use` + * @see `GlobalDefinitions` + * @see `Tool.AmmoType` + */ + final val canBeRepairedByNanoDispenser : Receive = { + case CommonMessages.Use(player, Some(item : Tool)) + if item.Definition == GlobalDefinitions.nano_dispenser && item.AmmoType == Ammo.armor_canister => + CanBeRepairedByNanoDispenser(player, item) + } + + /** + * Implementation of the mixin hook will be provided by a child class. + * Override this method only when directly implementing. + * @see `canBeRepairedByNanoDispenser` + */ + def CanBeRepairedByNanoDispenser(player : Player, item : Tool) : Unit + + /** + * The amount of repair that any specific tool provides. + * @see `Repairable.Quality` + * @param item the tool in question + * @return an amount to add to the repair attempt progress + */ + def RepairValue(item : Tool) : Int = 0 + + /** + * The entity is no longer destroyed. + * @param obj the entity + */ + def Restoration(obj : Repairable.Target) : Unit = { + Repairable.Restoration(obj) + } +} + +object Repairable { + /* the type of all entities governed by this mixin; see Damageable.Target */ + final type Target = PlanetSideServerObject with Vitality + /* the basic repair value; originally found on the `armor_canister` object definition */ + final val Quality : Int = 12 + + /** + * The entity is no longer destroyed. + * @param target the entity + */ + def Restoration(target : Repairable.Target) : Unit = { + target.Destroyed = false + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableAmenity.scala b/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableAmenity.scala new file mode 100644 index 000000000..8266d0e32 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableAmenity.scala @@ -0,0 +1,37 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.repair + +import net.psforever.objects.serverobject.structures.Amenity +import services.avatar.{AvatarAction, AvatarServiceMessage} + +/** + * The "control" `Actor` mixin for repair-handling code + * for the majority of `Repairable` `Amenity` objects installed in a facility or a field tower. + */ +trait RepairableAmenity extends RepairableEntity { + def RepairableObject : Amenity + + override def Restoration(obj : Repairable.Target) : Unit = { + super.Restoration(obj) + RepairableAmenity.Restoration(obj) + } +} + +object RepairableAmenity { + /** + * A resotred `Amenity` target dispatches two messages to chance its model and operational states. + * These `PlanetSideAttributeMessage` attributes are the same as reported during zone load client configuration. + * @see `AvatarAction.PlanetsideAttributeToAll` + * @see `AvatarServiceMessage` + * @see `Zone.AvatarEvents` + * @param target the entity being destroyed + */ + def Restoration(target : Repairable.Target) : Unit = { + val zone = target.Zone + val zoneId = zone.Id + val events = zone.AvatarEvents + val targetGUID = target.GUID + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 50, 0)) + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 51, 0)) + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala b/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala new file mode 100644 index 000000000..1724651e8 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala @@ -0,0 +1,108 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.repair + +import net.psforever.objects.{Player, Tool} +import net.psforever.packet.game.{InventoryStateMessage, RepairMessage} +import net.psforever.types.{PlanetSideEmpire, Vector3} +import services.Service +import services.avatar.{AvatarAction, AvatarServiceMessage} + +/** + * The "control" `Actor` mixin for repair-handling code, + * for both expansion into other mixins and specific application on its own. + * @see `Player` + * @see `Tool` + */ +trait RepairableEntity extends Repairable { + /** + * Catch the expected repair message and + * apply further checks to the combination of the target, the equipment, and tis user. + * If the checks pass, perform the repair. + * @param player the user of the nano dispenser tool + * @param item the nano dispenser tool + */ + def CanBeRepairedByNanoDispenser(player : Player, item : Tool) : Unit = { + val obj = RepairableObject + if(CanPerformRepairs(obj, player, item)) { + PerformRepairs(obj, player, item) + } + } + + /** + * Test the combination of target entity, equipment user, and the equipment + * to determine if the repair process attempt would be permitted. + * It is not necessary to check that the tool and its ammunition are correct types; + * that test was already performed.
+ *
+ * The target entity must be repairable and have less than full health + * and, if it is destroyed, must have an object attribute that permits it to be repaired after being destroyed.
+ * The user must have the same faction affinity as the target entity or be neutral.
+ * The equipment must have some ammunition.
+ * The user must be alive and be within a certain distance of the target entity. + * @see `org.log4s.getLogger` + * @see `PlanetSideEmpire` + * @see `Vector3.Distance` + * @see `VitalityDefinition` + * @param target the entity being repaired + * @param player the user of the nano dispenser tool + * @param item the nano dispenser tool + * @return `true`, if the target entity can be repaired; + * `false`, otherwise + */ + protected def CanPerformRepairs(target : Repairable.Target, player : Player, item : Tool) : Boolean = { + val definition = target.Definition + definition.Repairable && target.Health < definition.MaxHealth && (definition.RepairIfDestroyed || !target.Destroyed) && + (target.Faction == player.Faction || target.Faction == PlanetSideEmpire.NEUTRAL) && item.Magazine > 0 && + player.isAlive && Vector3.Distance(target.Position, player.Position) < definition.RepairDistance + } + + /** + * Calculate the health points change and enact that repair action if the targets are stationary. + * Restore the target entity to a not destroyed state if applicable. + * Always show the repair progress bar window by using the appropriate packet. + * @see `AvatarAction.PlanetsideAttributeToAll` + * @see `AvatarAction.SendResponse` + * @see `AvatarService` + * @see `InventoryStateMessage` + * @see `PlanetSideGameObject.isMoving` + * @see `RepairMessage` + * @see `Service.defaultPlayerGUID` + * @see `Tool.Discharge` + * @see `Zone.AvatarEvents` + * @param target the entity being repaired + * @param player the user of the nano dispenser tool + * @param item the nano dispenser tool + */ + protected def PerformRepairs(target : Repairable.Target, player : Player, item : Tool) : Unit = { + val definition = target.Definition + val zone = target.Zone + val events = zone.AvatarEvents + val name = player.Name + val tguid = target.GUID + val originalHealth = target.Health + val updatedHealth = if(!(player.isMoving || target.isMoving)) { //only allow stationary repairs + val newHealth = target.Health = originalHealth + Repairable.Quality + RepairValue(item) + definition.RepairMod + val zoneId = zone.Id + val magazine = item.Discharge + events ! AvatarServiceMessage(name, AvatarAction.SendResponse(Service.defaultPlayerGUID, InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong))) + if(target.Destroyed) { + if(newHealth >= definition.RepairRestoresAt) { + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 0, newHealth)) + Restoration(target) + } + } + else { + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 0, newHealth)) + } + newHealth + } + else { + originalHealth + } + //progress bar remains visible + events ! AvatarServiceMessage(name, AvatarAction.SendResponse(Service.defaultPlayerGUID, RepairMessage(tguid, updatedHealth * 100 / definition.MaxHealth))) + } + + /* random object repair modifier */ + override def RepairValue(item : Tool) : Int = item.FireMode.Modifiers.Damage1 +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableVehicle.scala b/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableVehicle.scala new file mode 100644 index 000000000..170337844 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableVehicle.scala @@ -0,0 +1,17 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.repair + +import net.psforever.objects.Tool + +/** + * The "control" `Actor` mixin for repair-handling code for `Vehicle` objects. + */ +trait RepairableVehicle extends RepairableEntity { + override def Restoration(obj : Repairable.Target) : Unit = { + obj.Health = 0 + obj.Destroyed = true + /* no vanilla vehicles are capable of being restored from destruction */ + /* if you wanted to properly restore a destroyed vehicle, the quickest way is an ObjectCreateMessage packet */ + /* additionally, the vehicle deconstruction task must be cancelled */ + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableWeaponTurret.scala b/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableWeaponTurret.scala new file mode 100644 index 000000000..d50a1e874 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/repair/RepairableWeaponTurret.scala @@ -0,0 +1,51 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.repair + +import net.psforever.objects.Tool +import net.psforever.objects.serverobject.turret.WeaponTurret +import net.psforever.objects.vehicles.MountedWeapons +import services.Service +import services.vehicle.{VehicleAction, VehicleServiceMessage} + +/** + * The "control" `Actor` mixin for repair-handling code for `WeaponTurret` objects. + */ +trait RepairableWeaponTurret extends RepairableEntity { + def RepairableObject : Repairable.Target with WeaponTurret + + override def Restoration(target : Repairable.Target) : Unit = { + super.Restoration(target) + RepairableWeaponTurret.Restoration(RepairableObject) + } +} + +object RepairableWeaponTurret { + /** + * A restored target dispatches messages to reconstruct the weapons that were previously mounted to the turret + * and may have been concealed/deleted when the target was destroyed. + * @see `MountedWeapons` + * @see `MountedWeapons.Weapons` + * @see `Service.defaultPlayerGUID` + * @see `WeaponTurret` + * @see `VehicleAction.EquipmentInSlot` + * @see `VehicleServiceMessage` + * @see `Zone.VehicleEvents` + * @param target the entity being destroyed; + * note: `MountedWeapons` is a parent of `WeaponTurret` + * but the handling code closely associates with the former + */ + def Restoration(target : Repairable.Target with MountedWeapons) : Unit = { + val zone = target.Zone + val zoneId = zone.Id + val tguid = target.GUID + val events = zone.VehicleEvents + target.Weapons + .map({ case (index, slot) => (index, slot.Equipment) }) + .collect { case (index, Some(tool : Tool)) => + events ! VehicleServiceMessage( + zoneId, + VehicleAction.EquipmentInSlot(Service.defaultPlayerGUID, tguid, index, tool) + ) + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSilo.scala b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSilo.scala index ac73f5191..315902bf9 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSilo.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSilo.scala @@ -2,7 +2,7 @@ package net.psforever.objects.serverobject.resourcesilo import akka.actor.{ActorContext, Props} -import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.{GlobalDefinitions, Player} import net.psforever.objects.serverobject.structures.Amenity import net.psforever.packet.game.UseItemMessage diff --git a/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala index 68287d67b..7a624187b 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala @@ -30,7 +30,14 @@ class ResourceSiloControl(resourceSilo : ResourceSilo) extends Actor with Factio def Processing : Receive = checkBehavior.orElse { case ResourceSilo.Use(player, msg) => - sender ! ResourceSilo.ResourceSiloMessage(player, msg, resourceSilo.Use(player, msg)) + if(resourceSilo.Faction == PlanetSideEmpire.NEUTRAL || player.Faction == resourceSilo.Faction) { + resourceSilo.Zone.Vehicles.find(v => v.PassengerInSeat(player).contains(0)) match { + case Some(vehicle) => + sender ! ResourceSilo.ResourceSiloMessage(player, msg, resourceSilo.Use(player, msg)) + case _ => + } + } + case ResourceSilo.LowNtuWarning(enabled: Boolean) => resourceSilo.LowNtuWarningOn = enabled log.trace(s"LowNtuWarning: Silo ${resourceSilo.GUID} low ntu warning set to $enabled") diff --git a/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala index 1f5570622..9169153d9 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala @@ -1,12 +1,12 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.resourcesilo -import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.structures.AmenityDefinition /** * The definition for any `Resource Silo`. * Object Id 731. */ -class ResourceSiloDefinition extends ObjectDefinition(731) { +class ResourceSiloDefinition extends AmenityDefinition(731) { Name = "resource_silo" } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala index 7a9ca07d4..a83c2fcbc 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala @@ -1,10 +1,12 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.structures +import net.psforever.objects.ballistics.ResolvedProjectile import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.vital.{DamageResistanceModel, StandardResistanceProfile, Vitality, VitalsActivity} import net.psforever.objects.zones.{Zone, ZoneAware} import net.psforever.types.{PlanetSideEmpire, Vector3} -import net.psforever.objects.zones.{ Zone => World } +import net.psforever.objects.zones.{Zone => World} /** * Amenities are elements of the game that belong to other elements of the game.
@@ -14,9 +16,13 @@ import net.psforever.objects.zones.{ Zone => World } * This association strips away at the internalization and redirects a reference to some properties somewhere else. * An `Amenity` object belongs to its `Owner` object; * the `Amenity` objects look to its `Owner` object for some of its properties. + * @see `AmenityOwner` * @see `FactionAffinity` */ -abstract class Amenity extends PlanetSideServerObject with ZoneAware { +abstract class Amenity extends PlanetSideServerObject + with Vitality + with ZoneAware + with StandardResistanceProfile { private[this] val log = org.log4s.getLogger("Amenity") /** what other entity has authority over this amenity; usually either a building or a vehicle */ private var owner : AmenityOwner = Building.NoBuilding @@ -71,4 +77,8 @@ abstract class Amenity extends PlanetSideServerObject with ZoneAware { } LocationOffset } + + def DamageModel = Definition.asInstanceOf[DamageResistanceModel] + + def Definition : AmenityDefinition } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/AmenityDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/AmenityDefinition.scala new file mode 100644 index 000000000..43df075fe --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/AmenityDefinition.scala @@ -0,0 +1,16 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.structures + +import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.vital.{DamageResistanceModel, StandardAmenityDamage, StandardAmenityResistance, StandardResolutions, VitalityDefinition} +import net.psforever.objects.vital.resistance.ResistanceProfileMutators + +abstract class AmenityDefinition(objectId : Int) extends ObjectDefinition(objectId) + with ResistanceProfileMutators + with DamageResistanceModel + with VitalityDefinition { + Name = "amenity" + DamageUsing = StandardAmenityDamage + ResistUsing = StandardAmenityResistance + Model = StandardResolutions.Amenities +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index 0bf1328e5..f68dc7e18 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -6,14 +6,15 @@ import java.util.concurrent.TimeUnit import akka.actor.{ActorContext, ActorRef} import net.psforever.objects.{GlobalDefinitions, Player} import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.painbox.Painbox import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.terminals.CaptureTerminal import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.zones.Zone -import net.psforever.packet.game._ -import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} +import net.psforever.packet.game.{Additional1, Additional2, Additional3} +import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3} import scalax.collection.{Graph, GraphEdge} import services.Service import services.local.{LocalAction, LocalServiceMessage} @@ -182,17 +183,17 @@ class Building(private val name: String, case _ => (false, PlanetSideEmpire.NEUTRAL, 0L) } - //TODO if we have a generator, get the current repair state - val (generatorState, boostGeneratorPain) = (PlanetSideGeneratorState.Normal, false) // todo: poll pain field strength + //if we have no generator, assume the state is "Normal" + val (generatorState, boostGeneratorPain) = Amenities.find(x => x.isInstanceOf[Generator]) match { + case Some(obj : Generator) => + (obj.Condition, false) // todo: poll pain field strength + case _ => + (PlanetSideGeneratorState.Normal, false) + } //if we have spawn tubes, determine if any of them are active val (spawnTubesNormal, boostSpawnPain) : (Boolean, Boolean) = { - val o = Amenities.collect({ case _ : SpawnTube => true }) ///TODO obj.Health > 0 - if(o.nonEmpty) { - (o.foldLeft(false)(_ || _), false) //TODO poll pain field strength - } - else { - (true, false) - } + val o = Amenities.collect({ case tube : SpawnTube if !tube.Destroyed => tube }) + (o.nonEmpty, false) //TODO poll pain field strength } val latticeBenefit : Int = { @@ -312,6 +313,7 @@ object Building { obj } + final case class AmenityStateChange(obj : Amenity) final case class SendMapUpdate(all_clients: Boolean) final case class TriggerZoneMapUpdate(zone_num: Int) } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala index 8bd88709e..7f06e1b02 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala @@ -3,6 +3,8 @@ package net.psforever.objects.serverobject.structures import akka.actor.{Actor, ActorRef} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.serverobject.generator.Generator +import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.zones.InterstellarCluster import net.psforever.packet.game.BuildingInfoUpdateMessage import services.ServiceManager @@ -35,43 +37,62 @@ class BuildingControl(building : Building) extends Actor with FactionAffinityBeh building.Amenities.foreach(_.Actor forward FactionAffinity.ConfirmFactionAffinity()) } sender ! FactionAffinity.AssertFactionAffinity(building, faction) - case Building.TriggerZoneMapUpdate(zone_num: Int) => - if(interstellarCluster != ActorRef.noSender) interstellarCluster ! InterstellarCluster.ZoneMapUpdate(zone_num) - case Building.SendMapUpdate(all_clients: Boolean) => - val zoneNumber = building.Zone.Number - val buildingNumber = building.MapId - log.trace(s"sending BuildingInfoUpdateMessage update - zone=$zoneNumber, building=$buildingNumber") - val ( - ntuLevel, - isHacked, empireHack, hackTimeRemaining, controllingEmpire, - unk1, unk1x, - generatorState, spawnTubesNormal, forceDomeActive, - latticeBenefit, cavernBenefit, - unk4, unk5, unk6, - unk7, unk7x, - boostSpawnPain, boostGeneratorPain - ) = building.Info - val msg = BuildingInfoUpdateMessage( - zoneNumber, - buildingNumber, - ntuLevel, - isHacked, empireHack, hackTimeRemaining, controllingEmpire, - unk1, unk1x, - generatorState, spawnTubesNormal, forceDomeActive, - latticeBenefit, cavernBenefit, - unk4, unk5, unk6, - unk7, unk7x, - boostSpawnPain, boostGeneratorPain - ) - if(all_clients) { - if(galaxyService != ActorRef.noSender) galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(msg)) - } else { - // Fake a GalaxyServiceResponse response back to just the sender - sender ! GalaxyServiceResponse("", GalaxyResponse.MapUpdate(msg)) + case Building.AmenityStateChange(obj : SpawnTube) => + if(building.Amenities.contains(obj)) { + SendMapUpdate(allClients = true) } - case default => - log.warn(s"BuildingControl: Unknown message $default received from ${sender().path}") + case Building.AmenityStateChange(obj : Generator) => + if(building.Amenities.contains(obj)) { + SendMapUpdate(allClients = true) + } + + case Building.TriggerZoneMapUpdate(zone_num: Int) => + if(interstellarCluster != ActorRef.noSender) interstellarCluster ! InterstellarCluster.ZoneMapUpdate(zone_num) + + case Building.SendMapUpdate(all_clients: Boolean) => + SendMapUpdate(all_clients) + + case _ => + } + + /** + * na + * @param allClients na + */ + def SendMapUpdate(allClients : Boolean) : Unit = { + val zoneNumber = building.Zone.Number + val buildingNumber = building.MapId + log.trace(s"sending BuildingInfoUpdateMessage update - zone=$zoneNumber, building=$buildingNumber") + val ( + ntuLevel, + isHacked, empireHack, hackTimeRemaining, controllingEmpire, + unk1, unk1x, + generatorState, spawnTubesNormal, forceDomeActive, + latticeBenefit, cavernBenefit, + unk4, unk5, unk6, + unk7, unk7x, + boostSpawnPain, boostGeneratorPain + ) = building.Info + val msg = BuildingInfoUpdateMessage( + zoneNumber, + buildingNumber, + ntuLevel, + isHacked, empireHack, hackTimeRemaining, controllingEmpire, + unk1, unk1x, + generatorState, spawnTubesNormal, forceDomeActive, + latticeBenefit, cavernBenefit, + unk4, unk5, unk6, + unk7, unk7x, + boostSpawnPain, boostGeneratorPain + ) + + if(allClients) { + if(galaxyService != ActorRef.noSender) galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(msg)) + } else { + // Fake a GalaxyServiceResponse response back to just the sender + sender ! GalaxyServiceResponse("", GalaxyResponse.MapUpdate(msg)) + } } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala index 9d7b196c5..9fb7e5189 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala @@ -6,8 +6,8 @@ import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.{GlobalDefinitions, SpawnPoint, SpawnPointDefinition} import net.psforever.objects.zones.Zone -import net.psforever.packet.game.{Additional1, Additional2, Additional3, PlanetSideGeneratorState} -import net.psforever.types.{PlanetSideEmpire, Vector3} +import net.psforever.packet.game.{Additional1, Additional2, Additional3} +import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState, Vector3} import scala.collection.mutable diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalControl.scala index 056f70cd5..249a8cabb 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalControl.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.terminals import akka.actor.Actor +import net.psforever.objects.{GlobalDefinitions, SimpleItem} import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.hackable.HackableBehavior @@ -14,6 +15,15 @@ class CaptureTerminalControl(terminal : CaptureTerminal) extends Actor with Fact def receive : Receive = checkBehavior .orElse(hackableBehavior) .orElse { - case _ => ; //no default message + case CommonMessages.Use(player, Some(item : SimpleItem)) if item.Definition == GlobalDefinitions.remote_electronics_kit => + val canHack = terminal.HackedBy match { + case Some(info) => info.hackerFaction != player.Faction + case _ => terminal.Faction != player.Faction + } + if(canHack) { + sender ! CommonMessages.Hack(player, terminal, Some(item)) + } + + case _ => ; //no default message } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalDefinition.scala index f65cf989e..901c72799 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminalDefinition.scala @@ -1,8 +1,8 @@ package net.psforever.objects.serverobject.terminals -import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.structures.AmenityDefinition -class CaptureTerminalDefinition(objectId : Int) extends ObjectDefinition(objectId) { +class CaptureTerminalDefinition(objectId : Int) extends AmenityDefinition(objectId) { Name = objectId match { case 158 => "capture_terminal" case 751 => "secondary_capture" diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminals.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminals.scala new file mode 100644 index 000000000..edb55e5a8 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CaptureTerminals.scala @@ -0,0 +1,40 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.terminals + +import net.psforever.objects.Player +import net.psforever.objects.serverobject.CommonMessages +import services.local.{LocalAction, LocalServiceMessage} + +import scala.util.{Failure, Success} + +object CaptureTerminals { + private val log = org.log4s.getLogger("CaptureTerminals") + + /** + * The process of hacking an object is completed. + * Pass the message onto the hackable object and onto the local events system. + * @param target the `Hackable` object that has been hacked + * @param unk na; + * used by `HackMessage` as `unk5` + * @see `HackMessage` + */ + //TODO add params here depending on which params in HackMessage are important + def FinishHackingCaptureConsole(target : CaptureTerminal, user : Player, unk : Long)() : Unit = { + import akka.pattern.ask + import scala.concurrent.duration._ + log.info(s"Hacked a $target") + // Wait for the target actor to set the HackedBy property, otherwise LocalAction.HackTemporarily will not complete properly + import scala.concurrent.ExecutionContext.Implicits.global + val tplayer = user + ask(target.Actor, CommonMessages.Hack(tplayer, target))(1 second).mapTo[Boolean].onComplete { + case Success(_) => + val zone = target.Zone + val zoneId = zone.Id + val pguid = tplayer.GUID + zone.LocalEvents ! LocalServiceMessage(zoneId, LocalAction.TriggerSound(pguid, target.HackSound, tplayer.Position, 30, 0.49803925f)) + zone.LocalEvents ! LocalServiceMessage(zoneId, LocalAction.HackCaptureTerminal(pguid, zone, target, unk, 8L, tplayer.Faction == target.Faction)) + case Failure(_) => log.warn(s"Hack message failed on target guid: ${target.GUID}") + } + } +} + diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala index 385ec627a..e7e960b6b 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala @@ -4,8 +4,11 @@ package net.psforever.objects.serverobject.terminals import akka.actor.{Actor, ActorRef, Cancellable} import net.psforever.objects._ import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior +import net.psforever.objects.serverobject.damage.DamageableAmenity import net.psforever.objects.serverobject.hackable.HackableBehavior +import net.psforever.objects.serverobject.repair.RepairableAmenity +import net.psforever.objects.serverobject.structures.Building import scala.collection.mutable import scala.concurrent.duration._ @@ -16,27 +19,42 @@ import scala.concurrent.duration._ * it returns the same type of messages - wrapped in a `TerminalMessage` - to the `sender`. * @param term the proximity unit (terminal) */ -class ProximityTerminalControl(term : Terminal with ProximityUnit) extends Actor with FactionAffinityBehavior.Check with HackableBehavior.GenericHackable { +class ProximityTerminalControl(term : Terminal with ProximityUnit) extends Actor + with FactionAffinityBehavior.Check + with HackableBehavior.GenericHackable + with DamageableAmenity + with RepairableAmenity { + def FactionObject = term + def HackableObject = term + def TerminalObject = term + def DamageableObject = term + def RepairableObject = term + var terminalAction : Cancellable = DefaultCancellable.obj val callbacks : mutable.ListBuffer[ActorRef] = new mutable.ListBuffer[ActorRef]() val log = org.log4s.getLogger - def FactionObject : FactionAffinity = term - def HackableObject = term - - def TerminalObject : Terminal with ProximityUnit = term - def receive : Receive = checkBehavior - .orElse(hackableBehavior) - .orElse { - case CommonMessages.Use(_, Some(target : PlanetSideGameObject)) => - if(term.Definition.asInstanceOf[ProximityDefinition].Validations.exists(p => p(target))) { - Use(target, term.Continent, sender) - } - case CommonMessages.Use(_, Some((target : PlanetSideGameObject, callback : ActorRef))) => - if(term.Definition.asInstanceOf[ProximityDefinition].Validations.exists(p => p(target))) { - Use(target, term.Continent, callback) - } + .orElse(hackableBehavior) + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) + .orElse { + case CommonMessages.Use(player, Some(item : SimpleItem)) if item.Definition == GlobalDefinitions.remote_electronics_kit => + //TODO setup certifications check + term.Owner match { + case b : Building if (b.Faction != player.Faction || b.CaptureConsoleIsHacked) && term.HackedBy.isEmpty => + sender ! CommonMessages.Hack(player, term, Some(item)) + case _ => ; + } + + case CommonMessages.Use(_, Some(target : PlanetSideGameObject)) => + if(!term.Destroyed && term.Definition.asInstanceOf[ProximityDefinition].Validations.exists(p => p(target))) { + Use(target, term.Continent, sender) + } + case CommonMessages.Use(_, Some((target : PlanetSideGameObject, callback : ActorRef))) => + if(!term.Destroyed && term.Definition.asInstanceOf[ProximityDefinition].Validations.exists(p => p(target))) { + Use(target, term.Continent, callback) + } case CommonMessages.Use(_, _) => log.warn(s"unexpected format for CommonMessages.Use in this context") @@ -52,7 +70,7 @@ class ProximityTerminalControl(term : Terminal with ProximityUnit) extends Actor val validateFunc : PlanetSideGameObject=>Boolean = term.Validate(proxDef.UseRadius * proxDef.UseRadius, proxDef.Validations) val callbackList = callbacks.toList term.Targets.zipWithIndex.foreach({ case(target, index) => - if(validateFunc(target)) { + if(!term.Destroyed && validateFunc(target)) { callbackList.lift(index) match { case Some(cback) => cback ! ProximityUnit.Action(term, target) diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala index c143665f5..c518b09cb 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala @@ -17,22 +17,20 @@ import net.psforever.types.{PlanetSideGUID, Vector3} * while `Vehicle`-owned terminals may not. * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ -class Terminal(tdef : TerminalDefinition) extends Amenity with Hackable { +class Terminal(tdef : TerminalDefinition) extends Amenity + with Hackable { HackSound = TriggeredSound.HackTerminal HackEffectDuration = Array(0, 30, 60, 90) HackDuration = Array(0, 10, 5, 3) //the following fields and related methods are neither finalized nor integrated; GOTO Request - private var health : Int = 100 //TODO not real health value - - def Health : Int = health def Damaged(dam : Int) : Unit = { - health = Math.max(0, Health - dam) + Health = Math.max(0, Health - dam) } def Repair(rep : Int) : Unit = { - health = Math.min(Health + rep, 100) + Health = Math.min(Health + rep, 100) } /** diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala index 4631182cc..4afbfb252 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala @@ -2,24 +2,43 @@ package net.psforever.objects.serverobject.terminals import akka.actor.Actor +import net.psforever.objects.{GlobalDefinitions, SimpleItem} import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior +import net.psforever.objects.serverobject.damage.DamageableAmenity import net.psforever.objects.serverobject.hackable.HackableBehavior +import net.psforever.objects.serverobject.repair.RepairableAmenity +import net.psforever.objects.serverobject.structures.Building /** * An `Actor` that handles messages being dispatched to a specific `Terminal`. * @param term the `Terminal` object being governed */ -class TerminalControl(term : Terminal) extends Actor with FactionAffinityBehavior.Check with HackableBehavior.GenericHackable { - def FactionObject : FactionAffinity = term +class TerminalControl(term : Terminal) extends Actor + with FactionAffinityBehavior.Check + with HackableBehavior.GenericHackable + with DamageableAmenity + with RepairableAmenity { + def FactionObject = term def HackableObject = term + def DamageableObject = term + def RepairableObject = term def receive : Receive = checkBehavior - .orElse(hackableBehavior) - .orElse { + .orElse(hackableBehavior) + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) + .orElse { case Terminal.Request(player, msg) => sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg)) + case CommonMessages.Use(player, Some(item : SimpleItem)) if item.Definition == GlobalDefinitions.remote_electronics_kit => + //TODO setup certifications check + term.Owner match { + case b : Building if (b.Faction != player.Faction || b.CaptureConsoleIsHacked) && term.HackedBy.isEmpty => + sender ! CommonMessages.Hack(player, term, Some(item)) + case _ => ; + } case _ => ; } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala index 5659e367b..10d017600 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala @@ -3,12 +3,13 @@ package net.psforever.objects.serverobject.terminals import net.psforever.objects.Player import net.psforever.objects.definition.converter.TerminalConverter +import net.psforever.objects.serverobject.structures.AmenityDefinition /** * The basic definition for any `Terminal` object. * @param objectId the object's identifier number */ -abstract class TerminalDefinition(objectId : Int) extends net.psforever.objects.definition.ObjectDefinition(objectId) { +abstract class TerminalDefinition(objectId : Int) extends AmenityDefinition(objectId) { Name = "terminal" Packet = new TerminalConverter diff --git a/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeControl.scala index 5b47ffe4c..6e7c53458 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeControl.scala @@ -2,16 +2,47 @@ package net.psforever.objects.serverobject.tube import akka.actor.Actor -import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.ballistics.ResolvedProjectile +import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior +import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.serverobject.damage.DamageableAmenity +import net.psforever.objects.serverobject.repair.{Repairable, RepairableAmenity} +import net.psforever.objects.serverobject.structures.Building /** * An `Actor` that handles messages being dispatched to a specific `SpawnTube`. * @param tube the `SpawnTube` object being governed */ -class SpawnTubeControl(tube : SpawnTube) extends Actor with FactionAffinityBehavior.Check { - def FactionObject : FactionAffinity = tube +class SpawnTubeControl(tube : SpawnTube) extends Actor + with FactionAffinityBehavior.Check + with DamageableAmenity + with RepairableAmenity { + def FactionObject = tube + def DamageableObject = tube + def RepairableObject = tube - def receive : Receive = checkBehavior.orElse { case _ =>; } + def receive : Receive = checkBehavior + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) + .orElse { + case _ => ; + } + + override protected def DestructionAwareness(target : Target, cause : ResolvedProjectile) : Unit = { + super.DestructionAwareness(target, cause) + tube.Owner match { + case b : Building => b.Actor ! Building.AmenityStateChange(tube) + case _ => ; + } + } + + override def Restoration(obj : Repairable.Target) : Unit = { + super.Restoration(obj) + tube.Owner match { + case b : Building => b.Actor ! Building.AmenityStateChange(tube) + case _ => ; + } + } override def toString : String = tube.Definition.Name } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeDefinition.scala index a78fb8dca..d96011b99 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/tube/SpawnTubeDefinition.scala @@ -3,15 +3,14 @@ package net.psforever.objects.serverobject.tube import akka.actor.ActorContext import net.psforever.objects.SpawnPointDefinition -import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.definition.converter.SpawnTubeConverter import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.objects.serverobject.structures.{Amenity, AmenityDefinition} /** * The definition for any spawn point in the game world. */ -class SpawnTubeDefinition(object_id : Int) extends ObjectDefinition(object_id) +class SpawnTubeDefinition(object_id : Int) extends AmenityDefinition(object_id) with SpawnPointDefinition { Packet = new SpawnTubeConverter } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala b/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala index a3b44afe7..b03053051 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala @@ -4,57 +4,14 @@ package net.psforever.objects.serverobject.turret import net.psforever.objects.equipment.JammableUnit import net.psforever.objects.serverobject.structures.Amenity import net.psforever.types.Vector3 -import net.psforever.objects.vital.{DamageResistanceModel, StandardResistanceProfile, Vitality} class FacilityTurret(tDef : FacilityTurretDefinition) extends Amenity with WeaponTurret - with JammableUnit - with Vitality - with StandardResistanceProfile { - /** some turrets can be updated; they all start without updates */ - private var upgradePath : TurretUpgrade.Value = TurretUpgrade.None - private var middleOfUpgrade : Boolean = false - + with JammableUnit { WeaponTurret.LoadDefinition(this) - override def Health_=(toHealth : Int) = super.Health_=(math.max(1, toHealth)) //TODO properly handle destroyed facility turrets - - def MaxHealth : Int = Definition.MaxHealth - def MountPoints : Map[Int, Int] = Definition.MountPoints.toMap - def Upgrade : TurretUpgrade.Value = upgradePath - - def Upgrade_=(upgrade : TurretUpgrade.Value) : TurretUpgrade.Value = { - middleOfUpgrade = true //blocking flag; block early - var updated = false - //upgrade each weapon as long as that weapon has a valid option for that upgrade - Definition.Weapons.foreach({ case(index, upgradePaths) => - if(upgradePaths.contains(upgrade)) { - updated = true - weapons(index).Equipment.get.asInstanceOf[TurretWeapon].Upgrade = upgrade - } - }) - if(updated) { - upgradePath = upgrade - } - else { - middleOfUpgrade = false //reset - } - Upgrade - } - - def ConfirmUpgrade(upgrade : TurretUpgrade.Value) : TurretUpgrade.Value = { - if(middleOfUpgrade && upgradePath == upgrade) { - middleOfUpgrade = false - } - upgradePath - } - - def isUpgrading : Boolean = middleOfUpgrade - - def DamageModel = Definition.asInstanceOf[DamageResistanceModel] - def Definition : FacilityTurretDefinition = tDef } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala index 5fedf16c8..feb2fb79a 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -1,20 +1,17 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.turret -import akka.actor.{Actor, Cancellable} -import net.psforever.objects.{DefaultCancellable, GlobalDefinitions, Player} +import akka.actor.Actor import net.psforever.objects.ballistics.ResolvedProjectile -import net.psforever.objects.equipment.{JammableMountedWeapons, JammableUnit} -import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} -import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} -import net.psforever.objects.vital.Vitality -import net.psforever.objects.zones.Zone -import net.psforever.types.PlanetSideGUID -import services.Service +import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.equipment.JammableMountedWeapons +import net.psforever.objects.serverobject.mount.MountableBehavior +import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior +import net.psforever.objects.serverobject.damage.DamageableWeaponTurret +import net.psforever.objects.serverobject.repair.Repairable.Target +import net.psforever.objects.serverobject.repair.RepairableWeaponTurret import services.avatar.{AvatarAction, AvatarServiceMessage} import services.local.{LocalAction, LocalServiceMessage} -import services.vehicle.{VehicleAction, VehicleServiceMessage} -import services.vehicle.support.TurretUpgrader import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -30,27 +27,30 @@ import scala.concurrent.duration._ */ class FacilityTurretControl(turret : FacilityTurret) extends Actor with FactionAffinityBehavior.Check + with MountableBehavior.TurretMount with MountableBehavior.Dismount + with DamageableWeaponTurret + with RepairableWeaponTurret with JammableMountedWeapons { - + def FactionObject = turret + def MountableObject = turret + def JammableObject = turret + def DamageableObject = turret + def RepairableObject = turret if(turret.Definition == GlobalDefinitions.vanu_sentry_turret) { // todo: Schedule this to start when weapon is discharged, not all the time context.system.scheduler.schedule(3 seconds, 200 milliseconds, self, FacilityTurret.RechargeAmmo()) } - def MountableObject = turret - - def JammableObject = turret - - def FactionObject : FactionAffinity = turret - def receive : Receive = checkBehavior .orElse(jammableBehavior) + .orElse(mountBehavior) .orElse(dismountBehavior) + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) .orElse { case FacilityTurret.RechargeAmmo() => val weapon = turret.ControlledWeapon(1).get.asInstanceOf[net.psforever.objects.Tool] - // recharge when last shot fired 3s delay, +1, 200ms interval if(weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) { weapon.Magazine += 1 @@ -60,117 +60,27 @@ class FacilityTurretControl(turret : FacilityTurret) extends Actor case _ => ; } } - case Mountable.TryMount(user, seat_num) => - turret.Seat(seat_num) match { - case Some(seat) => - if((!turret.Definition.FactionLocked || user.Faction == turret.Faction) && - (seat.Occupant = user).contains(user)) { - user.VehicleSeated = turret.GUID - sender ! Mountable.MountMessages(user, Mountable.CanMount(turret, seat_num)) - } - else { - sender ! Mountable.MountMessages(user, Mountable.CanNotMount(turret, seat_num)) - } - case None => - sender ! Mountable.MountMessages(user, Mountable.CanNotMount(turret, seat_num)) - } - - case Vitality.Damage(damage_func) => - if(turret.Health > 0) { - val originalHealth = turret.Health - val cause = damage_func(turret) - val health = turret.Health - val damageToHealth = originalHealth - health - FacilityTurretControl.HandleDamageResolution(turret, cause, damageToHealth) - if(damageToHealth > 0) { - val name = turret.Actor.toString - val slashPoint = name.lastIndexOf("/") - org.log4s.getLogger("DamageResolution").info(s"${name.substring(slashPoint + 1, name.length - 1)}: BEFORE=$originalHealth, AFTER=$health, CHANGE=$damageToHealth") - } - } case _ => ; } -} -object FacilityTurretControl { - def HandleDamageResolution(target : FacilityTurret, cause : ResolvedProjectile, damage : Int) : Unit = { - val zone = target.Zone - val targetGUID = target.GUID - val playerGUID = target.Zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match { - case Some(player) => player.GUID - case _ => targetGUID - } - val continentId = zone.Id - if(target.Health > 1) { - //alert occupants to damage source - if(damage > 0) { - zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) - //alert occupants to damage source - HandleDamageAwareness(target, playerGUID, cause) - } - if(cause.projectile.profile.JammerProjectile) { - target.Actor ! JammableUnit.Jammered(cause) - } - } - else { - //alert to vehicle death (hence, occupants' deaths) - HandleDestructionAwareness(target, playerGUID, cause) - } - zone.VehicleEvents ! VehicleServiceMessage(continentId, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, target.Health)) - } - - /** - * na - * @param target na - * @param attribution na - * @param lastShot na - */ - def HandleDamageAwareness(target : FacilityTurret, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = { + override protected def DestructionAwareness(target : Target, cause : ResolvedProjectile) : Unit = { + super.DestructionAwareness(target, cause) val zone = target.Zone val zoneId = zone.Id - //alert occupants to damage source - target.Seats.values.filter(seat => { - seat.isOccupied && seat.Occupant.get.isAlive - }).foreach(seat => { - val tplayer = seat.Occupant.get - zone.AvatarEvents ! AvatarServiceMessage(zoneId, AvatarAction.HitHint(attribution, tplayer.GUID)) - }) + val events = zone.AvatarEvents + val tguid = target.GUID + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 1)) + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 1)) } - /** - * na - * @param target na - * @param attribution na - * @param lastShot na - */ - def HandleDestructionAwareness(target : FacilityTurret, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = { - target.Actor ! JammableUnit.ClearJammeredSound() - target.Actor ! JammableUnit.ClearJammeredStatus() - val zone = target.Zone + override def Restoration(obj : Target) : Unit = { + super.Restoration(obj) + val zone = turret.Zone val zoneId = zone.Id - target.Seats.values.filter(seat => { - seat.isOccupied && seat.Occupant.get.isAlive - }).foreach(seat => { - val tplayer = seat.Occupant.get - tplayer.History(lastShot) - tplayer.Actor ! Player.Die() - }) - //turret wreckage has no weapons - // target.Weapons.values - // .filter { - // _.Equipment.nonEmpty - // } - // .foreach(slot => { - // val wep = slot.Equipment.get - // zone.AvatarEvents ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, wep.GUID)) - // }) - // zone.AvatarEvents ! AvatarServiceMessage(continentId, AvatarAction.Destroy(targetGUID, playerGUID, playerGUID, player.Position)) - target.Health = 1 //TODO turret "death" at 0, as is proper - zone.VehicleEvents ! VehicleServiceMessage(zoneId, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 0, target.Health)) //TODO not necessary - if(target.Upgrade != TurretUpgrade.None) { - zone.VehicleEvents ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.ClearSpecific(List(target), zone)) - zone.VehicleEvents ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.AddTask(target, zone, TurretUpgrade.None)) - } + val events = zone.AvatarEvents + val tguid = turret.GUID + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 0)) + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 0)) } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala index 43dd4db08..f4b1d203c 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretDefinition.scala @@ -1,17 +1,17 @@ // Copyright (c) 2019 PSForever package net.psforever.objects.serverobject.turret -import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.structures.AmenityDefinition import net.psforever.objects.vital.{StandardResolutions, StandardVehicleDamage, StandardVehicleResistance} /** * The definition for any `FacilityTurret`. * @param objectId the object's identifier number */ -class FacilityTurretDefinition(private val objectId : Int) extends ObjectDefinition(objectId) +class FacilityTurretDefinition(private val objectId : Int) extends AmenityDefinition(objectId) with TurretDefinition { - Damage = StandardVehicleDamage - Resistance = StandardVehicleResistance + DamageUsing = StandardVehicleDamage + ResistUsing = StandardVehicleResistance Model = StandardResolutions.FacilityTurrets } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala index d9a7eae2c..64ec3dc1f 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala @@ -15,8 +15,6 @@ trait TurretDefinition extends ResistanceProfileMutators with DamageResistanceModel { odef : ObjectDefinition => Turrets(odef.ObjectId) //let throw NoSuchElementException - - private var maxHealth : Int = 100 /* key - entry point index, value - seat index */ private val mountPoints : mutable.HashMap[Int, Int] = mutable.HashMap() /* key - seat number, value - hash map (below) */ @@ -29,13 +27,6 @@ trait TurretDefinition extends ResistanceProfileMutators * see `MannedTurret.TurretAmmoBox` for details */ private var hasReserveAmmunition : Boolean = false - def MaxHealth : Int = maxHealth - - def MaxHealth_=(health : Int) : Int = { - maxHealth = health - MaxHealth - } - def MountPoints : mutable.HashMap[Int, Int] = mountPoints def Weapons : mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weapons diff --git a/common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala b/common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala index 1117a40a4..cc3161322 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala @@ -1,8 +1,8 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.turret +import net.psforever.objects.{AmmoBox, PlanetSideGameObject, Player, Tool} import net.psforever.objects.definition.{AmmoBoxDefinition, SeatDefinition, ToolDefinition} -import net.psforever.objects._ import net.psforever.objects.equipment.{Equipment, EquipmentSlot} import net.psforever.objects.inventory.{Container, GridInventory} import net.psforever.objects.serverobject.affinity.FactionAffinity @@ -13,10 +13,8 @@ trait WeaponTurret extends FactionAffinity with Mountable with MountedWeapons with Container { - this : PlanetSideGameObject => + _ : PlanetSideGameObject => - private var health : Int = 1 - private var jammered : Boolean = false /** manned turrets have just one seat; this is just standard interface */ protected val seats : Map[Int, Chair] = Map(0 -> Chair(new SeatDefinition() { ControlledWeapon = Some(1) })) /** turrets have just one weapon; this is just standard interface */ @@ -24,21 +22,21 @@ trait WeaponTurret extends FactionAffinity /** may or may not have inaccessible inventory space * see `ReserveAmmunition` in the definition */ protected val inventory : GridInventory = new GridInventory() { - import net.psforever.types.PlanetSideGUID - override def Remove(index : Int) : Boolean = false override def Remove(guid : PlanetSideGUID) : Boolean = false } + /** some turrets can be updated; they all start without updates */ + private var upgradePath : TurretUpgrade.Value = TurretUpgrade.None + private var middleOfUpgrade : Boolean = false - def Health : Int = { - health - } - - def Health_=(toHealth : Int) : Int = { - health = toHealth - health - } + /* + do not mind what the IDE probably comments about these method prototypes for Health and MaxHealth + they do not override methods in Vitality, unless overrode in any class that implements this one + due to the inheritance requirement above, these statements are not required to be implemented or overrode ever + they are purely for class visibility + */ + def Health : Int def MaxHealth : Int @@ -81,13 +79,40 @@ trait WeaponTurret extends FactionAffinity } } - def Jammered : Boolean = jammered + def Upgrade : TurretUpgrade.Value = upgradePath - def Jammered_=(jamState : Boolean) : Boolean = { - jammered = jamState - Jammered + def Upgrade_=(upgrade : TurretUpgrade.Value) : TurretUpgrade.Value = { + middleOfUpgrade = true //blocking flag; block early + var updated = false + //upgrade each weapon as long as that weapon has a valid option for that upgrade + Definition match { + case definition : TurretDefinition => + definition.Weapons.foreach({ case(index, upgradePaths) => + if(upgradePaths.contains(upgrade)) { + updated = true + weapons(index).Equipment.get.asInstanceOf[TurretWeapon].Upgrade = upgrade + } + }) + case _ => ; + } + if(updated) { + upgradePath = upgrade + } + else { + middleOfUpgrade = false //reset + } + Upgrade } + def ConfirmUpgrade(upgrade : TurretUpgrade.Value) : TurretUpgrade.Value = { + if(middleOfUpgrade && upgradePath == upgrade) { + middleOfUpgrade = false + } + upgradePath + } + + def isUpgrading : Boolean = middleOfUpgrade + def Definition : TurretDefinition } @@ -110,8 +135,6 @@ object WeaponTurret { */ def LoadDefinition(turret : WeaponTurret, tdef : TurretDefinition) : WeaponTurret = { import net.psforever.objects.equipment.EquipmentSize.BaseTurretWeapon - //general stuff - turret.Health = tdef.MaxHealth //create weapons; note the class turret.weapons = tdef.Weapons.map({case (num, upgradePaths) => val slot = EquipmentSlot(BaseTurretWeapon) diff --git a/common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala b/common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala new file mode 100644 index 000000000..0cb89b04d --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala @@ -0,0 +1,50 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.turret + +import net.psforever.objects.{Player, Tool} +import net.psforever.packet.game.InventoryStateMessage +import services.Service +import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.vehicle.VehicleServiceMessage +import services.vehicle.support.TurretUpgrader + +object WeaponTurrets { + private val log = org.log4s.getLogger("WeaponTurrets") + + /** + * The process of upgrading a turret's weapon(s) is completed. + * Pass the message onto the turret and onto the vehicle events system. + * Additionally, force-deplete the ammunition count of the nano-dispenser used to perform the upgrade. + * @param target the turret + * @param tool the nano-dispenser that was used to perform this upgrade + * @param upgrade the new upgrade state + */ + def FinishUpgradingMannedTurret(target : FacilityTurret, user : Player, tool : Tool, upgrade : TurretUpgrade.Value)() : Unit = { + tool.Magazine = 0 + target.Zone.AvatarEvents ! AvatarServiceMessage( + user.Name, + AvatarAction.SendResponse(Service.defaultPlayerGUID, InventoryStateMessage(tool.AmmoSlot.Box.GUID, tool.GUID, 0)) + ) + FinishUpgradingMannedTurret(target, upgrade) + } + + /** + * The process of upgrading a turret's weapon(s) is completed. + * * Pass the message onto the turret and onto the vehicle events system. + * @see `FacilityTurret` + * @see `TurretUpgrade` + * @see `TurretUpgrader.AddTask` + * @see `TurretUpgrader.ClearSpecific` + * @see `VehicleServiceMessage.TurretUpgrade` + * @param target the facility turret being upgraded + * @param upgrade the upgrade being applied to the turret (usually, it's weapon system) + */ + def FinishUpgradingMannedTurret(target : FacilityTurret, upgrade : TurretUpgrade.Value) : Unit = { + log.info(s"Converting manned wall turret weapon to $upgrade") + val zone = target.Zone + val events = zone.VehicleEvents + events ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.ClearSpecific(List(target), zone)) + events ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.AddTask(target, zone, upgrade)) + } +} + diff --git a/common/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala b/common/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala new file mode 100644 index 000000000..689ffff6f --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala @@ -0,0 +1,423 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.vehicles + +import akka.actor.{Actor, Cancellable} +import net.psforever.objects.zones.Zone +import net.psforever.objects._ +import net.psforever.objects.vehicles.CargoBehavior.{CheckCargoDismount, CheckCargoMounting} +import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage} +import net.psforever.types.{CargoStatus, PlanetSideGUID, Vector3} +import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.{RemoverActor, Service} +import services.vehicle.{VehicleAction, VehicleServiceMessage} + +import scala.concurrent.duration._ + +trait CargoBehavior { + _ : Actor => + private var cargoMountTimer : Cancellable = DefaultCancellable.obj + private var cargoDismountTimer : Cancellable = DefaultCancellable.obj + + /* gate-keep mounting behavior so that unit does not try to dismount as cargo, or mount different vehicle */ + private var isMounting : Option[PlanetSideGUID] = None + /* gate-keep dismounting behavior so that unit does not try to mount as cargo, or dismount from different vehicle */ + private var isDismounting : Option[PlanetSideGUID] = None + + def CargoObject : Vehicle + + val cargoBehavior : Receive = { + case CheckCargoMounting(carrier_guid, mountPoint, iteration) => + val obj = CargoObject + if((isMounting.isEmpty || isMounting.contains(carrier_guid)) && isDismounting.isEmpty && + CargoBehavior.HandleCheckCargoMounting(obj.Zone, carrier_guid, obj.GUID, obj, mountPoint, iteration)) { + if(iteration == 0) { + //open the cargo bay door + obj.Zone.AvatarEvents ! AvatarServiceMessage( + obj.Zone.Id, + AvatarAction.SendResponse( + Service.defaultPlayerGUID, + CargoMountPointStatusMessage(carrier_guid, PlanetSideGUID(0), obj.GUID, PlanetSideGUID(0), mountPoint, CargoStatus.InProgress, 0) + ) + ) + } + isMounting = Some(carrier_guid) + import scala.concurrent.ExecutionContext.Implicits.global + cargoMountTimer.cancel + cargoMountTimer = context.system.scheduler.scheduleOnce(250 milliseconds, self, CheckCargoMounting(carrier_guid, mountPoint, iteration + 1)) + } + else { + isMounting = None + } + + case CheckCargoDismount(carrier_guid, mountPoint, iteration) => + val obj = CargoObject + if((isDismounting.isEmpty || isDismounting.contains(carrier_guid)) && isMounting.isEmpty && + CargoBehavior.HandleCheckCargoDismounting(obj.Zone, carrier_guid, obj.GUID, obj, mountPoint, iteration)) { + isDismounting = Some(carrier_guid) + import scala.concurrent.ExecutionContext.Implicits.global + cargoDismountTimer.cancel + cargoDismountTimer = context.system.scheduler.scheduleOnce(250 milliseconds, self, CheckCargoDismount(carrier_guid, mountPoint, iteration + 1)) + } + else { + isDismounting = None + } + } +} + +object CargoBehavior { + private val log = org.log4s.getLogger("CargoBehavior") + + final case class CheckCargoMounting(carrier_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int) + final case class CheckCargoDismount(carrier_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int) + + /** + * na + * @param carrierGUID the ferrying carrier vehicle + * @param cargoGUID the vehicle being ferried as cargo + * @param cargo the vehicle being ferried as cargo + * @param mountPoint the cargo hold to which the cargo vehicle is stowed + * @param iteration number of times a proper mounting for this combination has been queried + */ + def HandleCheckCargoMounting(zone : Zone, carrierGUID : PlanetSideGUID, cargoGUID : PlanetSideGUID, cargo : Vehicle, mountPoint : Int, iteration : Int) : Boolean = { + zone.GUID(carrierGUID) match { + case Some(carrier : Vehicle) => + HandleCheckCargoMounting(cargoGUID, cargo, carrierGUID, carrier, mountPoint, iteration) + case carrier if iteration > 0 => + log.error(s"HandleCheckCargoMounting: participant vehicles changed in the middle of a mounting event") + LogCargoEventMissingVehicleError("HandleCheckCargoMounting: carrier", carrier, carrierGUID) + false + case _ => + false + } + } + + /** + * na + * @param cargoGUID the vehicle being ferried as cargo + * @param cargo the vehicle being ferried as cargo + * @param carrierGUID the ferrying carrier vehicle + * @param carrier the ferrying carrier vehicle + * @param mountPoint the cargo hold to which the cargo vehicle is stowed + * @param iteration number of times a proper mounting for this combination has been queried + */ + private def HandleCheckCargoMounting(cargoGUID : PlanetSideGUID, cargo : Vehicle, carrierGUID : PlanetSideGUID, carrier : Vehicle, mountPoint : Int, iteration : Int) : Boolean = { + val zone = carrier.Zone + val distance = Vector3.DistanceSquared(cargo.Position, carrier.Position) + carrier.CargoHold(mountPoint) match { + case Some(hold) if !hold.isOccupied => + log.debug(s"HandleCheckCargoMounting: mount distance between $cargoGUID and $carrierGUID - actual=$distance, target=64") + if(distance <= 64) { + //cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it + log.info(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance") + cargo.MountedIn = carrierGUID + hold.Occupant = cargo + cargo.Velocity = None + zone.VehicleEvents ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health))) + zone.VehicleEvents ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))) + val (attachMsg, mountPointMsg) = CargoMountBehaviorForAll(carrier, cargo, mountPoint) + log.info(s"HandleCheckCargoMounting: $attachMsg") + log.info(s"HandleCheckCargoMounting: $mountPointMsg") + false + } + else if(distance > 625 || iteration >= 40) { + //vehicles moved too far away or took too long to get into proper position; abort mounting + log.info("HandleCheckCargoMounting: cargo vehicle is too far away or didn't mount within allocated time - aborting") + val cargoDriverGUID = cargo.Seats(0).Occupant.get.GUID + zone.VehicleEvents ! VehicleServiceMessage( + zone.Id, + VehicleAction.SendResponse( + cargoDriverGUID, + CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0) + ) + ) + false + //sending packet to the cargo vehicle's client results in player locking himself in his vehicle + //player gets stuck as "always trying to remount the cargo hold" + //obviously, don't do this + } + else { + //cargo vehicle still not in position but there is more time to wait; reschedule check + true + } + case None => ; + log.warn(s"HandleCheckCargoMounting: carrier vehicle $carrier does not have a cargo hold #$mountPoint") + false + case _ => + if(iteration == 0) { + log.warn(s"HandleCheckCargoMounting: carrier vehicle $carrier already possesses cargo in hold #$mountPoint; this operation was initiated incorrectly") + } + else { + log.error(s"HandleCheckCargoMounting: something has attached to the carrier vehicle $carrier cargo of hold #$mountPoint while a cargo dismount event was ongoing; stopped at iteration $iteration / 40") + } + false + } + } + + /** + * na + * @param cargoGUID na + * @param carrierGUID na + * @param mountPoint na + * @param iteration na + */ + def HandleCheckCargoDismounting(zone : Zone, carrierGUID : PlanetSideGUID, cargoGUID : PlanetSideGUID, cargo : Vehicle, mountPoint : Int, iteration : Int) : Boolean = { + zone.GUID(carrierGUID) match { + case Some(carrier : Vehicle) => + HandleCheckCargoDismounting(cargoGUID, cargo, carrierGUID, carrier, mountPoint, iteration) + case carrier if iteration > 0 => + log.error(s"HandleCheckCargoDismounting: participant vehicles changed in the middle of a mounting event") + LogCargoEventMissingVehicleError("HandleCheckCargoDismounting: carrier", carrier, carrierGUID) + false + case _ => + false + } + } + + /** + * na + * @param cargoGUID na + * @param cargo na + * @param carrierGUID na + * @param carrier na + * @param mountPoint na + * @param iteration na + */ + private def HandleCheckCargoDismounting(cargoGUID : PlanetSideGUID, cargo : Vehicle, carrierGUID : PlanetSideGUID, carrier : Vehicle, mountPoint : Int, iteration : Int) : Boolean = { + val zone = carrier.Zone + carrier.CargoHold(mountPoint) match { + case Some(hold) if !hold.isOccupied => + val distance = Vector3.DistanceSquared(cargo.Position, carrier.Position) + log.debug(s"HandleCheckCargoDismounting: mount distance between $cargoGUID and $carrierGUID - actual=$distance, target=225") + if(distance > 225) { + //cargo vehicle has moved far enough away; close the carrier's hold door + log.info(s"HandleCheckCargoDismounting: dismount of cargo vehicle from carrier complete at distance of $distance") + val cargoDriverGUID = cargo.Seats(0).Occupant.get.GUID + zone.VehicleEvents ! VehicleServiceMessage( + zone.Id, + VehicleAction.SendResponse( + cargoDriverGUID, + CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0) + ) + ) + false + //sending packet to the cargo vehicle's client results in player locking himself in his vehicle + //player gets stuck as "always trying to remount the cargo hold" + //obviously, don't do this + } + else if(iteration > 40) { + //cargo vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold + cargo.MountedIn = carrierGUID + hold.Occupant = cargo + CargoMountBehaviorForAll(carrier, cargo, mountPoint) + false + } + else { + //cargo vehicle did not move far away enough yet and there is more time to wait; reschedule check + true + } + case None => + log.warn(s"HandleCheckCargoDismounting: carrier vehicle $carrier does not have a cargo hold #$mountPoint") + false + case _ => + if(iteration == 0) { + log.warn(s"HandleCheckCargoDismounting: carrier vehicle $carrier will not discharge the cargo of hold #$mountPoint; this operation was initiated incorrectly") + } + else { + log.error(s"HandleCheckCargoDismounting: something has attached to the carrier vehicle $carrier cargo of hold #$mountPoint while a cargo dismount event was ongoing; stopped at iteration $iteration / 40") + } + false + } + } + + /** + * na + * @param player_guid na + * @param cargo_guid na + * @param bailed na + * @param requestedByPassenger na + * @param kicked na + */ + def HandleVehicleCargoDismount(zone : Zone, player_guid : PlanetSideGUID, cargo_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) : Unit = { + zone.GUID(cargo_guid) match { + case Some(cargo : Vehicle) => + zone.GUID(cargo.MountedIn) match { + case Some(ferry : Vehicle) => + HandleVehicleCargoDismount(player_guid, cargo_guid, cargo, ferry.GUID, ferry, bailed, requestedByPassenger, kicked) + case _ => + log.warn(s"DismountVehicleCargo: target ${cargo.Definition.Name}@$cargo_guid does not know what treats it as cargo") + } + case _ => + log.warn(s"DismountVehicleCargo: target $cargo_guid either is not a vehicle in ${zone.Id} or does not exist") + } + } + + /** + * na + * @param player_guid the target player + * @param cargoGUID the globally unique number for the vehicle being ferried + * @param cargo the vehicle being ferried + * @param carrierGUID the globally unique number for the vehicle doing the ferrying + * @param carrier the vehicle doing the ferrying + * @param bailed the ferried vehicle is bailing from the cargo hold + * @param requestedByPassenger the ferried vehicle is being politely disembarked from the cargo hold + * @param kicked the ferried vehicle is being kicked out of the cargo hold + */ + def HandleVehicleCargoDismount(player_guid : PlanetSideGUID, cargoGUID : PlanetSideGUID, cargo : Vehicle, carrierGUID : PlanetSideGUID, carrier : Vehicle, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) : Unit = { + val zone = carrier.Zone + carrier.CargoHolds.find({case(_, hold) => hold.Occupant.contains(cargo)}) match { + case Some((mountPoint, hold)) => + cargo.MountedIn = None + hold.Occupant = None + val driverOpt = cargo.Seats(0).Occupant + val rotation : Vector3 = if(Vehicles.CargoOrientation(cargo) == 1) { //TODO: BFRs will likely also need this set + //dismount router "sideways" in a lodestar + carrier.Orientation.xy + Vector3.z((carrier.Orientation.z - 90) % 360) + } + else { + carrier.Orientation + } + val cargoHoldPosition : Vector3 = if(carrier.Definition == GlobalDefinitions.dropship) { + //the galaxy cargo bay is offset backwards from the center of the vehicle + carrier.Position + Vector3.Rz(Vector3(0, 7, 0), math.toRadians(carrier.Orientation.z)) + } + else { + //the lodestar's cargo hold is almost the center of the vehicle + carrier.Position + } + zone.VehicleEvents ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health))) + zone.VehicleEvents ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))) + if(carrier.Flying) { + //the carrier vehicle is flying; eject the cargo vehicle + val ejectCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.InProgress, 0) + val detachCargoMsg = ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition - Vector3.z(1), rotation) + val resetCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0) + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(Service.defaultPlayerGUID, ejectCargoMsg)) + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(Service.defaultPlayerGUID, detachCargoMsg)) + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(Service.defaultPlayerGUID, resetCargoMsg)) + log.debug(ejectCargoMsg.toString) + log.debug(detachCargoMsg.toString) + if(driverOpt.isEmpty) { + //TODO cargo should drop like a rock like normal; until then, deconstruct it + zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(cargo), zone)) + zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(cargo, zone, Some(0 seconds))) + } + } + else { + //the carrier vehicle is not flying; just open the door and let the cargo vehicle back out; force it out if necessary + val cargoStatusMessage = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), cargoGUID, PlanetSideGUID(0), mountPoint, CargoStatus.InProgress, 0) + val cargoDetachMessage = ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition + Vector3.z(1f), rotation) + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(Service.defaultPlayerGUID, cargoStatusMessage)) + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(Service.defaultPlayerGUID, cargoDetachMessage)) + driverOpt match { + case Some(driver) => + zone.VehicleEvents ! VehicleServiceMessage(s"${driver.Name}", VehicleAction.KickCargo(player_guid, cargo, cargo.Definition.AutoPilotSpeed2, 2500)) + //check every quarter second if the vehicle has moved far enough away to be considered dismounted + cargo.Actor ! CheckCargoDismount(carrierGUID, mountPoint, 0) + case None => + val resetCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0) + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(PlanetSideGUID(0), resetCargoMsg)) //lazy + //TODO cargo should back out like normal; until then, deconstruct it + zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(cargo), zone)) + zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(cargo, zone, Some(0 seconds))) + } + } + + case None => + log.warn(s"HandleDismountVehicleCargo: can not locate cargo $cargo in any hold of the carrier vehicle $carrier") + } + } + + //logging and messaging support functions + /** + * na + * @param decorator custom text for these messages in the log + * @param target an optional the target object + * @param targetGUID the expected globally unique identifier of the target object + */ + def LogCargoEventMissingVehicleError(decorator : String, target : Option[PlanetSideGameObject], targetGUID : PlanetSideGUID) : Unit = { + target match { + case Some(_ : Vehicle) => ; + case Some(_) => log.error(s"$decorator target $targetGUID no longer identifies as a vehicle") + case None => log.error(s"$decorator target $targetGUID has gone missing") + } + } + + /** + * Produce an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + * that will set up a realized parent-child association between a ferrying vehicle and a ferried vehicle. + * @see `CargoMountPointStatusMessage` + * @see `ObjectAttachMessage` + * @see `Vehicles.CargoOrientation` + * @param carrier the ferrying vehicle + * @param cargo the ferried vehicle + * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached; + * also known as a "cargo hold" + * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + */ + def CargoMountMessages(carrier : Vehicle, cargo : Vehicle, mountPoint : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = { + CargoMountMessages(carrier.GUID, cargo.GUID, mountPoint, Vehicles.CargoOrientation(cargo)) + } + + /** + * Produce an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + * that will set up a realized parent-child association between a ferrying vehicle and a ferried vehicle. + * @see `CargoMountPointStatusMessage` + * @see `ObjectAttachMessage` + * @param carrierGUID the ferrying vehicle + * @param cargoGUID the ferried vehicle + * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached + * @param orientation the positioning of the cargo vehicle in the carrier cargo bay + * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + */ + def CargoMountMessages(carrierGUID : PlanetSideGUID, cargoGUID : PlanetSideGUID, mountPoint : Int, orientation : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = { + ( + ObjectAttachMessage(carrierGUID, cargoGUID, mountPoint), + CargoMountPointStatusMessage(carrierGUID, cargoGUID, cargoGUID, PlanetSideGUID(0), mountPoint, CargoStatus.Occupied, orientation) + ) + } + + /** + * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to all other clients, not this one. + * @see `CargoMountPointStatusMessage` + * @see `ObjectAttachMessage` + * @param carrier the ferrying vehicle + * @param cargo the ferried vehicle + * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached + * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + */ + def CargoMountBehaviorForOthers(carrier : Vehicle, cargo : Vehicle, mountPoint : Int, exclude : PlanetSideGUID) : (ObjectAttachMessage, CargoMountPointStatusMessage) = { + val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint) + CargoMountMessagesForOthers(carrier.Zone, exclude, attachMessage, mountPointStatusMessage) + msgs + } + + /** + * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to all other clients, not this one. + * @see `CargoMountPointStatusMessage` + * @see `ObjectAttachMessage` + * @param attachMessage an `ObjectAttachMessage` packet suitable for initializing cargo operations + * @param mountPointStatusMessage a `CargoMountPointStatusMessage` packet suitable for initializing cargo operations + */ + def CargoMountMessagesForOthers(zone : Zone, exclude : PlanetSideGUID, attachMessage : ObjectAttachMessage, mountPointStatusMessage : CargoMountPointStatusMessage) : Unit = { + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(exclude, attachMessage)) + zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.SendResponse(exclude, mountPointStatusMessage)) + } + + /** + * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to everyone. + * @see `CargoMountPointStatusMessage` + * @see `ObjectAttachMessage` + * @param carrier the ferrying vehicle + * @param cargo the ferried vehicle + * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached + * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet + */ + def CargoMountBehaviorForAll(carrier : Vehicle, cargo : Vehicle, mountPoint : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = { + val zone = carrier.Zone + val zoneId = zone.Id + val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint) + zone.VehicleEvents ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(Service.defaultPlayerGUID, attachMessage)) + zone.VehicleEvents ! VehicleServiceMessage(zoneId, VehicleAction.SendResponse(Service.defaultPlayerGUID, mountPointStatusMessage)) + msgs + } +} diff --git a/common/src/main/scala/net/psforever/objects/vehicles/Utility.scala b/common/src/main/scala/net/psforever/objects/vehicles/Utility.scala index 875be91f2..118290533 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/Utility.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/Utility.scala @@ -1,13 +1,16 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.vehicles -import akka.actor.ActorContext -import net.psforever.objects.definition.SimpleDeployableDefinition +import akka.actor.{ActorContext, ActorRef} +import net.psforever.objects.definition.BaseDeployableDefinition import net.psforever.objects._ -import net.psforever.objects.ce.TelepadLike -import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.objects.ce.{DeployedItem, TelepadLike} +import net.psforever.objects.definition.converter.SmallDeployableConverter +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.structures.{Amenity, AmenityDefinition} import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.tube.{SpawnTube, SpawnTubeDefinition} +import net.psforever.objects.vehicles.Utility.InternalTelepadDefinition import net.psforever.packet.game.ItemTransactionMessage import net.psforever.types.{PlanetSideGUID, Vector3} @@ -193,7 +196,8 @@ object Utility { * and allows it to serve as one of the terminal points of a Router-telepad teleportation system. * @param ddef na */ - class InternalTelepad(ddef : SimpleDeployableDefinition) extends Amenity + class InternalTelepad(ddef : InternalTelepadDefinition) extends Amenity + with UtilityWorldEntity with TelepadLike { /** a link to the telepad that serves as the other endpoint of this teleportation system */ private var activeTelepad : Option[PlanetSideGUID] = None @@ -215,6 +219,21 @@ object Utility { def Definition = ddef } + /** + * As the `InternalTelepad` object is a unique intersection of `Amenity` and `TelepadLike` + * that is treated like a `Deployable`, + * its definition must be a unique intersection of `AmenityDefinition` and `BaseDeployableDefinition`. + * @see `AmenityDefinition` + * @see `BaseDeployableDefinition` + * @see `DeployableDefinition` + */ + class InternalTelepadDefinition extends AmenityDefinition(DeployedItem.router_telepad_deployable.id) + with BaseDeployableDefinition { + Packet = new SmallDeployableConverter + + def Item : DeployedItem.Value = DeployedItem.router_telepad_deployable + } + /** * Provide the called-out object's logic. * @param util the type of the `Amenity` object @@ -241,3 +260,15 @@ object Utility { TelepadLike.Setup } } + +object InternalTelepadDefinition { + def apply() : InternalTelepadDefinition = + new InternalTelepadDefinition() + + def SimpleUninitialize(obj : PlanetSideGameObject, context : ActorContext) : Unit = { } + + def SimpleUninitialize(obj : PlanetSideServerObject, context : ActorContext) : Unit = { + context.stop(obj.Actor) + obj.Actor = ActorRef.noSender + } +} diff --git a/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala index 4451b8c5c..4683d3a64 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -2,19 +2,18 @@ package net.psforever.objects.vehicles import akka.actor.{Actor, ActorRef} -import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.{GlobalDefinitions, SimpleItem, Vehicle} import net.psforever.objects.ballistics.{ResolvedProjectile, VehicleSource} -import net.psforever.objects.equipment.{JammableMountedWeapons, JammableUnit} +import net.psforever.objects.equipment.JammableMountedWeapons +import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} -import net.psforever.objects.serverobject.deploy.{Deployment, DeploymentBehavior} -import net.psforever.objects.vital.{VehicleShieldCharge, Vitality} -import net.psforever.objects.zones.Zone -import net.psforever.types.{DriveState, ExoSuitType, PlanetSideGUID, Vector3} -import services.{RemoverActor, Service} -import services.avatar.{AvatarAction, AvatarServiceMessage} -import services.local.{LocalAction, LocalServiceMessage} -import services.vehicle.{VehicleAction, VehicleService, VehicleServiceMessage} +import net.psforever.objects.serverobject.damage.DamageableVehicle +import net.psforever.objects.serverobject.deploy.DeploymentBehavior +import net.psforever.objects.serverobject.repair.RepairableVehicle +import net.psforever.objects.vital.VehicleShieldCharge +import net.psforever.types.{ExoSuitType, PlanetSideGUID} +import services.vehicle.{VehicleAction, VehicleServiceMessage} /** * An `Actor` that handles messages being dispatched to a specific `Vehicle`.
@@ -28,32 +27,38 @@ class VehicleControl(vehicle : Vehicle) extends Actor with DeploymentBehavior with MountableBehavior.Mount with MountableBehavior.Dismount + with CargoBehavior + with DamageableVehicle + with RepairableVehicle with JammableMountedWeapons { //make control actors belonging to utilities when making control actor belonging to vehicle vehicle.Utilities.foreach({case (_, util) => util.Setup }) def MountableObject = vehicle - + def CargoObject = vehicle def JammableObject = vehicle - def FactionObject = vehicle - def DeploymentObject = vehicle + def DamageableObject = vehicle + def RepairableObject = vehicle def receive : Receive = Enabled override def postStop() : Unit = { super.postStop() vehicle.Utilities.values.foreach { util => - util().Actor ! akka.actor.PoisonPill + context.stop(util().Actor) util().Actor = ActorRef.noSender } } def Enabled : Receive = checkBehavior .orElse(deployBehavior) + .orElse(cargoBehavior) .orElse(jammableBehavior) + .orElse(takesDamage) + .orElse(canBeRepairedByNanoDispenser) .orElse { case msg : Mountable.TryMount => tryMountBehavior.apply(msg) @@ -61,23 +66,6 @@ class VehicleControl(vehicle : Vehicle) extends Actor case msg : Mountable.TryDismount => dismountBehavior.apply(msg) - case Vitality.Damage(damage_func) => - if(vehicle.Health > 0) { - val originalHealth = vehicle.Health - val originalShields = vehicle.Shields - val cause = damage_func(vehicle) - val health = vehicle.Health - val shields = vehicle.Shields - val damageToHealth = originalHealth - health - val damageToShields = originalShields - shields - VehicleControl.HandleDamageResolution(vehicle, cause, damageToHealth + damageToShields) - if(damageToHealth > 0 || damageToShields > 0) { - val name = vehicle.Actor.toString - val slashPoint = name.lastIndexOf("/") - org.log4s.getLogger("DamageResolution").info(s"${name.substring(slashPoint + 1, name.length - 1)}: BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields") - } - } - case Vehicle.ChargeShields(amount) => val now : Long = System.nanoTime //make certain vehicle doesn't charge shields too quickly @@ -95,6 +83,12 @@ class VehicleControl(vehicle : Vehicle) extends Actor } sender ! FactionAffinity.AssertFactionAffinity(vehicle, faction) + case CommonMessages.Use(player, Some(item : SimpleItem)) if item.Definition == GlobalDefinitions.remote_electronics_kit => + //TODO setup certifications check + if(vehicle.Faction != player.Faction) { + sender ! CommonMessages.Hack(player, vehicle, Some(item)) + } + case Vehicle.PrepareForDeletion() => CancelJammeredSound(vehicle) CancelJammeredStatus(vehicle) @@ -139,6 +133,12 @@ class VehicleControl(vehicle : Vehicle) extends Actor case _ => } + + override def TryJammerEffectActivate(target : Any, cause : ResolvedProjectile) : Unit = { + if(vehicle.MountedIn.isEmpty) { + super.TryJammerEffectActivate(target, cause) + } + } } object VehicleControl { @@ -160,115 +160,4 @@ object VehicleControl { case _ => false } } - - /** - * na - * @param target na - */ - def HandleDamageResolution(target : Vehicle, cause : ResolvedProjectile, damage : Int) : Unit = { - val zone = target.Zone - val targetGUID = target.GUID - val playerGUID = zone.LivePlayers.find { p => cause.projectile.owner.Name.equals(p.Name) } match { - case Some(player) => player.GUID - case _ => PlanetSideGUID(0) - } - if(target.Health > 0) { - //activity on map - if(damage > 0) { - zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) - //alert occupants to damage source - HandleDamageAwareness(target, playerGUID, cause) - } - if(cause.projectile.profile.JammerProjectile) { - target.Actor ! JammableUnit.Jammered(cause) - } - } - else { - //alert to vehicle death (hence, occupants' deaths) - HandleDestructionAwareness(target, playerGUID, cause) - } - zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, target.Health)) - zone.VehicleEvents ! VehicleServiceMessage(s"${target.Actor}", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 68, target.Shields)) - } - - /** - * na - * @param target na - * @param attribution na - * @param lastShot na - */ - def HandleDamageAwareness(target : Vehicle, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = { - val zone = target.Zone - //alert occupants to damage source - target.Seats.values.filter(seat => { - seat.isOccupied && seat.Occupant.get.isAlive - }).foreach(seat => { - val tplayer = seat.Occupant.get - zone.AvatarEvents ! AvatarServiceMessage(tplayer.Name, AvatarAction.HitHint(attribution, tplayer.GUID)) - }) - //alert cargo occupants to damage source - target.CargoHolds.values.foreach(hold => { - hold.Occupant match { - case Some(cargo) => - cargo.Health = 0 - cargo.Shields = 0 - cargo.History(lastShot) - HandleDamageAwareness(cargo, attribution, lastShot) - case None => ; - } - }) - } - - /** - * na - * @param target na - * @param attribution na - * @param lastShot na - */ - def HandleDestructionAwareness(target : Vehicle, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = { - target.Actor ! JammableUnit.ClearJammeredSound() - target.Actor ! JammableUnit.ClearJammeredStatus() - val zone = target.Zone - val continentId = zone.Id - //alert to vehicle death (hence, occupants' deaths) - target.Seats.values.filter(seat => { - seat.isOccupied && seat.Occupant.get.isAlive - }).foreach(seat => { - val tplayer = seat.Occupant.get - tplayer.History(lastShot) - tplayer.Actor ! Player.Die() - }) - //vehicle wreckage has no weapons - target.Weapons.values - .filter { - _.Equipment.nonEmpty - } - .foreach(slot => { - val wep = slot.Equipment.get - zone.AvatarEvents ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, wep.GUID)) - }) - target.CargoHolds.values.foreach(hold => { - hold.Occupant match { - case Some(cargo) => - cargo.Health = 0 - cargo.Shields = 0 - cargo.Position += Vector3.z(1) - cargo.History(lastShot) //necessary to kill cargo vehicle occupants //TODO: collision damage - HandleDestructionAwareness(cargo, attribution, lastShot) //might cause redundant packets - case None => ; - } - }) - target.Definition match { - case GlobalDefinitions.ams => - target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying) - case GlobalDefinitions.router => - target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying) - VehicleService.BeforeUnloadVehicle(target, zone) - zone.LocalEvents ! LocalServiceMessage(zone.Id, LocalAction.ToggleTeleportSystem(PlanetSideGUID(0), target, None)) - case _ => ; - } - zone.AvatarEvents ! AvatarServiceMessage(continentId, AvatarAction.Destroy(target.GUID, attribution, attribution, target.Position)) - zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), zone)) - zone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, zone, Some(1 minute))) - } } diff --git a/common/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala b/common/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala index 92d9b9897..791882ff3 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala @@ -11,19 +11,11 @@ import net.psforever.objects.zones.Zone * after it has been loaded to a new location or to a new zone; * this channel name should be unique to the vehicle for at least the duration of the transition; * the vehicle-specific channel with which all passengers are coordinated back to the original vehicle - * @param vehicle na - * @param origin na - * @param driverName na - * @param passengers na - * @param cargo na - */ -/** - * The channel name for summoning passengers to the vehicle - * after it has been loaded to a new location or to a new zone. - * This channel name should be unique to the vehicle for at least the duration of the transition. - * The vehicle-specific channel with which all passengers are coordinated back to the original vehicle. - * @param vehicle the vehicle being moved (or having been moved) - * @return the channel name + * @param vehicle the vehicle in transport + * @param origin where the vehicle originally was + * @param driverName the name of the driver when the transport process started + * @param passengers the paired names and seat indices of all passengers when the transport process started + * @param cargo the paired driver names and cargo hold indices of all cargo vehicles when the transport process started */ final case class VehicleManifest(file : String, vehicle : Vehicle, @@ -55,4 +47,4 @@ object VehicleManifest { def ManifestChannelName(vehicle : Vehicle) : String = { s"transport-vehicle-channel-${vehicle.GUID.guid}" } -} \ No newline at end of file +} diff --git a/common/src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala b/common/src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala index 7d98bc7ff..beb1134ba 100644 --- a/common/src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala +++ b/common/src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala @@ -26,26 +26,26 @@ import net.psforever.objects.vital.resolution.ResolutionCalculations */ trait DamageResistanceModel { /** the functionality that processes damage; required */ - private var damage : DamageSelection = NoDamageSelection + private var damageUsing : DamageSelection = NoDamageSelection /** the functionality that processes resistance; optional */ - private var resistance : ResistanceSelection = NoResistanceSelection + private var resistUsing : ResistanceSelection = NoResistanceSelection /** the functionality that prepares for damage application actions; required */ private var model : ResolutionCalculations.Form = NoResolutions.Calculate - def Damage : DamageSelection = damage + def DamageUsing : DamageSelection = damageUsing - def Damage_=(selector : DamageSelection) : DamageSelection = { - damage = selector - Damage + def DamageUsing_=(selector : DamageSelection) : DamageSelection = { + damageUsing = selector + DamageUsing } - def Resistance : ResistanceSelection = resistance + def ResistUsing : ResistanceSelection = resistUsing - def Resistance_=(selector : ResistanceSelection) : ResistanceSelection = { - resistance = selector - Resistance + def ResistUsing_=(selector : ResistanceSelection) : ResistanceSelection = { + resistUsing = selector + ResistUsing } def Model : ResolutionCalculations.Form = model @@ -61,8 +61,8 @@ trait DamageResistanceModel { * @return a function literal that encapsulates delayed modification instructions for certain objects */ def Calculate(data : ResolvedProjectile) : ResolutionCalculations.Output = { - val dam : ProjectileCalculations.Form = Damage(data) - val res : ProjectileCalculations.Form = Resistance(data) + val dam : ProjectileCalculations.Form = DamageUsing(data) + val res : ProjectileCalculations.Form = ResistUsing(data) Model(dam, res, data) } @@ -73,8 +73,8 @@ trait DamageResistanceModel { * @return a function literal that encapsulates delayed modification instructions for certain objects */ def Calculate(data : ResolvedProjectile, resolution : ProjectileResolution.Value) : ResolutionCalculations.Output = { - val dam : ProjectileCalculations.Form = Damage(resolution) - val res : ProjectileCalculations.Form = Resistance(resolution) + val dam : ProjectileCalculations.Form = DamageUsing(resolution) + val res : ProjectileCalculations.Form = ResistUsing(resolution) Model(dam, res, data) } } diff --git a/common/src/main/scala/net/psforever/objects/vital/StandardDamages.scala b/common/src/main/scala/net/psforever/objects/vital/StandardDamages.scala index 9d5ee0dcd..043064e49 100644 --- a/common/src/main/scala/net/psforever/objects/vital/StandardDamages.scala +++ b/common/src/main/scala/net/psforever/objects/vital/StandardDamages.scala @@ -95,6 +95,18 @@ object AircraftLashDamage extends DamageCalculations( DistanceBetweenTargetandSource ) +object AmenityHitDamage extends DamageCalculations( + DirectHitDamageWithDegrade, + DamageWithModifiers(DamageAgainstVehicle), + DistanceBetweenTargetandSource +) + +object AmenitySplashDamage extends DamageCalculations( + SplashDamageWithRadialDegrade, + DamageWithModifiers(DamageAgainstVehicle), + DistanceFromExplosionToTarget +) + object NoDamageSelection extends DamageSelection { def Direct = None def Splash = None @@ -130,3 +142,9 @@ object StandardDeployableDamage extends DamageSelection { def Splash = VehicleSplashDamage.Calculate def Lash = NoDamage.Calculate } + +object StandardAmenityDamage extends DamageSelection { + def Direct = AmenityHitDamage.Calculate + def Splash = AmenitySplashDamage.Calculate + def Lash = NoDamage.Calculate +} diff --git a/common/src/main/scala/net/psforever/objects/vital/StandardResistances.scala b/common/src/main/scala/net/psforever/objects/vital/StandardResistances.scala index 2c398c203..090f1f9f4 100644 --- a/common/src/main/scala/net/psforever/objects/vital/StandardResistances.scala +++ b/common/src/main/scala/net/psforever/objects/vital/StandardResistances.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.vital -import net.psforever.objects.ballistics.{PlayerSource, SourceEntry, VehicleSource} +import net.psforever.objects.ballistics.{ObjectSource, PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.vital.projectile.ProjectileCalculations import net.psforever.objects.vital.resistance.{ResistanceCalculations, ResistanceSelection} @@ -50,6 +50,16 @@ object VehicleAggravatedResistance extends ResistanceCalculations[VehicleSource] ResistanceCalculations.VehicleAggravatedExtractor ) +object AmenityHitResistance extends ResistanceCalculations[ObjectSource]( + ResistanceCalculations.ValidAmenityTarget, + ResistanceCalculations.OtherDirectExtractor +) + +object AMenitySplashResistance extends ResistanceCalculations[ObjectSource]( + ResistanceCalculations.ValidAmenityTarget, + ResistanceCalculations.OtherSplashExtractor +) + object NoResistanceSelection extends ResistanceSelection { def Direct : ProjectileCalculations.Form = None def Splash : ProjectileCalculations.Form = None @@ -70,3 +80,10 @@ object StandardVehicleResistance extends ResistanceSelection { def Lash : ProjectileCalculations.Form = VehicleLashResistance.Calculate def Aggravated : ProjectileCalculations.Form = VehicleAggravatedResistance.Calculate } + +object StandardAmenityResistance extends ResistanceSelection { + def Direct : ProjectileCalculations.Form = AmenityHitResistance.Calculate + def Splash : ProjectileCalculations.Form = AmenityHitResistance.Calculate + def Lash : ProjectileCalculations.Form = None + def Aggravated : ProjectileCalculations.Form = None +} diff --git a/common/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala b/common/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala index 75502f7d3..ade516d3b 100644 --- a/common/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala +++ b/common/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala @@ -41,4 +41,5 @@ object StandardResolutions extends ResolutionSelection { def SimpleDeployables : ResolutionCalculations.Form = SimpleResolutions.Calculate def ComplexDeployables : ResolutionCalculations.Form = ComplexDeployableResolutions.Calculate def FacilityTurrets : ResolutionCalculations.Form = SimpleResolutions.Calculate + def Amenities : ResolutionCalculations.Form = SimpleResolutions.Calculate } diff --git a/common/src/main/scala/net/psforever/objects/vital/Vitality.scala b/common/src/main/scala/net/psforever/objects/vital/Vitality.scala index 5cf22e3a2..ac4e2815d 100644 --- a/common/src/main/scala/net/psforever/objects/vital/Vitality.scala +++ b/common/src/main/scala/net/psforever/objects/vital/Vitality.scala @@ -1,101 +1,48 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.vital -import net.psforever.objects.PlanetSideGameObject -import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile, SourceEntry, VehicleSource} -import net.psforever.objects.definition.KitDefinition -import net.psforever.objects.serverobject.painbox.Painbox -import net.psforever.objects.serverobject.terminals.TerminalDefinition +import net.psforever.objects.ballistics.ResolvedProjectile import net.psforever.objects.vital.resolution.ResolutionCalculations -import net.psforever.types.{ExoSuitType, ImplantType} - -abstract class VitalsActivity(target : SourceEntry) { - def Target : SourceEntry = target - val t : Long = System.nanoTime //??? - - def time : Long = t -} - -abstract class HealingActivity(target : SourceEntry) extends VitalsActivity(target) - -abstract class DamagingActivity(target : SourceEntry) extends VitalsActivity(target) - -final case class HealFromKit(target : PlayerSource, amount : Int, kit_def : KitDefinition) extends HealingActivity(target) - -final case class HealFromTerm(target : PlayerSource, health : Int, armor : Int, term_def : TerminalDefinition) extends HealingActivity(target) - -final case class HealFromImplant(target : PlayerSource, amount : Int, implant : ImplantType.Value) extends HealingActivity(target) - -final case class HealFromExoSuitChange(target : PlayerSource, exosuit : ExoSuitType.Value) extends HealingActivity(target) - -final case class RepairFromKit(target : PlayerSource, amount : Int, kit_def : KitDefinition) extends HealingActivity(target) - -final case class RepairFromTerm(target : VehicleSource, amount : Int, term_def : TerminalDefinition) extends HealingActivity(target) - -final case class VehicleShieldCharge(target : VehicleSource, amount : Int) extends HealingActivity(target) //TODO facility - -final case class DamageFromProjectile(data : ResolvedProjectile) extends DamagingActivity(data.target) - -final case class DamageFromPainbox(target : PlayerSource, painbox : Painbox, damage : Int) extends DamagingActivity(target) - -final case class PlayerSuicide(target : PlayerSource) extends DamagingActivity(target) /** * A vital object can be hurt or damaged or healed or repaired (HDHR). * The amount of HDHR is controlled by the damage model of this vital object reacting to stimulus. - * A history of the previous changes in vital statistics of the underlying object is recorded - * in reverse chronological order. - * The damage model is also provided. + * The damage model is provided. */ -trait Vitality { - this : PlanetSideGameObject => +trait Vitality extends VitalsHistory { + private var health : Int = Definition.DefaultHealth + private var defaultHealth : Option[Int] = None + private var maxHealth : Option[Int] = None - /** a reverse-order list of chronological events that have occurred to these vital statistics */ - private var vitalHistory : List[VitalsActivity] = List.empty[VitalsActivity] + def Health : Int = health - def History : List[VitalsActivity] = vitalHistory - - /** - * A `VitalsActivity` event must be recorded. - * Add new entry to the front of the list (for recent activity). - * @param action the fully-informed entry - * @return the list of previous changes to this object's vital statistics - */ - def History(action : VitalsActivity) : List[VitalsActivity] = { - vitalHistory = action +: vitalHistory - vitalHistory + def Health_=(assignHealth : Int) : Int = { + health = math.min(math.max(0, assignHealth), MaxHealth) + Health } - /** - * Very common example of a `VitalsActivity` event involving weapon discharge. - * @param projectile the fully-informed entry of discharge of a weapon - * @return the list of previous changes to this object's vital statistics - */ - def History(projectile : ResolvedProjectile) : List[VitalsActivity] = { - vitalHistory = DamageFromProjectile(projectile) +: vitalHistory - vitalHistory + def DefaultHealth : Int = defaultHealth.getOrElse(Definition.DefaultHealth) + + def MaxHealth : Int = maxHealth.getOrElse(Definition.MaxHealth) + + def MaxHealth_=(default : Int) : Int = MaxHealth_=(Some(default)) + + def MaxHealth_=(default : Option[Int]) : Int = { + maxHealth = default + MaxHealth } - /** - * Find, specifically, the last instance of a weapon discharge vital statistics change. - * @return information about the discharge - */ - def LastShot : Option[ResolvedProjectile] = { - vitalHistory.find({p => p.isInstanceOf[DamageFromProjectile]}) match { - case Some(entry : DamageFromProjectile) => - Some(entry.data) - case _ => - None - } + def CanDamage : Boolean = { + Definition.Damageable && Health > 0 } - def ClearHistory() : List[VitalsActivity] = { - val out = vitalHistory - vitalHistory = List.empty[VitalsActivity] - out + def CanRepair : Boolean = { + Definition.Repairable && Health < MaxHealth && (Health > 0 || Definition.RepairIfDestroyed) } def DamageModel : DamageResistanceModel + + def Definition : VitalityDefinition } object Vitality { diff --git a/common/src/main/scala/net/psforever/objects/vital/VitalityDefinition.scala b/common/src/main/scala/net/psforever/objects/vital/VitalityDefinition.scala new file mode 100644 index 000000000..c0c74e902 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/vital/VitalityDefinition.scala @@ -0,0 +1,131 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.vital + +/** + * na
+ *
+ * The expected (but not enforced) relationship between values follows: + * `0 <= DamageDestroysAt <= DamageDisablesAt < RepairRestoresAt <= MaxHealth`. + */ +trait VitalityDefinition { + /** the maximum amount of health that any of the objects can be allocated; + * corresponds to ADB property "maxhealth" */ + private var maxHealth : Int = 0 + /** the amount of health that all of the objects are spawned with; + * defaults to `MaxHealth` if unset */ + private var defaultHealth : Option[Int] = None + + def MaxHealth : Int = maxHealth + + def MaxHealth_=(max : Int) : Int = { + maxHealth = math.min(math.max(0, max), 65535) + MaxHealth + } + + def DefaultHealth : Int = defaultHealth.getOrElse(MaxHealth) + + def DefaultHealth_=(default : Int) : Int = DefaultHealth_=(Some(default)) + + def DefaultHealth_=(default : Option[Int]) : Int = { + defaultHealth = default + DefaultHealth + } + + /* damageable */ + /** whether the object type accepts damage; + * corresponds to ABD property "damageable" */ + private var damageable : Boolean = false + /** whether the object type accepts damage from allied sources; + * corresponds to the opposite of ABD property "damage_immune_to_friendly_fire" */ + private var damageableByFriendlyFire : Boolean = true + /** at what `Health` value the object type is considered "destroyed" */ + private var damageDestroysAt : Int = 0 + /** at what `Health` value the object type is considered "disabled"; + * some object types do not have anything to disable and just transition between "not destroyed" and "destroyed" */ + private var damageDisablesAt : Option[Int] = None + + def Damageable : Boolean = damageable + + def Damageable_=(state : Boolean) : Boolean = { + damageable = state + Damageable + } + + def DamageableByFriendlyFire : Boolean = damageableByFriendlyFire + + def DamageableByFriendlyFire_=(state : Boolean) : Boolean = { + damageableByFriendlyFire = state + DamageableByFriendlyFire + } + + def DamageDisablesAt : Int = damageDisablesAt.getOrElse(MaxHealth/2) + + def DamageDisablesAt_=(value : Int) : Int = DamageDisablesAt_=(Some(value)) + + def DamageDisablesAt_=(value : Option[Int]) : Int = { + damageDisablesAt = value + DamageDisablesAt + } + + def DamageDestroysAt : Int = damageDestroysAt + + def DamageDestroysAt_=(value : Int) : Int = { + damageDestroysAt = value + DamageDestroysAt + } + + /* repairable */ + /** whether the object type accepts attempts to repair it with a nano dispenser tool; + * corresponds to ABD property "canberepairedbynanodispenser" */ + private var repairable : Boolean = false + /** how far away a target can get before repairing is no longer possible; + * not exact in the least */ + private var repairDistance : Float = 5 + /** if the object type is damaged to the condition of "destroyed", + * is it possible to repair it back to the condition of "not destroyed" */ + private var repairIfDestroyed : Boolean = false + /** at what `Health` value the object type is considered "not destroyed"; + * this state is synonymous with "normal" or "functional"; + * as thus, it is opposite of both `damageDestroysAt` and `damageDisablesAt` */ + private var repairRestoresAt : Option[Int] = None + /** object type specific modification value that chnages the base repair quality; + * treat as additive */ + private var repairMod : Int = 0 + + def Repairable : Boolean = repairable + + def Repairable_=(repair : Boolean) : Boolean = { + repairable = repair + Repairable + } + + def RepairDistance : Float = repairDistance + + def RepairDistance_=(distance : Float) : Float = { + repairDistance = math.max(0, distance) + RepairDistance + } + + def RepairIfDestroyed : Boolean = repairIfDestroyed + + def RepairIfDestroyed_=(repair : Boolean) : Boolean = { + repairIfDestroyed = repair + RepairIfDestroyed + } + + def RepairRestoresAt : Int = repairRestoresAt.getOrElse(MaxHealth/2) + + def RepairRestoresAt_=(restore : Int) : Int = RepairRestoresAt_=(Some(restore)) + + def RepairRestoresAt_=(restore : Option[Int]) : Int = { + repairRestoresAt = restore + RepairRestoresAt + } + + def RepairMod : Int = repairMod + + def RepairMod_=(mod : Int) : Int = { + repairMod = mod + RepairMod + } +} diff --git a/common/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala b/common/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala new file mode 100644 index 000000000..5a6e03738 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala @@ -0,0 +1,109 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.vital + +import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile, SourceEntry, VehicleSource} +import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ObjectDefinition} +import net.psforever.objects.serverobject.painbox.Painbox +import net.psforever.objects.serverobject.terminals.TerminalDefinition +import net.psforever.types.{ExoSuitType, ImplantType} + +abstract class VitalsActivity(target : SourceEntry) { + def Target : SourceEntry = target + val t : Long = System.nanoTime //??? + + def time : Long = t +} + +abstract class HealingActivity(target : SourceEntry) extends VitalsActivity(target) + +abstract class DamagingActivity(target : SourceEntry) extends VitalsActivity(target) + +final case class HealFromKit(target : PlayerSource, amount : Int, kit_def : KitDefinition) extends HealingActivity(target) + +final case class HealFromEquipment(target : PlayerSource, user : PlayerSource, amount : Int, equipment_def : EquipmentDefinition) extends HealingActivity(target) + +final case class HealFromTerm(target : PlayerSource, health : Int, armor : Int, term_def : TerminalDefinition) extends HealingActivity(target) + +final case class HealFromImplant(target : PlayerSource, amount : Int, implant : ImplantType.Value) extends HealingActivity(target) + +final case class HealFromExoSuitChange(target : PlayerSource, exosuit : ExoSuitType.Value) extends HealingActivity(target) + +final case class RepairFromKit(target : PlayerSource, amount : Int, kit_def : KitDefinition) extends HealingActivity(target) + +final case class RepairFromEquipment(target : PlayerSource, user : PlayerSource, amount : Int, equipment_def : EquipmentDefinition) extends HealingActivity(target) + +final case class RepairFromTerm(target : VehicleSource, amount : Int, term_def : TerminalDefinition) extends HealingActivity(target) + +final case class VehicleShieldCharge(target : VehicleSource, amount : Int) extends HealingActivity(target) //TODO facility + +final case class DamageFromProjectile(data : ResolvedProjectile) extends DamagingActivity(data.target) + +final case class DamageFromPainbox(target : PlayerSource, painbox : Painbox, damage : Int) extends DamagingActivity(target) + +final case class PlayerSuicide(target : PlayerSource) extends DamagingActivity(target) + +final case class DamageFromExplosion(target : PlayerSource, cause : ObjectDefinition) extends DamagingActivity(target) + +/** + * A vital object can be hurt or damaged or healed or repaired (HDHR). + * A history of the previous changes in vital statistics of the underlying object is recorded + * in reverse chronological order. + */ +trait VitalsHistory { + /** a reverse-order list of chronological events that have occurred to these vital statistics */ + private var vitalsHistory : List[VitalsActivity] = List.empty[VitalsActivity] + + def History : List[VitalsActivity] = vitalsHistory + + /** + * A `VitalsActivity` event must be recorded. + * Add new entry to the front of the list (for recent activity). + * @param action the fully-informed entry + * @return the list of previous changes to this object's vital statistics + */ + def History(action : VitalsActivity) : List[VitalsActivity] = History(Some(action)) + + /** + * A `VitalsActivity` event must be recorded. + * Add new entry to the front of the list (for recent activity). + * @param action the fully-informed entry + * @return the list of previous changes to this object's vital statistics + */ + def History(action : Option[VitalsActivity]) : List[VitalsActivity] = { + action match { + case Some(act) => + vitalsHistory = act +: vitalsHistory + case None => ; + } + vitalsHistory + } + + /** + * Very common example of a `VitalsActivity` event involving weapon discharge. + * @param projectile the fully-informed entry of discharge of a weapon + * @return the list of previous changes to this object's vital statistics + */ + def History(projectile : ResolvedProjectile) : List[VitalsActivity] = { + vitalsHistory = DamageFromProjectile(projectile) +: vitalsHistory + vitalsHistory + } + + /** + * Find, specifically, the last instance of a weapon discharge vital statistics change. + * @return information about the discharge + */ + def LastShot : Option[ResolvedProjectile] = { + vitalsHistory.find({p => p.isInstanceOf[DamageFromProjectile]}) match { + case Some(entry : DamageFromProjectile) => + Some(entry.data) + case _ => + None + } + } + + def ClearHistory() : List[VitalsActivity] = { + val out = vitalsHistory + vitalsHistory = List.empty[VitalsActivity] + out + } +} diff --git a/common/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala b/common/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala index f751e2fc5..971967f4e 100644 --- a/common/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala +++ b/common/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala @@ -59,7 +59,7 @@ object DamageCalculations { def DamageAgainstMaxSuit(profile : DamageProfile) : Int = profile.Damage3 - def DamageAgainstUnknown(profile : DamageProfile) : Int = profile.Damage4 + def DamageAgainstBFR(profile : DamageProfile) : Int = profile.Damage4 //raw damage selection functions /** diff --git a/common/src/main/scala/net/psforever/objects/vital/damage/DamageProfile.scala b/common/src/main/scala/net/psforever/objects/vital/damage/DamageProfile.scala index 95f99ffc5..a5d168364 100644 --- a/common/src/main/scala/net/psforever/objects/vital/damage/DamageProfile.scala +++ b/common/src/main/scala/net/psforever/objects/vital/damage/DamageProfile.scala @@ -6,23 +6,28 @@ package net.psforever.objects.vital.damage * In the same way, the five damage modifiers that are applied to the same kind of damage. */ trait DamageProfile { + /** `damage0` is for basic infantry */ def Damage0 : Int - + /** `damage0` is for basic infantry */ def Damage0_=(damage : Int) : Int + /** `damage1` is for armor, amenities, deployables, etc. */ def Damage1 : Int - + /** `damage1` is for armor, amenities, deployables, etc. */ def Damage1_=(damage : Int) : Int + /** `damage2` if for aircraft */ def Damage2 : Int - + /** `damage2` if for aircraft */ def Damage2_=(damage : Int) : Int + /** `damage3` is for mechanized infantry */ def Damage3 : Int - + /** `damage3` is for mechanized infantry */ def Damage3_=(damage : Int) : Int + /** `damage4` is for battleframe robotics */ def Damage4 : Int - + /** `damage4` is for battleframe robotics */ def Damage4_=(damage : Int) : Int } diff --git a/common/src/main/scala/net/psforever/objects/vital/resistance/ResistanceCalculations.scala b/common/src/main/scala/net/psforever/objects/vital/resistance/ResistanceCalculations.scala index 43433f85f..5b5b78eb1 100644 --- a/common/src/main/scala/net/psforever/objects/vital/resistance/ResistanceCalculations.scala +++ b/common/src/main/scala/net/psforever/objects/vital/resistance/ResistanceCalculations.scala @@ -4,6 +4,7 @@ package net.psforever.objects.vital.resistance import net.psforever.objects.GlobalDefinitions import net.psforever.objects.ballistics._ import net.psforever.objects.definition.ExoSuitDefinition +import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.vital.projectile.ProjectileCalculations import net.psforever.types.ExoSuitType @@ -103,6 +104,20 @@ object ResistanceCalculations { } } + def ValidAmenityTarget(data : ResolvedProjectile) : Try[ObjectSource] = { + data.target match { + case target : ObjectSource => + if(target.obj.isInstanceOf[Amenity]) { + Success(target) + } + else { + failure("something else") + } + case _ => + failure("something else") + } + } + //extractors def NoResistExtractor(target : SourceEntry) : Int = 0 @@ -122,5 +137,9 @@ object ResistanceCalculations { def VehicleRadiationExtractor(target : VehicleSource) : Float = target.Definition.RadiationShielding + def OtherDirectExtractor(target : ObjectSource) : Int = target.Definition.asInstanceOf[ResistanceProfile].ResistanceDirectHit + + def OtherSplashExtractor(target : ObjectSource) : Int = target.Definition.asInstanceOf[ResistanceProfile].ResistanceSplash + def MaximumResistance(target : SourceEntry) : Int = Integer.MAX_VALUE } 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 bd3957921..242a20b5f 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 @@ -4,7 +4,11 @@ package net.psforever.objects.vital.resolution import net.psforever.objects.{Player, TurretDeployable, Vehicle} import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile} import net.psforever.objects.ce.{ComplexDeployable, Deployable} +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.damage.Damageable +import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.serverobject.turret.FacilityTurret +import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.projectile.ProjectileCalculations /** @@ -112,10 +116,13 @@ object ResolutionCalculations { def SubtractWithRemainder(current : Int, damage : Int) : (Int, Int) = { val a = Math.max(0, current - damage) val remainingDamage = Math.abs(current - damage - a) - (a, remainingDamage) } + private def CanDamage(obj : Vitality with FactionAffinity, damage : Int, data : ResolvedProjectile) : Boolean = { + obj.Health > 0 && Damageable.CanDamage(obj, damage, data) + } + /** * The expanded `(Any)=>Unit` function for infantry. * Apply the damage values to the capacitor (if shielded NC max), health field and personal armor field for an infantry target. @@ -130,7 +137,6 @@ object ResolutionCalculations { var result = (0, 0) //TODO Personal Shield implant test should go here and modify the values a and b if(player.isAlive && !(a == 0 && b == 0)) { - player.History(data) if(player.Capacitor.toInt > 0 && player.isShielded) { // Subtract armour damage from capacitor result = SubtractWithRemainder(player.Capacitor.toInt, b) @@ -172,8 +178,7 @@ object ResolutionCalculations { */ def VehicleApplication(damage : Int, data : ResolvedProjectile)(target : Any) : ResolvedProjectile = { target match { - case vehicle : Vehicle if vehicle.Health > 0 && damage > 0 => - vehicle.History(data) + case vehicle : Vehicle if CanDamage(vehicle, damage, data) => val shields = vehicle.Shields if(shields > damage) { vehicle.Shields = shields - damage @@ -192,12 +197,12 @@ object ResolutionCalculations { def SimpleApplication(damage : Int, data : ResolvedProjectile)(target : Any) : ResolvedProjectile = { target match { - case obj : Deployable if obj.Health > 0 => + case obj : Deployable if CanDamage(obj, damage, data) => obj.Health -= damage - obj.History(data) - case turret : FacilityTurret if turret.Health > 0 => + case turret : FacilityTurret if CanDamage(turret, damage, data) => turret.Health -= damage - turret.History(data) + case amenity : Amenity if CanDamage(amenity, damage, data) => + amenity.Health -= damage case _ => ; } data @@ -205,8 +210,7 @@ object ResolutionCalculations { def ComplexDeployableApplication(damage : Int, data : ResolvedProjectile)(target : Any) : ResolvedProjectile = { target match { - case ce : ComplexDeployable if ce.Health > 0 && damage > 0 => - ce.History(data) + case ce : ComplexDeployable if CanDamage(ce, damage, data) => if(ce.Shields > 0) { if(damage > ce.Shields) { ce.Health -= (damage - ce.Shields) @@ -220,8 +224,7 @@ object ResolutionCalculations { ce.Health -= damage } - case ce : TurretDeployable if ce.Health > 0 && damage > 0 => - ce.History(data) + case ce : TurretDeployable if CanDamage(ce, damage, data) => if(ce.Shields > 0) { if(damage > ce.Shields) { ce.Health -= (damage - ce.Shields) diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala index 774a951b7..3b4db0ad6 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -402,6 +402,16 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { }) //after all fixed GUID's are defined ... other.foreach(obj => guid.register(obj, "dynamic")) + //TODO temporary; convert all old-style ImplantTerminalMech.Constructor with the kind that provides position data + import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech + import net.psforever.objects.serverobject.terminals.Terminal + zoneMap.TerminalToInterface.foreach { case (mech_guid, interface_guid) => + (GUID(mech_guid), GUID(interface_guid)) match { + case (Some(mech : ImplantTerminalMech), Some(interface : Terminal)) => + mech.Position = interface.Position + case _ => ; + } + } } private def MakeBuildings(implicit context : ActorContext) : PairMap[Int, Building] = { @@ -561,20 +571,26 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { def AvatarEvents : ActorRef = avatarEvents + def LocalEvents : ActorRef = localEvents + + def VehicleEvents : ActorRef = vehicleEvents + + //mainly for testing + def Activity_=(bus : ActorRef) : ActorRef = { + projector = bus + Activity + } + def AvatarEvents_=(bus : ActorRef) : ActorRef = { avatarEvents = bus AvatarEvents } - def LocalEvents : ActorRef = localEvents - def LocalEvents_=(bus : ActorRef) : ActorRef = { localEvents = bus LocalEvents } - def VehicleEvents : ActorRef = vehicleEvents - def VehicleEvents_=(bus : ActorRef) : ActorRef = { vehicleEvents = bus VehicleEvents diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala index af67b4533..43b87aff6 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala @@ -89,7 +89,8 @@ class ZoneActor(zone : Zone) extends Actor { //ams zone.Vehicles .filter(veh => - veh.Definition == GlobalDefinitions.ams && + veh.Definition == GlobalDefinitions.ams && + !veh.Destroyed && veh.DeploymentState == DriveState.Deployed && veh.Faction == player.Faction ) @@ -120,12 +121,12 @@ class ZoneActor(zone : Zone) extends Actor { Set.empty[StructureType.Value] } zone.SpawnGroups() - .filter({ case (building, _) => + .filter({ case (building, tubes) => buildingTypeSet.contains(building.BuildingType) && (building match { case wg : WarpGate => building.Faction == player.Faction || building.Faction == PlanetSideEmpire.NEUTRAL || wg.Broadcast case _ => - building.Faction == player.Faction + building.Faction == player.Faction && !tubes.forall(sp => sp.Offline) }) }) .toSeq @@ -144,7 +145,10 @@ class ZoneActor(zone : Zone) extends Actor { sender ! Zone.Lattice.SpawnPoint(zone.Id, tube) case Some(tubes) => - sender ! Zone.Lattice.SpawnPoint(zone.Id, scala.util.Random.shuffle(tubes).head) + val tube = scala.util.Random.shuffle( + tubes.filter(sp => !sp.Offline) + ).head + sender ! Zone.Lattice.SpawnPoint(zone.Id, tube) case None => sender ! Zone.Lattice.NoValidSpawnPoint(zone_number, Some(spawn_group)) diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala index 400303a50..fd7e56602 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala @@ -126,8 +126,8 @@ class ZoneMap(private val name : String) { def TerminalToInterface : Map[Int, Int] = linkTerminalInterface - def TerminalToInterface(interface_guid : Int, terminal_guid : Int) : Unit = { - linkTerminalInterface = linkTerminalInterface ++ Map(interface_guid -> terminal_guid) + def TerminalToInterface(terminal_guid : Int, interface_guid : Int) : Unit = { + linkTerminalInterface = linkTerminalInterface ++ Map(terminal_guid -> interface_guid) } def TurretToWeapon : Map[Int, Int] = linkTurretWeapon diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala index efc2d01d2..4fd317574 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneVehicleActor.scala @@ -51,7 +51,7 @@ class ZoneVehicleActor(zone : Zone, vehicleList : ListBuffer[Vehicle]) extends A ZoneVehicleActor.recursiveFindVehicle(vehicleList.iterator, vehicle) match { case Some(index) => vehicleList.remove(index) - vehicle.Actor ! akka.actor.PoisonPill + context.stop(vehicle.Actor) vehicle.Actor = ActorRef.noSender case None => ; sender ! Zone.Vehicle.CanNotDespawn(zone, vehicle, "can not find") diff --git a/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala index 723cdc7d3..cea36fc87 100644 --- a/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala @@ -2,25 +2,11 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import net.psforever.types.PlanetSideEmpire +import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState} import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} -/** - * An `Enumeration` `Codec` that represents that various states of a major facility's Generator. - */ -object PlanetSideGeneratorState extends Enumeration { - type Type = Value - val Normal, - Critical, - Destroyed, - Unk3 - = Value - - implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(2)) -} - /** * na * @param unk1 na diff --git a/common/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala b/common/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala index a56e3e7ed..c58ff2153 100644 --- a/common/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala @@ -7,6 +7,22 @@ import scodec.Codec import scodec.codecs._ import shapeless.{::, HNil} +/** + * na + * @param unk1 na + * @param unk2 if no global unique identifier (below), the alternate identification for the entity + * @param unk2a the global unique identifier of the entity inflicting the damage + * @param unk2b if no global unique identifier (above), the name of the entity inflicting the damage + * @param unk2c if no global unique identifier (above), the object type of the entity inflicting the damage + * @param unk3 if no global unique identifier (below), the alternate identification for the entity + * @param unk3a the global unique identifier of the entity absorbing the damage + * @param unk3b if no global unique identifier (above), the name of the entity absorbing the damage + * @param unk3c if no global unique identifier (above), the object type of the entity absorbing the damage + * @param unk3d na + * @param unk4 na + * @param unk5 the amount of damage + * @param unk6 na + */ final case class DamageFeedbackMessage(unk1 : Int, unk2 : Boolean, unk2a : Option[PlanetSideGUID], @@ -55,12 +71,12 @@ object DamageFeedbackMessage extends Marshallable[DamageFeedbackMessage] { bool >>:~ { u3 => ("unk2a" | conditional(u2, PlanetSideGUID.codec)) :: (("unk2b" | conditional(!u2 && u3, PacketHelpers.encodedWideStringAligned(6))) >>:~ { u2b => - ("unk2c" | conditional(!(u2 && u3), uintL(11))) :: + ("unk2c" | conditional(!u2 && !u3, uintL(11))) :: (bool >>:~ { u5 => bool >>:~ { u6 => ("unk3a" | conditional(u5, PlanetSideGUID.codec)) :: ("unk3b" | conditional(!u5 && u6, PacketHelpers.encodedWideStringAligned( if(u2b.nonEmpty) 3 else 1 ))) :: - ("unk3c" | conditional(!(u5 && u6), uintL(11))) :: + ("unk3c" | conditional(!u5 && !u6, uintL(11))) :: ("unk3d" | conditional(!u5, uint2)) :: ("unk4" | uint(3)) :: ("unk5" | uint32L) :: diff --git a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index 2ee959fc9..6ee5293fe 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -9,8 +9,17 @@ import scodec.codecs._ /** * na
* Global:
- * `50 - Common Initialization?`
- * `51 - Common Initialization?`
+ * `50 - State initialization for amenities`
+ * + * `51 - Common initialization for amenities, complementary to attribute 50`
+ * * `67 - ???`
*
* Global (GUID=0)
@@ -50,15 +59,16 @@ import scodec.codecs._ * `5 - armorMax`
* `6 - PA_RELEASED - transform the (other) avatar in backpack on ground`
* `7 - Sets charge level for MAX capacitor`
- * `8 - Enables empire specific max capacitor function - NC Shield, TR Overdrive, VS Jumpjets` - * `9 - Possibly unused now - PA_SHIELDSTRENGTH in beta client` + * `8 - Enables empire specific max capacitor function - NC Shield, TR Overdrive, VS Jumpjets`
+ * `9 - Possibly unused now - PA_SHIELDSTRENGTH in beta client`
* `14 - Something with grief`
* `15 - Weapon Lock. Value exemple : 600 to have 1 min lock. Max possible is 30min lock`
* `16 - PA_DECONSTRUCTING in beta client`
* `17 - BEP. Value seems to be the same as BattleExperienceMessage`
* `18 - CEP.`
* `19 - Anchors. Value is 0 to disengage, 1 to engage.`
- * `20 - Control console hacking. "The FactionName has hacked into BaseName` - also sets timer on CC and yellow base warning lights on
+ * `20 - Control console hacking, affects CC timer, yellow base warning lights and message "The FactionName has hacked into BaseName". + * Format is: Time left - 2 bytes, faction - 1 byte (1-4), isResecured - 1 byte (0-1)`
* - *
These values seem to correspond to the following data structure: Time left - 2 bytes, faction - 1 byte (1-4), isResecured - 1 byte (0-1)
- * `24 - Learn certifications with value :`
- * 01 : Medium Assault
- * 02 : Heavy Assault
- * 03 : Special Assault
- * 04 : Anti-Vehicular
- * 05 : Sniping
- * 06 : Elite Assault
- * 07 : Air Cavalry, Scout
- * 08 : Air Cavalry, Interceptor
- * 09 : Air Cavalry, Assault
- * 10 : Air Support
- * 11 : ATV
- * 12 : Light Scout
- * 13 : Assault Buggy
- * 14 : Armored Assault 1
- * 15 : Armored Assault 2
- * 16 : Ground Transport
- * 17 : Ground Support
- * 18 : BattleFrame Robotics
- * 19 : Flail
- * 20 : Switchblade
- * 21 : Harasser
- * 22 : Phantasm
- * 23 : Galaxy Gunship
- * 24 : BFR Anti Aircraft
- * 25 : BFR Anti Infantry
- * 26 : ?! Removed Cert ?
- * 27 : ?! Removed Cert ?
- * 28 : Reinforced ExoSuitDefinition
- * 29 : Infiltration Suit
- * 30 : MAX (Burster)
- * 31 : MAX (Dual-Cycler)
- * 32 : MAX (Pounder)
- * 33 : Uni-MAX
- * 34 : Medical
- * 35 : Advanced Medical
- * 36 : Hacking
- * 37 : Advanced Hacking
- * 38 : Expert Hacking
- * 39 : Data Corruption
- * 40 : Electronics Expert (= Expert Hacking + Data Corruption) Must have Advanced Hacking
- * 41 : Engineering
- * 42 : Combat Engineering
- * 43 : Fortification Engineering
- * 44 : Assault Engineering
- * 45 : Advanced Engineering (= Fortification Engineering + Assault Engineering) Must have Combat Engineering
- * `25 - Forget certifications (same order as 24)`
+ * `24 - Learn certification:`
+ * + * `25 - Forget certification: ... (see 24)`
* `26 - Certification reset timer (in seconds)` * `27 - PA_JAMMED - plays jammed buzzing sound in vicinity of target, jams weapon discharge`
* `28 - PA_IMPLANT_ACTIVE - Plays implant sounds. Valid values seem to be up to 20.`
* `29 - PA_VAPORIZED - Visible ?! That's not the cloaked effect, Maybe for spectator mode ?. Value is 0 to visible, 1 to invisible.`
* `31 - Looking for Squad info (marquee and ui):`
- * ` - 0 is LFS`
- * ` - 1 is LFSM (Looking for Squad Members)`
- * ` - n is the supplemental squad identifier number; same as "LFS;" for the leader, sets "LFSM" after the first manual flagging`
- * `32 - Maintain the squad role index, when a member of a squad;
- * - OLD: "Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`"
- * `35 - BR. Value is the BR`
- * `36 - CR. Value is the CR`
+ * + * `32 - Maintain the squad role index, when a member of a squad`
+ * `35 - Battle Rank`
+ * `36 - Command Rank`
* `38 - Spawn active or not. MUST use base MapId not base GUID`
* `43 - Info on avatar name : 0 = Nothing, 1 = "(LD)" message`
* `45 - NTU charge bar 0-10, 5 = 50% full. Seems to apply to both ANT and NTU Silo (possibly siphons?)`
@@ -136,13 +148,15 @@ import scodec.codecs._ * `49 - Vehicle texture effects state? (>0 turns on ANT panel glow or ntu silo panel glow + orbs) (bit?)`
* `52 - Vehicle particle effects? (>0 turns on orbs going towards ANT. Doesn't affect silo) (bit?)`
* `53 - LFS. Value is 1 to flag LFS`
- * `54 - Player "Aura". Values can be expressed in the first byte's lower nibble:`
- * - 0 is nothing
- * - 1 is plasma
- * - 2 is ancient
- * - 4 is LLU (?)
- * - 8 is fire
- * -- e.g., 13 = 8 + 4 + 1 = fire and LLU and plasma
+ * `54 - Player "Aura". Values can be expressed in the first byte's lower nibble:` + * * `55 - "Someone is attempting to Heal you". Value is 1`
* `56 - "Someone is attempting to Repair you". Value is 1`
* `64 - ????? related to using router telepads` @@ -151,14 +165,25 @@ import scodec.codecs._ * `77 - Cavern Facility Captures. Value is the number of captures`
* `78 - Cavern Kills. Value is the number of kills`
* `106 - Custom Head`
- * `116 - Apply colour to REK beam and REK icon above players (0 = yellow, 1 = red, 2 = purple, 3 = blue)`
+ * `116 - Apply colour to REK beam and REK icon above players` + * * Client to Server :
* `106 - Custom Head`
* `224 - Player/vehicle joins black ops`
* `228 - Player/vehicle leaves black ops`
*
* `Vehicles:`
- * `10 - Driver seat permissions (0 = Locked, 1 = Group, 3 = Empire)`
+ * `10 - Driver seat permissions` + * * `11 - Gunner seat(s) permissions (same)`
* `12 - Passenger seat(s) permissions (same)`
* `13 - Trunk permissions (same)`
@@ -170,7 +195,6 @@ import scodec.codecs._ * `80 - Damage vehicle (unknown value)`
* `81 - ???`
* `113 - Vehicle capacitor - e.g. Leviathan EMP charge` - * * @param guid the object * @param attribute_type the field * @param attribute_value the value diff --git a/common/src/main/scala/net/psforever/packet/game/RepairMessage.scala b/common/src/main/scala/net/psforever/packet/game/RepairMessage.scala index 45903f25a..4ee322479 100644 --- a/common/src/main/scala/net/psforever/packet/game/RepairMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/RepairMessage.scala @@ -8,9 +8,14 @@ import scodec.codecs._ /** * Dispatched to the client to report the amount of repair that is performed upon a target item. + * On the client, a progress bar window is displayed with the appropriate repair type and amount. * The item could be a player or a vehicle or a stationary game object, e.g., a terminal. - * @param item_guid an item - * @param repair_value how much the item has been repaired for + * @param item_guid a game object; + * the kind of object influences the kind of repair + * @param repair_value the percentage of maximum health that the object possesses after repairs; + * as the value is a percentage, it should be from 0 to 100; + * at 100, the progress window does not display anymore; + * above 100, the progress window stays displayed unless the underlying process is interrupted */ final case class RepairMessage(item_guid : PlanetSideGUID, repair_value : Long) diff --git a/common/src/main/scala/net/psforever/types/PlanetSideGeneratorState.scala b/common/src/main/scala/net/psforever/types/PlanetSideGeneratorState.scala new file mode 100644 index 000000000..6443016d7 --- /dev/null +++ b/common/src/main/scala/net/psforever/types/PlanetSideGeneratorState.scala @@ -0,0 +1,19 @@ +// Copyright (c) 2020 PSForever +package net.psforever.types + +import net.psforever.packet.PacketHelpers +import scodec.codecs._ + +/** + * An `Enumeration` `Codec` that represents that various states of a major facility's Generator. + */ +object PlanetSideGeneratorState extends Enumeration { + type Type = Value + val Normal, + Critical, + Destroyed, + Unk3 + = Value + + implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(2)) +} diff --git a/common/src/main/scala/services/account/AccountIntermediaryService.scala b/common/src/main/scala/services/account/AccountIntermediaryService.scala index babdc73d8..002346de1 100644 --- a/common/src/main/scala/services/account/AccountIntermediaryService.scala +++ b/common/src/main/scala/services/account/AccountIntermediaryService.scala @@ -37,12 +37,12 @@ class AccountIntermediaryService extends Actor { // Called by the WorldSessionActor case RetrieveAccountData(token) => - val account : Option[Account] = accountsByToken.remove(token) - if(account.nonEmpty) { - sender() ! ReceiveAccountData(account.get) - log.info(s"Retrieving intermediary account data for ${account.get.AccountId}") - } else { - log.error(s"Unable to retrieve intermediary account data for ${account.get.AccountId}") + accountsByToken.remove(token) match { + case Some(acc) => + sender() ! ReceiveAccountData(acc) + log.info(s"Retrieving intermediary account data for $acc") + case None => + log.error(s"Unable to retrieve intermediary account data for $token") } case StoreIPAddress(sessionID, address) => diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala index e4919d833..334455aad 100644 --- a/common/src/main/scala/services/teamwork/SquadService.scala +++ b/common/src/main/scala/services/teamwork/SquadService.scala @@ -1061,7 +1061,7 @@ class SquadService extends Actor { squad.GUID, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed))) ) - case Some(false) | None => ; + case Some(_) | None => ; } case AddSquadMemberPosition(position) => @@ -2267,75 +2267,75 @@ class SquadService extends Actor { def JoinSquad(player : Player, squad : Squad, position : Int) : Boolean = { val charId = player.CharId val role = squad.Membership(position) - if(UserEvents.get(charId).nonEmpty && squad.Leader.CharId != charId && squad.isAvailable(position, player.Certifications)) { - role.Name = player.Name - role.CharId = charId - role.Health = StatConverter.Health(player.Health, player.MaxHealth, min=1, max=64) - role.Armor = StatConverter.Health(player.Armor, player.MaxArmor, min=1, max=64) - role.Position = player.Position - role.ZoneId = 1 - memberToSquad(charId) = squad + UserEvents.get(charId) match { + case Some(events) if squad.Leader.CharId != charId && squad.isAvailable(position, player.Certifications) => + role.Name = player.Name + role.CharId = charId + role.Health = StatConverter.Health(player.Health, player.MaxHealth, min=1, max=64) + role.Armor = StatConverter.Health(player.Armor, player.MaxArmor, min=1, max=64) + role.Position = player.Position + role.ZoneId = 1 + memberToSquad(charId) = squad - continueToMonitorDetails.remove(charId) - RemoveAllInvitesWithPlayer(charId) - InitialAssociation(squad) - Publish(charId, SquadResponse.AssociateWithSquad(squad.GUID)) - val features = squadFeatures(squad.GUID) - val size = squad.Size - if(size == 2) { - //first squad member after leader; both members fully initialize - val (memberCharIds, indices) = squad.Membership - .zipWithIndex - .filterNot { case (member, _) => member.CharId == 0 } - .toList - .unzip { case (member, index) => (member.CharId, index) } - val toChannel = features.ToChannel - memberCharIds.foreach { charId => - SquadEvents.subscribe(UserEvents(charId), s"/$toChannel/Squad") - Publish(charId, + continueToMonitorDetails.remove(charId) + RemoveAllInvitesWithPlayer(charId) + InitialAssociation(squad) + Publish(charId, SquadResponse.AssociateWithSquad(squad.GUID)) + val features = squadFeatures(squad.GUID) + val size = squad.Size + if(size == 2) { + //first squad member after leader; both members fully initialize + val (memberCharIds, indices) = squad.Membership + .zipWithIndex + .filterNot { case (member, _) => member.CharId == 0 } + .toList + .unzip { case (member, index) => (member.CharId, index) } + val toChannel = features.ToChannel + memberCharIds.foreach { charId => + SquadEvents.subscribe(events, s"/$toChannel/Squad") + Publish(charId, + SquadResponse.Join( + squad, + indices.filterNot(_ == position) :+ position, + toChannel + ) + ) + InitWaypoints(charId, squad.GUID) + } + //fully update for all users + InitSquadDetail(squad) + } + else { + //joining an active squad; everybody updates differently + val updatedIndex = List(position) + val toChannel = features.ToChannel + //new member gets full squad UI updates + Publish( + charId, SquadResponse.Join( squad, - indices.filterNot(_ == position) :+ position, + position +: squad.Membership + .zipWithIndex + .collect({ case (member, index) if member.CharId > 0 => index }) + .filterNot(_ == position) + .toList, toChannel ) ) + //other squad members see new member joining the squad + Publish(toChannel, SquadResponse.Join(squad, updatedIndex, "")) InitWaypoints(charId, squad.GUID) - } - //fully update for all users - InitSquadDetail(squad) - } - else { - //joining an active squad; everybody updates differently - val updatedIndex = List(position) - val toChannel = features.ToChannel - //new member gets full squad UI updates - Publish( - charId, - SquadResponse.Join( - squad, - position +: squad.Membership - .zipWithIndex - .collect({ case (member, index) if member.CharId > 0 => index }) - .filterNot(_ == position) - .toList, - toChannel + InitSquadDetail(squad.GUID, Seq(charId), squad) + UpdateSquadDetail( + squad.GUID, + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().CharId(charId).Name(player.Name)))) ) - ) - //other squad members see new member joining the squad - Publish(toChannel, SquadResponse.Join(squad, updatedIndex, "")) - InitWaypoints(charId, squad.GUID) - InitSquadDetail(squad.GUID, Seq(charId), squad) - UpdateSquadDetail( - squad.GUID, - SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().CharId(charId).Name(player.Name)))) - ) - SquadEvents.subscribe(UserEvents(charId), s"/$toChannel/Squad") - } - UpdateSquadListWhenListed(features, SquadInfo().Size(size)) - true - } - else { - false + SquadEvents.subscribe(events, s"/$toChannel/Squad") + } + UpdateSquadListWhenListed(features, SquadInfo().Size(size)) + true + case _ => + false } } diff --git a/common/src/main/scala/services/vehicle/support/TurretUpgrader.scala b/common/src/main/scala/services/vehicle/support/TurretUpgrader.scala index b80aaf175..0d09607b3 100644 --- a/common/src/main/scala/services/vehicle/support/TurretUpgrader.scala +++ b/common/src/main/scala/services/vehicle/support/TurretUpgrader.scala @@ -4,7 +4,8 @@ package services.vehicle.support import akka.actor.{Actor, ActorRef, Cancellable} import net.psforever.objects.{AmmoBox, DefaultCancellable, PlanetSideGameObject, Tool} import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} -import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurret} import net.psforever.objects.vehicles.MountedWeapons import net.psforever.objects.zones.Zone import net.psforever.types.PlanetSideGUID @@ -265,7 +266,7 @@ object TurretUpgrader extends SupportActorCaseConversions { */ case class Entry(_obj : PlanetSideGameObject, _zone : Zone, upgrade : TurretUpgrade.Value, _duration : Long) extends SupportActor.Entry(_obj, _zone, _duration) - final case class AddTask(turret : FacilityTurret, zone : Zone, upgrade : TurretUpgrade.Value, duration : Option[FiniteDuration] = None) + final case class AddTask(turret : PlanetSideServerObject with WeaponTurret, zone : Zone, upgrade : TurretUpgrade.Value, duration : Option[FiniteDuration] = None) final case class Downgrade() diff --git a/common/src/test/scala/game/BuildingInfoUpdateMessageTest.scala b/common/src/test/scala/game/BuildingInfoUpdateMessageTest.scala index 983f25beb..d9ec3e174 100644 --- a/common/src/test/scala/game/BuildingInfoUpdateMessageTest.scala +++ b/common/src/test/scala/game/BuildingInfoUpdateMessageTest.scala @@ -4,7 +4,7 @@ package game import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ -import net.psforever.types.PlanetSideEmpire +import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState} import scodec.bits._ class BuildingInfoUpdateMessageTest extends Specification { diff --git a/common/src/test/scala/game/DamageFeedbackMessageTest.scala b/common/src/test/scala/game/DamageFeedbackMessageTest.scala index d5678537f..fab21f3ff 100644 --- a/common/src/test/scala/game/DamageFeedbackMessageTest.scala +++ b/common/src/test/scala/game/DamageFeedbackMessageTest.scala @@ -9,8 +9,9 @@ import scodec.bits._ class DamageFeedbackMessageTest extends Specification { val string = hex"7b 3d842f610b2040000000" + val string_2 = hex"7B 5E5826D8001DC0400000" - "decode" in { + "decode (string 1)" in { PacketCoding.DecodePacket(string).require match { case DamageFeedbackMessage(unk1, unk2, unk2a, unk2b, unk2c, unk3, unk3a, unk3b, unk3c, unk3d, unk4, unk5, unk6) => unk1 mustEqual 3 @@ -31,13 +32,41 @@ class DamageFeedbackMessageTest extends Specification { } } - "encode" in { + "decode (string 2)" in { + PacketCoding.DecodePacket(string_2).require match { + case DamageFeedbackMessage(unk1, unk2, unk2a, unk2b, unk2c, unk3, unk3a, unk3b, unk3c, unk3d, unk4, unk5, unk6) => + unk1 mustEqual 5 + unk2 mustEqual true + unk2a.contains(PlanetSideGUID(2454)) mustEqual true + unk2b.isEmpty mustEqual true + unk2c.isEmpty mustEqual true + unk3 mustEqual false + unk3a.contains(PlanetSideGUID(216)) mustEqual true + unk3b.isEmpty mustEqual true + unk3c.isEmpty mustEqual true + unk3d.isEmpty mustEqual true + unk4 mustEqual 0 + unk5 mustEqual 750 + unk6 mustEqual 0 + case _ => + ko + } + } + + "encode (string 1)" in { val msg = DamageFeedbackMessage(3, true, Some(PlanetSideGUID(2913)), None, None, true, Some(PlanetSideGUID(2913)), None, None, None, 1, 2, 0) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string } + "encode (string 2)" in { + val msg = DamageFeedbackMessage(5, true, Some(PlanetSideGUID(2454)), None, None, false, Some(PlanetSideGUID(216)), None, None, None, 0, 750, 0) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_2 + } + "assert catches" in { //unk2: no parameters DamageFeedbackMessage(3, true, None, None, None, true, Some(PlanetSideGUID(2913)), None, None, None, 1, 2, 0) must throwA[AssertionError] diff --git a/common/src/test/scala/objects/BuildingTest.scala b/common/src/test/scala/objects/BuildingTest.scala index f35bc5e2d..40278270a 100644 --- a/common/src/test/scala/objects/BuildingTest.scala +++ b/common/src/test/scala/objects/BuildingTest.scala @@ -4,7 +4,6 @@ package objects import akka.actor.{ActorRef, Props} import base.ActorTest import net.psforever.objects.GlobalDefinitions -import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.doors.{Door, DoorControl} import net.psforever.objects.serverobject.structures._ @@ -17,8 +16,11 @@ import services.galaxy.GalaxyService import scala.concurrent.duration._ class AmenityTest extends Specification { + val definition = new AmenityDefinition(0) { + //intentionally blank + } class AmenityObject extends Amenity { - def Definition : ObjectDefinition = null + def Definition : AmenityDefinition = definition } "Amenity" should { diff --git a/common/src/test/scala/objects/DamageModelTests.scala b/common/src/test/scala/objects/DamageModelTests.scala index 3409f5889..536612bb9 100644 --- a/common/src/test/scala/objects/DamageModelTests.scala +++ b/common/src/test/scala/objects/DamageModelTests.scala @@ -46,7 +46,7 @@ class DamageCalculationsTests extends Specification { } "extract damage against something" in { - DamageAgainstUnknown(proj_prof) mustEqual 66 + DamageAgainstBFR(proj_prof) mustEqual 66 } "extract a complete damage profile (1)" in { diff --git a/common/src/test/scala/objects/DamageableTest.scala b/common/src/test/scala/objects/DamageableTest.scala new file mode 100644 index 000000000..b857f0016 --- /dev/null +++ b/common/src/test/scala/objects/DamageableTest.scala @@ -0,0 +1,1560 @@ +// Copyright (c) 2020 PSForever +package objects + +import akka.actor.Props +import akka.testkit.TestProbe +import base.ActorTest +import net.psforever.objects._ +import net.psforever.objects.ballistics._ +import net.psforever.objects.equipment.JammableUnit +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.source.LimitedNumberSource +import net.psforever.objects.serverobject.damage.Damageable +import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl} +import net.psforever.objects.serverobject.implantmech.{ImplantTerminalMech, ImplantTerminalMechControl} +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.serverobject.terminals.{Terminal, TerminalControl, TerminalDefinition} +import net.psforever.objects.serverobject.tube.SpawnTube +import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretControl, TurretUpgrade} +import net.psforever.objects.vehicles.VehicleControl +import net.psforever.objects.vital.Vitality +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.packet.game.DamageWithPositionMessage +import net.psforever.types._ +import services.{RemoverActor, Service} +import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.support.SupportActor +import services.vehicle.support.TurretUpgrader +import services.vehicle.{VehicleAction, VehicleServiceMessage} +import org.specs2.mutable.Specification + +import scala.concurrent.duration._ + +class DamageableTest extends Specification { + val player1 = Player(Avatar("TestCharacter1",PlanetSideEmpire.TR,CharacterGender.Male,0,CharacterVoice.Mute)) + val pSource = PlayerSource(player1) + val weaponA = Tool(GlobalDefinitions.phoenix) //decimator + val projectileA = weaponA.Projectile + + "Damageable" should { + "permit damage" in { + val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + SourceEntry(target), + target.DamageModel, + Vector3.Zero + ) + + Damageable.CanDamage(target, projectileA.Damage0, resolved) mustEqual true + } + + "ignore attempts at non-zero damage" in { + val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileA, weaponA.Definition, weaponA.FireMode, PlayerSource(player1), 0, Vector3.Zero, Vector3.Zero), + SourceEntry(target), + target.DamageModel, + Vector3.Zero + ) + + Damageable.CanDamage(target, 0, resolved) mustEqual false + } + + "ignore attempts at damaging friendly targets not designated for friendly fire" in { + val target = new Generator(GlobalDefinitions.generator) + target.Owner = new Building("test-building", 0, 0, Zone.Nowhere, StructureType.Building, GlobalDefinitions.building) { + Faction = player1.Faction + } + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + SourceEntry(target), + target.DamageModel, + Vector3.Zero + ) + + target.Definition.DamageableByFriendlyFire mustEqual false + target.Faction == player1.Faction mustEqual true + Damageable.CanDamage(target, projectileA.Damage0, resolved) mustEqual false + + target.Owner.Faction = PlanetSideEmpire.NC + target.Faction != player1.Faction mustEqual true + Damageable.CanDamage(target, projectileA.Damage0, resolved) mustEqual true + } + + "ignore attempts at damaging a target that is not damageable" in { + val target = new SpawnTube(GlobalDefinitions.respawn_tube_sanctuary) + target.Owner = new Building("test-building", 0, 0, Zone.Nowhere, StructureType.Building, GlobalDefinitions.building) { + Faction = PlanetSideEmpire.NC + } + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + SourceEntry(target), + target.DamageModel, + Vector3.Zero + ) + + target.Definition.Damageable mustEqual false + target.Faction != player1.Faction mustEqual true + Damageable.CanDamage(target, projectileA.Damage0, resolved) mustEqual false + } + + "permit damaging friendly targets, even those not designated for friendly fire, if the target is hacked" in { + val player2 = Player(Avatar("TestCharacter2",PlanetSideEmpire.NC,CharacterGender.Male,0,CharacterVoice.Mute)) + player2.GUID = PlanetSideGUID(1) + val target = new Terminal(new TerminalDefinition(0) { + Damageable = true + DamageableByFriendlyFire = false + override def Request(player : Player, msg : Any) : Terminal.Exchange = null + }) + target.Owner = new Building("test-building", 0, 0, Zone.Nowhere, StructureType.Building, GlobalDefinitions.building) { + Faction = player1.Faction + } + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + SourceEntry(target), + target.DamageModel, + Vector3.Zero + ) + + target.Definition.DamageableByFriendlyFire mustEqual false + target.Faction == player1.Faction mustEqual true + target.HackedBy.isEmpty mustEqual true + Damageable.CanDamage(target, projectileA.Damage0, resolved) mustEqual false + + target.HackedBy = player2 + target.Faction == player1.Faction mustEqual true + target.HackedBy.nonEmpty mustEqual true + Damageable.CanDamage(target, projectileA.Damage0, resolved) mustEqual true + } + + val weaponB = Tool(GlobalDefinitions.jammer_grenade) + val projectileB = weaponB.Projectile + + "permit jamming" in { + val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + SourceEntry(target), + target.DamageModel, + Vector3.Zero + ) + + resolved.projectile.profile.JammerProjectile mustEqual true + Damageable.CanJammer(target, resolved) mustEqual true + } + + "ignore attempts at jamming if the projectile is does not cause the effect" in { + val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + SourceEntry(target), + target.DamageModel, + Vector3.Zero + ) //decimator + + resolved.projectile.profile.JammerProjectile mustEqual false + Damageable.CanJammer(target, resolved) mustEqual false + } + + "ignore attempts at jamming friendly targets" in { + val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + target.Faction = player1.Faction + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + SourceEntry(target), + target.DamageModel, + Vector3.Zero + ) + + resolved.projectile.profile.JammerProjectile mustEqual true + resolved.projectile.owner.Faction == target.Faction mustEqual true + Damageable.CanJammer(target, resolved) mustEqual false + } + + "ignore attempts at jamming targets that are not jammable" in { + val target = new TrapDeployable(GlobalDefinitions.tank_traps) + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + SourceEntry(target), + target.DamageModel, + Vector3.Zero + ) + + resolved.projectile.profile.JammerProjectile mustEqual true + resolved.projectile.owner.Faction == target.Faction mustEqual false + target.isInstanceOf[JammableUnit] mustEqual false + Damageable.CanJammer(target, resolved) mustEqual false + } + + "permit jamming friendly targets if the target is hacked" in { + val player2 = Player(Avatar("TestCharacter2",PlanetSideEmpire.NC,CharacterGender.Male,0,CharacterVoice.Mute)) + player2.GUID = PlanetSideGUID(1) + val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) + target.Faction = player1.Faction + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + SourceEntry(target), + target.DamageModel, + Vector3.Zero + ) + + resolved.projectile.profile.JammerProjectile mustEqual true + resolved.projectile.owner.Faction == target.Faction mustEqual true + target.isInstanceOf[JammableUnit] mustEqual true + target.HackedBy.nonEmpty mustEqual false + Damageable.CanJammer(target, resolved) mustEqual false + + target.HackedBy = player2 + target.HackedBy.nonEmpty mustEqual true + Damageable.CanJammer(target, resolved) mustEqual true + } + } +} + +/* +the damage targets, Generator, Terminal, etc., are used to test basic destruction +essentially, treat them more as generic entities whose object types are damageable (because they are) +see specific object type tests in relation to what those object types does above and beyond that during damage +*/ +class DamageableEntityDamageTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(5)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = { } + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val gen = Generator(GlobalDefinitions.generator) //guid=2 + val player1 = Player(Avatar("TestCharacter1",PlanetSideEmpire.TR,CharacterGender.Male,0,CharacterVoice.Mute)) //guid=3 + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + building.Position = Vector3(1,0,0) + building.Zone = zone + building.Amenities = gen + gen.Position = Vector3(1,0,0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + building.Actor = buildingProbe.ref + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2,0,0), Vector3(-1,0,0)), + SourceEntry(gen), + gen.DamageModel, + Vector3(1,0,0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + + "DamageableEntity" should { + "handle taking damage" in { + gen.Actor ! Vitality.Damage(applyDamageTo) + val msg1 = avatarProbe.receiveOne( 500 milliseconds) + val msg2 = activityProbe.receiveOne(500 milliseconds) + assert( + msg1 match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg2 match { + case activity : Zone.HotSpot.Activity => + activity.attacker == PlayerSource(player1) && + activity.defender == SourceEntry(gen) && + activity.location == Vector3(1,0,0) + case _ => false + } + ) + } + } +} + +class DamageableEntityDestroyedTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(5)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + val activityProbe = TestProbe() + zone.Activity = activityProbe.ref + val mech = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) //guid=2 + mech.Position = Vector3(1, 0, 0) + mech.Actor = system.actorOf(Props(classOf[ImplantTerminalMechControl], mech), "mech-control") + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Position = Vector3(14, 0, 0) //<14m from generator; dies + player1.Spawn + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = mech + val buildingProbe = TestProbe() + building.Actor = buildingProbe.ref + guid.register(building, 1) + guid.register(mech, 2) + guid.register(player1, 3) + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(mech), + mech.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "DamageableEntity" should { + "manage taking damage until being destroyed" in { + mech.Health = 1 //no matter what, the next shot destoys it + assert(mech.Health == 1) + assert(!mech.Destroyed) + + mech.Actor ! Vitality.Damage(applyDamageTo) + val msg1_2 = avatarProbe.receiveN(2, 500 milliseconds) + assert( + msg1_2.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg1_2(1) match { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, _, Vector3(1, 0, 0))) => true + case _ => false + } + ) + assert(mech.Health == 0) + assert(mech.Destroyed) + } + } +} + +class DamageableEntityNotDestroyTwice extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val gen = Generator(GlobalDefinitions.generator) //guid=2 + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = gen + gen.Position = Vector3(1, 0, 0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + building.Actor = buildingProbe.ref + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(gen), + gen.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "DamageableEntity" should { + "not be destroyed twice (skirting around the 'damage-destroys-at' value)" in { + val originalHealth = gen.Health = gen.Definition.DamageDestroysAt + 1 //damaged, not yet restored + gen.Condition = PlanetSideGeneratorState.Destroyed //initial state manip + gen.Destroyed = true + assert(gen.Destroyed) + assert(originalHealth < gen.Definition.DefaultHealth) + assert(originalHealth < gen.Definition.RepairRestoresAt) + assert(originalHealth > gen.Definition.DamageDestroysAt) + + gen.Actor ! Vitality.Damage(applyDamageTo) + avatarProbe.receiveOne(500 milliseconds) //only one message + avatarProbe.expectNoMsg(500 milliseconds) //only one message + activityProbe.receiveOne(500 milliseconds) //triggers activity hotspot, like it's not a killing blow + assert(gen.Health < originalHealth) + assert(gen.Destroyed) + assert(originalHealth < gen.Definition.DefaultHealth) + assert(originalHealth < gen.Definition.RepairRestoresAt) + assert(gen.Health <= gen.Definition.DamageDestroysAt) + } + } +} + +class DamageableAmenityTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val term = Terminal(GlobalDefinitions.order_terminal) //guid=2 + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + guid.register(building, 1) + guid.register(term, 2) + guid.register(player1, 3) + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = term + term.Position = Vector3(1, 0, 0) + term.Actor = system.actorOf(Props(classOf[TerminalControl], term), "terminal-control") + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + building.Actor = buildingProbe.ref + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(term), + term.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "DamageableAmenity" should { + "send de-initialization messages upon destruction" in { + //the decimator does enough damage to one-shot this terminal from any initial health + term.Health = term.Definition.DamageDestroysAt + 1 + assert(term.Health > term.Definition.DamageDestroysAt) + assert(!term.Destroyed) + + term.Actor ! Vitality.Damage(applyDamageTo) + val msg1234 = avatarProbe.receiveN(4, 500 milliseconds) + assert( + msg1234.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg1234(1) match { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, _, Vector3(1, 0, 0))) => true + case _ => false + } + ) + assert( + msg1234(2) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 50, 1)) => true + case _ => false + } + ) + assert( + msg1234(3) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 51, 1)) => true + case _ => false + } + ) + assert(term.Health <= term.Definition.DamageDestroysAt) + assert(term.Destroyed) + } + } +} + +class DamageableMountableDamageTest extends ActorTest { + //TODO this test with not send HitHint packets because LivePlayers is not being allocated for the players in the zone + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val mech = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) //guid=2 + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + player1.Position = Vector3(2,2,2) + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + player2.Spawn + guid.register(building, 1) + guid.register(mech, 2) + guid.register(player1, 3) + guid.register(player2, 4) + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = mech + mech.Position = Vector3(1, 0, 0) + mech.Actor = system.actorOf(Props(classOf[ImplantTerminalMechControl], mech), "mech-control") + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + building.Actor = buildingProbe.ref + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(mech), + mech.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + mech.Seats(0).Occupant = player2 //seat the player + player2.VehicleSeated = Some(mech.GUID) //seat the player + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "DamageableMountable" should { + "alert seated occupants about incoming damage (damage with position)" in { + assert(mech.Health == mech.Definition.DefaultHealth) + + mech.Actor ! Vitality.Damage(applyDamageTo) + val msg1_3 = avatarProbe.receiveN(2, 500 milliseconds) + val msg2 = activityProbe.receiveOne(500 milliseconds) + assert( + msg1_3.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg2 match { + case activity : Zone.HotSpot.Activity => + activity.attacker == PlayerSource(player1) && + activity.defender == SourceEntry(mech) && + activity.location == Vector3(1,0,0) + case _ => false + } + ) + assert( + msg1_3(1) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2,2,2)))) => true + case _ => false + } + ) + } + } +} + +class DamageableMountableDestroyTest extends ActorTest { + //TODO this test with not send HitHint packets because LivePlayers is not being allocated for the players in the zone + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val mech = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) //guid=2 + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + player1.Position = Vector3(2, 2, 2) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + player2.Spawn + val player2Probe = TestProbe() + player2.Actor = player2Probe.ref + guid.register(building, 1) + guid.register(mech, 2) + guid.register(player1, 3) + guid.register(player2, 4) + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = mech + mech.Position = Vector3(1, 0, 0) + mech.Actor = system.actorOf(Props(classOf[ImplantTerminalMechControl], mech), "mech-control") + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + building.Actor = buildingProbe.ref + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(mech), + mech.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + mech.Seats(0).Occupant = player2 //seat the player + player2.VehicleSeated = Some(mech.GUID) //seat the player + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "DamageableMountable" should { + "alert seated occupants that the mountable object has been destroyed and that they should have died" in { + val originalHealth = mech.Health = mech.Definition.DamageDestroysAt + 1 //initial state manip + assert(originalHealth > mech.Definition.DamageDestroysAt) + assert(!mech.Destroyed) + + mech.Actor ! Vitality.Damage(applyDamageTo) + val msg12 = avatarProbe.receiveN(2, 500 milliseconds) + player1Probe.expectNoMsg(500 milliseconds) + val msg3 = player2Probe.receiveOne(200 milliseconds) + assert( + msg12.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg12(1) match { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, _, Vector3(1, 0, 0))) => true + case _ => false + } + ) + assert( + msg3 match { + case Player.Die() => true + case _ => false + } + ) + assert(mech.Health <= mech.Definition.DamageDestroysAt) + assert(mech.Destroyed) + } + } +} + +class DamageableWeaponTurretDamageTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + val turret = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) //2 + turret.Actor = system.actorOf(Props(classOf[TurretControl], turret), "turret-control") + turret.Zone = zone + turret.Position = Vector3(1,0,0) + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + player1.Position = Vector3(2, 2, 2) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + player2.Spawn + val player2Probe = TestProbe() + player2.Actor = player2Probe.ref + guid.register(turret, 2) + guid.register(player1, 3) + guid.register(player2, 4) + turret.Seats(0).Occupant = player2 + player2.VehicleSeated = turret.GUID + + val weapon = Tool(GlobalDefinitions.suppressor) + val projectile = weapon.Projectile + val turretSource = SourceEntry(turret) + val resolved = ResolvedProjectile( + ProjectileResolution.Hit, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + turretSource, + turret.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "DamageableWeaponTurret" should { + "handle damage" in { + assert(turret.Health == turret.Definition.DefaultHealth) + + turret.Actor ! Vitality.Damage(applyDamageTo) + val msg1_3 = avatarProbe.receiveN(2, 500 milliseconds) + val msg2 = activityProbe.receiveOne(500 milliseconds) + assert( + msg1_3.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg2 match { + case activity : Zone.HotSpot.Activity => + activity.attacker == PlayerSource(player1) && + activity.defender == turretSource && + activity.location == Vector3(1,0,0) + case _ => false + } + ) + assert( + msg1_3(1) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2,2,2)))) => true + case _ => false + } + ) + assert(turret.Health < turret.Definition.DefaultHealth) + } + } +} + +class DamageableWeaponTurretJammerTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.VehicleEvents = vehicleProbe.ref + + val turret = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) //2, 5, 6 + turret.Actor = system.actorOf(Props(classOf[TurretControl], turret), "turret-control") + turret.Zone = zone + turret.Position = Vector3(1, 0, 0) + val turretWeapon = turret.Weapons.values.head.Equipment.get.asInstanceOf[Tool] + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + player1.Position = Vector3(2, 2, 2) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + player2.Spawn + val player2Probe = TestProbe() + player2.Actor = player2Probe.ref + + guid.register(turret, 2) + guid.register(player1, 3) + guid.register(player2, 4) + guid.register(turretWeapon, 5) + guid.register(turretWeapon.AmmoSlot.Box, 6) + turret.Seats(0).Occupant = player2 + player2.VehicleSeated = turret.GUID + + val weapon = Tool(GlobalDefinitions.jammer_grenade) + val projectile = weapon.Projectile + val turretSource = SourceEntry(turret) + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + turretSource, + turret.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "DamageableWeaponTurret" should { + "handle jammer effect" in { + assert(turret.Health == turret.Definition.DefaultHealth) + assert(!turret.Jammed) + + turret.Actor ! Vitality.Damage(applyDamageTo) + val msg12 = vehicleProbe.receiveN(2, 500 milliseconds) + assert( + msg12.head match { + case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, PlanetSideGUID(2), 27, 1))=> true + case _ => false + } + ) + assert( + msg12(1) match { + case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, PlanetSideGUID(5), 27, 1))=> true + case _ => false + } + ) + assert(turret.Health == turret.Definition.DefaultHealth) + assert(turret.Jammed) + } + } +} + +class DamageableWeaponTurretDestructionTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.VehicleEvents = vehicleProbe.ref + building.Actor = buildingProbe.ref + + val turret = new FacilityTurret(GlobalDefinitions.manned_turret) //2, 5, 6 + turret.Actor = system.actorOf(Props(classOf[FacilityTurretControl], turret), "turret-control") + turret.Zone = zone + turret.Position = Vector3(1, 0, 0) + val turretWeapon = turret.Weapons.values.head.Equipment.get.asInstanceOf[Tool] + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + player1.Position = Vector3(2, 2, 2) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + player2.Spawn + val player2Probe = TestProbe() + player2.Actor = player2Probe.ref + + guid.register(building, 1) + guid.register(turret, 2) + guid.register(player1, 3) + guid.register(player2, 4) + guid.register(turretWeapon, 5) + guid.register(turretWeapon.AmmoSlot.Box, 6) + turret.Seats(0).Occupant = player2 + player2.VehicleSeated = turret.GUID + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = turret + + val turretSource = SourceEntry(turret) + val weaponA = Tool(GlobalDefinitions.jammer_grenade) + val projectileA = weaponA.Projectile + val resolvedA = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileA, weaponA.Definition, weaponA.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + turretSource, + turret.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageToA = resolvedA.damage_model.Calculate(resolvedA) + + val weaponB = Tool(GlobalDefinitions.phoenix) //decimator + val projectileB = weaponB.Projectile + val resolvedB = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileB, weaponB.Definition, weaponB.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + turretSource, + turret.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageToB = resolvedB.damage_model.Calculate(resolvedB) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "DamageableWeaponTurret" should { + "handle being destroyed gracefully" in { + turret.Health = turret.Definition.DamageDestroysAt + 1 //initial state manip + turret.Upgrade = TurretUpgrade.AVCombo //initial state manip; act like having being upgraded properly + assert(turret.Health > turret.Definition.DamageDestroysAt) + assert(!turret.Jammed) + assert(!turret.Destroyed) + + turret.Actor ! Vitality.Damage(applyDamageToA) //also test destruction while jammered + vehicleProbe.receiveN(2, 500 milliseconds) //flush jammered messages (see above) + assert(turret.Health > turret.Definition.DamageDestroysAt) + assert(turret.Jammed) + assert(!turret.Destroyed) + + turret.Actor ! Vitality.Damage(applyDamageToB) //destroy + val msg12_4 = avatarProbe.receiveN(3, 500 milliseconds) + player1Probe.expectNoMsg(500 milliseconds) + val msg3 = player2Probe.receiveOne(200 milliseconds) + val msg56 = vehicleProbe.receiveN(2, 200 milliseconds) + assert( + msg12_4.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg12_4(1) match { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, _, Vector3(1, 0, 0))) => true + case _ => false + } + ) + assert( + msg3 match { + case Player.Die() => true + case _ => false + } + ) + assert( + msg12_4(2) match { + case AvatarServiceMessage("test", AvatarAction.ObjectDelete(PlanetSideGUID(0), PlanetSideGUID(5), _)) => true + case _ => false + } + ) + assert( + msg56.head match { + case VehicleServiceMessage.TurretUpgrade(SupportActor.ClearSpecific(List(t), _)) if t eq turret => true + case _ => false + } + ) + assert( + msg56(1) match { + case VehicleServiceMessage.TurretUpgrade(TurretUpgrader.AddTask(t, _, TurretUpgrade.None, _)) if t eq turret => true + case _ => false + } + ) + assert(turret.Health <= turret.Definition.DamageDestroysAt) + assert(!turret.Jammed) + assert(turret.Destroyed) + } + } +} + +class DamageableVehicleDamageTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.VehicleEvents = vehicleProbe.ref + + val atv = Vehicle(GlobalDefinitions.quadstealth) //guid=1 + atv.Actor = system.actorOf(Props(classOf[VehicleControl], atv), "vehicle-control") + atv.Position = Vector3(1,0,0) + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player1.Spawn + player1.Position = Vector3(2,0,0) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player2.Spawn + val player2Probe = TestProbe() + player2.Actor = player2Probe.ref + + guid.register(atv, 1) + guid.register(player1, 2) + guid.register(player2, 3) + atv.Zone = zone + atv.Seats(0).Occupant = player2 + player2.VehicleSeated = atv.GUID + + val weapon = Tool(GlobalDefinitions.suppressor) + val projectile = weapon.Projectile + val vehicleSource = SourceEntry(atv) + val resolved = ResolvedProjectile( + ProjectileResolution.Hit, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + vehicleSource, + atv.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "DamageableVehicle" should { + "handle damage" in { + atv.Shields = 1 //initial state manip + assert(atv.Health == atv.Definition.DefaultHealth) + assert(atv.Shields == 1) + + atv.Actor ! Vitality.Damage(applyDamageTo) + val msg1_3 = avatarProbe.receiveN(2,500 milliseconds) + val msg2 = activityProbe.receiveOne(200 milliseconds) + val msg4 = vehicleProbe.receiveOne(200 milliseconds) + assert( + msg1_3.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true + case _ => false + } + ) + assert( + msg2 match { + case activity : Zone.HotSpot.Activity => + activity.attacker == PlayerSource(player1) && + activity.defender == vehicleSource && + activity.location == Vector3(1,0,0) + case _ => false + } + ) + assert( + msg1_3(1) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2,0,0)))) => true + case _ => false + } + ) + assert( + msg4 match { + case VehicleServiceMessage(channel, VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)) + if channel.equals(atv.Actor.toString) => true + case _ => false + } + ) + assert(atv.Health < atv.Definition.DefaultHealth) + assert(atv.Shields == 0) + } + } +} + +class DamageableVehicleDamageMountedTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(15)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.VehicleEvents = vehicleProbe.ref + + val lodestar = Vehicle(GlobalDefinitions.lodestar) //guid=1 & 4,5,6,7,8,9 + lodestar.Position = Vector3(1,0,0) + val atv = Vehicle(GlobalDefinitions.quadstealth) //guid=11 + atv.Position = Vector3(1,0,0) + atv.Actor = system.actorOf(Props(classOf[VehicleControl], atv), "atv-control") + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player1.Spawn + player1.Position = Vector3(2,0,0) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player2.Spawn + val player2Probe = TestProbe() + player2.Actor = player2Probe.ref + val player3 = Player(Avatar("TestCharacter3", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=10 + player3.Spawn + val player3Probe = TestProbe() + player3.Actor = player3Probe.ref + + guid.register(lodestar, 1) + guid.register(player1, 2) + guid.register(player2, 3) + guid.register(lodestar.Utilities(2)(), 4) + guid.register(lodestar.Utilities(3)(), 5) + guid.register(lodestar.Utilities(4)(), 6) + guid.register(lodestar.Utilities(5)(), 7) + guid.register(lodestar.Utilities(6)(), 8) + guid.register(lodestar.Utilities(7)(), 9) + guid.register(player3, 10) + guid.register(atv, 11) + + //the lodestar control actor needs to load after the utilities have guid's assigned + lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") + lodestar.Zone = zone + lodestar.Seats(0).Occupant = player2 + player2.VehicleSeated = lodestar.GUID + atv.Zone = zone + atv.Seats(0).Occupant = player3 + player3.VehicleSeated = atv.GUID + lodestar.CargoHolds(1).Occupant = atv + atv.MountedIn = lodestar.GUID + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val vehicleSource = SourceEntry(lodestar) + val resolved = ResolvedProjectile( + ProjectileResolution.Hit, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + vehicleSource, + lodestar.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "handle damage with mounted vehicles" in { + lodestar.Shields = 1 //initial state manip + atv.Shields = 1 //initial state manip + assert(lodestar.Health == lodestar.Definition.DefaultHealth) + assert(lodestar.Shields == 1) + assert(atv.Health == atv.Definition.DefaultHealth) + assert(atv.Shields == 1) + + lodestar.Actor ! Vitality.Damage(applyDamageTo) + val msg1_35 = avatarProbe.receiveN(3,500 milliseconds) + val msg2 = activityProbe.receiveOne(200 milliseconds) + val msg4 = vehicleProbe.receiveOne(200 milliseconds) + assert( + msg1_35.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true + case _ => false + } + ) + assert( + msg2 match { + case activity : Zone.HotSpot.Activity => + activity.attacker == PlayerSource(player1) && + activity.defender == vehicleSource && + activity.location == Vector3(1,0,0) + case _ => false + } + ) + assert( + msg1_35(1) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2,0,0)))) => true + case _ => false + } + ) + assert( + msg4 match { + case VehicleServiceMessage(channel, VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)) + if channel.equals(lodestar.Actor.toString) => true + case _ => false + } + ) + assert( + msg1_35(2) match { + case AvatarServiceMessage("TestCharacter3", AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2,0,0)))) => true + case _ => false + } + ) + assert(lodestar.Health < lodestar.Definition.DefaultHealth) + assert(lodestar.Shields == 0) + assert(atv.Health == atv.Definition.DefaultHealth) + assert(atv.Shields == 1) + } +} + +class DamageableVehicleJammeringMountedTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(15)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.VehicleEvents = vehicleProbe.ref + + val atv = Vehicle(GlobalDefinitions.quadassault) //guid=1 + atv.Actor = system.actorOf(Props(classOf[VehicleControl], atv), "atv-control") + atv.Position = Vector3(1,0,0) + val atvWeapon = atv.Weapons(1).Equipment.get.asInstanceOf[Tool] //guid=4 & 5 + + val lodestar = Vehicle(GlobalDefinitions.lodestar) //guid=6 + lodestar.Position = Vector3(1,0,0) + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=7 + player1.Spawn + player1.Position = Vector3(2,0,0) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=8 + player2.Spawn + val player2Probe = TestProbe() + player2.Actor = player2Probe.ref + val player3 = Player(Avatar("TestCharacter3", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=9 + player3.Spawn + val player3Probe = TestProbe() + player3.Actor = player3Probe.ref + + guid.register(atv, 1) + guid.register(atvWeapon, 2) + guid.register(atvWeapon.AmmoSlot.Box, 3) + guid.register(lodestar, 4) + guid.register(lodestar.Utilities(2)(), 5) + guid.register(lodestar.Utilities(3)(), 6) + guid.register(lodestar.Utilities(4)(), 7) + guid.register(lodestar.Utilities(5)(), 8) + guid.register(lodestar.Utilities(6)(), 9) + guid.register(lodestar.Utilities(7)(), 10) + guid.register(player1, 11) + guid.register(player2, 12) + guid.register(player3, 13) + + lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") + atv.Zone = zone + lodestar.Zone = zone + atv.Seats(0).Occupant = player2 + player2.VehicleSeated = atv.GUID + lodestar.Seats(0).Occupant = player3 + player3.VehicleSeated = lodestar.GUID + lodestar.CargoHolds(1).Occupant = atv + atv.MountedIn = lodestar.GUID + + val vehicleSource = SourceEntry(lodestar) + val weapon = Tool(GlobalDefinitions.jammer_grenade) + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + vehicleSource, + lodestar.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "handle jammering with mounted vehicles" in { + assert(lodestar.Health == lodestar.Definition.DefaultHealth) + assert(!lodestar.Jammered) + assert(atv.Health == atv.Definition.DefaultHealth) + assert(!atv.Jammered) + + lodestar.Actor ! Vitality.Damage(applyDamageTo) + val msg12 = vehicleProbe.receiveOne(500 milliseconds) + avatarProbe.expectNoMsg(500 milliseconds) + player1Probe.expectNoMsg(200 milliseconds) + player2Probe.expectNoMsg(200 milliseconds) + player3Probe.expectNoMsg(200 milliseconds) + assert( + msg12 match { + case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, PlanetSideGUID(4), 27, 1))=> true + case _ => false + } + ) + assert(lodestar.Health == lodestar.Definition.DefaultHealth) + assert(lodestar.Jammed) + assert(atv.Health == atv.Definition.DefaultHealth) + assert(!atv.Jammed) + } +} + +class DamageableVehicleDestroyTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.VehicleEvents = vehicleProbe.ref + + val atv = Vehicle(GlobalDefinitions.quadassault) //guid=1 + atv.Actor = system.actorOf(Props(classOf[VehicleControl], atv), "vehicle-control") + atv.Position = Vector3(1,0,0) + val atvWeapon = atv.Weapons(1).Equipment.get.asInstanceOf[Tool] //guid=4 & 5 + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player1.Spawn + player1.Position = Vector3(2,0,0) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player2.Spawn + val player2Probe = TestProbe() + player2.Actor = player2Probe.ref + + guid.register(atv, 1) + guid.register(player1, 2) + guid.register(player2, 3) + guid.register(atvWeapon, 4) + guid.register(atvWeapon.AmmoSlot.Box, 5) + atv.Zone = zone + atv.Seats(0).Occupant = player2 + player2.VehicleSeated = atv.GUID + + val weapon = Tool(GlobalDefinitions.suppressor) + val projectile = weapon.Projectile + val vehicleSource = SourceEntry(atv) + val resolved = ResolvedProjectile( + ProjectileResolution.Hit, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + vehicleSource, + atv.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "DamageableVehicle" should { + "handle destruction" in { + atv.Health = atv.Definition.DamageDestroysAt + 1 //initial state manip + atv.Shields = 1 + assert(atv.Health > atv.Definition.DamageDestroysAt) + assert(atv.Shields == 1) + assert(!atv.Destroyed) + + atv.Actor ! Vitality.Damage(applyDamageTo) + val msg124 = avatarProbe.receiveN(3, 500 milliseconds) + val msg3 = player2Probe.receiveOne(200 milliseconds) + val msg567 = vehicleProbe.receiveN(2, 200 milliseconds) + assert( + msg124.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true + case _ => false + } + ) + assert( + msg124(1) match { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(1), _, _, Vector3(1, 0, 0))) => true + case _ => false + } + ) + assert( + msg3 match { + case Player.Die() => true + case _ => false + } + ) + assert( + msg124(2) match { + case AvatarServiceMessage("test", AvatarAction.ObjectDelete(PlanetSideGUID(0), PlanetSideGUID(4), _)) => true + case _ => false + } + ) + assert( + msg567.head match { + case VehicleServiceMessage.Decon(SupportActor.ClearSpecific(List(target), _zone)) if (target eq atv) && (_zone eq zone) => true + case _ => false + } + ) + assert( + msg567(1) match { + case VehicleServiceMessage.Decon(RemoverActor.AddTask(target, _zone, _)) if (target eq atv) && (_zone eq zone) => true + case _ => false + } + ) + assert(atv.Health <= atv.Definition.DamageDestroysAt) + assert(atv.Destroyed) + // + } + } +} + +class DamageableVehicleDestroyMountedTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(15)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.VehicleEvents = vehicleProbe.ref + + val atv = Vehicle(GlobalDefinitions.quadassault) //guid=1 + atv.Actor = system.actorOf(Props(classOf[VehicleControl], atv), "atv-control") + atv.Position = Vector3(1,0,0) + val atvWeapon = atv.Weapons(1).Equipment.get.asInstanceOf[Tool] //guid=4 & 5 + + val lodestar = Vehicle(GlobalDefinitions.lodestar) //guid=6 + lodestar.Position = Vector3(1,0,0) + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=7 + player1.Spawn + player1.Position = Vector3(2,0,0) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=8 + player2.Spawn + val player2Probe = TestProbe() + player2.Actor = player2Probe.ref + val player3 = Player(Avatar("TestCharacter3", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=9 + player3.Spawn + val player3Probe = TestProbe() + player3.Actor = player3Probe.ref + + guid.register(atv, 1) + guid.register(atvWeapon, 2) + guid.register(atvWeapon.AmmoSlot.Box, 3) + guid.register(lodestar, 4) + guid.register(lodestar.Utilities(2)(), 5) + guid.register(lodestar.Utilities(3)(), 6) + guid.register(lodestar.Utilities(4)(), 7) + guid.register(lodestar.Utilities(5)(), 8) + guid.register(lodestar.Utilities(6)(), 9) + guid.register(lodestar.Utilities(7)(), 10) + guid.register(player1, 11) + guid.register(player2, 12) + guid.register(player3, 13) + + lodestar.Actor = system.actorOf(Props(classOf[VehicleControl], lodestar), "lodestar-control") + atv.Zone = zone + lodestar.Zone = zone + atv.Seats(0).Occupant = player2 + player2.VehicleSeated = atv.GUID + lodestar.Seats(0).Occupant = player3 + player3.VehicleSeated = lodestar.GUID + lodestar.CargoHolds(1).Occupant = atv + atv.MountedIn = lodestar.GUID + + val vehicleSource = SourceEntry(lodestar) + val weaponA = Tool(GlobalDefinitions.jammer_grenade) + val projectileA = weaponA.Projectile + val resolvedA = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileA, weaponA.Definition, weaponA.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + vehicleSource, + lodestar.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageToA = resolvedA.damage_model.Calculate(resolvedA) + + val weaponB = Tool(GlobalDefinitions.phoenix) //decimator + val projectileB = weaponB.Projectile + val resolvedB = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectileB, weaponB.Definition, weaponB.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + vehicleSource, + lodestar.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageToB = resolvedB.damage_model.Calculate(resolvedB) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "handle jammering with mounted vehicles" in { + lodestar.Health = lodestar.Definition.DamageDestroysAt + 1 //initial state manip + atv.Shields = 1 //initial state manip + assert(lodestar.Health > lodestar.Definition.DamageDestroysAt) + assert(!lodestar.Jammered) + assert(!lodestar.Destroyed) + assert(atv.Health == atv.Definition.DefaultHealth) + assert(atv.Shields == 1) + assert(!atv.Jammered) + assert(!atv.Destroyed) + + lodestar.Actor ! Vitality.Damage(applyDamageToA) + vehicleProbe.receiveOne(500 milliseconds) //flush jammered message + avatarProbe.expectNoMsg(200 milliseconds) + player1Probe.expectNoMsg(200 milliseconds) + player2Probe.expectNoMsg(200 milliseconds) + player3Probe.expectNoMsg(200 milliseconds) + assert(lodestar.Health > lodestar.Definition.DamageDestroysAt) + assert(lodestar.Jammed) + assert(!lodestar.Destroyed) + assert(atv.Health == atv.Definition.DefaultHealth) + assert(atv.Shields == 1) + assert(!atv.Jammed) + assert(!atv.Destroyed) + + lodestar.Actor ! Vitality.Damage(applyDamageToB) + val msg_avatar = avatarProbe.receiveN(5, 500 milliseconds) + avatarProbe.expectNoMsg(10 milliseconds) + val msg_player2 = player2Probe.receiveOne(200 milliseconds) + player2Probe.expectNoMsg(10 milliseconds) + val msg_player3 = player3Probe.receiveOne(200 milliseconds) + player3Probe.expectNoMsg(10 milliseconds) + val msg_vehicle = vehicleProbe.receiveN(6, 200 milliseconds) + vehicleProbe.expectNoMsg(10 milliseconds) + assert( + msg_avatar.exists( { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(4), 0, _)) => true + case _ => false + }) + ) + assert( + msg_avatar.exists( { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(4), _, _, Vector3(1, 0, 0))) => true + case _ => false + }) + ) + assert( + msg_avatar.exists( { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true + case _ => false + }) + ) + assert( + msg_avatar.exists( { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(1), _, _, Vector3(1, 0, 0))) => true + case _ => false + }) + ) + assert( + msg_avatar.exists( { + case AvatarServiceMessage("test", AvatarAction.ObjectDelete(PlanetSideGUID(0), PlanetSideGUID(2), _)) => true + case _ => false + }) + ) + assert( + msg_player2 match { + case Player.Die() => true + case _ => false + } + ) + assert( + msg_player3 match { + case Player.Die() => true + case _ => false + } + ) + assert( + msg_vehicle.exists( { + case VehicleServiceMessage.Decon(SupportActor.ClearSpecific(List(target), _zone)) if (target eq lodestar) && (_zone eq zone) => true + case _ => false + }) + ) + assert( + msg_vehicle.exists( { + case VehicleServiceMessage.Decon(RemoverActor.AddTask(target, _zone, _)) if (target eq lodestar) && (_zone eq zone) => true + case _ => false + }) + ) + assert( + msg_vehicle.exists( { + case VehicleServiceMessage.Decon(SupportActor.ClearSpecific(List(target), _zone)) if (target eq atv) && (_zone eq zone) => true + case _ => false + }) + ) + assert( + msg_vehicle.exists( { + case VehicleServiceMessage.Decon(RemoverActor.AddTask(target, _zone, _)) if (target eq atv) && (_zone eq zone) => true + case _ => false + }) + ) + assert( + msg_vehicle.exists( { + case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, PlanetSideGUID(4), 27, 0))=> true + case _ => false + }) + ) + assert( + msg_vehicle.exists( { + case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, PlanetSideGUID(1), 68, 0))=> true + case _ => false + }) + ) + assert(lodestar.Health <= lodestar.Definition.DamageDestroysAt) + assert(!lodestar.Jammed) + assert(lodestar.Destroyed) + assert(atv.Health <= atv.Definition.DefaultHealth) + assert(atv.Shields == 0) + assert(!atv.Jammed) + assert(atv.Destroyed) + } +} + +object DamageableTest { } diff --git a/common/src/test/scala/objects/DeployableTest.scala b/common/src/test/scala/objects/DeployableTest.scala index 813ae7c92..d710d7334 100644 --- a/common/src/test/scala/objects/DeployableTest.scala +++ b/common/src/test/scala/objects/DeployableTest.scala @@ -2,12 +2,23 @@ package objects import akka.actor.{Actor, ActorRef, Props} +import akka.testkit.TestProbe import base.ActorTest +import net.psforever.objects.ballistics._ import net.psforever.objects.ce.DeployedItem +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.source.LimitedNumberSource import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.vital.Vitality +import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.objects.{TurretDeployable, _} -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, PlanetSideGUID} +import net.psforever.packet.game.{DeployableIcon, DeployableInfo, DeploymentAction} +import net.psforever.types._ import org.specs2.mutable.Specification +import services.{RemoverActor, Service} +import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.local.{LocalAction, LocalServiceMessage} +import services.support.SupportActor import scala.concurrent.duration._ @@ -49,14 +60,14 @@ class ExplosiveDeployableTest extends Specification { "ExplosiveDeployable" should { "construct" in { val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine) - obj.Exploded mustEqual false + obj.Destroyed mustEqual false } "explode" in { val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine) - obj.Exploded mustEqual false - obj.Exploded = true - obj.Exploded mustEqual true + obj.Destroyed mustEqual false + obj.Destroyed = true + obj.Destroyed mustEqual true } } } @@ -65,15 +76,15 @@ class BoomerDeployableTest extends Specification { "BoomerDeployable" should { "construct" in { val obj = new BoomerDeployable(GlobalDefinitions.boomer) - obj.Exploded mustEqual false + obj.Destroyed mustEqual false obj.Trigger.isEmpty mustEqual true } "explode" in { val obj = new BoomerDeployable(GlobalDefinitions.boomer) - obj.Exploded mustEqual false - obj.Exploded = true - obj.Exploded mustEqual true + obj.Destroyed mustEqual false + obj.Destroyed = true + obj.Destroyed mustEqual true } "manage its trigger" in { @@ -142,6 +153,130 @@ class TurretDeployableTest extends Specification { } } +class DeployableMake extends Specification { + "Deployables.Make" should { + "construct a boomer" in { + val func = Deployables.Make(DeployedItem.boomer) + func() match { + case _ : BoomerDeployable => ok + case _ => ko + } + } + + "construct an he mine" in { + val func = Deployables.Make(DeployedItem.he_mine) + func() match { + case obj : ExplosiveDeployable if obj.Definition == GlobalDefinitions.he_mine => ok + case _ => ko + } + } + + "construct a disruptor mine" in { + val func = Deployables.Make(DeployedItem.jammer_mine) + func() match { + case obj : ExplosiveDeployable if obj.Definition == GlobalDefinitions.jammer_mine => ok + case _ => ko + } + } + + "construct a spitfire turret" in { + val func = Deployables.Make(DeployedItem.spitfire_turret) + func() match { + case obj : TurretDeployable if obj.Definition == GlobalDefinitions.spitfire_turret => ok + case _ => ko + } + } + + "construct a shadow turret" in { + val func = Deployables.Make(DeployedItem.spitfire_cloaked) + func() match { + case obj : TurretDeployable if obj.Definition == GlobalDefinitions.spitfire_cloaked => ok + case _ => ko + } + } + + "construct a cerebus turret" in { + val func = Deployables.Make(DeployedItem.spitfire_aa) + func() match { + case obj : TurretDeployable if obj.Definition == GlobalDefinitions.spitfire_aa => ok + case _ => ko + } + } + + "construct a motion sensor" in { + val func = Deployables.Make(DeployedItem.motionalarmsensor) + func() match { + case obj : SensorDeployable if obj.Definition == GlobalDefinitions.motionalarmsensor => ok + case _ => ko + } + } + + "construct a sensor disruptor" in { + val func = Deployables.Make(DeployedItem.sensor_shield) + func() match { + case obj : SensorDeployable if obj.Definition == GlobalDefinitions.sensor_shield => ok + case _ => ko + } + } + + "construct three metal i-beams so huge that a driver must be blind to drive into them but does anyway" in { + val func = Deployables.Make(DeployedItem.tank_traps) + func() match { + case obj : TrapDeployable if obj.Definition == GlobalDefinitions.tank_traps => ok + case _ => ko + } + } + + "construct a generic field turret" in { + val func = Deployables.Make(DeployedItem.portable_manned_turret) + func() match { + case obj : TurretDeployable if obj.Definition == GlobalDefinitions.portable_manned_turret => ok + case _ => ko + } + } + + "construct an avenger turret" in { + val func = Deployables.Make(DeployedItem.portable_manned_turret_tr) + func() match { + case obj : TurretDeployable if obj.Definition == GlobalDefinitions.portable_manned_turret_tr => ok + case _ => ko + } + } + + "construct an aegis shield generator" in { + val func = Deployables.Make(DeployedItem.deployable_shield_generator) + func() match { + case obj : ShieldGeneratorDeployable if obj.Definition == GlobalDefinitions.deployable_shield_generator => ok + case _ => ko + } + } + + "construct a telepad" in { + val func = Deployables.Make(DeployedItem.router_telepad_deployable) + func() match { + case obj : TelepadDeployable if obj.Definition == GlobalDefinitions.router_telepad_deployable => ok + case _ => ko + } + } + + "construct an osprey turret" in { + val func = Deployables.Make(DeployedItem.portable_manned_turret_nc) + func() match { + case obj : TurretDeployable if obj.Definition == GlobalDefinitions.portable_manned_turret_nc => ok + case _ => ko + } + } + + "construct an orion turret" in { + val func = Deployables.Make(DeployedItem.portable_manned_turret_vs) + func() match { + case obj : TurretDeployable if obj.Definition == GlobalDefinitions.portable_manned_turret_vs => ok + case _ => ko + } + } + } +} + class ShieldGeneratorDeployableTest extends Specification { "ShieldGeneratorDeployable" should { "construct" in { @@ -158,6 +293,291 @@ class ShieldGeneratorDeployableTest extends Specification { } } + +class ExplosiveDeployableJammerTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val localProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.LocalEvents = localProbe.ref + + val j_mine = Deployables.Make(DeployedItem.jammer_mine)().asInstanceOf[ExplosiveDeployable] //guid=1 + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + player2.Spawn + val weapon = Tool(GlobalDefinitions.jammer_grenade) //guid=5 + guid.register(j_mine, 1) + guid.register(player1, 3) + guid.register(player2, 4) + guid.register(weapon, 5) + j_mine.Zone = zone + j_mine.Owner = player2 + j_mine.OwnerName = player2.Name + j_mine.Faction = PlanetSideEmpire.NC + j_mine.Actor = system.actorOf(Props(classOf[ExplosiveDeployableControl], j_mine), "j-mine-control") + + val jMineSource = SourceEntry(j_mine) + val pSource = PlayerSource(player1) + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + jMineSource, + j_mine.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageToJ = resolved.damage_model.Calculate(resolved) + + "ExplosiveDeployable" should { + "handle being jammered appropriately (no detonation)" in { + assert(!j_mine.Destroyed) + + j_mine.Actor ! Vitality.Damage(applyDamageToJ) + val msg_local = localProbe.receiveN(4, 200 milliseconds) + val msg_avatar = avatarProbe.receiveOne(200 milliseconds) + activityProbe.expectNoMsg(200 milliseconds) + assert( + msg_local.head match { + case LocalServiceMessage("TestCharacter2", LocalAction.AlertDestroyDeployable(PlanetSideGUID(0), target)) => target eq j_mine + case _ => false + } + ) + assert( + msg_local(1) match { + case LocalServiceMessage("NC", LocalAction.DeployableMapIcon( + PlanetSideGUID(0), + DeploymentAction.Dismiss, + DeployableInfo(PlanetSideGUID(1), DeployableIcon.DisruptorMine, _, PlanetSideGUID(0)) + )) => true + case _ => false + } + ) + assert( + msg_local(2) match { + case LocalServiceMessage.Deployables(SupportActor.ClearSpecific(List(target), _zone)) => (target eq j_mine) && (_zone eq zone) + case _ => false + } + ) + assert( + msg_local(3) match { + case LocalServiceMessage.Deployables(RemoverActor.AddTask(target, _zone, _)) => (target eq j_mine) && (_zone eq zone) + case _ => false + } + ) + assert( + msg_avatar match { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(1), _, Service.defaultPlayerGUID, _)) => true + case _ => false + } + ) + assert(j_mine.Destroyed) + } + } +} + +class ExplosiveDeployableJammerExplodeTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val localProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.LocalEvents = localProbe.ref + + val h_mine = Deployables.Make(DeployedItem.he_mine)().asInstanceOf[ExplosiveDeployable] //guid=2 + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + player2.Spawn + val weapon = Tool(GlobalDefinitions.jammer_grenade) //guid=5 + guid.register(h_mine, 2) + guid.register(player1, 3) + guid.register(player2, 4) + guid.register(weapon, 5) + h_mine.Zone = zone + h_mine.Owner = player2 + h_mine.OwnerName = player2.Name + h_mine.Faction = PlanetSideEmpire.NC + h_mine.Actor = system.actorOf(Props(classOf[ExplosiveDeployableControl], h_mine), "h-mine-control") + + val hMineSource = SourceEntry(h_mine) + val pSource = PlayerSource(player1) + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + hMineSource, + h_mine.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageToH = resolved.damage_model.Calculate(resolved) + + "ExplosiveDeployable" should { + "handle being jammered appropriately (detonation)" in { + assert(!h_mine.Destroyed) + + h_mine.Actor ! Vitality.Damage(applyDamageToH) + val msg_local = localProbe.receiveN(5, 200 milliseconds) + val msg_avatar = avatarProbe.receiveOne(200 milliseconds) + val msg_activity = activityProbe.receiveOne(200 milliseconds) + assert( + msg_local.head match { + case LocalServiceMessage("test", LocalAction.Detonate(PlanetSideGUID(2), target)) => target eq h_mine + case _ => false + } + ) + assert( + msg_local(1) match { + case LocalServiceMessage("TestCharacter2", LocalAction.AlertDestroyDeployable(PlanetSideGUID(0), target)) => target eq h_mine + case _ => false + } + ) + assert( + msg_local(2) match { + case LocalServiceMessage("NC", LocalAction.DeployableMapIcon( + PlanetSideGUID(0), + DeploymentAction.Dismiss, + DeployableInfo(PlanetSideGUID(2), DeployableIcon.HEMine, _, PlanetSideGUID(0)) + )) => true + case _ => false + } + ) + assert( + msg_local(3) match { + case LocalServiceMessage.Deployables(SupportActor.ClearSpecific(List(target), _zone)) => (target eq h_mine) && (_zone eq zone) + case _ => false + } + ) + assert( + msg_local(4) match { + case LocalServiceMessage.Deployables(RemoverActor.AddTask(target, _zone, _)) => (target eq h_mine) && (_zone eq zone) + case _ => false + } + ) + assert( + msg_avatar match { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, Service.defaultPlayerGUID, _)) => true + case _ => false + } + ) + assert( + msg_activity match { + case Zone.HotSpot.Activity(target, attacker, _) => (target eq hMineSource) && (attacker eq pSource) + case _ => false + } + ) + assert(h_mine.Destroyed) + } + } +} + +class ExplosiveDeployableDestructionTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val localProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.LocalEvents = localProbe.ref + + val h_mine = Deployables.Make(DeployedItem.he_mine)().asInstanceOf[ExplosiveDeployable] //guid=2 + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + player2.Spawn + val weapon = Tool(GlobalDefinitions.suppressor) //guid=5 + guid.register(h_mine, 2) + guid.register(player1, 3) + guid.register(player2, 4) + guid.register(weapon, 5) + h_mine.Zone = zone + h_mine.Owner = player2 + h_mine.OwnerName = player2.Name + h_mine.Faction = PlanetSideEmpire.NC + h_mine.Actor = system.actorOf(Props(classOf[ExplosiveDeployableControl], h_mine), "h-mine-control") + + val hMineSource = SourceEntry(h_mine) + val pSource = PlayerSource(player1) + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), + hMineSource, + h_mine.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + + "ExplosiveDeployable" should { + "handle being destroyed" in { + h_mine.Health = h_mine.Definition.DamageDestroysAt + 1 + assert(h_mine.Health > h_mine.Definition.DamageDestroysAt) + assert(!h_mine.Destroyed) + + h_mine.Actor ! Vitality.Damage(applyDamageTo) + val msg_local = localProbe.receiveN(5, 200 milliseconds) + val msg_avatar = avatarProbe.receiveOne(200 milliseconds) + activityProbe.expectNoMsg(200 milliseconds) + assert( + msg_local.head match { + case LocalServiceMessage("TestCharacter2", LocalAction.AlertDestroyDeployable(PlanetSideGUID(0), target)) => target eq h_mine + case _ => false + } + ) + assert( + msg_local(1) match { + case LocalServiceMessage("NC", LocalAction.DeployableMapIcon( + PlanetSideGUID(0), + DeploymentAction.Dismiss, + DeployableInfo(PlanetSideGUID(2), DeployableIcon.HEMine, _, PlanetSideGUID(0)) + )) => true + case _ => false + } + ) + assert( + msg_local(2) match { + case LocalServiceMessage.Deployables(SupportActor.ClearSpecific(List(target), _zone)) => (target eq h_mine) && (_zone eq zone) + case _ => false + } + ) + assert( + msg_local(3) match { + case LocalServiceMessage.Deployables(RemoverActor.AddTask(target, _zone, _)) => (target eq h_mine) && (_zone eq zone) + case _ => false + } + ) + assert( + msg_local(4) match { + case LocalServiceMessage("test", LocalAction.TriggerEffect(_, "detonate_damaged_mine", PlanetSideGUID(2))) => true + case _ => false + } + ) + assert( + msg_avatar match { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, Service.defaultPlayerGUID, _)) => true + case _ => false + } + ) + assert(h_mine.Health <= h_mine.Definition.DamageDestroysAt) + assert(h_mine.Destroyed) + } + } +} class TurretControlConstructTest extends ActorTest { "TurretControl" should { "construct" in { diff --git a/common/src/test/scala/objects/FacilityTurretTest.scala b/common/src/test/scala/objects/FacilityTurretTest.scala index 28b4c28e8..72d9664da 100644 --- a/common/src/test/scala/objects/FacilityTurretTest.scala +++ b/common/src/test/scala/objects/FacilityTurretTest.scala @@ -2,15 +2,22 @@ package objects import akka.actor.{ActorRef, Props} +import akka.testkit.TestProbe import base.ActorTest import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Tool} import net.psforever.objects.definition.ToolDefinition +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.source.LimitedNumberSource +import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.serverobject.turret._ -import net.psforever.objects.zones.Zone -import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, PlanetSideGUID} +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.packet.game.{InventoryStateMessage, RepairMessage} +import net.psforever.types._ import org.specs2.mutable.Specification +import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.vehicle.{VehicleAction, VehicleServiceMessage} import scala.collection.mutable import scala.concurrent.duration._ @@ -22,7 +29,7 @@ class FacilityTurretTest extends Specification { obj.Weapons mustEqual mutable.HashMap.empty[TurretUpgrade.Value, ToolDefinition] obj.ReserveAmmunition mustEqual false obj.FactionLocked mustEqual true - obj.MaxHealth mustEqual 100 + obj.MaxHealth mustEqual 0 obj.MountPoints mustEqual mutable.HashMap.empty[Int,Int] } @@ -36,17 +43,13 @@ class FacilityTurretTest extends Specification { ko } obj.Seats.size mustEqual 1 - obj.Seats(0).ControlledWeapon mustEqual Some(1) + obj.Seats(0).ControlledWeapon.contains(1) mustEqual true obj.MountPoints.size mustEqual 1 obj.MountPoints(1) mustEqual 0 obj.Health mustEqual 3600 obj.Upgrade mustEqual TurretUpgrade.None - obj.Jammered mustEqual false - obj.Health = 360 obj.Health mustEqual 360 - obj.Jammered = true - obj.Jammered mustEqual true } "upgrade to a different weapon" in { @@ -176,3 +179,97 @@ class FacilityTurretControl4Test extends ActorTest { } } } + +class FacilityTurretControlRestorationTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.VehicleEvents = vehicleProbe.ref + building.Actor = buildingProbe.ref + + val turret = new FacilityTurret(GlobalDefinitions.manned_turret) //2, 5, 6 + turret.Actor = system.actorOf(Props(classOf[FacilityTurretControl], turret), "turret-control") + turret.Zone = zone + turret.Position = Vector3(1, 0, 0) + val turretWeapon = turret.Weapons.values.head.Equipment.get.asInstanceOf[Tool] + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + player1.Position = Vector3(2, 2, 2) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + + guid.register(building, 1) + guid.register(turret, 2) + guid.register(player1, 3) + guid.register(turretWeapon, 5) + guid.register(turretWeapon.AmmoSlot.Box, 6) + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = turret + + val tool = Tool(GlobalDefinitions.nano_dispenser) //7 & 8 + guid.register(tool, 7) + guid.register(tool.AmmoSlot.Box, 8) + + "RepairableTurretWeapon" should { + "handle repairs and restoration" in { + turret.Health = turret.Definition.RepairRestoresAt - 1 //initial state manip + turret.Destroyed = true //initial state manip + assert(turret.Health < turret.Definition.RepairRestoresAt) + assert(turret.Destroyed) + + turret.Actor ! CommonMessages.Use(player1, Some(tool)) + val msg12345 = avatarProbe.receiveN(5, 500 milliseconds) + val msg4 = vehicleProbe.receiveOne(500 milliseconds) + assert( + msg12345.head match { + case AvatarServiceMessage("TestCharacter1", + AvatarAction.SendResponse(PlanetSideGUID(0), InventoryStateMessage(PlanetSideGUID(8), _, PlanetSideGUID(7), _))) => true + case _ => false + } + ) + assert( + msg12345(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg12345(2) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 50, 0)) => true + case _ => false + } + ) + assert( + msg12345(3) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 51, 0)) => true + case _ => false + } + ) + assert( + msg12345(4) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(PlanetSideGUID(0), RepairMessage(PlanetSideGUID(2), _))) => true + case _ => false + } + ) + assert( + msg4 match { + case VehicleServiceMessage("test", VehicleAction.EquipmentInSlot(_, PlanetSideGUID(2), 1, t)) if t eq turretWeapon => true + case _ => false + } + ) + assert(turret.Health > turret.Definition.RepairRestoresAt) + assert(!turret.Destroyed) + } + } +} diff --git a/common/src/test/scala/objects/GeneratorTest.scala b/common/src/test/scala/objects/GeneratorTest.scala new file mode 100644 index 000000000..aadae78c7 --- /dev/null +++ b/common/src/test/scala/objects/GeneratorTest.scala @@ -0,0 +1,766 @@ +// Copyright (c) 2020 PSForever +package objects + +import akka.actor.{ActorRef, Props} +import akka.testkit.TestProbe +import base.ActorTest +import net.psforever.objects.ballistics._ +import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Tool} +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.source.LimitedNumberSource +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl} +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.vital.Vitality +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.packet.game.{InventoryStateMessage, RepairMessage, TriggerEffectMessage} +import net.psforever.types._ +import org.specs2.mutable.Specification +import services.avatar.{AvatarAction, AvatarServiceMessage} + +import scala.concurrent.duration._ + +class GeneratorTest extends Specification { + "Generator" should { + "construct" in { + Generator(GlobalDefinitions.generator) + ok + } + + "start in 'Normal' condition" in { + val obj = Generator(GlobalDefinitions.generator) + obj.Condition mustEqual PlanetSideGeneratorState.Normal + } + } +} + +class GeneratorControlConstructTest extends ActorTest { + "GeneratorControl" should { + "construct" in { + val gen = Generator(GlobalDefinitions.generator) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "gen-control") + assert(gen.Actor != ActorRef.noSender) + } + } +} + +class GeneratorControlDamageTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(5)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = { } + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + val activityProbe = TestProbe() + zone.Activity = activityProbe.ref + + val gen = Generator(GlobalDefinitions.generator) //guid=2 + gen.Position = Vector3(1, 0, 0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Position = Vector3(14, 0, 0) //<14m from generator; dies + player1.Spawn + + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = gen + building.PlayersInSOI = List(player1) + val buildingProbe = TestProbe() + building.Actor = buildingProbe.ref + + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(gen), + gen.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "GeneratorControl" should { + "handle damage" in { + assert(gen.Health == gen.Definition.MaxHealth) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Normal) + + gen.Actor ! Vitality.Damage(applyDamageTo) + val msg_avatar = avatarProbe.receiveN(2, 500 milliseconds) + buildingProbe.expectNoMsg(200 milliseconds) + assert( + msg_avatar.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg_avatar(1) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 15)) => true + case _ => false + } + ) + assert(gen.Health < gen.Definition.MaxHealth) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Normal) + } + } +} + +class GeneratorControlCriticalTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(5)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = { } + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + val activityProbe = TestProbe() + zone.Activity = activityProbe.ref + + val gen = Generator(GlobalDefinitions.generator) //guid=2 + gen.Position = Vector3(1, 0, 0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Position = Vector3(14, 0, 0) //<14m from generator; dies + player1.Spawn + + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = gen + building.PlayersInSOI = List(player1) + val buildingProbe = TestProbe() + building.Actor = buildingProbe.ref + + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(gen), + gen.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + val halfHealth = gen.Definition.MaxHealth / 2 + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "GeneratorControl" should { + "handle damage through the generator's critical state" in { + gen.Health = halfHealth + 1 //no matter what, the next shot pushes it to critical status + assert(gen.Health > halfHealth) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Normal) + + gen.Actor ! Vitality.Damage(applyDamageTo) + val msg_avatar = avatarProbe.receiveN(2, 500 milliseconds) + val msg_building = buildingProbe.receiveOne(500 milliseconds) + assert( + msg_avatar.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg_avatar(1) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 15)) => true + case _ => false + } + ) + assert( + msg_building match { + case Building.AmenityStateChange(o) => o eq gen + case _ => false + } + ) + assert(gen.Health < halfHealth) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Critical) + } + } +} + +class GeneratorControlDestroyedTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(5)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = { } + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + val activityProbe = TestProbe() + zone.Activity = activityProbe.ref + + val gen = Generator(GlobalDefinitions.generator) //guid=2 + gen.Position = Vector3(1, 0, 0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Position = Vector3(14, 0, 0) //<14m from generator; dies + player1.Spawn + player1.Actor = TestProbe().ref + + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = gen + building.PlayersInSOI = List(player1) + val buildingProbe = TestProbe() + building.Actor = buildingProbe.ref + + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(gen), + gen.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "GeneratorControl" should { + "handle damage until destroyed" in { + gen.Health = 1 //no matter what, the next shot destroys the generator + assert(gen.Health == 1) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50% + + gen.Actor ! Vitality.Damage(applyDamageTo) + val msg_avatar1 = avatarProbe.receiveOne(500 milliseconds) + buildingProbe.expectNoMsg(200 milliseconds) + assert( + msg_avatar1 match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => true + case _ => false + } + ) + assert(gen.Health == 1) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Normal) + + avatarProbe.expectNoMsg(9 seconds) + buildingProbe.expectNoMsg(50 milliseconds) //no prior messages + val msg_avatar2 = avatarProbe.receiveN(3, 1000 milliseconds) //see DamageableEntity test file + val msg_building = buildingProbe.receiveOne(200 milliseconds) + assert( + msg_building match { + case Building.AmenityStateChange(o) => o eq gen + case _ => false + } + ) + assert( + msg_avatar2.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg_avatar2(1) match { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, _, Vector3(1, 0, 0))) => true + case _ => false + } + ) + assert( + msg_avatar2(2) match { + case AvatarServiceMessage("test", + AvatarAction.SendResponse(_, TriggerEffectMessage(PlanetSideGUID(2), "explosion_generator", None, None)) + ) => true + case _ => false + } + ) + assert(gen.Health == 0) + assert(gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Destroyed) + } + } +} + +class GeneratorControlKillsTest extends ActorTest { + /* + to perform this test, players need to be added to the SOI organization of the test base in proximity of the generator + under normal player scenario, this is an automatic process + extending from the act of players being in a zone + and players being within the SOI radius from the center of a facility on a periodic check + the test base being used has no established SOI region or automatic SOI check refresh, + but its SOI information can be loaded with the players manually + the players need something to catch the die message + */ + val guid = new NumberPoolHub(new LimitedNumberSource(5)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = { } + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + val activityProbe = TestProbe() + zone.Activity = activityProbe.ref + + val gen = Generator(GlobalDefinitions.generator) //guid=2 + gen.Position = Vector3(1, 0, 0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Position = Vector3(14, 0, 0) //<14m from generator; dies + player1.Spawn + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Female, 1, CharacterVoice.Mute)) //guid=4 + player2.Position = Vector3(15, 0, 0) //>14m from generator; lives + player2.Spawn + val player2Probe = TestProbe() + player2.Actor = player2Probe.ref + + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = gen + building.PlayersInSOI = List(player1, player2) + val buildingProbe = TestProbe() + building.Actor = buildingProbe.ref + + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + guid.register(player2, 4) + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(gen), + gen.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "GeneratorControl" should { + "kill players when the generator is destroyed" in { + gen.Health = 1 //no matter what, the next shot destroys the generator + assert(gen.Health == 1) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50% + + gen.Actor ! Vitality.Damage(applyDamageTo) + val msg_avatar1 = avatarProbe.receiveN(2, 500 milliseconds) + buildingProbe.expectNoMsg(200 milliseconds) + player1Probe.expectNoMsg(200 milliseconds) + player2Probe.expectNoMsg(200 milliseconds) + assert( + msg_avatar1.head match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => true + case _ => false + } + ) + assert( + msg_avatar1(1) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => true + case _ => false + } + ) + assert(gen.Health == 1) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Normal) + + val msg_building = buildingProbe.receiveOne(10500 milliseconds) + val msg_avatar2 = avatarProbe.receiveN(3, 200 milliseconds) + val msg_player1 = player1Probe.receiveOne(100 milliseconds) + player2Probe.expectNoMsg(200 milliseconds) + assert( + msg_building match { + case Building.AmenityStateChange(o) => o eq gen + case _ => false + } + ) + assert( + msg_avatar2.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg_avatar2(1) match { + case AvatarServiceMessage("test", AvatarAction.Destroy(PlanetSideGUID(2), _, _, Vector3(1, 0, 0))) => true + case _ => false + } + ) + assert( + msg_avatar2(2) match { + case AvatarServiceMessage("test", + AvatarAction.SendResponse(_, TriggerEffectMessage(PlanetSideGUID(2), "explosion_generator", None, None)) + ) => true + case _ => false + } + ) + assert( + msg_player1 match { + case _ @ Player.Die() => true + case _ => false + } + ) + assert(gen.Health == 0) + assert(gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Destroyed) + } + } +} + +class GeneratorControlNotDestroyTwice extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val gen = Generator(GlobalDefinitions.generator) //guid=2 + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = gen + gen.Position = Vector3(1, 0, 0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + building.Actor = buildingProbe.ref + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(gen), + gen.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "GeneratorControl" should { + "not send a status update if destroyed and partially repaired, but destroyed again" in { + //damaged, not yet restored, but will not be destroyed again within one shot + val originalHealth = gen.Health = gen.Definition.DamageDestroysAt + 1 + gen.Condition = PlanetSideGeneratorState.Destroyed //initial state manip + gen.Destroyed = true + assert(gen.Destroyed) + assert(originalHealth < gen.Definition.DefaultHealth) + assert(originalHealth < gen.Definition.RepairRestoresAt) + assert(originalHealth > gen.Definition.DamageDestroysAt) + + gen.Actor ! Vitality.Damage(applyDamageTo) + avatarProbe.expectNoMsg(500 milliseconds) + activityProbe.receiveOne(500 milliseconds) + buildingProbe.expectNoMsg(1000 milliseconds) + assert(gen.Health < originalHealth) + assert(gen.Destroyed) + assert(originalHealth < gen.Definition.DefaultHealth) + assert(originalHealth < gen.Definition.RepairRestoresAt) + assert(gen.Health <= gen.Definition.DamageDestroysAt) + + //damaged, not yet restored, and would have been destroyed with next shot + gen.Health = 1 + assert(gen.Health == 1) + assert(gen.Destroyed) + gen.Actor ! Vitality.Damage(applyDamageTo) + avatarProbe.expectNoMsg(500 milliseconds) + activityProbe.receiveOne(500 milliseconds) //activity alert occurs because this was not a kill shot + buildingProbe.expectNoMsg(1000 milliseconds) + assert(gen.Health == 0) + assert(gen.Destroyed) + } + } +} + +class GeneratorControlNotDamageIfExplodingTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(5)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = { } + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + val activityProbe = TestProbe() + zone.Activity = activityProbe.ref + + val gen = Generator(GlobalDefinitions.generator) //guid=2 + gen.Position = Vector3(1, 0, 0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Position = Vector3(14, 0, 0) //<14m from generator; dies + player1.Spawn + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = gen + building.PlayersInSOI = List(player1) + val buildingProbe = TestProbe() + building.Actor = buildingProbe.ref + + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(gen), + gen.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "GeneratorControl" should { + "not damage if the generator is going to explode" in { + gen.Health = 1 //no matter what, the next shot destroys the generator + assert(gen.Health == 1) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50% + + gen.Actor ! Vitality.Damage(applyDamageTo) + val msg_avatar = avatarProbe.receiveOne(500 milliseconds) + buildingProbe.expectNoMsg(200 milliseconds) + player1Probe.expectNoMsg(200 milliseconds) + assert( + msg_avatar match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => true + case _ => false + } + ) + assert(gen.Health == 1) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Normal) + //going to explode state + + //once + gen.Actor ! Vitality.Damage(applyDamageTo) + avatarProbe.expectNoMsg(500 milliseconds) + buildingProbe.expectNoMsg(200 milliseconds) + player1Probe.expectNoMsg(200 milliseconds) + assert(gen.Health == 1) + //twice + gen.Actor ! Vitality.Damage(applyDamageTo) + avatarProbe.expectNoMsg(500 milliseconds) + buildingProbe.expectNoMsg(200 milliseconds) + player1Probe.expectNoMsg(200 milliseconds) + assert(gen.Health == 1) + } + } +} + +class GeneratorControlNotRepairIfExplodingTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(5)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = { } + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + val activityProbe = TestProbe() + zone.Activity = activityProbe.ref + + val gen = Generator(GlobalDefinitions.generator) //guid=2 + gen.Position = Vector3(1, 0, 0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Position = Vector3(14, 0, 0) //<14m from generator; dies + player1.Spawn + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = gen + building.PlayersInSOI = List(player1) + val buildingProbe = TestProbe() + building.Actor = buildingProbe.ref + + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + + val weapon = Tool(GlobalDefinitions.phoenix) //decimator + val projectile = weapon.Projectile + val resolved = ResolvedProjectile( + ProjectileResolution.Splash, + Projectile(projectile, weapon.Definition, weapon.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(gen), + gen.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + + val tool = Tool(GlobalDefinitions.nano_dispenser) //4 & 5 + guid.register(tool, 4) + guid.register(tool.AmmoSlot.Box, 5) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "GeneratorControl" should { + "not repair if the generator is going to explode" in { + gen.Health = 1 //no matter what, the next shot destroys the generator + assert(gen.Health == 1) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Normal) //skipped critical state because didn't transition ~50% + + gen.Actor ! Vitality.Damage(applyDamageTo) + val msg_avatar1 = avatarProbe.receiveOne(500 milliseconds) + buildingProbe.expectNoMsg(200 milliseconds) + player1Probe.expectNoMsg(200 milliseconds) + assert( + msg_avatar1 match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 16)) => true + case _ => false + } + ) + assert(gen.Health == 1) + assert(!gen.Destroyed) + assert(gen.Condition == PlanetSideGeneratorState.Normal) + //going to explode state + + //once + gen.Actor ! CommonMessages.Use(player1, Some(tool)) //repair? + avatarProbe.expectNoMsg(1000 milliseconds) //no messages + buildingProbe.expectNoMsg(200 milliseconds) + player1Probe.expectNoMsg(200 milliseconds) + assert(gen.Health == 1) + //twice + gen.Actor ! CommonMessages.Use(player1, Some(tool)) //repair? + avatarProbe.expectNoMsg(1000 milliseconds) //no messages + buildingProbe.expectNoMsg(200 milliseconds) + player1Probe.expectNoMsg(200 milliseconds) + assert(gen.Health == 1) + } + } +} + +class GeneratorControlRepairPastRestorePoint extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(5)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = { } + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + val activityProbe = TestProbe() + zone.Activity = activityProbe.ref + + val gen = Generator(GlobalDefinitions.generator) //guid=2 + gen.Position = Vector3(1, 0, 0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Position = Vector3(14, 0, 0) //<14m from generator; dies + player1.Spawn + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = gen + building.PlayersInSOI = List(player1) + val buildingProbe = TestProbe() + building.Actor = buildingProbe.ref + + val tool = Tool(GlobalDefinitions.nano_dispenser) //4 & 5 + + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + guid.register(tool, 4) + guid.register(tool.AmmoSlot.Box, 5) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "GeneratorControl" should { + "send a status update if destroyed and repairing past the restoration point" in { + val originalHealth = gen.Health = gen.Definition.RepairRestoresAt - 1 //damage + gen.Condition = PlanetSideGeneratorState.Destroyed //initial state manip + gen.Destroyed = true + assert(originalHealth < gen.Definition.DefaultHealth) + assert(originalHealth < gen.Definition.RepairRestoresAt) + assert(gen.Destroyed) + + gen.Actor ! CommonMessages.Use(player1, Some(tool)) //repair + val msg_avatar = avatarProbe.receiveN(4, 500 milliseconds) //expected + val msg_building = buildingProbe.receiveOne(200 milliseconds) + assert( + msg_avatar.head match { + case AvatarServiceMessage("TestCharacter1", + AvatarAction.SendResponse(_, InventoryStateMessage(ValidPlanetSideGUID(5), _, ValidPlanetSideGUID(4), _)) + ) => true + case _ => false + } + ) + assert( + msg_avatar(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg_avatar(2) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.GenericObjectAction(_, PlanetSideGUID(1), 17)) => true + case _ => false + } + ) + assert( + msg_avatar(3) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(_, RepairMessage(ValidPlanetSideGUID(2), _))) => true + case _ => false + } + ) + assert( + msg_building match { + case Building.AmenityStateChange(o) => o eq gen + case _ => false + } + ) + assert(gen.Condition == PlanetSideGeneratorState.Normal) + assert(gen.Health > gen.Definition.RepairRestoresAt) + assert(!gen.Destroyed) + } + } +} diff --git a/common/src/test/scala/objects/IFFLockTest.scala b/common/src/test/scala/objects/IFFLockTest.scala index 9259cd93b..fe60a5304 100644 --- a/common/src/test/scala/objects/IFFLockTest.scala +++ b/common/src/test/scala/objects/IFFLockTest.scala @@ -60,7 +60,7 @@ class IFFLockControl2Test extends ActorTest { player.GUID = PlanetSideGUID(1) assert(lock.HackedBy.isEmpty) - lock.Actor ! CommonMessages.Hack(player) + lock.Actor ! CommonMessages.Hack(player, lock) Thread.sleep(500L) //blocking assert(lock.HackedBy.nonEmpty) //TODO rewrite later } @@ -74,7 +74,7 @@ class IFFLockControl3Test extends ActorTest { player.GUID = PlanetSideGUID(1) assert(lock.HackedBy.isEmpty) - lock.Actor ! CommonMessages.Hack(player) + lock.Actor ! CommonMessages.Hack(player, lock) Thread.sleep(500L) //blocking assert(lock.HackedBy.nonEmpty) //TODO rewrite later lock.Actor ! CommonMessages.ClearHack() diff --git a/common/src/test/scala/objects/PlayerControlTest.scala b/common/src/test/scala/objects/PlayerControlTest.scala new file mode 100644 index 000000000..6092b610f --- /dev/null +++ b/common/src/test/scala/objects/PlayerControlTest.scala @@ -0,0 +1,599 @@ +// Copyright (c) 2020 PSForever +package objects + +import akka.actor.Props +import akka.testkit.TestProbe +import base.ActorTest +import net.psforever.objects.avatar.PlayerControl +import net.psforever.objects.ballistics._ +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.source.LimitedNumberSource +import net.psforever.objects.vital.Vitality +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.objects._ +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.packet.game._ +import net.psforever.types._ +import services.Service +import services.avatar.{AvatarAction, AvatarServiceMessage} + +import scala.concurrent.duration._ + +class PlayerControlHealTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(15)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + player1.Zone = zone + player1.Spawn + player1.Position = Vector3(2, 0, 0) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player2.Zone = zone + player2.Spawn + player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") + + val tool = Tool(GlobalDefinitions.medicalapplicator) //guid=3 & 4 + + guid.register(player1, 1) + guid.register(player2, 2) + guid.register(tool, 3) + guid.register(tool.AmmoSlot.Box, 4) + + "PlayerControl" should { + "handle being healed by another player" in { + val originalHealth = player2.Health = 0 //initial state manip + val originalMagazine = tool.Magazine + assert(originalHealth < player2.MaxHealth) + + player2.Actor ! CommonMessages.Use(player1, Some(tool)) + val msg_avatar = avatarProbe.receiveN(4, 500 milliseconds) + assert( + msg_avatar.head match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(_, InventoryStateMessage(PlanetSideGUID(4), _, PlanetSideGUID(3), _))) => true + case _ => false + } + ) + assert( + msg_avatar(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg_avatar(2) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 55, 1)) => true + case _ => false + } + ) + assert( + msg_avatar(3) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(_, RepairMessage(PlanetSideGUID(2), _))) => true + case _ => false + } + ) + val raisedHealth = player2.Health + assert(raisedHealth > originalHealth) + assert(tool.Magazine < originalMagazine) + + player1.Position = Vector3(10,0,0) //moved more than 5m away + player2.Actor ! CommonMessages.Use(player1, Some(tool)) + avatarProbe.expectNoMsg(500 milliseconds) + assert(raisedHealth == player2.Health) + } + } +} + +class PlayerControlHealSelfTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(15)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + player1.Zone = zone + player1.Spawn + player1.Position = Vector3(2, 0, 0) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") + + val tool = Tool(GlobalDefinitions.medicalapplicator) //guid=3 & 4 + + guid.register(player1, 1) + guid.register(tool, 3) + guid.register(tool.AmmoSlot.Box, 4) + + "PlayerControl" should { + "handle healing own self" in { + val originalHealth = player1.Health = 1 //initial state manip + val originalMagazine = tool.Magazine + assert(originalHealth < player1.MaxHealth) + + player1.Actor ! CommonMessages.Use(player1, Some(tool)) + val msg_avatar1 = avatarProbe.receiveN(2, 500 milliseconds) + assert( + msg_avatar1.head match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(_, InventoryStateMessage(PlanetSideGUID(4), _, PlanetSideGUID(3), _))) => true + case _ => false + } + ) + assert( + msg_avatar1(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true + case _ => false + } + ) + val raisedHealth = player1.Health + assert(raisedHealth > originalHealth) + assert(tool.Magazine < originalMagazine) + + player1.Position = Vector3(10,0,0) //trying to move away from oneself doesn't work + player1.Actor ! CommonMessages.Use(player1, Some(tool)) + val msg_avatar2 = avatarProbe.receiveN(2, 500 milliseconds) + assert( + msg_avatar2.head match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(_, InventoryStateMessage(PlanetSideGUID(4), _, PlanetSideGUID(3), _))) => true + case _ => false + } + ) + assert( + msg_avatar2(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true + case _ => false + } + ) + assert(player1.Health > raisedHealth) + } + } +} + +class PlayerControlRepairTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(15)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + player1.Zone = zone + player1.Spawn + player1.Position = Vector3(2, 0, 0) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player2.Zone = zone + player2.Spawn + player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") + + val tool = Tool(GlobalDefinitions.bank) //guid=3 & 4 + + guid.register(player1, 1) + guid.register(player2, 2) + guid.register(tool, 3) + guid.register(tool.AmmoSlot.Box, 4) + + "PlayerControl" should { + "handle being repaired by another player" in { + val originalArmor = player2.Armor = 0 //initial state manip + val originalMagazine = tool.Magazine + assert(originalArmor < player2.MaxArmor) + + player2.Actor ! CommonMessages.Use(player1, Some(tool)) + val msg_avatar = avatarProbe.receiveN(5, 500 milliseconds) + assert( + msg_avatar.head match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(_, InventoryStateMessage(PlanetSideGUID(4), _, PlanetSideGUID(3), _))) => true + case _ => false + } + ) + assert( + msg_avatar(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 4, _)) => true + case _ => false + } + ) + assert( + msg_avatar(2) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 56, 1)) => true + case _ => false + } + ) + assert( + msg_avatar(3) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(_, RepairMessage(PlanetSideGUID(2), _))) => true + case _ => false + } + ) + assert( + msg_avatar(4) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 56, 1)) => true + case _ => false + } + ) + assert(player2.Armor > originalArmor) + assert(tool.Magazine < originalMagazine) + + val fixedArmor = player2.Armor + player1.Position = Vector3(10,0,0) //moved more than 5m away + player2.Actor ! CommonMessages.Use(player1, Some(tool)) + avatarProbe.expectNoMsg(500 milliseconds) + assert(fixedArmor == player2.Armor) + } + } +} + +class PlayerControlRepairSelfTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(15)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + player1.Zone = zone + player1.Spawn + player1.Position = Vector3(2, 0, 0) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") + + val tool = Tool(GlobalDefinitions.bank) //guid=3 & 4 + + guid.register(player1, 1) + guid.register(tool, 3) + guid.register(tool.AmmoSlot.Box, 4) + + "PlayerControl" should { + "handle repairing own self" in { + val originalArmor = player1.Armor = 0 //initial state manip + val originalMagazine = tool.Magazine + assert(originalArmor < player1.MaxArmor) + + player1.Actor ! CommonMessages.Use(player1, Some(tool)) + val msg_avatar1 = avatarProbe.receiveN(2, 500 milliseconds) + assert( + msg_avatar1.head match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(_, InventoryStateMessage(PlanetSideGUID(4), _, PlanetSideGUID(3), _))) => true + case _ => false + } + ) + assert( + msg_avatar1(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 4, _)) => true + case _ => false + } + ) + val fixedArmor = player1.Armor + assert(fixedArmor > originalArmor) + assert(tool.Magazine < originalMagazine) + + player1.Position = Vector3(10,0,0) //trying to move away from oneself doesn't work + player1.Actor ! CommonMessages.Use(player1, Some(tool)) + val msg_avatar2 = avatarProbe.receiveN(2, 500 milliseconds) + assert( + msg_avatar2.head match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(_, InventoryStateMessage(PlanetSideGUID(4), _, PlanetSideGUID(3), _))) => true + case _ => false + } + ) + assert( + msg_avatar2(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 4, _)) => true + case _ => false + } + ) + assert(player1.Armor > fixedArmor) + } + } +} + +class PlayerControlDamageTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(15)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + player1.Zone = zone + player1.Spawn + player1.Position = Vector3(2, 0, 0) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player2.Zone = zone + player2.Spawn + player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") + val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4 + val projectile = tool.Projectile + val playerSource = SourceEntry(player2) + val resolved = ResolvedProjectile( + ProjectileResolution.Hit, + Projectile(projectile, tool.Definition, tool.FireMode, PlayerSource(player1), 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + playerSource, + player1.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + guid.register(player1, 1) + guid.register(player2, 2) + guid.register(tool, 3) + guid.register(tool.AmmoSlot.Box, 4) + expectNoMsg(200 milliseconds) + "PlayerControl" should { + "handle damage" in { + 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_activity = activityProbe.receiveOne(200 milliseconds) + assert( + msg_avatar.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg_avatar(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 4, _)) => true + case _ => false + } + ) + assert( + msg_activity match { + case activity : Zone.HotSpot.Activity => + activity.attacker == PlayerSource(player1) && + activity.defender == playerSource && + activity.location == Vector3(1, 0, 0) + case _ => false + } + ) + assert( + msg_avatar(2) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0)))) => true + case _ => false + } + ) + assert(player2.Health < player2.Definition.DefaultHealth) + assert(player2.Armor < player2.MaxArmor) + } + } +} + +class PlayerControlDeathStandingTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(15)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + val activityProbe = TestProbe() + zone.Activity = activityProbe.ref + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + player1.Zone = zone + player1.Spawn + player1.Position = Vector3(2,0,0) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player2.Zone = zone + player2.Spawn + player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") + + val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4 + val projectile = tool.Projectile + val player1Source = SourceEntry(player1) + val resolved = ResolvedProjectile( + ProjectileResolution.Hit, + Projectile(projectile, tool.Definition, tool.FireMode, player1Source, 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(player2), + player2.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + guid.register(player1, 1) + guid.register(player2, 2) + guid.register(tool, 3) + guid.register(tool.AmmoSlot.Box, 4) + expectNoMsg(200 milliseconds) + + "PlayerControl" should { + "handle death" in { + player2.Health = player2.Definition.DamageDestroysAt + 1 //initial state manip + player2.ExoSuit = ExoSuitType.MAX + player2.Armor = 1 //initial state manip + player2.Capacitor = 1 //initial state manip + assert(player2.Health > player2.Definition.DamageDestroysAt) + assert(player2.Armor == 1) + assert(player2.Capacitor == 1) + assert(player2.isAlive) + + player2.Actor ! Vitality.Damage(applyDamageTo) + val msg_avatar = avatarProbe.receiveN(8, 500 milliseconds) + activityProbe.expectNoMsg(200 milliseconds) + assert( + msg_avatar.head match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 4, _)) => true + case _ => false + } + ) + assert( + msg_avatar(1) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2))) => true + case _ => false + } + ) + assert( + msg_avatar(2) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg_avatar(3) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => true + case _ => false + } + ) + assert( + msg_avatar(4) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 7, _)) => true + case _ => false + } + ) + assert( + msg_avatar(5) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(_, DestroyMessage(PlanetSideGUID(2), PlanetSideGUID(2), _, Vector3.Zero))) => true + case _ => false + } + ) + assert( + msg_avatar(6) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(_, AvatarDeadStateMessage(DeadState.Dead, 300000, 300000, Vector3.Zero, PlanetSideEmpire.NC, true))) => true + case _ => false + } + ) + assert( + msg_avatar(7) match { + case AvatarServiceMessage("test", AvatarAction.DestroyDisplay(killer, victim, _, _)) + if killer == player1Source && victim == PlayerSource(player2) => true + case _ => false + } + ) + assert(player2.Health <= player2.Definition.DamageDestroysAt) + assert(player2.Armor == 0) + assert(!player2.isAlive) + } + } +} + +class PlayerControlDeathSeatedTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(15)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + val activityProbe = TestProbe() + zone.Activity = activityProbe.ref + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + player1.Zone = zone + player1.Spawn + player1.Position = Vector3(2,0,0) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player2.Zone = zone + player2.Spawn + player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") + + val vehicle = Vehicle(GlobalDefinitions.quadstealth) //guid=5 + vehicle.Faction = player2.Faction + + val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4 + val projectile = tool.Projectile + val player1Source = SourceEntry(player1) + val resolved = ResolvedProjectile( + ProjectileResolution.Hit, + Projectile(projectile, tool.Definition, tool.FireMode, player1Source, 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), + SourceEntry(player2), + player2.DamageModel, + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.damage_model.Calculate(resolved) + guid.register(player1, 1) + guid.register(player2, 2) + guid.register(tool, 3) + guid.register(tool.AmmoSlot.Box, 4) + guid.register(vehicle, 5) + expectNoMsg(200 milliseconds) + + "PlayerControl" should { + "handle death when seated (in something)" in { + player2.Health = player2.Definition.DamageDestroysAt + 1 //initial state manip + player2.VehicleSeated = vehicle.GUID //initial state manip, anything + vehicle.Seats(0).Occupant = player2 + player2.Armor = 0 //initial state manip + assert(player2.Health > player2.Definition.DamageDestroysAt) + assert(player2.isAlive) + + player2.Actor ! Vitality.Damage(applyDamageTo) + val msg_avatar = avatarProbe.receiveN(9, 500 milliseconds) + activityProbe.expectNoMsg(200 milliseconds) + assert( + msg_avatar.head match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2))) => true + case _ => false + } + ) + assert( + msg_avatar(1) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(_, + ObjectDetachMessage(PlanetSideGUID(5), PlanetSideGUID(2), _, _, _, _)) + ) => true + case _ => false + } + ) + assert( + msg_avatar(2) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 29, 1)) => true + case _ => false + } + ) + assert( + msg_avatar(3) 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 _ => false + } + ) + assert( + msg_avatar(6) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(_, DestroyMessage(PlanetSideGUID(2), PlanetSideGUID(2), _, Vector3.Zero))) => true + case _ => false + } + ) + assert( + msg_avatar(7) match { + case AvatarServiceMessage("TestCharacter2", AvatarAction.SendResponse(_, AvatarDeadStateMessage(DeadState.Dead, 300000, 300000, Vector3.Zero, PlanetSideEmpire.NC, true))) => true + case _ => false + } + ) + assert( + msg_avatar(8) match { + case AvatarServiceMessage("test", AvatarAction.DestroyDisplay(killer, victim, _, _)) + if killer == player1Source && victim == PlayerSource(player2) => true + case _ => false + } + ) + assert(player2.Health <= player2.Definition.DamageDestroysAt) + assert(!player2.isAlive) + } + } +} + + +object PlayerControlTest { } diff --git a/common/src/test/scala/objects/PlayerTest.scala b/common/src/test/scala/objects/PlayerTest.scala index 3e312562c..69507fec2 100644 --- a/common/src/test/scala/objects/PlayerTest.scala +++ b/common/src/test/scala/objects/PlayerTest.scala @@ -127,9 +127,12 @@ class PlayerTest extends Specification { obj.MaxStamina mustEqual 100 obj.MaxHealth = 123 obj.MaxStamina = 456 - obj.Spawn - obj.Health mustEqual 123 - obj.Stamina mustEqual 456 + obj.MaxHealth mustEqual 123 + obj.MaxStamina mustEqual 456 + obj.MaxHealth = None + //MaxStamina has no equivalent + obj.MaxHealth mustEqual 100 + obj.MaxStamina mustEqual 456 } // "set new values (health, armor, stamina) but only when alive" in { diff --git a/common/src/test/scala/objects/ProjectileTest.scala b/common/src/test/scala/objects/ProjectileTest.scala index edd14a168..315c8c288 100644 --- a/common/src/test/scala/objects/ProjectileTest.scala +++ b/common/src/test/scala/objects/ProjectileTest.scala @@ -183,7 +183,7 @@ class ProjectileTest extends Specification { SourceEntry(fury) match { case o : VehicleSource => o.Name mustEqual "Fury" - o.Faction mustEqual PlanetSideEmpire.TR + o.Faction mustEqual PlanetSideEmpire.NEUTRAL o.Definition mustEqual GlobalDefinitions.fury o.Health mustEqual 650 o.Shields mustEqual 0 @@ -303,13 +303,12 @@ class ProjectileTest extends Specification { val fury_dm = fury.DamageModel "construct" in { - val obj = ResolvedProjectile(ProjectileResolution.Hit, projectile, PlayerSource(player2), fury_dm, Vector3(1.2f, 3.4f, 5.6f), 123456L) + val obj = ResolvedProjectile(ProjectileResolution.Hit, projectile, PlayerSource(player2), fury_dm, Vector3(1.2f, 3.4f, 5.6f)) obj.resolution mustEqual ProjectileResolution.Hit obj.projectile mustEqual projectile obj.target mustEqual p2_source obj.damage_model mustEqual fury.DamageModel obj.hit_pos mustEqual Vector3(1.2f, 3.4f, 5.6f) - obj.hit_time mustEqual 123456L } } } diff --git a/common/src/test/scala/objects/RepairableTest.scala b/common/src/test/scala/objects/RepairableTest.scala new file mode 100644 index 000000000..f154b02db --- /dev/null +++ b/common/src/test/scala/objects/RepairableTest.scala @@ -0,0 +1,400 @@ +// Copyright (c) 2020 PSForever +package objects + +import akka.actor.Props +import akka.testkit.TestProbe +import base.ActorTest +import net.psforever.objects._ +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.source.LimitedNumberSource +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl} +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.serverobject.terminals.{Terminal, TerminalControl} +import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretControl} +import net.psforever.objects.vehicles.VehicleControl +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.packet.game.{InventoryStateMessage, RepairMessage} +import net.psforever.types._ +import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.vehicle.{VehicleAction, VehicleServiceMessage} + +import scala.concurrent.duration._ + +/* +the generator is used to test basic entity repair +essentially, treat it more as a generic entity whose object type is repairable +see GeneratorTest in relation to what the generator does above and beyond that during repair +*/ +class RepairableEntityRepairTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val gen = Generator(GlobalDefinitions.generator) //guid=2 + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = gen + gen.Position = Vector3(1, 0, 0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + building.Actor = buildingProbe.ref + val tool = Tool(GlobalDefinitions.nano_dispenser) //4 & 5 + guid.register(tool, 4) + guid.register(tool.AmmoSlot.Box, 5) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "RepairableEntity" should { + "handle repairs" in { + assert(gen.Health == gen.Definition.DefaultHealth) //ideal + val originalHealth = gen.Health -= 50 + assert(gen.Health < gen.Definition.DefaultHealth) //damage + gen.Actor ! CommonMessages.Use(player1, Some(tool)) //repair + + val msg123 = avatarProbe.receiveN(3, 500 milliseconds) + assert( + msg123.head match { + case AvatarServiceMessage("TestCharacter1", + AvatarAction.SendResponse(PlanetSideGUID(0), InventoryStateMessage(PlanetSideGUID(5), _, PlanetSideGUID(4), _))) => true + case _ => false + } + ) + assert( + msg123(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg123(2) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(PlanetSideGUID(0), RepairMessage(PlanetSideGUID(2), _))) => true + case _ => false + } + ) + assert(originalHealth < gen.Health) //generator repaired a bit + } + } +} + +class RepairableEntityNotRepairTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val gen = Generator(GlobalDefinitions.generator) //guid=2 + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + guid.register(building, 1) + guid.register(gen, 2) + guid.register(player1, 3) + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = gen + gen.Position = Vector3(1, 0, 0) + gen.Actor = system.actorOf(Props(classOf[GeneratorControl], gen), "generator-control") + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + building.Actor = buildingProbe.ref + val tool = Tool(GlobalDefinitions.nano_dispenser) //4 & 5 + guid.register(tool, 4) + guid.register(tool.AmmoSlot.Box, 5) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "RepairableEntity" should { + "not repair if health is already full" in { + assert(gen.Health == gen.Definition.DefaultHealth) //ideal + gen.Actor ! CommonMessages.Use(player1, Some(tool)) //repair? + avatarProbe.expectNoMsg(1000 milliseconds) //no messages + } + } +} + +class RepairableAmenityTest extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val term = Terminal(GlobalDefinitions.order_terminal) //guid=2 + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + guid.register(building, 1) + guid.register(term, 2) + guid.register(player1, 3) + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = term + term.Position = Vector3(1, 0, 0) + term.Actor = system.actorOf(Props(classOf[TerminalControl], term), "terminal-control") + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + building.Actor = buildingProbe.ref + + val tool = Tool(GlobalDefinitions.nano_dispenser) //4 & 5 + guid.register(tool, 4) + guid.register(tool.AmmoSlot.Box, 5) + expectNoMsg(200 milliseconds) + //we're not testing that the math is correct + + "RepairableAmenity" should { + "send initialization messages upon restoration" in { + //the decimator does enough damage to one-shot this terminal from any initial health + val originalHealth = term.Health = term.Definition.RepairRestoresAt - 1 //initial state manip + term.Destroyed = true + assert(originalHealth < term.Definition.RepairRestoresAt) + assert(term.Destroyed) + + term.Actor ! CommonMessages.Use(player1, Some(tool)) + val msg12345 = avatarProbe.receiveN(5, 500 milliseconds) + assert( + msg12345.head match { + case AvatarServiceMessage("TestCharacter1", + AvatarAction.SendResponse(PlanetSideGUID(0), InventoryStateMessage(PlanetSideGUID(5), _, PlanetSideGUID(4), _))) => true + case _ => false + } + ) + assert( + msg12345(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + assert( + msg12345(2) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 50, 0)) => true + case _ => false + } + ) + assert( + msg12345(3) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 51, 0)) => true + case _ => false + } + ) + assert( + msg12345(4) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(PlanetSideGUID(0), RepairMessage(PlanetSideGUID(2), _))) => true + case _ => false + } + ) + assert(term.Health > term.Definition.RepairRestoresAt) + assert(!term.Destroyed) + } + } +} + +class RepairableTurretWeapon extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = Building("test-building", 1, 1, zone, StructureType.Facility) //guid=1 + val activityProbe = TestProbe() + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + val buildingProbe = TestProbe() + zone.Activity = activityProbe.ref + zone.AvatarEvents = avatarProbe.ref + zone.VehicleEvents = vehicleProbe.ref + building.Actor = buildingProbe.ref + + val turret = new FacilityTurret(GlobalDefinitions.manned_turret) //2, 5, 6 + turret.Actor = system.actorOf(Props(classOf[FacilityTurretControl], turret), "turret-control") + turret.Zone = zone + turret.Position = Vector3(1, 0, 0) + val turretWeapon = turret.Weapons.values.head.Equipment.get.asInstanceOf[Tool] + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + player1.Spawn + player1.Position = Vector3(2, 2, 2) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + + guid.register(building, 1) + guid.register(turret, 2) + guid.register(player1, 3) + guid.register(turretWeapon, 5) + guid.register(turretWeapon.AmmoSlot.Box, 6) + building.Position = Vector3(1, 0, 0) + building.Zone = zone + building.Amenities = turret + + val tool = Tool(GlobalDefinitions.nano_dispenser) //7 & 8 + guid.register(tool, 7) + guid.register(tool.AmmoSlot.Box, 8) + + "RepairableTurretWeapon" should { + "handle repairs and restoration" in { + turret.Health = turret.Definition.RepairRestoresAt - 1 //initial state manip + turret.Destroyed = true //initial state manip + assert(turret.Health < turret.Definition.RepairRestoresAt) + assert(turret.Destroyed) + + turret.Actor ! CommonMessages.Use(player1, Some(tool)) + val msg12345 = avatarProbe.receiveN(5, 500 milliseconds) + val msg4 = vehicleProbe.receiveOne(500 milliseconds) + assert( + msg12345.head match { + case AvatarServiceMessage("TestCharacter1", + AvatarAction.SendResponse(PlanetSideGUID(0), InventoryStateMessage(PlanetSideGUID(8), _, PlanetSideGUID(7), _))) => true + case _ => false + } + ) + assert( + msg12345(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + //msg12345(2) and msg12345(3) are related to RepairableAmenity + assert( + msg12345(4) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(PlanetSideGUID(0), RepairMessage(PlanetSideGUID(2), _))) => true + case _ => false + } + ) + assert( + msg4 match { + case VehicleServiceMessage("test", VehicleAction.EquipmentInSlot(_, PlanetSideGUID(2), 1, t)) if t eq turretWeapon => true + case _ => false + } + ) + assert(turret.Health > turret.Definition.RepairRestoresAt) + assert(!turret.Destroyed) + } + } +} + +class RepairableVehicleRepair extends ActorTest { + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + + val atv = Vehicle(GlobalDefinitions.quadassault) //guid=1, 2, 3 + atv.Actor = system.actorOf(Props(classOf[VehicleControl], atv), "vehicle-control") + atv.Position = Vector3(1,0,0) + val atvWeapon = atv.Weapons(1).Equipment.get.asInstanceOf[Tool] + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + player1.Spawn + player1.Position = Vector3(2, 2, 2) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + + guid.register(atv, 1) + guid.register(atvWeapon, 2) + guid.register(atvWeapon.AmmoSlot.Box, 3) + guid.register(player1, 4) + atv.Zone = zone + + val tool = Tool(GlobalDefinitions.nano_dispenser) //5 & 6 + guid.register(tool, 5) + guid.register(tool.AmmoSlot.Box, 6) + + "RepairableVehicle" should { + "handle repairs" in { + val originalHealth = atv.Health = atv.Definition.DamageDestroysAt + 1 //initial state manip + assert(atv.Health == originalHealth) + + atv.Actor ! CommonMessages.Use(player1, Some(tool)) + val msg123 = avatarProbe.receiveN(3, 500 milliseconds) + assert( + msg123.head match { + case AvatarServiceMessage("TestCharacter1", + AvatarAction.SendResponse(PlanetSideGUID(0), InventoryStateMessage(PlanetSideGUID(6), _, PlanetSideGUID(5), _))) => true + case _ => false + } + ) + assert( + msg123(1) match { + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true + case _ => false + } + ) + assert( + msg123(2) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.SendResponse(PlanetSideGUID(0), RepairMessage(PlanetSideGUID(1), _))) => true + case _ => false + } + ) + assert(atv.Health > originalHealth) + } + } +} + +class RepairableVehicleRestoration extends ActorTest { + /* + no messages are dispatched, in this case, because most vehicles are flagged to not be repairable if destroyed + */ + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val avatarProbe = TestProbe() + zone.AvatarEvents = avatarProbe.ref + + val atv = Vehicle(GlobalDefinitions.quadassault) //guid=1, 2, 3 + atv.Actor = system.actorOf(Props(classOf[VehicleControl], atv), "vehicle-control") + atv.Position = Vector3(1,0,0) + val atvWeapon = atv.Weapons(1).Equipment.get.asInstanceOf[Tool] + + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=4 + player1.Spawn + player1.Position = Vector3(2, 2, 2) + val player1Probe = TestProbe() + player1.Actor = player1Probe.ref + + guid.register(atv, 1) + guid.register(atvWeapon, 2) + guid.register(atvWeapon.AmmoSlot.Box, 3) + guid.register(player1, 4) + atv.Zone = zone + + val tool = Tool(GlobalDefinitions.nano_dispenser) //5 & 6 + guid.register(tool, 5) + guid.register(tool.AmmoSlot.Box, 6) + + "RepairableVehicle" should { + "will not restore a destroyed vehicle to working order" in { + atv.Health = atv.Definition.DamageDestroysAt - 1 //initial state manip + atv.Destroyed = true //initial state manip + assert(atv.Health <= atv.Definition.DamageDestroysAt) + assert(atv.Destroyed) + + atv.Actor ! CommonMessages.Use(player1, Some(tool)) + avatarProbe.expectNoMsg(500 milliseconds) + assert(atv.Health == 0) //set to zero explicitly + assert(atv.Destroyed) + } + } +} + +object RepairableTest { } diff --git a/common/src/test/scala/objects/ResourceSiloTest.scala b/common/src/test/scala/objects/ResourceSiloTest.scala index d1d7afdc9..b4d223a4e 100644 --- a/common/src/test/scala/objects/ResourceSiloTest.scala +++ b/common/src/test/scala/objects/ResourceSiloTest.scala @@ -5,11 +5,12 @@ import akka.actor.{Actor, Props} import akka.routing.RandomPool import akka.testkit.TestProbe import base.ActorTest -import net.psforever.objects.guid.TaskResolver -import net.psforever.objects.{Avatar, GlobalDefinitions, Player} +import net.psforever.objects.guid.{NumberPoolHub, TaskResolver} +import net.psforever.objects.guid.source.LimitedNumberSource +import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Vehicle} import net.psforever.objects.serverobject.resourcesilo.{ResourceSilo, ResourceSiloControl, ResourceSiloDefinition} import net.psforever.objects.serverobject.structures.{Building, StructureType} -import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap} import net.psforever.packet.game.UseItemMessage import net.psforever.types._ import org.specs2.mutable.Specification @@ -77,16 +78,34 @@ class ResourceSiloControlStartupTest extends ActorTest { } class ResourceSiloControlUseTest extends ActorTest { - val serviceManager = ServiceManager.boot(system) - serviceManager ! ServiceManager.Register(RandomPool(1).props(Props[TaskResolver]), "taskResolver") - val probe = TestProbe() - serviceManager ! ServiceManager.Register(Props(classOf[ResourceSiloTest.ProbedAvatarService], probe), "avatar") - val msg = UseItemMessage(PlanetSideGUID(1), PlanetSideGUID(0), PlanetSideGUID(2), 0L, false, Vector3(0f,0f,0f),Vector3(0f,0f,0f),0,0,0,0L) //faked - val obj = ResourceSilo() - obj.GUID = PlanetSideGUID(1) + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val map = new ZoneMap("test") + val zone = new Zone("test", map, 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "test-zone-actor") + zone.Actor ! Zone.Init() + val building = new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building) //guid=1 + + val obj = ResourceSilo() //guid=2 obj.Actor = system.actorOf(Props(classOf[ResourceSiloControl], obj), "test-silo") + obj.Owner = building obj.Actor ! "startup" + val player = Player(new Avatar(0L, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=3 + val vehicle = Vehicle(GlobalDefinitions.ant) //guid=4 + + guid.register(building, 1) + guid.register(obj, 2) + guid.register(player, 3) + guid.register(vehicle, 4) + zone.Transport ! Zone.Vehicle.Spawn(vehicle) + vehicle.Seats(0).Occupant = player + player.VehicleSeated = vehicle.GUID + val msg = UseItemMessage(PlanetSideGUID(1), PlanetSideGUID(0), PlanetSideGUID(2), 0L, false, Vector3.Zero,Vector3.Zero,0,0,0,0L) //faked + expectNoMsg(200 milliseconds) + "Resource silo" should { "respond when being used" in { expectNoMsg(1 seconds) diff --git a/common/src/test/scala/objects/UtilityTest.scala b/common/src/test/scala/objects/UtilityTest.scala index 27f078941..c7f200abc 100644 --- a/common/src/test/scala/objects/UtilityTest.scala +++ b/common/src/test/scala/objects/UtilityTest.scala @@ -5,8 +5,10 @@ import akka.actor.{Actor, ActorRef, Props} import base.ActorTest import net.psforever.objects._ import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.vehicles._ -import net.psforever.types.PlanetSideGUID +import net.psforever.packet.game.ItemTransactionMessage +import net.psforever.types._ import org.specs2.mutable._ import scala.concurrent.duration.Duration @@ -38,7 +40,6 @@ class UtilityTest extends Specification { } "create an ams_respawn_tube object" in { - import net.psforever.objects.serverobject.tube.SpawnTube val obj = Utility(UtilityType.ams_respawn_tube, UtilityTest.vehicle) obj.UtilType mustEqual UtilityType.ams_respawn_tube obj().isInstanceOf[SpawnTube] mustEqual true @@ -54,17 +55,18 @@ class UtilityTest extends Specification { obj().asInstanceOf[Terminal].Actor mustEqual ActorRef.noSender } - "teleportpad_terminal produces a telepad object (router_telepad)" in { - import net.psforever.packet.game.ItemTransactionMessage - import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, TransactionType} + "produce a telepad object through the teleportpad_terminal" in { val veh = Vehicle(GlobalDefinitions.quadstealth) + veh.Faction = PlanetSideEmpire.TR val obj = Utility(UtilityType.teleportpad_terminal, UtilityTest.vehicle) + val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) veh.GUID = PlanetSideGUID(101) obj().Owner = veh //hack obj().GUID = PlanetSideGUID(1) + player.GUID = PlanetSideGUID(2) val msg = obj().asInstanceOf[Terminal].Request( - Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)), + player, ItemTransactionMessage(PlanetSideGUID(853), TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0)) ) msg.isInstanceOf[Terminal.BuyEquipment] mustEqual true @@ -83,13 +85,13 @@ class UtilityTest extends Specification { val obj = Utility(UtilityType.internal_router_telepad_deployable, UtilityTest.vehicle) val inpad = obj().asInstanceOf[Utility.InternalTelepad] - inpad.Telepad mustEqual None + inpad.Telepad.isEmpty mustEqual true inpad.Telepad = PlanetSideGUID(5) - inpad.Telepad mustEqual Some(PlanetSideGUID(5)) + inpad.Telepad.contains(PlanetSideGUID(5)) mustEqual true inpad.Telepad = PlanetSideGUID(6) - inpad.Telepad mustEqual Some(PlanetSideGUID(6)) + inpad.Telepad.contains(PlanetSideGUID(6)) mustEqual true inpad.Telepad = None - inpad.Telepad mustEqual None + inpad.Telepad.isEmpty mustEqual true } "be located with their owner (terminal)" in { @@ -98,7 +100,6 @@ class UtilityTest extends Specification { obj().Position mustEqual veh.Position obj().Orientation mustEqual veh.Orientation - import net.psforever.types.Vector3 veh.Position = Vector3(1, 2, 3) veh.Orientation = Vector3(4, 5, 6) obj().Position mustEqual veh.Position @@ -111,7 +112,6 @@ class UtilityTest extends Specification { obj().Position mustEqual veh.Position obj().Orientation mustEqual veh.Orientation - import net.psforever.types.Vector3 veh.Position = Vector3(1, 2, 3) veh.Orientation = Vector3(4, 5, 6) obj().Position mustEqual veh.Position @@ -124,13 +124,12 @@ class UtilityTest extends Specification { obj().Position mustEqual veh.Position obj().Orientation mustEqual veh.Orientation - import net.psforever.types.Vector3 veh.Position = Vector3(1, 2, 3) veh.Orientation = Vector3(4, 5, 6) veh.GUID = PlanetSideGUID(101) obj().Position mustEqual veh.Position obj().Orientation mustEqual veh.Orientation - obj().asInstanceOf[Utility.InternalTelepad].Router mustEqual Some(veh.GUID) + obj().asInstanceOf[Utility.InternalTelepad].Router.contains(veh.GUID) mustEqual true } } } diff --git a/common/src/test/scala/objects/VehicleTest.scala b/common/src/test/scala/objects/VehicleTest.scala index d7897078f..7fc1fbaee 100644 --- a/common/src/test/scala/objects/VehicleTest.scala +++ b/common/src/test/scala/objects/VehicleTest.scala @@ -324,6 +324,7 @@ class VehicleControlStopMountingTest extends ActorTest { player1.GUID = PlanetSideGUID(1) val player2 = Player(VehicleTest.avatar2) val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(3) vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { @@ -349,6 +350,7 @@ class VehicleControlRestartMountingTest extends ActorTest { val player2 = Player(VehicleTest.avatar2) player2.GUID = PlanetSideGUID(2) val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(3) vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { @@ -380,6 +382,7 @@ class VehicleControlAlwaysDismountTest extends ActorTest { val player2 = Player(VehicleTest.avatar2) player2.GUID = PlanetSideGUID(2) val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(3) vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { @@ -430,6 +433,7 @@ class VehicleControlMountingBlockedExosuitTest extends ActorTest { } } val vehicle = Vehicle(GlobalDefinitions.apc_tr) + vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") @@ -490,6 +494,7 @@ class VehicleControlMountingBlockedSeatPermissionTest extends ActorTest { } } val vehicle = Vehicle(GlobalDefinitions.apc_tr) + vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") @@ -524,6 +529,7 @@ class VehicleControlMountingDriverSeatTest extends ActorTest { } } val vehicle = Vehicle(GlobalDefinitions.apc_tr) + vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") val player1 = Player(VehicleTest.avatar1) @@ -563,6 +569,7 @@ class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest { } } val vehicle = Vehicle(GlobalDefinitions.apc_tr) + vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") val player1 = Player(VehicleTest.avatar1) @@ -602,6 +609,7 @@ class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest { } } val vehicle = Vehicle(GlobalDefinitions.apc_tr) + vehicle.Faction = PlanetSideEmpire.TR vehicle.GUID = PlanetSideGUID(10) vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") val player1 = Player(VehicleTest.avatar1) @@ -630,34 +638,6 @@ class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest { } } -class VehicleControlRepairTest extends ActorTest { - val probe = new TestProbe(system) - val vehicle = Vehicle(GlobalDefinitions.fury) - vehicle.GUID = PlanetSideGUID(10) - vehicle.Health = 50 - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") - vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { - VehicleEvents = probe.ref - } - - "Can repair alive vehicle" in { - assert(vehicle.Health == 50) - - vehicle.Health += 10 - assert(vehicle.Health == 60) - } - - "Can't repair dead vehicle" in { - assert(vehicle.Health > 0) - - vehicle.Health = 0 - assert(vehicle.Health == 0) - - vehicle.Health += 10 - assert(vehicle.Health == 0) - } -} - class VehicleControlShieldsChargingTest extends ActorTest { val probe = new TestProbe(system) val vehicle = Vehicle(GlobalDefinitions.fury) @@ -767,7 +747,7 @@ class VehicleControlShieldsNotChargingTooEarlyTest extends ActorTest { // val p_source = PlayerSource( Player(Avatar("TestTarget", PlanetSideEmpire.NC, CharacterGender.Female, 1, CharacterVoice.Mute)) ) // val projectile = Projectile(beamer_wep.Projectile, GlobalDefinitions.beamer, beamer_wep.FireMode, p_source, GlobalDefinitions.beamer.ObjectId, Vector3.Zero, Vector3.Zero) // val fury_dm = Vehicle(GlobalDefinitions.fury).DamageModel -// val obj = ResolvedProjectile(ProjectileResolution.Hit, projectile, p_source, fury_dm, Vector3(1.2f, 3.4f, 5.6f), System.nanoTime) +// val obj = ResolvedProjectile(ProjectileResolution.Hit, projectile, p_source, fury_dm, Vector3(1.2f, 3.4f, 5.6f)) // // "not charge vehicle shields if recently damaged" in { // assert(vehicle.Shields == 0) diff --git a/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala b/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala index f44f2a7c9..387728d16 100644 --- a/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala +++ b/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala @@ -4,10 +4,14 @@ package objects.terminal import akka.actor.{ActorRef, ActorSystem, Props} import base.ActorTest import net.psforever.objects.definition.SeatDefinition +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.source.LimitedNumberSource import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.implantmech.{ImplantTerminalMech, ImplantTerminalMechControl} -import net.psforever.objects.serverobject.structures.StructureType +import net.psforever.objects.serverobject.structures.{Building, StructureType} +import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.vehicles.Seat +import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} import org.specs2.mutable.Specification @@ -24,7 +28,7 @@ class ImplantTerminalMechTest extends Specification { implant_terminal_mech.Seats(0).isInstanceOf[SeatDefinition] mustEqual true implant_terminal_mech.Seats(0).ArmorRestriction mustEqual net.psforever.objects.vehicles.SeatArmorRestriction.NoMax implant_terminal_mech.Seats(0).Bailable mustEqual false - implant_terminal_mech.Seats(0).ControlledWeapon mustEqual None + implant_terminal_mech.Seats(0).ControlledWeapon.isEmpty mustEqual true } } @@ -39,19 +43,19 @@ class ImplantTerminalMechTest extends Specification { "get seat from mount points" in { val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) - obj.GetSeatFromMountPoint(0) mustEqual None - obj.GetSeatFromMountPoint(1) mustEqual Some(0) - obj.GetSeatFromMountPoint(2) mustEqual None + obj.GetSeatFromMountPoint(0).isEmpty mustEqual true + obj.GetSeatFromMountPoint(1).contains(0) mustEqual true + obj.GetSeatFromMountPoint(2).isEmpty mustEqual true } "get passenger in a seat" in { val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) - obj.PassengerInSeat(player) mustEqual None + obj.PassengerInSeat(player).isEmpty mustEqual true obj.Seats(0).Occupant = player - obj.PassengerInSeat(player) mustEqual Some(0) + obj.PassengerInSeat(player).contains(0) mustEqual true obj.Seats(0).Occupant = None - obj.PassengerInSeat(player) mustEqual None + obj.PassengerInSeat(player).isEmpty mustEqual true } } } @@ -113,7 +117,7 @@ class ImplantTerminalMechControl4Test extends ActorTest { "dismount player after mounting" in { val (player, mech) = ImplantTerminalMechTest.SetUpAgents(PlanetSideEmpire.TR) mech.Actor ! Mountable.TryMount(player, 0) - receiveOne(Duration.create(100, "ms")) //consume reply + receiveOne(Duration.create(200, "ms")) //consume reply assert(mech.Seat(0).get.isOccupied) mech.Actor ! Mountable.TryDismount(player, 0) @@ -155,14 +159,26 @@ class ImplantTerminalMechControl5Test extends ActorTest { object ImplantTerminalMechTest { def SetUpAgents(faction : PlanetSideEmpire.Value)(implicit system : ActorSystem) : (Player, ImplantTerminalMech) = { - import net.psforever.objects.serverobject.structures.Building - import net.psforever.objects.zones.Zone - import net.psforever.types.PlanetSideGUID - val terminal = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) - terminal.Actor = system.actorOf(Props(classOf[ImplantTerminalMechControl], terminal), "mech") - terminal.Owner = new Building("Building", building_guid = 0, map_id = 0, Zone.Nowhere, StructureType.Building, GlobalDefinitions.building) - terminal.Owner.Faction = faction - terminal.GUID = PlanetSideGUID(1) + val guid = new NumberPoolHub(new LimitedNumberSource(10)) + val map = new ZoneMap("test") + val zone = new Zone("test", map, 0) { + override def SetupNumberPools() = {} + GUID(guid) + } + val building = new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building) //guid=3 + building.Faction = faction + + val interface = Terminal(GlobalDefinitions.implant_terminal_interface) //guid=2 + interface.Owner = building + val terminal = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) //guid=1 + terminal.Owner = building + + guid.register(terminal, 1) + guid.register(interface, 2) + guid.register(building, 3) + map.TerminalToInterface(1, 2) + terminal.Actor = system.actorOf(Props(classOf[ImplantTerminalMechControl], terminal), "terminal-control") + (Player(Avatar("test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)), terminal) } } diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index ab0bf2d80..3136b480d 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -19,11 +19,6 @@ import scodec.bits.ByteVector //project imports import csr.{CSRWarp, CSRZone, Traveler} import MDCContextAware.Implicits._ -import net.psforever.packet._ -import net.psforever.packet.control._ -import net.psforever.packet.game._ -import net.psforever.packet.game.objectcreate.{ConstructorData, DetailedCharacterData, DroppedItemData, ObjectClass, ObjectCreateMessageParent, PlacementData} -import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo} import net.psforever.objects._ import net.psforever.objects.avatar.{Certification, DeployableToolbox} import net.psforever.objects.ballistics.{PlayerSource, Projectile, ProjectileResolution, ResolvedProjectile, SourceEntry} @@ -31,34 +26,41 @@ import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployableCatego import net.psforever.objects.definition._ import net.psforever.objects.definition.converter.{CorpseConverter, DestroyedVehicleConverter} import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity} -import net.psforever.objects.equipment.{Ammo, CItem, EffectTarget, Equipment, EquipmentSize, EquipmentSlot, FireModeSwitch} +import net.psforever.objects.equipment.{Ammo, CItem, EffectTarget, Equipment, EquipmentSize, EquipmentSlot, FireModeSwitch, JammableUnit} import net.psforever.objects.GlobalDefinitions import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, SquadLoadout, VehicleLoadout} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.doors.Door -import net.psforever.objects.serverobject.hackable.Hackable +import net.psforever.objects.serverobject.generator.Generator +import net.psforever.objects.serverobject.hackable.{Hackable, GenericHackables} import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech -import net.psforever.objects.serverobject.locks.IFFLock +import net.psforever.objects.serverobject.locks.{IFFLock, IFFLocks} import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} import net.psforever.objects.serverobject.painbox.Painbox import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate} -import net.psforever.objects.serverobject.terminals.{CaptureTerminal, MatrixTerminalDefinition, MedicalTerminalDefinition, ProximityDefinition, ProximityTerminal, ProximityUnit, Terminal} +import net.psforever.objects.serverobject.terminals.{CaptureTerminal, CaptureTerminals, MatrixTerminalDefinition, MedicalTerminalDefinition, ProximityDefinition, ProximityTerminal, ProximityUnit, Terminal} import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage import net.psforever.objects.serverobject.tube.SpawnTube -import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurret} +import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurret, WeaponTurrets} import net.psforever.objects.serverobject.zipline.ZipLinePath import net.psforever.objects.teamwork.Squad -import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, MountedWeapons, Utility, UtilityType, VehicleLockState} +import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, CargoBehavior, MountedWeapons, Utility, UtilityType, VehicleLockState} import net.psforever.objects.vehicles.Utility.InternalTelepad -import net.psforever.objects.vital.{DamageFromPainbox, HealFromExoSuitChange, HealFromKit, HealFromTerm, PlayerSuicide, RepairFromKit, Vitality} +import net.psforever.objects.vital.{DamageFromPainbox, HealFromExoSuitChange, HealFromKit, HealFromTerm, PlayerSuicide, RepairFromKit, Vitality, VitalityDefinition} import net.psforever.objects.zones.{InterstellarCluster, Zone, ZoneHotSpotProjector} +import net.psforever.packet._ +import net.psforever.packet.control._ +import net.psforever.packet.game._ +import net.psforever.packet.game.objectcreate.{ConstructorData, DetailedCharacterData, DroppedItemData, ObjectClass, ObjectCreateMessageParent, PlacementData} +import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo} import net.psforever.types._ import services.{RemoverActor, Service, ServiceManager} import services.account.{AccountPersistenceService, PlayerToken, ReceiveAccountData, RetrieveAccountData} @@ -78,7 +80,7 @@ class WorldSessionActor extends Actor import WorldSessionActor._ private[this] val log = org.log4s.getLogger - private[this] val damageLog = org.log4s.getLogger("DamageResolution") + private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel) var sessionId : Long = 0 var leftRef : ActorRef = ActorRef.noSender var rightRef : ActorRef = ActorRef.noSender @@ -294,6 +296,101 @@ class WorldSessionActor extends Actor case AvatarServiceResponse(toChannel, guid, reply) => HandleAvatarServiceResponse(toChannel, guid, reply) + case CommonMessages.Hack(tplayer, obj : Locker, Some(item : SimpleItem)) if tplayer == player => + val hackSpeed = GenericHackables.GetHackSpeed(player, obj) + if(hackSpeed > 0) { + progressBarValue = Some(-hackSpeed) + self ! Progress( + hackSpeed, + GenericHackables.FinishHacking(obj, tplayer, 3212836864L), + GenericHackables.HackingTickAction(progressType = 1, tplayer, obj, item.GUID) + ) + log.info(s"${tplayer.Name} is hacking a locker") + } + + case CommonMessages.Hack(tplayer, obj : CaptureTerminal, Some(item : SimpleItem)) if tplayer == player => + val hackSpeed = GenericHackables.GetHackSpeed(player, obj) + if(hackSpeed > 0) { + progressBarValue = Some(-hackSpeed) + self ! Progress( + hackSpeed, + CaptureTerminals.FinishHackingCaptureConsole(obj, player, 3212836864L), + GenericHackables.HackingTickAction(progressType = 1, tplayer, obj, item.GUID) + ) + log.info(s"${tplayer.Name} is hacking a capture terminal") + } + + case CommonMessages.Hack(tplayer, obj : ImplantTerminalMech, Some(item : SimpleItem)) if tplayer == player => + val hackSpeed = GenericHackables.GetHackSpeed(player, obj) + if(hackSpeed > 0) { + progressBarValue = Some(-hackSpeed) + self ! Progress( + hackSpeed, + GenericHackables.FinishHacking(obj, tplayer, 3212836864L), + GenericHackables.HackingTickAction(progressType = 1, tplayer, obj, item.GUID) + ) + log.info(s"${tplayer.Name} is hacking an implant terminal") + } + + case CommonMessages.Hack(tplayer, obj : Terminal, Some(item : SimpleItem)) if tplayer == player => + val hackSpeed = GenericHackables.GetHackSpeed(player, obj) + if(hackSpeed > 0) { + progressBarValue = Some(-hackSpeed) + self ! Progress( + hackSpeed, + GenericHackables.FinishHacking(obj, tplayer, 3212836864L), + GenericHackables.HackingTickAction(progressType = 1, tplayer, obj, item.GUID) + ) + log.info(s"${tplayer.Name} is hacking a terminal") + } + + case CommonMessages.Hack(tplayer, obj : IFFLock, Some(item : SimpleItem)) if tplayer == player => + val hackSpeed = GenericHackables.GetHackSpeed(player, obj) + if(hackSpeed > 0) { + progressBarValue = Some(-hackSpeed) + if(obj.Faction != tplayer.Faction) { + // Enemy faction is hacking this IFF lock + self ! Progress( + hackSpeed, + GenericHackables.FinishHacking(obj, tplayer, 1114636288L), + GenericHackables.HackingTickAction(progressType = 1, tplayer, obj, item.GUID) + ) + log.info(s"${tplayer.Name} is hacking an IFF lock") + } + else { + // IFF Lock is being resecured by it's owner faction + self ! Progress( + hackSpeed, + IFFLocks.FinishResecuringIFFLock(obj), + GenericHackables.HackingTickAction(progressType = 1, player, obj, item.GUID) + ) + log.info(s"${player.Name} is resecuring an IFF lock") + } + } + + case CommonMessages.Hack(tplayer, obj : Vehicle, Some(item : SimpleItem)) if tplayer == player => + val hackSpeed = GenericHackables.GetHackSpeed(player, obj) + if(hackSpeed > 0) { + progressBarValue = Some(-hackSpeed) + self ! Progress( + hackSpeed, + Vehicles.FinishHackingVehicle(obj, tplayer,3212836864L), + GenericHackables.HackingTickAction(progressType = 1, tplayer, obj, item.GUID) + ) + log.trace(s"${tplayer.Name} is hacking a vehicle") + } + + case CommonMessages.Use(tplayer, Some((item : Tool, user : Player))) => + if(progressBarValue.isEmpty) { + progressBarValue = Some(-4) + self ! Progress( + 4, + Players.FinishRevivingPlayer(tplayer, user.Name), + Players.RevivingTickAction(tplayer, user, item) + ) + log.trace(s"${user.Name} is reviving a dead ally") + } + case Door.DoorMessage(tplayer, msg, order) => HandleDoorMessage(tplayer, msg, order) @@ -649,21 +746,19 @@ class WorldSessionActor extends Actor CanNotChangeDeployment(obj, state, reason) case ResourceSilo.ResourceSiloMessage(tplayer, msg, order) => - val vehicle_guid = msg.avatar_guid - val silo_guid = msg.object_guid - order match { - case ResourceSilo.ChargeEvent() => - antChargingTick.cancel() // If an ANT is refilling an NTU silo it isn't in a warpgate, so disable NTU regeneration - antDischargingTick.cancel() - antDischargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuDischarging(player, continent.GUID(vehicle_guid).get.asInstanceOf[Vehicle], silo_guid)) + continent.GUID(msg.avatar_guid) match { + case Some(vehicle : Vehicle) => + val silo_guid = msg.object_guid + order match { + case ResourceSilo.ChargeEvent() => + antChargingTick.cancel() // If an ANT is refilling an NTU silo it isn't in a warpgate, so disable NTU regeneration + antDischargingTick.cancel() + antDischargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuDischarging(player, vehicle, msg.object_guid)) + case _ => ; + } + case _ => ; } - case CheckCargoDismount(cargo_guid, carrier_guid, mountPoint, iteration) => - HandleCheckCargoDismounting(cargo_guid, carrier_guid, mountPoint, iteration) - - case CheckCargoMounting(cargo_guid, carrier_guid, mountPoint, iteration) => - HandleCheckCargoMounting(cargo_guid, carrier_guid, mountPoint, iteration) - case CreateCharacter(name, head, voice, gender, empire) => log.info(s"Creating new character $name...") Database.getConnection.connect.onComplete { @@ -676,6 +771,7 @@ class WorldSessionActor extends Actor ) }.onComplete { case scala.util.Success(insertResult) => + if(connection.isConnected) connection.disconnect insertResult match { case result: QueryResult => if (result.rows.nonEmpty) { @@ -691,7 +787,6 @@ class WorldSessionActor extends Actor case e => log.error(s"CreateCharacter: unexpected error while creating new character for $accountUserName") sendResponse(ActionResultMessage.Fail(3)) - if(connection.isConnected) connection.disconnect self ! ListAccountCharacters() } case scala.util.Failure(e) => @@ -710,6 +805,7 @@ class WorldSessionActor extends Actor "SELECT id, name, faction_id, gender_id, head_id, voice_id, deleted, last_login FROM characters where account_id=? ORDER BY last_login", Array(account.AccountId) ).onComplete { case scala.util.Success(result : QueryResult) => + if(connection.isConnected) connection.disconnect if(result.rows.nonEmpty) { import net.psforever.objects.definition.converter.CharacterSelectConverter val gen : AtomicInteger = new AtomicInteger(1) @@ -759,7 +855,6 @@ class WorldSessionActor extends Actor } } Thread.sleep(50) - if(connection.isConnected) connection.disconnect case scala.util.Success(result) => if(connection.isConnected) connection.disconnect //pre-empt failWithError @@ -827,7 +922,7 @@ class WorldSessionActor extends Actor case Zone.Lattice.SpawnPoint(zone_id, spawn_tube) => var (pos, ori) = spawn_tube.SpecificPoint(continent.GUID(player.VehicleSeated) match { - case Some(obj : Vehicle) if !obj.IsDead => + case Some(obj : Vehicle) if !obj.Destroyed => obj case _ => player @@ -875,6 +970,7 @@ class WorldSessionActor extends Actor avatar.Deployables.Remove(obj) UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(obj.Definition.Item)) continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent)) + obj.Faction = PlanetSideEmpire.NEUTRAL sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL)) continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.SetEmpire(playerGUID, guid, PlanetSideEmpire.NEUTRAL)) val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, PlanetSideGUID(0)) @@ -920,7 +1016,7 @@ class WorldSessionActor extends Actor continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(obj), continent)) sendResponse(SetEmpireMessage(guid, faction)) continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.SetEmpire(playerGUID, guid, faction)) - val info = DeployableInfo(obj.GUID, DeployableIcon.Boomer, obj.Position, obj.Owner.get) + val info = DeployableInfo(obj.GUID, DeployableIcon.Boomer, obj.Position, obj.Owner.getOrElse(PlanetSideGUID(0))) sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, info)) continent.LocalEvents ! LocalServiceMessage(factionChannel, LocalAction.DeployableMapIcon(playerGUID, DeploymentAction.Build, info)) case Some(_) | None => ; //pointless trigger; see Zone.Ground.ItemOnGround(BoomerTrigger, ...) @@ -1012,7 +1108,7 @@ class WorldSessionActor extends Actor StopBundlingPackets() case WorldSessionActor.FinalizeDeployable(obj : ComplexDeployable, tool, index) => - //spitfires and deployable field turrets and the deployable_shield_generator + //tank_traps, spitfires, deployable field turrets and the deployable_shield_generator StartBundlingPackets() DeployableBuildActivity(obj) CommonDestroyConstructionItem(tool, index) @@ -1029,7 +1125,7 @@ class WorldSessionActor extends Actor continent.GUID(router) match { case Some(vehicle : Vehicle) => val routerGUID = router.get - if(vehicle.Health == 0) { + if(vehicle.Destroyed) { //the Telepad was successfully deployed; but, before it could configure, its Router was destroyed sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", "@Telepad_NoDeploy_RouterLost", None)) continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds))) @@ -1052,21 +1148,13 @@ class WorldSessionActor extends Actor } StopBundlingPackets() - case WorldSessionActor.FinalizeDeployable(obj : SimpleDeployable, tool, index) => - //tank_trap - StartBundlingPackets() - DeployableBuildActivity(obj) - CommonDestroyConstructionItem(tool, index) - FindReplacementConstructionItem(tool, index) - StopBundlingPackets() - case WorldSessionActor.FinalizeDeployable(obj : PlanetSideGameObject with Deployable, tool, index) => val guid = obj.GUID val definition = obj.Definition StartBundlingPackets() sendResponse(GenericObjectActionMessage(guid, 21)) //reset build cooldown sendResponse(ObjectDeployedMessage.Failure(definition.Name)) - log.warn(s"FinalizeDeployable: deployable ${definition.asInstanceOf[SimpleDeployableDefinition].Item}@$guid not handled by specific case") + log.warn(s"FinalizeDeployable: deployable ${definition.asInstanceOf[BaseDeployableDefinition].Item}@$guid not handled by specific case") log.warn(s"FinalizeDeployable: deployable will be cleaned up, but may not get unregistered properly") TryDropConstructionTool(tool, index, obj.Position) obj.Position = Vector3.Zero @@ -1171,25 +1259,17 @@ class WorldSessionActor extends Actor case NtuDischarging(tplayer, vehicle, silo_guid) => HandleNtuDischarging(tplayer, vehicle, silo_guid) - case HackingProgress(progressType, tplayer, target, tool_guid, delta, completeAction, tickAction) => - HandleHackingProgress(progressType, tplayer, target, tool_guid, delta, completeAction, tickAction) - - case Vitality.DamageResolution(target : TrapDeployable, _) => - //tank_traps - val guid = target.GUID - val health = target.Health - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 0, health)) - if(health <= 0) { - AnnounceDestroyDeployable(target, None) - } + case Progress(delta, completeAction, tickAction) => + HandleProgressChange(delta, completeAction, tickAction) case Vitality.DamageResolution(target : TelepadDeployable, _) => //telepads if(target.Health <= 0) { //update if destroyed + target.Destroyed = true val guid = target.GUID continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, guid)) - AnnounceDestroyDeployable(target, Some(0 seconds)) + Deployables.AnnounceDestroyDeployable(target, Some(0 seconds)) } case Vitality.DamageResolution(target : PlanetSideGameObject, _) => @@ -1209,6 +1289,7 @@ class WorldSessionActor extends Actor "SELECT gm FROM accounts where id=?", Array(account.AccountId) )).onComplete { case scala.util.Success(queryResult) => + if(connection.isConnected) connection.disconnect queryResult match { case row: ArrayRowData => // If we got a row from the database log.info(s"ReceiveAccountData: ready to load character list for ${account.Username}") @@ -1216,12 +1297,11 @@ class WorldSessionActor extends Actor case _ => // If the account didn't exist in the database log.error(s"ReceiveAccountData: ${account.Username} data not found, or unexpected query result format - ${queryResult.getClass}") Thread.sleep(50) - if(connection.isConnected) connection.disconnect sendResponse(DropSession(sessionId, "You should not exist!")) } case scala.util.Failure(e) => - log.error(s"ReceiveAccountData: ${e.getMessage}") if(connection.isConnected) connection.disconnect + log.error(s"ReceiveAccountData: ${e.getMessage}") Thread.sleep(50) } case scala.util.Failure(e) => @@ -1347,7 +1427,7 @@ class WorldSessionActor extends Actor } else { inZone.GUID(p.VehicleSeated) match { - case Some(v : Vehicle) if v.Health == 0 => + case Some(v : Vehicle) if v.Destroyed => inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(v), inZone)) inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(v, inZone, if(v.Flying) { //TODO gravity @@ -1470,9 +1550,13 @@ class WorldSessionActor extends Actor case AvatarResponse.Revive(target_guid) => if(tplayer_guid == target_guid) { - deadState = DeadState.Alive reviveTimer.cancel + deadState = DeadState.Alive + player.Revive + val health = player.Health + sendResponse(PlanetsideAttributeMessage(target_guid, 0, health)) sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, player.Position, player.Faction, true)) + continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttributeToAll(target_guid, 0, health)) } case AvatarResponse.ArmorChanged(suit, subtype) => @@ -1782,26 +1866,29 @@ class WorldSessionActor extends Actor sendResponse(GenericObjectStateMsg(door_guid, 17)) case LocalResponse.EliminateDeployable(obj : TurretDeployable, guid, pos) => - if(obj.Health == 0) { + if(obj.Destroyed) { DeconstructDeployable(obj, guid, pos) } else { + obj.Destroyed = true DeconstructDeployable(obj, guid, pos, obj.Orientation, if(obj.MountPoints.isEmpty) 2 else 1) } case LocalResponse.EliminateDeployable(obj : ExplosiveDeployable, guid, pos) => - if(obj.Exploded || obj.Jammed || obj.Health == 0) { + if(obj.Destroyed || obj.Jammed || obj.Health == 0) { DeconstructDeployable(obj, guid, pos) } else { + obj.Destroyed = true DeconstructDeployable(obj, guid, pos, obj.Orientation, 2) } case LocalResponse.EliminateDeployable(obj : ComplexDeployable, guid, pos) => - if(obj.Health == 0) { + if(obj.Destroyed) { DeconstructDeployable(obj, guid, pos) } else { + obj.Destroyed = true DeconstructDeployable(obj, guid, pos, obj.Orientation, 1) } @@ -1826,18 +1913,20 @@ class WorldSessionActor extends Actor case _ => ; } //standard deployable elimination behavior - if(obj.Health == 0) { + if(obj.Destroyed) { DeconstructDeployable(obj, guid, pos) } else { + obj.Destroyed = true DeconstructDeployable(obj, guid, pos, obj.Orientation, 2) } case LocalResponse.EliminateDeployable(obj, guid, pos) => - if(obj.Health == 0) { + if(obj.Destroyed) { DeconstructDeployable(obj, guid, pos) } else { + obj.Destroyed = true DeconstructDeployable(obj, guid, pos, obj.Orientation, 2) } @@ -1847,44 +1936,11 @@ class WorldSessionActor extends Actor sendResponse(HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2)) case LocalResponse.HackObject(target_guid, unk1, unk2) => - sendResponse(HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)) + HackObject(target_guid, unk1, unk2) + case LocalResponse.HackCaptureTerminal(target_guid, unk1, unk2, isResecured) => - var value = 0L + HackCaptureTerminal(target_guid, unk1, unk2, isResecured) - if (isResecured) { - value = 17039360L - sendResponse(PlanetsideAttributeMessage(target_guid, 20, value)) - } - else { - continent.GUID(target_guid) match { - case Some(capture_terminal: Amenity with Hackable) => - capture_terminal.HackedBy match { - case Some(Hackable.HackInfo(_, _, hfaction, _, start, length)) => - val hack_time_remaining_ms = TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS) - val deciseconds_remaining = (hack_time_remaining_ms / 100) - - // See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated - val start_num = hfaction match { - case PlanetSideEmpire.TR => 65536L - case PlanetSideEmpire.NC => 131072L - case PlanetSideEmpire.VS => 196608L - } - value = start_num + deciseconds_remaining - - sendResponse(PlanetsideAttributeMessage(target_guid, 20, value)) - - continent.GUID(player.VehicleSeated) match { - case Some(mountable: Amenity with Mountable) => - if(mountable.Owner.GUID == capture_terminal.Owner.GUID) { - continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player.GUID, mountable.Seats.head._1, true, mountable.GUID)) - } - case _ => ; - } - case _ => log.warn("LocalResponse.HackCaptureTerminal: HackedBy not defined") - } - case _ => log.warn(s"LocalResponse.HackCaptureTerminal: Couldn't find capture terminal with GUID ${target_guid} in zone ${continent.Id}") - } - } case LocalResponse.ObjectDelete(object_guid, unk) => if(tplayer_guid != guid) { sendResponse(ObjectDeleteMessage(object_guid, unk)) @@ -2853,11 +2909,12 @@ class WorldSessionActor extends Actor } case VehicleResponse.ForceDismountVehicleCargo(cargo_guid, bailed, requestedByPassenger, kicked) => - DismountVehicleCargo(tplayer_guid, cargo_guid, bailed, requestedByPassenger, kicked) + CargoBehavior.HandleVehicleCargoDismount(continent, tplayer_guid, cargo_guid, bailed, requestedByPassenger, kicked) + case VehicleResponse.KickCargo(vehicle, speed, delay) => if(player.VehicleSeated.nonEmpty && deadState == DeadState.Alive) { if(speed > 0) { - val strafe = if(CargoOrientation(vehicle) == 1) 2 else 1 + val strafe = if(Vehicles.CargoOrientation(vehicle) == 1) 2 else 1 val reverseSpeed = if(strafe > 1) 0 else speed //strafe or reverse, not both controlled = Some(reverseSpeed) @@ -2911,219 +2968,6 @@ class WorldSessionActor extends Actor } } - /** - * na - * @param decorator custom text for these messages in the log - * @param target an optional the target object - * @param targetGUID the expected globally unique identifier of the target object - */ - def LogCargoEventMissingVehicleError(decorator : String, target : Option[PlanetSideGameObject], targetGUID : PlanetSideGUID) : Unit = { - target match { - case Some(_ : Vehicle) => ; - case Some(_) => log.error(s"$decorator target $targetGUID no longer identifies as a vehicle") - case None => log.error(s"$decorator target $targetGUID has gone missing") - } - } - - /** - * na - * @param cargoGUID na - * @param carrierGUID na - * @param mountPoint na - * @param iteration na - */ - def HandleCheckCargoDismounting(cargoGUID : PlanetSideGUID, carrierGUID : PlanetSideGUID, mountPoint : Int, iteration : Int) : Unit = { - (continent.GUID(cargoGUID), continent.GUID(carrierGUID)) match { - case ((Some(vehicle : Vehicle), Some(cargo_vehicle : Vehicle))) => - HandleCheckCargoDismounting(cargoGUID, vehicle, carrierGUID, cargo_vehicle, mountPoint, iteration) - case (cargo, carrier) if iteration > 0 => - log.error(s"HandleCheckCargoDismounting: participant vehicles changed in the middle of a mounting event") - LogCargoEventMissingVehicleError("HandleCheckCargoDismounting: cargo", cargo, cargoGUID) - LogCargoEventMissingVehicleError("HandleCheckCargoDismounting: carrier", carrier, carrierGUID) - case _ => - } - } - - /** - * na - * @param cargoGUID na - * @param cargo na - * @param carrierGUID na - * @param carrier na - * @param mountPoint na - * @param iteration na - */ - def HandleCheckCargoDismounting(cargoGUID : PlanetSideGUID, cargo : Vehicle, carrierGUID : PlanetSideGUID, carrier : Vehicle, mountPoint : Int, iteration : Int) : Unit = { - carrier.CargoHold(mountPoint) match { - case Some(hold) if !hold.isOccupied => - val distance = Vector3.DistanceSquared(cargo.Position, carrier.Position) - log.debug(s"HandleCheckCargoDismounting: mount distance between $cargoGUID and $carrierGUID - actual=$distance, target=225") - if(distance > 225) { - //cargo vehicle has moved far enough away; close the carrier's hold door - log.info(s"HandleCheckCargoDismounting: dismount of cargo vehicle from carrier complete at distance of $distance") - continent.VehicleEvents ! VehicleServiceMessage( - continent.Id, - VehicleAction.SendResponse( - player.GUID, - CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0) - ) - ) - //sending packet to the cargo vehicle's client results in player locking himself in his vehicle - //player gets stuck as "always trying to remount the cargo hold" - //obviously, don't do this - } - else if(iteration > 40) { - //cargo vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold - cargo.MountedIn = carrierGUID - hold.Occupant = cargo - StartBundlingPackets() - CargoMountBehaviorForAll(carrier, cargo, mountPoint) - StopBundlingPackets() - } - else { - //cargo vehicle did not move far away enough yet and there is more time to wait; reschedule check - import scala.concurrent.ExecutionContext.Implicits.global - cargoDismountTimer = context.system.scheduler.scheduleOnce(250 milliseconds, self, CheckCargoDismount(cargoGUID, carrierGUID, mountPoint, iteration + 1)) - } - case None => - log.warn(s"HandleCheckCargoDismounting: carrier vehicle $carrier does not have a cargo hold #$mountPoint") - case _ => - if(iteration == 0) { - log.warn(s"HandleCheckCargoDismounting: carrier vehicle $carrier will not discharge the cargo of hold #$mountPoint; this operation was initiated incorrectly") - } - else { - log.error(s"HandleCheckCargoDismounting: something has attached to the carrier vehicle $carrier cargo of hold #$mountPoint while a cargo dismount event was ongoing; stopped at iteration $iteration / 40") - } - } - } - - /** - * na - * @param cargoGUID the vehicle being ferried as cargo - * @param carrierGUID the ferrying carrier vehicle - * @param mountPoint the cargo hold to which the cargo vehicle is stowed - * @param iteration number of times a proper mounting for this combination has been queried - */ - def HandleCheckCargoMounting(cargoGUID : PlanetSideGUID, carrierGUID : PlanetSideGUID, mountPoint : Int, iteration : Int) : Unit = { - (continent.GUID(cargoGUID), continent.GUID(carrierGUID)) match { - case ((Some(cargo : Vehicle), Some(carrier : Vehicle))) => - HandleCheckCargoMounting(cargoGUID, cargo, carrierGUID, carrier, mountPoint, iteration) - case (cargo, carrier) if iteration > 0 => - log.error(s"HandleCheckCargoMounting: participant vehicles changed in the middle of a mounting event") - LogCargoEventMissingVehicleError("HandleCheckCargoMounting: cargo", cargo, cargoGUID) - LogCargoEventMissingVehicleError("HandleCheckCargoMounting: carrier", carrier, carrierGUID) - case _ => ; - } - } - - /** - * na - * @param cargoGUID the vehicle being ferried as cargo - * @param cargo the vehicle being ferried as cargo - * @param carrierGUID the ferrying carrier vehicle - * @param carrier the ferrying carrier vehicle - * @param mountPoint the cargo hold to which the cargo vehicle is stowed - * @param iteration number of times a proper mounting for this combination has been queried - */ - def HandleCheckCargoMounting(cargoGUID : PlanetSideGUID, cargo : Vehicle, carrierGUID : PlanetSideGUID, carrier : Vehicle, mountPoint : Int, iteration : Int) : Unit = { - val distance = Vector3.DistanceSquared(cargo.Position, carrier.Position) - carrier.CargoHold(mountPoint) match { - case Some(hold) if !hold.isOccupied => - log.debug(s"HandleCheckCargoMounting: mount distance between $cargoGUID and $carrierGUID - actual=$distance, target=64") - if(distance <= 64) { - //cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it - log.info(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance") - cargo.MountedIn = carrierGUID - hold.Occupant = cargo - cargo.Velocity = None - continent.VehicleEvents ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health))) - continent.VehicleEvents ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))) - StartBundlingPackets() - val (attachMsg, mountPointMsg) = CargoMountBehaviorForAll(carrier, cargo, mountPoint) - StopBundlingPackets() - log.info(s"HandleCheckCargoMounting: $attachMsg") - log.info(s"HandleCheckCargoMounting: $mountPointMsg") - } - else if(distance > 625 || iteration >= 40) { - //vehicles moved too far away or took too long to get into proper position; abort mounting - log.info("HandleCheckCargoMounting: cargo vehicle is too far away or didn't mount within allocated time - aborting") - continent.VehicleEvents ! VehicleServiceMessage( - continent.Id, - VehicleAction.SendResponse( - player.GUID, - CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0) - ) - ) - //sending packet to the cargo vehicle's client results in player locking himself in his vehicle - //player gets stuck as "always trying to remount the cargo hold" - //obviously, don't do this - } - else { - //cargo vehicle still not in position but there is more time to wait; reschedule check - import scala.concurrent.ExecutionContext.Implicits.global - cargoMountTimer = context.system.scheduler.scheduleOnce(250 milliseconds, self, CheckCargoMounting(cargoGUID, carrierGUID, mountPoint, iteration = iteration + 1)) - } - case None => ; - log.warn(s"HandleCheckCargoMounting: carrier vehicle $carrier does not have a cargo hold #$mountPoint") - case _ => - if(iteration == 0) { - log.warn(s"HandleCheckCargoMounting: carrier vehicle $carrier already possesses cargo in hold #$mountPoint; this operation was initiated incorrectly") - } - else { - log.error(s"HandleCheckCargoMounting: something has attached to the carrier vehicle $carrier cargo of hold #$mountPoint while a cargo dismount event was ongoing; stopped at iteration $iteration / 40") - } - } - } - - /** - * Produce an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - * that will set up a realized parent-child association between a ferrying vehicle and a ferried vehicle. - * @see `CargoMountPointStatusMessage` - * @see `CargoOrientation` - * @see `ObjectAttachMessage` - * @param carrier the ferrying vehicle - * @param cargo the ferried vehicle - * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached; - * also known as a "cargo hold" - * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - */ - def CargoMountMessages(carrier : Vehicle, cargo : Vehicle, mountPoint : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = { - CargoMountMessages(carrier.GUID, cargo.GUID, mountPoint, CargoOrientation(cargo)) - } - - /** - * Produce an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - * that will set up a realized parent-child association between a ferrying vehicle and a ferried vehicle. - * @see `CargoMountPointStatusMessage` - * @see `ObjectAttachMessage` - * @param carrier the ferrying vehicle - * @param cargo the ferried vehicle - * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached - * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - */ - def CargoMountMessages(carrierGUID : PlanetSideGUID, cargoGUID : PlanetSideGUID, mountPoint : Int, orientation : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = { - ( - ObjectAttachMessage(carrierGUID, cargoGUID, mountPoint), - CargoMountPointStatusMessage(carrierGUID, cargoGUID, cargoGUID, PlanetSideGUID(0), mountPoint, CargoStatus.Occupied, orientation) - ) - } - - /** - * The orientation of a cargo vehicle as it is being loaded into and contained by a carrier vehicle. - * The type of carrier is not an important consideration in determining the orientation, oddly enough. - * @param vehicle the cargo vehicle - * @return the orientation as an `Integer` value; - * `0` for almost all cases - */ - def CargoOrientation(vehicle : Vehicle) : Int = { - if(vehicle.Definition == GlobalDefinitions.router) { - 1 - } - else { - 0 - } - } - /** * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet only to this client. * @see `CargoMountPointStatusMessage` @@ -3134,7 +2978,7 @@ class WorldSessionActor extends Actor * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet */ def CargoMountBehaviorForUs(carrier : Vehicle, cargo : Vehicle, mountPoint : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = { - val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint) + val msgs @ (attachMessage, mountPointStatusMessage) = CargoBehavior.CargoMountMessages(carrier, cargo, mountPoint) CargoMountMessagesForUs(attachMessage, mountPointStatusMessage) msgs } @@ -3151,52 +2995,6 @@ class WorldSessionActor extends Actor sendResponse(mountPointStatusMessage) } - - - /** - * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to all other clients, not this one. - * @see `CargoMountPointStatusMessage` - * @see `ObjectAttachMessage` - * @param carrier the ferrying vehicle - * @param cargo the ferried vehicle - * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached - * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - */ - def CargoMountBehaviorForOthers(carrier : Vehicle, cargo : Vehicle, mountPoint : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = { - val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint) - CargoMountMessagesForOthers(attachMessage, mountPointStatusMessage) - msgs - } - - /** - * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to all other clients, not this one. - * @see `CargoMountPointStatusMessage` - * @see `ObjectAttachMessage` - * @param attachMessage an `ObjectAttachMessage` packet suitable for initializing cargo operations - * @param mountPointStatusMessage a `CargoMountPointStatusMessage` packet suitable for initializing cargo operations - */ - def CargoMountMessagesForOthers(attachMessage : ObjectAttachMessage, mountPointStatusMessage : CargoMountPointStatusMessage) : Unit = { - val pguid = player.GUID - continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(pguid, attachMessage)) - continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(pguid, mountPointStatusMessage)) - } - - /** - * Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to everyone. - * @see `CargoMountPointStatusMessage` - * @see `ObjectAttachMessage` - * @param carrier the ferrying vehicle - * @param cargo the ferried vehicle - * @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached - * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet - */ - def CargoMountBehaviorForAll(carrier : Vehicle, cargo : Vehicle, mountPoint : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = { - val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint) - CargoMountMessagesForUs(attachMessage, mountPointStatusMessage) - CargoMountMessagesForOthers(attachMessage, mountPointStatusMessage) - msgs - } - /** * na * @param tplayer na @@ -3282,60 +3080,66 @@ class WorldSessionActor extends Actor } /** - * na - * @param progressType na - * @param tplayer na - * @param target na - * @param tool_guid na - * @param delta na - * @param completeAction na - * @param tickAction na + * Handle the message that indidctes the level of completion of a process. + * The process is any form of user-driven activity with a certain eventual outcome + * but indeterminate progress feedback per cycle.
+ *
+ * This task is broken down into the "progression" from its initial state to the eventual outcome + * as is reported back to the player through some means of messaging window feedback. + * Though common in practice, this is not a requirement + * and the progress can accumulate without a user reportable method. + * To ensure that completion is reported properly, + * an exception is made that 99% completion is accounted uniquely + * before the final 100% is achieved. + * If the background process recording value is never set before running the initial operation + * or gets unset by failing a `tickAction` check + * the process is stopped. + * @see `progressBarUpdate` + * @see `progressBarValue` + * @see `WorldSessionActor.Progress` + * @param delta how much the progress changes each tick + * @param completeAction a custom action performed once the process is completed + * @param tickAction an optional action is is performed for each tick of progress; + * also performs a continuity check to determine if the process has been disrupted */ - def HandleHackingProgress(progressType : Int, tplayer : Player, target : PlanetSideServerObject, tool_guid : PlanetSideGUID, delta : Float, completeAction : ()=>Unit, tickAction : Option[()=>Unit]) : Unit = { + def HandleProgressChange(delta : Float, completionAction : ()=>Unit, tickAction : Float=>Boolean) : Unit = { progressBarUpdate.cancel - if(progressBarValue.isDefined) { - val progressBarVal : Float = if (progressBarValue.get + delta > 100) { 100f } else { progressBarValue.get + delta } - - val vis = if(progressBarVal == 0L) { - //hack state for progress bar visibility - HackState.Start - } - else if(progressBarVal >= 100L) { - HackState.Finished - } - else if(target.Velocity.isDefined && Vector3.Distance(Vector3.Zero, target.Velocity.get) > 1f) { - // If the object is moving (more than slightly to account for things like magriders rotating, or the last velocity reported being the magrider dipping down on dismount) then cancel the hack - HackState.Cancelled - } - else { - HackState.Ongoing - } - - if(!target.HasGUID) { - // Target is gone, cancel the hack. - sendResponse(HackMessage(progressType, target.GUID, player.GUID, 0, 0L, HackState.Cancelled, 8L)) - } - else if(vis == HackState.Cancelled) { - // Object moved. Cancel the hack (e.g. vehicle drove away) - sendResponse(HackMessage(progressType, target.GUID, player.GUID, 0, 0L, vis, 8L)) - } - else - { - sendResponse(HackMessage(progressType, target.GUID, player.GUID, progressBarVal.toInt, 0L, vis, 8L)) - - if(progressBarVal >= 100) { - //done + progressBarValue match { + case Some(value) => + val next = value + delta + if(value >= 100f) { + //complete progressBarValue = None - completeAction() + tickAction(100) + completionAction() + } + else if(value < 100f && next >= 100f) { + if(tickAction(99)) { + //will complete after this turn + progressBarValue = Some(next) + import scala.concurrent.ExecutionContext.Implicits.global + progressBarUpdate = context.system.scheduler.scheduleOnce(100 milliseconds, self, + Progress(delta, completionAction, tickAction) + ) + } + else { + progressBarValue = None + } } else { - //continue next tick - tickAction.getOrElse(() => Unit)() - progressBarValue = Some(progressBarVal) - import scala.concurrent.ExecutionContext.Implicits.global - progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, HackingProgress(progressType, tplayer, target, tool_guid, delta, completeAction)) + if(tickAction(next)) { + //normal progress activity + progressBarValue = Some(next) + import scala.concurrent.ExecutionContext.Implicits.global + progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, + Progress(delta, completionAction, tickAction) + ) + } + else { + progressBarValue = None + } } - } + case None => ; } } @@ -3606,34 +3410,33 @@ class WorldSessionActor extends Actor accountIntermediary ! RetrieveAccountData(token) - case msg @ MountVehicleCargoMsg(player_guid, vehicle_guid, cargo_vehicle_guid, unk4) => + case msg @ MountVehicleCargoMsg(player_guid, cargo_guid, carrier_guid, unk4) => log.info(msg.toString) - (continent.GUID(vehicle_guid), continent.GUID(cargo_vehicle_guid)) match { - case (Some(_ : Vehicle), Some(carrier : Vehicle)) => - carrier.Definition.Cargo.headOption match { - case Some((mountPoint, _)) => //begin the mount process - open the cargo door - val reply = CargoMountPointStatusMessage(cargo_vehicle_guid, PlanetSideGUID(0), vehicle_guid, PlanetSideGUID(0), mountPoint, CargoStatus.InProgress, 0) - log.debug(reply.toString) - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player.GUID, reply)) - sendResponse(reply) - - import scala.concurrent.duration._ - import scala.concurrent.ExecutionContext.Implicits.global - // Start timer to check every second if the vehicle is close enough to mount, or far enough away to cancel the mounting - cargoMountTimer.cancel - cargoMountTimer = context.system.scheduler.scheduleOnce(1 second, self, CheckCargoMounting(vehicle_guid, cargo_vehicle_guid, mountPoint, iteration = 0)) - case None => + (continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match { + case (Some(cargo : Vehicle), Some(carrier : Vehicle)) => + carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match { + case Some((mountPoint, _)) => //try begin the mount process + cargo.Actor ! CargoBehavior.CheckCargoMounting(carrier_guid, mountPoint, 0) + case _ => log.warn(s"MountVehicleCargoMsg: target carrier vehicle (${carrier.Definition.Name}) does not have a cargo hold") } case (None, _) | (Some(_), None) => - log.warn(s"MountVehicleCargoMsg: one or more of the target vehicles do not exist - $cargo_vehicle_guid or $vehicle_guid") + log.warn(s"MountVehicleCargoMsg: one or more of the target vehicles do not exist - $carrier_guid or $cargo_guid") case _ => ; } case msg @ DismountVehicleCargoMsg(player_guid, cargo_guid, bailed, requestedByPassenger, kicked) => log.info(msg.toString) - if(!requestedByPassenger) { - DismountVehicleCargo(player_guid, cargo_guid, bailed, requestedByPassenger, kicked) + //when kicked by carrier driver, player_guid will be PlanetSideGUID(0) + //when exiting of the cargo vehicle driver's own accord, player_guid will be the cargo vehicle driver + continent.GUID(cargo_guid) match { + case Some(cargo : Vehicle) if !requestedByPassenger => + continent.GUID(cargo.MountedIn) match { + case Some(carrier : Vehicle) => + CargoBehavior.HandleVehicleCargoDismount(continent, player_guid, cargo_guid, bailed, requestedByPassenger, kicked) + case _ => ; + } + case _ => ; } case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) => @@ -3644,6 +3447,7 @@ class WorldSessionActor extends Actor "SELECT account_id FROM characters where name ILIKE ? AND deleted = false", Array(name) )).onComplete { case scala.util.Success(queryResult) => + if(connection.isConnected) connection.disconnect queryResult match { case row: ArrayRowData => // If we got a row from the database if (row(0).asInstanceOf[Int] == account.AccountId) { // create char @@ -3658,7 +3462,6 @@ class WorldSessionActor extends Actor case _ => // If the char name didn't exist in the database, create char self ! CreateCharacter(name, head, voice, gender, empire) } - if(connection.isConnected) connection.disconnect case scala.util.Failure(e) => if(connection.isConnected) connection.disconnect sendResponse(ActionResultMessage.Fail(4)) @@ -3701,6 +3504,7 @@ class WorldSessionActor extends Actor "SELECT id, name, faction_id, gender_id, head_id, voice_id FROM characters where id=?", Array(charId) )).onComplete { case Success(queryResult) => + if(connection.isConnected) connection.disconnect queryResult match { case row : ArrayRowData => val lName : String = row(1).asInstanceOf[String] @@ -3719,7 +3523,6 @@ class WorldSessionActor extends Actor case _ => log.error(s"CharacterRequest/Select: no character for $charId found") } - if(connection.isConnected) connection.disconnect case e => if(connection.isConnected) connection.disconnect log.error(s"CharacterRequest/Select: toto tata; unexpected query result format - ${e.getClass}") @@ -3756,7 +3559,6 @@ class WorldSessionActor extends Actor sendResponse(ContinentalLockUpdateMessage(13, PlanetSideEmpire.VS)) // "The VS have captured the VS Sanctuary." sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks - //(0 to 255).foreach(i => { sendResponse(SetEmpireMessage(PlanetSideGUID(i), PlanetSideEmpire.VS)) }) //find and reclaim own deployables, if any val guid = player.GUID @@ -3810,13 +3612,22 @@ class WorldSessionActor extends Actor } } }) + //sensor animation normal - .filter(_.Definition.DeployCategory == DeployableCategory.Sensors) + .filter(obj => + obj.Definition.DeployCategory == DeployableCategory.Sensors && + !obj.Destroyed && + (obj match { case jObj : JammableUnit => !jObj.Jammed; case _ => true }) + ) .foreach(obj => { sendResponse(TriggerEffectMessage(obj.GUID, "on", true, 1000)) }) + //update the health of our faction's deployables (if necessary) //draw our faction's deployables on the map continent.DeployableList - .filter(obj => obj.Faction == faction && obj.Health > 0) + .filter(obj => obj.Faction == faction && !obj.Destroyed) .foreach(obj => { + if(obj.Health != obj.DefaultHealth) { + sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health)) + } val deployInfo = DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, obj.Owner.getOrElse(PlanetSideGUID(0))) sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo)) }) @@ -3847,13 +3658,15 @@ class WorldSessionActor extends Actor } //load vehicles in zone (put separate the one we may be using) val (wreckages, (vehicles, usedVehicle)) = { - val (a, b) = continent.Vehicles.partition(vehicle => { vehicle.Health == 0 && vehicle.Definition.DestroyedModel.nonEmpty }) + val (a, b) = continent.Vehicles.partition(vehicle => { vehicle.Destroyed && vehicle.Definition.DestroyedModel.nonEmpty }) (a, (continent.GUID(player.VehicleSeated) match { case Some(vehicle : Vehicle) if vehicle.PassengerInSeat(player).isDefined => b.partition { _.GUID != vehicle.GUID } - case None => + case Some(_) => + //vehicle, but we're not seated in it + player.VehicleSeated = None (b, List.empty[Vehicle]) - case _ => + case None => //throw error since VehicleSeated didn't point to a vehicle? player.VehicleSeated = None (b, List.empty[Vehicle]) @@ -3879,8 +3692,10 @@ class WorldSessionActor extends Actor ) ) }) - Vehicles.ReloadAccessPermissions(vehicle, player.Name) }) + vehicles.collect { case vehicle if vehicle.Faction == faction => + Vehicles.ReloadAccessPermissions(vehicle, player.Name) + } //our vehicle would have already been loaded; see NewPlayerLoaded/AvatarCreate usedVehicle.headOption match { case Some(vehicle) => @@ -3904,7 +3719,11 @@ class WorldSessionActor extends Actor (0 to 3).foreach { group => sendResponse(PlanetsideAttributeMessage(vguid, group + 10, vehicle.PermissionGroup(group).get.id)) } - case _ => ; //driver, or no vehicle + //positive shield strength + if(vehicle.Shields > 0) { + sendResponse(PlanetsideAttributeMessage(vguid, 68, vehicle.Shields)) + } + case _ => ; //no vehicle } //vehicle wreckages wreckages.foreach(vehicle => { @@ -3919,7 +3738,7 @@ class WorldSessionActor extends Actor //cargo occupants (including our own vehicle as cargo) vehicles.collect { case vehicle if vehicle.CargoHolds.nonEmpty => vehicle.CargoHolds.collect({ case (index, hold) if hold.isOccupied => { - CargoMountBehaviorForAll(vehicle, hold.Occupant.get, index) //CargoMountBehaviorForUs can fail to attach the cargo vehicle on some clients + CargoBehavior.CargoMountBehaviorForAll(vehicle, hold.Occupant.get, index) //CargoMountBehaviorForUs can fail to attach the cargo vehicle on some clients }}) } //special deploy states @@ -4200,6 +4019,7 @@ class WorldSessionActor extends Actor continent.Population ! Zone.Population.Release(avatar) player.VehicleSeated match { case None => + log.info("not in vehicle") PrepareToTurnPlayerIntoCorpse(player, continent) case Some(_) => @@ -4344,7 +4164,7 @@ class WorldSessionActor extends Actor case Some(pad : VehicleSpawnPad) => pad.Actor ! VehicleSpawnControl.ProcessControl.Flush case Some(turret : FacilityTurret) if turret.isUpgrading => - FinishUpgradingMannedTurret(turret, TurretUpgrade.None) + WeaponTurrets.FinishUpgradingMannedTurret(turret, TurretUpgrade.None) case _ => self ! PacketCoding.CreateGamePacket(0, RequestDestroyMessage(PlanetSideGUID(guid))) } @@ -4531,7 +4351,7 @@ class WorldSessionActor extends Actor continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Start(playerGUID, item_guid)) continent.GUID(trigger.Companion) match { case Some(boomer : BoomerDeployable) => - boomer.Exploded = true + boomer.Destroyed = true continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.Detonate(boomer.GUID, boomer)) Deployables.AnnounceDestroyDeployable(boomer, Some(500 milliseconds)) case Some(_) | None => ; @@ -4646,7 +4466,7 @@ class WorldSessionActor extends Actor case Some(unholsteredItem : Equipment) => if(unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) { // Player has unholstered a REK - we need to set an atttribute on the REK itself to change the beam/icon colour to the correct one for the player's hack level - continent.AvatarEvents ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, GetPlayerHackLevel())) + continent.AvatarEvents ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, Player.GetHackLevel(player))) } case None => ; } @@ -4703,9 +4523,9 @@ class WorldSessionActor extends Actor // TODO: Make sure this is the correct response for all cases ValidObject(object_guid) match { case Some(vehicle : Vehicle) => - if((player.VehicleOwned.contains(object_guid) && vehicle.Owner.contains(player.GUID)) - || (player.Faction == vehicle.Faction - && ((vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Health == 0))) { + if((player.VehicleOwned.contains(object_guid) && vehicle.Owner.contains(player.GUID)) || + (player.Faction == vehicle.Faction && + ((vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Destroyed))) { continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), continent)) continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent, Some(0 seconds))) log.info(s"RequestDestroy: vehicle $vehicle") @@ -4911,6 +4731,10 @@ class WorldSessionActor extends Actor //log.info("UseItem: " + msg) // TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok) // TODO: Not all incoming UseItemMessage's respond with another UseItemMessage (i.e. doors only send out GenericObjectStateMsg) + val equipment = player.Slot(player.DrawnSlot).Equipment match { + case out @ Some(item) if item.GUID == item_used_guid => out + case _ => None + } ValidObject(object_guid) match { case Some(door : Door) => if(player.Faction == door.Faction || (continent.Map.DoorToLock.get(object_guid.guid) match { @@ -4938,44 +4762,13 @@ class WorldSessionActor extends Actor } case Some(resourceSilo : ResourceSilo) => - log.info(s"UseItem: Vehicle $avatar_guid is refilling resource silo $object_guid") - val vehicle = continent.GUID(avatar_guid).get.asInstanceOf[Vehicle] - - if(resourceSilo.Faction == PlanetSideEmpire.NEUTRAL || player.Faction == resourceSilo.Faction) { - if(vehicle.Seat(0).get.Occupant.contains(player)) { - log.trace("UseItem: Player matches vehicle driver. Calling ResourceSilo.Use") - resourceSilo.Actor ! ResourceSilo.Use(player, msg) - } - } else { - log.warn(s"Player ${player.GUID} - ${player.Faction} tried to refill silo ${resourceSilo.GUID} - ${resourceSilo.Faction} belonging to another empire") - } + resourceSilo.Actor ! ResourceSilo.Use(player, msg) case Some(panel : IFFLock) => - if((panel.Faction != player.Faction && panel.HackedBy.isEmpty) || (panel.Faction == player.Faction && panel.HackedBy.isDefined)) { - player.Slot(player.DrawnSlot).Equipment match { - case Some(tool : SimpleItem) => - if(tool.Definition == GlobalDefinitions.remote_electronics_kit) { - val hackSpeed = GetPlayerHackSpeed(panel) - - if(hackSpeed > 0) { - progressBarValue = Some(-hackSpeed) - if(panel.Faction != player.Faction) { - // Enemy faction is hacking this IFF lock - self ! WorldSessionActor.HackingProgress(progressType = 1, player, panel, tool.GUID, hackSpeed, FinishHacking(panel, 1114636288L)) - log.info("Hacking an IFF lock") - } else { - // IFF Lock is being resecured by it's owner faction - self ! WorldSessionActor.HackingProgress(progressType = 1, player, panel, tool.GUID, hackSpeed, FinishResecuringIFFLock(panel)) - log.info("Resecuring an IFF lock") - } - } - } - case _ => ; - } - } else { - log.warn("IFF lock is being hacked, but don't know how to handle this state") - log.warn(s"Lock - HackedBy.isDefined: ${panel.HackedBy.isDefined} Faction: ${panel.Faction} HackedBy.isEmpty: ${panel.HackedBy.isEmpty}") - log.warn(s"Hacking player - Faction: ${player.Faction}") + equipment match { + case Some(item) => + panel.Actor ! CommonMessages.Use(player, Some(item)) + case _ => ; } case Some(obj : Player) => @@ -4985,7 +4778,7 @@ class WorldSessionActor extends Actor accessedContainer = Some(obj) } else if(!unk3 && player.isAlive) { //potential kit use - ValidObject(item_used_guid) match { + equipment match { case Some(kit : Kit) => player.Find(kit) match { case Some(index) => @@ -5097,167 +4890,79 @@ class WorldSessionActor extends Actor } } else if (itemType == ObjectClass.avatar && unk3) { - FindWeapon match { - case Some(tool: Tool) => - if (tool.Definition == GlobalDefinitions.bank) { - ValidObject(object_guid) match { - case Some(tplayer: Player) => - if (player.GUID != tplayer.GUID && Vector3.Distance(player.Position, tplayer.Position) < 5 && player.Faction == tplayer.Faction && !player.isMoving && tplayer.MaxArmor > 0 && tplayer.Armor < tplayer.MaxArmor) { - tplayer.Armor += 15 - tool.Discharge - sendResponse(InventoryStateMessage(tool.AmmoSlot.Box.GUID, obj.GUID, tool.Magazine)) - val RepairPercent: Int = tplayer.Armor * 100 / tplayer.MaxArmor - sendResponse(RepairMessage(object_guid, RepairPercent)) - continent.AvatarEvents ! AvatarServiceMessage(tplayer.Continent, AvatarAction.PlanetsideAttributeToAll(tplayer.GUID, 4, tplayer.Armor)) - } else if (player.GUID == tplayer.GUID && !player.isMoving && tplayer.MaxArmor > 0) { - player.Armor += 15 - tool.Discharge - sendResponse(InventoryStateMessage(tool.AmmoSlot.Box.GUID, obj.GUID, tool.Magazine)) - continent.AvatarEvents ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttributeToAll(player.GUID, 4, player.Armor)) - } - case _ => ; - } - } else if (tool.Definition == GlobalDefinitions.medicalapplicator) { - continent.GUID(object_guid) match { - case Some(tplayer: Player) => - if (player.GUID != tplayer.GUID && Vector3.Distance(player.Position, tplayer.Position) < 5 && player.Faction == tplayer.Faction && !player.isMoving && tplayer.MaxHealth > 0 && tplayer.Health < tplayer.MaxHealth) { - if(tplayer.isAlive) { - tplayer.Health += 10 - } else { - // Reviving another player is normally 25 "medical energy" (ammo) and 5,000 milliseconds duration, based on the game properties revive_ammo_required and revive_time - //todo: @NotEnoughAmmoToRevive=You do not have enough medical energy to revive this corpse. - tplayer.Health += 4 // 4 health per tick = 5 second revive timer from 0 health - } - tool.Discharge - sendResponse(InventoryStateMessage(tool.AmmoSlot.Box.GUID, obj.GUID, tool.Magazine)) - val repairPercent: Int = tplayer.Health * 100 / tplayer.MaxHealth - sendResponse(RepairMessage(object_guid, repairPercent)) + equipment match { + case Some(tool: Tool) if tool.Definition == GlobalDefinitions.bank => + obj.Actor ! CommonMessages.Use(player, equipment) - if(!tplayer.isAlive && tplayer.Health == tplayer.MaxHealth) { - tplayer.Revive - continent.AvatarEvents ! AvatarServiceMessage(tplayer.Continent, AvatarAction.Revive(tplayer.GUID)) - } + case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => + obj.Actor ! CommonMessages.Use(player, equipment) - if(tplayer.isAlive) { - continent.AvatarEvents ! AvatarServiceMessage(tplayer.Continent, AvatarAction.PlanetsideAttributeToAll(tplayer.GUID, 0, tplayer.Health)) - } - } else if (player.GUID == tplayer.GUID && !player.isMoving && tplayer.MaxHealth > 0 && player.isAlive) { - player.Health += 10 - tool.Discharge - sendResponse(InventoryStateMessage(tool.AmmoSlot.Box.GUID, obj.GUID, tool.Magazine)) - continent.AvatarEvents ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttributeToAll(player.GUID, 0, player.Health)) - } - case _ => ; - } - } - case None => ; + case _ => ; } } case Some(locker : Locker) => - if(locker.Faction != player.Faction && locker.HackedBy.isEmpty) { - player.Slot(player.DrawnSlot).Equipment match { - case Some(tool: SimpleItem) => - if (tool.Definition == GlobalDefinitions.remote_electronics_kit) { - val hackSpeed = GetPlayerHackSpeed(locker) - - if(hackSpeed > 0) { - progressBarValue = Some(-hackSpeed) - self ! WorldSessionActor.HackingProgress(progressType = 1, player, locker, tool.GUID, hackSpeed, FinishHacking(locker, 3212836864L)) - log.info("Hacking a locker") - } - } - case _ => ; - } - } else if(player.Faction == locker.Faction || !locker.HackedBy.isEmpty) { - log.info(s"UseItem: $player accessing a locker") - val container = player.Locker - accessedContainer = Some(container) - sendResponse(UseItemMessage(avatar_guid, item_used_guid, container.GUID, unk2, unk3, unk4, unk5, unk6, unk7, unk8, 456)) - } - else { - log.info(s"UseItem: not $player's locker") + equipment match { + case Some(item) => + locker.Actor ! CommonMessages.Use(player, Some(item)) + case None if locker.Faction == player.Faction || !locker.HackedBy.isEmpty => + log.trace(s"UseItem: $player accessing a locker") + val container = player.Locker + accessedContainer = Some(container) + sendResponse(UseItemMessage(avatar_guid, item_used_guid, container.GUID, unk2, unk3, unk4, unk5, unk6, unk7, unk8, 456)) + case _ => ; } - case Some(implant_terminal : ImplantTerminalMech) => - if(implant_terminal.Faction != player.Faction && implant_terminal.HackedBy.isEmpty) { - player.Slot(player.DrawnSlot).Equipment match { - case Some(tool: SimpleItem) => - if (tool.Definition == GlobalDefinitions.remote_electronics_kit) { - val hackSpeed = GetPlayerHackSpeed(implant_terminal) + case Some(gen : Generator) => + equipment match { + case Some(item) => + gen.Actor ! CommonMessages.Use(player, Some(item)) + case None => ; + } - if(hackSpeed > 0) { - progressBarValue = Some(-hackSpeed) - self ! WorldSessionActor.HackingProgress(progressType = 1, player, implant_terminal, tool.GUID, hackSpeed, FinishHacking(implant_terminal, 3212836864L)) - log.info("Hacking an implant terminal") - } - } - case _ => ; - } + case Some(mech : ImplantTerminalMech) => + equipment match { + case Some(item) => + mech.Actor ! CommonMessages.Use(player, Some(item)) + case None => ; } case Some(captureTerminal : CaptureTerminal) => - val hackedByCurrentFaction = (captureTerminal.Faction != player.Faction && !captureTerminal.HackedBy.isEmpty && captureTerminal.HackedBy.get.hackerFaction == player.Faction) - val ownedByPlayerFactionAndHackedByEnemyFaction = (captureTerminal.Faction == player.Faction && !captureTerminal.HackedBy.isEmpty) - if(!hackedByCurrentFaction || ownedByPlayerFactionAndHackedByEnemyFaction) { - player.Slot(player.DrawnSlot).Equipment match { - case Some(tool: SimpleItem) => - if (tool.Definition == GlobalDefinitions.remote_electronics_kit) { - val hackSpeed = GetPlayerHackSpeed(captureTerminal) - - if(hackSpeed > 0) { - progressBarValue = Some(-hackSpeed) - self ! WorldSessionActor.HackingProgress(progressType = 1, player, captureTerminal, tool.GUID, hackSpeed, FinishHacking(captureTerminal, 3212836864L)) - log.info("Hacking a capture terminal") - } - } - case _ => ; - } + equipment match { + case Some(item) => + captureTerminal.Actor ! CommonMessages.Use(player, Some(item)) + case _ => ; } case Some(obj : FacilityTurret) => - player.Slot(player.DrawnSlot).Equipment match { - case Some(tool : Tool) => - if(tool.Definition == GlobalDefinitions.nano_dispenser && tool.Magazine > 0) { - val ammo = tool.AmmoType - if(ammo == Ammo.upgrade_canister && obj.Seats.values.count(_.isOccupied) == 0) { + val tdef = obj.Definition + (obj.Owner, equipment) match { + case (b : Building, Some(gluegun : Tool)) if b.Faction == player.Faction && + gluegun.Definition == GlobalDefinitions.nano_dispenser => + gluegun.AmmoType match { + case Ammo.armor_canister => //repair + obj.Actor ! CommonMessages.Use(player, Some(gluegun)) + + case Ammo.upgrade_canister if gluegun.Magazine > 0 && obj.Seats.values.forall(!_.isOccupied) => //upgrade progressBarValue = Some(-1.25f) - self ! WorldSessionActor.HackingProgress( - progressType = 2, - player, - obj, - tool.GUID, + self ! Progress( delta = 1.25f, - FinishUpgradingMannedTurret(obj, tool, TurretUpgrade(unk2.toInt)) + WeaponTurrets.FinishUpgradingMannedTurret(obj, player, gluegun, TurretUpgrade(unk2.toInt)), + GenericHackables.HackingTickAction(progressType = 2, player, obj, gluegun.GUID) ) - } - else if(ammo == Ammo.armor_canister && obj.Health < obj.MaxHealth) { - //repair turret - obj.Health += 48 - if (obj.Health > obj.MaxHealth) obj.Health = obj.MaxHealth - // sendResponse(QuantityUpdateMessage(PlanetSideGUID(8214),ammo_quantity_left)) - val RepairPercent: Int = obj.Health * 100 / obj.MaxHealth - sendResponse(RepairMessage(object_guid, RepairPercent)) - continent.AvatarEvents ! AvatarServiceMessage(obj.Continent, AvatarAction.PlanetsideAttribute(obj.GUID, 0, obj.Health)) - } - } - else if(tool.Definition == GlobalDefinitions.trek) { - //infect turret with virus + + case _ => ; } + case _ => ; } case Some(obj : Vehicle) => - val equipment = player.Slot(player.DrawnSlot).Equipment - if(player.Faction == obj.Faction) { - if(equipment match { - case Some(tool : Tool) => - tool.Definition match { - case GlobalDefinitions.nano_dispenser => false - case _ => true - } - case _ => true - }) { + equipment match { + case Some(item) => + obj.Actor ! CommonMessages.Use(player, Some(item)) + + case None if player.Faction == obj.Faction => //access to trunk if(obj.AccessingTrunk.isEmpty && (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.Owner.contains(player.GUID))) { @@ -5266,80 +4971,22 @@ class WorldSessionActor extends Actor AccessContents(obj) sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) } - else { - log.info(s"UseItem: $obj's trunk is not currently accessible for $player") - } - } - else if(equipment.isDefined) { - equipment.get.Definition match { - case GlobalDefinitions.nano_dispenser => - if (!player.isMoving && Vector3.Distance(player.Position, obj.Position) < 5) { - if (obj.Health < obj.MaxHealth && !obj.IsDead) { - obj.Health += 48 - // sendResponse(QuantityUpdateMessage(PlanetSideGUID(8214),ammo_quantity_left)) - val RepairPercent: Int = obj.Health * 100 / obj.MaxHealth - sendResponse(RepairMessage(object_guid, RepairPercent)) - continent.AvatarEvents ! AvatarServiceMessage(obj.Continent, AvatarAction.PlanetsideAttribute(obj.GUID, 0, obj.Health)) - } - } - - case _ => ; - } - } - } - //enemy player interactions - else if(equipment.isDefined) { - equipment.get.Definition match { - case GlobalDefinitions.remote_electronics_kit => - val hackSpeed = GetPlayerHackSpeed(obj) - - if(hackSpeed > 0) { - progressBarValue = Some(-hackSpeed) - self ! WorldSessionActor.HackingProgress(progressType = 1, player, obj, equipment.get.GUID, hackSpeed, FinishHackingVehicle(obj, 3212836864L)) - log.info("Hacking a vehicle") - } - case _ => ; - } - } - - case Some(terminal : Terminal) => - val tdef = terminal.Definition - - // If the base this terminal belongs to has been hacked the owning faction needs to be able to hack it to gain access - val ownerIsHacked = terminal.Owner match { - case b: Building => b.CaptureConsoleIsHacked - case _ => false - } - var playerIsHacking = false - - player.Slot(player.DrawnSlot).Equipment match { - case Some(tool: SimpleItem) => - if (tool.Definition == GlobalDefinitions.remote_electronics_kit) { - if (!terminal.HackedBy.isEmpty) { - log.warn("Player tried to hack a terminal that is already hacked") - log.warn(s"Player faction ${player.Faction} terminal faction: ${terminal.Faction} terminal hacked: ${terminal.HackedBy.isDefined} owner hacked: ${ownerIsHacked}") - } - else if (terminal.Faction != player.Faction || ownerIsHacked) { - val hackSpeed = GetPlayerHackSpeed(terminal) - - if (hackSpeed > 0) { - progressBarValue = Some(-hackSpeed) - self ! WorldSessionActor.HackingProgress(progressType = 1, player, terminal, tool.GUID, hackSpeed, FinishHacking(terminal, 3212836864L)) - playerIsHacking = true - log.info("Hacking a terminal") - } - } - } case _ => ; } - if(!playerIsHacking) { - if (terminal.Faction == player.Faction) { - if (tdef.isInstanceOf[MatrixTerminalDefinition]) { + case Some(terminal : Terminal) => + log.info(s"$msg") + equipment match { + case Some(item) => + terminal.Actor ! CommonMessages.Use(player, Some(item)) + + case None if terminal.Faction == player.Faction || terminal.HackedBy.nonEmpty => + val tdef = terminal.Definition + if(tdef.isInstanceOf[MatrixTerminalDefinition]) { //TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks) sendResponse(BindPlayerMessage(BindStatus.Bind, "", true, true, SpawnGroup.Sanctuary, 0, 0, terminal.Position)) } - else if (tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal || + else if(tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal || tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal) { FindLocalVehicle match { case Some(vehicle) => @@ -5349,37 +4996,60 @@ class WorldSessionActor extends Actor log.error("UseItem: expected seated vehicle, but found none") } } - else if (tdef == GlobalDefinitions.teleportpad_terminal) { + else if(tdef == GlobalDefinitions.teleportpad_terminal) { //explicit request terminal.Actor ! Terminal.Request( player, ItemTransactionMessage(object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0)) ) } - else if (!ownerIsHacked || (ownerIsHacked && terminal.HackedBy.isDefined)) { + else { sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) } - else { - log.warn("Tried to use a terminal, but can't handle this case") - log.warn(s"Terminal - isHacked ${terminal.HackedBy.isDefined} ownerIsHacked ${ownerIsHacked}") - } - } - else if (terminal.HackedBy.isDefined || terminal.Owner.GUID == PlanetSideGUID(0)) { - sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) - } else { - log.warn("Tried to use a terminal that doesn't belong to this faction and isn't hacked") - log.warn(s"Player faction ${player.Faction} terminal faction: ${terminal.Faction} terminal hacked: ${terminal.HackedBy.isDefined} owner hacked: ${ownerIsHacked}") - } - } - case Some(obj : SpawnTube) => - if(item_used_guid == PlanetSideGUID(0)) { // Ensure that we're not trying to use a tool on the spawn tube, e.g. medical applicator - //deconstruction - PlayerActionsToCancel() - CancelAllProximityUnits() - continent.Population ! Zone.Population.Release(avatar) - GoToDeploymentMap() + + case _ => ; } + case Some(obj : SpawnTube) => + equipment match { + case Some(item) => + obj.Actor ! CommonMessages.Use(player, Some(item)) + case None if player.Faction == obj.Faction => + //deconstruction + PlayerActionsToCancel() + CancelAllProximityUnits() + continent.Population ! Zone.Population.Release(avatar) + GoToDeploymentMap() + case _ => ; + } + + case Some(obj : SensorDeployable) => + equipment match { + case Some(item) => + obj.Actor ! CommonMessages.Use(player, Some(item)) + case _ => ; + } + + case Some(obj : TurretDeployable) => + equipment match { + case Some(item) => + obj.Actor ! CommonMessages.Use(player, Some(item)) + case _ => ; + } + + case Some(obj : TrapDeployable) => + equipment match { + case Some(item) => + obj.Actor ! CommonMessages.Use(player, Some(item)) + case _ => ; + } + + case Some(obj : ShieldGeneratorDeployable) => + equipment match { + case Some(item) => + obj.Actor ! CommonMessages.Use(player, Some(item)) + case _ => ; + } case Some(obj : TelepadDeployable) => continent.GUID(obj.Router) match { @@ -5388,10 +5058,10 @@ class WorldSessionActor extends Actor case Some(util : Utility.InternalTelepad) => UseRouterTelepadSystem(router = vehicle, internalTelepad = util, remoteTelepad = obj, src = obj, dest = util) case _ => - log.error(s"telepad@${object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}@${obj.Router.get.guid}") + log.error(s"telepad@${object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}, ${obj.Router}") } case Some(o) => - log.error(s"telepad@${object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}@${obj.Router.get.guid}") + log.error(s"telepad@${object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}") case None => ; } @@ -6009,7 +5679,7 @@ class WorldSessionActor extends Actor case Some(vehicleGUID) => continent.GUID(vehicleGUID) match { case Some(obj : Vehicle) => - if(!obj.IsDead) { //vehicle will try to charge even if destroyed + if(!obj.Destroyed) { //vehicle will try to charge even if destroyed obj.Actor ! Vehicle.ChargeShields(15) } case _ => @@ -6639,155 +6309,6 @@ class WorldSessionActor extends Actor } } - /** - * The process of hacking an object is completed. - * Pass the message onto the hackable object and onto the local events system. - * @param target the `Hackable` object that has been hacked - * @param unk na; - * used by `HackMessage` as `unk5` - * @see `HackMessage` - */ - //TODO add params here depending on which params in HackMessage are important - private def FinishHacking(target : PlanetSideServerObject with Hackable, unk : Long)() : Unit = { - log.info(s"Hacked a $target") - // Wait for the target actor to set the HackedBy property, otherwise LocalAction.HackTemporarily will not complete properly - import scala.concurrent.ExecutionContext.Implicits.global - ask(target.Actor, CommonMessages.Hack(player))(1 second).mapTo[Boolean].onComplete { - case Success(_) => - continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.TriggerSound(player.GUID, target.HackSound, player.Position, 30, 0.49803925f)) - target match { - case term: CaptureTerminal => - val isResecured = player.Faction == target.Faction - continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.HackCaptureTerminal(player.GUID, continent, term, unk, 8L, isResecured)) - case _ => continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.HackTemporarily(player.GUID, continent, target, unk, target.HackEffectDuration(GetPlayerHackLevel()))) - } - case scala.util.Failure(_) => log.warn(s"Hack message failed on target guid: ${target.GUID}") - } - } - - /** - * The process of hacking/jacking a vehicle is complete. - * Change the faction of the vehicle to the hacker's faction and remove all occupants. - * - * @param target The `Vehicle` object that has been hacked/jacked - * @param unk na; used by HackMessage` as `unk5` - */ - private def FinishHackingVehicle(target : Vehicle, unk : Long)(): Unit = { - log.info(s"Vehicle guid: ${target.GUID} has been jacked") - - - // Forcefully dismount any cargo - target.CargoHolds.values.foreach(cargoHold => { - cargoHold.Occupant match { - case Some(cargo : Vehicle) => { - cargo.Seats(0).Occupant match { - case Some(cargoDriver: Player) => - DismountVehicleCargo(cargoDriver.GUID, cargo.GUID, bailed = target.Flying, requestedByPassenger = false, kicked = true ) - case None => - log.error("FinishHackingVehicle: vehicle in cargo hold missing driver") - HandleDismountVehicleCargo(player.GUID, cargo.GUID, cargo, target.GUID, target, false, false, true) - } - } - case None => ; - } - }) - - // Forcefully dismount all seated occupants from the vehicle - target.Seats.values.foreach(seat => { - seat.Occupant match { - case Some(tplayer) => - seat.Occupant = None - tplayer.VehicleSeated = None - if(tplayer.HasGUID) { - continent.VehicleEvents ! VehicleServiceMessage(tplayer.Continent, VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, target.GUID)) - } - case None => ; - } - }) - - // If the vehicle can fly and is flying deconstruct it, and well played to whomever managed to hack a plane in mid air. I'm impressed. - if(target.Definition.CanFly && target.Flying) { - // todo: Should this force the vehicle to land in the same way as when a pilot bails with passengers on board? - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), continent)) - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, continent, Some(0 seconds))) - } else { // Otherwise handle ownership transfer as normal - // Remove ownership of our current vehicle, if we have one - player.VehicleOwned match { - case Some(guid : PlanetSideGUID) => - continent.GUID(guid) match { - case Some(vehicle: Vehicle) => - Vehicles.Disown(player, vehicle) - case _ => ; - } - case _ => ; - } - - target.Owner match { - case Some(previousOwnerGuid: PlanetSideGUID) => - // Remove ownership of the vehicle from the previous player - continent.GUID(previousOwnerGuid) match { - case Some(player: Player) => - Vehicles.Disown(player, target) - case _ => ; // Vehicle already has no owner - } - case _ => ; - } - - // Now take ownership of the jacked vehicle - target.Faction = player.Faction - Vehicles.Own(target, player) - - //todo: Send HackMessage -> HackCleared to vehicle? can be found in packet captures. Not sure if necessary. - - // And broadcast the faction change to other clients - sendResponse(SetEmpireMessage(target.GUID, player.Faction)) - continent.AvatarEvents ! AvatarServiceMessage(player.Continent, AvatarAction.SetEmpire(player.GUID, target.GUID, player.Faction)) - } - - continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.TriggerSound(player.GUID, TriggeredSound.HackVehicle, target.Position, 30, 0.49803925f)) - - // Clean up after specific vehicles, e.g. remove router telepads - // If AMS is deployed, swap it to the new faction - target.Definition match { - case GlobalDefinitions.router => - log.info("FinishHackingVehicle: cleaning up after a router ...") - RemoveTelepads(target) - case GlobalDefinitions.ams - if(target.DeploymentState == DriveState.Deployed) => - continent.VehicleEvents ! VehicleServiceMessage.AMSDeploymentChange(continent) - case _ => ; - } - } - - /** - * The process of resecuring an IFF lock is finished - * Clear the hack state and send to clients - * @param lock the `IFFLock` object that has been resecured - */ - private def FinishResecuringIFFLock(lock: IFFLock)() : Unit = { - continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.ClearTemporaryHack(player.GUID, lock)) - } - - /** - * The process of upgrading a turret's weapon(s) is completed. - * Pass the message onto the turret and onto the vehicle events system. - * Additionally, force-deplete the ammunition count of the nano-dispenser used to perform the upgrade. - * @param target the turret - * @param tool the nano-dispenser that was used to perform this upgrade - * @param upgrade the new upgrade state - */ - private def FinishUpgradingMannedTurret(target : FacilityTurret, tool : Tool, upgrade : TurretUpgrade.Value)() : Unit = { - tool.Magazine = 0 - sendResponse(InventoryStateMessage(tool.AmmoSlot.Box.GUID, tool.GUID, 0)) - FinishUpgradingMannedTurret(target, upgrade) - } - - private def FinishUpgradingMannedTurret(target : FacilityTurret, upgrade : TurretUpgrade.Value) : Unit = { - log.info(s"Converting manned wall turret weapon to $upgrade") - continent.VehicleEvents ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.ClearSpecific(List(target), continent)) - continent.VehicleEvents ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.AddTask(target, continent, upgrade)) - } - /** * Gives a target player positive battle experience points only. * If the player has access to more implant slots as a result of changing battle experience points, unlock those slots. @@ -7123,7 +6644,11 @@ class WorldSessionActor extends Actor */ private def ModifyAmmunitionInVehicle(obj : Vehicle)(box : AmmoBox, reloadValue : Int) : Unit = { val capacity = ModifyAmmunition(obj)(box, reloadValue) - continent.VehicleEvents ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.InventoryState(player.GUID, box, obj.GUID, obj.Find(box).get, box.Definition.Packet.DetailedConstructorData(box).get)) + obj.Find(box) match { + case Some(index) => + continent.VehicleEvents ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.InventoryState(player.GUID, box, obj.GUID, index, box.Definition.Packet.DetailedConstructorData(box).get)) + case None => ; + } } /** @@ -7651,14 +7176,14 @@ class WorldSessionActor extends Actor state match { case DriveState.Deployed => // We only want this WSA (not other player's WSA) to manage timers - if(vehicle.Seat(0).get.Occupant.contains(player)){ + if(vehicle.Seats(0).Occupant.contains(player)){ // Start ntu regeneration // If vehicle sends UseItemMessage with silo as target NTU regeneration will be disabled and orb particles will be disabled antChargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuCharging(player, vehicle)) } case DriveState.Undeploying => // We only want this WSA (not other player's WSA) to manage timers - if(vehicle.Seat(0).get.Occupant.contains(player)){ + if(vehicle.Seats(0).Occupant.contains(player)){ antChargingTick.cancel() // Stop charging NTU if charging } @@ -7848,47 +7373,145 @@ class WorldSessionActor extends Actor if(building.IsCapitol && building.ForceDomeActive) { sendResponse(GenericObjectActionMessage(building.GUID, 13)) } - - building.Amenities.foreach(amenity => { - val amenityId = amenity.GUID - sendResponse(PlanetsideAttributeMessage(amenityId, 50, 0)) - sendResponse(PlanetsideAttributeMessage(amenityId, 51, 0)) - - amenity.Definition match { - case GlobalDefinitions.resource_silo => - // Synchronise warning light & silo capacity - val silo = amenity.asInstanceOf[ResourceSilo] - sendResponse(PlanetsideAttributeMessage(amenityId, 45, silo.CapacitorDisplay)) - sendResponse(PlanetsideAttributeMessage(silo.Owner.GUID, 47, if(silo.LowNtuWarningOn) 1 else 0)) - - if(silo.ChargeLevel == 0) { - sendResponse(PlanetsideAttributeMessage(silo.Owner.GUID, 48, 1)) - } - case _ => ; - } - - // Synchronise hack states to clients joining the zone. - // We'll have to fake LocalServiceResponse messages to self, otherwise it means duplicating the same hack handling code twice - if(amenity.isInstanceOf[Hackable]) { - val hackable = amenity.asInstanceOf[Hackable] - - if(hackable.HackedBy.isDefined) { - amenity.Definition match { - case GlobalDefinitions.capture_terminal => - self ! LocalServiceResponse("", PlanetSideGUID(0), LocalResponse.HackCaptureTerminal(amenity.GUID, 0L, 0L, false)) - case _ => - // Generic hackable object - self ! LocalServiceResponse("", PlanetSideGUID(0), LocalResponse.HackObject(amenity.GUID, 1114636288L, 8L)) - } - } - } - }) - -// sendResponse(HackMessage(3, PlanetSideGUID(building.ModelId), PlanetSideGUID(0), 0, 3212836864L, HackState.HackCleared, 8)) + // Synchronise amenities + building.Amenities.collect { + case obj if obj.Destroyed => configAmenityAsDestroyed(obj) + case obj => configAmenityAsWorking(obj) + } Thread.sleep(connectionState) }) } + /** + * Configure the specific working amenity by sending the client packets. + * Amenities that are not `Damageable` are also included. + * These actions are performed during the loading of a zone. + * @see `Door` + * @see `GenericObjectStateMsg` + * @see `Hackable` + * @see `HackCaptureTerminal` + * @see `HackObject` + * @see `PlanetsideAttributeMessage` + * @see `ResourceSilo` + * @see `SetEmpireMessage` + * @see `VitalityDefinition.Damageable` + * @param amenity the facility object + */ + def configAmenityAsWorking(amenity : Amenity) : Unit = { + val amenityId = amenity.GUID + //sync model access state + sendResponse(PlanetsideAttributeMessage(amenityId, 50, 0)) + sendResponse(PlanetsideAttributeMessage(amenityId, 51, 0)) + //sync damageable, if + val health = amenity.Health + if(amenity.Definition.Damageable && health < amenity.MaxHealth) { + sendResponse(PlanetsideAttributeMessage(amenityId, 0, health)) + } + //sync special object type cases + amenity match { + case silo : ResourceSilo => + //silo capacity + sendResponse(PlanetsideAttributeMessage(amenityId, 45, silo.CapacitorDisplay)) + //warning lights + sendResponse(PlanetsideAttributeMessage(silo.Owner.GUID, 47, if(silo.LowNtuWarningOn) 1 else 0)) + if(silo.ChargeLevel == 0) { + sendResponse(PlanetsideAttributeMessage(silo.Owner.GUID, 48, 1)) + } + case door : Door if door.isOpen => + sendResponse(GenericObjectStateMsg(amenityId, 16)) + + case _ => ; + } + //sync hack state + amenity match { + case obj : Hackable if obj.HackedBy.nonEmpty => + amenity.Definition match { + case GlobalDefinitions.capture_terminal => + HackCaptureTerminal(amenity.GUID, 0L, 0L, false) + case _ => + HackObject(amenity.GUID, 1114636288L, 8L) //generic hackable object + } + case _ => ; + } + } + + /** + * Configure the specific destroyed amenity by sending the client packets. + * These actions are performed during the loading of a zone. + * @see `Generator` + * @see `ImplantTerminalMech` + * @see `PlanetsideAttributeMessage` + * @see `PlanetSideGameObject.Destroyed` + * @param amenity the facility object + */ + def configAmenityAsDestroyed(amenity : Amenity) : Unit = { + val amenityId = amenity.GUID + val configValue = amenity match { + case _ : ImplantTerminalMech => 0 + case _ : Generator => 0 + case _ => 1 + } + //sync model access state + sendResponse(PlanetsideAttributeMessage(amenityId, 50, configValue)) + sendResponse(PlanetsideAttributeMessage(amenityId, 51, configValue)) + //sync damageable, if + if(amenity.Definition.Damageable) { + sendResponse(PlanetsideAttributeMessage(amenityId, 0, 0)) + } + } + + /** + * na + * @param target_guid na + * @param unk1 na + * @param unk2 na + */ + def HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) : Unit = { + sendResponse(HackMessage(0, target_guid, PlanetSideGUID(0), 100, unk1, HackState.Hacked, unk2)) + } + + /** + * na + * @param target_guid na + * @param unk1 na + * @param unk2 na + * @param isResecured na + */ + def HackCaptureTerminal(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long, isResecured : Boolean) : Unit = { + var value = 0L + if(isResecured) { + value = 17039360L + sendResponse(PlanetsideAttributeMessage(target_guid, 20, value)) + } + else { + continent.GUID(target_guid) match { + case Some(capture_terminal : Amenity with Hackable) => + capture_terminal.HackedBy match { + case Some(Hackable.HackInfo(_, _, hfaction, _, start, length)) => + val hack_time_remaining_ms = TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS) + val deciseconds_remaining = (hack_time_remaining_ms / 100) + //See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated + val start_num = hfaction match { + case PlanetSideEmpire.TR => 65536L + case PlanetSideEmpire.NC => 131072L + case PlanetSideEmpire.VS => 196608L + } + value = start_num + deciseconds_remaining + sendResponse(PlanetsideAttributeMessage(target_guid, 20, value)) + continent.GUID(player.VehicleSeated) match { + case Some(mountable : Amenity with Mountable) => + if(mountable.Owner.GUID == capture_terminal.Owner.GUID) { + continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player.GUID, mountable.Seats.head._1, true, mountable.GUID)) + } + case _ => ; + } + case _ => log.warn("HackCaptureTerminal: hack state monitor not defined") + } + case _ => log.warn(s"HackCaptureTerminal: couldn't find capture terminal with GUID ${target_guid} in zone ${continent.Id}") + } + } + } + /** * The player has lost the will to live and must be killed. * @see `Vitality`
@@ -7958,9 +7581,9 @@ class WorldSessionActor extends Actor * In reality, they produce the same output but enforce different relationships between the components. * The vehicle without a rendered player will always be created if that vehicle exists. * The vehicle should only be constructed once. - * @see `BeginZoningMessage` - * @see `CargoMountBehaviorForOthers` * @see `AvatarCreateInVehicle` + * @see `BeginZoningMessage` + * @see `CargoBehavior.CargoMountBehaviorForOthers` * @see `GetKnownVehicleAndSeat` * @see `LoadZoneTransferPassengerMessages` * @see `Player.Spawn` @@ -8007,7 +7630,7 @@ class WorldSessionActor extends Actor continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.LoadVehicle(player.GUID, vehicle, vdef.ObjectId, vguid, data)) carrierInfo match { case (Some(carrier), Some((index, _))) => - CargoMountBehaviorForOthers(carrier, vehicle, index) + CargoBehavior.CargoMountBehaviorForOthers(carrier, vehicle, index, player.GUID) case _ => vehicle.MountedIn = None } @@ -8195,7 +7818,7 @@ class WorldSessionActor extends Actor val vdef = vehicle.Definition val vguid = vehicle.GUID if(seat == 0) { - val seat = vehicle.Seat(0).get + val seat = vehicle.Seats(0) seat.Occupant = None val vdata = vdef.Packet.ConstructorData(vehicle).get sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, vdata)) @@ -8410,11 +8033,10 @@ class WorldSessionActor extends Actor } else { continent.GUID(player.VehicleSeated) match { - case Some(obj : Vehicle) if !obj.IsDead => + case Some(obj : Vehicle) if !obj.Destroyed => cluster ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 12) //warp gates for functioning vehicles - case None => - cluster ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 7) //player character spawns case _ => + cluster ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 7) //player character spawns } } } @@ -8784,20 +8406,20 @@ class WorldSessionActor extends Actor * The active `target` and the target of the `ResolvedProjectile` do not have be the same. * While the "tell" for being able to sustain damage is an entity of type `Vitality`, * only specific `Vitality` entity types are being screened for sustaining damage. - * @see `DamageResistanceModel`
- * `Vitality` + * @see `DamageResistanceModel` + * @see `Vitality` * @param target a valid game object that is known to the server * @param data a projectile that will affect the target */ def HandleDealingDamage(target : PlanetSideGameObject with Vitality, data : ResolvedProjectile) : Unit = { val func = data.damage_model.Calculate(data) target match { - case obj : Player if obj.Health > 0 => obj.Actor ! Vitality.Damage(func) - case obj : Vehicle if obj.Health > 0 => obj.Actor ! Vitality.Damage(func) - case obj : FacilityTurret if obj.Health > 0 => obj.Actor ! Vitality.Damage(func) - case obj : ComplexDeployable if obj.Health > 0 => obj.Actor ! Vitality.Damage(func) + case obj : Player if obj.CanDamage => obj.Actor ! Vitality.Damage(func) + case obj : Vehicle if obj.CanDamage => obj.Actor ! Vitality.Damage(func) + case obj : Amenity if obj.CanDamage => obj.Actor ! Vitality.Damage(func) + case obj : ComplexDeployable if obj.CanDamage => obj.Actor ! Vitality.Damage(func) - case obj : SimpleDeployable if obj.Health > 0 => + case obj : SimpleDeployable if obj.CanDamage => //damage is synchronized on `LSA` (results returned to and distributed from this `WSA`) continent.LocalEvents ! Vitality.DamageOn(obj, func) case _ => ; @@ -8915,12 +8537,12 @@ class WorldSessionActor extends Actor * but will not possess a valid GUID for that zone until he spawns in it at least once, * this function is swapped with another after the first spawn in any given zone. * This function is restored upon transferring zones. - * @see `SetCurrentAvatar`
- * `DontRedrawIcons` + * @see `DontRedrawIcons` + * @see `SetCurrentAvatar` * @param obj a `Deployable` object */ def RedrawDeployableIcons(obj : PlanetSideGameObject with Deployable) : Unit = { - val deployInfo = DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, obj.Owner.get) + val deployInfo = DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, obj.Owner.getOrElse(PlanetSideGUID(0))) sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo)) } @@ -8941,8 +8563,8 @@ class WorldSessionActor extends Actor * but will not possess a valid GUID for that zone until he spawns in it at least once, * this function swaps out with another after the first spawn in any given zone. * It stays swapped in until the player changes zones. - * @see `SetCurrentAvatar`
- * `RedrawDeployableIcons` + * @see `RedrawDeployableIcons` + * @see `SetCurrentAvatar` * @param obj a `Deployable` object */ def DontRedrawIcons(obj : PlanetSideGameObject with Deployable) : Unit = { } @@ -8953,8 +8575,8 @@ class WorldSessionActor extends Actor * and each of these sub-modes have certification requirements that must be met before they can be used. * Additional effort is exerted to ensure that the requirements for the given mode and given sub-mode are satisfied. * If no satisfactory combination is achieved, the original state will be restored. - * @see `PerformConstructionItemAmmoChange`
- * `FireModeSwitch.NextFireMode` + * @see `FireModeSwitch.NextFireMode` + * @see `PerformConstructionItemAmmoChange` * @param obj the `ConstructionItem` object * @param originalModeIndex the starting point fire mode index * @return the changed fire mode @@ -9322,44 +8944,6 @@ class WorldSessionActor extends Actor StopBundlingPackets() } - /** - * Distribute information that a deployable has been destroyed. - * The deployable may not have yet been eliminated from the game world (client or server), - * but its health is zero and it has entered the conditions where it is nearly irrelevant.
- *
- * The typical use case of this function involves destruction via weapon fire, attributed to a particular player. - * Contrast this to simply destroying a deployable by being the deployable's owner and using the map icon controls. - * This function eventually invokes the same routine - * but mainly goes into effect when the deployable has been destroyed - * and may still leave a physical component in the game world to be cleaned up later. - * That is the task `EliminateDeployable` performs. - * Additionally, since the player who destroyed the deployable isn't necessarily the owner, - * and the real owner will still be aware of the existence of the deployable, - * that player must be informed of the loss of the deployable directly. - * @see `DeployableRemover` - * @see `Vitality.DamageResolution` - * @see `LocalResponse.EliminateDeployable` - * @see `DeconstructDeployable` - * @param target the deployable that is destroyed - * @param time length of time that the deployable is allowed to exist in the game world; - * `None` indicates the normal un-owned existence time (180 seconds) - */ - def AnnounceDestroyDeployable(target : PlanetSideGameObject with Deployable, time : Option[FiniteDuration]) : Unit = { - target.OwnerName match { - case Some(owner) => - target.OwnerName = None - continent.LocalEvents ! LocalServiceMessage(owner, LocalAction.AlertDestroyDeployable(PlanetSideGUID(0), target)) - case None => ; - } - continent.LocalEvents ! LocalServiceMessage(s"${target.Faction}", LocalAction.DeployableMapIcon( - PlanetSideGUID(0), - DeploymentAction.Dismiss, - DeployableInfo(target.GUID, Deployable.Icon(target.Definition.Item), target.Position, PlanetSideGUID(0))) - ) - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(target), continent)) - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(target, continent, time)) - } - /** * Search through the player's holsters and their inventory space * and remove all `BoomerTrigger` objects, both functionally and visually. @@ -9579,7 +9163,7 @@ class WorldSessionActor extends Actor case ("MISSING_DRIVER", index) => val cargo = vehicle.CargoHolds(index).Occupant.get log.error(s"LoadZoneInVehicleAsDriver: eject cargo in hold $index; vehicle missing driver") - HandleDismountVehicleCargo(pguid, cargo.GUID, cargo, vehicle.GUID, vehicle, false, false, true) + CargoBehavior.HandleVehicleCargoDismount(pguid, cargo.GUID, cargo, vehicle.GUID, vehicle, false, false, true) case (name, index) => val cargo = vehicle.CargoHolds(index).Occupant.get continent.VehicleEvents ! VehicleServiceMessage(name, VehicleAction.TransferPassengerChannel(pguid, s"${cargo.Actor}", toChannel, cargo, topLevel)) @@ -9893,25 +9477,7 @@ class WorldSessionActor extends Actor case GlobalDefinitions.router => //this may repeat for multiple players on the same continent but that's okay(?) log.info("BeforeUnload: cleaning up after a router ...") - RemoveTelepads(vehicle) - case _ => ; - } - } - - def RemoveTelepads(vehicle: Vehicle) : Unit = { - (vehicle.Utility(UtilityType.internal_router_telepad_deployable) match { - case Some(util : Utility.InternalTelepad) => - val telepad = util.Telepad - util.Telepad = None - continent.GUID(telepad) - case _ => - None - }) match { - case Some(telepad : TelepadDeployable) => - log.info(s"BeforeUnload: deconstructing telepad $telepad that was linked to router $vehicle ...") - telepad.Active = false - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(telepad), continent)) - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(telepad, continent, Some(0 seconds))) + Deployables.RemoveTelepad(vehicle) case _ => ; } } @@ -9943,146 +9509,6 @@ class WorldSessionActor extends Actor continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.WeaponDryFire(player.GUID, weapon_guid)) } - /** - * na - * @param player_guid the target player - * @param cargoGUID the globally unique number for the vehicle being ferried - * @param cargo the vehicle being ferried - * @param carrierGUID the globally unique number for the vehicle doing the ferrying - * @param carrier the vehicle doing the ferrying - * @param bailed the ferried vehicle is bailing from the cargo hold - * @param requestedByPassenger the ferried vehicle is being politely disembarked from the cargo hold - * @param kicked the ferried vehicle is being kicked out of the cargo hold - */ - def HandleDismountVehicleCargo(player_guid : PlanetSideGUID, cargoGUID : PlanetSideGUID, cargo : Vehicle, carrierGUID : PlanetSideGUID, carrier : Vehicle, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) : Unit = { - carrier.CargoHolds.find({case((_, hold)) => hold.Occupant.contains(cargo)}) match { - case Some((mountPoint, hold)) => - cargo.MountedIn = None - hold.Occupant = None - val driverOpt = cargo.Seats(0).Occupant - val rotation : Vector3 = if(CargoOrientation(cargo) == 1) { //TODO: BFRs will likely also need this set - //dismount router "sideways" in a lodestar - carrier.Orientation.xy + Vector3.z((carrier.Orientation.z - 90) % 360) - } - else { - carrier.Orientation - } - val cargoHoldPosition : Vector3 = if(carrier.Definition == GlobalDefinitions.dropship) { - //the galaxy cargo bay is offset backwards from the center of the vehicle - carrier.Position + Vector3.Rz(Vector3(0, 7, 0), math.toRadians(carrier.Orientation.z)) - } - else { - //the lodestar's cargo hold is almost the center of the vehicle - carrier.Position - } - StartBundlingPackets() - continent.VehicleEvents ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health))) - continent.VehicleEvents ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))) - if(carrier.Flying) { - //the carrier vehicle is flying; eject the cargo vehicle - val ejectCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.InProgress, 0) - val detachCargoMsg = ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition - Vector3.z(1), rotation) - val resetCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0) - sendResponse(ejectCargoMsg) //dismount vehicle on UI and disable "shield" effect on lodestar - sendResponse(detachCargoMsg) - continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(player_guid, ejectCargoMsg)) - continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(player_guid, detachCargoMsg)) - continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(PlanetSideGUID(0), resetCargoMsg)) //lazy - log.debug(ejectCargoMsg.toString) - log.debug(detachCargoMsg.toString) - if(driverOpt.isEmpty) { - //TODO cargo should drop like a rock like normal; until then, deconstruct it - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(cargo), continent)) - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(cargo, continent, Some(0 seconds))) - } - } - else { - //the carrier vehicle is not flying; just open the door and let the cargo vehicle back out; force it out if necessary - val cargoStatusMessage = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), cargoGUID, PlanetSideGUID(0), mountPoint, CargoStatus.InProgress, 0) - val cargoDetachMessage = ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition + Vector3.z(1f), rotation) - sendResponse(cargoStatusMessage) - sendResponse(cargoDetachMessage) - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player_guid, cargoStatusMessage)) - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player_guid, cargoDetachMessage)) - driverOpt match { - case Some(driver) => - continent.VehicleEvents ! VehicleServiceMessage(s"${driver.Name}", VehicleAction.KickCargo(player_guid, cargo, cargo.Definition.AutoPilotSpeed2, 2500)) - - import scala.concurrent.duration._ - import scala.concurrent.ExecutionContext.Implicits.global - //check every quarter second if the vehicle has moved far enough away to be considered dismounted - cargoDismountTimer.cancel - cargoDismountTimer = context.system.scheduler.scheduleOnce(250 milliseconds, self, CheckCargoDismount(cargoGUID, carrierGUID, mountPoint, iteration = 0)) - case None => - val resetCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0) - continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(PlanetSideGUID(0), resetCargoMsg)) //lazy - //TODO cargo should back out like normal; until then, deconstruct it - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(cargo), continent)) - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(cargo, continent, Some(0 seconds))) - } - } - StopBundlingPackets() - - case None => - log.warn(s"HandleDismountVehicleCargo: can not locate cargo $cargo in any hold of the carrier vehicle $carrier") - } - } - - /** - * na - * @param player_guid na - * @param cargo_guid na - * @param bailed na - * @param requestedByPassenger na - * @param kicked na - */ - def DismountVehicleCargo(player_guid : PlanetSideGUID, cargo_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) : Unit = { - continent.GUID(cargo_guid) match { - case Some(cargo : Vehicle) => - continent.GUID(cargo.MountedIn) match { - case Some(ferry : Vehicle) => - HandleDismountVehicleCargo(player_guid, cargo_guid, cargo, ferry.GUID, ferry, bailed, requestedByPassenger, kicked) - case _ => - log.warn(s"DismountVehicleCargo: target ${cargo.Definition.Name}@$cargo_guid does not know what treats it as cargo") - } - case _ => - log.warn(s"DismountVehicleCargo: target $cargo_guid either is not a vehicle in ${continent.Id} or does not exist") - } - } - - def GetPlayerHackSpeed(obj: PlanetSideServerObject): Float = { - val playerHackLevel = GetPlayerHackLevel() - - val timeToHack = obj match { - case (hackable: Hackable) => hackable.HackDuration(playerHackLevel) - case (vehicle: Vehicle) => vehicle.JackingDuration(playerHackLevel) - case _ => - log.warn(s"Player tried to hack an object that has no hack time defined ${obj.GUID} - ${obj.Definition.Name}") - 0 - } - - if(timeToHack == 0) { - log.warn(s"Player ${player.GUID} tried to hack an object ${obj.GUID} - ${obj.Definition.Name} that they don't have the correct hacking level for") - 0f - } else { - // 250 ms per tick on the hacking progress bar - val ticks = (timeToHack * 1000) / 250 - 100f / ticks - } - } - - def GetPlayerHackLevel(): Int = { - if(player.Certifications.contains(CertificationType.ExpertHacking) || player.Certifications.contains(CertificationType.ElectronicsExpert)) { - 3 - } else if(player.Certifications.contains(CertificationType.AdvancedHacking)) { - 2 - } else if (player.Certifications.contains(CertificationType.Hacking)) { - 1 - } else { - 0 - } - } - def SaveLoadoutToDB(owner : Player, label : String, line : Int) = { val charId = owner.CharId val exosuitId = owner.ExoSuit.id @@ -10187,6 +9613,7 @@ class WorldSessionActor extends Actor "SELECT id, loadout_number, exosuit_id, name, items FROM loadouts where characters_id = ?", Array(owner.CharId) ).onComplete { case Success(queryResult) => + if(connection.isConnected) connection.disconnect queryResult match { case result: QueryResult => if (result.rows.nonEmpty) { @@ -10243,7 +9670,6 @@ class WorldSessionActor extends Actor case _ => log.debug(s"LoadDataBaseLoadouts: no saved loadout(s) for character with id ${owner.CharId}") } - if(connection.isConnected) connection.disconnect result success queryResult case scala.util.Failure(e) => if(connection.isConnected) connection.disconnect @@ -11193,33 +10619,19 @@ object WorldSessionActor { private final case class ListAccountCharacters() private final case class SetCurrentAvatar(tplayer : Player) private final case class VehicleLoaded(vehicle : Vehicle) - private final case class CheckCargoMounting(vehicle_guid : PlanetSideGUID, cargo_vehicle_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int) - private final case class CheckCargoDismount(vehicle_guid : PlanetSideGUID, cargo_vehicle_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int) - /** - * A message that indicates the user is using a remote electronics kit to hack some server object.
- *
- * Each time this message is sent for a given hack attempt counts as a single "tick" of progress. - * The process of "making progress" with a hack involves sending this message repeatedly until the progress is 100 or more. - * To calculate the actual amount of change in the progress `delta`, - * start with 100, divide by the length of time in seconds, then divide once more by 4. - * @param progressType 1 - REK hack - * 2 - Turret upgrade with glue gun + upgrade cannister - * @param tplayer the player - * @param target the object being hacked - * @param tool_guid the REK - * @param delta how much the progress bar value changes each tick - * @param completeAction a custom action performed once the hack is completed - * @param tickAction an optional action is is performed for each tick of progress + * The message that indidctes the level of completion of a process. + * The process is any form of user-driven activity with a certain eventual outcome + * but indeterminate progress feedback per cycle. + * @param delta how much the progress value changes each tick, which will be treated as a percentage; + * must be a positive value + * @param completeAction a finalizing action performed once the progress reaches 100(%) + * @param tickAction an action that is performed for each increase of progress */ - private final case class HackingProgress(progressType : Int, - tplayer : Player, - target : PlanetSideServerObject, - tool_guid : PlanetSideGUID, - delta : Float, - completeAction : () => Unit, - tickAction : Option[() => Unit] = None) + private final case class Progress(delta : Float, completionAction : ()=>Unit, tickAction : Float=>Boolean) { + assert(delta > 0, s"progress activity change value must be positive number - $delta") + } protected final case class SquadUIElement(name : String, index : Int, zone : Int, health : Int, armor : Int, position : Vector3) diff --git a/pslogin/src/main/scala/zonemaps/Map06.scala b/pslogin/src/main/scala/zonemaps/Map06.scala index 5c39e64d2..819be2c05 100644 --- a/pslogin/src/main/scala/zonemaps/Map06.scala +++ b/pslogin/src/main/scala/zonemaps/Map06.scala @@ -365,7 +365,7 @@ object Map06 { // Ceryshen LocalObject(180, CaptureTerminal.Constructor(capture_terminal), owning_building_guid = 20) LocalObject(222, Door.Constructor(Vector3(4012.339f, 4310.464f, 269.8218f)), owning_building_guid = 20) LocalObject(232, Terminal.Constructor(Vector3.Zero, gen_control), owning_building_guid = 20) //TODO placeholder - LocalObject(241, Generator.Constructor, owning_building_guid = 20) + LocalObject(241, Generator.Constructor(Vector3(3970.211f, 4304.297f, 248.04688f)), owning_building_guid = 20) LocalObject(370, Door.Constructor(Vector3(3884.196f, 4196.501f, 268.0948f)), owning_building_guid = 20) LocalObject(371, Door.Constructor(Vector3(3884.196f, 4214.693f, 276.0588f)), owning_building_guid = 20) LocalObject(372, Door.Constructor(Vector3(3901.307f, 4172.197f, 276.0588f)), owning_building_guid = 20)