diff --git a/server/src/test/scala/PacketCodingActorTest.scala b/server/src/test/scala/PacketCodingActorTest.scala index 7450451a0..63668a080 100644 --- a/server/src/test/scala/PacketCodingActorTest.scala +++ b/server/src/test/scala/PacketCodingActorTest.scala @@ -694,7 +694,7 @@ class PacketCodingActorKTest extends ActorTest { 0L, 0L, 0L, - Some(DCDExtra2(0, 0)), + Some(ImprintingProgress(0, 0)), Nil, Nil, false, diff --git a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala index 886b01039..86423a0e5 100644 --- a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala +++ b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala @@ -221,7 +221,7 @@ object VehicleSpawnPadControlTest { val terminal = Terminal(GlobalDefinitions.vehicle_terminal_combined) val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) - val weapon = vehicle.WeaponControlledFromSeat(1).get.asInstanceOf[Tool] + val weapon = vehicle.WeaponControlledFromSeat(1).head.asInstanceOf[Tool] val guid: NumberPoolHub = new NumberPoolHub(MaxNumberSource(5)) guid.AddPool("test-pool", (0 to 5).toList) guid.register(vehicle, "test-pool") diff --git a/src/main/resources/guid-pools/default.json b/src/main/resources/guid-pools/default.json index 7d6ec482d..ab7620a06 100644 --- a/src/main/resources/guid-pools/default.json +++ b/src/main/resources/guid-pools/default.json @@ -41,6 +41,12 @@ "Max": 39500, "Selector": "random" }, + { + "Name": "rc-projectiles", + "Start": 39701, + "Max": 40000, + "Selector": "random" + }, { "Name": "projectiles", "Start": 40100, diff --git a/src/main/resources/guid-pools/z3.json b/src/main/resources/guid-pools/z3.json index 6a4c3bf54..fff973ba0 100644 --- a/src/main/resources/guid-pools/z3.json +++ b/src/main/resources/guid-pools/z3.json @@ -40,5 +40,11 @@ "Start": 37501, "Max": 40099, "Selector": "random" + }, + { + "Name": "rc-projectiles", + "Start": 64001, + "Max": 64300, + "Selector": "random" } ] diff --git a/src/main/resources/guid-pools/z4.json b/src/main/resources/guid-pools/z4.json index bf0271228..bd690233f 100644 --- a/src/main/resources/guid-pools/z4.json +++ b/src/main/resources/guid-pools/z4.json @@ -40,5 +40,11 @@ "Start": 36601, "Max": 39600, "Selector": "random" + }, + { + "Name": "rc-projectiles", + "Start": 64001, + "Max": 64300, + "Selector": "random" } ] diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index f93261e4c..f0cb86112 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -377,7 +377,12 @@ class AvatarActor( implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None), locker = locker )) - defaultStaminaRegen(initialDelay = 0.5f seconds) + // if we need to start stamina regeneration + tryRestoreStaminaForSession(stamina = 1) match { + case Some(sess) => + defaultStaminaRegen(initialDelay = 0.5f seconds) + case _ => ; + } replyTo ! AvatarLoginResponse(avatar) case Failure(e) => log.error(e)("db failure") @@ -386,8 +391,7 @@ class AvatarActor( case ReplaceAvatar(newAvatar) => replaceAvatar(newAvatar) - staminaRegenTimer.cancel() - defaultStaminaRegen(initialDelay = 0.5f seconds) + startIfStoppedStaminaRegen(initialDelay = 0.5f seconds) Behaviors.same case AddFirstTimeEvent(event) => @@ -672,6 +676,18 @@ class AvatarActor( throwLoadoutFailure(s"no owned vehicle found for ${player.Name}") } ) + + case LoadoutType.Battleframe => + ( + number + 15, + player.Zone.GUID(avatar.vehicle) match { + case Some(vehicle: Vehicle) + if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) => + storeVehicleLoadout(player, name, number + 5, vehicle) + case _ => + throwLoadoutFailure(s"no owned battleframe found for ${player.Name}") + } + ) } result.onComplete { case Success(loadout) => @@ -697,9 +713,8 @@ class AvatarActor( ) ) case LoadoutType.Vehicle if avatar.loadouts(number + 10).nonEmpty => - val lineNo = number + 10 ( - lineNo, + number + 10, ctx.run( query[persistence.Vehicleloadout] .filter(_.avatarId == lift(avatar.id)) @@ -707,8 +722,18 @@ class AvatarActor( .delete ) ) + case LoadoutType.Battleframe if avatar.loadouts(number + 15).nonEmpty => + ( + number + 15, + ctx.run( + query[persistence.Vehicleloadout] + .filter(_.avatarId == lift(avatar.id)) + .filter(_.loadoutNumber == lift(number + 5)) + .delete + ) + ) case _ => - (number, throwLoadoutFailure("unhandled loadout type or no loadout")) + (number, throwLoadoutFailure(msg = "unhandled loadout type or no loadout")) } result.onComplete { case Success(_) => @@ -739,7 +764,7 @@ class AvatarActor( Avatar.purchaseCooldowns.get(item) match { case Some(cooldown) => //only send for items with cooldowns - newTimes = newTimes.updated(item.Name, time) + newTimes = newTimes.updated(name, time) updatePurchaseTimer(name, cooldown.toSeconds, unk1 = true) case _ => ; } @@ -782,14 +807,8 @@ class AvatarActor( implants = avatar.implants.updated(slot, Some(implant.copy(active = true))) )) sessionActor ! SessionActor.SendResponse( - AvatarImplantMessage( - session.get.player.GUID, - ImplantAction.Activation, - slot, - 1 - ) + AvatarImplantMessage(session.get.player.GUID, ImplantAction.Activation, slot, 1) ) - // Activation sound / effect session.get.zone.AvatarEvents ! AvatarServiceMessage( session.get.zone.id, @@ -799,9 +818,7 @@ class AvatarActor( implant.definition.implantType.value * 2 + 1 ) ) - implantTimers.get(slot).foreach(_.cancel()) - val interval = implant.definition.GetCostIntervalByExoSuit(session.get.player.ExoSuit).milliseconds // TODO costInterval should be an option ^ if (interval.toMillis > 0) { @@ -880,26 +897,17 @@ class AvatarActor( tryRestoreStaminaForSession(stamina) match { case Some(sess) => actuallyRestoreStamina(stamina, sess) - defaultStaminaRegen(initialDelay = 0.5f seconds) case _ => ; } Behaviors.same case RestoreStaminaPeriodically(stamina) => - tryRestoreStaminaForSession(stamina) match { - case Some(sess) => - actuallyRestoreStaminaIfStationary(stamina, sess) - case _ => ; - } - defaultStaminaRegen(initialDelay = 0.5f seconds) + restoreStaminaPeriodically(stamina) Behaviors.same case ConsumeStamina(stamina) => if (stamina > 0) { consumeThisMuchStamina(stamina) - if(staminaRegenTimer.isCancelled) { - defaultStaminaRegen(initialDelay = 0.5f seconds) - } } else { log.warn(s"consumed stamina must be larger than 0, but is: $stamina") } @@ -1053,35 +1061,52 @@ class AvatarActor( } def tryRestoreStaminaForSession(stamina: Int): Option[Session] = { - session match { - case out @ Some(_) if !avatar.staminaFull && stamina > 0 => out - case _ => None + (session, _avatar) match { + case (out @ Some(_), Some(a)) if !a.staminaFull && stamina > 0 => out + case _ => None } } def actuallyRestoreStaminaIfStationary(stamina: Int, session: Session): Unit = { - if (session.player.VehicleSeated.nonEmpty || !(session.player.isMoving || session.player.Jumping)) { + val player = session.player + if (player.VehicleSeated.nonEmpty || !(player.isMoving || player.Jumping)) { actuallyRestoreStamina(stamina, session) } } def actuallyRestoreStamina(stamina: Int, session: Session): Unit = { - val totalStamina = math.min(avatar.maxStamina, avatar.stamina + stamina) - val isFatigued = if (avatar.fatigued && totalStamina >= 20) { - val pguid = session.player.GUID - avatar.implants.zipWithIndex.foreach { - case (Some(_), slot) => - sessionActor ! SessionActor.SendResponse( - AvatarImplantMessage(pguid, ImplantAction.OutOfStamina, slot, 0) - ) - case _ => () + val originalStamina = avatar.stamina + val maxStamina = avatar.maxStamina + val totalStamina = math.min(maxStamina, originalStamina + stamina) + if (originalStamina < totalStamina) { + val originalFatigued = avatar.fatigued + val isFatigued = totalStamina < 20 + avatar = avatar.copy(stamina = totalStamina, fatigued = isFatigued) + if (totalStamina == maxStamina) { + staminaRegenTimer.cancel() + staminaRegenTimer = Default.Cancellable + } + if (session.player.HasGUID) { + val guid = session.player.GUID + if (originalFatigued && !isFatigued) { + avatar.implants.zipWithIndex.foreach { + case (Some(_), slot) => + sessionActor ! SessionActor.SendResponse(AvatarImplantMessage(guid, ImplantAction.OutOfStamina, slot, 0)) + case _ => ; + } + } + sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(guid, 2, totalStamina)) } - false - } else { - avatar.fatigued } - avatarCopy(avatar.copy(stamina = totalStamina, fatigued = isFatigued)) - sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.player.GUID, 2, totalStamina)) + } + + def restoreStaminaPeriodically(stamina: Int): Unit = { + tryRestoreStaminaForSession(stamina) match { + case Some(sess) => + actuallyRestoreStaminaIfStationary(stamina, sess) + case _ => ; + } + startIfStoppedStaminaRegen(initialDelay = 0.5f seconds) } /** @@ -1102,6 +1127,7 @@ class AvatarActor( val alreadyFatigued = avatar.fatigued val becomeFatigued = !alreadyFatigued && totalStamina == 0 avatarCopy(avatar.copy(stamina = totalStamina, fatigued = alreadyFatigued || becomeFatigued)) + startIfStoppedStaminaRegen(initialDelay = 0.5f seconds) val player = session.get.player if (player.HasGUID) { if (becomeFatigued) { @@ -1227,20 +1253,13 @@ class AvatarActor( avatarCopy(avatar.copy( implants = avatar.implants.updated(slot, Some(implant.copy(active = false))) )) - // Deactivation sound / effect session.get.zone.AvatarEvents ! AvatarServiceMessage( session.get.zone.id, AvatarAction.PlanetsideAttribute(session.get.player.GUID, 28, implant.definition.implantType.value * 2) ) - sessionActor ! SessionActor.SendResponse( - AvatarImplantMessage( - session.get.player.GUID, - ImplantAction.Activation, - slot, - 0 - ) + AvatarImplantMessage(session.get.player.GUID, ImplantAction.Activation, slot, 0) ) case None => log.error(s"requested deactivation of unknown implant $implantType") } @@ -1501,7 +1520,7 @@ class AvatarActor( } vehicles <- loadVehicleLoadouts().andThen { case out @ Success(_) => out - case Failure(_) => Future(Array.fill[Option[Loadout]](5)(None).toSeq) + case Failure(_) => Future(Array.fill[Option[Loadout]](10)(None).toSeq) } } yield infantry ++ vehicles } @@ -1533,7 +1552,8 @@ class AvatarActor( .run(query[persistence.Vehicleloadout].filter(_.avatarId == lift(avatar.id))) .map { loadouts => loadouts.map { loadout => - val toy = new Vehicle(DefinitionUtil.idToDefinition(loadout.vehicle).asInstanceOf[VehicleDefinition]) + val definition = DefinitionUtil.idToDefinition(loadout.vehicle).asInstanceOf[VehicleDefinition] + val toy = new Vehicle(definition) buildContainedEquipmentFromClob(toy, loadout.items) val result = (loadout.loadoutNumber, Loadout.Create(toy, loadout.name)) @@ -1544,29 +1564,36 @@ class AvatarActor( result } } - .map { loadouts => (0 until 5).map { index => loadouts.find(_._1 == index).map(_._2) } } + .map { loadouts => (0 until 10).map { index => loadouts.find(_._1 == index).map(_._2) } } } def refreshLoadouts(loadouts: Iterable[(Option[Loadout], Int)]): Unit = { loadouts.map { case (Some(loadout: InfantryLoadout), index) => - FavoritesMessage( - LoadoutType.Infantry, + FavoritesMessage.Infantry( session.get.player.GUID, index, loadout.label, InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype) ) + case (Some(loadout: VehicleLoadout), index) + if GlobalDefinitions.isBattleFrameVehicle(loadout.vehicle_definition) => + FavoritesMessage.Battleframe( + session.get.player.GUID, + index - 15, + loadout.label, + VehicleLoadout.DetermineBattleframeSubtype(loadout.vehicle_definition) + ) case (Some(loadout: VehicleLoadout), index) => - FavoritesMessage( - LoadoutType.Vehicle, + FavoritesMessage.Vehicle( session.get.player.GUID, index - 10, - loadout.label, - 0 + loadout.label ) case (_, index) => - val (mtype, lineNo) = if (index < 10) { + val (mtype, lineNo) = if (index > 14) { + (LoadoutType.Battleframe, index - 15) + } else if (index < 10) { (LoadoutType.Infantry, index) } else { (LoadoutType.Vehicle, index - 10) @@ -1585,29 +1612,38 @@ class AvatarActor( avatar.loadouts.lift(line) match { case Some(Some(loadout: InfantryLoadout)) => sessionActor ! SessionActor.SendResponse( - FavoritesMessage( - LoadoutType.Infantry, + FavoritesMessage.Infantry( session.get.player.GUID, line, loadout.label, InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype) ) ) + case Some(Some(loadout: VehicleLoadout)) + if GlobalDefinitions.isBattleFrameVehicle(loadout.vehicle_definition) => + sessionActor ! SessionActor.SendResponse( + FavoritesMessage.Battleframe( + session.get.player.GUID, + line - 15, + loadout.label, + VehicleLoadout.DetermineBattleframeSubtype(loadout.vehicle_definition) + ) + ) case Some(Some(loadout: VehicleLoadout)) => sessionActor ! SessionActor.SendResponse( - FavoritesMessage( - LoadoutType.Vehicle, + FavoritesMessage.Vehicle( session.get.player.GUID, line - 10, - loadout.label, - 0 + loadout.label ) ) case Some(None) => - val (mtype, lineNo) = if (line < 10) { - (LoadoutType.Infantry, line) + val (mtype, lineNo, subtype) = if (line > 14) { + (LoadoutType.Battleframe, line - 15, Some(0)) + } else if (line < 10) { + (LoadoutType.Infantry, line, Some(0)) } else { - (LoadoutType.Vehicle, line - 10) + (LoadoutType.Vehicle, line - 10, None) } sessionActor ! SessionActor.SendResponse( FavoritesMessage( @@ -1615,7 +1651,7 @@ class AvatarActor( session.get.player.GUID, lineNo, "", - 0 + subtype ) ) case _ => ; @@ -1679,15 +1715,18 @@ class AvatarActor( } } + def startIfStoppedStaminaRegen(initialDelay: FiniteDuration): Unit = { + if (staminaRegenTimer.isCancelled) { + defaultStaminaRegen(initialDelay) + } + } + def defaultStaminaRegen(initialDelay: FiniteDuration): Unit = { staminaRegenTimer.cancel() - staminaRegenTimer = if (!avatar.staminaFull) { - context.system.scheduler.scheduleWithFixedDelay(initialDelay, 0.5 seconds)(() => { - context.self ! RestoreStaminaPeriodically(1) - }) - } else { - Default.Cancellable - } + val restoreStaminaFunc: Int => Unit = restoreStaminaPeriodically + staminaRegenTimer = context.system.scheduler.scheduleWithFixedDelay(initialDelay, delay = 0.5 seconds)(() => { + restoreStaminaFunc(1) + }) } // same as in SA, this really doesn't belong here @@ -1723,7 +1762,7 @@ class AvatarActor( } def resolveSharedPurchaseTimeNames(pair: (BasicDefinition, String)): Seq[(BasicDefinition, String)] = { - val (_, name) = pair + val (definition, name) = pair if (name.matches("(tr|nc|vs)hev_.+") && Config.app.game.sharedMaxCooldown) { val faction = name.take(2) (if (faction.equals("nc")) { @@ -1738,7 +1777,22 @@ class AvatarActor( Seq(s"${faction}hev_antipersonnel", s"${faction}hev_antivehicular", s"${faction}hev_antiaircraft") ) } else { - Seq(pair) + definition match { + case vdef: VehicleDefinition + if GlobalDefinitions.isBattleFrameFlightVehicle(vdef) => + val bframe = name.substring(0, name.indexOf('_')) + val gunner = bframe+"_gunner" + Seq((DefinitionUtil.fromString(gunner), gunner), (vdef, name)) + + case vdef: VehicleDefinition + if GlobalDefinitions.isBattleFrameGunnerVehicle(vdef) => + val bframe = name.substring(0, name.indexOf('_')) + val flight = bframe+"_flight" + Seq((vdef, name), (DefinitionUtil.fromString(flight), flight)) + + case _ => + Seq(pair) + } } } diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 7dd530d4e..61e13a3d8 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -38,10 +38,11 @@ import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret} import net.psforever.objects.serverobject.zipline.ZipLinePath -import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject} import net.psforever.objects.teamwork.Squad import net.psforever.objects.vehicles.Utility.InternalTelepad import net.psforever.objects.vehicles._ +import net.psforever.objects.vehicles.control.BfrFlight import net.psforever.objects.vital._ import net.psforever.objects.vital.base._ import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason} @@ -158,8 +159,6 @@ object SessionActor { tool: ConstructionItem, index: Int ) - - private final case class LoadedRemoteProjectile(projectile_guid: PlanetSideGUID, projectile: Option[Projectile]) } class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long) @@ -181,10 +180,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con var cluster: typed.ActorRef[ICS.Command] = Default.Actor var _session: Session = Session() var progressBarValue: Option[Float] = None - var shooting: Option[PlanetSideGUID] = None //ChangeFireStateMessage_Start - var prefire: Option[PlanetSideGUID] = None //if WeaponFireMessage precedes ChangeFireStateMessage_Start - var shootingStart: Long = 0 - var shootingStop: Long = 0 + var shooting: mutable.Set[PlanetSideGUID] = mutable.Set.empty //ChangeFireStateMessage_Start + var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start + var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() + var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() var shotsWhileDead: Int = 0 var accessedContainer: Option[PlanetSideGameObject with Container] = None var connectionState: Int = 25 @@ -196,7 +195,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con var deadState: DeadState.Value = DeadState.Dead val projectiles: Array[Option[Projectile]] = Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None) - val projectilesToCleanUp: Array[Boolean] = Array.fill[Boolean](Projectile.rangeUID - Projectile.baseUID)(false) var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons var updateSquad: () => Unit = NoSquadUpdates var recentTeleportAttempt: Long = 0 @@ -331,13 +329,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //TODO put any temporary values back into the avatar squadService ! Service.Leave(Some(s"${avatar.faction}")) if (player != null && player.HasGUID) { - prefire.orElse(shooting) match { - case Some(guid) => - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Stop(player.GUID, guid) - ) - case None => ; + (prefire ++ shooting).foreach { guid => + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeFireState_Stop(player.GUID, guid) + ) } } } @@ -346,16 +342,23 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con context.stop(chatActor) } - def ValidObject(id: Int): Option[PlanetSideGameObject] = ValidObject(Some(PlanetSideGUID(id))) + def ValidObject(id: Int): Option[PlanetSideGameObject] = ValidObject(Some(PlanetSideGUID(id)), decorator = "") - def ValidObject(id: PlanetSideGUID): Option[PlanetSideGameObject] = ValidObject(Some(id)) + def ValidObject(id: Int, decorator: String): Option[PlanetSideGameObject] = ValidObject(Some(PlanetSideGUID(id)), decorator) - def ValidObject(id: Option[PlanetSideGUID]): Option[PlanetSideGameObject] = { + def ValidObject(id: PlanetSideGUID): Option[PlanetSideGameObject] = ValidObject(Some(id), decorator = "") + + def ValidObject(id: PlanetSideGUID, decorator: String): Option[PlanetSideGameObject] = ValidObject(Some(id), decorator) + + def ValidObject(id: Option[PlanetSideGUID]): Option[PlanetSideGameObject] = ValidObject(id, decorator = "") + + def ValidObject(id: Option[PlanetSideGUID], decorator: String): Option[PlanetSideGameObject] = { + val elevatedDecorator = if (decorator.nonEmpty) decorator else "ValidObject" id match { case Some(guid) => val hint = oldRefsMap.getOrElse(guid, "thing") continent.GUID(guid) match { - case Some(obj : LocalProjectile) => + case Some(obj: LocalProjectile) => FindProjectileEntry(guid) case Some(_: LocalLockerItem) => @@ -365,7 +368,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case None => //delete stale entity reference from client log.warn( - s"ValidObject: ${player.Name} is looking for an invalid GUID $guid, believing it a $hint in ${player.Sex.possessive} locker" + s"$elevatedDecorator: ${player.Name} is looking for an invalid GUID $guid, believing it a $hint in ${player.Sex.possessive} locker" ) sendResponse(ObjectDeleteMessage(guid, 0)) None @@ -373,10 +376,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(obj) if obj.HasGUID && obj.GUID != guid => log.error( - s"ValidObject: ${player.Name} found a ${obj.Definition.Name} that isn't the $hint ${player.Sex.pronounSubject} thought it was in zone ${continent.id}" + s"$elevatedDecorator: ${player.Name} found a ${obj.Definition.Name} that isn't the $hint ${player.Sex.pronounSubject} thought it was in zone ${continent.id}" ) log.debug( - s"ValidObject: potentially fatal error in ${continent.id} - requested $hint with $guid, got ${obj.Definition.Name} with ${obj.GUID}; mismatch" + s"$elevatedDecorator: potentially fatal error in ${continent.id} - requested $hint with $guid, got ${obj.Definition.Name} with ${obj.GUID}; mismatch" ) None @@ -385,7 +388,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case None if !id.contains(PlanetSideGUID(0)) => //delete stale entity reference from client - log.error(s"${player.Name} has an invalid reference to $hint with GUID $guid in zone ${continent.id}") + log.error(s"$elevatedDecorator: ${player.Name} has an invalid reference to $hint with GUID $guid in zone ${continent.id}") sendResponse(ObjectDeleteMessage(guid, 0)) None @@ -422,8 +425,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con */ case SetAvatar(avatar) => - if (session.player != null) session.player.avatar = avatar session = session.copy(avatar = avatar) + if (session.player != null) { + session.player.avatar = avatar + } LivePlayerList.Update(avatar.id, avatar) case AvatarActor.AvatarResponse(avatar) => @@ -1176,6 +1181,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case NewPlayerLoaded(tplayer) => //new zone log.info(s"${tplayer.Name} has spawned into ${session.zone.id}") + oldRefsMap.clear() persist = UpdatePersistenceAndRefs tplayer.avatar = avatar session = session.copy(player = tplayer) @@ -1347,40 +1353,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con session = session.copy(account = account) avatarActor ! AvatarActor.SetAccount(account) - case LoadedRemoteProjectile(projectile_guid, Some(projectile)) => - if (projectile.profile.ExistsOnRemoteClients) { - //spawn projectile on other clients - val definition = projectile.Definition - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.LoadProjectile( - player.GUID, - definition.ObjectId, - projectile_guid, - definition.Packet.ConstructorData(projectile).get - ) - ) - } - //immediately slated for deletion? - CleanUpRemoteProjectile(projectile.GUID, projectile) - - case LoadedRemoteProjectile(projectile_guid, None) => - continent.GUID(projectile_guid) match { - case Some(obj: Projectile) if obj.profile.ExistsOnRemoteClients => - //spawn projectile on other clients - val definition = obj.Definition - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.LoadProjectile( - player.GUID, - definition.ObjectId, - projectile_guid, - definition.Packet.ConstructorData(obj).get - ) - ) - case _ => ; - } - case PlayerToken.LoginInfo(name, Zone.Nowhere, _) => log.info(s"LoginInfo: player $name is considered a new character") //TODO poll the database for saved zone and coordinates? @@ -2026,7 +1998,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //cleanup sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, false)) (old_holsters ++ old_inventory ++ delete).foreach { - case (_, guid) => sendResponse(ObjectDeleteMessage(guid, 0)) + case (_, dguid) => sendResponse(ObjectDeleteMessage(dguid, 0)) } //functionally delete delete.foreach { case (obj, _) => TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) } @@ -2401,9 +2373,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(vehicle: MountableWeapons) => vehicle.PassengerInSeat(player) match { case Some(seat_num: Int) => - vehicle.WeaponControlledFromSeat(seat_num) match { - case Some(equipment) if equipment.GUID == weapon_guid => - val weapon = equipment.asInstanceOf[Tool] + vehicle.WeaponControlledFromSeat(seat_num) foreach { + case weapon: Tool if weapon.GUID == weapon_guid => sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine)) case _ => ; } @@ -2443,13 +2414,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the ${obj.Definition.Name} in ${obj.SeatPermissionGroup(seat_number) match { case Some(AccessPermissionGroup.Driver) => "the driver seat" - case Some(seatType) => s"a $seatType seat, #$seat_number" + case Some(seatType) => s"a $seatType seat (#$seat_number)" case None => "a seat" }}") val obj_guid: PlanetSideGUID = obj.GUID CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, 0, obj.Health)) - sendResponse(PlanetsideAttributeMessage(obj_guid, 68, obj.Shields)) + sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) if (obj.Definition == GlobalDefinitions.ant) { sendResponse(PlanetsideAttributeMessage(obj_guid, 45, obj.NtuCapacitorScaled)) } @@ -2462,6 +2433,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //phantasm doesn't uncloak if the driver is uncloaked and no other vehicle cloaks obj.Cloaked = tplayer.Cloaked } + sendResponse(GenericObjectActionMessage(obj_guid, 11)) } else if (obj.WeaponControlledFromSeat(seat_number).isEmpty) { keepAliveFunc = KeepAlivePersistence } @@ -2551,9 +2523,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val player_guid: PlanetSideGUID = tplayer.GUID if (player_guid == player.GUID) { //disembarking self - log.info( - s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name} from seat #$seat_num" - ) + log.info(s"${player.Name} dismounts the ${obj.Definition.Name}'s ${obj.SeatPermissionGroup(seat_num) match { + case Some(AccessPermissionGroup.Driver) => "driver seat" + case Some(seatType) => s"$seatType seat (#$seat_num)" + case None => "seat" + }}") ConditionalDriverVehicleControl(obj) UnaccessContainer(obj) DismountAction(tplayer, obj, seat_num) @@ -2603,11 +2577,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con tplayer.avatar.purchaseCooldown(item.Definition) match { case Some(_) => lastTerminalOrderFulfillment = true - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false)) + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) case None => avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition) TaskWorkflow.execute(BuyNewEquipmentPutInInventory( - continent.GUID(tplayer.VehicleSeated) match { case Some(v : Vehicle) => v; case _ => player }, + continent.GUID(tplayer.VehicleSeated) match { case Some(v: Vehicle) => v; case _ => player }, tplayer, msg.terminal_guid )(item)) @@ -2670,7 +2644,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con vTrunk.InsertQuickly(entry.start, entry.obj) }) TaskWorkflow.execute(registerVehicleFromSpawnPad(vehicle, pad, term)) - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = true)) + if(GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) { + sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid)) + } case _ => log.error( s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it" @@ -2687,10 +2664,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con s"${msg.transaction_type} order" } log.warn(s"NoDeal: ${tplayer.Name} made a request but the terminal rejected the $order") - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, false)) + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, success = false)) lastTerminalOrderFulfillment = true - case _ => ; + case _ => + val transaction = msg.transaction_type + log.warn(s"n/a: ${tplayer.Name} made a $transaction request but terminal#${msg.terminal_guid.guid} is missing or wrong") + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, transaction, success = false)) + lastTerminalOrderFulfillment = true } } @@ -2740,6 +2721,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(pkt) } + case VehicleResponse.FrameVehicleState(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA) => + if (tplayer_guid != guid) { + sendResponse(FrameVehicleStateMessage(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA)) + } + + case VehicleResponse.GenericObjectAction(object_guid, action) => + if (tplayer_guid != guid) { + sendResponse(GenericObjectActionMessage(object_guid, action)) + } + case VehicleResponse.HitHint(source_guid) => if (player.isAlive) { sendResponse(HitHint(source_guid, player.GUID)) @@ -2958,6 +2949,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) } ApplyPurchaseTimersBeforePackingLoadout(player, vehicle, added_weapons ++ new_inventory) + //jammer or unjamm new weapons based on vehicle status + val vehicleJammered = vehicle.Jammed + added_weapons + .map { _.obj } + .collect { + case jamItem: JammableUnit if jamItem.Jammed != vehicleJammered => + jamItem.Jammed = vehicleJammered + JammableMountedWeapons.JammedWeaponStatus(vehicle.Zone, jamItem, vehicleJammered) + } } else if (accessedContainer.contains(target)) { //external participant: observe changes to equipment (old_weapons ++ old_inventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, 0)) } @@ -3133,10 +3133,21 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(guid, 53, 1)) } sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0))) - (1 to 73).foreach(i => { - // not all GUID's are set, and not all of the set ones will always be zero; what does this section do? - sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(i), 67, 0)) - }) + //these are facilities and towers and bunkers in the zone, but not necessarily all of them for some reason + //for standard zones, facilities are 1, towers and bunkers are 0 + //for standard zone facilities in a position for valid vehicle gate shield benefits, 1 activates that shield + //for caverns, who knows what this does + //why is this all set in bulk? + continent.Buildings + .filter { case (_, building) => + val buildingType = building.BuildingType + buildingType == StructureType.Facility || + buildingType == StructureType.Tower || + buildingType == StructureType.Bunker + } + .foreach { case (_, building) => + sendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0/*building.BuildingType == StructureType.Facility*/)) + } (0 to 30).foreach(i => { //TODO 30 for a new character only? sendResponse(AvatarStatisticsMessage(2, Statistics(0L))) @@ -3194,7 +3205,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } //positive shield strength if (vehicle.Definition.MaxShields > 0) { - sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 68, vehicle.Shields)) + sendResponse(PlanetsideAttributeMessage(vehicle.GUID, vehicle.Definition.shieldUiAttribute, vehicle.Shields)) } // ANT capacitor if (vehicle.Definition == GlobalDefinitions.ant) { @@ -3400,15 +3411,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case msg @ BeginZoningMessage() => log.trace(s"BeginZoningMessage: ${player.Name} is reticulating ${continent.id}'s splines ...") zoneLoaded = None + val name = avatar.name val continentId = continent.id val faction = player.Faction val factionChannel = s"$faction" continent.AvatarEvents ! Service.Join(continentId) continent.AvatarEvents ! Service.Join(factionChannel) - continent.LocalEvents ! Service.Join(avatar.name) + continent.LocalEvents ! Service.Join(name) continent.LocalEvents ! Service.Join(continentId) continent.LocalEvents ! Service.Join(factionChannel) - continent.VehicleEvents ! Service.Join(avatar.name) + continent.VehicleEvents ! Service.Join(name) continent.VehicleEvents ! Service.Join(continentId) continent.VehicleEvents ! Service.Join(factionChannel) if (connectionState != 100) configZone(continent) @@ -3563,7 +3575,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } val allActiveVehicles = vehicles ++ usedVehicle //active vehicles (and some wreckage) - vehicles.foreach(vehicle => { + vehicles.foreach { vehicle => val vguid = vehicle.GUID val vdefinition = vehicle.Definition sendResponse( @@ -3593,7 +3605,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) ) } - }) + vehicle.SubsystemMessages().foreach { sendResponse } + } vehicles.collect { case vehicle if vehicle.Faction == faction => Vehicles.ReloadAccessPermissions(vehicle, player.Name) @@ -3601,6 +3614,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //our vehicle would have already been loaded; see NewPlayerLoaded/AvatarCreate usedVehicle.headOption match { case Some(vehicle) => + //subsystems + vehicle.Actor ! Vehicle.UpdateSubsystemStates(player.Name, Some(false)) //depict any other passengers already in this zone val vguid = vehicle.GUID vehicle.Seats @@ -3633,7 +3648,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } //positive shield strength if (vehicle.Shields > 0) { - sendResponse(PlanetsideAttributeMessage(vguid, 68, vehicle.Shields)) + sendResponse(PlanetsideAttributeMessage(vguid, vehicle.Definition.shieldUiAttribute, vehicle.Shields)) } case _ => ; //no vehicle } @@ -3682,7 +3697,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deployed, 0, false, Vector3.Zero)) ToggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) } - val name = avatar.name serviceManager .ask(Lookup("hart"))(Timeout(2 seconds)) .onComplete { @@ -3690,7 +3704,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ref ! HartTimer.Update(continentId, name) case _ => } - //implant terminals continent.map.terminalToInterface.foreach({ case (terminal_guid, interface_guid) => @@ -3727,8 +3740,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case _ => ; } }) - - //base turrets + //facility turrets continent.map.turretToWeapon .map { case (turret_guid: Int, _) => continent.GUID(turret_guid) } .collect { @@ -3736,8 +3748,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val pguid = turret.GUID //attached weapon if (!turret.isUpgrading) { - turret.ControlledWeapon(wepNumber = 1) match { - case Some(obj: Tool) => + turret.ControlledWeapon(wepNumber = 1).foreach { + case obj: Tool => val objDef = obj.Definition sendResponse( ObjectCreateMessage( @@ -3767,6 +3779,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case _ => ; } } + //remote projectiles and radiation clouds + continent.Projectiles.foreach { projectile => + val definition = projectile.Definition + sendResponse( + ObjectCreateMessage( + definition.ObjectId, + projectile.GUID, + definition.Packet.ConstructorData(projectile).get + ) + ) + } + //spawn point update request continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.UpdateAmsSpawnPoint(continent) @@ -3876,37 +3900,33 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con player.zoneInteractions() case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) => - //the majority of the following check retrieves information to determine if we are in control of the child - FindContainedWeapon match { - case (Some(o), Some(tool)) => - (o match { - case mount: Mountable => (o, mount.PassengerInSeat(player)) - case _ => (None, None) - }) match { - case (None, None) | (_, None) | (_: Vehicle, Some(0)) => ; - case _ => - persist() - turnCounterFunc(player.GUID) - } - if (tool.GUID == object_guid) { - //TODO set tool orientation? - player.Orientation = Vector3(0f, pitch, yaw) - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw) - ) - } else { - log.warn( - s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}" - ) - } - case (Some(obj), None) => - log.warn( - s"ChildObjectState: ${player.Name} can not find any controllable agent, let alone entity ${object_guid.guid}" - ) - case (None, _) => ; - //TODO status condition of "playing getting out of vehicle to allow for late packets without warning + val (o, tools) = FindContainedWeapon + //is COSM our primary upstream packet? + (o match { + case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) + case _ => (None, None) + }) match { + case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => ; + case _ => + persist() + turnCounterFunc(player.GUID) } + //the majority of the following check retrieves information to determine if we are in control of the child + tools.find { _.GUID == object_guid } match { + case None => + //todo: old warning; this state is problematic, but can trigger in otherwise valid instances + //log.warn( + // s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}" + //) + case Some(tool) => + //TODO set tool orientation? + player.Orientation = Vector3(0f, pitch, yaw) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw) + ) + } + //TODO status condition of "playing getting out of vehicle to allow for late packets without warning if (player.death_by == -1) { KickedByAdministration() } @@ -3917,7 +3937,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con pos, ang, vel, - flying, + is_flying, unk6, unk7, wheels, @@ -3946,7 +3966,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con obj.Velocity = Some(Vector3.Zero) } if (obj.Definition.CanFly) { - obj.Flying = flying //usually Some(7) + obj.Flying = is_flying //usually Some(7) } obj.Cloaked = obj.Definition.CanCloak && is_cloaked } else { @@ -3963,7 +3983,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ang, obj.Velocity, if (obj.isFlying) { - flying + is_flying } else { None }, @@ -3991,7 +4011,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case msg @ VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, unk2) => //log.info(s"msg") - ValidObject(vehicle_guid) match { + ValidObject(vehicle_guid, decorator = "VehicleSubState") match { case Some(obj: Vehicle) => obj.Position = pos obj.Orientation = ang @@ -4016,10 +4036,104 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) ) - case _ => - log.warn( - s"VehicleSubState: ${player.Name} should not be dispatching this kind of packet for vehicle #${vehicle_guid.guid}" + case _ => ; + } + + case msg @ FrameVehicleStateMessage( + vehicle_guid, + unk1, + pos, + ang, + vel, + unk2, + unk3, + unk4, + is_crouched, + is_airborne, + ascending_flight, + flight_time, + unk9, + unkA + ) => + //log.info(s"$msg") + GetVehicleAndSeat() match { + case (Some(obj), Some(0)) => + //we're driving the vehicle + persist() + turnCounterFunc(player.GUID) + val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { + case Some(v: Vehicle) => + updateBlockMap(obj, continent, pos) + (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) + case _ => + (pos, ang, vel, true) + } + player.Position = position //convenient + if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) { + player.Orientation = Vector3.z(ang.z) //convenient + } + obj.Position = position + obj.Orientation = angle + obj.Velocity = velocity +// if (is_crouched && obj.DeploymentState != DriveState.Kneeling) { +// //dev stuff goes here +// } +// else +// if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) { +// //dev stuff goes here +// } + obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile + if (notMountedState) { + if (obj.DeploymentState != DriveState.Kneeling) { + if (is_airborne) { + val flight = if (ascending_flight) flight_time else -flight_time + obj.Flying = Some(flight) + obj.Actor ! BfrFlight.Soaring(flight) + } else if (obj.Flying.nonEmpty) { + obj.Flying = None + obj.Actor ! BfrFlight.Landed + } + } else { + obj.Velocity = None + obj.Flying = None + } + obj.zoneInteractions() + } else { + obj.Velocity = None + obj.Flying = None + } + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.FrameVehicleState( + player.GUID, + vehicle_guid, + unk1, + position, + angle, + velocity, + unk2, + unk3, + unk4, + is_crouched, + is_airborne, + ascending_flight, + flight_time, + unk9, + unkA + ) ) + updateSquad() + case (None, _) => + //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") + //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle + case (_, Some(index)) => + log.error( + s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)" + ) + case _ => ; + } + if (player.death_by == -1) { + KickedByAdministration() } case msg @ ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) => @@ -4044,7 +4158,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) ) case _ if seq == 0 => - /* missing the first packet in the sequence is permissible */ + /* missing the first packet in the sequence is permissible */ case _ => log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found") } @@ -4052,8 +4166,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case msg @ LongRangeProjectileInfoMessage(guid, _, _) => //log.info(s"$msg") FindContainedWeapon match { - case (Some(vehicle: Vehicle), Some(weapon: Tool)) - if weapon.GUID == guid => ; //now what? + case (Some(vehicle: Vehicle), weapons) + if weapons.exists { _.GUID == guid } => ; //now what? case _ => ; } @@ -4098,24 +4212,32 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(VoiceHostKill()) case msg @ ChangeAmmoMessage(item_guid, unk1) => - FindContainedEquipment match { - case (Some(_), Some(obj: ConstructionItem)) => - if (Deployables.performConstructionItemAmmoChange(player.avatar.certifications, obj, obj.AmmoTypeIndex)) { - log.info( - s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${obj.AmmoType} (option #${obj.FireModeIndex})" - ) - sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) - } - case (Some(obj: PlanetSideServerObject), Some(tool: Tool)) => - PerformToolAmmoChange(tool, obj) - case (_, Some(obj)) => - log.warn(s"ChangeAmmo: the ${obj.Definition.Name} in ${player.Name}'s hands does not contain ammunition") - case (_, None) => - log.warn(s"ChangeAmmo: can not find $item_guid") + val (thing, equipment) = FindContainedEquipment() + if(equipment.isEmpty) { + log.warn(s"ChangeAmmo: either can not find $item_guid or the object found was not Equipment") + } else { + equipment foreach { + case obj : ConstructionItem => + if (Deployables.performConstructionItemAmmoChange(player.avatar.certifications, obj, obj.AmmoTypeIndex)) { + log.info( + s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${obj.AmmoType} (option #${obj.FireModeIndex})" + ) + sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) + } + case tool : Tool => + thing match { + case Some(correctThing: PlanetSideServerObject with Container) => + PerformToolAmmoChange(tool, correctThing) + case _ => + log.warn(s"ChangeAmmo: the ${thing.get.Definition.Name} in ${player.Name}'s is not the correct type") + } + case obj => + log.warn(s"ChangeAmmo: the ${obj.Definition.Name} in ${player.Name}'s hands does not contain ammunition") + } } case msg @ ChangeFireModeMessage(item_guid, fire_mode) => - FindEquipment match { + FindEquipment(item_guid) match { case Some(obj: PlanetSideGameObject with FireModeSwitch[_]) => val originalModeIndex = obj.FireModeIndex if (obj match { @@ -4150,12 +4272,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case msg @ ChangeFireStateMessage_Start(item_guid) => if (shooting.isEmpty) { - FindEquipment match { + FindEquipment(item_guid) match { case Some(tool: Tool) => - if (tool.Magazine > 0 || prefire.contains(item_guid)) { - prefire = None - shooting = Some(item_guid) - shootingStart = System.currentTimeMillis() + if (tool.FireMode.RoundsPerShot == 0 || tool.Magazine > 0 || prefire.contains(item_guid)) { + prefire -= item_guid + shooting += item_guid + shootingStart += item_guid -> System.currentTimeMillis() //special case - suppress the decimator's alternate fire mode, by projectile if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) { continent.AvatarEvents ! AvatarServiceMessage( @@ -4181,9 +4303,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con EmptyMagazine(item_guid, tool) } case Some(_) => //permissible, for now - prefire = None - shooting = Some(item_guid) - shootingStart = System.currentTimeMillis() + prefire -= item_guid + shooting += item_guid + shootingStart += item_guid -> System.currentTimeMillis() continent.AvatarEvents ! AvatarServiceMessage( continent.id, AvatarAction.ChangeFireState_Start(player.GUID, item_guid) @@ -4194,13 +4316,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case msg @ ChangeFireStateMessage_Stop(item_guid) => - prefire = None - shootingStop = System.currentTimeMillis() - if (shooting.contains(item_guid)) { - shooting = None - } + prefire -= item_guid + shootingStop += item_guid -> System.currentTimeMillis() + shooting -= item_guid val pguid = player.GUID - FindEquipment match { + FindEquipment(item_guid) match { case Some(tool: Tool) => //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why if ( @@ -4212,7 +4332,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.id, AvatarAction.ChangeFireState_Start(pguid, item_guid) ) - shootingStart = System.currentTimeMillis() - 1L + shootingStart += item_guid -> (System.currentTimeMillis() - 1L) } tool.FireMode match { case mode: ChargeFireModeDefinition => @@ -4248,7 +4368,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(EmoteMsg(avatar_guid, emote)) case msg @ DropItemMessage(item_guid) => - ValidObject(item_guid) match { + ValidObject(item_guid, decorator = "DropItem") match { case Some(anItem: Equipment) => player.FreeHand.Equipment match { case Some(item) => @@ -4262,19 +4382,19 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } case None => - log.warn(s"DropItem: ${player.Name} wanted to drop a $anItem, but it wasn't at hand") + continent.GUID(player.VehicleSeated) match { + case Some(_) => ; //in a vehicle, suppress the warning message + case None => + log.warn(s"DropItem: ${player.Name} wanted to drop a $anItem, but it wasn't at hand") + } } case Some(obj) => log.warn(s"DropItem: ${player.Name} wanted to drop a $obj, but that isn't possible") - case None => - sendResponse(ObjectDeleteMessage(item_guid, 0)) //this is fine; item doesn't exist to the server anyway - log.warn( - s"DropItem: ${player.Name} wanted to drop an item ${item_guid.guid}, but it was nowhere to be found" - ) + case None => ; } case msg @ PickupItemMessage(item_guid, player_guid, unk1, unk2) => - ValidObject(item_guid) match { + ValidObject(item_guid, decorator = "PickupItem") match { case Some(item: Equipment) => player.Fit(item) match { case Some(_) => @@ -4283,63 +4403,61 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case None => //skip sendResponse(ActionResultMessage.Fail(16)) //error code? } - case _ => - log.warn( - s"PickupItem: ${player.Name} requested an item ${item_guid.guid} that doesn't seem to exist" - ) + case _ => ; } case msg @ ReloadMessage(item_guid, ammo_clip, unk1) => FindContainedWeapon match { - case (Some(obj: PlanetSideServerObject with Container), Some(tool: Tool)) => - val currentMagazine: Int = tool.Magazine - val magazineSize: Int = tool.MaxMagazine - val reloadValue: Int = magazineSize - currentMagazine - if (magazineSize > 0 && reloadValue > 0) { - FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match { - case Nil => ; - case x :: xs => - val (deleteFunc, modifyFunc): (Equipment => Future[Any], (AmmoBox, Int) => Unit) = obj match { - case veh: Vehicle => - (RemoveOldEquipmentFromInventory(veh), ModifyAmmunitionInVehicle(veh)) - case _ => - (RemoveOldEquipmentFromInventory(obj), ModifyAmmunition(obj)) - } - xs.foreach { item => deleteFunc(item.obj) } - val box = x.obj.asInstanceOf[AmmoBox] - val tailReloadValue: Int = if (xs.isEmpty) { - 0 - } else { - xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum - } - val sumReloadValue: Int = box.Capacity + tailReloadValue - val actualReloadValue = if (sumReloadValue <= reloadValue) { - deleteFunc(box) - sumReloadValue - } else { - modifyFunc(box, reloadValue - tailReloadValue) - reloadValue - } - val finalReloadValue = actualReloadValue + currentMagazine - log.info( - s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}" - ) - tool.Magazine = finalReloadValue - sendResponse(ReloadMessage(item_guid, finalReloadValue, unk1)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.Reload(player.GUID, item_guid) - ) + case (Some(obj: PlanetSideServerObject with Container), tools) => + tools.filter { _.GUID == item_guid }.foreach { tool => + val currentMagazine : Int = tool.Magazine + val magazineSize : Int = tool.MaxMagazine + val reloadValue : Int = magazineSize - currentMagazine + if (magazineSize > 0 && reloadValue > 0) { + FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match { + case Nil => ; + case x :: xs => + val (deleteFunc, modifyFunc) : (Equipment => Future[Any], (AmmoBox, Int) => Unit) = obj match { + case veh : Vehicle => + (RemoveOldEquipmentFromInventory(veh), ModifyAmmunitionInVehicle(veh)) + case _ => + (RemoveOldEquipmentFromInventory(obj), ModifyAmmunition(obj)) + } + xs.foreach { item => deleteFunc(item.obj) } + val box = x.obj.asInstanceOf[AmmoBox] + val tailReloadValue : Int = if (xs.isEmpty) { + 0 + } + else { + xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum + } + val sumReloadValue : Int = box.Capacity + tailReloadValue + val actualReloadValue = if (sumReloadValue <= reloadValue) { + deleteFunc(box) + sumReloadValue + } + else { + modifyFunc(box, reloadValue - tailReloadValue) + reloadValue + } + val finalReloadValue = actualReloadValue + currentMagazine + log.info( + s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}" + ) + tool.Magazine = finalReloadValue + sendResponse(ReloadMessage(item_guid, finalReloadValue, unk1)) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.Reload(player.GUID, item_guid) + ) + } + } else { + //the weapon can not reload due to full magazine; the UI for the magazine is obvious bugged, so fix it + sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, magazineSize)) } - } else { - log.warn( - s"ReloadMessage: the ${tool.Definition.Name} under ${player.Name}'s control can not reload (full=$magazineSize, want=$reloadValue)" - ) } - case (_, Some(_)) => - log.warn(s"ReloadMessage: the object that was found for $item_guid was not a Tool") - case (_, None) => - log.warn(s"ReloadMessage: can not find $item_guid") + case (_, _) => + log.warn(s"ReloadMessage: either can not find $item_guid or the object found was not a Tool") } case msg @ ObjectHeldMessage(avatar_guid, held_holsters, unk1) => @@ -4431,7 +4549,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case msg @ RequestDestroyMessage(object_guid) => // TODO: Make sure this is the correct response for all cases - ValidObject(object_guid) match { + ValidObject(object_guid, decorator = "RequestDestroy") match { case Some(vehicle: Vehicle) => /* line 1a: player is admin (and overrules other access requirements) */ /* line 1b: vehicle and player (as the owner) acknowledge each other */ @@ -4452,20 +4570,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case Some(obj: Projectile) => - if (obj.isResolved) { - log.warn( - s"RequestDestroy: tried to clean up projectile ${object_guid.guid} but it was already resolved" - ) - } else { + if (!obj.isResolved) { obj.Miss() - if (obj.profile.ExistsOnRemoteClients && obj.HasGUID) { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ProjectileExplodes(player.GUID, obj.GUID, obj) - ) - TaskWorkflow.execute(unregisterProjectile(obj)) - } } + continent.Projectile ! ZoneProjectile.Remove(object_guid) case Some(obj: BoomerTrigger) => if (FindEquipmentToDelete(object_guid, obj)) { @@ -4492,21 +4600,24 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(thing) => log.warn(s"RequestDestroy: not allowed to delete this ${thing.Definition.Name}") - case None => - log.warn(s"RequestDestroy: object ${object_guid.guid} not found") + case None => ; } case msg @ ObjectDeleteMessage(object_guid, unk1) => sendResponse(ObjectDeleteMessage(object_guid, 0)) case msg @ MoveItemMessage(item_guid, source_guid, destination_guid, dest, _) => - (continent.GUID(source_guid), continent.GUID(destination_guid), ValidObject(item_guid)) match { + ( + continent.GUID(source_guid), + continent.GUID(destination_guid), + ValidObject(item_guid, decorator = "MoveItem") + ) match { case ( Some(source: PlanetSideServerObject with Container), Some(destination: PlanetSideServerObject with Container), Some(item: Equipment) ) => - ContainableMoveItem(player.Name, source, destination, item, dest) + ContainableMoveItem(player.Name, source, destination, item, destination.SlotMapResolution(dest)) case (None, _, _) => log.error( s"MoveItem: ${player.Name} wanted to move $item_guid from $source_guid, but could not find source object" @@ -4515,8 +4626,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.error( s"MoveItem: ${player.Name} wanted to move $item_guid to $destination_guid, but could not find destination object" ) - case (_, _, None) => - log.error(s"MoveItem: ${player.Name} wanted to move $item_guid, but could not find it") + case (_, _, None) => ; case _ => log.error( s"MoveItem: ${player.Name} wanted to move $item_guid from $source_guid to $destination_guid, but multiple problems were encountered" @@ -4524,7 +4634,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case msg @ LootItemMessage(item_guid, target_guid) => - (ValidObject(item_guid), continent.GUID(target_guid)) match { + (ValidObject(item_guid, decorator = "LootItem"), continent.GUID(target_guid)) match { case (Some(item: Equipment), Some(destination: PlanetSideServerObject with Container)) => //figure out the source ( @@ -4556,8 +4666,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case (Some(obj), _) => log.error(s"LootItem: item $obj is (probably) not lootable to ${player.Name}") - case (None, _) => - log.error(s"LootItem: ${player.Name} can not find $item_guid") + case (None, _) => ; case (_, None) => log.error(s"LootItem: ${player.Name} can not find where to put $item_guid") } @@ -4590,17 +4699,29 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con itemType ) => // TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok) - // TODO: Not all incoming UseItemMessage's respond with another UseItemMessage (i.e. doors only send out GenericObjectStateMsg) - val equipment = player.Slot(player.DrawnSlot).Equipment match { - case out @ Some(item) if item.GUID == item_used_guid => out - case _ => None + val equipment = FindContainedEquipment(item_used_guid) match { + case (o @ Some(_), a) + if a.exists(_.isInstanceOf[Tool]) => + FindEnabledWeaponsToHandleWeaponFireAccountability(o, a.collect { case w: Tool => w })._2.headOption + case (Some(_), a) => + a.headOption + case _ => + None } - ValidObject(object_guid) match { + ValidObject(object_guid, decorator = "UseItem") match { case Some(door: Door) => door.Actor ! CommonMessages.Use(player) case Some(resourceSilo: ResourceSilo) => - resourceSilo.Actor ! CommonMessages.Use(player) + CancelZoningProcessWithDescriptiveReason("cancel_use") + (continent.GUID(player.VehicleSeated), equipment) match { + case (Some(vehicle: Vehicle), Some(item)) + if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && + GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => + resourceSilo.Actor ! CommonMessages.Use(player, equipment) + case _ => + resourceSilo.Actor ! CommonMessages.Use(player) + } case Some(panel: IFFLock) => equipment match { @@ -4971,12 +5092,29 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } + case Some(gate: WarpGate) => + CancelZoningProcessWithDescriptiveReason("cancel_use") + (continent.GUID(player.VehicleSeated), equipment) match { + case (Some(vehicle: Vehicle), Some(item)) + if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && + GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => + vehicle.Actor ! CommonMessages.Use(player, equipment) + case _ => ; + } + case Some(obj) => CancelZoningProcessWithDescriptiveReason("cancel_use") - log.warn(s"UseItem: ${player.Name} does not know how to handle $obj") + equipment match { + case Some(item) + if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) || + GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => ; - case None => - log.error(s"UseItem: ${player.Name} can not find object $object_guid") + case _ => + log.warn(s"UseItem: ${player.Name} does not know how to handle $obj") + + } + + case None => ; } case msg @ ProximityTerminalUseMessage(player_guid, object_guid, _) => @@ -4990,7 +5128,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case msg @ UnuseItemMessage(player_guid, object_guid) => - ValidObject(object_guid) match { + ValidObject(object_guid, decorator = "UnuseItem") match { case Some(obj: Player) => UnaccessContainer(obj) TryDisposeOfLootedCorpse(obj) @@ -5037,23 +5175,44 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case msg @ GenericObjectActionMessage(object_guid, code) => - log.debug(s"$msg") - ValidObject(object_guid) match { - case Some(vehicle: Vehicle) => - if (code == 55) { - //apc emp - vehicle.Actor ! SpecialEmp.Burst() - } + //log.info(s"$msg") + ValidObject(object_guid, decorator = "GenericObjectAction") match { + case Some(vehicle: Vehicle) + if vehicle.OwnerName.contains(player.Name) => + vehicle.Actor ! ServerObject.GenericObjectAction(object_guid, code, Some(player.GUID)) + case Some(tool: Tool) => - if (tool.Definition == GlobalDefinitions.maelstrom && code == 35) { - //maelstrom primary fire mode effect (no target) + if (code == 35 && + (tool.Definition == GlobalDefinitions.maelstrom || tool.Definition.Name.startsWith("aphelion_laser")) + ) { + //maelstrom primary fire mode discharge (no target) + //aphelion_laser discharge (no target) HandleWeaponFireAccountability(object_guid, PlanetSideGUID(Projectile.baseUID)) + } else { + ValidObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") match { + case Some(vehicle: Vehicle) + if vehicle.OwnerName.contains(player.Name) => + vehicle.Actor ! ServerObject.GenericObjectAction(object_guid, code, Some(tool)) + case _ => ; + } + } + case _ => ; + } + + case msg @ GenericObjectActionAtPositionMessage(object_guid, _, _) => + //log.info(s"$msg") + ValidObject(object_guid, decorator = "GenericObjectActionAtPosition") match { + case Some(tool: Tool) if GlobalDefinitions.isBattleFrameNTUSiphon(tool.Definition) => + FindContainedWeapon match { + case (Some(vehicle: Vehicle), weps) if weps.exists(_.GUID == object_guid) => + vehicle.Actor ! SpecialEmp.Burst() + case _ => ; } case _ => ; } case msg @ GenericObjectStateMsg(object_guid, unk1) => - log.debug("GenericObjectState: " + msg) + log.debug(s"$msg") case msg @ GenericActionMessage(action) => if (player == null) { @@ -5183,16 +5342,21 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case msg @ FavoritesRequest(player_guid, loadoutType, action, line, label) => CancelZoningProcessWithDescriptiveReason("cancel_use") action match { - case FavoritesAction.Save => avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line) - case FavoritesAction.Delete => avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line) + case FavoritesAction.Save => + avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line) + case FavoritesAction.Delete => + avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line) case FavoritesAction.Unknown => log.warn(s"FavoritesRequest: ${player.Name} requested an unknown favorites action") } - case msg @ WeaponDelayFireMessage(seq_time, weapon_guid) => ; + case msg @ WeaponDelayFireMessage(seq_time, weapon_guid) => + log.info(s"$msg") case msg @ WeaponDryFireMessage(weapon_guid) => - FindWeapon.orElse { continent.GUID(weapon_guid) } match { + FindWeapon + .find { _.GUID == weapon_guid } + .orElse { continent.GUID(weapon_guid) } match { case Some(_: Equipment) => continent.AvatarEvents ! AvatarServiceMessage( continent.id, @@ -5230,8 +5394,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.info(s"${player.Name} is lazing a position$purpose") case msg @ ObjectDetectedMessage(guid1, guid2, unk, targets) => - FindWeapon match { - case Some(weapon) if weapon.Projectile.AutoLock => + FindWeapon.foreach { + case weapon if weapon.Projectile.AutoLock => //projectile with auto-lock instigates a warning on the target val detectedTargets = FindDetectedProjectileTargets(targets) if (detectedTargets.nonEmpty) { @@ -5260,7 +5424,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con (hit_info match { case Some(hitInfo) => val hitPos = hitInfo.hit_pos - ValidObject(hitInfo.hitobject_guid) match { + ValidObject(hitInfo.hitobject_guid, decorator = "Hit/hitInfo") match { case _ if projectile.profile == GlobalDefinitions.flail_projectile => val radius = projectile.profile.DamageRadius * projectile.profile.DamageRadius val targets = Zone.findAllTargets(hitPos)(continent, player, projectile.profile) @@ -5269,43 +5433,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } targets.map { target => CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target) - (target, hitPos, target.Position) + (target, projectile, hitPos, target.Position) } case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target) - List((target, hitInfo.shot_origin, hitPos)) + List((target, projectile, hitInfo.shot_origin, hitPos)) - case None if projectile.profile.DamageProxy.getOrElse(0) > 0 => - //server-side maelstrom grenade target selection - if (projectile.tool_def == GlobalDefinitions.maelstrom) { - val shotOrigin = hitInfo.shot_origin - val radius = projectile.profile.LashRadius * projectile.profile.LashRadius - val targets = continent.blockMap - .sector(hitPos, projectile.profile.LashRadius) - .livePlayerList - .filter { target => - Vector3.DistanceSquared(target.Position, hitPos) <= radius - } - //chainlash is separated from the actual damage application for convenience - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.SendResponse( - PlanetSideGUID(0), - ChainLashMessage( - hitPos, - projectile.profile.ObjectId, - targets.map { _.GUID } - ) - ) - ) - targets.map { target => - CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target) - (target, hitPos, target.Position) - } - } else { - Nil - } + case None => + HandleDamageProxy(projectile, projectile_guid, hitPos) case _ => Nil @@ -5316,10 +5452,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con .foreach({ case ( target: PlanetSideGameObject with FactionAffinity with Vitality, + proj: Projectile, shotOrigin: Vector3, hitPos: Vector3 ) => - ResolveProjectileInteraction(projectile, DamageResolution.Hit, target, hitPos) match { + ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos) match { case Some(resprojectile) => HandleDealingDamage(target, resprojectile) case None => ; @@ -5353,7 +5490,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con (DamageResolution.Splash, DamageResolution.Splash) } //direct_victim_uid - ValidObject(direct_victim_uid) match { + ValidObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match { case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target) ResolveProjectileInteraction(projectile, resolution1, target, target.Position) match { @@ -5365,7 +5502,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } //other victims targets.foreach(elem => { - ValidObject(elem.uid) match { + ValidObject(elem.uid, decorator = "SplashHit/other_victims") match { case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos) match { @@ -5376,6 +5513,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case _ => ; } }) + //... + HandleDamageProxy(projectile, projectile_guid, explosion_pos) if ( projectile.profile.HasJammedEffectDuration || projectile.profile.JammerProjectile || @@ -5395,9 +5534,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //cleanup val localIndex = projectile_guid.guid - Projectile.baseUID if (projectile.HasGUID) { - CleanUpRemoteProjectile(projectile.GUID, projectile, localIndex) - } else { - projectilesToCleanUp(localIndex) = true + continent.Projectile ! ZoneProjectile.Remove(projectile.GUID) } } case None => ; @@ -5405,7 +5542,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case msg @ LashMessage(seq_time, killer_guid, victim_guid, projectile_guid, hit_pos, unk1) => log.trace(s"${player.Name} lashes some targets - $msg") - ValidObject(victim_guid) match { + ValidObject(victim_guid, decorator = "Lash") match { case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target) ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos) match { @@ -5457,12 +5594,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } case msg @ MountVehicleMsg(player_guid, mountable_guid, entry_point) => - ValidObject(mountable_guid) match { + ValidObject(mountable_guid, decorator = "MountVehicle") match { case Some(obj: Mountable) => obj.Actor ! Mountable.TryMount(player, entry_point) - - case None | Some(_) => + case Some(_) => log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}") + case None => ; } case msg @ DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) => @@ -5526,7 +5663,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //kicking someone else out of a mount; need to own that mount/mountable player.avatar.vehicle match { case Some(obj_guid) => - ((ValidObject(obj_guid), ValidObject(player_guid)) match { + ( + ( + ValidObject(obj_guid, decorator = "DismountVehicle/Vehicle"), + ValidObject(player_guid, decorator = "DismountVehicle/Player") + ) match { case (vehicle @ Some(obj: Vehicle), tplayer) => if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None) case (mount @ Some(obj: Mountable), tplayer) => @@ -5618,7 +5759,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con 0f } } - val (target1, target2, bailProtectStatus, velocity) = (ctype, ValidObject(p)) match { + val (target1, target2, bailProtectStatus, velocity) = (ctype, ValidObject(p, decorator = "GenericCollision/Primary")) match { case (CollisionIs.OfInfantry, out @ Some(user: Player)) if user == player => val bailStatus = session.flying || player.spectator || session.speed > 1f || player.BailProtection @@ -5636,10 +5777,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if v.Seats(0).occupant.contains(player) => val bailStatus = v.BailProtection v.BailProtection = false - (out, ValidObject(t), bailStatus, pv) + (out, ValidObject(t, decorator = "GenericCollision/GroundVehicle"), bailStatus, pv) case (CollisionIs.OfAircraft, out @ Some(v: Vehicle)) if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => - (out, ValidObject(t), false, pv) + (out, ValidObject(t, decorator = "GenericCollision/Aircraft"), false, pv) + case (CollisionIs.BetweenThings, o_) => + log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case") + (None, None, false, Vector3.Zero) case _ => (None, None, false, Vector3.Zero) } @@ -5725,61 +5869,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //log.info("BindPlayerMessage: " + msg) case msg @ PlanetsideAttributeMessage(object_guid, attribute_type, attribute_value) => - ValidObject(object_guid) match { + ValidObject(object_guid, decorator = "PlanetsideAttribute") match { + case Some(vehicle: Vehicle) if player.avatar.vehicle.contains(vehicle.GUID) => + vehicle.Actor ! ServerObject.AttributeMsg(attribute_type, attribute_value) case Some(vehicle: Vehicle) => - if (player.avatar.vehicle.contains(vehicle.GUID)) { - if (9 < attribute_type && attribute_type < 14) { - vehicle.PermissionGroup(attribute_type, attribute_value) match { - case Some(allow) => - val group = AccessPermissionGroup(attribute_type - 10) - log.info(s"${player.Name} changed ${vehicle.Definition.Name}'s access permission $group to $allow") - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.SeatPermissions(player.GUID, vehicle.GUID, attribute_type, attribute_value) - ) - //kick players who should not be seated in the vehicle due to permission changes - if (allow == VehicleLockState.Locked) { //TODO only important permission atm - vehicle.Seats.foreach { - case (seatIndex, seat) => - seat.occupant match { - case Some(tplayer: Player) => - if (vehicle.SeatPermissionGroup(seatIndex).contains(group) && tplayer != player) { //can not kick self - seat.unmount(tplayer) - tplayer.VehicleSeated = None - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.KickPassenger(tplayer.GUID, 4, false, object_guid) - ) - } - case _ => ; // No player seated - } - } - vehicle.CargoHolds.foreach { - case (cargoIndex, hold) => - hold.occupant match { - case Some(cargo) => - if (vehicle.SeatPermissionGroup(cargoIndex).contains(group)) { - //todo: this probably doesn't work for passengers within the cargo vehicle - // Instruct client to start bail dismount procedure - self ! DismountVehicleCargoMsg(player.GUID, cargo.GUID, true, false, false) - } - case None => ; // No vehicle in cargo - } - } - } - case None => ; - } - } else { - log.warn( - s"PlanetsideAttribute: vehicle attributes - unsupported change on vehicle $object_guid - $attribute_type, ${player.Name}" - ) - } - } else { - log.warn( - s"PlanetsideAttribute: vehicle attributes - ${player.Name} does not own vehicle ${vehicle.GUID} and can not change it" - ) - } - + log.warn( + s"PlanetsideAttribute: ${player.Name} does not own vehicle ${vehicle.GUID} and can not change it" + ) // Cosmetics options case Some(player: Player) if attribute_type == 106 => avatarActor ! AvatarActor.SetCosmetics(Cosmetic.valuesFromAttributeValue(attribute_value)) @@ -5787,8 +5883,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(obj) => log.trace(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attribute_type to ${obj.Definition.Name}") - case _ => - log.warn(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attribute_type") + case _ => ; } case msg @ FacilityBenefitShieldChargeRequestMessage(guid) => @@ -5872,8 +5967,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con progressBarUpdate.cancel() progressBarValue = None - case msg @ TradeMessage(_,_) => - log.info(s"${player.Name} wants to trade, for some reason - $msg") + case TradeMessage(trade) => + log.info(s"${player.Name} wants to trade, for some reason - $trade") case _ => log.warn(s"Unhandled GamePacket $pkt") @@ -6045,30 +6140,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } - /** - * Construct tasking that adds a completed but unregistered projectile into the scene. - * After the projectile is registered to the curent zone's global unique identifier system, - * all connected clients save for the one that registered it will be informed about the projectile's "creation." - * @param obj the projectile to be registered - * @return a `TaskBundle` message - */ - private def registerProjectile(obj: Projectile): TaskBundle = { - TaskBundle( - new StraightforwardTask() { - private val globalProjectile = obj - private val localAnnounce = self - - override def description(): String = s"register a ${globalProjectile.profile.Name}" - - def action(): Future[Any] = { - localAnnounce ! LoadedRemoteProjectile(globalProjectile.GUID, Some(globalProjectile)) - Future(true) - } - }, - List(GUIDTask.registerObject(continent.GUID, obj)) - ) - } - private def unregisterDrivenVehicle(vehicle: Vehicle, driver: Player): TaskBundle = { TaskBundle( new StraightforwardTask() { @@ -6086,54 +6157,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) } - /** - * Construct tasking that removes a formerly complete and currently registered projectile from the scene. - * After the projectile is unregistered from the curent zone's global unique identifier system, - * all connected clients save for the one that registered it will be informed about the projectile's "destruction." - * @param obj the projectile to be unregistered - * @return a `TaskBundle` message - */ - private def unregisterProjectile(obj: Projectile): TaskBundle = { - TaskBundle( - new StraightforwardTask() { - private val globalProjectile = obj - private val localAnnounce = self - private val localMsg = AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(player.GUID, obj.GUID, 2)) - - override def description(): String = s"unregister a ${globalProjectile.profile.Name}" - - def action(): Future[Any] = { - localAnnounce ! localMsg - Future(true) - } - }, - List(GUIDTask.unregisterObject(continent.GUID, obj)) - ) - } - - /** - * If the projectile object is unregistered, register it. - * If the projectile object is already registered, unregister it and then register it again. - * @see `registerProjectile(Projectile)` - * @see `unregisterProjectile(Projectile)` - * @param obj the projectile to be registered (a second time?) - * @return a `TaskBundle` message - */ - def reregisterProjectile(obj: Projectile): TaskBundle = { - val reg = registerProjectile(obj) - if (obj.HasGUID) { - TaskBundle( - reg.mainTask, - TaskBundle( - reg.subTasks(0).mainTask, - unregisterProjectile(obj) - ) - ) - } else { - reg - } - } - def AccessContainer(container: Container): Unit = { container match { case v: Vehicle => @@ -6317,22 +6340,36 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * the first value is a `Container` object; * the second value is an `Equipment` object in the former */ - def FindContainedEquipment: (Option[PlanetSideGameObject with Container], Option[Equipment]) = { - player.VehicleSeated match { - case Some(vehicle_guid) => //weapon is vehicle turret? - continent.GUID(vehicle_guid) match { - case Some(vehicle: Mountable with MountableWeapons with Container) => - vehicle.PassengerInSeat(player) match { - case Some(seat_num) => - (Some(vehicle), vehicle.WeaponControlledFromSeat(seat_num)) - case None => ; - (None, None) - } - case _ => ; - (None, None) + def FindContainedEquipment(): (Option[PlanetSideGameObject with Container], Set[Equipment]) = { + continent.GUID(player.VehicleSeated) match { + case Some(vehicle: Mountable with MountableWeapons with Container) => + vehicle.PassengerInSeat(player) match { + case Some(seat_num) => + (Some(vehicle), vehicle.WeaponControlledFromSeat(seat_num)) + case None => + (None, Set.empty) } - case None => //not in vehicle; weapon in hand? - (Some(player), player.Slot(player.DrawnSlot).Equipment) + case _ => + player.Slot(player.DrawnSlot).Equipment match { + case Some(a) => + (Some(player), Set(a)) + case _ => + (None, Set.empty) + } + } + } + +/** + * Check two locations for a controlled piece of equipment that is associated with the `player` + * and has the specified global unique identifier number. + */ + def FindContainedEquipment( + guid: PlanetSideGUID + ): (Option[PlanetSideGameObject with Container], Set[Equipment]) = { + val (o, equipment) = FindContainedEquipment() + equipment.find { _.GUID == guid } match { + case Some(equip) => (o, Set(equip)) + case None => (None, Set.empty) } } @@ -6340,7 +6377,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Runs `FindContainedEquipment` but ignores the `Container` object output. * @return an `Equipment` object */ - def FindEquipment: Option[Equipment] = FindContainedEquipment._2 + def FindEquipment(): Set[Equipment] = FindContainedEquipment()._2 + + /** + * Runs `FindContainedEquipment` but ignores the `Container` object output + * and only discovers `Equipment` with the specified global unique identifier number. + * @return an `Equipment` object + */ + def FindEquipment(guid: PlanetSideGUID): Option[Equipment] = FindEquipment().find { _.GUID == guid } /** * Check two locations for a controlled piece of equipment that is associated with the `player`. @@ -6349,12 +6393,29 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * the first value is a `Container` object; * the second value is an `Tool` object in the former */ - def FindContainedWeapon: (Option[PlanetSideGameObject with Container], Option[Tool]) = { - FindContainedEquipment match { - case (container, Some(tool: Tool)) => - (container, Some(tool)) + def FindContainedWeapon: (Option[PlanetSideGameObject with Container], Set[Tool]) = { + FindContainedEquipment() match { + case (container, equipment) => + (container, equipment collect { case t: Tool => t }) case _ => - (None, None) + (None, Set.empty) + } + } + + /** + * Check two locations for a controlled piece of equipment that is associated with the `player`. + * Filter for discovered `Tool`-type `Equipment` with a specific global unique identifier number. + * @return a `Tuple` of the returned values; + * the first value is a `Container` object; + * the second value is an `Tool` object in the former + */ + def FindContainedWeapon( + guid: PlanetSideGUID + ): (Option[PlanetSideGameObject with Container], Set[Tool]) = { + val (o, equipment) = FindContainedWeapon + equipment.find { _.GUID == guid } match { + case Some(equip) => (o, Set(equip)) + case None => (None, Set.empty) } } @@ -6362,7 +6423,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Runs `FindContainedWeapon` but ignores the `Container` object output. * @return a `Tool` object */ - def FindWeapon: Option[Tool] = FindContainedWeapon._2 + def FindWeapon: Set[Tool] = FindContainedWeapon._2 /** * Get the current `Vehicle` object that the player is riding/driving. @@ -6907,6 +6968,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * This is not a complete list but, for the purpose of enforcement, some pointers will be documented here. */ def PlayerActionsToCancel(): Unit = { + shootingStart.clear() + shootingStop.clear() progressBarUpdate.cancel() progressBarValue = None lastTerminalOrderFulfillment = true @@ -6935,18 +6998,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case None => ; } - prefire.orElse(shooting) match { - case Some(guid) => - sendResponse(ChangeFireStateMessage_Stop(guid)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Stop(player.GUID, guid) - ) - prefire = None - shooting = None - shootingStop = System.currentTimeMillis() - case None => ; + val currTime = System.currentTimeMillis() + (prefire ++ shooting).foreach { guid => + sendResponse(ChangeFireStateMessage_Stop(guid)) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeFireState_Stop(player.GUID, guid) + ) } + prefire.clear() + shooting.clear() if (session.flying) { chatActor ! ChatActor.Message(ChatMsg(ChatMessageType.CMT_FLY, false, "", "off", None)) } @@ -6971,6 +7032,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con def AvatarCreate(): Unit = { val health = player.Health val armor = player.Armor + val events = continent.VehicleEvents + val zoneid = continent.id avatarActor ! AvatarActor.ResetImplants() player.Spawn() if (health != 0) { @@ -7009,8 +7072,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con vehicle.CargoHolds.values .collect { case hold if hold.isOccupied => hold.occupant.get } .foreach { _.MountedIn = vguid } - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, + events ! VehicleServiceMessage( + zoneid, VehicleAction.LoadVehicle(player.GUID, vehicle, vdef.ObjectId, vguid, data) ) carrierInfo match { @@ -7059,7 +7122,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val guid = player.GUID sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, guid, data)) continent.AvatarEvents ! AvatarServiceMessage( - continent.id, + zoneid, AvatarAction.LoadPlayer(guid, ObjectClass.avatar, guid, packet.ConstructorData(player).get, None) ) log.debug(s"AvatarCreate: ${player.Name}") @@ -7818,33 +7881,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con log.warn("expected projectile was already counted as a missed shot; can not resolve any further") None } else { - projectile.Resolve() - val outProjectile = - if (projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated)) { - //aggravated - val quality = projectile.profile.Aggravated match { - case Some(aggravation) - if aggravation.targets.exists(validation => validation.test(target)) && - aggravation.info.exists(_.damage_type == AggravatedDamage.basicDamageType(resolution)) => - ProjectileQuality.AggravatesTarget - case _ => - ProjectileQuality.Normal - } - projectile.quality(quality) - } else if (projectile.tool_def.Size == EquipmentSize.Melee) { - //melee - val quality = player.avatar.implants.flatten.find { entry => entry.definition.implantType == ImplantType.MeleeBooster } match { - case Some(booster) if booster.active && player.avatar.stamina > 9 => - avatarActor ! AvatarActor.ConsumeStamina(10) - ProjectileQuality.Modified(25f) - case _ => - ProjectileQuality.Normal - } - projectile.quality(quality) - } else { - //normal - projectile - } + val outProjectile = ProjectileQuality.modifiers(projectile, resolution, target, pos, Some(player)) + if (projectile.tool_def.Size == EquipmentSize.Melee && outProjectile.quality == ProjectileQuality.Modified(25)) { + avatarActor ! AvatarActor.ConsumeStamina(10) + } Some(DamageInteraction(SourceEntry(target), ProjectileReason(resolution, outProjectile, target.DamageModel), pos)) } } @@ -8123,8 +8163,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent)) true } else { - log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it") - false + Zone.EquipmentIs.Where(obj, object_guid, continent) match { + case None => + true + case Some(Zone.EquipmentIs.Orphaned()) => + if (obj.HasGUID) { + TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) + } + true + case _ => + log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it") + false + } } } } @@ -8677,8 +8727,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @param seatNum the mount */ def UpdateWeaponAtSeatPosition(objWithSeat: MountableWeapons, seatNum: Int): Unit = { - objWithSeat.WeaponControlledFromSeat(seatNum) match { - case Some(weapon: Tool) => + objWithSeat.WeaponControlledFromSeat(seatNum) foreach { + case weapon: Tool => //update mounted weapon belonging to mount weapon.AmmoSlots.foreach(slot => { //update the magazine(s) in the weapon, specifically @@ -8926,9 +8976,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con .flatMap { case Some(obj: Vehicle) if !obj.Cloaked => //TODO hint: vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.ProjectileAutoLockAwareness(mode)) - obj.Seats.values.flatMap { case seat => seat.occupants.map(_.Name) } + obj.Seats.values.flatMap { seat => seat.occupants.map(_.Name) } case Some(obj: Mountable) => - obj.Seats.values.flatMap { case seat => seat.occupants.map(_.Name) } + obj.Seats.values.flatMap { seat => seat.occupants.map(_.Name) } case Some(obj: Player) if obj.ExoSuit == ExoSuitType.MAX => Seq(obj.Name) case _ => @@ -8936,51 +8986,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } - /** - * For a given registered remote projectile, perform all the actions necessary to properly dispose of it. - * Those actions involve: - * informing that the projectile should explode, - * unregistering the projectile's globally unique identifier, - * and managing the projectiles's local status information. - * @see `CleanUpRemoteProjectile(PlanetSideGUID, Projectile, Int)` - * @param projectile_guid the globally unique identifier of the projectile - * @param projectile the projectile - */ - def CleanUpRemoteProjectile(projectile_guid: PlanetSideGUID, projectile: Projectile): Unit = { - projectiles.indexWhere({ - case Some(p) => p eq projectile - case None => false - }) match { - case -1 => ; //required catch - case index if projectilesToCleanUp(index) => - CleanUpRemoteProjectile(projectile_guid, projectile, index) - case _ => ; - } - } - - /** - * For a given registered remote projectile, perform all the actions necessary to properly dispose of it. - * Those actions involve: - * informing that the projectile should explode, - * unregistering the projectile's globally unique identifier, - * and managing the projectiles's local status information. - * @param projectile_guid the globally unique identifier of the projectile - * @param projectile the projectile - * @param local_index an index of the absolute sequence of the projectile, for internal lists - */ - def CleanUpRemoteProjectile(projectile_guid: PlanetSideGUID, projectile: Projectile, local_index: Int): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ProjectileExplodes(player.GUID, projectile_guid, projectile) - ) - TaskWorkflow.execute(unregisterProjectile(projectile)) - projectiles(local_index) match { - case Some(obj) if !obj.isResolved => obj.Miss() - case _ => ; - } - projectilesToCleanUp(local_index) = false - } - def CheckForHitPositionDiscrepancy( projectile_guid: PlanetSideGUID, hitPos: Vector3, @@ -9212,27 +9217,20 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) val initialQuality = tool.FireMode match { case mode: ChargeFireModeDefinition => - ProjectileQuality.Modified((projectile.fire_time - shootingStart) / mode.Time.toFloat) + ProjectileQuality.Modified( + projectile.fire_time - shootingStart.getOrElse(tool.GUID, System.currentTimeMillis()) / mode.Time.toFloat + ) case _ => ProjectileQuality.Normal } - projectiles(projectileIndex) = Some(projectile.quality(initialQuality)) + val qualityprojectile = projectile.quality(initialQuality) + projectiles(projectileIndex) = Some(qualityprojectile) if (projectile_info.ExistsOnRemoteClients) { log.trace( s"WeaponFireMessage: ${player.Name}'s ${projectile_info.Name} is a remote projectile" ) - if (projectile.HasGUID) { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ProjectileExplodes(player.GUID, projectile.GUID, projectile) - ) - TaskWorkflow.execute(reregisterProjectile(projectile)) - } else { - TaskWorkflow.execute(registerProjectile(projectile)) - } + continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile) } - projectilesToCleanUp(projectileIndex) = false - obj match { case turret: FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret => turret.Actor ! FacilityTurret.WeaponDischarged() @@ -9257,33 +9255,136 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con // Cancel NC MAX shield if it's active ToggleMaxSpecialState(enable = false) } - FindContainedWeapon match { - case out @ (Some(_), Some(tool: Tool)) => - if (tool.Magazine <= 0) { //safety: enforce ammunition depletion - prefire = None - EmptyMagazine(weaponGUID, tool) - } else if (!player.isAlive) { //proper internal accounting, but no projectile - prefire = shooting.orElse(Some(weaponGUID)) - tool.Discharge() - projectiles(projectileGUID.guid - Projectile.baseUID) = None - shotsWhileDead += 1 - } else { //shooting - if ( - avatar.stamina > 0 && - tool.FireModeIndex == 1 && - (tool.Definition.Name == "anniversary_guna" - || tool.Definition.Name == "anniversary_gun" - || tool.Definition.Name == "anniversary_gunb") - ) { - avatarActor ! AvatarActor.ConsumeStamina(avatar.stamina) + val (o, tools) = FindContainedWeapon + val (_, enabledTools) = FindEnabledWeaponsToHandleWeaponFireAccountability(o, tools) + if (enabledTools.size != tools.size) { + o match { + case Some(v: Vehicle) => + //assert subsystem states + v.SubsystemMessages().foreach { sendResponse } + case _ => ; + } + } + if (enabledTools.nonEmpty) { + val collectedTools = enabledTools.collect { + case tool: Tool if tool.GUID == weaponGUID => + if (tool.Magazine <= 0) { //safety: enforce ammunition depletion + prefire -= weaponGUID + EmptyMagazine(weaponGUID, tool) + } else if (!player.isAlive) { //proper internal accounting, but no projectile + prefire += weaponGUID + tool.Discharge() + projectiles(projectileGUID.guid - Projectile.baseUID) = None + shotsWhileDead += 1 + } else { //shooting + if ( + avatar.stamina > 0 && + tool.FireModeIndex == 1 && + (tool.Definition.Name == "anniversary_guna" + || tool.Definition.Name == "anniversary_gun" + || tool.Definition.Name == "anniversary_gunb") + ) { + avatarActor ! AvatarActor.ConsumeStamina(avatar.stamina) + } + avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds) + prefire += weaponGUID + tool.Discharge() + } + (o, Some(tool)) + } + collectedTools.headOption.getOrElse((None, None)) + } else { + (None, None) + } + } + + def FindEnabledWeaponsToHandleWeaponFireAccountability( + o: Option[PlanetSideGameObject with Container], + tools: Set[Tool] + ): (Option[PlanetSideGameObject with Container], Set[Tool]) = { + val enabledTools = o match { + case Some(v: Vehicle) + if GlobalDefinitions.isBattleFrameVehicle(v.Definition) => + val filteredTools = tools.filter { tool: Tool => + v.Weapons.find { + case (index, slot) => + //index = 2 or 3 for bfr_gunner; index = 1 or 2 for bfr_flight + index > 0 && index < 4 && slot.Equipment.nonEmpty && (tool eq slot.Equipment.get) + } match { + case Some((index, _)) => + val mountIsEnabled = v.Subsystems(if (v.Weapons.keys.min == index) { + "BattleframeLeftArm" + } else { + "BattleframeRightArm" + }).get.Enabled + if (!mountIsEnabled) { + //can't stop the local discharge, but it will not actually shoot anything; assert the magazine + sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) + } + mountIsEnabled + case None => + false } - avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds) - prefire = shooting.orElse(Some(weaponGUID)) - tool.Discharge() } - out + filteredTools + case Some(_) => + tools + case None => + Set[Tool]() + } + (o, enabledTools) + } + + /** + * Take a projectile that was introduced into the game world and + * determine if it generates a secondary damage projectile or + * an method of damage causation that requires additional management. + * @param projectile the projectile + * @param pguid the client-local projectile identifier + * @param hitPos the game world position where the projectile is being recorded + * @return a for all affected targets, a combination of projectiles, projectile location, and the target's location; + * nothing if no targets were affected + */ + def HandleDamageProxy( + projectile: Projectile, + pguid: PlanetSideGUID, + hitPos: Vector3 + ): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { + GlobalDefinitions.getDamageProxy(projectile, hitPos) match { + case Some(proxy) if proxy.profile.ExistsOnRemoteClients => + proxy.Position = hitPos + continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy) + Nil + + case Some(proxy) + if proxy.tool_def == GlobalDefinitions.maelstrom => + //server-side maelstrom grenade target selection + val radius = proxy.profile.LashRadius * proxy.profile.LashRadius + val targets = continent.blockMap + .sector(hitPos, proxy.profile.LashRadius) + .livePlayerList + .filter { target => + Vector3.DistanceSquared(target.Position, hitPos) <= radius + } + //chainlash is separated from the actual damage application for convenience + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.SendResponse( + PlanetSideGUID(0), + ChainLashMessage( + hitPos, + projectile.profile.ObjectId, + targets.map { _.GUID } + ) + ) + ) + targets.map { target => + CheckForHitPositionDiscrepancy(pguid, hitPos, target) + (target, proxy, hitPos, target.Position) + } + case _ => - (None, None) + Nil } } diff --git a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala index 9b65a341c..ad3275eee 100644 --- a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala +++ b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala @@ -487,8 +487,9 @@ class BuildingActor( class FakeNtuSource(private val building: Building) extends PlanetSideServerObject with NtuContainer { - override def NtuCapacitor = Float.MaxValue - override def NtuCapacitor_=(a: Float) = Float.MaxValue + override def NtuCapacitor = Int.MaxValue.toFloat + override def NtuCapacitor_=(a: Float) = Int.MaxValue.toFloat + override def MaxNtuCapacitor = Int.MaxValue.toFloat override def Faction = building.Faction override def Zone = building.Zone override def Definition = null diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 889582ca3..8d094e6fe 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -25,10 +25,10 @@ import net.psforever.objects.serverobject.structures.{AmenityDefinition, AutoRep import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalDefinition import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalDefinition, ImplantTerminalMechDefinition} import net.psforever.objects.serverobject.turret.{FacilityTurretDefinition, TurretUpgrade} -import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, UtilityType} +import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, UtilityType, VehicleSubsystemEntry} import net.psforever.objects.vital.base.DamageType import net.psforever.objects.vital.damage._ -import net.psforever.objects.vital.etc.ExplodingRadialDegrade +import net.psforever.objects.vital.etc.{ArmorSiphonMaxDistanceCutoff, ExplodingRadialDegrade, InfantryAggravatedRadiation, InfantryAggravatedRadiationBurn} import net.psforever.objects.vital.projectile._ import net.psforever.objects.vital.prop.DamageWithPosition import net.psforever.objects.vital._ @@ -37,6 +37,7 @@ import net.psforever.types._ import net.psforever.objects.serverobject.llu.{CaptureFlagDefinition, CaptureFlagSocketDefinition} import net.psforever.objects.vital.collision.TrapCollisionDamageMultiplier +import scala.annotation.switch import scala.collection.mutable import scala.concurrent.duration._ @@ -182,6 +183,8 @@ object GlobalDefinitions { val aphelion_laser_projectile = ProjectileDefinition(Projectiles.aphelion_laser_projectile) + val aphelion_plasma_cloud = ProjectileDefinition(Projectiles.aphelion_plasma_cloud) + val aphelion_plasma_rocket_projectile = ProjectileDefinition(Projectiles.aphelion_plasma_rocket_projectile) val aphelion_ppa_projectile = ProjectileDefinition(Projectiles.aphelion_ppa_projectile) @@ -228,7 +231,7 @@ object GlobalDefinitions { val flail_projectile = ProjectileDefinition(Projectiles.flail_projectile) - val flamethrower_fire_cloud = ProjectileDefinition(Projectiles.flamethrower_projectile) //flamethrower_fire_cloud + val flamethrower_fire_cloud = ProjectileDefinition(Projectiles.flamethrower_fire_cloud) val flamethrower_fireball = ProjectileDefinition(Projectiles.flamethrower_fireball) @@ -294,6 +297,8 @@ object GlobalDefinitions { val liberator_bomb_projectile = ProjectileDefinition(Projectiles.liberator_bomb_projectile) + val maelstrom_grenade_damager = ProjectileDefinition(Projectiles.maelstrom_grenade_damager) + val maelstrom_grenade_projectile = ProjectileDefinition(Projectiles.maelstrom_grenade_projectile) val maelstrom_grenade_projectile_contact = ProjectileDefinition(Projectiles.maelstrom_grenade_projectile_contact) @@ -336,6 +341,8 @@ object GlobalDefinitions { val peregrine_particle_cannon_projectile = ProjectileDefinition(Projectiles.peregrine_particle_cannon_projectile) + val peregrine_particle_cannon_radiation_cloud = ProjectileDefinition(Projectiles.peregrine_particle_cannon_radiation_cloud) + val peregrine_rocket_pod_projectile = ProjectileDefinition(Projectiles.peregrine_rocket_pod_projectile) val peregrine_sparrow_projectile = ProjectileDefinition(Projectiles.peregrine_sparrow_projectile) @@ -370,6 +377,8 @@ object GlobalDefinitions { val quasar_projectile = ProjectileDefinition(Projectiles.quasar_projectile) + val radiator_cloud = ProjectileDefinition(Projectiles.radiator_cloud) + val radiator_grenade_projectile = ProjectileDefinition(Projectiles.radiator_grenade_projectile) val radiator_sticky_projectile = ProjectileDefinition(Projectiles.radiator_sticky_projectile) @@ -427,6 +436,10 @@ object GlobalDefinitions { val wasp_rocket_projectile = ProjectileDefinition(Projectiles.wasp_rocket_projectile) val winchester_projectile = ProjectileDefinition(Projectiles.winchester_projectile) + + val armor_siphon_projectile = ProjectileDefinition(Projectiles.trek_projectile) //fake projectile for storing damage information + + val ntu_siphon_emp = ProjectileDefinition(Projectiles.ntu_siphon_emp) init_projectile() /* @@ -603,6 +616,10 @@ object GlobalDefinitions { val spitfire_aa_ammo = AmmoBoxDefinition(Ammo.spitfire_aa_ammo) val energy_gun_ammo = AmmoBoxDefinition(Ammo.energy_gun_ammo) + + val armor_siphon_ammo = AmmoBoxDefinition(Ammo.armor_siphon_ammo) + + val ntu_siphon_ammo = AmmoBoxDefinition(Ammo.ntu_siphon_ammo) init_ammo() val chainblade = ToolDefinition(ObjectClass.chainblade) @@ -871,6 +888,108 @@ object GlobalDefinitions { val energy_gun_tr = ToolDefinition(ObjectClass.energy_gun_tr) val energy_gun_vs = ToolDefinition(ObjectClass.energy_gun_vs) + + val aphelion_armor_siphon = ToolDefinition(ObjectClass.aphelion_armor_siphon) + + val aphelion_armor_siphon_left = ToolDefinition(ObjectClass.aphelion_armor_siphon_left) + + val aphelion_armor_siphon_right = ToolDefinition(ObjectClass.aphelion_armor_siphon_right) + + val aphelion_laser = ToolDefinition(ObjectClass.aphelion_laser) + + val aphelion_laser_left = ToolDefinition(ObjectClass.aphelion_laser_left) + + val aphelion_laser_right = ToolDefinition(ObjectClass.aphelion_laser_right) + + val aphelion_ntu_siphon = ToolDefinition(ObjectClass.aphelion_ntu_siphon) + + val aphelion_ntu_siphon_left = ToolDefinition(ObjectClass.aphelion_ntu_siphon_left) + + val aphelion_ntu_siphon_right = ToolDefinition(ObjectClass.aphelion_ntu_siphon_right) + + val aphelion_ppa = ToolDefinition(ObjectClass.aphelion_ppa) + + val aphelion_ppa_left = ToolDefinition(ObjectClass.aphelion_ppa_left) + + val aphelion_ppa_right = ToolDefinition(ObjectClass.aphelion_ppa_right) + + val aphelion_starfire = ToolDefinition(ObjectClass.aphelion_starfire) + + val aphelion_starfire_left = ToolDefinition(ObjectClass.aphelion_starfire_left) + + val aphelion_starfire_right = ToolDefinition(ObjectClass.aphelion_starfire_right) + + val aphelion_plasma_rocket_pod = ToolDefinition(ObjectClass.aphelion_plasma_rocket_pod) + + val aphelion_immolation_cannon = ToolDefinition(ObjectClass.aphelion_immolation_cannon) + + val colossus_armor_siphon = ToolDefinition(ObjectClass.colossus_armor_siphon) + + val colossus_armor_siphon_left = ToolDefinition(ObjectClass.colossus_armor_siphon_left) + + val colossus_armor_siphon_right = ToolDefinition(ObjectClass.colossus_armor_siphon_right) + + val colossus_burster = ToolDefinition(ObjectClass.colossus_burster) + + val colossus_burster_left = ToolDefinition(ObjectClass.colossus_burster_left) + + val colossus_burster_right = ToolDefinition(ObjectClass.colossus_burster_right) + + val colossus_chaingun = ToolDefinition(ObjectClass.colossus_chaingun) + + val colossus_chaingun_left = ToolDefinition(ObjectClass.colossus_chaingun_left) + + val colossus_chaingun_right = ToolDefinition(ObjectClass.colossus_chaingun_right) + + val colossus_ntu_siphon = ToolDefinition(ObjectClass.colossus_ntu_siphon) + + val colossus_ntu_siphon_left = ToolDefinition(ObjectClass.colossus_ntu_siphon_left) + + val colossus_ntu_siphon_right = ToolDefinition(ObjectClass.colossus_ntu_siphon_right) + + val colossus_tank_cannon = ToolDefinition(ObjectClass.colossus_tank_cannon) + + val colossus_tank_cannon_left = ToolDefinition(ObjectClass.colossus_tank_cannon_left) + + val colossus_tank_cannon_right = ToolDefinition(ObjectClass.colossus_tank_cannon_right) + + val colossus_dual_100mm_cannons = ToolDefinition(ObjectClass.colossus_dual_100mm_cannons) + + val colossus_cluster_bomb_pod = ToolDefinition(ObjectClass.colossus_cluster_bomb_pod) + + val peregrine_armor_siphon = ToolDefinition(ObjectClass.peregrine_armor_siphon) + + val peregrine_armor_siphon_left = ToolDefinition(ObjectClass.peregrine_armor_siphon_left) + + val peregrine_armor_siphon_right = ToolDefinition(ObjectClass.peregrine_armor_siphon_right) + + val peregrine_dual_machine_gun = ToolDefinition(ObjectClass.peregrine_dual_machine_gun) + + val peregrine_dual_machine_gun_left = ToolDefinition(ObjectClass.peregrine_dual_machine_gun_left) + + val peregrine_dual_machine_gun_right = ToolDefinition(ObjectClass.peregrine_dual_machine_gun_right) + + val peregrine_mechhammer = ToolDefinition(ObjectClass.peregrine_mechhammer) + + val peregrine_mechhammer_left = ToolDefinition(ObjectClass.peregrine_mechhammer_left) + + val peregrine_mechhammer_right = ToolDefinition(ObjectClass.peregrine_mechhammer_right) + + val peregrine_sparrow = ToolDefinition(ObjectClass.peregrine_sparrow) + + val peregrine_sparrow_left = ToolDefinition(ObjectClass.peregrine_sparrow_left) + + val peregrine_sparrow_right = ToolDefinition(ObjectClass.peregrine_sparrow_right) + + val peregrine_particle_cannon = ToolDefinition(ObjectClass.peregrine_particle_cannon) + + val peregrine_dual_rocket_pods = ToolDefinition(ObjectClass.peregrine_dual_rocket_pods) + + val peregrine_ntu_siphon = ToolDefinition(ObjectClass.peregrine_ntu_siphon) + + val peregrine_ntu_siphon_left = ToolDefinition(ObjectClass.peregrine_ntu_siphon_left) + + val peregrine_ntu_siphon_right = ToolDefinition(ObjectClass.peregrine_ntu_siphon_right) init_tools() /* @@ -942,6 +1061,18 @@ object GlobalDefinitions { val phantasm = VehicleDefinition(ObjectClass.phantasm) + val aphelion_gunner = VehicleDefinition.Bfr(ObjectClass.aphelion_gunner) + + val colossus_gunner = VehicleDefinition.Bfr(ObjectClass.colossus_gunner) + + val peregrine_gunner = VehicleDefinition.Bfr(ObjectClass.peregrine_gunner) + + val aphelion_flight = VehicleDefinition.BfrFlight(ObjectClass.aphelion_flight) //Eclipse + + val colossus_flight = VehicleDefinition.BfrFlight(ObjectClass.colossus_flight) //Invader + + val peregrine_flight = VehicleDefinition.BfrFlight(ObjectClass.peregrine_flight) //Eagle + val droppod = VehicleDefinition(ObjectClass.droppod) val orbital_shuttle = VehicleDefinition(ObjectClass.orbital_shuttle) @@ -1057,6 +1188,14 @@ object GlobalDefinitions { val vanu_vehicle_creation_pad = new VehicleSpawnPadDefinition(947) + val bfr_door = new VehicleSpawnPadDefinition(141) + + val pad_create = new VehicleSpawnPadDefinition(615) + + val pad_creation = new VehicleSpawnPadDefinition(616) + + val spawnpoint_vehicle = new VehicleSpawnPadDefinition(816) + val mb_locker = new LockerDefinition val lock_external = new IFFLockDefinition @@ -1160,12 +1299,12 @@ object GlobalDefinitions { hst.NoWarp += dropship hst.NoWarp += galaxy_gunship hst.NoWarp += lodestar - //hst.NoWarp += aphelion_gunner - //hst.NoWarp += aphelion_flight - //hst.NoWarp += colossus_gunner - //hst.NoWarp += colossus_flight - //hst.NoWarp += peregrine_gunner - //hst.NoWarp += peregrine_flight + hst.NoWarp += aphelion_gunner + hst.NoWarp += aphelion_flight + hst.NoWarp += colossus_gunner + hst.NoWarp += colossus_flight + hst.NoWarp += peregrine_gunner + hst.NoWarp += peregrine_flight hst.SpecificPointFunc = SpawnPoint.Gate val mainbase1 = new BuildingDefinition(474) { Name = "mainbase1" } @@ -1619,6 +1758,119 @@ object GlobalDefinitions { } } + /** + * Given the the definition of a piece of equipment, + * determine whether it is a weapon to be installed on battle frame robotics units. + * @param tdef the `EquipmentDefinition` of the alleged weapon + * @return `true`, if the definition represents a battle frame robotics weapon; + * `false`, otherwise + */ + def isBattleFrameWeapon(tdef : EquipmentDefinition) : Boolean = { + isBattleFrameWeaponForVS(tdef) || isBattleFrameWeaponForTR(tdef) || isBattleFrameWeaponForNC(tdef) + } + + /** + * Given the the definition of a battle frame robotics weapon, determine whether it is used by the specific faction. + * @param tdef the `EquipmentDefinition` of the alleged weapon + * @param faction the suggested alignment of the weapon + * @return `true`, if a battle frame robotics weapon and associated with the given faction; + * `false`, otherwise + */ + def isBattleFrameWeapon(tdef : EquipmentDefinition, faction : PlanetSideEmpire.Value) : Boolean = { + faction match { + case PlanetSideEmpire.VS => + isBattleFrameWeaponForVS(tdef) + case PlanetSideEmpire.TR => + isBattleFrameWeaponForTR(tdef) + case PlanetSideEmpire.NC => + isBattleFrameWeaponForNC(tdef) + case _ => + false + } + } + + def isBattleFrameArmorSiphon(edef : EquipmentDefinition) : Boolean = { + edef match { + case `aphelion_armor_siphon` | `aphelion_armor_siphon_left` | `aphelion_armor_siphon_right` | + `colossus_armor_siphon` | `colossus_armor_siphon_left` | `colossus_armor_siphon_right` | + `peregrine_armor_siphon` | `peregrine_armor_siphon_left` | `peregrine_armor_siphon_right` => + true + case _ => + false + } + } + + def isBattleFrameNTUSiphon(edef : EquipmentDefinition) : Boolean = { + edef match { + case `aphelion_ntu_siphon` | `aphelion_ntu_siphon_left` | `aphelion_ntu_siphon_right` | + `colossus_ntu_siphon` | `colossus_ntu_siphon_left` | `colossus_ntu_siphon_right` | + `peregrine_ntu_siphon` | `peregrine_ntu_siphon_left` | `peregrine_ntu_siphon_right` => + true + case _ => + false + } + } + + /** + * Given the the definition of a battle frame robotics weapon, determine whether it is used by the Vanu Sovereignty. + * @param tdef the `EquipmentDefinition` of the alleged weapon + * @return `true`, if a battle frame robotics weapon and associated with the given faction; + * `false`, otherwise + */ + def isBattleFrameWeaponForVS(tdef : EquipmentDefinition) : Boolean = { + tdef match { + case `aphelion_armor_siphon` | `aphelion_armor_siphon_left` | `aphelion_armor_siphon_right` | + `aphelion_laser` | `aphelion_laser_left` | `aphelion_laser_right` | + `aphelion_ntu_siphon` | `aphelion_ntu_siphon_left` | `aphelion_ntu_siphon_right` | + `aphelion_ppa` | `aphelion_ppa_left` | `aphelion_ppa_right` | + `aphelion_starfire` | `aphelion_starfire_left` | `aphelion_starfire_right` | + `aphelion_immolation_cannon` | `aphelion_plasma_rocket_pod` => + true + case _ => + false + } + } + + /** + * Given the the definition of a battle frame robotics weapon, determine whether it is used by the Terran Republic. + * @param tdef the `EquipmentDefinition` of the alleged weapon + * @return `true`, if a battle frame robotics weapon and associated with the given faction; + * `false`, otherwise + */ + def isBattleFrameWeaponForTR(tdef : EquipmentDefinition) : Boolean = { + tdef match { + case `colossus_armor_siphon` | `colossus_armor_siphon_left` | `colossus_armor_siphon_right` | + `colossus_burster` | `colossus_burster_left` | `colossus_burster_right` | + `colossus_chaingun` | `colossus_chaingun_left` | `colossus_chaingun_right` | + `colossus_ntu_siphon` | `colossus_ntu_siphon_left` | `colossus_ntu_siphon_right` | + `colossus_tank_cannon` | `colossus_tank_cannon_left` | `colossus_tank_cannon_right` | + `colossus_cluster_bomb_pod` | `colossus_dual_100mm_cannons` => + true + case _ => + false + } + } + + /** + * Given the the definition of a battle frame robotics weapon, determine whether it is used by the New Conglomerate. + * @param tdef the `EquipmentDefinition` of the alleged weapon + * @return `true`, if a battle frame robotics weapon and associated with the given faction; + * `false`, otherwise + */ + def isBattleFrameWeaponForNC(tdef : EquipmentDefinition) : Boolean = { + tdef match { + case `peregrine_armor_siphon` | `peregrine_armor_siphon_left` | `peregrine_armor_siphon_right` | + `peregrine_dual_machine_gun` | `peregrine_dual_machine_gun_left` | `peregrine_dual_machine_gun_right` | + `peregrine_mechhammer` | `peregrine_mechhammer_left` | `peregrine_mechhammer_right` | + `peregrine_ntu_siphon` | `colossus_ntu_siphon_left` | `peregrine_ntu_siphon_right` | + `peregrine_sparrow` | `peregrine_sparrow_left` | `peregrine_sparrow_right` | + `peregrine_particle_cannon` | `peregrine_dual_rocket_pods` => + true + case _ => + false + } + } + /** * Using the definition for a `Vehicle` determine whether it can fly. * @param vdef the `VehicleDefinition` of the vehicle @@ -1648,6 +1900,45 @@ object GlobalDefinitions { } } + /** + * Using the definition for a `Vehicle` determine whether it is a frame vehicle. + * @param vdef the `VehicleDefinition` of the vehicle + * @return `true`, if it is; `false`, otherwise + */ + def isBattleFrameVehicle(vdef : VehicleDefinition) : Boolean = { + isBattleFrameGunnerVehicle(vdef) || isBattleFrameFlightVehicle(vdef) + } + + /** + * Using the definition for a `Vehicle` determine whether it is a frame vehicle, + * primarily a gunner-variant battleframe vehicle. + * @param vdef the `VehicleDefinition` of the vehicle + * @return `true`, if it is; `false`, otherwise + */ + def isBattleFrameGunnerVehicle(vdef: VehicleDefinition): Boolean = { + vdef match { + case `colossus_gunner` | `peregrine_gunner` | `aphelion_gunner` => + true + case _ => + false + } + } + + /** + * Using the definition for a `Vehicle` determine whether it is a frame vehicle, + * primarily a flight-variant battleframe vehicle. + * @param vdef the `VehicleDefinition` of the vehicle + * @return `true`, if it is; `false`, otherwise + */ + def isBattleFrameFlightVehicle(vdef: VehicleDefinition): Boolean = { + vdef match { + case `colossus_flight` | `peregrine_flight` | `aphelion_flight` => + true + case _ => + false + } + } + /** * Using the definition for a `Vehicle` determine whether it can rotate its body without forward acceleration. * @param vdef the `VehicleDefinition` of the vehicle @@ -1683,6 +1974,45 @@ object GlobalDefinitions { } } + /** + * Return a projectile that is the damage proxy of another projectile, + * if such a damage proxy is defined in the appropriate field by its unique object identifier. + * @see `ProjectileDefinition.DamageProxy` + * @param projectile the original projectile + * @return the damage proxy projectile definition, if that can be produced + */ + def getDamageProxy(projectile: Projectile, hitPosition: Vector3): Option[Projectile] = { + projectile.Definition.DamageProxy match { + case Some(uoid) => + ((uoid: @switch) match { + case 96 => Some(aphelion_plasma_cloud) + case 301 => Some(projectile.profile) //'flamethrower_fire_cloud' can not be made into a packet + case 464 => Some(projectile.profile) //'maelstrom_grenade_damager' can not be made into a packet + case 655 => Some(peregrine_particle_cannon_radiation_cloud) + case 717 => Some(radiator_cloud) + case _ => None + }) match { + case Some(proxy) + if proxy eq projectile.profile => + Some(projectile) + case Some(proxy) => + Some(Projectile( + proxy, + projectile.tool_def, + projectile.fire_mode, + projectile.owner, + projectile.attribute_to, + hitPosition, + Vector3.Zero + )) + case None => + None + } + case None => + None + } + } + /** * Initialize `KitDefinition` globals. */ @@ -1764,6 +2094,7 @@ object GlobalDefinitions { max.ResistanceDirectHit = 6 max.ResistanceSplash = 35 max.ResistanceAggravated = 10 + max.RadiationShielding = 0.5f max.collision.forceFactor = 4f max.collision.massFactor = 10f max.DamageUsing = DamageCalculations.AgainstMaxSuit @@ -2103,15 +2434,33 @@ object GlobalDefinitions { energy_gun_ammo.Name = "energy_gun_ammo" energy_gun_ammo.Size = EquipmentSize.Inventory + + armor_siphon_ammo.Name = "armor_siphon_ammo" + armor_siphon_ammo.Capacity = 0 + armor_siphon_ammo.Size = EquipmentSize.Blocked + + ntu_siphon_ammo.Name = "ntu_siphon_ammo" + ntu_siphon_ammo.Capacity = 0 + ntu_siphon_ammo.Size = EquipmentSize.Blocked } /** * Initialize `ProjectileDefinition` globals. */ private def init_projectile(): Unit = { - val projectileConverter: ProjectileConverter = new ProjectileConverter + init_standard_projectile() + init_bfr_projectile() + } - no_projectile.Name = "none" + /** + * Initialize `ProjectileDefinition` globals for most projectiles. + */ + private def init_standard_projectile(): Unit = { + val projectileConverter: ProjectileConverter = new ProjectileConverter + val radCloudConverter: RadiationCloudConverter = new RadiationCloudConverter + + no_projectile.Name = "no_projectile" + no_projectile.DamageRadiusMin = 0f ProjectileDefinition.CalculateDerivedFields(no_projectile) no_projectile.Modifiers = Nil @@ -2139,6 +2488,7 @@ object GlobalDefinitions { bullet_12mm_projectile.DegradeMultiplier = 0.5f bullet_12mm_projectile.InitialVelocity = 500 bullet_12mm_projectile.Lifespan = 0.5f + bullet_12mm_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(bullet_12mm_projectile) bullet_12mm_projectileb.Name = "12mmbullet_projectileb" @@ -2153,6 +2503,7 @@ object GlobalDefinitions { bullet_12mm_projectileb.DegradeMultiplier = 0.5f bullet_12mm_projectileb.InitialVelocity = 500 bullet_12mm_projectileb.Lifespan = 0.5f + bullet_12mm_projectileb.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(bullet_12mm_projectileb) bullet_150mm_projectile.Name = "150mmbullet_projectile" @@ -2278,6 +2629,7 @@ object GlobalDefinitions { bullet_9mm_AP_projectile.InitialVelocity = 500 bullet_9mm_AP_projectile.Lifespan = 0.4f bullet_9mm_AP_projectile.UseDamage1Subtract = true + bullet_9mm_AP_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(bullet_9mm_AP_projectile) bullet_9mm_projectile.Name = "9mmbullet_projectile" @@ -2289,6 +2641,7 @@ object GlobalDefinitions { bullet_9mm_projectile.InitialVelocity = 500 bullet_9mm_projectile.Lifespan = 0.4f bullet_9mm_projectile.UseDamage1Subtract = true + bullet_9mm_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(bullet_9mm_projectile) anniversary_projectilea.Name = "anniversary_projectilea" @@ -2302,6 +2655,7 @@ object GlobalDefinitions { anniversary_projectilea.DegradeMultiplier = 0.2f anniversary_projectilea.InitialVelocity = 500 anniversary_projectilea.Lifespan = 0.5f + anniversary_projectilea.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(anniversary_projectilea) anniversary_projectileb.Name = "anniversary_projectileb" @@ -2316,95 +2670,9 @@ object GlobalDefinitions { anniversary_projectileb.DegradeMultiplier = 0.2f anniversary_projectileb.InitialVelocity = 500 anniversary_projectileb.Lifespan = 0.5f + anniversary_projectileb.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(anniversary_projectileb) - aphelion_immolation_cannon_projectile.Name = "aphelion_immolation_cannon_projectile" - aphelion_immolation_cannon_projectile.Damage0 = 55 - aphelion_immolation_cannon_projectile.Damage1 = 225 - aphelion_immolation_cannon_projectile.Damage2 = 210 - aphelion_immolation_cannon_projectile.Damage3 = 135 - aphelion_immolation_cannon_projectile.Damage4 = 140 - aphelion_immolation_cannon_projectile.DamageAtEdge = 0.1f - aphelion_immolation_cannon_projectile.DamageRadius = 2.0f - aphelion_immolation_cannon_projectile.ProjectileDamageType = DamageType.Splash - aphelion_immolation_cannon_projectile.InitialVelocity = 250 - aphelion_immolation_cannon_projectile.Lifespan = 1.4f - ProjectileDefinition.CalculateDerivedFields(aphelion_immolation_cannon_projectile) - aphelion_immolation_cannon_projectile.Modifiers = RadialDegrade - - aphelion_laser_projectile.Name = "aphelion_laser_projectile" - aphelion_laser_projectile.Damage0 = 3 - aphelion_laser_projectile.Damage1 = 5 - aphelion_laser_projectile.Damage2 = 5 - aphelion_laser_projectile.Damage3 = 4 - aphelion_laser_projectile.Damage4 = 5 - aphelion_laser_projectile.ProjectileDamageType = DamageType.Direct - aphelion_laser_projectile.DegradeDelay = .05f - aphelion_laser_projectile.DegradeMultiplier = 0.5f - aphelion_laser_projectile.InitialVelocity = 500 - aphelion_laser_projectile.Lifespan = 0.35f - ProjectileDefinition.CalculateDerivedFields(aphelion_laser_projectile) - - aphelion_plasma_rocket_projectile.Name = "aphelion_plasma_rocket_projectile" - //has property aggravated_damage_max_factor, but it's the aphelion_plasma_cloud that performs aggravated damage - aphelion_plasma_rocket_projectile.Damage0 = 38 - aphelion_plasma_rocket_projectile.Damage1 = 70 - aphelion_plasma_rocket_projectile.Damage2 = 95 - aphelion_plasma_rocket_projectile.Damage3 = 55 - aphelion_plasma_rocket_projectile.Damage4 = 60 - aphelion_plasma_rocket_projectile.Acceleration = 20 - aphelion_plasma_rocket_projectile.AccelerationUntil = 2f - aphelion_plasma_rocket_projectile.DamageAtEdge = .1f - aphelion_plasma_rocket_projectile.DamageRadius = 3f - aphelion_plasma_rocket_projectile.ProjectileDamageType = DamageType.Splash - aphelion_plasma_rocket_projectile.InitialVelocity = 75 - aphelion_plasma_rocket_projectile.Lifespan = 5f - ProjectileDefinition.CalculateDerivedFields(aphelion_plasma_rocket_projectile) - aphelion_plasma_rocket_projectile.Modifiers = RadialDegrade - - aphelion_ppa_projectile.Name = "aphelion_ppa_projectile" - // TODO for later, maybe : set_resource_parent aphelion_ppa_projectile game_objects ppa_projectile - aphelion_ppa_projectile.Damage0 = 31 - aphelion_ppa_projectile.Damage1 = 84 - aphelion_ppa_projectile.Damage2 = 58 - aphelion_ppa_projectile.Damage3 = 57 - aphelion_ppa_projectile.Damage4 = 60 - aphelion_ppa_projectile.DamageAtEdge = 0.10f - aphelion_ppa_projectile.DamageRadius = 1f - aphelion_ppa_projectile.ProjectileDamageType = DamageType.Splash - aphelion_ppa_projectile.DegradeDelay = .5f - aphelion_ppa_projectile.DegradeMultiplier = 0.55f - aphelion_ppa_projectile.InitialVelocity = 350 - aphelion_ppa_projectile.Lifespan = .7f - ProjectileDefinition.CalculateDerivedFields(aphelion_ppa_projectile) - aphelion_ppa_projectile.Modifiers = RadialDegrade - - aphelion_starfire_projectile.Name = "aphelion_starfire_projectile" - // TODO for later, maybe : set_resource_parent aphelion_starfire_projectile game_objects starfire_projectile - aphelion_starfire_projectile.Damage0 = 12 - aphelion_starfire_projectile.Damage1 = 20 - aphelion_starfire_projectile.Damage2 = 15 - aphelion_starfire_projectile.Damage3 = 19 - aphelion_starfire_projectile.Damage4 = 17 - aphelion_starfire_projectile.Acceleration = 11 - aphelion_starfire_projectile.AccelerationUntil = 5f - aphelion_starfire_projectile.InitialVelocity = 45 - aphelion_starfire_projectile.Lifespan = 7f - aphelion_starfire_projectile.ProjectileDamageType = DamageType.Aggravated - aphelion_starfire_projectile.Aggravated = AggravatedDamage( - AggravatedInfo(DamageType.Direct, 0.25f, 250), - Aura.None, - 2000, - 0f, - true, - List(TargetValidation(EffectTarget.Category.Aircraft, EffectTarget.Validation.Aircraft)) - ) - aphelion_starfire_projectile.ExistsOnRemoteClients = true - aphelion_starfire_projectile.RemoteClientData = (39577, 249) //starfire_projectile data - aphelion_starfire_projectile.AutoLock = true - aphelion_starfire_projectile.Packet = projectileConverter - ProjectileDefinition.CalculateDerivedFields(aphelion_starfire_projectile) - bolt_projectile.Name = "bolt_projectile" bolt_projectile.Damage0 = 100 bolt_projectile.Damage1 = 50 @@ -2414,6 +2682,7 @@ object GlobalDefinitions { bolt_projectile.ProjectileDamageType = DamageType.Splash bolt_projectile.InitialVelocity = 500 bolt_projectile.Lifespan = 1.0f + bolt_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(bolt_projectile) //TODO bolt_projectile.Modifiers = DistanceDegrade? @@ -2444,83 +2713,6 @@ object GlobalDefinitions { ProjectileDefinition.CalculateDerivedFields(chainblade_projectile) chainblade_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff) - colossus_100mm_projectile.Name = "colossus_100mm_projectile" - colossus_100mm_projectile.Damage0 = 58 - colossus_100mm_projectile.Damage1 = 330 - colossus_100mm_projectile.Damage2 = 300 - colossus_100mm_projectile.Damage3 = 165 - colossus_100mm_projectile.Damage4 = 190 - colossus_100mm_projectile.DamageAtEdge = 0.1f - colossus_100mm_projectile.DamageRadius = 5f - colossus_100mm_projectile.ProjectileDamageType = DamageType.Splash - colossus_100mm_projectile.InitialVelocity = 100 - colossus_100mm_projectile.Lifespan = 4f - ProjectileDefinition.CalculateDerivedFields(colossus_100mm_projectile) - colossus_100mm_projectile.Modifiers = RadialDegrade - - colossus_burster_projectile.Name = "colossus_burster_projectile" - // TODO for later, maybe : set_resource_parent colossus_burster_projectile game_objects burster_projectile - colossus_burster_projectile.Damage0 = 18 - colossus_burster_projectile.Damage1 = 26 - colossus_burster_projectile.Damage2 = 18 - colossus_burster_projectile.Damage3 = 22 - colossus_burster_projectile.Damage4 = 20 - colossus_burster_projectile.DamageAtEdge = 0.1f - colossus_burster_projectile.DamageRadius = 7f - colossus_burster_projectile.ProjectileDamageType = DamageType.Direct - colossus_burster_projectile.ProjectileDamageTypeSecondary = DamageType.Splash - colossus_burster_projectile.InitialVelocity = 175 - colossus_burster_projectile.Lifespan = 2.5f - ProjectileDefinition.CalculateDerivedFields(colossus_burster_projectile) - colossus_burster_projectile.Modifiers = List( - //FlakHit, - FlakBurst, - MaxDistanceCutoff - ) - - colossus_chaingun_projectile.Name = "colossus_chaingun_projectile" - // TODO for later, maybe : set_resource_parent colossus_chaingun_projectile game_objects 35mmbullet_projectile - colossus_chaingun_projectile.Damage0 = 15 - colossus_chaingun_projectile.Damage1 = 14 - colossus_chaingun_projectile.Damage2 = 15 - colossus_chaingun_projectile.Damage3 = 13 - colossus_chaingun_projectile.Damage4 = 11 - colossus_chaingun_projectile.ProjectileDamageType = DamageType.Direct - colossus_chaingun_projectile.DegradeDelay = .100f - colossus_chaingun_projectile.DegradeMultiplier = 0.44f - colossus_chaingun_projectile.InitialVelocity = 500 - colossus_chaingun_projectile.Lifespan = .50f - ProjectileDefinition.CalculateDerivedFields(colossus_chaingun_projectile) - - colossus_cluster_bomb_projectile.Name = "colossus_cluster_bomb_projectile" - colossus_cluster_bomb_projectile.Damage0 = 40 - colossus_cluster_bomb_projectile.Damage1 = 88 - colossus_cluster_bomb_projectile.Damage2 = 100 - colossus_cluster_bomb_projectile.Damage3 = 83 - colossus_cluster_bomb_projectile.Damage4 = 88 - colossus_cluster_bomb_projectile.DamageAtEdge = 0.1f - colossus_cluster_bomb_projectile.DamageRadius = 8f - colossus_cluster_bomb_projectile.ProjectileDamageType = DamageType.Splash - colossus_cluster_bomb_projectile.InitialVelocity = 75 - colossus_cluster_bomb_projectile.Lifespan = 5f - ProjectileDefinition.CalculateDerivedFields(colossus_cluster_bomb_projectile) - colossus_cluster_bomb_projectile.Modifiers = RadialDegrade - - colossus_tank_cannon_projectile.Name = "colossus_tank_cannon_projectile" - // TODO for later, maybe : set_resource_parent colossus_tank_cannon_projectile game_objects 75mmbullet_projectile - colossus_tank_cannon_projectile.Damage0 = 33 - colossus_tank_cannon_projectile.Damage1 = 90 - colossus_tank_cannon_projectile.Damage2 = 95 - colossus_tank_cannon_projectile.Damage3 = 71 - colossus_tank_cannon_projectile.Damage4 = 66 - colossus_tank_cannon_projectile.DamageAtEdge = 0.1f - colossus_tank_cannon_projectile.DamageRadius = 2f - colossus_tank_cannon_projectile.ProjectileDamageType = DamageType.Splash - colossus_tank_cannon_projectile.InitialVelocity = 165 - colossus_tank_cannon_projectile.Lifespan = 2f - ProjectileDefinition.CalculateDerivedFields(colossus_tank_cannon_projectile) - colossus_tank_cannon_projectile.Modifiers = RadialDegrade - comet_projectile.Name = "comet_projectile" comet_projectile.Damage0 = 15 comet_projectile.Damage1 = 60 @@ -2559,6 +2751,7 @@ object GlobalDefinitions { dualcycler_projectile.DegradeMultiplier = .5f dualcycler_projectile.InitialVelocity = 500 dualcycler_projectile.Lifespan = 0.5f + dualcycler_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(dualcycler_projectile) dynomite_projectile.Name = "dynomite_projectile" @@ -2583,6 +2776,7 @@ object GlobalDefinitions { energy_cell_projectile.InitialVelocity = 500 energy_cell_projectile.Lifespan = .4f energy_cell_projectile.UseDamage1Subtract = true + energy_cell_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(energy_cell_projectile) energy_gun_nc_projectile.Name = "energy_gun_nc_projectile" @@ -2591,6 +2785,7 @@ object GlobalDefinitions { energy_gun_nc_projectile.ProjectileDamageType = DamageType.Direct energy_gun_nc_projectile.InitialVelocity = 500 energy_gun_nc_projectile.Lifespan = 0.5f + energy_gun_nc_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(energy_gun_nc_projectile) energy_gun_tr_projectile.Name = "energy_gun_tr_projectile" @@ -2601,6 +2796,7 @@ object GlobalDefinitions { energy_gun_tr_projectile.DegradeMultiplier = .5f energy_gun_tr_projectile.InitialVelocity = 500 energy_gun_tr_projectile.Lifespan = 0.5f + energy_gun_tr_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(energy_gun_tr_projectile) energy_gun_vs_projectile.Name = "energy_gun_vs_projectile" @@ -2611,6 +2807,7 @@ object GlobalDefinitions { energy_gun_vs_projectile.DegradeMultiplier = 0.5f energy_gun_vs_projectile.InitialVelocity = 500 energy_gun_vs_projectile.Lifespan = .5f + energy_gun_vs_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(energy_gun_vs_projectile) enhanced_energy_cell_projectile.Name = "enhanced_energy_cell_projectile" @@ -2623,6 +2820,7 @@ object GlobalDefinitions { enhanced_energy_cell_projectile.InitialVelocity = 500 enhanced_energy_cell_projectile.Lifespan = .4f enhanced_energy_cell_projectile.UseDamage1Subtract = true + enhanced_energy_cell_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(enhanced_energy_cell_projectile) enhanced_quasar_projectile.Name = "enhanced_quasar_projectile" @@ -2634,6 +2832,7 @@ object GlobalDefinitions { enhanced_quasar_projectile.DegradeMultiplier = 0.5f enhanced_quasar_projectile.InitialVelocity = 500 enhanced_quasar_projectile.Lifespan = .4f + enhanced_quasar_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(enhanced_quasar_projectile) falcon_projectile.Name = "falcon_projectile" @@ -2688,6 +2887,33 @@ object GlobalDefinitions { RadialDegrade ) + flamethrower_fire_cloud.Name = "flamethrower_fire_cloud" + flamethrower_fire_cloud.Damage0 = 2 + flamethrower_fire_cloud.Damage1 = 0 + flamethrower_fire_cloud.Damage2 = 0 + flamethrower_fire_cloud.Damage3 = 1 + flamethrower_fire_cloud.Damage4 = 0 + flamethrower_fire_cloud.DamageAtEdge = 0.1f + flamethrower_fire_cloud.DamageRadius = 5f + flamethrower_fire_cloud.ProjectileDamageType = DamageType.Aggravated + flamethrower_fire_cloud.Aggravated = AggravatedDamage( + List(AggravatedInfo(DamageType.Direct, -1.5f, 500), AggravatedInfo(DamageType.Splash, -4.0f, 500)), + Aura.Fire, + AggravatedTiming(5000, 10), + 2.5f, + false, + false, + List(TargetValidation(EffectTarget.Category.Player, EffectTarget.Validation.Player)) + ) + flamethrower_fire_cloud.Lifespan = 5f + ProjectileDefinition.CalculateDerivedFields(flamethrower_fire_cloud) + flamethrower_fire_cloud.Modifiers = List( + InfantryAggravatedDirect, + InfantryAggravatedSplash, + RadialDegrade, + FireballAggravatedBurn + ) + flamethrower_fireball.Name = "flamethrower_fireball" flamethrower_fireball.Damage0 = 30 flamethrower_fireball.Damage1 = 0 @@ -2924,6 +3150,7 @@ object GlobalDefinitions { heavy_sniper_projectile.ProjectileDamageType = DamageType.Splash heavy_sniper_projectile.InitialVelocity = 500 heavy_sniper_projectile.Lifespan = 1.0f + heavy_sniper_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(heavy_sniper_projectile) heavy_sniper_projectile.Modifiers = RadialDegrade @@ -2968,6 +3195,7 @@ object GlobalDefinitions { hunter_seeker_missile_projectile.ProjectileDamageType = DamageType.Splash hunter_seeker_missile_projectile.InitialVelocity = 40 hunter_seeker_missile_projectile.Lifespan = 6.3f + hunter_seeker_missile_projectile.registerAs = "rc-projectiles" hunter_seeker_missile_projectile.ExistsOnRemoteClients = true hunter_seeker_missile_projectile.RemoteClientData = (39577, 201) hunter_seeker_missile_projectile.Packet = projectileConverter @@ -3169,6 +3397,7 @@ object GlobalDefinitions { lasher_projectile.InitialVelocity = 120 lasher_projectile.LashRadius = 2.5f lasher_projectile.Lifespan = 0.75f + lasher_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(lasher_projectile) lasher_projectile.Modifiers = List( DistanceDegrade, @@ -3187,6 +3416,7 @@ object GlobalDefinitions { lasher_projectile_ap.InitialVelocity = 120 lasher_projectile_ap.LashRadius = 2.5f lasher_projectile_ap.Lifespan = 0.75f + lasher_projectile_ap.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(lasher_projectile_ap) lasher_projectile_ap.Modifiers = List( DistanceDegrade, @@ -3228,6 +3458,10 @@ object GlobalDefinitions { ProjectileDefinition.CalculateDerivedFields(liberator_bomb_projectile) liberator_bomb_projectile.Modifiers = RadialDegrade + maelstrom_grenade_damager.Name = "maelstrom_grenade_damager" + maelstrom_grenade_damager.ProjectileDamageType = DamageType.Direct + //todo the maelstrom_grenade_damage is something of a broken entity atm + maelstrom_grenade_projectile.Name = "maelstrom_grenade_projectile" maelstrom_grenade_projectile.Damage0 = 32 maelstrom_grenade_projectile.Damage1 = 60 @@ -3237,7 +3471,7 @@ object GlobalDefinitions { maelstrom_grenade_projectile.ProjectileDamageType = DamageType.Direct maelstrom_grenade_projectile.InitialVelocity = 30 maelstrom_grenade_projectile.Lifespan = 2f - maelstrom_grenade_projectile.DamageProxy = 464 + maelstrom_grenade_projectile.DamageProxy = 464 //maelstrom_grenade_damager ProjectileDefinition.CalculateDerivedFields(maelstrom_grenade_projectile) maelstrom_grenade_projectile.Modifiers = RadialDegrade @@ -3251,7 +3485,7 @@ object GlobalDefinitions { maelstrom_grenade_projectile_contact.ProjectileDamageType = DamageType.Direct maelstrom_grenade_projectile_contact.InitialVelocity = 30 maelstrom_grenade_projectile_contact.Lifespan = 15f - maelstrom_grenade_projectile_contact.DamageProxy = 464 + maelstrom_grenade_projectile_contact.DamageProxy = 464 //maelstrom_grenade_damager ProjectileDefinition.CalculateDerivedFields(maelstrom_grenade_projectile_contact) maelstrom_grenade_projectile_contact.Modifiers = RadialDegrade @@ -3404,6 +3638,7 @@ object GlobalDefinitions { oicw_projectile.ProjectileDamageType = DamageType.Splash oicw_projectile.InitialVelocity = 5 oicw_projectile.Lifespan = 6.1f + oicw_projectile.registerAs = "rc-projectiles" oicw_projectile.ExistsOnRemoteClients = true oicw_projectile.RemoteClientData = (13107, 195) oicw_projectile.Packet = projectileConverter @@ -3434,82 +3669,6 @@ object GlobalDefinitions { pellet_gun_projectile.UseDamage1Subtract = false ProjectileDefinition.CalculateDerivedFields(pellet_gun_projectile) - peregrine_dual_machine_gun_projectile.Name = "peregrine_dual_machine_gun_projectile" - // TODO for later, maybe : set_resource_parent peregrine_dual_machine_gun_projectile game_objects 35mmbullet_projectile - peregrine_dual_machine_gun_projectile.Damage0 = 16 - peregrine_dual_machine_gun_projectile.Damage1 = 44 - peregrine_dual_machine_gun_projectile.Damage2 = 30 - peregrine_dual_machine_gun_projectile.Damage3 = 27 - peregrine_dual_machine_gun_projectile.Damage4 = 32 - peregrine_dual_machine_gun_projectile.ProjectileDamageType = DamageType.Direct - peregrine_dual_machine_gun_projectile.DegradeDelay = .25f - peregrine_dual_machine_gun_projectile.DegradeMultiplier = 0.65f - peregrine_dual_machine_gun_projectile.InitialVelocity = 250 - peregrine_dual_machine_gun_projectile.Lifespan = 1.1f - ProjectileDefinition.CalculateDerivedFields(peregrine_dual_machine_gun_projectile) - - peregrine_mechhammer_projectile.Name = "peregrine_mechhammer_projectile" - peregrine_mechhammer_projectile.Damage0 = 5 - peregrine_mechhammer_projectile.Damage1 = 4 - peregrine_mechhammer_projectile.Damage2 = 4 - peregrine_mechhammer_projectile.Damage3 = 5 - peregrine_mechhammer_projectile.Damage4 = 3 - peregrine_mechhammer_projectile.ProjectileDamageType = DamageType.Direct - peregrine_mechhammer_projectile.InitialVelocity = 500 - peregrine_mechhammer_projectile.Lifespan = 0.4f - ProjectileDefinition.CalculateDerivedFields(peregrine_mechhammer_projectile) - - peregrine_particle_cannon_projectile.Name = "peregrine_particle_cannon_projectile" - peregrine_particle_cannon_projectile.Damage0 = 70 - peregrine_particle_cannon_projectile.Damage1 = 525 - peregrine_particle_cannon_projectile.Damage2 = 350 - peregrine_particle_cannon_projectile.Damage3 = 318 - peregrine_particle_cannon_projectile.Damage4 = 310 - peregrine_particle_cannon_projectile.DamageAtEdge = 0.1f - peregrine_particle_cannon_projectile.DamageRadius = 3f - peregrine_particle_cannon_projectile.ProjectileDamageType = DamageType.Splash - peregrine_particle_cannon_projectile.InitialVelocity = 500 - peregrine_particle_cannon_projectile.Lifespan = .6f - ProjectileDefinition.CalculateDerivedFields(peregrine_particle_cannon_projectile) - peregrine_particle_cannon_projectile.Modifiers = RadialDegrade - - peregrine_rocket_pod_projectile.Name = "peregrine_rocket_pod_projectile" - peregrine_rocket_pod_projectile.Damage0 = 30 - peregrine_rocket_pod_projectile.Damage1 = 50 - peregrine_rocket_pod_projectile.Damage2 = 50 - peregrine_rocket_pod_projectile.Damage3 = 45 - peregrine_rocket_pod_projectile.Damage4 = 40 - peregrine_rocket_pod_projectile.Acceleration = 10 - peregrine_rocket_pod_projectile.AccelerationUntil = 2f - peregrine_rocket_pod_projectile.DamageAtEdge = 0.1f - peregrine_rocket_pod_projectile.DamageRadius = 3f - peregrine_rocket_pod_projectile.ProjectileDamageType = DamageType.Splash - peregrine_rocket_pod_projectile.InitialVelocity = 200 - peregrine_rocket_pod_projectile.Lifespan = 1.85f - ProjectileDefinition.CalculateDerivedFields(peregrine_rocket_pod_projectile) - peregrine_rocket_pod_projectile.Modifiers = RadialDegrade - - peregrine_sparrow_projectile.Name = "peregrine_sparrow_projectile" - // TODO for later, maybe : set_resource_parent peregrine_sparrow_projectile game_objects sparrow_projectile - peregrine_sparrow_projectile.Damage0 = 20 - peregrine_sparrow_projectile.Damage1 = 40 - peregrine_sparrow_projectile.Damage2 = 30 - peregrine_sparrow_projectile.Damage3 = 30 - peregrine_sparrow_projectile.Damage4 = 31 - peregrine_sparrow_projectile.Acceleration = 12 - peregrine_sparrow_projectile.AccelerationUntil = 5f - peregrine_sparrow_projectile.DamageAtEdge = 0.1f - peregrine_sparrow_projectile.DamageRadius = 2f - peregrine_sparrow_projectile.ProjectileDamageType = DamageType.Splash - peregrine_sparrow_projectile.InitialVelocity = 45 - peregrine_sparrow_projectile.Lifespan = 7.5f - peregrine_sparrow_projectile.ExistsOnRemoteClients = true - peregrine_sparrow_projectile.RemoteClientData = (13107, 187) //sparrow_projectile data - peregrine_sparrow_projectile.AutoLock = true - peregrine_sparrow_projectile.Packet = projectileConverter - ProjectileDefinition.CalculateDerivedFields(peregrine_sparrow_projectile) - peregrine_sparrow_projectile.Modifiers = RadialDegrade - phalanx_av_projectile.Name = "phalanx_av_projectile" phalanx_av_projectile.Damage0 = 60 phalanx_av_projectile.Damage1 = 140 @@ -3549,6 +3708,7 @@ object GlobalDefinitions { phalanx_projectile.DegradeMultiplier = 0.25f phalanx_projectile.InitialVelocity = 400 phalanx_projectile.Lifespan = 1f + phalanx_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(phalanx_projectile) phoenix_missile_guided_projectile.Name = "phoenix_missile_guided_projectile" @@ -3566,6 +3726,7 @@ object GlobalDefinitions { phoenix_missile_guided_projectile.InitialVelocity = 0 phoenix_missile_guided_projectile.Lifespan = 3f //not naturally a remote projectile, but being governed as one for convenience + phoenix_missile_guided_projectile.registerAs = "rc-projectiles" phoenix_missile_guided_projectile.ExistsOnRemoteClients = true phoenix_missile_guided_projectile.RemoteClientData = (0, 63) phoenix_missile_guided_projectile.Packet = projectileConverter @@ -3752,6 +3913,7 @@ object GlobalDefinitions { pulsar_ap_projectile.InitialVelocity = 500 pulsar_ap_projectile.Lifespan = .4f pulsar_ap_projectile.UseDamage1Subtract = true + pulsar_ap_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(pulsar_ap_projectile) pulsar_projectile.Name = "pulsar_projectile" @@ -3763,6 +3925,7 @@ object GlobalDefinitions { pulsar_projectile.InitialVelocity = 500 pulsar_projectile.Lifespan = .4f pulsar_projectile.UseDamage1Subtract = true + pulsar_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(pulsar_projectile) quasar_projectile.Name = "quasar_projectile" @@ -3773,8 +3936,26 @@ object GlobalDefinitions { quasar_projectile.DegradeMultiplier = 0.5f quasar_projectile.InitialVelocity = 500 quasar_projectile.Lifespan = .4f + quasar_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(quasar_projectile) + radiator_cloud.Name = "radiator_cloud" + radiator_cloud.Damage0 = 2 + radiator_cloud.DamageAtEdge = 1.0f + radiator_cloud.DamageRadius = 5f + radiator_cloud.radiation_cloud = true + radiator_cloud.ProjectileDamageType = DamageType.Radiation + radiator_cloud.Lifespan = 10.0f + ProjectileDefinition.CalculateDerivedFields(radiator_cloud) + radiator_cloud.registerAs = "rc-projectiles" + radiator_cloud.ExistsOnRemoteClients = true + radiator_cloud.Packet = radCloudConverter + radiator_cloud.Geometry = GeometryForm.representProjectileBySphere() + radiator_cloud.Modifiers = List( + MaxDistanceCutoff, + ShieldAgainstRadiation + ) + radiator_grenade_projectile.Name = "radiator_grenade_projectile" // Todo : Radiator damages ? radiator_grenade_projectile.GrenadeProjectile = true //not really, but technically yes radiator_grenade_projectile.ProjectileDamageType = DamageType.Direct @@ -3859,6 +4040,7 @@ object GlobalDefinitions { scattercannon_projectile.ProjectileDamageType = DamageType.Direct scattercannon_projectile.InitialVelocity = 400 scattercannon_projectile.Lifespan = 0.25f + scattercannon_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(scattercannon_projectile) scythe_projectile.Name = "scythe_projectile" @@ -3884,6 +4066,7 @@ object GlobalDefinitions { shotgun_shell_AP_projectile.InitialVelocity = 400 shotgun_shell_AP_projectile.Lifespan = 0.25f shotgun_shell_AP_projectile.UseDamage1Subtract = true + shotgun_shell_AP_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(shotgun_shell_AP_projectile) shotgun_shell_projectile.Name = "shotgun_shell_projectile" @@ -3893,6 +4076,7 @@ object GlobalDefinitions { shotgun_shell_projectile.InitialVelocity = 400 shotgun_shell_projectile.Lifespan = 0.25f shotgun_shell_projectile.UseDamage1Subtract = true + shotgun_shell_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(shotgun_shell_projectile) six_shooter_projectile.Name = "six_shooter_projectile" @@ -3935,6 +4119,7 @@ object GlobalDefinitions { sparrow_projectile.ProjectileDamageType = DamageType.Splash sparrow_projectile.InitialVelocity = 60 sparrow_projectile.Lifespan = 5.85f + sparrow_projectile.registerAs = "rc-projectiles" sparrow_projectile.ExistsOnRemoteClients = true sparrow_projectile.RemoteClientData = (13107, 187) sparrow_projectile.AutoLock = true @@ -3954,6 +4139,7 @@ object GlobalDefinitions { sparrow_secondary_projectile.ProjectileDamageType = DamageType.Splash sparrow_secondary_projectile.InitialVelocity = 60 sparrow_secondary_projectile.Lifespan = 5.85f + sparrow_secondary_projectile.registerAs = "rc-projectiles" sparrow_secondary_projectile.ExistsOnRemoteClients = true sparrow_secondary_projectile.RemoteClientData = (13107, 187) sparrow_secondary_projectile.AutoLock = true @@ -3971,6 +4157,7 @@ object GlobalDefinitions { spiker_projectile.ProjectileDamageType = DamageType.Splash spiker_projectile.InitialVelocity = 40 spiker_projectile.Lifespan = 5f + spiker_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(spiker_projectile) spiker_projectile.Modifiers = List( SpikerChargeDamage, @@ -4004,6 +4191,7 @@ object GlobalDefinitions { spitfire_ammo_projectile.DegradeMultiplier = 0.5f spitfire_ammo_projectile.InitialVelocity = 100 spitfire_ammo_projectile.Lifespan = .5f + spitfire_ammo_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(spitfire_ammo_projectile) starfire_projectile.Name = "starfire_projectile" @@ -4027,6 +4215,7 @@ object GlobalDefinitions { ) starfire_projectile.InitialVelocity = 45 starfire_projectile.Lifespan = 7.8f + starfire_projectile.registerAs = "rc-projectiles" starfire_projectile.ExistsOnRemoteClients = true starfire_projectile.RemoteClientData = (39577, 249) starfire_projectile.AutoLock = true @@ -4067,6 +4256,7 @@ object GlobalDefinitions { striker_missile_targeting_projectile.ProjectileDamageType = DamageType.Splash striker_missile_targeting_projectile.InitialVelocity = 30 striker_missile_targeting_projectile.Lifespan = 4.2f + striker_missile_targeting_projectile.registerAs = "rc-projectiles" striker_missile_targeting_projectile.ExistsOnRemoteClients = true striker_missile_targeting_projectile.RemoteClientData = (26214, 134) striker_missile_targeting_projectile.AutoLock = true @@ -4085,6 +4275,7 @@ object GlobalDefinitions { trek_projectile.ProjectileDamageType = DamageType.Direct trek_projectile.InitialVelocity = 40 trek_projectile.Lifespan = 7f + trek_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(trek_projectile) trek_projectile.Modifiers = MaxDistanceCutoff @@ -4149,6 +4340,7 @@ object GlobalDefinitions { wasp_gun_projectile.DegradeMultiplier = 0.5f wasp_gun_projectile.InitialVelocity = 500 wasp_gun_projectile.Lifespan = 0.5f + wasp_gun_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(wasp_gun_projectile) wasp_rocket_projectile.Name = "wasp_rocket_projectile" @@ -4162,6 +4354,7 @@ object GlobalDefinitions { wasp_rocket_projectile.ProjectileDamageType = DamageType.Splash wasp_rocket_projectile.InitialVelocity = 60 wasp_rocket_projectile.Lifespan = 6.5f + wasp_rocket_projectile.registerAs = "rc-projectiles" wasp_rocket_projectile.ExistsOnRemoteClients = true wasp_rocket_projectile.RemoteClientData = (0, 208) wasp_rocket_projectile.AutoLock = true @@ -4182,10 +4375,369 @@ object GlobalDefinitions { ProjectileDefinition.CalculateDerivedFields(winchester_projectile) } + /** + * Initialize `ProjectileDefinition` globals for projectiles utilized by battleframe robotics. + */ + private def init_bfr_projectile(): Unit = { + val projectileConverter: ProjectileConverter = new ProjectileConverter + val radCloudConverter: RadiationCloudConverter = new RadiationCloudConverter + + aphelion_immolation_cannon_projectile.Name = "aphelion_immolation_cannon_projectile" + aphelion_immolation_cannon_projectile.Damage0 = 55 + aphelion_immolation_cannon_projectile.Damage1 = 225 + aphelion_immolation_cannon_projectile.Damage2 = 210 + aphelion_immolation_cannon_projectile.Damage3 = 135 + aphelion_immolation_cannon_projectile.Damage4 = 140 + aphelion_immolation_cannon_projectile.DamageAtEdge = 0.1f + aphelion_immolation_cannon_projectile.DamageRadius = 2.0f + aphelion_immolation_cannon_projectile.ProjectileDamageType = DamageType.Splash + aphelion_immolation_cannon_projectile.InitialVelocity = 250 + aphelion_immolation_cannon_projectile.Lifespan = 1.4f + ProjectileDefinition.CalculateDerivedFields(aphelion_immolation_cannon_projectile) + aphelion_immolation_cannon_projectile.Modifiers = RadialDegrade + + aphelion_laser_projectile.Name = "aphelion_laser_projectile" + aphelion_laser_projectile.Damage0 = 3 + aphelion_laser_projectile.Damage1 = 5 + aphelion_laser_projectile.Damage2 = 5 + aphelion_laser_projectile.Damage3 = 4 + aphelion_laser_projectile.Damage4 = 5 + aphelion_laser_projectile.ProjectileDamageType = DamageType.Direct + aphelion_laser_projectile.DegradeDelay = .05f + aphelion_laser_projectile.DegradeMultiplier = 0.5f + aphelion_laser_projectile.InitialVelocity = 500 + aphelion_laser_projectile.Lifespan = 0.35f + ProjectileDefinition.CalculateDerivedFields(aphelion_laser_projectile) + + aphelion_plasma_cloud.Name = "aphelion_plasma_cloud" + aphelion_plasma_cloud.Damage0 = 3 + aphelion_plasma_cloud.DamageAtEdge = 1.0f + aphelion_plasma_cloud.DamageRadius = 3f + aphelion_plasma_cloud.radiation_cloud = true + aphelion_plasma_cloud.ProjectileDamageType = DamageType.Aggravated + aphelion_plasma_cloud.Aggravated = AggravatedDamage( + AggravatedInfo(DamageType.Splash, 0.5f, 1000), + Aura.Napalm, + AggravatedTiming(10000, 2), //10000 + 10f, //aphelion_plasma_rocket_projectile.aggravated_damage_max_factor + true, + List( + TargetValidation(EffectTarget.Category.Player, EffectTarget.Validation.Player) + ) + ) + aphelion_plasma_cloud.Lifespan = 10.0f + ProjectileDefinition.CalculateDerivedFields(aphelion_plasma_cloud) + aphelion_plasma_cloud.registerAs = "rc-projectiles" + aphelion_plasma_cloud.ExistsOnRemoteClients = true + aphelion_plasma_cloud.Packet = radCloudConverter + aphelion_plasma_cloud.Geometry = GeometryForm.representProjectileBySphere() + aphelion_plasma_cloud.Modifiers = List( //TODO placeholder values + MaxDistanceCutoff, + InfantryAggravatedRadiation, + InfantryAggravatedRadiationBurn, + ShieldAgainstRadiation + ) + + aphelion_plasma_rocket_projectile.Name = "aphelion_plasma_rocket_projectile" + //has property aggravated_damage_max_factor, but it's the aphelion_plasma_cloud that performs aggravated damage + aphelion_plasma_rocket_projectile.Damage0 = 38 + aphelion_plasma_rocket_projectile.Damage1 = 70 + aphelion_plasma_rocket_projectile.Damage2 = 95 + aphelion_plasma_rocket_projectile.Damage3 = 55 + aphelion_plasma_rocket_projectile.Damage4 = 60 + aphelion_plasma_rocket_projectile.Acceleration = 20 + aphelion_plasma_rocket_projectile.AccelerationUntil = 2f + aphelion_plasma_rocket_projectile.DamageAtEdge = .1f + aphelion_plasma_rocket_projectile.DamageRadius = 3f + aphelion_plasma_rocket_projectile.ProjectileDamageType = DamageType.Splash + aphelion_plasma_rocket_projectile.DamageProxy = 96 //aphelion_plama_cloud + aphelion_plasma_rocket_projectile.InitialVelocity = 75 + aphelion_plasma_rocket_projectile.Lifespan = 5f + ProjectileDefinition.CalculateDerivedFields(aphelion_plasma_rocket_projectile) + aphelion_plasma_rocket_projectile.Modifiers = RadialDegrade + + aphelion_ppa_projectile.Name = "aphelion_ppa_projectile" + // TODO for later, maybe : set_resource_parent aphelion_ppa_projectile game_objects ppa_projectile + aphelion_ppa_projectile.Damage0 = 31 + aphelion_ppa_projectile.Damage1 = 84 + aphelion_ppa_projectile.Damage2 = 58 + aphelion_ppa_projectile.Damage3 = 57 + aphelion_ppa_projectile.Damage4 = 60 + aphelion_ppa_projectile.DamageAtEdge = 0.10f + aphelion_ppa_projectile.DamageRadius = 1f + aphelion_ppa_projectile.ProjectileDamageType = DamageType.Splash + aphelion_ppa_projectile.DegradeDelay = .5f + aphelion_ppa_projectile.DegradeMultiplier = 0.55f + aphelion_ppa_projectile.InitialVelocity = 350 + aphelion_ppa_projectile.Lifespan = .7f + ProjectileDefinition.CalculateDerivedFields(aphelion_ppa_projectile) + aphelion_ppa_projectile.Modifiers = RadialDegrade + + aphelion_starfire_projectile.Name = "aphelion_starfire_projectile" + // TODO for later, maybe : set_resource_parent aphelion_starfire_projectile game_objects starfire_projectile + aphelion_starfire_projectile.Damage0 = 12 + aphelion_starfire_projectile.Damage1 = 20 + aphelion_starfire_projectile.Damage2 = 15 + aphelion_starfire_projectile.Damage3 = 19 + aphelion_starfire_projectile.Damage4 = 17 + aphelion_starfire_projectile.Acceleration = 11 + aphelion_starfire_projectile.AccelerationUntil = 5f + aphelion_starfire_projectile.InitialVelocity = 45 + aphelion_starfire_projectile.Lifespan = 7f + aphelion_starfire_projectile.ProjectileDamageType = DamageType.Aggravated + aphelion_starfire_projectile.Aggravated = AggravatedDamage( + AggravatedInfo(DamageType.Direct, 0.25f, 250), + Aura.None, + 2000, + 0f, + true, + List(TargetValidation(EffectTarget.Category.Aircraft, EffectTarget.Validation.Aircraft)) + ) + aphelion_starfire_projectile.registerAs = "rc-projectiles" + aphelion_starfire_projectile.ExistsOnRemoteClients = true + aphelion_starfire_projectile.RemoteClientData = (39577, 249) //starfire_projectile data + aphelion_starfire_projectile.AutoLock = true + aphelion_starfire_projectile.Packet = projectileConverter + ProjectileDefinition.CalculateDerivedFields(aphelion_starfire_projectile) + aphelion_starfire_projectile.Modifiers = List( + StarfireAggravated, + StarfireAggravatedBurn + ) + + colossus_100mm_projectile.Name = "colossus_100mm_projectile" + colossus_100mm_projectile.Damage0 = 58 + colossus_100mm_projectile.Damage1 = 330 + colossus_100mm_projectile.Damage2 = 300 + colossus_100mm_projectile.Damage3 = 165 + colossus_100mm_projectile.Damage4 = 190 + colossus_100mm_projectile.DamageAtEdge = 0.1f + colossus_100mm_projectile.DamageRadius = 5f + colossus_100mm_projectile.ProjectileDamageType = DamageType.Splash + colossus_100mm_projectile.InitialVelocity = 100 + colossus_100mm_projectile.Lifespan = 4f + ProjectileDefinition.CalculateDerivedFields(colossus_100mm_projectile) + colossus_100mm_projectile.Modifiers = RadialDegrade + + colossus_burster_projectile.Name = "colossus_burster_projectile" + // TODO for later, maybe : set_resource_parent colossus_burster_projectile game_objects burster_projectile + colossus_burster_projectile.Damage0 = 18 + colossus_burster_projectile.Damage1 = 26 + colossus_burster_projectile.Damage2 = 18 + colossus_burster_projectile.Damage3 = 22 + colossus_burster_projectile.Damage4 = 20 + colossus_burster_projectile.DamageAtEdge = 0.1f + colossus_burster_projectile.DamageRadius = 7f + colossus_burster_projectile.ProjectileDamageType = DamageType.Direct + colossus_burster_projectile.ProjectileDamageTypeSecondary = DamageType.Splash + colossus_burster_projectile.InitialVelocity = 175 + colossus_burster_projectile.Lifespan = 2.5f + ProjectileDefinition.CalculateDerivedFields(colossus_burster_projectile) + colossus_burster_projectile.Modifiers = List( + //FlakHit, + FlakBurst, + MaxDistanceCutoff + ) + + colossus_chaingun_projectile.Name = "colossus_chaingun_projectile" + // TODO for later, maybe : set_resource_parent colossus_chaingun_projectile game_objects 35mmbullet_projectile + colossus_chaingun_projectile.Damage0 = 15 + colossus_chaingun_projectile.Damage1 = 14 + colossus_chaingun_projectile.Damage2 = 15 + colossus_chaingun_projectile.Damage3 = 13 + colossus_chaingun_projectile.Damage4 = 11 + colossus_chaingun_projectile.ProjectileDamageType = DamageType.Direct + colossus_chaingun_projectile.DegradeDelay = .100f + colossus_chaingun_projectile.DegradeMultiplier = 0.44f + colossus_chaingun_projectile.InitialVelocity = 500 + colossus_chaingun_projectile.Lifespan = .50f + ProjectileDefinition.CalculateDerivedFields(colossus_chaingun_projectile) + + colossus_cluster_bomb_projectile.Name = "colossus_cluster_bomb_projectile" + colossus_cluster_bomb_projectile.Damage0 = 40 + colossus_cluster_bomb_projectile.Damage1 = 88 + colossus_cluster_bomb_projectile.Damage2 = 100 + colossus_cluster_bomb_projectile.Damage3 = 83 + colossus_cluster_bomb_projectile.Damage4 = 88 + colossus_cluster_bomb_projectile.DamageAtEdge = 0.1f + colossus_cluster_bomb_projectile.DamageRadius = 8f + colossus_cluster_bomb_projectile.ProjectileDamageType = DamageType.Splash + colossus_cluster_bomb_projectile.InitialVelocity = 75 + colossus_cluster_bomb_projectile.Lifespan = 5f + ProjectileDefinition.CalculateDerivedFields(colossus_cluster_bomb_projectile) + colossus_cluster_bomb_projectile.Modifiers = RadialDegrade + + colossus_tank_cannon_projectile.Name = "colossus_tank_cannon_projectile" + // TODO for later, maybe : set_resource_parent colossus_tank_cannon_projectile game_objects 75mmbullet_projectile + colossus_tank_cannon_projectile.Damage0 = 33 + colossus_tank_cannon_projectile.Damage1 = 90 + colossus_tank_cannon_projectile.Damage2 = 95 + colossus_tank_cannon_projectile.Damage3 = 71 + colossus_tank_cannon_projectile.Damage4 = 66 + colossus_tank_cannon_projectile.DamageAtEdge = 0.1f + colossus_tank_cannon_projectile.DamageRadius = 2f + colossus_tank_cannon_projectile.ProjectileDamageType = DamageType.Splash + colossus_tank_cannon_projectile.InitialVelocity = 165 + colossus_tank_cannon_projectile.Lifespan = 2f + ProjectileDefinition.CalculateDerivedFields(colossus_tank_cannon_projectile) + colossus_tank_cannon_projectile.Modifiers = RadialDegrade + + peregrine_dual_machine_gun_projectile.Name = "peregrine_dual_machine_gun_projectile" + // TODO for later, maybe : set_resource_parent peregrine_dual_machine_gun_projectile game_objects 35mmbullet_projectile + peregrine_dual_machine_gun_projectile.Damage0 = 16 + peregrine_dual_machine_gun_projectile.Damage1 = 44 + peregrine_dual_machine_gun_projectile.Damage2 = 30 + peregrine_dual_machine_gun_projectile.Damage3 = 27 + peregrine_dual_machine_gun_projectile.Damage4 = 32 + peregrine_dual_machine_gun_projectile.ProjectileDamageType = DamageType.Direct + peregrine_dual_machine_gun_projectile.DegradeDelay = .25f + peregrine_dual_machine_gun_projectile.DegradeMultiplier = 0.65f + peregrine_dual_machine_gun_projectile.InitialVelocity = 250 + peregrine_dual_machine_gun_projectile.Lifespan = 1.1f + ProjectileDefinition.CalculateDerivedFields(peregrine_dual_machine_gun_projectile) + + peregrine_mechhammer_projectile.Name = "peregrine_mechhammer_projectile" + peregrine_mechhammer_projectile.Damage0 = 5 + peregrine_mechhammer_projectile.Damage1 = 4 + peregrine_mechhammer_projectile.Damage2 = 4 + peregrine_mechhammer_projectile.Damage3 = 5 + peregrine_mechhammer_projectile.Damage4 = 3 + peregrine_mechhammer_projectile.ProjectileDamageType = DamageType.Direct + peregrine_mechhammer_projectile.InitialVelocity = 500 + peregrine_mechhammer_projectile.Lifespan = 0.4f + ProjectileDefinition.CalculateDerivedFields(peregrine_mechhammer_projectile) + + peregrine_particle_cannon_projectile.Name = "peregrine_particle_cannon_projectile" + peregrine_particle_cannon_projectile.Damage0 = 70 + peregrine_particle_cannon_projectile.Damage1 = 525 + peregrine_particle_cannon_projectile.Damage2 = 350 + peregrine_particle_cannon_projectile.Damage3 = 318 + peregrine_particle_cannon_projectile.Damage4 = 310 + peregrine_particle_cannon_projectile.DamageAtEdge = 0.1f + peregrine_particle_cannon_projectile.DamageRadius = 3f + peregrine_particle_cannon_projectile.ProjectileDamageType = DamageType.Splash + peregrine_particle_cannon_projectile.DamageProxy = 655 //peregrine_particle_cannon_radiation_cloud + peregrine_particle_cannon_projectile.InitialVelocity = 500 + peregrine_particle_cannon_projectile.Lifespan = .6f + ProjectileDefinition.CalculateDerivedFields(peregrine_particle_cannon_projectile) + peregrine_particle_cannon_projectile.Modifiers = RadialDegrade + + peregrine_particle_cannon_radiation_cloud.Name = "peregrine_particle_cannon_radiation_cloud" + peregrine_particle_cannon_radiation_cloud.Damage0 = 1 + peregrine_particle_cannon_radiation_cloud.DamageAtEdge = 1.0f + peregrine_particle_cannon_radiation_cloud.DamageRadius = 3f + peregrine_particle_cannon_radiation_cloud.radiation_cloud = true + peregrine_particle_cannon_radiation_cloud.ProjectileDamageType = DamageType.Radiation + peregrine_particle_cannon_radiation_cloud.Lifespan = 5.0f + ProjectileDefinition.CalculateDerivedFields(peregrine_particle_cannon_radiation_cloud) + peregrine_particle_cannon_radiation_cloud.registerAs = "rc-projectiles" + peregrine_particle_cannon_radiation_cloud.ExistsOnRemoteClients = true + peregrine_particle_cannon_radiation_cloud.Packet = radCloudConverter + peregrine_particle_cannon_radiation_cloud.Geometry = GeometryForm.representProjectileBySphere() + peregrine_particle_cannon_radiation_cloud.Modifiers = List( + MaxDistanceCutoff, + ShieldAgainstRadiation + ) + + peregrine_rocket_pod_projectile.Name = "peregrine_rocket_pod_projectile" + peregrine_rocket_pod_projectile.Damage0 = 30 + peregrine_rocket_pod_projectile.Damage1 = 50 + peregrine_rocket_pod_projectile.Damage2 = 50 + peregrine_rocket_pod_projectile.Damage3 = 45 + peregrine_rocket_pod_projectile.Damage4 = 40 + peregrine_rocket_pod_projectile.Acceleration = 10 + peregrine_rocket_pod_projectile.AccelerationUntil = 2f + peregrine_rocket_pod_projectile.DamageAtEdge = 0.1f + peregrine_rocket_pod_projectile.DamageRadius = 3f + peregrine_rocket_pod_projectile.ProjectileDamageType = DamageType.Splash + peregrine_rocket_pod_projectile.InitialVelocity = 200 + peregrine_rocket_pod_projectile.Lifespan = 1.85f + ProjectileDefinition.CalculateDerivedFields(peregrine_rocket_pod_projectile) + peregrine_rocket_pod_projectile.Modifiers = RadialDegrade + + peregrine_sparrow_projectile.Name = "peregrine_sparrow_projectile" + // TODO for later, maybe : set_resource_parent peregrine_sparrow_projectile game_objects sparrow_projectile + peregrine_sparrow_projectile.Damage0 = 20 + peregrine_sparrow_projectile.Damage1 = 40 + peregrine_sparrow_projectile.Damage2 = 30 + peregrine_sparrow_projectile.Damage3 = 30 + peregrine_sparrow_projectile.Damage4 = 31 + peregrine_sparrow_projectile.Acceleration = 12 + peregrine_sparrow_projectile.AccelerationUntil = 5f + peregrine_sparrow_projectile.DamageAtEdge = 0.1f + peregrine_sparrow_projectile.DamageRadius = 2f + peregrine_sparrow_projectile.ProjectileDamageType = DamageType.Splash + peregrine_sparrow_projectile.InitialVelocity = 45 + peregrine_sparrow_projectile.Lifespan = 7.5f + peregrine_sparrow_projectile.registerAs = "rc-projectiles" + peregrine_sparrow_projectile.ExistsOnRemoteClients = true + peregrine_sparrow_projectile.RemoteClientData = (13107, 187) //sparrow_projectile data + peregrine_sparrow_projectile.AutoLock = true + peregrine_sparrow_projectile.Packet = projectileConverter + ProjectileDefinition.CalculateDerivedFields(peregrine_sparrow_projectile) + peregrine_sparrow_projectile.Modifiers = RadialDegrade + + armor_siphon_projectile.Name = "armor_siphon_projectile" + armor_siphon_projectile.Damage0 = 0 + armor_siphon_projectile.Damage1 = 20 //ground vehicles, siphon drain + armor_siphon_projectile.Damage2 = 20 //aircraft, siphon drain + armor_siphon_projectile.Damage3 = 0 + armor_siphon_projectile.Damage4 = 20 //bfr's, siphon drain + armor_siphon_projectile.DamageToVehicleOnly = true + armor_siphon_projectile.DamageToBattleframeOnly = true + armor_siphon_projectile.DamageRadius = 35f + armor_siphon_projectile.ProjectileDamageType = DamageType.Siphon + ProjectileDefinition.CalculateDerivedFields(armor_siphon_projectile) + armor_siphon_projectile.Modifiers = List( + ArmorSiphonMaxDistanceCutoff, + ArmorSiphonRepairHost + ) + + ntu_siphon_emp.Name = "ntu_siphon_emp" + ntu_siphon_emp.Damage0 = 1 + ntu_siphon_emp.Damage1 = 1 + ntu_siphon_emp.DamageAtEdge = 0.1f + ntu_siphon_emp.DamageRadius = 50f + ntu_siphon_emp.ProjectileDamageType = DamageType.Splash + ntu_siphon_emp.JammedEffectDuration += TargetValidation( + EffectTarget.Category.Player, + EffectTarget.Validation.Player + ) -> 1000 + ntu_siphon_emp.JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.AMS + ) -> 5000 + ntu_siphon_emp.JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.MotionSensor + ) -> 30000 + ntu_siphon_emp.JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.Spitfire + ) -> 30000 + ntu_siphon_emp.JammedEffectDuration += TargetValidation( + EffectTarget.Category.Turret, + EffectTarget.Validation.Turret + ) -> 30000 + ntu_siphon_emp.JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.VehicleNotAMS + ) -> 10000 + ProjectileDefinition.CalculateDerivedFields(ntu_siphon_emp) + ntu_siphon_emp.Modifiers = MaxDistanceCutoff + } + /** * Initialize `ToolDefinition` globals. */ private def init_tools(): Unit = { + init_infantry_tools() + init_vehicle_tools() + } + + /** + * Initialize `ToolDefinition` globals. + */ + private def init_infantry_tools(): Unit = { chainblade.Name = "chainblade" chainblade.Size = EquipmentSize.Melee chainblade.AmmoTypes += melee_ammo @@ -5061,7 +5613,12 @@ object GlobalDefinitions { router_telepad.Modes.head.Item(DeployedItem.router_telepad_deployable, Set(Certification.GroundSupport)) router_telepad.Tile = InventoryTile.Tile33 router_telepad.Packet = new TelepadConverter + } + /** + * Initialize `ToolDefinition` globals. + */ + private def init_vehicle_tools(): Unit = { fury_weapon_systema.Name = "fury_weapon_systema" fury_weapon_systema.Size = EquipmentSize.VehicleWeapon fury_weapon_systema.AmmoTypes += hellfire_ammo @@ -5660,6 +6217,690 @@ object GlobalDefinitions { energy_gun_vs.FireModes.head.AmmoTypeIndices += 0 energy_gun_vs.FireModes.head.AmmoSlotIndex = 0 energy_gun_vs.FireModes.head.Magazine = 100 + + val battleFrameToolConverter = new BattleFrameToolConverter + aphelion_armor_siphon.Name = "aphelion_armor_siphon" + aphelion_armor_siphon.Size = EquipmentSize.BFRArmWeapon + aphelion_armor_siphon.AmmoTypes += armor_siphon_ammo + aphelion_armor_siphon.ProjectileTypes += armor_siphon_projectile + aphelion_armor_siphon.FireModes += new FireModeDefinition + aphelion_armor_siphon.FireModes.head.AmmoTypeIndices += 0 + aphelion_armor_siphon.FireModes.head.AmmoSlotIndex = 0 + aphelion_armor_siphon.FireModes.head.Magazine = 100 + aphelion_armor_siphon.Packet = battleFrameToolConverter + aphelion_armor_siphon.Tile = InventoryTile.Tile84 + + aphelion_armor_siphon_left.Name = "aphelion_armor_siphon_left" + aphelion_armor_siphon_left.Size = EquipmentSize.BFRArmWeapon + aphelion_armor_siphon_left.AmmoTypes += armor_siphon_ammo + aphelion_armor_siphon_left.ProjectileTypes += armor_siphon_projectile + aphelion_armor_siphon_left.FireModes += new FireModeDefinition + aphelion_armor_siphon_left.FireModes.head.AmmoTypeIndices += 0 + aphelion_armor_siphon_left.FireModes.head.AmmoSlotIndex = 0 + aphelion_armor_siphon_left.FireModes.head.Magazine = 100 + aphelion_armor_siphon_left.Packet = battleFrameToolConverter + aphelion_armor_siphon_left.Tile = InventoryTile.Tile84 + + aphelion_armor_siphon_right.Name = "aphelion_armor_siphon_right" + aphelion_armor_siphon_right.Size = EquipmentSize.BFRArmWeapon + aphelion_armor_siphon_right.AmmoTypes += armor_siphon_ammo + aphelion_armor_siphon_right.ProjectileTypes += armor_siphon_projectile + aphelion_armor_siphon_right.FireModes += new FireModeDefinition + aphelion_armor_siphon_right.FireModes.head.AmmoTypeIndices += 0 + aphelion_armor_siphon_right.FireModes.head.AmmoSlotIndex = 0 + aphelion_armor_siphon_right.FireModes.head.Magazine = 100 + aphelion_armor_siphon_right.Packet = battleFrameToolConverter + aphelion_armor_siphon_right.Tile = InventoryTile.Tile84 + + aphelion_laser.Name = "aphelion_laser" + aphelion_laser.Size = EquipmentSize.BFRArmWeapon + aphelion_laser.AmmoTypes += aphelion_laser_ammo + aphelion_laser.ProjectileTypes += aphelion_laser_projectile + aphelion_laser.FireModes += new FireModeDefinition + aphelion_laser.FireModes.head.AmmoTypeIndices += 0 + aphelion_laser.FireModes.head.AmmoSlotIndex = 0 + aphelion_laser.FireModes.head.Magazine = 350 + aphelion_laser.Packet = battleFrameToolConverter + aphelion_laser.Tile = InventoryTile.Tile84 + + aphelion_laser_left.Name = "aphelion_laser_left" + aphelion_laser_left.Size = EquipmentSize.BFRArmWeapon + aphelion_laser_left.AmmoTypes += aphelion_laser_ammo + aphelion_laser_left.ProjectileTypes += aphelion_laser_projectile + aphelion_laser_left.FireModes += new FireModeDefinition + aphelion_laser_left.FireModes.head.AmmoTypeIndices += 0 + aphelion_laser_left.FireModes.head.AmmoSlotIndex = 0 + aphelion_laser_left.FireModes.head.Magazine = 350 + aphelion_laser_left.Packet = battleFrameToolConverter + aphelion_laser_left.Tile = InventoryTile.Tile84 + + aphelion_laser_right.Name = "aphelion_laser_right" + aphelion_laser_right.Size = EquipmentSize.BFRArmWeapon + aphelion_laser_right.AmmoTypes += aphelion_laser_ammo + aphelion_laser_right.ProjectileTypes += aphelion_laser_projectile + aphelion_laser_right.FireModes += new FireModeDefinition + aphelion_laser_right.FireModes.head.AmmoTypeIndices += 0 + aphelion_laser_right.FireModes.head.AmmoSlotIndex = 0 + aphelion_laser_right.FireModes.head.Magazine = 350 + aphelion_laser_right.Packet = battleFrameToolConverter + aphelion_laser_right.Tile = InventoryTile.Tile84 + + aphelion_ntu_siphon.Name = "aphelion_ntu_siphon" + aphelion_ntu_siphon.Size = EquipmentSize.BFRArmWeapon + aphelion_ntu_siphon.AmmoTypes += ntu_siphon_ammo + aphelion_ntu_siphon.ProjectileTypes += no_projectile + aphelion_ntu_siphon.ProjectileTypes += ntu_siphon_emp + aphelion_ntu_siphon.FireModes += new FireModeDefinition + aphelion_ntu_siphon.FireModes.head.AmmoTypeIndices += 0 + aphelion_ntu_siphon.FireModes.head.AmmoSlotIndex = 0 + aphelion_ntu_siphon.FireModes.head.RoundsPerShot = 5 + aphelion_ntu_siphon.FireModes.head.Magazine = 150 + aphelion_ntu_siphon.FireModes.head.DefaultMagazine = 0 + aphelion_ntu_siphon.FireModes += new FireModeDefinition + aphelion_ntu_siphon.FireModes(1).AmmoTypeIndices += 0 + aphelion_ntu_siphon.FireModes(1).AmmoSlotIndex = 0 + aphelion_ntu_siphon.FireModes(1).ProjectileTypeIndices += 1 + aphelion_ntu_siphon.FireModes(1).RoundsPerShot = 30 + aphelion_ntu_siphon.FireModes(1).Magazine = 150 + aphelion_ntu_siphon.FireModes(1).DefaultMagazine = 0 + aphelion_ntu_siphon.Packet = battleFrameToolConverter + aphelion_ntu_siphon.Tile = InventoryTile.Tile84 + + aphelion_ntu_siphon_left.Name = "aphelion_ntu_siphon_left" + aphelion_ntu_siphon_left.Size = EquipmentSize.BFRArmWeapon + aphelion_ntu_siphon_left.AmmoTypes += ntu_siphon_ammo + aphelion_ntu_siphon_left.ProjectileTypes += no_projectile + aphelion_ntu_siphon_left.ProjectileTypes += ntu_siphon_emp + aphelion_ntu_siphon_left.FireModes += new FireModeDefinition + aphelion_ntu_siphon_left.FireModes.head.AmmoTypeIndices += 0 + aphelion_ntu_siphon_left.FireModes.head.AmmoSlotIndex = 0 + aphelion_ntu_siphon_left.FireModes.head.RoundsPerShot = 5 + aphelion_ntu_siphon_left.FireModes.head.Magazine = 150 + aphelion_ntu_siphon_left.FireModes.head.DefaultMagazine = 0 + aphelion_ntu_siphon_left.FireModes += new FireModeDefinition + aphelion_ntu_siphon_left.FireModes(1).AmmoTypeIndices += 0 + aphelion_ntu_siphon_left.FireModes(1).AmmoSlotIndex = 0 + aphelion_ntu_siphon_left.FireModes(1).ProjectileTypeIndices += 1 + aphelion_ntu_siphon_left.FireModes(1).RoundsPerShot = 30 + aphelion_ntu_siphon_left.FireModes(1).Magazine = 150 + aphelion_ntu_siphon_left.FireModes(1).DefaultMagazine = 0 + aphelion_ntu_siphon_left.Packet = battleFrameToolConverter + aphelion_ntu_siphon_left.Tile = InventoryTile.Tile84 + + aphelion_ntu_siphon_right.Name = "aphelion_ntu_siphon_right" + aphelion_ntu_siphon_right.Size = EquipmentSize.BFRArmWeapon + aphelion_ntu_siphon_right.AmmoTypes += ntu_siphon_ammo + aphelion_ntu_siphon_right.ProjectileTypes += no_projectile + aphelion_ntu_siphon_right.ProjectileTypes += ntu_siphon_emp + aphelion_ntu_siphon_right.FireModes += new FireModeDefinition + aphelion_ntu_siphon_right.FireModes.head.AmmoTypeIndices += 0 + aphelion_ntu_siphon_right.FireModes.head.AmmoSlotIndex = 0 + aphelion_ntu_siphon_right.FireModes.head.RoundsPerShot = 5 + aphelion_ntu_siphon_right.FireModes.head.Magazine = 150 + aphelion_ntu_siphon_right.FireModes.head.DefaultMagazine = 0 + aphelion_ntu_siphon_right.FireModes += new FireModeDefinition + aphelion_ntu_siphon_right.FireModes(1).AmmoTypeIndices += 0 + aphelion_ntu_siphon_right.FireModes(1).AmmoSlotIndex = 0 + aphelion_ntu_siphon_right.FireModes(1).ProjectileTypeIndices += 1 + aphelion_ntu_siphon_right.FireModes(1).RoundsPerShot = 30 + aphelion_ntu_siphon_right.FireModes(1).Magazine = 150 + aphelion_ntu_siphon_right.FireModes(1).DefaultMagazine = 0 + aphelion_ntu_siphon_right.Packet = battleFrameToolConverter + aphelion_ntu_siphon_right.Tile = InventoryTile.Tile84 + + aphelion_ppa.Name = "aphelion_ppa" + aphelion_ppa.Size = EquipmentSize.BFRArmWeapon + aphelion_ppa.AmmoTypes += aphelion_ppa_ammo + aphelion_ppa.ProjectileTypes += aphelion_ppa_projectile + aphelion_ppa.FireModes += new FireModeDefinition + aphelion_ppa.FireModes.head.AmmoTypeIndices += 0 + aphelion_ppa.FireModes.head.AmmoSlotIndex = 0 + aphelion_ppa.FireModes.head.Magazine = 25 + aphelion_ppa.Packet = battleFrameToolConverter + aphelion_ppa.Tile = InventoryTile.Tile84 + + aphelion_ppa_left.Name = "aphelion_ppa_left" + aphelion_ppa_left.Size = EquipmentSize.BFRArmWeapon + aphelion_ppa_left.AmmoTypes += aphelion_ppa_ammo + aphelion_ppa_left.ProjectileTypes += aphelion_ppa_projectile + aphelion_ppa_left.FireModes += new FireModeDefinition + aphelion_ppa_left.FireModes.head.AmmoTypeIndices += 0 + aphelion_ppa_left.FireModes.head.AmmoSlotIndex = 0 + aphelion_ppa_left.FireModes.head.Magazine = 25 + aphelion_ppa_left.Packet = battleFrameToolConverter + aphelion_ppa_left.Tile = InventoryTile.Tile84 + + aphelion_ppa_right.Name = "aphelion_ppa_right" + aphelion_ppa_right.Size = EquipmentSize.BFRArmWeapon + aphelion_ppa_right.AmmoTypes += aphelion_ppa_ammo + aphelion_ppa_right.ProjectileTypes += aphelion_ppa_projectile + aphelion_ppa_right.FireModes += new FireModeDefinition + aphelion_ppa_right.FireModes.head.AmmoTypeIndices += 0 + aphelion_ppa_right.FireModes.head.AmmoSlotIndex = 0 + aphelion_ppa_right.FireModes.head.Magazine = 25 + aphelion_ppa_right.Packet = battleFrameToolConverter + aphelion_ppa_right.Tile = InventoryTile.Tile84 + + aphelion_starfire.Name = "aphelion_starfire" + aphelion_starfire.Size = EquipmentSize.BFRArmWeapon + aphelion_starfire.AmmoTypes += aphelion_starfire_ammo + aphelion_starfire.ProjectileTypes += aphelion_starfire_projectile + aphelion_starfire.FireModes += new FireModeDefinition + aphelion_starfire.FireModes.head.AmmoTypeIndices += 0 + aphelion_starfire.FireModes.head.AmmoSlotIndex = 0 + aphelion_starfire.FireModes.head.Magazine = 20 + aphelion_starfire.Packet = battleFrameToolConverter + aphelion_starfire.Tile = InventoryTile.Tile84 + + aphelion_starfire_left.Name = "aphelion_starfire_left" + aphelion_starfire_left.Size = EquipmentSize.BFRArmWeapon + aphelion_starfire_left.AmmoTypes += aphelion_starfire_ammo + aphelion_starfire_left.ProjectileTypes += aphelion_starfire_projectile + aphelion_starfire_left.FireModes += new FireModeDefinition + aphelion_starfire_left.FireModes.head.AmmoTypeIndices += 0 + aphelion_starfire_left.FireModes.head.AmmoSlotIndex = 0 + aphelion_starfire_left.FireModes.head.Magazine = 20 + aphelion_starfire_left.Packet = battleFrameToolConverter + aphelion_starfire_left.Tile = InventoryTile.Tile84 + + aphelion_starfire_right.Name = "aphelion_starfire_right" + aphelion_starfire_right.Size = EquipmentSize.BFRArmWeapon + aphelion_starfire_right.AmmoTypes += aphelion_starfire_ammo + aphelion_starfire_right.ProjectileTypes += aphelion_starfire_projectile + aphelion_starfire_right.FireModes += new FireModeDefinition + aphelion_starfire_right.FireModes.head.AmmoTypeIndices += 0 + aphelion_starfire_right.FireModes.head.AmmoSlotIndex = 0 + aphelion_starfire_right.FireModes.head.Magazine = 20 + aphelion_starfire_right.Packet = battleFrameToolConverter + aphelion_starfire_right.Tile = InventoryTile.Tile84 + + aphelion_plasma_rocket_pod.Name = "aphelion_plasma_rocket_pod" + aphelion_plasma_rocket_pod.Size = EquipmentSize.BFRGunnerWeapon + aphelion_plasma_rocket_pod.AmmoTypes += aphelion_plasma_rocket_ammo + aphelion_plasma_rocket_pod.ProjectileTypes += aphelion_plasma_rocket_projectile + aphelion_plasma_rocket_pod.FireModes += new FireModeDefinition + aphelion_plasma_rocket_pod.FireModes.head.AmmoTypeIndices += 0 + aphelion_plasma_rocket_pod.FireModes.head.AmmoSlotIndex = 0 + aphelion_plasma_rocket_pod.FireModes.head.Magazine = 40 + aphelion_plasma_rocket_pod.Packet = battleFrameToolConverter + aphelion_plasma_rocket_pod.Tile = InventoryTile.Tile1004 + + aphelion_immolation_cannon.Name = "aphelion_immolation_cannon" + aphelion_immolation_cannon.Size = EquipmentSize.BFRGunnerWeapon + aphelion_immolation_cannon.AmmoTypes += aphelion_immolation_cannon_ammo + aphelion_immolation_cannon.ProjectileTypes += aphelion_immolation_cannon_projectile + aphelion_immolation_cannon.FireModes += new FireModeDefinition + aphelion_immolation_cannon.FireModes.head.AmmoTypeIndices += 0 + aphelion_immolation_cannon.FireModes.head.AmmoSlotIndex = 0 + aphelion_immolation_cannon.FireModes.head.Magazine = 25 + aphelion_immolation_cannon.Packet = battleFrameToolConverter + aphelion_immolation_cannon.Tile = InventoryTile.Tile1004 + + colossus_armor_siphon.Name = "colossus_armor_siphon" + colossus_armor_siphon.Size = EquipmentSize.BFRArmWeapon + colossus_armor_siphon.AmmoTypes += armor_siphon_ammo + colossus_armor_siphon.ProjectileTypes += armor_siphon_projectile + colossus_armor_siphon.FireModes += new FireModeDefinition + colossus_armor_siphon.FireModes.head.AmmoTypeIndices += 0 + colossus_armor_siphon.FireModes.head.AmmoSlotIndex = 0 + colossus_armor_siphon.FireModes.head.Magazine = 100 + colossus_armor_siphon.Packet = battleFrameToolConverter + colossus_armor_siphon.Tile = InventoryTile.Tile84 + + colossus_armor_siphon_left.Name = "colossus_armor_siphon_left" + colossus_armor_siphon_left.Size = EquipmentSize.BFRArmWeapon + colossus_armor_siphon_left.AmmoTypes += armor_siphon_ammo + colossus_armor_siphon_left.ProjectileTypes += armor_siphon_projectile + colossus_armor_siphon_left.FireModes += new FireModeDefinition + colossus_armor_siphon_left.FireModes.head.AmmoTypeIndices += 0 + colossus_armor_siphon_left.FireModes.head.AmmoSlotIndex = 0 + colossus_armor_siphon_left.FireModes.head.Magazine = 100 + colossus_armor_siphon_left.Packet = battleFrameToolConverter + colossus_armor_siphon_left.Tile = InventoryTile.Tile84 + + colossus_armor_siphon_right.Name = "colossus_armor_siphon_right" + colossus_armor_siphon_right.Size = EquipmentSize.BFRArmWeapon + colossus_armor_siphon_right.AmmoTypes += armor_siphon_ammo + colossus_armor_siphon_right.ProjectileTypes += armor_siphon_projectile + colossus_armor_siphon_right.FireModes += new FireModeDefinition + colossus_armor_siphon_right.FireModes.head.AmmoTypeIndices += 0 + colossus_armor_siphon_right.FireModes.head.AmmoSlotIndex = 0 + colossus_armor_siphon_right.FireModes.head.Magazine = 100 + colossus_armor_siphon_right.Packet = battleFrameToolConverter + colossus_armor_siphon_right.Tile = InventoryTile.Tile84 + + colossus_burster.Name = "colossus_burster" + colossus_burster.Size = EquipmentSize.BFRArmWeapon + colossus_burster.AmmoTypes += colossus_burster_ammo + colossus_burster.ProjectileTypes += colossus_burster_projectile + colossus_burster.FireModes += new FireModeDefinition + colossus_burster.FireModes.head.AmmoTypeIndices += 0 + colossus_burster.FireModes.head.AmmoSlotIndex = 0 + colossus_burster.FireModes.head.Magazine = 25 + colossus_burster.Packet = battleFrameToolConverter + colossus_burster.Tile = InventoryTile.Tile84 + + colossus_burster_left.Name = "colossus_burster_left" + colossus_burster_left.Size = EquipmentSize.BFRArmWeapon + colossus_burster_left.AmmoTypes += colossus_burster_ammo + colossus_burster_left.ProjectileTypes += colossus_burster_projectile + colossus_burster_left.FireModes += new FireModeDefinition + colossus_burster_left.FireModes.head.AmmoTypeIndices += 0 + colossus_burster_left.FireModes.head.AmmoSlotIndex = 0 + colossus_burster_left.FireModes.head.Magazine = 25 + colossus_burster_left.Packet = battleFrameToolConverter + colossus_burster_left.Tile = InventoryTile.Tile84 + + colossus_burster_right.Name = "colossus_burster_right" + colossus_burster_right.Size = EquipmentSize.BFRArmWeapon + colossus_burster_right.AmmoTypes += colossus_burster_ammo + colossus_burster_right.ProjectileTypes += colossus_burster_projectile + colossus_burster_right.FireModes += new FireModeDefinition + colossus_burster_right.FireModes.head.AmmoTypeIndices += 0 + colossus_burster_right.FireModes.head.AmmoSlotIndex = 0 + colossus_burster_right.FireModes.head.Magazine = 25 + colossus_burster_right.Packet = battleFrameToolConverter + colossus_burster_right.Tile = InventoryTile.Tile84 + + colossus_chaingun.Name = "colossus_chaingun" + colossus_chaingun.Size = EquipmentSize.BFRArmWeapon + colossus_chaingun.AmmoTypes += colossus_chaingun_ammo + colossus_chaingun.ProjectileTypes += colossus_chaingun_projectile + colossus_chaingun.FireModes += new FireModeDefinition + colossus_chaingun.FireModes.head.AmmoTypeIndices += 0 + colossus_chaingun.FireModes.head.AmmoSlotIndex = 0 + colossus_chaingun.FireModes.head.Magazine = 125 + colossus_chaingun.Packet = battleFrameToolConverter + colossus_chaingun.Tile = InventoryTile.Tile84 + + colossus_chaingun_left.Name = "colossus_chaingun_left" + colossus_chaingun_left.Size = EquipmentSize.BFRArmWeapon + colossus_chaingun_left.AmmoTypes += colossus_chaingun_ammo + colossus_chaingun_left.ProjectileTypes += colossus_chaingun_projectile + colossus_chaingun_left.FireModes += new FireModeDefinition + colossus_chaingun_left.FireModes.head.AmmoTypeIndices += 0 + colossus_chaingun_left.FireModes.head.AmmoSlotIndex = 0 + colossus_chaingun_left.FireModes.head.Magazine = 125 + colossus_chaingun_left.Packet = battleFrameToolConverter + colossus_chaingun_left.Tile = InventoryTile.Tile84 + + colossus_chaingun_right.Name = "colossus_chaingun_right" + colossus_chaingun_right.Size = EquipmentSize.BFRArmWeapon + colossus_chaingun_right.AmmoTypes += colossus_chaingun_ammo + colossus_chaingun_right.ProjectileTypes += colossus_chaingun_projectile + colossus_chaingun_right.FireModes += new FireModeDefinition + colossus_chaingun_right.FireModes.head.AmmoTypeIndices += 0 + colossus_chaingun_right.FireModes.head.AmmoSlotIndex = 0 + colossus_chaingun_right.FireModes.head.Magazine = 125 + colossus_chaingun_right.Packet = battleFrameToolConverter + colossus_chaingun_right.Tile = InventoryTile.Tile84 + + colossus_ntu_siphon.Name = "colossus_ntu_siphon" + colossus_ntu_siphon.Size = EquipmentSize.BFRArmWeapon + colossus_ntu_siphon.AmmoTypes += ntu_siphon_ammo + colossus_ntu_siphon.ProjectileTypes += no_projectile + colossus_ntu_siphon.ProjectileTypes += ntu_siphon_emp + colossus_ntu_siphon.FireModes += new FireModeDefinition + colossus_ntu_siphon.FireModes.head.AmmoTypeIndices += 0 + colossus_ntu_siphon.FireModes.head.AmmoSlotIndex = 0 + colossus_ntu_siphon.FireModes.head.RoundsPerShot = 5 + colossus_ntu_siphon.FireModes.head.Magazine = 150 + colossus_ntu_siphon.FireModes.head.DefaultMagazine = 0 + colossus_ntu_siphon.FireModes += new FireModeDefinition + colossus_ntu_siphon.FireModes(1).AmmoTypeIndices += 0 + colossus_ntu_siphon.FireModes(1).AmmoSlotIndex = 0 + colossus_ntu_siphon.FireModes(1).ProjectileTypeIndices += 1 + colossus_ntu_siphon.FireModes(1).RoundsPerShot = 30 + colossus_ntu_siphon.FireModes(1).Magazine = 150 + colossus_ntu_siphon.FireModes(1).DefaultMagazine = 0 + colossus_ntu_siphon.FireModes(1).AmmoSlotIndex = 0 + colossus_ntu_siphon.Packet = battleFrameToolConverter + colossus_ntu_siphon.Tile = InventoryTile.Tile84 + + colossus_ntu_siphon_left.Name = "colossus_ntu_siphon_left" + colossus_ntu_siphon_left.Size = EquipmentSize.BFRArmWeapon + colossus_ntu_siphon_left.AmmoTypes += ntu_siphon_ammo + colossus_ntu_siphon_left.ProjectileTypes += no_projectile + colossus_ntu_siphon_left.ProjectileTypes += ntu_siphon_emp + colossus_ntu_siphon_left.FireModes += new FireModeDefinition + colossus_ntu_siphon_left.FireModes.head.AmmoTypeIndices += 0 + colossus_ntu_siphon_left.FireModes.head.AmmoSlotIndex = 0 + colossus_ntu_siphon_left.FireModes.head.RoundsPerShot = 5 + colossus_ntu_siphon_left.FireModes.head.Magazine = 150 + colossus_ntu_siphon_left.FireModes.head.DefaultMagazine = 0 + colossus_ntu_siphon_left.FireModes += new FireModeDefinition + colossus_ntu_siphon_left.FireModes(1).AmmoTypeIndices += 0 + colossus_ntu_siphon_left.FireModes(1).AmmoSlotIndex = 0 + colossus_ntu_siphon_left.FireModes(1).ProjectileTypeIndices += 1 + colossus_ntu_siphon_left.FireModes(1).RoundsPerShot = 30 + colossus_ntu_siphon_left.FireModes(1).Magazine = 150 + colossus_ntu_siphon_left.FireModes(1).DefaultMagazine = 0 + colossus_ntu_siphon_left.FireModes(1).AmmoSlotIndex = 0 + colossus_ntu_siphon_left.Packet = battleFrameToolConverter + colossus_ntu_siphon_left.Tile = InventoryTile.Tile84 + + colossus_ntu_siphon_right.Name = "colossus_ntu_siphon_right" + colossus_ntu_siphon_right.Size = EquipmentSize.BFRArmWeapon + colossus_ntu_siphon_right.AmmoTypes += ntu_siphon_ammo + colossus_ntu_siphon_right.ProjectileTypes += no_projectile + colossus_ntu_siphon_right.ProjectileTypes += ntu_siphon_emp + colossus_ntu_siphon_right.FireModes += new FireModeDefinition + colossus_ntu_siphon_right.FireModes.head.AmmoTypeIndices += 0 + colossus_ntu_siphon_right.FireModes.head.AmmoSlotIndex = 0 + colossus_ntu_siphon_right.FireModes.head.RoundsPerShot = 5 + colossus_ntu_siphon_right.FireModes.head.Magazine = 150 + colossus_ntu_siphon_right.FireModes.head.DefaultMagazine = 0 + colossus_ntu_siphon_right.FireModes += new FireModeDefinition + colossus_ntu_siphon_right.FireModes(1).AmmoTypeIndices += 0 + colossus_ntu_siphon_right.FireModes(1).AmmoSlotIndex = 0 + colossus_ntu_siphon_right.FireModes(1).ProjectileTypeIndices += 1 + colossus_ntu_siphon_right.FireModes(1).RoundsPerShot = 30 + colossus_ntu_siphon_right.FireModes(1).Magazine = 150 + colossus_ntu_siphon_right.FireModes(1).DefaultMagazine = 0 + colossus_ntu_siphon_right.FireModes(1).AmmoSlotIndex = 0 + colossus_ntu_siphon_right.Packet = battleFrameToolConverter + colossus_ntu_siphon_right.Tile = InventoryTile.Tile84 + + colossus_tank_cannon.Name = "colossus_tank_cannon" + colossus_tank_cannon.Size = EquipmentSize.BFRArmWeapon + colossus_tank_cannon.AmmoTypes += colossus_tank_cannon_ammo + colossus_tank_cannon.ProjectileTypes += colossus_tank_cannon_projectile + colossus_tank_cannon.FireModes += new FireModeDefinition + colossus_tank_cannon.FireModes.head.AmmoTypeIndices += 0 + colossus_tank_cannon.FireModes.head.AmmoSlotIndex = 0 + colossus_tank_cannon.FireModes.head.Magazine = 25 + colossus_tank_cannon.Packet = battleFrameToolConverter + colossus_tank_cannon.Tile = InventoryTile.Tile84 + + colossus_tank_cannon_left.Name = "colossus_tank_cannon_left" + colossus_tank_cannon_left.Size = EquipmentSize.BFRArmWeapon + colossus_tank_cannon_left.AmmoTypes += colossus_tank_cannon_ammo + colossus_tank_cannon_left.ProjectileTypes += colossus_tank_cannon_projectile + colossus_tank_cannon_left.FireModes += new FireModeDefinition + colossus_tank_cannon_left.FireModes.head.AmmoTypeIndices += 0 + colossus_tank_cannon_left.FireModes.head.AmmoSlotIndex = 0 + colossus_tank_cannon_left.FireModes.head.Magazine = 25 + colossus_tank_cannon_left.Packet = battleFrameToolConverter + colossus_tank_cannon_left.Tile = InventoryTile.Tile84 + + colossus_tank_cannon_right.Name = "colossus_tank_cannon_right" + colossus_tank_cannon_right.Size = EquipmentSize.BFRArmWeapon + colossus_tank_cannon_right.AmmoTypes += colossus_tank_cannon_ammo + colossus_tank_cannon_right.ProjectileTypes += colossus_tank_cannon_projectile + colossus_tank_cannon_right.FireModes += new FireModeDefinition + colossus_tank_cannon_right.FireModes.head.AmmoTypeIndices += 0 + colossus_tank_cannon_right.FireModes.head.AmmoSlotIndex = 0 + colossus_tank_cannon_right.FireModes.head.Magazine = 25 + colossus_tank_cannon_right.Packet = battleFrameToolConverter + colossus_tank_cannon_right.Tile = InventoryTile.Tile84 + + colossus_dual_100mm_cannons.Name = "colossus_dual_100mm_cannons" + colossus_dual_100mm_cannons.Size = EquipmentSize.BFRGunnerWeapon + colossus_dual_100mm_cannons.AmmoTypes += colossus_100mm_cannon_ammo + colossus_dual_100mm_cannons.ProjectileTypes += colossus_100mm_projectile + colossus_dual_100mm_cannons.FireModes += new FireModeDefinition + colossus_dual_100mm_cannons.FireModes.head.AmmoTypeIndices += 0 + colossus_dual_100mm_cannons.FireModes.head.AmmoSlotIndex = 0 + colossus_dual_100mm_cannons.FireModes.head.Magazine = 22 + colossus_dual_100mm_cannons.Packet = battleFrameToolConverter + colossus_dual_100mm_cannons.Tile = InventoryTile.Tile1004 + + colossus_cluster_bomb_pod.Name = "colossus_cluster_bomb_pod" + colossus_cluster_bomb_pod.Size = EquipmentSize.BFRGunnerWeapon + colossus_cluster_bomb_pod.AmmoTypes += colossus_cluster_bomb_ammo + colossus_cluster_bomb_pod.ProjectileTypes += colossus_cluster_bomb_projectile + colossus_cluster_bomb_pod.FireModes += new FireModeDefinition + colossus_cluster_bomb_pod.FireModes.head.AmmoTypeIndices += 0 + colossus_cluster_bomb_pod.FireModes.head.AmmoSlotIndex = 0 + colossus_cluster_bomb_pod.FireModes.head.Magazine = 125 + colossus_cluster_bomb_pod.FireModes += new FireModeDefinition + colossus_cluster_bomb_pod.FireModes(1).AmmoTypeIndices += 0 + colossus_cluster_bomb_pod.FireModes(1).AmmoSlotIndex = 0 + colossus_cluster_bomb_pod.FireModes(1).Magazine = 125 + colossus_cluster_bomb_pod.Packet = battleFrameToolConverter + colossus_cluster_bomb_pod.Tile = InventoryTile.Tile1004 + + peregrine_armor_siphon.Name = "peregrine_armor_siphon" + peregrine_armor_siphon.Size = EquipmentSize.BFRArmWeapon + peregrine_armor_siphon.AmmoTypes += armor_siphon_ammo + peregrine_armor_siphon.ProjectileTypes += armor_siphon_projectile + peregrine_armor_siphon.FireModes += new FireModeDefinition + peregrine_armor_siphon.FireModes.head.AmmoTypeIndices += 0 + peregrine_armor_siphon.FireModes.head.AmmoSlotIndex = 0 + peregrine_armor_siphon.FireModes.head.Magazine = 100 + peregrine_armor_siphon.Packet = battleFrameToolConverter + peregrine_armor_siphon.Tile = InventoryTile.Tile84 + + peregrine_armor_siphon_left.Name = "peregrine_armor_siphon_left" + peregrine_armor_siphon_left.Size = EquipmentSize.BFRArmWeapon + peregrine_armor_siphon_left.AmmoTypes += armor_siphon_ammo + peregrine_armor_siphon_left.ProjectileTypes += armor_siphon_projectile + peregrine_armor_siphon_left.FireModes += new FireModeDefinition + peregrine_armor_siphon_left.FireModes.head.AmmoTypeIndices += 0 + peregrine_armor_siphon_left.FireModes.head.AmmoSlotIndex = 0 + peregrine_armor_siphon_left.FireModes.head.Magazine = 100 + peregrine_armor_siphon_left.Packet = battleFrameToolConverter + peregrine_armor_siphon_left.Tile = InventoryTile.Tile84 + + peregrine_armor_siphon_right.Name = "peregrine_armor_siphon_right" + peregrine_armor_siphon_right.Size = EquipmentSize.BFRArmWeapon + peregrine_armor_siphon_right.AmmoTypes += armor_siphon_ammo + peregrine_armor_siphon_right.ProjectileTypes += armor_siphon_projectile + peregrine_armor_siphon_right.FireModes += new FireModeDefinition + peregrine_armor_siphon_right.FireModes.head.AmmoTypeIndices += 0 + peregrine_armor_siphon_right.FireModes.head.AmmoSlotIndex = 0 + peregrine_armor_siphon_right.FireModes.head.Magazine = 100 + peregrine_armor_siphon_right.Packet = battleFrameToolConverter + peregrine_armor_siphon_right.Tile = InventoryTile.Tile84 + + peregrine_dual_machine_gun.Name = "peregrine_dual_machine_gun" + peregrine_dual_machine_gun.Size = EquipmentSize.BFRArmWeapon + peregrine_dual_machine_gun.AmmoTypes += peregrine_dual_machine_gun_ammo + peregrine_dual_machine_gun.ProjectileTypes += peregrine_dual_machine_gun_projectile + peregrine_dual_machine_gun.FireModes += new FireModeDefinition + peregrine_dual_machine_gun.FireModes.head.AmmoTypeIndices += 0 + peregrine_dual_machine_gun.FireModes.head.AmmoSlotIndex = 0 + peregrine_dual_machine_gun.FireModes.head.Magazine = 55 + peregrine_dual_machine_gun.Packet = battleFrameToolConverter + peregrine_dual_machine_gun.Tile = InventoryTile.Tile84 + + peregrine_dual_machine_gun_left.Name = "peregrine_dual_machine_gun_left" + peregrine_dual_machine_gun_left.Size = EquipmentSize.BFRArmWeapon + peregrine_dual_machine_gun_left.AmmoTypes += peregrine_dual_machine_gun_ammo + peregrine_dual_machine_gun_left.ProjectileTypes += peregrine_dual_machine_gun_projectile + peregrine_dual_machine_gun_left.FireModes += new FireModeDefinition + peregrine_dual_machine_gun_left.FireModes.head.AmmoTypeIndices += 0 + peregrine_dual_machine_gun_left.FireModes.head.AmmoSlotIndex = 0 + peregrine_dual_machine_gun_left.FireModes.head.Magazine = 55 + peregrine_dual_machine_gun_left.Packet = battleFrameToolConverter + peregrine_dual_machine_gun_left.Tile = InventoryTile.Tile84 + + peregrine_dual_machine_gun_right.Name = "peregrine_dual_machine_gun_right" + peregrine_dual_machine_gun_right.Size = EquipmentSize.BFRArmWeapon + peregrine_dual_machine_gun_right.AmmoTypes += peregrine_dual_machine_gun_ammo + peregrine_dual_machine_gun_right.ProjectileTypes += peregrine_dual_machine_gun_projectile + peregrine_dual_machine_gun_right.FireModes += new FireModeDefinition + peregrine_dual_machine_gun_right.FireModes.head.AmmoTypeIndices += 0 + peregrine_dual_machine_gun_right.FireModes.head.AmmoSlotIndex = 0 + peregrine_dual_machine_gun_right.FireModes.head.Magazine = 55 + peregrine_dual_machine_gun_right.Packet = battleFrameToolConverter + peregrine_dual_machine_gun_right.Tile = InventoryTile.Tile84 + + peregrine_mechhammer.Name = "peregrine_mechhammer" + peregrine_mechhammer.Size = EquipmentSize.BFRArmWeapon + peregrine_mechhammer.AmmoTypes += peregrine_mechhammer_ammo + peregrine_mechhammer.ProjectileTypes += peregrine_mechhammer_projectile + peregrine_mechhammer.FireModes += new PelletFireModeDefinition + peregrine_mechhammer.FireModes.head.AmmoTypeIndices += 0 + peregrine_mechhammer.FireModes.head.AmmoSlotIndex = 0 + peregrine_mechhammer.FireModes.head.Magazine = 30 + peregrine_mechhammer.FireModes.head.Chamber = 16 //30 shells * 12 pellets = 480 + peregrine_mechhammer.FireModes += new PelletFireModeDefinition + peregrine_mechhammer.FireModes(1).AmmoTypeIndices += 0 + peregrine_mechhammer.FireModes(1).AmmoSlotIndex = 0 + peregrine_mechhammer.FireModes(1).Magazine = 30 + peregrine_mechhammer.FireModes(1).Chamber = 12 //30 shells * 12 pellets = 360 + peregrine_mechhammer.Packet = battleFrameToolConverter + peregrine_mechhammer.Tile = InventoryTile.Tile84 + + peregrine_mechhammer_left.Name = "peregrine_mechhammer_left" + peregrine_mechhammer_left.Size = EquipmentSize.BFRArmWeapon + peregrine_mechhammer_left.AmmoTypes += peregrine_mechhammer_ammo + peregrine_mechhammer_left.ProjectileTypes += peregrine_mechhammer_projectile + peregrine_mechhammer_left.FireModes += new PelletFireModeDefinition + peregrine_mechhammer_left.FireModes.head.AmmoTypeIndices += 0 + peregrine_mechhammer_left.FireModes.head.AmmoSlotIndex = 0 + peregrine_mechhammer_left.FireModes.head.Magazine = 30 + peregrine_mechhammer_left.FireModes.head.Chamber = 16 //30 shells * 12 pellets = 480 + peregrine_mechhammer_left.FireModes += new PelletFireModeDefinition + peregrine_mechhammer_left.FireModes(1).AmmoTypeIndices += 0 + peregrine_mechhammer_left.FireModes(1).AmmoSlotIndex = 0 + peregrine_mechhammer_left.FireModes(1).Magazine = 30 + peregrine_mechhammer_left.FireModes(1).Chamber = 12 //30 shells * 12 pellets = 360 + peregrine_mechhammer_left.Packet = battleFrameToolConverter + peregrine_mechhammer_left.Tile = InventoryTile.Tile84 + + peregrine_mechhammer_right.Name = "peregrine_mechhammer_right" + peregrine_mechhammer_right.Size = EquipmentSize.BFRArmWeapon + peregrine_mechhammer_right.AmmoTypes += peregrine_mechhammer_ammo + peregrine_mechhammer_right.ProjectileTypes += peregrine_mechhammer_projectile + peregrine_mechhammer_right.FireModes += new PelletFireModeDefinition + peregrine_mechhammer_right.FireModes.head.AmmoTypeIndices += 0 + peregrine_mechhammer_right.FireModes.head.AmmoSlotIndex = 0 + peregrine_mechhammer_right.FireModes.head.Magazine = 30 + peregrine_mechhammer_right.FireModes.head.Chamber = 16 //30 shells * 12 pellets = 480 + peregrine_mechhammer_right.FireModes += new PelletFireModeDefinition + peregrine_mechhammer_right.FireModes(1).AmmoTypeIndices += 0 + peregrine_mechhammer_right.FireModes(1).AmmoSlotIndex = 0 + peregrine_mechhammer_right.FireModes(1).Magazine = 30 + peregrine_mechhammer_right.FireModes(1).Chamber = 12 //30 shells * 12 pellets = 360 + peregrine_mechhammer_right.Packet = battleFrameToolConverter + peregrine_mechhammer_right.Tile = InventoryTile.Tile84 + + peregrine_ntu_siphon.Name = "peregrine_ntu_siphon" + peregrine_ntu_siphon.Size = EquipmentSize.BFRArmWeapon + peregrine_ntu_siphon.AmmoTypes += ntu_siphon_ammo + peregrine_ntu_siphon.ProjectileTypes += no_projectile + peregrine_ntu_siphon.ProjectileTypes += ntu_siphon_emp + peregrine_ntu_siphon.FireModes += new FireModeDefinition + peregrine_ntu_siphon.FireModes.head.AmmoTypeIndices += 0 + peregrine_ntu_siphon.FireModes.head.AmmoSlotIndex = 0 + peregrine_ntu_siphon.FireModes.head.RoundsPerShot = 5 + peregrine_ntu_siphon.FireModes.head.Magazine = 150 + peregrine_ntu_siphon.FireModes.head.DefaultMagazine = 0 + peregrine_ntu_siphon.FireModes += new FireModeDefinition + peregrine_ntu_siphon.FireModes(1).AmmoTypeIndices += 0 + peregrine_ntu_siphon.FireModes(1).AmmoSlotIndex = 0 + peregrine_ntu_siphon.FireModes(1).ProjectileTypeIndices += 1 + peregrine_ntu_siphon.FireModes(1).RoundsPerShot = 30 + peregrine_ntu_siphon.FireModes(1).Magazine = 150 + peregrine_ntu_siphon.FireModes(1).DefaultMagazine = 0 + peregrine_ntu_siphon.FireModes(1).AmmoSlotIndex = 0 + peregrine_ntu_siphon.Packet = battleFrameToolConverter + peregrine_ntu_siphon.Tile = InventoryTile.Tile84 + + peregrine_ntu_siphon_left.Name = "peregrine_ntu_siphon_left" + peregrine_ntu_siphon_left.Size = EquipmentSize.BFRArmWeapon + peregrine_ntu_siphon_left.AmmoTypes += ntu_siphon_ammo + peregrine_ntu_siphon_left.ProjectileTypes += no_projectile + peregrine_ntu_siphon_left.ProjectileTypes += ntu_siphon_emp + peregrine_ntu_siphon_left.FireModes += new FireModeDefinition + peregrine_ntu_siphon_left.FireModes.head.AmmoTypeIndices += 0 + peregrine_ntu_siphon_left.FireModes.head.AmmoSlotIndex = 0 + peregrine_ntu_siphon_left.FireModes.head.RoundsPerShot = 5 + peregrine_ntu_siphon_left.FireModes.head.Magazine = 150 + peregrine_ntu_siphon_left.FireModes.head.DefaultMagazine = 0 + peregrine_ntu_siphon_left.FireModes += new FireModeDefinition + peregrine_ntu_siphon_left.FireModes(1).AmmoTypeIndices += 0 + peregrine_ntu_siphon_left.FireModes(1).AmmoSlotIndex = 0 + peregrine_ntu_siphon_left.FireModes(1).ProjectileTypeIndices += 1 + peregrine_ntu_siphon_left.FireModes(1).RoundsPerShot = 30 + peregrine_ntu_siphon_left.FireModes(1).Magazine = 150 + peregrine_ntu_siphon_left.FireModes(1).DefaultMagazine = 0 + peregrine_ntu_siphon_left.FireModes(1).AmmoSlotIndex = 0 + peregrine_ntu_siphon_left.Packet = battleFrameToolConverter + peregrine_ntu_siphon_left.Tile = InventoryTile.Tile84 + + peregrine_ntu_siphon_right.Name = "peregrine_ntu_siphon_right" + peregrine_ntu_siphon_right.Size = EquipmentSize.BFRArmWeapon + peregrine_ntu_siphon_right.AmmoTypes += ntu_siphon_ammo + peregrine_ntu_siphon_right.ProjectileTypes += no_projectile + peregrine_ntu_siphon_right.ProjectileTypes += ntu_siphon_emp + peregrine_ntu_siphon_right.FireModes += new FireModeDefinition + peregrine_ntu_siphon_right.FireModes.head.AmmoTypeIndices += 0 + peregrine_ntu_siphon_right.FireModes.head.AmmoSlotIndex = 0 + peregrine_ntu_siphon_right.FireModes.head.RoundsPerShot = 5 + peregrine_ntu_siphon_right.FireModes.head.Magazine = 150 + peregrine_ntu_siphon_right.FireModes.head.DefaultMagazine = 0 + peregrine_ntu_siphon_right.FireModes += new FireModeDefinition + peregrine_ntu_siphon_right.FireModes(1).AmmoTypeIndices += 0 + peregrine_ntu_siphon_right.FireModes(1).AmmoSlotIndex = 0 + peregrine_ntu_siphon_right.FireModes(1).ProjectileTypeIndices += 1 + peregrine_ntu_siphon_right.FireModes(1).RoundsPerShot = 30 + peregrine_ntu_siphon_right.FireModes(1).Magazine = 150 + peregrine_ntu_siphon_right.FireModes(1).DefaultMagazine = 0 + peregrine_ntu_siphon_right.FireModes(1).AmmoSlotIndex = 0 + peregrine_ntu_siphon_right.Packet = battleFrameToolConverter + peregrine_ntu_siphon_right.Tile = InventoryTile.Tile84 + + peregrine_sparrow.Name = "peregrine_sparrow" + peregrine_sparrow.Size = EquipmentSize.BFRArmWeapon + peregrine_sparrow.AmmoTypes += peregrine_sparrow_ammo + peregrine_sparrow.ProjectileTypes += peregrine_sparrow_projectile + peregrine_sparrow.FireModes += new FireModeDefinition + peregrine_sparrow.FireModes.head.AmmoTypeIndices += 0 + peregrine_sparrow.FireModes.head.AmmoSlotIndex = 0 + peregrine_sparrow.FireModes.head.Magazine = 12 + peregrine_sparrow.Packet = battleFrameToolConverter + peregrine_sparrow.Tile = InventoryTile.Tile84 + + peregrine_sparrow_left.Name = "peregrine_sparrow_left" + peregrine_sparrow_left.Size = EquipmentSize.BFRArmWeapon + peregrine_sparrow_left.AmmoTypes += peregrine_sparrow_ammo + peregrine_sparrow_left.ProjectileTypes += peregrine_sparrow_projectile + peregrine_sparrow_left.FireModes += new FireModeDefinition + peregrine_sparrow_left.FireModes.head.AmmoTypeIndices += 0 + peregrine_sparrow_left.FireModes.head.AmmoSlotIndex = 0 + peregrine_sparrow_left.FireModes.head.Magazine = 12 + peregrine_sparrow_left.Packet = battleFrameToolConverter + peregrine_sparrow_left.Tile = InventoryTile.Tile84 + + peregrine_sparrow_right.Name = "peregrine_sparrow_right" + peregrine_sparrow_right.Size = EquipmentSize.BFRArmWeapon + peregrine_sparrow_right.AmmoTypes += peregrine_sparrow_ammo + peregrine_sparrow_right.ProjectileTypes += peregrine_sparrow_projectile + peregrine_sparrow_right.FireModes += new FireModeDefinition + peregrine_sparrow_right.FireModes.head.AmmoTypeIndices += 0 + peregrine_sparrow_right.FireModes.head.AmmoSlotIndex = 0 + peregrine_sparrow_right.FireModes.head.Magazine = 12 + peregrine_sparrow_right.Packet = battleFrameToolConverter + peregrine_sparrow_right.Tile = InventoryTile.Tile84 + + peregrine_particle_cannon.Name = "peregrine_particle_cannon" + peregrine_particle_cannon.Size = EquipmentSize.BFRGunnerWeapon + peregrine_particle_cannon.AmmoTypes += peregrine_particle_cannon_ammo + peregrine_particle_cannon.ProjectileTypes += peregrine_particle_cannon_projectile + peregrine_particle_cannon.FireModes += new FireModeDefinition + peregrine_particle_cannon.FireModes.head.AmmoTypeIndices += 0 + peregrine_particle_cannon.FireModes.head.AmmoSlotIndex = 0 + peregrine_particle_cannon.FireModes.head.Magazine = 10 + peregrine_particle_cannon.Packet = battleFrameToolConverter + peregrine_particle_cannon.Tile = InventoryTile.Tile1004 + + peregrine_dual_rocket_pods.Name = "peregrine_dual_rocket_pods" + peregrine_dual_rocket_pods.Size = EquipmentSize.BFRGunnerWeapon + peregrine_dual_rocket_pods.AmmoTypes += peregrine_rocket_pod_ammo + peregrine_dual_rocket_pods.ProjectileTypes += peregrine_rocket_pod_projectile + peregrine_dual_rocket_pods.FireModes += new FireModeDefinition + peregrine_dual_rocket_pods.FireModes.head.AmmoTypeIndices += 0 + peregrine_dual_rocket_pods.FireModes.head.AmmoSlotIndex = 0 + peregrine_dual_rocket_pods.FireModes.head.Magazine = 24 + peregrine_dual_rocket_pods.FireModes += new FireModeDefinition + peregrine_dual_rocket_pods.FireModes(1).AmmoTypeIndices += 0 + peregrine_dual_rocket_pods.FireModes(1).AmmoSlotIndex = 0 + peregrine_dual_rocket_pods.FireModes(1).Magazine = 24 + peregrine_dual_rocket_pods.Packet = battleFrameToolConverter + peregrine_dual_rocket_pods.Tile = InventoryTile.Tile1004 } /** @@ -5668,6 +6909,7 @@ object GlobalDefinitions { private def init_vehicles(): Unit = { init_ground_vehicles() init_flight_vehicles() + init_bfr_vehicles() } /** @@ -5678,6 +6920,10 @@ object GlobalDefinitions { val delivererForm = GeometryForm.representByCylinder(radius = 2.46095f, height = 2.40626f) _ //TODO hexahedron val apcForm = GeometryForm.representByCylinder(radius = 4.6211f, height = 3.90626f) _ //TODO hexahedron + val driverSeat = new SeatDefinition() { + restriction = NoReinforcedOrMax + } + val normalSeat = new SeatDefinition() val bailableSeat = new SeatDefinition() { bailable = true } @@ -5685,6 +6931,8 @@ object GlobalDefinitions { restriction = MaxOnly } + val controlSubsystem = List(VehicleSubsystemEntry.Controls) + fury.Name = "fury" fury.MaxHealth = 650 fury.Damageable = true @@ -5692,17 +6940,17 @@ object GlobalDefinitions { fury.RepairIfDestroyed = false fury.MaxShields = 130 fury.Seats += 0 -> bailableSeat - fury.controlledWeapons += 0 -> 1 + fury.controlledWeapons(seat = 0, weapon = 1) fury.Weapons += 1 -> fury_weapon_systema fury.MountPoints += 1 -> MountInfo(0) fury.MountPoints += 2 -> MountInfo(0) + fury.subsystems = controlSubsystem fury.TrunkSize = InventoryTile.Tile1111 fury.TrunkOffset = 30 fury.TrunkLocation = Vector3(-1.71f, 0f, 0f) fury.AutoPilotSpeeds = (24, 10) fury.DestroyedModel = Some(DestroyedVehicle.QuadAssault) fury.JackingDuration = Array(0, 10, 3, 2) - fury.explodes = true fury.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 150 @@ -5728,17 +6976,17 @@ object GlobalDefinitions { quadassault.RepairIfDestroyed = false quadassault.MaxShields = 130 quadassault.Seats += 0 -> bailableSeat - quadassault.controlledWeapons += 0 -> 1 + quadassault.controlledWeapons(seat = 0, weapon = 1) quadassault.Weapons += 1 -> quadassault_weapon_system quadassault.MountPoints += 1 -> MountInfo(0) quadassault.MountPoints += 2 -> MountInfo(0) + quadassault.subsystems = controlSubsystem quadassault.TrunkSize = InventoryTile.Tile1111 quadassault.TrunkOffset = 30 quadassault.TrunkLocation = Vector3(-1.71f, 0f, 0f) quadassault.AutoPilotSpeeds = (24, 10) quadassault.DestroyedModel = Some(DestroyedVehicle.QuadAssault) quadassault.JackingDuration = Array(0, 10, 3, 2) - quadassault.explodes = true quadassault.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 150 @@ -5768,13 +7016,13 @@ object GlobalDefinitions { quadstealth.CanCloak = true quadstealth.MountPoints += 1 -> MountInfo(0) quadstealth.MountPoints += 2 -> MountInfo(0) + quadstealth.subsystems = controlSubsystem quadstealth.TrunkSize = InventoryTile.Tile1111 quadstealth.TrunkOffset = 30 quadstealth.TrunkLocation = Vector3(-1.71f, 0f, 0f) quadstealth.AutoPilotSpeeds = (24, 10) quadstealth.DestroyedModel = Some(DestroyedVehicle.QuadStealth) quadstealth.JackingDuration = Array(0, 10, 3, 2) - quadstealth.explodes = true quadstealth.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 150 @@ -5801,17 +7049,18 @@ object GlobalDefinitions { two_man_assault_buggy.MaxShields = 250 two_man_assault_buggy.Seats += 0 -> bailableSeat two_man_assault_buggy.Seats += 1 -> bailableSeat - two_man_assault_buggy.controlledWeapons += 1 -> 2 + two_man_assault_buggy.controlledWeapons(seat = 1, weapon = 2) two_man_assault_buggy.Weapons += 2 -> chaingun_p two_man_assault_buggy.MountPoints += 1 -> MountInfo(0) two_man_assault_buggy.MountPoints += 2 -> MountInfo(1) + two_man_assault_buggy.subsystems = controlSubsystem two_man_assault_buggy.TrunkSize = InventoryTile.Tile1511 two_man_assault_buggy.TrunkOffset = 30 two_man_assault_buggy.TrunkLocation = Vector3(-2.5f, 0f, 0f) two_man_assault_buggy.AutoPilotSpeeds = (22, 8) two_man_assault_buggy.DestroyedModel = Some(DestroyedVehicle.TwoManAssaultBuggy) + two_man_assault_buggy.RadiationShielding = 0.5f two_man_assault_buggy.JackingDuration = Array(0, 15, 5, 3) - two_man_assault_buggy.explodes = true two_man_assault_buggy.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -5838,18 +7087,19 @@ object GlobalDefinitions { skyguard.MaxShields = 200 skyguard.Seats += 0 -> bailableSeat skyguard.Seats += 1 -> bailableSeat - skyguard.controlledWeapons += 1 -> 2 + skyguard.controlledWeapons(seat = 1, weapon = 2) skyguard.Weapons += 2 -> skyguard_weapon_system skyguard.MountPoints += 1 -> MountInfo(0) skyguard.MountPoints += 2 -> MountInfo(0) skyguard.MountPoints += 3 -> MountInfo(1) + skyguard.subsystems = controlSubsystem skyguard.TrunkSize = InventoryTile.Tile1511 skyguard.TrunkOffset = 30 skyguard.TrunkLocation = Vector3(2.5f, 0f, 0f) skyguard.AutoPilotSpeeds = (22, 8) skyguard.DestroyedModel = Some(DestroyedVehicle.Skyguard) skyguard.JackingDuration = Array(0, 15, 5, 3) - skyguard.explodes = true + skyguard.RadiationShielding = 0.5f skyguard.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -5877,13 +7127,14 @@ object GlobalDefinitions { threemanheavybuggy.Seats += 0 -> bailableSeat threemanheavybuggy.Seats += 1 -> bailableSeat threemanheavybuggy.Seats += 2 -> bailableSeat - threemanheavybuggy.controlledWeapons += 1 -> 3 - threemanheavybuggy.controlledWeapons += 2 -> 4 + threemanheavybuggy.controlledWeapons(seat = 1, weapon = 3) + threemanheavybuggy.controlledWeapons(seat = 2, weapon = 4) threemanheavybuggy.Weapons += 3 -> chaingun_p threemanheavybuggy.Weapons += 4 -> grenade_launcher_marauder threemanheavybuggy.MountPoints += 1 -> MountInfo(0) threemanheavybuggy.MountPoints += 2 -> MountInfo(1) threemanheavybuggy.MountPoints += 3 -> MountInfo(2) + threemanheavybuggy.subsystems = controlSubsystem threemanheavybuggy.TrunkSize = InventoryTile.Tile1511 threemanheavybuggy.TrunkOffset = 30 threemanheavybuggy.TrunkLocation = Vector3(3.01f, 0f, 0f) @@ -5891,7 +7142,6 @@ object GlobalDefinitions { threemanheavybuggy.DestroyedModel = Some(DestroyedVehicle.ThreeManHeavyBuggy) threemanheavybuggy.Subtract.Damage1 = 5 threemanheavybuggy.JackingDuration = Array(0, 20, 7, 5) - threemanheavybuggy.explodes = true threemanheavybuggy.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -5918,18 +7168,19 @@ object GlobalDefinitions { twomanheavybuggy.MaxShields = 360 twomanheavybuggy.Seats += 0 -> bailableSeat twomanheavybuggy.Seats += 1 -> bailableSeat - twomanheavybuggy.controlledWeapons += 1 -> 2 + twomanheavybuggy.controlledWeapons(seat = 1, weapon = 2) twomanheavybuggy.Weapons += 2 -> advanced_missile_launcher_t twomanheavybuggy.MountPoints += 1 -> MountInfo(0) twomanheavybuggy.MountPoints += 2 -> MountInfo(1) + twomanheavybuggy.subsystems = controlSubsystem twomanheavybuggy.TrunkSize = InventoryTile.Tile1511 twomanheavybuggy.TrunkOffset = 30 twomanheavybuggy.TrunkLocation = Vector3(-0.23f, -2.05f, 0f) twomanheavybuggy.AutoPilotSpeeds = (22, 8) twomanheavybuggy.DestroyedModel = Some(DestroyedVehicle.TwoManHeavyBuggy) + twomanheavybuggy.RadiationShielding = 0.5f twomanheavybuggy.Subtract.Damage1 = 5 twomanheavybuggy.JackingDuration = Array(0, 20, 7, 5) - twomanheavybuggy.explodes = true twomanheavybuggy.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -5956,18 +7207,19 @@ object GlobalDefinitions { twomanhoverbuggy.MaxShields = 320 twomanhoverbuggy.Seats += 0 -> bailableSeat twomanhoverbuggy.Seats += 1 -> bailableSeat - twomanhoverbuggy.controlledWeapons += 1 -> 2 + twomanhoverbuggy.controlledWeapons(seat = 1, weapon = 2) twomanhoverbuggy.Weapons += 2 -> flux_cannon_thresher twomanhoverbuggy.MountPoints += 1 -> MountInfo(0) twomanhoverbuggy.MountPoints += 2 -> MountInfo(1) + twomanhoverbuggy.subsystems = controlSubsystem twomanhoverbuggy.TrunkSize = InventoryTile.Tile1511 twomanhoverbuggy.TrunkOffset = 30 twomanhoverbuggy.TrunkLocation = Vector3(-3.39f, 0f, 0f) twomanhoverbuggy.AutoPilotSpeeds = (22, 10) twomanhoverbuggy.DestroyedModel = Some(DestroyedVehicle.TwoManHoverBuggy) + twomanhoverbuggy.RadiationShielding = 0.5f twomanhoverbuggy.Subtract.Damage1 = 5 twomanhoverbuggy.JackingDuration = Array(0, 20, 7, 5) - twomanhoverbuggy.explodes = true twomanhoverbuggy.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -5994,15 +7246,13 @@ object GlobalDefinitions { mediumtransport.Repairable = true mediumtransport.RepairIfDestroyed = false mediumtransport.MaxShields = 500 - mediumtransport.Seats += 0 -> new SeatDefinition() { - restriction = NoReinforcedOrMax - } - mediumtransport.Seats += 1 -> new SeatDefinition() - mediumtransport.Seats += 2 -> new SeatDefinition() - mediumtransport.Seats += 3 -> new SeatDefinition() - mediumtransport.Seats += 4 -> new SeatDefinition() - mediumtransport.controlledWeapons += 1 -> 5 - mediumtransport.controlledWeapons += 2 -> 6 + mediumtransport.Seats += 0 -> driverSeat + mediumtransport.Seats += 1 -> normalSeat + mediumtransport.Seats += 2 -> normalSeat + mediumtransport.Seats += 3 -> normalSeat + mediumtransport.Seats += 4 -> normalSeat + mediumtransport.controlledWeapons(seat = 1, weapon = 5) + mediumtransport.controlledWeapons(seat = 2, weapon = 6) mediumtransport.Weapons += 5 -> mediumtransport_weapon_systemA mediumtransport.Weapons += 6 -> mediumtransport_weapon_systemB mediumtransport.MountPoints += 1 -> MountInfo(0) @@ -6010,6 +7260,7 @@ object GlobalDefinitions { mediumtransport.MountPoints += 3 -> MountInfo(2) mediumtransport.MountPoints += 4 -> MountInfo(3) mediumtransport.MountPoints += 5 -> MountInfo(4) + mediumtransport.subsystems = controlSubsystem mediumtransport.TrunkSize = InventoryTile.Tile1515 mediumtransport.TrunkOffset = 30 mediumtransport.TrunkLocation = Vector3(-3.46f, 0f, 0f) @@ -6017,7 +7268,6 @@ object GlobalDefinitions { mediumtransport.DestroyedModel = Some(DestroyedVehicle.MediumTransport) mediumtransport.Subtract.Damage1 = 7 mediumtransport.JackingDuration = Array(0, 25, 8, 5) - mediumtransport.explodes = true mediumtransport.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -6042,17 +7292,15 @@ object GlobalDefinitions { battlewagon.Repairable = true battlewagon.RepairIfDestroyed = false battlewagon.MaxShields = 500 - battlewagon.Seats += 0 -> new SeatDefinition() { - restriction = NoReinforcedOrMax - } - battlewagon.Seats += 1 -> new SeatDefinition() - battlewagon.Seats += 2 -> new SeatDefinition() - battlewagon.Seats += 3 -> new SeatDefinition() - battlewagon.Seats += 4 -> new SeatDefinition() - battlewagon.controlledWeapons += 1 -> 5 - battlewagon.controlledWeapons += 2 -> 6 - battlewagon.controlledWeapons += 3 -> 7 - battlewagon.controlledWeapons += 4 -> 8 + battlewagon.Seats += 0 -> driverSeat + battlewagon.Seats += 1 -> normalSeat + battlewagon.Seats += 2 -> normalSeat + battlewagon.Seats += 3 -> normalSeat + battlewagon.Seats += 4 -> normalSeat + battlewagon.controlledWeapons(seat = 1, weapon = 5) + battlewagon.controlledWeapons(seat = 2, weapon = 6) + battlewagon.controlledWeapons(seat = 3, weapon = 7) + battlewagon.controlledWeapons(seat = 4, weapon = 8) battlewagon.Weapons += 5 -> battlewagon_weapon_systema battlewagon.Weapons += 6 -> battlewagon_weapon_systemb battlewagon.Weapons += 7 -> battlewagon_weapon_systemc @@ -6062,13 +7310,13 @@ object GlobalDefinitions { battlewagon.MountPoints += 3 -> MountInfo(2) battlewagon.MountPoints += 4 -> MountInfo(3) battlewagon.MountPoints += 5 -> MountInfo(4) + battlewagon.subsystems = controlSubsystem battlewagon.TrunkSize = InventoryTile.Tile1515 battlewagon.TrunkOffset = 30 battlewagon.TrunkLocation = Vector3(-3.46f, 0f, 0f) battlewagon.AutoPilotSpeeds = (18, 6) battlewagon.DestroyedModel = Some(DestroyedVehicle.MediumTransport) battlewagon.JackingDuration = Array(0, 25, 8, 5) - battlewagon.explodes = true battlewagon.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -6093,22 +7341,21 @@ object GlobalDefinitions { thunderer.Repairable = true thunderer.RepairIfDestroyed = false thunderer.MaxShields = 500 - thunderer.Seats += 0 -> new SeatDefinition() { - restriction = NoReinforcedOrMax - } - thunderer.Seats += 1 -> new SeatDefinition() - thunderer.Seats += 2 -> new SeatDefinition() - thunderer.Seats += 3 -> new SeatDefinition() - thunderer.Seats += 4 -> new SeatDefinition() + thunderer.Seats += 0 -> driverSeat + thunderer.Seats += 1 -> normalSeat + thunderer.Seats += 2 -> normalSeat + thunderer.Seats += 3 -> normalSeat + thunderer.Seats += 4 -> normalSeat thunderer.Weapons += 5 -> thunderer_weapon_systema thunderer.Weapons += 6 -> thunderer_weapon_systemb - thunderer.controlledWeapons += 1 -> 5 - thunderer.controlledWeapons += 2 -> 6 + thunderer.controlledWeapons(seat = 1, weapon = 5) + thunderer.controlledWeapons(seat = 2, weapon = 6) thunderer.MountPoints += 1 -> MountInfo(0) thunderer.MountPoints += 2 -> MountInfo(1) thunderer.MountPoints += 3 -> MountInfo(2) thunderer.MountPoints += 4 -> MountInfo(3) thunderer.MountPoints += 5 -> MountInfo(4) + thunderer.subsystems = controlSubsystem thunderer.TrunkSize = InventoryTile.Tile1515 thunderer.TrunkOffset = 30 thunderer.TrunkLocation = Vector3(-3.46f, 0f, 0f) @@ -6116,7 +7363,6 @@ object GlobalDefinitions { thunderer.DestroyedModel = Some(DestroyedVehicle.MediumTransport) thunderer.Subtract.Damage1 = 7 thunderer.JackingDuration = Array(0, 25, 8, 5) - thunderer.explodes = true thunderer.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -6141,15 +7387,13 @@ object GlobalDefinitions { aurora.Repairable = true aurora.RepairIfDestroyed = false aurora.MaxShields = 500 - aurora.Seats += 0 -> new SeatDefinition() { - restriction = NoReinforcedOrMax - } - aurora.Seats += 1 -> new SeatDefinition() - aurora.Seats += 2 -> new SeatDefinition() - aurora.Seats += 3 -> new SeatDefinition() - aurora.Seats += 4 -> new SeatDefinition() - aurora.controlledWeapons += 1 -> 5 - aurora.controlledWeapons += 2 -> 6 + aurora.Seats += 0 -> driverSeat + aurora.Seats += 1 -> normalSeat + aurora.Seats += 2 -> normalSeat + aurora.Seats += 3 -> normalSeat + aurora.Seats += 4 -> normalSeat + aurora.controlledWeapons(seat = 1, weapon = 5) + aurora.controlledWeapons(seat = 2, weapon = 6) aurora.Weapons += 5 -> aurora_weapon_systema aurora.Weapons += 6 -> aurora_weapon_systemb aurora.MountPoints += 1 -> MountInfo(0) @@ -6157,6 +7401,7 @@ object GlobalDefinitions { aurora.MountPoints += 3 -> MountInfo(2) aurora.MountPoints += 4 -> MountInfo(3) aurora.MountPoints += 5 -> MountInfo(4) + aurora.subsystems = controlSubsystem aurora.TrunkSize = InventoryTile.Tile1515 aurora.TrunkOffset = 30 aurora.TrunkLocation = Vector3(-3.46f, 0f, 0f) @@ -6164,7 +7409,6 @@ object GlobalDefinitions { aurora.DestroyedModel = Some(DestroyedVehicle.MediumTransport) aurora.Subtract.Damage1 = 7 aurora.JackingDuration = Array(0, 25, 8, 5) - aurora.explodes = true aurora.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -6189,23 +7433,23 @@ object GlobalDefinitions { apc_tr.Repairable = true apc_tr.RepairIfDestroyed = false apc_tr.MaxShields = 1200 - apc_tr.Seats += 0 -> new SeatDefinition() - apc_tr.Seats += 1 -> new SeatDefinition() - apc_tr.Seats += 2 -> new SeatDefinition() - apc_tr.Seats += 3 -> new SeatDefinition() - apc_tr.Seats += 4 -> new SeatDefinition() - apc_tr.Seats += 5 -> new SeatDefinition() - apc_tr.Seats += 6 -> new SeatDefinition() - apc_tr.Seats += 7 -> new SeatDefinition() - apc_tr.Seats += 8 -> new SeatDefinition() + apc_tr.Seats += 0 -> normalSeat + apc_tr.Seats += 1 -> normalSeat + apc_tr.Seats += 2 -> normalSeat + apc_tr.Seats += 3 -> normalSeat + apc_tr.Seats += 4 -> normalSeat + apc_tr.Seats += 5 -> normalSeat + apc_tr.Seats += 6 -> normalSeat + apc_tr.Seats += 7 -> normalSeat + apc_tr.Seats += 8 -> normalSeat apc_tr.Seats += 9 -> maxOnlySeat apc_tr.Seats += 10 -> maxOnlySeat - apc_tr.controlledWeapons += 1 -> 11 - apc_tr.controlledWeapons += 2 -> 12 - apc_tr.controlledWeapons += 5 -> 15 - apc_tr.controlledWeapons += 6 -> 16 - apc_tr.controlledWeapons += 7 -> 13 - apc_tr.controlledWeapons += 8 -> 14 + apc_tr.controlledWeapons(seat = 1, weapon = 11) + apc_tr.controlledWeapons(seat = 2, weapon = 12) + apc_tr.controlledWeapons(seat = 5, weapon = 15) + apc_tr.controlledWeapons(seat = 6, weapon = 16) + apc_tr.controlledWeapons(seat = 7, weapon = 13) + apc_tr.controlledWeapons(seat = 8, weapon = 14) apc_tr.Weapons += 11 -> apc_weapon_systemc_tr apc_tr.Weapons += 12 -> apc_weapon_systemb apc_tr.Weapons += 13 -> apc_weapon_systema @@ -6224,14 +7468,15 @@ object GlobalDefinitions { apc_tr.MountPoints += 10 -> MountInfo(8) apc_tr.MountPoints += 11 -> MountInfo(9) apc_tr.MountPoints += 12 -> MountInfo(10) + apc_tr.subsystems = controlSubsystem apc_tr.TrunkSize = InventoryTile.Tile2016 apc_tr.TrunkOffset = 30 apc_tr.TrunkLocation = Vector3(-5.82f, 0f, 0f) apc_tr.AutoPilotSpeeds = (16, 6) apc_tr.DestroyedModel = Some(DestroyedVehicle.Apc) apc_tr.JackingDuration = Array(0, 45, 15, 10) + apc_tr.RadiationShielding = 0.5f apc_tr.Subtract.Damage1 = 10 - apc_tr.explodes = true apc_tr.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 300 @@ -6245,6 +7490,7 @@ object GlobalDefinitions { apc_tr.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) apc_tr.Geometry = apcForm apc_tr.MaxCapacitor = 300 + apc_tr.CapacitorRecharge = 10 apc_tr.collision.avatarCollisionDamageMax = 300 apc_tr.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 10), (0.5f, 40), (0.75f, 70), (1f, 110))) apc_tr.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) @@ -6257,23 +7503,23 @@ object GlobalDefinitions { apc_nc.Repairable = true apc_nc.RepairIfDestroyed = false apc_nc.MaxShields = 1200 - apc_nc.Seats += 0 -> new SeatDefinition() - apc_nc.Seats += 1 -> new SeatDefinition() - apc_nc.Seats += 2 -> new SeatDefinition() - apc_nc.Seats += 3 -> new SeatDefinition() - apc_nc.Seats += 4 -> new SeatDefinition() - apc_nc.Seats += 5 -> new SeatDefinition() - apc_nc.Seats += 6 -> new SeatDefinition() - apc_nc.Seats += 7 -> new SeatDefinition() - apc_nc.Seats += 8 -> new SeatDefinition() + apc_nc.Seats += 0 -> normalSeat + apc_nc.Seats += 1 -> normalSeat + apc_nc.Seats += 2 -> normalSeat + apc_nc.Seats += 3 -> normalSeat + apc_nc.Seats += 4 -> normalSeat + apc_nc.Seats += 5 -> normalSeat + apc_nc.Seats += 6 -> normalSeat + apc_nc.Seats += 7 -> normalSeat + apc_nc.Seats += 8 -> normalSeat apc_nc.Seats += 9 -> maxOnlySeat apc_nc.Seats += 10 -> maxOnlySeat - apc_nc.controlledWeapons += 1 -> 11 - apc_nc.controlledWeapons += 2 -> 12 - apc_nc.controlledWeapons += 5 -> 15 - apc_nc.controlledWeapons += 6 -> 16 - apc_nc.controlledWeapons += 7 -> 13 - apc_nc.controlledWeapons += 8 -> 14 + apc_nc.controlledWeapons(seat = 1, weapon = 11) + apc_nc.controlledWeapons(seat = 2, weapon = 12) + apc_nc.controlledWeapons(seat = 5, weapon = 15) + apc_nc.controlledWeapons(seat = 6, weapon = 16) + apc_nc.controlledWeapons(seat = 7, weapon = 13) + apc_nc.controlledWeapons(seat = 8, weapon = 14) apc_nc.Weapons += 11 -> apc_weapon_systemc_nc apc_nc.Weapons += 12 -> apc_weapon_systemb apc_nc.Weapons += 13 -> apc_weapon_systema @@ -6292,14 +7538,15 @@ object GlobalDefinitions { apc_nc.MountPoints += 10 -> MountInfo(8) apc_nc.MountPoints += 11 -> MountInfo(9) apc_nc.MountPoints += 12 -> MountInfo(10) + apc_nc.subsystems = controlSubsystem apc_nc.TrunkSize = InventoryTile.Tile2016 apc_nc.TrunkOffset = 30 apc_nc.TrunkLocation = Vector3(-5.82f, 0f, 0f) apc_nc.AutoPilotSpeeds = (16, 6) apc_nc.DestroyedModel = Some(DestroyedVehicle.Apc) apc_nc.JackingDuration = Array(0, 45, 15, 10) + apc_nc.RadiationShielding = 0.5f apc_nc.Subtract.Damage1 = 10 - apc_nc.explodes = true apc_nc.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 300 @@ -6313,6 +7560,7 @@ object GlobalDefinitions { apc_nc.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) apc_nc.Geometry = apcForm apc_nc.MaxCapacitor = 300 + apc_nc.CapacitorRecharge = 10 apc_nc.collision.avatarCollisionDamageMax = 300 apc_nc.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 10), (0.5f, 40), (0.75f, 70), (1f, 110))) apc_nc.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) @@ -6325,23 +7573,23 @@ object GlobalDefinitions { apc_vs.Repairable = true apc_vs.RepairIfDestroyed = false apc_vs.MaxShields = 1200 - apc_vs.Seats += 0 -> new SeatDefinition() - apc_vs.Seats += 1 -> new SeatDefinition() - apc_vs.Seats += 2 -> new SeatDefinition() - apc_vs.Seats += 3 -> new SeatDefinition() - apc_vs.Seats += 4 -> new SeatDefinition() - apc_vs.Seats += 5 -> new SeatDefinition() - apc_vs.Seats += 6 -> new SeatDefinition() - apc_vs.Seats += 7 -> new SeatDefinition() - apc_vs.Seats += 8 -> new SeatDefinition() + apc_vs.Seats += 0 -> normalSeat + apc_vs.Seats += 1 -> normalSeat + apc_vs.Seats += 2 -> normalSeat + apc_vs.Seats += 3 -> normalSeat + apc_vs.Seats += 4 -> normalSeat + apc_vs.Seats += 5 -> normalSeat + apc_vs.Seats += 6 -> normalSeat + apc_vs.Seats += 7 -> normalSeat + apc_vs.Seats += 8 -> normalSeat apc_vs.Seats += 9 -> maxOnlySeat apc_vs.Seats += 10 -> maxOnlySeat - apc_vs.controlledWeapons += 1 -> 11 - apc_vs.controlledWeapons += 2 -> 12 - apc_vs.controlledWeapons += 5 -> 15 - apc_vs.controlledWeapons += 6 -> 16 - apc_vs.controlledWeapons += 7 -> 13 - apc_vs.controlledWeapons += 8 -> 14 + apc_vs.controlledWeapons(seat = 1, weapon = 11) + apc_vs.controlledWeapons(seat = 2, weapon = 12) + apc_vs.controlledWeapons(seat = 5, weapon = 15) + apc_vs.controlledWeapons(seat = 6, weapon = 16) + apc_vs.controlledWeapons(seat = 7, weapon = 13) + apc_vs.controlledWeapons(seat = 8, weapon = 14) apc_vs.Weapons += 11 -> apc_weapon_systemc_vs apc_vs.Weapons += 12 -> apc_weapon_systemb apc_vs.Weapons += 13 -> apc_weapon_systema @@ -6360,14 +7608,15 @@ object GlobalDefinitions { apc_vs.MountPoints += 10 -> MountInfo(8) apc_vs.MountPoints += 11 -> MountInfo(9) apc_vs.MountPoints += 12 -> MountInfo(10) + apc_vs.subsystems = controlSubsystem apc_vs.TrunkSize = InventoryTile.Tile2016 apc_vs.TrunkOffset = 30 apc_vs.TrunkLocation = Vector3(-5.82f, 0f, 0f) apc_vs.AutoPilotSpeeds = (16, 6) apc_vs.DestroyedModel = Some(DestroyedVehicle.Apc) apc_vs.JackingDuration = Array(0, 45, 15, 10) + apc_vs.RadiationShielding = 0.5f apc_vs.Subtract.Damage1 = 10 - apc_vs.explodes = true apc_vs.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 300 @@ -6381,6 +7630,7 @@ object GlobalDefinitions { apc_vs.UnderwaterLifespan(suffocation = 15000L, recovery = 7500L) apc_vs.Geometry = apcForm apc_vs.MaxCapacitor = 300 + apc_vs.CapacitorRecharge = 10 apc_vs.collision.avatarCollisionDamageMax = 300 apc_vs.collision.xy = CollisionXYData(Array((0.1f, 1), (0.25f, 10), (0.5f, 40), (0.75f, 70), (1f, 110))) apc_vs.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) @@ -6393,21 +7643,20 @@ object GlobalDefinitions { lightning.Repairable = true lightning.RepairIfDestroyed = false lightning.MaxShields = 400 - lightning.Seats += 0 -> new SeatDefinition() { - restriction = NoReinforcedOrMax - } - lightning.controlledWeapons += 0 -> 1 + lightning.Seats += 0 -> driverSeat + lightning.controlledWeapons(seat = 0, weapon = 1) lightning.Weapons += 1 -> lightning_weapon_system lightning.MountPoints += 1 -> MountInfo(0) lightning.MountPoints += 2 -> MountInfo(0) + lightning.subsystems = controlSubsystem lightning.TrunkSize = InventoryTile.Tile1511 lightning.TrunkOffset = 30 lightning.TrunkLocation = Vector3(-3f, 0f, 0f) lightning.AutoPilotSpeeds = (20, 8) lightning.DestroyedModel = Some(DestroyedVehicle.Lightning) + lightning.RadiationShielding = 0.5f lightning.Subtract.Damage1 = 7 lightning.JackingDuration = Array(0, 20, 7, 5) - lightning.explodes = true lightning.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 250 @@ -6432,26 +7681,25 @@ object GlobalDefinitions { prowler.Repairable = true prowler.RepairIfDestroyed = false prowler.MaxShields = 960 - prowler.Seats += 0 -> new SeatDefinition() { - restriction = NoReinforcedOrMax - } - prowler.Seats += 1 -> new SeatDefinition() - prowler.Seats += 2 -> new SeatDefinition() - prowler.controlledWeapons += 1 -> 3 - prowler.controlledWeapons += 2 -> 4 + prowler.Seats += 0 -> driverSeat + prowler.Seats += 1 -> normalSeat + prowler.Seats += 2 -> normalSeat + prowler.controlledWeapons(seat = 1, weapon = 3) + prowler.controlledWeapons(seat = 2, weapon = 4) prowler.Weapons += 3 -> prowler_weapon_systemA prowler.Weapons += 4 -> prowler_weapon_systemB prowler.MountPoints += 1 -> MountInfo(0) prowler.MountPoints += 2 -> MountInfo(1) prowler.MountPoints += 3 -> MountInfo(2) + prowler.subsystems = controlSubsystem prowler.TrunkSize = InventoryTile.Tile1511 prowler.TrunkOffset = 30 prowler.TrunkLocation = Vector3(-4.71f, 0f, 0f) prowler.AutoPilotSpeeds = (14, 6) prowler.DestroyedModel = Some(DestroyedVehicle.Prowler) + prowler.RadiationShielding = 0.5f prowler.Subtract.Damage1 = 9 prowler.JackingDuration = Array(0, 30, 10, 5) - prowler.explodes = true prowler.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 250 @@ -6476,22 +7724,21 @@ object GlobalDefinitions { vanguard.Repairable = true vanguard.RepairIfDestroyed = false vanguard.MaxShields = 1080 - vanguard.Seats += 0 -> new SeatDefinition() { - restriction = NoReinforcedOrMax - } - vanguard.Seats += 1 -> new SeatDefinition() - vanguard.controlledWeapons += 1 -> 2 + vanguard.Seats += 0 -> driverSeat + vanguard.Seats += 1 -> normalSeat + vanguard.controlledWeapons(seat = 1, weapon = 2) vanguard.Weapons += 2 -> vanguard_weapon_system vanguard.MountPoints += 1 -> MountInfo(0) vanguard.MountPoints += 2 -> MountInfo(1) + vanguard.subsystems = controlSubsystem vanguard.TrunkSize = InventoryTile.Tile1511 vanguard.TrunkOffset = 30 vanguard.TrunkLocation = Vector3(-4.84f, 0f, 0f) vanguard.AutoPilotSpeeds = (16, 6) vanguard.DestroyedModel = Some(DestroyedVehicle.Vanguard) + vanguard.RadiationShielding = 0.5f vanguard.Subtract.Damage1 = 9 vanguard.JackingDuration = Array(0, 30, 10, 5) - vanguard.explodes = true vanguard.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 250 @@ -6516,24 +7763,23 @@ object GlobalDefinitions { magrider.Repairable = true magrider.RepairIfDestroyed = false magrider.MaxShields = 840 - magrider.Seats += 0 -> new SeatDefinition() { - restriction = NoReinforcedOrMax - } - magrider.Seats += 1 -> new SeatDefinition() - magrider.controlledWeapons += 0 -> 2 - magrider.controlledWeapons += 1 -> 3 + magrider.Seats += 0 -> driverSeat + magrider.Seats += 1 -> normalSeat + magrider.controlledWeapons(seat = 0, weapon = 2) + magrider.controlledWeapons(seat = 1, weapon = 3) magrider.Weapons += 2 -> particle_beam_magrider magrider.Weapons += 3 -> heavy_rail_beam_magrider magrider.MountPoints += 1 -> MountInfo(0) magrider.MountPoints += 2 -> MountInfo(1) + magrider.subsystems = controlSubsystem magrider.TrunkSize = InventoryTile.Tile1511 magrider.TrunkOffset = 30 magrider.TrunkLocation = Vector3(5.06f, 0f, 0f) magrider.AutoPilotSpeeds = (18, 6) magrider.DestroyedModel = Some(DestroyedVehicle.Magrider) + magrider.RadiationShielding = 0.5f magrider.Subtract.Damage1 = 9 magrider.JackingDuration = Array(0, 30, 10, 5) - magrider.explodes = true magrider.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 250 @@ -6559,11 +7805,10 @@ object GlobalDefinitions { ant.Repairable = true ant.RepairIfDestroyed = false ant.MaxShields = 400 - ant.Seats += 0 -> new SeatDefinition() { - restriction = NoReinforcedOrMax - } + ant.Seats += 0 -> driverSeat ant.MountPoints += 1 -> MountInfo(0) ant.MountPoints += 2 -> MountInfo(0) + ant.subsystems = controlSubsystem ant.Deployment = true ant.DeployTime = 1500 ant.UndeployTime = 1500 @@ -6571,9 +7816,9 @@ object GlobalDefinitions { ant.MaxNtuCapacitor = 1500 ant.Packet = utilityConverter ant.DestroyedModel = Some(DestroyedVehicle.Ant) + ant.RadiationShielding = 0.5f ant.Subtract.Damage1 = 5 ant.JackingDuration = Array(0, 60, 20, 15) - ant.explodes = true ant.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 300 @@ -6598,15 +7843,14 @@ object GlobalDefinitions { ams.Repairable = true ams.RepairIfDestroyed = false ams.MaxShields = 600 + 1 - ams.Seats += 0 -> new SeatDefinition() { - restriction = NoReinforcedOrMax - } + ams.Seats += 0 -> driverSeat ams.MountPoints += 1 -> MountInfo(0) ams.MountPoints += 2 -> MountInfo(0) ams.Utilities += 1 -> UtilityType.matrix_terminalc ams.Utilities += 2 -> UtilityType.ams_respawn_tube ams.Utilities += 3 -> UtilityType.order_terminala ams.Utilities += 4 -> UtilityType.order_terminalb + ams.subsystems = controlSubsystem ams.Deployment = true ams.DeployTime = 2000 ams.UndeployTime = 2000 @@ -6614,9 +7858,9 @@ object GlobalDefinitions { ams.AutoPilotSpeeds = (18, 6) ams.Packet = utilityConverter ams.DestroyedModel = Some(DestroyedVehicle.Ams) + ams.RadiationShielding = 0.5f ams.Subtract.Damage1 = 10 ams.JackingDuration = Array(0, 60, 20, 15) - ams.explodes = true ams.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.Splash Damage0 = 300 @@ -6642,10 +7886,11 @@ object GlobalDefinitions { router.Repairable = true router.RepairIfDestroyed = false router.MaxShields = 800 - router.Seats += 0 -> new SeatDefinition() + router.Seats += 0 -> normalSeat router.MountPoints += 1 -> MountInfo(0) router.Utilities += 1 -> UtilityType.teleportpad_terminal router.Utilities += 2 -> UtilityType.internal_router_telepad_deployable + router.subsystems = controlSubsystem router.TrunkSize = InventoryTile.Tile1511 router.TrunkOffset = 30 router.TrunkLocation = Vector3(0f, 3.4f, 0f) @@ -6656,9 +7901,9 @@ object GlobalDefinitions { router.AutoPilotSpeeds = (16, 6) router.Packet = variantConverter router.DestroyedModel = Some(DestroyedVehicle.Router) + router.RadiationShielding = 0.5f router.Subtract.Damage1 = 5 router.JackingDuration = Array(0, 20, 7, 5) - router.explodes = true router.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -6683,11 +7928,12 @@ object GlobalDefinitions { switchblade.Repairable = true switchblade.RepairIfDestroyed = false switchblade.MaxShields = 350 - switchblade.Seats += 0 -> new SeatDefinition() - switchblade.controlledWeapons += 0 -> 1 + switchblade.Seats += 0 -> normalSeat + switchblade.controlledWeapons(seat = 0, weapon = 1) switchblade.Weapons += 1 -> scythe switchblade.MountPoints += 1 -> MountInfo(0) switchblade.MountPoints += 2 -> MountInfo(0) + switchblade.subsystems = controlSubsystem switchblade.TrunkSize = InventoryTile.Tile1511 switchblade.TrunkOffset = 30 switchblade.TrunkLocation = Vector3(-2.5f, 0f, 0f) @@ -6697,10 +7943,10 @@ object GlobalDefinitions { switchblade.AutoPilotSpeeds = (22, 8) switchblade.Packet = variantConverter switchblade.DestroyedModel = Some(DestroyedVehicle.Switchblade) + switchblade.RadiationShielding = 0.5f switchblade.Subtract.Damage0 = 5 switchblade.Subtract.Damage1 = 5 switchblade.JackingDuration = Array(0, 20, 7, 5) - switchblade.explodes = true switchblade.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -6728,11 +7974,12 @@ object GlobalDefinitions { flail.Repairable = true flail.RepairIfDestroyed = false flail.MaxShields = 480 - flail.Seats += 0 -> new SeatDefinition() - flail.controlledWeapons += 0 -> 1 + flail.Seats += 0 -> normalSeat + flail.controlledWeapons(seat = 0, weapon = 1) flail.Weapons += 1 -> flail_weapon flail.Utilities += 2 -> UtilityType.targeting_laser_dispenser flail.MountPoints += 1 -> MountInfo(0) + flail.subsystems = controlSubsystem flail.TrunkSize = InventoryTile.Tile1511 flail.TrunkOffset = 30 flail.TrunkLocation = Vector3(-3.75f, 0f, 0f) @@ -6742,9 +7989,9 @@ object GlobalDefinitions { flail.AutoPilotSpeeds = (14, 6) flail.Packet = variantConverter flail.DestroyedModel = Some(DestroyedVehicle.Flail) + flail.RadiationShielding = 0.5f flail.Subtract.Damage1 = 7 flail.JackingDuration = Array(0, 20, 7, 5) - flail.explodes = true flail.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -6772,8 +8019,10 @@ object GlobalDefinitions { val bailableSeat = new SeatDefinition() { bailable = true } - val variantConverter = new VariantVehicleConverter + val flightSubsystems = List(VehicleSubsystemEntry.Controls, VehicleSubsystemEntry.Ejection) + + val variantConverter = new VariantVehicleConverter mosquito.Name = "mosquito" mosquito.MaxHealth = 665 mosquito.Damageable = true @@ -6782,10 +8031,11 @@ object GlobalDefinitions { mosquito.MaxShields = 133 mosquito.CanFly = true mosquito.Seats += 0 -> bailableSeat - mosquito.controlledWeapons += 0 -> 1 + mosquito.controlledWeapons(seat = 0, weapon = 1) mosquito.Weapons += 1 -> rotarychaingun_mosquito mosquito.MountPoints += 1 -> MountInfo(0) mosquito.MountPoints += 2 -> MountInfo(0) + mosquito.subsystems = flightSubsystems :+ VehicleSubsystemEntry.MosquitoRadar mosquito.TrunkSize = InventoryTile.Tile1111 mosquito.TrunkOffset = 30 mosquito.TrunkLocation = Vector3(-4.6f, 0f, 0f) @@ -6793,8 +8043,8 @@ object GlobalDefinitions { mosquito.Packet = variantConverter mosquito.DestroyedModel = Some(DestroyedVehicle.Mosquito) mosquito.JackingDuration = Array(0, 20, 7, 5) + mosquito.RadiationShielding = 0.5f mosquito.DamageUsing = DamageCalculations.AgainstAircraft - mosquito.explodes = true mosquito.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -6820,20 +8070,21 @@ object GlobalDefinitions { lightgunship.MaxShields = 171 // Temporary - Correct Reaver Shields from pre-"Coder Madness 2" Event lightgunship.CanFly = true lightgunship.Seats += 0 -> bailableSeat - lightgunship.controlledWeapons += 0 -> 1 + lightgunship.controlledWeapons(seat = 0, weapon = 1) lightgunship.Weapons += 1 -> lightgunship_weapon_system lightgunship.MountPoints += 1 -> MountInfo(0) lightgunship.MountPoints += 2 -> MountInfo(0) + lightgunship.subsystems = flightSubsystems lightgunship.TrunkSize = InventoryTile.Tile1511 lightgunship.TrunkOffset = 30 lightgunship.TrunkLocation = Vector3(-5.61f, 0f, 0f) lightgunship.AutoPilotSpeeds = (0, 4) lightgunship.Packet = variantConverter lightgunship.DestroyedModel = Some(DestroyedVehicle.LightGunship) + lightgunship.RadiationShielding = 0.5f lightgunship.Subtract.Damage1 = 3 lightgunship.JackingDuration = Array(0, 30, 10, 5) lightgunship.DamageUsing = DamageCalculations.AgainstAircraft - lightgunship.explodes = true lightgunship.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 250 @@ -6859,10 +8110,11 @@ object GlobalDefinitions { wasp.MaxShields = 103 wasp.CanFly = true wasp.Seats += 0 -> bailableSeat - wasp.controlledWeapons += 0 -> 1 + wasp.controlledWeapons(seat = 0, weapon = 1) wasp.Weapons += 1 -> wasp_weapon_system wasp.MountPoints += 1 -> MountInfo(0) wasp.MountPoints += 2 -> MountInfo(0) + wasp.subsystems = flightSubsystems wasp.TrunkSize = InventoryTile.Tile1111 wasp.TrunkOffset = 30 wasp.TrunkLocation = Vector3(-4.6f, 0f, 0f) @@ -6871,7 +8123,6 @@ object GlobalDefinitions { wasp.DestroyedModel = Some(DestroyedVehicle.Mosquito) //set_resource_parent wasp game_objects mosquito wasp.JackingDuration = Array(0, 20, 7, 5) wasp.DamageUsing = DamageCalculations.AgainstAircraft - wasp.explodes = true wasp.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -6899,9 +8150,9 @@ object GlobalDefinitions { liberator.Seats += 0 -> bailableSeat //new SeatDefinition() liberator.Seats += 1 -> bailableSeat liberator.Seats += 2 -> bailableSeat - liberator.controlledWeapons += 0 -> 3 - liberator.controlledWeapons += 1 -> 4 - liberator.controlledWeapons += 2 -> 5 + liberator.controlledWeapons(seat = 0, weapon = 3) + liberator.controlledWeapons(seat = 1, weapon = 4) + liberator.controlledWeapons(seat = 2, weapon = 5) liberator.Weapons += 3 -> liberator_weapon_system liberator.Weapons += 4 -> liberator_bomb_bay liberator.Weapons += 5 -> liberator_25mm_cannon @@ -6909,16 +8160,17 @@ object GlobalDefinitions { liberator.MountPoints += 2 -> MountInfo(1) liberator.MountPoints += 3 -> MountInfo(1) liberator.MountPoints += 4 -> MountInfo(2) + liberator.subsystems = flightSubsystems liberator.TrunkSize = InventoryTile.Tile1515 liberator.TrunkOffset = 30 liberator.TrunkLocation = Vector3(-0.76f, -1.88f, 0f) liberator.AutoPilotSpeeds = (0, 4) liberator.Packet = variantConverter liberator.DestroyedModel = Some(DestroyedVehicle.Liberator) + liberator.RadiationShielding = 0.5f liberator.Subtract.Damage1 = 5 liberator.JackingDuration = Array(0, 30, 10, 5) liberator.DamageUsing = DamageCalculations.AgainstAircraft - liberator.explodes = true liberator.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 250 @@ -6946,9 +8198,9 @@ object GlobalDefinitions { vulture.Seats += 0 -> bailableSeat //new SeatDefinition() vulture.Seats += 1 -> bailableSeat vulture.Seats += 2 -> bailableSeat - vulture.controlledWeapons += 0 -> 3 - vulture.controlledWeapons += 1 -> 4 - vulture.controlledWeapons += 2 -> 5 + vulture.controlledWeapons(seat = 0, weapon = 3) + vulture.controlledWeapons(seat = 1, weapon = 4) + vulture.controlledWeapons(seat = 2, weapon = 5) vulture.Weapons += 3 -> vulture_nose_weapon_system vulture.Weapons += 4 -> vulture_bomb_bay vulture.Weapons += 5 -> vulture_tail_cannon @@ -6956,6 +8208,7 @@ object GlobalDefinitions { vulture.MountPoints += 2 -> MountInfo(1) vulture.MountPoints += 3 -> MountInfo(1) vulture.MountPoints += 4 -> MountInfo(2) + vulture.subsystems = flightSubsystems vulture.TrunkSize = InventoryTile.Tile1611 vulture.TrunkOffset = 30 vulture.TrunkLocation = Vector3(-0.76f, -1.88f, 0f) @@ -6963,10 +8216,10 @@ object GlobalDefinitions { vulture.Packet = variantConverter vulture.DestroyedModel = Some(DestroyedVehicle.Liberator) //add_property vulture destroyedphysics liberator_destroyed + vulture.RadiationShielding = 0.5f vulture.Subtract.Damage1 = 5 vulture.JackingDuration = Array(0, 30, 10, 5) vulture.DamageUsing = DamageCalculations.AgainstAircraft - vulture.explodes = true vulture.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 250 @@ -7010,9 +8263,9 @@ object GlobalDefinitions { restriction = MaxOnly } dropship.Seats += 11 -> bailableSeat - dropship.controlledWeapons += 1 -> 12 - dropship.controlledWeapons += 2 -> 13 - dropship.controlledWeapons += 11 -> 14 + dropship.controlledWeapons(seat = 1, weapon = 12) + dropship.controlledWeapons(seat = 2, weapon = 13) + dropship.controlledWeapons(seat = 11, weapon = 14) dropship.Weapons += 12 -> cannon_dropship_20mm dropship.Weapons += 13 -> cannon_dropship_20mm dropship.Weapons += 14 -> dropship_rear_turret @@ -7032,16 +8285,17 @@ object GlobalDefinitions { dropship.MountPoints += 11 -> MountInfo(9) dropship.MountPoints += 12 -> MountInfo(10) dropship.MountPoints += 13 -> MountInfo(15) + dropship.subsystems = flightSubsystems dropship.TrunkSize = InventoryTile.Tile1612 dropship.TrunkOffset = 30 dropship.TrunkLocation = Vector3(-7.39f, -4.96f, 0f) dropship.AutoPilotSpeeds = (0, 4) dropship.Packet = variantConverter dropship.DestroyedModel = Some(DestroyedVehicle.Dropship) + dropship.RadiationShielding = 0.5f dropship.Subtract.Damage1 = 7 dropship.JackingDuration = Array(0, 60, 20, 10) dropship.DamageUsing = DamageCalculations.AgainstAircraft - dropship.explodes = true dropship.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 300 @@ -7073,11 +8327,11 @@ object GlobalDefinitions { galaxy_gunship.Seats += 3 -> bailableSeat galaxy_gunship.Seats += 4 -> bailableSeat galaxy_gunship.Seats += 5 -> bailableSeat - galaxy_gunship.controlledWeapons += 1 -> 6 - galaxy_gunship.controlledWeapons += 2 -> 7 - galaxy_gunship.controlledWeapons += 3 -> 8 - galaxy_gunship.controlledWeapons += 4 -> 9 - galaxy_gunship.controlledWeapons += 5 -> 10 + galaxy_gunship.controlledWeapons(seat = 1, weapon = 6) + galaxy_gunship.controlledWeapons(seat = 2, weapon = 7) + galaxy_gunship.controlledWeapons(seat = 3, weapon = 8) + galaxy_gunship.controlledWeapons(seat = 4, weapon = 9) + galaxy_gunship.controlledWeapons(seat = 5, weapon = 10) galaxy_gunship.Weapons += 6 -> galaxy_gunship_cannon galaxy_gunship.Weapons += 7 -> galaxy_gunship_cannon galaxy_gunship.Weapons += 8 -> galaxy_gunship_tailgun @@ -7089,6 +8343,7 @@ object GlobalDefinitions { galaxy_gunship.MountPoints += 4 -> MountInfo(2) galaxy_gunship.MountPoints += 5 -> MountInfo(4) galaxy_gunship.MountPoints += 6 -> MountInfo(5) + galaxy_gunship.subsystems = flightSubsystems galaxy_gunship.TrunkSize = InventoryTile.Tile1816 galaxy_gunship.TrunkOffset = 30 galaxy_gunship.TrunkLocation = Vector3(-9.85f, 0f, 0f) @@ -7096,11 +8351,11 @@ object GlobalDefinitions { galaxy_gunship.Packet = variantConverter galaxy_gunship.DestroyedModel = Some(DestroyedVehicle.Dropship) //the adb calls out a galaxy_gunship_destroyed but no such asset exists + galaxy_gunship.RadiationShielding = 0.5f galaxy_gunship.Subtract.Damage1 = 7 galaxy_gunship.JackingDuration = Array(0, 60, 20, 10) galaxy_gunship.DamageUsing = DamageCalculations.AgainstAircraft galaxy_gunship.Modifiers = GalaxyGunshipReduction(0.63f) - galaxy_gunship.explodes = true galaxy_gunship.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 300 @@ -7135,19 +8390,24 @@ object GlobalDefinitions { lodestar.Utilities += 3 -> UtilityType.lodestar_repair_terminal lodestar.UtilityOffset += 3 -> Vector3(0, -20, 0) lodestar.Utilities += 4 -> UtilityType.multivehicle_rearm_terminal + lodestar.UtilityOffset += 4 -> Vector3(0, 20, 0) lodestar.Utilities += 5 -> UtilityType.multivehicle_rearm_terminal + lodestar.UtilityOffset += 5 -> Vector3(0, -20, 0) lodestar.Utilities += 6 -> UtilityType.bfr_rearm_terminal + lodestar.UtilityOffset += 6 -> Vector3(0, 20, 0) lodestar.Utilities += 7 -> UtilityType.bfr_rearm_terminal + lodestar.UtilityOffset += 7 -> Vector3(0, -20, 0) + lodestar.subsystems = flightSubsystems lodestar.TrunkSize = InventoryTile.Tile1612 lodestar.TrunkOffset = 30 lodestar.TrunkLocation = Vector3(6.85f, -6.8f, 0f) lodestar.AutoPilotSpeeds = (0, 4) lodestar.Packet = variantConverter lodestar.DestroyedModel = Some(DestroyedVehicle.Lodestar) + lodestar.RadiationShielding = 0.5f lodestar.Subtract.Damage1 = 7 lodestar.JackingDuration = Array(0, 60, 20, 10) lodestar.DamageUsing = DamageCalculations.AgainstAircraft - lodestar.explodes = true lodestar.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 300 @@ -7159,6 +8419,8 @@ object GlobalDefinitions { lodestar.DrownAtMaxDepth = true lodestar.MaxDepth = 2 lodestar.Geometry = GeometryForm.representByCylinder(radius = 7.8671f, height = 6.79688f) //TODO hexahedron + lodestar.collision.avatarCollisionDamageMax = 300 + lodestar.collision.xy = CollisionXYData(Array((0.1f, 5), (0.25f, 125), (0.5f, 250), (0.75f, 500), (1f, 1000))) lodestar.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) lodestar.maxForwardSpeed = 80f lodestar.mass = 128.2f @@ -7181,6 +8443,7 @@ object GlobalDefinitions { phantasm.MountPoints += 3 -> MountInfo(2) phantasm.MountPoints += 4 -> MountInfo(3) phantasm.MountPoints += 5 -> MountInfo(4) + phantasm.subsystems = flightSubsystems phantasm.TrunkSize = InventoryTile.Tile1107 phantasm.TrunkOffset = 30 phantasm.TrunkLocation = Vector3(-6.16f, 0f, 0f) @@ -7188,8 +8451,8 @@ object GlobalDefinitions { phantasm.Packet = variantConverter phantasm.DestroyedModel = None //the adb calls out a phantasm_destroyed but no such asset exists phantasm.JackingDuration = Array(0, 60, 20, 10) + phantasm.RadiationShielding = 0.5f phantasm.DamageUsing = DamageCalculations.AgainstAircraft - phantasm.explodes = true phantasm.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 100 @@ -7258,6 +8521,355 @@ object GlobalDefinitions { orbital_shuttle.mass = 25000f } + private def init_bfr_vehicles(): Unit = { + val driverSeat = new SeatDefinition() { + restriction = NoReinforcedOrMax + } + val bailableSeat = new SeatDefinition() { + restriction = NoReinforcedOrMax + bailable = true + } + val normalSeat = new SeatDefinition() + val bfrSubsystems = List( + VehicleSubsystemEntry.BattleframeMovementServos, + VehicleSubsystemEntry.BattleframeSensorArray, + VehicleSubsystemEntry.BattleframeShieldGenerator, + VehicleSubsystemEntry.BattleframeTrunk + ) + val bfrGunnerSubsystems = List( + VehicleSubsystemEntry.BattleframeLeftArm, + VehicleSubsystemEntry.BattleframeRightArm, + VehicleSubsystemEntry.BattleframeLeftWeapon, + VehicleSubsystemEntry.BattleframeRightWeapon, + VehicleSubsystemEntry.BattleframeGunnerWeapon + ) ++ bfrSubsystems + val bfrFlightSubsystems = List( + VehicleSubsystemEntry.BattleframeFlightLeftArm, + VehicleSubsystemEntry.BattleframeFlightRightArm, + VehicleSubsystemEntry.BattleframeFlightLeftWeapon, + VehicleSubsystemEntry.BattleframeFlightRightWeapon + ) ++ bfrSubsystems ++ List( + VehicleSubsystemEntry.BattleframeFlightPod + ) + + val battleFrameConverter = new BattleFrameRoboticsConverter + aphelion_gunner.Name = "aphelion_gunner" + aphelion_gunner.MaxHealth = 4500 + aphelion_gunner.Damageable = true + aphelion_gunner.Repairable = true + aphelion_gunner.RepairIfDestroyed = false + aphelion_gunner.shieldUiAttribute = 79 + aphelion_gunner.MaxShields = 3000 + aphelion_gunner.ShieldPeriodicDelay = 500 + aphelion_gunner.ShieldDamageDelay = 3500 + aphelion_gunner.ShieldAutoRecharge = 45 + aphelion_gunner.ShieldAutoRechargeSpecial = 85 + aphelion_gunner.DefaultShields = aphelion_gunner.MaxShields + aphelion_gunner.Seats += 0 -> driverSeat + aphelion_gunner.Seats += 1 -> normalSeat + aphelion_gunner.controlledWeapons(seat = 0, weapons = Set(2, 3)) + aphelion_gunner.controlledWeapons(seat = 1, weapon = 4) + aphelion_gunner.Weapons += 2 -> aphelion_ppa_left + aphelion_gunner.Weapons += 3 -> aphelion_ppa_right + aphelion_gunner.Weapons += 4 -> aphelion_plasma_rocket_pod + aphelion_gunner.MountPoints += 1 -> MountInfo(0) + aphelion_gunner.MountPoints += 2 -> MountInfo(1) + aphelion_gunner.subsystems = bfrGunnerSubsystems + aphelion_gunner.TrunkSize = InventoryTile.Tile1518 + aphelion_gunner.TrunkOffset = 30 + aphelion_gunner.TrunkLocation = Vector3(0f, -2f, 0f) + aphelion_gunner.AutoPilotSpeeds = (5, 1) + aphelion_gunner.Packet = battleFrameConverter + aphelion_gunner.DestroyedModel = None + aphelion_gunner.destructionDelay = Some(4000L) + aphelion_gunner.JackingDuration = Array(0, 62, 60, 30) + aphelion_gunner.RadiationShielding = 0.5f + aphelion_gunner.DamageUsing = DamageCalculations.AgainstBfr + aphelion_gunner.Model = BfrResolutions.calculate + aphelion_gunner.innateDamage = new DamageWithPosition { + CausesDamageType = DamageType.One + Damage0 = 400 + Damage1 = 500 + DamageRadius = 10 + DamageAtEdge = 0.2f + Modifiers = ExplodingRadialDegrade + } + aphelion_gunner.DrownAtMaxDepth = true + aphelion_gunner.MaxDepth = 5.09375f + aphelion_gunner.UnderwaterLifespan(suffocation = 6000L, recovery = 6000L) + aphelion_gunner.Geometry = GeometryForm.representByCylinder(radius = 1.2618f, height = 6.01562f) + aphelion_gunner.collision.avatarCollisionDamageMax = 300 + aphelion_gunner.collision.xy = CollisionXYData(Array((0.2f, 1), (0.35f, 5), (0.55f, 20), (0.75f, 40), (1f, 60))) + aphelion_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) + aphelion_gunner.maxForwardSpeed = 17 + aphelion_gunner.mass = 615.1f + + colossus_gunner.Name = "colossus_gunner" + colossus_gunner.MaxHealth = 4500 + colossus_gunner.Damageable = true + colossus_gunner.Repairable = true + colossus_gunner.RepairIfDestroyed = false + colossus_gunner.shieldUiAttribute = 79 + colossus_gunner.MaxShields = 3000 + colossus_gunner.ShieldPeriodicDelay = 500 + colossus_gunner.ShieldDamageDelay = 3500 + colossus_gunner.ShieldAutoRecharge = 45 + colossus_gunner.ShieldAutoRechargeSpecial = 85 + colossus_gunner.DefaultShields = colossus_gunner.MaxShields + colossus_gunner.Seats += 0 -> driverSeat + colossus_gunner.Seats += 1 -> normalSeat + colossus_gunner.controlledWeapons(seat = 0, weapons = Set(2, 3)) + colossus_gunner.controlledWeapons(seat = 1, weapon = 4) + colossus_gunner.Weapons += 2 -> colossus_tank_cannon_left + colossus_gunner.Weapons += 3 -> colossus_tank_cannon_right + colossus_gunner.Weapons += 4 -> colossus_dual_100mm_cannons + colossus_gunner.MountPoints += 1 -> MountInfo(0) + colossus_gunner.MountPoints += 2 -> MountInfo(1) + colossus_gunner.subsystems = bfrGunnerSubsystems + colossus_gunner.TrunkSize = InventoryTile.Tile1518 + colossus_gunner.TrunkOffset = 30 + colossus_gunner.TrunkLocation = Vector3(0f, -5f, 0f) + colossus_gunner.AutoPilotSpeeds = (5, 1) + colossus_gunner.Packet = battleFrameConverter + colossus_gunner.DestroyedModel = None + colossus_gunner.destructionDelay = Some(4000L) + colossus_gunner.JackingDuration = Array(0, 62, 60, 30) + colossus_gunner.RadiationShielding = 0.5f + colossus_gunner.DamageUsing = DamageCalculations.AgainstBfr + colossus_gunner.Model = BfrResolutions.calculate + colossus_gunner.innateDamage = new DamageWithPosition { + CausesDamageType = DamageType.One + Damage0 = 400 + Damage1 = 500 + DamageRadius = 10 + DamageAtEdge = 0.2f + Modifiers = ExplodingRadialDegrade + } + colossus_gunner.DrownAtMaxDepth = true + colossus_gunner.MaxDepth = 5.515625f + colossus_gunner.UnderwaterLifespan(suffocation = 6000L, recovery = 6000L) + colossus_gunner.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 5.984375f) + colossus_gunner.collision.avatarCollisionDamageMax = 300 + colossus_gunner.collision.xy = CollisionXYData(Array((0.2f, 1), (0.35f, 5), (0.55f, 20), (0.75f, 40), (1f, 60))) + colossus_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) + colossus_gunner.maxForwardSpeed = 17 + colossus_gunner.mass = 709.7f + + peregrine_gunner.Name = "peregrine_gunner" + peregrine_gunner.MaxHealth = 4500 + peregrine_gunner.Damageable = true + peregrine_gunner.Repairable = true + peregrine_gunner.RepairIfDestroyed = false + peregrine_gunner.shieldUiAttribute = 79 + peregrine_gunner.MaxShields = 3000 + peregrine_gunner.ShieldPeriodicDelay = 500 + peregrine_gunner.ShieldDamageDelay = 3500 + peregrine_gunner.ShieldAutoRecharge = 45 + peregrine_gunner.ShieldAutoRechargeSpecial = 85 + peregrine_gunner.DefaultShields = peregrine_gunner.MaxShields + peregrine_gunner.Seats += 0 -> driverSeat + peregrine_gunner.Seats += 1 -> normalSeat + peregrine_gunner.controlledWeapons(seat = 0, weapons = Set(2, 3)) + peregrine_gunner.controlledWeapons(seat = 1, weapon = 4) + peregrine_gunner.Weapons += 2 -> peregrine_dual_machine_gun_left + peregrine_gunner.Weapons += 3 -> peregrine_dual_machine_gun_right + peregrine_gunner.Weapons += 4 -> peregrine_particle_cannon + peregrine_gunner.MountPoints += 1 -> MountInfo(0) + peregrine_gunner.MountPoints += 2 -> MountInfo(1) + peregrine_gunner.subsystems = bfrGunnerSubsystems + peregrine_gunner.TrunkSize = InventoryTile.Tile1518 + peregrine_gunner.TrunkOffset = 30 + peregrine_gunner.TrunkLocation = Vector3(0f, -5f, 0f) + peregrine_gunner.AutoPilotSpeeds = (5, 1) + peregrine_gunner.Packet = battleFrameConverter + peregrine_gunner.DestroyedModel = None + peregrine_gunner.destructionDelay = Some(4000L) + peregrine_gunner.JackingDuration = Array(0, 62, 60, 30) + peregrine_gunner.RadiationShielding = 0.5f + peregrine_gunner.DamageUsing = DamageCalculations.AgainstBfr + peregrine_gunner.Model = BfrResolutions.calculate + peregrine_gunner.innateDamage = new DamageWithPosition { + CausesDamageType = DamageType.One + Damage0 = 400 + Damage1 = 500 + DamageRadius = 10 + DamageAtEdge = 0.2f + Modifiers = ExplodingRadialDegrade + } + peregrine_gunner.DrownAtMaxDepth = true + peregrine_gunner.MaxDepth = 6.03125f + peregrine_gunner.UnderwaterLifespan(suffocation = 6000L, recovery = 6000L) + peregrine_gunner.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 6.421875f) + peregrine_gunner.collision.avatarCollisionDamageMax = 300 + peregrine_gunner.collision.xy = CollisionXYData(Array((0.2f, 1), (0.35f, 5), (0.55f, 20), (0.75f, 40), (1f, 60))) + peregrine_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) + peregrine_gunner.maxForwardSpeed = 17 + peregrine_gunner.mass = 713f + + val battleFrameFlightConverter = new BattleFrameFlightConverter + aphelion_flight.Name = "aphelion_flight" + aphelion_flight.MaxHealth = 3500 + aphelion_flight.Damageable = true + aphelion_flight.Repairable = true + aphelion_flight.RepairIfDestroyed = false + aphelion_flight.CanFly = true + aphelion_flight.shieldUiAttribute = 79 + aphelion_flight.MaxShields = 2500 + aphelion_flight.ShieldPeriodicDelay = 500 + aphelion_flight.ShieldDamageDelay = 3500 + aphelion_flight.ShieldAutoRecharge = 12 //12.5 + aphelion_flight.ShieldAutoRechargeSpecial = 25 + aphelion_flight.ShieldDrain = 30 + aphelion_flight.DefaultShields = aphelion_flight.MaxShields + aphelion_flight.Seats += 0 -> bailableSeat + aphelion_flight.controlledWeapons(seat = 0, weapons = Set(1, 2)) + aphelion_flight.Weapons += 1 -> aphelion_ppa_left + aphelion_flight.Weapons += 2 -> aphelion_ppa_right + aphelion_flight.MountPoints += 1 -> MountInfo(0) + aphelion_flight.subsystems = bfrFlightSubsystems + aphelion_flight.TrunkSize = InventoryTile.Tile1511 + aphelion_flight.TrunkOffset = 30 + aphelion_flight.TrunkLocation = Vector3(0f, -2f, 0f) + aphelion_flight.AutoPilotSpeeds = (5, 1) + aphelion_flight.Packet = battleFrameFlightConverter + aphelion_flight.DestroyedModel = None + aphelion_flight.destructionDelay = Some(4000L) + aphelion_flight.JackingDuration = Array(0, 62, 60, 30) + aphelion_flight.RadiationShielding = 0.5f + aphelion_flight.DamageUsing = DamageCalculations.AgainstBfr + aphelion_flight.Model = BfrResolutions.calculate + aphelion_flight.innateDamage = new DamageWithPosition { + CausesDamageType = DamageType.One + Damage0 = 400 + Damage1 = 500 + DamageRadius = 10 + DamageAtEdge = 0.2f + Modifiers = ExplodingRadialDegrade + } + aphelion_flight.DrownAtMaxDepth = true + aphelion_flight.MaxDepth = 5.09375f + aphelion_flight.UnderwaterLifespan(suffocation = 6000L, recovery = 6000L) + aphelion_flight.Geometry = GeometryForm.representByCylinder(radius = 1.98045f, height = 6.03125f) + aphelion_flight.MaxCapacitor = 156 + aphelion_flight.DefaultCapacitor = aphelion_flight.MaxCapacitor + aphelion_flight.CapacitorDrain = 16 + aphelion_flight.CapacitorDrainSpecial = 3 + aphelion_flight.CapacitorRecharge = 42 + aphelion_flight.collision.avatarCollisionDamageMax = 300 + aphelion_flight.collision.xy = CollisionXYData(Array((0.2f, 1), (0.35f, 5), (0.55f, 20), (0.75f, 40), (1f, 60))) + aphelion_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) + aphelion_flight.maxForwardSpeed = 35 + aphelion_flight.mass = 615.1f + + colossus_flight.Name = "colossus_flight" + colossus_flight.MaxHealth = 3500 + colossus_flight.Damageable = true + colossus_flight.Repairable = true + colossus_flight.RepairIfDestroyed = false + colossus_flight.CanFly = true + colossus_flight.shieldUiAttribute = 79 + colossus_flight.MaxShields = 2500 + colossus_flight.ShieldPeriodicDelay = 500 + colossus_flight.ShieldDamageDelay = 3500 + colossus_flight.ShieldAutoRecharge = 12 //12.5 + colossus_flight.ShieldAutoRechargeSpecial = 25 + colossus_flight.ShieldDrain = 30 + colossus_flight.DefaultShields = colossus_flight.MaxShields + colossus_flight.Seats += 0 -> bailableSeat + colossus_flight.controlledWeapons(seat = 0, weapons = Set(1, 2)) + colossus_flight.Weapons += 1 -> colossus_tank_cannon_left + colossus_flight.Weapons += 2 -> colossus_tank_cannon_right + colossus_flight.MountPoints += 1 -> MountInfo(0) + colossus_flight.subsystems = bfrFlightSubsystems + colossus_flight.TrunkSize = InventoryTile.Tile1511 + colossus_flight.TrunkOffset = 30 + colossus_flight.TrunkLocation = Vector3(0f, -5f, 0f) + colossus_flight.AutoPilotSpeeds = (5, 1) + colossus_flight.Packet = battleFrameFlightConverter + colossus_flight.DestroyedModel = None + colossus_flight.destructionDelay = Some(4000L) + colossus_flight.JackingDuration = Array(0, 62, 60, 30) + colossus_flight.RadiationShielding = 0.5f + colossus_flight.DamageUsing = DamageCalculations.AgainstBfr + colossus_flight.Model = BfrResolutions.calculate + colossus_flight.innateDamage = new DamageWithPosition { + CausesDamageType = DamageType.One + Damage0 = 400 + Damage1 = 500 + DamageRadius = 10 + DamageAtEdge = 0.2f + Modifiers = ExplodingRadialDegrade + } + colossus_flight.DrownAtMaxDepth = true + colossus_flight.MaxDepth = 5.515625f + colossus_flight.UnderwaterLifespan(suffocation = 6000L, recovery = 6000L) + colossus_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 5.984375f) + colossus_flight.MaxCapacitor = 156 + colossus_flight.DefaultCapacitor = aphelion_flight.MaxCapacitor + colossus_flight.CapacitorDrain = 16 + colossus_flight.CapacitorDrainSpecial = 3 + colossus_flight.CapacitorRecharge = 42 + colossus_flight.collision.avatarCollisionDamageMax = 300 + colossus_flight.collision.xy = CollisionXYData(Array((0.2f, 1), (0.35f, 5), (0.55f, 20), (0.75f, 40), (1f, 60))) + colossus_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) + colossus_flight.maxForwardSpeed = 34 + colossus_flight.mass = 709.7f + + peregrine_flight.Name = "peregrine_flight" + peregrine_flight.MaxHealth = 3500 + peregrine_flight.Damageable = true + peregrine_flight.Repairable = true + peregrine_flight.RepairIfDestroyed = false + peregrine_flight.CanFly = true + peregrine_flight.shieldUiAttribute = 79 + peregrine_flight.MaxShields = 2500 + peregrine_flight.ShieldPeriodicDelay = 500 + peregrine_flight.ShieldDamageDelay = 3500 + peregrine_flight.ShieldAutoRecharge = 12 //12.5 + peregrine_flight.ShieldAutoRechargeSpecial = 25 + peregrine_flight.ShieldDrain = 30 + peregrine_flight.DefaultShields = peregrine_flight.MaxShields + peregrine_flight.Seats += 0 -> bailableSeat + peregrine_flight.controlledWeapons(seat = 0, weapons = Set(1, 2)) + peregrine_flight.Weapons += 1 -> peregrine_dual_machine_gun_left + peregrine_flight.Weapons += 2 -> peregrine_dual_machine_gun_right + peregrine_flight.MountPoints += 1 -> MountInfo(0) + peregrine_flight.subsystems = bfrFlightSubsystems + peregrine_flight.TrunkSize = InventoryTile.Tile1511 + peregrine_flight.TrunkOffset = 30 + peregrine_flight.TrunkLocation = Vector3(0f, -5f, 0f) + peregrine_flight.AutoPilotSpeeds = (5, 1) + peregrine_flight.Packet = battleFrameFlightConverter + peregrine_flight.DestroyedModel = None + peregrine_flight.destructionDelay = Some(4000L) + peregrine_flight.JackingDuration = Array(0, 62, 60, 30) + peregrine_flight.RadiationShielding = 0.5f + peregrine_flight.DamageUsing = DamageCalculations.AgainstBfr + peregrine_flight.Model = BfrResolutions.calculate + peregrine_flight.innateDamage = new DamageWithPosition { + CausesDamageType = DamageType.One + Damage0 = 400 + Damage1 = 500 + DamageRadius = 10 + DamageAtEdge = 0.2f + Modifiers = ExplodingRadialDegrade + } + peregrine_flight.DrownAtMaxDepth = true + peregrine_flight.MaxDepth = 6.03125f + peregrine_flight.UnderwaterLifespan(suffocation = 6000L, recovery = 6000L) + peregrine_flight.Geometry = GeometryForm.representByCylinder(radius = 3.60935f, height = 6.421875f) + peregrine_flight.MaxCapacitor = 156 + peregrine_flight.DefaultCapacitor = aphelion_flight.MaxCapacitor + peregrine_flight.CapacitorDrain = 16 + peregrine_flight.CapacitorDrainSpecial = 3 + peregrine_flight.CapacitorRecharge = 42 + peregrine_flight.collision.avatarCollisionDamageMax = 300 + peregrine_flight.collision.xy = CollisionXYData(Array((0.2f, 1), (0.35f, 5), (0.55f, 20), (0.75f, 40), (1f, 60))) + peregrine_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) + peregrine_flight.maxForwardSpeed = 35 + peregrine_flight.mass = 713f + } + /** * Initialize `Deployable` globals. */ @@ -7276,7 +8888,6 @@ object GlobalDefinitions { boomer.DeployCategory = DeployableCategory.Boomers boomer.DeployTime = Duration.create(1000, "ms") boomer.deployAnimation = DeployAnimation.Standard - boomer.explodes = true boomer.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.Splash SympatheticExplosion = true @@ -7299,7 +8910,6 @@ object GlobalDefinitions { he_mine.Repairable = false he_mine.DeployTime = Duration.create(1000, "ms") he_mine.deployAnimation = DeployAnimation.Standard - he_mine.explodes = true he_mine.triggerRadius = 3f he_mine.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.Splash @@ -7324,7 +8934,6 @@ object GlobalDefinitions { jammer_mine.DeployTime = Duration.create(1000, "ms") jammer_mine.deployAnimation = DeployAnimation.Standard jammer_mine.DetonateOnJamming = false - jammer_mine.explodes = true jammer_mine.triggerRadius = 3f jammer_mine.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.Splash @@ -7372,7 +8981,6 @@ object GlobalDefinitions { spitfire_turret.DeployTime = Duration.create(5000, "ms") spitfire_turret.Model = ComplexDeployableResolutions.calculate spitfire_turret.deployAnimation = DeployAnimation.Standard - spitfire_turret.explodes = true spitfire_turret.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -7399,7 +9007,6 @@ object GlobalDefinitions { spitfire_cloaked.DeployTime = Duration.create(5000, "ms") spitfire_cloaked.deployAnimation = DeployAnimation.Standard spitfire_cloaked.Model = ComplexDeployableResolutions.calculate - spitfire_cloaked.explodes = true spitfire_cloaked.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 50 @@ -7426,7 +9033,6 @@ object GlobalDefinitions { spitfire_aa.DeployTime = Duration.create(5000, "ms") spitfire_aa.deployAnimation = DeployAnimation.Standard spitfire_aa.Model = ComplexDeployableResolutions.calculate - spitfire_aa.explodes = true spitfire_aa.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 200 @@ -7491,7 +9097,7 @@ object GlobalDefinitions { portable_manned_turret.Damageable = true portable_manned_turret.Repairable = true portable_manned_turret.RepairIfDestroyed = false - portable_manned_turret.controlledWeapons += 0 -> 1 + portable_manned_turret.controlledWeapons(seat = 0, weapon = 1) portable_manned_turret.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret.WeaponPaths(1) += TurretUpgrade.None -> energy_gun portable_manned_turret.MountPoints += 1 -> MountInfo(0) @@ -7503,7 +9109,7 @@ object GlobalDefinitions { portable_manned_turret.DeployTime = Duration.create(6000, "ms") portable_manned_turret.deployAnimation = DeployAnimation.Fdu portable_manned_turret.Model = ComplexDeployableResolutions.calculate - portable_manned_turret.explodes = true + portable_manned_turret.RadiationShielding = 0.5f portable_manned_turret.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 150 @@ -7525,7 +9131,7 @@ object GlobalDefinitions { portable_manned_turret_nc.RepairIfDestroyed = false portable_manned_turret_nc.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret_nc.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_nc - portable_manned_turret_nc.controlledWeapons += 0 -> 1 + portable_manned_turret_nc.controlledWeapons(seat = 0, weapon = 1) portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0) portable_manned_turret_nc.MountPoints += 2 -> MountInfo(0) portable_manned_turret_nc.ReserveAmmunition = true @@ -7535,7 +9141,6 @@ object GlobalDefinitions { portable_manned_turret_nc.DeployTime = Duration.create(6000, "ms") portable_manned_turret_nc.deployAnimation = DeployAnimation.Fdu portable_manned_turret_nc.Model = ComplexDeployableResolutions.calculate - portable_manned_turret_nc.explodes = true portable_manned_turret_nc.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 150 @@ -7557,7 +9162,7 @@ object GlobalDefinitions { portable_manned_turret_tr.RepairIfDestroyed = false portable_manned_turret_tr.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret_tr.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_tr - portable_manned_turret_tr.controlledWeapons += 0 -> 1 + portable_manned_turret_tr.controlledWeapons(seat = 0, weapon = 1) portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0) portable_manned_turret_tr.MountPoints += 2 -> MountInfo(0) portable_manned_turret_tr.ReserveAmmunition = true @@ -7567,7 +9172,6 @@ object GlobalDefinitions { portable_manned_turret_tr.DeployTime = Duration.create(6000, "ms") portable_manned_turret_tr.deployAnimation = DeployAnimation.Fdu portable_manned_turret_tr.Model = ComplexDeployableResolutions.calculate - portable_manned_turret_tr.explodes = true portable_manned_turret_tr.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 150 @@ -7589,7 +9193,7 @@ object GlobalDefinitions { portable_manned_turret_vs.RepairIfDestroyed = false portable_manned_turret_vs.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret_vs.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_vs - portable_manned_turret_vs.controlledWeapons += 0 -> 1 + portable_manned_turret_vs.controlledWeapons(seat = 0, weapon = 1) portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0) portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0) portable_manned_turret_vs.ReserveAmmunition = true @@ -7599,7 +9203,6 @@ object GlobalDefinitions { portable_manned_turret_vs.DeployTime = Duration.create(6000, "ms") portable_manned_turret_vs.deployAnimation = DeployAnimation.Fdu portable_manned_turret_vs.Model = ComplexDeployableResolutions.calculate - portable_manned_turret_vs.explodes = true portable_manned_turret_vs.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 150 @@ -7765,6 +9368,7 @@ object GlobalDefinitions { implant_terminal_mech.Repairable = true implant_terminal_mech.autoRepair = AutoRepairStats(1.6f, 5000, 2400, 0.5f) implant_terminal_mech.RepairIfDestroyed = true + implant_terminal_mech.RadiationShielding = 0.5f implant_terminal_mech.Geometry = GeometryForm.representByCylinder(radius = 2.7813f, height = 6.4375f) implant_terminal_interface.Name = "implant_terminal_interface" @@ -7782,7 +9386,7 @@ object GlobalDefinitions { VehicleTerminalDefinition.groundVehicles, VehicleTerminalDefinition.trunk ) - ground_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + ground_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) ground_vehicle_terminal.MaxHealth = 500 ground_vehicle_terminal.Damageable = true ground_vehicle_terminal.Repairable = true @@ -7796,7 +9400,7 @@ object GlobalDefinitions { VehicleTerminalDefinition.flight1Vehicles, VehicleTerminalDefinition.trunk ) - air_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + air_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) air_vehicle_terminal.MaxHealth = 500 air_vehicle_terminal.Damageable = true air_vehicle_terminal.Repairable = true @@ -7810,7 +9414,7 @@ object GlobalDefinitions { VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.flight2Vehicles, VehicleTerminalDefinition.trunk ) - dropship_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + dropship_vehicle_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) dropship_vehicle_terminal.MaxHealth = 500 dropship_vehicle_terminal.Damageable = true dropship_vehicle_terminal.Repairable = true @@ -7824,7 +9428,7 @@ object GlobalDefinitions { VehicleTerminalDefinition.flight1Vehicles ++ VehicleTerminalDefinition.groundVehicles, VehicleTerminalDefinition.trunk ) - vehicle_terminal_combined.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + vehicle_terminal_combined.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) vehicle_terminal_combined.MaxHealth = 500 vehicle_terminal_combined.Damageable = true vehicle_terminal_combined.Repairable = true @@ -7838,7 +9442,7 @@ object GlobalDefinitions { VehicleTerminalDefinition.flight1Vehicles, VehicleTerminalDefinition.trunk ) - vanu_air_vehicle_term.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + vanu_air_vehicle_term.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) vanu_air_vehicle_term.MaxHealth = 500 vanu_air_vehicle_term.Damageable = true vanu_air_vehicle_term.Repairable = true @@ -7851,7 +9455,7 @@ object GlobalDefinitions { VehicleTerminalDefinition.groundVehicles, VehicleTerminalDefinition.trunk ) - vanu_vehicle_term.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + vanu_vehicle_term.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) vanu_vehicle_term.MaxHealth = 500 vanu_vehicle_term.Damageable = true vanu_vehicle_term.Repairable = true @@ -7860,11 +9464,19 @@ object GlobalDefinitions { vanu_vehicle_term.Subtract.Damage1 = 8 bfr_terminal.Name = "bfr_terminal" - bfr_terminal.Tab += 46769 -> OrderTerminalDefinition.VehiclePage( + bfr_terminal.Tab += 0 -> OrderTerminalDefinition.VehiclePage( VehicleTerminalDefinition.bfrVehicles, VehicleTerminalDefinition.trunk ) - bfr_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + bfr_terminal.Tab += 1 -> OrderTerminalDefinition.EquipmentPage( + EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons + ) //inaccessible? + bfr_terminal.Tab += 2 -> OrderTerminalDefinition.EquipmentPage( + EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrGunnerWeapons + ) //inaccessible? + bfr_terminal.Tab += 3 -> OrderTerminalDefinition.BattleframeSpawnLoadoutPage( + VehicleTerminalDefinition.bfrVehicles + ) bfr_terminal.MaxHealth = 500 bfr_terminal.Damageable = true bfr_terminal.Repairable = true @@ -8004,6 +9616,7 @@ object GlobalDefinitions { mb_pad_creation.Name = "mb_pad_creation" mb_pad_creation.Damageable = false mb_pad_creation.Repairable = false + mb_pad_creation.VehicleCreationZOffset = 2.52604f mb_pad_creation.killBox = VehicleSpawnPadDefinition.prepareKillBox( forwardLimit = 14, backLimit = 10, @@ -8022,6 +9635,8 @@ object GlobalDefinitions { dropship_pad_doors.Name = "dropship_pad_doors" dropship_pad_doors.Damageable = false dropship_pad_doors.Repairable = false + dropship_pad_doors.VehicleCreationZOffset = 4.89507f + dropship_pad_doors.VehicleCreationZOrientOffset = -90f dropship_pad_doors.killBox = VehicleSpawnPadDefinition.prepareKillBox( forwardLimit = 14, backLimit = 14, @@ -8053,6 +9668,43 @@ object GlobalDefinitions { //damage is 99999 at 14m, dropping rapidly to ~1 at 14.5m } + bfr_door.Name = "bfr_door" + bfr_door.Damageable = false + bfr_door.Repairable = false + //bfr_door.VehicleCreationZOffset = -4.5f + bfr_door.VehicleCreationZOrientOffset = 0f //90f + bfr_door.killBox = VehicleSpawnPadDefinition.prepareBfrShedKillBox( + radius = 10f, + aboveLimit = 10f + ) + bfr_door.innateDamage = new DamageWithPosition { + CausesDamageType = DamageType.One + Damage0 = 99999 + DamageRadiusMin = 14 //TODO fix this + DamageRadius = 14.5f //TODO fix this + DamageAtEdge = 0.00002f + //damage is 99999 at 14m, dropping rapidly to ~1 at 14.5m + } + + pad_create.Name = "pad_create" + pad_create.Damageable = false + pad_create.Repairable = false + //pad_create.killBox = ... + //pad_create.innateDamage = ... + + pad_creation.Name = "pad_creation" + pad_creation.Damageable = false + pad_creation.Repairable = false + pad_creation.VehicleCreationZOffset = 1.70982f + //pad_creation.killBox = ... + //pad_creation.innateDamage = ... + + spawnpoint_vehicle.Name = "spawnpoint_vehicle" + spawnpoint_vehicle.Damageable = false + spawnpoint_vehicle.Repairable = false + //spawnpoint_vehicle.killBox = ... + //spawnpoint_vehicle.innateDamage = ... + mb_locker.Name = "mb_locker" mb_locker.Damageable = false mb_locker.Repairable = false @@ -8106,23 +9758,26 @@ object GlobalDefinitions { multivehicle_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage( EquipmentTerminalDefinition.vehicleAmmunition ) - multivehicle_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + multivehicle_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) multivehicle_rearm_terminal.SellEquipmentByDefault = true //TODO ? multivehicle_rearm_terminal.Damageable = false multivehicle_rearm_terminal.Repairable = false bfr_rearm_terminal.Name = "bfr_rearm_terminal" - bfr_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage( - Map.empty[String, () => Equipment] - ) //TODO add stock to page - bfr_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + bfr_rearm_terminal.Tab += 1 -> OrderTerminalDefinition.EquipmentPage( + EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrArmWeapons + ) + bfr_rearm_terminal.Tab += 2 -> OrderTerminalDefinition.EquipmentPage( + EquipmentTerminalDefinition.bfrAmmunition ++ EquipmentTerminalDefinition.bfrGunnerWeapons + ) + bfr_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.VehicleLoadoutPage(15) bfr_rearm_terminal.SellEquipmentByDefault = true //TODO ? bfr_rearm_terminal.Damageable = false bfr_rearm_terminal.Repairable = false air_rearm_terminal.Name = "air_rearm_terminal" air_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) - air_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + air_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) air_rearm_terminal.SellEquipmentByDefault = true //TODO ? air_rearm_terminal.Damageable = false air_rearm_terminal.Repairable = false @@ -8131,7 +9786,7 @@ object GlobalDefinitions { ground_rearm_terminal.Tab += 3 -> OrderTerminalDefinition.EquipmentPage( EquipmentTerminalDefinition.vehicleAmmunition ) - ground_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage() + ground_rearm_terminal.Tab += 4 -> OrderTerminalDefinition.VehicleLoadoutPage(10) ground_rearm_terminal.SellEquipmentByDefault = true //TODO ? ground_rearm_terminal.Damageable = false ground_rearm_terminal.Repairable = false @@ -8147,11 +9802,11 @@ object GlobalDefinitions { manned_turret.WeaponPaths(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan manned_turret.WeaponPaths(1) += TurretUpgrade.AVCombo -> phalanx_avcombo manned_turret.WeaponPaths(1) += TurretUpgrade.FlakCombo -> phalanx_flakcombo - manned_turret.controlledWeapons += 0 -> 1 + manned_turret.controlledWeapons(seat = 0, weapon = 1) manned_turret.MountPoints += 1 -> MountInfo(0) manned_turret.FactionLocked = true manned_turret.ReserveAmmunition = false - manned_turret.explodes = true + manned_turret.RadiationShielding = 0.5f manned_turret.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 150 @@ -8171,7 +9826,7 @@ object GlobalDefinitions { vanu_sentry_turret.RepairIfDestroyed = true vanu_sentry_turret.WeaponPaths += 1 -> new mutable.HashMap() vanu_sentry_turret.WeaponPaths(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon - vanu_sentry_turret.controlledWeapons += 0 -> 1 + vanu_sentry_turret.controlledWeapons(seat = 0, weapon = 1) vanu_sentry_turret.MountPoints += 1 -> MountInfo(0) vanu_sentry_turret.MountPoints += 2 -> MountInfo(0) vanu_sentry_turret.FactionLocked = false @@ -8255,7 +9910,6 @@ object GlobalDefinitions { generator.RepairDistance = 13.5f generator.RepairIfDestroyed = true generator.Subtract.Damage1 = 9 - generator.explodes = true generator.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One Damage0 = 99999 diff --git a/src/main/scala/net/psforever/objects/Ntu.scala b/src/main/scala/net/psforever/objects/Ntu.scala index b5ce3807e..04be5cbc1 100644 --- a/src/main/scala/net/psforever/objects/Ntu.scala +++ b/src/main/scala/net/psforever/objects/Ntu.scala @@ -3,6 +3,7 @@ package net.psforever.objects import akka.actor.{Actor, ActorRef} import net.psforever.actors.commands.NtuCommand +import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.serverobject.transfer.{TransferBehavior, TransferContainer} object Ntu { @@ -34,12 +35,26 @@ object Ntu { final case class Grant(src: NtuContainer, amount: Float) } +trait NtuContainerOwner { + def getNtuContainer: Option[NtuContainer] +} + trait NtuContainer extends TransferContainer { def NtuCapacitor: Float def NtuCapacitor_=(value: Float): Float - def Definition: NtuContainerDefinition + def NtuCapacitorScaled: Int = { + if (Definition.MaxNtuCapacitor > 0) { + scala.math.ceil((NtuCapacitor / Definition.MaxNtuCapacitor) * 10).toInt + } else { + 0 + } + } + + def MaxNtuCapacitor: Float + + def Definition: ObjectDefinition with NtuContainerDefinition } trait CommonNtuContainer extends NtuContainer { @@ -51,8 +66,6 @@ trait CommonNtuContainer extends NtuContainer { ntuCapacitor = scala.math.max(0, scala.math.min(value, Definition.MaxNtuCapacitor)) NtuCapacitor } - - def Definition: NtuContainerDefinition } trait NtuContainerDefinition { diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 05403a288..db1d39907 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -2,6 +2,7 @@ package net.psforever.objects import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry} +import net.psforever.objects.ballistics.InteractWithRadiationClouds import net.psforever.objects.ce.{Deployable, InteractWithMines} import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition} import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} @@ -15,7 +16,7 @@ import net.psforever.objects.vital.resistance.ResistanceProfile import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.resolution.DamageResistanceModel -import net.psforever.objects.zones.blockmap.BlockMapEntity +import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation} import net.psforever.objects.zones.{InteractsWithZone, ZoneAware, Zoning} import net.psforever.types.{PlanetSideGUID, _} @@ -36,6 +37,7 @@ class Player(var avatar: Avatar) with MountableEntity { interaction(new InteractWithEnvironment()) interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10)) + interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) private var backpack: Boolean = false private var released: Boolean = false @@ -599,11 +601,11 @@ object Player { private class InteractWithMinesUnlessSpectating( private val obj: Player, - range: Float + override val range: Float ) extends InteractWithMines(range) { - override def interaction(target: InteractsWithZone): Unit = { + override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { if (!obj.spectator) { - super.interaction(target) + super.interaction(sector, target) } } } diff --git a/src/main/scala/net/psforever/objects/SpecialEmp.scala b/src/main/scala/net/psforever/objects/SpecialEmp.scala index 7fd2da1db..7c6f4551b 100644 --- a/src/main/scala/net/psforever/objects/SpecialEmp.scala +++ b/src/main/scala/net/psforever/objects/SpecialEmp.scala @@ -3,6 +3,7 @@ package net.psforever.objects import net.psforever.objects.ballistics.SourceEntry import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.equipment.{EffectTarget, TargetValidation} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.vital.{Vitality, VitalityDefinition} @@ -28,6 +29,30 @@ object SpecialEmp { DamageAtEdge = 1.0f DamageRadius = 5f AdditionalEffect = true + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Player, + EffectTarget.Validation.Player + ) -> 1000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.AMS + ) -> 5000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.MotionSensor + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.Spitfire + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Turret, + EffectTarget.Validation.Turret + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.VehicleNotAMS + ) -> 10000 Modifiers = MaxDistanceCutoff } @@ -37,7 +62,6 @@ object SpecialEmp { MaxHealth = 1 Damageable = false Repairable = false - explodes = true innateDamage = emp } diff --git a/src/main/scala/net/psforever/objects/Tool.scala b/src/main/scala/net/psforever/objects/Tool.scala index b5f4546c1..173513f12 100644 --- a/src/main/scala/net/psforever/objects/Tool.scala +++ b/src/main/scala/net/psforever/objects/Tool.scala @@ -21,15 +21,14 @@ class Tool(private val toolDef: ToolDefinition) extends Equipment with FireModeSwitch[FireModeDefinition] with JammableUnit { - + private var tdef = toolDef /** index of the current fire mode on the `ToolDefinition`'s list of fire modes */ - private var fireModeIndex: Int = toolDef.DefaultFireModeIndex - + private var fireModeIndex: Int = tdef.DefaultFireModeIndex /** current ammunition slot being used by this fire mode */ private var ammoSlots: List[Tool.FireModeSlot] = List.empty var lastDischarge: Long = 0 - Tool.LoadDefinition(this) + Tool.LoadDefinition(tool = this) def FireModeIndex: Int = fireModeIndex @@ -114,7 +113,7 @@ class Tool(private val toolDef: ToolDefinition) def MaxAmmoSlot: Int = ammoSlots.length - def Definition: ToolDefinition = toolDef + def Definition: ToolDefinition = tdef override def toString: String = Tool.toString(this) } @@ -131,9 +130,20 @@ object Tool { * @param tool the `Tool` being initialized */ def LoadDefinition(tool: Tool): Unit = { - val tdef: ToolDefinition = tool.Definition - val maxSlot = tdef.FireModes.maxBy(fmode => fmode.AmmoSlotIndex).AmmoSlotIndex + val tdef = tool.Definition + val maxSlot = tdef.FireModes.maxBy(fmode => fmode.AmmoSlotIndex).AmmoSlotIndex tool.ammoSlots = buildFireModes(tdef, (0 to maxSlot).iterator, tdef.FireModes.toList) + tool.fireModeIndex = tdef.DefaultFireModeIndex + } + /** + * Substitute this `Definition` for the one that was originally provided for this entity. + * Calling this will not reconstruct the internal fields of the entity. + * @param tool the `Tool` being modified + * @param tdef the definition used to override the definition that was previously assigned this `Tool`; + * WILL override the assignment in the original constructor + */ + def LoadDefinition(tool: Tool, tdef: ToolDefinition): Unit = { + tool.tdef = tdef } @tailrec private def buildFireModes( @@ -226,7 +236,7 @@ object Tool { def MaxMagazine(): Int = { fdef.CustomMagazine.get(AmmoType) match { case Some(value) => value - case None => fdef.Magazine + case None => fdef.DefaultMagazine } } diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala index b2fdd83a7..79126100b 100644 --- a/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/src/main/scala/net/psforever/objects/Vehicle.scala @@ -3,7 +3,7 @@ package net.psforever.objects import net.psforever.objects.ce.InteractWithMines import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition} -import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot, JammableUnit} +import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile} import net.psforever.objects.serverobject.mount.{MountableEntity, Seat, SeatDefinition} import net.psforever.objects.serverobject.PlanetSideServerObject @@ -19,8 +19,10 @@ import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.zones.InteractsWithZone import net.psforever.objects.zones.blockmap.BlockMapEntity +import net.psforever.packet.PlanetSideGamePacket import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} +import scala.annotation.tailrec import scala.concurrent.duration.FiniteDuration import scala.util.{Success, Try} @@ -89,13 +91,13 @@ class Vehicle(private val vehicleDef: VehicleDefinition) with AuraContainer with MountableEntity { interaction(new InteractWithEnvironment()) - interaction(new InteractWithMines(range = 30)) + interaction(new InteractWithMines(range = 20)) + interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20)) private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var shields: Int = 0 private var decal: Int = 0 private var trunkAccess: Option[PlanetSideGUID] = None - private var jammered: Boolean = false private var cloaked: Boolean = false private var flying: Option[Int] = None @@ -107,9 +109,10 @@ class Vehicle(private val vehicleDef: VehicleDefinition) */ private val groupPermissions: Array[VehicleLockState.Value] = Array(VehicleLockState.Locked, VehicleLockState.Empire, VehicleLockState.Empire, VehicleLockState.Locked) - private var cargoHolds: Map[Int, Cargo] = Map.empty - private var utilities: Map[Int, Utility] = Map() - private val trunk: GridInventory = GridInventory() + private var cargoHolds: Map[Int, Cargo] = Map.empty + private var utilities: Map[Int, Utility] = Map.empty + private var subsystems: List[VehicleSubsystem] = Nil + private val trunk: GridInventory = GridInventory() /* * Records the GUID of the cargo vehicle (galaxy/lodestar) this vehicle is stored in for DismountVehicleCargoMsg use @@ -128,7 +131,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition) * @see `Vehicle.LoadDefinition` */ protected def LoadDefinition(): Unit = { - Vehicle.LoadDefinition(this) + Vehicle.LoadDefinition(vehicle = this) } def Faction: PlanetSideEmpire.Value = { @@ -173,13 +176,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition) Decal } - def Jammered: Boolean = jammered - - def Jammered_=(jamState: Boolean): Boolean = { - jammered = jamState - Jammered - } - def Cloaked: Boolean = cloaked def Cloaked_=(isCloaked: Boolean): Boolean = { @@ -198,14 +194,6 @@ class Vehicle(private val vehicleDef: VehicleDefinition) Flying } - def NtuCapacitorScaled: Int = { - if (Definition.MaxNtuCapacitor > 0) { - scala.math.ceil((NtuCapacitor / Definition.MaxNtuCapacitor) * 10).toInt - } else { - 0 - } - } - def Capacitor: Int = capacitor def Capacitor_=(value: Int): Int = { @@ -291,7 +279,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition) } else { Seat(seatNumber) match { case Some(_) => - Definition.controlledWeapons.get(seatNumber) match { + Definition.controlledWeapons().get(seatNumber) match { case Some(_) => Some(AccessPermissionGroup.Gunner) case None => @@ -341,6 +329,42 @@ class Vehicle(private val vehicleDef: VehicleDefinition) } } + def Subsystems(): List[VehicleSubsystem] = subsystems + + def Subsystems(sys: VehicleSubsystemEntry): Option[VehicleSubsystem] = subsystems.find { _.sys == sys } + + def Subsystems(sys: String): Option[VehicleSubsystem] = subsystems.find { _.sys.name.contains(sys) } + + def SubsystemMessages(): List[PlanetSideGamePacket] = { + subsystems + .filter { sub => sub.Enabled != sub.sys.defaultState } + .flatMap { _.getMessage(vehicle = this) } + } + + def SubsystemStatus(sys: String): Option[Boolean] = { + val elems = sys.split("\\.") + if (elems.length < 2) { + None + } else { + Subsystems(elems.head) match { + case Some(sub) => sub.stateOfStatus(elems(1)) + case None => Some(false) + } + } + } + + def SubsystemStatusMultiplier(sys: String): Float = { + val elems = sys.split("\\.") + if (elems.length < 2) { + 1f + } else { + Subsystems(elems.head) match { + case Some(sub) => sub.multiplierOfStatus(elems(1)) + case None => 1f + } + } + } + override def DeployTime = Definition.DeployTime override def UndeployTime = Definition.UndeployTime @@ -352,35 +376,58 @@ class Vehicle(private val vehicleDef: VehicleDefinition) override def Slot(slotNum: Int): EquipmentSlot = { weapons .get(slotNum) - // .orElse(utilities.get(slotNum) match { - // case Some(_) => - // //TODO what do now? - // None - // case None => ; - // None - // }) .orElse(Some(Inventory.Slot(slotNum))) .get } + override def SlotMapResolution(slot: Int): Int = { + if (GlobalDefinitions.isBattleFrameVehicle(vehicleDef)) { + //for the benefit of BFR equipment slots interacting with MoveItemMessage + if (VisibleSlots.size == 2) { + if (slot == 0) 1 else if (slot == 1) 2 else slot //*_flight + } else { + if (slot == 0) 2 else if (slot == 1) 3 else if (slot == 2) 4 else slot //*_gunner + } + } else { + slot + } + } + override def Find(guid: PlanetSideGUID): Option[Int] = { weapons.find({ case (_, obj) => obj.Equipment match { - case Some(item) => - if (item.HasGUID && item.GUID == guid) { - true - } else { - false - } - case None => - false + case Some(item) => item.HasGUID && item.GUID == guid + case None => false } }) match { - case Some((index, _)) => + case Some((index, _)) => Some(index) + case None => Inventory.Find(guid) + } + } + + override def Fit(obj: Equipment): Option[Int] = { + recursiveSlotFit(weapons.iterator, obj.Size) match { + case Some(index) => Some(index) case None => - Inventory.Find(guid) + trunk.Fit(obj.Definition.Tile) + } + } + + @tailrec private def recursiveSlotFit( + iter: Iterator[(Int, EquipmentSlot)], + objSize: EquipmentSize.Value + ): Option[Int] = { + if (!iter.hasNext) { + None + } else { + val (index, slot) = iter.next() + if (slot.Equipment.isEmpty && slot.Size.equals(objSize)) { + Some(index) + } else { + recursiveSlotFit(iter, objSize) + } } } @@ -388,13 +435,10 @@ class Vehicle(private val vehicleDef: VehicleDefinition) weapons.get(dest) match { case Some(slot) => slot.Equipment match { - case Some(item) => - Success(List(InventoryItem(item, dest))) - case None => - Success(List()) + case Some(item) => Success(List(InventoryItem(item, dest))) + case None => Success(List()) } - case None => - super.Collisions(dest, width, height) + case None => super.Collisions(dest, width, height) } } @@ -514,6 +558,8 @@ class Vehicle(private val vehicleDef: VehicleDefinition) override def toString: String = { Vehicle.toString(this) } + + def MaxNtuCapacitor: Float = Definition.MaxNtuCapacitor } object Vehicle { @@ -558,6 +604,8 @@ object Vehicle { */ final case class UpdateShieldsCharge(vehicle: Vehicle) + final case class UpdateSubsystemStates(toChannel: String, stateToUpdateFor: Option[Boolean] = None) + /** * Change a vehicle's internal ownership property to match that of the target player. * @param player the person who will own the vehicle, or `None` if the vehicle will go unowned @@ -602,10 +650,12 @@ object Vehicle { val vdef: VehicleDefinition = vehicle.Definition //general stuff vehicle.Health = vdef.DefaultHealth + vehicle.Shields = vdef.DefaultShields + vehicle.Capacitor = vdef.DefaultCapacitor //create weapons vehicle.weapons = vdef.Weapons.map[Int, EquipmentSlot] { case (num: Int, definition: ToolDefinition) => - val slot = EquipmentSlot(EquipmentSize.VehicleWeapon) + val slot = EquipmentSlot(definition.Size) slot.Equipment = Tool(definition) num -> slot }.toMap @@ -628,6 +678,8 @@ object Vehicle { utilObj.LocationOffset = vdef.UtilityOffset.get(num) num -> obj }.toMap + //subsystems + vehicle.subsystems = vdef.subsystems.map { entry => new VehicleSubsystem(entry) } //trunk vdef.TrunkSize match { case InventoryTile.None => ; diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala index dc4ee5cdb..ffb583fc1 100644 --- a/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/src/main/scala/net/psforever/objects/Vehicles.scala @@ -4,8 +4,9 @@ package net.psforever.objects import net.psforever.objects.ce.TelepadLike import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.deploy.Deployment +import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.transfer.TransferContainer -import net.psforever.objects.serverobject.structures.{StructureType, WarpGate} +import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.vehicles._ import net.psforever.objects.zones.Zone import net.psforever.packet.game.TriggeredSound @@ -210,11 +211,12 @@ object Vehicles { * The orientation of a cargo vehicle as it is being loaded into and contained by a carrier vehicle. * The type of carrier is not an important consideration in determining the orientation, oddly enough. * @param vehicle the cargo vehicle - * @return the orientation as an `Integer` value; - * `0` for almost all cases + * @return the orientation; + * `1` is for unique sideways mounting; + * `0` is or straight-on mounting, valid for almost all cases */ def CargoOrientation(vehicle: Vehicle): Int = { - if (vehicle.Definition == GlobalDefinitions.router) { + if (vehicle.Definition == GlobalDefinitions.router || GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) { 1 } else { 0 @@ -232,7 +234,7 @@ object Vehicles { log.info(s"${hacker.Name} has jacked a ${target.Definition.Name}") val zone = target.Zone // Forcefully dismount any cargo - target.CargoHolds.foreach { case (index, cargoHold) => + target.CargoHolds.foreach { case (_, cargoHold) => cargoHold.occupant match { case Some(cargo: Vehicle) => cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false) @@ -339,33 +341,66 @@ object Vehicles { } def FindANTDischargingTarget( - obj: TransferContainer, - ntuChargingTarget: Option[TransferContainer] - ): Option[TransferContainer] = { - (ntuChargingTarget match { - case out @ Some(target: NtuContainer) if { - Vector3.DistanceSquared(obj.Position.xy, target.Position.xy) < 400 //20m is generous ... - } => + obj: TransferContainer, + ntuChargingTarget: Option[TransferContainer] + ): Option[TransferContainer] = { + FindResourceSiloToDischargeInto(obj, ntuChargingTarget, radius = 20) + } + + def FindBfrChargingSource( + obj: TransferContainer, + ntuChargingTarget: Option[TransferContainer] + ): Option[TransferContainer] = { + //determine if we are close enough to charge from something + val position = obj.Position.xy + ntuChargingTarget.orElse( + obj.Zone + .blockMap + .sector(position, range = 20f).buildingList + .sortBy { b => Vector3.DistanceSquared(position, b.Position.xy) } + .flatMap { _.NtuSource } + .headOption + ) match { + case out @ Some(_: WarpGate) => + out + case Some(silo: ResourceSilo) if { + val radius = 20f//3.6135f + Vector3.DistanceSquared(position, silo.Position.xy) < radius * radius && obj.Faction != silo.Faction + } => + Some(silo) + case _ => + None + } + } + + def FindBfrDischargingTarget( + obj: TransferContainer, + ntuChargingTarget: Option[TransferContainer] + ): Option[TransferContainer] = { + FindResourceSiloToDischargeInto(obj, ntuChargingTarget, radius = 20) //3.6135f? + } + + def FindResourceSiloToDischargeInto( + obj: TransferContainer, + ntuChargingTarget: Option[TransferContainer], + radius: Float + ): Option[TransferContainer] = { + //determine if we are close enough to charge from something + val position = obj.Position.xy + ntuChargingTarget.orElse( + obj.Zone + .blockMap + .sector(position, range = 20f) + .buildingList + .sortBy { b => Vector3.DistanceSquared(position, b.Position.xy) } + .flatMap { _.NtuSource } + .headOption + ) match { + case out @ Some(silo: ResourceSilo) + if Vector3.DistanceSquared(position, silo.Position.xy) < radius * radius && obj.Faction == silo.Faction => out case _ => None - }).orElse { - val position = obj.Position.xy - obj.Zone.Buildings.values - .find { building => - building.BuildingType == StructureType.Facility && { - val soiRadius = building.Definition.SOIRadius - Vector3.DistanceSquared(position, building.Position.xy) < soiRadius * soiRadius - } - } match { - case Some(building) => - building.Amenities - .collect { case obj: NtuContainer => obj } - .sortBy { o => Vector3.DistanceSquared(position, o.Position.xy) < 400 } //20m is generous ... - .headOption - case None => - None - } } } diff --git a/src/main/scala/net/psforever/objects/avatar/Avatar.scala b/src/main/scala/net/psforever/objects/avatar/Avatar.scala index d418a3831..036273e17 100644 --- a/src/main/scala/net/psforever/objects/avatar/Avatar.scala +++ b/src/main/scala/net/psforever/objects/avatar/Avatar.scala @@ -15,15 +15,19 @@ import scala.concurrent.duration._ object Avatar { val purchaseCooldowns: Map[BasicDefinition, FiniteDuration] = Map( GlobalDefinitions.ams -> 5.minutes, - GlobalDefinitions.ant -> 5.minutes, + GlobalDefinitions.ant -> 4.minutes, GlobalDefinitions.apc_nc -> 5.minutes, GlobalDefinitions.apc_tr -> 5.minutes, GlobalDefinitions.apc_vs -> 5.minutes, + GlobalDefinitions.aphelion_flight -> 15.minutes, //Temporarily - Default is 25 minutes + GlobalDefinitions.aphelion_gunner -> 15.minutes, //Temporarily - Default is 25 minutes GlobalDefinitions.aurora -> 5.minutes, GlobalDefinitions.battlewagon -> 5.minutes, + GlobalDefinitions.colossus_flight -> 15.minutes, //Temporarily - Default is 25 minutes + GlobalDefinitions.colossus_gunner -> 15.minutes, //Temporarily - Default is 25 minutes GlobalDefinitions.dropship -> 5.minutes, GlobalDefinitions.flail -> 5.minutes, - GlobalDefinitions.fury -> 5.minutes, + GlobalDefinitions.fury -> 2.minutes, GlobalDefinitions.galaxy_gunship -> 15.minutes, //Temporary - Default is 10 minutes GlobalDefinitions.lodestar -> 5.minutes, GlobalDefinitions.liberator -> 5.minutes, @@ -32,18 +36,20 @@ object Avatar { GlobalDefinitions.magrider -> 5.minutes, GlobalDefinitions.mediumtransport -> 5.minutes, GlobalDefinitions.mosquito -> 5.minutes, + GlobalDefinitions.peregrine_flight -> 15.minutes, //Temporarily - Default is 25 minutes + GlobalDefinitions.peregrine_gunner -> 15.minutes, //Temporarily - Default is 25 minutes GlobalDefinitions.phantasm -> 5.minutes, GlobalDefinitions.prowler -> 5.minutes, - GlobalDefinitions.quadassault -> 5.minutes, - GlobalDefinitions.quadstealth -> 5.minutes, + GlobalDefinitions.quadassault -> 2.minutes, + GlobalDefinitions.quadstealth -> 2.minutes, GlobalDefinitions.router -> 5.minutes, GlobalDefinitions.switchblade -> 5.minutes, - GlobalDefinitions.skyguard -> 5.minutes, - GlobalDefinitions.threemanheavybuggy -> 5.minutes, + GlobalDefinitions.skyguard -> 2.minutes, + GlobalDefinitions.threemanheavybuggy -> 2.minutes, GlobalDefinitions.thunderer -> 5.minutes, - GlobalDefinitions.two_man_assault_buggy -> 5.minutes, - GlobalDefinitions.twomanhoverbuggy -> 5.minutes, - GlobalDefinitions.twomanheavybuggy -> 5.minutes, + GlobalDefinitions.two_man_assault_buggy -> 2.minutes, + GlobalDefinitions.twomanhoverbuggy -> 2.minutes, + GlobalDefinitions.twomanheavybuggy -> 2.minutes, GlobalDefinitions.vanguard -> 5.minutes, GlobalDefinitions.vulture -> 5.minutes, GlobalDefinitions.wasp -> 5.minutes, @@ -89,7 +95,7 @@ case class Avatar( fatigued: Boolean = false, cosmetics: Option[Set[Cosmetic]] = None, certifications: Set[Certification] = Set(), - loadouts: Seq[Option[Loadout]] = Seq.fill(15)(None), + loadouts: Seq[Option[Loadout]] = Seq.fill(20)(None), squadLoadouts: Seq[Option[SquadLoadout]] = Seq.fill(10)(None), implants: Seq[Option[Implant]] = Seq(None, None, None), locker: LockerContainer = Avatar.makeLocker(), diff --git a/src/main/scala/net/psforever/objects/avatar/Certification.scala b/src/main/scala/net/psforever/objects/avatar/Certification.scala index 1fe4cd856..a0a0ac8d5 100644 --- a/src/main/scala/net/psforever/objects/avatar/Certification.scala +++ b/src/main/scala/net/psforever/objects/avatar/Certification.scala @@ -74,7 +74,7 @@ case object Certification extends IntEnum[Certification] { case object GroundSupport extends Certification(value = 17, name = "ground_support", cost = 2) case object BattleFrameRobotics - extends Certification(value = 18, name = "TODO2", cost = 4, requires = Set(ArmoredAssault2)) // TODO name + extends Certification(value = 18, name = "bfr_basic", cost = 4, requires = Set(ArmoredAssault2)) case object Flail extends Certification(value = 19, name = "flail", cost = 1, requires = Set(ArmoredAssault2)) @@ -87,10 +87,10 @@ case object Certification extends IntEnum[Certification] { case object GalaxyGunship extends Certification(value = 23, name = "gunship", cost = 2, requires = Set(AirSupport)) case object BFRAntiAircraft - extends Certification(value = 24, name = "TODO3", cost = 1, requires = Set(BattleFrameRobotics)) + extends Certification(value = 24, name = "bfr_aa_gunnery", cost = 1, requires = Set(BattleFrameRobotics)) case object BFRAntiInfantry - extends Certification(value = 25, name = "TODO4", cost = 1, requires = Set(BattleFrameRobotics)) // TODO name + extends Certification(value = 25, name = "bfr_ai_gunnery", cost = 1, requires = Set(BattleFrameRobotics)) case object StandardExoSuit extends Certification(value = 26, name = "standard_armor", cost = 0) diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 27aec2495..1426c0945 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -8,6 +8,7 @@ import net.psforever.objects.{Player, _} import net.psforever.objects.ballistics.PlayerSource import net.psforever.objects.ce.Deployable import net.psforever.objects.definition.DeployAnimation +import net.psforever.objects.definition.converter.OCM import net.psforever.objects.equipment._ import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.inventory.{GridInventory, InventoryItem} @@ -1111,7 +1112,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm val zone = obj.Zone val events = zone.AvatarEvents val name = player.Name - val definition = item.Definition val faction = obj.Faction val toChannel = if (player.isBackpack) { self.toString } else { name } val willBeVisible = obj.VisibleSlots.contains(slot) @@ -1143,12 +1143,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm toChannel, AvatarAction.SendResponse( Service.defaultPlayerGUID, - ObjectCreateDetailedMessage( - definition.ObjectId, - item.GUID, - ObjectCreateMessageParent(guid, slot), - definition.Packet.DetailedConstructorData(item).get - ) + OCM.detailed(item, ObjectCreateMessageParent(guid, slot)) ) ) if (!player.isBackpack && willBeVisible) { @@ -1328,10 +1323,10 @@ object PlayerControl { */ private def auraEffectToAttributeValue(effect: Aura): Int = effect match { case Aura.Plasma => 1 - case Aura.Comet => 2 + case Aura.Comet => 2 case Aura.Napalm => 4 - case Aura.Fire => 8 - case _ => 0 + case Aura.Fire => 8 + case _ => 0 } def sendResponse(zone: Zone, channel: String, msg: PlanetSideGamePacket): Unit = { diff --git a/src/main/scala/net/psforever/objects/ballistics/AggravatedDamage.scala b/src/main/scala/net/psforever/objects/ballistics/AggravatedDamage.scala index 26ab98f30..72cf20986 100644 --- a/src/main/scala/net/psforever/objects/ballistics/AggravatedDamage.scala +++ b/src/main/scala/net/psforever/objects/ballistics/AggravatedDamage.scala @@ -171,6 +171,8 @@ object AggravatedDamage { DamageType.Direct case DamageResolution.AggravatedSplash | DamageResolution.AggravatedSplashBurn => DamageType.Splash + case DamageResolution.Radiation => + DamageType.Splash case _ => DamageType.None } diff --git a/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala b/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala index 6eaa5a619..e13482eb6 100644 --- a/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala +++ b/src/main/scala/net/psforever/objects/ballistics/DeployableSource.scala @@ -15,16 +15,16 @@ final case class DeployableSource( position: Vector3, orientation: Vector3 ) extends SourceEntry { - override def Name = SourceEntry.NameFormat(obj_def.Name) - override def Faction = faction + override def Name = obj_def.Descriptor + override def Faction = faction def Definition: ObjectDefinition with DeployableDefinition = obj_def - def Health = health - def Shields = shields - def OwnerName = owner.Name - def Position = position - def Orientation = orientation - def Velocity = None - def Modifiers = obj_def.asInstanceOf[ResistanceProfile] + def Health = health + def Shields = shields + def OwnerName = owner.Name + def Position = position + def Orientation = orientation + def Velocity = None + def Modifiers = obj_def.asInstanceOf[ResistanceProfile] } object DeployableSource { diff --git a/src/main/scala/net/psforever/objects/ballistics/InteractWithRadiationClouds.scala b/src/main/scala/net/psforever/objects/ballistics/InteractWithRadiationClouds.scala new file mode 100644 index 000000000..bc8debcfa --- /dev/null +++ b/src/main/scala/net/psforever/objects/ballistics/InteractWithRadiationClouds.scala @@ -0,0 +1,84 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.ballistics + +import net.psforever.objects.Player +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.base.DamageResolution +import net.psforever.objects.vital.etc.RadiationReason +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.zones.blockmap.SectorPopulation +import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType} +import net.psforever.types.PlanetSideGUID + +case object RadiationInteraction extends ZoneInteractionType + +/** + * This game entity may infrequently test whether it may interact with radiation cloud projectiles + * that may be emitted in the game environment for a limited amount of time. + */ +class InteractWithRadiationClouds( + val range: Float, + private val user: Option[Player] + ) extends ZoneInteraction { + /** + * radiation clouds that, though detected, are skipped from affecting the target; + * in between interaction tests, a memory of the clouds that were tested last are retained and + * are excluded from being tested this next time; + * clouds that are detected a second time are cleared from the list and are available to be tested next time + */ + private var skipTargets: List[PlanetSideGUID] = List() + + def Type = RadiationInteraction + + /** + * Wander into a radiation cloud and suffer the consequences. + * @param sector the portion of the block map being tested + * @param target the fixed element in this test + */ + def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { + target match { + case t: Vitality => + val position = target.Position + //collect all projectiles in sector/range + val projectiles = sector + .projectileList + .filter { cloud => + val radius = cloud.Definition.DamageRadius + cloud.Definition.radiation_cloud && Zone.distanceCheck(target, cloud, radius * radius) + } + .distinct + val notSkipped = projectiles.filterNot { t => skipTargets.contains(t.GUID) } + skipTargets = notSkipped.map { _.GUID } + if (notSkipped.nonEmpty) { + //isolate one of each type of projectile + notSkipped + .foldLeft(Nil: List[Projectile]) { + (acc, next) => if (acc.exists { _.profile == next.profile }) acc else next :: acc + } + .foreach { projectile => + t.Actor ! Vitality.Damage( + DamageInteraction( + SourceEntry(target), + RadiationReason( + ProjectileQuality.modifiers(projectile, DamageResolution.Radiation, t, t.Position, user), + t.DamageModel, + 1f + ), + position + ).calculate() + ) + } + } + case _ => ; + } + } + + /** + * Any radiation clouds blocked from being tested should be cleared. + * All that can be done is blanking our retained previous effect targets. + * @param target the fixed element in this test + */ + def resetInteraction(target: InteractsWithZone): Unit = { + skipTargets = List() + } +} diff --git a/src/main/scala/net/psforever/objects/ballistics/Projectile.scala b/src/main/scala/net/psforever/objects/ballistics/Projectile.scala index 4e400d09f..b43ba44d7 100644 --- a/src/main/scala/net/psforever/objects/ballistics/Projectile.scala +++ b/src/main/scala/net/psforever/objects/ballistics/Projectile.scala @@ -9,6 +9,7 @@ import net.psforever.objects.entity.SimpleWorldEntity import net.psforever.objects.equipment.FireModeDefinition import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.vital.base.DamageResolution +import net.psforever.objects.zones.blockmap.BlockMapEntity import net.psforever.types.Vector3 /** @@ -47,7 +48,8 @@ final case class Projectile( quality: ProjectileQuality = ProjectileQuality.Normal, id: Long = Projectile.idGenerator.getAndIncrement(), fire_time: Long = System.currentTimeMillis() -) extends PlanetSideGameObject { +) extends PlanetSideGameObject + with BlockMapEntity { Position = shot_origin Orientation = shot_angle Velocity = shot_velocity.getOrElse { diff --git a/src/main/scala/net/psforever/objects/ballistics/ProjectileQuality.scala b/src/main/scala/net/psforever/objects/ballistics/ProjectileQuality.scala index 9a6308455..20145639f 100644 --- a/src/main/scala/net/psforever/objects/ballistics/ProjectileQuality.scala +++ b/src/main/scala/net/psforever/objects/ballistics/ProjectileQuality.scala @@ -1,6 +1,13 @@ //Copyright (c) 2020 PSForever package net.psforever.objects.ballistics +import net.psforever.objects.{PlanetSideGameObject, Player} +import net.psforever.objects.equipment.EquipmentSize +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.base.{DamageResolution, DamageType} +import net.psforever.types.{ImplantType, Vector3} + /** * Projectile quality is an external aspect of projectiles * that is not dependent on hard-coded definitions of the entities @@ -33,4 +40,48 @@ object ProjectileQuality { /** Assign a custom numeric qualifier value, usually to be applied to damage calculations. */ case class Modified(mod: Float) extends ProjectileQuality + + /** + * na + * @param projectile the projectile object + * @param resolution the resolution status to promote the projectile + * @return a copy of the projectile + */ + def modifiers( + projectile: Projectile, + resolution: DamageResolution.Value, + target: PlanetSideGameObject with FactionAffinity with Vitality, + pos: Vector3, + user: Option[Player] + ): Projectile = { + projectile.Resolve() //if not yet resolved once + if (projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated)) { + //aggravated + val quality = projectile.profile.Aggravated match { + case Some(aggravation) + if aggravation.targets.exists(validation => validation.test(target)) && + aggravation.info.exists(_.damage_type == AggravatedDamage.basicDamageType(resolution)) => + ProjectileQuality.AggravatesTarget + case _ => + ProjectileQuality.Normal + } + projectile.quality(quality) + } else if (projectile.tool_def.Size == EquipmentSize.Melee) { + //melee + user match { + case Some(player) => + val quality = player.avatar.implants.flatten.find { entry => entry.definition.implantType == ImplantType.MeleeBooster } match { + case Some(booster) if booster.active && player.avatar.stamina > 9 => + ProjectileQuality.Modified(25f) + case _ => + ProjectileQuality.Normal + } + projectile.quality(quality) + case None => + projectile + } + } else { + projectile + } + } } diff --git a/src/main/scala/net/psforever/objects/ballistics/Projectiles.scala b/src/main/scala/net/psforever/objects/ballistics/Projectiles.scala index dba5ff1a9..9dceecc20 100644 --- a/src/main/scala/net/psforever/objects/ballistics/Projectiles.scala +++ b/src/main/scala/net/psforever/objects/ballistics/Projectiles.scala @@ -25,6 +25,7 @@ object Projectiles extends Enumeration { final val anniversary_projectileb = Value(59) final val aphelion_immolation_cannon_projectile = Value(87) final val aphelion_laser_projectile = Value(91) + final val aphelion_plasma_cloud = Value(96) //radiation cloud final val aphelion_plasma_rocket_projectile = Value(99) final val aphelion_ppa_projectile = Value(103) final val aphelion_starfire_projectile = Value(108) @@ -48,6 +49,7 @@ object Projectiles extends Enumeration { final val falcon_projectile = Value(286) final val firebird_missile_projectile = Value(288) final val flail_projectile = Value(296) + final val flamethrower_fire_cloud = Value(301) final val flamethrower_fireball = Value(302) final val flamethrower_projectile = Value(303) final val flux_cannon_apc_projectile = Value(305) @@ -79,6 +81,7 @@ object Projectiles extends Enumeration { final val liberator_bomb_cluster_bomblet_projectile = Value(436) final val liberator_bomb_cluster_projectile = Value(437) final val liberator_bomb_projectile = Value(438) + final val maelstrom_grenade_damager = Value(464) final val maelstrom_grenade_projectile = Value(465) final val maelstrom_grenade_projectile_contact = Value(466) final val maelstrom_stream_projectile = Value(467) @@ -94,12 +97,14 @@ object Projectiles extends Enumeration { final val mine_projectile = Value(551) final val mine_sweeper_projectile = Value(554) final val mine_sweeper_projectile_enh = Value(555) + final val ntu_siphon_emp = Value(596) final val oicw_little_buddy = Value(601) final val oicw_projectile = Value(602) final val pellet_gun_projectile = Value(631) final val peregrine_dual_machine_gun_projectile = Value(639) final val peregrine_mechhammer_projectile = Value(647) final val peregrine_particle_cannon_projectile = Value(654) + final val peregrine_particle_cannon_radiation_cloud = Value(655) //radiation cloud final val peregrine_rocket_pod_projectile = Value(657) final val peregrine_sparrow_projectile = Value(661) final val phalanx_av_projectile = Value(665) @@ -117,6 +122,7 @@ object Projectiles extends Enumeration { final val pulsar_ap_projectile = Value(702) final val pulsar_projectile = Value(703) final val quasar_projectile = Value(713) + final val radiator_cloud = Value(717) //radiation cloud final val radiator_grenade_projectile = Value(718) final val radiator_sticky_projectile = Value(719) final val reaver_rocket_projectile = Value(723) diff --git a/src/main/scala/net/psforever/objects/ce/InteractWithMines.scala b/src/main/scala/net/psforever/objects/ce/InteractWithMines.scala index 3d3d83943..39e3f3fa2 100644 --- a/src/main/scala/net/psforever/objects/ce/InteractWithMines.scala +++ b/src/main/scala/net/psforever/objects/ce/InteractWithMines.scala @@ -1,16 +1,19 @@ // Copyright (c) 2021 PSForever package net.psforever.objects.ce -import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction} +import net.psforever.objects.zones.blockmap.SectorPopulation +import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType} import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable} import net.psforever.types.PlanetSideGUID +case object MineInteraction extends ZoneInteractionType + /** * This game entity may infrequently test whether it may interact with game world deployable extra-territorial munitions. * "Interact", here, is a graceful word for "trample upon" and the consequence should be an explosion * and maybe death. */ -class InteractWithMines(range: Float) +class InteractWithMines(val range: Float) extends ZoneInteraction { /** * mines that, though detected, are skipped from being alerted; @@ -20,14 +23,16 @@ class InteractWithMines(range: Float) */ private var skipTargets: List[PlanetSideGUID] = List() + def Type = MineInteraction + /** * Trample upon active mines in our current detection sector and alert those mines. + * @param sector the portion of the block map being tested * @param target the fixed element in this test */ - def interaction(target: InteractsWithZone): Unit = { + def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { val faction = target.Faction - val targets = target.Zone.blockMap - .sector(target.Position, range) + val targets = sector .deployableList .filter { case _: BoomerDeployable => false //boomers are specific types of ExplosiveDeployable but do not count here diff --git a/src/main/scala/net/psforever/objects/definition/ProjectileDefinition.scala b/src/main/scala/net/psforever/objects/definition/ProjectileDefinition.scala index cb72bf8de..28253156a 100644 --- a/src/main/scala/net/psforever/objects/definition/ProjectileDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/ProjectileDefinition.scala @@ -46,6 +46,8 @@ class ProjectileDefinition(objectId: Int) /** projectile takes the form of a type of "grenade"; * grenades arc with gravity rather than travel in a relatively straight path */ private var grenade_projectile: Boolean = false + /** radiation clouds create independent damage-dealing areas in a zone that last for the projectile's lifespan */ + var radiation_cloud: Boolean = false //derived calculations /** the calculated distance at which the projectile have traveled far enough to despawn (m); * typically handled as the projectile no longer performing damage; diff --git a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala index f5cbf394e..c89fd5b6e 100644 --- a/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala @@ -6,7 +6,7 @@ import net.psforever.objects.{Default, NtuContainerDefinition, Vehicle} import net.psforever.objects.definition.converter.VehicleConverter import net.psforever.objects.inventory.InventoryTile import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.vehicles.{DestroyedVehicle, MountableWeaponsDefinition, UtilityType} +import net.psforever.objects.vehicles.{DestroyedVehicle, MountableWeaponsDefinition, UtilityType, VehicleSubsystemEntry} import net.psforever.objects.vital._ import net.psforever.objects.vital.damage.DamageCalculations import net.psforever.objects.vital.resistance.ResistanceProfileMutators @@ -27,20 +27,39 @@ class VehicleDefinition(objectId: Int) with NtuContainerDefinition with ResistanceProfileMutators with DamageResistanceModel { - /** vehicle shields offered through amp station facility benefits (generally: 20% of health + 1) */ + /** ... */ + var shieldUiAttribute: Int = 68 + /** how many points of shield the vehicle starts with (should default to 0 if unset through the accessor) */ + private var defaultShields : Option[Int] = None + /** maximum vehicle shields (generally: 20% of health) + * for normal vehicles, offered through amp station facility benefits + * for BFR's, it charges naturally + **/ private var maxShields: Int = 0 + /** the minimum amount of time that must elapse in between damage and shield charge activities (ms) */ + private var shieldChargeDamageCooldown : Long = 5000L + /** the minimum amount of time that must elapse in between distinct shield charge activities (ms) */ + private var shieldChargePeriodicCooldown : Long = 1000L + /** if the shield recharges on its own, this value will be non-`None` and indicate by how much */ + private var autoShieldRecharge : Option[Int] = None + private var autoShieldRechargeSpecial : Option[Int] = None + /** shield drain is what happens to the shield under special conditions, e.g., bfr flight; + * the drain interval is 250ms which is convenient for us + * we can skip needing to define is explicitly */ + private var shieldDrain : Option[Int] = None private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]() private var deployment: Boolean = false private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap() private val utilityOffsets: mutable.HashMap[Int, Vector3] = mutable.HashMap() + var subsystems: List[VehicleSubsystemEntry] = Nil private var deploymentTime_Deploy: Int = 0 //ms private var deploymentTime_Undeploy: Int = 0 //ms private var trunkSize: InventoryTile = InventoryTile.None private var trunkOffset: Int = 0 /* The position offset of the trunk, orientation as East = 0 */ - private var trunkLocation: Vector3 = Vector3.Zero - private var canCloak: Boolean = false - private var canFly: Boolean = false + private var trunkLocation: Vector3 = Vector3.Zero + private var canCloak: Boolean = false + private var canFly: Boolean = false /** whether the vehicle gains and/or maintains ownership based on access to the driver seat
* `Some(true)` - assign ownership upon the driver mount, maintains ownership after the driver dismounts
* `Some(false)` - assign ownership upon the driver mount, becomes unowned after the driver dismounts
@@ -48,12 +67,22 @@ class VehicleDefinition(objectId: Int) * Be cautious about using `None` as the client tends to equate the driver seat as the owner's seat for many vehicles * and breaking from the client's convention either requires additional fields or just doesn't work. */ - private var canBeOwned: Option[Boolean] = Some(true) - private var serverVehicleOverrideSpeeds: (Int, Int) = (0, 0) - var undergoesDecay: Boolean = true - private var deconTime: Option[FiniteDuration] = None - private var maxCapacitor: Int = 0 - private var destroyedModel: Option[DestroyedVehicle.Value] = None + private var canBeOwned: Option[Boolean] = Some(true) + private var serverVehicleOverrideSpeeds: (Int, Int) = (0, 0) + var undergoesDecay: Boolean = true + private var deconTime: Option[FiniteDuration] = None + private var defaultCapacitor: Int = 0 + private var maxCapacitor: Int = 0 + private var capacitorRecharge: Int = 0 + private var capacitorDrain: Int = 0 + private var capacitorDrainSpecial: Int = 0 + /** + * extend the time of the final scrapping and explosion further beyond when the vehicle is functionally rendered destroyed; + * see `innateDamage` for explosion information; + * for BFR's, the ADB field is `death_large_explosion_interval` + */ + var destructionDelay: Option[Long] = None + private var destroyedModel: Option[DestroyedVehicle.Value] = None Name = "vehicle" Packet = VehicleDefinition.converter DamageUsing = DamageCalculations.AgainstVehicle @@ -63,6 +92,15 @@ class VehicleDefinition(objectId: Int) RepairRestoresAt = 1 registerAs = "vehicles" + def DefaultShields: Int = defaultShields.getOrElse(0) + + def DefaultShields_=(shield: Int): Int = DefaultShields_=(Some(shield)) + + def DefaultShields_=(shield: Option[Int]): Int = { + defaultShields = shield + DefaultShields + } + def MaxShields: Int = maxShields def MaxShields_=(shields: Int): Int = { @@ -70,6 +108,47 @@ class VehicleDefinition(objectId: Int) MaxShields } + def ShieldPeriodicDelay : Long = shieldChargePeriodicCooldown + + def ShieldPeriodicDelay_=(cooldown: Long): Long = { + shieldChargePeriodicCooldown = cooldown + ShieldPeriodicDelay + } + + def ShieldDamageDelay: Long = shieldChargeDamageCooldown + + def ShieldDamageDelay_=(cooldown: Long): Long = { + shieldChargeDamageCooldown = cooldown + ShieldDamageDelay + } + + def ShieldAutoRecharge: Option[Int] = autoShieldRecharge + + def ShieldAutoRecharge_=(charge: Int): Option[Int] = ShieldAutoRecharge_=(Some(charge)) + + def ShieldAutoRecharge_=(charge: Option[Int]): Option[Int] = { + autoShieldRecharge = charge + ShieldAutoRecharge + } + + def ShieldAutoRechargeSpecial: Option[Int] = autoShieldRechargeSpecial.orElse(ShieldAutoRecharge) + + def ShieldAutoRechargeSpecial_=(charge: Int): Option[Int] = ShieldAutoRechargeSpecial_=(Some(charge)) + + def ShieldAutoRechargeSpecial_=(charge: Option[Int]): Option[Int] = { + autoShieldRechargeSpecial = charge + ShieldAutoRechargeSpecial + } + + def ShieldDrain: Option[Int] = shieldDrain + + def ShieldDrain_=(drain: Int): Option[Int] = ShieldDrain_=(Some(drain)) + + def ShieldDrain_=(drain: Option[Int]): Option[Int] = { + shieldDrain = drain + ShieldDrain + } + def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo def CanBeOwned: Option[Boolean] = canBeOwned @@ -164,6 +243,13 @@ class VehicleDefinition(objectId: Int) def AutoPilotSpeed2: Int = serverVehicleOverrideSpeeds._2 + def DefaultCapacitor: Int = defaultCapacitor + + def DefaultCapacitor_=(defValue: Int): Int = { + defaultCapacitor = defValue + DefaultCapacitor + } + def MaxCapacitor : Int = maxCapacitor def MaxCapacitor_=(max: Int) : Int = { @@ -171,6 +257,27 @@ class VehicleDefinition(objectId: Int) MaxCapacitor } + def CapacitorRecharge: Int = capacitorRecharge + + def CapacitorRecharge_=(charge: Int): Int = { + capacitorRecharge = charge + CapacitorRecharge + } + + def CapacitorDrain: Int = capacitorDrain + + def CapacitorDrain_=(charge: Int): Int = { + capacitorDrain = charge + CapacitorDrain + } + + def CapacitorDrainSpecial: Int = capacitorDrainSpecial + + def CapacitorDrainSpecial_=(charge: Int): Int = { + capacitorDrainSpecial = charge + CapacitorDrainSpecial + } + private var jackDuration = Array(0, 0, 0, 0) def JackingDuration: Array[Int] = jackDuration def JackingDuration_=(arr: Array[Int]): Array[Int] = { @@ -253,6 +360,36 @@ object VehicleDefinition { */ def Apc(objectId: Int): VehicleDefinition = new ApcDefinition(objectId) + protected class BfrDefinition(objectId: Int) extends VehicleDefinition(objectId) { + import net.psforever.objects.vehicles.control.BfrControl + override def Initialize(obj: Vehicle, context: ActorContext): Unit = { + obj.Actor = context.actorOf( + Props(classOf[BfrControl], obj), + PlanetSideServerObject.UniqueActorName(obj) + ) + } + } + /** + * Vehicle definition(s) for the battle frame robotics vehicles. + * @param objectId the object id that is associated with this sort of `Vehicle` + */ + def Bfr(objectId: Int): VehicleDefinition = new BfrDefinition(objectId) + + protected class BfrFlightDefinition(objectId: Int) extends VehicleDefinition(objectId) { + import net.psforever.objects.vehicles.control.BfrFlightControl + override def Initialize(obj: Vehicle, context: ActorContext): Unit = { + obj.Actor = context.actorOf( + Props(classOf[BfrFlightControl], obj), + PlanetSideServerObject.UniqueActorName(obj) + ) + } + } + /** + * Vehicle definition(s) for the flight variant of the battle frame robotics vehicles. + * @param objectId the object id that is associated with this sort of `Vehicle` + */ + def BfrFlight(objectId: Int): VehicleDefinition = new BfrFlightDefinition(objectId) + protected class CarrierDefinition(objectId: Int) extends VehicleDefinition(objectId) { import net.psforever.objects.vehicles.control.CargoCarrierControl override def Initialize(obj: Vehicle, context: ActorContext): Unit = { diff --git a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala index 819f697dc..97c77b543 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala @@ -160,7 +160,7 @@ object AvatarConverter { 0L, 0L, 0L, - Some(DCDExtra2(0, 0)), + None, //Some(ImprintingProgress(0, 0)), Nil, Nil, unkC = false, diff --git a/src/main/scala/net/psforever/objects/definition/converter/BattleFrameFlightConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameFlightConverter.scala new file mode 100644 index 000000000..49fcd57c7 --- /dev/null +++ b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameFlightConverter.scala @@ -0,0 +1,115 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.equipment.{Equipment, EquipmentSlot} +import net.psforever.objects.vehicles.VehicleSubsystemEntry +import net.psforever.objects.{PlanetSideGameObject, Vehicle} +import net.psforever.types.PlanetSideGUID +import net.psforever.packet.game.objectcreate._ + +import scala.util.{Failure, Success, Try} + +class BattleFrameFlightConverter extends ObjectCreateConverter[Vehicle]() { + override def DetailedConstructorData(obj: Vehicle): Try[BattleFrameRoboticsData] = + Failure(new Exception("BattleFrameFlightConverter should not be used to generate detailed BattleFrameRoboticsData (nothing should)")) + + override def ConstructorData(obj: Vehicle): Try[BattleFrameRoboticsData] = { + val health = StatConverter.Health(obj.Health, obj.MaxHealth) + if(health > 0) { //active + Success( + BattleFrameRoboticsData( + PlacementData(obj.Position, obj.Orientation, obj.Velocity), + CommonFieldData( + obj.Faction, + bops = false, + alternate = false, + v1 = true, + v2 = None, + jammered = false, + v4 = None, + v5 = None, + obj.Owner match { + case Some(owner) => owner + case None => PlanetSideGUID(0) + } + ), + health, + StatConverter.Health(obj.Shields, obj.MaxShields), + unk1 = 0, + unk2 = false, + no_mount_points = false, + driveState = 60, + proper_anim = true, + unk3 = 0, + show_bfr_shield = showBfrShield(obj), + unk4 = Some(false), + Some(InventoryData(MakeDriverSeat(obj) ++ MakeUtilities(obj) ++ MakeMountings(obj))) + ) + ) + } + else { //destroyed + Success( + BattleFrameRoboticsData( + PlacementData(obj.Position, obj.Orientation), + CommonFieldData( + obj.Faction, + bops = false, + alternate = false, + v1 = true, + v2 = None, + jammered = false, + v4 = None, + v5 = None, + guid = PlanetSideGUID(0) + ), + 0, + 0, + unk1 = 0, + unk2 = false, + no_mount_points = false, + driveState = 0, + proper_anim = true, + unk3 = 0, + show_bfr_shield = false, + unk4 = Some(false), + inventory = None + ) + ) + } + } + + private def MakeDriverSeat(obj: Vehicle): List[InventoryItemData.InventoryItem] = { + val offset: Long = MountableInventory.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, VehicleFormat.BattleframeFlight) + obj.Seats(0).occupant match { + case Some(player) => + List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, SeatConverter.MakeSeat(player, offset))) + case None => + Nil + } + } + + private def MakeMountings(obj: Vehicle): List[InventoryItemData.InventoryItem] = { + obj.Weapons.collect { + case (index, slot: EquipmentSlot) if slot.Equipment.nonEmpty => + val equip: Equipment = slot.Equipment.get + val equipDef = equip.Definition + InventoryItemData(equipDef.ObjectId, equip.GUID, index, equipDef.Packet.ConstructorData(equip).get) + }.toList + } + + protected def MakeUtilities(obj: Vehicle): List[InventoryItemData.InventoryItem] = { + Vehicle + .EquipmentUtilities(obj.Utilities) + .map({ + case (index, utilContainer) => + val util: PlanetSideGameObject = utilContainer() + val utilDef = util.Definition + InventoryItemData(utilDef.ObjectId, util.GUID, index, utilDef.Packet.ConstructorData(util).get) + }) + .toList + } + + def showBfrShield(obj: Vehicle): Boolean = { + obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled && obj.Shields > 0 + } +} diff --git a/src/main/scala/net/psforever/objects/definition/converter/BattleFrameRoboticsConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameRoboticsConverter.scala new file mode 100644 index 000000000..0f94dfc9c --- /dev/null +++ b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameRoboticsConverter.scala @@ -0,0 +1,115 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.equipment.{Equipment, EquipmentSlot} +import net.psforever.objects.vehicles.VehicleSubsystemEntry +import net.psforever.objects.{PlanetSideGameObject, Vehicle} +import net.psforever.types.PlanetSideGUID +import net.psforever.packet.game.objectcreate._ + +import scala.util.{Failure, Success, Try} + +class BattleFrameRoboticsConverter extends ObjectCreateConverter[Vehicle]() { + override def DetailedConstructorData(obj: Vehicle): Try[BattleFrameRoboticsData] = + Failure(new Exception("BattleFrameRoboticsConverter should not be used to generate detailed BattleFrameRoboticsData (nothing should)")) + + override def ConstructorData(obj: Vehicle): Try[BattleFrameRoboticsData] = { + val health = StatConverter.Health(obj.Health, obj.MaxHealth) + if(health > 0) { //active + Success( + BattleFrameRoboticsData( + PlacementData(obj.Position, obj.Orientation, obj.Velocity), + CommonFieldData( + obj.Faction, + bops = false, + alternate = false, + v1 = true, + v2 = None, + jammered = obj.Jammed, + v4 = None, + v5 = None, + obj.Owner match { + case Some(owner) => owner + case None => PlanetSideGUID(0) + } + ), + health, + StatConverter.Health(obj.Shields, obj.MaxShields), + unk1 = 0, + unk2 = false, + no_mount_points = false, + driveState = 60, + proper_anim = true, + unk3 = 0, + show_bfr_shield = showBfrShield(obj), + unk4 = None, + Some(InventoryData(MakeDriverSeat(obj) ++ MakeUtilities(obj) ++ MakeMountings(obj))) + ) + ) + } + else { //destroyed + Success( + BattleFrameRoboticsData( + PlacementData(obj.Position, obj.Orientation), + CommonFieldData( + obj.Faction, + bops = false, + alternate = false, + v1 = true, + v2 = None, + jammered = false, + v4 = None, + v5 = None, + guid = PlanetSideGUID(0) + ), + 0, + 0, + unk1 = 0, + unk2 = false, + no_mount_points = false, + driveState = 0, + proper_anim = true, + unk3 = 0, + show_bfr_shield = false, + unk4 = None, + inventory = None + ) + ) + } + } + + private def MakeDriverSeat(obj: Vehicle): List[InventoryItemData.InventoryItem] = { + val offset: Long = MountableInventory.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, VehicleFormat.Battleframe) + obj.Seats(0).occupant match { + case Some(player) => + List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, SeatConverter.MakeSeat(player, offset))) + case None => + Nil + } + } + + private def MakeMountings(obj: Vehicle): List[InventoryItemData.InventoryItem] = { + obj.Weapons.collect { + case (index, slot: EquipmentSlot) if slot.Equipment.nonEmpty => + val equip: Equipment = slot.Equipment.get + val equipDef = equip.Definition + InventoryItemData(equipDef.ObjectId, equip.GUID, index, equipDef.Packet.ConstructorData(equip).get) + }.toList + } + + protected def MakeUtilities(obj: Vehicle): List[InventoryItemData.InventoryItem] = { + Vehicle + .EquipmentUtilities(obj.Utilities) + .map({ + case (index, utilContainer) => + val util: PlanetSideGameObject = utilContainer() + val utilDef = util.Definition + InventoryItemData(utilDef.ObjectId, util.GUID, index, utilDef.Packet.ConstructorData(util).get) + }) + .toList + } + + def showBfrShield(obj: Vehicle): Boolean = { + obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled && obj.Shields > 0 + } +} diff --git a/src/main/scala/net/psforever/objects/definition/converter/BattleFrameToolConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameToolConverter.scala new file mode 100644 index 000000000..1075dc12d --- /dev/null +++ b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameToolConverter.scala @@ -0,0 +1,37 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.Tool +import net.psforever.packet.game.objectcreate.{CommonFieldData, DetailedWeaponData, InternalSlot, WeaponData} +import net.psforever.types.PlanetSideGUID + +import scala.util.{Failure, Success, Try} + +class BattleFrameToolConverter extends ObjectCreateConverter[Tool]() { + override def ConstructorData(obj: Tool): Try[WeaponData] = { + val slots: List[InternalSlot] = (0 until obj.MaxAmmoSlot).map(index => { + val box = obj.AmmoSlots(index).Box + InternalSlot(box.Definition.ObjectId, box.GUID, index, box.Definition.Packet.ConstructorData(box).get) + }).toList + Success( + WeaponData( + CommonFieldData( + obj.Faction, + bops = false, + alternate = false, + true, + None, + obj.Jammed, + Some(false), + None, + PlanetSideGUID(0) + ), + obj.FireModeIndex, + slots + ) + ) + } + + override def DetailedConstructorData(obj: Tool): Try[DetailedWeaponData] = + Failure(new Exception("BattleFrameToolConverter should not be used to generate detailed BattleFrameRToolData (nothing should)")) +} diff --git a/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala index 49328f092..12a5d77d8 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala @@ -119,7 +119,7 @@ class CharacterSelectConverter extends AvatarConverter { 0L, 0L, 0L, - Some(DCDExtra2(0, 0)), + Some(ImprintingProgress(0, 0)), Nil, Nil, unkC = false, diff --git a/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala index dcdf82fde..fd9ba6344 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala @@ -108,7 +108,7 @@ class CorpseConverter extends AvatarConverter { 0L, 0L, 0L, - Some(DCDExtra2(0, 0)), + Some(ImprintingProgress(0, 0)), Nil, Nil, unkC = false, diff --git a/src/main/scala/net/psforever/objects/definition/converter/OCM.scala b/src/main/scala/net/psforever/objects/definition/converter/OCM.scala new file mode 100644 index 000000000..16354c98c --- /dev/null +++ b/src/main/scala/net/psforever/objects/definition/converter/OCM.scala @@ -0,0 +1,101 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.PlanetSideGameObject +import net.psforever.packet.PlanetSideGamePacket +import net.psforever.packet.game.{ObjectCreateDetailedMessage, ObjectCreateMessage} +import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent + +/** + * Compose an `ObjectCreateMessage` packet or, if requesting and allowing, an `ObjectCreateDetailedMessage` packet. + */ +object OCM { + /** + * Compose an `ObjectCreateMessage` packet of an entity. + * @param obj the entity being converted into a packet + * @return an `ObjectCreateMessage` packet + */ + def apply(obj: PlanetSideGameObject): PlanetSideGamePacket = { + val definition = obj.Definition + ObjectCreateMessage( + definition.ObjectId, + obj.GUID, + definition.Packet.ConstructorData(obj).get + ) + } + + /** + * Compose a contained `ObjectCreateMessage` packet of an entity. + * @param obj the entity being converted into a packet + * @param parent information about the container for this entity + * @return an `ObjectCreateMessage` packet + */ + def apply(obj: PlanetSideGameObject, parent: Option[ObjectCreateMessageParent]): PlanetSideGamePacket = { + parent match { + case Some(info) => apply(obj, info) + case _ => apply(obj) + } + } + /** + * Compose a contained `ObjectCreateMessage` packet of an entity. + * @param obj the entity being converted into a packet + * @param parent information about the container for this entity + * @return an `ObjectCreateMessage` packet + */ + def apply(obj: PlanetSideGameObject, parent: ObjectCreateMessageParent): PlanetSideGamePacket = { + val definition = obj.Definition + ObjectCreateMessage( + definition.ObjectId, + obj.GUID, + parent, + definition.Packet.ConstructorData(obj).get + ) + } + + def detailed(obj: PlanetSideGameObject): PlanetSideGamePacket = { + val definition = obj.Definition + val packet = definition.Packet + if (packet.noDetailedForm(obj)) { + apply(obj) //fall back + } else { + ObjectCreateDetailedMessage( + definition.ObjectId, + obj.GUID, + definition.Packet.DetailedConstructorData(obj).get + ) + } + } + + /** + * Compose a contained detailed `ObjectCreateMessage` packet of an entity. + * @param obj the entity being converted into a packet + * @param parent information about the container for this entity + * @return an `ObjectCreateMessage` packet + */ + def detailed(obj: PlanetSideGameObject, parent: Option[ObjectCreateMessageParent]): PlanetSideGamePacket = { + parent match { + case Some(info) => detailed(obj, info) + case _ => detailed(obj) + } + } + /** + * Compose a contained detailed `ObjectCreateMessage` packet of an entity. + * @param obj the entity being converted into a packet + * @param parent information about the container for this entity + * @return an `ObjectCreateMessage` packet + */ + def detailed(obj: PlanetSideGameObject, parent: ObjectCreateMessageParent): PlanetSideGamePacket = { + val definition = obj.Definition + val packet = definition.Packet + if (packet.noDetailedForm(obj)) { + apply(obj, parent) //fall back + } else { + ObjectCreateDetailedMessage( + definition.ObjectId, + obj.GUID, + parent, + definition.Packet.DetailedConstructorData(obj).get + ) + } + } +} diff --git a/src/main/scala/net/psforever/objects/definition/converter/PacketConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/PacketConverter.scala index b772e0f4f..e4f5ca73f 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/PacketConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/PacketConverter.scala @@ -17,6 +17,8 @@ sealed trait PacketConverter * @tparam A the type of game object */ abstract class ObjectCreateConverter[A <: PlanetSideGameObject] extends PacketConverter { + /** some objects do not have a detailed constructor data form */ + def noDetailedForm(obj: A): Boolean = DetailedConstructorData(obj).isFailure /** * Take a game object and transform it into its equivalent data for an `0x17` packet. diff --git a/src/main/scala/net/psforever/objects/definition/converter/RadiationCloudConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/RadiationCloudConverter.scala new file mode 100644 index 000000000..2a7d1a554 --- /dev/null +++ b/src/main/scala/net/psforever/objects/definition/converter/RadiationCloudConverter.scala @@ -0,0 +1,16 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.ballistics.Projectile +import net.psforever.packet.game.objectcreate._ + +import scala.util.{Failure, Success, Try} + +class RadiationCloudConverter extends ObjectCreateConverter[Projectile]() { + override def ConstructorData(obj: Projectile): Try[RadiationCloudData] = { + Success(RadiationCloudData(PlacementData(obj.Position, obj.Orientation), obj.owner.Faction)) + } + + override def DetailedConstructorData(obj: Projectile): Try[RadiationCloudData] = + Failure(new Exception("RadiationCloudConverter should not be used to generate detailed RadiationCloudData (nothing should)")) +} diff --git a/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala index a7a23f7b5..dfa7d5b20 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/SeatConverter.scala @@ -3,11 +3,11 @@ package net.psforever.objects.definition.converter import net.psforever.objects.Player import net.psforever.objects.serverobject.mount.Seat -import net.psforever.packet.game.objectcreate.{InventoryItemData, ObjectClass, PlayerData, VehicleData} +import net.psforever.packet.game.objectcreate._ object SeatConverter { def MakeSeat(player: Player, offset: Long): PlayerData = { - VehicleData.PlayerData( + MountableInventory.PlayerData( AvatarConverter.MakeAppearanceData(player), AvatarConverter.MakeCharacterData(player), AvatarConverter.MakeInventoryData(player), diff --git a/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala index 8ee353dbb..3a3665bc8 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -75,7 +75,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { } private def MakeDriverSeat(obj: Vehicle): List[InventoryItemData.InventoryItem] = { - val offset: Long = VehicleData.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier) + val offset: Long = MountableInventory.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier) obj.Seats(0).occupant match { case Some(player) => List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, SeatConverter.MakeSeat(player, offset))) diff --git a/src/main/scala/net/psforever/objects/equipment/ArmorSiphonBehavior.scala b/src/main/scala/net/psforever/objects/equipment/ArmorSiphonBehavior.scala new file mode 100644 index 000000000..b465a882c --- /dev/null +++ b/src/main/scala/net/psforever/objects/equipment/ArmorSiphonBehavior.scala @@ -0,0 +1,156 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.equipment + +import akka.actor.{Actor, Cancellable} +import net.psforever.objects.ballistics.VehicleSource +import net.psforever.objects.{GlobalDefinitions, Tool, Vehicle} +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.damage.Damageable +import net.psforever.objects.vital.RepairFromArmorSiphon +import net.psforever.objects.vital.etc.{ArmorSiphonModifiers, ArmorSiphonReason} +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.packet.game.QuantityUpdateMessage +import net.psforever.services.Service +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types.PlanetSideGUID + +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +object ArmorSiphonBehavior { + sealed case class RepairedByArmorSiphon(cause: DamageInteraction, amount: Int) + + sealed case class Recharge(guid: PlanetSideGUID) + + trait Target { + _: Actor with Damageable => + def SiphonableObject: Vehicle + + val siphoningBehavior: Receive = { + case CommonMessages.Use(player, Some(item : Tool)) + if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) && player.Faction != DamageableObject.Faction => + val obj = SiphonableObject + val zone = obj.Zone + val iguid = item.GUID + //see Damageable.takesDamage + zone.Vehicles.find { v => + v.Weapons.values.exists { slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == iguid} + } match { + case Some(v: Vehicle) if v.CanDamage => + //remember: we are the vehicle being siphoned; we need the vehicle doing the siphoning + val before = item.Magazine + val after = item.Discharge() + if (before > after) { + v.Actor ! ArmorSiphonBehavior.Recharge(iguid) + PerformDamage( + obj, + DamageInteraction( + VehicleSource(obj), + ArmorSiphonReason(v, item, obj.DamageModel), + obj.Position + ).calculate() + ) + } + case _ => ; + } + } + } + + trait SiphonOwner { + _: Actor => + def SiphoningObject: Vehicle + + private val siphonRecharge: mutable.HashMap[PlanetSideGUID, Cancellable] = mutable.HashMap[PlanetSideGUID, Cancellable]() + + def repairPostStop(): Unit = { + siphonRecharge.keys.foreach { endSiphonRecharge } + } + + val siphonRepairBehavior: Receive = { + case RepairedByArmorSiphon(cause, amount) => + val obj = SiphoningObject + val before = obj.Health + cause.cause match { + case asr: ArmorSiphonReason + if before < obj.MaxHealth => + val after = obj.Health += amount + if(before < after) { + obj.History(RepairFromArmorSiphon(asr.siphon.Definition, before - after)) + val zone = obj.Zone + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, obj.GUID, 0, after) + ) + } + case _ => ; + } + + case ArmorSiphonBehavior.Recharge(guid) => + siphonRecharge.remove(guid) match { + case Some(timer) => timer.cancel() + case None => ; + } + val obj = SiphoningObject + obj.Weapons.values.find { slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid } match { + case Some(siphonSlot) => + val siphon = siphonSlot.Equipment.get.asInstanceOf[Tool] + val zone = obj.Zone + //update current charge level + zone.VehicleEvents ! VehicleServiceMessage( + obj.Actor.toString, + VehicleAction.SendResponse(Service.defaultPlayerGUID, QuantityUpdateMessage(siphon.AmmoSlot.Box.GUID, siphon.Magazine)) + ) + siphonRecharge.put(guid, context.system.scheduler.scheduleWithFixedDelay( + initialDelay = 3000 milliseconds, + delay = 200 milliseconds, + self, + SiphonOwner.Recharge(guid) + )) + case _ => ; + } + + case SiphonOwner.Recharge(guid) => + val obj = SiphoningObject + val zone = obj.Zone + obj.Weapons.values.find { slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid } match { + case Some(slot: EquipmentSlot) => + val siphon = slot.Equipment.get.asInstanceOf[Tool] + val before = siphon.Magazine + val after = siphon.Magazine = before + 1 + if (after > before) { + zone.VehicleEvents ! VehicleServiceMessage( + obj.Actor.toString, + VehicleAction.SendResponse(Service.defaultPlayerGUID, QuantityUpdateMessage(siphon.AmmoSlot.Box.GUID, after)) + ) + if (after == siphon.MaxMagazine) { + endSiphonRecharge(guid) + } + } + + case _ => + endSiphonRecharge(guid) + } + } + + def endSiphonRecharge(guid: PlanetSideGUID): Unit = { + siphonRecharge.remove(guid) match { + case Some(c) => c.cancel() + case None => ; + } + } + } + + object SiphonOwner { + private case class Recharge(guid: PlanetSideGUID) + } +} + +case object ArmorSiphonRepairHost extends ArmorSiphonModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: ArmorSiphonReason): Int = { + if (damage > 0) { + cause.hostVehicle.Actor ! ArmorSiphonBehavior.RepairedByArmorSiphon(data, damage) + } + damage + } +} diff --git a/src/main/scala/net/psforever/objects/equipment/EquipmentHandiness.scala b/src/main/scala/net/psforever/objects/equipment/EquipmentHandiness.scala new file mode 100644 index 000000000..025ff69e2 --- /dev/null +++ b/src/main/scala/net/psforever/objects/equipment/EquipmentHandiness.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.equipment + +import net.psforever.objects.definition.EquipmentDefinition +import enumeratum.values.{StringEnum, StringEnumEntry} + +sealed abstract class Hand(val value: String) extends StringEnumEntry + +object Handiness extends StringEnum[Hand] { + val values = findValues + + case object Generic extends Hand(value = "Generic") + case object Left extends Hand(value = "Left") + case object Right extends Hand(value = "Right") +} + +final case class EquipmentHandiness( + generic: EquipmentDefinition, + left: EquipmentDefinition, + right: EquipmentDefinition + ) { + def transform(handiness: Hand): EquipmentDefinition = { + handiness match { + case Handiness.Generic => generic + case Handiness.Left => left + case Handiness.Right => right + } + } + + def contains(findDef: EquipmentDefinition): Boolean = { + generic == findDef || left == findDef || right == findDef + } +} diff --git a/src/main/scala/net/psforever/objects/equipment/FireModeDefinition.scala b/src/main/scala/net/psforever/objects/equipment/FireModeDefinition.scala index 36b14ba4c..b4f50cd3b 100644 --- a/src/main/scala/net/psforever/objects/equipment/FireModeDefinition.scala +++ b/src/main/scala/net/psforever/objects/equipment/FireModeDefinition.scala @@ -25,6 +25,7 @@ class FireModeDefinition extends DamageModifiers { /** how many rounds are replenished each reload cycle */ private var magazine: Int = 1 + private var defaultMagazine: Option[Int] = None /** how many rounds are replenished each reload cycle, per type of ammunition loaded * key - ammo type index, value - magazine capacity @@ -63,6 +64,15 @@ class FireModeDefinition extends DamageModifiers { projectileTypeIndices += index } + def DefaultMagazine: Int = defaultMagazine.getOrElse(magazine) + + def DefaultMagazine_=(inMagazine: Int): Int = DefaultMagazine_=(Some(inMagazine)) + + def DefaultMagazine_=(inMagazine: Option[Int]): Int = { + defaultMagazine = inMagazine + DefaultMagazine + } + def Magazine: Int = magazine def Magazine_=(inMagazine: Int): Int = { diff --git a/src/main/scala/net/psforever/objects/equipment/JammingUnit.scala b/src/main/scala/net/psforever/objects/equipment/JammingUnit.scala index a52e1fcd0..dfd1d6014 100644 --- a/src/main/scala/net/psforever/objects/equipment/JammingUnit.scala +++ b/src/main/scala/net/psforever/objects/equipment/JammingUnit.scala @@ -7,7 +7,7 @@ import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.vehicles.MountedWeapons import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.projectile.ProjectileReason -import net.psforever.objects.zones.ZoneAware +import net.psforever.objects.zones.{Zone, ZoneAware} import net.psforever.types.Vector3 import net.psforever.services.Service import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} @@ -254,7 +254,7 @@ trait JammableMountedWeapons extends JammableBehavior { override def StartJammeredStatus(target: Any, dur: Int): Unit = { target match { case obj: PlanetSideServerObject with MountedWeapons with JammableUnit if !obj.Jammed => - JammableMountedWeapons.JammeredStatus(obj, 1) + JammableMountedWeaponsJammeredStatus(obj, statusCode = 1) super.StartJammeredStatus(target, dur) case _ => ; } @@ -275,11 +275,15 @@ trait JammableMountedWeapons extends JammableBehavior { override def CancelJammeredStatus(target: Any): Unit = { target match { case obj: PlanetSideServerObject with MountedWeapons with JammableUnit if obj.Jammed => - JammableMountedWeapons.JammeredStatus(obj, 0) + JammableMountedWeaponsJammeredStatus(obj, statusCode = 0) case _ => ; } super.CancelJammeredStatus(target) } + + def JammableMountedWeaponsJammeredStatus(target: PlanetSideServerObject with MountedWeapons, statusCode: Int): Unit = { + JammableMountedWeapons.JammeredStatus(target, statusCode) + } } object JammableMountedWeapons { @@ -292,17 +296,20 @@ object JammableMountedWeapons { * 1 for activation */ def JammeredStatus(target: PlanetSideServerObject with MountedWeapons, statusCode: Int): Unit = { - val zone = target.Zone - val zoneId = zone.id + val zone = target.Zone target.Weapons.values .map { _.Equipment } .collect { case Some(item: Tool) => - item.Jammed = statusCode == 1 - zone.VehicleEvents ! VehicleServiceMessage( - zoneId, - VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, item.GUID, 27, statusCode) - ) + JammedWeaponStatus(zone, item, statusCode) } } + + def JammedWeaponStatus(zone: Zone, target: Equipment with JammableUnit, statusCode: Int): Unit = { + target.Jammed = statusCode == 1 + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 27, statusCode) + ) + } } diff --git a/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala index 5586c3de6..382ad578c 100644 --- a/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala +++ b/src/main/scala/net/psforever/objects/geometry/GeometryForm.scala @@ -1,7 +1,7 @@ // Copyright (c) 2021 PSForever package net.psforever.objects.geometry -import net.psforever.objects.ballistics.{PlayerSource, SourceEntry} +import net.psforever.objects.ballistics.{PlayerSource, Projectile, SourceEntry} import net.psforever.objects.geometry.d3._ import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player} import net.psforever.types.{ExoSuitType, Vector3} @@ -80,6 +80,22 @@ object GeometryForm { } } + /** + * The geometric representation is a sphere around the entity's centroid + * positioned following the axis of rotation (the entity's base). + * The specific entity should be a projectile, else the result is invalid. + * @param o the entity + * @return the representation + */ + def representProjectileBySphere()(o: Any): VolumetricGeometry = { + o match { + case p: Projectile => + Sphere(p.Position, p.Definition.DamageRadius) + case _ => + invalidPoint + } + } + /** * The geometric representation is a cylinder around the entity's base. * @param radius half the distance across diff --git a/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala b/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala index f4f425e24..9b39df6d6 100644 --- a/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala +++ b/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala @@ -78,7 +78,7 @@ class NumberPoolHub(private val source: NumberSource) { case Nil => ; case collisions => throw new IllegalArgumentException( - s"can not add pool $name - it contains the following redundant numbers: ${collisions.mkString(",")}" + s"can not add pool $name - it contains the following redundant numbers: ${collisions.sorted.mkString(",")}" ) } pool.foreach(i => bigpool += i.toLong -> name) diff --git a/src/main/scala/net/psforever/objects/inventory/Container.scala b/src/main/scala/net/psforever/objects/inventory/Container.scala index 1dacfdeb7..e4751e5fe 100644 --- a/src/main/scala/net/psforever/objects/inventory/Container.scala +++ b/src/main/scala/net/psforever/objects/inventory/Container.scala @@ -76,6 +76,15 @@ trait Container { } } + /** + * When the slot reported is not the slot requested, change the slot. + * @param slot the original slot index + * @return the modified slot index + */ + def SlotMapResolution(slot: Int): Int = { + slot + } + /** * Given a region of "searchable unit positions" considered as stowable, * determine if any previously stowed items are contained within that region.
diff --git a/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala b/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala index 290b48871..f3a8b3c76 100644 --- a/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala +++ b/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala @@ -22,20 +22,23 @@ object InventoryTile { final val Tile11 = InventoryTile(1, 1) //occasional placeholder final val Tile22 = InventoryTile(2, 2) //grenades, boomer trigger final val Tile23 = InventoryTile(2, 3) //canister ammo - final val Tile42 = InventoryTile(4, 2) //medkit final val Tile33 = InventoryTile(3, 3) //ammo box, pistols, ace + final val Tile42 = InventoryTile(4, 2) //medkit final val Tile44 = InventoryTile(4, 4) //large ammo box final val Tile55 = InventoryTile(5, 5) //bfr ammo box final val Tile66 = InventoryTile(6, 6) //infiltration suit inventory final val Tile63 = InventoryTile(6, 3) //rifles final val Tile93 = InventoryTile(9, 3) //long-body weapons + final val Tile84 = InventoryTile(8, 4) //bfr arm weapons final val Tile96 = InventoryTile(9, 6) //standard exo-suit inventory final val Tile99 = InventoryTile(9, 9) //agile exo-suit inventory + final val Tile1004 = InventoryTile(10, 4) //bfr gunner weapons final val Tile1107 = InventoryTile(11, 7) //uncommon small trunk capacity - phantasm final val Tile1111 = InventoryTile(11, 11) //common small trunk capacity final val Tile1209 = InventoryTile(12, 9) //reinforced exo-suit inventory final val Tile1511 = InventoryTile(15, 11) //common medium trunk capacity final val Tile1515 = InventoryTile(15, 15) //common large trunk capacity + final val Tile1518 = InventoryTile(15, 18) //gunner bfr trunk capacity final val Tile1611 = InventoryTile(16, 11) //uncommon medium trunk capacity - vulture final val Tile1612 = InventoryTile(16, 12) //MAX; uncommon medium trunk capacity - lodestar final val Tile1816 = InventoryTile(18, 16) //uncommon massive trunk capacity - galaxy_gunship diff --git a/src/main/scala/net/psforever/objects/loadouts/Loadout.scala b/src/main/scala/net/psforever/objects/loadouts/Loadout.scala index f5a735ad0..f488f6e19 100644 --- a/src/main/scala/net/psforever/objects/loadouts/Loadout.scala +++ b/src/main/scala/net/psforever/objects/loadouts/Loadout.scala @@ -51,12 +51,13 @@ object Loadout { * @return a `VehicleLoadout` object populated with appropriate information about the current state of the vehicle */ def Create(vehicle: Vehicle, label: String): Loadout = { + val (_, entries: List[Loadout.SimplifiedEntry]) = vehicle.Weapons.collect { + case (index, slot: EquipmentSlot) if slot.Equipment.nonEmpty => + (index, SimplifiedEntry(buildSimplification(slot.Equipment.get), index)) + }.unzip VehicleLoadout( label, - packageSimplifications(vehicle.Weapons.collect { - case (index, slot) if slot.Equipment.nonEmpty => - InventoryItem(slot.Equipment.get, index) - }.toList), + entries, packageSimplifications(vehicle.Trunk.Items), vehicle.Definition ) diff --git a/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala b/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala index cf07fb489..defacfde7 100644 --- a/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala +++ b/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala @@ -1,6 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.loadouts +import net.psforever.objects.GlobalDefinitions import net.psforever.objects.definition._ /** @@ -27,3 +28,24 @@ final case class VehicleLoadout( inventory: List[Loadout.SimplifiedEntry], vehicle_definition: VehicleDefinition ) extends EquipmentLoadout(label, visible_slots, inventory) + +object VehicleLoadout { + /** + * The variant of the battleframe vehicle. + * Why these numbers map to the specific type of battleframe is a mystery. + * @see `FavoritesMessage` + * @param definition the vehicle's definition + * @return a number directly indicative of the type + */ + def DetermineBattleframeSubtype(definition: VehicleDefinition): Int = { + definition match { + case GlobalDefinitions.aphelion_flight => 1 + case GlobalDefinitions.aphelion_gunner => 2 + case GlobalDefinitions.colossus_flight => 4 + case GlobalDefinitions.colossus_gunner => 5 + case GlobalDefinitions.peregrine_flight => 7 + case GlobalDefinitions.peregrine_gunner => 8 + case _ => 0 + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/ServerObjectControl.scala b/src/main/scala/net/psforever/objects/serverobject/ServerObjectControl.scala new file mode 100644 index 000000000..070307461 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/ServerObjectControl.scala @@ -0,0 +1,37 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.serverobject + +import akka.actor.Actor +import net.psforever.types.PlanetSideGUID + +abstract class ServerObjectControl + extends Actor { + protected val log = org.log4s.getLogger(toString()) + + val attributeBehavior: Receive = { + case ServerObject.AttributeMsg(attribute, value, other) => + parseAttribute(attribute, value, other) + + case ServerObject.GenericObjectAction(guid, action, other) => + parseObjectAction(guid, action, other) + + case ServerObject.GenericAction(guid, action, other) => + parseGenericAction(guid, action, other) + } + + def parseAttribute(attribute: Int, value: Long, other: Option[Any]): Unit + + def parseGenericAction(guid: PlanetSideGUID, action: Int, other: Option[Any]): Unit = { /*intentionally blank*/ } + + def parseObjectAction(guid: PlanetSideGUID, action: Int, other: Option[Any]): Unit = { /*intentionally blank*/ } +} + +object ServerObject { + final case class AttributeMsg(attribute: Int, value: Long, other: Option[Any] = None) + + final case class GenericAction(guid: PlanetSideGUID, action: Int, other: Option[Any] = None) + + final case class GenericObjectAction(guid: PlanetSideGUID, action: Int, other: Option[Any] = None) + + final case class StateChangeDenied(original: Any, msg: String) +} diff --git a/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala index 5e8bbbfc9..a08e338e1 100644 --- a/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala @@ -4,11 +4,11 @@ package net.psforever.objects.serverobject.containable import akka.actor.{Actor, ActorRef} import akka.pattern.{AskTimeoutException, ask} import akka.util.Timeout -import net.psforever.objects.equipment.Equipment +import net.psforever.objects.equipment.{Equipment, EquipmentSize} import net.psforever.objects.inventory.{Container, InventoryItem} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.zones.Zone -import net.psforever.objects.{BoomerTrigger, GlobalDefinitions, Player} +import net.psforever.objects._ import net.psforever.types.{PlanetSideEmpire, Vector3} import scala.concurrent.ExecutionContext.Implicits.global @@ -92,58 +92,7 @@ trait ContainableBehavior { case msg @ Containable.MoveItem(destination, equipment, destSlot) => /* can be deferred */ - if (ContainableBehavior.TestPutItemInSlot(destination, equipment, destSlot).nonEmpty) { //test early, before we try to move the item - val source = ContainerObject - val item = equipment - val dest = destSlot - LocalRemoveItemFromSlot(item) match { - case Containable.ItemFromSlot(_, Some(_), slot @ Some(originalSlot)) => - if (source eq destination) { - //when source and destination are the same, moving the item can be performed in one pass - LocalPutItemInSlot(item, dest) match { - case Containable.ItemPutInSlot(_, _, _, None) => ; //success - case Containable.ItemPutInSlot(_, _, _, Some(swapItem)) => //success, but with swap item - LocalPutItemInSlotOnlyOrAway(swapItem, slot) match { - case Containable.ItemPutInSlot(_, _, _, None) => ; - case _ => - source.Zone.Ground.tell( - Zone.Ground.DropItem(swapItem, source.Position, Vector3.z(source.Orientation.z)), - source.Actor - ) //drop it - } - case _: Containable.CanNotPutItemInSlot => //failure case ; try restore original item placement - LocalPutItemInSlot(item, originalSlot) - } - } else { - //destination sync - destination.Actor ! ContainableBehavior.Wait() - implicit val timeout = new Timeout(1000 milliseconds) - val moveItemOver = ask(destination.Actor, ContainableBehavior.MoveItemPutItemInSlot(item, dest)) - moveItemOver.onComplete { - case Success(Containable.ItemPutInSlot(_, _, _, None)) => ; //successful - - case Success(Containable.ItemPutInSlot(_, _, _, Some(swapItem))) => //successful, but with swap item - PutItBackOrDropIt(source, swapItem, slot, destination.Actor) - - case Success(_: Containable.CanNotPutItemInSlot) => //failure case ; try restore original item placement - PutItBackOrDropIt(source, item, slot, source.Actor) - - case Failure(_) => //failure case ; try restore original item placement - PutItBackOrDropIt(source, item, slot, source.Actor) - - case _ => ; //TODO what? - } - //always do this - moveItemOver - .recover { case _: AskTimeoutException => destination.Actor ! ContainableBehavior.Resume() } - .onComplete { _ => destination.Actor ! ContainableBehavior.Resume() } - } - case _ => ; - //we could not find the item to be moved in the source location; trying to act on old data? - } - } else { - MessageDeferredCallback(msg) - } + ContainableMoveItem(destination, equipment, destSlot, msg) case ContainableBehavior.MoveItemPutItemInSlot(item, dest) => sender() ! LocalPutItemInSlot(item, dest) @@ -188,6 +137,66 @@ trait ContainableBehavior { /* Functions (item transfer) */ + protected def ContainableMoveItem( + destination: PlanetSideServerObject with Container, + equipment: Equipment, + destSlot: Int, + msg: Any + ) : Unit = { + if (ContainableBehavior.TestPutItemInSlot(destination, equipment, destSlot).nonEmpty) { //test early, before we try to move the item + val source = ContainerObject + val item = equipment + val dest = destSlot + LocalRemoveItemFromSlot(item) match { + case Containable.ItemFromSlot(_, Some(_), slot @ Some(originalSlot)) => + if (source eq destination) { + //when source and destination are the same, moving the item can be performed in one pass + LocalPutItemInSlot(item, dest) match { + case Containable.ItemPutInSlot(_, _, _, None) => ; //success + case Containable.ItemPutInSlot(_, _, _, Some(swapItem)) => //success, but with swap item + LocalPutItemInSlotOnlyOrAway(swapItem, slot) match { + case Containable.ItemPutInSlot(_, _, _, None) => ; + case _ => + source.Zone.Ground.tell( + Zone.Ground.DropItem(swapItem, source.Position, Vector3.z(source.Orientation.z)), + source.Actor + ) //drop it + } + case _: Containable.CanNotPutItemInSlot => //failure case ; try restore original item placement + LocalPutItemInSlot(item, originalSlot) + } + } else { + //destination sync + destination.Actor ! ContainableBehavior.Wait() + implicit val timeout = new Timeout(1000 milliseconds) + val moveItemOver = ask(destination.Actor, ContainableBehavior.MoveItemPutItemInSlot(item, dest)) + moveItemOver.onComplete { + case Success(Containable.ItemPutInSlot(_, _, _, None)) => ; //successful + + case Success(Containable.ItemPutInSlot(_, _, _, Some(swapItem))) => //successful, but with swap item + PutItBackOrDropIt(source, swapItem, slot, destination.Actor) + + case Success(_: Containable.CanNotPutItemInSlot) => //failure case ; try restore original item placement + PutItBackOrDropIt(source, item, slot, source.Actor) + + case Failure(_) => //failure case ; try restore original item placement + PutItBackOrDropIt(source, item, slot, source.Actor) + + case _ => ; //TODO what? + } + //always do this + moveItemOver + .recover { case _: AskTimeoutException => destination.Actor ! ContainableBehavior.Resume() } + .onComplete { _ => destination.Actor ! ContainableBehavior.Resume() } + } + case _ => ; + //we could not find the item to be moved in the source location; trying to act on old data? + } + } else { + MessageDeferredCallback(msg) + } + } + private def LocalRemoveItemFromSlot(slot: Int): Any = { val source = ContainerObject val (outSlot, item) = ContainableBehavior.TryRemoveItemFromSlot(source, slot) @@ -370,14 +379,15 @@ object ContainableBehavior { item: Equipment ): (Option[Int], Option[Equipment]) = { source.Find(item) match { - case slot @ Some(index) => + case slot @ Some(index) + if ContainableBehavior.PermitEquipmentExtract(source, item, index)=> source.Slot(index).Equipment = None if (source.Slot(index).Equipment.isEmpty) { (slot, Some(item)) } else { (None, None) } - case None => + case _ => (None, None) } } @@ -399,14 +409,14 @@ object ContainableBehavior { source: PlanetSideServerObject with Container, slot: Int ): (Option[Int], Option[Equipment]) = { - val (item, outSlot) = source.Slot(slot).Equipment match { - case Some(thing) => (Some(thing), source.Find(thing)) - case None => (None, None) - } - source.Slot(slot).Equipment = None - item match { - case Some(_) if item.nonEmpty && source.Slot(slot).Equipment.isEmpty => - (outSlot, item) + source.Slot(slot).Equipment match { + case Some(thing) + if ContainableBehavior.PermitEquipmentExtract(source, thing, slot) => + if ((source.Slot(slot).Equipment = None).isEmpty) { + (Some(slot), Some(thing)) + } else { + (None, None) + } case _ => (None, None) } @@ -431,7 +441,7 @@ object ContainableBehavior { item: Equipment, dest: Int ): Option[List[InventoryItem]] = { - if (ContainableBehavior.PermitEquipmentStow(destination, item)) { + if (ContainableBehavior.PermitEquipmentStow(destination, item, dest)) { val tile = item.Definition.Tile val destinationCollisionTest = destination.Collisions(dest, tile.Width, tile.Height) destinationCollisionTest match { @@ -505,7 +515,7 @@ object ContainableBehavior { def TryPutItemAway(destination: PlanetSideServerObject with Container, item: Equipment): Option[Int] = { destination.Fit(item) match { case out @ Some(dest) - if ContainableBehavior.PermitEquipmentStow(destination, item) && (destination.Slot(dest).Equipment = item) + if ContainableBehavior.PermitEquipmentStow(destination, item, dest) && (destination.Slot(dest).Equipment = item) .contains(item) => out case _ => @@ -572,22 +582,67 @@ object ContainableBehavior { } } + //TODO convert PermitEquipmentExtract and PermitEquipmentStow into instance methods? + /** + * Apply incontestable, arbitrary limitations + * whereby certain items are denied removal from certain containers + * for vaguely documented but assuredly fantastic excuses on the part of the developer. + * @see `ContainableBehavior.PermitEquipmentStow` + * @param source the container + * @param equipment the item to be removed + * @param slot where the equipment can be found + * @return `true`, if the type of equipment object is allowed to be removed from the containing entity; + * `false`, otherwise + */ + def PermitEquipmentExtract( + source: PlanetSideServerObject with Container, + equipment: Equipment, + slot: Int + ): Boolean = { + source match { + case v: Vehicle if v.VisibleSlots.contains(slot) => + //can not remove equipment slot items if vehicle is jammed + //applies mostly to BFR's, but we do not need to filter + !v.Jammed + case _ => + true + } + } + /** * Apply incontestable, arbitrary limitations * whereby certain items are denied insertion into certain containers * for vaguely documented but assuredly fantastic excuses on the part of the developer. + * @see `ContainableBehavior.PermitEquipmentExtract` * @param destination the container * @param equipment the item to be inserted * @return `true`, if the object is allowed to contain the type of equipment object; * `false`, otherwise */ - def PermitEquipmentStow(destination: PlanetSideServerObject with Container, equipment: Equipment): Boolean = { + def PermitEquipmentStow( + destination: PlanetSideServerObject with Container, + equipment: Equipment, + dest: Int + ): Boolean = { import net.psforever.objects.{BoomerTrigger, Player} equipment match { case _: BoomerTrigger => //a BoomerTrigger can only be stowed in a player's holsters or inventory //this is only a requirement until they, and their Boomer explosive complement, are cleaned-up properly destination.isInstanceOf[Player] + case weapon: Tool + if weapon.Size == EquipmentSize.BFRArmWeapon || weapon.Size == EquipmentSize.BFRGunnerWeapon => + //Battleframe weaponry must be placed in an appropriate equipment mount spot, or held in the player's free hand + //if in the vehicle slots, then the vehicle must not be jammed + destination match { + case v: Vehicle + if GlobalDefinitions.isBattleFrameVehicle(v.Definition) => + v.VisibleSlots.contains(dest) && !v.Jammed + case _: Player => + dest == Player.FreeHandSlot + case _ => + false + } case _ => true } diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala index b38056602..95134cdf2 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -1,7 +1,7 @@ //Copyright (c) 2020 PSForever package net.psforever.objects.serverobject.damage -import akka.actor.Actor +import akka.actor.{Actor, Cancellable} import net.psforever.objects.{Vehicle, Vehicles} import net.psforever.objects.equipment.JammableUnit import net.psforever.objects.serverobject.damage.Damageable.Target @@ -30,6 +30,8 @@ trait DamageableVehicle /** whether or not the vehicle has been damaged directly, report that damage has occurred */ protected var reportDamageToVehicle: Boolean = false + /** when the vehicle is destroyed, its major explosion is delayed */ + protected var queuedDestruction: Option[Cancellable] = None def DamageableObject: Vehicle def AggravatedObject : Vehicle = DamageableObject @@ -44,6 +46,7 @@ trait DamageableVehicle case DamageableVehicle.Destruction(cause) => //cargo vehicles are destroyed when carrier is destroyed + //bfrs undergo a shiver spell before exploding val obj = DamageableObject obj.Health = 0 obj.History(cause) @@ -59,24 +62,28 @@ trait DamageableVehicle target: Damageable.Target, applyDamageTo: ResolutionCalculations.Output ): Unit = { - val obj = DamageableObject - val originalHealth = obj.Health - val originalShields = obj.Shields - val cause = applyDamageTo(obj) - val health = obj.Health - val shields = obj.Shields - val damageToHealth = originalHealth - health - val damageToShields = originalShields - shields - if (WillAffectTarget(target, damageToHealth + damageToShields, cause)) { - target.History(cause) - DamageLog( - target, - s"BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields" - ) - HandleDamage(target, cause, (damageToHealth, damageToShields)) - } else { - obj.Health = originalHealth - obj.Shields = originalShields + queuedDestruction match { + case Some(_) => ; + case None => + val obj = DamageableObject + val originalHealth = obj.Health + val originalShields = obj.Shields + val cause = applyDamageTo(obj) + val health = obj.Health + val shields = obj.Shields + val damageToHealth = originalHealth - health + val damageToShields = originalShields - shields + if (WillAffectTarget(target, damageToHealth + damageToShields, cause)) { + target.History(cause) + DamageLog( + target, + s"BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields" + ) + HandleDamage(target, cause, (damageToHealth, damageToShields)) + } else { + obj.Health = originalHealth + obj.Shields = originalShields + } } } @@ -127,7 +134,7 @@ trait DamageableVehicle if (damageToShields > 0) { events ! VehicleServiceMessage( vehicleChannel, - VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 68, obj.Shields) + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, obj.Definition.shieldUiAttribute, obj.Shields) ) announceConfrontation = true } @@ -175,29 +182,60 @@ trait DamageableVehicle * @param cause historical information about the damage */ override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = { - super.DestructionAwareness(target, cause) - val obj = DamageableObject - val zone = target.Zone - //aggravation cancel - EndAllAggravation() - //passengers die with us - DamageableMountable.DestructionAwareness(obj, cause) - //things positioned around us can get hurt from us - Zone.serverSideDamage(obj.Zone, target, Zone.explosionDamage(Some(cause))) - //special considerations for certain vehicles - Vehicles.BeforeUnloadVehicle(obj, zone) - //shields - if (obj.Shields > 0) { - obj.Shields = 0 - zone.VehicleEvents ! VehicleServiceMessage( - zone.id, - VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 68, 0) - ) + (queuedDestruction, DamageableObject.Definition.destructionDelay) match { + case (None, Some(delay)) => //set a future explosion for later + destructionDelayed(delay, cause) + case (Some(_), _) | (None, None) => //explode now + super.DestructionAwareness(target, cause) + val obj = DamageableObject + val zone = target.Zone + //aggravation cancel + EndAllAggravation() + //passengers die with us + DamageableMountable.DestructionAwareness(obj, cause) + Zone.serverSideDamage(obj.Zone, target, Zone.explosionDamage(Some(cause))) + //special considerations for certain vehicles + Vehicles.BeforeUnloadVehicle(obj, zone) + //shields + if (obj.Shields > 0) { + obj.Shields = 0 + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, obj.Definition.shieldUiAttribute, 0) + ) + } + //clean up + target.Actor ! Vehicle.Deconstruct(Some(1 minute)) + target.ClearHistory() + DamageableWeaponTurret.DestructionAwareness(obj, cause) + case _ => ; } - //clean up - target.Actor ! Vehicle.Deconstruct(Some(1 minute)) - target.ClearHistory() - DamageableWeaponTurret.DestructionAwareness(obj, cause) + } + + def destructionDelayed(delay: Long, cause: DamageResult): Unit = { + import scala.concurrent.ExecutionContext.Implicits.global + import scala.concurrent.duration._ + val obj = DamageableObject + //health to 1, shields to 0 + obj.Health = 1 + obj.Shields = 0 + val guid = obj.GUID + val guid0 = Service.defaultPlayerGUID + val zone = obj.Zone + val zoneid = zone.id + val events = zone.VehicleEvents + events ! VehicleServiceMessage( + zoneid, + VehicleAction.PlanetsideAttribute(guid0, guid, 0, 1) + ) + events ! VehicleServiceMessage( + zoneid, + VehicleAction.PlanetsideAttribute(guid0, guid, obj.Definition.shieldUiAttribute, 0) + ) + //passengers die with us + DamageableMountable.DestructionAwareness(DamageableObject, cause) + //come back to this death later + queuedDestruction = Some(context.system.scheduler.scheduleOnce(delay milliseconds, self, DamageableVehicle.Destruction(cause))) } } diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala index 7b1752f6c..ae024daa6 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/InteractWithEnvironment.scala @@ -4,7 +4,9 @@ package net.psforever.objects.serverobject.environment import net.psforever.objects.GlobalDefinitions import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.zones._ -import net.psforever.objects.zones.blockmap.BlockMapEntity +import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation} + +case object EnvironmentInteraction extends ZoneInteractionType /** * This game entity may infrequently test whether it may interact with game world environment. @@ -14,6 +16,10 @@ class InteractWithEnvironment() private var interactingWithEnvironment: (PlanetSideServerObject, Boolean) => Any = InteractWithEnvironment.onStableEnvironment() + def Type = EnvironmentInteraction + + def range: Float = 0f + /** * The method by which zone interactions are tested or a current interaction maintained. * Utilize a function literal that, when called, returns a function literal of the same type; @@ -24,8 +30,10 @@ class InteractWithEnvironment() * @see `InteractsWithEnvironment.blockedFromInteracting` * @see `InteractsWithEnvironment.onStableEnvironment` * @see `InteractsWithEnvironment.awaitOngoingInteraction` + * @param sector the portion of the block map being tested + * @param target the fixed element in this test */ - def interaction(target: InteractsWithZone): Unit = { + def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { interactingWithEnvironment = interactingWithEnvironment(target, true) .asInstanceOf[(PlanetSideServerObject, Boolean) => Any] } diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountRestriction.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountRestriction.scala index be674fcb0..f52bf3921 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/MountRestriction.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountRestriction.scala @@ -41,5 +41,7 @@ case object SmallCargo extends MountRestriction[Vehicle] { } case object LargeCargo extends MountRestriction[Vehicle] { - def test(target : Vehicle) : Boolean = !target.Definition.CanFly + def test(target: Vehicle): Boolean = { + GlobalDefinitions.isBattleFrameVehicle(target.Definition) || !target.Definition.CanFly + } } diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala index 447af1e08..51c4c4a3b 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala @@ -18,28 +18,16 @@ class VehicleSpawnPadDefinition(objectId: Int) extends AmenityDefinition(objectI // However, it seems these values need to be reversed to turn CCW to CW rotation (e.g. +90 to -90) private var vehicle_creation_z_orient_offset = 0f - def VehicleCreationZOffset: Float = vehicle_creation_z_offset + def VehicleCreationZOffset: Float = vehicle_creation_z_offset def VehicleCreationZOrientOffset: Float = vehicle_creation_z_orient_offset - objectId match { - case 141 => - Name = "bfr_door" - vehicle_creation_z_offset = -4.5f - vehicle_creation_z_orient_offset = 90f - case 261 => - Name = "dropship_pad_doors" - vehicle_creation_z_offset = 4.89507f - vehicle_creation_z_orient_offset = -90f - case 525 => - Name = "mb_pad_creation" - vehicle_creation_z_offset = 2.52604f - case 615 => Name = "pad_create" - case 616 => - Name = "pad_creation" - vehicle_creation_z_offset = 1.70982f - case 816 => Name = "spawnpoint_vehicle" - case 947 => Name = "vanu_vehicle_creation_pad" - case _ => throw new IllegalArgumentException("Not a valid object id with the type vehicle_creation_pad") + def VehicleCreationZOffset_=(offset: Float): Float = { + vehicle_creation_z_offset = offset + vehicle_creation_z_offset + } + def VehicleCreationZOrientOffset_=(offset: Float): Float = { + vehicle_creation_z_orient_offset = offset + vehicle_creation_z_orient_offset } /** The region surrounding a vehicle spawn pad that is cleared of damageable targets prior to a vehicle being spawned. @@ -161,17 +149,44 @@ object VehicleSpawnPadDefinition { flightVehicle: Boolean ): (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = { if (flightVehicle) { - vanuKillBox(pad.Position, radius, aboveLimit * 2) + cylinderKillBox(pad.Position, radius, aboveLimit * 2) } else { - vanuKillBox(pad.Position, radius * 1.2f, aboveLimit) + cylinderKillBox(pad.Position, radius * 1.2f, aboveLimit) } } + /** + * A function that sets up the region around a battleframe vehicle spawn chamber's doors + * to be cleared of damageable targets upon spawning of a vehicle. + * All measurements are provided in terms of distance from the middle of the door. + * Internally, the pad is referred to as `bfr_door`; + * colloquially, the pad is referred to as a "BFR shed". + * @param radius the distance from the middle of the spawn pad + * @param aboveLimit how far above the spawn pad is to be cleared + * @param pad he vehicle spawn pad in question + * @param requiredButUnused required by the function prototype + * @return a function that describes a region ahead of the battleframe vehicle spawn shed + */ + def prepareBfrShedKillBox( + radius: Float, + aboveLimit: Float + ) + ( + pad: VehicleSpawnPad, + requiredButUnused: Boolean + ): (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = { + cylinderKillBox( + Vector3(0,radius,0).Rz(pad.Orientation.z + pad.Definition.VehicleCreationZOrientOffset) + pad.Position, + radius, + aboveLimit + ) + } + /** * A function that finalizes the detection for the region around a vehicle spawn pad * to be cleared of damageable targets upon spawning of a vehicle. * All measurements are provided in terms of distance from the center of the pad. - * These pads are only found in the cavern zones and are cylindrical in shape. + * These pads are cylindrical in shape. * @param origin the center of the spawn pad * @param radius the distance from the middle of the spawn pad * @param aboveLimit how far above the spawn pad is to be cleared @@ -182,16 +197,16 @@ object VehicleSpawnPadDefinition { * @return `true`, if the two entities are near enough to each other; * `false`, otherwise */ - def vanuKillBox( - origin: Vector3, - radius: Float, - aboveLimit: Float - ) - ( - obj1: PlanetSideGameObject, - obj2: PlanetSideGameObject, - maxDistance: Float - ): Boolean = { + def cylinderKillBox( + origin: Vector3, + radius: Float, + aboveLimit: Float + ) + ( + obj1: PlanetSideGameObject, + obj2: PlanetSideGameObject, + maxDistance: Float + ): Boolean = { val dir: Vector3 = { val g2 = obj2.Definition.Geometry(obj2) val cdir = Vector3.Unit(origin - g2.center.asVector3) diff --git a/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala b/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala index 4930bff57..0b2581fbc 100644 --- a/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/painbox/PainboxControl.scala @@ -173,12 +173,12 @@ object PainboxControl { val min = Vector3( nearbyAmenities.minBy(_.Position.x).Position.x - 0.5f, nearbyAmenities.minBy(_.Position.y).Position.y - 0.5f, - nearbyAmenities.minBy(_.Position.z).Position.z + nearbyAmenities.minBy(_.Position.z).Position.z - 0.5f ) val max = Vector3( nearbyAmenities.maxBy(_.Position.x).Position.x + 0.5f, nearbyAmenities.maxBy(_.Position.y).Position.y + 0.5f, - painbox.Position.z + painbox.Position.z + 0.5f ) (min, max, Vector3.midpoint(min, max)) case _ => diff --git a/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala b/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala index 30d322520..07025bd69 100644 --- a/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala +++ b/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala @@ -266,7 +266,7 @@ trait AmenityAutoRepair import scala.concurrent.ExecutionContext.Implicits.global autoRepairTimer.cancel() autoRepairQueueTask = Some(System.currentTimeMillis() + delay) - val modifiedDrain = drain * Config.app.game.amenityAutorepairDrainRate + val modifiedDrain = drain * 2 * Config.app.game.amenityAutorepairDrainRate //doubled intentionally autoRepairTimer = if(AutoRepairObject.Owner == Building.NoBuilding) { //without an owner, auto-repair freely context.system.scheduler.scheduleOnce( diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala index fb906b76b..e989f95be 100644 --- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala @@ -7,12 +7,11 @@ import net.psforever.actors.zone.BuildingActor import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.transfer.TransferBehavior import net.psforever.objects.serverobject.structures.Building -import net.psforever.objects.{Ntu, NtuContainer, NtuStorageBehavior} +import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior} import net.psforever.types.PlanetSideEmpire import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.util.Config import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -53,16 +52,32 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) .orElse(storageBehavior) .orElse { case CommonMessages.Use(player, _) => - if (resourceSilo.Faction == PlanetSideEmpire.NEUTRAL || player.Faction == resourceSilo.Faction) { - resourceSilo.Zone.Vehicles.find(v => v.PassengerInSeat(player).contains(0)) match { - case Some(vehicle) => - context.system.scheduler.scheduleOnce( - delay = 1000 milliseconds, - vehicle.Actor, - TransferBehavior.Discharging(Ntu.Nanites) - ) - case _ => - } + val siloFaction = resourceSilo.Faction + val playerFaction = player.Faction + resourceSilo.Zone.Vehicles.find(v => v.PassengerInSeat(player).contains(0)) match { + case Some(vehicle) => + (if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) { + //bfr's discharge into friendly silos and charge from enemy and neutral silos + if (siloFaction == playerFaction) { + Some(TransferBehavior.Discharging(Ntu.Nanites)) + } else { + Some(TransferBehavior.Charging(Ntu.Nanites)) + } + } else if(siloFaction == PlanetSideEmpire.NEUTRAL || siloFaction == playerFaction) { + //ants discharge into neutral and friendly silos + Some(TransferBehavior.Discharging(Ntu.Nanites)) + } else { + None + }) match { + case Some(msg) => + context.system.scheduler.scheduleOnce( + delay = 1000 milliseconds, + vehicle.Actor, + msg + ) + case None => ; + } + case _ => ; } case ResourceSilo.LowNtuWarning(enabled: Boolean) => @@ -128,11 +143,11 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) */ def HandleNtuOffer(sender: ActorRef, src: NtuContainer): Unit = { sender ! (if (resourceSilo.NtuCapacitor < resourceSilo.MaxNtuCapacitor) { - Ntu.Request(0, resourceSilo.MaxNtuCapacitor - resourceSilo.NtuCapacitor) - } else { - StopNtuBehavior(sender) - Ntu.Request(0, 0) - }) + Ntu.Request(0, resourceSilo.MaxNtuCapacitor - resourceSilo.NtuCapacitor) + } else { + StopNtuBehavior(sender) + Ntu.Request(0, 0) + }) } /** @@ -152,7 +167,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) */ def HandleNtuRequest(sender: ActorRef, min: Float, max: Float): Unit = { val originalAmount = resourceSilo.NtuCapacitor - UpdateChargeLevel(-(min * Config.app.game.amenityAutorepairDrainRate)) + UpdateChargeLevel(-min) sender ! Ntu.Grant(resourceSilo, originalAmount - resourceSilo.NtuCapacitor) } @@ -175,15 +190,30 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) * if negative or zero, disable the animation */ def PanelAnimation(source: ActorRef, trigger: Float): Unit = { - val zone = resourceSilo.Zone - zone.VehicleEvents ! VehicleServiceMessage( - zone.id, - VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, resourceSilo.GUID, 49, if (trigger > 0) 1 else 0) - ) // panel glow & orb particles + val currentlyHas = resourceSilo.NtuCapacitor // do not let the trigger charge go to waste, but also do not let the silo be filled // attempting to return it to the source may sabotage an ongoing transfer process - val amount = math.min(resourceSilo.MaxNtuCapacitor - resourceSilo.NtuCapacitor, trigger) - UpdateChargeLevel(amount - amount*0.1f) + val amount = (if (trigger > 0) { + // panel glow & orb particles on + val zone = resourceSilo.Zone + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, resourceSilo.GUID, 49, 1) + ) + math.min(resourceSilo.MaxNtuCapacitor - currentlyHas, trigger) + } else if (trigger < 0) { + // no change to animation state + if (currentlyHas > -trigger) { trigger } else { -currentlyHas } + } else { + // panel glow & orb particles off + val zone = resourceSilo.Zone + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, resourceSilo.GUID, 49, 0) + ) + 0 + }) * 0.9f + UpdateChargeLevel(amount) } /** diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala b/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala index 75010ffaa..6d0a2472d 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/WarpGate.scala @@ -155,6 +155,10 @@ class WarpGate(name: String, building_guid: Int, map_id: Int, zone: Zone, buildi def NtuCapacitor_=(value: Float): Float = NtuCapacitor + def MaxNtuCapacitor : Float = Int.MaxValue + + override def NtuSource: Option[NtuContainer] = Some(this) + override def hasLatticeBenefit(wantedBenefit: ObjectDefinition): Boolean = false override def latticeConnectedFacilityBenefits(): Set[ObjectDefinition] = Set.empty diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala index 1a369a653..950802b77 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala @@ -208,6 +208,61 @@ object EquipmentTerminalDefinition { "flail_targeting_laser" -> MakeSimpleItem(flail_targeting_laser) ) + /** + * A `Map` of operations for producing the `Tool` `Equipment` for battleframe arm weapons. + * key - an identification string sent by the client + * value - a curried function that builds the object + */ + val bfrArmWeapons : Map[String, () => Equipment] = Map( + "aphelion_armor_siphon" -> MakeTool(aphelion_armor_siphon), + "aphelion_laser" -> MakeTool(aphelion_laser), + "aphelion_ntu_siphon" -> MakeTool(aphelion_ntu_siphon), + "aphelion_ppa" -> MakeTool(aphelion_ppa), + "aphelion_starfire" -> MakeTool(aphelion_starfire), + "colossus_armor_siphon" -> MakeTool(colossus_armor_siphon), + "colossus_burster" -> MakeTool(colossus_burster), + "colossus_chaingun" -> MakeTool(colossus_chaingun), + "colossus_ntu_siphon" -> MakeTool(colossus_ntu_siphon), + "colossus_tank_cannon" -> MakeTool(colossus_tank_cannon), + "peregrine_armor_siphon" -> MakeTool(peregrine_armor_siphon), + "peregrine_dual_machine_gun" -> MakeTool(peregrine_dual_machine_gun), + "peregrine_mechhammer" -> MakeTool(peregrine_mechhammer), + "peregrine_ntu_siphon" -> MakeTool(peregrine_ntu_siphon), + "peregrine_sparrow" -> MakeTool(peregrine_sparrow) + ) + + /** + * A `Map` of operations for producing the `Tool` `Equipment` for battleframe gunner weapons. + * key - an identification string sent by the client + * value - a curried function that builds the object + */ + val bfrGunnerWeapons : Map[String, () => Equipment] = Map( + "aphelion_immolation_cannon" -> MakeTool(aphelion_immolation_cannon), + "aphelion_plasma_rocket_pod" -> MakeTool(aphelion_plasma_rocket_pod), + "colossus_cluster_bomb_pod" -> MakeTool(colossus_cluster_bomb_pod), + "colossus_dual_100mm_cannons" -> MakeTool(colossus_dual_100mm_cannons), + "peregrine_dual_rocket_pods" -> MakeTool(peregrine_dual_rocket_pods), + "peregrine_particle_cannon" -> MakeTool(peregrine_particle_cannon) + ) + + val bfrAmmunition : Map[String, () => AmmoBox] = Map( + "aphelion_laser_ammo" -> MakeAmmoBox(aphelion_laser_ammo), + "aphelion_immolation_cannon_ammo" -> MakeAmmoBox(aphelion_immolation_cannon_ammo), + "aphelion_plasma_rocket_ammo" -> MakeAmmoBox(aphelion_plasma_rocket_ammo), + "aphelion_ppa_ammo" -> MakeAmmoBox(aphelion_ppa_ammo), + "aphelion_starfire_ammo" -> MakeAmmoBox(aphelion_starfire_ammo), + "colossus_100mm_cannon_ammo" -> MakeAmmoBox(colossus_100mm_cannon_ammo), + "colossus_burster_ammo" -> MakeAmmoBox(colossus_burster_ammo), + "colossus_cluster_bomb_ammo" -> MakeAmmoBox(colossus_cluster_bomb_ammo), + "colossus_chaingun_ammo" -> MakeAmmoBox(colossus_chaingun_ammo), + "colossus_tank_cannon_ammo" -> MakeAmmoBox(colossus_tank_cannon_ammo), + "peregrine_dual_machine_gun_ammo" -> MakeAmmoBox(peregrine_dual_machine_gun_ammo), + "peregrine_mechhammer_ammo" -> MakeAmmoBox(peregrine_mechhammer_ammo), + "peregrine_particle_cannon_ammo" -> MakeAmmoBox(peregrine_particle_cannon_ammo), + "peregrine_rocket_pod_ammo" -> MakeAmmoBox(peregrine_rocket_pod_ammo), + "peregrine_sparrow_ammo" -> MakeAmmoBox(peregrine_sparrow_ammo) + ) + /** * A single-element `Map` of the one piece of `Equipment` specific to the Router. */ diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala index 3a0dab791..474f9d09e 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala @@ -334,9 +334,9 @@ object OrderTerminalDefinition { * @see `Loadout` * @see `VehicleLoadout` */ - final case class VehicleLoadoutPage() extends LoadoutTab { + final case class VehicleLoadoutPage(lineOffset: Int) extends LoadoutTab { override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = { - player.avatar.loadouts(msg.unk1 + 10) match { + player.avatar.loadouts(msg.unk1 + lineOffset) match { case Some(loadout: VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) => val weapons = loadout.visible_slots .map(entry => { @@ -401,6 +401,43 @@ object OrderTerminalDefinition { } } + /** + * The special page used by the `bfr_terminal` to select a vehicle to be spawned + * based on the player's previous loadouts for battleframe vehicles. + * Vehicle loadouts are defined by a superfluous redefinition of the vehicle's mounted weapons + * and equipment in the trunk. + * @see `Equipment` + * @see `Loadout` + * @see `Vehicle` + * @see `VehicleLoadout` + */ + final case class BattleframeSpawnLoadoutPage(vehicles: Map[String, () => Vehicle]) extends LoadoutTab { + override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = { + player.avatar.loadouts(msg.unk1 + 15) match { + case Some(loadout: VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) => + vehicles.get(loadout.vehicle_definition.Name) match { + case Some(vehicle) => + val weapons = loadout.visible_slots.map(entry => { + InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index) + }) + val inventory = loadout.inventory.map(entry => { + InventoryItem(EquipmentTerminalDefinition.BuildSimplifiedPattern(entry.item), entry.index) + }) + Terminal.BuyVehicle(vehicle(), weapons, inventory) + case None => + Terminal.NoDeal() + } + + case _ => + Terminal.NoDeal() + } + } + + def Dispatch(sender: ActorRef, terminal: Terminal, msg: Terminal.TerminalMessage): Unit = { + sender ! msg + } + } + /** * Assemble some logic for a provided object. * @param obj an `Amenity` object; diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/VehicleTerminalDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/VehicleTerminalDefinition.scala index e02b3c64e..69bac620b 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/VehicleTerminalDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/VehicleTerminalDefinition.scala @@ -71,12 +71,12 @@ object VehicleTerminalDefinition { * value - a curried function that builds the object */ val bfrVehicles: Map[String, () => Vehicle] = Map( - // "colossus_gunner" -> (()=>Unit), - // "colossus_flight" -> (()=>Unit), - // "peregrine_gunner" -> (()=>Unit), - // "peregrine_flight" -> (()=>Unit), - // "aphelion_gunner" -> (()=>Unit), - // "aphelion_flight" -> (()=>Unit) + "colossus_gunner" -> MakeVehicle(colossus_gunner), + "colossus_flight" -> MakeVehicle(colossus_flight), + "peregrine_gunner" -> MakeVehicle(peregrine_gunner), + "peregrine_flight" -> MakeVehicle(peregrine_flight), + "aphelion_gunner" -> MakeVehicle(aphelion_gunner), + "aphelion_flight" -> MakeVehicle(aphelion_flight) ) import net.psforever.objects.loadouts.{Loadout => _Loadout} //distinguish from Terminal.Loadout message @@ -88,15 +88,18 @@ object VehicleTerminalDefinition { * value - a curried function that builds the object */ val trunk: Map[String, _Loadout] = { - val ammo_12mm = ShorthandAmmoBox(bullet_12mm, bullet_12mm.Capacity) - val ammo_15mm = ShorthandAmmoBox(bullet_15mm, bullet_15mm.Capacity) - val ammo_25mm = ShorthandAmmoBox(bullet_25mm, bullet_25mm.Capacity) - val ammo_35mm = ShorthandAmmoBox(bullet_35mm, bullet_35mm.Capacity) - val ammo_20mm = ShorthandAmmoBox(bullet_20mm, bullet_20mm.Capacity) - val ammo_75mm = ShorthandAmmoBox(bullet_75mm, bullet_75mm.Capacity) - val ammo_mortar = ShorthandAmmoBox(heavy_grenade_mortar, heavy_grenade_mortar.Capacity) - val ammo_flux = ShorthandAmmoBox(flux_cannon_thresher_battery, flux_cannon_thresher_battery.Capacity) - val ammo_bomb = ShorthandAmmoBox(liberator_bomb, liberator_bomb.Capacity) + val ammo_12mm = ShorthandAmmoBox(bullet_12mm, bullet_12mm.Capacity) + val ammo_15mm = ShorthandAmmoBox(bullet_15mm, bullet_15mm.Capacity) + val ammo_25mm = ShorthandAmmoBox(bullet_25mm, bullet_25mm.Capacity) + val ammo_35mm = ShorthandAmmoBox(bullet_35mm, bullet_35mm.Capacity) + val ammo_20mm = ShorthandAmmoBox(bullet_20mm, bullet_20mm.Capacity) + val ammo_75mm = ShorthandAmmoBox(bullet_75mm, bullet_75mm.Capacity) + val ammo_mortar = ShorthandAmmoBox(heavy_grenade_mortar, heavy_grenade_mortar.Capacity) + val ammo_flux = ShorthandAmmoBox(flux_cannon_thresher_battery, flux_cannon_thresher_battery.Capacity) + val ammo_bomb = ShorthandAmmoBox(liberator_bomb, liberator_bomb.Capacity) + val ammo_ppa = ShorthandAmmoBox(aphelion_ppa_ammo, aphelion_ppa_ammo.Capacity) + val ammo_tcannon = ShorthandAmmoBox(colossus_tank_cannon_ammo, colossus_tank_cannon_ammo.Capacity) + val ammo_mgun = ShorthandAmmoBox(peregrine_dual_machine_gun_ammo, peregrine_dual_machine_gun_ammo.Capacity) Map( //"quadstealth" -> VehicleLoadout("default_quadstealth", List(), List(), quadstealth), "quadassault" -> VehicleLoadout( @@ -513,9 +516,114 @@ object VehicleTerminalDefinition { SimplifiedEntry(ammo_mortar, 186) ), galaxy_gunship - ) + ), //"phantasm" -> VehicleLoadout("default_phantasm", List(), List(), phantasm), //"lodestar" -> VehicleLoadout("default_lodestar", List(), List(), lodestar), + { + val ammo = ShorthandAmmoBox(aphelion_plasma_rocket_ammo, aphelion_plasma_rocket_ammo.Capacity) + "aphelion_gunner" -> VehicleLoadout( + "default_aphelion_gunner", + List(), + List( + SimplifiedEntry(ammo_ppa, 30), + SimplifiedEntry(ammo_ppa, 34), + SimplifiedEntry(ammo_ppa, 38), + SimplifiedEntry(ammo_ppa, 90), + SimplifiedEntry(ammo_ppa, 94), + SimplifiedEntry(ammo_ppa, 98), + SimplifiedEntry(ammo, 150), + SimplifiedEntry(ammo, 155), + SimplifiedEntry(ammo, 160), + SimplifiedEntry(ammo, 225), + SimplifiedEntry(ammo, 230), + SimplifiedEntry(ammo, 235) + ), + aphelion_gunner + ) + }, + "aphelion_flight" -> VehicleLoadout( + "default_aphelion_flight", + List(), + List( + SimplifiedEntry(ammo_ppa, 30), + SimplifiedEntry(ammo_ppa, 34), + SimplifiedEntry(ammo_ppa, 38), + SimplifiedEntry(ammo_ppa, 90), + SimplifiedEntry(ammo_ppa, 94), + SimplifiedEntry(ammo_ppa, 98) + ), + aphelion_flight + ), + { + val ammo = ShorthandAmmoBox(colossus_100mm_cannon_ammo, colossus_100mm_cannon_ammo.Capacity) + "colossus_gunner" -> VehicleLoadout( + "default_colossus_gunner", + List(), + List( + SimplifiedEntry(ammo_tcannon, 30), + SimplifiedEntry(ammo_tcannon, 34), + SimplifiedEntry(ammo_tcannon, 38), + SimplifiedEntry(ammo_tcannon, 90), + SimplifiedEntry(ammo_tcannon, 94), + SimplifiedEntry(ammo_tcannon, 98), + SimplifiedEntry(ammo, 150), + SimplifiedEntry(ammo, 155), + SimplifiedEntry(ammo, 160), + SimplifiedEntry(ammo, 225), + SimplifiedEntry(ammo, 230), + SimplifiedEntry(ammo, 235) + ), + colossus_gunner + ) + }, + "colossus_flight" -> VehicleLoadout( + "default_colossus_flight", + List(), + List( + SimplifiedEntry(ammo_tcannon, 30), + SimplifiedEntry(ammo_tcannon, 34), + SimplifiedEntry(ammo_tcannon, 38), + SimplifiedEntry(ammo_tcannon, 90), + SimplifiedEntry(ammo_tcannon, 94), + SimplifiedEntry(ammo_tcannon, 98) + ), + colossus_flight + ), + { + val ammo = ShorthandAmmoBox(peregrine_particle_cannon_ammo, peregrine_particle_cannon_ammo.Capacity) + "peregrine_gunner" -> VehicleLoadout( + "default_peregrine_gunner", + List(), + List( + SimplifiedEntry(ammo_mgun, 30), + SimplifiedEntry(ammo_mgun, 34), + SimplifiedEntry(ammo_mgun, 38), + SimplifiedEntry(ammo_mgun, 90), + SimplifiedEntry(ammo_mgun, 94), + SimplifiedEntry(ammo_mgun, 98), + SimplifiedEntry(ammo, 150), + SimplifiedEntry(ammo, 155), + SimplifiedEntry(ammo, 160), + SimplifiedEntry(ammo, 225), + SimplifiedEntry(ammo, 230), + SimplifiedEntry(ammo, 235) + ), + peregrine_gunner + ) + }, + "peregrine_flight" -> VehicleLoadout( + "default_peregrine_flight", + List(), + List( + SimplifiedEntry(ammo_mgun, 30), + SimplifiedEntry(ammo_mgun, 34), + SimplifiedEntry(ammo_mgun, 38), + SimplifiedEntry(ammo_mgun, 90), + SimplifiedEntry(ammo_mgun, 94), + SimplifiedEntry(ammo_mgun, 98) + ), + peregrine_flight + ) ) } diff --git a/src/main/scala/net/psforever/objects/serverobject/transfer/TransferBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/transfer/TransferBehavior.scala index dc95c7ed8..94bfbebce 100644 --- a/src/main/scala/net/psforever/objects/serverobject/transfer/TransferBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/transfer/TransferBehavior.scala @@ -86,18 +86,21 @@ object TransferBehavior { Discharging = Value } + + sealed trait Command + /** * Message to cue a process of transferring into oneself. */ - final case class Charging(transferMaterial : Any) + final case class Charging(transferMaterial : Any) extends Command /** * Message to cue a process of transferring from oneself. */ - final case class Discharging(transferMaterial : Any) + final case class Discharging(transferMaterial : Any) extends Command /** * Message to cue a stopping the transfer process. */ - final case class Stopping() + final case class Stopping() extends Command /** * A default search function that does not actually search for anything or ever find anything. diff --git a/src/main/scala/net/psforever/objects/serverobject/transfer/TransferContainer.scala b/src/main/scala/net/psforever/objects/serverobject/transfer/TransferContainer.scala index 1575e159e..5e78ebba7 100644 --- a/src/main/scala/net/psforever/objects/serverobject/transfer/TransferContainer.scala +++ b/src/main/scala/net/psforever/objects/serverobject/transfer/TransferContainer.scala @@ -2,12 +2,17 @@ package net.psforever.objects.serverobject.transfer import akka.actor.ActorRef +import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.entity.{Identifiable, WorldEntity} +import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.zones.ZoneAware -trait TransferContainer extends Identifiable +trait TransferContainer + extends PlanetSideGameObject + with Identifiable with ZoneAware - with WorldEntity { + with WorldEntity + with FactionAffinity { def Actor : ActorRef } diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala index d735fe689..5fdddea49 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -97,25 +97,28 @@ class FacilityTurretControl(turret: FacilityTurret) ) case FacilityTurret.RechargeAmmo() => - val weapon = turret.ControlledWeapon(1).get.asInstanceOf[net.psforever.objects.Tool] - // recharge when last shot fired 3s delay, +1, 200ms interval - if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) { - weapon.Magazine += 1 - val seat = turret.Seat(0).get - seat.occupant match { - case Some(player: Player) => - turret.Zone.LocalEvents ! LocalServiceMessage( - turret.Zone.id, - LocalAction.RechargeVehicleWeapon(player.GUID, turret.GUID, weapon.GUID) - ) - case _ => ; - } + turret.ControlledWeapon(wepNumber = 1).foreach { + case weapon: Tool => + // recharge when last shot fired 3s delay, +1, 200ms interval + if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) { + weapon.Magazine += 1 + val seat = turret.Seat(0).get + seat.occupant match { + case Some(player : Player) => + turret.Zone.LocalEvents ! LocalServiceMessage( + turret.Zone.id, + LocalAction.RechargeVehicleWeapon(player.GUID, turret.GUID, weapon.GUID) + ) + case _ => ; + } + } + else if (weapon.Magazine == weapon.MaxMagazine && weaponAmmoRechargeTimer != Default.Cancellable) { + weaponAmmoRechargeTimer.cancel() + weaponAmmoRechargeTimer = Default.Cancellable + } + case _ => ; } - if (weapon.Magazine == weapon.MaxMagazine && weaponAmmoRechargeTimer != Default.Cancellable) { - weaponAmmoRechargeTimer.cancel() - weaponAmmoRechargeTimer = Default.Cancellable - } case _ => ; } diff --git a/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala index bb8f43c60..5a948cd93 100644 --- a/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala +++ b/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala @@ -1,7 +1,7 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.vehicles -import akka.actor.{ActorRef, Cancellable} +import akka.actor.ActorRef import net.psforever.actors.commands.NtuCommand import net.psforever.actors.zone.BuildingActor import net.psforever.objects.serverobject.deploy.Deployment @@ -18,8 +18,10 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior { - var ntuChargingTick: Cancellable = Default.Cancellable var panelAnimationFunc: () => Unit = NoCharge + var ntuChargingTick = Default.Cancellable + findChargeTargetFunc = Vehicles.FindANTChargingSource + findDischargeTargetFunc = Vehicles.FindANTDischargingTarget def TransferMaterial = Ntu.Nanites @@ -28,7 +30,8 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior { def antBehavior: Receive = storageBehavior.orElse(transferBehavior) def ActivatePanelsForChargingEvent(vehicle: NtuContainer): Unit = { - val zone = vehicle.Zone + val obj = ChargeTransferObject + val zone = obj.Zone zone.VehicleEvents ! VehicleServiceMessage( zone.id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vehicle.GUID, 52, 1L) @@ -37,7 +40,8 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior { /** Charging */ def StartNtuChargingEvent(vehicle: NtuContainer): Unit = { - val zone = vehicle.Zone + val obj = ChargeTransferObject + val zone = obj.Zone zone.VehicleEvents ! VehicleServiceMessage( zone.id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vehicle.GUID, 49, 1L) @@ -190,7 +194,6 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior { } else { scala.math.min(min, chargeable.NtuCapacitor) } - // var chargeToDeposit = Math.min(Math.min(chargeable.NtuCapacitor, 100), max) chargeable.NtuCapacitor -= chargeToDeposit UpdateNtuUI(chargeable) sender ! Ntu.Grant(chargeable, chargeToDeposit) diff --git a/src/main/scala/net/psforever/objects/vehicles/BfrTransferBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/BfrTransferBehavior.scala new file mode 100644 index 000000000..24aa6d9c2 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/BfrTransferBehavior.scala @@ -0,0 +1,327 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles + +import akka.actor.ActorRef +import akka.actor.typed.scaladsl.adapter._ +import net.psforever.actors.commands.NtuCommand +import net.psforever.actors.zone.BuildingActor +import net.psforever.objects.{NtuContainer, NtuContainerDefinition, _} +import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.equipment.EquipmentSlot +import net.psforever.objects.serverobject.resourcesilo.ResourceSilo +import net.psforever.objects.serverobject.structures.WarpGate +import net.psforever.objects.serverobject.transfer.{TransferBehavior, TransferContainer} +import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +trait BfrTransferBehavior + extends TransferBehavior + with NtuStorageBehavior { + var ntuProcessingRequest: Boolean = false + var ntuProcessingTick = Default.Cancellable + + findChargeTargetFunc = Vehicles.FindBfrChargingSource + findDischargeTargetFunc = Vehicles.FindBfrDischargingTarget + + def TransferMaterial = Ntu.Nanites + + private var pairedSlotList: Option[List[(VehicleSubsystem, (Int, EquipmentSlot))]] = None + /** + * Return the paired arm weapon subsystems with arm weapon equipment mount and the slot number for that mount, + * connecting "left" to "left" and "right" to "right". + * Either return the existing connection or create that connection for the first time and retain it for future use. + * Works regardless of the type of battleframe unit. + * @return the arm weapon subsystems for each arm weapon mount and that mount's slot number + */ + def pairedArmSlotSubsystems(): List[(VehicleSubsystem, (Int, EquipmentSlot))] = { + pairedSlotList.getOrElse { + val obj = ChargeTransferObject + val pairs = obj.Subsystems() + .filter { sub => + sub.sys.name.startsWith("BattleframeLeftArm") || sub.sys.name.startsWith("BattleframeRightArm") + } + .zip( + obj.Weapons.filter { case (a, _) => + a == 1 || a == 2 || a == 3 //gunner -> 2,3; flight -> 1,2 + } + ) + pairedSlotList = Some(pairs) + pairs + } + } + private var pairedList: Option[List[(VehicleSubsystem, EquipmentSlot)]] = None + /** + * Return the paired arm weapon subsystems with arm weapon mount, + * connecting "left" to "left" and "right" to "right". + * Either return the existing connection or create that connection for the first time and retain it for future use. + * Works regardless of the type of battleframe unit. + * @return the arm weapon subsystems for each arm weapon mount + */ + def pairedArmSubsystems(): List[(VehicleSubsystem, EquipmentSlot)] = { + pairedList.getOrElse { + val pairs = pairedArmSlotSubsystems().map { case (a, (_, c)) => (a, c) } + pairedList = Some(pairs) + pairs + } + } + + def getNtuContainer(): Option[NtuContainer] = { + pairedArmSubsystems() + .find { case (sub, arm) => + //find an active ntu siphon + arm.Equipment.nonEmpty && + GlobalDefinitions.isBattleFrameNTUSiphon(arm.Equipment.get.Definition) && + sub.Enabled + } + .map { d => d._2.Equipment.get } match { + case Some(equipment: Tool) => + Some(new NtuSiphon(equipment, ChargeTransferObject.Definition)) + case _ => + None + } + } + + def ChargeTransferObject: Vehicle with NtuContainer + + def bfrBehavior: Receive = storageBehavior + .orElse(transferBehavior) + .orElse { + case BfrTransferBehavior.NextProcessTick(event) => + transferTarget match { + case Some(target) + if event == transferEvent && ntuProcessingRequest && event == TransferBehavior.Event.Charging => + HandleChargingOps(target) + case Some(target) + if event == transferEvent && ntuProcessingRequest && event == TransferBehavior.Event.Discharging => + HandleDischargingEvent(target) + case Some(target) + if event == transferEvent && !ntuProcessingRequest => + TryStopChargingEvent(target) + case _ => ; + TryStopChargingEvent(ChargeTransferObject) + } + } + + def UpdateNtuUI(vehicle: Vehicle with NtuContainer): Unit = { + getNtuContainer() match { + case Some(siphon) => + UpdateNtuUI(vehicle, siphon) + case None => ; + } + } + + def UpdateNtuUI(vehicle: Vehicle with NtuContainer, siphon: NtuContainer): Unit = { + siphon match { + case equip: NtuSiphon => + vehicle.Zone.VehicleEvents ! VehicleServiceMessage( + vehicle.Actor.toString, + VehicleAction.InventoryState2(PlanetSideGUID(0), equip.storageGUID, siphon.GUID, siphon.NtuCapacitor.toInt) + ) + case _ => ; + } + } + + def HandleChargingEvent(target: TransferContainer): Boolean = { + if (transferEvent == TransferBehavior.Event.None) { + HandleChargingOps(target) + } else { + ntuProcessingRequest = true + false + } + } + + def HandleChargingOps(target: TransferContainer): Boolean = { + ntuProcessingRequest = false + getNtuContainer() match { + case Some(siphon: NtuSiphon) + if siphon.NtuCapacitor < siphon.MaxNtuCapacitor => + //charging + transferTarget = Some(target) + transferEvent = TransferBehavior.Event.Charging + val max = siphon.NtuCapacitor + val fromMax = siphon.MaxNtuCapacitor - max + target match { + case _: WarpGate => + //siphon.drain -> math.min(math.min(siphon.MaxNtuCapacitor / 75f, fromMax) + target.Actor ! BuildingActor.Ntu(NtuCommand.Request(math.min(siphon.drain.toFloat, fromMax), context.self)) + case _: ResourceSilo => + //siphon.drain -> scala.math.min(silo.MaxNtuCapacitor * 0.325f / max, fromMax) + target.Actor ! NtuCommand.Request(scala.math.min(0.5f * siphon.drain, fromMax), context.self) + case _ => ; + } + ntuProcessingTick.cancel() + ntuProcessingTick = context.system.scheduler.scheduleOnce( + delay = 1250 milliseconds, + self, + BfrTransferBehavior.NextProcessTick(transferEvent) + ) + true + case _ => + TryStopChargingEvent(ChargeTransferObject) + false + } + } + + def ReceiveAndDepositUntilFull(vehicle: Vehicle, amount: Float): Boolean = { + getNtuContainer() match { + case Some(siphon) => + ReceiveAndDepositUntilFull(vehicle, siphon, amount) + case None => + false + } + } + + def ReceiveAndDepositUntilFull(vehicle: Vehicle, obj: NtuContainer, amount: Float): Boolean = { + val isNotFull = (obj.NtuCapacitor += amount) < obj.MaxNtuCapacitor + UpdateNtuUI(vehicle, obj) + isNotFull + } + + /** Discharging */ + def HandleDischargingEvent(target: TransferContainer): Boolean = { + if (transferEvent == TransferBehavior.Event.None) { + HandleDischargingOps(target) + } else { + ntuProcessingRequest = true + false + } + } + + def HandleDischargingOps(target: TransferContainer): Boolean = { + ntuProcessingRequest = false + val obj = ChargeTransferObject + getNtuContainer() match { + case Some(siphon) + if siphon.NtuCapacitor > 0 => + transferTarget = Some(target) + transferEvent = TransferBehavior.Event.Discharging + target.Actor ! Ntu.Offer(obj) + ntuProcessingTick.cancel() + ntuProcessingTick = context.system.scheduler.scheduleOnce( + delay = 1250 milliseconds, + self, + BfrTransferBehavior.NextProcessTick(transferEvent) + ) + true + case _ => + TryStopChargingEvent(obj) + false + } + } + + def WithdrawAndTransmit(vehicle: Vehicle, maxRequested: Float): Any = { + val chargeable = ChargeTransferObject + val chargeToDeposit = getNtuContainer() match { + case Some(siphon) => + var chargeToDeposit = Math.min(Math.min(siphon.NtuCapacitor, 100), maxRequested) + siphon.NtuCapacitor -= chargeToDeposit + UpdateNtuUI(chargeable, siphon) + chargeToDeposit + case _ => + 0 + } + Ntu.Grant(chargeable, chargeToDeposit) + } + + /** Stopping */ + override def TryStopChargingEvent(container: TransferContainer): Unit = { + ntuProcessingTick.cancel() + ntuProcessingRequest = false + transferTarget match { + case Some(target: WarpGate) => + target.Actor ! BuildingActor.Ntu(NtuCommand.Grant(null, 0)) + case Some(target) => + target.Actor ! NtuCommand.Grant(null, 0) + case _ => ; + } + //cleanup + val obj = ChargeTransferObject + super.TryStopChargingEvent(obj) + } + + def StopNtuBehavior(sender: ActorRef): Unit = TryStopChargingEvent(ChargeTransferObject) + + def HandleNtuOffer(sender: ActorRef, src: NtuContainer): Unit = {} + + def HandleNtuRequest(sender: ActorRef, min: Float, max: Float): Unit = { + val chargeable = ChargeTransferObject + getNtuContainer() match { + case Some(siphon) => + if (transferEvent == TransferBehavior.Event.Discharging) { + val capacitor = siphon.NtuCapacitor + val bonus = System.currentTimeMillis()%2 + val (chargeBase, chargeToDeposit): (Float, Float) = if (min == 0) { + transferTarget match { + case Some(silo: ResourceSilo) => + // silos would charge from 0-30% in a full siphon's payload according to the wiki + val calcChargeBase = scala.math.min(scala.math.min(silo.MaxNtuCapacitor * 0.325f / siphon.MaxNtuCapacitor, capacitor), max) + (calcChargeBase, calcChargeBase + bonus) + case _ => + (0f, 0) + } + } else { + val charge = scala.math.min(min, capacitor) + (charge, charge + bonus) + } + siphon.NtuCapacitor -= chargeBase + UpdateNtuUI(chargeable, siphon) + sender ! Ntu.Grant(chargeable, chargeToDeposit) + } else { + TryStopChargingEvent(chargeable) + sender ! Ntu.Grant(chargeable, 0) + } + case None => ; + } + } + + def HandleNtuGrant(sender: ActorRef, src: NtuContainer, amount: Float): Unit = { + val obj = ChargeTransferObject + if (transferEvent != TransferBehavior.Event.Charging || !ReceiveAndDepositUntilFull(obj, amount)) { + sender ! Ntu.Request(0, 0) + } + } +} + +object BfrTransferBehavior { + private case class NextProcessTick(eventType: TransferBehavior.Event.Value) +} + +class NtuSiphon( + val equipment: Tool, + private val definition: ObjectDefinition with NtuContainerDefinition + ) extends NtuContainer { + def Faction: PlanetSideEmpire.Value = equipment.Faction + + def storageGUID: PlanetSideGUID = equipment.AmmoSlot.Box.GUID + + def drain: Int = equipment.FireMode.RoundsPerShot + + def NtuCapacitor: Float = equipment.Magazine.toFloat + + def NtuCapacitor_=(value: Float): Float = equipment.Magazine_=(value.toInt).toFloat + + def MaxNtuCapacitor: Float = equipment.MaxMagazine.toFloat + + override def Definition: ObjectDefinition with NtuContainerDefinition = definition + + def Actor: ActorRef = null + + override def GUID : PlanetSideGUID = equipment.GUID + + override def GUID_=(guid : PlanetSideGUID): PlanetSideGUID = equipment.GUID + + override def Position: Vector3 = Vector3.Zero + + override def Position_=(vec: Vector3): Vector3 = Vector3.Zero + + override def Orientation: Vector3 = Vector3.Zero + + override def Orientation_=(vec: Vector3): Vector3 = Vector3.Zero + + override def Velocity: Option[Vector3] = None + + override def Velocity_=(vec: Option[Vector3]): Option[Vector3] = None +} diff --git a/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala index d2629f30b..42ebd7de0 100644 --- a/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala +++ b/src/main/scala/net/psforever/objects/vehicles/CarrierBehavior.scala @@ -223,7 +223,7 @@ object CarrierBehavior { ) zone.VehicleEvents ! VehicleServiceMessage( s"${cargo.Actor}", - VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)) + VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, cargo.Definition.shieldUiAttribute, cargo.Shields)) ) CargoMountBehaviorForAll(carrier, cargo, mountPoint) zone.actor ! ZoneActor.RemoveFromBlockMap(cargo) @@ -470,7 +470,7 @@ object CarrierBehavior { ) events ! VehicleServiceMessage( s"$cargoActor", - VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)) + VehicleAction.SendResponse(GUID0, PlanetsideAttributeMessage(cargoGUID, cargo.Definition.shieldUiAttribute, cargo.Shields)) ) zone.actor ! ZoneActor.AddToBlockMap(cargo, carrier.Position) if (carrier.isFlying) { diff --git a/src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala b/src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala new file mode 100644 index 000000000..588df47b9 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala @@ -0,0 +1,109 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles + +import net.psforever.objects.Vehicle +import net.psforever.objects.ballistics.{Projectile, ProjectileQuality, SourceEntry} +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.base.{DamageResolution, DamageType} +import net.psforever.objects.vital.etc.RadiationReason +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.zones.blockmap.SectorPopulation +import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType} +import net.psforever.types.PlanetSideGUID + +case object RadiationInVehicleInteraction extends ZoneInteractionType + +/** + * This game entity may infrequently test whether it may interact with radiation cloud projectiles + * that may be emitted in the game environment for a limited amount of time. + * Since the entity in question is a vehicle, the occupants of the vehicle get tested their interaction. + */ +class InteractWithRadiationCloudsSeatedInVehicle( + private val obj: Vehicle, + val range: Float + ) extends ZoneInteraction { + /** + * radiation clouds that, though detected, are skipped from affecting the target; + * in between interaction tests, a memory of the clouds that were tested last are retained and + * are excluded from being tested this next time; + * clouds that are detected a second time are cleared from the list and are available to be tested next time + */ + private var skipTargets: List[PlanetSideGUID] = List() + + def Type = RadiationInVehicleInteraction + + /** + * Drive into a radiation cloud and all the vehicle's occupants suffer the consequences. + * @param sector the portion of the block map being tested + * @param target the fixed element in this test + */ + override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { + val position = target.Position + //collect all projectiles in sector/range + val projectiles = sector + .projectileList + .filter { cloud => + val definition = cloud.Definition + definition.radiation_cloud && + definition.AllDamageTypes.contains(DamageType.Radiation) && + { + val radius = definition.DamageRadius + Zone.distanceCheck(target, cloud, radius * radius) + } + } + .distinct + val notSkipped = projectiles.filterNot { t => skipTargets.contains(t.GUID) } + skipTargets = notSkipped.map { _.GUID } + if (notSkipped.nonEmpty) { + ( + //isolate one of each type of projectile + notSkipped + .foldLeft(Nil: List[Projectile]) { + (acc, next) => if (acc.exists { _.profile == next.profile }) acc else next :: acc + }, + obj.Seats + .values + .collect { case seat => seat.occupant } + .flatten + ) match { + case (uniqueProjectiles, targets) if uniqueProjectiles.nonEmpty && targets.nonEmpty => + val shielding = obj.Definition.RadiationShielding + targets.foreach { t => + uniqueProjectiles.foreach { p => + t.Actor ! Vitality.Damage( + DamageInteraction( + SourceEntry(t), + RadiationReason( + ProjectileQuality.modifiers(p, DamageResolution.Radiation, t, t.Position, None), + t.DamageModel, + shielding + ), + position + ).calculate() + ) + } + } + case _ => ; + } + } + obj.CargoHolds + .values + .collect { + case hold if hold.isOccupied => + val target = hold.occupant.get + target.interaction().find { _.Type == RadiationInVehicleInteraction } match { + case Some(func) => func.interaction(sector, target) + case _ => ; + } + } + } + + /** + * Any radiation clouds blocked from being tested should be cleared. + * All that can be done is blanking our retained previous effect targets. + * @param target the fixed element in this test + */ + def resetInteraction(target: InteractsWithZone): Unit = { + skipTargets = List() + } +} diff --git a/src/main/scala/net/psforever/objects/vehicles/MountableWeapons.scala b/src/main/scala/net/psforever/objects/vehicles/MountableWeapons.scala index fdbba2b20..f0756350a 100644 --- a/src/main/scala/net/psforever/objects/vehicles/MountableWeapons.scala +++ b/src/main/scala/net/psforever/objects/vehicles/MountableWeapons.scala @@ -15,21 +15,26 @@ trait MountableWeapons * @param seatNumber the mount number * @return a mounted weapon by index, or `None` if either the mount doesn't exist or there is no controlled weapon */ - def WeaponControlledFromSeat(seatNumber: Int): Option[Equipment] = { + def WeaponControlledFromSeat(seatNumber: Int): Set[Equipment] = { Definition .asInstanceOf[MountableWeaponsDefinition] - .controlledWeapons.get(seatNumber) match { - case Some(wepNumber) if seats.get(seatNumber).nonEmpty => controlledWeapon(wepNumber) - case _ => None + .controlledWeapons().get(seatNumber) match { + case Some(wepNumbers) if seats.get(seatNumber).nonEmpty => wepNumbers.flatMap { controlledWeapon } + case _ => Set.empty } } - def controlledWeapon(wepNumber: Int): Option[Equipment] = ControlledWeapon(wepNumber) + def controlledWeapon(wepNumber: Int): Set[Equipment] = ControlledWeapon(wepNumber) - def ControlledWeapon(wepNumber: Int): Option[Equipment] = { + def ControlledWeapon(wepNumber: Int): Set[Equipment] = { weapons.get(wepNumber) match { - case Some(slot) => slot.Equipment - case _ => None + case Some(slot) => + slot.Equipment match { + case Some(weapon) => Set(weapon) + case None => Set.empty + } + case _ => + Set.empty } } diff --git a/src/main/scala/net/psforever/objects/vehicles/MountableWeaponsDefinition.scala b/src/main/scala/net/psforever/objects/vehicles/MountableWeaponsDefinition.scala index f897b913c..b533058f5 100644 --- a/src/main/scala/net/psforever/objects/vehicles/MountableWeaponsDefinition.scala +++ b/src/main/scala/net/psforever/objects/vehicles/MountableWeaponsDefinition.scala @@ -8,5 +8,17 @@ import scala.collection.mutable trait MountableWeaponsDefinition extends MountedWeaponsDefinition with MountableDefinition { - val controlledWeapons: mutable.HashMap[Int, Int] = mutable.HashMap[Int, Int]() + private val _controlledWeapons: mutable.HashMap[Int, Set[Int]] = mutable.HashMap[Int, Set[Int]]() + + def controlledWeapons(): Map[Int, Set[Int]] = _controlledWeapons.toMap + + def controlledWeapons(seat: Int, weapon: Int): Map[Int, Set[Int]] = { + _controlledWeapons.put(seat, Set(weapon)) + _controlledWeapons.toMap + } + + def controlledWeapons(seat: Int, weapons: Set[Int]): Map[Int, Set[Int]] = { + _controlledWeapons.put(seat, weapons) + _controlledWeapons.toMap + } } diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala new file mode 100644 index 000000000..29033e8ba --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala @@ -0,0 +1,1165 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles + +import enumeratum.values.{IntEnum, IntEnumEntry} +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Vehicle} +import net.psforever.objects.equipment.{Equipment, JammableUnit} +import net.psforever.packet.PlanetSideGamePacket +import net.psforever.packet.game.{ComponentDamageField, ComponentDamageMessage, GenericObjectActionMessage} +import net.psforever.types.{PlanetSideGUID, SubsystemComponent} + +//data + +sealed abstract class VehicleSubsystemConditionModifier( + val value: Int, + val multiplier: Float, + val addend: Int + ) extends IntEnumEntry + +object VehicleSubsystemConditionModifier extends IntEnum[VehicleSubsystemConditionModifier] { + val values = findValues + + case object Off extends VehicleSubsystemConditionModifier(value = 1065353216, multiplier = 0f, addend = 0) + + case object Decay20 extends VehicleSubsystemConditionModifier(value = 1061997772, multiplier = 0.8f, addend = 0) + case object Decay40 extends VehicleSubsystemConditionModifier(value = 1058642329, multiplier = 0.6f, addend = 0) + case object Decay60 extends VehicleSubsystemConditionModifier(value = 1053609164, multiplier = 0.4f, addend = 0) + + case object Decay55 extends VehicleSubsystemConditionModifier(value = 1055286886, multiplier = 0.45f, addend = 0) + case object Decay90 extends VehicleSubsystemConditionModifier(value = 1036831948, multiplier = 0.1f, addend = 0) + + case object Range25 extends VehicleSubsystemConditionModifier(value = 1061158912, multiplier = 1f, addend = -25) + case object Range50 extends VehicleSubsystemConditionModifier(value = 1056964608, multiplier = 1f, addend = -50) + + case object Add100 extends VehicleSubsystemConditionModifier(value = 1073741824, multiplier = 200.0f, addend = 0) + case object Add200 extends VehicleSubsystemConditionModifier(value = 1077936128, multiplier = 300.0f, addend = 0) + case object Add300 extends VehicleSubsystemConditionModifier(value = 1082130432, multiplier = 400.0f, addend = 0) + case object Add500 extends VehicleSubsystemConditionModifier(value = 1086324736, multiplier = 600.0f, addend = 0) + case object Add700 extends VehicleSubsystemConditionModifier(value = 1090519040, multiplier = 800.0f, addend = 0) +} + +//Conditions + +trait VehicleSubsystemCondition { + def getMultiplier(): Float = 1f + + def getMessage(id: SubsystemComponent, vehicle: Vehicle, guid: PlanetSideGUID): List[PlanetSideGamePacket] +} + +final case class VehicleComponentCondition( + alarmLevel: Long, + factor: VehicleSubsystemConditionModifier, + unk: Boolean + ) extends VehicleSubsystemCondition { + override def getMultiplier(): Float = factor.multiplier + + def getMessage(id: SubsystemComponent, vehicle: Vehicle, guid: PlanetSideGUID): List[PlanetSideGamePacket] = { + if (vehicle.Jammed) { + List(ComponentDamageMessage(guid, id, Some(ComponentDamageField(alarm_level = 0, factor.value, unk)))) + } else { + List(ComponentDamageMessage(guid, id, Some(ComponentDamageField(alarmLevel, factor.value, unk)))) + } + } +} + +object VehicleComponentCondition { + def apply(alarm: Long, factor: VehicleSubsystemConditionModifier): VehicleComponentCondition = + VehicleComponentCondition(alarm, factor, unk = true) +} + +sealed abstract class BattleframeArmMountCondition(code: Int) extends VehicleSubsystemCondition { + def getMessage(id: SubsystemComponent, vehicle: Vehicle, guid: PlanetSideGUID): List[PlanetSideGamePacket] = { + List(GenericObjectActionMessage(guid, code)) + } +} + +private case object BattleframeArmActive extends BattleframeArmMountCondition(code = 38) + +private case object BattleframeArmInactive extends BattleframeArmMountCondition(code = 39) + +//Statuses + +trait VehicleSubsystemStatus { + def name: String + def effects: List[VehicleSubsystemCondition] + def priority: Int + def jamState: Int + def damageState: Option[Any] + + def damageable: Boolean = damageState.nonEmpty + + def jammable: Boolean = jamState > 0 + + def getMessageTarget(vehicle: Vehicle): Option[IdentifiableEntity] = Some(vehicle) + + def getMessageTargetId(vehicle: Vehicle): PlanetSideGUID = vehicle.GUID + + def getMessage(toState: Int, vehicle: Vehicle): List[PlanetSideGamePacket] + + def jammerMessages(toState: Int, vehicle: Vehicle): List[PlanetSideGamePacket] + + def clearJammerMessages(toState: Int, vehicle: Vehicle): List[PlanetSideGamePacket] +} + +trait VehicleSubsystemComponent + extends VehicleSubsystemStatus { + def componentId: SubsystemComponent + + def getMessage(toState: Int, vehicle: Vehicle): List[PlanetSideGamePacket] = { + effects(toState).getMessage(componentId, vehicle, getMessageTargetId(vehicle)) + } + + def jammerMessages(toState: Int, vehicle: Vehicle): List[PlanetSideGamePacket] = { + if (jammable && toState < jamState) { + effects(jamState).getMessage(componentId, vehicle, getMessageTargetId(vehicle)) + } else { + Nil + } + } + + def clearJammerMessages(toState: Int, vehicle: Vehicle): List[PlanetSideGamePacket] = { + if (jammable) { + effects(math.max(0, toState)).getMessage(componentId, vehicle, getMessageTargetId(vehicle)) + } else { + Nil + } + } +} + +final case class VehicleComponentStatus( + name: String, + componentId: SubsystemComponent, + effects: List[VehicleSubsystemCondition], + damageState: Option[Any], + jamState: Int, + priority: Int + ) extends VehicleSubsystemComponent + +object VehicleComponentStatus { + def apply(state: String, cid: SubsystemComponent, states: List[VehicleSubsystemCondition]): VehicleComponentStatus = + VehicleComponentStatus(state, cid, states, damageState = None, jamState = 0, priority = 0) +} + +trait VehicleWeaponStatus + extends VehicleSubsystemStatus { + def slotIndex: Int + + override def getMessageTarget(vehicle : Vehicle) : Option[PlanetSideGameObject] = { + vehicle.Weapons.get(slotIndex) match { + case Some(slot) => slot.Equipment + case None => throw new IllegalArgumentException(s"subsystem for battleframe arm mount missing mount - $slotIndex") + } + } + + override def getMessageTargetId(vehicle: Vehicle): PlanetSideGUID = { + getMessageTarget(vehicle) match { + case Some(e) => e.GUID + case None => PlanetSideGUID(0) + } + } + + override def getMessage(toState: Int, vehicle: Vehicle) : List[PlanetSideGamePacket] = { + getMessageTarget(vehicle) match { + case Some(_) => effects(toState).getMessage(id = SubsystemComponent.Unknown(36), vehicle, getMessageTargetId(vehicle)) + case None => Nil + } + } +} + +sealed abstract class BattleframeWeaponComponent extends VehicleSubsystemComponent with VehicleWeaponStatus { + override def getMessage(toState: Int, vehicle: Vehicle) : List[PlanetSideGamePacket] = { + getMessageTarget(vehicle) match { + case Some(_) => effects(toState).getMessage(componentId, vehicle, getMessageTargetId(vehicle)) + case None => Nil + } + } +} + +final case class BattleframeWeaponComponentStatus( + override val name: String, + override val componentId: SubsystemComponent, + override val effects: List[VehicleSubsystemCondition], + override val damageState: Option[Any], + override val jamState: Int, + override val priority: Int, + slotIndex: Int + ) extends BattleframeWeaponComponent + +final case class BattleframeWeaponOnlyComponentStatus( + override val name: String, + override val componentId: SubsystemComponent, + override val effects: List[VehicleSubsystemCondition], + override val damageState: Option[Any], + override val jamState: Int, + override val priority: Int, + slotIndex: Int + ) extends BattleframeWeaponComponent { + override def getMessageTarget(vehicle : Vehicle) : Option[PlanetSideGameObject] = { + super.getMessageTarget(vehicle) match { + case out @ Some(e: Equipment) + if !(GlobalDefinitions.isBattleFrameArmorSiphon(e.Definition) || + GlobalDefinitions.isBattleFrameNTUSiphon(e.Definition)) => + out + case _ => + None + } + } +} + +final case class BattleframeSiphonOnlyComponentStatus( + override val name: String, + override val componentId: SubsystemComponent, + override val effects: List[VehicleSubsystemCondition], + override val damageState: Option[Any], + override val jamState: Int, + override val priority: Int, + slotIndex: Int + ) extends BattleframeWeaponComponent { + override def getMessageTarget(vehicle : Vehicle) : Option[PlanetSideGameObject] = { + super.getMessageTarget(vehicle) match { + case out @ Some(e: Equipment) + if GlobalDefinitions.isBattleFrameArmorSiphon(e.Definition) || + GlobalDefinitions.isBattleFrameNTUSiphon(e.Definition) => + out + case _ => + None + } + } +} + +final case class BattleframeArmorSiphonComponentStatus( + override val name: String, + override val componentId: SubsystemComponent, + override val effects: List[VehicleSubsystemCondition], + override val damageState: Option[Any], + override val jamState: Int, + override val priority: Int, + slotIndex: Int + ) extends BattleframeWeaponComponent { + override def getMessageTarget(vehicle : Vehicle) : Option[PlanetSideGameObject] = { + super.getMessageTarget(vehicle) match { + case out @ Some(e: Equipment) + if GlobalDefinitions.isBattleFrameArmorSiphon(e.Definition) => + out + case _ => + None + } + } +} + +final case class BattleframeWeaponToggle(slotIndex: Int) + extends VehicleWeaponStatus { + def name: String = "Toggle" + + def effects: List[VehicleSubsystemCondition] = List(BattleframeArmActive, BattleframeArmInactive) + + def priority: Int = 0 + + def jamState: Int = 0 + + def damageState: Option[Any] = None + + def jammerMessages(toState: Int, vehicle: Vehicle): List[PlanetSideGamePacket] = Nil + + def clearJammerMessages(toState: Int, vehicle: Vehicle): List[PlanetSideGamePacket] = Nil +} + +//Entry + +trait VehicleSubsystemFields { + def name: String + + def statuses: List[VehicleSubsystemStatus] + + def startsEnabled: Boolean + + def enabledStatus: List[String] + + def defaultState: Boolean = startsEnabled + + def damageable: Boolean = statuses.exists { _.damageable } + + def jammable: Boolean = statuses.exists { _.jammable } +} + +sealed abstract class VehicleSubsystemEntry( + val name: String, + val statuses: List[VehicleSubsystemStatus], + val startsEnabled: Boolean, + val enabledStatus: List[String] + ) extends VehicleSubsystemFields { + def this(value: String, statuses: List[VehicleSubsystemStatus]) = + this(value, statuses, startsEnabled = true, enabledStatus = Nil) + +// def jammerMessages(vehicle: Vehicle): List[PlanetSideGamePacket] = Nil //TODO +// +// def clearJammerMessages(vehicle: Vehicle): List[PlanetSideGamePacket] = Nil //TODO +// +// def getMessages(vehicle: Vehicle): List[PlanetSideGamePacket] = Nil //TODO +// +// def currentMessages(vehicle: Vehicle): List[PlanetSideGamePacket] = Nil //TODO +} + +sealed abstract class BattleframeArmToggleEntry(override val name: String, slotIndex: Int) + extends VehicleSubsystemEntry( + name, + statuses = List(BattleframeWeaponToggle(slotIndex)), + startsEnabled = true, + enabledStatus = List("Toggle") + ) + +sealed abstract class BattleframeArmWeaponEntry(override val name: String, slotIndex: Int) extends VehicleSubsystemEntry( + name, + statuses = List( + //weapons only + BattleframeWeaponOnlyComponentStatus( + "COFRecovery", + SubsystemComponent.WeaponSystemsCOFRecovery, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Add100), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Add200), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Add300), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Add500), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Add700) + ), + damageState = Some(1), + jamState = 1, + priority = 0, + slotIndex + ), + BattleframeWeaponOnlyComponentStatus( + "COF", + SubsystemComponent.WeaponSystemsCOF, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Add100), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Add200), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Add300), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Add500), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Add700) + ), + damageState = Some(1), + jamState = 0, + priority = 0, + slotIndex + ), + BattleframeWeaponOnlyComponentStatus( + "AmmoLoss", + SubsystemComponent.WeaponSystemsAmmoLoss, + List( + VehicleComponentNormal, + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Off) + ), + damageState = None, + jamState = 0, + priority = 0, + slotIndex + ), + BattleframeWeaponOnlyComponentStatus( + "RefireTime", + SubsystemComponent.WeaponSystemsRefireTime, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Add100), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Add200), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Add300), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Add500), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Add700) + ), + damageState = Some(1), + jamState = 0, + priority = 0, + slotIndex + ), + BattleframeWeaponOnlyComponentStatus( + "ReloadTime", + SubsystemComponent.WeaponSystemsReloadTime, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Add100), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Add200), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Add300), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Add500), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Add700) + ), + damageState = Some(1), + jamState = 0, + priority = 0, + slotIndex + ), + //siphons only + BattleframeSiphonOnlyComponentStatus( + "TransferEfficiency", + SubsystemComponent.SiphonTransferEfficiency, + List(VehicleComponentNormal,PlaceholderNormalReplaceLater), + damageState = None, + jamState = 0, + priority = 0, + slotIndex + ), + BattleframeSiphonOnlyComponentStatus( + "TransferRate", + SubsystemComponent.SiphonTransferRateA, + List(VehicleComponentNormal,PlaceholderNormalReplaceLater), + damageState = None, + jamState = 0, + priority = 0, + slotIndex + ), + BattleframeSiphonOnlyComponentStatus( + "DrainOnly", + SubsystemComponent.SiphonDrainOnly, + List(VehicleComponentNormal,PlaceholderNormalReplaceLater), + damageState = None, + jamState = 0, + priority = 0, + slotIndex + ), + BattleframeSiphonOnlyComponentStatus( + "StorageCapacity", + SubsystemComponent.SiphonStorageCapacity, + List(VehicleComponentNormal,PlaceholderNormalReplaceLater), + damageState = None, + jamState = 0, + priority = 0, + slotIndex + ), + BattleframeSiphonOnlyComponentStatus( + "TransferRate", + SubsystemComponent.SiphonTransferRateB, + List(PlaceholderNormalReplaceLater,PlaceholderNormalReplaceLater), + damageState = None, + jamState = 0, + priority = 0, + slotIndex + ), + //unknown + BattleframeWeaponComponentStatus( + "ProjectileRange", + SubsystemComponent.UnknownProjectileRange, + List( + VehicleComponentNormal, + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Decay55), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Decay90) + ), + damageState = None, + jamState = 1, + priority = 0, + slotIndex + ), + BattleframeWeaponComponentStatus( + "SensorRange", + SubsystemComponent.UnknownSensorRange, + List(PlaceholderNormalReplaceLater,PlaceholderNormalReplaceLater), + damageState = None, + jamState = 0, + priority = 0, + slotIndex + ), + BattleframeWeaponComponentStatus( + "RechargeInterval", + SubsystemComponent.UnknownRechargeInterval, + List(PlaceholderNormalReplaceLater,PlaceholderNormalReplaceLater), + damageState = None, + jamState = 0, + priority = 0, + slotIndex + ), + //all + BattleframeWeaponComponentStatus( + "Online", + SubsystemComponent.WeaponSystemsOffline, + List( + VehicleComponentNormal, + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Off) + ), + damageState = Some(1), + jamState = 0, + priority = 1, + slotIndex + ), + BattleframeWeaponComponentStatus( + "Destroyed", + SubsystemComponent.WeaponSystemsDestroyed, + List( + VehicleComponentNormal, + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Off) + ), + damageState = Some(1), + jamState = 0, + priority = 2, + slotIndex + ) + ), + startsEnabled = true, + enabledStatus = List("Online", "Destroyed") +) + +//// + +sealed abstract class BattleframeShieldGeneratorCondition(code: Int) extends VehicleSubsystemCondition { + def getMessage(id: SubsystemComponent, vehicle: Vehicle, guid: PlanetSideGUID): List[PlanetSideGamePacket] = { + if (vehicle.Shields > 0) { + List(GenericObjectActionMessage(guid, code)) + } else { + Nil + } + } +} + +case object PlaceholderNormalReplaceLater extends VehicleSubsystemCondition { + def getMessage(id: SubsystemComponent, vehicle: Vehicle, guid: PlanetSideGUID): List[PlanetSideGamePacket] = Nil +} + +case object VehicleComponentNormal extends VehicleSubsystemCondition { + def getMessage(id: SubsystemComponent, vehicle: Vehicle, guid: PlanetSideGUID): List[PlanetSideGamePacket] = { + List(ComponentDamageMessage(guid, id, None)) + } +} + +private case object BattleframeShieldGeneratorOnline extends BattleframeShieldGeneratorCondition(code = 44) + +private case object BattleframeShieldGeneratorOffline extends BattleframeShieldGeneratorCondition(code = 45) { + override def getMultiplier(): Float = 0f +} + +private case object BattleframeShieldGeneratorFixed extends VehicleSubsystemCondition { + def getMessage(id: SubsystemComponent, vehicle: Vehicle, guid: PlanetSideGUID): List[PlanetSideGamePacket] = { + List(ComponentDamageMessage(guid, id, None)) + } +} + +private case object BattleframeShieldGeneratorDamaged extends VehicleSubsystemCondition { + override def getMultiplier(): Float = 0f + + def getMessage(id: SubsystemComponent, vehicle: Vehicle, guid: PlanetSideGUID): List[PlanetSideGamePacket] = { + if (vehicle.SubsystemStatus("BattleframeShieldGenerator.Online").contains(true)) { + List(ComponentDamageMessage(guid, id, Some(ComponentDamageField(4, VehicleSubsystemConditionModifier.Off.value)))) + } else { + Nil + } + } +} + +private case object BattleframeShieldGeneratorDestroyed extends VehicleSubsystemCondition { + override def getMultiplier(): Float = 0f + + def getMessage(id: SubsystemComponent, vehicle: Vehicle, guid: PlanetSideGUID): List[PlanetSideGamePacket] = { + BattleframeShieldGeneratorOffline.getMessage(id, vehicle, guid) ++ + List(ComponentDamageMessage(guid, id, Some( + ComponentDamageField(4, VehicleSubsystemConditionModifier.Off.value, unk = false) + ))) + } +} + +object VehicleSubsystemEntry { + case object Controls extends VehicleSubsystemEntry( + name = "Controls", + statuses = List( + VehicleComponentStatus( + "Impaired", + SubsystemComponent.Unknown(36), + List(PlaceholderNormalReplaceLater,PlaceholderNormalReplaceLater), + damageState = None, + jamState = 0, + priority = 0 + ) + ), + startsEnabled = true, + enabledStatus = List("Impaired") + ) + + case object Ejection extends VehicleSubsystemEntry( + name = "Ejection", + statuses = List( + VehicleComponentStatus( + "Online", + SubsystemComponent.Unknown(36), + List(PlaceholderNormalReplaceLater,PlaceholderNormalReplaceLater), + damageState = Some(1), + jamState = 0, + priority = 0 + ) + ), + startsEnabled = true, + enabledStatus = List("Online") + ) + + case object MosquitoRadar extends VehicleSubsystemEntry( + name = "MosquitoRadar", + statuses = List( + VehicleComponentStatus( + "Online", + SubsystemComponent.Unknown(36), + List(PlaceholderNormalReplaceLater,PlaceholderNormalReplaceLater), + damageState = None, + jamState = 0, + priority = 0 + ) + ), + startsEnabled = true, + enabledStatus = List("Online") + ) + + case object BattleframeMovementServos extends VehicleSubsystemEntry( + name = "BattleframeMovementServos", + statuses = List( + VehicleComponentStatus( + "Transit", + SubsystemComponent.MovementServosTransit, + List( + VehicleComponentNormal, + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Off) + ), + damageState = Some(1), + jamState = 1, + priority = 0 + ), + VehicleComponentStatus( + "Backward", + SubsystemComponent.MovementServosBackward, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Decay20), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Decay40), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Decay60) + ), + damageState = Some(1), + jamState = 1, + priority = 0 + ), + VehicleComponentStatus( + "Forward", + SubsystemComponent.MovementServosForward, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Decay20), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Decay40), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Decay60) + ), + damageState = Some(1), + jamState = 1, + priority = 0 + ), + VehicleComponentStatus( + "PivotSpeed", + SubsystemComponent.MovementServosPivotSpeed, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Decay20), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Decay40), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Decay60) + ), + damageState = Some(1), + jamState = 2, + priority = 0 + ), + VehicleComponentStatus( + "StrafeSpeed", + SubsystemComponent.MovementServosStrafeSpeed, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Decay20), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Decay40), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Decay60) + ), + damageState = Some(1), + jamState = 1, + priority = 0 + ) + ), + startsEnabled = true, + enabledStatus = Nil + ) + + case object BattleframeSensorArray extends VehicleSubsystemEntry( + name = "BattleframeSensorArray", + statuses = List( + VehicleComponentStatus( + "NoEnemies", + SubsystemComponent.SensorArrayNoEnemies, + List( + VehicleComponentNormal, + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Off) + ), + damageState = Some(1), + jamState = 1, + priority = 1 + ), + VehicleComponentStatus( + "NoEnemyAircraft", + SubsystemComponent.SensorArrayNoEnemyAircraft, + List( + VehicleComponentNormal, + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Off) + ), + damageState = Some(1), + jamState = 0, + priority = 0 + ), + VehicleComponentStatus( + "NoEnemyGroundVehicles", + SubsystemComponent.SensorArrayNoEnemyGroundVehicles, + List( + VehicleComponentNormal, + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Off) + ), + damageState = Some(1), + jamState = 0, + priority = 0 + ), + VehicleComponentStatus( + "NoEnemyProjectiles", + SubsystemComponent.SensorArrayNoEnemyProjectiles, + List( + VehicleComponentNormal, + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Off) + ), + damageState = Some(1), + jamState = 0, + priority = 0 + ), + VehicleComponentStatus( + "SensorRange", + SubsystemComponent.SensorArrayRange, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Range50), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Range25), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Off) + ), + damageState = Some(1), + jamState = 0, + priority = 0 + ) + ), + startsEnabled = true, + enabledStatus = List("NoEnemies"), + ) + + case object BattleframeFlightPod extends VehicleSubsystemEntry( + name = "BattleframeFlightPod", + statuses = List( + VehicleComponentStatus( + "RechargeRate", + SubsystemComponent.FlightSystemsRechargeRate, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Decay20), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Decay40), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Decay60) + ) + ), + VehicleComponentStatus( + "UseRate", + SubsystemComponent.FlightSystemsUseRate, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Add100), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Add200) + ) + ), + VehicleComponentStatus( + "HorizontalForce", + SubsystemComponent.FlightSystemsHorizontalForce, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Decay20), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Decay40), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Decay60) + ) + ), + VehicleComponentStatus( + "VerticalForce", + SubsystemComponent.FlightSystemsVerticalForce, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Decay20), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Decay40), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Decay60) + ) + ), + VehicleComponentStatus( + "Online", + SubsystemComponent.FlightSystemsOffline, + List( + VehicleComponentNormal, + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Off, unk = false) + ), + damageState = Some(1), + jamState = 1, + priority = 1 + ), + VehicleComponentStatus( + "Destroyed", + SubsystemComponent.FlightSystemsOffline, + List( + VehicleComponentNormal, + VehicleComponentCondition(7, VehicleSubsystemConditionModifier.Off, unk = false) + ), + damageState = Some(1), + jamState = 0, + priority = 2 + ) + ), + startsEnabled = true, + enabledStatus = List("Online", "Destroyed") + ) + + case object BattleframeShieldGenerator extends VehicleSubsystemEntry( + name = "BattleframeShieldGenerator", + List( + VehicleComponentStatus( + "RechargeRate", + SubsystemComponent.ShieldGeneratorRechargeRate, + List( + VehicleComponentNormal, + VehicleComponentCondition(3, VehicleSubsystemConditionModifier.Decay20), + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Decay40), + VehicleComponentCondition(5, VehicleSubsystemConditionModifier.Decay60) + ), + damageState = None, + jamState = 2, + priority = 0 + ), + VehicleComponentStatus( + "Online", + SubsystemComponent.Unknown(36), //doesn't matter + List( + BattleframeShieldGeneratorOnline, + BattleframeShieldGeneratorOffline + ), + damageState = None, + jamState = 0, + priority = 1 + ), + VehicleComponentStatus( + "Damaged", + SubsystemComponent.ShieldGeneratorOffline, + List( + VehicleComponentNormal, + BattleframeShieldGeneratorDamaged + ), + damageState = Some(1), + jamState = 1, + priority = 1 + ), + VehicleComponentStatus( + "Destroyed", + SubsystemComponent.ShieldGeneratorDestroyed, + List( + BattleframeShieldGeneratorFixed, + BattleframeShieldGeneratorDestroyed + ), + damageState = Some(1), + jamState = 0, + priority = 2 + ) + ), + startsEnabled = true, + enabledStatus = List("Online", "Damaged", "Destroyed") + ) + + case object BattleframeTrunk extends VehicleSubsystemEntry( + name = "BattleframeTrunk", + statuses = List( + VehicleComponentStatus( + "Damaged", + SubsystemComponent.Trunk, + List( + VehicleComponentNormal, + VehicleComponentCondition(4, VehicleSubsystemConditionModifier.Off) //one item destroyed + ), + damageState = Some(1), + jamState = 0, + priority = 0 + ) + ), + startsEnabled = true, + enabledStatus = Nil + ) + + case object BattleframeLeftArm extends BattleframeArmToggleEntry(name = "BattleframeLeftArm", slotIndex = 2) + + case object BattleframeRightArm extends BattleframeArmToggleEntry(name = "BattleframeRightArm", slotIndex = 3) + + case object BattleframeFlightLeftArm extends BattleframeArmToggleEntry(name = "BattleframeLeftArmF", slotIndex = 1) + + case object BattleframeFlightRightArm extends BattleframeArmToggleEntry(name = "BattleframeRightArmF", slotIndex = 2) + + case object BattleframeLeftWeapon extends BattleframeArmWeaponEntry(name = "BattleframeLeftWeapon", slotIndex = 2) + + case object BattleframeRightWeapon extends BattleframeArmWeaponEntry(name = "BattleframeRightWeapon", slotIndex = 3) + + case object BattleframeGunnerWeapon extends BattleframeArmWeaponEntry(name = "BattleframeGunnerWeapon", slotIndex = 4) + + case object BattleframeFlightLeftWeapon extends BattleframeArmWeaponEntry(name = "BattleframeLeftWeaponF", slotIndex = 1) + + case object BattleframeFlightRightWeapon extends BattleframeArmWeaponEntry(name = "BattleframeRightWeaponF", slotIndex = 2) +} + +class VehicleSubsystem(val sys: VehicleSubsystemEntry) + extends JammableUnit { + /** na */ + private val damageStates: Array[VehicleSubsystemStatusMonitor] = { + sys.statuses.zipWithIndex.map { case (a, i) => new VehicleSubsystemStatusMonitor(a, i) }.toArray + } + /** na */ + private var activeJammedMsgs: List[Int] = Nil + /** na */ + private var changedWhilejammed: Array[Int] = Array.emptyIntArray + /** whether this subsystem is currently active or inactive */ + private var enabled: Boolean = sys.startsEnabled + /** these statuses must be in good condition for the subsystem to be considered operational (enabled) */ + private val enabledStatusIndices = sys.enabledStatus.map { + str => sys.statuses.indexWhere { _.name.equals(str) } + } + //first enabled status condition defaults to a damaged state if the subsystem defaults to disabled + if (!enabled && enabledStatusIndices.nonEmpty) { + damageStates(enabledStatusIndices.head).Condition = 1 + } + + /** + * If this subsystem is activated, + * any and all accredited statuses are considered healthy and/or + * the internal field (if a primary flag) is set and + * the subsystem is not jammed. + * @return whether the subsystem is activated + */ + def Enabled: Boolean = { + !Jammed && (if (enabledStatusIndices.nonEmpty) { + enabledStatusIndices.forall { damageStates(_).Condition == 0 } + } else { + enabled + }) + } + + /** + * Treat this subsystem as activated or deactivated. + * If this subsystem has specific statuses whose conditions are linked to its activation state, + * the first accredited status is selected and set to the same activation state. + * @param state the new state of the subsystem + * @return the new state of the subsystem + */ + def Enabled_=(state: Boolean): Boolean = { + if (enabled != state && enabledStatusIndices.nonEmpty) { + //for any VehicleSubsystemEvent.status, index=0 is normal and index>0 is damaged/jammed/disabled + val condIndex = enabledStatusIndices.head + val stateAsInt = if (state) { 0 } else { 1 } + if ((stateAsInt == 1 && damageStates(condIndex).Condition == 0) || + (stateAsInt == 0 && damageStates(condIndex).Condition > 0)) { + damageStates(condIndex).Condition = stateAsInt + } + } + enabled = state + Enabled + } + + def jam(): Unit = { + val statuses = sys.statuses + if (sys.jammable && activeJammedMsgs.isEmpty) { + val indexed = statuses.indices.toList + //find the highest priority amongst the damaged status conditions + //if no damage, default to lowest priority - 0 + val highestPriorityDamagedStatus = indexed + .filter { i => damageStates(i).Condition > 0 } + .maxByOption { i => statuses(i).priority } match { + case Some(i) => statuses(i).priority + case None => 0 + } + //find all jammable statuses with priority equal to or greater than the highest priority + //ignore all statuses where its current damage state is higher than its jam state (jamming does nothing) + //turn the resulting statuses into packets + val indices = indexed + .filter { i => + val status = statuses(i) + status.jammable && + status.priority >= highestPriorityDamagedStatus && + status.jamState > damageStates(i).Condition + } + activeJammedMsgs = indices + indices.foreach { i => damageStates(i).jam() } + Jammed = indices.nonEmpty + } + } + + def unjam(): Unit = { + if (activeJammedMsgs.nonEmpty) { + activeJammedMsgs.foreach { i => damageStates(i).jam() } + activeJammedMsgs = Nil + Jammed = false + } + } + + def messagesForStatus(statusName: String, vehicle: Vehicle): List[PlanetSideGamePacket] = { + sys.statuses.indexWhere { _.name.contains(statusName) } match { + case -1 => Nil + case n => sys.statuses(n).getMessage(damageStates(n).Condition, vehicle) + } + } + + def stateOfStatus(statusName: String): Option[Boolean] = { + damageStates.find { _.status.name.contains(statusName) } match { + case Some(status) => Some(status.Condition == 0) + case _ => None + } + } + + def multiplierOfStatus(statusName: String, defaultMultiplier: Float = 1f): Float = { + sys.statuses.indexWhere { _.name.contains(statusName) } match { + case -1 => + defaultMultiplier + case n => + val status = sys.statuses(n) + if (Jammed && status.jammable) { + status.effects(status.jamState).getMultiplier() + } else { + status.effects(damageStates(n).Condition).getMultiplier() + } + } + } + + def jammerMessages(vehicle: Vehicle): List[PlanetSideGamePacket] = { + val statuses = sys.statuses + val sysIndices = if (activeJammedMsgs.isEmpty) { + val indexed = statuses.indices.toList + //find the highest priority amongst the damaged status conditions + //if no damage, default to lowest priority - 0 + val highestPriorityDamagedStatus = indexed + .filter { i => damageStates(i).Condition > 0 } + .maxByOption { i => statuses(i).priority } match { + case Some(i) => statuses(i).priority + case None => 0 + } + //find all jammable statuses with priority equal to or greater than the highest priority + //ignore all statuses where its current damage state is higher than its jam state (jamming does nothing) + //turn the resulting statuses into packets + val indices = indexed + .filter { i => + val status = statuses(i) + status.jammable && + status.priority >= highestPriorityDamagedStatus && + status.jamState > damageStates(i).Condition + } + activeJammedMsgs = indices + indices + } else { + activeJammedMsgs + } + val msgs = sysIndices.flatMap { i => statuses(i).jammerMessages(toState = -1, vehicle) } + activeJammedMsgs = sysIndices + Jammed = msgs.nonEmpty + msgs + } + + def clearJammerMessages(vehicle: Vehicle): List[PlanetSideGamePacket] = { + if (Jammed) { + Jammed = false + val statuses = sys.statuses + val clearMsgs = activeJammedMsgs.flatMap { i => statuses(i).clearJammerMessages(damageStates(i).Condition, vehicle) } + activeJammedMsgs = Nil + changedWhilejammed = Array.emptyIntArray + clearMsgs + } else { + Nil + } + } + + /** + * Produce packets that are tailored to the current active situation of the subsystem. + * When the subsystem is jammed, report packets that reflect the jammed conditions. + * When not jammed, report any condition that is not neutral / normal. + * @param vehicle the vehicle in which the subsystem module is operating + * @return game packets that reflect the condition + */ + def getMessage(vehicle: Vehicle): List[PlanetSideGamePacket] = { + if (Jammed) { + jammerMessages(vehicle) + } else { + damageStates + .zipWithIndex + .collect { case (state, index) if state.Condition > 0 => sys.statuses(index).getMessage(state.Condition, vehicle) } + .flatten + .toList + } + } + + /** + * Regardless of meta-conditions surrounding the subsystem, + * always try to produce packets that report the current situation of the subsystem. + * May return a condition status "update" that does not actually change anything. + * @param vehicle the vehicle in which the subsystem module is operating + * @return game packets that reflect the condition + */ + def currentMessages(vehicle: Vehicle): List[PlanetSideGamePacket] = { + toPacketList(damageStates.zipWithIndex, vehicle) + } + + def changedMessages(vehicle: Vehicle): List[PlanetSideGamePacket] = { + toPacketList(damageStates.zipWithIndex.filter { case (status, _) => status.wasChanged }, vehicle, always = true) + } + + def specificStatusMessage(name: String, vehicle: Vehicle): List[PlanetSideGamePacket] = { + damageStates.zipWithIndex.find { case (status, _) => status.status.name.contains(name) } match { + case Some(pair) => + toPacketList(List(pair), vehicle) + case _ => + Nil + } + } + + private def toPacketList( + list: Iterable[(VehicleSubsystemStatusMonitor, Int)], + vehicle: Vehicle, + always: Boolean = false + ): List[PlanetSideGamePacket] = { + list.flatMap { case (state: VehicleSubsystemStatusMonitor, index) => + if (Jammed && state.status.jammable) { + sys.statuses(index).getMessage(state.status.jamState, vehicle) + } else if (state.Condition > 0 || always) { + sys.statuses(index).getMessage(state.Condition, vehicle) + } else { + Nil + } + } + .toList + } +} + +class VehicleSubsystemStatusMonitor( + val status: VehicleSubsystemStatus, + val index: Int + ) { + private var condition: Int = 0 + private var changed: Boolean = false + + def Condition: Int = condition + + def Condition_=(state: Int): Int = { + changed = state != condition + condition = state + Condition + } + + def jam(): Unit = { + changed = status.jammable + } + + def Changed: Boolean = changed + + /** + * A kind of temporary meta-filter. + * If state change occurred to this subsystem status's condition, the flag will be set. + * When polled like this, the current state of the flag will be tested and returned, + * but the flag will then be cleared too. + * Subsequent tests should not observe this flag until another condition change. + * @return whether or not a state changed since the last time this status was tested for a state change + */ + def wasChanged: Boolean = { + val originalValue = changed + changed = false + originalValue + } +} diff --git a/src/main/scala/net/psforever/objects/vehicles/control/AntControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/AntControl.scala index d682fafe6..32cc6f0c1 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/AntControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/AntControl.scala @@ -21,9 +21,6 @@ class AntControl(vehicle: Vehicle) with AntTransferBehavior { def ChargeTransferObject = vehicle - findChargeTargetFunc = Vehicles.FindANTChargingSource - findDischargeTargetFunc = Vehicles.FindANTDischargingTarget - override def commonEnabledBehavior: Receive = super.commonEnabledBehavior.orElse(antBehavior) /** diff --git a/src/main/scala/net/psforever/objects/vehicles/control/ApcControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/ApcControl.scala index 5655ec931..c08b7a432 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/ApcControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/ApcControl.scala @@ -2,15 +2,17 @@ package net.psforever.objects.vehicles.control import net.psforever.objects._ +import net.psforever.objects.equipment.{EffectTarget, TargetValidation} import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.vital.base.DamageType import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.vital.projectile.MaxDistanceCutoff +import net.psforever.objects.vital.prop.DamageWithPosition import net.psforever.objects.zones.Zone import net.psforever.packet.game.{TriggerEffectMessage, TriggeredEffectLocation} import net.psforever.services.Service import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ +import net.psforever.types.PlanetSideGUID /** * A vehicle control agency exclusive to the armored personnel carrier (APC) ground transport vehicles. @@ -20,97 +22,112 @@ import scala.concurrent.duration._ * @param vehicle the APC */ class ApcControl(vehicle: Vehicle) - extends VehicleControl(vehicle) { - protected var capacitor = Default.Cancellable - - startCapacitorTimer() + extends VehicleControl(vehicle) + with VehicleCapacitance { + def CapacitanceObject: Vehicle = vehicle override def postStop() : Unit = { super.postStop() - capacitor.cancel() + capacitancePostStop() } - override def commonEnabledBehavior : Receive = - super.commonEnabledBehavior - .orElse { - case ApcControl.CapacitorCharge(amount) => - if (vehicle.Capacitor < vehicle.Definition.MaxCapacitor) { - val capacitance = vehicle.Capacitor += amount - vehicle.Zone.VehicleEvents ! VehicleServiceMessage( - self.toString(), - VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vehicle.GUID, 113, capacitance) - ) - startCapacitorTimer() - } else { - capacitor = Default.Cancellable - } + override def commonEnabledBehavior : Receive = super.commonEnabledBehavior + .orElse(capacitorBehavior) + .orElse { + case SpecialEmp.Burst() => + performEmpBurst() - case SpecialEmp.Burst() => - if (vehicle.Capacitor == vehicle.Definition.MaxCapacitor) { //only if the capacitor is full - val zone = vehicle.Zone - val events = zone.VehicleEvents - val pos = vehicle.Position - val GUID0 = Service.defaultPlayerGUID - val emp = vehicle.Definition.innateDamage.getOrElse { SpecialEmp.emp } - val faction = vehicle.Faction - //drain the capacitor - vehicle.Capacitor = 0 - events ! VehicleServiceMessage( - self.toString(), - VehicleAction.PlanetsideAttribute(GUID0, vehicle.GUID, 113, 0) - ) - //cause the emp - events ! VehicleServiceMessage( - zone.id, - VehicleAction.SendResponse( - GUID0, - TriggerEffectMessage( - GUID0, - s"apc_explosion_emp_${faction.toString.toLowerCase}", - None, - Some(TriggeredEffectLocation(pos, vehicle.Orientation)) - ) - ) - ) - //resolve what targets are affected by the emp - Zone.serverSideDamage( - zone, - vehicle, - emp, - SpecialEmp.createEmpInteraction(emp, pos), - ExplosiveDeployableControl.detectionForExplosiveSource(vehicle), - Zone.findAllTargets - ) - //start charging again - startCapacitorTimer() - } - } + case _ => ; + } + + def performEmpBurst(): Unit = { + val obj = CapacitanceObject + if (obj.Capacitor == obj.Definition.MaxCapacitor) { //only if the capacitor is full + val zone = obj.Zone + val events = zone.VehicleEvents + val pos = obj.Position + val GUID0 = Service.defaultPlayerGUID + val emp = ApcControl.apc_emp + val faction = obj.Faction + //drain the capacitor + capacitorCharge(-vehicle.Capacitor) + //cause the emp + events ! VehicleServiceMessage( + zone.id, + VehicleAction.SendResponse( + GUID0, + TriggerEffectMessage( + GUID0, + s"apc_explosion_emp_${faction.toString.toLowerCase}", + None, + Some(TriggeredEffectLocation(pos, obj.Orientation)) + ) + ) + ) + //resolve what targets are affected by the emp + Zone.serverSideDamage( + zone, + obj, + emp, + SpecialEmp.createEmpInteraction(emp, pos), + ExplosiveDeployableControl.detectionForExplosiveSource(obj), + Zone.findAllTargets + ) + //start charging again + //startCapacitorTimer() + } + } override def PrepareForDisabled(kickPassengers: Boolean) : Unit = { super.PrepareForDisabled(kickPassengers) - capacitor.cancel() + capacitanceStop() } override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = { super.DestructionAwareness(target, cause) - capacitor.cancel() - vehicle.Capacitor = 0 + capacitancePostStop() } - //TODO switch from magic numbers to definition numbers? - private def startCapacitorTimer(): Unit = { - capacitor = context.system.scheduler.scheduleOnce( - delay = 1000 millisecond, - self, - ApcControl.CapacitorCharge(10) - ) + override def parseObjectAction(guid: PlanetSideGUID, action: Int, other: Option[Any]): Unit = { + super.parseObjectAction(guid, action, other) + if (action == 55) { + performEmpBurst() + } } } object ApcControl { - /** - * Charge the vehicle's internal capacitor by the given amount during the schedulefd charge event. - * @param amount how much energy in the charge - */ - private case class CapacitorCharge(amount: Int) + final val apc_emp = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 0 + DamageAtEdge = 1.0f + DamageRadius = 15f + AdditionalEffect = true + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Player, + EffectTarget.Validation.Player + ) -> 1000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.AMS + ) -> 5000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.MotionSensor + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.Spitfire + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Turret, + EffectTarget.Validation.Turret + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.VehicleNotAMS + ) -> 10000 + Modifiers = MaxDistanceCutoff + } } diff --git a/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala new file mode 100644 index 000000000..e87fb0015 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala @@ -0,0 +1,631 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles.control + +import akka.actor.Cancellable +import net.psforever.objects._ +import net.psforever.objects.ballistics.VehicleSource +import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition} +import net.psforever.objects.equipment._ +import net.psforever.objects.inventory.{GridInventory, InventoryItem} +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.serverobject.containable.ContainableBehavior +import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.serverobject.transfer.TransferBehavior +import net.psforever.objects.vehicles._ +import net.psforever.objects.vital.VehicleShieldCharge +import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.zones.Zone +import net.psforever.packet.game._ +import net.psforever.services.Service +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types._ + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +/** + * A vehicle control agency exclusive to the battleframe robotics (BFR) combat vehicle system. + * @param vehicle the battleframe robotics unit + */ +class BfrControl(vehicle: Vehicle) + extends VehicleControl(vehicle) + with BfrTransferBehavior + with ArmorSiphonBehavior.SiphonOwner { + /** shield-auto charge */ + var shieldCharge: Cancellable = Default.Cancellable + + def SiphoningObject = vehicle + + def ChargeTransferObject = vehicle + + if (vehicle.Shields < vehicle.MaxShields) { + chargeShields(amount = 0) //start charging if starts as uncharged + } + + override def postStop(): Unit = { + super.postStop() + shieldCharge.cancel() + repairPostStop() + } + + def explosionBehavior: Receive = { + case BfrControl.VehicleExplosion => + val guid = vehicle.GUID + val guid0 = Service.defaultPlayerGUID + val zone = vehicle.Zone + val zoneid = zone.id + val events = zone.VehicleEvents + events ! VehicleServiceMessage( + zoneid, + VehicleAction.GenericObjectAction(guid0, guid, 46) + ) + context.system.scheduler.scheduleOnce(delay = 500 milliseconds, self, BfrControl.VehicleExplosion) + } + + override def commonEnabledBehavior: Receive = super.commonEnabledBehavior + .orElse(siphonRepairBehavior) + .orElse(bfrBehavior) + .orElse(explosionBehavior) + .orElse { + case CommonMessages.Use(_, Some(item: Tool)) => + if (GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition)) { + context.system.scheduler.scheduleOnce( + delay = 1000 milliseconds, + self, + TransferBehavior.Charging(Ntu.Nanites) + ) + } + + case SpecialEmp.Burst() => + performEmpBurst() + } + + override def commonDisabledBehavior: Receive = super.commonDisabledBehavior.orElse(explosionBehavior) + + override def PrepareForDisabled(kickPassengers: Boolean) : Unit = { + super.PrepareForDisabled(kickPassengers) + if (vehicle.Health == 0) { + //shield off + disableShield() + } + } + + override def DamageAwareness(target: Target, cause: DamageResult, amount: Any) : Unit = { + super.DamageAwareness(target, cause, amount) + //manage shield display and charge + disableShieldIfDrained() + if (shieldCharge != Default.Cancellable && vehicle.Shields < vehicle.MaxShields) { + shieldCharge.cancel() + shieldCharge = context.system.scheduler.scheduleOnce( + delay = vehicle.Definition.ShieldDamageDelay milliseconds, + self, + Vehicle.ChargeShields(0) + ) + } + } + + override def destructionDelayed(delay: Long, cause: DamageResult): Unit = { + super.destructionDelayed(delay, cause) + shieldCharge.cancel() + shieldCharge = Default.Cancellable + //harmless boom boom's + context.system.scheduler.scheduleOnce(delay = 0 milliseconds, self, BfrControl.VehicleExplosion) + } + + override def DestructionAwareness(target: Target, cause: DamageResult): Unit = { + super.DestructionAwareness(target, cause) + shieldCharge.cancel() + shieldCharge = Default.Cancellable + disableShield() + } + + override def RemoveItemFromSlotCallback(item: Equipment, slot: Int): Unit = { + BfrControl.dimorphics.find { _.contains(item.Definition) } match { + case Some(dimorph) if vehicle.VisibleSlots.contains(slot) => //revert to a generic variant + Tool.LoadDefinition( + item.asInstanceOf[Tool], + dimorph.transform(Handiness.Generic).asInstanceOf[ToolDefinition] + ) + case _ => ; //no dimorphic entry; place as-is + } + val guid0 = PlanetSideGUID(0) + //if the weapon arm is disabled, enable it for later (makes life easy) + parseObjectAction(guid0, BfrControl.ArmState.Enabled, Some(slot)) + //enable the other arm weapon regardless + parseObjectAction(guid0, BfrControl.ArmState.Enabled, Some( + //budget logic: the arm weapons are "next to each other" index-wise + if (vehicle.Weapons.keys.min == slot) { slot + 1 } else { slot - 1 } + )) + super.RemoveItemFromSlotCallback(item, slot) + } + + override def PutItemInSlotCallback(item: Equipment, slot: Int): Unit = { + val definition = item.Definition + val handiness = BfrControl.dimorphics.find { _.contains(definition) } match { + case Some(dimorph) if vehicle.VisibleSlots.contains(slot) => //left-handed or right-handed variant + val handiness = bfrHandiness(slot) + Tool.LoadDefinition( + item.asInstanceOf[Tool], + dimorph.transform(handiness).asInstanceOf[ToolDefinition] + ) + handiness + case Some(dimorph) => //revert to a generic variant + Tool.LoadDefinition( + item.asInstanceOf[Tool], + dimorph.transform(Handiness.Generic).asInstanceOf[ToolDefinition] + ) + Handiness.Generic + case None => //no dimorphic entry; place as-is + Handiness.Generic + } + super.PutItemInSlotCallback(item, slot) + specialArmWeaponEquipManagement(item, slot, handiness) + } + + override def dismountCleanup(seatBeingDismounted: Int): Unit = { + super.dismountCleanup(seatBeingDismounted) + if (!vehicle.Seats.values.exists(_.isOccupied)) { + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { + case Some(subsys) => + if (vehicle.Shields > 0) { + vehicleSubsystemMessages( + if (subsys.Enabled && !subsys.Enabled_=(state = false)) { + //turn off shield visually + subsys.changedMessages(vehicle) + } else if (subsys.Jammed || subsys.stateOfStatus(statusName = "Damaged").contains(false)) { + //hard coded: shield is "off" functionally, turn off static effect and turn off standard shield swirl + ComponentDamageMessage(vehicle.GUID, SubsystemComponent.ShieldGeneratorOffline, None) +: + BattleframeShieldGeneratorOffline.getMessage(SubsystemComponent.ShieldGeneratorOffline, vehicle, vehicle.GUID) + } else { + //shield is already off visually + Nil + } + ) + } + case _ => ; + } + } + } + + override def mountCleanup(mount_point: Int, user: Player): Unit = { + super.mountCleanup(mount_point, user) + if (vehicle.Seats.values.exists(_.isOccupied)) { + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { + case Some(subsys) + if !subsys.Enabled && vehicle.Shields > 0 && subsys.Enabled_=(state = true) => + //if the shield is damaged, it does not turn on until the damaged is cleared + vehicleSubsystemMessages(subsys.changedMessages(vehicle)) + case _ => ; + } + } + } + + override def permitTerminalMessage(player: Player, msg: ItemTransactionMessage): Boolean = { + if (msg.transaction_type == TransactionType.Loadout) { + !vehicle.Jammed + } else { + true + } + } + + override def handleTerminalMessageVehicleLoadout( + player: Player, + definition: VehicleDefinition, + weapons: List[InventoryItem], + inventory: List[InventoryItem] + ): ( + List[(Equipment, PlanetSideGUID)], + List[InventoryItem], + List[(Equipment, PlanetSideGUID)], + List[InventoryItem] + ) = { + val vFaction = vehicle.Faction + val vWeapons = vehicle.Weapons + //remove old inventory + val oldInventory = vehicle.Inventory.Clear().map { case InventoryItem(obj, _) => (obj, obj.GUID) } + //"dropped" items are lost; if it doesn't go in the trunk, it vanishes into the nanite cloud + val (_, afterInventory) = inventory.partition(ContainableBehavior.DropPredicate(player)) + val pairedArmSubsys = pairedArmSubsystems() + val (oldWeapons, newWeapons, finalInventory) = if (GlobalDefinitions.isBattleFrameVehicle(definition)) { + //vehicles are both battleframes; weapons must be swapped properly + if(vWeapons.size == 3 && GlobalDefinitions.isBattleFrameFlightVehicle(definition)) { + //battleframe is a gunner variant but loadout spec is for flight variant + // remap the hands, ignore the gunner weapon mount, and refit the trunk + val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) + val afterWeapons = weapons + .map { item => item.start += 1; item } + (culledWeaponMounts(pairedArmSubsys.unzip._2), afterWeapons, stow) + } else if(vWeapons.size == 2 && GlobalDefinitions.isBattleFrameGunnerVehicle(definition)) { + //battleframe is a flight variant but loadout spec is for gunner variant + // remap the hands, shave the gunner mount from the spec, and refit the trunk + val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) + val afterWeapons = weapons + .filterNot { _.obj.Size == EquipmentSize.BFRGunnerWeapon } + .map { item => item.start -= 1; item } + (culledWeaponMounts(vWeapons.values), afterWeapons, stow) + } else { + //same variant type of battleframe + // place as-is + (culledWeaponMounts(vWeapons.values), weapons, afterInventory) + } + } + else { + //vehicle loadout is not for this vehicle; do not transfer over weapon ammo + if ( + vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset + ) { + (Nil, Nil, afterInventory) //trunk is the same dimensions, however + } + else { + //accommodate as much of inventory as possible + val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) + (Nil, Nil, stow) + } + } + finalInventory.foreach { + _.obj.Faction = vFaction + } + (oldWeapons, newWeapons, oldInventory, finalInventory) + } + + def culledWeaponMounts(values: Iterable[EquipmentSlot]): List[(Equipment, PlanetSideGUID)] = { + values.collect { case slot if slot.Equipment.nonEmpty => + val obj = slot.Equipment.get + slot.Equipment = None + (obj, obj.GUID) + }.toList + } + + def disableShieldIfDrained(): Unit = { + if (vehicle.Shields == 0) { + disableShield() + } + } + + def disableShield(): Unit = { + val zone = vehicle.Zone + zone.VehicleEvents ! VehicleServiceMessage( + s"${zone.id}", + VehicleAction.SendResponse(PlanetSideGUID(0), GenericObjectActionMessage(vehicle.GUID, 45)) + ) + } + + def enableShieldIfNotDrained(): Unit = { + if (vehicle.Shields > 0) { + enableShield() + } + } + + def enableShield(): Unit = { + val zone = vehicle.Zone + zone.VehicleEvents ! VehicleServiceMessage( + s"${zone.id}", + VehicleAction.SendResponse(PlanetSideGUID(0), GenericObjectActionMessage(vehicle.GUID, 44)) + ) + } + + override def chargeShields(amount: Int): Unit = { + chargeShieldsOnly(amount) + shieldCharge(vehicle.Shields, vehicle.Definition, delay = 0) //continue charge? + } + + def chargeShieldsOnly(amount: Int): Unit = { + val definition = vehicle.Definition + val before = vehicle.Shields + if (canChargeShields()) { + val chargeAmount = math.max(1, ((if (vehicle.DeploymentState == DriveState.Kneeling && vehicle.Seats(0).occupant.nonEmpty) { + definition.ShieldAutoRechargeSpecial + } else { + definition.ShieldAutoRecharge + }).getOrElse(amount) * vehicle.SubsystemStatusMultiplier(sys = "BattleframeShieldGenerator.RechargeRate")).toInt) + vehicle.Shields = before + chargeAmount + val after = vehicle.Shields + vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), after - before)) + showShieldCharge() + if (before == 0 && after > 0) { + enableShield() + } + } + } + + def shieldCharge(delay: Long): Unit = { + shieldCharge(vehicle.Shields, vehicle.Definition, delay) + } + + def shieldCharge(after:Int, definition: VehicleDefinition, delay: Long): Unit = { + shieldCharge.cancel() + if (after < definition.MaxShields && !vehicle.Jammed) { + shieldCharge = context.system.scheduler.scheduleOnce( + delay = definition.ShieldPeriodicDelay + delay milliseconds, + self, + Vehicle.ChargeShields(0) + ) + } else { + shieldCharge = Default.Cancellable + } + } + + def showShieldCharge(): Unit = { + val vguid = vehicle.GUID + val zone = vehicle.Zone + val shields = vehicle.Shields + zone.VehicleEvents ! VehicleServiceMessage( + s"${vehicle.Actor}", + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, vguid, vehicle.Definition.shieldUiAttribute, shields) + ) + } + + override def StartJammeredStatus(target: Any, dur: Int): Unit = { + super.StartJammeredStatus(target, dur) + //cancels shield charge timer + shieldCharge(after = 0, vehicle.Definition, delay = 0) + } + + override def CancelJammeredStatus(target: Any): Unit = { + super.CancelJammeredStatus(target) + //restarts shield charge timer + shieldCharge(vehicle.Shields, vehicle.Definition, delay = 100) + } + + override def JammableMountedWeaponsJammeredStatus(target: PlanetSideServerObject with MountedWeapons, statusCode: Int): Unit = { + /** bfr weapons do not jam the same way normal vehicle weapons do */ + } + + override def parseObjectAction(guid: PlanetSideGUID, action: Int, other: Option[Any]): Unit = { + super.parseObjectAction(guid, action, other) + if (action == BfrControl.ArmState.Enabled || action == BfrControl.ArmState.Disabled) { + //disable or enable fire control for the left arm weapon or for the right arm weapon + ((other match { + case Some(slot: Int) => (slot, bfrHandSubsystem(bfrHandiness(slot))) + case _ => + vehicle.Weapons.find { case (_, slot) => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid } match { + case Some((slot, _)) => (slot, bfrHandSubsystem(bfrHandiness(slot))) + case _ => (0, None) + } + }) match { + case out @ (_, Some(subsystem)) => + if (action == BfrControl.ArmState.Enabled && !subsystem.Enabled) { + subsystem.Enabled = true + out + } else if (action == BfrControl.ArmState.Disabled && subsystem.Enabled) { + subsystem.Enabled = false + out + } else { + (0, None) + } + case _ => + (0, None) + }) match { + case (slot, Some(_)) => + specialArmWeaponActiveManagement(slot) + val guid0 = Service.defaultPlayerGUID + val doNotSendTo = other match { + case Some(pguid: PlanetSideGUID) => pguid + case _ => guid0 + } + (if (guid == guid0) { + vehicle.Weapons(slot).Equipment match { + case Some(equip) => Some(equip.GUID) + case None => None + } + } else { + Some(guid) + }) match { + case Some(useThisGuid) => + val zone = vehicle.Zone + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.GenericObjectAction(doNotSendTo, useThisGuid, action) + ) + case _ => ; + } + case _ => ; + } + } + } + + def bfrHandiness(side: equipment.Hand): Int = { + if (side == Handiness.Left) 2 + else if (side == Handiness.Right) 3 + else throw new Exception("no hand associated with this slot") + } + + def bfrHandiness(slot: Int): equipment.Hand = { + //for the benefit of BFR equipment slots interacting with MoveItemMessage + if (slot == 2) Handiness.Left + else if (slot == 3) Handiness.Right + else Handiness.Generic + } + + def bfrHandSubsystem(side: equipment.Hand): Option[VehicleSubsystem] = { + //for the benefit of BFR equipment slots interacting with MoveItemMessage + side match { + case Handiness.Left => vehicle.Subsystems(VehicleSubsystemEntry.BattleframeLeftArm) + case Handiness.Right => vehicle.Subsystems(VehicleSubsystemEntry.BattleframeRightArm) + case _ => None + } + } + + def specialArmWeaponEquipManagement(item: Equipment, slot: Int, handiness: equipment.Hand): Unit = { + if (item.Size == EquipmentSize.BFRArmWeapon && vehicle.VisibleSlots.contains(slot)) { + val weapons = vehicle.Weapons + //budget logic: the arm weapons are "next to each other" index-wise + val firstArmSlot = vehicle.Weapons.keys.min + val otherArmSlot = if (firstArmSlot == slot) { + slot + 1 + } + else { + slot - 1 + } + val otherArmEquipment = weapons(otherArmSlot).Equipment + if ( { + val itemDef = item.Definition + GlobalDefinitions.isBattleFrameArmorSiphon(itemDef) || GlobalDefinitions.isBattleFrameNTUSiphon(itemDef) + } || + (otherArmEquipment match { + case Some(thing) => + //some equipment is attached to the other arm weapon mount + val otherDef = thing.Definition + GlobalDefinitions.isBattleFrameArmorSiphon(otherDef) || GlobalDefinitions.isBattleFrameNTUSiphon(otherDef) + case None => + false + }) + ) { + //installing a siphon; this siphon can safely be disabled + //alternately, installing normal equipment, but the other arm weapon is a siphon + parseObjectAction(PlanetSideGUID(0), BfrControl.ArmState.Enabled, Some(otherArmSlot)) //ensure enabled + parseObjectAction(item.GUID, BfrControl.ArmState.Disabled, Some(slot)) + } + } + } + + /** since `specialArmWeaponActiveManagement` is called from `parseObjectAction`, + * and `parseObjectAction` gets called in `specialArmWeaponActiveManagement`, + * kill endless logic loops before they can happen */ + var notSpecialManagingArmWeapon: Boolean = true + def specialArmWeaponActiveManagement(slotChanged: Int): Unit = { + if (notSpecialManagingArmWeapon) { + notSpecialManagingArmWeapon = false + val (thisArm, otherArm) = { + val pairedSystemsToSlots = pairedArmSlotSubsystems() + if (pairedSystemsToSlots.head._2._1 == slotChanged) { + (pairedSystemsToSlots.head, pairedSystemsToSlots(1)) + } + else { + (pairedSystemsToSlots(1), pairedSystemsToSlots.head) + } + } + if (thisArm._1.Enabled) { + //this arm weapon slot was enabled + if ({ + val (thisArmExists, thisArmIsSiphon) = thisArm._2._2.Equipment match { + case Some(thing) => + //some equipment is attached to the other arm weapon mount + val definition = thing.Definition + ( + true, + GlobalDefinitions.isBattleFrameArmorSiphon(definition) || GlobalDefinitions.isBattleFrameNTUSiphon(definition) + ) + case None => + (false, false) + } + val (otherArmExists, otherArmIsSiphon) = otherArm._2._2.Equipment match { + case Some(thing) => + //some equipment is attached to the other arm weapon mount + val definition = thing.Definition + ( + true, + GlobalDefinitions.isBattleFrameArmorSiphon(definition) || GlobalDefinitions.isBattleFrameNTUSiphon(definition) + ) + case None => + (false, false) + } + thisArmExists && otherArmExists && (thisArmIsSiphon || otherArmIsSiphon) + }) { + //both arms weapons are installed and at least one of them is a siphon + parseObjectAction(PlanetSideGUID(0), BfrControl.ArmState.Disabled, Some(otherArm._2._1)) + } + } + else { + //this arm weapon slot was disabled + thisArm._2._2.Equipment match { + case Some(item) => + parseObjectAction(item.GUID, BfrControl.ArmState.Enabled, Some(otherArm._2._1)) //other arm must be enabled + case None => + parseObjectAction(PlanetSideGUID(0), BfrControl.ArmState.Enabled, Some(thisArm._2._1)) //must stay enabled + } + } + notSpecialManagingArmWeapon = true + } + } + + def performEmpBurst(): Unit = { + val now = System.currentTimeMillis() + val obj = ChargeTransferObject + val zone = obj.Zone + val events = zone.VehicleEvents + val GUID0 = Service.defaultPlayerGUID + getNtuContainer() match { + case Some(siphon : NtuSiphon) + if GlobalDefinitions.isBattleFrameNTUSiphon(siphon.equipment.Definition) && + siphon.equipment.FireModeIndex == 1 && + siphon.NtuCapacitor > 29 => + val elapsedWait = now - siphon.equipment.lastDischarge + if (elapsedWait >= 30000) { + val pos = obj.Position + val emp = siphon.equipment.Projectile + val faction = obj.Faction + //need at least 30 ntu, so consume the charge + siphon.NtuCapacitor -= 30 + UpdateNtuUI(obj, siphon) + //cause the emp + siphon.equipment.lastDischarge = now + //TODO this is the apc emp effect; is there an ntu siphon emp effect? + events ! VehicleServiceMessage( + zone.id, + VehicleAction.SendResponse( + GUID0, + TriggerEffectMessage( + GUID0, + s"apc_explosion_emp_${faction.toString.toLowerCase}", + None, + Some(TriggeredEffectLocation(pos, obj.Orientation)) + ) + ) + ) + //resolve what targets are affected by the emp + Zone.serverSideDamage( + zone, + obj, + emp, + SpecialEmp.createEmpInteraction(emp, pos), + ExplosiveDeployableControl.detectionForExplosiveSource(obj), + Zone.findAllTargets + ) + } else { + //the siphon is not ready to dispatch another emp; chat message borrowed from kit use logic + //the client actually enforces a hard limit of 30s before it will react to use of the siphon emp mode + //it does not even dispatch the packet before that, making it rare if this precautionary message is seen + events ! VehicleServiceMessage( + obj.Seats(0).occupant.get.Name, + VehicleAction.SendResponse( + GUID0, + ChatMsg(ChatMessageType.UNK_225, wideContents = false, "", s"@TimeUntilNextUse^${30000 - elapsedWait}", None) + ) + ) + } + case _ => ; + } + } +} + +object BfrControl { + /** arm state values related to the `GenericObjectActionMessage` action codes */ + object ArmState extends Enumeration { + final val Enabled = 38 + final val Disabled = 39 + } + + private case object VehicleExplosion + + val dimorphics: List[EquipmentHandiness] = { + import GlobalDefinitions._ + List( + EquipmentHandiness(aphelion_armor_siphon, aphelion_armor_siphon_left, aphelion_armor_siphon_right), + EquipmentHandiness(aphelion_laser, aphelion_laser_left, aphelion_laser_right), + EquipmentHandiness(aphelion_ntu_siphon, aphelion_ntu_siphon_left, aphelion_ntu_siphon_right), + EquipmentHandiness(aphelion_ppa, aphelion_ppa_left, aphelion_ppa_right), + EquipmentHandiness(aphelion_starfire, aphelion_starfire_left, aphelion_starfire_right), + EquipmentHandiness(colossus_armor_siphon, colossus_armor_siphon_left, colossus_armor_siphon_right), + EquipmentHandiness(colossus_burster, colossus_burster_left, colossus_burster_right), + EquipmentHandiness(colossus_chaingun, colossus_chaingun_left, colossus_chaingun_right), + EquipmentHandiness(colossus_ntu_siphon, colossus_ntu_siphon_left, colossus_ntu_siphon_right), + EquipmentHandiness(colossus_tank_cannon, colossus_tank_cannon_left, colossus_tank_cannon_right), + EquipmentHandiness(peregrine_armor_siphon, peregrine_armor_siphon_left, peregrine_armor_siphon_right), + EquipmentHandiness(peregrine_dual_machine_gun, peregrine_dual_machine_gun_left, peregrine_dual_machine_gun_right), + EquipmentHandiness(peregrine_mechhammer, peregrine_mechhammer_left, peregrine_mechhammer_right), + EquipmentHandiness(peregrine_ntu_siphon, peregrine_ntu_siphon_left, peregrine_ntu_siphon_right), + EquipmentHandiness(peregrine_sparrow, peregrine_sparrow_left, peregrine_sparrow_right) + ) + } +} diff --git a/src/main/scala/net/psforever/objects/vehicles/control/BfrFlightControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/BfrFlightControl.scala new file mode 100644 index 000000000..144ecb7d9 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/control/BfrFlightControl.scala @@ -0,0 +1,148 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles.control + +import net.psforever.objects.equipment.Handiness +import net.psforever.objects.{Vehicle, equipment} +import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.vehicles.{VehicleSubsystem, VehicleSubsystemEntry} +import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.types.Vector3 + +/** + * ... + */ +class BfrFlightControl(vehicle: Vehicle) + extends BfrControl(vehicle) + with VehicleCapacitance { + def CapacitanceObject: Vehicle = vehicle + + var flying: Option[Boolean] = None + + override def postStop() : Unit = { + super.postStop() + capacitancePostStop() + } + + override def commonEnabledBehavior: Receive = super.commonEnabledBehavior + .orElse(capacitorBehavior) + .orElse { + case BfrFlight.Soaring(flightValue) => + val localFlyingValue = flying + vehicle.Flying = Some(flightValue) + //capacitor drain + if (vehicle.Capacitor > 0) { + val definition = vehicle.Definition + val (_, cdrain) = if (flightValue == 0 || flightValue == -0) { + (0, vehicle.Capacitor) + } else { + val vdrain = if (flightValue > 0) definition.CapacitorDrain else 0 + val hdrain = if ({ + val vec = vehicle.Velocity.getOrElse(Vector3.Zero).xy + vec.x > 0.5f || vec.y > 0.5f + }) definition.CapacitorDrainSpecial else 0 + (vdrain, vdrain + hdrain) + } + flying = Some(if (cdrain > 0) { + val modDrain = math.max(1, (cdrain * vehicle.SubsystemStatusMultiplier(sys = "BattleframeFlightPod.UseRate")).toInt) + if (super.capacitorOnlyCharge(-modDrain) || vehicle.Capacitor < vehicle.Definition.MaxCapacitor) { + startCapacitorTimer() + } + true + } else { + false + }) + } + //shield drain + if (vehicle.Shields > 0) { + vehicle.Definition.ShieldDrain match { + case Some(drain) if localFlyingValue.isEmpty => + //shields off + disableShield() + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = false + vehicle.Shields -= drain + showShieldCharge() + case None if localFlyingValue.isEmpty => + //shields off + disableShield() + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = false + case Some(drain) => + vehicle.Shields -= drain + showShieldCharge() + case _ => ; + } + } + if (vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightPod).get.Jammed) { + + } + + case BfrFlight.Landed => + if (flying.nonEmpty) { + flying = None + vehicle.Flying = None + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = true + if (vehicle.Shields > 0) { + enableShield() + } + shieldCharge(delay = 2000) + } + + case _ => ; + } + + override def PrepareForDisabled(kickPassengers: Boolean) : Unit = { + super.PrepareForDisabled(kickPassengers) + capacitanceStop() + } + + override def destructionDelayed(delay: Long, cause: DamageResult): Unit = { + super.destructionDelayed(delay, cause) + capacitanceStop() + } + + override def DestructionAwareness(target: Target, cause: DamageResult): Unit = { + super.DestructionAwareness(target, cause) + capacitancePostStop() + } + + override def chargeShieldsOnly(amount: Int): Unit = { + if (flying != null && (flying.isEmpty || flying.contains(false))) { + super.chargeShieldsOnly(amount) + } + } + + override protected def capacitorOnlyCharge(amount: Int): Boolean = { + if (flying.isEmpty || flying.contains(false)) { + val mod = math.max(1, amount * vehicle.SubsystemStatusMultiplier(sys = "BattleframeFlightPod.RechargeRate").toInt) + super.capacitorOnlyCharge(mod) + } else { + false + } + } + + override def bfrHandiness(side: equipment.Hand): Int = { + if (side == Handiness.Left) 1 + else if (side == Handiness.Right) 2 + else throw new Exception("no hand associated with this slot; caller screwed up") + } + + override def bfrHandiness(slot: Int): equipment.Hand = { + //for the benefit of BFR equipment slots interacting with MoveItemMessage + if (slot == 1) Handiness.Left + else if (slot == 2) Handiness.Right + else Handiness.Generic + } + + override def bfrHandSubsystem(side: equipment.Hand): Option[VehicleSubsystem] = { + //for the benefit of BFR equipment slots interacting with MoveItemMessage + side match { + case Handiness.Left => vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightLeftArm) + case Handiness.Right => vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightRightArm) + case _ => None + } + } +} + +object BfrFlight { + final case class Soaring(flyingValue: Int) + case object Landed +} diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleCapacitance.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleCapacitance.scala new file mode 100644 index 000000000..bb2ca9374 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleCapacitance.scala @@ -0,0 +1,87 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vehicles.control + +import akka.actor.Actor +import net.psforever.objects._ +import net.psforever.services.Service +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +/** + * ... + */ +trait VehicleCapacitance { + _: Actor => + def CapacitanceObject: Vehicle + + protected var capacitor = Default.Cancellable + + startCapacitorTimer() + + def capacitanceStop(): Unit = { + capacitor.cancel() + } + + def capacitanceStopAndBlank(): Unit = { + capacitor.cancel() + capacitor = Default.Cancellable + } + + def capacitancePostStop(): Unit = { + capacitanceStopAndBlank() + CapacitanceObject.Capacitor = 0 + } + + def capacitorBehavior: Receive = { + case VehicleCapacitance.CapacitorCharge(amount) => + capacitorCharge(amount) + } + + protected def capacitorCharge(amount: Int): Boolean = { + capacitorOnlyCharge(amount) + startCapacitorTimer() + true + } + + protected def capacitorOnlyCharge(amount: Int): Boolean = { + val obj = CapacitanceObject + val capacitorBefore = obj.Capacitor + val capacitorAfter = obj.Capacitor += amount + if (capacitorBefore != capacitorAfter) { + showCapacitorCharge() + true + } else { + false + } + } + + protected def showCapacitorCharge(): Unit = { + val obj = CapacitanceObject + obj.Zone.VehicleEvents ! VehicleServiceMessage( + self.toString(), + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, obj.GUID, 113, obj.Capacitor) + ) + } + + protected def startCapacitorTimer(): Unit = { + val obj = CapacitanceObject + if (obj.Capacitor < obj.Definition.MaxCapacitor) { + capacitor.cancel() + capacitor = context.system.scheduler.scheduleOnce( + delay = 1000 millisecond, + self, + VehicleCapacitance.CapacitorCharge(obj.Definition.CapacitorRecharge) + ) + } + } +} + +object VehicleCapacitance { + /** + * Charge the vehicle's internal capacitor by the given amount during the scheduled charge event. + * @param amount how much energy in the charge + */ + private case class CapacitorCharge(amount: Int) +} diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala index 6496d944e..8b82af8dd 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -1,15 +1,17 @@ // Copyright (c) 2017-2021 PSForever package net.psforever.objects.vehicles.control -import akka.actor.{Actor, Cancellable} +import akka.actor.Cancellable import net.psforever.actors.zone.ZoneActor import net.psforever.objects._ import net.psforever.objects.ballistics.VehicleSource +import net.psforever.objects.definition.VehicleDefinition +import net.psforever.objects.definition.converter.OCM import net.psforever.objects.entity.WorldEntity -import net.psforever.objects.equipment.{Equipment, EquipmentSlot, JammableMountedWeapons} +import net.psforever.objects.equipment.{ArmorSiphonBehavior, Equipment, EquipmentSlot, JammableMountedWeapons} import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.inventory.{GridInventory, InventoryItem} -import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObjectControl} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior} import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle} @@ -20,10 +22,11 @@ import net.psforever.objects.serverobject.repair.RepairableVehicle import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.vehicles._ import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} -import net.psforever.objects.vital.VehicleShieldCharge +import net.psforever.objects.vital.{DamagingActivity, VehicleShieldCharge, VitalsActivity} import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.etc.SuicideReason import net.psforever.objects.zones._ +import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game._ import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent import net.psforever.types._ @@ -43,10 +46,11 @@ import scala.concurrent.duration._ * @param vehicle the `Vehicle` object being governed */ class VehicleControl(vehicle: Vehicle) - extends Actor + extends ServerObjectControl with FactionAffinityBehavior.Check with MountableBehavior with DamageableVehicle + with ArmorSiphonBehavior.Target with RepairableVehicle with JammableMountedWeapons with ContainableBehavior @@ -64,6 +68,8 @@ class VehicleControl(vehicle: Vehicle) def DamageableObject = vehicle + def SiphonableObject = vehicle + def RepairableObject = vehicle def ContainerObject = vehicle @@ -85,6 +91,8 @@ class VehicleControl(vehicle: Vehicle) var decayTimer : Cancellable = Default.Cancellable /** becoming waterlogged, or drying out? */ var submergedCondition : Option[OxygenState] = None + /** ... */ + var passengerRadiationCloudTimer: Cancellable = Default.Cancellable def receive : Receive = Enabled @@ -102,8 +110,10 @@ class VehicleControl(vehicle: Vehicle) } def commonEnabledBehavior: Receive = checkBehavior + .orElse(attributeBehavior) .orElse(jammableBehavior) .orElse(takesDamage) + .orElse(siphoningBehavior) .orElse(canBeRepairedByNanoDispenser) .orElse(containerBehavior) .orElse(environmentBehavior) @@ -124,23 +134,26 @@ class VehicleControl(vehicle: Vehicle) dismountCleanup(seat_num) case Vehicle.ChargeShields(amount) => - val now : Long = System.currentTimeMillis() - //make certain vehicles don't charge shields too quickly - if ( - vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields && - !vehicle.History.exists(VehicleControl.LastShieldChargeOrDamage(now)) - ) { - vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), amount)) - vehicle.Shields = vehicle.Shields + amount - vehicle.Zone.VehicleEvents ! VehicleServiceMessage( - s"${vehicle.Actor}", - VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), vehicle.GUID, 68, vehicle.Shields) - ) - } + chargeShields(amount) case Vehicle.UpdateZoneInteractionProgressUI(player) => updateZoneInteractionProgressUI(player) + case Vehicle.UpdateSubsystemStates(toChannel, stateToResolve) => + val events = vehicle.Zone.VehicleEvents + val guid0 = Service.defaultPlayerGUID + (stateToResolve match { + case Some(state) => + vehicle.Subsystems().filter { _.Enabled == state } //only subsystems that are enabled or are disabled + case None => + vehicle.Subsystems() //all subsystems + }) + .flatMap { _.getMessage(vehicle) } + .foreach { pkt => + events ! VehicleServiceMessage(toChannel, VehicleAction.SendResponse(guid0, pkt)) + } + + case FactionAffinity.ConvertFactionAffinity(faction) => val originalAffinity = vehicle.Faction if (originalAffinity != (vehicle.Faction = faction)) { @@ -162,62 +175,29 @@ class VehicleControl(vehicle: Vehicle) } case Terminal.TerminalMessage(player, msg, reply) => - reply match { - case Terminal.VehicleLoadout(definition, weapons, inventory) => - org.log4s - .getLogger(vehicle.Definition.Name) - .info(s"changing vehicle equipment loadout to ${player.Name}'s option #${msg.unk1 + 1}") - //remove old inventory - val oldInventory = vehicle.Inventory.Clear().map { case InventoryItem(obj, _) => (obj, obj.GUID) } - //"dropped" items are lost; if it doesn't go in the trunk, it vanishes into the nanite cloud - val (_, afterInventory) = inventory.partition(ContainableBehavior.DropPredicate(player)) - val (oldWeapons, newWeapons, finalInventory) = if (vehicle.Definition == definition) { - //vehicles are the same type - //TODO want to completely swap weapons, but holster icon vanishes temporarily after swap - //TODO BFR arms must be swapped properly - // //remove old weapons - // val oldWeapons = vehicle.Weapons.values.collect { case slot if slot.Equipment.nonEmpty => - // val obj = slot.Equipment.get - // slot.Equipment = None - // (obj, obj.GUID) - // }.toList - // (oldWeapons, weapons, afterInventory) - //TODO for now, just refill ammo; assume weapons stay the same - vehicle.Weapons - .collect { case (_, slot: EquipmentSlot) if slot.Equipment.nonEmpty => slot.Equipment.get } - .collect { - case weapon: Tool => - weapon.AmmoSlots.foreach { ammo => ammo.Box.Capacity = ammo.MaxMagazine() } - } - (Nil, Nil, afterInventory) - } - else { - //vehicle loadout is not for this vehicle - //do not transfer over weapon ammo - if ( - vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset - ) { - (Nil, Nil, afterInventory) //trunk is the same dimensions, however - } - else { - //accommodate as much of inventory as possible - val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) - (Nil, Nil, stow) - } - } - finalInventory.foreach { - _.obj.Faction = vehicle.Faction - } - player.Zone.VehicleEvents ! VehicleServiceMessage( - player.Zone.id, - VehicleAction.ChangeLoadout(vehicle.GUID, oldWeapons, newWeapons, oldInventory, finalInventory) - ) - player.Zone.AvatarEvents ! AvatarServiceMessage( - player.Name, - AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true) - ) + val zone = vehicle.Zone + if (permitTerminalMessage(player, msg)) { + reply match { + case Terminal.VehicleLoadout(definition, weapons, inventory) => + log.info(s"changing vehicle equipment loadout to ${player.Name}'s option #${msg.unk1 + 1}") + val (oldWeapons, newWeapons, oldInventory, finalInventory) = + handleTerminalMessageVehicleLoadout(player, definition, weapons, inventory) + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.ChangeLoadout(vehicle.GUID, oldWeapons, newWeapons, oldInventory, finalInventory) + ) + zone.AvatarEvents ! AvatarServiceMessage( + player.Name, + AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true) + ) - case _ => ; + case _ => ; + } + } else { + zone.AvatarEvents ! AvatarServiceMessage( + player.Name, + AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, false) + ) } case VehicleControl.Disable() => @@ -248,6 +228,11 @@ class VehicleControl(vehicle: Vehicle) final def Enabled: Receive = commonEnabledBehavior .orElse { + case VehicleControl.RadiationTick => + vehicle.interaction().find { _.Type == RadiationInVehicleInteraction } match { + case Some(func) => func.interaction(vehicle.getInteractionSector(), vehicle) + case _ => ; + } case _ => ; } @@ -323,8 +308,8 @@ class VehicleControl(vehicle: Vehicle) user.avatar.vehicle = None } GainOwnership(user) //gain new ownership - } - else { + passengerRadiationCloudTimer.cancel() + } else { decaying = false decayTimer.cancel() } @@ -347,6 +332,14 @@ class VehicleControl(vehicle: Vehicle) if (!obj.Seats(0).isOccupied) { obj.Velocity = Some(Vector3.Zero) } + if (seatBeingDismounted == 0) { + passengerRadiationCloudTimer = context.system.scheduler.scheduleWithFixedDelay( + 250.milliseconds, + 250.milliseconds, + self, + VehicleControl.RadiationTick + ) + } if (!obj.Seats(seatBeingDismounted).isOccupied) { //seat was vacated //we were only owning the vehicle while we sat in its driver seat val canBeOwned = obj.Definition.CanBeOwned @@ -473,25 +466,19 @@ class VehicleControl(vehicle: Vehicle) } def PutItemInSlotCallback(item: Equipment, slot: Int): Unit = { - val obj = ContainerObject - val oguid = obj.GUID - val zone = obj.Zone - val channel = self.toString - val events = zone.VehicleEvents - val iguid = item.GUID - val definition = item.Definition + val obj = ContainerObject + val oguid = obj.GUID + val zone = obj.Zone + val channel = self.toString + val events = zone.VehicleEvents + val iguid = item.GUID item.Faction = obj.Faction events ! VehicleServiceMessage( //TODO when a new weapon, the equipment slot ui goes blank, but the weapon functions; remount vehicle to correct it if (obj.VisibleSlots.contains(slot)) zone.id else channel, VehicleAction.SendResponse( Service.defaultPlayerGUID, - ObjectCreateDetailedMessage( - definition.ObjectId, - iguid, - ObjectCreateMessageParent(oguid, slot), - definition.Packet.DetailedConstructorData(item).get - ) + OCM.detailed(item, ObjectCreateMessageParent(oguid, slot)) ) ) item match { @@ -504,7 +491,7 @@ class VehicleControl(vehicle: Vehicle) weapon.AmmoSlots.map { slot => slot.Box }.foreach { box => events ! VehicleServiceMessage( channel, - VehicleAction.InventoryState2(Service.defaultPlayerGUID, iguid, weapon.GUID, box.Capacity) + VehicleAction.InventoryState2(Service.defaultPlayerGUID, box.GUID, iguid, box.Capacity) ) } case _ => ; @@ -521,6 +508,70 @@ class VehicleControl(vehicle: Vehicle) ) } + def permitTerminalMessage(player: Player, msg: ItemTransactionMessage): Boolean = true + + def handleTerminalMessageVehicleLoadout( + player: Player, + definition: VehicleDefinition, + weapons: List[InventoryItem], + inventory: List[InventoryItem] + ): ( + List[(Equipment, PlanetSideGUID)], + List[InventoryItem], + List[(Equipment, PlanetSideGUID)], + List[InventoryItem] + ) = { + //remove old inventory + val oldInventory = vehicle.Inventory.Clear().map { case InventoryItem(obj, _) => (obj, obj.GUID) } + //"dropped" items are lost; if it doesn't go in the trunk, it vanishes into the nanite cloud + val (_, afterInventory) = inventory.partition(ContainableBehavior.DropPredicate(player)) + val (oldWeapons, newWeapons, finalInventory) = if (vehicle.Definition == definition) { + //vehicles are the same type; just refill ammo, assuming weapons stay the same + vehicle.Weapons + .collect { case (_, slot: EquipmentSlot) if slot.Equipment.nonEmpty => slot.Equipment.get } + .collect { + case weapon: Tool => + weapon.AmmoSlots.foreach { ammo => ammo.Box.Capacity = ammo.MaxMagazine() } + } + (Nil, Nil, afterInventory) + } + else { + //vehicle loadout is not for this vehicle; do not transfer over weapon ammo + if ( + vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset + ) { + (Nil, Nil, afterInventory) //trunk is the same dimensions, however + } + else { + //accommodate as much of inventory as possible + val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) + (Nil, Nil, stow) + } + } + finalInventory.foreach { + _.obj.Faction = vehicle.Faction + } + (oldWeapons, newWeapons, oldInventory, finalInventory) + } + + //make certain vehicles don't charge shields too quickly + def canChargeShields(): Boolean = { + val func: VitalsActivity => Boolean = VehicleControl.LastShieldChargeOrDamage(System.currentTimeMillis(), vehicle.Definition) + vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields && + !vehicle.History.exists(func) + } + + def chargeShields(amount: Int): Unit = { + if (canChargeShields()) { + vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), amount)) + vehicle.Shields = vehicle.Shields + amount + vehicle.Zone.VehicleEvents ! VehicleServiceMessage( + s"${vehicle.Actor}", + VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), vehicle.GUID, vehicle.Definition.shieldUiAttribute, vehicle.Shields) + ) + } + } + /** * Water causes vehicles to become disabled if they dive off too far, too deep. * Flying vehicles do not display progress towards being waterlogged. They just disable outright. @@ -717,11 +768,96 @@ class VehicleControl(vehicle: Vehicle) case None => ; } } + + override def parseAttribute(attribute: Int, value: Long, other: Option[Any]) : Unit = { + val vguid = vehicle.GUID + val (dname, dguid) = other match { + case Some(p: Player) => (p.Name, p.GUID) + case _ => (vehicle.OwnerName.getOrElse("The driver"), PlanetSideGUID(0)) + } + val zone = vehicle.Zone + if (9 < attribute && attribute < 14) { + vehicle.PermissionGroup(attribute, value) match { + case Some(allow) => + val group = AccessPermissionGroup(attribute - 10) + log.info(s"$dname changed ${vehicle.Definition.Name}'s access permission $group to $allow") + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.SeatPermissions(dguid, vguid, attribute, value) + ) + //kick players who should not be seated in the vehicle due to permission changes + if (allow == VehicleLockState.Locked) { //TODO only important permission atm + vehicle.Seats.foreach { + case (seatIndex, seat) => + seat.occupant match { + case Some(tplayer: Player) => + if (vehicle.SeatPermissionGroup(seatIndex).contains(group) && !tplayer.Name.equals(dname)) { //can not kick self + seat.unmount(tplayer) + tplayer.VehicleSeated = None + zone.VehicleEvents ! VehicleServiceMessage( + zone.id, + VehicleAction.KickPassenger(tplayer.GUID, 4, false, vguid) + ) + } + case _ => ; // No player seated + } + } + vehicle.CargoHolds.foreach { + case (cargoIndex, hold) => + hold.occupant match { + case Some(cargo) => + if (vehicle.SeatPermissionGroup(cargoIndex).contains(group)) { + //todo: this probably doesn't work for passengers within the cargo vehicle + // Instruct client to start bail dismount procedure + self ! DismountVehicleCargoMsg(dguid, cargo.GUID, true, false, false) + } + case None => ; // No vehicle in cargo + } + } + } + case None => ; + } + } else { + log.warn( + s"parseAttributes: unsupported change on $vguid - $attribute, $dname" + ) + } + } + + override def StartJammeredStatus(target: Any, dur: Int): Unit = { + super.StartJammeredStatus(target, dur) + val subsystems = vehicle.Subsystems() + if (!subsystems.exists { _.Jammed }) { + subsystems.foreach { _.jam() } + vehicleSubsystemMessages(subsystems.flatMap { _.changedMessages(vehicle) }) + } + } + + override def CancelJammeredStatus(target: Any): Unit = { + super.CancelJammeredStatus(target) + val subsystems = vehicle.Subsystems() + if (subsystems.exists { _.Jammed }) { + subsystems.foreach { _.unjam() } + vehicleSubsystemMessages(subsystems.flatMap { _.changedMessages(vehicle) }) + } + } + + def vehicleSubsystemMessages(messages: List[PlanetSideGamePacket]): Unit = { + val zone = vehicle.Zone + val zoneid = zone.id + val events = zone.VehicleEvents + val guid0 = Service.defaultPlayerGUID + messages.foreach { pkt => + events ! VehicleServiceMessage( + zoneid, + VehicleAction.SendResponse(guid0, pkt) + ) + } + } } object VehicleControl { - import net.psforever.objects.vital.{DamageFromProjectile, VehicleShieldCharge, VitalsActivity} - import scala.concurrent.duration._ + import net.psforever.objects.vital.{VehicleShieldCharge, VitalsActivity} private case class PrepareForDeletion() @@ -729,21 +865,22 @@ object VehicleControl { private case class Deletion() + private case object RadiationTick + final case class AssignOwnership(player: Option[Player]) /** * Determine if a given activity entry would invalidate the act of charging vehicle shields this tick. * @param now the current time (in nanoseconds) * @param act a `VitalsActivity` entry to test - * @return `true`, if the vehicle took damage in the last five seconds or - * charged shields in the last second; + * @return `true`, if the shield charge would be blocked; * `false`, otherwise */ - def LastShieldChargeOrDamage(now: Long)(act: VitalsActivity): Boolean = { + def LastShieldChargeOrDamage(now: Long, vdef: VehicleDefinition)(act: VitalsActivity): Boolean = { act match { - case DamageFromProjectile(data) => now - data.interaction.hitTime < (5 seconds).toMillis //damage delays next charge by 5s - case vsc: VehicleShieldCharge => now - vsc.time < (1 seconds).toMillis //previous charge delays next by 1s - case _ => false + case dact: DamagingActivity => now - dact.time < vdef.ShieldDamageDelay //damage delays next charge + case vsc: VehicleShieldCharge => now - vsc.time < vdef.ShieldPeriodicDelay //previous charge delays next + case _ => false } } } diff --git a/src/main/scala/net/psforever/objects/vital/StandardResistances.scala b/src/main/scala/net/psforever/objects/vital/StandardResistances.scala index 6a7f5cbc6..55d4f8ce5 100644 --- a/src/main/scala/net/psforever/objects/vital/StandardResistances.scala +++ b/src/main/scala/net/psforever/objects/vital/StandardResistances.scala @@ -74,6 +74,7 @@ object NoResistanceSelection extends ResistanceSelection { def Splash: ResistanceSelection.Format = NoResistance.Calculate def Lash: ResistanceSelection.Format = NoResistance.Calculate def Aggravated: ResistanceSelection.Format = NoResistance.Calculate + def Radiation: ResistanceSelection.Format = ResistanceSelection.None } object StandardInfantryResistance extends ResistanceSelection { @@ -81,6 +82,7 @@ object StandardInfantryResistance extends ResistanceSelection { def Splash: ResistanceSelection.Format = InfantrySplashResistance.Calculate def Lash: ResistanceSelection.Format = InfantryLashResistance.Calculate def Aggravated: ResistanceSelection.Format = InfantryAggravatedResistance.Calculate + def Radiation: ResistanceSelection.Format = InfantrySplashResistance.Calculate } object StandardVehicleResistance extends ResistanceSelection { @@ -88,6 +90,7 @@ object StandardVehicleResistance extends ResistanceSelection { def Splash: ResistanceSelection.Format = VehicleSplashResistance.Calculate def Lash: ResistanceSelection.Format = VehicleLashResistance.Calculate def Aggravated: ResistanceSelection.Format = VehicleAggravatedResistance.Calculate + def Radiation: ResistanceSelection.Format = ResistanceSelection.None } object StandardAmenityResistance extends ResistanceSelection { @@ -95,4 +98,5 @@ object StandardAmenityResistance extends ResistanceSelection { def Splash: ResistanceSelection.Format = AmenityHitResistance.Calculate def Lash: ResistanceSelection.Format = ResistanceSelection.None def Aggravated: ResistanceSelection.Format = ResistanceSelection.None + def Radiation: ResistanceSelection.Format = ResistanceSelection.None } diff --git a/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala b/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala index cf8d002fc..fe7de831b 100644 --- a/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala +++ b/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala @@ -33,6 +33,12 @@ object VehicleResolutions ResolutionCalculations.VehicleApplication ) +object BfrResolutions + extends DamageResistanceCalculations( + ResolutionCalculations.VehicleDamageAfterResist, + ResolutionCalculations.BfrApplication + ) + object SimpleResolutions extends DamageResistanceCalculations( ResolutionCalculations.VehicleDamageAfterResist, diff --git a/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala b/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala index 4f7408473..ff09d540c 100644 --- a/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala +++ b/src/main/scala/net/psforever/objects/vital/VitalsHistory.scala @@ -2,7 +2,7 @@ package net.psforever.objects.vital import net.psforever.objects.ballistics.{PlayerSource, VehicleSource} -import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition} +import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition} import net.psforever.objects.serverobject.terminals.TerminalDefinition import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason} @@ -15,7 +15,7 @@ trait VitalsActivity { } trait HealingActivity extends VitalsActivity { - def time: Long = System.currentTimeMillis() + val time: Long = System.currentTimeMillis() } trait DamagingActivity extends VitalsActivity { @@ -53,6 +53,9 @@ final case class RepairFromEquipment( final case class RepairFromTerm(term_def: TerminalDefinition, amount: Int) extends HealingActivity +final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, amount: Int) + extends HealingActivity + final case class VehicleShieldCharge(amount: Int) extends HealingActivity //TODO facility diff --git a/src/main/scala/net/psforever/objects/vital/base/DamageResolution.scala b/src/main/scala/net/psforever/objects/vital/base/DamageResolution.scala index f47e2b4a3..65181331e 100644 --- a/src/main/scala/net/psforever/objects/vital/base/DamageResolution.scala +++ b/src/main/scala/net/psforever/objects/vital/base/DamageResolution.scala @@ -31,6 +31,7 @@ object DamageResolution extends Enumeration { Explosion, //area of effect damage caused by an internal mechanism; unrelated to Splash Environmental, //died to environmental causes Suicide, //i don't want to be the one the battles always choose - Collision //went splat + Collision, //went splat + Radiation //it hurts to stand too close = Value } diff --git a/src/main/scala/net/psforever/objects/vital/base/DamageType.scala b/src/main/scala/net/psforever/objects/vital/base/DamageType.scala index 74a1a8a9f..a6aad0d7f 100644 --- a/src/main/scala/net/psforever/objects/vital/base/DamageType.scala +++ b/src/main/scala/net/psforever/objects/vital/base/DamageType.scala @@ -10,5 +10,5 @@ object DamageType extends Enumeration(1) { type Type = Value //"one" (numerical 1 in the ADB) corresponds to objects that explode - final val Direct, Splash, Lash, Radiation, Aggravated, One, None = Value + final val Direct, Splash, Lash, Radiation, Aggravated, One, Siphon, None = Value } diff --git a/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala b/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala index 3e090838a..c2bceeb6a 100644 --- a/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala +++ b/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala @@ -21,7 +21,7 @@ object DamageCalculations { def AgainstMaxSuit(profile : DamageProfile) : Int = profile.Damage3 - def AgainstBFR(profile : DamageProfile) : Int = profile.Damage4 + def AgainstBfr(profile : DamageProfile) : Int = profile.Damage4 /** * Get the damage value. diff --git a/src/main/scala/net/psforever/objects/vital/etc/ArmorSiphonReason.scala b/src/main/scala/net/psforever/objects/vital/etc/ArmorSiphonReason.scala new file mode 100644 index 000000000..687c4c074 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/etc/ArmorSiphonReason.scala @@ -0,0 +1,55 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vital.etc + +import net.psforever.objects.{GlobalDefinitions, Tool, Vehicle} +import net.psforever.objects.ballistics.SourceEntry +import net.psforever.objects.vital.base.{DamageModifiers, DamageReason, DamageResolution} +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.vital.prop.DamageWithPosition +import net.psforever.objects.vital.resolution.DamageResistanceModel +import net.psforever.types.Vector3 + +final case class ArmorSiphonReason( + hostVehicle: Vehicle, + siphon: Tool, + damageModel: DamageResistanceModel + ) extends DamageReason { + assert(GlobalDefinitions.isBattleFrameArmorSiphon(siphon.Definition), "acting entity is not an armor siphon") + + def source: DamageWithPosition = siphon.Projectile + + def resolution: DamageResolution.Value = DamageResolution.Resolved + + def same(test: DamageReason): Boolean = test match { + case asr: ArmorSiphonReason => (asr.hostVehicle eq hostVehicle) && (asr.siphon eq siphon) + case _ => false + } + + def adversary: Option[SourceEntry] = None + + override def attribution: Int = hostVehicle.Definition.ObjectId +} + +object ArmorSiphonModifiers { + trait Mod extends DamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: DamageReason): Int = { + cause match { + case o: ArmorSiphonReason => calculate(damage, data, o) + case _ => 0 + } + } + + def calculate(damage: Int, data: DamageInteraction, cause: ArmorSiphonReason): Int + } +} + +case object ArmorSiphonMaxDistanceCutoff extends ArmorSiphonModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: ArmorSiphonReason): Int = { + if (Vector3.DistanceSquared(data.target.Position, cause.hostVehicle.Position) < cause.source.DamageRadius * cause.source.DamageRadius) { + damage + } + else { + 0 + } + } +} diff --git a/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala b/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala index 1f310c279..6e9984cd8 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala @@ -16,7 +16,6 @@ import net.psforever.objects.zones.Zone * A wrapper for a "damage source" in damage calculations * that parameterizes information necessary to explain a server-driven explosion occurring. * Some game objects cause area-of-effect damage upon being destroyed. - * @see `VitalityDefinition.explodes` * @see `VitalityDefinition.innateDamage` * @see `Zone.causesExplosion` * @param entity what is accredited as the source of the explosive yield @@ -61,7 +60,7 @@ object ExplodingEntityReason { instigation: Option[DamageResult] ): ExplodingEntityReason = { val definition = entity.Definition.asInstanceOf[ObjectDefinition with VitalityDefinition] - assert(definition.explodes && definition.innateDamage.nonEmpty, "causal entity does not explode") + assert(definition.innateDamage.nonEmpty, "causal entity does not explode") ExplodingEntityReason(SourceEntry(entity), definition.innateDamage.get, damageModel, instigation) } } diff --git a/src/main/scala/net/psforever/objects/vital/etc/RadiationReason.scala b/src/main/scala/net/psforever/objects/vital/etc/RadiationReason.scala new file mode 100644 index 000000000..a69be08e6 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/etc/RadiationReason.scala @@ -0,0 +1,104 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.vital.etc + +import net.psforever.objects.ballistics.{PlayerSource, SourceEntry, Projectile => ActualProjectile} +import net.psforever.objects.vital.base._ +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.vital.projectile.{ProjectileDamageModifierFunctions, ProjectileReason} +import net.psforever.objects.vital.prop.DamageProperties +import net.psforever.objects.vital.resolution.DamageAndResistance + +/** + * A wrapper for a "damage source" in damage calculations + * that parameterizes information necessary to explain a radiation cloud. + * @param resolution how the damage is processed + * @param projectile the projectile that caused the damage + * @param damageModel the model to be utilized in these calculations; + * typically, but not always, defined by the target + * @param radiationShielding the amount of reduction to radiation damage that occurs due to external reasons; + * best utilized for protection extended to vehicle passengers + */ +final case class RadiationReason( + projectile: ActualProjectile, + damageModel: DamageAndResistance, + radiationShielding: Float + ) extends DamageReason { + def resolution: DamageResolution.Value = DamageResolution.Radiation + + def source: DamageProperties = projectile.profile + + def same(test: DamageReason): Boolean = { + test match { + case o: RadiationReason => o.projectile.id == projectile.id //can only be another projectile with the same uid + case _ => false + } + } + + def adversary: Option[SourceEntry] = Some(projectile.owner) + + override def unstructuredModifiers: List[DamageModifiers.Mod] = List(ShieldAgainstRadiation) + + override def attribution: Int = projectile.attribute_to +} + +object RadiationDamageModifiers { + trait Mod extends DamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: DamageReason): Int = { + cause match { + case o: RadiationReason => calculate(damage, data, o) + case _ => damage + } + } + + def calculate(damage: Int, data: DamageInteraction, cause: RadiationReason): Int + } +} + +/** + * If the damge is caused by a projectile that emits a field that permeates vehicle armor, + * determine by how much the traversed armor's shielding reduces the damage. + * Infantry take damage, reduced only if one is equipped with a mechanized assault exo-suit. + */ +case object ShieldAgainstRadiation extends RadiationDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: RadiationReason): Int = { + if (data.resolution == DamageResolution.Radiation) { + data.target match { + case _: PlayerSource => + damage - (damage * cause.radiationShielding).toInt + case _ => + 0 + } + } else { + damage + } + } +} + +/** + * The initial application of aggravated damage against an infantry target + * due to interaction with a radiation field + * where the specific damage component is `Splash`. + */ +case object InfantryAggravatedRadiation extends RadiationDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: RadiationReason): Int = { + ProjectileDamageModifierFunctions.baseAggravatedFormula( + DamageResolution.Radiation, + DamageType.Splash + )(damage, data, ProjectileReason(cause.resolution, cause.projectile, cause.damageModel)) + } +} + +/** + * The ongoing application of aggravated damage ticks against an infantry target + * due to interaction with a radiation field + * where the specific damage component is `Splash`. + * This is called "burning" regardless of what the active aura effect actually is. + */ +case object InfantryAggravatedRadiationBurn extends RadiationDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: RadiationReason): Int = { + ProjectileDamageModifierFunctions.baseAggravatedBurnFormula( + DamageResolution.Radiation, + DamageType.Splash + )(damage, data, ProjectileReason(cause.resolution, cause.projectile, cause.damageModel)) + } +} diff --git a/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala b/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala index 431f9a492..ab2a54902 100644 --- a/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala +++ b/src/main/scala/net/psforever/objects/vital/projectile/ProjectileDamageModifierFunctions.scala @@ -1,7 +1,7 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.vital.projectile -import net.psforever.objects.ballistics.{ChargeDamage, PlayerSource, ProjectileQuality} +import net.psforever.objects.ballistics._ import net.psforever.objects.equipment.ChargeFireModeDefinition import net.psforever.objects.vital.base._ import net.psforever.objects.vital.damage.DamageModifierFunctions @@ -329,6 +329,28 @@ case object FlailDistanceDamageBoost extends ProjectileDamageModifiers.Mod { } } +/** + * If the damge is caused by a projectile that emits a field that permeates vehicle armor, + * determine by how much the traversed armor's shielding reduces the damage. + * Infantry take damage, reduced only if one is equipped with a mechanized assault exo-suit. + */ +case object ShieldAgainstRadiation extends ProjectileDamageModifiers.Mod { + def calculate(damage: Int, data: DamageInteraction, cause: ProjectileReason): Int = { + if (data.resolution == DamageResolution.Radiation) { + data.target match { + case p: PlayerSource if p.ExoSuit == ExoSuitType.MAX => + damage - (damage * p.Modifiers.RadiationShielding).toInt + case _: PlayerSource => + damage + case _ => + 0 + } + } else { + damage + } + } +} + /* Functions */ object ProjectileDamageModifierFunctions { /** diff --git a/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala b/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala index 3d513ba8d..e3c85da3a 100644 --- a/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala +++ b/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala @@ -29,7 +29,8 @@ trait DamageProperties /** use a specific modifier as a part of damage calculations */ private var useDamage1Subtract: Boolean = false /** some other entity confers damage; - * a set value should not `None` and not `0` but is preferred to be the damager's uid */ + * a set value should be the damager's object uid + * usually corresponding to a projectile */ private var damageProxy: Option[Int] = None /** na; * currently used with jammer properties only */ diff --git a/src/main/scala/net/psforever/objects/vital/resistance/ResistanceSelection.scala b/src/main/scala/net/psforever/objects/vital/resistance/ResistanceSelection.scala index e1f6cd2bf..5daa3dc9d 100644 --- a/src/main/scala/net/psforever/objects/vital/resistance/ResistanceSelection.scala +++ b/src/main/scala/net/psforever/objects/vital/resistance/ResistanceSelection.scala @@ -14,12 +14,14 @@ trait ResistanceSelection { def Splash: ResistanceSelection.Format def Lash: ResistanceSelection.Format def Aggravated: ResistanceSelection.Format + def Radiation: ResistanceSelection.Format def apply(data: DamageInteraction) : ResistanceSelection.Format = data.cause.source.CausesDamageType match { case DamageType.Direct => Direct case DamageType.Splash => Splash case DamageType.Lash => Lash case DamageType.Aggravated => Aggravated + case DamageType.Radiation => Splash case _ => ResistanceSelection.None } @@ -28,6 +30,7 @@ trait ResistanceSelection { case DamageType.Splash => Splash case DamageType.Lash => Lash case DamageType.Aggravated => Aggravated + case DamageType.Radiation => Splash case _ => ResistanceSelection.None } } diff --git a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala index be9672e6e..a6daa6545 100644 --- a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala +++ b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala @@ -1,11 +1,12 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.vital.resolution -import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle} +import net.psforever.objects._ import net.psforever.objects.ballistics.{PlayerSource, SourceEntry} import net.psforever.objects.ce.Deployable import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.damage.Damageable +import net.psforever.objects.vehicles.VehicleSubsystemEntry import net.psforever.objects.vital.base.DamageResolution import net.psforever.objects.vital.{DamagingActivity, Vitality, VitalsHistory} import net.psforever.objects.vital.damage.DamageCalculations @@ -227,20 +228,36 @@ object ResolutionCalculations { val targetBefore = SourceEntry(target) target match { case vehicle: Vehicle if CanDamage(vehicle, damage, data) => - val shields = vehicle.Shields - if (shields > damage) { - vehicle.Shields = shields - damage - } else if (shields > 0) { - vehicle.Health = vehicle.Health - (damage - shields) - vehicle.Shields = 0 - } else { - vehicle.Health = vehicle.Health - damage - } + vehicleDamageAfterShieldTest( + vehicle, + damage, + { vehicle.Shields == 0 || data.cause.source.DamageToVehicleOnly } + ) case _ => ; } DamageResult(targetBefore, SourceEntry(target), data) } + def vehicleDamageAfterShieldTest( + vehicle: Vehicle, + damage: Int, + ignoreShieldsDamage: Boolean + ): Unit = { + val shields = vehicle.Shields + if (ignoreShieldsDamage) { + vehicle.Health = vehicle.Health - damage + } else { + if (shields > damage) { + vehicle.Shields = shields - damage + } else if (shields > 0) { + vehicle.Health = vehicle.Health - (damage - shields) + vehicle.Shields = 0 + } else { + vehicle.Health = vehicle.Health - damage + } + } + } + def SimpleApplication(damage: Int, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = { val targetBefore = SourceEntry(target) target match { @@ -326,6 +343,31 @@ object ResolutionCalculations { } } + def BfrApplication(damage: Int, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = { + val targetBefore = SourceEntry(target) + target match { + case obj: Vehicle + if CanDamage(obj, damage, data) && GlobalDefinitions.isBattleFrameVehicle(obj.Definition) => + vehicleDamageAfterShieldTest( + obj, + damage, + { + data.cause.source.DamageToBattleframeOnly || + data.cause.source.DamageToVehicleOnly || + !obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled || + obj.Shields == 0 + } + ) + DamageResult(targetBefore, SourceEntry(target), data) + + case _: Vehicle => + VehicleApplication(damage, data)(target) + + case _ => + DamageResult(targetBefore, SourceEntry(target), data) + } + } + private def noDoubleLash(target: PlanetSideGameObject with VitalsHistory, data: DamageInteraction): Boolean = { data.cause match { case reason: ProjectileReason if reason.resolution == DamageResolution.Lash => diff --git a/src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala b/src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala index c5341478d..7a059d59b 100644 --- a/src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala +++ b/src/main/scala/net/psforever/objects/zones/InteractsWithZone.scala @@ -2,11 +2,14 @@ package net.psforever.objects.zones import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.zones.blockmap.SectorPopulation trait InteractsWithZone extends PlanetSideServerObject { /** interactions for this particular entity is allowed */ private var _allowInteraction: Boolean = true + /** maximum interaction range used to generate the commonly tested sector */ + private var interactionRange: Float = 0.1f /** * If the interactive permissions of this entity change. @@ -24,7 +27,7 @@ trait InteractsWithZone _allowInteraction = permit if (before != permit) { if (permit) { - interactions.foreach { _.interaction(target = this) } + doInteractions() } else { interactions.foreach ( _.resetInteraction(target = this) ) } @@ -36,14 +39,26 @@ trait InteractsWithZone def interaction(func: ZoneInteraction): List[ZoneInteraction] = { interactions = interactions :+ func + if (func.range > interactionRange) { + interactionRange = func.range + } interactions } def interaction(): List[ZoneInteraction] = interactions + def getInteractionSector(): SectorPopulation = { + this.Zone.blockMap.sector(this.Position, interactionRange) + } + + def doInteractions(): Unit = { + val sector = getInteractionSector() + interactions.foreach { _.interaction(sector, target = this) } + } + def zoneInteractions(): Unit = { if (_allowInteraction) { - interactions.foreach { _.interaction(target = this) } + doInteractions() } } @@ -52,18 +67,31 @@ trait InteractsWithZone } } +trait ZoneInteractionType + /** * The basic behavior of an entity in a zone. * @see `InteractsWithZone` * @see `Zone` */ trait ZoneInteraction { + /** + * A categorical descriptor for this interaction. + */ + def Type: ZoneInteractionType + + /** + * The anticipated (radial?) distance across which this interaction affects the zone's blockmap. + */ + def range: Float + /** * The method by which zone interactions are tested. * How a target tests this interaction with elements of the target's zone. + * @param sector the portion of the block map being tested * @param target the fixed element in this test */ - def interaction(target: InteractsWithZone): Unit + def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit /** * Suspend any current interaction procedures. diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index f775935f2..032e02c07 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -3,7 +3,7 @@ package net.psforever.objects.zones import akka.actor.{ActorContext, ActorRef, Props} import net.psforever.objects.{PlanetSideGameObject, _} -import net.psforever.objects.ballistics.SourceEntry +import net.psforever.objects.ballistics.{Projectile, SourceEntry} import net.psforever.objects.ce.Deployable import net.psforever.objects.equipment.Equipment import net.psforever.objects.guid.{NumberPoolHub, UniqueNumberOps, UniqueNumberSetup} @@ -12,10 +12,10 @@ import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.inventory.Container import net.psforever.objects.serverobject.painbox.{Painbox, PainboxDefinition} import net.psforever.objects.serverobject.resourcesilo.ResourceSilo -import net.psforever.objects.serverobject.structures.{Amenity, AmenityOwner, Building, StructureType, WarpGate} +import net.psforever.objects.serverobject.structures._ import net.psforever.objects.serverobject.turret.FacilityTurret import net.psforever.objects.serverobject.zipline.ZipLinePath -import net.psforever.types.{DriveState, PlanetSideEmpire, PlanetSideGUID, SpawnGroup, Vector3} +import net.psforever.types._ import org.log4s.Logger import net.psforever.services.avatar.AvatarService import net.psforever.services.local.LocalService @@ -124,6 +124,9 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { */ private val corpses: ListBuffer[Player] = ListBuffer[Player]() + private var projectiles: ActorRef = Default.Actor + private val projectileList: ListBuffer[Projectile] = ListBuffer[Projectile]() + /** */ private var population: ActorRef = Default.Actor @@ -189,6 +192,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { context.actorOf(Props(classOf[UniqueNumberSys], this, this.guid), s"zone-$id-uns") ground = context.actorOf(Props(classOf[ZoneGroundActor], this, equipmentOnGround), s"zone-$id-ground") deployables = context.actorOf(Props(classOf[ZoneDeployableActor], this, constructions), s"zone-$id-deployables") + projectiles = context.actorOf(Props(classOf[ZoneProjectileActor], this, projectileList), s"zone-$id-projectiles") transport = context.actorOf(Props(classOf[ZoneVehicleActor], this, vehicles), s"zone-$id-vehicles") population = context.actorOf(Props(classOf[ZonePopulationActor], this, players, corpses), s"zone-$id-players") projector = context.actorOf( @@ -540,6 +544,10 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { def Deployables: ActorRef = deployables + def Projectile: ActorRef = projectiles + + def Projectiles: List[Projectile] = projectileList.toList + def Transport: ActorRef = transport def Population: ActorRef = population @@ -1061,11 +1069,11 @@ object Zone { * and a token that qualifies the current location of the object in the zone is returned. * The following groups of objects are searched: * the inventories of all players and all corpses, - * all vehicles trunks, + * all vehicles weapon mounts and trunks, * the lockers of all players and corpses; * and, if still not found, the ground is scoured too. - * @see `ItemLocation`
- * `LockerContainer` + * @see `ItemLocation` + * @see `LockerContainer` * @param equipment the target object * @param guid that target object's globally unique identifier * @param continent the zone whose objects to search diff --git a/src/main/scala/net/psforever/objects/zones/ZoneProjectileActor.scala b/src/main/scala/net/psforever/objects/zones/ZoneProjectileActor.scala new file mode 100644 index 000000000..f8c5b0186 --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/ZoneProjectileActor.scala @@ -0,0 +1,224 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.zones + +import akka.actor.{Actor, Cancellable} +import net.psforever.objects.ballistics.Projectile +import net.psforever.objects.guid.{GUIDTask, StraightforwardTask, TaskBundle, TaskWorkflow} +import net.psforever.services.Service +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.types.PlanetSideGUID + +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.concurrent.duration._ + +/** + * Synchronize management of the list of some `Projectile`s maintained by a zone. + * @param zone the zone being represented + * @param projectileList the zone's projectile list + */ +class ZoneProjectileActor( + zone: Zone, + projectileList: mutable.ListBuffer[Projectile] + ) extends Actor { + /** a series of timers matched against projectile unique identifiers, + * marking the maximum lifespan of the projectile */ + val projectileLifespan: mutable.HashMap[PlanetSideGUID, Cancellable] = new mutable.HashMap[PlanetSideGUID, Cancellable] + + override def postStop() : Unit = { + projectileLifespan.values.foreach { _.cancel() } + projectileList.iterator.filter(_.HasGUID).foreach { p => cleanUpRemoteProjectile(p.GUID, p) } + projectileList.clear() + } + + def receive: Receive = { + case ZoneProjectile.Add(filterGuid, projectile) => + if (projectile.Definition.ExistsOnRemoteClients) { + if (projectile.HasGUID) { + cleanUpRemoteProjectile(projectile.GUID, projectile) + TaskWorkflow.execute(reregisterProjectile(filterGuid, projectile)) + } else { + TaskWorkflow.execute(registerProjectile(filterGuid, projectile)) + } + } + + case ZoneProjectile.Remove(guid) => + projectileList.find(_.GUID == guid) match { + case Some(projectile) => + cleanUpRemoteProjectile(guid, projectile) + TaskWorkflow.execute(unregisterProjectile(projectile)) + case _ => + projectileLifespan.remove(guid) + //if we can't find this projectile by guid, remove any projectiles that are unregistered + val (in, out) = projectileList.filter(_.HasGUID).partition { p => zone.GUID(p.GUID).nonEmpty } + projectileList.clear() + projectileList.addAll(in) + out.foreach { p => + cleanUpRemoteProjectile(p.GUID, p) + } + } + + case _ => ; + } + + /** + * Construct tasking that adds a completed but unregistered projectile into the scene. + * After the projectile is registered to the curent zone's global unique identifier system, + * all connected clients save for the one that registered it will be informed about the projectile's "creation." + * @param obj the projectile to be registered + * @return a `TaskBundle` message + */ + private def registerProjectile(filterGuid: PlanetSideGUID, obj: Projectile): TaskBundle = { + TaskBundle( + new StraightforwardTask() { + private val filter = filterGuid + private val globalProjectile = obj + private val func: (PlanetSideGUID, PlanetSideGUID, Projectile) => Unit = loadedRemoteProjectile + + override def description(): String = s"register a ${globalProjectile.profile.Name}" + + def action(): Future[Any] = { + func(filter, globalProjectile.GUID, globalProjectile) + Future(true) + } + }, + List(GUIDTask.registerObject(zone.GUID, obj)) + ) + } + + /** + * Construct tasking that removes a formerly complete and currently registered projectile from the scene. + * After the projectile is unregistered from the curent zone's global unique identifier system, + * all connected clients save for the one that registered it will be informed about the projectile's "destruction." + * @param obj the projectile to be unregistered + * @return a `TaskBundle` message + */ + private def unregisterProjectile(obj: Projectile): TaskBundle = GUIDTask.unregisterObject(zone.GUID, obj) + + /** + * If the projectile object is unregistered, register it. + * If the projectile object is already registered, unregister it and then register it again. + * @see `registerProjectile(Projectile)` + * @see `unregisterProjectile(Projectile)` + * @param obj the projectile to be registered (a second time?) + * @return a `TaskBundle` message + */ + def reregisterProjectile(filterGuid: PlanetSideGUID, obj: Projectile): TaskBundle = { + val reg = registerProjectile(filterGuid, obj) + if (obj.HasGUID) { + TaskBundle( + reg.mainTask, + TaskBundle( + reg.subTasks(0).mainTask, + unregisterProjectile(obj) + ) + ) + } else { + reg + } + } + + /** + * For a given registered remote projectile, + * perform all the actions necessary to properly integrate it into the management system.
+ *
+ * Those actions involve:
+ * - determine whether or not the default filter needs to be applied,
+ * - add the projectile to the zone managing list,
+ * - if the projectile is a radiation cloud, add it to the zone blockmap
+ * - set up the internal disposal timer, and
+ * - dispatch a message to introduce the projectile to the game world. + * @param filterGuid a unique identifier filtering messages from a certain recipient + * @param projectileGuid the projectile unique identifier that was assigned by the zone's unique number system + * @param projectile the projectile being included + */ + def loadedRemoteProjectile( + filterGuid: PlanetSideGUID, + projectileGuid: PlanetSideGUID, + projectile: Projectile + ): Unit = { + val definition = projectile.Definition + projectileList.addOne(projectile) + val (clarifiedFilterGuid, duration) = if (definition.radiation_cloud) { + zone.blockMap.addTo(projectile) + (Service.defaultPlayerGUID, projectile.profile.Lifespan seconds) + } else { + //remote projectiles that are not radiation clouds have lifespans controlled by the controller (user) + //if the controller fails, the projectile has a bit more than its normal lifespan before automatic clean up + (filterGuid, projectile.profile.Lifespan * 1.5f seconds) + } + projectileLifespan.put( + projectileGuid, + context.system.scheduler.scheduleOnce(duration, self, ZoneProjectile.Remove(projectileGuid)) + ) + zone.AvatarEvents ! AvatarServiceMessage( + zone.id, + AvatarAction.LoadProjectile( + clarifiedFilterGuid, + definition.ObjectId, + projectileGuid, + definition.Packet.ConstructorData(projectile).get + ) + ) + } + + /** + * For a given registered remote projectile, perform all the actions necessary to properly dispose of it. + * The projectile doesn't have to be registered at the moment, + * but you do need to know it's (previous) globally unique identifier.
+ *
+ * Those actions involve:
+ * - remove and cancel the internal disposal timer,
+ * - if the projectile is a radiation cloud, remove it from the zone blockmap
+ * - remove the projectile from the zone managing list, and
+ * - dispatch messages to eliminate the projectile from the game world. + * @param projectile_guid the globally unique identifier of the projectile + * @param projectile the projectile + */ + def cleanUpRemoteProjectile(projectile_guid: PlanetSideGUID, projectile: Projectile): Unit = { + projectileLifespan.remove(projectile_guid) match { + case Some(c) => c.cancel() + case _ => ; + } + projectileList.remove(projectileList.indexOf(projectile)) + if (projectile.Definition.radiation_cloud) { + zone.blockMap.removeFrom(projectile) + zone.AvatarEvents ! AvatarServiceMessage( + zone.id, + AvatarAction.ObjectDelete(PlanetSideGUID(0), projectile_guid, 2) + ) + } else { + zone.AvatarEvents ! AvatarServiceMessage( + zone.id, + AvatarAction.ProjectileExplodes(PlanetSideGUID(0), projectile_guid, projectile) + ) + } + } +} + +object ZoneProjectile { + /** + * Start monitoring the projectile. + * @param filterGuid a unique identifier filtering messages from a certain recipient + * @param projectile the projectile being included + */ + final case class Add(filterGuid: PlanetSideGUID, projectile: Projectile) + + object Add { + /** + * Overloaded constructor for `Add` which onyl requires the projectile + * and defaults the filtering. + * @param projectile the projectile being included + * @return an `Add` message + */ + def apply(projectile: Projectile): Add = Add(PlanetSideGUID(0), projectile) + } + + /** + * Stop the projectile from being monitored. + * @param guid the projectile assigned global unique identifier; + * not the same as the client local unique identifier (40100 to 40125) + */ + final case class Remove(guid: PlanetSideGUID) +} diff --git a/src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala b/src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala index ca8506b3f..2204a971f 100644 --- a/src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala +++ b/src/main/scala/net/psforever/objects/zones/blockmap/BlockMap.scala @@ -77,7 +77,7 @@ class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) { * @return a conglomerate sector which lists all of the entities in the discovered sector(s) */ def sector(p: Vector3, range: Float): SectorPopulation = { - BlockMap.quickToSectorGroup( BlockMap.findSectorIndices(blockMap = this, p, range).map { blocks } ) + BlockMap.quickToSectorGroup(range, BlockMap.findSectorIndices(blockMap = this, p, range).map { blocks } ) } /** @@ -129,7 +129,7 @@ class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) { val toSectors = to.toSet.map { blocks } toSectors.foreach { block => block.addTo(target) } target.blockMapEntry = Some(BlockMapEntry(toPosition, range, to.toSet)) - BlockMap.quickToSectorGroup(toSectors) + BlockMap.quickToSectorGroup(range, toSectors) } /** @@ -201,7 +201,7 @@ class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) { target.blockMapEntry = None val from = entry.sectors.map { blocks } from.foreach { block => block.removeFrom(target) } - BlockMap.quickToSectorGroup(from) + BlockMap.quickToSectorGroup(range, from) case None => SectorGroup(Nil) } @@ -262,7 +262,7 @@ class BlockMap(fullMapWidth: Int, fullMapHeight: Int, desiredSpanSize: Int) { to.diff(from).foreach { index => blocks(index).addTo(target) } from.diff(to).foreach { index => blocks(index).removeFrom(target) } target.blockMapEntry = Some(BlockMapEntry(toPosition, range, to)) - BlockMap.quickToSectorGroup(to.map { blocks }) + BlockMap.quickToSectorGroup(range, to.map { blocks }) case None => SectorGroup(Nil) } @@ -394,4 +394,19 @@ object BlockMap { SectorGroup(to) } } + + /** + * If only one sector, just return that sector. + * If a group of sectors, organize them into a single referential sector. + * @param range a custom range value + * @param to all allocated sectors + * @return a conglomerate sector which lists all of the entities in the allocated sector(s) + */ + def quickToSectorGroup(range: Float, to: Iterable[Sector]): SectorPopulation = { + if (to.size == 1) { + SectorGroup(range, to.head) + } else { + SectorGroup(range, to) + } + } } diff --git a/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala b/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala index 2cbfeb42a..838783e82 100644 --- a/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala +++ b/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala @@ -1,6 +1,7 @@ // Copyright (c) 2021 PSForever package net.psforever.objects.zones.blockmap +import net.psforever.objects.ballistics.Projectile import net.psforever.objects.ce.Deployable import net.psforever.objects.equipment.Equipment import net.psforever.objects.serverobject.environment.PieceOfEnvironment @@ -13,6 +14,8 @@ import scala.collection.mutable.ListBuffer * The collections of entities in a sector conglomerate. */ trait SectorPopulation { + def range: Float + def livePlayerList: List[Player] def corpseList: List[Player] @@ -29,6 +32,8 @@ trait SectorPopulation { def environmentList: List[PieceOfEnvironment] + def projectileList: List[Projectile] + /** * A count of all the entities in all the lists. */ @@ -40,7 +45,8 @@ trait SectorPopulation { deployableList.size + buildingList.size + amenityList.size + - environmentList.size + environmentList.size + + projectileList.size } } @@ -143,6 +149,12 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int) (a: PieceOfEnvironment, b: PieceOfEnvironment) => a eq b ) + private val projectiles: SectorListOf[Projectile] = new SectorListOf[Projectile]( + (a: Projectile, b: Projectile) => a.id == b.id + ) + + def range: Float = span.toFloat + def livePlayerList : List[Player] = livePlayers.list def corpseList: List[Player] = corpses.list @@ -159,6 +171,8 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int) def environmentList: List[PieceOfEnvironment] = environment.list + def projectileList: List[Projectile] = projectiles.list + /** * Appropriate an entity added to this blockmap bucket * inot a list of objects that are like itself. @@ -189,6 +203,8 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int) amenities.list.size < amenities.addTo(a).size case e: PieceOfEnvironment => environment.list.size < environment.addTo(e).size + case p: Projectile => + projectiles.list.size < projectiles.addTo(p).size case _ => false } @@ -211,6 +227,8 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int) equipmentOnGround.list.size > equipmentOnGround.removeFrom(e).size case d: Deployable => deployables.list.size > deployables.removeFrom(d).size + case p: Projectile => + projectiles.list.size > projectiles.removeFrom(p).size case _ => false } @@ -230,6 +248,7 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int) * @param environmentList fields that represent the game world environment */ class SectorGroup( + val range: Float, val livePlayerList: List[Player], val corpseList: List[Player], val vehicleList: List[Vehicle], @@ -237,7 +256,8 @@ class SectorGroup( val deployableList: List[Deployable], val buildingList: List[Building], val amenityList: List[Amenity], - val environmentList: List[PieceOfEnvironment] + val environmentList: List[PieceOfEnvironment], + val projectileList: List[Projectile] ) extends SectorPopulation @@ -250,6 +270,7 @@ object SectorGroup { */ def apply(sector: Sector): SectorGroup = { new SectorGroup( + sector.range, sector.livePlayerList, sector.corpseList, sector.vehicleList, @@ -257,7 +278,30 @@ object SectorGroup { sector.deployableList, sector.buildingList, sector.amenityList, - sector.environmentList + sector.environmentList, + sector.projectileList + ) + } + + /** + * Overloaded constructor that takes a single sector + * and transfers the lists of entities into a single conglomeration of the sector populations. + * @param range a custom range value + * @param sector the sector to be counted + * @return a `SectorGroup` object + */ + def apply(range: Float, sector: Sector): SectorGroup = { + new SectorGroup( + range, + sector.livePlayerList, + sector.corpseList, + sector.vehicleList, + sector.equipmentOnGroundList, + sector.deployableList, + sector.buildingList, + sector.amenityList, + sector.environmentList, + sector.projectileList ) } @@ -268,15 +312,52 @@ object SectorGroup { * @return a `SectorGroup` object */ def apply(sectors: Iterable[Sector]): SectorGroup = { - new SectorGroup( - sectors.flatMap { _.livePlayerList }.toList.distinct, - sectors.flatMap { _.corpseList }.toList.distinct, - sectors.flatMap { _.vehicleList }.toList.distinct, - sectors.flatMap { _.equipmentOnGroundList }.toList.distinct, - sectors.flatMap { _.deployableList }.toList.distinct, - sectors.flatMap { _.buildingList }.toList.distinct, - sectors.flatMap { _.amenityList }.toList.distinct, - sectors.flatMap { _.environmentList }.toList.distinct - ) + if (sectors.isEmpty) { + SectorGroup(range = 0, sectors = Nil) + } else if (sectors.size == 1) { + SectorGroup(sectors.head.range, sectors) + } else { + SectorGroup(sectors.maxBy { _.range }.range, sectors) + } + } + + /** + * Overloaded constructor that takes a group of sectors + * and condenses all of the lists of entities into a single conglomeration of the sector populations. + * @param range a custom range value + * @param sectors the series of sectors to be counted + * @return a `SectorGroup` object + */ + def apply(range: Float, sectors: Iterable[Sector]): SectorGroup = { + if (sectors.isEmpty) { + new SectorGroup(range, Nil, Nil, Nil, Nil, Nil, Nil, Nil, Nil, Nil) + } else if (sectors.size == 1) { + val sector = sectors.head + new SectorGroup( + range, + sector.livePlayerList, + sector.corpseList, + sector.vehicleList, + sector.equipmentOnGroundList, + sector.deployableList, + sector.buildingList, + sector.amenityList, + sector.environmentList, + sector.projectileList + ) + } else { + new SectorGroup( + range, + sectors.flatMap { _.livePlayerList }.toList.distinct, + sectors.flatMap { _.corpseList }.toList.distinct, + sectors.flatMap { _.vehicleList }.toList.distinct, + sectors.flatMap { _.equipmentOnGroundList }.toList.distinct, + sectors.flatMap { _.deployableList }.toList.distinct, + sectors.flatMap { _.buildingList }.toList.distinct, + sectors.flatMap { _.amenityList }.toList.distinct, + sectors.flatMap { _.environmentList }.toList.distinct, + sectors.flatMap { _.projectileList }.toList.distinct + ) + } } } diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 64fa94f2a..7cbbb3e04 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -335,7 +335,7 @@ object GamePacketOpcode extends Enumeration { case 0x19 => game.ObjectDeleteMessage.decode case 0x1a => game.PingMsg.decode case 0x1b => game.VehicleStateMessage.decode - case 0x1c => noDecoder(FrameVehicleStateMessage) + case 0x1c => game.FrameVehicleStateMessage.decode case 0x1d => game.GenericObjectStateMsg.decode case 0x1e => game.ChildObjectStateMessage.decode case 0x1f => game.ActionResultMessage.decode @@ -547,14 +547,14 @@ object GamePacketOpcode extends Enumeration { case 0xcc => noDecoder(ClockCalibrationMessage) case 0xcd => game.DensityLevelUpdateMessage.decode case 0xce => noDecoder(ActOfGodMessage) - case 0xcf => noDecoder(AvatarAwardMessage) + case 0xcf => game.AvatarAwardMessage.decode // OPCODES 0xd0-df case 0xd0 => noDecoder(UnknownMessage208) case 0xd1 => game.DisplayedAwardMessage.decode case 0xd2 => game.RespawnAMSInfoMessage.decode - case 0xd3 => noDecoder(ComponentDamageMessage) - case 0xd4 => noDecoder(GenericObjectActionAtPositionMessage) + case 0xd3 => game.ComponentDamageMessage.decode + case 0xd4 => game.GenericObjectActionAtPositionMessage.decode case 0xd5 => game.PropertyOverrideMessage.decode case 0xd6 => noDecoder(WarpgateLinkOverrideMessage) case 0xd7 => noDecoder(EmpireBenefitsMessage) diff --git a/src/main/scala/net/psforever/packet/game/AvatarAwardMessage.scala b/src/main/scala/net/psforever/packet/game/AvatarAwardMessage.scala new file mode 100644 index 000000000..bda9214a0 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/AvatarAwardMessage.scala @@ -0,0 +1,101 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +import scala.annotation.switch + +abstract class AwardOption(val code: Int) { + def unk1: Long + def unk2: Long +} + +final case class AwardOptionZero(unk1: Long, unk2: Long) extends AwardOption(code = 0) + +final case class AwardOptionOne(unk1: Long) extends AwardOption(code = 1) { + def unk2: Long = 0L +} + +final case class AwardOptionTwo(unk1: Long) extends AwardOption(code = 3) { + def unk2: Long = 0L +} + +/** + * na + * @param unk1 na + * @param unk2 na + * @param unk3 na + */ +final case class AvatarAwardMessage( + unk1: Long, + unk2: AwardOption, + unk3: Int + ) + extends PlanetSideGamePacket { + type Packet = AvatarAwardMessage + def opcode = GamePacketOpcode.AvatarAwardMessage + def encode = AvatarAwardMessage.encode(this) +} + +object AvatarAwardMessage extends Marshallable[AvatarAwardMessage] { + private val codec_one: Codec[AwardOptionOne] = { + uint32L.hlist + }.xmap[AwardOptionOne]( + { + case a :: HNil => AwardOptionOne(a) + }, + { + case AwardOptionOne(a) => a :: HNil + } + ) + + private val codec_two: Codec[AwardOptionTwo] = { + uint32L.hlist + }.xmap[AwardOptionTwo]( + { + case a :: HNil => AwardOptionTwo(a) + }, + { + case AwardOptionTwo(a) => a :: HNil + } + ) + + private val codec_zero: Codec[AwardOptionZero] = { + uint32L :: uint32L + }.xmap[AwardOptionZero]( + { + case a :: b :: HNil => AwardOptionZero(a, b) + }, + { + case AwardOptionZero(a, b) => a :: b :: HNil + } + ) + + private def selectAwardOption(code: Int): Codec[AwardOption] = { + ((code: @switch) match { + case 2 | 3 => codec_two + case 1 => codec_one + case 0 => codec_zero + }).asInstanceOf[Codec[AwardOption]] + } + + implicit val codec: Codec[AvatarAwardMessage] = ( + ("unk1" | uint32L) :: + (uint2 >>:~ { code => + ("unk2" | selectAwardOption(code)) :: + ("unk3" | uint8L) + }) + ).xmap[AvatarAwardMessage]( + { + case unk1 :: _ :: unk2 :: unk3 :: HNil => + AvatarAwardMessage(unk1, unk2, unk3) + }, + { + case AvatarAwardMessage(unk1, unk2, unk3) => + unk1 :: unk2.code :: unk2 :: unk3 :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/ComponentDamageMessage.scala b/src/main/scala/net/psforever/packet/game/ComponentDamageMessage.scala new file mode 100644 index 000000000..01c97fb3e --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/ComponentDamageMessage.scala @@ -0,0 +1,82 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.{PlanetSideGUID, SubsystemComponent} +import scodec.codecs._ +import scodec.Codec + +/** + * The status of the component's changing condition, + * including the level of alert the player experiences when the change occurs. + * @param alarm_level the klaxon sound effect associated with this damage + * @param damage the amount of damage (encoded ...) + * @param unk na; + * usually, `true`; + * known `false` states during shield generator offline and destruction conditions + */ +final case class ComponentDamageField(alarm_level: Long, damage: Long, unk: Boolean) + +object ComponentDamageField { + def apply(alarmLevel: Long, dam: Long): ComponentDamageField = ComponentDamageField(alarmLevel, dam, unk = true) +} + +/** + * Vehicles have aspects that are neither registered - + * do not necessarily represented unique entities of the vehicle - + * and are not statistical behaviors derived from the same level as the game files - + * modify vehicle stats but are not vehicle stats themselves. + * When these "components of the vehicle" are affected, however, + * such as when the vehicle has been jammed or when it has sustained damage, + * changes to the handling of the vehicle will occur through the said statistical mechanics. + * @see `VehicleSubsystem` + * @see `VehicleSubsystemEntity` + * @param guid the entity that owns this component, usually a vehicle + * @param component the subsystem, or part of the subsystem, being affected + * @param status specific about the component damage; + * `None`, when damage issues are cleared + */ +final case class ComponentDamageMessage( + guid: PlanetSideGUID, + component: SubsystemComponent, + status: Option[ComponentDamageField] + ) extends PlanetSideGamePacket { + type Packet = ComponentDamageMessage + def opcode = GamePacketOpcode.ComponentDamageMessage + def encode = ComponentDamageMessage.encode(this) +} + +object ComponentDamageMessage extends Marshallable[ComponentDamageMessage] { + /** + * Overloaded constructor where the component's current state is be cleared. + * @param guid the entity that owns this component, usually a vehicle + * @param component the subsystem, or part of the subsystem, being affected + * @return a `ComponentDamageMessage` packet + */ + def apply(guid: PlanetSideGUID, component: SubsystemComponent): ComponentDamageMessage = + ComponentDamageMessage(guid, component, None) + + /** + * Overloaded constructor where the component's current state is always defined. + * @param guid the entity that owns this component, usually a vehicle + * @param component the subsystem, or part of the subsystem, being affected + * @param status specific about the component damage + * @return a `ComponentDamageMessage` packet + */ + def apply(guid: PlanetSideGUID, component: SubsystemComponent, status: ComponentDamageField): ComponentDamageMessage = + ComponentDamageMessage(guid, component, Some(status)) + + private val subsystemComponentCodec = PacketHelpers.createLongIntEnumCodec(SubsystemComponent, uint32L) + + private val componentDamageFieldCodec: Codec[ComponentDamageField] = ( + ("unk1" | uint32L) :: + ("unk2" | uint32L) :: + ("unk3" | bool) + ).as[ComponentDamageField] + + implicit val codec: Codec[ComponentDamageMessage] = ( + ("guid" | PlanetSideGUID.codec) :: + ("component" | subsystemComponentCodec) :: + ("status" | optional(bool, componentDamageFieldCodec)) + ).as[ComponentDamageMessage] +} diff --git a/src/main/scala/net/psforever/packet/game/FavoritesMessage.scala b/src/main/scala/net/psforever/packet/game/FavoritesMessage.scala index b31b95f10..49f6a153f 100644 --- a/src/main/scala/net/psforever/packet/game/FavoritesMessage.scala +++ b/src/main/scala/net/psforever/packet/game/FavoritesMessage.scala @@ -37,14 +37,16 @@ import shapeless.{::, HNil} * @param player_guid the player * @param line the zero-indexed line number of this entry in its list * @param label the identifier for this entry - * @param armor the type of exo-suit, if an Infantry loadout + * @param armor_type the type of exo-suit, if an Infantry loadout; + * the type of battleframe, if a Battleframe loadout; + * `None`, if just a Vehicle loadout */ final case class FavoritesMessage( list: LoadoutType.Value, player_guid: PlanetSideGUID, line: Int, label: String, - armor: Option[Int] + armor_type: Option[Int] ) extends PlanetSideGamePacket { type Packet = FavoritesMessage def opcode = GamePacketOpcode.FavoritesMessage @@ -52,9 +54,8 @@ final case class FavoritesMessage( } object FavoritesMessage extends Marshallable[FavoritesMessage] { - /** - * Overloaded constructor, for infantry loadouts specifically. + * Overloaded constructor. * @param list the destination list * @param player_guid the player * @param line the zero-indexed line number of this entry in its list @@ -83,11 +84,66 @@ object FavoritesMessage extends Marshallable[FavoritesMessage] { def apply(list: LoadoutType.Value, player_guid: PlanetSideGUID, line: Int, label: String): FavoritesMessage = { FavoritesMessage(list, player_guid, line, label, None) } + + /** + * Overloaded constructor for infantry loadouts. + * @param player_guid the player + * @param line the zero-indexed line number of this entry in its list + * @param label the identifier for this entry + * @param armor the type of exo-suit + * @return a `FavoritesMessage` object + */ + def Infantry( + player_guid: PlanetSideGUID, + line: Int, + label: String, + armor: Int + ): FavoritesMessage = { + FavoritesMessage(LoadoutType.Infantry, player_guid, line, label, Some(armor)) + } + + /** + * Overloaded constructor for vehicle loadouts. + * @param player_guid the player + * @param line the zero-indexed line number of this entry in its list + * @param label the identifier for this entry + * @return a `FavoritesMessage` object + */ + def Vehicle( + player_guid: PlanetSideGUID, + line: Int, + label: String + ): FavoritesMessage = { + FavoritesMessage(LoadoutType.Vehicle, player_guid, line, label, None) + } + + /** + * Overloaded constructor for battleframe loadouts. + * @param player_guid the player + * @param line the zero-indexed line number of this entry in its list + * @param label the identifier for this entry + * @param subtype the type of battleframe unit + * @return a `FavoritesMessage` object + */ + def Battleframe( + player_guid: PlanetSideGUID, + line: Int, + label: String, + subtype: Int + ): FavoritesMessage = { + FavoritesMessage(LoadoutType.Battleframe, player_guid, line, label, Some(subtype)) + } + implicit val codec: Codec[FavoritesMessage] = (("list" | LoadoutType.codec) >>:~ { value => ("player_guid" | PlanetSideGUID.codec) :: ("line" | uint4L) :: - ("label" | PacketHelpers.encodedWideStringAligned(2)) :: - conditional(value == LoadoutType.Infantry, "armor" | uintL(3)) + ("label" | PacketHelpers.encodedWideStringAligned(adjustment = 2)) :: + ("armor_type" | conditional(value != LoadoutType.Vehicle, + { + if (value == LoadoutType.Infantry) uint(bits = 3) + else uint4 + } + )) }).xmap[FavoritesMessage]( { case lst :: guid :: ln :: str :: arm :: HNil => @@ -95,8 +151,7 @@ object FavoritesMessage extends Marshallable[FavoritesMessage] { }, { case FavoritesMessage(lst, guid, ln, str, arm) => - val armset: Option[Int] = if (lst == LoadoutType.Infantry && arm.isEmpty) { Some(0) } - else { arm } + val armset = if (lst != LoadoutType.Vehicle && arm.isEmpty) { Some(0) } else { arm } lst :: guid :: ln :: str :: armset :: HNil } ) diff --git a/src/main/scala/net/psforever/packet/game/FrameVehicleStateMessage.scala b/src/main/scala/net/psforever/packet/game/FrameVehicleStateMessage.scala new file mode 100644 index 000000000..3b3f8b485 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/FrameVehicleStateMessage.scala @@ -0,0 +1,81 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.{Angular, PlanetSideGUID, Vector3} +import scodec.Codec +import scodec.codecs._ + +//TODO write more thorough comments later. +/** + * Dispatched to report and update the operational condition of a given battle frame robotics vehicle. + * @param vehicle_guid the battleframe robotic unit + * @param unk1 na + * @param pos the xyz-coordinate location in the world + * @param orient the orientation of the vehicle + * @param vel optional movement data + * @param unk2 na + * @param unk3 na + * @param unk4 na + * @param is_crouched the battleframe unit is crouched + * @param is_airborne the battleframe unit is either flying or falling (after flying) + * @param ascending_flight is the battleframe unit ascending; + * normally reports `ascending_flight` before properly reporting as `is_airborne`; + * continues to report `ascending_flight` until begins falling + * @param flight_time_remaining a measure of how much longer the battleframe unit, if it can fly, can fly; + * reported as a 0-10 value, counting down from 10 when airborne and provided vertical thrust + * @param unk9 na + * @param unkA na + * @see `PlacementData` + */ +final case class FrameVehicleStateMessage( + vehicle_guid: PlanetSideGUID, + unk1: Int, + pos: Vector3, + orient: Vector3, + vel: Option[Vector3], + unk2: Boolean, + unk3: Int, + unk4: Int, + is_crouched: Boolean, + is_airborne: Boolean, + ascending_flight: Boolean, + flight_time_remaining: Int, + unk9: Long, + unkA: Long + ) extends PlanetSideGamePacket { + type Packet = FrameVehicleStateMessage + def opcode = GamePacketOpcode.FrameVehicleStateMessage + def encode = FrameVehicleStateMessage.encode(this) +} + +object FrameVehicleStateMessage extends Marshallable[FrameVehicleStateMessage] { + /** + * Calculate common orientation from little-endian bit data. + * @see `Angular.codec_roll` + * @see `Angular.codec_pitch` + * @see `Angular.codec_yaw` + */ + val codec_orient : Codec[Vector3] = ( + ("roll" | Angular.codec_roll(bits = 10)) :: + ("pitch" | Angular.codec_pitch(bits = 10)) :: + ("yaw" | Angular.codec_yaw(bits = 10, North = 90f)) + ).as[Vector3] + + implicit val codec : Codec[FrameVehicleStateMessage] = ( + ("vehicle_guid" | PlanetSideGUID.codec) :: + ("unk1" | uint(bits = 3)) :: + ("pos" | Vector3.codec_pos) :: + ("orient" | codec_orient) :: + optional(bool, target = "vel" | Vector3.codec_vel) :: + ("unk2" | bool) :: + ("unk3" | uint2) :: + ("unk4" | uint2) :: + ("is_crouched" | bool) :: + ("is_airborne" | bool) :: + ("ascending_flight" | bool) :: + ("flight_time_remaining" | uint4) :: + ("unk9" | uint32) :: + ("unkA" | uint32) + ).as[FrameVehicleStateMessage] +} diff --git a/src/main/scala/net/psforever/packet/game/FriendsResponse.scala b/src/main/scala/net/psforever/packet/game/FriendsResponse.scala index ada9e346c..75c7dfdb0 100644 --- a/src/main/scala/net/psforever/packet/game/FriendsResponse.scala +++ b/src/main/scala/net/psforever/packet/game/FriendsResponse.scala @@ -14,7 +14,7 @@ object FriendAction extends Enumeration { val InitializeFriendList, AddFriend, RemoveFriend, UpdateFriend, InitializeIgnoreList, AddIgnoredPlayer, RemoveIgnoredPlayer = Value - implicit val codec: Codec[FriendAction.Value] = PacketHelpers.createEnumerationCodec(this, uint(3)) + implicit val codec: Codec[FriendAction.Value] = PacketHelpers.createEnumerationCodec(this, uint(bits = 3)) } /** @@ -42,16 +42,17 @@ final case class Friend(name: String, online: Boolean = false) * 5 - add entry to ignored players list
* 6 - remove entry from ignored players list
* @param action the purpose of the entry(s) in this packet - * @param unk1 na; always 0? - * @param unk2 na; always `true`? - * @param unk3 na; always `true`? + * @param unk1 na; + * always 0? + * @param first_entry this is the first packet for this action + * @param last_entry this is the last packet for this action * @param friends a list of `Friend`s */ final case class FriendsResponse( action: FriendAction.Value, unk1: Int, - unk2: Boolean, - unk3: Boolean, + first_entry: Boolean, + last_entry: Boolean, friends: List[Friend] = Nil ) extends PlanetSideGamePacket { type Packet = FriendsResponse @@ -81,18 +82,18 @@ object FriendsResponse extends Marshallable[FriendsResponse] { implicit val codec: Codec[FriendsResponse] = ( ("action" | FriendAction.codec) :: ("unk1" | uint4L) :: - ("unk2" | bool) :: - ("unk3" | bool) :: + ("first_entry" | bool) :: + ("last_entry" | bool) :: (("number_of_friends" | uint4L) >>:~ { len => conditional(len > 0, "friend" | Friend.codec) :: ("friends" | PacketHelpers.listOfNSized(len - 1, Friend.codec_list)) }) ).xmap[FriendsResponse]( { - case act :: u1 :: u2 :: u3 :: _ :: friend1 :: friends :: HNil => + case act :: u1 :: first :: last :: _ :: friend1 :: friends :: HNil => val friendList: List[Friend] = if (friend1.isDefined) { friend1.get +: friends } else { friends } - FriendsResponse(act, u1, u2, u3, friendList) + FriendsResponse(act, u1, first, last, friendList) }, { case FriendsResponse(act, u1, u2, u3, friends) => diff --git a/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala b/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala index 2e8c19d67..4f95473c2 100644 --- a/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala @@ -26,8 +26,8 @@ import scodec.codecs._ * 16 - MAX Undeploy
* 22 - message: awarded a cavern capture (updates cavern capture status)
* 23 - award a cavern kill
- * 24 - message: you have been imprinted (updates imprinted status; does it?)
- * 25 - message: you are no longer imprinted (updates imprinted status; does it?)
+ * 24 - message: you have been imprinted (updates imprinted status)
+ * 25 - message: you are no longer imprinted (updates imprinted status)
* 27 - event: purchase timers reset (does it?)
* 31 - forced into first person view; * in third person view, player character sinks into the ground; green deconstruction particle effect under feet
diff --git a/src/main/scala/net/psforever/packet/game/GenericObjectActionAtPositionMessage.scala b/src/main/scala/net/psforever/packet/game/GenericObjectActionAtPositionMessage.scala new file mode 100644 index 000000000..ff287ff80 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/GenericObjectActionAtPositionMessage.scala @@ -0,0 +1,38 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.{PlanetSideGUID, Vector3} +import scodec.{Attempt, Codec} +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * na + */ +final case class GenericObjectActionAtPositionMessage( + object_guid: PlanetSideGUID, + code: Int, + pos: Vector3 + ) extends PlanetSideGamePacket { + type Packet = GenericObjectActionAtPositionMessage + def opcode = GamePacketOpcode.GenericObjectActionAtPositionMessage + def encode = GenericObjectActionAtPositionMessage.encode(this) +} + +object GenericObjectActionAtPositionMessage extends Marshallable[GenericObjectActionAtPositionMessage] { + implicit val codec: Codec[GenericObjectActionAtPositionMessage] = ( + ("object_guid" | PlanetSideGUID.codec) :: + ("code" | uint(bits = 8)) :: + ("pos" | Vector3.codec_pos) + ).exmap[GenericObjectActionAtPositionMessage]( + { + case guid :: code :: pos :: HNil => + Attempt.Successful(GenericObjectActionAtPositionMessage(guid, code, pos)) + }, + { + case GenericObjectActionAtPositionMessage(guid, code, pos) => + Attempt.Successful(guid :: code :: pos :: HNil) + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala index 1f3c6c560..916ee0310 100644 --- a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala @@ -21,7 +21,7 @@ import shapeless.{::, HNil} * 11 - Deploy capital base shield pole with animation and broadcasts "The capitol force dome at X has been activated"
* 12 - Stow capital base shield pole with animation and broadcasts "The capitol force dome at X has been deactivated"
* 13 - Deploy capital base shield pole (instantly, unless still in the middle of the stow animation)
- * 14 - Changes capture console to say "Facility hacked by the LLU has been spawned." when looked at
+ * 14 - Changes capture console to say "Facility hacked by the [Faction] LLU has been spawned." when looked at
* 15 - Displays "This facility's generator is under attack!"
* 16 - Displays "Generator has Overloaded! Evacuate Generator Room Immediately!"
* 17 - Displays "This facility's generator is back on line"
@@ -31,6 +31,7 @@ import shapeless.{::, HNil} * 22 - ???? (Has been seen on vehicle pad objects, possibly some sort of reset flag after base faction flip / hack clear?)
* 23 - Plays vehicle pad animation moving downwards
* 24 - Makes the vehicle bounce slightly. Have seen this in packet captures after taking a vehicle through a warpgate
+ * 25 - for observed driven BFR's, model resets animation following GOAM90?
* 27 - Activates the router internal telepad for linking
* 28 - Activates the router internal telepad for linking
* 29 - Activates the telepad deployable (also used on the router's internal telepad)
@@ -38,11 +39,23 @@ import shapeless.{::, HNil} * 31 - Animation during router teleportation (source)
* 32 - Animation during router teleportation (destination)
* 34 - Time until item can be used ?????
+ * 38 - for BFR's, enable a disabled arm weapon
+ * 39 - for BFR's, disable an enabled arm weapon
+ * 44 - for BFR's, animates the energy shield
+ * 45 - for BFR's, energy shield dissipates
+ * 46 - for BFR's, causes an explosions on the machine's midsection
+ * 48 - for BFR's, Control Interface unstable messages
+ * 49 - for BFR's, Control Interface malfunction messages
* 50 - For aircraft - client shows "The bailing mechanism failed! To fix the mechanism, land and repair the vehicle!"
* 53 - Put down an FDU
* 56 - Sets vehicle or player to be black ops
* 57 - Reverts player from black ops
- * @see GenericObjectActionEnum + *
+ * What are these values?
+ * 90? - for observed driven BFR's, model pitches up slightly and stops idle animation
+ * 29? - ??? (response to GOAM55)
+ * 55? - ??? (client responds with GOAM29) + * @see `GenericObjectActionEnum` */ final case class GenericObjectActionMessage(object_guid: PlanetSideGUID, code: Int) extends PlanetSideGamePacket { type Packet = GenericObjectActionMessage diff --git a/src/main/scala/net/psforever/packet/game/TradeMessage.scala b/src/main/scala/net/psforever/packet/game/TradeMessage.scala index c3f836107..ff782874c 100644 --- a/src/main/scala/net/psforever/packet/game/TradeMessage.scala +++ b/src/main/scala/net/psforever/packet/game/TradeMessage.scala @@ -12,7 +12,7 @@ sealed trait Trade { } final case class NoTrade(value: Int) extends Trade { - assert(value == 1 || value == 2 || value == 3, s"NoTrade has wrong code value - $value not in [a-f]") + assert(value > 0 || value < 10, s"NoTrade has wrong code value - $value not in [0,a-f]") } final case class TradeOne(value: Int, unk1: PlanetSideGUID, unk2: PlanetSideGUID, unk3: PlanetSideGUID) extends Trade { @@ -29,15 +29,36 @@ final case class TradeThree(value: Int, unk: PlanetSideGUID) extends Trade { final case class TradeFour(value: Int, unk: Int) extends Trade { assert(value == 9, s"TradeFour has wrong code value - $value not in [9]") + assert(unk < 0 || unk > 15, s"TradeFour value is out of bounds - $unk not in [0-f]") } -final case class TradeMessage(unk: Int, trade: Trade) +final case class TradeMessage(trade: Trade) extends PlanetSideGamePacket { type Packet = TradeMessage def opcode = GamePacketOpcode.TradeMessage def encode = TradeMessage.encode(this) } +object NoTrade { + def apply(): NoTrade = NoTrade(0) +} + +object TradeOne { + def apply(unk1: PlanetSideGUID, unk2: PlanetSideGUID, unk3: PlanetSideGUID): TradeOne = TradeOne(1, unk1, unk2, unk3) +} + +object TradeTwo { + def apply(unk1: PlanetSideGUID, unk2: PlanetSideGUID): TradeTwo = TradeTwo(4, unk1, unk2) +} + +object TradeThree { + def apply(unk: PlanetSideGUID): TradeThree = TradeThree(6, unk) +} + +object TradeFour { + def apply(unk: Int): TradeFour = TradeFour(6, unk) +} + object TradeMessage extends Marshallable[TradeMessage] { private def tradeOneCodec(value: Int): Codec[TradeOne] = ( ("u1" | PlanetSideGUID.codec) :: @@ -96,16 +117,13 @@ object TradeMessage extends Marshallable[TradeMessage] { } implicit val codec: Codec[TradeMessage] = ( - ("unk" | uint8) :: - (uint4 >>:~ { code => - ("trade" | selectTradeCodec(code)).hlist - }) + uint4 >>:~ { code => ("trade" | selectTradeCodec(code)).hlist } ).xmap[TradeMessage]( { - case unk :: _ :: trade :: HNil => TradeMessage(unk, trade) + case _ :: trade :: HNil => TradeMessage(trade) }, { - case TradeMessage(unk, trade) => unk :: trade.value :: trade :: HNil + case TradeMessage(trade) => trade.value :: trade :: HNil } ) } diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/BattleFrameRoboticsData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/BattleFrameRoboticsData.scala new file mode 100644 index 000000000..03c4692b5 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/objectcreate/BattleFrameRoboticsData.scala @@ -0,0 +1,128 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import scodec.{Attempt, Codec, Err} +import shapeless.HNil +import scodec.codecs._ + +/** + * A representation of a battle frame robotics vehicle. + * @param pos where the vehicle is and how it is oriented in the game world + * @param data common vehicle field data + * @param health the amount of health the vehicle has, as a percentage of a filled bar (255) + * @param shield the strength of the shield the vehicle has, as a percentage of a filled bar (255) + * @param unk1 na + * @param unk2 na + * @param no_mount_points do not display entry points for the seats + * @param driveState a representation for the current mobility state; + * various vehicles also use this field to indicate "deployment," e.g., the advanced mobile spawn + * @param proper_anim na; + * I forget what this does + * @param unk3 na + * @param show_bfr_shield display the swirling shield of the battle frame + * @param unk4 na + * @param inventory the seats, mounted weapons, and utilities (such as terminals) that are currently included; + * will also include trunk contents; + * the driver is the only valid seat entry (more will cause the access permissions to act up) + */ +final case class BattleFrameRoboticsData( + pos: PlacementData, + data: CommonFieldData, + health: Int, + shield: Int, + unk1: Int, + unk2: Boolean, + no_mount_points: Boolean, + driveState: Int, + proper_anim: Boolean, + unk3: Int, + show_bfr_shield: Boolean, + unk4: Option[Boolean], + inventory: Option[InventoryData] = None + ) extends ConstructorData { + override def bitsize: Long = { + val posSize: Long = pos.bitsize + val dataSize: Long = data.bitsize + val unk4Size = unk4 match { + case Some(_) => 1L + case None => 0L + } + val inventorySize = inventory match { + case Some(inv) => inv.bitsize + case None => 0L + } + 49L + posSize + dataSize + unk4Size + inventorySize + } +} + +object BattleFrameRoboticsData extends Marshallable[BattleFrameRoboticsData] { + implicit val codec : Codec[BattleFrameRoboticsData] = { + import shapeless.:: + ( + ("pos" | PlacementData.codec) >>:~ { pos => + ("data" | CommonFieldData.codec(extra = false)) :: + ("health" | uint8L) :: + ("shield" | uint8L) :: + ("unk1" | uint16) :: //usually 0 + ("unk2" | bool) :: + ("no_mount_points" | bool) :: + ("driveState" | uint8L) :: //used for deploy state + ("proper_anim" | bool) :: //when unflagged, bfr stands, even if unmanned + ("unk3" | uint4) :: + ("show_bfr_shield" | bool) :: + optional(bool, target = "inventory" | MountableInventory.custom_inventory_codec(pos.vel.isDefined, VehicleFormat.Battleframe)) + } + ).exmap[BattleFrameRoboticsData] ( + { + case pos :: data :: health :: shield :: 0 :: u2 :: no_mount :: drive :: proper_anim :: u3 :: show_bfr_shield :: inv :: HNil => + Attempt.successful(BattleFrameRoboticsData(pos, data, health, shield, 0, u2, no_mount, drive, proper_anim, u3, show_bfr_shield, None, inv)) + + case data => + Attempt.failure(Err(s"decoding invalid battleframe data - $data")) + }, + { + case BattleFrameRoboticsData(pos, data, health, shield, 0, u2, no_mount, drive, proper_anim, u3, show_bfr_shield, None, inv) => + Attempt.successful(pos :: data :: health :: shield :: 0 :: u2 :: no_mount :: drive :: proper_anim :: u3 :: show_bfr_shield :: inv :: HNil) + + case data => + Attempt.failure(Err(s"encoding invalid battleframe data - $data")) + } + ) + } + + val codec_flight: Codec[BattleFrameRoboticsData] = { + import shapeless.:: + ( + ("pos" | PlacementData.codec) >>:~ { pos => + ("data" | CommonFieldData.codec(extra = false)) :: + ("health" | uint8L) :: + ("shield" | uint8L) :: + ("unk1" | uint16) :: //usually 0 + ("unk2" | bool) :: + ("no_mount_points" | bool) :: + ("driveState" | uint8L) :: //used for deploy state + ("proper_anim" | bool) :: //when unflagged, bfr stands, even if unmanned + ("unk3" | uint4) :: + ("show_bfr_shield" | bool) :: + ("unk4" | bool) :: + optional(bool, target = "inventory" | MountableInventory.custom_inventory_codec(pos.vel.isDefined, VehicleFormat.BattleframeFlight)) + } + ).exmap[BattleFrameRoboticsData] ( + { + case pos :: data :: health :: shield :: 0 :: u2 :: no_mount :: drive :: proper_anim :: u3 :: show_bfr_shield :: unk4 :: inv :: HNil => + Attempt.successful(BattleFrameRoboticsData(pos, data, health, shield, 0, u2, no_mount, drive, proper_anim, u3, show_bfr_shield, Some(unk4), inv)) + + case data => + Attempt.failure(Err(s"decoding invalid battleframe data - $data")) + }, + { + case BattleFrameRoboticsData(pos, data, health, shield, 0, u2, no_mount, drive, proper_anim, u3, show_bfr_shield, Some(unk4), inv) => + Attempt.successful(pos :: data :: health :: shield :: 0 :: u2 :: no_mount :: drive :: proper_anim :: u3 :: show_bfr_shield :: unk4 :: inv :: HNil) + + case data => + Attempt.failure(Err(s"encoding invalid battleframe data - $data")) + } + ) + } +} diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/CommonFieldData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/CommonFieldData.scala index 88cb20273..f1ca73535 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/CommonFieldData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/CommonFieldData.scala @@ -131,7 +131,7 @@ object CommonFieldData extends Marshallable[CommonFieldData] { } ) - implicit val codec: Codec[CommonFieldData] = codec(false) + implicit val codec: Codec[CommonFieldData] = codec(extra = false) def codec2(extra: Boolean): Codec[CommonFieldData] = ( @@ -139,7 +139,7 @@ object CommonFieldData extends Marshallable[CommonFieldData] { ("bops" | bool) :: ("alternate" | bool) :: ("v1" | bool) :: //though the code path differs depending on the previous bit, this one gets read one way or another - conditional(extra, "v2" | CommonFieldDataExtra.codec(unk1 = false)) :: + conditional(extra, codec = "v2" | CommonFieldDataExtra.codec(unk1 = false)) :: ("jammered" | bool) :: optional(bool, "v5" | uint16L) :: ("v4" | bool) :: diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala index 84bcdbc4f..a44a64bc0 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala @@ -47,10 +47,12 @@ final case class DCDExtra1(unk1: String, unk2: Int) extends StreamBitSize { /** * na - * @param unk1 an - * @param unk2 na + * @param cavern_captures the number of facility captures in a cavern zone; + * five are needed before imprinting in vanilla + * @param cavern_kills the number of unique player kills in a cavern zone; + * seventy-five are needed before imprinting in vanilla */ -final case class DCDExtra2(unk1: Int, unk2: Int) extends StreamBitSize { +final case class ImprintingProgress(cavern_captures: Int, cavern_kills: Int) extends StreamBitSize { override def bitsize: Long = 13L } @@ -118,22 +120,22 @@ final case class DetailedCharacterA( * these flags do not exist if they are not applicable */ final case class DetailedCharacterB( - unk1: Option[Long], - implants: List[ImplantEntry], - unk2: List[DCDExtra1], - unk3: List[DCDExtra1], - firstTimeEvents: List[String], - tutorials: List[String], - unk4: Long, - unk5: Long, - unk6: Long, - unk7: Long, - unk8: Long, - unk9: Option[DCDExtra2], - unkA: List[Long], - unkB: List[String], - unkC: Boolean, - cosmetics: Option[Set[Cosmetic]] + unk1: Option[Long], + implants: List[ImplantEntry], + unk2: List[DCDExtra1], + unk3: List[DCDExtra1], + firstTimeEvents: List[String], + tutorials: List[String], + unk4: Long, + unk5: Long, + unk6: Long, + unk7: Long, + unk8: Long, + imprinting: Option[ImprintingProgress], + unkA: List[Long], + unkB: List[String], + unkC: Boolean, + cosmetics: Option[Set[Cosmetic]] )( bep: Long, pad_length: Option[Int] @@ -158,7 +160,7 @@ final case class DetailedCharacterB( 0L } //character is at least BR24 - val unk9Size: Long = if (unk9.isEmpty) { + val imprintingSize: Long = if (imprinting.isEmpty) { 0L } else { 13L @@ -180,12 +182,12 @@ final case class DetailedCharacterB( tutorials.size ) + /* tutorials */ DetailedCharacterData.paddingCalculations( - DetailedCharacterData.displaceByUnk9(pad_length, unk9, 5), + DetailedCharacterData.displaceByOptionTest(pad_length, imprinting, 5), implants, - List(unk9.toList, tutorials, firstTimeEvents, unk3, unk2) + List(imprinting.toList, tutorials, firstTimeEvents, unk3, unk2) )(unkB.length) /* unkB */ - 275L + unk1Size + implantSize + eventListSize + unk2_3ListSize + tutorialListSize + unk9Size + unkASize + unkBSize + cosmeticsSize + paddingSize + 275L + unk1Size + implantSize + eventListSize + unk2_3ListSize + tutorialListSize + imprintingSize + unkASize + unkBSize + cosmeticsSize + paddingSize } } @@ -232,7 +234,7 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { health, unk4 = false, armor, - 0L, + 32831L, staminaMax, stamina, maxField, @@ -254,7 +256,7 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { 0L, 0L, 0L, - None, + Some(ImprintingProgress(0, 0)), Nil, Nil, unkC = false, @@ -371,12 +373,12 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { ) /** - * `Codec` for a `DCDExtra2` object. + * `Codec` for a `ImprintingProgress` object. */ - private val dcd_extra2_codec: Codec[DCDExtra2] = ( + private val imprint_progress_codec: Codec[ImprintingProgress] = ( uint(5) :: uint8L - ).as[DCDExtra2] + ).as[ImprintingProgress] /** * `Codec` for a `List` of `String` objects. @@ -426,7 +428,7 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { * @param value how much to add to `start` * @return the amount after testing */ - def displaceByUnk9(start: Option[Int], test: Option[Any], value: Int): Option[Int] = + def displaceByOptionTest(start: Option[Int], test: Option[Any], value: Int): Option[Int] = test match { case Some(_) => Some(start.getOrElse(0) + value) @@ -439,7 +441,7 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { * in reverse order of encountered `String` fields (later to earlier). * The distances are not the actual lengths but are modulo eight. * Specific strings include (the contents of):
- * - `unk9` (as a `List` object)
+ * - `imprinting` (as a `List` object)
* - `tutorials`
* - `firstTimeEvents`
* - `unk3`
@@ -605,13 +607,13 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { ("unk6" | uint32L) :: ("unk7" | uint32L) :: ("unk8" | uint32L) :: - (optional(isFalse, "unk9" | dcd_extra2_codec) >>:~ { unk9 => + (optional(isFalse, "imprinting" | imprint_progress_codec) >>:~ { imprinting => ("unkA" | listOfN(uint16L, uint32L)) :: ("unkB" | unkBCodec( paddingCalculations( - displaceByUnk9(pad_length, unk9, 5), + displaceByOptionTest(pad_length, imprinting, 5), implants, - List(unk9.toList, tut, fte, unk3, unk2) + List(imprinting.toList, tut, fte, unk3, unk2) ) )) :: ("unkC" | bool) :: @@ -624,16 +626,16 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { }) ).exmap[DetailedCharacterB]( { - case u1 :: implants :: u2 :: u3 :: fte :: tut :: u4 :: u5 :: u6 :: u7 :: u8 :: u9 :: uA :: uB :: uC :: cosmetics :: HNil => + case u1 :: implants :: u2 :: u3 :: fte :: tut :: u4 :: u5 :: u6 :: u7 :: u8 :: imprint :: uA :: uB :: uC :: cosmetics :: HNil => Attempt.successful( - DetailedCharacterB(u1, implants, u2, u3, fte, tut, u4, u5, u6, u7, u8, u9, uA, uB, uC, cosmetics)( + DetailedCharacterB(u1, implants, u2, u3, fte, tut, u4, u5, u6, u7, u8, imprint, uA, uB, uC, cosmetics)( bep, pad_length ) ) }, { - case DetailedCharacterB(u1, implants, u2, u3, fte, tut, u4, u5, u6, u7, u8, u9, uA, uB, uC, cosmetics) => + case DetailedCharacterB(u1, implants, u2, u3, fte, tut, u4, u5, u6, u7, u8, imprint, uA, uB, uC, cosmetics) => val implantList = (0 until BattleRank.withExperience(bep).implantSlots) .map(index => { implants.lift(index) match { @@ -646,7 +648,7 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { if (bep >= BattleRank.BR24.experience) cosmetics.orElse(Some(Set[Cosmetic]())) else None Attempt.successful( - u1 :: implantList :: u2 :: u3 :: fte :: tut :: u4 :: u5 :: u6 :: u7 :: u8 :: u9 :: uA :: uB :: uC :: cos :: HNil + u1 :: implantList :: u2 :: u3 :: fte :: tut :: u4 :: u5 :: u6 :: u7 :: u8 :: imprint :: uA :: uB :: uC :: cos :: HNil ) } ) diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/MountableInventory.scala b/src/main/scala/net/psforever/packet/game/objectcreate/MountableInventory.scala new file mode 100644 index 000000000..56eabc0e7 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/objectcreate/MountableInventory.scala @@ -0,0 +1,335 @@ +// Copyright (c) 2017-2021 PSForever +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.PacketHelpers +import net.psforever.types.PlanetSideGUID +import scodec.Attempt.Successful +import scodec.Codec +import scodec.codecs._ +import shapeless.HNil //note: do not import shapeless.:: at top level; it messes up List's :: functionality + +import scala.collection.mutable.ListBuffer + +/* +Originally located in `VehicleData` and duplicated in `BattleFrameRoboticsData`, extracted to here and shared. + */ +object MountableInventory { +/** + * A special method of handling mounted players within the same inventory space as normal `Equipment` can be encountered + * before restoring normal inventory operations. + * @see `custom_inventory_codec(Long)` + * @see `InitialStreamLengthToSeatEntries(Boolean, VehicleFormat)` + * @param hasVelocity the presence of a velocity field - `vel` - in the `PlacementData` object for this vehicle + * @param format the subtype for this vehicle + * @return a `Codec` that translates `InventoryData` + */ + def custom_inventory_codec(hasVelocity: Boolean, format: VehicleFormat.Type): Codec[InventoryData] = + custom_inventory_codec(InitialStreamLengthToSeatEntries(hasVelocity, format)) + + /** + * A special method of handling mounted players within the same inventory space as normal `Equipment` can be encountered + * before restoring normal inventory operations.
+ *
+ * Due to variable-length fields within `PlayerData` extracted from the input, + * the distance of the bit(stream) vector to the initial inventory entry is calculated + * to produce the initial value for padding the `PlayerData` object's name field. + * After player-related entries have been extracted and processed in isolation, + * the remainder of the inventory must be handled as standard inventory + * and finally both groups must be repackaged into a single standard `InventoryData` object. + * Due to the unique value for the mounted players that must be updated for each entry processed, + * the entries are temporarily formatted into a linked list before being put back into a normal `List`.
+ *
+ * 6 June 2018:
+ * Due to curious behavior in the vehicle mount access controls, + * please only encode and decode the driver mount even though all seats are currently reachable. + * @param length the distance in bits to the first inventory entry + * @return a `Codec` that translates `InventoryData` + */ + def custom_inventory_codec(length: Long): Codec[InventoryData] = { + import shapeless.:: + ( + uint8 >>:~ { size => + uint2 :: + (inventory_seat_codec( + length, //length of stream until current mount + CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next mount + ) >>:~ { seats => + PacketHelpers.listOfNSized(size - countSeats(seats), InternalSlot.codec).hlist + }) + } + ).xmap[InventoryData]( + { + case _ :: _ :: None :: inv :: HNil => + InventoryData(inv) + + case _ :: _ :: seats :: inv :: HNil => + InventoryData(unlinkSeats(seats) ++ inv) + }, + { + case InventoryData(inv) => + val (seats, slots) = inv.partition(entry => entry.objectClass == ObjectClass.avatar) + inv.size :: 0 :: chainSeats(seats) :: slots :: HNil + } + ) + } + + import net.psforever.packet.game.objectcreate.{PlayerData => Player_Data} + + /** + * Constructor that ignores the coordinate information + * and performs a vehicle-unique calculation of the padding value. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are mounted. + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param inventory the player's inventory + * @param drawn_slot the holster that is initially drawn + * @param accumulative the input position for the stream up to which this entry; + * used to calculate the padding value for the player's name in `CharacterAppearanceData` + * @return a `PlayerData` object + */ + def PlayerData( + basic_appearance: Int => CharacterAppearanceData, + character_data: (Boolean, Boolean) => CharacterData, + inventory: InventoryData, + drawn_slot: DrawnSlot.Type, + accumulative: Long + ): Player_Data = { + val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative)) + Player_Data(None, appearance, character_data(appearance.b.backpack, true), Some(inventory), drawn_slot)(false) + } + + /** + * Constructor for `PlayerData` that ignores the coordinate information and the inventory + * and performs a vehicle-unique calculation of the padding value. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are mounted. + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param drawn_slot the holster that is initially drawn + * @param accumulative the input position for the stream up to which this entry; + * used to calculate the padding value for the player's name in `CharacterAppearanceData` + * @return a `PlayerData` object + */ + def PlayerData( + basic_appearance: Int => CharacterAppearanceData, + character_data: (Boolean, Boolean) => CharacterData, + drawn_slot: DrawnSlot.Type, + accumulative: Long + ): Player_Data = { + val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative)) + Player_Data.apply(None, appearance, character_data(appearance.b.backpack, true), None, drawn_slot)(false) + } + + /** + * Distance from the length field of a vehicle creation packet up until the start of the vehicle's inventory data. + * The only field excluded belongs to the original opcode for the packet. + * The parameters outline reasons why the length of the stream would be different + * and are used to determine the exact difference value.
+ *
+ * Note:
+ * 198 includes the `ObjectCreateMessage` packet fields, without parent data, + * the `VehicleData` fields, + * and the first three fields of the `InternalSlot`. + * @see `ObjectCreateMessage` + * @param hasVelocity the presence of a velocity field - `vel` - in the `PlacementData` object for this vehicle + * @param format the subtype for this vehicle + * @return the length of the bitstream + */ + def InitialStreamLengthToSeatEntries(hasVelocity: Boolean, format: VehicleFormat.Type): Long = { + 198 + + (if (hasVelocity) 42 else 0) + + (format match { + case VehicleFormat.Utility => 6 + case VehicleFormat.Variant => 8 + case VehicleFormat.Battleframe => 1 + case VehicleFormat.BattleframeFlight => 2 + case _ => 0 + }) + } + + /** + * Increment the distance to the next mounted player's `name` field with the length of the previous entry, + * then calculate the new padding value for that next entry's `name` field. + * @param base the original distance to the last entry + * @param next the length of the last entry, if one was parsed + * @return the padding value, 0-7 bits + */ + def CumulativeSeatedPlayerNamePadding(base: Long, next: Option[StreamBitSize]): Int = { + CumulativeSeatedPlayerNamePadding(base + (next match { + case Some(o) => o.bitsize + case None => 0 + })) + } + + /** + * Calculate the padding value for the next mounted player character's name `String`. + * Due to the depth of seated player characters, the `name` field can have a variable amount of padding + * between the string size field and the first character. + * Specifically, the padding value is the number of bits after the size field + * that would cause the first character of the name to be aligned to the first bit of the next byte. + * The 35 counts the object class, unique identifier, and slot fields of the enclosing `InternalSlot`. + * The 23 counts all of the fields before the player's `name` field in `CharacterAppearanceData`. + * @see `InternalSlot`
+ * `CharacterAppearanceData.name`
+ * `VehicleData.InitialStreamLengthToSeatEntries` + * @param accumulative current entry stream offset (start of this player's entry) + * @return the padding value, 0-7 bits + */ + def CumulativeSeatedPlayerNamePadding(accumulative: Long): Int = { + Player_Data.ByteAlignmentPadding(accumulative + 23 + 35) + } + + /** + * The format for the linked list of extracted mounted `PlayerData`. + * @param seat data for this entry extracted via `PlayerData` + * @param next the next entry + */ + private case class InventorySeat(seat: Option[InternalSlot], next: Option[InventorySeat]) + + /** + * Look ahead at the next value to determine if it is an example of a player character + * and would be processed as a `PlayerData` object. + * Update the stream read position with each extraction. + * Continue to process values so long as they represent player character data. + * @param length the distance in bits to the current inventory entry + * @param offset the padding value for this entry's player character's `name` field + * @return a recursive `Codec` that translates subsequent `PlayerData` entries until exhausted + */ + private def inventory_seat_codec(length: Long, offset: Int): Codec[Option[InventorySeat]] = { + import shapeless.:: + ( + PacketHelpers.peek(uintL(11)) >>:~ { objClass => + conditional(objClass == ObjectClass.avatar, seat_codec(offset)) >>:~ { seat => + conditional( + objClass == ObjectClass.avatar, + inventory_seat_codec( + { //length of stream until next mount + length + (seat match { + case Some(o) => o.bitsize + case None => 0 + }) + }, + CumulativeSeatedPlayerNamePadding(length, seat) //calculated offset of name field in next mount + ) + ).hlist + } + } + ).exmap[Option[InventorySeat]]( + { + case _ :: None :: None :: HNil => + Successful(None) + + case _ :: slot :: Some(next) :: HNil => + Successful(Some(InventorySeat(slot, next))) + }, + { + case None => + Successful(0 :: None :: None :: HNil) + + case Some(InventorySeat(slot, None)) => + Successful(ObjectClass.avatar :: slot :: None :: HNil) + + case Some(InventorySeat(slot, next)) => + Successful(ObjectClass.avatar :: slot :: Some(next) :: HNil) + } + ) + } + + /** + * Translate data the is verified to involve a player who is seated (mounted) to the parent object at a given slot. + * The operation performed by this `Codec` is very similar to `InternalSlot.codec`. + * @param pad the padding offset for the player's name; + * 0-7 bits; + * this padding value must recalculate for each represented mount + * @see `CharacterAppearanceData`
+ * `VehicleData.InitialStreamLengthToSeatEntries`
+ * `CumulativeSeatedPlayerNamePadding` + * @return a `Codec` that translates `PlayerData` + */ + private def seat_codec(pad: Int): Codec[InternalSlot] = { + import shapeless.:: + ( + ("objectClass" | uintL(11)) :: + ("guid" | PlanetSideGUID.codec) :: + ("parentSlot" | PacketHelpers.encodedStringSize) :: + ("obj" | Player_Data.codec(pad)) + ).xmap[InternalSlot]( + { + case objectClass :: guid :: parentSlot :: obj :: HNil => + InternalSlot(objectClass, guid, parentSlot, obj) + }, + { + case InternalSlot(objectClass, guid, parentSlot, obj) => + objectClass :: guid :: parentSlot :: obj.asInstanceOf[PlayerData] :: HNil + } + ) + } + + /** + * Count the number of entries in a linked list. + * @param chain the head of the linked list + * @return the number of entries + */ + private def countSeats(chain: Option[InventorySeat]): Int = { + chain match { + case Some(_) => + var curr = chain + var count = 0 + do { + val link = curr.get + count += (if (link.seat.nonEmpty) { 1 } + else { 0 }) + curr = link.next + } while (curr.nonEmpty) + count + + case None => + 0 + } + } + + /** + * Transform a linked list of `InventorySlot` slot objects into a formal list of `InternalSlot` objects. + * @param chain the head of the linked list + * @return a proper list of the contents of the input linked list + */ + private def unlinkSeats(chain: Option[InventorySeat]): List[InternalSlot] = { + var curr = chain + val out = new ListBuffer[InternalSlot] + while (curr.isDefined) { + val link = curr.get + link.seat match { + case None => + curr = None + case Some(seat) => + out += seat + curr = link.next + } + } + out.toList + } + + /** + * Transform a formal list of `InternalSlot` objects into a linked list of `InventorySlot` slot objects. + * @param list a proper list of objects + * @return a linked list composed of the contents of the input list + */ + private def chainSeats(list: List[InternalSlot]): Option[InventorySeat] = { + list match { + case Nil => + None + case x :: Nil => + Some(InventorySeat(Some(x), None)) + case _ :: _ => + var link = InventorySeat(Some(list.last), None) //build the chain in reverse order, starting with the last entry + list.reverse + .drop(1) + .foreach { seat => + link = InventorySeat(Some(seat), Some(link)) + } + Some(link) + } + } +} + diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala index 804f9ec72..c420e59b5 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -123,10 +123,16 @@ object ObjectClass { final val apc_weapon_systemd_nc = 76 final val apc_weapon_systemd_tr = 77 final val apc_weapon_systemd_vs = 78 + final val aphelion_armor_siphon = 80 + final val aphelion_armor_siphon_left = 81 + final val aphelion_armor_siphon_right = 82 final val aphelion_immolation_cannon = 85 final val aphelion_laser = 88 final val aphelion_laser_left = 90 final val aphelion_laser_right = 92 + final val aphelion_ntu_siphon = 93 + final val aphelion_ntu_siphon_left = 94 + final val aphelion_ntu_siphon_right = 95 final val aphelion_plasma_rocket_pod = 98 final val aphelion_ppa = 100 final val aphelion_ppa_left = 102 @@ -144,6 +150,9 @@ object ObjectClass { final val bolt_driver = 146 final val chainblade = 175 final val chaingun_p = 177 + final val colossus_armor_siphon = 182 + final val colossus_armor_siphon_left = 183 + final val colossus_armor_siphon_right = 184 final val colossus_burster = 185 final val colossus_burster_left = 187 final val colossus_burster_right = 189 @@ -152,6 +161,9 @@ object ObjectClass { final val colossus_chaingun_right = 194 final val colossus_cluster_bomb_pod = 196 final val colossus_dual_100mm_cannons = 198 + final val colossus_ntu_siphon = 201 + final val colossus_ntu_siphon_left = 202 + final val colossus_ntu_siphon_right = 203 final val colossus_tank_cannon = 204 final val colossus_tank_cannon_left = 206 final val colossus_tank_cannon_right = 208 @@ -205,6 +217,9 @@ object ObjectClass { final val oicw = 599 final val particle_beam_magrider = 628 final val pellet_gun = 629 + final val peregrine_armor_siphon = 633 + final val peregrine_armor_siphon_left = 634 + final val peregrine_armor_siphon_right = 635 final val peregrine_dual_machine_gun = 636 final val peregrine_dual_machine_gun_left = 638 final val peregrine_dual_machine_gun_right = 640 @@ -212,6 +227,9 @@ object ObjectClass { final val peregrine_mechhammer = 644 final val peregrine_mechhammer_left = 646 final val peregrine_mechhammer_right = 648 + final val peregrine_ntu_siphon = 649 + final val peregrine_ntu_siphon_left = 650 + final val peregrine_ntu_siphon_right = 651 final val peregrine_particle_cannon = 652 final val peregrine_sparrow = 658 final val peregrine_sparrow_left = 660 @@ -297,22 +315,26 @@ object ObjectClass { final val portable_manned_turret_tr = 687 final val portable_manned_turret_vs = 688 //projectiles - final val hunter_seeker_missile_projectile = 405 //phoenix projectile - final val meteor_common = 543 - final val meteor_projectile_b_large = 544 - final val meteor_projectile_b_medium = 545 - final val meteor_projectile_b_small = 546 - final val meteor_projectile_large = 547 - final val meteor_projectile_medium = 548 - final val meteor_projectile_small = 549 - final val phoenix_missile_guided_projectile = 675 //decimator projectile - final val oicw_little_buddy = 601 //scorpion projectile's projectiles - final val oicw_projectile = 602 //scorpion projectile - final val radiator_cload = 717 - final val sparrow_projectile = 792 //nc aa max projectile - final val starfire_projectile = 831 //vs aa max projectile - final val striker_missile_targeting_projectile = 841 //striker projectile - final val wasp_rocket_projectile = 1001 //wasp projectile + final val aphelion_plasma_cloud = 96 + final val flamethrower_fire_cloud = 301 + final val hunter_seeker_missile_projectile = 405 //phoenix projectile + final val maelstrom_grenade_damager = 464 + final val meteor_common = 543 + final val meteor_projectile_b_large = 544 + final val meteor_projectile_b_medium = 545 + final val meteor_projectile_b_small = 546 + final val meteor_projectile_large = 547 + final val meteor_projectile_medium = 548 + final val meteor_projectile_small = 549 + final val peregrine_particle_cannon_radiation_cloud = 655 + final val phoenix_missile_guided_projectile = 675 //decimator projectile + final val oicw_little_buddy = 601 //scorpion projectile's projectiles + final val oicw_projectile = 602 //scorpion projectile + final val radiator_cloud = 717 + final val sparrow_projectile = 792 //nc aa max projectile + final val starfire_projectile = 831 //vs aa max projectile + final val striker_missile_targeting_projectile = 841 //striker projectile + final val wasp_rocket_projectile = 1001 //wasp projectile //vehicles final val apc_destroyed = 65 final val apc_tr = 67 //juggernaut @@ -435,7 +457,7 @@ object ObjectClass { def ByName(name: String): Int = { // This whole thing is a dirty "temporary" hack so I don't have to make a huge map out of the above object vars by hand // Forgive me. - if (objectClassMap.size == 0) { + if (objectClassMap.isEmpty) { val objectClassMethods = ObjectClass.getClass.getDeclaredMethods objectClassMethods.foreach(x => { if (x.getReturnType.getName == "int" && x.getParameterTypes.isEmpty) { // ints only & Ignore functions with parameters @@ -550,73 +572,17 @@ object ObjectClass { case ObjectClass.winchester_ammo => ConstructorData(DetailedAmmoBoxData.codec, "ammo box") //weapons case ObjectClass.beamer => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.cannon_dropship_20mm => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.anniversary_gun => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.anniversary_guna => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.anniversary_gunb => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_ballgun_l => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_ballgun_r => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_weapon_systema => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_weapon_systemb => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_weapon_systemc => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_weapon_systemc_nc => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_weapon_systemc_tr => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_weapon_systemc_vs => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_weapon_systemd => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_weapon_systemd_nc => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_weapon_systemd_tr => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.apc_weapon_systemd_vs => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aphelion_immolation_cannon => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aphelion_laser => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aphelion_laser_left => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aphelion_laser_right => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aphelion_plasma_rocket_pod => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aphelion_ppa => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aphelion_ppa_left => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aphelion_ppa_right => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aphelion_starfire => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aphelion_starfire_left => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aphelion_starfire_right => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aurora_weapon_systema => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.aurora_weapon_systemb => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.battlewagon_weapon_systema => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.battlewagon_weapon_systemb => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.battlewagon_weapon_systemc => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.battlewagon_weapon_systemd => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.bolt_driver => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.chainblade => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.colossus_burster => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.colossus_burster_left => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.colossus_burster_right => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.colossus_chaingun => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.colossus_chaingun_left => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.colossus_chaingun_right => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.colossus_cluster_bomb_pod => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.colossus_dual_100mm_cannons => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.colossus_tank_cannon => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.colossus_tank_cannon_left => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.colossus_tank_cannon_right => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.cycler => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.dropship_rear_turret => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.energy_gun => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.energy_gun_nc => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.energy_gun_tr => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.energy_gun_vs => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.flail_weapon => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.flamethrower => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.flechette => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.flux_cannon_thresher => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.fluxpod => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.forceblade => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.fragmentation_grenade => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.fury_weapon_systema => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.galaxy_gunship_cannon => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.galaxy_gunship_gun => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.galaxy_gunship_tailgun => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.gauss => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.gauss_cannon => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.grenade_launcher_marauder => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.heavy_rail_beam_magrider => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.heavy_sniper => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.hellfire => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.hunterseeker => ConstructorData(DetailedWeaponData.codec, "weapon") @@ -626,70 +592,32 @@ object ObjectClass { case ObjectClass.katana => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.lancer => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.lasher => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.liberator_25mm_cannon => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.liberator_bomb_bay => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.liberator_weapon_system => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.lightgunship_weapon_system => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.lightning_weapon_system => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.maelstrom => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.magcutter => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.mediumtransport_weapon_systemA => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.mediumtransport_weapon_systemB => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.mini_chaingun => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.oicw => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.nchev_falcon => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.nchev_scattercannon => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.nchev_sparrow => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.particle_beam_magrider => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.peregrine_dual_machine_gun => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.peregrine_dual_machine_gun_left => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.peregrine_dual_machine_gun_right => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.peregrine_dual_rocket_pods => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.peregrine_mechhammer => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.peregrine_mechhammer_left => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.peregrine_mechhammer_right => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.peregrine_particle_cannon => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.peregrine_sparrow => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.peregrine_sparrow_left => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.peregrine_sparrow_right => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.phalanx_avcombo => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.phalanx_flakcombo => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.phalanx_sgl_hevgatcan => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.phoenix => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.prowler_weapon_systemA => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.prowler_weapon_systemB => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.plasma_grenade => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.pulsar => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.pulsed_particle_accelerator => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.punisher => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.quadassault_weapon_system => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.r_shotgun => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.radiator => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.repeater => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.rocklet => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.rotarychaingun_mosquito => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.scythe => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.skyguard_weapon_system => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.spiker => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.spitfire_aa_weapon => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.spitfire_weapon => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.striker => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.suppressor => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.thumper => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.thunderer_weapon_systema => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.thunderer_weapon_systemb => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.trhev_burster => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.trhev_dualcycler => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.trhev_pounder => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.vanguard_weapon_system => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.vanu_sentry_turret_weapon => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.vshev_comet => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.vshev_starfire => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.vshev_quasar => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.vulture_bomb_bay => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.vulture_nose_weapon_system => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.vulture_tail_cannon => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.wasp_weapon_system => ConstructorData(DetailedWeaponData.codec, "weapon") //other weapons case ObjectClass.ace_deployable => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.advanced_missile_launcher_t => ConstructorData(DetailedWeaponData.codec, "weapon") @@ -709,7 +637,6 @@ object ObjectClass { case ObjectClass.lightning_75mm => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.mine_sweeper => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.pellet_gun => ConstructorData(DetailedWeaponData.codec, "weapon") -// case ObjectClass.phantasm_12mm_machinegun => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.six_shooter => ConstructorData(DetailedWeaponData.codec, "weapon") case ObjectClass.winchester => ConstructorData(DetailedWeaponData.codec, "weapon") //medkits @@ -862,17 +789,23 @@ object ObjectClass { case ObjectClass.apc_weapon_systemd_nc => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.apc_weapon_systemd_tr => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.apc_weapon_systemd_vs => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.aphelion_immolation_cannon => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.aphelion_laser => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.aphelion_laser_left => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.aphelion_laser_right => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.aphelion_plasma_rocket_pod => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.aphelion_ppa => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.aphelion_ppa_left => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.aphelion_ppa_right => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.aphelion_starfire => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.aphelion_starfire_left => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.aphelion_starfire_right => ConstructorData(WeaponData.codec, "weapon") + case ObjectClass.aphelion_armor_siphon => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_armor_siphon_left => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_armor_siphon_right => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_immolation_cannon => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_laser => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_laser_left => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_laser_right => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_ntu_siphon => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_ntu_siphon_left => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_ntu_siphon_right => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_plasma_rocket_pod => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_ppa => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_ppa_left => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_ppa_right => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_starfire => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_starfire_left => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_starfire_right => ConstructorData(WeaponData.codec2, "weapon") case ObjectClass.aurora_weapon_systema => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.aurora_weapon_systemb => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.battlewagon_weapon_systema => ConstructorData(WeaponData.codec, "weapon") @@ -882,17 +815,23 @@ object ObjectClass { case ObjectClass.bolt_driver => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.chainblade => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.chaingun_p => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.colossus_burster => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.colossus_burster_left => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.colossus_burster_right => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.colossus_chaingun => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.colossus_chaingun_left => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.colossus_chaingun_right => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.colossus_cluster_bomb_pod => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.colossus_dual_100mm_cannons => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.colossus_tank_cannon => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.colossus_tank_cannon_left => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.colossus_tank_cannon_right => ConstructorData(WeaponData.codec, "weapon") + case ObjectClass.colossus_armor_siphon => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_armor_siphon_left => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_armor_siphon_right => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_burster => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_burster_left => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_burster_right => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_chaingun => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_chaingun_left => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_chaingun_right => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_cluster_bomb_pod => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_dual_100mm_cannons => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_ntu_siphon => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_ntu_siphon_left => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_ntu_siphon_right => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_tank_cannon => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_tank_cannon_left => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_tank_cannon_right => ConstructorData(WeaponData.codec2, "weapon") case ObjectClass.cycler => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.dropship_rear_turret => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.energy_gun_nc => ConstructorData(WeaponData.codec, "weapon") @@ -938,17 +877,23 @@ object ObjectClass { case ObjectClass.oicw => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.particle_beam_magrider => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.pellet_gun => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.peregrine_dual_machine_gun => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.peregrine_dual_machine_gun_left => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.peregrine_dual_machine_gun_right => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.peregrine_dual_rocket_pods => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.peregrine_mechhammer => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.peregrine_mechhammer_left => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.peregrine_mechhammer_right => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.peregrine_particle_cannon => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.peregrine_sparrow => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.peregrine_sparrow_left => ConstructorData(WeaponData.codec, "weapon") - case ObjectClass.peregrine_sparrow_right => ConstructorData(WeaponData.codec, "weapon") + case ObjectClass.peregrine_armor_siphon => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_armor_siphon_left => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_armor_siphon_right => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_dual_machine_gun => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_dual_machine_gun_left => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_dual_machine_gun_right => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_dual_rocket_pods => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_mechhammer => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_mechhammer_left => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_mechhammer_right => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_ntu_siphon => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_ntu_siphon_left => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_ntu_siphon_right => ConstructorData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_particle_cannon => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_sparrow => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_sparrow_left => ConstructorData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_sparrow_right => ConstructorData(WeaponData.codec2, "weapon") case ObjectClass.phalanx_avcombo => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.phalanx_flakcombo => ConstructorData(WeaponData.codec, "weapon") case ObjectClass.phalanx_sgl_hevgatcan => ConstructorData(WeaponData.codec, "weapon") @@ -1101,7 +1046,6 @@ object ObjectClass { case ObjectClass.flux_cannon_thresher_battery => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.fluxpod_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.frag_cartridge => DroppedItemData(CommonFieldData.codec2, "ammo box") -// case ObjectClass.frag_grenade_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.gauss_cannon_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.grenade => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.health_canister => DroppedItemData(CommonFieldData.codec2, "ammo box") @@ -1110,7 +1054,6 @@ object ObjectClass { case ObjectClass.hellfire_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.hunter_seeker_missile => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.jammer_cartridge => DroppedItemData(CommonFieldData.codec2, "ammo box") -// case ObjectClass.jammer_grenade_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.lancer_cartridge => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.liberator_bomb => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.maelstrom_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") @@ -1128,7 +1071,6 @@ object ObjectClass { case ObjectClass.phalanx_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.phoenix_missile => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.plasma_cartridge => DroppedItemData(CommonFieldData.codec2, "ammo box") -// case ObjectClass.plasma_grenade_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.pounder_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.pulse_battery => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.quasar_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") @@ -1144,7 +1086,6 @@ object ObjectClass { case ObjectClass.spitfire_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.starfire_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.striker_missile_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") -// case ObjectClass.trek_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.upgrade_canister => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.wasp_gun_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") case ObjectClass.wasp_rocket_ammo => DroppedItemData(CommonFieldData.codec2, "ammo box") @@ -1154,43 +1095,49 @@ object ObjectClass { case ObjectClass.anniversary_gun => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.anniversary_guna => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.anniversary_gunb => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.aphelion_immolation_cannon => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.aphelion_laser => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.aphelion_laser_left => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.aphelion_laser_right => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.aphelion_plasma_rocket_pod => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.aphelion_ppa => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.aphelion_ppa_left => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.aphelion_ppa_right => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.aphelion_starfire => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.aphelion_starfire_left => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.aphelion_starfire_right => DroppedItemData(WeaponData.codec, "weapon") + case ObjectClass.aphelion_immolation_cannon => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_armor_siphon => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_armor_siphon_left => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_armor_siphon_right => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_laser => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_laser_left => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_laser_right => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_ntu_siphon => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_ntu_siphon_left => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_ntu_siphon_right => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.aphelion_plasma_rocket_pod => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_ppa => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_ppa_left => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_ppa_right => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_starfire => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_starfire_left => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.aphelion_starfire_right => DroppedItemData(WeaponData.codec2, "weapon") case ObjectClass.bolt_driver => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.chainblade => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.chaingun_p => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.colossus_burster => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.colossus_burster_left => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.colossus_burster_right => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.colossus_chaingun => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.colossus_chaingun_left => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.colossus_chaingun_right => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.colossus_cluster_bomb_pod => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.colossus_dual_100mm_cannons => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.colossus_tank_cannon => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.colossus_tank_cannon_left => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.colossus_tank_cannon_right => DroppedItemData(WeaponData.codec, "weapon") + case ObjectClass.colossus_armor_siphon => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_armor_siphon_left => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_armor_siphon_right => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_burster => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_burster_left => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_burster_right => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_chaingun => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_chaingun_left => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_chaingun_right => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_cluster_bomb_pod => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_dual_100mm_cannons => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_ntu_siphon => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_ntu_siphon_left => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_ntu_siphon_right => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.colossus_tank_cannon => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_tank_cannon_left => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.colossus_tank_cannon_right => DroppedItemData(WeaponData.codec2, "weapon") case ObjectClass.cycler => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.energy_gun => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.energy_gun_nc => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.energy_gun_tr => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.energy_gun_vs => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.flamethrower => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.flechette => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.forceblade => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.fragmentation_grenade => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.gauss => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.heavy_sniper => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.hellfire => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.hunterseeker => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.ilc9 => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.isp => DroppedItemData(WeaponData.codec, "weapon") @@ -1202,17 +1149,23 @@ object ObjectClass { case ObjectClass.magcutter => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.mini_chaingun => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.oicw => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.peregrine_dual_machine_gun => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.peregrine_dual_machine_gun_left => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.peregrine_dual_machine_gun_right => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.peregrine_dual_rocket_pods => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.peregrine_mechhammer => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.peregrine_mechhammer_left => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.peregrine_mechhammer_right => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.peregrine_particle_cannon => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.peregrine_sparrow => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.peregrine_sparrow_left => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.peregrine_sparrow_right => DroppedItemData(WeaponData.codec, "weapon") + case ObjectClass.peregrine_armor_siphon => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_armor_siphon_left => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_armor_siphon_right => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_dual_machine_gun => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_dual_machine_gun_left => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_dual_machine_gun_right => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_dual_rocket_pods => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_mechhammer => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_mechhammer_left => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_mechhammer_right => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_ntu_siphon => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_ntu_siphon_left => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_ntu_siphon_right => DroppedItemData(WeaponData.codec2, "siphon") + case ObjectClass.peregrine_particle_cannon => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_sparrow => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_sparrow_left => DroppedItemData(WeaponData.codec2, "weapon") + case ObjectClass.peregrine_sparrow_right => DroppedItemData(WeaponData.codec2, "weapon") case ObjectClass.phoenix => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.pulsar => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.plasma_grenade => DroppedItemData(WeaponData.codec, "weapon") @@ -1222,8 +1175,6 @@ object ObjectClass { case ObjectClass.repeater => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.rocklet => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.spiker => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.spitfire_aa_weapon => DroppedItemData(WeaponData.codec, "weapon") -// case ObjectClass.spitfire_weapon => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.striker => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.suppressor => DroppedItemData(WeaponData.codec, "weapon") case ObjectClass.thumper => DroppedItemData(WeaponData.codec, "weapon") @@ -1278,64 +1229,67 @@ object ObjectClass { case ObjectClass.portable_manned_turret_vs => ConstructorData(OneMannedFieldTurretData.codec, "field turret") case ObjectClass.router_telepad_deployable => DroppedItemData(TelepadDeployableData.codec, "telepad deployable") //projectiles - case ObjectClass.hunter_seeker_missile_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") - case ObjectClass.meteor_common => ConstructorData(RemoteProjectileData.codec, "meteor") - case ObjectClass.meteor_projectile_b_large => ConstructorData(RemoteProjectileData.codec, "meteor") - case ObjectClass.meteor_projectile_b_medium => ConstructorData(RemoteProjectileData.codec, "meteor") - case ObjectClass.meteor_projectile_b_small => ConstructorData(RemoteProjectileData.codec, "meteor") - case ObjectClass.meteor_projectile_large => ConstructorData(RemoteProjectileData.codec, "meteor") - case ObjectClass.meteor_projectile_medium => ConstructorData(RemoteProjectileData.codec, "meteor") - case ObjectClass.meteor_projectile_small => ConstructorData(RemoteProjectileData.codec, "meteor") - case ObjectClass.phoenix_missile_guided_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") - case ObjectClass.oicw_little_buddy => ConstructorData(RemoteProjectileData.codec, "projectile") - case ObjectClass.oicw_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") - case ObjectClass.sparrow_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") - case ObjectClass.starfire_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") - case ObjectClass.striker_missile_targeting_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") - case ObjectClass.wasp_rocket_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") + case ObjectClass.aphelion_plasma_cloud => ConstructorData(RadiationCloudData.codec, "radiation cloud") + case ObjectClass.hunter_seeker_missile_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") + case ObjectClass.meteor_common => ConstructorData(RemoteProjectileData.codec, "meteor") + case ObjectClass.meteor_projectile_b_large => ConstructorData(RemoteProjectileData.codec, "meteor") + case ObjectClass.meteor_projectile_b_medium => ConstructorData(RemoteProjectileData.codec, "meteor") + case ObjectClass.meteor_projectile_b_small => ConstructorData(RemoteProjectileData.codec, "meteor") + case ObjectClass.meteor_projectile_large => ConstructorData(RemoteProjectileData.codec, "meteor") + case ObjectClass.meteor_projectile_medium => ConstructorData(RemoteProjectileData.codec, "meteor") + case ObjectClass.meteor_projectile_small => ConstructorData(RemoteProjectileData.codec, "meteor") + case ObjectClass.peregrine_particle_cannon_radiation_cloud => ConstructorData(RadiationCloudData.codec, "radiation cloud") + case ObjectClass.phoenix_missile_guided_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") + case ObjectClass.oicw_little_buddy => ConstructorData(RemoteProjectileData.codec, "projectile") + case ObjectClass.oicw_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") + case ObjectClass.radiator_cloud => ConstructorData(RadiationCloudData.codec, "radiation cloud") + case ObjectClass.sparrow_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") + case ObjectClass.starfire_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") + case ObjectClass.striker_missile_targeting_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") + case ObjectClass.wasp_rocket_projectile => ConstructorData(RemoteProjectileData.codec, "projectile") //vehicles - case ObjectClass.ams => ConstructorData(VehicleData.codec(VehicleFormat.Utility), "ams") - case ObjectClass.ams_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.ant => ConstructorData(VehicleData.codec(VehicleFormat.Utility), "ant") - case ObjectClass.ant_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.apc_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.apc_nc => ConstructorData(VehicleData.codec, "vehicle") - case ObjectClass.apc_tr => ConstructorData(VehicleData.codec, "vehicle") - case ObjectClass.apc_vs => ConstructorData(VehicleData.codec, "vehicle") + case ObjectClass.ams => ConstructorData(VehicleData.codec(VehicleFormat.Utility), "ams") + case ObjectClass.ams_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.ant => ConstructorData(VehicleData.codec(VehicleFormat.Utility), "ant") + case ObjectClass.ant_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.apc_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.apc_nc => ConstructorData(VehicleData.codec, "vehicle") + case ObjectClass.apc_tr => ConstructorData(VehicleData.codec, "vehicle") + case ObjectClass.apc_vs => ConstructorData(VehicleData.codec, "vehicle") //case ObjectClass.aphelion_destroyed => normal @ 0 health - case ObjectClass.aphelion_flight => ConstructorData(VehicleData.codec(VehicleFormat.Battleframe), "vehicle") - case ObjectClass.aphelion_gunner => ConstructorData(VehicleData.codec(VehicleFormat.Battleframe), "vehicle") - case ObjectClass.aurora => ConstructorData(VehicleData.codec, "vehicle") - case ObjectClass.battlewagon => ConstructorData(VehicleData.codec, "vehicle") + case ObjectClass.aphelion_flight => ConstructorData(BattleFrameRoboticsData.codec_flight, "bfr") + case ObjectClass.aphelion_gunner => ConstructorData(BattleFrameRoboticsData.codec, "bfr") + case ObjectClass.aurora => ConstructorData(VehicleData.codec, "vehicle") + case ObjectClass.battlewagon => ConstructorData(VehicleData.codec, "vehicle") //case ObjectClass.colossus_destroyed => normal @ 0 health - case ObjectClass.colossus_flight => ConstructorData(VehicleData.codec(VehicleFormat.Battleframe), "vehicle") - case ObjectClass.colossus_gunner => ConstructorData(VehicleData.codec(VehicleFormat.Battleframe), "vehicle") - case ObjectClass.droppod => ConstructorData(DroppodData.codec, "droppod") - case ObjectClass.dropship => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") - case ObjectClass.dropship_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.flail => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") - case ObjectClass.flail_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.fury => ConstructorData(VehicleData.codec, "vehicle") - case ObjectClass.galaxy_gunship => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") - case ObjectClass.liberator => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") - case ObjectClass.liberator_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.lightgunship => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") - case ObjectClass.lightgunship_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.lightning => ConstructorData(VehicleData.codec, "vehicle") - case ObjectClass.lightning_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.lodestar => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") - case ObjectClass.lodestar_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.magrider => ConstructorData(VehicleData.codec, "vehicle") - case ObjectClass.magrider_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.mediumtransport => ConstructorData(VehicleData.codec, "vehicle") - case ObjectClass.mediumtransport_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.mosquito => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") - case ObjectClass.mosquito_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") - case ObjectClass.orbital_shuttle => ConstructorData(OrbitalShuttleData.codec_pos, "HART") + case ObjectClass.colossus_flight => ConstructorData(BattleFrameRoboticsData.codec_flight, "bfr") + case ObjectClass.colossus_gunner => ConstructorData(BattleFrameRoboticsData.codec, "bfr") + case ObjectClass.droppod => ConstructorData(DroppodData.codec, "droppod") + case ObjectClass.dropship => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") + case ObjectClass.dropship_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.flail => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") + case ObjectClass.flail_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.fury => ConstructorData(VehicleData.codec, "vehicle") + case ObjectClass.galaxy_gunship => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") + case ObjectClass.liberator => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") + case ObjectClass.liberator_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.lightgunship => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") + case ObjectClass.lightgunship_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.lightning => ConstructorData(VehicleData.codec, "vehicle") + case ObjectClass.lightning_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.lodestar => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") + case ObjectClass.lodestar_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.magrider => ConstructorData(VehicleData.codec, "vehicle") + case ObjectClass.magrider_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.mediumtransport => ConstructorData(VehicleData.codec, "vehicle") + case ObjectClass.mediumtransport_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.mosquito => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") + case ObjectClass.mosquito_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") + case ObjectClass.orbital_shuttle => ConstructorData(OrbitalShuttleData.codec_pos, "HART") //case ObjectClass.peregrine_destroyed => normal @ 0 health - case ObjectClass.peregrine_flight => ConstructorData(VehicleData.codec(VehicleFormat.Battleframe), "vehicle") - case ObjectClass.peregrine_gunner => ConstructorData(VehicleData.codec(VehicleFormat.Battleframe), "vehicle") - case ObjectClass.phantasm => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") + case ObjectClass.peregrine_flight => ConstructorData(BattleFrameRoboticsData.codec_flight, "bfr") + case ObjectClass.peregrine_gunner => ConstructorData(BattleFrameRoboticsData.codec, "bfr") + case ObjectClass.phantasm => ConstructorData(VehicleData.codec(VehicleFormat.Variant), "vehicle") //case ObjectClass.phantasm_destroyed => normal @ 0 health case ObjectClass.prowler => ConstructorData(VehicleData.codec, "vehicle") case ObjectClass.prowler_destroyed => ConstructorData(DestroyedVehicleData.codec, "wreckage") diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/RadiationCloudData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/RadiationCloudData.scala new file mode 100644 index 000000000..3575d7eb8 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/objectcreate/RadiationCloudData.scala @@ -0,0 +1,41 @@ +// Copyright (c) 2021 PSForever +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import net.psforever.types.PlanetSideEmpire +import scodec.codecs._ +import scodec.Codec +import shapeless.{::, HNil} + +/** + * A representation of a stationary projectile field. + * @param pos where the vehicle is and how it is oriented in the game world + * @param faction faction affinity + */ +final case class RadiationCloudData( + pos: PlacementData, + faction: PlanetSideEmpire.Value + ) extends ConstructorData { + override def bitsize: Long = { + pos.bitsize + 24L + } +} + +object RadiationCloudData extends Marshallable[RadiationCloudData] { + implicit val codec: Codec[RadiationCloudData] = { + ("pos" | PlacementData.codec) :: + ignore(size = 1) :: + ("faction" | PlanetSideEmpire.codec) :: + uint(bits = 4) :: + ignore(size = 17) + }.xmap[RadiationCloudData] ( + { + case pos :: _ :: fac :: _ :: _ :: HNil => + RadiationCloudData(pos, fac) + }, + { + case RadiationCloudData(pos, fac) => + pos :: () :: fac :: 4 :: () :: HNil + } + ) +} diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala index 2c311853d..6e912423a 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala @@ -1,14 +1,12 @@ // Copyright (c) 2017 PSForever package net.psforever.packet.game.objectcreate -import net.psforever.packet.{Marshallable, PacketHelpers} +import net.psforever.packet.Marshallable import scodec.Attempt.{Failure, Successful} import scodec.{Attempt, Codec, Err} import shapeless.HNil import scodec.codecs._ -import net.psforever.types.{DriveState, PlanetSideGUID} - -import scala.collection.mutable.ListBuffer +import net.psforever.types.DriveState /** * An `Enumeration` of the various formats that known structures that the stream of bits for `VehicleData` can assume. @@ -16,8 +14,7 @@ import scala.collection.mutable.ListBuffer object VehicleFormat extends Enumeration { type Type = Value - val Battleframe, //future expansion? - Normal, Utility, Variant = Value + val Battleframe, BattleframeFlight, Normal, Utility, Variant = Value } /** @@ -102,7 +99,6 @@ final case class VehicleData( } object VehicleData extends Marshallable[VehicleData] { - /** * Overloaded constructor for specifically handling `Normal` vehicle format. * @param basic a field that encompasses some data used by the vehicle, including `faction` and `owner` @@ -168,54 +164,6 @@ object VehicleData extends Marshallable[VehicleData] { ) } - import net.psforever.packet.game.objectcreate.{PlayerData => Player_Data} - - /** - * Constructor that ignores the coordinate information - * and performs a vehicle-unique calculation of the padding value. - * It passes information between the three major divisions for the purposes of offset calculations. - * This constructor should be used for players that are mounted. - * @param basic_appearance a curried function for the common fields regarding the the character's appearance - * @param character_data a curried function for the class-specific data that explains about the character - * @param inventory the player's inventory - * @param drawn_slot the holster that is initially drawn - * @param accumulative the input position for the stream up to which this entry; - * used to calculate the padding value for the player's name in `CharacterAppearanceData` - * @return a `PlayerData` object - */ - def PlayerData( - basic_appearance: Int => CharacterAppearanceData, - character_data: (Boolean, Boolean) => CharacterData, - inventory: InventoryData, - drawn_slot: DrawnSlot.Type, - accumulative: Long - ): Player_Data = { - val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative)) - Player_Data(None, appearance, character_data(appearance.b.backpack, true), Some(inventory), drawn_slot)(false) - } - - /** - * Constructor for `PlayerData` that ignores the coordinate information and the inventory - * and performs a vehicle-unique calculation of the padding value. - * It passes information between the three major divisions for the purposes of offset calculations. - * This constructor should be used for players that are mounted. - * @param basic_appearance a curried function for the common fields regarding the the character's appearance - * @param character_data a curried function for the class-specific data that explains about the character - * @param drawn_slot the holster that is initially drawn - * @param accumulative the input position for the stream up to which this entry; - * used to calculate the padding value for the player's name in `CharacterAppearanceData` - * @return a `PlayerData` object - */ - def PlayerData( - basic_appearance: Int => CharacterAppearanceData, - character_data: (Boolean, Boolean) => CharacterData, - drawn_slot: DrawnSlot.Type, - accumulative: Long - ): Player_Data = { - val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative)) - Player_Data.apply(None, appearance, character_data(appearance.b.backpack, true), None, drawn_slot)(false) - } - private val driveState8u = uint8.xmap[DriveState.Value]( n => DriveState(n), n => n.id @@ -280,22 +228,16 @@ object VehicleData extends Marshallable[VehicleData] { ( ("pos" | PlacementData.codec) >>:~ { pos => ("data" | CommonFieldData.codec2(false)) :: - ("unk3" | bool) :: - ("health" | uint8L) :: - ("unk4" | bool) :: //usually 0 - ("no_mount_points" | bool) :: - ("driveState" | driveState8u) :: //used for deploy state - ("unk5" | bool) :: //unknown but generally false; can cause stream misalignment if set when unexpectedly - ("unk6" | bool) :: - ("cloak" | bool) :: //cloak as wraith, phantasm - conditional( - vehicle_type != VehicleFormat.Normal, - "vehicle_format_data" | selectFormatReader(vehicle_type) - ) :: //padding? - optional( - bool, - "inventory" | custom_inventory_codec(InitialStreamLengthToSeatEntries(pos.vel.isDefined, vehicle_type)) - ) + ("unk3" | bool) :: + ("health" | uint8L) :: + ("unk4" | bool) :: //usually 0 + ("no_mount_points" | bool) :: + ("driveState" | driveState8u) :: //used for deploy state + ("unk5" | bool) :: //unknown but generally false; can cause stream misalignment if set when unexpectedly + ("unk6" | bool) :: + ("cloak" | bool) :: //cloak as wraith, phantasm + conditional(vehicle_type != VehicleFormat.Normal,"vehicle_format_data" | selectFormatReader(vehicle_type)) :: + optional(bool, target = "inventory" | MountableInventory.custom_inventory_codec(pos.vel.isDefined, VehicleFormat.Normal)) } ).exmap[VehicleData]( { @@ -322,261 +264,5 @@ object VehicleData extends Marshallable[VehicleData] { ) } - /** - * Distance from the length field of a vehicle creation packet up until the start of the vehicle's inventory data. - * The only field excluded belongs to the original opcode for the packet. - * The parameters outline reasons why the length of the stream would be different - * and are used to determine the exact difference value.
- * Note:
- * 198 includes the `ObjectCreateMessage` packet fields, without parent data, - * the `VehicleData` fields, - * and the first three fields of the `InternalSlot`. - * @see `ObjectCreateMessage` - * @param hasVelocity the presence of a velocity field - `vel` - in the `PlacementData` object for this vehicle - * @param format the `Codec` subtype for this vehicle - * @return the length of the bitstream - */ - def InitialStreamLengthToSeatEntries(hasVelocity: Boolean, format: VehicleFormat.Type): Long = { - 198 + - (if (hasVelocity) { 42 } - else { 0 }) + - (format match { - case VehicleFormat.Utility => 6 - case VehicleFormat.Variant => 8 - case _ => 0 - }) - } - - /** - * Increment the distance to the next mounted player's `name` field with the length of the previous entry, - * then calculate the new padding value for that next entry's `name` field. - * @param base the original distance to the last entry - * @param next the length of the last entry, if one was parsed - * @return the padding value, 0-7 bits - */ - def CumulativeSeatedPlayerNamePadding(base: Long, next: Option[StreamBitSize]): Int = { - CumulativeSeatedPlayerNamePadding(base + (next match { - case Some(o) => o.bitsize - case None => 0 - })) - } - - /** - * Calculate the padding value for the next mounted player character's name `String`. - * Due to the depth of seated player characters, the `name` field can have a variable amount of padding - * between the string size field and the first character. - * Specifically, the padding value is the number of bits after the size field - * that would cause the first character of the name to be aligned to the first bit of the next byte. - * The 35 counts the object class, unique identifier, and slot fields of the enclosing `InternalSlot`. - * The 23 counts all of the fields before the player's `name` field in `CharacterAppearanceData`. - * @see `InternalSlot`
- * `CharacterAppearanceData.name`
- * `VehicleData.InitialStreamLengthToSeatEntries` - * @param accumulative current entry stream offset (start of this player's entry) - * @return the padding value, 0-7 bits - */ - private def CumulativeSeatedPlayerNamePadding(accumulative: Long): Int = { - Player_Data.ByteAlignmentPadding(accumulative + 23 + 35) - } - - /** - * A special method of handling mounted players within the same inventory space as normal `Equipment` can be encountered - * before restoring normal inventory operations.
- *
- * Due to variable-length fields within `PlayerData` extracted from the input, - * the distance of the bit(stream) vector to the initial inventory entry is calculated - * to produce the initial value for padding the `PlayerData` object's name field. - * After player-related entries have been extracted and processed in isolation, - * the remainder of the inventory must be handled as standard inventory - * and finally both groups must be repackaged into a single standard `InventoryData` object. - * Due to the unique value for the mounted players that must be updated for each entry processed, - * the entries are temporarily formatted into a linked list before being put back into a normal `List`.
- *
- * 6 June 2018:
- * Due to curious behavior in the vehicle mount access controls, - * please only encode and decode the driver mount even though all seats are currently reachable. - * @param length the distance in bits to the first inventory entry - * @return a `Codec` that translates `InventoryData` - */ - private def custom_inventory_codec(length: Long): Codec[InventoryData] = { - import shapeless.:: - ( - uint8 >>:~ { size => - uint2 :: - (inventory_seat_codec( - length, //length of stream until current mount - CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next mount - ) >>:~ { seats => - PacketHelpers.listOfNSized(size - countSeats(seats), InternalSlot.codec).hlist - }) - } - ).xmap[InventoryData]( - { - case _ :: _ :: None :: inv :: HNil => - InventoryData(inv) - - case _ :: _ :: seats :: inv :: HNil => - InventoryData(unlinkSeats(seats) ++ inv) - }, - { - case InventoryData(inv) => - val (seats, slots) = inv.partition(entry => entry.objectClass == ObjectClass.avatar) - inv.size :: 0 :: chainSeats(seats) :: slots :: HNil - } - ) - } - - /** - * The format for the linked list of extracted mounted `PlayerData`. - * @param seat data for this entry extracted via `PlayerData` - * @param next the next entry - */ - private case class InventorySeat(seat: Option[InternalSlot], next: Option[InventorySeat]) - - /** - * Look ahead at the next value to determine if it is an example of a player character - * and would be processed as a `PlayerData` object. - * Update the stream read position with each extraction. - * Continue to process values so long as they represent player character data. - * @param length the distance in bits to the current inventory entry - * @param offset the padding value for this entry's player character's `name` field - * @return a recursive `Codec` that translates subsequent `PlayerData` entries until exhausted - */ - private def inventory_seat_codec(length: Long, offset: Int): Codec[Option[InventorySeat]] = { - import shapeless.:: - ( - PacketHelpers.peek(uintL(11)) >>:~ { objClass => - conditional(objClass == ObjectClass.avatar, seat_codec(offset)) >>:~ { seat => - conditional( - objClass == ObjectClass.avatar, - inventory_seat_codec( - { //length of stream until next mount - length + (seat match { - case Some(o) => o.bitsize - case None => 0 - }) - }, - CumulativeSeatedPlayerNamePadding(length, seat) //calculated offset of name field in next mount - ) - ).hlist - } - } - ).exmap[Option[InventorySeat]]( - { - case _ :: None :: None :: HNil => - Successful(None) - - case _ :: slot :: Some(next) :: HNil => - Successful(Some(InventorySeat(slot, next))) - }, - { - case None => - Successful(0 :: None :: None :: HNil) - - case Some(InventorySeat(slot, None)) => - Successful(ObjectClass.avatar :: slot :: None :: HNil) - - case Some(InventorySeat(slot, next)) => - Successful(ObjectClass.avatar :: slot :: Some(next) :: HNil) - } - ) - } - - /** - * Translate data the is verified to involve a player who is seated (mounted) to the parent object at a given slot. - * The operation performed by this `Codec` is very similar to `InternalSlot.codec`. - * @param pad the padding offset for the player's name; - * 0-7 bits; - * this padding value must recalculate for each represented mount - * @see `CharacterAppearanceData`
- * `VehicleData.InitialStreamLengthToSeatEntries`
- * `CumulativeSeatedPlayerNamePadding` - * @return a `Codec` that translates `PlayerData` - */ - private def seat_codec(pad: Int): Codec[InternalSlot] = { - import shapeless.:: - ( - ("objectClass" | uintL(11)) :: - ("guid" | PlanetSideGUID.codec) :: - ("parentSlot" | PacketHelpers.encodedStringSize) :: - ("obj" | Player_Data.codec(pad)) - ).xmap[InternalSlot]( - { - case objectClass :: guid :: parentSlot :: obj :: HNil => - InternalSlot(objectClass, guid, parentSlot, obj) - }, - { - case InternalSlot(objectClass, guid, parentSlot, obj) => - objectClass :: guid :: parentSlot :: obj.asInstanceOf[PlayerData] :: HNil - } - ) - } - - /** - * Count the number of entries in a linked list. - * @param chain the head of the linked list - * @return the number of entries - */ - private def countSeats(chain: Option[InventorySeat]): Int = { - chain match { - case Some(_) => - var curr = chain - var count = 0 - do { - val link = curr.get - count += (if (link.seat.nonEmpty) { 1 } - else { 0 }) - curr = link.next - } while (curr.nonEmpty) - count - - case None => - 0 - } - } - - /** - * Transform a linked list of `InventorySlot` slot objects into a formal list of `InternalSlot` objects. - * @param chain the head of the linked list - * @return a proper list of the contents of the input linked list - */ - private def unlinkSeats(chain: Option[InventorySeat]): List[InternalSlot] = { - var curr = chain - val out = new ListBuffer[InternalSlot] - while (curr.isDefined) { - val link = curr.get - link.seat match { - case None => - curr = None - case Some(seat) => - out += seat - curr = link.next - } - } - out.toList - } - - /** - * Transform a formal list of `InternalSlot` objects into a linked list of `InventorySlot` slot objects. - * @param list a proper list of objects - * @return a linked list composed of the contents of the input list - */ - private def chainSeats(list: List[InternalSlot]): Option[InventorySeat] = { - list match { - case Nil => - None - case x :: Nil => - Some(InventorySeat(Some(x), None)) - case _ :: _ => - var link = InventorySeat(Some(list.last), None) //build the chain in reverse order, starting with the last entry - list.reverse - .drop(1) - .foreach(seat => { - link = InventorySeat(Some(seat), Some(link)) - }) - Some(link) - } - } - implicit val codec: Codec[VehicleData] = codec(VehicleFormat.Normal) } diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala index a5b1ea2d3..0763dde0f 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala @@ -151,13 +151,13 @@ object WeaponData extends Marshallable[WeaponData] { ) } - implicit val codec: Codec[WeaponData] = ( - ("data" | CommonFieldData.codec) :: - ("fire_mode" | int8) :: - bool :: - optional(bool, "ammo" | InventoryData.codec) :: - ("unk" | bool) - ).exmap[WeaponData]( + private def baseCodec(commonFieldCodec: Codec[CommonFieldData]): Codec[WeaponData] = ( + ("data" | commonFieldCodec) :: + ("fire_mode" | int8) :: + bool :: + optional(bool, "ammo" | InventoryData.codec) :: + ("unk" | bool) + ).exmap[WeaponData]( { case data :: fmode :: false :: Some(InventoryData(ammo)) :: unk :: HNil => val magSize = ammo.size @@ -186,4 +186,8 @@ object WeaponData extends Marshallable[WeaponData] { Attempt.failure(Err("invalid weapon data format")) } ) + + implicit val codec: Codec[WeaponData] = baseCodec(CommonFieldData.codec) + + val codec2: Codec[WeaponData] = baseCodec(CommonFieldData.codec2) } diff --git a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala index 0fcd4fed0..2e3599fec 100644 --- a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala @@ -54,8 +54,7 @@ class HackClearActor() extends Actor { RestartTimer() case HackClearActor.ObjectIsResecured(target) => - val obj = hackedObjects.filter(x => x.target == target).headOption - obj match { + hackedObjects.find { _.target == target } match { case Some(entry: HackClearActor.HackEntry) => hackedObjects = hackedObjects.filterNot(x => x.target == target) entry.target.Actor ! CommonMessages.ClearHack() @@ -68,16 +67,16 @@ class HackClearActor() extends Actor { // Restart the timer in case the object we just removed was the next one scheduled RestartTimer() - case None => ; + case _ => ; } case _ => ; } private def RestartTimer(): Unit = { - if (hackedObjects.length != 0) { + if (hackedObjects.nonEmpty) { val now = System.nanoTime() - val (unhackObjects, stillHackedObjects) = PartitionEntries(hackedObjects, now) + val (_/*unhackObjects*/, stillHackedObjects) = PartitionEntries(hackedObjects, now) stillHackedObjects.headOption match { case Some(hackEntry) => diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala index 4c9e49b16..effcb1912 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleService.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleService.scala @@ -73,6 +73,53 @@ class VehicleService(zone: Zone) extends Actor { VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.EquipmentInSlot(pkt)) ) + case VehicleAction.FrameVehicleState( + player_guid, + vehicle_guid, + unk1, + pos, + orient, + vel, + unk2, + unk3, + unk4, + is_crouched, + unk6, + unk7, + unk8, + unk9, + unkA + ) => + VehicleEvents.publish( + VehicleServiceResponse( + s"/$forChannel/Vehicle", + player_guid, + VehicleResponse.FrameVehicleState( + vehicle_guid, + unk1, + pos, + orient, + vel, + unk2, + unk3, + unk4, + is_crouched, + unk6, + unk7, + unk8, + unk9, + unkA + ) + ) + ) + case VehicleAction.GenericObjectAction(player_guid, guid, code) => + VehicleEvents.publish( + VehicleServiceResponse( + s"/$forChannel/Vehicle", + player_guid, + VehicleResponse.GenericObjectAction(guid, code) + ) + ) case VehicleAction.InventoryState(player_guid, obj, parent_guid, start, con_data) => VehicleEvents.publish( VehicleServiceResponse( diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala b/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala index 8f2b5d7be..49c49b81c 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleServiceMessage.scala @@ -40,6 +40,24 @@ object VehicleAction { slot: Int, equipment: Equipment ) extends Action + final case class FrameVehicleState( + player_guid: PlanetSideGUID, + vehicle_guid: PlanetSideGUID, + unk1: Int, + pos: Vector3, + orient: Vector3, + vel: Option[Vector3], + unk2: Boolean, + unk3: Int, + unk4: Int, + is_crouched: Boolean, + unk6: Boolean, + unk7: Boolean, + unk8: Int, + unk9: Long, + unkA: Long + ) extends Action + final case class GenericObjectAction(player_guid: PlanetSideGUID, guid: PlanetSideGUID, action: Int) extends Action final case class InventoryState( player_guid: PlanetSideGUID, obj: PlanetSideGameObject, diff --git a/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala b/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala index 87b724e91..a22e3129c 100644 --- a/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala +++ b/src/main/scala/net/psforever/services/vehicle/VehicleServiceResponse.scala @@ -33,6 +33,23 @@ object VehicleResponse { ) extends Response final case class DismountVehicle(bailType: BailType.Value, unk2: Boolean) extends Response final case class EquipmentInSlot(pkt: ObjectCreateMessage) extends Response + final case class FrameVehicleState( + vehicle_guid: PlanetSideGUID, + unk1: Int, + pos: Vector3, + orient: Vector3, + vel: Option[Vector3], + unk2: Boolean, + unk3: Int, + unk4: Int, + is_crouched: Boolean, + unk6: Boolean, + unk7: Boolean, + unk8: Int, + unk9: Long, + unkA: Long + ) extends Response + final case class GenericObjectAction(guid: PlanetSideGUID, action: Int) extends Response final case class HitHint(source_guid: PlanetSideGUID) extends Response final case class InventoryState( obj: PlanetSideGameObject, diff --git a/src/main/scala/net/psforever/types/Angular.scala b/src/main/scala/net/psforever/types/Angular.scala index bd27195f8..091795222 100644 --- a/src/main/scala/net/psforever/types/Angular.scala +++ b/src/main/scala/net/psforever/types/Angular.scala @@ -95,4 +95,39 @@ object Angular { } correctedAng } + + /** + * Take an angle in counterclockwise unit circle rotation angles + * and convert it into clockwise compass rose direction angles; + * or, perform the reverse conversion, from clockwise to counterclockwise. + * The calling context must decide the original rotation direction and thus the resultant direction. + * This function can swap back and forth between the two directions by repeated application upon the output value. + * @param angle the original angle in degrees + * @return the rotation flipped angle in degrees within the range of 0 to 359 + */ + def flipClockwise(angle: Float): Float = { + //counterclockwise: 0-degrees starts at East Vector3(1,0,0) + //clockwise: 0-degrees starts at North Vector3(0,1,0) + val boundedAngle = { + //the result will always be -1 < n < 360 + var pos = angle + while (pos < 0) pos = pos + 360f + pos % 360f + } + if (boundedAngle < 91f) { + //1st quarter / quadrant maps 0-90 to 90-0 + 90f - boundedAngle + } else { + val resultingAngle = 450f - boundedAngle + if (boundedAngle < 181f) { + //2nd quarter / 4rd quadrant maps 90-180 to 0-270 + // turn any 360 results into 0 + resultingAngle % 360f + } else { + //3rd quarter / quadrant maps 180-270 to 270-180 + //4th quarter / 2nd quadrant maps 270-0 to 180-90 + resultingAngle + } + } + } } diff --git a/src/main/scala/net/psforever/types/DriveState.scala b/src/main/scala/net/psforever/types/DriveState.scala index 4a511948f..887d93a6e 100644 --- a/src/main/scala/net/psforever/types/DriveState.scala +++ b/src/main/scala/net/psforever/types/DriveState.scala @@ -20,4 +20,6 @@ object DriveState extends Enumeration { val UNK6 = Value(6) // to have Xtoolspar working val State7 = Value(7) //unknown; not encountered on a vehicle that can deploy; functions like Mobile val State127 = Value(127) //unknown + + val Kneeling = Value(-1) //flag bfr kneeling state; should not not encode } diff --git a/src/main/scala/net/psforever/types/LoadoutType.scala b/src/main/scala/net/psforever/types/LoadoutType.scala index a8ea7caf6..1f5940590 100644 --- a/src/main/scala/net/psforever/types/LoadoutType.scala +++ b/src/main/scala/net/psforever/types/LoadoutType.scala @@ -7,7 +7,7 @@ import scodec.codecs.uint2L object LoadoutType extends Enumeration { type Type = Value - val Infantry, Vehicle = Value + val Infantry, Vehicle, Battleframe = Value implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L) } diff --git a/src/main/scala/net/psforever/types/SubsystemComponent.scala b/src/main/scala/net/psforever/types/SubsystemComponent.scala new file mode 100644 index 000000000..2c8edd481 --- /dev/null +++ b/src/main/scala/net/psforever/types/SubsystemComponent.scala @@ -0,0 +1,59 @@ +// Copyright (c) 2021 PSForever +package net.psforever.types + +import enumeratum.values.{IntEnum, IntEnumEntry} + +sealed abstract class SubsystemComponent(val value: Int) extends IntEnumEntry + +object SubsystemComponent extends IntEnum[SubsystemComponent] { + val values = findValues + + case object SiphonTransferEfficiency extends SubsystemComponent(value = 0) + case object SiphonTransferRateA extends SubsystemComponent(value = 1) + + //DamageToken_BFRDestroyed = 2 (visible using weapon guid) + + case object FlightSystemsRechargeRate extends SubsystemComponent(value = 3) + case object FlightSystemsUseRate extends SubsystemComponent(value = 4) + case object FlightSystemsDestroyed extends SubsystemComponent(value = 5) + case object FlightSystemsHorizontalForce extends SubsystemComponent(value = 6) + case object FlightSystemsOffline extends SubsystemComponent(value = 7) + case object FlightSystemsVerticalForce extends SubsystemComponent(value = 8) + + case object MovementServosTransit extends SubsystemComponent(value = 9) + case object MovementServosBackward extends SubsystemComponent(value = 10) + case object MovementServosForward extends SubsystemComponent(value = 11) + case object MovementServosPivotSpeed extends SubsystemComponent(value = 12) + case object MovementServosStrafeSpeed extends SubsystemComponent(value = 13) + + case object SiphonDrainOnly extends SubsystemComponent(value = 14) + case object SiphonStorageCapacity extends SubsystemComponent(value = 15) + case object SiphonTransferRateB extends SubsystemComponent(value = 16) + + case object SensorArrayNoEnemies extends SubsystemComponent(value = 17) + case object SensorArrayNoEnemyAircraft extends SubsystemComponent(value = 18) + case object SensorArrayNoEnemyGroundVehicles extends SubsystemComponent(value = 19) + case object SensorArrayNoEnemyProjectiles extends SubsystemComponent(value = 20) + case object SensorArrayRange extends SubsystemComponent(value = 21) + + case object ShieldGeneratorDestroyed extends SubsystemComponent(value = 22) + case object ShieldGeneratorOffline extends SubsystemComponent(value = 23) + case object ShieldGeneratorRechargeRate extends SubsystemComponent(value = 24) + + case object Trunk extends SubsystemComponent(value = 25) + + case object WeaponSystemsCOFRecovery extends SubsystemComponent(value = 26) + case object WeaponSystemsCOF extends SubsystemComponent(value = 27) + case object WeaponSystemsDestroyed extends SubsystemComponent(value = 28) + case object WeaponSystemsAmmoLoss extends SubsystemComponent(value = 29) + case object WeaponSystemsOffline extends SubsystemComponent(value = 30) + + case object UnknownProjectileRange extends SubsystemComponent(value = 31) + case object UnknownSensorRange extends SubsystemComponent(value = 32) + case object UnknownRechargeInterval extends SubsystemComponent(value = 33) + + case object WeaponSystemsRefireTime extends SubsystemComponent(value = 34) + case object WeaponSystemsReloadTime extends SubsystemComponent(value = 35) + + case class Unknown(override val value: Int) extends SubsystemComponent(value) +} diff --git a/src/main/scala/net/psforever/util/DefinitionUtil.scala b/src/main/scala/net/psforever/util/DefinitionUtil.scala index ea6f9ac5d..a8b86e174 100644 --- a/src/main/scala/net/psforever/util/DefinitionUtil.scala +++ b/src/main/scala/net/psforever/util/DefinitionUtil.scala @@ -108,6 +108,23 @@ object DefinitionUtil { case 76 => apc_weapon_systemd_nc case 77 => apc_weapon_systemd_tr case 78 => apc_weapon_systemd_vs + case 80 => aphelion_armor_siphon + case 81 => aphelion_armor_siphon_left + case 82 => aphelion_armor_siphon_right + case 85 => aphelion_immolation_cannon + case 88 => aphelion_laser + case 90 => aphelion_laser_left + case 92 => aphelion_laser_right + case 93 => aphelion_ntu_siphon + case 94 => aphelion_ntu_siphon_left + case 95 => aphelion_ntu_siphon_right + case 98 => aphelion_plasma_rocket_pod + case 100 => aphelion_ppa + case 102 => aphelion_ppa_left + case 104 => aphelion_ppa_right + case 105 => aphelion_starfire + case 107 => aphelion_starfire_left + case 109 => aphelion_starfire_right case 119 => aurora_weapon_systema case 120 => aurora_weapon_systemb case 136 => battlewagon_weapon_systema @@ -118,6 +135,23 @@ object DefinitionUtil { case 146 => bolt_driver case 175 => chainblade case 177 => chaingun_p + case 182 => colossus_armor_siphon + case 183 => colossus_armor_siphon_left + case 184 => colossus_armor_siphon_right + case 185 => colossus_burster + case 187 => colossus_burster_left + case 189 => colossus_burster_right + case 190 => colossus_chaingun + case 192 => colossus_chaingun_left + case 194 => colossus_chaingun_right + case 196 => colossus_cluster_bomb_pod + case 198 => colossus_dual_100mm_cannons + case 201 => colossus_ntu_siphon + case 202 => colossus_ntu_siphon_left + case 203 => colossus_ntu_siphon_right + case 204 => colossus_tank_cannon + case 206 => colossus_tank_cannon_left + case 208 => colossus_tank_cannon_right case 233 => cycler case 262 => dropship_rear_turret case 274 => energy_gun @@ -159,6 +193,23 @@ object DefinitionUtil { case 599 => oicw case 628 => particle_beam_magrider case 629 => pellet_gun + case 623 => peregrine_armor_siphon + case 624 => peregrine_armor_siphon_left + case 625 => peregrine_armor_siphon_right + case 636 => peregrine_dual_machine_gun + case 638 => peregrine_dual_machine_gun_left + case 640 => peregrine_dual_machine_gun_right + case 641 => peregrine_dual_rocket_pods + case 644 => peregrine_mechhammer + case 646 => peregrine_mechhammer_left + case 648 => peregrine_mechhammer_right + case 649 => peregrine_ntu_siphon + case 650 => peregrine_ntu_siphon_left + case 651 => peregrine_ntu_siphon_right + case 652 => peregrine_particle_cannon + case 658 => peregrine_sparrow + case 660 => peregrine_sparrow_left + case 662 => peregrine_sparrow_right case 666 => phalanx_avcombo case 668 => phalanx_flakcombo case 670 => phalanx_sgl_hevgatcan @@ -224,12 +275,12 @@ object DefinitionUtil { case 68 => apc_vs case 46 => ams case 60 => ant - //case 83 => aphelion_flight - //case 84 => aphelion_gunner + case 83 => aphelion_flight + case 84 => aphelion_gunner case 118 => aurora case 135 => battlewagon - //case 199 => colossus_flight - //case 200 => colossus_gunner + case 199 => colossus_flight + case 200 => colossus_gunner case 258 => droppod case 259 => dropship case 294 => flail @@ -243,8 +294,8 @@ object DefinitionUtil { case 572 => mosquito case 532 => mediumtransport case 608 => orbital_shuttle - //case 642 => peregrine_flight - //case 643 => peregrine_gunner + case 642 => peregrine_flight + case 643 => peregrine_gunner case 671 => phantasm case 697 => prowler case 707 => quadassault diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index d35e67873..10b71bf1b 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -28,7 +28,7 @@ import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretDefinition} import net.psforever.objects.serverobject.zipline.ZipLinePath import net.psforever.objects.zones.{MapInfo, Zone, ZoneInfo, ZoneMap} -import net.psforever.types.{PlanetSideEmpire, Vector3} +import net.psforever.types.{Angular, PlanetSideEmpire, Vector3} import net.psforever.util.DefinitionUtil import scala.io.Source @@ -106,8 +106,6 @@ object Zones { // Force domes have GUIDs but are currently classed as separate entities. The dome is controlled by sending GOAM 44 / 48 / 52 to the building GUID private val ignoredEntities = Seq( "monolith", - "bfr_door", - "bfr_terminal", "force_dome_dsp_physics", "force_dome_comm_physics", "force_dome_cryo_physics", @@ -167,14 +165,16 @@ object Zones { "vehicle_terminal_combined", "dropship_vehicle_terminal", "vanu_air_vehicle_term", - "vanu_vehicle_term" + "vanu_vehicle_term", + "bfr_terminal" ) private val terminalTypes = basicTerminalTypes ++ spawnPadTerminalTypes private val spawnPadTypes = Seq( "mb_pad_creation", "dropship_pad_doors", - "vanu_vehicle_creation_pad" + "vanu_vehicle_creation_pad", + "bfr_door" ) private val doorTypes = Seq( @@ -295,23 +295,33 @@ object Zones { ) ) ) - } - + val filteredZoneEntities = + data.filter { _.owner.contains(structure.id) } ++ + { + val structurePosition = structure.position + if (structure.objectType.startsWith("orbital_building_")) { + data.filter { entity => + entity.objectType.startsWith("bfr_") && + Vector3.DistanceSquared(entity.position, structurePosition) < 160000f + } + } else { + List() + } + } createObjects( zoneMap, - data.filter(_.owner.contains(structure.id)), + filteredZoneEntities, structure.guid, Some(structure), turretWeaponGuid ) - } createObjects( zoneMap, - zoneObjects, - 0, + zoneObjects.filterNot { _.objectType.startsWith("bfr_") }, + ownerGuid = 0, None, turretWeaponGuid ) @@ -444,11 +454,31 @@ object Zones { val closestSpawnPad = spawnPads.minBy(point => Vector3.DistanceSquared(point.position, obj.position)) - // It appears that spawn pads have a default rotation that it +90 degrees from where it should be - // presumably the model is rotated differently to the expected orientation - // On top of that, some spawn pads also have an additional rotation (vehiclecreationzorientoffset) - // when spawning vehicles set in game_objects.adb.lst - this should be handled on the Scala side - val adjustedYaw = closestSpawnPad.yaw - 90 + val adjustedYaw = structure match { + case Some(building) + if objectType.equals("bfr_terminal") => + //bfr_terminal entities are paired with bfr_door entities + //rotations are not correctly set in the zone list, but assumptions can be made based on facility type + if (building.objectType.startsWith("orbital_building")) { + //sanctuary bfr sheds actually have their rotation angle set correctly in the zone map + obj.yaw + 45f + } else { + //predictable angles based on the facility type + Angular.flipClockwise(building.yaw) + (if (building.objectType.startsWith("comm_station")) { //includes comm_station_dsp + -45f + } else if (building.objectType.equals("cryo_facility") || building.objectType.equals("tech_plant")) { + 135f + } else if (building.objectType.equals("amp_station")) { + 225f + } else { + 0f + }) + } + case _ => + //spawn pads have a default rotation that it +90 degrees from where it should be + //presumably the model is rotated differently to the expected orientation + closestSpawnPad.yaw - 90 + } zoneMap.addLocalObject( closestSpawnPad.guid, @@ -510,6 +540,11 @@ object Zones { Terminal.Constructor(obj.position, GlobalDefinitions.ground_rearm_terminal), owningBuildingGuid = ownerGuid ) + zoneMap.addLocalObject( + obj.guid + 2, + Terminal.Constructor(obj.position, GlobalDefinitions.bfr_rearm_terminal), + owningBuildingGuid = ownerGuid + ) zoneMap.addLocalObject( obj.guid + 3, ProximityTerminal.Constructor(obj.position, GlobalDefinitions.recharge_terminal), diff --git a/src/test/scala/Vector3Test.scala b/src/test/scala/Vector3Test.scala index 3d585e82e..bc6ec8a23 100644 --- a/src/test/scala/Vector3Test.scala +++ b/src/test/scala/Vector3Test.scala @@ -1,6 +1,6 @@ // Copyright (c) 2017 PSForever import org.specs2.mutable._ -import net.psforever.types.Vector3 +import net.psforever.types.{Angular, Vector3} class Vector3Test extends Specification { val vec = Vector3(1.3f, -2.6f, 3.9f) @@ -321,4 +321,20 @@ class Vector3Test extends Specification { Vector3.relativeUp(Vector3(270, 90, 0)) mustEqual Vector3(1,0,0) //east } } + + "Angular" should { + "convert from clockwise to counterclockwise" in { + Angular.flipClockwise(angle = -45f) mustEqual 135f + Angular.flipClockwise(angle = 0f) mustEqual 90f + Angular.flipClockwise(angle = 45f) mustEqual 45f + Angular.flipClockwise(angle = 90f) mustEqual 0f + Angular.flipClockwise(angle = 135f) mustEqual 315f + Angular.flipClockwise(angle = 180f) mustEqual 270f + Angular.flipClockwise(angle = 225f) mustEqual 225f + Angular.flipClockwise(angle = 270f) mustEqual 180f + Angular.flipClockwise(angle = 315f) mustEqual 135f + Angular.flipClockwise(angle = 360f) mustEqual 90f + Angular.flipClockwise(angle = 405f) mustEqual 45f + } + } } diff --git a/src/test/scala/game/AvatarAwardMessageTest.scala b/src/test/scala/game/AvatarAwardMessageTest.scala new file mode 100644 index 000000000..5568b26c3 --- /dev/null +++ b/src/test/scala/game/AvatarAwardMessageTest.scala @@ -0,0 +1,67 @@ +// Copyright (c) 2021 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class AvatarAwardMessageTest extends Specification { + val string0 = hex"cf 15010000014000003d0040000000" + val string1 = hex"cf 2a010000c717b12a0000" + val string2 = hex"cf a6010000e9058cab0080" + + "decode (0)" in { + PacketCoding.decodePacket(string0).require match { + case AvatarAwardMessage(unk1, unk2, unk3) => + unk1 mustEqual 277 + unk2 mustEqual AwardOptionZero(5, 500) + unk3 mustEqual 0 + case _ => + ko + } + } + + "decode (1)" in { + PacketCoding.decodePacket(string1).require match { + case AvatarAwardMessage(unk1, unk2, unk3) => + unk1 mustEqual 298 + unk2 mustEqual AwardOptionTwo(2831441436L) + unk3 mustEqual 0 + case _ => + ko + } + } + + "decode (2)" in { + PacketCoding.decodePacket(string2).require match { + case AvatarAwardMessage(unk1, unk2, unk3) => + unk1 mustEqual 422 + unk2 mustEqual AwardOptionTwo(2888963748L) + unk3 mustEqual 2 + case _ => + ko + } + } + + "encode (0)" in { + val msg = AvatarAwardMessage(277, AwardOptionZero(5, 500), 0) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string0 + } + + "encode (1)" in { + val msg = AvatarAwardMessage(298, AwardOptionTwo(2831441436L), 0) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string1 + } + + "encode (2)" in { + val msg = AvatarAwardMessage(422, AwardOptionTwo(2888963748L), 2) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string2 + } +} diff --git a/src/test/scala/game/ComponentDamageMessageTest.scala b/src/test/scala/game/ComponentDamageMessageTest.scala new file mode 100644 index 000000000..5618eee56 --- /dev/null +++ b/src/test/scala/game/ComponentDamageMessageTest.scala @@ -0,0 +1,68 @@ +// Copyright (c) 2021 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.{PlanetSideGUID, SubsystemComponent} +import scodec.bits._ + +class ComponentDamageMessageTest extends Specification { + val string_on = hex"d3 8f01 1a000000820000000000202040" + val string_off = hex"d3 8f01 1a00000000" + + "decode (on)" in { + PacketCoding.decodePacket(string_on).require match { + case ComponentDamageMessage(guid, code, data) => + guid mustEqual PlanetSideGUID(399) + code mustEqual SubsystemComponent.WeaponSystemsCOFRecovery + data match { + case Some(ComponentDamageField(u2, u3, u4)) => + u2 mustEqual 4 + u3 mustEqual 1077936128 + u4 mustEqual true + case _ => + ko + } + case _ => + ko + } + } + + "decode (off)" in { + PacketCoding.decodePacket(string_off).require match { + case ComponentDamageMessage(guid, code, data) => + guid mustEqual PlanetSideGUID(399) + code mustEqual SubsystemComponent.WeaponSystemsCOFRecovery + data.isEmpty mustEqual true + case _ => + ko + } + } + + "encode (on)" in { + val msg = ComponentDamageMessage( + PlanetSideGUID(399), + SubsystemComponent.WeaponSystemsCOFRecovery, + Some(ComponentDamageField(4, 1077936128, unk = true)) + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string_on + } + + "encode (off; 1)" in { + val msg = ComponentDamageMessage(PlanetSideGUID(399), SubsystemComponent.WeaponSystemsCOFRecovery, None) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string_off + } + + "encode (off; 2)" in { + val msg = ComponentDamageMessage(PlanetSideGUID(399), SubsystemComponent.WeaponSystemsCOFRecovery) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string_off + } +} + diff --git a/src/test/scala/game/FavoritesMessageTest.scala b/src/test/scala/game/FavoritesMessageTest.scala index 1780f47b0..05877f6c7 100644 --- a/src/test/scala/game/FavoritesMessageTest.scala +++ b/src/test/scala/game/FavoritesMessageTest.scala @@ -26,7 +26,7 @@ class FavoritesMessageTest extends Specification { } "encode (for infantry)" in { - val msg = FavoritesMessage(LoadoutType.Infantry, PlanetSideGUID(3760), 0, "Agile (basic)", 1) + val msg = FavoritesMessage.Infantry(PlanetSideGUID(3760), line = 0, label = "Agile (basic)", armor = 1) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual stringInfantry @@ -46,7 +46,7 @@ class FavoritesMessageTest extends Specification { } "encode (for vehicles)" in { - val msg = FavoritesMessage(LoadoutType.Vehicle, PlanetSideGUID(4210), 0, "Skyguard") + val msg = FavoritesMessage.Vehicle(PlanetSideGUID(4210), line = 0, label = "Skyguard") val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual stringVehicles diff --git a/src/test/scala/game/FrameVehicleStateMessageTest.scala b/src/test/scala/game/FrameVehicleStateMessageTest.scala new file mode 100644 index 000000000..3dfc14282 --- /dev/null +++ b/src/test/scala/game/FrameVehicleStateMessageTest.scala @@ -0,0 +1,57 @@ +// Copyright (c) 2021 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.{PlanetSideGUID, Vector3} +import scodec.bits._ + +class FrameVehicleStateMessageTest extends Specification { + val string = hex"1c b101 087778ad7e62609ddf771beee0647fc6001a0000000000000000" + + "decode" in { + PacketCoding.decodePacket(string).require match { + case FrameVehicleStateMessage(guid, unk1, pos, ang, vel, unk2, unk3, unk4, crouched, airborne, ascending, ftime, u9, uA) => + guid mustEqual PlanetSideGUID(433) + unk1 mustEqual 0 + pos mustEqual Vector3(6518.5234f, 1918.6719f, 16.296875f) + ang mustEqual Vector3(353.67188f, 6.328125f, 130.42969f) + vel.contains(Vector3(21.0375f, -17.55f, 27.1125f)) mustEqual true + unk2 mustEqual false + unk3 mustEqual 0 + unk4 mustEqual 0 + crouched mustEqual false + airborne mustEqual false + ascending mustEqual true + ftime mustEqual 10 + u9 mustEqual 0 + uA mustEqual 0 + case _ => + ko + } + } + + "encode" in { + val msg = FrameVehicleStateMessage( + PlanetSideGUID(433), + 0, + Vector3(6518.5234f, 1918.6719f, 16.296875f), + Vector3(353.67188f, 6.328125f, 130.42969f), + Some(Vector3(21.0375f, -17.55f, 27.1125f)), + false, + 0, + 0, + false, + false, + true, + 10, + 0, + 0 + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} + diff --git a/src/test/scala/game/GenericObjectActionAtPositionMessageTest.scala b/src/test/scala/game/GenericObjectActionAtPositionMessageTest.scala new file mode 100644 index 000000000..45d545c28 --- /dev/null +++ b/src/test/scala/game/GenericObjectActionAtPositionMessageTest.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2021 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.{PlanetSideGUID, Vector3} +import scodec.bits._ + +class GenericObjectActionAtPositionMessageTest extends Specification { + val string = hex"d4 d504 09 00060 00110 000e" //faked + + "decode" in { + PacketCoding.decodePacket(string).require match { + case GenericObjectActionAtPositionMessage(object_guid, action, pos) => + object_guid mustEqual PlanetSideGUID(1237) + action mustEqual 9 + pos mustEqual Vector3(12f, 34f, 56f) + case _ => + ko + } + } + + "encode" in { + val msg = GenericObjectActionAtPositionMessage(PlanetSideGUID(1237), 9, Vector3(12f, 34f, 56f)) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} + diff --git a/src/test/scala/game/objectcreate/RadiationCloudDataTest.scala b/src/test/scala/game/objectcreate/RadiationCloudDataTest.scala new file mode 100644 index 000000000..b52b2bd4e --- /dev/null +++ b/src/test/scala/game/objectcreate/RadiationCloudDataTest.scala @@ -0,0 +1,43 @@ +// Copyright (c) 2021 PSForever +package game.objectcreate + +import net.psforever.packet.PacketCoding +import net.psforever.packet.game.ObjectCreateMessage +import net.psforever.packet.game.objectcreate._ +import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} +import org.specs2.mutable._ +import scodec.bits._ + +class RadiationCloudDataTest extends Specification { + val string = hex"17 a5000000 e6a5905 35e6e 52141 bf02 00 00 00 2400000" + + "CaptureFlagData" in { + "decode" in { + PacketCoding.decodePacket(string).require match { + case ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 165 + cls mustEqual ObjectClass.radiator_cloud + guid mustEqual PlanetSideGUID(1369) + parent.isDefined mustEqual false + data match { + case RadiationCloudData(pos, faction) => + pos.coord mustEqual Vector3(7628.414f, 552.6406f, 10.984375f) + pos.orient mustEqual Vector3.z(value = 90) + pos.vel.isEmpty mustEqual true + faction mustEqual PlanetSideEmpire.VS + case _ => + ko + } + case _ => + ko + } + } + + "encode" in { + val obj = RadiationCloudData(PlacementData(7628.414f, 552.6406f, 10.984375f, 0f, 0f, 90f), PlanetSideEmpire.VS) + val msg = ObjectCreateMessage(ObjectClass.radiator_cloud, PlanetSideGUID(1369), obj) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + pkt mustEqual string + } + } +} diff --git a/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala b/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala index ce539a455..ef7dc5dfa 100644 --- a/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala +++ b/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala @@ -160,7 +160,7 @@ class DetailedCharacterDataTest extends Specification { b.unk6 mustEqual 0L b.unk7 mustEqual 0L b.unk8 mustEqual 0L - b.unk9.contains(DCDExtra2(0, 0)) mustEqual true + b.imprinting.contains(ImprintingProgress(0, 0)) mustEqual true b.unkA mustEqual Nil b.unkB mustEqual Nil b.unkC mustEqual false @@ -348,7 +348,7 @@ class DetailedCharacterDataTest extends Specification { b.unk6 mustEqual 0L b.unk7 mustEqual 0L b.unk8 mustEqual 0L - b.unk9.contains(DCDExtra2(0, 0)) mustEqual true + b.imprinting.contains(ImprintingProgress(0, 0)) mustEqual true b.unkA mustEqual Nil b.unkB mustEqual Nil b.unkC mustEqual false @@ -575,7 +575,7 @@ class DetailedCharacterDataTest extends Specification { b.unk6 mustEqual 0L b.unk7 mustEqual 0L b.unk8 mustEqual 0L - b.unk9.contains(DCDExtra2(0, 0)) mustEqual true + b.imprinting.contains(ImprintingProgress(0, 0)) mustEqual true b.unkA mustEqual Nil b.unkB mustEqual Nil b.unkC mustEqual false @@ -1073,7 +1073,7 @@ class DetailedCharacterDataTest extends Specification { b.unk6 mustEqual 0L b.unk7 mustEqual 0L b.unk8 mustEqual 0L - b.unk9.isEmpty mustEqual true + b.imprinting.isEmpty mustEqual true b.unkA mustEqual Nil b.unkB mustEqual Nil b.unkC mustEqual false @@ -1294,7 +1294,7 @@ class DetailedCharacterDataTest extends Specification { b.unk6 mustEqual 0L b.unk7 mustEqual 0L b.unk8 mustEqual 0L - b.unk9.isEmpty mustEqual true + b.imprinting.isEmpty mustEqual true b.unkA.size mustEqual 86 b.unkA mustEqual List( 9, 10, 11, 16, 17, 18, 30, 31, 32, 37, 38, 52, 53, 57, 69, 92, 99, 100, 107, 108, 109, 110, 114, @@ -1432,7 +1432,7 @@ class DetailedCharacterDataTest extends Specification { b.unk6 mustEqual 0L b.unk7 mustEqual 0L b.unk8 mustEqual 0L - b.unk9.isEmpty mustEqual true + b.imprinting.isEmpty mustEqual true b.unkA mustEqual List( 9, 16, 17, 69, 107, 108, 114, 115, 134, 171, 178, 196, 199, 222, 229, 283, 297, 347, 360, 391, 399, 421 @@ -1614,7 +1614,7 @@ class DetailedCharacterDataTest extends Specification { 0L, 0L, 0L, - Some(DCDExtra2(0, 0)), + Some(ImprintingProgress(0, 0)), Nil, Nil, false, @@ -1796,7 +1796,7 @@ class DetailedCharacterDataTest extends Specification { 0L, 0L, 0L, - Some(DCDExtra2(0, 0)), + Some(ImprintingProgress(0, 0)), Nil, Nil, false, @@ -2022,7 +2022,7 @@ class DetailedCharacterDataTest extends Specification { 0L, 0L, 0L, - Some(DCDExtra2(0, 0)), + Some(ImprintingProgress(0, 0)), Nil, Nil, false, diff --git a/src/test/scala/game/objectcreatevehicle/BattleframeRoboticsTest.scala b/src/test/scala/game/objectcreatevehicle/BattleframeRoboticsTest.scala new file mode 100644 index 000000000..d94e039f4 --- /dev/null +++ b/src/test/scala/game/objectcreatevehicle/BattleframeRoboticsTest.scala @@ -0,0 +1,350 @@ +// Copyright (c) 2021 PSForever +package game.objectcreatevehicle + +import net.psforever.packet._ +import net.psforever.packet.game.objectcreate._ +import net.psforever.packet.game.ObjectCreateMessage +import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} +import org.specs2.mutable._ +import scodec.bits._ + +class BattleframeRoboticsTest extends Specification { + val string_aphelion_gunner = hex"17 80 02 00 00 AA 0B F0 15 EB 1C FE C3 30 90 40 00 00 E4 40 00 0F FF F0 00 00 F2 08 18 CC 13 C0 60 B2 00 00 00 10 11 94 2A 00 C0 64 00 00 1A 04 D8 0C 1E 40 00 00 02 02 32 85 60 18 0C 80 00 03 10 99 01 84 C8 00 00 00 40 46 10 CE 03 01 90 00 00" + val string_aphelion_flight = hex"17 f6010000 a98 8901 5eb1c fec33 0904 00 00 0e4 40000ffff0000002040866102030390000000808ca1cc0603200000d0140060b20000001011943c00c06400000" + + "Battle Frame Robotics" should { + "decode (aphelion)" in { + PacketCoding.decodePacket(string_aphelion_gunner).require match { + case ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 640L + cls mustEqual ObjectClass.aphelion_gunner + guid mustEqual PlanetSideGUID(447) + parent.isDefined mustEqual false + data match { + case BattleFrameRoboticsData(pos, vdata, health, shields, unk1, unk2, no_mount_points, drive_state, proper_anim, unk3, show_shield, unk4, Some(inv)) => + pos.coord mustEqual Vector3(6498.7344f, 1927.9844f, 16.140625f) + pos.orient mustEqual Vector3.z(50.625f) + pos.vel.isEmpty mustEqual true + + vdata match { + case CommonFieldData(faction, bops, alternate, v1, v2, v3, v4, v5, vguid) => + faction mustEqual PlanetSideEmpire.VS + bops mustEqual false + alternate mustEqual false + v1 mustEqual true + v2.isEmpty mustEqual true + v3 mustEqual false + v4.isEmpty mustEqual true + v5.isEmpty mustEqual true + vguid mustEqual PlanetSideGUID(0) + case _ => + ko + } + + health mustEqual 255 + shields mustEqual 255 + unk1 mustEqual 0 + unk2 mustEqual false + no_mount_points mustEqual false + drive_state mustEqual 60 + proper_anim mustEqual true + unk3 mustEqual 0 + show_shield mustEqual false + unk4.isEmpty mustEqual true + + inv match { + case InventoryData(list) => + list.head.objectClass mustEqual ObjectClass.aphelion_ppa_left + list.head.guid mustEqual PlanetSideGUID(335) + list.head.parentSlot mustEqual 2 + list.head.obj match { + case WeaponData(wdata, fire_mode, ammo, unk) => + wdata match { + case CommonFieldData(faction, bops, alternate, v1, v2, v3, v4, v5, wguid) => + faction mustEqual PlanetSideEmpire.NEUTRAL + bops mustEqual false + alternate mustEqual false + v1 mustEqual true + v2.isEmpty mustEqual true + v3 mustEqual false + v4.contains(false) mustEqual true + v5.isEmpty mustEqual true + wguid mustEqual PlanetSideGUID(0) + case _ => + ko + } + fire_mode mustEqual 0 + ammo.head.objectClass mustEqual ObjectClass.aphelion_ppa_ammo + ammo.head.guid mustEqual PlanetSideGUID(340) + ammo.head.parentSlot mustEqual 0 + unk mustEqual false + case _ => + ko + } + + list(1).objectClass mustEqual ObjectClass.aphelion_ppa_right + list(1).guid mustEqual PlanetSideGUID(411) + list(1).parentSlot mustEqual 3 + list(1).obj match { + case WeaponData(wdata, fire_mode, ammo, unk) => + wdata match { + case CommonFieldData(faction, bops, alternate, v1, v2, v3, v4, v5, wguid) => + faction mustEqual PlanetSideEmpire.NEUTRAL + bops mustEqual false + alternate mustEqual false + v1 mustEqual true + v2.isEmpty mustEqual true + v3 mustEqual false + v4.contains(false) mustEqual true + v5.isEmpty mustEqual true + wguid mustEqual PlanetSideGUID(0) + case _ => + ko + } + fire_mode mustEqual 0 + ammo.head.objectClass mustEqual ObjectClass.aphelion_ppa_ammo + ammo.head.guid mustEqual PlanetSideGUID(342) + ammo.head.parentSlot mustEqual 0 + unk mustEqual false + case _ => + ko + } + + list(2).objectClass mustEqual ObjectClass.aphelion_plasma_rocket_pod + list(2).guid mustEqual PlanetSideGUID(409) + list(2).parentSlot mustEqual 4 + list(2).obj match { + case WeaponData(wdata, fire_mode, ammo, unk) => + wdata match { + case CommonFieldData(faction, bops, alternate, v1, v2, v3, v4, v5, wguid) => + faction mustEqual PlanetSideEmpire.NEUTRAL + bops mustEqual false + alternate mustEqual false + v1 mustEqual true + v2.isEmpty mustEqual true + v3 mustEqual false + v4.contains(false) mustEqual true + v5.isEmpty mustEqual true + wguid mustEqual PlanetSideGUID(0) + case _ => + ko + } + fire_mode mustEqual 0 + ammo.head.objectClass mustEqual ObjectClass.aphelion_plasma_rocket_ammo + ammo.head.guid mustEqual PlanetSideGUID(359) + ammo.head.parentSlot mustEqual 0 + unk mustEqual false + case _ => + ko + } + case _ => + ko + } + case _ => + ko + } + case _ => + ko + } + } + + "decode (eclipse)" in { + PacketCoding.decodePacket(string_aphelion_flight).require match { + case ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 502L + cls mustEqual ObjectClass.aphelion_flight + guid mustEqual PlanetSideGUID(393) + parent.isDefined mustEqual false + data match { + case BattleFrameRoboticsData(pos, vdata, health, shields, unk1, unk2, no_mount_points, drive_state, proper_anim, unk3, show_shield, unk4, Some(inv)) => + pos.coord mustEqual Vector3(6498.7344f, 1927.9844f, 16.140625f) + pos.orient mustEqual Vector3.z(50.625f) + pos.vel.isEmpty mustEqual true + + vdata match { + case CommonFieldData(faction, bops, alternate, v1, v2, v3, v4, v5, vguid) => + faction mustEqual PlanetSideEmpire.VS + bops mustEqual false + alternate mustEqual false + v1 mustEqual true + v2.isEmpty mustEqual true + v3 mustEqual false + v4.isEmpty mustEqual true + v5.isEmpty mustEqual true + vguid mustEqual PlanetSideGUID(0) + case _ => + ko + } + + health mustEqual 255 + shields mustEqual 255 + unk1 mustEqual 0 + unk2 mustEqual false + no_mount_points mustEqual false + drive_state mustEqual 0 + proper_anim mustEqual true + unk3 mustEqual 0 + show_shield mustEqual false + unk4.contains(false) mustEqual true + + inv match { + case InventoryData(list) => + list.head.objectClass mustEqual ObjectClass.aphelion_ppa_left + list.head.guid mustEqual PlanetSideGUID(385) + list.head.parentSlot mustEqual 1 + list.head.obj match { + case WeaponData(wdata, fire_mode, ammo, unk) => + wdata match { + case CommonFieldData(faction, bops, alternate, v1, v2, v3, v4, v5, wguid) => + faction mustEqual PlanetSideEmpire.NEUTRAL + bops mustEqual false + alternate mustEqual false + v1 mustEqual true + v2.isEmpty mustEqual true + v3 mustEqual false + v4.contains(false) mustEqual true + v5.isEmpty mustEqual true + wguid mustEqual PlanetSideGUID(0) + case _ => + ko + } + fire_mode mustEqual 0 + ammo.head.objectClass mustEqual ObjectClass.aphelion_ppa_ammo + ammo.head.guid mustEqual PlanetSideGUID(371) + ammo.head.parentSlot mustEqual 0 + unk mustEqual false + case _ => + ko + } + + list(1).objectClass mustEqual ObjectClass.aphelion_ppa_right + list(1).guid mustEqual PlanetSideGUID(336) + list(1).parentSlot mustEqual 2 + list(1).obj match { + case WeaponData(wdata, fire_mode, ammo, unk) => + wdata match { + case CommonFieldData(faction, bops, alternate, v1, v2, v3, v4, v5, wguid) => + faction mustEqual PlanetSideEmpire.NEUTRAL + bops mustEqual false + alternate mustEqual false + v1 mustEqual true + v2.isEmpty mustEqual true + v3 mustEqual false + v4.contains(false) mustEqual true + v5.isEmpty mustEqual true + wguid mustEqual PlanetSideGUID(0) + case _ => + ko + } + fire_mode mustEqual 0 + ammo.head.objectClass mustEqual ObjectClass.aphelion_ppa_ammo + ammo.head.guid mustEqual PlanetSideGUID(376) + ammo.head.parentSlot mustEqual 0 + unk mustEqual false + case _ => + ko + } + case _ => + ko + } + case _ => + ko + } + case _ => + ko + } + } + + "encode (aphelion)" in { + val obj = BattleFrameRoboticsData( + PlacementData(6498.7344f, 1927.9844f, 16.140625f, 0, 0, 50.625f), + CommonFieldData(PlanetSideEmpire.VS, false, false, true, None, false, None, None, PlanetSideGUID(0)), + 255, + 255, + 0, + false, + false, + 60, + true, + 0, + false, + None, + Some(InventoryData(List( + InventoryItemData(ObjectClass.aphelion_ppa_left, PlanetSideGUID(335), 2, + WeaponData( + CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, Some(false), None, PlanetSideGUID(0)), + 0, + List( + InternalSlot(ObjectClass.aphelion_ppa_ammo, PlanetSideGUID(340), 0, CommonFieldData(PlanetSideEmpire.NEUTRAL, 2)(false)) + ) + ) + ), + InventoryItemData(ObjectClass.aphelion_ppa_right, PlanetSideGUID(411), 3, + WeaponData( + CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, Some(false), None, PlanetSideGUID(0)), + 0, + List( + InternalSlot(ObjectClass.aphelion_ppa_ammo, PlanetSideGUID(342), 0, CommonFieldData(PlanetSideEmpire.NEUTRAL, 2)(false)) + ) + ) + ), + InventoryItemData(ObjectClass.aphelion_plasma_rocket_pod, PlanetSideGUID(409), 4, + WeaponData( + CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, Some(false), None, PlanetSideGUID(0)), + 0, + List( + InternalSlot(ObjectClass.aphelion_plasma_rocket_ammo, PlanetSideGUID(359), 0, CommonFieldData(PlanetSideEmpire.NEUTRAL, 2)(false)) + ) + ) + ) + ))) + ) + val msg = ObjectCreateMessage(ObjectClass.aphelion_gunner, PlanetSideGUID(447), obj) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string_aphelion_gunner + } + + "encode (eclipse)" in { + val obj = BattleFrameRoboticsData( + PlacementData(6498.7344f, 1927.9844f, 16.140625f, 0, 0, 50.625f), + CommonFieldData(PlanetSideEmpire.VS, false, false, true, None, false, None, None, PlanetSideGUID(0)), + 255, + 255, + 0, + false, + false, + 0, + true, + 0, + false, + Some(false), + Some(InventoryData(List( + InventoryItemData(ObjectClass.aphelion_ppa_left, PlanetSideGUID(385), 1, + WeaponData( + CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, Some(false), None, PlanetSideGUID(0)), + 0, + List( + InternalSlot(ObjectClass.aphelion_ppa_ammo, PlanetSideGUID(371), 0, CommonFieldData(PlanetSideEmpire.NEUTRAL, 2)(false)) + ) + ) + ), + InventoryItemData(ObjectClass.aphelion_ppa_right, PlanetSideGUID(336), 2, + WeaponData( + CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, Some(false), None, PlanetSideGUID(0)), + 0, + List( + InternalSlot(ObjectClass.aphelion_ppa_ammo, PlanetSideGUID(376), 0, CommonFieldData(PlanetSideEmpire.NEUTRAL, 2)(false)) + ) + ) + ) + ))) + ) + val msg = ObjectCreateMessage(ObjectClass.aphelion_flight, PlanetSideGUID(393), obj) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string_aphelion_flight + } + } +} + diff --git a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala index c592df7d6..7153b65b3 100644 --- a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala +++ b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala @@ -253,12 +253,12 @@ class MountedVehiclesTest extends Specification { ) ) ) - val player = VehicleData.PlayerData( + val player = MountableInventory.PlayerData( app, char, inv, DrawnSlot.None, - VehicleData.InitialStreamLengthToSeatEntries(true, VehicleFormat.Variant) + MountableInventory.InitialStreamLengthToSeatEntries(true, VehicleFormat.Variant) ) val obj = VehicleData( PlacementData( diff --git a/src/test/scala/objects/ConverterTest.scala b/src/test/scala/objects/ConverterTest.scala index 6938a7347..3e5aab963 100644 --- a/src/test/scala/objects/ConverterTest.scala +++ b/src/test/scala/objects/ConverterTest.scala @@ -874,7 +874,7 @@ class ConverterTest extends Specification { val fury_def = VehicleDefinition(ObjectClass.fury) fury_def.Seats += 0 -> new SeatDefinition() fury_def.Seats(0).bailable = true - fury_def.controlledWeapons += 0 -> 1 + fury_def.controlledWeapons(0, 1) fury_def.MountPoints += 0 -> MountInfo(0) fury_def.MountPoints += 2 -> MountInfo(0) fury_def.Weapons += 1 -> fury_weapon_systema_def @@ -889,8 +889,8 @@ class ConverterTest extends Specification { fury.Faction = PlanetSideEmpire.VS fury.Position = Vector3(3674.8438f, 2732f, 91.15625f) fury.Orientation = Vector3(0.0f, 0.0f, 90.0f) - fury.WeaponControlledFromSeat(0).get.GUID = PlanetSideGUID(400) - fury.WeaponControlledFromSeat(0).get.asInstanceOf[Tool].AmmoSlots.head.Box = hellfire_ammo_box + fury.WeaponControlledFromSeat(0).head.GUID = PlanetSideGUID(400) + fury.WeaponControlledFromSeat(0).head.asInstanceOf[Tool].AmmoSlots.head.Box = hellfire_ammo_box fury.Definition.Packet.ConstructorData(fury).isSuccess mustEqual true ok //TODO write more of this test diff --git a/src/test/scala/objects/DamageModelTests.scala b/src/test/scala/objects/DamageModelTests.scala index 13f60d95a..44ebb4021 100644 --- a/src/test/scala/objects/DamageModelTests.scala +++ b/src/test/scala/objects/DamageModelTests.scala @@ -61,7 +61,7 @@ class DamageCalculationsTests extends Specification { } "extract damage against battleframe robotics" in { - AgainstBFR(proj_prof) == proj_prof.Damage4 mustEqual true + AgainstBfr(proj_prof) == proj_prof.Damage4 mustEqual true } "no degrade damage modifier" in { diff --git a/src/test/scala/objects/DamageableTest.scala b/src/test/scala/objects/DamageableTest.scala index aa4f6c256..94c056042 100644 --- a/src/test/scala/objects/DamageableTest.scala +++ b/src/test/scala/objects/DamageableTest.scala @@ -1437,9 +1437,9 @@ class DamageableVehicleJammeringMountedTest extends FreedContextActorTest { "handle jammering with mounted vehicles" in { assert(lodestar.Health == lodestar.Definition.DefaultHealth) - assert(!lodestar.Jammered) + assert(!lodestar.Jammed) assert(atv.Health == atv.Definition.DefaultHealth) - assert(!atv.Jammered) + assert(!atv.Jammed) lodestar.Actor ! Vitality.Damage(applyDamageTo) val msg12 = vehicleProbe.receiveOne(500 milliseconds) @@ -1684,11 +1684,11 @@ class DamageableVehicleDestroyMountedTest extends FreedContextActorTest { lodestar.Health = lodestar.Definition.DamageDestroysAt + 1 //initial state manip atv.Shields = 1 //initial state manip assert(lodestar.Health > lodestar.Definition.DamageDestroysAt) - assert(!lodestar.Jammered) + assert(!lodestar.Jammed) assert(!lodestar.Destroyed) assert(atv.Health == atv.Definition.DefaultHealth) assert(atv.Shields == 1) - assert(!atv.Jammered) + assert(!atv.Jammed) assert(!atv.Destroyed) lodestar.Actor ! Vitality.Damage(applyDamageToA) diff --git a/src/test/scala/objects/GeneratorTest.scala b/src/test/scala/objects/GeneratorTest.scala index feb62dc09..5825c7f8b 100644 --- a/src/test/scala/objects/GeneratorTest.scala +++ b/src/test/scala/objects/GeneratorTest.scala @@ -861,7 +861,6 @@ object GeneratorTest { Repairable = true RepairDistance = 13.5f RepairIfDestroyed = true - explodes = true innateDamage = new DamageWithPosition { DamageRadius = 14 } diff --git a/src/test/scala/objects/VehicleTest.scala b/src/test/scala/objects/VehicleTest.scala index 3293946b9..e850ce911 100644 --- a/src/test/scala/objects/VehicleTest.scala +++ b/src/test/scala/objects/VehicleTest.scala @@ -136,7 +136,7 @@ class VehicleTest extends Specification { fury_vehicle.Weapons.get(1).isDefined mustEqual true fury_vehicle.Weapons(1).Equipment.isDefined mustEqual true fury_vehicle.Weapons(1).Equipment.get.Definition mustEqual GlobalDefinitions.fury.Weapons(1) - fury_vehicle.WeaponControlledFromSeat(0) mustEqual fury_vehicle.Weapons(1).Equipment + fury_vehicle.WeaponControlledFromSeat(0) contains fury_vehicle.Weapons(1).Equipment.get mustEqual true fury_vehicle.Trunk.Width mustEqual 11 fury_vehicle.Trunk.Height mustEqual 11 fury_vehicle.Trunk.Offset mustEqual 30 @@ -248,7 +248,7 @@ class VehicleTest extends Specification { chaingun_p.isDefined mustEqual true harasser_vehicle.WeaponControlledFromSeat(0).isEmpty mustEqual true - harasser_vehicle.WeaponControlledFromSeat(1) mustEqual chaingun_p + harasser_vehicle.WeaponControlledFromSeat(1).contains(chaingun_p.get) mustEqual true } "can filter utilities with indices that are natural numbers" in { diff --git a/src/test/scala/objects/guidtask/GUIDTaskRegisterVehicleTest.scala b/src/test/scala/objects/guidtask/GUIDTaskRegisterVehicleTest.scala index a0416c153..a6a8ce3c6 100644 --- a/src/test/scala/objects/guidtask/GUIDTaskRegisterVehicleTest.scala +++ b/src/test/scala/objects/guidtask/GUIDTaskRegisterVehicleTest.scala @@ -11,8 +11,8 @@ class GUIDTaskRegisterVehicleTest extends ActorTest { "RegisterVehicle" in { val (_, uns, probe) = GUIDTaskTest.CommonTestSetup val obj = Vehicle(GlobalDefinitions.fury) - val obj_wep = obj.WeaponControlledFromSeat(0).get - val obj_wep_ammo = (obj.WeaponControlledFromSeat(0).get.asInstanceOf[Tool].AmmoSlots.head.Box = + val obj_wep = obj.WeaponControlledFromSeat(0).head + val obj_wep_ammo = (obj_wep.asInstanceOf[Tool].AmmoSlots.head.Box = AmmoBox(GlobalDefinitions.hellfire_ammo)).get obj.Trunk += 30 -> AmmoBox(GlobalDefinitions.hellfire_ammo) val obj_trunk_ammo = obj.Trunk.Items(0).obj diff --git a/src/test/scala/objects/guidtask/GUIDTaskUnregisterVehicleTest.scala b/src/test/scala/objects/guidtask/GUIDTaskUnregisterVehicleTest.scala index 850cea312..ca442baab 100644 --- a/src/test/scala/objects/guidtask/GUIDTaskUnregisterVehicleTest.scala +++ b/src/test/scala/objects/guidtask/GUIDTaskUnregisterVehicleTest.scala @@ -11,8 +11,8 @@ class GUIDTaskUnregisterVehicleTest extends ActorTest { "RegisterVehicle" in { val (guid, uns, probe) = GUIDTaskTest.CommonTestSetup val obj = Vehicle(GlobalDefinitions.fury) - val obj_wep = obj.WeaponControlledFromSeat(0).get - val obj_wep_ammo = (obj.WeaponControlledFromSeat(0).get.asInstanceOf[Tool].AmmoSlots.head.Box = + val obj_wep = obj.WeaponControlledFromSeat(0).head + val obj_wep_ammo = (obj_wep.asInstanceOf[Tool].AmmoSlots.head.Box = AmmoBox(GlobalDefinitions.hellfire_ammo)).get obj.Trunk += 30 -> AmmoBox(GlobalDefinitions.hellfire_ammo) val obj_trunk_ammo = obj.Trunk.Items(0).obj