Lump of Coal (#982)

* preliminary elements needed to battle frame robotics; mostly from previous branch

* introduction of FrameVehicleStateMessage and anticipated event system paths for BFR's; spawning amenities for BFR's are parsed and built from the zonemap files, but their coordinates are currently incorrect, and the resulting entity will not function atm

* bfr's spawn correctly; default arm weapons will spawn correctly; bfr rearm terminal added but arm swap not working correctly; bfr shields charge if not full; proper separation of vehicle spawn pad types

* arm weapon swapping in bfr's; swapped weapons switch, contextually, to either *_left or to *_right depending on the mounting; partial support for entities that do not have an OCDM packet form

* crouching improves shield regeneration

* some projectiles damage the bfr regardless of its shield

* delay the final vehicle explosion; start of vehicle subsystems

* handling for bfr shield ui updates; more of vehicle subsystems; corrections to TradeMessage packet; clarifications for FrameVehicleStateMessage package; report on flight status of bfr's

* control agency support for vehicle subsystems for arm weapon fire control

* vehicle capacitor, for what it's worth; shield and capacitor are influenced by recharge freeze and drain

* initial packet and tests for AvatarAwardMessage; update the fields of FreindsResponse, DetailedCharacterData, and LoadoutType for FavoritesMessage; corrections to intiailization packets in SessionActor; players start as imprinted by default

* support for GOAM and GAM integration into vehicle control agencies using a basic actor superclass; addition of vehicle subsystems; modifications to bfr control agency to allow for weapon handiness and subsystem control; fixed Fit mapping for vehicle override; made mountable seat transcoders independent

* delayed explosions to accompany the delayed death for the bfr; bfr terminal window closes on successful purchase

* the bfr armor siphon works

* clarification for bfr inventory item manipulation; corrections to length of bfr transcoder for flight variants; everything else in in support of the various arm weapons that can be assigned to the bfr, including damage proxy support for causing/interacting with/cleaning up after radiation cloud projectiles

* fixed the apc emp burst; fixed bfr arm weapon manipulation for activated subsystem; armor and ntu siphon support

* battleframe loadouts available upon vehicle spawn (vs and tr only)

* adb values for siphons; subsystem update message; some repairs

* cargo vehicles are subject to radiation damage; damage for battleframes are different depending on shield evasion status; battleframe loadout deleting supported; bfr kill box; automatically wire bfr sheds, includeing the ones in sanctuary

* proper bfr spawn angles; bfr vehicle timers; projectiles are no longer radiation clouds by default; better remote projectile cleanup; resolving incorrect weapon arm enabled states for bfrs

* added tests for FrameVehicleState and GenericObjectActionAtPosition; pass around maximum sector for zone interactions

* changed the triggers for the stamina regeneration timer

* potential fix for issue related to finding arm weapon mounts

* modifications to how vehicle subsystems are automated; jammer field updates; support and passing around custom block map ranges; does include activated dev tests for battleframe PAM, which will need to be stripped out later

* commit while working on subsystems mk2

* subsystems fail when jammed; an unoccupied bfr does not have shields active; pulling a bfr of one variant should block the other variant too

* fix distance check with radiation clouds; blocked bfr weaponry from anywhere but bfr arm mounts and cursor; ammunition depletion of aphelion laser; bfr shields deactivates when unoccupied

* significant modifications to vehicle subsystem operations; disambiguation of weapon subsystems; debuffs to charge rate and use rate for the capacitor and shield of bfr; test for ComponentDamageMessage; somewhat proper jammering operations for bfr
This commit is contained in:
Fate-JH 2022-01-27 09:57:51 -05:00 committed by GitHub
parent 46763b7877
commit 6ae0b44848
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
140 changed files with 10426 additions and 2462 deletions

View file

@ -694,7 +694,7 @@ class PacketCodingActorKTest extends ActorTest {
0L,
0L,
0L,
Some(DCDExtra2(0, 0)),
Some(ImprintingProgress(0, 0)),
Nil,
Nil,
false,

View file

@ -221,7 +221,7 @@ object VehicleSpawnPadControlTest {
val terminal = Terminal(GlobalDefinitions.vehicle_terminal_combined)
val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy)
val weapon = vehicle.WeaponControlledFromSeat(1).get.asInstanceOf[Tool]
val weapon = vehicle.WeaponControlledFromSeat(1).head.asInstanceOf[Tool]
val guid: NumberPoolHub = new NumberPoolHub(MaxNumberSource(5))
guid.AddPool("test-pool", (0 to 5).toList)
guid.register(vehicle, "test-pool")

View file

@ -41,6 +41,12 @@
"Max": 39500,
"Selector": "random"
},
{
"Name": "rc-projectiles",
"Start": 39701,
"Max": 40000,
"Selector": "random"
},
{
"Name": "projectiles",
"Start": 40100,

View file

@ -40,5 +40,11 @@
"Start": 37501,
"Max": 40099,
"Selector": "random"
},
{
"Name": "rc-projectiles",
"Start": 64001,
"Max": 64300,
"Selector": "random"
}
]

View file

@ -40,5 +40,11 @@
"Start": 36601,
"Max": 39600,
"Selector": "random"
},
{
"Name": "rc-projectiles",
"Start": 64001,
"Max": 64300,
"Selector": "random"
}
]

View file

