mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-05-10 15:56:15 +00:00
Merge bf57630182 into fc6d3defde
This commit is contained in:
commit
88b5bbe2b3
34 changed files with 2199 additions and 130 deletions
|
|
@ -154,6 +154,20 @@ game {
|
|||
force-rotation-immediately = false
|
||||
}
|
||||
|
||||
virtual-training = {
|
||||
# Enables VR Shooting Range targets, if you know the VR Shooting Range is not going to be used,
|
||||
# you can disable target spawns to slightly reduce the server's baseline CPU and memory usage.
|
||||
shooting-range-targets-enabled = true
|
||||
|
||||
# You can customize the list of VR Shooting Range bot names here.
|
||||
# The default names are all names that were observed from the original servers.
|
||||
male-bot-names = ["Aphtek", "Arrius", "Augustus", "Batezek", "Baulk", "Bayloon", "Bentar", "Braydon", "Brok", "Brutus", "Byatis", "Cantrip", "Doragon", "Elion", "Garbazhu", "GAxir", "Ghurmak", "Grailbait", "Graznack", "Gorabbus", "Gorganogan", "Hlanith", "Jitrick", "Julianus", "Knyan", "Lupus", "Marcus", "Morkardar", "Morungat", "Murch", "Nerick", "Nezcaro", "Plinius", "Prondus", "Publius", "Quillion", "Remus", "Secarr", "Shantak", "Shoggoth", "Shulogrem", "Taractus", "Tazok", "Thran", "Thremnir", "Urzican", "Zorach", "Yarblek", "Yith", "Yoth"]
|
||||
|
||||
female-bot-names = ["Agatz", "Aggan", "Arham", "Borador", "Brrt", "Cahh", "Ceth", "Chazor", "Cherik", "Corax", "Cyaegha", "Cykranosh", "Dentarg", "Drask", "Fathrd", "Ghorak", "Grogdish", "Grotta", "Gorman", "Hurrbrrmn", "Hrrd", "Ithak", "Julius", "Kephnes", "Kherek", "Korros", "Kraa", "Kradak", "Kryle", "Marcius", "Melderthra", "Minx", "Mnar", "Nanak", "Ngranek", "Nir", "Nyariathotep", "Sharagar", "Sharrak", "Shkar", "Ssaad", "Ssikz", "Tarach", "Tarkacho", "Tassadar", "Thraa", "Thurraz", "Tyr", "Utz", "Whea", "Yakkaz", "Yarak", "Zadrak"]
|
||||
|
||||
universal-bot-names = ["Bane", "Barzai", "BoacTreth", "Brazoragh", "Cicero", "Chamak", "Chrkss", "Cshsssh", "DylathLeen", "Gnomon", "GrizzleBok", "Hatheg", "Izch", "Jazzkkrt", "Mogor", "Myst", "Nero", "OothNarga", "Orrg", "Shadow", "TchoTcho", "Twilight", "Vthak", "Vulthoom", "Ychch", "Zar", "Zhar"]
|
||||
}
|
||||
|
||||
saved-msg = {
|
||||
# A brief delay to the @charsaved message, in seconds.
|
||||
# Use this when the message should display very soon for any reason.
|
||||
|
|
|
|||
14
src/main/resources/guid-pools/tzshnc.json
Normal file
14
src/main/resources/guid-pools/tzshnc.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[
|
||||
{
|
||||
"Name": "environment",
|
||||
"Start": 0,
|
||||
"Max": 896,
|
||||
"Selector": "specific"
|
||||
},
|
||||
{
|
||||
"Name": "bots",
|
||||
"Start": 1300,
|
||||
"Max": 1600,
|
||||
"Selector": "random"
|
||||
}
|
||||
]
|
||||
14
src/main/resources/guid-pools/tzshtr.json
Normal file
14
src/main/resources/guid-pools/tzshtr.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[
|
||||
{
|
||||
"Name": "environment",
|
||||
"Start": 0,
|
||||
"Max": 896,
|
||||
"Selector": "specific"
|
||||
},
|
||||
{
|
||||
"Name": "bots",
|
||||
"Start": 1300,
|
||||
"Max": 1600,
|
||||
"Selector": "random"
|
||||
}
|
||||
]
|
||||
14
src/main/resources/guid-pools/tzshvs.json
Normal file
14
src/main/resources/guid-pools/tzshvs.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[
|
||||
{
|
||||
"Name": "environment",
|
||||
"Start": 0,
|
||||
"Max": 896,
|
||||
"Selector": "specific"
|
||||
},
|
||||
{
|
||||
"Name": "bots",
|
||||
"Start": 1300,
|
||||
"Max": 1600,
|
||||
"Selector": "random"
|
||||
}
|
||||
]
|
||||
|
|
@ -5,7 +5,7 @@ import akka.actor.{ActorContext, ActorRef, typed}
|
|||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
|
||||
import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, GlobalDefinitions, LivePlayerList, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle}
|
||||
import net.psforever.objects.avatar.{Avatar, PlayerControl}
|
||||
import net.psforever.objects.avatar.{Avatar, AvatarBot, PlayerControl}
|
||||
import net.psforever.objects.ballistics.Projectile
|
||||
import net.psforever.objects.ce.Deployable
|
||||
import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition}
|
||||
|
|
@ -286,6 +286,8 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
|
|||
ops.handleUseGeneralEntity(panel, equipment)
|
||||
case Some(obj: Player) =>
|
||||
ops.handleUsePlayer(obj, equipment, pkt)
|
||||
case Some(obj: AvatarBot) =>
|
||||
ops.handleUseBot(obj, equipment, pkt)
|
||||
case Some(locker: Locker) =>
|
||||
ops.handleUseLocker(locker, equipment, pkt)
|
||||
case Some(gen: Generator) =>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import akka.actor.{ActorContext, ActorRef, typed}
|
|||
import net.psforever.actors.session.{AvatarActor, SessionActor}
|
||||
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData, SessionOutfitHandlers}
|
||||
import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, GlobalDefinitions, LivePlayerList, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle}
|
||||
import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry}
|
||||
import net.psforever.objects.avatar.{Avatar, AvatarBot, PlayerControl, SpecialCarry}
|
||||
import net.psforever.objects.ballistics.Projectile
|
||||
import net.psforever.objects.ce.{Deployable, DeployedItem}
|
||||
import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition}
|
||||
|
|
@ -356,6 +356,8 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
|
|||
ops.handleUseGeneralEntity(panel, equipment)
|
||||
case Some(obj: Player) =>
|
||||
ops.handleUsePlayer(obj, equipment, pkt)
|
||||
case Some(obj: AvatarBot) =>
|
||||
ops.handleUseBot(obj, equipment, pkt)
|
||||
case Some(locker: Locker) =>
|
||||
ops.handleUseLocker(locker, equipment, pkt)
|
||||
case Some(gen: Generator) =>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.actors.session.support
|
||||
|
||||
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
|
||||
import net.psforever.objects.avatar.AvatarBot
|
||||
import net.psforever.objects.serverobject.containable.Containable
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
import net.psforever.objects.serverobject.interior.Sidedness
|
||||
|
|
@ -393,9 +394,17 @@ class GeneralOperations(
|
|||
0
|
||||
}
|
||||
Some(TargetInfo(player.GUID, health, armor))
|
||||
case Some(bot: AvatarBot) =>
|
||||
val health = bot.Health.toFloat / bot.MaxHealth
|
||||
val armor = if (bot.MaxArmor > 0) {
|
||||
bot.Armor.toFloat / bot.MaxArmor
|
||||
} else {
|
||||
0
|
||||
}
|
||||
Some(TargetInfo(bot.GUID, health, armor))
|
||||
case _ =>
|
||||
log.warn(
|
||||
s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player"
|
||||
s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player or bot"
|
||||
)
|
||||
None
|
||||
}
|
||||
|
|
@ -1060,9 +1069,7 @@ class GeneralOperations(
|
|||
}
|
||||
|
||||
def trainingGriefWarning(): Unit = {
|
||||
//don't know the correct ChatMessageType for this one yet;
|
||||
//it's supposed to use a pop-up that takes 5 seconds before you are allowed to close it, so disabled for now
|
||||
//sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, wideContents=true, "", "@TrainingGriefWarning", None))
|
||||
sendResponse(GenericActionMessage(GenericAction.TrainingGriefWarning))
|
||||
}
|
||||
|
||||
def noVoicedChat(pkt: PlanetSideGamePacket): Unit = {
|
||||
|
|
@ -1199,6 +1206,27 @@ class GeneralOperations(
|
|||
}
|
||||
}
|
||||
|
||||
def handleUseBot(obj: AvatarBot, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||
if (msg.unk3) {
|
||||
msg.object_id match {
|
||||
case ObjectClass.avatar | ObjectClass.avatar_bot | ObjectClass.avatar_bot_agile | ObjectClass.avatar_bot_agile_no_weapon |
|
||||
ObjectClass.avatar_bot_max | ObjectClass.avatar_bot_max_no_weapon | ObjectClass.avatar_bot_reinforced |
|
||||
ObjectClass.avatar_bot_reinforced_no_weapon | ObjectClass.avatar_bot_standard | ObjectClass.avatar_bot_standard_no_weapon =>
|
||||
equipment match {
|
||||
case Some(tool: Tool) if tool.Definition == GlobalDefinitions.bank =>
|
||||
obj.Actor ! CommonMessages.Use(player, equipment)
|
||||
|
||||
case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
|
||||
obj.Actor ! CommonMessages.Use(player, equipment)
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
|
||||
equipment match {
|
||||
case Some(item) =>
|
||||
|
|
|
|||
|
|
@ -422,14 +422,25 @@ class SessionData(
|
|||
if (obj.spectator && obj != player) {
|
||||
administrativeKick(player)
|
||||
} else {
|
||||
if (obj.IsInVRZone && obj.Faction == player.Faction && obj.CharId != player.CharId) {
|
||||
//don't do friendly-fire in VR zones
|
||||
general.trainingGriefWarning()
|
||||
if (obj.IsInVRZone && obj.Faction == player.Faction) {
|
||||
//disable self-damage and friendly-fire in VR zones
|
||||
if (obj.CharId != player.CharId) {
|
||||
general.trainingGriefWarning()
|
||||
}
|
||||
} else {
|
||||
obj.Actor ! Vitality.Damage(func)
|
||||
}
|
||||
}
|
||||
|
||||
case obj: AvatarBot if obj.CanDamage && obj.Actor != Default.Actor =>
|
||||
log.info(s"${player.Name} is attacking ${obj.Name}")
|
||||
if (obj.IsInVRZone && obj.Faction == player.Faction) {
|
||||
//disable friendly-fire in VR zones
|
||||
general.trainingGriefWarning()
|
||||
} else {
|
||||
obj.Actor ! Vitality.Damage(func)
|
||||
}
|
||||
|
||||
case obj: Vehicle if obj.CanDamage =>
|
||||
val name = player.Name
|
||||
val ownerName = obj.OwnerName.getOrElse("someone")
|
||||
|
|
@ -438,17 +449,18 @@ class SessionData(
|
|||
} else {
|
||||
log.info(s"$name is attacking $ownerName's ${obj.Definition.Name}")
|
||||
}
|
||||
if (obj.IsInVRZone && obj.Faction == player.Faction && !ownerName.equals(name)) {
|
||||
//don't do friendly-fire in VR zones
|
||||
general.trainingGriefWarning()
|
||||
if (obj.IsInVRZone && obj.Faction == player.Faction) {
|
||||
//disable self-damage and friendly-fire in VR zones
|
||||
if (!ownerName.equals(name)) {
|
||||
general.trainingGriefWarning()
|
||||
}
|
||||
} else {
|
||||
obj.Actor ! Vitality.Damage(func)
|
||||
}
|
||||
|
||||
case obj: Amenity if obj.CanDamage =>
|
||||
if (obj.IsInVRZone && obj.Faction == player.Faction) {
|
||||
//don't do friendly-fire in VR zones
|
||||
general.trainingGriefWarning()
|
||||
//disable friendly-fire in VR zones
|
||||
} else {
|
||||
obj.Actor ! Vitality.Damage(func)
|
||||
}
|
||||
|
|
@ -461,12 +473,7 @@ class SessionData(
|
|||
} else {
|
||||
log.info(s"$name is attacking $ownerName's ${obj.Definition.Name}")
|
||||
}
|
||||
if (obj.IsInVRZone && obj.Faction == player.Faction && !ownerName.equals(name)) {
|
||||
//don't do friendly-fire in VR zones
|
||||
general.trainingGriefWarning()
|
||||
} else {
|
||||
obj.Actor ! Vitality.Damage(func)
|
||||
}
|
||||
obj.Actor ! Vitality.Damage(func)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -725,7 +725,7 @@ class WeaponAndProjectileOperations(
|
|||
proxy.Position = hitPos
|
||||
proxy.WhichSide = Sidedness.StrictlyBetweenSides
|
||||
val radiusSquared = proxy.profile.LashRadius * proxy.profile.LashRadius
|
||||
var availableTargets = sessionLogic.localSector.livePlayerList
|
||||
var availableTargets = sessionLogic.localSector.livePlayerList ++ sessionLogic.localSector.botList
|
||||
var unresolvedChainLashHits: Seq[VolumetricGeometry] = Seq(Point(hitPos))
|
||||
var uniqueChainLashTargets: Seq[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile)] = Seq()
|
||||
while (unresolvedChainLashHits.nonEmpty) {
|
||||
|
|
|
|||
|
|
@ -352,6 +352,9 @@ class ZoningOperations(
|
|||
.foreach { targetPlayer =>
|
||||
sendResponse(PlanetsideAttributeMessage(targetPlayer.GUID, 1, 120))
|
||||
}
|
||||
//load active bots in zone
|
||||
val bots = continent.BotAvatars
|
||||
bots.foreach(bot => sendResponse(OCM.apply(bot)))
|
||||
//load corpses in zone
|
||||
continent.Corpses.foreach {
|
||||
spawn.DepictPlayerAsCorpse
|
||||
|
|
@ -887,30 +890,18 @@ class ZoningOperations(
|
|||
RequestSanctuaryZoneSpawn(player, player.Zone.Number)
|
||||
// leveraging SetZone for now because the VR zones have no "proper" spawn points configured yet
|
||||
case 17 =>
|
||||
if (player.Zone.id != "tzshtr") {
|
||||
context.self ! SessionActor.SetZone("tzshtr", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
|
||||
}
|
||||
context.self ! SessionActor.SetZone("tzshtr", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
|
||||
case 18 =>
|
||||
if (player.Zone.id != "tzshnc") {
|
||||
context.self ! SessionActor.SetZone("tzshnc", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
|
||||
}
|
||||
context.self ! SessionActor.SetZone("tzshnc", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
|
||||
case 19 =>
|
||||
if (player.Zone.id != "tzshvs") {
|
||||
context.self ! SessionActor.SetZone("tzshvs", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
|
||||
}
|
||||
context.self ! SessionActor.SetZone("tzshvs", vrShootingZoneSpawns.toArray.apply(rand.nextInt(vrShootingZoneSpawns.size)))
|
||||
case 20 =>
|
||||
if (player.Zone.id != "tzdrtr") {
|
||||
context.self ! SessionActor.SetZone("tzdrtr", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size)))
|
||||
}
|
||||
context.self ! SessionActor.SetZone("tzdrtr", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size)))
|
||||
case 21 =>
|
||||
if (player.Zone.id != "tzdrnc") {
|
||||
context.self ! SessionActor.SetZone("tzdrnc", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size)))
|
||||
}
|
||||
context.self ! SessionActor.SetZone("tzdrnc", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size)))
|
||||
case 22 =>
|
||||
if (player.Zone.id != "tzdrvs") {
|
||||
context.self ! SessionActor.SetZone("tzdrvs", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size)))
|
||||
}
|
||||
case _ =>
|
||||
context.self ! SessionActor.SetZone("tzdrvs", vrDrivingAreaSpawns.toArray.apply(rand.nextInt(vrDrivingAreaSpawns.size)))
|
||||
case _ =>
|
||||
log.warn(s"Received TrainingZoneMessage that requests unexpected zone number ${pkt.zone.guid}?")
|
||||
}
|
||||
}
|
||||
|
|
@ -1280,9 +1271,11 @@ class ZoningOperations(
|
|||
ICS.FindZone(_.id.equals(zoneId), context.self)
|
||||
))
|
||||
} else if (player.HasGUID) {
|
||||
if (player.IsInVRZone && !zoneId.startsWith("tz")) {
|
||||
// reset the player to default gear to prevent them from smuggling things out of VR
|
||||
log.info(s"${player.Name} is exiting VR Training, resetting ${player.Sex.possessive} loadout")
|
||||
if (zoneId.startsWith("tz") || player.IsInVRZone) {
|
||||
// reset the players loadout when entering or exiting any VR Training zone
|
||||
// this is to prevent both entering the VR Driving Area with an ExoSuit too heavy to drive,
|
||||
// or smuggling special equipment out of the VR Shooting Range
|
||||
log.info(s"${player.Name} is zoning to or from a VR Training zone, resetting ${player.Sex.possessive} loadout")
|
||||
val newPlayer = Player.Respawn(player)
|
||||
DefinitionUtil.applyDefaultLoadout(newPlayer)
|
||||
session = session.copy(player = newPlayer)
|
||||
|
|
@ -3176,10 +3169,12 @@ class ZoningOperations(
|
|||
|
||||
case _ if player.HasGUID => // player is deconstructing self or instant action
|
||||
val player_guid = player.GUID
|
||||
sendResponse(ObjectDeleteMessage(player_guid, unk1=1))
|
||||
// entering or exiting VR zones uses a fade-out effect for the player instead of the usual green cloud deconstruction effect
|
||||
val effect = if (player.IsInVRZone || zoneId.startsWith("tz")) 2 else 1
|
||||
sendResponse(ObjectDeleteMessage(player_guid, unk1=effect))
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
continent.id,
|
||||
AvatarAction.ObjectDelete(player_guid, player_guid, unk=1)
|
||||
AvatarAction.ObjectDelete(player_guid, player_guid, unk=effect)
|
||||
)
|
||||
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
|
||||
LoadZoneAsPlayerUsing(player, pos, ori, toSide, zoneId)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,390 @@
|
|||
// Copyright (c) 2026 PSForever
|
||||
package net.psforever.actors.zone
|
||||
|
||||
import akka.actor.{Actor, Props}
|
||||
import net.psforever.objects.{Default, GlobalDefinitions, Tool, Vehicle}
|
||||
import net.psforever.objects.avatar.{AvatarBot, AvatarBotActor}
|
||||
import net.psforever.objects.guid.{GUIDTask, StraightforwardTask, TaskBundle, TaskWorkflow}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.types.{CharacterSex, CharacterVoice, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||
import net.psforever.util.Config
|
||||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
object ShootingRangeTargetSpawner {
|
||||
final case class InfantryTargetReleased(bot: AvatarBot)
|
||||
|
||||
final case class VehicleTargetDeconstructed(vehicle: Vehicle)
|
||||
}
|
||||
|
||||
class ShootingRangeTargetSpawnerActor(zone: Zone) extends Actor {
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
private val maleOnlyBotNames = Config.app.game.virtualTraining.maleBotNames
|
||||
private val femaleOnlyBotNames = Config.app.game.virtualTraining.femaleBotNames
|
||||
private val universalBotNames = Config.app.game.virtualTraining.universalBotNames
|
||||
|
||||
private val airVehicleSpawns = List[(Vector3, Float)](
|
||||
(Vector3(526.1094f, 517.84375f, 18.65625f), 295.3125f),
|
||||
(Vector3(507.9297f, 567.4297f, 20.328125f), 182.8125f),
|
||||
(Vector3(533.8516f, 551.28125f, 20.96875f), 278.4375f),
|
||||
(Vector3(504.5390f, 491.9922f, 22.015625f), 64.6875f),
|
||||
(Vector3(464.3281f, 513.5625f, 22.4375f), 348.75f),
|
||||
(Vector3(457.4375f, 546.71094f, 21.90625f), 146.25f),
|
||||
(Vector3(502.7734f, 467.5078f, 31.34375f), 334.6875f),
|
||||
(Vector3(489.2580f, 608.71094f, 28.046875f), 78.75f),
|
||||
(Vector3(570.1562f, 569.4766f, 26.328125f), 230.625f),
|
||||
(Vector3(576.2891f, 487.8594f, 28.71875f), 351.5625f),
|
||||
(Vector3(419.3906f, 572.8984f, 22.296875f), 137.8125f),
|
||||
(Vector3(410.7266f, 493.95312f, 25.578125f), 19.6875f)
|
||||
)
|
||||
|
||||
private val groundVehicleSpawns = List[(Vector3, Float)](
|
||||
(Vector3(501.6562f, 479.1562f, 12.421875f), 351.5625f),
|
||||
(Vector3(463.1484f, 522.72656f, 12.0625f), 59.0625f),
|
||||
(Vector3(510.2812f, 573.9844f, 11.453125f), 295.3125f),
|
||||
(Vector3(543.1330f, 519.0703f, 11.0625f), 188.4375f),
|
||||
(Vector3(547.1406f, 555.09375f, 11.859375f), 315.0f),
|
||||
(Vector3(463.9220f, 570.71094f, 12.15625f), 75.9375f)
|
||||
)
|
||||
|
||||
private val infantrySpawns = List[(Vector3, Float)](
|
||||
(Vector3(499.7969f, 546.6484f, 14.906250f), 191.2500f),
|
||||
(Vector3(507.1172f, 547.7188f, 14.906250f), 205.3125f),
|
||||
(Vector3(497.9688f, 550.0469f, 14.734375f), 199.6875f),
|
||||
(Vector3(519.3438f, 536.0000f, 14.796875f), 275.6250f),
|
||||
(Vector3(481.4609f, 539.5000f, 14.421875f), 135.0000f),
|
||||
(Vector3(522.1094f, 524.4766f, 13.593750f), 306.5625f),
|
||||
(Vector3(481.4453f, 524.0781f, 14.796875f), 84.3750f),
|
||||
(Vector3(499.6875f, 557.7578f, 13.375000f), 180.0000f),
|
||||
(Vector3(483.3984f, 517.5938f, 14.796875f), 67.5000f),
|
||||
(Vector3(487.9531f, 555.8828f, 13.468750f), 157.5000f),
|
||||
(Vector3(527.2109f, 546.3984f, 13.765625f), 250.3125f),
|
||||
(Vector3(472.9609f, 536.8125f, 13.093750f), 95.6250f),
|
||||
(Vector3(476.3984f, 548.1094f, 12.437500f), 126.5625f),
|
||||
(Vector3(503.7188f, 503.8047f, 14.296875f), 357.1875f),
|
||||
(Vector3(475.7344f, 514.3984f, 14.125000f), 56.2500f),
|
||||
(Vector3(492.8750f, 502.5547f, 13.828125f), 5.6250f),
|
||||
(Vector3(511.1172f, 501.3047f, 13.812500f), 345.9375f),
|
||||
(Vector3(498.2422f, 592.5938f, 13.796875f), 180.0000f),
|
||||
(Vector3(448.2500f, 579.2500f, 13.015625f), 120.9375f),
|
||||
(Vector3(417.7109f, 511.1719f, 13.828125f), 70.3125f),
|
||||
(Vector3(589.2812f, 551.2266f, 13.687500f), 250.3125f),
|
||||
(Vector3(505.4062f, 440.4062f, 13.203125f), 19.6875f),
|
||||
(Vector3(540.4297f, 464.6406f, 13.593750f), 306.5625f),
|
||||
(Vector3(549.6016f, 558.6719f, 13.687500f), 250.3125f)
|
||||
)
|
||||
|
||||
private val infantrySpawnsMAX = List[(Vector3, Float)](
|
||||
(Vector3(506.6484f, 544.3984f, 14.937500f), 196.8750f),
|
||||
(Vector3(503.9219f, 559.7109f, 13.375000f), 188.4375f),
|
||||
(Vector3(497.3125f, 510.0312f, 14.640625f), 14.0625f),
|
||||
(Vector3(507.9922f, 511.7656f, 14.625000f), 357.1875f),
|
||||
(Vector3(511.1875f, 558.5625f, 14.640625f), 199.6875f),
|
||||
(Vector3(473.9062f, 517.5156f, 14.265625f), 70.3125f),
|
||||
(Vector3(474.9062f, 541.5234f, 12.765625f), 106.8750f),
|
||||
(Vector3(530.0000f, 513.5469f, 13.046875f), 303.7500f),
|
||||
(Vector3(529.4062f, 538.7891f, 13.390625f), 267.1875f)
|
||||
)
|
||||
|
||||
private val activeInfantryTargets = ListBuffer[AvatarBot]()
|
||||
private val activeVehicleTargets = ListBuffer[(Vehicle, Vector3)]()
|
||||
private var botNamesInUse = List[String]()
|
||||
|
||||
override def preStart() = {
|
||||
if (Config.app.game.virtualTraining.shootingRangeTargetsEnabled) {
|
||||
//delayed to avoid potential GUID registration errors
|
||||
context.system.scheduler.scheduleOnce(
|
||||
3.seconds,
|
||||
new Runnable() { override def run(): Unit = StartSpawner() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override def postStop(): Unit = {
|
||||
activeInfantryTargets.foreach{ target =>
|
||||
target.Actor ! AvatarBot.Release()
|
||||
}
|
||||
activeInfantryTargets.clear()
|
||||
botNamesInUse = List[String]()
|
||||
activeVehicleTargets.foreach{ case (target, pos) =>
|
||||
if (target.Actor != Default.Actor) {
|
||||
target.Actor ! Vehicle.Deconstruct(None)
|
||||
}
|
||||
}
|
||||
activeVehicleTargets.clear()
|
||||
}
|
||||
|
||||
def receive: Receive = {
|
||||
case ShootingRangeTargetSpawner.InfantryTargetReleased(bot) =>
|
||||
RemoveBot(bot)
|
||||
|
||||
case ShootingRangeTargetSpawner.VehicleTargetDeconstructed(vehicle) =>
|
||||
OnVehicleTargetDeconstructed(vehicle)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
private def StartSpawner(): Unit = {
|
||||
val validZone = zone.id match {
|
||||
case "tzshtr" | "tzshnc" | "tzshvs" => true
|
||||
case _ => false
|
||||
}
|
||||
if (!validZone) {
|
||||
log.warn(s"Failed to enable target spawns for zone ${zone.id}; not a valid zone for this behavior")
|
||||
} else {
|
||||
airVehicleSpawns.foreach{case (pos, yaw) => CreateVehicleTarget(pos, yaw, true)}
|
||||
groundVehicleSpawns.foreach{case (pos, yaw) => CreateVehicleTarget(pos, yaw, false)}
|
||||
infantrySpawns.foreach{case (pos, yaw) => CreateInfantryTarget(pos, false)}
|
||||
infantrySpawnsMAX.foreach{case (pos, yaw) => CreateInfantryTarget(pos, true)}
|
||||
|
||||
log.info(s"Enabled target spawns for zone ${zone.id}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new infantry target at the specified coordinates.
|
||||
* @param position the position the target will be created at
|
||||
* @param facingYaw the direction the target will be facing
|
||||
* @param isMAX if this target is a MAX unit
|
||||
*/
|
||||
def CreateInfantryTarget(position: Vector3, isMAX: Boolean): Unit = {
|
||||
val definition = if (isMAX) GlobalDefinitions.avatar_bot_max_no_weapon else Random.nextInt(3) match {
|
||||
case 0 => GlobalDefinitions.avatar_bot_agile_no_weapon
|
||||
case 1 => GlobalDefinitions.avatar_bot_reinforced_no_weapon
|
||||
case 2 => GlobalDefinitions.avatar_bot_standard_no_weapon
|
||||
}
|
||||
val gender = if (Random.nextBoolean()) CharacterSex.Female else CharacterSex.Male
|
||||
val name = GetRandomBotName(gender)
|
||||
val factionRNG = Random.nextBoolean()
|
||||
val faction = zone.id match {
|
||||
case "tzshtr" => if (factionRNG) PlanetSideEmpire.VS else PlanetSideEmpire.NC
|
||||
case "tzshnc" => if (factionRNG) PlanetSideEmpire.VS else PlanetSideEmpire.TR
|
||||
case "tzshvs" => if (factionRNG) PlanetSideEmpire.NC else PlanetSideEmpire.TR
|
||||
}
|
||||
val head = Random.nextInt(if (gender == CharacterSex.Female) 10 else 11)
|
||||
val voiceRNG = Random.nextInt(4)
|
||||
val voice = voiceRNG match {
|
||||
case 0 => CharacterVoice.Voice1
|
||||
case 1 => CharacterVoice.Voice2
|
||||
case 2 => CharacterVoice.Voice3
|
||||
case 3 => CharacterVoice.Voice4
|
||||
case 4 => CharacterVoice.Voice5
|
||||
}
|
||||
|
||||
val facingYaw = if (isMAX) infantrySpawnsMAX.find(_._1 == position) match {
|
||||
case Some((_, yaw)) => yaw
|
||||
case _ => 0
|
||||
}
|
||||
else infantrySpawns.find(_._1 == position) match {
|
||||
case Some((_, yaw)) => yaw
|
||||
case _ => 0
|
||||
}
|
||||
|
||||
val bot = AvatarBot(name, faction, gender, head, voice, definition)
|
||||
bot.Position = position
|
||||
bot.Orientation = Vector3(0f, 0f, facingYaw)
|
||||
bot.Zone = zone
|
||||
bot.ExoSuit = definition match {
|
||||
case GlobalDefinitions.avatar_bot_agile | GlobalDefinitions.avatar_bot_agile_no_weapon =>
|
||||
ExoSuitType.Agile
|
||||
case GlobalDefinitions.avatar_bot_max | GlobalDefinitions.avatar_bot_max_no_weapon =>
|
||||
ExoSuitType.MAX
|
||||
case GlobalDefinitions.avatar_bot_reinforced | GlobalDefinitions.avatar_bot_reinforced_no_weapon =>
|
||||
ExoSuitType.Reinforced
|
||||
case GlobalDefinitions.avatar_bot_standard | GlobalDefinitions.avatar_bot_standard_no_weapon =>
|
||||
ExoSuitType.Standard
|
||||
case _ =>
|
||||
ExoSuitType.Standard
|
||||
}
|
||||
if (bot.ExoSuit == ExoSuitType.MAX) {
|
||||
val subtype = 1 + Random.nextInt(3)
|
||||
bot.Slot(0).Equipment = Tool(GlobalDefinitions.MAXArms(subtype, faction))
|
||||
bot.DrawnSlot = 0 //max arm up
|
||||
}
|
||||
|
||||
TaskWorkflow.execute(RegisterAndSpawnBot(bot))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a random name for a bot and removes the name from the name pool.
|
||||
* @param gender determines if it should pull from the male or female name pools
|
||||
* @return the name as a string
|
||||
*/
|
||||
private def GetRandomBotName(gender: CharacterSex): String = {
|
||||
try {
|
||||
gender match {
|
||||
case CharacterSex.Male =>
|
||||
val availableNames = (maleOnlyBotNames ++ universalBotNames).filterNot(n => botNamesInUse.contains(n))
|
||||
if (!availableNames.isEmpty) {
|
||||
val name = availableNames(Random.nextInt(availableNames.size))
|
||||
botNamesInUse = botNamesInUse :+ name
|
||||
name
|
||||
} else {
|
||||
log.warn(s"Male bot name pool in ${zone.id} is empty!")
|
||||
"Bot"
|
||||
}
|
||||
case CharacterSex.Female =>
|
||||
val availableNames = (femaleOnlyBotNames ++ universalBotNames).filterNot(n => botNamesInUse.contains(n))
|
||||
if (!availableNames.isEmpty) {
|
||||
val name = availableNames(Random.nextInt(availableNames.size))
|
||||
botNamesInUse = botNamesInUse :+ name
|
||||
name
|
||||
} else {
|
||||
log.warn(s"Female bot name pool in ${zone.id} is empty!")
|
||||
"Bot"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
//while the issue that was causing a mutation during iteration exception to be thrown here rarely should hopefully be fixed now,
|
||||
//this is still being put here as a fallback to not block the bot from spawning
|
||||
case ex: Exception =>
|
||||
"Bot"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the `AvatarBot` object and spawns it into the zone.
|
||||
* @param bot the `AvatarBot` object
|
||||
* @return a `TaskBundle` message
|
||||
*/
|
||||
private def RegisterAndSpawnBot(bot: AvatarBot): TaskBundle = {
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
TaskBundle(
|
||||
new StraightforwardTask() {
|
||||
private val localBot = bot
|
||||
|
||||
override def description(): String = s"register a ${localBot.Definition.Name}"
|
||||
|
||||
def action(): Future[Any] = {
|
||||
localBot.Actor = context.actorOf(
|
||||
Props(classOf[AvatarBotActor], localBot, context.self),
|
||||
PlanetSideServerObject.UniqueActorName(localBot)
|
||||
)
|
||||
localBot.Actor ! AvatarBot.Spawn()
|
||||
activeInfantryTargets.addOne(localBot)
|
||||
log.debug(s"Spawned a ${localBot.Faction} bot named ${localBot.Name} in ${zone.id} at ${localBot.Position}")
|
||||
Future(true)
|
||||
}
|
||||
},
|
||||
List(GUIDTask.registerBot(zone.GUID, bot))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified bot from the scene and unregisters it.
|
||||
* @param bot the bot to remove
|
||||
*/
|
||||
private def RemoveBot(bot: AvatarBot): Boolean = {
|
||||
import net.psforever.services.Service
|
||||
activeInfantryTargets.indexOf(bot) match {
|
||||
case -1 =>
|
||||
log.warn(s"Failed to remove bot with GUID ${bot.GUID} from ${zone.id}'s active targets list! This shouldn't happen... and probably just caused a leak.")
|
||||
false
|
||||
case index =>
|
||||
activeInfantryTargets.remove(index)
|
||||
}
|
||||
zone.LocalEvents ! LocalServiceMessage(
|
||||
zone.id,
|
||||
LocalAction.TriggerEffectLocation(Service.defaultPlayerGUID, "bot_destroyed_effect", bot.Position, bot.Orientation)
|
||||
)
|
||||
//spawn a replacement bot
|
||||
context.system.scheduler.scheduleOnce(
|
||||
5.seconds,
|
||||
new Runnable() { override def run(): Unit = CreateInfantryTarget(bot.Position, bot.ExoSuit == ExoSuitType.MAX) }
|
||||
)
|
||||
//unregister bot (delay is to prevent ValidObjects from complaining if the bot is getting hit too quickly when it is destroyed)
|
||||
context.system.scheduler.scheduleOnce(
|
||||
1.seconds,
|
||||
new Runnable() { override def run(): Unit = TaskWorkflow.execute(GUIDTask.unregisterBot(bot.Zone.GUID, bot)) }
|
||||
)
|
||||
//return bot name to name pool
|
||||
botNamesInUse.indexOf(bot.Name) match {
|
||||
case -1 => log.warn(s"Failed to restore bot name `${bot.Name}` to the bot name pool!")
|
||||
case index => botNamesInUse = botNamesInUse.filterNot(n => n == bot.Name)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new vehicle target at the specified coordinates.
|
||||
* @param position the position the target will be created at
|
||||
* @param facingYaw the direction the target will be facing
|
||||
* @param airVehicle if this target is an aircraft
|
||||
*/
|
||||
private def CreateVehicleTarget(position: Vector3, facingYaw: Float, airVehicle: Boolean): Unit = {
|
||||
val definition = if (airVehicle) Random.nextBoolean() match {
|
||||
case true => GlobalDefinitions.lightgunship
|
||||
case false => GlobalDefinitions.mosquito
|
||||
} else Random.nextInt(4) match {
|
||||
case 0 => GlobalDefinitions.lightning
|
||||
case 1 => GlobalDefinitions.quadassault
|
||||
case 2 => GlobalDefinitions.quadstealth
|
||||
case 3 => GlobalDefinitions.two_man_assault_buggy
|
||||
}
|
||||
val factionRNG = Random.nextBoolean()
|
||||
val faction = zone.id match {
|
||||
case "tzshtr" => if (factionRNG) PlanetSideEmpire.VS else PlanetSideEmpire.NC
|
||||
case "tzshnc" => if (factionRNG) PlanetSideEmpire.VS else PlanetSideEmpire.TR
|
||||
case "tzshvs" => if (factionRNG) PlanetSideEmpire.NC else PlanetSideEmpire.TR
|
||||
}
|
||||
|
||||
val vehicle = Vehicle(definition)
|
||||
vehicle.Position = position
|
||||
vehicle.Orientation = Vector3(0f, 0f, facingYaw)
|
||||
vehicle.Faction = faction
|
||||
TaskWorkflow.execute(RegisterAndSpawnVehicle(vehicle))
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the `Vehicle` object and spawns it into the zone.
|
||||
* @param vehicle the `Vehicle` object
|
||||
* @return a `TaskBundle` message
|
||||
*/
|
||||
private def RegisterAndSpawnVehicle(vehicle: Vehicle): TaskBundle = {
|
||||
TaskBundle(
|
||||
new StraightforwardTask() {
|
||||
private val localVehicle = vehicle
|
||||
|
||||
override def description(): String = s"register a ${localVehicle.Definition.Name}"
|
||||
|
||||
def action(): Future[Any] = {
|
||||
zone.Transport ! Zone.Vehicle.Spawn(localVehicle)
|
||||
zone.VehicleEvents ! VehicleServiceMessage(
|
||||
zone.id,
|
||||
VehicleAction.LoadVehicle(
|
||||
PlanetSideGUID(0),
|
||||
localVehicle,
|
||||
localVehicle.Definition.ObjectId,
|
||||
localVehicle.GUID,
|
||||
localVehicle.Definition.Packet.ConstructorData(localVehicle).get
|
||||
)
|
||||
)
|
||||
activeVehicleTargets.addOne((localVehicle, localVehicle.Position))
|
||||
log.debug(s"Spawned a ${localVehicle.Faction} ${localVehicle.Definition.Name} in ${zone.id} at ${localVehicle.Position}")
|
||||
Future(true)
|
||||
}
|
||||
},
|
||||
List(GUIDTask.registerVehicle(zone.GUID, vehicle))
|
||||
)
|
||||
}
|
||||
|
||||
private def OnVehicleTargetDeconstructed(vehicle: Vehicle): Unit = {
|
||||
activeVehicleTargets.find(_._1 == vehicle) match {
|
||||
case Some((target, pos)) =>
|
||||
val index = activeVehicleTargets.indexOf((target, pos))
|
||||
activeVehicleTargets.remove(index)
|
||||
context.system.scheduler.scheduleOnce(
|
||||
5.seconds,
|
||||
new Runnable() { override def run(): Unit = CreateVehicleTarget(pos, vehicle.Orientation.z, GlobalDefinitions.isFlightVehicle(vehicle.Definition)) }
|
||||
)
|
||||
case None =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,96 @@ object GlobalDefinitions {
|
|||
avatar.collision.z = CollisionZData(Array((0.1f, 0), (5f, 1), (10f, 3), (20f, 5), (35f, 7), (50f, 10), (75f, 40), (100f, 100))) //not in the ADB
|
||||
avatar.maxForwardSpeed = 27f //not in the ADB; running speed
|
||||
|
||||
val avatar_bot = new AvatarBotDefinition(122)
|
||||
avatar_bot.MaxHealth = 100
|
||||
avatar_bot.Damageable = true
|
||||
avatar_bot.DrownAtMaxDepth = true
|
||||
avatar_bot.MaxDepth = 1.609375f //Male, standing, not MAX
|
||||
avatar_bot.UnderwaterLifespan(suffocation = 60000L, recovery = 10000L)
|
||||
avatar_bot.collision.xy = CollisionXYData(Array((0.1f, 0), (0.2f, 5), (0.50f, 15), (0.75f, 20), (1f, 30))) //not in the ADB
|
||||
avatar_bot.collision.z = CollisionZData(Array((0.1f, 0), (5f, 1), (10f, 3), (20f, 5), (35f, 7), (50f, 10), (75f, 40), (100f, 100))) //not in the ADB
|
||||
avatar_bot.maxForwardSpeed = 27f //not in the ADB; running speed
|
||||
|
||||
val avatar_bot_agile = new AvatarBotDefinition(123)
|
||||
avatar_bot_agile.MaxHealth = 100
|
||||
avatar_bot_agile.Damageable = true
|
||||
avatar_bot_agile.DrownAtMaxDepth = true
|
||||
avatar_bot_agile.MaxDepth = 1.609375f //Male, standing, not MAX
|
||||
avatar_bot_agile.UnderwaterLifespan(suffocation = 60000L, recovery = 10000L)
|
||||
avatar_bot_agile.collision.xy = CollisionXYData(Array((0.1f, 0), (0.2f, 5), (0.50f, 15), (0.75f, 20), (1f, 30))) //not in the ADB
|
||||
avatar_bot_agile.collision.z = CollisionZData(Array((0.1f, 0), (5f, 1), (10f, 3), (20f, 5), (35f, 7), (50f, 10), (75f, 40), (100f, 100))) //not in the ADB
|
||||
avatar_bot_agile.maxForwardSpeed = 27f //not in the ADB; running speed
|
||||
|
||||
val avatar_bot_agile_no_weapon = new AvatarBotDefinition(124)
|
||||
avatar_bot_agile_no_weapon.MaxHealth = 100
|
||||
avatar_bot_agile_no_weapon.Damageable = true
|
||||
avatar_bot_agile_no_weapon.DrownAtMaxDepth = true
|
||||
avatar_bot_agile_no_weapon.MaxDepth = 1.609375f //Male, standing, not MAX
|
||||
avatar_bot_agile_no_weapon.UnderwaterLifespan(suffocation = 60000L, recovery = 10000L)
|
||||
avatar_bot_agile_no_weapon.collision.xy = CollisionXYData(Array((0.1f, 0), (0.2f, 5), (0.50f, 15), (0.75f, 20), (1f, 30))) //not in the ADB
|
||||
avatar_bot_agile_no_weapon.collision.z = CollisionZData(Array((0.1f, 0), (5f, 1), (10f, 3), (20f, 5), (35f, 7), (50f, 10), (75f, 40), (100f, 100))) //not in the ADB
|
||||
avatar_bot_agile_no_weapon.maxForwardSpeed = 27f //not in the ADB; running speed
|
||||
|
||||
val avatar_bot_max = new AvatarBotDefinition(125)
|
||||
avatar_bot_max.MaxHealth = 100
|
||||
avatar_bot_max.Damageable = true
|
||||
avatar_bot_max.DrownAtMaxDepth = true
|
||||
avatar_bot_max.MaxDepth = 1.609375f //Male, standing, not MAX
|
||||
avatar_bot_max.UnderwaterLifespan(suffocation = 60000L, recovery = 10000L)
|
||||
avatar_bot_max.collision.xy = CollisionXYData(Array((0.1f, 0), (0.2f, 5), (0.50f, 15), (0.75f, 20), (1f, 30))) //not in the ADB
|
||||
avatar_bot_max.collision.z = CollisionZData(Array((0.1f, 0), (5f, 1), (10f, 3), (20f, 5), (35f, 7), (50f, 10), (75f, 40), (100f, 100))) //not in the ADB
|
||||
avatar_bot_max.maxForwardSpeed = 27f //not in the ADB; running speed
|
||||
|
||||
val avatar_bot_max_no_weapon = new AvatarBotDefinition(126)
|
||||
avatar_bot_max_no_weapon.MaxHealth = 100
|
||||
avatar_bot_max_no_weapon.Damageable = true
|
||||
avatar_bot_max_no_weapon.DrownAtMaxDepth = true
|
||||
avatar_bot_max_no_weapon.MaxDepth = 1.609375f //Male, standing, not MAX
|
||||
avatar_bot_max_no_weapon.UnderwaterLifespan(suffocation = 60000L, recovery = 10000L)
|
||||
avatar_bot_max_no_weapon.collision.xy = CollisionXYData(Array((0.1f, 0), (0.2f, 5), (0.50f, 15), (0.75f, 20), (1f, 30))) //not in the ADB
|
||||
avatar_bot_max_no_weapon.collision.z = CollisionZData(Array((0.1f, 0), (5f, 1), (10f, 3), (20f, 5), (35f, 7), (50f, 10), (75f, 40), (100f, 100))) //not in the ADB
|
||||
avatar_bot_max_no_weapon.maxForwardSpeed = 27f //not in the ADB; running speed
|
||||
|
||||
val avatar_bot_reinforced = new AvatarBotDefinition(127)
|
||||
avatar_bot_reinforced.MaxHealth = 100
|
||||
avatar_bot_reinforced.Damageable = true
|
||||
avatar_bot_reinforced.DrownAtMaxDepth = true
|
||||
avatar_bot_reinforced.MaxDepth = 1.609375f //Male, standing, not MAX
|
||||
avatar_bot_reinforced.UnderwaterLifespan(suffocation = 60000L, recovery = 10000L)
|
||||
avatar_bot_reinforced.collision.xy = CollisionXYData(Array((0.1f, 0), (0.2f, 5), (0.50f, 15), (0.75f, 20), (1f, 30))) //not in the ADB
|
||||
avatar_bot_reinforced.collision.z = CollisionZData(Array((0.1f, 0), (5f, 1), (10f, 3), (20f, 5), (35f, 7), (50f, 10), (75f, 40), (100f, 100))) //not in the ADB
|
||||
avatar_bot_reinforced.maxForwardSpeed = 27f //not in the ADB; running speed
|
||||
|
||||
val avatar_bot_reinforced_no_weapon = new AvatarBotDefinition(128)
|
||||
avatar_bot_reinforced_no_weapon.MaxHealth = 100
|
||||
avatar_bot_reinforced_no_weapon.Damageable = true
|
||||
avatar_bot_reinforced_no_weapon.DrownAtMaxDepth = true
|
||||
avatar_bot_reinforced_no_weapon.MaxDepth = 1.609375f //Male, standing, not MAX
|
||||
avatar_bot_reinforced_no_weapon.UnderwaterLifespan(suffocation = 60000L, recovery = 10000L)
|
||||
avatar_bot_reinforced_no_weapon.collision.xy = CollisionXYData(Array((0.1f, 0), (0.2f, 5), (0.50f, 15), (0.75f, 20), (1f, 30))) //not in the ADB
|
||||
avatar_bot_reinforced_no_weapon.collision.z = CollisionZData(Array((0.1f, 0), (5f, 1), (10f, 3), (20f, 5), (35f, 7), (50f, 10), (75f, 40), (100f, 100))) //not in the ADB
|
||||
avatar_bot_reinforced_no_weapon.maxForwardSpeed = 27f //not in the ADB; running speed
|
||||
|
||||
val avatar_bot_standard = new AvatarBotDefinition(129)
|
||||
avatar_bot_standard.MaxHealth = 100
|
||||
avatar_bot_standard.Damageable = true
|
||||
avatar_bot_standard.DrownAtMaxDepth = true
|
||||
avatar_bot_standard.MaxDepth = 1.609375f //Male, standing, not MAX
|
||||
avatar_bot_standard.UnderwaterLifespan(suffocation = 60000L, recovery = 10000L)
|
||||
avatar_bot_standard.collision.xy = CollisionXYData(Array((0.1f, 0), (0.2f, 5), (0.50f, 15), (0.75f, 20), (1f, 30))) //not in the ADB
|
||||
avatar_bot_standard.collision.z = CollisionZData(Array((0.1f, 0), (5f, 1), (10f, 3), (20f, 5), (35f, 7), (50f, 10), (75f, 40), (100f, 100))) //not in the ADB
|
||||
avatar_bot_standard.maxForwardSpeed = 27f //not in the ADB; running speed
|
||||
|
||||
val avatar_bot_standard_no_weapon = new AvatarBotDefinition(130)
|
||||
avatar_bot_standard_no_weapon.MaxHealth = 100
|
||||
avatar_bot_standard_no_weapon.Damageable = true
|
||||
avatar_bot_standard_no_weapon.DrownAtMaxDepth = true
|
||||
avatar_bot_standard_no_weapon.MaxDepth = 1.609375f //Male, standing, not MAX
|
||||
avatar_bot_standard_no_weapon.UnderwaterLifespan(suffocation = 60000L, recovery = 10000L)
|
||||
avatar_bot_standard_no_weapon.collision.xy = CollisionXYData(Array((0.1f, 0), (0.2f, 5), (0.50f, 15), (0.75f, 20), (1f, 30))) //not in the ADB
|
||||
avatar_bot_standard_no_weapon.collision.z = CollisionZData(Array((0.1f, 0), (5f, 1), (10f, 3), (20f, 5), (35f, 7), (50f, 10), (75f, 40), (100f, 100))) //not in the ADB
|
||||
avatar_bot_standard_no_weapon.maxForwardSpeed = 27f //not in the ADB; running speed
|
||||
|
||||
/*
|
||||
exo-suits
|
||||
*/
|
||||
|
|
|
|||
284
src/main/scala/net/psforever/objects/avatar/AvatarBot.scala
Normal file
284
src/main/scala/net/psforever/objects/avatar/AvatarBot.scala
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
// Copyright (c) 2026 PSForever
|
||||
package net.psforever.objects.avatar
|
||||
|
||||
import net.psforever.objects.avatar.interaction.{InteractWithForceDomeProtection, TriggerOnPlayerRule, WithEntrance, WithGantry, WithLava, WithWater}
|
||||
import net.psforever.objects.{GlobalDefinitions, Player, OffhandEquipmentSlot}
|
||||
import net.psforever.objects.ballistics.InteractWithRadiationClouds
|
||||
import net.psforever.objects.ce.{InteractWithMines, InteractWithTurrets}
|
||||
import net.psforever.objects.definition.{AvatarBotDefinition, ExoSuitDefinition}
|
||||
import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot, JammableUnit}
|
||||
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
|
||||
import net.psforever.objects.serverobject.{PlanetSideServerObject, environment}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.aura.AuraContainer
|
||||
import net.psforever.objects.serverobject.environment.interaction.common.{WithDeath, WithMovementTrigger}
|
||||
import net.psforever.objects.serverobject.interior.InteriorAwareFromInteraction
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.damage.DamageProfile
|
||||
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||
import net.psforever.objects.vital.resistance.ResistanceProfile
|
||||
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
import net.psforever.objects.zones.interaction.InteractsWithZone
|
||||
import net.psforever.objects.zones.ZoneAware
|
||||
import net.psforever.packet.game.objectcreate.BasicCharacterData
|
||||
import net.psforever.types._
|
||||
|
||||
/**
|
||||
* A stripped down combination of the `Avatar` and `Player` classes.
|
||||
* This acts as a base class for NPC avatars, and is used for VR Shooting Range infantry targets.
|
||||
* @see `Avatar`
|
||||
* @see `Player`
|
||||
*/
|
||||
case class AvatarBot(
|
||||
basic: BasicCharacterData,
|
||||
definition: AvatarBotDefinition,
|
||||
bep: Long = 0,
|
||||
cep: Long = 0,
|
||||
certifications: Set[Certification] = Set(),
|
||||
implants: Seq[Option[Implant]] = Seq(None, None, None),
|
||||
decoration: ProgressDecoration = ProgressDecoration()
|
||||
) extends PlanetSideServerObject
|
||||
with BlockMapEntity
|
||||
with InteractsWithZone
|
||||
with FactionAffinity
|
||||
with Vitality
|
||||
with ResistanceProfile
|
||||
with Container
|
||||
with JammableUnit
|
||||
with ZoneAware
|
||||
with InteriorAwareFromInteraction
|
||||
with AuraContainer {
|
||||
interaction(new InteractWithForceDomeProtection())
|
||||
interaction(environment.interaction.InteractWithEnvironment(Seq(
|
||||
new WithEntrance(),
|
||||
new WithWater(Name),
|
||||
new WithLava(),
|
||||
new WithDeath(),
|
||||
new WithGantry(Name),
|
||||
new WithMovementTrigger()
|
||||
)))
|
||||
interaction(new InteractWithMines(range = 10, TriggerOnPlayerRule))
|
||||
interaction(new InteractWithTurrets())
|
||||
interaction(new InteractWithRadiationClouds(range = 10f, None))
|
||||
|
||||
val br: BattleRank = BattleRank.withExperience(bep)
|
||||
val cr: CommandRank = CommandRank.withExperience(cep)
|
||||
|
||||
private var armor: Int = 0
|
||||
|
||||
private var exosuit: ExoSuitDefinition = GlobalDefinitions.Standard
|
||||
private val freeHand: EquipmentSlot = new OffhandEquipmentSlot(EquipmentSize.Inventory)
|
||||
private val holsters: Array[EquipmentSlot] = Array.fill[EquipmentSlot](5)(new EquipmentSlot)
|
||||
private val inventory: GridInventory = GridInventory()
|
||||
private var drawnSlot: Int = Player.HandsDownSlot
|
||||
private var lastDrawnSlot: Int = Player.HandsDownSlot
|
||||
|
||||
private var facingYawUpper: Float = 0f
|
||||
private var crouching: Boolean = false
|
||||
private var jumping: Boolean = false
|
||||
private var cloaked: Boolean = false
|
||||
|
||||
/** The maximum stamina amount */
|
||||
val maxStamina: Int = 100
|
||||
|
||||
var fatigued: Boolean = false
|
||||
var stamina: Int = 100
|
||||
|
||||
//init
|
||||
Health = 0 //bot health is artificially managed as a part of their lifecycle; start entity as dead
|
||||
Destroyed = true //see isAlive
|
||||
AvatarBot.SuitSetup(this, exosuit)
|
||||
|
||||
def Definition: AvatarBotDefinition = definition
|
||||
|
||||
def Name: String = basic.name
|
||||
|
||||
def Faction: PlanetSideEmpire.Value = basic.faction
|
||||
|
||||
def Sex: CharacterSex = basic.sex
|
||||
|
||||
def Head: Int = basic.head
|
||||
|
||||
def Voice: CharacterVoice.Value = basic.voice
|
||||
|
||||
def isAlive: Boolean = !Destroyed
|
||||
|
||||
def Spawn(): Boolean = {
|
||||
if (!isAlive) {
|
||||
Destroyed = false
|
||||
Health = Definition.DefaultHealth
|
||||
Armor = MaxArmor
|
||||
}
|
||||
isAlive
|
||||
}
|
||||
|
||||
def Die: Boolean = {
|
||||
Destroyed = true
|
||||
Health = 0
|
||||
false
|
||||
}
|
||||
|
||||
def Armor: Int = armor
|
||||
|
||||
def Armor_=(assignArmor: Int): Int = {
|
||||
armor = math.min(math.max(0, assignArmor), MaxArmor)
|
||||
Armor
|
||||
}
|
||||
|
||||
def MaxArmor: Int = exosuit.MaxArmor
|
||||
|
||||
override def Slot(slot: Int): EquipmentSlot = {
|
||||
if (inventory.Offset <= slot && slot <= inventory.LastIndex) {
|
||||
inventory.Slot(slot)
|
||||
} else if (slot > -1 && slot < 5) {
|
||||
holsters(slot)
|
||||
} else if (slot == 5) {
|
||||
OffhandEquipmentSlot.BlockedSlot
|
||||
} else if (slot == Player.FreeHandSlot) {
|
||||
freeHand
|
||||
} else {
|
||||
OffhandEquipmentSlot.BlockedSlot
|
||||
}
|
||||
}
|
||||
|
||||
def VisibleSlots: Set[Int] =
|
||||
if (exosuit.SuitType == ExoSuitType.MAX) {
|
||||
Set(0)
|
||||
} else {
|
||||
(0 to 4).filterNot(index => holsters(index).Size == EquipmentSize.Blocked).toSet
|
||||
}
|
||||
|
||||
def Holsters(): Array[EquipmentSlot] = holsters
|
||||
|
||||
/**
|
||||
* Transform the holster equipment slots
|
||||
* into a list of the kind of item wrapper found in an inventory.
|
||||
* @see `GridInventory`
|
||||
* @see `InventoryItem`
|
||||
* @return a list of items that would be found in a proper inventory
|
||||
*/
|
||||
def HolsterItems(): List[InventoryItem] = holsters
|
||||
.zipWithIndex
|
||||
.collect {
|
||||
case (slot: EquipmentSlot, index: Int) =>
|
||||
slot.Equipment match {
|
||||
case Some(item) => Some(InventoryItem(item, index))
|
||||
case None => None
|
||||
}
|
||||
}.flatten.toList
|
||||
|
||||
def Inventory: GridInventory = inventory
|
||||
|
||||
def DrawnSlot: Int = drawnSlot
|
||||
|
||||
def DrawnSlot_=(slot: Int): Int = {
|
||||
if (slot != drawnSlot) {
|
||||
if (slot == Player.HandsDownSlot) {
|
||||
drawnSlot = slot
|
||||
} else if (VisibleSlots.contains(slot) && holsters(slot).Equipment.isDefined) {
|
||||
drawnSlot = slot
|
||||
lastDrawnSlot = slot
|
||||
}
|
||||
}
|
||||
DrawnSlot
|
||||
}
|
||||
|
||||
def LastDrawnSlot: Int = lastDrawnSlot
|
||||
|
||||
def ExoSuit: ExoSuitType.Value = exosuit.SuitType
|
||||
def ExoSuitDef: ExoSuitDefinition = exosuit
|
||||
|
||||
def ExoSuit_=(suit: ExoSuitType.Value): Unit = {
|
||||
val eSuit = ExoSuitDefinition.Select(suit, Faction)
|
||||
exosuit = eSuit
|
||||
AvatarBot.SuitSetup(this, eSuit)
|
||||
}
|
||||
|
||||
def Subtract: DamageProfile = exosuit.Subtract
|
||||
|
||||
def ResistanceDirectHit: Int = exosuit.ResistanceDirectHit
|
||||
|
||||
def ResistanceSplash: Int = exosuit.ResistanceSplash
|
||||
|
||||
def ResistanceAggravated: Int = exosuit.ResistanceAggravated
|
||||
|
||||
def RadiationShielding: Float = exosuit.RadiationShielding
|
||||
|
||||
def FacingYawUpper: Float = facingYawUpper
|
||||
|
||||
def FacingYawUpper_=(facing: Float): Float = {
|
||||
facingYawUpper = facing
|
||||
FacingYawUpper
|
||||
}
|
||||
|
||||
def Crouching: Boolean = crouching
|
||||
|
||||
def Crouching_=(crouched: Boolean): Boolean = {
|
||||
crouching = crouched
|
||||
Crouching
|
||||
}
|
||||
|
||||
def Jumping: Boolean = jumping
|
||||
|
||||
def Jumping_=(jumped: Boolean): Boolean = {
|
||||
jumping = jumped
|
||||
Jumping
|
||||
}
|
||||
|
||||
def Cloaked: Boolean = cloaked
|
||||
|
||||
def Cloaked_=(isCloaked: Boolean): Boolean = {
|
||||
cloaked = isCloaked
|
||||
Cloaked
|
||||
}
|
||||
|
||||
override def CanDamage: Boolean = {
|
||||
isAlive && super.CanDamage
|
||||
}
|
||||
|
||||
def DamageModel: DamageResistanceModel = exosuit.asInstanceOf[DamageResistanceModel]
|
||||
|
||||
/** Return true if the stamina is at the maximum amount */
|
||||
def staminaFull: Boolean = {
|
||||
stamina == maxStamina
|
||||
}
|
||||
|
||||
override def toString: String = {
|
||||
val guid = if (HasGUID) {
|
||||
s" $Continent-${GUID.guid}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
s"${basic.name}$guid ${basic.faction} H: $Health/$MaxHealth A: $Armor/$MaxArmor"
|
||||
}
|
||||
}
|
||||
|
||||
object AvatarBot {
|
||||
final case class Die(reason: Option[DamageInteraction])
|
||||
|
||||
final case class Release()
|
||||
|
||||
final case class Spawn()
|
||||
|
||||
object Die {
|
||||
def apply(): Die = Die(None)
|
||||
|
||||
def apply(reason: DamageInteraction): Die = {
|
||||
Die(Some(reason))
|
||||
}
|
||||
}
|
||||
|
||||
def apply(name: String, faction: PlanetSideEmpire.Value, sex: CharacterSex, head: Int, voice: CharacterVoice.Value, definition: AvatarBotDefinition): AvatarBot = {
|
||||
AvatarBot(BasicCharacterData(name, faction, sex, head, voice), definition)
|
||||
}
|
||||
|
||||
private def SuitSetup(bot: AvatarBot, eSuit: ExoSuitDefinition): Unit = {
|
||||
//inventory
|
||||
bot.Inventory.Clear()
|
||||
bot.Inventory.Resize(eSuit.InventoryScale.Width, eSuit.InventoryScale.Height)
|
||||
bot.Inventory.Offset = eSuit.InventoryOffset
|
||||
//holsters
|
||||
(0 until 5).foreach { index => bot.Slot(index).Size = eSuit.Holster(index) }
|
||||
}
|
||||
}
|
||||
546
src/main/scala/net/psforever/objects/avatar/AvatarBotActor.scala
Normal file
546
src/main/scala/net/psforever/objects/avatar/AvatarBotActor.scala
Normal file
|
|
@ -0,0 +1,546 @@
|
|||
// Copyright (c) 2026 PSForever
|
||||
package net.psforever.objects.avatar
|
||||
|
||||
import akka.actor.{Actor, ActorRef}
|
||||
import net.psforever.actors.zone.ShootingRangeTargetSpawner
|
||||
import net.psforever.objects.{GlobalDefinitions, Tool}
|
||||
import net.psforever.objects.avatar.AvatarBot
|
||||
import net.psforever.objects.equipment._
|
||||
import net.psforever.objects.serverobject.aura.{Aura, AuraEffectBehavior}
|
||||
import net.psforever.objects.serverobject.CommonMessages
|
||||
import net.psforever.objects.serverobject.damage.Damageable.Target
|
||||
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, Damageable, DamageableEntity}
|
||||
import net.psforever.objects.vital.resolution.ResolutionCalculations.Output
|
||||
import net.psforever.objects.zones._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types._
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.objects.serverobject.environment.interaction.RespondsToZoneEnvironment
|
||||
import net.psforever.objects.serverobject.repair.Repairable
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.vital.{HealFromEquipment, RepairFromEquipment}
|
||||
import net.psforever.objects.vital.etc.SuicideReason
|
||||
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
|
||||
|
||||
import java.util.concurrent.{Executors, TimeUnit}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
class AvatarBotActor(bot: AvatarBot, spawnerActor: ActorRef)
|
||||
extends Actor
|
||||
with JammableBehavior
|
||||
with Damageable
|
||||
with AggravatedBehavior
|
||||
with AuraEffectBehavior
|
||||
with RespondsToZoneEnvironment {
|
||||
def JammableObject: AvatarBot = bot
|
||||
|
||||
def DamageableObject: AvatarBot = bot
|
||||
|
||||
def ContainerObject: AvatarBot = bot
|
||||
|
||||
def AggravatedObject: AvatarBot = bot
|
||||
|
||||
def AuraTargetObject: AvatarBot = bot
|
||||
ApplicableEffect(Aura.Plasma)
|
||||
ApplicableEffect(Aura.Napalm)
|
||||
ApplicableEffect(Aura.Comet)
|
||||
ApplicableEffect(Aura.Fire)
|
||||
|
||||
def InteractiveObject: AvatarBot = bot
|
||||
|
||||
private[this] val log = org.log4s.getLogger(bot.Name)
|
||||
private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel)
|
||||
private val scheduler = Executors.newScheduledThreadPool(2)
|
||||
/** suffocating, or regaining breath? */
|
||||
var submergedCondition: Option[OxygenState] = None
|
||||
private var canEmote = false
|
||||
private var canRotate = false
|
||||
|
||||
override def postStop(): Unit = {
|
||||
EndAllEffects()
|
||||
EndAllAggravation()
|
||||
respondToEnvironmentPostStop()
|
||||
scheduler.shutdown()
|
||||
}
|
||||
|
||||
def receive: Receive = Enabled
|
||||
|
||||
def Enabled: Receive =
|
||||
jammableBehavior
|
||||
.orElse(takesDamage)
|
||||
.orElse(aggravatedBehavior)
|
||||
.orElse(auraBehavior)
|
||||
.orElse(environmentBehavior)
|
||||
.orElse {
|
||||
case AvatarBot.Spawn() =>
|
||||
spawn()
|
||||
|
||||
case AvatarBot.Die(Some(reason)) =>
|
||||
dieWithReason(reason)
|
||||
|
||||
case AvatarBot.Die(None) =>
|
||||
suicide()
|
||||
|
||||
case AvatarBot.Release() =>
|
||||
release()
|
||||
|
||||
case CommonMessages.Use(user, Some(item: Tool))
|
||||
if item.Definition == GlobalDefinitions.medicalapplicator && bot.isAlive =>
|
||||
//heal
|
||||
val originalHealth = bot.Health
|
||||
val definition = bot.Definition
|
||||
if (
|
||||
bot.MaxHealth > 0 && originalHealth < bot.MaxHealth &&
|
||||
user.Faction == bot.Faction &&
|
||||
item.Magazine > 0 &&
|
||||
Vector3.Distance(user.Position, bot.Position) < definition.RepairDistance
|
||||
) {
|
||||
val zone = bot.Zone
|
||||
val events = zone.AvatarEvents
|
||||
val uname = user.Name
|
||||
val guid = bot.GUID
|
||||
if (!(bot.isMoving || user.isMoving)) { //only allow stationary heals
|
||||
val newHealth = bot.Health = originalHealth + 10
|
||||
val magazine = item.Discharge()
|
||||
events ! AvatarServiceMessage(
|
||||
uname,
|
||||
AvatarAction.SendResponse(
|
||||
Service.defaultPlayerGUID,
|
||||
InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong)
|
||||
)
|
||||
)
|
||||
events ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttributeToAll(guid, 0, newHealth))
|
||||
bot.LogActivity(
|
||||
HealFromEquipment(
|
||||
PlayerSource(user),
|
||||
GlobalDefinitions.medicalapplicator,
|
||||
newHealth - originalHealth
|
||||
)
|
||||
)
|
||||
}
|
||||
//progress bar remains visible for all heal attempts
|
||||
events ! AvatarServiceMessage(
|
||||
uname,
|
||||
AvatarAction.SendResponse(
|
||||
Service.defaultPlayerGUID,
|
||||
RepairMessage(guid, bot.Health * 100 / definition.MaxHealth)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
case CommonMessages.Use(user, Some(item: Tool)) if item.Definition == GlobalDefinitions.bank =>
|
||||
val originalArmor = bot.Armor
|
||||
val definition = bot.Definition
|
||||
if (
|
||||
bot.MaxArmor > 0 && originalArmor < bot.MaxArmor &&
|
||||
user.Faction == bot.Faction &&
|
||||
item.AmmoType == Ammo.armor_canister && item.Magazine > 0 &&
|
||||
Vector3.Distance(user.Position, bot.Position) < definition.RepairDistance
|
||||
) {
|
||||
val zone = bot.Zone
|
||||
val events = zone.AvatarEvents
|
||||
val uname = user.Name
|
||||
val guid = bot.GUID
|
||||
if (!(bot.isMoving || user.isMoving)) { //only allow stationary repairs
|
||||
val newArmor = bot.Armor =
|
||||
originalArmor + Repairable.applyLevelModifier(user, item, RepairToolValue(item)).toInt + definition.RepairMod
|
||||
val magazine = item.Discharge()
|
||||
events ! AvatarServiceMessage(
|
||||
uname,
|
||||
AvatarAction.SendResponse(
|
||||
Service.defaultPlayerGUID,
|
||||
InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong)
|
||||
)
|
||||
)
|
||||
events ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttributeToAll(guid, 4, bot.Armor))
|
||||
bot.LogActivity(
|
||||
RepairFromEquipment(
|
||||
PlayerSource(user),
|
||||
GlobalDefinitions.bank,
|
||||
newArmor - originalArmor
|
||||
)
|
||||
)
|
||||
}
|
||||
//progress bar remains visible for all repair attempts
|
||||
events ! AvatarServiceMessage(
|
||||
uname,
|
||||
AvatarAction
|
||||
.SendResponse(Service.defaultPlayerGUID, RepairMessage(guid, bot.Armor * 100 / bot.MaxArmor))
|
||||
)
|
||||
}
|
||||
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
def Disabled: Receive = {
|
||||
case AvatarBot.Spawn() =>
|
||||
spawn()
|
||||
|
||||
case AvatarBot.Die(Some(reason)) =>
|
||||
dieWithReason(reason)
|
||||
|
||||
case AvatarBot.Die(None) =>
|
||||
suicide()
|
||||
|
||||
case AvatarBot.Release() =>
|
||||
release()
|
||||
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
override protected def PerformDamage(
|
||||
target: Target,
|
||||
applyDamageTo: Output
|
||||
): Unit = {
|
||||
if (bot.isAlive) {
|
||||
val originalHealth = bot.Health
|
||||
val originalArmor = bot.Armor
|
||||
val originalStamina = bot.stamina
|
||||
val cause = applyDamageTo(bot)
|
||||
val health = bot.Health
|
||||
val armor = bot.Armor
|
||||
val stamina = bot.stamina
|
||||
val damageToHealth = originalHealth - health
|
||||
val damageToArmor = originalArmor - armor
|
||||
val damageToStamina = originalStamina - stamina
|
||||
HandleDamage(bot, cause, damageToHealth, damageToArmor, damageToStamina)
|
||||
if (damageToHealth > 0 || damageToArmor > 0 || damageToStamina > 0) {
|
||||
damageLog.info(
|
||||
s"${bot.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalStamina, AFTER=$health/$armor/$stamina, CHANGE=$damageToHealth/$damageToArmor/$damageToStamina"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param target na
|
||||
*/
|
||||
def HandleDamage(
|
||||
target: AvatarBot,
|
||||
cause: DamageResult,
|
||||
damageToHealth: Int,
|
||||
damageToArmor: Int,
|
||||
damageToStamina: Int
|
||||
): Unit = {
|
||||
//always do armor update
|
||||
if (damageToArmor > 0) {
|
||||
val zone = target.Zone
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.PlanetsideAttributeToAll(target.GUID, 4, target.Armor)
|
||||
)
|
||||
}
|
||||
//choose
|
||||
if (target.Health > 0) {
|
||||
//alive, take damage/update
|
||||
DamageAwareness(target, cause, damageToHealth, damageToArmor, damageToStamina)
|
||||
} else {
|
||||
//ded
|
||||
DestructionAwareness(target, cause)
|
||||
}
|
||||
}
|
||||
|
||||
def DamageAwareness(
|
||||
target: AvatarBot,
|
||||
cause: DamageResult,
|
||||
damageToHealth: Int,
|
||||
damageToArmor: Int,
|
||||
damageToStamina: Int
|
||||
): Unit = {
|
||||
val targetGUID = target.GUID
|
||||
val zone = target.Zone
|
||||
val zoneId = zone.id
|
||||
val events = zone.AvatarEvents
|
||||
val health = target.Health
|
||||
var announceConfrontation = damageToArmor > 0
|
||||
//special effects
|
||||
if (Damageable.CanJammer(target, cause.interaction)) {
|
||||
TryJammerEffectActivate(target, cause)
|
||||
}
|
||||
val aggravated: Boolean = TryAggravationEffectActivate(cause) match {
|
||||
case Some(aggravation) =>
|
||||
StartAuraEffect(aggravation.effect_type, aggravation.timing.duration)
|
||||
announceConfrontation = true //useful if initial damage (to anything) is zero
|
||||
//initial damage for aggravation, but never treat as "aggravated"
|
||||
false
|
||||
case _ =>
|
||||
cause.interaction.cause.source.Aggravated.nonEmpty
|
||||
}
|
||||
//log historical event (always)
|
||||
target.LogActivity(cause)
|
||||
//stat changes
|
||||
if (damageToStamina > 0) {
|
||||
target.stamina = math.max(0, target.stamina - damageToStamina)
|
||||
announceConfrontation = true //TODO should we?
|
||||
}
|
||||
if (damageToHealth > 0) {
|
||||
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 0, health))
|
||||
announceConfrontation = true
|
||||
}
|
||||
val countableDamage = damageToHealth + damageToArmor
|
||||
if(announceConfrontation) {
|
||||
if (aggravated) {
|
||||
events ! AvatarServiceMessage(
|
||||
zoneId,
|
||||
AvatarAction.SendResponse(targetGUID, AggravatedDamageMessage(targetGUID, countableDamage))
|
||||
)
|
||||
} else {
|
||||
//activity on map
|
||||
zone.Activity ! Zone.HotSpot.Activity(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The bot has lost all his vitality and must be killed.<br>
|
||||
* <br>
|
||||
* Shift directly into a state of being dead on the client by setting health to zero points,
|
||||
* whereupon the bot will perform a dramatic death animation.
|
||||
* Stamina is also set to zero points.
|
||||
* Do not move or completely destroy the `AvatarBot` object as its coordinates of death will be important.<br>
|
||||
* <br>
|
||||
* @param target na
|
||||
* @param cause na
|
||||
*/
|
||||
def DestructionAwareness(target: AvatarBot, cause: DamageResult): Unit = {
|
||||
val bot_guid = target.GUID
|
||||
val pos = target.Position
|
||||
val zone = target.Zone
|
||||
val events = zone.AvatarEvents
|
||||
val nameChannel = target.Name
|
||||
val zoneChannel = zone.id
|
||||
target.Die
|
||||
//aura effects cancel
|
||||
EndAllEffects()
|
||||
//aggravation cancel
|
||||
EndAllAggravation()
|
||||
//unjam
|
||||
CancelJammeredSound(target)
|
||||
super.CancelJammeredStatus(target)
|
||||
//no stamina
|
||||
target.stamina = 0
|
||||
|
||||
//log historical event
|
||||
target.LogActivity(cause)
|
||||
//log message
|
||||
cause.adversarial match {
|
||||
case Some(a) =>
|
||||
damageLog.info(s"${a.defender.Name} was killed by ${a.attacker.Name}")
|
||||
events ! AvatarServiceMessage(
|
||||
zoneChannel,
|
||||
AvatarAction.DestroyDisplay(a.attacker, a.defender, a.implement)
|
||||
)
|
||||
case _ =>
|
||||
damageLog.info(s"${bot.Name} killed ${bot.Sex.pronounObject}self")
|
||||
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(cause.interaction.target, cause.interaction.target, 0))
|
||||
}
|
||||
|
||||
events ! AvatarServiceMessage(nameChannel, AvatarAction.Killed(bot_guid, cause, None)) //align client interface fields with state
|
||||
events ! AvatarServiceMessage(zoneChannel, AvatarAction.PlanetsideAttributeToAll(bot_guid, 0, 0)) //health
|
||||
val attribute = DamageableEntity.attributionTo(cause, target.Zone, bot_guid)
|
||||
events ! AvatarServiceMessage(
|
||||
nameChannel,
|
||||
AvatarAction.SendResponse(
|
||||
bot_guid,
|
||||
DestroyMessage(bot_guid, attribute, bot_guid, pos)
|
||||
) //how many players get this message?
|
||||
)
|
||||
|
||||
context.self ! AvatarBot.Release()
|
||||
}
|
||||
|
||||
def suicide() : Unit = {
|
||||
if (bot.Health > 0 || bot.isAlive) {
|
||||
PerformDamage(
|
||||
bot,
|
||||
DamageInteraction(
|
||||
PlayerSource(bot),
|
||||
SuicideReason(),
|
||||
bot.Position
|
||||
).calculate()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def spawn(): Unit = {
|
||||
bot.Spawn()
|
||||
bot.Zone.Population ! Zone.Bots.Spawn(bot)
|
||||
canEmote = true
|
||||
canRotate = true
|
||||
scheduler.scheduleAtFixedRate(new Runnable() { override def run(): Unit = tickLogic() }, 0, 250, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
|
||||
private def dieWithReason(reason: DamageInteraction): Unit = {
|
||||
if (bot.isAlive) {
|
||||
//primary death
|
||||
val health = bot.Health
|
||||
val psource = PlayerSource(bot)
|
||||
bot.Health = 0
|
||||
HandleDamage(
|
||||
bot,
|
||||
DamageResult(psource, psource.copy(health = 0), reason),
|
||||
health,
|
||||
damageToArmor = 0,
|
||||
damageToStamina = 0
|
||||
)
|
||||
damageLog.info(s"${bot.Name}-infantry: dead by explicit reason - ${reason.cause.resolution}")
|
||||
}
|
||||
}
|
||||
|
||||
private def release(): Unit = {
|
||||
bot.Zone.Population ! Zone.Bots.Release(bot)
|
||||
spawnerActor ! ShootingRangeTargetSpawner.InfantryTargetReleased(bot)
|
||||
scheduler.shutdown()
|
||||
}
|
||||
|
||||
private def performEmote(): Unit = {
|
||||
val zone = bot.Zone
|
||||
zone.blockMap.sector(bot).livePlayerList.collect { t =>
|
||||
zone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(TriggerBotAction(bot.GUID)))
|
||||
}
|
||||
}
|
||||
|
||||
private def tickLogic(): Unit = {
|
||||
val zone = bot.Zone
|
||||
if (!bot.Destroyed && zone.AllPlayers.size > 0) {
|
||||
bot.zoneInteractions()
|
||||
val rotateRNG = Random.nextDouble()
|
||||
if (canRotate) {
|
||||
if (rotateRNG > 0.95) {
|
||||
val amount = 5f + Random.nextInt(10)
|
||||
val finalRotation = if (Random.nextBoolean()) -amount else amount
|
||||
bot.Orientation = Vector3(bot.Orientation.x, bot.Orientation.y, (bot.Orientation.z + finalRotation) % 360)
|
||||
canRotate = false
|
||||
//rotation cooldown
|
||||
context.system.scheduler.scheduleOnce(
|
||||
2.seconds,
|
||||
new Runnable() { override def run(): Unit = if (!bot.Destroyed) canRotate = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.PlayerState(
|
||||
bot.GUID,
|
||||
bot.Position,
|
||||
bot.Velocity,
|
||||
bot.Orientation.z,
|
||||
bot.Orientation.y,
|
||||
bot.FacingYawUpper,
|
||||
0,
|
||||
bot.Crouching,
|
||||
bot.Jumping,
|
||||
false,
|
||||
bot.Cloaked,
|
||||
false,
|
||||
false
|
||||
)
|
||||
)
|
||||
if (canEmote) {
|
||||
val emoteRNG = Random.nextDouble()
|
||||
if (emoteRNG > 0.98) {
|
||||
performEmote()
|
||||
canEmote = false
|
||||
//emote cooldown
|
||||
context.system.scheduler.scheduleOnce(
|
||||
5.seconds,
|
||||
new Runnable() { override def run(): Unit = if (!bot.Destroyed) canEmote = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the jammered buzzing.
|
||||
* Although, as a rule, the jammering sound effect should last as long as the jammering status,
|
||||
* Infantry seem to hear the sound for a bit longer than the effect.
|
||||
* @see `JammableBehavior.StartJammeredSound`
|
||||
* @param target an object that can be affected by the jammered status
|
||||
* @param dur the duration of the timer, in milliseconds;
|
||||
* by default, 30000
|
||||
*/
|
||||
override def StartJammeredSound(target: Any, dur: Int): Unit =
|
||||
target match {
|
||||
case obj: AvatarBot if !jammedSound =>
|
||||
obj.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||
obj.Zone.id,
|
||||
AvatarAction.PlanetsideAttributeToAll(obj.GUID, 27, 1)
|
||||
)
|
||||
super.StartJammeredSound(obj, 3000)
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a variety of tasks to indicate being jammered.
|
||||
* Deactivate implants (should also uninitialize them),
|
||||
* delay stamina regeneration for a certain number of turns,
|
||||
* and set the jammered status on specific holstered equipment.
|
||||
* @see `JammableBehavior.StartJammeredStatus`
|
||||
* @param target an object that can be affected by the jammered status
|
||||
* @param dur the duration of the timer, in milliseconds
|
||||
*/
|
||||
override def StartJammeredStatus(target: Any, dur: Int): Unit = {
|
||||
super.StartJammeredStatus(target, dur)
|
||||
}
|
||||
|
||||
override def CancelJammeredStatus(target: Any): Unit = {
|
||||
super.CancelJammeredStatus(target)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the jammered buzzing.
|
||||
* @see `JammableBehavior.CancelJammeredSound`
|
||||
* @param target an object that can be affected by the jammered status
|
||||
*/
|
||||
override def CancelJammeredSound(target: Any): Unit =
|
||||
target match {
|
||||
case obj: AvatarBot if jammedSound =>
|
||||
obj.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||
obj.Zone.id,
|
||||
AvatarAction.PlanetsideAttributeToAll(obj.GUID, 27, 0)
|
||||
)
|
||||
super.CancelJammeredSound(obj)
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
def RepairToolValue(item: Tool): Float = {
|
||||
item.AmmoSlot.Box.Definition.repairAmount +
|
||||
(if (bot.ExoSuit != ExoSuitType.MAX) {
|
||||
item.FireMode.Add.Damage0
|
||||
}
|
||||
else {
|
||||
item.FireMode.Add.Damage3
|
||||
})
|
||||
}
|
||||
|
||||
def UpdateAuraEffect(target: AuraEffectBehavior.Target) : Unit = {
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
val zone = target.Zone
|
||||
val value = target.Aura.foldLeft(0)(_ + AvatarBotActor.auraEffectToAttributeValue(_))
|
||||
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttributeToAll(target.GUID, 54, value))
|
||||
}
|
||||
}
|
||||
|
||||
object AvatarBotActor {
|
||||
/**
|
||||
* Transform an applicable Aura effect into its `PlanetsideAttributeMessage` value.
|
||||
* @see `Aura`
|
||||
* @see `PlanetsideAttributeMessage`
|
||||
* @param effect the aura effect
|
||||
* @return the attribute value for that effect
|
||||
*/
|
||||
private def auraEffectToAttributeValue(effect: Aura): Int = effect match {
|
||||
case Aura.Plasma => 1
|
||||
case Aura.Comet => 2
|
||||
case Aura.Napalm => 4
|
||||
case Aura.Fire => 8
|
||||
case _ => 0
|
||||
}
|
||||
}
|
||||
|
|
@ -417,11 +417,15 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
|
|||
}
|
||||
val hasCavernEquipmentBenefit: Boolean =
|
||||
terminalOpt.exists { terminal =>
|
||||
terminal.Owner match {
|
||||
case fac: Building =>
|
||||
fac.hasCavernLockBenefit && player.Faction == fac.Faction
|
||||
case _ =>
|
||||
false
|
||||
if (terminal.IsInVRZone) {
|
||||
true
|
||||
} else {
|
||||
terminal.Owner match {
|
||||
case fac: Building =>
|
||||
fac.hasCavernLockBenefit && player.Faction == fac.Faction
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
val dropPred =
|
||||
|
|
|
|||
|
|
@ -27,17 +27,28 @@ class InteractWithRadiationClouds(
|
|||
val position = target.Position
|
||||
projectiles
|
||||
.foreach { projectile =>
|
||||
target.Actor ! Vitality.Damage(
|
||||
DamageInteraction(
|
||||
SourceEntry(target),
|
||||
RadiationReason(
|
||||
ProjectileQuality.modifiers(projectile, DamageResolution.Radiation, target, target.Position, user),
|
||||
target.DamageModel,
|
||||
RadiationCloudInteraction.RadiationShieldingFrom(target)
|
||||
),
|
||||
position
|
||||
).calculate()
|
||||
)
|
||||
val shouldDamage = user match {
|
||||
case Some(player) if (player.IsInVRZone && target.Faction == player.Faction) =>
|
||||
//disable self-damage and friendly-fire in VR zones
|
||||
false
|
||||
case Some(player) =>
|
||||
true
|
||||
case None =>
|
||||
true
|
||||
}
|
||||
if (shouldDamage) {
|
||||
target.Actor ! Vitality.Damage(
|
||||
DamageInteraction(
|
||||
SourceEntry(target),
|
||||
RadiationReason(
|
||||
ProjectileQuality.modifiers(projectile, DamageResolution.Radiation, target, target.Position, user),
|
||||
target.DamageModel,
|
||||
RadiationCloudInteraction.RadiationShieldingFrom(target)
|
||||
),
|
||||
position
|
||||
).calculate()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) 2026 PSForever
|
||||
package net.psforever.objects.definition
|
||||
|
||||
import net.psforever.objects.avatar.Avatars
|
||||
import net.psforever.objects.definition.converter.AvatarBotConverter
|
||||
import net.psforever.objects.geometry.GeometryForm
|
||||
import net.psforever.objects.vital.VitalityDefinition
|
||||
|
||||
/**
|
||||
* The definition for game objects that look like players.
|
||||
* @param objectId the object type number
|
||||
*/
|
||||
class AvatarBotDefinition(objectId: Int) extends ObjectDefinition(objectId) with VitalityDefinition {
|
||||
Name = "avatar_bot"
|
||||
Avatars(objectId) //let throw NoSuchElementException
|
||||
Packet = AvatarBotDefinition.converter
|
||||
Geometry = GeometryForm.representPlayerByCylinder(radius = 1.6f)
|
||||
//do NOT attempt to create AvatarBot's outside of the VR Shooting Range zones with this definition,
|
||||
//space will need to be made to add a "bots" pool to the target zone
|
||||
registerAs = "bots"
|
||||
}
|
||||
|
||||
object AvatarBotDefinition {
|
||||
private val converter = new AvatarBotConverter()
|
||||
|
||||
def apply(objectId: Int): AvatarBotDefinition = {
|
||||
new AvatarBotDefinition(objectId)
|
||||
}
|
||||
|
||||
def apply(avatar: Avatars.Value): AvatarBotDefinition = {
|
||||
new AvatarBotDefinition(avatar.id)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
// Copyright (c) 2026 PSForever
|
||||
package net.psforever.objects.definition.converter
|
||||
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.avatar.{AvatarBot, BattleRank}
|
||||
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
|
||||
import net.psforever.packet.game.objectcreate._
|
||||
import net.psforever.types.{ExoSuitType, GrenadeState, PlanetSideEmpire, PlanetSideGUID}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.{Success, Try}
|
||||
|
||||
class AvatarBotConverter extends ObjectCreateConverter[AvatarBot]() {
|
||||
override def ConstructorData(obj: AvatarBot): Try[PlayerData] = {
|
||||
import AvatarBotConverter._
|
||||
Success(
|
||||
PlayerData(
|
||||
PlacementData(obj.Position, obj.Orientation, None),
|
||||
MakeAppearanceData(obj),
|
||||
MakeCharacterData(obj),
|
||||
MakeInventoryData(obj),
|
||||
GetDrawnSlot(obj)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override def DetailedConstructorData(obj: AvatarBot): Try[DetailedPlayerData] = {
|
||||
import AvatarBotConverter._
|
||||
Success(
|
||||
DetailedPlayerData.apply(
|
||||
PlacementData(obj.Position, obj.Orientation, None),
|
||||
MakeAppearanceData(obj),
|
||||
MakeDetailedCharacterData(obj),
|
||||
MakeDetailedInventoryData(obj),
|
||||
GetDrawnSlot(obj)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object AvatarBotConverter {
|
||||
|
||||
/**
|
||||
* Compose some data from a `AvatarBot` into a representation common to both `CharacterData` and `DetailedCharacterData`.
|
||||
* @param obj the `AvatarBot` game object
|
||||
* @return the resulting `CharacterAppearanceData`
|
||||
*/
|
||||
def MakeAppearanceData(obj: AvatarBot): Int => CharacterAppearanceData = {
|
||||
val aa: Int => CharacterAppearanceA = CharacterAppearanceA(
|
||||
obj.basic,
|
||||
CommonFieldData(
|
||||
obj.Faction,
|
||||
bops = false,
|
||||
false,
|
||||
v1 = false,
|
||||
None,
|
||||
obj.Jammed,
|
||||
None,
|
||||
v5 = None,
|
||||
PlanetSideGUID(0)
|
||||
),
|
||||
obj.ExoSuit,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
val ab: (Boolean, Int) => CharacterAppearanceB = CharacterAppearanceB(
|
||||
0,
|
||||
"",
|
||||
outfit_logo = 0,
|
||||
unk1 = false,
|
||||
false,
|
||||
unk2 = false,
|
||||
unk3 = false,
|
||||
unk4 = false,
|
||||
facingPitch = obj.Orientation.y,
|
||||
facingYawUpper = obj.FacingYawUpper,
|
||||
false,
|
||||
GrenadeState.None,
|
||||
obj.Cloaked,
|
||||
unk5 = false,
|
||||
unk6 = false,
|
||||
charging_pose = false,
|
||||
unk7 = false,
|
||||
on_zipline = None
|
||||
)
|
||||
CharacterAppearanceData(aa, ab, obj.decoration.ribbonBars)
|
||||
}
|
||||
|
||||
def MakeCharacterData(obj: AvatarBot): (Boolean, Boolean) => CharacterData = {
|
||||
val uniformStyle = obj.br.uniformStyle
|
||||
val cosmetics = if (BattleRank.showCosmetics(uniformStyle)) {
|
||||
obj.decoration.cosmetics
|
||||
} else {
|
||||
None
|
||||
}
|
||||
val MaxArmor = obj.MaxArmor
|
||||
val armor = if (MaxArmor == 0) {
|
||||
0
|
||||
} else {
|
||||
StatConverter.Health(obj.Armor, MaxArmor)
|
||||
}
|
||||
CharacterData(
|
||||
StatConverter.Health(obj.Health, obj.MaxHealth),
|
||||
armor,
|
||||
uniformStyle,
|
||||
0,
|
||||
obj.cr.value,
|
||||
obj.implants.flatten.filter(_.active).flatMap(_.definition.implantType.effect).toList,
|
||||
cosmetics
|
||||
)
|
||||
}
|
||||
|
||||
def MakeDetailedCharacterData(obj: AvatarBot): Option[Int] => DetailedCharacterData = {
|
||||
val maxOpt: Option[Long] = if (obj.ExoSuit == ExoSuitType.MAX) {
|
||||
Some(0L)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
val cosmetics = if (BattleRank.BR24.experience >= obj.bep) {
|
||||
obj.decoration.cosmetics
|
||||
} else {
|
||||
None
|
||||
}
|
||||
val ba: DetailedCharacterA = DetailedCharacterA(
|
||||
obj.bep,
|
||||
obj.cep,
|
||||
0L,
|
||||
0L,
|
||||
0L,
|
||||
obj.MaxHealth,
|
||||
obj.Health,
|
||||
unk4 = false,
|
||||
obj.Armor,
|
||||
0L,
|
||||
obj.maxStamina,
|
||||
obj.stamina,
|
||||
maxOpt,
|
||||
0,
|
||||
0,
|
||||
0L,
|
||||
List(0, 0, 0, 0, 0, 0),
|
||||
obj.certifications.toList.sortBy(_.value) //TODO is sorting necessary?
|
||||
)
|
||||
val bb: (Long, Option[Int]) => DetailedCharacterB = DetailedCharacterB(
|
||||
None,
|
||||
Nil,
|
||||
Nil,
|
||||
Nil,
|
||||
Nil,
|
||||
tutorials = List.empty[String], //TODO tutorial list
|
||||
0L,
|
||||
0L,
|
||||
0L,
|
||||
0L,
|
||||
0L,
|
||||
None, //Some(ImprintingProgress(0, 0)),
|
||||
Nil,
|
||||
Nil,
|
||||
unkC = false,
|
||||
cosmetics
|
||||
)
|
||||
pad_length: Option[Int] => DetailedCharacterData(ba, bb(0, pad_length))(pad_length)
|
||||
}
|
||||
|
||||
def MakeInventoryData(obj: AvatarBot): InventoryData = {
|
||||
InventoryData(MakeHolsters(obj, BuildEquipment))
|
||||
}
|
||||
|
||||
def MakeDetailedInventoryData(obj: AvatarBot): InventoryData = {
|
||||
InventoryData(
|
||||
MakeHolsters(obj, BuildDetailedEquipment) ++
|
||||
MakeFifthSlot(obj) ++
|
||||
MakeInventory(obj)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a player with an inventory, convert the contents of that inventory into converted-decoded packet data.
|
||||
* The inventory is not represented in a `0x17` `AvatarBot`, so the conversion is only valid for `0x18` avatars.
|
||||
* It will always be "`Detailed`".
|
||||
* @param obj the `AvatarBot` game object
|
||||
* @return a list of all items that were in the inventory in decoded packet form
|
||||
*/
|
||||
private def MakeInventory(obj: AvatarBot): List[InternalSlot] = {
|
||||
obj.Inventory.Items
|
||||
.map(item => {
|
||||
val equip: Equipment = item.obj
|
||||
InternalSlot(
|
||||
equip.Definition.ObjectId,
|
||||
equip.GUID,
|
||||
item.start,
|
||||
equip.Definition.Packet.DetailedConstructorData(equip).get
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a player with equipment holsters, convert the contents of those holsters into converted-decoded packet data.
|
||||
* The decoded packet form is determined by the function in the parameters as both `0x17` and `0x18` conversions are available,
|
||||
* with exception to the contents of the fifth slot.
|
||||
* The fifth slot is only represented if the `AvatarBot` is an `0x18` type.
|
||||
* @param obj the `AvatarBot` game object
|
||||
* @param builder the function used to transform to the decoded packet form
|
||||
* @return a list of all items that were in the holsters in decoded packet form
|
||||
*/
|
||||
def MakeHolsters(obj: AvatarBot, builder: (Int, Equipment) => InternalSlot): List[InternalSlot] = {
|
||||
recursiveMakeHolsters(obj.Holsters().iterator, builder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a player with equipment holsters, convert any content of the fifth holster slot into converted-decoded packet data.
|
||||
* The fifth holster is a curious divider between the standard holsters and the formal inventory.
|
||||
* This fifth slot is only ever represented if the `AvatarBot` is an `0x18` type.
|
||||
* @param obj the `AvatarBot` game object
|
||||
* @return a list of any item that was in the fifth holster in decoded packet form
|
||||
*/
|
||||
private def MakeFifthSlot(obj: AvatarBot): List[InternalSlot] = {
|
||||
obj.Slot(slot = 5).Equipment match {
|
||||
case Some(equip) =>
|
||||
List(InternalSlot(
|
||||
equip.Definition.ObjectId,
|
||||
equip.GUID,
|
||||
5,
|
||||
DetailedLockerContainerData(
|
||||
CommonFieldData(PlanetSideEmpire.NEUTRAL, bops=false, alternate=false, v1=true, None, jammered=false, None, None, PlanetSideGUID(0)),
|
||||
None
|
||||
)
|
||||
))
|
||||
case _ =>
|
||||
Nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder method for turning an object into `0x17` decoded packet form.
|
||||
* @param index the position of the object
|
||||
* @param equip the game object
|
||||
* @return the game object in decoded packet form
|
||||
*/
|
||||
private def BuildEquipment(index: Int, equip: Equipment): InternalSlot = {
|
||||
InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.ConstructorData(equip).get)
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder method for turning an object into `0x18` decoded packet form.
|
||||
* @param index the position of the object
|
||||
* @param equip the game object
|
||||
* @return the game object in decoded packet form
|
||||
*/
|
||||
def BuildDetailedEquipment(index: Int, equip: Equipment): InternalSlot = {
|
||||
InternalSlot(
|
||||
equip.Definition.ObjectId,
|
||||
equip.GUID,
|
||||
index,
|
||||
equip.Definition.Packet.DetailedConstructorData(equip).get
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given some equipment holsters, convert the contents of those holsters into converted-decoded packet data.
|
||||
* @param iter an `Iterator` of `EquipmentSlot` objects that are a part of the player's holsters
|
||||
* @param builder the function used to transform to the decoded packet form
|
||||
* @param list the current `List` of transformed data
|
||||
* @param index which holster is currently being explored
|
||||
* @return the `List` of inventory data created from the holsters
|
||||
*/
|
||||
@tailrec private def recursiveMakeHolsters(
|
||||
iter: Iterator[EquipmentSlot],
|
||||
builder: (Int, Equipment) => InternalSlot,
|
||||
list: List[InternalSlot] = Nil,
|
||||
index: Int = 0
|
||||
): List[InternalSlot] = {
|
||||
if (!iter.hasNext) {
|
||||
list
|
||||
} else {
|
||||
val slot: EquipmentSlot = iter.next()
|
||||
if (slot.Equipment.isDefined) {
|
||||
val equip: Equipment = slot.Equipment.get
|
||||
recursiveMakeHolsters(
|
||||
iter,
|
||||
builder,
|
||||
list :+ builder(index, equip),
|
||||
index + 1
|
||||
)
|
||||
} else {
|
||||
recursiveMakeHolsters(iter, builder, list, index + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve which holster the player has drawn, if any.
|
||||
* @param obj the `AvatarBot` game object
|
||||
* @return the holster's Enumeration value
|
||||
*/
|
||||
def GetDrawnSlot(obj: AvatarBot): DrawnSlot.Value = {
|
||||
obj.DrawnSlot match {
|
||||
case Player.HandsDownSlot | Player.FreeHandSlot => DrawnSlot.None
|
||||
case n => DrawnSlot(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.objects.equipment
|
||||
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.avatar.AvatarBot
|
||||
import net.psforever.objects.ce.{DeployableCategory, DeployedItem}
|
||||
import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
|
||||
import net.psforever.objects.vital.{DamagingActivity, InGameHistory, Vitality}
|
||||
|
|
@ -111,6 +112,8 @@ object EffectTarget {
|
|||
target match {
|
||||
case p: Player =>
|
||||
p.isAlive
|
||||
case b: AvatarBot =>
|
||||
b.isAlive
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2021 PSForever
|
||||
package net.psforever.objects.geometry
|
||||
|
||||
import net.psforever.objects.avatar.AvatarBot
|
||||
import net.psforever.objects.ballistics.Projectile
|
||||
import net.psforever.objects.geometry.d3._
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||
|
|
@ -152,6 +153,13 @@ object GeometryForm {
|
|||
radius + radialOffset,
|
||||
heightOffset
|
||||
)
|
||||
case b: AvatarBot =>
|
||||
val radialOffset = if(b.ExoSuit == ExoSuitType.MAX) 0.25f else 0f
|
||||
Cylinder(
|
||||
b.Position,
|
||||
radius + radialOffset,
|
||||
GlobalDefinitions.MaxDepth(b)
|
||||
)
|
||||
case _ =>
|
||||
invalidCylinder
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.objects.guid
|
||||
|
||||
import akka.util.Timeout
|
||||
import net.psforever.objects.avatar.AvatarBot
|
||||
import net.psforever.objects.entity.IdentifiableEntity
|
||||
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
|
||||
import net.psforever.objects._
|
||||
|
|
@ -203,6 +204,19 @@ object GUIDTask {
|
|||
TaskBundle(RegisterObjectTask(guid, tplayer), holsterTasks ++ inventoryTasks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct tasking that registers an object with a globally unique identifier selected from a pool of numbers, as an `AvatarBot`.<br>
|
||||
* <br>
|
||||
* @param bot the `AvatarBot` object being registered
|
||||
* @param guid implicit reference to a unique number system
|
||||
* @return a `TaskBundle` message
|
||||
*/
|
||||
def registerBot(guid: UniqueNumberOps, bot: AvatarBot): TaskBundle = {
|
||||
val holsterTasks = visibleSlotTaskBuilding(guid, bot.Holsters(), registerEquipment)
|
||||
val inventoryTasks = registerInventory(guid, bot)
|
||||
TaskBundle(RegisterObjectTask(guid, bot), holsterTasks ++ inventoryTasks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct tasking that registers an object with a globally unique identifier selected from a pool of numbers, as a `Vehicle`.<br>
|
||||
* <br>
|
||||
|
|
@ -370,6 +384,21 @@ object GUIDTask {
|
|||
TaskBundle(UnregisterObjectTask(guid, tplayer), holsterTasks ++ inventoryTasks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct tasking that unregisters a `AvatarBot` object from a globally unique identifier system.<br>
|
||||
* <br>
|
||||
* This task performs an operation that reverses the effect of `RegisterBot`.
|
||||
* @param bot the `AvatarBot` object being unregistered
|
||||
* @param guid implicit reference to a unique number system
|
||||
* @see `GUIDTask.registerAvatar`
|
||||
* @return a `TaskBundle` message
|
||||
*/
|
||||
def unregisterBot(guid: UniqueNumberOps, bot: AvatarBot): TaskBundle = {
|
||||
val holsterTasks = visibleSlotTaskBuilding(guid, bot.Holsters(), unregisterEquipment)
|
||||
val inventoryTasks = unregisterInventory(guid, bot)
|
||||
TaskBundle(UnregisterObjectTask(guid, bot), holsterTasks ++ inventoryTasks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct tasking that unregisters a `Vehicle` object from a globally unique identifier system.<br>
|
||||
* <br>
|
||||
|
|
|
|||
|
|
@ -67,13 +67,14 @@ case object NoCavernEquipmentRule extends ExclusionRule {
|
|||
/**
|
||||
* Do not allow the player to spawn cavern equipment if not pulled from a facility and
|
||||
* only if the facility is subject to the benefit of an appropriate cavern perk.
|
||||
* This is bypassed for VR Training terminals.
|
||||
*/
|
||||
case object CavernEquipmentQuestion extends ExclusionRule {
|
||||
def checkRule(player: Player, msg: ItemTransactionMessage, obj: Any): Boolean = {
|
||||
obj match {
|
||||
case equipment: Equipment =>
|
||||
import net.psforever.objects.serverobject.structures.Building
|
||||
if(GlobalDefinitions.isCavernWeapon(equipment.Definition)) {
|
||||
if(GlobalDefinitions.isCavernWeapon(equipment.Definition) && !player.IsInVRZone) {
|
||||
(player.Zone.GUID(msg.terminal_guid) match {
|
||||
case Some(term: Amenity) => Some(term.Owner)
|
||||
case _ => None
|
||||
|
|
@ -106,6 +107,7 @@ final case class NoVehicleRule(illegalDefinition: VehicleDefinition) extends Exc
|
|||
/**
|
||||
* Do not allow the player to spawn cavern vehicles if not pulled from a facility and
|
||||
* only if the facility is subject to the benefit of an appropriate cavern perk.
|
||||
* This is bypassed for VR Training terminals.
|
||||
*/
|
||||
case object CavernVehicleQuestion extends ExclusionRule {
|
||||
def checkRule(player: Player, msg: ItemTransactionMessage, obj: Any): Boolean = {
|
||||
|
|
@ -113,7 +115,7 @@ case object CavernVehicleQuestion extends ExclusionRule {
|
|||
case vehicle: Vehicle =>
|
||||
import net.psforever.objects.serverobject.structures.Building
|
||||
val definition = vehicle.Definition
|
||||
if (definition == GlobalDefinitions.flail || definition == GlobalDefinitions.switchblade) {
|
||||
if ((definition == GlobalDefinitions.flail || definition == GlobalDefinitions.switchblade) && !player.IsInVRZone) {
|
||||
(player.Zone.GUID(msg.terminal_guid) match {
|
||||
case Some(term: Amenity) => Some(term.Owner)
|
||||
case _ => None
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ final case class VehiclePage(stock: Map[String, () => Vehicle], trunk: Map[Strin
|
|||
stock.get(msg.item_name) match {
|
||||
case Some(vehicle) =>
|
||||
val createdVehicle = vehicle()
|
||||
if(!Exclude.exists(_.checkRule(player, msg, createdVehicle)) || player.IsInVRZone) {
|
||||
if(!Exclude.exists(_.checkRule(player, msg, createdVehicle))) {
|
||||
val (weapons, inventory) = trunk.get(msg.item_name) match {
|
||||
case Some(loadout: VehicleLoadout) =>
|
||||
(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.sourcing
|
||||
|
||||
import net.psforever.objects.avatar.AvatarBot
|
||||
import net.psforever.objects.avatar.scoring.Life
|
||||
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
|
|
@ -72,6 +73,27 @@ object PlayerSource {
|
|||
)
|
||||
}
|
||||
|
||||
def apply(b: AvatarBot): PlayerSource = {
|
||||
val exosuit = b.ExoSuit
|
||||
val faction = b.Faction
|
||||
PlayerSource(
|
||||
GlobalDefinitions.avatar,
|
||||
exosuit,
|
||||
seatedIn = None,
|
||||
b.Health,
|
||||
b.Armor,
|
||||
b.Position,
|
||||
b.Orientation,
|
||||
b.Velocity,
|
||||
b.Crouching,
|
||||
b.Jumping,
|
||||
ExoSuitDefinition.Select(exosuit, faction),
|
||||
b.bep,
|
||||
progress = tokenLife,
|
||||
UniquePlayer(0L, b.Name, b.Sex, faction)
|
||||
)
|
||||
}
|
||||
|
||||
def apply(name: String, faction: PlanetSideEmpire.Value, position: Vector3): PlayerSource = {
|
||||
this(UniquePlayer(0L, name, CharacterSex.Male, faction), position)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.sourcing
|
||||
|
||||
import net.psforever.objects.avatar.AvatarBot
|
||||
import net.psforever.objects.ce.Deployable
|
||||
import net.psforever.objects.definition.ObjectDefinition
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
|
|
@ -49,6 +50,7 @@ object SourceEntry {
|
|||
def apply(target: PlanetSideGameObject with FactionAffinity): SourceEntry = {
|
||||
target match {
|
||||
case obj: Player => PlayerSource(obj)
|
||||
case obj: AvatarBot => PlayerSource(obj)
|
||||
case obj: Vehicle => VehicleSource(obj)
|
||||
case obj: FacilityTurret => TurretSource(obj)
|
||||
case obj: Amenity => AmenitySource(obj)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package net.psforever.objects.vehicles.control
|
||||
|
||||
import akka.actor.Cancellable
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.actors.zone.{ShootingRangeTargetSpawner, ZoneActor}
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.avatar.SpecialCarry
|
||||
import net.psforever.objects.definition.{VehicleDefinition, WithShields}
|
||||
|
|
@ -266,6 +266,15 @@ class VehicleControl(vehicle: Vehicle)
|
|||
PrepareForDisabled(kickPassengers)
|
||||
context.become(Disabled)
|
||||
|
||||
case Vehicle.Deconstruct(_) if (vehicle.Zone.id.startsWith("tzsh") && vehicle.OwnerGuid.isEmpty) =>
|
||||
//deconstruct the vehicle immediately if this is a VR Shooting Range target
|
||||
context.become(ReadyToDelete)
|
||||
//cancel jammed behavior
|
||||
CancelJammeredSound(vehicle)
|
||||
CancelJammeredStatus(vehicle)
|
||||
self ! VehicleControl.Deletion()
|
||||
vehicle.ClearHistory()
|
||||
|
||||
case Vehicle.Deconstruct(time) =>
|
||||
time match {
|
||||
case Some(delay) if vehicle.Definition.undergoesDecay =>
|
||||
|
|
@ -326,6 +335,13 @@ class VehicleControl(vehicle: Vehicle)
|
|||
VehicleAction.UnloadVehicle(Service.defaultPlayerGUID, vehicle, vehicle.GUID)
|
||||
)
|
||||
zone.Transport.tell(Zone.Vehicle.Despawn(vehicle), zone.Transport)
|
||||
//notify target spawner that the vehicle has despawned if this is a VR Shooting Range zone
|
||||
//todo: make this behavior cleaner
|
||||
if (zone.id.startsWith("tzsh") && vehicle.OwnerGuid.isEmpty && zone.NPCPopulation != Default.Actor) {
|
||||
zone.NPCPopulation.tell(ShootingRangeTargetSpawner.VehicleTargetDeconstructed(vehicle), zone.NPCPopulation)
|
||||
}
|
||||
//unregister
|
||||
TaskWorkflow.execute(GUIDTask.unregisterVehicle(zone.GUID, vehicle))
|
||||
}
|
||||
|
||||
final def ReadyToDelete: Receive = commonDeleteBehavior
|
||||
|
|
@ -448,8 +464,16 @@ class VehicleControl(vehicle: Vehicle)
|
|||
//cancel jammed behavior
|
||||
CancelJammeredSound(vehicle)
|
||||
CancelJammeredStatus(vehicle)
|
||||
//unregister
|
||||
TaskWorkflow.execute(GUIDTask.unregisterVehicle(zone.GUID, vehicle))
|
||||
if (!vehicle.OwnerGuid.isEmpty) {
|
||||
//remove vehicle ownership is not already removed
|
||||
val obj = MountableObject
|
||||
Vehicles.Disown(obj.GUID, obj)
|
||||
}
|
||||
//lock the vehicle to prevent interaction attempts during deletion
|
||||
vehicle.PermissionGroup(0, 0)
|
||||
vehicle.PermissionGroup(1, 0)
|
||||
vehicle.PermissionGroup(2, 0)
|
||||
vehicle.PermissionGroup(3, 0)
|
||||
//banished to the shadow realm
|
||||
vehicle.Position = Vector3.Zero
|
||||
vehicle.ClearHistory()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.objects.vital.resolution
|
||||
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.avatar.AvatarBot
|
||||
import net.psforever.objects.ce.Deployable
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.damage.Damageable
|
||||
|
|
@ -236,6 +237,50 @@ object ResolutionCalculations {
|
|||
player.avatar.copy(stamina = math.max(0, player.avatar.stamina - math.floor(delta / 2).toInt))
|
||||
}
|
||||
}
|
||||
case bot: AvatarBot if noDoubleLash(bot, data) =>
|
||||
var (a, b) = damageValues
|
||||
if (bot.isAlive && !(a == 0 && b == 0)) {
|
||||
val originalHealth = bot.Health
|
||||
if (data.cause.source.DamageToHealthOnly) {
|
||||
bot.Health = SubtractWithRemainder(bot.Health, a)._1
|
||||
} else {
|
||||
var result = (0, 0)
|
||||
bot.implants.flatten.find(x => x.definition.implantType == ImplantType.PersonalShield) match {
|
||||
case Some(implant) if implant.active =>
|
||||
// Subtract armour damage from stamina
|
||||
result = SubtractWithRemainder(bot.stamina, b)
|
||||
bot.stamina = result._1
|
||||
b = result._2
|
||||
|
||||
// Then follow up with health damage if any stamina is left
|
||||
result = SubtractWithRemainder(bot.stamina, a)
|
||||
bot.stamina = result._1
|
||||
a = result._2
|
||||
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
// Subtract any remaining armour damage from armour
|
||||
result = SubtractWithRemainder(bot.Armor, b)
|
||||
bot.Armor = result._1
|
||||
b = result._2
|
||||
// Then bleed through to health if armour ran out
|
||||
result = SubtractWithRemainder(bot.Health, b)
|
||||
bot.Health = result._1
|
||||
b = result._2
|
||||
|
||||
// Finally, apply health damage to health
|
||||
result = SubtractWithRemainder(bot.Health, a)
|
||||
bot.Health = result._1
|
||||
//if b > 0 (armor) or result._2 > 0 (health), then we did the math wrong
|
||||
}
|
||||
|
||||
// If any health damage was applied also drain an amount of stamina equal to half the health damage
|
||||
if (bot.Health < originalHealth) {
|
||||
val delta = originalHealth - bot.Health
|
||||
bot.stamina = math.max(0, bot.stamina - math.floor(delta / 2).toInt)
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
DamageResult(targetBefore, SourceEntry(target), data)
|
||||
|
|
@ -336,7 +381,7 @@ object ResolutionCalculations {
|
|||
|
||||
def WildcardApplication(damage: Any, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = {
|
||||
target match {
|
||||
case _: Player =>
|
||||
case _: Player | _: AvatarBot =>
|
||||
val dam : (Int, Int) = damage match {
|
||||
case (a: Int, b: Int) => (a, b)
|
||||
case a: Int => (a, 0)
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ import scalax.collection.GraphEdge._
|
|||
import scala.util.Try
|
||||
import akka.actor.typed
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.zone.{BuildingActor, ZoneActor}
|
||||
import net.psforever.actors.zone.{BuildingActor, ShootingRangeTargetSpawnerActor, ZoneActor}
|
||||
import net.psforever.actors.zone.building.WarpGateLogic
|
||||
import net.psforever.objects.avatar.{Avatar, PlayerControl}
|
||||
import net.psforever.objects.avatar.{Avatar, AvatarBot, PlayerControl}
|
||||
import net.psforever.objects.definition.ObjectDefinition
|
||||
import net.psforever.objects.geometry.d3.VolumetricGeometry
|
||||
import net.psforever.objects.guid.pool.NumberPool
|
||||
|
|
@ -134,6 +134,10 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
*/
|
||||
private val players: TrieMap[Int, Option[Player]] = TrieMap[Int, Option[Player]]()
|
||||
|
||||
/**
|
||||
*/
|
||||
private val bots: ListBuffer[AvatarBot] = ListBuffer[AvatarBot]()
|
||||
|
||||
/**
|
||||
*/
|
||||
private val corpses: ListBuffer[Player] = ListBuffer[Player]()
|
||||
|
|
@ -145,6 +149,11 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
*/
|
||||
private var population: ActorRef = Default.Actor
|
||||
|
||||
/**
|
||||
* Actor that handles non-player controlled entities, used for VR Shooting Range targets.
|
||||
*/
|
||||
private var npcPopulation: ActorRef = Default.Actor
|
||||
|
||||
private var buildings: PairMap[Int, Building] = PairMap.empty[Int, Building]
|
||||
|
||||
private var lattice: Graph[Building, UnDiEdge] = Graph()
|
||||
|
|
@ -444,6 +453,8 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
|
||||
def Players: List[Avatar] = AllPlayers.map(_.avatar)
|
||||
|
||||
def BotAvatars: List[AvatarBot] = bots.toList
|
||||
|
||||
def LivePlayers: List[Player] = AllPlayers.filterNot(_.spectator)
|
||||
|
||||
def Spectator: List[Player] = AllPlayers.filter(_.spectator)
|
||||
|
|
@ -470,6 +481,8 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
|
||||
def Population: ActorRef = population
|
||||
|
||||
def NPCPopulation: ActorRef = npcPopulation
|
||||
|
||||
def Buildings: Map[Int, Building] = buildings
|
||||
|
||||
def Building(id: Int): Option[Building] = {
|
||||
|
|
@ -1221,6 +1234,21 @@ object Zone {
|
|||
final case class PlayerCanNotSpawn(zone: Zone, player: Player)
|
||||
}
|
||||
|
||||
object Bots {
|
||||
|
||||
/**
|
||||
* Message that reports to the zone of a freshly spawned bot.
|
||||
* @param bot the `AvatarBot`
|
||||
*/
|
||||
final case class Spawn(bot: AvatarBot)
|
||||
|
||||
/**
|
||||
* Message that tells the zone to no longer mind the bot.
|
||||
* @param bot the `AvatarBot`
|
||||
*/
|
||||
final case class Release(bot: AvatarBot)
|
||||
}
|
||||
|
||||
object Corpse {
|
||||
|
||||
/**
|
||||
|
|
@ -1574,13 +1602,18 @@ object Zone {
|
|||
zone.deployables = context.actorOf(Props(classOf[ZoneDeployableActor], zone, zone.constructions, zone.linkDynamicTurretWeapon), s"$id-deployables")
|
||||
zone.projectiles = context.actorOf(Props(classOf[ZoneProjectileActor], zone, zone.projectileList), s"$id-projectiles")
|
||||
zone.transport = context.actorOf(Props(classOf[ZoneVehicleActor], zone, zone.vehicles, zone.linkDynamicTurretWeapon), s"$id-vehicles")
|
||||
zone.population = context.actorOf(Props(classOf[ZonePopulationActor], zone, zone.players, zone.corpses), s"$id-players")
|
||||
zone.population = context.actorOf(Props(classOf[ZonePopulationActor], zone, zone.players, zone.bots, zone.corpses), s"$id-players")
|
||||
zone.projector = context.actorOf(
|
||||
Props(classOf[ZoneHotSpotDisplay], zone, zone.hotspots, 15 seconds, zone.hotspotHistory, 60 seconds),
|
||||
s"$id-hotspots"
|
||||
)
|
||||
zone.soi = context.actorOf(Props(classOf[SphereOfInfluenceActor], zone), s"$id-soi")
|
||||
|
||||
//enable target spawns for VR Shooting Range zones
|
||||
if (zone.id.startsWith("tzsh")) {
|
||||
zone.npcPopulation = context.actorOf(Props(classOf[ShootingRangeTargetSpawnerActor], zone), s"$id-npcs")
|
||||
}
|
||||
|
||||
zone.avatarEvents = context.actorOf(Props(classOf[AvatarService], zone), s"$id-avatar-events")
|
||||
zone.localEvents = context.actorOf(Props(classOf[LocalService], zone), s"$id-local-events")
|
||||
zone.vehicleEvents = context.actorOf(Props(classOf[VehicleService], zone), s"$id-vehicle-events")
|
||||
|
|
@ -1943,9 +1976,14 @@ object Zone {
|
|||
val allAffectedTargets = pssos.filter { target => testTargetsFromZone(source, target, radius) }
|
||||
//inform remaining targets that they have suffered damage
|
||||
allAffectedTargets
|
||||
.foreach { target =>
|
||||
if (zone.id.startsWith("tz") && source.Faction == target.Faction) {
|
||||
//do not perform friendly-fire in VR zones
|
||||
.foreach { target =>
|
||||
if (target.IsInVRZone) {
|
||||
//disable all server-side damage in VR zones, unless the target is a bot in the VR Shooting Range
|
||||
target match {
|
||||
case bot: AvatarBot =>
|
||||
target.Actor ! Vitality.Damage(createInteraction(source, target).calculate())
|
||||
case _ =>
|
||||
}
|
||||
} else {
|
||||
target.Actor ! Vitality.Damage(createInteraction(source, target).calculate())
|
||||
}
|
||||
|
|
@ -2107,6 +2145,8 @@ object Zone {
|
|||
//collect all targets that can be damaged
|
||||
//players
|
||||
val playerTargets = sector.livePlayerList.filterNot { _.VehicleSeated.nonEmpty }
|
||||
//bots
|
||||
val botTargets = sector.botList
|
||||
//vehicles
|
||||
val vehicleTargets = sector.vehicleList.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
|
||||
//deployables
|
||||
|
|
@ -2114,7 +2154,7 @@ object Zone {
|
|||
//amenities
|
||||
val soiTargets = sector.amenityList.collect { case amenity: Vitality if !amenity.Destroyed => amenity }
|
||||
//altogether ...
|
||||
playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets
|
||||
playerTargets ++ botTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2140,6 +2180,8 @@ object Zone {
|
|||
//collect all targets that can be damaged
|
||||
//players
|
||||
val playerTargets = sector.livePlayerList.filter { player => player.VehicleSeated.isEmpty && player.WhichSide == Sidedness.OutsideOf }
|
||||
//bots
|
||||
val botTargets = sector.botList.filter { bot => bot.WhichSide == Sidedness.OutsideOf }
|
||||
//vehicles
|
||||
val vehicleTargets = sector.vehicleList.filterNot { _.Destroyed }
|
||||
//deployables
|
||||
|
|
@ -2148,7 +2190,7 @@ object Zone {
|
|||
val soiTargets = sector.amenityList.collect {
|
||||
case amenity: Vitality if !amenity.Destroyed && amenity.WhichSide == Sidedness.OutsideOf && amenity.CanDamage => amenity }
|
||||
//altogether ...
|
||||
playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets
|
||||
playerTargets ++ botTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,17 +3,18 @@ package net.psforever.objects.zones
|
|||
|
||||
import akka.actor.{Actor, ActorRef, Props}
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.avatar.{CorpseControl, PlayerControl}
|
||||
import net.psforever.objects.avatar.{AvatarBot, CorpseControl, PlayerControl}
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.vital.{InGameHistory, SpawningActivity}
|
||||
import net.psforever.objects.{Default, Player}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
/**
|
||||
* A support `Actor` that sequences adding and removing `Avatar` and `Player` objects to mappings and lists.
|
||||
* A support `Actor` that sequences adding and removing `Avatar`, `AvatarBot`, and `Player` objects to mappings and lists.
|
||||
* The former mapping is considered to represent every user connect to the `zone` (`as Avatar` objects)
|
||||
* and their current representation (as `Player` objects).
|
||||
* The latter list keeps track of a group of former user representations.
|
||||
|
|
@ -22,7 +23,7 @@ import scala.collection.mutable.ListBuffer
|
|||
* @param playerMap the mapping of `Avatar` objects to `Player` objects
|
||||
* @param corpseList a list of `Player` objects
|
||||
*/
|
||||
class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], corpseList: ListBuffer[Player])
|
||||
class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], botList: ListBuffer[AvatarBot], corpseList: ListBuffer[Player])
|
||||
extends Actor {
|
||||
|
||||
import ZonePopulationActor._
|
||||
|
|
@ -83,6 +84,27 @@ class ZonePopulationActor(zone: Zone, playerMap: TrieMap[Int, Option[Player]], c
|
|||
sender() ! Zone.Population.PlayerHasLeft(zone, None)
|
||||
}
|
||||
|
||||
case Zone.Bots.Spawn(bot) =>
|
||||
if (BotSpawn(bot, botList)) {
|
||||
bot.Zone = zone
|
||||
zone.actor ! ZoneActor.AddToBlockMap(bot, bot.Position)
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.LoadPlayer(bot.GUID, bot.Definition.ObjectId, bot.GUID, bot.Definition.Packet.ConstructorData(bot).get, None)
|
||||
)
|
||||
}
|
||||
|
||||
case Zone.Bots.Release(bot) =>
|
||||
if (BotRelease(bot, botList)) {
|
||||
if (bot.Actor != null) bot.Actor ! akka.actor.PoisonPill
|
||||
bot.Actor = Default.Actor
|
||||
zone.actor ! ZoneActor.RemoveFromBlockMap(bot)
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.ObjectDelete(bot.GUID, bot.GUID, unk=0)
|
||||
)
|
||||
}
|
||||
|
||||
case Zone.Corpse.Add(player) =>
|
||||
//player can be a corpse if they are in the current zone or are not in any zone
|
||||
//player is "found" if their avatar can be matched by name within this zone and it has a character
|
||||
|
|
@ -202,6 +224,31 @@ object ZonePopulationActor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given `AvatarBot` to the list of bots.
|
||||
* @param bot an `AvatarBot` object
|
||||
* @param botList a list of `AvatarBot` objects
|
||||
*/
|
||||
def BotSpawn(bot: AvatarBot, botList: ListBuffer[AvatarBot]): Boolean = {
|
||||
botList += bot
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given `AvatarBot` from the list of bots.
|
||||
* @param bot an `AvatarBot` object
|
||||
* @param botList a list of `AvatarBot` objects
|
||||
*/
|
||||
def BotRelease(bot: AvatarBot, botList: ListBuffer[AvatarBot]): Boolean = {
|
||||
botList.indexOf(bot) match {
|
||||
case -1 =>
|
||||
false
|
||||
case index =>
|
||||
botList.remove(index)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given `player` passes a condition check, add it to the list.
|
||||
* @param player a `Player` object
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import net.psforever.objects.equipment.Equipment
|
|||
import net.psforever.objects.serverobject.environment.PieceOfEnvironment
|
||||
import net.psforever.objects.serverobject.structures.{Amenity, Building}
|
||||
import net.psforever.objects.{Player, Vehicle}
|
||||
import net.psforever.objects.avatar.AvatarBot
|
||||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
|
|
@ -20,6 +21,8 @@ trait SectorPopulation {
|
|||
|
||||
def livePlayerList: List[Player]
|
||||
|
||||
def botList: List[AvatarBot]
|
||||
|
||||
def corpseList: List[Player]
|
||||
|
||||
def vehicleList: List[Vehicle]
|
||||
|
|
@ -41,6 +44,7 @@ trait SectorPopulation {
|
|||
*/
|
||||
def total: Int = {
|
||||
livePlayerList.size +
|
||||
botList.size +
|
||||
corpseList.size +
|
||||
vehicleList.size +
|
||||
equipmentOnGroundList.size +
|
||||
|
|
@ -123,6 +127,10 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int)
|
|||
(a: Player, b: Player) => a.GUID == b.GUID
|
||||
)
|
||||
|
||||
private val bots: SectorListOf[AvatarBot] = new SectorListOf[AvatarBot](
|
||||
(a: AvatarBot, b: AvatarBot) => a.GUID == b.GUID
|
||||
)
|
||||
|
||||
private val corpses: SectorListOf[Player] = new SectorListOf[Player](
|
||||
(a: Player, b: Player) => a eq b
|
||||
)
|
||||
|
|
@ -161,6 +169,8 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int)
|
|||
|
||||
def livePlayerList : List[Player] = livePlayers.list
|
||||
|
||||
def botList : List[AvatarBot] = bots.list
|
||||
|
||||
def corpseList: List[Player] = corpses.list
|
||||
|
||||
def vehicleList: List[Vehicle] = vehicles.list
|
||||
|
|
@ -191,6 +201,8 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int)
|
|||
corpses.list.size < corpses.addTo(p).size
|
||||
case p: Player =>
|
||||
livePlayers.list.size < livePlayers.addTo(p).size
|
||||
case ab: AvatarBot =>
|
||||
bots.list.size < bots.addTo(ab).size
|
||||
case v: Vehicle =>
|
||||
vehicles.list.size < vehicles.addTo(v).size
|
||||
case e: Equipment =>
|
||||
|
|
@ -226,6 +238,8 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int)
|
|||
corpses.list.size > corpses.removeFrom(p).size
|
||||
case p: Player =>
|
||||
livePlayers.list.size > livePlayers.removeFrom(p).size
|
||||
case ab: AvatarBot =>
|
||||
bots.list.size > bots.removeFrom(ab).size
|
||||
case v: Vehicle =>
|
||||
vehicles.list.size > vehicles.removeFrom(v).size
|
||||
case e: Equipment =>
|
||||
|
|
@ -253,6 +267,7 @@ object Sector {
|
|||
* The specific datastructure that is mentioned when using the term "sector conglomerate".
|
||||
* Typically used to compose the lists of entities from various individual sectors.
|
||||
* @param livePlayerList the living players
|
||||
* @param botList the bot avatars
|
||||
* @param corpseList the dead players
|
||||
* @param vehicleList vehicles
|
||||
* @param equipmentOnGroundList dropped equipment
|
||||
|
|
@ -265,6 +280,7 @@ class SectorGroup(
|
|||
val rangeX: Float,
|
||||
val rangeY: Float,
|
||||
val livePlayerList: List[Player],
|
||||
val botList: List[AvatarBot],
|
||||
val corpseList: List[Player],
|
||||
val vehicleList: List[Vehicle],
|
||||
val equipmentOnGroundList: List[Equipment],
|
||||
|
|
@ -314,6 +330,7 @@ object SectorGroup {
|
|||
rangeX,
|
||||
rangeY,
|
||||
sector.livePlayerList.filterNot(p => p.spectator || !p.allowInteraction),
|
||||
sector.botList.filterNot(b => !b.allowInteraction),
|
||||
sector.corpseList,
|
||||
sector.vehicleList,
|
||||
sector.equipmentOnGroundList,
|
||||
|
|
@ -362,13 +379,14 @@ object SectorGroup {
|
|||
*/
|
||||
def apply(rangeX: Float, rangeY: Float, sectors: Iterable[Sector]): SectorGroup = {
|
||||
if (sectors.isEmpty) {
|
||||
new SectorGroup(rangeX, rangeY, Nil, Nil, Nil, Nil, Nil, Nil, Nil, Nil, Nil)
|
||||
new SectorGroup(rangeX, rangeY, Nil, Nil, Nil, Nil, Nil, Nil, Nil, Nil, Nil, Nil)
|
||||
} else if (sectors.size == 1) {
|
||||
val sector = sectors.head
|
||||
new SectorGroup(
|
||||
rangeX,
|
||||
rangeY,
|
||||
sector.livePlayerList.filterNot(p => p.spectator || !p.allowInteraction),
|
||||
sector.botList.filterNot(b => !b.allowInteraction),
|
||||
sector.corpseList,
|
||||
sector.vehicleList,
|
||||
sector.equipmentOnGroundList,
|
||||
|
|
@ -383,6 +401,7 @@ object SectorGroup {
|
|||
rangeX,
|
||||
rangeY,
|
||||
sectors.flatMap { _.livePlayerList }.toList.distinct.filterNot(p => p.spectator || !p.allowInteraction),
|
||||
sectors.flatMap { _.botList }.toList.distinct.filterNot(b => !b.allowInteraction),
|
||||
sectors.flatMap { _.corpseList }.toList.distinct,
|
||||
sectors.flatMap { _.vehicleList }.toList.distinct,
|
||||
sectors.flatMap { _.equipmentOnGroundList }.toList.distinct,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ object GenericAction extends IntEnum[GenericAction] {
|
|||
final case object FailToDeconstruct extends GenericAction(value = 33)
|
||||
final case object LookingForSquad_RCV extends GenericAction(value = 36)
|
||||
final case object NotLookingForSquad_RCV extends GenericAction(value = 37)
|
||||
final case object TrainingGriefWarning extends GenericAction(value = 43)
|
||||
final case object Unknown45 extends GenericAction(value = 45)
|
||||
|
||||
final case class Unknown(override val value: Int) extends GenericAction(value)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) 2026 PSForever
|
||||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
|
||||
/**
|
||||
* Dispatched from the server to cause a bot avatar to perform a random emote
|
||||
* @param bot_guid the target bot's global unique identifier
|
||||
*/
|
||||
final case class TriggerBotAction(bot_guid: PlanetSideGUID, unk1: Long = 0, unk2: Long = 0, unk3: Long = 4294967295L)
|
||||
extends PlanetSideGamePacket {
|
||||
type Packet = TriggerBotAction
|
||||
def opcode = GamePacketOpcode.TriggerBotAction
|
||||
def encode = TriggerBotAction.encode(this)
|
||||
}
|
||||
|
||||
object TriggerBotAction extends Marshallable[TriggerBotAction] {
|
||||
implicit val codec : Codec[TriggerBotAction] = (
|
||||
("bot_guid" | PlanetSideGUID.codec) ::
|
||||
("unk1" | uint32L) ::
|
||||
("unk2" | uint32L) ::
|
||||
("unk3" | uint32L)
|
||||
).as[TriggerBotAction]
|
||||
}
|
||||
|
|
@ -404,28 +404,37 @@ object ObjectClass {
|
|||
final val vulture = 986
|
||||
final val wasp = 997
|
||||
//other
|
||||
final val ams_order_terminal = 48
|
||||
final val ams_respawn_tube = 49
|
||||
final val avatar = 121
|
||||
final val bfr_rearm_terminal = 142
|
||||
final val capture_flag = 157
|
||||
final val implant_terminal_interface = 409
|
||||
final val locker_container = 456
|
||||
final val lodestar_repair_terminal = 461
|
||||
final val manned_turret = 480
|
||||
final val matrix_terminala = 517
|
||||
final val matrix_terminalb = 518
|
||||
final val matrix_terminalc = 519
|
||||
final val multivehicle_rearm_terminal = 576
|
||||
final val order_terminal = 612
|
||||
final val order_terminala = 613
|
||||
final val order_terminalb = 614
|
||||
final val portable_ammo_terminal = 684
|
||||
final val portable_order_terminal = 690
|
||||
final val spawn_pad = 800
|
||||
final val spawn_zone = 815
|
||||
final val targeting_laser_dispenser = 851
|
||||
final val teleportpad_terminal = 853
|
||||
final val ams_order_terminal = 48
|
||||
final val ams_respawn_tube = 49
|
||||
final val avatar = 121
|
||||
final val avatar_bot = 122
|
||||
final val avatar_bot_agile = 123
|
||||
final val avatar_bot_agile_no_weapon = 124
|
||||
final val avatar_bot_max = 125
|
||||
final val avatar_bot_max_no_weapon = 126
|
||||
final val avatar_bot_reinforced = 127
|
||||
final val avatar_bot_reinforced_no_weapon = 128
|
||||
final val avatar_bot_standard = 129
|
||||
final val avatar_bot_standard_no_weapon = 130
|
||||
final val bfr_rearm_terminal = 142
|
||||
final val capture_flag = 157
|
||||
final val implant_terminal_interface = 409
|
||||
final val locker_container = 456
|
||||
final val lodestar_repair_terminal = 461
|
||||
final val manned_turret = 480
|
||||
final val matrix_terminala = 517
|
||||
final val matrix_terminalb = 518
|
||||
final val matrix_terminalc = 519
|
||||
final val multivehicle_rearm_terminal = 576
|
||||
final val order_terminal = 612
|
||||
final val order_terminala = 613
|
||||
final val order_terminalb = 614
|
||||
final val portable_ammo_terminal = 684
|
||||
final val portable_order_terminal = 690
|
||||
final val spawn_pad = 800
|
||||
final val spawn_zone = 815
|
||||
final val targeting_laser_dispenser = 851
|
||||
final val teleportpad_terminal = 853
|
||||
|
||||
// For property overrides
|
||||
final val delivererv = 239
|
||||
|
|
@ -664,8 +673,17 @@ object ObjectClass {
|
|||
case ObjectClass.router_telepad => ConstructorData(DetailedConstructionToolData.codec, "router telepad")
|
||||
case ObjectClass.boomer_trigger => ConstructorData(DetailedConstructionToolData.codec, "boomer trigger")
|
||||
//other
|
||||
case ObjectClass.avatar => ConstructorData(DetailedPlayerData.codec(false), "avatar")
|
||||
case ObjectClass.locker_container => ConstructorData(DetailedLockerContainerData.codec, "locker container")
|
||||
case ObjectClass.avatar => ConstructorData(DetailedPlayerData.codec(false), "avatar")
|
||||
case ObjectClass.avatar_bot => ConstructorData(DetailedPlayerData.codec(false), "avatar bot")
|
||||
case ObjectClass.avatar_bot_agile => ConstructorData(DetailedPlayerData.codec(false), "avatar bot agile")
|
||||
case ObjectClass.avatar_bot_agile_no_weapon => ConstructorData(DetailedPlayerData.codec(false), "avatar bot agile no weapon")
|
||||
case ObjectClass.avatar_bot_max => ConstructorData(DetailedPlayerData.codec(false), "avatar bot max")
|
||||
case ObjectClass.avatar_bot_max_no_weapon => ConstructorData(DetailedPlayerData.codec(false), "avatar bot max no weapon")
|
||||
case ObjectClass.avatar_bot_reinforced => ConstructorData(DetailedPlayerData.codec(false), "avatar bot reinforced")
|
||||
case ObjectClass.avatar_bot_reinforced_no_weapon => ConstructorData(DetailedPlayerData.codec(false), "avatar bot reinforced no weapon")
|
||||
case ObjectClass.avatar_bot_standard => ConstructorData(DetailedPlayerData.codec(false), "avatar bot standard")
|
||||
case ObjectClass.avatar_bot_standard_no_weapon => ConstructorData(DetailedPlayerData.codec(false), "avatar bot standard no weapon")
|
||||
case ObjectClass.locker_container => ConstructorData(DetailedLockerContainerData.codec, "locker container")
|
||||
|
||||
//failure case
|
||||
case _ => defaultFailureCodec(objClass)
|
||||
|
|
@ -983,21 +1001,30 @@ object ObjectClass {
|
|||
//vehicles?
|
||||
case ObjectClass.orbital_shuttle => ConstructorData(OrbitalShuttleData.codec, "HART")
|
||||
//other
|
||||
case ObjectClass.ams_order_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.ams_respawn_tube => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.avatar => ConstructorData(PlayerData.codec(false), "avatar")
|
||||
case ObjectClass.bfr_rearm_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.implant_terminal_interface => ConstructorData(CommonFieldData.codec2, "implant terminal")
|
||||
case ObjectClass.lodestar_repair_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.matrix_terminala => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.matrix_terminalb => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.matrix_terminalc => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.multivehicle_rearm_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminala => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminalb => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.targeting_laser_dispenser => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.teleportpad_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.ams_order_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.ams_respawn_tube => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.avatar => ConstructorData(PlayerData.codec(false), "avatar")
|
||||
case ObjectClass.avatar_bot => ConstructorData(PlayerData.codec(false), "avatar bot")
|
||||
case ObjectClass.avatar_bot_agile => ConstructorData(PlayerData.codec(false), "avatar bot agile")
|
||||
case ObjectClass.avatar_bot_agile_no_weapon => ConstructorData(PlayerData.codec(false), "avatar bot agile no weapon")
|
||||
case ObjectClass.avatar_bot_max => ConstructorData(PlayerData.codec(false), "avatar bot max")
|
||||
case ObjectClass.avatar_bot_max_no_weapon => ConstructorData(PlayerData.codec(false), "avatar bot max no weapon")
|
||||
case ObjectClass.avatar_bot_reinforced => ConstructorData(PlayerData.codec(false), "avatar bot reinforced")
|
||||
case ObjectClass.avatar_bot_reinforced_no_weapon => ConstructorData(PlayerData.codec(false), "avatar bot reinforced no weapon")
|
||||
case ObjectClass.avatar_bot_standard => ConstructorData(PlayerData.codec(false), "avatar bot standard")
|
||||
case ObjectClass.avatar_bot_standard_no_weapon => ConstructorData(PlayerData.codec(false), "avatar bot standard no weapon")
|
||||
case ObjectClass.bfr_rearm_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.implant_terminal_interface => ConstructorData(CommonFieldData.codec2, "implant terminal")
|
||||
case ObjectClass.lodestar_repair_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.matrix_terminala => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.matrix_terminalb => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.matrix_terminalc => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.multivehicle_rearm_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminala => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminalb => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.targeting_laser_dispenser => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.teleportpad_terminal => ConstructorData(CommonFieldData.codec2, "terminal")
|
||||
//failure case
|
||||
case _ => defaultFailureCodec(objClass)
|
||||
}
|
||||
|
|
@ -1324,17 +1351,26 @@ object ObjectClass {
|
|||
case ObjectClass.vulture => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle")
|
||||
case ObjectClass.wasp => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle")
|
||||
//other
|
||||
case ObjectClass.ams_respawn_tube => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.avatar => ConstructorData(PlayerData.codec(true), "avatar")
|
||||
case ObjectClass.capture_flag => ConstructorData(CaptureFlagData.codec, "capture flag")
|
||||
case ObjectClass.implant_terminal_interface => ConstructorData(CommonFieldData.codec2, "implant terminal")
|
||||
case ObjectClass.locker_container => ConstructorData(LockerContainerData.codec, "locker container")
|
||||
case ObjectClass.matrix_terminala => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.matrix_terminalb => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.matrix_terminalc => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminal => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminala => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminalb => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.ams_respawn_tube => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.avatar => ConstructorData(PlayerData.codec(true), "avatar")
|
||||
case ObjectClass.avatar_bot => ConstructorData(PlayerData.codec(true), "avatar bot")
|
||||
case ObjectClass.avatar_bot_agile => ConstructorData(PlayerData.codec(true), "avatar bot agile")
|
||||
case ObjectClass.avatar_bot_agile_no_weapon => ConstructorData(PlayerData.codec(true), "avatar bot agile no weapon")
|
||||
case ObjectClass.avatar_bot_max => ConstructorData(PlayerData.codec(true), "avatar bot max")
|
||||
case ObjectClass.avatar_bot_max_no_weapon => ConstructorData(PlayerData.codec(true), "avatar bot max no weapon")
|
||||
case ObjectClass.avatar_bot_reinforced => ConstructorData(PlayerData.codec(true), "avatar bot reinforced")
|
||||
case ObjectClass.avatar_bot_reinforced_no_weapon => ConstructorData(PlayerData.codec(true), "avatar bot reinforced no weapon")
|
||||
case ObjectClass.avatar_bot_standard => ConstructorData(PlayerData.codec(true), "avatar bot standard")
|
||||
case ObjectClass.avatar_bot_standard_no_weapon => ConstructorData(PlayerData.codec(true), "avatar bot standard no weapon")
|
||||
case ObjectClass.capture_flag => ConstructorData(CaptureFlagData.codec, "capture flag")
|
||||
case ObjectClass.implant_terminal_interface => ConstructorData(CommonFieldData.codec2, "implant terminal")
|
||||
case ObjectClass.locker_container => ConstructorData(LockerContainerData.codec, "locker container")
|
||||
case ObjectClass.matrix_terminala => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.matrix_terminalb => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.matrix_terminalc => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminal => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminala => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
case ObjectClass.order_terminalb => DroppedItemData(CommonFieldData.codec2, "terminal")
|
||||
//failure case
|
||||
case _ => defaultFailureCodec(objClass)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ case class GameConfig(
|
|||
baseCertifications: Seq[Certification],
|
||||
warpGates: WarpGateConfig,
|
||||
cavernRotation: CavernRotationConfig,
|
||||
virtualTraining: VirtualTrainingConfig,
|
||||
savedMsg: SavedMessageEvents,
|
||||
playerDraw: PlayerStateDrawSettings,
|
||||
doorsCanBeOpenedByMedAppFromThisDistance: Float,
|
||||
|
|
@ -218,6 +219,13 @@ case class CavernRotationConfig(
|
|||
forceRotationImmediately: Boolean
|
||||
)
|
||||
|
||||
case class VirtualTrainingConfig(
|
||||
shootingRangeTargetsEnabled: Boolean,
|
||||
maleBotNames: List[String],
|
||||
femaleBotNames: List[String],
|
||||
universalBotNames: List[String]
|
||||
)
|
||||
|
||||
case class SavedMessageEvents(
|
||||
short: SavedMessageTimings,
|
||||
renewal: SavedMessageTimings,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue