completely retooled water interactions for players and vehicles to handle a wading status, before drowning; when player with llu or vehicle with player with llu come into contact with water in this wading state, the llu is lost and the facility hack ends; adding messages to support this and differentiate from an llu facility capture timeout

This commit is contained in:
Fate-JH 2024-08-19 13:59:10 -04:00
parent d522a09335
commit a4084e52ce
18 changed files with 646 additions and 141 deletions

View file

@ -51,7 +51,6 @@ import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, Drive
import net.psforever.util.Config
import scala.concurrent.duration._
import scala.util.Success
object GeneralLogic {
def apply(ops: GeneralOperations): GeneralLogic = {
@ -170,6 +169,9 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
}
case None => ()
}
//llu destruction check
sessionLogic.localResponse.loseFlagViolently(ops.specialItemSlotGuid, player)
//
val eagleEye: Boolean = ops.canSeeReallyFar
val isNotVisible: Boolean = sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing ||
(player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime)

View file

@ -414,6 +414,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed)
if obj.DeploymentState == DriveState.AutoPilot =>
//todo @Vehicle_CannotBailInWarpgateEnvelope
sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotBailAtThisTime"))
log.warn(s"DismountVehicleMsg: ${tplayer.Name} can not bail from $obj's when in autopilot")

View file

@ -4,6 +4,7 @@ package net.psforever.actors.session.normal
import akka.actor.{ActorContext, ActorRef, typed}
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, SessionVehicleHandlers, VehicleHandlerFunctions}
import net.psforever.objects.avatar.SpecialCarry
import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles}
import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit}
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
@ -62,6 +63,13 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context:
player.Orientation = orient
player.Velocity = vel
sessionLogic.updateLocalBlockMap(pos)
if (player.Carrying.contains(SpecialCarry.CaptureFlag)) {
continent
.GUID(player.VehicleSeated)
.collect { case vehicle: Vehicle =>
sessionLogic.localResponse.loseFlagViolently(sessionLogic.general.specialItemSlotGuid, vehicle)
}
}
case VehicleResponse.VehicleState(
vehicleGuid,

View file

@ -70,6 +70,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
obj.Velocity = None
obj.Flying = None
}
//
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.VehicleState(

View file

@ -2,12 +2,17 @@
package net.psforever.actors.session.support
import akka.actor.ActorContext
import net.psforever.objects.{Players, TurretDeployable}
import net.psforever.objects.{PlanetSideGameObject, Players, TurretDeployable}
import net.psforever.objects.ce.Deployable
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.serverobject.environment.EnvironmentAttribute
import net.psforever.objects.serverobject.environment.interaction.InteractWithEnvironment
import net.psforever.objects.serverobject.environment.interaction.common.Watery
import net.psforever.objects.serverobject.interior.Sidedness
import net.psforever.objects.serverobject.llu.CaptureFlag
import net.psforever.objects.zones.InteractsWithZone
import net.psforever.packet.game.GenericObjectActionMessage
import net.psforever.services.local.LocalResponse
import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage}
import net.psforever.types.PlanetSideGUID
trait LocalHandlerFunctions extends CommonSessionInterfacingFunctionality {
@ -47,4 +52,40 @@ class SessionLocalHandlers(
else
400f
}
/**
* na
* @param target evaluate this to determine if to continue with this loss
* @return whether or not we are sufficiently submerged in water
*/
def wadingInWater(target: PlanetSideGameObject with InteractsWithZone): Boolean = {
target
.interaction()
.collectFirst {
case env: InteractWithEnvironment =>
env
.Interactions
.get(EnvironmentAttribute.Water)
.collectFirst {
case water: Watery => water.Depth > 0f
}
}
.flatten
.contains(true)
}
/**
* na
* @param flagGuid flag that may exist
* @param target evaluate this to determine if to continue with this loss
*/
def loseFlagViolently(flagGuid: Option[PlanetSideGUID], target: PlanetSideGameObject with InteractsWithZone): Unit = {
continent
.GUID(flagGuid)
.collect {
case flag: CaptureFlag if wadingInWater(target) =>
flag.Destroyed = true
continent.LocalEvents ! LocalServiceMessage("", LocalAction.LluLost(flag))
}
}
}

View file