@ -377,7 +377,12 @@ class AvatarActor(
implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None),
locker = locker
))
defaultStaminaRegen(initialDelay = 0.5f seconds)
// if we need to start stamina regeneration
tryRestoreStaminaForSession(stamina = 1) match {
case Some(sess) =>
defaultStaminaRegen(initialDelay = 0.5f seconds)
case _ => ;
}
replyTo ! AvatarLoginResponse(avatar)
case Failure(e) =>
log.error(e)("db failure")
@ -386,8 +391,7 @@ class AvatarActor(
case ReplaceAvatar(newAvatar) =>
replaceAvatar(newAvatar)
staminaRegenTimer.cancel()
defaultStaminaRegen(initialDelay = 0.5f seconds)
startIfStoppedStaminaRegen(initialDelay = 0.5f seconds)
Behaviors.same
case AddFirstTimeEvent(event) =>
@ -672,6 +676,18 @@ class AvatarActor(
throwLoadoutFailure(s"no owned vehicle found for ${player.Name}")
}
)
case LoadoutType.Battleframe =>
(
number + 15,
player.Zone.GUID(avatar.vehicle) match {
case Some(vehicle: Vehicle)
if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) =>
storeVehicleLoadout(player, name, number + 5, vehicle)
case _ =>
throwLoadoutFailure(s"no owned battleframe found for ${player.Name}")
}
)
}
result.onComplete {
case Success(loadout) =>
@ -697,9 +713,8 @@ class AvatarActor(
)
)
case LoadoutType.Vehicle if avatar.loadouts(number + 10).nonEmpty =>
val lineNo = number + 10
(
lineNo,
number + 10,
ctx.run(
query[persistence.Vehicleloadout]
.filter(_.avatarId == lift(avatar.id))
@ -707,8 +722,18 @@ class AvatarActor(
.delete
)
)
case LoadoutType.Battleframe if avatar.loadouts(number + 15).nonEmpty =>
(
number + 15,
ctx.run(
query[persistence.Vehicleloadout]
.filter(_.avatarId == lift(avatar.id))
.filter(_.loadoutNumber == lift(number + 5))
.delete
)
)
case _ =>
(number, throwLoadoutFailure("unhandled loadout type or no loadout"))
(number, throwLoadoutFailure(msg = "unhandled loadout type or no loadout"))
}
result.onComplete {
case Success(_) =>
@ -739,7 +764,7 @@ class AvatarActor(
Avatar.purchaseCooldowns.get(item) match {
case Some(cooldown) =>
//only send for items with cooldowns
newTimes = newTimes.updated(item.Name, time)
newTimes = newTimes.updated(name, time)
updatePurchaseTimer(name, cooldown.toSeconds, unk1 = true)
case _ => ;
}
@ -782,14 +807,8 @@ class AvatarActor(
implants = avatar.implants.updated(slot, Some(implant.copy(active = true)))
))
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(
session.get.player.GUID,
ImplantAction.Activation,
slot,
1
)
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Activation, slot, 1)
)
// Activation sound / effect
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
@ -799,9 +818,7 @@ class AvatarActor(
implant.definition.implantType.value * 2 + 1
)
)
implantTimers.get(slot).foreach(_.cancel())
val interval = implant.definition.GetCostIntervalByExoSuit(session.get.player.ExoSuit).milliseconds
// TODO costInterval should be an option ^
if (interval.toMillis > 0) {
@ -880,26 +897,17 @@ class AvatarActor(
tryRestoreStaminaForSession(stamina) match {
case Some(sess) =>
actuallyRestoreStamina(stamina, sess)
defaultStaminaRegen(initialDelay = 0.5f seconds)
case _ => ;
}
Behaviors.same
case RestoreStaminaPeriodically(stamina) =>
tryRestoreStaminaForSession(stamina) match {
case Some(sess) =>
actuallyRestoreStaminaIfStationary(stamina, sess)
case _ => ;
}
defaultStaminaRegen(initialDelay = 0.5f seconds)
restoreStaminaPeriodically(stamina)
Behaviors.same
case ConsumeStamina(stamina) =>
if (stamina > 0) {
consumeThisMuchStamina(stamina)
if(staminaRegenTimer.isCancelled) {
defaultStaminaRegen(initialDelay = 0.5f seconds)
}
} else {
log.warn(s"consumed stamina must be larger than 0, but is: $stamina")
}
@ -1053,35 +1061,52 @@ class AvatarActor(
}
def tryRestoreStaminaForSession(stamina: Int): Option[Session] = {
session match {
case out @ Some(_) if !avatar.staminaFull && stamina > 0 => out
case _ => None
(session, _avatar) match {
case (out @ Some(_), Some(a)) if !a.staminaFull && stamina > 0 => out
case _ => None
}
}
def actuallyRestoreStaminaIfStationary(stamina: Int, session: Session): Unit = {
if (session.player.VehicleSeated.nonEmpty || !(session.player.isMoving || session.player.Jumping)) {
val player = session.player
if (player.VehicleSeated.nonEmpty || !(player.isMoving || player.Jumping)) {
actuallyRestoreStamina(stamina, session)
}
}
def actuallyRestoreStamina(stamina: Int, session: Session): Unit = {
val totalStamina = math.min(avatar.maxStamina, avatar.stamina + stamina)
val isFatigued = if (avatar.fatigued && totalStamina >= 20) {
val pguid = session.player.GUID
avatar.implants.zipWithIndex.foreach {
case (Some(_), slot) =>
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(pguid, ImplantAction.OutOfStamina, slot, 0)
)
case _ => ()
val originalStamina = avatar.stamina
val maxStamina = avatar.maxStamina
val totalStamina = math.min(maxStamina, originalStamina + stamina)
if (originalStamina < totalStamina) {
val originalFatigued = avatar.fatigued
val isFatigued = totalStamina < 20
avatar = avatar.copy(stamina = totalStamina, fatigued = isFatigued)
if (totalStamina == maxStamina) {
staminaRegenTimer.cancel()
staminaRegenTimer = Default.Cancellable
}
if (session.player.HasGUID) {
val guid = session.player.GUID
if (originalFatigued && !isFatigued) {
avatar.implants.zipWithIndex.foreach {
case (Some(_), slot) =>
sessionActor ! SessionActor.SendResponse(AvatarImplantMessage(guid, ImplantAction.OutOfStamina, slot, 0))
case _ => ;
}
}
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(guid, 2, totalStamina))
}
false
} else {
avatar.fatigued
}
avatarCopy(avatar.copy(stamina = totalStamina, fatigued = isFatigued))
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.player.GUID, 2, totalStamina))
}
def restoreStaminaPeriodically(stamina: Int): Unit = {
tryRestoreStaminaForSession(stamina) match {
case Some(sess) =>
actuallyRestoreStaminaIfStationary(stamina, sess)
case _ => ;
}
startIfStoppedStaminaRegen(initialDelay = 0.5f seconds)
}
/**
@ -1102,6 +1127,7 @@ class AvatarActor(
val alreadyFatigued = avatar.fatigued
val becomeFatigued = !alreadyFatigued && totalStamina == 0
avatarCopy(avatar.copy(stamina = totalStamina, fatigued = alreadyFatigued || becomeFatigued))
startIfStoppedStaminaRegen(initialDelay = 0.5f seconds)
val player = session.get.player
if (player.HasGUID) {
if (becomeFatigued) {
@ -1227,20 +1253,13 @@ class AvatarActor(
avatarCopy(avatar.copy(
implants = avatar.implants.updated(slot, Some(implant.copy(active = false)))
))
// Deactivation sound / effect
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
AvatarAction.PlanetsideAttribute(session.get.player.GUID, 28, implant.definition.implantType.value * 2)
)
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(
session.get.player.GUID,
ImplantAction.Activation,
slot,
0
)
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Activation, slot, 0)
)
case None => log.error(s"requested deactivation of unknown implant $implantType")
}
@ -1501,7 +1520,7 @@ class AvatarActor(
}
vehicles <- loadVehicleLoadouts().andThen {
case out @ Success(_) => out
case Failure(_) => Future(Array.fill[Option[Loadout]](5)(None).toSeq)
case Failure(_) => Future(Array.fill[Option[Loadout]](10)(None).toSeq)
}
} yield infantry ++ vehicles
}
@ -1533,7 +1552,8 @@ class AvatarActor(
.run(query[persistence.Vehicleloadout].filter(_.avatarId == lift(avatar.id)))
.map { loadouts =>
loadouts.map { loadout =>
val toy = new Vehicle(DefinitionUtil.idToDefinition(loadout.vehicle).asInstanceOf[VehicleDefinition])
val definition = DefinitionUtil.idToDefinition(loadout.vehicle).asInstanceOf[VehicleDefinition]
val toy = new Vehicle(definition)
buildContainedEquipmentFromClob(toy, loadout.items)
val result = (loadout.loadoutNumber, Loadout.Create(toy, loadout.name))
@ -1544,29 +1564,36 @@ class AvatarActor(
result
}
}
.map { loadouts => (0 until 5).map { index => loadouts.find(_._1 == index).map(_._2) } }
.map { loadouts => (0 until 10).map { index => loadouts.find(_._1 == index).map(_._2) } }
}
def refreshLoadouts(loadouts: Iterable[(Option[Loadout], Int)]): Unit = {
loadouts.map {
case (Some(loadout: InfantryLoadout), index) =>
FavoritesMessage(
LoadoutType.Infantry,
FavoritesMessage.Infantry(
session.get.player.GUID,
index,
loadout.label,
InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype)
)
case (Some(loadout: VehicleLoadout), index)
if GlobalDefinitions.isBattleFrameVehicle(loadout.vehicle_definition) =>
FavoritesMessage.Battleframe(
session.get.player.GUID,
index - 15,
loadout.label,
VehicleLoadout.DetermineBattleframeSubtype(loadout.vehicle_definition)
)
case (Some(loadout: VehicleLoadout), index) =>
FavoritesMessage(
LoadoutType.Vehicle,
FavoritesMessage.Vehicle(
session.get.player.GUID,
index - 10,
loadout.label,
0
loadout.label
)
case (_, index) =>
val (mtype, lineNo) = if (index < 10) {
val (mtype, lineNo) = if (index > 14) {
(LoadoutType.Battleframe, index - 15)
} else if (index < 10) {
(LoadoutType.Infantry, index)
} else {
(LoadoutType.Vehicle, index - 10)
@ -1585,29 +1612,38 @@ class AvatarActor(
avatar.loadouts.lift(line) match {
case Some(Some(loadout: InfantryLoadout)) =>
sessionActor ! SessionActor.SendResponse(
FavoritesMessage(
LoadoutType.Infantry,
FavoritesMessage.Infantry(
session.get.player.GUID,
line,
loadout.label,
InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype)
)
)
case Some(Some(loadout: VehicleLoadout))
if GlobalDefinitions.isBattleFrameVehicle(loadout.vehicle_definition) =>
sessionActor ! SessionActor.SendResponse(
FavoritesMessage.Battleframe(
session.get.player.GUID,
line - 15,
loadout.label,
VehicleLoadout.DetermineBattleframeSubtype(loadout.vehicle_definition)
)
)
case Some(Some(loadout: VehicleLoadout)) =>
sessionActor ! SessionActor.SendResponse(
FavoritesMessage(
LoadoutType.Vehicle,
FavoritesMessage.Vehicle(
session.get.player.GUID,
line - 10,
loadout.label,
0
loadout.label
)
)
case Some(None) =>
val (mtype, lineNo) = if (line < 10) {
(LoadoutType.Infantry, line)
val (mtype, lineNo, subtype) = if (line > 14) {
(LoadoutType.Battleframe, line - 15, Some(0))
} else if (line < 10) {
(LoadoutType.Infantry, line, Some(0))
} else {
(LoadoutType.Vehicle, line - 10)
(LoadoutType.Vehicle, line - 10, None)
}
sessionActor ! SessionActor.SendResponse(
FavoritesMessage(
@ -1615,7 +1651,7 @@ class AvatarActor(
session.get.player.GUID,
lineNo,
"",
0
subtype
)
)
case _ => ;
@ -1679,15 +1715,18 @@ class AvatarActor(
}
}
def startIfStoppedStaminaRegen(initialDelay: FiniteDuration): Unit = {
if (staminaRegenTimer.isCancelled) {
defaultStaminaRegen(initialDelay)
}
}
def defaultStaminaRegen(initialDelay: FiniteDuration): Unit = {
staminaRegenTimer.cancel()
staminaRegenTimer = if (!avatar.staminaFull) {
context.system.scheduler.scheduleWithFixedDelay(initialDelay, 0.5 seconds)(() => {
context.self ! RestoreStaminaPeriodically(1)
})
} else {
Default.Cancellable
}
val restoreStaminaFunc: Int => Unit = restoreStaminaPeriodically
staminaRegenTimer = context.system.scheduler.scheduleWithFixedDelay(initialDelay, delay = 0.5 seconds)(() => {
restoreStaminaFunc(1)
})
}
// same as in SA, this really doesn't belong here
@ -1723,7 +1762,7 @@ class AvatarActor(
}
def resolveSharedPurchaseTimeNames(pair: (BasicDefinition, String)): Seq[(BasicDefinition, String)] = {
val (_, name) = pair
val (definition, name) = pair
if (name.matches("(tr|nc|vs)hev_.+") && Config.app.game.sharedMaxCooldown) {
val faction = name.take(2)
(if (faction.equals("nc")) {
@ -1738,7 +1777,22 @@ class AvatarActor(
Seq(s"${faction}hev_antipersonnel", s"${faction}hev_antivehicular", s"${faction}hev_antiaircraft")
)
} else {
Seq(pair)
definition match {
case vdef: VehicleDefinition
if GlobalDefinitions.isBattleFrameFlightVehicle(vdef) =>
val bframe = name.substring(0, name.indexOf('_'))
val gunner = bframe+"_gunner"
Seq((DefinitionUtil.fromString(gunner), gunner), (vdef, name))
case vdef: VehicleDefinition
if GlobalDefinitions.isBattleFrameGunnerVehicle(vdef) =>
val bframe = name.substring(0, name.indexOf('_'))
val flight = bframe+"_flight"
Seq((vdef, name), (DefinitionUtil.fromString(flight), flight))
case _ =>
Seq(pair)
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -487,8 +487,9 @@ class BuildingActor(
class FakeNtuSource(private val building: Building)
extends PlanetSideServerObject
with NtuContainer {
override def NtuCapacitor = Float.MaxValue
override def NtuCapacitor_=(a: Float) = Float.MaxValue
override def NtuCapacitor = Int.MaxValue.toFloat
override def NtuCapacitor_=(a: Float) = Int.MaxValue.toFloat
override def MaxNtuCapacitor = Int.MaxValue.toFloat
override def Faction = building.Faction
override def Zone = building.Zone
override def Definition = null

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,7 @@ package net.psforever.objects
import akka.actor.{Actor, ActorRef}
import net.psforever.actors.commands.NtuCommand
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.transfer.{TransferBehavior, TransferContainer}
object Ntu {
@ -34,12 +35,26 @@ object Ntu {
final case class Grant(src: NtuContainer, amount: Float)
}
trait NtuContainerOwner {
def getNtuContainer: Option[NtuContainer]
}
trait NtuContainer extends TransferContainer {
def NtuCapacitor: Float
def NtuCapacitor_=(value: Float): Float
def Definition: NtuContainerDefinition
def NtuCapacitorScaled: Int = {
if (Definition.MaxNtuCapacitor > 0) {
scala.math.ceil((NtuCapacitor / Definition.MaxNtuCapacitor) * 10).toInt
} else {
0
}
}
def MaxNtuCapacitor: Float
def Definition: ObjectDefinition with NtuContainerDefinition
}
trait CommonNtuContainer extends NtuContainer {
@ -51,8 +66,6 @@ trait CommonNtuContainer extends NtuContainer {
ntuCapacitor = scala.math.max(0, scala.math.min(value, Definition.MaxNtuCapacitor))
NtuCapacitor
}
def Definition: NtuContainerDefinition
}
trait NtuContainerDefinition {

View file

@ -2,6 +2,7 @@
package net.psforever.objects
import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.ballistics.InteractWithRadiationClouds
import net.psforever.objects.ce.{Deployable, InteractWithMines}
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
@ -15,7 +16,7 @@ import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation}
import net.psforever.objects.zones.{InteractsWithZone, ZoneAware, Zoning}
import net.psforever.types.{PlanetSideGUID, _}
@ -36,6 +37,7 @@ class Player(var avatar: Avatar)
with MountableEntity {
interaction(new InteractWithEnvironment())
interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10))
interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))
private var backpack: Boolean = false
private var released: Boolean = false
@ -599,11 +601,11 @@ object Player {
private class InteractWithMinesUnlessSpectating(
private val obj: Player,
range: Float
override val range: Float
) extends InteractWithMines(range) {
override def interaction(target: InteractsWithZone): Unit = {
override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
if (!obj.spectator) {
super.interaction(target)
super.interaction(sector, target)
}
}
}

View file

@ -3,6 +3,7 @@ package net.psforever.objects
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.equipment.{EffectTarget, TargetValidation}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.{Vitality, VitalityDefinition}
@ -28,6 +29,30 @@ object SpecialEmp {
DamageAtEdge = 1.0f
DamageRadius = 5f
AdditionalEffect = true
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Player,
EffectTarget.Validation.Player
) -> 1000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.AMS
) -> 5000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.MotionSensor
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.Spitfire
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Turret,
EffectTarget.Validation.Turret
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.VehicleNotAMS
) -> 10000
Modifiers = MaxDistanceCutoff
}
@ -37,7 +62,6 @@ object SpecialEmp {
MaxHealth = 1
Damageable = false
Repairable = false
explodes = true
innateDamage = emp
}

View file

@ -21,15 +21,14 @@ class Tool(private val toolDef: ToolDefinition)
extends Equipment
with FireModeSwitch[FireModeDefinition]
with JammableUnit {
private var tdef = toolDef
/** index of the current fire mode on the `ToolDefinition`'s list of fire modes */
private var fireModeIndex: Int = toolDef.DefaultFireModeIndex
private var fireModeIndex: Int = tdef.DefaultFireModeIndex
/** current ammunition slot being used by this fire mode */
private var ammoSlots: List[Tool.FireModeSlot] = List.empty
var lastDischarge: Long = 0
Tool.LoadDefinition(this)
Tool.LoadDefinition(tool = this)
def FireModeIndex: Int = fireModeIndex
@ -114,7 +113,7 @@ class Tool(private val toolDef: ToolDefinition)
def MaxAmmoSlot: Int = ammoSlots.length
def Definition: ToolDefinition = toolDef
def Definition: ToolDefinition = tdef
override def toString: String = Tool.toString(this)
}
@ -131,9 +130,20 @@ object Tool {
* @param tool the `Tool` being initialized
*/
def LoadDefinition(tool: Tool): Unit = {
val tdef: ToolDefinition = tool.Definition
val maxSlot = tdef.FireModes.maxBy(fmode => fmode.AmmoSlotIndex).AmmoSlotIndex
val tdef = tool.Definition
val maxSlot = tdef.FireModes.maxBy(fmode => fmode.AmmoSlotIndex).AmmoSlotIndex
tool.ammoSlots = buildFireModes(tdef, (0 to maxSlot).iterator, tdef.FireModes.toList)
tool.fireModeIndex = tdef.DefaultFireModeIndex
}
/**
* Substitute this `Definition` for the one that was originally provided for this entity.
* Calling this will not reconstruct the internal fields of the entity.
* @param tool the `Tool` being modified
* @param tdef the definition used to override the definition that was previously assigned this `Tool`;
* WILL override the assignment in the original constructor
*/
def LoadDefinition(tool: Tool, tdef: ToolDefinition): Unit = {
tool.tdef = tdef
}
@tailrec private def buildFireModes(
@ -226,7 +236,7 @@ object Tool {
def MaxMagazine(): Int = {
fdef.CustomMagazine.get(AmmoType) match {
case Some(value) => value
case None => fdef.Magazine
case None => fdef.DefaultMagazine
}
}

View file

@ -3,7 +3,7 @@ package net.psforever.objects
import net.psforever.objects.ce.InteractWithMines
import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition}
import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile}
import net.psforever.objects.serverobject.mount.{MountableEntity, Seat, SeatDefinition}
import net.psforever.objects.serverobject.PlanetSideServerObject
@ -19,8 +19,10 @@ import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.InteractsWithZone
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import scala.annotation.tailrec
import scala.concurrent.duration.FiniteDuration
import scala.util.{Success, Try}
@ -89,13 +91,13 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
with AuraContainer
with MountableEntity {
interaction(new InteractWithEnvironment())
interaction(new InteractWithMines(range = 30))
interaction(new InteractWithMines(range = 20))
interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20))
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var shields: Int = 0
private var decal: Int = 0
private var trunkAccess: Option[PlanetSideGUID] = None
private var jammered: Boolean = false
private var cloaked: Boolean = false
private var flying: Option[Int] = None
@ -107,9 +109,10 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
*/
private val groupPermissions: Array[VehicleLockState.Value] =
Array(VehicleLockState.Locked, VehicleLockState.Empire, VehicleLockState.Empire, VehicleLockState.Locked)
private var cargoHolds: Map[Int, Cargo] = Map.empty
private var utilities: Map[Int, Utility] = Map()
private val trunk: GridInventory = GridInventory()
private var cargoHolds: Map[Int, Cargo] = Map.empty
private var utilities: Map[Int, Utility] = Map.empty
private var subsystems: List[VehicleSubsystem] = Nil
private val trunk: GridInventory = GridInventory()
/*
* Records the GUID of the cargo vehicle (galaxy/lodestar) this vehicle is stored in for DismountVehicleCargoMsg use
@ -128,7 +131,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
* @see `Vehicle.LoadDefinition`
*/
protected def LoadDefinition(): Unit = {
Vehicle.LoadDefinition(this)
Vehicle.LoadDefinition(vehicle = this)
}
def Faction: PlanetSideEmpire.Value = {
@ -173,13 +176,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
Decal
}
def Jammered: Boolean = jammered
def Jammered_=(jamState: Boolean): Boolean = {
jammered = jamState
Jammered
}
def Cloaked: Boolean = cloaked
def Cloaked_=(isCloaked: Boolean): Boolean = {
@ -198,14 +194,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
Flying
}
def NtuCapacitorScaled: Int = {
if (Definition.MaxNtuCapacitor > 0) {
scala.math.ceil((NtuCapacitor / Definition.MaxNtuCapacitor) * 10).toInt
} else {
0
}
}
def Capacitor: Int = capacitor
def Capacitor_=(value: Int): Int = {
@ -291,7 +279,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
} else {
Seat(seatNumber) match {
case Some(_) =>
Definition.controlledWeapons.get(seatNumber) match {
Definition.controlledWeapons().get(seatNumber) match {
case Some(_) =>
Some(AccessPermissionGroup.Gunner)
case None =>
@ -341,6 +329,42 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
}
}
def Subsystems(): List[VehicleSubsystem] = subsystems
def Subsystems(sys: VehicleSubsystemEntry): Option[VehicleSubsystem] = subsystems.find { _.sys == sys }
def Subsystems(sys: String): Option[VehicleSubsystem] = subsystems.find { _.sys.name.contains(sys) }
def SubsystemMessages(): List[PlanetSideGamePacket] = {
subsystems
.filter { sub => sub.Enabled != sub.sys.defaultState }
.flatMap { _.getMessage(vehicle = this) }
}
def SubsystemStatus(sys: String): Option[Boolean] = {
val elems = sys.split("\\.")
if (elems.length < 2) {
None
} else {
Subsystems(elems.head) match {
case Some(sub) => sub.stateOfStatus(elems(1))
case None => Some(false)
}
}
}
def SubsystemStatusMultiplier(sys: String): Float = {
val elems = sys.split("\\.")
if (elems.length < 2) {
1f
} else {
Subsystems(elems.head) match {
case Some(sub) => sub.multiplierOfStatus(elems(1))
case None => 1f
}
}
}
override def DeployTime = Definition.DeployTime
override def UndeployTime = Definition.UndeployTime
@ -352,35 +376,58 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
override def Slot(slotNum: Int): EquipmentSlot = {
weapons
.get(slotNum)
// .orElse(utilities.get(slotNum) match {
// case Some(_) =>
// //TODO what do now?
// None
// case None => ;
// None
// })
.orElse(Some(Inventory.Slot(slotNum)))
.get
}
override def SlotMapResolution(slot: Int): Int = {
if (GlobalDefinitions.isBattleFrameVehicle(vehicleDef)) {
//for the benefit of BFR equipment slots interacting with MoveItemMessage
if (VisibleSlots.size == 2) {
if (slot == 0) 1 else if (slot == 1) 2 else slot //*_flight
} else {
if (slot == 0) 2 else if (slot == 1) 3 else if (slot == 2) 4 else slot //*_gunner
}
} else {
slot
}
}
override def Find(guid: PlanetSideGUID): Option[Int] = {
weapons.find({
case (_, obj) =>
obj.Equipment match {
case Some(item) =>
if (item.HasGUID && item.GUID == guid) {
true
} else {
false
}
case None =>
false
case Some(item) => item.HasGUID && item.GUID == guid
case None => false
}
}) match {
case Some((index, _)) =>
case Some((index, _)) => Some(index)
case None => Inventory.Find(guid)
}
}
override def Fit(obj: Equipment): Option[Int] = {
recursiveSlotFit(weapons.iterator, obj.Size) match {
case Some(index) =>
Some(index)
case None =>
Inventory.Find(guid)
trunk.Fit(obj.Definition.Tile)
}
}
@tailrec private def recursiveSlotFit(
iter: Iterator[(Int, EquipmentSlot)],
objSize: EquipmentSize.Value
): Option[Int] = {
if (!iter.hasNext) {
None
} else {
val (index, slot) = iter.next()
if (slot.Equipment.isEmpty && slot.Size.equals(objSize)) {
Some(index)
} else {
recursiveSlotFit(iter, objSize)
}
}
}
@ -388,13 +435,10 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
weapons.get(dest) match {
case Some(slot) =>
slot.Equipment match {
case Some(item) =>
Success(List(InventoryItem(item, dest)))
case None =>
Success(List())
case Some(item) => Success(List(InventoryItem(item, dest)))
case None => Success(List())
}
case None =>
super.Collisions(dest, width, height)
case None => super.Collisions(dest, width, height)
}
}
@ -514,6 +558,8 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
override def toString: String = {
Vehicle.toString(this)
}
def MaxNtuCapacitor: Float = Definition.MaxNtuCapacitor
}
object Vehicle {
@ -558,6 +604,8 @@ object Vehicle {
*/
final case class UpdateShieldsCharge(vehicle: Vehicle)
final case class UpdateSubsystemStates(toChannel: String, stateToUpdateFor: Option[Boolean] = None)
/**
* Change a vehicle's internal ownership property to match that of the target player.
* @param player the person who will own the vehicle, or `None` if the vehicle will go unowned
@ -602,10 +650,12 @@ object Vehicle {
val vdef: VehicleDefinition = vehicle.Definition
//general stuff
vehicle.Health = vdef.DefaultHealth
vehicle.Shields = vdef.DefaultShields
vehicle.Capacitor = vdef.DefaultCapacitor
//create weapons
vehicle.weapons = vdef.Weapons.map[Int, EquipmentSlot] {
case (num: Int, definition: ToolDefinition) =>
val slot = EquipmentSlot(EquipmentSize.VehicleWeapon)
val slot = EquipmentSlot(definition.Size)
slot.Equipment = Tool(definition)
num -> slot
}.toMap
@ -628,6 +678,8 @@ object Vehicle {
utilObj.LocationOffset = vdef.UtilityOffset.get(num)
num -> obj
}.toMap
//subsystems
vehicle.subsystems = vdef.subsystems.map { entry => new VehicleSubsystem(entry) }
//trunk
vdef.TrunkSize match {
case InventoryTile.None => ;

View file

@ -4,8 +4,9 @@ package net.psforever.objects
import net.psforever.objects.ce.TelepadLike
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.transfer.TransferContainer
import net.psforever.objects.serverobject.structures.{StructureType, WarpGate}
import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.vehicles._
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.TriggeredSound
@ -210,11 +211,12 @@ object Vehicles {
* The orientation of a cargo vehicle as it is being loaded into and contained by a carrier vehicle.
* The type of carrier is not an important consideration in determining the orientation, oddly enough.
* @param vehicle the cargo vehicle
* @return the orientation as an `Integer` value;
* `0` for almost all cases
* @return the orientation;
* `1` is for unique sideways mounting;
* `0` is or straight-on mounting, valid for almost all cases
*/
def CargoOrientation(vehicle: Vehicle): Int = {
if (vehicle.Definition == GlobalDefinitions.router) {
if (vehicle.Definition == GlobalDefinitions.router || GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
1
} else {
0
@ -232,7 +234,7 @@ object Vehicles {
log.info(s"${hacker.Name} has jacked a ${target.Definition.Name}")
val zone = target.Zone
// Forcefully dismount any cargo
target.CargoHolds.foreach { case (index, cargoHold) =>
target.CargoHolds.foreach { case (_, cargoHold) =>
cargoHold.occupant match {
case Some(cargo: Vehicle) =>
cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false)
@ -339,33 +341,66 @@ object Vehicles {
}
def FindANTDischargingTarget(
obj: TransferContainer,
ntuChargingTarget: Option[TransferContainer]
): Option[TransferContainer] = {
(ntuChargingTarget match {
case out @ Some(target: NtuContainer) if {
Vector3.DistanceSquared(obj.Position.xy, target.Position.xy) < 400 //20m is generous ...
} =>
obj: TransferContainer,
ntuChargingTarget: Option[TransferContainer]
): Option[TransferContainer] = {
FindResourceSiloToDischargeInto(obj, ntuChargingTarget, radius = 20)
}
def FindBfrChargingSource(
obj: TransferContainer,
ntuChargingTarget: Option[TransferContainer]
): Option[TransferContainer] = {
//determine if we are close enough to charge from something
val position = obj.Position.xy
ntuChargingTarget.orElse(
obj.Zone
.blockMap
.sector(position, range = 20f).buildingList
.sortBy { b => Vector3.DistanceSquared(position, b.Position.xy) }
.flatMap { _.NtuSource }
.headOption
) match {
case out @ Some(_: WarpGate) =>
out
case Some(silo: ResourceSilo) if {
val radius = 20f//3.6135f
Vector3.DistanceSquared(position, silo.Position.xy) < radius * radius && obj.Faction != silo.Faction
} =>
Some(silo)
case _ =>
None
}
}
def FindBfrDischargingTarget(
obj: TransferContainer,
ntuChargingTarget: Option[TransferContainer]
): Option[TransferContainer] = {
FindResourceSiloToDischargeInto(obj, ntuChargingTarget, radius = 20) //3.6135f?
}
def FindResourceSiloToDischargeInto(
obj: TransferContainer,
ntuChargingTarget: Option[TransferContainer],
radius: Float
): Option[TransferContainer] = {
//determine if we are close enough to charge from something
val position = obj.Position.xy
ntuChargingTarget.orElse(
obj.Zone
.blockMap
.sector(position, range = 20f)
.buildingList
.sortBy { b => Vector3.DistanceSquared(position, b.Position.xy) }
.flatMap { _.NtuSource }
.headOption
) match {
case out @ Some(silo: ResourceSilo)
if Vector3.DistanceSquared(position, silo.Position.xy) < radius * radius && obj.Faction == silo.Faction =>
out
case _ =>
None
}).orElse {
val position = obj.Position.xy
obj.Zone.Buildings.values
.find { building =>
building.BuildingType == StructureType.Facility && {
val soiRadius = building.Definition.SOIRadius
Vector3.DistanceSquared(position, building.Position.xy) < soiRadius * soiRadius
}
} match {
case Some(building) =>
building.Amenities
.collect { case obj: NtuContainer => obj }
.sortBy { o => Vector3.DistanceSquared(position, o.Position.xy) < 400 } //20m is generous ...
.headOption
case None =>
None
}
}
}

View file

@ -15,15 +15,19 @@ import scala.concurrent.duration._
object Avatar {
val purchaseCooldowns: Map[BasicDefinition, FiniteDuration] = Map(
GlobalDefinitions.ams -> 5.minutes,
GlobalDefinitions.ant -> 5.minutes,
GlobalDefinitions.ant -> 4.minutes,
GlobalDefinitions.apc_nc -> 5.minutes,
GlobalDefinitions.apc_tr -> 5.minutes,
GlobalDefinitions.apc_vs -> 5.minutes,
GlobalDefinitions.aphelion_flight -> 15.minutes, //Temporarily - Default is 25 minutes
GlobalDefinitions.aphelion_gunner -> 15.minutes, //Temporarily - Default is 25 minutes
GlobalDefinitions.aurora -> 5.minutes,
GlobalDefinitions.battlewagon -> 5.minutes,
GlobalDefinitions.colossus_flight -> 15.minutes, //Temporarily - Default is 25 minutes
GlobalDefinitions.colossus_gunner -> 15.minutes, //Temporarily - Default is 25 minutes
GlobalDefinitions.dropship -> 5.minutes,
GlobalDefinitions.flail -> 5.minutes,
GlobalDefinitions.fury -> 5.minutes,
GlobalDefinitions.fury -> 2.minutes,
GlobalDefinitions.galaxy_gunship -> 15.minutes, //Temporary - Default is 10 minutes
GlobalDefinitions.lodestar -> 5.minutes,
GlobalDefinitions.liberator -> 5.minutes,
@ -32,18 +36,20 @@ object Avatar {
GlobalDefinitions.magrider -> 5.minutes,
GlobalDefinitions.mediumtransport -> 5.minutes,
GlobalDefinitions.mosquito -> 5.minutes,
GlobalDefinitions.peregrine_flight -> 15.minutes, //Temporarily - Default is 25 minutes
GlobalDefinitions.peregrine_gunner -> 15.minutes, //Temporarily - Default is 25 minutes
GlobalDefinitions.phantasm -> 5.minutes,
GlobalDefinitions.prowler -> 5.minutes,
GlobalDefinitions.quadassault -> 5.minutes,
GlobalDefinitions.quadstealth -> 5.minutes,
GlobalDefinitions.quadassault -> 2.minutes,
GlobalDefinitions.quadstealth -> 2.minutes,
GlobalDefinitions.router -> 5.minutes,
GlobalDefinitions.switchblade -> 5.minutes,
GlobalDefinitions.skyguard -> 5.minutes,
GlobalDefinitions.threemanheavybuggy -> 5.minutes,
GlobalDefinitions.skyguard -> 2.minutes,
GlobalDefinitions.threemanheavybuggy -> 2.minutes,
GlobalDefinitions.thunderer -> 5.minutes,
GlobalDefinitions.two_man_assault_buggy -> 5.minutes,
GlobalDefinitions.twomanhoverbuggy -> 5.minutes,
GlobalDefinitions.twomanheavybuggy -> 5.minutes,
GlobalDefinitions.two_man_assault_buggy -> 2.minutes,
GlobalDefinitions.twomanhoverbuggy -> 2.minutes,
GlobalDefinitions.twomanheavybuggy -> 2.minutes,
GlobalDefinitions.vanguard -> 5.minutes,
GlobalDefinitions.vulture -> 5.minutes,
GlobalDefinitions.wasp -> 5.minutes,
@ -89,7 +95,7 @@ case class Avatar(
fatigued: Boolean = false,
cosmetics: Option[Set[Cosmetic]] = None,
certifications: Set[Certification] = Set(),
loadouts: Seq[Option[Loadout]] = Seq.fill(15)(None),
loadouts: Seq[Option[Loadout]] = Seq.fill(20)(None),
squadLoadouts: Seq[Option[SquadLoadout]] = Seq.fill(10)(None),
implants: Seq[Option[Implant]] = Seq(None, None, None),
locker: LockerContainer = Avatar.makeLocker(),

View file

@ -74,7 +74,7 @@ case object Certification extends IntEnum[Certification] {
case object GroundSupport extends Certification(value = 17, name = "ground_support", cost = 2)
case object BattleFrameRobotics
extends Certification(value = 18, name = "TODO2", cost = 4, requires = Set(ArmoredAssault2)) // TODO name
extends Certification(value = 18, name = "bfr_basic", cost = 4, requires = Set(ArmoredAssault2))
case object Flail extends Certification(value = 19, name = "flail", cost = 1, requires = Set(ArmoredAssault2))
@ -87,10 +87,10 @@ case object Certification extends IntEnum[Certification] {
case object GalaxyGunship extends Certification(value = 23, name = "gunship", cost = 2, requires = Set(AirSupport))
case object BFRAntiAircraft
extends Certification(value = 24, name = "TODO3", cost = 1, requires = Set(BattleFrameRobotics))
extends Certification(value = 24, name = "bfr_aa_gunnery", cost = 1, requires = Set(BattleFrameRobotics))
case object BFRAntiInfantry
extends Certification(value = 25, name = "TODO4", cost = 1, requires = Set(BattleFrameRobotics)) // TODO name
extends Certification(value = 25, name = "bfr_ai_gunnery", cost = 1, requires = Set(BattleFrameRobotics))
case object StandardExoSuit extends Certification(value = 26, name = "standard_armor", cost = 0)

View file

@ -8,6 +8,7 @@ import net.psforever.objects.{Player, _}
import net.psforever.objects.ballistics.PlayerSource
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.DeployAnimation
import net.psforever.objects.definition.converter.OCM
import net.psforever.objects.equipment._
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
@ -1111,7 +1112,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
val zone = obj.Zone
val events = zone.AvatarEvents
val name = player.Name
val definition = item.Definition
val faction = obj.Faction
val toChannel = if (player.isBackpack) { self.toString } else { name }
val willBeVisible = obj.VisibleSlots.contains(slot)
@ -1143,12 +1143,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
toChannel,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
ObjectCreateDetailedMessage(
definition.ObjectId,
item.GUID,
ObjectCreateMessageParent(guid, slot),
definition.Packet.DetailedConstructorData(item).get
)
OCM.detailed(item, ObjectCreateMessageParent(guid, slot))
)
)
if (!player.isBackpack && willBeVisible) {
@ -1328,10 +1323,10 @@ object PlayerControl {
*/
private def auraEffectToAttributeValue(effect: Aura): Int = effect match {
case Aura.Plasma => 1
case Aura.Comet => 2
case Aura.Comet => 2
case Aura.Napalm => 4
case Aura.Fire => 8
case _ => 0
case Aura.Fire => 8
case _ => 0
}
def sendResponse(zone: Zone, channel: String, msg: PlanetSideGamePacket): Unit = {

View file

@ -171,6 +171,8 @@ object AggravatedDamage {
DamageType.Direct
case DamageResolution.AggravatedSplash | DamageResolution.AggravatedSplashBurn =>
DamageType.Splash
case DamageResolution.Radiation =>
DamageType.Splash
case _ =>
DamageType.None
}

View file

@ -15,16 +15,16 @@ final case class DeployableSource(
position: Vector3,
orientation: Vector3
) extends SourceEntry {
override def Name = SourceEntry.NameFormat(obj_def.Name)
override def Faction = faction
override def Name = obj_def.Descriptor
override def Faction = faction
def Definition: ObjectDefinition with DeployableDefinition = obj_def
def Health = health
def Shields = shields
def OwnerName = owner.Name
def Position = position
def Orientation = orientation
def Velocity = None
def Modifiers = obj_def.asInstanceOf[ResistanceProfile]
def Health = health
def Shields = shields
def OwnerName = owner.Name
def Position = position
def Orientation = orientation
def Velocity = None
def Modifiers = obj_def.asInstanceOf[ResistanceProfile]
}
object DeployableSource {

View file

@ -0,0 +1,84 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.Player
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.etc.RadiationReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones.blockmap.SectorPopulation
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType}
import net.psforever.types.PlanetSideGUID
case object RadiationInteraction extends ZoneInteractionType
/**
* This game entity may infrequently test whether it may interact with radiation cloud projectiles
* that may be emitted in the game environment for a limited amount of time.
*/
class InteractWithRadiationClouds(
val range: Float,
private val user: Option[Player]
) extends ZoneInteraction {
/**
* radiation clouds that, though detected, are skipped from affecting the target;
* in between interaction tests, a memory of the clouds that were tested last are retained and
* are excluded from being tested this next time;
* clouds that are detected a second time are cleared from the list and are available to be tested next time
*/
private var skipTargets: List[PlanetSideGUID] = List()
def Type = RadiationInteraction
/**
* Wander into a radiation cloud and suffer the consequences.
* @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 = {
target match {
case t: Vitality =>
val position = target.Position
//collect all projectiles in sector/range
val projectiles = sector
.projectileList
.filter { cloud =>
val radius = cloud.Definition.DamageRadius
cloud.Definition.radiation_cloud && Zone.distanceCheck(target, cloud, radius * radius)
}
.distinct
val notSkipped = projectiles.filterNot { t => skipTargets.contains(t.GUID) }
skipTargets = notSkipped.map { _.GUID }
if (notSkipped.nonEmpty) {
//isolate one of each type of projectile
notSkipped
.foldLeft(Nil: List[Projectile]) {
(acc, next) => if (acc.exists { _.profile == next.profile }) acc else next :: acc
}
.foreach { projectile =>
t.Actor ! Vitality.Damage(
DamageInteraction(
SourceEntry(target),
RadiationReason(
ProjectileQuality.modifiers(projectile, DamageResolution.Radiation, t, t.Position, user),
t.DamageModel,
1f
),
position
).calculate()
)
}
}
case _ => ;
}
}
/**
* Any radiation clouds blocked from being tested should be cleared.
* All that can be done is blanking our retained previous effect targets.
* @param target the fixed element in this test
*/
def resetInteraction(target: InteractsWithZone): Unit = {
skipTargets = List()
}
}

View file

@ -9,6 +9,7 @@ import net.psforever.objects.entity.SimpleWorldEntity
import net.psforever.objects.equipment.FireModeDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.types.Vector3
/**
@ -47,7 +48,8 @@ final case class Projectile(
quality: ProjectileQuality = ProjectileQuality.Normal,
id: Long = Projectile.idGenerator.getAndIncrement(),
fire_time: Long = System.currentTimeMillis()
) extends PlanetSideGameObject {
) extends PlanetSideGameObject
with BlockMapEntity {
Position = shot_origin
Orientation = shot_angle
Velocity = shot_velocity.getOrElse {

View file

@ -1,6 +1,13 @@
//Copyright (c) 2020 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.{PlanetSideGameObject, Player}
import net.psforever.objects.equipment.EquipmentSize
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.{DamageResolution, DamageType}
import net.psforever.types.{ImplantType, Vector3}
/**
* Projectile quality is an external aspect of projectiles
* that is not dependent on hard-coded definitions of the entities
@ -33,4 +40,48 @@ object ProjectileQuality {
/** Assign a custom numeric qualifier value, usually to be applied to damage calculations. */
case class Modified(mod: Float) extends ProjectileQuality
/**
* na
* @param projectile the projectile object
* @param resolution the resolution status to promote the projectile
* @return a copy of the projectile
*/
def modifiers(
projectile: Projectile,
resolution: DamageResolution.Value,
target: PlanetSideGameObject with FactionAffinity with Vitality,
pos: Vector3,
user: Option[Player]
): Projectile = {
projectile.Resolve() //if not yet resolved once
if (projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated)) {
//aggravated
val quality = projectile.profile.Aggravated match {
case Some(aggravation)
if aggravation.targets.exists(validation => validation.test(target)) &&
aggravation.info.exists(_.damage_type == AggravatedDamage.basicDamageType(resolution)) =>
ProjectileQuality.AggravatesTarget
case _ =>
ProjectileQuality.Normal
}
projectile.quality(quality)
} else if (projectile.tool_def.Size == EquipmentSize.Melee) {
//melee
user match {
case Some(player) =>
val quality = player.avatar.implants.flatten.find { entry => entry.definition.implantType == ImplantType.MeleeBooster } match {
case Some(booster) if booster.active && player.avatar.stamina > 9 =>
ProjectileQuality.Modified(25f)
case _ =>
ProjectileQuality.Normal
}
projectile.quality(quality)
case None =>
projectile
}
} else {
projectile
}
}
}

View file

@ -25,6 +25,7 @@ object Projectiles extends Enumeration {
final val anniversary_projectileb = Value(59)
final val aphelion_immolation_cannon_projectile = Value(87)
final val aphelion_laser_projectile = Value(91)
final val aphelion_plasma_cloud = Value(96) //radiation cloud
final val aphelion_plasma_rocket_projectile = Value(99)
final val aphelion_ppa_projectile = Value(103)
final val aphelion_starfire_projectile = Value(108)
@ -48,6 +49,7 @@ object Projectiles extends Enumeration {
final val falcon_projectile = Value(286)
final val firebird_missile_projectile = Value(288)
final val flail_projectile = Value(296)
final val flamethrower_fire_cloud = Value(301)
final val flamethrower_fireball = Value(302)
final val flamethrower_projectile = Value(303)
final val flux_cannon_apc_projectile = Value(305)
@ -79,6 +81,7 @@ object Projectiles extends Enumeration {
final val liberator_bomb_cluster_bomblet_projectile = Value(436)
final val liberator_bomb_cluster_projectile = Value(437)
final val liberator_bomb_projectile = Value(438)
final val maelstrom_grenade_damager = Value(464)
final val maelstrom_grenade_projectile = Value(465)
final val maelstrom_grenade_projectile_contact = Value(466)
final val maelstrom_stream_projectile = Value(467)
@ -94,12 +97,14 @@ object Projectiles extends Enumeration {
final val mine_projectile = Value(551)
final val mine_sweeper_projectile = Value(554)
final val mine_sweeper_projectile_enh = Value(555)
final val ntu_siphon_emp = Value(596)
final val oicw_little_buddy = Value(601)
final val oicw_projectile = Value(602)
final val pellet_gun_projectile = Value(631)
final val peregrine_dual_machine_gun_projectile = Value(639)
final val peregrine_mechhammer_projectile = Value(647)
final val peregrine_particle_cannon_projectile = Value(654)
final val peregrine_particle_cannon_radiation_cloud = Value(655) //radiation cloud
final val peregrine_rocket_pod_projectile = Value(657)
final val peregrine_sparrow_projectile = Value(661)
final val phalanx_av_projectile = Value(665)
@ -117,6 +122,7 @@ object Projectiles extends Enumeration {
final val pulsar_ap_projectile = Value(702)
final val pulsar_projectile = Value(703)
final val quasar_projectile = Value(713)
final val radiator_cloud = Value(717) //radiation cloud
final val radiator_grenade_projectile = Value(718)
final val radiator_sticky_projectile = Value(719)
final val reaver_rocket_projectile = Value(723)

View file

@ -1,16 +1,19 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.ce
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction}
import net.psforever.objects.zones.blockmap.SectorPopulation
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType}
import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable}
import net.psforever.types.PlanetSideGUID
case object MineInteraction extends ZoneInteractionType
/**
* This game entity may infrequently test whether it may interact with game world deployable extra-territorial munitions.
* "Interact", here, is a graceful word for "trample upon" and the consequence should be an explosion
* and maybe death.
*/
class InteractWithMines(range: Float)
class InteractWithMines(val range: Float)
extends ZoneInteraction {
/**
* mines that, though detected, are skipped from being alerted;
@ -20,14 +23,16 @@ class InteractWithMines(range: Float)
*/
private var skipTargets: List[PlanetSideGUID] = List()
def Type = MineInteraction
/**
* Trample upon active mines in our current detection sector and alert those mines.
* @param sector the portion of the block map being tested
* @param target the fixed element in this test
*/
def interaction(target: InteractsWithZone): Unit = {
def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
val faction = target.Faction
val targets = target.Zone.blockMap
.sector(target.Position, range)
val targets = sector
.deployableList
.filter {
case _: BoomerDeployable => false //boomers are specific types of ExplosiveDeployable but do not count here

View file

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

View file

@ -6,7 +6,7 @@ import net.psforever.objects.{Default, NtuContainerDefinition, Vehicle}
import net.psforever.objects.definition.converter.VehicleConverter
import net.psforever.objects.inventory.InventoryTile
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.vehicles.{DestroyedVehicle, MountableWeaponsDefinition, UtilityType}
import net.psforever.objects.vehicles.{DestroyedVehicle, MountableWeaponsDefinition, UtilityType, VehicleSubsystemEntry}
import net.psforever.objects.vital._
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
@ -27,20 +27,39 @@ class VehicleDefinition(objectId: Int)
with NtuContainerDefinition
with ResistanceProfileMutators
with DamageResistanceModel {
/** vehicle shields offered through amp station facility benefits (generally: 20% of health + 1) */
/** ... */
var shieldUiAttribute: Int = 68
/** how many points of shield the vehicle starts with (should default to 0 if unset through the accessor) */
private var defaultShields : Option[Int] = None
/** maximum vehicle shields (generally: 20% of health)
* for normal vehicles, offered through amp station facility benefits
* for BFR's, it charges naturally
**/
private var maxShields: Int = 0
/** the minimum amount of time that must elapse in between damage and shield charge activities (ms) */
private var shieldChargeDamageCooldown : Long = 5000L
/** the minimum amount of time that must elapse in between distinct shield charge activities (ms) */
private var shieldChargePeriodicCooldown : Long = 1000L
/** if the shield recharges on its own, this value will be non-`None` and indicate by how much */
private var autoShieldRecharge : Option[Int] = None
private var autoShieldRechargeSpecial : Option[Int] = None
/** shield drain is what happens to the shield under special conditions, e.g., bfr flight;
* the drain interval is 250ms which is convenient for us
* we can skip needing to define is explicitly */
private var shieldDrain : Option[Int] = None
private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]()
private var deployment: Boolean = false
private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap()
private val utilityOffsets: mutable.HashMap[Int, Vector3] = mutable.HashMap()
var subsystems: List[VehicleSubsystemEntry] = Nil
private var deploymentTime_Deploy: Int = 0 //ms
private var deploymentTime_Undeploy: Int = 0 //ms
private var trunkSize: InventoryTile = InventoryTile.None
private var trunkOffset: Int = 0
/* The position offset of the trunk, orientation as East = 0 */
private var trunkLocation: Vector3 = Vector3.Zero
private var canCloak: Boolean = false
private var canFly: Boolean = false
private var trunkLocation: Vector3 = Vector3.Zero
private var canCloak: Boolean = false
private var canFly: Boolean = false
/** whether the vehicle gains and/or maintains ownership based on access to the driver seat<br>
* `Some(true)` - assign ownership upon the driver mount, maintains ownership after the driver dismounts<br>
* `Some(false)` - assign ownership upon the driver mount, becomes unowned after the driver dismounts<br>
@ -48,12 +67,22 @@ class VehicleDefinition(objectId: Int)
* Be cautious about using `None` as the client tends to equate the driver seat as the owner's seat for many vehicles
* and breaking from the client's convention either requires additional fields or just doesn't work.
*/
private var canBeOwned: Option[Boolean] = Some(true)
private var serverVehicleOverrideSpeeds: (Int, Int) = (0, 0)
var undergoesDecay: Boolean = true
private var deconTime: Option[FiniteDuration] = None
private var maxCapacitor: Int = 0
private var destroyedModel: Option[DestroyedVehicle.Value] = None
private var canBeOwned: Option[Boolean] = Some(true)
private var serverVehicleOverrideSpeeds: (Int, Int) = (0, 0)
var undergoesDecay: Boolean = true
private var deconTime: Option[FiniteDuration] = None
private var defaultCapacitor: Int = 0
private var maxCapacitor: Int = 0
private var capacitorRecharge: Int = 0
private var capacitorDrain: Int = 0
private var capacitorDrainSpecial: Int = 0
/**
* extend the time of the final scrapping and explosion further beyond when the vehicle is functionally rendered destroyed;
* see `innateDamage` for explosion information;
* for BFR's, the ADB field is `death_large_explosion_interval`
*/
var destructionDelay: Option[Long] = None
private var destroyedModel: Option[DestroyedVehicle.Value] = None
Name = "vehicle"
Packet = VehicleDefinition.converter
DamageUsing = DamageCalculations.AgainstVehicle
@ -63,6 +92,15 @@ class VehicleDefinition(objectId: Int)
RepairRestoresAt = 1
registerAs = "vehicles"
def DefaultShields: Int = defaultShields.getOrElse(0)
def DefaultShields_=(shield: Int): Int = DefaultShields_=(Some(shield))
def DefaultShields_=(shield: Option[Int]): Int = {
defaultShields = shield
DefaultShields
}
def MaxShields: Int = maxShields
def MaxShields_=(shields: Int): Int = {
@ -70,6 +108,47 @@ class VehicleDefinition(objectId: Int)
MaxShields
}
def ShieldPeriodicDelay : Long = shieldChargePeriodicCooldown
def ShieldPeriodicDelay_=(cooldown: Long): Long = {
shieldChargePeriodicCooldown = cooldown
ShieldPeriodicDelay
}
def ShieldDamageDelay: Long = shieldChargeDamageCooldown
def ShieldDamageDelay_=(cooldown: Long): Long = {
shieldChargeDamageCooldown = cooldown
ShieldDamageDelay
}
def ShieldAutoRecharge: Option[Int] = autoShieldRecharge
def ShieldAutoRecharge_=(charge: Int): Option[Int] = ShieldAutoRecharge_=(Some(charge))
def ShieldAutoRecharge_=(charge: Option[Int]): Option[Int] = {
autoShieldRecharge = charge
ShieldAutoRecharge
}
def ShieldAutoRechargeSpecial: Option[Int] = autoShieldRechargeSpecial.orElse(ShieldAutoRecharge)
def ShieldAutoRechargeSpecial_=(charge: Int): Option[Int] = ShieldAutoRechargeSpecial_=(Some(charge))
def ShieldAutoRechargeSpecial_=(charge: Option[Int]): Option[Int] = {
autoShieldRechargeSpecial = charge
ShieldAutoRechargeSpecial
}
def ShieldDrain: Option[Int] = shieldDrain
def ShieldDrain_=(drain: Int): Option[Int] = ShieldDrain_=(Some(drain))
def ShieldDrain_=(drain: Option[Int]): Option[Int] = {
shieldDrain = drain
ShieldDrain
}
def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo
def CanBeOwned: Option[Boolean] = canBeOwned
@ -164,6 +243,13 @@ class VehicleDefinition(objectId: Int)
def AutoPilotSpeed2: Int = serverVehicleOverrideSpeeds._2
def DefaultCapacitor: Int = defaultCapacitor
def DefaultCapacitor_=(defValue: Int): Int = {
defaultCapacitor = defValue
DefaultCapacitor
}
def MaxCapacitor : Int = maxCapacitor
def MaxCapacitor_=(max: Int) : Int = {
@ -171,6 +257,27 @@ class VehicleDefinition(objectId: Int)
MaxCapacitor
}
def CapacitorRecharge: Int = capacitorRecharge
def CapacitorRecharge_=(charge: Int): Int = {
capacitorRecharge = charge
CapacitorRecharge
}
def CapacitorDrain: Int = capacitorDrain
def CapacitorDrain_=(charge: Int): Int = {
capacitorDrain = charge
CapacitorDrain
}
def CapacitorDrainSpecial: Int = capacitorDrainSpecial
def CapacitorDrainSpecial_=(charge: Int): Int = {
capacitorDrainSpecial = charge
CapacitorDrainSpecial
}
private var jackDuration = Array(0, 0, 0, 0)
def JackingDuration: Array[Int] = jackDuration
def JackingDuration_=(arr: Array[Int]): Array[Int] = {
@ -253,6 +360,36 @@ object VehicleDefinition {
*/
def Apc(objectId: Int): VehicleDefinition = new ApcDefinition(objectId)
protected class BfrDefinition(objectId: Int) extends VehicleDefinition(objectId) {
import net.psforever.objects.vehicles.control.BfrControl
override def Initialize(obj: Vehicle, context: ActorContext): Unit = {
obj.Actor = context.actorOf(
Props(classOf[BfrControl], obj),
PlanetSideServerObject.UniqueActorName(obj)
)
}
}
/**
* Vehicle definition(s) for the battle frame robotics vehicles.
* @param objectId the object id that is associated with this sort of `Vehicle`
*/
def Bfr(objectId: Int): VehicleDefinition = new BfrDefinition(objectId)
protected class BfrFlightDefinition(objectId: Int) extends VehicleDefinition(objectId) {
import net.psforever.objects.vehicles.control.BfrFlightControl
override def Initialize(obj: Vehicle, context: ActorContext): Unit = {
obj.Actor = context.actorOf(
Props(classOf[BfrFlightControl], obj),
PlanetSideServerObject.UniqueActorName(obj)
)
}
}
/**
* Vehicle definition(s) for the flight variant of the battle frame robotics vehicles.
* @param objectId the object id that is associated with this sort of `Vehicle`
*/
def BfrFlight(objectId: Int): VehicleDefinition = new BfrFlightDefinition(objectId)
protected class CarrierDefinition(objectId: Int) extends VehicleDefinition(objectId) {
import net.psforever.objects.vehicles.control.CargoCarrierControl
override def Initialize(obj: Vehicle, context: ActorContext): Unit = {

View file

@ -160,7 +160,7 @@ object AvatarConverter {
0L,
0L,
0L,
Some(DCDExtra2(0, 0)),
None, //Some(ImprintingProgress(0, 0)),
Nil,
Nil,
unkC = false,

View file

@ -0,0 +1,115 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.definition.converter
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.objects.vehicles.VehicleSubsystemEntry
import net.psforever.objects.{PlanetSideGameObject, Vehicle}
import net.psforever.types.PlanetSideGUID
import net.psforever.packet.game.objectcreate._
import scala.util.{Failure, Success, Try}
class BattleFrameFlightConverter extends ObjectCreateConverter[Vehicle]() {
override def DetailedConstructorData(obj: Vehicle): Try[BattleFrameRoboticsData] =
Failure(new Exception("BattleFrameFlightConverter should not be used to generate detailed BattleFrameRoboticsData (nothing should)"))
override def ConstructorData(obj: Vehicle): Try[BattleFrameRoboticsData] = {
val health = StatConverter.Health(obj.Health, obj.MaxHealth)
if(health > 0) { //active
Success(
BattleFrameRoboticsData(
PlacementData(obj.Position, obj.Orientation, obj.Velocity),
CommonFieldData(
obj.Faction,
bops = false,
alternate = false,
v1 = true,
v2 = None,
jammered = false,
v4 = None,
v5 = None,
obj.Owner match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}
),
health,
StatConverter.Health(obj.Shields, obj.MaxShields),
unk1 = 0,
unk2 = false,
no_mount_points = false,
driveState = 60,
proper_anim = true,
unk3 = 0,
show_bfr_shield = showBfrShield(obj),
unk4 = Some(false),
Some(InventoryData(MakeDriverSeat(obj) ++ MakeUtilities(obj) ++ MakeMountings(obj)))
)
)
}
else { //destroyed
Success(
BattleFrameRoboticsData(
PlacementData(obj.Position, obj.Orientation),
CommonFieldData(
obj.Faction,
bops = false,
alternate = false,
v1 = true,
v2 = None,
jammered = false,
v4 = None,
v5 = None,
guid = PlanetSideGUID(0)
),
0,
0,
unk1 = 0,
unk2 = false,
no_mount_points = false,
driveState = 0,
proper_anim = true,
unk3 = 0,
show_bfr_shield = false,
unk4 = Some(false),
inventory = None
)
)
}
}
private def MakeDriverSeat(obj: Vehicle): List[InventoryItemData.InventoryItem] = {
val offset: Long = MountableInventory.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, VehicleFormat.BattleframeFlight)
obj.Seats(0).occupant match {
case Some(player) =>
List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, SeatConverter.MakeSeat(player, offset)))
case None =>
Nil
}
}
private def MakeMountings(obj: Vehicle): List[InventoryItemData.InventoryItem] = {
obj.Weapons.collect {
case (index, slot: EquipmentSlot) if slot.Equipment.nonEmpty =>
val equip: Equipment = slot.Equipment.get
val equipDef = equip.Definition
InventoryItemData(equipDef.ObjectId, equip.GUID, index, equipDef.Packet.ConstructorData(equip).get)
}.toList
}
protected def MakeUtilities(obj: Vehicle): List[InventoryItemData.InventoryItem] = {
Vehicle
.EquipmentUtilities(obj.Utilities)
.map({
case (index, utilContainer) =>
val util: PlanetSideGameObject = utilContainer()
val utilDef = util.Definition
InventoryItemData(utilDef.ObjectId, util.GUID, index, utilDef.Packet.ConstructorData(util).get)
})
.toList
}
def showBfrShield(obj: Vehicle): Boolean = {
obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled && obj.Shields > 0
}
}

View file

@ -0,0 +1,115 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.definition.converter
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.objects.vehicles.VehicleSubsystemEntry
import net.psforever.objects.{PlanetSideGameObject, Vehicle}
import net.psforever.types.PlanetSideGUID
import net.psforever.packet.game.objectcreate._
import scala.util.{Failure, Success, Try}
class BattleFrameRoboticsConverter extends ObjectCreateConverter[Vehicle]() {
override def DetailedConstructorData(obj: Vehicle): Try[BattleFrameRoboticsData] =
Failure(new Exception("BattleFrameRoboticsConverter should not be used to generate detailed BattleFrameRoboticsData (nothing should)"))
override def ConstructorData(obj: Vehicle): Try[BattleFrameRoboticsData] = {
val health = StatConverter.Health(obj.Health, obj.MaxHealth)
if(health > 0) { //active
Success(
BattleFrameRoboticsData(
PlacementData(obj.Position, obj.Orientation, obj.Velocity),
CommonFieldData(
obj.Faction,
bops = false,
alternate = false,
v1 = true,
v2 = None,
jammered = obj.Jammed,
v4 = None,
v5 = None,
obj.Owner match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}
),
health,
StatConverter.Health(obj.Shields, obj.MaxShields),
unk1 = 0,
unk2 = false,
no_mount_points = false,
driveState = 60,
proper_anim = true,
unk3 = 0,
show_bfr_shield = showBfrShield(obj),
unk4 = None,
Some(InventoryData(MakeDriverSeat(obj) ++ MakeUtilities(obj) ++ MakeMountings(obj)))
)
)
}
else { //destroyed
Success(
BattleFrameRoboticsData(
PlacementData(obj.Position, obj.Orientation),
CommonFieldData(
obj.Faction,
bops = false,
alternate = false,
v1 = true,
v2 = None,
jammered = false,
v4 = None,
v5 = None,
guid = PlanetSideGUID(0)
),
0,
0,
unk1 = 0,
unk2 = false,
no_mount_points = false,
driveState = 0,
proper_anim = true,
unk3 = 0,
show_bfr_shield = false,
unk4 = None,
inventory = None
)
)
}
}
private def MakeDriverSeat(obj: Vehicle): List[InventoryItemData.InventoryItem] = {
val offset: Long = MountableInventory.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, VehicleFormat.Battleframe)
obj.Seats(0).occupant match {
case Some(player) =>
List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, SeatConverter.MakeSeat(player, offset)))
case None =>
Nil
}
}
private def MakeMountings(obj: Vehicle): List[InventoryItemData.InventoryItem] = {
obj.Weapons.collect {
case (index, slot: EquipmentSlot) if slot.Equipment.nonEmpty =>
val equip: Equipment = slot.Equipment.get
val equipDef = equip.Definition
InventoryItemData(equipDef.ObjectId, equip.GUID, index, equipDef.Packet.ConstructorData(equip).get)
}.toList
}
protected def MakeUtilities(obj: Vehicle): List[InventoryItemData.InventoryItem] = {
Vehicle
.EquipmentUtilities(obj.Utilities)
.map({
case (index, utilContainer) =>
val util: PlanetSideGameObject = utilContainer()
val utilDef = util.Definition
InventoryItemData(utilDef.ObjectId, util.GUID, index, utilDef.Packet.ConstructorData(util).get)
})
.toList
}
def showBfrShield(obj: Vehicle): Boolean = {
obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled && obj.Shields > 0
}
}

View file

@ -0,0 +1,37 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.definition.converter
import net.psforever.objects.Tool
import net.psforever.packet.game.objectcreate.{CommonFieldData, DetailedWeaponData, InternalSlot, WeaponData}
import net.psforever.types.PlanetSideGUID
import scala.util.{Failure, Success, Try}
class BattleFrameToolConverter extends ObjectCreateConverter[Tool]() {
override def ConstructorData(obj: Tool): Try[WeaponData] = {
val slots: List[InternalSlot] = (0 until obj.MaxAmmoSlot).map(index => {
val box = obj.AmmoSlots(index).Box
InternalSlot(box.Definition.ObjectId, box.GUID, index, box.Definition.Packet.ConstructorData(box).get)
}).toList
Success(
WeaponData(
CommonFieldData(
obj.Faction,
bops = false,
alternate = false,
true,
None,
obj.Jammed,
Some(false),
None,
PlanetSideGUID(0)
),
obj.FireModeIndex,
slots
)
)
}
override def DetailedConstructorData(obj: Tool): Try[DetailedWeaponData] =
Failure(new Exception("BattleFrameToolConverter should not be used to generate detailed BattleFrameRToolData (nothing should)"))
}

View file

@ -119,7 +119,7 @@ class CharacterSelectConverter extends AvatarConverter {
0L,
0L,
0L,
Some(DCDExtra2(0, 0)),
Some(ImprintingProgress(0, 0)),
Nil,
Nil,
unkC = false,

View file

@ -108,7 +108,7 @@ class CorpseConverter extends AvatarConverter {
0L,
0L,
0L,
Some(DCDExtra2(0, 0)),
Some(ImprintingProgress(0, 0)),
Nil,
Nil,
unkC = false,

View file

@ -0,0 +1,101 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.definition.converter
import net.psforever.objects.PlanetSideGameObject
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{ObjectCreateDetailedMessage, ObjectCreateMessage}
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
/**
* Compose an `ObjectCreateMessage` packet or, if requesting and allowing, an `ObjectCreateDetailedMessage` packet.
*/
object OCM {
/**
* Compose an `ObjectCreateMessage` packet of an entity.
* @param obj the entity being converted into a packet
* @return an `ObjectCreateMessage` packet
*/
def apply(obj: PlanetSideGameObject): PlanetSideGamePacket = {
val definition = obj.Definition
ObjectCreateMessage(
definition.ObjectId,
obj.GUID,
definition.Packet.ConstructorData(obj).get
)
}
/**
* Compose a contained `ObjectCreateMessage` packet of an entity.
* @param obj the entity being converted into a packet
* @param parent information about the container for this entity
* @return an `ObjectCreateMessage` packet
*/
def apply(obj: PlanetSideGameObject, parent: Option[ObjectCreateMessageParent]): PlanetSideGamePacket = {
parent match {
case Some(info) => apply(obj, info)
case _ => apply(obj)
}
}
/**
* Compose a contained `ObjectCreateMessage` packet of an entity.
* @param obj the entity being converted into a packet
* @param parent information about the container for this entity
* @return an `ObjectCreateMessage` packet
*/
def apply(obj: PlanetSideGameObject, parent: ObjectCreateMessageParent): PlanetSideGamePacket = {
val definition = obj.Definition
ObjectCreateMessage(
definition.ObjectId,
obj.GUID,
parent,
definition.Packet.ConstructorData(obj).get
)
}
def detailed(obj: PlanetSideGameObject): PlanetSideGamePacket = {
val definition = obj.Definition
val packet = definition.Packet
if (packet.noDetailedForm(obj)) {
apply(obj) //fall back
} else {
ObjectCreateDetailedMessage(
definition.ObjectId,
obj.GUID,
definition.Packet.DetailedConstructorData(obj).get
)
}
}
/**
* Compose a contained detailed `ObjectCreateMessage` packet of an entity.
* @param obj the entity being converted into a packet
* @param parent information about the container for this entity
* @return an `ObjectCreateMessage` packet
*/
def detailed(obj: PlanetSideGameObject, parent: Option[ObjectCreateMessageParent]): PlanetSideGamePacket = {
parent match {
case Some(info) => detailed(obj, info)
case _ => detailed(obj)
}
}
/**
* Compose a contained detailed `ObjectCreateMessage` packet of an entity.
* @param obj the entity being converted into a packet
* @param parent information about the container for this entity
* @return an `ObjectCreateMessage` packet
*/
def detailed(obj: PlanetSideGameObject, parent: ObjectCreateMessageParent): PlanetSideGamePacket = {
val definition = obj.Definition
val packet = definition.Packet
if (packet.noDetailedForm(obj)) {
apply(obj, parent) //fall back
} else {
ObjectCreateDetailedMessage(
definition.ObjectId,
obj.GUID,
parent,
definition.Packet.DetailedConstructorData(obj).get
)
}
}
}

View file

@ -17,6 +17,8 @@ sealed trait PacketConverter
* @tparam A the type of game object
*/
abstract class ObjectCreateConverter[A <: PlanetSideGameObject] extends PacketConverter {
/** some objects do not have a detailed constructor data form */
def noDetailedForm(obj: A): Boolean = DetailedConstructorData(obj).isFailure
/**
* Take a game object and transform it into its equivalent data for an `0x17` packet.

View file

@ -0,0 +1,16 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.definition.converter
import net.psforever.objects.ballistics.Projectile
import net.psforever.packet.game.objectcreate._
import scala.util.{Failure, Success, Try}
class RadiationCloudConverter extends ObjectCreateConverter[Projectile]() {
override def ConstructorData(obj: Projectile): Try[RadiationCloudData] = {
Success(RadiationCloudData(PlacementData(obj.Position, obj.Orientation), obj.owner.Faction))
}
override def DetailedConstructorData(obj: Projectile): Try[RadiationCloudData] =
Failure(new Exception("RadiationCloudConverter should not be used to generate detailed RadiationCloudData (nothing should)"))
}

View file

@ -3,11 +3,11 @@ package net.psforever.objects.definition.converter
import net.psforever.objects.Player
import net.psforever.objects.serverobject.mount.Seat
import net.psforever.packet.game.objectcreate.{InventoryItemData, ObjectClass, PlayerData, VehicleData}
import net.psforever.packet.game.objectcreate._
object SeatConverter {
def MakeSeat(player: Player, offset: Long): PlayerData = {
VehicleData.PlayerData(
MountableInventory.PlayerData(
AvatarConverter.MakeAppearanceData(player),
AvatarConverter.MakeCharacterData(player),
AvatarConverter.MakeInventoryData(player),

View file

@ -75,7 +75,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
}
private def MakeDriverSeat(obj: Vehicle): List[InventoryItemData.InventoryItem] = {
val offset: Long = VehicleData.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier)
val offset: Long = MountableInventory.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier)
obj.Seats(0).occupant match {
case Some(player) =>
List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, SeatConverter.MakeSeat(player, offset)))

View file

@ -0,0 +1,156 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.equipment
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.{GlobalDefinitions, Tool, Vehicle}
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.vital.RepairFromArmorSiphon
import net.psforever.objects.vital.etc.{ArmorSiphonModifiers, ArmorSiphonReason}
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.packet.game.QuantityUpdateMessage
import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.PlanetSideGUID
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
object ArmorSiphonBehavior {
sealed case class RepairedByArmorSiphon(cause: DamageInteraction, amount: Int)
sealed case class Recharge(guid: PlanetSideGUID)
trait Target {
_: Actor with Damageable =>
def SiphonableObject: Vehicle
val siphoningBehavior: Receive = {
case CommonMessages.Use(player, Some(item : Tool))
if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) && player.Faction != DamageableObject.Faction =>
val obj = SiphonableObject
val zone = obj.Zone
val iguid = item.GUID
//see Damageable.takesDamage
zone.Vehicles.find { v =>
v.Weapons.values.exists { slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == iguid}
} match {
case Some(v: Vehicle) if v.CanDamage =>
//remember: we are the vehicle being siphoned; we need the vehicle doing the siphoning
val before = item.Magazine
val after = item.Discharge()
if (before > after) {
v.Actor ! ArmorSiphonBehavior.Recharge(iguid)
PerformDamage(
obj,
DamageInteraction(
VehicleSource(obj),
ArmorSiphonReason(v, item, obj.DamageModel),
obj.Position
).calculate()
)
}
case _ => ;
}
}
}
trait SiphonOwner {
_: Actor =>
def SiphoningObject: Vehicle
private val siphonRecharge: mutable.HashMap[PlanetSideGUID, Cancellable] = mutable.HashMap[PlanetSideGUID, Cancellable]()
def repairPostStop(): Unit = {
siphonRecharge.keys.foreach { endSiphonRecharge }
}
val siphonRepairBehavior: Receive = {
case RepairedByArmorSiphon(cause, amount) =>
val obj = SiphoningObject
val before = obj.Health
cause.cause match {
case asr: ArmorSiphonReason
if before < obj.MaxHealth =>
val after = obj.Health += amount
if(before < after) {
obj.History(RepairFromArmorSiphon(asr.siphon.Definition, before - after))
val zone = obj.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, obj.GUID, 0, after)
)
}
case _ => ;
}
case ArmorSiphonBehavior.Recharge(guid) =>
siphonRecharge.remove(guid) match {
case Some(timer) => timer.cancel()
case None => ;
}
val obj = SiphoningObject
obj.Weapons.values.find { slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid } match {
case Some(siphonSlot) =>
val siphon = siphonSlot.Equipment.get.asInstanceOf[Tool]
val zone = obj.Zone
//update current charge level
zone.VehicleEvents ! VehicleServiceMessage(
obj.Actor.toString,
VehicleAction.SendResponse(Service.defaultPlayerGUID, QuantityUpdateMessage(siphon.AmmoSlot.Box.GUID, siphon.Magazine))
)
siphonRecharge.put(guid, context.system.scheduler.scheduleWithFixedDelay(
initialDelay = 3000 milliseconds,
delay = 200 milliseconds,
self,
SiphonOwner.Recharge(guid)
))
case _ => ;
}
case SiphonOwner.Recharge(guid) =>
val obj = SiphoningObject
val zone = obj.Zone
obj.Weapons.values.find { slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid } match {
case Some(slot: EquipmentSlot) =>
val siphon = slot.Equipment.get.asInstanceOf[Tool]
val before = siphon.Magazine
val after = siphon.Magazine = before + 1
if (after > before) {
zone.VehicleEvents ! VehicleServiceMessage(
obj.Actor.toString,
VehicleAction.SendResponse(Service.defaultPlayerGUID, QuantityUpdateMessage(siphon.AmmoSlot.Box.GUID, after))
)
if (after == siphon.MaxMagazine) {
endSiphonRecharge(guid)
}
}
case _ =>
endSiphonRecharge(guid)
}
}
def endSiphonRecharge(guid: PlanetSideGUID): Unit = {
siphonRecharge.remove(guid) match {
case Some(c) => c.cancel()
case None => ;
}
}
}
object SiphonOwner {
private case class Recharge(guid: PlanetSideGUID)
}
}
case object ArmorSiphonRepairHost extends ArmorSiphonModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ArmorSiphonReason): Int = {
if (damage > 0) {
cause.hostVehicle.Actor ! ArmorSiphonBehavior.RepairedByArmorSiphon(data, damage)
}
damage
}
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.equipment
import net.psforever.objects.definition.EquipmentDefinition
import enumeratum.values.{StringEnum, StringEnumEntry}
sealed abstract class Hand(val value: String) extends StringEnumEntry
object Handiness extends StringEnum[Hand] {
val values = findValues
case object Generic extends Hand(value = "Generic")
case object Left extends Hand(value = "Left")
case object Right extends Hand(value = "Right")
}
final case class EquipmentHandiness(
generic: EquipmentDefinition,
left: EquipmentDefinition,
right: EquipmentDefinition
) {
def transform(handiness: Hand): EquipmentDefinition = {
handiness match {
case Handiness.Generic => generic
case Handiness.Left => left
case Handiness.Right => right
}
}
def contains(findDef: EquipmentDefinition): Boolean = {
generic == findDef || left == findDef || right == findDef
}
}

View file

@ -25,6 +25,7 @@ class FireModeDefinition extends DamageModifiers {
/** how many rounds are replenished each reload cycle */
private var magazine: Int = 1
private var defaultMagazine: Option[Int] = None
/** how many rounds are replenished each reload cycle, per type of ammunition loaded
* key - ammo type index, value - magazine capacity
@ -63,6 +64,15 @@ class FireModeDefinition extends DamageModifiers {
projectileTypeIndices += index
}
def DefaultMagazine: Int = defaultMagazine.getOrElse(magazine)
def DefaultMagazine_=(inMagazine: Int): Int = DefaultMagazine_=(Some(inMagazine))
def DefaultMagazine_=(inMagazine: Option[Int]): Int = {
defaultMagazine = inMagazine
DefaultMagazine
}
def Magazine: Int = magazine
def Magazine_=(inMagazine: Int): Int = {

View file

@ -7,7 +7,7 @@ import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.vehicles.MountedWeapons
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.ZoneAware
import net.psforever.objects.zones.{Zone, ZoneAware}
import net.psforever.types.Vector3
import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
@ -254,7 +254,7 @@ trait JammableMountedWeapons extends JammableBehavior {
override def StartJammeredStatus(target: Any, dur: Int): Unit = {
target match {
case obj: PlanetSideServerObject with MountedWeapons with JammableUnit if !obj.Jammed =>
JammableMountedWeapons.JammeredStatus(obj, 1)
JammableMountedWeaponsJammeredStatus(obj, statusCode = 1)
super.StartJammeredStatus(target, dur)
case _ => ;
}
@ -275,11 +275,15 @@ trait JammableMountedWeapons extends JammableBehavior {
override def CancelJammeredStatus(target: Any): Unit = {
target match {
case obj: PlanetSideServerObject with MountedWeapons with JammableUnit if obj.Jammed =>
JammableMountedWeapons.JammeredStatus(obj, 0)
JammableMountedWeaponsJammeredStatus(obj, statusCode = 0)
case _ => ;
}
super.CancelJammeredStatus(target)
}
def JammableMountedWeaponsJammeredStatus(target: PlanetSideServerObject with MountedWeapons, statusCode: Int): Unit = {
JammableMountedWeapons.JammeredStatus(target, statusCode)
}
}
object JammableMountedWeapons {
@ -292,17 +296,20 @@ object JammableMountedWeapons {
* 1 for activation
*/
def JammeredStatus(target: PlanetSideServerObject with MountedWeapons, statusCode: Int): Unit = {
val zone = target.Zone
val zoneId = zone.id
val zone = target.Zone
target.Weapons.values
.map { _.Equipment }
.collect {
case Some(item: Tool) =>
item.Jammed = statusCode == 1
zone.VehicleEvents ! VehicleServiceMessage(
zoneId,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, item.GUID, 27, statusCode)
)
JammedWeaponStatus(zone, item, statusCode)
}
}
def JammedWeaponStatus(zone: Zone, target: Equipment with JammableUnit, statusCode: Int): Unit = {
target.Jammed = statusCode == 1
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 27, statusCode)
)
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry}
import net.psforever.objects.ballistics.{PlayerSource, Projectile, SourceEntry}
import net.psforever.objects.geometry.d3._
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player}
import net.psforever.types.{ExoSuitType, Vector3}
@ -80,6 +80,22 @@ object GeometryForm {
}
}
/**
* The geometric representation is a sphere around the entity's centroid
* positioned following the axis of rotation (the entity's base).
* The specific entity should be a projectile, else the result is invalid.
* @param o the entity
* @return the representation
*/
def representProjectileBySphere()(o: Any): VolumetricGeometry = {
o match {
case p: Projectile =>
Sphere(p.Position, p.Definition.DamageRadius)
case _ =>
invalidPoint
}
}
/**
* The geometric representation is a cylinder around the entity's base.
* @param radius half the distance across

View file

@ -78,7 +78,7 @@ class NumberPoolHub(private val source: NumberSource) {
case Nil => ;
case collisions =>
throw new IllegalArgumentException(
s"can not add pool $name - it contains the following redundant numbers: ${collisions.mkString(",")}"
s"can not add pool $name - it contains the following redundant numbers: ${collisions.sorted.mkString(",")}"
)
}
pool.foreach(i => bigpool += i.toLong -> name)

View file

@ -76,6 +76,15 @@ trait Container {
}
}
/**
* When the slot reported is not the slot requested, change the slot.
* @param slot the original slot index
* @return the modified slot index
*/
def SlotMapResolution(slot: Int): Int = {
slot
}
/**
* Given a region of "searchable unit positions" considered as stowable,
* determine if any previously stowed items are contained within that region.<br>

View file

@ -22,20 +22,23 @@ object InventoryTile {
final val Tile11 = InventoryTile(1, 1) //occasional placeholder
final val Tile22 = InventoryTile(2, 2) //grenades, boomer trigger
final val Tile23 = InventoryTile(2, 3) //canister ammo
final val Tile42 = InventoryTile(4, 2) //medkit
final val Tile33 = InventoryTile(3, 3) //ammo box, pistols, ace
final val Tile42 = InventoryTile(4, 2) //medkit
final val Tile44 = InventoryTile(4, 4) //large ammo box
final val Tile55 = InventoryTile(5, 5) //bfr ammo box
final val Tile66 = InventoryTile(6, 6) //infiltration suit inventory
final val Tile63 = InventoryTile(6, 3) //rifles
final val Tile93 = InventoryTile(9, 3) //long-body weapons
final val Tile84 = InventoryTile(8, 4) //bfr arm weapons
final val Tile96 = InventoryTile(9, 6) //standard exo-suit inventory
final val Tile99 = InventoryTile(9, 9) //agile exo-suit inventory
final val Tile1004 = InventoryTile(10, 4) //bfr gunner weapons
final val Tile1107 = InventoryTile(11, 7) //uncommon small trunk capacity - phantasm
final val Tile1111 = InventoryTile(11, 11) //common small trunk capacity
final val Tile1209 = InventoryTile(12, 9) //reinforced exo-suit inventory
final val Tile1511 = InventoryTile(15, 11) //common medium trunk capacity
final val Tile1515 = InventoryTile(15, 15) //common large trunk capacity
final val Tile1518 = InventoryTile(15, 18) //gunner bfr trunk capacity
final val Tile1611 = InventoryTile(16, 11) //uncommon medium trunk capacity - vulture
final val Tile1612 = InventoryTile(16, 12) //MAX; uncommon medium trunk capacity - lodestar
final val Tile1816 = InventoryTile(18, 16) //uncommon massive trunk capacity - galaxy_gunship

View file

@ -51,12 +51,13 @@ object Loadout {
* @return a `VehicleLoadout` object populated with appropriate information about the current state of the vehicle
*/
def Create(vehicle: Vehicle, label: String): Loadout = {
val (_, entries: List[Loadout.SimplifiedEntry]) = vehicle.Weapons.collect {
case (index, slot: EquipmentSlot) if slot.Equipment.nonEmpty =>
(index, SimplifiedEntry(buildSimplification(slot.Equipment.get), index))
}.unzip
VehicleLoadout(
label,
packageSimplifications(vehicle.Weapons.collect {
case (index, slot) if slot.Equipment.nonEmpty =>
InventoryItem(slot.Equipment.get, index)
}.toList),
entries,
packageSimplifications(vehicle.Trunk.Items),
vehicle.Definition
)

View file

@ -1,6 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.loadouts
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.definition._
/**
@ -27,3 +28,24 @@ final case class VehicleLoadout(
inventory: List[Loadout.SimplifiedEntry],
vehicle_definition: VehicleDefinition
) extends EquipmentLoadout(label, visible_slots, inventory)
object VehicleLoadout {
/**
* The variant of the battleframe vehicle.
* Why these numbers map to the specific type of battleframe is a mystery.
* @see `FavoritesMessage`
* @param definition the vehicle's definition
* @return a number directly indicative of the type
*/
def DetermineBattleframeSubtype(definition: VehicleDefinition): Int = {
definition match {
case GlobalDefinitions.aphelion_flight => 1
case GlobalDefinitions.aphelion_gunner => 2
case GlobalDefinitions.colossus_flight => 4
case GlobalDefinitions.colossus_gunner => 5
case GlobalDefinitions.peregrine_flight => 7
case GlobalDefinitions.peregrine_gunner => 8
case _ => 0
}
}
}

View file

@ -0,0 +1,37 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.serverobject
import akka.actor.Actor
import net.psforever.types.PlanetSideGUID
abstract class ServerObjectControl
extends Actor {
protected val log = org.log4s.getLogger(toString())
val attributeBehavior: Receive = {
case ServerObject.AttributeMsg(attribute, value, other) =>
parseAttribute(attribute, value, other)
case ServerObject.GenericObjectAction(guid, action, other) =>
parseObjectAction(guid, action, other)
case ServerObject.GenericAction(guid, action, other) =>
parseGenericAction(guid, action, other)
}
def parseAttribute(attribute: Int, value: Long, other: Option[Any]): Unit
def parseGenericAction(guid: PlanetSideGUID, action: Int, other: Option[Any]): Unit = { /*intentionally blank*/ }
def parseObjectAction(guid: PlanetSideGUID, action: Int, other: Option[Any]): Unit = { /*intentionally blank*/ }
}
object ServerObject {
final case class AttributeMsg(attribute: Int, value: Long, other: Option[Any] = None)
final case class GenericAction(guid: PlanetSideGUID, action: Int, other: Option[Any] = None)
final case class GenericObjectAction(guid: PlanetSideGUID, action: Int, other: Option[Any] = None)
final case class StateChangeDenied(original: Any, msg: String)
}

View file

@ -4,11 +4,11 @@ package net.psforever.objects.serverobject.containable
import akka.actor.{Actor, ActorRef}
import akka.pattern.{AskTimeoutException, ask}
import akka.util.Timeout
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.equipment.{Equipment, EquipmentSize}
import net.psforever.objects.inventory.{Container, InventoryItem}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.zones.Zone
import net.psforever.objects.{BoomerTrigger, GlobalDefinitions, Player}
import net.psforever.objects._
import net.psforever.types.{PlanetSideEmpire, Vector3}
import scala.concurrent.ExecutionContext.Implicits.global
@ -92,58 +92,7 @@ trait ContainableBehavior {
case msg @ Containable.MoveItem(destination, equipment, destSlot) =>
/* can be deferred */
if (ContainableBehavior.TestPutItemInSlot(destination, equipment, destSlot).nonEmpty) { //test early, before we try to move the item
val source = ContainerObject
val item = equipment
val dest = destSlot
LocalRemoveItemFromSlot(item) match {
case Containable.ItemFromSlot(_, Some(_), slot @ Some(originalSlot)) =>
if (source eq destination) {
//when source and destination are the same, moving the item can be performed in one pass
LocalPutItemInSlot(item, dest) match {
case Containable.ItemPutInSlot(_, _, _, None) => ; //success
case Containable.ItemPutInSlot(_, _, _, Some(swapItem)) => //success, but with swap item
LocalPutItemInSlotOnlyOrAway(swapItem, slot) match {
case Containable.ItemPutInSlot(_, _, _, None) => ;
case _ =>
source.Zone.Ground.tell(
Zone.Ground.DropItem(swapItem, source.Position, Vector3.z(source.Orientation.z)),
source.Actor
) //drop it
}
case _: Containable.CanNotPutItemInSlot => //failure case ; try restore original item placement
LocalPutItemInSlot(item, originalSlot)
}
} else {
//destination sync
destination.Actor ! ContainableBehavior.Wait()
implicit val timeout = new Timeout(1000 milliseconds)
val moveItemOver = ask(destination.Actor, ContainableBehavior.MoveItemPutItemInSlot(item, dest))
moveItemOver.onComplete {
case Success(Containable.ItemPutInSlot(_, _, _, None)) => ; //successful
case Success(Containable.ItemPutInSlot(_, _, _, Some(swapItem))) => //successful, but with swap item
PutItBackOrDropIt(source, swapItem, slot, destination.Actor)
case Success(_: Containable.CanNotPutItemInSlot) => //failure case ; try restore original item placement
PutItBackOrDropIt(source, item, slot, source.Actor)
case Failure(_) => //failure case ; try restore original item placement
PutItBackOrDropIt(source, item, slot, source.Actor)
case _ => ; //TODO what?
}
//always do this
moveItemOver
.recover { case _: AskTimeoutException => destination.Actor ! ContainableBehavior.Resume() }
.onComplete { _ => destination.Actor ! ContainableBehavior.Resume() }
}
case _ => ;
//we could not find the item to be moved in the source location; trying to act on old data?
}
} else {
MessageDeferredCallback(msg)
}
ContainableMoveItem(destination, equipment, destSlot, msg)
case ContainableBehavior.MoveItemPutItemInSlot(item, dest) =>
sender() ! LocalPutItemInSlot(item, dest)
@ -188,6 +137,66 @@ trait ContainableBehavior {
/* Functions (item transfer) */
protected def ContainableMoveItem(
destination: PlanetSideServerObject with Container,
equipment: Equipment,
destSlot: Int,
msg: Any
) : Unit = {
if (ContainableBehavior.TestPutItemInSlot(destination, equipment, destSlot).nonEmpty) { //test early, before we try to move the item
val source = ContainerObject
val item = equipment
val dest = destSlot
LocalRemoveItemFromSlot(item) match {
case Containable.ItemFromSlot(_, Some(_), slot @ Some(originalSlot)) =>
if (source eq destination) {
//when source and destination are the same, moving the item can be performed in one pass
LocalPutItemInSlot(item, dest) match {
case Containable.ItemPutInSlot(_, _, _, None) => ; //success
case Containable.ItemPutInSlot(_, _, _, Some(swapItem)) => //success, but with swap item
LocalPutItemInSlotOnlyOrAway(swapItem, slot) match {
case Containable.ItemPutInSlot(_, _, _, None) => ;
case _ =>
source.Zone.Ground.tell(
Zone.Ground.DropItem(swapItem, source.Position, Vector3.z(source.Orientation.z)),
source.Actor
) //drop it
}
case _: Containable.CanNotPutItemInSlot => //failure case ; try restore original item placement
LocalPutItemInSlot(item, originalSlot)
}
} else {
//destination sync
destination.Actor ! ContainableBehavior.Wait()
implicit val timeout = new Timeout(1000 milliseconds)
val moveItemOver = ask(destination.Actor, ContainableBehavior.MoveItemPutItemInSlot(item, dest))
moveItemOver.onComplete {
case Success(Containable.ItemPutInSlot(_, _, _, None)) => ; //successful
case Success(Containable.ItemPutInSlot(_, _, _, Some(swapItem))) => //successful, but with swap item
PutItBackOrDropIt(source, swapItem, slot, destination.Actor)
case Success(_: Containable.CanNotPutItemInSlot) => //failure case ; try restore original item placement
PutItBackOrDropIt(source, item, slot, source.Actor)
case Failure(_) => //failure case ; try restore original item placement
PutItBackOrDropIt(source, item, slot, source.Actor)
case _ => ; //TODO what?
}
//always do this
moveItemOver
.recover { case _: AskTimeoutException => destination.Actor ! ContainableBehavior.Resume() }
.onComplete { _ => destination.Actor ! ContainableBehavior.Resume() }
}
case _ => ;
//we could not find the item to be moved in the source location; trying to act on old data?
}
} else {
MessageDeferredCallback(msg)
}
}
private def LocalRemoveItemFromSlot(slot: Int): Any = {
val source = ContainerObject
val (outSlot, item) = ContainableBehavior.TryRemoveItemFromSlot(source, slot)
@ -370,14 +379,15 @@ object ContainableBehavior {
item: Equipment
): (Option[Int], Option[Equipment]) = {
source.Find(item) match {
case slot @ Some(index) =>
case slot @ Some(index)
if ContainableBehavior.PermitEquipmentExtract(source, item, index)=>
source.Slot(index).Equipment = None
if (source.Slot(index).Equipment.isEmpty) {
(slot, Some(item))
} else {
(None, None)
}
case None =>
case _ =>
(None, None)
}
}
@ -399,14 +409,14 @@ object ContainableBehavior {
source: PlanetSideServerObject with Container,
slot: Int
): (Option[Int], Option[Equipment]) = {
val (item, outSlot) = source.Slot(slot).Equipment match {
case Some(thing) => (Some(thing), source.Find(thing))
case None => (None, None)
}
source.Slot(slot).Equipment = None
item match {
case Some(_) if item.nonEmpty && source.Slot(slot).Equipment.isEmpty =>
(outSlot, item)
source.Slot(slot).Equipment match {
case Some(thing)
if ContainableBehavior.PermitEquipmentExtract(source, thing, slot) =>
if ((source.Slot(slot).Equipment = None).isEmpty) {
(Some(slot), Some(thing))
} else {
(None, None)
}
case _ =>
(None, None)
}
@ -431,7 +441,7 @@ object ContainableBehavior {
item: Equipment,
dest: Int
): Option[List[InventoryItem]] = {
if (ContainableBehavior.PermitEquipmentStow(destination, item)) {
if (ContainableBehavior.PermitEquipmentStow(destination, item, dest)) {
val tile = item.Definition.Tile
val destinationCollisionTest = destination.Collisions(dest, tile.Width, tile.Height)
destinationCollisionTest match {
@ -505,7 +515,7 @@ object ContainableBehavior {
def TryPutItemAway(destination: PlanetSideServerObject with Container, item: Equipment): Option[Int] = {
destination.Fit(item) match {
case out @ Some(dest)
if ContainableBehavior.PermitEquipmentStow(destination, item) && (destination.Slot(dest).Equipment = item)
if ContainableBehavior.PermitEquipmentStow(destination, item, dest) && (destination.Slot(dest).Equipment = item)
.contains(item) =>
out
case _ =>
@ -572,22 +582,67 @@ object ContainableBehavior {
}
}
//TODO convert PermitEquipmentExtract and PermitEquipmentStow into instance methods?
/**
* Apply incontestable, arbitrary limitations
* whereby certain items are denied removal from certain containers
* for vaguely documented but assuredly fantastic excuses on the part of the developer.
* @see `ContainableBehavior.PermitEquipmentStow`
* @param source the container
* @param equipment the item to be removed
* @param slot where the equipment can be found
* @return `true`, if the type of equipment object is allowed to be removed from the containing entity;
* `false`, otherwise
*/
def PermitEquipmentExtract(
source: PlanetSideServerObject with Container,
equipment: Equipment,
slot: Int
): Boolean = {
source match {
case v: Vehicle if v.VisibleSlots.contains(slot) =>
//can not remove equipment slot items if vehicle is jammed
//applies mostly to BFR's, but we do not need to filter
!v.Jammed
case _ =>
true
}
}
/**
* Apply incontestable, arbitrary limitations
* whereby certain items are denied insertion into certain containers
* for vaguely documented but assuredly fantastic excuses on the part of the developer.
* @see `ContainableBehavior.PermitEquipmentExtract`
* @param destination the container
* @param equipment the item to be inserted
* @return `true`, if the object is allowed to contain the type of equipment object;
* `false`, otherwise
*/
def PermitEquipmentStow(destination: PlanetSideServerObject with Container, equipment: Equipment): Boolean = {
def PermitEquipmentStow(
destination: PlanetSideServerObject with Container,
equipment: Equipment,
dest: Int
): Boolean = {
import net.psforever.objects.{BoomerTrigger, Player}
equipment match {
case _: BoomerTrigger =>
//a BoomerTrigger can only be stowed in a player's holsters or inventory
//this is only a requirement until they, and their Boomer explosive complement, are cleaned-up properly
destination.isInstanceOf[Player]
case weapon: Tool
if weapon.Size == EquipmentSize.BFRArmWeapon || weapon.Size == EquipmentSize.BFRGunnerWeapon =>
//Battleframe weaponry must be placed in an appropriate equipment mount spot, or held in the player's free hand
//if in the vehicle slots, then the vehicle must not be jammed
destination match {
case v: Vehicle
if GlobalDefinitions.isBattleFrameVehicle(v.Definition) =>
v.VisibleSlots.contains(dest) && !v.Jammed
case _: Player =>
dest == Player.FreeHandSlot
case _ =>
false
}
case _ =>
true
}

View file

@ -1,7 +1,7 @@
//Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.damage
import akka.actor.Actor
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.{Vehicle, Vehicles}
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.damage.Damageable.Target
@ -30,6 +30,8 @@ trait DamageableVehicle
/** whether or not the vehicle has been damaged directly, report that damage has occurred */
protected var reportDamageToVehicle: Boolean = false
/** when the vehicle is destroyed, its major explosion is delayed */
protected var queuedDestruction: Option[Cancellable] = None
def DamageableObject: Vehicle
def AggravatedObject : Vehicle = DamageableObject
@ -44,6 +46,7 @@ trait DamageableVehicle
case DamageableVehicle.Destruction(cause) =>
//cargo vehicles are destroyed when carrier is destroyed
//bfrs undergo a shiver spell before exploding
val obj = DamageableObject
obj.Health = 0
obj.History(cause)
@ -59,24 +62,28 @@ trait DamageableVehicle
target: Damageable.Target,
applyDamageTo: ResolutionCalculations.Output
): Unit = {
val obj = DamageableObject
val originalHealth = obj.Health
val originalShields = obj.Shields
val cause = applyDamageTo(obj)
val health = obj.Health
val shields = obj.Shields
val damageToHealth = originalHealth - health
val damageToShields = originalShields - shields
if (WillAffectTarget(target, damageToHealth + damageToShields, cause)) {
target.History(cause)
DamageLog(
target,
s"BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields"
)
HandleDamage(target, cause, (damageToHealth, damageToShields))
} else {
obj.Health = originalHealth
obj.Shields = originalShields
queuedDestruction match {
case Some(_) => ;
case None =>
val obj = DamageableObject
val originalHealth = obj.Health
val originalShields = obj.Shields
val cause = applyDamageTo(obj)
val health = obj.Health
val shields = obj.Shields
val damageToHealth = originalHealth - health
val damageToShields = originalShields - shields
if (WillAffectTarget(target, damageToHealth + damageToShields, cause)) {
target.History(cause)
DamageLog(
target,
s"BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields"
)
HandleDamage(target, cause, (damageToHealth, damageToShields))
} else {
obj.Health = originalHealth
obj.Shields = originalShields
}
}
}
@ -127,7 +134,7 @@ trait DamageableVehicle
if (damageToShields > 0) {
events ! VehicleServiceMessage(
vehicleChannel,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 68, obj.Shields)
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, obj.Definition.shieldUiAttribute, obj.Shields)
)
announceConfrontation = true
}
@ -175,29 +182,60 @@ trait DamageableVehicle
* @param cause historical information about the damage
*/
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
val obj = DamageableObject
val zone = target.Zone
//aggravation cancel
EndAllAggravation()
//passengers die with us
DamageableMountable.DestructionAwareness(obj, cause)
//things positioned around us can get hurt from us
Zone.serverSideDamage(obj.Zone, target, Zone.explosionDamage(Some(cause)))
//special considerations for certain vehicles
Vehicles.BeforeUnloadVehicle(obj, zone)
//shields
if (obj.Shields > 0) {
obj.Shields = 0
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 68, 0)
)
(queuedDestruction, DamageableObject.Definition.destructionDelay) match {
case (None, Some(delay)) => //set a future explosion for later
destructionDelayed(delay, cause)
case (Some(_), _) | (None, None) => //explode now
super.DestructionAwareness(target, cause)
val obj = DamageableObject
val zone = target.Zone
//aggravation cancel
EndAllAggravation()
//passengers die with us
DamageableMountable.DestructionAwareness(obj, cause)
Zone.serverSideDamage(obj.Zone, target, Zone.explosionDamage(Some(cause)))
//special considerations for certain vehicles
Vehicles.BeforeUnloadVehicle(obj, zone)
//shields
if (obj.Shields > 0) {
obj.Shields = 0
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, obj.Definition.shieldUiAttribute, 0)
)
}
//clean up
target.Actor ! Vehicle.Deconstruct(Some(1 minute))
target.ClearHistory()
DamageableWeaponTurret.DestructionAwareness(obj, cause)
case _ => ;
}
//clean up
target.Actor ! Vehicle.Deconstruct(Some(1 minute))
target.ClearHistory()
DamageableWeaponTurret.DestructionAwareness(obj, cause)
}
def destructionDelayed(delay: Long, cause: DamageResult): Unit = {
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
val obj = DamageableObject
//health to 1, shields to 0
obj.Health = 1
obj.Shields = 0
val guid = obj.GUID
val guid0 = Service.defaultPlayerGUID
val zone = obj.Zone
val zoneid = zone.id
val events = zone.VehicleEvents
events ! VehicleServiceMessage(
zoneid,
VehicleAction.PlanetsideAttribute(guid0, guid, 0, 1)
)
events ! VehicleServiceMessage(
zoneid,
VehicleAction.PlanetsideAttribute(guid0, guid, obj.Definition.shieldUiAttribute, 0)
)
//passengers die with us
DamageableMountable.DestructionAwareness(DamageableObject, cause)
//come back to this death later
queuedDestruction = Some(context.system.scheduler.scheduleOnce(delay milliseconds, self, DamageableVehicle.Destruction(cause)))
}
}

View file

@ -4,7 +4,9 @@ package net.psforever.objects.serverobject.environment
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.zones._
import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation}
case object EnvironmentInteraction extends ZoneInteractionType
/**
* This game entity may infrequently test whether it may interact with game world environment.
@ -14,6 +16,10 @@ class InteractWithEnvironment()
private var interactingWithEnvironment: (PlanetSideServerObject, Boolean) => Any =
InteractWithEnvironment.onStableEnvironment()
def Type = EnvironmentInteraction
def range: Float = 0f
/**
* The method by which zone interactions are tested or a current interaction maintained.
* Utilize a function literal that, when called, returns a function literal of the same type;
@ -24,8 +30,10 @@ class InteractWithEnvironment()
* @see `InteractsWithEnvironment.blockedFromInteracting`
* @see `InteractsWithEnvironment.onStableEnvironment`
* @see `InteractsWithEnvironment.awaitOngoingInteraction`
* @param sector the portion of the block map being tested
* @param target the fixed element in this test
*/
def interaction(target: InteractsWithZone): Unit = {
def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
interactingWithEnvironment = interactingWithEnvironment(target, true)
.asInstanceOf[(PlanetSideServerObject, Boolean) => Any]
}

View file

@ -41,5 +41,7 @@ case object SmallCargo extends MountRestriction[Vehicle] {
}
case object LargeCargo extends MountRestriction[Vehicle] {
def test(target : Vehicle) : Boolean = !target.Definition.CanFly
def test(target: Vehicle): Boolean = {
GlobalDefinitions.isBattleFrameVehicle(target.Definition) || !target.Definition.CanFly
}
}

View file

@ -18,28 +18,16 @@ class VehicleSpawnPadDefinition(objectId: Int) extends AmenityDefinition(objectI
// However, it seems these values need to be reversed to turn CCW to CW rotation (e.g. +90 to -90)
private var vehicle_creation_z_orient_offset = 0f
def VehicleCreationZOffset: Float = vehicle_creation_z_offset
def VehicleCreationZOffset: Float = vehicle_creation_z_offset
def VehicleCreationZOrientOffset: Float = vehicle_creation_z_orient_offset
objectId match {
case 141 =>
Name = "bfr_door"
vehicle_creation_z_offset = -4.5f
vehicle_creation_z_orient_offset = 90f
case 261 =>
Name = "dropship_pad_doors"
vehicle_creation_z_offset = 4.89507f
vehicle_creation_z_orient_offset = -90f
case 525 =>
Name = "mb_pad_creation"
vehicle_creation_z_offset = 2.52604f
case 615 => Name = "pad_create"
case 616 =>
Name = "pad_creation"
vehicle_creation_z_offset = 1.70982f
case 816 => Name = "spawnpoint_vehicle"
case 947 => Name = "vanu_vehicle_creation_pad"
case _ => throw new IllegalArgumentException("Not a valid object id with the type vehicle_creation_pad")
def VehicleCreationZOffset_=(offset: Float): Float = {
vehicle_creation_z_offset = offset
vehicle_creation_z_offset
}
def VehicleCreationZOrientOffset_=(offset: Float): Float = {
vehicle_creation_z_orient_offset = offset
vehicle_creation_z_orient_offset
}
/** The region surrounding a vehicle spawn pad that is cleared of damageable targets prior to a vehicle being spawned.
@ -161,17 +149,44 @@ object VehicleSpawnPadDefinition {
flightVehicle: Boolean
): (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = {
if (flightVehicle) {
vanuKillBox(pad.Position, radius, aboveLimit * 2)
cylinderKillBox(pad.Position, radius, aboveLimit * 2)
} else {
vanuKillBox(pad.Position, radius * 1.2f, aboveLimit)
cylinderKillBox(pad.Position, radius * 1.2f, aboveLimit)
}
}
/**
* A function that sets up the region around a battleframe vehicle spawn chamber's doors
* to be cleared of damageable targets upon spawning of a vehicle.
* All measurements are provided in terms of distance from the middle of the door.
* Internally, the pad is referred to as `bfr_door`;
* colloquially, the pad is referred to as a "BFR shed".
* @param radius the distance from the middle of the spawn pad
* @param aboveLimit how far above the spawn pad is to be cleared
* @param pad he vehicle spawn pad in question
* @param requiredButUnused required by the function prototype
* @return a function that describes a region ahead of the battleframe vehicle spawn shed
*/
def prepareBfrShedKillBox(
radius: Float,
aboveLimit: Float
)
(
pad: VehicleSpawnPad,
requiredButUnused: Boolean
): (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = {
cylinderKillBox(
Vector3(0,radius,0).Rz(pad.Orientation.z + pad.Definition.VehicleCreationZOrientOffset) + pad.Position,
radius,
aboveLimit
)
}
/**
* A function that finalizes the detection for the region around a vehicle spawn pad
* to be cleared of damageable targets upon spawning of a vehicle.
* All measurements are provided in terms of distance from the center of the pad.
* These pads are only found in the cavern zones and are cylindrical in shape.
* These pads are cylindrical in shape.
* @param origin the center of the spawn pad
* @param radius the distance from the middle of the spawn pad
* @param aboveLimit how far above the spawn pad is to be cleared
@ -182,16 +197,16 @@ object VehicleSpawnPadDefinition {
* @return `true`, if the two entities are near enough to each other;
* `false`, otherwise
*/
def vanuKillBox(
origin: Vector3,
radius: Float,
aboveLimit: Float
)
(
obj1: PlanetSideGameObject,
obj2: PlanetSideGameObject,
maxDistance: Float
): Boolean = {
def cylinderKillBox(
origin: Vector3,
radius: Float,
aboveLimit: Float
)
(
obj1: PlanetSideGameObject,
obj2: PlanetSideGameObject,
maxDistance: Float
): Boolean = {
val dir: Vector3 = {
val g2 = obj2.Definition.Geometry(obj2)
val cdir = Vector3.Unit(origin - g2.center.asVector3)

View file

@ -173,12 +173,12 @@ object PainboxControl {
val min = Vector3(
nearbyAmenities.minBy(_.Position.x).Position.x - 0.5f,
nearbyAmenities.minBy(_.Position.y).Position.y - 0.5f,
nearbyAmenities.minBy(_.Position.z).Position.z
nearbyAmenities.minBy(_.Position.z).Position.z - 0.5f
)
val max = Vector3(
nearbyAmenities.maxBy(_.Position.x).Position.x + 0.5f,
nearbyAmenities.maxBy(_.Position.y).Position.y + 0.5f,
painbox.Position.z
painbox.Position.z + 0.5f
)
(min, max, Vector3.midpoint(min, max))
case _ =>

View file

@ -266,7 +266,7 @@ trait AmenityAutoRepair
import scala.concurrent.ExecutionContext.Implicits.global
autoRepairTimer.cancel()
autoRepairQueueTask = Some(System.currentTimeMillis() + delay)
val modifiedDrain = drain * Config.app.game.amenityAutorepairDrainRate
val modifiedDrain = drain * 2 * Config.app.game.amenityAutorepairDrainRate //doubled intentionally
autoRepairTimer = if(AutoRepairObject.Owner == Building.NoBuilding) {
//without an owner, auto-repair freely
context.system.scheduler.scheduleOnce(

View file

@ -7,12 +7,11 @@ import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.transfer.TransferBehavior
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.{Ntu, NtuContainer, NtuStorageBehavior}
import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior}
import net.psforever.types.PlanetSideEmpire
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.util.Config
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
@ -53,16 +52,32 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
.orElse(storageBehavior)
.orElse {
case CommonMessages.Use(player, _) =>
if (resourceSilo.Faction == PlanetSideEmpire.NEUTRAL || player.Faction == resourceSilo.Faction) {
resourceSilo.Zone.Vehicles.find(v => v.PassengerInSeat(player).contains(0)) match {
case Some(vehicle) =>
context.system.scheduler.scheduleOnce(
delay = 1000 milliseconds,
vehicle.Actor,
TransferBehavior.Discharging(Ntu.Nanites)
)
case _ =>
}
val siloFaction = resourceSilo.Faction
val playerFaction = player.Faction
resourceSilo.Zone.Vehicles.find(v => v.PassengerInSeat(player).contains(0)) match {
case Some(vehicle) =>
(if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
//bfr's discharge into friendly silos and charge from enemy and neutral silos
if (siloFaction == playerFaction) {
Some(TransferBehavior.Discharging(Ntu.Nanites))
} else {
Some(TransferBehavior.Charging(Ntu.Nanites))
}
} else if(siloFaction == PlanetSideEmpire.NEUTRAL || siloFaction == playerFaction) {
//ants discharge into neutral and friendly silos
Some(TransferBehavior.Discharging(Ntu.Nanites))
} else {
None
}) match {
case Some(msg) =>
context.system.scheduler.scheduleOnce(
delay = 1000 milliseconds,
vehicle.Actor,
msg
)
case None => ;
}
case _ => ;
}
case ResourceSilo.LowNtuWarning(enabled: Boolean) =>
@ -128,11 +143,11 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
*/
def HandleNtuOffer(sender: ActorRef, src: NtuContainer): Unit = {
sender ! (if (resourceSilo.NtuCapacitor < resourceSilo.MaxNtuCapacitor) {
Ntu.Request(0, resourceSilo.MaxNtuCapacitor - resourceSilo.NtuCapacitor)
} else {
StopNtuBehavior(sender)
Ntu.Request(0, 0)
})
Ntu.Request(0, resourceSilo.MaxNtuCapacitor - resourceSilo.NtuCapacitor)
} else {
StopNtuBehavior(sender)
Ntu.Request(0, 0)
})
}
/**
@ -152,7 +167,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
*/
def HandleNtuRequest(sender: ActorRef, min: Float, max: Float): Unit = {
val originalAmount = resourceSilo.NtuCapacitor
UpdateChargeLevel(-(min * Config.app.game.amenityAutorepairDrainRate))
UpdateChargeLevel(-min)
sender ! Ntu.Grant(resourceSilo, originalAmount - resourceSilo.NtuCapacitor)
}
@ -175,15 +190,30 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
* if negative or zero, disable the animation
*/
def PanelAnimation(source: ActorRef, trigger: Float): Unit = {
val zone = resourceSilo.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, resourceSilo.GUID, 49, if (trigger > 0) 1 else 0)
) // panel glow & orb particles
val currentlyHas = resourceSilo.NtuCapacitor
// do not let the trigger charge go to waste, but also do not let the silo be filled
// attempting to return it to the source may sabotage an ongoing transfer process
val amount = math.min(resourceSilo.MaxNtuCapacitor - resourceSilo.NtuCapacitor, trigger)
UpdateChargeLevel(amount - amount*0.1f)
val amount = (if (trigger > 0) {
// panel glow & orb particles on
val zone = resourceSilo.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, resourceSilo.GUID, 49, 1)
)
math.min(resourceSilo.MaxNtuCapacitor - currentlyHas, trigger)
} else if (trigger < 0) {
// no change to animation state
if (currentlyHas > -trigger) { trigger } else { -currentlyHas }
} else {
// panel glow & orb particles off
val zone = resourceSilo.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, resourceSilo.GUID, 49, 0)
)
0
}) * 0.9f
UpdateChargeLevel(amount)
}
/**

View file

@ -155,6 +155,10 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi
def NtuCapacitor_=(value: Float): Float = NtuCapacitor
def MaxNtuCapacitor : Float = Int.MaxValue
override def NtuSource: Option[NtuContainer] = Some(this)
override def hasLatticeBenefit(wantedBenefit: ObjectDefinition): Boolean = false
override def latticeConnectedFacilityBenefits(): Set[ObjectDefinition] = Set.empty

View file

@ -208,6 +208,61 @@ object EquipmentTerminalDefinition {
"flail_targeting_laser" -> MakeSimpleItem(flail_targeting_laser)
)
/**
* A `Map` of operations for producing the `Tool` `Equipment` for battleframe arm weapons.
* key - an identification string sent by the client
* value - a curried function that builds the object
*/
val bfrArmWeapons : Map[String, () => Equipment] = Map(
"aphelion_armor_siphon" -> MakeTool(aphelion_armor_siphon),
"aphelion_laser" -> MakeTool(aphelion_laser),
"aphelion_ntu_siphon" -> MakeTool(aphelion_ntu_siphon),
"aphelion_ppa" -> MakeTool(aphelion_ppa),
"aphelion_starfire" -> MakeTool(aphelion_starfire),
"colossus_armor_siphon" -> MakeTool(colossus_armor_siphon),
"colossus_burster" -> MakeTool(colossus_burster),
"colossus_chaingun" -> MakeTool(colossus_chaingun),
"colossus_ntu_siphon" -> MakeTool(colossus_ntu_siphon),
"colossus_tank_cannon" -> MakeTool(colossus_tank_cannon),
"peregrine_armor_siphon" -> MakeTool(peregrine_armor_siphon),
"peregrine_dual_machine_gun" -> MakeTool(peregrine_dual_machine_gun),
"peregrine_mechhammer" -> MakeTool(peregrine_mechhammer),
"peregrine_ntu_siphon" -> MakeTool(peregrine_ntu_siphon),
"peregrine_sparrow" -> MakeTool(peregrine_sparrow)
)
/**
* A `Map` of operations for producing the `Tool` `Equipment` for battleframe gunner weapons.
* key - an identification string sent by the client
* value - a curried function that builds the object
*/
val bfrGunnerWeapons : Map[String, () => Equipment] = Map(
"aphelion_immolation_cannon" -> MakeTool(aphelion_immolation_cannon),
"aphelion_plasma_rocket_pod" -> MakeTool(aphelion_plasma_rocket_pod),
"colossus_cluster_bomb_pod" -> MakeTool(colossus_cluster_bomb_pod),
"colossus_dual_100mm_cannons" -> MakeTool(colossus_dual_100mm_cannons),
"peregrine_dual_rocket_pods" -> MakeTool(peregrine_dual_rocket_pods),
"peregrine_particle_cannon" -> MakeTool(peregrine_particle_cannon)
)
val bfrAmmunition : Map[String, () => AmmoBox] = Map(
"aphelion_laser_ammo" -> MakeAmmoBox(aphelion_laser_ammo),
"aphelion_immolation_cannon_ammo" -> MakeAmmoBox(aphelion_immolation_cannon_ammo),
"aphelion_plasma_rocket_ammo" -> MakeAmmoBox(aphelion_plasma_rocket_ammo),
"aphelion_ppa_ammo" -> MakeAmmoBox(aphelion_ppa_ammo),
"aphelion_starfire_ammo" -> MakeAmmoBox(aphelion_starfire_ammo),
"colossus_100mm_cannon_ammo" -> MakeAmmoBox(colossus_100mm_cannon_ammo),
"colossus_burster_ammo" -> MakeAmmoBox(colossus_burster_ammo),
"colossus_cluster_bomb_ammo" -> MakeAmmoBox(colossus_cluster_bomb_ammo),
"colossus_chaingun_ammo" -> MakeAmmoBox(colossus_chaingun_ammo),
"colossus_tank_cannon_ammo" -> MakeAmmoBox(colossus_tank_cannon_ammo),
"peregrine_dual_machine_gun_ammo" -> MakeAmmoBox(peregrine_dual_machine_gun_ammo),
"peregrine_mechhammer_ammo" -> MakeAmmoBox(peregrine_mechhammer_ammo),
"peregrine_particle_cannon_ammo" -> MakeAmmoBox(peregrine_particle_cannon_ammo),
"peregrine_rocket_pod_ammo" -> MakeAmmoBox(peregrine_rocket_pod_ammo),
"peregrine_sparrow_ammo" -> MakeAmmoBox(peregrine_sparrow_ammo)
)
/**
* A single-element `Map` of the one piece of `Equipment` specific to the Router.
*/

View file

@ -334,9 +334,9 @@ object OrderTerminalDefinition {
* @see `Loadout`
* @see `VehicleLoadout`
*/
final case class VehicleLoadoutPage() extends LoadoutTab {
final case class VehicleLoadoutPage(lineOffset: Int) extends LoadoutTab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
player.avatar.loadouts(msg.unk1 + 10) match {
player.avatar.loadouts(msg.unk1 + lineOffset) match {
case Some(loadout: VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) =>
val weapons = loadout.visible_slots
.map(entry => {
@ -401,6 +401,43 @@ object OrderTerminalDefinition {
}
}
/**
* The special page used by the `bfr_terminal` to select a vehicle to be spawned
* based on the player's previous loadouts for battleframe vehicles.
* Vehicle loadouts are defined by a superfluous redefinition of the vehicle's mounted weapons
* and equipment in the trunk.
* @see `Equipment`
* @see `Loadout`
* @see `Vehicle`
* @see `VehicleLoadout`
*/
final case class BattleframeSpawnLoadoutPage(vehicles: Map[String, () => Vehicle]) extends LoadoutTab {
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
player.avatar.loadouts(msg.unk1 + 15) match {
case Some(loadout: VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) =>
vehicles.get(loadout.vehicle_definition.Name) match {
case Some(vehicle) =>
val weapons = loadout.visible_slots.map(entry => {
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
})
val inventory = loadout.inventory.map(entry => {
InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index)
})
Terminal.BuyVehicle(vehicle(), weapons, inventory)
case None =>
Terminal.NoDeal()
}
case _ =>
Terminal.NoDeal()
}
}
def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = {
sender ! msg
}
}
/**
* Assemble some logic for a provided object.
* @param obj an `Amenity` object;

View file

@ -71,12 +71,12 @@ object VehicleTerminalDefinition {
* value - a curried function that builds the object
*/
val bfrVehicles: Map[String, () => Vehicle] = Map(
// "colossus_gunner" -> (()=>Unit),
// "colossus_flight" -> (()=>Unit),
// "peregrine_gunner" -> (()=>Unit),
// "peregrine_flight" -> (()=>Unit),
// "aphelion_gunner" -> (()=>Unit),
// "aphelion_flight" -> (()=>Unit)
"colossus_gunner" -> MakeVehicle(colossus_gunner),
"colossus_flight" -> MakeVehicle(colossus_flight),
"peregrine_gunner" -> MakeVehicle(peregrine_gunner),
"peregrine_flight" -> MakeVehicle(peregrine_flight),
"aphelion_gunner" -> MakeVehicle(aphelion_gunner),
"aphelion_flight" -> MakeVehicle(aphelion_flight)
)
import net.psforever.objects.loadouts.{Loadout => _Loadout} //distinguish from Terminal.Loadout message
@ -88,15 +88,18 @@ object VehicleTerminalDefinition {
* value - a curried function that builds the object
*/
val trunk: Map[String, _Loadout] = {
val ammo_12mm = ShorthandAmmoBox(bullet_12mm, bullet_12mm.Capacity)
val ammo_15mm = ShorthandAmmoBox(bullet_15mm, bullet_15mm.Capacity)
val ammo_25mm = ShorthandAmmoBox(bullet_25mm, bullet_25mm.Capacity)
val ammo_35mm = ShorthandAmmoBox(bullet_35mm, bullet_35mm.Capacity)
val ammo_20mm = ShorthandAmmoBox(bullet_20mm, bullet_20mm.Capacity)
val ammo_75mm = ShorthandAmmoBox(bullet_75mm, bullet_75mm.Capacity)
val ammo_mortar = ShorthandAmmoBox(heavy_grenade_mortar, heavy_grenade_mortar.Capacity)
val ammo_flux = ShorthandAmmoBox(flux_cannon_thresher_battery, flux_cannon_thresher_battery.Capacity)
val ammo_bomb = ShorthandAmmoBox(liberator_bomb, liberator_bomb.Capacity)
val ammo_12mm = ShorthandAmmoBox(bullet_12mm, bullet_12mm.Capacity)
val ammo_15mm = ShorthandAmmoBox(bullet_15mm, bullet_15mm.Capacity)
val ammo_25mm = ShorthandAmmoBox(bullet_25mm, bullet_25mm.Capacity)
val ammo_35mm = ShorthandAmmoBox(bullet_35mm, bullet_35mm.Capacity)
val ammo_20mm = ShorthandAmmoBox(bullet_20mm, bullet_20mm.Capacity)
val ammo_75mm = ShorthandAmmoBox(bullet_75mm, bullet_75mm.Capacity)
val ammo_mortar = ShorthandAmmoBox(heavy_grenade_mortar, heavy_grenade_mortar.Capacity)
val ammo_flux = ShorthandAmmoBox(flux_cannon_thresher_battery, flux_cannon_thresher_battery.Capacity)
val ammo_bomb = ShorthandAmmoBox(liberator_bomb, liberator_bomb.Capacity)
val ammo_ppa = ShorthandAmmoBox(aphelion_ppa_ammo, aphelion_ppa_ammo.Capacity)
val ammo_tcannon = ShorthandAmmoBox(colossus_tank_cannon_ammo, colossus_tank_cannon_ammo.Capacity)
val ammo_mgun = ShorthandAmmoBox(peregrine_dual_machine_gun_ammo, peregrine_dual_machine_gun_ammo.Capacity)
Map(
//"quadstealth" -> VehicleLoadout("default_quadstealth", List(), List(), quadstealth),
"quadassault" -> VehicleLoadout(
@ -513,9 +516,114 @@ object VehicleTerminalDefinition {
SimplifiedEntry(ammo_mortar, 186)
),
galaxy_gunship
)
),
//"phantasm" -> VehicleLoadout("default_phantasm", List(), List(), phantasm),
//"lodestar" -> VehicleLoadout("default_lodestar", List(), List(), lodestar),
{
val ammo = ShorthandAmmoBox(aphelion_plasma_rocket_ammo, aphelion_plasma_rocket_ammo.Capacity)
"aphelion_gunner" -> VehicleLoadout(
"default_aphelion_gunner",
List(),
List(
SimplifiedEntry(ammo_ppa, 30),
SimplifiedEntry(ammo_ppa, 34),
SimplifiedEntry(ammo_ppa, 38),
SimplifiedEntry(ammo_ppa, 90),
SimplifiedEntry(ammo_ppa, 94),
SimplifiedEntry(ammo_ppa, 98),
SimplifiedEntry(ammo, 150),
SimplifiedEntry(ammo, 155),
SimplifiedEntry(ammo, 160),
SimplifiedEntry(ammo, 225),
SimplifiedEntry(ammo, 230),
SimplifiedEntry(ammo, 235)
),
aphelion_gunner
)
},
"aphelion_flight" -> VehicleLoadout(
"default_aphelion_flight",
List(),
List(
SimplifiedEntry(ammo_ppa, 30),
SimplifiedEntry(ammo_ppa, 34),
SimplifiedEntry(ammo_ppa, 38),
SimplifiedEntry(ammo_ppa, 90),
SimplifiedEntry(ammo_ppa, 94),
SimplifiedEntry(ammo_ppa, 98)
),
aphelion_flight
),
{
val ammo = ShorthandAmmoBox(colossus_100mm_cannon_ammo, colossus_100mm_cannon_ammo.Capacity)
"colossus_gunner" -> VehicleLoadout(
"default_colossus_gunner",
List(),
List(
SimplifiedEntry(ammo_tcannon, 30),
SimplifiedEntry(ammo_tcannon, 34),
SimplifiedEntry(ammo_tcannon, 38),
SimplifiedEntry(ammo_tcannon, 90),
SimplifiedEntry(ammo_tcannon, 94),
SimplifiedEntry(ammo_tcannon, 98),
SimplifiedEntry(ammo, 150),
SimplifiedEntry(ammo, 155),
SimplifiedEntry(ammo, 160),
SimplifiedEntry(ammo, 225),
SimplifiedEntry(ammo, 230),
SimplifiedEntry(ammo, 235)
),
colossus_gunner
)
},
"colossus_flight" -> VehicleLoadout(
"default_colossus_flight",
List(),
List(
SimplifiedEntry(ammo_tcannon, 30),
SimplifiedEntry(ammo_tcannon, 34),
SimplifiedEntry(ammo_tcannon, 38),
SimplifiedEntry(ammo_tcannon, 90),
SimplifiedEntry(ammo_tcannon, 94),
SimplifiedEntry(ammo_tcannon, 98)
),
colossus_flight
),
{
val ammo = ShorthandAmmoBox(peregrine_particle_cannon_ammo, peregrine_particle_cannon_ammo.Capacity)
"peregrine_gunner" -> VehicleLoadout(
"default_peregrine_gunner",
List(),
List(
SimplifiedEntry(ammo_mgun, 30),
SimplifiedEntry(ammo_mgun, 34),
SimplifiedEntry(ammo_mgun, 38),
SimplifiedEntry(ammo_mgun, 90),
SimplifiedEntry(ammo_mgun, 94),
SimplifiedEntry(ammo_mgun, 98),
SimplifiedEntry(ammo, 150),
SimplifiedEntry(ammo, 155),
SimplifiedEntry(ammo, 160),
SimplifiedEntry(ammo, 225),
SimplifiedEntry(ammo, 230),
SimplifiedEntry(ammo, 235)
),
peregrine_gunner
)
},
"peregrine_flight" -> VehicleLoadout(
"default_peregrine_flight",
List(),
List(
SimplifiedEntry(ammo_mgun, 30),
SimplifiedEntry(ammo_mgun, 34),
SimplifiedEntry(ammo_mgun, 38),
SimplifiedEntry(ammo_mgun, 90),
SimplifiedEntry(ammo_mgun, 94),
SimplifiedEntry(ammo_mgun, 98)
),
peregrine_flight
)
)
}

View file

@ -86,18 +86,21 @@ object TransferBehavior {
Discharging
= Value
}
sealed trait Command
/**
* Message to cue a process of transferring into oneself.
*/
final case class Charging(transferMaterial : Any)
final case class Charging(transferMaterial : Any) extends Command
/**
* Message to cue a process of transferring from oneself.
*/
final case class Discharging(transferMaterial : Any)
final case class Discharging(transferMaterial : Any) extends Command
/**
* Message to cue a stopping the transfer process.
*/
final case class Stopping()
final case class Stopping() extends Command
/**
* A default search function that does not actually search for anything or ever find anything.

View file

@ -2,12 +2,17 @@
package net.psforever.objects.serverobject.transfer
import akka.actor.ActorRef
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.entity.{Identifiable, WorldEntity}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.zones.ZoneAware
trait TransferContainer extends Identifiable
trait TransferContainer
extends PlanetSideGameObject
with Identifiable
with ZoneAware
with WorldEntity {
with WorldEntity
with FactionAffinity {
def Actor : ActorRef
}

View file

@ -97,25 +97,28 @@ class FacilityTurretControl(turret: FacilityTurret)
)
case FacilityTurret.RechargeAmmo() =>
val weapon = turret.ControlledWeapon(1).get.asInstanceOf[net.psforever.objects.Tool]
// recharge when last shot fired 3s delay, +1, 200ms interval
if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) {
weapon.Magazine += 1
val seat = turret.Seat(0).get
seat.occupant match {
case Some(player: Player) =>
turret.Zone.LocalEvents ! LocalServiceMessage(
turret.Zone.id,
LocalAction.RechargeVehicleWeapon(player.GUID, turret.GUID, weapon.GUID)
)
case _ => ;
}
turret.ControlledWeapon(wepNumber = 1).foreach {
case weapon: Tool =>
// recharge when last shot fired 3s delay, +1, 200ms interval
if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) {
weapon.Magazine += 1
val seat = turret.Seat(0).get
seat.occupant match {
case Some(player : Player) =>
turret.Zone.LocalEvents ! LocalServiceMessage(
turret.Zone.id,
LocalAction.RechargeVehicleWeapon(player.GUID, turret.GUID, weapon.GUID)
)
case _ => ;
}
}
else if (weapon.Magazine == weapon.MaxMagazine && weaponAmmoRechargeTimer != Default.Cancellable) {
weaponAmmoRechargeTimer.cancel()
weaponAmmoRechargeTimer = Default.Cancellable
}
case _ => ;
}
if (weapon.Magazine == weapon.MaxMagazine && weaponAmmoRechargeTimer != Default.Cancellable) {
weaponAmmoRechargeTimer.cancel()
weaponAmmoRechargeTimer = Default.Cancellable
}
case _ => ;
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vehicles
import akka.actor.{ActorRef, Cancellable}
import akka.actor.ActorRef
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.deploy.Deployment
@ -18,8 +18,10 @@ import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
var ntuChargingTick: Cancellable = Default.Cancellable
var panelAnimationFunc: () => Unit = NoCharge
var ntuChargingTick = Default.Cancellable
findChargeTargetFunc = Vehicles.FindANTChargingSource
findDischargeTargetFunc = Vehicles.FindANTDischargingTarget
def TransferMaterial = Ntu.Nanites
@ -28,7 +30,8 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
def antBehavior: Receive = storageBehavior.orElse(transferBehavior)
def ActivatePanelsForChargingEvent(vehicle: NtuContainer): Unit = {
val zone = vehicle.Zone
val obj = ChargeTransferObject
val zone = obj.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vehicle.GUID, 52, 1L)
@ -37,7 +40,8 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
/** Charging */
def StartNtuChargingEvent(vehicle: NtuContainer): Unit = {
val zone = vehicle.Zone
val obj = ChargeTransferObject
val zone = obj.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vehicle.GUID, 49, 1L)
@ -190,7 +194,6 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
} else {
scala.math.min(min, chargeable.NtuCapacitor)
}
// var chargeToDeposit = Math.min(Math.min(chargeable.NtuCapacitor, 100), max)
chargeable.NtuCapacitor -= chargeToDeposit
UpdateNtuUI(chargeable)
sender ! Ntu.Grant(chargeable, chargeToDeposit)

View file

@ -0,0 +1,327 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vehicles
import akka.actor.ActorRef
import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.{NtuContainer, NtuContainerDefinition, _}
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.equipment.EquipmentSlot
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.serverobject.transfer.{TransferBehavior, TransferContainer}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
trait BfrTransferBehavior
extends TransferBehavior
with NtuStorageBehavior {
var ntuProcessingRequest: Boolean = false
var ntuProcessingTick = Default.Cancellable
findChargeTargetFunc = Vehicles.FindBfrChargingSource
findDischargeTargetFunc = Vehicles.FindBfrDischargingTarget
def TransferMaterial = Ntu.Nanites
private var pairedSlotList: Option[List[(VehicleSubsystem, (Int, EquipmentSlot))]] = None
/**
* Return the paired arm weapon subsystems with arm weapon equipment mount and the slot number for that mount,
* connecting "left" to "left" and "right" to "right".
* Either return the existing connection or create that connection for the first time and retain it for future use.
* Works regardless of the type of battleframe unit.
* @return the arm weapon subsystems for each arm weapon mount and that mount's slot number
*/
def pairedArmSlotSubsystems(): List[(VehicleSubsystem, (Int, EquipmentSlot))] = {
pairedSlotList.getOrElse {
val obj = ChargeTransferObject
val pairs = obj.Subsystems()
.filter { sub =>
sub.sys.name.startsWith("BattleframeLeftArm") || sub.sys.name.startsWith("BattleframeRightArm")
}
.zip(
obj.Weapons.filter { case (a, _) =>
a == 1 || a == 2 || a == 3 //gunner -> 2,3; flight -> 1,2
}
)
pairedSlotList = Some(pairs)
pairs
}
}
private var pairedList: Option[List[(VehicleSubsystem, EquipmentSlot)]] = None
/**
* Return the paired arm weapon subsystems with arm weapon mount,
* connecting "left" to "left" and "right" to "right".
* Either return the existing connection or create that connection for the first time and retain it for future use.
* Works regardless of the type of battleframe unit.
* @return the arm weapon subsystems for each arm weapon mount
*/
def pairedArmSubsystems(): List[(VehicleSubsystem, EquipmentSlot)] = {
pairedList.getOrElse {
val pairs = pairedArmSlotSubsystems().map { case (a, (_, c)) => (a, c) }
pairedList = Some(pairs)
pairs
}
}
def getNtuContainer(): Option[NtuContainer] = {
pairedArmSubsystems()
.find { case (sub, arm) =>
//find an active ntu siphon
arm.Equipment.nonEmpty &&
GlobalDefinitions.isBattleFrameNTUSiphon(arm.Equipment.get.Definition) &&
sub.Enabled
}
.map { d => d._2.Equipment.get } match {
case Some(equipment: Tool) =>
Some(new NtuSiphon(equipment, ChargeTransferObject.Definition))
case _ =>
None
}
}
def ChargeTransferObject: Vehicle with NtuContainer
def bfrBehavior: Receive = storageBehavior
.orElse(transferBehavior)
.orElse {
case BfrTransferBehavior.NextProcessTick(event) =>
transferTarget match {
case Some(target)
if event == transferEvent && ntuProcessingRequest && event == TransferBehavior.Event.Charging =>
HandleChargingOps(target)
case Some(target)
if event == transferEvent && ntuProcessingRequest && event == TransferBehavior.Event.Discharging =>
HandleDischargingEvent(target)
case Some(target)
if event == transferEvent && !ntuProcessingRequest =>
TryStopChargingEvent(target)
case _ => ;
TryStopChargingEvent(ChargeTransferObject)
}
}
def UpdateNtuUI(vehicle: Vehicle with NtuContainer): Unit = {
getNtuContainer() match {
case Some(siphon) =>
UpdateNtuUI(vehicle, siphon)
case None => ;
}
}
def UpdateNtuUI(vehicle: Vehicle with NtuContainer, siphon: NtuContainer): Unit = {
siphon match {
case equip: NtuSiphon =>
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
vehicle.Actor.toString,
VehicleAction.InventoryState2(PlanetSideGUID(0), equip.storageGUID, siphon.GUID, siphon.NtuCapacitor.toInt)
)
case _ => ;
}
}
def HandleChargingEvent(target: TransferContainer): Boolean = {
if (transferEvent == TransferBehavior.Event.None) {
HandleChargingOps(target)
} else {
ntuProcessingRequest = true
false
}
}
def HandleChargingOps(target: TransferContainer): Boolean = {
ntuProcessingRequest = false
getNtuContainer() match {
case Some(siphon: NtuSiphon)
if siphon.NtuCapacitor < siphon.MaxNtuCapacitor =>
//charging
transferTarget = Some(target)
transferEvent = TransferBehavior.Event.Charging
val max = siphon.NtuCapacitor
val fromMax = siphon.MaxNtuCapacitor - max
target match {
case _: WarpGate =>
//siphon.drain -> math.min(math.min(siphon.MaxNtuCapacitor / 75f, fromMax)
target.Actor ! BuildingActor.Ntu(NtuCommand.Request(math.min(siphon.drain.toFloat, fromMax), context.self))
case _: ResourceSilo =>
//siphon.drain -> scala.math.min(silo.MaxNtuCapacitor * 0.325f / max, fromMax)
target.Actor ! NtuCommand.Request(scala.math.min(0.5f * siphon.drain, fromMax), context.self)
case _ => ;
}
ntuProcessingTick.cancel()
ntuProcessingTick = context.system.scheduler.scheduleOnce(
delay = 1250 milliseconds,
self,
BfrTransferBehavior.NextProcessTick(transferEvent)
)
true
case _ =>
TryStopChargingEvent(ChargeTransferObject)
false
}
}
def ReceiveAndDepositUntilFull(vehicle: Vehicle, amount: Float): Boolean = {
getNtuContainer() match {
case Some(siphon) =>
ReceiveAndDepositUntilFull(vehicle, siphon, amount)
case None =>
false
}
}
def ReceiveAndDepositUntilFull(vehicle: Vehicle, obj: NtuContainer, amount: Float): Boolean = {
val isNotFull = (obj.NtuCapacitor += amount) < obj.MaxNtuCapacitor
UpdateNtuUI(vehicle, obj)
isNotFull
}
/** Discharging */
def HandleDischargingEvent(target: TransferContainer): Boolean = {
if (transferEvent == TransferBehavior.Event.None) {
HandleDischargingOps(target)
} else {
ntuProcessingRequest = true
false
}
}
def HandleDischargingOps(target: TransferContainer): Boolean = {
ntuProcessingRequest = false
val obj = ChargeTransferObject
getNtuContainer() match {
case Some(siphon)
if siphon.NtuCapacitor > 0 =>
transferTarget = Some(target)
transferEvent = TransferBehavior.Event.Discharging
target.Actor ! Ntu.Offer(obj)
ntuProcessingTick.cancel()
ntuProcessingTick = context.system.scheduler.scheduleOnce(
delay = 1250 milliseconds,
self,
BfrTransferBehavior.NextProcessTick(transferEvent)
)
true
case _ =>
TryStopChargingEvent(obj)
false
}
}
def WithdrawAndTransmit(vehicle: Vehicle, maxRequested: Float): Any = {
val chargeable = ChargeTransferObject
val chargeToDeposit = getNtuContainer() match {
case Some(siphon) =>
var chargeToDeposit = Math.min(Math.min(siphon.NtuCapacitor, 100), maxRequested)
siphon.NtuCapacitor -= chargeToDeposit
UpdateNtuUI(chargeable, siphon)
chargeToDeposit
case _ =>
0
}
Ntu.Grant(chargeable, chargeToDeposit)
}
/** Stopping */
override def TryStopChargingEvent(container: TransferContainer): Unit = {
ntuProcessingTick.cancel()
ntuProcessingRequest = false
transferTarget match {
case Some(target: WarpGate) =>
target.Actor ! BuildingActor.Ntu(NtuCommand.Grant(null, 0))
case Some(target) =>
target.Actor ! NtuCommand.Grant(null, 0)
case _ => ;
}
//cleanup
val obj = ChargeTransferObject
super.TryStopChargingEvent(obj)
}
def StopNtuBehavior(sender: ActorRef): Unit = TryStopChargingEvent(ChargeTransferObject)
def HandleNtuOffer(sender: ActorRef, src: NtuContainer): Unit = {}
def HandleNtuRequest(sender: ActorRef, min: Float, max: Float): Unit = {
val chargeable = ChargeTransferObject
getNtuContainer() match {
case Some(siphon) =>
if (transferEvent == TransferBehavior.Event.Discharging) {
val capacitor = siphon.NtuCapacitor
val bonus = System.currentTimeMillis()%2
val (chargeBase, chargeToDeposit): (Float, Float) = if (min == 0) {
transferTarget match {
case Some(silo: ResourceSilo) =>
// silos would charge from 0-30% in a full siphon's payload according to the wiki
val calcChargeBase = scala.math.min(scala.math.min(silo.MaxNtuCapacitor * 0.325f / siphon.MaxNtuCapacitor, capacitor), max)
(calcChargeBase, calcChargeBase + bonus)
case _ =>
(0f, 0)
}
} else {
val charge = scala.math.min(min, capacitor)
(charge, charge + bonus)
}
siphon.NtuCapacitor -= chargeBase
UpdateNtuUI(chargeable, siphon)
sender ! Ntu.Grant(chargeable, chargeToDeposit)
} else {
TryStopChargingEvent(chargeable)
sender ! Ntu.Grant(chargeable, 0)
}
case None => ;
}
}
def HandleNtuGrant(sender: ActorRef, src: NtuContainer, amount: Float): Unit = {
val obj = ChargeTransferObject
if (transferEvent != TransferBehavior.Event.Charging || !ReceiveAndDepositUntilFull(obj, amount)) {
sender ! Ntu.Request(0, 0)
}
}
}
object BfrTransferBehavior {
private case class NextProcessTick(eventType: TransferBehavior.Event.Value)
}
class NtuSiphon(
val equipment: Tool,
private val definition: ObjectDefinition with NtuContainerDefinition
) extends NtuContainer {
def Faction: PlanetSideEmpire.Value = equipment.Faction
def storageGUID: PlanetSideGUID = equipment.AmmoSlot.Box.GUID
def drain: Int = equipment.FireMode.RoundsPerShot
def NtuCapacitor: Float = equipment.Magazine.toFloat
def NtuCapacitor_=(value: Float): Float = equipment.Magazine_=(value.toInt).toFloat
def MaxNtuCapacitor: Float = equipment.MaxMagazine.toFloat
override def Definition: ObjectDefinition with NtuContainerDefinition = definition
def Actor: ActorRef = null
override def GUID : PlanetSideGUID = equipment.GUID
override def GUID_=(guid : PlanetSideGUID): PlanetSideGUID = equipment.GUID
override def Position: Vector3 = Vector3.Zero
override def Position_=(vec: Vector3): Vector3 = Vector3.Zero
override def Orientation: Vector3 = Vector3.Zero
override def Orientation_=(vec: Vector3): Vector3 = Vector3.Zero
override def Velocity: Option[Vector3] = None
override def Velocity_=(vec: Option[Vector3]): Option[Vector3] = None
}

View file

@ -223,7 +223,7 @@ object CarrierBehavior {
)
zone.VehicleEvents ! VehicleServiceMessage(
s"${cargo.Actor}",
VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))
VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, cargo.Definition.shieldUiAttribute, cargo.Shields))
)
CargoMountBehaviorForAll(carrier, cargo, mountPoint)
zone.actor ! ZoneActor.RemoveFromBlockMap(cargo)
@ -470,7 +470,7 @@ object CarrierBehavior {
)
events ! VehicleServiceMessage(
s"$cargoActor",
VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields))
VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, cargo.Definition.shieldUiAttribute, cargo.Shields))
)
zone.actor ! ZoneActor.AddToBlockMap(cargo, carrier.Position)
if (carrier.isFlying) {

View file

@ -0,0 +1,109 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vehicles
import net.psforever.objects.Vehicle
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality, SourceEntry}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.{DamageResolution, DamageType}
import net.psforever.objects.vital.etc.RadiationReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones.blockmap.SectorPopulation
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType}
import net.psforever.types.PlanetSideGUID
case object RadiationInVehicleInteraction extends ZoneInteractionType
/**
* This game entity may infrequently test whether it may interact with radiation cloud projectiles
* that may be emitted in the game environment for a limited amount of time.
* Since the entity in question is a vehicle, the occupants of the vehicle get tested their interaction.
*/
class InteractWithRadiationCloudsSeatedInVehicle(
private val obj: Vehicle,
val range: Float
) extends ZoneInteraction {
/**
* radiation clouds that, though detected, are skipped from affecting the target;
* in between interaction tests, a memory of the clouds that were tested last are retained and
* are excluded from being tested this next time;
* clouds that are detected a second time are cleared from the list and are available to be tested next time
*/
private var skipTargets: List[PlanetSideGUID] = List()
def Type = RadiationInVehicleInteraction
/**
* Drive into a radiation cloud and all the vehicle's occupants suffer the consequences.
* @param sector the portion of the block map being tested
* @param target the fixed element in this test
*/
override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
val position = target.Position
//collect all projectiles in sector/range
val projectiles = sector
.projectileList
.filter { cloud =>
val definition = cloud.Definition
definition.radiation_cloud &&
definition.AllDamageTypes.contains(DamageType.Radiation) &&
{
val radius = definition.DamageRadius
Zone.distanceCheck(target, cloud, radius * radius)
}
}
.distinct
val notSkipped = projectiles.filterNot { t => skipTargets.contains(t.GUID) }
skipTargets = notSkipped.map { _.GUID }
if (notSkipped.nonEmpty) {
(
//isolate one of each type of projectile
notSkipped
.foldLeft(Nil: List[Projectile]) {
(acc, next) => if (acc.exists { _.profile == next.profile }) acc else next :: acc
},
obj.Seats
.values
.collect { case seat => seat.occupant }
.flatten
) match {
case (uniqueProjectiles, targets) if uniqueProjectiles.nonEmpty && targets.nonEmpty =>
val shielding = obj.Definition.RadiationShielding
targets.foreach { t =>
uniqueProjectiles.foreach { p =>
t.Actor ! Vitality.Damage(
DamageInteraction(
SourceEntry(t),
RadiationReason(
ProjectileQuality.modifiers(p, DamageResolution.Radiation, t, t.Position, None),
t.DamageModel,
shielding
),
position
).calculate()
)
}
}
case _ => ;
}
}
obj.CargoHolds
.values
.collect {
case hold if hold.isOccupied =>
val target = hold.occupant.get
target.interaction().find { _.Type == RadiationInVehicleInteraction } match {
case Some(func) => func.interaction(sector, target)
case _ => ;
}
}
}
/**
* Any radiation clouds blocked from being tested should be cleared.
* All that can be done is blanking our retained previous effect targets.
* @param target the fixed element in this test
*/
def resetInteraction(target: InteractsWithZone): Unit = {
skipTargets = List()
}
}

View file

@ -15,21 +15,26 @@ trait MountableWeapons
* @param seatNumber the mount number
* @return a mounted weapon by index, or `None` if either the mount doesn't exist or there is no controlled weapon
*/
def WeaponControlledFromSeat(seatNumber: Int): Option[Equipment] = {
def WeaponControlledFromSeat(seatNumber: Int): Set[Equipment] = {
Definition
.asInstanceOf[MountableWeaponsDefinition]
.controlledWeapons.get(seatNumber) match {
case Some(wepNumber) if seats.get(seatNumber).nonEmpty => controlledWeapon(wepNumber)
case _ => None
.controlledWeapons().get(seatNumber) match {
case Some(wepNumbers) if seats.get(seatNumber).nonEmpty => wepNumbers.flatMap { controlledWeapon }
case _ => Set.empty
}
}
def controlledWeapon(wepNumber: Int): Option[Equipment] = ControlledWeapon(wepNumber)
def controlledWeapon(wepNumber: Int): Set[Equipment] = ControlledWeapon(wepNumber)
def ControlledWeapon(wepNumber: Int): Option[Equipment] = {
def ControlledWeapon(wepNumber: Int): Set[Equipment] = {
weapons.get(wepNumber) match {
case Some(slot) => slot.Equipment
case _ => None
case Some(slot) =>
slot.Equipment match {
case Some(weapon) => Set(weapon)
case None => Set.empty
}
case _ =>
Set.empty
}
}

View file

@ -8,5 +8,17 @@ import scala.collection.mutable
trait MountableWeaponsDefinition
extends MountedWeaponsDefinition
with MountableDefinition {
val controlledWeapons: mutable.HashMap[Int, Int] = mutable.HashMap[Int, Int]()
private val _controlledWeapons: mutable.HashMap[Int, Set[Int]] = mutable.HashMap[Int, Set[Int]]()
def controlledWeapons(): Map[Int, Set[Int]] = _controlledWeapons.toMap
def controlledWeapons(seat: Int, weapon: Int): Map[Int, Set[Int]] = {
_controlledWeapons.put(seat, Set(weapon))
_controlledWeapons.toMap
}
def controlledWeapons(seat: Int, weapons: Set[Int]): Map[Int, Set[Int]] = {
_controlledWeapons.put(seat, weapons)
_controlledWeapons.toMap
}
}

File diff suppressed because it is too large Load diff

View file

@ -21,9 +21,6 @@ class AntControl(vehicle: Vehicle)
with AntTransferBehavior {
def ChargeTransferObject = vehicle
findChargeTargetFunc = Vehicles.FindANTChargingSource
findDischargeTargetFunc = Vehicles.FindANTDischargingTarget
override def commonEnabledBehavior: Receive = super.commonEnabledBehavior.orElse(antBehavior)
/**

View file

@ -2,15 +2,17 @@
package net.psforever.objects.vehicles.control
import net.psforever.objects._
import net.psforever.objects.equipment.{EffectTarget, TargetValidation}
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.projectile.MaxDistanceCutoff
import net.psforever.objects.vital.prop.DamageWithPosition
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{TriggerEffectMessage, TriggeredEffectLocation}
import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import net.psforever.types.PlanetSideGUID
/**
* A vehicle control agency exclusive to the armored personnel carrier (APC) ground transport vehicles.
@ -20,97 +22,112 @@ import scala.concurrent.duration._
* @param vehicle the APC
*/
class ApcControl(vehicle: Vehicle)
extends VehicleControl(vehicle) {
protected var capacitor = Default.Cancellable
startCapacitorTimer()
extends VehicleControl(vehicle)
with VehicleCapacitance {
def CapacitanceObject: Vehicle = vehicle
override def postStop() : Unit = {
super.postStop()
capacitor.cancel()
capacitancePostStop()
}
override def commonEnabledBehavior : Receive =
super.commonEnabledBehavior
.orElse {
case ApcControl.CapacitorCharge(amount) =>
if (vehicle.Capacitor < vehicle.Definition.MaxCapacitor) {
val capacitance = vehicle.Capacitor += amount
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
self.toString(),
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vehicle.GUID, 113, capacitance)
)
startCapacitorTimer()
} else {
capacitor = Default.Cancellable
}
override def commonEnabledBehavior : Receive = super.commonEnabledBehavior
.orElse(capacitorBehavior)
.orElse {
case SpecialEmp.Burst() =>
performEmpBurst()
case SpecialEmp.Burst() =>
if (vehicle.Capacitor == vehicle.Definition.MaxCapacitor) { //only if the capacitor is full
val zone = vehicle.Zone
val events = zone.VehicleEvents
val pos = vehicle.Position
val GUID0 = Service.defaultPlayerGUID
val emp = vehicle.Definition.innateDamage.getOrElse { SpecialEmp.emp }
val faction = vehicle.Faction
//drain the capacitor
vehicle.Capacitor = 0
events ! VehicleServiceMessage(
self.toString(),
VehicleAction.PlanetsideAttribute(GUID0, vehicle.GUID, 113, 0)
)
//cause the emp
events ! VehicleServiceMessage(
zone.id,
VehicleAction.SendResponse(
GUID0,
TriggerEffectMessage(
GUID0,
s"apc_explosion_emp_${faction.toString.toLowerCase}",
None,
Some(TriggeredEffectLocation(pos, vehicle.Orientation))
)
)
)
//resolve what targets are affected by the emp
Zone.serverSideDamage(
zone,
vehicle,
emp,
SpecialEmp.createEmpInteraction(emp, pos),
ExplosiveDeployableControl.detectionForExplosiveSource(vehicle),
Zone.findAllTargets
)
//start charging again
startCapacitorTimer()
}
}
case _ => ;
}
def performEmpBurst(): Unit = {
val obj = CapacitanceObject
if (obj.Capacitor == obj.Definition.MaxCapacitor) { //only if the capacitor is full
val zone = obj.Zone
val events = zone.VehicleEvents
val pos = obj.Position
val GUID0 = Service.defaultPlayerGUID
val emp = ApcControl.apc_emp
val faction = obj.Faction
//drain the capacitor
capacitorCharge(-vehicle.Capacitor)
//cause the emp
events ! VehicleServiceMessage(
zone.id,
VehicleAction.SendResponse(
GUID0,
TriggerEffectMessage(
GUID0,
s"apc_explosion_emp_${faction.toString.toLowerCase}",
None,
Some(TriggeredEffectLocation(pos, obj.Orientation))
)
)
)
//resolve what targets are affected by the emp
Zone.serverSideDamage(
zone,
obj,
emp,
SpecialEmp.createEmpInteraction(emp, pos),
ExplosiveDeployableControl.detectionForExplosiveSource(obj),
Zone.findAllTargets
)
//start charging again
//startCapacitorTimer()
}
}
override def PrepareForDisabled(kickPassengers: Boolean) : Unit = {
super.PrepareForDisabled(kickPassengers)
capacitor.cancel()
capacitanceStop()
}
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
capacitor.cancel()
vehicle.Capacitor = 0
capacitancePostStop()
}
//TODO switch from magic numbers to definition numbers?
private def startCapacitorTimer(): Unit = {
capacitor = context.system.scheduler.scheduleOnce(
delay = 1000 millisecond,
self,
ApcControl.CapacitorCharge(10)
)
override def parseObjectAction(guid: PlanetSideGUID, action: Int, other: Option[Any]): Unit = {
super.parseObjectAction(guid, action, other)
if (action == 55) {
performEmpBurst()
}
}
}
object ApcControl {
/**
* Charge the vehicle's internal capacitor by the given amount during the schedulefd charge event.
* @param amount how much energy in the charge
*/
private case class CapacitorCharge(amount: Int)
final val apc_emp = new DamageWithPosition {
CausesDamageType = DamageType.Splash
SympatheticExplosion = true
Damage0 = 0
DamageAtEdge = 1.0f
DamageRadius = 15f
AdditionalEffect = true
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Player,
EffectTarget.Validation.Player
) -> 1000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.AMS
) -> 5000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.MotionSensor
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Deployable,
EffectTarget.Validation.Spitfire
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Turret,
EffectTarget.Validation.Turret
) -> 30000
JammedEffectDuration += TargetValidation(
EffectTarget.Category.Vehicle,
EffectTarget.Validation.VehicleNotAMS
) -> 10000
Modifiers = MaxDistanceCutoff
}
}

View file

@ -0,0 +1,631 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vehicles.control
import akka.actor.Cancellable
import net.psforever.objects._
import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition}
import net.psforever.objects.equipment._
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.containable.ContainableBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.transfer.TransferBehavior
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.VehicleShieldCharge
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.Zone
import net.psforever.packet.game._
import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
/**
* A vehicle control agency exclusive to the battleframe robotics (BFR) combat vehicle system.
* @param vehicle the battleframe robotics unit
*/
class BfrControl(vehicle: Vehicle)
extends VehicleControl(vehicle)
with BfrTransferBehavior
with ArmorSiphonBehavior.SiphonOwner {
/** shield-auto charge */
var shieldCharge: Cancellable = Default.Cancellable
def SiphoningObject = vehicle
def ChargeTransferObject = vehicle
if (vehicle.Shields < vehicle.MaxShields) {
chargeShields(amount = 0) //start charging if starts as uncharged
}
override def postStop(): Unit = {
super.postStop()
shieldCharge.cancel()
repairPostStop()
}
def explosionBehavior: Receive = {
case BfrControl.VehicleExplosion =>
val guid = vehicle.GUID
val guid0 = Service.defaultPlayerGUID
val zone = vehicle.Zone
val zoneid = zone.id
val events = zone.VehicleEvents
events ! VehicleServiceMessage(
zoneid,
VehicleAction.GenericObjectAction(guid0, guid, 46)
)
context.system.scheduler.scheduleOnce(delay = 500 milliseconds, self, BfrControl.VehicleExplosion)
}
override def commonEnabledBehavior: Receive = super.commonEnabledBehavior
.orElse(siphonRepairBehavior)
.orElse(bfrBehavior)
.orElse(explosionBehavior)
.orElse {
case CommonMessages.Use(_, Some(item: Tool)) =>
if (GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition)) {
context.system.scheduler.scheduleOnce(
delay = 1000 milliseconds,
self,
TransferBehavior.Charging(Ntu.Nanites)
)
}
case SpecialEmp.Burst() =>
performEmpBurst()
}
override def commonDisabledBehavior: Receive = super.commonDisabledBehavior.orElse(explosionBehavior)
override def PrepareForDisabled(kickPassengers: Boolean) : Unit = {
super.PrepareForDisabled(kickPassengers)
if (vehicle.Health == 0) {
//shield off
disableShield()
}
}
override def DamageAwareness(target: Target, cause: DamageResult, amount: Any) : Unit = {
super.DamageAwareness(target, cause, amount)
//manage shield display and charge
disableShieldIfDrained()
if (shieldCharge != Default.Cancellable && vehicle.Shields < vehicle.MaxShields) {
shieldCharge.cancel()
shieldCharge = context.system.scheduler.scheduleOnce(
delay = vehicle.Definition.ShieldDamageDelay milliseconds,
self,
Vehicle.ChargeShields(0)
)
}
}
override def destructionDelayed(delay: Long, cause: DamageResult): Unit = {
super.destructionDelayed(delay, cause)
shieldCharge.cancel()
shieldCharge = Default.Cancellable
//harmless boom boom's
context.system.scheduler.scheduleOnce(delay = 0 milliseconds, self, BfrControl.VehicleExplosion)
}
override def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
shieldCharge.cancel()
shieldCharge = Default.Cancellable
disableShield()
}
override def RemoveItemFromSlotCallback(item: Equipment, slot: Int): Unit = {
BfrControl.dimorphics.find { _.contains(item.Definition) } match {
case Some(dimorph) if vehicle.VisibleSlots.contains(slot) => //revert to a generic variant
Tool.LoadDefinition(
item.asInstanceOf[Tool],
dimorph.transform(Handiness.Generic).asInstanceOf[ToolDefinition]
)
case _ => ; //no dimorphic entry; place as-is
}
val guid0 = PlanetSideGUID(0)
//if the weapon arm is disabled, enable it for later (makes life easy)
parseObjectAction(guid0, BfrControl.ArmState.Enabled, Some(slot))
//enable the other arm weapon regardless
parseObjectAction(guid0, BfrControl.ArmState.Enabled, Some(
//budget logic: the arm weapons are "next to each other" index-wise
if (vehicle.Weapons.keys.min == slot) { slot + 1 } else { slot - 1 }
))
super.RemoveItemFromSlotCallback(item, slot)
}
override def PutItemInSlotCallback(item: Equipment, slot: Int): Unit = {
val definition = item.Definition
val handiness = BfrControl.dimorphics.find { _.contains(definition) } match {
case Some(dimorph) if vehicle.VisibleSlots.contains(slot) => //left-handed or right-handed variant
val handiness = bfrHandiness(slot)
Tool.LoadDefinition(
item.asInstanceOf[Tool],
dimorph.transform(handiness).asInstanceOf[ToolDefinition]
)
handiness
case Some(dimorph) => //revert to a generic variant
Tool.LoadDefinition(
item.asInstanceOf[Tool],
dimorph.transform(Handiness.Generic).asInstanceOf[ToolDefinition]
)
Handiness.Generic
case None => //no dimorphic entry; place as-is
Handiness.Generic
}
super.PutItemInSlotCallback(item, slot)
specialArmWeaponEquipManagement(item, slot, handiness)
}
override def dismountCleanup(seatBeingDismounted: Int): Unit = {
super.dismountCleanup(seatBeingDismounted)
if (!vehicle.Seats.values.exists(_.isOccupied)) {
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
case Some(subsys) =>
if (vehicle.Shields > 0) {
vehicleSubsystemMessages(
if (subsys.Enabled && !subsys.Enabled_=(state = false)) {
//turn off shield visually
subsys.changedMessages(vehicle)
} else if (subsys.Jammed || subsys.stateOfStatus(statusName = "Damaged").contains(false)) {
//hard coded: shield is "off" functionally, turn off static effect and turn off standard shield swirl
ComponentDamageMessage(vehicle.GUID, SubsystemComponent.ShieldGeneratorOffline, None) +:
BattleframeShieldGeneratorOffline.getMessage(SubsystemComponent.ShieldGeneratorOffline, vehicle, vehicle.GUID)
} else {
//shield is already off visually
Nil
}
)
}
case _ => ;
}
}
}
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
} else {
true
}
}
override def handleTerminalMessageVehicleLoadout(
player: Player,
definition: VehicleDefinition,
weapons: List[InventoryItem],
inventory: List[InventoryItem]
): (
List[(Equipment, PlanetSideGUID)],
List[InventoryItem],
List[(Equipment, PlanetSideGUID)],
List[InventoryItem]
) = {
val vFaction = vehicle.Faction
val vWeapons = vehicle.Weapons
//remove old inventory
val oldInventory = vehicle.Inventory.Clear().map { case InventoryItem(obj, _) => (obj, obj.GUID) }
//"dropped" items are lost; if it doesn't go in the trunk, it vanishes into the nanite cloud
val (_, afterInventory) = inventory.partition(ContainableBehavior.DropPredicate(player))
val pairedArmSubsys = pairedArmSubsystems()
val (oldWeapons, newWeapons, finalInventory) = if (GlobalDefinitions.isBattleFrameVehicle(definition)) {
//vehicles are both battleframes; weapons must be swapped properly
if(vWeapons.size == 3 && GlobalDefinitions.isBattleFrameFlightVehicle(definition)) {
//battleframe is a gunner variant but loadout spec is for flight variant
// remap the hands, ignore the gunner weapon mount, and refit the trunk
val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory)
val afterWeapons = weapons
.map { item => item.start += 1; item }
(culledWeaponMounts(pairedArmSubsys.unzip._2), afterWeapons, stow)
} else if(vWeapons.size == 2 && GlobalDefinitions.isBattleFrameGunnerVehicle(definition)) {
//battleframe is a flight variant but loadout spec is for gunner variant
// remap the hands, shave the gunner mount from the spec, and refit the trunk
val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory)
val afterWeapons = weapons
.filterNot { _.obj.Size == EquipmentSize.BFRGunnerWeapon }
.map { item => item.start -= 1; item }
(culledWeaponMounts(vWeapons.values), afterWeapons, stow)
} else {
//same variant type of battleframe
// place as-is
(culledWeaponMounts(vWeapons.values), weapons, afterInventory)
}
}
else {
//vehicle loadout is not for this vehicle; do not transfer over weapon ammo
if (
vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset
) {
(Nil, Nil, afterInventory) //trunk is the same dimensions, however
}
else {
//accommodate as much of inventory as possible
val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory)
(Nil, Nil, stow)
}
}
finalInventory.foreach {
_.obj.Faction = vFaction
}
(oldWeapons, newWeapons, oldInventory, finalInventory)
}
def culledWeaponMounts(values: Iterable[EquipmentSlot]): List[(Equipment, PlanetSideGUID)] = {
values.collect { case slot if slot.Equipment.nonEmpty =>
val obj = slot.Equipment.get
slot.Equipment = None
(obj, obj.GUID)
}.toList
}
def disableShieldIfDrained(): Unit = {
if (vehicle.Shields == 0) {
disableShield()
}
}
def disableShield(): Unit = {
val zone = vehicle.Zone
zone.VehicleEvents ! VehicleServiceMessage(
s"${zone.id}",
VehicleAction.SendResponse(PlanetSideGUID(0), GenericObjectActionMessage(vehicle.GUID, 45))
)
}
def enableShieldIfNotDrained(): Unit = {
if (vehicle.Shields > 0) {
enableShield()
}
}
def enableShield(): Unit = {
val zone = vehicle.Zone
zone.VehicleEvents ! VehicleServiceMessage(
s"${zone.id}",
VehicleAction.SendResponse(PlanetSideGUID(0), GenericObjectActionMessage(vehicle.GUID, 44))
)
}
override def chargeShields(amount: Int): Unit = {
chargeShieldsOnly(amount)
shieldCharge(vehicle.Shields, vehicle.Definition, delay = 0) //continue charge?
}
def chargeShieldsOnly(amount: Int): Unit = {
val definition = vehicle.Definition
val before = vehicle.Shields
if (canChargeShields()) {
val chargeAmount = math.max(1, ((if (vehicle.DeploymentState == DriveState.Kneeling && vehicle.Seats(0).occupant.nonEmpty) {
definition.ShieldAutoRechargeSpecial
} else {
definition.ShieldAutoRecharge
}).getOrElse(amount) * vehicle.SubsystemStatusMultiplier(sys = "BattleframeShieldGenerator.RechargeRate")).toInt)
vehicle.Shields = before + chargeAmount
val after = vehicle.Shields
vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), after - before))
showShieldCharge()
if (before == 0 && after > 0) {
enableShield()
}
}
}
def shieldCharge(delay: Long): Unit = {
shieldCharge(vehicle.Shields, vehicle.Definition, delay)
}
def shieldCharge(after:Int, definition: VehicleDefinition, delay: Long): Unit = {
shieldCharge.cancel()
if (after < definition.MaxShields && !vehicle.Jammed) {
shieldCharge = context.system.scheduler.scheduleOnce(
delay = definition.ShieldPeriodicDelay + delay milliseconds,
self,
Vehicle.ChargeShields(0)
)
} else {
shieldCharge = Default.Cancellable
}
}
def showShieldCharge(): Unit = {
val vguid = vehicle.GUID
val zone = vehicle.Zone
val shields = vehicle.Shields
zone.VehicleEvents ! VehicleServiceMessage(
s"${vehicle.Actor}",
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vguid, vehicle.Definition.shieldUiAttribute, shields)
)
}
override def StartJammeredStatus(target: Any, dur: Int): Unit = {
super.StartJammeredStatus(target, dur)
//cancels shield charge timer
shieldCharge(after = 0, vehicle.Definition, delay = 0)
}
override def CancelJammeredStatus(target: Any): Unit = {
super.CancelJammeredStatus(target)
//restarts shield charge timer
shieldCharge(vehicle.Shields, vehicle.Definition, delay = 100)
}
override def JammableMountedWeaponsJammeredStatus(target: PlanetSideServerObject with MountedWeapons, statusCode: Int): Unit = {
/** bfr weapons do not jam the same way normal vehicle weapons do */
}
override def parseObjectAction(guid: PlanetSideGUID, action: Int, other: Option[Any]): Unit = {
super.parseObjectAction(guid, action, other)
if (action == BfrControl.ArmState.Enabled || action == BfrControl.ArmState.Disabled) {
//disable or enable fire control for the left arm weapon or for the right arm weapon
((other match {
case Some(slot: Int) => (slot, bfrHandSubsystem(bfrHandiness(slot)))
case _ =>
vehicle.Weapons.find { case (_, slot) => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid } match {
case Some((slot, _)) => (slot, bfrHandSubsystem(bfrHandiness(slot)))
case _ => (0, None)
}
}) match {
case out @ (_, Some(subsystem)) =>
if (action == BfrControl.ArmState.Enabled && !subsystem.Enabled) {
subsystem.Enabled = true
out
} else if (action == BfrControl.ArmState.Disabled && subsystem.Enabled) {
subsystem.Enabled = false
out
} else {
(0, None)
}
case _ =>
(0, None)
}) match {
case (slot, Some(_)) =>
specialArmWeaponActiveManagement(slot)
val guid0 = Service.defaultPlayerGUID
val doNotSendTo = other match {
case Some(pguid: PlanetSideGUID) => pguid
case _ => guid0
}
(if (guid == guid0) {
vehicle.Weapons(slot).Equipment match {
case Some(equip) => Some(equip.GUID)
case None => None
}
} else {
Some(guid)
}) match {
case Some(useThisGuid) =>
val zone = vehicle.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.GenericObjectAction(doNotSendTo, useThisGuid, action)
)
case _ => ;
}
case _ => ;
}
}
}
def bfrHandiness(side: equipment.Hand): Int = {
if (side == Handiness.Left) 2
else if (side == Handiness.Right) 3
else throw new Exception("no hand associated with this slot")
}
def bfrHandiness(slot: Int): equipment.Hand = {
//for the benefit of BFR equipment slots interacting with MoveItemMessage
if (slot == 2) Handiness.Left
else if (slot == 3) Handiness.Right
else Handiness.Generic
}
def bfrHandSubsystem(side: equipment.Hand): Option[VehicleSubsystem] = {
//for the benefit of BFR equipment slots interacting with MoveItemMessage
side match {
case Handiness.Left => vehicle.Subsystems(VehicleSubsystemEntry.BattleframeLeftArm)
case Handiness.Right => vehicle.Subsystems(VehicleSubsystemEntry.BattleframeRightArm)
case _ => None
}
}
def specialArmWeaponEquipManagement(item: Equipment, slot: Int, 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
val firstArmSlot = vehicle.Weapons.keys.min
val otherArmSlot = if (firstArmSlot == slot) {
slot + 1
}
else {
slot - 1
}
val otherArmEquipment = weapons(otherArmSlot).Equipment
if ( {
val itemDef = item.Definition
GlobalDefinitions.isBattleFrameArmorSiphon(itemDef) || GlobalDefinitions.isBattleFrameNTUSiphon(itemDef)
} ||
(otherArmEquipment match {
case Some(thing) =>
//some equipment is attached to the other arm weapon mount
val otherDef = thing.Definition
GlobalDefinitions.isBattleFrameArmorSiphon(otherDef) || GlobalDefinitions.isBattleFrameNTUSiphon(otherDef)
case None =>
false
})
) {
//installing a siphon; this siphon can safely be disabled
//alternately, installing normal equipment, but the other arm weapon is a siphon
parseObjectAction(PlanetSideGUID(0), BfrControl.ArmState.Enabled, Some(otherArmSlot)) //ensure enabled
parseObjectAction(item.GUID, BfrControl.ArmState.Disabled, Some(slot))
}
}
}
/** since `specialArmWeaponActiveManagement` is called from `parseObjectAction`,
* and `parseObjectAction` gets called in `specialArmWeaponActiveManagement`,
* kill endless logic loops before they can happen */
var notSpecialManagingArmWeapon: Boolean = true
def specialArmWeaponActiveManagement(slotChanged: Int): Unit = {
if (notSpecialManagingArmWeapon) {
notSpecialManagingArmWeapon = false
val (thisArm, otherArm) = {
val pairedSystemsToSlots = pairedArmSlotSubsystems()
if (pairedSystemsToSlots.head._2._1 == slotChanged) {
(pairedSystemsToSlots.head, pairedSystemsToSlots(1))
}
else {
(pairedSystemsToSlots(1), pairedSystemsToSlots.head)
}
}
if (thisArm._1.Enabled) {
//this arm weapon slot was enabled
if ({
val (thisArmExists, thisArmIsSiphon) = thisArm._2._2.Equipment match {
case Some(thing) =>
//some equipment is attached to the other arm weapon mount
val definition = thing.Definition
(
true,
GlobalDefinitions.isBattleFrameArmorSiphon(definition) || GlobalDefinitions.isBattleFrameNTUSiphon(definition)
)
case None =>
(false, false)
}
val (otherArmExists, otherArmIsSiphon) = otherArm._2._2.Equipment match {
case Some(thing) =>
//some equipment is attached to the other arm weapon mount
val definition = thing.Definition
(
true,
GlobalDefinitions.isBattleFrameArmorSiphon(definition) || GlobalDefinitions.isBattleFrameNTUSiphon(definition)
)
case None =>
(false, false)
}
thisArmExists && otherArmExists && (thisArmIsSiphon || otherArmIsSiphon)
}) {
//both arms weapons are installed and at least one of them is a siphon
parseObjectAction(PlanetSideGUID(0), BfrControl.ArmState.Disabled, Some(otherArm._2._1))
}
}
else {
//this arm weapon slot was disabled
thisArm._2._2.Equipment match {
case Some(item) =>
parseObjectAction(item.GUID, BfrControl.ArmState.Enabled, Some(otherArm._2._1)) //other arm must be enabled
case None =>
parseObjectAction(PlanetSideGUID(0), BfrControl.ArmState.Enabled, Some(thisArm._2._1)) //must stay enabled
}
}
notSpecialManagingArmWeapon = true
}
}
def performEmpBurst(): Unit = {
val now = System.currentTimeMillis()
val obj = ChargeTransferObject
val zone = obj.Zone
val events = zone.VehicleEvents
val GUID0 = Service.defaultPlayerGUID
getNtuContainer() match {
case Some(siphon : NtuSiphon)
if GlobalDefinitions.isBattleFrameNTUSiphon(siphon.equipment.Definition) &&
siphon.equipment.FireModeIndex == 1 &&
siphon.NtuCapacitor > 29 =>
val elapsedWait = now - siphon.equipment.lastDischarge
if (elapsedWait >= 30000) {
val pos = obj.Position
val emp = siphon.equipment.Projectile
val faction = obj.Faction
//need at least 30 ntu, so consume the charge
siphon.NtuCapacitor -= 30
UpdateNtuUI(obj, siphon)
//cause the emp
siphon.equipment.lastDischarge = now
//TODO this is the apc emp effect; is there an ntu siphon emp effect?
events ! VehicleServiceMessage(
zone.id,
VehicleAction.SendResponse(
GUID0,
TriggerEffectMessage(
GUID0,
s"apc_explosion_emp_${faction.toString.toLowerCase}",
None,
Some(TriggeredEffectLocation(pos, obj.Orientation))
)
)
)
//resolve what targets are affected by the emp
Zone.serverSideDamage(
zone,
obj,
emp,
SpecialEmp.createEmpInteraction(emp, pos),
ExplosiveDeployableControl.detectionForExplosiveSource(obj),
Zone.findAllTargets
)
} else {
//the siphon is not ready to dispatch another emp; chat message borrowed from kit use logic
//the client actually enforces a hard limit of 30s before it will react to use of the siphon emp mode
//it does not even dispatch the packet before that, making it rare if this precautionary message is seen
events ! VehicleServiceMessage(
obj.Seats(0).occupant.get.Name,
VehicleAction.SendResponse(
GUID0,
ChatMsg(ChatMessageType.UNK_225, wideContents = false, "", s"@TimeUntilNextUse^${30000 - elapsedWait}", None)
)
)
}
case _ => ;
}
}
}
object BfrControl {
/** arm state values related to the `GenericObjectActionMessage` action codes */
object ArmState extends Enumeration {
final val Enabled = 38
final val Disabled = 39
}
private case object VehicleExplosion
val dimorphics: List[EquipmentHandiness] = {
import GlobalDefinitions._
List(
EquipmentHandiness(aphelion_armor_siphon, aphelion_armor_siphon_left, aphelion_armor_siphon_right),
EquipmentHandiness(aphelion_laser, aphelion_laser_left, aphelion_laser_right),
EquipmentHandiness(aphelion_ntu_siphon, aphelion_ntu_siphon_left, aphelion_ntu_siphon_right),
EquipmentHandiness(aphelion_ppa, aphelion_ppa_left, aphelion_ppa_right),
EquipmentHandiness(aphelion_starfire, aphelion_starfire_left, aphelion_starfire_right),
EquipmentHandiness(colossus_armor_siphon, colossus_armor_siphon_left, colossus_armor_siphon_right),
EquipmentHandiness(colossus_burster, colossus_burster_left, colossus_burster_right),
EquipmentHandiness(colossus_chaingun, colossus_chaingun_left, colossus_chaingun_right),
EquipmentHandiness(colossus_ntu_siphon, colossus_ntu_siphon_left, colossus_ntu_siphon_right),
EquipmentHandiness(colossus_tank_cannon, colossus_tank_cannon_left, colossus_tank_cannon_right),
EquipmentHandiness(peregrine_armor_siphon, peregrine_armor_siphon_left, peregrine_armor_siphon_right),
EquipmentHandiness(peregrine_dual_machine_gun, peregrine_dual_machine_gun_left, peregrine_dual_machine_gun_right),
EquipmentHandiness(peregrine_mechhammer, peregrine_mechhammer_left, peregrine_mechhammer_right),
EquipmentHandiness(peregrine_ntu_siphon, peregrine_ntu_siphon_left, peregrine_ntu_siphon_right),
EquipmentHandiness(peregrine_sparrow, peregrine_sparrow_left, peregrine_sparrow_right)
)
}
}

