cavern lock benefits, faster capturebase all checkpoint

This commit is contained in:
ScrawnyRonnie 2025-12-30 21:46:53 -05:00
parent 5b9e0ec384
commit 3f1efefc20
16 changed files with 248 additions and 52 deletions

View file

@ -45,6 +45,13 @@ class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers, implicit val context: A
case GalaxyResponse.MapUpdate(msg) =>
sendResponse(msg)
import net.psforever.actors.zone.ZoneActor
import net.psforever.zones.Zones
Zones.zones.find(_.Number == msg.continent_id) match {
case Some(zone) =>
zone.actor ! ZoneActor.BuildingInfoState(msg)
case None =>
}
case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
val faction = player.Faction

View file

@ -368,7 +368,8 @@ class ChatOperations(
case (Some(buildings), Some(faction), Some(_)) =>
//TODO implement timer
//schedule processing of buildings with a delay
processBuildingsWithDelay(buildings, faction, 1000) { zone =>
processBuildingsWithDelay(buildings, faction, 100) { zone =>
zone.actor ! ZoneActor.ZoneMapUpdate()
zone.actor ! ZoneActor.AssignLockedBy(zone, notifyPlayers=true)
}
true
@ -382,6 +383,7 @@ class ChatOperations(
faction: PlanetSideEmpire.Value,
delayMillis: Long
)(onComplete: Zone => Unit): Unit = {
import net.psforever.objects.serverobject.structures.StructureType
val buildingsToProcess = buildings.filter(b => b.CaptureTerminal.isDefined && b.Faction != faction)
val iterator = buildingsToProcess.iterator
val zone = buildings.head.Zone
@ -391,18 +393,23 @@ class ChatOperations(
if (iterator.hasNext) {
val building = iterator.next()
val terminal = building.CaptureTerminal.get
val zoneActor = zone.actor
if (building.CaptureTerminalIsHacked) {
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody)
)
if (building.BuildingType == StructureType.Tower) {
building.Actor ! BuildingActor.SetFaction(faction)
building.Actor ! BuildingActor.AmenityStateChange(terminal, Some(false))
building.Actor ! BuildingActor.MapUpdate()
}
zoneActor ! ZoneActor.ZoneMapUpdate()
building.Actor ! BuildingActor.SetFaction(faction)
building.Actor ! BuildingActor.AmenityStateChange(terminal, Some(false))
zoneActor ! ZoneActor.ZoneMapUpdate()
} else {
else {
if (building.CaptureTerminalIsHacked) {
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody)
)
}
building.Actor ! BuildingActor.SetFaction(faction)
building.Actor ! BuildingActor.AmenityStateChange(terminal, Some(false))
}
}
else {
handle.cancel(false)
onComplete(zone)
}

View file

@ -342,6 +342,13 @@ class ZoningOperations(
sendResponse(PlanetsideAttributeMessage(targetPlayer.GUID, 19, 1))
}
}
//adjust for health module benefit so overhead health bar accounts for added health
live.filter { tplayer =>
tplayer.MaxHealth == 120
}
.foreach { targetPlayer =>
sendResponse(PlanetsideAttributeMessage(targetPlayer.GUID, 1, 120))
}
//load corpses in zone
continent.Corpses.foreach {
spawn.DepictPlayerAsCorpse
@ -2943,16 +2950,40 @@ class ZoningOperations(
0 seconds
} else {
//for other zones ...
//Searhus lock benefit also gives biolab faster respawn
val searhusBenefit = Zones.zones.find(_.Number == 9).exists(_.benefitRecipient == player.Faction)
//biolabs have/grant benefits
val cryoBenefit: Float = toSpawnPoint.Owner match {
case b: Building if (b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) && b.virusId != 1) ||
(b.BuildingType == StructureType.Facility && !b.CaptureTerminalIsHacked && searhusBenefit) => 0.5f
case _ => 1f
val spawnTimeBenefit: Float = toSpawnPoint.Owner match {
case b: Building => FasterRespawnBenefits(b)
case _ => 1f
}
//TODO cumulative death penalty
(toSpawnPoint.Definition.Delay.toFloat * cryoBenefit).seconds
(toSpawnPoint.Definition.Delay.toFloat * spawnTimeBenefit).seconds
}
}
/**
* Multiple benefits can be given to an empire based on global ownership of certain zones or facility types that
* are linked to the facility being spawned at.
* @return float to potentially lower the respawn time if benefits are available
*/
def FasterRespawnBenefits(building: Building): Float = {
//Searhus lock benefit also gives biolab faster respawn
val searhusBenefit = Zones.zones.find(_.Number == 9).exists(_.benefitRecipient == player.Faction)
building match {
case b: Building
if (b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) && b.virusId != 1 &&
b.hasCavernLockBenefit) ||
(b.BuildingType == StructureType.Facility && !b.CaptureTerminalIsHacked &&
searhusBenefit && b.hasCavernLockBenefit) =>
0.3f
case b: Building
if !b.CaptureTerminalIsHacked && b.hasCavernLockBenefit && b.virusId != 1 =>
0.5f
case b: Building
if (b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) && b.virusId != 1) ||
(b.BuildingType == StructureType.Facility && !b.CaptureTerminalIsHacked &&
searhusBenefit) =>
0.5f
case _ =>
1f
}
}
@ -3228,7 +3259,11 @@ class ZoningOperations(
buildingType == StructureType.Bunker
}
.foreach { case (_, building) =>
sendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0 /*building.BuildingType == StructureType.Facility*/))
if (building.hasCavernLockBenefit) {
sendResponse(PlanetsideAttributeMessage(building.GUID, 67, 1))
}
else
sendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0))
}
statisticsPacketFunc()
if (tplayer.ExoSuit == ExoSuitType.MAX) {
@ -3353,6 +3388,20 @@ class ZoningOperations(
}
})
}
nextSpawnPoint.map(_.Owner) match {
case Some(b: Building) if b.hasCavernLockBenefit =>
tplayer.MaxHealth = 120
tplayer.Health = 120
tplayer.Zone.AvatarEvents ! AvatarServiceMessage(
tplayer.Zone.id,
AvatarAction.PlanetsideAttributeToAll(tplayer.GUID, 0, 120)
)
tplayer.Zone.AvatarEvents ! AvatarServiceMessage(
tplayer.Zone.id,
AvatarAction.PlanetsideAttributeToAll(tplayer.GUID, 1, 120)
)
case _ => ()
}
doorsThatShouldBeOpenInRange(pos, range = 100f)
setAvatar = true
player.allowInteraction = true

View file

@ -167,7 +167,6 @@ object BuildingActor {
val building = details.building
val zone = building.Zone
building.Faction = faction
zone.actor ! ZoneActor.ZoneMapUpdate() // Update entire lattice to show lattice benefits
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SetEmpire(building.GUID, faction))
}
}

View file

@ -8,7 +8,7 @@ import net.psforever.objects.serverobject.structures.{StructureType, WarpGate}
import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3}
import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.building.MajorFacilityLogic
import net.psforever.objects.avatar.scoring.Kill
@ -18,8 +18,10 @@ import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator}
import net.psforever.packet.game.{BuildingInfoUpdateMessage, PlanetsideAttributeMessage}
import net.psforever.util.Database._
import net.psforever.persistence
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import scala.collection.mutable
import scala.util.{Failure, Success}
@ -80,6 +82,8 @@ object ZoneActor {
final case class RewardOurSupporters(target: SourceEntry, history: Iterable[InGameActivity], kill: Kill, bep: Long) extends Command
final case class AssignLockedBy(zone: Zone, notifyPlayers: Boolean) extends Command
final case class BuildingInfoState(msg: BuildingInfoUpdateMessage) extends Command
}
class ZoneActor(
@ -186,7 +190,7 @@ class ZoneActor(
case ZoneMapUpdate() =>
zone.Buildings
.filter(building =>
building._2.BuildingType == StructureType.Facility || building._2.BuildingType == StructureType.Tower)
building._2.BuildingType == StructureType.Facility)
.values
.foreach(_.Actor ! BuildingActor.MapUpdate())
Behaviors.same
@ -194,6 +198,10 @@ class ZoneActor(
case AssignLockedBy(zone, notifyPlayers) =>
AssignLockedBy(zone, notifyPlayers)
Behaviors.same
case BuildingInfoState(msg) =>
UpdateBuildingState(msg)
Behaviors.same
}
.receiveSignal {
case (_, PostStop) =>
@ -221,4 +229,36 @@ class ZoneActor(
zone.benefitRecipient
if (facilities.nonEmpty && notifyPlayers) { zone.NotifyContinentalLockBenefits(zone, facilities.head) }
}
def UpdateBuildingState(msg: BuildingInfoUpdateMessage): Unit = {
val buildingOpt = zone.Buildings.collectFirst {
case (_, b) if b.MapId == msg.building_map_id => b
}
buildingOpt.foreach { building =>
if (msg.generator_state == PlanetSideGeneratorState.Normal && building.hasCavernLockBenefit) {
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.SendResponse(PlanetsideAttributeMessage(building.GUID, 67, 1))
)
}
msg.is_hacked match {
case true if building.BuildingType == StructureType.Facility && !zone.map.cavern =>
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.SendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0))
)
case false if building.hasCavernLockBenefit =>
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.SendResponse(PlanetsideAttributeMessage(building.GUID, 67, 1))
)
case false if building.BuildingType == StructureType.Facility && !zone.map.cavern && !building.hasCavernLockBenefit =>
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.SendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0))
)
case _ =>
}
}
}
}

