Environmental Redo (#1169)

* switched environment handling from the control agency side to the entity side to match other zone interactions

* reusing the existing sector data; better resets, negating an issue during mounting where the player does not switch back to recovery; disabling was bugged due to missed timer upgrade

* making lava no longer deadly; wait ...

* only one method of cancelling a lava interaction at a time
This commit is contained in:
Fate-JH 2024-03-02 23:43:53 -05:00 committed by GitHub
parent ea77d4728f
commit b43e7a6993
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1159 additions and 875 deletions

View file

@ -3,6 +3,7 @@ package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions
import net.psforever.objects.vital.InGameHistory import net.psforever.objects.vital.InGameHistory
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -61,6 +62,7 @@ class SessionMountHandlers(
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=45, obj.NtuCapacitorScaled)) sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=45, obj.NtuCapacitorScaled))
sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sendResponse(GenericObjectActionMessage(obj_guid, code=11))
sessionData.accessContainer(obj) sessionData.accessContainer(obj)
tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) case Mountable.CanMount(obj: Vehicle, seatNumber, _)
@ -76,6 +78,7 @@ class SessionMountHandlers(
obj.Cloaked = tplayer.Cloaked obj.Cloaked = tplayer.Cloaked
sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sendResponse(GenericObjectActionMessage(obj_guid, code=11))
sessionData.accessContainer(obj) sessionData.accessContainer(obj)
tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) case Mountable.CanMount(obj: Vehicle, seatNumber, _)
@ -90,6 +93,7 @@ class SessionMountHandlers(
sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sendResponse(GenericObjectActionMessage(obj_guid, code=11))
sessionData.accessContainer(obj) sessionData.accessContainer(obj)
sessionData.updateWeaponAtSeatPosition(obj, seatNumber) sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) case Mountable.CanMount(obj: Vehicle, seatNumber, _)
@ -103,6 +107,7 @@ class SessionMountHandlers(
sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sendResponse(GenericObjectActionMessage(obj_guid, code=11))
sessionData.accessContainer(obj) sessionData.accessContainer(obj)
sessionData.updateWeaponAtSeatPosition(obj, seatNumber) sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) case Mountable.CanMount(obj: Vehicle, seatNumber, _)
@ -122,6 +127,7 @@ class SessionMountHandlers(
sessionData.accessContainer(obj) sessionData.accessContainer(obj)
sessionData.updateWeaponAtSeatPosition(obj, seatNumber) sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
sessionData.keepAliveFunc = sessionData.keepAlivePersistence sessionData.keepAliveFunc = sessionData.keepAlivePersistence
tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) => case Mountable.CanMount(obj: Vehicle, seatNumber, _) =>
@ -139,6 +145,7 @@ class SessionMountHandlers(
sessionData.accessContainer(obj) sessionData.accessContainer(obj)
sessionData.updateWeaponAtSeatPosition(obj, seatNumber) sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
sessionData.keepAliveFunc = sessionData.keepAlivePersistence sessionData.keepAliveFunc = sessionData.keepAlivePersistence
tplayer.Actor ! ResetAllEnvironmentInteractions
MountingAction(tplayer, obj, seatNumber) MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
@ -173,7 +180,7 @@ class SessionMountHandlers(
MountingAction(tplayer, obj, seatNumber) MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Mountable, _, _) => 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, _) => case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) =>
log.info(s"${tplayer.Name} dismounts the implant terminal") log.info(s"${tplayer.Name} dismounts the implant terminal")

View file

@ -1,16 +1,17 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects 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.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.ballistics.InteractWithRadiationClouds import net.psforever.objects.ballistics.InteractWithRadiationClouds
import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets} import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets}
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition} import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} 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.affinity.FactionAffinity
import net.psforever.objects.serverobject.aura.AuraContainer 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.serverobject.mount.MountableEntity
import net.psforever.objects.vital.resistance.ResistanceProfile import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFromEquipment, Vitality} 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.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation} import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation}
import net.psforever.objects.zones.{InteractsWithZone, ZoneAware, Zoning} import net.psforever.objects.zones.{InteractsWithZone, ZoneAware, Zoning}
import net.psforever.types.{PlanetSideGUID, _} import net.psforever.types._
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.util.{Success, Try} import scala.util.{Success, Try}
@ -36,7 +37,13 @@ class Player(var avatar: Avatar)
with ZoneAware with ZoneAware
with AuraContainer with AuraContainer
with MountableEntity { 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 InteractWithMinesUnlessSpectating(obj = this, range = 10))
interaction(new InteractWithTurrets()) interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))