View file

@ -0,0 +1,148 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vehicles.control
import net.psforever.objects.equipment.Handiness
import net.psforever.objects.{Vehicle, equipment}
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.vehicles.{VehicleSubsystem, VehicleSubsystemEntry}
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.types.Vector3
/**
* ...
*/
class BfrFlightControl(vehicle: Vehicle)
extends BfrControl(vehicle)
with VehicleCapacitance {
def CapacitanceObject: Vehicle = vehicle
var flying: Option[Boolean] = None
override def postStop() : Unit = {
super.postStop()
capacitancePostStop()
}
override def commonEnabledBehavior: Receive = super.commonEnabledBehavior
.orElse(capacitorBehavior)
.orElse {
case BfrFlight.Soaring(flightValue) =>
val localFlyingValue = flying
vehicle.Flying = Some(flightValue)
//capacitor drain
if (vehicle.Capacitor > 0) {
val definition = vehicle.Definition
val (_, cdrain) = if (flightValue == 0 || flightValue == -0) {
(0, vehicle.Capacitor)
} else {
val vdrain = if (flightValue > 0) definition.CapacitorDrain else 0
val hdrain = if ({
val vec = vehicle.Velocity.getOrElse(Vector3.Zero).xy
vec.x > 0.5f || vec.y > 0.5f
}) definition.CapacitorDrainSpecial else 0
(vdrain, vdrain + hdrain)
}
flying = Some(if (cdrain > 0) {
val modDrain = math.max(1, (cdrain * vehicle.SubsystemStatusMultiplier(sys = "BattleframeFlightPod.UseRate")).toInt)
if (super.capacitorOnlyCharge(-modDrain) || vehicle.Capacitor < vehicle.Definition.MaxCapacitor) {
startCapacitorTimer()
}
true
} else {
false
})
}
//shield drain
if (vehicle.Shields > 0) {
vehicle.Definition.ShieldDrain match {
case Some(drain) if localFlyingValue.isEmpty =>
//shields off
disableShield()
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = false
vehicle.Shields -= drain
showShieldCharge()
case None if localFlyingValue.isEmpty =>
//shields off
disableShield()
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = false
case Some(drain) =>
vehicle.Shields -= drain
showShieldCharge()
case _ => ;
}
}
if (vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightPod).get.Jammed) {
}
case BfrFlight.Landed =>
if (flying.nonEmpty) {
flying = None
vehicle.Flying = None
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = true
if (vehicle.Shields > 0) {
enableShield()
}
shieldCharge(delay = 2000)
}
case _ => ;
}
override def PrepareForDisabled(kickPassengers: Boolean) : Unit = {
super.PrepareForDisabled(kickPassengers)
capacitanceStop()
}
override def destructionDelayed(delay: Long, cause: DamageResult): Unit = {
super.destructionDelayed(delay, cause)
capacitanceStop()
}
override def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
capacitancePostStop()
}
override def chargeShieldsOnly(amount: Int): Unit = {
if (flying != null && (flying.isEmpty || flying.contains(false))) {
super.chargeShieldsOnly(amount)
}
}
override protected def capacitorOnlyCharge(amount: Int): Boolean = {
if (flying.isEmpty || flying.contains(false)) {
val mod = math.max(1, amount * vehicle.SubsystemStatusMultiplier(sys = "BattleframeFlightPod.RechargeRate").toInt)
super.capacitorOnlyCharge(mod)
} else {
false
}
}
override def bfrHandiness(side: equipment.Hand): Int = {
if (side == Handiness.Left) 1
else if (side == Handiness.Right) 2
else throw new Exception("no hand associated with this slot; caller screwed up")
}
override def bfrHandiness(slot: Int): equipment.Hand = {
//for the benefit of BFR equipment slots interacting with MoveItemMessage
if (slot == 1) Handiness.Left
else if (slot == 2) Handiness.Right
else Handiness.Generic
}
override def bfrHandSubsystem(side: equipment.Hand): Option[VehicleSubsystem] = {
//for the benefit of BFR equipment slots interacting with MoveItemMessage
side match {
case Handiness.Left => vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightLeftArm)
case Handiness.Right => vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightRightArm)
case _ => None
}
}
}
object BfrFlight {
final case class Soaring(flyingValue: Int)
case object Landed
}

