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 net.psforever.util.Config
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.Success
object GeneralLogic { object GeneralLogic {
def apply(ops: GeneralOperations): GeneralLogic = { def apply(ops: GeneralOperations): GeneralLogic = {
@ -170,6 +169,9 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
} }
case None => () case None => ()
} }
//llu destruction check
sessionLogic.localResponse.loseFlagViolently(ops.specialItemSlotGuid, player)
//
val eagleEye: Boolean = ops.canSeeReallyFar val eagleEye: Boolean = ops.canSeeReallyFar
val isNotVisible: Boolean = sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing || val isNotVisible: Boolean = sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing ||
(player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime) (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) case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed)
if obj.DeploymentState == DriveState.AutoPilot => if obj.DeploymentState == DriveState.AutoPilot =>
//todo @Vehicle_CannotBailInWarpgateEnvelope
sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotBailAtThisTime")) sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotBailAtThisTime"))
log.warn(s"DismountVehicleMsg: ${tplayer.Name} can not bail from $obj's when in autopilot") 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 akka.actor.{ActorContext, ActorRef, typed}
import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, SessionVehicleHandlers, VehicleHandlerFunctions} 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.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles}
import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit} import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit}
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
@ -62,6 +63,13 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context:
player.Orientation = orient player.Orientation = orient
player.Velocity = vel player.Velocity = vel
sessionLogic.updateLocalBlockMap(pos) 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( case VehicleResponse.VehicleState(
vehicleGuid, vehicleGuid,

View file

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

View file

@ -2,12 +2,17 @@
package net.psforever.actors.session.support package net.psforever.actors.session.support
import akka.actor.ActorContext 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.ce.Deployable
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} 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.interior.Sidedness
import net.psforever.objects.serverobject.llu.CaptureFlag
import net.psforever.objects.zones.InteractsWithZone
import net.psforever.packet.game.GenericObjectActionMessage 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 import net.psforever.types.PlanetSideGUID
trait LocalHandlerFunctions extends CommonSessionInterfacingFunctionality { trait LocalHandlerFunctions extends CommonSessionInterfacingFunctionality {
@ -47,4 +52,40 @@ class SessionLocalHandlers(
else else
400f 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 // Copyright (c) 2024 PSForever
package net.psforever.objects.avatar.interaction 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.{InteractionWith, RespondsToZoneEnvironment}
import net.psforever.objects.serverobject.environment.interaction.common.Watery 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.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.objects.zones.InteractsWithZone
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.OxygenState import net.psforever.types.OxygenState
@ -15,9 +15,11 @@ import scala.concurrent.duration._
class WithWater(val channel: String) class WithWater(val channel: String)
extends InteractionWith extends InteractionWith
with Watery { 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. * Water is wet.
* When they (finally) drown, they will die.
* @param obj the target * @param obj the target
* @param body the environment * @param body the environment
*/ */
@ -26,22 +28,84 @@ class WithWater(val channel: String)
body: PieceOfEnvironment, body: PieceOfEnvironment,
data: Option[Any] data: Option[Any]
): Unit = { ): Unit = {
val extra = data.collect { if (getExtra(data).nonEmpty) {
case t: OxygenStateTarget => Some(t) inheritAndPushExtraData(obj, body, data)
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 { } else {
val (effect, time, percentage) = Watery.drowningInWateryConditions(obj, condition.map(_.state), waterInteractionTime) depth = math.max(0f, body.collision.altitude - obj.Position.z)
if (effect) { doInteractingWithBehavior(obj, body, data)
val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage) obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = 500 milliseconds, obj.Actor, interaction.InteractingWithEnvironment(body, Some("wading")))
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)) * 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 obj the target
* @param body the environment * @param body the environment
*/ */
override def stopInteractingWith( private def recoverFromDrowning(
obj: InteractsWithZone, obj: InteractsWithZone,
body: PieceOfEnvironment, body: PieceOfEnvironment,
data: Option[Any] data: Option[Any]
): Unit = { ): Unit = {
val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) val state = condition.map(_.state)
if (percentage > 99f) { if (state.contains(OxygenState.Suffocation)) {
recoverFromInteracting(obj) //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 { } 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 time current time until completion of the next effect
* @param percentage value to display in the drowning UI progress bar * @param percentage value to display in the drowning UI progress bar
*/ */
private def stopInteractingAction( private def recoverFromDrowning(
obj: InteractsWithZone, obj: InteractsWithZone,
body: PieceOfEnvironment, body: PieceOfEnvironment,
data: Option[Any], data: Option[Any],
effect: Boolean, effect: Boolean,
time: Long, time: Long,
percentage: Float percentage: Float
): Unit = { ): Unit = {
val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage) val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage)
val extra = data.collect { val extra = data.collect {
case t: OxygenStateTarget => Some(t) case t: OxygenStateTarget => Some(t)
case w: Watery => w.Condition case w: Watery => w.Condition
}.flatten }.flatten
if (effect) { if (effect) {
condition = Some(cond) condition = Some(cond)
waterInteractionTime = System.currentTimeMillis() + time 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 //inform the player
obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra)) displayOxygenState(obj, cond, extra)
} else if (extra.isDefined) { } else if (extra.isDefined) {
//inform the player //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 = { override def recoverFromInteracting(obj: InteractsWithZone): Unit = {
super.recoverFromInteracting(obj) super.recoverFromInteracting(obj)
if (condition.exists(_.state == OxygenState.Suffocation)) { val cond = condition.map(_.state)
val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) //whether or not we were suffocating or recovering, we need to undo the visuals for that
stopInteractingAction(obj, condition.map(_.body).get, None, effect, time, percentage) 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 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 { abstract class EnvironmentTrait {
def canInteractWith(obj: PlanetSideGameObject): Boolean def canInteractWith(obj: PlanetSideGameObject): Boolean
def testingDepth: Float
} }
object EnvironmentAttribute { object EnvironmentAttribute {
@ -25,16 +27,22 @@ object EnvironmentAttribute {
case _ => false case _ => false
}) })
} }
def testingDepth: Float = 0.2f
} }
case object Lava extends EnvironmentTrait { case object Lava extends EnvironmentTrait {
/** lava can only interact with anything capable of registering damage */ /** lava can only interact with anything capable of registering damage */
def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithDamagingFields(obj) def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithDamagingFields(obj)
val testingDepth: Float = 0f
} }
case object Death extends EnvironmentTrait { case object Death extends EnvironmentTrait {
/** death can only interact with anything capable of registering damage */ /** death can only interact with anything capable of registering damage */
def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithDamagingFields(obj) def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithDamagingFields(obj)
val testingDepth: Float = 0f
} }
case object GantryDenialField case object GantryDenialField
@ -46,18 +54,24 @@ object EnvironmentAttribute {
case _ => false case _ => false
} }
} }
val testingDepth: Float = 0f
} }
case object MovementFieldTrigger case object MovementFieldTrigger
extends EnvironmentTrait { extends EnvironmentTrait {
/** only interact with living player characters or vehicles */ /** only interact with living player characters or vehicles */
def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj) def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj)
val testingDepth: Float = 0f
} }
case object InteriorField case object InteriorField
extends EnvironmentTrait { extends EnvironmentTrait {
/** only interact with living player characters or vehicles */ /** only interact with living player characters or vehicles */
def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj) def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj)
val testingDepth: Float = 0f
} }
/** /**

View file

@ -1,7 +1,6 @@
// Copyright (c) 2021 PSForever // Copyright (c) 2021 PSForever
package net.psforever.objects.serverobject.environment.interaction package net.psforever.objects.serverobject.environment.interaction
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment} import net.psforever.objects.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment}
import net.psforever.objects.zones._ import net.psforever.objects.zones._
@ -117,9 +116,8 @@ object InteractWithEnvironment {
obj: PlanetSideServerObject, obj: PlanetSideServerObject,
sector: SectorPopulation sector: SectorPopulation
): Set[PieceOfEnvironment] = { ): Set[PieceOfEnvironment] = {
val depth = GlobalDefinitions.MaxDepth(obj)
sector.environmentList 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) .distinctBy(_.attribute)
.toSet .toSet
} }
@ -136,7 +134,7 @@ object InteractWithEnvironment {
body: PieceOfEnvironment, body: PieceOfEnvironment,
obj: PlanetSideServerObject obj: PlanetSideServerObject
): Option[PieceOfEnvironment] = { ): 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) Some(body)
} else { } else {
None None

View file

@ -8,13 +8,16 @@ import net.psforever.types.{OxygenState, PlanetSideGUID}
trait Watery { trait Watery {
val attribute: EnvironmentTrait = EnvironmentAttribute.Water val attribute: EnvironmentTrait = EnvironmentAttribute.Water
/** how long the current interaction has been progressing in the current way */ /** how long the current interaction has been progressing in the current way */
protected var waterInteractionTime: Long = 0 protected var waterInteractionTime: Long = 0
/** information regarding the drowning state */ /** information regarding the drowning state */
protected var condition: Option[OxygenStateTarget] = None protected var condition: Option[OxygenStateTarget] = None
/** information regarding the drowning state */ /** information regarding the drowning state */
def Condition: Option[OxygenStateTarget] = condition 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 { 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 faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var carrier: Option[Player] = None private var carrier: Option[Player] = None
private var lastTimeCollected: Long = System.currentTimeMillis() private var lastTimeCollected: Long = System.currentTimeMillis()
private val spawnedTime: Long = lastTimeCollected
def Target: Building = target def Target: Building = target
def Target_=(newTarget: Building): Building = { 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. * 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 * @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) { override def Position: Vector3 = {
carrier.get.Position carrier match {
} else { case Some(player) => player.Position
super.Position case None => super.Position
}
} }
def Carrier: Option[Player] = carrier 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 LastCollectionTime: Long = carrier.map { _ => lastTimeCollected }.getOrElse { System.currentTimeMillis() }
def InitialSpawnTime: Long = spawnedTime
} }
object CaptureFlag { object CaptureFlag {

View file

@ -606,9 +606,9 @@ class VehicleControl(vehicle: Vehicle)
c 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) => case watery: WithWater if watery.Condition.map(_.state).contains(OxygenState.Recovery) =>
watery.stopInteractingWithTargets( WithWater.stopInteractingWithTargets(
player, player,
Watery.recoveringFromWater(vehicle, watery)._3, Watery.recoveringFromWater(vehicle, watery)._3,
watery.Condition.map(_.body).get, watery.Condition.map(_.body).get,

View file

@ -1,37 +1,71 @@
// Copyright (c) 2024 PSForever // Copyright (c) 2024 PSForever
package net.psforever.objects.vehicles.interaction 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.PlanetSideServerObject
import net.psforever.objects.serverobject.environment.interaction.{InteractionWith, RespondsToZoneEnvironment} 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
import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget 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.vehicles.control.VehicleControl
import net.psforever.objects.zones.InteractsWithZone import net.psforever.objects.zones.InteractsWithZone
import net.psforever.types.OxygenState import net.psforever.types.OxygenState
import scala.annotation.unused
import scala.concurrent.duration._ import scala.concurrent.duration._
class WithWater() class WithWater()
extends InteractionWith extends InteractionWith
with Watery { 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. * Water is wet.
* Flying vehicles do not display progress towards being waterlogged. * @param obj the target
* They just disable outright. * @param body the environment
* @param obj the target */
* @param body the environment
* @param data additional interaction information, if applicable
*/
def doInteractingWith( def doInteractingWith(
obj: InteractsWithZone, obj: InteractsWithZone,
body: PieceOfEnvironment, body: PieceOfEnvironment,
data: Option[Any] data: Option[Any]
): Unit = { ): 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 { obj match {
case vehicle: Vehicle => 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) val (a, b, c) = Watery.drowningInWateryConditions(obj, condition.map(_.state), waterInteractionTime)
if (a && GlobalDefinitions.isFlightVehicle(vehicle.Definition)) { if (a && GlobalDefinitions.isFlightVehicle(vehicle.Definition)) {
(true, 0L, 0f) //no progress bar (true, 0L, 0f) //no progress bar
@ -42,8 +76,94 @@ class WithWater()
if (effect) { if (effect) {
condition = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage)) condition = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage))
waterInteractionTime = System.currentTimeMillis() + time waterInteractionTime = System.currentTimeMillis() + time
obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, VehicleControl.Disable(true)) obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction)
doInteractingWithTargets( 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, obj,
percentage, percentage,
body, body,
@ -55,34 +175,52 @@ class WithWater()
} }
/** /**
* When out of water, the vehicle no longer risks becoming disabled. * The recovery period is much faster than the waterlogging process.
* It does have to endure a recovery period to get back to full dehydration * Check for when the vehicle fully recovers,
* Flying vehicles are exempt from this process due to the abrupt disability they experience. * and that the vehicle does not regress back to waterlogging.
* @param obj the target * @param obj the target
* @param body the environment * @param body the environment
* @param data additional interaction information, if applicable */
*/ 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( override def stopInteractingWith(
obj: InteractsWithZone, obj: InteractsWithZone,
body: PieceOfEnvironment, body: PieceOfEnvironment,
data: Option[Any] data: Option[Any]
): Unit = { ): Unit = {
obj match { val cond = condition.map(_.state)
case vehicle: Vehicle => if (cond.contains(OxygenState.Suffocation)) {
val (effect: Boolean, time: Long, percentage: Float) = //go from suffocating to recovery
Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) recoverFromDrowning(obj, body, data)
if (effect) { } else if (cond.isEmpty) {
condition = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage)) //neither suffocating nor recovering, so just reset everything
waterInteractionTime = System.currentTimeMillis() + time recoverFromInteracting(obj)
obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute)) obj.Actor ! RespondsToZoneEnvironment.StopTimer(attribute)
stopInteractingWithTargets( waterInteractionTime = 0L
obj, depth = 0f
percentage, condition = None
body, doInteractingWithBehavior = wadingBeforeDrowning
vehicle.Seats.values.flatMap(_.occupants).filter(p => p.isAlive && (p.Zone eq obj.Zone))
)
}
case _ => ()
} }
} }
@ -91,46 +229,53 @@ class WithWater()
if (condition.exists(_.state == OxygenState.Suffocation)) { if (condition.exists(_.state == OxygenState.Suffocation)) {
stopInteractingWith(obj, condition.map(_.body).get, None) stopInteractingWith(obj, condition.map(_.body).get, None)
} }
waterInteractionTime = 0L
condition = None 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. * Tell the given targets that water causes vehicles to become disabled if they dive off too far, too deep.
* @see `InteractingWithEnvironment` * @see `InteractingWithEnvironment`
* @see `OxygenState` * @see `OxygenState`
* @see `OxygenStateTarget` * @see `OxygenStateTarget`
* @param obj the target * @param obj the target
* @param percentage the progress bar completion state * @param percentage the progress bar completion state
* @param body the environment * @param body the environment
* @param targets recipients of the information * @param targets recipients of the information
*/ */
def doInteractingWithTargets( def doInteractingWithTargets(
obj: PlanetSideServerObject, obj: PlanetSideServerObject,
percentage: Float, percentage: Float,
body: PieceOfEnvironment, body: PieceOfEnvironment,
targets: Iterable[PlanetSideServerObject] targets: Iterable[PlanetSideServerObject]
): Unit = { ): Unit = {
val state = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage)) val state = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage))
targets.foreach(_.Actor ! interaction.InteractingWithEnvironment(body, state)) targets.foreach(_.Actor ! interaction.InteractingWithEnvironment(body, state))
} }
/** /**
* Tell the given targets that, when out of water, the vehicle no longer risks becoming disabled. * Tell the given targets that, when out of water, the vehicle no longer risks becoming disabled.
* @see `EscapeFromEnvironment` * @see `EscapeFromEnvironment`
* @see `OxygenState` * @see `OxygenState`
* @see `OxygenStateTarget` * @see `OxygenStateTarget`
* @param obj the target * @param obj the target
* @param percentage the progress bar completion state * @param percentage the progress bar completion state
* @param body the environment * @param body the environment
* @param targets recipients of the information * @param targets recipients of the information
*/ */
def stopInteractingWithTargets( def stopInteractingWithTargets(
obj: PlanetSideServerObject, obj: PlanetSideServerObject,
percentage: Float, percentage: Float,
body: PieceOfEnvironment, body: PieceOfEnvironment,
targets: Iterable[PlanetSideServerObject] targets: Iterable[PlanetSideServerObject]
): Unit = { ): Unit = {
val state = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage)) val state = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage))
targets.foreach(_.Actor ! interaction.EscapeFromEnvironment(body, state)) targets.foreach(_.Actor ! interaction.EscapeFromEnvironment(body, state))
} }

