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

This commit is contained in:
Fate-JH 2025-12-22 20:58:15 -05:00
parent 4b3f8ea6c0
commit dd0f5fc928
6 changed files with 167 additions and 18 deletions

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects 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.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.ballistics.InteractWithRadiationClouds import net.psforever.objects.ballistics.InteractWithRadiationClouds
import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets} 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 InteractWithMines(range = 10, TriggerOnPlayerRule))
interaction(new InteractWithTurrets()) interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))
interaction(new InteractWithForceDomeProtection())
private var backpack: Boolean = false private var backpack: Boolean = false
private var released: Boolean = false private var released: Boolean = false

View file

@ -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
}
}

View file

@ -29,6 +29,12 @@ object ForceDomeControl {
final case object NormalBehavior extends Command 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. * 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
@ -202,8 +208,9 @@ object ForceDomeControl {
Zone.serverSideDamage( Zone.serverSideDamage(
dome.Zone, dome.Zone,
dome, dome,
ForceDomeExposure.damageProperties,
makesContactWithForceDome, makesContactWithForceDome,
targetUnderForceDome(perimeter), TargetUnderForceDome(perimeter),
forceDomeTargets(dome.Definition.UseRadius, dome.Faction) forceDomeTargets(dome.Definition.UseRadius, dome.Faction)
) )
} }
@ -235,14 +242,14 @@ object ForceDomeControl {
* @return `true`, if target is detected within the force dome kill region * @return `true`, if target is detected within the force dome kill region
* `false`, otherwise * `false`, otherwise
*/ */
private def targetUnderForceDome( def TargetUnderForceDome(
segments: List[(Vector3, Vector3)] segments: List[(Vector3, Vector3)]
) )
( (
obj1: PlanetSideGameObject, obj1: PlanetSideGameObject,
obj2: PlanetSideGameObject, obj2: PlanetSideGameObject,
@unused maxDistance: Float @unused maxDistance: Float
): Boolean = { ): Boolean = {
val centerPos @ Vector3(centerX, centerY, centerZ) = obj1.Position val centerPos @ Vector3(centerX, centerY, centerZ) = obj1.Position
val Vector3(targetX, targetY, targetZ) = obj2.Position - centerPos //deltas of segment of target to dome val Vector3(targetX, targetY, targetZ) = obj2.Position - centerPos //deltas of segment of target to dome
val checkForIntersection = segments.exists { case (point1, point2) => val checkForIntersection = segments.exists { case (point1, point2) =>
@ -343,12 +350,12 @@ class ForceDomeControl(dome: ForceDomePhysics)
def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware = dome def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware = dome
def FactionObject: FactionAffinity = dome def FactionObject: FactionAffinity = dome
/** a capitol force dome's owner should always be a facility, preferably the capitol facility of the continent; /** a capitol force dome's owner should always be a facility;
* to save time, casted this entity and cache it for repeated use once; * to save time, cast this entity and cache it for repeated use once;
* force dome is not immediately owned (by correct facility) so delay determination */ * force dome is not immediately owned by its correct facility so delay determination */
private lazy val domeOwnerAsABuilding = dome.Owner.asInstanceOf[Building] private lazy val domeOwnerAsABuilding = dome.Owner.asInstanceOf[Building]
/** ground-level perimeter of the force dome is defined by these segments (as vertex pairs) */ /** 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 */ /** force the dome into a certain state regardless of what conditions would normally transition it into that state */
private var customState: Option[Boolean] = None private var customState: Option[Boolean] = None
@ -382,6 +389,16 @@ class ForceDomeControl(dome: ForceDomePhysics)
ForceDomeControl.NormalDomeStateMessage(domeOwnerAsABuilding) ForceDomeControl.NormalDomeStateMessage(domeOwnerAsABuilding)
ForceDomeControl.AlignForceDomeStatusAndUpdate(domeOwnerAsABuilding, dome) ForceDomeControl.AlignForceDomeStatusAndUpdate(domeOwnerAsABuilding, dome)
ForceDomeControl.ForceDomeKills(dome, perimeterSegments) 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 = { 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. * 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. * 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 * @param func function to run if not blocked
* @return current energized state of the dome * @return current energized state of the dome
*/ */
private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Boolean): Boolean = { private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Boolean): Boolean = {
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
customState match { customState match {
case None => case None =>
val oldState = dome.Energized
val newState = func(domeOwnerAsABuilding, dome) val newState = func(domeOwnerAsABuilding, dome)
if (!dome.Energized && newState) { if (!oldState && newState) {
ForceDomeControl.ForceDomeKills(dome, perimeterSegments) //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 newState
case Some(state) => case Some(state) =>

View file

@ -1,12 +1,16 @@
// Copyright (c) 2025 PSForever // Copyright (c) 2025 PSForever
package net.psforever.objects.serverobject.dome 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.serverobject.structures.AmenityDefinition
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.types.Vector3 import net.psforever.types.Vector3
class ForceDomeDefinition(objectId: Int) class ForceDomeDefinition(objectId: Int)
extends AmenityDefinition(objectId) { extends AmenityDefinition(objectId) {
Name = "force_dome" Name = "force_dome"
Geometry = ForceDomeDefinition.representBy
/** offsets that define the perimeter of the pyramidal force "dome" barrier; /** 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; * these points are the closest to where the dome interacts with the ground at a corner;
* should be sequential, either clockwise or counterclockwise */ * should be sequential, either clockwise or counterclockwise */
@ -19,3 +23,22 @@ class ForceDomeDefinition(objectId: Int)
PerimeterOffsets 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)
}
}
}

View file

@ -10,6 +10,8 @@ class ForceDomePhysics(private val cfddef: ForceDomeDefinition)
with CaptureTerminalAware { with CaptureTerminalAware {
private var energized: Boolean = false private var energized: Boolean = false
private var perimeter: List[(Vector3, Vector3)] = List()
def Energized: Boolean = energized def Energized: Boolean = energized
def Energized_=(state: Boolean): Boolean = { def Energized_=(state: Boolean): Boolean = {
@ -17,6 +19,13 @@ class ForceDomePhysics(private val cfddef: ForceDomeDefinition)
Energized Energized
} }
def Perimeter: List[(Vector3, Vector3)] = perimeter
def Perimeter_=(list: List[(Vector3, Vector3)]): List[(Vector3, Vector3)] = {
perimeter = list
Perimeter
}
def Definition: ForceDomeDefinition = cfddef def Definition: ForceDomeDefinition = cfddef
} }

View file

@ -5,7 +5,7 @@ import net.psforever.objects.sourcing.{AmenitySource, SourceEntry}
import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions}
import net.psforever.objects.vital.base.{DamageReason, DamageResolution} import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.damage.DamageCalculations 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} import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel}
/** /**
@ -52,7 +52,7 @@ object ForceDomeExposure {
Model = SimpleResolutions.calculate Model = SimpleResolutions.calculate
} }
final val damageProperties = new DamageProperties { final val damageProperties = new DamageWithPosition {
Damage0 = 99999 Damage0 = 99999
DamageToHealthOnly = true DamageToHealthOnly = true
DamageToVehicleOnly = true DamageToVehicleOnly = true