View file

@ -0,0 +1,87 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vehicles.control
import akka.actor.Actor
import net.psforever.objects._
import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
/**
* ...
*/
trait VehicleCapacitance {
_: Actor =>
def CapacitanceObject: Vehicle
protected var capacitor = Default.Cancellable
startCapacitorTimer()
def capacitanceStop(): Unit = {
capacitor.cancel()
}
def capacitanceStopAndBlank(): Unit = {
capacitor.cancel()
capacitor = Default.Cancellable
}
def capacitancePostStop(): Unit = {
capacitanceStopAndBlank()
CapacitanceObject.Capacitor = 0
}
def capacitorBehavior: Receive = {
case VehicleCapacitance.CapacitorCharge(amount) =>
capacitorCharge(amount)
}
protected def capacitorCharge(amount: Int): Boolean = {
capacitorOnlyCharge(amount)
startCapacitorTimer()
true
}
protected def capacitorOnlyCharge(amount: Int): Boolean = {
val obj = CapacitanceObject
val capacitorBefore = obj.Capacitor
val capacitorAfter = obj.Capacitor += amount
if (capacitorBefore != capacitorAfter) {
showCapacitorCharge()
true
} else {
false
}
}
protected def showCapacitorCharge(): Unit = {
val obj = CapacitanceObject
obj.Zone.VehicleEvents ! VehicleServiceMessage(
self.toString(),
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, obj.GUID, 113, obj.Capacitor)
)
}
protected def startCapacitorTimer(): Unit = {
val obj = CapacitanceObject
if (obj.Capacitor < obj.Definition.MaxCapacitor) {
capacitor.cancel()
capacitor = context.system.scheduler.scheduleOnce(
delay = 1000 millisecond,
self,
VehicleCapacitance.CapacitorCharge(obj.Definition.CapacitorRecharge)
)
}
}
}
object VehicleCapacitance {
/**
* Charge the vehicle's internal capacitor by the given amount during the scheduled charge event.
* @param amount how much energy in the charge
*/
private case class CapacitorCharge(amount: Int)
}

View file

@ -1,15 +1,17 @@
// Copyright (c) 2017-2021 PSForever
package net.psforever.objects.vehicles.control
import akka.actor.{Actor, Cancellable}
import akka.actor.Cancellable
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects._
import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.definition.VehicleDefinition
import net.psforever.objects.definition.converter.OCM
import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.equipment.{Equipment, EquipmentSlot, JammableMountedWeapons}
import net.psforever.objects.equipment.{ArmorSiphonBehavior, Equipment, EquipmentSlot, JammableMountedWeapons}
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObjectControl}
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle}
@ -20,10 +22,11 @@ import net.psforever.objects.serverobject.repair.RepairableVehicle
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.VehicleShieldCharge
import net.psforever.objects.vital.{DamagingActivity, VehicleShieldCharge, VitalsActivity}
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.zones._
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.types._
@ -43,10 +46,11 @@ import scala.concurrent.duration._
* @param vehicle the `Vehicle` object being governed
*/
class VehicleControl(vehicle: Vehicle)
extends Actor
extends ServerObjectControl
with FactionAffinityBehavior.Check
with MountableBehavior
with DamageableVehicle
with ArmorSiphonBehavior.Target
with RepairableVehicle
with JammableMountedWeapons
with ContainableBehavior
@ -64,6 +68,8 @@ class VehicleControl(vehicle: Vehicle)
def DamageableObject = vehicle
def SiphonableObject = vehicle
def RepairableObject = vehicle
def ContainerObject = vehicle
@ -85,6 +91,8 @@ 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
@ -102,8 +110,10 @@ class VehicleControl(vehicle: Vehicle)
}
def commonEnabledBehavior: Receive = checkBehavior
.orElse(attributeBehavior)
.orElse(jammableBehavior)
.orElse(takesDamage)
.orElse(siphoningBehavior)
.orElse(canBeRepairedByNanoDispenser)
.orElse(containerBehavior)
.orElse(environmentBehavior)
@ -124,23 +134,26 @@ class VehicleControl(vehicle: Vehicle)
dismountCleanup(seat_num)
case Vehicle.ChargeShields(amount) =>
val now : Long = System.currentTimeMillis()
//make certain vehicles don't charge shields too quickly
if (
vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields &&
!vehicle.History.exists(VehicleControl.LastShieldChargeOrDamage(now))
) {
vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), amount))
vehicle.Shields = vehicle.Shields + amount
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
s"${vehicle.Actor}",
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), vehicle.GUID, 68, vehicle.Shields)
)
}
chargeShields(amount)
case Vehicle.UpdateZoneInteractionProgressUI(player) =>
updateZoneInteractionProgressUI(player)
case Vehicle.UpdateSubsystemStates(toChannel, stateToResolve) =>
val events = vehicle.Zone.VehicleEvents
val guid0 = Service.defaultPlayerGUID
(stateToResolve match {
case Some(state) =>
vehicle.Subsystems().filter { _.Enabled == state } //only subsystems that are enabled or are disabled
case None =>
vehicle.Subsystems() //all subsystems
})
.flatMap { _.getMessage(vehicle) }
.foreach { pkt =>
events ! VehicleServiceMessage(toChannel, VehicleAction.SendResponse(guid0, pkt))
}
case FactionAffinity.ConvertFactionAffinity(faction) =>
val originalAffinity = vehicle.Faction
if (originalAffinity != (vehicle.Faction = faction)) {
@ -162,62 +175,29 @@ class VehicleControl(vehicle: Vehicle)
}
case Terminal.TerminalMessage(player, msg, reply) =>
reply match {
case Terminal.VehicleLoadout(definition, weapons, inventory) =>
org.log4s
.getLogger(vehicle.Definition.Name)
.info(s"changing vehicle equipment loadout to ${player.Name}'s option #${msg.unk1 + 1}")
//remove old inventory
val oldInventory = vehicle.Inventory.Clear().map { case InventoryItem(obj, _) => (obj, obj.GUID) }
//"dropped" items are lost; if it doesn't go in the trunk, it vanishes into the nanite cloud
val (_, afterInventory) = inventory.partition(ContainableBehavior.DropPredicate(player))
val (oldWeapons, newWeapons, finalInventory) = if (vehicle.Definition == definition) {
//vehicles are the same type
//TODO want to completely swap weapons, but holster icon vanishes temporarily after swap
//TODO BFR arms must be swapped properly
// //remove old weapons
// val oldWeapons = vehicle.Weapons.values.collect { case slot if slot.Equipment.nonEmpty =>
// val obj = slot.Equipment.get
// slot.Equipment = None
// (obj, obj.GUID)
// }.toList
// (oldWeapons, weapons, afterInventory)
//TODO for now, just refill ammo; assume weapons stay the same
vehicle.Weapons
.collect { case (_, slot: EquipmentSlot) if slot.Equipment.nonEmpty => slot.Equipment.get }
.collect {
case weapon: Tool =>
weapon.AmmoSlots.foreach { ammo => ammo.Box.Capacity = ammo.MaxMagazine() }
}
(Nil, Nil, afterInventory)
}
else {
//vehicle loadout is not for this vehicle
//do not transfer over weapon ammo
if (
vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset
) {
(Nil, Nil, afterInventory) //trunk is the same dimensions, however
}
else {
//accommodate as much of inventory as possible
val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory)
(Nil, Nil, stow)
}
}
finalInventory.foreach {
_.obj.Faction = vehicle.Faction
}
player.Zone.VehicleEvents ! VehicleServiceMessage(
player.Zone.id,
VehicleAction.ChangeLoadout(vehicle.GUID, oldWeapons, newWeapons, oldInventory, finalInventory)
)
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true)
)
val zone = vehicle.Zone
if (permitTerminalMessage(player, msg)) {
reply match {
case Terminal.VehicleLoadout(definition, weapons, inventory) =>
log.info(s"changing vehicle equipment loadout to ${player.Name}'s option #${msg.unk1 + 1}")
val (oldWeapons, newWeapons, oldInventory, finalInventory) =
handleTerminalMessageVehicleLoadout(player, definition, weapons, inventory)
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.ChangeLoadout(vehicle.GUID, oldWeapons, newWeapons, oldInventory, finalInventory)
)
zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true)
)
case _ => ;
case _ => ;
}
} else {
zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, false)
)
}
case VehicleControl.Disable() =>
@ -248,6 +228,11 @@ class VehicleControl(vehicle: Vehicle)
final def Enabled: Receive =
commonEnabledBehavior
.orElse {
case VehicleControl.RadiationTick =>
vehicle.interaction().find { _.Type == RadiationInVehicleInteraction } match {
case Some(func) => func.interaction(vehicle.getInteractionSector(), vehicle)
case _ => ;
}
case _ => ;
}
@ -323,8 +308,8 @@ class VehicleControl(vehicle: Vehicle)
user.avatar.vehicle = None
}
GainOwnership(user) //gain new ownership
}
else {
passengerRadiationCloudTimer.cancel()
} else {
decaying = false
decayTimer.cancel()
}
@ -347,6 +332,14 @@ class VehicleControl(vehicle: Vehicle)
if (!obj.Seats(0).isOccupied) {
obj.Velocity = Some(Vector3.Zero)
}
if (seatBeingDismounted == 0) {
passengerRadiationCloudTimer = context.system.scheduler.scheduleWithFixedDelay(
250.milliseconds,
250.milliseconds,
self,
VehicleControl.RadiationTick
)
}
if (!obj.Seats(seatBeingDismounted).isOccupied) { //seat was vacated
//we were only owning the vehicle while we sat in its driver seat
val canBeOwned = obj.Definition.CanBeOwned
@ -473,25 +466,19 @@ class VehicleControl(vehicle: Vehicle)
}
def PutItemInSlotCallback(item: Equipment, slot: Int): Unit = {
val obj = ContainerObject
val oguid = obj.GUID
val zone = obj.Zone
val channel = self.toString
val events = zone.VehicleEvents
val iguid = item.GUID
val definition = item.Definition
val obj = ContainerObject
val oguid = obj.GUID
val zone = obj.Zone
val channel = self.toString
val events = zone.VehicleEvents
val iguid = item.GUID
item.Faction = obj.Faction
events ! VehicleServiceMessage(
//TODO when a new weapon, the equipment slot ui goes blank, but the weapon functions; remount vehicle to correct it
if (obj.VisibleSlots.contains(slot)) zone.id else channel,
VehicleAction.SendResponse(
Service.defaultPlayerGUID,
ObjectCreateDetailedMessage(
definition.ObjectId,
iguid,
ObjectCreateMessageParent(oguid, slot),
definition.Packet.DetailedConstructorData(item).get
)
OCM.detailed(item, ObjectCreateMessageParent(oguid, slot))
)
)
item match {
@ -504,7 +491,7 @@ class VehicleControl(vehicle: Vehicle)
weapon.AmmoSlots.map { slot => slot.Box }.foreach { box =>
events ! VehicleServiceMessage(
channel,
VehicleAction.InventoryState2(Service.defaultPlayerGUID, iguid, weapon.GUID, box.Capacity)
VehicleAction.InventoryState2(Service.defaultPlayerGUID, box.GUID, iguid, box.Capacity)
)
}
case _ => ;
@ -521,6 +508,70 @@ class VehicleControl(vehicle: Vehicle)
)
}
def permitTerminalMessage(player: Player, msg: ItemTransactionMessage): Boolean = true
def handleTerminalMessageVehicleLoadout(
player: Player,
definition: VehicleDefinition,
weapons: List[InventoryItem],
inventory: List[InventoryItem]
): (
List[(Equipment, PlanetSideGUID)],
List[InventoryItem],
List[(Equipment, PlanetSideGUID)],
List[InventoryItem]
) = {
//remove old inventory
val oldInventory = vehicle.Inventory.Clear().map { case InventoryItem(obj, _) => (obj, obj.GUID) }
//"dropped" items are lost; if it doesn't go in the trunk, it vanishes into the nanite cloud
val (_, afterInventory) = inventory.partition(ContainableBehavior.DropPredicate(player))
val (oldWeapons, newWeapons, finalInventory) = if (vehicle.Definition == definition) {
//vehicles are the same type; just refill ammo, assuming weapons stay the same
vehicle.Weapons
.collect { case (_, slot: EquipmentSlot) if slot.Equipment.nonEmpty => slot.Equipment.get }
.collect {
case weapon: Tool =>
weapon.AmmoSlots.foreach { ammo => ammo.Box.Capacity = ammo.MaxMagazine() }
}
(Nil, Nil, afterInventory)
}
else {
//vehicle loadout is not for this vehicle; do not transfer over weapon ammo
if (
vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset
) {
(Nil, Nil, afterInventory) //trunk is the same dimensions, however
}
else {
//accommodate as much of inventory as possible
val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory)
(Nil, Nil, stow)
}
}
finalInventory.foreach {
_.obj.Faction = vehicle.Faction
}
(oldWeapons, newWeapons, oldInventory, finalInventory)
}
//make certain vehicles don't charge shields too quickly
def canChargeShields(): Boolean = {
val func: VitalsActivity => Boolean = VehicleControl.LastShieldChargeOrDamage(System.currentTimeMillis(), vehicle.Definition)
vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields &&
!vehicle.History.exists(func)
}
def chargeShields(amount: Int): Unit = {
if (canChargeShields()) {
vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), amount))
vehicle.Shields = vehicle.Shields + amount
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
s"${vehicle.Actor}",
VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), vehicle.GUID, vehicle.Definition.shieldUiAttribute, vehicle.Shields)
)
}
}
/**
* Water causes vehicles to become disabled if they dive off too far, too deep.
* Flying vehicles do not display progress towards being waterlogged. They just disable outright.
@ -717,11 +768,96 @@ class VehicleControl(vehicle: Vehicle)
case None => ;
}
}
override def parseAttribute(attribute: Int, value: Long, other: Option[Any]) : Unit = {
val vguid = vehicle.GUID
val (dname, dguid) = other match {
case Some(p: Player) => (p.Name, p.GUID)
case _ => (vehicle.OwnerName.getOrElse("The driver"), PlanetSideGUID(0))
}
val zone = vehicle.Zone
if (9 < attribute && attribute < 14) {
vehicle.PermissionGroup(attribute, value) match {
case Some(allow) =>
val group = AccessPermissionGroup(attribute - 10)
log.info(s"$dname changed ${vehicle.Definition.Name}'s access permission $group to $allow")
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.SeatPermissions(dguid, vguid, attribute, value)
)
//kick players who should not be seated in the vehicle due to permission changes
if (allow == VehicleLockState.Locked) { //TODO only important permission atm
vehicle.Seats.foreach {
case (seatIndex, seat) =>
seat.occupant match {
case Some(tplayer: Player) =>
if (vehicle.SeatPermissionGroup(seatIndex).contains(group) && !tplayer.Name.equals(dname)) { //can not kick self
seat.unmount(tplayer)
tplayer.VehicleSeated = None
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.KickPassenger(tplayer.GUID, 4, false, vguid)
)
}
case _ => ; // No player seated
}
}
vehicle.CargoHolds.foreach {
case (cargoIndex, hold) =>
hold.occupant match {
case Some(cargo) =>
if (vehicle.SeatPermissionGroup(cargoIndex).contains(group)) {
//todo: this probably doesn't work for passengers within the cargo vehicle
// Instruct client to start bail dismount procedure
self ! DismountVehicleCargoMsg(dguid, cargo.GUID, true, false, false)
}
case None => ; // No vehicle in cargo
}
}
}
case None => ;
}
} else {
log.warn(
s"parseAttributes: unsupported change on $vguid - $attribute, $dname"
)
}
}
override def StartJammeredStatus(target: Any, dur: Int): Unit = {
super.StartJammeredStatus(target, dur)
val subsystems = vehicle.Subsystems()
if (!subsystems.exists { _.Jammed }) {
subsystems.foreach { _.jam() }
vehicleSubsystemMessages(subsystems.flatMap { _.changedMessages(vehicle) })
}
}
override def CancelJammeredStatus(target: Any): Unit = {
super.CancelJammeredStatus(target)
val subsystems = vehicle.Subsystems()
if (subsystems.exists { _.Jammed }) {
subsystems.foreach { _.unjam() }
vehicleSubsystemMessages(subsystems.flatMap { _.changedMessages(vehicle) })
}
}
def vehicleSubsystemMessages(messages: List[PlanetSideGamePacket]): Unit = {
val zone = vehicle.Zone
val zoneid = zone.id
val events = zone.VehicleEvents
val guid0 = Service.defaultPlayerGUID
messages.foreach { pkt =>
events ! VehicleServiceMessage(
zoneid,
VehicleAction.SendResponse(guid0, pkt)
)
}
}
}
object VehicleControl {
import net.psforever.objects.vital.{DamageFromProjectile, VehicleShieldCharge, VitalsActivity}
import scala.concurrent.duration._
import net.psforever.objects.vital.{VehicleShieldCharge, VitalsActivity}
private case class PrepareForDeletion()
@ -729,21 +865,22 @@ object VehicleControl {
private case class Deletion()
private case object RadiationTick
final case class AssignOwnership(player: Option[Player])
/**
* Determine if a given activity entry would invalidate the act of charging vehicle shields this tick.
* @param now the current time (in nanoseconds)
* @param act a `VitalsActivity` entry to test
* @return `true`, if the vehicle took damage in the last five seconds or
* charged shields in the last second;
* @return `true`, if the shield charge would be blocked;
* `false`, otherwise
*/
def LastShieldChargeOrDamage(now: Long)(act: VitalsActivity): Boolean = {
def LastShieldChargeOrDamage(now: Long, vdef: VehicleDefinition)(act: VitalsActivity): Boolean = {
act match {
case DamageFromProjectile(data) => now - data.interaction.hitTime < (5 seconds).toMillis //damage delays next charge by 5s
case vsc: VehicleShieldCharge => now - vsc.time < (1 seconds).toMillis //previous charge delays next by 1s
case _ => false
case dact: DamagingActivity => now - dact.time < vdef.ShieldDamageDelay //damage delays next charge
case vsc: VehicleShieldCharge => now - vsc.time < vdef.ShieldPeriodicDelay //previous charge delays next
case _ => false
}
}
}

