From dd0f5fc928fc41b9def3ec31dcfd7d4e55dcef67 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 22 Dec 2025 20:58:15 -0500 Subject: [PATCH] force domes should be on the zone blockmap; correct issue with force dome death; interaction that sets players found under the force dome to be invulnerable works --- .../scala/net/psforever/objects/Player.scala | 3 +- .../InteractWithForceDomeProtection.scala | 89 +++++++++++++++++++ .../serverobject/dome/ForceDomeControl.scala | 57 ++++++++---- .../dome/ForceDomeDefinition.scala | 23 +++++ .../serverobject/dome/ForceDomePhysics.scala | 9 ++ .../objects/vital/etc/ForceDomeExposure.scala | 4 +- 6 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index c0023ebb6..4cb25871e 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.avatar.interaction.{TriggerOnPlayerRule, WithEntrance, WithGantry, WithLava, WithWater} +import net.psforever.objects.avatar.interaction.{InteractWithForceDomeProtection, TriggerOnPlayerRule, WithEntrance, WithGantry, WithLava, WithWater} import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry} import net.psforever.objects.ballistics.InteractWithRadiationClouds import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets} @@ -51,6 +51,7 @@ class Player(var avatar: Avatar) interaction(new InteractWithMines(range = 10, TriggerOnPlayerRule)) interaction(new InteractWithTurrets()) interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) + interaction(new InteractWithForceDomeProtection()) private var backpack: Boolean = false private var released: Boolean = false diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala b/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala new file mode 100644 index 000000000..fc4062791 --- /dev/null +++ b/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala @@ -0,0 +1,89 @@ +// Copyright (c) 2025 PSForever +package net.psforever.objects.avatar.interaction + +import net.psforever.objects.serverobject.damage.Damageable +import net.psforever.objects.serverobject.dome.{ForceDomeControl, ForceDomePhysics} +import net.psforever.objects.zones.blockmap.SectorPopulation +import net.psforever.objects.zones.{InteractsWithZone, ZoneInteraction, ZoneInteractionType} + +case object ForceZoneProtection extends ZoneInteractionType + +/** + * Entities under the capitol force dome that have not died in its initial activation + * do not take further damage until removed from under the dome or until the dome is deactivated. + */ +class InteractWithForceDomeProtection + extends ZoneInteraction { + def Type: ZoneInteractionType = ForceZoneProtection + + def range: Float = 10f + + /** increment to n, reevaluate the dome protecting the target, reset counter to 0 */ + private var protectSkipCounter: Int = 0 + /** dome currently protecting the target */ + private var protectedBy: Option[ForceDomePhysics] = None + + /** + * na + * @see `ForceDomeControl.TargetUnderForceDome` + * @param sector the portion of the block map being tested + * @param target the fixed element in this test + */ + def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { + if (protectSkipCounter < 4) { + protectSkipCounter += 1 + } else { + protectSkipCounter = 0 + protectedBy match { + case Some(dome) + if dome.Perimeter.isEmpty || + target.Zone != dome.Zone || + !ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(target, dome, maxDistance = 0f) => + resetInteraction(target) + case Some(_) => + () //no action + case None => + searchForInteractionCause(sector, target) + } + } + } + + /** + * Look the through the list of amenities in this sector for capitol force domes, + * determine which force domes are energized (activated, expanded, enveloping, etc.), + * and find the first active dome under which the target `entity` is positioned. + * The target `entity` is considered protected and can not be damaged until further notice. + * @see `Damageable.MakeInvulnerable` + * @see `ForceDomeControl.TargetUnderForceDome` + * @param sector – the portion of the block map being tested + * @param target – the fixed element in this test + * @return whichever force dome entity is detected to encircle this target `entity`, if any + */ + private def searchForInteractionCause(sector: SectorPopulation, target: InteractsWithZone): Option[ForceDomePhysics] = { + sector + .amenityList + .flatMap { + case dome: ForceDomePhysics if dome.Perimeter.nonEmpty => Some(dome) + case _ => None + } + .find { dome => + ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(target, dome, maxDistance = 0f) + } + .map { dome => + protectedBy = Some(dome) + target.Actor ! Damageable.MakeInvulnerable + dome + } + } + + /** + * na + * @see `Damageable.MakeVulnerable` + * @param target the fixed element in this test + */ + def resetInteraction(target: InteractsWithZone): Unit = { + protectSkipCounter = 0 + protectedBy = None + target.Actor ! Damageable.MakeVulnerable + } +} 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 4ee8e7261..8b66a5a1c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala @@ -29,6 +29,12 @@ object ForceDomeControl { final case object NormalBehavior extends Command + final case object ApplyProtection extends Command + + final case object RemoveProtection extends Command + + final case object Purge extends Command + /** * Dispatch a message to update the state of the clients with the server state of the capitol force dome. * @param dome force dome @@ -202,8 +208,9 @@ object ForceDomeControl { Zone.serverSideDamage( dome.Zone, dome, + ForceDomeExposure.damageProperties, makesContactWithForceDome, - targetUnderForceDome(perimeter), + TargetUnderForceDome(perimeter), forceDomeTargets(dome.Definition.UseRadius, dome.Faction) ) } @@ -235,14 +242,14 @@ object ForceDomeControl { * @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 = { + 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) => @@ -343,12 +350,12 @@ 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 */ + /** a capitol force dome's owner should always be a facility; + * to save time, cast this entity and cache it for repeated use once; + * force dome is not immediately owned by its 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) + private lazy 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 @@ -382,6 +389,16 @@ class ForceDomeControl(dome: ForceDomePhysics) ForceDomeControl.NormalDomeStateMessage(domeOwnerAsABuilding) ForceDomeControl.AlignForceDomeStatusAndUpdate(domeOwnerAsABuilding, dome) ForceDomeControl.ForceDomeKills(dome, perimeterSegments) + + case ForceDomeControl.ApplyProtection + if dome.Energized => + dome.Perimeter = perimeterSegments + + case ForceDomeControl.RemoveProtection => + dome.Perimeter = List.empty + + case ForceDomeControl.Purge => + ForceDomeControl.ForceDomeKills(dome, perimeterSegments) } def poweredStateLogic: Receive = { @@ -437,15 +454,25 @@ class ForceDomeControl(dome: ForceDomePhysics) /** * 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. + * Apply changes to region represented as "bound" by the perimeter as indicated by a state change. * @param func function to run if not blocked * @return current energized state of the dome */ private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Boolean): Boolean = { + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + customState match { case None => + val oldState = dome.Energized val newState = func(domeOwnerAsABuilding, dome) - if (!dome.Energized && newState) { - ForceDomeControl.ForceDomeKills(dome, perimeterSegments) + if (!oldState && newState) { + //dome activating + context.system.scheduler.scheduleOnce(delay = 1500 milliseconds, self, ForceDomeControl.Purge) + context.system.scheduler.scheduleOnce(delay = 4000 milliseconds, self, ForceDomeControl.ApplyProtection) + } else if (oldState && !newState) { + //dome de-activating + dome.Zone.blockMap.removeFrom(dome) } newState case Some(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 e5858502c..e6df1ecdb 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala @@ -1,12 +1,16 @@ // Copyright (c) 2025 PSForever 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) extends AmenityDefinition(objectId) { Name = "force_dome" + Geometry = ForceDomeDefinition.representBy + /** 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 */ @@ -19,3 +23,22 @@ class ForceDomeDefinition(objectId: Int) PerimeterOffsets } } + +object ForceDomeDefinition { + /** + * na + * @param o na + * @return na + */ + def representBy(o: Any): VolumetricGeometry = { + import net.psforever.objects.geometry.GeometryForm.invalidPoint + o match { + case fdp: ForceDomePhysics => + Sphere(fdp.Position, fdp.Definition.UseRadius) + case s: SourceEntry => + Sphere(s.Position, s.Definition.UseRadius) + case _ => + Sphere(invalidPoint, 1f) + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala index 88d62963c..2b2641537 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala @@ -10,6 +10,8 @@ class ForceDomePhysics(private val cfddef: ForceDomeDefinition) with CaptureTerminalAware { private var energized: Boolean = false + private var perimeter: List[(Vector3, Vector3)] = List() + def Energized: Boolean = energized def Energized_=(state: Boolean): Boolean = { @@ -17,6 +19,13 @@ class ForceDomePhysics(private val cfddef: ForceDomeDefinition) Energized } + def Perimeter: List[(Vector3, Vector3)] = perimeter + + def Perimeter_=(list: List[(Vector3, Vector3)]): List[(Vector3, Vector3)] = { + perimeter = list + Perimeter + } + def Definition: ForceDomeDefinition = cfddef } diff --git a/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala b/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala index d09ddd880..557a7e061 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala @@ -5,7 +5,7 @@ import net.psforever.objects.sourcing.{AmenitySource, SourceEntry} import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} import net.psforever.objects.vital.base.{DamageReason, DamageResolution} import net.psforever.objects.vital.damage.DamageCalculations -import net.psforever.objects.vital.prop.DamageProperties +import net.psforever.objects.vital.prop.{DamageProperties, DamageWithPosition} import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel} /** @@ -52,7 +52,7 @@ object ForceDomeExposure { Model = SimpleResolutions.calculate } - final val damageProperties = new DamageProperties { + final val damageProperties = new DamageWithPosition { Damage0 = 99999 DamageToHealthOnly = true DamageToVehicleOnly = true