View file

@ -134,7 +134,17 @@ object OxygenStateMessage extends Marshallable[OxygenStateMessage] {
204.8f, 204.8f,
11 11
) :: //hackish: 2^11 == 2047, so it should be 204.7; but, 204.8 allows decode == encode ) :: //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] ).as[DrowningTarget]
implicit val codec: Codec[OxygenStateMessage] = ( implicit val codec: Codec[OxygenStateMessage] = (

View file

@ -120,6 +120,8 @@ class LocalService(zone: Zone) extends Actor {
hackCapturer ! HackCaptureActor.StartCaptureTerminalHack(target, zone, 0, 8L) hackCapturer ! HackCaptureActor.StartCaptureTerminalHack(target, zone, 0, 8L)
case LocalAction.LluCaptured(llu) => case LocalAction.LluCaptured(llu) =>
hackCapturer ! HackCaptureActor.FlagCaptured(llu) hackCapturer ! HackCaptureActor.FlagCaptured(llu)
case LocalAction.LluLost(llu) =>
hackCapturer ! HackCaptureActor.FlagLost(llu)
case LocalAction.LluSpawned(player_guid, llu) => case LocalAction.LluSpawned(player_guid, llu) =>
// Forward to all clients to create object locally // 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 ResecureCaptureTerminal(target: CaptureTerminal, hacker: PlayerSource) extends Action
final case class StartCaptureTerminalHack(target: CaptureTerminal) extends Action final case class StartCaptureTerminalHack(target: CaptureTerminal) extends Action
final case class LluCaptured(llu: CaptureFlag) 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 LluSpawned(player_guid: PlanetSideGUID, llu: CaptureFlag) extends Action
final case class LluDespawned(player_guid: PlanetSideGUID, guid: PlanetSideGUID, position: Vector3) 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, flag.Zone,
CaptureFlagChatMessageStrings.CTF_Failed_TimedOut(building.Name, flag.Target.Name, flag.Faction) 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 => case CaptureFlagLostReasonEnum.Ended =>
() ()
} }
@ -197,6 +203,7 @@ class CaptureFlagManager(zone: Zone) extends Actor {
} }
private def ChatBroadcast(zone: Zone, message: String, fanfare: Boolean = true): Unit = { private def ChatBroadcast(zone: Zone, message: String, fanfare: Boolean = true): Unit = {
//todo use UNK_222 sometimes
val messageType: ChatMessageType = if (fanfare) { val messageType: ChatMessageType = if (fanfare) {
ChatMessageType.UNK_223 ChatMessageType.UNK_223
} else { } else {
@ -224,15 +231,6 @@ object CaptureFlagManager {
} }
object CaptureFlagChatMessageStrings { 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! // @CTF_Success=%1 captured %2's LLU for the %3!
/** {player.Name} captured {ownerName}'s LLU for the {player.Faction}! */ /** {player.Name} captured {ownerName}'s LLU for the {player.Faction}! */
private[support] def CTF_Success(playerName:String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String = 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 = private[support] def CTF_Failed_TimedOut(ownerName: String, name: String, faction: PlanetSideEmpire.Value): String =
s"@CTF_Failed_TimedOut^@${GetFactionString(faction)}~^@$ownerName~^@$name~" 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! // @CTF_Failed_SourceResecured=The %1 resecured %2!\nThe LLU was lost!
/** The {faction} resecured {name}!\nThe LLU was lost! */ /** The {faction} resecured {name}!\nThe LLU was lost! */
private[support] def CTF_Failed_SourceResecured(name: String, faction: PlanetSideEmpire.Value): String = 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! // @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! */ /** {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 // @CTF_FlagPickedUp=%1 of the %2 picked up %3's LLU
/** {player.Name} of the {player.Faction} picked up {ownerName}'s LLU */ /** {player.Name} of the {player.Faction} picked up {ownerName}'s LLU */
def CTF_FlagPickedUp(playerName: String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String = 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 // @CTF_FlagDropped=%1 of the %2 dropped %3's LLU
/** {playerName} of the {faction} dropped {facilityName}'s LLU */ /** {playerName} of the {faction} dropped {facilityName}'s LLU */
def CTF_FlagDropped(playerName: String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String = 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 = { private def GetFactionString: PlanetSideEmpire.Value=>String = {
case PlanetSideEmpire.TR => "TerranRepublic" case PlanetSideEmpire.TR => "TerranRepublic"
@ -277,4 +329,5 @@ object CaptureFlagLostReasonEnum {
final case object Resecured extends CaptureFlagLostReasonEnum final case object Resecured extends CaptureFlagLostReasonEnum
final case object TimedOut extends CaptureFlagLostReasonEnum final case object TimedOut extends CaptureFlagLostReasonEnum
final case object Ended 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. // 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] val building = terminal.Owner.asInstanceOf[Building]
building.GetFlag match { 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) => case Some(llu) =>
// LLU was not delivered in time. Send resecured notifications // 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) NotifyHackStateChange(terminal, isResecured = true)
case _ => 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") 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 _ => () case _ => ()
} }
@ -256,6 +281,7 @@ object HackCaptureActor {
final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource) final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource)
final case class FlagCaptured(flag: CaptureFlag) final case class FlagCaptured(flag: CaptureFlag)
final case class FlagLost(flag: CaptureFlag)
private final case class ProcessCompleteHacks() private final case class ProcessCompleteHacks()

View file

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