View file

@ -4,8 +4,8 @@ package net.psforever.actors.zone.building
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails}
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails, ZoneActor}
import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
@ -84,7 +84,16 @@ case object CavernFacilityLogic
): Behavior[Command] = {
BuildingActor.setFactionTo(details, faction, log)
val building = details.building
building.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(building) }
val gates: Iterable[Building] = building.Zone.Buildings.values.filter(_.BuildingType == StructureType.WarpGate)
gates.foreach { g =>
val neighbors = g.Neighbours.getOrElse(Nil)
neighbors.collect {
case otherWg: Building => otherWg
}
.filter(_.Zone != g.Zone)
.foreach { otherGate => otherGate.Zone.actor ! ZoneActor.ZoneMapUpdate()
}
}
Behaviors.same
}

View file

@ -10,6 +10,7 @@ import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.packet.game.PlanetsideAttributeMessage
import net.psforever.services.{InterstellarClusterService, Service}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
@ -244,6 +245,8 @@ case object MajorFacilityLogic
}
setFactionTo(details, PlanetSideEmpire.NEUTRAL)
details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply = false
details.building.Zone.lockedBy = PlanetSideEmpire.NEUTRAL
details.building.Zone.NotifyContinentalLockBenefits(details.building.Zone, details.building)
Behaviors.same
}
@ -300,6 +303,12 @@ case object MajorFacilityLogic
building.PlayersInSOI.foreach { player =>
events ! AvatarServiceMessage(player.Name, msg)
}
if (building.hasCavernLockBenefit) {
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.SendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0))
)
}
false
case Some(GeneratorControl.Event.Destroyed) =>
true

View file

@ -122,9 +122,6 @@ case object WarpGateLogic
}
updateBroadcastCapabilitiesOfWarpGate(details, wg, setBroadcastTo)
updateBroadcastCapabilitiesOfWarpGate(details, otherWg, setBroadcastTo)
if (wg.Zone.map.cavern && !otherWg.Zone.map.cavern) {
otherWg.Zone.actor ! ZoneActor.ZoneMapUpdate()
}
case (Some(_), Some(wg : WarpGate), Some(otherWg : WarpGate), None) =>
handleWarpGateDeadendPair(details, otherWg, wg)

View file

