the force dome exhibits a perimeter in which enemies will be destroyed when it energizes; the facility generator will become undestroyable when the force dome is energized

This commit is contained in:
Fate-JH 2025-12-18 19:29:06 -05:00
parent 6a960ed5ac
commit 4b3f8ea6c0
5 changed files with 245 additions and 100 deletions

View file

@ -6,6 +6,7 @@ import akka.actor.typed.{ActorRef, Behavior}
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails, ZoneActor}
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.dome.ForceDomePhysics
import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl}
import net.psforever.objects.serverobject.resourcesilo.ResourceSiloControl
@ -107,9 +108,12 @@ 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
val multiplier: Float = calculateNtuDrainMultiplierFrom(details.building, domeOpt = Some(dome))
details.building.NtuSource.foreach(_.Actor ! ResourceSiloControl.DrainMultiplier(multiplier))
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) }
case _ =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
}
@ -373,7 +377,10 @@ case object MajorFacilityLogic
): Float = {
// The force dome being expanded means all repairs are essentially for free
dome
.map { case d if d.Energized => 0f }
.flatMap {
case d if d.Energized => Some(0f)
case _ => None
}
.orElse {
mainTerminal.flatMap { _ => Some(2f) } //todo main terminal and viruses
}

View file

@ -959,17 +959,57 @@ object GlobalDefinitionsMiscellaneous {
force_dome_amp_physics.Name = "force_dome_amp_physics"
force_dome_amp_physics.UseRadius = 142.26f
force_dome_amp_physics.PerimeterOffsets = List(
Vector3(83.05469f, 114.875f, 0f),
Vector3(-90.328125f, 114.875f, 0f),
Vector3(-90.328125f, -106.90625f, 0f),
Vector3(83.05469f, -106.90625f, 0f)
)
force_dome_comm_physics.Name = "force_dome_comm_physics"
force_dome_comm_physics.UseRadius = 121.8149f
force_dome_comm_physics.PerimeterOffsets = List(
Vector3(35.1875f, -89.859375f, 0f),
Vector3(80.96875f, -43.773438f, 0f),
Vector3(80.96875f, 91.08594f, 0f),
Vector3(-37.296875f, 91.08594f, 0f),
Vector3(-83.640625f, 45.601562f, 0f),
Vector3(-83.640625f, -89.859375f, 0f)
)
force_dome_cryo_physics.Name = "force_dome_cryo_physics"
force_dome_cryo_physics.UseRadius = 127.9241f //127.7963f
force_dome_cryo_physics.PerimeterOffsets = List(
Vector3(72.75476f, 39.902725f, 0),
Vector3(24.505968f, 88.03482f, 0),
Vector3(-74.73426f, 88.03482f, 0),
Vector3(-74.73426f, -103.47f, 0),
Vector3(72.75476f, -103.47f, 0)
)
force_dome_dsp_physics.Name = "force_dome_dsp_physics"
force_dome_dsp_physics.UseRadius = 175.8838f //175.7081f
force_dome_dsp_physics.PerimeterOffsets = List(
Vector3(35.03125f, -93.25f, 0f),
Vector3(-83.1875f, -93.25f, 0f),
Vector3(-83.1875f, 114.515625f, 0f),
Vector3(-12.109375f, 188.26562f, 0f),
Vector3(130.44531f, 188.26562f, 0f),
Vector3(130.44531f, -93.28125f, 0f)
)
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
Vector3(130.14636f, -95.20665f, 0f),
Vector3(130.14636f, 34.441734f, 0f),
Vector3(103.98575f, 52.58408f, 0f),
Vector3(16.405174f, 54.746464f, 0f),
Vector3(14.256668f, 107.01521f, 0f),
Vector3(-92.08687f, 107.01521f, 0f),
Vector3(-92.08687f, -96.176155f, 0f),
Vector3(-73.64424f, -114.65837f, 0f),
Vector3(102.12191f, -114.65837f, 0f)
)
}
}

View file