View file

@ -74,6 +74,7 @@ object NoResistanceSelection extends ResistanceSelection {
def Splash: ResistanceSelection.Format = NoResistance.Calculate
def Lash: ResistanceSelection.Format = NoResistance.Calculate
def Aggravated: ResistanceSelection.Format = NoResistance.Calculate
def Radiation: ResistanceSelection.Format = ResistanceSelection.None
}
object StandardInfantryResistance extends ResistanceSelection {
@ -81,6 +82,7 @@ object StandardInfantryResistance extends ResistanceSelection {
def Splash: ResistanceSelection.Format = InfantrySplashResistance.Calculate
def Lash: ResistanceSelection.Format = InfantryLashResistance.Calculate
def Aggravated: ResistanceSelection.Format = InfantryAggravatedResistance.Calculate
def Radiation: ResistanceSelection.Format = InfantrySplashResistance.Calculate
}
object StandardVehicleResistance extends ResistanceSelection {
@ -88,6 +90,7 @@ object StandardVehicleResistance extends ResistanceSelection {
def Splash: ResistanceSelection.Format = VehicleSplashResistance.Calculate
def Lash: ResistanceSelection.Format = VehicleLashResistance.Calculate
def Aggravated: ResistanceSelection.Format = VehicleAggravatedResistance.Calculate
def Radiation: ResistanceSelection.Format = ResistanceSelection.None
}
object StandardAmenityResistance extends ResistanceSelection {
@ -95,4 +98,5 @@ object StandardAmenityResistance extends ResistanceSelection {
def Splash: ResistanceSelection.Format = AmenityHitResistance.Calculate
def Lash: ResistanceSelection.Format = ResistanceSelection.None
def Aggravated: ResistanceSelection.Format = ResistanceSelection.None
def Radiation: ResistanceSelection.Format = ResistanceSelection.None
}

View file

@ -33,6 +33,12 @@ object VehicleResolutions
ResolutionCalculations.VehicleApplication
)
object BfrResolutions
extends DamageResistanceCalculations(
ResolutionCalculations.VehicleDamageAfterResist,
ResolutionCalculations.BfrApplication
)
object SimpleResolutions
extends DamageResistanceCalculations(
ResolutionCalculations.VehicleDamageAfterResist,

View file

@ -2,7 +2,7 @@
package net.psforever.objects.vital
import net.psforever.objects.ballistics.{PlayerSource, VehicleSource}
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition}
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
import net.psforever.objects.serverobject.terminals.TerminalDefinition
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason}
@ -15,7 +15,7 @@ trait VitalsActivity {
}
trait HealingActivity extends VitalsActivity {
def time: Long = System.currentTimeMillis()
val time: Long = System.currentTimeMillis()
}
trait DamagingActivity extends VitalsActivity {
@ -53,6 +53,9 @@ final case class RepairFromEquipment(
final case class RepairFromTerm(term_def: TerminalDefinition, amount: Int)
extends HealingActivity
final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, amount: Int)
extends HealingActivity
final case class VehicleShieldCharge(amount: Int)
extends HealingActivity //TODO facility

View file

@ -31,6 +31,7 @@ object DamageResolution extends Enumeration {
Explosion, //area of effect damage caused by an internal mechanism; unrelated to Splash
Environmental, //died to environmental causes
Suicide, //i don't want to be the one the battles always choose
Collision //went splat
Collision, //went splat
Radiation //it hurts to stand too close
= Value
}

View file

@ -10,5 +10,5 @@ object DamageType extends Enumeration(1) {
type Type = Value
//"one" (numerical 1 in the ADB) corresponds to objects that explode
final val Direct, Splash, Lash, Radiation, Aggravated, One, None = Value
final val Direct, Splash, Lash, Radiation, Aggravated, One, Siphon, None = Value
}

View file

@ -21,7 +21,7 @@ object DamageCalculations {
def AgainstMaxSuit(profile : DamageProfile) : Int = profile.Damage3
def AgainstBFR(profile : DamageProfile) : Int = profile.Damage4
def AgainstBfr(profile : DamageProfile) : Int = profile.Damage4
/**
* Get the damage value.

View file

@ -0,0 +1,55 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.{GlobalDefinitions, Tool, Vehicle}
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.vital.base.{DamageModifiers, DamageReason, DamageResolution}
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.prop.DamageWithPosition
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.types.Vector3
final case class ArmorSiphonReason(
hostVehicle: Vehicle,
siphon: Tool,
damageModel: DamageResistanceModel
) extends DamageReason {
assert(GlobalDefinitions.isBattleFrameArmorSiphon(siphon.Definition), "acting entity is not an armor siphon")
def source: DamageWithPosition = siphon.Projectile
def resolution: DamageResolution.Value = DamageResolution.Resolved
def same(test: DamageReason): Boolean = test match {
case asr: ArmorSiphonReason => (asr.hostVehicle eq hostVehicle) && (asr.siphon eq siphon)
case _ => false
}
def adversary: Option[SourceEntry] = None
override def attribution: Int = hostVehicle.Definition.ObjectId
}
object ArmorSiphonModifiers {
trait Mod extends DamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: DamageReason): Int = {
cause match {
case o: ArmorSiphonReason => calculate(damage, data, o)
case _ => 0
}
}
def calculate(damage: Int, data: DamageInteraction, cause: ArmorSiphonReason): Int
}
}
case object ArmorSiphonMaxDistanceCutoff extends ArmorSiphonModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ArmorSiphonReason): Int = {
if (Vector3.DistanceSquared(data.target.Position, cause.hostVehicle.Position) < cause.source.DamageRadius * cause.source.DamageRadius) {
damage
}
else {
0
}
}
}

View file

@ -16,7 +16,6 @@ import net.psforever.objects.zones.Zone
* A wrapper for a "damage source" in damage calculations
* that parameterizes information necessary to explain a server-driven explosion occurring.
* Some game objects cause area-of-effect damage upon being destroyed.
* @see `VitalityDefinition.explodes`
* @see `VitalityDefinition.innateDamage`
* @see `Zone.causesExplosion`
* @param entity what is accredited as the source of the explosive yield
@ -61,7 +60,7 @@ object ExplodingEntityReason {
instigation: Option[DamageResult]
): ExplodingEntityReason = {
val definition = entity.Definition.asInstanceOf[ObjectDefinition with VitalityDefinition]
assert(definition.explodes && definition.innateDamage.nonEmpty, "causal entity does not explode")
assert(definition.innateDamage.nonEmpty, "causal entity does not explode")
ExplodingEntityReason(SourceEntry(entity), definition.innateDamage.get, damageModel, instigation)
}
}

View file

@ -0,0 +1,104 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry, Projectile => ActualProjectile}
import net.psforever.objects.vital.base._
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.{ProjectileDamageModifierFunctions, ProjectileReason}
import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.vital.resolution.DamageAndResistance
/**
* A wrapper for a "damage source" in damage calculations
* that parameterizes information necessary to explain a radiation cloud.
* @param resolution how the damage is processed
* @param projectile the projectile that caused the damage
* @param damageModel the model to be utilized in these calculations;
* typically, but not always, defined by the target
* @param radiationShielding the amount of reduction to radiation damage that occurs due to external reasons;
* best utilized for protection extended to vehicle passengers
*/
final case class RadiationReason(
projectile: ActualProjectile,
damageModel: DamageAndResistance,
radiationShielding: Float
) extends DamageReason {
def resolution: DamageResolution.Value = DamageResolution.Radiation
def source: DamageProperties = projectile.profile
def same(test: DamageReason): Boolean = {
test match {
case o: RadiationReason => o.projectile.id == projectile.id //can only be another projectile with the same uid
case _ => false
}
}
def adversary: Option[SourceEntry] = Some(projectile.owner)
override def unstructuredModifiers: List[DamageModifiers.Mod] = List(ShieldAgainstRadiation)
override def attribution: Int = projectile.attribute_to
}
object RadiationDamageModifiers {
trait Mod extends DamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: DamageReason): Int = {
cause match {
case o: RadiationReason => calculate(damage, data, o)
case _ => damage
}
}
def calculate(damage: Int, data: DamageInteraction, cause: RadiationReason): Int
}
}
/**
* If the damge is caused by a projectile that emits a field that permeates vehicle armor,
* determine by how much the traversed armor's shielding reduces the damage.
* Infantry take damage, reduced only if one is equipped with a mechanized assault exo-suit.
*/
case object ShieldAgainstRadiation extends RadiationDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: RadiationReason): Int = {
if (data.resolution == DamageResolution.Radiation) {
data.target match {
case _: PlayerSource =>
damage - (damage * cause.radiationShielding).toInt
case _ =>
0
}
} else {
damage
}
}
}
/**
* The initial application of aggravated damage against an infantry target
* due to interaction with a radiation field
* where the specific damage component is `Splash`.
*/
case object InfantryAggravatedRadiation extends RadiationDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: RadiationReason): Int = {
ProjectileDamageModifierFunctions.baseAggravatedFormula(
DamageResolution.Radiation,
DamageType.Splash
)(damage, data, ProjectileReason(cause.resolution, cause.projectile, cause.damageModel))
}
}
/**
* The ongoing application of aggravated damage ticks against an infantry target
* due to interaction with a radiation field
* where the specific damage component is `Splash`.
* This is called "burning" regardless of what the active aura effect actually is.
*/
case object InfantryAggravatedRadiationBurn extends RadiationDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: RadiationReason): Int = {
ProjectileDamageModifierFunctions.baseAggravatedBurnFormula(
DamageResolution.Radiation,
DamageType.Splash
)(damage, data, ProjectileReason(cause.resolution, cause.projectile, cause.damageModel))
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.projectile
import net.psforever.objects.ballistics.{ChargeDamage, PlayerSource, ProjectileQuality}
import net.psforever.objects.ballistics._
import net.psforever.objects.equipment.ChargeFireModeDefinition
import net.psforever.objects.vital.base._
import net.psforever.objects.vital.damage.DamageModifierFunctions
@ -329,6 +329,28 @@ case object FlailDistanceDamageBoost extends ProjectileDamageModifiers.Mod {
}
}
/**
* If the damge is caused by a projectile that emits a field that permeates vehicle armor,
* determine by how much the traversed armor's shielding reduces the damage.
* Infantry take damage, reduced only if one is equipped with a mechanized assault exo-suit.
*/
case object ShieldAgainstRadiation extends ProjectileDamageModifiers.Mod {
def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = {
if (data.resolution == DamageResolution.Radiation) {
data.target match {
case p: PlayerSource if p.ExoSuit == ExoSuitType.MAX =>
damage - (damage * p.Modifiers.RadiationShielding).toInt
case _: PlayerSource =>
damage
case _ =>
0
}
} else {
damage
}
}
}
/* Functions */
object ProjectileDamageModifierFunctions {
/**

View file

@ -29,7 +29,8 @@ trait DamageProperties
/** use a specific modifier as a part of damage calculations */
private var useDamage1Subtract: Boolean = false
/** some other entity confers damage;
* a set value should not `None` and not `0` but is preferred to be the damager's uid */
* a set value should be the damager's object uid
* usually corresponding to a projectile */
private var damageProxy: Option[Int] = None
/** na;
* currently used with jammer properties only */

View file

@ -14,12 +14,14 @@ trait ResistanceSelection {
def Splash: ResistanceSelection.Format
def Lash: ResistanceSelection.Format
def Aggravated: ResistanceSelection.Format
def Radiation: ResistanceSelection.Format
def apply(data: DamageInteraction) : ResistanceSelection.Format = data.cause.source.CausesDamageType match {
case DamageType.Direct => Direct
case DamageType.Splash => Splash
case DamageType.Lash => Lash
case DamageType.Aggravated => Aggravated
case DamageType.Radiation => Splash
case _ => ResistanceSelection.None
}
@ -28,6 +30,7 @@ trait ResistanceSelection {
case DamageType.Splash => Splash
case DamageType.Lash => Lash
case DamageType.Aggravated => Aggravated
case DamageType.Radiation => Splash
case _ => ResistanceSelection.None
}
}

View file

@ -1,11 +1,12 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resolution
import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle}
import net.psforever.objects._
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry}
import net.psforever.objects.ce.Deployable
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.vehicles.VehicleSubsystemEntry
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.{DamagingActivity, Vitality, VitalsHistory}
import net.psforever.objects.vital.damage.DamageCalculations
@ -227,20 +228,36 @@ object ResolutionCalculations {
val targetBefore = SourceEntry(target)
target match {
case vehicle: Vehicle if CanDamage(vehicle, damage, data) =>
val shields = vehicle.Shields
if (shields > damage) {
vehicle.Shields = shields - damage
} else if (shields > 0) {
vehicle.Health = vehicle.Health - (damage - shields)
vehicle.Shields = 0
} else {
vehicle.Health = vehicle.Health - damage
}
vehicleDamageAfterShieldTest(
vehicle,
damage,
{ vehicle.Shields == 0 || data.cause.source.DamageToVehicleOnly }
)
case _ => ;
}
DamageResult(targetBefore, SourceEntry(target), data)
}
def vehicleDamageAfterShieldTest(
vehicle: Vehicle,
damage: Int,
ignoreShieldsDamage: Boolean
): Unit = {
val shields = vehicle.Shields
if (ignoreShieldsDamage) {
vehicle.Health = vehicle.Health - damage
} else {
if (shields > damage) {
vehicle.Shields = shields - damage
} else if (shields > 0) {
vehicle.Health = vehicle.Health - (damage - shields)
vehicle.Shields = 0
} else {
vehicle.Health = vehicle.Health - damage
}
}
}
def SimpleApplication(damage: Int, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = {
val targetBefore = SourceEntry(target)
target match {
@ -326,6 +343,31 @@ object ResolutionCalculations {
}
}
def BfrApplication(damage: Int, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = {
val targetBefore = SourceEntry(target)
target match {
case obj: Vehicle
if CanDamage(obj, damage, data) && GlobalDefinitions.isBattleFrameVehicle(obj.Definition) =>
vehicleDamageAfterShieldTest(
obj,
damage,
{
data.cause.source.DamageToBattleframeOnly ||
data.cause.source.DamageToVehicleOnly ||
!obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled ||
obj.Shields == 0
}
)
DamageResult(targetBefore, SourceEntry(target), data)
case _: Vehicle =>
VehicleApplication(damage, data)(target)
case _ =>
DamageResult(targetBefore, SourceEntry(target), data)
}
}
private def noDoubleLash(target: PlanetSideGameObject with VitalsHistory, data: DamageInteraction): Boolean = {
data.cause match {
case reason: ProjectileReason if reason.resolution == DamageResolution.Lash =>

View file

@ -2,11 +2,14 @@
package net.psforever.objects.zones
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.zones.blockmap.SectorPopulation
trait InteractsWithZone
extends PlanetSideServerObject {
/** interactions for this particular entity is allowed */
private var _allowInteraction: Boolean = true
/** maximum interaction range used to generate the commonly tested sector */
private var interactionRange: Float = 0.1f
/**
* If the interactive permissions of this entity change.
@ -24,7 +27,7 @@ trait InteractsWithZone
_allowInteraction = permit
if (before != permit) {
if (permit) {
interactions.foreach { _.interaction(target = this) }
doInteractions()
} else {
interactions.foreach ( _.resetInteraction(target = this) )
}
@ -36,14 +39,26 @@ trait InteractsWithZone
def interaction(func: ZoneInteraction): List[ZoneInteraction] = {
interactions = interactions :+ func
if (func.range > interactionRange) {
interactionRange = func.range
}
interactions
}
def interaction(): List[ZoneInteraction] = interactions
def getInteractionSector(): SectorPopulation = {
this.Zone.blockMap.sector(this.Position, interactionRange)
}
def doInteractions(): Unit = {
val sector = getInteractionSector()
interactions.foreach { _.interaction(sector, target = this) }
}
def zoneInteractions(): Unit = {
if (_allowInteraction) {
interactions.foreach { _.interaction(target = this) }
doInteractions()
}
}
@ -52,18 +67,31 @@ trait InteractsWithZone
}
}
trait ZoneInteractionType
/**
* The basic behavior of an entity in a zone.
* @see `InteractsWithZone`
* @see `Zone`
*/
trait ZoneInteraction {
/**
* A categorical descriptor for this interaction.
*/
def Type: ZoneInteractionType
/**
* The anticipated (radial?) distance across which this interaction affects the zone's blockmap.
*/
def range: Float
/**
* The method by which zone interactions are tested.
* How a target tests this interaction with elements of the target's zone.
* @param sector the portion of the block map being tested
* @param target the fixed element in this test
*/
def interaction(target: InteractsWithZone): Unit
def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit
/**
* Suspend any current interaction procedures.

View file

@ -3,7 +3,7 @@ package net.psforever.objects.zones
import akka.actor.{ActorContext, ActorRef, Props}
import net.psforever.objects.{PlanetSideGameObject, _}
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.ballistics.{Projectile, SourceEntry}
import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.guid.{NumberPoolHub, UniqueNumberOps, UniqueNumberSetup}
@ -12,10 +12,10 @@ import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.inventory.Container
import net.psforever.objects.serverobject.painbox.{Painbox, PainboxDefinition}
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.{Amenity, AmenityOwner, Building, StructureType, WarpGate}
import net.psforever.objects.serverobject.structures._
import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.serverobject.zipline.ZipLinePath
import net.psforever.types.{DriveState, PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3}
import net.psforever.types._
import org.log4s.Logger
import net.psforever.services.avatar.AvatarService
import net.psforever.services.local.LocalService
@ -124,6 +124,9 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
*/
private val corpses: ListBuffer[Player] = ListBuffer[Player]()
private var projectiles: ActorRef = Default.Actor
private val projectileList: ListBuffer[Projectile] = ListBuffer[Projectile]()
/**
*/
private var population: ActorRef = Default.Actor
@ -189,6 +192,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
context.actorOf(Props(classOf[UniqueNumberSys], this, this.guid), s"zone-$id-uns")
ground = context.actorOf(Props(classOf[ZoneGroundActor], this, equipmentOnGround), s"zone-$id-ground")
deployables = context.actorOf(Props(classOf[ZoneDeployableActor], this, constructions), s"zone-$id-deployables")
projectiles = context.actorOf(Props(classOf[ZoneProjectileActor], this, projectileList), s"zone-$id-projectiles")
transport = context.actorOf(Props(classOf[ZoneVehicleActor], this, vehicles), s"zone-$id-vehicles")
population = context.actorOf(Props(classOf[ZonePopulationActor], this, players, corpses), s"zone-$id-players")
projector = context.actorOf(
@ -540,6 +544,10 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
def Deployables: ActorRef = deployables
def Projectile: ActorRef = projectiles
def Projectiles: List[Projectile] = projectileList.toList
def Transport: ActorRef = transport
def Population: ActorRef = population
@ -1061,11 +1069,11 @@ object Zone {
* and a token that qualifies the current location of the object in the zone is returned.
* The following groups of objects are searched:
* the inventories of all players and all corpses,
* all vehicles trunks,
* all vehicles weapon mounts and trunks,
* the lockers of all players and corpses;
* and, if still not found, the ground is scoured too.
* @see `ItemLocation`<br>
* `LockerContainer`
* @see `ItemLocation`
* @see `LockerContainer`
* @param equipment the target object
* @param guid that target object's globally unique identifier
* @param continent the zone whose objects to search

View file

@ -0,0 +1,224 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.zones
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.guid.{GUIDTask, StraightforwardTask, TaskBundle, TaskWorkflow}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideGUID
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.concurrent.duration._
/**
* Synchronize management of the list of some `Projectile`s maintained by a zone.
* @param zone the zone being represented
* @param projectileList the zone's projectile list
*/
class ZoneProjectileActor(
zone: Zone,
projectileList: mutable.ListBuffer[Projectile]
) extends Actor {
/** a series of timers matched against projectile unique identifiers,
* marking the maximum lifespan of the projectile */
val projectileLifespan: mutable.HashMap[PlanetSideGUID, Cancellable] = new mutable.HashMap[PlanetSideGUID, Cancellable]
override def postStop() : Unit = {
projectileLifespan.values.foreach { _.cancel() }
projectileList.iterator.filter(_.HasGUID).foreach { p => cleanUpRemoteProjectile(p.GUID, p) }
projectileList.clear()
}
def receive: Receive = {
case ZoneProjectile.Add(filterGuid, projectile) =>
if (projectile.Definition.ExistsOnRemoteClients) {
if (projectile.HasGUID) {
cleanUpRemoteProjectile(projectile.GUID, projectile)
TaskWorkflow.execute(reregisterProjectile(filterGuid, projectile))
} else {
TaskWorkflow.execute(registerProjectile(filterGuid, projectile))
}
}
case ZoneProjectile.Remove(guid) =>
projectileList.find(_.GUID == guid) match {
case Some(projectile) =>
cleanUpRemoteProjectile(guid, projectile)
TaskWorkflow.execute(unregisterProjectile(projectile))
case _ =>
projectileLifespan.remove(guid)
//if we can't find this projectile by guid, remove any projectiles that are unregistered
val (in, out) = projectileList.filter(_.HasGUID).partition { p => zone.GUID(p.GUID).nonEmpty }
projectileList.clear()
projectileList.addAll(in)
out.foreach { p =>
cleanUpRemoteProjectile(p.GUID, p)
}
}
case _ => ;
}
/**
* Construct tasking that adds a completed but unregistered projectile into the scene.
* After the projectile is registered to the curent zone's global unique identifier system,
* all connected clients save for the one that registered it will be informed about the projectile's "creation."
* @param obj the projectile to be registered
* @return a `TaskBundle` message
*/
private def registerProjectile(filterGuid: PlanetSideGUID, obj: Projectile): TaskBundle = {
TaskBundle(
new StraightforwardTask() {
private val filter = filterGuid
private val globalProjectile = obj
private val func: (PlanetSideGUID, PlanetSideGUID, Projectile) => Unit = loadedRemoteProjectile
override def description(): String = s"register a ${globalProjectile.profile.Name}"
def action(): Future[Any] = {
func(filter, globalProjectile.GUID, globalProjectile)
Future(true)
}
},
List(GUIDTask.registerObject(zone.GUID, obj))
)
}
/**
* Construct tasking that removes a formerly complete and currently registered projectile from the scene.
* After the projectile is unregistered from the curent zone's global unique identifier system,
* all connected clients save for the one that registered it will be informed about the projectile's "destruction."
* @param obj the projectile to be unregistered
* @return a `TaskBundle` message
*/
private def unregisterProjectile(obj: Projectile): TaskBundle = GUIDTask.unregisterObject(zone.GUID, obj)
/**
* If the projectile object is unregistered, register it.
* If the projectile object is already registered, unregister it and then register it again.
* @see `registerProjectile(Projectile)`
* @see `unregisterProjectile(Projectile)`
* @param obj the projectile to be registered (a second time?)
* @return a `TaskBundle` message
*/
def reregisterProjectile(filterGuid: PlanetSideGUID, obj: Projectile): TaskBundle = {
val reg = registerProjectile(filterGuid, obj)
if (obj.HasGUID) {
TaskBundle(
reg.mainTask,
TaskBundle(
reg.subTasks(0).mainTask,
unregisterProjectile(obj)
)
)
} else {
reg
}
}
/**
* For a given registered remote projectile,
* perform all the actions necessary to properly integrate it into the management system.<br>
* <br>
* Those actions involve:<br>
* - determine whether or not the default filter needs to be applied,<br>
* - add the projectile to the zone managing list,<br>
* - if the projectile is a radiation cloud, add it to the zone blockmap<br>
* - set up the internal disposal timer, and<br>
* - dispatch a message to introduce the projectile to the game world.
* @param filterGuid a unique identifier filtering messages from a certain recipient
* @param projectileGuid the projectile unique identifier that was assigned by the zone's unique number system
* @param projectile the projectile being included
*/
def loadedRemoteProjectile(
filterGuid: PlanetSideGUID,
projectileGuid: PlanetSideGUID,
projectile: Projectile
): Unit = {
val definition = projectile.Definition
projectileList.addOne(projectile)
val (clarifiedFilterGuid, duration) = if (definition.radiation_cloud) {
zone.blockMap.addTo(projectile)
(Service.defaultPlayerGUID, projectile.profile.Lifespan seconds)
} else {
//remote projectiles that are not radiation clouds have lifespans controlled by the controller (user)
//if the controller fails, the projectile has a bit more than its normal lifespan before automatic clean up
(filterGuid, projectile.profile.Lifespan * 1.5f seconds)
}
projectileLifespan.put(
projectileGuid,
context.system.scheduler.scheduleOnce(duration, self, ZoneProjectile.Remove(projectileGuid))
)
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.LoadProjectile(
clarifiedFilterGuid,
definition.ObjectId,
projectileGuid,
definition.Packet.ConstructorData(projectile).get
)
)
}
/**
* For a given registered remote projectile, perform all the actions necessary to properly dispose of it.
* The projectile doesn't have to be registered at the moment,
* but you do need to know it's (previous) globally unique identifier.<br>
* <br>
* Those actions involve:<br>
* - remove and cancel the internal disposal timer,<br>
* - if the projectile is a radiation cloud, remove it from the zone blockmap<br>
* - remove the projectile from the zone managing list, and<br>
* - dispatch messages to eliminate the projectile from the game world.
* @param projectile_guid the globally unique identifier of the projectile
* @param projectile the projectile
*/
def cleanUpRemoteProjectile(projectile_guid: PlanetSideGUID, projectile: Projectile): Unit = {
projectileLifespan.remove(projectile_guid) match {
case Some(c) => c.cancel()
case _ => ;
}
projectileList.remove(projectileList.indexOf(projectile))
if (projectile.Definition.radiation_cloud) {
zone.blockMap.removeFrom(projectile)
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.ObjectDelete(PlanetSideGUID(0), projectile_guid, 2)
)
} else {
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.ProjectileExplodes(PlanetSideGUID(0), projectile_guid, projectile)
)
}
}
}
object ZoneProjectile {
/**
* Start monitoring the projectile.
* @param filterGuid a unique identifier filtering messages from a certain recipient
* @param projectile the projectile being included
*/
final case class Add(filterGuid: PlanetSideGUID, projectile: Projectile)
object Add {
/**
* Overloaded constructor for `Add` which onyl requires the projectile
* and defaults the filtering.
* @param projectile the projectile being included
* @return an `Add` message
*/
def apply(projectile: Projectile): Add = Add(PlanetSideGUID(0), projectile)
}
/**
* Stop the projectile from being monitored.
* @param guid the projectile assigned global unique identifier;
* not the same as the client local unique identifier (40100 to 40125)
*/
final case class Remove(guid: PlanetSideGUID)
}

View file

@ -77,7 +77,7 @@ class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) {
* @return a conglomerate sector which lists all of the entities in the discovered sector(s)
*/
def sector(p: Vector3, range: Float): SectorPopulation = {
BlockMap.quickToSectorGroup( BlockMap.findSectorIndices(blockMap = this, p, range).map { blocks } )
BlockMap.quickToSectorGroup(range, BlockMap.findSectorIndices(blockMap = this, p, range).map { blocks } )
}
/**
@ -129,7 +129,7 @@ class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) {
val toSectors = to.toSet.map { blocks }
toSectors.foreach { block => block.addTo(target) }
target.blockMapEntry = Some(BlockMapEntry(toPosition, range, to.toSet))
BlockMap.quickToSectorGroup(toSectors)
BlockMap.quickToSectorGroup(range, toSectors)
}
/**
@ -201,7 +201,7 @@ class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) {
target.blockMapEntry = None
val from = entry.sectors.map { blocks }
from.foreach { block => block.removeFrom(target) }
BlockMap.quickToSectorGroup(from)
BlockMap.quickToSectorGroup(range, from)
case None =>
SectorGroup(Nil)
}
@ -262,7 +262,7 @@ class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) {
to.diff(from).foreach { index => blocks(index).addTo(target) }
from.diff(to).foreach { index => blocks(index).removeFrom(target) }
target.blockMapEntry = Some(BlockMapEntry(toPosition, range, to))
BlockMap.quickToSectorGroup(to.map { blocks })
BlockMap.quickToSectorGroup(range, to.map { blocks })
case None =>
SectorGroup(Nil)
}
@ -394,4 +394,19 @@ object BlockMap {
SectorGroup(to)
}
}
/**
* If only one sector, just return that sector.
* If a group of sectors, organize them into a single referential sector.
* @param range a custom range value
* @param to all allocated sectors
* @return a conglomerate sector which lists all of the entities in the allocated sector(s)
*/
def quickToSectorGroup(range: Float, to: Iterable[Sector]): SectorPopulation = {
if (to.size == 1) {
SectorGroup(range, to.head)
} else {
SectorGroup(range, to)
}
}
}

View file

@ -1,6 +1,7 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.zones.blockmap
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.environment.PieceOfEnvironment
@ -13,6 +14,8 @@ import scala.collection.mutable.ListBuffer
* The collections of entities in a sector conglomerate.
*/
trait SectorPopulation {
def range: Float
def livePlayerList: List[Player]
def corpseList: List[Player]
@ -29,6 +32,8 @@ trait SectorPopulation {
def environmentList: List[PieceOfEnvironment]
def projectileList: List[Projectile]
/**
* A count of all the entities in all the lists.
*/
@ -40,7 +45,8 @@ trait SectorPopulation {
deployableList.size +
buildingList.size +
amenityList.size +
environmentList.size
environmentList.size +
projectileList.size
}
}
@ -143,6 +149,12 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int)
(a: PieceOfEnvironment, b: PieceOfEnvironment) => a eq b
)
private val projectiles: SectorListOf[Projectile] = new SectorListOf[Projectile](
(a: Projectile, b: Projectile) => a.id == b.id
)
def range: Float = span.toFloat
def livePlayerList : List[Player] = livePlayers.list
def corpseList: List[Player] = corpses.list
@ -159,6 +171,8 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int)
def environmentList: List[PieceOfEnvironment] = environment.list
def projectileList: List[Projectile] = projectiles.list
/**
* Appropriate an entity added to this blockmap bucket
* inot a list of objects that are like itself.
@ -189,6 +203,8 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int)
amenities.list.size < amenities.addTo(a).size
case e: PieceOfEnvironment =>
environment.list.size < environment.addTo(e).size
case p: Projectile =>
projectiles.list.size < projectiles.addTo(p).size
case _ =>
false
}
@ -211,6 +227,8 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int)
equipmentOnGround.list.size > equipmentOnGround.removeFrom(e).size
case d: Deployable =>
deployables.list.size > deployables.removeFrom(d).size
case p: Projectile =>
projectiles.list.size > projectiles.removeFrom(p).size
case _ =>
false
}
@ -230,6 +248,7 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int)
* @param environmentList fields that represent the game world environment
*/
class SectorGroup(
val range: Float,
val livePlayerList: List[Player],
val corpseList: List[Player],
val vehicleList: List[Vehicle],
@ -237,7 +256,8 @@ class SectorGroup(
val deployableList: List[Deployable],
val buildingList: List[Building],
val amenityList: List[Amenity],
val environmentList: List[PieceOfEnvironment]
val environmentList: List[PieceOfEnvironment],
val projectileList: List[Projectile]
)
extends SectorPopulation
@ -250,6 +270,7 @@ object SectorGroup {
*/
def apply(sector: Sector): SectorGroup = {
new SectorGroup(
sector.range,
sector.livePlayerList,
sector.corpseList,
sector.vehicleList,
@ -257,7 +278,30 @@ object SectorGroup {
sector.deployableList,
sector.buildingList,
sector.amenityList,
sector.environmentList
sector.environmentList,
sector.projectileList
)
}
/**
* Overloaded constructor that takes a single sector
* and transfers the lists of entities into a single conglomeration of the sector populations.
* @param range a custom range value
* @param sector the sector to be counted
* @return a `SectorGroup` object
*/
def apply(range: Float, sector: Sector): SectorGroup = {
new SectorGroup(
range,
sector.livePlayerList,
sector.corpseList,
sector.vehicleList,
sector.equipmentOnGroundList,
sector.deployableList,
sector.buildingList,
sector.amenityList,
sector.environmentList,
sector.projectileList
)
}
@ -268,15 +312,52 @@ object SectorGroup {
* @return a `SectorGroup` object
*/
def apply(sectors: Iterable[Sector]): SectorGroup = {
new SectorGroup(
sectors.flatMap { _.livePlayerList }.toList.distinct,
sectors.flatMap { _.corpseList }.toList.distinct,
sectors.flatMap { _.vehicleList }.toList.distinct,
sectors.flatMap { _.equipmentOnGroundList }.toList.distinct,
sectors.flatMap { _.deployableList }.toList.distinct,
sectors.flatMap { _.buildingList }.toList.distinct,
sectors.flatMap { _.amenityList }.toList.distinct,
sectors.flatMap { _.environmentList }.toList.distinct
)
if (sectors.isEmpty) {
SectorGroup(range = 0, sectors = Nil)
} else if (sectors.size == 1) {
SectorGroup(sectors.head.range, sectors)
} else {
SectorGroup(sectors.maxBy { _.range }.range, sectors)
}
}
/**
* Overloaded constructor that takes a group of sectors
* and condenses all of the lists of entities into a single conglomeration of the sector populations.
* @param range a custom range value
* @param sectors the series of sectors to be counted
* @return a `SectorGroup` object
*/
def apply(range: Float, sectors: Iterable[Sector]): SectorGroup = {
if (sectors.isEmpty) {
new SectorGroup(range, Nil, Nil, Nil, Nil, Nil, Nil, Nil, Nil, Nil)
} else if (sectors.size == 1) {
val sector = sectors.head
new SectorGroup(
range,
sector.livePlayerList,
sector.corpseList,
sector.vehicleList,
sector.equipmentOnGroundList,
sector.deployableList,
sector.buildingList,
sector.amenityList,
sector.environmentList,
sector.projectileList
)
} else {
new SectorGroup(
range,
sectors.flatMap { _.livePlayerList }.toList.distinct,
sectors.flatMap { _.corpseList }.toList.distinct,
sectors.flatMap { _.vehicleList }.toList.distinct,
sectors.flatMap { _.equipmentOnGroundList }.toList.distinct,
sectors.flatMap { _.deployableList }.toList.distinct,
sectors.flatMap { _.buildingList }.toList.distinct,
sectors.flatMap { _.amenityList }.toList.distinct,
sectors.flatMap { _.environmentList }.toList.distinct,
sectors.flatMap { _.projectileList }.toList.distinct
)
}
}
}

View file

@ -335,7 +335,7 @@ object GamePacketOpcode extends Enumeration {
case 0x19 => game.ObjectDeleteMessage.decode
case 0x1a => game.PingMsg.decode
case 0x1b => game.VehicleStateMessage.decode
case 0x1c => noDecoder(FrameVehicleStateMessage)
case 0x1c => game.FrameVehicleStateMessage.decode
case 0x1d => game.GenericObjectStateMsg.decode
case 0x1e => game.ChildObjectStateMessage.decode
case 0x1f => game.ActionResultMessage.decode
@ -547,14 +547,14 @@ object GamePacketOpcode extends Enumeration {
case 0xcc => noDecoder(ClockCalibrationMessage)
case 0xcd => game.DensityLevelUpdateMessage.decode
case 0xce => noDecoder(ActOfGodMessage)
case 0xcf => noDecoder(AvatarAwardMessage)
case 0xcf => game.AvatarAwardMessage.decode
// OPCODES 0xd0-df
case 0xd0 => noDecoder(UnknownMessage208)
case 0xd1 => game.DisplayedAwardMessage.decode
case 0xd2 => game.RespawnAMSInfoMessage.decode
case 0xd3 => noDecoder(ComponentDamageMessage)
case 0xd4 => noDecoder(GenericObjectActionAtPositionMessage)
case 0xd3 => game.ComponentDamageMessage.decode
case 0xd4 => game.GenericObjectActionAtPositionMessage.decode
case 0xd5 => game.PropertyOverrideMessage.decode
case 0xd6 => noDecoder(WarpgateLinkOverrideMessage)
case 0xd7 => noDecoder(EmpireBenefitsMessage)

View file

@ -0,0 +1,101 @@
// Copyright (c) 2021 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.Codec
import scodec.codecs._
import shapeless.{::, HNil}
import scala.annotation.switch
abstract class AwardOption(val code: Int) {
def unk1: Long
def unk2: Long
}
final case class AwardOptionZero(unk1: Long, unk2: Long) extends AwardOption(code = 0)
final case class AwardOptionOne(unk1: Long) extends AwardOption(code = 1) {
def unk2: Long = 0L
}
final case class AwardOptionTwo(unk1: Long) extends AwardOption(code = 3) {
def unk2: Long = 0L
}
/**
* na
* @param unk1 na
* @param unk2 na
* @param unk3 na
*/
final case class AvatarAwardMessage(
unk1: Long,
unk2: AwardOption,
unk3: Int
)
extends PlanetSideGamePacket {
type Packet = AvatarAwardMessage
def opcode = GamePacketOpcode.AvatarAwardMessage
def encode = AvatarAwardMessage.encode(this)
}
object AvatarAwardMessage extends Marshallable[AvatarAwardMessage] {
private val codec_one: Codec[AwardOptionOne] = {
uint32L.hlist
}.xmap[AwardOptionOne](
{
case a :: HNil => AwardOptionOne(a)
},
{
case AwardOptionOne(a) => a :: HNil
}
)
private val codec_two: Codec[AwardOptionTwo] = {
uint32L.hlist
}.xmap[AwardOptionTwo](
{
case a :: HNil => AwardOptionTwo(a)
},
{
case AwardOptionTwo(a) => a :: HNil
}
)
private val codec_zero: Codec[AwardOptionZero] = {
uint32L :: uint32L
}.xmap[AwardOptionZero](
{
case a :: b :: HNil => AwardOptionZero(a, b)
},
{
case AwardOptionZero(a, b) => a :: b :: HNil
}
)
private def selectAwardOption(code: Int): Codec[AwardOption] = {
((code: @switch) match {
case 2 | 3 => codec_two
case 1 => codec_one
case 0 => codec_zero
}).asInstanceOf[Codec[AwardOption]]
}
implicit val codec: Codec[AvatarAwardMessage] = (
("unk1" | uint32L) ::
(uint2 >>:~ { code =>
("unk2" | selectAwardOption(code)) ::
("unk3" | uint8L)
})
).xmap[AvatarAwardMessage](
{
case unk1 :: _ :: unk2 :: unk3 :: HNil =>
AvatarAwardMessage(unk1, unk2, unk3)
},
{
case AvatarAwardMessage(unk1, unk2, unk3) =>
unk1 :: unk2.code :: unk2 :: unk3 :: HNil
}
)
}

View file

@ -0,0 +1,82 @@
// Copyright (c) 2021 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.{PlanetSideGUID, SubsystemComponent}
import scodec.codecs._
import scodec.Codec
/**
* The status of the component's changing condition,
* including the level of alert the player experiences when the change occurs.
* @param alarm_level the klaxon sound effect associated with this damage
* @param damage the amount of damage (encoded ...)
* @param unk na;
* usually, `true`;
* known `false` states during shield generator offline and destruction conditions
*/
final case class ComponentDamageField(alarm_level: Long, damage: Long, unk: Boolean)
object ComponentDamageField {
def apply(alarmLevel: Long, dam: Long): ComponentDamageField = ComponentDamageField(alarmLevel, dam, unk = true)
}
/**
* Vehicles have aspects that are neither registered -
* do not necessarily represented unique entities of the vehicle -
* and are not statistical behaviors derived from the same level as the game files -
* modify vehicle stats but are not vehicle stats themselves.
* When these "components of the vehicle" are affected, however,
* such as when the vehicle has been jammed or when it has sustained damage,
* changes to the handling of the vehicle will occur through the said statistical mechanics.
* @see `VehicleSubsystem`
* @see `VehicleSubsystemEntity`
* @param guid the entity that owns this component, usually a vehicle
* @param component the subsystem, or part of the subsystem, being affected
* @param status specific about the component damage;
* `None`, when damage issues are cleared
*/
final case class ComponentDamageMessage(
guid: PlanetSideGUID,
component: SubsystemComponent,
status: Option[ComponentDamageField]
) extends PlanetSideGamePacket {
type Packet = ComponentDamageMessage
def opcode = GamePacketOpcode.ComponentDamageMessage
def encode = ComponentDamageMessage.encode(this)
}
object ComponentDamageMessage extends Marshallable[ComponentDamageMessage] {
/**
* Overloaded constructor where the component's current state is be cleared.
* @param guid the entity that owns this component, usually a vehicle
* @param component the subsystem, or part of the subsystem, being affected
* @return a `ComponentDamageMessage` packet
*/
def apply(guid: PlanetSideGUID, component: SubsystemComponent): ComponentDamageMessage =
ComponentDamageMessage(guid, component, None)
/**
* Overloaded constructor where the component's current state is always defined.
* @param guid the entity that owns this component, usually a vehicle
* @param component the subsystem, or part of the subsystem, being affected
* @param status specific about the component damage
* @return a `ComponentDamageMessage` packet
*/
def apply(guid: PlanetSideGUID, component: SubsystemComponent, status: ComponentDamageField): ComponentDamageMessage =
ComponentDamageMessage(guid, component, Some(status))
private val subsystemComponentCodec = PacketHelpers.createLongIntEnumCodec(SubsystemComponent, uint32L)
private val componentDamageFieldCodec: Codec[ComponentDamageField] = (
("unk1" | uint32L) ::
("unk2" | uint32L) ::
("unk3" | bool)
).as[ComponentDamageField]
implicit val codec: Codec[ComponentDamageMessage] = (
("guid" | PlanetSideGUID.codec) ::
("component" | subsystemComponentCodec) ::
("status" | optional(bool, componentDamageFieldCodec))
).as[ComponentDamageMessage]
}

View file

@ -37,14 +37,16 @@ import shapeless.{::, HNil}
* @param player_guid the player
* @param line the zero-indexed line number of this entry in its list
* @param label the identifier for this entry
* @param armor the type of exo-suit, if an Infantry loadout
* @param armor_type the type of exo-suit, if an Infantry loadout;
* the type of battleframe, if a Battleframe loadout;
* `None`, if just a Vehicle loadout
*/
final case class FavoritesMessage(
list: LoadoutType.Value,
player_guid: PlanetSideGUID,
line: Int,
label: String,
armor: Option[Int]
armor_type: Option[Int]
) extends PlanetSideGamePacket {
type Packet = FavoritesMessage
def opcode = GamePacketOpcode.FavoritesMessage
@ -52,9 +54,8 @@ final case class FavoritesMessage(
}
object FavoritesMessage extends Marshallable[FavoritesMessage] {
/**
* Overloaded constructor, for infantry loadouts specifically.
* Overloaded constructor.
* @param list the destination list
* @param player_guid the player
* @param line the zero-indexed line number of this entry in its list
@ -83,11 +84,66 @@ object FavoritesMessage extends Marshallable[FavoritesMessage] {
def apply(list: LoadoutType.Value, player_guid: PlanetSideGUID, line: Int, label: String): FavoritesMessage = {
FavoritesMessage(list, player_guid, line, label, None)
}
/**
* Overloaded constructor for infantry loadouts.
* @param player_guid the player
* @param line the zero-indexed line number of this entry in its list
* @param label the identifier for this entry
* @param armor the type of exo-suit
* @return a `FavoritesMessage` object
*/
def Infantry(
player_guid: PlanetSideGUID,
line: Int,
label: String,
armor: Int
): FavoritesMessage = {
FavoritesMessage(LoadoutType.Infantry, player_guid, line, label, Some(armor))
}
/**
* Overloaded constructor for vehicle loadouts.
* @param player_guid the player
* @param line the zero-indexed line number of this entry in its list
* @param label the identifier for this entry
* @return a `FavoritesMessage` object
*/
def Vehicle(
player_guid: PlanetSideGUID,
line: Int,
label: String
): FavoritesMessage = {
FavoritesMessage(LoadoutType.Vehicle, player_guid, line, label, None)
}
/**
* Overloaded constructor for battleframe loadouts.
* @param player_guid the player
* @param line the zero-indexed line number of this entry in its list
* @param label the identifier for this entry
* @param subtype the type of battleframe unit
* @return a `FavoritesMessage` object
*/
def Battleframe(
player_guid: PlanetSideGUID,
line: Int,
label: String,
subtype: Int
): FavoritesMessage = {
FavoritesMessage(LoadoutType.Battleframe, player_guid, line, label, Some(subtype))
}
implicit val codec: Codec[FavoritesMessage] = (("list" | LoadoutType.codec) >>:~ { value =>
("player_guid" | PlanetSideGUID.codec) ::
("line" | uint4L) ::
("label" | PacketHelpers.encodedWideStringAligned(2)) ::
conditional(value == LoadoutType.Infantry, "armor" | uintL(3))
("label" | PacketHelpers.encodedWideStringAligned(adjustment = 2)) ::
("armor_type" | conditional(value != LoadoutType.Vehicle,
{
if (value == LoadoutType.Infantry) uint(bits = 3)
else uint4
}
))
}).xmap[FavoritesMessage](
{
case lst :: guid :: ln :: str :: arm :: HNil =>
@ -95,8 +151,7 @@ object FavoritesMessage extends Marshallable[FavoritesMessage] {
},
{
case FavoritesMessage(lst, guid, ln, str, arm) =>
val armset: Option[Int] = if (lst == LoadoutType.Infantry && arm.isEmpty) { Some(0) }
else { arm }
val armset = if (lst != LoadoutType.Vehicle && arm.isEmpty) { Some(0) } else { arm }
lst :: guid :: ln :: str :: armset :: HNil
}
)

View file

@ -0,0 +1,81 @@
// Copyright (c) 2021 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.{Angular, PlanetSideGUID, Vector3}
import scodec.Codec
import scodec.codecs._
//TODO write more thorough comments later.
/**
* Dispatched to report and update the operational condition of a given battle frame robotics vehicle.
* @param vehicle_guid the battleframe robotic unit
* @param unk1 na
* @param pos the xyz-coordinate location in the world
* @param orient the orientation of the vehicle
* @param vel optional movement data
* @param unk2 na
* @param unk3 na
* @param unk4 na
* @param is_crouched the battleframe unit is crouched
* @param is_airborne the battleframe unit is either flying or falling (after flying)
* @param ascending_flight is the battleframe unit ascending;
* normally reports `ascending_flight` before properly reporting as `is_airborne`;
* continues to report `ascending_flight` until begins falling
* @param flight_time_remaining a measure of how much longer the battleframe unit, if it can fly, can fly;
* reported as a 0-10 value, counting down from 10 when airborne and provided vertical thrust
* @param unk9 na
* @param unkA na
* @see `PlacementData`
*/
final case class FrameVehicleStateMessage(
vehicle_guid: PlanetSideGUID,
unk1: Int,
pos: Vector3,
orient: Vector3,
vel: Option[Vector3],
unk2: Boolean,
unk3: Int,
unk4: Int,
is_crouched: Boolean,
is_airborne: Boolean,
ascending_flight: Boolean,
flight_time_remaining: Int,
unk9: Long,
unkA: Long
) extends PlanetSideGamePacket {
type Packet = FrameVehicleStateMessage
def opcode = GamePacketOpcode.FrameVehicleStateMessage
def encode = FrameVehicleStateMessage.encode(this)
}
object FrameVehicleStateMessage extends Marshallable[FrameVehicleStateMessage] {
/**
* Calculate common orientation from little-endian bit data.
* @see `Angular.codec_roll`
* @see `Angular.codec_pitch`
* @see `Angular.codec_yaw`
*/
val codec_orient : Codec[Vector3] = (
("roll" | Angular.codec_roll(bits = 10)) ::
("pitch" | Angular.codec_pitch(bits = 10)) ::
("yaw" | Angular.codec_yaw(bits = 10, North = 90f))
).as[Vector3]
implicit val codec : Codec[FrameVehicleStateMessage] = (
("vehicle_guid" | PlanetSideGUID.codec) ::
("unk1" | uint(bits = 3)) ::
("pos" | Vector3.codec_pos) ::
("orient" | codec_orient) ::
optional(bool, target = "vel" | Vector3.codec_vel) ::
("unk2" | bool) ::
("unk3" | uint2) ::
("unk4" | uint2) ::
("is_crouched" | bool) ::
("is_airborne" | bool) ::
("ascending_flight" | bool) ::
("flight_time_remaining" | uint4) ::
("unk9" | uint32) ::
("unkA" | uint32)
).as[FrameVehicleStateMessage]
}

Some files were not shown because too many files have changed in this diff Show more