From f557ecc13d2205dfb038cad23fd1d2cf0d3efa01 Mon Sep 17 00:00:00 2001 From: "Jason_DiDonato@yahoo.com" Date: Tue, 12 Jan 2021 23:01:08 -0500 Subject: [PATCH 1/6] using the boomer trigger now causes boomers to explode and harm targets --- .../resources/overrides/game_objects0.adb.lst | 4 +- .../actors/session/SessionActor.scala | 35 ++++++---- .../objects/ExplosiveDeployable.scala | 64 ++++++++++++++++--- .../objects/vital/etc/TriggerUsedReason.scala | 61 ++++++++++++++++++ 4 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala diff --git a/server/src/main/resources/overrides/game_objects0.adb.lst b/server/src/main/resources/overrides/game_objects0.adb.lst index 2b6529eac..a44d9237b 100644 --- a/server/src/main/resources/overrides/game_objects0.adb.lst +++ b/server/src/main/resources/overrides/game_objects0.adb.lst @@ -1,7 +1,7 @@ -add_property ace allowed false +add_property ace allowed true add_property ace equiptime 500 add_property ace holstertime 500 -add_property ace_deployable allowed false +add_property ace_deployable allowed true add_property ace_deployable equiptime 500 add_property ace_deployable holstertime 500 add_property advanced_ace equiptime 750 diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index f29757b02..d0b120fbe 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -2184,6 +2184,17 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (player.HasGUID) player.GUID else PlanetSideGUID(0) reply match { + case LocalResponse.AlertDestroyDeployable(obj: BoomerDeployable) => + //the (former) owner (obj.OwnerName) should process this message + obj.Trigger match { + case Some(item: BoomerTrigger) => + FindEquipmentToDelete(item.GUID, item) + item.Companion = None + case _ => ; + } + avatar.deployables.Remove(obj) + UpdateDeployableUIElements(avatar.deployables.UpdateUIElement(obj.Definition.Item)) + case LocalResponse.AlertDestroyDeployable(obj) => //the (former) owner (obj.OwnerName) should process this message avatar.deployables.Remove(obj) @@ -2194,17 +2205,17 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo)) } - case LocalResponse.Detonate(guid, obj: BoomerDeployable) => - sendResponse(TriggerEffectMessage(guid, "detonate_boomer")) - sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) - sendResponse(ObjectDeleteMessage(guid, 0)) + case LocalResponse.Detonate(dguid, obj: BoomerDeployable) => + sendResponse(TriggerEffectMessage(dguid, "detonate_boomer")) + sendResponse(PlanetsideAttributeMessage(dguid, 29, 1)) + sendResponse(ObjectDeleteMessage(dguid, 0)) - case LocalResponse.Detonate(guid, obj: ExplosiveDeployable) => - sendResponse(GenericObjectActionMessage(guid, 19)) - sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) - sendResponse(ObjectDeleteMessage(guid, 0)) + case LocalResponse.Detonate(dguid, obj: ExplosiveDeployable) => + sendResponse(GenericObjectActionMessage(dguid, 19)) + sendResponse(PlanetsideAttributeMessage(dguid, 29, 1)) + sendResponse(ObjectDeleteMessage(dguid, 0)) - case LocalResponse.Detonate(guid, obj) => + case LocalResponse.Detonate(_, obj) => log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly") case LocalResponse.DoorOpens(door_guid) => @@ -4039,13 +4050,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) continent.GUID(trigger.Companion) match { case Some(boomer: BoomerDeployable) => - boomer.Destroyed = true - continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.Detonate(boomer.GUID, boomer)) - Deployables.AnnounceDestroyDeployable(boomer, Some(500 milliseconds)) + boomer.Actor ! CommonMessages.Use(player, Some(trigger)) case Some(_) | None => ; } - FindEquipmentToDelete(item_guid, trigger) - trigger.Companion = None case _ => ; } progressBarUpdate.cancel() diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 8b749a608..591d004fb 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -2,16 +2,18 @@ package net.psforever.objects import akka.actor.{Actor, ActorContext, Props} +import net.psforever.objects.ballistics.{PlayerSource, SourceEntry} import net.psforever.objects.ce._ import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition} import net.psforever.objects.definition.converter.SmallDeployableConverter import net.psforever.objects.equipment.JammableUnit -import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity} import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.vital.resolution.ResolutionCalculations.Output import net.psforever.objects.vital.SimpleResolutions -import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.vital.etc.TriggerUsedReason +import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.zones.Zone import net.psforever.types.Vector3 @@ -21,7 +23,9 @@ import net.psforever.services.local.{LocalAction, LocalServiceMessage} import scala.concurrent.duration._ -class ExplosiveDeployable(cdef: ExplosiveDeployableDefinition) extends ComplexDeployable(cdef) with JammableUnit { +class ExplosiveDeployable(cdef: ExplosiveDeployableDefinition) + extends ComplexDeployable(cdef) + with JammableUnit { override def Definition: ExplosiveDeployableDefinition = cdef } @@ -63,6 +67,24 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D def receive: Receive = takesDamage .orElse { + case CommonMessages.Use(player, Some(trigger: BoomerTrigger)) if { + mine match { + case boomer: BoomerDeployable => boomer.Trigger.contains(trigger) && mine.Definition.Damageable + case _ => false + } + } => + // the mine damages itself, which sets it off, which causes an explosion + // think of this as an initiator to the proper explosion + mine.Destroyed = true + ExplosiveDeployableControl.DamageResolution( + mine, + DamageInteraction( + SourceEntry(mine), + TriggerUsedReason(PlayerSource(player), trigger), + mine.Position + ).calculate()(mine), + damage = 0 + ) case _ => ; } @@ -84,9 +106,18 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D } object ExplosiveDeployableControl { + /** + * na + * @param target na + * @param cause na + * @param damage na + */ def DamageResolution(target: ExplosiveDeployable, cause: DamageResult, damage: Int): Unit = { target.History(cause) - if (target.Health == 0) { + if (cause.interaction.cause.source.SympatheticExplosion) { + explodes(target, cause) + DestructionAwareness(target, cause) + } else if (target.Health == 0) { DestructionAwareness(target, cause) } else if (!target.Jammed && Damageable.CanJammer(target, cause.interaction)) { if ( { @@ -99,17 +130,27 @@ object ExplosiveDeployableControl { } } ) { - if (cause.interaction.cause.source.SympatheticExplosion || target.Definition.DetonateOnJamming) { - val zone = target.Zone - zone.Activity ! Zone.HotSpot.Activity(cause) - zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.Detonate(target.GUID, target)) - Zone.causeExplosion(zone, target, Some(cause)) + if (target.Definition.DetonateOnJamming) { + explodes(target, cause) } DestructionAwareness(target, cause) } } } + /** + * na + * @param target na + * @param cause na + */ + def explodes(target: Damageable.Target, cause: DamageResult): Unit = { + target.Health = 1 // short-circuit logic in DestructionAwareness + val zone = target.Zone + zone.Activity ! Zone.HotSpot.Activity(cause) + zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.Detonate(target.GUID, target)) + Zone.causeExplosion(zone, target, Some(cause)) + } + /** * na * @param target na @@ -118,8 +159,11 @@ object ExplosiveDeployableControl { def DestructionAwareness(target: ExplosiveDeployable, cause: DamageResult): Unit = { val zone = target.Zone val attribution = DamageableEntity.attributionTo(cause, target.Zone) + Deployables.AnnounceDestroyDeployable( + target, + Some(if (target.Jammed || target.Destroyed) 0 seconds else 500 milliseconds) + ) target.Destroyed = true - Deployables.AnnounceDestroyDeployable(target, Some(if (target.Jammed) 0 seconds else 500 milliseconds)) zone.AvatarEvents ! AvatarServiceMessage( zone.id, AvatarAction.Destroy(target.GUID, attribution, Service.defaultPlayerGUID, target.Position) diff --git a/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala b/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala new file mode 100644 index 000000000..575613366 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala @@ -0,0 +1,61 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.vital.etc + +import net.psforever.objects.BoomerTrigger +import net.psforever.objects.ballistics.{PlayerSource, SourceEntry} +import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} +import net.psforever.objects.vital.base.{DamageReason, DamageResolution} +import net.psforever.objects.vital.damage.DamageCalculations.AgainstExoSuit +import net.psforever.objects.vital.prop.DamageProperties +import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel} + +/** + * A wrapper for a "damage source" in damage calculations + * that parameterizes information necessary to explain a `BoomerDeployable` being detonated + * using its complementary trigger. + * Should be applied as the reason applied to the Boomer + * in `DamageInteractions` that lead up to the Boomer exploding + * which will carry the trigger as the reason and the user as the culprit. + * Due to faction affiliation complicity between the user and the Boomer, however, + * normal `Damageable` functionality would have to interject in a way where the trigger works anyway. + * @see `BoomerDeployable` + * @see `BoomerTrigger` + * @see `DamageCalculations` + * @see `VitalityDefinition.DamageableByFriendlyFire` + * @param user the player who is holding the trigger + * @param item the trigger + */ +final case class TriggerUsedReason(user: PlayerSource, item: BoomerTrigger) + extends DamageReason { + def source: DamageProperties = TriggerUsedReason.triggered + + def resolution: DamageResolution.Value = DamageResolution.Resolved + + def same(test: DamageReason): Boolean = test match { + case tur: TriggerUsedReason => tur.item eq item + case _ => false + } + + /** lay the blame on the player who caused this explosion to occur */ + def adversary: Option[SourceEntry] = Some(user) + + override def damageModel : DamageAndResistance = TriggerUsedReason.drm + + /** while weird, the trigger was accredited as the method of death on Gemini Live; + * even though its icon looks like an misshapen AMS */ + override def attribution: Int = item.Definition.ObjectId +} + +object TriggerUsedReason { + private val triggered = new DamageProperties { + Damage0 = 1 //token damage + SympatheticExplosion = true //sets off a boomer + } + + /** basic damage, no resisting, quick and simple */ + private val drm = new DamageResistanceModel { + DamageUsing = AgainstExoSuit + ResistUsing = NoResistanceSelection + Model = SimpleResolutions.calculate + } +} \ No newline at end of file From 4304ea7f4dbfa5efa1a0b8aa680e7b97bbc7195f Mon Sep 17 00:00:00 2001 From: "Jason_DiDonato@yahoo.com" Date: Wed, 20 Jan 2021 22:56:29 -0500 Subject: [PATCH 2/6] line and line segment intersection code and tests --- .../actors/session/SessionActor.scala | 2 +- .../objects/ExplosiveDeployable.scala | 2 +- .../objects/geometry/ClosestDistance.scala | 173 ++++++++++++++++ .../psforever/objects/geometry/Geometry.scala | 79 +++++++ .../objects/geometry/Intersection.scala | 107 ++++++++++ .../scala/net/psforever/types/Vector3.scala | 8 + src/test/scala/objects/GeometryTest.scala | 194 ++++++++++++++++++ 7 files changed, 563 insertions(+), 2 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/Geometry.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/Intersection.scala create mode 100644 src/test/scala/objects/GeometryTest.scala diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index d0b120fbe..f10a9ca6e 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -1433,7 +1433,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con deadState = DeadState.RespawnTime session = session.copy(player = new Player(avatar)) - //xy-coordinates indicate sanctuary spawn bias: + //ay-coordinates indicate sanctuary spawn bias: player.Position = math.abs(scala.util.Random.nextInt() % avatar.name.hashCode % 4) match { case 0 => Vector3(8192, 8192, 0) //NE case 1 => Vector3(8192, 0, 0) //SE diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 591d004fb..812fd4072 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -73,7 +73,7 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D case _ => false } } => - // the mine damages itself, which sets it off, which causes an explosion + // the trigger damages the mine, which sets it off, which causes an explosion // think of this as an initiator to the proper explosion mine.Destroyed = true ExplosiveDeployableControl.DamageResolution( diff --git a/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala b/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala new file mode 100644 index 000000000..ddd5ab15f --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala @@ -0,0 +1,173 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.geometry + +import net.psforever.types.Vector3 + +object ClosestDistance { + object Between { + def apply(origin1 : Vector3, origin2 : Vector3, point : Vector3, seg : Segment2D) : Float = { + val segdx = seg.bx - seg.ax + val segdy = seg.by - seg.ay + ((point.x + origin1.x - seg.ax + origin2.x) * segdx + (point.y + origin1.y - seg.ay + origin2.y) * segdy) / + Vector3.MagnitudeSquared(Vector3(segdx, segdy, 0)) + } + + def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line2D, line2 : Line2D) : Float = { + if (Intersection.Test(origin1, origin2, line1, line2)) { //intersecting lines + 0f + } else { + math.abs( + Vector3.DotProduct( + Vector3(line2.x - line1.x, line2.y - line1.y, 0), + Vector3(-1/line1.d.y, 1/line1.d.x, 0) + ) + ) + } + } + + def apply(origin1: Vector3, origin2: Vector3, seg1: Segment2D, seg2: Segment2D): Float = { + if (Intersection.Test(origin1, origin2, seg1, seg2)) { //intersecting line segments + 0f + } else { + val v1a = Vector3(seg1.ax, seg1.ay, 0) + val v2a = Vector3(seg2.ax, seg2.ay, 0) + val v1b = Vector3(seg1.bx, seg1.by, 0) + val v2b = Vector3(seg2.bx, seg2.by, 0) + math.min( + apply(origin1, origin2, v1a, seg2), + math.min( + apply(origin1, origin2, v1b, seg2), + math.min( + apply(origin1, origin2, v2a, seg1), + apply(origin1, origin2, v2b, seg1) + ) + ) + ) + } + } + + def apply(origin1: Vector3, origin2: Vector3, line1: Line3D, line2: Line3D): Float = { + val cross = Vector3.CrossProduct(line1.d, line2.d) + if(cross != Vector3.Zero) { + math.abs( + Vector3.DotProduct(cross, Vector3(line1.x - line2.x, line1.y - line2.y, line1.z - line2.z)) + ) / Vector3.Magnitude(cross) + } else { + //lines are parallel + Vector3.Magnitude( + Vector3.CrossProduct( + line1.d, + Vector3(line2.x - line1.x, line2.y - line1.y, line2.z - line1.z) + ) + ) + } + } + + def apply(origin1: Vector3, origin2: Vector3, seg1: Segment3D, seg2: Segment3D): Float = { + //TODO make not as expensive as finding the plotted closest distance segment + Plotted(origin1, origin2, seg1, seg2) match { + case Some(seg) => seg.length + case None => Float.MaxValue + } + } + } + + object Plotted { + /** + * na + * This function can only operate normally if a perpendicular line segment between the two lines can be established, + * this is, if the cross product of the two lines exists. + * As such, for coincidental lines, a segment of zero length from the first line's point is produced. + * @param origin1 na + * @param origin2 na + * @param line1 na + * @param line2 na + * @return na + */ + def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line3D, line2 : Line3D): Option[Segment3D] = { + val p1 = Vector3(line1.x, line1.y, line1.z) + val p2 = p1 + line1.d + val p3 = Vector3(line2.x, line2.y, line2.z) + val p4 = p3 + line2.d + val p13 = p1 - p3 // vector between point on first line and point on second line + val p43 = line2.d + val p21 = line1.d + if (Vector3.MagnitudeSquared(p43) < Float.MinPositiveValue || + Vector3.MagnitudeSquared(p21) < Float.MinPositiveValue) { + None + } else { + val d2121 = Vector3.MagnitudeSquared(p21) + val d4343 = Vector3.MagnitudeSquared(p43) + val d4321 = Vector3.DotProduct(p43, p21) + val denom = d2121 * d4343 - d4321 * d4321 // n where d = (m/n) and a(x,y,z) + d * V = b(x,y,z) for line1 + if (math.abs(denom) < Float.MinPositiveValue) { + val p13u = Vector3.Unit(p13) + if (p21 == p13u || p21 == Vector3.neg(p13u)) { //coincidental lines + // can not produce a valid cross product, but a coincidental line does produce an overlap + Some(Segment3D( + line1.x, line1.y, line1.z, + line1.x, line1.y, line1.z + )) + } else { + None + } + } else { + val d1343 = Vector3.DotProduct(p13, p43) + val numer = d1343 * d4321 -d4343 * Vector3.DotProduct(p13, p21) // m where d = (m/n) and ..., etc. + val mua = numer / denom + val mub = (d1343 + d4321 * mua) / d4343 + Some(Segment3D( + p1.x + mua * p21.x, + p1.y + mua * p21.y, + p1.z + mua * p21.z, + p3.x + mub * p43.x, + p3.y + mub * p43.y, + p3.z + mub * p43.z + )) + } + } + } + + def apply(origin1 : Vector3, origin2 : Vector3, line1 : Segment3D, line2 : Segment3D): Option[Segment3D] = { + val uline1 = Vector3.Unit(line1.d) + val uline2 = Vector3.Unit(line2.d) + apply( + origin1, + origin2, + Line3D(line1.ax, line1.ay, line1.az, uline1), + Line3D(line2.ax, line2.ay, line2.az, uline2) + ) match { + case out @ Some(seg: Segment3D) + if seg.length == 0 && (uline1 == uline2 || uline1 == Vector3.neg(uline2)) => //coincidental lines + out + case Some(seg: Segment3D) => //segment of shortest distance when two segments treated as lines + val sega = Vector3(seg.ax, seg.ay, seg.az) + val p1 = Vector3(line1.ax, line1.ay, line1.az) + val d1 = sega - p1 + val out1 = if (!Geometry.equalVectors(Vector3.Unit(d1), uline1)) { //clamp seg.a(xyz) to segment line1's bounds + p1 + } else if (Vector3.MagnitudeSquared(d1) > Vector3.MagnitudeSquared(line1.d)) { + Vector3(line1.bx, line1.by, line1.bz) + } else { + sega + } + val segb = Vector3(seg.bx, seg.by, seg.bz) + val p2 = Vector3(line2.ax, line2.ay, line2.az) + val d2 = segb - p2 + val out2 = if (!Geometry.equalVectors(Vector3.Unit(d2), uline2)) { //clamp seg.b(xyz) to segment line2's bounds + p2 + } else if (Vector3.MagnitudeSquared(d2) > Vector3.MagnitudeSquared(line2.d)) { + Vector3(line2.bx, line2.by, line2.bz) + } else { + segb + } + Some(Segment3D( + out1.x, out1.y, out1.z, + out2.x, out2.y, out2.z + )) + case None => + None + } + } + } +} diff --git a/src/main/scala/net/psforever/objects/geometry/Geometry.scala b/src/main/scala/net/psforever/objects/geometry/Geometry.scala new file mode 100644 index 000000000..4233b6897 --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/Geometry.scala @@ -0,0 +1,79 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.geometry + +import net.psforever.types.Vector3 + +trait Slope { + def d: Vector3 + + def length: Float +} + +trait Line extends Slope { + assert({ + val mag = Vector3.Magnitude(d) + mag - 0.05f < 1f && mag + 0.05f > 1f + }, "not a unit vector") + + def length: Float = Float.PositiveInfinity +} + +trait Segment extends Slope { + def length: Float = Vector3.Magnitude(d) +} + +final case class Line2D(x: Float, y: Float, d: Vector3) extends Line + +object Line2D { + def apply(ax: Float, ay: Float, bx: Float, by: Float): Line2D = { + Line2D(ax, ay, Vector3.Unit(Vector3(bx-ax, by-ay, 0))) + } +} + +final case class Segment2D(ax: Float, ay: Float, bx: Float, by: Float) extends Segment { + def d: Vector3 = Vector3(bx - ax, by - ay, 0) +} + +object Segment2D { + def apply(x: Float, y: Float, z: Float, d: Vector3): Segment2D = { + Segment2D(x, y, x + d.x, y + d.y) + } +} + +final case class Line3D(x: Float, y: Float, z: Float, d: Vector3) extends Line + +final case class Segment3D(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float) extends Segment { + def d: Vector3 = Vector3(bx - ax, by - ay, bz - az) +} + +object Segment3D { + def apply(x: Float, y: Float, z: Float, d: Vector3): Segment3D = { + Segment3D(x, y, z, z+d.x, y+d.y, z+d.z) + } +} + +object Geometry { + def equalFloats(value1: Float, value2: Float, off: Float = 0.001f): Boolean = { + val diff = value1 - value2 + (diff >= 0 && diff <= off) || diff > -off + } + + def equalVectors(value1: Vector3, value2: Vector3, off: Float = 0.001f): Boolean = { + equalFloats(value1.x, value2.x, off) && + equalFloats(value1.y, value2.y, off) && + equalFloats(value1.z, value2.z, off) + } + + def closeToInsignificance(d: Float, epsilon: Float = 10f): Float = { + val ulp = math.ulp(epsilon) + math.signum(d) match { + case -1f => + val n = math.abs(d) + val p = math.abs(n - n.toInt) + if (p < ulp || d > ulp) d + p else d + case _ => + val p = math.abs(d - d.toInt) + if (p < ulp || d < ulp) d - p else d + } + } +} diff --git a/src/main/scala/net/psforever/objects/geometry/Intersection.scala b/src/main/scala/net/psforever/objects/geometry/Intersection.scala new file mode 100644 index 000000000..74dc781cb --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/Intersection.scala @@ -0,0 +1,107 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.geometry + +import net.psforever.types.Vector3 + +object Intersection { + object Test { + /** + * Do these two lines intersect? + * Lines in 2D space will always intersect unless they are parallel or antiparallel. + * In that case, they can still "intersect" if the lines are coincidental. + */ + def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line2D, line2 : Line2D): Boolean = { + line1.d != line2.d || { + //parallel or antiparallel? + val u = Vector3.Unit(Vector3(line2.x - line1.x, line2.y - line1.y, 0)) + line1.d == u || line1.d == Vector3.neg(u) + } + } + + private def pointOnSegment(ax : Float, ay : Float, px : Float, py : Float, bx : Float, by : Float): Boolean = { + px <= math.max(ax, bx) && px >= math.min(ax, bx) && py <= math.max(ay, by) && py >= math.min(ay, by) + } + + object PointTripleOrientation extends Enumeration { + val Colinear, Clockwise, Counterclockwise = Value + } + + /** + * Determine the orientation of the given three two-dimensional points. + * Any triple has one of three orientations: + * clockwise - the third point is to the right side of a line plotted by the first two points; + * counterclockwise - the third point is to the left side of a line plotted by the first two points; + * and, colinear - the third point is reachable along the line plotted by the first two points. + * @param ax x-coordinate of the first point + * @param ay y-coordinate of the first point + * @param px x-coordinate of the second point + * @param py y-coordinate of the second point + * @param bx x-coordinate of the third point + * @param by y-coordinate of the third point + * @return the orientation value + */ + private def orientationOfPoints( + ax : Float, ay : Float, + px : Float, py : Float, + bx : Float, by : Float + ): PointTripleOrientation.Value = { + val out = (py - ay) * (bx - px) - (px - ax) * (by - py) + if (out == 0) PointTripleOrientation.Colinear + else if (out > 0) PointTripleOrientation.Clockwise + else PointTripleOrientation.Counterclockwise + } + + /** + * Do these two line segments intersect? + * Intersection of two two-dimensional line segments can be determined by the orientation of their endpoints. + * If a test of multiple ordered triple points reveals that certain triples have different orientations, + * then we can safely assume the intersection state of the segments. + */ + def apply(origin1 : Vector3, origin2 : Vector3, line1 : Segment2D, line2 : Segment2D): Boolean = { + //setup + val ln1ax = line1.ax + origin1.x + val ln1ay = line1.ay + origin1.y + val ln1bx = ln1ax + origin1.x + val ln1by = ln1ay + origin1.y + val ln2ax = line2.ax + origin2.x + val ln2ay = line2.ay + origin2.y + val ln2bx = ln2ax + origin2.x + val ln2by = ln2ay + origin2.y + val ln1_ln2a = orientationOfPoints(ln1ax, ln1ay, ln1bx, ln1by, ln2ax, ln2ay) + val ln1_ln2b = orientationOfPoints(ln1ax, ln1ay, ln1bx, ln1by, ln2bx, ln2by) + val ln2_ln1a = orientationOfPoints(ln2ax, ln2ay, ln2bx, ln2by, ln1ax, ln1ay) + val ln2_ln1b = orientationOfPoints(ln2ax, ln2ay, ln2bx, ln2by, ln1bx, ln1by) + //results + import PointTripleOrientation._ + (ln1_ln2a != ln1_ln2b && ln2_ln1a != ln2_ln1b) || + (ln1_ln2a == Colinear && pointOnSegment(ln1ax, ln1ay, ln2ax, ln2ay, ln1bx, ln1by)) || // line2 A is on line1 + (ln1_ln2b == Colinear && pointOnSegment(ln1ax, ln1ay, ln2bx, ln2by, ln1bx, ln1by)) || // line2 B is on line1 + (ln2_ln1a == Colinear && pointOnSegment(ln2ax, ln2ay, ln1ax, ln1ay, ln2bx, ln2by)) || // line1 A is on line2 + (ln2_ln1b == Colinear && pointOnSegment(ln2ax, ln2ay, ln1bx, ln1by, ln2bx, ln2by)) // line1 B is on line2 + } + + /** + * Do these two lines intersect? + * Actual mathematically-sound intersection between lines and line segments in 3D-space is terribly uncommon. + * Instead, check that the closest distance between two line segments is below a threshold value. + */ + def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line3D, line2 : Line3D): Boolean = { + apply(origin1, origin2, line1, line2, 0.15f) + } + def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line3D, line2 : Line3D, threshold: Float): Boolean = { + ClosestDistance.Between(origin1, origin2, line1, line2) < threshold + } + + /** + * Do these two line segments intersect? + * Actual mathematically-sound intersection between lines and line segments in 3D-space is terribly uncommon. + * Instead, check that the closest distance between two line segments is below a threshold value. + */ + def apply(origin1 : Vector3, origin2 : Vector3, seg1 : Segment3D, seg2 : Segment3D): Boolean = { + apply(origin1, origin2, seg1, seg2, 0.15f) + } + def apply(origin1 : Vector3, origin2 : Vector3, seg1 : Segment3D, seg2 : Segment3D, threshold: Float): Boolean = { + ClosestDistance.Between(origin1, origin2, seg1, seg2) < threshold + } + } +} diff --git a/src/main/scala/net/psforever/types/Vector3.scala b/src/main/scala/net/psforever/types/Vector3.scala index 43c4f43f6..e008c7a2a 100644 --- a/src/main/scala/net/psforever/types/Vector3.scala +++ b/src/main/scala/net/psforever/types/Vector3.scala @@ -117,6 +117,14 @@ object Vector3 { */ def z(value: Float): Vector3 = Vector3(0, 0, value) + /** + * Calculate the negation of this vector, + * the same vector in the antiparallel direction. + * @param v the original vector + * @return the negation of the original vector + */ + def neg(v: Vector3): Vector3 = Vector3(-v.x, -v.y, -v.z) + /** * Calculate the actual distance between two points. * @param pos1 the first point diff --git a/src/test/scala/objects/GeometryTest.scala b/src/test/scala/objects/GeometryTest.scala new file mode 100644 index 000000000..71b4734ee --- /dev/null +++ b/src/test/scala/objects/GeometryTest.scala @@ -0,0 +1,194 @@ +// Copyright (c) 2020 PSForever +package objects + +import net.psforever.objects.geometry.{Intersection, Line3D, Segment3D} +import net.psforever.types.Vector3 +import org.specs2.mutable.Specification + +class IntersectionTest extends Specification { + "Line3D" should { + "detect intersection on target point(s)" in { + //these lines intersect at (0, 0, 0) + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Line3D(0,0,0, Vector3(1,0,0)), + Line3D(0,0,0, Vector3(0,1,0)) + ) + result mustEqual true + } + + "detect intersection on a target point" in { + //these lines intersect at (0, 0, 0); start of segment 1, middle of segment 2 + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Line3D(0,0,0, Vector3(0,1,0)), + Line3D(-1,0,0, Vector3(1,0,0)) + ) + result mustEqual true + } + + "detect intersection in the middle(s)" in { + //these lines intersect at (0.5f, 0.5f, 0) + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Line3D(0,0,0, Vector3.Unit(Vector3(1, 1, 0))), + Line3D(1,0,0, Vector3(0,1,0)) + ) + result mustEqual true + } + + "detect intersection in the middle " in { + //these lines intersect at (0, 0.5, 0) + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Line3D(0,0,0, Vector3(1,0,0)), + Line3D(0.5f,1,0, Vector3.Unit(Vector3(0.5f,-1,0))) + ) + result mustEqual true + } + + "detect intersection if the point of intersection would be before the start of the segments" in { + //these lines would intersect at (0, 0, 0) + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Line3D(1,1,0, Vector3.Unit(Vector3(2, 2, 0))), + Line3D(1,0,0, Vector3.Unit(Vector3(2,0,0))) + ) + result mustEqual true + } + + "detect intersection if the point of intersection would be after the end of the segments" in { + //these lines would intersect at (2, 2, 0) + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Line3D(0,0,0, Vector3.Unit(Vector3(1,1,0))), + Line3D(2,0,0, Vector3.Unit(Vector3(2,1,0))) + ) + result mustEqual true + } + + "not detect intersection if the line segments are parallel" in { + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Line3D(0,0,0, Vector3.Unit(Vector3(1,1,1))), + Line3D(1,1,2, Vector3.Unit(Vector3(1,1,1))) + ) + result mustEqual false + } + + "detect overlap" in { + //the sub-segment (1,0,0) to (2,0,0) is an overlap region shared between the two segments + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Line3D(0,0,0, Vector3.Unit(Vector3(2,0,0))), + Line3D(1,0,0, Vector3.Unit(Vector3(3,0,0))) + ) + result mustEqual true + } + + "not detect intersection (generic skew)" in { + //these segments will not intersect + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(-3,-8,7, Vector3.Unit(Vector3(-3,-9,8))), + Segment3D(6,3,0, Vector3.Unit(Vector3(2,0,0))) + ) + result mustEqual false + } + } + + "Segment3D" should { + "detect intersection of the first point(s)" in { + //these segments intersect at (0, 0, 0) + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(0,0,0, 1,0,0), + Segment3D(0,0,0, 0,1,0) + ) + result mustEqual true + } + + "detect intersection of the first point" in { + //these segments intersect at (0, 0, 0); start of segment 1, middle of segment 2 + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(0,0,0, 0,2,0), + Segment3D(-1,0,0, 1,0,0) + ) + result mustEqual true + } + + "detect intersection on the farther point(s)" in { + //these segments intersect at (0, 1, 0) + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(0,0,1, 0,1,0), + Segment3D(1,0,0, 0,1,0) + ) + result mustEqual true + } + + "detect intersection on the farther point" in { + //these segments intersect at (1, 1, 0); end of segment 1, middle of segment 2 + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(1,0,0, 1,1,0), + Segment3D(2,0,0, 0,2,0) + ) + result mustEqual true + } + + "detect intersection in the middle(s)" in { + //these segments intersect at (0.5f, 0.5f, 0) + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(0,0,0, 1,1,0), + Segment3D(1,0,0, 0,1,0) + ) + result mustEqual true + } + + "detect intersection in the middle " in { + //these segments intersect at (0, 0.5, 0) + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(0,0,0, 1,0,0), + Segment3D(0.5f,1,0, 0.5f,-1,0) + ) + result mustEqual true + } + + "not detect intersection if the point of intersection would be before the start of the segments" in { + //these segments will not intersect as segments; but, as lines, they would intersect at (0, 0, 0) + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(1,1,0, 2,2,0), + Segment3D(1,0,0, 2,0,0) + ) + result mustEqual false + } + + "not detect intersection if the point of intersection would be after the end of the segments" in { + //these segments will not intersect as segments; but, as lines, they would intersect at (2, 2, 0) + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(0,0,0, 1,1,0), + Segment3D(2,0,0, 2,1,0) + ) + result mustEqual false + } + + "not detect intersection if the line segments are parallel" in { + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(0,0,0, 1,1,1), + Segment3D(1,1,2, 2,2,3) + ) + result mustEqual false + } + + "detect overlap" in { + //the sub-segment (1,0,0) to (2,0,0) is an overlap region shared between the two segments + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(0,0,0, 2,0,0), + Segment3D(1,0,0, 3,0,0) + ) + result mustEqual true + } + + "not detect intersection (generic skew)" in { + //these segments will not intersect + val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + Segment3D(-3,-8,7, -3,-9,8), + Segment3D(6,3,0, 2,0,0) + ) + result mustEqual false + } + } +} + +object GeometryTest { + +} From 9d86844396d73050b3b35987b2e4cc9616af86df Mon Sep 17 00:00:00 2001 From: "Jason_DiDonato@yahoo.com" Date: Sun, 24 Jan 2021 23:11:02 -0500 Subject: [PATCH 3/6] modified shape structures and operations on said shapes --- .../psforever/objects/geometry/Closest.scala | 280 ++++++++++++ .../objects/geometry/ClosestDistance.scala | 173 ------- .../psforever/objects/geometry/Geometry.scala | 20 + .../objects/geometry/Intersection.scala | 99 +++- src/test/scala/objects/GeometryTest.scala | 429 ++++++++++++++++-- 5 files changed, 759 insertions(+), 242 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/geometry/Closest.scala delete mode 100644 src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala diff --git a/src/main/scala/net/psforever/objects/geometry/Closest.scala b/src/main/scala/net/psforever/objects/geometry/Closest.scala new file mode 100644 index 000000000..397a7d247 --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/Closest.scala @@ -0,0 +1,280 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.geometry + +import net.psforever.types.Vector3 + +object Closest { + object Distance { + def apply(point : Vector3, seg : Segment2D) : Float = { + val segdx = seg.bx - seg.ax + val segdy = seg.by - seg.ay + ((point.x - seg.ax) * segdx + (point.y - seg.ay) * segdy) / + Vector3.MagnitudeSquared(Vector3(segdx, segdy, 0)) + } + + def apply(line1 : Line2D, line2 : Line2D) : Float = { + if (Intersection.Test(line1, line2)) { //intersecting lines + 0f + } else { + math.abs( + Vector3.DotProduct( + Vector3(line2.x - line1.x, line2.y - line1.y, 0), + Vector3(-1/line1.d.y, 1/line1.d.x, 0) + ) + ) + } + } + + def apply(seg1: Segment2D, seg2: Segment2D): Float = { + if (Intersection.Test(seg1, seg2)) { //intersecting line segments + 0f + } else { + val v1a = Vector3(seg1.ax, seg1.ay, 0) + val v2a = Vector3(seg2.ax, seg2.ay, 0) + val v1b = Vector3(seg1.bx, seg1.by, 0) + val v2b = Vector3(seg2.bx, seg2.by, 0) + math.min( + apply(v1a, seg2), + math.min( + apply(v1b, seg2), + math.min( + apply(v2a, seg1), + apply(v2b, seg1) + ) + ) + ) + } + } + + def apply(c1: Circle, c2 : Circle): Float = { + math.max(0, Vector3.Magnitude(Vector3(c1.x - c2.x, c1.y - c2.y, 0)) - c1.radius - c2.radius) + } + + /** + * na + * @param line1 na + * @param line2 na + * @return the shortest distance between the lines; + * if parallel, the common perpendicular distance between the lines; + * if coincidental, this distance will be 0 + */ + def apply(line1: Line3D, line2: Line3D): Float = { + val cross = Vector3.CrossProduct(line1.d, line2.d) + if(cross != Vector3.Zero) { + math.abs( + Vector3.DotProduct(cross, Vector3(line1.x - line2.x, line1.y - line2.y, line1.z - line2.z)) + ) / Vector3.Magnitude(cross) + } else { + // lines are parallel or coincidental + // construct a right triangle with one leg on line1 and the hypotenuse between the line's known points + val hypotenuse = Vector3(line2.x - line1.x, line2.y - line1.y, line2.z - line1.z) + val legOnLine1 = line1.d * Vector3.DotProduct(hypotenuse, line1.d) + Vector3.Magnitude(hypotenuse - legOnLine1) + } + } + + def apply(seg1: Segment3D, seg2: Segment3D): Float = { + //TODO make not as expensive as finding the plotted closest distance segment + Segment(seg1, seg2) match { + case Some(seg) => seg.length + case None => Float.MaxValue + } + } + + def apply(s1: Sphere, s2 : Sphere): Float = { + math.max(0, Vector3.Magnitude(Vector3(s1.x - s2.x, s1.y - s2.y, s1.z - s2.z)) - s1.radius - s2.radius) + } + } + + object Segment { + /** + * na + * @param c1 na + * @param c2 na + * @return a line segment that represents the closest distance between the circle's circumferences; + * `None`, if the circles have no distance between them (overlapping) + */ + def apply(c1 : Circle, c2 : Circle): Option[Segment2D] = { + val distance = Distance(c1, c2) + if (distance > 0) { + val c1x = c1.x + val c1y = c1.y + val v = Vector3.Unit(Vector3(c2.x - c1x, c2.y - c1y, 0f)) + val c1d = v * c1.radius + val c2d = v * c2.radius + Some( + Segment2D( + c1x + c1d.x, c1y + c1d.y, + c1x + c2d.x, c1y + c2d.y, + ) + ) + } else { + None + } + } + + /** + * na + * @param line1 na + * @param line2 na + * @return a line segment representing the closest distance between the two not intersecting lines; + * in the case of parallel lines, one of infinite closest distances is plotted; + * `None`, if the lines intersect with each other + */ + def apply(line1 : Line3D, line2 : Line3D): Option[Segment3D] = { + val p1 = Vector3(line1.x, line1.y, line1.z) + val p3 = Vector3(line2.x, line2.y, line2.z) + val p13 = p1 - p3 // vector between point on first line and point on second line + val p43 = line2.d + val p21 = line1.d + if (Vector3.MagnitudeSquared(p43) < Float.MinPositiveValue || + Vector3.MagnitudeSquared(p21) < Float.MinPositiveValue) { + None + } else { + val d2121 = Vector3.MagnitudeSquared(p21) + val d4343 = Vector3.MagnitudeSquared(p43) + val d4321 = Vector3.DotProduct(p43, p21) + val denom = d2121 * d4343 - d4321 * d4321 // n where d = (m/n) and a(x,y,z) + d * V = b(x,y,z) for line1 + if (math.abs(denom) < Float.MinPositiveValue) { + // without a denominator, we have no cross product solution + val p13u = Vector3.Unit(p13) + if (p21 == p13u || p21 == Vector3.neg(p13u)) { //coincidental lines overlap / intersect + None + } else { //parallel lines + val connecting = Vector3(line2.x - line1.x, line2.y - line1.y, line2.z - line1.z) + val legOnLine1 = line1.d * Vector3.DotProduct(connecting, line1.d) + val v = connecting - legOnLine1 + Some(Segment3D( + line1.x, line1.y, line1.z, + line1.x + v.x, line1.y + v.y, line1.z + v.z + )) + } + } else { + val d1343 = Vector3.DotProduct(p13, p43) + val numer = d1343 * d4321 -d4343 * Vector3.DotProduct(p13, p21) // m where d = (m/n) and ..., etc. + val mua = numer / denom + val mub = (d1343 + d4321 * mua) / d4343 + Some(Segment3D( + p1.x + mua * p21.x, + p1.y + mua * p21.y, + p1.z + mua * p21.z, + p3.x + mub * p43.x, + p3.y + mub * p43.y, + p3.z + mub * p43.z + )) + } + } + } + + def apply(line1 : Segment3D, line2 : Segment3D): Option[Segment3D] = { + val uline1 = Vector3.Unit(line1.d) + val uline2 = Vector3.Unit(line2.d) + apply(Line3D(line1.ax, line1.ay, line1.az, uline1), Line3D(line2.ax, line2.ay, line2.az, uline2)) match { + case Some(seg: Segment3D) => // common skew lines and parallel lines + val sega = Vector3(seg.ax, seg.ay, seg.az) + val p1 = Vector3(line1.ax, line1.ay, line1.az) + val d1 = sega - p1 + val out1 = if (!Geometry.equalVectors(Vector3.Unit(d1), uline1)) { //clamp seg.a(xyz) to segment line1's bounds + p1 + } else if (Vector3.MagnitudeSquared(d1) > Vector3.MagnitudeSquared(line1.d)) { + Vector3(line1.bx, line1.by, line1.bz) + } else { + sega + } + val segb = Vector3(seg.bx, seg.by, seg.bz) + val p2 = Vector3(line2.ax, line2.ay, line2.az) + val d2 = segb - p2 + val out2 = if (!Geometry.equalVectors(Vector3.Unit(d2), uline2)) { //clamp seg.b(xyz) to segment line2's bounds + p2 + } else if (Vector3.MagnitudeSquared(d2) > Vector3.MagnitudeSquared(line2.d)) { + Vector3(line2.bx, line2.by, line2.bz) + } else { + segb + } + Some(Segment3D( + out1.x, out1.y, out1.z, + out2.x, out2.y, out2.z + )) + case None => + val connectingU = Vector3.Unit(Vector3(line2.ax - line1.ax, line2.ay - line1.ay, line2.az - line1.az)) + if (uline1 == connectingU || uline1 == Vector3.neg(connectingU)) { // coincidental line segments + val line1a = Vector3(line1.ax, line1.ay, line1.az) + val line1b = Vector3(line1.bx, line1.by, line1.bz) + val line2a = Vector3(line2.ax, line2.ay, line2.az) + val line2b = Vector3(line2.bx, line2.by, line2.bz) + if (Vector3.Unit(line2a - line1a) != Vector3.Unit(line2b - line1a) || + Vector3.Unit(line2a - line1b) != Vector3.Unit(line2b - line1b) || + Vector3.Unit(line1a - line2a) != Vector3.Unit(line1b - line2a) || + Vector3.Unit(line1a - line2b) != Vector3.Unit(line1b - line2b)) { + Some(Segment3D( + line1.ax, line1.ay, line1a.z, + line1.ax, line1.ay, line1a.z + )) // overlap regions + } + else { + val segs = List((line1a, line2a), (line1a, line2b), (line2a, line1b)) + val (a, b) = segs({ + //val dist = segs.map { case (_a, _b) => Vector3.DistanceSquared(_a, _b) } + //dist.indexOf(dist.min) + var index = 0 + var minDist = Vector3.DistanceSquared(segs.head._1, segs.head._2) + (1 to 2).foreach { i => + val dist = Vector3.DistanceSquared(segs(i)._1, segs(i)._2) + if (minDist < dist) { + index = i + minDist = dist + } + } + index + }) + Some(Segment3D(a.x, a.y, a.z, b.x, b.y, b.z)) // connecting across the smallest gap + } + } else { + None + } + } + } + + /** + * na + * @param s1 na + * @param s2 na + * @return a line segment that represents the closest distance between the sphere's surface areas; + * `None`, if the spheres have no distance between them (overlapping) + */ + def apply(s1 : Sphere, s2 : Sphere): Option[Segment3D] = { + val distance = Distance(s1, s2) + if (distance > 0) { + val s1x = s1.x + val s1y = s1.y + val s1z = s1.z + val v = Vector3.Unit(Vector3(s2.x - s1x, s2.y - s1y, s2.z - s1z)) + val s1d = v * s1.radius + val s2d = v * (s1.radius + distance) + Some(Segment3D(s1x + s1d.x, s1y + s1d.y, s1y + s1d.y, s1x + s2d.x, s1y + s2d.y, s1y + s2d.y)) + } else { + None + } + } + + def apply(line : Line3D, sphere : Sphere): Option[Segment3D] = { + val sphereAsPoint = Vector3(sphere.x, sphere.y, sphere.z) + val lineAsPoint = Vector3(line.x, line.y, line.z) + val direct = sphereAsPoint - lineAsPoint + val projectionOfDirect = line.d * Vector3.DotProduct(direct, line.d) + val heightFromProjection = projectionOfDirect - direct + val heightFromProjectionDist = Vector3.Magnitude(heightFromProjection) + if (heightFromProjectionDist <= sphere.radius) { //intersection + None + } else { + val pointOnLine = lineAsPoint + projectionOfDirect + val pointOnSphere = pointOnLine + + Vector3.Unit(heightFromProjection) * (heightFromProjectionDist - sphere.radius) + Some(Segment3D( + pointOnLine.x, pointOnLine.y, pointOnLine.z, + pointOnSphere.x, pointOnSphere.y, pointOnSphere.z + )) + } + } + } +} diff --git a/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala b/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala deleted file mode 100644 index ddd5ab15f..000000000 --- a/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) 2021 PSForever -package net.psforever.objects.geometry - -import net.psforever.types.Vector3 - -object ClosestDistance { - object Between { - def apply(origin1 : Vector3, origin2 : Vector3, point : Vector3, seg : Segment2D) : Float = { - val segdx = seg.bx - seg.ax - val segdy = seg.by - seg.ay - ((point.x + origin1.x - seg.ax + origin2.x) * segdx + (point.y + origin1.y - seg.ay + origin2.y) * segdy) / - Vector3.MagnitudeSquared(Vector3(segdx, segdy, 0)) - } - - def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line2D, line2 : Line2D) : Float = { - if (Intersection.Test(origin1, origin2, line1, line2)) { //intersecting lines - 0f - } else { - math.abs( - Vector3.DotProduct( - Vector3(line2.x - line1.x, line2.y - line1.y, 0), - Vector3(-1/line1.d.y, 1/line1.d.x, 0) - ) - ) - } - } - - def apply(origin1: Vector3, origin2: Vector3, seg1: Segment2D, seg2: Segment2D): Float = { - if (Intersection.Test(origin1, origin2, seg1, seg2)) { //intersecting line segments - 0f - } else { - val v1a = Vector3(seg1.ax, seg1.ay, 0) - val v2a = Vector3(seg2.ax, seg2.ay, 0) - val v1b = Vector3(seg1.bx, seg1.by, 0) - val v2b = Vector3(seg2.bx, seg2.by, 0) - math.min( - apply(origin1, origin2, v1a, seg2), - math.min( - apply(origin1, origin2, v1b, seg2), - math.min( - apply(origin1, origin2, v2a, seg1), - apply(origin1, origin2, v2b, seg1) - ) - ) - ) - } - } - - def apply(origin1: Vector3, origin2: Vector3, line1: Line3D, line2: Line3D): Float = { - val cross = Vector3.CrossProduct(line1.d, line2.d) - if(cross != Vector3.Zero) { - math.abs( - Vector3.DotProduct(cross, Vector3(line1.x - line2.x, line1.y - line2.y, line1.z - line2.z)) - ) / Vector3.Magnitude(cross) - } else { - //lines are parallel - Vector3.Magnitude( - Vector3.CrossProduct( - line1.d, - Vector3(line2.x - line1.x, line2.y - line1.y, line2.z - line1.z) - ) - ) - } - } - - def apply(origin1: Vector3, origin2: Vector3, seg1: Segment3D, seg2: Segment3D): Float = { - //TODO make not as expensive as finding the plotted closest distance segment - Plotted(origin1, origin2, seg1, seg2) match { - case Some(seg) => seg.length - case None => Float.MaxValue - } - } - } - - object Plotted { - /** - * na - * This function can only operate normally if a perpendicular line segment between the two lines can be established, - * this is, if the cross product of the two lines exists. - * As such, for coincidental lines, a segment of zero length from the first line's point is produced. - * @param origin1 na - * @param origin2 na - * @param line1 na - * @param line2 na - * @return na - */ - def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line3D, line2 : Line3D): Option[Segment3D] = { - val p1 = Vector3(line1.x, line1.y, line1.z) - val p2 = p1 + line1.d - val p3 = Vector3(line2.x, line2.y, line2.z) - val p4 = p3 + line2.d - val p13 = p1 - p3 // vector between point on first line and point on second line - val p43 = line2.d - val p21 = line1.d - if (Vector3.MagnitudeSquared(p43) < Float.MinPositiveValue || - Vector3.MagnitudeSquared(p21) < Float.MinPositiveValue) { - None - } else { - val d2121 = Vector3.MagnitudeSquared(p21) - val d4343 = Vector3.MagnitudeSquared(p43) - val d4321 = Vector3.DotProduct(p43, p21) - val denom = d2121 * d4343 - d4321 * d4321 // n where d = (m/n) and a(x,y,z) + d * V = b(x,y,z) for line1 - if (math.abs(denom) < Float.MinPositiveValue) { - val p13u = Vector3.Unit(p13) - if (p21 == p13u || p21 == Vector3.neg(p13u)) { //coincidental lines - // can not produce a valid cross product, but a coincidental line does produce an overlap - Some(Segment3D( - line1.x, line1.y, line1.z, - line1.x, line1.y, line1.z - )) - } else { - None - } - } else { - val d1343 = Vector3.DotProduct(p13, p43) - val numer = d1343 * d4321 -d4343 * Vector3.DotProduct(p13, p21) // m where d = (m/n) and ..., etc. - val mua = numer / denom - val mub = (d1343 + d4321 * mua) / d4343 - Some(Segment3D( - p1.x + mua * p21.x, - p1.y + mua * p21.y, - p1.z + mua * p21.z, - p3.x + mub * p43.x, - p3.y + mub * p43.y, - p3.z + mub * p43.z - )) - } - } - } - - def apply(origin1 : Vector3, origin2 : Vector3, line1 : Segment3D, line2 : Segment3D): Option[Segment3D] = { - val uline1 = Vector3.Unit(line1.d) - val uline2 = Vector3.Unit(line2.d) - apply( - origin1, - origin2, - Line3D(line1.ax, line1.ay, line1.az, uline1), - Line3D(line2.ax, line2.ay, line2.az, uline2) - ) match { - case out @ Some(seg: Segment3D) - if seg.length == 0 && (uline1 == uline2 || uline1 == Vector3.neg(uline2)) => //coincidental lines - out - case Some(seg: Segment3D) => //segment of shortest distance when two segments treated as lines - val sega = Vector3(seg.ax, seg.ay, seg.az) - val p1 = Vector3(line1.ax, line1.ay, line1.az) - val d1 = sega - p1 - val out1 = if (!Geometry.equalVectors(Vector3.Unit(d1), uline1)) { //clamp seg.a(xyz) to segment line1's bounds - p1 - } else if (Vector3.MagnitudeSquared(d1) > Vector3.MagnitudeSquared(line1.d)) { - Vector3(line1.bx, line1.by, line1.bz) - } else { - sega - } - val segb = Vector3(seg.bx, seg.by, seg.bz) - val p2 = Vector3(line2.ax, line2.ay, line2.az) - val d2 = segb - p2 - val out2 = if (!Geometry.equalVectors(Vector3.Unit(d2), uline2)) { //clamp seg.b(xyz) to segment line2's bounds - p2 - } else if (Vector3.MagnitudeSquared(d2) > Vector3.MagnitudeSquared(line2.d)) { - Vector3(line2.bx, line2.by, line2.bz) - } else { - segb - } - Some(Segment3D( - out1.x, out1.y, out1.z, - out2.x, out2.y, out2.z - )) - case None => - None - } - } - } -} diff --git a/src/main/scala/net/psforever/objects/geometry/Geometry.scala b/src/main/scala/net/psforever/objects/geometry/Geometry.scala index 4233b6897..1a679d02d 100644 --- a/src/main/scala/net/psforever/objects/geometry/Geometry.scala +++ b/src/main/scala/net/psforever/objects/geometry/Geometry.scala @@ -40,6 +40,12 @@ object Segment2D { } } +final case class Circle(x: Float, y: Float, radius: Float) + +object Circle { + def apply(radius: Float): Circle = Circle(0f, 0f, radius) +} + final case class Line3D(x: Float, y: Float, z: Float, d: Vector3) extends Line final case class Segment3D(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float) extends Segment { @@ -52,6 +58,20 @@ object Segment3D { } } +final case class Sphere(x: Float, y: Float, z: Float, radius: Float) + +final case class Cylinder(circle: Circle, z: Float, height: Float) + +object Cylinder { + def apply(x: Float, y: Float, z: Float, radius: Float, height: Float): Cylinder = { + Cylinder(Circle(x, y, radius), z, height) + } +} + +object Sphere { + def apply(p: Vector3, radius: Float): Sphere = Sphere(p.x, p.y, p.z, radius) +} + object Geometry { def equalFloats(value1: Float, value2: Float, off: Float = 0.001f): Boolean = { val diff = value1 - value2 diff --git a/src/main/scala/net/psforever/objects/geometry/Intersection.scala b/src/main/scala/net/psforever/objects/geometry/Intersection.scala index 74dc781cb..a2e2834c7 100644 --- a/src/main/scala/net/psforever/objects/geometry/Intersection.scala +++ b/src/main/scala/net/psforever/objects/geometry/Intersection.scala @@ -8,17 +8,17 @@ object Intersection { /** * Do these two lines intersect? * Lines in 2D space will always intersect unless they are parallel or antiparallel. - * In that case, they can still "intersect" if the lines are coincidental. + * In that case, however, they can still "intersect" if provided that the lines are coincidental. */ - def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line2D, line2 : Line2D): Boolean = { + def apply(line1: Line2D, line2: Line2D): Boolean = { line1.d != line2.d || { //parallel or antiparallel? val u = Vector3.Unit(Vector3(line2.x - line1.x, line2.y - line1.y, 0)) - line1.d == u || line1.d == Vector3.neg(u) + u == Vector3.Zero || line1.d == u || line1.d == Vector3.neg(u) } } - private def pointOnSegment(ax : Float, ay : Float, px : Float, py : Float, bx : Float, by : Float): Boolean = { + private def pointOnSegment(ax: Float, ay: Float, px: Float, py: Float, bx: Float, by: Float): Boolean = { px <= math.max(ax, bx) && px >= math.min(ax, bx) && py <= math.max(ay, by) && py >= math.min(ay, by) } @@ -41,9 +41,9 @@ object Intersection { * @return the orientation value */ private def orientationOfPoints( - ax : Float, ay : Float, - px : Float, py : Float, - bx : Float, by : Float + ax: Float, ay: Float, + px: Float, py: Float, + bx: Float, by: Float ): PointTripleOrientation.Value = { val out = (py - ay) * (bx - px) - (px - ax) * (by - py) if (out == 0) PointTripleOrientation.Colinear @@ -57,16 +57,16 @@ object Intersection { * If a test of multiple ordered triple points reveals that certain triples have different orientations, * then we can safely assume the intersection state of the segments. */ - def apply(origin1 : Vector3, origin2 : Vector3, line1 : Segment2D, line2 : Segment2D): Boolean = { + def apply(line1: Segment2D, line2: Segment2D): Boolean = { //setup - val ln1ax = line1.ax + origin1.x - val ln1ay = line1.ay + origin1.y - val ln1bx = ln1ax + origin1.x - val ln1by = ln1ay + origin1.y - val ln2ax = line2.ax + origin2.x - val ln2ay = line2.ay + origin2.y - val ln2bx = ln2ax + origin2.x - val ln2by = ln2ay + origin2.y + val ln1ax = line1.ax + val ln1ay = line1.ay + val ln1bx = line1.bx + val ln1by = line1.by + val ln2ax = line2.ax + val ln2ay = line2.ay + val ln2bx = line2.bx + val ln2by = line2.by val ln1_ln2a = orientationOfPoints(ln1ax, ln1ay, ln1bx, ln1by, ln2ax, ln2ay) val ln1_ln2b = orientationOfPoints(ln1ax, ln1ay, ln1bx, ln1by, ln2bx, ln2by) val ln2_ln1a = orientationOfPoints(ln2ax, ln2ay, ln2bx, ln2by, ln1ax, ln1ay) @@ -85,11 +85,15 @@ object Intersection { * Actual mathematically-sound intersection between lines and line segments in 3D-space is terribly uncommon. * Instead, check that the closest distance between two line segments is below a threshold value. */ - def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line3D, line2 : Line3D): Boolean = { - apply(origin1, origin2, line1, line2, 0.15f) + def apply(line1: Line3D, line2: Line3D): Boolean = { + apply(line1, line2, 0.15f) } - def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line3D, line2 : Line3D, threshold: Float): Boolean = { - ClosestDistance.Between(origin1, origin2, line1, line2) < threshold + def apply(line1: Line3D, line2: Line3D, threshold: Float): Boolean = { + Closest.Distance(line1, line2) < threshold + } + + def apply(c1: Circle, c2 : Circle): Boolean = { + Vector3.Magnitude(Vector3(c1.x - c2.x, c1.y - c2.y, 0)) <= c1.radius + c2.radius } /** @@ -97,11 +101,58 @@ object Intersection { * Actual mathematically-sound intersection between lines and line segments in 3D-space is terribly uncommon. * Instead, check that the closest distance between two line segments is below a threshold value. */ - def apply(origin1 : Vector3, origin2 : Vector3, seg1 : Segment3D, seg2 : Segment3D): Boolean = { - apply(origin1, origin2, seg1, seg2, 0.15f) + def apply(seg1: Segment3D, seg2: Segment3D): Boolean = { + apply(seg1, seg2, 0.15f) } - def apply(origin1 : Vector3, origin2 : Vector3, seg1 : Segment3D, seg2 : Segment3D, threshold: Float): Boolean = { - ClosestDistance.Between(origin1, origin2, seg1, seg2) < threshold + def apply(seg1: Segment3D, seg2: Segment3D, threshold: Float): Boolean = { + Closest.Distance(seg1, seg2) < threshold + } + + def apply(s1: Sphere, s2 : Sphere): Boolean = { + Vector3.Magnitude( + Vector3( + s1.x - s2.x, + s1.y - s2.y, + s1.z - s2.z + ) + ) <= s1.radius + s2.radius + } + + def apply(c1: Cylinder, c2: Cylinder): Boolean = { + apply(c1.circle, c2.circle) && + ((c1.height >= c2.z && c1.z <= c2.height) || (c2.height >= c1.z && c2.z <= c1.height)) + } + + def apply(cylinder: Cylinder, sphere: Sphere): Boolean = { + val cylinderCircle = cylinder.circle + val cylinderCircleRadius = cylinderCircle.radius + val cylinderTop = cylinder.z + cylinder.height + val sphereRadius = sphere.radius + val sphereBase = sphere.z - sphereRadius + val sphereTop = sphere.z + sphereRadius + if (apply(cylinderCircle, Circle(sphere.x, sphere.y, sphereRadius)) && + ((sphereTop >= cylinder.z && sphereBase <= cylinderTop) || + (cylinderTop >= sphereBase && cylinder.z <= sphereTop))) { + // potential intersection ... + val sphereAsPoint = Vector3(sphere.x, sphere.y, sphere.z) + val cylinderAsPoint = Vector3(cylinderCircle.x, cylinderCircle.y, cylinder.z) + val segmentFromCylinderToSphere = sphereAsPoint - cylinderAsPoint + val segmentFromCylinderToSphereXY = segmentFromCylinderToSphere.xy + if ((cylinder.z <= sphere.z && sphere.z <= cylinderTop) || + Vector3.MagnitudeSquared(segmentFromCylinderToSphereXY) <= cylinderCircleRadius * cylinderCircleRadius) { + true // top or bottom of sphere, or widest part of the sphere, must interact with the cylinder + } else { + // only option left is the curves of the sphere interacting with the cylinder's rim, top or base + val directionFromCylinderToSphere = Vector3.Unit(segmentFromCylinderToSphereXY) + val pointOnCylinderRimBase = cylinderAsPoint + directionFromCylinderToSphere * cylinderCircleRadius + val pointOnCylinderRimTop = pointOnCylinderRimBase + Vector3.z(cylinder.height) + val sqSphereRadius = sphereRadius * sphereRadius + Vector3.DistanceSquared(sphereAsPoint, pointOnCylinderRimTop) <= sqSphereRadius || + Vector3.DistanceSquared(sphereAsPoint, pointOnCylinderRimBase) <= sqSphereRadius + } + } else { + false + } } } } diff --git a/src/test/scala/objects/GeometryTest.scala b/src/test/scala/objects/GeometryTest.scala index 71b4734ee..ea782da97 100644 --- a/src/test/scala/objects/GeometryTest.scala +++ b/src/test/scala/objects/GeometryTest.scala @@ -1,15 +1,175 @@ // Copyright (c) 2020 PSForever package objects -import net.psforever.objects.geometry.{Intersection, Line3D, Segment3D} +import net.psforever.objects.geometry._ import net.psforever.types.Vector3 import org.specs2.mutable.Specification class IntersectionTest extends Specification { + "Line2D" should { + "detect intersection on target points(s)" in { + //these lines intersect at (0, 0) + val result = Intersection.Test( + Line2D(0,0, 1,0), + Line2D(0,0, 0,1) + ) + result mustEqual true + } + + "detect intersection on a target point" in { + //these lines intersect at (0, 0); start of segment 1, middle of segment 2 + val result = Intersection.Test( + Line2D( 0,0, 0,1), + Line2D(-1,0, 1,0) + ) + result mustEqual true + } + + "detect intersection anywhere else" in { + //these lines intersect at (0.5f, 0.5f) + val result = Intersection.Test( + Line2D(0,0, 1,1), + Line2D(1,0, 0,1) + ) + result mustEqual true + } + + "detect intersection anywhere else (2)" in { + //these lines intersect at (0, 0.5) + val result = Intersection.Test( + Line2D(0, 0, 1, 0), + Line2D(0.5f,1, 0.5f,-1) + ) + result mustEqual true + } + + "not detect intersection if the lines are parallel" in { + val result = Intersection.Test( + Line2D(0,0, 1,1), + Line2D(1,0, 2,1) + ) + result mustEqual false + } + + "detect intersection if the lines overlap" in { + //the lines are coincidental + val result = Intersection.Test( + Line2D(0,0, 1,1), + Line2D(1,1, 2,2) + ) + result mustEqual true + } + } + + "Segment2D" should { + "detect intersection on target points(s)" in { + //these line segments intersect at (0, 0) + val result = Intersection.Test( + Segment2D(0,0, 1,0), + Segment2D(0,0, 0,1) + ) + result mustEqual true + } + + "detect intersection on a target point" in { + //these line segments intersect at (0, 0); start of segment 1, middle of segment 2 + val result = Intersection.Test( + Segment2D( 0,0, 0,1), + Segment2D(-1,0, 1,0) + ) + result mustEqual true + } + + "detect intersection anywhere else" in { + //these line segments intersect at (0.5f, 0.5f) + val result = Intersection.Test( + Segment2D(0,0, 1,1), + Segment2D(1,0, 0,1) + ) + result mustEqual true + } + + "detect intersection anywhere else (2)" in { + //these line segments intersect at (0, 0.5) + val result = Intersection.Test( + Segment2D(0, 0, 1, 0), + Segment2D(0.5f,1, 0.5f,-1) + ) + result mustEqual true + } + + "not detect intersection if the lines are parallel" in { + val result = Intersection.Test( + Segment2D(0,0, 1,1), + Segment2D(1,0, 2,1) + ) + result mustEqual false + } + + "detect intersection if the lines overlap" in { + //the lines are coincidental + val result = Intersection.Test( + Line2D(0,0, 1,1), + Line2D(1,1, 2,2) + ) + result mustEqual true + } + } + + "Circle" should { + "intersect when overlapping (coincidental)" in { + val result = Intersection.Test( + Circle(0,0, 1), + Circle(0,0, 1) + ) + result mustEqual true + } + + "intersect when overlapping (engulfed)" in { + val result = Intersection.Test( + Circle(0,0, 2), + Circle(1,0, 1) + ) + result mustEqual true + } + + "intersect when overlapping (partial 1)" in { + val result = Intersection.Test( + Circle(0,0, 2), + Circle(2,0, 1) + ) + result mustEqual true + } + + "intersect when overlapping (partial 2)" in { + val result = Intersection.Test( + Circle(0, 0, 2), + Circle(2.5f,0, 1) + ) + result mustEqual true + } + + "intersect when the circumferences are touching" in { + val result = Intersection.Test( + Circle(0,0, 2), + Circle(3,0, 1) + ) + result mustEqual true + } + + "not intersect when not touching" in { + val result = Intersection.Test( + Circle(0,0, 2), + Circle(4,0, 1) + ) + result mustEqual false + } + } + "Line3D" should { "detect intersection on target point(s)" in { //these lines intersect at (0, 0, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Line3D(0,0,0, Vector3(1,0,0)), Line3D(0,0,0, Vector3(0,1,0)) ) @@ -18,60 +178,42 @@ class IntersectionTest extends Specification { "detect intersection on a target point" in { //these lines intersect at (0, 0, 0); start of segment 1, middle of segment 2 - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Line3D(0,0,0, Vector3(0,1,0)), Line3D(-1,0,0, Vector3(1,0,0)) ) result mustEqual true } - "detect intersection in the middle(s)" in { + "detect intersection anywhere else" in { //these lines intersect at (0.5f, 0.5f, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Line3D(0,0,0, Vector3.Unit(Vector3(1, 1, 0))), Line3D(1,0,0, Vector3(0,1,0)) ) result mustEqual true } - "detect intersection in the middle " in { + "detect intersection anywhere else (2)" in { //these lines intersect at (0, 0.5, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Line3D(0,0,0, Vector3(1,0,0)), Line3D(0.5f,1,0, Vector3.Unit(Vector3(0.5f,-1,0))) ) result mustEqual true } - "detect intersection if the point of intersection would be before the start of the segments" in { - //these lines would intersect at (0, 0, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, - Line3D(1,1,0, Vector3.Unit(Vector3(2, 2, 0))), - Line3D(1,0,0, Vector3.Unit(Vector3(2,0,0))) - ) - result mustEqual true - } - - "detect intersection if the point of intersection would be after the end of the segments" in { - //these lines would intersect at (2, 2, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, - Line3D(0,0,0, Vector3.Unit(Vector3(1,1,0))), - Line3D(2,0,0, Vector3.Unit(Vector3(2,1,0))) - ) - result mustEqual true - } - - "not detect intersection if the line segments are parallel" in { - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + "not detect intersection if the lines are parallel" in { + val result = Intersection.Test( Line3D(0,0,0, Vector3.Unit(Vector3(1,1,1))), Line3D(1,1,2, Vector3.Unit(Vector3(1,1,1))) ) result mustEqual false } - "detect overlap" in { + "detect intersection if the lines overlap" in { //the sub-segment (1,0,0) to (2,0,0) is an overlap region shared between the two segments - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Line3D(0,0,0, Vector3.Unit(Vector3(2,0,0))), Line3D(1,0,0, Vector3.Unit(Vector3(3,0,0))) ) @@ -80,7 +222,7 @@ class IntersectionTest extends Specification { "not detect intersection (generic skew)" in { //these segments will not intersect - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(-3,-8,7, Vector3.Unit(Vector3(-3,-9,8))), Segment3D(6,3,0, Vector3.Unit(Vector3(2,0,0))) ) @@ -91,7 +233,7 @@ class IntersectionTest extends Specification { "Segment3D" should { "detect intersection of the first point(s)" in { //these segments intersect at (0, 0, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,0, 1,0,0), Segment3D(0,0,0, 0,1,0) ) @@ -100,7 +242,7 @@ class IntersectionTest extends Specification { "detect intersection of the first point" in { //these segments intersect at (0, 0, 0); start of segment 1, middle of segment 2 - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,0, 0,2,0), Segment3D(-1,0,0, 1,0,0) ) @@ -109,7 +251,7 @@ class IntersectionTest extends Specification { "detect intersection on the farther point(s)" in { //these segments intersect at (0, 1, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,1, 0,1,0), Segment3D(1,0,0, 0,1,0) ) @@ -118,7 +260,7 @@ class IntersectionTest extends Specification { "detect intersection on the farther point" in { //these segments intersect at (1, 1, 0); end of segment 1, middle of segment 2 - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(1,0,0, 1,1,0), Segment3D(2,0,0, 0,2,0) ) @@ -127,7 +269,7 @@ class IntersectionTest extends Specification { "detect intersection in the middle(s)" in { //these segments intersect at (0.5f, 0.5f, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,0, 1,1,0), Segment3D(1,0,0, 0,1,0) ) @@ -136,7 +278,7 @@ class IntersectionTest extends Specification { "detect intersection in the middle " in { //these segments intersect at (0, 0.5, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,0, 1,0,0), Segment3D(0.5f,1,0, 0.5f,-1,0) ) @@ -145,7 +287,7 @@ class IntersectionTest extends Specification { "not detect intersection if the point of intersection would be before the start of the segments" in { //these segments will not intersect as segments; but, as lines, they would intersect at (0, 0, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(1,1,0, 2,2,0), Segment3D(1,0,0, 2,0,0) ) @@ -154,7 +296,7 @@ class IntersectionTest extends Specification { "not detect intersection if the point of intersection would be after the end of the segments" in { //these segments will not intersect as segments; but, as lines, they would intersect at (2, 2, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,0, 1,1,0), Segment3D(2,0,0, 2,1,0) ) @@ -162,33 +304,230 @@ class IntersectionTest extends Specification { } "not detect intersection if the line segments are parallel" in { - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,0, 1,1,1), Segment3D(1,1,2, 2,2,3) ) result mustEqual false } - "detect overlap" in { + "detect intersection with overlapping" in { //the sub-segment (1,0,0) to (2,0,0) is an overlap region shared between the two segments - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,0, 2,0,0), Segment3D(1,0,0, 3,0,0) ) result mustEqual true } + "not detect intersection with coincidental, non-overlapping" in { + //the sub-segment (1,0,0) to (2,0,0) is an overlap region shared between the two segments + val result = Intersection.Test( + Segment3D(0,0,0, 1,0,0), + Segment3D(2,0,0, 3,0,0) + ) + result mustEqual false + } + "not detect intersection (generic skew)" in { //these segments will not intersect - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(-3,-8,7, -3,-9,8), Segment3D(6,3,0, 2,0,0) ) result mustEqual false } } + + "Sphere" should { + "intersect when overlapping (coincidental)" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 1), + Sphere(Vector3.Zero, 1) + ) + result mustEqual true + } + + "intersect when overlapping (engulfed)" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 5), + Sphere(Vector3(1,0,0), 1) + ) + result mustEqual true + } + + "intersect when overlapping (partial 1)" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 2), + Sphere(Vector3(2,0,0), 1) + ) + result mustEqual true + } + + "intersect when overlapping (partial 2)" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 2), + Sphere(Vector3(2.5f,0,0), 1) + ) + result mustEqual true + } + + "intersect when the circumferences are touching" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 2), + Sphere(Vector3(3,0,0), 1) + ) + result mustEqual true + } + + "not intersect when not touching" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 2), + Sphere(Vector3(4,0,0), 1) + ) + result mustEqual false + } + } + + "Cylinder" should { + "detect intersection if overlapping" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 2), + Cylinder(0, 0, 0, 1, 2) + ) + result mustEqual true + } + + "detect intersection if sides clip" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 2), + Cylinder(0.5f, 0.5f, 0, 1, 2) + ) + result mustEqual true + } + + "detect intersection if touching" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 2), + Cylinder(1, 0, 0, 1, 2) + ) + result mustEqual true + } + + "detect intersection if stacked" in { + val result = Intersection.Test( + Cylinder(1, 0, 0, 1, 2), + Cylinder(1, 0, 2, 1, 2) + ) + result mustEqual true + } + + "detect intersection if one is sunken into the other" in { + val result = Intersection.Test( + Cylinder(1, 0, 0, 1, 2), + Cylinder(1, 0, 1, 1, 2) + ) + result mustEqual true + } + + "not detect intersection if not near each other" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 2), + Cylinder(2, 2, 0, 1, 2) + ) + result mustEqual false + } + + "not detect intersection if one is too high / low" in { + val result = Intersection.Test( + Cylinder(1, 0, 0, 1, 2), + Cylinder(1, 0, 5, 1, 2) + ) + result mustEqual false + } + } + + "Cylinder and Sphere" should { + "detect intersection if overlapping" in { + val result = Intersection.Test( + Cylinder(1, 0, 0, 1, 1), + Sphere(1, 0, 2, 1) + ) + result mustEqual true + } + + "detect intersection if cylinder top touches sphere base" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(1, 0, 2, 1) + ) + result mustEqual true + } + + "detect intersection if cylinder base touches sphere top" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(-1, 0, -1, 1) + ) + result mustEqual true + } + + "detect intersection if cylinder edge touches sphere edge" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(2, 0, 0.5f, 1) + ) + result mustEqual true + } + + "detect intersection if on cylinder top rim" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(1.75f, 0, 1.25f, 1) + ) + result mustEqual true + } + + "detect intersection if on cylinder base rim" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(1.75f, 0, -0.5f, 1) + ) + result mustEqual true + } + + "not detect intersection if too far above" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(0, 0, 3, 1) + ) + result mustEqual false + } + + "not detect intersection if too far below" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(0, 0, -3, 1) + ) + result mustEqual false + } + + "not detect intersection if too far out (sideways)" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(2, 2, 0, 1) + ) + result mustEqual false + } + + "not detect intersection if too far out (skew)" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(1.5f, 1.5f, 1.5f, 1) + ) + result mustEqual false + } + } } -object GeometryTest { - -} +object GeometryTest { } From fe386bd79bd4fdc9a884f954b51ab663ac44e068 Mon Sep 17 00:00:00 2001 From: "Jason_DiDonato@yahoo.com" Date: Wed, 27 Jan 2021 14:33:43 -0500 Subject: [PATCH 4/6] integrated geometric representation with object definition, explosion target detection, and explosion resolution --- .../objects/ExplosiveDeployable.scala | 25 ++- .../psforever/objects/GlobalDefinitions.scala | 91 ++++---- .../objects/ballistics/PlayerSource.scala | 4 + .../objects/definition/AvatarDefinition.scala | 6 +- .../objects/definition/ObjectDefinition.scala | 23 ++ .../psforever/objects/geometry/Closest.scala | 80 +++---- .../psforever/objects/geometry/Geometry.scala | 204 +++++++++++++++--- .../objects/geometry/GeometryForm.scala | 113 ++++++++++ .../objects/geometry/Intersection.scala | 38 ++-- .../vital/etc/ExplodingEntityReason.scala | 50 ++++- .../net/psforever/objects/zones/Zone.scala | 35 ++- 11 files changed, 531 insertions(+), 138 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/geometry/GeometryForm.scala diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 812fd4072..56ed5fca4 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -7,11 +7,12 @@ import net.psforever.objects.ce._ import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition} import net.psforever.objects.definition.converter.SmallDeployableConverter import net.psforever.objects.equipment.JammableUnit +import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity} import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.vital.resolution.ResolutionCalculations.Output -import net.psforever.objects.vital.SimpleResolutions +import net.psforever.objects.vital.{SimpleResolutions, Vitality} import net.psforever.objects.vital.etc.TriggerUsedReason import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.projectile.ProjectileReason @@ -96,13 +97,33 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D val originalHealth = mine.Health val cause = applyDamageTo(mine) val damage = originalHealth - mine.Health - if (Damageable.CanDamageOrJammer(mine, damage, cause.interaction)) { + if (CanDetonate(mine, damage, cause.interaction)) { ExplosiveDeployableControl.DamageResolution(mine, cause, damage) } else { mine.Health = originalHealth } } } + + /** + * A supplement for checking target susceptibility + * to account for sympathetic explosives even if there is no damage. + * This does not supercede other underlying checks or undo prior damage checks. + * @see `Damageable.CanDamageOrJammer` + * @see `DamageProperties.SympatheticExplosives` + * @param obj the entity being damaged + * @param damage the amount of damage + * @param data historical information about the damage + * @return `true`, if the target can be affected; + * `false`, otherwise + */ + def CanDetonate(obj: Vitality with FactionAffinity, damage: Int, data: DamageInteraction): Boolean = { + if (damage == 0 && data.cause.source.SympatheticExplosion) { + Damageable.CanDamageOrJammer(mine, damage = 1, data) + } else { + Damageable.CanDamageOrJammer(mine, damage, data) + } + } } object ExplosiveDeployableControl { diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 18d126666..62373100a 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -24,6 +24,7 @@ import net.psforever.objects.serverobject.turret.{FacilityTurretDefinition, Turr import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, SeatArmorRestriction, UtilityType} import net.psforever.objects.vital.base.DamageType import net.psforever.objects.vital.damage._ +import net.psforever.objects.vital.etc.ExplodingRadialDegrade import net.psforever.objects.vital.projectile._ import net.psforever.objects.vital.prop.DamageWithPosition import net.psforever.objects.vital.{ComplexDeployableResolutions, MaxResolutions, SimpleResolutions} @@ -5626,7 +5627,7 @@ object GlobalDefinitions { Damage1 = 225 DamageRadius = 5 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } fury.DrownAtMaxDepth = true fury.MaxDepth = 1.3f @@ -5657,7 +5658,7 @@ object GlobalDefinitions { Damage1 = 225 DamageRadius = 5 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } quadassault.DrownAtMaxDepth = true quadassault.MaxDepth = 1.3f @@ -5688,7 +5689,7 @@ object GlobalDefinitions { Damage1 = 225 DamageRadius = 5 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } quadstealth.DrownAtMaxDepth = true quadstealth.MaxDepth = 1.25f @@ -5721,7 +5722,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } two_man_assault_buggy.DrownAtMaxDepth = true two_man_assault_buggy.MaxDepth = 1.5f @@ -5756,7 +5757,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } skyguard.DrownAtMaxDepth = true skyguard.MaxDepth = 1.5f @@ -5795,7 +5796,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } threemanheavybuggy.DrownAtMaxDepth = true threemanheavybuggy.MaxDepth = 1.83f @@ -5829,7 +5830,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } twomanheavybuggy.DrownAtMaxDepth = true twomanheavybuggy.MaxDepth = 1.95f @@ -5863,7 +5864,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } twomanhoverbuggy.DrownAtMaxDepth = true twomanhoverbuggy.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the thresher hovers over water, so ...? @@ -5903,7 +5904,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } mediumtransport.DrownAtMaxDepth = false mediumtransport.MaxDepth = 1.2f @@ -5947,7 +5948,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } battlewagon.DrownAtMaxDepth = true battlewagon.MaxDepth = 1.2f @@ -5988,7 +5989,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } thunderer.DrownAtMaxDepth = true thunderer.MaxDepth = 1.2f @@ -6029,7 +6030,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } aurora.DrownAtMaxDepth = true aurora.MaxDepth = 1.2f @@ -6093,7 +6094,7 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } apc_tr.DrownAtMaxDepth = true apc_tr.MaxDepth = 3 @@ -6157,7 +6158,7 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } apc_nc.DrownAtMaxDepth = true apc_nc.MaxDepth = 3 @@ -6221,7 +6222,7 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } apc_vs.DrownAtMaxDepth = true apc_vs.MaxDepth = 3 @@ -6253,7 +6254,7 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } lightning.DrownAtMaxDepth = true lightning.MaxDepth = 1.38f @@ -6290,7 +6291,7 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } prowler.DrownAtMaxDepth = true prowler.MaxDepth = 3 @@ -6323,7 +6324,7 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } vanguard.DrownAtMaxDepth = true vanguard.MaxDepth = 2.7f @@ -6358,7 +6359,7 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } magrider.DrownAtMaxDepth = true magrider.MaxDepth = 2 @@ -6391,7 +6392,7 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } ant.DrownAtMaxDepth = true ant.MaxDepth = 2 @@ -6427,7 +6428,7 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } ams.DrownAtMaxDepth = true ams.MaxDepth = 3 @@ -6463,7 +6464,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } router.DrownAtMaxDepth = true router.MaxDepth = 2 @@ -6499,7 +6500,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } switchblade.DrownAtMaxDepth = true switchblade.MaxDepth = 2 @@ -6533,7 +6534,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } flail.DrownAtMaxDepth = true flail.MaxDepth = 2 @@ -6567,7 +6568,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } mosquito.DrownAtMaxDepth = true mosquito.MaxDepth = 2 //flying vehicles will automatically disable @@ -6601,7 +6602,7 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } lightgunship.DrownAtMaxDepth = true lightgunship.MaxDepth = 2 //flying vehicles will automatically disable @@ -6634,7 +6635,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } wasp.DrownAtMaxDepth = true wasp.MaxDepth = 2 //flying vehicles will automatically disable @@ -6675,7 +6676,7 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } liberator.DrownAtMaxDepth = true liberator.MaxDepth = 2 //flying vehicles will automatically disable @@ -6717,7 +6718,7 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } vulture.DrownAtMaxDepth = true vulture.MaxDepth = 2 //flying vehicles will automatically disable @@ -6791,7 +6792,7 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 30 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } dropship.DrownAtMaxDepth = true dropship.MaxDepth = 2 @@ -6844,7 +6845,7 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 30 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } galaxy_gunship.DrownAtMaxDepth = true galaxy_gunship.MaxDepth = 2 @@ -6885,7 +6886,7 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 30 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } lodestar.DrownAtMaxDepth = true lodestar.MaxDepth = 2 @@ -6927,7 +6928,7 @@ object GlobalDefinitions { Damage1 = 150 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } phantasm.DrownAtMaxDepth = true phantasm.MaxDepth = 2 @@ -6969,7 +6970,7 @@ object GlobalDefinitions { Damage4 = 1850 DamageRadius = 5.1f DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } he_mine.Name = "he_mine" @@ -6990,7 +6991,7 @@ object GlobalDefinitions { Damage4 = 1600 DamageRadius = 6.6f DamageAtEdge = 0.25f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } jammer_mine.Name = "jammer_mine" @@ -7022,7 +7023,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } spitfire_cloaked.Name = "spitfire_cloaked" @@ -7044,7 +7045,7 @@ object GlobalDefinitions { Damage1 = 75 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } spitfire_aa.Name = "spitfire_aa" @@ -7066,7 +7067,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } motionalarmsensor.Name = "motionalarmsensor" @@ -7100,7 +7101,7 @@ object GlobalDefinitions { Damage1 = 10 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } val fieldTurretConverter = new FieldTurretConverter @@ -7127,7 +7128,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } portable_manned_turret_nc.Name = "portable_manned_turret_nc" @@ -7153,7 +7154,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } portable_manned_turret_tr.Name = "portable_manned_turret_tr" @@ -7179,7 +7180,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } portable_manned_turret_vs.Name = "portable_manned_turret_vs" @@ -7205,7 +7206,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } deployable_shield_generator.Name = "deployable_shield_generator" @@ -7668,7 +7669,7 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 5 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } vanu_sentry_turret.Name = "vanu_sentry_turret" @@ -7770,7 +7771,7 @@ object GlobalDefinitions { DamageRadius = 15.75f DamageRadiusMin = 14 DamageAtEdge = 0.00002f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade //damage is 99999 at 14m, dropping rapidly to ~1 at 15.75m } } diff --git a/src/main/scala/net/psforever/objects/ballistics/PlayerSource.scala b/src/main/scala/net/psforever/objects/ballistics/PlayerSource.scala index 5413f325f..0a2d21205 100644 --- a/src/main/scala/net/psforever/objects/ballistics/PlayerSource.scala +++ b/src/main/scala/net/psforever/objects/ballistics/PlayerSource.scala @@ -18,6 +18,8 @@ final case class PlayerSource( position: Vector3, orientation: Vector3, velocity: Option[Vector3], + crouching: Boolean, + jumping: Boolean, modifiers: ResistanceProfile ) extends SourceEntry { override def Name = name @@ -48,6 +50,8 @@ object PlayerSource { tplayer.Position, tplayer.Orientation, tplayer.Velocity, + tplayer.Crouching, + tplayer.Jumping, ExoSuitDefinition.Select(tplayer.ExoSuit, tplayer.Faction) ) } diff --git a/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala b/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala index da18c0985..f99de8847 100644 --- a/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/AvatarDefinition.scala @@ -3,15 +3,17 @@ package net.psforever.objects.definition import net.psforever.objects.avatar.Avatars import net.psforever.objects.definition.converter.AvatarConverter +import net.psforever.objects.geometry.GeometryForm import net.psforever.objects.vital.VitalityDefinition /** - * The definition for game objects that look like other people, and also for players. - * @param objectId the object's identifier number + * The definition for game objects that look like players. + * @param objectId the object type number */ class AvatarDefinition(objectId: Int) extends ObjectDefinition(objectId) with VitalityDefinition { Avatars(objectId) //let throw NoSuchElementException Packet = AvatarDefinition.converter + Geometry = GeometryForm.representPlayerByCylinder(radius = 1.6f) } object AvatarDefinition { diff --git a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala index 29e7609cd..5378b5224 100644 --- a/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/ObjectDefinition.scala @@ -3,6 +3,7 @@ package net.psforever.objects.definition import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.definition.converter.{ObjectCreateConverter, PacketConverter} +import net.psforever.objects.geometry.{Geometry3D, GeometryForm} import net.psforever.types.OxygenState /** @@ -76,5 +77,27 @@ abstract class ObjectDefinition(private val objectId: Int) extends BasicDefiniti UnderwaterLifespan() } + private var serverSplashTargetsCentroid: Boolean = false + + def ServerSplashTargetsCentroid: Boolean = serverSplashTargetsCentroid + + def ServerSplashTargetsCentroid_=(splash: Boolean): Boolean = { + serverSplashTargetsCentroid = splash + ServerSplashTargetsCentroid + } + + private var serverGeometry: Any => Geometry3D = GeometryForm.representByPoint() + + def Geometry: Any => Geometry3D = if (ServerSplashTargetsCentroid) { + serverGeometry + } else { + GeometryForm.representByPoint() + } + + def Geometry_=(func: Any => Geometry3D): Any => Geometry3D = { + serverGeometry = func + Geometry + } + def ObjectId: Int = objectId } diff --git a/src/main/scala/net/psforever/objects/geometry/Closest.scala b/src/main/scala/net/psforever/objects/geometry/Closest.scala index 397a7d247..4b3dc6ebb 100644 --- a/src/main/scala/net/psforever/objects/geometry/Closest.scala +++ b/src/main/scala/net/psforever/objects/geometry/Closest.scala @@ -6,9 +6,9 @@ import net.psforever.types.Vector3 object Closest { object Distance { def apply(point : Vector3, seg : Segment2D) : Float = { - val segdx = seg.bx - seg.ax - val segdy = seg.by - seg.ay - ((point.x - seg.ax) * segdx + (point.y - seg.ay) * segdy) / + val segdx = seg.p2.x - seg.p1.x + val segdy = seg.p2.y - seg.p1.y + ((point.x - seg.p1.x) * segdx + (point.y - seg.p1.y) * segdy) / Vector3.MagnitudeSquared(Vector3(segdx, segdy, 0)) } @@ -18,7 +18,7 @@ object Closest { } else { math.abs( Vector3.DotProduct( - Vector3(line2.x - line1.x, line2.y - line1.y, 0), + Vector3(line2.p.x - line1.p.x, line2.p.y - line1.p.y, 0), Vector3(-1/line1.d.y, 1/line1.d.x, 0) ) ) @@ -29,10 +29,10 @@ object Closest { if (Intersection.Test(seg1, seg2)) { //intersecting line segments 0f } else { - val v1a = Vector3(seg1.ax, seg1.ay, 0) - val v2a = Vector3(seg2.ax, seg2.ay, 0) - val v1b = Vector3(seg1.bx, seg1.by, 0) - val v2b = Vector3(seg2.bx, seg2.by, 0) + val v1a = Vector3(seg1.p1.x, seg1.p1.y, 0) + val v2a = Vector3(seg2.p1.x, seg2.p1.y, 0) + val v1b = Vector3(seg1.p2.x, seg1.p2.y, 0) + val v2b = Vector3(seg2.p2.x, seg2.p2.y, 0) math.min( apply(v1a, seg2), math.min( @@ -47,7 +47,7 @@ object Closest { } def apply(c1: Circle, c2 : Circle): Float = { - math.max(0, Vector3.Magnitude(Vector3(c1.x - c2.x, c1.y - c2.y, 0)) - c1.radius - c2.radius) + math.max(0, Vector3.Magnitude(Vector3(c1.p.x - c2.p.x, c1.p.y - c2.p.y, 0)) - c1.radius - c2.radius) } /** @@ -62,12 +62,12 @@ object Closest { val cross = Vector3.CrossProduct(line1.d, line2.d) if(cross != Vector3.Zero) { math.abs( - Vector3.DotProduct(cross, Vector3(line1.x - line2.x, line1.y - line2.y, line1.z - line2.z)) + Vector3.DotProduct(cross, Vector3(line1.p.x - line2.p.x, line1.p.y - line2.p.y, line1.p.z - line2.p.z)) ) / Vector3.Magnitude(cross) } else { // lines are parallel or coincidental // construct a right triangle with one leg on line1 and the hypotenuse between the line's known points - val hypotenuse = Vector3(line2.x - line1.x, line2.y - line1.y, line2.z - line1.z) + val hypotenuse = Vector3(line2.p.x - line1.p.x, line2.p.y - line1.p.y, line2.p.z - line1.p.z) val legOnLine1 = line1.d * Vector3.DotProduct(hypotenuse, line1.d) Vector3.Magnitude(hypotenuse - legOnLine1) } @@ -82,7 +82,7 @@ object Closest { } def apply(s1: Sphere, s2 : Sphere): Float = { - math.max(0, Vector3.Magnitude(Vector3(s1.x - s2.x, s1.y - s2.y, s1.z - s2.z)) - s1.radius - s2.radius) + math.max(0, Vector3.Magnitude(Vector3(s1.p.x - s2.p.x, s1.p.y - s2.p.y, s1.p.z - s2.p.z)) - s1.radius - s2.radius) } } @@ -97,9 +97,9 @@ object Closest { def apply(c1 : Circle, c2 : Circle): Option[Segment2D] = { val distance = Distance(c1, c2) if (distance > 0) { - val c1x = c1.x - val c1y = c1.y - val v = Vector3.Unit(Vector3(c2.x - c1x, c2.y - c1y, 0f)) + val c1x = c1.p.x + val c1y = c1.p.y + val v = Vector3.Unit(Vector3(c2.p.x - c1x, c2.p.y - c1y, 0f)) val c1d = v * c1.radius val c2d = v * c2.radius Some( @@ -122,8 +122,8 @@ object Closest { * `None`, if the lines intersect with each other */ def apply(line1 : Line3D, line2 : Line3D): Option[Segment3D] = { - val p1 = Vector3(line1.x, line1.y, line1.z) - val p3 = Vector3(line2.x, line2.y, line2.z) + val p1 = Vector3(line1.p.x, line1.p.y, line1.p.z) + val p3 = Vector3(line2.p.x, line2.p.y, line2.p.z) val p13 = p1 - p3 // vector between point on first line and point on second line val p43 = line2.d val p21 = line1.d @@ -141,12 +141,12 @@ object Closest { if (p21 == p13u || p21 == Vector3.neg(p13u)) { //coincidental lines overlap / intersect None } else { //parallel lines - val connecting = Vector3(line2.x - line1.x, line2.y - line1.y, line2.z - line1.z) + val connecting = Vector3(line2.p.x - line1.p.x, line2.p.y - line1.p.y, line2.p.z - line1.p.z) val legOnLine1 = line1.d * Vector3.DotProduct(connecting, line1.d) val v = connecting - legOnLine1 Some(Segment3D( - line1.x, line1.y, line1.z, - line1.x + v.x, line1.y + v.y, line1.z + v.z + line1.p.x, line1.p.y, line1.p.z, + line1.p.x + v.x, line1.p.y + v.y, line1.p.z + v.z )) } } else { @@ -169,25 +169,25 @@ object Closest { def apply(line1 : Segment3D, line2 : Segment3D): Option[Segment3D] = { val uline1 = Vector3.Unit(line1.d) val uline2 = Vector3.Unit(line2.d) - apply(Line3D(line1.ax, line1.ay, line1.az, uline1), Line3D(line2.ax, line2.ay, line2.az, uline2)) match { + apply(Line3D(line1.p1.x, line1.p1.y, line1.p1.z, uline1), Line3D(line2.p1.x, line2.p1.y, line2.p1.z, uline2)) match { case Some(seg: Segment3D) => // common skew lines and parallel lines - val sega = Vector3(seg.ax, seg.ay, seg.az) - val p1 = Vector3(line1.ax, line1.ay, line1.az) + val sega = Vector3(seg.p1.x, seg.p1.y, seg.p1.z) + val p1 = Vector3(line1.p1.x, line1.p1.y, line1.p1.z) val d1 = sega - p1 val out1 = if (!Geometry.equalVectors(Vector3.Unit(d1), uline1)) { //clamp seg.a(xyz) to segment line1's bounds p1 } else if (Vector3.MagnitudeSquared(d1) > Vector3.MagnitudeSquared(line1.d)) { - Vector3(line1.bx, line1.by, line1.bz) + Vector3(line1.p2.x, line1.p2.y, line1.p2.z) } else { sega } - val segb = Vector3(seg.bx, seg.by, seg.bz) - val p2 = Vector3(line2.ax, line2.ay, line2.az) + val segb = Vector3(seg.p2.x, seg.p2.y, seg.p2.z) + val p2 = Vector3(line2.p1.x, line2.p1.y, line2.p1.z) val d2 = segb - p2 val out2 = if (!Geometry.equalVectors(Vector3.Unit(d2), uline2)) { //clamp seg.b(xyz) to segment line2's bounds p2 } else if (Vector3.MagnitudeSquared(d2) > Vector3.MagnitudeSquared(line2.d)) { - Vector3(line2.bx, line2.by, line2.bz) + Vector3(line2.p2.x, line2.p2.y, line2.p2.z) } else { segb } @@ -196,19 +196,19 @@ object Closest { out2.x, out2.y, out2.z )) case None => - val connectingU = Vector3.Unit(Vector3(line2.ax - line1.ax, line2.ay - line1.ay, line2.az - line1.az)) + val connectingU = Vector3.Unit(Vector3(line2.p1.x - line1.p1.x, line2.p1.y - line1.p1.y, line2.p1.z - line1.p1.z)) if (uline1 == connectingU || uline1 == Vector3.neg(connectingU)) { // coincidental line segments - val line1a = Vector3(line1.ax, line1.ay, line1.az) - val line1b = Vector3(line1.bx, line1.by, line1.bz) - val line2a = Vector3(line2.ax, line2.ay, line2.az) - val line2b = Vector3(line2.bx, line2.by, line2.bz) + val line1a = Vector3(line1.p1.x, line1.p1.y, line1.p1.z) + val line1b = Vector3(line1.p2.x, line1.p2.y, line1.p2.z) + val line2a = Vector3(line2.p1.x, line2.p1.y, line2.p1.z) + val line2b = Vector3(line2.p2.x, line2.p2.y, line2.p2.z) if (Vector3.Unit(line2a - line1a) != Vector3.Unit(line2b - line1a) || Vector3.Unit(line2a - line1b) != Vector3.Unit(line2b - line1b) || Vector3.Unit(line1a - line2a) != Vector3.Unit(line1b - line2a) || Vector3.Unit(line1a - line2b) != Vector3.Unit(line1b - line2b)) { Some(Segment3D( - line1.ax, line1.ay, line1a.z, - line1.ax, line1.ay, line1a.z + line1.p1.x, line1.p1.y, line1a.z, + line1.p1.x, line1.p1.y, line1a.z )) // overlap regions } else { @@ -245,10 +245,10 @@ object Closest { def apply(s1 : Sphere, s2 : Sphere): Option[Segment3D] = { val distance = Distance(s1, s2) if (distance > 0) { - val s1x = s1.x - val s1y = s1.y - val s1z = s1.z - val v = Vector3.Unit(Vector3(s2.x - s1x, s2.y - s1y, s2.z - s1z)) + val s1x = s1.p.x + val s1y = s1.p.y + val s1z = s1.p.z + val v = Vector3.Unit(Vector3(s2.p.x - s1x, s2.p.y - s1y, s2.p.z - s1z)) val s1d = v * s1.radius val s2d = v * (s1.radius + distance) Some(Segment3D(s1x + s1d.x, s1y + s1d.y, s1y + s1d.y, s1x + s2d.x, s1y + s2d.y, s1y + s2d.y)) @@ -258,8 +258,8 @@ object Closest { } def apply(line : Line3D, sphere : Sphere): Option[Segment3D] = { - val sphereAsPoint = Vector3(sphere.x, sphere.y, sphere.z) - val lineAsPoint = Vector3(line.x, line.y, line.z) + val sphereAsPoint = Vector3(sphere.p.x, sphere.p.y, sphere.p.z) + val lineAsPoint = Vector3(line.p.x, line.p.y, line.p.z) val direct = sphereAsPoint - lineAsPoint val projectionOfDirect = line.d * Vector3.DotProduct(direct, line.d) val heightFromProjection = projectionOfDirect - direct diff --git a/src/main/scala/net/psforever/objects/geometry/Geometry.scala b/src/main/scala/net/psforever/objects/geometry/Geometry.scala index 1a679d02d..3cd47c061 100644 --- a/src/main/scala/net/psforever/objects/geometry/Geometry.scala +++ b/src/main/scala/net/psforever/objects/geometry/Geometry.scala @@ -3,6 +3,30 @@ package net.psforever.objects.geometry import net.psforever.types.Vector3 +trait PrimitiveGeometry { + def center: Point + + def pointOnOutside(line: Line) : Point = pointOnOutside(line.d) + + def pointOnOutside(v: Vector3) : Point +} + +trait Geometry2D extends PrimitiveGeometry { + def center: Point2D + + def pointOnOutside(v: Vector3): Point2D = center +} + +trait Geometry3D extends PrimitiveGeometry { + def center: Point3D + + def pointOnOutside(v: Vector3): Point3D = center +} + +trait Point { + def asVector3: Vector3 +} + trait Slope { def d: Vector3 @@ -15,52 +39,186 @@ trait Line extends Slope { mag - 0.05f < 1f && mag + 0.05f > 1f }, "not a unit vector") + def p: Point + def length: Float = Float.PositiveInfinity } trait Segment extends Slope { + def p1: Point + + def p2: Point + def length: Float = Vector3.Magnitude(d) + + def asLine: PrimitiveGeometry } -final case class Line2D(x: Float, y: Float, d: Vector3) extends Line +final case class Point2D(x: Float, y: Float) extends Geometry2D with Point { + def center: Point2D = this + + def asVector3: Vector3 = Vector3(x, y, 0) +} + +object Point2D { + def apply(): Point2D = Point2D(0, 0) + + def apply(v: Vector3): Point2D = Point2D(v.x, v.y) +} + +final case class Ray2D(p: Point2D, d: Vector3) extends Geometry2D with Line { + def center: Point2D = p +} + +object Ray2D { + def apply(x: Float, y: Float, d: Vector3): Ray2D = Ray2D(Point2D(x, y), d) +} + +final case class Line2D(p: Point2D, d: Vector3) extends Geometry2D with Line { + def center: Point2D = p +} object Line2D { + def apply(ax: Float, ay: Float, d: Vector3): Line2D = { + Line2D(Point2D(ax, ay), d) + } + def apply(ax: Float, ay: Float, bx: Float, by: Float): Line2D = { - Line2D(ax, ay, Vector3.Unit(Vector3(bx-ax, by-ay, 0))) + Line2D(Point2D(ax, ay), Vector3.Unit(Vector3(bx-ax, by-ay, 0))) + } + + def apply(p1: Point2D, p2: Point2D): Line2D = { + Line2D(p1, Vector3.Unit(Vector3(p2.x-p1.x, p2.y-p1.y, 0))) } } -final case class Segment2D(ax: Float, ay: Float, bx: Float, by: Float) extends Segment { - def d: Vector3 = Vector3(bx - ax, by - ay, 0) +final case class Segment2D(p1: Point2D, p2: Point2D) extends Geometry2D with Segment { + def center: Point2D = Point2D(d * 0.5f) + + def d: Vector3 = p2.asVector3 - p1.asVector3 + + def asLine: Line2D = Line2D(p1, Vector3.Unit(d)) } object Segment2D { - def apply(x: Float, y: Float, z: Float, d: Vector3): Segment2D = { + def apply(ax: Float, ay: Float, bx: Float, by: Float): Segment2D = { + Segment2D(Point2D(ax, ay), Point2D(bx, by)) + } + + def apply(x: Float, y: Float, d: Vector3): Segment2D = { Segment2D(x, y, x + d.x, y + d.y) } } -final case class Circle(x: Float, y: Float, radius: Float) +final case class Circle(p: Point2D, radius: Float) extends Geometry2D { + def center : Point2D = p -object Circle { - def apply(radius: Float): Circle = Circle(0f, 0f, radius) -} - -final case class Line3D(x: Float, y: Float, z: Float, d: Vector3) extends Line - -final case class Segment3D(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float) extends Segment { - def d: Vector3 = Vector3(bx - ax, by - ay, bz - az) -} - -object Segment3D { - def apply(x: Float, y: Float, z: Float, d: Vector3): Segment3D = { - Segment3D(x, y, z, z+d.x, y+d.y, z+d.z) + override def pointOnOutside(v: Vector3) : Point2D = { + val slope = Vector3.Unit(v) + val pointOnRim = p.asVector3 + slope * radius + Point2D(pointOnRim.x, pointOnRim.y) } } -final case class Sphere(x: Float, y: Float, z: Float, radius: Float) +object Circle { + def apply(radius: Float): Circle = Circle(Point2D(), radius) -final case class Cylinder(circle: Circle, z: Float, height: Float) + def apply(x: Float, y: Float, radius: Float): Circle = Circle(Point2D(x, y), radius) +} + + +final case class Point3D(x: Float, y: Float, z: Float) extends Geometry3D with Point { + def center: Point3D = this + + def asVector3: Vector3 = Vector3(x, y, z) +} + +object Point3D { + def apply(): Point3D = Point3D(0,0,0) + + def apply(v: Vector3): Point3D = Point3D(v.x, v.y, v.z) +} + +final case class Ray3D(p: Point3D, d: Vector3) extends Geometry3D with Line { + def center: Point3D = p +} + +object Ray3D { + def apply(x: Float, y: Float, z: Float, d: Vector3): Ray3D = Ray3D(Point3D(x,y,z), d) +} + +final case class Line3D(p: Point3D, d: Vector3) extends Geometry3D with Line { + def center: Point3D = p +} + +object Line3D { + def apply(x: Float, y: Float, z: Float, d: Vector3): Line3D = { + Line3D(Point3D(x,y,z), d) + } + + def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Line3D = { + Line3D(Point3D(ax, ay, az), Vector3.Unit(Vector3(bx-ax, by-ay, bz-az))) + } + + def apply(p1: Point3D, p2: Point3D): Line3D = { + Line3D(p1, Vector3.Unit(Vector3(p2.x-p1.x, p2.y-p1.y, p2.z-p1.z))) + } +} + +final case class Segment3D(p1: Point3D, p2: Point3D) extends Geometry3D with Segment { + def center: Point3D = Point3D(d * 0.5f) + + def d: Vector3 = p2.asVector3 - p1.asVector3 + + def asLine: Line3D = Line3D(p1, Vector3.Unit(d)) +} + +object Segment3D { + def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Segment3D = { + Segment3D(Point3D(ax, ay, az), Point3D(bx, by, bz)) + } + + def apply(x: Float, y: Float, z: Float, d: Vector3): Segment3D = { + Segment3D(Point3D(x, y, z), Point3D(x + d.x, y + d.y, z + d.z)) + } +} + +final case class Sphere(p: Point3D, radius: Float) extends Geometry3D { + def center: Point3D = p + + override def pointOnOutside(v: Vector3): Point3D = { + val slope = Vector3.Unit(v) + val mult = radius / math.sqrt(slope.x * slope.x + slope.y * slope.y + slope.z * slope.z) + val pointOnSurface = center.asVector3 + slope * mult.toFloat + Point3D(pointOnSurface.x, pointOnSurface.y, pointOnSurface.z) + } +} + +object Sphere { + def apply(radius: Float): Sphere = Sphere(Point3D(), radius) + + def apply(x: Float, y: Float, z: Float, radius: Float): Sphere = Sphere(Point3D(x,y,z), radius) + + def apply(v: Vector3, radius: Float): Sphere = Sphere(Point3D(v), radius) +} + +final case class Cylinder(circle: Circle, z: Float, height: Float) extends Geometry3D { + def center: Point3D = Point3D(circle.p.x, circle.p.y, z + height * 0.5f) + + override def pointOnOutside(v: Vector3): Point3D = { + val centerVector = center.asVector3 + val slope = Vector3.Unit(v) + val mult = circle.radius / math.sqrt(slope.x * slope.x + slope.y * slope.y) + val pointOnRim = centerVector + slope * mult.toFloat + val point = if (z >= pointOnRim.z && pointOnRim.z <= height) { //side + pointOnRim + } else { //top or base + val rise = height * 0.5f / slope.z + centerVector + slope * rise + } + Point3D(point.x, point.y, point.z) + } +} object Cylinder { def apply(x: Float, y: Float, z: Float, radius: Float, height: Float): Cylinder = { @@ -68,10 +226,6 @@ object Cylinder { } } -object Sphere { - def apply(p: Vector3, radius: Float): Sphere = Sphere(p.x, p.y, p.z, radius) -} - object Geometry { def equalFloats(value1: Float, value2: Float, off: Float = 0.001f): Boolean = { val diff = value1 - value2 diff --git a/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala new file mode 100644 index 000000000..3b053916e --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala @@ -0,0 +1,113 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.geometry + +import net.psforever.objects.ballistics.{PlayerSource, SourceEntry} +import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player} +import net.psforever.types.ExoSuitType + +object GeometryForm { + /** this point can not be used for purposes of geometric representation */ + lazy val invalidPoint: Point3D = Point3D(Float.MinValue, Float.MinValue, Float.MinValue) + /** this circle can not be used for purposes of geometric representation */ + lazy val invalidCircle: Circle = Circle(Point2D(invalidPoint.asVector3), 0) + /** this cylinder can not be used for purposes of geometric representation */ + lazy val invalidCylinder: Cylinder = Cylinder(invalidCircle, Float.MinValue, 0) + + /** + * The geometric representation is the entity's centroid. + * @param o the entity + * @return the representation + */ + def representByPoint()(o: Any): Geometry3D = { + o match { + case p: PlanetSideGameObject => Point3D(p.Position) + case s: SourceEntry => Point3D(s.Position) + case _ => invalidPoint + } + } + + /** + * The geometric representation is the a sphere around the entity's centroid. + * @param radius how wide a hemisphere is + * @param o the entity + * @return the representation + */ + def representBySphere(radius: Float)(o: Any): Geometry3D = { + o match { + case p: PlanetSideGameObject => Sphere(p.Position, radius) + case s: SourceEntry => Sphere(s.Position, radius) + case _ => Sphere(invalidPoint, radius) + } + } + + /** + * The geometric representation is the a cylinder around the entity's base. + * @param radius half the distance across + * @param height how tall the cylinder is (the distance of the top to the base) + * @param o the entity + * @return the representation + */ + def representByCylinder(radius: Float, height: Float)(o: Any): Geometry3D = { + o match { + case p: PlanetSideGameObject => Cylinder(Circle(p.Position.x, p.Position.y, radius), p.Position.z, height) + case s: SourceEntry => Cylinder(Circle(s.Position.x, s.Position.y, radius), s.Position.z, height) + case _ => invalidCylinder + } + } + + /** + * The geometric representation is the a cylinder around the entity's base + * if the target represents a player entity. + * @param radius a measure of the player's bulk + * @param o the entity + * @return the representation + */ + def representPlayerByCylinder(radius: Float)(o: Any): Geometry3D = { + o match { + case p: Player => + val radialOffset = if(p.ExoSuit == ExoSuitType.MAX) 0.25f else 0f + Cylinder( + Circle(p.Position.x, p.Position.y, radius + radialOffset), + p.Position.z, + GlobalDefinitions.MaxDepth(p) + ) + case p: PlayerSource => + val radialOffset = if(p.ExoSuit == ExoSuitType.MAX) 0.25f else 0f + Cylinder( + Circle(p.Position.x, p.Position.y, radius + radialOffset), + p.Position.z, + GlobalDefinitions.avatar.MaxDepth + ) + case _ => + invalidCylinder + } + } + + /** + * The geometric representation is the a cylinder around the entity's base + * as if the target is displaced from the ground at an expected (fixed?) distance. + * @param radius half the distance across + * @param height how tall the cylinder is (the distance of the top to the base) + * @param hoversAt how far off the base coordinates the actual cylinder begins + * @param o the entity + * @return the representation + */ + def representHoveringEntityByCylinder(radius: Float, height: Float, hoversAt: Float)(o: Any): Geometry3D = { + o match { + case p: PlanetSideGameObject => + Cylinder( + Circle(p.Position.x, p.Position.y, radius), + p.Position.z + hoversAt, + height + ) + case s: SourceEntry => + Cylinder( + Circle(s.Position.x, s.Position.y, radius), + s.Position.z + hoversAt, + height + ) + case _ => + invalidCylinder + } + } +} diff --git a/src/main/scala/net/psforever/objects/geometry/Intersection.scala b/src/main/scala/net/psforever/objects/geometry/Intersection.scala index a2e2834c7..cda1f31c9 100644 --- a/src/main/scala/net/psforever/objects/geometry/Intersection.scala +++ b/src/main/scala/net/psforever/objects/geometry/Intersection.scala @@ -13,7 +13,7 @@ object Intersection { def apply(line1: Line2D, line2: Line2D): Boolean = { line1.d != line2.d || { //parallel or antiparallel? - val u = Vector3.Unit(Vector3(line2.x - line1.x, line2.y - line1.y, 0)) + val u = Vector3.Unit(Vector3(line2.p.x - line1.p.x, line2.p.y - line1.p.y, 0)) u == Vector3.Zero || line1.d == u || line1.d == Vector3.neg(u) } } @@ -59,14 +59,14 @@ object Intersection { */ def apply(line1: Segment2D, line2: Segment2D): Boolean = { //setup - val ln1ax = line1.ax - val ln1ay = line1.ay - val ln1bx = line1.bx - val ln1by = line1.by - val ln2ax = line2.ax - val ln2ay = line2.ay - val ln2bx = line2.bx - val ln2by = line2.by + val ln1ax = line1.p1.x + val ln1ay = line1.p1.y + val ln1bx = line1.p2.x + val ln1by = line1.p2.y + val ln2ax = line2.p1.x + val ln2ay = line2.p1.y + val ln2bx = line2.p2.x + val ln2by = line2.p2.y val ln1_ln2a = orientationOfPoints(ln1ax, ln1ay, ln1bx, ln1by, ln2ax, ln2ay) val ln1_ln2b = orientationOfPoints(ln1ax, ln1ay, ln1bx, ln1by, ln2bx, ln2by) val ln2_ln1a = orientationOfPoints(ln2ax, ln2ay, ln2bx, ln2by, ln1ax, ln1ay) @@ -93,7 +93,7 @@ object Intersection { } def apply(c1: Circle, c2 : Circle): Boolean = { - Vector3.Magnitude(Vector3(c1.x - c2.x, c1.y - c2.y, 0)) <= c1.radius + c2.radius + Vector3.Magnitude(Vector3(c1.p.x - c2.p.x, c1.p.y - c2.p.y, 0)) <= c1.radius + c2.radius } /** @@ -111,9 +111,9 @@ object Intersection { def apply(s1: Sphere, s2 : Sphere): Boolean = { Vector3.Magnitude( Vector3( - s1.x - s2.x, - s1.y - s2.y, - s1.z - s2.z + s1.p.x - s2.p.x, + s1.p.y - s2.p.y, + s1.p.z - s2.p.z ) ) <= s1.radius + s2.radius } @@ -128,17 +128,17 @@ object Intersection { val cylinderCircleRadius = cylinderCircle.radius val cylinderTop = cylinder.z + cylinder.height val sphereRadius = sphere.radius - val sphereBase = sphere.z - sphereRadius - val sphereTop = sphere.z + sphereRadius - if (apply(cylinderCircle, Circle(sphere.x, sphere.y, sphereRadius)) && + val sphereBase = sphere.p.z - sphereRadius + val sphereTop = sphere.p.z + sphereRadius + if (apply(cylinderCircle, Circle(sphere.p.x, sphere.p.y, sphereRadius)) && ((sphereTop >= cylinder.z && sphereBase <= cylinderTop) || (cylinderTop >= sphereBase && cylinder.z <= sphereTop))) { // potential intersection ... - val sphereAsPoint = Vector3(sphere.x, sphere.y, sphere.z) - val cylinderAsPoint = Vector3(cylinderCircle.x, cylinderCircle.y, cylinder.z) + val sphereAsPoint = Vector3(sphere.p.x, sphere.p.y, sphere.p.z) + val cylinderAsPoint = Vector3(cylinderCircle.p.x, cylinderCircle.p.y, cylinder.z) val segmentFromCylinderToSphere = sphereAsPoint - cylinderAsPoint val segmentFromCylinderToSphereXY = segmentFromCylinderToSphere.xy - if ((cylinder.z <= sphere.z && sphere.z <= cylinderTop) || + if ((cylinder.z <= sphere.p.z && sphere.p.z <= cylinderTop) || Vector3.MagnitudeSquared(segmentFromCylinderToSphereXY) <= cylinderCircleRadius * cylinderCircleRadius) { true // top or bottom of sphere, or widest part of the sphere, must interact with the cylinder } else { diff --git a/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala b/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala index 7e652ff13..af266474b 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala @@ -5,10 +5,11 @@ import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.ballistics.SourceEntry import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.vital.{Vitality, VitalityDefinition} -import net.psforever.objects.vital.base.{DamageReason, DamageResolution} -import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.vital.base.{DamageModifiers, DamageReason, DamageResolution} +import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.prop.DamageWithPosition import net.psforever.objects.vital.resolution.DamageAndResistance +import net.psforever.objects.zones.Zone /** * A wrapper for a "damage source" in damage calculations @@ -48,3 +49,48 @@ final case class ExplodingEntityReason( /** the entity that exploded is the source of the damage */ override def attribution: Int = definition.ObjectId } + +object ExplodingDamageModifiers { + trait Mod extends DamageModifiers.Mod { + def calculate(damage : Int, data : DamageInteraction, cause : DamageReason) : Int = { + cause match { + case o: ExplodingEntityReason => calculate(damage, data, o) + case _ => damage + } + } + + def calculate(damage : Int, data : DamageInteraction, cause : ExplodingEntityReason) : Int + } +} + +/** + * A variation of the normal radial damage degradation + * that uses the geometric representations of the exploding entity and of the affected target + * in its calculations that determine the distance between them. + * @see `DamageModifierFunctions.RadialDegrade` + */ +case object ExplodingRadialDegrade extends ExplodingDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: ExplodingEntityReason): Int = { + cause.source match { + case withPosition: DamageWithPosition => + val distance = math.sqrt(Zone.distanceCheck( + cause.entity.Definition.asInstanceOf[ObjectDefinition].Geometry(cause.entity), + data.target.Definition.Geometry(data.target) + )) + val radius = withPosition.DamageRadius + val radiusMin = withPosition.DamageRadiusMin + if (distance <= radiusMin) { + damage + } else if (distance <= radius) { + //damage - (damage * profile.DamageAtEdge * (distance - radiusMin) / (radius - radiusMin)).toInt + val base = withPosition.DamageAtEdge + val radi = radius - radiusMin + (damage * ((1 - base) * ((radi - (distance - radiusMin)) / radi) + base)).toInt + } else { + 0 + } + case _ => + damage + } + } +} diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 8622ca769..b61a414f8 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -38,6 +38,7 @@ import akka.actor.typed import net.psforever.actors.session.AvatarActor import net.psforever.actors.zone.ZoneActor import net.psforever.objects.avatar.Avatar +import net.psforever.objects.geometry.Geometry3D import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.vehicles.UtilityType @@ -1175,7 +1176,7 @@ object Zone { * Two game entities are considered "near" each other if they are within a certain distance of one another. * A default function literal mainly used for `causesExplosion`. * @see `causeExplosion` - * @see `Vector3.DistanceSquare` + * @see `ObjectDefinition.Geometry` * @param obj1 a game entity * @param obj2 a game entity * @param maxDistance the square of the maximum distance permissible between game entities @@ -1183,7 +1184,35 @@ object Zone { * @return `true`, if the target entities are near to each other; * `false`, otherwise */ - private def distanceCheck(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = { - Vector3.DistanceSquared(obj1.Position, obj2.Position) <= maxDistance + def distanceCheck(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = { + distanceCheck(obj1.Definition.Geometry(obj1), obj2.Definition.Geometry(obj2), maxDistance) + } + /** + * Two game entities are considered "near" each other if they are within a certain distance of one another. + * @param g1 the geometric representation of a game entity + * @param g2 the geometric representation of a game entity + * @param maxDistance the square of the maximum distance permissible between game entities + * before they are no longer considered "near" + * @return `true`, if the target entities are near to each other; + * `false`, otherwise + */ + def distanceCheck(g1: Geometry3D, g2: Geometry3D, maxDistance: Float): Boolean = { + distanceCheck(g1, g2) <= maxDistance + } + /** + * Two game entities are considered "near" each other if they are within a certain distance of one another. + * @see `PrimitiveGeometry.pointOnOutside` + * @see `Vector3.DistanceSquared` + * @see `Vector3.neg` + * @see `Vector3.Unit` + * @param g1 the geometric representation of a game entity + * @param g2 the geometric representation of a game entity + * @return the crude distance between the two geometric representations + */ + def distanceCheck(g1: Geometry3D, g2: Geometry3D): Float = { + val dir = Vector3.Unit(g2.center.asVector3 - g1.center.asVector3) + val point1 = g1.pointOnOutside(dir).asVector3 + val point2 = g2.pointOnOutside(Vector3.neg(dir)).asVector3 + Vector3.DistanceSquared(point1, point2) } } From e41e7e7cfafc22006a782b517f7cd3a601efb729 Mon Sep 17 00:00:00 2001 From: "Jason_DiDonato@yahoo.com" Date: Wed, 3 Feb 2021 19:39:32 -0500 Subject: [PATCH 5/6] established geometry definitions for damageable object types; fixed rotations; removed unnecessary classes --- .../objects/ExplosiveDeployable.scala | 51 +- .../psforever/objects/GlobalDefinitions.scala | 118 +++- .../psforever/objects/geometry/Closest.scala | 280 --------- .../psforever/objects/geometry/Geometry.scala | 223 -------- .../objects/geometry/GeometryForm.scala | 71 ++- .../objects/geometry/Intersection.scala | 158 ------ .../objects/geometry/PrimitiveShape.scala | 165 ++++++ .../net/psforever/objects/zones/Zone.scala | 10 +- .../scala/net/psforever/types/Vector3.scala | 13 +- src/test/scala/Vector3Test.scala | 48 +- src/test/scala/objects/GeometryTest.scala | 533 ------------------ 11 files changed, 413 insertions(+), 1257 deletions(-) delete mode 100644 src/main/scala/net/psforever/objects/geometry/Closest.scala delete mode 100644 src/main/scala/net/psforever/objects/geometry/Intersection.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala delete mode 100644 src/test/scala/objects/GeometryTest.scala diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 56ed5fca4..c291b4487 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -7,6 +7,7 @@ import net.psforever.objects.ce._ import net.psforever.objects.definition.{ComplexDeployableDefinition, SimpleDeployableDefinition} import net.psforever.objects.definition.converter.SmallDeployableConverter import net.psforever.objects.equipment.JammableUnit +import net.psforever.objects.geometry.Geometry3D import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity} @@ -118,11 +119,11 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D * `false`, otherwise */ def CanDetonate(obj: Vitality with FactionAffinity, damage: Int, data: DamageInteraction): Boolean = { - if (damage == 0 && data.cause.source.SympatheticExplosion) { + !mine.Destroyed && (if (damage == 0 && data.cause.source.SympatheticExplosion) { Damageable.CanDamageOrJammer(mine, damage = 1, data) } else { Damageable.CanDamageOrJammer(mine, damage, data) - } + }) } } @@ -169,7 +170,7 @@ object ExplosiveDeployableControl { val zone = target.Zone zone.Activity ! Zone.HotSpot.Activity(cause) zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.Detonate(target.GUID, target)) - Zone.causeExplosion(zone, target, Some(cause)) + Zone.causeExplosion(zone, target, Some(cause), ExplosiveDeployableControl.detectionForExplosiveSource(target)) } /** @@ -196,4 +197,48 @@ object ExplosiveDeployableControl { ) } } + + /** + * Two game entities are considered "near" each other if they are within a certain distance of one another. + * For explosives, the source of the explosion is always typically constant. + * @see `detectsTarget` + * @see `ObjectDefinition.Geometry` + * @see `Vector3.relativeUp` + * @param obj a game entity that explodes + * @return a function that resolves a potential target as detected + */ + def detectionForExplosiveSource(obj: PlanetSideGameObject): (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = { + val up = Vector3.relativeUp(obj.Orientation) //check relativeUp; rotate as little as necessary! + val g1 = obj.Definition.Geometry(obj) + detectTarget(g1, up) + } + + /** + * Two game entities are considered "near" each other if they are within a certain distance of one another. + * For explosives, targets in the damage radius in the direction of the blast (above the explosive) are valid targets. + * Targets that are ~0.5916f units in the opposite direction of the blast (below the explosive) are also selected. + * @see `ObjectDefinition.Geometry` + * @see `PrimitiveGeometry.pointOnOutside` + * @see `Vector3.DistanceSquared` + * @see `Vector3.neg` + * @see `Vector3.relativeUp` + * @see `Vector3.ScalarProjection` + * @see `Vector3.Unit` + * @param g1 a cached geometric representation that should belong to `obj1` + * @param up a cached vector in the direction of "above `obj1`'s geometric representation" + * @param obj1 a game entity that explodes + * @param obj2 a game entity that suffers the explosion + * @param maxDistance the square of the maximum distance permissible between game entities + * before they are no longer considered "near" + * @return `true`, if the target entities are near enough to each other; + * `false`, otherwise + */ + def detectTarget(g1: Geometry3D, up: Vector3)(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float) : Boolean = { + val g2 = obj2.Definition.Geometry(obj2) + val dir = g2.center.asVector3 - g1.center.asVector3 + val scalar = Vector3.ScalarProjection(dir, up) + val point1 = g1.pointOnOutside(dir).asVector3 + val point2 = g2.pointOnOutside(Vector3.neg(dir)).asVector3 + (scalar >= 0 || Vector3.MagnitudeSquared(up * scalar) < 0.35f) && Vector3.DistanceSquared(point1, point2) <= maxDistance + } } diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 62373100a..11463740f 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -7,6 +7,7 @@ import net.psforever.objects.ce.{DeployableCategory, DeployedItem} import net.psforever.objects.definition._ import net.psforever.objects.definition.converter._ import net.psforever.objects.equipment._ +import net.psforever.objects.geometry.GeometryForm import net.psforever.objects.inventory.InventoryTile import net.psforever.objects.serverobject.aura.Aura import net.psforever.objects.serverobject.doors.DoorDefinition @@ -5602,6 +5603,11 @@ object GlobalDefinitions { * Initialize `VehicleDefinition` globals. */ private def init_vehicles(): Unit = { + val atvForm = GeometryForm.representByCylinder(radius = 1.1797f, height = 1.1875f) _ + val delivererForm = GeometryForm.representByCylinder(radius = 2.46095f, height = 2.40626f) _ //TODO hexahedron + val apcForm = GeometryForm.representByCylinder(radius = 4.6211f, height = 3.90626f) _ //TODO hexahedron + val liberatorForm = GeometryForm.representByCylinder(radius = 3.74615f, height = 2.51563f) _ + fury.Name = "fury" fury.MaxHealth = 650 fury.Damageable = true @@ -5632,6 +5638,7 @@ object GlobalDefinitions { fury.DrownAtMaxDepth = true fury.MaxDepth = 1.3f fury.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + fury.Geometry = atvForm quadassault.Name = "quadassault" // Basilisk quadassault.MaxHealth = 650 @@ -5663,6 +5670,7 @@ object GlobalDefinitions { quadassault.DrownAtMaxDepth = true quadassault.MaxDepth = 1.3f quadassault.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + quadassault.Geometry = atvForm quadstealth.Name = "quadstealth" // Wraith quadstealth.MaxHealth = 650 @@ -5694,6 +5702,7 @@ object GlobalDefinitions { quadstealth.DrownAtMaxDepth = true quadstealth.MaxDepth = 1.25f quadstealth.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + quadstealth.Geometry = atvForm two_man_assault_buggy.Name = "two_man_assault_buggy" // Harasser two_man_assault_buggy.MaxHealth = 1250 @@ -5727,6 +5736,7 @@ object GlobalDefinitions { two_man_assault_buggy.DrownAtMaxDepth = true two_man_assault_buggy.MaxDepth = 1.5f two_man_assault_buggy.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + two_man_assault_buggy.Geometry = GeometryForm.representByCylinder(radius = 2.10545f, height = 1.59376f) skyguard.Name = "skyguard" skyguard.MaxHealth = 1000 @@ -5749,7 +5759,6 @@ object GlobalDefinitions { skyguard.AutoPilotSpeeds = (22, 8) skyguard.DestroyedModel = Some(DestroyedVehicle.Skyguard) skyguard.JackingDuration = Array(0, 15, 5, 3) - skyguard.explodes = true skyguard.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One @@ -5762,6 +5771,7 @@ object GlobalDefinitions { skyguard.DrownAtMaxDepth = true skyguard.MaxDepth = 1.5f skyguard.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + skyguard.Geometry = GeometryForm.representByCylinder(radius = 1.8867f, height = 1.4375f) threemanheavybuggy.Name = "threemanheavybuggy" // Marauder threemanheavybuggy.MaxHealth = 1700 @@ -5801,6 +5811,7 @@ object GlobalDefinitions { threemanheavybuggy.DrownAtMaxDepth = true threemanheavybuggy.MaxDepth = 1.83f threemanheavybuggy.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + threemanheavybuggy.Geometry = GeometryForm.representByCylinder(radius = 2.1953f, height = 2.03125f) twomanheavybuggy.Name = "twomanheavybuggy" // Enforcer twomanheavybuggy.MaxHealth = 1800 @@ -5835,6 +5846,7 @@ object GlobalDefinitions { twomanheavybuggy.DrownAtMaxDepth = true twomanheavybuggy.MaxDepth = 1.95f twomanheavybuggy.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + twomanheavybuggy.Geometry = GeometryForm.representByCylinder(radius = 2.60935f, height = 1.79688f) twomanhoverbuggy.Name = "twomanhoverbuggy" // Thresher twomanhoverbuggy.MaxHealth = 1600 @@ -5868,6 +5880,7 @@ object GlobalDefinitions { } twomanhoverbuggy.DrownAtMaxDepth = true twomanhoverbuggy.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the thresher hovers over water, so ...? + twomanhoverbuggy.Geometry = GeometryForm.representByCylinder(radius = 2.1875f, height = 2.01563f) mediumtransport.Name = "mediumtransport" // Deliverer mediumtransport.MaxHealth = 2500 @@ -5909,6 +5922,7 @@ object GlobalDefinitions { mediumtransport.DrownAtMaxDepth = false mediumtransport.MaxDepth = 1.2f mediumtransport.UnderwaterLifespan(suffocation = -1, recovery = -1) + mediumtransport.Geometry = delivererForm battlewagon.Name = "battlewagon" // Raider battlewagon.MaxHealth = 2500 @@ -5953,6 +5967,7 @@ object GlobalDefinitions { battlewagon.DrownAtMaxDepth = true battlewagon.MaxDepth = 1.2f battlewagon.UnderwaterLifespan(suffocation = -1, recovery = -1) + battlewagon.Geometry = delivererForm thunderer.Name = "thunderer" thunderer.MaxHealth = 2500 @@ -5994,6 +6009,7 @@ object GlobalDefinitions { thunderer.DrownAtMaxDepth = true thunderer.MaxDepth = 1.2f thunderer.UnderwaterLifespan(suffocation = -1, recovery = -1) + thunderer.Geometry = delivererForm aurora.Name = "aurora" aurora.MaxHealth = 2500 @@ -6035,6 +6051,7 @@ object GlobalDefinitions { aurora.DrownAtMaxDepth = true aurora.MaxDepth = 1.2f aurora.UnderwaterLifespan(suffocation = -1, recovery = -1) + aurora.Geometry = delivererForm apc_tr.Name = "apc_tr" // Juggernaut apc_tr.MaxHealth = 6000 @@ -6099,6 +6116,7 @@ object GlobalDefinitions { apc_tr.DrownAtMaxDepth = true apc_tr.MaxDepth = 3 apc_tr.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) + apc_tr.Geometry = apcForm apc_nc.Name = "apc_nc" // Vindicator apc_nc.MaxHealth = 6000 @@ -6163,6 +6181,7 @@ object GlobalDefinitions { apc_nc.DrownAtMaxDepth = true apc_nc.MaxDepth = 3 apc_nc.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) + apc_nc.Geometry = apcForm apc_vs.Name = "apc_vs" // Leviathan apc_vs.MaxHealth = 6000 @@ -6227,6 +6246,7 @@ object GlobalDefinitions { apc_vs.DrownAtMaxDepth = true apc_vs.MaxDepth = 3 apc_vs.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) + apc_vs.Geometry = apcForm lightning.Name = "lightning" lightning.MaxHealth = 2000 @@ -6259,6 +6279,7 @@ object GlobalDefinitions { lightning.DrownAtMaxDepth = true lightning.MaxDepth = 1.38f lightning.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) + lightning.Geometry = GeometryForm.representByCylinder(radius = 2.5078f, height = 1.79688f) prowler.Name = "prowler" prowler.MaxHealth = 4800 @@ -6296,6 +6317,7 @@ object GlobalDefinitions { prowler.DrownAtMaxDepth = true prowler.MaxDepth = 3 prowler.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) + prowler.Geometry = GeometryForm.representByCylinder(radius = 3.461f, height = 3.48438f) vanguard.Name = "vanguard" vanguard.MaxHealth = 5400 @@ -6329,6 +6351,7 @@ object GlobalDefinitions { vanguard.DrownAtMaxDepth = true vanguard.MaxDepth = 2.7f vanguard.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) + vanguard.Geometry = GeometryForm.representByCylinder(radius = 3.8554f, height = 2.60938f) magrider.Name = "magrider" magrider.MaxHealth = 4200 @@ -6364,6 +6387,7 @@ object GlobalDefinitions { magrider.DrownAtMaxDepth = true magrider.MaxDepth = 2 magrider.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the magrider hovers over water, so ...? + magrider.Geometry = GeometryForm.representByCylinder(radius = 3.3008f, height = 3.26562f) val utilityConverter = new UtilityVehicleConverter ant.Name = "ant" @@ -6397,6 +6421,7 @@ object GlobalDefinitions { ant.DrownAtMaxDepth = true ant.MaxDepth = 2 ant.UnderwaterLifespan(suffocation = 12000L, recovery = 6000L) + ant.Geometry = GeometryForm.representByCylinder(radius = 2.16795f, height = 2.09376f) //TODO hexahedron ams.Name = "ams" ams.MaxHealth = 5000 // Temporary - original value is 3000 @@ -6433,6 +6458,7 @@ object GlobalDefinitions { ams.DrownAtMaxDepth = true ams.MaxDepth = 3 ams.UnderwaterLifespan(suffocation = 5000L, recovery = 5000L) + ams.Geometry = GeometryForm.representByCylinder(radius = 3.0117f, height = 3.39062f) //TODO hexahedron val variantConverter = new VariantVehicleConverter router.Name = "router" @@ -6469,6 +6495,7 @@ object GlobalDefinitions { router.DrownAtMaxDepth = true router.MaxDepth = 2 router.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the router hovers over water, so ...? + router.Geometry = GeometryForm.representByCylinder(radius = 3.64845f, height = 3.51563f) //TODO hexahedron switchblade.Name = "switchblade" switchblade.MaxHealth = 1750 @@ -6505,6 +6532,7 @@ object GlobalDefinitions { switchblade.DrownAtMaxDepth = true switchblade.MaxDepth = 2 switchblade.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the switchblade hovers over water, so ...? + switchblade.Geometry = GeometryForm.representByCylinder(radius = 2.4335f, height = 2.73438f) flail.Name = "flail" flail.MaxHealth = 2400 @@ -6539,6 +6567,7 @@ object GlobalDefinitions { flail.DrownAtMaxDepth = true flail.MaxDepth = 2 flail.UnderwaterLifespan(suffocation = 45000L, recovery = 5000L) //but the flail hovers over water, so ...? + flail.Geometry = GeometryForm.representByCylinder(radius = 2.1875f, height = 2.21875f) mosquito.Name = "mosquito" mosquito.MaxHealth = 665 @@ -6572,6 +6601,7 @@ object GlobalDefinitions { } mosquito.DrownAtMaxDepth = true mosquito.MaxDepth = 2 //flying vehicles will automatically disable + mosquito.Geometry = GeometryForm.representByCylinder(radius = 2.72108f, height = 2.5f) lightgunship.Name = "lightgunship" // Reaver lightgunship.MaxHealth = 1000 @@ -6606,6 +6636,7 @@ object GlobalDefinitions { } lightgunship.DrownAtMaxDepth = true lightgunship.MaxDepth = 2 //flying vehicles will automatically disable + lightgunship.Geometry = GeometryForm.representByCylinder(radius = 2.375f, height = 1.98438f) wasp.Name = "wasp" wasp.MaxHealth = 515 @@ -6639,6 +6670,7 @@ object GlobalDefinitions { } wasp.DrownAtMaxDepth = true wasp.MaxDepth = 2 //flying vehicles will automatically disable + wasp.Geometry = GeometryForm.representByCylinder(radius = 2.88675f, height = 2.5f) liberator.Name = "liberator" liberator.MaxHealth = 2500 @@ -6680,6 +6712,7 @@ object GlobalDefinitions { } liberator.DrownAtMaxDepth = true liberator.MaxDepth = 2 //flying vehicles will automatically disable + liberator.Geometry = liberatorForm vulture.Name = "vulture" vulture.MaxHealth = 2500 @@ -6722,6 +6755,7 @@ object GlobalDefinitions { } vulture.DrownAtMaxDepth = true vulture.MaxDepth = 2 //flying vehicles will automatically disable + vulture.Geometry = liberatorForm dropship.Name = "dropship" // Galaxy dropship.MaxHealth = 5000 @@ -6796,6 +6830,7 @@ object GlobalDefinitions { } dropship.DrownAtMaxDepth = true dropship.MaxDepth = 2 + dropship.Geometry = GeometryForm.representByCylinder(radius = 10.52202f, height = 6.23438f) galaxy_gunship.Name = "galaxy_gunship" galaxy_gunship.MaxHealth = 6000 @@ -6849,6 +6884,7 @@ object GlobalDefinitions { } galaxy_gunship.DrownAtMaxDepth = true galaxy_gunship.MaxDepth = 2 + galaxy_gunship.Geometry = GeometryForm.representByCylinder(radius = 9.2382f, height = 5.01562f) lodestar.Name = "lodestar" lodestar.MaxHealth = 5000 @@ -6890,6 +6926,7 @@ object GlobalDefinitions { } lodestar.DrownAtMaxDepth = true lodestar.MaxDepth = 2 + lodestar.Geometry = GeometryForm.representByCylinder(radius = 7.8671f, height = 6.79688f) //TODO hexahedron phantasm.Name = "phantasm" phantasm.MaxHealth = 2500 @@ -6932,6 +6969,7 @@ object GlobalDefinitions { } phantasm.DrownAtMaxDepth = true phantasm.MaxDepth = 2 + phantasm.Geometry = GeometryForm.representByCylinder(radius = 5.2618f, height = 3f) droppod.Name = "droppod" droppod.MaxHealth = 20000 @@ -6945,12 +6983,18 @@ object GlobalDefinitions { droppod.DestroyedModel = None //the adb calls out a droppod; the cyclic nature of this confounds me droppod.DamageUsing = DamageCalculations.AgainstAircraft droppod.DrownAtMaxDepth = false + //TODO geometry? } /** * Initialize `Deployable` globals. */ private def init_deployables(): Unit = { + val mine = GeometryForm.representByCylinder(radius = 0.1914f, height = 0.0957f) _ + val smallTurret = GeometryForm.representByCylinder(radius = 0.48435f, height = 1.23438f) _ + val sensor = GeometryForm.representByCylinder(radius = 0.1914f, height = 1.21875f) _ + val largeTurret = GeometryForm.representByCylinder(radius = 0.8437f, height = 2.29687f) _ + boomer.Name = "boomer" boomer.Descriptor = "Boomers" boomer.MaxHealth = 100 @@ -6972,6 +7016,7 @@ object GlobalDefinitions { DamageAtEdge = 0.1f Modifiers = ExplodingRadialDegrade } + boomer.Geometry = mine he_mine.Name = "he_mine" he_mine.Descriptor = "Mines" @@ -6993,6 +7038,7 @@ object GlobalDefinitions { DamageAtEdge = 0.25f Modifiers = ExplodingRadialDegrade } + he_mine.Geometry = mine jammer_mine.Name = "jammer_mine" jammer_mine.Descriptor = "JammerMines" @@ -7002,6 +7048,7 @@ object GlobalDefinitions { jammer_mine.Repairable = false jammer_mine.DeployTime = Duration.create(1000, "ms") jammer_mine.DetonateOnJamming = false + jammer_mine.Geometry = mine spitfire_turret.Name = "spitfire_turret" spitfire_turret.Descriptor = "Spitfires" @@ -7025,6 +7072,7 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = ExplodingRadialDegrade } + spitfire_turret.Geometry = smallTurret spitfire_cloaked.Name = "spitfire_cloaked" spitfire_cloaked.Descriptor = "CloakingSpitfires" @@ -7047,6 +7095,7 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = ExplodingRadialDegrade } + spitfire_cloaked.Geometry = smallTurret spitfire_aa.Name = "spitfire_aa" spitfire_aa.Descriptor = "FlakSpitfires" @@ -7069,6 +7118,7 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = ExplodingRadialDegrade } + spitfire_aa.Geometry = smallTurret motionalarmsensor.Name = "motionalarmsensor" motionalarmsensor.Descriptor = "MotionSensors" @@ -7077,6 +7127,7 @@ object GlobalDefinitions { motionalarmsensor.Repairable = true motionalarmsensor.RepairIfDestroyed = false motionalarmsensor.DeployTime = Duration.create(1000, "ms") + motionalarmsensor.Geometry = sensor sensor_shield.Name = "sensor_shield" sensor_shield.Descriptor = "SensorShields" @@ -7085,6 +7136,7 @@ object GlobalDefinitions { sensor_shield.Repairable = true sensor_shield.RepairIfDestroyed = false sensor_shield.DeployTime = Duration.create(5000, "ms") + sensor_shield.Geometry = sensor tank_traps.Name = "tank_traps" tank_traps.Descriptor = "TankTraps" @@ -7103,6 +7155,7 @@ object GlobalDefinitions { DamageAtEdge = 0.2f Modifiers = ExplodingRadialDegrade } + tank_traps.Geometry = GeometryForm.representByCylinder(radius = 2.89680997f, height = 3.57812f) val fieldTurretConverter = new FieldTurretConverter portable_manned_turret.Name = "portable_manned_turret" @@ -7130,6 +7183,7 @@ object GlobalDefinitions { DamageAtEdge = 0.1f Modifiers = ExplodingRadialDegrade } + portable_manned_turret.Geometry = largeTurret portable_manned_turret_nc.Name = "portable_manned_turret_nc" portable_manned_turret_nc.Descriptor = "FieldTurrets" @@ -7156,6 +7210,7 @@ object GlobalDefinitions { DamageAtEdge = 0.1f Modifiers = ExplodingRadialDegrade } + portable_manned_turret_nc.Geometry = largeTurret portable_manned_turret_tr.Name = "portable_manned_turret_tr" portable_manned_turret_tr.Descriptor = "FieldTurrets" @@ -7182,6 +7237,7 @@ object GlobalDefinitions { DamageAtEdge = 0.1f Modifiers = ExplodingRadialDegrade } + portable_manned_turret_tr.Geometry = largeTurret portable_manned_turret_vs.Name = "portable_manned_turret_vs" portable_manned_turret_vs.Descriptor = "FieldTurrets" @@ -7208,6 +7264,7 @@ object GlobalDefinitions { DamageAtEdge = 0.1f Modifiers = ExplodingRadialDegrade } + portable_manned_turret_vs.Geometry = largeTurret deployable_shield_generator.Name = "deployable_shield_generator" deployable_shield_generator.Descriptor = "ShieldGenerators" @@ -7217,6 +7274,7 @@ object GlobalDefinitions { deployable_shield_generator.RepairIfDestroyed = false deployable_shield_generator.DeployTime = Duration.create(6000, "ms") deployable_shield_generator.Model = ComplexDeployableResolutions.calculate + deployable_shield_generator.Geometry = GeometryForm.representByCylinder(radius = 0.6562f, height = 2.17188f) router_telepad_deployable.Name = "router_telepad_deployable" router_telepad_deployable.MaxHealth = 100 @@ -7226,6 +7284,7 @@ object GlobalDefinitions { router_telepad_deployable.DeployCategory = DeployableCategory.Telepads router_telepad_deployable.Packet = new TelepadDeployableConverter router_telepad_deployable.Model = SimpleResolutions.calculate + router_telepad_deployable.Geometry = GeometryForm.representByRaisedSphere(radius = 1.2344f) internal_router_telepad_deployable.Name = "router_telepad_deployable" internal_router_telepad_deployable.MaxHealth = 1 @@ -7240,6 +7299,8 @@ object GlobalDefinitions { * Initialize `Miscellaneous` globals. */ private def initMiscellaneous(): Unit = { + val vterm = GeometryForm.representByCylinder(radius = 1.03515f, height = 1.09374f) _ + ams_respawn_tube.Name = "ams_respawn_tube" ams_respawn_tube.Delay = 10 ams_respawn_tube.SpecificPointFunc = SpawnPoint.AMS @@ -7280,9 +7341,10 @@ object GlobalDefinitions { order_terminal.MaxHealth = 500 order_terminal.Damageable = true order_terminal.Repairable = true - order_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + order_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) order_terminal.RepairIfDestroyed = true order_terminal.Subtract.Damage1 = 8 + order_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.8438f, height = 1.3f) order_terminala.Name = "order_terminala" order_terminala.Tab += 0 -> OrderTerminalDefinition.EquipmentPage( @@ -7344,16 +7406,18 @@ object GlobalDefinitions { cert_terminal.MaxHealth = 500 cert_terminal.Damageable = true cert_terminal.Repairable = true - cert_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + cert_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) cert_terminal.RepairIfDestroyed = true cert_terminal.Subtract.Damage1 = 8 + cert_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.66405f, height = 1.09374f) implant_terminal_mech.Name = "implant_terminal_mech" implant_terminal_mech.MaxHealth = 1500 //TODO 1000; right now, 1000 (mech) + 500 (interface) implant_terminal_mech.Damageable = true implant_terminal_mech.Repairable = true - implant_terminal_mech.autoRepair = AutoRepairStats(1.6f, 5000, 2400, 0.5f) //ori. 1, 5000, 2400, 0.5f + implant_terminal_mech.autoRepair = AutoRepairStats(1.6f, 5000, 2400, 0.5f) implant_terminal_mech.RepairIfDestroyed = true + implant_terminal_mech.Geometry = GeometryForm.representByCylinder(radius = 2.7813f, height = 6.4375f) implant_terminal_interface.Name = "implant_terminal_interface" implant_terminal_interface.Tab += 0 -> OrderTerminalDefinition.ImplantPage(ImplantTerminalDefinition.implants) @@ -7362,6 +7426,7 @@ object GlobalDefinitions { implant_terminal_interface.Repairable = true implant_terminal_interface.autoRepair = AutoRepairStats(1, 5000, 200, 1) //TODO amount and drain are default? undefined? implant_terminal_interface.RepairIfDestroyed = true + //TODO will need geometry when Damageable = true ground_vehicle_terminal.Name = "ground_vehicle_terminal" ground_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( @@ -7372,9 +7437,10 @@ object GlobalDefinitions { ground_vehicle_terminal.MaxHealth = 500 ground_vehicle_terminal.Damageable = true ground_vehicle_terminal.Repairable = true - ground_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + ground_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) ground_vehicle_terminal.RepairIfDestroyed = true ground_vehicle_terminal.Subtract.Damage1 = 8 + ground_vehicle_terminal.Geometry = vterm air_vehicle_terminal.Name = "air_vehicle_terminal" air_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( @@ -7385,9 +7451,10 @@ object GlobalDefinitions { air_vehicle_terminal.MaxHealth = 500 air_vehicle_terminal.Damageable = true air_vehicle_terminal.Repairable = true - air_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + air_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) air_vehicle_terminal.RepairIfDestroyed = true air_vehicle_terminal.Subtract.Damage1 = 8 + air_vehicle_terminal.Geometry = vterm dropship_vehicle_terminal.Name = "dropship_vehicle_terminal" dropship_vehicle_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( @@ -7398,9 +7465,10 @@ object GlobalDefinitions { dropship_vehicle_terminal.MaxHealth = 500 dropship_vehicle_terminal.Damageable = true dropship_vehicle_terminal.Repairable = true - dropship_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + dropship_vehicle_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) dropship_vehicle_terminal.RepairIfDestroyed = true dropship_vehicle_terminal.Subtract.Damage1 = 8 + dropship_vehicle_terminal.Geometry = vterm vehicle_terminal_combined.Name = "vehicle_terminal_combined" vehicle_terminal_combined.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( @@ -7411,9 +7479,10 @@ object GlobalDefinitions { vehicle_terminal_combined.MaxHealth = 500 vehicle_terminal_combined.Damageable = true vehicle_terminal_combined.Repairable = true - vehicle_terminal_combined.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + vehicle_terminal_combined.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) vehicle_terminal_combined.RepairIfDestroyed = true vehicle_terminal_combined.Subtract.Damage1 = 8 + vehicle_terminal_combined.Geometry = vterm vanu_air_vehicle_term.Name = "vanu_air_vehicle_term" vanu_air_vehicle_term.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( @@ -7424,7 +7493,7 @@ object GlobalDefinitions { vanu_air_vehicle_term.MaxHealth = 500 vanu_air_vehicle_term.Damageable = true vanu_air_vehicle_term.Repairable = true - vanu_air_vehicle_term.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + vanu_air_vehicle_term.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) vanu_air_vehicle_term.RepairIfDestroyed = true vanu_air_vehicle_term.Subtract.Damage1 = 8 @@ -7437,7 +7506,7 @@ object GlobalDefinitions { vanu_vehicle_term.MaxHealth = 500 vanu_vehicle_term.Damageable = true vanu_vehicle_term.Repairable = true - vanu_vehicle_term.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + vanu_vehicle_term.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) vanu_vehicle_term.RepairIfDestroyed = true vanu_vehicle_term.Subtract.Damage1 = 8 @@ -7450,9 +7519,10 @@ object GlobalDefinitions { bfr_terminal.MaxHealth = 500 bfr_terminal.Damageable = true bfr_terminal.Repairable = true - bfr_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + bfr_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) bfr_terminal.RepairIfDestroyed = true bfr_terminal.Subtract.Damage1 = 8 + bfr_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.92185f, height = 2.64693f) respawn_tube.Name = "respawn_tube" respawn_tube.Delay = 10 @@ -7461,9 +7531,10 @@ object GlobalDefinitions { respawn_tube.Damageable = true respawn_tube.DamageableByFriendlyFire = false respawn_tube.Repairable = true - respawn_tube.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //orig. 1, 10000, 2400, 1 + respawn_tube.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) respawn_tube.RepairIfDestroyed = true respawn_tube.Subtract.Damage1 = 8 + respawn_tube.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f) respawn_tube_sanctuary.Name = "respawn_tube" respawn_tube_sanctuary.Delay = 10 @@ -7472,7 +7543,8 @@ object GlobalDefinitions { respawn_tube_sanctuary.Damageable = false //true? respawn_tube_sanctuary.DamageableByFriendlyFire = false respawn_tube_sanctuary.Repairable = true - respawn_tube_sanctuary.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //orig. 1, 10000, 2400, 1 + respawn_tube_sanctuary.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) + //TODO will need geometry when Damageable = true respawn_tube_tower.Name = "respawn_tube_tower" respawn_tube_tower.Delay = 10 @@ -7481,9 +7553,10 @@ object GlobalDefinitions { respawn_tube_tower.Damageable = true respawn_tube_tower.DamageableByFriendlyFire = false respawn_tube_tower.Repairable = true - respawn_tube_tower.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) //orig. 1, 10000, 2400, 1 + respawn_tube_tower.autoRepair = AutoRepairStats(1.6f, 10000, 2400, 1) respawn_tube_tower.RepairIfDestroyed = true respawn_tube_tower.Subtract.Damage1 = 8 + respawn_tube_tower.Geometry = GeometryForm.representByCylinder(radius = 0.9336f, height = 2.84375f) teleportpad_terminal.Name = "teleportpad_terminal" teleportpad_terminal.Tab += 0 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.routerTerminal) @@ -7499,8 +7572,9 @@ object GlobalDefinitions { medical_terminal.MaxHealth = 500 medical_terminal.Damageable = true medical_terminal.Repairable = true - medical_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + medical_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) medical_terminal.RepairIfDestroyed = true + medical_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.711f, height = 1.75f) adv_med_terminal.Name = "adv_med_terminal" adv_med_terminal.Interval = 500 @@ -7511,8 +7585,9 @@ object GlobalDefinitions { adv_med_terminal.MaxHealth = 750 adv_med_terminal.Damageable = true adv_med_terminal.Repairable = true - adv_med_terminal.autoRepair = AutoRepairStats(1.57894f, 5000, 2400, 0.5f) //orig. 1, 5000, 2400, 0.5f + adv_med_terminal.autoRepair = AutoRepairStats(1.57894f, 5000, 2400, 0.5f) adv_med_terminal.RepairIfDestroyed = true + adv_med_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.8662125f, height = 3.47f) crystals_health_a.Name = "crystals_health_a" crystals_health_a.Interval = 500 @@ -7539,7 +7614,7 @@ object GlobalDefinitions { portable_med_terminal.MaxHealth = 500 portable_med_terminal.Damageable = false //TODO actually true portable_med_terminal.Repairable = false - portable_med_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) //orig. 1, 5000, 3500, 0.5f + portable_med_terminal.autoRepair = AutoRepairStats(2.24215f, 5000, 3500, 0.5f) pad_landing_frame.Name = "pad_landing_frame" pad_landing_frame.Interval = 1000 @@ -7653,7 +7728,7 @@ object GlobalDefinitions { manned_turret.Damageable = true manned_turret.DamageDisablesAt = 0 manned_turret.Repairable = true - manned_turret.autoRepair = AutoRepairStats(1.0909f, 10000, 1600, 0.5f) //orig. 1, 10000, 1600, 0.5f + manned_turret.autoRepair = AutoRepairStats(1.0909f, 10000, 1600, 0.5f) manned_turret.RepairIfDestroyed = true manned_turret.Weapons += 1 -> new mutable.HashMap() manned_turret.Weapons(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan @@ -7671,13 +7746,14 @@ object GlobalDefinitions { DamageAtEdge = 0.1f Modifiers = ExplodingRadialDegrade } + manned_turret.Geometry = GeometryForm.representByCylinder(radius = 1.2695f, height = 2.6875f) vanu_sentry_turret.Name = "vanu_sentry_turret" vanu_sentry_turret.MaxHealth = 1500 vanu_sentry_turret.Damageable = true vanu_sentry_turret.DamageDisablesAt = 0 vanu_sentry_turret.Repairable = true - vanu_sentry_turret.autoRepair = AutoRepairStats(3.27272f, 10000, 1000, 0.5f) //orig. 3, 10000, 1000, 0.5f + vanu_sentry_turret.autoRepair = AutoRepairStats(3.27272f, 10000, 1000, 0.5f) vanu_sentry_turret.RepairIfDestroyed = true vanu_sentry_turret.Weapons += 1 -> new mutable.HashMap() vanu_sentry_turret.Weapons(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon @@ -7685,6 +7761,7 @@ object GlobalDefinitions { vanu_sentry_turret.MountPoints += 2 -> 0 vanu_sentry_turret.FactionLocked = false vanu_sentry_turret.ReserveAmmunition = false + vanu_sentry_turret.Geometry = GeometryForm.representByCylinder(radius = 1.76311f, height = 3.984375f) painbox.Name = "painbox" painbox.alwaysOn = false @@ -7759,7 +7836,7 @@ object GlobalDefinitions { generator.Damageable = true generator.DamageableByFriendlyFire = false generator.Repairable = true - generator.autoRepair = AutoRepairStats(0.77775f, 5000, 875, 1) //orig. 1, 5000, 875, 1 + generator.autoRepair = AutoRepairStats(0.77775f, 5000, 875, 1) generator.RepairDistance = 13.5f generator.RepairIfDestroyed = true generator.Subtract.Damage1 = 9 @@ -7774,5 +7851,6 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade //damage is 99999 at 14m, dropping rapidly to ~1 at 15.75m } + generator.Geometry = GeometryForm.representByCylinder(radius = 1.2617f, height = 9.14063f) } } diff --git a/src/main/scala/net/psforever/objects/geometry/Closest.scala b/src/main/scala/net/psforever/objects/geometry/Closest.scala deleted file mode 100644 index 4b3dc6ebb..000000000 --- a/src/main/scala/net/psforever/objects/geometry/Closest.scala +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright (c) 2021 PSForever -package net.psforever.objects.geometry - -import net.psforever.types.Vector3 - -object Closest { - object Distance { - def apply(point : Vector3, seg : Segment2D) : Float = { - val segdx = seg.p2.x - seg.p1.x - val segdy = seg.p2.y - seg.p1.y - ((point.x - seg.p1.x) * segdx + (point.y - seg.p1.y) * segdy) / - Vector3.MagnitudeSquared(Vector3(segdx, segdy, 0)) - } - - def apply(line1 : Line2D, line2 : Line2D) : Float = { - if (Intersection.Test(line1, line2)) { //intersecting lines - 0f - } else { - math.abs( - Vector3.DotProduct( - Vector3(line2.p.x - line1.p.x, line2.p.y - line1.p.y, 0), - Vector3(-1/line1.d.y, 1/line1.d.x, 0) - ) - ) - } - } - - def apply(seg1: Segment2D, seg2: Segment2D): Float = { - if (Intersection.Test(seg1, seg2)) { //intersecting line segments - 0f - } else { - val v1a = Vector3(seg1.p1.x, seg1.p1.y, 0) - val v2a = Vector3(seg2.p1.x, seg2.p1.y, 0) - val v1b = Vector3(seg1.p2.x, seg1.p2.y, 0) - val v2b = Vector3(seg2.p2.x, seg2.p2.y, 0) - math.min( - apply(v1a, seg2), - math.min( - apply(v1b, seg2), - math.min( - apply(v2a, seg1), - apply(v2b, seg1) - ) - ) - ) - } - } - - def apply(c1: Circle, c2 : Circle): Float = { - math.max(0, Vector3.Magnitude(Vector3(c1.p.x - c2.p.x, c1.p.y - c2.p.y, 0)) - c1.radius - c2.radius) - } - - /** - * na - * @param line1 na - * @param line2 na - * @return the shortest distance between the lines; - * if parallel, the common perpendicular distance between the lines; - * if coincidental, this distance will be 0 - */ - def apply(line1: Line3D, line2: Line3D): Float = { - val cross = Vector3.CrossProduct(line1.d, line2.d) - if(cross != Vector3.Zero) { - math.abs( - Vector3.DotProduct(cross, Vector3(line1.p.x - line2.p.x, line1.p.y - line2.p.y, line1.p.z - line2.p.z)) - ) / Vector3.Magnitude(cross) - } else { - // lines are parallel or coincidental - // construct a right triangle with one leg on line1 and the hypotenuse between the line's known points - val hypotenuse = Vector3(line2.p.x - line1.p.x, line2.p.y - line1.p.y, line2.p.z - line1.p.z) - val legOnLine1 = line1.d * Vector3.DotProduct(hypotenuse, line1.d) - Vector3.Magnitude(hypotenuse - legOnLine1) - } - } - - def apply(seg1: Segment3D, seg2: Segment3D): Float = { - //TODO make not as expensive as finding the plotted closest distance segment - Segment(seg1, seg2) match { - case Some(seg) => seg.length - case None => Float.MaxValue - } - } - - def apply(s1: Sphere, s2 : Sphere): Float = { - math.max(0, Vector3.Magnitude(Vector3(s1.p.x - s2.p.x, s1.p.y - s2.p.y, s1.p.z - s2.p.z)) - s1.radius - s2.radius) - } - } - - object Segment { - /** - * na - * @param c1 na - * @param c2 na - * @return a line segment that represents the closest distance between the circle's circumferences; - * `None`, if the circles have no distance between them (overlapping) - */ - def apply(c1 : Circle, c2 : Circle): Option[Segment2D] = { - val distance = Distance(c1, c2) - if (distance > 0) { - val c1x = c1.p.x - val c1y = c1.p.y - val v = Vector3.Unit(Vector3(c2.p.x - c1x, c2.p.y - c1y, 0f)) - val c1d = v * c1.radius - val c2d = v * c2.radius - Some( - Segment2D( - c1x + c1d.x, c1y + c1d.y, - c1x + c2d.x, c1y + c2d.y, - ) - ) - } else { - None - } - } - - /** - * na - * @param line1 na - * @param line2 na - * @return a line segment representing the closest distance between the two not intersecting lines; - * in the case of parallel lines, one of infinite closest distances is plotted; - * `None`, if the lines intersect with each other - */ - def apply(line1 : Line3D, line2 : Line3D): Option[Segment3D] = { - val p1 = Vector3(line1.p.x, line1.p.y, line1.p.z) - val p3 = Vector3(line2.p.x, line2.p.y, line2.p.z) - val p13 = p1 - p3 // vector between point on first line and point on second line - val p43 = line2.d - val p21 = line1.d - if (Vector3.MagnitudeSquared(p43) < Float.MinPositiveValue || - Vector3.MagnitudeSquared(p21) < Float.MinPositiveValue) { - None - } else { - val d2121 = Vector3.MagnitudeSquared(p21) - val d4343 = Vector3.MagnitudeSquared(p43) - val d4321 = Vector3.DotProduct(p43, p21) - val denom = d2121 * d4343 - d4321 * d4321 // n where d = (m/n) and a(x,y,z) + d * V = b(x,y,z) for line1 - if (math.abs(denom) < Float.MinPositiveValue) { - // without a denominator, we have no cross product solution - val p13u = Vector3.Unit(p13) - if (p21 == p13u || p21 == Vector3.neg(p13u)) { //coincidental lines overlap / intersect - None - } else { //parallel lines - val connecting = Vector3(line2.p.x - line1.p.x, line2.p.y - line1.p.y, line2.p.z - line1.p.z) - val legOnLine1 = line1.d * Vector3.DotProduct(connecting, line1.d) - val v = connecting - legOnLine1 - Some(Segment3D( - line1.p.x, line1.p.y, line1.p.z, - line1.p.x + v.x, line1.p.y + v.y, line1.p.z + v.z - )) - } - } else { - val d1343 = Vector3.DotProduct(p13, p43) - val numer = d1343 * d4321 -d4343 * Vector3.DotProduct(p13, p21) // m where d = (m/n) and ..., etc. - val mua = numer / denom - val mub = (d1343 + d4321 * mua) / d4343 - Some(Segment3D( - p1.x + mua * p21.x, - p1.y + mua * p21.y, - p1.z + mua * p21.z, - p3.x + mub * p43.x, - p3.y + mub * p43.y, - p3.z + mub * p43.z - )) - } - } - } - - def apply(line1 : Segment3D, line2 : Segment3D): Option[Segment3D] = { - val uline1 = Vector3.Unit(line1.d) - val uline2 = Vector3.Unit(line2.d) - apply(Line3D(line1.p1.x, line1.p1.y, line1.p1.z, uline1), Line3D(line2.p1.x, line2.p1.y, line2.p1.z, uline2)) match { - case Some(seg: Segment3D) => // common skew lines and parallel lines - val sega = Vector3(seg.p1.x, seg.p1.y, seg.p1.z) - val p1 = Vector3(line1.p1.x, line1.p1.y, line1.p1.z) - val d1 = sega - p1 - val out1 = if (!Geometry.equalVectors(Vector3.Unit(d1), uline1)) { //clamp seg.a(xyz) to segment line1's bounds - p1 - } else if (Vector3.MagnitudeSquared(d1) > Vector3.MagnitudeSquared(line1.d)) { - Vector3(line1.p2.x, line1.p2.y, line1.p2.z) - } else { - sega - } - val segb = Vector3(seg.p2.x, seg.p2.y, seg.p2.z) - val p2 = Vector3(line2.p1.x, line2.p1.y, line2.p1.z) - val d2 = segb - p2 - val out2 = if (!Geometry.equalVectors(Vector3.Unit(d2), uline2)) { //clamp seg.b(xyz) to segment line2's bounds - p2 - } else if (Vector3.MagnitudeSquared(d2) > Vector3.MagnitudeSquared(line2.d)) { - Vector3(line2.p2.x, line2.p2.y, line2.p2.z) - } else { - segb - } - Some(Segment3D( - out1.x, out1.y, out1.z, - out2.x, out2.y, out2.z - )) - case None => - val connectingU = Vector3.Unit(Vector3(line2.p1.x - line1.p1.x, line2.p1.y - line1.p1.y, line2.p1.z - line1.p1.z)) - if (uline1 == connectingU || uline1 == Vector3.neg(connectingU)) { // coincidental line segments - val line1a = Vector3(line1.p1.x, line1.p1.y, line1.p1.z) - val line1b = Vector3(line1.p2.x, line1.p2.y, line1.p2.z) - val line2a = Vector3(line2.p1.x, line2.p1.y, line2.p1.z) - val line2b = Vector3(line2.p2.x, line2.p2.y, line2.p2.z) - if (Vector3.Unit(line2a - line1a) != Vector3.Unit(line2b - line1a) || - Vector3.Unit(line2a - line1b) != Vector3.Unit(line2b - line1b) || - Vector3.Unit(line1a - line2a) != Vector3.Unit(line1b - line2a) || - Vector3.Unit(line1a - line2b) != Vector3.Unit(line1b - line2b)) { - Some(Segment3D( - line1.p1.x, line1.p1.y, line1a.z, - line1.p1.x, line1.p1.y, line1a.z - )) // overlap regions - } - else { - val segs = List((line1a, line2a), (line1a, line2b), (line2a, line1b)) - val (a, b) = segs({ - //val dist = segs.map { case (_a, _b) => Vector3.DistanceSquared(_a, _b) } - //dist.indexOf(dist.min) - var index = 0 - var minDist = Vector3.DistanceSquared(segs.head._1, segs.head._2) - (1 to 2).foreach { i => - val dist = Vector3.DistanceSquared(segs(i)._1, segs(i)._2) - if (minDist < dist) { - index = i - minDist = dist - } - } - index - }) - Some(Segment3D(a.x, a.y, a.z, b.x, b.y, b.z)) // connecting across the smallest gap - } - } else { - None - } - } - } - - /** - * na - * @param s1 na - * @param s2 na - * @return a line segment that represents the closest distance between the sphere's surface areas; - * `None`, if the spheres have no distance between them (overlapping) - */ - def apply(s1 : Sphere, s2 : Sphere): Option[Segment3D] = { - val distance = Distance(s1, s2) - if (distance > 0) { - val s1x = s1.p.x - val s1y = s1.p.y - val s1z = s1.p.z - val v = Vector3.Unit(Vector3(s2.p.x - s1x, s2.p.y - s1y, s2.p.z - s1z)) - val s1d = v * s1.radius - val s2d = v * (s1.radius + distance) - Some(Segment3D(s1x + s1d.x, s1y + s1d.y, s1y + s1d.y, s1x + s2d.x, s1y + s2d.y, s1y + s2d.y)) - } else { - None - } - } - - def apply(line : Line3D, sphere : Sphere): Option[Segment3D] = { - val sphereAsPoint = Vector3(sphere.p.x, sphere.p.y, sphere.p.z) - val lineAsPoint = Vector3(line.p.x, line.p.y, line.p.z) - val direct = sphereAsPoint - lineAsPoint - val projectionOfDirect = line.d * Vector3.DotProduct(direct, line.d) - val heightFromProjection = projectionOfDirect - direct - val heightFromProjectionDist = Vector3.Magnitude(heightFromProjection) - if (heightFromProjectionDist <= sphere.radius) { //intersection - None - } else { - val pointOnLine = lineAsPoint + projectionOfDirect - val pointOnSphere = pointOnLine + - Vector3.Unit(heightFromProjection) * (heightFromProjectionDist - sphere.radius) - Some(Segment3D( - pointOnLine.x, pointOnLine.y, pointOnLine.z, - pointOnSphere.x, pointOnSphere.y, pointOnSphere.z - )) - } - } - } -} diff --git a/src/main/scala/net/psforever/objects/geometry/Geometry.scala b/src/main/scala/net/psforever/objects/geometry/Geometry.scala index 3cd47c061..70ac40ec0 100644 --- a/src/main/scala/net/psforever/objects/geometry/Geometry.scala +++ b/src/main/scala/net/psforever/objects/geometry/Geometry.scala @@ -3,229 +3,6 @@ package net.psforever.objects.geometry import net.psforever.types.Vector3 -trait PrimitiveGeometry { - def center: Point - - def pointOnOutside(line: Line) : Point = pointOnOutside(line.d) - - def pointOnOutside(v: Vector3) : Point -} - -trait Geometry2D extends PrimitiveGeometry { - def center: Point2D - - def pointOnOutside(v: Vector3): Point2D = center -} - -trait Geometry3D extends PrimitiveGeometry { - def center: Point3D - - def pointOnOutside(v: Vector3): Point3D = center -} - -trait Point { - def asVector3: Vector3 -} - -trait Slope { - def d: Vector3 - - def length: Float -} - -trait Line extends Slope { - assert({ - val mag = Vector3.Magnitude(d) - mag - 0.05f < 1f && mag + 0.05f > 1f - }, "not a unit vector") - - def p: Point - - def length: Float = Float.PositiveInfinity -} - -trait Segment extends Slope { - def p1: Point - - def p2: Point - - def length: Float = Vector3.Magnitude(d) - - def asLine: PrimitiveGeometry -} - -final case class Point2D(x: Float, y: Float) extends Geometry2D with Point { - def center: Point2D = this - - def asVector3: Vector3 = Vector3(x, y, 0) -} - -object Point2D { - def apply(): Point2D = Point2D(0, 0) - - def apply(v: Vector3): Point2D = Point2D(v.x, v.y) -} - -final case class Ray2D(p: Point2D, d: Vector3) extends Geometry2D with Line { - def center: Point2D = p -} - -object Ray2D { - def apply(x: Float, y: Float, d: Vector3): Ray2D = Ray2D(Point2D(x, y), d) -} - -final case class Line2D(p: Point2D, d: Vector3) extends Geometry2D with Line { - def center: Point2D = p -} - -object Line2D { - def apply(ax: Float, ay: Float, d: Vector3): Line2D = { - Line2D(Point2D(ax, ay), d) - } - - def apply(ax: Float, ay: Float, bx: Float, by: Float): Line2D = { - Line2D(Point2D(ax, ay), Vector3.Unit(Vector3(bx-ax, by-ay, 0))) - } - - def apply(p1: Point2D, p2: Point2D): Line2D = { - Line2D(p1, Vector3.Unit(Vector3(p2.x-p1.x, p2.y-p1.y, 0))) - } -} - -final case class Segment2D(p1: Point2D, p2: Point2D) extends Geometry2D with Segment { - def center: Point2D = Point2D(d * 0.5f) - - def d: Vector3 = p2.asVector3 - p1.asVector3 - - def asLine: Line2D = Line2D(p1, Vector3.Unit(d)) -} - -object Segment2D { - def apply(ax: Float, ay: Float, bx: Float, by: Float): Segment2D = { - Segment2D(Point2D(ax, ay), Point2D(bx, by)) - } - - def apply(x: Float, y: Float, d: Vector3): Segment2D = { - Segment2D(x, y, x + d.x, y + d.y) - } -} - -final case class Circle(p: Point2D, radius: Float) extends Geometry2D { - def center : Point2D = p - - override def pointOnOutside(v: Vector3) : Point2D = { - val slope = Vector3.Unit(v) - val pointOnRim = p.asVector3 + slope * radius - Point2D(pointOnRim.x, pointOnRim.y) - } -} - -object Circle { - def apply(radius: Float): Circle = Circle(Point2D(), radius) - - def apply(x: Float, y: Float, radius: Float): Circle = Circle(Point2D(x, y), radius) -} - - -final case class Point3D(x: Float, y: Float, z: Float) extends Geometry3D with Point { - def center: Point3D = this - - def asVector3: Vector3 = Vector3(x, y, z) -} - -object Point3D { - def apply(): Point3D = Point3D(0,0,0) - - def apply(v: Vector3): Point3D = Point3D(v.x, v.y, v.z) -} - -final case class Ray3D(p: Point3D, d: Vector3) extends Geometry3D with Line { - def center: Point3D = p -} - -object Ray3D { - def apply(x: Float, y: Float, z: Float, d: Vector3): Ray3D = Ray3D(Point3D(x,y,z), d) -} - -final case class Line3D(p: Point3D, d: Vector3) extends Geometry3D with Line { - def center: Point3D = p -} - -object Line3D { - def apply(x: Float, y: Float, z: Float, d: Vector3): Line3D = { - Line3D(Point3D(x,y,z), d) - } - - def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Line3D = { - Line3D(Point3D(ax, ay, az), Vector3.Unit(Vector3(bx-ax, by-ay, bz-az))) - } - - def apply(p1: Point3D, p2: Point3D): Line3D = { - Line3D(p1, Vector3.Unit(Vector3(p2.x-p1.x, p2.y-p1.y, p2.z-p1.z))) - } -} - -final case class Segment3D(p1: Point3D, p2: Point3D) extends Geometry3D with Segment { - def center: Point3D = Point3D(d * 0.5f) - - def d: Vector3 = p2.asVector3 - p1.asVector3 - - def asLine: Line3D = Line3D(p1, Vector3.Unit(d)) -} - -object Segment3D { - def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Segment3D = { - Segment3D(Point3D(ax, ay, az), Point3D(bx, by, bz)) - } - - def apply(x: Float, y: Float, z: Float, d: Vector3): Segment3D = { - Segment3D(Point3D(x, y, z), Point3D(x + d.x, y + d.y, z + d.z)) - } -} - -final case class Sphere(p: Point3D, radius: Float) extends Geometry3D { - def center: Point3D = p - - override def pointOnOutside(v: Vector3): Point3D = { - val slope = Vector3.Unit(v) - val mult = radius / math.sqrt(slope.x * slope.x + slope.y * slope.y + slope.z * slope.z) - val pointOnSurface = center.asVector3 + slope * mult.toFloat - Point3D(pointOnSurface.x, pointOnSurface.y, pointOnSurface.z) - } -} - -object Sphere { - def apply(radius: Float): Sphere = Sphere(Point3D(), radius) - - def apply(x: Float, y: Float, z: Float, radius: Float): Sphere = Sphere(Point3D(x,y,z), radius) - - def apply(v: Vector3, radius: Float): Sphere = Sphere(Point3D(v), radius) -} - -final case class Cylinder(circle: Circle, z: Float, height: Float) extends Geometry3D { - def center: Point3D = Point3D(circle.p.x, circle.p.y, z + height * 0.5f) - - override def pointOnOutside(v: Vector3): Point3D = { - val centerVector = center.asVector3 - val slope = Vector3.Unit(v) - val mult = circle.radius / math.sqrt(slope.x * slope.x + slope.y * slope.y) - val pointOnRim = centerVector + slope * mult.toFloat - val point = if (z >= pointOnRim.z && pointOnRim.z <= height) { //side - pointOnRim - } else { //top or base - val rise = height * 0.5f / slope.z - centerVector + slope * rise - } - Point3D(point.x, point.y, point.z) - } -} - -object Cylinder { - def apply(x: Float, y: Float, z: Float, radius: Float, height: Float): Cylinder = { - Cylinder(Circle(x, y, radius), z, height) - } -} - object Geometry { def equalFloats(value1: Float, value2: Float, off: Float = 0.001f): Boolean = { val diff = value1 - value2 diff --git a/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala index 3b053916e..8614163e7 100644 --- a/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala +++ b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala @@ -3,15 +3,13 @@ package net.psforever.objects.geometry import net.psforever.objects.ballistics.{PlayerSource, SourceEntry} import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player} -import net.psforever.types.ExoSuitType +import net.psforever.types.{ExoSuitType, Vector3} object GeometryForm { /** this point can not be used for purposes of geometric representation */ lazy val invalidPoint: Point3D = Point3D(Float.MinValue, Float.MinValue, Float.MinValue) - /** this circle can not be used for purposes of geometric representation */ - lazy val invalidCircle: Circle = Circle(Point2D(invalidPoint.asVector3), 0) /** this cylinder can not be used for purposes of geometric representation */ - lazy val invalidCylinder: Cylinder = Cylinder(invalidCircle, Float.MinValue, 0) + lazy val invalidCylinder: Cylinder = Cylinder(invalidPoint.asVector3, Vector3.Zero, Float.MinValue, 0) /** * The geometric representation is the entity's centroid. @@ -27,21 +25,43 @@ object GeometryForm { } /** - * The geometric representation is the a sphere around the entity's centroid. + * The geometric representation is a sphere around the entity's centroid + * positioned following the axis of rotation (the entity's base). * @param radius how wide a hemisphere is * @param o the entity * @return the representation */ def representBySphere(radius: Float)(o: Any): Geometry3D = { o match { - case p: PlanetSideGameObject => Sphere(p.Position, radius) - case s: SourceEntry => Sphere(s.Position, radius) - case _ => Sphere(invalidPoint, radius) + case p: PlanetSideGameObject => + Sphere(p.Position, radius) + case s: SourceEntry => + Sphere(s.Position, radius) + case _ => + Sphere(invalidPoint, radius) } } /** - * The geometric representation is the a cylinder around the entity's base. + * The geometric representation is a sphere around the entity's centroid + * positioned following the axis of rotation (the entity's base). + * @param radius how wide a hemisphere is + * @param o the entity + * @return the representation + */ + def representByRaisedSphere(radius: Float)(o: Any): Geometry3D = { + o match { + case p: PlanetSideGameObject => + Sphere(p.Position + Vector3.relativeUp(p.Orientation) * radius, radius) + case s: SourceEntry => + Sphere(s.Position + Vector3.relativeUp(s.Orientation) * radius, radius) + case _ => + Sphere(invalidPoint, radius) + } + } + + /** + * The geometric representation is a cylinder around the entity's base. * @param radius half the distance across * @param height how tall the cylinder is (the distance of the top to the base) * @param o the entity @@ -49,14 +69,14 @@ object GeometryForm { */ def representByCylinder(radius: Float, height: Float)(o: Any): Geometry3D = { o match { - case p: PlanetSideGameObject => Cylinder(Circle(p.Position.x, p.Position.y, radius), p.Position.z, height) - case s: SourceEntry => Cylinder(Circle(s.Position.x, s.Position.y, radius), s.Position.z, height) + case p: PlanetSideGameObject => Cylinder(p.Position, Vector3.relativeUp(p.Orientation), radius, height) + case s: SourceEntry => Cylinder(s.Position, Vector3.relativeUp(s.Orientation), radius, height) case _ => invalidCylinder } } /** - * The geometric representation is the a cylinder around the entity's base + * The geometric representation is a cylinder around the entity's base * if the target represents a player entity. * @param radius a measure of the player's bulk * @param o the entity @@ -67,16 +87,17 @@ object GeometryForm { case p: Player => val radialOffset = if(p.ExoSuit == ExoSuitType.MAX) 0.25f else 0f Cylinder( - Circle(p.Position.x, p.Position.y, radius + radialOffset), - p.Position.z, + p.Position, + radius + radialOffset, GlobalDefinitions.MaxDepth(p) ) case p: PlayerSource => - val radialOffset = if(p.ExoSuit == ExoSuitType.MAX) 0.25f else 0f + val radialOffset = if(p.ExoSuit == ExoSuitType.MAX) 0.125f else 0f + val heightOffset = if(p.crouching) 1.093750f else GlobalDefinitions.avatar.MaxDepth Cylinder( - Circle(p.Position.x, p.Position.y, radius + radialOffset), - p.Position.z, - GlobalDefinitions.avatar.MaxDepth + p.Position, + radius + radialOffset, + heightOffset ) case _ => invalidCylinder @@ -84,7 +105,7 @@ object GeometryForm { } /** - * The geometric representation is the a cylinder around the entity's base + * The geometric representation is a cylinder around the entity's base * as if the target is displaced from the ground at an expected (fixed?) distance. * @param radius half the distance across * @param height how tall the cylinder is (the distance of the top to the base) @@ -95,17 +116,9 @@ object GeometryForm { def representHoveringEntityByCylinder(radius: Float, height: Float, hoversAt: Float)(o: Any): Geometry3D = { o match { case p: PlanetSideGameObject => - Cylinder( - Circle(p.Position.x, p.Position.y, radius), - p.Position.z + hoversAt, - height - ) + Cylinder(p.Position, Vector3.relativeUp(p.Orientation), radius, height) case s: SourceEntry => - Cylinder( - Circle(s.Position.x, s.Position.y, radius), - s.Position.z + hoversAt, - height - ) + Cylinder(s.Position, Vector3.relativeUp(s.Orientation), radius, height) case _ => invalidCylinder } diff --git a/src/main/scala/net/psforever/objects/geometry/Intersection.scala b/src/main/scala/net/psforever/objects/geometry/Intersection.scala deleted file mode 100644 index cda1f31c9..000000000 --- a/src/main/scala/net/psforever/objects/geometry/Intersection.scala +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) 2021 PSForever -package net.psforever.objects.geometry - -import net.psforever.types.Vector3 - -object Intersection { - object Test { - /** - * Do these two lines intersect? - * Lines in 2D space will always intersect unless they are parallel or antiparallel. - * In that case, however, they can still "intersect" if provided that the lines are coincidental. - */ - def apply(line1: Line2D, line2: Line2D): Boolean = { - line1.d != line2.d || { - //parallel or antiparallel? - val u = Vector3.Unit(Vector3(line2.p.x - line1.p.x, line2.p.y - line1.p.y, 0)) - u == Vector3.Zero || line1.d == u || line1.d == Vector3.neg(u) - } - } - - private def pointOnSegment(ax: Float, ay: Float, px: Float, py: Float, bx: Float, by: Float): Boolean = { - px <= math.max(ax, bx) && px >= math.min(ax, bx) && py <= math.max(ay, by) && py >= math.min(ay, by) - } - - object PointTripleOrientation extends Enumeration { - val Colinear, Clockwise, Counterclockwise = Value - } - - /** - * Determine the orientation of the given three two-dimensional points. - * Any triple has one of three orientations: - * clockwise - the third point is to the right side of a line plotted by the first two points; - * counterclockwise - the third point is to the left side of a line plotted by the first two points; - * and, colinear - the third point is reachable along the line plotted by the first two points. - * @param ax x-coordinate of the first point - * @param ay y-coordinate of the first point - * @param px x-coordinate of the second point - * @param py y-coordinate of the second point - * @param bx x-coordinate of the third point - * @param by y-coordinate of the third point - * @return the orientation value - */ - private def orientationOfPoints( - ax: Float, ay: Float, - px: Float, py: Float, - bx: Float, by: Float - ): PointTripleOrientation.Value = { - val out = (py - ay) * (bx - px) - (px - ax) * (by - py) - if (out == 0) PointTripleOrientation.Colinear - else if (out > 0) PointTripleOrientation.Clockwise - else PointTripleOrientation.Counterclockwise - } - - /** - * Do these two line segments intersect? - * Intersection of two two-dimensional line segments can be determined by the orientation of their endpoints. - * If a test of multiple ordered triple points reveals that certain triples have different orientations, - * then we can safely assume the intersection state of the segments. - */ - def apply(line1: Segment2D, line2: Segment2D): Boolean = { - //setup - val ln1ax = line1.p1.x - val ln1ay = line1.p1.y - val ln1bx = line1.p2.x - val ln1by = line1.p2.y - val ln2ax = line2.p1.x - val ln2ay = line2.p1.y - val ln2bx = line2.p2.x - val ln2by = line2.p2.y - val ln1_ln2a = orientationOfPoints(ln1ax, ln1ay, ln1bx, ln1by, ln2ax, ln2ay) - val ln1_ln2b = orientationOfPoints(ln1ax, ln1ay, ln1bx, ln1by, ln2bx, ln2by) - val ln2_ln1a = orientationOfPoints(ln2ax, ln2ay, ln2bx, ln2by, ln1ax, ln1ay) - val ln2_ln1b = orientationOfPoints(ln2ax, ln2ay, ln2bx, ln2by, ln1bx, ln1by) - //results - import PointTripleOrientation._ - (ln1_ln2a != ln1_ln2b && ln2_ln1a != ln2_ln1b) || - (ln1_ln2a == Colinear && pointOnSegment(ln1ax, ln1ay, ln2ax, ln2ay, ln1bx, ln1by)) || // line2 A is on line1 - (ln1_ln2b == Colinear && pointOnSegment(ln1ax, ln1ay, ln2bx, ln2by, ln1bx, ln1by)) || // line2 B is on line1 - (ln2_ln1a == Colinear && pointOnSegment(ln2ax, ln2ay, ln1ax, ln1ay, ln2bx, ln2by)) || // line1 A is on line2 - (ln2_ln1b == Colinear && pointOnSegment(ln2ax, ln2ay, ln1bx, ln1by, ln2bx, ln2by)) // line1 B is on line2 - } - - /** - * Do these two lines intersect? - * Actual mathematically-sound intersection between lines and line segments in 3D-space is terribly uncommon. - * Instead, check that the closest distance between two line segments is below a threshold value. - */ - def apply(line1: Line3D, line2: Line3D): Boolean = { - apply(line1, line2, 0.15f) - } - def apply(line1: Line3D, line2: Line3D, threshold: Float): Boolean = { - Closest.Distance(line1, line2) < threshold - } - - def apply(c1: Circle, c2 : Circle): Boolean = { - Vector3.Magnitude(Vector3(c1.p.x - c2.p.x, c1.p.y - c2.p.y, 0)) <= c1.radius + c2.radius - } - - /** - * Do these two line segments intersect? - * Actual mathematically-sound intersection between lines and line segments in 3D-space is terribly uncommon. - * Instead, check that the closest distance between two line segments is below a threshold value. - */ - def apply(seg1: Segment3D, seg2: Segment3D): Boolean = { - apply(seg1, seg2, 0.15f) - } - def apply(seg1: Segment3D, seg2: Segment3D, threshold: Float): Boolean = { - Closest.Distance(seg1, seg2) < threshold - } - - def apply(s1: Sphere, s2 : Sphere): Boolean = { - Vector3.Magnitude( - Vector3( - s1.p.x - s2.p.x, - s1.p.y - s2.p.y, - s1.p.z - s2.p.z - ) - ) <= s1.radius + s2.radius - } - - def apply(c1: Cylinder, c2: Cylinder): Boolean = { - apply(c1.circle, c2.circle) && - ((c1.height >= c2.z && c1.z <= c2.height) || (c2.height >= c1.z && c2.z <= c1.height)) - } - - def apply(cylinder: Cylinder, sphere: Sphere): Boolean = { - val cylinderCircle = cylinder.circle - val cylinderCircleRadius = cylinderCircle.radius - val cylinderTop = cylinder.z + cylinder.height - val sphereRadius = sphere.radius - val sphereBase = sphere.p.z - sphereRadius - val sphereTop = sphere.p.z + sphereRadius - if (apply(cylinderCircle, Circle(sphere.p.x, sphere.p.y, sphereRadius)) && - ((sphereTop >= cylinder.z && sphereBase <= cylinderTop) || - (cylinderTop >= sphereBase && cylinder.z <= sphereTop))) { - // potential intersection ... - val sphereAsPoint = Vector3(sphere.p.x, sphere.p.y, sphere.p.z) - val cylinderAsPoint = Vector3(cylinderCircle.p.x, cylinderCircle.p.y, cylinder.z) - val segmentFromCylinderToSphere = sphereAsPoint - cylinderAsPoint - val segmentFromCylinderToSphereXY = segmentFromCylinderToSphere.xy - if ((cylinder.z <= sphere.p.z && sphere.p.z <= cylinderTop) || - Vector3.MagnitudeSquared(segmentFromCylinderToSphereXY) <= cylinderCircleRadius * cylinderCircleRadius) { - true // top or bottom of sphere, or widest part of the sphere, must interact with the cylinder - } else { - // only option left is the curves of the sphere interacting with the cylinder's rim, top or base - val directionFromCylinderToSphere = Vector3.Unit(segmentFromCylinderToSphereXY) - val pointOnCylinderRimBase = cylinderAsPoint + directionFromCylinderToSphere * cylinderCircleRadius - val pointOnCylinderRimTop = pointOnCylinderRimBase + Vector3.z(cylinder.height) - val sqSphereRadius = sphereRadius * sphereRadius - Vector3.DistanceSquared(sphereAsPoint, pointOnCylinderRimTop) <= sqSphereRadius || - Vector3.DistanceSquared(sphereAsPoint, pointOnCylinderRimBase) <= sqSphereRadius - } - } else { - false - } - } - } -} diff --git a/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala b/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala new file mode 100644 index 000000000..5aef779f4 --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala @@ -0,0 +1,165 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.geometry + +import net.psforever.types.Vector3 + +trait PrimitiveGeometry { + def center: Point + + def pointOnOutside(line: Line) : Point = pointOnOutside(line.d) + + def pointOnOutside(v: Vector3) : Point +} + +//trait Geometry2D extends PrimitiveGeometry { +// def center: Point2D +// +// def pointOnOutside(v: Vector3): Point2D = center +//} + +trait Geometry3D extends PrimitiveGeometry { + def center: Point3D + + def pointOnOutside(v: Vector3): Point3D = center +} + +trait Point { + def asVector3: Vector3 +} + +trait Slope { + def d: Vector3 + + def length: Float +} + +trait Line extends Slope { + assert({ + val mag = Vector3.Magnitude(d) + mag - 0.05f < 1f && mag + 0.05f > 1f + }, "not a unit vector") + + def p: Point + + def length: Float = Float.PositiveInfinity +} + +trait Segment extends Slope { + def p1: Point + + def p2: Point + + def length: Float = Vector3.Magnitude(d) + + def asLine: PrimitiveGeometry +} + +final case class Point3D(x: Float, y: Float, z: Float) extends Geometry3D with Point { + def center: Point3D = this + + def asVector3: Vector3 = Vector3(x, y, z) +} + +object Point3D { + def apply(): Point3D = Point3D(0,0,0) + + def apply(v: Vector3): Point3D = Point3D(v.x, v.y, v.z) +} + +final case class Ray3D(p: Point3D, d: Vector3) extends Geometry3D with Line { + def center: Point3D = p +} + +object Ray3D { + def apply(x: Float, y: Float, z: Float, d: Vector3): Ray3D = Ray3D(Point3D(x,y,z), d) +} + +final case class Line3D(p: Point3D, d: Vector3) extends Geometry3D with Line { + def center: Point3D = p +} + +object Line3D { + def apply(x: Float, y: Float, z: Float, d: Vector3): Line3D = { + Line3D(Point3D(x,y,z), d) + } + + def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Line3D = { + Line3D(Point3D(ax, ay, az), Vector3.Unit(Vector3(bx-ax, by-ay, bz-az))) + } + + def apply(p1: Point3D, p2: Point3D): Line3D = { + Line3D(p1, Vector3.Unit(Vector3(p2.x-p1.x, p2.y-p1.y, p2.z-p1.z))) + } +} + +final case class Segment3D(p1: Point3D, p2: Point3D) extends Geometry3D with Segment { + def center: Point3D = Point3D(d * 0.5f) + + def d: Vector3 = p2.asVector3 - p1.asVector3 + + def asLine: Line3D = Line3D(p1, Vector3.Unit(d)) +} + +object Segment3D { + def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Segment3D = { + Segment3D(Point3D(ax, ay, az), Point3D(bx, by, bz)) + } + + def apply(x: Float, y: Float, z: Float, d: Vector3): Segment3D = { + Segment3D(Point3D(x, y, z), Point3D(x + d.x, y + d.y, z + d.z)) + } +} + +final case class Sphere(p: Point3D, radius: Float) extends Geometry3D { + def center: Point3D = p + + override def pointOnOutside(v: Vector3): Point3D = { + val slope = Vector3.Unit(v) + val mult = radius / math.sqrt(slope.x * slope.x + slope.y * slope.y + slope.z * slope.z) + val pointOnSurface = center.asVector3 + slope * mult.toFloat + Point3D(pointOnSurface.x, pointOnSurface.y, pointOnSurface.z) + } +} + +object Sphere { + def apply(radius: Float): Sphere = Sphere(Point3D(), radius) + + def apply(x: Float, y: Float, z: Float, radius: Float): Sphere = Sphere(Point3D(x,y,z), radius) + + def apply(v: Vector3, radius: Float): Sphere = Sphere(Point3D(v), radius) +} + +final case class Cylinder(position: Vector3, relativeUp: Vector3, radius: Float, height: Float) extends Geometry3D { + def center: Point3D = Point3D(position + relativeUp * height * 0.5f) + + override def pointOnOutside(v: Vector3): Point3D = { + val centerVector = center.asVector3 + val slope = Vector3.Unit(v) + val acrossTopAndBase = slope - relativeUp + val pointOnSide = centerVector + slope * (radius / Vector3.Magnitude(acrossTopAndBase)) + val pointOnBase = position + acrossTopAndBase * radius + val pointOnTop = pointOnBase + relativeUp * height + val fromPointOnTopToSide = Vector3.Unit(pointOnTop - pointOnSide) + val fromPointOnSideToBase = Vector3.Unit(pointOnSide - pointOnBase) + val target = if(fromPointOnTopToSide == Vector3.Zero || + fromPointOnSideToBase == Vector3.Zero || + Geometry.equalVectors(fromPointOnTopToSide, fromPointOnSideToBase)) { + //on side, including top rim or base rim + pointOnSide + } else { + //on top or base + // the full equation would be 'centerVector + slope * (height * 0.5f / Vector3.Magnitude(relativeUp))' + // 'relativeUp` is already a unit vector (magnitude of 1) + centerVector + slope * height * 0.5f + } + Point3D(target) + } +} + +object Cylinder { + def apply(v: Vector3, radius: Float, height: Float): Cylinder = Cylinder(v, Vector3(0,0,1), radius, height) + + def apply(p: Point3D, radius: Float, height: Float): Cylinder = Cylinder(p.asVector3, Vector3(0,0,1), radius, height) + + def apply(p: Point3D, v: Vector3, radius: Float, height: Float): Cylinder = Cylinder(p.asVector3, v, radius, height) +} diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index b61a414f8..66d2e1b3e 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -1135,7 +1135,7 @@ object Zone { .flatMap { _.Amenities } .filter { _.Definition.Damageable } } - //restrict to targets in the damage radius + //restrict to targets according to the detection plan val allAffectedTargets = (playerTargets ++ vehicleTargets ++ complexDeployableTargets ++ soiTargets) .filter { target => (target ne obj) && detectionTest(obj, target, radius) @@ -1177,11 +1177,11 @@ object Zone { * A default function literal mainly used for `causesExplosion`. * @see `causeExplosion` * @see `ObjectDefinition.Geometry` - * @param obj1 a game entity - * @param obj2 a game entity + * @param obj1 a game entity, should be the source of the explosion + * @param obj2 a game entity, should be the target of the explosion * @param maxDistance the square of the maximum distance permissible between game entities * before they are no longer considered "near" - * @return `true`, if the target entities are near to each other; + * @return `true`, if the target entities are near enough to each other; * `false`, otherwise */ def distanceCheck(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = { @@ -1193,7 +1193,7 @@ object Zone { * @param g2 the geometric representation of a game entity * @param maxDistance the square of the maximum distance permissible between game entities * before they are no longer considered "near" - * @return `true`, if the target entities are near to each other; + * @return `true`, if the target entities are near enough to each other; * `false`, otherwise */ def distanceCheck(g1: Geometry3D, g2: Geometry3D, maxDistance: Float): Boolean = { diff --git a/src/main/scala/net/psforever/types/Vector3.scala b/src/main/scala/net/psforever/types/Vector3.scala index e008c7a2a..76fecca4f 100644 --- a/src/main/scala/net/psforever/types/Vector3.scala +++ b/src/main/scala/net/psforever/types/Vector3.scala @@ -387,8 +387,8 @@ object Vector3 { /** * Given a `Vector3` element composed of Euler angles - * and a `Vector3` element in the direction of "up", - * find a standard unit vector that points in the direction of "up" after rotating by the Euler angles. + * and a `Vector3` element in the vector direction of "up", + * find a standard unit vector that points in the direction of the entity's "up" after rotating by the Euler angles. * Compass direction rules apply (North is 0 degrees, East is 90 degrees, etc.). * @see `Vector3.Rx(Float)` * @see `Vector3.Ry(Float)` @@ -398,7 +398,12 @@ object Vector3 { * @return a mathematical vector representing a relative "up" direction */ def relativeUp(orient: Vector3, up: Vector3): Vector3 = { - //TODO is the missing calculation before Rz(Rx(Ry(v, x), y), z) or after Rz(Ry(Rx(v, y), x), z)? - Rz(Rx(up, orient.y), (orient.z + 180) % 360f) + /* + rotate in Ry using the x-component and rotate in Rx using the y-component + only Rz is rotated using its corresponding component, and you add 180 clamping to 0-360 degrees + I'm sure mathematicians know what's going on here, but I don't + the purpose of this comment is to make certain that the future me knows that all this is not a mistake + */ + Rz(Rx(Ry(Unit(up), orient.x), orient.y), (orient.z + 180) % 360f) } } diff --git a/src/test/scala/Vector3Test.scala b/src/test/scala/Vector3Test.scala index ef39a2bbc..38cb36f96 100644 --- a/src/test/scala/Vector3Test.scala +++ b/src/test/scala/Vector3Test.scala @@ -257,18 +257,62 @@ class Vector3Test extends Specification { "find a relative up (y-rot)" in { Vector3.relativeUp(Vector3(0, 0, 0)) mustEqual Vector3(0,0,1) //up - Vector3.relativeUp(Vector3(0, 90, 0)) mustEqual Vector3(0,-1,0) //north + Vector3.relativeUp(Vector3(0, 90, 0)) mustEqual Vector3(0,-1,0) //south Vector3.relativeUp(Vector3(0, 180, 0)) mustEqual Vector3(0,0,-1) //down - Vector3.relativeUp(Vector3(0, 270, 0)) mustEqual Vector3(0,1,0) //south + Vector3.relativeUp(Vector3(0, 270, 0)) mustEqual Vector3(0,1,0) //north Vector3.relativeUp(Vector3(0, 360, 0)) mustEqual Vector3(0,0,1) //up } + "find a relative up (x-rot)" in { + Vector3.relativeUp(Vector3(0, 0, 0)) mustEqual Vector3(0,0,1) //up + Vector3.relativeUp(Vector3(90, 0, 0)) mustEqual Vector3(-1,0,0) //west + Vector3.relativeUp(Vector3(180, 0, 0)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(270, 0, 0)) mustEqual Vector3(1,0,0) //east + Vector3.relativeUp(Vector3(360, 0, 0)) mustEqual Vector3(0,0,1) //up + } + "find a relative up (combined y,z)" in { Vector3.relativeUp(Vector3(0, 0, 90)) mustEqual Vector3(0,0,1) //up Vector3.relativeUp(Vector3(0, 90, 90)) mustEqual Vector3(-1,0,0) //west Vector3.relativeUp(Vector3(0, 180, 90)) mustEqual Vector3(0,0,-1) //down Vector3.relativeUp(Vector3(0, 270, 90)) mustEqual Vector3(1,0,0) //east Vector3.relativeUp(Vector3(0, 360, 90)) mustEqual Vector3(0,0,1) //up + + Vector3.relativeUp(Vector3(0, 90, 180)) mustEqual Vector3(0,1,0) //north + Vector3.relativeUp(Vector3(0, 180, 180)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(0, 270, 180)) mustEqual Vector3(0,-1,0) //south + Vector3.relativeUp(Vector3(0, 360, 180)) mustEqual Vector3(0,0,1) //up + + Vector3.relativeUp(Vector3(0, 90, 270)) mustEqual Vector3(1,0,0) //east + Vector3.relativeUp(Vector3(0, 180, 270)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(0, 270, 270)) mustEqual Vector3(-1,0,0) //west + Vector3.relativeUp(Vector3(0, 360, 270)) mustEqual Vector3(0,0,1) //up + } + + "find a relative up (combined x,z)" in { + Vector3.relativeUp(Vector3(0, 0, 90)) mustEqual Vector3(0,0,1) //up + Vector3.relativeUp(Vector3(90, 0, 90)) mustEqual Vector3(0,-1,0) //south + Vector3.relativeUp(Vector3(180, 0, 90)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(270, 0, 90)) mustEqual Vector3(0,1,0) //north + Vector3.relativeUp(Vector3(360, 0, 90)) mustEqual Vector3(0,0,1) //up + + Vector3.relativeUp(Vector3(90, 0, 180)) mustEqual Vector3(1,0,0) //east + Vector3.relativeUp(Vector3(180, 0, 180)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(270, 0, 180)) mustEqual Vector3(-1,0,0) //west + Vector3.relativeUp(Vector3(360, 0, 180)) mustEqual Vector3(0,0,1) //up + + Vector3.relativeUp(Vector3(90, 0, 270)) mustEqual Vector3(0,1,0) //north + Vector3.relativeUp(Vector3(180, 0, 270)) mustEqual Vector3(0,0,-1) //down + Vector3.relativeUp(Vector3(270, 0, 270)) mustEqual Vector3(0,-1,0) //south + Vector3.relativeUp(Vector3(360, 0, 270)) mustEqual Vector3(0,0,1) //up + } + + "find a relative up (combined x,y)" in { + val south = Vector3(0,-1,0) + Vector3.relativeUp(Vector3(0, 90, 0)) mustEqual Vector3(0,-1,0) //south + Vector3.relativeUp(Vector3(90, 90, 0)) mustEqual Vector3(-1,0,0) //west + Vector3.relativeUp(Vector3(180, 90, 0)) mustEqual Vector3(0,1,0) //north + Vector3.relativeUp(Vector3(270, 90, 0)) mustEqual Vector3(1,0,0) //east } } } diff --git a/src/test/scala/objects/GeometryTest.scala b/src/test/scala/objects/GeometryTest.scala deleted file mode 100644 index ea782da97..000000000 --- a/src/test/scala/objects/GeometryTest.scala +++ /dev/null @@ -1,533 +0,0 @@ -// Copyright (c) 2020 PSForever -package objects - -import net.psforever.objects.geometry._ -import net.psforever.types.Vector3 -import org.specs2.mutable.Specification - -class IntersectionTest extends Specification { - "Line2D" should { - "detect intersection on target points(s)" in { - //these lines intersect at (0, 0) - val result = Intersection.Test( - Line2D(0,0, 1,0), - Line2D(0,0, 0,1) - ) - result mustEqual true - } - - "detect intersection on a target point" in { - //these lines intersect at (0, 0); start of segment 1, middle of segment 2 - val result = Intersection.Test( - Line2D( 0,0, 0,1), - Line2D(-1,0, 1,0) - ) - result mustEqual true - } - - "detect intersection anywhere else" in { - //these lines intersect at (0.5f, 0.5f) - val result = Intersection.Test( - Line2D(0,0, 1,1), - Line2D(1,0, 0,1) - ) - result mustEqual true - } - - "detect intersection anywhere else (2)" in { - //these lines intersect at (0, 0.5) - val result = Intersection.Test( - Line2D(0, 0, 1, 0), - Line2D(0.5f,1, 0.5f,-1) - ) - result mustEqual true - } - - "not detect intersection if the lines are parallel" in { - val result = Intersection.Test( - Line2D(0,0, 1,1), - Line2D(1,0, 2,1) - ) - result mustEqual false - } - - "detect intersection if the lines overlap" in { - //the lines are coincidental - val result = Intersection.Test( - Line2D(0,0, 1,1), - Line2D(1,1, 2,2) - ) - result mustEqual true - } - } - - "Segment2D" should { - "detect intersection on target points(s)" in { - //these line segments intersect at (0, 0) - val result = Intersection.Test( - Segment2D(0,0, 1,0), - Segment2D(0,0, 0,1) - ) - result mustEqual true - } - - "detect intersection on a target point" in { - //these line segments intersect at (0, 0); start of segment 1, middle of segment 2 - val result = Intersection.Test( - Segment2D( 0,0, 0,1), - Segment2D(-1,0, 1,0) - ) - result mustEqual true - } - - "detect intersection anywhere else" in { - //these line segments intersect at (0.5f, 0.5f) - val result = Intersection.Test( - Segment2D(0,0, 1,1), - Segment2D(1,0, 0,1) - ) - result mustEqual true - } - - "detect intersection anywhere else (2)" in { - //these line segments intersect at (0, 0.5) - val result = Intersection.Test( - Segment2D(0, 0, 1, 0), - Segment2D(0.5f,1, 0.5f,-1) - ) - result mustEqual true - } - - "not detect intersection if the lines are parallel" in { - val result = Intersection.Test( - Segment2D(0,0, 1,1), - Segment2D(1,0, 2,1) - ) - result mustEqual false - } - - "detect intersection if the lines overlap" in { - //the lines are coincidental - val result = Intersection.Test( - Line2D(0,0, 1,1), - Line2D(1,1, 2,2) - ) - result mustEqual true - } - } - - "Circle" should { - "intersect when overlapping (coincidental)" in { - val result = Intersection.Test( - Circle(0,0, 1), - Circle(0,0, 1) - ) - result mustEqual true - } - - "intersect when overlapping (engulfed)" in { - val result = Intersection.Test( - Circle(0,0, 2), - Circle(1,0, 1) - ) - result mustEqual true - } - - "intersect when overlapping (partial 1)" in { - val result = Intersection.Test( - Circle(0,0, 2), - Circle(2,0, 1) - ) - result mustEqual true - } - - "intersect when overlapping (partial 2)" in { - val result = Intersection.Test( - Circle(0, 0, 2), - Circle(2.5f,0, 1) - ) - result mustEqual true - } - - "intersect when the circumferences are touching" in { - val result = Intersection.Test( - Circle(0,0, 2), - Circle(3,0, 1) - ) - result mustEqual true - } - - "not intersect when not touching" in { - val result = Intersection.Test( - Circle(0,0, 2), - Circle(4,0, 1) - ) - result mustEqual false - } - } - - "Line3D" should { - "detect intersection on target point(s)" in { - //these lines intersect at (0, 0, 0) - val result = Intersection.Test( - Line3D(0,0,0, Vector3(1,0,0)), - Line3D(0,0,0, Vector3(0,1,0)) - ) - result mustEqual true - } - - "detect intersection on a target point" in { - //these lines intersect at (0, 0, 0); start of segment 1, middle of segment 2 - val result = Intersection.Test( - Line3D(0,0,0, Vector3(0,1,0)), - Line3D(-1,0,0, Vector3(1,0,0)) - ) - result mustEqual true - } - - "detect intersection anywhere else" in { - //these lines intersect at (0.5f, 0.5f, 0) - val result = Intersection.Test( - Line3D(0,0,0, Vector3.Unit(Vector3(1, 1, 0))), - Line3D(1,0,0, Vector3(0,1,0)) - ) - result mustEqual true - } - - "detect intersection anywhere else (2)" in { - //these lines intersect at (0, 0.5, 0) - val result = Intersection.Test( - Line3D(0,0,0, Vector3(1,0,0)), - Line3D(0.5f,1,0, Vector3.Unit(Vector3(0.5f,-1,0))) - ) - result mustEqual true - } - - "not detect intersection if the lines are parallel" in { - val result = Intersection.Test( - Line3D(0,0,0, Vector3.Unit(Vector3(1,1,1))), - Line3D(1,1,2, Vector3.Unit(Vector3(1,1,1))) - ) - result mustEqual false - } - - "detect intersection if the lines overlap" in { - //the sub-segment (1,0,0) to (2,0,0) is an overlap region shared between the two segments - val result = Intersection.Test( - Line3D(0,0,0, Vector3.Unit(Vector3(2,0,0))), - Line3D(1,0,0, Vector3.Unit(Vector3(3,0,0))) - ) - result mustEqual true - } - - "not detect intersection (generic skew)" in { - //these segments will not intersect - val result = Intersection.Test( - Segment3D(-3,-8,7, Vector3.Unit(Vector3(-3,-9,8))), - Segment3D(6,3,0, Vector3.Unit(Vector3(2,0,0))) - ) - result mustEqual false - } - } - - "Segment3D" should { - "detect intersection of the first point(s)" in { - //these segments intersect at (0, 0, 0) - val result = Intersection.Test( - Segment3D(0,0,0, 1,0,0), - Segment3D(0,0,0, 0,1,0) - ) - result mustEqual true - } - - "detect intersection of the first point" in { - //these segments intersect at (0, 0, 0); start of segment 1, middle of segment 2 - val result = Intersection.Test( - Segment3D(0,0,0, 0,2,0), - Segment3D(-1,0,0, 1,0,0) - ) - result mustEqual true - } - - "detect intersection on the farther point(s)" in { - //these segments intersect at (0, 1, 0) - val result = Intersection.Test( - Segment3D(0,0,1, 0,1,0), - Segment3D(1,0,0, 0,1,0) - ) - result mustEqual true - } - - "detect intersection on the farther point" in { - //these segments intersect at (1, 1, 0); end of segment 1, middle of segment 2 - val result = Intersection.Test( - Segment3D(1,0,0, 1,1,0), - Segment3D(2,0,0, 0,2,0) - ) - result mustEqual true - } - - "detect intersection in the middle(s)" in { - //these segments intersect at (0.5f, 0.5f, 0) - val result = Intersection.Test( - Segment3D(0,0,0, 1,1,0), - Segment3D(1,0,0, 0,1,0) - ) - result mustEqual true - } - - "detect intersection in the middle " in { - //these segments intersect at (0, 0.5, 0) - val result = Intersection.Test( - Segment3D(0,0,0, 1,0,0), - Segment3D(0.5f,1,0, 0.5f,-1,0) - ) - result mustEqual true - } - - "not detect intersection if the point of intersection would be before the start of the segments" in { - //these segments will not intersect as segments; but, as lines, they would intersect at (0, 0, 0) - val result = Intersection.Test( - Segment3D(1,1,0, 2,2,0), - Segment3D(1,0,0, 2,0,0) - ) - result mustEqual false - } - - "not detect intersection if the point of intersection would be after the end of the segments" in { - //these segments will not intersect as segments; but, as lines, they would intersect at (2, 2, 0) - val result = Intersection.Test( - Segment3D(0,0,0, 1,1,0), - Segment3D(2,0,0, 2,1,0) - ) - result mustEqual false - } - - "not detect intersection if the line segments are parallel" in { - val result = Intersection.Test( - Segment3D(0,0,0, 1,1,1), - Segment3D(1,1,2, 2,2,3) - ) - result mustEqual false - } - - "detect intersection with overlapping" in { - //the sub-segment (1,0,0) to (2,0,0) is an overlap region shared between the two segments - val result = Intersection.Test( - Segment3D(0,0,0, 2,0,0), - Segment3D(1,0,0, 3,0,0) - ) - result mustEqual true - } - - "not detect intersection with coincidental, non-overlapping" in { - //the sub-segment (1,0,0) to (2,0,0) is an overlap region shared between the two segments - val result = Intersection.Test( - Segment3D(0,0,0, 1,0,0), - Segment3D(2,0,0, 3,0,0) - ) - result mustEqual false - } - - "not detect intersection (generic skew)" in { - //these segments will not intersect - val result = Intersection.Test( - Segment3D(-3,-8,7, -3,-9,8), - Segment3D(6,3,0, 2,0,0) - ) - result mustEqual false - } - } - - "Sphere" should { - "intersect when overlapping (coincidental)" in { - val result = Intersection.Test( - Sphere(Vector3.Zero, 1), - Sphere(Vector3.Zero, 1) - ) - result mustEqual true - } - - "intersect when overlapping (engulfed)" in { - val result = Intersection.Test( - Sphere(Vector3.Zero, 5), - Sphere(Vector3(1,0,0), 1) - ) - result mustEqual true - } - - "intersect when overlapping (partial 1)" in { - val result = Intersection.Test( - Sphere(Vector3.Zero, 2), - Sphere(Vector3(2,0,0), 1) - ) - result mustEqual true - } - - "intersect when overlapping (partial 2)" in { - val result = Intersection.Test( - Sphere(Vector3.Zero, 2), - Sphere(Vector3(2.5f,0,0), 1) - ) - result mustEqual true - } - - "intersect when the circumferences are touching" in { - val result = Intersection.Test( - Sphere(Vector3.Zero, 2), - Sphere(Vector3(3,0,0), 1) - ) - result mustEqual true - } - - "not intersect when not touching" in { - val result = Intersection.Test( - Sphere(Vector3.Zero, 2), - Sphere(Vector3(4,0,0), 1) - ) - result mustEqual false - } - } - - "Cylinder" should { - "detect intersection if overlapping" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 2), - Cylinder(0, 0, 0, 1, 2) - ) - result mustEqual true - } - - "detect intersection if sides clip" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 2), - Cylinder(0.5f, 0.5f, 0, 1, 2) - ) - result mustEqual true - } - - "detect intersection if touching" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 2), - Cylinder(1, 0, 0, 1, 2) - ) - result mustEqual true - } - - "detect intersection if stacked" in { - val result = Intersection.Test( - Cylinder(1, 0, 0, 1, 2), - Cylinder(1, 0, 2, 1, 2) - ) - result mustEqual true - } - - "detect intersection if one is sunken into the other" in { - val result = Intersection.Test( - Cylinder(1, 0, 0, 1, 2), - Cylinder(1, 0, 1, 1, 2) - ) - result mustEqual true - } - - "not detect intersection if not near each other" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 2), - Cylinder(2, 2, 0, 1, 2) - ) - result mustEqual false - } - - "not detect intersection if one is too high / low" in { - val result = Intersection.Test( - Cylinder(1, 0, 0, 1, 2), - Cylinder(1, 0, 5, 1, 2) - ) - result mustEqual false - } - } - - "Cylinder and Sphere" should { - "detect intersection if overlapping" in { - val result = Intersection.Test( - Cylinder(1, 0, 0, 1, 1), - Sphere(1, 0, 2, 1) - ) - result mustEqual true - } - - "detect intersection if cylinder top touches sphere base" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 1), - Sphere(1, 0, 2, 1) - ) - result mustEqual true - } - - "detect intersection if cylinder base touches sphere top" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 1), - Sphere(-1, 0, -1, 1) - ) - result mustEqual true - } - - "detect intersection if cylinder edge touches sphere edge" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 1), - Sphere(2, 0, 0.5f, 1) - ) - result mustEqual true - } - - "detect intersection if on cylinder top rim" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 1), - Sphere(1.75f, 0, 1.25f, 1) - ) - result mustEqual true - } - - "detect intersection if on cylinder base rim" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 1), - Sphere(1.75f, 0, -0.5f, 1) - ) - result mustEqual true - } - - "not detect intersection if too far above" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 1), - Sphere(0, 0, 3, 1) - ) - result mustEqual false - } - - "not detect intersection if too far below" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 1), - Sphere(0, 0, -3, 1) - ) - result mustEqual false - } - - "not detect intersection if too far out (sideways)" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 1), - Sphere(2, 2, 0, 1) - ) - result mustEqual false - } - - "not detect intersection if too far out (skew)" in { - val result = Intersection.Test( - Cylinder(0, 0, 0, 1, 1), - Sphere(1.5f, 1.5f, 1.5f, 1) - ) - result mustEqual false - } - } -} - -object GeometryTest { } From 6e81ee7e95e73f7055db0e49b1ca835f9b80c3ee Mon Sep 17 00:00:00 2001 From: "Jason_DiDonato@yahoo.com" Date: Mon, 8 Feb 2021 00:20:17 -0500 Subject: [PATCH 6/6] mechanism for server-driven emp caused by projectiles with emp properties; finalization of geometry elements and tests for geometric tests --- .../actors/session/SessionActor.scala | 20 +- .../objects/ExplosiveDeployable.scala | 8 +- .../psforever/objects/GlobalDefinitions.scala | 23 +- .../psforever/objects/geometry/Geometry.scala | 2 +- .../objects/geometry/PrimitiveShape.scala | 332 ++++++++++++++++-- .../objects/vital/etc/EmpReason.scala | 48 +++ .../vital/etc/ExplodingEntityReason.scala | 6 +- .../objects/vital/etc/TriggerUsedReason.scala | 11 +- .../net/psforever/objects/zones/Zone.scala | 62 +++- .../net/psforever/util/PointOfInterest.scala | 2 +- src/test/scala/objects/GeometryTest.scala | 244 +++++++++++++ 11 files changed, 706 insertions(+), 52 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala create mode 100644 src/test/scala/objects/GeometryTest.scala diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index f10a9ca6e..d84c41f69 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -5252,8 +5252,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target) ResolveProjectileInteraction(projectile, resolution1, target, target.Position) match { - case Some(projectile) => - HandleDealingDamage(target, projectile) + case Some(_projectile) => + HandleDealingDamage(target, _projectile) case None => ; } case _ => ; @@ -5264,13 +5264,25 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos) match { - case Some(projectile) => - HandleDealingDamage(target, projectile) + case Some(_projectile) => + HandleDealingDamage(target, _projectile) case None => ; } case _ => ; } }) + if ( + projectile.profile.HasJammedEffectDuration || + projectile.profile.JammerProjectile || + projectile.profile.SympatheticExplosion + ) { + Zone.causeSpecialEmp( + continent, + player, + explosion_pos, + GlobalDefinitions.special_emp.innateDamage.get + ) + } if (profile.ExistsOnRemoteClients && projectile.HasGUID) { //cleanup val localIndex = projectile_guid.guid - Projectile.baseUID diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index c291b4487..6b9eabb57 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -82,7 +82,7 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D mine, DamageInteraction( SourceEntry(mine), - TriggerUsedReason(PlayerSource(player), trigger), + TriggerUsedReason(PlayerSource(player), trigger.GUID), mine.Position ).calculate()(mine), damage = 0 @@ -239,6 +239,10 @@ object ExplosiveDeployableControl { val scalar = Vector3.ScalarProjection(dir, up) val point1 = g1.pointOnOutside(dir).asVector3 val point2 = g2.pointOnOutside(Vector3.neg(dir)).asVector3 - (scalar >= 0 || Vector3.MagnitudeSquared(up * scalar) < 0.35f) && Vector3.DistanceSquared(point1, point2) <= maxDistance + (scalar >= 0 || Vector3.MagnitudeSquared(up * scalar) < 0.35f) && + math.min( + Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3), + Vector3.DistanceSquared(point1, point2) + ) <= maxDistance } } diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 11463740f..f1cdc5d2c 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -972,6 +972,8 @@ object GlobalDefinitions { val router_telepad_deployable = SimpleDeployableDefinition(DeployedItem.router_telepad_deployable) + val special_emp = ExplosiveDeployableDefinition(DeployedItem.jammer_mine) + //this is only treated like a deployable val internal_router_telepad_deployable = InternalTelepadDefinition() //objectId: 744 init_deployables() @@ -7293,6 +7295,22 @@ object GlobalDefinitions { internal_router_telepad_deployable.DeployTime = Duration.create(1, "ms") internal_router_telepad_deployable.DeployCategory = DeployableCategory.Telepads internal_router_telepad_deployable.Packet = new InternalTelepadDeployableConverter + + special_emp.Name = "emp" + special_emp.MaxHealth = 1 + special_emp.Damageable = false + special_emp.Repairable = false + special_emp.DeployCategory = DeployableCategory.Mines + special_emp.explodes = true + special_emp.innateDamage = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 0 + DamageAtEdge = 1.0f + DamageRadius = 5f + AdditionalEffect = true + Modifiers = MaxDistanceCutoff + } } /** @@ -7844,12 +7862,11 @@ object GlobalDefinitions { generator.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 99999 - //DamageRadius should be 14, but 14 is insufficient for hitting the whole chamber; hence, ... - DamageRadius = 15.75f DamageRadiusMin = 14 + DamageRadius = 14.5f DamageAtEdge = 0.00002f Modifiers = ExplodingRadialDegrade - //damage is 99999 at 14m, dropping rapidly to ~1 at 15.75m + //damage is 99999 at 14m, dropping rapidly to ~1 at 14.5m } generator.Geometry = GeometryForm.representByCylinder(radius = 1.2617f, height = 9.14063f) } diff --git a/src/main/scala/net/psforever/objects/geometry/Geometry.scala b/src/main/scala/net/psforever/objects/geometry/Geometry.scala index 70ac40ec0..ce796b756 100644 --- a/src/main/scala/net/psforever/objects/geometry/Geometry.scala +++ b/src/main/scala/net/psforever/objects/geometry/Geometry.scala @@ -6,7 +6,7 @@ import net.psforever.types.Vector3 object Geometry { def equalFloats(value1: Float, value2: Float, off: Float = 0.001f): Boolean = { val diff = value1 - value2 - (diff >= 0 && diff <= off) || diff > -off + if (diff >= 0) diff <= off else diff > -off } def equalVectors(value1: Vector3, value2: Vector3, off: Float = 0.001f): Boolean = { diff --git a/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala b/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala index 5aef779f4..738e45964 100644 --- a/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala +++ b/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala @@ -3,11 +3,22 @@ package net.psforever.objects.geometry import net.psforever.types.Vector3 +/** + * Basic interface for all geometry. + */ trait PrimitiveGeometry { + /** + * The centroid of the geometry. + * @return a point + */ def center: Point - def pointOnOutside(line: Line) : Point = pointOnOutside(line.d) - + /** + * Find a point on the exterior of the geometry if a line was drawn outwards from the centroid. + * What counts as "the exterior" is limited to the complexity of the geometry. + * @param v the vector in the direction of the point on the exterior + * @return a point + */ def pointOnOutside(v: Vector3) : Point } @@ -17,43 +28,103 @@ trait PrimitiveGeometry { // def pointOnOutside(v: Vector3): Point2D = center //} +/** + * Basic interface of all three-dimensional geometry. + * For the only real requirement for a hree-dimensional geometric figure is that it has three components of position + * and an equal number of components demonstrating equal that said dimensionality. + */ trait Geometry3D extends PrimitiveGeometry { def center: Point3D def pointOnOutside(v: Vector3): Point3D = center } +/** + * Characteristics of a geometric figure with only three coordinates to define a position. + */ trait Point { + /** + * Transform the point into the common interchangeable format for coordinates. + * They're very similar, anyway. + * @return a `Vector3` entity of the same denomination + */ def asVector3: Vector3 } +/** + * Characteristics of a geometric figure defining a direction or a progressive change in coordinates. + */ trait Slope { + /** + * The slope itself. + * @return a `Vector3` entity + */ def d: Vector3 + /** + * How long the slope goes on for. + * @return The length of the slope + */ def length: Float } +object Slope { + /** + * On occasions, the defined slope should have a length of one unit. + * It is a unit vector. + * @param v the input slope as a `Vector3` entity + * @throws `AssertionError` if the length is more or less than 1. + */ + def assertUnitVector(v: Vector3): Unit = { + assert({ + val mag = Vector3.Magnitude(v) + mag - 0.05f < 1f && mag + 0.05f > 1f + }, "not a unit vector") + } +} + +/** + * Characteristics of a geometric figure indicating an infinite slope - a mathematical line. + * The slope is always a unit vector. + * The point that assists to define the line is a constraint that the line must pass through. + */ trait Line extends Slope { - assert({ - val mag = Vector3.Magnitude(d) - mag - 0.05f < 1f && mag + 0.05f > 1f - }, "not a unit vector") + Slope.assertUnitVector(d) def p: Point + /** + * The length of a mathematical line is infinite. + * @return The length of the slope + */ def length: Float = Float.PositiveInfinity } +/** + * Characteristics of a geometric figure that have two endpoints, defining a fixed-length slope. + */ trait Segment extends Slope { + /** The first point, considered the "start". */ def p1: Point - + /** The second point, considered the "end". */ def p2: Point def length: Float = Vector3.Magnitude(d) + /** + * Transform the segment into a matheatical line of the same slope. + * @return + */ def asLine: PrimitiveGeometry } +/** + * The instance of a geometric coordinate position. + * @see `Vector3` + * @param x the 'x' coordinate of the position + * @param y the 'y' coordinate of the position + * @param z the 'z' coordinate of the position + */ final case class Point3D(x: Float, y: Float, z: Float) extends Geometry3D with Point { def center: Point3D = this @@ -61,39 +132,119 @@ final case class Point3D(x: Float, y: Float, z: Float) extends Geometry3D with P } object Point3D { + /** + * An overloaded constructor that assigns world origin coordinates. + * @return a `Point3D` entity + */ def apply(): Point3D = Point3D(0,0,0) + /** + * An overloaded constructor that uses the same coordinates from a `Vector3` entity. + * @param v the entity with the corresponding points + * @return a `Point3D` entity + */ def apply(v: Vector3): Point3D = Point3D(v.x, v.y, v.z) } +/** + * The instance of a geometric coordinate position and a specific direction from that position. + * Rays are like mathematical lines in that they have infinite length; + * but, that infinite length is only expressed in a single direction, + * rather than proceeding in both a direction and its opposite direction from a target point. + * Infinity just be like that. + * Additionally, the point is not merely any point on the ray used to assist defining it + * and is instead considered the clearly-defined origin of the ray. + * @param p the point of origin + * @param d the direction + */ final case class Ray3D(p: Point3D, d: Vector3) extends Geometry3D with Line { def center: Point3D = p } object Ray3D { + /** + * An overloaded constructor that uses individual coordinates. + * @param x the 'x' coordinate of the position + * @param y the 'y' coordinate of the position + * @param z the 'z' coordinate of the position + * @param d the direction + * @return a `Ray3D` entity + */ def apply(x: Float, y: Float, z: Float, d: Vector3): Ray3D = Ray3D(Point3D(x,y,z), d) + + /** + * An overloaded constructor that uses a `Vector3` entity to express coordinates. + * @param v the coordinates of the position + * @param d the direction + * @return a `Ray3D` entity + */ + def apply(v: Vector3, d: Vector3): Ray3D = Ray3D(Point3D(v.x, v.y, v.z), d) } +/** + * The instance of a geometric coordinate position and a specific direction from that position. + * Mathematical lines have infinite length and their slope is represented as a unit vector. + * The point is merely a point used to assist in defining the line. + * @param p the point of origin + * @param d the direction + */ final case class Line3D(p: Point3D, d: Vector3) extends Geometry3D with Line { def center: Point3D = p } object Line3D { + /** + * An overloaded constructor that uses individual coordinates. + * @param x the 'x' coordinate of the position + * @param y the 'y' coordinate of the position + * @param z the 'z' coordinate of the position + * @param d the direction + * @return a `Line3D` entity + */ def apply(x: Float, y: Float, z: Float, d: Vector3): Line3D = { Line3D(Point3D(x,y,z), d) } + /** + * An overloaded constructor that uses a pair of individual coordinates + * and uses their difference to produce a unit vector to define a direction. + * @param ax the 'x' coordinate of the position + * @param ay the 'y' coordinate of the position + * @param az the 'z' coordinate of the position + * @param bx the 'x' coordinate of a destination position + * @param by the 'y' coordinate of a destination position + * @param bz the 'z' coordinate of a destination position + * @return a `Line3D` entity + */ def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Line3D = { Line3D(Point3D(ax, ay, az), Vector3.Unit(Vector3(bx-ax, by-ay, bz-az))) } + /** + * An overloaded constructor that uses a pair of points + * and uses their difference to produce a unit vector to define a direction. + * @param p1 the coordinates of the position + * @param p2 the coordinates of a destination position + * @return a `Line3D` entity + */ def apply(p1: Point3D, p2: Point3D): Line3D = { Line3D(p1, Vector3.Unit(Vector3(p2.x-p1.x, p2.y-p1.y, p2.z-p1.z))) } } +/** + * The instance of a limited span between two geometric coordinate positions, called "endpoints". + * Unlike mathematical lines, slope is treated the same as the vector leading from one point to the other + * and is the length of the segment. + * @param p1 a point + * @param p2 another point + */ final case class Segment3D(p1: Point3D, p2: Point3D) extends Geometry3D with Segment { - def center: Point3D = Point3D(d * 0.5f) + /** + * The center point of a segment is a position that is equally in between both endpoints. + * @return a point + */ + def center: Point3D = Point3D((p2.asVector3 + p1.asVector3) * 0.5f) def d: Vector3 = p2.asVector3 - p1.asVector3 @@ -101,65 +252,182 @@ final case class Segment3D(p1: Point3D, p2: Point3D) extends Geometry3D with Seg } object Segment3D { + /** + * An overloaded constructor that uses a pair of individual coordinates + * and uses their difference to define a direction. + * @param ax the 'x' coordinate of the position + * @param ay the 'y' coordinate of the position + * @param az the 'z' coordinate of the position + * @param bx the 'x' coordinate of a destination position + * @param by the 'y' coordinate of a destination position + * @param bz the 'z' coordinate of a destination position + * @return a `Segment3D` entity + */ def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Segment3D = { Segment3D(Point3D(ax, ay, az), Point3D(bx, by, bz)) } + /** + * An overloaded constructor. + * @param p the point of origin + * @param d the direction and distance (of the second point) + */ + def apply(p: Point3D, d: Vector3): Segment3D = { + Segment3D(p, Point3D(p.x + d.x, p.y + d.y, p.z + d.z)) + } + + /** + * An overloaded constructor that uses individual coordinates. + * @param x the 'x' coordinate of the position + * @param y the 'y' coordinate of the position + * @param z the 'z' coordinate of the position + * @param d the direction + * @return a `Segment3D` entity + */ def apply(x: Float, y: Float, z: Float, d: Vector3): Segment3D = { Segment3D(Point3D(x, y, z), Point3D(x + d.x, y + d.y, z + d.z)) } } +/** + * The instance of a volumetric region that encapsulates all points within a certain distance of a central point. + * (That's what a sphere is.) + * A sphere has no real "top", "base", or "side" as all directions are described the same. + * @param p the point + * @param radius a distance that spans all points in any direction from the central point + */ final case class Sphere(p: Point3D, radius: Float) extends Geometry3D { def center: Point3D = p + /** + * Find a point on the exterior of the geometry if a line was drawn outwards from the centroid. + * All points that exist on the exterior of a sphere are on the surface of that sphere + * and are equally distant from the central point. + * @param v the vector in the direction of the point on the exterior + * @return a point + */ override def pointOnOutside(v: Vector3): Point3D = { val slope = Vector3.Unit(v) - val mult = radius / math.sqrt(slope.x * slope.x + slope.y * slope.y + slope.z * slope.z) - val pointOnSurface = center.asVector3 + slope * mult.toFloat - Point3D(pointOnSurface.x, pointOnSurface.y, pointOnSurface.z) + val mult = radius / Vector3.Magnitude(slope) + Point3D(center.asVector3 + slope * mult) } } object Sphere { + /** + * An overloaded constructor that only defines the radius of the sphere + * and places it at the world origin. + * @param radius a distance around the world origin coordinates + * @return a `Sphere` entity + */ def apply(radius: Float): Sphere = Sphere(Point3D(), radius) + /** + * An overloaded constructor that uses individual coordinates to define the central point. + * * @param x the 'x' coordinate of the position + * * @param y the 'y' coordinate of the position + * * @param z the 'z' coordinate of the position + * @param radius a distance around the world origin coordinates + * @return a `Sphere` entity + */ def apply(x: Float, y: Float, z: Float, radius: Float): Sphere = Sphere(Point3D(x,y,z), radius) + /** + * An overloaded constructor that uses vector coordinates to define the central point. + * @param v the coordinates of the position + * @param radius a distance around the world origin coordinates + * @return a `Sphere` entity + */ def apply(v: Vector3, radius: Float): Sphere = Sphere(Point3D(v), radius) } -final case class Cylinder(position: Vector3, relativeUp: Vector3, radius: Float, height: Float) extends Geometry3D { - def center: Point3D = Point3D(position + relativeUp * height * 0.5f) +/** + * The instance of a volumetric region that encapsulates all points within a certain distance of a central point. + * The region is characterized by a regular circular cross-section when observed from above or below + * and a flat top and a flat base when viewed from the side. + * The "base" is where the origin point is defined (at the center of a circular cross-section) + * and the "top" is discovered a `height` from the base along what the cylinder considers its `relativeUp` direction. + * @param p the point + * @param relativeUp what the cylinder considers its "up" direction + * @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction + * @param height the distance between the "base" and the "top" + */ +final case class Cylinder(p: Point3D, relativeUp: Vector3, radius: Float, height: Float) extends Geometry3D { + Slope.assertUnitVector(relativeUp) + /** + * The center point of a cylinder is halfway between the "top" and the "base" along the direction of `relativeUp`. + * @return a point + */ + def center: Point3D = Point3D(p.asVector3 + relativeUp * height * 0.5f) + + /** + * Find a point on the exterior of the geometry if a line was drawn outwards from the centroid. + * A cylinder is composed of three clearly-defined regions on its exterior - + * two flat but circular surfaces that are the "top" and the "base" + * and a wrapped "sides" surface that defines all points connecting the "base" to the "top" + * along the `relativeUp` direction. + * The requested point may exist on any of these surfaces. + * @param v the vector in the direction of the point on the exterior + * @return a point + */ override def pointOnOutside(v: Vector3): Point3D = { val centerVector = center.asVector3 val slope = Vector3.Unit(v) - val acrossTopAndBase = slope - relativeUp - val pointOnSide = centerVector + slope * (radius / Vector3.Magnitude(acrossTopAndBase)) - val pointOnBase = position + acrossTopAndBase * radius - val pointOnTop = pointOnBase + relativeUp * height - val fromPointOnTopToSide = Vector3.Unit(pointOnTop - pointOnSide) - val fromPointOnSideToBase = Vector3.Unit(pointOnSide - pointOnBase) - val target = if(fromPointOnTopToSide == Vector3.Zero || - fromPointOnSideToBase == Vector3.Zero || - Geometry.equalVectors(fromPointOnTopToSide, fromPointOnSideToBase)) { - //on side, including top rim or base rim - pointOnSide + val dotProdOfSlopeAndUp = Vector3.DotProduct(slope, relativeUp) + if (Geometry.equalFloats(dotProdOfSlopeAndUp, value2 = 1) || Geometry.equalFloats(dotProdOfSlopeAndUp, value2 = -1)) { + // very rare condition: 'slope' and 'relativeUp' are parallel or antiparallel + Point3D(centerVector + slope * height * 0.5f) } else { - //on top or base - // the full equation would be 'centerVector + slope * (height * 0.5f / Vector3.Magnitude(relativeUp))' - // 'relativeUp` is already a unit vector (magnitude of 1) - centerVector + slope * height * 0.5f + val acrossTopAndBase = slope - relativeUp * dotProdOfSlopeAndUp + val pointOnSide = centerVector + slope * (radius / Vector3.Magnitude(acrossTopAndBase)) + val pointOnBase = p.asVector3 + acrossTopAndBase * radius + val pointOnTop = pointOnBase + relativeUp * height + val fromPointOnTopToSide = Vector3.Unit(pointOnTop - pointOnSide) + val fromPointOnSideToBase = Vector3.Unit(pointOnSide - pointOnBase) + val target = if(Geometry.equalVectors(fromPointOnTopToSide, Vector3.Zero) || + Geometry.equalVectors(fromPointOnSideToBase, Vector3.Zero) || + Geometry.equalVectors(fromPointOnTopToSide, fromPointOnSideToBase)) { + //on side, including top rim or base rim + pointOnSide + } else { + //on top or base + // the full equation would be 'centerVector + slope * (height * 0.5f / Vector3.Magnitude(relativeUp))' + // 'relativeUp` is already a unit vector (magnitude of 1) + centerVector + slope * height * 0.5f + } + Point3D(target) } - Point3D(target) } } object Cylinder { - def apply(v: Vector3, radius: Float, height: Float): Cylinder = Cylinder(v, Vector3(0,0,1), radius, height) + /** + * An overloaded constructor where the 'relativeUp' of the cylinder is perpendicular to the xy-plane. + * @param p the point + * @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction + * @param height the distance between the "base" and the "top" + * @return + */ + def apply(p: Point3D, radius: Float, height: Float): Cylinder = Cylinder(p, Vector3(0,0,1), radius, height) - def apply(p: Point3D, radius: Float, height: Float): Cylinder = Cylinder(p.asVector3, Vector3(0,0,1), radius, height) + /** + * An overloaded constructor where the origin point is expressed as a vector + * and the 'relativeUp' of the cylinder is perpendicular to the xy-plane. + * @param p the point + * @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction + * @param height the distance between the "base" and the "top" + * @return + */ + def apply(p: Vector3, radius: Float, height: Float): Cylinder = Cylinder(Point3D(p), Vector3(0,0,1), radius, height) - def apply(p: Point3D, v: Vector3, radius: Float, height: Float): Cylinder = Cylinder(p.asVector3, v, radius, height) + /** + * An overloaded constructor the origin point is expressed as a vector. + * @param p the point + * @param v what the cylinder considers its "up" direction + * @param radius a distance expressed in all circular cross-sections along the `relativeUp` direction + * @param height the distance between the "base" and the "top" + * @return + */ + def apply(p: Vector3, v: Vector3, radius: Float, height: Float): Cylinder = Cylinder(Point3D(p), v, radius, height) } diff --git a/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala b/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala new file mode 100644 index 000000000..3693b9e16 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala @@ -0,0 +1,48 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vital.etc + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.ballistics.SourceEntry +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.base.{DamageReason, DamageResolution} +import net.psforever.objects.vital.prop.DamageWithPosition +import net.psforever.objects.vital.resolution.DamageAndResistance + +/** + * A wrapper for a "damage source" in damage calculations + * that parameterizes information necessary to explain a server-driven electromagnetic pulse occurring. + * @see `VitalityDefinition.explodes` + * @see `VitalityDefinition.innateDamage` + * @see `Zone.causesSpecialEmp` + * @param entity the source of the explosive yield + * @param damageModel the model to be utilized in these calculations; + * typically, but not always, defined by the target + */ +final case class EmpReason( + entity: SourceEntry, + source: DamageWithPosition, + damageModel: DamageAndResistance, + override val attribution: Int + ) extends DamageReason { + def resolution: DamageResolution.Value = DamageResolution.Splash + + def same(test: DamageReason): Boolean = test match { + case eer: ExplodingEntityReason => eer.entity eq entity + case _ => false + } + + /** lay the blame on that which caused this emp to occur */ + def adversary: Option[SourceEntry] = Some(entity) +} + +object EmpReason { + def apply( + owner: PlanetSideGameObject with FactionAffinity, + source: DamageWithPosition, + target: PlanetSideServerObject with Vitality + ): EmpReason = { + EmpReason(SourceEntry(owner), source, target.DamageModel, owner.Definition.ObjectId) + } +} diff --git a/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala b/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala index af266474b..ed1096ab9 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala @@ -73,12 +73,12 @@ case object ExplodingRadialDegrade extends ExplodingDamageModifiers.Mod { def calculate(damage: Int, data: DamageInteraction, cause: ExplodingEntityReason): Int = { cause.source match { case withPosition: DamageWithPosition => - val distance = math.sqrt(Zone.distanceCheck( + val radius = withPosition.DamageRadius + val radiusMin = withPosition.DamageRadiusMin + val distance = math.sqrt(Zone.distanceCheck( cause.entity.Definition.asInstanceOf[ObjectDefinition].Geometry(cause.entity), data.target.Definition.Geometry(data.target) )) - val radius = withPosition.DamageRadius - val radiusMin = withPosition.DamageRadiusMin if (distance <= radiusMin) { damage } else if (distance <= radius) { diff --git a/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala b/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala index 575613366..b25531a89 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala @@ -1,13 +1,14 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.vital.etc -import net.psforever.objects.BoomerTrigger +import net.psforever.objects.GlobalDefinitions import net.psforever.objects.ballistics.{PlayerSource, SourceEntry} import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} import net.psforever.objects.vital.base.{DamageReason, DamageResolution} import net.psforever.objects.vital.damage.DamageCalculations.AgainstExoSuit import net.psforever.objects.vital.prop.DamageProperties import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel} +import net.psforever.types.PlanetSideGUID /** * A wrapper for a "damage source" in damage calculations @@ -23,16 +24,16 @@ import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResist * @see `DamageCalculations` * @see `VitalityDefinition.DamageableByFriendlyFire` * @param user the player who is holding the trigger - * @param item the trigger + * @param item_guid the trigger */ -final case class TriggerUsedReason(user: PlayerSource, item: BoomerTrigger) +final case class TriggerUsedReason(user: PlayerSource, item_guid: PlanetSideGUID) extends DamageReason { def source: DamageProperties = TriggerUsedReason.triggered def resolution: DamageResolution.Value = DamageResolution.Resolved def same(test: DamageReason): Boolean = test match { - case tur: TriggerUsedReason => tur.item eq item + case tur: TriggerUsedReason => tur.item_guid == item_guid && tur.user.Name.equals(user.Name) case _ => false } @@ -43,7 +44,7 @@ final case class TriggerUsedReason(user: PlayerSource, item: BoomerTrigger) /** while weird, the trigger was accredited as the method of death on Gemini Live; * even though its icon looks like an misshapen AMS */ - override def attribution: Int = item.Definition.ObjectId + override def attribution: Int = GlobalDefinitions.boomer_trigger.ObjectId } object TriggerUsedReason { diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 66d2e1b3e..95d545956 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -42,9 +42,10 @@ import net.psforever.objects.geometry.Geometry3D import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.vehicles.UtilityType -import net.psforever.objects.vital.etc.ExplodingEntityReason +import net.psforever.objects.vital.etc.{EmpReason, ExplodingEntityReason} import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} +import net.psforever.objects.vital.prop.DamageWithPosition /** * A server object representing the one-landmass planets as well as the individual subterranean caverns.
@@ -1172,6 +1173,64 @@ object Zone { } } + /** + * Allocates `Damageable` targets within the radius of a server-prepared electromagnetic pulse + * and informs those entities that they have affected by the aforementioned pulse. + * Targets within the effect radius within other rooms are affected, unlike with normal damage. + * The only affected target is Boomer deployables. + * @see `Amenity.Owner` + * @see `BoomerDeployable` + * @see `DamageInteraction` + * @see `DamageResult` + * @see `DamageWithPosition` + * @see `EmpReason` + * @see `Zone.DeployableList` + * @param zone the zone in which the emp should occur + * @param obj the entity that triggered the emp (information) + * @param sourcePosition where the emp physically originates + * @param effect characteristics of the emp produced + * @param detectionTest a custom test to determine if any given target is affected; + * defaults to an internal test for simple radial proximity + * @return a list of affected entities + */ + def causeSpecialEmp( + zone: Zone, + obj: PlanetSideServerObject with Vitality, + sourcePosition: Vector3, + effect: DamageWithPosition, + detectionTest: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck + ): List[PlanetSideServerObject] = { + val proxy: ExplosiveDeployable = { + //construct a proxy unit to represent the pulse + val o = new ExplosiveDeployable(GlobalDefinitions.special_emp) + o.Owner = Some(obj.GUID) + o.OwnerName = obj match { + case p: Player => p.Name + case o: OwnableByPlayer => o.OwnerName.getOrElse("") + case _ => "" + } + o.Position = sourcePosition + o.Faction = obj.Faction + o + } + val radius = effect.DamageRadius * effect.DamageRadius + //only boomers can be affected (that's why it's special) + val allAffectedTargets = zone.DeployableList + .collect { case o: BoomerDeployable if !o.Destroyed && (o ne obj) && detectionTest(proxy, o, radius) => o } + //inform targets that they have suffered the effects of the emp + allAffectedTargets + .foreach { target => + target.Actor ! Vitality.Damage( + DamageInteraction( + SourceEntry(target), + EmpReason(obj, effect, target), + sourcePosition + ).calculate() + ) + } + allAffectedTargets + } + /** * Two game entities are considered "near" each other if they are within a certain distance of one another. * A default function literal mainly used for `causesExplosion`. @@ -1197,6 +1256,7 @@ object Zone { * `false`, otherwise */ def distanceCheck(g1: Geometry3D, g2: Geometry3D, maxDistance: Float): Boolean = { + Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3) <= maxDistance || distanceCheck(g1, g2) <= maxDistance } /** diff --git a/src/main/scala/net/psforever/util/PointOfInterest.scala b/src/main/scala/net/psforever/util/PointOfInterest.scala index e6e708661..2a22b7b94 100644 --- a/src/main/scala/net/psforever/util/PointOfInterest.scala +++ b/src/main/scala/net/psforever/util/PointOfInterest.scala @@ -364,7 +364,7 @@ object PointOfInterest { "anguta" -> Vector3(3999, 4170, 266), "igaluk" -> Vector3(3241, 5658, 235), "keelut" -> Vector3(3630, 1904, 265), - "nerrivik" -> Vector3(3522, 3703, 322), + "nerrivik" -> Vector3(3522, 3703, 222), "pinga" -> Vector3(5938, 3545, 96), "sedna" -> Vector3(3932, 5160, 232), "tarqaq" -> Vector3(2980, 2155, 237), diff --git a/src/test/scala/objects/GeometryTest.scala b/src/test/scala/objects/GeometryTest.scala new file mode 100644 index 000000000..056404534 --- /dev/null +++ b/src/test/scala/objects/GeometryTest.scala @@ -0,0 +1,244 @@ +// Copyright (c) 2021 PSForever +package objects + +import net.psforever.objects.geometry._ +import net.psforever.types.Vector3 +import org.specs2.mutable.Specification + +class GeometryTest extends Specification { + "Point3D" should { + "construct (1)" in { + Point3D(1,2,3.5f) + ok + } + + "construct (2)" in { + Point3D() mustEqual Point3D(0,0,0) + } + + "construct (3)" in { + Point3D(Vector3(1,2,3)) mustEqual Point3D(1,2,3) + } + + "be its own center point" in { + val obj = Point3D(1,2,3.5f) + obj.center mustEqual obj + } + + "define its own exterior" in { + val obj = Point3D(1,2,3.5f) + obj.pointOnOutside(Vector3(1,0,0)) mustEqual obj + obj.pointOnOutside(Vector3(0,1,0)) mustEqual obj + obj.pointOnOutside(Vector3(0,0,1)) mustEqual obj + } + + "convert to Vector3" in { + val obj = Point3D(1,2,3.5f) + obj.asVector3 mustEqual Vector3(1,2,3.5f) + } + } + + "Ray3D" should { + "construct (1)" in { + Ray3D(Point3D(1,2,3.5f), Vector3(1,0,0)) + ok + } + + "construct (2)" in { + Ray3D(1,2,3.5f, Vector3(1,0,0)) mustEqual Ray3D(Point3D(1,2,3.5f), Vector3(1,0,0)) + } + + "construct (3)" in { + Ray3D(Vector3(1,2,3.5f), Vector3(1,0,0)) mustEqual Ray3D(Point3D(1,2,3.5f), Vector3(1,0,0)) + } + + "have a unit vector as its direction vector" in { + Ray3D(1,2,3.5f, Vector3(1,1,1)) must throwA[AssertionError] + } + + "have its target point as the center point" in { + val obj = Ray3D(1,2,3.5f, Vector3(1,0,0)) + obj.center mustEqual Point3D(1,2,3.5f) + } + + "define its own exterior" in { + val obj1 = Ray3D(1,2,3.5f, Vector3(1,0,0)) + val obj2 = Point3D(1,2,3.5f) + obj1.pointOnOutside(Vector3(1,0,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,1,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,0,1)) mustEqual obj2 + } + } + + "Line3D" should { + "construct (1)" in { + Line3D(Point3D(1,2,3.5f), Vector3(1,0,0)) + ok + } + + "construct (2)" in { + Line3D(1,2,3.5f, Vector3(1,0,0)) + ok + } + + "construct (3)" in { + Line3D(1,2,3.5f, 2,2,3.5f) mustEqual Line3D(1,2,3.5f, Vector3(1,0,0)) + } + + "have a unit vector as its direction vector" in { + Line3D(1,2,3.5f, Vector3(1,1,1)) must throwA[AssertionError] + } + + "have its target point as the center point" in { + val obj = Line3D(1,2,3.5f, Vector3(1,0,0)) + obj.center mustEqual Point3D(1,2,3.5f) + } + + "define its own exterior" in { + val obj1 = Line3D(1,2,3.5f, Vector3(1,0,0)) + val obj2 = Point3D(1,2,3.5f) + obj1.pointOnOutside(Vector3(1,0,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,1,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,0,1)) mustEqual obj2 + } + } + + "Segment3D" should { + "construct (1)" in { + Segment3D(Point3D(1,2,3), Point3D(3,2,3)) + ok + } + + "construct (2)" in { + Segment3D(1,2,3, 3,2,3) mustEqual Segment3D(Point3D(1,2,3), Point3D(3,2,3)) + ok + } + + "construct (3)" in { + Segment3D(Point3D(1,2,3), Vector3(1,0,0)) mustEqual Segment3D(Point3D(1,2,3), Point3D(2,2,3)) + } + + "construct (4)" in { + Segment3D(1,2,3, Vector3(1,0,0)) mustEqual Segment3D(Point3D(1,2,3), Point3D(2,2,3)) + } + + "does not need to have unit vector as its direction vector" in { + val obj1 = Segment3D(1,2,3, Vector3(5,1,1)) + val obj2 = Segment3D(Point3D(1,2,3), Point3D(6,3,4)) + obj1 mustEqual obj2 + obj1.d mustEqual obj2.d + } + + "have a midway point between its two endpoints" in { + Segment3D(Point3D(1,2,3), Point3D(3,4,5)).center mustEqual Point3D(2,3,4) + } + + "report the point on the outside as its center point" in { + val obj1 = Segment3D(Point3D(1,2,3), Point3D(3,4,5)) + val obj2 = obj1.center + obj1.pointOnOutside(Vector3(1,0,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,1,0)) mustEqual obj2 + obj1.pointOnOutside(Vector3(0,0,1)) mustEqual obj2 + } + } + + "Sphere3D" should { + "construct (1)" in { + Sphere(Point3D(1,2,3), 3) + ok + } + + "construct (2)" in { + Sphere(3) mustEqual Sphere(Point3D(0,0,0), 3) + ok + } + + "construct (3)" in { + Sphere(1,2,3, 3) mustEqual Sphere(Point3D(1,2,3), 3) + } + + "construct (4)" in { + Sphere(Vector3(1,2,3), 3) mustEqual Sphere(Point3D(1,2,3), 3) + } + + "the center point is self-evident" in { + Sphere(Point3D(1,2,3), 3).center mustEqual Point3D(1,2,3) + } + + "report the point on the outside depending on the requested direction" in { + val obj1 = Sphere(1,2,3, 3) + obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point3D( 4, 2,3) //east + obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point3D( 1, 5,3) //north + obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point3D( 1, 2,6) //up + obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point3D(-2, 2,3) //west + obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point3D( 1,-1,3) //south + obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point3D( 1, 2,0) //down + } + } + + "Cylinder (normal)" should { + "construct (1)" in { + Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3) + ok + } + + "construct (2)" in { + Cylinder(Point3D(1,2,3), 2, 3) mustEqual Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3) + } + + "construct (3)" in { + Cylinder(Vector3(1,2,3), 2, 3) mustEqual Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3) + } + + "construct (4)" in { + Cylinder(Vector3(1,2,3), Vector3(0,0,1), 2, 3) mustEqual Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3) + } + + "report the center point as the center of the cylinder" in { + Cylinder(Point3D(1,2,3), 2, 3).center mustEqual Point3D(1,2,4.5f) + } + + "the point on the outside is different depending on the requested direction" in { + val obj1 = Cylinder(Point3D(1,2,3), 2, 3) + obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point3D( 3, 2, 4.5f) //east + obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point3D( 1, 4, 4.5f) //north + obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point3D( 1, 2, 6f) //up + obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point3D(-1, 2, 4.5f) //west + obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point3D( 1, 0, 4.5f) //south + obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point3D( 1, 2, 3f) //down + } + } + + "Cylinder (side tilt)" should { + "not require a specific direction to be relative up" in { + Cylinder(Point3D(1,2,3), Vector3(1,0,0), 2, 3) + ok + } + + "require its specific relative up direction to be expressed as a unit vector" in { + Cylinder(Point3D(1,2,3), Vector3(4,0,0), 2, 3) must throwA[AssertionError] + } + + "report the center point as the center of the cylinder, as if rotated about its base" in { + Cylinder(Point3D(1,2,3), Vector3(1,0,0), 2, 3).center mustEqual Point3D(2.5f, 2, 3) + } + + "report the point on the outside as different depending on the requested direction and the relative up direction" in { + val obj1 = Cylinder(Point3D(1,2,3), Vector3(1,0,0), 2, 3) + obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point3D(4, 2, 3) //east + obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point3D(2.5f, 4, 3) //north + obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point3D(2.5f, 2, 5) //up + obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point3D(1, 2, 3) //west + obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point3D(2.5f, 0, 3) //south + obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point3D(2.5f, 2, 1) //down + + val obj2 = Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3) + obj1.pointOnOutside(Vector3( 1, 0, 0)) mustNotEqual obj2.pointOnOutside(Vector3( 1, 0, 0)) + obj1.pointOnOutside(Vector3( 0, 1, 0)) mustNotEqual obj2.pointOnOutside(Vector3( 1, 1, 0)) + obj1.pointOnOutside(Vector3( 0, 0, 1)) mustNotEqual obj2.pointOnOutside(Vector3( 1, 0, 1)) + obj1.pointOnOutside(Vector3(-1, 0, 0)) mustNotEqual obj2.pointOnOutside(Vector3(-1, 0, 0)) + obj1.pointOnOutside(Vector3( 0,-1, 0)) mustNotEqual obj2.pointOnOutside(Vector3( 0,-1, 0)) + obj1.pointOnOutside(Vector3( 0, 0,-1)) mustNotEqual obj2.pointOnOutside(Vector3( 0, 0,-1)) + } + } +}