re-writing BFR timed deatruction; unwinding explicit logic antipattern; reactivating bfr spawning

This commit is contained in:
Fate-JH 2026-04-19 12:44:26 -04:00
parent 575f5fd7fc
commit 9770e286dc
4 changed files with 171 additions and 143 deletions

View file

@ -95,27 +95,27 @@ add_property trek allowed true
add_property trek equiptime 500
add_property trek holstertime 500
add_property vulture requirement_award0 false
add_property aphelion allowed false
add_property aphelion_flight allowed false
add_property aphelion_gunner allowed false
add_property aphelion allowed true
add_property aphelion_flight allowed true
add_property aphelion_gunner allowed true
add_property aphelion_armor_siphon allowed false
add_property aphelion_armor_siphon_left allowed false
add_property aphelion_armor_siphon_right allowed false
add_property aphelion_ntu_siphon allowed false
add_property aphelion_ntu_siphon_left allowed false
add_property aphelion_ntu_siphon_right allowed false
add_property colossus allowed false
add_property colossus_flight allowed false
add_property colossus_gunner allowed false
add_property colossus allowed true
add_property colossus_flight allowed true
add_property colossus_gunner allowed true
add_property colossus_armor_siphon allowed false
add_property colossus_armor_siphon_left allowed false
add_property colossus_armor_siphon_right allowed false
add_property colossus_ntu_siphon allowed false
add_property colossus_ntu_siphon_left allowed false
add_property colossus_ntu_siphon_right allowed false
add_property peregrine allowed false
add_property peregrine_flight allowed false
add_property peregrine_gunner allowed false
add_property peregrine allowed true
add_property peregrine_flight allowed true
add_property peregrine_gunner allowed true
add_property peregrine_armor_siphon allowed false
add_property peregrine_armor_siphon_left allowed false
add_property peregrine_armor_siphon_right allowed false

View file

