diff --git a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala index 73d98304..7fcda14b 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala @@ -3,6 +3,7 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions import net.psforever.objects.vital.InGameHistory import scala.concurrent.duration._ @@ -61,6 +62,7 @@ class SessionMountHandlers( sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=45, obj.NtuCapacitorScaled)) sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sessionData.accessContainer(obj) + tplayer.Actor ! ResetAllEnvironmentInteractions MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) @@ -76,6 +78,7 @@ class SessionMountHandlers( obj.Cloaked = tplayer.Cloaked sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sessionData.accessContainer(obj) + tplayer.Actor ! ResetAllEnvironmentInteractions MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) @@ -90,6 +93,7 @@ class SessionMountHandlers( sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sessionData.accessContainer(obj) sessionData.updateWeaponAtSeatPosition(obj, seatNumber) + tplayer.Actor ! ResetAllEnvironmentInteractions MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) @@ -103,6 +107,7 @@ class SessionMountHandlers( sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sessionData.accessContainer(obj) sessionData.updateWeaponAtSeatPosition(obj, seatNumber) + tplayer.Actor ! ResetAllEnvironmentInteractions MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) @@ -122,6 +127,7 @@ class SessionMountHandlers( sessionData.accessContainer(obj) sessionData.updateWeaponAtSeatPosition(obj, seatNumber) sessionData.keepAliveFunc = sessionData.keepAlivePersistence + tplayer.Actor ! ResetAllEnvironmentInteractions MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) => @@ -139,6 +145,7 @@ class SessionMountHandlers( sessionData.accessContainer(obj) sessionData.updateWeaponAtSeatPosition(obj, seatNumber) sessionData.keepAliveFunc = sessionData.keepAlivePersistence + tplayer.Actor ! ResetAllEnvironmentInteractions MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) @@ -173,7 +180,7 @@ class SessionMountHandlers( MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Mountable, _, _) => - log.warn(s"MountVehicleMsg: $obj is some mountable object and nothing will happen for ${player.Name}") + log.warn(s"MountVehicleMsg: $obj is some kind of mountable object but nothing will happen for ${player.Name}") case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) => log.info(s"${tplayer.Name} dismounts the implant terminal") diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 52b65fcc..6f3383af 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -1,16 +1,17 @@ // Copyright (c) 2017 PSForever package net.psforever.objects +import net.psforever.objects.avatar.interaction.{WithGantry, WithLava, WithWater} import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry} import net.psforever.objects.ballistics.InteractWithRadiationClouds import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets} import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition} import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} -import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.{PlanetSideServerObject, environment} import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.aura.AuraContainer -import net.psforever.objects.serverobject.environment.InteractWithEnvironment +import net.psforever.objects.serverobject.environment.interaction.common.{WithDeath, WithMovementTrigger} import net.psforever.objects.serverobject.mount.MountableEntity import net.psforever.objects.vital.resistance.ResistanceProfile import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFromEquipment, Vitality} @@ -19,7 +20,7 @@ import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation} import net.psforever.objects.zones.{InteractsWithZone, ZoneAware, Zoning} -import net.psforever.types.{PlanetSideGUID, _} +import net.psforever.types._ import scala.annotation.tailrec import scala.util.{Success, Try} @@ -36,7 +37,13 @@ class Player(var avatar: Avatar) with ZoneAware with AuraContainer with MountableEntity { - interaction(new InteractWithEnvironment()) + interaction(environment.interaction.InteractWithEnvironment(Seq( + new WithWater(avatar.name), + new WithLava(), + new WithDeath(), + new WithGantry(avatar.name), + new WithMovementTrigger() + ))) interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10)) interaction(new InteractWithTurrets()) interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala index d6bbd19d..5bd31056 100644 --- a/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/src/main/scala/net/psforever/objects/Vehicle.scala @@ -6,14 +6,15 @@ import net.psforever.objects.definition.{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.{MountableEntity, Seat, SeatDefinition} -import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.{PlanetSideServerObject, environment} 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.InteractWithEnvironment +import net.psforever.objects.serverobject.environment.interaction.common.{WithDeath, WithMovementTrigger} import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.structures.AmenityOwner import net.psforever.objects.vehicles._ +import net.psforever.objects.vehicles.interaction.{WithLava, WithWater} import net.psforever.objects.vital.resistance.StandardResistanceProfile import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.resolution.DamageResistanceModel @@ -76,21 +77,26 @@ import scala.util.{Success, Try} */ class Vehicle(private val vehicleDef: VehicleDefinition) extends AmenityOwner - with BlockMapEntity - with MountableWeapons - with InteractsWithZone - with Hackable - with FactionAffinity - with Deployment - with Vitality - with OwnableByPlayer - with StandardResistanceProfile - with JammableUnit - with CommonNtuContainer - with Container - with AuraContainer - with MountableEntity { - interaction(new InteractWithEnvironment()) + with BlockMapEntity + with MountableWeapons + with InteractsWithZone + with Hackable + with FactionAffinity + with Deployment + with Vitality + with OwnableByPlayer + with StandardResistanceProfile + with JammableUnit + with CommonNtuContainer + with Container + with AuraContainer + with MountableEntity { + interaction(environment.interaction.InteractWithEnvironment(Seq( + new WithWater(), + new WithLava(), + new WithDeath(), + new WithMovementTrigger() + ))) interaction(new InteractWithMines(range = 20)) interaction(new InteractWithTurrets()) interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20)) diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 002475f4..c2ee863c 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -30,15 +30,12 @@ import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.objects.locker.LockerContainerControl -import net.psforever.objects.serverobject.environment._ +import net.psforever.objects.serverobject.environment.interaction.RespondsToZoneEnvironment import net.psforever.objects.serverobject.repair.Repairable -import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad import net.psforever.objects.sourcing.{AmenitySource, PlayerSource} import net.psforever.objects.vital.collision.CollisionReason -import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} import net.psforever.objects.vital.interaction.{Adversarial, DamageInteraction, DamageResult} -import net.psforever.services.hart.ShuttleState import net.psforever.packet.PlanetSideGamePacket import scala.concurrent.duration._ @@ -66,12 +63,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm ApplicableEffect(Aura.Fire) def InteractiveObject: Player = player - SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) - SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) - SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath) - SetInteraction(EnvironmentAttribute.GantryDenialField, doInteractingWithGantryField) - SetInteraction(EnvironmentAttribute.MovementFieldTrigger, doInteractingWithMovementTrigger) - 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? */ @@ -93,6 +85,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm player.avatar.locker.Actor = Default.Actor EndAllEffects() EndAllAggravation() + respondToEnvironmentPostStop() } def receive: Receive = @@ -1215,159 +1208,6 @@ 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, InteractingWithEnvironment(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 = { - player.History.findLast { entry => entry.isInstanceOf[ReconstructionActivity] } match { - case Some(entry) if System.currentTimeMillis() - entry.time > 4000L => suicide() - case _ => - } - } - - //noinspection ScalaUnusedSymbol - def doInteractingWithGantryField( - obj: PlanetSideServerObject, - body: PieceOfEnvironment, - data: Option[OxygenStateTarget] - ): Unit = { - import scala.concurrent.ExecutionContext.Implicits.global - val field = body.asInstanceOf[GantryDenialField] - val zone = player.Zone - (zone.GUID(field.obbasemesh) match { - case Some(pad : OrbitalShuttlePad) => zone.GUID(pad.shuttle) - case _ => None - }) match { - case Some(shuttle: Vehicle) - if shuttle.Flying.contains(ShuttleState.State11.id) || shuttle.Faction != player.Faction => - val (pos, zang) = Vehicles.dismountShuttle(shuttle, field.mountPoint) - shuttle.Zone.AvatarEvents ! AvatarServiceMessage( - player.Name, - AvatarAction.SendResponse( - Service.defaultPlayerGUID, - PlayerStateShiftMessage(ShiftState(0, pos, zang, None))) - ) - case Some(_: Vehicle) => - interactionTimer = context.system.scheduler.scheduleOnce( - delay = 250 milliseconds, - self, - InteractingWithEnvironment(player, body, None) - ) - case _ => ; - //something configured incorrectly; no need to keep checking - } - } - - /** - * The player will be affected by this action. - * @param obj the target - * @param body the environment - * @param data additional interaction information, if applicable - */ - def doInteractingWithMovementTrigger( - obj: PlanetSideServerObject, - body: PieceOfEnvironment, - data: Option[OxygenStateTarget] - ): Unit = { - body.asInstanceOf[GeneralMovementField].triggerAction(obj) - } - - /** - * 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/avatar/interaction/WithGantry.scala b/src/main/scala/net/psforever/objects/avatar/interaction/WithGantry.scala new file mode 100644 index 00000000..5f637e00 --- /dev/null +++ b/src/main/scala/net/psforever/objects/avatar/interaction/WithGantry.scala @@ -0,0 +1,51 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects.avatar.interaction + +import net.psforever.objects.serverobject.environment.interaction.{InteractionWith, RespondsToZoneEnvironment} +import net.psforever.objects.{Vehicle, Vehicles} +import net.psforever.objects.serverobject.environment.{EnvironmentAttribute, EnvironmentTrait, GantryDenialField, PieceOfEnvironment, interaction} +import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad +import net.psforever.objects.zones.InteractsWithZone +import net.psforever.packet.game.{PlayerStateShiftMessage, ShiftState} +import net.psforever.services.Service +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.hart.ShuttleState + +import scala.annotation.unused +import scala.concurrent.duration._ + +class WithGantry(val channel: String) + extends InteractionWith { + val attribute: EnvironmentTrait = EnvironmentAttribute.GantryDenialField + + def doInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + @unused data: Option[Any] + ): Unit = { + val field = body.asInstanceOf[GantryDenialField] + val zone = obj.Zone + (zone.GUID(field.obbasemesh) match { + case Some(pad : OrbitalShuttlePad) => zone.GUID(pad.shuttle) + case _ => None + }) match { + case Some(shuttle: Vehicle) + if shuttle.Flying.contains(ShuttleState.State11.id) || shuttle.Faction != obj.Faction => + val (pos, ang) = Vehicles.dismountShuttle(shuttle, field.mountPoint) + shuttle.Zone.AvatarEvents ! AvatarServiceMessage( + channel, + AvatarAction.SendResponse( + Service.defaultPlayerGUID, + PlayerStateShiftMessage(ShiftState(0, pos, ang, None))) + ) + case Some(_: Vehicle) => + obj.Actor ! RespondsToZoneEnvironment.Timer( + attribute, + delay = 250 milliseconds, + obj.Actor, + interaction.InteractingWithEnvironment(body, None) + ) + case _ => () //something configured incorrectly; no need to keep checking + } + } +} diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/WithLava.scala b/src/main/scala/net/psforever/objects/avatar/interaction/WithLava.scala new file mode 100644 index 00000000..f2f5b617 --- /dev/null +++ b/src/main/scala/net/psforever/objects/avatar/interaction/WithLava.scala @@ -0,0 +1,50 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects.avatar.interaction + +import net.psforever.objects.serverobject.aura.{Aura, AuraEffectBehavior} +import net.psforever.objects.serverobject.environment.interaction.{InteractionWith, RespondsToZoneEnvironment} +import net.psforever.objects.serverobject.environment.{EnvironmentAttribute, EnvironmentTrait, PieceOfEnvironment, interaction} +import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.environment.EnvironmentReason +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.zones.InteractsWithZone + +import scala.concurrent.duration._ + +class WithLava() + extends InteractionWith { + val attribute: EnvironmentTrait = EnvironmentAttribute.Lava + + private var stopBurn: Boolean = false + + /** + * Lava causes players to take (considerable) damage until they inevitably die. + * @param obj the target + * @param body the environment + */ + def doInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + if (stopBurn && data.nonEmpty) { + stopBurn = false + } else if (!obj.Destroyed) { + obj.Actor ! Vitality.Damage( + DamageInteraction( + SourceEntry(obj), + EnvironmentReason(body, obj), + obj.Position + ).calculate() + ) + obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Fire, duration = 1250L) //burn + //keep doing damage + obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = 250 milliseconds, obj.Actor, interaction.InteractingWithEnvironment(body, Some("burning"))) + } + } + + override def stopInteractingWith(obj: InteractsWithZone, body: PieceOfEnvironment, parentInfo: Option[Any]): Unit = { + stopBurn = true + } +} diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/WithWater.scala b/src/main/scala/net/psforever/objects/avatar/interaction/WithWater.scala new file mode 100644 index 00000000..ca17db45 --- /dev/null +++ b/src/main/scala/net/psforever/objects/avatar/interaction/WithWater.scala @@ -0,0 +1,89 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects.avatar.interaction + +import net.psforever.objects.Player +import net.psforever.objects.serverobject.environment.interaction.{InteractionWith, RespondsToZoneEnvironment} +import net.psforever.objects.serverobject.environment.interaction.common.Watery +import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget +import net.psforever.objects.serverobject.environment.{PieceOfEnvironment, interaction} +import net.psforever.objects.zones.InteractsWithZone +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.types.OxygenState + +import scala.concurrent.duration._ + +class WithWater(val channel: String) + extends InteractionWith + with Watery { + /** + * Water causes players to slowly suffocate. + * When they (finally) drown, they will die. + * @param obj the target + * @param body the environment + */ + def doInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val extra = data.collect { + case t: OxygenStateTarget => Some(t) + case w: Watery => w.Condition + }.flatten + if (extra.isDefined) { + //inform the player that their mounted vehicle is in trouble (that they are in trouble (but not from drowning (yet))) + stopInteractingWith(obj, body, data) + } else { + val (effect, time, percentage) = Watery.drowningInWateryConditions(obj, condition.map(_.state), waterInteractionTime) + if (effect) { + val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage) + waterInteractionTime = System.currentTimeMillis() + time + condition = Some(cond) + obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, Player.Die()) + //inform the player that they are in trouble + obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra)) + } + } + } + + /** + * 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 + */ + override def stopInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) + val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage) + val extra = data.collect { + case t: OxygenStateTarget => Some(t) + case w: Watery => w.Condition + }.flatten + if (percentage > 99f) { + recoverFromInteracting(obj) + } + if (effect) { + condition = Some(cond) + waterInteractionTime = System.currentTimeMillis() + time + obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute)) + //inform the player + obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra)) + } else if (extra.isDefined) { + //inform the player + obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra)) + } + } + + override def recoverFromInteracting(obj: InteractsWithZone): Unit = { + super.recoverFromInteracting(obj) + if (condition.exists(_.state == OxygenState.Suffocation)) { + stopInteractingWith(obj, condition.map(_.body).get, None) + } + waterInteractionTime = 0L + condition = None + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala deleted file mode 100644 index ae024daa..00000000 --- a/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2021 PSForever -package net.psforever.objects.serverobject.environment - -import net.psforever.objects.GlobalDefinitions -import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.zones._ -import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation} - -case object EnvironmentInteraction extends ZoneInteractionType - -/** - * This game entity may infrequently test whether it may interact with game world environment. - */ -class InteractWithEnvironment() - extends ZoneInteraction { - private var interactingWithEnvironment: (PlanetSideServerObject, Boolean) => Any = - InteractWithEnvironment.onStableEnvironment() - - def Type = EnvironmentInteraction - - def range: Float = 0f - - /** - * 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 `InteractsWithEnvironment.blockedFromInteracting` - * @see `InteractsWithEnvironment.onStableEnvironment` - * @see `InteractsWithEnvironment.awaitOngoingInteraction` - * @param sector the portion of the block map being tested - * @param target the fixed element in this test - */ - def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { - interactingWithEnvironment = interactingWithEnvironment(target, true) - .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 `InteractsWithEnvironment.onStableEnvironment` - */ - def resetInteraction(target: InteractsWithZone) : Unit = { - interactingWithEnvironment(target, false) - interactingWithEnvironment = InteractWithEnvironment.onStableEnvironment() - } -} - -object InteractWithEnvironment { - /** - * 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 ! InteractingWithEnvironment(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 ! InteractingWithEnvironment(obj, newBody, None) - awaitOngoingInteraction(obj.Zone, newBody)(_, _) - case Some(newBody) => - obj.Actor ! EscapeFromEnvironment(obj, body, None) - obj.Actor ! InteractingWithEnvironment(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 match { - case bme: BlockMapEntity => - obj.Zone.blockMap.sector(bme).environmentList - case _ => - 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/InteractingWithEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/InteractingWithEnvironment.scala deleted file mode 100644 index d514624c..00000000 --- a/src/main/scala/net/psforever/objects/serverobject/environment/InteractingWithEnvironment.scala +++ /dev/null @@ -1,49 +0,0 @@ -// 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 InteractingWithEnvironment( - 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/PieceOfEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala index 01e271a1..c6e76251 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/PieceOfEnvironment.scala @@ -67,7 +67,11 @@ object EnvironmentAttribute extends Enum[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 + obj.Definition.DrownAtMaxDepth || obj.Definition.DisableAtMaxDepth || (obj match { + case p: Player => p.VehicleSeated.isEmpty + case v: Vehicle => v.MountedIn.isEmpty + case _ => true + }) } } diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/RespondsToZoneEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/RespondsToZoneEnvironment.scala deleted file mode 100644 index f243c317..00000000 --- a/src/main/scala/net/psforever/objects/serverobject/environment/RespondsToZoneEnvironment.scala +++ /dev/null @@ -1,180 +0,0 @@ -// 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.objects.zones.InteractsWithZone -import net.psforever.types.OxygenState - -import scala.collection.mutable - -/** - * The mixin code for any server object that responds to environmental representations in the game world. - * Specific types of environmental region is bound by geometry, - * designated by attributes, - * and targets react when coming into contact with it. - * 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 `InteractsWithEnvironment` - * @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 InteractsWithZone - - val environmentBehavior: Receive = { - case InteractingWithEnvironment(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/environment/interaction/InteractWithEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractWithEnvironment.scala new file mode 100644 index 00000000..8c7c749e --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractWithEnvironment.scala @@ -0,0 +1,272 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject.environment.interaction + +import net.psforever.objects.GlobalDefinitions +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment} +import net.psforever.objects.zones._ +import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup, SectorPopulation} + +import scala.collection.mutable + +case object EnvironmentInteraction extends ZoneInteractionType + +/** + * This game entity may infrequently test whether it may interact with game world environment. + */ +class InteractWithEnvironment() + extends ZoneInteraction { + private var interactionBehavior: InteractionBehavior = OnStableEnvironment() + + def Type: EnvironmentInteraction.type = EnvironmentInteraction + + def range: Float = 0f + + /** the environment that we are currently in interaction with */ + private[environment] var interactWith: Set[PieceOfEnvironment] = Set[PieceOfEnvironment]() + + /** + * 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 `AwaitOngoingInteraction` + * @see `BlockedFromInteracting` + * @see `OnStableEnvironment` + * @param sector the portion of the block map being tested + * @param target the fixed element in this test + */ + def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { + target match { + case t: InteractsWithZone => + interactWith = interactionBehavior.perform(t, sector, interactWith, allow=true) + interactionBehavior = interactionBehavior.next + case _ => () + } + } + + /** + * 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 `AwaitOngoingInteraction` + * @see `OnStableEnvironment` + */ + def resetInteraction(target: InteractsWithZone) : Unit = { + target match { + case t: InteractsWithZone with BlockMapEntity => + AwaitOngoingInteraction(target.Zone).perform(t, SectorGroup.emptySector, interactWith, allow=false) + case _ => () + } + interactWith = Set() + interactionBehavior = OnStableEnvironment() + } + + private val interactWithEnvironment: mutable.HashMap[EnvironmentTrait, InteractionWith] = + mutable.HashMap[EnvironmentTrait, InteractionWith]() + + def Interactions: Map[EnvironmentTrait, InteractionWith] = interactWithEnvironment.toMap + + def SetInteraction(attribute: EnvironmentTrait, action: InteractionWith): Unit = { + interactWithEnvironment += attribute -> action + } + + def SetInteractionStop(attribute: EnvironmentTrait, action: InteractionWith): Unit = { + interactWithEnvironment += attribute -> action + } + + def doEnvironmentInteracting(obj: InteractsWithZone, body: PieceOfEnvironment): Unit = { + val attribute = body.attribute + if (interactWith.isEmpty || interactWith.exists(_.attribute == attribute)) { + interactWithEnvironment + .get(attribute) + .foreach(_.doInteractingWith(obj, body, None)) + } + } + + def stopEnvironmentInteracting(obj: InteractsWithZone, body: PieceOfEnvironment): Unit = { + val attribute = body.attribute + if (interactWith.exists(_.attribute == attribute)) { + interactWithEnvironment + .get(attribute) + .foreach(_.stopInteractingWith(obj, body, None)) + } + } +} + +object InteractWithEnvironment { + def apply(list: Iterable[InteractionWith]): InteractWithEnvironment = { + val obj = new InteractWithEnvironment() + list.foreach(env => obj.SetInteraction(env.attribute, env)) + obj + } + + /** + * Test whether any special terrain component has an affect upon the target entity. + * @param obj the target entity + * @param sector the portion of the block map being tested + * @return any unstable, interactive, or special terrain that is being interacted + */ + def checkAllEnvironmentInteractions( + obj: PlanetSideServerObject, + sector: SectorPopulation + ): Set[PieceOfEnvironment] = { + val position = obj.Position + val depth = GlobalDefinitions.MaxDepth(obj) + sector.environmentList + .filter(body => body.attribute.canInteractWith(obj) && body.testInteraction(position, depth)) + .distinctBy(_.attribute) + .toSet + } + + /** + * 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 + */ + 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 + } + } +} + +trait InteractionBehavior { + protected var nextstep: InteractionBehavior = this + + def perform( + obj: InteractsWithZone, + sector: SectorPopulation, + existing: Set[PieceOfEnvironment], + allow: Boolean + ): Set[PieceOfEnvironment] + + def next: InteractionBehavior = { + val out = nextstep + nextstep = this + out + } +} + +case class OnStableEnvironment() extends InteractionBehavior { + /** + * 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 `InteractWithEnvironment.checkAllEnvironmentInteractions` + * @see `AwaitOngoingInteraction` + * @see `OnStableEnvironment` + * @param obj target entity + * @param sector the portion of the block map being tested + * @param existing not applicable + * @param allow is this permitted, or will it be blocked? + * @return applicable interactive environmental fields + */ + def perform( + obj: InteractsWithZone, + sector: SectorPopulation, + existing: Set[PieceOfEnvironment], + allow: Boolean + ): Set[PieceOfEnvironment] = { + if (allow) { + val interactions = obj.interaction().collectFirst { case inter: InteractWithEnvironment => inter.Interactions } + val env = InteractWithEnvironment.checkAllEnvironmentInteractions(obj, sector) + env.foreach(body => interactions.flatMap(_.get(body.attribute)).foreach(_.doInteractingWith(obj, body, None))) + if (env.nonEmpty) { + nextstep = AwaitOngoingInteraction(obj.Zone) + } + env + } else { + nextstep = BlockedFromInteracting() + Set() + } + } +} + +final case class AwaitOngoingInteraction(zone: Zone) extends InteractionBehavior { + /** + * 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 `InteractWithEnvironment.checkAllEnvironmentInteractions` + * @see `InteractWithEnvironment.checkSpecificEnvironmentInteraction` + * @see `OnStableEnvironment` + * @param obj target entity + * @param sector the portion of the block map being tested + * @param existing environment fields from the previous step + * @param allow is this permitted, or will it be blocked? + * @return applicable interactive environmental fields + */ + def perform( + obj: InteractsWithZone, + sector: SectorPopulation, + existing: Set[PieceOfEnvironment], + allow: Boolean + ): Set[PieceOfEnvironment] = { + val interactions = obj.interaction().collectFirst { case inter: InteractWithEnvironment => inter.Interactions } + if (allow) { + val env = InteractWithEnvironment.checkAllEnvironmentInteractions(obj, sector) + val (in, out) = existing.partition(body => InteractWithEnvironment.checkSpecificEnvironmentInteraction(zone, body, obj).nonEmpty) + env.diff(in).foreach(body => interactions.flatMap(_.get(body.attribute)).foreach(_.doInteractingWith(obj, body, None))) + out.foreach(body => interactions.flatMap(_.get(body.attribute)).foreach(_.stopInteractingWith(obj, body, None))) + if (env.isEmpty) { + val n = OnStableEnvironment() + val out = n.perform(obj, sector, Set(), allow) + nextstep = n.next + out + } else { + env + } + } else { + existing.foreach(body => interactions.flatMap(_.get(body.attribute)).foreach(_.stopInteractingWith(obj, body, None))) + nextstep = BlockedFromInteracting() + Set() + } + } +} + +case class BlockedFromInteracting() extends InteractionBehavior { + /** + * 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 target entity + * @param sector the portion of the block map being tested + * @param existing not applicable + * @param allow is this permitted, or will it be blocked? + * @return an empty set + */ + def perform( + obj: InteractsWithZone, + sector: SectorPopulation, + existing: Set[PieceOfEnvironment], + allow: Boolean + ): Set[PieceOfEnvironment] = { + if (allow) { + nextstep = OnStableEnvironment() + } + Set() + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractingWithEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractingWithEnvironment.scala new file mode 100644 index 00000000..25d49d90 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractingWithEnvironment.scala @@ -0,0 +1,36 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.environment.interaction + +import net.psforever.objects.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment} + +/** + * The target has clipped into a critical region of a piece of environment. + * @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 InteractingWithEnvironment( + environment: PieceOfEnvironment, + mountedVehicle: Option[Any] + ) + +/** + * The target has ceased to clip into a critical region of a piece of environment. + * @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( + environment: PieceOfEnvironment, + mountedVehicle: Option[Any] + ) + +/** + * Completely reset internal actions or processes related to environment clipping. + */ +final case class RecoveredFromEnvironmentInteraction(attribute: EnvironmentTrait) + +/** + * Completely reset internal actions or processes related to environment clipping. + */ +case object ResetAllEnvironmentInteractions diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractionWith.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractionWith.scala new file mode 100644 index 00000000..c0991fc2 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractionWith.scala @@ -0,0 +1,25 @@ +package net.psforever.objects.serverobject.environment.interaction + +import net.psforever.objects.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment} +import net.psforever.objects.zones.InteractsWithZone + +trait InteractionWith { + def attribute: EnvironmentTrait + + //noinspection ScalaUnusedSymbol + def doInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + parentInfo: Option[Any] + ): Unit + + //noinspection ScalaUnusedSymbol + def stopInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + parentInfo: Option[Any] + ): Unit = { /*mainly for overriding*/ } + + //noinspection ScalaUnusedSymbol + def recoverFromInteracting(obj: InteractsWithZone): Unit = { /*mainly for overriding*/ } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/RespondsToZoneEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/RespondsToZoneEnvironment.scala new file mode 100644 index 00000000..30ed65b6 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/RespondsToZoneEnvironment.scala @@ -0,0 +1,78 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.environment.interaction + +import akka.actor.{Actor, ActorRef, Cancellable} +import net.psforever.objects.Default +import net.psforever.objects.serverobject.environment.EnvironmentTrait +import net.psforever.objects.zones.InteractsWithZone + +import scala.collection.mutable +import scala.concurrent.duration.FiniteDuration + +/** + * The mixin code for any server entity that responds to environmental representations in the game world. + * Specific types of environmental region is bound by geometry, + * designated by attributes, + * and targets react when maneuvering contact with it. + * @see `EnvironmentTrait` + * @see `EscapeFromEnvironment` + * @see `InteractingWithEnvironment` + * @see `InteractionWith` + * @see `InteractsWithEnvironment` + * @see `PieceOfEnvironment` + * @see `RecoveredFromEnvironmentInteraction` + */ +trait RespondsToZoneEnvironment { + _: Actor => + /** a gesture of automation added to the interaction */ + private val interactionTimers: mutable.HashMap[EnvironmentTrait, Cancellable] = mutable.HashMap[EnvironmentTrait, Cancellable]() + + def InteractiveObject: InteractsWithZone + + private lazy val applicableInteractions: Map[EnvironmentTrait, InteractionWith] = InteractiveObject + .interaction() + .collectFirst { case inter: InteractWithEnvironment => inter.Interactions } + .getOrElse(RespondsToZoneEnvironment.defaultInteractions) + + val environmentBehavior: Receive = { + case RespondsToZoneEnvironment.Timer(attribute, delay, to, msg) => + import scala.concurrent.ExecutionContext.Implicits.global + interactionTimers.get(attribute).foreach(_.cancel()) + interactionTimers.update(attribute, context.system.scheduler.scheduleOnce(delay, to, msg)) + + case RespondsToZoneEnvironment.StopTimer(attribute) => + interactionTimers.get(attribute).foreach(_.cancel()) + interactionTimers.update(attribute, Default.Cancellable) + + case InteractingWithEnvironment(body, optional) => + applicableInteractions + .get(body.attribute) + .foreach(_.doInteractingWith(InteractiveObject, body, optional)) + + case EscapeFromEnvironment(body, optional) => + applicableInteractions + .get(body.attribute) + .foreach(_.stopInteractingWith(InteractiveObject, body, optional)) + + case RecoveredFromEnvironmentInteraction(attribute) => + applicableInteractions + .get(attribute) + .foreach(_.recoverFromInteracting(InteractiveObject)) + + case ResetAllEnvironmentInteractions => + applicableInteractions.values.foreach(_.recoverFromInteracting(InteractiveObject)) + interactionTimers.values.foreach(_.cancel()) + } + + def respondToEnvironmentPostStop(): Unit = { + interactionTimers.values.foreach(_.cancel()) + } +} + +object RespondsToZoneEnvironment { + val defaultInteractions: Map[EnvironmentTrait, InteractionWith] = Map.empty[EnvironmentTrait, InteractionWith] + + final case class Timer(attribute: EnvironmentTrait, delay: FiniteDuration, to: ActorRef, msg: Any) + + final case class StopTimer(attribute: EnvironmentTrait) +} diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala new file mode 100644 index 00000000..799c166c --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala @@ -0,0 +1,142 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects.serverobject.environment.interaction.common + +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget +import net.psforever.objects.serverobject.environment.{EnvironmentAttribute, EnvironmentTrait, PieceOfEnvironment} +import net.psforever.types.{OxygenState, PlanetSideGUID} + +trait Watery { + val attribute: EnvironmentTrait = EnvironmentAttribute.Water + + /** how long the current interaction has been progressing in the current way */ + protected var waterInteractionTime: Long = 0 + /** information regarding the drowning state */ + protected var condition: Option[OxygenStateTarget] = None + /** information regarding the drowning state */ + def Condition: Option[OxygenStateTarget] = condition +} + +object Watery { + /** + * 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 target + * @param body environment being interacted with + * @param state whether recovering or suffocating + * @param progress the percentage of completion towards the state + */ + final case class OxygenStateTarget( + guid: PlanetSideGUID, + body: PieceOfEnvironment, + state: OxygenState, + progress: Float + ) + + /** + * Calculate the effect of being exposed to a watery environment beyond an entity's critical region. + * @param obj the target + * @param interaction na + * @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 drowningInWater( + obj: PlanetSideServerObject, + interaction: Watery + ): (Boolean, Long, Float) = { + drowningInWateryConditions(obj, interaction.condition.map(_.state), interaction.waterInteractionTime) + } + /** + * Calculate the effect of being exposed to a watery environment beyond an entity's 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 over an entity's critical region. + * @param obj the target + * @param interaction na + * @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 recoveringFromWater( + obj: PlanetSideServerObject, + interaction: Watery + ): (Boolean, Long, Float) = { + recoveringFromWateryConditions(obj, interaction.condition.map(_.state), interaction.waterInteractionTime) + } + /** + * Calculate the effect of being removed from a watery environment over an entity's 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/environment/interaction/common/WithDeath.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/WithDeath.scala new file mode 100644 index 00000000..a389564e --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/WithDeath.scala @@ -0,0 +1,46 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects.serverobject.environment.interaction.common + +import net.psforever.objects.serverobject.environment.interaction.InteractionWith +import net.psforever.objects.serverobject.environment.{EnvironmentAttribute, EnvironmentTrait, PieceOfEnvironment} +import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.vital.etc.SuicideReason +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.vital.{ReconstructionActivity, Vitality} +import net.psforever.objects.zones.InteractsWithZone + +import scala.annotation.unused + +class WithDeath() + extends InteractionWith { + val channel: String = "" + val attribute: EnvironmentTrait = EnvironmentAttribute.Death + + /** + * Death causes target to be destroyed outright. + * It's not even considered as environmental damage anymore. + * @param obj target + * @param body environment + * @param data additional interaction information, if applicable + */ + def doInteractingWith( + obj: InteractsWithZone, + @unused body: PieceOfEnvironment, + @unused data: Option[Any] + ): Unit = { + if (!obj.Destroyed) { + obj.History.findLast { entry => entry.isInstanceOf[ReconstructionActivity] } match { + case Some(entry) if System.currentTimeMillis() - entry.time > 4000L => + obj.Actor ! Vitality.Damage( + DamageInteraction( + SourceEntry(obj), + SuicideReason(), + obj.Position + ).calculate() + ) + case _ => + } + } + } +} + diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/WithMovementTrigger.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/WithMovementTrigger.scala new file mode 100644 index 00000000..603c2365 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/WithMovementTrigger.scala @@ -0,0 +1,26 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects.serverobject.environment.interaction.common + +import net.psforever.objects.serverobject.environment._ +import net.psforever.objects.serverobject.environment.interaction.InteractionWith +import net.psforever.objects.zones.InteractsWithZone + +import scala.annotation.unused + +class WithMovementTrigger() + extends InteractionWith { + val attribute: EnvironmentTrait = EnvironmentAttribute.MovementFieldTrigger + /** + * The target will be affected by this action. + * @param obj target + * @param body environment + * //@param data additional interaction information, if applicable + */ + def doInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + @unused data: Option[Any] + ): Unit = { + body.asInstanceOf[GeneralMovementField].triggerAction(obj) + } +} diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala index 8670b889..bdf653a1 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -16,6 +16,8 @@ import net.psforever.objects.serverobject.containable.{Containable, ContainableB import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle} import net.psforever.objects.serverobject.environment._ +import net.psforever.objects.serverobject.environment.interaction.common.Watery +import net.psforever.objects.serverobject.environment.interaction.{InteractWithEnvironment, RespondsToZoneEnvironment} import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior, RadiationInMountableInteraction} import net.psforever.objects.serverobject.repair.RepairableVehicle @@ -23,10 +25,9 @@ import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.turret.auto.AffectedByAutomaticTurretFire import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.vehicles._ -import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} -import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ReconstructionActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity} -import net.psforever.objects.vital.environment.EnvironmentReason -import net.psforever.objects.vital.etc.SuicideReason +import net.psforever.objects.vehicles.interaction.WithWater +import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity} import net.psforever.objects.zones._ import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game._ @@ -74,14 +75,14 @@ class VehicleControl(vehicle: Vehicle) def CargoObject: Vehicle = vehicle def AffectedObject: Vehicle = vehicle - SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) - SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) - SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath) - SetInteraction(EnvironmentAttribute.MovementFieldTrigger, doInteractingWithMovementTrigger) - if (!GlobalDefinitions.isFlightVehicle(vehicle.Definition)) { - //can recover from sinking disability - SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater) - } +// SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) +// SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) +// SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath) +// SetInteraction(EnvironmentAttribute.MovementFieldTrigger, doInteractingWithMovementTrigger) +// if (!GlobalDefinitions.isFlightVehicle(vehicle.Definition)) { +// //can recover from sinking disability +// SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater) +// } /** cheap flag for whether the vehicle is decaying */ var decaying : Boolean = false @@ -104,7 +105,7 @@ class VehicleControl(vehicle: Vehicle) context.stop(util().Actor) util().Actor = Default.Actor } - recoverFromEnvironmentInteracting() + respondToEnvironmentPostStop() endAllCargoOperations() } @@ -218,8 +219,8 @@ class VehicleControl(vehicle: Vehicle) ) } - case VehicleControl.Disable() => - PrepareForDisabled(kickPassengers = false) + case VehicleControl.Disable(kickPassengers) => + PrepareForDisabled(kickPassengers) context.become(Disabled) case Vehicle.Deconstruct(time) => @@ -249,9 +250,9 @@ class VehicleControl(vehicle: Vehicle) case VehicleControl.RadiationTick => vehicle.interaction().find { _.Type == RadiationInMountableInteraction } match { case Some(func) => func.interaction(vehicle.getInteractionSector(), vehicle) - case _ => ; + case _ => () } - case _ => ; + case _ => () } def commonDisabledBehavior: Receive = checkBehavior @@ -387,28 +388,20 @@ class VehicleControl(vehicle: Vehicle) val zone = vehicle.Zone val zoneId = zone.id val events = zone.VehicleEvents - //miscellaneous changes - recoverFromEnvironmentInteracting() //escape being someone else's cargo - vehicle.MountedIn match { - case Some(_) => - startCargoDismounting(bailed = true) - case _ => ; - } + vehicle.MountedIn.foreach(_ => startCargoDismounting(bailed = true)) if (!vehicle.isFlying || kickPassengers) { //kick all passengers (either not flying, or being explicitly instructed) vehicle.Seats.values.foreach { seat => - seat.occupant match { - case Some(player) => - seat.unmount(player, BailType.Kicked) - player.VehicleSeated = None - if (player.isAlive) { - zone.actor ! ZoneActor.AddToBlockMap(player, vehicle.Position) - } - if (player.HasGUID) { - events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2 = true, guid)) - } - case None => ; + seat.occupant.foreach { player => + seat.unmount(player, BailType.Kicked) + player.VehicleSeated = None + if (player.isAlive) { + zone.actor ! ZoneActor.AddToBlockMap(player, vehicle.Position) + } + if (player.HasGUID) { + events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2 = true, guid)) + } } } } @@ -594,219 +587,40 @@ class VehicleControl(vehicle: Vehicle) } } - /** - * 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 && GlobalDefinitions.isFlightVehicle(vehicle.Definition)) { - (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 - .flatMap { - case seat if seat.isOccupied => seat.occupants - case _ => Nil - } - .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 `InteractingWithEnvironment` - * @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 ! InteractingWithEnvironment(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, InteractingWithEnvironment(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) { - vehicle.History.findLast { entry => entry.isInstanceOf[ReconstructionActivity] } match { - case Some(entry) if System.currentTimeMillis() - entry.time > 4000L => - PerformDamage( - vehicle, - DamageInteraction( - VehicleSource(vehicle), - SuicideReason(), - vehicle.Position - ).calculate() - ) - case _ => - } - } - } - - /** - * The vehicle will be affected by this action. - * @param obj the target - * @param body the environment - * @param data additional interaction information, if applicable - */ - def doInteractingWithMovementTrigger( - obj: PlanetSideServerObject, - body: PieceOfEnvironment, - data: Option[OxygenStateTarget] - ): Unit = { - body.asInstanceOf[GeneralMovementField].triggerAction(obj) - } - - /** - * 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 - .flatMap { - case seat if seat.isOccupied => seat.occupants - case _ => Nil - } - .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 && GlobalDefinitions.isFlightVehicle(vehicle.Definition)) { - 0f //no progress bar - } else { - c - } + def updateZoneInteractionProgressUI(player: Player) : Unit = { + val interactions = vehicle + .interaction() + .collectFirst { case inter: InteractWithEnvironment => inter.Interactions } + .getOrElse(RespondsToZoneEnvironment.defaultInteractions) + //water + interactions + .get(EnvironmentAttribute.Water) + .collect { + case watery: WithWater if watery.Condition.map(_.state).contains(OxygenState.Suffocation) => + val percentage: Float = { + val (a, _, c) = Watery.drowningInWater(vehicle, watery) + if (a && GlobalDefinitions.isFlightVehicle(vehicle.Definition)) { + 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 => ; - } + } + watery.doInteractingWithTargets(player, percentage, watery.Condition.map(_.body).get, List(player)) + case watery: WithWater if watery.Condition.map(_.state).contains(OxygenState.Recovery) => + watery.stopInteractingWithTargets( + player, + Watery.recoveringFromWater(vehicle, watery)._3, + watery.Condition.map(_.body).get, + List(player) + ) + case watery: WithWater => + watery.recoverFromInteracting(player) + } } override def parseAttribute(attribute: Int, value: Long, other: Option[Any]) : Unit = { @@ -904,7 +718,7 @@ object VehicleControl { private case class PrepareForDeletion() - private case class Disable() + final case class Disable(kickPassengers: Boolean = false) private case class Deletion() diff --git a/src/main/scala/net/psforever/objects/vehicles/interaction/WithLava.scala b/src/main/scala/net/psforever/objects/vehicles/interaction/WithLava.scala new file mode 100644 index 00000000..1670c027 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/interaction/WithLava.scala @@ -0,0 +1,49 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects.vehicles.interaction + +import net.psforever.objects.serverobject.environment.interaction.{InteractionWith, RespondsToZoneEnvironment} +import net.psforever.objects.serverobject.environment.{EnvironmentAttribute, EnvironmentTrait, PieceOfEnvironment, interaction} +import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.environment.EnvironmentReason +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.zones.InteractsWithZone + +import scala.concurrent.duration._ + +class WithLava() + extends InteractionWith { + val attribute: EnvironmentTrait = EnvironmentAttribute.Lava + + private var stopBurn: Boolean = false + + /** + * 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 doInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + if (stopBurn && data.nonEmpty) { + stopBurn = false + } else if (!obj.Destroyed) { + obj.Actor ! Vitality.Damage( + DamageInteraction( + SourceEntry(obj), + EnvironmentReason(body, obj), + obj.Position + ).calculate() + ) + //keep doing damage + obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = 250 milliseconds, obj.Actor, interaction.InteractingWithEnvironment(body, Some("burning"))) + } + } + + override def stopInteractingWith(obj: InteractsWithZone, body: PieceOfEnvironment, parentInfo: Option[Any]): Unit = { + stopBurn = true + } +} diff --git a/src/main/scala/net/psforever/objects/vehicles/interaction/WithWater.scala b/src/main/scala/net/psforever/objects/vehicles/interaction/WithWater.scala new file mode 100644 index 00000000..39fd899b --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/interaction/WithWater.scala @@ -0,0 +1,137 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects.vehicles.interaction + +import net.psforever.objects.{GlobalDefinitions, Vehicle} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.environment.interaction.{InteractionWith, RespondsToZoneEnvironment} +import net.psforever.objects.serverobject.environment.interaction.common.Watery +import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget +import net.psforever.objects.serverobject.environment.{PieceOfEnvironment, interaction} +import net.psforever.objects.vehicles.control.VehicleControl +import net.psforever.objects.zones.InteractsWithZone +import net.psforever.types.OxygenState + +import scala.concurrent.duration._ + +class WithWater() + extends InteractionWith + with Watery { + /** + * 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 doInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + obj match { + case vehicle: Vehicle => + val (effect: Boolean, time: Long, percentage: Float) = { + val (a, b, c) = Watery.drowningInWateryConditions(obj, condition.map(_.state), waterInteractionTime) + if (a && GlobalDefinitions.isFlightVehicle(vehicle.Definition)) { + (true, 0L, 0f) //no progress bar + } else { + (a, b, c) + } + } + if (effect) { + condition = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage)) + waterInteractionTime = System.currentTimeMillis() + time + obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, VehicleControl.Disable(true)) + doInteractingWithTargets( + obj, + percentage, + body, + vehicle.Seats.values.flatMap(_.occupants).filter(p => p.isAlive && (p.Zone eq obj.Zone)) + ) + } + case _ => () + } + } + + /** + * 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 + */ + override def stopInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + obj match { + case vehicle: Vehicle => + val (effect: Boolean, time: Long, percentage: Float) = + Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) + if (effect) { + condition = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage)) + waterInteractionTime = System.currentTimeMillis() + time + obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute)) + stopInteractingWithTargets( + obj, + percentage, + body, + vehicle.Seats.values.flatMap(_.occupants).filter(p => p.isAlive && (p.Zone eq obj.Zone)) + ) + } + case _ => () + } + } + + override def recoverFromInteracting(obj: InteractsWithZone): Unit = { + super.recoverFromInteracting(obj) + if (condition.exists(_.state == OxygenState.Suffocation)) { + stopInteractingWith(obj, condition.map(_.body).get, None) + } + waterInteractionTime = 0L + condition = None + } + + /** + * Tell the given targets that water causes vehicles to become disabled if they dive off too far, too deep. + * @see `InteractingWithEnvironment` + * @see `OxygenState` + * @see `OxygenStateTarget` + * @param obj the target + * @param percentage the progress bar completion state + * @param body the environment + * @param targets recipients of the information + */ + def doInteractingWithTargets( + obj: PlanetSideServerObject, + percentage: Float, + body: PieceOfEnvironment, + targets: Iterable[PlanetSideServerObject] + ): Unit = { + val state = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage)) + targets.foreach(_.Actor ! interaction.InteractingWithEnvironment(body, state)) + } + + /** + * Tell the given targets that, when out of water, the vehicle no longer risks becoming disabled. + * @see `EscapeFromEnvironment` + * @see `OxygenState` + * @see `OxygenStateTarget` + * @param obj the target + * @param percentage the progress bar completion state + * @param body the environment + * @param targets recipients of the information + */ + def stopInteractingWithTargets( + obj: PlanetSideServerObject, + percentage: Float, + body: PieceOfEnvironment, + targets: Iterable[PlanetSideServerObject] + ): Unit = { + val state = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage)) + targets.foreach(_.Actor ! interaction.EscapeFromEnvironment(body, state)) + } +} diff --git a/src/main/scala/net/psforever/objects/vital/environment/EnvironmentReason.scala b/src/main/scala/net/psforever/objects/vital/environment/EnvironmentReason.scala index 4d1c08d1..c354ffb1 100644 --- a/src/main/scala/net/psforever/objects/vital/environment/EnvironmentReason.scala +++ b/src/main/scala/net/psforever/objects/vital/environment/EnvironmentReason.scala @@ -7,7 +7,8 @@ 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} +import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} +import net.psforever.objects.zones.InteractsWithZone /** * A wrapper for a "damage source" in damage calculations @@ -40,11 +41,11 @@ object EnvironmentReason { * @param target the target being involved in this interaction * @return an `EnvironmentReason` object */ - def apply(body: PieceOfEnvironment, target: Vitality): EnvironmentReason = + def apply(body: PieceOfEnvironment, target: InteractsWithZone): EnvironmentReason = EnvironmentReason(body, target.DamageModel.DamageUsing) /** variable, no resisting, quick and simple */ - def drm(against: DamageCalculations.Selector) = new DamageResistanceModel { + def drm(against: DamageCalculations.Selector): DamageResistanceModel = new DamageResistanceModel { DamageUsing = against ResistUsing = NoResistanceSelection Model = SimpleResolutions.calculate diff --git a/src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala b/src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala index 35ecc892..4304a651 100644 --- a/src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala +++ b/src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala @@ -1,11 +1,14 @@ // Copyright (c) 2021 PSForever package net.psforever.objects.zones +import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.vital.{Vitality, VitalityDefinition} import net.psforever.objects.zones.blockmap.SectorPopulation trait InteractsWithZone - extends PlanetSideServerObject { + extends PlanetSideServerObject + with Vitality { /** interactions for this particular entity is allowed */ private var _allowInteraction: Boolean = true /** maximum interaction range used to generate the commonly tested sector */ @@ -47,12 +50,12 @@ trait InteractsWithZone def interaction(): List[ZoneInteraction] = interactions - def getInteractionSector(): SectorPopulation = { + def getInteractionSector: SectorPopulation = { this.Zone.blockMap.sector(this.Position, interactionRange) } def doInteractions(): Unit = { - val sector = getInteractionSector() + val sector = getInteractionSector //println(sector.environmentList.map { _.attribute }.mkString(" ")) interactions.foreach { _.interaction(sector, target = this) } } @@ -66,6 +69,8 @@ trait InteractsWithZone def resetInteractions(): Unit = { interactions.foreach { _.resetInteraction(target = this) } } + + override def Definition: ObjectDefinition with VitalityDefinition } trait ZoneInteractionType diff --git a/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala b/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala index 60ce74e6..f76e27f8 100644 --- a/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala +++ b/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala @@ -273,6 +273,9 @@ class SectorGroup( extends SectorPopulation object SectorGroup { + /** cached sector of no range and no entity coverage */ + final val emptySector: SectorGroup = SectorGroup(range = 0, sectors = Nil) + /** * Overloaded constructor that takes a single sector * and transfers the lists of entities into a single conglomeration of the sector populations. @@ -326,7 +329,7 @@ object SectorGroup { */ def apply(sectors: Iterable[Sector]): SectorGroup = { if (sectors.isEmpty) { - SectorGroup(range = 0, sectors = Nil) + SectorGroup.emptySector } else if (sectors.size == 1) { SectorGroup(sectors.head.rangeX, sectors.head.rangeY, sectors) } else { diff --git a/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala b/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala index d08c6ba3..d80f16d7 100644 --- a/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala +++ b/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala @@ -4,11 +4,12 @@ package net.psforever.packet.game import net.psforever.newcodecs.newcodecs import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} import net.psforever.types.{OxygenState, PlanetSideGUID} -import scodec.Codec +import scodec.bits.BitVector +import scodec.{Attempt, Codec} import scodec.codecs._ /** - * Infomation about the progress bar displayed for a certain target's drowning condition. + * Information 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 @@ -41,8 +42,8 @@ final case class OxygenStateMessage( vehicle: Option[DrowningTarget] ) extends PlanetSideGamePacket { type Packet = OxygenStateMessage - def opcode = GamePacketOpcode.OxygenStateMessage - def encode = OxygenStateMessage.encode(this) + def opcode: GamePacketOpcode.Type = GamePacketOpcode.OxygenStateMessage + def encode: Attempt[BitVector] = OxygenStateMessage.encode(this) } object DrowningTarget { diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index 33d5b894..68333969 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -7,7 +7,7 @@ import net.psforever.objects.ballistics.Projectile 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.serverobject.environment.interaction.common.Watery.OxygenStateTarget import net.psforever.objects.sourcing.SourceEntry import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala index 05ba1a1d..3e0c020c 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -6,7 +6,7 @@ import net.psforever.objects.avatar.scoring.KDAStat import net.psforever.objects.ballistics.Projectile import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem -import net.psforever.objects.serverobject.environment.OxygenStateTarget +import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget import net.psforever.objects.sourcing.SourceEntry import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.objectcreate.ConstructorData diff --git a/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala b/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala index de43ae73..2e72d777 100644 --- a/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala +++ b/src/test/scala/objects/InteractsWithZoneEnvironmentTest.scala @@ -5,6 +5,7 @@ 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.serverobject.environment.interaction.{EscapeFromEnvironment, InteractWithEnvironment, InteractingWithEnvironment} import net.psforever.objects.vital.{Vitality, VitalityDefinition} import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneMap} import net.psforever.types.{PlanetSideEmpire, Vector3} @@ -48,7 +49,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { val msg = testProbe.receiveOne(max = 250 milliseconds) assert( msg match { - case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case InteractingWithEnvironment(b, _) => (b eq pool1) case _ => false } ) @@ -67,7 +68,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { val msg1 = testProbe.receiveOne(max = 250 milliseconds) assert( msg1 match { - case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case InteractingWithEnvironment(b, _) => (b eq pool1) case _ => false } ) @@ -77,7 +78,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { val msg2 = testProbe.receiveOne(max = 250 milliseconds) assert( msg2 match { - case EscapeFromEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case EscapeFromEnvironment(b, _) => (b eq pool1) case _ => false } ) @@ -96,7 +97,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { val msg1 = testProbe.receiveOne(max = 250 milliseconds) assert( msg1 match { - case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case InteractingWithEnvironment(b, _) => (b eq pool1) case _ => false } ) @@ -106,7 +107,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { val msg2 = testProbe.receiveOne(max = 250 milliseconds) assert( msg2 match { - case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool2) + case InteractingWithEnvironment(b, _) => (b eq pool1) case _ => false } ) @@ -124,7 +125,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { val msg1 = testProbe.receiveOne(max = 250 milliseconds) assert( msg1 match { - case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case InteractingWithEnvironment(b, _) => (b eq pool1) case _ => false } ) @@ -134,13 +135,13 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { val msgs = testProbe.receiveN(2, max = 250 milliseconds) assert( msgs.head match { - case EscapeFromEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case EscapeFromEnvironment(b, _) => (b eq pool1) case _ => false } ) assert( msgs(1) match { - case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool3) + case InteractingWithEnvironment(b, _) => (b eq pool1) case _ => false } ) @@ -159,7 +160,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { val msg1 = testProbe.receiveOne(max = 250 milliseconds) assert( msg1 match { - case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case InteractingWithEnvironment(b, _) => (b eq pool1) case _ => false } ) @@ -167,7 +168,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { obj.allowInteraction = false val msg2 = testProbe.receiveOne(max = 250 milliseconds) msg2 match { - case EscapeFromEnvironment(o, b, _) => assert((o eq obj) && (b eq pool1)) + case EscapeFromEnvironment(b, _) => (b eq pool1) case _ => assert( false) } obj.zoneInteractions() @@ -189,7 +190,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { val msg1 = testProbe.receiveOne(max = 250 milliseconds) assert( msg1 match { - case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) + case InteractingWithEnvironment(b, _) => (b eq pool1) case _ => false } ) @@ -199,8 +200,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest { object InteractsWithZoneEnvironmentTest { def testObject(): PlanetSideServerObject with InteractsWithZone = { new PlanetSideServerObject - with InteractsWithZone - with Vitality { + with InteractsWithZone { interaction(new InteractWithEnvironment()) def Faction: PlanetSideEmpire.Value = PlanetSideEmpire.VS def DamageModel = null diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala index df583369..6bac5887 100644 --- a/src/test/scala/objects/PlayerControlTest.scala +++ b/src/test/scala/objects/PlayerControlTest.scala @@ -16,7 +16,8 @@ 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.serverobject.environment.interaction.common.Watery.OxygenStateTarget +import net.psforever.objects.serverobject.environment.{DeepSquare, EnvironmentAttribute, Pool} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.vital.base.DamageResolution import net.psforever.objects.vital.interaction.DamageInteraction @@ -812,7 +813,7 @@ class PlayerControlInteractWithWaterTest extends ActorTest { msg_drown match { case AvatarServiceMessage( "TestCharacter1", - AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), OxygenState.Suffocation, 100f), _) + AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), _, OxygenState.Suffocation, 100f), _) ) => true case _ => false } @@ -866,7 +867,7 @@ class PlayerControlStopInteractWithWaterTest extends ActorTest { msg_drown match { case AvatarServiceMessage( "TestCharacter1", - AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), OxygenState.Suffocation, 100f), _) + AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), _, OxygenState.Suffocation, 100f), _) ) => true case _ => false } @@ -879,7 +880,7 @@ class PlayerControlStopInteractWithWaterTest extends ActorTest { msg_recover match { case AvatarServiceMessage( "TestCharacter1", - AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), OxygenState.Recovery, _), _) + AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), _, OxygenState.Recovery, _), _) ) => true case _ => false } diff --git a/src/test/scala/objects/VehicleControlTest.scala b/src/test/scala/objects/VehicleControlTest.scala index 8baf1aa4..460618e7 100644 --- a/src/test/scala/objects/VehicleControlTest.scala +++ b/src/test/scala/objects/VehicleControlTest.scala @@ -14,6 +14,8 @@ import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.environment._ +import net.psforever.objects.serverobject.environment.interaction.{EscapeFromEnvironment, InteractingWithEnvironment} +import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vehicles.VehicleLockState import net.psforever.objects.vehicles.control.VehicleControl @@ -617,10 +619,9 @@ class VehicleControlInteractWithWaterPartialTest extends ActorTest { assert( msg_drown match { case InteractingWithEnvironment( - p1, p2, - Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f)) - ) => (p1 eq player1) && (p2 eq pool) + Some(OxygenStateTarget(PlanetSideGUID(2), _, OxygenState.Suffocation, 100f)) + ) => (p2 eq pool) case _ => false } ) @@ -679,8 +680,8 @@ class VehicleControlInteractWithWaterTest extends ActorTest { case AvatarServiceMessage( "TestCharacter1", AvatarAction.OxygenState( - OxygenStateTarget(PlanetSideGUID(1), OxygenState.Suffocation, 100f), - Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f)) + OxygenStateTarget(PlanetSideGUID(1), _, OxygenState.Suffocation, 100f), + Some(OxygenStateTarget(PlanetSideGUID(2), _, OxygenState.Suffocation, 100f)) ) ) => true case _ => false @@ -742,10 +743,9 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest { assert( msg_drown match { case InteractingWithEnvironment( - p1, p2, - Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f)) - ) => (p1 eq player1) && (p2 eq pool) + Some(OxygenStateTarget(PlanetSideGUID(2), _, OxygenState.Suffocation, 100f)) + ) => (p2 eq pool) case _ => false } ) @@ -756,10 +756,9 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest { assert( msg_recover match { case EscapeFromEnvironment( - p1, p2, - Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Recovery, _)) - ) => (p1 eq player1) && (p2 eq pool) + Some(OxygenStateTarget(PlanetSideGUID(2), _, OxygenState.Recovery, _)) + ) => (p2 eq pool) case _ => false } )