@ -1125,6 +1125,8 @@ object GlobalDefinitions {
val medical_terminal = new MedicalTerminalDefinition(529)
val medical_terminal_healing_module = new MedicalTerminalDefinition(530)
val portable_med_terminal = new MedicalTerminalDefinition(689)
val pad_landing_frame = new MedicalTerminalDefinition(618)

View file

@ -26,13 +26,23 @@ object EffectTarget {
//noinspection ScalaUnusedSymbol
def Invalid(target: PlanetSideGameObject): Boolean = false
def Medical(target: PlanetSideGameObject): Boolean =
def Medical(target: PlanetSideGameObject): Boolean = {
target match {
case p: Player =>
p.Health > 0 && (p.Health < p.MaxHealth || p.Armor < p.MaxArmor)
case _ =>
false
}
}
def HealthModule(target: PlanetSideGameObject): Boolean = {
target match {
case p: Player =>
p.Health > 0 && p.Health < 120
case _ =>
false
}
}
def HealthCrystal(target: PlanetSideGameObject): Boolean =
target match {

View file

@ -370,6 +370,15 @@ object GlobalDefinitionsMiscellaneous {
medical_terminal.RepairIfDestroyed = true
medical_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.711f, height = 1.75f)
medical_terminal_healing_module.Name = "medical_terminal_healing_module"
medical_terminal_healing_module.Interval = 2000
medical_terminal_healing_module.HealAmount = 1
medical_terminal_healing_module.ArmorAmount = 0
medical_terminal_healing_module.UseRadius = 300
medical_terminal_healing_module.TargetValidation += EffectTarget.Category.Player -> EffectTarget.Validation.HealthModule
medical_terminal_healing_module.Damageable = false
medical_terminal_healing_module.Repairable = false
adv_med_terminal.Name = "adv_med_terminal"
adv_med_terminal.Interval = 500
adv_med_terminal.HealAmount = 8

View file

@ -36,6 +36,7 @@ class Building(
private var participationFunc: ParticipationLogic = NoParticipation
var virusId: Long = 8 // 8 default = no virus
var virusInstalledBy: Option[Int] = None // faction id
var hasCavernLockBenefit: Boolean = false
super.Zone_=(zone)
super.GUID_=(PlanetSideGUID(building_guid)) //set
Invalidate() //unset; guid can be used during setup, but does not stop being registered properly later
@ -206,11 +207,14 @@ class Building(
}
val cavernBenefit: Set[CavernBenefit] = if (
generatorState != PlanetSideGeneratorState.Destroyed &&
faction != PlanetSideEmpire.NEUTRAL &&
connectedCavern().nonEmpty
faction != PlanetSideEmpire.NEUTRAL && !CaptureTerminalIsHacked &&
connectedCavern().exists(_.Zone.lockedBy == faction)
) {
Set(CavernBenefit.VehicleModule, CavernBenefit.EquipmentModule)
hasCavernLockBenefit = true
Set(CavernBenefit.VehicleModule, CavernBenefit.EquipmentModule, CavernBenefit.ShieldModule,
CavernBenefit.SpeedModule, CavernBenefit.HealthModule, CavernBenefit.PainModule)
} else {
hasCavernLockBenefit = false
Set(CavernBenefit.None)
}
val (installedVirus, installedByFac) = if (virusId == 8) {

View file

@ -247,6 +247,9 @@ object ProximityTerminalControl {
target: PlanetSideGameObject
): Boolean = {
(terminal.Definition, target) match {
case (_: MedicalTerminalDefinition, p: Player)
if terminal.Definition ==
GlobalDefinitions.medical_terminal_healing_module => HealthModule(terminal, p)
case (_: MedicalTerminalDefinition, p: Player) => HealthAndArmorTerminal(terminal, p)
case (_: WeaponRechargeTerminalDefinition, p: Player) => WeaponRechargeTerminal(terminal, p)
case (_: MedicalTerminalDefinition, v: Vehicle) => VehicleRepairTerminal(terminal, v)
@ -269,6 +272,16 @@ object ProximityTerminalControl {
fullHeal && fullRepair
}
/**
* Activated by a facility having a linked cavern lock or health module installed. Friendly players
* within the SOI receive constant healing as requested by the client
*/
def HealthModule(unit: Terminal with ProximityUnit, target: Player): Boolean = {
val medDef = unit.Definition.asInstanceOf[MedicalTerminalDefinition]
val fullHeal = HealthModuleAction(unit, target, medDef.HealAmount, PlayerHealthCallback)
fullHeal
}
/**
* When driving a vehicle close to a rearm/repair silo,
* restore the vehicle's health points.
@ -318,6 +331,35 @@ object ProximityTerminalControl {
}
}
/**
* Heals players and increases their health/max health up to 120 if they enter the SOI this benefit is active in.
*/
def HealthModuleAction(
terminal: Terminal,
target: PlanetSideGameObject with Vitality with ZoneAware,
healAmount: Int,
updateFunc: PlanetSideGameObject with Vitality with ZoneAware => Unit
): Boolean = {
val maxHealthCap = 120
val zone = target.Zone
val oldMax = target.MaxHealth
val newMax = math.min(oldMax + healAmount, maxHealthCap)
if (oldMax < maxHealthCap) {
target.MaxHealth = newMax
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.PlanetsideAttributeToAll(target.GUID, 1, newMax)
)
}
if (target.Health < newMax) {
target.Health = math.min(target.Health + healAmount, newMax)
target.LogActivity(HealFromTerminal(AmenitySource(terminal), 1))
updateFunc(target)
}
target.Health == newMax
}
def PlayerHealthCallback(target: PlanetSideGameObject with Vitality with ZoneAware): Unit = {
val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage(

View file

@ -1,7 +1,9 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.Vehicle
import net.psforever.objects.serverobject.painbox.Painbox
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions}
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
@ -13,7 +15,18 @@ final case class PainboxReason(entity: Painbox) extends DamageReason {
private val definition = entity.Definition
assert(definition.innateDamage.nonEmpty, s"causal entity '${definition.Name}' does not emit pain field")
def source: DamageWithPosition = definition.innateDamage.get
def source: DamageWithPosition = {
val base = definition.innateDamage.get
entity.Owner match {
case b: Building if b.hasCavernLockBenefit =>
new DamageWithPosition {
Damage0 = 5
DamageRadius = 0
DamageToHealthOnly = true
}
case _ => base
}
}
def resolution: DamageResolution.Value = DamageResolution.Resolved

View file

@ -259,7 +259,10 @@ class HackCaptureActor extends Actor {
)
}
// Push map update to clients
owner.Zone.actor ! ZoneActor.ZoneMapUpdate()
if (owner.BuildingType == StructureType.Tower)
owner.Actor ! BuildingActor.MapUpdate()
else
owner.Zone.actor ! ZoneActor.ZoneMapUpdate()
}
private def HackCompleted(terminal: CaptureTerminal with Hackable, hackedByFaction: PlanetSideEmpire.Value): Unit = {
@ -268,7 +271,6 @@ class HackCaptureActor extends Actor {
building.virusId = 8
building.virusInstalledBy = None
log.info(s"Setting base ${building.GUID} / MapId: ${building.MapId} as owned by $hackedByFaction")
building.Actor ! BuildingActor.SetFaction(hackedByFaction)
//dispatch to players aligned with the capturing faction within the SOI
val events = building.Zone.LocalEvents
val msg = LocalAction.SendGenericActionMessage(Service.defaultPlayerGUID, GenericAction.FacilityCaptureFanfare)
@ -304,6 +306,7 @@ class HackCaptureActor extends Actor {
building.Zone.lockedBy = PlanetSideEmpire.NEUTRAL
building.Zone.NotifyContinentalLockBenefits(building.Zone, building)
}
building.Actor ! BuildingActor.SetFaction(hackedByFaction)
} else {
log.info("Base hack completed, but base was out of NTU.")
}
@ -333,23 +336,10 @@ class HackCaptureActor extends Actor {
if (buildingIterator.hasNext) {
val building = buildingIterator.next()
val terminal = building.CaptureTerminal.get
val zone = building.Zone
val zoneActor = zone.actor
val buildingActor = building.Actor
//clear any previous hack
if (building.CaptureTerminalIsHacked) {
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody)
)
}
//push any updates this might cause
zoneActor ! ZoneActor.ZoneMapUpdate()
//convert faction affiliation
buildingActor ! BuildingActor.SetFaction(faction)
buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false))
//push for map updates again
zoneActor ! ZoneActor.ZoneMapUpdate()
buildingActor ! BuildingActor.MapUpdate()
}
},
0,

View file

@ -345,6 +345,15 @@ object Zones {
),
owningBuildingGuid = buildingGuid
)
//health module slowly heals friendly players in the soi
zoneMap.addLocalObject(
buildingGuid + 2,
ProximityTerminal.Constructor(
structure.position,
GlobalDefinitions.medical_terminal_healing_module
),
owningBuildingGuid = buildingGuid
)
}
}
val filteredZoneEntities =
@ -557,7 +566,7 @@ object Zones {
case "adv_med_terminal" | "repair_silo" | "pad_landing_frame" | "pad_landing_tower_frame" | "medical_terminal" |
"crystals_health_a" | "crystals_health_b" | "crystals_repair_a" | "crystals_repair_b" | "crystals_vehicle_a" |
"crystals_vehicle_b" | "crystals_energy_a" | "crystals_energy_b" =>
"crystals_vehicle_b" | "crystals_energy_a" | "crystals_energy_b" | "medical_terminal_healing_module" =>
zoneMap.addLocalObject(
obj.guid,
ProximityTerminal