View file

@ -6,14 +6,15 @@ import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile}
import net.psforever.objects.serverobject.mount.{MountableEntity, Seat, SeatDefinition} 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.affinity.FactionAffinity
import net.psforever.objects.serverobject.aura.AuraContainer import net.psforever.objects.serverobject.aura.AuraContainer
import net.psforever.objects.serverobject.deploy.Deployment 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.hackable.Hackable
import net.psforever.objects.serverobject.structures.AmenityOwner import net.psforever.objects.serverobject.structures.AmenityOwner
import net.psforever.objects.vehicles._ 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.resistance.StandardResistanceProfile
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.vital.resolution.DamageResistanceModel
@ -76,21 +77,26 @@ import scala.util.{Success, Try}
*/ */
class Vehicle(private val vehicleDef: VehicleDefinition) class Vehicle(private val vehicleDef: VehicleDefinition)
extends AmenityOwner extends AmenityOwner
with BlockMapEntity with BlockMapEntity
with MountableWeapons with MountableWeapons
with InteractsWithZone with InteractsWithZone
with Hackable with Hackable
with FactionAffinity with FactionAffinity
with Deployment with Deployment
with Vitality with Vitality
with OwnableByPlayer with OwnableByPlayer
with StandardResistanceProfile with StandardResistanceProfile
with JammableUnit with JammableUnit
with CommonNtuContainer with CommonNtuContainer
with Container with Container
with AuraContainer with AuraContainer
with MountableEntity { with MountableEntity {
interaction(new InteractWithEnvironment()) interaction(environment.interaction.InteractWithEnvironment(Seq(
new WithWater(),
new WithLava(),
new WithDeath(),
new WithMovementTrigger()
)))
interaction(new InteractWithMines(range = 20)) interaction(new InteractWithMines(range = 20))
interaction(new InteractWithTurrets()) interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20)) interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20))

View file

@ -30,15 +30,12 @@ import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.objects.locker.LockerContainerControl 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.repair.Repairable
import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource} import net.psforever.objects.sourcing.{AmenitySource, PlayerSource}
import net.psforever.objects.vital.collision.CollisionReason 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.etc.{PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{Adversarial, DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{Adversarial, DamageInteraction, DamageResult}
import net.psforever.services.hart.ShuttleState
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -66,12 +63,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
ApplicableEffect(Aura.Fire) ApplicableEffect(Aura.Fire)
def InteractiveObject: Player = player 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 log = org.log4s.getLogger(player.Name)
private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel) private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel)
/** suffocating, or regaining breath? */ /** suffocating, or regaining breath? */
@ -93,6 +85,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
player.avatar.locker.Actor = Default.Actor player.avatar.locker.Actor = Default.Actor
EndAllEffects() EndAllEffects()
EndAllAggravation() EndAllAggravation()
respondToEnvironmentPostStop()
} }
def receive: Receive = def receive: Receive =
@ -1215,159 +1208,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
val value = target.Aura.foldLeft(0)(_ + PlayerControl.auraEffectToAttributeValue(_)) val value = target.Aura.foldLeft(0)(_ + PlayerControl.auraEffectToAttributeValue(_))
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttributeToAll(target.GUID, 54, value)) 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 { object PlayerControl {

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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()

View file

@ -67,7 +67,11 @@ object EnvironmentAttribute extends Enum[EnvironmentTrait] {
/** water can only interact with objects that are negatively affected by being exposed to water; /** water can only interact with objects that are negatively affected by being exposed to water;
* it's better this way */ * it's better this way */
def canInteractWith(obj: PlanetSideGameObject): Boolean = { 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
})
} }
} }

View file

@ -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)
}
}
}

View file

@ -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()
}
}

View file

@ -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

View file

@ -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*/ }
}

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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 _ =>
}
}
}
}

View file

@ -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)
}
}

View file

@ -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.Damageable.Target
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle} import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle}
import net.psforever.objects.serverobject.environment._ 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.hackable.GenericHackables
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior, RadiationInMountableInteraction} import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior, RadiationInMountableInteraction}
import net.psforever.objects.serverobject.repair.RepairableVehicle 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.serverobject.turret.auto.AffectedByAutomaticTurretFire
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles._
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vehicles.interaction.WithWater
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ReconstructionActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity} import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity}
import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.zones._ import net.psforever.objects.zones._
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game._ import net.psforever.packet.game._
@ -74,14 +75,14 @@ class VehicleControl(vehicle: Vehicle)
def CargoObject: Vehicle = vehicle def CargoObject: Vehicle = vehicle
def AffectedObject: Vehicle = vehicle def AffectedObject: Vehicle = vehicle
SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) // SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater)
SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) // SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava)
SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath) // SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath)
SetInteraction(EnvironmentAttribute.MovementFieldTrigger, doInteractingWithMovementTrigger) // SetInteraction(EnvironmentAttribute.MovementFieldTrigger, doInteractingWithMovementTrigger)
if (!GlobalDefinitions.isFlightVehicle(vehicle.Definition)) { // if (!GlobalDefinitions.isFlightVehicle(vehicle.Definition)) {
//can recover from sinking disability // //can recover from sinking disability
SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater) // SetInteractionStop(EnvironmentAttribute.Water, stopInteractingWithWater)
} // }
/** cheap flag for whether the vehicle is decaying */ /** cheap flag for whether the vehicle is decaying */
var decaying : Boolean = false var decaying : Boolean = false
@ -104,7 +105,7 @@ class VehicleControl(vehicle: Vehicle)
context.stop(util().Actor) context.stop(util().Actor)
util().Actor = Default.Actor util().Actor = Default.Actor
} }
recoverFromEnvironmentInteracting() respondToEnvironmentPostStop()
endAllCargoOperations() endAllCargoOperations()
} }
@ -218,8 +219,8 @@ class VehicleControl(vehicle: Vehicle)
) )
} }
case VehicleControl.Disable() => case VehicleControl.Disable(kickPassengers) =>
PrepareForDisabled(kickPassengers = false) PrepareForDisabled(kickPassengers)
context.become(Disabled) context.become(Disabled)
case Vehicle.Deconstruct(time) => case Vehicle.Deconstruct(time) =>
@ -249,9 +250,9 @@ class VehicleControl(vehicle: Vehicle)
case VehicleControl.RadiationTick => case VehicleControl.RadiationTick =>
vehicle.interaction().find { _.Type == RadiationInMountableInteraction } match { vehicle.interaction().find { _.Type == RadiationInMountableInteraction } match {
case Some(func) => func.interaction(vehicle.getInteractionSector(), vehicle) case Some(func) => func.interaction(vehicle.getInteractionSector(), vehicle)
case _ => ; case _ => ()
} }
case _ => ; case _ => ()
} }
def commonDisabledBehavior: Receive = checkBehavior def commonDisabledBehavior: Receive = checkBehavior
@ -387,28 +388,20 @@ class VehicleControl(vehicle: Vehicle)
val zone = vehicle.Zone val zone = vehicle.Zone
val zoneId = zone.id val zoneId = zone.id
val events = zone.VehicleEvents val events = zone.VehicleEvents
//miscellaneous changes
recoverFromEnvironmentInteracting()
//escape being someone else's cargo //escape being someone else's cargo
vehicle.MountedIn match { vehicle.MountedIn.foreach(_ => startCargoDismounting(bailed = true))
case Some(_) =>
startCargoDismounting(bailed = true)
case _ => ;
}
if (!vehicle.isFlying || kickPassengers) { if (!vehicle.isFlying || kickPassengers) {
//kick all passengers (either not flying, or being explicitly instructed) //kick all passengers (either not flying, or being explicitly instructed)
vehicle.Seats.values.foreach { seat => vehicle.Seats.values.foreach { seat =>
seat.occupant match { seat.occupant.foreach { player =>
case Some(player) => seat.unmount(player, BailType.Kicked)
seat.unmount(player, BailType.Kicked) player.VehicleSeated = None
player.VehicleSeated = None if (player.isAlive) {
if (player.isAlive) { zone.actor ! ZoneActor.AddToBlockMap(player, vehicle.Position)
zone.actor ! ZoneActor.AddToBlockMap(player, vehicle.Position) }
} if (player.HasGUID) {
if (player.HasGUID) { events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2 = true, guid))
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2 = true, guid)) }
}
case None => ;
} }
} }
} }
@ -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, * 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. * update the visual progress element (progress bar) that is visible to the recipient's client.
* @param player the recipient of this ui update * @param player the recipient of this ui update
*/ */
def updateZoneInteractionProgressUI(player : Player) : Unit = { def updateZoneInteractionProgressUI(player: Player) : Unit = {
submergedCondition match { val interactions = vehicle
case Some(OxygenState.Suffocation) => .interaction()
interactWith match { .collectFirst { case inter: InteractWithEnvironment => inter.Interactions }
case Some(body) => .getOrElse(RespondsToZoneEnvironment.defaultInteractions)
val percentage: Float = { //water
val (a, _, c) = RespondsToZoneEnvironment.drowningInWateryConditions(vehicle, submergedCondition, interactionTime) interactions
if (a && GlobalDefinitions.isFlightVehicle(vehicle.Definition)) { .get(EnvironmentAttribute.Water)
0f //no progress bar .collect {
} else { case watery: WithWater if watery.Condition.map(_.state).contains(OxygenState.Suffocation) =>
c 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 _ => watery.doInteractingWithTargets(player, percentage, watery.Condition.map(_.body).get, List(player))
recoverFromEnvironmentInteracting() case watery: WithWater if watery.Condition.map(_.state).contains(OxygenState.Recovery) =>
} watery.stopInteractingWithTargets(
case Some(OxygenState.Recovery) => player,
vehicle.Zone.map.environment.find { _.attribute == EnvironmentAttribute.Water } match { Watery.recoveringFromWater(vehicle, watery)._3,
case Some(body) => //any body of water will do ... watery.Condition.map(_.body).get,
stopInteractingWithWaterToTargets( List(player)
RespondsToZoneEnvironment.recoveringFromWateryConditions(vehicle, submergedCondition, interactionTime)._3, )
body, case watery: WithWater =>
List(player) watery.recoverFromInteracting(player)
) }
case _ =>
recoverFromEnvironmentInteracting()
}
case None => ;
}
} }
override def parseAttribute(attribute: Int, value: Long, other: Option[Any]) : Unit = { override def parseAttribute(attribute: Int, value: Long, other: Option[Any]) : Unit = {
@ -904,7 +718,7 @@ object VehicleControl {
private case class PrepareForDeletion() private case class PrepareForDeletion()
private case class Disable() final case class Disable(kickPassengers: Boolean = false)
private case class Deletion() private case class Deletion()

View file

@ -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
}
}

View file

@ -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))
}
}

View file

@ -7,7 +7,8 @@ import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.damage.DamageCalculations import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.prop.DamageProperties import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel} 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 * A wrapper for a "damage source" in damage calculations
@ -40,11 +41,11 @@ object EnvironmentReason {
* @param target the target being involved in this interaction * @param target the target being involved in this interaction
* @return an `EnvironmentReason` object * @return an `EnvironmentReason` object
*/ */
def apply(body: PieceOfEnvironment, target: Vitality): EnvironmentReason = def apply(body: PieceOfEnvironment, target: InteractsWithZone): EnvironmentReason =
EnvironmentReason(body, target.DamageModel.DamageUsing) EnvironmentReason(body, target.DamageModel.DamageUsing)
/** variable, no resisting, quick and simple */ /** variable, no resisting, quick and simple */
def drm(against: DamageCalculations.Selector) = new DamageResistanceModel { def drm(against: DamageCalculations.Selector): DamageResistanceModel = new DamageResistanceModel {
DamageUsing = against DamageUsing = against
ResistUsing = NoResistanceSelection ResistUsing = NoResistanceSelection
Model = SimpleResolutions.calculate Model = SimpleResolutions.calculate

View file

@ -1,11 +1,14 @@
// Copyright (c) 2021 PSForever // Copyright (c) 2021 PSForever
package net.psforever.objects.zones package net.psforever.objects.zones
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.vital.{Vitality, VitalityDefinition}
import net.psforever.objects.zones.blockmap.SectorPopulation import net.psforever.objects.zones.blockmap.SectorPopulation
trait InteractsWithZone trait InteractsWithZone
extends PlanetSideServerObject { extends PlanetSideServerObject
with Vitality {
/** interactions for this particular entity is allowed */ /** interactions for this particular entity is allowed */
private var _allowInteraction: Boolean = true private var _allowInteraction: Boolean = true
/** maximum interaction range used to generate the commonly tested sector */ /** maximum interaction range used to generate the commonly tested sector */
@ -47,12 +50,12 @@ trait InteractsWithZone
def interaction(): List[ZoneInteraction] = interactions def interaction(): List[ZoneInteraction] = interactions
def getInteractionSector(): SectorPopulation = { def getInteractionSector: SectorPopulation = {
this.Zone.blockMap.sector(this.Position, interactionRange) this.Zone.blockMap.sector(this.Position, interactionRange)
} }
def doInteractions(): Unit = { def doInteractions(): Unit = {
val sector = getInteractionSector() val sector = getInteractionSector
//println(sector.environmentList.map { _.attribute }.mkString(" ")) //println(sector.environmentList.map { _.attribute }.mkString(" "))
interactions.foreach { _.interaction(sector, target = this) } interactions.foreach { _.interaction(sector, target = this) }
} }
@ -66,6 +69,8 @@ trait InteractsWithZone
def resetInteractions(): Unit = { def resetInteractions(): Unit = {
interactions.foreach { _.resetInteraction(target = this) } interactions.foreach { _.resetInteraction(target = this) }
} }
override def Definition: ObjectDefinition with VitalityDefinition
} }
trait ZoneInteractionType trait ZoneInteractionType

View file

@ -273,6 +273,9 @@ class SectorGroup(
extends SectorPopulation extends SectorPopulation
object SectorGroup { 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 * Overloaded constructor that takes a single sector
* and transfers the lists of entities into a single conglomeration of the sector populations. * 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 = { def apply(sectors: Iterable[Sector]): SectorGroup = {
if (sectors.isEmpty) { if (sectors.isEmpty) {
SectorGroup(range = 0, sectors = Nil) SectorGroup.emptySector
} else if (sectors.size == 1) { } else if (sectors.size == 1) {
SectorGroup(sectors.head.rangeX, sectors.head.rangeY, sectors) SectorGroup(sectors.head.rangeX, sectors.head.rangeY, sectors)
} else { } else {

View file

@ -4,11 +4,12 @@ package net.psforever.packet.game
import net.psforever.newcodecs.newcodecs import net.psforever.newcodecs.newcodecs
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.{OxygenState, PlanetSideGUID} import net.psforever.types.{OxygenState, PlanetSideGUID}
import scodec.Codec import scodec.bits.BitVector
import scodec.{Attempt, Codec}
import scodec.codecs._ 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 guid the target
* @param progress the remaining countdown * @param progress the remaining countdown
* @param condition in what state of drowning the target is progressing * @param condition in what state of drowning the target is progressing
@ -41,8 +42,8 @@ final case class OxygenStateMessage(
vehicle: Option[DrowningTarget] vehicle: Option[DrowningTarget]
) extends PlanetSideGamePacket { ) extends PlanetSideGamePacket {
type Packet = OxygenStateMessage type Packet = OxygenStateMessage
def opcode = GamePacketOpcode.OxygenStateMessage def opcode: GamePacketOpcode.Type = GamePacketOpcode.OxygenStateMessage
def encode = OxygenStateMessage.encode(this) def encode: Attempt[BitVector] = OxygenStateMessage.encode(this)
} }
object DrowningTarget { object DrowningTarget {

View file

@ -7,7 +7,7 @@ import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.ce.Deployable import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.InventoryItem 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.sourcing.SourceEntry
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket

View file

@ -6,7 +6,7 @@ import net.psforever.objects.avatar.scoring.KDAStat
import net.psforever.objects.ballistics.Projectile import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.InventoryItem 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.sourcing.SourceEntry
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.objectcreate.ConstructorData

View file

@ -5,6 +5,7 @@ import base.ActorTest
import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.environment._ 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.vital.{Vitality, VitalityDefinition}
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneMap} import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneMap}
import net.psforever.types.{PlanetSideEmpire, Vector3} import net.psforever.types.{PlanetSideEmpire, Vector3}
@ -48,7 +49,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
val msg = testProbe.receiveOne(max = 250 milliseconds) val msg = testProbe.receiveOne(max = 250 milliseconds)
assert( assert(
msg match { msg match {
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) case InteractingWithEnvironment(b, _) => (b eq pool1)
case _ => false case _ => false
} }
) )
@ -67,7 +68,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
val msg1 = testProbe.receiveOne(max = 250 milliseconds) val msg1 = testProbe.receiveOne(max = 250 milliseconds)
assert( assert(
msg1 match { msg1 match {
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) case InteractingWithEnvironment(b, _) => (b eq pool1)
case _ => false case _ => false
} }
) )
@ -77,7 +78,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
val msg2 = testProbe.receiveOne(max = 250 milliseconds) val msg2 = testProbe.receiveOne(max = 250 milliseconds)
assert( assert(
msg2 match { msg2 match {
case EscapeFromEnvironment(o, b, _) => (o eq obj) && (b eq pool1) case EscapeFromEnvironment(b, _) => (b eq pool1)
case _ => false case _ => false
} }
) )
@ -96,7 +97,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
val msg1 = testProbe.receiveOne(max = 250 milliseconds) val msg1 = testProbe.receiveOne(max = 250 milliseconds)
assert( assert(
msg1 match { msg1 match {
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) case InteractingWithEnvironment(b, _) => (b eq pool1)
case _ => false case _ => false
} }
) )
@ -106,7 +107,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
val msg2 = testProbe.receiveOne(max = 250 milliseconds) val msg2 = testProbe.receiveOne(max = 250 milliseconds)
assert( assert(
msg2 match { msg2 match {
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool2) case InteractingWithEnvironment(b, _) => (b eq pool1)
case _ => false case _ => false
} }
) )
@ -124,7 +125,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
val msg1 = testProbe.receiveOne(max = 250 milliseconds) val msg1 = testProbe.receiveOne(max = 250 milliseconds)
assert( assert(
msg1 match { msg1 match {
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) case InteractingWithEnvironment(b, _) => (b eq pool1)
case _ => false case _ => false
} }
) )
@ -134,13 +135,13 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
val msgs = testProbe.receiveN(2, max = 250 milliseconds) val msgs = testProbe.receiveN(2, max = 250 milliseconds)
assert( assert(
msgs.head match { msgs.head match {
case EscapeFromEnvironment(o, b, _) => (o eq obj) && (b eq pool1) case EscapeFromEnvironment(b, _) => (b eq pool1)
case _ => false case _ => false
} }
) )
assert( assert(
msgs(1) match { msgs(1) match {
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool3) case InteractingWithEnvironment(b, _) => (b eq pool1)
case _ => false case _ => false
} }
) )
@ -159,7 +160,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
val msg1 = testProbe.receiveOne(max = 250 milliseconds) val msg1 = testProbe.receiveOne(max = 250 milliseconds)
assert( assert(
msg1 match { msg1 match {
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) case InteractingWithEnvironment(b, _) => (b eq pool1)
case _ => false case _ => false
} }
) )
@ -167,7 +168,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
obj.allowInteraction = false obj.allowInteraction = false
val msg2 = testProbe.receiveOne(max = 250 milliseconds) val msg2 = testProbe.receiveOne(max = 250 milliseconds)
msg2 match { msg2 match {
case EscapeFromEnvironment(o, b, _) => assert((o eq obj) && (b eq pool1)) case EscapeFromEnvironment(b, _) => (b eq pool1)
case _ => assert( false) case _ => assert( false)
} }
obj.zoneInteractions() obj.zoneInteractions()
@ -189,7 +190,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
val msg1 = testProbe.receiveOne(max = 250 milliseconds) val msg1 = testProbe.receiveOne(max = 250 milliseconds)
assert( assert(
msg1 match { msg1 match {
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1) case InteractingWithEnvironment(b, _) => (b eq pool1)
case _ => false case _ => false
} }
) )
@ -199,8 +200,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
object InteractsWithZoneEnvironmentTest { object InteractsWithZoneEnvironmentTest {
def testObject(): PlanetSideServerObject with InteractsWithZone = { def testObject(): PlanetSideServerObject with InteractsWithZone = {
new PlanetSideServerObject new PlanetSideServerObject
with InteractsWithZone with InteractsWithZone {
with Vitality {
interaction(new InteractWithEnvironment()) interaction(new InteractWithEnvironment())
def Faction: PlanetSideEmpire.Value = PlanetSideEmpire.VS def Faction: PlanetSideEmpire.Value = PlanetSideEmpire.VS
def DamageModel = null def DamageModel = null

View file

@ -16,7 +16,8 @@ import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.objects.zones.{Zone, ZoneMap}
import net.psforever.objects._ import net.psforever.objects._
import net.psforever.objects.serverobject.CommonMessages 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.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.base.DamageResolution import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.interaction.DamageInteraction
@ -812,7 +813,7 @@ class PlayerControlInteractWithWaterTest extends ActorTest {
msg_drown match { msg_drown match {
case AvatarServiceMessage( case AvatarServiceMessage(
"TestCharacter1", "TestCharacter1",
AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), OxygenState.Suffocation, 100f), _) AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), _, OxygenState.Suffocation, 100f), _)
) => true ) => true
case _ => false case _ => false
} }
@ -866,7 +867,7 @@ class PlayerControlStopInteractWithWaterTest extends ActorTest {
msg_drown match { msg_drown match {
case AvatarServiceMessage( case AvatarServiceMessage(
"TestCharacter1", "TestCharacter1",
AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), OxygenState.Suffocation, 100f), _) AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), _, OxygenState.Suffocation, 100f), _)
) => true ) => true
case _ => false case _ => false
} }
@ -879,7 +880,7 @@ class PlayerControlStopInteractWithWaterTest extends ActorTest {
msg_recover match { msg_recover match {
case AvatarServiceMessage( case AvatarServiceMessage(
"TestCharacter1", "TestCharacter1",
AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), OxygenState.Recovery, _), _) AvatarAction.OxygenState(OxygenStateTarget(PlanetSideGUID(1), _, OxygenState.Recovery, _), _)
) => true ) => true
case _ => false case _ => false
} }

