diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index f10a9ca6..d84c41f6 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 c291b448..6b9eabb5 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 11463740..f1cdc5d2 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 70ac40ec..ce796b75 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 5aef779f..738e4596 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 00000000..3693b9e1 --- /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 af266474..ed1096ab 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 57561336..b25531a8 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 66d2e1b3..95d54595 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 e6e70866..2a22b7b9 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 00000000..05640453 --- /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)) + } + } +}