@ -1,11 +1,11 @@
// Copyright (c) 2024 PSForever
package net.psforever.objects.avatar.interaction
import net.psforever.objects.Player
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, 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.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment, interaction}
import net.psforever.objects.zones.InteractsWithZone
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.OxygenState
@ -15,9 +15,11 @@ import scala.concurrent.duration._
class WithWater(val channel: String)
extends InteractionWith
with Watery {
/** do this every time we're in sufficient contact with water */
private var doInteractingWithBehavior: (InteractsWithZone, PieceOfEnvironment, Option[Any]) => Unit = wadingBeforeDrowning
/**
* Water causes players to slowly suffocate.
* When they (finally) drown, they will die.
* Water is wet.
* @param obj the target
* @param body the environment
*/
@ -26,22 +28,84 @@ class WithWater(val channel: String)
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)
if (getExtra(data).nonEmpty) {
inheritAndPushExtraData(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))
depth = math.max(0f, body.collision.altitude - obj.Position.z)
doInteractingWithBehavior(obj, body, data)
obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = 500 milliseconds, obj.Actor, interaction.InteractingWithEnvironment(body, Some("wading")))
}
}
/**
* Wading only happens while the player's head is above the water.
* @param obj the target
* @param body the environment
*/
private def wadingBeforeDrowning(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
//we're already "wading", let's see if we're drowning
if (depth >= GlobalDefinitions.MaxDepth(obj)) {
//drowning
beginDrowning(obj, body, data)
} else {
//inform the player that their mounted vehicle is in trouble (that they are in trouble (but not from drowning (yet)))
val extra = getExtra(data)
if (extra.nonEmpty) {
displayOxygenState(
obj,
condition.getOrElse(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, 95f)),
extra
)
}
}
}
/**
* Too much water causes players to slowly suffocate.
* When they (finally) drown, they will die.
* @param obj the target
* @param body the environment
*/
private def beginDrowning(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
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.StopTimer(WithWater.WaterAction)
obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, Player.Die())
//inform the player that they are in trouble
displayOxygenState(obj, cond, getExtra(data))
doInteractingWithBehavior = drowning
}
}
/**
* Too much water causes players to slowly suffocate.
* When they (finally) drown, they will die.
* @param obj the target
* @param body the environment
*/
private def drowning(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
//test if player ever gets head above the water level
if (depth < GlobalDefinitions.MaxDepth(obj)) {
val (_, _, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime)
//switch to recovery
if (percentage > 0) {
recoverFromDrowning(obj, body, data)
doInteractingWithBehavior = recoverFromDrowning
}
}
}
@ -52,16 +116,22 @@ class WithWater(val channel: String)
* @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)
if (percentage > 99f) {
recoverFromInteracting(obj)
private def recoverFromDrowning(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
val state = condition.map(_.state)
if (state.contains(OxygenState.Suffocation)) {
//set up for recovery
val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime)
if (percentage < 99f) {
//we're not too far gone
recoverFromDrowning(obj, body, data, effect, time, percentage)
}
doInteractingWithBehavior = recovering
} else {
stopInteractingAction(obj, body, data, effect, time, percentage)
doInteractingWithBehavior = wadingBeforeDrowning
}
}
@ -74,38 +144,169 @@ class WithWater(val channel: String)
* @param time current time until completion of the next effect
* @param percentage value to display in the drowning UI progress bar
*/
private def stopInteractingAction(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any],
effect: Boolean,
time: Long,
percentage: Float
): Unit = {
private def recoverFromDrowning(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any],
effect: Boolean,
time: Long,
percentage: Float
): Unit = {
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 (effect) {
if (effect) {
condition = Some(cond)
waterInteractionTime = System.currentTimeMillis() + time
obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute))
obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction)
obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute))
//inform the player
obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra))
displayOxygenState(obj, cond, extra)
} else if (extra.isDefined) {
//inform the player
obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra))
displayOxygenState(obj, cond, extra)
}
}
/**
* The recovery period is much faster than the drowning process.
* Check for when the player fully recovers,
* and that the player does not regress back to drowning.
* @param obj the target
* @param body the environment
*/
def recovering(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
lazy val state = condition.map(_.state)
if (depth >= GlobalDefinitions.MaxDepth(obj)) {
//go back to drowning
beginDrowning(obj, body, data)
} else if (state.contains(OxygenState.Recovery)) {
//check recovery conditions
val (_, _, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime)
if (percentage < 1f) {
doInteractingWithBehavior = wadingBeforeDrowning
}
}
}
/**
* When out of water, the player is no longer suffocating.
* He's even stopped wading.
* The only thing we should let complete now is recovery.
* @param obj the target
* @param body the environment
*/
override def stopInteractingWith(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
if (getExtra(data).nonEmpty) {
inheritAndPushExtraData(obj, body, data)
} else {
stopInteractingWithAction(obj, body, data)
}
}
/**
* When out of water, the player is no longer suffocating.
* He's even stopped wading.
* The only thing we should let complete now is recovery.
* @param obj the target
* @param body the environment
*/
private def stopInteractingWithAction(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
val cond = condition.map(_.state)
if (cond.contains(OxygenState.Suffocation)) {
//go from suffocating to recovery
recoverFromDrowning(obj, body, data)
} else if (cond.isEmpty) {
//neither suffocating nor recovering, so just reset everything
recoverFromInteracting(obj)
obj.Actor ! RespondsToZoneEnvironment.StopTimer(attribute)
waterInteractionTime = 0L
depth = 0f
condition = None
doInteractingWithBehavior = wadingBeforeDrowning
}
}
override def recoverFromInteracting(obj: InteractsWithZone): Unit = {
super.recoverFromInteracting(obj)
if (condition.exists(_.state == OxygenState.Suffocation)) {
val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime)
stopInteractingAction(obj, condition.map(_.body).get, None, effect, time, percentage)
val cond = condition.map(_.state)
//whether or not we were suffocating or recovering, we need to undo the visuals for that
if (cond.nonEmpty) {
obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction)
displayOxygenState(
obj,
OxygenStateTarget(obj.GUID, condition.map(_.body).get, OxygenState.Recovery, 100f),
None
)
}
waterInteractionTime = 0L
condition = None
}
/**
* From the "condition" of someone else's drowning status,
* extract target information and progress.
* @param data any information
* @return target information and drowning progress
*/
private def getExtra(data: Option[Any]): Option[OxygenStateTarget] = {
data.collect {
case t: OxygenStateTarget => Some(t)
case w: Watery => w.Condition
}.flatten
}
/**
* Send the message regarding drowning and recovery
* that includes additional information about a related target that is drowning or recovering.
* @param obj the target
* @param body the environment
* @param data essential information about someone else's interaction with water
*/
private def inheritAndPushExtraData(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
val state = condition.map(_.state).getOrElse(OxygenState.Recovery)
val Some((_, _, percentage)) = state match {
case OxygenState.Suffocation => Some(Watery.drowningInWateryConditions(obj, Some(state), waterInteractionTime))
case OxygenState.Recovery => Some(Watery.recoveringFromWateryConditions(obj, Some(state), waterInteractionTime))
}
displayOxygenState(obj, OxygenStateTarget(obj.GUID, body, state, percentage), getExtra(data))
}
/**
* Send the message regarding drowning and recovery.
* @param obj the target
* @param cond the environment
*/
private def displayOxygenState(
obj: InteractsWithZone,
cond: OxygenStateTarget,
data: Option[OxygenStateTarget]
): Unit = {
obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, data))
}
}
object WithWater {
/** special environmental trait to queue actions independent from the primary wading test */
case object WaterAction extends EnvironmentTrait {
override def canInteractWith(obj: PlanetSideGameObject): Boolean = false
override def testingDepth: Float = Float.PositiveInfinity
}
}

View file

@ -10,6 +10,8 @@ import net.psforever.types.Vector3
*/
abstract class EnvironmentTrait {
def canInteractWith(obj: PlanetSideGameObject): Boolean
def testingDepth: Float
}
object EnvironmentAttribute {
@ -25,16 +27,22 @@ object EnvironmentAttribute {
case _ => false
})
}
def testingDepth: Float = 0.2f
}
case object Lava extends EnvironmentTrait {
/** lava can only interact with anything capable of registering damage */
def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithDamagingFields(obj)
val testingDepth: Float = 0f
}
case object Death extends EnvironmentTrait {
/** death can only interact with anything capable of registering damage */
def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithDamagingFields(obj)
val testingDepth: Float = 0f
}
case object GantryDenialField
@ -46,18 +54,24 @@ object EnvironmentAttribute {
case _ => false
}
}
val testingDepth: Float = 0f
}
case object MovementFieldTrigger
extends EnvironmentTrait {
/** only interact with living player characters or vehicles */
def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj)
val testingDepth: Float = 0f
}
case object InteriorField
extends EnvironmentTrait {
/** only interact with living player characters or vehicles */
def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj)
val testingDepth: Float = 0f
}
/**

View file

@ -1,7 +1,6 @@
// 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._
@ -117,9 +116,8 @@ object InteractWithEnvironment {
obj: PlanetSideServerObject,
sector: SectorPopulation
): Set[PieceOfEnvironment] = {
val depth = GlobalDefinitions.MaxDepth(obj)
sector.environmentList
.filter(body => body.attribute.canInteractWith(obj) && body.testInteraction(obj, depth))
.filter(body => body.attribute.canInteractWith(obj) && body.testInteraction(obj, body.attribute.testingDepth))
.distinctBy(_.attribute)
.toSet
}
@ -136,7 +134,7 @@ object InteractWithEnvironment {
body: PieceOfEnvironment,
obj: PlanetSideServerObject
): Option[PieceOfEnvironment] = {
if ((obj.Zone eq zone) && body.testInteraction(obj, GlobalDefinitions.MaxDepth(obj))) {
if ((obj.Zone eq zone) && body.testInteraction(obj, body.attribute.testingDepth)) {
Some(body)
} else {
None

View file

@ -8,13 +8,16 @@ 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
/** how far the player's feet are below the surface of the water */
protected var depth: Float = 0f
/** how far the player's feet are below the surface of the water */
def Depth: Float = depth
}
object Watery {

View file

@ -34,6 +34,7 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity {
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var carrier: Option[Player] = None
private var lastTimeCollected: Long = System.currentTimeMillis()
private val spawnedTime: Long = lastTimeCollected
def Target: Building = target
def Target_=(newTarget: Building): Building = {
@ -56,10 +57,11 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity {
* When the flag is carried by a player, the position returned should be that of the carrier not the flag.
* @return the position of the carrier, if there is a player carrying the flag, or the flag itself
*/
override def Position: Vector3 = if (Carrier.nonEmpty) {
carrier.get.Position
} else {
super.Position
override def Position: Vector3 = {
carrier match {
case Some(player) => player.Position
case None => super.Position
}
}
def Carrier: Option[Player] = carrier
@ -70,6 +72,8 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity {
}
def LastCollectionTime: Long = carrier.map { _ => lastTimeCollected }.getOrElse { System.currentTimeMillis() }
def InitialSpawnTime: Long = spawnedTime
}
object CaptureFlag {

View file

@ -606,9 +606,9 @@ class VehicleControl(vehicle: Vehicle)
c
}
}
watery.doInteractingWithTargets(player, percentage, watery.Condition.map(_.body).get, List(player))
WithWater.doInteractingWithTargets(player, percentage, watery.Condition.map(_.body).get, List(player))
case watery: WithWater if watery.Condition.map(_.state).contains(OxygenState.Recovery) =>
watery.stopInteractingWithTargets(
WithWater.stopInteractingWithTargets(
player,
Watery.recoveringFromWater(vehicle, watery)._3,
watery.Condition.map(_.body).get,

View file

@ -1,37 +1,71 @@
// Copyright (c) 2024 PSForever
package net.psforever.objects.vehicles.interaction
import net.psforever.objects.{GlobalDefinitions, Vehicle}
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, 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.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment, interaction}
import net.psforever.objects.vehicles.control.VehicleControl
import net.psforever.objects.zones.InteractsWithZone
import net.psforever.types.OxygenState
import scala.annotation.unused
import scala.concurrent.duration._
class WithWater()
extends InteractionWith
with Watery {
/** do this every time we're in sufficient contact with water */
private var doInteractingWithBehavior: (InteractsWithZone, PieceOfEnvironment, Option[Any]) => Unit = wadingBeforeDrowning
/**
* 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
*/
* Water is wet.
* @param obj the target
* @param body the environment
*/
def doInteractingWith(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
depth = math.max(0f, body.collision.altitude - obj.Position.z)
doInteractingWithBehavior(obj, body, data)
obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = 500 milliseconds, obj.Actor, interaction.InteractingWithEnvironment(body, Some("wading")))
}
/**
* Wading only happens while the vehicle's wheels are mostly above the water.
* @param obj the target
* @param body the environment
*/
private def wadingBeforeDrowning(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
//we're already "wading", let's see if we're drowning
if (depth >= GlobalDefinitions.MaxDepth(obj)) {
//drowning
beginDrowning(obj, body, data)
}
}
/**
* Too much water causes players to slowly suffocate.
* When they (finally) drown, they will die.
* @param obj the target
* @param body the environment
*/
private def beginDrowning(
obj: InteractsWithZone,
body: PieceOfEnvironment,
@unused data: Option[Any]
): Unit = {
obj match {
case vehicle: Vehicle =>
val (effect: Boolean, time: Long, percentage: Float) = {
val (effect, time, percentage): (Boolean, Long, Float) = {
val (a, b, c) = Watery.drowningInWateryConditions(obj, condition.map(_.state), waterInteractionTime)
if (a && GlobalDefinitions.isFlightVehicle(vehicle.Definition)) {
(true, 0L, 0f) //no progress bar
@ -42,8 +76,94 @@ class WithWater()
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.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction)
obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, VehicleControl.Disable(true))
WithWater.doInteractingWithTargets(
obj,
percentage,
body,
vehicle.Seats.values.flatMap(_.occupants).filter(p => p.isAlive && (p.Zone eq obj.Zone))
)
doInteractingWithBehavior = drowning
}
case _ => ()
}
}
/**
* Too much water causes vehicles to slowly disable.
* When fully waterlogged, the vehicle is completely immobile.
* @param obj the target
* @param body the environment
*/
private def drowning(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
//test if player ever gets head above the water level
if (depth < GlobalDefinitions.MaxDepth(obj)) {
val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime)
//switch to recovery
if (percentage > 0) {
recoverFromDrowning(obj, body, data, effect, time, percentage)
doInteractingWithBehavior = recovering
}
}
}
/**
* When out of water, the vehicle is no longer being waterlogged.
* It does have to endure a recovery period to get back to normal, though.
* @param obj the target
* @param body the environment
*/
private def recoverFromDrowning(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
val state = condition.map(_.state)
if (state.contains(OxygenState.Suffocation)) {
//set up for recovery
val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime)
if (percentage < 99f) {
//we're not too far gone
recoverFromDrowning(obj, body, data, effect, time, percentage)
}
doInteractingWithBehavior = recovering
} else {
doInteractingWithBehavior = wadingBeforeDrowning
}
}
/**
* When out of water, the vehicle is no longer being waterlogged.
* It does have to endure a recovery period to get back to normal, though.
* @param obj the target
* @param body the environment
* @param effect na
* @param time current time until completion of the next effect
* @param percentage value to display in the drowning UI progress bar
*/
private def recoverFromDrowning(
obj: InteractsWithZone,
body: PieceOfEnvironment,
@unused data: Option[Any],
effect: Boolean,
time: Long,
percentage: Float
): Unit = {
obj match {
case vehicle: Vehicle =>
val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage)
if (effect) {
condition = Some(cond)
waterInteractionTime = System.currentTimeMillis() + time
obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction)
obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute))
//inform the players
WithWater.stopInteractingWithTargets(
obj,
percentage,
body,
@ -55,34 +175,52 @@ class WithWater()
}
/**
* 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
*/
* The recovery period is much faster than the waterlogging process.
* Check for when the vehicle fully recovers,
* and that the vehicle does not regress back to waterlogging.
* @param obj the target
* @param body the environment
*/
def recovering(
obj: InteractsWithZone,
body: PieceOfEnvironment,
data: Option[Any]
): Unit = {
lazy val state = condition.map(_.state)
if (depth >= GlobalDefinitions.MaxDepth(obj)) {
//go back to drowning
beginDrowning(obj, body, data)
} else if (state.contains(OxygenState.Recovery)) {
//check recovery conditions
val (_, _, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime)
if (percentage < 1f) {
doInteractingWithBehavior = wadingBeforeDrowning
}
}
}/**
* 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
*/
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 _ => ()
val cond = condition.map(_.state)
if (cond.contains(OxygenState.Suffocation)) {
//go from suffocating to recovery
recoverFromDrowning(obj, body, data)
} else if (cond.isEmpty) {
//neither suffocating nor recovering, so just reset everything
recoverFromInteracting(obj)
obj.Actor ! RespondsToZoneEnvironment.StopTimer(attribute)
waterInteractionTime = 0L
depth = 0f
condition = None
doInteractingWithBehavior = wadingBeforeDrowning
}
}
@ -91,46 +229,53 @@ class WithWater()
if (condition.exists(_.state == OxygenState.Suffocation)) {
stopInteractingWith(obj, condition.map(_.body).get, None)
}
waterInteractionTime = 0L
condition = None
}
}
object WithWater {
/** special environmental trait to queue actions independent from the primary wading test */
case object WaterAction extends EnvironmentTrait {
override def canInteractWith(obj: PlanetSideGameObject): Boolean = false
override def testingDepth: Float = Float.PositiveInfinity
}
/**
* 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
*/
* 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 = {
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
*/
* 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 = {
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

@ -134,7 +134,17 @@ object OxygenStateMessage extends Marshallable[OxygenStateMessage] {
204.8f,
11
) :: //hackish: 2^11 == 2047, so it should be 204.7; but, 204.8 allows decode == encode
OxygenState.codec
bool.xmap[OxygenState](
{
case false => OxygenState.Recovery
case true => OxygenState.Suffocation
},
{
case OxygenState.Recovery => false
case OxygenState.Suffocation => true
case _ => false
}
)
).as[DrowningTarget]
implicit val codec: Codec[OxygenStateMessage] = (

View file

@ -120,6 +120,8 @@ class LocalService(zone: Zone) extends Actor {
hackCapturer ! HackCaptureActor.StartCaptureTerminalHack(target, zone, 0, 8L)
case LocalAction.LluCaptured(llu) =>
hackCapturer ! HackCaptureActor.FlagCaptured(llu)
case LocalAction.LluLost(llu) =>
hackCapturer ! HackCaptureActor.FlagLost(llu)
case LocalAction.LluSpawned(player_guid, llu) =>
// Forward to all clients to create object locally

View file

@ -61,6 +61,7 @@ object LocalAction {
final case class ResecureCaptureTerminal(target: CaptureTerminal, hacker: PlayerSource) extends Action
final case class StartCaptureTerminalHack(target: CaptureTerminal) extends Action
final case class LluCaptured(llu: CaptureFlag) extends Action
final case class LluLost(llu: CaptureFlag) extends Action
final case class LluSpawned(player_guid: PlanetSideGUID, llu: CaptureFlag) extends Action
final case class LluDespawned(player_guid: PlanetSideGUID, guid: PlanetSideGUID, position: Vector3) extends Action

View file

@ -98,6 +98,12 @@ class CaptureFlagManager(zone: Zone) extends Actor {
flag.Zone,
CaptureFlagChatMessageStrings.CTF_Failed_TimedOut(building.Name, flag.Target.Name, flag.Faction)
)
case CaptureFlagLostReasonEnum.FlagLost =>
val building = flag.Owner.asInstanceOf[Building]
ChatBroadcast(
flag.Zone,
CaptureFlagChatMessageStrings.CTF_Failed_FlagLost(building.Name, flag.Faction)
)
case CaptureFlagLostReasonEnum.Ended =>
()
}
@ -197,6 +203,7 @@ class CaptureFlagManager(zone: Zone) extends Actor {
}
private def ChatBroadcast(zone: Zone, message: String, fanfare: Boolean = true): Unit = {
//todo use UNK_222 sometimes
val messageType: ChatMessageType = if (fanfare) {
ChatMessageType.UNK_223
} else {
@ -224,15 +231,6 @@ object CaptureFlagManager {
}
object CaptureFlagChatMessageStrings {
/*
@CTF_Failed_TargetLost=%1's LLU target facility %2 was lost!\nHack canceled!
@CTF_Failed_FlagLost=The %1 lost %2's LLU!\nHack canceled!
@CTF_Warning_Carrier=%1 of the %2 has %3's LLU.\nIt must be taken to %4 within %5 minutes!
@CTF_Warning_NoCarrier=%1's LLU is in the field.\nThe %2 must take it to %3 within %4 minutes!
@CTF_Warning_Carrier1Min=%1 of the %2 has %3's LLU.\nIt must be taken to %4 within the next minute!
@CTF_Warning_NoCarrier1Min=%1's LLU is in the field.\nThe %2 must take it to %3 within the next minute!
*/
// @CTF_Success=%1 captured %2's LLU for the %3!
/** {player.Name} captured {ownerName}'s LLU for the {player.Faction}! */
private[support] def CTF_Success(playerName:String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String =
@ -243,10 +241,20 @@ object CaptureFlagChatMessageStrings {
private[support] def CTF_Failed_TimedOut(ownerName: String, name: String, faction: PlanetSideEmpire.Value): String =
s"@CTF_Failed_TimedOut^@${GetFactionString(faction)}~^@$ownerName~^@$name~"
// @CTF_Failed_Lost=The %1 lost %2's LLU!\nHack canceled!
/** The {faction} lost {ownerName}'s LLU!\nHack canceled! */
private[support] def CTF_Failed_FlagLost(ownerName: String, faction: PlanetSideEmpire.Value): String =
s"@CTF_Failed_FlagLost^@${GetFactionString(faction)}~^@$ownerName~"
// @CTF_Failed_TargetLost=%1's LLU target facility %2 was lost!\nHack canceled!
/** {hackFacility}'s LLU target facility {targetFacility} was lost!\nHack canceled! */
private[support] def CTF_Failed_TargetLost(hackFacility: String, targetFacility: String): String =
s"@CTF_Failed_TargetLost^@$hackFacility~^@$targetFacility~"
// @CTF_Failed_SourceResecured=The %1 resecured %2!\nThe LLU was lost!
/** The {faction} resecured {name}!\nThe LLU was lost! */
private[support] def CTF_Failed_SourceResecured(name: String, faction: PlanetSideEmpire.Value): String =
s"@CTF_Failed_SourceResecured^@${CaptureFlagChatMessageStrings.GetFactionString(faction)}~^@$name~"
s"@CTF_Failed_SourceResecured^@${GetFactionString(faction)}~^@$name~"
// @CTF_FlagSpawned=%1 %2 has spawned a LLU.\nIt must be taken to %3 %4's Control Console within %5 minutes or the hack will fail!
/** {facilityType} {facilityName} has spawned a LLU.\nIt must be taken to {targetFacilityType} {targetFacilityName}'s Control Console within 15 minutes or the hack will fail! */
@ -256,12 +264,56 @@ object CaptureFlagChatMessageStrings {
// @CTF_FlagPickedUp=%1 of the %2 picked up %3's LLU
/** {player.Name} of the {player.Faction} picked up {ownerName}'s LLU */
def CTF_FlagPickedUp(playerName: String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String =
s"@CTF_FlagPickedUp^$playerName~^@${CaptureFlagChatMessageStrings.GetFactionString(playerFaction)}~^@$ownerName~"
s"@CTF_FlagPickedUp^$playerName~^@${GetFactionString(playerFaction)}~^@$ownerName~"
// @CTF_FlagDropped=%1 of the %2 dropped %3's LLU
/** {playerName} of the {faction} dropped {facilityName}'s LLU */
def CTF_FlagDropped(playerName: String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String =
s"@CTF_FlagDropped^$playerName~^@${CaptureFlagChatMessageStrings.GetFactionString(playerFaction)}~^@$ownerName~"
s"@CTF_FlagDropped^$playerName~^@${GetFactionString(playerFaction)}~^@$ownerName~"
// @CTF_Warning_Carrier=%1's LLU is in the field.\nThe %2 must take it to %3 within %4 minutes!
/** {facilityName}'s LLU is in the field.\nThe {faction} must take it to {targetFacilityName} within {time} minutes! */
def CTF_Warning_Carrier(
playerName:String,
playerFaction: PlanetSideEmpire.Value,
facilityName: String,
targetFacilityName: String,
time: Int
): String = {
s"@CTF_Warning_Carrier^$playerName~^@${GetFactionString(playerFaction)}~^@$facilityName~^@$targetFacilityName~^@$time~"
}
// @CTF_Warning_Carrier1Min=%1 of the %2 has %3's LLU.\nIt must be taken to %4 within the next minute!
/** {playerName} of the {faction} has {facilityName}'s LLU.\nIt must be taken to {targetFacilityName} within the next minute! */
def CTF_Warning_Carrier1Min(
playerName:String,
playerFaction: PlanetSideEmpire.Value,
facilityName: String,
targetFacilityName: String
): String = {
s"@CTF_Warning_Carrier1Min^$playerName~^@${GetFactionString(playerFaction)}~^@$facilityName~^@$targetFacilityName~"
}
// @CTF_Warning_NoCarrier=%1's LLU is in the field.\nThe %2 must take it to %3 within %4 minutes!
/** {facilityName}'s LLU is in the field.\nThe {faction} must take it to {targetFacilityName} within {time} minute! */
def CTF_Warning_NoCarrier(
facilityName: String,
playerFaction: PlanetSideEmpire.Value,
targetFacilityName: String,
time: Int
): String = {
s"@CTF_Warning_NoCarrier^@$facilityName~^@${GetFactionString(playerFaction)}~^@$targetFacilityName~^$time~"
}
// @CTF_Warning_NoCarrier1Min=%1's LLU is in the field.\nThe %2 must take it to %3 within the next minute!
/** {facilityName}'s LLU is in the field.\nThe {faction} must take it to {targetFacilityName} within the next minute! */
def CTF_Warning_NoCarrier1Min(
facilityName: String,
playerFaction: PlanetSideEmpire.Value,
targetFacilityName: String
): String = {
s"@CTF_Warning_NoCarrier1Min^@$facilityName~^@${GetFactionString(playerFaction)}~^@$targetFacilityName~"
}
private def GetFactionString: PlanetSideEmpire.Value=>String = {
case PlanetSideEmpire.TR => "TerranRepublic"
@ -277,4 +329,5 @@ object CaptureFlagLostReasonEnum {
final case object Resecured extends CaptureFlagLostReasonEnum
final case object TimedOut extends CaptureFlagLostReasonEnum
final case object Ended extends CaptureFlagLostReasonEnum
final case object FlagLost extends CaptureFlagLostReasonEnum
}

View file

@ -62,9 +62,14 @@ class HackCaptureActor extends Actor {
// If the base has a socket, but no flag spawned it means the hacked base is neutral with no friendly neighbouring bases to deliver to, making it a timed hack.
val building = terminal.Owner.asInstanceOf[Building]
building.GetFlag match {
case Some(llu) if llu.LastCollectionTime == llu.InitialSpawnTime =>
// LLU was never once collected. Send resecured notifications
terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(llu, CaptureFlagLostReasonEnum.TimedOut)
NotifyHackStateChange(terminal, isResecured = true)
case Some(llu) =>
// LLU was not delivered in time. Send resecured notifications
terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(llu, CaptureFlagLostReasonEnum.TimedOut)
terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(llu, CaptureFlagLostReasonEnum.FlagLost)
NotifyHackStateChange(terminal, isResecured = true)
case _ =>
@ -138,6 +143,26 @@ class HackCaptureActor extends Actor {
log.error(s"Attempted LLU capture for ${flag.Owner.asInstanceOf[Building].Name} but CC GUID ${flag.Owner.asInstanceOf[Building].CaptureTerminal.get.GUID} was not in list of hacked objects")
}
case HackCaptureActor.FlagLost(flag) =>
val terminalOpt = flag.Owner.asInstanceOf[Building].CaptureTerminal
hackedObjects
.find(entry => terminalOpt.contains(entry.target))
.collect { entry =>
val terminal = terminalOpt.get
hackedObjects = hackedObjects.filterNot(x => x == entry)
log.info(s"FlagLost: ${flag.Carrier.map(_.Name).getOrElse("")} the flag carrier screwed up the capture for ${flag.Target.Name} and the LLU has been lost")
terminal.Actor ! CommonMessages.ClearHack()
NotifyHackStateChange(terminal, isResecured = true)
// If there's hacked objects left in the list restart the timer with the shortest hack time left
RestartTimer()
}
.orElse{
log.warn(s"FlagLost: flag data does not match to an entry in the hacked objects list")
None
}
context.parent ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.FlagLost)
case _ => ()
}
@ -256,6 +281,7 @@ object HackCaptureActor {
final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource)
final case class FlagCaptured(flag: CaptureFlag)
final case class FlagLost(flag: CaptureFlag)
private final case class ProcessCompleteHacks()

View file

@ -1,9 +1,6 @@
package net.psforever.types
import enumeratum.{Enum, EnumEntry}
import net.psforever.packet.PacketHelpers
import scodec.Codec
import scodec.codecs.uint
/**
* The progress state of being a drowning victim.
@ -22,6 +19,4 @@ object OxygenState extends Enum[OxygenState] {
case object Recovery extends OxygenState
case object Suffocation extends OxygenState
implicit val codec: Codec[OxygenState] = PacketHelpers.createEnumCodec(e = this, uint(bits = 1))
}