projectiles that, by definition, are allowed to damage targets through walls (radiation_cloud, with DamageThroughWalls) regardless of sidedness; any unflagged radiation_cloud must be on the side as its target; fixed chat bang-commands not being executed

This commit is contained in:
Fate-JH 2024-03-15 01:17:16 -04:00
parent b5d60a7f9e
commit 4508c1ae45
12 changed files with 266 additions and 60 deletions

View file

@ -224,6 +224,10 @@ class ChatActor(
session.account.gm || Config.app.development.unprivilegedGmCommands.contains(message.messageType)
(message.messageType, message.recipient.trim, message.contents.trim) match {
/** Messages starting with ! are custom chat commands */
case (_, _, contents) if contents.startsWith("!") &&
customCommandMessages(message, session, chatService, cluster, gmCommandAllowed) => ()
case (CMT_FLY, recipient, contents) if gmCommandAllowed =>
val flying = contents match {
case "on" => true
@ -899,10 +903,6 @@ class ChatActor(
ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt)
)
/** Messages starting with ! are custom chat commands */
case (_, _, contents) if contents.startsWith("!") &&
customCommandMessages(message, session, chatService, cluster, gmCommandAllowed) => ;
case _ =>
log.warn(s"Unhandled chat message $message")
}
@ -1126,7 +1126,7 @@ class ChatActor(
)
true
} else if (contents.startsWith("!loc ")) {
} else if (contents.startsWith("!loc")) {
val continent = session.zone
val player = session.player
val loc =

View file

@ -3,6 +3,8 @@ package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
import net.psforever.objects.definition.ProjectileDefinition
import net.psforever.objects.serverobject.doors.InteriorDoorPassage
import net.psforever.objects.serverobject.interior.Sidedness
import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.zones.Zoning
import net.psforever.objects.serverobject.turret.VanuSentry
@ -303,11 +305,11 @@ private[support] class WeaponAndProjectileOperations(
//find target(s)
(hit_info match {
case Some(hitInfo) =>
val hitPos = hitInfo.hit_pos
val hitPos = hitInfo.hit_pos
sessionData.validObject(hitInfo.hitobject_guid, decorator = "Hit/hitInfo") match {
case _ if projectile.profile == GlobalDefinitions.flail_projectile =>
val radius = projectile.profile.DamageRadius * projectile.profile.DamageRadius
val targets = Zone.findAllTargets(hitPos)(continent, player, projectile.profile)
val targets = Zone.findAllTargets(continent, player, hitPos, projectile.profile)
.filter { target =>
Vector3.DistanceSquared(target.Position, hitPos) <= radius
}
@ -361,7 +363,6 @@ private[support] class WeaponAndProjectileOperations(
FindProjectileEntry(projectile_guid) match {
case Some(projectile) =>
val profile = projectile.profile
projectile.Position = explosion_pos
projectile.Velocity = projectile_vel
val (resolution1, resolution2) = profile.Aggravated match {
case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) =>
@ -372,7 +373,7 @@ private[support] class WeaponAndProjectileOperations(
//direct_victim_uid
sessionData.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target)
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile =>
addShotsLanded(resprojectile.cause.attribution, shots = 1)
sessionData.handleDealingDamage(target, resprojectile)
@ -563,6 +564,7 @@ private[support] class WeaponAndProjectileOperations(
ProjectileQuality.Normal
}
val qualityprojectile = projectile.quality(initialQuality)
qualityprojectile.WhichSide = player.WhichSide
projectiles(projectileIndex) = Some(qualityprojectile)
if (projectile_info.ExistsOnRemoteClients) {
log.trace(
@ -1048,19 +1050,22 @@ private[support] class WeaponAndProjectileOperations(
GlobalDefinitions.getDamageProxy(projectile, hitPos) match {
case Nil =>
Nil
case list if list.isEmpty =>
Nil
case list =>
HandleDamageProxySetupLittleBuddy(list, hitPos)
UpdateProjectileSidednessAfterHit(projectile, hitPos)
val projectileSide = projectile.WhichSide
list.flatMap { proxy =>
if (proxy.profile.ExistsOnRemoteClients) {
proxy.Position = hitPos
proxy.WhichSide = projectileSide
continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy)
Nil
} else if (proxy.tool_def == GlobalDefinitions.maelstrom) {
//server-side maelstrom grenade target selection
val radius = proxy.profile.LashRadius * proxy.profile.LashRadius
val targets = continent.blockMap
.sector(hitPos, proxy.profile.LashRadius)
.livePlayerList
val targets = Zone.findAllTargets(continent, hitPos, proxy.profile.LashRadius, { _.livePlayerList })
.filter { target =>
Vector3.DistanceSquared(target.Position, hitPos) <= radius
}
@ -1498,6 +1503,88 @@ private[support] class WeaponAndProjectileOperations(
}
}
private def UpdateProjectileSidednessAfterHit(projectile: Projectile, hitPosition: Vector3): Unit = {
val origin = projectile.Position
val distance = Vector3.Magnitude(hitPosition - origin)
continent.blockMap
.sector(hitPosition, distance)
.environmentList
.collect { case o: InteriorDoorPassage =>
val door = o.door
val intersectTest = quickLineSphereIntersectionPoints(
origin,
hitPosition,
door.Position,
door.Definition.geometryInteractionRadius.get + 0.1f
)
(door, intersectTest)
}
.collect { case (door, intersectionTest) if intersectionTest.nonEmpty =>
(door, Vector3.Magnitude(hitPosition - door.Position), intersectionTest)
}
.minByOption { case (_, dist, _) => dist }
.foreach { case (door, _, intersects) =>
val strictly = if (Vector3.DotProduct(Vector3.Unit(hitPosition - door.Position), door.Outwards) > 0f) {
Sidedness.OutsideOf
} else {
Sidedness.InsideOf
}
projectile.WhichSide = if (intersects.size == 1) {
Sidedness.InBetweenSides(door, strictly)
} else {
strictly
}
}
}
/**
* Does a line segment line intersect with a sphere?<br>
* This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package.
* @param start first point of the line segment
* @param end second point of the line segment
* @param center center of the sphere
* @param radius radius of the sphere
* @return list of all points of intersection, if any
* @see `Vector3.DistanceSquared`
* @see `Vector3.MagnitudeSquared`
*/
private def quickLineSphereIntersectionPoints(
start: Vector3,
end: Vector3,
center: Vector3,
radius: Float
): Iterable[Vector3] = {
/*
Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere,
because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation.
*/
val Vector3(cx, cy, cz) = center
val Vector3(sx, sy, sz) = start
val vector = end - start
//speed our way through a quadratic equation
val (a, b) = {
val Vector3(dx, dy, dz) = vector
(
dx * dx + dy * dy + dz * dz,
2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz))
)
}
val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius
val result = b * b - 4 * a * c
if (result < 0f) {
//negative, no intersection
Seq()
} else if (result < 0.00001f) {
//zero-ish, one intersection point
Seq(start - vector * (b / (2f * a)))
} else {
//positive, two intersection points
val sqrt = math.sqrt(result).toFloat
val endStart = vector / (2f * a)
Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f)
}.filter(p => Vector3.DistanceSquared(start, p) <= a)
}
override protected[session] def stop(): Unit = {
if (player != null && player.HasGUID) {
(prefire ++ shooting).foreach { guid =>

View file

@ -29,7 +29,7 @@ class InteractWithRadiationClouds(
*/
private var skipTargets: List[PlanetSideGUID] = List()
def Type = RadiationInteraction
def Type: ZoneInteractionType = RadiationInteraction
/**
* Wander into a radiation cloud and suffer the consequences.
@ -40,12 +40,16 @@ class InteractWithRadiationClouds(
target match {
case t: Vitality =>
val position = target.Position
val targetList = List(target)
//collect all projectiles in sector/range
val projectiles = sector
.projectileList
.filter { cloud =>
val radius = cloud.Definition.DamageRadius
cloud.Definition.radiation_cloud && Zone.distanceCheck(target, cloud, radius * radius)
val definition = cloud.Definition
val radius = definition.DamageRadius
definition.radiation_cloud &&
Zone.allOnSameSide(cloud, definition, targetList).nonEmpty &&
Zone.distanceCheck(target, cloud, radius * radius)
}
.distinct
val notSkipped = projectiles.filterNot { t => skipTargets.contains(t.GUID) }

View file

@ -1,6 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.serverobject.interior.TraditionalInteriorAware
import net.psforever.objects.sourcing.SourceEntry
import java.util.concurrent.atomic.AtomicLong
@ -53,7 +54,8 @@ final case class Projectile(
id: Long = Projectile.idGenerator.getAndIncrement(),
fire_time: Long = System.currentTimeMillis()
) extends PlanetSideGameObject
with BlockMapEntity {
with BlockMapEntity
with TraditionalInteriorAware {
Position = shot_origin
Orientation = shot_angle
Velocity = shot_velocity.getOrElse {
@ -73,7 +75,8 @@ final case class Projectile(
* Create a copy of this projectile with all the same information
* save for the quality.
* Used mainly for aggravated damage.
* It is important to note that the new projectile shares the (otherwise) exclusive id of the original.
* It is important to note that the new projectile shares the (otherwise) exclusive id of the original
* and that it is not added to a block map structure.
* @param value the new quality
* @return a new `Projectile` entity
*/
@ -94,6 +97,7 @@ final case class Projectile(
)
if(isMiss) projectile.Miss()
else if(isResolved) projectile.Resolve()
projectile.WhichSide = this.WhichSide
projectile
}

View file

@ -46,8 +46,6 @@ class ProjectileDefinition(objectId: Int)
/** projectile takes the form of a type of "grenade";
* grenades arc with gravity rather than travel in a relatively straight path */
private var grenade_projectile: Boolean = false
/** radiation clouds create independent damage-dealing areas in a zone that last for the projectile's lifespan */
var radiation_cloud: Boolean = false
//derived calculations
/** the calculated distance at which the projectile have traveled far enough to despawn (m);
* typically handled as the projectile no longer performing damage;

View file

@ -57,6 +57,16 @@ object Segment {
Segment(p, Point(p.x + d.x, p.y + d.y, p.z + d.z))
}
/**
* An overloaded constructor that uses individual coordinates.
* @param a origin
* @param b destination
* @return a `Segment` entity
*/
def apply(a: Vector3, b: Vector3): Segment = {
Segment(Point(a), Point(b))
}
/**
* An overloaded constructor that uses individual coordinates.
* @param x the 'x' coordinate of the position

View file

@ -1040,7 +1040,7 @@ object GlobalDefinitionsProjectile {
maelstrom_grenade_damager.Name = "maelstrom_grenade_damager"
maelstrom_grenade_damager.ProjectileDamageType = DamageType.Direct
//the maelstrom_grenade_damage is something of a broken entity atm
//the maelstrom_grenade_damage will be treated as a broken entity
maelstrom_grenade_projectile.Name = "maelstrom_grenade_projectile"
maelstrom_grenade_projectile.Damage0 = 32
@ -1049,6 +1049,7 @@ object GlobalDefinitionsProjectile {
maelstrom_grenade_projectile.LashRadius = 5f
maelstrom_grenade_projectile.GrenadeProjectile = true
maelstrom_grenade_projectile.ProjectileDamageType = DamageType.Direct
maelstrom_grenade_projectile.DamageThroughWalls = true
maelstrom_grenade_projectile.InitialVelocity = 30
maelstrom_grenade_projectile.Lifespan = 2f
maelstrom_grenade_projectile.DamageProxy = 464 //maelstrom_grenade_damager
@ -1063,6 +1064,7 @@ object GlobalDefinitionsProjectile {
maelstrom_grenade_projectile_contact.LashRadius = 5f
maelstrom_grenade_projectile_contact.GrenadeProjectile = true
maelstrom_grenade_projectile_contact.ProjectileDamageType = DamageType.Direct
maelstrom_grenade_projectile_contact.DamageThroughWalls = true
maelstrom_grenade_projectile_contact.InitialVelocity = 30
maelstrom_grenade_projectile_contact.Lifespan = 15f
maelstrom_grenade_projectile_contact.DamageProxy = 464 //maelstrom_grenade_damager
@ -1533,6 +1535,7 @@ object GlobalDefinitionsProjectile {
radiator_cloud.DamageToHealthOnly = true
radiator_cloud.radiation_cloud = true
radiator_cloud.ProjectileDamageType = DamageType.Radiation
radiator_cloud.DamageThroughWalls = true
//custom aggravated information
radiator_cloud.ProjectileDamageTypeSecondary = DamageType.Aggravated
radiator_cloud.Aggravated = AggravatedDamage(
@ -2017,6 +2020,7 @@ object GlobalDefinitionsProjectile {
aphelion_plasma_cloud.DamageRadius = 3f
aphelion_plasma_cloud.radiation_cloud = true
aphelion_plasma_cloud.ProjectileDamageType = DamageType.Aggravated
//aphelion_plasma_cloud.DamageThroughWalls = true
aphelion_plasma_cloud.Aggravated = AggravatedDamage(
AggravatedInfo(DamageType.Splash, 0.5f, 1000),
Aura.Napalm,
@ -2229,6 +2233,7 @@ object GlobalDefinitionsProjectile {
peregrine_particle_cannon_radiation_cloud.DamageRadius = 3f
peregrine_particle_cannon_radiation_cloud.radiation_cloud = true
peregrine_particle_cannon_radiation_cloud.ProjectileDamageType = DamageType.Radiation
peregrine_particle_cannon_radiation_cloud.DamageThroughWalls = true
peregrine_particle_cannon_radiation_cloud.Lifespan = 5.0f
ProjectileDefinition.CalculateDerivedFields(peregrine_particle_cannon_radiation_cloud)
peregrine_particle_cannon_radiation_cloud.registerAs = "rc-projectiles"

View file

@ -6,6 +6,8 @@ import net.psforever.objects.serverobject.doors.Door
sealed trait SidenessComparison
sealed trait Sidedness {
def opposite: Sidedness
protected def value: SidenessComparison
}
@ -23,14 +25,17 @@ object Sidedness {
/* Immutable value containers */
case object InsideOf extends Inside with Sidedness {
def opposite: Sidedness = OutsideOf
protected def value: SidenessComparison = IsInside
}
case object OutsideOf extends Outside with Sidedness {
def opposite: Sidedness = InsideOf
protected def value: SidenessComparison = IsOutside
}
case object StrictlyBetweenSides extends Inside with Outside with Sidedness {
def opposite: Sidedness = this
protected def value: SidenessComparison = IsBetween
}
@ -39,6 +44,7 @@ object Sidedness {
private val door: Door,
private val strictly: Sidedness
) extends Inside with Outside with Sidedness {
def opposite: Sidedness = this
protected def value: SidenessComparison = {
if (door.isOpen) {
IsBetween

View file

@ -4,7 +4,7 @@ package net.psforever.objects.serverobject.mount
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.{DamageResolution, DamageType}
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.etc.RadiationReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.resistance.StandardResistanceProfile
@ -38,17 +38,16 @@ class InteractWithRadiationCloudsSeatedInEntity(
*/
override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
val position = target.Position
val targetList = List(target)
//collect all projectiles in sector/range
val projectiles = sector
.projectileList
.filter { cloud =>
val definition = cloud.Definition
val radius = definition.DamageRadius
definition.radiation_cloud &&
definition.AllDamageTypes.contains(DamageType.Radiation) &&
{
val radius = definition.DamageRadius
Zone.distanceCheck(target, cloud, radius * radius)
}
Zone.allOnSameSide(cloud, definition, targetList).nonEmpty &&
Zone.distanceCheck(target, cloud, radius * radius)
}
.distinct
val notSkipped = projectiles.filterNot { t => skipTargets.contains(t.GUID) }

View file

@ -42,6 +42,11 @@ trait DamageProperties
private var charging: Option[ChargeDamage] = None
/** a destroyed mine will detonate rather than fizzle-out */
private var sympathy: Boolean = false
/** radiation clouds create independent damage-dealing areas in a zone that last for the projectile's lifespan;
* not implicate a damage type (primary or secondary or aggravated) of `Radiation` */
private var radiationCloud: Boolean = false
/** server damage is applied when comparing against sided-ness of the source and the target */
private var damageThroughWalls: Boolean = false
def UseDamage1Subtract: Boolean = useDamage1Subtract
@ -132,4 +137,18 @@ trait DamageProperties
sympathy = chain
SympatheticExplosion
}
def radiation_cloud: Boolean = radiationCloud
def radiation_cloud_=(isCloud: Boolean): Boolean = {
radiationCloud = isCloud
radiation_cloud
}
def DamageThroughWalls: Boolean = damageThroughWalls
def DamageThroughWalls_=(through: Boolean): Boolean = {
damageThroughWalls = through
DamageThroughWalls
}
}

View file

@ -51,7 +51,7 @@ import net.psforever.objects.vital.etc.ExplodingEntityReason
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.prop.DamageWithPosition
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.blockmap.BlockMap
import net.psforever.objects.zones.blockmap.{BlockMap, SectorPopulation}
import net.psforever.services.Service
import net.psforever.zones.Zones
@ -1437,6 +1437,48 @@ object Zone {
allAffectedTargets
}
/**
* na
* @param source na
* @param damageProperties na
* @param targets na
* @return na
*/
def allOnSameSide(
source: PlanetSideGameObject,
damageProperties: DamageWithPosition,
targets: List[PlanetSideServerObject with Vitality]
): List[PlanetSideServerObject with Vitality] = {
source match {
case awareSource: InteriorAware if !damageProperties.DamageThroughWalls =>
allOnSameSide(awareSource.WhichSide, targets)
case _ if !damageProperties.DamageThroughWalls =>
val sourcePosition = source.Position
targets
.sortBy(t => Vector3.DistanceSquared(sourcePosition, t.Position))
.collectFirst { case awareSource: InteriorAware => allOnSameSide(awareSource.WhichSide, targets) }
.getOrElse(targets)
case _ =>
targets
}
}
/**
* na
* @param side na
* @param targets na
* @return na
*/
def allOnSameSide(
side: Sidedness,
targets: List[PlanetSideServerObject with Vitality]
): List[PlanetSideServerObject with Vitality] = {
targets.flatMap {
case awareTarget: InteriorAware if !Sidedness.equals(side, awareTarget.WhichSide) => None
case anyTarget => Some(anyTarget)
}
}
/**
* na
* @see `DamageWithPosition`
@ -1451,9 +1493,13 @@ object Zone {
source: PlanetSideGameObject with Vitality,
damagePropertiesBySource: DamageWithPosition
): List[PlanetSideServerObject with Vitality] = {
allOnSameSide(
findAllTargets(
zone,
source,
findAllTargets(zone, source.Position, damagePropertiesBySource).filter { target => target ne source }
source.Position,
damagePropertiesBySource,
damagePropertiesBySource.DamageRadius,
getAllTargets
)
}
@ -1461,45 +1507,29 @@ object Zone {
* na
* @see `DamageWithPosition`
* @see `Zone.blockMap.sector`
* @param zone the zone in which the explosion should occur
* @param sourcePosition a custom position that is used as the origin of the explosion;
* not necessarily related to source
* @param zone the zone in which the explosion should occur
* @param source a game entity that is treated as the origin and is excluded from results
* @param damagePropertiesBySource information about the effect/damage
* @return a list of affected entities
*/
def findAllTargets(
sourcePosition: Vector3
)
(
zone: Zone,
source: PlanetSideGameObject with Vitality,
sourcePosition: Vector3,
damagePropertiesBySource: DamageWithPosition
): List[PlanetSideServerObject with Vitality] = {
allOnSameSide(
findAllTargets(
zone,
source,
findAllTargets(zone, sourcePosition, damagePropertiesBySource).filter { target => target ne source }
sourcePosition,
damagePropertiesBySource,
damagePropertiesBySource.DamageRadius,
getAllTargets
)
}
private def allOnSameSide(
source: PlanetSideGameObject with Vitality,
targets: List[PlanetSideServerObject with Vitality]
): List[PlanetSideServerObject with Vitality] = {
source match {
case awareSource: InteriorAware =>
val awareSide = awareSource.WhichSide
targets.flatMap {
case awareTarget: InteriorAware if !Sidedness.equals(awareSide, awareTarget.WhichSide) =>
None
case anyTarget =>
Some(anyTarget)
}
case _ =>
targets
}
}
/**
* na
* @see `DamageWithPosition`
@ -1507,24 +1537,66 @@ object Zone {
* @param zone the zone in which the explosion should occur
* @param sourcePosition a position that is used as the origin of the explosion
* @param damagePropertiesBySource information about the effect/damage
* @param getTargetsFromSector get this list of entities from a sector
* @return a list of affected entities
*/
def findAllTargets(
zone: Zone,
source: PlanetSideGameObject with Vitality,
sourcePosition: Vector3,
damagePropertiesBySource: DamageWithPosition
damagePropertiesBySource: DamageWithPosition,
radius: Float,
getTargetsFromSector: SectorPopulation => List[PlanetSideServerObject with Vitality]
): List[PlanetSideServerObject with Vitality] = {
val sourcePositionXY = sourcePosition.xy
val sectors = zone.blockMap.sector(sourcePositionXY, damagePropertiesBySource.DamageRadius)
allOnSameSide(
source,
damagePropertiesBySource,
findAllTargets(zone, sourcePosition, radius, getTargetsFromSector).filter { target => target ne source }
)
}
def findAllTargets(
sector: SectorPopulation,
source: PlanetSideGameObject with Vitality,
damagePropertiesBySource: DamageWithPosition,
getTargetsFromSector: SectorPopulation => List[PlanetSideServerObject with Vitality]
): List[PlanetSideServerObject with Vitality] = {
allOnSameSide(
source,
damagePropertiesBySource,
getTargetsFromSector(sector)
)
}
/**
* na
* @see `DamageWithPosition`
* @see `Zone.blockMap.sector`
* @param zone the zone in which the explosion should occur
* @param sourcePosition a position that is used as the origin of the explosion
* @param radius idistance
* @param getTargetsFromSector get this list of entities from a sector
* @return a list of affected entities
*/
def findAllTargets(
zone: Zone,
sourcePosition: Vector3,
radius: Float,
getTargetsFromSector: SectorPopulation => List[PlanetSideServerObject with Vitality]
): List[PlanetSideServerObject with Vitality] = {
getTargetsFromSector(zone.blockMap.sector(sourcePosition.xy, radius))
}
def getAllTargets(sector: SectorPopulation): List[PlanetSideServerObject with Vitality] = {
//collect all targets that can be damaged
//players
val playerTargets = sectors.livePlayerList.filterNot { _.VehicleSeated.nonEmpty }
val playerTargets = sector.livePlayerList.filterNot { _.VehicleSeated.nonEmpty }
//vehicles
val vehicleTargets = sectors.vehicleList.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
val vehicleTargets = sector.vehicleList.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
//deployables
val deployableTargets = sectors.deployableList.filterNot { _.Destroyed }
val deployableTargets = sector.deployableList.filterNot { _.Destroyed }
//amenities
val soiTargets = sectors.amenityList.collect { case amenity: Vitality if !amenity.Destroyed => amenity }
val soiTargets = sector.amenityList.collect { case amenity: Vitality if !amenity.Destroyed => amenity }
//altogether ...
playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets
}

View file

@ -101,6 +101,8 @@ final case class Vector3(x: Float, y: Float, z: Float) {
object Vector3 {
final val Zero: Vector3 = Vector3(0f, 0f, 0f)
def unapply(v: Vector3): Option[(Float, Float, Float)] = Some((v.x, v.y, v.z))
private def closeToInsignificance(d: Float, epsilon: Float = 10f): Float = {
val ulp = math.ulp(epsilon)
math.signum(d) match {