diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 96f04672..f29757b0 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -20,7 +20,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.util.Success import net.psforever.login.WorldSession._ import net.psforever.objects._ -import net.psforever.objects.avatar.{Avatar, Certification, Cosmetic, DeployableToolbox} +import net.psforever.objects.avatar._ import net.psforever.objects.ballistics._ import net.psforever.objects.ce._ import net.psforever.objects.definition._ @@ -1298,8 +1298,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * The user is either already in the current zone and merely transporting himself from one location to another, * also called "dying", or occasionally "deconstructing," * or is completely switching in between zones. - * These correspond to the message NewPlayerLoaded for the case of "dying" or the latter zone switching case, - * and PlayerLoaded for "deconstruction." + * These correspond to the message `NewPlayerLoaded` for the case of "dying" or the latter zone switching case, + * and `PlayerLoaded` for "deconstruction." * In the latter case, the user must wait for the zone to be recognized as loaded for the server * and this is performed through the send LoadMapMessage, receive BeginZoningMessage exchange * The user's player should have already been registered into the new zone @@ -1880,6 +1880,17 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(ObjectHeldMessage(guid, slot, false)) } + case AvatarResponse.OxygenState(player, vehicle) => + sendResponse( + OxygenStateMessage( + DrowningTarget(player.guid, player.progress, player.state), + vehicle match { + case Some(vinfo) => Some(DrowningTarget(vinfo.guid, vinfo.progress, vinfo.state)) + case None => None + } + ) + ) + case AvatarResponse.PlanetsideAttribute(attribute_type, attribute_value) => if (tplayer_guid != guid) { sendResponse(PlanetsideAttributeMessage(guid, attribute_type, attribute_value)) @@ -3035,7 +3046,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) ) case (Some(vehicle), Some(0)) => - //summon any passengers and cargo vehicles left behind on previous continent + //driver; summon any passengers and cargo vehicles left behind on previous continent if (vehicle.Jammed) { //TODO something better than just canceling? vehicle.Actor ! JammableUnit.ClearJammeredStatus() @@ -3056,6 +3067,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.id, vehicle ) + case (Some(vehicle), _) => + //passenger + vehicle.Actor ! Vehicle.UpdateZoneInteractionProgressUI(player) case _ => ; } interstellarFerryTopLevelGUID = None @@ -3691,6 +3705,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (player.death_by == -1) { KickedByAdministration() } + player.zoneInteraction() case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) => //log.info(s"$msg") @@ -3792,6 +3807,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) ) updateSquad() + obj.zoneInteraction() case (None, _) => //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle @@ -6761,7 +6777,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @param tplayer the player to be killed */ def suicide(tplayer: Player): Unit = { - tplayer.History(PlayerSuicide(PlayerSource(tplayer))) + tplayer.History(PlayerSuicide()) tplayer.Actor ! Player.Die() } @@ -6881,6 +6897,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case _ => vehicle.MountedIn = None } + vehicle.allowZoneEnvironmentInteractions = true data } else { //passenger @@ -8501,6 +8518,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } // + vehicle.allowZoneEnvironmentInteractions = false if (!zoneReload && zoneId == continent.id) { if (vehicle.Definition == GlobalDefinitions.droppod) { //instant action droppod in the same zone @@ -9153,7 +9171,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) { // If the target position on the server does not match the position where the projectile landed within reason there may be foul play log.warn( - s"Shot guid ${projectile_guid} has hit location discrepancy with target location. Target: ${target.Position} Reported: ${hitPos}, Distance: ${hitPositionDiscrepancy} / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect" + s"Shot guid $projectile_guid has hit location discrepancy with target location. Target: ${target.Position} Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect" ) } } diff --git a/src/main/scala/net/psforever/login/WorldSession.scala b/src/main/scala/net/psforever/login/WorldSession.scala index 0790c253..feae7e9c 100644 --- a/src/main/scala/net/psforever/login/WorldSession.scala +++ b/src/main/scala/net/psforever/login/WorldSession.scala @@ -10,7 +10,7 @@ import net.psforever.objects.locker.LockerContainer import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.zones.Zone -import net.psforever.objects.{AmmoBox, GlobalDefinitions, Player, Tool} +import net.psforever.objects._ import net.psforever.packet.game.ObjectHeldMessage import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3} import net.psforever.services.Service diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 4f031f16..2c67790d 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -28,6 +28,7 @@ import net.psforever.objects.vital.projectile._ import net.psforever.objects.vital.prop.DamageWithPosition import net.psforever.objects.vital.{ComplexDeployableResolutions, MaxResolutions, SimpleResolutions} import net.psforever.types.{ExoSuitType, ImplantType, PlanetSideEmpire, Vector3} +import net.psforever.types._ import scala.collection.mutable import scala.concurrent.duration._ @@ -37,6 +38,9 @@ object GlobalDefinitions { val avatar = new AvatarDefinition(121) avatar.MaxHealth = 100 avatar.Damageable = true + avatar.DrownAtMaxDepth = true + avatar.MaxDepth = 1.609375f //Male, standing, not MAX + avatar.UnderwaterLifespan(suffocation = 60000L, recovery = 10000L) /* exo-suits */ @@ -1623,6 +1627,23 @@ object GlobalDefinitions { } } + def MaxDepth(obj: PlanetSideGameObject): Float = { + obj match { + case p: Player => + if (p.Crouching) { + 1.093750f // same regardless of gender + } else if (p.ExoSuit == ExoSuitType.MAX) { + 1.906250f // VS female MAX + } else if (p.Sex == CharacterGender.Male) { + obj.Definition.MaxDepth // male + } else { + 1.546875f // female + } + case _ => + obj.Definition.MaxDepth + } + } + /** * Initialize `KitDefinition` globals. */ @@ -5607,6 +5628,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + fury.DrownAtMaxDepth = true + fury.MaxDepth = 1.3f + fury.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) quadassault.Name = "quadassault" // Basilisk quadassault.MaxHealth = 650 @@ -5635,6 +5659,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + quadassault.DrownAtMaxDepth = true + quadassault.MaxDepth = 1.3f + quadassault.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) quadstealth.Name = "quadstealth" // Wraith quadstealth.MaxHealth = 650 @@ -5663,6 +5690,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + quadstealth.DrownAtMaxDepth = true + quadstealth.MaxDepth = 1.25f + quadstealth.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) two_man_assault_buggy.Name = "two_man_assault_buggy" // Harasser two_man_assault_buggy.MaxHealth = 1250 @@ -5693,6 +5723,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + two_man_assault_buggy.DrownAtMaxDepth = true + two_man_assault_buggy.MaxDepth = 1.5f + two_man_assault_buggy.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) skyguard.Name = "skyguard" skyguard.MaxHealth = 1000 @@ -5715,6 +5748,7 @@ object GlobalDefinitions { skyguard.AutoPilotSpeeds = (22, 8) skyguard.DestroyedModel = Some(DestroyedVehicle.Skyguard) skyguard.JackingDuration = Array(0, 15, 5, 3) + skyguard.explodes = true skyguard.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One @@ -5724,6 +5758,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + skyguard.DrownAtMaxDepth = true + skyguard.MaxDepth = 1.5f + skyguard.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) threemanheavybuggy.Name = "threemanheavybuggy" // Marauder threemanheavybuggy.MaxHealth = 1700 @@ -5760,6 +5797,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + threemanheavybuggy.DrownAtMaxDepth = true + threemanheavybuggy.MaxDepth = 1.83f + threemanheavybuggy.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) twomanheavybuggy.Name = "twomanheavybuggy" // Enforcer twomanheavybuggy.MaxHealth = 1800 @@ -5791,6 +5831,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + twomanheavybuggy.DrownAtMaxDepth = true + twomanheavybuggy.MaxDepth = 1.95f + twomanheavybuggy.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) twomanhoverbuggy.Name = "twomanhoverbuggy" // Thresher twomanhoverbuggy.MaxHealth = 1600 @@ -5822,6 +5865,8 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + twomanhoverbuggy.DrownAtMaxDepth = true + twomanhoverbuggy.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the thresher hovers over water, so ...? mediumtransport.Name = "mediumtransport" // Deliverer mediumtransport.MaxHealth = 2500 @@ -5860,6 +5905,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + mediumtransport.DrownAtMaxDepth = false + mediumtransport.MaxDepth = 1.2f + mediumtransport.UnderwaterLifespan(suffocation = -1, recovery = -1) battlewagon.Name = "battlewagon" // Raider battlewagon.MaxHealth = 2500 @@ -5901,6 +5949,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + battlewagon.DrownAtMaxDepth = true + battlewagon.MaxDepth = 1.2f + battlewagon.UnderwaterLifespan(suffocation = -1, recovery = -1) thunderer.Name = "thunderer" thunderer.MaxHealth = 2500 @@ -5939,6 +5990,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + thunderer.DrownAtMaxDepth = true + thunderer.MaxDepth = 1.2f + thunderer.UnderwaterLifespan(suffocation = -1, recovery = -1) aurora.Name = "aurora" aurora.MaxHealth = 2500 @@ -5977,6 +6031,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + aurora.DrownAtMaxDepth = true + aurora.MaxDepth = 1.2f + aurora.UnderwaterLifespan(suffocation = -1, recovery = -1) apc_tr.Name = "apc_tr" // Juggernaut apc_tr.MaxHealth = 6000 @@ -6038,6 +6095,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + apc_tr.DrownAtMaxDepth = true + apc_tr.MaxDepth = 3 + apc_tr.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) apc_nc.Name = "apc_nc" // Vindicator apc_nc.MaxHealth = 6000 @@ -6099,6 +6159,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + apc_nc.DrownAtMaxDepth = true + apc_nc.MaxDepth = 3 + apc_nc.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) apc_vs.Name = "apc_vs" // Leviathan apc_vs.MaxHealth = 6000 @@ -6160,6 +6223,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + apc_vs.DrownAtMaxDepth = true + apc_vs.MaxDepth = 3 + apc_vs.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) lightning.Name = "lightning" lightning.MaxHealth = 2000 @@ -6189,6 +6255,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + lightning.DrownAtMaxDepth = true + lightning.MaxDepth = 1.38f + lightning.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) prowler.Name = "prowler" prowler.MaxHealth = 4800 @@ -6223,6 +6292,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + prowler.DrownAtMaxDepth = true + prowler.MaxDepth = 3 + prowler.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) vanguard.Name = "vanguard" vanguard.MaxHealth = 5400 @@ -6253,6 +6325,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + vanguard.DrownAtMaxDepth = true + vanguard.MaxDepth = 2.7f + vanguard.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) magrider.Name = "magrider" magrider.MaxHealth = 4200 @@ -6285,6 +6360,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + magrider.DrownAtMaxDepth = true + magrider.MaxDepth = 2 + magrider.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the magrider hovers over water, so ...? val utilityConverter = new UtilityVehicleConverter ant.Name = "ant" @@ -6315,6 +6393,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + ant.DrownAtMaxDepth = true + ant.MaxDepth = 2 + ant.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) ams.Name = "ams" ams.MaxHealth = 5000 // Temporary - original value is 3000 @@ -6348,6 +6429,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + ams.DrownAtMaxDepth = true + ams.MaxDepth = 3 + ams.UnderwaterLifespan(suffocation = 5000L, recovery = 5000L) val variantConverter = new VariantVehicleConverter router.Name = "router" @@ -6381,6 +6465,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + router.DrownAtMaxDepth = true + router.MaxDepth = 2 + router.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the router hovers over water, so ...? switchblade.Name = "switchblade" switchblade.MaxHealth = 1750 @@ -6414,6 +6501,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + switchblade.DrownAtMaxDepth = true + switchblade.MaxDepth = 2 + switchblade.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the switchblade hovers over water, so ...? flail.Name = "flail" flail.MaxHealth = 2400 @@ -6445,6 +6535,9 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + flail.DrownAtMaxDepth = true + flail.MaxDepth = 2 + flail.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the flail hovers over water, so ...? mosquito.Name = "mosquito" mosquito.MaxHealth = 665 @@ -6476,6 +6569,8 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + mosquito.DrownAtMaxDepth = true + mosquito.MaxDepth = 2 //flying vehicles will automatically disable lightgunship.Name = "lightgunship" // Reaver lightgunship.MaxHealth = 1000 @@ -6508,6 +6603,8 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + lightgunship.DrownAtMaxDepth = true + lightgunship.MaxDepth = 2 //flying vehicles will automatically disable wasp.Name = "wasp" wasp.MaxHealth = 515 @@ -6539,6 +6636,8 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + wasp.DrownAtMaxDepth = true + wasp.MaxDepth = 2 //flying vehicles will automatically disable liberator.Name = "liberator" liberator.MaxHealth = 2500 @@ -6578,6 +6677,8 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + liberator.DrownAtMaxDepth = true + liberator.MaxDepth = 2 //flying vehicles will automatically disable vulture.Name = "vulture" vulture.MaxHealth = 2500 @@ -6618,6 +6719,8 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + vulture.DrownAtMaxDepth = true + vulture.MaxDepth = 2 //flying vehicles will automatically disable dropship.Name = "dropship" // Galaxy dropship.MaxHealth = 5000 @@ -6690,6 +6793,8 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + dropship.DrownAtMaxDepth = true + dropship.MaxDepth = 2 galaxy_gunship.Name = "galaxy_gunship" galaxy_gunship.MaxHealth = 6000 @@ -6741,6 +6846,8 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + galaxy_gunship.DrownAtMaxDepth = true + galaxy_gunship.MaxDepth = 2 lodestar.Name = "lodestar" lodestar.MaxHealth = 5000 @@ -6780,6 +6887,8 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + lodestar.DrownAtMaxDepth = true + lodestar.MaxDepth = 2 phantasm.Name = "phantasm" phantasm.MaxHealth = 2500 @@ -6820,6 +6929,8 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = RadialDegrade } + phantasm.DrownAtMaxDepth = true + phantasm.MaxDepth = 2 droppod.Name = "droppod" droppod.MaxHealth = 20000 @@ -6832,6 +6943,7 @@ object GlobalDefinitions { droppod.DeconstructionTime = Some(5 seconds) droppod.DestroyedModel = None //the adb calls out a droppod; the cyclic nature of this confounds me droppod.DamageUsing = DamageCalculations.AgainstAircraft + droppod.DrownAtMaxDepth = false } /** diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 79c4c2fd..dd16b592 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -8,6 +8,7 @@ import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.aura.AuraContainer +import net.psforever.objects.serverobject.environment.InteractsWithZoneEnvironment import net.psforever.objects.vital.resistance.ResistanceProfile import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.interaction.DamageInteraction @@ -20,6 +21,7 @@ import scala.util.{Success, Try} class Player(var avatar: Avatar) extends PlanetSideServerObject + with InteractsWithZoneEnvironment with FactionAffinity with Vitality with ResistanceProfile @@ -27,8 +29,6 @@ class Player(var avatar: Avatar) with JammableUnit with ZoneAware with AuraContainer { - 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 armor: Int = 0 @@ -66,6 +66,9 @@ class Player(var avatar: Avatar) val squadLoadouts = new LoadoutManager(10) + //init + Health = 0 //player health is artificially managed as a part of their lifecycle; start entity as dead + Destroyed = true //see isAlive Player.SuitSetup(this, exosuit) def Definition: AvatarDefinition = avatar.definition diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala index 6676aa43..ec3a0be3 100644 --- a/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/src/main/scala/net/psforever/objects/Vehicle.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.definition.VehicleDefinition +import net.psforever.objects.definition.{SeatDefinition, ToolDefinition, VehicleDefinition} import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile} import net.psforever.objects.serverobject.mount.Mountable @@ -9,6 +9,7 @@ import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.aura.AuraContainer import net.psforever.objects.serverobject.deploy.Deployment +import net.psforever.objects.serverobject.environment.InteractsWithZoneEnvironment import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.structures.AmenityOwner import net.psforever.objects.vehicles._ @@ -71,6 +72,7 @@ import scala.util.{Success, Try} */ class Vehicle(private val vehicleDef: VehicleDefinition) extends AmenityOwner + with InteractsWithZoneEnvironment with Hackable with FactionAffinity with Mountable @@ -205,7 +207,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition) def NtuCapacitorScaled: Int = { if (Definition.MaxNtuCapacitor > 0) { - scala.math.ceil((NtuCapacitor.toFloat / Definition.MaxNtuCapacitor.toFloat) * 10).toInt + scala.math.ceil((NtuCapacitor / Definition.MaxNtuCapacitor) * 10).toInt } else { 0 } @@ -630,6 +632,14 @@ object Vehicle { def apply(player: Player): Ownership = Ownership(Some(player)) } + /** + * For vehicles, this pertains mainly to resending information needs to display the the drowning red progress bar + * that is a product of the `OxygenStateMessage` packet to vehicle passengers. + * It also forces passengers to update their internal understanding of their own drowning state. + * @param passenger a player mounted in the vehicle + */ + final case class UpdateZoneInteractionProgressUI(passenger : Player) + /** * Overloaded constructor. * @param vehicleDef the vehicle's definition entry @@ -657,30 +667,31 @@ object Vehicle { //general stuff vehicle.Health = vdef.DefaultHealth //create weapons - vehicle.weapons = vdef.Weapons - .map({ - case (num, definition) => - val slot = EquipmentSlot(EquipmentSize.VehicleWeapon) - slot.Equipment = Tool(definition) - num -> slot - }) - .toMap + vehicle.weapons = vdef.Weapons.map[Int, EquipmentSlot] { + case (num: Int, definition: ToolDefinition) => + val slot = EquipmentSlot(EquipmentSize.VehicleWeapon) + slot.Equipment = Tool(definition) + num -> slot + }.toMap //create seats - vehicle.seats = vdef.Seats.map({ case (num, definition) => num -> Seat(definition) }).toMap + vehicle.seats = vdef.Seats.map[Int, Seat] { + case (num: Int, definition: SeatDefinition) => + num -> Seat(definition) + }.toMap // create cargo holds - vehicle.cargoHolds = vdef.Cargo.map({ case (num, definition) => num -> Cargo(definition) }).toMap - + vehicle.cargoHolds = vdef.Cargo.map[Int, Cargo] { + case (num, definition) => + num -> Cargo(definition) + }.toMap //create utilities - vehicle.utilities = vdef.Utilities - .map({ - case (num, util) => - val obj = Utility(util, vehicle) - val utilObj = obj() - vehicle.Amenities = utilObj - utilObj.LocationOffset = vdef.UtilityOffset.get(num) - num -> obj - }) - .toMap + vehicle.utilities = vdef.Utilities.map[Int, Utility] { + case (num: Int, util: UtilityType.Value) => + val obj = Utility(util, vehicle) + val utilObj = obj() + vehicle.Amenities = utilObj + utilObj.LocationOffset = vdef.UtilityOffset.get(num) + num -> obj + }.toMap //trunk vdef.TrunkSize match { case InventoryTile.None => ; diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 91e06966..a96b8924 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -1,10 +1,10 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.avatar -import akka.actor.{Actor, ActorRef, Props} +import akka.actor.{Actor, ActorRef, Props, typed} import net.psforever.actors.session.AvatarActor import net.psforever.objects.{Player, _} -import net.psforever.objects.ballistics.{ObjectSource, PlayerSource} +import net.psforever.objects.ballistics.PlayerSource import net.psforever.objects.equipment._ import net.psforever.objects.inventory.{GridInventory, InventoryItem} import net.psforever.objects.loadouts.Loadout @@ -18,18 +18,17 @@ import net.psforever.objects.serverobject.repair.Repairable import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.vital._ import net.psforever.objects.vital.resolution.ResolutionCalculations.Output -import net.psforever.objects.zones.Zone +import net.psforever.objects.zones._ import net.psforever.packet.game._ import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent import net.psforever.types._ import net.psforever.services.{RemoverActor, Service} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} -import akka.actor.typed import net.psforever.objects.locker.LockerContainerControl -import net.psforever.objects.serverobject.painbox.Painbox -import net.psforever.objects.vital.base.DamageResolution -import net.psforever.objects.vital.etc.SuicideReason +import net.psforever.objects.serverobject.environment._ +import net.psforever.objects.vital.environment.EnvironmentReason +import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import scala.concurrent.duration._ @@ -40,7 +39,8 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm with Damageable with ContainableBehavior with AggravatedBehavior - with AuraEffectBehavior { + with AuraEffectBehavior + with RespondsToZoneEnvironment { def JammableObject = player @@ -49,15 +49,23 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm def ContainerObject = player def AggravatedObject = player + + def AuraTargetObject = player ApplicableEffect(Aura.Plasma) ApplicableEffect(Aura.Napalm) ApplicableEffect(Aura.Comet) ApplicableEffect(Aura.Fire) - def AuraTargetObject = player + def InteractiveObject = player + SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) + SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) + SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath) + SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater) private[this] val log = org.log4s.getLogger(player.Name) private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel) + /** suffocating, or regaining breath? */ + var submergedCondition: Option[OxygenState] = None /** control agency for the player's locker container (dedicated inventory slot #5) */ val lockerControlAgent: ActorRef = { @@ -82,33 +90,20 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm .orElse(aggravatedBehavior) .orElse(auraBehavior) .orElse(containerBehavior) + .orElse(environmentBehavior) .orElse { case Player.Die(Some(reason)) => if (player.isAlive) { //primary death PerformDamage(player, reason.calculate()) - if(player.Health > 0 || player.isAlive) { - //that wasn't good enough - DestructionAwareness(player, None) - } + suicide() } case Player.Die(None) => - if (player.isAlive) { - //suicide - PerformDamage( - player, - DamageInteraction( - DamageResolution.Resolved, - PlayerSource(player), - SuicideReason(), - player.Position - ).calculate() - ) - } + suicide() case CommonMessages.Use(user, Some(item: Tool)) - if item.Definition == GlobalDefinitions.medicalapplicator && player.isAlive => + if item.Definition == GlobalDefinitions.medicalapplicator && player.isAlive => //heal val originalHealth = player.Health val definition = player.Definition @@ -564,7 +559,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm if (target.Health > 0) { DamageAwareness(target, cause, damageToHealth, damageToArmor, damageToStamina, damageToCapacitor) } else { - DestructionAwareness(target, Some(cause)) + DestructionAwareness(target, cause) } } @@ -639,11 +634,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm ) ) } - case source: ObjectSource if source.obj.isInstanceOf[Painbox] => - zone.AvatarEvents ! AvatarServiceMessage( - target.Name, - AvatarAction.EnvironmentalDamage(target.GUID, source.obj.GUID, countableDamage) - ) case source => zone.AvatarEvents ! AvatarServiceMessage( target.Name, @@ -654,6 +644,18 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm ) } case None => + cause.interaction.cause match { + case o: PainboxReason => + zone.AvatarEvents ! AvatarServiceMessage( + target.Name, + AvatarAction.EnvironmentalDamage(target.GUID, o.entity.GUID, countableDamage) + ) + case _ => + zone.AvatarEvents ! AvatarServiceMessage( + target.Name, + AvatarAction.EnvironmentalDamage(target.GUID, ValidPlanetSideGUID(0), countableDamage) + ) + } } } else { @@ -687,7 +689,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm * @param target na * @param cause na */ - def DestructionAwareness(target: Player, cause: Option[DamageResult]): Unit = { + def DestructionAwareness(target: Player, cause: DamageResult): Unit = { val player_guid = target.GUID val pos = target.Position val respawnTimer = 300000 //milliseconds @@ -702,7 +704,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm EndAllAggravation() //unjam CancelJammeredSound(target) - CancelJammeredStatus(target) + super.CancelJammeredStatus(target) //uninitialize implants avatarActor ! AvatarActor.DeinitializeImplants() events ! AvatarServiceMessage( @@ -736,10 +738,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm target.Capacitor = 0 events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 7, 0)) // capacitor } - val attribute = cause match { - case Some(reason) => DamageableEntity.attributionTo(reason, target.Zone, player_guid) - case None => player_guid - } + val attribute = DamageableEntity.attributionTo(cause, target.Zone, player_guid) events ! AvatarServiceMessage( nameChannel, AvatarAction.SendResponse( @@ -756,17 +755,17 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm ) //TODO other methods of death? val pentry = PlayerSource(target) - (cause match { - case Some(result) => - result.adversarial - case None => + (cause.adversarial match { + case out @ Some(_) => + out + case _ => target.LastDamage match { case Some(attack) if System.currentTimeMillis() - attack.interaction.hitTime < (10 seconds).toMillis => attack.adversarial case None => None } - }) match { + }) match { case Some(adversarial) => events ! AvatarServiceMessage( zoneChannel, @@ -777,6 +776,19 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm } } + def suicide() : Unit = { + if (player.Health > 0 || player.isAlive) { + PerformDamage( + player, + DamageInteraction( + PlayerSource(player), + SuicideReason(), + player.Position + ).calculate() + ) + } + } + /** * Start the jammered buzzing. * Although, as a rule, the jammering sound effect should last as long as the jammering status, @@ -968,6 +980,109 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm val value = target.Aura.foldLeft(0)(_ + PlayerControl.auraEffectToAttributeValue(_)) zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttributeToAll(target.GUID, 54, value)) } + + /** + * Water causes players to slowly suffocate. + * When they (finally) drown, they will die. + * @param obj the target + * @param body the environment + * @param data additional interaction information, if applicable; + * for players, this will be data from any mounted vehicles + */ + def doInteractingWithWater(obj: PlanetSideServerObject, body: PieceOfEnvironment, data: Option[OxygenStateTarget]): Unit = { + val (effect: Boolean, time: Long, percentage: Float) = + RespondsToZoneEnvironment.drowningInWateryConditions(obj, submergedCondition, interactionTime) + if (effect) { + import scala.concurrent.ExecutionContext.Implicits.global + interactionTime = System.currentTimeMillis() + time + submergedCondition = Some(OxygenState.Suffocation) + interactionTimer = context.system.scheduler.scheduleOnce(delay = time milliseconds, self, Player.Die()) + //inform the player that they are in trouble + player.Zone.AvatarEvents ! AvatarServiceMessage( + player.Name, + AvatarAction.OxygenState(OxygenStateTarget(player.GUID, OxygenState.Suffocation, percentage), data) + ) + } else if (data.isDefined) { + //inform the player that their mounted vehicle is in trouble (that they are in trouble) + player.Zone.AvatarEvents ! AvatarServiceMessage( + player.Name, + AvatarAction.OxygenState(OxygenStateTarget(player.GUID, OxygenState.Suffocation, percentage), data) + ) + } + } + + /** + * Lava causes players to take (considerable) damage until they inevitably die. + * @param obj the target + * @param body the environment + * @param data additional interaction information, if applicable + */ + def doInteractingWithLava(obj: PlanetSideServerObject, body: PieceOfEnvironment, data: Option[OxygenStateTarget]): Unit = { + if (player.isAlive) { + PerformDamage( + player, + DamageInteraction( + PlayerSource(player), + EnvironmentReason(body, player), + player.Position + ).calculate() + ) + if (player.Health > 0) { + StartAuraEffect(Aura.Fire, duration = 1250L) //burn + import scala.concurrent.ExecutionContext.Implicits.global + interactionTimer = context.system.scheduler.scheduleOnce(delay = 250 milliseconds, self, InteractWithEnvironment(player, body, None)) + } + } + } + + /** + * Death causes players to die outright. + * It's not even considered as environmental damage anymore. + * @param obj the target + * @param body the environment + * @param data additional interaction information, if applicable + */ + def doInteractingWithDeath(obj: PlanetSideServerObject, body: PieceOfEnvironment, data: Option[OxygenStateTarget]): Unit = { + suicide() + } + + /** + * When out of water, the player is no longer suffocating. + * The player does have to endure a recovery period to get back to normal, though. + * @param obj the target + * @param body the environment + * @param data additional interaction information, if applicable; + * for players, this will be data from any mounted vehicles + */ + def stopInteractingWithWater(obj: PlanetSideServerObject, body: PieceOfEnvironment, data: Option[OxygenStateTarget]): Unit = { + val (effect: Boolean, time: Long, percentage: Float) = + RespondsToZoneEnvironment.recoveringFromWateryConditions(obj, submergedCondition, interactionTime) + if (percentage == 100f) { + recoverFromEnvironmentInteracting() + } + if (effect) { + import scala.concurrent.ExecutionContext.Implicits.global + submergedCondition = Some(OxygenState.Recovery) + interactionTime = System.currentTimeMillis() + time + interactionTimer = context.system.scheduler.scheduleOnce(delay = time milliseconds, self, RecoveredFromEnvironmentInteraction()) + //inform the player + player.Zone.AvatarEvents ! AvatarServiceMessage( + player.Name, + AvatarAction.OxygenState(OxygenStateTarget(player.GUID, OxygenState.Recovery, percentage), data) + ) + } else if (data.isDefined) { + //inform the player + player.Zone.AvatarEvents ! AvatarServiceMessage( + player.Name, + AvatarAction.OxygenState(OxygenStateTarget(player.GUID, OxygenState.Recovery, percentage), data) + ) + } + } + + override def recoverFromEnvironmentInteracting(): Unit = { + super.recoverFromEnvironmentInteracting() + submergedCondition = None + } } object PlayerControl { diff --git a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala index 7b960c0a..29e7609c 100644 --- a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala @@ -3,6 +3,7 @@ package net.psforever.objects.definition import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.definition.converter.{ObjectCreateConverter, PacketConverter} +import net.psforever.types.OxygenState /** * Associate an object's canned in-game representation with its basic game identification unit. @@ -40,5 +41,40 @@ abstract class ObjectDefinition(private val objectId: Int) extends BasicDefiniti Packet } + private var maxDepth: Float = 0 //water_maxdragdepth + private var disableAtMaxDepth: Boolean = false + private var drownAtMaxDepth: Boolean = false + private var underwaterLifespan: Map[OxygenState, Long] = Map.empty //water_underwaterlifespan and water_underwaterlifespanrecovery + + def MaxDepth: Float = maxDepth + + def MaxDepth_=(height: Float): Float = { + maxDepth = height + MaxDepth + } + + def DisableAtMaxDepth: Boolean = disableAtMaxDepth + + def DisableAtMaxDepth_=(drowns: Boolean): Boolean = { + disableAtMaxDepth = drowns + DisableAtMaxDepth + } + + def DrownAtMaxDepth: Boolean = drownAtMaxDepth + + def DrownAtMaxDepth_=(drowns: Boolean): Boolean = { + drownAtMaxDepth = drowns + DrownAtMaxDepth + } + + def UnderwaterLifespan(): Map[OxygenState, Long] = underwaterLifespan + + def UnderwaterLifespan(key: OxygenState): Long = underwaterLifespan.getOrElse(key, 1L) + + def UnderwaterLifespan(suffocation: Long, recovery: Long): Map[OxygenState, Long] = { + underwaterLifespan = Map(OxygenState.Suffocation -> suffocation, OxygenState.Recovery -> recovery) + UnderwaterLifespan() + } + def ObjectId: Int = objectId } diff --git a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala index 7bfc8f28..779e46c9 100644 --- a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala +++ b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala @@ -42,7 +42,7 @@ object EffectTarget { def RepairSilo(target: PlanetSideGameObject): Boolean = target match { case v: Vehicle => - !GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 && v.Health < v.MaxHealth && v.History.find(x => x.isInstanceOf[DamagingActivity] && x.t >= (System.nanoTime - 5000000000L)).isEmpty + !GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 && v.Health < v.MaxHealth && v.History.exists(x => x.isInstanceOf[DamagingActivity] && x.time >= (System.currentTimeMillis() - 5000000000L)) case _ => false } @@ -50,7 +50,7 @@ object EffectTarget { def PadLanding(target: PlanetSideGameObject): Boolean = target match { case v: Vehicle => - GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 && v.Health < v.MaxHealth && v.History.find(x => x.isInstanceOf[DamagingActivity] && x.t >= (System.nanoTime - 5000000000L)).isEmpty + GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 && v.Health < v.MaxHealth && v.History.exists(x => x.isInstanceOf[DamagingActivity] && x.time >= (System.currentTimeMillis() - 5000000000L)) case _ => false } diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala index ba8b1524..1aeb5264 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala @@ -33,6 +33,10 @@ trait AggravatedBehavior { (o.projectile.quality == ProjectileQuality.AggravatesTarget || damage.targets.exists(validation => validation.test(AggravatedObject))) => TryAggravationEffectActivate(damage, data.interaction) + case (_: DamageReason, Some(damage)) + if damage.effect_type != Aura.Nothing && + damage.targets.exists(validation => validation.test(AggravatedObject)) => + TryAggravationEffectActivate(damage, data.interaction) case _ => None } diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala index edbb5195..69401059 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -115,8 +115,10 @@ trait DamageableVehicle } reportDamageToVehicle = false - //log historical event - target.History(cause) + if (obj.MountedIn.nonEmpty) { + //log historical event + target.History(cause) + } //damage if (Damageable.CanDamageOrJammer(target, totalDamage, cause.interaction)) { //jammering diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentCollision.scala b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentCollision.scala new file mode 100644 index 00000000..7187fcad --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentCollision.scala @@ -0,0 +1,86 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.environment + +import net.psforever.types.Vector3 + +/** + * The coordinate representation of a feature of the game world that is not a formal game object, + * usually terrain, but can be used to represent any bounded region. + * Calling this "geometry" would be accurate yet still generous. + */ +trait EnvironmentCollision { + /** in general, the highest point in this geometry */ + def altitude: Float + + /** + * Is the test point "within" the bounds of the represented environment? + * @param pos the test point + * @param varDepth how far "into" the environment the point must be + * @return `true`, if the point is sufficiently "deep"; + * `false`, otherwise + */ + def testInteraction(pos: Vector3, varDepth: Float): Boolean +} + +/** + * A mathematical plane that is always perpendicular to world-up. + * The modifier "deep" indicates that the valid area goes down from the altitude to the bottom of the world. + * @param altitude the z-coordinate of the geometry (height) + */ +final case class DeepPlane(altitude: Float) + extends EnvironmentCollision { + def testInteraction(pos: Vector3, varDepth: Float): Boolean = { + pos.z + varDepth < altitude + } +} + +/** + * From above, a rectangular region that is always perpendicular to world-up + * and whose sides align with the X-axis and Y-axis, respectively. + * The modifier "deep" indicates that the valid area goes down from the altitude to the bottom of the world. + * @param altitude the z-coordinate of the geometry (height) + * @param north the y-coordinate of the greatest side + * @param east the x-coordinate of the other greatest side + * @param south the y-coordinate of the least side + * @param west the x-coordinate of the other least side + */ +final case class DeepSquare(altitude: Float, north: Float, east: Float, south: Float, west: Float) + extends EnvironmentCollision { + def testInteraction(pos: Vector3, varDepth: Float): Boolean = { + pos.z + varDepth < altitude && north > pos.y && pos.y >= south && east > pos.x && pos.x >= west + } +} + +/** + * Similar to `DeepRectangle`, + * from above, a rectangular region that is always perpendicular to world-up + * and whose sides align with the X-axis and Y-axis, respectively. + * The modifier "deep" indicates that the valid area goes down from the altitude to the bottom of the world. + * It is never subject to variable intersection depth during testing. + * @param altitude the z-coordinate of the geometry (height) + * @param north the y-coordinate of the greatest side + * @param east the x-coordinate of the other greatest side + * @param south the y-coordinate of the least side + * @param west the x-coordinate of the other least side + */ +final case class DeepSurface(altitude: Float, north: Float, east: Float, south: Float, west: Float) + extends EnvironmentCollision { + def testInteraction(pos: Vector3, varDepth: Float): Boolean = { + pos.z < altitude && north > pos.y && pos.y >= south && east > pos.x && pos.x >= west + } +} + +/** + * From above, a circular region that is always perpendicular to world-up. + * The modifier "deep" indicates that the valid area goes down from the altitude to the bottom of the world. + * @param center the center of the geometry (height) + * @param radius how large the circle is + */ +final case class DeepCircularSurface(center: Vector3, radius: Float) + extends EnvironmentCollision { + def altitude: Float = center.z + + def testInteraction(pos: Vector3, varDepth: Float): Boolean = { + pos.z < center.z && Vector3.DistanceSquared(pos.xy, center.xy) < radius * radius + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala new file mode 100644 index 00000000..5a757875 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala @@ -0,0 +1,49 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.environment + +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.types.{OxygenState, PlanetSideGUID} + +/** + * Related to the progress of interacting with a body of water deeper than you are tall or + * deeper than your vehicle is off the ground. + * @param guid the target + * @param state whether they are recovering or suffocating + * @param progress the percentage of completion towards the state + */ +final case class OxygenStateTarget( + guid: PlanetSideGUID, + state: OxygenState, + progress: Float + ) + +/** + * The target has clipped into a critical region of a piece of environment. + * @param obj the target + * @param environment the terrain clipping region + * @param mountedVehicle whether or not the target is mounted + * (specifically, if the target is a `Player` who is mounted in a `Vehicle`) + */ +final case class InteractWithEnvironment( + obj: PlanetSideServerObject, + environment: PieceOfEnvironment, + mountedVehicle: Option[OxygenStateTarget] + ) + +/** + * The target has ceased to clip into a critical region of a piece of environment. + * @param obj the target + * @param environment the previous terrain clipping region + * @param mountedVehicle whether or not the target is mounted + * (specifically, if the target is a `Player` who is mounted in a `Vehicle`) + */ +final case class EscapeFromEnvironment( + obj: PlanetSideServerObject, + environment: PieceOfEnvironment, + mountedVehicle: Option[OxygenStateTarget] + ) + +/** + * Completely reset any internal actions or processes related to environment clipping. + */ +final case class RecoveredFromEnvironmentInteraction() diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/InteractsWithZoneEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/InteractsWithZoneEnvironment.scala new file mode 100644 index 00000000..7d59d962 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/InteractsWithZoneEnvironment.scala @@ -0,0 +1,185 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.environment + +import net.psforever.objects.GlobalDefinitions +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.zones.Zone + +/** + * This game entity may infrequently test whether it may interact with game world environment. + */ +trait InteractsWithZoneEnvironment { + _: PlanetSideServerObject => + /** interactions for this particular entity is allowed */ + private var _allowZoneEnvironmentInteractions: Boolean = true + + /** + * If the environmental interactive permissions of this entity change. + */ + def allowZoneEnvironmentInteractions: Boolean = _allowZoneEnvironmentInteractions + + /** + * If the environmental interactive permissions of this entity change, + * trigger a formal change to the interaction methodology. + * @param allow whether or not interaction is permitted + * @return whether or not interaction is permitted + */ + def allowZoneEnvironmentInteractions_=(allow: Boolean): Boolean = { + val before = _allowZoneEnvironmentInteractions + _allowZoneEnvironmentInteractions = allow + if (before != allow) { + zoneInteraction() + } + _allowZoneEnvironmentInteractions + } + + private var interactingWithEnvironment: (PlanetSideServerObject, Boolean) => Any = + InteractsWithZoneEnvironment.onStableEnvironment() + + /** + * The method by which zone interactions are tested or a current interaction maintained. + * Utilize a function literal that, when called, returns a function literal of the same type; + * the function that is returned will not necessarily be the same as the one that was used + * but will represent the existing and ongoing status of interaction with the environment. + * Calling one function and exchanging it for another function to be called like this creates a procedure + * that controls and limits the interactions with the environment to only what is necessary. + * @see `InteractsWithZoneEnvironment.blockedFromInteracting` + * @see `InteractsWithZoneEnvironment.onStableEnvironment` + * @see `InteractsWithZoneEnvironment.awaitOngoingInteraction` + */ + def zoneInteraction(): Unit = { + //val func: (PlanetSideServerObject, Boolean) => Any = interactingWithEnvironment(this, allowZoneEnvironmentInteractions) + interactingWithEnvironment = interactingWithEnvironment(this, allowZoneEnvironmentInteractions) + .asInstanceOf[(PlanetSideServerObject, Boolean) => Any] + } + + /** + * Suspend any current interaction procedures through the proper channels + * or deactivate a previously flagged interaction blocking procedure + * and reset the system to its neutral state. + * The main difference between resetting and flagging the blocking procedure + * is that resetting will (probably) restore the previously active procedure on the next `zoneInteraction` call + * while blocking will halt all attempts to establish a new active interaction procedure + * and unblocking will immediately install whatever is the current active interaction. + * @see `InteractsWithZoneEnvironment.onStableEnvironment` + */ + def resetZoneInteraction() : Unit = { + _allowZoneEnvironmentInteractions = true + interactingWithEnvironment(this, false) + interactingWithEnvironment = InteractsWithZoneEnvironment.onStableEnvironment() + } +} + +object InteractsWithZoneEnvironment { + /** + * While on stable non-interactive terrain, + * test whether any special terrain component has an affect upon the target entity. + * If so, instruct the target that an interaction should occur. + * Considered tail recursive, but not treated that way. + * @see `blockedFromInteracting` + * @see `checkAllEnvironmentInteractions` + * @see `awaitOngoingInteraction` + * @param obj the target entity + * @return the function literal that represents the next iterative call of ongoing interaction testing; + * may return itself + */ + def onStableEnvironment()(obj: PlanetSideServerObject, allow: Boolean): Any = { + if(allow) { + checkAllEnvironmentInteractions(obj) match { + case Some(body) => + obj.Actor ! InteractWithEnvironment(obj, body, None) + awaitOngoingInteraction(obj.Zone, body)(_,_) + case None => + onStableEnvironment()(_,_) + } + } else { + blockedFromInteracting()(_,_) + } + } + + /** + * While on unstable, interactive, or special terrain, + * test whether that special terrain component has an affect upon the target entity. + * If no interaction exists, + * treat the target as if it had been previously affected by the given terrain, + * and instruct it to cease that assumption. + * Transition between the affects of different special terrains is possible. + * Considered tail recursive, but not treated that way. + * @see `blockedFromInteracting` + * @see `checkAllEnvironmentInteractions` + * @see `checkSpecificEnvironmentInteraction` + * @see `onStableEnvironment` + * @param zone the zone in which the terrain is located + * @param body the special terrain + * @param obj the target entity + * @return the function literal that represents the next iterative call of ongoing interaction testing; + * may return itself + */ + def awaitOngoingInteraction(zone: Zone, body: PieceOfEnvironment)(obj: PlanetSideServerObject, allow: Boolean): Any = { + if (allow) { + checkSpecificEnvironmentInteraction(zone, body)(obj) match { + case Some(_) => + awaitOngoingInteraction(obj.Zone, body)(_, _) + case None => + checkAllEnvironmentInteractions(obj) match { + case Some(newBody) if newBody.attribute == body.attribute => + obj.Actor ! InteractWithEnvironment(obj, newBody, None) + awaitOngoingInteraction(obj.Zone, newBody)(_, _) + case Some(newBody) => + obj.Actor ! EscapeFromEnvironment(obj, body, None) + obj.Actor ! InteractWithEnvironment(obj, newBody, None) + awaitOngoingInteraction(obj.Zone, newBody)(_, _) + case None => + obj.Actor ! EscapeFromEnvironment(obj, body, None) + onStableEnvironment()(_, _) + } + } + } else { + obj.Actor ! EscapeFromEnvironment(obj, body, None) + blockedFromInteracting()(_,_) + } + } + + /** + * Do not care whether on stable non-interactive terrain or on unstable interactive terrain. + * Wait until allowed to test again (external flag). + * Considered tail recursive, but not treated that way. + * @see `onStableEnvironment` + * @param obj the target entity + * @return the function literal that represents the next iterative call of ongoing interaction testing; + * may return itself + */ + def blockedFromInteracting()(obj: PlanetSideServerObject, allow: Boolean): Any = { + if (allow) { + onStableEnvironment()(obj, allow) + } else { + blockedFromInteracting()(_,_) + } + } + + /** + * Test whether any special terrain component has an affect upon the target entity. + * @param obj the target entity + * @return any unstable, interactive, or special terrain that is being interacted + */ + def checkAllEnvironmentInteractions(obj: PlanetSideServerObject): Option[PieceOfEnvironment] = { + val position = obj.Position + val depth = GlobalDefinitions.MaxDepth(obj) + obj.Zone.map.environment.find { body => body.attribute.canInteractWith(obj) && body.testInteraction(position, depth) } + } + + /** + * Test whether a special terrain component has an affect upon the target entity. + * @param zone the zone in which the terrain is located + * @param body the special terrain + * @param obj the target entity + * @return any unstable, interactive, or special terrain that is being interacted + */ + private def checkSpecificEnvironmentInteraction(zone: Zone, body: PieceOfEnvironment)(obj: PlanetSideServerObject): Option[PieceOfEnvironment] = { + if ((obj.Zone eq zone) && body.testInteraction(obj.Position, GlobalDefinitions.MaxDepth(obj))) { + Some(body) + } else { + None + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala new file mode 100644 index 00000000..105acb1e --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala @@ -0,0 +1,146 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.environment + +import enumeratum.{Enum, EnumEntry} +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.vital.Vitality +import net.psforever.types.Vector3 + +/** + * The representation of a feature of the game world that is not a formal game object, + * usually terrain, but can be used to represent any bounded region. + */ +trait PieceOfEnvironment { + /** a general description of this environment */ + def attribute: EnvironmentTrait + /** a special representation of the region that qualifies as "this environment" */ + def collision: EnvironmentCollision + + /** + * Is the test point "within" the bounds of the represented environment? + * @param pos the test point + * @param varDepth how far "into" the environment the point must be + * @return `true`, if the point is sufficiently "deep"; + * `false`, otherwise + */ + def testInteraction(pos: Vector3, varDepth: Float): Boolean = collision.testInteraction(pos, varDepth) + + /** + * Did the test point move into or leave the bounds of the represented environment since its previous test? + * @param pos the test point + * @param previousPos the previous test point which is being compared against + * @param varDepth how far "into" the environment the point must be + * @return `Some(true)`, if the point has become sufficiently "deep"; + * `Some(false)`, if the point has left the sufficiently "deep" region; + * `None`, otherwise + */ + def testStepIntoInteraction(pos: Vector3, previousPos: Vector3, varDepth: Float): Option[Boolean] = + PieceOfEnvironment.testStepIntoInteraction(body = this, pos, previousPos, varDepth) +} + +/** + * A general description of environment and its interactive possibilities. + */ +sealed abstract class EnvironmentTrait extends EnumEntry { + def canInteractWith(obj: PlanetSideGameObject): Boolean +} + +object EnvironmentAttribute extends Enum[EnvironmentTrait] { + /** glue connecting `EnumEntry` to `Enumeration` */ + val values: IndexedSeq[EnvironmentTrait] = findValues + + case object Water extends EnvironmentTrait { + /** water can only interact with objects that are negatively affected by being exposed to water; + * it's better this way */ + def canInteractWith(obj: PlanetSideGameObject): Boolean = { + obj.Definition.DrownAtMaxDepth || obj.Definition.DisableAtMaxDepth + } + } + + case object Lava extends EnvironmentTrait { + /** lava can only interact with anything capable of registering damage */ + def canInteractWith(obj: PlanetSideGameObject): Boolean = { + obj match { + case o: Vitality => o.Definition.Damageable + case _ => false + } + } + } + + case object Death extends EnvironmentTrait { + /** death can only interact with anything capable of registering damage */ + def canInteractWith(obj: PlanetSideGameObject): Boolean = { + obj match { + case o: Vitality => o.Definition.Damageable + case _ => false + } + } + } +} + +/** + * A planar environment that spans the whole of the game world + * and starts at and below a certain altitude. + * @param attribute of what the environment is composed + * @param altitude how high the environment starts + */ +final case class SeaLevel(attribute: EnvironmentTrait, altitude: Float) + extends PieceOfEnvironment { + private val planar = DeepPlane(altitude) + + def collision : EnvironmentCollision = planar +} + +object SeaLevel { + /** + * An overloaded constructor that applies only to water. + * @param altitude how high the environment starts + * @return a `SeaLevel` `PieceOfEnvironment` object + */ + def apply(altitude: Float): SeaLevel = SeaLevel(EnvironmentAttribute.Water, altitude) +} + +/** + * A limited environment that spans no specific region. + * @param attribute of what the environment is composed + * @param collision a special representation of the region that qualifies as "this environment" + */ +final case class Pool(attribute: EnvironmentTrait, collision: EnvironmentCollision) + extends PieceOfEnvironment + +object Pool { + /** + * An overloaded constructor that creates environment backed by a `DeepSquare`. + * @param attribute of what the environment is composed + * @param altitude the z-coordinate of the geometry (height) + * @param north the y-coordinate of the greatest side + * @param east the x-coordinate of the other greatest side + * @param south the y-coordinate of the least side + * @param west the x-coordinate of the other least side + * @return a `Pool` `PieceOfEnvironment` object + */ + def apply(attribute: EnvironmentTrait, altitude: Float, north: Float, east: Float, south: Float, west: Float): Pool = + Pool(attribute, DeepSquare(altitude, north, east, south, west)) +} + +object PieceOfEnvironment { + /** + * Did the test point move into or leave the bounds of the represented environment since its previous test? + * @param body the environment + * @param pos the test point + * @param previousPos the previous test point which is being compared against + * @param varDepth how far "into" the environment the point must be + * @return `Some(true)`, if the point has become sufficiently "deep"; + * `Some(false)`, if the point has left the sufficiently "deep" region; + * `None`, if the described points only exist outside of or only exists inside of the critical region + */ + def testStepIntoInteraction(body: PieceOfEnvironment, pos: Vector3, previousPos: Vector3, varDepth: Float): Option[Boolean] = { + val isEncroaching = body.collision.testInteraction(pos, varDepth) + val wasEncroaching = body.collision.testInteraction(previousPos, varDepth) + if (isEncroaching != wasEncroaching) { + Some(isEncroaching) + } else { + None + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/RespondsToZoneEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/RespondsToZoneEnvironment.scala new file mode 100644 index 00000000..0e1feb03 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/RespondsToZoneEnvironment.scala @@ -0,0 +1,179 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.environment + +import akka.actor.{Actor, Cancellable} +import net.psforever.objects.Default +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.types.OxygenState + +import scala.collection.mutable + +/** + * The mixin code for any server object that responds to the game world around it. + * Specific types of environmental region is bound by geometry, + * designated by attributes, + * and gets reacted to when coming into contact with that geometry. + * Ideally, the target under control instigates the responses towards the environment + * by independently re-evaluating the conditions of its interactions. + * Only one kind of environment can elicit a response at a time. + * While a reversal of this trigger scheme is possible, it is not ideal. + * @see `InteractsWithZoneEnvironment` + * @see `PieceOfEnvironment` + */ +trait RespondsToZoneEnvironment { + _: Actor => + /** how long the current interaction has been progressing in the current way */ + var interactionTime : Long = 0 + /** the environment that we are currently in interaction with */ + var interactWith : Option[PieceOfEnvironment] = None + /** a gesture of automation added to the interaction */ + var interactionTimer : Cancellable = Default.Cancellable + /** a mapping of responses when specific interactions occur; + * select from these options when starting an effect; + * key - type of environment, value - reaction function */ + private var interactWithEnvironmentStart: mutable.HashMap[EnvironmentTrait, RespondsToZoneEnvironment.Interaction] = + mutable.HashMap[EnvironmentTrait, RespondsToZoneEnvironment.Interaction]() + /** a mapping of responses when specific interactions cease; + * select from these options when ending an effect; + * key - type of environment, value - reaction function */ + private var interactWithEnvironmentStop: mutable.HashMap[EnvironmentTrait, RespondsToZoneEnvironment.Interaction] = + mutable.HashMap[EnvironmentTrait, RespondsToZoneEnvironment.Interaction]() + + def InteractiveObject: PlanetSideServerObject with InteractsWithZoneEnvironment + + val environmentBehavior: Receive = { + case InteractWithEnvironment(target, body, optional) => + doEnvironmentInteracting(target, body, optional) + + case EscapeFromEnvironment(target, body, optional) => + stopEnvironmentInteracting(target, body, optional) + + case RecoveredFromEnvironmentInteraction() => + recoverFromEnvironmentInteracting() + } + + def InteractWith: Option[PieceOfEnvironment] = interactWith + + def SetInteraction(attribute: EnvironmentTrait, action: RespondsToZoneEnvironment.Interaction): Unit = { + interactWithEnvironmentStart += attribute -> action + } + + def SetInteractionStop(attribute: EnvironmentTrait, action: RespondsToZoneEnvironment.Interaction): Unit = { + interactWithEnvironmentStop += attribute -> action + } + + def doEnvironmentInteracting(obj: PlanetSideServerObject, body: PieceOfEnvironment, data: Option[OxygenStateTarget]): Unit = { + val attribute = body.attribute + if (interactWith.isEmpty || interactWith.get.attribute == attribute) { + interactWith = Some(body) + interactionTimer.cancel() + interactWithEnvironmentStart.get(attribute) match { + case Some(func) => func(obj, body, data) + case None => ; + } + } + } + + def stopEnvironmentInteracting(obj: PlanetSideServerObject, body: PieceOfEnvironment, data: Option[OxygenStateTarget]): Unit = { + val attribute = body.attribute + if (interactWith.nonEmpty && interactWith.get.attribute == attribute) { + interactWith = None + interactionTimer.cancel() + interactWithEnvironmentStop.get(attribute) match { + case Some(func) => func(obj, body, data) + case _ => recoverFromEnvironmentInteracting() + } + } + } + + /** + * Reset the environment encounter fields and completely stop whatever is the current mechanic. + * This does not perform messaging relay either with mounted occupants or with any other service. + */ + def recoverFromEnvironmentInteracting(): Unit = { + interactionTimer.cancel() + interactionTime = 0 + interactWith = None + } +} + +object RespondsToZoneEnvironment { + type Interaction = (PlanetSideServerObject, PieceOfEnvironment, Option[OxygenStateTarget]) => Unit + + /** + * Calculate the effect of being exposed to a watery environment beyond its critical region. + * @param obj the target + * @param condition the current environment progressive event of the target, e.g., already drowning + * @param completionTime how long since the current environment progressive event started + * @return three values: + * whether any change in effect will occur, + * for how long this new change if effect will occur after starting, + * and what the starting progress value of this new effect looks like + */ + def drowningInWateryConditions( + obj: PlanetSideServerObject, + condition: Option[OxygenState], + completionTime: Long + ): (Boolean, Long, Float) = { + condition match { + case None => + //start suffocation process + (true, obj.Definition.UnderwaterLifespan(OxygenState.Suffocation), 100f) + case Some(OxygenState.Recovery) => + //switching from recovery to suffocation + val oldDuration: Long = obj.Definition.UnderwaterLifespan(OxygenState.Recovery) + val newDuration: Long = obj.Definition.UnderwaterLifespan(OxygenState.Suffocation) + val oldTimeRemaining: Long = completionTime - System.currentTimeMillis() + val oldTimeRatio: Float = 1f - oldTimeRemaining / oldDuration.toFloat + val percentage: Float = oldTimeRatio * 100 + val newDrownTime: Long = (newDuration * oldTimeRatio).toLong + (true, newDrownTime, percentage) + case Some(OxygenState.Suffocation) => + //interrupted while suffocating, calculate the progress and keep suffocating + val oldDuration: Long = obj.Definition.UnderwaterLifespan(OxygenState.Suffocation) + val oldTimeRemaining: Long = completionTime - System.currentTimeMillis() + val percentage: Float = (oldTimeRemaining / oldDuration.toFloat) * 100f + (false, oldTimeRemaining, percentage) + case _ => + (false, 0L, 0f) + } + } + + /** + * Calculate the effect of being removed from a watery environment beyond its critical region. + * @param obj the target + * @param condition the current environment progressive event of the target, e.g., already drowning + * @param completionTime how long since the current environment progressive event started + * @return three values: + * whether any change in effect will occur, + * for how long this new change if effect will occur after starting, + * and what the starting progress value of this new effect looks like + */ + def recoveringFromWateryConditions( + obj: PlanetSideServerObject, + condition: Option[OxygenState], + completionTime: Long + ): (Boolean, Long, Float) = { + condition match { + case Some(OxygenState.Suffocation) => + //switching from suffocation to recovery + val oldDuration: Long = obj.Definition.UnderwaterLifespan(OxygenState.Suffocation) + val newDuration: Long = obj.Definition.UnderwaterLifespan(OxygenState.Recovery) + val oldTimeRemaining: Long = completionTime - System.currentTimeMillis() + val oldTimeRatio: Float = oldTimeRemaining / oldDuration.toFloat + val percentage: Float = oldTimeRatio * 100 + val recoveryTime: Long = newDuration - (newDuration * oldTimeRatio).toLong + (true, recoveryTime, percentage) + case Some(OxygenState.Recovery) => + //interrupted while recovering, calculate the progress and keep recovering + val currTime = System.currentTimeMillis() + val duration: Long = obj.Definition.UnderwaterLifespan(OxygenState.Recovery) + val startTime: Long = completionTime - duration + val timeRemaining: Long = completionTime - currTime + val percentage: Float = ((currTime - startTime) / duration.toFloat) * 100f + (false, timeRemaining, percentage) + case _ => + (false, 0L, 100f) + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/painbox/Painbox.scala b/src/main/scala/net/psforever/objects/serverobject/painbox/Painbox.scala index 5108167f..952a8b48 100644 --- a/src/main/scala/net/psforever/objects/serverobject/painbox/Painbox.scala +++ b/src/main/scala/net/psforever/objects/serverobject/painbox/Painbox.scala @@ -13,6 +13,8 @@ object Painbox { final case class Tick() final case class Stop() + final case class EnvironmentalDamage(obj: Painbox, amount: Int) + def apply(tdef: PainboxDefinition): Painbox = { new Painbox(tdef) } diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala index 29c322dd..f197e53f 100644 --- a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -8,23 +8,26 @@ import net.psforever.objects.ce.TelepadLike import net.psforever.objects.equipment.{Equipment, EquipmentSlot, JammableMountedWeapons} import net.psforever.objects.guid.GUIDTask import net.psforever.objects.inventory.{GridInventory, InventoryItem} -import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior} import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle} import net.psforever.objects.serverobject.deploy.Deployment.DeploymentObject import net.psforever.objects.serverobject.deploy.{Deployment, DeploymentBehavior} +import net.psforever.objects.serverobject.environment._ import net.psforever.objects.serverobject.hackable.GenericHackables -import net.psforever.objects.serverobject.transfer.TransferBehavior +import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.repair.RepairableVehicle import net.psforever.objects.serverobject.terminals.Terminal -import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.serverobject.transfer.TransferBehavior +import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.VehicleShieldCharge -import net.psforever.objects.zones.Zone +import net.psforever.objects.vital.environment.EnvironmentReason +import net.psforever.objects.vital.etc.SuicideReason +import net.psforever.objects.zones._ import net.psforever.packet.game._ import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent -import net.psforever.types.{DriveState, ExoSuitType, PlanetSideGUID, Vector3} +import net.psforever.types._ import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} @@ -53,11 +56,11 @@ class VehicleControl(vehicle: Vehicle) with JammableMountedWeapons with ContainableBehavior with AntTransferBehavior - with AggravatedBehavior { - + with AggravatedBehavior + with RespondsToZoneEnvironment { //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 @@ -76,20 +79,28 @@ class VehicleControl(vehicle: Vehicle) def ChargeTransferObject = vehicle - if(vehicle.Definition == GlobalDefinitions.ant) { + def InteractiveObject = vehicle + SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) + SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) + SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath) + if (!vehicle.Definition.CanFly) { //can not recover from sinking disability + SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater) + } + + if (vehicle.Definition == GlobalDefinitions.ant) { findChargeTargetFunc = Vehicles.FindANTChargingSource findDischargeTargetFunc = Vehicles.FindANTDischargingTarget } - /** cheap flag for whether the vehicle is decaying */ - var decaying: Boolean = false - + var decaying : Boolean = false /** primary vehicle decay timer */ - var decayTimer: Cancellable = Default.Cancellable + var decayTimer : Cancellable = Default.Cancellable + /** becoming waterlogged, or drying out? */ + var submergedCondition : Option[OxygenState] = None - def receive: Receive = Enabled + def receive : Receive = Enabled - override def postStop(): Unit = { + override def postStop() : Unit = { super.postStop() damageableVehiclePostStop() decaying = false @@ -98,9 +109,10 @@ class VehicleControl(vehicle: Vehicle) context.stop(util().Actor) util().Actor = Default.Actor } + recoverFromEnvironmentInteracting() } - def Enabled: Receive = + def Enabled : Receive = checkBehavior .orElse(deployBehavior) .orElse(cargoBehavior) @@ -109,6 +121,7 @@ class VehicleControl(vehicle: Vehicle) .orElse(canBeRepairedByNanoDispenser) .orElse(containerBehavior) .orElse(antBehavior) + .orElse(environmentBehavior) .orElse { case Vehicle.Ownership(None) => LoseOwnership() @@ -116,7 +129,7 @@ class VehicleControl(vehicle: Vehicle) case Vehicle.Ownership(Some(player)) => GainOwnership(player) - case msg @ Mountable.TryMount(player, seat_num) => + case msg@Mountable.TryMount(player, seat_num) => tryMountBehavior.apply(msg) val obj = MountableObject //check that the player has actually been sat in the expected seat @@ -125,39 +138,28 @@ class VehicleControl(vehicle: Vehicle) if (seat_num == 0 && !obj.OwnerName.contains(player.Name)) { //whatever vehicle was previously owned vehicle.Zone.GUID(player.avatar.vehicle) match { - case Some(v: Vehicle) => + case Some(v : Vehicle) => v.Actor ! Vehicle.Ownership(None) case _ => player.avatar.vehicle = None } - LoseOwnership() //lose our current ownership + LoseOwnership() //lose our current ownership GainOwnership(player) //gain new ownership - } else { + } + else { decaying = false decayTimer.cancel() } + // + updateZoneInteractionProgressUI(player) } - case msg: Mountable.TryDismount => + case msg : Mountable.TryDismount => dismountBehavior.apply(msg) - val obj = MountableObject - - // Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount - if (!obj.Seats(0).isOccupied) { - obj.Velocity = Some(Vector3.Zero) - } - //are we already decaying? are we unowned? is no one seated anywhere? - if (!decaying && obj.Owner.isEmpty && obj.Seats.values.forall(!_.isOccupied)) { - decaying = true - decayTimer = context.system.scheduler.scheduleOnce( - MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes), - self, - VehicleControl.PrepareForDeletion() - ) - } + dismountCleanup() case Vehicle.ChargeShields(amount) => - val now: Long = System.currentTimeMillis() + val now : Long = System.currentTimeMillis() //make certain vehicle doesn't charge shields too quickly if ( vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields && @@ -171,17 +173,20 @@ class VehicleControl(vehicle: Vehicle) ) } + case Vehicle.UpdateZoneInteractionProgressUI(player) => + updateZoneInteractionProgressUI(player) + case FactionAffinity.ConvertFactionAffinity(faction) => val originalAffinity = vehicle.Faction if (originalAffinity != (vehicle.Faction = faction)) { vehicle.Utilities.foreach({ - case (_: Int, util: Utility) => util().Actor forward FactionAffinity.ConfirmFactionAffinity() + case (_ : Int, util : Utility) => util().Actor forward FactionAffinity.ConfirmFactionAffinity() }) } sender() ! FactionAffinity.AssertFactionAffinity(vehicle, faction) - case CommonMessages.Use(player, Some(item: SimpleItem)) - if item.Definition == GlobalDefinitions.remote_electronics_kit => + 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.Progress( @@ -205,35 +210,39 @@ class VehicleControl(vehicle: Vehicle) //vehicles are the same type //TODO want to completely swap weapons, but holster icon vanishes temporarily after swap //TODO BFR arms must be swapped properly -// //remove old weapons -// val oldWeapons = vehicle.Weapons.values.collect { case slot if slot.Equipment.nonEmpty => -// val obj = slot.Equipment.get -// slot.Equipment = None -// (obj, obj.GUID) -// }.toList -// (oldWeapons, weapons, afterInventory) + // //remove old weapons + // val oldWeapons = vehicle.Weapons.values.collect { case slot if slot.Equipment.nonEmpty => + // val obj = slot.Equipment.get + // slot.Equipment = None + // (obj, obj.GUID) + // }.toList + // (oldWeapons, weapons, afterInventory) //TODO for now, just refill ammo; assume weapons stay the same vehicle.Weapons - .collect { case (_, slot: EquipmentSlot) if slot.Equipment.nonEmpty => slot.Equipment.get } + .collect { case (_, slot : EquipmentSlot) if slot.Equipment.nonEmpty => slot.Equipment.get } .collect { - case weapon: Tool => + case weapon : Tool => weapon.AmmoSlots.foreach { ammo => ammo.Box.Capacity = ammo.Box.Definition.Capacity } } (Nil, Nil, afterInventory) - } else { + } + else { //vehicle loadout is not for this vehicle //do not transfer over weapon ammo if ( vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset ) { (Nil, Nil, afterInventory) //trunk is the same dimensions, however - } else { + } + else { //accommodate as much of inventory as possible val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) (Nil, Nil, stow) } } - finalInventory.foreach { _.obj.Faction = vehicle.Faction } + finalInventory.foreach { + _.obj.Faction = vehicle.Faction + } player.Zone.VehicleEvents ! VehicleServiceMessage( player.Zone.id, VehicleAction.ChangeLoadout(vehicle.GUID, oldWeapons, newWeapons, oldInventory, finalInventory) @@ -246,6 +255,40 @@ class VehicleControl(vehicle: Vehicle) case _ => ; } + case VehicleControl.Disable() => + PrepareForDisabled(kickPassengers = false) + context.become(Disabled) + + case Vehicle.Deconstruct(time) => + time match { + case Some(delay) => + decaying = true + decayTimer.cancel() + decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) + case _ => + PrepareForDisabled(kickPassengers = true) + PrepareForDeletion() + context.become(ReadyToDelete) + } + + case VehicleControl.PrepareForDeletion() => + PrepareForDisabled(kickPassengers = true) + PrepareForDeletion() + context.become(ReadyToDelete) + + case _ => ; + } + + def Disabled : Receive = + checkBehavior + .orElse { + case msg : Deployment.TryUndeploy => + deployBehavior.apply(msg) + + case msg : Mountable.TryDismount => + dismountBehavior.apply(msg) + dismountCleanup() + case Vehicle.Deconstruct(time) => time match { case Some(delay) => @@ -254,51 +297,84 @@ class VehicleControl(vehicle: Vehicle) decayTimer = context.system.scheduler.scheduleOnce(delay, self, VehicleControl.PrepareForDeletion()) case _ => PrepareForDeletion() + context.become(ReadyToDelete) } case VehicleControl.PrepareForDeletion() => PrepareForDeletion() + context.become(ReadyToDelete) - case _ => ; + case _ => } - val tryMountBehavior: Receive = { + def ReadyToDelete : Receive = + checkBehavior + .orElse { + case msg : Deployment.TryUndeploy => + deployBehavior.apply(msg) + + case VehicleControl.Deletion() => + val zone = vehicle.Zone + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.UnloadVehicle(Service.defaultPlayerGUID, vehicle, vehicle.GUID) + ) + zone.Transport ! Zone.Vehicle.Despawn(vehicle) + + case _ => + } + + val tryMountBehavior : Receive = { case msg @ Mountable.TryMount(user, seat_num) => - val exosuit = user.ExoSuit + val exosuit = user.ExoSuit val restriction = vehicle.Seats(seat_num).ArmorRestriction - val seatGroup = vehicle.SeatPermissionGroup(seat_num).getOrElse(AccessPermissionGroup.Passenger) - val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire) + val seatGroup = vehicle.SeatPermissionGroup(seat_num).getOrElse(AccessPermissionGroup.Passenger) + val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire) if ( (if (seatGroup == AccessPermissionGroup.Driver) { - vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked - } else { - permission != VehicleLockState.Locked - }) && + vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked + } + else { + permission != VehicleLockState.Locked + }) && (exosuit match { - case ExoSuitType.MAX => restriction == SeatArmorRestriction.MaxOnly + case ExoSuitType.MAX => restriction == SeatArmorRestriction.MaxOnly case ExoSuitType.Reinforced => restriction == SeatArmorRestriction.NoMax - case _ => restriction != SeatArmorRestriction.MaxOnly + case _ => restriction != SeatArmorRestriction.MaxOnly }) ) { mountBehavior.apply(msg) - } else { + } + else { sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, seat_num)) } } - def PrepareForDeletion(): Unit = { - decaying = false - val guid = vehicle.GUID - val zone = vehicle.Zone + def dismountCleanup(): Unit = { + val obj = MountableObject + // Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount + if (!obj.Seats(0).isOccupied) { + obj.Velocity = Some(Vector3.Zero) + } + //are we already decaying? are we unowned? is no one seated anywhere? + if (!decaying && obj.Owner.isEmpty && obj.Seats.values.forall(!_.isOccupied)) { + decaying = true + decayTimer = context.system.scheduler.scheduleOnce( + MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes), + self, + VehicleControl.PrepareForDeletion() + ) + } + } + + def PrepareForDisabled(kickPassengers: Boolean) : Unit = { + val guid = vehicle.GUID + val zone = vehicle.Zone val zoneId = zone.id val events = zone.VehicleEvents //miscellaneous changes + recoverFromEnvironmentInteracting() Vehicles.BeforeUnloadVehicle(vehicle, zone) - //become disabled - context.become(Disabled) - //cancel jammed behavior - CancelJammeredSound(vehicle) - CancelJammeredStatus(vehicle) //escape being someone else's cargo vehicle.MountedIn match { case Some(_) => @@ -311,33 +387,48 @@ class VehicleControl(vehicle: Vehicle) ) case _ => ; } - //kick all passengers - vehicle.Seats.values.foreach(seat => { - seat.Occupant match { - case Some(player) => - seat.Occupant = None - player.VehicleSeated = None - if (player.HasGUID) { - events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) - } - case None => ; - } - //abandon all cargo - vehicle.CargoHolds.values - .collect { - case hold if hold.isOccupied => - val cargo = hold.Occupant.get - CargoBehavior.HandleVehicleCargoDismount( - cargo.GUID, - cargo, - guid, - vehicle, - bailed = false, - requestedByPassenger = false, - kicked = false - ) + if (!vehicle.Flying || kickPassengers) { + //kick all passengers (either not flying, or being explicitly instructed) + vehicle.Seats.values.foreach { seat => + seat.Occupant match { + case Some(player) => + seat.Occupant = None + player.VehicleSeated = None + if (player.HasGUID) { + events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid)) + } + case None => ; } - }) + } + } + //abandon all cargo + vehicle.CargoHolds.values + .collect { + case hold if hold.isOccupied => + val cargo = hold.Occupant.get + CargoBehavior.HandleVehicleCargoDismount( + cargo.GUID, + cargo, + guid, + vehicle, + bailed = false, + requestedByPassenger = false, + kicked = false + ) + } + } + + def PrepareForDeletion() : Unit = { + decaying = false + val guid = vehicle.GUID + val zone = vehicle.Zone + val zoneId = zone.id + val events = zone.VehicleEvents + //miscellaneous changes + Vehicles.BeforeUnloadVehicle(vehicle, zone) + //cancel jammed behavior + CancelJammeredSound(vehicle) + CancelJammeredStatus(vehicle) //unregister zone.tasks ! GUIDTask.UnregisterVehicle(vehicle)(zone.GUID) //banished to the shadow realm @@ -346,22 +437,6 @@ class VehicleControl(vehicle: Vehicle) decayTimer = context.system.scheduler.scheduleOnce(5 seconds, self, VehicleControl.Deletion()) } - def Disabled: Receive = - checkBehavior - .orElse { - case msg: Deployment.TryUndeploy => - deployBehavior.apply(msg) - - case VehicleControl.Deletion() => - val zone = vehicle.Zone - zone.VehicleEvents ! VehicleServiceMessage( - zone.id, - VehicleAction.UnloadVehicle(Service.defaultPlayerGUID, vehicle, vehicle.GUID) - ) - zone.Transport ! Zone.Vehicle.Despawn(vehicle) - case _ => - } - override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = { if (vehicle.MountedIn.isEmpty) { super.TryJammerEffectActivate(target, cause) @@ -582,6 +657,197 @@ class VehicleControl(vehicle: Vehicle) } out } + + /** + * Water causes vehicles to become disabled if they dive off too far, too deep. + * Flying vehicles do not display progress towards being waterlogged. They just disable outright. + * @param obj the target + * @param body the environment + * @param data additional interaction information, if applicable + */ + def doInteractingWithWater(obj: PlanetSideServerObject, body: PieceOfEnvironment, data: Option[OxygenStateTarget]): Unit = { + val (effect: Boolean, time: Long, percentage: Float) = { + val (a, b, c) = RespondsToZoneEnvironment.drowningInWateryConditions(obj, submergedCondition, interactionTime) + if (a && vehicle.Definition.CanFly) { + (true, 0L, 0f) //no progress bar + } else { + (a, b, c) + } + } + if (effect) { + import scala.concurrent.ExecutionContext.Implicits.global + submergedCondition = Some(OxygenState.Suffocation) + interactionTime = System.currentTimeMillis() + time + interactionTimer = context.system.scheduler.scheduleOnce(delay = time milliseconds, self, VehicleControl.Disable()) + doInteractingWithWaterToTargets( + percentage, + body, + vehicle.Seats.values + .collect { case seat if seat.isOccupied => seat.Occupant.get } + .filter { p => p.isAlive && (p.Zone eq vehicle.Zone) } + ) + } + } + + /** + * Tell the given targets that + * water causes vehicles to become disabled if they dive off too far, too deep. + * @see `InteractWithEnvironment` + * @see `OxygenState` + * @see `OxygenStateTarget` + * @param percentage the progress bar completion state + * @param body the environment + * @param targets recipients of the information + */ + def doInteractingWithWaterToTargets( + percentage: Float, + body: PieceOfEnvironment, + targets: Iterable[PlanetSideServerObject] + ): Unit = { + val vtarget = Some(OxygenStateTarget(vehicle.GUID, OxygenState.Suffocation, percentage)) + targets.foreach { target => + target.Actor ! InteractWithEnvironment(target, body, vtarget) + } + } + + /** + * Lava causes vehicles to take (considerable) damage until they are inevitably destroyed. + * @param obj the target + * @param body the environment + * @param data additional interaction information, if applicable + */ + def doInteractingWithLava(obj: PlanetSideServerObject, body: PieceOfEnvironment, data: Option[OxygenStateTarget]): Unit = { + val vehicle = DamageableObject + if (!obj.Destroyed) { + PerformDamage( + vehicle, + DamageInteraction( + VehicleSource(vehicle), + EnvironmentReason(body, vehicle), + vehicle.Position + ).calculate() + ) + //keep doing damage + if (vehicle.Health > 0) { + import scala.concurrent.ExecutionContext.Implicits.global + interactionTimer = context.system.scheduler.scheduleOnce(delay = 250 milliseconds, self, InteractWithEnvironment(obj, body, None)) + } + } + } + + /** + * Death causes vehicles to be destroyed outright. + * It's not even considered as environmental damage anymore. + * @param obj the target + * @param body the environment + * @param data additional interaction information, if applicable + */ + def doInteractingWithDeath(obj: PlanetSideServerObject, body: PieceOfEnvironment, data: Option[OxygenStateTarget]): Unit = { + if (!obj.Destroyed) { + PerformDamage( + vehicle, + DamageInteraction( + VehicleSource(vehicle), + SuicideReason(), + vehicle.Position + ).calculate() + ) + } + } + + /** + * When out of water, the vehicle no longer risks becoming disabled. + * It does have to endure a recovery period to get back to full dehydration + * Flying vehicles are exempt from this process due to the abrupt disability they experience. + * @param obj the target + * @param body the environment + * @param data additional interaction information, if applicable + */ + def stopInteractingWithWater(obj: PlanetSideServerObject, body: PieceOfEnvironment, data: Option[OxygenStateTarget]): Unit = { + val (effect: Boolean, time: Long, percentage: Float) = + RespondsToZoneEnvironment.recoveringFromWateryConditions(obj, submergedCondition, interactionTime) + if (effect) { + recoverFromEnvironmentInteracting() + import scala.concurrent.ExecutionContext.Implicits.global + submergedCondition = Some(OxygenState.Recovery) + interactionTime = System.currentTimeMillis() + time + interactionTimer = context.system.scheduler.scheduleOnce(delay = time milliseconds, self, RecoveredFromEnvironmentInteraction()) + stopInteractingWithWaterToTargets( + percentage, + body, + vehicle.Seats.values + .collect { case seat if seat.isOccupied => seat.Occupant.get } + .filter { p => p.isAlive && (p.Zone eq vehicle.Zone) } + ) + } + } + + /** + * Tell the given targets that, + * when out of water, the vehicle no longer risks becoming disabled. + * @see `EscapeFromEnvironment` + * @see `OxygenState` + * @see `OxygenStateTarget` + * @param percentage the progress bar completion state + * @param body the environment + * @param targets recipients of the information + */ + def stopInteractingWithWaterToTargets( + percentage: Float, + body: PieceOfEnvironment, + targets: Iterable[PlanetSideServerObject] + ): Unit = { + val vtarget = Some(OxygenStateTarget(vehicle.GUID, OxygenState.Recovery, percentage)) + targets.foreach { target => + target.Actor ! EscapeFromEnvironment(target, body, vtarget) + } + } + + /** + * Reset the environment encounter fields and completely stop whatever is the current mechanic. + * This does not perform messaging relay either with mounted occupants or with any other service. + */ + override def recoverFromEnvironmentInteracting(): Unit = { + super.recoverFromEnvironmentInteracting() + submergedCondition = None + } + + /** + * Without altering the state or progress of a zone interaction related to water, + * update the visual progress element (progress bar) that is visible to the recipient's client. + * @param player the recipient of this ui update + */ + def updateZoneInteractionProgressUI(player : Player) : Unit = { + submergedCondition match { + case Some(OxygenState.Suffocation) => + interactWith match { + case Some(body) => + val percentage: Float = { + val (a, _, c) = RespondsToZoneEnvironment.drowningInWateryConditions(vehicle, submergedCondition, interactionTime) + if (a && vehicle.Definition.CanFly) { + 0f //no progress bar + } else { + c + } + } + doInteractingWithWaterToTargets(percentage, body, List(player)) + case _ => + recoverFromEnvironmentInteracting() + } + case Some(OxygenState.Recovery) => + vehicle.Zone.map.environment.find { _.attribute == EnvironmentAttribute.Water } match { + case Some(body) => //any body of water will do ... + stopInteractingWithWaterToTargets( + RespondsToZoneEnvironment.recoveringFromWateryConditions(vehicle, submergedCondition, interactionTime)._3, + body, + List(player) + ) + case _ => + recoverFromEnvironmentInteracting() + } + case None => ; + } + } } object VehicleControl { @@ -590,6 +856,8 @@ object VehicleControl { private case class PrepareForDeletion() + private case class Disable() + private case class Deletion() /** diff --git a/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala b/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala index d1ec63ca..4f740847 100644 --- a/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala +++ b/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala @@ -1,66 +1,80 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.vital -import net.psforever.objects.ballistics._ -import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ObjectDefinition} +import net.psforever.objects.ballistics.{PlayerSource, VehicleSource} +import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition} import net.psforever.objects.serverobject.terminals.TerminalDefinition +import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason} import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.types.{ExoSuitType, ImplantType} -abstract class VitalsActivity(target: SourceEntry) { - def Target: SourceEntry = target - val t: Long = System.currentTimeMillis() //??? - - def time: Long = t +trait VitalsActivity { + def time: Long } -abstract class HealingActivity(target: SourceEntry) extends VitalsActivity(target) +trait HealingActivity extends VitalsActivity { + def time: Long = System.currentTimeMillis() +} -abstract class DamagingActivity(target: SourceEntry) extends VitalsActivity(target) +trait DamagingActivity extends VitalsActivity { + def time: Long = data.interaction.hitTime + def data: DamageResult +} -final case class HealFromKit(target: PlayerSource, amount: Int, kit_def: KitDefinition) extends HealingActivity(target) +final case class HealFromKit(kit_def: KitDefinition, amount: Int) + extends HealingActivity final case class HealFromEquipment( - target: PlayerSource, user: PlayerSource, - amount: Int, - equipment_def: EquipmentDefinition -) extends HealingActivity(target) + equipment_def: EquipmentDefinition, + amount: Int +) extends HealingActivity -final case class HealFromTerm(target: PlayerSource, health: Int, armor: Int, term_def: TerminalDefinition) - extends HealingActivity(target) +final case class HealFromTerm(term_def: TerminalDefinition, health: Int, armor: Int) + extends HealingActivity -final case class HealFromImplant(target: PlayerSource, amount: Int, implant: ImplantType) - extends HealingActivity(target) +final case class HealFromImplant(implant: ImplantType, health: Int) + extends HealingActivity -final case class HealFromExoSuitChange(target: PlayerSource, exosuit: ExoSuitType.Value) extends HealingActivity(target) +final case class HealFromExoSuitChange(exosuit: ExoSuitType.Value) + extends HealingActivity -final case class RepairFromKit(target: PlayerSource, amount: Int, kit_def: KitDefinition) - extends HealingActivity(target) +final case class RepairFromKit(kit_def: KitDefinition, amount: Int) + extends HealingActivity() final case class RepairFromEquipment( - target: PlayerSource, user: PlayerSource, - amount: Int, - equipment_def: EquipmentDefinition -) extends HealingActivity(target) + equipment_def: EquipmentDefinition, + amount: Int +) extends HealingActivity -final case class RepairFromTerm(target: VehicleSource, amount: Int, term_def: TerminalDefinition) - extends HealingActivity(target) +final case class RepairFromTerm(term_def: TerminalDefinition, amount: Int) + extends HealingActivity -final case class VehicleShieldCharge(target: VehicleSource, amount: Int) extends HealingActivity(target) //TODO facility +final case class VehicleShieldCharge(amount: Int) + extends HealingActivity //TODO facility -final case class DamageFromProjectile(data: DamageResult) extends DamagingActivity(data.targetBefore) +final case class DamageFrom(data: DamageResult) + extends DamagingActivity -final case class DamageFromPainbox(data: DamageResult) extends DamagingActivity(data.targetBefore) +final case class DamageFromProjectile(data: DamageResult) + extends DamagingActivity -final case class PlayerSuicide(target: PlayerSource) extends DamagingActivity(target) +final case class DamageFromPainbox(data: DamageResult) + extends DamagingActivity -final case class DamageFromExplosion(target: PlayerSource, cause: ObjectDefinition) extends DamagingActivity(target) +final case class DamageFromEnvironment(data: DamageResult) + extends DamagingActivity -final case class DamageFromExplodingEntity(data: DamageResult) extends DamagingActivity(data.targetBefore) +final case class PlayerSuicide() + extends DamagingActivity { + def data: DamageResult = null //TODO do something +} + +final case class DamageFromExplodingEntity(data: DamageResult) + extends DamagingActivity /** * A vital object can be hurt or damaged or healed or repaired (HDHR). @@ -114,7 +128,13 @@ trait VitalsHistory { lastDamage = Some(result) case _: PainboxReason => vitalsHistory = DamageFromPainbox(result) +: vitalsHistory + case _: EnvironmentReason => + vitalsHistory = DamageFromEnvironment(result) +: vitalsHistory case _ => ; + vitalsHistory = DamageFrom(result) +: vitalsHistory + if(result.adversarial.nonEmpty) { + lastDamage = Some(result) + } } vitalsHistory } @@ -140,3 +160,59 @@ trait VitalsHistory { out } } + +//deprecated overrides +object HealFromKit { + def apply(Target: PlayerSource, amount: Int, kit_def: KitDefinition): HealFromKit = + HealFromKit(kit_def, amount) +} + +object HealFromEquipment { + def apply( + Target: PlayerSource, + user: PlayerSource, + amount: Int, + equipment_def: EquipmentDefinition + ): HealFromEquipment = + HealFromEquipment(user, equipment_def, amount) +} + +object HealFromTerm { + def apply(Target: PlayerSource, health: Int, armor: Int, term_def: TerminalDefinition): HealFromTerm = + HealFromTerm(term_def, health, armor) +} + +object HealFromImplant { + def apply(Target: PlayerSource, amount: Int, implant: ImplantType): HealFromImplant = + HealFromImplant(implant, amount) +} + +object HealFromExoSuitChange { + def apply(Target: PlayerSource, exosuit: ExoSuitType.Value): HealFromExoSuitChange = + HealFromExoSuitChange(exosuit) +} + +object RepairFromKit { + def apply(Target: PlayerSource, amount: Int, kit_def: KitDefinition): RepairFromKit = + RepairFromKit(kit_def, amount) +} + +object RepairFromEquipment { + def apply( + Target: PlayerSource, + user: PlayerSource, + amount: Int, + equipment_def: EquipmentDefinition + ) : RepairFromEquipment = + RepairFromEquipment(user, equipment_def, amount) +} + +object RepairFromTerm { + def apply(Target: VehicleSource, amount: Int, term_def: TerminalDefinition): RepairFromTerm = + RepairFromTerm(term_def, amount) +} + +object VehicleShieldCharge { + def apply(Target: VehicleSource, amount: Int): VehicleShieldCharge = + VehicleShieldCharge(amount) +} diff --git a/src/main/scala/net/psforever/objects/vital/environment/EnvironmentDamageModifierFunctions.scala b/src/main/scala/net/psforever/objects/vital/environment/EnvironmentDamageModifierFunctions.scala new file mode 100644 index 00000000..0f9149bf --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/environment/EnvironmentDamageModifierFunctions.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.vital.environment + +import net.psforever.objects.ballistics.PlayerSource +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.serverobject.environment.EnvironmentAttribute + +/** + * The deeper you move into lava, the greater the amount of health you burn through. + * Vehicles take significant damage. + * What do you hope to achieve by wading through molten rock anyway? + */ +case object LavaDepth extends EnvironmentDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: EnvironmentReason): Int = { + if (cause.body.attribute == EnvironmentAttribute.Lava) { + val depth: Float = scala.math.max(0, cause.body.collision.altitude - data.target.Position.z) + data.target match { + case _: PlayerSource => + (damage * (1f + depth)).toInt + case t => + damage + (0.1f * depth * t.Definition.MaxHealth).toInt + } + } else { + damage + } + } +} + +object EnvironmentDamageModifierFunctions { + //intentionally blank +} diff --git a/src/main/scala/net/psforever/objects/vital/environment/EnvironmentDamageModifiers.scala b/src/main/scala/net/psforever/objects/vital/environment/EnvironmentDamageModifiers.scala new file mode 100644 index 00000000..4050addc --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/environment/EnvironmentDamageModifiers.scala @@ -0,0 +1,17 @@ +package net.psforever.objects.vital.environment + +import net.psforever.objects.vital.base.{DamageModifiers, DamageReason} +import net.psforever.objects.vital.interaction.DamageInteraction + +object EnvironmentDamageModifiers { + trait Mod extends DamageModifiers.Mod { + def calculate(damage : Int, data : DamageInteraction, cause : DamageReason) : Int = { + cause match { + case o : EnvironmentReason => calculate(damage, data, o) + case _ => damage + } + } + + def calculate(damage : Int, data : DamageInteraction, cause : EnvironmentReason) : Int + } +} diff --git a/src/main/scala/net/psforever/objects/vital/environment/EnvironmentReason.scala b/src/main/scala/net/psforever/objects/vital/environment/EnvironmentReason.scala new file mode 100644 index 00000000..126aae11 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/environment/EnvironmentReason.scala @@ -0,0 +1,81 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.vital.environment + +import net.psforever.objects.ballistics.SourceEntry +import net.psforever.objects.serverobject.environment.{EnvironmentAttribute, PieceOfEnvironment} +import net.psforever.objects.vital.base.{DamageReason, DamageResolution} +import net.psforever.objects.vital.damage.DamageCalculations +import net.psforever.objects.vital.prop.DamageProperties +import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel} +import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions, Vitality} + +/** + * A wrapper for a "damage source" in damage calculations + * that parameterizes information necessary to explain the environment being antagonistic. + * @see `DamageCalculations` + * @param body a representative of an element of the environment + * @param against for the purposes of damage, what kind of target is being acted upon + */ +final case class EnvironmentReason(body: PieceOfEnvironment, against: DamageCalculations.Selector) extends DamageReason { + def resolution: DamageResolution.Value = DamageResolution.Hit + + def source: DamageProperties = EnvironmentReason.selectDamage(body) + + def same(test: DamageReason): Boolean = { + test match { + case o : EnvironmentReason => body == o.body //TODO eq + case _ => false + } + } + + def adversary: Option[SourceEntry] = None + + def damageModel: DamageAndResistance = EnvironmentReason.drm(against) +} + +object EnvironmentReason { + /** + * Overloaded constructor. + * @param body a representative of an element of the environment + * @param target the target being involved in this interaction + * @return an `EnvironmentReason` object + */ + def apply(body: PieceOfEnvironment, target: Vitality): EnvironmentReason = + EnvironmentReason(body, target.DamageModel.DamageUsing) + + /** variable, no resisting, quick and simple */ + def drm(against: DamageCalculations.Selector) = new DamageResistanceModel { + DamageUsing = against + ResistUsing = NoResistanceSelection + Model = SimpleResolutions.calculate + } + + /** The flags for calculating an absence of environment damage. */ + private val noDamage = new DamageProperties { } + /** The flags for calculating lava-based environment damage. */ + private val lavaDamage = new DamageProperties { + Damage0 = 5 //20 dps per 250ms + Damage1 = 37 //150 dps per 250ms + Damage2 = 12 //50 dps per 250ms + Damage3 = 12 //50 dps per 250ms + Damage4 = 37 //150 dps per 250ms + DamageToHealthOnly = true + DamageToVehicleOnly = true + DamageToBattleframeOnly = true + Modifiers = LavaDepth + //TODO Aggravated? + } + + /** + * Given an element in the environment, + * denote the type of flags and values used in the damage resulting from an interaction. + * @param environment the environmental element, with a specific attribute + * @return the damage information flags for that attribute + */ + def selectDamage(environment: PieceOfEnvironment): DamageProperties = { + environment.attribute match { + case EnvironmentAttribute.Lava => lavaDamage + case _ => noDamage + } + } +} diff --git a/src/main/scala/net/psforever/objects/vital/etc/OtherReasons.scala b/src/main/scala/net/psforever/objects/vital/etc/OtherReasons.scala deleted file mode 100644 index 3e5e437e..00000000 --- a/src/main/scala/net/psforever/objects/vital/etc/OtherReasons.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2020 PSForever -package net.psforever.objects.vital.etc - -import net.psforever.objects.ballistics.SourceEntry -import net.psforever.objects.vital.base.{DamageReason, DamageResolution} -import net.psforever.objects.vital.prop.DamageProperties -import net.psforever.objects.vital.resolution.DamageAndResistance - -final case class EnvironmentReason(body: Any, source: DamageProperties) extends DamageReason { - def resolution: DamageResolution.Value = DamageResolution.Unresolved - - def same(test: DamageReason): Boolean = { - test match { - case o : EnvironmentReason => body == o.body //TODO eq - case _ => false - } - } - - def adversary: Option[SourceEntry] = None - - def damageModel: DamageAndResistance = null -} diff --git a/src/main/scala/net/psforever/objects/zones/MapInfo.scala b/src/main/scala/net/psforever/objects/zones/MapInfo.scala index da05b431..8191c47b 100644 --- a/src/main/scala/net/psforever/objects/zones/MapInfo.scala +++ b/src/main/scala/net/psforever/objects/zones/MapInfo.scala @@ -1,11 +1,14 @@ package net.psforever.objects.zones -import enumeratum.values.{StringEnumEntry, StringEnum} +import enumeratum.values.{StringEnum, StringEnumEntry} +import net.psforever.objects.serverobject.environment._ +import net.psforever.types.Vector3 sealed abstract class MapInfo( val value: String, val checksum: Long, - val scale: MapScale + val scale: MapScale, + val environment: List[PieceOfEnvironment] ) extends StringEnumEntry {} case object MapInfo extends StringEnum[MapInfo] { @@ -14,184 +17,303 @@ case object MapInfo extends StringEnum[MapInfo] { extends MapInfo( value = "map01", checksum = 2094187456L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List( + SeaLevel(EnvironmentAttribute.Water, 35), + Pool(EnvironmentAttribute.Water, 44.92f, 5965.164f, 4801.2266f, 5893.1094f, 4730.203f), //east of seth + Pool(EnvironmentAttribute.Water, 43.625f, 5296.289f, 5356.8594f, 5265.789f, 5315.9062f), //south of bastet + Pool(EnvironmentAttribute.Water, 43.57f, 6263.2812f, 3742.9375f, 6238.0f, 3712.7188f), //north of aton + Pool(EnvironmentAttribute.Water, 43.515625f, 4805.5f, 4324.3984f, 4727.867f, 4280.2188f), //north of hapi + Pool(EnvironmentAttribute.Water, 43.0625f, 3313.1094f, 4746.4844f, 3259.4219f, 4691.2266f), //east of thoth + Pool(EnvironmentAttribute.Water, 43.51f, 1917.1016f, 4086.8984f, 1893.4844f, 4038.2734f) //between horus and amun + ) ) case object Map02 extends MapInfo( value = "map02", checksum = 1113780607L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = { + //exclude parts of voltan and naum due to their generator rooms being below sealevel + val northVoltan = 3562.4844f + val southVoltan = 3401.6875f + val eastVoltan = 4556.703f + val westVoltan = 4411.6875f + val northNaum = 3575.8047f + val southNaum = 3539.5234f + val eastNaum = 5490.6875f + val westNaum = 5427.078f + List( + Pool(EnvironmentAttribute.Water, 11, 8192, westVoltan, 0, 0), //west of voltan + Pool(EnvironmentAttribute.Water, 11, 8192, westNaum, 0, eastVoltan), //between voltan and naum + Pool(EnvironmentAttribute.Water, 11, 8192, 8192, 0, eastNaum), //east of naum + Pool(EnvironmentAttribute.Water, 11, 8192, eastVoltan, northVoltan, westVoltan), //north of voltan + Pool(EnvironmentAttribute.Water, 11, southVoltan, eastVoltan, 0, westVoltan), //south of voltan + Pool(EnvironmentAttribute.Water, 11, 8192, eastNaum, northNaum, westNaum), //north of naum + Pool(EnvironmentAttribute.Water, 11, southNaum, eastNaum, 0, westNaum) //south of naum + //TODO voltan Killplane + //TODO naum Killplane + ) + } ) case object Map03 extends MapInfo( value = "map03", checksum = 1624200906L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List( + SeaLevel(EnvironmentAttribute.Water, 35), + Pool(EnvironmentAttribute.Water, 67.3125f, 3449.586f, 5870.383f, 3313.75f, 5715.3203f), //east of itan, south of kaang + Pool(EnvironmentAttribute.Water, 53.71875f, 6013.0625f, 1861.7969f, 5947.1406f, 1634.7734f), //E6 + Pool(EnvironmentAttribute.Water, 49.625f, 7181.6953f, 1496.3828f, 6972.992f, 1340.1328f), //east of wele + Pool(EnvironmentAttribute.Water, 48.71875f, 992.5156f, 1806.5469f, 811.5547f, 1676.3359f), //west, island of leza + Pool(EnvironmentAttribute.Water, 48.5f, 1327.8125f, 2069.5781f, 152.5234f, 1979.3281f), //east, island of leza + Pool(EnvironmentAttribute.Water, 46.625f, 2384.9688f, 3659.1172f, 2238.3516f, 3483.3828f), //east of tore + Pool(EnvironmentAttribute.Water, 39.15625f, 4112.953f, 2509.3438f, 3778.5781f, 2312.789f), //south of hunhau south geowarp + Pool(EnvironmentAttribute.Water, 39.046875f, 5877.8203f, 7131.664f, 5690.5547f, 6955.383f), //north of gate2 + Pool(EnvironmentAttribute.Water, 37.984375f, 2737.2578f, 3409.9219f, 2648.3984f, 3210.711f), //northeast of tore + Pool(EnvironmentAttribute.Water, 37.703125f, 4689.1875f, 4788.922f, 4568.8438f, 4665.1016f), //north of gunuku + Pool(EnvironmentAttribute.Water, 37.53125f, 2701.6797f, 806.6172f, 2648.3984f, 738.4375f), //island with mukuru + Pool(EnvironmentAttribute.Water, 36.921875f, 3162.1094f, 1689.5703f, 3085.7422f, 1612.7734f), //north of nzame + Pool(EnvironmentAttribute.Water, 36.390625f, 4143.797f, 4872.3906f, 4021.9766f, 4798.578f), //south of gunuku + Pool(EnvironmentAttribute.Water, 35.71875f, 2591.336f, 1752.5938f, 2512.7578f, 1663.1172f) //south of nzame + ) ) case object Map04 extends MapInfo( value = "map04", checksum = 2455050867L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List(SeaLevel(EnvironmentAttribute.Water, 19.984375f)) ) case object Map05 extends MapInfo( value = "map05", checksum = 107922342L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List( + SeaLevel(EnvironmentAttribute.Water, 35.015625f), + Pool(EnvironmentAttribute.Water, 51.875f, 4571.8125f, 3015.5547f, 4455.8047f, 2852.711f), //down the road, west of bel + Pool(EnvironmentAttribute.Water, 49.8125f, 4902.336f, 3413.461f, 4754.0938f, 3210.8125f), //west of bel + Pool(EnvironmentAttribute.Water, 49.515625f, 4044.3984f, 4700.8516f, 3999.9688f, 4517.375f), //southeast of neit + Pool(EnvironmentAttribute.Water, 48.515625f, 4553.75f, 4110.2188f, 4438.6875f, 3995.3125f), //northwest of neit + Pool(EnvironmentAttribute.Water, 48.28125f, 4474.3906f, 4551.2812f, 4339.3984f, 4472.4375f), //northeast of neit + Pool(EnvironmentAttribute.Water, 45.828125f, 3808.0547f, 3901.3828f, 1432.5625f, 3720.9844f), //J17 + Pool(EnvironmentAttribute.Water, 43.765625f, 3997.2812f, 3991.539f, 3937.8906f, 3937.875f), //southwest of neit + Pool(EnvironmentAttribute.Water, 43.671875f, 2694.2031f, 3079.875f, 2552.414f, 2898.8203f), //west of anu + Pool(EnvironmentAttribute.Water, 42.671875f, 5174.4844f, 5930.133f, 4981.4297f, 5812.383f), //west of lugh + Pool(EnvironmentAttribute.Water, 42.203125f, 4935.742f, 5716.086f, 5496.6953f, 5444.5625f), //across road, west of lugh + Pool(EnvironmentAttribute.Water, 41.765625f, 2073.914f, 4982.5938f, 1995.4688f, 4899.086f), //L15-M16 + Pool(EnvironmentAttribute.Water, 41.3125f, 3761.1484f, 2616.75f, 3627.4297f, 2505.1328f), //G11, south + Pool(EnvironmentAttribute.Water, 40.421875f, 4058.8281f, 2791.6562f, 3985.1016f, 2685.3672f) //G11, north + ) ) case object Map06 extends MapInfo( value = "map06", checksum = 579139514L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List( + SeaLevel(EnvironmentAttribute.Water, 10.03125f), + Pool(EnvironmentAttribute.Water, 213.03125f, 3116.7266f, 4724.414f, 2685.8281f, 4187.4375f) //southwest of tootega + ) ) case object Map07 extends MapInfo( value = "map07", checksum = 1564014762L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List(SeaLevel(EnvironmentAttribute.Water, 29.984375f)) ) case object Map08 extends MapInfo( value = "map08", checksum = 0L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List(SeaLevel(EnvironmentAttribute.Water, 26.078125f)) ) case object Map09 extends MapInfo( value = "map09", checksum = 1380643455L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List( + SeaLevel(EnvironmentAttribute.Water, 30), + Pool(EnvironmentAttribute.Water, 41.46875f, 5964.461f, 1947.1328f, 5701.6016f, 1529.8438f), //north of wakea + Pool(EnvironmentAttribute.Water, 39.21875f, 5694.125f, 6939.8984f, 5516.922f, 6814.211f), //northeast of iva + Pool(EnvironmentAttribute.Water, 39.078125f, 4381.789f, 6650.8203f, 4071.4766f, 6445.133f), //south of iva + Pool(EnvironmentAttribute.Lava, DeepCircularSurface(Vector3(3901.5547f, 4422.746f, 224.57812f), 82.6797f)), //upper west lava pool + Pool(EnvironmentAttribute.Lava, DeepSurface(189.54688f, 4032.914f, 3893.6562f, 3912.3906f, 3666.4453f)), //lower west lava pool + Pool(EnvironmentAttribute.Lava, DeepSurface(187.57812f, 4288.1484f, 4589.0703f, 3996.3125f, 4355.6406f)), //lower central lava pool + Pool(EnvironmentAttribute.Lava, DeepSurface(181.45312f, 4635.1953f, 4579.3516f, 4406.3438f, 4303.828f)), //upper central lava pool + Pool(EnvironmentAttribute.Lava, DeepSurface(176.64062f, 4274.8125f, 4969.9688f, 4101.7734f, 4766.3594f)) //east lava pool + ) ) case object Map10 extends MapInfo( value = "map10", checksum = 230810349L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List(SeaLevel(EnvironmentAttribute.Water, 28)) ) case object Map11 extends MapInfo( value = "map11", checksum = 4129515529L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List( + SeaLevel(EnvironmentAttribute.Water, 24), + Pool(EnvironmentAttribute.Water, 44.453125f, 4289.4766f, 3124.8125f, 4070.7031f, 2892.9922f), //H10 + Pool(EnvironmentAttribute.Water, 39.984375f, 5405.9297f, 2843.8672f, 5190.1562f, 2653.5625f), //southeast of hart c campus + Pool(EnvironmentAttribute.Water, 36.15625f, 4622.3594f, 3861.6797f, 4497.9844f, 3717.3516f), //J9 + Pool(EnvironmentAttribute.Water, 35.234375f, 5596.086f, 4019.6797f, 5354.078f, 3814.1875f), //south of hart b campus + Pool(EnvironmentAttribute.Water, 34.96875f, 5899.367f, 3235.5781f, 5573.8516f, 2865.7812f), //northeast of hart c campus + Pool(EnvironmentAttribute.Water, 34.328125f, 3880.7422f, 5261.508f, 3780.9219f, 5166.953f), //east of hart a campus + Pool(EnvironmentAttribute.Water, 31.03125f, 4849.797f, 2415.4297f, 4731.8594f, 2252.1484f) //south of hart c campus + ) ) case object Map12 extends MapInfo( value = "map12", checksum = 962888126L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List(SeaLevel(EnvironmentAttribute.Water, 20.03125f)) ) case object Map13 extends MapInfo( value = "map13", checksum = 3904659548L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List(SeaLevel(EnvironmentAttribute.Water, 30)) ) case object Map14 extends MapInfo( value = "map14", checksum = 0L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List(SeaLevel(EnvironmentAttribute.Water, 0)) ) case object Map15 extends MapInfo( value = "map15", checksum = 0L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List(SeaLevel(EnvironmentAttribute.Water, 0)) ) case object Map16 extends MapInfo( value = "map16", checksum = 0L, - scale = MapScale.Dim8192 + scale = MapScale.Dim8192, + environment = List(SeaLevel(EnvironmentAttribute.Water, 0)) ) case object Ugd01 extends MapInfo( value = "ugd01", checksum = 3405929729L, - scale = MapScale.Dim2560 + scale = MapScale.Dim2560, + environment = List(SeaLevel(EnvironmentAttribute.Water, 50.734375f)) //TODO waterfalls! ) case object Ugd02 extends MapInfo( value = "ugd02", checksum = 2702486449L, - scale = MapScale.Dim2560 + scale = MapScale.Dim2560, + environment = List( + Pool(EnvironmentAttribute.Water, 194.89062f, 1763.4141f, 1415.125f, 1333.9531f, 1280.4609f), //east, northern pool + Pool(EnvironmentAttribute.Water, 192.40625f, 1717.5703f, 1219.3359f, 1572.8828f, 1036.1328f), //bottom, northern pool + Pool(EnvironmentAttribute.Water, 192.32812f, 1966.1562f, 1252.7344f, 1889.8047f, 1148.5312f), //top, northern pool + Pool(EnvironmentAttribute.Water, 191.65625f, 1869.1484f, 1195.6406f, 1743.8125f, 1050.7344f), //middle, northern pool + Pool(EnvironmentAttribute.Water, 183.98438f, 914.33594f, 1369.5f, 626.03906f, 666.3047f), //upper southern pools + Pool(EnvironmentAttribute.Water, 182.96875f, 580.7578f, 913.52344f, 520.4531f, 843.97656f) //lowest southern pool + ) ) case object Ugd03 extends MapInfo( value = "ugd03", checksum = 1673539651L, - scale = MapScale.Dim2048 + scale = MapScale.Dim2048, + environment = List(SeaLevel(EnvironmentAttribute.Death, 30)) //not actually lava, but a kill plane if you fall beneath the map ) case object Ugd04 extends MapInfo( value = "ugd04", checksum = 3797992164L, - scale = MapScale.Dim2048 + scale = MapScale.Dim2048, + environment = List(SeaLevel(EnvironmentAttribute.Death, 51.215f)) //ADB: 51.414f ) case object Ugd05 extends MapInfo( value = "ugd05", checksum = 1769572498L, - scale = MapScale.Dim2048 + scale = MapScale.Dim2048, + environment = List(SeaLevel(EnvironmentAttribute.Death, 115)) //not actually lava, but a kill plane if you fall beneath the map ) case object Ugd06 extends MapInfo( value = "ugd06", checksum = 4274683970L, - scale = MapScale.Dim2560 + scale = MapScale.Dim2560, + environment = List(SeaLevel(EnvironmentAttribute.Death, 55)) //not actually lava, but a kill plane if you fall beneath the map ) case object Map96 extends MapInfo( value = "map96", checksum = 846603446L, - scale = MapScale.Dim4096 + scale = MapScale.Dim4096, + environment = List(SeaLevel(EnvironmentAttribute.Water, 17.015625f)) ) case object Map97 extends MapInfo( value = "map97", checksum = 2810790213L, - scale = MapScale.Dim4096 + scale = MapScale.Dim4096, + environment = List( + SeaLevel(EnvironmentAttribute.Water, 10.09375f), + Pool(EnvironmentAttribute.Water, 20.484375f, 2183.8203f, 2086.5078f, 2127.2266f, 1992.5f), //north + Pool(EnvironmentAttribute.Water, 20.421875f, 1880.4375f, 1961.875f, 1816.1484f, 1915.0625f), //west + Pool(EnvironmentAttribute.Water, 20.421875f, 2028.1172f, 2232.4375f, 1976.9141f, 2181.0312f) //east + ) ) case object Map98 extends MapInfo( value = "map98", checksum = 3654267088L, - scale = MapScale.Dim4096 + scale = MapScale.Dim4096, + environment = List(SeaLevel(EnvironmentAttribute.Water, 3.5f)) ) case object Map99 extends MapInfo( value = "map99", checksum = 4113726460L, - scale = MapScale.Dim4096 + scale = MapScale.Dim4096, + environment = List(SeaLevel(EnvironmentAttribute.Water, 44.0625f)) ) val values: IndexedSeq[MapInfo] = findValues - } diff --git a/src/main/scala/net/psforever/objects/zones/ZoneMap.scala b/src/main/scala/net/psforever/objects/zones/ZoneMap.scala index 9cba03bd..47a755e7 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneMap.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneMap.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.zones +import net.psforever.objects.serverobject.environment.PieceOfEnvironment import net.psforever.objects.serverobject.structures.FoundationBuilder import net.psforever.objects.serverobject.zipline.ZipLinePath import net.psforever.objects.serverobject.{PlanetSideServerObject, ServerObjectBuilder} @@ -32,6 +33,7 @@ class ZoneMap(val name: String) { var checksum: Long = 0 var zipLinePaths: List[ZipLinePath] = List() var cavern: Boolean = false + var environment: List[PieceOfEnvironment] = List() private var linkTurretWeapon: Map[Int, Int] = Map() private var linkTerminalPad: Map[Int, Int] = Map() private var linkTerminalInterface: Map[Int, Int] = Map() diff --git a/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala b/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala index 2f8b3c82..d08c6ba3 100644 --- a/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala +++ b/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala @@ -3,122 +3,141 @@ package net.psforever.packet.game import net.psforever.newcodecs.newcodecs import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} -import net.psforever.types.PlanetSideGUID -import scodec.{Attempt, Codec} +import net.psforever.types.{OxygenState, PlanetSideGUID} +import scodec.Codec import scodec.codecs._ -import shapeless.{::, HNil} /** - * Alert the condition of a vehicle the player is using when going too far underwater. - * The player must be mounted in/on this vehicle at start time for this countdown to display. - * @param vehicle_guid the player's mounted vehicle - * @param progress the remaining countdown; - * for vehicle waterlog condition, the progress per second rate is very high - * @param active show a new countdown if `true` (resets any active countdown); - * clear any active countdowns if `false`; - * defaults to `true` + * Infomation about the progress bar displayed for a certain target's drowning condition. + * @param guid the target + * @param progress the remaining countdown + * @param condition in what state of drowning the target is progressing */ -final case class WaterloggedVehicleState(vehicle_guid: PlanetSideGUID, progress: Float, active: Boolean = true) +final case class DrowningTarget(guid: PlanetSideGUID, progress: Float, condition: OxygenState) /** * Dispatched by the server to cause the player to slowly drown. * If the player is mounted in a vehicle at the time, alert the player that the vehicle may be disabled.
*
* When a player walks too far underwater, a borderless red progress bar with a countdown from 100 (98) is displayed across the screen. - * The countdown proceeds to zero at a fixed rate and is timed with the depleting progress bar. + * The flavor text reads "Oxygen level". + * The countdown proceeds to zero at a fixed rate - it takes approximately 60s - and is timed with the depleting progress bar. * When it reaches zero, the player will be killed. * If the player is in a vehicle after a certain depth, a blue bar and countdown pair will superimpose the red indicators. - * It depletes much more rapidly than the red indicators. + * It depletes much more rapidly than the red indicators - it takes approximately 5s. * When it reaches zero, the vehicle will become disabled. * All players in the vehicle's seats will be kicked and they will not be allowed back in.
*
* Normally, the countdowns should be set to begin at 100 (100.0). * This is the earliest the drowning GUI will appear for either blue or red indicators. - * Passing greater intervals - up to 204.8 - will start the countdown silently but the GUI will be hidden until 100.0. + * Greater intervals - up to 204.8 - will start the countdown silently but the GUI will be hidden until 100.0. * (The progress indicators will actually appear to start counting from 98.) - * Managing the secondary vehicle countdown independent of the primary player countdown requires updating with the correct levels. - * The countdown can be cancelled by instructing it to be `active = false`.
- *
- * Except for updating the indicators, all other functionality of "drowning" is automated by the server. - * @param player_guid the player - * @param progress the remaining countdown; - * for character oxygen, the progress per second rate is about 1 - * @param active show a new countdown if `true` (resets any active countdown); - * clear any active countdowns if `false` - * @param vehicle_state optional state of the vehicle the player is driving + * @param player the player's oxygen state + * @param vehicle optional oxygen state of the vehicle the player is driving; + * the player must be mounted in the vehicle (at start time) */ final case class OxygenStateMessage( - player_guid: PlanetSideGUID, - progress: Float, - active: Boolean, - vehicle_state: Option[WaterloggedVehicleState] = None -) extends PlanetSideGamePacket { + player: DrowningTarget, + vehicle: Option[DrowningTarget] + ) extends PlanetSideGamePacket { type Packet = OxygenStateMessage def opcode = GamePacketOpcode.OxygenStateMessage def encode = OxygenStateMessage.encode(this) } +object DrowningTarget { + def apply(guid: PlanetSideGUID): DrowningTarget = + DrowningTarget(guid, 100, OxygenState.Suffocation) + + def apply(guid: PlanetSideGUID, progress: Float): DrowningTarget = + DrowningTarget(guid, progress, OxygenState.Suffocation) + + def recover(guid: PlanetSideGUID, progress: Float): DrowningTarget = + DrowningTarget(guid, progress, OxygenState.Recovery) +} + object OxygenStateMessage extends Marshallable[OxygenStateMessage] { - - /** - * Overloaded constructor that removes the optional state of the `WaterloggedVehicleState` parameter. - * @param player_guid the player - * @param progress the remaining countdown - * @param active show or clear the countdown - * @param vehicle_state state of the vehicle the player is driving - * @return - */ def apply( - player_guid: PlanetSideGUID, - progress: Float, - active: Boolean, - vehicle_state: WaterloggedVehicleState - ): OxygenStateMessage = - OxygenStateMessage(player_guid, progress, active, Some(vehicle_state)) + player_guid: PlanetSideGUID + ): OxygenStateMessage = + OxygenStateMessage(DrowningTarget(player_guid), None) - /** - * A simple pattern that expands the datatypes of the packet's basic `Codec`. - */ - private type basePattern = PlanetSideGUID :: Float :: Boolean :: HNil + def apply( + player_guid: PlanetSideGUID, + progress: Float + ): OxygenStateMessage = + OxygenStateMessage(DrowningTarget(player_guid, progress), None) + + def apply( + player: DrowningTarget + ): OxygenStateMessage = + OxygenStateMessage(player, None) + + def apply( + player_guid: PlanetSideGUID, + player_progress: Float, + vehicle_guid: PlanetSideGUID, + vehicle_progress: Float + ): OxygenStateMessage = + OxygenStateMessage( + DrowningTarget(player_guid, player_progress), + Some(DrowningTarget(vehicle_guid, vehicle_progress)) + ) + + def apply( + player_guid: PlanetSideGUID, + player_progress: Float, + vehicle_guid: PlanetSideGUID + ): OxygenStateMessage = + OxygenStateMessage( + DrowningTarget(player_guid, player_progress), + Some(DrowningTarget(vehicle_guid)) + ) + + def recover( + player_guid: PlanetSideGUID, + progress: Float + ): OxygenStateMessage = + OxygenStateMessage(DrowningTarget.recover(player_guid, progress), None) + + def recoverVehicle( + player_guid: PlanetSideGUID, + player_progress: Float, + vehicle_guid: PlanetSideGUID, + vehicle_progress: Float + ): OxygenStateMessage = + OxygenStateMessage( + DrowningTarget(player_guid, player_progress), + Some(DrowningTarget.recover(vehicle_guid, vehicle_progress)) + ) + + def recover( + player_guid: PlanetSideGUID, + player_progress: Float, + vehicle_guid: PlanetSideGUID, + vehicle_progress: Float + ): OxygenStateMessage = + OxygenStateMessage( + DrowningTarget.recover(player_guid, player_progress), + Some(DrowningTarget.recover(vehicle_guid, vehicle_progress)) + ) /** * A `Codec` for the repeated processing of three values. * This `Codec` is the basis for the packet's data. */ - private val base_codec: Codec[basePattern] = + private val oxygen_deprivation_codec: Codec[DrowningTarget] = ( PlanetSideGUID.codec :: newcodecs.q_float( 0.0f, 204.8f, 11 ) :: //hackish: 2^11 == 2047, so it should be 204.7; but, 204.8 allows decode == encode - bool + OxygenState.codec + ).as[DrowningTarget] implicit val codec: Codec[OxygenStateMessage] = ( - base_codec.exmap[basePattern]( - { - case guid :: time :: active :: HNil => - Attempt.successful(guid :: time :: active :: HNil) - }, - { - case guid :: time :: active :: HNil => - Attempt.successful(guid :: time :: active :: HNil) - } - ) :+ - optional( - bool, - "vehicle_state" | base_codec - .exmap[WaterloggedVehicleState]( - { - case guid :: time :: active :: HNil => - Attempt.successful(WaterloggedVehicleState(guid, time, active)) - }, - { - case WaterloggedVehicleState(guid, time, active) => - Attempt.successful(guid :: time :: active :: HNil) - } - ) - .as[WaterloggedVehicleState] - ) - ).as[OxygenStateMessage] + ("player" | oxygen_deprivation_codec) :: + optional(bool, "vehicle" | oxygen_deprivation_codec) + ).as[OxygenStateMessage] } diff --git a/src/main/scala/net/psforever/services/ServiceManager.scala b/src/main/scala/net/psforever/services/ServiceManager.scala index 4c5c9e97..23726695 100644 --- a/src/main/scala/net/psforever/services/ServiceManager.scala +++ b/src/main/scala/net/psforever/services/ServiceManager.scala @@ -65,8 +65,9 @@ class ServiceManager extends Actor { } catch { case e: InvalidActorNameException => //if an entry already exists, no harm, no foul, just don't do it again - log.warn(s"service manager says: ${e.getMessage}") - case _ => ; + log.warn(s"service manager says: service already exists - ${e.getMessage}") + case e: Exception => + log.error(s"service manager says: service could not start - ${e.getMessage}") } case Lookup(name) => diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index eabbd882..27b3faad 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -184,6 +184,10 @@ class AvatarService(zone: Zone) extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ObjectHeld(slot)) ) + case AvatarAction.OxygenState(player, vehicle) => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player.guid, AvatarResponse.OxygenState(player, vehicle)) + ) case AvatarAction.PlanetsideAttribute(guid, attribute_type, attribute_value) => AvatarEvents.publish( AvatarServiceResponse( diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index f41ea898..015abd93 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -6,6 +6,7 @@ import net.psforever.objects.ballistics.{Projectile, SourceEntry} import net.psforever.objects.ce.Deployable import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem +import net.psforever.objects.serverobject.environment.OxygenStateTarget import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent} @@ -67,6 +68,7 @@ object AvatarAction { ) extends Action final case class ObjectDelete(player_guid: PlanetSideGUID, item_guid: PlanetSideGUID, unk: Int = 0) extends Action final case class ObjectHeld(player_guid: PlanetSideGUID, slot: Int) extends Action + final case class OxygenState(player: OxygenStateTarget, vehicle: Option[OxygenStateTarget]) extends Action final case class PlanetsideAttribute(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long) extends Action final case class PlanetsideAttributeToAll(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long) diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala index 86f1497d..f4f51b3b 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -5,6 +5,7 @@ import net.psforever.objects.Player import net.psforever.objects.ballistics.{Projectile, SourceEntry} import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem +import net.psforever.objects.serverobject.environment.OxygenStateTarget import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.ObjectCreateMessage @@ -47,6 +48,7 @@ object AvatarResponse { final case class LoadProjectile(pkt: ObjectCreateMessage) extends Response final case class ObjectDelete(item_guid: PlanetSideGUID, unk: Int) extends Response final case class ObjectHeld(slot: Int) extends Response + final case class OxygenState(player: OxygenStateTarget, vehicle: Option[OxygenStateTarget]) extends Response final case class PlanetsideAttribute(attribute_type: Int, attribute_value: Long) extends Response final case class PlanetsideAttributeToAll(attribute_type: Int, attribute_value: Long) extends Response final case class PlanetsideAttributeSelf(attribute_type: Int, attribute_value: Long) extends Response diff --git a/src/main/scala/net/psforever/types/OxygenState.scala b/src/main/scala/net/psforever/types/OxygenState.scala new file mode 100644 index 00000000..be272f0f --- /dev/null +++ b/src/main/scala/net/psforever/types/OxygenState.scala @@ -0,0 +1,27 @@ +package net.psforever.types + +import enumeratum.{Enum, EnumEntry} +import net.psforever.packet.PacketHelpers +import scodec.Codec +import scodec.codecs.uint + +/** + * The progress state of being a drowning victim. + */ +sealed abstract class OxygenState extends EnumEntry {} + +/** + * The progress state of being a drowning victim. + * `Suffocation` means being too far under water. + * In terms of percentage, progress proceeds towards 0. + * `Recovery` means emerging from being too far under water. + * In terms of percentage, progress proceeds towards 100. + */ +object OxygenState extends Enum[OxygenState] { + val values: IndexedSeq[OxygenState] = findValues + + case object Recovery extends OxygenState + case object Suffocation extends OxygenState + + implicit val codec: Codec[OxygenState] = PacketHelpers.createEnumCodec(enum = this, uint(bits = 1)) +} diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index bceb9c2f..afe22d36 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -222,6 +222,7 @@ object Zones { zoneMap.checksum = info.checksum zoneMap.scale = info.scale + zoneMap.environment = info.environment zoneMap.zipLinePaths = zplData.toList diff --git a/src/test/scala/game/OxygenStateMessageTest.scala b/src/test/scala/game/OxygenStateMessageTest.scala index bc3b58c1..0b07bc07 100644 --- a/src/test/scala/game/OxygenStateMessageTest.scala +++ b/src/test/scala/game/OxygenStateMessageTest.scala @@ -4,7 +4,7 @@ package game import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ -import net.psforever.types.PlanetSideGUID +import net.psforever.types.{OxygenState, PlanetSideGUID} import scodec.bits._ class OxygenStateMessageTest extends Specification { @@ -13,11 +13,12 @@ class OxygenStateMessageTest extends Specification { "decode (self)" in { PacketCoding.decodePacket(string_self).require match { - case OxygenStateMessage(guid, progress, active, veh_state) => - guid mustEqual PlanetSideGUID(75) - progress mustEqual 50.0 - active mustEqual true - veh_state.isDefined mustEqual false + case OxygenStateMessage(player, vehicle) => + player.guid mustEqual PlanetSideGUID(75) + player.progress mustEqual 50.0 + player.condition mustEqual OxygenState.Suffocation + + vehicle.isDefined mustEqual false case _ => ko } @@ -25,21 +26,23 @@ class OxygenStateMessageTest extends Specification { "decode (vehicle)" in { PacketCoding.decodePacket(string_vehicle).require match { - case OxygenStateMessage(guid, progress, active, veh_state) => - guid mustEqual PlanetSideGUID(75) - progress mustEqual 50.0f - active mustEqual true - veh_state.isDefined mustEqual true - veh_state.get.vehicle_guid mustEqual PlanetSideGUID(1546) - veh_state.get.progress mustEqual 50.0f - veh_state.get.active mustEqual true + case OxygenStateMessage(player, vehicle) => + player.guid mustEqual PlanetSideGUID(75) + player.progress mustEqual 50.0f + player.condition mustEqual OxygenState.Suffocation + + vehicle.isDefined mustEqual true + val v = vehicle.get + v.guid mustEqual PlanetSideGUID(1546) + v.progress mustEqual 50.0f + v.condition mustEqual OxygenState.Suffocation case _ => ko } } "encode (self)" in { - val msg = OxygenStateMessage(PlanetSideGUID(75), 50.0f, true) + val msg = OxygenStateMessage(PlanetSideGUID(75), 50.0f) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string_self @@ -47,7 +50,7 @@ class OxygenStateMessageTest extends Specification { "encode (vehicle)" in { val msg = - OxygenStateMessage(PlanetSideGUID(75), 50.0f, true, WaterloggedVehicleState(PlanetSideGUID(1546), 50.0f, true)) + OxygenStateMessage(PlanetSideGUID(75), 50.0f, PlanetSideGUID(1546), 50.0f) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string_vehicle diff --git a/src/test/scala/objects/EnvironmentTest.scala b/src/test/scala/objects/EnvironmentTest.scala new file mode 100644 index 00000000..51613ef1 --- /dev/null +++ b/src/test/scala/objects/EnvironmentTest.scala @@ -0,0 +1,307 @@ +// Copyright (c) 2020 PSForever +package objects + +import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle} +import net.psforever.objects.definition.VehicleDefinition +import net.psforever.objects.serverobject.environment._ +import net.psforever.objects.serverobject.terminals.{Terminal, TerminalDefinition} +import net.psforever.objects.vital.Vitality +import net.psforever.packet.game.objectcreate.ObjectClass +import net.psforever.types.Vector3 +import org.specs2.mutable.Specification + +class EnvironmentCollisionTest extends Specification { + "DeepPlane" should { + val point: Float = 10f + val plane = DeepPlane(point) + + "have altitude" in { + plane.altitude mustEqual point + } + + "must have interaction that passes" in { + plane.testInteraction(Vector3(0,0,10), varDepth = -1) mustEqual true + plane.testInteraction(Vector3(0,0, 9), varDepth = 0) mustEqual true + plane.testInteraction(Vector3(0,0, 8), varDepth = 1) mustEqual true + } + + "must have interaction that fails" in { + plane.testInteraction(Vector3(0,0,11), varDepth = -1) mustEqual false + plane.testInteraction(Vector3(0,0,10), varDepth = 0) mustEqual false + plane.testInteraction(Vector3(0,0, 9), varDepth = 1) mustEqual false + } + } + + "DeepSquare" should { + val point: Float = 10f + val square = DeepSquare(point, 9, 9, 1, 1) + + "must have altitude" in { + square.altitude mustEqual point + } + + "must have interaction that passes" in { + square.testInteraction(Vector3(1,1, 0), varDepth = 0) mustEqual true + square.testInteraction(Vector3(1,8, 0), varDepth = 0) mustEqual true + square.testInteraction(Vector3(8,8, 0), varDepth = 0) mustEqual true + square.testInteraction(Vector3(8,1, 0), varDepth = 0) mustEqual true + square.testInteraction(Vector3(1,1,10), varDepth = -1) mustEqual true + square.testInteraction(Vector3(1,1, 9), varDepth = 0) mustEqual true + square.testInteraction(Vector3(1,1, 8), varDepth = 1) mustEqual true + } + + "must have interaction that fails" in { + square.testInteraction(Vector3(1,0, 0), varDepth = 0) mustEqual false + square.testInteraction(Vector3(1,9, 0), varDepth = 0) mustEqual false + square.testInteraction(Vector3(0,9, 0), varDepth = 0) mustEqual false + square.testInteraction(Vector3(0,1, 0), varDepth = 0) mustEqual false + square.testInteraction(Vector3(1,1,11), varDepth = -1) mustEqual false + square.testInteraction(Vector3(1,1,10), varDepth = 0) mustEqual false + square.testInteraction(Vector3(1,1, 9), varDepth = 1) mustEqual false + } + } + + "DeepSurface" should { + val point: Float = 10f + val surface = DeepSurface(point, 9, 9, 1, 1) + + "must have altitude" in { + surface.altitude mustEqual point + } + + "must have interaction that passes" in { + surface.testInteraction(Vector3(1,1,0), varDepth = 0) mustEqual true + surface.testInteraction(Vector3(1,8,0), varDepth = 0) mustEqual true + surface.testInteraction(Vector3(8,8,0), varDepth = 0) mustEqual true + surface.testInteraction(Vector3(8,1,0), varDepth = 0) mustEqual true + surface.testInteraction(Vector3(1,1,9), varDepth = -1) mustEqual true + surface.testInteraction(Vector3(1,1,9), varDepth = 0) mustEqual true + surface.testInteraction(Vector3(1,1,9), varDepth = 1) mustEqual true + } + + "must have interaction that fails" in { + surface.testInteraction(Vector3(1,0, 0), varDepth = 0) mustEqual false + surface.testInteraction(Vector3(1,9, 0), varDepth = 0) mustEqual false + surface.testInteraction(Vector3(0,9, 0), varDepth = 0) mustEqual false + surface.testInteraction(Vector3(0,1, 0), varDepth = 0) mustEqual false + surface.testInteraction(Vector3(1,1,11), varDepth = -1) mustEqual false + surface.testInteraction(Vector3(1,1,10), varDepth = 0) mustEqual false + } + } + + "DeepCircularSurface" should { + val point: Float = 10f + val center = Vector3(3, 3, point) + val surface = DeepCircularSurface(center, 3) + + "must have altitude" in { + surface.altitude mustEqual point + } + + "must have interaction that passes" in { + surface.testInteraction(Vector3(3,1,0), varDepth = 0) mustEqual true + surface.testInteraction(Vector3(1,3,0), varDepth = 0) mustEqual true + surface.testInteraction(Vector3(3,5,0), varDepth = 0) mustEqual true + surface.testInteraction(Vector3(5,3,0), varDepth = 0) mustEqual true + surface.testInteraction(Vector3(2,2,9), varDepth = -1) mustEqual true + surface.testInteraction(Vector3(2,2,9), varDepth = 0) mustEqual true + surface.testInteraction(Vector3(2,2,9), varDepth = 1) mustEqual true + } + + "must have interaction that fails" in { + surface.testInteraction(Vector3(3,0, 0), varDepth = 0) mustEqual false + surface.testInteraction(Vector3(0,3, 0), varDepth = 0) mustEqual false + surface.testInteraction(Vector3(3,6, 0), varDepth = 0) mustEqual false + surface.testInteraction(Vector3(6,3, 0), varDepth = 0) mustEqual false + surface.testInteraction(Vector3(2,2,11), varDepth = -1) mustEqual false + surface.testInteraction(Vector3(2,2,10), varDepth = 0) mustEqual false + } + } +} + +class EnvironmentAttributeTest extends Specification { + "Water" should { + "interact with drownable object" in { + EnvironmentAttribute.Water.canInteractWith( + Vehicle( + new VehicleDefinition(objectId = ObjectClass.apc_tr) { DrownAtMaxDepth = true } + ) + ) mustEqual true + } + + "not interact with object that does not drown" in { + EnvironmentAttribute.Water.canInteractWith( + Vehicle( + new VehicleDefinition(objectId = ObjectClass.apc_tr) { DrownAtMaxDepth = false } + ) + ) mustEqual false + } + + "interact with depth-disable object" in { + EnvironmentAttribute.Water.canInteractWith( + Vehicle( + new VehicleDefinition(objectId = ObjectClass.apc_tr) { DisableAtMaxDepth = true } + ) + ) mustEqual true + } + + "not interact with object that does not depth-disable" in { + EnvironmentAttribute.Water.canInteractWith( + Vehicle( + new VehicleDefinition(objectId = ObjectClass.apc_tr) { DisableAtMaxDepth = false } + ) + ) mustEqual false + } + } + + "Lava" should { + "interact with a vital object that is damageable" in { + val obj = Terminal(GlobalDefinitions.order_terminal) + obj.isInstanceOf[Vitality] mustEqual true + obj.asInstanceOf[Vitality].Definition.Damageable mustEqual true + EnvironmentAttribute.Lava.canInteractWith(obj) mustEqual true + } + + "not interact with a vital object that is not damageable" in { + val obj = Terminal(new TerminalDefinition(objectId = 455) { + def Request(player : Player, msg : Any) : Terminal.Exchange = null + Damageable = false + }) + obj.isInstanceOf[Vitality] mustEqual true + obj.asInstanceOf[Vitality].Definition.Damageable mustEqual false + EnvironmentAttribute.Lava.canInteractWith(obj) mustEqual false + } + + "not interact with an object that has no vitality" in { + val obj = Tool(GlobalDefinitions.suppressor) + obj.isInstanceOf[Vitality] mustEqual false + EnvironmentAttribute.Lava.canInteractWith(obj) mustEqual false + } + } + + "Death" should { + "interact with a vital object that is damageable" in { + val obj = Terminal(GlobalDefinitions.order_terminal) + obj.isInstanceOf[Vitality] mustEqual true + obj.asInstanceOf[Vitality].Definition.Damageable mustEqual true + EnvironmentAttribute.Death.canInteractWith(obj) mustEqual true + } + + "not interact with a vital object that is not damageable" in { + val obj = Terminal(new TerminalDefinition(objectId = 455) { + def Request(player : Player, msg : Any) : Terminal.Exchange = null + Damageable = false + }) + obj.isInstanceOf[Vitality] mustEqual true + obj.asInstanceOf[Vitality].Definition.Damageable mustEqual false + EnvironmentAttribute.Death.canInteractWith(obj) mustEqual false + } + + "not interact with an object that has no vitality" in { + val obj = Tool(GlobalDefinitions.suppressor) + obj.isInstanceOf[Vitality] mustEqual false + EnvironmentAttribute.Death.canInteractWith(obj) mustEqual false + } + } +} + +class SeaLevelTest extends Specification { + "SeaLevel" should { + val point: Float = 10f + val plane = DeepPlane(point) + val level = SeaLevel(point) + + "have altitude (same as DeepPlane)" in { + plane.altitude mustEqual level.altitude + } + + "must have interaction that passes (same as DeepPlane)" in { + plane.testInteraction(Vector3(0,0,10), varDepth = -1) mustEqual + level.testInteraction(Vector3(0,0,10), varDepth = -1) + plane.testInteraction(Vector3(0,0, 9), varDepth = 0) mustEqual + level.testInteraction(Vector3(0,0, 9), varDepth = 0) + plane.testInteraction(Vector3(0,0, 8), varDepth = 1) mustEqual + level.testInteraction(Vector3(0,0, 8), varDepth = 1) + } + + "must have interaction that fails (same as DeepPlane)" in { + plane.testInteraction(Vector3(0,0,11), varDepth = -1) mustEqual + level.testInteraction(Vector3(0,0,11), varDepth = -1) + plane.testInteraction(Vector3(0,0,10), varDepth = 0) mustEqual + level.testInteraction(Vector3(0,0,10), varDepth = 0) + plane.testInteraction(Vector3(0,0, 9), varDepth = 1) mustEqual + level.testInteraction(Vector3(0,0, 9), varDepth = 1) + } + } +} + +class PoolTest extends Specification { + "Pool" should { + val point: Float = 10f + val square = DeepSquare(point, 1, 10, 10, 1) + val pool = Pool(EnvironmentAttribute.Water, point, 1, 10, 10, 1) + + "have altitude (same as DeepSquare)" in { + pool.collision.altitude mustEqual square.altitude + } + + "must have interaction that passes (same as DeepSquare)" in { + pool.testInteraction(Vector3(1,1, 0), varDepth = 0) mustEqual + square.testInteraction(Vector3(1,1, 0), varDepth = 0) + pool.testInteraction(Vector3(1,8, 0), varDepth = 0) mustEqual + square.testInteraction(Vector3(1,8, 0), varDepth = 0) + pool.testInteraction(Vector3(8,8, 0), varDepth = 0) mustEqual + square.testInteraction(Vector3(8,8, 0), varDepth = 0) + pool.testInteraction(Vector3(8,1, 0), varDepth = 0) mustEqual + square.testInteraction(Vector3(8,1, 0), varDepth = 0) + pool.testInteraction(Vector3(1,1,10), varDepth = -1) mustEqual + square.testInteraction(Vector3(1,1,10), varDepth = -1) + pool.testInteraction(Vector3(1,1, 9), varDepth = 0) mustEqual + square.testInteraction(Vector3(1,1, 9), varDepth = 0) + pool.testInteraction(Vector3(1,1, 8), varDepth = 1) mustEqual + square.testInteraction(Vector3(1,1, 8), varDepth = 1) + } + + "must have interaction that fails (same as DeepSquare)" in { + pool.testInteraction(Vector3(1,0, 0), varDepth = 0) mustEqual + square.testInteraction(Vector3(1,0, 0), varDepth = 0) + pool.testInteraction(Vector3(1,9, 0), varDepth = 0) mustEqual + square.testInteraction(Vector3(1,9, 0), varDepth = 0) + pool.testInteraction(Vector3(0,9, 0), varDepth = 0) mustEqual + square.testInteraction(Vector3(0,9, 0), varDepth = 0) + pool.testInteraction(Vector3(0,1, 0), varDepth = 0) mustEqual + square.testInteraction(Vector3(0,1, 0), varDepth = 0) + pool.testInteraction(Vector3(1,1,11), varDepth = -1) mustEqual + square.testInteraction(Vector3(1,1,11), varDepth = -1) + pool.testInteraction(Vector3(1,1,10), varDepth = 0) mustEqual + square.testInteraction(Vector3(1,1,10), varDepth = 0) + pool.testInteraction(Vector3(1,1, 9), varDepth = 1) mustEqual + square.testInteraction(Vector3(1,1, 9), varDepth = 1) + } + } +} + +class PieceOfEnvironmentTest extends Specification { + "PieceOfEnvironment" should { + import PieceOfEnvironment.testStepIntoInteraction + val level = SeaLevel(10f) + + "detect entering a critical region" in { + testStepIntoInteraction(level, Vector3(0,0,9), Vector3(0,0,11), varDepth = 0).contains(true) mustEqual true + } + + "detect leaving a critical region" in { + testStepIntoInteraction(level, Vector3(0,0,11), Vector3(0,0,9), varDepth = 0).contains(false) mustEqual true + } + + "not detect moving outside of a critical region" in { + testStepIntoInteraction(level, Vector3(0,0,12), Vector3(0,0,11), varDepth = 0).isEmpty mustEqual true + } + + "not detect moving within a critical region" in { + testStepIntoInteraction(level, Vector3(0,0,9), Vector3(0,0,8), varDepth = 0).isEmpty mustEqual true + } + } +} + +object EnvironmentTest { } diff --git a/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala b/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala new file mode 100644 index 00000000..d6d1b8f8 --- /dev/null +++ b/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala @@ -0,0 +1,211 @@ +package objects + +import akka.testkit.TestProbe +import base.ActorTest +import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.environment._ +import net.psforever.objects.vital.{Vitality, VitalityDefinition} +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.types.{PlanetSideEmpire, Vector3} + +import scala.concurrent.duration._ + +class InteractsWithZoneEnvironmentTest extends ActorTest { + val pool1 = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) + val pool2 = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 15, 5, 10)) + val pool3 = Pool(EnvironmentAttribute.Lava, DeepSquare(-1, 15, 10, 10, 5)) + val testZone = { + val testMap = new ZoneMap(name = "test-map") { + environment = List(pool1, pool2, pool3) + } + new Zone("test-zone", testMap, zoneNumber = 0) + } + + "InteractsWithZoneEnvironment" should { + "not interact with any environment when it does not encroach any environment" in { + val testProbe = TestProbe() + val obj = InteractsWithZoneEnvironmentTest.testObject() + obj.Zone = testZone + obj.Actor = testProbe.ref + + assert(obj.Position == Vector3.Zero) + obj.zoneInteraction() + testProbe.expectNoMessage(max = 500 milliseconds) + } + + "acknowledge interaction when moved into the critical region of a registered environment object (just once)" in { + val testProbe = TestProbe() + val obj = InteractsWithZoneEnvironmentTest.testObject() + obj.Zone = testZone + obj.Actor = testProbe.ref + + obj.Position = Vector3(1,1,-2) + obj.zoneInteraction() + val msg = testProbe.receiveOne(max = 250 milliseconds) + assert( + msg match { + case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case _ => false + } + ) + obj.zoneInteraction() + testProbe.expectNoMessage(max = 500 milliseconds) + } + + "acknowledge ceasation of interaction when moved out of a previous occupied the critical region (just once)" in { + val testProbe = TestProbe() + val obj = InteractsWithZoneEnvironmentTest.testObject() + obj.Zone = testZone + obj.Actor = testProbe.ref + + obj.Position = Vector3(1,1,-2) + obj.zoneInteraction() + val msg1 = testProbe.receiveOne(max = 250 milliseconds) + assert( + msg1 match { + case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case _ => false + } + ) + + obj.Position = Vector3(1,1,1) + obj.zoneInteraction() + val msg2 = testProbe.receiveOne(max = 250 milliseconds) + assert( + msg2 match { + case EscapeFromEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case _ => false + } + ) + obj.zoneInteraction() + testProbe.expectNoMessage(max = 500 milliseconds) + } + + "transition between two different critical regions when the regions that the same attribute" in { + val testProbe = TestProbe() + val obj = InteractsWithZoneEnvironmentTest.testObject() + obj.Zone = testZone + obj.Actor = testProbe.ref + + obj.Position = Vector3(7,7,-2) + obj.zoneInteraction() + val msg1 = testProbe.receiveOne(max = 250 milliseconds) + assert( + msg1 match { + case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case _ => false + } + ) + + obj.Position = Vector3(12,7,-2) + obj.zoneInteraction() + val msg2 = testProbe.receiveOne(max = 250 milliseconds) + assert( + msg2 match { + case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool2) + case _ => false + } + ) + assert(pool1.attribute == pool2.attribute) + } + + "transition between two different critical regions when the regions have different attributes" in { + val testProbe = TestProbe() + val obj = InteractsWithZoneEnvironmentTest.testObject() + obj.Zone = testZone + obj.Actor = testProbe.ref + + obj.Position = Vector3(7,7,-2) + obj.zoneInteraction() + val msg1 = testProbe.receiveOne(max = 250 milliseconds) + assert( + msg1 match { + case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case _ => false + } + ) + + obj.Position = Vector3(7,12,-2) + obj.zoneInteraction() + val msgs = testProbe.receiveN(2, max = 250 milliseconds) + assert( + msgs.head match { + case EscapeFromEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case _ => false + } + ) + assert( + msgs(1) match { + case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool3) + case _ => false + } + ) + assert(pool1.attribute != pool3.attribute) + } + } + + "when interactions are disallowed, end any current interaction" in { + val testProbe = TestProbe() + val obj = InteractsWithZoneEnvironmentTest.testObject() + obj.Zone = testZone + obj.Actor = testProbe.ref + + obj.Position = Vector3(1,1,-2) + obj.zoneInteraction() + val msg1 = testProbe.receiveOne(max = 250 milliseconds) + assert( + msg1 match { + case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case _ => false + } + ) + + obj.allowZoneEnvironmentInteractions = false + val msg2 = testProbe.receiveOne(max = 250 milliseconds) + assert( + msg2 match { + case EscapeFromEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case _ => false + } + ) + obj.zoneInteraction() + testProbe.expectNoMessage(max = 500 milliseconds) + } + + "when interactions are allowed, after having been disallowed, engage in any detected interaction" in { + val testProbe = TestProbe() + val obj = InteractsWithZoneEnvironmentTest.testObject() + obj.Zone = testZone + obj.Actor = testProbe.ref + + obj.allowZoneEnvironmentInteractions = false + obj.Position = Vector3(1,1,-2) + obj.zoneInteraction() + testProbe.expectNoMessage(max = 500 milliseconds) + + obj.allowZoneEnvironmentInteractions = true + val msg1 = testProbe.receiveOne(max = 250 milliseconds) + assert( + msg1 match { + case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case _ => false + } + ) + } +} + +object InteractsWithZoneEnvironmentTest { + def testObject(): PlanetSideServerObject with InteractsWithZoneEnvironment = { + new PlanetSideServerObject + with InteractsWithZoneEnvironment + with Vitality { + def Faction: PlanetSideEmpire.Value = PlanetSideEmpire.VS + def DamageModel = null + def Definition: ObjectDefinition with VitalityDefinition = new ObjectDefinition(objectId = 0) with VitalityDefinition { + Damageable = true + DrownAtMaxDepth = true + } + } + } +} diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala index c258de4d..4bde843f 100644 --- a/src/test/scala/objects/PlayerControlTest.scala +++ b/src/test/scala/objects/PlayerControlTest.scala @@ -1,9 +1,11 @@ // Copyright (c) 2020 PSForever package objects -/* -import akka.actor.Props + +import akka.actor.typed.ActorRef +import akka.actor.{ActorSystem, Props} import akka.testkit.TestProbe import base.ActorTest +import net.psforever.actors.session.AvatarActor import net.psforever.objects.avatar.{Avatar, PlayerControl} import net.psforever.objects.ballistics._ import net.psforever.objects.guid.NumberPoolHub @@ -12,38 +14,41 @@ 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.objects.serverobject.environment.{DeepSquare, EnvironmentAttribute, OxygenStateTarget, Pool} +import net.psforever.objects.vital.base.DamageResolution +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.packet.game._ import net.psforever.types._ -import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import scala.concurrent.duration._ class PlayerControlHealTest extends ActorTest { + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val player2 = + Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { override def SetupNumberPools() = {} GUID(guid) + override def LivePlayers = List(player1, player2) + override def AvatarEvents = avatarProbe.ref } - val avatarProbe = TestProbe() - zone.AvatarEvents = avatarProbe.ref - val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 player1.Zone = zone player1.Spawn() player1.Position = Vector3(2, 0, 0) guid.register(player1.avatar.locker, 5) - player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") - val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, null), "player1-control") player2.Zone = zone player2.Spawn() guid.register(player2.avatar.locker, 6) - player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") + player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2, null), "player2-control") val tool = Tool(GlobalDefinitions.medicalapplicator) //guid=3 & 4 - guid.register(player1, 1) guid.register(player2, 2) guid.register(tool, 3) @@ -104,26 +109,25 @@ class PlayerControlHealTest extends ActorTest { } } } - class PlayerControlHealSelfTest extends ActorTest { + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { override def SetupNumberPools() = {} GUID(guid) + override def LivePlayers = List(player1) + override def AvatarEvents = avatarProbe.ref } - val avatarProbe = TestProbe() - zone.AvatarEvents = avatarProbe.ref - val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 player1.Zone = zone player1.Spawn() player1.Position = Vector3(2, 0, 0) guid.register(player1.avatar.locker, 5) - player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, null), "player1-control") val tool = Tool(GlobalDefinitions.medicalapplicator) //guid=3 & 4 - guid.register(player1, 1) guid.register(tool, 3) guid.register(tool.AmmoSlot.Box, 4) @@ -181,30 +185,30 @@ class PlayerControlHealSelfTest extends ActorTest { } class PlayerControlRepairTest extends ActorTest { + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val player2 = + Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { override def SetupNumberPools() = {} GUID(guid) + override def LivePlayers = List(player1, player2) + override def AvatarEvents = avatarProbe.ref } - val avatarProbe = TestProbe() - zone.AvatarEvents = avatarProbe.ref - val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 player1.Zone = zone player1.Spawn() player1.Position = Vector3(2, 0, 0) guid.register(player1.avatar.locker, 5) - player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") - val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, null), "player1-control") player2.Zone = zone player2.Spawn() guid.register(player2.avatar.locker, 6) - player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") + player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2, null), "player2-control") val tool = Tool(GlobalDefinitions.bank) //guid=3 & 4 - guid.register(player1, 1) guid.register(player2, 2) guid.register(tool, 3) @@ -277,24 +281,24 @@ class PlayerControlRepairTest extends ActorTest { } class PlayerControlRepairSelfTest extends ActorTest { + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val avatarProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { override def SetupNumberPools() = {} GUID(guid) + override def LivePlayers = List(player1) + override def AvatarEvents = avatarProbe.ref } - val avatarProbe = TestProbe() - zone.AvatarEvents = avatarProbe.ref - val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 player1.Zone = zone player1.Spawn() player1.Position = Vector3(2, 0, 0) guid.register(player1.avatar.locker, 5) - player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, null), "player1-control") val tool = Tool(GlobalDefinitions.bank) //guid=3 & 4 - guid.register(player1, 1) guid.register(tool, 3) guid.register(tool.AmmoSlot.Box, 4) @@ -352,40 +356,44 @@ class PlayerControlRepairSelfTest extends ActorTest { } class PlayerControlDamageTest extends ActorTest { + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val player2 = + Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + val avatarProbe = TestProbe() + val activityProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { override def SetupNumberPools() = {} GUID(guid) + override def LivePlayers = List(player1, player2) + override def AvatarEvents = avatarProbe.ref + override def Activity = activityProbe.ref } - val activityProbe = TestProbe() - val avatarProbe = TestProbe() - zone.Activity = activityProbe.ref - zone.AvatarEvents = avatarProbe.ref - val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + player1.Zone = zone player1.Spawn() player1.Position = Vector3(2, 0, 0) guid.register(player1.avatar.locker, 5) - player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") - val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, null), name = "player1-control") player2.Zone = zone player2.Spawn() guid.register(player2.avatar.locker, 6) - player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") + val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) + player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2, avatarActor), name = "player2-control") + val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4 val projectile = tool.Projectile - val playerSource = SourceEntry(player2) + val player1Source = PlayerSource(player1) val resolved = DamageInteraction( - playerSource, + SourceEntry(player2), ProjectileReason( DamageResolution.Hit, Projectile( projectile, tool.Definition, tool.FireMode, - PlayerSource(player1), + player1Source, 0, Vector3(2, 0, 0), Vector3(-1, 0, 0) @@ -400,12 +408,14 @@ class PlayerControlDamageTest extends ActorTest { guid.register(tool, 3) guid.register(tool.AmmoSlot.Box, 4) expectNoMessage(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(4, 500 milliseconds) + val msg_avatar = avatarProbe.receiveN(3, 500 milliseconds) + val msg_stamina = probe.receiveOne(500 milliseconds) val msg_activity = activityProbe.receiveOne(200 milliseconds) assert( msg_avatar.head match { @@ -414,13 +424,13 @@ class PlayerControlDamageTest extends ActorTest { } ) assert( - msg_avatar(1) match { - case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => true - case _ => false + msg_stamina match { + case AvatarActor.ConsumeStamina(_) => true + case _ => false } ) assert( - msg_avatar(2) match { + msg_avatar(1) match { case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true case _ => false } @@ -428,17 +438,17 @@ class PlayerControlDamageTest extends ActorTest { assert( msg_activity match { case activity: Zone.HotSpot.Activity => - activity.attacker == PlayerSource(player1) && - activity.defender == playerSource && + activity.attacker == player1Source && + activity.defender == PlayerSource(player2) && activity.location == Vector3(1, 0, 0) case _ => false } ) assert( - msg_avatar(3) match { + msg_avatar(2) match { case AvatarServiceMessage( "TestCharacter2", - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(17, Vector3(2, 0, 0))) + AvatarAction.HitHint(PlanetSideGUID(1), PlanetSideGUID(2)) ) => true case _ => false @@ -451,39 +461,49 @@ class PlayerControlDamageTest extends ActorTest { } class PlayerControlDeathStandingTest extends ActorTest { + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val player2 = + Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + val avatarProbe = TestProbe() + val activityProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { override def SetupNumberPools() = {} GUID(guid) + override def LivePlayers = List(player1, player2) + override def AvatarEvents = avatarProbe.ref + override def Activity = activityProbe.ref } - val avatarProbe = TestProbe() - zone.AvatarEvents = avatarProbe.ref - val activityProbe = TestProbe() - zone.Activity = activityProbe.ref - val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 player1.Zone = zone player1.Spawn() player1.Position = Vector3(2, 0, 0) guid.register(player1.avatar.locker, 5) - player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") - val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, null), name = "player1-control") player2.Zone = zone player2.Spawn() guid.register(player2.avatar.locker, 6) - player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") + val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) + player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2, avatarActor), name = "player2-control") - val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4 - val projectile = tool.Projectile - val player1Source = SourceEntry(player1) + val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4 + val projectile = tool.Projectile + val player1Source = PlayerSource(player1) val resolved = DamageInteraction( SourceEntry(player2), ProjectileReason( DamageResolution.Hit, - Projectile(projectile, tool.Definition, tool.FireMode, player1Source, 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), - player2.DamageModel + Projectile( + projectile, + tool.Definition, + tool.FireMode, + player1Source, + 0, + Vector3(2, 0, 0), + Vector3(-1, 0, 0) + ), + player1.DamageModel ), Vector3(1, 0, 0) ) @@ -506,7 +526,8 @@ class PlayerControlDeathStandingTest extends ActorTest { assert(player2.isAlive) player2.Actor ! Vitality.Damage(applyDamageTo) - val msg_avatar = avatarProbe.receiveN(8, 500 milliseconds) + val msg_avatar = avatarProbe.receiveN(7, 500 milliseconds) + val msg_stamina = probe.receiveOne(500 milliseconds) activityProbe.expectNoMessage(200 milliseconds) assert( msg_avatar.head match { @@ -515,43 +536,42 @@ class PlayerControlDeathStandingTest extends ActorTest { } ) assert( - msg_avatar(1) match { - case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => - true - case _ => false + msg_stamina match { + case AvatarActor.DeinitializeImplants() => true + case _ => false } ) assert( - msg_avatar(2) match { + msg_avatar(1) match { case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), None)) => true case _ => false } ) assert( - msg_avatar(3) match { + msg_avatar(2) match { case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true case _ => false } ) assert( - msg_avatar(4) match { + msg_avatar(3) match { case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 7, _)) => true case _ => false } ) assert( - msg_avatar(5) match { + msg_avatar(4) match { case AvatarServiceMessage( "TestCharacter2", - AvatarAction.SendResponse(_, DestroyMessage(PlanetSideGUID(2), PlanetSideGUID(2), _, Vector3.Zero)) + AvatarAction.SendResponse(_, DestroyMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _)) ) => true case _ => false } ) assert( - msg_avatar(6) match { + msg_avatar(5) match { case AvatarServiceMessage( "TestCharacter2", AvatarAction.SendResponse( @@ -564,7 +584,7 @@ class PlayerControlDeathStandingTest extends ActorTest { } ) assert( - msg_avatar(7) match { + msg_avatar(6) match { case AvatarServiceMessage("test", AvatarAction.DestroyDisplay(killer, victim, _, _)) if killer.Name.equals(player1.Name) && victim.Name.equals(player2.Name) => true @@ -579,51 +599,61 @@ class PlayerControlDeathStandingTest extends ActorTest { } class PlayerControlDeathSeatedTest extends ActorTest { + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val player2 = + Player(Avatar(1, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + val avatarProbe = TestProbe() + val activityProbe = TestProbe() val guid = new NumberPoolHub(new MaxNumberSource(15)) val zone = new Zone("test", new ZoneMap("test"), 0) { override def SetupNumberPools() = {} GUID(guid) + override def LivePlayers = List(player1, player2) + override def AvatarEvents = avatarProbe.ref + override def Activity = activityProbe.ref } - val avatarProbe = TestProbe() - zone.AvatarEvents = avatarProbe.ref - val activityProbe = TestProbe() - zone.Activity = activityProbe.ref - val player1 = - Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 player1.Zone = zone player1.Spawn() player1.Position = Vector3(2, 0, 0) - guid.register(player1.avatar.locker, 6) - player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1), "player1-control") - val player2 = - Player(Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=2 + guid.register(player1.avatar.locker, 5) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, null), name = "player1-control") player2.Zone = zone player2.Spawn() - guid.register(player2.avatar.locker, 7) - player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2), "player2-control") + guid.register(player2.avatar.locker, 6) + val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) + player2.Actor = system.actorOf(Props(classOf[PlayerControl], player2, avatarActor), name = "player2-control") + val tool = Tool(GlobalDefinitions.suppressor) //guid 3 & 4 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 = DamageInteraction( - SourceEntry(player2), - ProjectileReason( - DamageResolution.Hit, - Projectile(projectile, tool.Definition, tool.FireMode, player1Source, 0, Vector3(2, 0, 0), Vector3(-1, 0, 0)), - player2.DamageMode - ), - Vector3(1, 0, 0) - ) - val applyDamageTo = resolved.calculate() guid.register(player1, 1) guid.register(player2, 2) guid.register(tool, 3) guid.register(tool.AmmoSlot.Box, 4) - guid.register(vehicle, 5) + guid.register(vehicle, 7) + val projectile = tool.Projectile + val player1Source = PlayerSource(player1) + val resolved = DamageInteraction( + SourceEntry(player2), + ProjectileReason( + DamageResolution.Hit, + Projectile( + projectile, + tool.Definition, + tool.FireMode, + player1Source, + 0, + Vector3(2, 0, 0), + Vector3(-1, 0, 0) + ), + player1.DamageModel + ), + Vector3(1, 0, 0) + ) + val applyDamageTo = resolved.calculate() expectNoMessage(200 milliseconds) "PlayerControl" should { @@ -636,11 +666,21 @@ class PlayerControlDeathSeatedTest extends ActorTest { assert(player2.isAlive) player2.Actor ! Vitality.Damage(applyDamageTo) - val msg_avatar = avatarProbe.receiveN(9, 500 milliseconds) + val msg_avatar = avatarProbe.receiveN(8, 500 milliseconds) + val msg_stamina = probe.receiveOne(500 milliseconds) activityProbe.expectNoMessage(200 milliseconds) + assert( + msg_stamina match { + case AvatarActor.DeinitializeImplants() => true + case _ => false + } + ) assert( msg_avatar.head match { - case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => + case AvatarServiceMessage( + "TestCharacter2", + AvatarAction.Killed(PlanetSideGUID(2), Some(PlanetSideGUID(7))) + ) => true case _ => false } @@ -649,7 +689,7 @@ class PlayerControlDeathSeatedTest extends ActorTest { msg_avatar(1) match { case AvatarServiceMessage( "TestCharacter2", - AvatarAction.Killed(PlanetSideGUID(2), Some(PlanetSideGUID(5))) + AvatarAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(7), PlanetSideGUID(2), _, _, _, _)) ) => true case _ => false @@ -657,16 +697,6 @@ class PlayerControlDeathSeatedTest extends ActorTest { ) assert( msg_avatar(2) match { - case AvatarServiceMessage( - "TestCharacter2", - AvatarAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(5), PlanetSideGUID(2), _, _, _, _)) - ) => - true - case _ => false - } - ) - assert( - msg_avatar(3) match { case AvatarServiceMessage( "TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 29, 1) @@ -676,29 +706,29 @@ class PlayerControlDeathSeatedTest extends ActorTest { } ) assert( - msg_avatar(4) match { + msg_avatar(3) match { case AvatarServiceMessage("test", AvatarAction.ObjectDelete(PlanetSideGUID(2), PlanetSideGUID(2), _)) => true case _ => false } ) assert( - msg_avatar(5) match { + msg_avatar(4) match { case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true case _ => false } ) assert( - msg_avatar(6) match { + msg_avatar(5) match { case AvatarServiceMessage( "TestCharacter2", - AvatarAction.SendResponse(_, DestroyMessage(PlanetSideGUID(2), PlanetSideGUID(2), _, Vector3.Zero)) + AvatarAction.SendResponse(_, DestroyMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _)) ) => true case _ => false } ) assert( - msg_avatar(7) match { + msg_avatar(6) match { case AvatarServiceMessage( "TestCharacter2", AvatarAction.SendResponse( @@ -711,7 +741,7 @@ class PlayerControlDeathSeatedTest extends ActorTest { } ) assert( - msg_avatar(8) match { + msg_avatar(7) match { case AvatarServiceMessage("test", AvatarAction.DestroyDisplay(killer, victim, _, _)) if killer.Name.equals(player1.Name) && victim.Name.equals(player2.Name) => true @@ -724,7 +754,235 @@ class PlayerControlDeathSeatedTest extends ActorTest { } } -object PlayerControlTest {} +class PlayerControlInteractWithWaterTest extends ActorTest { + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val avatarProbe = TestProbe() + val guid = new NumberPoolHub(new MaxNumberSource(15)) + val pool = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) + val zone = new Zone( + id = "test", + new ZoneMap(name = "test-map") { + environment = List(pool) + }, + zoneNumber = 0 + ) { + override def SetupNumberPools() = {} + GUID(guid) + override def LivePlayers = List(player1) + override def AvatarEvents = avatarProbe.ref + } + player1.Zone = zone + player1.Spawn() + guid.register(player1.avatar.locker, 5) + val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") - */ + guid.register(player1, 1) + + "PlayerControl" should { + "cause drowning when player steps too deep in water" in { + assert(player1.Health == 100) + player1.Position = Vector3(5,5,-3) //right in the pool + player1.zoneInteraction() //trigger + + val msg_drown = avatarProbe.receiveOne(250 milliseconds) + assert( + msg_drown match { + case AvatarServiceMessage( + "TestCharacter1", + AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), OxygenState.Suffocation, 100f), _) + ) => true + case _ => false + } + ) + //player will die in 60s + //detailing these death messages is not necessary + assert(player1.Health == 100) + probe.receiveOne(65 seconds) //wait until our implants deinitialize + assert(player1.Health == 0) //ded + } + } +} + +class PlayerControlStopInteractWithWaterTest extends ActorTest { + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val avatarProbe = TestProbe() + val guid = new NumberPoolHub(new MaxNumberSource(15)) + val pool = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) + val zone = new Zone( + id = "test", + new ZoneMap(name = "test-map") { + environment = List(pool) + }, + zoneNumber = 0 + ) { + override def SetupNumberPools() = {} + GUID(guid) + override def LivePlayers = List(player1) + override def AvatarEvents = avatarProbe.ref + } + + player1.Zone = zone + player1.Spawn() + guid.register(player1.avatar.locker, 5) + val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") + + guid.register(player1, 1) + + "PlayerControl" should { + "stop drowning if player steps out of deep water" in { + assert(player1.Health == 100) + player1.Position = Vector3(5,5,-3) //right in the pool + player1.zoneInteraction() //trigger + + val msg_drown = avatarProbe.receiveOne(250 milliseconds) + assert( + msg_drown match { + case AvatarServiceMessage( + "TestCharacter1", + AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), OxygenState.Suffocation, 100f), _) + ) => true + case _ => false + } + ) + //player would normally die in 60s + player1.Position = Vector3.Zero //pool's closed + player1.zoneInteraction() //trigger + val msg_recover = avatarProbe.receiveOne(250 milliseconds) + assert( + msg_recover match { + case AvatarServiceMessage( + "TestCharacter1", + AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), OxygenState.Recovery, _), _) + ) => true + case _ => false + } + ) + assert(player1.Health == 100) //still alive? + probe.expectNoMessage(65 seconds) + assert(player1.Health == 100) //yep, still alive + } + } +} + +class PlayerControlInteractWithLavaTest extends ActorTest { + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val avatarProbe = TestProbe() + val guid = new NumberPoolHub(new MaxNumberSource(15)) + val pool = Pool(EnvironmentAttribute.Lava, DeepSquare(-1, 10, 10, 0, 0)) + val zone = new Zone( + id = "test-map", + new ZoneMap(name = "test-map") { + environment = List(pool) + }, + zoneNumber = 0 + ) { + override def SetupNumberPools() = {} + GUID(guid) + override def LivePlayers = List(player1) + override def AvatarEvents = avatarProbe.ref + override def Activity = TestProbe().ref + } + + player1.Zone = zone + player1.Spawn() + guid.register(player1.avatar.locker, 5) + val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") + + guid.register(player1, 1) + + "PlayerControl" should { + "take continuous damage if player steps into lava" in { + assert(player1.Health == 100) //alive + player1.Position = Vector3(5,5,-3) //right in the pool + player1.zoneInteraction() //trigger + + val msg_burn = avatarProbe.receiveN(3, 1 seconds) + assert( + msg_burn.head match { + case AvatarServiceMessage("test-map", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true + case _ => false + } + ) + assert( + msg_burn(1) match { + case AvatarServiceMessage("TestCharacter1", AvatarAction.EnvironmentalDamage(PlanetSideGUID(1), _, _)) => true + case _ => false + } + ) + assert( + msg_burn(2) match { + case AvatarServiceMessage("test-map", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 54, _)) => true + case _ => false + } + ) + assert(player1.Health > 0) //still alive? + probe.receiveOne(65 seconds) //wait until player1's implants deinitialize + assert(player1.Health == 0) //ded + } + } +} + +class PlayerControlInteractWithDeathTest extends ActorTest { + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val avatarProbe = TestProbe() + val guid = new NumberPoolHub(new MaxNumberSource(15)) + val pool = Pool(EnvironmentAttribute.Death, DeepSquare(-1, 10, 10, 0, 0)) + val zone = new Zone( + id = "test-map", + new ZoneMap(name = "test-map") { + environment = List(pool) + }, + zoneNumber = 0 + ) { + override def SetupNumberPools() = {} + GUID(guid) + override def LivePlayers = List(player1) + override def AvatarEvents = avatarProbe.ref + override def Activity = TestProbe().ref + } + + player1.Zone = zone + player1.Spawn() + guid.register(player1.avatar.locker, 5) + val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") + + guid.register(player1, 1) + + "PlayerControl" should { + "take continuous damage if player steps into a pool of death" in { + assert(player1.Health == 100) //alive + player1.Position = Vector3(5,5,-3) //right in the pool + player1.zoneInteraction() //trigger + + probe.receiveOne(250 milliseconds) //wait until oplayer1's implants deinitialize + assert(player1.Health == 0) //ded + } + } +} + +object PlayerControlTest { + /** + * A `TestProbe` whose `ActorRef` is packaged as a return type with it + * and is passable as a typed `AvatarActor.Command` `Behavior` object. + * Used for spawning `PlayControl` `Actor` objects with a refence to the `AvatarActor`, + * when messaging callback renders it necessary during tests + * but when accurate responses are unnecessary to emulate. + * @param system what we use to spawn the `Actor` + * @return the resulting probe, and it's modified `ActorRef` + */ + def DummyAvatar(system: ActorSystem): (TestProbe, ActorRef[AvatarActor.Command]) = { + import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps + val probe = new TestProbe(system) + val actor = ClassicActorRefOps(probe.ref).toTyped[AvatarActor.Command] + (probe, actor) + } +} diff --git a/src/test/scala/objects/VehicleControlTest.scala b/src/test/scala/objects/VehicleControlTest.scala new file mode 100644 index 00000000..ac3d833a --- /dev/null +++ b/src/test/scala/objects/VehicleControlTest.scala @@ -0,0 +1,978 @@ +// Copyright (c) 2020 PSForever +package objects + +import akka.actor.Props +import akka.actor.typed.scaladsl.adapter._ +import akka.testkit.TestProbe +import base.{ActorTest, FreedContextActorTest} +import net.psforever.actors.zone.ZoneActor +import net.psforever.objects.avatar.{Avatar, PlayerControl} +import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.objects.guid.source.MaxNumberSource +import net.psforever.objects.serverobject.environment._ +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.vehicles.{VehicleControl, VehicleLockState} +import net.psforever.objects.vital.VehicleShieldCharge +import net.psforever.objects.zones.{Zone, ZoneMap} +import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectDetachMessage, PlanetsideAttributeMessage} +import net.psforever.services.ServiceManager +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types._ + +import scala.concurrent.duration._ + +class VehicleControlPrepareForDeletionTest extends ActorTest { + val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + vehicle.Faction = PlanetSideEmpire.TR + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + val vehicleProbe = new TestProbe(system) + vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { + VehicleEvents = vehicleProbe.ref + } + + vehicle.GUID = PlanetSideGUID(1) + expectNoMessage(200 milliseconds) + + "VehicleControl" should { + "submit for unregistering when marked for deconstruction" in { + vehicle.Actor ! Vehicle.Deconstruct() + vehicleProbe.expectNoMessage(5 seconds) + } + } +} + +class VehicleControlPrepareForDeletionPassengerTest extends ActorTest { + val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + vehicle.Faction = PlanetSideEmpire.TR + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + val vehicleProbe = new TestProbe(system) + vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { + VehicleEvents = vehicleProbe.ref + } + val player1 = Player(VehicleTest.avatar1) + + vehicle.GUID = PlanetSideGUID(1) + player1.GUID = PlanetSideGUID(2) + vehicle.Seats(1).Occupant = player1 //passenger seat + player1.VehicleSeated = vehicle.GUID + expectNoMessage(200 milliseconds) + + "VehicleControl" should { + "kick all players when marked for deconstruction" in { + vehicle.Actor ! Vehicle.Deconstruct() + + val vehicle_msg = vehicleProbe.receiveN(1, 500 milliseconds) + assert( + vehicle_msg.head match { + case VehicleServiceMessage( + "test", + VehicleAction.KickPassenger(PlanetSideGUID(2), 4, false, PlanetSideGUID(1)) + ) => + true + case _ => false + } + ) + assert(player1.VehicleSeated.isEmpty) + assert(vehicle.Seats(1).Occupant.isEmpty) + } + } +} + +class VehicleControlPrepareForDeletionMountedInTest extends FreedContextActorTest { + ServiceManager.boot + val guid = new NumberPoolHub(new MaxNumberSource(10)) + val zone = new Zone("test", new ZoneMap("test"), 0) { + GUID(guid) + + override def SetupNumberPools(): Unit = {} + } + zone.actor = system.spawn(ZoneActor(zone), "test-zone-actor") + // crappy workaround but without it the zone doesn't get initialized in time + expectNoMessage(400 milliseconds) + + val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + vehicle.Faction = PlanetSideEmpire.TR + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test-cargo") + vehicle.Zone = zone + val lodestar = Vehicle(GlobalDefinitions.lodestar) + lodestar.Faction = PlanetSideEmpire.TR + val player1 = Player(VehicleTest.avatar1) //name="test1" + val player2 = Player(VehicleTest.avatar2) //name="test2" + + guid.register(vehicle, 1) + guid.register(lodestar, 2) + player1.GUID = PlanetSideGUID(3) + var utilityId = 10 + lodestar.Utilities.values.foreach { util => + util().GUID = PlanetSideGUID(utilityId) + utilityId += 1 + } + vehicle.Seats(1).Occupant = player1 //passenger seat + player1.VehicleSeated = vehicle.GUID + lodestar.Seats(0).Occupant = player2 + player2.VehicleSeated = lodestar.GUID + lodestar.CargoHolds(1).Occupant = vehicle + vehicle.MountedIn = lodestar.GUID + + val vehicleProbe = new TestProbe(system) + zone.VehicleEvents = vehicleProbe.ref + zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this + expectNoMessage(200 milliseconds) + + "VehicleControl" should { + "if mounted as cargo, self-eject when marked for deconstruction" in { + vehicle.Actor ! Vehicle.Deconstruct() + + val vehicle_msg = vehicleProbe.receiveN(6, 500 milliseconds) + //dismounting as cargo messages + assert( + vehicle_msg.head match { + case VehicleServiceMessage( + _, + VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _)) + ) => + true + case _ => false + } + ) + assert( + vehicle_msg(1) match { + case VehicleServiceMessage( + _, + VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _)) + ) => + true + case _ => false + } + ) + assert( + vehicle_msg(2) match { + case VehicleServiceMessage( + "test", + VehicleAction.SendResponse( + _, + CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0) + ) + ) => + true + case _ => false + } + ) + assert( + vehicle_msg(3) match { + case VehicleServiceMessage( + "test", + VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _)) + ) => + true + case _ => false + } + ) + assert( + vehicle_msg(4) match { + case VehicleServiceMessage( + "test", + VehicleAction.SendResponse( + _, + CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0) + ) + ) => + true + case _ => false + } + ) + //dismounting as cargo messages + //TODO: does not actually kick out the cargo, but instigates the process + assert( + vehicle_msg(5) match { + case VehicleServiceMessage( + "test", + VehicleAction.KickPassenger(PlanetSideGUID(3), 4, false, PlanetSideGUID(1)) + ) => + true + case _ => false + } + ) + assert(player1.VehicleSeated.isEmpty) + assert(vehicle.Seats(1).Occupant.isEmpty) + } + } +} + +class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActorTest { + val guid = new NumberPoolHub(new MaxNumberSource(10)) + ServiceManager.boot + val zone = new Zone("test", new ZoneMap("test"), 0) { + GUID(guid) + + override def SetupNumberPools(): Unit = {} + } + zone.actor = system.spawn(ZoneActor(zone), "test-zone-actor") + // crappy workaround but without it the zone doesn't get initialized in time + expectNoMessage(200 milliseconds) + + val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + vehicle.Faction = PlanetSideEmpire.TR + vehicle.Zone = zone + val cargoProbe = new TestProbe(system) + vehicle.Actor = cargoProbe.ref + val lodestar = Vehicle(GlobalDefinitions.lodestar) + lodestar.Faction = PlanetSideEmpire.TR + val player1 = Player(VehicleTest.avatar1) //name="test1" + val player2 = Player(VehicleTest.avatar2) //name="test2" + + guid.register(vehicle, 1) + guid.register(lodestar, 2) + player1.GUID = PlanetSideGUID(3) + player2.GUID = PlanetSideGUID(4) + var utilityId = 10 + lodestar.Utilities.values.foreach { util => + util().GUID = PlanetSideGUID(utilityId) + utilityId += 1 + } + vehicle.Seats(1).Occupant = player1 //passenger seat + player1.VehicleSeated = vehicle.GUID + lodestar.Seats(0).Occupant = player2 + player2.VehicleSeated = lodestar.GUID + lodestar.CargoHolds(1).Occupant = vehicle + vehicle.MountedIn = lodestar.GUID + + val vehicleProbe = new TestProbe(system) + zone.VehicleEvents = vehicleProbe.ref + zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this + expectNoMessage(200 milliseconds) + + "VehicleControl" should { + "if with mounted cargo, eject it when marked for deconstruction" in { + lodestar.Actor ! Vehicle.Deconstruct() + + val vehicle_msg = vehicleProbe.receiveN(6, 500 milliseconds) + assert( + vehicle_msg.head match { + case VehicleServiceMessage( + "test", + VehicleAction.KickPassenger(PlanetSideGUID(4), 4, false, PlanetSideGUID(2)) + ) => + true + case _ => false + } + ) + assert(player2.VehicleSeated.isEmpty) + assert(lodestar.Seats(0).Occupant.isEmpty) + //cargo dismounting messages + assert( + vehicle_msg(1) match { + case VehicleServiceMessage( + _, + VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _)) + ) => + true + case _ => false + } + ) + assert( + vehicle_msg(2) match { + case VehicleServiceMessage( + _, + VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _)) + ) => + true + case _ => false + } + ) + assert( + vehicle_msg(3) match { + case VehicleServiceMessage( + "test", + VehicleAction.SendResponse( + _, + CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0) + ) + ) => + true + case _ => false + } + ) + assert( + vehicle_msg(4) match { + case VehicleServiceMessage( + "test", + VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _)) + ) => + true + case _ => false + } + ) + assert( + vehicle_msg(5) match { + case VehicleServiceMessage( + "test", + VehicleAction.SendResponse( + _, + CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0) + ) + ) => + true + case _ => false + } + ) + } + } +} + +class VehicleControlMountingBlockedExosuitTest extends ActorTest { + val probe = new TestProbe(system) + def checkCanNotMount(): Unit = { + val reply = probe.receiveOne(Duration.create(100, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanNotMount]) + case _ => + assert(false) + } + } + + def checkCanMount(): Unit = { + val reply = probe.receiveOne(Duration.create(100, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount]) + case _ => + assert(false) + } + } + 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) + player1.ExoSuit = ExoSuitType.Reinforced + player1.GUID = PlanetSideGUID(1) + val player2 = Player(VehicleTest.avatar1) + player2.ExoSuit = ExoSuitType.MAX + player2.GUID = PlanetSideGUID(2) + val player3 = Player(VehicleTest.avatar1) + player3.ExoSuit = ExoSuitType.Agile + player3.GUID = PlanetSideGUID(3) + + "Vehicle Control" should { + "block players from sitting if their exo-suit is not allowed by the seat" in { + //disallow + vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) //Reinforced in non-MAX seat + checkCanNotMount() + vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) //MAX in non-Reinforced seat + checkCanNotMount() + vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) //MAX in non-MAX seat + checkCanNotMount() + vehicle.Actor.tell(Mountable.TryMount(player1, 9), probe.ref) //Reinforced in MAX-only seat + checkCanNotMount() + vehicle.Actor.tell(Mountable.TryMount(player3, 9), probe.ref) //Agile in MAX-only seat + checkCanNotMount() + + //allow + vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) + checkCanMount() + vehicle.Actor.tell(Mountable.TryMount(player2, 9), probe.ref) + checkCanMount() + vehicle.Actor.tell(Mountable.TryMount(player3, 0), probe.ref) + checkCanMount() + } + } +} + +class VehicleControlMountingBlockedSeatPermissionTest extends ActorTest { + val probe = new TestProbe(system) + def checkCanNotMount(): Unit = { + val reply = probe.receiveOne(Duration.create(100, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanNotMount]) + case _ => + assert(false) + } + } + + def checkCanMount(): Unit = { + val reply = probe.receiveOne(Duration.create(100, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount]) + case _ => + assert(false) + } + } + 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) + player1.GUID = PlanetSideGUID(1) + val player2 = Player(VehicleTest.avatar1) + player2.GUID = PlanetSideGUID(2) + + "Vehicle Control" should { + //11 June 2018: Group is not supported yet so do not bother testing it + "block players from sitting if the seat does not allow it" in { + + vehicle.PermissionGroup(2, 3) //passenger group -> empire + vehicle.Actor.tell(Mountable.TryMount(player1, 3), probe.ref) //passenger seat + checkCanMount() + vehicle.PermissionGroup(2, 0) //passenger group -> locked + vehicle.Actor.tell(Mountable.TryMount(player2, 4), probe.ref) //passenger seat + checkCanNotMount() + } + } +} + +class VehicleControlMountingDriverSeatTest extends ActorTest { + val probe = new TestProbe(system) + def checkCanMount(): Unit = { + val reply = probe.receiveOne(Duration.create(100, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount]) + case _ => + assert(false) + } + } + 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) + player1.GUID = PlanetSideGUID(1) + + "Vehicle Control" should { + "allow players to sit in the driver seat, even if it is locked, if the vehicle is unowned" in { + assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked + assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Owner.isEmpty) + vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) + checkCanMount() + assert(vehicle.Seats(0).Occupant.nonEmpty) + } + } +} + +class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest { + val probe = new TestProbe(system) + def checkCanNotMount(): Unit = { + val reply = probe.receiveOne(Duration.create(100, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanNotMount]) + case _ => + assert(false) + } + } + + def checkCanMount(): Unit = { + val reply = probe.receiveOne(Duration.create(100, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount]) + case _ => + assert(false) + } + } + 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) + player1.GUID = PlanetSideGUID(1) + val player2 = Player(VehicleTest.avatar1) + player2.GUID = PlanetSideGUID(2) + + "Vehicle Control" should { + "block players that are not the current owner from sitting in the driver seat (locked)" in { + assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked + assert(vehicle.Seats(0).Occupant.isEmpty) + vehicle.Owner = player1.GUID + + vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) + checkCanMount() + assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) + probe.receiveOne(Duration.create(100, "ms")) //discard + assert(vehicle.Seats(0).Occupant.isEmpty) + + vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) + checkCanNotMount() + assert(vehicle.Seats(0).Occupant.isEmpty) + } + } +} + +class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest { + val probe = new TestProbe(system) + def checkCanMount(): Unit = { + val reply = probe.receiveOne(Duration.create(100, "ms")) + reply match { + case msg: Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount]) + case _ => + assert(false) + } + } + 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) + player1.GUID = PlanetSideGUID(1) + val player2 = Player(VehicleTest.avatar1) + player2.GUID = PlanetSideGUID(2) + + "Vehicle Control" should { + "allow players that are not the current owner to sit in the driver seat (empire)" in { + vehicle.PermissionGroup(0, 3) //passenger group -> empire + assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Empire)) //driver group -> empire + assert(vehicle.Seats(0).Occupant.isEmpty) + vehicle.Owner = player1.GUID //owner set + + vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) + checkCanMount() + assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) + probe.receiveOne(Duration.create(100, "ms")) //discard + assert(vehicle.Seats(0).Occupant.isEmpty) + + vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) + checkCanMount() + assert(vehicle.Seats(0).Occupant.nonEmpty) + } + } +} + +class VehicleControlShieldsChargingTest extends ActorTest { + val probe = new TestProbe(system) + val vehicle = Vehicle(GlobalDefinitions.fury) + vehicle.GUID = PlanetSideGUID(10) + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { + VehicleEvents = probe.ref + } + + "charge vehicle shields" in { + assert(vehicle.Shields == 0) + assert(!vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) + + vehicle.Actor ! Vehicle.ChargeShields(15) + val msg = probe.receiveOne(500 milliseconds) + assert(msg match { + case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(_, PlanetSideGUID(10), 68, 15)) => true + case _ => false + }) + assert(vehicle.Shields == 15) + assert(vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) + } +} + +class VehicleControlShieldsNotChargingVehicleDeadTest extends ActorTest { + val probe = new TestProbe(system) + val vehicle = Vehicle(GlobalDefinitions.fury) + vehicle.GUID = PlanetSideGUID(10) + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { + VehicleEvents = probe.ref + } + + "not charge vehicle shields if the vehicle is destroyed" in { + assert(vehicle.Health > 0) + vehicle.Health = 0 + assert(vehicle.Health == 0) + assert(vehicle.Shields == 0) + assert(!vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) + vehicle.Actor.tell(Vehicle.ChargeShields(15), probe.ref) + + probe.expectNoMessage(1 seconds) + assert(vehicle.Shields == 0) + assert(!vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) + } +} + +class VehicleControlShieldsNotChargingVehicleShieldsFullTest extends ActorTest { + val probe = new TestProbe(system) + val vehicle = Vehicle(GlobalDefinitions.fury) + vehicle.GUID = PlanetSideGUID(10) + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { + VehicleEvents = probe.ref + } + + "not charge vehicle shields if the vehicle is destroyed" in { + assert(vehicle.Shields == 0) + vehicle.Shields = vehicle.MaxShields + assert(vehicle.Shields == vehicle.MaxShields) + assert(!vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) + vehicle.Actor ! Vehicle.ChargeShields(15) + + probe.expectNoMessage(1 seconds) + assert(!vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) + } +} + +class VehicleControlShieldsNotChargingTooEarlyTest extends ActorTest { + val probe = new TestProbe(system) + val vehicle = Vehicle(GlobalDefinitions.fury) + vehicle.GUID = PlanetSideGUID(10) + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { + VehicleEvents = probe.ref + } + + "charge vehicle shields" in { + assert(vehicle.Shields == 0) + + vehicle.Actor ! Vehicle.ChargeShields(15) + val msg = probe.receiveOne(200 milliseconds) + //assert(msg.isInstanceOf[Vehicle.UpdateShieldsCharge]) + assert(msg match { + case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(_, PlanetSideGUID(10), 68, 15)) => true + case _ => false + }) + assert(vehicle.Shields == 15) + + vehicle.Actor ! Vehicle.ChargeShields(15) + probe.expectNoMessage(200 milliseconds) + assert(vehicle.Shields == 15) + } +} + +//TODO implement message protocol for zone startup completion +//class VehicleControlShieldsNotChargingDamagedTest extends ActorTest { +// val probe = new TestProbe(system) +// val vehicle = Vehicle(GlobalDefinitions.fury) +// vehicle.GUID = PlanetSideGUID(10) +// vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") +// vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { +// VehicleEvents = probe.ref +// } +// // +// val beamer_wep = Tool(GlobalDefinitions.beamer) +// val p_source = PlayerSource( Player(Avatar(0, "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 = DamageInteraction(p_source, ProjectileReason(DamageResolution.Hit, projectile, fury_dm), Vector3(1.2f, 3.4f, 5.6f)) +// +// "not charge vehicle shields if recently damaged" in { +// assert(vehicle.Shields == 0) +// vehicle.Actor.tell(Vitality.Damage({case v : Vehicle => v.History(obj); obj }), probe.ref) +// +// val msg = probe.receiveOne(200 milliseconds) +// assert(msg.isInstanceOf[Vitality.DamageResolution]) +// assert(vehicle.Shields == 0) +// vehicle.Actor.tell(Vehicle.ChargeShields(15), probe.ref) +// +// probe.expectNoMessage(200 milliseconds) +// assert(vehicle.Shields == 0) +// } +//} + +class VehicleControlInteractWithWaterPartialTest extends ActorTest { + val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val playerProbe = TestProbe() + val guid = new NumberPoolHub(new MaxNumberSource(15)) + val pool = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) + val zone = new Zone( + id = "test-zone", + new ZoneMap(name = "test-map") { + environment = List(pool) + }, + zoneNumber = 0 + ) { + override def SetupNumberPools() = {} + GUID(guid) + override def LivePlayers = List(player1) + override def Vehicles = List(vehicle) + } + + guid.register(player1, 1) + guid.register(vehicle, 2) + player1.Zone = zone + player1.Spawn() + vehicle.Zone = zone + vehicle.Faction = PlanetSideEmpire.TR + vehicle.Seats(0).Occupant = player1 + player1.VehicleSeated = vehicle.GUID + player1.Actor = playerProbe.ref + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-control") + + "VehicleControl" should { + "causes disability when the vehicle drives too deep in water (check driver messaging)" in { + vehicle.Position = Vector3(5,5,-3) //right in the pool + vehicle.zoneInteraction() //trigger + + val msg_drown = playerProbe.receiveOne(250 milliseconds) + assert( + msg_drown match { + case InteractWithEnvironment( + p1, + p2, + Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f)) + ) => (p1 eq player1) && (p2 eq pool) + case _ => false + } + ) + } + } +} + +class VehicleControlInteractWithWaterTest extends ActorTest { + val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + val guid = new NumberPoolHub(new MaxNumberSource(15)) + val pool = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) + val zone = new Zone( + id = "test-zone", + new ZoneMap(name = "test-map") { + environment = List(pool) + }, + zoneNumber = 0 + ) { + override def SetupNumberPools() = {} + GUID(guid) + override def LivePlayers = List(player1) + override def Vehicles = List(vehicle) + override def AvatarEvents = avatarProbe.ref + override def VehicleEvents = vehicleProbe.ref + } + + guid.register(player1, 1) + guid.register(vehicle, 2) + guid.register(player1.avatar.locker, 5) + player1.Zone = zone + player1.Spawn() + vehicle.Zone = zone + vehicle.Faction = PlanetSideEmpire.TR + vehicle.Seats(0).Occupant = player1 + player1.VehicleSeated = vehicle.GUID + val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-control") + + "VehicleControl" should { + "causes disability when the vehicle drives too deep in water" in { + vehicle.Position = Vector3(5,5,-3) //right in the pool + vehicle.zoneInteraction() //trigger + + val msg_drown = avatarProbe.receiveOne(250 milliseconds) + assert( + msg_drown match { + case AvatarServiceMessage( + "TestCharacter1", + AvatarAction.OxygenState( + OxygenStateTarget(PlanetSideGUID(1), OxygenState.Suffocation, 100f), + Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f)) + ) + ) => true + case _ => false + } + ) + //player will die in 60s + //vehicle will disable in 5s; driver will be kicked + val msg_kick = vehicleProbe.receiveOne(6 seconds) + assert( + msg_kick match { + case VehicleServiceMessage( + "test-zone", + VehicleAction.KickPassenger(PlanetSideGUID(1), 4, _, PlanetSideGUID(2)) + ) => true + case _ => false + } + ) + //player will die, but detailing players death messages is not necessary for this test + } + } +} + +class VehicleControlStopInteractWithWaterTest extends ActorTest { + val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val playerProbe = TestProbe() + val guid = new NumberPoolHub(new MaxNumberSource(15)) + val pool = Pool(EnvironmentAttribute.Water, DeepSquare(-1, 10, 10, 0, 0)) + val zone = new Zone( + id = "test-zone", + new ZoneMap(name = "test-map") { + environment = List(pool) + }, + zoneNumber = 0 + ) { + override def SetupNumberPools() = {} + GUID(guid) + override def LivePlayers = List(player1) + override def Vehicles = List(vehicle) + } + + guid.register(player1, 1) + guid.register(vehicle, 2) + player1.Zone = zone + player1.Spawn() + vehicle.Zone = zone + vehicle.Faction = PlanetSideEmpire.TR + vehicle.Seats(0).Occupant = player1 + player1.VehicleSeated = vehicle.GUID + player1.Actor = playerProbe.ref + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-control") + + "VehicleControl" should { + "stop becoming disabled if the vehicle drives out of the water" in { + vehicle.Position = Vector3(5,5,-3) //right in the pool + vehicle.zoneInteraction() //trigger + val msg_drown = playerProbe.receiveOne(250 milliseconds) + assert( + msg_drown match { + case InteractWithEnvironment( + p1, + p2, + Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f)) + ) => (p1 eq player1) && (p2 eq pool) + case _ => false + } + ) + + vehicle.Position = Vector3.Zero //that's enough of that + vehicle.zoneInteraction() + val msg_recover = playerProbe.receiveOne(250 milliseconds) + assert( + msg_recover match { + case EscapeFromEnvironment( + p1, + p2, + Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Recovery, _)) + ) => (p1 eq player1) && (p2 eq pool) + case _ => false + } + ) + } + } +} + +class VehicleControlInteractWithLavaTest extends ActorTest { + val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() + val guid = new NumberPoolHub(new MaxNumberSource(15)) + val pool = Pool(EnvironmentAttribute.Lava, DeepSquare(-1, 10, 10, 0, 0)) + val zone = new Zone( + id = "test-zone", + new ZoneMap(name = "test-map") { + environment = List(pool) + }, + zoneNumber = 0 + ) { + override def SetupNumberPools() = {} + GUID(guid) + override def LivePlayers = List(player1) + override def Vehicles = List(vehicle) + override def AvatarEvents = avatarProbe.ref + override def VehicleEvents = vehicleProbe.ref + override def Activity = TestProbe().ref + } + + guid.register(player1, 1) + guid.register(vehicle, 2) + guid.register(player1.avatar.locker, 5) + player1.Zone = zone + player1.Spawn() + vehicle.Zone = zone + vehicle.Faction = PlanetSideEmpire.TR + vehicle.Seats(0).Occupant = player1 + player1.VehicleSeated = vehicle.GUID + val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-control") + + "VehicleControl" should { + "take continuous damage if vehicle drives into lava" in { + assert(vehicle.Health > 0) //alive + assert(player1.Health == 100) //alive + vehicle.Position = Vector3(5,5,-3) //right in the pool + vehicle.zoneInteraction() //trigger + + val msg_burn = vehicleProbe.receiveN(3,1 seconds) + msg_burn.foreach { msg => + assert( + msg match { + case VehicleServiceMessage("test-zone", VehicleAction.PlanetsideAttribute(_, PlanetSideGUID(2), 0, _)) => true + case _ => false + } + ) + } + //etc.. + probe.receiveOne(65 seconds) //wait until player1's implants deinitialize + assert(vehicle.Health == 0) //ded + assert(player1.Health == 0) //ded + } + } +} + +class VehicleControlInteractWithDeathTest extends ActorTest { + val vehicle = Vehicle(GlobalDefinitions.fury) //guid=2 + val player1 = + Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //guid=1 + val guid = new NumberPoolHub(new MaxNumberSource(15)) + val pool = Pool(EnvironmentAttribute.Death, DeepSquare(-1, 10, 10, 0, 0)) + val zone = new Zone( + id = "test-zone", + new ZoneMap(name = "test-map") { + environment = List(pool) + }, + zoneNumber = 0 + ) { + override def SetupNumberPools() = {} + GUID(guid) + override def LivePlayers = List(player1) + override def Vehicles = List(vehicle) + override def AvatarEvents = TestProbe().ref + override def VehicleEvents = TestProbe().ref + } + + guid.register(player1, 1) + guid.register(vehicle, 2) + guid.register(player1.avatar.locker, 5) + player1.Zone = zone + player1.Spawn() + vehicle.Zone = zone + vehicle.Faction = PlanetSideEmpire.TR + vehicle.Seats(0).Occupant = player1 + player1.VehicleSeated = vehicle.GUID + val (probe, avatarActor) = PlayerControlTest.DummyAvatar(system) + player1.Actor = system.actorOf(Props(classOf[PlayerControl], player1, avatarActor), "player1-control") + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-control") + + "VehicleControl" should { + "take continuous damage if vehicle drives into a pool of death" in { + assert(vehicle.Health > 0) //alive + assert(player1.Health == 100) //alive + vehicle.Position = Vector3(5,5,-3) //right in the pool + vehicle.zoneInteraction() //trigger + + probe.receiveOne(2 seconds) //wait until player1's implants deinitialize + assert(vehicle.Health == 0) //ded + assert(player1.Health == 0) //ded + } + } +} + +object VehicleControlTest { + import net.psforever.objects.avatar.Avatar + import net.psforever.types.{CharacterGender, PlanetSideEmpire} + + val avatar1 = Avatar(0, "test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) + val avatar2 = Avatar(1, "test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) +} diff --git a/src/test/scala/objects/VehicleTest.scala b/src/test/scala/objects/VehicleTest.scala index ad34959f..cb5a860a 100644 --- a/src/test/scala/objects/VehicleTest.scala +++ b/src/test/scala/objects/VehicleTest.scala @@ -1,29 +1,13 @@ // Copyright (c) 2017 PSForever package objects -import akka.actor.Props -import akka.testkit.TestProbe -import base.{ActorTest, FreedContextActorTest} import net.psforever.objects._ import net.psforever.objects.definition.{SeatDefinition, VehicleDefinition} -import net.psforever.objects.guid.NumberPoolHub -import net.psforever.objects.guid.source.MaxNumberSource -import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vehicles._ -import net.psforever.objects.vital.VehicleShieldCharge -import net.psforever.objects.zones.{Zone, ZoneMap} -import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectDetachMessage, PlanetsideAttributeMessage} import net.psforever.types.{PlanetSideGUID, _} import org.specs2.mutable._ -import net.psforever.services.ServiceManager -import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} - -import scala.concurrent.duration._ -import akka.actor.typed.scaladsl.adapter._ -import net.psforever.actors.zone.ZoneActor class VehicleTest extends Specification { - import VehicleTest._ "SeatDefinition" should { @@ -327,659 +311,7 @@ class VehicleTest extends Specification { } } -class VehicleControlPrepareForDeletionTest extends ActorTest { - val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) - vehicle.Faction = PlanetSideEmpire.TR - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") - val vehicleProbe = new TestProbe(system) - vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { - VehicleEvents = vehicleProbe.ref - } - - vehicle.GUID = PlanetSideGUID(1) - expectNoMessage(200 milliseconds) - - "VehicleControl" should { - "submit for unregistering when marked for deconstruction" in { - vehicle.Actor ! Vehicle.Deconstruct() - vehicleProbe.expectNoMessage(5 seconds) - } - } -} - -class VehicleControlPrepareForDeletionPassengerTest extends ActorTest { - val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) - vehicle.Faction = PlanetSideEmpire.TR - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") - val vehicleProbe = new TestProbe(system) - vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { - VehicleEvents = vehicleProbe.ref - } - val player1 = Player(VehicleTest.avatar1) - - vehicle.GUID = PlanetSideGUID(1) - player1.GUID = PlanetSideGUID(2) - vehicle.Seats(1).Occupant = player1 //passenger seat - player1.VehicleSeated = vehicle.GUID - expectNoMessage(200 milliseconds) - - "VehicleControl" should { - "kick all players when marked for deconstruction" in { - vehicle.Actor ! Vehicle.Deconstruct() - - val vehicle_msg = vehicleProbe.receiveN(1, 500 milliseconds) - assert( - vehicle_msg.head match { - case VehicleServiceMessage( - "test", - VehicleAction.KickPassenger(PlanetSideGUID(2), 4, false, PlanetSideGUID(1)) - ) => - true - case _ => false - } - ) - assert(player1.VehicleSeated.isEmpty) - assert(vehicle.Seats(1).Occupant.isEmpty) - } - } -} - -class VehicleControlPrepareForDeletionMountedInTest extends FreedContextActorTest { - ServiceManager.boot - val guid = new NumberPoolHub(new MaxNumberSource(10)) - val zone = new Zone("test", new ZoneMap("test"), 0) { - GUID(guid) - - override def SetupNumberPools(): Unit = {} - } - zone.actor = system.spawn(ZoneActor(zone), "test-zone-actor") - // crappy workaround but without it the zone doesn't get initialized in time - expectNoMessage(400 milliseconds) - - val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) - vehicle.Faction = PlanetSideEmpire.TR - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test-cargo") - vehicle.Zone = zone - val lodestar = Vehicle(GlobalDefinitions.lodestar) - lodestar.Faction = PlanetSideEmpire.TR - val player1 = Player(VehicleTest.avatar1) //name="test1" - val player2 = Player(VehicleTest.avatar2) //name="test2" - - guid.register(vehicle, 1) - guid.register(lodestar, 2) - player1.GUID = PlanetSideGUID(3) - var utilityId = 10 - lodestar.Utilities.values.foreach { util => - util().GUID = PlanetSideGUID(utilityId) - utilityId += 1 - } - vehicle.Seats(1).Occupant = player1 //passenger seat - player1.VehicleSeated = vehicle.GUID - lodestar.Seats(0).Occupant = player2 - player2.VehicleSeated = lodestar.GUID - lodestar.CargoHolds(1).Occupant = vehicle - vehicle.MountedIn = lodestar.GUID - - val vehicleProbe = new TestProbe(system) - zone.VehicleEvents = vehicleProbe.ref - zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this - expectNoMessage(200 milliseconds) - - "VehicleControl" should { - "if mounted as cargo, self-eject when marked for deconstruction" in { - vehicle.Actor ! Vehicle.Deconstruct() - - val vehicle_msg = vehicleProbe.receiveN(6, 500 milliseconds) - //dismounting as cargo messages - assert( - vehicle_msg.head match { - case VehicleServiceMessage( - _, - VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(1) match { - case VehicleServiceMessage( - _, - VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(2) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse( - _, - CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0) - ) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(3) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(4) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse( - _, - CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0) - ) - ) => - true - case _ => false - } - ) - //dismounting as cargo messages - //TODO: does not actually kick out the cargo, but instigates the process - assert( - vehicle_msg(5) match { - case VehicleServiceMessage( - "test", - VehicleAction.KickPassenger(PlanetSideGUID(3), 4, false, PlanetSideGUID(1)) - ) => - true - case _ => false - } - ) - assert(player1.VehicleSeated.isEmpty) - assert(vehicle.Seats(1).Occupant.isEmpty) - } - } -} - -class VehicleControlPrepareForDeletionMountedCargoTest extends FreedContextActorTest { - val guid = new NumberPoolHub(new MaxNumberSource(10)) - ServiceManager.boot - val zone = new Zone("test", new ZoneMap("test"), 0) { - GUID(guid) - - override def SetupNumberPools(): Unit = {} - } - zone.actor = system.spawn(ZoneActor(zone), "test-zone-actor") - // crappy workaround but without it the zone doesn't get initialized in time - expectNoMessage(200 milliseconds) - - val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) - vehicle.Faction = PlanetSideEmpire.TR - vehicle.Zone = zone - val cargoProbe = new TestProbe(system) - vehicle.Actor = cargoProbe.ref - val lodestar = Vehicle(GlobalDefinitions.lodestar) - lodestar.Faction = PlanetSideEmpire.TR - val player1 = Player(VehicleTest.avatar1) //name="test1" - val player2 = Player(VehicleTest.avatar2) //name="test2" - - guid.register(vehicle, 1) - guid.register(lodestar, 2) - player1.GUID = PlanetSideGUID(3) - player2.GUID = PlanetSideGUID(4) - var utilityId = 10 - lodestar.Utilities.values.foreach { util => - util().GUID = PlanetSideGUID(utilityId) - utilityId += 1 - } - vehicle.Seats(1).Occupant = player1 //passenger seat - player1.VehicleSeated = vehicle.GUID - lodestar.Seats(0).Occupant = player2 - player2.VehicleSeated = lodestar.GUID - lodestar.CargoHolds(1).Occupant = vehicle - vehicle.MountedIn = lodestar.GUID - - val vehicleProbe = new TestProbe(system) - zone.VehicleEvents = vehicleProbe.ref - zone.Transport ! Zone.Vehicle.Spawn(lodestar) //can not fake this - expectNoMessage(200 milliseconds) - - "VehicleControl" should { - "if with mounted cargo, eject it when marked for deconstruction" in { - lodestar.Actor ! Vehicle.Deconstruct() - - val vehicle_msg = vehicleProbe.receiveN(6, 500 milliseconds) - assert( - vehicle_msg.head match { - case VehicleServiceMessage( - "test", - VehicleAction.KickPassenger(PlanetSideGUID(4), 4, false, PlanetSideGUID(2)) - ) => - true - case _ => false - } - ) - assert(player2.VehicleSeated.isEmpty) - assert(lodestar.Seats(0).Occupant.isEmpty) - //cargo dismounting messages - assert( - vehicle_msg(1) match { - case VehicleServiceMessage( - _, - VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 0, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(2) match { - case VehicleServiceMessage( - _, - VehicleAction.SendResponse(_, PlanetsideAttributeMessage(PlanetSideGUID(1), 68, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(3) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse( - _, - CargoMountPointStatusMessage(PlanetSideGUID(2), _, PlanetSideGUID(1), _, 1, CargoStatus.InProgress, 0) - ) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(4) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _, _, _)) - ) => - true - case _ => false - } - ) - assert( - vehicle_msg(5) match { - case VehicleServiceMessage( - "test", - VehicleAction.SendResponse( - _, - CargoMountPointStatusMessage(PlanetSideGUID(2), _, _, PlanetSideGUID(1), 1, CargoStatus.Empty, 0) - ) - ) => - true - case _ => false - } - ) - } - } -} - -class VehicleControlMountingBlockedExosuitTest extends ActorTest { - val probe = new TestProbe(system) - def checkCanNotMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanNotMount]) - case _ => - assert(false) - } - } - - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } - 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) - player1.ExoSuit = ExoSuitType.Reinforced - player1.GUID = PlanetSideGUID(1) - val player2 = Player(VehicleTest.avatar1) - player2.ExoSuit = ExoSuitType.MAX - player2.GUID = PlanetSideGUID(2) - val player3 = Player(VehicleTest.avatar1) - player3.ExoSuit = ExoSuitType.Agile - player3.GUID = PlanetSideGUID(3) - - "Vehicle Control" should { - "block players from sitting if their exo-suit is not allowed by the seat" in { - //disallow - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) //Reinforced in non-MAX seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) //MAX in non-Reinforced seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player2, 1), probe.ref) //MAX in non-MAX seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player1, 9), probe.ref) //Reinforced in MAX-only seat - checkCanNotMount() - vehicle.Actor.tell(Mountable.TryMount(player3, 9), probe.ref) //Agile in MAX-only seat - checkCanNotMount() - - //allow - vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) - checkCanMount() - vehicle.Actor.tell(Mountable.TryMount(player2, 9), probe.ref) - checkCanMount() - vehicle.Actor.tell(Mountable.TryMount(player3, 0), probe.ref) - checkCanMount() - } - } -} - -class VehicleControlMountingBlockedSeatPermissionTest extends ActorTest { - val probe = new TestProbe(system) - def checkCanNotMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanNotMount]) - case _ => - assert(false) - } - } - - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } - 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) - player1.GUID = PlanetSideGUID(1) - val player2 = Player(VehicleTest.avatar1) - player2.GUID = PlanetSideGUID(2) - - "Vehicle Control" should { - //11 June 2018: Group is not supported yet so do not bother testing it - "block players from sitting if the seat does not allow it" in { - - vehicle.PermissionGroup(2, 3) //passenger group -> empire - vehicle.Actor.tell(Mountable.TryMount(player1, 3), probe.ref) //passenger seat - checkCanMount() - vehicle.PermissionGroup(2, 0) //passenger group -> locked - vehicle.Actor.tell(Mountable.TryMount(player2, 4), probe.ref) //passenger seat - checkCanNotMount() - } - } -} - -class VehicleControlMountingDriverSeatTest extends ActorTest { - val probe = new TestProbe(system) - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } - 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) - player1.GUID = PlanetSideGUID(1) - - "Vehicle Control" should { - "allow players to sit in the driver seat, even if it is locked, if the vehicle is unowned" in { - assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked - assert(vehicle.Seats(0).Occupant.isEmpty) - assert(vehicle.Owner.isEmpty) - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) - } - } -} - -class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest { - val probe = new TestProbe(system) - def checkCanNotMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanNotMount]) - case _ => - assert(false) - } - } - - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } - 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) - player1.GUID = PlanetSideGUID(1) - val player2 = Player(VehicleTest.avatar1) - player2.GUID = PlanetSideGUID(2) - - "Vehicle Control" should { - "block players that are not the current owner from sitting in the driver seat (locked)" in { - assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked - assert(vehicle.Seats(0).Occupant.isEmpty) - vehicle.Owner = player1.GUID - - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) - vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) - probe.receiveOne(Duration.create(100, "ms")) //discard - assert(vehicle.Seats(0).Occupant.isEmpty) - - vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) - checkCanNotMount() - assert(vehicle.Seats(0).Occupant.isEmpty) - } - } -} - -class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest { - val probe = new TestProbe(system) - def checkCanMount(): Unit = { - val reply = probe.receiveOne(Duration.create(100, "ms")) - reply match { - case msg: Mountable.MountMessages => - assert(msg.response.isInstanceOf[Mountable.CanMount]) - case _ => - assert(false) - } - } - 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) - player1.GUID = PlanetSideGUID(1) - val player2 = Player(VehicleTest.avatar1) - player2.GUID = PlanetSideGUID(2) - - "Vehicle Control" should { - "allow players that are not the current owner to sit in the driver seat (empire)" in { - vehicle.PermissionGroup(0, 3) //passenger group -> empire - assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Empire)) //driver group -> empire - assert(vehicle.Seats(0).Occupant.isEmpty) - vehicle.Owner = player1.GUID //owner set - - vehicle.Actor.tell(Mountable.TryMount(player1, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) - vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) - probe.receiveOne(Duration.create(100, "ms")) //discard - assert(vehicle.Seats(0).Occupant.isEmpty) - - vehicle.Actor.tell(Mountable.TryMount(player2, 0), probe.ref) - checkCanMount() - assert(vehicle.Seats(0).Occupant.nonEmpty) - } - } -} - -class VehicleControlShieldsChargingTest extends ActorTest { - val probe = new TestProbe(system) - val vehicle = Vehicle(GlobalDefinitions.fury) - vehicle.GUID = PlanetSideGUID(10) - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") - vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { - VehicleEvents = probe.ref - } - - "charge vehicle shields" in { - assert(vehicle.Shields == 0) - assert(!vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) - - vehicle.Actor ! Vehicle.ChargeShields(15) - val msg = probe.receiveOne(500 milliseconds) - assert(msg match { - case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(_, PlanetSideGUID(10), 68, 15)) => true - case _ => false - }) - assert(vehicle.Shields == 15) - assert(vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) - } -} - -class VehicleControlShieldsNotChargingVehicleDeadTest extends ActorTest { - val probe = new TestProbe(system) - val vehicle = Vehicle(GlobalDefinitions.fury) - vehicle.GUID = PlanetSideGUID(10) - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") - vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { - VehicleEvents = probe.ref - } - - "not charge vehicle shields if the vehicle is destroyed" in { - assert(vehicle.Health > 0) - vehicle.Health = 0 - assert(vehicle.Health == 0) - assert(vehicle.Shields == 0) - assert(!vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) - vehicle.Actor.tell(Vehicle.ChargeShields(15), probe.ref) - - probe.expectNoMessage(1 seconds) - assert(vehicle.Shields == 0) - assert(!vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) - } -} - -class VehicleControlShieldsNotChargingVehicleShieldsFullTest extends ActorTest { - val probe = new TestProbe(system) - val vehicle = Vehicle(GlobalDefinitions.fury) - vehicle.GUID = PlanetSideGUID(10) - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") - vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { - VehicleEvents = probe.ref - } - - "not charge vehicle shields if the vehicle is destroyed" in { - assert(vehicle.Shields == 0) - vehicle.Shields = vehicle.MaxShields - assert(vehicle.Shields == vehicle.MaxShields) - assert(!vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) - vehicle.Actor ! Vehicle.ChargeShields(15) - - probe.expectNoMessage(1 seconds) - assert(!vehicle.History.exists({ p => p.isInstanceOf[VehicleShieldCharge] })) - } -} - -class VehicleControlShieldsNotChargingTooEarlyTest extends ActorTest { - val probe = new TestProbe(system) - val vehicle = Vehicle(GlobalDefinitions.fury) - vehicle.GUID = PlanetSideGUID(10) - vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") - vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { - VehicleEvents = probe.ref - } - - "charge vehicle shields" in { - assert(vehicle.Shields == 0) - - vehicle.Actor ! Vehicle.ChargeShields(15) - val msg = probe.receiveOne(200 milliseconds) - //assert(msg.isInstanceOf[Vehicle.UpdateShieldsCharge]) - assert(msg match { - case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(_, PlanetSideGUID(10), 68, 15)) => true - case _ => false - }) - assert(vehicle.Shields == 15) - - vehicle.Actor ! Vehicle.ChargeShields(15) - probe.expectNoMessage(200 milliseconds) - assert(vehicle.Shields == 15) - } -} - -//TODO implement message protocol for zone startup completion -//class VehicleControlShieldsNotChargingDamagedTest extends ActorTest { -// val probe = new TestProbe(system) -// val vehicle = Vehicle(GlobalDefinitions.fury) -// vehicle.GUID = PlanetSideGUID(10) -// vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") -// vehicle.Zone = new Zone("test", new ZoneMap("test"), 0) { -// VehicleEvents = probe.ref -// } -// // -// val beamer_wep = Tool(GlobalDefinitions.beamer) -// val p_source = PlayerSource( Player(Avatar(0, "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 = DamageInteraction(p_source, ProjectileReason(DamageResolution.Hit, projectile, fury_dm), Vector3(1.2f, 3.4f, 5.6f)) -// -// "not charge vehicle shields if recently damaged" in { -// assert(vehicle.Shields == 0) -// vehicle.Actor.tell(Vitality.Damage({case v : Vehicle => v.History(obj); obj }), probe.ref) -// -// val msg = probe.receiveOne(200 milliseconds) -// assert(msg.isInstanceOf[Vitality.DamageResolution]) -// assert(vehicle.Shields == 0) -// vehicle.Actor.tell(Vehicle.ChargeShields(15), probe.ref) -// -// probe.expectNoMessage(200 milliseconds) -// assert(vehicle.Shields == 0) -// } -//} - object VehicleTest { - import net.psforever.objects.avatar.Avatar import net.psforever.types.{CharacterGender, PlanetSideEmpire} diff --git a/src/test/scala/objects/VitalityTest.scala b/src/test/scala/objects/VitalityTest.scala index d097232c..e8aab79b 100644 --- a/src/test/scala/objects/VitalityTest.scala +++ b/src/test/scala/objects/VitalityTest.scala @@ -42,7 +42,7 @@ class VitalityTest extends Specification { player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard)) player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal)) player.History(VehicleShieldCharge(vSource, 10)) - player.History(PlayerSuicide(pSource)) + player.History(PlayerSuicide()) ok } @@ -56,7 +56,7 @@ class VitalityTest extends Specification { player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard)) player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal)) player.History(VehicleShieldCharge(vSource, 10)) - player.History(PlayerSuicide(pSource)) + player.History(PlayerSuicide()) player.History.size mustEqual 7 val list = player.ClearHistory() @@ -92,7 +92,7 @@ class VitalityTest extends Specification { player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard)) player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal)) player.History(VehicleShieldCharge(vSource, 10)) - player.History(PlayerSuicide(pSource)) + player.History(PlayerSuicide()) player.LastShot match { case Some(resolved_projectile) =>