diff --git a/server/src/main/resources/overrides/game_objects0.adb.lst b/server/src/main/resources/overrides/game_objects0.adb.lst index 2b6529ea..a44d9237 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 f29757b0..d84c41f6 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 @@ -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() @@ -5245,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 _ => ; @@ -5257,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 8b749a60..6b9eabb5 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -2,16 +2,20 @@ 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.geometry.Geometry3D +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.interaction.DamageResult +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 import net.psforever.objects.zones.Zone import net.psforever.types.Vector3 @@ -21,7 +25,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 +69,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 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( + mine, + DamageInteraction( + SourceEntry(mine), + TriggerUsedReason(PlayerSource(player), trigger.GUID), + mine.Position + ).calculate()(mine), + damage = 0 + ) case _ => ; } @@ -74,19 +98,48 @@ 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 = { + !mine.Destroyed && (if (damage == 0 && data.cause.source.SympatheticExplosion) { + Damageable.CanDamageOrJammer(mine, damage = 1, data) + } else { + Damageable.CanDamageOrJammer(mine, damage, data) + }) + } } 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 +152,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), ExplosiveDeployableControl.detectionForExplosiveSource(target)) + } + /** * na * @param target na @@ -118,8 +181,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) @@ -131,4 +197,52 @@ 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) && + 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 9d887e45..ab086ed3 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 @@ -24,6 +25,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} @@ -970,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() @@ -5601,6 +5605,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 @@ -5626,11 +5635,12 @@ object GlobalDefinitions { Damage1 = 225 DamageRadius = 5 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } fury.DrownAtMaxDepth = true fury.MaxDepth = 1.3f fury.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + fury.Geometry = atvForm quadassault.Name = "quadassault" // Basilisk quadassault.MaxHealth = 650 @@ -5657,11 +5667,12 @@ object GlobalDefinitions { Damage1 = 225 DamageRadius = 5 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } quadassault.DrownAtMaxDepth = true quadassault.MaxDepth = 1.3f quadassault.UnderwaterLifespan(suffocation = 5000L, recovery = 2500L) + quadassault.Geometry = atvForm quadstealth.Name = "quadstealth" // Wraith quadstealth.MaxHealth = 650 @@ -5688,11 +5699,12 @@ object GlobalDefinitions { Damage1 = 225 DamageRadius = 5 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -5721,11 +5733,12 @@ 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 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 @@ -5748,7 +5761,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 @@ -5756,11 +5768,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -5795,11 +5808,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -5829,11 +5843,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -5863,10 +5878,11 @@ 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 ...? + twomanhoverbuggy.Geometry = GeometryForm.representByCylinder(radius = 2.1875f, height = 2.01563f) mediumtransport.Name = "mediumtransport" // Deliverer mediumtransport.MaxHealth = 2500 @@ -5903,11 +5919,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } mediumtransport.DrownAtMaxDepth = false mediumtransport.MaxDepth = 1.2f mediumtransport.UnderwaterLifespan(suffocation = -1, recovery = -1) + mediumtransport.Geometry = delivererForm battlewagon.Name = "battlewagon" // Raider battlewagon.MaxHealth = 2500 @@ -5947,11 +5964,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } battlewagon.DrownAtMaxDepth = true battlewagon.MaxDepth = 1.2f battlewagon.UnderwaterLifespan(suffocation = -1, recovery = -1) + battlewagon.Geometry = delivererForm thunderer.Name = "thunderer" thunderer.MaxHealth = 2500 @@ -5988,11 +6006,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } thunderer.DrownAtMaxDepth = true thunderer.MaxDepth = 1.2f thunderer.UnderwaterLifespan(suffocation = -1, recovery = -1) + thunderer.Geometry = delivererForm aurora.Name = "aurora" aurora.MaxHealth = 2500 @@ -6029,11 +6048,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6092,11 +6112,12 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6155,11 +6176,12 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6218,11 +6240,12 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6250,11 +6273,12 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6287,11 +6311,12 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6320,11 +6345,12 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6355,11 +6381,12 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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" @@ -6388,11 +6415,12 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6424,11 +6452,12 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 15 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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" @@ -6460,11 +6489,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6496,11 +6526,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6530,11 +6561,12 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6564,10 +6596,11 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6598,10 +6631,11 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6631,10 +6665,11 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 10 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6674,10 +6709,11 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } liberator.DrownAtMaxDepth = true liberator.MaxDepth = 2 //flying vehicles will automatically disable + liberator.Geometry = liberatorForm vulture.Name = "vulture" vulture.MaxHealth = 2500 @@ -6718,10 +6754,11 @@ object GlobalDefinitions { Damage1 = 375 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } vulture.DrownAtMaxDepth = true vulture.MaxDepth = 2 //flying vehicles will automatically disable + vulture.Geometry = liberatorForm dropship.Name = "dropship" // Galaxy dropship.MaxHealth = 5000 @@ -6792,10 +6829,11 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 30 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6850,10 +6888,11 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 30 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } 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 @@ -6891,10 +6930,11 @@ object GlobalDefinitions { Damage1 = 450 DamageRadius = 30 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } lodestar.DrownAtMaxDepth = true lodestar.MaxDepth = 2 + lodestar.Geometry = GeometryForm.representByCylinder(radius = 7.8671f, height = 6.79688f) //TODO hexahedron phantasm.Name = "phantasm" phantasm.MaxHealth = 2500 @@ -6933,10 +6973,11 @@ object GlobalDefinitions { Damage1 = 150 DamageRadius = 12 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } phantasm.DrownAtMaxDepth = true phantasm.MaxDepth = 2 + phantasm.Geometry = GeometryForm.representByCylinder(radius = 5.2618f, height = 3f) droppod.Name = "droppod" droppod.MaxHealth = 20000 @@ -6950,12 +6991,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 @@ -6975,8 +7022,9 @@ object GlobalDefinitions { Damage4 = 1850 DamageRadius = 5.1f DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + boomer.Geometry = mine he_mine.Name = "he_mine" he_mine.Descriptor = "Mines" @@ -6996,8 +7044,9 @@ object GlobalDefinitions { Damage4 = 1600 DamageRadius = 6.6f DamageAtEdge = 0.25f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + he_mine.Geometry = mine jammer_mine.Name = "jammer_mine" jammer_mine.Descriptor = "JammerMines" @@ -7007,6 +7056,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" @@ -7028,8 +7078,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + spitfire_turret.Geometry = smallTurret spitfire_cloaked.Name = "spitfire_cloaked" spitfire_cloaked.Descriptor = "CloakingSpitfires" @@ -7050,8 +7101,9 @@ object GlobalDefinitions { Damage1 = 75 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + spitfire_cloaked.Geometry = smallTurret spitfire_aa.Name = "spitfire_aa" spitfire_aa.Descriptor = "FlakSpitfires" @@ -7072,8 +7124,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + spitfire_aa.Geometry = smallTurret motionalarmsensor.Name = "motionalarmsensor" motionalarmsensor.Descriptor = "MotionSensors" @@ -7082,6 +7135,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" @@ -7090,6 +7144,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" @@ -7106,8 +7161,9 @@ object GlobalDefinitions { Damage1 = 10 DamageRadius = 8 DamageAtEdge = 0.2f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + tank_traps.Geometry = GeometryForm.representByCylinder(radius = 2.89680997f, height = 3.57812f) val fieldTurretConverter = new FieldTurretConverter portable_manned_turret.Name = "portable_manned_turret" @@ -7133,8 +7189,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + portable_manned_turret.Geometry = largeTurret portable_manned_turret_nc.Name = "portable_manned_turret_nc" portable_manned_turret_nc.Descriptor = "FieldTurrets" @@ -7159,8 +7216,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + portable_manned_turret_nc.Geometry = largeTurret portable_manned_turret_tr.Name = "portable_manned_turret_tr" portable_manned_turret_tr.Descriptor = "FieldTurrets" @@ -7185,8 +7243,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + portable_manned_turret_tr.Geometry = largeTurret portable_manned_turret_vs.Name = "portable_manned_turret_vs" portable_manned_turret_vs.Descriptor = "FieldTurrets" @@ -7211,8 +7270,9 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 8 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + Modifiers = ExplodingRadialDegrade } + portable_manned_turret_vs.Geometry = largeTurret deployable_shield_generator.Name = "deployable_shield_generator" deployable_shield_generator.Descriptor = "ShieldGenerators" @@ -7222,6 +7282,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 @@ -7231,6 +7292,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 @@ -7239,12 +7301,30 @@ 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 + } } /** * 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 @@ -7285,9 +7365,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( @@ -7349,16 +7430,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) @@ -7367,6 +7450,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( @@ -7377,9 +7461,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( @@ -7390,9 +7475,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( @@ -7403,9 +7489,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( @@ -7416,9 +7503,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( @@ -7429,7 +7517,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 @@ -7442,7 +7530,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 @@ -7455,9 +7543,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 @@ -7466,9 +7555,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 @@ -7477,7 +7567,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 @@ -7486,9 +7577,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) @@ -7504,8 +7596,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 @@ -7516,8 +7609,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 @@ -7544,7 +7638,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 @@ -7658,7 +7752,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 @@ -7674,15 +7768,16 @@ object GlobalDefinitions { Damage1 = 300 DamageRadius = 5 DamageAtEdge = 0.1f - Modifiers = RadialDegrade + 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 @@ -7690,6 +7785,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 @@ -7764,7 +7860,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 @@ -7772,12 +7868,12 @@ 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 = RadialDegrade - //damage is 99999 at 14m, dropping rapidly to ~1 at 15.75m + Modifiers = ExplodingRadialDegrade + //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/ballistics/PlayerSource.scala b/src/main/scala/net/psforever/objects/ballistics/PlayerSource.scala index 5413f325..0a2d2120 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 da18c098..f99de884 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 29e7609c..5378b522 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/Geometry.scala b/src/main/scala/net/psforever/objects/geometry/Geometry.scala new file mode 100644 index 00000000..ce796b75 --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/Geometry.scala @@ -0,0 +1,30 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.geometry + +import net.psforever.types.Vector3 + +object Geometry { + def equalFloats(value1: Float, value2: Float, off: Float = 0.001f): Boolean = { + val diff = value1 - value2 + if (diff >= 0) diff <= off else 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/GeometryForm.scala b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala new file mode 100644 index 00000000..8614163e --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala @@ -0,0 +1,126 @@ +// 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, 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 cylinder can not be used for purposes of geometric representation */ + lazy val invalidCylinder: Cylinder = Cylinder(invalidPoint.asVector3, Vector3.Zero, 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 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) + } + } + + /** + * 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 + * @return the representation + */ + def representByCylinder(radius: Float, height: Float)(o: Any): Geometry3D = { + o match { + 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 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( + p.Position, + radius + radialOffset, + GlobalDefinitions.MaxDepth(p) + ) + case p: PlayerSource => + val radialOffset = if(p.ExoSuit == ExoSuitType.MAX) 0.125f else 0f + val heightOffset = if(p.crouching) 1.093750f else GlobalDefinitions.avatar.MaxDepth + Cylinder( + p.Position, + radius + radialOffset, + heightOffset + ) + case _ => + invalidCylinder + } + } + + /** + * 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) + * @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(p.Position, Vector3.relativeUp(p.Orientation), radius, height) + case s: SourceEntry => + Cylinder(s.Position, Vector3.relativeUp(s.Orientation), radius, height) + case _ => + invalidCylinder + } + } +} 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 00000000..738e4596 --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala @@ -0,0 +1,433 @@ +// Copyright (c) 2021 PSForever +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 + + /** + * 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 +} + +//trait Geometry2D extends PrimitiveGeometry { +// def center: Point2D +// +// 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 { + 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 + + def asVector3: Vector3 = Vector3(x, y, z) +} + +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 { + /** + * 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 + + def asLine: Line3D = Line3D(p1, Vector3.Unit(d)) +} + +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 / 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) +} + +/** + * 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 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 { + 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) + } + } +} + +object Cylinder { + /** + * 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) + + /** + * 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) + + /** + * 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 7e652ff1..ed1096ab 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 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) + )) + 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/vital/etc/TriggerUsedReason.scala b/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala new file mode 100644 index 00000000..b25531a8 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/etc/TriggerUsedReason.scala @@ -0,0 +1,62 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.vital.etc + +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 + * 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_guid the trigger + */ +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_guid == item_guid && tur.user.Name.equals(user.Name) + 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 = GlobalDefinitions.boomer_trigger.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 diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 8622ca76..95d54595 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -38,12 +38,14 @@ 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 -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.
@@ -1134,7 +1136,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) @@ -1171,19 +1173,106 @@ 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`. * @see `causeExplosion` - * @see `Vector3.DistanceSquare` - * @param obj1 a game entity - * @param obj2 a game entity + * @see `ObjectDefinition.Geometry` + * @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 */ - 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 enough to each other; + * `false`, otherwise + */ + def distanceCheck(g1: Geometry3D, g2: Geometry3D, maxDistance: Float): Boolean = { + Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3) <= maxDistance || + 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) } } diff --git a/src/main/scala/net/psforever/types/Vector3.scala b/src/main/scala/net/psforever/types/Vector3.scala index 43c4f43f..76fecca4 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 @@ -379,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)` @@ -390,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/main/scala/net/psforever/util/PointOfInterest.scala b/src/main/scala/net/psforever/util/PointOfInterest.scala index 87467a3c..4c04f4d9 100644 --- a/src/main/scala/net/psforever/util/PointOfInterest.scala +++ b/src/main/scala/net/psforever/util/PointOfInterest.scala @@ -373,7 +373,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/Vector3Test.scala b/src/test/scala/Vector3Test.scala index ef39a2bb..38cb36f9 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 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)) + } + } +}