@ -17,7 +17,6 @@ import net.psforever.objects.vital.resolution.ResolutionCalculations
* All of these should be affected by the damage where applicable.
*/
trait Damageable {
/**
* Contextual access to the object being the target of this damage.
* Needs declaration in lowest implementing code.
@ -25,19 +24,34 @@ trait Damageable {
*/
def DamageableObject: Damageable.Target
/** a local `canDamage` flag */
private var isVulnerable: Boolean = true
/** the official mixin hook;
* `orElse` onto the "control" `Actor` `receive`; or,
* cite the `originalTakesDamage` protocol during inheritance overrides */
val takesDamage: Receive = {
case Damageable.MakeVulnerable =>
isVulnerable = false
case Damageable.MakeInvulnerable =>
isVulnerable = true
case Vitality.Damage(damage_func) =>
val obj = DamageableObject
if (obj.CanDamage) {
if (isVulnerable && obj.CanDamage) {
PerformDamage(obj, damage_func)
}
}
/** a duplicate of the core implementation for the default mixin hook, for use in overriding */
final val originalTakesDamage: Receive = {
case Damageable.MakeVulnerable =>
isVulnerable = false
case Damageable.MakeInvulnerable =>
isVulnerable = true
case Vitality.Damage(damage_func) =>
val obj = DamageableObject
if (obj.CanDamage) {
@ -67,6 +81,20 @@ object Damageable {
*/
final val LogChannel: String = "DamageResolution"
trait PersonalVulnerability
final case object MakeVulnerable extends PersonalVulnerability
final case object MakeInvulnerable extends PersonalVulnerability
def Vulnerability(state: Boolean): PersonalVulnerability = {
if (state) {
MakeInvulnerable
} else {
MakeVulnerable
}
}
/**
* Does the possibility exist that the designated target can be affected by this projectile's damage?
* @see `Hackable`

View file

@ -7,7 +7,6 @@ import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.structures.{Amenity, Building, PoweredAmenityControl}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.etc.ForceDomeExposure
@ -19,6 +18,8 @@ import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGeneratorState, Vector3}
import scala.annotation.unused
object ForceDomeControl {
trait Command
@ -30,7 +31,7 @@ object ForceDomeControl {
/**
* Dispatch a message to update the state of the clients with the server state of the capitol force dome.
* @param dome force dome
* @param dome force dome
* @param activationState new force dome status
*/
def ChangeDomeEnergizedState(dome: ForceDomePhysics, activationState: Boolean): Unit = {
@ -49,7 +50,7 @@ object ForceDomeControl {
* use the faction affinity, the generator status, and the resource silo's capacitance level
* to determine if the capitol force dome should be active.
* @param building building being evaluated
* @param dome force dome
* @param dome force dome
* @return the condition of the capitol force dome;
* `None`, if the facility is not a capitol building;
* `Some(true|false)` to indicate the state of the force dome
@ -78,7 +79,7 @@ object ForceDomeControl {
* for capitol force dome expansion.
* @param building target building
* @return `true`, if the conditions for capitol force dome are not met;
* `false`, otherwise
* `false`, otherwise
*/
def InvalidBuildingCapitolForceDomeConditions(building: Building): Boolean = {
building.Faction == PlanetSideEmpire.NEUTRAL ||
@ -87,59 +88,30 @@ object ForceDomeControl {
}
/**
* na
* Apply a fixed point and a rotation value to a series of vertex offsets,
* then daisy-chain the resulting vertices in such a way that
* it creates a perimeter around the (building) owner of the capitol force dome.
* The resulting capitol force dome barrier is a blocky pyramoid shape.
* @param dome force dome
* @return na
* @return perimeter of the force dome barrier
*/
def GeneralFacilityPerimeter(dome: ForceDomePhysics): List[(Vector3, Vector3)] = {
val generatorTowerCenter = dome.Position.xy
val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy)
val pointsOfForceDomePerimeter = turretPoints.map { p =>
val segmentFromTowerToTurret = p - generatorTowerCenter
Vector3.Unit(segmentFromTowerToTurret) * (Vector3.Magnitude(segmentFromTowerToTurret) + 20) //todo get correct distance offset
def SetupForceDomePerimeter(dome: ForceDomePhysics): List[(Vector3, Vector3)] = {
val center = dome.Position.xy
val rotation = math.toRadians(dome.Owner.Orientation.z).toFloat
val perimeterOffsets = dome.Definition.PerimeterOffsets
val perimeterPoints = perimeterOffsets.map {
center + Vector3.PlanarRotateAroundPoint(_, Vector3(0, 0, 1), rotation)
}
pointsOfForceDomePerimeter
.flatMap { point =>
pointsOfForceDomePerimeter
.sortBy(p => Vector3.DistanceSquared(p, point))
.slice(1, 3)
.map { otherPoint =>
if (point.y > otherPoint.y || point.x < otherPoint.x) {
(point, otherPoint)
} else {
(otherPoint, point)
}
}
}
.distinct
}
import scala.annotation.unused
def TechPlantFacilityPerimeter(@unused dome: ForceDomePhysics): List[(Vector3, Vector3)] = {
// val generatorTowerCenter = dome.Position.xy
// val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy)
// val organizedByClosestToGarage = dome
// .Owner
// .Amenities
// .find(_.Definition.Name.equals("gr_door_garage_ext"))
// .map { garage =>
// val doorPosition = garage.Position.xy
// turretPoints.sortBy(point => Vector3.DistanceSquared(doorPosition, point))
// }
// .getOrElse(List[Vector3]())
//
// //val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy)
// val pointsOfForceDomePerimeter = turretPoints.map { p =>
// val segmentFromTowerToTurret = p - generatorTowerCenter
// Vector3.Unit(segmentFromTowerToTurret) * (Vector3.Magnitude(segmentFromTowerToTurret) + 20) //todo get correct distance offset
// }
Nil
((0 until perimeterPoints.size - 1).map { index =>
(perimeterPoints(index), perimeterPoints(index + 1))
} :+ (perimeterPoints.last, perimeterPoints.head)).toList
}
/**
* na
* The capitol force dome should have changed states but it will not!
* Make certain everyone knows!
* @param building target building
* @param state na
* @param state whether the force dome is energized or not
*/
def CustomDomeStateEnforcedMessage(
building: Building,
@ -156,7 +128,8 @@ object ForceDomeControl {
}
/**
* na
* The capitol force dome will start changing states normally.
* Make certain everyone knows.
* @param building facility
*/
def NormalDomeStateMessage(building: Building): Unit = {
@ -179,19 +152,19 @@ object ForceDomeControl {
* pass a message onto that facility that it should check its own state alignment.
* @param building facility with `dome`
*/
def AlignForceDomeStatusAndUpdate(building: Building, dome: ForceDomePhysics): Unit = {
CheckForceDomeStatus(building, dome).foreach {
case true =>
if (!dome.Energized) {
ChangeDomeEnergizedState(dome, activationState = true)
ForceDomeKills(dome)
dome.Owner.Actor ! BuildingActor.MapUpdate()
}
case false =>
if (dome.Energized) {
ChangeDomeEnergizedState(dome, activationState = false)
dome.Owner.Actor ! BuildingActor.MapUpdate()
}
def AlignForceDomeStatusAndUpdate(building: Building, dome: ForceDomePhysics): Boolean = {
val energizedState = dome.Energized
CheckForceDomeStatus(building, dome).exists {
case true if !energizedState =>
dome.Owner.Actor ! BuildingActor.MapUpdate()
ChangeDomeEnergizedState(dome, activationState = true)
true
case false if energizedState =>
ChangeDomeEnergizedState(dome, activationState = false)
dome.Owner.Actor ! BuildingActor.MapUpdate()
false
case _ =>
energizedState
}
}
@ -204,44 +177,44 @@ object ForceDomeControl {
* pass a message onto that facility that it should check its own state alignment.
* @param building facility with `dome`
*/
private def AlignForceDomeStatus(building: Building, dome: ForceDomePhysics): Unit = {
CheckForceDomeStatus(building, dome).foreach {
case true =>
if (!dome.Energized) {
ChangeDomeEnergizedState(dome, activationState = true)
ForceDomeKills(dome)
}
case false =>
if (dome.Energized) {
ChangeDomeEnergizedState(dome, activationState = false)
}
private def AlignForceDomeStatus(building: Building, dome: ForceDomePhysics): Boolean = {
val energizedState = dome.Energized
CheckForceDomeStatus(building, dome).exists {
case true if !energizedState =>
ChangeDomeEnergizedState(dome, activationState = true)
true
case false if energizedState =>
ChangeDomeEnergizedState(dome, activationState = false)
false
case _ =>
energizedState
}
}
/**
* Being too close to the force dome can destroy targets if they do not match the faction alignment of the dome.
* This is the usual fate of opponents upon it being expanded (energeized).
* This is the usual fate of opponents upon it being expanded (energized).
* @see `Zone.serverSideDamage`
* @param dome force dome
* @return a list of affected entities
*/
def ForceDomeKills(dome: ForceDomePhysics): List[PlanetSideServerObject] = {
def ForceDomeKills(dome: ForceDomePhysics, perimeter: List[(Vector3, Vector3)]): List[PlanetSideServerObject] = {
Zone.serverSideDamage(
dome.Zone,
dome,
contactWithForceDome,
Zone.distanceCheck,
makesContactWithForceDome,
targetUnderForceDome(perimeter),
forceDomeTargets(dome.Definition.UseRadius, dome.Faction)
)
}
/**
* na
* Prepare damage information related to being caugt underneath the capitol force dome when it expands.
* @param source a game object that represents the source of the explosion
* @param target a game object that is affected by the explosion
* @return a `DamageInteraction` object
*/
private def contactWithForceDome(
private def makesContactWithForceDome(
source: PlanetSideGameObject with FactionAffinity with Vitality,
target: PlanetSideGameObject with FactionAffinity with Vitality
): DamageInteraction = {
@ -254,6 +227,84 @@ object ForceDomeControl {
/**
* na
* @see `Zone.distanceCheck`
* @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
* @return `true`, if target is detected within the force dome kill region
* `false`, otherwise
*/
private def targetUnderForceDome(
segments: List[(Vector3, Vector3)]
)
(
obj1: PlanetSideGameObject,
obj2: PlanetSideGameObject,
@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) =>
//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))
}
/**
* A function to assist line segment intersection tests.
* The important frame of reference is checking whether a hypothetical segment between a point and a target
* intersects with an established line segment between two other points.
* For our purposes, the resulting line segments will never be collinear, so there is no reason to test that.
* @param pointX x-coordinate used to create a test segment
* @param pointY y-coordinate used to create a test segment
* @param targetX x-coordinate of an important point for a test segment
* @param targetY y-coordinate of an important point for a test segment
* @param segmentPoint1x x-coordinate of one point from a segment
* @param segmentPoint1y y-coordinate of one point from a segment
* @param segmentPoint2x x-coordinate of a different point from a segment
* @param segmentPoint2y y-coordinate of a different point from a segment
* @return `true`, if the points form into two segments that intersect;
* `false`, otherwise
*/
private def segmentIntersectionTestPerSegment(
pointX: Float,
pointY: Float,
targetX: Float,
targetY: Float,
segmentPoint1x: Float,
segmentPoint1y: Float,
segmentPoint2x: Float,
segmentPoint2y: Float
): Boolean = {
//based on Franklin Antonio's "Faster Line Segment Intersection" topic "in Graphics Gems III" book (http://www.graphicsgems.org/)
//compare, java.awt.geom.Line2D.linesIntersect
val bx = segmentPoint1x - segmentPoint2x //delta-x of segment
val by = segmentPoint1y - segmentPoint2y //delta-y of segment
val cx = pointX - segmentPoint1x //delta-x of hypotenuse of triangle formed by center, segment endpoint, and intersection point
val cy = pointY - segmentPoint1y //delta-y of hypotenuse of triangle formed by center, segment endpoint, and intersection point
val alphaNumerator = by * cx - bx * cy
val commonDenominator = targetY * bx - targetX * by
val betaNumerator = targetX * cy - targetY * cx
if (
commonDenominator > 0 &&
(alphaNumerator < 0 || alphaNumerator > commonDenominator || betaNumerator < 0 || betaNumerator > commonDenominator)
) {
false
} else if (
commonDenominator < 0 &&
(alphaNumerator > 0 || alphaNumerator < commonDenominator || betaNumerator > 0 || betaNumerator < commonDenominator)
) {
false
} else {
//a collinear line test could go here, but we don't need it
true
}
}
/**
* Collect all enemy players, vehicles, and combat engineering deployables in a sector.
* @see `DamageWithPosition`
* @see `Zone.blockMap.sector`
* @param zone the zone in which the explosion should occur
@ -292,8 +343,13 @@ class ForceDomeControl(dome: ForceDomePhysics)
def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware = dome
def FactionObject: FactionAffinity = dome
/** a capitol force dome's owner should always be a facility, preferably the capitol facility of the continent;
* to save time, casted this entity and cache it for repeated use once;
* force dome is not immediately owned (by correct facility) so delay determination */
private lazy val domeOwnerAsABuilding = dome.Owner.asInstanceOf[Building]
/** ground-level perimeter of the force dome is defined by these segments (as vertex pairs) */
private val perimeterSegments: List[(Vector3, Vector3)] = ForceDomeControl.SetupForceDomePerimeter(dome)
/** force the dome into a certain state regardless of what conditions would normally transition it into that state */
private var customState: Option[Boolean] = None
def commonBehavior: Receive = checkBehavior
@ -325,6 +381,7 @@ class ForceDomeControl(dome: ForceDomePhysics)
customState = None
ForceDomeControl.NormalDomeStateMessage(domeOwnerAsABuilding)
ForceDomeControl.AlignForceDomeStatusAndUpdate(domeOwnerAsABuilding, dome)
ForceDomeControl.ForceDomeKills(dome, perimeterSegments)
}
def poweredStateLogic: Receive = {
@ -363,6 +420,10 @@ class ForceDomeControl(dome: ForceDomePhysics)
deenergizeUnlessSuppressedDueToCustomState()
}
/**
* Power down the force dome if it was previously being powered and
* as long as a custom state of being energized is not being enforced.
*/
private def deenergizeUnlessSuppressedDueToCustomState(): Unit = {
if (dome.Energized) {
if (customState.isEmpty) {
@ -374,25 +435,22 @@ class ForceDomeControl(dome: ForceDomePhysics)
}
/**
* na
* Yield to a custom value enforcing a certain force dome state - energized or powered down.
* If the custom state is not declared, run the function and analyze any change in the force dome's natural state.
* @param func function to run if not blocked
* @return next behavior for an actor state
* @return current energized state of the dome
*/
private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Unit): Unit = {
blockedByCustomStateOr(func, domeOwnerAsABuilding, dome)
}
/**
* na
* @param func function to run if not blocked
* @param building facility to operate upon (parameter to `func`)
* @return next behavior for an actor state
*/
private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Unit, building: Building, dome: ForceDomePhysics): Unit = {
private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Boolean): Boolean = {
customState match {
case None =>
func(building, dome)
val newState = func(domeOwnerAsABuilding, dome)
if (!dome.Energized && newState) {
ForceDomeControl.ForceDomeKills(dome, perimeterSegments)
}
newState
case Some(state) =>
ForceDomeControl.CustomDomeStateEnforcedMessage(building, state)
ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state)
state
}
}
}

View file

@ -2,8 +2,20 @@
package net.psforever.objects.serverobject.dome
import net.psforever.objects.serverobject.structures.AmenityDefinition
import net.psforever.types.Vector3
class ForceDomeDefinition(objectId: Int)
extends AmenityDefinition(objectId) {
Name = "force_dome"
/** offsets that define the perimeter of the pyramidal force "dome" barrier;
* these points are the closest to where the dome interacts with the ground at a corner;
* should be sequential, either clockwise or counterclockwise */
private var perimeter: List[Vector3] = List()
def PerimeterOffsets: List[Vector3] = perimeter
def PerimeterOffsets_=(points: List[Vector3]): List[Vector3] = {
perimeter = points
PerimeterOffsets
}
}