@ -1737,7 +1737,7 @@ object GlobalDefinitionsVehicle {
aphelion_gunner.AutoPilotSpeeds = (5, 1)
aphelion_gunner.Packet = battleFrameConverter
aphelion_gunner.DestroyedModel = None
//aphelion_gunner.destructionDelay = Some(4000L)
aphelion_gunner.destructionDelay = Some(4000L)
aphelion_gunner.JackingDuration = Array(0, 62, 60, 30)
aphelion_gunner.RadiationShielding = 0.5f
aphelion_gunner.DamageUsing = DamageCalculations.AgainstBfr
@ -1789,7 +1789,7 @@ object GlobalDefinitionsVehicle {
colossus_gunner.AutoPilotSpeeds = (5, 1)
colossus_gunner.Packet = battleFrameConverter
colossus_gunner.DestroyedModel = None
//colossus_gunner.destructionDelay = Some(4000L)
colossus_gunner.destructionDelay = Some(4000L)
colossus_gunner.JackingDuration = Array(0, 62, 60, 30)
colossus_gunner.RadiationShielding = 0.5f
colossus_gunner.DamageUsing = DamageCalculations.AgainstBfr
@ -1841,7 +1841,7 @@ object GlobalDefinitionsVehicle {
peregrine_gunner.AutoPilotSpeeds = (5, 1)
peregrine_gunner.Packet = battleFrameConverter
peregrine_gunner.DestroyedModel = None
//peregrine_gunner.destructionDelay = Some(4000L)
peregrine_gunner.destructionDelay = Some(4000L)
peregrine_gunner.JackingDuration = Array(0, 62, 60, 30)
peregrine_gunner.RadiationShielding = 0.5f
peregrine_gunner.DamageUsing = DamageCalculations.AgainstBfr

View file

@ -193,72 +193,107 @@ trait DamageableVehicle
*/
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
(queuedDestruction, DamageableObject.Definition.destructionDelay) match {
case (_, None) => //explode now
destructionImmediate(target, cause)
case (None, Some(delay)) => //set a future explosion for later
destructionDelayed(delay, cause)
case (Some(_), _) | (None, None) => //explode now
val obj = DamageableObject
val zone = target.Zone
obj.PrepareGatingManifest()
super.DestructionAwareness(target, cause)
//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)
)
}
//database entry
cause.adversarial.collect {
case Adversarial(attacker, victim: VehicleSource, implement) =>
ToDatabase.reportMachineDestruction(
attacker.CharId,
victim,
DamageableObject.HackedBy,
DamageableObject.MountedIn.nonEmpty,
implement,
obj.Zone.Number
)
}
//clean up
target.Actor ! Vehicle.Deconstruct(Some(1 minute))
DamageableWeaponTurret.DestructionAwareness(obj, cause)
case _ => ;
case (Some(_), _) => //destroy now that delay is expended (can also skip existing delay)
destructionAfterDelay(target, cause)
}
}
def destructionImmediate(target: Target, cause: DamageResult): Unit = {
val obj = DamageableObject
val zone = target.Zone
obj.PrepareGatingManifest()
super.DestructionAwareness(target, cause)
//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)
)
}
//database entry
cause.adversarial.collect {
case Adversarial(attacker, victim: VehicleSource, implement) =>
ToDatabase.reportMachineDestruction(
attacker.CharId,
victim,
DamageableObject.HackedBy,
DamageableObject.MountedIn.nonEmpty,
implement,
obj.Zone.Number
)
}
//clean up
target.Actor ! Vehicle.Deconstruct(Some(1 minute))
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
//health to 1, shields to 0
obj.Health = 1
val guid = obj.GUID
events ! VehicleServiceMessage(
zoneid,
VehicleAction.PlanetsideAttribute(guid0, guid, 0, 1)
)
events ! VehicleServiceMessage(
zoneid,
VehicleAction.PlanetsideAttribute(guid0, guid, obj.Definition.shieldUiAttribute, 0)
)
if (obj.Shields > 0) {
obj.Shields = 0
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, obj.GUID, obj.Definition.shieldUiAttribute, 0)
)
}
//aggravation cancel
EndAllAggravation()
//special considerations for certain vehicles
Vehicles.BeforeUnloadVehicle(obj, zone)
//true death (expedited)
DamageableEntity.DestructionAwareness(obj, cause)
//passengers die with us
DamageableMountable.DestructionAwareness(DamageableObject, cause)
//database entry
cause.adversarial.collect {
case Adversarial(attacker, victim: VehicleSource, implement) =>
ToDatabase.reportMachineDestruction(
attacker.CharId,
victim,
DamageableObject.HackedBy,
DamageableObject.MountedIn.nonEmpty,
implement,
obj.Zone.Number
)
}
//come back to this death later
queuedDestruction = Some(context.system.scheduler.scheduleOnce(delay milliseconds, self, DamageableVehicle.Destruction(cause)))
}
def destructionAfterDelay(target: Target, cause: DamageResult): Unit = {
val obj = DamageableObject
obj.PrepareGatingManifest()
super.DestructionAwareness(target, cause)
Zone.serverSideDamage(obj.Zone, target, Zone.explosionDamage(Some(cause)))
//clean up
target.Actor ! Vehicle.Deconstruct(Some(1 minute))
DamageableWeaponTurret.DestructionAwareness(obj, cause)
}
}
object DamageableVehicle {

View file

@ -20,6 +20,7 @@ import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types._
import scala.annotation.unused
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
@ -51,24 +52,9 @@ class BfrControl(vehicle: Vehicle)
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)) {
@ -83,8 +69,6 @@ class BfrControl(vehicle: Vehicle)
performEmpBurst()
}
override def commonDisabledBehavior: Receive = super.commonDisabledBehavior.orElse(explosionBehavior)
override def PrepareForDisabled(kickPassengers: Boolean) : Unit = {
super.PrepareForDisabled(kickPassengers)
if (vehicle.Health == 0) {
@ -116,14 +100,6 @@ class BfrControl(vehicle: Vehicle)
super.destructionDelayed(delay, cause)
shieldCharge.cancel()
shieldCharge = Cancellable.alreadyCancelled
//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 = Cancellable.alreadyCancelled
disableShield()
}
@ -138,9 +114,9 @@ class BfrControl(vehicle: Vehicle)
}
val guid0 = PlanetSideGUID(0)
//if the weapon arm is disabled, enable it for later (makes life easy)
parseObjectAction(guid0, BfrControl.ArmState.Enabled, Some(slot))
parseObjectActionForBFRs(guid0, BfrControl.ArmState.Enabled, Some(slot))
//enable the other arm weapon regardless
parseObjectAction(guid0, BfrControl.ArmState.Enabled, Some(
parseObjectActionForBFRs(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 }
))
@ -380,8 +356,24 @@ class BfrControl(vehicle: Vehicle)
/** 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)
/**
* Object action changes specifically tailored for the battleframe robotics.
* Manipulates active state of the arm weapons.
* @param guid unique identifier of the entity being manipulated;
* use no identifier to search for entity or equipment
* @param action how the entity is being manipulated;
* in the case of arm control, whether is it should be enabled or disabled
* @param other additional information necessary to perform this action;
* in the case of arm control, the slot of the arm being manipulated
* @param armManagementFunc how to manipulate the battleframe arm if its active state changed;
* defaults to normal management routine
*/
private def parseObjectActionForBFRs(
guid: PlanetSideGUID,
action: Int,
other: Option[Any],
armManagementFunc: Int => Unit = specialArmWeaponActiveManagement
): Unit = {
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 {
@ -406,7 +398,7 @@ class BfrControl(vehicle: Vehicle)
(0, None)
}) match {
case (slot, Some(_)) =>
specialArmWeaponActiveManagement(slot)
armManagementFunc(slot)
val guid0 = Service.defaultPlayerGUID
val doNotSendTo = other match {
case Some(pguid: PlanetSideGUID) => pguid
@ -433,6 +425,11 @@ class BfrControl(vehicle: Vehicle)
}
}
override def parseObjectAction(guid: PlanetSideGUID, action: Int, other: Option[Any]): Unit = {
super.parseObjectAction(guid, action, other)
parseObjectActionForBFRs(guid, action, other)
}
def bfrHandiness(side: equipment.Hand): Int = {
if (side == Handiness.Left) 2
else if (side == Handiness.Right) 3
@ -455,7 +452,7 @@ class BfrControl(vehicle: Vehicle)
}
}
def specialArmWeaponEquipManagement(item: Equipment, slot: Int, handiness: equipment.Hand): Unit = {
def specialArmWeaponEquipManagement(item: Equipment, slot: Int, @unused handiness: equipment.Hand): Unit = {
if (item.Size == EquipmentSize.BFRArmWeapon && vehicle.VisibleSlots.contains(slot)) {
val weapons = vehicle.Weapons
//budget logic: the arm weapons are "next to each other" index-wise
@ -482,69 +479,65 @@ class BfrControl(vehicle: Vehicle)
) {
//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))
parseObjectActionForBFRs(PlanetSideGUID(0), BfrControl.ArmState.Enabled, Some(otherArmSlot)) //ensure enabled
parseObjectActionForBFRs(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 */
/* since `specialArmWeaponActiveManagement` is called from `parseObjectActionForBFRs`,
* and `parseObjectActionForBFRs` gets called in `specialArmWeaponActiveManagement`,
* kill endless logic loops before they can happen by instructing the former to not call the latter (again) */
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))
}
val ((thisSubsystem, (thisSlot, thisEquipment)), (_, (otherSlot, otherEquipment))) = {
val pairedSystemsToSlots = pairedArmSlotSubsystems()
if (pairedSystemsToSlots.head._2._1 == slotChanged) {
(pairedSystemsToSlots.head, pairedSystemsToSlots(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
}
(pairedSystemsToSlots(1), pairedSystemsToSlots.head)
}
}
if (thisSubsystem.Enabled) {
//this arm weapon slot was enabled
if ({
val (thisArmExists, thisArmIsSiphon) = thisEquipment.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) = otherEquipment.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
parseObjectActionForBFRs(PlanetSideGUID(0), BfrControl.ArmState.Disabled, Some(otherSlot), BfrControl.NoArmManagement)
}
}
else {
//this arm weapon slot was disabled
thisEquipment.Equipment match {
case Some(item) =>
parseObjectActionForBFRs(item.GUID, BfrControl.ArmState.Enabled, Some(otherSlot), BfrControl.NoArmManagement) //other arm must be enabled
case None =>
parseObjectActionForBFRs(PlanetSideGUID(0), BfrControl.ArmState.Enabled, Some(thisSlot), BfrControl.NoArmManagement) //must stay enabled
}
notSpecialManagingArmWeapon = true
}
}
@ -615,8 +608,6 @@ object BfrControl {
final val Disabled = 39
}
private case object VehicleExplosion
val dimorphics: List[EquipmentHandiness] = {
import GlobalDefinitions._
List(
@ -637,4 +628,6 @@ object BfrControl {
EquipmentHandiness(peregrine_sparrow, peregrine_sparrow_left, peregrine_sparrow_right)
)
}
private def NoArmManagement(@unused x: Int):Unit = { }
}