Merge pull request #689 from Fate-JH/ok-boomer

Ok Boomer
This commit is contained in:
Fate-JH 2021-02-27 07:29:13 -05:00 committed by GitHub
commit fcf0240342
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1513 additions and 120 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.<br>
@ -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)
}
}

View file

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

View file

@ -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),

View file

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

View file

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