diff --git a/server/src/test/scala/actor/service/AvatarServiceTest.scala b/server/src/test/scala/actor/service/AvatarServiceTest.scala index f912c0f0..4f9d2365 100644 --- a/server/src/test/scala/actor/service/AvatarServiceTest.scala +++ b/server/src/test/scala/actor/service/AvatarServiceTest.scala @@ -5,7 +5,7 @@ import akka.actor.Props import akka.routing.RandomPool import actor.base.ActorTest import net.psforever.objects._ -import net.psforever.objects.guid.{GUIDTask, TaskResolver} +import net.psforever.objects.guid.{NumberPoolHub, TaskResolver} import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.packet.game.objectcreate.{DroppedItemData, ObjectClass, ObjectCreateMessageParent, PlacementData} import net.psforever.packet.game.{ObjectCreateMessage, PlayerStateMessageUpstream} @@ -17,6 +17,7 @@ import scala.concurrent.duration._ import akka.actor.typed.scaladsl.adapter._ import net.psforever.actors.zone.ZoneActor import net.psforever.objects.avatar.Avatar +import net.psforever.objects.guid.source.LimitedNumberSource class AvatarService1Test extends ActorTest { "AvatarService" should { @@ -511,10 +512,13 @@ class AvatarReleaseTest extends ActorTest { val zone = new Zone("test", new ZoneMap("test-map"), 0) { override def SetupNumberPools() = { AddPool("dynamic", 1 to 10) } } + val guid1: NumberPoolHub = new NumberPoolHub(new LimitedNumberSource(100)) + zone.GUID(guid1) val service = system.actorOf(Props(classOf[AvatarService], zone), "release-test-service") - val taskResolver = system.actorOf(Props[TaskResolver](), "release-test-resolver") zone.actor = system.spawn(ZoneActor(zone), "release-test-zone") val obj = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + guid1.register(obj) + guid1.register(obj.Slot(5).Equipment.get) obj.Continent = "test" obj.Release @@ -523,7 +527,6 @@ class AvatarReleaseTest extends ActorTest { expectNoMessage(100 milliseconds) //spacer service ! Service.Join("test") - taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) assert(zone.Corpses.isEmpty) zone.Population ! Zone.Corpse.Add(obj) expectNoMessage(200 milliseconds) //spacer @@ -561,10 +564,13 @@ class AvatarReleaseEarly1Test extends ActorTest { val zone = new Zone("test", new ZoneMap("test-map"), 0) { override def SetupNumberPools() = { AddPool("dynamic", 1 to 10) } } + val guid1: NumberPoolHub = new NumberPoolHub(new LimitedNumberSource(100)) + zone.GUID(guid1) val service = system.actorOf(Props(classOf[AvatarService], zone), "release-test-service") - val taskResolver = system.actorOf(Props[TaskResolver](), "release-test-resolver") zone.actor = system.spawn(ZoneActor(zone), "release-test-zone") val obj = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + guid1.register(obj) + guid1.register(obj.Slot(5).Equipment.get) obj.Continent = "test" obj.Release @@ -573,7 +579,6 @@ class AvatarReleaseEarly1Test extends ActorTest { expectNoMessage(100 milliseconds) //spacer service ! Service.Join("test") - taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) assert(zone.Corpses.isEmpty) zone.Population ! Zone.Corpse.Add(obj) expectNoMessage(200 milliseconds) //spacer @@ -612,14 +617,19 @@ class AvatarReleaseEarly2Test extends ActorTest { val zone = new Zone("test", new ZoneMap("test-map"), 0) { override def SetupNumberPools() = { AddPool("dynamic", 1 to 10) } } + val guid1: NumberPoolHub = new NumberPoolHub(new LimitedNumberSource(100)) + zone.GUID(guid1) val service = system.actorOf(Props(classOf[AvatarService], zone), "release-test-service") - val taskResolver = system.actorOf(Props[TaskResolver](), "release-test-resolver") zone.actor = system.spawn(ZoneActor(zone), "release-test-zone") val objAlt = Player( Avatar(0, "TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 1, CharacterVoice.Voice1) ) //necessary clutter - val obj = Player(Avatar(0, "TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + objAlt.GUID = PlanetSideGUID(3) + objAlt.Slot(5).Equipment.get.GUID = PlanetSideGUID(4) + val obj = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + guid1.register(obj) + guid1.register(obj.Slot(5).Equipment.get) obj.Continent = "test" obj.Release @@ -628,7 +638,6 @@ class AvatarReleaseEarly2Test extends ActorTest { expectNoMessage(100 milliseconds) //spacer service ! Service.Join("test") - taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) assert(zone.Corpses.isEmpty) zone.Population ! Zone.Corpse.Add(obj) expectNoMessage(200 milliseconds) //spacer diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index a5bfcd6c..f8c14fe1 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -1,13 +1,26 @@ package net.psforever.actors.session -import java.util.concurrent.TimeUnit - -import akka.actor.MDCContextAware.Implicits._ +//language imports import akka.actor.typed import akka.actor.typed.receptionist.Receptionist +import akka.actor.typed.scaladsl.adapter._ import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} -import net.psforever.objects.{GlobalDefinitions, _} -import net.psforever.objects.avatar.{Avatar, Certification, DeployableToolbox} +import akka.pattern.ask +import akka.util.Timeout +import java.util.concurrent.TimeUnit +import MDCContextAware.Implicits._ +import org.log4s.MDC +import scala.collection.mutable +import scala.concurrent.{Await, Future} +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.util.Success +import scodec.bits.ByteVector +//project imports +import net.psforever.login.{DropCryptoSession, DropSession, HelloFriend, RawPacket} +import net.psforever.login.WorldSession._ +import net.psforever.objects._ +import net.psforever.objects.avatar.{Avatar, Certification, Cosmetic, DeployableToolbox} import net.psforever.objects.ballistics._ import net.psforever.objects.ce._ import net.psforever.objects.definition._ @@ -16,6 +29,7 @@ import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity} import net.psforever.objects.equipment.{EffectTarget, Equipment, FireModeSwitch, JammableUnit} import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.inventory.{Container, InventoryItem} +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.serverobject.damage.Damageable @@ -35,30 +49,19 @@ import net.psforever.objects.serverobject.terminals._ 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.teamwork.Squad -import net.psforever.objects.vehicles.{ - AccessPermissionGroup, - CargoBehavior, - MountedWeapons, - Utility, - UtilityType, - VehicleControl, - VehicleLockState -} +import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles.Utility.InternalTelepad import net.psforever.objects.vital._ import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning} import net.psforever.packet._ import net.psforever.packet.control._ -import net.psforever.packet.game.objectcreate._ import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo, _} -import net.psforever.types._ -import org.log4s.MDC -import scodec.bits.ByteVector +import net.psforever.packet.game.objectcreate._ import net.psforever.services.ServiceManager.LookupResult import net.psforever.services.account.{AccountPersistenceService, PlayerToken, ReceiveAccountData, RetrieveAccountData} import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse} +import net.psforever.services.chat.ChatService import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage, GalaxyServiceResponse} import net.psforever.services.local.support.RouterTelepadActivation import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} @@ -72,22 +75,9 @@ import net.psforever.services.teamwork.{ } import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} import net.psforever.services.{InterstellarClusterService, RemoverActor, Service, ServiceManager} -import net.psforever.login.{DropCryptoSession, DropSession, HelloFriend, RawPacket} +import net.psforever.types._ import net.psforever.util.{Config, DefinitionUtil} -import net.psforever.login.WorldSession._ import net.psforever.zones.Zones -import net.psforever.services.chat.ChatService -import net.psforever.objects.avatar.Cosmetic - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ -import scala.concurrent.{Await, Future} -import scala.util.Success -import akka.actor.typed.scaladsl.adapter._ -import akka.pattern.ask -import akka.util.Timeout - -import scala.collection.mutable object SessionActor { sealed trait Command @@ -732,7 +722,7 @@ class SessionActor extends Actor with MDCContextAware { unk7 = 0 ) ) //repeat of our entry - val playerGuid = player.GUID + val playerGuid = player.GUID //turn lfs off val factionChannel = s"${player.Faction}" if (avatar.lookingForSquad) { @@ -885,7 +875,7 @@ class SessionActor extends Actor with MDCContextAware { SquadUIElement(element.name, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) entry case (entry, element) - if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => + if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => //other elements that need to be updated squadUI(entry.char_id) = SquadUIElement(element.name, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) @@ -1390,7 +1380,7 @@ class SessionActor extends Actor with MDCContextAware { } else if (tplayer.isAlive) { if ( zoneLoaded.contains(true) && - tplayer.HasGUID && tplayer.Actor != Default.Actor && (continent.GUID(tplayer.VehicleSeated) match { + tplayer.HasGUID && tplayer.Actor != Default.Actor && (continent.GUID(tplayer.VehicleSeated) match { case Some(o: Vehicle) => o.HasGUID && o.Actor != Default.Actor && !o.Destroyed case _ => true }) @@ -1554,11 +1544,11 @@ class SessionActor extends Actor with MDCContextAware { } case msg @ Containable.ItemPutInSlot( - _: PlanetSideServerObject with Container, - _: Equipment, - _: Int, - _: Option[Equipment] - ) => + _: PlanetSideServerObject with Container, + _: Equipment, + _: Int, + _: Option[Equipment] + ) => log.info(s"$msg") case msg @ Containable.CanNotPutItemInSlot(_: PlanetSideServerObject with Container, _: Equipment, _: Int) => @@ -1960,19 +1950,19 @@ class SessionActor extends Actor with MDCContextAware { } case AvatarResponse.PlayerState( - pos, - vel, - yaw, - pitch, - yaw_upper, - seq_time, - is_crouching, - is_jumping, - jump_thrust, - is_cloaking, - spectating, - weaponInHand - ) => + pos, + vel, + yaw, + pitch, + yaw_upper, + seq_time, + is_crouching, + is_jumping, + jump_thrust, + is_cloaking, + spectating, + weaponInHand + ) => if (tplayer_guid != guid) { val now = System.currentTimeMillis() val (location, time, distanceSq): (Vector3, Long, Float) = if (spectating) { @@ -2803,18 +2793,18 @@ class SessionActor extends Actor with MDCContextAware { } case VehicleResponse.VehicleState( - vehicle_guid, - unk1, - pos, - ang, - vel, - unk2, - unk3, - unk4, - wheel_direction, - unk5, - unk6 - ) => + vehicle_guid, + unk1, + pos, + ang, + vel, + unk2, + unk3, + unk4, + wheel_direction, + unk5, + unk6 + ) => if (tplayer_guid != guid) { sendResponse( VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6) @@ -2948,10 +2938,10 @@ class SessionActor extends Actor with MDCContextAware { * @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet */ def CargoMountBehaviorForUs( - carrier: Vehicle, - cargo: Vehicle, - mountPoint: Int - ): (ObjectAttachMessage, CargoMountPointStatusMessage) = { + carrier: Vehicle, + cargo: Vehicle, + mountPoint: Int + ): (ObjectAttachMessage, CargoMountPointStatusMessage) = { val msgs @ (attachMessage, mountPointStatusMessage) = CargoBehavior.CargoMountMessages(carrier, cargo, mountPoint) CargoMountMessagesForUs(attachMessage, mountPointStatusMessage) msgs @@ -2965,9 +2955,9 @@ class SessionActor extends Actor with MDCContextAware { * @param mountPointStatusMessage a `CargoMountPointStatusMessage` packet suitable for initializing cargo operations */ def CargoMountMessagesForUs( - attachMessage: ObjectAttachMessage, - mountPointStatusMessage: CargoMountPointStatusMessage - ): Unit = { + attachMessage: ObjectAttachMessage, + mountPointStatusMessage: CargoMountPointStatusMessage + ): Unit = { sendResponse(attachMessage) sendResponse(mountPointStatusMessage) } @@ -3329,10 +3319,10 @@ class SessionActor extends Actor with MDCContextAware { context.system.scheduler.scheduleWithFixedDelay(0 seconds, 500 milliseconds, self, PokeClient()) accountIntermediary ! RetrieveAccountData(token) - case msg @ MountVehicleCargoMsg(player_guid, cargo_guid, carrier_guid, unk4) => + case msg@MountVehicleCargoMsg(player_guid, cargo_guid, carrier_guid, unk4) => log.info(msg.toString) (continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match { - case (Some(cargo: Vehicle), Some(carrier: Vehicle)) => + case (Some(cargo : Vehicle), Some(carrier : Vehicle)) => carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match { case Some((mountPoint, _)) => //try begin the mount process cargo.Actor ! CargoBehavior.CheckCargoMounting(carrier_guid, mountPoint, 0) @@ -3348,24 +3338,24 @@ class SessionActor extends Actor with MDCContextAware { case _ => ; } - case msg @ DismountVehicleCargoMsg(player_guid, cargo_guid, bailed, requestedByPassenger, kicked) => + case msg@DismountVehicleCargoMsg(player_guid, cargo_guid, bailed, requestedByPassenger, kicked) => log.info(msg.toString) //when kicked by carrier driver, player_guid will be PlanetSideGUID(0) //when exiting of the cargo vehicle driver's own accord, player_guid will be the cargo vehicle driver continent.GUID(cargo_guid) match { - case Some(cargo: Vehicle) if !requestedByPassenger => + case Some(cargo : Vehicle) if !requestedByPassenger => continent.GUID(cargo.MountedIn) match { - case Some(carrier: Vehicle) => + case Some(carrier : Vehicle) => CargoBehavior.HandleVehicleCargoDismount(continent, cargo_guid, bailed, requestedByPassenger, kicked) case _ => ; } case _ => ; } - case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) => + case msg@CharacterCreateRequestMessage(name, head, voice, gender, empire) => avatarActor ! AvatarActor.CreateAvatar(name, head, voice, gender, empire) - case msg @ CharacterRequestMessage(charId, action) => + case msg@CharacterRequestMessage(charId, action) => action match { case CharacterRequestAction.Delete => avatarActor ! AvatarActor.DeleteAvatar(charId.toInt) @@ -3376,11 +3366,11 @@ class SessionActor extends Actor with MDCContextAware { case KeepAliveMessage(_) => keepAliveFunc() - case msg @ BeginZoningMessage() => + case msg@BeginZoningMessage() => log.info("Reticulating splines ...") zoneLoaded = None - val continentId = continent.id - val faction = player.Faction + val continentId = continent.id + val faction = player.Faction val factionChannel = s"$faction" continent.AvatarEvents ! Service.Join(continentId) continent.AvatarEvents ! Service.Join(factionChannel) @@ -3393,7 +3383,7 @@ class SessionActor extends Actor with MDCContextAware { if (connectionState != 100) configZone(continent) sendResponse(TimeOfDayMessage(1191182336)) //custom - sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list + sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks //find and reclaim own deployables, if any @@ -3422,7 +3412,7 @@ class SessionActor extends Actor with MDCContextAware { ) }) turrets.foreach(obj => { - val objGUID = obj.GUID + val objGUID = obj.GUID val definition = obj.Definition sendResponse( ObjectCreateMessage( @@ -3456,11 +3446,11 @@ class SessionActor extends Actor with MDCContextAware { normal .filter(obj => obj.Definition.DeployCategory == DeployableCategory.Sensors && - !obj.Destroyed && - (obj match { - case jObj: JammableUnit => !jObj.Jammed; - case _ => true - }) + !obj.Destroyed && + (obj match { + case jObj : JammableUnit => !jObj.Jammed; + case _ => true + }) ) .foreach(obj => { sendResponse(TriggerEffectMessage(obj.GUID, "on", true, 1000)) @@ -3527,7 +3517,7 @@ class SessionActor extends Actor with MDCContextAware { ( a, (continent.GUID(player.VehicleSeated) match { - case Some(vehicle: Vehicle) if vehicle.PassengerInSeat(player).isDefined => + case Some(vehicle : Vehicle) if vehicle.PassengerInSeat(player).isDefined => b.partition { _.GUID != vehicle.GUID } @@ -3545,7 +3535,7 @@ class SessionActor extends Actor with MDCContextAware { val allActiveVehicles = vehicles ++ usedVehicle //active vehicles (and some wreckage) vehicles.foreach(vehicle => { - val vguid = vehicle.GUID + val vguid = vehicle.GUID val vdefinition = vehicle.Definition sendResponse( ObjectCreateMessage(vdefinition.ObjectId, vguid, vdefinition.Packet.ConstructorData(vehicle).get) @@ -3555,7 +3545,7 @@ class SessionActor extends Actor with MDCContextAware { .filter({ case (index, seat) => seat.isOccupied && live.contains(seat.Occupant.get) && index > 0 }) .foreach({ case (index, seat) => - val targetPlayer = seat.Occupant.get + val targetPlayer = seat.Occupant.get val targetDefiniton = targetPlayer.avatar.definition sendResponse( ObjectCreateMessage( @@ -3583,7 +3573,7 @@ class SessionActor extends Actor with MDCContextAware { }) .foreach({ case (index, seat) => - val targetPlayer = seat.Occupant.get + val targetPlayer = seat.Occupant.get val targetDefinition = targetPlayer.avatar.definition sendResponse( ObjectCreateMessage( @@ -3637,9 +3627,9 @@ class SessionActor extends Actor with MDCContextAware { //special effects sendResponse(PlanetsideAttributeMessage(obj.GUID, 52, 1)) // ant panel glow Vehicles.FindANTChargingSource(obj, None).orElse(Vehicles.FindANTDischargingTarget(obj, None)) match { - case Some(silo: ResourceSilo) => + case Some(silo : ResourceSilo) => sendResponse(PlanetsideAttributeMessage(silo.GUID, 49, 1)) // silo orb particle effect - case Some(_: WarpGate) => + case Some(_ : WarpGate) => sendResponse(PlanetsideAttributeMessage(obj.GUID, 49, 1)) // ant orb particle effect case _ => ; } @@ -3656,7 +3646,7 @@ class SessionActor extends Actor with MDCContextAware { case ((terminal_guid, interface_guid)) => val parent_guid = PlanetSideGUID(terminal_guid) continent.GUID(interface_guid) match { - case Some(obj: Terminal) => + case Some(obj : Terminal) => val objDef = obj.Definition sendResponse( ObjectCreateMessage( @@ -3670,7 +3660,7 @@ class SessionActor extends Actor with MDCContextAware { } //seat terminal occupants continent.GUID(terminal_guid) match { - case Some(obj: Mountable) => + case Some(obj : Mountable) => obj.Seats(0).Occupant match { case Some(targetPlayer) => val targetDefinition = targetPlayer.avatar.definition @@ -3692,12 +3682,12 @@ class SessionActor extends Actor with MDCContextAware { continent.map.turretToWeapon .map { case ((turret_guid, _)) => continent.GUID(turret_guid) } .collect { - case Some(turret: FacilityTurret) => + case Some(turret : FacilityTurret) => val pguid = turret.GUID //attached weapon if (!turret.isUpgrading) { turret.ControlledWeapon(wepNumber = 1) match { - case Some(obj: Tool) => + case Some(obj : Tool) => val objDef = obj.Definition sendResponse( ObjectCreateMessage( @@ -3727,7 +3717,10 @@ class SessionActor extends Actor with MDCContextAware { case None => ; } } - continent.VehicleEvents ! VehicleServiceMessage(continent.id, VehicleAction.UpdateAmsSpawnPoint(continent)) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.UpdateAmsSpawnPoint(continent) + ) upstreamMessageCount = 0 zoneLoaded = Some(true) @@ -3750,7 +3743,7 @@ class SessionActor extends Actor with MDCContextAware { //log.info(s"$msg") persist() turnCounterFunc(avatar_guid) - val isMoving = WorldEntity.isMoving(vel) + val isMoving = WorldEntity.isMoving(vel) val isMovingPlus = isMoving || is_jumping || jump_thrust if (isMovingPlus) { CancelZoningProcessWithDescriptiveReason("cancel_motion") @@ -3775,7 +3768,10 @@ class SessionActor extends Actor with MDCContextAware { } accessedContainer match { case Some(veh: Vehicle) => - if (isMoving || veh.isMoving(1) || Vector3.DistanceSquared(player.Position, veh.TrunkLocation) > 9) { + if (isMoving || veh.isMoving(1) || Vector3.DistanceSquared( + player.Position, + veh.TrunkLocation + ) > 9) { val guid = player.GUID sendResponse(UnuseItemMessage(guid, veh.GUID)) sendResponse(UnuseItemMessage(guid, guid)) @@ -3861,18 +3857,18 @@ class SessionActor extends Actor with MDCContextAware { } case msg @ VehicleStateMessage( - vehicle_guid, - unk1, - pos, - ang, - vel, - flying, - unk6, - unk7, - wheels, - is_decelerating, - is_cloaked - ) => + vehicle_guid, + unk1, + pos, + ang, + vel, + flying, + unk6, + unk7, + wheels, + is_decelerating, + is_cloaked + ) => //log.info(s"$msg") GetVehicleAndSeat() match { case (Some(obj), Some(0)) => @@ -4096,7 +4092,7 @@ class SessionActor extends Actor with MDCContextAware { //the decimator does not send a ChangeFireState_Start on the last shot if ( tool.Definition == GlobalDefinitions.phoenix && - tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile + tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile ) { //suppress the decimator's alternate fire mode, however continent.AvatarEvents ! AvatarServiceMessage( @@ -4221,12 +4217,12 @@ class SessionActor extends Actor with MDCContextAware { } val sumReloadValue: Int = box.Capacity + tailReloadValue val actualReloadValue = (if (sumReloadValue <= reloadValue) { - deleteFunc(box) - sumReloadValue - } else { - modifyFunc(box, reloadValue - tailReloadValue) - reloadValue - }) + currentMagazine + deleteFunc(box) + sumReloadValue + } else { + modifyFunc(box, reloadValue - tailReloadValue) + reloadValue + }) + currentMagazine log.info(s"ReloadMessage: success, $tool <- $actualReloadValue ${tool.AmmoType}") tool.Magazine = actualReloadValue sendResponse(ReloadMessage(item_guid, actualReloadValue, unk1)) @@ -4439,10 +4435,10 @@ class SessionActor extends Actor with MDCContextAware { log.info(s"MoveItem: $msg") (continent.GUID(source_guid), continent.GUID(destination_guid), ValidObject(item_guid)) match { case ( - Some(source: PlanetSideServerObject with Container), - Some(destination: PlanetSideServerObject with Container), - Some(item: Equipment) - ) => + Some(source: PlanetSideServerObject with Container), + Some(destination: PlanetSideServerObject with Container), + Some(item: Equipment) + ) => source.Actor ! Containable.MoveItem(destination, item, dest) case (None, _, _) => log.error(s"MoveItem: wanted to move $item_guid from $source_guid, but could not find source object") @@ -4514,18 +4510,18 @@ class SessionActor extends Actor with MDCContextAware { } case msg @ UseItemMessage( - avatar_guid, - item_used_guid, - object_guid, - unk2, - unk3, - unk4, - unk5, - unk6, - unk7, - unk8, - itemType - ) => + avatar_guid, + item_used_guid, + object_guid, + unk2, + unk3, + unk4, + unk5, + unk6, + unk7, + unk8, + itemType + ) => //log.info("UseItem: " + msg) // 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) @@ -4802,8 +4798,8 @@ class SessionActor extends Actor with MDCContextAware { //access to trunk if ( obj.AccessingTrunk.isEmpty && - (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.Owner - .contains(player.GUID)) + (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.Owner + .contains(player.GUID)) ) { CancelZoningProcessWithDescriptiveReason("cancel_use") obj.AccessingTrunk = player.GUID @@ -4836,7 +4832,7 @@ class SessionActor extends Actor with MDCContextAware { terminal.Actor ! CommonMessages.Use(player, Some(item)) case None - if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction || terminal.HackedBy.nonEmpty => + if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction || terminal.HackedBy.nonEmpty => val tdef = terminal.Definition if (tdef.isInstanceOf[MatrixTerminalDefinition]) { //TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks) @@ -4846,7 +4842,7 @@ class SessionActor extends Actor with MDCContextAware { ) } else if ( tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal || - tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal + tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal ) { FindLocalVehicle match { case Some(vehicle) => @@ -5215,7 +5211,6 @@ class SessionActor extends Actor with MDCContextAware { case msg @ FavoritesRequest(player_guid, loadoutType, action, line, label) => CancelZoningProcessWithDescriptiveReason("cancel_use") log.info(s"FavoritesRequest: $msg") - action match { case FavoritesAction.Save => avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line) case FavoritesAction.Delete => avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line) @@ -5237,18 +5232,18 @@ class SessionActor extends Actor with MDCContextAware { } case msg @ WeaponFireMessage( - seq_time, - weapon_guid, - projectile_guid, - shot_origin, - unk1, - unk2, - unk3, - unk4, - unk5, - unk6, - unk7 - ) => + seq_time, + weapon_guid, + projectile_guid, + shot_origin, + unk1, + unk2, + unk3, + unk4, + unk5, + unk6, + unk7 + ) => //log.info(s"WeaponFire: $msg") HandleWeaponFire(weapon_guid, projectile_guid, shot_origin) @@ -5270,7 +5265,15 @@ class SessionActor extends Actor with MDCContextAware { case _ => ; } - case msg @ HitMessage(seq_time, projectile_guid, unk1, hit_info, unk2, unk3, unk4) => + case msg @ HitMessage( + seq_time, + projectile_guid, + unk1, + hit_info, + unk2, + unk3, + unk4 + ) => log.info(s"Hit: $msg") //find defined projectile FindProjectileEntry(projectile_guid) match { @@ -5337,25 +5340,33 @@ class SessionActor extends Actor with MDCContextAware { } case msg @ SplashHitMessage( - seq_time, - projectile_guid, - explosion_pos, - direct_victim_uid, - unk3, - projectile_vel, - unk4, - targets - ) => + seq_time, + projectile_guid, + explosion_pos, + direct_victim_uid, + unk3, + projectile_vel, + unk4, + targets + ) => log.info(s"Splash: $msg") FindProjectileEntry(projectile_guid) match { case Some(projectile) => + val profile = projectile.profile projectile.Position = explosion_pos projectile.Velocity = projectile_vel + val (resolution1, resolution2) = profile.Aggravated match { + case Some(_) + if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) => + (ProjectileResolution.AggravatedDirect, ProjectileResolution.AggravatedSplash) + case _ => + (ProjectileResolution.Splash, ProjectileResolution.Splash) + } //direct_victim_uid ValidObject(direct_victim_uid) match { case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) - ResolveProjectileEntry(projectile, ProjectileResolution.Splash, target, target.Position) match { + CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target) + ResolveProjectileEntry(projectile, resolution1, target, target.Position) match { case Some(projectile) => HandleDealingDamage(target, projectile) case None => ; @@ -5367,7 +5378,7 @@ class SessionActor extends Actor with MDCContextAware { ValidObject(elem.uid) match { case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) - ResolveProjectileEntry(projectile, ProjectileResolution.Splash, target, explosion_pos) match { + ResolveProjectileEntry(projectile, resolution2, target, explosion_pos) match { case Some(projectile) => HandleDealingDamage(target, projectile) case None => ; @@ -5375,7 +5386,7 @@ class SessionActor extends Actor with MDCContextAware { case _ => ; } }) - if (projectile.profile.ExistsOnRemoteClients && projectile.HasGUID) { + if (profile.ExistsOnRemoteClients && projectile.HasGUID) { //cleanup val localIndex = projectile_guid.guid - Projectile.baseUID if (projectile.HasGUID) { @@ -5416,11 +5427,11 @@ class SessionActor extends Actor with MDCContextAware { if (deadState != DeadState.RespawnTime) { continent.Buildings.values.find(building => building.GUID == building_guid) match { case Some(wg: WarpGate) if (wg.Active && (GetKnownVehicleAndSeat() match { - case (Some(vehicle), _) => - wg.Definition.VehicleAllowance && !wg.Definition.NoWarp.contains(vehicle.Definition) - case _ => - true - })) => + case (Some(vehicle), _) => + wg.Definition.VehicleAllowance && !wg.Definition.NoWarp.contains(vehicle.Definition) + case _ => + true + })) => deadState = DeadState.RespawnTime cluster ! InterstellarClusterService.GetSpawnPoint( destinationZoneGuid.guid, @@ -5578,17 +5589,17 @@ class SessionActor extends Actor with MDCContextAware { log.debug("Ouch! " + msg) case msg @ BugReportMessage( - version_major, - version_minor, - version_date, - bug_type, - repeatable, - location, - zone, - pos, - summary, - desc - ) => + version_major, + version_minor, + version_date, + bug_type, + repeatable, + location, + zone, + pos, + summary, + desc + ) => log.info("BugReportMessage: " + msg) case msg @ BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) => @@ -6295,7 +6306,7 @@ class SessionActor extends Actor with MDCContextAware { sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3.Zero, 0f)) sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3.Zero, 0f)) obj.Inventory -= x.start //remove replacement ammo from inventory - val ammoSlotIndex = tool.FireMode.AmmoSlotIndex + val ammoSlotIndex = tool.FireMode.AmmoSlotIndex tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool sendResponse(ObjectAttachMessage(tool.GUID, box.GUID, ammoSlotIndex)) @@ -6320,16 +6331,16 @@ class SessionActor extends Actor with MDCContextAware { //handle inventory contents box.Capacity = (if (sumReloadValue <= fullMagazine) { - sumReloadValue - } else { - val splitReloadAmmo: Int = sumReloadValue - fullMagazine - log.info( - s"ChangeAmmo: taking ${originalBoxCapacity - splitReloadAmmo} from a box of ${originalBoxCapacity} $requestedAmmoType" - ) - val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) - taskResolver ! stowNewFunc(boxForInventory) - fullMagazine - }) + sumReloadValue + } else { + val splitReloadAmmo: Int = sumReloadValue - fullMagazine + log.info( + s"ChangeAmmo: taking ${originalBoxCapacity - splitReloadAmmo} from a box of ${originalBoxCapacity} $requestedAmmoType" + ) + val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) + taskResolver ! stowNewFunc(boxForInventory) + fullMagazine + }) sendResponse( InventoryStateMessage(box.GUID, tool.GUID, box.Capacity) ) //should work for both players and vehicles @@ -6443,12 +6454,12 @@ class SessionActor extends Actor with MDCContextAware { else { xs.map(_.obj.asInstanceOf[Tool].Magazine).reduce(_ + _) } val sumReloadValue: Int = box.Magazine + tailReloadValue val actualReloadValue = (if (sumReloadValue <= 3) { - RemoveOldEquipmentFromInventory(player, taskResolver)(x.obj) - sumReloadValue - } else { - ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) - 3 - }) + RemoveOldEquipmentFromInventory(player, taskResolver)(x.obj) + sumReloadValue + } else { + ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) + 3 + }) log.info(s"found $actualReloadValue more $ammoType grenades to throw") ModifyAmmunition(player)( tool.AmmoSlot.Box, @@ -6470,8 +6481,8 @@ class SessionActor extends Actor with MDCContextAware { * the second value is the slot position of the object */ def FindInLocalContainer( - object_guid: PlanetSideGUID - )(parent: PlanetSideServerObject with Container): Option[(PlanetSideServerObject with Container, Option[Int])] = { + object_guid: PlanetSideGUID + )(parent: PlanetSideServerObject with Container): Option[(PlanetSideServerObject with Container, Option[Int])] = { val slot: Option[Int] = parent.Find(object_guid) slot match { case place @ Some(_) => @@ -6488,10 +6499,10 @@ class SessionActor extends Actor with MDCContextAware { * @param reason a string explaining why the state can not or will not change */ def CanNotChangeDeployment( - obj: PlanetSideServerObject with Deployment, - state: DriveState.Value, - reason: String - ): Unit = { + obj: PlanetSideServerObject with Deployment, + state: DriveState.Value, + reason: String + ): Unit = { val mobileShift: String = if (obj.DeploymentState != DriveState.Mobile) { obj.DeploymentState = DriveState.Mobile sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Mobile, 0, false, Vector3.Zero)) @@ -6933,10 +6944,10 @@ class SessionActor extends Actor with MDCContextAware { * `(None, None)`, otherwise (even if the vehicle can be determined) */ def GetMountableAndSeat( - direct: Option[PlanetSideGameObject with Mountable], - occupant: Player, - zone: Zone - ): (Option[PlanetSideGameObject with Mountable], Option[Int]) = + direct: Option[PlanetSideGameObject with Mountable], + occupant: Player, + zone: Zone + ): (Option[PlanetSideGameObject with Mountable], Option[Int]) = direct.orElse(zone.GUID(occupant.VehicleSeated)) match { case Some(obj: PlanetSideGameObject with Mountable) => obj.PassengerInSeat(occupant) match { @@ -7613,11 +7624,11 @@ class SessionActor extends Actor with MDCContextAware { * @return the projectile */ def ResolveProjectileEntry( - projectile_guid: PlanetSideGUID, - resolution: ProjectileResolution.Value, - target: PlanetSideGameObject with FactionAffinity with Vitality, - pos: Vector3 - ): Option[ResolvedProjectile] = { + projectile_guid: PlanetSideGUID, + resolution: ProjectileResolution.Value, + target: PlanetSideGameObject with FactionAffinity with Vitality, + pos: Vector3 + ): Option[ResolvedProjectile] = { FindProjectileEntry(projectile_guid) match { case Some(projectile) => ResolveProjectileEntry(projectile, resolution, target, pos) @@ -7635,12 +7646,12 @@ class SessionActor extends Actor with MDCContextAware { * @return a copy of the projectile */ def ResolveProjectileEntry( - projectile: Projectile, - index: Int, - resolution: ProjectileResolution.Value, - target: PlanetSideGameObject with FactionAffinity with Vitality, - pos: Vector3 - ): Option[ResolvedProjectile] = { + projectile: Projectile, + index: Int, + resolution: ProjectileResolution.Value, + target: PlanetSideGameObject with FactionAffinity with Vitality, + pos: Vector3 + ): Option[ResolvedProjectile] = { if (!projectiles(index).contains(projectile)) { log.error(s"expected projectile could not be found at $index; can not resolve") None @@ -7656,17 +7667,31 @@ class SessionActor extends Actor with MDCContextAware { * @return a copy of the projectile */ def ResolveProjectileEntry( - projectile: Projectile, - resolution: ProjectileResolution.Value, - target: PlanetSideGameObject with FactionAffinity with Vitality, - pos: Vector3 - ): Option[ResolvedProjectile] = { + projectile: Projectile, + resolution: ProjectileResolution.Value, + target: PlanetSideGameObject with FactionAffinity with Vitality, + pos: Vector3 + ): Option[ResolvedProjectile] = { if (projectile.isMiss) { log.error("expected projectile was already counted as a missed shot; can not resolve any further") None } else { projectile.Resolve() - Some(ResolvedProjectile(resolution, projectile, SourceEntry(target), target.DamageModel, pos)) + val outProjectile = if(projectile.profile.ProjectileDamageTypes.contains(DamageType.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 { + projectile + } + Some(ResolvedProjectile(resolution, outProjectile, SourceEntry(target), target.DamageModel, pos)) } } @@ -7752,11 +7777,11 @@ class SessionActor extends Actor with MDCContextAware { * @return a `DestroyDisplayMessage` packet that is properly formatted */ def DestroyDisplayMessage( - killer: SourceEntry, - victim: SourceEntry, - method: Int, - unk: Int = 121 - ): DestroyDisplayMessage = { + killer: SourceEntry, + victim: SourceEntry, + method: Int, + unk: Int = 121 + ): DestroyDisplayMessage = { val killer_seated = killer match { case obj: PlayerSource => obj.Seated case _ => false @@ -7916,9 +7941,9 @@ class SessionActor extends Actor with MDCContextAware { * @return `true`, if the desired certification requirements are met; `false`, otherwise */ def ConstructionItemPermissionComparison( - sample: Set[Certification], - test: Set[Certification] - ): Boolean = { + sample: Set[Certification], + test: Set[Certification] + ): Boolean = { import Certification._ val engineeringCerts: Set[Certification] = Set(AssaultEngineering, FortificationEngineering) val testDiff: Set[Certification] = test diff (engineeringCerts ++ Set(AdvancedEngineering)) @@ -8063,10 +8088,10 @@ class SessionActor extends Actor with MDCContextAware { * `false`, otherwise */ def SafelyRemoveConstructionItemFromSlot( - tool: ConstructionItem, - index: Int, - logDecorator: String = "SafelyRemoveConstructionItemFromSlot" - ): Boolean = { + tool: ConstructionItem, + index: Int, + logDecorator: String = "SafelyRemoveConstructionItemFromSlot" + ): Boolean = { if ({ val holster = player.Slot(index) if (holster.Equipment.contains(tool)) { @@ -8165,7 +8190,7 @@ class SessionActor extends Actor with MDCContextAware { */ def FindEquipmentToDelete(object_guid: PlanetSideGUID, obj: Equipment): Boolean = { val findFunc - : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] = + : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] = FindInLocalContainer(object_guid) findFunc(player.avatar.locker) @@ -8232,12 +8257,12 @@ class SessionActor extends Actor with MDCContextAware { * @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation */ def DeconstructDeployable( - obj: PlanetSideGameObject with Deployable, - guid: PlanetSideGUID, - pos: Vector3, - orient: Vector3, - deletionType: Int - ): Unit = { + obj: PlanetSideGameObject with Deployable, + guid: PlanetSideGUID, + pos: Vector3, + orient: Vector3, + deletionType: Int + ): Unit = { StartBundlingPackets() sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL)) //for some, removes the green marker circle sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient)) @@ -8501,17 +8526,17 @@ class SessionActor extends Actor with MDCContextAware { player.Continent = zoneId //forward-set the continent id to perform a test interstellarFerryTopLevelGUID = (if ( - manifest.passengers.isEmpty && manifest.cargo.count { case (name, _) => !name.equals("MISSING_DRIVER") } == 0 - ) { - //do not delete if vehicle has passengers or cargo - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.UnloadVehicle(pguid, continent, vehicle, topLevel) - ) - None - } else { - Some(topLevel) - }) + manifest.passengers.isEmpty && manifest.cargo.count { case (name, _) => !name.equals("MISSING_DRIVER") } == 0 + ) { + //do not delete if vehicle has passengers or cargo + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.UnloadVehicle(pguid, continent, vehicle, topLevel) + ) + None + } else { + Some(topLevel) + }) //unregister vehicle and driver whole + GiveWorld continent.Transport ! Zone.Vehicle.Despawn(vehicle) taskThenZoneChange( @@ -8675,10 +8700,10 @@ class SessionActor extends Actor with MDCContextAware { * @param remoteTelepad the endpoint of the teleportation system that exists in the environment */ def LinkRouterToRemoteTelepad( - router: Vehicle, - internalTelepad: Utility.InternalTelepad, - remoteTelepad: TelepadDeployable - ): Unit = { + router: Vehicle, + internalTelepad: Utility.InternalTelepad, + remoteTelepad: TelepadDeployable + ): Unit = { internalTelepad.Telepad = remoteTelepad.GUID //necessary; backwards link to the (new) telepad CreateRouterInternalTelepad(router, internalTelepad) LinkRemoteTelepad(remoteTelepad.GUID) @@ -8739,12 +8764,12 @@ class SessionActor extends Actor with MDCContextAware { * @param dest the destination of the teleportation (where the player is going) */ def UseRouterTelepadSystem( - router: Vehicle, - internalTelepad: InternalTelepad, - remoteTelepad: TelepadDeployable, - src: PlanetSideGameObject with TelepadLike, - dest: PlanetSideGameObject with TelepadLike - ) = { + router: Vehicle, + internalTelepad: InternalTelepad, + remoteTelepad: TelepadDeployable, + src: PlanetSideGameObject with TelepadLike, + dest: PlanetSideGameObject with TelepadLike + ) = { val time = System.nanoTime if ( time - recentTeleportAttempt > (2 seconds).toNanos && router.DeploymentState == DriveState.Deployed && internalTelepad.Active && remoteTelepad.Active @@ -8993,7 +9018,7 @@ class SessionActor extends Actor with MDCContextAware { // Charge else if ( player.Capacitor < player.ExoSuitDef.MaxCapacitor - && (player.CapacitorState == CapacitorStateType.Idle || player.CapacitorState == CapacitorStateType.Charging || (player.CapacitorState == CapacitorStateType.ChargeDelay && System + && (player.CapacitorState == CapacitorStateType.Idle || player.CapacitorState == CapacitorStateType.Charging || (player.CapacitorState == CapacitorStateType.ChargeDelay && System .currentTimeMillis() - player.CapacitorLastUsedMillis > player.ExoSuitDef.CapacitorRechargeDelayMillis)) ) { if (player.CapacitorState == CapacitorStateType.Charging) { @@ -9127,10 +9152,10 @@ class SessionActor extends Actor with MDCContextAware { } def CheckForHitPositionDiscrepancy( - projectile_guid: PlanetSideGUID, - hitPos: Vector3, - target: PlanetSideGameObject with FactionAffinity with Vitality - ): Unit = { + projectile_guid: PlanetSideGUID, + hitPos: Vector3, + target: PlanetSideGameObject with FactionAffinity with Vitality + ): Unit = { val hitPositionDiscrepancy = Vector3.DistanceSquared(hitPos, target.Position) if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) { // If the target position on the server does not match the position where the projectile landed within reason there may be foul play @@ -9340,18 +9365,18 @@ class SessionActor extends Actor with MDCContextAware { s"WeaponFireMessage: ${projectile_info.Name} is a remote projectile" ) taskResolver ! (if (projectile.HasGUID) { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ProjectileExplodes( - player.GUID, - projectile.GUID, - projectile - ) - ) - ReregisterProjectile(projectile) - } else { - RegisterProjectile(projectile) - }) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ProjectileExplodes( + player.GUID, + projectile.GUID, + projectile + ) + ) + ReregisterProjectile(projectile) + } else { + RegisterProjectile(projectile) + }) } projectilesToCleanUp(projectileIndex) = false diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 66c261ab..252d3e0c 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -9,7 +9,9 @@ import net.psforever.objects.definition.converter.SmallDeployableConverter import net.psforever.objects.equipment.JammableUnit import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.damage.Damageable -import net.psforever.objects.vital.{StandardResolutions, Vitality} +import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.vital.resolution.ResolutionCalculations.Output +import net.psforever.objects.vital.StandardResolutions import net.psforever.objects.zones.Zone import net.psforever.types.{PlanetSideGUID, Vector3} import net.psforever.services.Service @@ -63,18 +65,20 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D case _ => ; } - protected def TakesDamage: Receive = { - case Vitality.Damage(applyDamageTo) => - if (mine.CanDamage) { - val originalHealth = mine.Health - val cause = applyDamageTo(mine) - val damage = originalHealth - mine.Health - if (Damageable.CanDamageOrJammer(mine, damage, cause)) { - ExplosiveDeployableControl.DamageResolution(mine, cause, damage) - } else { - mine.Health = originalHealth - } + override protected def PerformDamage( + target: Target, + applyDamageTo: Output + ): Unit = { + if (mine.CanDamage) { + val originalHealth = mine.Health + val cause = applyDamageTo(mine) + val damage = originalHealth - mine.Health + if (Damageable.CanDamageOrJammer(mine, damage, cause)) { + ExplosiveDeployableControl.DamageResolution(mine, cause, damage) + } else { + mine.Health = originalHealth } + } } } diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 00f38e4e..8e1d65ff 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -2,12 +2,13 @@ package net.psforever.objects import net.psforever.objects.avatar.Certification -import net.psforever.objects.ballistics.Projectiles +import net.psforever.objects.ballistics.{AggravatedDamage, AggravatedInfo, AggravatedTiming, Projectiles} import net.psforever.objects.ce.{DeployableCategory, DeployedItem} import net.psforever.objects.definition._ import net.psforever.objects.definition.converter._ import net.psforever.objects.equipment._ import net.psforever.objects.inventory.InventoryTile +import net.psforever.objects.serverobject.aura.Aura import net.psforever.objects.serverobject.doors.DoorDefinition import net.psforever.objects.serverobject.generator.GeneratorDefinition import net.psforever.objects.serverobject.implantmech.ImplantTerminalMechDefinition @@ -207,6 +208,8 @@ object GlobalDefinitions { val flail_projectile = ProjectileDefinition(Projectiles.flail_projectile) + val flamethrower_fire_cloud = ProjectileDefinition(Projectiles.flamethrower_projectile) //flamethrower_fire_cloud + val flamethrower_fireball = ProjectileDefinition(Projectiles.flamethrower_fireball) val flamethrower_projectile = ProjectileDefinition(Projectiles.flamethrower_projectile) @@ -2037,6 +2040,7 @@ object GlobalDefinitions { no_projectile.Name = "none" ProjectileDefinition.CalculateDerivedFields(no_projectile) + no_projectile.Modifiers = Nil bullet_105mm_projectile.Name = "105mmbullet_projectile" bullet_105mm_projectile.Damage0 = 150 @@ -2269,6 +2273,7 @@ object GlobalDefinitions { 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 @@ -2313,6 +2318,14 @@ object GlobalDefinitions { 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 @@ -2352,6 +2365,7 @@ object GlobalDefinitions { chainblade_projectile.InitialVelocity = 100 chainblade_projectile.Lifespan = .02f ProjectileDefinition.CalculateDerivedFields(chainblade_projectile) + chainblade_projectile.Modifiers = DamageModifiers.MaxDistanceCutoff colossus_100mm_projectile.Name = "colossus_100mm_projectile" colossus_100mm_projectile.Damage0 = 58 @@ -2437,9 +2451,24 @@ object GlobalDefinitions { comet_projectile.DamageAtEdge = 0.45f comet_projectile.DamageRadius = 1.0f comet_projectile.ProjectileDamageType = DamageType.Aggravated + comet_projectile.Aggravated = AggravatedDamage( + AggravatedInfo(DamageType.Direct, 0.25f, 500), //originally, .2 + Aura.Comet, + AggravatedTiming(2000, 3), + 10f, + List( + TargetValidation(EffectTarget.Category.Player, EffectTarget.Validation.Player), + TargetValidation(EffectTarget.Category.Vehicle, EffectTarget.Validation.Vehicle), + TargetValidation(EffectTarget.Category.Turret, EffectTarget.Validation.Turret) + ) + ) comet_projectile.InitialVelocity = 80 comet_projectile.Lifespan = 3.1f ProjectileDefinition.CalculateDerivedFields(comet_projectile) + comet_projectile.Modifiers = List( + DamageModifiers.CometAggravated, + DamageModifiers.CometAggravatedBurn + ) dualcycler_projectile.Name = "dualcycler_projectile" dualcycler_projectile.Damage0 = 18 @@ -2457,6 +2486,7 @@ object GlobalDefinitions { dynomite_projectile.Damage1 = 175 dynomite_projectile.DamageAtEdge = 0.1f dynomite_projectile.DamageRadius = 10f + dynomite_projectile.GrenadeProjectile = true dynomite_projectile.ProjectileDamageType = DamageType.Splash dynomite_projectile.InitialVelocity = 30 dynomite_projectile.Lifespan = 3f @@ -2579,12 +2609,28 @@ object GlobalDefinitions { flamethrower_fireball.Damage2 = 0 flamethrower_fireball.Damage3 = 20 flamethrower_fireball.Damage4 = 0 + flamethrower_fireball.DamageToHealthOnly = true flamethrower_fireball.DamageAtEdge = 0.15f flamethrower_fireball.DamageRadius = 5f flamethrower_fireball.ProjectileDamageType = DamageType.Aggravated + flamethrower_fireball.Aggravated = AggravatedDamage( + List(AggravatedInfo(DamageType.Direct, 0.9f, 500), AggravatedInfo(DamageType.Splash, 0.9f, 500)), + Aura.Fire, + AggravatedTiming(5000, 10), + 0.1f, + false, + false, + List(TargetValidation(EffectTarget.Category.Player, EffectTarget.Validation.Player)) + ) flamethrower_fireball.InitialVelocity = 15 flamethrower_fireball.Lifespan = 1.2f ProjectileDefinition.CalculateDerivedFields(flamethrower_fireball) + flamethrower_fireball.Modifiers = List( + DamageModifiers.InfantryAggravatedDirect, + DamageModifiers.InfantryAggravatedSplash, + DamageModifiers.RadialDegrade, + DamageModifiers.FireballAggravatedBurn + ) flamethrower_projectile.Name = "flamethrower_projectile" flamethrower_projectile.Damage0 = 10 @@ -2592,14 +2638,29 @@ object GlobalDefinitions { flamethrower_projectile.Damage2 = 0 flamethrower_projectile.Damage3 = 4 flamethrower_projectile.Damage4 = 0 + flamethrower_projectile.DamageToHealthOnly = true flamethrower_projectile.Acceleration = -5 flamethrower_projectile.AccelerationUntil = 2f flamethrower_projectile.ProjectileDamageType = DamageType.Aggravated + flamethrower_projectile.Aggravated = AggravatedDamage( + List(AggravatedInfo(DamageType.Direct, 0.5f, 500)), + Aura.Fire, + AggravatedTiming(5000, 10), + 0.5f, + false, + false, + List(TargetValidation(EffectTarget.Category.Player, EffectTarget.Validation.Player)) + ) flamethrower_projectile.DegradeDelay = 1.0f flamethrower_projectile.DegradeMultiplier = 0.5f flamethrower_projectile.InitialVelocity = 10 flamethrower_projectile.Lifespan = 2.0f ProjectileDefinition.CalculateDerivedFields(flamethrower_projectile) + flamethrower_projectile.Modifiers = List( + DamageModifiers.InfantryAggravatedDirect, + DamageModifiers.FireballAggravatedBurn, + DamageModifiers.MaxDistanceCutoff + ) flux_cannon_apc_projectile.Name = "flux_cannon_apc_projectile" // TODO for later, maybe : set_resource_parent flux_cannon_apc_projectile game_objects flux_cannon_thresher_projectile @@ -2651,6 +2712,7 @@ object GlobalDefinitions { forceblade_projectile.InitialVelocity = 100 forceblade_projectile.Lifespan = .02f ProjectileDefinition.CalculateDerivedFields(forceblade_projectile) + forceblade_projectile.Modifiers = DamageModifiers.MaxDistanceCutoff frag_cartridge_projectile.Name = "frag_cartridge_projectile" // TODO for later, maybe : set_resource_parent frag_cartridge_projectile game_objects frag_grenade_projectile @@ -2658,6 +2720,7 @@ object GlobalDefinitions { frag_cartridge_projectile.Damage1 = 100 frag_cartridge_projectile.DamageAtEdge = 0.1f frag_cartridge_projectile.DamageRadius = 7f + frag_cartridge_projectile.GrenadeProjectile = true frag_cartridge_projectile.ProjectileDamageType = DamageType.Splash frag_cartridge_projectile.InitialVelocity = 30 frag_cartridge_projectile.Lifespan = 15f @@ -2670,6 +2733,7 @@ object GlobalDefinitions { frag_cartridge_projectile_b.Damage1 = 100 frag_cartridge_projectile_b.DamageAtEdge = 0.1f frag_cartridge_projectile_b.DamageRadius = 5f + frag_cartridge_projectile_b.GrenadeProjectile = true frag_cartridge_projectile_b.ProjectileDamageType = DamageType.Splash frag_cartridge_projectile_b.InitialVelocity = 30 frag_cartridge_projectile_b.Lifespan = 2f @@ -2681,6 +2745,7 @@ object GlobalDefinitions { frag_grenade_projectile.Damage1 = 100 frag_grenade_projectile.DamageAtEdge = 0.1f frag_grenade_projectile.DamageRadius = 7f + frag_grenade_projectile.GrenadeProjectile = true frag_grenade_projectile.ProjectileDamageType = DamageType.Splash frag_grenade_projectile.InitialVelocity = 30 frag_grenade_projectile.Lifespan = 15f @@ -2693,6 +2758,7 @@ object GlobalDefinitions { frag_grenade_projectile_enh.Damage1 = 100 frag_grenade_projectile_enh.DamageAtEdge = 0.1f frag_grenade_projectile_enh.DamageRadius = 7f + frag_grenade_projectile_enh.GrenadeProjectile = true frag_grenade_projectile_enh.ProjectileDamageType = DamageType.Splash frag_grenade_projectile_enh.InitialVelocity = 30 frag_grenade_projectile_enh.Lifespan = 2f @@ -2743,6 +2809,7 @@ object GlobalDefinitions { heavy_grenade_projectile.Damage4 = 66 heavy_grenade_projectile.DamageAtEdge = 0.1f heavy_grenade_projectile.DamageRadius = 5f + heavy_grenade_projectile.GrenadeProjectile = true heavy_grenade_projectile.ProjectileDamageType = DamageType.Splash heavy_grenade_projectile.InitialVelocity = 75 heavy_grenade_projectile.Lifespan = 5f @@ -2828,6 +2895,7 @@ object GlobalDefinitions { jammer_cartridge_projectile.Damage1 = 0 jammer_cartridge_projectile.DamageAtEdge = 1.0f jammer_cartridge_projectile.DamageRadius = 10f + jammer_cartridge_projectile.GrenadeProjectile = true jammer_cartridge_projectile.ProjectileDamageType = DamageType.Splash jammer_cartridge_projectile.InitialVelocity = 30 jammer_cartridge_projectile.Lifespan = 15f @@ -2858,7 +2926,7 @@ object GlobalDefinitions { EffectTarget.Validation.VehicleNotAMS ) -> 10000 ProjectileDefinition.CalculateDerivedFields(jammer_cartridge_projectile) - jammer_cartridge_projectile.Modifiers = Nil + jammer_cartridge_projectile.Modifiers = DamageModifiers.MaxDistanceCutoff jammer_cartridge_projectile_b.Name = "jammer_cartridge_projectile_b" // TODO for later, maybe : set_resource_parent jammer_cartridge_projectile_b game_objects jammer_grenade_projectile_enh @@ -2866,6 +2934,7 @@ object GlobalDefinitions { jammer_cartridge_projectile_b.Damage1 = 0 jammer_cartridge_projectile_b.DamageAtEdge = 1.0f jammer_cartridge_projectile_b.DamageRadius = 10f + jammer_cartridge_projectile_b.GrenadeProjectile = true jammer_cartridge_projectile_b.ProjectileDamageType = DamageType.Splash jammer_cartridge_projectile_b.InitialVelocity = 30 jammer_cartridge_projectile_b.Lifespan = 2f @@ -2896,13 +2965,14 @@ object GlobalDefinitions { EffectTarget.Validation.VehicleNotAMS ) -> 10000 ProjectileDefinition.CalculateDerivedFields(jammer_cartridge_projectile_b) - jammer_cartridge_projectile_b.Modifiers = Nil + jammer_cartridge_projectile_b.Modifiers = DamageModifiers.MaxDistanceCutoff jammer_grenade_projectile.Name = "jammer_grenade_projectile" jammer_grenade_projectile.Damage0 = 0 jammer_grenade_projectile.Damage1 = 0 jammer_grenade_projectile.DamageAtEdge = 1.0f jammer_grenade_projectile.DamageRadius = 10f + jammer_grenade_projectile.GrenadeProjectile = true jammer_grenade_projectile.ProjectileDamageType = DamageType.Splash jammer_grenade_projectile.InitialVelocity = 30 jammer_grenade_projectile.Lifespan = 15f @@ -2933,7 +3003,7 @@ object GlobalDefinitions { EffectTarget.Validation.VehicleNotAMS ) -> 10000 ProjectileDefinition.CalculateDerivedFields(jammer_grenade_projectile) - jammer_grenade_projectile.Modifiers = Nil + jammer_grenade_projectile.Modifiers = DamageModifiers.MaxDistanceCutoff jammer_grenade_projectile_enh.Name = "jammer_grenade_projectile_enh" // TODO for later, maybe : set_resource_parent jammer_grenade_projectile_enh game_objects jammer_grenade_projectile @@ -2941,6 +3011,7 @@ object GlobalDefinitions { jammer_grenade_projectile_enh.Damage1 = 0 jammer_grenade_projectile_enh.DamageAtEdge = 1.0f jammer_grenade_projectile_enh.DamageRadius = 10f + jammer_grenade_projectile_enh.GrenadeProjectile = true jammer_grenade_projectile_enh.ProjectileDamageType = DamageType.Splash jammer_grenade_projectile_enh.InitialVelocity = 30 jammer_grenade_projectile_enh.Lifespan = 3f @@ -2971,7 +3042,7 @@ object GlobalDefinitions { EffectTarget.Validation.VehicleNotAMS ) -> 10000 ProjectileDefinition.CalculateDerivedFields(jammer_grenade_projectile_enh) - jammer_grenade_projectile_enh.Modifiers = Nil + jammer_grenade_projectile_enh.Modifiers = DamageModifiers.MaxDistanceCutoff katana_projectile.Name = "katana_projectile" katana_projectile.Damage0 = 25 @@ -3014,7 +3085,10 @@ object GlobalDefinitions { lasher_projectile.LashRadius = 2.5f lasher_projectile.Lifespan = 0.75f ProjectileDefinition.CalculateDerivedFields(lasher_projectile) - lasher_projectile.Modifiers = List(DamageModifiers.DistanceDegrade, DamageModifiers.Lash) + lasher_projectile.Modifiers = List( + DamageModifiers.DistanceDegrade, + DamageModifiers.Lash + ) lasher_projectile_ap.Name = "lasher_projectile_ap" lasher_projectile_ap.Damage0 = 12 @@ -3029,7 +3103,10 @@ object GlobalDefinitions { lasher_projectile_ap.LashRadius = 2.5f lasher_projectile_ap.Lifespan = 0.75f ProjectileDefinition.CalculateDerivedFields(lasher_projectile_ap) - lasher_projectile_ap.Modifiers = List(DamageModifiers.DistanceDegrade, DamageModifiers.Lash) + lasher_projectile_ap.Modifiers = List( + DamageModifiers.DistanceDegrade, + DamageModifiers.Lash + ) liberator_bomb_cluster_bomblet_projectile.Name = "liberator_bomb_cluster_bomblet_projectile" liberator_bomb_cluster_bomblet_projectile.Damage0 = 75 @@ -3071,6 +3148,7 @@ object GlobalDefinitions { maelstrom_grenade_projectile.Damage1 = 60 maelstrom_grenade_projectile.DamageRadius = 20f maelstrom_grenade_projectile.LashRadius = 5f + maelstrom_grenade_projectile.GrenadeProjectile = true maelstrom_grenade_projectile.ProjectileDamageType = DamageType.Direct maelstrom_grenade_projectile.InitialVelocity = 30 maelstrom_grenade_projectile.Lifespan = 2f @@ -3084,6 +3162,7 @@ object GlobalDefinitions { maelstrom_grenade_projectile_contact.Damage1 = 60 maelstrom_grenade_projectile_contact.DamageRadius = 20f maelstrom_grenade_projectile_contact.LashRadius = 5f + maelstrom_grenade_projectile_contact.GrenadeProjectile = true maelstrom_grenade_projectile_contact.ProjectileDamageType = DamageType.Direct maelstrom_grenade_projectile_contact.InitialVelocity = 30 maelstrom_grenade_projectile_contact.Lifespan = 15f @@ -3100,6 +3179,7 @@ object GlobalDefinitions { maelstrom_stream_projectile.InitialVelocity = 200 maelstrom_stream_projectile.Lifespan = 0.2f ProjectileDefinition.CalculateDerivedFields(maelstrom_stream_projectile) + maelstrom_stream_projectile.Modifiers = DamageModifiers.MaxDistanceCutoff magcutter_projectile.Name = "magcutter_projectile" // TODO for later, maybe : set_resource_parent magcutter_projectile game_objects melee_ammo_projectile @@ -3109,6 +3189,7 @@ object GlobalDefinitions { magcutter_projectile.InitialVelocity = 100 magcutter_projectile.Lifespan = .02f ProjectileDefinition.CalculateDerivedFields(magcutter_projectile) + magcutter_projectile.Modifiers = DamageModifiers.MaxDistanceCutoff melee_ammo_projectile.Name = "melee_ammo_projectile" melee_ammo_projectile.Damage0 = 25 @@ -3117,6 +3198,7 @@ object GlobalDefinitions { melee_ammo_projectile.InitialVelocity = 100 melee_ammo_projectile.Lifespan = .02f ProjectileDefinition.CalculateDerivedFields(melee_ammo_projectile) + melee_ammo_projectile.Modifiers = DamageModifiers.MaxDistanceCutoff meteor_common.Name = "meteor_common" meteor_common.DamageAtEdge = .1f @@ -3208,6 +3290,7 @@ object GlobalDefinitions { mine_sweeper_projectile.Damage1 = 0 mine_sweeper_projectile.DamageAtEdge = .33f mine_sweeper_projectile.DamageRadius = 25f + mine_sweeper_projectile.GrenadeProjectile = true mine_sweeper_projectile.ProjectileDamageType = DamageType.Splash mine_sweeper_projectile.InitialVelocity = 30 mine_sweeper_projectile.Lifespan = 15f @@ -3219,6 +3302,7 @@ object GlobalDefinitions { mine_sweeper_projectile_enh.Damage1 = 0 mine_sweeper_projectile_enh.DamageAtEdge = 0.33f mine_sweeper_projectile_enh.DamageRadius = 25f + mine_sweeper_projectile_enh.GrenadeProjectile = true mine_sweeper_projectile_enh.ProjectileDamageType = DamageType.Splash mine_sweeper_projectile_enh.InitialVelocity = 30 mine_sweeper_projectile_enh.Lifespan = 3f @@ -3422,10 +3506,27 @@ object GlobalDefinitions { plasma_cartridge_projectile.Damage1 = 15 plasma_cartridge_projectile.DamageAtEdge = 0.2f plasma_cartridge_projectile.DamageRadius = 7f + plasma_cartridge_projectile.GrenadeProjectile = true plasma_cartridge_projectile.ProjectileDamageType = DamageType.Aggravated + plasma_cartridge_projectile.Aggravated = AggravatedDamage( + List(AggravatedInfo(DamageType.Direct, 0.25f, 750), AggravatedInfo(DamageType.Splash, 0.25f, 1000)), + Aura.Plasma, + AggravatedTiming(3000), + 1.5f, + true, + false, + List(TargetValidation(EffectTarget.Category.Player, EffectTarget.Validation.Player)) + ) plasma_cartridge_projectile.InitialVelocity = 30 plasma_cartridge_projectile.Lifespan = 15f ProjectileDefinition.CalculateDerivedFields(plasma_cartridge_projectile) + plasma_cartridge_projectile.Modifiers = List( + DamageModifiers.InfantryAggravatedDirect, + DamageModifiers.InfantryAggravatedDirectBurn, + DamageModifiers.InfantryAggravatedSplash, + DamageModifiers.InfantryAggravatedSplashBurn, + DamageModifiers.RadialDegrade + ) plasma_cartridge_projectile_b.Name = "plasma_cartridge_projectile_b" // TODO for later, maybe : set_resource_parent plasma_cartridge_projectile_b game_objects plasma_grenade_projectile_B @@ -3433,20 +3534,54 @@ object GlobalDefinitions { plasma_cartridge_projectile_b.Damage1 = 15 plasma_cartridge_projectile_b.DamageAtEdge = 0.2f plasma_cartridge_projectile_b.DamageRadius = 7f + plasma_cartridge_projectile_b.GrenadeProjectile = true plasma_cartridge_projectile_b.ProjectileDamageType = DamageType.Aggravated + plasma_cartridge_projectile_b.Aggravated = AggravatedDamage( + List(AggravatedInfo(DamageType.Direct, 0.25f, 750), AggravatedInfo(DamageType.Splash, 0.25f, 1000)), + Aura.Plasma, + AggravatedTiming(3000), + 1.5f, + true, + false, + List(TargetValidation(EffectTarget.Category.Player, EffectTarget.Validation.Player)) + ) plasma_cartridge_projectile_b.InitialVelocity = 30 plasma_cartridge_projectile_b.Lifespan = 2f ProjectileDefinition.CalculateDerivedFields(plasma_cartridge_projectile_b) + plasma_cartridge_projectile_b.Modifiers = List( + DamageModifiers.InfantryAggravatedDirect, + DamageModifiers.InfantryAggravatedDirectBurn, + DamageModifiers.InfantryAggravatedSplash, + DamageModifiers.InfantryAggravatedSplashBurn, + DamageModifiers.RadialDegrade + ) plasma_grenade_projectile.Name = "plasma_grenade_projectile" plasma_grenade_projectile.Damage0 = 40 plasma_grenade_projectile.Damage1 = 30 plasma_grenade_projectile.DamageAtEdge = 0.1f plasma_grenade_projectile.DamageRadius = 7f + plasma_grenade_projectile.GrenadeProjectile = true plasma_grenade_projectile.ProjectileDamageType = DamageType.Aggravated + plasma_grenade_projectile.Aggravated = AggravatedDamage( + List(AggravatedInfo(DamageType.Direct, 0.25f, 750), AggravatedInfo(DamageType.Splash, 0.25f, 1000)), + Aura.Plasma, + AggravatedTiming(3000), + 1.5f, + true, + false, + List(TargetValidation(EffectTarget.Category.Player, EffectTarget.Validation.Player)) + ) plasma_grenade_projectile.InitialVelocity = 30 plasma_grenade_projectile.Lifespan = 15f ProjectileDefinition.CalculateDerivedFields(plasma_grenade_projectile) + plasma_grenade_projectile.Modifiers = List( + DamageModifiers.InfantryAggravatedDirect, + DamageModifiers.InfantryAggravatedDirectBurn, + DamageModifiers.InfantryAggravatedSplash, + DamageModifiers.InfantryAggravatedSplashBurn, + DamageModifiers.RadialDegrade + ) plasma_grenade_projectile_B.Name = "plasma_grenade_projectile_B" // TODO for later, maybe : set_resource_parent plasma_grenade_projectile_B game_objects plasma_grenade_projectile @@ -3454,10 +3589,27 @@ object GlobalDefinitions { plasma_grenade_projectile_B.Damage1 = 30 plasma_grenade_projectile_B.DamageAtEdge = 0.1f plasma_grenade_projectile_B.DamageRadius = 7f + plasma_grenade_projectile_B.GrenadeProjectile = true plasma_grenade_projectile_B.ProjectileDamageType = DamageType.Aggravated + plasma_grenade_projectile_B.Aggravated = AggravatedDamage( + List(AggravatedInfo(DamageType.Direct, 0.25f, 750), AggravatedInfo(DamageType.Splash, 0.25f, 1000)), + Aura.Plasma, + AggravatedTiming(3000), + 1.5f, + true, + false, + List(TargetValidation(EffectTarget.Category.Player, EffectTarget.Validation.Player)) + ) plasma_grenade_projectile_B.InitialVelocity = 30 plasma_grenade_projectile_B.Lifespan = 3f ProjectileDefinition.CalculateDerivedFields(plasma_grenade_projectile_B) + plasma_grenade_projectile_B.Modifiers = List( + DamageModifiers.InfantryAggravatedDirect, + DamageModifiers.InfantryAggravatedDirectBurn, + DamageModifiers.InfantryAggravatedSplash, + DamageModifiers.InfantryAggravatedSplashBurn, + DamageModifiers.RadialDegrade + ) pounder_projectile.Name = "pounder_projectile" pounder_projectile.Damage0 = 31 @@ -3467,6 +3619,7 @@ object GlobalDefinitions { pounder_projectile.Damage4 = 132 pounder_projectile.DamageAtEdge = 0.1f pounder_projectile.DamageRadius = 1f + pounder_projectile.GrenadeProjectile = true pounder_projectile.ProjectileDamageType = DamageType.Splash pounder_projectile.InitialVelocity = 120 pounder_projectile.Lifespan = 2.5f @@ -3482,6 +3635,7 @@ object GlobalDefinitions { pounder_projectile_enh.Damage4 = 132 pounder_projectile_enh.DamageAtEdge = 0.1f pounder_projectile_enh.DamageRadius = 1f + pounder_projectile_enh.GrenadeProjectile = true pounder_projectile_enh.ProjectileDamageType = DamageType.Splash pounder_projectile_enh.InitialVelocity = 120 pounder_projectile_enh.Lifespan = 3.2f @@ -3533,6 +3687,7 @@ object GlobalDefinitions { ProjectileDefinition.CalculateDerivedFields(quasar_projectile) 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 radiator_grenade_projectile.InitialVelocity = 30 radiator_grenade_projectile.Lifespan = 3f @@ -3540,6 +3695,7 @@ object GlobalDefinitions { radiator_sticky_projectile.Name = "radiator_sticky_projectile" // TODO for later, maybe : set_resource_parent radiator_sticky_projectile game_objects radiator_grenade_projectile + radiator_sticky_projectile.GrenadeProjectile = true //not really, but technically yes radiator_sticky_projectile.ProjectileDamageType = DamageType.Direct radiator_sticky_projectile.InitialVelocity = 30 radiator_sticky_projectile.Lifespan = 4f @@ -3602,7 +3758,7 @@ object GlobalDefinitions { rocklet_jammer_projectile.InitialVelocity = 50 rocklet_jammer_projectile.Lifespan = 8f ProjectileDefinition.CalculateDerivedFields(rocklet_jammer_projectile) - rocklet_jammer_projectile.Modifiers = Nil + //TODO rocklet_jammer_projectile.Modifiers = DamageModifiers.RadialDegrade? scattercannon_projectile.Name = "scattercannon_projectile" scattercannon_projectile.Damage0 = 11 @@ -3722,7 +3878,6 @@ object GlobalDefinitions { spiker_projectile.InitialVelocity = 40 spiker_projectile.Lifespan = 5f ProjectileDefinition.CalculateDerivedFields(spiker_projectile) - spiker_projectile.Modifiers = DamageModifiers.RadialDegrade spitfire_aa_ammo_projectile.Name = "spitfire_aa_ammo_projectile" spitfire_aa_ammo_projectile.Damage0 = 5 @@ -3756,6 +3911,18 @@ object GlobalDefinitions { starfire_projectile.Acceleration = 12 starfire_projectile.AccelerationUntil = 5f starfire_projectile.ProjectileDamageType = DamageType.Aggravated + starfire_projectile.Aggravated = AggravatedDamage( + AggravatedInfo(DamageType.Direct, 0.25f, 250), + Aura.Comet, + 2000, + 0f, + true, + List( + TargetValidation(EffectTarget.Category.Player, EffectTarget.Validation.Player), + TargetValidation(EffectTarget.Category.Vehicle, EffectTarget.Validation.Vehicle), + TargetValidation(EffectTarget.Category.Turret, EffectTarget.Validation.Turret) + ) + ) starfire_projectile.InitialVelocity = 45 starfire_projectile.Lifespan = 7.8f starfire_projectile.ExistsOnRemoteClients = true @@ -3763,6 +3930,10 @@ object GlobalDefinitions { starfire_projectile.AutoLock = true starfire_projectile.Packet = projectileConverter ProjectileDefinition.CalculateDerivedFields(starfire_projectile) + starfire_projectile.Modifiers = List( + DamageModifiers.StarfireAggravatedBurn, + DamageModifiers.RadialDegrade + ) striker_missile_projectile.Name = "striker_missile_projectile" striker_missile_projectile.Damage0 = 35 @@ -3813,6 +3984,7 @@ object GlobalDefinitions { trek_projectile.InitialVelocity = 40 trek_projectile.Lifespan = 7f ProjectileDefinition.CalculateDerivedFields(trek_projectile) + trek_projectile.Modifiers = DamageModifiers.MaxDistanceCutoff vanu_sentry_turret_projectile.Name = "vanu_sentry_turret_projectile" vanu_sentry_turret_projectile.Damage0 = 25 @@ -4491,7 +4663,7 @@ object GlobalDefinitions { flamethrower.FireModes(1).AmmoSlotIndex = 0 flamethrower.FireModes(1).Magazine = 100 flamethrower.FireModes(1).RoundsPerShot = 50 - flamethrower.Tile = InventoryTile.Tile63 + flamethrower.Tile = InventoryTile.Tile93 winchester.Name = "winchester" winchester.Size = EquipmentSize.Rifle diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index bd79347f..d7585f01 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -1,12 +1,20 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.avatar.{Avatar, LoadoutManager} -import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition} +import net.psforever.objects.avatar.{ + Avatar, + LoadoutManager +} +import net.psforever.objects.definition.{ + AvatarDefinition, + ExoSuitDefinition, + SpecialExoSuitDefinition +} import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.aura.AuraContainer import net.psforever.objects.vital.resistance.ResistanceProfile import net.psforever.objects.vital.{DamageResistanceModel, Vitality} import net.psforever.objects.zones.ZoneAware @@ -22,7 +30,8 @@ class Player(var avatar: Avatar) with ResistanceProfile with Container with JammableUnit - with ZoneAware { + with ZoneAware + with AuraContainer { Health = 0 //player health is artificially managed as a part of their lifecycle; start entity as dead Destroyed = true //see isAlive private var backpack: Boolean = false diff --git a/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala b/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala index feb1f129..75a375ec 100644 --- a/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala +++ b/src/main/scala/net/psforever/objects/ShieldGeneratorDeployable.scala @@ -44,7 +44,6 @@ class ShieldGeneratorControl(gen: ShieldGeneratorDeployable) def JammableObject = gen def DamageableObject = gen def RepairableObject = gen - private var handleDamageToShields: Boolean = false def receive: Receive = jammableBehavior @@ -90,18 +89,20 @@ class ShieldGeneratorControl(gen: ShieldGeneratorDeployable) target, s"BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields" ) - handleDamageToShields = damageToShields > 0 - HandleDamage(target, cause, damageToHealth) + HandleDamage(target, cause, (damageToHealth, damageToShields)) } else { gen.Health = originalHealth gen.Shields = originalShields } } - override protected def DamageAwareness(target: Damageable.Target, cause: ResolvedProjectile, amount: Int): Unit = { - super.DamageAwareness(target, cause, amount) - ShieldGeneratorControl.DamageAwareness(gen, cause, handleDamageToShields) - handleDamageToShields = false + override protected def DamageAwareness(target: Damageable.Target, cause: ResolvedProjectile, amount: Any): Unit = { + val (damageToHealth, damageToShields) = amount match { + case (a: Int, b: Int) => (a, b) + case _ => (0, 0) + } + super.DamageAwareness(target, cause, damageToHealth) + ShieldGeneratorControl.DamageAwareness(gen, cause, damageToShields > 0) } override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = { diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala index 2c3b5e8d..fd9f8569 100644 --- a/src/main/scala/net/psforever/objects/TurretDeployable.scala +++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala @@ -75,6 +75,11 @@ class TurretControl(turret: TurretDeployable) def DamageableObject = turret def RepairableObject = turret + override def postStop(): Unit = { + super.postStop() + damageableWeaponTurretPostStop() + } + def receive: Receive = checkBehavior .orElse(jammableBehavior) diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala index d108fad3..b1a47202 100644 --- a/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/src/main/scala/net/psforever/objects/Vehicle.scala @@ -7,6 +7,7 @@ import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.aura.AuraContainer import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.structures.AmenityOwner @@ -78,7 +79,8 @@ class Vehicle(private val vehicleDef: VehicleDefinition) with StandardResistanceProfile with JammableUnit with CommonNtuContainer - with Container { + with Container + with AuraContainer { private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var shields: Int = 0 private var decal: Int = 0 diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 391bdf56..b47f3338 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -8,14 +8,17 @@ import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile} import net.psforever.objects.equipment._ import net.psforever.objects.inventory.{GridInventory, InventoryItem} import net.psforever.objects.loadouts.Loadout +import net.psforever.objects.serverobject.aura.{Aura, AuraEffectBehavior} import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior} -import net.psforever.objects.vital.{PlayerSuicide, Vitality} +import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.vital.PlayerSuicide import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} -import net.psforever.objects.serverobject.damage.Damageable +import net.psforever.objects.serverobject.damage.{AggravatedBehavior, Damageable} import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.repair.Repairable import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.vital._ +import net.psforever.objects.vital.resolution.ResolutionCalculations.Output import net.psforever.objects.zones.Zone import net.psforever.packet.game._ import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent @@ -30,13 +33,24 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm extends Actor with JammableBehavior with Damageable - with ContainableBehavior { - def JammableObject = player + with ContainableBehavior + with AggravatedBehavior + with AuraEffectBehavior { + + def JammableObject = player def DamageableObject = player def ContainerObject = player + def AggravatedObject = player + ApplicableEffect(Aura.Plasma) + ApplicableEffect(Aura.Napalm) + ApplicableEffect(Aura.Comet) + ApplicableEffect(Aura.Fire) + + def AuraTargetObject = player + private[this] val log = org.log4s.getLogger(player.Name) private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel) @@ -53,11 +67,15 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm override def postStop(): Unit = { lockerControlAgent ! akka.actor.PoisonPill player.avatar.locker.Actor = Default.Actor + EndAllEffects() + EndAllAggravation() } def receive: Receive = jammableBehavior .orElse(takesDamage) + .orElse(aggravatedBehavior) + .orElse(auraBehavior) .orElse(containerBehavior) .orElse { case Player.Die() => @@ -480,29 +498,31 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm case _ => ; } - protected def TakesDamage: Receive = { - case Vitality.Damage(applyDamageTo) => - if (player.isAlive && !player.spectator) { - val originalHealth = player.Health - val originalArmor = player.Armor - val originalStamina = player.avatar.stamina - val originalCapacitor = player.Capacitor.toInt - val cause = applyDamageTo(player) - val health = player.Health - val armor = player.Armor - val stamina = player.avatar.stamina - val capacitor = player.Capacitor.toInt - val damageToHealth = originalHealth - health - val damageToArmor = originalArmor - armor - val damageToStamina = originalStamina - stamina - val damageToCapacitor = originalCapacitor - capacitor - HandleDamage(player, cause, damageToHealth, damageToArmor, damageToStamina, damageToCapacitor) - if (damageToHealth > 0 || damageToArmor > 0 || damageToStamina > 0 || damageToCapacitor > 0) { - damageLog.info( - s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalStamina/$originalCapacitor, AFTER=$health/$armor/$stamina/$capacitor, CHANGE=$damageToHealth/$damageToArmor/$damageToStamina/$damageToCapacitor" - ) - } + override protected def PerformDamage( + target: Target, + applyDamageTo: Output + ): Unit = { + if (player.isAlive && !player.spectator) { + val originalHealth = player.Health + val originalArmor = player.Armor + val originalStamina = player.avatar.stamina + val originalCapacitor = player.Capacitor.toInt + val cause = applyDamageTo(player) + val health = player.Health + val armor = player.Armor + val stamina = player.avatar.stamina + val capacitor = player.Capacitor.toInt + val damageToHealth = originalHealth - health + val damageToArmor = originalArmor - armor + val damageToStamina = originalStamina - stamina + val damageToCapacitor = originalCapacitor - capacitor + HandleDamage(player, cause, damageToHealth, damageToArmor, damageToStamina, damageToCapacitor) + if (damageToHealth > 0 || damageToArmor > 0 || damageToStamina > 0 || damageToCapacitor > 0) { + damageLog.info( + s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalStamina/$originalCapacitor, AFTER=$health/$armor/$stamina/$capacitor, CHANGE=$damageToHealth/$damageToArmor/$damageToStamina/$damageToCapacitor" + ) } + } } /** @@ -510,70 +530,110 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm * @param target na */ def HandleDamage( - target: Player, - cause: ResolvedProjectile, - damageToHealth: Int, - damageToArmor: Int, - damageToStamina: Int, - damageToCapacitor: Int - ): Unit = { - val targetGUID = target.GUID - val zone = target.Zone - val zoneId = zone.id - val events = zone.AvatarEvents - val health = target.Health + target: Player, + cause: ResolvedProjectile, + damageToHealth: Int, + damageToArmor: Int, + damageToStamina: Int, + damageToCapacitor: Int + ): Unit = { + //always do armor update if (damageToArmor > 0) { - events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 4, target.Armor)) + val zone = target.Zone + zone.AvatarEvents ! AvatarServiceMessage( + zone.id, + AvatarAction.PlanetsideAttributeToAll(target.GUID, 4, target.Armor) + ) } - if (health > 0) { - if (damageToCapacitor > 0) { - events ! AvatarServiceMessage( - target.Name, - AvatarAction.PlanetsideAttributeSelf(targetGUID, 7, target.Capacitor.toLong) - ) - } - if (damageToHealth > 0 || damageToStamina > 0) { - target.History(cause) - if (damageToHealth > 0) { - events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 0, health)) - } - if (damageToStamina > 0) { - avatarActor ! AvatarActor.ConsumeStamina(damageToStamina) - } - //activity on map - zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) - //alert damage source - DamageAwareness(target, cause) - } - if (Damageable.CanJammer(target, cause)) { - target.Actor ! JammableUnit.Jammered(cause) - } + //choose + if (target.Health > 0) { + DamageAwareness(target, cause, damageToHealth, damageToArmor, damageToStamina, damageToCapacitor) } else { DestructionAwareness(target, Some(cause)) } } - /** - * na - * @param target na - * @param cause na - */ - def DamageAwareness(target: Player, cause: ResolvedProjectile): Unit = { - val zone = target.Zone - zone.AvatarEvents ! AvatarServiceMessage( - target.Name, - cause.projectile.owner match { - case pSource: PlayerSource => //player damage - val name = pSource.Name - zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match { - case Some(tplayer) => AvatarAction.HitHint(tplayer.GUID, target.GUID) - case None => - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, pSource.Position)) + def DamageAwareness( + target: Player, + cause: ResolvedProjectile, + damageToHealth: Int, + damageToArmor: Int, + damageToStamina: Int, + damageToCapacitor: Int + ): Unit = { + val targetGUID = target.GUID + val zone = target.Zone + val zoneId = zone.id + val events = zone.AvatarEvents + val health = target.Health + var announceConfrontation = damageToArmor > 0 + //special effects + if (Damageable.CanJammer(target, cause)) { + TryJammerEffectActivate(target, cause) + } + val aggravated: Boolean = TryAggravationEffectActivate(cause) match { + case Some(aggravation) => + StartAuraEffect(aggravation.effect_type, aggravation.timing.duration) + announceConfrontation = true //useful if initial damage (to anything) is zero + //initial damage for aggravation, but never treat as "aggravated" + false + case _ => + cause.projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated) + } + //log historical event + target.History(cause) + //stat changes + if (damageToCapacitor > 0) { + events ! AvatarServiceMessage( + target.Name, + AvatarAction.PlanetsideAttributeSelf(targetGUID, 7, target.Capacitor.toLong) + ) + announceConfrontation = true + } + if (damageToStamina > 0) { + avatarActor ! AvatarActor.ConsumeStamina(damageToStamina) + announceConfrontation = true + } + if (damageToHealth > 0) { + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 0, health)) + announceConfrontation = true + } + val countableDamage = damageToHealth + damageToArmor + if(announceConfrontation) { + if (!aggravated) { + //activity on map + zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) + //alert to damage source + zone.AvatarEvents ! AvatarServiceMessage( + target.Name, + cause.projectile.owner match { + case pSource: PlayerSource => //player damage + val name = pSource.Name + zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match { + case Some(tplayer) => + AvatarAction.HitHint(tplayer.GUID, target.GUID) + case None => + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(countableDamage, pSource.Position)) + } + case source => + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(countableDamage, source.Position)) } - case source => - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, source.Position)) + ) } - ) + else { + //general alert + zone.AvatarEvents ! AvatarServiceMessage( + target.Name, + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(countableDamage, Vector3.Zero)) + ) + } + } + if (aggravated) { + events ! AvatarServiceMessage( + zoneId, + AvatarAction.SendResponse(Service.defaultPlayerGUID, AggravatedDamageMessage(targetGUID, countableDamage)) + ) + } } /** @@ -600,6 +660,10 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm val nameChannel = target.Name val zoneChannel = zone.id target.Die + //aura effects cancel + EndAllEffects() + //aggravation cancel + EndAllAggravation() //unjam CancelJammeredSound(target) CancelJammeredStatus(target) @@ -864,4 +928,28 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item.GUID) ) } + + def UpdateAuraEffect(target: AuraEffectBehavior.Target) : Unit = { + import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} + val zone = target.Zone + val value = target.Aura.foldLeft(0)(_ + PlayerControl.auraEffectToAttributeValue(_)) + zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttributeToAll(target.GUID, 54, value)) + } +} + +object PlayerControl { + /** + * Transform an applicable Aura effect into its `PlanetsideAttributeMessage` value. + * @see `Aura` + * @see `PlanetsideAttributeMessage` + * @param effect the aura effect + * @return the attribute value for that effect + */ + private def auraEffectToAttributeValue(effect: Aura): Int = effect match { + case Aura.Plasma => 1 + case Aura.Comet => 2 + case Aura.Napalm => 4 + case Aura.Fire => 8 + case _ => 0 + } } diff --git a/src/main/scala/net/psforever/objects/ballistics/AggravatedDamage.scala b/src/main/scala/net/psforever/objects/ballistics/AggravatedDamage.scala new file mode 100644 index 00000000..91c9c3c2 --- /dev/null +++ b/src/main/scala/net/psforever/objects/ballistics/AggravatedDamage.scala @@ -0,0 +1,178 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.ballistics +import net.psforever.objects.equipment.TargetValidation +import net.psforever.objects.serverobject.aura.Aura +import net.psforever.objects.vital.DamageType + +/** + * In what manner of pacing the aggravated damage ticks are applied. + * @param duration for how long the over-all effect is applied + * @param ticks a custom number of damage applications, + * as opposed to whatever calculations normally estimate the number of applications + */ +final case class AggravatedTiming(duration: Long, ticks: Option[Int]) + +object AggravatedTiming { + /** + * Overloaded constructor that only defines the duration. + * @param duration for how long the over-all effect lasts + * @return an `AggravatedTiming` object + */ + def apply(duration: Long): AggravatedTiming = AggravatedTiming(duration, None) + + /** + * Overloaded constructor. + * @param duration for how long the over-all effect lasts + * @param ticks a custom number of damage applications + * @return an `AggravatedTiming` object + */ + def apply(duration: Long, ticks: Int): AggravatedTiming = AggravatedTiming(duration, Some(ticks)) +} + +/** + * Aggravation damage has components that are mainly divided by the `DamageType` they inflict. + * Only `Direct` and `Splash` are valid damage types, however. + * @param damage_type the type of damage + * @param degradation_percentage by how much the damage is degraded + * @param infliction_rate how often the damage is inflicted (ms) + */ +final case class AggravatedInfo(damage_type: DamageType.Value, + degradation_percentage: Float, + infliction_rate: Long) { + assert(damage_type == DamageType.Direct || damage_type == DamageType.Splash, s"aggravated damage is an unsupported type - $damage_type") +} + +/** + * Information related to the aggravated damage. + * @param info the specific kinds of aggravation damage available + * @param effect_type what effect is exhibited by this aggravated damage + * @param timing the timing for the damage application + * @param max_factor na (if the target is a mechanized assault exo-suit?) + * @param cumulative_damage_degrade na (can multiple instances of this type of aggravated damage apply to the same target at once?) + * @param vanu_aggravated na (search me) + * @param targets validation information indicating whether a certain entity is applicable for aggravation + */ +final case class AggravatedDamage(info: List[AggravatedInfo], + effect_type: Aura, + timing: AggravatedTiming, + max_factor: Float, + cumulative_damage_degrade: Boolean, + vanu_aggravated: Boolean, + targets: List[TargetValidation]) + +object AggravatedDamage { + /** + * Overloaded constructor. + * @param info the specific kinds of aggravation damage available + * @param effect_type what effect is exhibited by this aggravated damage + * @param timing the timing for the damage application + * @param max_factor na + * @param targets validation information indicating whether a certain entity is applicable for aggravation + */ + def apply(info: AggravatedInfo, + effect_type: Aura, + timing: AggravatedTiming, + max_factor: Float, + targets: List[TargetValidation]): AggravatedDamage = + AggravatedDamage( + List(info), + effect_type, + timing, + max_factor, + cumulative_damage_degrade = true, + vanu_aggravated = false, + targets + ) + + /** + * Overloaded constructor. + * @param info the specific kinds of aggravation damage available + * @param effect_type what effect is exhibited by this aggravated damage + * @param timing the timing for the damage application + * @param max_factor na + * @param vanu_aggravated na + * @param targets validation information indicating whether a certain entity is applicable for aggravation + */ + def apply(info: AggravatedInfo, + effect_type: Aura, + timing: AggravatedTiming, + max_factor: Float, + vanu_aggravated: Boolean, + targets: List[TargetValidation]): AggravatedDamage = + AggravatedDamage( + List(info), + effect_type, + timing, + max_factor, + cumulative_damage_degrade = true, + vanu_aggravated, + targets + ) + + /** + * Overloaded constructor. + * @param info the specific kinds of aggravation damage available + * @param effect_type what effect is exhibited by this aggravated damage + * @param duration for how long the over-all effect is applied + * @param max_factor na + * @param targets validation information indicating whether a certain entity is applicable for aggravation + */ + def apply(info: AggravatedInfo, + effect_type: Aura, + duration: Long, + max_factor: Float, + targets: List[TargetValidation]): AggravatedDamage = + AggravatedDamage( + List(info), + effect_type, + AggravatedTiming(duration), + max_factor, + cumulative_damage_degrade = true, + vanu_aggravated = false, + targets + ) + + /** + * Overloaded constructor. + * @param info the specific kinds of aggravation damage available + * @param effect_type what effect is exhibited by this aggravated damage + * @param duration for how long the over-all effect is applied + * @param max_factor na + * @param vanu_aggravated na + * @param targets validation information indicating whether a certain entity is applicable for aggravation + */ + def apply(info: AggravatedInfo, + effect_type: Aura, + duration: Long, + max_factor: Float, + vanu_aggravated: Boolean, + targets: List[TargetValidation]): AggravatedDamage = + AggravatedDamage( + List(info), + effect_type, + AggravatedTiming(duration), + max_factor, + cumulative_damage_degrade = true, + vanu_aggravated, + targets + ) + + def burning(resolution: ProjectileResolution.Value): ProjectileResolution.Value = { + resolution match { + case ProjectileResolution.AggravatedDirect => ProjectileResolution.AggravatedDirectBurn + case ProjectileResolution.AggravatedSplash => ProjectileResolution.AggravatedSplashBurn + case _ => resolution + } + } + + def basicDamageType(resolution: ProjectileResolution.Value): DamageType.Value = { + resolution match { + case ProjectileResolution.AggravatedDirect | ProjectileResolution.AggravatedDirectBurn => + DamageType.Direct + case ProjectileResolution.AggravatedSplash | ProjectileResolution.AggravatedSplashBurn => + DamageType.Splash + case _ => + DamageType.None + } + } +} diff --git a/src/main/scala/net/psforever/objects/ballistics/Projectile.scala b/src/main/scala/net/psforever/objects/ballistics/Projectile.scala index 05705de3..a00b79e4 100644 --- a/src/main/scala/net/psforever/objects/ballistics/Projectile.scala +++ b/src/main/scala/net/psforever/objects/ballistics/Projectile.scala @@ -1,6 +1,8 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.ballistics +import java.util.concurrent.atomic.AtomicLong + import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.definition.{ProjectileDefinition, ToolDefinition} import net.psforever.objects.entity.SimpleWorldEntity @@ -10,11 +12,11 @@ import net.psforever.types.Vector3 /** * A summation of weapon (`Tool`) discharge. - * @see `ProjectileDefinition`
- * `ToolDefinition`
- * `FireModeDefinition`
- * `SourceEntry`
- * `PlayerSource` + * @see `ProjectileDefinition` + * @see `ToolDefinition` + * @see `FireModeDefinition` + * @see `SourceEntry` + * @see `PlayerSource` * @param profile an explanation of the damage that can be performed by this discharge * @param tool_def the weapon that caused this discharge * @param fire_mode the current fire mode of the tool used @@ -25,6 +27,9 @@ import net.psforever.types.Vector3 * if not, then it is a type of vehicle (and owner should have a positive `seated` field) * @param shot_origin where the projectile started * @param shot_angle in which direction the projectile was aimed when it was discharged + * @param quality na + * @param id an exclusive identifier for this projectile; + * normally generated internally, but can be manually set * @param fire_time when the weapon discharged was recorded; * defaults to `System.nanoTime` */ @@ -36,6 +41,8 @@ final case class Projectile( attribute_to: Int, shot_origin: Vector3, shot_angle: Vector3, + quality: ProjectileQuality = ProjectileQuality.Normal, + id: Long = Projectile.idGenerator.getAndIncrement(), fire_time: Long = System.nanoTime ) extends PlanetSideGameObject { Position = shot_origin @@ -52,6 +59,32 @@ final case class Projectile( val current: SimpleWorldEntity = new SimpleWorldEntity() private var resolved: ProjectileResolution.Value = ProjectileResolution.Unresolved + /** + * Create a copy of this projectile with all the same information + * save for the quality. + * Used mainly for aggravated damage. + * It is important to note that the new projectile shares the (otherwise) exclusive id of the original. + * @param value the new quality + * @return a new `Projectile` entity + */ + def quality(value: ProjectileQuality): Projectile = { + val projectile = Projectile( + profile, + tool_def, + fire_mode, + owner, + attribute_to, + shot_origin, + shot_angle, + value, + id, + fire_time + ) + if(isMiss) projectile.Miss() + else if(isResolved) projectile.Resolve() + projectile + } + /** * Mark the projectile as being "encountered" or "managed" at least once. */ @@ -80,6 +113,8 @@ object Projectile { */ final val rangeUID: Int = 40150 + private val idGenerator: AtomicLong = new AtomicLong + /** * Overloaded constructor for an `Unresolved` projectile. * @param profile an explanation of the damage that can be performed by this discharge diff --git a/src/main/scala/net/psforever/objects/ballistics/ProjectileQuality.scala b/src/main/scala/net/psforever/objects/ballistics/ProjectileQuality.scala new file mode 100644 index 00000000..9a630845 --- /dev/null +++ b/src/main/scala/net/psforever/objects/ballistics/ProjectileQuality.scala @@ -0,0 +1,36 @@ +//Copyright (c) 2020 PSForever +package net.psforever.objects.ballistics + +/** + * Projectile quality is an external aspect of projectiles + * that is not dependent on hard-coded definitions of the entities + * used to compose the projectile such as the knowlegde of the emitting `Tool` (weapon). + * A flag or a damage modifier, depending on use. + * To the extent that it can be used as a numeric modifier, + * insists on defining a numeric modifier component rather to what it is trying to express. + * That numeric modifier does not have to be used for anything. + */ +sealed trait ProjectileQuality { + def mod: Float +} + +/** + * Implement the numeric modifier with the value as one. + */ +sealed trait SameAsQuality extends ProjectileQuality { + def mod: Float = 1f +} + +object ProjectileQuality { + /** Standard projectile quality. More of a flag than a modifier. */ + case object Normal extends SameAsQuality + + /** Quality that flags the first stage of aggravation (initial damage). */ + case object AggravatesTarget extends SameAsQuality + + /** The complete lack of quality. Even the numeric modifier is zeroed. */ + case object Zeroed extends ProjectileQuality { def mod = 0f } + + /** Assign a custom numeric qualifier value, usually to be applied to damage calculations. */ + case class Modified(mod: Float) extends ProjectileQuality +} diff --git a/src/main/scala/net/psforever/objects/ballistics/ProjectileResolution.scala b/src/main/scala/net/psforever/objects/ballistics/ProjectileResolution.scala index 87b56805..09b2e3b3 100644 --- a/src/main/scala/net/psforever/objects/ballistics/ProjectileResolution.scala +++ b/src/main/scala/net/psforever/objects/ballistics/ProjectileResolution.scala @@ -2,16 +2,32 @@ package net.psforever.objects.ballistics /** - * An `Enumeration` of outcomes regarding what actually happened to the projectile. + * An `Enumeration` of outcomes regarding what actually happened to the projectile, + * complementing normal damage type distinction in directing damage calculations.
+ *
+ * Although the latter states reflect what sort of damage the projectile might perform - `Hit`, `Splash`, etc. - + * the state is more a communication about how that damage is interpreted by the server. + * For example, some projectiles: + * perform `Direct` damage, are reported by `HitMessage` packets, and resolve as `Hit`; + * or, perform `Direct` damage, are reported by `LashDamage` packets, and resolve as `Lash`. + * Furthermore, some projectiles: + * perform `Splash` damage, are reported by `SplashHitMessage` packets, and resolve as `Splash`; + * or, perform `Aggravated` damage, are reported by `SplashHitMessage` packets + * and resolve either as `AggravatedDirect` or as `AggravatedSplash`. */ object ProjectileResolution extends Enumeration { type Type = Value - val Unresolved, //original basic non-resolution - MissedShot, //projectile did not encounter any collision object and was despawned - Resolved, //a general "projectile encountered something" status with a more specific resolution - Hit, //direct hit, one target - Splash, //area of effect damage, potentially multiple targets - Lash //lashing damage, potentially multiple targets + val + Unresolved, //original basic non-resolution + MissedShot, //projectile did not encounter any collision object and was despawned + Resolved, //a general "projectile encountered something" status with a more specific resolution + Hit, //direct hit, one target + Splash, //area of effect damage, potentially multiple targets + Lash, //lashing damage, potentially multiple targets + AggravatedDirect, //direct hit aggravated damage + AggravatedDirectBurn, //continuous direct hit aggravated damage + AggravatedSplash, //splashed aggravated damage + AggravatedSplashBurn //continuous splashed aggravated damage = Value } diff --git a/src/main/scala/net/psforever/objects/ballistics/ResolvedProjectile.scala b/src/main/scala/net/psforever/objects/ballistics/ResolvedProjectile.scala index a51fb03f..c06e3e16 100644 --- a/src/main/scala/net/psforever/objects/ballistics/ResolvedProjectile.scala +++ b/src/main/scala/net/psforever/objects/ballistics/ResolvedProjectile.scala @@ -9,14 +9,13 @@ import net.psforever.types.Vector3 * about the interaction of weapons discharge and a target * to the point that the original event might be reconstructed. * Reenacting the calculations of this entry should always produce the same values. - * @param resolution how the projectile hit was executed * @param projectile the original projectile * @param target what the projectile hit * @param damage_model the kind of damage model to which the `target` is/was subject * @param hit_pos where the projectile hit */ final case class ResolvedProjectile( - resolution: ProjectileResolution.Value, + resolution : ProjectileResolution.Value, projectile: Projectile, target: SourceEntry, damage_model: DamageResistanceModel, diff --git a/src/main/scala/net/psforever/objects/ce/Deployable.scala b/src/main/scala/net/psforever/objects/ce/Deployable.scala index 6448daf8..56736d35 100644 --- a/src/main/scala/net/psforever/objects/ce/Deployable.scala +++ b/src/main/scala/net/psforever/objects/ce/Deployable.scala @@ -33,7 +33,7 @@ object Deployable { def Includes(category: DeployableCategory.Value): List[DeployedItem.Value] = { (for { - (ce, cat) <- deployablesToCategories + (ce: DeployedItem.Value, cat: DeployableCategory.Value) <- deployablesToCategories if cat == category } yield ce) toList } diff --git a/src/main/scala/net/psforever/objects/definition/ProjectileDefinition.scala b/src/main/scala/net/psforever/objects/definition/ProjectileDefinition.scala index 69c6ed51..6c225fb0 100644 --- a/src/main/scala/net/psforever/objects/definition/ProjectileDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/ProjectileDefinition.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.definition -import net.psforever.objects.ballistics.Projectiles +import net.psforever.objects.ballistics.{AggravatedDamage, Projectiles} import net.psforever.objects.equipment.JammingUnit import net.psforever.objects.vital.damage.DamageModifiers import net.psforever.objects.vital.{DamageType, StandardDamageProfile} @@ -16,30 +16,68 @@ class ProjectileDefinition(objectId: Int) with JammingUnit with StandardDamageProfile with DamageModifiers { + /** ascertain that this object is a valid projectile type */ private val projectileType: Projectiles.Value = Projectiles(objectId) //let throw NoSuchElementException + /** how much faster (or slower) the projectile moves (m/s^2^) */ private var acceleration: Int = 0 + /** when the acceleration stops being applied (s) */ private var accelerationUntil: Float = 0f + /** the type of damage that the projectile causes */ private var damageType: DamageType.Value = DamageType.None + /** an auxillary type of damage that the projectile causes */ private var damageTypeSecondary: DamageType.Value = DamageType.None + /** against Infantry targets, this projectile does not do armor damage */ + private var damageToHealthOnly: Boolean = false + /** number of seconds before an airborne projectile's damage begins to degrade (s) */ private var degradeDelay: Float = 1f + /** the rate of degrade of projectile damage after the degrade delay */ private var degradeMultiplier: Float = 1f + /** the out-of-the-muzzle speed of a projectile (m/s) */ private var initialVelocity: Int = 1 + /** for how long the projectile exists (s) */ private var lifespan: Float = 1f + /** for radial damage, how much damage has been lost the further away from the impact point (m) */ private var damageAtEdge: Float = 1f + /** for radial damage, the radial distance of the explosion effect (m) */ private var damageRadius: Float = 1f + /** for lashing damage, how far away a target will be affected by the projectile (m) */ private var lashRadius : Float = 0f + /** use a specific modifier as a part of damage calculations */ private var useDamage1Subtract: Boolean = false - private var existsOnRemoteClients: Boolean = false //`true` spawns a server-managed object - private var remoteClientData: (Int, Int) = - (0, 0) //artificial values; for ObjectCreateMessage packet (oicw_little_buddy is undefined) + /** the projectile is represented by a server-side entity + * that is updated by the projectile owner + * and transmitted to all projectile observers; + * `true` spawns a server-managed object */ + private var existsOnRemoteClients: Boolean = false + /** the values used by the `ObjectCreateMessage` packet for construction of the server-managed projectile + * `0, 0` are artificial values; + * the oicw_little_buddy is undefined for these values */ + private var remoteClientData: (Int, Int) = (0, 0) + /** some other entity confers projectile damage; + * a set value should not `None` and not `0` but is preferred to be the damager's uid */ private var damageProxy: Option[Int] = None + /** this projectile follows its target, after a fashion */ private var autoLock: Boolean = false + /** na; + * currently used with jammer properties only */ private var additionalEffect: Boolean = false + /** the projectile tries to confer the jammered status effect to its target(s) */ private var jammerProjectile: Boolean = false + /** 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 + /** projectile tries to confers aggravated damage burn to its target */ + private var aggravated_damage : Option[AggravatedDamage] = None //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; + * occasionally, this value is purely mathematical as opposed to realistic, e.g., the melee weapons */ private var distanceMax: Float = 0f + /** how far the projectile will travel while accelerating (m) */ private var distanceFromAcceleration: Float = 0f + /** how far the projectile will travel while no degrading (m) */ private var distanceNoDegrade: Float = 0f + /** after acceleration, if any, what is the final speed of the projectile (m/s) */ private var finalVelocity: Float = 0f Name = "projectile" Modifiers = DamageModifiers.DistanceDegrade @@ -81,6 +119,17 @@ class ProjectileDefinition(objectId: Int) ProjectileDamageTypeSecondary } + def ProjectileDamageTypes : Set[DamageType.Value] = { + Set(damageType, damageTypeSecondary).filterNot(_ == DamageType.None) + } + + def DamageToHealthOnly : Boolean = damageToHealthOnly + + def DamageToHealthOnly_=(healthOnly: Boolean) : Boolean = { + damageToHealthOnly = healthOnly + DamageToHealthOnly + } + def DegradeDelay: Float = degradeDelay def DegradeDelay_=(degradeDelay: Float): Float = { @@ -174,7 +223,23 @@ class ProjectileDefinition(objectId: Int) JammerProjectile } - def DistanceMax: Float = distanceMax //accessor only + def GrenadeProjectile : Boolean = grenade_projectile + + def GrenadeProjectile_=(isGrenade : Boolean) : Boolean = { + grenade_projectile = isGrenade + GrenadeProjectile + } + + def Aggravated : Option[AggravatedDamage] = aggravated_damage + + def Aggravated_=(damage : AggravatedDamage) : Option[AggravatedDamage] = Aggravated_=(Some(damage)) + + def Aggravated_=(damage : Option[AggravatedDamage]) : Option[AggravatedDamage] = { + aggravated_damage = damage + Aggravated + } + + def DistanceMax : Float = distanceMax //accessor only def DistanceFromAcceleration: Float = distanceFromAcceleration //accessor only diff --git a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala index d69ee638..7bfc8f28 100644 --- a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala +++ b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala @@ -89,10 +89,10 @@ object EffectTarget { false } - def AMS(target: PlanetSideGameObject): Boolean = + def Vehicle(target: PlanetSideGameObject): Boolean = target match { case v: Vehicle => - v.Health > 0 && v.Definition == GlobalDefinitions.ams + v.Health > 0 case _ => false } @@ -104,5 +104,21 @@ object EffectTarget { case _ => false } + + def AMS(target: PlanetSideGameObject): Boolean = + target match { + case v: Vehicle => + v.Health > 0 && v.Definition == GlobalDefinitions.ams + case _ => + false + } + + def Aircraft(target: PlanetSideGameObject): Boolean = + target match { + case v: Vehicle => + GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 + case _ => + false + } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/aura/Aura.scala b/src/main/scala/net/psforever/objects/serverobject/aura/Aura.scala new file mode 100644 index 00000000..f78867e0 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/aura/Aura.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.aura + +/** + * An effect that can be emitted by a target game object entity. + */ +sealed class Aura() + +/** + * Visual effects emitted by a target, usually a `Player` entity. + * Most often paired with aggravated damage. + * Unrelated to the effects emitted by obtaining and transporting + * the lattice logic unit, or a facility module, or the rabbit ball. + */ +object Aura { + /** Since `None` is an actual effect, the "no effect" default is repurposed as "Nothing". */ + final case object Nothing extends Aura + + /** Conferred by the `aphelion_starfire_projectile`. */ + final case object None extends Aura + + /** A green emission. */ + final case object Plasma extends Aura + + /** A purple emission. */ + final case object Comet extends Aura + + /** A white and yellow starburst emission. */ + final case object Napalm extends Aura + + /** A red and orange emission. */ + final case object Fire extends Aura +} diff --git a/src/main/scala/net/psforever/objects/serverobject/aura/AuraContainer.scala b/src/main/scala/net/psforever/objects/serverobject/aura/AuraContainer.scala new file mode 100644 index 00000000..78d42069 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/aura/AuraContainer.scala @@ -0,0 +1,25 @@ +package net.psforever.objects.serverobject.aura + +import net.psforever.objects.serverobject.aura.{Aura => AuraEffect} + +/** + * An entity that can display specific special effects that decorate its model. + * These animations confer information about the nature of some status that is affecting the target entity. + */ +trait AuraContainer { + private var aura : Set[AuraEffect] = Set.empty[AuraEffect] + + def Aura : Set[AuraEffect] = aura + + def AddEffectToAura(effect : AuraEffect) : Set[AuraEffect] = { + if(effect != AuraEffect.None) { + aura = aura + effect + } + Aura + } + + def RemoveEffectFromAura(effect : AuraEffect) : Set[AuraEffect] = { + aura = aura - effect + Aura + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/aura/AuraEffectBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/aura/AuraEffectBehavior.scala new file mode 100644 index 00000000..1a041b62 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/aura/AuraEffectBehavior.scala @@ -0,0 +1,186 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.aura + +import akka.actor.{Actor, Cancellable} +import net.psforever.objects.Default +import net.psforever.objects.serverobject.PlanetSideServerObject + +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +/** + * A mixin that governs the addition, display, and removal of aura particle effects + * on a target with control agency. + * @see `Aura` + * @see `AuraContainer` + * @see `PlayerControl` + */ +trait AuraEffectBehavior { + _ : Actor => + /** active aura effects are monotonic, but the timer will be updated for continuing and cancelling effects as well
+ * only effects that are initialized to this mapping are approved for display on this target
+ * key - aura effect; value - the timer for that effect + * @see `ApplicableEffect` + */ + private val effectToTimer: mutable.HashMap[Aura, AuraEffectBehavior.Entry] = mutable.HashMap.empty[Aura, AuraEffectBehavior.Entry] + + def AuraTargetObject: AuraEffectBehavior.Target + + val auraBehavior: Receive = { + case AuraEffectBehavior.StartEffect(effect, duration) => + StartAuraEffect(effect, duration) + + case AuraEffectBehavior.EndEffect(effect) => + EndAuraEffectAndUpdate(effect) + + case AuraEffectBehavior.EndAllEffects() => + EndAllEffectsAndUpdate() + } + + /** + * Only pre-apporved aura effects will be emitted by this target. + * @param effect the aura effect + */ + def ApplicableEffect(effect: Aura): Unit = { + //create entry + effectToTimer += effect -> AuraEffectBehavior.Entry() + } + + /** + * An aura particle effect is to be emitted by the target. + * If the effect was not previously applied to the target in an ongoing manner, + * animate it appropriately. + * @param effect the effect to be emitted + * @param duration for how long the effect will be emitted + * @return the active effect index number + */ + def StartAuraEffect(effect: Aura, duration: Long): Unit = { + val obj = AuraTargetObject + val auraEffectsBefore = obj.Aura.size + if(StartAuraTimer(effect, duration) && obj.AddEffectToAura(effect).size > auraEffectsBefore) { + //new effect; update visuals + UpdateAuraEffect(AuraTargetObject) + } + } + + /** + * As long as the effect has been approved for this target, + * the timer will either start if it is stopped or has never been started, + * or the timer will stop and be recreated with the new duration if is currently running for a shoreter amount of time. + * @param effect the effect to be emitted + * @param duration for how long the effect will be emitted + * @return `true`, if the timer was started or restarted; + * `false`, otherwise + */ + private def StartAuraTimer(effect: Aura, duration: Long): Boolean = { + //pair aura effect with entry + (effectToTimer.get(effect) match { + case Some(timer) if timer.start + timer.duration < System.currentTimeMillis() + duration => + timer.cancel() + Some(effect) + case _ => + None + }) match { + case None => + false + case Some(_) => + //retime + effectToTimer(effect) = AuraEffectBehavior.Entry( + duration, + context.system.scheduler.scheduleOnce(duration milliseconds, self, AuraEffectBehavior.EndEffect(effect)) + ) + true + } + } + + /** + * Stop the target entity from emitting the aura particle effect, if it currently is. + * @param effect the target effect + * @return `true`, if the effect was being emitted but has been stopped + * `false`, if the effect was not approved or is not being emitted + */ + def EndAuraEffect(effect: Aura): Boolean = { + effectToTimer.get(effect) match { + case Some(timer) if !timer.isCancelled => + timer.cancel() + //effectToTimer(effect) = Default.Cancellable + AuraTargetObject.RemoveEffectFromAura(effect) + true + case _ => + false + } + } + + /** + * Stop the target entity from emitting all aura particle effects. + */ + def EndAllEffects() : Unit = { + effectToTimer.keysIterator.foreach { effect => + effectToTimer(effect).cancel() + //effectToTimer(effect) = Default.Cancellable + } + val obj = AuraTargetObject + obj.Aura.foreach { obj.RemoveEffectFromAura } + } + + /** + * Stop the target entity from emitting the aura particle effect, if it currently is. + * If the effect has been stopped, animate the new particle effect state. + */ + def EndAuraEffectAndUpdate(effect: Aura) : Unit = { + if(EndAuraEffect(effect)) { + UpdateAuraEffect(AuraTargetObject) + } + } + + /** + * Stop the target entity from emitting all aura particle effects. + * Animate the new particle effect state. + */ + def EndAllEffectsAndUpdate() : Unit = { + EndAllEffects() + UpdateAuraEffect(AuraTargetObject) + } + + /** + * Is the target entity emitting the aura effect? + * @param effect the effect being tested + * @return `true`, if the effect is currently being emitted; + * `false`, otherwise + */ + def TestForEffect(effect: Aura): Boolean = { + effectToTimer.get(effect) match { + case None => false + case Some(timer) => timer.isCancelled + } + } + + /** + * An override callback to display aura effects emitted. + * @param target the entity from which the aura effects are being emitted + */ + def UpdateAuraEffect(target: AuraEffectBehavior.Target) : Unit +} + +object AuraEffectBehavior { + type Target = PlanetSideServerObject with AuraContainer + + case class Entry(duration: Long, timer: Cancellable) extends Cancellable { + val start: Long = System.currentTimeMillis() + + override def isCancelled : Boolean = timer.isCancelled + + override def cancel(): Boolean = timer.cancel() + } + + object Entry { + def apply(): Entry = Entry(0, Default.Cancellable) + } + + final case class StartEffect(effect: Aura, duration: Long) + + final case class EndEffect(aura: Aura) + + final case class EndAllEffects() +} diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala new file mode 100644 index 00000000..ad211cdb --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/damage/AggravatedBehavior.scala @@ -0,0 +1,209 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.serverobject.damage + +import akka.actor.{Actor, Cancellable} +import net.psforever.objects.ballistics._ +import net.psforever.objects.serverobject.aura.Aura +import net.psforever.objects.vital.{DamageType, Vitality} + +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +trait AggravatedBehavior { + _ : Actor with Damageable => + private val entryIdToEntry: mutable.LongMap[AggravatedBehavior.Entry] = + mutable.LongMap.empty[AggravatedBehavior.Entry] + private val aggravationToTimer: mutable.LongMap[Cancellable] = + mutable.LongMap.empty[Cancellable] + /** ongoing flag to indicate whether the target is being afflicted by any form of aggravated damage */ + private var ongoingAggravated: Boolean = false + + def AggravatedObject: AggravatedBehavior.Target + + def TryAggravationEffectActivate(data: ResolvedProjectile): Option[AggravatedDamage] = { + val projectile = data.projectile + projectile.profile.Aggravated match { + case Some(damage) + if projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated) && + damage.info.exists(_.damage_type == AggravatedDamage.basicDamageType(data.resolution)) && + damage.effect_type != Aura.Nothing && + (projectile.quality == ProjectileQuality.AggravatesTarget || + damage.targets.exists(validation => validation.test(AggravatedObject))) => + TryAggravationEffectActivate(damage, data) + case _ => + None + } + } + + private def TryAggravationEffectActivate( + aggravation: AggravatedDamage, + data: ResolvedProjectile + ): Option[AggravatedDamage] = { + val effect = aggravation.effect_type + if(CheckForUniqueUnqueuedProjectile(data.projectile)) { + val sameEffect = entryIdToEntry.values.filter(entry => entry.effect == effect) + if(sameEffect.isEmpty || sameEffect.nonEmpty && aggravation.cumulative_damage_degrade) { + SetupAggravationEntry(aggravation, data) + Some(aggravation) + } + else { + None + } + } + else { + None + } + } + + private def CheckForUniqueUnqueuedProjectile(projectile : Projectile): Boolean = { + !entryIdToEntry.values.exists { entry => entry.data.projectile.id == projectile.id } + } + + private def SetupAggravationEntry(aggravation: AggravatedDamage, data: ResolvedProjectile): Boolean = { + val effect = aggravation.effect_type + aggravation.info.find(_.damage_type == AggravatedDamage.basicDamageType(data.resolution)) match { + case Some(info) => + val timing = aggravation.timing + val duration = timing.duration + //setup effect + val id = data.projectile.id + //setup timer data + val (tick: Long, iterations: Int) = timing.ticks match { + case Some(n) if n < 1 => + val rate = info.infliction_rate + (rate, (duration / rate).toInt) + case Some(ticks) => + (duration / ticks, ticks) + case None => + (1000L, (duration / 1000).toInt) + } + //quality per tick + val totalPower = (duration.toFloat / info.infliction_rate).toInt - 1 + val averagePowerPerTick = totalPower.toFloat / iterations + val lastTickRemainder = totalPower - averagePowerPerTick * iterations + val qualityPerTick: List[Float] = if (lastTickRemainder > 0) { + 0f +: List.fill[Float](iterations - 1)(averagePowerPerTick) :+ (lastTickRemainder + averagePowerPerTick) + } + else { + 0f +: List.fill[Float](iterations)(averagePowerPerTick) + } + //pair id with entry + PairIdWithAggravationEntry(id, effect, tick, data, data.target, qualityPerTick) + //pair id with timer + aggravationToTimer += id -> context.system.scheduler.scheduleOnce(tick milliseconds, self, AggravatedBehavior.Aggravate(id, iterations)) + ongoingAggravated = true + true + case _ => + false + } + } + + private def PairIdWithAggravationEntry( + id: Long, + effect: Aura, + retime: Long, + data: ResolvedProjectile, + target: SourceEntry, + powerOffset: List[Float] + ): AggravatedBehavior.Entry = { + val aggravatedDamageInfo = ResolvedProjectile( + AggravatedDamage.burning(data.resolution), + data.projectile, + target, + data.damage_model, + data.hit_pos + ) + val entry = AggravatedBehavior.Entry(id, effect, retime, aggravatedDamageInfo, powerOffset) + entryIdToEntry += id -> entry + entry + } + + val aggravatedBehavior: Receive = { + case AggravatedBehavior.Aggravate(id, 0) => + AggravationCleanup(id) + + case AggravatedBehavior.Aggravate(id, iteration) => + RetimeEventAndPerformAggravation(id, iteration, None) + } + + private def RetimeEventAndPerformAggravation(id: Long, iteration: Int, time: Option[Long]) : Unit = { + RetimeAggravation(id, iteration - 1, time) match { + case Some(entry) => + PerformAggravation(entry, iteration) + case _ => ; + } + } + + private def RetimeAggravation( + id: Long, + iteration: Int, + time: Option[Long] + ): Option[AggravatedBehavior.Entry] = { + CleanupAggravationTimer(id) + entryIdToEntry.get(id) match { + case out @ Some(oldEntry) => + aggravationToTimer += id -> context.system.scheduler.scheduleOnce( + time.getOrElse(oldEntry.retime) milliseconds, + self, + AggravatedBehavior.Aggravate(id, iteration) + ) + out + case _ => + AggravationCleanup(id) + None + } + } + + def RemoveAggravatedEntry(id: Long): Aura = { + entryIdToEntry.remove(id) match { + case Some(entry) => + ongoingAggravated = entryIdToEntry.nonEmpty + entry.data.projectile.profile.Aggravated.get.effect_type + case _ => + Aura.Nothing + } + } + + def CleanupAggravationTimer(id: Long): Unit = { + //remove and cancel timer + aggravationToTimer.remove(id) match { + case Some(timer) => timer.cancel() + case _ => ; + } + } + + def AggravationCleanup(id: Long): Unit = { + RemoveAggravatedEntry(id) + CleanupAggravationTimer(id) + } + + def EndAllAggravation(): Unit = { + entryIdToEntry.clear() + aggravationToTimer.values.foreach { _.cancel() } + aggravationToTimer.clear() + } + + def AggravatedReaction: Boolean = ongoingAggravated + + private def PerformAggravation(entry: AggravatedBehavior.Entry, tick: Int = 0): Unit = { + val data = entry.data + val model = data.damage_model + val aggravatedProjectileData = ResolvedProjectile( + data.resolution, + data.projectile.quality(ProjectileQuality.Modified(entry.qualityPerTick(tick))), + data.target, + model, + data.hit_pos + ) + takesDamage.apply(Vitality.Damage(model.Calculate(aggravatedProjectileData))) + } +} + +object AggravatedBehavior { + type Target = Damageable.Target + + private case class Entry(id: Long, effect: Aura, retime: Long, data: ResolvedProjectile, qualityPerTick: List[Float]) + + private case class Aggravate(id: Long, iterations: Int) +} diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala index 5dc87756..e36b3c95 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala @@ -8,6 +8,7 @@ import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.resolution.ResolutionCalculations /** * The base "control" `Actor` mixin for damage-handling code. @@ -24,16 +25,35 @@ trait Damageable { */ def DamageableObject: Damageable.Target - /** the official mixin hook; `orElse` onto the "control" `Actor` `receive` */ - final val takesDamage: Receive = TakesDamage + /** the official mixin hook; + * `orElse` onto the "control" `Actor` `receive`; or, + * cite the `originalTakesDamage` protocol during inheritance overrides */ + val takesDamage: Receive = { + case Vitality.Damage(damage_func) => + val obj = DamageableObject + if (obj.CanDamage) { + PerformDamage(obj, damage_func) + } + } + + /** a duplicate of the core implementation for the default mixin hook, for use in overriding */ + final val originalTakesDamage: Receive = { + case Vitality.Damage(damage_func) => + val obj = DamageableObject + if (obj.CanDamage) { + PerformDamage(obj, damage_func) + } + } /** - * Implementation of the mixin hook will be provided by a child class. - * Override this method only when directly implementing. - * @see `takesDamage` - * @see `DamageableAmenity.PerformDamage` + * Assess the vital statistics of the target, apply the damage, and determine if any of those statistics changed. + * By default, only take an interest in the change of "health". + * If implementing custom damage with no new message handling, override this method. + * @see `ResolutionCalculations.Output` + * @param target the entity to be damaged + * @param applyDamageTo the function that applies the damage to the target in a target-tailored fashion */ - protected def TakesDamage: Receive + protected def PerformDamage(target: Damageable.Target, applyDamageTo: ResolutionCalculations.Output): Unit } object Damageable { @@ -59,7 +79,7 @@ object Damageable { */ def CanDamage(obj: Vitality with FactionAffinity, damage: Int, data: ResolvedProjectile): Boolean = { val definition = obj.Definition - damage > 0 && + (damage > 0 || data.projectile.profile.Aggravated.nonEmpty) && definition.Damageable && (definition.DamageableByFriendlyFire || (data.projectile.owner.Faction != obj.Faction || diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala index e4591747..a2db8e3c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala @@ -1,10 +1,8 @@ //Copyright (c) 2020 PSForever package net.psforever.objects.serverobject.damage -import akka.actor.Actor.Receive import net.psforever.objects.ballistics.ResolvedProjectile import net.psforever.objects.equipment.JammableUnit -import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.resolution.ResolutionCalculations import net.psforever.objects.zones.Zone import net.psforever.types.PlanetSideGUID @@ -42,23 +40,6 @@ trait DamageableEntity extends Damageable { DamageLog(s"${name.substring(slashPoint + 1, name.length - 1)}: $msg") } - /** - * Catch the expected damage message and apply checks to the target. - * If adding custom message handling in an future child implementation, - * override this method and call `super.TakesDamage.orElse { ... }`. - * @see `Damageable.TakesDamage` - * @see `ResolutionCalcultions.Output` - * @see `Vitality.CanDamage` - * @see `Vitality.Damage` - */ - protected def TakesDamage: Receive = { - case Vitality.Damage(damage_func) => - val obj = DamageableObject - if (obj.CanDamage) { - PerformDamage(obj, damage_func) - } - } - /** * Assess the vital statistics of the target, apply the damage, and determine if any of those statistics changed. * By default, only take an interest in the change of "health". @@ -108,7 +89,7 @@ trait DamageableEntity extends Damageable { * @param cause historical information about the damage * @param damage the amount of damage */ - protected def HandleDamage(target: Damageable.Target, cause: ResolvedProjectile, damage: Int): Unit = { + protected def HandleDamage(target: Damageable.Target, cause: ResolvedProjectile, damage: Any): Unit = { if (!target.Destroyed && target.Health <= target.Definition.DamageDestroysAt) { DestructionAwareness(target, cause) } else { @@ -122,8 +103,12 @@ trait DamageableEntity extends Damageable { * @param cause historical information about the damage * @param amount the amount of damage */ - protected def DamageAwareness(target: Damageable.Target, cause: ResolvedProjectile, amount: Int): Unit = { - DamageableEntity.DamageAwareness(target, cause, amount) + protected def DamageAwareness(target: Damageable.Target, cause: ResolvedProjectile, amount: Any): Unit = { + amount match { + case value: Int => + DamageableEntity.DamageAwareness(target, cause, value) + case _ => ; + } } /** @@ -162,16 +147,22 @@ object DamageableEntity { if (Damageable.CanJammer(target, cause)) { target.Actor ! JammableUnit.Jammered(cause) } - if (amount > 0) { + if (DamageToHealth(target, cause, amount)) { + target.Zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) + } + } + + def DamageToHealth(target: Damageable.Target, cause: ResolvedProjectile, amount: Int): Boolean = { + if (amount > 0 && !target.Destroyed) { val zone = target.Zone - if (!target.Destroyed) { - val tguid = target.GUID - zone.AvatarEvents ! AvatarServiceMessage( - zone.id, - AvatarAction.PlanetsideAttributeToAll(tguid, 0, target.Health) - ) - } - zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) + zone.AvatarEvents ! AvatarServiceMessage( + zone.id, + AvatarAction.PlanetsideAttributeToAll(target.GUID, 0, target.Health) + ) + true + } + else { + false } } diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala index cc3e5a8b..abfb7922 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableMountable.scala @@ -26,8 +26,13 @@ object DamageableMountable { * @see `Zone.LivePlayers` * @param target the entity being damaged * @param cause historical information about the damage + * @param countableDamage the amount of damage being done, translating to the intensity of the damage indicator */ - def DamageAwareness(target: Damageable.Target with Mountable, cause: ResolvedProjectile): Unit = { + def DamageAwareness( + target: Damageable.Target with Mountable, + cause: ResolvedProjectile, + countableDamage: Int + ): Unit = { val zone = target.Zone val events = zone.AvatarEvents val occupants = target.Seats.values.collect { @@ -38,9 +43,10 @@ object DamageableMountable { case pSource: PlayerSource => //player damage val name = pSource.Name (zone.LivePlayers.find(_.Name == name).orElse(zone.Corpses.find(_.Name == name)) match { - case Some(player) => AvatarAction.HitHint(player.GUID, player.GUID) + case Some(player) => + AvatarAction.HitHint(player.GUID, player.GUID) case None => - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, pSource.Position)) + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(countableDamage, pSource.Position)) }) match { case AvatarAction.HitHint(_, guid) => occupants.map { tplayer => (tplayer.Name, AvatarAction.HitHint(guid, tplayer.GUID)) } @@ -48,7 +54,7 @@ object DamageableMountable { occupants.map { tplayer => (tplayer.Name, msg) } } case source => //object damage - val msg = AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, source.Position)) + val msg = AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(countableDamage, source.Position)) occupants.map { tplayer => (tplayer.Name, msg) } }).foreach { case (channel, msg) => 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 c3d26a3d..ceea8af0 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -1,43 +1,55 @@ //Copyright (c) 2020 PSForever package net.psforever.objects.serverobject.damage -import akka.actor.Actor.Receive +import akka.actor.Actor import net.psforever.objects.{Vehicle, Vehicles} import net.psforever.objects.ballistics.ResolvedProjectile +import net.psforever.objects.equipment.JammableUnit import net.psforever.objects.serverobject.damage.Damageable.Target +import net.psforever.objects.vital.DamageType import net.psforever.objects.vital.resolution.ResolutionCalculations import net.psforever.services.Service import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.DamageWithPositionMessage +import net.psforever.types.Vector3 import scala.concurrent.duration._ /** * The "control" `Actor` mixin for damage-handling code for `Vehicle` objects. */ -trait DamageableVehicle extends DamageableEntity { +trait DamageableVehicle + extends DamageableEntity + with AggravatedBehavior { + _ : Actor => - /** vehicles (may) have shields; they need to be handled */ - private var handleDamageToShields: Boolean = false + def damageableVehiclePostStop(): Unit = { + EndAllAggravation() + } /** whether or not the vehicle has been damaged directly, report that damage has occurred */ private var reportDamageToVehicle: Boolean = false def DamageableObject: Vehicle + def AggravatedObject : Vehicle = DamageableObject - override protected def TakesDamage: Receive = - super.TakesDamage.orElse { - case DamageableVehicle.Damage(cause, damage) => - //cargo vehicles inherit feedback from carrier - reportDamageToVehicle = damage > 0 - DamageAwareness(DamageableObject, cause, amount = 0) + override val takesDamage: Receive = + originalTakesDamage + .orElse(aggravatedBehavior) + .orElse { + case DamageableVehicle.Damage(cause, damage) => + //cargo vehicles inherit feedback from carrier + reportDamageToVehicle = damage > 0 + DamageAwareness(DamageableObject, cause, amount = 0) - case DamageableVehicle.Destruction(cause) => - //cargo vehicles are destroyed when carrier is destroyed - val obj = DamageableObject - obj.Health = 0 - obj.History(cause) - DestructionAwareness(obj, cause) - } + case DamageableVehicle.Destruction(cause) => + //cargo vehicles are destroyed when carrier is destroyed + val obj = DamageableObject + obj.Health = 0 + obj.History(cause) + DestructionAwareness(obj, cause) + } /** * Vehicles may have charged shields that absorb damage before the vehicle's own health is affected. @@ -62,53 +74,13 @@ trait DamageableVehicle extends DamageableEntity { target, s"BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields" ) - handleDamageToShields = damageToShields > 0 - HandleDamage(target, cause, damageToHealth + damageToShields) + HandleDamage(target, cause, (damageToHealth, damageToShields)) } else { obj.Health = originalHealth obj.Shields = originalShields } } - override protected def DamageAwareness(target: Target, cause: ResolvedProjectile, amount: Int): Unit = { - val obj = DamageableObject - val handleShields = handleDamageToShields - handleDamageToShields = false - val handleReport = reportDamageToVehicle || amount > 0 - reportDamageToVehicle = false - if (Damageable.CanDamageOrJammer(target, amount, cause)) { - super.DamageAwareness(target, cause, amount) - } - if (handleReport) { - DamageableMountable.DamageAwareness(obj, cause) - } - DamageableVehicle.DamageAwareness(obj, cause, amount, handleShields) - } - - override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = { - super.DestructionAwareness(target, cause) - val obj = DamageableObject - DamageableMountable.DestructionAwareness(obj, cause) - DamageableVehicle.DestructionAwareness(obj, cause) - DamageableWeaponTurret.DestructionAwareness(obj, cause) - } -} - -object DamageableVehicle { - - /** - * Message for instructing the target's cargo vehicles about a damage source affecting their carrier. - * @param cause historical information about damage - */ - private case class Damage(cause: ResolvedProjectile, amount: Int) - - /** - * Message for instructing the target's cargo vehicles that their carrier is destroyed, - * and they should be destroyed too. - * @param cause historical information about damage - */ - private case class Destruction(cause: ResolvedProjectile) - /** * Most all vehicles and the weapons mounted to them can jam * if the projectile that strikes (near) them has jammering properties. @@ -121,25 +93,76 @@ object DamageableVehicle { * @see `VehicleServiceMessage` * @param target the entity being destroyed * @param cause historical information about the damage - * @param damage how much damage was performed - * @param damageToShields dispatch a shield strength update + * @param amount how much damage was performed */ - def DamageAwareness(target: Vehicle, cause: ResolvedProjectile, damage: Int, damageToShields: Boolean): Unit = { - //alert cargo occupants to damage source - target.CargoHolds.values.foreach(hold => { - hold.Occupant match { - case Some(cargo) => - cargo.Actor ! DamageableVehicle.Damage(cause, damage + (if (damageToShields) 1 else 0)) - case None => ; + override protected def DamageAwareness(target: Target, cause: ResolvedProjectile, amount: Any): Unit = { + val obj = DamageableObject + val zone = target.Zone + val events = zone.VehicleEvents + val targetGUID = target.GUID + val zoneId = zone.id + val vehicleChannel = s"${obj.Actor}" + val (damageToHealth, damageToShields, totalDamage) = amount match { + case (a: Int, b: Int) => (a, b, a+b) + case _ => (0, 0, 0) + } + var announceConfrontation: Boolean = reportDamageToVehicle || totalDamage > 0 + val aggravated = TryAggravationEffectActivate(cause) match { + case Some(_) => + announceConfrontation = true + false + case _ => + cause.projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated) + } + reportDamageToVehicle = false + + //log historical event + target.History(cause) + //damage + if (Damageable.CanDamageOrJammer(target, totalDamage, cause)) { + //jammering + if (Damageable.CanJammer(target, cause)) { + target.Actor ! JammableUnit.Jammered(cause) } - }) - //shields - if (damageToShields) { - val zone = target.Zone - zone.VehicleEvents ! VehicleServiceMessage( - s"${target.Actor}", - VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 68, target.Shields) - ) + //stat changes + if (damageToShields > 0) { + events ! VehicleServiceMessage( + vehicleChannel, + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 68, obj.Shields) + ) + announceConfrontation = true + } + if (damageToHealth > 0) { + events ! VehicleServiceMessage( + zoneId, + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, obj.Health) + ) + announceConfrontation = true + } + } + if (announceConfrontation) { + if (aggravated) { + val msg = VehicleAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(totalDamage, Vector3.Zero)) + obj.Seats.values + .collect { case seat if seat.Occupant.nonEmpty => seat.Occupant.get.Name } + .foreach { channel => + events ! VehicleServiceMessage(channel, msg) + } + } + else { + //activity on map + zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) + //alert to damage source + DamageableMountable.DamageAwareness(obj, cause, totalDamage) + } + //alert cargo occupants to damage source + obj.CargoHolds.values.foreach(hold => { + hold.Occupant match { + case Some(cargo) => + cargo.Actor ! DamageableVehicle.Damage(cause, totalDamage) + case None => ; + } + }) } } @@ -164,10 +187,15 @@ object DamageableVehicle { * @param target the entity being destroyed * @param cause historical information about the damage */ - def DestructionAwareness(target: Vehicle, cause: ResolvedProjectile): Unit = { + override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = { + super.DestructionAwareness(target, cause) + val obj = DamageableObject + DamageableMountable.DestructionAwareness(obj, cause) val zone = target.Zone + //aggravation cancel + EndAllAggravation() //cargo vehicles die with us - target.CargoHolds.values.foreach(hold => { + obj.CargoHolds.values.foreach(hold => { hold.Occupant match { case Some(cargo) => cargo.Actor ! DamageableVehicle.Destruction(cause) @@ -175,10 +203,10 @@ object DamageableVehicle { } }) //special considerations for certain vehicles - Vehicles.BeforeUnloadVehicle(target, zone) + Vehicles.BeforeUnloadVehicle(obj, zone) //shields - if (target.Shields > 0) { - target.Shields = 0 + if (obj.Shields > 0) { + obj.Shields = 0 zone.VehicleEvents ! VehicleServiceMessage( zone.id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 68, 0) @@ -186,5 +214,22 @@ object DamageableVehicle { } target.Actor ! Vehicle.Deconstruct(Some(1 minute)) target.ClearHistory() + DamageableWeaponTurret.DestructionAwareness(obj, cause) } } + +object DamageableVehicle { + + /** + * Message for instructing the target's cargo vehicles about a damage source affecting their carrier. + * @param cause historical information about damage + */ + private case class Damage(cause: ResolvedProjectile, amount: Int) + + /** + * Message for instructing the target's cargo vehicles that their carrier is destroyed, + * and they should be destroyed too. + * @param cause historical information about damage + */ + private case class Destruction(cause: ResolvedProjectile) +} diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala index b7fab97a..f4ff4b2c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala @@ -1,30 +1,97 @@ //Copyright (c) 2020 PSForever package net.psforever.objects.serverobject.damage +import akka.actor.Actor import net.psforever.objects.ballistics.ResolvedProjectile +import net.psforever.objects.equipment.JammableUnit import net.psforever.objects.serverobject.turret.{TurretUpgrade, WeaponTurret} import net.psforever.objects.vehicles.MountedWeapons +import net.psforever.objects.vital.DamageType +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.DamageWithPositionMessage +import net.psforever.types.Vector3 import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.support.TurretUpgrader -import net.psforever.services.vehicle.VehicleServiceMessage +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} /** * The "control" `Actor` mixin for damage-handling code for `WeaponTurret` objects. */ -trait DamageableWeaponTurret extends DamageableEntity { - def DamageableObject: Damageable.Target with WeaponTurret +trait DamageableWeaponTurret + extends DamageableEntity + with AggravatedBehavior { + _: Actor => - override protected def DamageAwareness(target: Damageable.Target, cause: ResolvedProjectile, amount: Int): Unit = { - super.DamageAwareness(target, cause, amount) - if (amount > 0) { - DamageableMountable.DamageAwareness(DamageableObject, cause) + def damageableWeaponTurretPostStop(): Unit = { + EndAllAggravation() + } + + def DamageableObject: Damageable.Target with WeaponTurret + def AggravatedObject: Damageable.Target with WeaponTurret = DamageableObject + + override val takesDamage: Receive = originalTakesDamage.orElse(aggravatedBehavior) + + override protected def DamageAwareness(target: Damageable.Target, cause: ResolvedProjectile, amount: Any): Unit = { + val obj = DamageableObject + val zone = target.Zone + val events = zone.VehicleEvents + val targetGUID = target.GUID + val zoneId = zone.id + val damageToHealth = amount match { + case a: Int => a + case _ => 0 + } + var announceConfrontation: Boolean = damageToHealth > 0 + val aggravated = TryAggravationEffectActivate(cause) match { + case Some(_) => + announceConfrontation = true + false + case _ => + cause.projectile.profile.ProjectileDamageTypes.contains(DamageType.Aggravated) + } + + //log historical event + target.History(cause) + //damage + if (Damageable.CanDamageOrJammer(target, damageToHealth, cause)) { + //jammering + if (Damageable.CanJammer(target, cause)) { + target.Actor ! JammableUnit.Jammered(cause) + } + //stat changes + //TODO some turrets have shields + if (damageToHealth > 0) { + DamageableMountable.DamageAwareness(DamageableObject, cause, damageToHealth) + events ! VehicleServiceMessage( + zoneId, + VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, obj.Health) + ) + announceConfrontation = true + } + } + if (announceConfrontation) { + if (aggravated) { + val msg = VehicleAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(damageToHealth, Vector3.Zero)) + obj.Seats.values + .collect { case seat if seat.Occupant.nonEmpty => seat.Occupant.get.Name } + .foreach { channel => + events ! VehicleServiceMessage(channel, msg) + } + } + else { + //activity on map + zone.Activity ! Zone.HotSpot.Activity(cause.target, cause.projectile.owner, cause.hit_pos) + //alert to damage source + DamageableMountable.DamageAwareness(obj, cause, damageToHealth) + } } } override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = { super.DestructionAwareness(target, cause) val obj = DamageableObject + EndAllAggravation() DamageableWeaponTurret.DestructionAwareness(obj, cause) DamageableMountable.DestructionAwareness(obj, cause) } diff --git a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala index 799b339f..e4258dba 100644 --- a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala @@ -88,9 +88,13 @@ class GeneratorControl(gen: Generator) !imminentExplosion && super.WillAffectTarget(target, damage, cause) } - override protected def DamageAwareness(target: Target, cause: ResolvedProjectile, amount: Int): Unit = { + override protected def DamageAwareness(target: Target, cause: ResolvedProjectile, amount: Any): Unit = { super.DamageAwareness(target, cause, amount) - GeneratorControl.DamageAwareness(gen, cause, amount) + val damageTo = amount match { + case a: Int => a + case _ => 0 + } + GeneratorControl.DamageAwareness(gen, cause, damageTo) } override protected def DestructionAwareness(target: Target, cause: ResolvedProjectile): Unit = { diff --git a/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala b/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala index ec5b7346..2bbb5ed4 100644 --- a/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala @@ -72,9 +72,13 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) } } - override protected def DamageAwareness(target: Target, cause: ResolvedProjectile, amount: Int): Unit = { + override protected def DamageAwareness(target: Target, cause: ResolvedProjectile, amount: Any): Unit = { super.DamageAwareness(target, cause, amount) - DamageableMountable.DamageAwareness(DamageableObject, cause) + val damageTo = amount match { + case a: Int => a + case _ => 0 + } + DamageableMountable.DamageAwareness(DamageableObject, cause, damageTo) } override protected def DestructionAwareness(target: Damageable.Target, cause: ResolvedProjectile): Unit = { 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 44a85f83..a7e3436c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -44,6 +44,11 @@ class FacilityTurretControl(turret: FacilityTurret) // Used for timing ammo recharge for vanu turrets in caves var weaponAmmoRechargeTimer = Default.Cancellable + override def postStop(): Unit = { + super.postStop() + damageableWeaponTurretPostStop() + } + def receive: Receive = checkBehavior .orElse(jammableBehavior) diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala index 1e8a4038..355698d4 100644 --- a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -11,7 +11,7 @@ import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior} -import net.psforever.objects.serverobject.damage.DamageableVehicle +import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle} import net.psforever.objects.serverobject.deploy.Deployment.DeploymentObject import net.psforever.objects.serverobject.deploy.{Deployment, DeploymentBehavior} import net.psforever.objects.serverobject.hackable.GenericHackables @@ -51,11 +51,12 @@ class VehicleControl(vehicle: Vehicle) with RepairableVehicle with JammableMountedWeapons with ContainableBehavior - with AntTransferBehavior { + with AntTransferBehavior + with AggravatedBehavior { //make control actors belonging to utilities when making control actor belonging to vehicle vehicle.Utilities.foreach({ case (_, util) => util.Setup }) - + def MountableObject = vehicle def CargoObject = vehicle @@ -74,7 +75,7 @@ class VehicleControl(vehicle: Vehicle) def ChargeTransferObject = vehicle - if (vehicle.Definition == GlobalDefinitions.ant) { + if(vehicle.Definition == GlobalDefinitions.ant) { findChargeTargetFunc = Vehicles.FindANTChargingSource findDischargeTargetFunc = Vehicles.FindANTDischargingTarget } @@ -89,6 +90,7 @@ class VehicleControl(vehicle: Vehicle) override def postStop(): Unit = { super.postStop() + damageableVehiclePostStop() decaying = false decayTimer.cancel() vehicle.Utilities.values.foreach { util => diff --git a/src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala b/src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala index 1fd9efc7..56140883 100644 --- a/src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala +++ b/src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala @@ -1,8 +1,8 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.vital -import net.psforever.objects.ballistics.{ProjectileResolution, ResolvedProjectile} import net.psforever.objects.vital.damage.DamageCalculations +import net.psforever.objects.ballistics.ResolvedProjectile import net.psforever.objects.vital.projectile.ProjectileCalculations import net.psforever.objects.vital.resistance.ResistanceSelection import net.psforever.objects.vital.resolution.ResolutionCalculations @@ -72,7 +72,7 @@ trait DamageResistanceModel { * @param resolution an explicit damage resolution overriding the one in the `ResolvedProjectile` object * @return a function literal that encapsulates delayed modification instructions for certain objects */ - def Calculate(data: ResolvedProjectile, resolution: ProjectileResolution.Value): ResolutionCalculations.Output = { + def Calculate(data: ResolvedProjectile, resolution: DamageType.Value): ResolutionCalculations.Output = { val res: ProjectileCalculations.Form = ResistUsing(resolution) Model(DamageUsing, res, data) } diff --git a/src/main/scala/net/psforever/objects/vital/DamageType.scala b/src/main/scala/net/psforever/objects/vital/DamageType.scala index c34a6ea2..5f5edb65 100644 --- a/src/main/scala/net/psforever/objects/vital/DamageType.scala +++ b/src/main/scala/net/psforever/objects/vital/DamageType.scala @@ -9,5 +9,5 @@ package net.psforever.objects.vital object DamageType extends Enumeration(1) { type Type = Value - final val Direct, Splash, Radiation, Aggravated, Plasma, Comet, None = Value + final val Direct, Splash, Lash, Radiation, Aggravated, Plasma, Comet, None = Value } diff --git a/src/main/scala/net/psforever/objects/vital/StandardResistances.scala b/src/main/scala/net/psforever/objects/vital/StandardResistances.scala index 34b0e454..c8a5a74f 100644 --- a/src/main/scala/net/psforever/objects/vital/StandardResistances.scala +++ b/src/main/scala/net/psforever/objects/vital/StandardResistances.scala @@ -6,70 +6,70 @@ import net.psforever.objects.vital.projectile.ProjectileCalculations import net.psforever.objects.vital.resistance.{ResistanceCalculations, ResistanceSelection} object NoResistance - extends ResistanceCalculations[SourceEntry]( - ResistanceCalculations.ValidInfantryTarget, - ResistanceCalculations.NoResistExtractor - ) + extends ResistanceCalculations[SourceEntry]( + ResistanceCalculations.ValidInfantryTarget, + ResistanceCalculations.NoResistExtractor + ) object InfantryHitResistance - extends ResistanceCalculations[PlayerSource]( - ResistanceCalculations.ValidInfantryTarget, - ResistanceCalculations.ExoSuitDirectExtractor - ) + extends ResistanceCalculations[PlayerSource]( + ResistanceCalculations.ValidInfantryTarget, + ResistanceCalculations.ExoSuitDirectExtractor + ) object InfantrySplashResistance - extends ResistanceCalculations[PlayerSource]( - ResistanceCalculations.ValidInfantryTarget, - ResistanceCalculations.ExoSuitSplashExtractor - ) + extends ResistanceCalculations[PlayerSource]( + ResistanceCalculations.ValidInfantryTarget, + ResistanceCalculations.ExoSuitSplashExtractor + ) object InfantryLashResistance - extends ResistanceCalculations[PlayerSource]( - ResistanceCalculations.ValidInfantryTarget, - ResistanceCalculations.MaximumResistance - ) + extends ResistanceCalculations[PlayerSource]( + ResistanceCalculations.ValidInfantryTarget, + ResistanceCalculations.MaximumResistance + ) object InfantryAggravatedResistance - extends ResistanceCalculations[PlayerSource]( - ResistanceCalculations.ValidInfantryTarget, - ResistanceCalculations.ExoSuitAggravatedExtractor - ) + extends ResistanceCalculations[PlayerSource]( + ResistanceCalculations.ValidInfantryTarget, + ResistanceCalculations.ExoSuitAggravatedExtractor + ) object VehicleHitResistance - extends ResistanceCalculations[VehicleSource]( - ResistanceCalculations.ValidVehicleTarget, - ResistanceCalculations.VehicleDirectExtractor - ) + extends ResistanceCalculations[VehicleSource]( + ResistanceCalculations.ValidVehicleTarget, + ResistanceCalculations.VehicleDirectExtractor + ) object VehicleSplashResistance - extends ResistanceCalculations[VehicleSource]( - ResistanceCalculations.ValidVehicleTarget, - ResistanceCalculations.VehicleSplashExtractor - ) + extends ResistanceCalculations[VehicleSource]( + ResistanceCalculations.ValidVehicleTarget, + ResistanceCalculations.VehicleSplashExtractor + ) object VehicleLashResistance - extends ResistanceCalculations[VehicleSource]( - ResistanceCalculations.ValidVehicleTarget, - ResistanceCalculations.NoResistExtractor - ) + extends ResistanceCalculations[VehicleSource]( + ResistanceCalculations.ValidVehicleTarget, + ResistanceCalculations.NoResistExtractor + ) object VehicleAggravatedResistance - extends ResistanceCalculations[VehicleSource]( - ResistanceCalculations.ValidVehicleTarget, - ResistanceCalculations.VehicleAggravatedExtractor - ) + extends ResistanceCalculations[VehicleSource]( + ResistanceCalculations.ValidVehicleTarget, + ResistanceCalculations.VehicleAggravatedExtractor + ) object AmenityHitResistance - extends ResistanceCalculations[ObjectSource]( - ResistanceCalculations.ValidAmenityTarget, - ResistanceCalculations.OtherDirectExtractor - ) + extends ResistanceCalculations[ObjectSource]( + ResistanceCalculations.ValidAmenityTarget, + ResistanceCalculations.OtherDirectExtractor + ) -object AMenitySplashResistance - extends ResistanceCalculations[ObjectSource]( - ResistanceCalculations.ValidAmenityTarget, - ResistanceCalculations.OtherSplashExtractor - ) +object AmenitySplashResistance + extends ResistanceCalculations[ObjectSource]( + ResistanceCalculations.ValidAmenityTarget, + ResistanceCalculations.OtherSplashExtractor + ) object NoResistanceSelection extends ResistanceSelection { def Direct: ProjectileCalculations.Form = None diff --git a/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala b/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala index fb5eb164..673c7aea 100644 --- a/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala +++ b/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala @@ -11,13 +11,13 @@ object NoResolutions object InfantryResolutions extends DamageResistCalculations( - ResolutionCalculations.InfantryDamageAfterResist, + ResolutionCalculations.InfantryDamage, ResolutionCalculations.InfantryApplication ) object MaxResolutions extends DamageResistCalculations( - ResolutionCalculations.MaxDamageAfterResist, + ResolutionCalculations.MaxDamage, ResolutionCalculations.InfantryApplication ) diff --git a/src/main/scala/net/psforever/objects/vital/damage/DamageModifiers.scala b/src/main/scala/net/psforever/objects/vital/damage/DamageModifiers.scala index 478f5bfb..82a874ea 100644 --- a/src/main/scala/net/psforever/objects/vital/damage/DamageModifiers.scala +++ b/src/main/scala/net/psforever/objects/vital/damage/DamageModifiers.scala @@ -1,8 +1,9 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.vital.damage -import net.psforever.objects.ballistics.{ProjectileResolution, ResolvedProjectile} -import net.psforever.types.Vector3 +import net.psforever.objects.ballistics._ +import net.psforever.objects.vital.DamageType +import net.psforever.types.{ExoSuitType, Vector3} /** * Adjustments performed on the subsequent manipulations of the "base damage" value of an attack vector @@ -45,6 +46,21 @@ object DamageModifiers { private def function(damage: Int, data: ResolvedProjectile): Int = damage } + case object MaxDistanceCutoff extends Mod { + def Calculate: DamageModifiers.Format = function + + private def function(damage: Int, data: ResolvedProjectile): Int = { + val projectile = data.projectile + val profile = projectile.profile + val distance = Vector3.Distance(data.hit_pos, projectile.shot_origin) + if (distance <= profile.DistanceMax) { + damage + } else { + 0 + } + } + } + /** * The input value degrades (lessens) * the further the distance between the point of origin (`shot_origin`) @@ -81,14 +97,12 @@ object DamageModifiers { def Calculate: DamageModifiers.Format = function private def function(damage: Int, data: ResolvedProjectile): Int = { - val projectile = data.projectile - val profile = projectile.profile + val profile = data.projectile.profile val distance = Vector3.Distance(data.hit_pos, data.target.Position) val radius = profile.DamageRadius if (distance <= radius) { - val base: Float = profile.DamageAtEdge - val degrade: Float = (1 - base) * ((radius - distance) / radius) + base - (damage * degrade).toInt + val base: Float = profile.DamageAtEdge + (damage * ((1 - base) * ((radius - distance) / radius) + base)).toInt } else { 0 } @@ -117,4 +131,291 @@ object DamageModifiers { } } } + + /* + Below this point are the calculations for sources of aggravated damage. + For the most part, these calculations are individualistic and arbitrary. + They exist in their current form to satisfy observed shots to kill (STK) of specific weapon systems + according to 2012 standards of the Youtube video series by TheLegendaryNarwhal. + */ + /** + * The initial application of aggravated damage against an infantry target + * where the specific damage component is `Direct`. + */ + case object InfantryAggravatedDirect extends Mod { + def Calculate: DamageModifiers.Format = + BaseAggravatedFormula(ProjectileResolution.AggravatedDirect, DamageType.Direct) + } + + /** + * The initial application of aggravated damage against an infantry target + * where the specific damage component is `Splash`. + */ + case object InfantryAggravatedSplash extends Mod { + def Calculate: DamageModifiers.Format = + BaseAggravatedFormula(ProjectileResolution.AggravatedSplash, DamageType.Splash) + } + + /** + * The ongoing application of aggravated damage ticks against an infantry target + * where the specific damage component is `Direct`. + * This is called "burning" regardless of what the active aura effect actually is. + */ + case object InfantryAggravatedDirectBurn extends Mod { + def Calculate: DamageModifiers.Format = + BaseAggravatedBurnFormula(ProjectileResolution.AggravatedDirectBurn, DamageType.Direct) + } + + /** + * The ongoing application of aggravated damage ticks against an infantry target + * where the specific damage component is `Splash`. + * This is called "burning" regardless of what the active aura effect actually is. + */ + case object InfantryAggravatedSplashBurn extends Mod { + def Calculate: DamageModifiers.Format = + BaseAggravatedBurnFormula(ProjectileResolution.AggravatedSplashBurn, DamageType.Splash) + } + + /** + * For damage application that involves aggravation of a particular damage type, + * calculate that initial damage application for infantry targets + * and produce the modified damage value. + * Infantry wearing mechanized assault exo-suits (MAX) incorporate an additional modifier. + * @see `AggravatedDamage` + * @see `ExoSuitType` + * @see `InfantryAggravatedDirect` + * @see `InfantryAggravatedSplash` + * @see `PlayerSource` + * @see `ProjectileTarget.AggravatesTarget` + * @see `ResolvedProjectile` + * @param resolution the projectile resolution to match against + * @param damageType the damage type to find in as a component of aggravated information + * @param damage the base damage value + * @param data historical information related to the damage interaction + * @return the modified damage + */ + private def BaseAggravatedFormula( + resolution: ProjectileResolution.Value, + damageType : DamageType.Value + ) + ( + damage: Int, + data: ResolvedProjectile + ): Int = { + if (data.resolution == resolution && + data.projectile.quality == ProjectileQuality.AggravatesTarget) { + (data.projectile.profile.Aggravated, data.target) match { + case (Some(aggravation), p: PlayerSource) => + val aggravatedDamage = aggravation.info.find(_.damage_type == damageType) match { + case Some(infos) => + damage * infos.degradation_percentage + damage + case _ => + damage toFloat + } + if(p.ExoSuit == ExoSuitType.MAX) { + (aggravatedDamage * aggravation.max_factor) toInt + } else { + aggravatedDamage toInt + } + case _ => + damage + } + } else { + damage + } + } + + /** + * For damage application that involves aggravation of a particular damage type, + * calculate that damage application burn for each tick for infantry targets + * and produce the modified damage value. + * Infantry wearing mechanized assault exo-suits (MAX) incorporate an additional modifier. + * Vanilla infantry incorporate their resistance value into a slightly different calculation than usual. + * @see `AggravatedDamage` + * @see `ExoSuitType` + * @see `InfantryAggravatedDirectBurn` + * @see `InfantryAggravatedSplashBurn` + * @see `PlayerSource` + * @see `ResolvedProjectile` + * @param resolution the projectile resolution to match against + * @param damageType the damage type to find in as a component of aggravated information + * @param damage the base damage value + * @param data historical information related to the damage interaction + * @return the modified damage + */ + private def BaseAggravatedBurnFormula( + resolution: ProjectileResolution.Value, + damageType : DamageType.Value + ) + ( + damage: Int, + data: ResolvedProjectile + ): Int = { + if (data.resolution == resolution) { + (data.projectile.profile.Aggravated, data.target) match { + case (Some(aggravation), p: PlayerSource) => + val degradation = aggravation.info.find(_.damage_type == damageType) match { + case Some(info) => + info.degradation_percentage + case _ => + 1f + } + if (p.exosuit == ExoSuitType.MAX) { + (damage * degradation * aggravation.max_factor) toInt + } else { + val resist = data.damage_model.ResistUsing(data)(data) + //add resist to offset resist subtraction later + if (damage > resist) { + ((damage - resist) * degradation).toInt + resist + } else { + (damage * degradation).toInt + resist + } + } + case _ => + 0 + } + } else { + damage + } + } + + /** + * For damage application that involves aggravation of a fireball (Dragon secondary fire mode), + * perform 1 damage. + * @see `ResolvedProjectile` + */ + case object FireballAggravatedBurn extends Mod { + def Calculate: DamageModifiers.Format = formula + + private def formula(damage: Int, data: ResolvedProjectile): Int = { + if (damage > 0 && + data.resolution == ProjectileResolution.AggravatedDirectBurn || + data.resolution == ProjectileResolution.AggravatedSplashBurn) { + //add resist to offset resist subtraction later + 1 + data.damage_model.ResistUsing(data)(data) + } else { + damage + } + } + } + + /** + * The initial application of aggravated damage against an aircraft target. + * Primarily for use in the starfire weapon system. + * @see `AggravatedDamage` + * @see `ProjectileQuality.AggravatesTarget` + * @see `ResolvedProjectile` + */ + case object StarfireAggravated extends Mod { + def Calculate: DamageModifiers.Format = formula + + private def formula(damage: Int, data: ResolvedProjectile): Int = { + if (data.resolution == ProjectileResolution.AggravatedDirect && + data.projectile.quality == ProjectileQuality.AggravatesTarget) { + data.projectile.profile.Aggravated match { + case Some(aggravation) => + aggravation.info.find(_.damage_type == DamageType.Direct) match { + case Some(infos) => + (damage * infos.degradation_percentage + damage) toInt + case _ => + damage + } + case _ => + damage + } + } else { + damage + } + } + } + + /** + * The ongoing application of aggravated damage ticks against an aircraft target. + * Primarily for use in the starfire weapon system. + * This is called "burning" regardless of what the active aura effect actually is. + * @see `AggravatedDamage` + * @see `ProjectileQuality` + * @see `ResolvedProjectile` + */ + case object StarfireAggravatedBurn extends Mod { + def Calculate: DamageModifiers.Format = formula + + private def formula(damage: Int, data: ResolvedProjectile): Int = { + if (data.resolution == ProjectileResolution.AggravatedDirectBurn) { + data.projectile.profile.Aggravated match { + case Some(aggravation) => + aggravation.info.find(_.damage_type == DamageType.Direct) match { + case Some(infos) => + (math.floor(damage * infos.degradation_percentage) * data.projectile.quality.mod) toInt + case _ => + damage + } + case _ => + 0 + } + } else { + damage + } + } + } + + /** + * The initial application of aggravated damage against a target. + * Primarily for use in the comet weapon system. + * @see `AggravatedDamage` + * @see `ProjectileQuality.AggravatesTarget` + * @see `ResolvedProjectile` + */ + case object CometAggravated extends Mod { + def Calculate: DamageModifiers.Format = formula + + private def formula(damage: Int, data: ResolvedProjectile): Int = { + if (data.resolution == ProjectileResolution.AggravatedDirect && + data.projectile.quality == ProjectileQuality.AggravatesTarget) { + data.projectile.profile.Aggravated match { + case Some(aggravation) => + aggravation.info.find(_.damage_type == DamageType.Direct) match { + case Some(infos) => + damage - (damage * infos.degradation_percentage) toInt + case _ => + damage + } + case _ => + damage + } + } else { + damage + } + } + } + + /** + * The ongoing application of aggravated damage ticks against a target. + * Primarily for use in the comet weapon system. + * This is called "burning" regardless of what the active aura effect actually is. + * @see `AggravatedDamage` + * @see `ProjectileQuality` + * @see `ResolvedProjectile` + */ + case object CometAggravatedBurn extends Mod { + def Calculate: DamageModifiers.Format = formula + + private def formula(damage: Int, data: ResolvedProjectile): Int = { + if (data.resolution == ProjectileResolution.AggravatedDirectBurn) { + data.projectile.profile.Aggravated match { + case Some(aggravation) => + aggravation.info.find(_.damage_type == DamageType.Direct) match { + case Some(infos) => + damage - (damage * infos.degradation_percentage) toInt + case _ => + damage + } + case _ => + 0 + } + } else { + damage + } + } + } } 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 ea889e1b..0e7c466d 100644 --- a/src/main/scala/net/psforever/objects/vital/resistance/ResistanceSelection.scala +++ b/src/main/scala/net/psforever/objects/vital/resistance/ResistanceSelection.scala @@ -2,7 +2,7 @@ package net.psforever.objects.vital.resistance import net.psforever.objects.ballistics._ -import net.psforever.objects.vital.NoResistance +import net.psforever.objects.vital.{DamageType, NoResistance} import net.psforever.objects.vital.projectile.ProjectileCalculations /** @@ -17,19 +17,19 @@ trait ResistanceSelection { def Lash: ProjectileCalculations.Form def Aggravated: ProjectileCalculations.Form - def apply(data: ResolvedProjectile): ProjectileCalculations.Form = - data.resolution match { - case ProjectileResolution.Hit => Direct - case ProjectileResolution.Splash => Splash - case ProjectileResolution.Lash => Lash - case _ => None - } + def apply(data : ResolvedProjectile) : ProjectileCalculations.Form = data.projectile.profile.ProjectileDamageType match { + case DamageType.Direct => Direct + case DamageType.Splash => Splash + case DamageType.Lash => Lash + case DamageType.Aggravated => Aggravated + case _ => None + } - def apply(res: ProjectileResolution.Value): ProjectileCalculations.Form = - res match { - case ProjectileResolution.Hit => Direct - case ProjectileResolution.Splash => Splash - case ProjectileResolution.Lash => Lash - case _ => None - } + def apply(res : DamageType.Value) : ProjectileCalculations.Form = res match { + case DamageType.Direct => Direct + case DamageType.Splash => Splash + case DamageType.Lash => Lash + case DamageType.Aggravated => Aggravated + case _ => 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 5067a74e..9166f7d0 100644 --- a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala +++ b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala @@ -38,15 +38,31 @@ object ResolutionCalculations { def NoDamage(data: ResolvedProjectile)(a: Int, b: Int): Int = 0 - def InfantryDamageAfterResist(data: ResolvedProjectile): (Int, Int) => (Int, Int) = { + def InfantryDamage(data: ResolvedProjectile): (Int, Int) => (Int, Int) = { data.target match { case target: PlayerSource => - InfantryDamageAfterResist(target.health, target.armor) + if(data.projectile.profile.DamageToHealthOnly) { + DamageToHealthOnly(target.health) + } else { + InfantryDamageAfterResist(target.health, target.armor) + } case _ => InfantryDamageAfterResist(0, 0) } } + def DamageToHealthOnly(currentHP: Int)(damages: Int, resistance: Int): (Int, Int) = { + if (damages > 0 && currentHP > 0) { + if(damages > resistance) { + (damages - resistance, 0) + } else { + (damages, 0) + } + } else { + (0, 0) + } + } + def InfantryDamageAfterResist(currentHP: Int, currentArmor: Int)(damages: Int, resistance: Int): (Int, Int) = { if (damages > 0 && currentHP > 0) { if (currentArmor <= 0) { @@ -67,10 +83,14 @@ object ResolutionCalculations { } } - def MaxDamageAfterResist(data: ResolvedProjectile): (Int, Int) => (Int, Int) = { + def MaxDamage(data: ResolvedProjectile): (Int, Int) => (Int, Int) = { data.target match { case target: PlayerSource => - MaxDamageAfterResist(target.health, target.armor) + if(data.projectile.profile.DamageToHealthOnly) { + DamageToHealthOnly(target.health) + } else { + MaxDamageAfterResist(target.health, target.armor) + } case _ => MaxDamageAfterResist(0, 0) } diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 5f17e46b..debbf627 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -451,7 +451,7 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { * `false`, if the new pool can not be created because the system has already been started */ def AddPool(name: String, pool: Seq[Int]): Boolean = { - if (accessor == Default.Actor) { + if (accessor == Default.Actor || accessor == null) { guid.AddPool(name, pool.toList) true } else { diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index b77e183b..a3a7256a 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -255,7 +255,7 @@ object GamePacketOpcode extends Enumeration { // 0x68 case 0x68 => game.DroppodFreefallingMessage.decode case 0x69 => game.AvatarFirstTimeEventMessage.decode - case 0x6a => noDecoder(AggravatedDamageMessage) + case 0x6a => game.AggravatedDamageMessage.decode case 0x6b => game.TriggerSoundMessage.decode case 0x6c => game.LootItemMessage.decode case 0x6d => game.VehicleSubStateMessage.decode diff --git a/src/main/scala/net/psforever/packet/game/AggravatedDamageMessage.scala b/src/main/scala/net/psforever/packet/game/AggravatedDamageMessage.scala new file mode 100644 index 00000000..2456bee2 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/AggravatedDamageMessage.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.PlanetSideGUID +import scodec.Codec +import scodec.codecs._ + +/** + * Dispatched from the server to cause a damage reaction from a specific target. + * Infantry targets should be the primary target of this packet, as indicated by their identifier. + * Infantry targets display their flinch animation. + * All targets yelp in agony. + * Infantry targets use their assigned voice. + * Non-infantry targets use the "grizzled"(?) voice. + * @param guid the target entity's global unique identifier + * @param damage the amount of damsge being simulated + */ +final case class AggravatedDamageMessage(guid : PlanetSideGUID, + damage : Long) + extends PlanetSideGamePacket { + type Packet = AggravatedDamageMessage + def opcode = GamePacketOpcode.AggravatedDamageMessage + def encode = AggravatedDamageMessage.encode(this) +} + +object AggravatedDamageMessage extends Marshallable[AggravatedDamageMessage] { + implicit val codec : Codec[AggravatedDamageMessage] = ( + ("guid" | PlanetSideGUID.codec) :: + ("damage" | uint32L) + ).as[AggravatedDamageMessage] +} diff --git a/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala b/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala index 181a67b2..f88bcf03 100644 --- a/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala +++ b/src/main/scala/net/psforever/packet/game/DamageFeedbackMessage.scala @@ -19,7 +19,7 @@ import shapeless.{::, HNil} * @param unk3b if no global unique identifier (above), the name of the entity absorbing the damage * @param unk3c if no global unique identifier (above), the object type of the entity absorbing the damage * @param unk3d na - * @param unk4 na + * @param unk4 an indicator for the target-specific vital statistic being affected * @param unk5 the amount of damage * @param unk6 na */ @@ -66,6 +66,13 @@ final case class DamageFeedbackMessage( } object DamageFeedbackMessage extends Marshallable[DamageFeedbackMessage] { + def apply(unk1: Int, + unk2: PlanetSideGUID, + unk3: PlanetSideGUID, + unk4: Int, + unk5: Long): DamageFeedbackMessage = + DamageFeedbackMessage(unk1, true, Some(unk2), None, None, true, Some(unk3), None, None, None, unk4, unk5, 0) + implicit val codec: Codec[DamageFeedbackMessage] = ( ("unk1" | uint4) :: (bool >>:~ { u2 => diff --git a/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala b/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala index 8f2a09e1..d6e4a2df 100644 --- a/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala @@ -5,10 +5,13 @@ import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacke import net.psforever.types.Vector3 import scodec.Codec import scodec.codecs._ +import shapeless.{::, HNil} /** * Dispatched by the server to indicate a source of damage affecting the player. - * Unlike `HitHint` the damage source is defined by an actual coordinate location rather than a physical target.
+ * Unlike `HitHint` the damage source is defined by an actual coordinate location rather than a physical target. + * Setting the position to the world origin, however, + * can cause the damage tick mark to point towards the previous damaging entity in some situations.
*
* The player will be shown a fading, outwards drifting, red tick mark. * The location will indicate a general direction towards the source. @@ -27,5 +30,14 @@ object DamageWithPositionMessage extends Marshallable[DamageWithPositionMessage] implicit val codec: Codec[DamageWithPositionMessage] = ( ("unk" | uint8L) :: ("pos" | Vector3.codec_pos) - ).as[DamageWithPositionMessage] + ).xmap[DamageWithPositionMessage] ( + { + case unk :: pos :: HNil => + DamageWithPositionMessage(math.max(0, math.min(unk, 255)), pos) + }, + { + case DamageWithPositionMessage(unk, pos) => + unk :: pos :: HNil + } + ) } diff --git a/src/test/scala/game/AggravatedDamageMessageTest.scala b/src/test/scala/game/AggravatedDamageMessageTest.scala new file mode 100644 index 00000000..7cc9783a --- /dev/null +++ b/src/test/scala/game/AggravatedDamageMessageTest.scala @@ -0,0 +1,29 @@ +// Copyright (c) 2020 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.PlanetSideGUID +import org.specs2.mutable._ +import scodec.bits._ + +class AggravatedDamageMessageTest extends Specification { + val string = hex"6a350a0e000000" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case AggravatedDamageMessage(guid,unk) => + guid mustEqual PlanetSideGUID(2613) + unk mustEqual 14 + case _ => + ko + } + } + + "encode" in { + val msg = AggravatedDamageMessage(PlanetSideGUID(2613), 14) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} diff --git a/src/test/scala/objects/AuraTest.scala b/src/test/scala/objects/AuraTest.scala new file mode 100644 index 00000000..40aec57c --- /dev/null +++ b/src/test/scala/objects/AuraTest.scala @@ -0,0 +1,285 @@ +// Copyright (c) 2020 PSForever +package objects + +import akka.actor.{Actor, ActorRef, Props} +import akka.testkit.TestProbe +import base.ActorTest +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.aura.AuraEffectBehavior.Target +import net.psforever.objects.serverobject.aura.{Aura, AuraContainer, AuraEffectBehavior} +import net.psforever.types.PlanetSideEmpire +import org.specs2.mutable.Specification + +import scala.concurrent.duration._ + +class AuraContainerTest extends Specification { + "AuraContainer" should { + "have no default effects" in { + new AuraTest.Entity().Aura.isEmpty mustEqual true + } + + "add effects" in { + val obj = new AuraTest.Entity() + obj.Aura.size mustEqual 0 + obj.Aura.contains(Aura.Plasma) mustEqual false + obj.AddEffectToAura(Aura.Plasma) + obj.Aura.size mustEqual 1 + obj.Aura.contains(Aura.Plasma) mustEqual true + } + + "do nothing if adding repeated effects" in { + val obj = new AuraTest.Entity() + obj.Aura.size mustEqual 0 + obj.AddEffectToAura(Aura.Plasma) + obj.Aura.size mustEqual 1 + obj.AddEffectToAura(Aura.Plasma) + obj.Aura.size mustEqual 1 + } + + "remove effects" in { + val obj = new AuraTest.Entity() + obj.Aura.size mustEqual 0 + obj.Aura.contains(Aura.Plasma) mustEqual false + obj.AddEffectToAura(Aura.Plasma) + obj.Aura.size mustEqual 1 + obj.Aura.contains(Aura.Plasma) mustEqual true + obj.RemoveEffectFromAura(Aura.Plasma) + obj.Aura.size mustEqual 0 + obj.Aura.contains(Aura.Plasma) mustEqual false + } + + "do nothing if no effects" in { + val obj = new AuraTest.Entity() + obj.Aura.size mustEqual 0 + obj.Aura.contains(Aura.Plasma) mustEqual false + obj.RemoveEffectFromAura(Aura.Plasma) + obj.Aura.size mustEqual 0 + obj.Aura.contains(Aura.Plasma) mustEqual false + } + + "do nothing if trying to remove wrong effect" in { + val obj = new AuraTest.Entity() + obj.Aura.size mustEqual 0 + obj.Aura.contains(Aura.Plasma) mustEqual false + obj.Aura.contains(Aura.Fire) mustEqual false + obj.AddEffectToAura(Aura.Plasma) + obj.Aura.size mustEqual 1 + obj.Aura.contains(Aura.Plasma) mustEqual true + obj.Aura.contains(Aura.Fire) mustEqual false + obj.RemoveEffectFromAura(Aura.Fire) + obj.Aura.size mustEqual 1 + obj.Aura.contains(Aura.Plasma) mustEqual true + obj.Aura.contains(Aura.Fire) mustEqual false + } + } +} + +class AuraEffectBehaviorInitTest extends ActorTest { + val obj = new AuraTest.Entity() + + "AuraEffectBehavior" should { + "init" in { + obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, ActorRef.noSender), "aura-test-actor") + expectNoMessage(500 milliseconds) + } + } +} + +class AuraEffectBehaviorStartEffectTest extends ActorTest { + val obj = new AuraTest.Entity() + val updateProbe = new TestProbe(system) + obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor") + + "AuraEffectBehavior" should { + "start effect (ends naturally)" in { + assert(obj.Aura.isEmpty) + obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500) + val msg1 = updateProbe.receiveOne(100 milliseconds) + assert( + msg1 match { + case AuraTest.DoUpdateAuraEffect() => true + case _ => false + } + ) + assert(obj.Aura.contains(Aura.Plasma)) + expectNoMessage(2000 milliseconds) + assert(obj.Aura.contains(Aura.Plasma)) + val msg2 = updateProbe.receiveOne(750 milliseconds) + assert( + msg2 match { + case AuraTest.DoUpdateAuraEffect() => true + case _ => false + } + ) + assert(obj.Aura.isEmpty) + } + } +} + +class AuraEffectBehaviorStartLongerEffectTest extends ActorTest { + val obj = new AuraTest.Entity() + val updateProbe = new TestProbe(system) + obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor") + + "AuraEffectBehavior" should { + "replace a shorter effect with a longer one" in { + assert(obj.Aura.isEmpty) + obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 500) + val msg1 = updateProbe.receiveOne(100 milliseconds) + assert( + msg1 match { + case AuraTest.DoUpdateAuraEffect() => true + case _ => false + } + ) + assert(obj.Aura.contains(Aura.Plasma)) + obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500) + updateProbe.expectNoMessage(2000 milliseconds) + //first effect has not ended naturally (yet) + assert(obj.Aura.contains(Aura.Plasma)) + } + } +} + +class AuraEffectBehaviorNoRedundantStartEffectTest extends ActorTest { + val obj = new AuraTest.Entity() + val updateProbe = new TestProbe(system) + obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor") + + "AuraEffectBehavior" should { + "not start an effect if already active" in { + assert(obj.Aura.isEmpty) + obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500) + val msg1 = updateProbe.receiveOne(100 milliseconds) + assert( + msg1 match { + case AuraTest.DoUpdateAuraEffect() => true + case _ => false + } + ) + assert(obj.Aura.contains(Aura.Plasma)) + expectNoMessage(1000 milliseconds) //wait for half of the effect's duration + assert(obj.Aura.contains(Aura.Plasma)) + obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500) + updateProbe.expectNoMessage(1500 milliseconds) + } + } +} + +class AuraEffectBehaviorNoOverrideStartEffectTest extends ActorTest { + val obj = new AuraTest.Entity() + val updateProbe = new TestProbe(system) + obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor") + + "AuraEffectBehavior" should { + "not replace a long-running effect with a short-running effect" in { + assert(obj.Aura.isEmpty) + obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500) + val msg1 = updateProbe.receiveOne(100 milliseconds) + assert( + msg1 match { + case AuraTest.DoUpdateAuraEffect() => true + case _ => false + } + ) + assert(obj.Aura.contains(Aura.Plasma)) + obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 500) + updateProbe.expectNoMessage(1500 milliseconds) + //effect has not ended naturally + assert(obj.Aura.contains(Aura.Plasma)) + } + } +} + +class AuraEffectBehaviorNoStartUnsupportedEffectTest extends ActorTest { + val obj = new AuraTest.Entity() + val updateProbe = new TestProbe(system) + obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor") //supports Plasma only + + "AuraEffectBehavior" should { + "not start an effect that is not approved" in { + assert(obj.Aura.isEmpty) + obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Fire, 2500) + assert(obj.Aura.isEmpty) + updateProbe.expectNoMessage(2000 milliseconds) + } + } +} + + + +class AuraEffectBehaviorEndEarlyTest extends ActorTest { + val obj = new AuraTest.Entity() + val updateProbe = new TestProbe(system) + obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor") + + "AuraEffectBehavior" should { + "start effect (ends early)" in { + assert(obj.Aura.isEmpty) + obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500) + val msg1 = updateProbe.receiveOne(100 milliseconds) + assert( + msg1 match { + case AuraTest.DoUpdateAuraEffect() => true + case _ => false + } + ) + assert(obj.Aura.contains(Aura.Plasma)) + obj.Actor ! AuraEffectBehavior.EndEffect(Aura.Plasma) + val msg2 = updateProbe.receiveOne(100 milliseconds) + assert( + msg2 match { + case AuraTest.DoUpdateAuraEffect() => true + case _ => false + } + ) + assert(obj.Aura.isEmpty) + } + } +} + +class AuraEffectBehaviorEndNothingTest extends ActorTest { + val obj = new AuraTest.Entity() + val updateProbe = new TestProbe(system) + obj.Actor = system.actorOf(Props(classOf[AuraTest.Agency], obj, updateProbe.ref), "aura-test-actor") + + "AuraEffectBehavior" should { + "can not end an effect that is not supported (hence, not started)" in { + assert(obj.Aura.isEmpty) + obj.Actor ! AuraEffectBehavior.StartEffect(Aura.Plasma, 2500) + val msg1 = updateProbe.receiveOne(100 milliseconds) + assert( + msg1 match { + case AuraTest.DoUpdateAuraEffect() => true + case _ => false + } + ) + assert(obj.Aura.size == 1) + obj.Actor ! AuraEffectBehavior.EndEffect(Aura.Fire) + updateProbe.expectNoMessage(1000 milliseconds) + assert(obj.Aura.size == 1) + } + } +} + +object AuraTest { + class Agency(obj: AuraEffectBehavior.Target, updateRef: ActorRef) extends Actor with AuraEffectBehavior { + def AuraTargetObject : Target = obj + ApplicableEffect(Aura.Plasma) + + def receive: Receive = auraBehavior.orElse { + case _ => ; + } + + def UpdateAuraEffect(target : Target) : Unit = { + updateRef ! DoUpdateAuraEffect() + } + } + + class Entity extends PlanetSideServerObject with AuraContainer { + def Faction = PlanetSideEmpire.NEUTRAL + def Definition = null + } + + final case class DoUpdateAuraEffect() +} diff --git a/src/test/scala/objects/DamageModelTests.scala b/src/test/scala/objects/DamageModelTests.scala index 06f461a3..656e91fc 100644 --- a/src/test/scala/objects/DamageModelTests.scala +++ b/src/test/scala/objects/DamageModelTests.scala @@ -28,7 +28,7 @@ class DamageCalculationsTests extends Specification { val target = Vehicle(GlobalDefinitions.fury) target.Position = Vector3(10, 0, 0) val resprojectile = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target), target.DamageModel, @@ -64,14 +64,7 @@ class DamageCalculationsTests extends Specification { } "degrade over distance damage modifier (no degrade)" in { - val resprojectile2 = ResolvedProjectile( - ProjectileResolution.Splash, - projectile, - SourceEntry(target), - target.DamageModel, - Vector3(10, 0, 0) - ) - DamageModifiers.DistanceDegrade.Calculate(100, resprojectile2) == 100 mustEqual true + DamageModifiers.DistanceDegrade.Calculate(100, resprojectile) == 100 mustEqual true } "degrade over distance damage modifier (some degrade)" in { @@ -109,7 +102,14 @@ class DamageCalculationsTests extends Specification { } "degrade at radial distance damage modifier (some degrade)" in { - val damage = DamageModifiers.RadialDegrade.Calculate(100, resprojectile) + val resprojectile2 = ResolvedProjectile( + ProjectileResolution.Splash, + projectile, + SourceEntry(target), + target.DamageModel, + Vector3(12, 0, 0) + ) + val damage = DamageModifiers.RadialDegrade.Calculate(100, resprojectile2) damage < 100 && damage > 0 mustEqual true } @@ -119,7 +119,7 @@ class DamageCalculationsTests extends Specification { projectile, SourceEntry(target), target.DamageModel, - Vector3(1000, 0, 0) + Vector3(100, 0, 0) ) DamageModifiers.RadialDegrade.Calculate(100, resprojectile2) == 0 mustEqual true } @@ -180,7 +180,7 @@ class ResistanceCalculationsTests extends Specification { "ignore all targets" in { val target = Vehicle(GlobalDefinitions.fury) val resprojectile = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target), target.DamageModel, @@ -192,7 +192,7 @@ class ResistanceCalculationsTests extends Specification { "discern standard infantry targets" in { val target = player val resprojectile = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target), target.DamageModel, @@ -208,7 +208,7 @@ class ResistanceCalculationsTests extends Specification { val target = Player(Avatar(0, "TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) target.ExoSuit = ExoSuitType.MAX val resprojectile = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target), target.DamageModel, @@ -223,7 +223,7 @@ class ResistanceCalculationsTests extends Specification { "discern ground vehicle targets" in { val target = Vehicle(GlobalDefinitions.fury) val resprojectile = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target), target.DamageModel, @@ -238,7 +238,7 @@ class ResistanceCalculationsTests extends Specification { "discern flying vehicle targets" in { val target = Vehicle(GlobalDefinitions.mosquito) val resprojectile = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target), target.DamageModel, @@ -284,7 +284,7 @@ class ResolutionCalculationsTests extends Specification { "calculate no damage" in { val target = player val resprojectile = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target), target.DamageModel, @@ -296,23 +296,23 @@ class ResolutionCalculationsTests extends Specification { "calculate no infantry damage for vehicles" in { val target1 = Vehicle(GlobalDefinitions.fury) //! val resprojectile1 = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target1), target1.DamageModel, Vector3.Zero ) - InfantryDamageAfterResist(resprojectile1)(50, 10) mustEqual (0, 0) + InfantryDamage(resprojectile1)(50, 10) mustEqual (0, 0) val target2 = player val resprojectile2 = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target2), target2.DamageModel, Vector3.Zero ) - InfantryDamageAfterResist(resprojectile2)(50, 10) mustEqual (40, 10) + InfantryDamage(resprojectile2)(50, 10) mustEqual (40, 10) } "calculate health and armor damage for infantry target" in { @@ -340,23 +340,23 @@ class ResolutionCalculationsTests extends Specification { "calculate no max damage for vehicles" in { val target1 = Vehicle(GlobalDefinitions.fury) //! val resprojectile1 = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target1), target1.DamageModel, Vector3.Zero ) - MaxDamageAfterResist(resprojectile1)(50, 10) mustEqual (0, 0) + MaxDamage(resprojectile1)(50, 10) mustEqual (0, 0) val target2 = player2 val resprojectile2 = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target2), target2.DamageModel, Vector3.Zero ) - MaxDamageAfterResist(resprojectile2)(50, 10) mustEqual (0, 40) + MaxDamage(resprojectile2)(50, 10) mustEqual (0, 40) } "calculate health and armor damage for max target" in { @@ -376,7 +376,7 @@ class ResolutionCalculationsTests extends Specification { "do not care if target is infantry for vehicle calculations" in { val target1 = player val resprojectile1 = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target1), target1.DamageModel, @@ -386,7 +386,7 @@ class ResolutionCalculationsTests extends Specification { val target2 = Vehicle(GlobalDefinitions.fury) //! val resprojectile2 = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(target2), target2.DamageModel, @@ -450,10 +450,9 @@ class DamageModelTests extends Specification { Vector3.Zero ) val func: Any => ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile) - func(tplayer) - tplayer.Health mustEqual 54 - tplayer.Armor mustEqual 46 + tplayer.Health mustEqual 65 + tplayer.Armor mustEqual 35 } "resolve infantry targets in a specific way" in { @@ -471,8 +470,7 @@ class DamageModelTests extends Specification { Vector3.Zero ) val func: Any => ResolvedProjectile = - resprojectile.damage_model.Calculate(resprojectile, ProjectileResolution.Splash) - + resprojectile.damage_model.Calculate(resprojectile, DamageType.Splash) func(tplayer) tplayer.Health mustEqual 65 tplayer.Armor mustEqual 35 @@ -572,7 +570,7 @@ class DamageModelTests extends Specification { Vector3.Zero ) val func: Any => ResolvedProjectile = - resprojectile.damage_model.Calculate(resprojectile, ProjectileResolution.Splash) + resprojectile.damage_model.Calculate(resprojectile, DamageType.Splash) func(vehicle) vehicle.Health mustEqual 518 diff --git a/src/test/scala/objects/DamageableTest.scala b/src/test/scala/objects/DamageableTest.scala index 6cce2e8b..d5973874 100644 --- a/src/test/scala/objects/DamageableTest.scala +++ b/src/test/scala/objects/DamageableTest.scala @@ -40,7 +40,7 @@ class DamageableTest extends Specification { "permit damage" in { val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), SourceEntry(target), target.DamageModel, @@ -53,7 +53,7 @@ class DamageableTest extends Specification { "ignore attempts at non-zero damage" in { val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectileA, weaponA.Definition, @@ -78,7 +78,7 @@ class DamageableTest extends Specification { Faction = player1.Faction } val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), SourceEntry(target), target.DamageModel, @@ -101,7 +101,7 @@ class DamageableTest extends Specification { Faction = PlanetSideEmpire.NC } val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), SourceEntry(target), target.DamageModel, @@ -128,7 +128,7 @@ class DamageableTest extends Specification { Faction = player1.Faction } val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), SourceEntry(target), target.DamageModel, @@ -152,7 +152,7 @@ class DamageableTest extends Specification { "permit jamming" in { val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), SourceEntry(target), target.DamageModel, @@ -166,7 +166,7 @@ class DamageableTest extends Specification { "ignore attempts at jamming if the projectile is does not cause the effect" in { val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectileA, weaponA.Definition, weaponA.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), SourceEntry(target), target.DamageModel, @@ -181,7 +181,7 @@ class DamageableTest extends Specification { val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) target.Faction = player1.Faction val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), SourceEntry(target), target.DamageModel, @@ -196,7 +196,7 @@ class DamageableTest extends Specification { "ignore attempts at jamming targets that are not jammable" in { val target = new TrapDeployable(GlobalDefinitions.tank_traps) val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), SourceEntry(target), target.DamageModel, @@ -216,7 +216,7 @@ class DamageableTest extends Specification { val target = new SensorDeployable(GlobalDefinitions.motionalarmsensor) target.Faction = player1.Faction val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectileB, weaponB.Definition, weaponB.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), SourceEntry(target), target.DamageModel, @@ -271,7 +271,7 @@ class DamageableEntityDamageTest extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -342,7 +342,7 @@ class DamageableEntityDestroyedTest extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -416,7 +416,7 @@ class DamageableEntityNotDestroyTwice extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -487,7 +487,7 @@ class DamageableAmenityTest extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -580,7 +580,7 @@ class DamageableMountableDamageTest extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -626,7 +626,7 @@ class DamageableMountableDamageTest extends ActorTest { msg1_3(1) match { case AvatarServiceMessage( "TestCharacter2", - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 2, 2))) + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(_, Vector3(2, 2, 2))) ) => true case _ => false @@ -675,7 +675,7 @@ class DamageableMountableDestroyTest extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -737,8 +737,10 @@ class DamageableWeaponTurretDamageTest extends ActorTest { } val activityProbe = TestProbe() val avatarProbe = TestProbe() + val vehicleProbe = TestProbe() zone.Activity = activityProbe.ref zone.AvatarEvents = avatarProbe.ref + zone.VehicleEvents = vehicleProbe.ref val turret = new TurretDeployable(GlobalDefinitions.portable_manned_turret_tr) //2 turret.Actor = system.actorOf(Props(classOf[TurretControl], turret), "turret-control") turret.Zone = zone @@ -787,16 +789,17 @@ class DamageableWeaponTurretDamageTest extends ActorTest { assert(turret.Health == turret.Definition.DefaultHealth) turret.Actor ! Vitality.Damage(applyDamageTo) - val msg1_3 = avatarProbe.receiveN(2, 500 milliseconds) - val msg2 = activityProbe.receiveOne(500 milliseconds) + val msg12 = vehicleProbe.receiveOne(500 milliseconds) + val msg3 = activityProbe.receiveOne(500 milliseconds) + val msg4 = avatarProbe.receiveOne(500 milliseconds) assert( - msg1_3.head match { - case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true - case _ => false + msg12 match { + case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(2), 0, _)) => true + case _ => false } ) assert( - msg2 match { + msg3 match { case activity: Zone.HotSpot.Activity => activity.attacker == PlayerSource(player1) && activity.defender == turretSource && @@ -805,10 +808,10 @@ class DamageableWeaponTurretDamageTest extends ActorTest { } ) assert( - msg1_3(1) match { + msg4 match { case AvatarServiceMessage( "TestCharacter2", - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 2, 2))) + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(_, Vector3(2, 2, 2))) ) => true case _ => false @@ -862,7 +865,7 @@ class DamageableWeaponTurretJammerTest extends ActorTest { val projectile = weapon.Projectile val turretSource = SourceEntry(turret) val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -964,7 +967,7 @@ class DamageableWeaponTurretDestructionTest extends ActorTest { val weaponA = Tool(GlobalDefinitions.jammer_grenade) val projectileA = weaponA.Projectile val resolvedA = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectileA, weaponA.Definition, @@ -983,7 +986,7 @@ class DamageableWeaponTurretDestructionTest extends ActorTest { val weaponB = Tool(GlobalDefinitions.phoenix) //decimator val projectileB = weaponB.Projectile val resolvedB = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectileB, weaponB.Definition, @@ -1046,8 +1049,8 @@ class DamageableWeaponTurretDestructionTest extends ActorTest { ) assert( msg56.head match { - case VehicleServiceMessage.TurretUpgrade(SupportActor.ClearSpecific(List(t), _)) if t eq turret => true - case _ => false + case VehicleServiceMessage.TurretUpgrade(SupportActor.ClearSpecific(List(t), _)) if turret eq t => true + case _ => false } ) assert( @@ -1130,40 +1133,36 @@ class DamageableVehicleDamageTest extends ActorTest { assert(atv.Shields == 1) atv.Actor ! Vitality.Damage(applyDamageTo) - val msg1_3 = avatarProbe.receiveN(2, 500 milliseconds) - val msg2 = activityProbe.receiveOne(200 milliseconds) - val msg4 = vehicleProbe.receiveOne(200 milliseconds) + val msg12 = vehicleProbe.receiveN(2, 200 milliseconds) + val msg3 = activityProbe.receiveOne(200 milliseconds) + val msg4 = avatarProbe.receiveOne(200 milliseconds) assert( - msg1_3.head match { - case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true - case _ => false + msg12.head match { + case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)) => true + case _ => false } ) assert( - msg2 match { + msg12(1) match { + case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 0, _)) => true + case _ => false + } + ) + assert( + msg3 match { case activity: Zone.HotSpot.Activity => activity.attacker == PlayerSource(player1) && - activity.defender == vehicleSource && - activity.location == Vector3(1, 0, 0) - case _ => false - } - ) - assert( - msg1_3(1) match { - case AvatarServiceMessage( - "TestCharacter2", - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0))) - ) => - true + activity.defender == vehicleSource && + activity.location == Vector3(1, 0, 0) case _ => false } ) assert( msg4 match { - case VehicleServiceMessage( - channel, - VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _) - ) if channel.equals(atv.Actor.toString) => + case AvatarServiceMessage( + "TestCharacter2", + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(9, Vector3(2, 0, 0))) + ) => true case _ => false } @@ -1264,17 +1263,23 @@ class DamageableVehicleDamageMountedTest extends ActorTest { assert(atv.Shields == 1) lodestar.Actor ! Vitality.Damage(applyDamageTo) - val msg1_35 = avatarProbe.receiveN(3, 500 milliseconds) - val msg2 = activityProbe.receiveOne(200 milliseconds) - val msg4 = vehicleProbe.receiveOne(200 milliseconds) + val msg12 = vehicleProbe.receiveN(2, 200 milliseconds) + val msg3 = activityProbe.receiveOne(200 milliseconds) + val msg45 = avatarProbe.receiveN(2,200 milliseconds) assert( - msg1_35.head match { - case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(1), 0, _)) => true - case _ => false + msg12.head match { + case VehicleServiceMessage(_, VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _)) => true + case _ => false } ) assert( - msg2 match { + msg12(1) match { + case VehicleServiceMessage("test", VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 0, _)) => true + case _ => false + } + ) + assert( + msg3 match { case activity: Zone.HotSpot.Activity => activity.attacker == PlayerSource(player1) && activity.defender == vehicleSource && @@ -1283,30 +1288,20 @@ class DamageableVehicleDamageMountedTest extends ActorTest { } ) assert( - msg1_35(1) match { + msg45.head match { case AvatarServiceMessage( "TestCharacter2", - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0))) + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(400, Vector3(2, 0, 0))) ) => true case _ => false } ) assert( - msg4 match { - case VehicleServiceMessage( - channel, - VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), PlanetSideGUID(1), 68, _) - ) if channel.equals(lodestar.Actor.toString) => - true - case _ => false - } - ) - assert( - msg1_35(2) match { + msg45(1) match { case AvatarServiceMessage( "TestCharacter3", - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0))) + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(0, Vector3(2, 0, 0))) ) => true case _ => false @@ -1385,7 +1380,7 @@ class DamageableVehicleJammeringMountedTest extends ActorTest { val weapon = Tool(GlobalDefinitions.jammer_grenade) val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -1601,7 +1596,7 @@ class DamageableVehicleDestroyMountedTest extends ActorTest { val weaponA = Tool(GlobalDefinitions.jammer_grenade) val projectileA = weaponA.Projectile val resolvedA = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectileA, weaponA.Definition, @@ -1620,7 +1615,7 @@ class DamageableVehicleDestroyMountedTest extends ActorTest { val weaponB = Tool(GlobalDefinitions.phoenix) //decimator val projectileB = weaponB.Projectile val resolvedB = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectileB, weaponB.Definition, diff --git a/src/test/scala/objects/DeployableTest.scala b/src/test/scala/objects/DeployableTest.scala index e96e4cfe..39ab6107 100644 --- a/src/test/scala/objects/DeployableTest.scala +++ b/src/test/scala/objects/DeployableTest.scala @@ -334,7 +334,7 @@ class ExplosiveDeployableJammerTest extends ActorTest { val pSource = PlayerSource(player1) val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), jMineSource, j_mine.DamageModel, @@ -374,7 +374,7 @@ class ExplosiveDeployableJammerTest extends ActorTest { assert( msg_local(2) match { case LocalServiceMessage.Deployables(SupportActor.ClearSpecific(List(target), _zone)) => - (target eq j_mine) && (_zone eq zone) + (j_mine eq target) && (_zone eq zone) case _ => false } ) @@ -432,7 +432,7 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest { val pSource = PlayerSource(player1) val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), hMineSource, h_mine.DamageModel, @@ -478,7 +478,7 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest { assert( msg_local(3) match { case LocalServiceMessage.Deployables(SupportActor.ClearSpecific(List(target), _zone)) => - (target eq h_mine) && (_zone eq zone) + (h_mine eq target) && (_zone eq zone) case _ => false } ) @@ -542,7 +542,7 @@ class ExplosiveDeployableDestructionTest extends ActorTest { val pSource = PlayerSource(player1) val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile(projectile, weapon.Definition, weapon.FireMode, pSource, 0, Vector3.Zero, Vector3.Zero), hMineSource, h_mine.DamageModel, @@ -584,7 +584,7 @@ class ExplosiveDeployableDestructionTest extends ActorTest { assert( msg_local(2) match { case LocalServiceMessage.Deployables(SupportActor.ClearSpecific(List(target), _zone)) => - (target eq h_mine) && (_zone eq zone) + (h_mine eq target) && (_zone eq zone) case _ => false } ) diff --git a/src/test/scala/objects/GeneratorTest.scala b/src/test/scala/objects/GeneratorTest.scala index d334f156..4d53ae41 100644 --- a/src/test/scala/objects/GeneratorTest.scala +++ b/src/test/scala/objects/GeneratorTest.scala @@ -80,7 +80,7 @@ class GeneratorControlDamageTest extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -162,7 +162,7 @@ class GeneratorControlCriticalTest extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -253,7 +253,7 @@ class GeneratorControlDestroyedTest extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -384,7 +384,7 @@ class GeneratorControlKillsTest extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -507,7 +507,7 @@ class GeneratorControlNotDestroyTwice extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -597,7 +597,7 @@ class GeneratorControlNotDamageIfExplodingTest extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, @@ -691,7 +691,7 @@ class GeneratorControlNotRepairIfExplodingTest extends ActorTest { val weapon = Tool(GlobalDefinitions.phoenix) //decimator val projectile = weapon.Projectile val resolved = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, Projectile( projectile, weapon.Definition, diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala index 56b34460..9b261bdf 100644 --- a/src/test/scala/objects/PlayerControlTest.scala +++ b/src/test/scala/objects/PlayerControlTest.scala @@ -413,14 +413,13 @@ class PlayerControlDamageTest extends ActorTest { ) assert( msg_avatar(1) match { - case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true + case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => true case _ => false } ) assert( msg_avatar(2) match { - case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 2, _)) => - true + case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true case _ => false } ) @@ -437,7 +436,7 @@ class PlayerControlDamageTest extends ActorTest { msg_avatar(3) match { case AvatarServiceMessage( "TestCharacter2", - AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(10, Vector3(2, 0, 0))) + AvatarAction.SendResponse(Service.defaultPlayerGUID, DamageWithPositionMessage(17, Vector3(2, 0, 0))) ) => true case _ => false diff --git a/src/test/scala/objects/ProjectileTest.scala b/src/test/scala/objects/ProjectileTest.scala index 414e7d85..4d96ccd1 100644 --- a/src/test/scala/objects/ProjectileTest.scala +++ b/src/test/scala/objects/ProjectileTest.scala @@ -358,7 +358,6 @@ class ProjectileTest extends Specification { fury_dm, Vector3(1.2f, 3.4f, 5.6f) ) - obj.resolution mustEqual ProjectileResolution.Hit obj.projectile mustEqual projectile obj.target mustEqual p2_source obj.damage_model mustEqual fury.DamageModel diff --git a/src/test/scala/objects/VehicleTest.scala b/src/test/scala/objects/VehicleTest.scala index 9992e8fb..7bd0fbf7 100644 --- a/src/test/scala/objects/VehicleTest.scala +++ b/src/test/scala/objects/VehicleTest.scala @@ -997,7 +997,7 @@ class VehicleControlShieldsNotChargingTooEarlyTest extends ActorTest { // val p_source = PlayerSource( Player(Avatar(0, "TestTarget", PlanetSideEmpire.NC, CharacterGender.Female, 1, CharacterVoice.Mute)) ) // val projectile = Projectile(beamer_wep.Projectile, GlobalDefinitions.beamer, beamer_wep.FireMode, p_source, GlobalDefinitions.beamer.ObjectId, Vector3.Zero, Vector3.Zero) // val fury_dm = Vehicle(GlobalDefinitions.fury).DamageModel -// val obj = ResolvedProjectile(ProjectileResolution.Hit, projectile, p_source, fury_dm, Vector3(1.2f, 3.4f, 5.6f)) +// val obj = ResolvedProjectile(projectile, p_source, fury_dm, Vector3(1.2f, 3.4f, 5.6f)) // // "not charge vehicle shields if recently damaged" in { // assert(vehicle.Shields == 0) diff --git a/src/test/scala/objects/VitalityTest.scala b/src/test/scala/objects/VitalityTest.scala index f5b1a74e..ee459f06 100644 --- a/src/test/scala/objects/VitalityTest.scala +++ b/src/test/scala/objects/VitalityTest.scala @@ -21,7 +21,7 @@ class VitalityTest extends Specification { val pSource = PlayerSource(player) val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero) val resprojectile = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(player), player.DamageModel, @@ -69,7 +69,7 @@ class VitalityTest extends Specification { val pSource = PlayerSource(player) val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero) val resprojectile = ResolvedProjectile( - ProjectileResolution.Splash, + ProjectileResolution.Hit, projectile, SourceEntry(player), player.DamageModel, diff --git a/src/test/scala/objects/ZoneTest.scala b/src/test/scala/objects/ZoneTest.scala index 520ad0c4..7ac0988e 100644 --- a/src/test/scala/objects/ZoneTest.scala +++ b/src/test/scala/objects/ZoneTest.scala @@ -110,7 +110,8 @@ class ZoneTest extends Specification { zone.AddPool("pool2", (51 to 75).toList) val obj = new TestObject() - guid1.register(obj, "pool2").isSuccess mustEqual true + val registration = guid1.register(obj, "pool2") + registration.isSuccess mustEqual true guid1.WhichPool(obj).contains("pool2") mustEqual true zone.GUID(new NumberPoolHub(new LimitedNumberSource(150))) mustEqual false @@ -214,13 +215,15 @@ class ZonePopulationTest extends ActorTest { override def SetupNumberPools() = {} } val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val player = Player(avatar) + player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) expectNoMessage(200 milliseconds) assert(zone.Players.isEmpty) assert(zone.LivePlayers.isEmpty) zone.Population ! Zone.Population.Join(avatar) - zone.Population ! Zone.Population.Spawn(avatar, Player(avatar), null) + zone.Population ! Zone.Population.Spawn(avatar, player, null) expectNoMessage(Duration.create(200, "ms")) assert(zone.Players.size == 1) assert(zone.Players.head == avatar) @@ -232,10 +235,12 @@ class ZonePopulationTest extends ActorTest { override def SetupNumberPools() = {} } val avatar = Avatar(1, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val player = Player(avatar) + player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) receiveOne(Duration.create(200, "ms")) //consume zone.Population ! Zone.Population.Join(avatar) - zone.Population ! Zone.Population.Spawn(avatar, Player(avatar), null) + zone.Population ! Zone.Population.Spawn(avatar, player, null) expectNoMessage(Duration.create(100, "ms")) assert(zone.Players.size == 1) @@ -251,6 +256,7 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) + player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) expectNoMessage(200 milliseconds) zone.Population ! Zone.Population.Join(avatar) @@ -271,6 +277,7 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) + player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) expectNoMessage(200 milliseconds) zone.Population ! Zone.Population.Join(avatar) @@ -318,7 +325,9 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player1 = Player(avatar) + player1.GUID = PlanetSideGUID(1) val player2 = Player(avatar) + player2.GUID = PlanetSideGUID(2) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) expectNoMessage(200 milliseconds) zone.Population ! Zone.Population.Join(avatar) @@ -344,6 +353,7 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = {} } val avatar = Avatar(0, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) + player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) expectNoMessage(200 milliseconds) @@ -364,10 +374,12 @@ class ZonePopulationTest extends ActorTest { override def SetupNumberPools() = {} } val avatar = Avatar(2, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + val player = Player(avatar) + player.GUID = PlanetSideGUID(1) zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) expectNoMessage(200 milliseconds) zone.Population ! Zone.Population.Join(avatar) - zone.Population ! Zone.Population.Spawn(avatar, Player(avatar), null) + zone.Population ! Zone.Population.Spawn(avatar, player, null) expectNoMessage(Duration.create(100, "ms")) assert(zone.Players.size == 1) @@ -388,6 +400,7 @@ class ZonePopulationTest extends ActorTest { override def SetupNumberPools() = {} } val player = Player(Avatar(3, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + player.GUID = PlanetSideGUID(1) player.Release zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) expectNoMessage(200 milliseconds) @@ -404,6 +417,7 @@ class ZonePopulationTest extends ActorTest { override def SetupNumberPools() = {} } val player = Player(Avatar(4, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + player.GUID = PlanetSideGUID(1) player.Release zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) expectNoMessage(200 milliseconds) @@ -422,10 +436,13 @@ class ZonePopulationTest extends ActorTest { override def SetupNumberPools() = {} } val player1 = Player(Avatar(5, "Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + player1.GUID = PlanetSideGUID(1) player1.Release val player2 = Player(Avatar(6, "Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + player2.GUID = PlanetSideGUID(2) player2.Release val player3 = Player(Avatar(7, "Chord3", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + player3.GUID = PlanetSideGUID(3) player3.Release zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) expectNoMessage(200 milliseconds) @@ -450,6 +467,7 @@ class ZonePopulationTest extends ActorTest { override def SetupNumberPools() = {} } val player = Player(Avatar(8, "Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) + player.GUID = PlanetSideGUID(1) //player.Release !!important zone.actor = system.spawn(ZoneActor(zone), ZoneTest.TestName) expectNoMessage(200 milliseconds)