mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-03-23 14:20:45 +00:00
Merge f5d7fed1cf into 697547da25
This commit is contained in:
commit
8422ddf68a
63 changed files with 2094 additions and 718 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 */ }
|
||||
|
||||
|
|
|
|||
|
|
@ -466,7 +466,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1060,7 +1060,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
|
||||
|
|
@ -1930,7 +1930,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)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,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
|
||||
|
|
@ -1286,6 +1287,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(),
|
||||
|
|
|
|||
|
|
@ -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,65 @@ 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_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_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_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_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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,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 +24,46 @@ 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 =>
|
||||
isVulnerable = true
|
||||
|
||||
case 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 =>
|
||||
isVulnerable = true
|
||||
|
||||
case Damageable.MakeInvulnerable =>
|
||||
isVulnerable = false
|
||||
|
||||
case Vitality.Damage(damage_func) =>
|
||||
val obj = DamageableObject
|
||||
if (obj.CanDamage) {
|
||||
PerformDamage(obj, damage_func)
|
||||
}
|
||||
PerformDamageIfVulnerable(DamageableObject, damage_func)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +88,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,494 @@
|
|||
// 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 base of the force dome represented by a literal "dome" (half of a sphere).
|
||||
* @see `Zone.distanceCheck`
|
||||
* @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 centerPos @ Vector3(centerX, centerY, centerZ) = obj1.Position
|
||||
val Vector3(targetX, targetY, _) = obj2.Position.xy - centerPos.xy //deltas of segment of target to dome
|
||||
lazy val checkForIntersection = segments.exists { case (point1, point2) =>
|
||||
//want targets within the perimeter; if there's an intersection, target is outside of the perimeter
|
||||
segmentIntersectionTestPerSegment(centerX, centerY, targetX, targetY, point1.x, point1.y, point2.x, point2.y)
|
||||
}
|
||||
segments.nonEmpty && !checkForIntersection && (obj2.Position.z <= centerZ || Zone.distanceCheck(obj1, obj2, math.pow(obj1.Definition.UseRadius, 2).toFloat))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 bx = segmentPoint1x - segmentPoint2x //delta-x of segment
|
||||
val by = segmentPoint1y - segmentPoint2y //delta-y of segment
|
||||
val cx = pointX - segmentPoint1x //delta-x of hypotenuse of triangle formed by center, segment endpoint, and intersection point
|
||||
val cy = pointY - segmentPoint1y //delta-y of hypotenuse of triangle formed by center, segment endpoint, and intersection point
|
||||
val alphaNumerator = by * cx - bx * cy
|
||||
val commonDenominator = targetY * bx - targetX * by
|
||||
val betaNumerator = targetX * cy - targetY * cx
|
||||
if (
|
||||
commonDenominator > 0 &&
|
||||
(alphaNumerator < 0 || alphaNumerator > commonDenominator || betaNumerator < 0 || betaNumerator > commonDenominator)
|
||||
) {
|
||||
false
|
||||
} else if (
|
||||
commonDenominator < 0 &&
|
||||
(alphaNumerator > 0 || alphaNumerator < commonDenominator || betaNumerator > 0 || betaNumerator < commonDenominator)
|
||||
) {
|
||||
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,53 @@
|
|||
// 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()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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, but that's not a consideration */
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ 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(
|
||||
|
|
@ -767,9 +743,29 @@ class VehicleControl(vehicle: Vehicle)
|
|||
}
|
||||
|
||||
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 +775,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")
|
||||
|
|
@ -127,6 +120,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",
|
||||
|
|
@ -380,11 +380,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 =>
|
||||
|
|
@ -710,7 +726,6 @@ object Zones {
|
|||
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue