mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
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:
parent
46763b7877
commit
6ae0b44848
|
|
@ -694,7 +694,7 @@ class PacketCodingActorKTest extends ActorTest {
|
|||
0L,
|
||||
0L,
|
||||
0L,
|
||||
Some(DCDExtra2(0, 0)),
|
||||
Some(ImprintingProgress(0, 0)),
|
||||
Nil,
|
||||
Nil,
|
||||
false,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@
|
|||
"Max": 39500,
|
||||
"Selector": "random"
|
||||
},
|
||||
{
|
||||
"Name": "rc-projectiles",
|
||||
"Start": 39701,
|
||||
"Max": 40000,
|
||||
"Selector": "random"
|
||||
},
|
||||
{
|
||||
"Name": "projectiles",
|
||||
"Start": 40100,
|
||||
|
|
|
|||
|
|
@ -40,5 +40,11 @@
|
|||
"Start": 37501,
|
||||
"Max": 40099,
|
||||
"Selector": "random"
|
||||
},
|
||||
{
|
||||
"Name": "rc-projectiles",
|
||||
"Start": 64001,
|
||||
"Max": 64300,
|
||||
"Selector": "random"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -40,5 +40,11 @@
|
|||
"Start": 36601,
|
||||
"Max": 39600,
|
||||
"Selector": "random"
|
||||
},
|
||||
{
|
||||
"Name": "rc-projectiles",
|
||||
"Start": 64001,
|
||||
"Max": 64300,
|
||||
"Selector": "random"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => ;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@ object AggravatedDamage {
|
|||
DamageType.Direct
|
||||
case DamageResolution.AggravatedSplash | DamageResolution.AggravatedSplashBurn =>
|
||||
DamageType.Splash
|
||||
case DamageResolution.Radiation =>
|
||||
DamageType.Splash
|
||||
case _ =>
|
||||
DamageType.None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ object AvatarConverter {
|
|||
0L,
|
||||
0L,
|
||||
0L,
|
||||
Some(DCDExtra2(0, 0)),
|
||||
None, //Some(ImprintingProgress(0, 0)),
|
||||
Nil,
|
||||
Nil,
|
||||
unkC = false,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)"))
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ class CharacterSelectConverter extends AvatarConverter {
|
|||
0L,
|
||||
0L,
|
||||
0L,
|
||||
Some(DCDExtra2(0, 0)),
|
||||
Some(ImprintingProgress(0, 0)),
|
||||
Nil,
|
||||
Nil,
|
||||
unkC = false,
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class CorpseConverter extends AvatarConverter {
|
|||
0L,
|
||||
0L,
|
||||
0L,
|
||||
Some(DCDExtra2(0, 0)),
|
||||
Some(ImprintingProgress(0, 0)),
|
||||
Nil,
|
||||
Nil,
|
||||
unkC = false,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)"))
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 _ =>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 _ => ;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1165
src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala
Normal file
1165
src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ object VehicleResolutions
|
|||
ResolutionCalculations.VehicleApplication
|
||||
)
|
||||
|
||||
object BfrResolutions
|
||||
extends DamageResistanceCalculations(
|
||||
ResolutionCalculations.VehicleDamageAfterResist,
|
||||
ResolutionCalculations.BfrApplication
|
||||
)
|
||||
|
||||
object SimpleResolutions
|
||||
extends DamageResistanceCalculations(
|
||||
ResolutionCalculations.VehicleDamageAfterResist,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue