From 4b3f8ea6c054e96892c0da174707c6d3a77610e6 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 18 Dec 2025 19:29:06 -0500 Subject: [PATCH] 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 --- .../zone/building/MajorFacilityLogic.scala | 11 +- .../GlobalDefinitionsMiscellaneous.scala | 40 +++ .../serverobject/damage/Damageable.scala | 32 ++- .../serverobject/dome/ForceDomeControl.scala | 250 +++++++++++------- .../dome/ForceDomeDefinition.scala | 12 + 5 files changed, 245 insertions(+), 100 deletions(-) diff --git a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala index 00325776c..24825d7f4 100644 --- a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala +++ b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala @@ -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 } diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala index 9714631bd..6a5e90721 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala @@ -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) + ) } } diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala index 7f1863ffb..b6da16d55 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala @@ -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` diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala index 12c438043..4ee8e7261 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala @@ -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 } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala index b362682e6..e5858502c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala @@ -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 + } }