mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
The Blockmap (#852)
* separating geometry classes * 2d geometry; retirement of the *3D suffix * makings of an early block map datastructure * entities in a zone - players, corpses, vehicles, deployables, ground clutter, and buildings - divided between sectors of the zone upon creation, management, or mounting; superfluous messages to keep track of blockmap state, for now * trait for entities to be added to the blockmap; internal entity data keeps track of current blockmap sector information; calls to add/remove/update functions changed * modified pieces of environment into an entities that can be added to a block map and have a countable bounding region; fixes for vehicle control seat occcupant collection; fix for squad individual callback references (original issue still remains?) * introduced the block map into various existijng game calculationa where target selection can be reduced by its probing * he_mines and jammer_mines now trigger if a valid target is detected at the initial point of deploy; they also trigger later, after a valid target has moved into the arming range of the mine * conversion of interactions with zone into a queued, periodic set of tasks * explosive deployable control -> mine deployable control * tests repaired and all tests working * mostly comments and documentation * amenities are now represented on the blockmap
This commit is contained in:
parent
8bf0c4cbff
commit
3966b0264d
|
|
@ -6,6 +6,7 @@ import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware, typed}
|
|||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import net.psforever.actors.net.MiddlewareActor
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.login.WorldSession._
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.avatar._
|
||||
|
|
@ -46,7 +47,8 @@ import net.psforever.objects.vital.base._
|
|||
import net.psforever.objects.vital.etc.ExplodingEntityReason
|
||||
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
||||
import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning}
|
||||
import net.psforever.objects.zones._
|
||||
import net.psforever.objects.zones.blockmap.{BlockMap, BlockMapEntity}
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
|
||||
import net.psforever.packet.game.objectcreate._
|
||||
|
|
@ -2101,6 +2103,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
)
|
||||
sendResponse(ObjectDeleteMessage(kguid, 0))
|
||||
|
||||
case AvatarResponse.KitNotUsed(_, "") =>
|
||||
kitToBeUsed = None
|
||||
|
||||
case AvatarResponse.KitNotUsed(_, msg) =>
|
||||
kitToBeUsed = None
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", msg, None))
|
||||
|
|
@ -2469,9 +2474,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, 0, 0, zang))
|
||||
)
|
||||
} else {
|
||||
log.info(s"${player.Name} is prepped for dropping")
|
||||
//get ready for orbital drop
|
||||
DismountAction(tplayer, obj, seat_num)
|
||||
log.info(s"${player.Name} is prepped for dropping")
|
||||
continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it
|
||||
//DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
player.Name,
|
||||
|
|
@ -3748,6 +3754,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
) =>
|
||||
persist()
|
||||
turnCounterFunc(avatar_guid)
|
||||
updateBlockMap(player, continent, pos)
|
||||
val isMoving = WorldEntity.isMoving(vel)
|
||||
val isMovingPlus = isMoving || is_jumping || jump_thrust
|
||||
if (isMovingPlus) {
|
||||
|
|
@ -3827,7 +3834,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
if (player.death_by == -1) {
|
||||
KickedByAdministration()
|
||||
}
|
||||
player.zoneInteraction()
|
||||
player.zoneInteractions()
|
||||
|
||||
case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) =>
|
||||
//the majority of the following check retrieves information to determine if we are in control of the child
|
||||
|
|
@ -3883,6 +3890,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
//we're driving the vehicle
|
||||
persist()
|
||||
turnCounterFunc(player.GUID)
|
||||
if (obj.MountedIn.isEmpty) {
|
||||
updateBlockMap(obj, continent, pos)
|
||||
}
|
||||
val seat = obj.Seats(0)
|
||||
player.Position = pos //convenient
|
||||
if (obj.WeaponControlledFromSeat(0).isEmpty) {
|
||||
|
|
@ -3926,7 +3936,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
)
|
||||
)
|
||||
updateSquad()
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
case (None, _) =>
|
||||
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
|
||||
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
|
||||
|
|
@ -5283,7 +5293,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
SpecialEmp.emp,
|
||||
SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos),
|
||||
SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction),
|
||||
SpecialEmp.findAllBoomers
|
||||
SpecialEmp.findAllBoomers(profile.DamageRadius)
|
||||
)
|
||||
}
|
||||
if (profile.ExistsOnRemoteClients && projectile.HasGUID) {
|
||||
|
|
@ -6910,7 +6920,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
case _ =>
|
||||
vehicle.MountedIn = None
|
||||
}
|
||||
vehicle.allowZoneEnvironmentInteractions = true
|
||||
vehicle.allowInteraction = true
|
||||
data
|
||||
} else {
|
||||
//passenger
|
||||
|
|
@ -7747,7 +7757,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
}
|
||||
|
||||
/**
|
||||
* Common activities/procedure when a player dismounts a valid object.
|
||||
* Common activities/procedure when a player dismounts a valid mountable object.
|
||||
* @param tplayer the player
|
||||
* @param obj the mountable object
|
||||
* @param seatNum the mount out of which which the player is disembarking
|
||||
|
|
@ -8236,7 +8246,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
)
|
||||
}
|
||||
//
|
||||
vehicle.allowZoneEnvironmentInteractions = false
|
||||
vehicle.allowInteraction = false
|
||||
if (!zoneReload && zoneId == continent.id) {
|
||||
if (vehicle.Definition == GlobalDefinitions.droppod) {
|
||||
//instant action droppod in the same zone
|
||||
|
|
@ -9149,6 +9159,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
}
|
||||
}
|
||||
|
||||
def updateBlockMap(target: BlockMapEntity, zone: Zone, newCoords: Vector3): Unit = {
|
||||
target.blockMapEntry match {
|
||||
case Some(entry) =>
|
||||
if (BlockMap.findSectorIndices(continent.blockMap, newCoords, entry.range).toSet.equals(entry.sectors)) {
|
||||
target.updateBlockMapEntry(newCoords) //soft update
|
||||
} else {
|
||||
zone.actor ! ZoneActor.UpdateBlockMap(target, newCoords) //hard update
|
||||
}
|
||||
case None => ;
|
||||
}
|
||||
}
|
||||
|
||||
def failWithError(error: String) = {
|
||||
log.error(error)
|
||||
middlewareActor ! MiddlewareActor.Teardown()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import net.psforever.objects.ce.Deployable
|
|||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.objects.serverobject.structures.{Building, StructureType}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
|
||||
import net.psforever.objects.{ConstructionItem, Player, Vehicle}
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||
|
||||
|
|
@ -53,6 +54,14 @@ object ZoneActor {
|
|||
|
||||
final case class DespawnVehicle(vehicle: Vehicle) extends Command
|
||||
|
||||
final case class AddToBlockMap(target: BlockMapEntity, toPosition: Vector3) extends Command
|
||||
|
||||
final case class UpdateBlockMap(target: BlockMapEntity, toPosition: Vector3) extends Command
|
||||
|
||||
final case class RemoveFromBlockMap(target: BlockMapEntity) extends Command
|
||||
|
||||
final case class ChangedSectors(addedTo: SectorGroup, removedFrom: SectorGroup)
|
||||
|
||||
final case class HotSpotActivity(defender: SourceEntry, attacker: SourceEntry, location: Vector3) extends Command
|
||||
|
||||
// TODO remove
|
||||
|
|
@ -124,6 +133,15 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
|
|||
case DespawnVehicle(vehicle) =>
|
||||
zone.Transport ! Zone.Vehicle.Despawn(vehicle)
|
||||
|
||||
case AddToBlockMap(target, toPosition) =>
|
||||
zone.blockMap.addTo(target, toPosition)
|
||||
|
||||
case UpdateBlockMap(target, toPosition) =>
|
||||
zone.blockMap.move(target, toPosition)
|
||||
|
||||
case RemoveFromBlockMap(target) =>
|
||||
zone.blockMap.removeFrom(target)
|
||||
|
||||
case HotSpotActivity(defender, attacker, location) =>
|
||||
zone.Activity ! Zone.HotSpot.Activity(defender, attacker, location)
|
||||
|
||||
|
|
|
|||
|
|
@ -50,9 +50,8 @@ object BoomerDeployableDefinition {
|
|||
class BoomerDeployableControl(mine: BoomerDeployable)
|
||||
extends ExplosiveDeployableControl(mine) {
|
||||
|
||||
override def receive: Receive =
|
||||
deployableBehavior
|
||||
.orElse(takesDamage)
|
||||
def receive: Receive =
|
||||
commonMineBehavior
|
||||
.orElse {
|
||||
case CommonMessages.Use(player, Some(trigger: BoomerTrigger)) if mine.Trigger.contains(trigger) =>
|
||||
// the trigger damages the mine, which sets it off, which causes an explosion
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
// Copyright (c) 2018 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import akka.actor.{Actor, ActorContext, Props}
|
||||
import akka.actor.{Actor, ActorContext, ActorRef, Props}
|
||||
import net.psforever.objects.ballistics.{DeployableSource, PlayerSource, SourceEntry}
|
||||
import net.psforever.objects.ce._
|
||||
import net.psforever.objects.definition.DeployableDefinition
|
||||
import net.psforever.objects.definition.{DeployableDefinition, ExoSuitDefinition}
|
||||
import net.psforever.objects.definition.converter.SmallDeployableConverter
|
||||
import net.psforever.objects.equipment.JammableUnit
|
||||
import net.psforever.objects.geometry.Geometry3D
|
||||
import net.psforever.objects.geometry.d3.VolumetricGeometry
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
|
||||
import net.psforever.objects.serverobject.damage.Damageable.Target
|
||||
import net.psforever.objects.vital.etc.TrippedMineReason
|
||||
import net.psforever.objects.vital.resolution.ResolutionCalculations.Output
|
||||
import net.psforever.objects.vital.{SimpleResolutions, Vitality}
|
||||
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
|
||||
import net.psforever.types.{ExoSuitType, Vector3}
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
|
|
@ -30,7 +32,12 @@ class ExplosiveDeployable(cdef: ExplosiveDeployableDefinition)
|
|||
override def Definition: ExplosiveDeployableDefinition = cdef
|
||||
}
|
||||
|
||||
class ExplosiveDeployableDefinition(private val objectId: Int) extends DeployableDefinition(objectId) {
|
||||
object ExplosiveDeployable {
|
||||
final case class TriggeredBy(obj: PlanetSideServerObject)
|
||||
}
|
||||
|
||||
class ExplosiveDeployableDefinition(private val objectId: Int)
|
||||
extends DeployableDefinition(objectId) {
|
||||
Name = "explosive_deployable"
|
||||
DeployCategory = DeployableCategory.Mines
|
||||
Model = SimpleResolutions.calculate
|
||||
|
|
@ -38,6 +45,8 @@ class ExplosiveDeployableDefinition(private val objectId: Int) extends Deployabl
|
|||
|
||||
private var detonateOnJamming: Boolean = true
|
||||
|
||||
var triggerRadius: Float = 0f
|
||||
|
||||
def DetonateOnJamming: Boolean = detonateOnJamming
|
||||
|
||||
def DetonateOnJamming_=(detonate: Boolean): Boolean = {
|
||||
|
|
@ -47,7 +56,7 @@ class ExplosiveDeployableDefinition(private val objectId: Int) extends Deployabl
|
|||
|
||||
override def Initialize(obj: Deployable, context: ActorContext) = {
|
||||
obj.Actor =
|
||||
context.actorOf(Props(classOf[ExplosiveDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
|
||||
context.actorOf(Props(classOf[MineDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +66,7 @@ object ExplosiveDeployableDefinition {
|
|||
}
|
||||
}
|
||||
|
||||
class ExplosiveDeployableControl(mine: ExplosiveDeployable)
|
||||
abstract class ExplosiveDeployableControl(mine: ExplosiveDeployable)
|
||||
extends Actor
|
||||
with DeployableBehavior
|
||||
with Damageable {
|
||||
|
|
@ -69,12 +78,9 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable)
|
|||
deployableBehaviorPostStop()
|
||||
}
|
||||
|
||||
def receive: Receive =
|
||||
def commonMineBehavior: Receive =
|
||||
deployableBehavior
|
||||
.orElse(takesDamage)
|
||||
.orElse {
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
override protected def PerformDamage(
|
||||
target: Target,
|
||||
|
|
@ -224,7 +230,15 @@ object ExplosiveDeployableControl {
|
|||
* @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 = {
|
||||
def detectTarget(
|
||||
g1: VolumetricGeometry,
|
||||
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)
|
||||
|
|
@ -238,3 +252,109 @@ object ExplosiveDeployableControl {
|
|||
) <= maxDistance
|
||||
}
|
||||
}
|
||||
|
||||
class MineDeployableControl(mine: ExplosiveDeployable)
|
||||
extends ExplosiveDeployableControl(mine) {
|
||||
|
||||
def receive: Receive =
|
||||
commonMineBehavior
|
||||
.orElse {
|
||||
case ExplosiveDeployable.TriggeredBy(obj) =>
|
||||
setTriggered(Some(obj), delay = 200)
|
||||
|
||||
case MineDeployableControl.Triggered() =>
|
||||
explodes(testForTriggeringTarget(
|
||||
mine,
|
||||
mine.Definition.innateDamage.map { _.DamageRadius }.getOrElse(mine.Definition.triggerRadius)
|
||||
))
|
||||
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
override def finalizeDeployable(callback: ActorRef): Unit = {
|
||||
super.finalizeDeployable(callback)
|
||||
//initial triggering upon build
|
||||
setTriggered(testForTriggeringTarget(mine, mine.Definition.triggerRadius), delay = 1000)
|
||||
}
|
||||
|
||||
def testForTriggeringTarget(mine: ExplosiveDeployable, range: Float): Option[PlanetSideServerObject] = {
|
||||
val position = mine.Position
|
||||
val faction = mine.Faction
|
||||
val range2 = range * range
|
||||
val sector = mine.Zone.blockMap.sector(position, range)
|
||||
(sector.livePlayerList ++ sector.vehicleList)
|
||||
.find { thing => thing.Faction != faction && Vector3.DistanceSquared(thing.Position, position) < range2 }
|
||||
}
|
||||
|
||||
def setTriggered(instigator: Option[PlanetSideServerObject], delay: Long): Unit = {
|
||||
instigator match {
|
||||
case Some(_) if isConstructed.contains(true) && setup.isCancelled =>
|
||||
//re-use the setup timer here
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
setup = context.system.scheduler.scheduleOnce(delay milliseconds, self, MineDeployableControl.Triggered())
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
|
||||
def explodes(instigator: Option[PlanetSideServerObject]): Unit = {
|
||||
instigator match {
|
||||
case Some(_) =>
|
||||
//explosion
|
||||
mine.Destroyed = true
|
||||
ExplosiveDeployableControl.DamageResolution(
|
||||
mine,
|
||||
DamageInteraction(
|
||||
SourceEntry(mine),
|
||||
MineDeployableControl.trippedMineReason(mine),
|
||||
mine.Position
|
||||
).calculate()(mine),
|
||||
damage = 0
|
||||
)
|
||||
case None =>
|
||||
//reset
|
||||
setup = Default.Cancellable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object MineDeployableControl {
|
||||
private case class Triggered()
|
||||
|
||||
def trippedMineReason(mine: ExplosiveDeployable): TrippedMineReason = {
|
||||
val deployableSource = DeployableSource(mine)
|
||||
val blame = mine.OwnerName match {
|
||||
case Some(name) =>
|
||||
val(charId, exosuit, seated): (Long, ExoSuitType.Value, Boolean) = mine.Zone
|
||||
.LivePlayers
|
||||
.find { _.Name.equals(name) } match {
|
||||
case Some(player) =>
|
||||
//if the owner is alive in the same zone as the mine, use data from their body to create the source
|
||||
(player.CharId, player.ExoSuit, player.VehicleSeated.nonEmpty)
|
||||
case None =>
|
||||
//if the owner is as dead as a corpse or is not in the same zone as the mine, use defaults
|
||||
(0L, ExoSuitType.Standard, false)
|
||||
}
|
||||
val faction = mine.Faction
|
||||
PlayerSource(
|
||||
name,
|
||||
charId,
|
||||
GlobalDefinitions.avatar,
|
||||
mine.Faction,
|
||||
exosuit,
|
||||
seated,
|
||||
100,
|
||||
100,
|
||||
mine.Position,
|
||||
Vector3.Zero,
|
||||
None,
|
||||
crouching = false,
|
||||
jumping = false,
|
||||
ExoSuitDefinition.Select(exosuit, faction)
|
||||
)
|
||||
case None =>
|
||||
//credit where credit is due
|
||||
deployableSource
|
||||
}
|
||||
TrippedMineReason(deployableSource, blame)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7082,6 +7082,7 @@ object GlobalDefinitions {
|
|||
he_mine.DeployTime = Duration.create(1000, "ms")
|
||||
he_mine.deployAnimation = DeployAnimation.Standard
|
||||
he_mine.explodes = true
|
||||
he_mine.triggerRadius = 3f
|
||||
he_mine.innateDamage = new DamageWithPosition {
|
||||
CausesDamageType = DamageType.Splash
|
||||
SympatheticExplosion = true
|
||||
|
|
@ -7105,6 +7106,39 @@ object GlobalDefinitions {
|
|||
jammer_mine.DeployTime = Duration.create(1000, "ms")
|
||||
jammer_mine.deployAnimation = DeployAnimation.Standard
|
||||
jammer_mine.DetonateOnJamming = false
|
||||
jammer_mine.explodes = true
|
||||
jammer_mine.triggerRadius = 3f
|
||||
jammer_mine.innateDamage = new DamageWithPosition {
|
||||
CausesDamageType = DamageType.Splash
|
||||
Damage0 = 0
|
||||
DamageRadius = 10f
|
||||
DamageAtEdge = 1.0f
|
||||
AdditionalEffect = true
|
||||
JammedEffectDuration += TargetValidation(
|
||||
EffectTarget.Category.Player,
|
||||
EffectTarget.Validation.Player
|
||||
) -> 1000
|
||||
JammedEffectDuration += TargetValidation(
|
||||
EffectTarget.Category.Vehicle,
|
||||
EffectTarget.Validation.AMS
|
||||
) -> 5000
|
||||
JammedEffectDuration += TargetValidation(
|
||||
EffectTarget.Category.Deployable,
|
||||
EffectTarget.Validation.MotionSensor
|
||||
) -> 30000
|
||||
JammedEffectDuration += TargetValidation(
|
||||
EffectTarget.Category.Deployable,
|
||||
EffectTarget.Validation.Spitfire
|
||||
) -> 30000
|
||||
JammedEffectDuration += TargetValidation(
|
||||
EffectTarget.Category.Turret,
|
||||
EffectTarget.Validation.Turret
|
||||
) -> 30000
|
||||
JammedEffectDuration += TargetValidation(
|
||||
EffectTarget.Category.Vehicle,
|
||||
EffectTarget.Validation.VehicleNotAMS
|
||||
) -> 10000
|
||||
}
|
||||
jammer_mine.Geometry = mine
|
||||
|
||||
spitfire_turret.Name = "spitfire_turret"
|
||||
|
|
|
|||
|
|
@ -2,19 +2,20 @@
|
|||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
|
||||
import net.psforever.objects.ce.Deployable
|
||||
import net.psforever.objects.ce.{Deployable, InteractWithMines}
|
||||
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
|
||||
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
|
||||
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.aura.AuraContainer
|
||||
import net.psforever.objects.serverobject.environment.InteractsWithZoneEnvironment
|
||||
import net.psforever.objects.serverobject.environment.InteractWithEnvironment
|
||||
import net.psforever.objects.vital.resistance.ResistanceProfile
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
||||
import net.psforever.objects.zones.{ZoneAware, Zoning}
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
import net.psforever.objects.zones.{InteractsWithZone, ZoneAware, Zoning}
|
||||
import net.psforever.types.{PlanetSideGUID, _}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
|
@ -22,7 +23,8 @@ import scala.util.{Success, Try}
|
|||
|
||||
class Player(var avatar: Avatar)
|
||||
extends PlanetSideServerObject
|
||||
with InteractsWithZoneEnvironment
|
||||
with BlockMapEntity
|
||||
with InteractsWithZone
|
||||
with FactionAffinity
|
||||
with Vitality
|
||||
with ResistanceProfile
|
||||
|
|
@ -30,6 +32,9 @@ class Player(var avatar: Avatar)
|
|||
with JammableUnit
|
||||
with ZoneAware
|
||||
with AuraContainer {
|
||||
interaction(new InteractWithEnvironment())
|
||||
interaction(new InteractWithMines(range = 10))
|
||||
|
||||
private var backpack: Boolean = false
|
||||
private var released: Boolean = false
|
||||
private var armor: Int = 0
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ object SpecialEmp {
|
|||
* A sort of `SpecialEmp` that only affects deployed boomer explosives
|
||||
* must find affected deployed boomer explosive entities.
|
||||
* @see `BoomerDeployable`
|
||||
* @param range the distance to search for applicable sector
|
||||
* @param zone the zone in which to search
|
||||
* @param obj a game entity that is excluded from results
|
||||
* @param properties information about the effect/damage
|
||||
|
|
@ -131,10 +132,17 @@ object SpecialEmp {
|
|||
* since only boomer explosives are returned, this second list can be ignored
|
||||
*/
|
||||
def findAllBoomers(
|
||||
range: Float
|
||||
)
|
||||
(
|
||||
zone: Zone,
|
||||
obj: PlanetSideGameObject with FactionAffinity with Vitality,
|
||||
properties: DamageWithPosition
|
||||
): List[PlanetSideServerObject with Vitality] = {
|
||||
zone.DeployableList.collect { case o: BoomerDeployable if !o.Destroyed && (o ne obj) => o }
|
||||
zone
|
||||
.blockMap
|
||||
.sector(obj.Position, range)
|
||||
.deployableList
|
||||
.collect { case o: BoomerDeployable if !o.Destroyed && (o ne obj) => o }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.ce.InteractWithMines
|
||||
import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition}
|
||||
import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot, JammableUnit}
|
||||
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile}
|
||||
|
|
@ -9,13 +10,15 @@ import net.psforever.objects.serverobject.PlanetSideServerObject
|
|||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.aura.AuraContainer
|
||||
import net.psforever.objects.serverobject.deploy.Deployment
|
||||
import net.psforever.objects.serverobject.environment.InteractsWithZoneEnvironment
|
||||
import net.psforever.objects.serverobject.environment.InteractWithEnvironment
|
||||
import net.psforever.objects.serverobject.hackable.Hackable
|
||||
import net.psforever.objects.serverobject.structures.AmenityOwner
|
||||
import net.psforever.objects.vehicles._
|
||||
import net.psforever.objects.vital.resistance.StandardResistanceProfile
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
||||
import net.psforever.objects.zones.InteractsWithZone
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
|
@ -71,8 +74,9 @@ import scala.util.{Success, Try}
|
|||
*/
|
||||
class Vehicle(private val vehicleDef: VehicleDefinition)
|
||||
extends AmenityOwner
|
||||
with BlockMapEntity
|
||||
with MountableWeapons
|
||||
with InteractsWithZoneEnvironment
|
||||
with InteractsWithZone
|
||||
with Hackable
|
||||
with FactionAffinity
|
||||
with Deployment
|
||||
|
|
@ -83,6 +87,9 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
|
|||
with CommonNtuContainer
|
||||
with Container
|
||||
with AuraContainer {
|
||||
interaction(new InteractWithEnvironment())
|
||||
interaction(new InteractWithMines(range = 30))
|
||||
|
||||
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
private var shields: Int = 0
|
||||
private var decal: Int = 0
|
||||
|
|
|
|||
|
|
@ -293,12 +293,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
|
|||
player.Name,
|
||||
AvatarAction.UseKit(kguid, kdef.ObjectId)
|
||||
)
|
||||
case None if msg.length > 0 =>
|
||||
case _ =>
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||
player.Name,
|
||||
AvatarAction.KitNotUsed(kit.GUID, msg)
|
||||
)
|
||||
case None => ;
|
||||
}
|
||||
|
||||
case PlayerControl.SetExoSuit(exosuit: ExoSuitType.Value, subtype: Int) =>
|
||||
|
|
@ -1217,7 +1216,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
|
|||
if (player.Health > 0) {
|
||||
StartAuraEffect(Aura.Fire, duration = 1250L) //burn
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
interactionTimer = context.system.scheduler.scheduleOnce(delay = 250 milliseconds, self, InteractWithEnvironment(player, body, None))
|
||||
interactionTimer = context.system.scheduler.scheduleOnce(delay = 250 milliseconds, self, InteractingWithEnvironment(player, body, None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1258,7 +1257,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
|
|||
interactionTimer = context.system.scheduler.scheduleOnce(
|
||||
delay = 250 milliseconds,
|
||||
self,
|
||||
InteractWithEnvironment(player, body, None)
|
||||
InteractingWithEnvironment(player, body, None)
|
||||
)
|
||||
case _ => ;
|
||||
//something configured incorrectly; no need to keep checking
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ import net.psforever.objects.serverobject.affinity.FactionAffinity
|
|||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
||||
import net.psforever.objects.zones.ZoneAware
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
import net.psforever.packet.game.DeployableIcon
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
trait BaseDeployable
|
||||
extends PlanetSideServerObject
|
||||
with FactionAffinity
|
||||
with BlockMapEntity
|
||||
with Vitality
|
||||
with OwnableByPlayer
|
||||
with ZoneAware {
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ trait DeployableBehavior {
|
|||
*/
|
||||
def finalizeDeployable(callback: ActorRef): Unit = {
|
||||
setup.cancel()
|
||||
setup = Default.Cancellable
|
||||
constructed = Some(true)
|
||||
val obj = DeployableObject
|
||||
val zone = obj.Zone
|
||||
|
|
@ -267,6 +268,7 @@ trait DeployableBehavior {
|
|||
val duration = time.getOrElse(Deployable.cleanup)
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
setup.cancel()
|
||||
setup = Default.Cancellable
|
||||
decay.cancel()
|
||||
decay = context.system.scheduler.scheduleOnce(duration, self, DeployableBehavior.FinalizeElimination())
|
||||
}
|
||||
|
|
@ -288,6 +290,7 @@ trait DeployableBehavior {
|
|||
*/
|
||||
def dismissDeployable(): Unit = {
|
||||
setup.cancel()
|
||||
setup = Default.Cancellable
|
||||
decay.cancel()
|
||||
val obj = DeployableObject
|
||||
val zone = obj.Zone
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.ce
|
||||
|
||||
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction}
|
||||
import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
/**
|
||||
* This game entity may infrequently test whether it may interact with game world deployable extra-territorial munitions.
|
||||
* "Interact", here, is a graceful word for "trample upon" and the consequence should be an explosion
|
||||
* and maybe death.
|
||||
*/
|
||||
class InteractWithMines(range: Float)
|
||||
extends ZoneInteraction {
|
||||
/**
|
||||
* mines that, though detected, are skipped from being alerted;
|
||||
* in between interaction tests, a memory of the mines that were messaged last test are retained and
|
||||
* are excluded from being messaged this test;
|
||||
* mines that are detected a second time are cleared from the list and are available to be messaged in the next test
|
||||
*/
|
||||
private var skipTargets: List[PlanetSideGUID] = List()
|
||||
|
||||
/**
|
||||
* Trample upon active mines in our current detection sector and alert those mines.
|
||||
* @param target the fixed element in this test
|
||||
*/
|
||||
def interaction(target: InteractsWithZone): Unit = {
|
||||
val faction = target.Faction
|
||||
val targets = target.Zone.blockMap
|
||||
.sector(target.Position, range)
|
||||
.deployableList
|
||||
.filter {
|
||||
case _: BoomerDeployable => false //boomers are specific types of ExplosiveDeployable but do not count here
|
||||
case ex: ExplosiveDeployable => ex.Faction != faction &&
|
||||
Zone.distanceCheck(target, ex, ex.Definition.triggerRadius)
|
||||
case _ => false
|
||||
}
|
||||
val notSkipped = targets.filterNot { t => skipTargets.contains(t.GUID) }
|
||||
skipTargets = notSkipped.map { _.GUID }
|
||||
notSkipped.foreach { t =>
|
||||
t.Actor ! ExplosiveDeployable.TriggeredBy(target)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mines can not be un-exploded or un-alerted.
|
||||
* All that can be done is blanking our retained previous messaging targets.
|
||||
* @param target the fixed element in this test
|
||||
*/
|
||||
def resetInteraction(target: InteractsWithZone): Unit = {
|
||||
skipTargets = List()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@ 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.objects.geometry.GeometryForm
|
||||
import net.psforever.objects.geometry.d3.VolumetricGeometry
|
||||
import net.psforever.types.OxygenState
|
||||
|
||||
/**
|
||||
|
|
@ -86,15 +87,15 @@ abstract class ObjectDefinition(private val objectId: Int) extends BasicDefiniti
|
|||
ServerSplashTargetsCentroid
|
||||
}
|
||||
|
||||
private var serverGeometry: Any => Geometry3D = GeometryForm.representByPoint()
|
||||
private var serverGeometry: Any => VolumetricGeometry = GeometryForm.representByPoint()
|
||||
|
||||
def Geometry: Any => Geometry3D = if (ServerSplashTargetsCentroid) {
|
||||
def Geometry: Any => VolumetricGeometry = if (ServerSplashTargetsCentroid) {
|
||||
GeometryForm.representByPoint()
|
||||
} else {
|
||||
serverGeometry
|
||||
}
|
||||
|
||||
def Geometry_=(func: Any => Geometry3D): Any => Geometry3D = {
|
||||
def Geometry_=(func: Any => VolumetricGeometry): Any => VolumetricGeometry = {
|
||||
serverGeometry = func
|
||||
Geometry
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import net.psforever.objects.PlanetSideGameObject
|
|||
import net.psforever.objects.definition.EquipmentDefinition
|
||||
import net.psforever.objects.inventory.InventoryTile
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
/**
|
||||
|
|
@ -14,7 +15,10 @@ import net.psforever.types.PlanetSideEmpire
|
|||
* and, special carried (like a lattice logic unit);
|
||||
* and, dropped on the ground in the game world and render where it was deposited.
|
||||
*/
|
||||
abstract class Equipment extends PlanetSideGameObject with FactionAffinity {
|
||||
abstract class Equipment
|
||||
extends PlanetSideGameObject
|
||||
with FactionAffinity
|
||||
with BlockMapEntity {
|
||||
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
|
||||
def Faction: PlanetSideEmpire.Value = faction
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry
|
||||
|
||||
import enumeratum.{Enum, EnumEntry}
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* For geometric entities that exist only in a given cardinal direction, using a plane of reference,
|
||||
* the plane of reference that maps the values to a cordinate vector
|
||||
* @see `Vector3D`
|
||||
* @param direction the coordinate vector of "relative up"
|
||||
*/
|
||||
sealed abstract class AxisAlignment(direction: Vector3) extends EnumEntry {
|
||||
/**
|
||||
* Project one vector as a vector that can be represented in this coordinate axis.
|
||||
* @param v the original vector
|
||||
* @return the projected vector
|
||||
*/
|
||||
def asVector3(v: Vector3): Vector3
|
||||
}
|
||||
|
||||
/**
|
||||
* For geometric entities that exist in a two-dimensional context.
|
||||
* @param direction the coordinate vector of "relative up"
|
||||
*/
|
||||
sealed abstract class AxisAlignment2D(direction: Vector3) extends AxisAlignment(direction) {
|
||||
/**
|
||||
* Project two values as a vector that can be represented in this coordinate axis.
|
||||
* @param a the first value
|
||||
* @param b the second value
|
||||
* @return the projected vector
|
||||
*/
|
||||
def asVector3(a: Float, b: Float): Vector3
|
||||
}
|
||||
|
||||
/**
|
||||
* For geometric entities that exist in a three-dimensional context.
|
||||
* More ceremonial, than anything else.
|
||||
*/
|
||||
sealed abstract class AxisAlignment3D extends AxisAlignment(Vector3.Zero) {
|
||||
/**
|
||||
* Project three values as a vector that can be represented in this coordinate axis.
|
||||
* @param a the first value
|
||||
* @param b the second value
|
||||
* @param c the third value
|
||||
* @return the projected vector
|
||||
*/
|
||||
def asVector3(a: Float, b: Float, c: Float): Vector3
|
||||
}
|
||||
|
||||
object AxisAlignment extends Enum[AxisAlignment] {
|
||||
val values: IndexedSeq[AxisAlignment] = findValues
|
||||
|
||||
/**
|
||||
* Geometric entities in the XY-axis.
|
||||
* Coordinates are x- and y-; up is the z-axis.
|
||||
*/
|
||||
case object XY extends AxisAlignment2D(Vector3(0,0,1)) {
|
||||
def asVector3(v: Vector3): Vector3 = v.xy
|
||||
|
||||
def asVector3(a: Float, b: Float): Vector3 = Vector3(a,b,0)
|
||||
}
|
||||
/**
|
||||
* Geometric entities in the YZ-axis.
|
||||
* Coordinates are y- and z-; up is the x-axis.
|
||||
*/
|
||||
case object YZ extends AxisAlignment2D(Vector3(1,0,0)) {
|
||||
def asVector3(v: Vector3): Vector3 = Vector3(0,v.y,v.z)
|
||||
|
||||
def asVector3(a: Float, b: Float): Vector3 = Vector3(0,a,b)
|
||||
}
|
||||
/**
|
||||
* Geometric entities in the XZ-axis.
|
||||
* Coordinates are x- and z-; up is the y-axis.
|
||||
*/
|
||||
case object XZ extends AxisAlignment2D(Vector3(0,1,0)) {
|
||||
def asVector3(v: Vector3): Vector3 = Vector3(v.x,0,v.z)
|
||||
|
||||
def asVector3(a: Float, b: Float): Vector3 = Vector3(a,0,b)
|
||||
}
|
||||
/**
|
||||
* For geometric entities that exist in a three-dimensional context.
|
||||
* More ceremonial, than anything else.
|
||||
*/
|
||||
case object Free extends AxisAlignment3D() {
|
||||
def asVector3(v: Vector3): Vector3 = v
|
||||
|
||||
def asVector3(a: Float, b: Float, c: Float): Vector3 = Vector3(a,b,c)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,28 +3,73 @@ package net.psforever.objects.geometry
|
|||
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* Calculation support for the geometric code.
|
||||
*/
|
||||
object Geometry {
|
||||
/**
|
||||
* Are two `Float` numbers equal enough to be considered equal?
|
||||
* @param value1 the first value
|
||||
* @param value2 the second value
|
||||
* @param off how far the number can be inequal from each other
|
||||
* @return `true`, if the two `Float` numbers are close enough to be considered equal;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def equalFloats(value1: Float, value2: Float, off: Float = 0.001f): Boolean = {
|
||||
val diff = value1 - value2
|
||||
if (diff >= 0) diff <= off else diff > -off
|
||||
}
|
||||
|
||||
/**
|
||||
* Are two `Vector3` entities equal enough to be considered equal?
|
||||
* @see `equalFloats`
|
||||
* @param value1 the first coordinate triple
|
||||
* @param value2 the second coordinate triple
|
||||
* @param off how far any individual coordinate can be inequal from each other
|
||||
* @return `true`, if the two `Vector3` entities are close enough to be considered equal;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Are two `Vector3` entities equal enough to be considered equal?
|
||||
* @see `equalFloats`
|
||||
* @param value1 the first coordinate triple
|
||||
* @param value2 the second coordinate triple
|
||||
* @param off how far each individual coordinate can be inequal from the other
|
||||
* @return `true`, if the two `Vector3` entities are close enough to be considered equal;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def equalVectors(value1: Vector3, value2: Vector3, off: Vector3): Boolean = {
|
||||
equalFloats(value1.x, value2.x, off.x) &&
|
||||
equalFloats(value1.y, value2.y, off.y) &&
|
||||
equalFloats(value1.z, value2.z, off.z)
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the value close enough to be zero to be equivalently replaceable with zero?
|
||||
* @see `math.abs`
|
||||
* @see `math.signum`
|
||||
* @see `math.ulp`
|
||||
* @see `Vector3.closeToInsignificance`
|
||||
* @param d the original number
|
||||
* @param epsilon how far from zero the value is allowed to stray
|
||||
* @return the original number, or zero
|
||||
*/
|
||||
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
|
||||
if (p < ulp || d > ulp) 0f else d
|
||||
case _ =>
|
||||
val p = math.abs(d - d.toInt)
|
||||
if (p < ulp || d < ulp) d - p else d
|
||||
if (p < ulp || d < ulp) 0f else d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
package net.psforever.objects.geometry
|
||||
|
||||
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry}
|
||||
import net.psforever.objects.geometry.d3._
|
||||
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)
|
||||
lazy val invalidPoint: d3.Point = d3.Point(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)
|
||||
|
||||
|
|
@ -16,22 +17,22 @@ object GeometryForm {
|
|||
* @param o the entity
|
||||
* @return the representation
|
||||
*/
|
||||
def representByPoint()(o: Any): Geometry3D = {
|
||||
def representByPoint()(o: Any): VolumetricGeometry = {
|
||||
o match {
|
||||
case p: PlanetSideGameObject => Point3D(p.Position)
|
||||
case s: SourceEntry => Point3D(s.Position)
|
||||
case p: PlanetSideGameObject => Point(p.Position)
|
||||
case s: SourceEntry => Point(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
|
||||
* The geometric representation is a sphere using a position as the entity's centroid
|
||||
* and all points exist a distance from that point.
|
||||
* @param radius how wide a quarter-sphere is
|
||||
* @param o the entity
|
||||
* @return the representation
|
||||
*/
|
||||
def representBySphere(radius: Float)(o: Any): Geometry3D = {
|
||||
def representBySphere(radius: Float)(o: Any): VolumetricGeometry = {
|
||||
o match {
|
||||
case p: PlanetSideGameObject =>
|
||||
Sphere(p.Position, radius)
|
||||
|
|
@ -42,6 +43,25 @@ object GeometryForm {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The geometric representation is a sphere using a position as the entity's base (foot position)
|
||||
* and the centroid is located just above it a fixed distance.
|
||||
* All points exist a distance from that centroid.
|
||||
* @param radius how wide a quarter-sphere is
|
||||
* @param o the entity
|
||||
* @return the representation
|
||||
*/
|
||||
def representBySphereOnBase(radius: Float)(o: Any): VolumetricGeometry = {
|
||||
o match {
|
||||
case p: PlanetSideGameObject =>
|
||||
Sphere(p.Position + Vector3.z(radius), radius)
|
||||
case s: SourceEntry =>
|
||||
Sphere(s.Position + Vector3.z(radius), 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).
|
||||
|
|
@ -49,7 +69,7 @@ object GeometryForm {
|
|||
* @param o the entity
|
||||
* @return the representation
|
||||
*/
|
||||
def representByRaisedSphere(radius: Float)(o: Any): Geometry3D = {
|
||||
def representByRaisedSphere(radius: Float)(o: Any): VolumetricGeometry = {
|
||||
o match {
|
||||
case p: PlanetSideGameObject =>
|
||||
Sphere(p.Position + Vector3.relativeUp(p.Orientation) * radius, radius)
|
||||
|
|
@ -67,7 +87,7 @@ object GeometryForm {
|
|||
* @param o the entity
|
||||
* @return the representation
|
||||
*/
|
||||
def representByCylinder(radius: Float, height: Float)(o: Any): Geometry3D = {
|
||||
def representByCylinder(radius: Float, height: Float)(o: Any): VolumetricGeometry = {
|
||||
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)
|
||||
|
|
@ -82,7 +102,7 @@ object GeometryForm {
|
|||
* @param o the entity
|
||||
* @return the representation
|
||||
*/
|
||||
def representPlayerByCylinder(radius: Float)(o: Any): Geometry3D = {
|
||||
def representPlayerByCylinder(radius: Float)(o: Any): VolumetricGeometry = {
|
||||
o match {
|
||||
case p: Player =>
|
||||
val radialOffset = if(p.ExoSuit == ExoSuitType.MAX) 0.25f else 0f
|
||||
|
|
@ -113,7 +133,7 @@ object GeometryForm {
|
|||
* @param o the entity
|
||||
* @return the representation
|
||||
*/
|
||||
def representHoveringEntityByCylinder(radius: Float, height: Float, hoversAt: Float)(o: Any): Geometry3D = {
|
||||
def representHoveringEntityByCylinder(radius: Float, height: Float, hoversAt: Float)(o: Any): VolumetricGeometry = {
|
||||
o match {
|
||||
case p: PlanetSideGameObject =>
|
||||
Cylinder(p.Position, Vector3.relativeUp(p.Orientation), radius, height)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
// 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
|
||||
|
||||
/**
|
||||
* Move the centroid of the shape to the given point
|
||||
* @param point the new center point
|
||||
* @return geometry centered on the new point;
|
||||
* ideally, should be the same type of geometry as the original object
|
||||
*/
|
||||
def moveCenter(point: Point): PrimitiveGeometry
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
|
@ -1,503 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Untested geometry.
|
||||
* @param p na
|
||||
* @param relativeForward na
|
||||
* @param relativeUp na
|
||||
* @param length na
|
||||
* @param width na
|
||||
* @param height na
|
||||
*/
|
||||
final case class Cuboid(
|
||||
p: Point3D,
|
||||
relativeForward: Vector3,
|
||||
relativeUp: Vector3,
|
||||
length: Float,
|
||||
width: Float,
|
||||
height: Float,
|
||||
) extends Geometry3D {
|
||||
def center: Point3D = Point3D(p.asVector3 + relativeUp * height * 0.5f)
|
||||
|
||||
override def pointOnOutside(v: Vector3): Point3D = {
|
||||
import net.psforever.types.Vector3.{CrossProduct, DotProduct, neg}
|
||||
val height2 = height * 0.5f
|
||||
val relativeSide = CrossProduct(relativeForward, relativeUp)
|
||||
//val forwardVector = relativeForward * length
|
||||
//val sideVector = relativeSide * width
|
||||
//val upVector = relativeUp * height2
|
||||
val closestVector: Vector3 = Seq(
|
||||
relativeForward, relativeSide, relativeUp,
|
||||
neg(relativeForward), neg(relativeSide), neg(relativeUp)
|
||||
).maxBy { dir => DotProduct(dir, v) }
|
||||
def dz(): Float = {
|
||||
if (Geometry.closeToInsignificance(v.z) != 0) {
|
||||
closestVector.z / v.z
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
def dy(): Float = {
|
||||
if (Geometry.closeToInsignificance(v.y) != 0) {
|
||||
val fyfactor = closestVector.y / v.y
|
||||
if (v.z * fyfactor <= height2) {
|
||||
fyfactor
|
||||
} else {
|
||||
dz()
|
||||
}
|
||||
} else {
|
||||
dz()
|
||||
}
|
||||
}
|
||||
|
||||
val scaleFactor: Float = {
|
||||
if (Geometry.closeToInsignificance(v.x) != 0) {
|
||||
val fxfactor = closestVector.x / v.x
|
||||
if (v.y * fxfactor <= length) {
|
||||
if (v.z * fxfactor <= height2) {
|
||||
fxfactor
|
||||
} else {
|
||||
dy()
|
||||
}
|
||||
} else {
|
||||
dy()
|
||||
}
|
||||
} else {
|
||||
dy()
|
||||
}
|
||||
}
|
||||
Point3D(center.asVector3 + (v * scaleFactor))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry.d2
|
||||
|
||||
import net.psforever.objects.geometry.{AxisAlignment2D, PrimitiveGeometry}
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* Basic interface of all two-dimensional geometry.
|
||||
*/
|
||||
trait Geometry2D extends PrimitiveGeometry {
|
||||
def center: Point
|
||||
|
||||
def inPlane: AxisAlignment2D
|
||||
|
||||
def pointOnOutside(v: Vector3): Point = center
|
||||
}
|
||||
|
||||
trait Flat extends Geometry2D
|
||||
76
src/main/scala/net/psforever/objects/geometry/d2/Point.scala
Normal file
76
src/main/scala/net/psforever/objects/geometry/d2/Point.scala
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry.d2
|
||||
|
||||
import net.psforever.objects.geometry
|
||||
import net.psforever.objects.geometry.{AxisAlignment, AxisAlignment2D}
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* The instance of a coordinate position in two-dimensional space.
|
||||
* @see `Vector3`
|
||||
* @param a the first coordinate of the position
|
||||
* @param b the second coordinate of the position
|
||||
* @param inPlane the planar orientation of "out", e.g., the XY-axis excludes z-coordinates;
|
||||
* includes the identity of the coordinates as they are in-order
|
||||
*/
|
||||
final case class Point(a: Float, b: Float, inPlane: AxisAlignment2D)
|
||||
extends Geometry2D
|
||||
with geometry.Point {
|
||||
def center: Point = this
|
||||
|
||||
def moveCenter(point: geometry.Point): Point = {
|
||||
point match {
|
||||
case p: Point if inPlane == p.inPlane => Point(p.a, p.b, inPlane)
|
||||
case _ => this
|
||||
}
|
||||
}
|
||||
|
||||
def asVector3: Vector3 = inPlane.asVector3(a, b)
|
||||
}
|
||||
|
||||
object Point {
|
||||
/**
|
||||
* An overloaded constructor that assigns world origin coordinates.
|
||||
* By default, the planar frame is the common XY-axis.
|
||||
* @return a `Point2D` entity
|
||||
*/
|
||||
def apply(): Point = Point(0,0, AxisAlignment.XY)
|
||||
|
||||
/**
|
||||
* An overloaded constructor that assigns world origin coordinates.
|
||||
* By default, the planar frame is the common XY-axis.
|
||||
* @return a `Point2D` entity
|
||||
*/
|
||||
def apply(point: geometry.Point): Point = {
|
||||
val p = point.asVector3
|
||||
Point(p.x, p.y, AxisAlignment.XY)
|
||||
}
|
||||
|
||||
/**
|
||||
* An overloaded constructor that assigns world origin coordinates in the given planar frame.
|
||||
* @return a `Point2D` entity
|
||||
*/
|
||||
def apply(frame: AxisAlignment2D): Point = Point(0,0, frame)
|
||||
|
||||
/**
|
||||
* An overloaded constructor that uses the same coordinates from a `Vector3` entity.
|
||||
* By default, the planar frame is the common XY-axis.
|
||||
* @param v the entity with the corresponding points
|
||||
* @return a `Point2D` entity
|
||||
*/
|
||||
def apply(v: Vector3): Point = Point(v.x, v.y, AxisAlignment.XY)
|
||||
|
||||
/**
|
||||
* An overloaded constructor that uses the same coordinates from a `Vector3` entity in the given planar frame.
|
||||
* By default, the planar frame is the common XY-axis.
|
||||
* @param v the entity with the corresponding points
|
||||
* @return a `Point2D` entity
|
||||
*/
|
||||
def apply(v: Vector3, frame: AxisAlignment2D): Point = {
|
||||
frame match {
|
||||
case AxisAlignment.XY => Point(v.x, v.y, frame)
|
||||
case AxisAlignment.YZ => Point(v.y, v.z, frame)
|
||||
case AxisAlignment.XZ => Point(v.x, v.z, frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry.d2
|
||||
|
||||
import net.psforever.objects.geometry
|
||||
import net.psforever.objects.geometry.{AxisAlignment, AxisAlignment2D}
|
||||
|
||||
/**
|
||||
* An axis-aligned planar region.
|
||||
* @param top the highest "vertical" coordinate
|
||||
* @param right the furthest "horizontal" coordinate
|
||||
* @param base the highest "vertical" coordinate
|
||||
* @param left the nearest "horizontal" coordinate
|
||||
* @param inPlane the axis plan that the geometry occupies;
|
||||
* for example, an XY-axis rectangle is Z-up
|
||||
*/
|
||||
final case class Rectangle(top: Float, right: Float, base: Float, left: Float, inPlane: AxisAlignment2D)
|
||||
extends Flat {
|
||||
assert(right > left, s"right needs to be greater than left - $right > $left")
|
||||
assert(top > base, s"top needs to be greater than base - $top > $base")
|
||||
|
||||
def center: Point = Point((right + left) * 0.5f, (top + base) * 0.5f, inPlane)
|
||||
|
||||
def moveCenter(point: geometry.Point): Rectangle = {
|
||||
point match {
|
||||
case p: Point if inPlane == p.inPlane =>
|
||||
val halfWidth = (right - left) * 0.5f
|
||||
val halfHeight = (top - base) * 0.5f
|
||||
Rectangle(p.b + halfHeight, p.a + halfWidth, p.b - halfHeight, p.a - halfWidth, inPlane)
|
||||
case _ =>
|
||||
this //TODO?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Rectangle {
|
||||
/**
|
||||
* Overloaded constructor for a `Rectangle` in the XY-plane.
|
||||
* @param top the highest "vertical" coordinate
|
||||
* @param right the furthest "horizontal" coordinate
|
||||
* @param base the highest "vertical" coordinate
|
||||
* @param left the nearest "horizontal" coordinate
|
||||
* @return a `Rectangle` entity
|
||||
*/
|
||||
def apply(top: Float, right: Float, base: Float, left: Float): Rectangle =
|
||||
Rectangle(top, right, base, left, AxisAlignment.XY)
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry.d3
|
||||
|
||||
import net.psforever.objects.geometry
|
||||
import net.psforever.objects.geometry.Geometry
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* Untested geometry.
|
||||
* @param p na
|
||||
* @param relativeForward na
|
||||
* @param relativeUp na
|
||||
* @param length na
|
||||
* @param width na
|
||||
* @param height na
|
||||
*/
|
||||
final case class Cuboid(
|
||||
p: Point,
|
||||
relativeForward: Vector3,
|
||||
relativeUp: Vector3,
|
||||
length: Float,
|
||||
width: Float,
|
||||
height: Float,
|
||||
) extends VolumetricGeometry {
|
||||
def center: Point = Point(p.asVector3 + relativeUp * height * 0.5f)
|
||||
|
||||
def moveCenter(point: geometry.Point): VolumetricGeometry = Cuboid(Point(point), relativeForward, relativeUp, length, width, height)
|
||||
|
||||
override def pointOnOutside(v: Vector3): Point = {
|
||||
import net.psforever.types.Vector3.{CrossProduct, DotProduct, neg}
|
||||
val height2 = height * 0.5f
|
||||
val relativeSide = CrossProduct(relativeForward, relativeUp)
|
||||
//val forwardVector = relativeForward * length
|
||||
//val sideVector = relativeSide * width
|
||||
//val upVector = relativeUp * height2
|
||||
val closestVector: Vector3 = Seq(
|
||||
relativeForward, relativeSide, relativeUp,
|
||||
neg(relativeForward), neg(relativeSide), neg(relativeUp)
|
||||
).maxBy { dir => DotProduct(dir, v) }
|
||||
def dz(): Float = {
|
||||
if (Geometry.closeToInsignificance(v.z) != 0) {
|
||||
closestVector.z / v.z
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
def dy(): Float = {
|
||||
if (Geometry.closeToInsignificance(v.y) != 0) {
|
||||
val fyfactor = closestVector.y / v.y
|
||||
if (v.z * fyfactor <= height2) {
|
||||
fyfactor
|
||||
} else {
|
||||
dz()
|
||||
}
|
||||
} else {
|
||||
dz()
|
||||
}
|
||||
}
|
||||
|
||||
val scaleFactor: Float = {
|
||||
if (Geometry.closeToInsignificance(v.x) != 0) {
|
||||
val fxfactor = closestVector.x / v.x
|
||||
if (v.y * fxfactor <= length) {
|
||||
if (v.z * fxfactor <= height2) {
|
||||
fxfactor
|
||||
} else {
|
||||
dy()
|
||||
}
|
||||
} else {
|
||||
dy()
|
||||
}
|
||||
} else {
|
||||
dy()
|
||||
}
|
||||
}
|
||||
Point(center.asVector3 + (v * scaleFactor))
|
||||
}
|
||||
}
|
||||
100
src/main/scala/net/psforever/objects/geometry/d3/Cylinder.scala
Normal file
100
src/main/scala/net/psforever/objects/geometry/d3/Cylinder.scala
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry.d3
|
||||
|
||||
import net.psforever.objects.geometry
|
||||
import net.psforever.objects.geometry.{Geometry, Slope}
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* 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: Point, relativeUp: Vector3, radius: Float, height: Float)
|
||||
extends VolumetricGeometry {
|
||||
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: Point = Point(p.asVector3 + relativeUp * height * 0.5f)
|
||||
|
||||
def moveCenter(point: geometry.Point): VolumetricGeometry = Cylinder(Point(point), relativeUp, radius, height)
|
||||
|
||||
/**
|
||||
* 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): Point = {
|
||||
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
|
||||
Point(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
|
||||
}
|
||||
Point(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: Point, 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(Point(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(Point(p), v, radius, height)
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry.d3
|
||||
|
||||
import net.psforever.objects.geometry
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* Basic interface of all three-dimensional geometry.
|
||||
* For the only real requirement for a three-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 geometry.PrimitiveGeometry {
|
||||
def center: Point
|
||||
|
||||
def moveCenter(point: geometry.Point): Geometry3D
|
||||
}
|
||||
|
||||
trait VolumetricGeometry extends Geometry3D {
|
||||
|
||||
def moveCenter(point: geometry.Point): VolumetricGeometry
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
60
src/main/scala/net/psforever/objects/geometry/d3/Line.scala
Normal file
60
src/main/scala/net/psforever/objects/geometry/d3/Line.scala
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry.d3
|
||||
|
||||
import net.psforever.objects.geometry
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* 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 Line(p: Point, d: Vector3)
|
||||
extends Geometry3D
|
||||
with geometry.Line {
|
||||
def center: Point = p
|
||||
|
||||
def moveCenter(point: geometry.Point): Geometry3D = Line(Point(point), d)
|
||||
}
|
||||
|
||||
object Line {
|
||||
/**
|
||||
* 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 `Line` entity
|
||||
*/
|
||||
def apply(x: Float, y: Float, z: Float, d: Vector3): Line = {
|
||||
Line(Point(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 `Line` entity
|
||||
*/
|
||||
def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Line = {
|
||||
Line(Point(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 `Line` entity
|
||||
*/
|
||||
def apply(p1: Point, p2: Point): Line = {
|
||||
Line(p1, Vector3.Unit(Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)))
|
||||
}
|
||||
}
|
||||
43
src/main/scala/net/psforever/objects/geometry/d3/Point.scala
Normal file
43
src/main/scala/net/psforever/objects/geometry/d3/Point.scala
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry.d3
|
||||
|
||||
import net.psforever.objects.geometry
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* The instance of a minute geometric coordinate position in three-dimensional space.
|
||||
* The point is allowed to substitute for a sphere of zero radius, hence why it is volumetric
|
||||
* (ignoring that a sphere of zero radius has no volume).
|
||||
* @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 Point(x: Float, y: Float, z: Float)
|
||||
extends VolumetricGeometry
|
||||
with geometry.Point {
|
||||
def center: Point = this
|
||||
|
||||
def moveCenter(point: geometry.Point): VolumetricGeometry = Point(point)
|
||||
|
||||
def asVector3: Vector3 = Vector3(x, y, z)
|
||||
|
||||
def pointOnOutside(v: Vector3): Point = center
|
||||
}
|
||||
|
||||
object Point {
|
||||
/**
|
||||
* An overloaded constructor that assigns world origin coordinates.
|
||||
* @return a `Point` entity
|
||||
*/
|
||||
def apply(): Point = Point(0,0,0)
|
||||
|
||||
def apply(point: geometry.Point): Point = Point(point.asVector3)
|
||||
|
||||
/**
|
||||
* An overloaded constructor that uses the same coordinates from a `Vector3` entity.
|
||||
* @param v the entity with the corresponding points
|
||||
* @return a `Point` entity
|
||||
*/
|
||||
def apply(v: Vector3): Point = Point(v.x, v.y, v.z)
|
||||
}
|
||||
44
src/main/scala/net/psforever/objects/geometry/d3/Ray.scala
Normal file
44
src/main/scala/net/psforever/objects/geometry/d3/Ray.scala
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry.d3
|
||||
|
||||
import net.psforever.objects.geometry
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* 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 Ray(p: Point, d: Vector3)
|
||||
extends Geometry3D
|
||||
with geometry.Line {
|
||||
def center: Point = p
|
||||
|
||||
def moveCenter(point: geometry.Point): Geometry3D = Ray(Point(point), d)
|
||||
}
|
||||
|
||||
object Ray {
|
||||
/**
|
||||
* 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 `Ray` entity
|
||||
*/
|
||||
def apply(x: Float, y: Float, z: Float, d: Vector3): Ray = Ray(Point(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 `Ray` entity
|
||||
*/
|
||||
def apply(v: Vector3, d: Vector3): Ray = Ray(Point(v.x, v.y, v.z), d)
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry.d3
|
||||
|
||||
import net.psforever.objects.geometry
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* 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 Segment(p1: Point, p2: Point)
|
||||
extends Geometry3D
|
||||
with geometry.Segment {
|
||||
/**
|
||||
* The center point of a segment is a position that is equally in between both endpoints.
|
||||
* @return a point
|
||||
*/
|
||||
def center: Point = Point((p2.asVector3 + p1.asVector3) * 0.5f)
|
||||
|
||||
def moveCenter(point: geometry.Point): Geometry3D = {
|
||||
Segment(
|
||||
Point(point.asVector3 - Vector3.Unit(d) * Vector3.Magnitude(d) * 0.5f),
|
||||
d
|
||||
)
|
||||
}
|
||||
|
||||
def d: Vector3 = p2.asVector3 - p1.asVector3
|
||||
|
||||
def asLine: Line = Line(p1, Vector3.Unit(d))
|
||||
}
|
||||
|
||||
object Segment {
|
||||
/**
|
||||
* 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 `Segment` entity
|
||||
*/
|
||||
def apply(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Segment = {
|
||||
Segment(Point(ax, ay, az), Point(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: Point, d: Vector3): Segment = {
|
||||
Segment(p, Point(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 `Segment` entity
|
||||
*/
|
||||
def apply(x: Float, y: Float, z: Float, d: Vector3): Segment = {
|
||||
Segment(Point(x, y, z), Point(x + d.x, y + d.y, z + d.z))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry.d3
|
||||
|
||||
import net.psforever.objects.geometry
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
* The instance of a volumetric region that encapsulates all points within a certain distance of a central point.
|
||||
* (That's what a sphere is.)
|
||||
* When described by its center point, a sphere has no distinct "top", "base", or "side";
|
||||
* all directions are described in the same way in reference to this center.
|
||||
* It can be considered having a "base" and other "faces" for the purposes of settling on a surface (the ground).
|
||||
* @param p the point
|
||||
* @param radius a distance that spans all points in any direction from the central point
|
||||
*/
|
||||
final case class Sphere(p: Point, radius: Float)
|
||||
extends VolumetricGeometry {
|
||||
def center: Point = p
|
||||
|
||||
def moveCenter(point: geometry.Point): Sphere = Sphere(Point(point), radius)
|
||||
|
||||
/**
|
||||
* 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): Point = {
|
||||
val slope = Vector3.Unit(v)
|
||||
val mult = radius / Vector3.Magnitude(slope)
|
||||
Point(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(Point(), 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(Point(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(Point(v), radius)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.objects.serverobject.environment
|
||||
|
||||
import net.psforever.objects.geometry.d2.Rectangle
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
/**
|
||||
|
|
@ -20,6 +21,8 @@ trait EnvironmentCollision {
|
|||
* `false`, otherwise
|
||||
*/
|
||||
def testInteraction(pos: Vector3, varDepth: Float): Boolean
|
||||
|
||||
def bounding: Rectangle
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -32,6 +35,12 @@ final case class DeepPlane(altitude: Float)
|
|||
def testInteraction(pos: Vector3, varDepth: Float): Boolean = {
|
||||
pos.z + varDepth < altitude
|
||||
}
|
||||
|
||||
def bounding: Rectangle = {
|
||||
val max = Float.MaxValue * 0.25f
|
||||
val min = Float.MinValue * 0.25f
|
||||
Rectangle(max, max, min, min)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -49,6 +58,8 @@ final case class DeepSquare(altitude: Float, north: Float, east: Float, south: F
|
|||
def testInteraction(pos: Vector3, varDepth: Float): Boolean = {
|
||||
pos.z + varDepth < altitude && north > pos.y && pos.y >= south && east > pos.x && pos.x >= west
|
||||
}
|
||||
|
||||
def bounding: Rectangle = Rectangle(north, east, south, west)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -68,6 +79,8 @@ final case class DeepSurface(altitude: Float, north: Float, east: Float, south:
|
|||
def testInteraction(pos: Vector3, varDepth: Float): Boolean = {
|
||||
pos.z < altitude && north > pos.y && pos.y >= south && east > pos.x && pos.x >= west
|
||||
}
|
||||
|
||||
def bounding: Rectangle = Rectangle(north, east, south, west)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -80,6 +93,8 @@ final case class DeepCircularSurface(center: Vector3, radius: Float)
|
|||
extends EnvironmentCollision {
|
||||
def altitude: Float = center.z
|
||||
|
||||
def bounding: Rectangle = Rectangle(center.y + radius, center.x + radius, center.y - radius, center.x - radius)
|
||||
|
||||
def testInteraction(pos: Vector3, varDepth: Float): Boolean = {
|
||||
pos.z < center.z && Vector3.DistanceSquared(pos.xy, center.xy) < radius * radius
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +1,168 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.serverobject.environment
|
||||
|
||||
import net.psforever.objects.GlobalDefinitions
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.types.{OxygenState, PlanetSideGUID}
|
||||
import net.psforever.objects.zones._
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
|
||||
/**
|
||||
* Related to the progress of interacting with a body of water deeper than you are tall or
|
||||
* deeper than your vehicle is off the ground.
|
||||
* @param guid the target
|
||||
* @param state whether they are recovering or suffocating
|
||||
* @param progress the percentage of completion towards the state
|
||||
* This game entity may infrequently test whether it may interact with game world environment.
|
||||
*/
|
||||
final case class OxygenStateTarget(
|
||||
guid: PlanetSideGUID,
|
||||
state: OxygenState,
|
||||
progress: Float
|
||||
)
|
||||
class InteractWithEnvironment()
|
||||
extends ZoneInteraction {
|
||||
private var interactingWithEnvironment: (PlanetSideServerObject, Boolean) => Any =
|
||||
InteractWithEnvironment.onStableEnvironment()
|
||||
|
||||
/**
|
||||
* The target has clipped into a critical region of a piece of environment.
|
||||
* @param obj the target
|
||||
* @param environment the terrain clipping region
|
||||
* @param mountedVehicle whether or not the target is mounted
|
||||
* (specifically, if the target is a `Player` who is mounted in a `Vehicle`)
|
||||
*/
|
||||
final case class InteractWithEnvironment(
|
||||
obj: PlanetSideServerObject,
|
||||
environment: PieceOfEnvironment,
|
||||
mountedVehicle: Option[OxygenStateTarget]
|
||||
)
|
||||
/**
|
||||
* The method by which zone interactions are tested or a current interaction maintained.
|
||||
* Utilize a function literal that, when called, returns a function literal of the same type;
|
||||
* the function that is returned will not necessarily be the same as the one that was used
|
||||
* but will represent the existing and ongoing status of interaction with the environment.
|
||||
* Calling one function and exchanging it for another function to be called like this creates a procedure
|
||||
* that controls and limits the interactions with the environment to only what is necessary.
|
||||
* @see `InteractsWithEnvironment.blockedFromInteracting`
|
||||
* @see `InteractsWithEnvironment.onStableEnvironment`
|
||||
* @see `InteractsWithEnvironment.awaitOngoingInteraction`
|
||||
*/
|
||||
def interaction(target: InteractsWithZone): Unit = {
|
||||
interactingWithEnvironment = interactingWithEnvironment(target, true)
|
||||
.asInstanceOf[(PlanetSideServerObject, Boolean) => Any]
|
||||
}
|
||||
|
||||
/**
|
||||
* The target has ceased to clip into a critical region of a piece of environment.
|
||||
* @param obj the target
|
||||
* @param environment the previous terrain clipping region
|
||||
* @param mountedVehicle whether or not the target is mounted
|
||||
* (specifically, if the target is a `Player` who is mounted in a `Vehicle`)
|
||||
*/
|
||||
final case class EscapeFromEnvironment(
|
||||
obj: PlanetSideServerObject,
|
||||
environment: PieceOfEnvironment,
|
||||
mountedVehicle: Option[OxygenStateTarget]
|
||||
)
|
||||
/**
|
||||
* Suspend any current interaction procedures through the proper channels
|
||||
* or deactivate a previously flagged interaction blocking procedure
|
||||
* and reset the system to its neutral state.
|
||||
* The main difference between resetting and flagging the blocking procedure
|
||||
* is that resetting will (probably) restore the previously active procedure on the next `zoneInteraction` call
|
||||
* while blocking will halt all attempts to establish a new active interaction procedure
|
||||
* and unblocking will immediately install whatever is the current active interaction.
|
||||
* @see `InteractsWithEnvironment.onStableEnvironment`
|
||||
*/
|
||||
def resetInteraction(target: InteractsWithZone) : Unit = {
|
||||
interactingWithEnvironment(target, false)
|
||||
interactingWithEnvironment = InteractWithEnvironment.onStableEnvironment()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely reset any internal actions or processes related to environment clipping.
|
||||
*/
|
||||
final case class RecoveredFromEnvironmentInteraction()
|
||||
object InteractWithEnvironment {
|
||||
/**
|
||||
* While on stable non-interactive terrain,
|
||||
* test whether any special terrain component has an affect upon the target entity.
|
||||
* If so, instruct the target that an interaction should occur.
|
||||
* Considered tail recursive, but not treated that way.
|
||||
* @see `blockedFromInteracting`
|
||||
* @see `checkAllEnvironmentInteractions`
|
||||
* @see `awaitOngoingInteraction`
|
||||
* @param obj the target entity
|
||||
* @return the function literal that represents the next iterative call of ongoing interaction testing;
|
||||
* may return itself
|
||||
*/
|
||||
def onStableEnvironment()(obj: PlanetSideServerObject, allow: Boolean): Any = {
|
||||
if(allow) {
|
||||
checkAllEnvironmentInteractions(obj) match {
|
||||
case Some(body) =>
|
||||
obj.Actor ! InteractingWithEnvironment(obj, body, None)
|
||||
awaitOngoingInteraction(obj.Zone, body)(_,_)
|
||||
case None =>
|
||||
onStableEnvironment()(_,_)
|
||||
}
|
||||
} else {
|
||||
blockedFromInteracting()(_,_)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* While on unstable, interactive, or special terrain,
|
||||
* test whether that special terrain component has an affect upon the target entity.
|
||||
* If no interaction exists,
|
||||
* treat the target as if it had been previously affected by the given terrain,
|
||||
* and instruct it to cease that assumption.
|
||||
* Transition between the affects of different special terrains is possible.
|
||||
* Considered tail recursive, but not treated that way.
|
||||
* @see `blockedFromInteracting`
|
||||
* @see `checkAllEnvironmentInteractions`
|
||||
* @see `checkSpecificEnvironmentInteraction`
|
||||
* @see `onStableEnvironment`
|
||||
* @param zone the zone in which the terrain is located
|
||||
* @param body the special terrain
|
||||
* @param obj the target entity
|
||||
* @return the function literal that represents the next iterative call of ongoing interaction testing;
|
||||
* may return itself
|
||||
*/
|
||||
def awaitOngoingInteraction(zone: Zone, body: PieceOfEnvironment)(obj: PlanetSideServerObject, allow: Boolean): Any = {
|
||||
if (allow) {
|
||||
checkSpecificEnvironmentInteraction(zone, body)(obj) match {
|
||||
case Some(_) =>
|
||||
awaitOngoingInteraction(obj.Zone, body)(_, _)
|
||||
case None =>
|
||||
checkAllEnvironmentInteractions(obj) match {
|
||||
case Some(newBody) if newBody.attribute == body.attribute =>
|
||||
obj.Actor ! InteractingWithEnvironment(obj, newBody, None)
|
||||
awaitOngoingInteraction(obj.Zone, newBody)(_, _)
|
||||
case Some(newBody) =>
|
||||
obj.Actor ! EscapeFromEnvironment(obj, body, None)
|
||||
obj.Actor ! InteractingWithEnvironment(obj, newBody, None)
|
||||
awaitOngoingInteraction(obj.Zone, newBody)(_, _)
|
||||
case None =>
|
||||
obj.Actor ! EscapeFromEnvironment(obj, body, None)
|
||||
onStableEnvironment()(_, _)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
obj.Actor ! EscapeFromEnvironment(obj, body, None)
|
||||
blockedFromInteracting()(_,_)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not care whether on stable non-interactive terrain or on unstable interactive terrain.
|
||||
* Wait until allowed to test again (external flag).
|
||||
* Considered tail recursive, but not treated that way.
|
||||
* @see `onStableEnvironment`
|
||||
* @param obj the target entity
|
||||
* @return the function literal that represents the next iterative call of ongoing interaction testing;
|
||||
* may return itself
|
||||
*/
|
||||
def blockedFromInteracting()(obj: PlanetSideServerObject, allow: Boolean): Any = {
|
||||
if (allow) {
|
||||
onStableEnvironment()(obj, allow)
|
||||
} else {
|
||||
blockedFromInteracting()(_,_)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether any special terrain component has an affect upon the target entity.
|
||||
* @param obj the target entity
|
||||
* @return any unstable, interactive, or special terrain that is being interacted
|
||||
*/
|
||||
def checkAllEnvironmentInteractions(obj: PlanetSideServerObject): Option[PieceOfEnvironment] = {
|
||||
val position = obj.Position
|
||||
val depth = GlobalDefinitions.MaxDepth(obj)
|
||||
(obj match {
|
||||
case bme: BlockMapEntity =>
|
||||
obj.Zone.blockMap.sector(bme).environmentList
|
||||
case _ =>
|
||||
obj.Zone.map.environment
|
||||
}).find { body =>
|
||||
body.attribute.canInteractWith(obj) && body.testInteraction(position, depth)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a special terrain component has an affect upon the target entity.
|
||||
* @param zone the zone in which the terrain is located
|
||||
* @param body the special terrain
|
||||
* @param obj the target entity
|
||||
* @return any unstable, interactive, or special terrain that is being interacted
|
||||
*/
|
||||
private def checkSpecificEnvironmentInteraction(zone: Zone, body: PieceOfEnvironment)(obj: PlanetSideServerObject): Option[PieceOfEnvironment] = {
|
||||
if ((obj.Zone eq zone) && body.testInteraction(obj.Position, GlobalDefinitions.MaxDepth(obj))) {
|
||||
Some(body)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.objects.serverobject.environment
|
||||
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.types.{OxygenState, PlanetSideGUID}
|
||||
|
||||
/**
|
||||
* Related to the progress of interacting with a body of water deeper than you are tall or
|
||||
* deeper than your vehicle is off the ground.
|
||||
* @param guid the target
|
||||
* @param state whether they are recovering or suffocating
|
||||
* @param progress the percentage of completion towards the state
|
||||
*/
|
||||
final case class OxygenStateTarget(
|
||||
guid: PlanetSideGUID,
|
||||
state: OxygenState,
|
||||
progress: Float
|
||||
)
|
||||
|
||||
/**
|
||||
* The target has clipped into a critical region of a piece of environment.
|
||||
* @param obj the target
|
||||
* @param environment the terrain clipping region
|
||||
* @param mountedVehicle whether or not the target is mounted
|
||||
* (specifically, if the target is a `Player` who is mounted in a `Vehicle`)
|
||||
*/
|
||||
final case class InteractingWithEnvironment(
|
||||
obj: PlanetSideServerObject,
|
||||
environment: PieceOfEnvironment,
|
||||
mountedVehicle: Option[OxygenStateTarget]
|
||||
)
|
||||
|
||||
/**
|
||||
* The target has ceased to clip into a critical region of a piece of environment.
|
||||
* @param obj the target
|
||||
* @param environment the previous terrain clipping region
|
||||
* @param mountedVehicle whether or not the target is mounted
|
||||
* (specifically, if the target is a `Player` who is mounted in a `Vehicle`)
|
||||
*/
|
||||
final case class EscapeFromEnvironment(
|
||||
obj: PlanetSideServerObject,
|
||||
environment: PieceOfEnvironment,
|
||||
mountedVehicle: Option[OxygenStateTarget]
|
||||
)
|
||||
|
||||
/**
|
||||
* Completely reset any internal actions or processes related to environment clipping.
|
||||
*/
|
||||
final case class RecoveredFromEnvironmentInteraction()
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.objects.serverobject.environment
|
||||
|
||||
import net.psforever.objects.GlobalDefinitions
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.zones.Zone
|
||||
|
||||
/**
|
||||
* This game entity may infrequently test whether it may interact with game world environment.
|
||||
*/
|
||||
trait InteractsWithZoneEnvironment {
|
||||
_: PlanetSideServerObject =>
|
||||
/** interactions for this particular entity is allowed */
|
||||
private var _allowZoneEnvironmentInteractions: Boolean = true
|
||||
|
||||
/**
|
||||
* If the environmental interactive permissions of this entity change.
|
||||
*/
|
||||
def allowZoneEnvironmentInteractions: Boolean = _allowZoneEnvironmentInteractions
|
||||
|
||||
/**
|
||||
* If the environmental interactive permissions of this entity change,
|
||||
* trigger a formal change to the interaction methodology.
|
||||
* @param allow whether or not interaction is permitted
|
||||
* @return whether or not interaction is permitted
|
||||
*/
|
||||
def allowZoneEnvironmentInteractions_=(allow: Boolean): Boolean = {
|
||||
val before = _allowZoneEnvironmentInteractions
|
||||
_allowZoneEnvironmentInteractions = allow
|
||||
if (before != allow) {
|
||||
zoneInteraction()
|
||||
}
|
||||
_allowZoneEnvironmentInteractions
|
||||
}
|
||||
|
||||
private var interactingWithEnvironment: (PlanetSideServerObject, Boolean) => Any =
|
||||
InteractsWithZoneEnvironment.onStableEnvironment()
|
||||
|
||||
/**
|
||||
* The method by which zone interactions are tested or a current interaction maintained.
|
||||
* Utilize a function literal that, when called, returns a function literal of the same type;
|
||||
* the function that is returned will not necessarily be the same as the one that was used
|
||||
* but will represent the existing and ongoing status of interaction with the environment.
|
||||
* Calling one function and exchanging it for another function to be called like this creates a procedure
|
||||
* that controls and limits the interactions with the environment to only what is necessary.
|
||||
* @see `InteractsWithZoneEnvironment.blockedFromInteracting`
|
||||
* @see `InteractsWithZoneEnvironment.onStableEnvironment`
|
||||
* @see `InteractsWithZoneEnvironment.awaitOngoingInteraction`
|
||||
*/
|
||||
def zoneInteraction(): Unit = {
|
||||
//val func: (PlanetSideServerObject, Boolean) => Any = interactingWithEnvironment(this, allowZoneEnvironmentInteractions)
|
||||
interactingWithEnvironment = interactingWithEnvironment(this, allowZoneEnvironmentInteractions)
|
||||
.asInstanceOf[(PlanetSideServerObject, Boolean) => Any]
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend any current interaction procedures through the proper channels
|
||||
* or deactivate a previously flagged interaction blocking procedure
|
||||
* and reset the system to its neutral state.
|
||||
* The main difference between resetting and flagging the blocking procedure
|
||||
* is that resetting will (probably) restore the previously active procedure on the next `zoneInteraction` call
|
||||
* while blocking will halt all attempts to establish a new active interaction procedure
|
||||
* and unblocking will immediately install whatever is the current active interaction.
|
||||
* @see `InteractsWithZoneEnvironment.onStableEnvironment`
|
||||
*/
|
||||
def resetZoneInteraction() : Unit = {
|
||||
_allowZoneEnvironmentInteractions = true
|
||||
interactingWithEnvironment(this, false)
|
||||
interactingWithEnvironment = InteractsWithZoneEnvironment.onStableEnvironment()
|
||||
}
|
||||
}
|
||||
|
||||
object InteractsWithZoneEnvironment {
|
||||
/**
|
||||
* While on stable non-interactive terrain,
|
||||
* test whether any special terrain component has an affect upon the target entity.
|
||||
* If so, instruct the target that an interaction should occur.
|
||||
* Considered tail recursive, but not treated that way.
|
||||
* @see `blockedFromInteracting`
|
||||
* @see `checkAllEnvironmentInteractions`
|
||||
* @see `awaitOngoingInteraction`
|
||||
* @param obj the target entity
|
||||
* @return the function literal that represents the next iterative call of ongoing interaction testing;
|
||||
* may return itself
|
||||
*/
|
||||
def onStableEnvironment()(obj: PlanetSideServerObject, allow: Boolean): Any = {
|
||||
if(allow) {
|
||||
checkAllEnvironmentInteractions(obj) match {
|
||||
case Some(body) =>
|
||||
obj.Actor ! InteractWithEnvironment(obj, body, None)
|
||||
awaitOngoingInteraction(obj.Zone, body)(_,_)
|
||||
case None =>
|
||||
onStableEnvironment()(_,_)
|
||||
}
|
||||
} else {
|
||||
blockedFromInteracting()(_,_)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* While on unstable, interactive, or special terrain,
|
||||
* test whether that special terrain component has an affect upon the target entity.
|
||||
* If no interaction exists,
|
||||
* treat the target as if it had been previously affected by the given terrain,
|
||||
* and instruct it to cease that assumption.
|
||||
* Transition between the affects of different special terrains is possible.
|
||||
* Considered tail recursive, but not treated that way.
|
||||
* @see `blockedFromInteracting`
|
||||
* @see `checkAllEnvironmentInteractions`
|
||||
* @see `checkSpecificEnvironmentInteraction`
|
||||
* @see `onStableEnvironment`
|
||||
* @param zone the zone in which the terrain is located
|
||||
* @param body the special terrain
|
||||
* @param obj the target entity
|
||||
* @return the function literal that represents the next iterative call of ongoing interaction testing;
|
||||
* may return itself
|
||||
*/
|
||||
def awaitOngoingInteraction(zone: Zone, body: PieceOfEnvironment)(obj: PlanetSideServerObject, allow: Boolean): Any = {
|
||||
if (allow) {
|
||||
checkSpecificEnvironmentInteraction(zone, body)(obj) match {
|
||||
case Some(_) =>
|
||||
awaitOngoingInteraction(obj.Zone, body)(_, _)
|
||||
case None =>
|
||||
checkAllEnvironmentInteractions(obj) match {
|
||||
case Some(newBody) if newBody.attribute == body.attribute =>
|
||||
obj.Actor ! InteractWithEnvironment(obj, newBody, None)
|
||||
awaitOngoingInteraction(obj.Zone, newBody)(_, _)
|
||||
case Some(newBody) =>
|
||||
obj.Actor ! EscapeFromEnvironment(obj, body, None)
|
||||
obj.Actor ! InteractWithEnvironment(obj, newBody, None)
|
||||
awaitOngoingInteraction(obj.Zone, newBody)(_, _)
|
||||
case None =>
|
||||
obj.Actor ! EscapeFromEnvironment(obj, body, None)
|
||||
onStableEnvironment()(_, _)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
obj.Actor ! EscapeFromEnvironment(obj, body, None)
|
||||
blockedFromInteracting()(_,_)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not care whether on stable non-interactive terrain or on unstable interactive terrain.
|
||||
* Wait until allowed to test again (external flag).
|
||||
* Considered tail recursive, but not treated that way.
|
||||
* @see `onStableEnvironment`
|
||||
* @param obj the target entity
|
||||
* @return the function literal that represents the next iterative call of ongoing interaction testing;
|
||||
* may return itself
|
||||
*/
|
||||
def blockedFromInteracting()(obj: PlanetSideServerObject, allow: Boolean): Any = {
|
||||
if (allow) {
|
||||
onStableEnvironment()(obj, allow)
|
||||
} else {
|
||||
blockedFromInteracting()(_,_)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether any special terrain component has an affect upon the target entity.
|
||||
* @param obj the target entity
|
||||
* @return any unstable, interactive, or special terrain that is being interacted
|
||||
*/
|
||||
def checkAllEnvironmentInteractions(obj: PlanetSideServerObject): Option[PieceOfEnvironment] = {
|
||||
val position = obj.Position
|
||||
val depth = GlobalDefinitions.MaxDepth(obj)
|
||||
obj.Zone.map.environment.find { body => body.attribute.canInteractWith(obj) && body.testInteraction(position, depth) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a special terrain component has an affect upon the target entity.
|
||||
* @param zone the zone in which the terrain is located
|
||||
* @param body the special terrain
|
||||
* @param obj the target entity
|
||||
* @return any unstable, interactive, or special terrain that is being interacted
|
||||
*/
|
||||
private def checkSpecificEnvironmentInteraction(zone: Zone, body: PieceOfEnvironment)(obj: PlanetSideServerObject): Option[PieceOfEnvironment] = {
|
||||
if ((obj.Zone eq zone) && body.testInteraction(obj.Position, GlobalDefinitions.MaxDepth(obj))) {
|
||||
Some(body)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,15 @@ package net.psforever.objects.serverobject.environment
|
|||
import enumeratum.{Enum, EnumEntry}
|
||||
import net.psforever.objects.{PlanetSideGameObject, Player}
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
import net.psforever.types.{PlanetSideGUID, Vector3}
|
||||
|
||||
/**
|
||||
* The representation of a feature of the game world that is not a formal game object,
|
||||
* usually terrain, but can be used to represent any bounded region.
|
||||
*/
|
||||
trait PieceOfEnvironment {
|
||||
trait PieceOfEnvironment
|
||||
extends BlockMapEntity {
|
||||
/** a general description of this environment */
|
||||
def attribute: EnvironmentTrait
|
||||
/** a special representation of the region that qualifies as "this environment" */
|
||||
|
|
@ -36,6 +38,18 @@ trait PieceOfEnvironment {
|
|||
*/
|
||||
def testStepIntoInteraction(pos: Vector3, previousPos: Vector3, varDepth: Float): Option[Boolean] =
|
||||
PieceOfEnvironment.testStepIntoInteraction(body = this, pos, previousPos, varDepth)
|
||||
|
||||
def Position: Vector3 = collision.bounding.center.asVector3 + Vector3.z(collision.altitude)
|
||||
|
||||
def Position_=(vec : Vector3) : Vector3 = Position
|
||||
|
||||
def Orientation: Vector3 = Vector3.Zero
|
||||
|
||||
def Orientation_=(vec: Vector3): Vector3 = Vector3.Zero
|
||||
|
||||
def Velocity: Option[Vector3] = None
|
||||
|
||||
def Velocity_=(vec: Option[Vector3]): Option[Vector3] = None
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -100,6 +114,8 @@ final case class SeaLevel(attribute: EnvironmentTrait, altitude: Float)
|
|||
private val planar = DeepPlane(altitude)
|
||||
|
||||
def collision : EnvironmentCollision = planar
|
||||
|
||||
override def Position: Vector3 = Vector3.Zero
|
||||
}
|
||||
|
||||
object SeaLevel {
|
||||
|
|
|
|||
|
|
@ -4,20 +4,21 @@ package net.psforever.objects.serverobject.environment
|
|||
import akka.actor.{Actor, Cancellable}
|
||||
import net.psforever.objects.Default
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.zones.InteractsWithZone
|
||||
import net.psforever.types.OxygenState
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
/**
|
||||
* The mixin code for any server object that responds to the game world around it.
|
||||
* The mixin code for any server object that responds to environmental representations in the game world.
|
||||
* Specific types of environmental region is bound by geometry,
|
||||
* designated by attributes,
|
||||
* and gets reacted to when coming into contact with that geometry.
|
||||
* and targets react when coming into contact with it.
|
||||
* Ideally, the target under control instigates the responses towards the environment
|
||||
* by independently re-evaluating the conditions of its interactions.
|
||||
* Only one kind of environment can elicit a response at a time.
|
||||
* While a reversal of this trigger scheme is possible, it is not ideal.
|
||||
* @see `InteractsWithZoneEnvironment`
|
||||
* @see `InteractsWithEnvironment`
|
||||
* @see `PieceOfEnvironment`
|
||||
*/
|
||||
trait RespondsToZoneEnvironment {
|
||||
|
|
@ -39,10 +40,10 @@ trait RespondsToZoneEnvironment {
|
|||
private var interactWithEnvironmentStop: mutable.HashMap[EnvironmentTrait, RespondsToZoneEnvironment.Interaction] =
|
||||
mutable.HashMap[EnvironmentTrait, RespondsToZoneEnvironment.Interaction]()
|
||||
|
||||
def InteractiveObject: PlanetSideServerObject with InteractsWithZoneEnvironment
|
||||
def InteractiveObject: PlanetSideServerObject with InteractsWithZone
|
||||
|
||||
val environmentBehavior: Receive = {
|
||||
case InteractWithEnvironment(target, body, optional) =>
|
||||
case InteractingWithEnvironment(target, body, optional) =>
|
||||
doEnvironmentInteracting(target, body, optional)
|
||||
|
||||
case EscapeFromEnvironment(target, body, optional) =>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.objects.serverobject.mount
|
||||
|
||||
import akka.actor.Actor
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.entity.WorldEntity
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
|
|
@ -43,6 +44,7 @@ trait MountableBehavior {
|
|||
case Some(seatNum) if mountTest(obj, seatNum, user) && tryMount(obj, seatNum, user) =>
|
||||
user.VehicleSeated = obj.GUID
|
||||
usedMountPoint.put(user.Name, mount_point)
|
||||
obj.Zone.actor ! ZoneActor.RemoveFromBlockMap(user)
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seatNum, mount_point))
|
||||
case _ =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, mount_point))
|
||||
|
|
@ -84,6 +86,7 @@ trait MountableBehavior {
|
|||
val obj = MountableObject
|
||||
if (dismountTest(obj, seat_number, user) && tryDismount(obj, seat_number, user)) {
|
||||
user.VehicleSeated = None
|
||||
obj.Zone.actor ! ZoneActor.AddToBlockMap(user, obj.Position)
|
||||
sender() ! Mountable.MountMessages(
|
||||
user,
|
||||
Mountable.CanDismount(obj, seat_number, getUsedMountPoint(user.Name, seat_number))
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import net.psforever.objects.vital.resistance.StandardResistanceProfile
|
|||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.resolution.DamageAndResistance
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
||||
import net.psforever.objects.zones.{Zone => World}
|
||||
|
||||
|
|
@ -23,7 +24,8 @@ import net.psforever.objects.zones.{Zone => World}
|
|||
abstract class Amenity
|
||||
extends PlanetSideServerObject
|
||||
with Vitality
|
||||
with StandardResistanceProfile {
|
||||
with StandardResistanceProfile
|
||||
with BlockMapEntity {
|
||||
private[this] val log = org.log4s.getLogger("Amenity")
|
||||
|
||||
/** what other entity has authority over this amenity; usually either a building or a vehicle */
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.objects.serverobject.structures
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import akka.actor.ActorContext
|
||||
import net.psforever.actors.zone.BuildingActor
|
||||
import net.psforever.objects.{GlobalDefinitions, NtuContainer, Player}
|
||||
|
|
@ -12,6 +13,7 @@ import net.psforever.objects.serverobject.painbox.Painbox
|
|||
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
||||
import net.psforever.objects.serverobject.tube.SpawnTube
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
import net.psforever.packet.game.BuildingInfoUpdateMessage
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3}
|
||||
import scalax.collection.{Graph, GraphEdge}
|
||||
|
|
@ -26,7 +28,8 @@ class Building(
|
|||
private val zone: Zone,
|
||||
private val buildingType: StructureType,
|
||||
private val buildingDefinition: BuildingDefinition
|
||||
) extends AmenityOwner {
|
||||
) extends AmenityOwner
|
||||
with BlockMapEntity {
|
||||
|
||||
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
private var playersInSOI: List[Player] = List.empty
|
||||
|
|
|
|||
|
|
@ -2,15 +2,11 @@
|
|||
package net.psforever.objects.vehicles
|
||||
|
||||
import akka.actor.{Actor, Cancellable}
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.vehicles.CargoBehavior.{CheckCargoDismount, CheckCargoMounting}
|
||||
import net.psforever.packet.game.{
|
||||
CargoMountPointStatusMessage,
|
||||
ObjectAttachMessage,
|
||||
ObjectDetachMessage,
|
||||
PlanetsideAttributeMessage
|
||||
}
|
||||
import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage}
|
||||
import net.psforever.types.{CargoStatus, PlanetSideGUID, Vector3}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.Service
|
||||
|
|
@ -160,6 +156,7 @@ object CargoBehavior {
|
|||
VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))
|
||||
)
|
||||
CargoMountBehaviorForAll(carrier, cargo, mountPoint)
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(cargo)
|
||||
false
|
||||
} else if (distance > 625 || iteration >= 40) {
|
||||
//vehicles moved too far away or took too long to get into proper position; abort mounting
|
||||
|
|
@ -289,6 +286,7 @@ object CargoBehavior {
|
|||
hold.mount(cargo)
|
||||
cargo.MountedIn = carrierGUID
|
||||
CargoMountBehaviorForAll(carrier, cargo, mountPoint)
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(cargo)
|
||||
false
|
||||
} else {
|
||||
//cargo vehicle did not move far away enough yet and there is more time to wait; reschedule check
|
||||
|
|
@ -391,6 +389,7 @@ object CargoBehavior {
|
|||
s"$cargoActor",
|
||||
VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))
|
||||
)
|
||||
zone.actor ! ZoneActor.AddToBlockMap(cargo, carrier.Position)
|
||||
if (carrier.isFlying) {
|
||||
//the carrier vehicle is flying; eject the cargo vehicle
|
||||
val ejectCargoMsg =
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.objects.vehicles
|
||||
|
||||
import akka.actor.{Actor, Cancellable}
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.ballistics.VehicleSource
|
||||
import net.psforever.objects.ce.TelepadLike
|
||||
|
|
@ -406,6 +407,9 @@ class VehicleControl(vehicle: Vehicle)
|
|||
case Some(player) =>
|
||||
seat.unmount(player)
|
||||
player.VehicleSeated = None
|
||||
if (player.isAlive) {
|
||||
zone.actor ! ZoneActor.AddToBlockMap(player, vehicle.Position)
|
||||
}
|
||||
if (player.HasGUID) {
|
||||
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid))
|
||||
}
|
||||
|
|
@ -703,7 +707,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
/**
|
||||
* Tell the given targets that
|
||||
* water causes vehicles to become disabled if they dive off too far, too deep.
|
||||
* @see `InteractWithEnvironment`
|
||||
* @see `InteractingWithEnvironment`
|
||||
* @see `OxygenState`
|
||||
* @see `OxygenStateTarget`
|
||||
* @param percentage the progress bar completion state
|
||||
|
|
@ -717,7 +721,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
): Unit = {
|
||||
val vtarget = Some(OxygenStateTarget(vehicle.GUID, OxygenState.Suffocation, percentage))
|
||||
targets.foreach { target =>
|
||||
target.Actor ! InteractWithEnvironment(target, body, vtarget)
|
||||
target.Actor ! InteractingWithEnvironment(target, body, vtarget)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -741,7 +745,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
//keep doing damage
|
||||
if (vehicle.Health > 0) {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
interactionTimer = context.system.scheduler.scheduleOnce(delay = 250 milliseconds, self, InteractWithEnvironment(obj, body, None))
|
||||
interactionTimer = context.system.scheduler.scheduleOnce(delay = 250 milliseconds, self, InteractingWithEnvironment(obj, body, None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -787,7 +791,10 @@ class VehicleControl(vehicle: Vehicle)
|
|||
percentage,
|
||||
body,
|
||||
vehicle.Seats.values
|
||||
.flatMap { case seat if seat.isOccupied => seat.occupants }
|
||||
.flatMap {
|
||||
case seat if seat.isOccupied => seat.occupants
|
||||
case _ => Nil
|
||||
}
|
||||
.filter { p => p.isAlive && (p.Zone eq vehicle.Zone) }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.vital.etc
|
||||
|
||||
import net.psforever.objects.ballistics.{DeployableSource, SourceEntry}
|
||||
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
|
||||
import net.psforever.objects.vital.prop.DamageProperties
|
||||
import net.psforever.objects.vital.resolution.DamageAndResistance
|
||||
|
||||
/**
|
||||
* A wrapper for a "damage source" in damage calculations
|
||||
* that parameterizes information necessary to explain an `ExplosiveDeployable` being detonated
|
||||
* by incursion of an acceptable target into its automatic triggering range.
|
||||
* @see `ExplosiveDeployable`
|
||||
* @param mine na
|
||||
* @param owner na
|
||||
*/
|
||||
final case class TrippedMineReason(mine: DeployableSource, owner: SourceEntry)
|
||||
extends DamageReason {
|
||||
|
||||
def source: DamageProperties = mine.Definition.innateDamage.getOrElse(TrippedMineReason.triggered)
|
||||
|
||||
def resolution: DamageResolution.Value = DamageResolution.Resolved
|
||||
|
||||
def same(test: DamageReason): Boolean = test match {
|
||||
case trip: TrippedMineReason => mine == trip.mine && mine.OwnerName == trip.mine.OwnerName
|
||||
case _ => false
|
||||
}
|
||||
|
||||
/** lay the blame on the player who laid this mine, if possible */
|
||||
def adversary: Option[SourceEntry] = Some(owner)
|
||||
|
||||
override def damageModel : DamageAndResistance = mine.Definition
|
||||
|
||||
override def attribution: Int = mine.Definition.ObjectId
|
||||
}
|
||||
|
||||
object TrippedMineReason {
|
||||
private val triggered = new DamageProperties {
|
||||
Damage0 = 1 //token damage
|
||||
SympatheticExplosion = true //sets off mine
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.zones
|
||||
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
|
||||
trait InteractsWithZone
|
||||
extends PlanetSideServerObject {
|
||||
/** interactions for this particular entity is allowed */
|
||||
private var _allowInteraction: Boolean = true
|
||||
|
||||
/**
|
||||
* If the interactive permissions of this entity change.
|
||||
*/
|
||||
def allowInteraction: Boolean = _allowInteraction
|
||||
|
||||
/**
|
||||
* If the interactive permissions of this entity change,
|
||||
* trigger a formal change to the interaction methodology.
|
||||
* @param permit whether or not interaction is permitted
|
||||
* @return whether or not interaction is permitted
|
||||
*/
|
||||
def allowInteraction_=(permit: Boolean): Boolean = {
|
||||
val before = _allowInteraction
|
||||
_allowInteraction = permit
|
||||
if (before != permit) {
|
||||
if (permit) {
|
||||
interactions.foreach { _.interaction(target = this) }
|
||||
} else {
|
||||
interactions.foreach ( _.resetInteraction(target = this) )
|
||||
}
|
||||
}
|
||||
_allowInteraction
|
||||
}
|
||||
|
||||
private var interactions: List[ZoneInteraction] = List()
|
||||
|
||||
def interaction(func: ZoneInteraction): List[ZoneInteraction] = {
|
||||
interactions = interactions :+ func
|
||||
interactions
|
||||
}
|
||||
|
||||
def interaction(): List[ZoneInteraction] = interactions
|
||||
|
||||
def zoneInteractions(): Unit = {
|
||||
if (_allowInteraction) {
|
||||
interactions.foreach { _.interaction(target = this) }
|
||||
}
|
||||
}
|
||||
|
||||
def resetInteractions(): Unit = {
|
||||
interactions.foreach { _.resetInteraction(target = this) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The basic behavior of an entity in a zone.
|
||||
* @see `InteractsWithZone`
|
||||
* @see `Zone`
|
||||
*/
|
||||
trait ZoneInteraction {
|
||||
/**
|
||||
* The method by which zone interactions are tested.
|
||||
* How a target tests this interaction with elements of the target's zone.
|
||||
* @param target the fixed element in this test
|
||||
*/
|
||||
def interaction(target: InteractsWithZone): Unit
|
||||
|
||||
/**
|
||||
* Suspend any current interaction procedures.
|
||||
* How the interactions are undone and stability restored to elements engaged with this target,
|
||||
* even if only possible by small measure.
|
||||
* Not all interactions can be reversed.
|
||||
* @param target the fixed element in this test
|
||||
*/
|
||||
def resetInteraction(target: InteractsWithZone): Unit
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@ case object MapInfo extends StringEnum[MapInfo] {
|
|||
Pool(EnvironmentAttribute.Water, 43.765625f, 3997.2812f, 3991.539f, 3937.8906f, 3937.875f), //southwest of neit
|
||||
Pool(EnvironmentAttribute.Water, 43.671875f, 2694.2031f, 3079.875f, 2552.414f, 2898.8203f), //west of anu
|
||||
Pool(EnvironmentAttribute.Water, 42.671875f, 5174.4844f, 5930.133f, 4981.4297f, 5812.383f), //west of lugh
|
||||
Pool(EnvironmentAttribute.Water, 42.203125f, 4935.742f, 5716.086f, 5496.6953f, 5444.5625f), //across road, west of lugh
|
||||
Pool(EnvironmentAttribute.Water, 42.203125f, 4935.742f, 5716.086f, 4711.289f, 5444.5625f), //across road, west of lugh
|
||||
Pool(EnvironmentAttribute.Water, 41.765625f, 2073.914f, 4982.5938f, 1995.4688f, 4899.086f), //L15-M16
|
||||
Pool(EnvironmentAttribute.Water, 41.3125f, 3761.1484f, 2616.75f, 3627.4297f, 2505.1328f), //G11, south
|
||||
Pool(EnvironmentAttribute.Water, 40.421875f, 4058.8281f, 2791.6562f, 3985.1016f, 2685.3672f) //G11, north
|
||||
|
|
|
|||
|
|
@ -54,8 +54,10 @@ class SphereOfInfluenceActor(zone: Zone) extends Actor {
|
|||
def UpdateSOI(): Unit = {
|
||||
sois.foreach {
|
||||
case (facility, radius) =>
|
||||
facility.PlayersInSOI =
|
||||
zone.LivePlayers.filter(p => Vector3.DistanceSquared(facility.Position.xy, p.Position.xy) < radius)
|
||||
val facilityXY = facility.Position.xy
|
||||
facility.PlayersInSOI = zone.blockMap.sector(facility)
|
||||
.livePlayerList
|
||||
.filter(p => Vector3.DistanceSquared(facilityXY, p.Position.xy) < radius)
|
||||
}
|
||||
populateTick.cancel()
|
||||
populateTick = context.system.scheduler.scheduleOnce(5 seconds, self, SOI.Populate())
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import akka.actor.typed
|
|||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
import net.psforever.objects.geometry.Geometry3D
|
||||
import net.psforever.objects.geometry.d3.VolumetricGeometry
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
|
|
@ -51,6 +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.services.Service
|
||||
|
||||
/**
|
||||
|
|
@ -87,6 +88,13 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
/** The basic support structure for the globally unique number system used by this `Zone`. */
|
||||
private var guid: NumberPoolHub = new NumberPoolHub(new MaxNumberSource(65536))
|
||||
|
||||
/** The blockmap structure for partitioning entities and environmental aspects of the zone.
|
||||
* For a standard 8912`^`2 map, each of the four hundred formal map grids is 445.6m long and wide.
|
||||
* A `desiredSpanSize` of 100m divides the blockmap into 8100 sectors.
|
||||
* A `desiredSpanSize` of 50m divides the blockmap into 32041 sectors.
|
||||
*/
|
||||
val blockMap: BlockMap = BlockMap(map.scale, desiredSpanSize = 100)
|
||||
|
||||
/** A synchronized `List` of items (`Equipment`) dropped by players on the ground and can be collected again. */
|
||||
private val equipmentOnGround: ListBuffer[Equipment] = ListBuffer[Equipment]()
|
||||
|
||||
|
|
@ -209,6 +217,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
MakeLattice()
|
||||
AssignAmenities()
|
||||
CreateSpawnGroups()
|
||||
PopulateBlockMap()
|
||||
|
||||
validate()
|
||||
}
|
||||
|
|
@ -728,6 +737,15 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
entry
|
||||
}
|
||||
|
||||
def PopulateBlockMap(): Unit = {
|
||||
vehicles.foreach { vehicle => blockMap.addTo(vehicle) }
|
||||
buildings.values.foreach { building =>
|
||||
blockMap.addTo(building)
|
||||
building.Amenities.foreach { amenity => blockMap.addTo(amenity) }
|
||||
}
|
||||
map.environment.foreach { env => blockMap.addTo(env) }
|
||||
}
|
||||
|
||||
def StartPlayerManagementSystems(): Unit = {
|
||||
soi ! SOI.Start()
|
||||
}
|
||||
|
|
@ -1163,14 +1181,8 @@ object Zone {
|
|||
|
||||
/**
|
||||
* na
|
||||
* @see `Amenity.Owner`
|
||||
* @see `ComplexDeployable`
|
||||
* @see `DamageWithPosition`
|
||||
* @see `SimpleDeployable`
|
||||
* @see `Zone.Buildings`
|
||||
* @see `Zone.DeployableList`
|
||||
* @see `Zone.LivePlayers`
|
||||
* @see `Zone.Vehicles`
|
||||
* @see `Zone.blockMap.sector`
|
||||
* @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
|
||||
|
|
@ -1183,30 +1195,17 @@ object Zone {
|
|||
): List[PlanetSideServerObject with Vitality] = {
|
||||
val sourcePosition = source.Position
|
||||
val sourcePositionXY = sourcePosition.xy
|
||||
val radius = damagePropertiesBySource.DamageRadius * damagePropertiesBySource.DamageRadius
|
||||
val sectors = zone.blockMap.sector(sourcePositionXY, damagePropertiesBySource.DamageRadius)
|
||||
//collect all targets that can be damaged
|
||||
//players
|
||||
val playerTargets = zone.LivePlayers.filterNot { _.VehicleSeated.nonEmpty }
|
||||
val playerTargets = sectors.livePlayerList.filterNot { _.VehicleSeated.nonEmpty }
|
||||
//vehicles
|
||||
val vehicleTargets = zone.Vehicles.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
|
||||
val vehicleTargets = sectors.vehicleList.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
|
||||
//deployables
|
||||
val deployableTargets = zone.DeployableList.filterNot { _.Destroyed }
|
||||
val deployableTargets = sectors.deployableList.filterNot { _.Destroyed }
|
||||
//amenities
|
||||
val soiTargets = source match {
|
||||
case o: Amenity =>
|
||||
//fortunately, even where soi overlap, amenities in different buildings are never that close to each other
|
||||
o.Owner.Amenities
|
||||
case _ =>
|
||||
zone.Buildings.values
|
||||
.filter { b =>
|
||||
val soiRadius = b.Definition.SOIRadius * b.Definition.SOIRadius
|
||||
Vector3.DistanceSquared(sourcePositionXY, b.Position.xy) < soiRadius || soiRadius <= radius
|
||||
}
|
||||
.flatMap { _.Amenities }
|
||||
.filter { _.Definition.Damageable }
|
||||
}
|
||||
|
||||
//restrict to targets according to the detection plan
|
||||
val soiTargets = sectors.amenityList.collect { case amenity: Vitality if !amenity.Destroyed => amenity }
|
||||
//altogether ...
|
||||
(playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets).filter { target => target ne source }
|
||||
}
|
||||
|
||||
|
|
@ -1256,7 +1255,7 @@ object Zone {
|
|||
* @return `true`, if the target entities are near enough to each other;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
private def distanceCheck(g1: Geometry3D, g2: Geometry3D, maxDistance: Float): Boolean = {
|
||||
private def distanceCheck(g1: VolumetricGeometry, g2: VolumetricGeometry, maxDistance: Float): Boolean = {
|
||||
Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3) <= maxDistance ||
|
||||
distanceCheck(g1, g2) <= maxDistance
|
||||
}
|
||||
|
|
@ -1270,7 +1269,7 @@ object Zone {
|
|||
* @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 = {
|
||||
def distanceCheck(g1: VolumetricGeometry, g2: VolumetricGeometry): 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
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package net.psforever.objects.zones
|
|||
|
||||
import akka.actor.Actor
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.ce.Deployable
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
|
@ -13,7 +14,6 @@ import scala.collection.mutable.ListBuffer
|
|||
* @param zone the `Zone` object
|
||||
*/
|
||||
class ZoneDeployableActor(zone: Zone, deployableList: ListBuffer[Deployable]) extends Actor {
|
||||
|
||||
import ZoneDeployableActor._
|
||||
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
|
@ -23,6 +23,7 @@ class ZoneDeployableActor(zone: Zone, deployableList: ListBuffer[Deployable]) ex
|
|||
if (DeployableBuild(obj, deployableList)) {
|
||||
obj.Zone = zone
|
||||
obj.Definition.Initialize(obj, context)
|
||||
zone.actor ! ZoneActor.AddToBlockMap(obj, obj.Position)
|
||||
obj.Actor ! Zone.Deployable.Setup()
|
||||
} else {
|
||||
log.warn(s"failed to build a ${obj.Definition.Name}")
|
||||
|
|
@ -33,6 +34,7 @@ class ZoneDeployableActor(zone: Zone, deployableList: ListBuffer[Deployable]) ex
|
|||
if (DeployableBuild(obj, deployableList)) {
|
||||
obj.Zone = zone
|
||||
obj.Definition.Initialize(obj, context)
|
||||
zone.actor ! ZoneActor.AddToBlockMap(obj, obj.Position)
|
||||
owner.Actor ! Player.BuildDeployable(obj, tool)
|
||||
} else {
|
||||
log.warn(s"failed to build a ${obj.Definition.Name} belonging to ${obj.OwnerName.getOrElse("no one")}")
|
||||
|
|
@ -43,6 +45,7 @@ class ZoneDeployableActor(zone: Zone, deployableList: ListBuffer[Deployable]) ex
|
|||
if (DeployableDismiss(obj, deployableList)) {
|
||||
obj.Actor ! Zone.Deployable.IsDismissed(obj)
|
||||
obj.Definition.Uninitialize(obj, context)
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(obj)
|
||||
}
|
||||
|
||||
case Zone.Deployable.IsBuilt(_) => ;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.objects.zones
|
||||
|
||||
import akka.actor.Actor
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
import net.psforever.services.Service
|
||||
|
|
@ -21,26 +22,28 @@ class ZoneGroundActor(zone: Zone, equipmentOnGround: ListBuffer[Equipment]) exte
|
|||
def receive: Receive = {
|
||||
case Zone.Ground.DropItem(item, pos, orient) =>
|
||||
sender() ! (if (!item.HasGUID) {
|
||||
Zone.Ground.CanNotDropItem(zone, item, "not registered yet")
|
||||
} else if (zone.GUID(item.GUID).isEmpty) {
|
||||
Zone.Ground.CanNotDropItem(zone, item, "registered to some other zone")
|
||||
} else if (equipmentOnGround.contains(item)) {
|
||||
Zone.Ground.CanNotDropItem(zone, item, "already dropped")
|
||||
} else {
|
||||
equipmentOnGround += item
|
||||
item.Position = pos
|
||||
item.Orientation = orient
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.DropItem(Service.defaultPlayerGUID, item)
|
||||
)
|
||||
Zone.Ground.ItemOnGround(item, pos, orient)
|
||||
})
|
||||
Zone.Ground.CanNotDropItem(zone, item, "not registered yet")
|
||||
} else if (zone.GUID(item.GUID).isEmpty) {
|
||||
Zone.Ground.CanNotDropItem(zone, item, "registered to some other zone")
|
||||
} else if (equipmentOnGround.contains(item)) {
|
||||
Zone.Ground.CanNotDropItem(zone, item, "already dropped")
|
||||
} else {
|
||||
equipmentOnGround += item
|
||||
item.Position = pos
|
||||
item.Orientation = orient
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.DropItem(Service.defaultPlayerGUID, item)
|
||||
)
|
||||
zone.actor ! ZoneActor.AddToBlockMap(item, pos)
|
||||
Zone.Ground.ItemOnGround(item, pos, orient)
|
||||
})
|
||||
|
||||
case Zone.Ground.PickupItem(item_guid) =>
|
||||
sender() ! (FindItemOnGround(item_guid) match {
|
||||
case Some(item) =>
|
||||
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PickupItem(Service.defaultPlayerGUID, item, 0))
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(item)
|
||||
Zone.Ground.ItemInHand(item)
|
||||
case None =>
|
||||
Zone.Ground.CanNotPickupItem(zone, item_guid, "can not find")
|
||||
|
|
@ -50,6 +53,7 @@ class ZoneGroundActor(zone: Zone, equipmentOnGround: ListBuffer[Equipment]) exte
|
|||
//intentionally no callback
|
||||
FindItemOnGround(item_guid) match {
|
||||
case Some(item) =>
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(item)
|
||||
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PickupItem(Service.defaultPlayerGUID, item, 0))
|
||||
case None => ;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
package net.psforever.objects.zones
|
||||
|
||||
import akka.actor.{Actor, ActorRef, Props}
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.avatar.{CorpseControl, PlayerControl}
|
||||
import net.psforever.objects.{Default, Player}
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
|
|
@ -34,6 +36,9 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
|
|||
case player @ Some(tplayer) =>
|
||||
tplayer.Zone = Zone.Nowhere
|
||||
PlayerLeave(tplayer)
|
||||
if (tplayer.VehicleSeated.isEmpty) {
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
|
||||
}
|
||||
sender() ! Zone.Population.PlayerHasLeft(zone, player)
|
||||
if (playerMap.isEmpty) {
|
||||
zone.StopPlayerManagementSystems()
|
||||
|
|
@ -52,6 +57,9 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
|
|||
name = GetPlayerControlName(player, None)
|
||||
)
|
||||
player.Zone = zone
|
||||
if (player.VehicleSeated.isEmpty) {
|
||||
zone.actor ! ZoneActor.AddToBlockMap(player, player.Position)
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
sender() ! Zone.Population.PlayerCanNotSpawn(zone, player)
|
||||
|
|
@ -61,6 +69,9 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
|
|||
PopulationRelease(avatar.id, playerMap) match {
|
||||
case Some(tplayer) =>
|
||||
PlayerLeave(tplayer)
|
||||
if (tplayer.VehicleSeated.isEmpty) {
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(tplayer)
|
||||
}
|
||||
sender() ! Zone.Population.PlayerHasLeft(zone, Some(tplayer))
|
||||
case None =>
|
||||
sender() ! Zone.Population.PlayerHasLeft(zone, None)
|
||||
|
|
@ -85,11 +96,13 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
|
|||
name = s"corpse_of_${GetPlayerControlName(player, control)}"
|
||||
)
|
||||
player.Zone = zone
|
||||
zone.actor ! ZoneActor.AddToBlockMap(player, player.Position)
|
||||
}
|
||||
|
||||
case Zone.Corpse.Remove(player) =>
|
||||
if (CorpseRemove(player, corpseList)) {
|
||||
PlayerLeave(player)
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(player)
|
||||
}
|
||||
|
||||
case _ => ;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.objects.zones
|
||||
|
||||
import akka.actor.{Actor, Props}
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.{Default, Vehicle}
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.vehicles.VehicleControl
|
||||
|
|
@ -44,6 +45,9 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act
|
|||
vehicle.Actor =
|
||||
context.actorOf(Props(classOf[VehicleControl], vehicle), PlanetSideServerObject.UniqueActorName(vehicle))
|
||||
}
|
||||
if (vehicle.MountedIn.isEmpty) {
|
||||
zone.actor ! ZoneActor.AddToBlockMap(vehicle, vehicle.Position)
|
||||
}
|
||||
sender() ! Zone.Vehicle.HasSpawned(zone, vehicle)
|
||||
|
||||
case Zone.Vehicle.Despawn(vehicle) =>
|
||||
|
|
@ -52,6 +56,7 @@ class ZoneVehicleActor(zone: Zone, vehicleList: ListBuffer[Vehicle]) extends Act
|
|||
vehicleList.remove(index)
|
||||
context.stop(vehicle.Actor)
|
||||
vehicle.Actor = Default.Actor
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(vehicle)
|
||||
sender() ! Zone.Vehicle.HasDespawned(zone, vehicle)
|
||||
case None => ;
|
||||
sender() ! Zone.Vehicle.CanNotDespawn(zone, vehicle, "can not find")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,397 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.zones.blockmap
|
||||
|
||||
import net.psforever.objects.PlanetSideGameObject
|
||||
import net.psforever.objects.serverobject.environment.PieceOfEnvironment
|
||||
import net.psforever.objects.serverobject.structures.Building
|
||||
import net.psforever.objects.zones.MapScale
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
/**
|
||||
* A data structure which divides coordinate space into buckets or coordinate spans.
|
||||
* The function of the blockmap is to organize the instantiated game objects (entities)
|
||||
* that can be represented in coordinate space into a bucket each or into multiple buckets each
|
||||
* that reflect their locality with other game objects in the same coordinate space.
|
||||
* Polling based on either positions or on entities should be able to recover a lists of entities
|
||||
* that are considered neighbors in the context of that position and a rectangular distance around the position.
|
||||
* The purpose of the blockmap is to improve targeting when making such locality determinations.<br>
|
||||
* <br>
|
||||
* The coordinate space of a PlanetSide zone may contain 65535 entities, one of which is the same target entity.
|
||||
* A bucket on the blockmap should contain only a small fraction of the full zone's entities.
|
||||
* @param fullMapWidth maximum width of the coordinate space (m)
|
||||
* @param fullMapHeight maximum height of the coordinate space (m)
|
||||
* @param desiredSpanSize the amount of coordinate space attributed to each bucket in the blockmap (m)
|
||||
*/
|
||||
class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) {
|
||||
/** a clamping of the desired span size to a realistic value to use for the span size;
|
||||
* blocks can not be too small, but also should not be much larger than the width of the representable region
|
||||
* a block spanning as wide as the map is an acceptable cap
|
||||
*/
|
||||
val spanSize: Int = math.min(math.max(10, desiredSpanSize), fullMapWidth)
|
||||
/** how many sectors are in a row;
|
||||
* the far side sector may run off into un-navigable regions but will always contain a sliver of represented map space,
|
||||
* for example, on a 0-10 grid where the span size is 3, the spans will begin at (0, 3, 6, 9)
|
||||
* and the last span will only have two-thirds of its region valid;
|
||||
* the invalid, not represented regions should be silently ignored
|
||||
*/
|
||||
val blocksInRow: Int = fullMapWidth / spanSize + (if (fullMapWidth % spanSize > 0) 1 else 0)
|
||||
/** the sectors / blocks / buckets into which entities that submit themselves are divided;
|
||||
* while the represented region need not be square, the sectors are defined as squares
|
||||
*/
|
||||
val blocks: ListBuffer[Sector] = {
|
||||
val horizontal: List[Int] = List.range(0, fullMapWidth, spanSize)
|
||||
val vertical: List[Int] = List.range(0, fullMapHeight, spanSize)
|
||||
ListBuffer.newBuilder[Sector].addAll(
|
||||
vertical.flatMap { latitude =>
|
||||
horizontal.map { longitude =>
|
||||
new Sector(longitude, latitude, spanSize)
|
||||
}
|
||||
}
|
||||
).result()
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a blockmap entity,
|
||||
* one that is allegedly represented on this blockmap,
|
||||
* find the sector conglomerate in which this entity is allocated.
|
||||
* @see `BlockMap.quickToSectorGroup`
|
||||
* @param entity the target entity
|
||||
* @return a conglomerate sector which lists all of the entities in the discovered sector(s)
|
||||
*/
|
||||
def sector(entity: BlockMapEntity): SectorPopulation = {
|
||||
entity.blockMapEntry match {
|
||||
case Some(entry) => BlockMap.quickToSectorGroup(entry.sectors.map { blocks })
|
||||
case None => SectorGroup(Nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a coordinate position within representable space and a range from that representable space,
|
||||
* find the sector conglomerate to which this range allocates.
|
||||
* @see `BlockMap.findSectorIndices`
|
||||
* @see `BlockMap.quickToSectorGroup`
|
||||
* @param p the game world coordinates
|
||||
* @param range the axis distance from the provided coordinates
|
||||
* @return a conglomerate sector which lists all of the entities in the discovered sector(s)
|
||||
*/
|
||||
def sector(p: Vector3, range: Float): SectorPopulation = {
|
||||
BlockMap.quickToSectorGroup( BlockMap.findSectorIndices(blockMap = this, p, range).map { blocks } )
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate this entity into appropriate sectors on the blockmap.
|
||||
* @see `addTo(BlockMapEntity, Vector3)`
|
||||
* @param target the entity
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def addTo(target: BlockMapEntity): SectorPopulation = {
|
||||
addTo(target, target.Position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate this entity into appropriate sectors on the blockmap
|
||||
* at the provided game world coordinates.
|
||||
* @see `addTo(BlockMapEntity, Vector3, Float)`
|
||||
* @see `BlockMap.rangeFromEntity`
|
||||
* @param target the entity
|
||||
* @param toPosition the custom game world coordinates that indicate the central sector
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def addTo(target: BlockMapEntity, toPosition: Vector3): SectorPopulation = {
|
||||
addTo(target, toPosition, BlockMap.rangeFromEntity(target))
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate this entity into appropriate sectors on the blockmap
|
||||
* using the provided custom axis range.
|
||||
* @see `addTo(BlockMapEntity, Vector3, Float)`
|
||||
* @param target the entity
|
||||
* @param range the custom distance from the central sector along the major axes
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def addTo(target: BlockMapEntity, range: Float): SectorPopulation = {
|
||||
addTo(target, target.Position, range)
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate this entity into appropriate sectors on the blockmap
|
||||
* using the provided game world coordinates and the provided axis range.
|
||||
* @see `BlockMap.findSectorIndices`
|
||||
* @param target the entity
|
||||
* @param toPosition the game world coordinates that indicate the central sector
|
||||
* @param range the distance from the central sector along the major axes
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def addTo(target: BlockMapEntity, toPosition: Vector3, range: Float): SectorPopulation = {
|
||||
val to = BlockMap.findSectorIndices(blockMap = this, toPosition, range)
|
||||
val toSectors = to.toSet.map { blocks }
|
||||
toSectors.foreach { block => block.addTo(target) }
|
||||
target.blockMapEntry = Some(BlockMapEntry(toPosition, range, to.toSet))
|
||||
BlockMap.quickToSectorGroup(toSectors)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deallocate this entity from appropriate sectors on the blockmap.
|
||||
* @see `actuallyRemoveFrom(BlockMapEntity, Vector3, Float)`
|
||||
* @param target the entity
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def removeFrom(target: BlockMapEntity): SectorPopulation = {
|
||||
target.blockMapEntry match {
|
||||
case Some(entry) => actuallyRemoveFrom(target, entry.coords, entry.range)
|
||||
case None => SectorGroup(Nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deallocate this entity from appropriate sectors on the blockmap.
|
||||
* Other parameters are included for symmetry with a respective `addto` method,
|
||||
* but are ignored since removing an entity from a sector from which it is not represented is ill-advised
|
||||
* as is not removing an entity from any sector that it occupies.
|
||||
* @see `removeFrom(BlockMapEntity)`
|
||||
* @param target the entity
|
||||
* @param fromPosition ignored
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def removeFrom(target: BlockMapEntity, fromPosition: Vector3): SectorPopulation = {
|
||||
removeFrom(target)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deallocate this entity from appropriate sectors on the blockmap.
|
||||
* Other parameters are included for symmetry with a respective `addto` method,
|
||||
* but are ignored since removing an entity from a sector from which it is not represented is ill-advised
|
||||
* as is not removing an entity from any sector that it occupies.
|
||||
* @see `removeFrom(BlockMapEntity)`
|
||||
* @param target the entity
|
||||
* @param range ignored
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def removeFrom(target: BlockMapEntity, range: Float): SectorPopulation =
|
||||
removeFrom(target)
|
||||
|
||||
/**
|
||||
* Deallocate this entity from appropriate sectors on the blockmap.
|
||||
* Other parameters are included for symmetry with a respective `addto` method,
|
||||
* but are ignored since removing an entity from a sector from which it is not represented is ill-advised
|
||||
* as is not removing an entity from any sector that it occupies.
|
||||
* @see `removeFrom(BlockMapEntity)`
|
||||
* @param target the entity
|
||||
* @param fromPosition ignored
|
||||
* @param range ignored
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def removeFrom(target: BlockMapEntity, fromPosition: Vector3, range: Float): SectorPopulation = {
|
||||
removeFrom(target)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deallocate this entity from appropriate sectors on the blockmap.
|
||||
* Really.
|
||||
* @param target the entity
|
||||
* @param fromPosition the game world coordinates that indicate the central sector
|
||||
* @param range the distance from the central sector along the major axes
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
private def actuallyRemoveFrom(target: BlockMapEntity, fromPosition: Vector3, range: Float): SectorPopulation = {
|
||||
target.blockMapEntry match {
|
||||
case Some(entry) =>
|
||||
target.blockMapEntry = None
|
||||
val from = entry.sectors.map { blocks }
|
||||
from.foreach { block => block.removeFrom(target) }
|
||||
BlockMap.quickToSectorGroup(from)
|
||||
case None =>
|
||||
SectorGroup(Nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an entity on the blockmap structure and update the prerequisite internal information.
|
||||
* @see `move(BlockMapEntity, Vector3, Vector3, Float)`
|
||||
* @param target the entity
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def move(target: BlockMapEntity): SectorPopulation = {
|
||||
target.blockMapEntry match {
|
||||
case Some(entry) => move(target, target.Position, entry.coords, entry.range)
|
||||
case None => SectorGroup(Nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an entity on the blockmap structure and update the prerequisite internal information.
|
||||
* @see `move(BlockMapEntity, Vector3, Vector3, Float)`
|
||||
* @param target the entity
|
||||
* @param toPosition the next location of the entity in world coordinates
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def move(target: BlockMapEntity, toPosition: Vector3): SectorPopulation = {
|
||||
target.blockMapEntry match {
|
||||
case Some(entry) => move(target, toPosition, entry.coords, entry.range)
|
||||
case None => SectorGroup(Nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an entity on the blockmap structure and update the prerequisite internal information.
|
||||
* @see `move(BlockMapEntity, Vector3)`
|
||||
* @param target the entity
|
||||
* @param toPosition the next location of the entity in world coordinates
|
||||
* @param fromPosition ignored
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def move(target: BlockMapEntity, toPosition: Vector3, fromPosition: Vector3): SectorPopulation = {
|
||||
move(target, toPosition)
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an entity on the blockmap structure and update the prerequisite internal information.
|
||||
* @param target the entity
|
||||
* @param toPosition the next location of the entity in world coordinates
|
||||
* @param fromPosition the current location of the entity in world coordinates
|
||||
* @param range the distance from the location along the major axes
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def move(target: BlockMapEntity, toPosition: Vector3, fromPosition: Vector3, range: Float): SectorPopulation = {
|
||||
target.blockMapEntry match {
|
||||
case Some(entry) =>
|
||||
val from = entry.sectors
|
||||
val to = BlockMap.findSectorIndices(blockMap = this, toPosition, range).toSet
|
||||
to.diff(from).foreach { index => blocks(index).addTo(target) }
|
||||
from.diff(to).foreach { index => blocks(index).removeFrom(target) }
|
||||
target.blockMapEntry = Some(BlockMapEntry(toPosition, range, to))
|
||||
BlockMap.quickToSectorGroup(to.map { blocks })
|
||||
case None =>
|
||||
SectorGroup(Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object BlockMap {
|
||||
/**
|
||||
* Overloaded constructor that uses a `MapScale` field, common with `Zone` entities.
|
||||
* @param scale the two-dimensional scale of the map
|
||||
* @param desiredSpanSize the length and width of a sector
|
||||
* @return a ` BlockMap` entity
|
||||
*/
|
||||
def apply(scale: MapScale, desiredSpanSize: Int): BlockMap = {
|
||||
new BlockMap(scale.width.toInt, scale.height.toInt, desiredSpanSize)
|
||||
}
|
||||
|
||||
/**
|
||||
* The blockmap is mapped to a coordinate range in two directions,
|
||||
* so find the indices of the sectors that correspond to the region
|
||||
* defined by the range around a coordinate position.
|
||||
* @param blockMap the blockmap structure
|
||||
* @param p the coordinate position
|
||||
* @param range a rectangular range aigned with lateral axes extending from a coordinate position
|
||||
* @return the indices of the sectors in the blockmap structure
|
||||
*/
|
||||
def findSectorIndices(blockMap: BlockMap, p: Vector3, range: Float): Iterable[Int] = {
|
||||
findSectorIndices(blockMap.spanSize, blockMap.blocksInRow, blockMap.blocks.size, p, range)
|
||||
}
|
||||
|
||||
/**
|
||||
* The blockmap is mapped to a coordinate range in two directions,
|
||||
* so find the indices of the sectors that correspond to the region
|
||||
* defined by the range around a coordinate position.
|
||||
* @param spanSize the length and width of a sector
|
||||
* @param blocksInRow the number of sectors across the width (in a row) of the blockmap
|
||||
* @param blocksTotal the number of sectors in the blockmap
|
||||
* @param p the coordinate position
|
||||
* @param range a rectangular range aigned with lateral axes extending from a coordinate position
|
||||
* @return the indices of the sectors in the blockmap structure
|
||||
*/
|
||||
private def findSectorIndices(spanSize: Int, blocksInRow: Int, blocksTotal: Int, p: Vector3, range: Float): Iterable[Int] = {
|
||||
val corners = {
|
||||
/*
|
||||
find the corners of a rectangular region extending in all cardinal directions from the position;
|
||||
transform these corners into four sector indices;
|
||||
if the first index matches the last index, the position and range are only in one sector;
|
||||
[----][----][----]
|
||||
[----][1234][----]
|
||||
[----][----][----]
|
||||
if the first and the second or the first and the third are further apart than an adjacent column or row,
|
||||
then the missing indices need to be filled in and all of those sectors include the position and range;
|
||||
[----][----][----][----][----]
|
||||
[----][1 ][ ][2 ][----]
|
||||
[----][ ][ ][ ][----]
|
||||
[----][3 ][ ][4 ][----]
|
||||
[----][----][----][----][----]
|
||||
if neither of the previous, just return all distinct corner indices
|
||||
[----][----][----][----] [----][----][----] [----][----][----][----]
|
||||
[----][1 ][2 ][----] [----][1 2][----] [----][1 3][2 4][----]
|
||||
[----][3 ][4 ][----] [----][3 4][----] [----][----][----][----]
|
||||
[----][----][----][----] [----][----][----]
|
||||
*/
|
||||
val blocksInColumn = blocksTotal / blocksInRow
|
||||
val lowx = math.max(0, p.x - range)
|
||||
val highx = math.min(p.x + range, (blocksInRow * spanSize - 1).toFloat)
|
||||
val lowy = math.max(0, p.y - range)
|
||||
val highy = math.min(p.y + range, (blocksInColumn * spanSize - 1).toFloat)
|
||||
Seq( (lowx, lowy), (highx, lowy), (lowx, highy), (highx, highy) )
|
||||
}.map { case (x, y) =>
|
||||
(y / spanSize).toInt * blocksInRow + (x / spanSize).toInt
|
||||
}
|
||||
if (corners.head == corners(3)) {
|
||||
List(corners.head)
|
||||
} else if (corners(1) - corners.head > 1 || corners(2) - corners.head > blocksInRow) {
|
||||
(0 to (corners(2) - corners.head) / blocksInRow).flatMap { d =>
|
||||
val perRow = d * blocksInRow
|
||||
(corners.head + perRow) to (corners(1) + perRow)
|
||||
}
|
||||
} else {
|
||||
corners.distinct
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the range expressed by a certain entity that can be allocated into a sector on the blockmap.
|
||||
* Entities have different ways of expressing these ranges.
|
||||
* @param target the entity
|
||||
* @param defaultRadius a default radius, if no specific case is discovered;
|
||||
* if no default case, the default-default case is a single unit (`1.0f`)
|
||||
* @return the distance from a central position along the major axes
|
||||
*/
|
||||
def rangeFromEntity(target: BlockMapEntity, defaultRadius: Option[Float] = None): Float = {
|
||||
target match {
|
||||
case b: Building =>
|
||||
//use the building's sphere of influence
|
||||
b.Definition.SOIRadius.toFloat// * 0.5f
|
||||
|
||||
case o: PlanetSideGameObject =>
|
||||
//use the server geometry
|
||||
val pos = target.Position
|
||||
val v = o.Definition.Geometry(o)
|
||||
math.sqrt(math.max(
|
||||
Vector3.DistanceSquared(pos, v.pointOnOutside(Vector3(1,0,0)).asVector3),
|
||||
Vector3.DistanceSquared(pos, v.pointOnOutside(Vector3(0,1,0)).asVector3)
|
||||
)).toFloat
|
||||
|
||||
case e: PieceOfEnvironment =>
|
||||
//use the bounds (like server geometry, but is alawys a rectangle on the XY-plane)
|
||||
val bounds = e.collision.bounding
|
||||
math.max(bounds.top - bounds.base, bounds.right - bounds.left) * 0.5f
|
||||
|
||||
case _ =>
|
||||
//default and default-default
|
||||
defaultRadius.getOrElse(1.0f)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If only one sector, just return that sector.
|
||||
* If a group of sectors, organize them into a single referential sector.
|
||||
* @param to all allocated sectors
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def quickToSectorGroup(to: Iterable[Sector]): SectorPopulation = {
|
||||
if (to.size == 1) {
|
||||
SectorGroup(to.head)
|
||||
} else {
|
||||
SectorGroup(to)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.zones.blockmap
|
||||
|
||||
import net.psforever.objects.entity.WorldEntity
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
sealed case class BlockMapEntry(coords: Vector3, range: Float, sectors: Set[Int])
|
||||
|
||||
/**
|
||||
* An game object that can be represented on a blockmap.
|
||||
* The only requirement is that the entity can position itself in a zone's coordinate space.
|
||||
* @see `BlockMap`
|
||||
* @see `WorldEntity`
|
||||
*/
|
||||
trait BlockMapEntity
|
||||
extends WorldEntity {
|
||||
/** internal data regarding an active representation on a blockmap */
|
||||
private var _blockMapEntry: Option[BlockMapEntry] = None
|
||||
/** the function that allows for updates of the internal data */
|
||||
private var _updateBlockMapEntryFunc: (BlockMapEntity, Vector3) => Boolean = BlockMapEntity.doNotUpdateBlockMap
|
||||
|
||||
/** internal data regarding an active representation on a blockmap */
|
||||
def blockMapEntry: Option[BlockMapEntry] = _blockMapEntry
|
||||
/** internal data regarding an active representation on a blockmap */
|
||||
def blockMapEntry_=(entry: Option[BlockMapEntry]): Option[BlockMapEntry] = {
|
||||
entry match {
|
||||
case None =>
|
||||
_updateBlockMapEntryFunc = BlockMapEntity.doNotUpdateBlockMap
|
||||
_blockMapEntry = None
|
||||
case Some(_) =>
|
||||
_updateBlockMapEntryFunc = BlockMapEntity.updateBlockMap
|
||||
_blockMapEntry = entry
|
||||
}
|
||||
entry
|
||||
}
|
||||
|
||||
/**
|
||||
* Buckets in the blockmap are called "sectors".
|
||||
* Find the sectors in a given blockmap in which the entity would be represented within a given range.
|
||||
* @param zone what region the blockmap represents
|
||||
* @param range the custom distance from the central sector along the major axes
|
||||
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
|
||||
*/
|
||||
def sector(zone: Zone, range: Float): SectorPopulation = {
|
||||
zone.blockMap.sector(
|
||||
//TODO same zone check?
|
||||
_blockMapEntry match {
|
||||
case Some(entry) => entry.coords
|
||||
case None => Position
|
||||
},
|
||||
range
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the internal data's known coordinate position without changing representation on whatever blockmap.
|
||||
* Has the potential to cause major issues with the blockmap if used without external checks.
|
||||
* @param newCoords the coordinate position
|
||||
* @return `true`, if the coordinates were updated;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def updateBlockMapEntry(newCoords: Vector3): Boolean = _updateBlockMapEntryFunc(this, newCoords)
|
||||
}
|
||||
|
||||
object BlockMapEntity {
|
||||
/**
|
||||
* The entity is currently excluded from being represented on a blockmap structure.
|
||||
* There is no need to update.
|
||||
* @param target the entity on the blockmap
|
||||
* @param newCoords the world coordinates of the entity, the position to which it is moving / being moved
|
||||
* @return always `false`; we're not updating the entry
|
||||
*/
|
||||
private def doNotUpdateBlockMap(target: BlockMapEntity, newCoords: Vector3): Boolean = false
|
||||
|
||||
/**
|
||||
* Re-using other data from the entry,
|
||||
* update the data of the target entity's internal understanding of where it is represented on a blockmap.
|
||||
* Act as if the sector and the range that is encompassed never change,
|
||||
* though the game world coordinates of the entity have been changed.
|
||||
* (The range would probably not have changed in any case.
|
||||
* To properly update the range, perform a proper update.)
|
||||
* @param target the entity on the blockmap
|
||||
* @param newCoords the world coordinates of the entity, the position to which it is moving / being moved
|
||||
* @return always `true`; we are updating this entry
|
||||
*/
|
||||
private def updateBlockMap(target: BlockMapEntity, newCoords: Vector3): Boolean = {
|
||||
val oldEntry = target.blockMapEntry.get
|
||||
target.blockMapEntry = Some(BlockMapEntry(newCoords, oldEntry.range, oldEntry.sectors))
|
||||
true
|
||||
}
|
||||
}
|
||||
282
src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala
Normal file
282
src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.zones.blockmap
|
||||
|
||||
import net.psforever.objects.ce.Deployable
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.objects.serverobject.environment.PieceOfEnvironment
|
||||
import net.psforever.objects.serverobject.structures.{Amenity, Building}
|
||||
import net.psforever.objects.{Player, Vehicle}
|
||||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
/**
|
||||
* The collections of entities in a sector conglomerate.
|
||||
*/
|
||||
trait SectorPopulation {
|
||||
def livePlayerList: List[Player]
|
||||
|
||||
def corpseList: List[Player]
|
||||
|
||||
def vehicleList: List[Vehicle]
|
||||
|
||||
def equipmentOnGroundList: List[Equipment]
|
||||
|
||||
def deployableList: List[Deployable]
|
||||
|
||||
def buildingList: List[Building]
|
||||
|
||||
def amenityList: List[Amenity]
|
||||
|
||||
def environmentList: List[PieceOfEnvironment]
|
||||
|
||||
/**
|
||||
* A count of all the entities in all the lists.
|
||||
*/
|
||||
def total: Int = {
|
||||
livePlayerList.size +
|
||||
corpseList.size +
|
||||
vehicleList.size +
|
||||
equipmentOnGroundList.size +
|
||||
deployableList.size +
|
||||
buildingList.size +
|
||||
amenityList.size +
|
||||
environmentList.size
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the sector.
|
||||
*/
|
||||
trait SectorTraits {
|
||||
/** the starting coordinate of the region (in terms of width) */
|
||||
def longitude: Float
|
||||
/** the starting coordinate of the region (in terms of length) */
|
||||
def latitude: Float
|
||||
/** how width and long is the region */
|
||||
def span: Int
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom lists of entities for sector buckets.
|
||||
* @param eqFunc a custom equivalence function to distinguish between the entities in the list
|
||||
* @tparam A the type of object that will be the entities stored in the list
|
||||
*/
|
||||
class SectorListOf[A](eqFunc: (A, A) => Boolean = (a: A, b: A) => a equals b) {
|
||||
private val internalList: ListBuffer[A] = ListBuffer[A]()
|
||||
|
||||
/**
|
||||
* Insert the entity into the list as long as it cannot be found in the list
|
||||
* according to the custom equivalence function.
|
||||
* @param elem the entity
|
||||
* @return a conventional list of entities
|
||||
*/
|
||||
def addTo(elem: A): List[A] = {
|
||||
internalList.indexWhere { item => eqFunc(elem, item) } match {
|
||||
case -1 => internalList.addOne(elem)
|
||||
case _ => ;
|
||||
}
|
||||
list
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the entity from the list as long as it can be found in the list
|
||||
* according to the custom equivalence function.
|
||||
* @param elem the entity
|
||||
* @return a conventional list of entities
|
||||
*/
|
||||
def removeFrom(elem: A): List[A] = {
|
||||
internalList.indexWhere { item => eqFunc(elem, item) } match {
|
||||
case -1 => ;
|
||||
case index => internalList.remove(index)
|
||||
}
|
||||
list
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast this specialized list of entities into a conventional list of entities.
|
||||
* @return a conventional list of entities
|
||||
*/
|
||||
def list: List[A] = internalList.toList
|
||||
}
|
||||
|
||||
/**
|
||||
* The bucket of a blockmap structure
|
||||
* that contains lists of entities that, within a given span of coordinate distance,
|
||||
* are considered neighbors.
|
||||
* While the coordinate space that supports a blockmap (?) may be any combination of two dimensions,
|
||||
* the sectors are always square.
|
||||
* @param longitude a starting coordinate of the region (in terms of width)
|
||||
* @param latitude a starting coordinate of the region (in terms of length)
|
||||
* @param span the distance across the sector in both directions
|
||||
*/
|
||||
class Sector(val longitude: Int, val latitude: Int, val span: Int)
|
||||
extends SectorPopulation {
|
||||
private val livePlayers: SectorListOf[Player] = new SectorListOf[Player](
|
||||
(a: Player, b: Player) => a.CharId == b.CharId
|
||||
)
|
||||
|
||||
private val corpses: SectorListOf[Player] = new SectorListOf[Player](
|
||||
(a: Player, b: Player) => a.GUID == b.GUID || (a eq b)
|
||||
)
|
||||
|
||||
private val vehicles: SectorListOf[Vehicle] = new SectorListOf[Vehicle](
|
||||
(a: Vehicle, b: Vehicle) => a eq b
|
||||
)
|
||||
|
||||
private val equipmentOnGround: SectorListOf[Equipment] = new SectorListOf[Equipment](
|
||||
(a: Equipment, b: Equipment) => a eq b
|
||||
)
|
||||
|
||||
private val deployables: SectorListOf[Deployable] = new SectorListOf[Deployable](
|
||||
(a: Deployable, b: Deployable) => a eq b
|
||||
)
|
||||
|
||||
private val buildings: SectorListOf[Building] = new SectorListOf[Building](
|
||||
(a: Building, b: Building) => a eq b
|
||||
)
|
||||
|
||||
private val amenities: SectorListOf[Amenity] = new SectorListOf[Amenity](
|
||||
(a: Amenity, b: Amenity) => a eq b
|
||||
)
|
||||
|
||||
private val environment: SectorListOf[PieceOfEnvironment] = new SectorListOf[PieceOfEnvironment](
|
||||
(a: PieceOfEnvironment, b: PieceOfEnvironment) => a eq b
|
||||
)
|
||||
|
||||
def livePlayerList : List[Player] = livePlayers.list
|
||||
|
||||
def corpseList: List[Player] = corpses.list
|
||||
|
||||
def vehicleList: List[Vehicle] = vehicles.list
|
||||
|
||||
def equipmentOnGroundList: List[Equipment] = equipmentOnGround.list
|
||||
|
||||
def deployableList: List[Deployable] = deployables.list
|
||||
|
||||
def buildingList: List[Building] = buildings.list
|
||||
|
||||
def amenityList : List[Amenity] = amenities.list
|
||||
|
||||
def environmentList: List[PieceOfEnvironment] = environment.list
|
||||
|
||||
/**
|
||||
* Appropriate an entity added to this blockmap bucket
|
||||
* inot a list of objects that are like itself.
|
||||
* @param o the entity
|
||||
* @return whether or not the entity was added
|
||||
*/
|
||||
def addTo(o: BlockMapEntity): Boolean = {
|
||||
o match {
|
||||
case p: Player =>
|
||||
//players and corpses are the same kind of object, but are distinguished by a single flag
|
||||
//when adding to the "corpse" list, first attempt to remove from the "player" list
|
||||
if (!p.isBackpack) {
|
||||
livePlayers.list.size < livePlayers.addTo(p).size
|
||||
}
|
||||
else {
|
||||
livePlayers.removeFrom(p)
|
||||
corpses.list.size < corpses.addTo(p).size
|
||||
}
|
||||
case v: Vehicle =>
|
||||
vehicles.list.size < vehicles.addTo(v).size
|
||||
case e: Equipment =>
|
||||
equipmentOnGround.list.size < equipmentOnGround.addTo(e).size
|
||||
case d: Deployable =>
|
||||
deployables.list.size < deployables.addTo(d).size
|
||||
case b: Building =>
|
||||
buildings.list.size < buildings.addTo(b).size
|
||||
case a: Amenity =>
|
||||
amenities.list.size < amenities.addTo(a).size
|
||||
case e: PieceOfEnvironment =>
|
||||
environment.list.size < environment.addTo(e).size
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an entity added to this blockmap bucket
|
||||
* from a list of already-added objects that are like itself.
|
||||
* @param o the entity
|
||||
* @return whether or not the entity was removed
|
||||
*/
|
||||
def removeFrom(o: Any): Boolean = {
|
||||
o match {
|
||||
case p: Player =>
|
||||
livePlayers.list.size > livePlayers.removeFrom(p).size ||
|
||||
corpses.list.size > corpses.removeFrom(p).size
|
||||
case v: Vehicle =>
|
||||
vehicles.list.size > vehicles.removeFrom(v).size
|
||||
case e: Equipment =>
|
||||
equipmentOnGround.list.size > equipmentOnGround.removeFrom(e).size
|
||||
case d: Deployable =>
|
||||
deployables.list.size > deployables.removeFrom(d).size
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The specific datastructure that is mentioned when using the term "sector conglomerate".
|
||||
* Typically used to compose the lists of entities from various individual sectors.
|
||||
* @param livePlayerList the living players
|
||||
* @param corpseList the dead players
|
||||
* @param vehicleList vehicles
|
||||
* @param equipmentOnGroundList dropped equipment
|
||||
* @param deployableList deployed combat engineering gear
|
||||
* @param buildingList the structures
|
||||
* @param amenityList the structures within the structures
|
||||
* @param environmentList fields that represent the game world environment
|
||||
*/
|
||||
class SectorGroup(
|
||||
val livePlayerList: List[Player],
|
||||
val corpseList: List[Player],
|
||||
val vehicleList: List[Vehicle],
|
||||
val equipmentOnGroundList: List[Equipment],
|
||||
val deployableList: List[Deployable],
|
||||
val buildingList: List[Building],
|
||||
val amenityList: List[Amenity],
|
||||
val environmentList: List[PieceOfEnvironment]
|
||||
)
|
||||
extends SectorPopulation
|
||||
|
||||
object SectorGroup {
|
||||
/**
|
||||
* Overloaded constructor that takes a single sector
|
||||
* and transfers the lists of entities into a single conglomeration of the sector populations.
|
||||
* @param sector the sector to be counted
|
||||
* @return a `SectorGroup` object
|
||||
*/
|
||||
def apply(sector: Sector): SectorGroup = {
|
||||
new SectorGroup(
|
||||
sector.livePlayerList,
|
||||
sector.corpseList,
|
||||
sector.vehicleList,
|
||||
sector.equipmentOnGroundList,
|
||||
sector.deployableList,
|
||||
sector.buildingList,
|
||||
sector.amenityList,
|
||||
sector.environmentList
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloaded constructor that takes a group of sectors
|
||||
* and condenses all of the lists of entities into a single conglomeration of the sector populations.
|
||||
* @param sectors the series of sectors to be counted
|
||||
* @return a `SectorGroup` object
|
||||
*/
|
||||
def apply(sectors: Iterable[Sector]): SectorGroup = {
|
||||
new SectorGroup(
|
||||
sectors.flatMap { _.livePlayerList }.toList.distinct,
|
||||
sectors.flatMap { _.corpseList }.toList.distinct,
|
||||
sectors.flatMap { _.vehicleList }.toList.distinct,
|
||||
sectors.flatMap { _.equipmentOnGroundList }.toList.distinct,
|
||||
sectors.flatMap { _.deployableList }.toList.distinct,
|
||||
sectors.flatMap { _.buildingList }.toList.distinct,
|
||||
sectors.flatMap { _.amenityList }.toList.distinct,
|
||||
sectors.flatMap { _.environmentList }.toList.distinct
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2923,18 +2923,20 @@ class SquadService extends Actor {
|
|||
.toList
|
||||
.unzip { case (member, index) => (member.CharId, index) }
|
||||
val toChannel = features.ToChannel
|
||||
memberCharIds.foreach { charId =>
|
||||
SquadEvents.subscribe(UserEvents(charId), s"/$toChannel/Squad")
|
||||
Publish(
|
||||
charId,
|
||||
SquadResponse.Join(
|
||||
squad,
|
||||
indices.filterNot(_ == position) :+ position,
|
||||
toChannel
|
||||
)
|
||||
)
|
||||
InitWaypoints(charId, squad.GUID)
|
||||
}
|
||||
memberCharIds
|
||||
.map { id => (id, UserEvents.get(id)) }
|
||||
.collect { case (id, Some(sub)) =>
|
||||
SquadEvents.subscribe(sub, s"/$toChannel/Squad")
|
||||
Publish(
|
||||
id,
|
||||
SquadResponse.Join(
|
||||
squad,
|
||||
indices.filterNot(_ == position) :+ position,
|
||||
toChannel
|
||||
)
|
||||
)
|
||||
InitWaypoints(id, squad.GUID)
|
||||
}
|
||||
//fully update for all users
|
||||
InitSquadDetail(squad)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ package objects
|
|||
import akka.actor.{ActorRef, Props}
|
||||
import akka.testkit.TestProbe
|
||||
import base.{ActorTest, FreedContextActorTest}
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.avatar.{Avatar, Certification, PlayerControl}
|
||||
import net.psforever.objects.{ConstructionItem, Deployables, GlobalDefinitions, Player}
|
||||
import net.psforever.objects.ce.{Deployable, DeployedItem}
|
||||
|
|
@ -32,6 +34,8 @@ class DeployableBehaviorSetupTest extends ActorTest {
|
|||
override def AvatarEvents: ActorRef = eventsProbe.ref
|
||||
override def LocalEvents: ActorRef = eventsProbe.ref
|
||||
override def Deployables: ActorRef = deployables
|
||||
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
guid.register(jmine, number = 1)
|
||||
jmine.Faction = PlanetSideEmpire.TR
|
||||
|
|
@ -91,6 +95,8 @@ class DeployableBehaviorSetupOwnedP1Test extends ActorTest {
|
|||
override def Deployables: ActorRef = deployables
|
||||
override def Players = List(avatar)
|
||||
override def LivePlayers = List(player)
|
||||
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
guid.register(jmine, number = 1)
|
||||
guid.register(citem, number = 2)
|
||||
|
|
@ -136,6 +142,8 @@ class DeployableBehaviorSetupOwnedP2Test extends FreedContextActorTest {
|
|||
override def Players = List(avatar)
|
||||
override def LivePlayers = List(player)
|
||||
override def tasks: ActorRef = eventsProbe.ref
|
||||
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
guid.register(jmine, number = 1)
|
||||
guid.register(citem, number = 2)
|
||||
|
|
@ -238,6 +246,8 @@ class DeployableBehaviorDeconstructTest extends ActorTest {
|
|||
override def LocalEvents: ActorRef = eventsProbe.ref
|
||||
override def tasks: ActorRef = eventsProbe.ref
|
||||
override def Deployables: ActorRef = deployables
|
||||
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
guid.register(jmine, number = 1)
|
||||
jmine.Faction = PlanetSideEmpire.TR
|
||||
|
|
@ -295,6 +305,8 @@ class DeployableBehaviorDeconstructOwnedTest extends FreedContextActorTest {
|
|||
override def Players = List(avatar)
|
||||
override def LivePlayers = List(player)
|
||||
override def tasks: ActorRef = eventsProbe.ref
|
||||
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
guid.register(jmine, number = 1)
|
||||
guid.register(citem, number = 2)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package objects
|
|||
import akka.actor.{Actor, ActorRef, Props}
|
||||
import akka.testkit.TestProbe
|
||||
import base.ActorTest
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.ballistics._
|
||||
import net.psforever.objects.ce.DeployedItem
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
|
|
@ -23,6 +24,8 @@ import net.psforever.objects.vital.base.DamageResolution
|
|||
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
||||
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.concurrent.duration._
|
||||
|
||||
|
|
@ -339,7 +342,7 @@ class ExplosiveDeployableJammerTest extends ActorTest {
|
|||
j_mine.Owner = player2
|
||||
j_mine.OwnerName = player2.Name
|
||||
j_mine.Faction = PlanetSideEmpire.NC
|
||||
j_mine.Actor = system.actorOf(Props(classOf[ExplosiveDeployableControl], j_mine), "j-mine-control")
|
||||
j_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], j_mine), "j-mine-control")
|
||||
|
||||
val jMineSource = SourceEntry(j_mine)
|
||||
val pSource = PlayerSource(player1)
|
||||
|
|
@ -406,6 +409,8 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
|
|||
override def Players = List(avatar1, avatar2)
|
||||
override def LivePlayers = List(player1, player2)
|
||||
override def tasks: ActorRef = eventsProbe.ref
|
||||
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
player1.Spawn()
|
||||
player1.Actor = player1Probe.ref
|
||||
|
|
@ -421,7 +426,9 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
|
|||
h_mine.Owner = player2
|
||||
h_mine.OwnerName = player2.Name
|
||||
h_mine.Faction = PlanetSideEmpire.NC
|
||||
h_mine.Actor = system.actorOf(Props(classOf[ExplosiveDeployableControl], h_mine), "h-mine-control")
|
||||
h_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], h_mine), "h-mine-control")
|
||||
zone.blockMap.addTo(player1)
|
||||
zone.blockMap.addTo(player2)
|
||||
|
||||
val pSource = PlayerSource(player1)
|
||||
val projectile = weapon.Projectile
|
||||
|
|
@ -527,7 +534,7 @@ class ExplosiveDeployableDestructionTest extends ActorTest {
|
|||
h_mine.Owner = player2
|
||||
h_mine.OwnerName = player2.Name
|
||||
h_mine.Faction = PlanetSideEmpire.NC
|
||||
h_mine.Actor = system.actorOf(Props(classOf[ExplosiveDeployableControl], h_mine), "h-mine-control")
|
||||
h_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], h_mine), "h-mine-control")
|
||||
|
||||
val hMineSource = SourceEntry(h_mine)
|
||||
val pSource = PlayerSource(player1)
|
||||
|
|
@ -633,6 +640,10 @@ class TurretControlMountTest extends ActorTest {
|
|||
"control mounting" in {
|
||||
val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) { GUID = PlanetSideGUID(1) }
|
||||
obj.Faction = PlanetSideEmpire.TR
|
||||
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
|
||||
override def SetupNumberPools() = {}
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test")
|
||||
|
||||
assert(obj.Seats(0).occupant.isEmpty)
|
||||
|
|
@ -654,6 +665,10 @@ class TurretControlBlockMountTest extends ActorTest {
|
|||
val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) { GUID = PlanetSideGUID(1) }
|
||||
obj.Faction = PlanetSideEmpire.TR
|
||||
obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test")
|
||||
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
|
||||
override def SetupNumberPools() = {}
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
|
||||
assert(obj.Seats(0).occupant.isEmpty)
|
||||
val player1 = Player(Avatar(0, "test1", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
|
||||
|
|
@ -701,6 +716,10 @@ class TurretControlDismountTest extends ActorTest {
|
|||
"control dismounting" in {
|
||||
val obj = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) { GUID = PlanetSideGUID(1) }
|
||||
obj.Faction = PlanetSideEmpire.TR
|
||||
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
|
||||
override def SetupNumberPools() = {}
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test")
|
||||
|
||||
assert(obj.Seats(0).occupant.isEmpty)
|
||||
|
|
@ -733,7 +752,13 @@ class TurretControlBetrayalMountTest extends ActorTest {
|
|||
MountPoints += 1 -> MountInfo(0, Vector3.Zero)
|
||||
FactionLocked = false
|
||||
} //required (defaults to true)
|
||||
) { GUID = PlanetSideGUID(1) }
|
||||
) {
|
||||
GUID = PlanetSideGUID(1)
|
||||
Zone = new Zone("test", new ZoneMap("test"), 0) {
|
||||
override def SetupNumberPools() = {}
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
}
|
||||
obj.Faction = PlanetSideEmpire.TR
|
||||
obj.Actor = system.actorOf(Props(classOf[TurretControl], obj), s"${obj.Definition.Name}_test")
|
||||
val probe = new TestProbe(system)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ package objects
|
|||
|
||||
import akka.actor.Props
|
||||
import akka.testkit.TestProbe
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import base.ActorTest
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.avatar.{Avatar, Certification}
|
||||
import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool}
|
||||
import net.psforever.objects.definition.ToolDefinition
|
||||
|
|
@ -103,6 +105,10 @@ class FacilityTurretControl2Test extends ActorTest {
|
|||
val player = Player(Avatar(0, "", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
|
||||
val obj = FacilityTurret(GlobalDefinitions.manned_turret)
|
||||
obj.GUID = PlanetSideGUID(1)
|
||||
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
|
||||
override def SetupNumberPools() = {}
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control")
|
||||
val bldg = Building("Building", guid = 0, map_id = 0, Zone.Nowhere, StructureType.Building)
|
||||
bldg.Amenities = obj
|
||||
|
|
@ -156,6 +162,10 @@ class FacilityTurretControl4Test extends ActorTest {
|
|||
val player = Player(Avatar(0, "", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
|
||||
val obj = FacilityTurret(GlobalDefinitions.vanu_sentry_turret)
|
||||
obj.GUID = PlanetSideGUID(1)
|
||||
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
|
||||
override def SetupNumberPools() = {}
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
obj.Actor = system.actorOf(Props(classOf[FacilityTurretControl], obj), "turret-control")
|
||||
val bldg = Building("Building", guid = 0, map_id = 0, Zone.Nowhere, StructureType.Building)
|
||||
bldg.Amenities = obj
|
||||
|
|
|
|||
|
|
@ -375,6 +375,8 @@ class GeneratorControlKillsTest extends ActorTest {
|
|||
guid.register(gen, 2)
|
||||
guid.register(player1, 3)
|
||||
guid.register(player2, 4)
|
||||
zone.blockMap.addTo(player1)
|
||||
zone.blockMap.addTo(player2)
|
||||
|
||||
val weapon = Tool(GlobalDefinitions.phoenix) //decimator
|
||||
val projectile = weapon.Projectile
|
||||
|
|
|
|||
|
|
@ -1,238 +1,214 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package objects
|
||||
|
||||
import net.psforever.objects.geometry._
|
||||
import net.psforever.objects.geometry.d3._
|
||||
import net.psforever.types.Vector3
|
||||
import org.specs2.mutable.Specification
|
||||
|
||||
class GeometryTest extends Specification {
|
||||
"Point3D" should {
|
||||
"Point" should {
|
||||
"construct (1)" in {
|
||||
Point3D(1,2,3.5f)
|
||||
Point(1,2,3.5f)
|
||||
ok
|
||||
}
|
||||
|
||||
"construct (2)" in {
|
||||
Point3D() mustEqual Point3D(0,0,0)
|
||||
Point() mustEqual Point(0,0,0)
|
||||
}
|
||||
|
||||
"construct (3)" in {
|
||||
Point3D(Vector3(1,2,3)) mustEqual Point3D(1,2,3)
|
||||
Point(Vector3(1,2,3)) mustEqual Point(1,2,3)
|
||||
}
|
||||
|
||||
"be its own center point" in {
|
||||
val obj = Point3D(1,2,3.5f)
|
||||
val obj = Point(1,2,3.5f)
|
||||
obj.center mustEqual obj
|
||||
}
|
||||
|
||||
"define its own exterior" in {
|
||||
val obj = Point3D(1,2,3.5f)
|
||||
val obj = Point(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)
|
||||
val obj = Point(1,2,3.5f)
|
||||
obj.asVector3 mustEqual Vector3(1,2,3.5f)
|
||||
}
|
||||
}
|
||||
|
||||
"Ray3D" should {
|
||||
"Ray" should {
|
||||
"construct (1)" in {
|
||||
Ray3D(Point3D(1,2,3.5f), Vector3(1,0,0))
|
||||
Ray(Point(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))
|
||||
Ray(1,2,3.5f, Vector3(1,0,0)) mustEqual Ray(Point(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))
|
||||
Ray(Vector3(1,2,3.5f), Vector3(1,0,0)) mustEqual Ray(Point(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]
|
||||
Ray(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
|
||||
val obj = Ray(1,2,3.5f, Vector3(1,0,0))
|
||||
obj.center mustEqual Point(1,2,3.5f)
|
||||
}
|
||||
}
|
||||
|
||||
"Line3D" should {
|
||||
"Line" should {
|
||||
"construct (1)" in {
|
||||
Line3D(Point3D(1,2,3.5f), Vector3(1,0,0))
|
||||
Line(Point(1,2,3.5f), Vector3(1,0,0))
|
||||
ok
|
||||
}
|
||||
|
||||
"construct (2)" in {
|
||||
Line3D(1,2,3.5f, Vector3(1,0,0))
|
||||
Line(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))
|
||||
Line(1,2,3.5f, 2,2,3.5f) mustEqual Line(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]
|
||||
Line(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
|
||||
val obj = Line(1,2,3.5f, Vector3(1,0,0))
|
||||
obj.center mustEqual Point(1,2,3.5f)
|
||||
}
|
||||
}
|
||||
|
||||
"Segment3D" should {
|
||||
"Segment" should {
|
||||
"construct (1)" in {
|
||||
Segment3D(Point3D(1,2,3), Point3D(3,2,3))
|
||||
Segment(Point(1,2,3), Point(3,2,3))
|
||||
ok
|
||||
}
|
||||
|
||||
"construct (2)" in {
|
||||
Segment3D(1,2,3, 3,2,3) mustEqual Segment3D(Point3D(1,2,3), Point3D(3,2,3))
|
||||
Segment(1,2,3, 3,2,3) mustEqual Segment(Point(1,2,3), Point(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))
|
||||
Segment(Point(1,2,3), Vector3(1,0,0)) mustEqual Segment(Point(1,2,3), Point(2,2,3))
|
||||
}
|
||||
|
||||
"construct (4)" in {
|
||||
Segment3D(1,2,3, Vector3(1,0,0)) mustEqual Segment3D(Point3D(1,2,3), Point3D(2,2,3))
|
||||
Segment(1,2,3, Vector3(1,0,0)) mustEqual Segment(Point(1,2,3), Point(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))
|
||||
val obj1 = Segment(1,2,3, Vector3(5,1,1))
|
||||
val obj2 = Segment(Point(1,2,3), Point(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
|
||||
Segment(Point(1,2,3), Point(3,4,5)).center mustEqual Point(2,3,4)
|
||||
}
|
||||
}
|
||||
|
||||
"Sphere3D" should {
|
||||
"construct (1)" in {
|
||||
Sphere(Point3D(1,2,3), 3)
|
||||
Sphere(Point(1,2,3), 3)
|
||||
ok
|
||||
}
|
||||
|
||||
"construct (2)" in {
|
||||
Sphere(3) mustEqual Sphere(Point3D(0,0,0), 3)
|
||||
Sphere(3) mustEqual Sphere(Point(0,0,0), 3)
|
||||
ok
|
||||
}
|
||||
|
||||
"construct (3)" in {
|
||||
Sphere(1,2,3, 3) mustEqual Sphere(Point3D(1,2,3), 3)
|
||||
Sphere(1,2,3, 3) mustEqual Sphere(Point(1,2,3), 3)
|
||||
}
|
||||
|
||||
"construct (4)" in {
|
||||
Sphere(Vector3(1,2,3), 3) mustEqual Sphere(Point3D(1,2,3), 3)
|
||||
Sphere(Vector3(1,2,3), 3) mustEqual Sphere(Point(1,2,3), 3)
|
||||
}
|
||||
|
||||
"the center point is self-evident" in {
|
||||
Sphere(Point3D(1,2,3), 3).center mustEqual Point3D(1,2,3)
|
||||
Sphere(Point(1,2,3), 3).center mustEqual Point(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
|
||||
obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point( 4, 2,3) //east
|
||||
obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point( 1, 5,3) //north
|
||||
obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point( 1, 2,6) //up
|
||||
obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point(-2, 2,3) //west
|
||||
obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point( 1,-1,3) //south
|
||||
obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point( 1, 2,0) //down
|
||||
}
|
||||
}
|
||||
|
||||
"Cylinder (normal)" should {
|
||||
"construct (1)" in {
|
||||
Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3)
|
||||
Cylinder(Point(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)
|
||||
Cylinder(Point(1,2,3), 2, 3) mustEqual Cylinder(Point(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)
|
||||
Cylinder(Vector3(1,2,3), 2, 3) mustEqual Cylinder(Point(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)
|
||||
Cylinder(Vector3(1,2,3), Vector3(0,0,1), 2, 3) mustEqual Cylinder(Point(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)
|
||||
Cylinder(Point(1,2,3), 2, 3).center mustEqual Point(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
|
||||
val obj1 = Cylinder(Point(1,2,3), 2, 3)
|
||||
obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point( 3, 2, 4.5f) //east
|
||||
obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point( 1, 4, 4.5f) //north
|
||||
obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point( 1, 2, 6f) //up
|
||||
obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point(-1, 2, 4.5f) //west
|
||||
obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point( 1, 0, 4.5f) //south
|
||||
obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point( 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)
|
||||
Cylinder(Point(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]
|
||||
Cylinder(Point(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)
|
||||
Cylinder(Point(1,2,3), Vector3(1,0,0), 2, 3).center mustEqual Point(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 obj1 = Cylinder(Point(1,2,3), Vector3(1,0,0), 2, 3)
|
||||
obj1.pointOnOutside(Vector3( 1, 0, 0)) mustEqual Point(4, 2, 3) //east
|
||||
obj1.pointOnOutside(Vector3( 0, 1, 0)) mustEqual Point(2.5f, 4, 3) //north
|
||||
obj1.pointOnOutside(Vector3( 0, 0, 1)) mustEqual Point(2.5f, 2, 5) //up
|
||||
obj1.pointOnOutside(Vector3(-1, 0, 0)) mustEqual Point(1, 2, 3) //west
|
||||
obj1.pointOnOutside(Vector3( 0,-1, 0)) mustEqual Point(2.5f, 0, 3) //south
|
||||
obj1.pointOnOutside(Vector3( 0, 0,-1)) mustEqual Point(2.5f, 2, 1) //down
|
||||
|
||||
val obj2 = Cylinder(Point3D(1,2,3), Vector3(0,0,1), 2, 3)
|
||||
val obj2 = Cylinder(Point(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))
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import net.psforever.objects.definition.ObjectDefinition
|
|||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.environment._
|
||||
import net.psforever.objects.vital.{Vitality, VitalityDefinition}
|
||||
import net.psforever.objects.zones.{Zone, ZoneMap}
|
||||
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneMap}
|
||||
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
|
@ -21,6 +21,9 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
|
|||
}
|
||||
new Zone("test-zone", testMap, zoneNumber = 0)
|
||||
}
|
||||
testZone.blockMap.addTo(pool1)
|
||||
testZone.blockMap.addTo(pool2)
|
||||
testZone.blockMap.addTo(pool3)
|
||||
|
||||
"InteractsWithZoneEnvironment" should {
|
||||
"not interact with any environment when it does not encroach any environment" in {
|
||||
|
|
@ -30,7 +33,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
|
|||
obj.Actor = testProbe.ref
|
||||
|
||||
assert(obj.Position == Vector3.Zero)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
testProbe.expectNoMessage(max = 500 milliseconds)
|
||||
}
|
||||
|
||||
|
|
@ -41,15 +44,15 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
|
|||
obj.Actor = testProbe.ref
|
||||
|
||||
obj.Position = Vector3(1,1,-2)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
val msg = testProbe.receiveOne(max = 250 milliseconds)
|
||||
assert(
|
||||
msg match {
|
||||
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
testProbe.expectNoMessage(max = 500 milliseconds)
|
||||
}
|
||||
|
||||
|
|
@ -60,17 +63,17 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
|
|||
obj.Actor = testProbe.ref
|
||||
|
||||
obj.Position = Vector3(1,1,-2)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
val msg1 = testProbe.receiveOne(max = 250 milliseconds)
|
||||
assert(
|
||||
msg1 match {
|
||||
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
|
||||
obj.Position = Vector3(1,1,1)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
val msg2 = testProbe.receiveOne(max = 250 milliseconds)
|
||||
assert(
|
||||
msg2 match {
|
||||
|
|
@ -78,7 +81,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
|
|||
case _ => false
|
||||
}
|
||||
)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
testProbe.expectNoMessage(max = 500 milliseconds)
|
||||
}
|
||||
|
||||
|
|
@ -89,21 +92,21 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
|
|||
obj.Actor = testProbe.ref
|
||||
|
||||
obj.Position = Vector3(7,7,-2)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
val msg1 = testProbe.receiveOne(max = 250 milliseconds)
|
||||
assert(
|
||||
msg1 match {
|
||||
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
|
||||
obj.Position = Vector3(12,7,-2)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
val msg2 = testProbe.receiveOne(max = 250 milliseconds)
|
||||
assert(
|
||||
msg2 match {
|
||||
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool2)
|
||||
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool2)
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
|
|
@ -117,17 +120,17 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
|
|||
obj.Actor = testProbe.ref
|
||||
|
||||
obj.Position = Vector3(7,7,-2)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
val msg1 = testProbe.receiveOne(max = 250 milliseconds)
|
||||
assert(
|
||||
msg1 match {
|
||||
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
|
||||
obj.Position = Vector3(7,12,-2)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
val msgs = testProbe.receiveN(2, max = 250 milliseconds)
|
||||
assert(
|
||||
msgs.head match {
|
||||
|
|
@ -137,7 +140,7 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
|
|||
)
|
||||
assert(
|
||||
msgs(1) match {
|
||||
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool3)
|
||||
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool3)
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
|
|
@ -152,24 +155,22 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
|
|||
obj.Actor = testProbe.ref
|
||||
|
||||
obj.Position = Vector3(1,1,-2)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
val msg1 = testProbe.receiveOne(max = 250 milliseconds)
|
||||
assert(
|
||||
msg1 match {
|
||||
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
|
||||
obj.allowZoneEnvironmentInteractions = false
|
||||
obj.allowInteraction = false
|
||||
val msg2 = testProbe.receiveOne(max = 250 milliseconds)
|
||||
assert(
|
||||
msg2 match {
|
||||
case EscapeFromEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
obj.zoneInteraction()
|
||||
msg2 match {
|
||||
case EscapeFromEnvironment(o, b, _) => assert((o eq obj) && (b eq pool1))
|
||||
case _ => assert( false)
|
||||
}
|
||||
obj.zoneInteractions()
|
||||
testProbe.expectNoMessage(max = 500 milliseconds)
|
||||
}
|
||||
|
||||
|
|
@ -179,16 +180,16 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
|
|||
obj.Zone = testZone
|
||||
obj.Actor = testProbe.ref
|
||||
|
||||
obj.allowZoneEnvironmentInteractions = false
|
||||
obj.allowInteraction = false
|
||||
obj.Position = Vector3(1,1,-2)
|
||||
obj.zoneInteraction()
|
||||
obj.zoneInteractions()
|
||||
testProbe.expectNoMessage(max = 500 milliseconds)
|
||||
|
||||
obj.allowZoneEnvironmentInteractions = true
|
||||
obj.allowInteraction = true
|
||||
val msg1 = testProbe.receiveOne(max = 250 milliseconds)
|
||||
assert(
|
||||
msg1 match {
|
||||
case InteractWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case InteractingWithEnvironment(o, b, _) => (o eq obj) && (b eq pool1)
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
|
|
@ -196,10 +197,11 @@ class InteractsWithZoneEnvironmentTest extends ActorTest {
|
|||
}
|
||||
|
||||
object InteractsWithZoneEnvironmentTest {
|
||||
def testObject(): PlanetSideServerObject with InteractsWithZoneEnvironment = {
|
||||
def testObject(): PlanetSideServerObject with InteractsWithZone = {
|
||||
new PlanetSideServerObject
|
||||
with InteractsWithZoneEnvironment
|
||||
with InteractsWithZone
|
||||
with Vitality {
|
||||
interaction(new InteractWithEnvironment())
|
||||
def Faction: PlanetSideEmpire.Value = PlanetSideEmpire.VS
|
||||
def DamageModel = null
|
||||
def Definition: ObjectDefinition with VitalityDefinition = new ObjectDefinition(objectId = 0) with VitalityDefinition {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@
|
|||
package objects
|
||||
|
||||
import akka.actor.{Actor, ActorRef, Props}
|
||||
import akka.testkit.TestProbe
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import base.ActorTest
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
import net.psforever.objects.definition.ObjectDefinition
|
||||
import net.psforever.objects.serverobject.mount._
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.zones.{Zone, ZoneMap}
|
||||
import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire, PlanetSideGUID}
|
||||
|
||||
import scala.concurrent.duration.Duration
|
||||
|
|
@ -27,6 +31,10 @@ class MountableControl2Test extends ActorTest {
|
|||
"let a player mount" in {
|
||||
val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
|
||||
val obj = new MountableTest.MountableTestObject
|
||||
obj.Zone = new Zone("test", new ZoneMap("test"), 0) {
|
||||
override def SetupNumberPools() = {}
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
obj.Actor = system.actorOf(Props(classOf[MountableTest.MountableTestControl], obj), "mountable")
|
||||
val msg = Mountable.TryMount(player, 0)
|
||||
|
||||
|
|
@ -69,9 +77,8 @@ class MountableControl3Test extends ActorTest {
|
|||
object MountableTest {
|
||||
class MountableTestObject extends PlanetSideServerObject with Mountable {
|
||||
seats += 0 -> new Seat(new SeatDefinition())
|
||||
GUID = PlanetSideGUID(1)
|
||||
//eh whatever
|
||||
def Faction = PlanetSideEmpire.TR
|
||||
GUID = PlanetSideGUID(1) //eh whatever
|
||||
def Faction = PlanetSideEmpire.TR
|
||||
def Definition = new ObjectDefinition(1) with MountableDefinition {
|
||||
MountPoints += 0 -> MountInfo(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import akka.actor.{ActorRef, Props}
|
|||
import akka.routing.RandomPool
|
||||
import akka.testkit.TestProbe
|
||||
import base.FreedContextActorTest
|
||||
import net.psforever.actors.zone.BuildingActor
|
||||
import net.psforever.actors.zone.{BuildingActor, ZoneActor}
|
||||
import net.psforever.objects.guid.actor.UniqueNumberSystem
|
||||
import net.psforever.objects.{GlobalDefinitions, Vehicle}
|
||||
import net.psforever.objects.guid.{NumberPoolHub, TaskResolver}
|
||||
|
|
@ -56,6 +56,9 @@ class OrbitalShuttlePadControltest extends FreedContextActorTest {
|
|||
override def Vehicles = { vehicles.toList }
|
||||
override def Buildings = { buildingMap.toMap }
|
||||
override def tasks = { resolver }
|
||||
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
val building = new Building(
|
||||
name = "test-orbital-building-tr",
|
||||
|
|
|
|||
|
|
@ -786,6 +786,8 @@ class PlayerControlInteractWithWaterTest extends ActorTest {
|
|||
override def LivePlayers = List(player1)
|
||||
override def AvatarEvents = avatarProbe.ref
|
||||
}
|
||||
zone.blockMap.addTo(player1)
|
||||
zone.blockMap.addTo(pool)
|
||||
|
||||
player1.Zone = zone
|
||||
player1.Spawn()
|
||||
|
|
@ -799,7 +801,7 @@ class PlayerControlInteractWithWaterTest extends ActorTest {
|
|||
"cause drowning when player steps too deep in water" in {
|
||||
assert(player1.Health == 100)
|
||||
player1.Position = Vector3(5,5,-3) //right in the pool
|
||||
player1.zoneInteraction() //trigger
|
||||
player1.zoneInteractions() //trigger
|
||||
|
||||
val msg_drown = avatarProbe.receiveOne(250 milliseconds)
|
||||
assert(
|
||||
|
|
@ -838,6 +840,8 @@ class PlayerControlStopInteractWithWaterTest extends ActorTest {
|
|||
override def LivePlayers = List(player1)
|
||||
override def AvatarEvents = avatarProbe.ref
|
||||
}
|
||||
zone.blockMap.addTo(player1)
|
||||
zone.blockMap.addTo(pool)
|
||||
|
||||
player1.Zone = zone
|
||||
player1.Spawn()
|
||||
|
|
@ -851,7 +855,7 @@ class PlayerControlStopInteractWithWaterTest extends ActorTest {
|
|||
"stop drowning if player steps out of deep water" in {
|
||||
assert(player1.Health == 100)
|
||||
player1.Position = Vector3(5,5,-3) //right in the pool
|
||||
player1.zoneInteraction() //trigger
|
||||
player1.zoneInteractions() //trigger
|
||||
|
||||
val msg_drown = avatarProbe.receiveOne(250 milliseconds)
|
||||
assert(
|
||||
|
|
@ -865,7 +869,7 @@ class PlayerControlStopInteractWithWaterTest extends ActorTest {
|
|||
)
|
||||
//player would normally die in 60s
|
||||
player1.Position = Vector3.Zero //pool's closed
|
||||
player1.zoneInteraction() //trigger
|
||||
player1.zoneInteractions() //trigger
|
||||
val msg_recover = avatarProbe.receiveOne(250 milliseconds)
|
||||
assert(
|
||||
msg_recover match {
|
||||
|
|
@ -902,6 +906,8 @@ class PlayerControlInteractWithLavaTest extends ActorTest {
|
|||
override def AvatarEvents = avatarProbe.ref
|
||||
override def Activity = TestProbe().ref
|
||||
}
|
||||
zone.blockMap.addTo(player1)
|
||||
zone.blockMap.addTo(pool)
|
||||
|
||||
player1.Zone = zone
|
||||
player1.Spawn()
|
||||
|
|
@ -915,7 +921,7 @@ class PlayerControlInteractWithLavaTest extends ActorTest {
|
|||
"take continuous damage if player steps into lava" in {
|
||||
assert(player1.Health == 100) //alive
|
||||
player1.Position = Vector3(5,5,-3) //right in the pool
|
||||
player1.zoneInteraction() //trigger
|
||||
player1.zoneInteractions() //trigger
|
||||
|
||||
val msg_burn = avatarProbe.receiveN(3, 1 seconds)
|
||||
assert(
|
||||
|
|
@ -962,6 +968,8 @@ class PlayerControlInteractWithDeathTest extends ActorTest {
|
|||
override def AvatarEvents = avatarProbe.ref
|
||||
override def Activity = TestProbe().ref
|
||||
}
|
||||
zone.blockMap.addTo(player1)
|
||||
zone.blockMap.addTo(pool)
|
||||
|
||||
player1.Zone = zone
|
||||
player1.Spawn()
|
||||
|
|
@ -975,7 +983,7 @@ class PlayerControlInteractWithDeathTest extends ActorTest {
|
|||
"take continuous damage if player steps into a pool of death" in {
|
||||
assert(player1.Health == 100) //alive
|
||||
player1.Position = Vector3(5,5,-3) //right in the pool
|
||||
player1.zoneInteraction() //trigger
|
||||
player1.zoneInteractions() //trigger
|
||||
|
||||
probe.receiveOne(250 milliseconds) //wait until oplayer1's implants deinitialize
|
||||
assert(player1.Health == 0) //ded
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
package objects
|
||||
|
||||
import akka.actor.{ActorRef, Props}
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import akka.testkit.TestProbe
|
||||
import base.ActorTest
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.ce.{DeployableCategory, DeployedItem, TelepadLike}
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
|
|
@ -33,6 +35,8 @@ class TelepadDeployableNoRouterTest extends ActorTest {
|
|||
override def AvatarEvents: ActorRef = eventsProbe.ref
|
||||
override def LocalEvents: ActorRef = eventsProbe.ref
|
||||
override def Deployables: ActorRef = deployables
|
||||
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
guid.register(telepad, number = 1)
|
||||
|
||||
|
|
@ -96,6 +100,8 @@ class TelepadDeployableNoActivationTest extends ActorTest {
|
|||
override def LocalEvents: ActorRef = eventsProbe.ref
|
||||
override def Deployables: ActorRef = deployables
|
||||
override def Vehicles: List[Vehicle] = List(router)
|
||||
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
guid.register(telepad, number = 1)
|
||||
guid.register(router, number = 2)
|
||||
|
|
@ -165,6 +171,8 @@ class TelepadDeployableAttemptTest extends ActorTest {
|
|||
override def LocalEvents: ActorRef = eventsProbe.ref
|
||||
override def Deployables: ActorRef = deployables
|
||||
override def Vehicles: List[Vehicle] = List(router)
|
||||
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
guid.register(telepad, number = 1)
|
||||
guid.register(router, number = 2)
|
||||
|
|
@ -226,6 +234,8 @@ class TelepadDeployableResponseFromRouterTest extends ActorTest {
|
|||
override def VehicleEvents: ActorRef = eventsProbe.ref
|
||||
override def Deployables: ActorRef = deployables
|
||||
override def Vehicles: List[Vehicle] = List(router)
|
||||
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
guid.register(telepad, number = 1)
|
||||
guid.register(router, number = 2)
|
||||
|
|
|
|||
|
|
@ -329,6 +329,7 @@ class VehicleControlMountingBlockedExosuitTest extends ActorTest {
|
|||
override def LocalEvents: ActorRef = catchall
|
||||
override def VehicleEvents: ActorRef = catchall
|
||||
override def Activity: ActorRef = catchall
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
val vehicle = Vehicle(GlobalDefinitions.apc_tr)
|
||||
vehicle.Faction = PlanetSideEmpire.TR
|
||||
|
|
@ -392,6 +393,10 @@ class VehicleControlMountingBlockedSeatPermissionTest extends ActorTest {
|
|||
vehicle.Faction = PlanetSideEmpire.TR
|
||||
vehicle.GUID = PlanetSideGUID(10)
|
||||
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
|
||||
vehicle.Zone = new Zone("test", new ZoneMap("test-map"), 0) {
|
||||
override def SetupNumberPools(): Unit = {}
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
|
||||
val player1 = Player(VehicleTest.avatar1)
|
||||
player1.GUID = PlanetSideGUID(1)
|
||||
|
|
@ -418,6 +423,10 @@ class VehicleControlMountingDriverSeatTest extends ActorTest {
|
|||
vehicle.Faction = PlanetSideEmpire.TR
|
||||
vehicle.GUID = PlanetSideGUID(10)
|
||||
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
|
||||
vehicle.Zone = new Zone("test", new ZoneMap("test-map"), 0) {
|
||||
override def SetupNumberPools(): Unit = {}
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
val player1 = Player(VehicleTest.avatar1)
|
||||
player1.GUID = PlanetSideGUID(1)
|
||||
|
||||
|
|
@ -439,6 +448,11 @@ class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest {
|
|||
vehicle.Faction = PlanetSideEmpire.TR
|
||||
vehicle.GUID = PlanetSideGUID(10)
|
||||
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
|
||||
vehicle.Zone = new Zone("test", new ZoneMap("test-map"), 0) {
|
||||
override def SetupNumberPools(): Unit = {}
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
|
||||
val player1 = Player(VehicleTest.avatar1)
|
||||
player1.GUID = PlanetSideGUID(1)
|
||||
val player2 = Player(VehicleTest.avatar1)
|
||||
|
|
@ -470,6 +484,11 @@ class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest {
|
|||
vehicle.Faction = PlanetSideEmpire.TR
|
||||
vehicle.GUID = PlanetSideGUID(10)
|
||||
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
|
||||
vehicle.Zone = new Zone("test", new ZoneMap("test-map"), 0) {
|
||||
override def SetupNumberPools(): Unit = {}
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
|
||||
val player1 = Player(VehicleTest.avatar1)
|
||||
player1.GUID = PlanetSideGUID(1)
|
||||
val player2 = Player(VehicleTest.avatar1)
|
||||
|
|
@ -640,6 +659,8 @@ class VehicleControlInteractWithWaterPartialTest extends ActorTest {
|
|||
override def LivePlayers = List(player1)
|
||||
override def Vehicles = List(vehicle)
|
||||
}
|
||||
zone.blockMap.addTo(vehicle)
|
||||
zone.blockMap.addTo(pool)
|
||||
|
||||
guid.register(player1, 1)
|
||||
guid.register(vehicle, 2)
|
||||
|
|
@ -655,12 +676,12 @@ class VehicleControlInteractWithWaterPartialTest extends ActorTest {
|
|||
"VehicleControl" should {
|
||||
"causes disability when the vehicle drives too deep in water (check driver messaging)" in {
|
||||
vehicle.Position = Vector3(5,5,-3) //right in the pool
|
||||
vehicle.zoneInteraction() //trigger
|
||||
vehicle.zoneInteractions() //trigger
|
||||
|
||||
val msg_drown = playerProbe.receiveOne(250 milliseconds)
|
||||
assert(
|
||||
msg_drown match {
|
||||
case InteractWithEnvironment(
|
||||
case InteractingWithEnvironment(
|
||||
p1,
|
||||
p2,
|
||||
Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f))
|
||||
|
|
@ -693,7 +714,11 @@ class VehicleControlInteractWithWaterTest extends ActorTest {
|
|||
override def Vehicles = List(vehicle)
|
||||
override def AvatarEvents = avatarProbe.ref
|
||||
override def VehicleEvents = vehicleProbe.ref
|
||||
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
zone.blockMap.addTo(vehicle)
|
||||
zone.blockMap.addTo(pool)
|
||||
|
||||
guid.register(player1, 1)
|
||||
guid.register(vehicle, 2)
|
||||
|
|
@ -711,7 +736,7 @@ class VehicleControlInteractWithWaterTest extends ActorTest {
|
|||
"VehicleControl" should {
|
||||
"causes disability when the vehicle drives too deep in water" in {
|
||||
vehicle.Position = Vector3(5,5,-3) //right in the pool
|
||||
vehicle.zoneInteraction() //trigger
|
||||
vehicle.zoneInteractions() //trigger
|
||||
|
||||
val msg_drown = avatarProbe.receiveOne(250 milliseconds)
|
||||
assert(
|
||||
|
|
@ -728,16 +753,14 @@ class VehicleControlInteractWithWaterTest extends ActorTest {
|
|||
)
|
||||
//player will die in 60s
|
||||
//vehicle will disable in 5s; driver will be kicked
|
||||
val msg_kick = vehicleProbe.receiveOne(6 seconds)
|
||||
assert(
|
||||
msg_kick match {
|
||||
case VehicleServiceMessage(
|
||||
"test-zone",
|
||||
VehicleAction.KickPassenger(PlanetSideGUID(1), 4, _, PlanetSideGUID(2))
|
||||
) => true
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
val msg_kick = vehicleProbe.receiveOne(10 seconds)
|
||||
msg_kick match {
|
||||
case VehicleServiceMessage(
|
||||
"test-zone",
|
||||
VehicleAction.KickPassenger(PlanetSideGUID(1), 4, _, PlanetSideGUID(2))
|
||||
) => assert(true)
|
||||
case _ => assert(false)
|
||||
}
|
||||
//player will die, but detailing players death messages is not necessary for this test
|
||||
}
|
||||
}
|
||||
|
|
@ -762,6 +785,8 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest {
|
|||
override def LivePlayers = List(player1)
|
||||
override def Vehicles = List(vehicle)
|
||||
}
|
||||
zone.blockMap.addTo(vehicle)
|
||||
zone.blockMap.addTo(pool)
|
||||
|
||||
guid.register(player1, 1)
|
||||
guid.register(vehicle, 2)
|
||||
|
|
@ -777,11 +802,11 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest {
|
|||
"VehicleControl" should {
|
||||
"stop becoming disabled if the vehicle drives out of the water" in {
|
||||
vehicle.Position = Vector3(5,5,-3) //right in the pool
|
||||
vehicle.zoneInteraction() //trigger
|
||||
vehicle.zoneInteractions() //trigger
|
||||
val msg_drown = playerProbe.receiveOne(250 milliseconds)
|
||||
assert(
|
||||
msg_drown match {
|
||||
case InteractWithEnvironment(
|
||||
case InteractingWithEnvironment(
|
||||
p1,
|
||||
p2,
|
||||
Some(OxygenStateTarget(PlanetSideGUID(2), OxygenState.Suffocation, 100f))
|
||||
|
|
@ -791,7 +816,7 @@ class VehicleControlStopInteractWithWaterTest extends ActorTest {
|
|||
)
|
||||
|
||||
vehicle.Position = Vector3.Zero //that's enough of that
|
||||
vehicle.zoneInteraction()
|
||||
vehicle.zoneInteractions()
|
||||
val msg_recover = playerProbe.receiveOne(250 milliseconds)
|
||||
assert(
|
||||
msg_recover match {
|
||||
|
|
@ -830,6 +855,8 @@ class VehicleControlInteractWithLavaTest extends ActorTest {
|
|||
override def VehicleEvents = vehicleProbe.ref
|
||||
override def Activity = TestProbe().ref
|
||||
}
|
||||
zone.blockMap.addTo(vehicle)
|
||||
zone.blockMap.addTo(pool)
|
||||
|
||||
guid.register(player1, 1)
|
||||
guid.register(vehicle, 2)
|
||||
|
|
@ -849,7 +876,7 @@ class VehicleControlInteractWithLavaTest extends ActorTest {
|
|||
assert(vehicle.Health > 0) //alive
|
||||
assert(player1.Health == 100) //alive
|
||||
vehicle.Position = Vector3(5,5,-3) //right in the pool
|
||||
vehicle.zoneInteraction() //trigger
|
||||
vehicle.zoneInteractions() //trigger
|
||||
|
||||
val msg_burn = vehicleProbe.receiveN(3,1 seconds)
|
||||
msg_burn.foreach { msg =>
|
||||
|
|
@ -888,6 +915,8 @@ class VehicleControlInteractWithDeathTest extends ActorTest {
|
|||
override def AvatarEvents = TestProbe().ref
|
||||
override def VehicleEvents = TestProbe().ref
|
||||
}
|
||||
zone.blockMap.addTo(vehicle)
|
||||
zone.blockMap.addTo(pool)
|
||||
|
||||
guid.register(player1, 1)
|
||||
guid.register(vehicle, 2)
|
||||
|
|
@ -907,7 +936,7 @@ class VehicleControlInteractWithDeathTest extends ActorTest {
|
|||
assert(vehicle.Health > 0) //alive
|
||||
assert(player1.Health == 100) //alive
|
||||
vehicle.Position = Vector3(5,5,-3) //right in the pool
|
||||
vehicle.zoneInteraction() //trigger
|
||||
vehicle.zoneInteractions() //trigger
|
||||
|
||||
probe.receiveOne(2 seconds) //wait until player1's implants deinitialize
|
||||
assert(vehicle.Health == 0) //ded
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
package objects.terminal
|
||||
|
||||
import akka.actor.{ActorSystem, Props}
|
||||
import akka.testkit.TestProbe
|
||||
import base.ActorTest
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
import net.psforever.objects.{Default, GlobalDefinitions, Player}
|
||||
import net.psforever.objects.guid.NumberPoolHub
|
||||
|
|
@ -156,11 +158,14 @@ class ImplantTerminalMechControl5Test extends ActorTest {
|
|||
|
||||
object ImplantTerminalMechTest {
|
||||
def SetUpAgents(faction: PlanetSideEmpire.Value)(implicit system: ActorSystem): (Player, ImplantTerminalMech) = {
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
|
||||
val guid = new NumberPoolHub(new MaxNumberSource(10))
|
||||
val map = new ZoneMap("test")
|
||||
val zone = new Zone("test", map, 0) {
|
||||
override def SetupNumberPools() = {}
|
||||
GUID(guid)
|
||||
this.actor = new TestProbe(system).ref.toTyped[ZoneActor.Command]
|
||||
}
|
||||
val building = new Building(
|
||||
"Building",
|
||||
|
|
|
|||
Loading…
Reference in a new issue