View file

@ -14,6 +14,8 @@ import net.psforever.objects.guid.NumberPoolHub
import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.environment._ 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.serverobject.mount.Mountable
import net.psforever.objects.vehicles.VehicleLockState import net.psforever.objects.vehicles.VehicleLockState
import net.psforever.objects.vehicles.control.VehicleControl import net.psforever.objects.vehicles.control.VehicleControl
@ -617,10 +619,9 @@ class VehicleControlInteractWithWaterPartialTest extends ActorTest {
assert( assert(
msg_drown match { msg_drown match {
case InteractingWithEnvironment( case InteractingWithEnvironment(
p1,
p2, p2,
Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f)) Some(OxygenStateTarget(PlanetSideGUID(2), _, OxygenState.Suffocation, 100f))
) => (p1 eq player1) && (p2 eq pool) ) => (p2 eq pool)
case _ => false case _ => false
} }
) )
@ -679,8 +680,8 @@ class VehicleControlInteractWithWaterTest extends ActorTest {
case AvatarServiceMessage( case AvatarServiceMessage(
"TestCharacter1", "TestCharacter1",
AvatarAction.OxygenState( AvatarAction.OxygenState(
OxygenStateTarget(PlanetSideGUID(1), OxygenState.Suffocation, 100f), OxygenStateTarget(PlanetSideGUID(1), _, OxygenState.Suffocation, 100f),
Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f)) Some(OxygenStateTarget(PlanetSideGUID(2), _, OxygenState.Suffocation, 100f))
) )
) => true ) => true
case _ => false case _ => false
@ -742,10 +743,9 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest {
assert( assert(
msg_drown match { msg_drown match {
case InteractingWithEnvironment( case InteractingWithEnvironment(
p1,
p2, p2,
Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f)) Some(OxygenStateTarget(PlanetSideGUID(2), _, OxygenState.Suffocation, 100f))
) => (p1 eq player1) && (p2 eq pool) ) => (p2 eq pool)
case _ => false case _ => false
} }
) )
@ -756,10 +756,9 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest {
assert( assert(
msg_recover match { msg_recover match {
case EscapeFromEnvironment( case EscapeFromEnvironment(
p1,
p2, p2,
Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Recovery, _)) Some(OxygenStateTarget(PlanetSideGUID(2), _, OxygenState.Recovery, _))
) => (p1 eq player1) && (p2 eq pool) ) => (p2 eq pool)
case _ => false case _ => false
} }
) )