mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-07-08 13:04:41 +00:00
Capitol Force Dome (#1350)
* adding entity to represent force dome, and wiring force dome to capitol facility; touching the force dome while it is active causes death on both server and client * force dome is now an amenity owned by its encompassing capitol facility and has its own control agency that is aware of the faction affinity, hack state, etc. conditions that would cause it to close over or open up * force dome messages owner about change in state, triggering the NTU silo to give away repairs for free; activating the force dome kills or destroys all enemies within its radius * the force dome exhibits a perimeter in which enemies will be destroyed when it energizes; the facility generator will become undestroyable when the force dome is energized * force domes should be on the zone blockmap; correct issue with force dome death; interaction that sets players found under the force dome to be invulnerable works * wrote variable and method documentation; prepared interaction handlers for force domes for mountable (vehicle) targets * force dome provides damage protection to certain amenities, e.g., the generator, the turrets, and any implant machines (cryo); force dome will also suspend hacking attempts under it's envelope, but counter-hacking (resecure) should still be possible; operated turret deployables gain protection while manned; turrets no longer share knowledge of each other's upgrade cycles * csr-level commands for force dome manipulation * extending the force dome protection over a variety of entities in a different manner, with a focus on how to perform state reset (dismounting and dome protect end); completely refactored and reworked the self-reported zone interaction timer for vehicles; separated passenger seat mounting from gunner seat mounting * adjusted self-reporting zone interaction logic; allowed for status of mounting into turret entities; made routine mounting behavior callbacks * streamline pass on the vehicle operation and aaccountability * expanded scope of 'csr override message for Capitol force dome state; spelling and guard condition changes to dismount blocking cases * corrected death by force dome contact, for all players; corrected reset of force dome protection field condition * corrected input data to force door wall inclusion test; adjusted force dome ceiling inclusion test(s); force dome generator locations now retained; invulnerability can not be granted to players or vehicles if they've taken damage recently
This commit is contained in:
parent
1b31b3bad7
commit
ceb6e58119
64 changed files with 2157 additions and 719 deletions
|
|
@ -8,6 +8,7 @@ import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, Sess
|
|||
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Session, TurretDeployable}
|
||||
import net.psforever.objects.ce.{Deployable, DeployableCategory}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.dome.ForceDomeControl
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.serverobject.hackable.Hackable
|
||||
import net.psforever.objects.serverobject.structures.Building
|
||||
|
|
@ -227,6 +228,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
|
|||
case "sayspectator" => customCommandSpeakAsSpectator(params, message)
|
||||
case "setempire" => customCommandSetEmpire(params)
|
||||
case "weaponlock" => customCommandZoneWeaponUnlock(session, params)
|
||||
case "forcedome" => customForceDomeCommand(session, params)
|
||||
case _ =>
|
||||
// command was not handled
|
||||
sendResponse(
|
||||
|
|
@ -509,6 +511,41 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
|
|||
true
|
||||
}
|
||||
|
||||
private def customForceDomeCommand(session: Session, contents: Seq[String]): Boolean = {
|
||||
//locate force dome
|
||||
var postUsageMessage: Boolean = false
|
||||
val locatedForceDomesInZone = session.zone.Buildings.values.flatMap(_.ForceDome)
|
||||
if (locatedForceDomesInZone.nonEmpty) {
|
||||
contents
|
||||
.headOption
|
||||
.map(_.toLowerCase())
|
||||
.collect {
|
||||
case "on" | "o" | "" =>
|
||||
locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.CustomExpand)
|
||||
case "off" | "of" =>
|
||||
locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.CustomCollapse)
|
||||
case "protect" =>
|
||||
locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.ApplyProtection)
|
||||
case "normal" =>
|
||||
locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.NormalBehavior)
|
||||
case "purge" =>
|
||||
locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.Purge)
|
||||
case "help" | "usage" =>
|
||||
postUsageMessage = true
|
||||
case token =>
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_227, s"unknown command - $token"))
|
||||
postUsageMessage = true
|
||||
}
|
||||
if (postUsageMessage) {
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_227, "!forcedome [o[n]|of[f]|protect|normal|purge]"))
|
||||
}
|
||||
} else {
|
||||
//no force domes in zone
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_227, "no capitol force dome(s) detected in zone"))
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
private def customCommandOnOffStateOrNone(stateOpt: Option[String]): Option[Boolean] = {
|
||||
stateOpt match {
|
||||
case None =>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import net.psforever.objects.serverobject.ServerObject
|
|||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
import net.psforever.packet.game.{ChatMsg, ObjectCreateDetailedMessage, PlanetsideAttributeMessage}
|
||||
import net.psforever.packet.game.objectcreate.RibbonBars
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
|
|
@ -110,69 +109,21 @@ class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic {
|
|||
|
||||
private def keepAlivePersistanceCSR(): Unit = {
|
||||
val player = data.player
|
||||
data.keepAlivePersistence()
|
||||
topOffHealthOfPlayer(player)
|
||||
player.allowInteraction = false
|
||||
topOffHealthOfPlayer(player)
|
||||
data.continent.GUID(data.player.VehicleSeated)
|
||||
.collect {
|
||||
case obj: PlanetSideGameObject with Vitality with BlockMapEntity =>
|
||||
topOffHealth(obj)
|
||||
data.updateBlockMap(obj, obj.Position)
|
||||
obj
|
||||
}
|
||||
.getOrElse {
|
||||
data.updateBlockMap(player, player.Position)
|
||||
}
|
||||
}
|
||||
|
||||
private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = {
|
||||
obj match {
|
||||
case p: Player => topOffHealthOfPlayer(p)
|
||||
case v: Vehicle => topOffHealthOfVehicle(v)
|
||||
case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(o)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
private def topOffHealthOfPlayer(player: Player): Unit = {
|
||||
//driver below half health, full heal
|
||||
val maxHealthOfPlayer = player.MaxHealth.toLong
|
||||
if (player.Health < maxHealthOfPlayer * 0.5f) {
|
||||
player.Health = maxHealthOfPlayer.toInt
|
||||
player.LogActivity(player.ClearHistory().head)
|
||||
data.sendResponse(PlanetsideAttributeMessage(player.GUID, 0, maxHealthOfPlayer))
|
||||
data.continent.AvatarEvents ! AvatarServiceMessage(data.zoning.zoneChannel, AvatarAction.PlanetsideAttribute(player.GUID, 0, maxHealthOfPlayer))
|
||||
}
|
||||
}
|
||||
|
||||
private def topOffHealthOfVehicle(vehicle: Vehicle): Unit = {
|
||||
topOffHealthOfGeneric(vehicle)
|
||||
//vehicle shields below half, full shields
|
||||
val maxShieldsOfVehicle = vehicle.MaxShields.toLong
|
||||
val shieldsUi = vehicle.Definition.shieldUiAttribute
|
||||
if (vehicle.Shields < maxShieldsOfVehicle) {
|
||||
val guid = vehicle.GUID
|
||||
vehicle.Shields = maxShieldsOfVehicle.toInt
|
||||
data.sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle))
|
||||
data.continent.VehicleEvents ! VehicleServiceMessage(
|
||||
data.continent.id,
|
||||
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def topOffHealthOfGeneric(obj: PlanetSideGameObject with Vitality): Unit = {
|
||||
//below half health, full heal
|
||||
val guid = obj.GUID
|
||||
val maxHealthOf = obj.MaxHealth.toLong
|
||||
if (obj.Health < maxHealthOf) {
|
||||
obj.Health = maxHealthOf.toInt
|
||||
data.sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf))
|
||||
data.continent.VehicleEvents ! VehicleServiceMessage(
|
||||
data.continent.id,
|
||||
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf)
|
||||
)
|
||||
CustomerServiceRepresentativeMode.topOffHealthOfPlayer(data, player)
|
||||
data.zoning.spawn.interimUngunnedVehicle = None
|
||||
data.keepAlivePersistence()
|
||||
if (player.HasGUID) {
|
||||
data.zoning.spawn.tryQueuedActivity()
|
||||
data.turnCounterFunc(player.GUID)
|
||||
data.continent
|
||||
.GUID(player.VehicleSeated)
|
||||
.collect { case obj: PlanetSideGameObject with Vitality =>
|
||||
CustomerServiceRepresentativeMode.topOffHealth(data, obj)
|
||||
}
|
||||
data.squad.updateSquad()
|
||||
} else {
|
||||
data.turnCounterFunc(PlanetSideGUID(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -201,4 +152,66 @@ case object CustomerServiceRepresentativeMode extends PlayerMode {
|
|||
None
|
||||
))
|
||||
}
|
||||
|
||||
def topOffHealth(data: SessionData, obj: PlanetSideGameObject with Vitality): Unit = {
|
||||
obj match {
|
||||
case p: Player => topOffHealthOfPlayer(data, p)
|
||||
case v: Vehicle => topOffHealthOfVehicle(data, v)
|
||||
case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(data, o)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
def topOffHealthOfPlayer(data: SessionData, player: Player): Unit = {
|
||||
//below half health, full heal
|
||||
val maxHealthOfPlayer = player.MaxHealth.toLong
|
||||
val guid = player.GUID
|
||||
val zoneid = data.zoning.zoneChannel
|
||||
if (player.Health < maxHealthOfPlayer * 0.5f) {
|
||||
if (player.Health == 0) {
|
||||
player.Revive
|
||||
}
|
||||
player.Health = maxHealthOfPlayer.toInt
|
||||
player.LogActivity(player.ClearHistory().head)
|
||||
data.sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOfPlayer))
|
||||
data.continent.AvatarEvents ! AvatarServiceMessage(zoneid, AvatarAction.PlanetsideAttribute(guid, 0, maxHealthOfPlayer))
|
||||
}
|
||||
//below half armor, full armor
|
||||
val maxArmor = player.MaxArmor.toLong
|
||||
if (player.Armor < maxArmor) {
|
||||
player.Armor = maxArmor.toInt
|
||||
data.sendResponse(PlanetsideAttributeMessage(guid, 4, maxArmor))
|
||||
data.continent.AvatarEvents ! AvatarServiceMessage(zoneid, AvatarAction.PlanetsideAttribute(guid, 4, maxArmor))
|
||||
}
|
||||
}
|
||||
|
||||
def topOffHealthOfVehicle(data: SessionData, vehicle: Vehicle): Unit = {
|
||||
topOffHealthOfGeneric(data, vehicle)
|
||||
//vehicle shields below half, full shields
|
||||
val maxShieldsOfVehicle = vehicle.MaxShields.toLong
|
||||
val shieldsUi = vehicle.Definition.shieldUiAttribute
|
||||
if (vehicle.Shields < maxShieldsOfVehicle) {
|
||||
val guid = vehicle.GUID
|
||||
vehicle.Shields = maxShieldsOfVehicle.toInt
|
||||
data.sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle))
|
||||
data.continent.VehicleEvents ! VehicleServiceMessage(
|
||||
data.continent.id,
|
||||
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def topOffHealthOfGeneric(data: SessionData, obj: PlanetSideGameObject with Vitality): Unit = {
|
||||
//below half health, full heal
|
||||
val guid = obj.GUID
|
||||
val maxHealthOf = obj.MaxHealth.toLong
|
||||
if (obj.Health < maxHealthOf) {
|
||||
obj.Health = maxHealthOf.toInt
|
||||
data.sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf))
|
||||
data.continent.VehicleEvents ! VehicleServiceMessage(
|
||||
data.continent.id,
|
||||
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import net.psforever.objects.equipment.Equipment
|
|||
import net.psforever.objects.inventory.Container
|
||||
import net.psforever.objects.serverobject.{CommonMessages, ServerObject}
|
||||
import net.psforever.objects.serverobject.containable.Containable
|
||||
import net.psforever.objects.serverobject.dome.ForceDomePhysics
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
import net.psforever.objects.serverobject.generator.Generator
|
||||
import net.psforever.objects.serverobject.llu.CaptureFlag
|
||||
|
|
@ -30,14 +31,14 @@ import net.psforever.objects.vehicles.Utility
|
|||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.zones.{ZoneProjectile, Zoning}
|
||||
import net.psforever.packet.PlanetSideGamePacket
|
||||
import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Initial, Unk1}
|
||||
import net.psforever.packet.game.OutfitEventAction.{Initial, OutfitInfo, OutfitRankNames, Unk1}
|
||||
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitEvent, OutfitMemberEvent, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, OutfitRequestAction, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
|
||||
import net.psforever.services.RemoverActor
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Success
|
||||
|
||||
object GeneralLogic {
|
||||
|
|
@ -77,28 +78,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
|
|||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(avatarGuid)
|
||||
sessionLogic.updateBlockMap(player, pos)
|
||||
//below half health, full heal
|
||||
val maxHealth = player.MaxHealth.toLong
|
||||
if (player.Health < maxHealth) {
|
||||
player.Health = maxHealth.toInt
|
||||
player.LogActivity(player.ClearHistory().head)
|
||||
sendResponse(PlanetsideAttributeMessage(avatarGuid, 0, maxHealth))
|
||||
continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(avatarGuid, 0, maxHealth))
|
||||
}
|
||||
//below half stamina, full stamina
|
||||
val avatar = player.avatar
|
||||
val maxStamina = avatar.maxStamina
|
||||
if (avatar.stamina < maxStamina) {
|
||||
avatarActor ! AvatarActor.RestoreStamina(maxStamina)
|
||||
sendResponse(PlanetsideAttributeMessage(player.GUID, 2, maxStamina.toLong))
|
||||
}
|
||||
//below half armor, full armor
|
||||
val maxArmor = player.MaxArmor.toLong
|
||||
if (player.Armor < maxArmor) {
|
||||
player.Armor = maxArmor.toInt
|
||||
sendResponse(PlanetsideAttributeMessage(avatarGuid, 4, maxArmor))
|
||||
continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(avatarGuid, 4, maxArmor))
|
||||
}
|
||||
topOffHealthOfPlayer()
|
||||
//expected
|
||||
val isMoving = WorldEntity.isMoving(vel)
|
||||
val isMovingPlus = isMoving || isJumping || jumpThrust
|
||||
|
|
@ -538,7 +518,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
|
|||
|
||||
def handleGenericCollision(pkt: GenericCollisionMsg): Unit = {
|
||||
player.BailProtection = false
|
||||
val GenericCollisionMsg(ctype, p, _, _, pv, _, _, _, _, _, _, _) = pkt
|
||||
val GenericCollisionMsg(ctype, p, _, _, pv, t, _, _, _, _, _, _) = pkt
|
||||
if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) {
|
||||
if (ops.heightTrend) {
|
||||
ops.heightHistory = ops.heightLast
|
||||
|
|
@ -555,8 +535,21 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
|
|||
v.BailProtection = false
|
||||
case (CollisionIs.OfAircraft, Some(v: Vehicle))
|
||||
if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => ()
|
||||
case (CollisionIs.BetweenThings, Some(v: Vehicle)) =>
|
||||
v.Actor ! Vehicle.Deconstruct(Some(1 millisecond))
|
||||
continent.GUID(t) match {
|
||||
case Some(_: ForceDomePhysics) =>
|
||||
player.Actor ! Player.Die()
|
||||
case _ => ()
|
||||
}
|
||||
case (CollisionIs.BetweenThings, Some(_: Player)) =>
|
||||
continent.GUID(t) match {
|
||||
case Some(_: ForceDomePhysics) =>
|
||||
player.Actor ! Player.Die()
|
||||
case _ => ()
|
||||
}
|
||||
case (CollisionIs.BetweenThings, _) =>
|
||||
log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case")
|
||||
log.warn(s"GenericCollision: CollisionIs.BetweenThings detected - no handling case for obj id:${t.guid}")
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
|
@ -805,4 +798,16 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
|
|||
player.CapacitorState = CapacitorStateType.Idle
|
||||
}
|
||||
}
|
||||
|
||||
def topOffHealthOfPlayer(): Unit = {
|
||||
//below half health, full heal
|
||||
CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player)
|
||||
//below half stamina, full stamina
|
||||
val avatar = player.avatar
|
||||
val maxStamina = avatar.maxStamina
|
||||
if (avatar.stamina < maxStamina) {
|
||||
avatarActor ! AvatarActor.RestoreStamina(maxStamina)
|
||||
sendResponse(PlanetsideAttributeMessage(player.GUID, 2, maxStamina.toLong))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
|
|||
ops.MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
|
||||
if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L =>
|
||||
if !obj.isUpgrading || System.currentTimeMillis() - obj.CheckTurretUpgradeTime >= 1500L =>
|
||||
obj.setMiddleOfUpgrade(false)
|
||||
sessionLogic.zoning.CancelZoningProcess()
|
||||
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
|
||||
|
|
|
|||
|
|
@ -5,16 +5,15 @@ import akka.actor.{ActorContext, typed}
|
|||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations}
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle, Vehicles}
|
||||
import net.psforever.objects.{PlanetSideGameObject, Vehicle, Vehicles}
|
||||
import net.psforever.objects.serverobject.deploy.Deployment
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.vehicles.control.BfrFlight
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, PlanetsideAttributeMessage, VehicleStateMessage, VehicleSubStateMessage}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.objects.zones.interaction.InteractsWithZone
|
||||
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage}
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.types.{DriveState, PlanetSideGUID, Vector3}
|
||||
import net.psforever.types.{DriveState, Vector3}
|
||||
|
||||
object VehicleLogic {
|
||||
def apply(ops: VehicleOperations): VehicleLogic = {
|
||||
|
|
@ -30,6 +29,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
/* packets */
|
||||
|
||||
def handleVehicleState(pkt: VehicleStateMessage): Unit = {
|
||||
player.allowInteraction = false
|
||||
val VehicleStateMessage(
|
||||
vehicle_guid,
|
||||
unk1,
|
||||
|
|
@ -46,23 +46,21 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
ops.GetVehicleAndSeat() match {
|
||||
case (Some(obj), Some(0)) =>
|
||||
//we're driving the vehicle
|
||||
sessionLogic.zoning.spawn.tryQueuedActivity(vel)
|
||||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(player.GUID)
|
||||
sessionLogic.general.fallHeightTracker(pos.z)
|
||||
if (obj.MountedIn.isEmpty) {
|
||||
sessionLogic.updateBlockMap(obj, pos)
|
||||
CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player)
|
||||
CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, obj)
|
||||
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
|
||||
case Some(v: Vehicle) =>
|
||||
(pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
|
||||
case _ =>
|
||||
(pos, ang, vel, true)
|
||||
}
|
||||
topOffHealthOfPlayer()
|
||||
topOffHealth(obj)
|
||||
player.Position = pos //convenient
|
||||
if (obj.WeaponControlledFromSeat(0).isEmpty) {
|
||||
player.Orientation = Vector3.z(ang.z) //convenient
|
||||
}
|
||||
obj.Position = pos
|
||||
obj.Orientation = ang
|
||||
if (obj.MountedIn.isEmpty) {
|
||||
if (notMountedState) {
|
||||
sessionLogic.updateBlockMap(obj, position)
|
||||
if (obj.DeploymentState != DriveState.Deployed) {
|
||||
obj.Velocity = vel
|
||||
obj.Velocity = velocity
|
||||
} else {
|
||||
obj.Velocity = Some(Vector3.Zero)
|
||||
}
|
||||
|
|
@ -74,20 +72,20 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
obj.Velocity = None
|
||||
obj.Flying = None
|
||||
}
|
||||
player.Position = position //convenient
|
||||
obj.Position = position
|
||||
obj.Orientation = angle
|
||||
//
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.VehicleState(
|
||||
player.GUID,
|
||||
vehicle_guid,
|
||||
unk1,
|
||||
obj.Position,
|
||||
ang,
|
||||
obj.Velocity,
|
||||
if (obj.isFlying) {
|
||||
is_flying
|
||||
} else {
|
||||
None
|
||||
},
|
||||
position,
|
||||
angle,
|
||||
velocity,
|
||||
obj.Flying,
|
||||
unk6,
|
||||
unk7,
|
||||
wheels,
|
||||
|
|
@ -96,8 +94,6 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
)
|
||||
)
|
||||
sessionLogic.squad.updateSquad()
|
||||
player.allowInteraction = false
|
||||
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
|
||||
|
|
@ -113,6 +109,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
}
|
||||
|
||||
def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = {
|
||||
player.allowInteraction = false
|
||||
val FrameVehicleStateMessage(
|
||||
vehicle_guid,
|
||||
unk1,
|
||||
|
|
@ -132,34 +129,21 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
ops.GetVehicleAndSeat() match {
|
||||
case (Some(obj), Some(0)) =>
|
||||
//we're driving the vehicle
|
||||
sessionLogic.zoning.spawn.tryQueuedActivity(vel)
|
||||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(player.GUID)
|
||||
topOffHealthOfPlayer()
|
||||
topOffHealth(obj)
|
||||
CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player)
|
||||
CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, obj)
|
||||
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
|
||||
case Some(v: Vehicle) =>
|
||||
sessionLogic.updateBlockMap(obj, pos)
|
||||
(pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
|
||||
case _ =>
|
||||
(pos, ang, vel, true)
|
||||
}
|
||||
player.Position = position //convenient
|
||||
if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) {
|
||||
player.Orientation = Vector3.z(ang.z) //convenient
|
||||
}
|
||||
obj.Position = position
|
||||
obj.Orientation = angle
|
||||
obj.Velocity = velocity
|
||||
// if (is_crouched && obj.DeploymentState != DriveState.Kneeling) {
|
||||
// //dev stuff goes here
|
||||
// }
|
||||
// else
|
||||
// if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) {
|
||||
// //dev stuff goes here
|
||||
// }
|
||||
obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
|
||||
if (notMountedState) {
|
||||
sessionLogic.updateBlockMap(obj, position)
|
||||
if (obj.DeploymentState != DriveState.Kneeling) {
|
||||
obj.Velocity = velocity
|
||||
if (is_airborne) {
|
||||
val flight = if (ascending_flight) flight_time else -flight_time
|
||||
obj.Flying = Some(flight)
|
||||
|
|
@ -172,12 +156,14 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
obj.Velocity = None
|
||||
obj.Flying = None
|
||||
}
|
||||
player.allowInteraction = false
|
||||
obj.zoneInteractions()
|
||||
} else {
|
||||
obj.Velocity = None
|
||||
obj.Flying = None
|
||||
}
|
||||
player.Position = position //convenient
|
||||
obj.Position = position
|
||||
obj.Orientation = angle
|
||||
obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.FrameVehicleState(
|
||||
|
|
@ -214,34 +200,40 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
}
|
||||
|
||||
def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
|
||||
player.allowInteraction = false
|
||||
val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
|
||||
val (o, tools) = sessionLogic.shooting.FindContainedWeapon
|
||||
//is COSM our primary upstream packet?
|
||||
(o match {
|
||||
case Some(mount: Mountable) => (o, mount.PassengerInSeat(player))
|
||||
case Some(mount: Mountable) => (mount, mount.PassengerInSeat(player))
|
||||
case _ => (None, None)
|
||||
}) match {
|
||||
case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) =>
|
||||
case (None, _) | (_, None) => //error - we do not recognize being mounted or controlling anything, but what can we do about it?
|
||||
()
|
||||
case (Some(obj: PlanetSideGameObject with Vitality), _) =>
|
||||
case (Some(_: Vehicle), Some(0)) => //see VSM or FVSM for valid cases
|
||||
()
|
||||
case (Some(entity: PlanetSideGameObject with Mountable with InteractsWithZone), Some(_)) => //COSM is our primary upstream packet
|
||||
sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity)
|
||||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(player.GUID)
|
||||
topOffHealthOfPlayer()
|
||||
topOffHealth(obj)
|
||||
case _ =>
|
||||
CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player)
|
||||
CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, entity)
|
||||
sessionLogic.squad.updateSquad()
|
||||
case _ => //we can't disprove that COSM is our primary upstream packet, it's just that we may be missing some details
|
||||
sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity)
|
||||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(player.GUID)
|
||||
}
|
||||
//the majority of the following check retrieves information to determine if we are in control of the child
|
||||
tools.find { _.GUID == object_guid } match {
|
||||
//in the following condition we are in control of the child
|
||||
tools.find(_.GUID == object_guid) match {
|
||||
case None =>
|
||||
//todo: old warning; this state is problematic, but can trigger in otherwise valid instances
|
||||
//old warning; this state is problematic, but can trigger in otherwise valid instances
|
||||
//log.warn(
|
||||
// s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}"
|
||||
//)
|
||||
case Some(_) =>
|
||||
//TODO set tool orientation?
|
||||
player.Orientation = Vector3(0f, pitch, yaw)
|
||||
case Some(tool) =>
|
||||
val angle = Vector3(0f, pitch, yaw)
|
||||
tool.Orientation = angle
|
||||
player.Orientation = angle
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw)
|
||||
|
|
@ -342,56 +334,4 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = {
|
||||
obj match {
|
||||
case _: Player => topOffHealthOfPlayer()
|
||||
case v: Vehicle => topOffHealthOfVehicle(v)
|
||||
case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(o)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
private def topOffHealthOfPlayer(): Unit = {
|
||||
//driver below half health, full heal
|
||||
val maxHealthOfPlayer = player.MaxHealth.toLong
|
||||
if (player.Health < maxHealthOfPlayer * 0.5f) {
|
||||
player.Health = maxHealthOfPlayer.toInt
|
||||
player.LogActivity(player.ClearHistory().head)
|
||||
sendResponse(PlanetsideAttributeMessage(player.GUID, 0, maxHealthOfPlayer))
|
||||
continent.AvatarEvents ! AvatarServiceMessage(sessionLogic.zoning.zoneChannel, AvatarAction.PlanetsideAttribute(player.GUID, 0, maxHealthOfPlayer))
|
||||
}
|
||||
}
|
||||
|
||||
private def topOffHealthOfVehicle(vehicle: Vehicle): Unit = {
|
||||
topOffHealthOfPlayer()
|
||||
topOffHealthOfGeneric(vehicle)
|
||||
//vehicle shields below half, full shields
|
||||
val maxShieldsOfVehicle = vehicle.MaxShields.toLong
|
||||
val shieldsUi = vehicle.Definition.shieldUiAttribute
|
||||
if (vehicle.Shields < maxShieldsOfVehicle) {
|
||||
val guid = vehicle.GUID
|
||||
vehicle.Shields = maxShieldsOfVehicle.toInt
|
||||
sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle))
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def topOffHealthOfGeneric(obj: PlanetSideGameObject with Vitality): Unit = {
|
||||
topOffHealthOfPlayer()
|
||||
//vehicle below half health, full heal
|
||||
val guid = obj.GUID
|
||||
val maxHealthOf = obj.MaxHealth.toLong
|
||||
if (obj.Health < maxHealthOf) {
|
||||
obj.Health = maxHealthOf.toInt
|
||||
sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf))
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,15 @@ import net.psforever.objects.inventory.Container
|
|||
import net.psforever.objects.serverobject.{PlanetSideServerObject, ServerObject}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.containable.Containable
|
||||
import net.psforever.objects.serverobject.damage.Damageable
|
||||
import net.psforever.objects.serverobject.dome.ForceDomePhysics
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
import net.psforever.objects.serverobject.generator.Generator
|
||||
import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf
|
||||
import net.psforever.objects.serverobject.llu.CaptureFlag
|
||||
import net.psforever.objects.serverobject.locks.IFFLock
|
||||
import net.psforever.objects.serverobject.mblocker.Locker
|
||||
import net.psforever.objects.serverobject.mount.MountableEntity
|
||||
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
||||
import net.psforever.objects.serverobject.structures.WarpGate
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
|
||||
|
|
@ -29,11 +32,11 @@ import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
|
|||
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
|
||||
import net.psforever.objects.serverobject.tube.SpawnTube
|
||||
import net.psforever.objects.serverobject.turret.FacilityTurret
|
||||
import net.psforever.objects.sourcing.SourceEntry
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||
import net.psforever.objects.vehicles.Utility
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason}
|
||||
import net.psforever.objects.vital.etc.SuicideReason
|
||||
import net.psforever.objects.vital.etc.{ForceDomeExposure, SuicideReason}
|
||||
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||
import net.psforever.objects.zones.{ZoneProjectile, Zoning}
|
||||
import net.psforever.packet.PlanetSideGamePacket
|
||||
|
|
@ -636,10 +639,15 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
|
|||
case (CollisionIs.OfAircraft, out @ Some(v: Vehicle))
|
||||
if v.Definition.CanFly && v.Seats(0).occupant.contains(player) =>
|
||||
(out, sessionLogic.validObject(t, decorator = "GenericCollision/Aircraft"), false, pv)
|
||||
case (CollisionIs.BetweenThings, _) =>
|
||||
log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case")
|
||||
case (CollisionIs.BetweenThings, out @ Some(target: PlanetSideServerObject with MountableEntity)) =>
|
||||
target.BailProtection = false
|
||||
player.BailProtection = false
|
||||
(out, sessionLogic.validObject(t, decorator = "GenericCollision/Surface"), false, pv)
|
||||
case (_, Some(obj)) =>
|
||||
log.error(s"GenericCollision: $ctype detected: no handling case for ${obj.Definition.Name}")
|
||||
(None, None, false, Vector3.Zero)
|
||||
case _ =>
|
||||
case (_, None) =>
|
||||
log.error(s"GenericCollision: $ctype detected: no entity detected as 'Primary'")
|
||||
(None, None, false, Vector3.Zero)
|
||||
}
|
||||
val curr = System.currentTimeMillis()
|
||||
|
|
@ -661,6 +669,16 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
|
|||
}
|
||||
}
|
||||
|
||||
case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _, Some(field: ForceDomePhysics)) =>
|
||||
us.Actor ! Damageable.MakeVulnerable
|
||||
us.Actor ! Vitality.Damage(
|
||||
DamageInteraction(
|
||||
PlayerSource(player),
|
||||
ForceDomeExposure(SourceEntry(field)),
|
||||
player.Position
|
||||
).calculate()
|
||||
)
|
||||
|
||||
case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) =>
|
||||
collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, V
|
|||
import net.psforever.objects.definition.{BasicDefinition, ObjectDefinition}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions
|
||||
import net.psforever.objects.serverobject.hackable.GenericHackables
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.structures.WarpGate
|
||||
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
|
||||
|
|
@ -105,7 +104,8 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
|
|||
ops.MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 =>
|
||||
if seatNumber == 0 &&
|
||||
obj.Definition.MaxCapacitor > 0 =>
|
||||
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
|
|
@ -134,13 +134,9 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
|
|||
ops.MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if obj.Definition.MaxCapacitor > 0 =>
|
||||
log.info(s"${player.Name} mounts ${
|
||||
obj.SeatPermissionGroup(seatNumber) match {
|
||||
case Some(seatType) => s"a $seatType seat (#$seatNumber)"
|
||||
case None => "a seat"
|
||||
}
|
||||
} of the ${obj.Definition.Name}")
|
||||
if obj.Definition.MaxCapacitor > 0 &&
|
||||
obj.SeatPermissionGroup(seatNumber).contains(AccessPermissionGroup.Gunner) =>
|
||||
log.info(s"${player.Name} mounts the #$seatNumber gunner seat of the ${obj.Definition.Name}")
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
|
|
@ -149,17 +145,26 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
|
|||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
|
||||
sessionLogic.general.accessContainer(obj)
|
||||
ops.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
ops.MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _) =>
|
||||
log.info(s"${player.Name} mounts the ${
|
||||
obj.SeatPermissionGroup(seatNumber) match {
|
||||
case Some(seatType) => s"a $seatType seat (#$seatNumber)"
|
||||
case None => "a seat"
|
||||
}
|
||||
} of the ${obj.Definition.Name}")
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if obj.Definition.MaxCapacitor > 0 =>
|
||||
log.info(s"${player.Name} mounts the #$seatNumber seat of the ${obj.Definition.Name}")
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
|
||||
sessionLogic.general.accessContainer(obj)
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
ops.MountingAction(tplayer, obj, seatNumber)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if obj.SeatPermissionGroup(seatNumber).contains(AccessPermissionGroup.Gunner) =>
|
||||
log.info(s"${player.Name} mounts the #$seatNumber gunner seat of the ${obj.Definition.Name}")
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
|
|
@ -167,10 +172,21 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
|
|||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sessionLogic.general.accessContainer(obj)
|
||||
ops.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
ops.MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _) =>
|
||||
log.info(s"${player.Name} mounts the #$seatNumber seat of the ${obj.Definition.Name}")
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sessionLogic.general.accessContainer(obj)
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
ops.MountingAction(tplayer, obj, seatNumber)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
|
||||
|
||||
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
|
||||
if obj.Definition == GlobalDefinitions.vanu_sentry_turret =>
|
||||
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
|
||||
|
|
@ -181,7 +197,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
|
|||
ops.MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
|
||||
if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L =>
|
||||
if !obj.isUpgrading || System.currentTimeMillis() - obj.CheckTurretUpgradeTime >= 1500L =>
|
||||
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
|
||||
obj.setMiddleOfUpgrade(false)
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ import akka.actor.{ActorContext, typed}
|
|||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations}
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.{Vehicle, Vehicles}
|
||||
import net.psforever.objects.{PlanetSideGameObject, Vehicle, Vehicles}
|
||||
import net.psforever.objects.serverobject.deploy.Deployment
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.vehicles.control.BfrFlight
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects.zones.interaction.InteractsWithZone
|
||||
import net.psforever.packet.game.{ChatMsg, ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage}
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.types.{ChatMessageType, DriveState, Vector3}
|
||||
|
|
@ -48,18 +49,16 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(player.GUID)
|
||||
sessionLogic.general.fallHeightTracker(pos.z)
|
||||
if (obj.MountedIn.isEmpty) {
|
||||
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
|
||||
case Some(v: Vehicle) =>
|
||||
(pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
|
||||
case _ =>
|
||||
(pos, ang, vel, true)
|
||||
}
|
||||
if (notMountedState) {
|
||||
sessionLogic.updateBlockMap(obj, pos)
|
||||
}
|
||||
player.Position = pos //convenient
|
||||
if (obj.WeaponControlledFromSeat(0).isEmpty) {
|
||||
player.Orientation = Vector3.z(ang.z) //convenient
|
||||
}
|
||||
obj.Position = pos
|
||||
obj.Orientation = ang
|
||||
if (obj.MountedIn.isEmpty) {
|
||||
if (obj.DeploymentState != DriveState.Deployed) {
|
||||
obj.Velocity = vel
|
||||
obj.Velocity = velocity
|
||||
} else {
|
||||
obj.Velocity = Some(Vector3.Zero)
|
||||
}
|
||||
|
|
@ -67,10 +66,14 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
obj.Flying = is_flying //usually Some(7)
|
||||
}
|
||||
obj.Cloaked = obj.Definition.CanCloak && is_cloaked
|
||||
obj.zoneInteractions()
|
||||
} else {
|
||||
obj.Velocity = None
|
||||
obj.Flying = None
|
||||
}
|
||||
player.Position = position //convenient
|
||||
obj.Position = position
|
||||
obj.Orientation = angle
|
||||
//
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
|
|
@ -78,14 +81,10 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
player.GUID,
|
||||
vehicle_guid,
|
||||
unk1,
|
||||
obj.Position,
|
||||
ang,
|
||||
obj.Velocity,
|
||||
if (obj.isFlying) {
|
||||
is_flying
|
||||
} else {
|
||||
None
|
||||
},
|
||||
position,
|
||||
angle,
|
||||
velocity,
|
||||
obj.Flying,
|
||||
unk6,
|
||||
unk7,
|
||||
wheels,
|
||||
|
|
@ -94,10 +93,9 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
)
|
||||
)
|
||||
sessionLogic.squad.updateSquad()
|
||||
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
|
||||
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
|
||||
//placing a "not driving" warning here may trigger as we are disembarking the vehicle
|
||||
case (_, Some(index)) =>
|
||||
log.error(
|
||||
s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
|
||||
|
|
@ -132,30 +130,17 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
sessionLogic.zoning.spawn.tryQueuedActivity(vel)
|
||||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(player.GUID)
|
||||
sessionLogic.general.fallHeightTracker(pos.z)
|
||||
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
|
||||
case Some(v: Vehicle) =>
|
||||
sessionLogic.updateBlockMap(obj, pos)
|
||||
(pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
|
||||
case _ =>
|
||||
(pos, ang, vel, true)
|
||||
}
|
||||
player.Position = position //convenient
|
||||
if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) {
|
||||
player.Orientation = Vector3.z(ang.z) //convenient
|
||||
}
|
||||
obj.Position = position
|
||||
obj.Orientation = angle
|
||||
obj.Velocity = velocity
|
||||
// if (is_crouched && obj.DeploymentState != DriveState.Kneeling) {
|
||||
// //dev stuff goes here
|
||||
// }
|
||||
// else
|
||||
// if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) {
|
||||
// //dev stuff goes here
|
||||
// }
|
||||
obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
|
||||
if (notMountedState) {
|
||||
sessionLogic.updateBlockMap(obj, position)
|
||||
if (obj.DeploymentState != DriveState.Kneeling) {
|
||||
obj.Velocity = velocity
|
||||
if (is_airborne) {
|
||||
val flight = if (ascending_flight) flight_time else -flight_time
|
||||
obj.Flying = Some(flight)
|
||||
|
|
@ -173,6 +158,10 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
obj.Velocity = None
|
||||
obj.Flying = None
|
||||
}
|
||||
player.Position = position //convenient
|
||||
obj.Position = position
|
||||
obj.Orientation = angle
|
||||
obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.FrameVehicleState(
|
||||
|
|
@ -195,8 +184,8 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
)
|
||||
sessionLogic.squad.updateSquad()
|
||||
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
|
||||
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
|
||||
//placing a "not driving" warning here may trigger as we are disembarking the vehicle
|
||||
case (_, Some(index)) =>
|
||||
log.error(
|
||||
s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
|
||||
|
|
@ -211,28 +200,36 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
|
|||
def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
|
||||
val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
|
||||
val (o, tools) = sessionLogic.shooting.FindContainedWeapon
|
||||
//is COSM our primary upstream packet?
|
||||
(o match {
|
||||
case Some(mount: Mountable) => (o, mount.PassengerInSeat(player))
|
||||
case Some(mount: Mountable) => (mount, mount.PassengerInSeat(player))
|
||||
case _ => (None, None)
|
||||
}) match {
|
||||
case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) =>
|
||||
case (None, _) | (_, None) => //error - we do not recognize being mounted or controlling anything, but what can we do about it?
|
||||
()
|
||||
case _ =>
|
||||
sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals?
|
||||
case (Some(_: Vehicle), Some(0)) => //see VSM or FVSM for valid cases
|
||||
()
|
||||
case (Some(entity: PlanetSideGameObject with Mountable with InteractsWithZone), Some(seatNumber)) => //COSM is our primary upstream packet
|
||||
sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity)
|
||||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(player.GUID)
|
||||
VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(entity, seatNumber)
|
||||
sessionLogic.squad.updateSquad()
|
||||
case _ => //we can't disprove that COSM is our primary upstream packet, it's just that we may be missing some details
|
||||
sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity)
|
||||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(player.GUID)
|
||||
}
|
||||
//the majority of the following check retrieves information to determine if we are in control of the child
|
||||
tools.find { _.GUID == object_guid } match {
|
||||
//in the following condition we are in control of the child
|
||||
tools.find(_.GUID == object_guid) match {
|
||||
case None =>
|
||||
//todo: old warning; this state is problematic, but can trigger in otherwise valid instances
|
||||
//old warning; this state is problematic, but can trigger in otherwise valid instances
|
||||
//log.warn(
|
||||
// s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}"
|
||||
//)
|
||||
case Some(_) =>
|
||||
//TODO set tool orientation?
|
||||
player.Orientation = Vector3(0f, pitch, yaw)
|
||||
case Some(tool) =>
|
||||
val angle = Vector3(0f, pitch, yaw)
|
||||
tool.Orientation = angle
|
||||
player.Orientation = angle
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw)
|
||||
|
|
|
|||
|
|
@ -9,15 +9,18 @@ import net.psforever.objects.avatar.{Avatar, Implant}
|
|||
import net.psforever.objects.ballistics.Projectile
|
||||
import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition}
|
||||
import net.psforever.objects.serverobject.containable.Containable
|
||||
import net.psforever.objects.serverobject.dome.ForceDomePhysics
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
import net.psforever.objects.vehicles.Utility
|
||||
import net.psforever.objects.zones.ZoneProjectile
|
||||
import net.psforever.packet.PlanetSideGamePacket
|
||||
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
|
||||
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
|
||||
import net.psforever.services.account.AccountPersistenceService
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.types.{ExoSuitType, Vector3}
|
||||
|
||||
import scala.concurrent.duration.DurationInt
|
||||
|
||||
object GeneralLogic {
|
||||
def apply(ops: GeneralOperations): GeneralLogic = {
|
||||
new GeneralLogic(ops, ops.context)
|
||||
|
|
@ -283,7 +286,34 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
|
|||
}
|
||||
}
|
||||
|
||||
def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { /* intentionally blank */ }
|
||||
def handleGenericCollision(pkt: GenericCollisionMsg): Unit = {
|
||||
player.BailProtection = false
|
||||
val GenericCollisionMsg(ctype, p, _, _, pv, t, _, _, _, _, _, _) = pkt
|
||||
if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) {
|
||||
if (ops.heightTrend) {
|
||||
ops.heightHistory = ops.heightLast
|
||||
}
|
||||
else {
|
||||
ops.heightLast = ops.heightHistory
|
||||
}
|
||||
}
|
||||
(ctype, sessionLogic.validObject(p, decorator = "GenericCollision/Primary")) match {
|
||||
case (CollisionIs.BetweenThings, Some(v: Vehicle)) =>
|
||||
v.Actor ! Vehicle.Deconstruct(Some(1 millisecond))
|
||||
continent.GUID(t) match {
|
||||
case Some(_: ForceDomePhysics) =>
|
||||
player.Actor ! Player.Die()
|
||||
case _ => ()
|
||||
}
|
||||
case (CollisionIs.BetweenThings, Some(_: Player)) =>
|
||||
continent.GUID(t) match {
|
||||
case Some(_: ForceDomePhysics) =>
|
||||
player.Actor ! Player.Die()
|
||||
case _ => ()
|
||||
}
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
|
|
|
|||
|
|
@ -486,7 +486,14 @@ class SessionData(
|
|||
zoning.spawn.interimUngunnedVehicle = None
|
||||
persist()
|
||||
if (player.HasGUID) {
|
||||
zoning.spawn.tryQueuedActivity(player.Velocity)
|
||||
turnCounterFunc(player.GUID)
|
||||
continent
|
||||
.GUID(player.VehicleSeated)
|
||||
.collect { case v: PlanetSideGameObject with Mountable =>
|
||||
VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(v, player)
|
||||
}
|
||||
squad.updateSquad()
|
||||
} else {
|
||||
turnCounterFunc(PlanetSideGUID(0))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import net.psforever.objects.serverobject.deploy.Deployment
|
|||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.zones.interaction.InteractsWithZone
|
||||
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, VehicleSubStateMessage, _}
|
||||
import net.psforever.types.DriveState
|
||||
|
||||
|
|
@ -195,3 +196,55 @@ class VehicleOperations(
|
|||
sendResponse(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
object VehicleOperations {
|
||||
def updateMountableZoneInteractionFromEarliestSeat(obj: PlanetSideGameObject with Mountable, passenger: Player): Unit = {
|
||||
obj.PassengerInSeat(passenger).foreach { seatNumber =>
|
||||
updateMountableZoneInteractionFromEarliestSeat(obj, seatNumber)
|
||||
}
|
||||
}
|
||||
|
||||
def updateMountableZoneInteractionFromEarliestSeat(obj: PlanetSideGameObject with Mountable, seatNumber: Int): Unit = {
|
||||
obj match {
|
||||
case obj: Vehicle =>
|
||||
updateVehicleZoneInteractionFromEarliestSeat(obj, seatNumber)
|
||||
case obj: Mountable with InteractsWithZone =>
|
||||
updateEntityZoneInteractionFromEarliestSeat(obj, seatNumber, obj)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
private def updateVehicleZoneInteractionFromEarliestSeat(obj: Vehicle, seatNumber: Int): Unit = {
|
||||
//vehicle being ferried; check if the ferry has occupants that might have speaking rights before us
|
||||
var targetVehicle = obj
|
||||
val carrierSeatVacancy: Boolean = obj match {
|
||||
case v if v.MountedIn.nonEmpty =>
|
||||
obj.Zone.GUID(v.MountedIn) match {
|
||||
case Some(carrier: Vehicle) =>
|
||||
targetVehicle = carrier
|
||||
!carrier.Seats.values.exists(_.isOccupied)
|
||||
case _ =>
|
||||
true
|
||||
}
|
||||
case _ => true
|
||||
}
|
||||
if (carrierSeatVacancy) {
|
||||
updateEntityZoneInteractionFromEarliestSeat(obj, seatNumber, targetVehicle)
|
||||
}
|
||||
}
|
||||
|
||||
private def updateEntityZoneInteractionFromEarliestSeat(
|
||||
obj: Mountable with InteractsWithZone,
|
||||
seatNumber: Int,
|
||||
updateTarget: InteractsWithZone
|
||||
): Unit = {
|
||||
if (seatNumber == 0) {
|
||||
//we're responsible as the primary operator
|
||||
updateTarget.zoneInteractions()
|
||||
} else if(!obj.Seat(seatNumber = 0).exists(_.isOccupied) && obj.OccupiedSeats().headOption.contains(seatNumber)) {
|
||||
//there is no primary operator
|
||||
//we are responsible as the player in the seat closest to the "front"
|
||||
updateTarget.zoneInteractions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1130,7 +1130,7 @@ class ZoningOperations(
|
|||
case _ => ()
|
||||
}
|
||||
// capitol force dome state
|
||||
if (building.IsCapitol && building.ForceDomeActive) {
|
||||
if (building.IsCapitol && building.ForceDome.exists(_.Energized)) {
|
||||
sendResponse(GenericObjectActionMessage(guid, 13))
|
||||
}
|
||||
// amenities
|
||||
|
|
@ -2007,7 +2007,7 @@ class ZoningOperations(
|
|||
/** Upstream message counter<br>
|
||||
* Checks for server acknowledgement of the following messages in the following conditions:<br>
|
||||
* `PlayerStateMessageUpstream` (infantry)<br>
|
||||
* `VehicleStateMessage` (driver mount only)<br>
|
||||
* `VehicleStateMessage` and `FrameVehicleStateMessage` (driver mount)<br>
|
||||
* `ChildObjectStateMessage` (any gunner mount that is not the driver)<br>
|
||||
* `KeepAliveMessage` (any passenger mount that is not the driver)<br>
|
||||
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
|
|||
import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle}
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3}
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.actors.zone.building.MajorFacilityLogic
|
||||
import net.psforever.objects.avatar.scoring.Kill
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior
|
||||
|
|
@ -110,7 +109,6 @@ class ZoneActor(
|
|||
//warp gates are controlled by game logic and are better off not restored via the database
|
||||
case Some(b) =>
|
||||
if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) {
|
||||
b.ForceDomeActive = MajorFacilityLogic.checkForceDomeStatus(b).getOrElse(false)
|
||||
b.Neighbours.getOrElse(Nil).foreach(_.Actor ! BuildingActor.AlertToFactionChange(b))
|
||||
b.CaptureTerminal.collect { terminal =>
|
||||
val msg = CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured = true)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import akka.actor.typed.{ActorRef, Behavior}
|
|||
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
|
||||
import net.psforever.actors.commands.NtuCommand
|
||||
import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails, ZoneActor}
|
||||
import net.psforever.objects.serverobject.damage.Damageable
|
||||
import net.psforever.objects.serverobject.dome.ForceDomePhysics
|
||||
import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl}
|
||||
import net.psforever.objects.serverobject.resourcesilo.ResourceSiloControl
|
||||
import net.psforever.objects.serverobject.structures.{Amenity, Building}
|
||||
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
|
|
@ -15,7 +18,7 @@ import net.psforever.services.{InterstellarClusterService, Service}
|
|||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState}
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
|
||||
|
||||
/**
|
||||
* A package class that conveys the important information for handling facility updates.
|
||||
|
|
@ -56,99 +59,6 @@ case object MajorFacilityLogic
|
|||
MajorFacilityWrapper(building, context, details.galaxyService, details.interstellarCluster)
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate the conditions of the building
|
||||
* and determine if its capitol force dome state should be updated
|
||||
* to reflect the actual conditions of the base or its surrounding bases.
|
||||
* If this building is considered a subcapitol facility to the zone's actual capitol facility,
|
||||
* and has the capitol force dome has a dependency upon it,
|
||||
* pass a message onto that facility that it should check its own state alignment.
|
||||
* @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building
|
||||
*/
|
||||
private def alignForceDomeStatus(details: BuildingWrapper, mapUpdateOnChange: Boolean = true): Behavior[Command] = {
|
||||
val building = details.building
|
||||
checkForceDomeStatus(building) match {
|
||||
case Some(updatedStatus) if updatedStatus != building.ForceDomeActive =>
|
||||
updateForceDomeStatus(details, updatedStatus, mapUpdateOnChange)
|
||||
case _ => ;
|
||||
}
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a message to update the state of the clients with the server state of the capitol force dome.
|
||||
* @param updatedStatus the new capitol force dome status
|
||||
* @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building
|
||||
*/
|
||||
private def updateForceDomeStatus(
|
||||
details: BuildingWrapper,
|
||||
updatedStatus: Boolean,
|
||||
mapUpdateOnChange: Boolean
|
||||
): Unit = {
|
||||
val building = details.building
|
||||
val zone = building.Zone
|
||||
building.ForceDomeActive = updatedStatus
|
||||
zone.LocalEvents ! LocalServiceMessage(
|
||||
zone.id,
|
||||
LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, building.GUID, updatedStatus)
|
||||
)
|
||||
if (mapUpdateOnChange) {
|
||||
details.context.self ! BuildingActor.MapUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The natural conditions of a facility that is not eligible for its capitol force dome to be expanded.
|
||||
* The only test not employed is whether or not the target building is a capitol.
|
||||
* Ommission of this condition makes this test capable of evaluating subcapitol eligibility
|
||||
* for capitol force dome expansion.
|
||||
* @param building the target building
|
||||
* @return `true`, if the conditions for capitol force dome are not met;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
private def invalidBuildingCapitolForceDomeConditions(building: Building): Boolean = {
|
||||
building.Faction == PlanetSideEmpire.NEUTRAL ||
|
||||
building.NtuLevel == 0 ||
|
||||
(building.Generator match {
|
||||
case Some(o) => o.Condition == PlanetSideGeneratorState.Destroyed
|
||||
case _ => false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* If this building is a capitol major facility,
|
||||
* use the faction affinity, the generator status, and the resource silo's capacitance level
|
||||
* to determine if the capitol force dome should be active.
|
||||
* @param building the building being evaluated
|
||||
* @return the condition of the capitol force dome;
|
||||
* `None`, if the facility is not a capitol building;
|
||||
* `Some(true|false)` to indicate the state of the force dome
|
||||
*/
|
||||
def checkForceDomeStatus(building: Building): Option[Boolean] = {
|
||||
if (building.IsCapitol) {
|
||||
val originalStatus = building.ForceDomeActive
|
||||
val faction = building.Faction
|
||||
val updatedStatus = if (invalidBuildingCapitolForceDomeConditions(building)) {
|
||||
false
|
||||
} else {
|
||||
val ownedSubCapitols = building.Neighbours(faction) match {
|
||||
case Some(buildings: Set[Building]) => buildings.count { b => !invalidBuildingCapitolForceDomeConditions(b) }
|
||||
case None => 0
|
||||
}
|
||||
if (originalStatus && ownedSubCapitols <= 1) {
|
||||
false
|
||||
} else if (!originalStatus && ownedSubCapitols > 1) {
|
||||
true
|
||||
} else {
|
||||
originalStatus
|
||||
}
|
||||
}
|
||||
Some(updatedStatus)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The power structure of major facilities has to be statused on the continental map
|
||||
* via the state of its nanite-to-energy generator, and
|
||||
|
|
@ -197,6 +107,18 @@ case object MajorFacilityLogic
|
|||
)
|
||||
}
|
||||
// No map update needed - will be sent by `HackCaptureActor` when required
|
||||
case dome: ForceDomePhysics =>
|
||||
val building = details.building
|
||||
// The protection of the force dome modifies the NTU drain rate
|
||||
val multiplier: Float = calculateNtuDrainMultiplierFrom(details.building, domeOpt = Some(dome))
|
||||
building.NtuSource.foreach(_.Actor ! ResourceSiloControl.DrainMultiplier(multiplier))
|
||||
// The protection of the force dome marks the generator (and some other amenities) as being invulnerable
|
||||
val msg = Damageable.Vulnerability(dome.Perimeter.nonEmpty)
|
||||
val applicable = dome.Definition.ApplyProtectionTo
|
||||
building
|
||||
.Amenities
|
||||
.filter(amenity => applicable.contains(amenity.Definition))
|
||||
.foreach { _.Actor ! msg }
|
||||
case _ =>
|
||||
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
|
||||
}
|
||||
|
|
@ -267,7 +189,7 @@ case object MajorFacilityLogic
|
|||
}
|
||||
|
||||
/**
|
||||
* The generator is an extrememly important amenity of a major facility
|
||||
* The generator is an extremely important amenity of a major facility
|
||||
* that is given its own status indicators that are apparent from the continental map
|
||||
* and warning messages that are displayed to everyone who might have an interest in the that particular generator.
|
||||
* @param details package class that conveys the important information
|
||||
|
|
@ -314,7 +236,6 @@ case object MajorFacilityLogic
|
|||
true
|
||||
case Some(GeneratorControl.Event.Offline) =>
|
||||
powerLost(details)
|
||||
alignForceDomeStatus(details, mapUpdateOnChange = false)
|
||||
val zone = building.Zone
|
||||
val msg = AvatarAction.PlanetsideAttributeToAll(building.GUID, 46, 2)
|
||||
building.PlayersInSOI.foreach { player =>
|
||||
|
|
@ -326,7 +247,6 @@ case object MajorFacilityLogic
|
|||
case Some(GeneratorControl.Event.Online) =>
|
||||
// Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal.
|
||||
powerRestored(details)
|
||||
alignForceDomeStatus(details, mapUpdateOnChange = false)
|
||||
val events = zone.AvatarEvents
|
||||
val guid = building.GUID
|
||||
val msg1 = AvatarAction.PlanetsideAttributeToAll(guid, 46, 0)
|
||||
|
|
@ -348,16 +268,17 @@ case object MajorFacilityLogic
|
|||
): Behavior[Command] = {
|
||||
if (details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply) {
|
||||
BuildingActor.setFactionTo(details, faction, log)
|
||||
alignForceDomeStatus(details, mapUpdateOnChange = false)
|
||||
val building = details.building
|
||||
building.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(building) }
|
||||
val alertMsg = BuildingActor.AlertToFactionChange(building)
|
||||
building.Neighbours.getOrElse(Nil).foreach { _.Actor ! alertMsg }
|
||||
building.Amenities.foreach { _.Actor ! alertMsg }
|
||||
}
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
def alertToFactionChange(details: BuildingWrapper, building: Building): Behavior[Command] = {
|
||||
alignForceDomeStatus(details)
|
||||
val bldg = details.building
|
||||
bldg.Amenities.foreach { _.Actor ! BuildingActor.AlertToFactionChange(building) } //todo map update?
|
||||
//the presence of the flag means that we are involved in an ongoing llu hack
|
||||
(bldg.GetFlag, bldg.CaptureTerminal) match {
|
||||
case (Some(flag), Some(terminal)) if (flag.Target eq building) && flag.Faction != building.Faction =>
|
||||
|
|
@ -439,4 +360,35 @@ case object MajorFacilityLogic
|
|||
}
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
private def calculateNtuDrainMultiplierFrom(
|
||||
building: Building,
|
||||
domeOpt: Option[ForceDomePhysics] = None,
|
||||
mainTerminalOpt: Option[Any] = None
|
||||
): Float = {
|
||||
val domeParam = domeOpt.orElse {
|
||||
building.Amenities.find(_.isInstanceOf[ForceDomePhysics]) match {
|
||||
case Some(d: ForceDomePhysics) => Some(d)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
val mainTerminalParam = mainTerminalOpt.orElse(None) //todo main terminal and viruses
|
||||
getNtuDrainMultiplierFromAmenities(domeParam, mainTerminalParam)
|
||||
}
|
||||
|
||||
private def getNtuDrainMultiplierFromAmenities(
|
||||
dome: Option[ForceDomePhysics],
|
||||
mainTerminal: Option[Any]
|
||||
): Float = {
|
||||
// The force dome being expanded means all repairs are essentially for free
|
||||
dome
|
||||
.flatMap {
|
||||
case d if d.Energized => Some(0f)
|
||||
case _ => None
|
||||
}
|
||||
.orElse {
|
||||
mainTerminal.flatMap { _ => Some(2f) } //todo main terminal and viruses
|
||||
}
|
||||
.getOrElse(1f)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,7 +187,8 @@ object ExplosiveDeployableControl {
|
|||
zone,
|
||||
target,
|
||||
Zone.explosionDamage(Some(cause)),
|
||||
ExplosiveDeployableControl.detectionForExplosiveSource(target)
|
||||
ExplosiveDeployableControl.detectionForExplosiveSource(target),
|
||||
Zone.findAllTargets
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ import net.psforever.objects.serverobject.hackable.GenericHackables
|
|||
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
|
||||
import net.psforever.objects.serverobject.turret.{MountableTurret, WeaponTurrets}
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.sourcing.SourceEntry
|
||||
import net.psforever.objects.vital.{InGameActivity, ShieldCharge}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, TurretSource}
|
||||
import net.psforever.objects.vital.{DismountingActivity, InGameActivity, MountingActivity, ShieldCharge}
|
||||
import net.psforever.packet.game.HackState1
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
/** definition */
|
||||
|
||||
class FieldTurretDeployableDefinition(private val objectId: Int)
|
||||
|
|
@ -70,6 +72,21 @@ class FieldTurretControl(turret: TurretDeployable)
|
|||
player: Player
|
||||
): Boolean = MountableTurret.MountTest(TurretObject, player)
|
||||
|
||||
override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = {
|
||||
super.mountActionResponse(user, mountPoint, seatNumber)
|
||||
if (turret.PassengerInSeat(user).contains(0)) {
|
||||
val vsrc = TurretSource(turret)
|
||||
user.LogActivity(MountingActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber = 0), turret.Zone.Number))
|
||||
}
|
||||
}
|
||||
|
||||
override def dismountActionResponse(user: Player, seatBeingDismounted: Int): Unit = {
|
||||
super.dismountActionResponse(user, seatBeingDismounted)
|
||||
if (!turret.Seats(seatBeingDismounted).isOccupied) { //this seat really was vacated
|
||||
user.LogActivity(DismountingActivity(TurretSource(turret), PlayerSource(user), turret.Zone.Number))
|
||||
}
|
||||
}
|
||||
|
||||
//make certain vehicles don't charge shields too quickly
|
||||
private def canChargeShields: Boolean = {
|
||||
val func: InGameActivity => Boolean = WithShields.LastShieldChargeOrDamage(System.currentTimeMillis(), turret.Definition)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import net.psforever.objects.definition.converter._
|
|||
import net.psforever.objects.equipment._
|
||||
import net.psforever.objects.global.{GlobalDefinitionsAmmo, GlobalDefinitionsBuilding, GlobalDefinitionsDeployable, GlobalDefinitionsExoSuit, GlobalDefinitionsImplant, GlobalDefinitionsKit, GlobalDefinitionsMiscellaneous, GlobalDefinitionsProjectile, GlobalDefinitionsTool, GlobalDefinitionsVehicle}
|
||||
import net.psforever.objects.locker.LockerContainerDefinition
|
||||
import net.psforever.objects.serverobject.dome.ForceDomeDefinition
|
||||
import net.psforever.objects.serverobject.doors.DoorDefinition
|
||||
import net.psforever.objects.serverobject.generator.GeneratorDefinition
|
||||
import net.psforever.objects.serverobject.locks.IFFLockDefinition
|
||||
|
|
@ -1291,6 +1292,18 @@ object GlobalDefinitions {
|
|||
|
||||
val zipline = new GenericTeleportationDefinition(1047)
|
||||
|
||||
val force_dome_generator = new ForceDomeDefinition(322)
|
||||
|
||||
val force_dome_amp_physics = new ForceDomeDefinition(313)
|
||||
|
||||
val force_dome_comm_physics = new ForceDomeDefinition(316)
|
||||
|
||||
val force_dome_cryo_physics = new ForceDomeDefinition(319)
|
||||
|
||||
val force_dome_dsp_physics = new ForceDomeDefinition(321)
|
||||
|
||||
val force_dome_tech_physics = new ForceDomeDefinition(323)
|
||||
|
||||
/*
|
||||
Buildings
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.avatar.interaction.{TriggerOnPlayerRule, WithEntrance, WithGantry, WithLava, WithWater}
|
||||
import net.psforever.objects.avatar.interaction.{InteractWithForceDomeProtection, TriggerOnPlayerRule, WithEntrance, WithGantry, WithLava, WithWater}
|
||||
import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
|
||||
import net.psforever.objects.ballistics.InteractWithRadiationClouds
|
||||
import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets}
|
||||
|
|
@ -40,6 +40,7 @@ class Player(var avatar: Avatar)
|
|||
with InteriorAwareFromInteraction
|
||||
with AuraContainer
|
||||
with MountableEntity {
|
||||
interaction(new InteractWithForceDomeProtection())
|
||||
interaction(environment.interaction.InteractWithEnvironment(Seq(
|
||||
new WithEntrance(),
|
||||
new WithWater(avatar.name),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
|||
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
|
||||
import net.psforever.objects.serverobject.damage.Damageable
|
||||
import net.psforever.objects.serverobject.hackable.Hackable
|
||||
import net.psforever.objects.serverobject.mount.InteractWithRadiationCloudsSeatedInEntity
|
||||
import net.psforever.objects.serverobject.mount.interaction.{InteractWithForceDomeProtectionSeatedInEntity, InteractWithRadiationCloudsSeatedInEntity}
|
||||
import net.psforever.objects.serverobject.turret.auto.{AffectedByAutomaticTurretFire, AutomatedTurret}
|
||||
import net.psforever.objects.serverobject.turret.{TurretControl, TurretDefinition, WeaponTurret}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||
|
|
@ -36,6 +36,7 @@ class TurretDeployable(tdef: TurretDeployableDefinition)
|
|||
HackDuration = Array(0, 20, 10, 5)
|
||||
|
||||
if (tdef.Seats.nonEmpty) {
|
||||
interaction(new InteractWithForceDomeProtectionSeatedInEntity)
|
||||
interaction(new InteractWithTurrets())
|
||||
interaction(new InteractWithRadiationCloudsSeatedInEntity(obj = this, range = 100f))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import net.psforever.objects.serverobject.hackable.Hackable
|
|||
import net.psforever.objects.serverobject.interior.{InteriorAwareFromInteraction, Sidedness}
|
||||
import net.psforever.objects.serverobject.structures.AmenityOwner
|
||||
import net.psforever.objects.vehicles._
|
||||
import net.psforever.objects.vehicles.interaction.{TriggerOnVehicleRule, WithLava, WithWater}
|
||||
import net.psforever.objects.vehicles.interaction.{InteractWithForceDomeProtectionSeatedInVehicle, InteractWithRadiationCloudsSeatedInVehicle, TriggerOnVehicleRule, WithLava, WithWater}
|
||||
import net.psforever.objects.vital.resistance.StandardResistanceProfile
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
||||
|
|
@ -94,6 +94,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
|
|||
with AuraContainer
|
||||
with MountableEntity
|
||||
with InteriorAwareFromInteraction {
|
||||
interaction(new InteractWithForceDomeProtectionSeatedInVehicle)
|
||||
interaction(environment.interaction.InteractWithEnvironment(Seq(
|
||||
new WithEntrance(),
|
||||
new WithWater(),
|
||||
|
|
|
|||
|
|
@ -805,6 +805,15 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
|
|||
}
|
||||
}
|
||||
|
||||
override protected def canChangeVulnerability(state: Damageable.PersonalVulnerability): Boolean = {
|
||||
state match {
|
||||
case Damageable.MakeInvulnerable =>
|
||||
player.LastDamage.exists { d => System.currentTimeMillis() - d.interaction.hitTime > 2500L }
|
||||
case _ =>
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Prevent a player from deploying a telepad that belongs to a Router of a different faction
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright (c) 2025 PSForever
|
||||
package net.psforever.objects.avatar.interaction
|
||||
|
||||
import net.psforever.objects.serverobject.damage.Damageable
|
||||
import net.psforever.objects.serverobject.dome.{ForceDomeControl, ForceDomePhysics}
|
||||
import net.psforever.objects.zones.blockmap.SectorPopulation
|
||||
import net.psforever.objects.zones.interaction.{InteractsWithZone, ZoneInteraction, ZoneInteractionType}
|
||||
|
||||
case object ForceZoneProtection extends ZoneInteractionType
|
||||
|
||||
/**
|
||||
* Entities under the capitol force dome that have not died in its initial activation
|
||||
* do not take further damage until removed from under the dome or until the dome is deactivated.
|
||||
*/
|
||||
class InteractWithForceDomeProtection
|
||||
extends ZoneInteraction {
|
||||
def Type: ZoneInteractionType = ForceZoneProtection
|
||||
|
||||
def range: Float = 10f
|
||||
|
||||
/** increment to n, reevaluate the dome protecting the target, reset counter to 0 */
|
||||
private var protectSkipCounter: Int = 0
|
||||
/** dome currently protecting the target */
|
||||
private var protectedBy: Option[ForceDomePhysics] = None
|
||||
|
||||
/**
|
||||
* If the target is protected, do conditions allow it to remain protected?
|
||||
* If the target was vulnerable, can it be protected?
|
||||
* Five second pause between evaluations (0-3, wait; 4, test).
|
||||
* @see `ForceDomeControl.TargetUnderForceDome`
|
||||
* @param sector the portion of the block map being tested
|
||||
* @param target the fixed element in this test
|
||||
*/
|
||||
def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
|
||||
if (protectSkipCounter < 4) {
|
||||
protectSkipCounter += 1
|
||||
} else {
|
||||
protectSkipCounter = 0
|
||||
protectedBy match {
|
||||
case Some(dome)
|
||||
if dome.Perimeter.isEmpty ||
|
||||
target.Zone != dome.Zone ||
|
||||
!ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(dome, target, maxDistance = 0f) =>
|
||||
resetInteraction(target)
|
||||
case Some(_) =>
|
||||
() //no action
|
||||
case None =>
|
||||
searchForInteractionCause(sector, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look the through the list of amenities in this sector for capitol force domes,
|
||||
* determine which force domes are energized (activated, expanded, enveloping, etc.),
|
||||
* and find the first active dome under which the target `entity` is positioned.
|
||||
* The target `entity` is considered protected and can not be damaged until further notice.
|
||||
* @see `Damageable.MakeInvulnerable`
|
||||
* @see `ForceDomeControl.TargetUnderForceDome`
|
||||
* @param sector – the portion of the block map being tested
|
||||
* @param target – the fixed element in this test
|
||||
* @return whichever force dome entity is detected to encircle this target `entity`, if any
|
||||
*/
|
||||
private def searchForInteractionCause(sector: SectorPopulation, target: InteractsWithZone): Option[ForceDomePhysics] = {
|
||||
sector
|
||||
.amenityList
|
||||
.flatMap {
|
||||
case dome: ForceDomePhysics if dome.Perimeter.nonEmpty => Some(dome)
|
||||
case _ => None
|
||||
}
|
||||
.find { dome =>
|
||||
ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(dome, target, maxDistance = 0f)
|
||||
}
|
||||
.map { dome =>
|
||||
applyProtection(target, dome)
|
||||
dome
|
||||
}
|
||||
}
|
||||
|
||||
def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
|
||||
protectedBy = Some(dome)
|
||||
target.Actor ! Damageable.MakeInvulnerable
|
||||
}
|
||||
|
||||
/**
|
||||
* No longer invulnerable (if ever).
|
||||
* Set the counter to force a reevaluation of the vulnerability state next turn.
|
||||
* @see `Damageable.MakeVulnerable`
|
||||
* @param target the fixed element in this test
|
||||
*/
|
||||
def resetInteraction(target: InteractsWithZone): Unit = {
|
||||
protectSkipCounter = 5
|
||||
protectedBy = None
|
||||
target.Actor ! Damageable.MakeVulnerable
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ object ArmorSiphonBehavior {
|
|||
val after = item.Discharge()
|
||||
if (before > after) {
|
||||
v.Actor ! ArmorSiphonBehavior.Recharge(iguid)
|
||||
PerformDamage(
|
||||
PerformDamageIfVulnerable(
|
||||
obj,
|
||||
DamageInteraction(
|
||||
VehicleSource(obj),
|
||||
|
|
|
|||
|
|
@ -956,5 +956,70 @@ object GlobalDefinitionsMiscellaneous {
|
|||
|
||||
zipline.Name = "zipline"
|
||||
zipline.interference = InterferenceRange(deployables = 5.5f)
|
||||
|
||||
force_dome_amp_physics.Name = "force_dome_amp_physics"
|
||||
force_dome_amp_physics.UseRadius = 142.26f
|
||||
force_dome_amp_physics.PerimeterOffsets = List(
|
||||
Vector3(83.05469f, 114.875f, 0f),
|
||||
Vector3(-90.328125f, 114.875f, 0f),
|
||||
Vector3(-90.328125f, -106.90625f, 0f),
|
||||
Vector3(83.05469f, -106.90625f, 0f)
|
||||
)
|
||||
force_dome_amp_physics.ApplyProtectionTo = List(generator, manned_turret)
|
||||
force_dome_amp_physics.GeneratorOffset = Vector3(0.148f, -21.867f, 17.623f)
|
||||
|
||||
force_dome_comm_physics.Name = "force_dome_comm_physics"
|
||||
force_dome_comm_physics.UseRadius = 121.8149f
|
||||
force_dome_comm_physics.PerimeterOffsets = List(
|
||||
Vector3(35.1875f, -89.859375f, 0f),
|
||||
Vector3(80.96875f, -43.773438f, 0f),
|
||||
Vector3(80.96875f, 91.08594f, 0f),
|
||||
Vector3(-37.296875f, 91.08594f, 0f),
|
||||
Vector3(-83.640625f, 45.601562f, 0f),
|
||||
Vector3(-83.640625f, -89.859375f, 0f)
|
||||
)
|
||||
force_dome_comm_physics.ApplyProtectionTo = List(generator, manned_turret)
|
||||
force_dome_comm_physics.GeneratorOffset = Vector3(-19.481f, -4.25f, 12.649f)
|
||||
|
||||
force_dome_cryo_physics.Name = "force_dome_cryo_physics"
|
||||
force_dome_cryo_physics.UseRadius = 127.9241f //127.7963f
|
||||
force_dome_cryo_physics.PerimeterOffsets = List(
|
||||
Vector3(72.75476f, 39.902725f, 0),
|
||||
Vector3(24.505968f, 88.03482f, 0),
|
||||
Vector3(-74.73426f, 88.03482f, 0),
|
||||
Vector3(-74.73426f, -103.47f, 0),
|
||||
Vector3(72.75476f, -103.47f, 0)
|
||||
)
|
||||
force_dome_cryo_physics.ApplyProtectionTo = List(generator, implant_terminal_mech, manned_turret)
|
||||
force_dome_cryo_physics.GeneratorOffset = Vector3(-20.709f, 5.387f, 19.466f)
|
||||
|
||||
force_dome_dsp_physics.Name = "force_dome_dsp_physics"
|
||||
force_dome_dsp_physics.UseRadius = 175.8838f //175.7081f
|
||||
force_dome_dsp_physics.PerimeterOffsets = List(
|
||||
Vector3(35.03125f, -93.25f, 0f),
|
||||
Vector3(-83.1875f, -93.25f, 0f),
|
||||
Vector3(-83.1875f, 114.515625f, 0f),
|
||||
Vector3(-12.109375f, 188.26562f, 0f),
|
||||
Vector3(130.44531f, 188.26562f, 0f),
|
||||
Vector3(130.44531f, -93.28125f, 0f)
|
||||
)
|
||||
force_dome_dsp_physics.ApplyProtectionTo = List(generator, manned_turret)
|
||||
force_dome_dsp_physics.GeneratorOffset = Vector3(12.698f, 25.15f, 12.541f)
|
||||
|
||||
force_dome_tech_physics.Name = "force_dome_tech_physics"
|
||||
force_dome_tech_physics.UseRadius = 150.1284f
|
||||
force_dome_tech_physics.PerimeterOffsets = List( //todo double-check, e.g., eisa, esamir
|
||||
Vector3(130.14636f, -95.20665f, 0f),
|
||||
Vector3(130.14636f, 34.441734f, 0f),
|
||||
Vector3(103.98575f, 52.58408f, 0f),
|
||||
Vector3(16.405174f, 54.746464f, 0f),
|
||||
Vector3(14.256668f, 107.01521f, 0f),
|
||||
Vector3(-92.08687f, 107.01521f, 0f),
|
||||
Vector3(-92.08687f, -96.176155f, 0f),
|
||||
Vector3(-73.64424f, -114.65837f, 0f),
|
||||
Vector3(102.12191f, -114.65837f, 0f)
|
||||
)
|
||||
force_dome_tech_physics.ApplyProtectionTo = List(generator, manned_turret)
|
||||
force_dome_tech_physics.GeneratorOffset = Vector3(13.001f, -32.612f, 22.59f)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import net.psforever.objects.vital.Vitality
|
|||
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
|
||||
import net.psforever.objects.vital.resolution.ResolutionCalculations
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
/**
|
||||
* The base "control" `Actor` mixin for damage-handling code.
|
||||
* A valid entity requires health points and
|
||||
|
|
@ -17,7 +19,6 @@ import net.psforever.objects.vital.resolution.ResolutionCalculations
|
|||
* All of these should be affected by the damage where applicable.
|
||||
*/
|
||||
trait Damageable {
|
||||
|
||||
/**
|
||||
* Contextual access to the object being the target of this damage.
|
||||
* Needs declaration in lowest implementing code.
|
||||
|
|
@ -25,24 +26,50 @@ trait Damageable {
|
|||
*/
|
||||
def DamageableObject: Damageable.Target
|
||||
|
||||
/** a local `canDamage` flag */
|
||||
private var isVulnerable: Boolean = true
|
||||
|
||||
/** the official mixin hook;
|
||||
* `orElse` onto the "control" `Actor` `receive`; or,
|
||||
* cite the `originalTakesDamage` protocol during inheritance overrides */
|
||||
val takesDamage: Receive = {
|
||||
case Damageable.MakeVulnerable if canChangeVulnerability(Damageable.MakeVulnerable) =>
|
||||
isVulnerable = true
|
||||
|
||||
case Damageable.MakeInvulnerable if canChangeVulnerability(Damageable.MakeInvulnerable) =>
|
||||
isVulnerable = false
|
||||
|
||||
case Vitality.Damage(damage_func) =>
|
||||
val obj = DamageableObject
|
||||
if (obj.CanDamage) {
|
||||
PerformDamage(obj, damage_func)
|
||||
}
|
||||
PerformDamageIfVulnerable(DamageableObject, damage_func)
|
||||
}
|
||||
|
||||
/** a duplicate of the core implementation for the default mixin hook, for use in overriding */
|
||||
final val originalTakesDamage: Receive = {
|
||||
case Damageable.MakeVulnerable if canChangeVulnerability(Damageable.MakeVulnerable) =>
|
||||
isVulnerable = true
|
||||
|
||||
case Damageable.MakeInvulnerable if canChangeVulnerability(Damageable.MakeInvulnerable) =>
|
||||
isVulnerable = false
|
||||
|
||||
case Vitality.Damage(damage_func) =>
|
||||
val obj = DamageableObject
|
||||
if (obj.CanDamage) {
|
||||
PerformDamage(obj, damage_func)
|
||||
}
|
||||
PerformDamageIfVulnerable(DamageableObject, damage_func)
|
||||
}
|
||||
|
||||
protected def canChangeVulnerability(@unused state: Damageable.PersonalVulnerability): Boolean = {
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
* Assess if the target is vulnerable to damage.
|
||||
* If so, attempt damage calculations.
|
||||
* @see `ResolutionCalculations.Output`
|
||||
* @param obj the entity to be damaged
|
||||
* @param applyDamageTo the function that applies the damage to the target in a target-tailored fashion
|
||||
*/
|
||||
def PerformDamageIfVulnerable(obj: Damageable.Target, applyDamageTo: ResolutionCalculations.Output): Unit = {
|
||||
if (isVulnerable && obj.CanDamage) {
|
||||
PerformDamage(obj, applyDamageTo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -67,6 +94,20 @@ object Damageable {
|
|||
*/
|
||||
final val LogChannel: String = "DamageResolution"
|
||||
|
||||
trait PersonalVulnerability
|
||||
|
||||
final case object MakeVulnerable extends PersonalVulnerability
|
||||
|
||||
final case object MakeInvulnerable extends PersonalVulnerability
|
||||
|
||||
def Vulnerability(state: Boolean): PersonalVulnerability = {
|
||||
if (state) {
|
||||
MakeInvulnerable
|
||||
} else {
|
||||
MakeVulnerable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the possibility exist that the designated target can be affected by this projectile's damage?
|
||||
* @see `Hackable`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,518 @@
|
|||
// Copyright (c) 2025 PSForever
|
||||
package net.psforever.objects.serverobject.dome
|
||||
|
||||
import net.psforever.actors.zone.BuildingActor
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.PlanetSideGameObject
|
||||
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
|
||||
import net.psforever.objects.serverobject.structures.{Amenity, Building, PoweredAmenityControl}
|
||||
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
|
||||
import net.psforever.objects.sourcing.SourceEntry
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.etc.ForceDomeExposure
|
||||
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||
import net.psforever.objects.vital.prop.DamageWithPosition
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game.ChatMsg
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGeneratorState, Vector3}
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
object ForceDomeControl {
|
||||
trait Command
|
||||
|
||||
final case object CustomExpand extends Command
|
||||
|
||||
final case object CustomCollapse extends Command
|
||||
|
||||
final case object NormalBehavior extends Command
|
||||
|
||||
final case object ApplyProtection extends Command
|
||||
|
||||
final case object RemoveProtection extends Command
|
||||
|
||||
final case object Purge extends Command
|
||||
|
||||
/**
|
||||
* Dispatch a message to update the state of the clients with the server state of the capitol force dome.
|
||||
* @param dome force dome
|
||||
* @param activationState new force dome status
|
||||
*/
|
||||
def ChangeDomeEnergizedState(dome: ForceDomePhysics, activationState: Boolean): Unit = {
|
||||
dome.Energized = activationState
|
||||
val owner = dome.Owner
|
||||
val zone = owner.Zone
|
||||
owner.Actor ! BuildingActor.AmenityStateChange(dome)
|
||||
zone.LocalEvents ! LocalServiceMessage(
|
||||
zone.id,
|
||||
LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, owner.GUID, activationState)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* If this building is a capitol major facility,
|
||||
* use the faction affinity, the generator status, and the resource silo's capacitance level
|
||||
* to determine if the capitol force dome should be active.
|
||||
* @param building building being evaluated
|
||||
* @param dome force dome
|
||||
* @return the condition of the capitol force dome;
|
||||
* `None`, if the facility is not a capitol building;
|
||||
* `Some(true|false)` to indicate the state of the force dome
|
||||
*/
|
||||
def CheckForceDomeStatus(building: Building, dome: ForceDomePhysics): Option[Boolean] = {
|
||||
if (building.IsCapitol) {
|
||||
Some(
|
||||
if (InvalidBuildingCapitolForceDomeConditions(building)) {
|
||||
false
|
||||
} else {
|
||||
building
|
||||
.Neighbours(building.Faction)
|
||||
.map(_.count(b => !InvalidBuildingCapitolForceDomeConditions(b)))
|
||||
.exists(_ > 1)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The natural conditions of a facility that is not eligible for its capitol force dome to be expanded.
|
||||
* The only test not employed is whether or not the target building is a capitol.
|
||||
* Omission of this condition makes this test capable of evaluating subcapitol eligibility
|
||||
* for capitol force dome expansion.
|
||||
* @param building target building
|
||||
* @return `true`, if the conditions for capitol force dome are not met;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def InvalidBuildingCapitolForceDomeConditions(building: Building): Boolean = {
|
||||
building.Faction == PlanetSideEmpire.NEUTRAL ||
|
||||
building.NtuLevel == 0 ||
|
||||
building.Generator.exists(_.Condition == PlanetSideGeneratorState.Destroyed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a fixed point and a rotation value to a series of vertex offsets,
|
||||
* then daisy-chain the resulting vertices in such a way that
|
||||
* it creates a perimeter around the (building) owner of the capitol force dome.
|
||||
* The resulting capitol force dome barrier is a blocky pyramoid shape.
|
||||
* @param dome force dome
|
||||
* @return perimeter of the force dome barrier
|
||||
*/
|
||||
def SetupForceDomePerimeter(dome: ForceDomePhysics): List[(Vector3, Vector3)] = {
|
||||
val center = dome.Position.xy
|
||||
val rotation = math.toRadians(dome.Owner.Orientation.z).toFloat
|
||||
val perimeterOffsets = dome.Definition.PerimeterOffsets
|
||||
val perimeterPoints = perimeterOffsets.map {
|
||||
center + Vector3.PlanarRotateAroundPoint(_, Vector3(0, 0, 1), rotation)
|
||||
}
|
||||
((0 until perimeterPoints.size - 1).map { index =>
|
||||
(perimeterPoints(index), perimeterPoints(index + 1))
|
||||
} :+ (perimeterPoints.last, perimeterPoints.head)).toList
|
||||
}
|
||||
|
||||
/**
|
||||
* The capitol force dome should have changed states but it will not!
|
||||
* Make certain everyone knows!
|
||||
* @param building target building
|
||||
* @param state whether the force dome is energized or not
|
||||
*/
|
||||
def CustomDomeStateEnforcedMessage(
|
||||
building: Building,
|
||||
state: Boolean
|
||||
): Unit = {
|
||||
val zone = building.Zone
|
||||
val message = LocalAction.SendResponse(ChatMsg(
|
||||
ChatMessageType.UNK_229,
|
||||
s"The Capitol force dome at ${building.Name} will remain ${if (state) "activated" else "deactivated"}."
|
||||
))
|
||||
zone.LocalEvents ! LocalServiceMessage(zone.id, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* The capitol force dome will start changing states normally.
|
||||
* Make certain everyone knows.
|
||||
* @param building facility
|
||||
*/
|
||||
def NormalDomeStateMessage(building: Building): Unit = {
|
||||
val events = building.Zone.LocalEvents
|
||||
val message = LocalAction.SendResponse(ChatMsg(
|
||||
ChatMessageType.UNK_227,
|
||||
"Expected capitol force dome state change will resume."
|
||||
))
|
||||
building.PlayersInSOI.foreach { player =>
|
||||
events ! LocalServiceMessage(player.Name, message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate the conditions of the building
|
||||
* and determine if its capitol force dome state should be updated
|
||||
* to reflect the actual conditions of the base or its surrounding bases.
|
||||
* If this building is considered a subcapitol facility to the zone's actual capitol facility,
|
||||
* and has the capitol force dome has a dependency upon it,
|
||||
* pass a message onto that facility that it should check its own state alignment.
|
||||
* @param building facility with `dome`
|
||||
* @param dome force dome
|
||||
* @return current state of the capitol force dome
|
||||
*/
|
||||
def AlignForceDomeStatusAndUpdate(building: Building, dome: ForceDomePhysics): Boolean = {
|
||||
val energizedState = dome.Energized
|
||||
CheckForceDomeStatus(building, dome).exists {
|
||||
case true if !energizedState =>
|
||||
ChangeDomeEnergizedState(dome, activationState = true)
|
||||
dome.Owner.Actor ! BuildingActor.MapUpdate()
|
||||
true
|
||||
case false if energizedState =>
|
||||
ChangeDomeEnergizedState(dome, activationState = false)
|
||||
dome.Owner.Actor ! BuildingActor.MapUpdate()
|
||||
false
|
||||
case _ =>
|
||||
energizedState
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate the conditions of the building
|
||||
* and determine if its capitol force dome state should be updated
|
||||
* to reflect the actual conditions of the base or its surrounding bases.
|
||||
* If this building is considered a subcapitol facility to the zone's actual capitol facility,
|
||||
* and has the capitol force dome has a dependency upon it,
|
||||
* pass a message onto that facility that it should check its own state alignment.
|
||||
* @param building facility with `dome`
|
||||
* @param dome force dome
|
||||
* @return current state of the capitol force dome
|
||||
*/
|
||||
private def AlignForceDomeStatus(building: Building, dome: ForceDomePhysics): Boolean = {
|
||||
val energizedState = dome.Energized
|
||||
CheckForceDomeStatus(building, dome).exists {
|
||||
case true if !energizedState =>
|
||||
ChangeDomeEnergizedState(dome, activationState = true)
|
||||
true
|
||||
case false if energizedState =>
|
||||
ChangeDomeEnergizedState(dome, activationState = false)
|
||||
false
|
||||
case _ =>
|
||||
energizedState
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Being too close to the force dome can destroy targets if they do not match the faction alignment of the dome.
|
||||
* This is the usual fate of opponents upon it being expanded (energized).
|
||||
* @see `Zone.serverSideDamage`
|
||||
* @param dome force dome
|
||||
* @param perimeter ground-level perimeter of the force dome is defined by these segments (as vertex pairs)
|
||||
* @return list of affected entities
|
||||
*/
|
||||
def ForceDomeKills(dome: ForceDomePhysics, perimeter: List[(Vector3, Vector3)]): List[PlanetSideServerObject] = {
|
||||
Zone.serverSideDamage(
|
||||
dome.Zone,
|
||||
dome,
|
||||
ForceDomeExposure.damageProperties,
|
||||
makesContactWithForceDome,
|
||||
TargetUnderForceDome(perimeter),
|
||||
forceDomeTargets(dome.Definition.UseRadius, dome.Faction)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare damage information related to being caugt underneath the capitol force dome when it expands.
|
||||
* @param source a game object that represents the source of the explosion
|
||||
* @param target a game object that is affected by the explosion
|
||||
* @return a `DamageInteraction` object
|
||||
*/
|
||||
private def makesContactWithForceDome(
|
||||
source: PlanetSideGameObject with FactionAffinity with Vitality,
|
||||
target: PlanetSideGameObject with FactionAffinity with Vitality
|
||||
): DamageInteraction = {
|
||||
DamageInteraction(
|
||||
SourceEntry(target),
|
||||
ForceDomeExposure(SourceEntry(source)),
|
||||
target.Position
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* To be considered within a force dome, a target entity must satisfy two orientations
|
||||
* where the second condition is one of two qualifications:
|
||||
* 1. within an angular perimeter boundary, and
|
||||
* 2a. below the base coordinate of the force dome or
|
||||
* 2b. within a region above the facility encapsulated by the force dome
|
||||
* @param segments ground-level perimeter of the force dome is defined by these segments (as vertex pairs)
|
||||
* @param obj1 a game entity, should be the force dome
|
||||
* @param obj2 a game entity, should be a damageable target of the force dome's wrath
|
||||
* @param maxDistance not applicable
|
||||
* @return `true`, if target is detected within the force dome kill region
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def TargetUnderForceDome(
|
||||
segments: List[(Vector3, Vector3)]
|
||||
)
|
||||
(
|
||||
obj1: PlanetSideGameObject,
|
||||
obj2: PlanetSideGameObject,
|
||||
@unused maxDistance: Float
|
||||
): Boolean = {
|
||||
val Vector3(centerX, centerY, centerZ) = obj1.Position
|
||||
val Vector3(targetX, targetY, targetZ) = obj2.Position
|
||||
lazy val insideOfThePerimeter = !segments.exists { case (point1, point2) =>
|
||||
//want targets within the perimeter; if there's any intersection, target is outside of the perimeter
|
||||
segmentIntersectionTestPerSegment(centerX, centerY, targetX, targetY, point1.x, point1.y, point2.x, point2.y)
|
||||
}
|
||||
segments.nonEmpty && insideOfThePerimeter && (targetZ <= centerZ || underForceDomeCeiling(obj1, obj2, targetZ))
|
||||
}
|
||||
|
||||
/**
|
||||
* The force dome is the top portion of an irregular prism.
|
||||
* It's walls are similarly angled inwards, usually.
|
||||
* There's no way to properly represent this without too much work.
|
||||
* Just ensure the target is either below the dome or within a circular region
|
||||
* defined by the base of the generator to the top of the extended stalk as the measurement of a hemisphere.
|
||||
* @see `Zone.distanceCheck`
|
||||
* @param obj1 a game entity, should be the force dome
|
||||
* @param obj2 a game entity, should be a damageable target of the force dome's wrath
|
||||
* @param height target height that must be under the ceiling
|
||||
* @return `true`, if target is detected within the force dome kill region
|
||||
* `false`, otherwise
|
||||
*/
|
||||
private def underForceDomeCeiling(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, height: Float): Boolean = {
|
||||
obj1 match {
|
||||
case dome: ForceDomePhysics =>
|
||||
val generatorHeightOffsetZ = dome.Definition.GeneratorOffset.z
|
||||
height < dome.Owner.Position.z + generatorHeightOffsetZ * 1.5f || Zone.distanceCheck(obj1, obj2, generatorHeightOffsetZ + 42f)
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to assist line segment intersection tests.
|
||||
* The important frame of reference is checking whether a hypothetical segment between a point and a target
|
||||
* intersects with an established line segment between two other points.
|
||||
* For our purposes, the resulting line segments will never be collinear, so there is no reason to test that.
|
||||
* @param pointX x-coordinate used to create a test segment
|
||||
* @param pointY y-coordinate used to create a test segment
|
||||
* @param targetX x-coordinate of an important point for a test segment
|
||||
* @param targetY y-coordinate of an important point for a test segment
|
||||
* @param segmentPoint1x x-coordinate of one point from a segment
|
||||
* @param segmentPoint1y y-coordinate of one point from a segment
|
||||
* @param segmentPoint2x x-coordinate of a different point from a segment
|
||||
* @param segmentPoint2y y-coordinate of a different point from a segment
|
||||
* @return `true`, if the points form into two segments that intersect;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
private def segmentIntersectionTestPerSegment(
|
||||
pointX: Float,
|
||||
pointY: Float,
|
||||
targetX: Float,
|
||||
targetY: Float,
|
||||
segmentPoint1x: Float,
|
||||
segmentPoint1y: Float,
|
||||
segmentPoint2x: Float,
|
||||
segmentPoint2y: Float
|
||||
): Boolean = {
|
||||
//based on Franklin Antonio's "Faster Line Segment Intersection" topic "in Graphics Gems III" book (http://www.graphicsgems.org/)
|
||||
//compare, java.awt.geom.Line2D.linesIntersect
|
||||
val ax = targetX - pointX //delta-x of segment from center to target
|
||||
val ay = targetY - pointY //delta-y of segment from center to target
|
||||
val bx = segmentPoint1x - segmentPoint2x //delta-x of test segment
|
||||
val by = segmentPoint1y - segmentPoint2y //delta-y of test segment
|
||||
val cx = pointX - segmentPoint1x //delta-x of hypotenuse of triangle formed by center, test segment endpoint, and intersection point
|
||||
val cy = pointY - segmentPoint1y //delta-y of hypotenuse of triangle formed by center, test segment endpoint, and intersection point
|
||||
val alphaNumerator = by * cx - bx * cy //cross product of matrix [(by, bx)(cy, cx)]
|
||||
val denominator = ay * bx - ax * by //cross product of matrix [(ay, ax)(cy, cx)]
|
||||
val betaNumerator = ax * cy - ay * cx //cross product of matrix [(ax, ay)(cx, cy)]
|
||||
if (
|
||||
denominator > 0 &&
|
||||
(alphaNumerator < 0 || alphaNumerator > denominator || betaNumerator < 0 || betaNumerator > denominator)
|
||||
) {
|
||||
false
|
||||
} else if (
|
||||
denominator < 0 &&
|
||||
(alphaNumerator > 0 || alphaNumerator < denominator || betaNumerator > 0 || betaNumerator < denominator)
|
||||
) {
|
||||
false
|
||||
} else {
|
||||
//a collinear line test could go here, but we don't need it
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all enemy players, vehicles, and combat engineering deployables in a sector.
|
||||
* @see `DamageWithPosition`
|
||||
* @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
|
||||
* @return a list of affected entities
|
||||
*/
|
||||
private def forceDomeTargets(
|
||||
radius: Float,
|
||||
targetFaction: PlanetSideEmpire.Value
|
||||
)
|
||||
(
|
||||
zone: Zone,
|
||||
source: PlanetSideGameObject with Vitality,
|
||||
damagePropertiesBySource: DamageWithPosition
|
||||
): List[PlanetSideServerObject with Vitality] = {
|
||||
val sector = zone.blockMap.sector(source.Position.xy, radius)
|
||||
val playerTargets = sector.livePlayerList.filterNot { _.VehicleSeated.nonEmpty }
|
||||
//vehicles
|
||||
val vehicleTargets = sector.vehicleList.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
|
||||
//deployables
|
||||
val deployableTargets = sector.deployableList.filterNot { _.Destroyed }
|
||||
//altogether ...
|
||||
(playerTargets ++ vehicleTargets ++ deployableTargets).filterNot(_.Faction == targetFaction)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An `Actor` that handles messages being dispatched to a specific capitol facility's force dome.
|
||||
* @param dome the `ForceDomePhysics` object being governed
|
||||
*/
|
||||
class ForceDomeControl(dome: ForceDomePhysics)
|
||||
extends PoweredAmenityControl
|
||||
with CaptureTerminalAwareBehavior
|
||||
with FactionAffinityBehavior.Check {
|
||||
def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware = dome
|
||||
def FactionObject: FactionAffinity = dome
|
||||
|
||||
/** a capitol force dome's owner should always be a facility;
|
||||
* to save time, cast this entity and cache it for repeated use once;
|
||||
* force dome is not immediately owned by its correct facility so delay determination */
|
||||
private lazy val domeOwnerAsABuilding = dome.Owner.asInstanceOf[Building]
|
||||
/** ground-level perimeter of the force dome is defined by these segments (as vertex pairs) */
|
||||
private lazy val perimeterSegments: List[(Vector3, Vector3)] = ForceDomeControl.SetupForceDomePerimeter(dome)
|
||||
/** force the dome into a certain state regardless of what conditions would normally transition it into that state */
|
||||
private var customState: Option[Boolean] = None
|
||||
|
||||
def commonBehavior: Receive = checkBehavior
|
||||
.orElse {
|
||||
case ForceDomeControl.CustomExpand
|
||||
if !dome.Energized && (customState.isEmpty || customState.contains(false)) =>
|
||||
customState = Some(true)
|
||||
ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = true)
|
||||
ForceDomeControl.ChangeDomeEnergizedState(dome, activationState = true)
|
||||
|
||||
case ForceDomeControl.CustomExpand
|
||||
if customState.isEmpty =>
|
||||
customState = Some(true)
|
||||
ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = true)
|
||||
|
||||
case ForceDomeControl.CustomCollapse
|
||||
if dome.Energized && (customState.isEmpty || customState.contains(true)) =>
|
||||
customState = Some(false)
|
||||
ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = false)
|
||||
ForceDomeControl.ChangeDomeEnergizedState(dome, activationState = false)
|
||||
|
||||
case ForceDomeControl.CustomCollapse
|
||||
if customState.isEmpty =>
|
||||
customState = Some(false)
|
||||
ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = false)
|
||||
|
||||
case ForceDomeControl.NormalBehavior
|
||||
if customState.nonEmpty =>
|
||||
customState = None
|
||||
ForceDomeControl.NormalDomeStateMessage(domeOwnerAsABuilding)
|
||||
if (!blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatusAndUpdate)) {
|
||||
ForceDomeControl.ForceDomeKills(dome, perimeterSegments)
|
||||
}
|
||||
|
||||
case ForceDomeControl.ApplyProtection
|
||||
if dome.Energized =>
|
||||
dome.Perimeter = perimeterSegments
|
||||
dome.Owner.Actor ! BuildingActor.AmenityStateChange(dome)
|
||||
|
||||
case ForceDomeControl.RemoveProtection =>
|
||||
dome.Perimeter = List.empty
|
||||
dome.Owner.Actor ! BuildingActor.AmenityStateChange(dome)
|
||||
|
||||
case ForceDomeControl.Purge =>
|
||||
ForceDomeControl.ForceDomeKills(dome, perimeterSegments)
|
||||
}
|
||||
|
||||
def poweredStateLogic: Receive = {
|
||||
commonBehavior
|
||||
.orElse(captureTerminalAwareBehaviour)
|
||||
.orElse {
|
||||
case BuildingActor.AlertToFactionChange(_) =>
|
||||
blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatusAndUpdate)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
def unpoweredStateLogic: Receive = {
|
||||
commonBehavior
|
||||
.orElse {
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
def powerTurnOffCallback() : Unit = {
|
||||
deenergizeUnlessSuppressedDueToCustomState()
|
||||
}
|
||||
|
||||
def powerTurnOnCallback() : Unit = {
|
||||
blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatus)
|
||||
}
|
||||
|
||||
override protected def captureTerminalIsResecured(terminal: CaptureTerminal): Unit = {
|
||||
super.captureTerminalIsResecured(terminal)
|
||||
blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatus)
|
||||
}
|
||||
|
||||
override protected def captureTerminalIsHacked(terminal: CaptureTerminal): Unit = {
|
||||
super.captureTerminalIsHacked(terminal)
|
||||
deenergizeUnlessSuppressedDueToCustomState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Power down the force dome if it was previously being powered and
|
||||
* as long as a custom state of being energized is not being enforced.
|
||||
*/
|
||||
private def deenergizeUnlessSuppressedDueToCustomState(): Unit = {
|
||||
if (dome.Energized) {
|
||||
if (customState.isEmpty) {
|
||||
ForceDomeControl.ChangeDomeEnergizedState(dome, activationState = false)
|
||||
} else {
|
||||
ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Yield to a custom value enforcing a certain force dome state - energized or powered down.
|
||||
* If the custom state is not declared, run the function and analyze any change in the force dome's natural state.
|
||||
* Apply changes to region represented as "bound" by the perimeter as indicated by a state change.
|
||||
* @param func function to run if not blocked
|
||||
* @return current energized state of the dome
|
||||
*/
|
||||
private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Boolean): Boolean = {
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
customState match {
|
||||
case None =>
|
||||
val oldState = dome.Energized
|
||||
val newState = func(domeOwnerAsABuilding, dome)
|
||||
if (!oldState && newState) {
|
||||
//dome activating
|
||||
context.system.scheduler.scheduleOnce(delay = 1500 milliseconds, self, ForceDomeControl.Purge)
|
||||
context.system.scheduler.scheduleOnce(delay = 4000 milliseconds, self, ForceDomeControl.ApplyProtection)
|
||||
} else if (oldState && !newState) {
|
||||
context.system.scheduler.scheduleOnce(delay = 1500 milliseconds, self, ForceDomeControl.RemoveProtection)
|
||||
}
|
||||
newState
|
||||
case Some(state)
|
||||
if !ForceDomeControl.CheckForceDomeStatus(domeOwnerAsABuilding, dome).contains(state) =>
|
||||
ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state)
|
||||
state
|
||||
case Some(state) =>
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) 2025 PSForever
|
||||
package net.psforever.objects.serverobject.dome
|
||||
|
||||
import net.psforever.objects.geometry.d3.{Sphere, VolumetricGeometry}
|
||||
import net.psforever.objects.serverobject.structures.AmenityDefinition
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
class ForceDomeDefinition(objectId: Int)
|
||||
extends AmenityDefinition(objectId) {
|
||||
Name = "force_dome"
|
||||
Geometry = ForceDomeDefinition.representBy
|
||||
|
||||
/** offsets that define the perimeter of the pyramidal force "dome" barrier;
|
||||
* these points are the closest to where the dome interacts with the ground at a corner;
|
||||
* should be sequential, either clockwise or counterclockwise */
|
||||
private var perimeter: List[Vector3] = List()
|
||||
/** offset of the physical location of the force dome generator stalk */
|
||||
private var genOffset: Vector3 = Vector3.Zero
|
||||
|
||||
def PerimeterOffsets: List[Vector3] = perimeter
|
||||
|
||||
def PerimeterOffsets_=(points: List[Vector3]): List[Vector3] = {
|
||||
perimeter = points
|
||||
PerimeterOffsets
|
||||
}
|
||||
|
||||
private var protects: List[AmenityDefinition] = List()
|
||||
|
||||
def ApplyProtectionTo: List[AmenityDefinition] = protects
|
||||
|
||||
def ApplyProtectionTo_=(protect: AmenityDefinition): List[AmenityDefinition] = {
|
||||
ApplyProtectionTo_=(List(protect))
|
||||
}
|
||||
|
||||
def ApplyProtectionTo_=(protect: List[AmenityDefinition]): List[AmenityDefinition] = {
|
||||
protects = protect
|
||||
ApplyProtectionTo
|
||||
}
|
||||
|
||||
def GeneratorOffset: Vector3 = genOffset
|
||||
|
||||
def GeneratorOffset_=(offset: Vector3): Vector3 = {
|
||||
genOffset = offset
|
||||
GeneratorOffset
|
||||
}
|
||||
}
|
||||
|
||||
object ForceDomeDefinition {
|
||||
/**
|
||||
* Transform a capitol force dome into a bounded geometric representation.
|
||||
* @param o any entity from which to produce a geometric representation
|
||||
* @return geometric representation
|
||||
*/
|
||||
def representBy(o: Any): VolumetricGeometry = {
|
||||
o match {
|
||||
case fdp: ForceDomePhysics =>
|
||||
Sphere(fdp.Position, fdp.Definition.UseRadius)
|
||||
case _ =>
|
||||
net.psforever.objects.geometry.GeometryForm.invalidPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) 2025 PSForever
|
||||
package net.psforever.objects.serverobject.dome
|
||||
|
||||
import net.psforever.objects.serverobject.structures.Amenity
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
class ForceDomePhysics(private val cfddef: ForceDomeDefinition)
|
||||
extends Amenity
|
||||
with CaptureTerminalAware {
|
||||
/** whether the dome is active or not */
|
||||
private var energized: Boolean = false
|
||||
/** defined perimeter of this force dome on the floor;
|
||||
* the walls created by this perimeter are angled inwards towards the facility vertically, but that's not a consideration here */
|
||||
private var perimeter: List[(Vector3, Vector3)] = List()
|
||||
|
||||
override def Position: Vector3 = Owner.Position
|
||||
|
||||
override def Position_=(vec: Vector3): Vector3 = Owner.Position
|
||||
|
||||
override def Orientation: Vector3 = Owner.Orientation
|
||||
|
||||
override def Orientation_=(vec: Vector3): Vector3 = Owner.Orientation
|
||||
|
||||
def Energized: Boolean = energized
|
||||
|
||||
def Energized_=(state: Boolean): Boolean = {
|
||||
energized = state
|
||||
Energized
|
||||
}
|
||||
|
||||
def Perimeter: List[(Vector3, Vector3)] = perimeter
|
||||
|
||||
def Perimeter_=(list: List[(Vector3, Vector3)]): List[(Vector3, Vector3)] = {
|
||||
perimeter = list
|
||||
Perimeter
|
||||
}
|
||||
|
||||
def Definition: ForceDomeDefinition = cfddef
|
||||
}
|
||||
|
||||
object ForceDomePhysics {
|
||||
import akka.actor.ActorContext
|
||||
|
||||
/**
|
||||
* Instantiate and configure a `CapitolForceDome` object.
|
||||
* @param fddef specific type of force dome
|
||||
* @param id the unique id that will be assigned to this entity
|
||||
* @param context a context to allow the object to properly set up `ActorSystem` functionality
|
||||
* @return the `CapitolForceDome` object
|
||||
*/
|
||||
def Constructor(fddef: ForceDomeDefinition)(id: Int, context: ActorContext): ForceDomePhysics = {
|
||||
import akka.actor.Props
|
||||
|
||||
val obj = new ForceDomePhysics(fddef)
|
||||
obj.Actor = context.actorOf(Props(classOf[ForceDomeControl], obj), name = s"${fddef.Name}_$id")
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
|
@ -122,7 +122,7 @@ class GeneratorControl(gen: Generator)
|
|||
queuedExplosion = Default.Cancellable
|
||||
imminentExplosion = false
|
||||
//hate on everything nearby
|
||||
Zone.serverSideDamage(gen.Zone, gen, Zone.explosionDamage(gen.LastDamage), explosionFunc)
|
||||
Zone.serverSideDamage(gen.Zone, gen, Zone.explosionDamage(gen.LastDamage), explosionFunc, Zone.findAllTargets)
|
||||
|
||||
case GeneratorControl.Restored() =>
|
||||
gen.ClearHistory()
|
||||
|
|
|
|||
|
|
@ -2,38 +2,25 @@
|
|||
package net.psforever.objects.serverobject.hackable
|
||||
|
||||
import net.psforever.actors.zone.BuildingActor
|
||||
import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate}
|
||||
import net.psforever.objects.serverobject.dome.ForceDomeControl
|
||||
import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate}
|
||||
import net.psforever.objects.serverobject.terminals.Terminal
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
|
||||
import net.psforever.objects.{GlobalDefinitions, Player, Vehicle}
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
import net.psforever.packet.game.{GenericObjectActionMessage, HackMessage, HackState, HackState1, HackState7, TriggeredSound}
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
|
||||
import scala.annotation.unused
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object GenericHackables {
|
||||
private val log = org.log4s.getLogger("HackableBehavior")
|
||||
private var turretUpgradeTime: Long = System.currentTimeMillis()
|
||||
private var turretUpgradeTimeSet: Boolean = false
|
||||
|
||||
def updateTurretUpgradeTime(): Long = {
|
||||
turretUpgradeTime = System.currentTimeMillis()
|
||||
turretUpgradeTimeSet = true
|
||||
turretUpgradeTime
|
||||
}
|
||||
|
||||
// Used for checking the time without updating it
|
||||
def getTurretUpgradeTime: Long = {
|
||||
if (!turretUpgradeTimeSet) {
|
||||
turretUpgradeTime = System.currentTimeMillis()
|
||||
turretUpgradeTimeSet = true
|
||||
}
|
||||
turretUpgradeTime
|
||||
}
|
||||
/**
|
||||
* na
|
||||
*
|
||||
|
|
@ -63,6 +50,8 @@ object GenericHackables {
|
|||
}
|
||||
}
|
||||
|
||||
private def DontStopHackAttempt(@unused target: PlanetSideServerObject, @unused hacker: Player): Boolean = false
|
||||
|
||||
/**
|
||||
* Evaluate the progress of the user applying a tool to modify some server object.
|
||||
* This action is using the remote electronics kit to convert an enemy unit into an allied unit, primarily.
|
||||
|
|
@ -76,10 +65,17 @@ object GenericHackables {
|
|||
* @param target the object being affected
|
||||
* @param tool_guid the tool being used to affest the object
|
||||
* @param progress the current progress value
|
||||
* @param additionalCancellationTests context-specific tests for hack continuation
|
||||
* @return `true`, if the next cycle of progress should occur;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def HackingTickAction(progressType: HackState1, hacker: Player, target: PlanetSideServerObject, tool_guid: PlanetSideGUID)(
|
||||
def HackingTickAction(
|
||||
progressType: HackState1,
|
||||
hacker: Player,
|
||||
target: PlanetSideServerObject,
|
||||
tool_guid: PlanetSideGUID,
|
||||
additionalCancellationTests: (PlanetSideServerObject, Player) => Boolean
|
||||
)(
|
||||
progress: Float
|
||||
): Boolean = {
|
||||
//hack state for progress bar visibility
|
||||
|
|
@ -87,9 +83,7 @@ object GenericHackables {
|
|||
(HackState.Start, 0)
|
||||
} else if (progress >= 100L) {
|
||||
(HackState.Finished, 100)
|
||||
} else if (target.isMoving(test = 1f) || target.Destroyed || !target.HasGUID) {
|
||||
(HackState.Cancelled, 0)
|
||||
} else if (target.isInstanceOf[CaptureTerminal] && EndHackProgress(target, hacker)) {
|
||||
} else if (target.isMoving(test = 1f) || target.Destroyed || !target.HasGUID || additionalCancellationTests(target, hacker)) {
|
||||
(HackState.Cancelled, 0)
|
||||
} else {
|
||||
(HackState.Ongoing, progress.toInt)
|
||||
|
|
@ -103,6 +97,55 @@ object GenericHackables {
|
|||
)
|
||||
progressState != HackState.Cancelled
|
||||
}
|
||||
/**
|
||||
* Evaluate the progress of the user applying a tool to modify some server object.
|
||||
* This action is using the remote electronics kit to convert an enemy unit into an allied unit, primarily.
|
||||
* The act of transforming allied units of one kind into allied units of another kind (facility turret upgrades)
|
||||
* is also governed by this action per tick of progress.
|
||||
* @param progressType 1 - remote electronics kit hack (various ...);
|
||||
* 2 - nano dispenser (upgrade canister) turret upgrade
|
||||
* @param hacker the player performing the action
|
||||
* @param target the object being affected
|
||||
* @param tool_guid the tool being used to affest the object
|
||||
* @param progress the current progress value
|
||||
* @return `true`, if the next cycle of progress should occur;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def HackingTickAction(
|
||||
progressType: HackState1,
|
||||
hacker: Player,
|
||||
target: PlanetSideServerObject,
|
||||
tool_guid: PlanetSideGUID
|
||||
)(
|
||||
progress: Float
|
||||
): Boolean = {
|
||||
HackingTickAction(progressType, hacker, target, tool_guid, DontStopHackAttempt)(progress)
|
||||
}
|
||||
|
||||
/**
|
||||
* The force dome prevents hacking if its protection has been declared over a capitol.
|
||||
* Under normal circumstances, the dome will be visible in the sky at his point,
|
||||
* blocking enemy encounter within its boundaries,
|
||||
* so anything that can be hacked is on that boundary perimeter,
|
||||
* or an alternate method of entry (Router) has been compromised.
|
||||
* @see `ForceDomeControl.TargetUnderForceDome`
|
||||
* @see `Sector`
|
||||
* @param target the `Hackable` object that has been hacked
|
||||
* @param hacker the player performing the action
|
||||
* @return `true`, if the target is within boundary of a working force dome and thus protected;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def ForceDomeProtectsFromHacking(target: PlanetSideServerObject, hacker: Player): Boolean = {
|
||||
//explicitly allow friendly hacking which is typically clearing a hack
|
||||
target.Faction != hacker.Faction &&
|
||||
(target match {
|
||||
case obj: Amenity => obj.Owner.asInstanceOf[Building].ForceDome.toList
|
||||
case obj: BlockMapEntity => target.Zone.blockMap.sector(obj).buildingList.flatMap(_.ForceDome)
|
||||
case _ => List()
|
||||
})
|
||||
.filter(_.Perimeter.nonEmpty)
|
||||
.exists(dome => ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(dome, target, maxDistance = 0f))
|
||||
}
|
||||
|
||||
/**
|
||||
* The process of hacking an object is completed.
|
||||
|
|
|
|||
|
|
@ -34,6 +34,17 @@ trait Mountable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All the seats that have occupants by their seat number.
|
||||
* @return list of the numbers of all occupied seats
|
||||
*/
|
||||
def OccupiedSeats(): List[Int] = {
|
||||
seats
|
||||
.collect { case (index, seat) if seat.isOccupied => index }
|
||||
.toList
|
||||
.sorted
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a mapping of each mount from its mount point index.
|
||||
* @return the mapping of mount point to mount
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import net.psforever.objects.serverobject.PlanetSideServerObject
|
|||
import net.psforever.objects.serverobject.hackable.Hackable
|
||||
import net.psforever.types.BailType
|
||||
|
||||
import scala.annotation.unused
|
||||
import scala.collection.mutable
|
||||
|
||||
trait MountableBehavior {
|
||||
|
|
@ -45,13 +46,17 @@ 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)
|
||||
mountActionResponse(user, mount_point, seatNum)
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seatNum, mount_point))
|
||||
case _ =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, mount_point))
|
||||
}
|
||||
}
|
||||
|
||||
def mountActionResponse(user: Player, @unused mountPoint: Int, @unused seatIndex: Int): Unit = {
|
||||
MountableObject.Zone.actor ! ZoneActor.RemoveFromBlockMap(user)
|
||||
}
|
||||
|
||||
protected def mountTest(
|
||||
obj: PlanetSideServerObject with Mountable,
|
||||
seatNumber: Int,
|
||||
|
|
@ -87,7 +92,7 @@ trait MountableBehavior {
|
|||
val obj = MountableObject
|
||||
if (dismountTest(obj, seat_number, user) && tryDismount(obj, seat_number, user, bail_type)) {
|
||||
user.VehicleSeated = None
|
||||
obj.Zone.actor ! ZoneActor.AddToBlockMap(user, obj.Position)
|
||||
dismountActionResponse(user, seat_number)
|
||||
sender() ! Mountable.MountMessages(
|
||||
user,
|
||||
Mountable.CanDismount(obj, seat_number, getUsedMountPoint(user.Name, seat_number))
|
||||
|
|
@ -98,6 +103,10 @@ trait MountableBehavior {
|
|||
}
|
||||
}
|
||||
|
||||
def dismountActionResponse(user: Player, @unused seatIndex: Int): Unit = {
|
||||
MountableObject.Zone.actor ! ZoneActor.AddToBlockMap(user, MountableObject.Position)
|
||||
}
|
||||
|
||||
protected def dismountTest(
|
||||
obj: Mountable with WorldEntity,
|
||||
seatNumber: Int,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) 2025 PSForever
|
||||
package net.psforever.objects.serverobject.mount.interaction
|
||||
|
||||
import net.psforever.objects.avatar.interaction.InteractWithForceDomeProtection
|
||||
import net.psforever.objects.serverobject.damage.Damageable
|
||||
import net.psforever.objects.serverobject.dome.ForceDomePhysics
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.zones.interaction.InteractsWithZone
|
||||
|
||||
class InteractWithForceDomeProtectionSeatedInEntity
|
||||
extends InteractWithForceDomeProtection {
|
||||
override def range: Float = 30f
|
||||
|
||||
override def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
|
||||
super.applyProtection(target, dome)
|
||||
target
|
||||
.asInstanceOf[Mountable]
|
||||
.Seats
|
||||
.values
|
||||
.flatMap(_.occupants)
|
||||
.foreach { occupant =>
|
||||
occupant.Actor ! Damageable.MakeInvulnerable
|
||||
}
|
||||
}
|
||||
|
||||
override def resetInteraction(target: InteractsWithZone): Unit = {
|
||||
super.resetInteraction(target)
|
||||
target
|
||||
.asInstanceOf[Mountable]
|
||||
.Seats
|
||||
.values
|
||||
.flatMap(_.occupants)
|
||||
.foreach { occupant =>
|
||||
occupant.Actor ! Damageable.MakeVulnerable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.objects.serverobject.mount
|
||||
package net.psforever.objects.serverobject.mount.interaction
|
||||
|
||||
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.sourcing.SourceEntry
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.base.DamageResolution
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package net.psforever.objects.serverobject.mount
|
||||
package net.psforever.objects.serverobject.mount.interaction
|
||||
|
||||
import net.psforever.objects.zones.interaction.ZoneInteractionType
|
||||
|
||||
|
|
@ -37,7 +37,8 @@ class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnCont
|
|||
pad.Zone,
|
||||
pad,
|
||||
VehicleSpawnControlRailJack.prepareSpawnExplosion(pad, SourceEntry(driver), SourceEntry(vehicle)),
|
||||
pad.Definition.killBox(pad, vehicle.Definition.CanFly)
|
||||
pad.Definition.killBox(pad, vehicle.Definition.CanFly),
|
||||
Zone.findAllTargets
|
||||
)
|
||||
pad.Zone.VehicleEvents ! VehicleSpawnPad.AttachToRails(vehicle, pad)
|
||||
context.system.scheduler.scheduleOnce(10 milliseconds, seatDriver, order)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ import net.psforever.util.Config
|
|||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object ResourceSiloControl {
|
||||
final case class DrainMultiplier(multiplier: Float)
|
||||
}
|
||||
|
||||
/**
|
||||
* An `Actor` that handles messages being dispatched to a specific `ResourceSilo` entity.
|
||||
*
|
||||
|
|
@ -30,7 +34,9 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
|
|||
def FactionObject: FactionAffinity = resourceSilo
|
||||
|
||||
private[this] val log = org.log4s.getLogger
|
||||
var panelAnimationFunc: (ActorRef, Float) => Unit = PanelAnimation
|
||||
private var panelAnimationFunc: (ActorRef, Float) => Unit = PanelAnimation
|
||||
/** the higher the multiplier, the greater the drain */
|
||||
private var drainMultiplier: Float = 1.0f
|
||||
|
||||
def receive: Receive = {
|
||||
case Service.Startup() =>
|
||||
|
|
@ -53,6 +59,9 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
|
|||
checkBehavior
|
||||
.orElse(storageBehavior)
|
||||
.orElse {
|
||||
case ResourceSiloControl.DrainMultiplier(multiplier) =>
|
||||
drainMultiplier = multiplier
|
||||
|
||||
case CommonMessages.Use(_, Some(vehicle: Vehicle))
|
||||
if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) =>
|
||||
val siloFaction = resourceSilo.Faction
|
||||
|
|
@ -171,7 +180,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
|
|||
*/
|
||||
def HandleNtuRequest(sender: ActorRef, min: Float, max: Float): Unit = {
|
||||
val originalAmount = resourceSilo.NtuCapacitor
|
||||
UpdateChargeLevel(-min)
|
||||
UpdateChargeLevel(-min * drainMultiplier)
|
||||
sender ! Ntu.Grant(resourceSilo, originalAmount - resourceSilo.NtuCapacitor)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import net.psforever.packet.game.{Additional3, BuildingInfoUpdateMessage, Densit
|
|||
import net.psforever.types._
|
||||
import scalax.collection.{Graph, GraphEdge}
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.objects.serverobject.dome.ForceDomePhysics
|
||||
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
|
||||
import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation}
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
|
||||
|
|
@ -32,7 +33,6 @@ class Building(
|
|||
|
||||
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
private var playersInSOI: List[Player] = List.empty
|
||||
private var forceDomeActive: Boolean = false
|
||||
private var participationFunc: ParticipationLogic = NoParticipation
|
||||
var virusId: Long = 8 // 8 default = no virus
|
||||
var virusInstalledBy: Option[Int] = None // faction id
|
||||
|
|
@ -59,11 +59,6 @@ class Building(
|
|||
case None => false
|
||||
}
|
||||
}
|
||||
def ForceDomeActive: Boolean = forceDomeActive
|
||||
def ForceDomeActive_=(activated: Boolean): Boolean = {
|
||||
forceDomeActive = activated
|
||||
forceDomeActive
|
||||
}
|
||||
|
||||
def Faction: PlanetSideEmpire.Value = faction
|
||||
|
||||
|
|
@ -108,6 +103,13 @@ class Building(
|
|||
}
|
||||
}
|
||||
|
||||
def ForceDome: Option[ForceDomePhysics] = {
|
||||
Amenities.find(_.isInstanceOf[ForceDomePhysics]) match {
|
||||
case Some(out: ForceDomePhysics) => Some(out)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
def NtuSource: Option[NtuContainer] = {
|
||||
Amenities.find(_.isInstanceOf[NtuContainer]) match {
|
||||
case Some(o: NtuContainer) => Some(o)
|
||||
|
|
@ -223,6 +225,7 @@ class Building(
|
|||
else {
|
||||
(virusId.toInt, Some(Additional3(inform_defenders=true, virusInstalledBy.getOrElse(3))))
|
||||
}
|
||||
val forceDomeActive = ForceDome.exists(_.Energized)
|
||||
|
||||
BuildingInfoUpdateMessage(
|
||||
Zone.Number,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class CaptureTerminalControl(terminal: CaptureTerminal)
|
|||
sender() ! CommonMessages.Progress(
|
||||
GenericHackables.GetHackSpeed(player, terminal),
|
||||
CaptureTerminals.FinishHackingCaptureConsole(terminal, player, unk = -1),
|
||||
GenericHackables.HackingTickAction(HackState1.Unk1, player, terminal, item.GUID)
|
||||
GenericHackables.HackingTickAction(HackState1.Unk1, player, terminal, item.GUID, CaptureTerminals.EndHackProgress)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.serverobject.terminals.capture
|
||||
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.serverobject.CommonMessages
|
||||
import net.psforever.objects.serverobject.hackable.GenericHackables
|
||||
import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate}
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object CaptureTerminals {import scala.concurrent.duration._
|
||||
object CaptureTerminals {
|
||||
private val log = org.log4s.getLogger("CaptureTerminals")
|
||||
|
||||
/**
|
||||
|
|
@ -55,4 +60,47 @@ object CaptureTerminals {import scala.concurrent.duration._
|
|||
log.warn(s"Hack message failed on target guid: ${target.GUID}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the state of connected facilities has changed since the hack progress began. It accounts for a friendly facility
|
||||
* on the other side of a warpgate as well in case there are no friendly facilities in the same zone
|
||||
* @param target the `Hackable` object that has been hacked
|
||||
* @param hacker the player performing the action
|
||||
* @return `true`, if the hack should be ended; `false`, otherwise
|
||||
*/
|
||||
def EndHackProgress(target: PlanetSideServerObject, hacker: Player): Boolean = {
|
||||
val building = target.asInstanceOf[CaptureTerminal].Owner.asInstanceOf[Building]
|
||||
val hackerFaction = hacker.Faction
|
||||
if (GenericHackables.ForceDomeProtectsFromHacking(target, hacker)) {
|
||||
true
|
||||
} else if (building.Faction == PlanetSideEmpire.NEUTRAL ||
|
||||
building.BuildingType == StructureType.Tower ||
|
||||
building.Faction == hackerFaction) {
|
||||
false
|
||||
} else {
|
||||
val stopHackingCount = building.Neighbours match {
|
||||
case Some(neighbors) =>
|
||||
neighbors.count {
|
||||
case wg: WarpGate if wg.Faction == hackerFaction =>
|
||||
true
|
||||
case wg: WarpGate =>
|
||||
val friendlyBaseOpt = for {
|
||||
otherWg <- wg.Neighbours.flatMap(_.find(_.isInstanceOf[WarpGate]))
|
||||
friendly <- otherWg.Neighbours.flatMap(_.collectFirst { case b: Building if !b.isInstanceOf[WarpGate] => b })
|
||||
} yield friendly
|
||||
friendlyBaseOpt.exists { fb =>
|
||||
fb.Faction == hackerFaction &&
|
||||
!fb.CaptureTerminalIsHacked &&
|
||||
fb.NtuLevel > 0
|
||||
}
|
||||
case b =>
|
||||
b.Faction == hackerFaction &&
|
||||
!b.CaptureTerminalIsHacked &&
|
||||
b.NtuLevel > 0
|
||||
}
|
||||
case None => 0
|
||||
}
|
||||
stopHackingCount == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
package net.psforever.objects.serverobject.terminals.implant
|
||||
|
||||
import net.psforever.objects.serverobject.hackable.Hackable
|
||||
import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, Mountable, Seat}
|
||||
import net.psforever.objects.serverobject.mount.interaction.InteractWithRadiationCloudsSeatedInEntity
|
||||
import net.psforever.objects.serverobject.mount.{Mountable, Seat}
|
||||
import net.psforever.objects.serverobject.structures.Amenity
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware
|
||||
import net.psforever.objects.vital.resistance.StandardResistanceProfile
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package net.psforever.objects.serverobject.turret
|
|||
|
||||
import net.psforever.objects.equipment.JammableUnit
|
||||
import net.psforever.objects.serverobject.interior.Sidedness
|
||||
import net.psforever.objects.serverobject.mount.InteractWithRadiationCloudsSeatedInEntity
|
||||
import net.psforever.objects.serverobject.mount.interaction.InteractWithRadiationCloudsSeatedInEntity
|
||||
import net.psforever.objects.serverobject.structures.{Amenity, AmenityOwner, Building}
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware
|
||||
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
|
||||
|
|
@ -23,6 +23,28 @@ class FacilityTurret(tDef: FacilityTurretDefinition)
|
|||
WeaponTurret.LoadDefinition(turret = this)
|
||||
WhichSide = Sidedness.OutsideOf
|
||||
|
||||
private var turretUpgradeTime: Long = System.currentTimeMillis()
|
||||
private var turretUpgradeTimeSet: Boolean = false
|
||||
|
||||
def UpdateTurretUpgradeTime(): Long = {
|
||||
turretUpgradeTime = System.currentTimeMillis()
|
||||
turretUpgradeTimeSet = true
|
||||
turretUpgradeTime
|
||||
}
|
||||
|
||||
// Used for checking the time without updating it
|
||||
def CheckTurretUpgradeTime: Long = {
|
||||
if (!turretUpgradeTimeSet) {
|
||||
turretUpgradeTime = System.currentTimeMillis()
|
||||
turretUpgradeTimeSet = true
|
||||
}
|
||||
turretUpgradeTime
|
||||
}
|
||||
|
||||
def FinishedTurretUpgradeReset(): Unit = {
|
||||
turretUpgradeTimeSet = false
|
||||
}
|
||||
|
||||
def TurretOwner: SourceEntry = {
|
||||
Seats
|
||||
.headOption
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import net.psforever.objects.{GlobalDefinitions, Player, Tool}
|
|||
import net.psforever.objects.equipment.Ammo
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.serverobject.damage.Damageable
|
||||
import net.psforever.objects.serverobject.hackable.GenericHackables
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.repair.AmenityAutoRepair
|
||||
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
|
||||
|
|
@ -100,7 +99,7 @@ class FacilityTurretControl(turret: FacilityTurret)
|
|||
seatNumber: Int,
|
||||
player: Player): Boolean = {
|
||||
super.mountTest(obj, seatNumber, player) &&
|
||||
(!TurretObject.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L)
|
||||
(!TurretObject.isUpgrading || System.currentTimeMillis() - TurretObject.CheckTurretUpgradeTime >= 1500L)
|
||||
}
|
||||
|
||||
override protected def tryMount(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ package net.psforever.objects.serverobject.turret
|
|||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, TurretSource}
|
||||
import net.psforever.objects.vital.{DismountingActivity, MountingActivity}
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
trait MountableTurretControl
|
||||
extends TurretControl
|
||||
|
|
@ -11,9 +15,22 @@ trait MountableTurretControl
|
|||
override def TurretObject: PlanetSideServerObject with WeaponTurret with Mountable
|
||||
|
||||
/** commonBehavior does not implement mountingBehavior; please do so when implementing */
|
||||
override def commonBehavior: Receive =
|
||||
super.commonBehavior
|
||||
.orElse(dismountBehavior)
|
||||
override def commonBehavior: Receive = super.commonBehavior.orElse(dismountBehavior)
|
||||
|
||||
override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = {
|
||||
super.mountActionResponse(user, mountPoint, seatNumber)
|
||||
if (TurretObject.PassengerInSeat(user).contains(0)) {
|
||||
val vsrc = TurretSource(TurretObject)
|
||||
user.LogActivity(MountingActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber = 0), TurretObject.Zone.Number))
|
||||
}
|
||||
}
|
||||
|
||||
override def dismountActionResponse(user: Player, seatBeingDismounted: Int): Unit = {
|
||||
super.dismountActionResponse(user, seatBeingDismounted)
|
||||
if (!TurretObject.Seats(seatBeingDismounted).isOccupied) { //this seat really was vacated
|
||||
user.LogActivity(DismountingActivity(TurretSource(TurretObject), PlayerSource(user), TurretObject.Zone.Number))
|
||||
}
|
||||
}
|
||||
|
||||
override protected def mountTest(
|
||||
obj: PlanetSideServerObject with Mountable,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package net.psforever.objects.serverobject.turret
|
|||
|
||||
import net.psforever.objects.avatar.Certification
|
||||
import net.psforever.objects.ce.Deployable
|
||||
import net.psforever.objects.serverobject.hackable.GenericHackables.updateTurretUpgradeTime
|
||||
import net.psforever.objects.{Player, Tool, TurretDeployable}
|
||||
import net.psforever.packet.game.{HackMessage, HackState, HackState1, HackState7, InventoryStateMessage}
|
||||
import net.psforever.services.Service
|
||||
|
|
@ -83,7 +82,7 @@ object WeaponTurrets {
|
|||
} else if (turret.Destroyed) {
|
||||
(HackState.Cancelled, 0)
|
||||
} else {
|
||||
updateTurretUpgradeTime()
|
||||
turret.UpdateTurretUpgradeTime()
|
||||
(HackState.Ongoing, progress.toInt)
|
||||
}
|
||||
turret.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ trait AffectedByAutomaticTurretFire extends Damageable {
|
|||
ProjectileReason(DamageResolution.Hit, modProjectile, target.DamageModel),
|
||||
correctedTargetPosition
|
||||
)
|
||||
PerformDamage(target, resolvedProjectile.calculate())
|
||||
PerformDamageIfVulnerable(target, resolvedProjectile.calculate())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2026 PSForever
|
||||
package net.psforever.objects.sourcing
|
||||
|
||||
trait MountableEntry {
|
||||
def occupants: List[SourceEntry]
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ final case class TurretSource(
|
|||
Orientation: Vector3,
|
||||
occupants: List[SourceEntry],
|
||||
unique: SourceUniqueness
|
||||
) extends SourceWithHealthEntry with SourceWithShieldsEntry {
|
||||
) extends SourceWithHealthEntry with SourceWithShieldsEntry with MountableEntry {
|
||||
def Name: String = SourceEntry.NameFormat(Definition.Descriptor)
|
||||
def Health: Int = health
|
||||
def Shields: Int = shields
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ final case class VehicleSource(
|
|||
occupants: List[SourceEntry],
|
||||
Modifiers: ResistanceProfile,
|
||||
unique: UniqueVehicle
|
||||
) extends SourceWithHealthEntry with SourceWithShieldsEntry {
|
||||
) extends SourceWithHealthEntry with SourceWithShieldsEntry with MountableEntry {
|
||||
def Name: String = SourceEntry.NameFormat(Definition.Name)
|
||||
def Health: Int = health
|
||||
def Shields: Int = shields
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ trait CargoBehavior {
|
|||
val zone = obj.Zone
|
||||
zone.GUID(isMounting) match {
|
||||
case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
isMounting = None
|
||||
zone.GUID(isDismounting) match {
|
||||
case Some(v: Vehicle) => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID)
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
isDismounting = None
|
||||
startCargoDismountingNoCleanup(bailed = false)
|
||||
|
|
@ -38,14 +38,10 @@ trait CargoBehavior {
|
|||
startCargoDismounting(bailed)
|
||||
|
||||
case CargoBehavior.EndCargoMounting(carrier_guid) =>
|
||||
if (isMounting.contains(carrier_guid)) {
|
||||
isMounting = None
|
||||
}
|
||||
endCargoMounting(carrier_guid)
|
||||
|
||||
case CargoBehavior.EndCargoDismounting(carrier_guid) =>
|
||||
if (isDismounting.contains(carrier_guid)) {
|
||||
isDismounting = None
|
||||
}
|
||||
endCargoDismounting(carrier_guid)
|
||||
}
|
||||
|
||||
def startCargoMounting(carrier_guid: PlanetSideGUID, mountPoint: Int): Unit = {
|
||||
|
|
@ -84,6 +80,18 @@ trait CargoBehavior {
|
|||
}
|
||||
.nonEmpty
|
||||
}
|
||||
|
||||
def endCargoMounting(carrierGuid: PlanetSideGUID): Unit = {
|
||||
if (isMounting.contains(carrierGuid)) {
|
||||
isMounting = None
|
||||
}
|
||||
}
|
||||
|
||||
def endCargoDismounting(carrierGuid: PlanetSideGUID): Unit = {
|
||||
if (isDismounting.contains(carrierGuid)) {
|
||||
isDismounting = None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object CargoBehavior {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import net.psforever.services.Service
|
|||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.types._
|
||||
|
||||
import scala.annotation.unused
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
|
||||
|
|
@ -170,8 +171,21 @@ class BfrControl(vehicle: Vehicle)
|
|||
specialArmWeaponEquipManagement(item, slot, handiness)
|
||||
}
|
||||
|
||||
override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = {
|
||||
super.dismountCleanup(seatBeingDismounted, player)
|
||||
override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = {
|
||||
super.mountActionResponse(user, mountPoint, seatNumber)
|
||||
if (vehicle.Seats.values.exists(_.isOccupied)) {
|
||||
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
|
||||
case Some(subsys)
|
||||
if !subsys.Enabled && vehicle.Shields > 0 && subsys.Enabled_=(state = true) =>
|
||||
//if the shield is damaged, it does not turn on until the damaged is cleared
|
||||
vehicleSubsystemMessages(subsys.changedMessages(vehicle))
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def dismountActionResponse(user: Player, seatBeingDismounted: Int): Unit = {
|
||||
super.dismountActionResponse(user, seatBeingDismounted)
|
||||
if (!vehicle.Seats.values.exists(_.isOccupied)) {
|
||||
vehicle
|
||||
.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
|
||||
|
|
@ -196,19 +210,6 @@ class BfrControl(vehicle: Vehicle)
|
|||
}
|
||||
}
|
||||
|
||||
override def mountCleanup(mount_point: Int, user: Player): Unit = {
|
||||
super.mountCleanup(mount_point, user)
|
||||
if (vehicle.Seats.values.exists(_.isOccupied)) {
|
||||
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
|
||||
case Some(subsys)
|
||||
if !subsys.Enabled && vehicle.Shields > 0 && subsys.Enabled_=(state = true) =>
|
||||
//if the shield is damaged, it does not turn on until the damaged is cleared
|
||||
vehicleSubsystemMessages(subsys.changedMessages(vehicle))
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def permitTerminalMessage(player: Player, msg: ItemTransactionMessage): Boolean = {
|
||||
if (msg.transaction_type == TransactionType.Loadout) {
|
||||
!vehicle.Jammed
|
||||
|
|
@ -455,7 +456,7 @@ class BfrControl(vehicle: Vehicle)
|
|||
}
|
||||
}
|
||||
|
||||
def specialArmWeaponEquipManagement(item: Equipment, slot: Int, handiness: equipment.Hand): Unit = {
|
||||
def specialArmWeaponEquipManagement(item: Equipment, slot: Int, @unused handiness: equipment.Hand): Unit = {
|
||||
if (item.Size == EquipmentSize.BFRArmWeapon && vehicle.VisibleSlots.contains(slot)) {
|
||||
val weapons = vehicle.Weapons
|
||||
//budget logic: the arm weapons are "next to each other" index-wise
|
||||
|
|
|
|||
|
|
@ -14,7 +14,16 @@ import net.psforever.objects.vital.interaction.DamageResult
|
|||
class CargoCarrierControl(vehicle: Vehicle)
|
||||
extends VehicleControl(vehicle)
|
||||
with CarrierBehavior {
|
||||
def CarrierObject = vehicle
|
||||
def CarrierObject: Vehicle = vehicle
|
||||
|
||||
override def TestToStartSelfReporting(): Boolean = {
|
||||
super.TestToStartSelfReporting() &&
|
||||
!CarrierObject
|
||||
.CargoHolds
|
||||
.values
|
||||
.flatMap(_.occupants)
|
||||
.exists(_.Seats.values.exists(_.isOccupied))
|
||||
}
|
||||
|
||||
override def postStop() : Unit = {
|
||||
super.postStop()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import akka.actor.ActorRef
|
|||
import net.psforever.objects._
|
||||
import net.psforever.objects.serverobject.deploy.Deployment.DeploymentObject
|
||||
import net.psforever.objects.serverobject.deploy.{Deployment, DeploymentBehavior}
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.types._
|
||||
|
||||
/**
|
||||
|
|
@ -36,13 +35,10 @@ class DeployingVehicleControl(vehicle: Vehicle)
|
|||
*/
|
||||
override def commonDisabledBehavior : Receive =
|
||||
super.commonDisabledBehavior
|
||||
.orElse(dismountBehavior)
|
||||
.orElse {
|
||||
case msg : Deployment.TryUndeploy =>
|
||||
case msg: Deployment.TryUndeploy =>
|
||||
deployBehavior.apply(msg)
|
||||
|
||||
case msg @ Mountable.TryDismount(player, seat_num, _) =>
|
||||
dismountBehavior.apply(msg)
|
||||
dismountCleanup(seat_num, player)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObjec
|
|||
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
|
||||
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
|
||||
import net.psforever.objects.serverobject.damage.Damageable.Target
|
||||
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle}
|
||||
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, Damageable, DamageableVehicle}
|
||||
import net.psforever.objects.serverobject.environment._
|
||||
import net.psforever.objects.serverobject.environment.interaction.common.Watery
|
||||
import net.psforever.objects.serverobject.environment.interaction.{InteractWithEnvironment, RespondsToZoneEnvironment}
|
||||
import net.psforever.objects.serverobject.hackable.GenericHackables
|
||||
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior, RadiationInMountableInteraction}
|
||||
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
|
||||
import net.psforever.objects.serverobject.repair.RepairableVehicle
|
||||
import net.psforever.objects.serverobject.structures.WarpGate
|
||||
import net.psforever.objects.serverobject.terminals.Terminal
|
||||
|
|
@ -29,8 +29,9 @@ import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
|
|||
import net.psforever.objects.vehicles._
|
||||
import net.psforever.objects.vehicles.interaction.WithWater
|
||||
import net.psforever.objects.vital.interaction.DamageResult
|
||||
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity}
|
||||
import net.psforever.objects.vital.{DamagingActivity, DismountingActivity, InGameActivity, MountingActivity, ShieldCharge, SpawningActivity}
|
||||
import net.psforever.objects.zones._
|
||||
import net.psforever.objects.zones.interaction.IndependentZoneInteraction
|
||||
import net.psforever.packet.PlanetSideGamePacket
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
|
||||
|
|
@ -39,6 +40,7 @@ import net.psforever.services.Service
|
|||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
|
||||
import scala.annotation.unused
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
|
@ -63,7 +65,8 @@ class VehicleControl(vehicle: Vehicle)
|
|||
with AggravatedBehavior
|
||||
with RespondsToZoneEnvironment
|
||||
with CargoBehavior
|
||||
with AffectedByAutomaticTurretFire {
|
||||
with AffectedByAutomaticTurretFire
|
||||
with IndependentZoneInteraction {
|
||||
//make control actors belonging to utilities when making control actor belonging to vehicle
|
||||
vehicle.Utilities.foreach { case (_, util) => util.Setup }
|
||||
|
||||
|
|
@ -77,6 +80,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
def InteractiveObject: Vehicle = vehicle
|
||||
def CargoObject: Vehicle = vehicle
|
||||
def AffectedObject: Vehicle = vehicle
|
||||
def ZoneInteractionObject: Vehicle = vehicle
|
||||
|
||||
/** cheap flag for whether the vehicle is decaying */
|
||||
var decaying : Boolean = false
|
||||
|
|
@ -84,8 +88,6 @@ class VehicleControl(vehicle: Vehicle)
|
|||
var decayTimer : Cancellable = Default.Cancellable
|
||||
/** becoming waterlogged, or drying out? */
|
||||
var submergedCondition : Option[OxygenState] = None
|
||||
/** ... */
|
||||
var passengerRadiationCloudTimer: Cancellable = Default.Cancellable
|
||||
|
||||
def receive : Receive = Enabled
|
||||
|
||||
|
|
@ -94,7 +96,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
damageableVehiclePostStop()
|
||||
decaying = false
|
||||
decayTimer.cancel()
|
||||
passengerRadiationCloudTimer.cancel()
|
||||
StopInteractionSelfReporting()
|
||||
vehicle.Utilities.values.foreach { util =>
|
||||
context.stop(util().Actor)
|
||||
util().Actor = Default.Actor
|
||||
|
|
@ -103,7 +105,80 @@ class VehicleControl(vehicle: Vehicle)
|
|||
endAllCargoOperations()
|
||||
}
|
||||
|
||||
private val mountingFailureReasons: Receive = {
|
||||
case Mountable.TryMount(user, mountPoint)
|
||||
if vehicle.DeploymentState == DriveState.AutoPilot =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint))
|
||||
|
||||
case Mountable.TryMount(user, mountPoint)
|
||||
if vehicle.Zone.blockMap.sector(vehicle).buildingList.exists {
|
||||
case wg: WarpGate =>
|
||||
Vector3.DistanceSquared(vehicle.Position, wg.Position) < math.pow(wg.Definition.SOIRadius, 2)
|
||||
case _ => false
|
||||
} && user.Carrying.contains(SpecialCarry.CaptureFlag) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint))
|
||||
}
|
||||
|
||||
private val dismountingFailureReasons: Receive = {
|
||||
case Mountable.TryDismount(user, seat_num, bailType)
|
||||
if vehicle.DeploymentState == DriveState.AutoPilot =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
// Issue 1133. Todo: There may be a better way to address the issue?
|
||||
case Mountable.TryDismount(user, seat_num, bailType) if GlobalDefinitions.isFlightVehicle(vehicle.Definition) &&
|
||||
(vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match {
|
||||
case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true
|
||||
case _ => false
|
||||
}) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case Mountable.TryDismount(user, seat_num, bailType) if !GlobalDefinitions.isFlightVehicle(vehicle.Definition) &&
|
||||
(vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match {
|
||||
case Some(entry) if System.currentTimeMillis() - entry.time < 8500L => true
|
||||
case _ => false
|
||||
}) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case Mountable.TryDismount(user, seat_num, bailType)
|
||||
if vehicle.Health <= (vehicle.Definition.MaxHealth * .1).round && bailType == BailType.Bailed
|
||||
&& GlobalDefinitions.isFlightVehicle(vehicle.Definition)
|
||||
&& (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Gunner))
|
||||
&& (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
|
||||
case Some(entry) if System.currentTimeMillis() - entry.time < 4000L => true
|
||||
case _ if Random.nextInt(10) == 1 => false
|
||||
case _ => true }) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case Mountable.TryDismount(user, seat_num, bailType)
|
||||
if vehicle.Health <= (vehicle.Definition.MaxHealth * .2).round && bailType == BailType.Bailed
|
||||
&& GlobalDefinitions.isFlightVehicle(vehicle.Definition)
|
||||
&& (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Gunner))
|
||||
&& (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
|
||||
case Some(entry) if System.currentTimeMillis() - entry.time < 3500L => true
|
||||
case _ if Random.nextInt(5) == 1 => false
|
||||
case _ => true }) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case Mountable.TryDismount(user, seat_num, bailType)
|
||||
if vehicle.Health <= (vehicle.Definition.MaxHealth * .35).round && bailType == BailType.Bailed
|
||||
&& GlobalDefinitions.isFlightVehicle(vehicle.Definition)
|
||||
&& (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Gunner))
|
||||
&& (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
|
||||
case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true
|
||||
case _ if Random.nextInt(4) == 1 => false
|
||||
case _ => true }) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case Mountable.TryDismount(user, seat_num, bailType)
|
||||
if vehicle.isMoving(test = 1f) && bailType == BailType.Normal =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
}
|
||||
|
||||
def commonEnabledBehavior: Receive = checkBehavior
|
||||
.orElse(mountingFailureReasons)
|
||||
.orElse(mountBehavior)
|
||||
.orElse(dismountingFailureReasons)
|
||||
.orElse(dismountBehavior)
|
||||
.orElse(attributeBehavior)
|
||||
.orElse(jammableBehavior)
|
||||
.orElse(takesDamage)
|
||||
|
|
@ -113,6 +188,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
.orElse(environmentBehavior)
|
||||
.orElse(cargoBehavior)
|
||||
.orElse(takeAutomatedDamage)
|
||||
.orElse(zoneInteractionBehavior)
|
||||
.orElse {
|
||||
case Vehicle.Ownership(None) =>
|
||||
LoseOwnership()
|
||||
|
|
@ -120,79 +196,6 @@ class VehicleControl(vehicle: Vehicle)
|
|||
case Vehicle.Ownership(Some(player)) =>
|
||||
GainOwnership(player)
|
||||
|
||||
case Mountable.TryMount(user, mountPoint)
|
||||
if vehicle.DeploymentState == DriveState.AutoPilot =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint))
|
||||
|
||||
case Mountable.TryMount(user, mountPoint)
|
||||
if vehicle.Zone.blockMap.sector(vehicle).buildingList.exists {
|
||||
case wg: WarpGate =>
|
||||
Vector3.DistanceSquared(vehicle.Position, wg.Position) < math.pow(wg.Definition.SOIRadius, 2)
|
||||
case _ => false
|
||||
} && user.Carrying.contains(SpecialCarry.CaptureFlag) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint))
|
||||
|
||||
case msg @ Mountable.TryMount(player, mount_point) =>
|
||||
mountBehavior.apply(msg)
|
||||
mountCleanup(mount_point, player)
|
||||
|
||||
// Issue 1133. Todo: There may be a better way to address the issue?
|
||||
case Mountable.TryDismount(user, seat_num, bailType) if GlobalDefinitions.isFlightVehicle(vehicle.Definition) &&
|
||||
(vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match {
|
||||
case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true
|
||||
case _ => false
|
||||
}) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case Mountable.TryDismount(user, seat_num, bailType) if !GlobalDefinitions.isFlightVehicle(vehicle.Definition) &&
|
||||
(vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match {
|
||||
case Some(entry) if System.currentTimeMillis() - entry.time < 8500L => true
|
||||
case _ => false
|
||||
}) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case Mountable.TryDismount(user, seat_num, bailType)
|
||||
if vehicle.Health <= (vehicle.Definition.MaxHealth * .1).round && bailType == BailType.Bailed
|
||||
&& GlobalDefinitions.isFlightVehicle(vehicle.Definition)
|
||||
&& (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner)
|
||||
&& (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
|
||||
case Some(entry) if System.currentTimeMillis() - entry.time < 4000L => true
|
||||
case _ if Random.nextInt(10) == 1 => false
|
||||
case _ => true }) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case Mountable.TryDismount(user, seat_num, bailType)
|
||||
if vehicle.Health <= (vehicle.Definition.MaxHealth * .2).round && bailType == BailType.Bailed
|
||||
&& GlobalDefinitions.isFlightVehicle(vehicle.Definition)
|
||||
&& (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner)
|
||||
&& (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
|
||||
case Some(entry) if System.currentTimeMillis() - entry.time < 3500L => true
|
||||
case _ if Random.nextInt(5) == 1 => false
|
||||
case _ => true }) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case Mountable.TryDismount(user, seat_num, bailType)
|
||||
if vehicle.Health <= (vehicle.Definition.MaxHealth * .35).round && bailType == BailType.Bailed
|
||||
&& GlobalDefinitions.isFlightVehicle(vehicle.Definition)
|
||||
&& (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner)
|
||||
&& (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
|
||||
case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true
|
||||
case _ if Random.nextInt(4) == 1 => false
|
||||
case _ => true }) =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case Mountable.TryDismount(user, seat_num, bailType)
|
||||
if vehicle.DeploymentState == DriveState.AutoPilot =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case Mountable.TryDismount(user, seat_num, bailType)
|
||||
if vehicle.isMoving(test = 1f) && bailType == BailType.Normal =>
|
||||
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
|
||||
|
||||
case msg @ Mountable.TryDismount(player, seat_num, _) =>
|
||||
dismountBehavior.apply(msg)
|
||||
dismountCleanup(seat_num, player)
|
||||
|
||||
case CommonMessages.ChargeShields(amount, motivator) =>
|
||||
chargeShields(amount, motivator.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) })
|
||||
|
||||
|
|
@ -213,7 +216,6 @@ class VehicleControl(vehicle: Vehicle)
|
|||
events ! VehicleServiceMessage(toChannel, VehicleAction.SendResponse(guid0, pkt))
|
||||
}
|
||||
|
||||
|
||||
case FactionAffinity.ConvertFactionAffinity(faction) =>
|
||||
val originalAffinity = vehicle.Faction
|
||||
if (originalAffinity != (vehicle.Faction = faction)) {
|
||||
|
|
@ -251,7 +253,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result = true)
|
||||
)
|
||||
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
} else {
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
|
|
@ -288,20 +290,12 @@ class VehicleControl(vehicle: Vehicle)
|
|||
final def Enabled: Receive =
|
||||
commonEnabledBehavior
|
||||
.orElse {
|
||||
case VehicleControl.RadiationTick if !passengerRadiationCloudTimer.isCancelled =>
|
||||
vehicle
|
||||
.interaction()
|
||||
.find(_.Type == RadiationInMountableInteraction)
|
||||
.foreach(_.interaction(vehicle.getInteractionSector, vehicle))
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
def commonDisabledBehavior: Receive = checkBehavior
|
||||
.orElse(dismountBehavior)
|
||||
.orElse {
|
||||
case msg @ Mountable.TryDismount(user, seat_num, _) =>
|
||||
dismountBehavior.apply(msg)
|
||||
dismountCleanup(seat_num, user)
|
||||
|
||||
case Vehicle.Deconstruct(time) =>
|
||||
time match {
|
||||
case Some(delay) if vehicle.Definition.undergoesDecay =>
|
||||
|
|
@ -320,7 +314,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
|
||||
final def Disabled: Receive = commonDisabledBehavior
|
||||
.orElse {
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
def commonDeleteBehavior: Receive = checkBehavior
|
||||
|
|
@ -336,7 +330,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
|
||||
final def ReadyToDelete: Receive = commonDeleteBehavior
|
||||
.orElse {
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
override protected def mountTest(
|
||||
|
|
@ -354,39 +348,32 @@ class VehicleControl(vehicle: Vehicle)
|
|||
super.mountTest(obj, seatNumber, user)
|
||||
}
|
||||
|
||||
def mountCleanup(mount_point: Int, user: Player): Unit = {
|
||||
vehicle.PassengerInSeat(user) match {
|
||||
case Some(0) => //driver seat
|
||||
val vsrc = VehicleSource(vehicle)
|
||||
user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber = 0), vehicle.Zone.Number))
|
||||
//if the driver mount, change ownership if that is permissible for this vehicle
|
||||
if (!vehicle.OwnerName.contains(user.Name) && vehicle.Definition.CanBeOwned.nonEmpty) {
|
||||
//whatever vehicle was previously owned
|
||||
vehicle.Zone.GUID(user.avatar.vehicle) match {
|
||||
case Some(v: Vehicle) =>
|
||||
v.Actor ! Vehicle.Ownership(None)
|
||||
case _ =>
|
||||
user.avatar.vehicle = None
|
||||
}
|
||||
GainOwnership(user) //gain new ownership
|
||||
} else {
|
||||
decaying = false
|
||||
decayTimer.cancel()
|
||||
override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = {
|
||||
super.mountActionResponse(user, mountPoint, seatNumber)
|
||||
val vsrc = VehicleSource(vehicle)
|
||||
user.LogActivity(MountingActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number))
|
||||
if (seatNumber == 0) {
|
||||
//if the driver mount, change ownership if that is permissible for this vehicle
|
||||
if (!vehicle.OwnerName.contains(user.Name) && vehicle.Definition.CanBeOwned.nonEmpty) {
|
||||
//whatever vehicle was previously owned
|
||||
vehicle.Zone.GUID(user.avatar.vehicle) match {
|
||||
case Some(v: Vehicle) =>
|
||||
v.Actor ! Vehicle.Ownership(None)
|
||||
case _ =>
|
||||
user.avatar.vehicle = None
|
||||
}
|
||||
passengerRadiationCloudTimer.cancel()
|
||||
updateZoneInteractionProgressUI(user)
|
||||
|
||||
case Some(seatNumber) => //literally any other seat
|
||||
val vsrc = VehicleSource(vehicle)
|
||||
user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number))
|
||||
GainOwnership(user) //gain new ownership
|
||||
} else {
|
||||
decaying = false
|
||||
decayTimer.cancel()
|
||||
if (!vehicle.Seats(0).isOccupied && passengerRadiationCloudTimer.isCancelled) {
|
||||
StartRadiationSelfReporting()
|
||||
}
|
||||
updateZoneInteractionProgressUI(user)
|
||||
|
||||
case None => ()
|
||||
}
|
||||
TryStopInteractionSelfReporting()
|
||||
updateZoneInteractionProgressUI(user)
|
||||
} else {
|
||||
decaying = false
|
||||
decayTimer.cancel()
|
||||
StopInteractionSelfReporting()
|
||||
updateZoneInteractionProgressUI(user)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -398,48 +385,37 @@ class VehicleControl(vehicle: Vehicle)
|
|||
vehicle.DeploymentState == DriveState.Deployed || super.dismountTest(obj, seatNumber, user)
|
||||
}
|
||||
|
||||
def dismountCleanup(seatBeingDismounted: Int, user: Player): Unit = {
|
||||
override def dismountActionResponse(user: Player, @unused seatBeingDismounted: Int): Unit = {
|
||||
super.dismountActionResponse(user, seatBeingDismounted)
|
||||
user.LogActivity(DismountingActivity(VehicleSource(vehicle), PlayerSource(user), vehicle.Zone.Number))
|
||||
val obj = MountableObject
|
||||
val allSeatsUnoccupied = !obj.Seats.values.exists(_.isOccupied)
|
||||
// Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount
|
||||
if (!obj.Seats(0).isOccupied) {
|
||||
if (seatBeingDismounted == 0) {
|
||||
obj.Velocity = Some(Vector3.Zero)
|
||||
}
|
||||
if (allSeatsUnoccupied) {
|
||||
passengerRadiationCloudTimer.cancel()
|
||||
} else if (seatBeingDismounted == 0) {
|
||||
StartRadiationSelfReporting()
|
||||
if (TestToStartSelfReporting()) {
|
||||
StartInteractionSelfReporting()
|
||||
}
|
||||
if (!obj.Seats(seatBeingDismounted).isOccupied) { //this seat really was vacated
|
||||
user.LogActivity(VehicleDismountActivity(VehicleSource(vehicle), PlayerSource(user), vehicle.Zone.Number))
|
||||
//we were only owning the vehicle while we sat in its driver seat
|
||||
val canBeOwned = obj.Definition.CanBeOwned
|
||||
if (canBeOwned.contains(false) && seatBeingDismounted == 0) {
|
||||
LoseOwnership()
|
||||
}
|
||||
//are we already decaying? are we unowned? is no one seated anywhere?
|
||||
if (!decaying &&
|
||||
obj.Definition.undergoesDecay &&
|
||||
obj.OwnerGuid.isEmpty &&
|
||||
allSeatsUnoccupied) {
|
||||
decaying = true
|
||||
decayTimer = context.system.scheduler.scheduleOnce(
|
||||
MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes),
|
||||
self,
|
||||
VehicleControl.PrepareForDeletion()
|
||||
)
|
||||
}
|
||||
//we were only owning the vehicle while we sat in its driver seat
|
||||
val canBeOwned = obj.Definition.CanBeOwned
|
||||
if (canBeOwned.contains(false) && seatBeingDismounted == 0) {
|
||||
LoseOwnership()
|
||||
}
|
||||
//are we already decaying? are we unowned? is no one seated anywhere?
|
||||
if (!decaying &&
|
||||
obj.Definition.undergoesDecay &&
|
||||
obj.OwnerGuid.isEmpty &&
|
||||
!vehicle.Seats.values.exists(_.isOccupied)) {
|
||||
decaying = true
|
||||
decayTimer = context.system.scheduler.scheduleOnce(
|
||||
MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes),
|
||||
self,
|
||||
VehicleControl.PrepareForDeletion()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def StartRadiationSelfReporting(): Unit = {
|
||||
passengerRadiationCloudTimer.cancel()
|
||||
passengerRadiationCloudTimer = context.system.scheduler.scheduleWithFixedDelay(
|
||||
250.milliseconds,
|
||||
250.milliseconds,
|
||||
self,
|
||||
VehicleControl.RadiationTick
|
||||
)
|
||||
def TestToStartSelfReporting(): Boolean = {
|
||||
vehicle.MountedIn.isEmpty && !vehicle.Seats.values.exists(_.isOccupied)
|
||||
}
|
||||
|
||||
def PrepareForDisabled(kickPassengers: Boolean) : Unit = {
|
||||
|
|
@ -568,7 +544,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
VehicleAction.InventoryState2(Service.defaultPlayerGUID, box.GUID, iguid, box.Capacity)
|
||||
)
|
||||
}
|
||||
case _ => ;
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -712,7 +688,7 @@ class VehicleControl(vehicle: Vehicle)
|
|||
VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, vguid)
|
||||
)
|
||||
}
|
||||
case _ => ; // No player seated
|
||||
case _ => () // No player seated
|
||||
}
|
||||
}
|
||||
vehicle.CargoHolds.foreach {
|
||||
|
|
@ -724,11 +700,11 @@ class VehicleControl(vehicle: Vehicle)
|
|||
// Instruct client to start bail dismount procedure
|
||||
self ! DismountVehicleCargoMsg(dguid, cargo.GUID, bailed = true, requestedByPassenger = false, kicked = false)
|
||||
}
|
||||
case None => ; // No vehicle in cargo
|
||||
case None => () // No vehicle in cargo
|
||||
}
|
||||
}
|
||||
}
|
||||
case None => ;
|
||||
case None => ()
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
|
|
@ -766,10 +742,39 @@ class VehicleControl(vehicle: Vehicle)
|
|||
}
|
||||
}
|
||||
|
||||
override protected def canChangeVulnerability(state: Damageable.PersonalVulnerability): Boolean = {
|
||||
state match {
|
||||
case Damageable.MakeInvulnerable =>
|
||||
vehicle.LastDamage.exists { d => System.currentTimeMillis() - d.interaction.hitTime > 2500L }
|
||||
case _ =>
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
|
||||
passengerRadiationCloudTimer.cancel()
|
||||
StopInteractionSelfReportingNoReset()
|
||||
super.DestructionAwareness(target, cause)
|
||||
}
|
||||
|
||||
override def endCargoMounting(carrierGuid: PlanetSideGUID): Unit = {
|
||||
super.endCargoMounting(carrierGuid)
|
||||
StopInteractionSelfReporting()
|
||||
vehicle.Zone.GUID(carrierGuid) match {
|
||||
case Some(v: Vehicle) => v.Actor ! IndependentZoneInteraction.SelfReportRunCheck
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
override def endCargoDismounting(carrierGuid: PlanetSideGUID): Unit = {
|
||||
super.endCargoDismounting(carrierGuid)
|
||||
if (TestToStartSelfReporting()) {
|
||||
StartInteractionSelfReporting()
|
||||
}
|
||||
vehicle.Zone.GUID(carrierGuid) match {
|
||||
case Some(v: Vehicle) => v.Actor ! IndependentZoneInteraction.SelfReportRunCheck
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object VehicleControl {
|
||||
|
|
@ -779,7 +784,5 @@ object VehicleControl {
|
|||
|
||||
private case class Deletion()
|
||||
|
||||
private case object RadiationTick
|
||||
|
||||
final case class AssignOwnership(player: Option[Player])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) 2025 PSForever
|
||||
package net.psforever.objects.vehicles.interaction
|
||||
|
||||
import net.psforever.objects.Vehicle
|
||||
import net.psforever.objects.avatar.interaction.{ForceZoneProtection, InteractWithForceDomeProtection}
|
||||
import net.psforever.objects.serverobject.dome.ForceDomePhysics
|
||||
import net.psforever.objects.serverobject.mount.interaction.InteractWithForceDomeProtectionSeatedInEntity
|
||||
import net.psforever.objects.zones.interaction.InteractsWithZone
|
||||
|
||||
class InteractWithForceDomeProtectionSeatedInVehicle
|
||||
extends InteractWithForceDomeProtectionSeatedInEntity {
|
||||
override def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
|
||||
super.applyProtection(target, dome)
|
||||
target
|
||||
.asInstanceOf[Vehicle]
|
||||
.CargoHolds
|
||||
.values
|
||||
.flatMap(_.occupants)
|
||||
.foreach { vehicle =>
|
||||
vehicle
|
||||
.interaction()
|
||||
.find(_.Type == ForceZoneProtection)
|
||||
.foreach {
|
||||
case interaction: InteractWithForceDomeProtection =>
|
||||
interaction.applyProtection(vehicle, dome)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def resetInteraction(target: InteractsWithZone): Unit = {
|
||||
super.resetInteraction(target)
|
||||
target
|
||||
.asInstanceOf[Vehicle]
|
||||
.CargoHolds
|
||||
.values
|
||||
.flatMap(_.occupants)
|
||||
.foreach { vehicle =>
|
||||
vehicle
|
||||
.interaction()
|
||||
.find(_.Type == ForceZoneProtection)
|
||||
.foreach {
|
||||
case interaction: InteractWithForceDomeProtection =>
|
||||
interaction.resetInteraction(vehicle)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.vehicles
|
||||
package net.psforever.objects.vehicles.interaction
|
||||
|
||||
import net.psforever.objects.Vehicle
|
||||
import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, RadiationInMountableInteraction}
|
||||
import net.psforever.objects.serverobject.mount.interaction.{InteractWithRadiationCloudsSeatedInEntity, RadiationInMountableInteraction}
|
||||
import net.psforever.objects.zones.blockmap.SectorPopulation
|
||||
import net.psforever.objects.zones.interaction.InteractsWithZone
|
||||
|
||||
|
|
@ -4,7 +4,7 @@ package net.psforever.objects.vital
|
|||
import net.psforever.objects.PlanetSideGameObject
|
||||
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.sourcing.{AmenitySource, DeployableSource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource}
|
||||
import net.psforever.objects.sourcing.{AmenitySource, DeployableSource, MountableEntry, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource}
|
||||
import net.psforever.objects.vital.environment.EnvironmentReason
|
||||
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
|
||||
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
|
||||
|
|
@ -79,44 +79,48 @@ final case class RevivingActivity(target: SourceEntry, user: PlayerSource, amoun
|
|||
final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
|
||||
extends GeneralActivity
|
||||
|
||||
trait TerminalUse {
|
||||
def terminal: AmenitySource
|
||||
}
|
||||
|
||||
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
|
||||
extends GeneralActivity
|
||||
extends GeneralActivity with TerminalUse
|
||||
|
||||
final case class TelepadUseActivity(router: VehicleSource, telepad: DeployableSource, player: PlayerSource)
|
||||
extends GeneralActivity
|
||||
|
||||
sealed trait VehicleMountChange extends GeneralActivity {
|
||||
def vehicle: VehicleSource
|
||||
sealed trait MountChange extends GeneralActivity {
|
||||
def mount: SourceEntry with MountableEntry
|
||||
def zoneNumber: Int
|
||||
}
|
||||
|
||||
sealed trait VehiclePassengerMountChange extends VehicleMountChange {
|
||||
sealed trait PassengerMountChange extends MountChange {
|
||||
def player: PlayerSource
|
||||
}
|
||||
|
||||
sealed trait VehicleCargoMountChange extends VehicleMountChange {
|
||||
sealed trait CargoMountChange extends MountChange {
|
||||
def cargo: VehicleSource
|
||||
}
|
||||
|
||||
final case class VehicleMountActivity(vehicle: VehicleSource, player: PlayerSource, zoneNumber: Int)
|
||||
extends VehiclePassengerMountChange
|
||||
final case class MountingActivity(mount: SourceEntry with MountableEntry, player: PlayerSource, zoneNumber: Int)
|
||||
extends PassengerMountChange
|
||||
|
||||
final case class VehicleDismountActivity(
|
||||
vehicle: VehicleSource,
|
||||
player: PlayerSource,
|
||||
zoneNumber: Int,
|
||||
pairedEvent: Option[VehicleMountActivity] = None
|
||||
) extends VehiclePassengerMountChange
|
||||
final case class DismountingActivity(
|
||||
mount: SourceEntry with MountableEntry,
|
||||
player: PlayerSource,
|
||||
zoneNumber: Int,
|
||||
pairedEvent: Option[MountingActivity] = None
|
||||
) extends PassengerMountChange
|
||||
|
||||
final case class VehicleCargoMountActivity(vehicle: VehicleSource, cargo: VehicleSource, zoneNumber: Int)
|
||||
extends VehicleCargoMountChange
|
||||
final case class VehicleCargoMountActivity(mount: VehicleSource, cargo: VehicleSource, zoneNumber: Int)
|
||||
extends CargoMountChange
|
||||
|
||||
final case class VehicleCargoDismountActivity(
|
||||
vehicle: VehicleSource,
|
||||
mount: VehicleSource,
|
||||
cargo: VehicleSource,
|
||||
zoneNumber: Int,
|
||||
pairedEvent: Option[VehicleCargoMountActivity] = None
|
||||
) extends VehicleCargoMountChange
|
||||
) extends CargoMountChange
|
||||
|
||||
final case class Contribution(src: SourceUniqueness, entries: List[InGameActivity])
|
||||
extends GeneralActivity {
|
||||
|
|
@ -165,8 +169,8 @@ final case class HealFromKit(kit_def: KitDefinition, amount: Int)
|
|||
final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
|
||||
extends HealingActivity with SupportActivityCausedByAnother
|
||||
|
||||
final case class HealFromTerminal(term: AmenitySource, amount: Int)
|
||||
extends HealingActivity
|
||||
final case class HealFromTerminal(terminal: AmenitySource, amount: Int)
|
||||
extends HealingActivity with TerminalUse
|
||||
|
||||
final case class HealFromImplant(implant: ImplantType, amount: Int)
|
||||
extends HealingActivity
|
||||
|
|
@ -180,7 +184,8 @@ final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
|
|||
final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
|
||||
extends RepairingActivity with SupportActivityCausedByAnother
|
||||
|
||||
final case class RepairFromTerminal(term: AmenitySource, amount: Int) extends RepairingActivity
|
||||
final case class RepairFromTerminal(terminal: AmenitySource, amount: Int)
|
||||
extends RepairingActivity with TerminalUse
|
||||
|
||||
final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, vehicle: VehicleSource, amount: Int)
|
||||
extends RepairingActivity
|
||||
|
|
@ -251,24 +256,24 @@ trait InGameHistory {
|
|||
*/
|
||||
def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = {
|
||||
action match {
|
||||
case Some(act: VehicleDismountActivity) if act.pairedEvent.isEmpty =>
|
||||
case Some(act: DismountingActivity) if act.pairedEvent.isEmpty =>
|
||||
history
|
||||
.findLast(_.isInstanceOf[VehicleMountActivity])
|
||||
.findLast(_.isInstanceOf[MountingActivity])
|
||||
.collect {
|
||||
case event: VehicleMountActivity if event.vehicle.unique == act.vehicle.unique =>
|
||||
case event: MountingActivity if event.mount.unique == act.mount.unique =>
|
||||
history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
|
||||
}
|
||||
.orElse {
|
||||
history = history :+ act
|
||||
None
|
||||
}
|
||||
case Some(act: VehicleDismountActivity) =>
|
||||
case Some(act: DismountingActivity) =>
|
||||
history = history :+ act
|
||||
case Some(act: VehicleCargoDismountActivity) =>
|
||||
history
|
||||
.findLast(_.isInstanceOf[VehicleCargoMountActivity])
|
||||
.collect {
|
||||
case event: VehicleCargoMountActivity if event.vehicle.unique == act.vehicle.unique =>
|
||||
case event: VehicleCargoMountActivity if event.mount.unique == act.mount.unique =>
|
||||
history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
|
||||
}
|
||||
.orElse {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) 2025 PSForever
|
||||
package net.psforever.objects.vital.etc
|
||||
|
||||
import net.psforever.objects.sourcing.{AmenitySource, SourceEntry}
|
||||
import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions}
|
||||
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
|
||||
import net.psforever.objects.vital.damage.DamageCalculations
|
||||
import net.psforever.objects.vital.prop.{DamageProperties, DamageWithPosition}
|
||||
import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel}
|
||||
|
||||
/**
|
||||
* A wrapper for a "damage source" in damage calculations that indicates a harmful interaction from a capitol force dome.
|
||||
* @param field the target of the field in question
|
||||
*/
|
||||
final case class ForceDomeExposure(field: SourceEntry)
|
||||
extends DamageReason {
|
||||
def resolution: DamageResolution.Value = DamageResolution.Collision
|
||||
|
||||
def same(test: DamageReason): Boolean = test match {
|
||||
case eer: ForceDomeExposure => eer.field eq field
|
||||
case _ => false
|
||||
}
|
||||
|
||||
/**
|
||||
* Blame the capitol facility that is being protected.
|
||||
*/
|
||||
override def attribution: Int = field match {
|
||||
case a: AmenitySource => a.installation.Definition.ObjectId
|
||||
case _ => field.Definition.ObjectId
|
||||
}
|
||||
|
||||
override def source: DamageProperties = ForceDomeExposure.damageProperties
|
||||
|
||||
override def damageModel: DamageAndResistance = ForceDomeExposure.drm
|
||||
|
||||
/**
|
||||
* No one person will be blamed for this.
|
||||
*/
|
||||
override def adversary: Option[SourceEntry] = None
|
||||
}
|
||||
|
||||
object ForceDomeExposure {
|
||||
final val drm = new DamageResistanceModel {
|
||||
DamageUsing = DamageCalculations.AgainstExoSuit
|
||||
ResistUsing = NoResistanceSelection
|
||||
Model = SimpleResolutions.calculate
|
||||
}
|
||||
|
||||
final val damageProperties = new DamageWithPosition {
|
||||
Damage0 = 99999
|
||||
DamageToHealthOnly = true
|
||||
DamageToVehicleOnly = true
|
||||
DamageToBattleframeOnly = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ final case class SuicideReason()
|
|||
eventually, they stop logging in.
|
||||
|
||||
Anyway, this has nothing to do with that.
|
||||
Most playes probably just want to jump to the next base over.
|
||||
Most players probably just want to jump to the next base over.
|
||||
*/
|
||||
def source: DamageProperties = SuicideReason.damageProperties
|
||||
|
||||
|
|
|
|||
|
|
@ -1809,6 +1809,29 @@ object Zone {
|
|||
|
||||
/* explosions */
|
||||
|
||||
/**
|
||||
* Allocates `Damageable` targets within the vicinity of server-prepared damage dealing
|
||||
* and informs those entities that they have affected by the aforementioned damage.
|
||||
* Usually, this is considered an "explosion;" but, the application can be utilized for a variety of unbound damage.
|
||||
* @param zone the zone in which the damage should occur
|
||||
* @param source the entity that embodies the damage (information)
|
||||
* @param createInteraction how the interaction for this damage is to prepared
|
||||
* @return a list of affected entities;
|
||||
* only mostly complete due to the exclusion of objects whose damage resolution is different than usual
|
||||
*/
|
||||
def serverSideDamage(
|
||||
zone: Zone,
|
||||
source: PlanetSideGameObject with FactionAffinity with Vitality,
|
||||
createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction
|
||||
): List[PlanetSideServerObject] = {
|
||||
source.Definition.innateDamage match {
|
||||
case Some(damage) =>
|
||||
serverSideDamage(zone, source, damage, createInteraction, distanceCheck, findAllTargets)
|
||||
case None =>
|
||||
Nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates `Damageable` targets within the vicinity of server-prepared damage dealing
|
||||
* and informs those entities that they have affected by the aforementioned damage.
|
||||
|
|
@ -1816,8 +1839,10 @@ object Zone {
|
|||
* @param zone the zone in which the damage should occur
|
||||
* @param source the entity that embodies the damage (information)
|
||||
* @param createInteraction how the interaction for this damage is to prepared
|
||||
* @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage
|
||||
* @param acquireTargetsFromZone the main target-collecting algorithm
|
||||
* @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage;
|
||||
* filters targets from the existing selection
|
||||
* @param acquireTargetsFromZone the main target-collecting algorithm;
|
||||
* collects targets from sector information
|
||||
* @return a list of affected entities;
|
||||
* only mostly complete due to the exclusion of objects whose damage resolution is different than usual
|
||||
*/
|
||||
|
|
@ -1825,8 +1850,8 @@ object Zone {
|
|||
zone: Zone,
|
||||
source: PlanetSideGameObject with FactionAffinity with Vitality,
|
||||
createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction,
|
||||
testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck,
|
||||
acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => List[PlanetSideServerObject with Vitality] = findAllTargets
|
||||
testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean,
|
||||
acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => List[PlanetSideServerObject with Vitality]
|
||||
): List[PlanetSideServerObject] = {
|
||||
source.Definition.innateDamage match {
|
||||
case Some(damage) =>
|
||||
|
|
@ -1851,8 +1876,10 @@ object Zone {
|
|||
* @param zone the zone in which the damage should occur
|
||||
* @param source the entity that embodies the damage (information)
|
||||
* @param createInteraction how the interaction for this damage is to prepared
|
||||
* @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage
|
||||
* @param acquireTargetsFromZone the main target-collecting algorithm
|
||||
* @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage;
|
||||
* filters targets from the existing selection
|
||||
* @param acquireTargetsFromZone the main target-collecting algorithm;
|
||||
* collects targets from sector information
|
||||
* @return a list of affected entities;
|
||||
* only mostly complete due to the exclusion of objects whose damage resolution is different than usual
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ package net.psforever.objects.zones.exp
|
|||
import akka.actor.ActorRef
|
||||
import net.psforever.objects.GlobalDefinitions
|
||||
import net.psforever.objects.avatar.scoring.{Kill, SupportActivity}
|
||||
import net.psforever.objects.sourcing.{BuildingSource, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, VehicleSource}
|
||||
import net.psforever.objects.vital.{Contribution, HealFromTerminal, InGameActivity, RepairFromTerminal, RevivingActivity, TelepadUseActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleDismountActivity, VehicleMountActivity}
|
||||
import net.psforever.objects.sourcing.{BuildingSource, MountableEntry, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, UniquePlayer, VehicleSource}
|
||||
import net.psforever.objects.vital.{Contribution, InGameActivity, RevivingActivity, TelepadUseActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, DismountingActivity, MountingActivity}
|
||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
||||
import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
|
|
@ -388,13 +388,15 @@ object KillContributions {
|
|||
the player should not get credit from being the vehicle owner in matters of transportation
|
||||
there are considerations of time and distance traveled before the kill as well
|
||||
*/
|
||||
case out: VehicleDismountActivity
|
||||
if !out.vehicle.owner.contains(out.player.unique) && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
|
||||
case out: DismountingActivity
|
||||
if out.mount.isInstanceOf[VehicleSource] &&
|
||||
!ownershipFromMount(out.mount).contains(out.player.unique) &&
|
||||
out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
|
||||
}
|
||||
.collect {
|
||||
case (in: VehicleMountActivity, out: VehicleDismountActivity)
|
||||
if in.vehicle.unique == out.vehicle.unique &&
|
||||
out.vehicle.Faction == out.player.Faction &&
|
||||
case (in: MountingActivity, out: DismountingActivity)
|
||||
if in.mount.unique == out.mount.unique &&
|
||||
out.mount.Faction == out.player.Faction &&
|
||||
/*
|
||||
considerations of time and distance transported before the kill
|
||||
*/
|
||||
|
|
@ -407,7 +409,7 @@ object KillContributions {
|
|||
}
|
||||
} || {
|
||||
val sameZone = in.zoneNumber == out.zoneNumber
|
||||
val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
|
||||
val distanceTransported = Vector3.DistanceSquared(in.mount.Position.xy, out.mount.Position.xy)
|
||||
val distanceMoved = {
|
||||
val killLocation = killerOpt.map(_.Position.xy).getOrElse(Vector3.Zero)
|
||||
Vector3.DistanceSquared(killLocation, out.player.Position.xy)
|
||||
|
|
@ -423,9 +425,9 @@ object KillContributions {
|
|||
}
|
||||
//apply
|
||||
dismountActivity
|
||||
.groupBy { _.vehicle }
|
||||
.collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty =>
|
||||
val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
|
||||
.groupBy { _.mount }
|
||||
.collect { case (mount, dismountsFromVehicle) if ownershipFromMount(mount).nonEmpty =>
|
||||
val promotedOwner = PlayerSource(ownershipFromMount(mount).get, mount.Position)
|
||||
val size = dismountsFromVehicle.size
|
||||
val time = dismountsFromVehicle.maxBy(_.time).time
|
||||
List((HotDropKillAssist(mount.Definition.ObjectId, 0), "hotdrop", promotedOwner))
|
||||
|
|
@ -457,6 +459,24 @@ object KillContributions {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the owner of the entity based on information about the entity.
|
||||
* @param mount mountable entity which can be owned
|
||||
* @return the optional unique referential signature for the owner
|
||||
*/
|
||||
private def ownershipFromMount(mount: SourceEntry with MountableEntry): Option[UniquePlayer] = {
|
||||
mount match {
|
||||
case v: VehicleSource =>
|
||||
v.owner
|
||||
case t: TurretSource => t.occupants.headOption.flatMap {
|
||||
case p: PlayerSource => Some(p.unique)
|
||||
case _ => None
|
||||
}
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather and reward specific in-game equipment use activity.<br>
|
||||
* na
|
||||
|
|
@ -486,14 +506,14 @@ object KillContributions {
|
|||
val dismountActivity = history
|
||||
.collect {
|
||||
case out: VehicleCargoDismountActivity
|
||||
if out.vehicle.owner.nonEmpty && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
|
||||
if ownershipFromMount(out.mount).nonEmpty && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
|
||||
}
|
||||
.collect {
|
||||
case (in: VehicleCargoMountActivity, out: VehicleCargoDismountActivity)
|
||||
if in.vehicle.unique == out.vehicle.unique &&
|
||||
out.vehicle.Faction == out.cargo.Faction &&
|
||||
(in.vehicle.Definition == GlobalDefinitions.router || {
|
||||
val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
|
||||
if in.mount.unique == out.mount.unique &&
|
||||
out.mount.Faction == out.cargo.Faction &&
|
||||
(in.mount.Definition == GlobalDefinitions.router || {
|
||||
val distanceTransported = Vector3.DistanceSquared(in.mount.Position.xy, out.mount.Position.xy)
|
||||
val distanceMoved = {
|
||||
val killLocation = kill.info.adversarial
|
||||
.collect { adversarial => adversarial.attacker.Position.xy }
|
||||
|
|
@ -513,7 +533,7 @@ object KillContributions {
|
|||
val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
|
||||
val mountId = mount.Definition.ObjectId
|
||||
dismountsFromVehicle
|
||||
.groupBy(_.vehicle)
|
||||
.groupBy(_.mount)
|
||||
.map { case (vehicle, events) =>
|
||||
val size = events.size
|
||||
val time = events.maxBy(_.time).time
|
||||
|
|
@ -673,8 +693,6 @@ object KillContributions {
|
|||
): Unit = {
|
||||
history
|
||||
.collect {
|
||||
case h: HealFromTerminal => (h.term, h)
|
||||
case r: RepairFromTerminal => (r.term, r)
|
||||
case t: TerminalUsedActivity => (t.terminal, t)
|
||||
}
|
||||
.groupBy(_._1.unique)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package net.psforever.objects.zones.exp
|
||||
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.vital.{ExoSuitChange, InGameActivity, RevivingActivity, TerminalUsedActivity, VehicleDismountActivity, VehicleMountActivity, VehicleMountChange, VitalityDefinition}
|
||||
import net.psforever.objects.vital.{ExoSuitChange, InGameActivity, RevivingActivity, TerminalUsedActivity, DismountingActivity, MountingActivity, MountChange, VitalityDefinition}
|
||||
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
|
||||
import net.psforever.util.{Config, DefinitionUtil, ThreatAssessment, ThreatLevel}
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ object Support {
|
|||
val wornTime: mutable.HashMap[Int, Long] = mutable.HashMap[Int, Long]()
|
||||
var currentSuit: Int = initialExosuit.id
|
||||
var lastActTime: Long = history.head.time
|
||||
var lastMountAct: Option[VehicleMountChange] = None
|
||||
var lastMountAct: Option[MountChange] = None
|
||||
//collect history events that encompass changes to exo-suits and to mounting conditions
|
||||
history.collect {
|
||||
case suitChange: ExoSuitChange =>
|
||||
|
|
@ -93,7 +93,7 @@ object Support {
|
|||
)
|
||||
currentSuit = suitChange.exosuit.id
|
||||
lastActTime = suitChange.time
|
||||
case mount: VehicleMountActivity =>
|
||||
case mount: MountingActivity =>
|
||||
updateEquippedEntry(
|
||||
currentSuit,
|
||||
mount.time - lastActTime,
|
||||
|
|
@ -101,18 +101,18 @@ object Support {
|
|||
)
|
||||
lastActTime = mount.time
|
||||
lastMountAct = Some(mount)
|
||||
case dismount: VehicleDismountActivity
|
||||
case dismount: DismountingActivity
|
||||
if dismount.pairedEvent.isEmpty =>
|
||||
updateEquippedEntry(
|
||||
dismount.vehicle.Definition.ObjectId,
|
||||
dismount.mount.Definition.ObjectId,
|
||||
dismount.time - lastActTime,
|
||||
wornTime
|
||||
)
|
||||
lastActTime = dismount.time
|
||||
lastMountAct = None
|
||||
case dismount: VehicleDismountActivity =>
|
||||
case dismount: DismountingActivity =>
|
||||
updateEquippedEntry(
|
||||
dismount.vehicle.Definition.ObjectId,
|
||||
dismount.mount.Definition.ObjectId,
|
||||
dismount.time - dismount.pairedEvent.get.time,
|
||||
wornTime
|
||||
)
|
||||
|
|
@ -125,7 +125,7 @@ object Support {
|
|||
.collect { mount =>
|
||||
//dying in a vehicle is a reason to care about the last mount activity
|
||||
updateEquippedEntry(
|
||||
mount.vehicle.Definition.ObjectId,
|
||||
mount.mount.Definition.ObjectId,
|
||||
lastTime - mount.time,
|
||||
wornTime
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright (c) 2026 PSForever
|
||||
package net.psforever.objects.zones.interaction
|
||||
|
||||
import akka.actor.{Actor, Cancellable}
|
||||
import net.psforever.objects.Default
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
|
||||
trait IndependentZoneInteraction {
|
||||
_: Actor =>
|
||||
/** ... */
|
||||
private var zoneInteractionIntervalDefault: FiniteDuration = 250.milliseconds
|
||||
/** ... */
|
||||
private var zoneInteractionTimer: Cancellable = Default.Cancellable
|
||||
|
||||
def ZoneInteractionObject: InteractsWithZone
|
||||
|
||||
val zoneInteractionBehavior: Receive = {
|
||||
case IndependentZoneInteraction.InteractionTick =>
|
||||
PerformZoneInteractionSelfReporting()
|
||||
|
||||
case IndependentZoneInteraction.SelfReportRunCheck =>
|
||||
PerformSelfReportRunCheck()
|
||||
}
|
||||
|
||||
def ZoneInteractionInterval: FiniteDuration = zoneInteractionIntervalDefault
|
||||
|
||||
def ZoneInteractionInterval_=(interval: FiniteDuration): FiniteDuration = {
|
||||
zoneInteractionIntervalDefault = interval
|
||||
ZoneInteractionInterval
|
||||
}
|
||||
|
||||
def TestToStartSelfReporting(): Boolean
|
||||
|
||||
def PerformZoneInteractionSelfReporting(): Unit = {
|
||||
if (!zoneInteractionTimer.isCancelled) {
|
||||
ZoneInteractionObject.zoneInteractions()
|
||||
}
|
||||
}
|
||||
|
||||
def PerformSelfReportRunCheck(): Unit = {
|
||||
if (TestToStartSelfReporting()) {
|
||||
StartInteractionSelfReporting()
|
||||
} else {
|
||||
StopInteractionSelfReporting()
|
||||
}
|
||||
}
|
||||
|
||||
final def StartInteractionSelfReporting(): Unit = {
|
||||
zoneInteractionTimer.cancel()
|
||||
zoneInteractionTimer = context.system.scheduler.scheduleWithFixedDelay(
|
||||
0.seconds,
|
||||
zoneInteractionIntervalDefault,
|
||||
self,
|
||||
IndependentZoneInteraction.InteractionTick
|
||||
)
|
||||
}
|
||||
|
||||
final def StartInteractionSelfReporting(initialDelay: FiniteDuration): Unit = {
|
||||
zoneInteractionTimer.cancel()
|
||||
zoneInteractionTimer = context.system.scheduler.scheduleWithFixedDelay(
|
||||
initialDelay,
|
||||
zoneInteractionIntervalDefault,
|
||||
self,
|
||||
IndependentZoneInteraction.InteractionTick
|
||||
)
|
||||
}
|
||||
|
||||
final def TryStopInteractionSelfReporting(): Boolean = {
|
||||
if (!zoneInteractionTimer.isCancelled) {
|
||||
ZoneInteractionObject.resetInteractions()
|
||||
zoneInteractionTimer.cancel()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
final def StopInteractionSelfReporting(): Boolean = {
|
||||
ZoneInteractionObject.resetInteractions()
|
||||
zoneInteractionTimer.cancel()
|
||||
}
|
||||
|
||||
final def StopInteractionSelfReportingNoReset(): Boolean = {
|
||||
zoneInteractionTimer.cancel()
|
||||
}
|
||||
|
||||
final def ZoneInteractionSelfReportingIsRunning: Boolean = !zoneInteractionTimer.isCancelled
|
||||
}
|
||||
|
||||
object IndependentZoneInteraction {
|
||||
private case object InteractionTick
|
||||
|
||||
final case object SelfReportRunCheck
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import io.circe.parser._
|
|||
import net.psforever.objects.{GlobalDefinitions, LocalLockerItem, LocalProjectile}
|
||||
import net.psforever.objects.definition.BasicDefinition
|
||||
import net.psforever.objects.guid.selector.{NumberSelector, RandomSelector, SpecificSelector}
|
||||
import net.psforever.objects.serverobject.dome.{ForceDomeDefinition, ForceDomePhysics}
|
||||
import net.psforever.objects.serverobject.doors.{Door, DoorDefinition, SpawnTubeDoor}
|
||||
import net.psforever.objects.serverobject.generator.Generator
|
||||
import net.psforever.objects.serverobject.llu.{CaptureFlagSocket, CaptureFlagSocketDefinition}
|
||||
|
|
@ -100,17 +101,9 @@ object Zones {
|
|||
"PathPoints"
|
||||
)(ZipLinePath.apply)
|
||||
|
||||
// monolith, hst, warpgate are ignored for now as the scala code isn't ready to handle them.
|
||||
// BFR terminals/doors are ignored as top level elements as sanctuaries have them with no associated building. (repair_silo also has this problem, but currently is ignored in the AmenityExtrator project)
|
||||
// Force domes have GUIDs but are currently classed as separate entities. The dome is controlled by sending GOAM 44 / 48 / 52 to the building GUID
|
||||
private val ignoredEntities = Seq(
|
||||
"monolith",
|
||||
"force_dome_dsp_physics",
|
||||
"force_dome_comm_physics",
|
||||
"force_dome_cryo_physics",
|
||||
"force_dome_tech_physics",
|
||||
"force_dome_amp_physics"
|
||||
)
|
||||
private val ignoredEntities = Seq("monolith")
|
||||
|
||||
private val towerTypes = Seq("tower_a", "tower_b", "tower_c")
|
||||
private val facilityTypes = Seq("amp_station", "cryo_facility", "comm_station", "comm_station_dsp", "tech_plant")
|
||||
|
|
@ -129,6 +122,13 @@ object Zones {
|
|||
"vt_spawn",
|
||||
"vt_vehicle"
|
||||
)
|
||||
private val forceDomeTypes = Seq(
|
||||
"force_dome_dsp_physics",
|
||||
"force_dome_comm_physics",
|
||||
"force_dome_cryo_physics",
|
||||
"force_dome_tech_physics",
|
||||
"force_dome_amp_physics"
|
||||
)
|
||||
private val cavernBuildingTypes = Seq(
|
||||
"ceiling_bldg_a",
|
||||
"ceiling_bldg_b",
|
||||
|
|
@ -382,11 +382,27 @@ object Zones {
|
|||
|
||||
createObjects(
|
||||
zoneMap,
|
||||
zoneObjects.filterNot { _.objectType.startsWith("bfr_") },
|
||||
zoneObjects.filterNot { obj => obj.objectType.startsWith("bfr_") || forceDomeTypes.contains(obj.objectType) },
|
||||
ownerGuid = 0,
|
||||
None,
|
||||
turretWeaponGuid
|
||||
)
|
||||
//force dome physics objects have no owner
|
||||
//for our benefit, we can attach them as amenities to the zone's capitol facility
|
||||
zoneObjects
|
||||
.find { obj => forceDomeTypes.contains(obj.objectType) }
|
||||
.foreach { forceDome =>
|
||||
structures
|
||||
.find { structure => Building.Capitols.contains(structure.objectName) }
|
||||
.foreach { structure =>
|
||||
val definition = DefinitionUtil.fromString(forceDome.objectType).asInstanceOf[ForceDomeDefinition]
|
||||
zoneMap.addLocalObject(
|
||||
forceDome.guid,
|
||||
ForceDomePhysics.Constructor(definition),
|
||||
owningBuildingGuid = structure.guid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
lattice.asObject.get(mapid).foreach { obj =>
|
||||
obj.asArray.get.foreach { entry =>
|
||||
|
|
@ -716,7 +732,6 @@ object Zones {
|
|||
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue