force dome provides damage protection to certain amenities, e.g., the generator, the turrets, and any implant machines (cryo); force dome will also suspend hacking attempts under it's envelope, but counter-hacking (resecure) should still be possible; operated turret deployables gain protection while manned; turrets no longer share knowledge of each other's upgrade cycles

This commit is contained in:
Fate-JH 2026-01-03 10:30:52 -05:00
parent 94bd315354
commit 73f352490c
17 changed files with 154 additions and 44 deletions

View file

@ -166,7 +166,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L =>
if !obj.isUpgrading || System.currentTimeMillis() - obj.CheckTurretUpgradeTime >= 1500L =>
obj.setMiddleOfUpgrade(false)
sessionLogic.zoning.CancelZoningProcess()
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))

View file

@ -181,7 +181,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L =>
if !obj.isUpgrading || System.currentTimeMillis() - obj.CheckTurretUpgradeTime >= 1500L =>
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
obj.setMiddleOfUpgrade(false)
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")

View file

@ -109,11 +109,16 @@ case object MajorFacilityLogic
// No map update needed - will be sent by `HackCaptureActor` when required
case dome: ForceDomePhysics =>
val building = details.building
// The force dome being expanded modifies the NTU drain rate
// The protection of the force dome modifies the NTU drain rate
val multiplier: Float = calculateNtuDrainMultiplierFrom(details.building, domeOpt = Some(dome))
building.NtuSource.foreach(_.Actor ! ResourceSiloControl.DrainMultiplier(multiplier))
// The force dome being expanded marks the generator as being invulnerable; it can be damaged otherwise
building.Generator.foreach { _.Actor ! Damageable.Vulnerability(dome.Energized) }
// The protection of the force dome marks the generator (and some other amenities) as being invulnerable
val msg = Damageable.Vulnerability(dome.Perimeter.nonEmpty)
val applicable = dome.Definition.ApplyProtectionTo
building
.Amenities
.filter(amenity => applicable.contains(amenity.Definition))
.foreach { _.Actor ! msg }
case _ =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
}

View file

@ -10,7 +10,7 @@ import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.mount.InteractWithRadiationCloudsSeatedInEntity
import net.psforever.objects.serverobject.mount.{InteractWithForceDomeProtectionSeatedInEntity, InteractWithRadiationCloudsSeatedInEntity}
import net.psforever.objects.serverobject.turret.auto.{AffectedByAutomaticTurretFire, AutomatedTurret}
import net.psforever.objects.serverobject.turret.{TurretControl, TurretDefinition, WeaponTurret}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
@ -36,6 +36,7 @@ class TurretDeployable(tdef: TurretDeployableDefinition)
HackDuration = Array(0, 20, 10, 5)
if (tdef.Seats.nonEmpty) {
interaction(new InteractWithForceDomeProtectionSeatedInEntity)
interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationCloudsSeatedInEntity(obj = this, range = 100f))
}

View file

@ -16,7 +16,7 @@ import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.interior.{InteriorAwareFromInteraction, Sidedness}
import net.psforever.objects.serverobject.structures.AmenityOwner
import net.psforever.objects.vehicles._
import net.psforever.objects.vehicles.interaction.{TriggerOnVehicleRule, WithLava, WithWater}
import net.psforever.objects.vehicles.interaction.{InteractWithForceDomeProtectionSeatedInVehicle, TriggerOnVehicleRule, WithLava, WithWater}
import net.psforever.objects.vital.resistance.StandardResistanceProfile
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageResistanceModel
@ -94,6 +94,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
with AuraContainer
with MountableEntity
with InteriorAwareFromInteraction {
interaction(new InteractWithForceDomeProtectionSeatedInVehicle)
interaction(environment.interaction.InteractWithEnvironment(Seq(
new WithEntrance(),
new WithWater(),

View file

@ -77,7 +77,7 @@ class InteractWithForceDomeProtection
}
}
protected def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
protectedBy = Some(dome)
target.Actor ! Damageable.MakeInvulnerable
}

View file

@ -965,6 +965,7 @@ object GlobalDefinitionsMiscellaneous {
Vector3(-90.328125f, -106.90625f, 0f),
Vector3(83.05469f, -106.90625f, 0f)
)
force_dome_amp_physics.ApplyProtectionTo = List(generator, manned_turret)
force_dome_comm_physics.Name = "force_dome_comm_physics"
force_dome_comm_physics.UseRadius = 121.8149f
@ -976,6 +977,7 @@ object GlobalDefinitionsMiscellaneous {
Vector3(-83.640625f, 45.601562f, 0f),
Vector3(-83.640625f, -89.859375f, 0f)
)
force_dome_comm_physics.ApplyProtectionTo = List(generator, manned_turret)
force_dome_cryo_physics.Name = "force_dome_cryo_physics"
force_dome_cryo_physics.UseRadius = 127.9241f //127.7963f
@ -986,6 +988,7 @@ object GlobalDefinitionsMiscellaneous {
Vector3(-74.73426f, -103.47f, 0),
Vector3(72.75476f, -103.47f, 0)
)
force_dome_cryo_physics.ApplyProtectionTo = List(generator, implant_terminal_mech, manned_turret)
force_dome_dsp_physics.Name = "force_dome_dsp_physics"
force_dome_dsp_physics.UseRadius = 175.8838f //175.7081f
@ -997,10 +1000,11 @@ object GlobalDefinitionsMiscellaneous {
Vector3(130.44531f, 188.26562f, 0f),
Vector3(130.44531f, -93.28125f, 0f)
)
force_dome_dsp_physics.ApplyProtectionTo = List(generator, manned_turret)
force_dome_tech_physics.Name = "force_dome_tech_physics"
force_dome_tech_physics.UseRadius = 150.1284f
force_dome_tech_physics.PerimeterOffsets = List( //todo double-check eisa, esamir
force_dome_tech_physics.PerimeterOffsets = List( //todo double-check, e.g., eisa, esamir
Vector3(130.14636f, -95.20665f, 0f),
Vector3(130.14636f, 34.441734f, 0f),
Vector3(103.98575f, 52.58408f, 0f),
@ -1011,5 +1015,6 @@ object GlobalDefinitionsMiscellaneous {
Vector3(-73.64424f, -114.65837f, 0f),
Vector3(102.12191f, -114.65837f, 0f)
)
force_dome_tech_physics.ApplyProtectionTo = List(generator, manned_turret)
}
}

View file

@ -164,8 +164,8 @@ object ForceDomeControl {
val energizedState = dome.Energized
CheckForceDomeStatus(building, dome).exists {
case true if !energizedState =>
dome.Owner.Actor ! BuildingActor.MapUpdate()
ChangeDomeEnergizedState(dome, activationState = true)
dome.Owner.Actor ! BuildingActor.MapUpdate()
true
case false if energizedState =>
ChangeDomeEnergizedState(dome, activationState = false)
@ -247,7 +247,7 @@ object ForceDomeControl {
* @param segments ground-level perimeter of the force dome is defined by these segments (as vertex pairs)
* @param obj1 a game entity, should be the force dome
* @param obj2 a game entity, should be a damageable target of the force dome's wrath
* @param maxDistance ot applicable
* @param maxDistance not applicable
* @return `true`, if target is detected within the force dome kill region
* `false`, otherwise
*/
@ -260,12 +260,12 @@ object ForceDomeControl {
@unused maxDistance: Float
): Boolean = {
val centerPos @ Vector3(centerX, centerY, centerZ) = obj1.Position
val Vector3(targetX, targetY, targetZ) = obj2.Position - centerPos //deltas of segment of target to dome
val checkForIntersection = segments.exists { case (point1, point2) =>
val Vector3(targetX, targetY, _) = obj2.Position.xy - centerPos.xy //deltas of segment of target to dome
lazy val checkForIntersection = segments.exists { case (point1, point2) =>
//want targets within the perimeter; if there's an intersection, target is outside of the perimeter
segmentIntersectionTestPerSegment(centerX, centerY, targetX, targetY, point1.x, point1.y, point2.x, point2.y)
}
!checkForIntersection && (targetZ < centerZ || Zone.distanceCheck(obj1, obj2, math.pow(obj1.Definition.UseRadius, 2).toFloat))
segments.nonEmpty && !checkForIntersection && (obj2.Position.z <= centerZ || Zone.distanceCheck(obj1, obj2, math.pow(obj1.Definition.UseRadius, 2).toFloat))
}
/**
@ -402,9 +402,11 @@ class ForceDomeControl(dome: ForceDomePhysics)
case ForceDomeControl.ApplyProtection
if dome.Energized =>
dome.Perimeter = perimeterSegments
dome.Owner.Actor ! BuildingActor.AmenityStateChange(dome)
case ForceDomeControl.RemoveProtection =>
dome.Perimeter = List.empty
dome.Owner.Actor ! BuildingActor.AmenityStateChange(dome)
case ForceDomeControl.Purge =>
ForceDomeControl.ForceDomeKills(dome, perimeterSegments)

View file

@ -3,7 +3,6 @@ package net.psforever.objects.serverobject.dome
import net.psforever.objects.geometry.d3.{Sphere, VolumetricGeometry}
import net.psforever.objects.serverobject.structures.AmenityDefinition
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.types.Vector3
class ForceDomeDefinition(objectId: Int)
@ -22,6 +21,19 @@ class ForceDomeDefinition(objectId: Int)
perimeter = points
PerimeterOffsets
}
private var protects: List[AmenityDefinition] = List()
def ApplyProtectionTo: List[AmenityDefinition] = protects
def ApplyProtectionTo_=(protect: AmenityDefinition): List[AmenityDefinition] = {
ApplyProtectionTo_=(List(protect))
}
def ApplyProtectionTo_=(protect: List[AmenityDefinition]): List[AmenityDefinition] = {
protects = protect
ApplyProtectionTo
}
}
object ForceDomeDefinition {

View file

@ -2,11 +2,13 @@
package net.psforever.objects.serverobject.hackable
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate}
import net.psforever.objects.serverobject.dome.ForceDomeControl
import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate}
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import net.psforever.objects.{GlobalDefinitions, Player, Vehicle}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.game.{GenericObjectActionMessage, HackMessage, HackState, HackState1, HackState7, TriggeredSound}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
import net.psforever.services.Service
@ -17,23 +19,7 @@ import scala.util.{Failure, Success}
object GenericHackables {
private val log = org.log4s.getLogger("HackableBehavior")
private var turretUpgradeTime: Long = System.currentTimeMillis()
private var turretUpgradeTimeSet: Boolean = false
def updateTurretUpgradeTime(): Long = {
turretUpgradeTime = System.currentTimeMillis()
turretUpgradeTimeSet = true
turretUpgradeTime
}
// Used for checking the time without updating it
def getTurretUpgradeTime: Long = {
if (!turretUpgradeTimeSet) {
turretUpgradeTime = System.currentTimeMillis()
turretUpgradeTimeSet = true
}
turretUpgradeTime
}
/**
* na
*
@ -79,7 +65,13 @@ object GenericHackables {
* @return `true`, if the next cycle of progress should occur;
* `false`, otherwise
*/
def HackingTickAction(progressType: HackState1, hacker: Player, target: PlanetSideServerObject, tool_guid: PlanetSideGUID)(
def HackingTickAction(
progressType: HackState1,
hacker: Player,
target: PlanetSideServerObject,
tool_guid: PlanetSideGUID,
additionalCancellationTests: (PlanetSideServerObject, Player) => Boolean = ForceDomeProtectsFromHacking
)(
progress: Float
): Boolean = {
//hack state for progress bar visibility
@ -87,9 +79,7 @@ object GenericHackables {
(HackState.Start, 0)
} else if (progress >= 100L) {
(HackState.Finished, 100)
} else if (target.isMoving(test = 1f) || target.Destroyed || !target.HasGUID) {
(HackState.Cancelled, 0)
} else if (target.isInstanceOf[CaptureTerminal] && EndHackProgress(target, hacker)) {
} else if (target.isMoving(test = 1f) || target.Destroyed || !target.HasGUID || additionalCancellationTests(target, hacker)) {
(HackState.Cancelled, 0)
} else {
(HackState.Ongoing, progress.toInt)
@ -104,6 +94,31 @@ object GenericHackables {
progressState != HackState.Cancelled
}
/**
* The force dome prevents hacking if its protection has been declared over a capitol.
* Under normal circumstances, the dome will be visible in the sky at his point,
* blocking enemy encounter within its boundaries,
* so anything that can be hacked is on that boundary perimeter,
* or an alternate method of entry (Router) has been compromised.
* @see `ForceDomeControl.TargetUnderForceDome`
* @see `Sector`
* @param target the `Hackable` object that has been hacked
* @param hacker the player performing the action
* @return `true`, if the target is within boundary of a working force dome and thus protected;
* `false`, otherwise
*/
def ForceDomeProtectsFromHacking(target: PlanetSideServerObject, hacker: Player): Boolean = {
//explicitly allow friendly hacking which is typically clearing a hack
target.Faction != hacker.Faction &&
(target match {
case obj: Amenity => obj.Owner.asInstanceOf[Building].ForceDome.toList
case obj: BlockMapEntity => target.Zone.blockMap.sector(obj).buildingList.flatMap(_.ForceDome)
case _ => List()
})
.filter(_.Perimeter.nonEmpty)
.exists(dome => ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(dome, target, maxDistance = 0f))
}
/**
* The process of hacking an object is completed.
* Pass the message onto the hackable object and onto the local events system.

View file

@ -10,7 +10,7 @@ class InteractWithForceDomeProtectionSeatedInEntity
extends InteractWithForceDomeProtection {
override def range: Float = 30f
override protected def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
override def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
super.applyProtection(target, dome)
target
.asInstanceOf[Mountable]

View file

@ -29,7 +29,7 @@ class CaptureTerminalControl(terminal: CaptureTerminal)
sender() ! CommonMessages.Progress(
GenericHackables.GetHackSpeed(player, terminal),
CaptureTerminals.FinishHackingCaptureConsole(terminal, player, unk = -1),
GenericHackables.HackingTickAction(HackState1.Unk1, player, terminal, item.GUID)
GenericHackables.HackingTickAction(HackState1.Unk1, player, terminal, item.GUID, CaptureTerminals.EndHackProgress)
)
}

View file

@ -1,13 +1,18 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.serverobject.terminals.capture
import net.psforever.objects.Player
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.PlanetSideEmpire
import scala.concurrent.duration._
import scala.util.{Failure, Success}
object CaptureTerminals {import scala.concurrent.duration._
object CaptureTerminals {
private val log = org.log4s.getLogger("CaptureTerminals")
/**
@ -55,4 +60,47 @@ object CaptureTerminals {import scala.concurrent.duration._
log.warn(s"Hack message failed on target guid: ${target.GUID}")
}
}
/**
* Check if the state of connected facilities has changed since the hack progress began. It accounts for a friendly facility
* on the other side of a warpgate as well in case there are no friendly facilities in the same zone
* @param target the `Hackable` object that has been hacked
* @param hacker the player performing the action
* @return `true`, if the hack should be ended; `false`, otherwise
*/
def EndHackProgress(target: PlanetSideServerObject, hacker: Player): Boolean = {
val building = target.asInstanceOf[CaptureTerminal].Owner.asInstanceOf[Building]
val hackerFaction = hacker.Faction
if (GenericHackables.ForceDomeProtectsFromHacking(target, hacker)) {
true
} else if (building.Faction == PlanetSideEmpire.NEUTRAL ||
building.BuildingType == StructureType.Tower ||
building.Faction == hackerFaction) {
false
} else {
val stopHackingCount = building.Neighbours match {
case Some(neighbors) =>
neighbors.count {
case wg: WarpGate if wg.Faction == hackerFaction =>
true
case wg: WarpGate =>
val friendlyBaseOpt = for {
otherWg <- wg.Neighbours.flatMap(_.find(_.isInstanceOf[WarpGate]))
friendly <- otherWg.Neighbours.flatMap(_.collectFirst { case b: Building if !b.isInstanceOf[WarpGate] => b })
} yield friendly
friendlyBaseOpt.exists { fb =>
fb.Faction == hackerFaction &&
!fb.CaptureTerminalIsHacked &&
fb.NtuLevel > 0
}
case b =>
b.Faction == hackerFaction &&
!b.CaptureTerminalIsHacked &&
b.NtuLevel > 0
}
case None => 0
}
stopHackingCount == 0
}
}
}

View file

@ -23,6 +23,28 @@ class FacilityTurret(tDef: FacilityTurretDefinition)
WeaponTurret.LoadDefinition(turret = this)
WhichSide = Sidedness.OutsideOf
private var turretUpgradeTime: Long = System.currentTimeMillis()
private var turretUpgradeTimeSet: Boolean = false
def UpdateTurretUpgradeTime(): Long = {
turretUpgradeTime = System.currentTimeMillis()
turretUpgradeTimeSet = true
turretUpgradeTime
}
// Used for checking the time without updating it
def CheckTurretUpgradeTime: Long = {
if (!turretUpgradeTimeSet) {
turretUpgradeTime = System.currentTimeMillis()
turretUpgradeTimeSet = true
}
turretUpgradeTime
}
def FinishedTurretUpgradeReset(): Unit = {
turretUpgradeTimeSet = false
}
def TurretOwner: SourceEntry = {
Seats
.headOption

View file

@ -100,7 +100,7 @@ class FacilityTurretControl(turret: FacilityTurret)
seatNumber: Int,
player: Player): Boolean = {
super.mountTest(obj, seatNumber, player) &&
(!TurretObject.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L)
(!TurretObject.isUpgrading || System.currentTimeMillis() - TurretObject.CheckTurretUpgradeTime >= 1500L)
}
override protected def tryMount(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = {

View file

@ -3,7 +3,6 @@ package net.psforever.objects.serverobject.turret
import net.psforever.objects.avatar.Certification
import net.psforever.objects.ce.Deployable
import net.psforever.objects.serverobject.hackable.GenericHackables.updateTurretUpgradeTime
import net.psforever.objects.{Player, Tool, TurretDeployable}
import net.psforever.packet.game.{HackMessage, HackState, HackState1, HackState7, InventoryStateMessage}
import net.psforever.services.Service
@ -83,7 +82,7 @@ object WeaponTurrets {
} else if (turret.Destroyed) {
(HackState.Cancelled, 0)
} else {
updateTurretUpgradeTime()
turret.UpdateTurretUpgradeTime()
(HackState.Ongoing, progress.toInt)
}
turret.Zone.AvatarEvents ! AvatarServiceMessage(

View file

@ -9,7 +9,7 @@ import net.psforever.objects.zones.InteractsWithZone
class InteractWithForceDomeProtectionSeatedInVehicle
extends InteractWithForceDomeProtectionSeatedInEntity {
override protected def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
override def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
super.applyProtection(target, dome)
target
.asInstanceOf[Vehicle]