diff --git a/src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala index 80eac5b79..b7294aca0 100644 --- a/src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala @@ -2,6 +2,8 @@ package net.psforever.actors.session.csr import akka.actor.{ActorContext, typed} +import net.psforever.actors.session.SessionActor +import net.psforever.actors.session.normal.NormalMode import net.psforever.actors.session.support.AvatarHandlerFunctions import net.psforever.login.WorldSession.PutLoadoutEquipmentInInventory import net.psforever.objects.PlanetSideGameObject @@ -9,6 +11,7 @@ import net.psforever.objects.inventory.Container import net.psforever.objects.serverobject.containable.ContainableBehavior import net.psforever.objects.serverobject.mount.Mountable import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction} +import net.psforever.services.avatar.AvatarServiceResponse import net.psforever.types.ImplantType // @@ -54,9 +57,13 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A val isSameTarget = !isNotSameTarget reply match { /* special messages */ + case AvatarResponse.TeardownConnection() if player.spectator => + context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode) + context.self.forward(AvatarServiceResponse(toChannel, guid, reply)) + case AvatarResponse.TeardownConnection() => - log.trace(s"ending ${player.Name}'s old session by event system request (relog)") - context.stop(context.self) + context.self ! SessionActor.SetMode(NormalMode) + context.self.forward(AvatarServiceResponse(toChannel, guid, reply)) /* really common messages (very frequently, every life) */ case pstate @ AvatarResponse.PlayerState( @@ -435,28 +442,38 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) - case AvatarResponse.Killed(mount) => + case AvatarResponse.Killed(_, mount) => //pure logic sessionLogic.shooting.shotsWhileDead = 0 sessionLogic.zoning.CancelZoningProcess() sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive sessionLogic.zoning.zoningStatus = Zoning.Status.None - continent.GUID(mount) - .collect { - case obj: Vehicle if obj.Destroyed => - sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) - sessionLogic.general.unaccessContainer(obj) - case obj: PlanetSideGameObject with Mountable with Container if obj.Destroyed => - sessionLogic.general.unaccessContainer(obj) - case _ => () - } + continent.GUID(mount).collect { + case obj: Vehicle if obj.Destroyed => + ops.killedWhileMounted(obj, resolvedPlayerGuid) + sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) + sessionLogic.general.unaccessContainer(obj) + + case obj: Vehicle => + ops.killedWhileMounted(obj, resolvedPlayerGuid) + sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) + + case obj: PlanetSideGameObject with Mountable with Container if obj.Destroyed => + ops.killedWhileMounted(obj, resolvedPlayerGuid) + sessionLogic.general.unaccessContainer(obj) + + case obj: PlanetSideGameObject with Mountable with Container => + ops.killedWhileMounted(obj, resolvedPlayerGuid) + + case obj: PlanetSideGameObject with Mountable => + ops.killedWhileMounted(obj, resolvedPlayerGuid) + } //player state changes sessionLogic.general.dropSpecialSlotItem() sessionLogic.general.toggleMaxSpecialState(enable = false) player.FreeHand.Equipment.foreach(DropEquipmentFromInventory(player)(_)) AvatarActor.updateToolDischargeFor(avatar) AvatarActor.savePlayerLocation(player) - player.VehicleSeated = None ops.revive(player.GUID) avatarActor ! AvatarActor.InitializeImplants //render diff --git a/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala index ba3b85987..a6b17186b 100644 --- a/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala +++ b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala @@ -8,6 +8,7 @@ import net.psforever.objects.serverobject.ServerObject import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.Zone +import net.psforever.objects.zones.blockmap.BlockMapEntity import net.psforever.packet.game.{ChatMsg, ObjectCreateDetailedMessage, PlanetsideAttributeMessage} import net.psforever.packet.game.objectcreate.RibbonBars import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -57,6 +58,7 @@ class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic { data.keepAlivePersistenceFunc = keepAlivePersistanceCSR // CustomerServiceRepresentativeMode.renderPlayer(data, continent, player) + player.allowInteraction = false if (player.silenced) { data.chat.commandIncomingSilence(session, ChatMsg(ChatMessageType.CMT_SILENCE, "player 0")) } @@ -85,6 +87,7 @@ class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic { data.keepAlivePersistenceFunc = data.keepAlivePersistence // CustomerServiceRepresentativeMode.renderPlayer(data, continent, player) + player.allowInteraction = true data.chat.LeaveChannel(SpectatorChannel) data.chat.LeaveChannel(CustomerServiceChannel) data.sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR MODE OFF")) @@ -106,11 +109,20 @@ class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic { } private def keepAlivePersistanceCSR(): Unit = { + val player = data.player data.keepAlivePersistence() - topOffHealthOfPlayer(data.player) + topOffHealthOfPlayer(player) + player.allowInteraction = false + topOffHealthOfPlayer(player) data.continent.GUID(data.player.VehicleSeated) .collect { - case obj: PlanetSideGameObject with Vitality => topOffHealth(obj) + case obj: PlanetSideGameObject with Vitality with BlockMapEntity => + topOffHealth(obj) + data.updateBlockMap(obj, obj.Position) + obj + } + .getOrElse { + data.updateBlockMap(player, player.Position) } } diff --git a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala index c679577c2..7692a4501 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -71,6 +71,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex ) = pkt sessionLogic.persist() sessionLogic.turnCounterFunc(avatarGuid) + sessionLogic.updateBlockMap(player, pos) //below half health, full heal val maxHealth = player.MaxHealth.toLong if (player.Health < maxHealth) { @@ -165,6 +166,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex ) ) sessionLogic.squad.updateSquad() + player.allowInteraction = false } def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = { diff --git a/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala b/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala index d1fd6e257..592fbedd5 100644 --- a/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala +++ b/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala @@ -2,7 +2,6 @@ package net.psforever.actors.session.csr import net.psforever.actors.session.support.{AvatarHandlerFunctions, ChatFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions} -import net.psforever.actors.zone.ZoneActor import net.psforever.objects.serverobject.ServerObject import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.{Player, Session, Vehicle} @@ -45,10 +44,7 @@ class SpectatorCSRModeLogic(data: SessionData) extends ModeLogic { } // player.spectator = true - //player.bops = true - player.allowInteraction = false data.chat.JoinChannel(SpectatorChannel) - continent.actor ! ZoneActor.RemoveFromBlockMap(player) continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(pguid, pguid)) sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "on")) sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR SPECTATOR MODE ON")) @@ -62,10 +58,7 @@ class SpectatorCSRModeLogic(data: SessionData) extends ModeLogic { val sendResponse: PlanetSidePacket => Unit = data.sendResponse // player.spectator = false - player.bops = false - player.allowInteraction = true data.chat.LeaveChannel(SpectatorChannel) - data.continent.actor ! ZoneActor.AddToBlockMap(player, player.Position) continent.AvatarEvents ! AvatarServiceMessage( continent.id, AvatarAction.LoadPlayer(pguid, avatarId, pguid, player.Definition.Packet.ConstructorData(player).get, None) diff --git a/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala index bea267d6a..4307dce9f 100644 --- a/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala @@ -48,12 +48,12 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex //we're driving the vehicle sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) - topOffHealthOfPlayer() - topOffHealth(obj) sessionLogic.general.fallHeightTracker(pos.z) if (obj.MountedIn.isEmpty) { sessionLogic.updateBlockMap(obj, pos) } + topOffHealthOfPlayer() + topOffHealth(obj) player.Position = pos //convenient if (obj.WeaponControlledFromSeat(0).isEmpty) { player.Orientation = Vector3.z(ang.z) //convenient @@ -96,6 +96,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ) ) sessionLogic.squad.updateSquad() + player.allowInteraction = false obj.zoneInteractions() case (None, _) => //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") @@ -171,6 +172,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex obj.Velocity = None obj.Flying = None } + player.allowInteraction = false obj.zoneInteractions() } else { obj.Velocity = None diff --git a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala index f770de36c..96ef7e81c 100644 --- a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala @@ -3,9 +3,15 @@ package net.psforever.actors.session.normal import akka.actor.{ActorContext, typed} import net.psforever.actors.session.support.AvatarHandlerFunctions -import net.psforever.objects.Default +import net.psforever.actors.zone.ZoneActor +import net.psforever.objects.inventory.Container +import net.psforever.objects.{Default, PlanetSideGameObject} import net.psforever.objects.serverobject.containable.ContainableBehavior +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.sourcing.PlayerSource +import net.psforever.objects.vital.interaction.Adversarial import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction} +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.types.ImplantType import scala.concurrent.duration._ @@ -404,7 +410,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A val maxArmWeapon = GlobalDefinitions.MAXArms(subtype, player.Faction) sendResponse(PlanetsideAttributeMessage(target, attribute_type=7, player.Capacitor.toLong)) TaskWorkflow.execute(HoldNewEquipmentUp(player)(Tool(maxArmWeapon), slot = 0)) - val cooldown = player.avatar.purchaseCooldown(maxArmWeapon) + player.avatar.purchaseCooldown(maxArmWeapon) if (!oldHolsters.exists { case (e, _) => e.Definition == maxArmWeapon } && player.avatar.purchaseCooldown(maxArmWeapon).isEmpty) { avatarActor ! AvatarActor.UpdatePurchaseTime(maxArmWeapon) //switching for first time causes cooldown @@ -478,9 +484,33 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) - case AvatarResponse.Killed(mount) => + case AvatarResponse.Killed(cause, mount) => //log and chat messages - val cause = player.LastDamage.flatMap { damage => + //destroy display + val zoneChannel = continent.id + val events = continent.AvatarEvents + val pentry = PlayerSource(player) + cause + .adversarial + .collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out } + .orElse { + player.LastDamage.collect { + case attack if System.currentTimeMillis() - attack.interaction.hitTime < (10 seconds).toMillis => + attack + .adversarial + .collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out } + }.flatten + } match { + case Some(adversarial) => + events ! AvatarServiceMessage( + zoneChannel, + AvatarAction.DestroyDisplay(adversarial.attacker, pentry, adversarial.implement) + ) + case _ => + events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0)) + } + //events chat and log + val excuse = player.LastDamage.flatMap { damage => val interaction = damage.interaction val reason = interaction.cause val adversarial = interaction.adversarial.map { _.attacker } @@ -492,15 +522,17 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A } adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") } }.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" } - log.info(s"${player.Name} has died, killed by $cause") + log.info(s"${player.Name} has died, killed by $excuse") if (sessionLogic.shooting.shotsWhileDead > 0) { log.warn( s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionLogic.shooting.shotsWhileDead} rounds while character was dead on server" ) sessionLogic.shooting.shotsWhileDead = 0 } + //TODO other methods of death? sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel") sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) + continent.actor ! ZoneActor.RewardThisDeath(player) //player state changes AvatarActor.updateToolDischargeFor(avatar) @@ -512,9 +544,18 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive sessionLogic.zoning.zoningStatus = Zoning.Status.None sessionLogic.zoning.spawn.deadState = DeadState.Dead - continent.GUID(mount).collect { case obj: Vehicle => - sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) - sessionLogic.general.unaccessContainer(obj) + continent.GUID(mount).collect { + case obj: Vehicle => + killedWhileMounted(obj, resolvedPlayerGuid) + sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) + sessionLogic.general.unaccessContainer(obj) + + case obj: PlanetSideGameObject with Mountable with Container => + killedWhileMounted(obj, resolvedPlayerGuid) + sessionLogic.general.unaccessContainer(obj) + + case obj: PlanetSideGameObject with Mountable => + killedWhileMounted(obj, resolvedPlayerGuid) } sessionLogic.actionsToCancel() sessionLogic.terminals.CancelAllProximityUnits() @@ -628,4 +669,13 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case _ => () } } + + def killedWhileMounted(obj: PlanetSideGameObject with Mountable, playerGuid: PlanetSideGUID): Unit = { + val events = continent.AvatarEvents + ops.killedWhileMounted(obj, playerGuid) + //make player invisible on client + events ! AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(playerGuid, 29, 1)) + //only the dead player should "see" their own body, so that the death camera has something to focus on + events ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(playerGuid, playerGuid)) + } } diff --git a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala index 9c402a6fd..aa3c93ace 100644 --- a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala @@ -156,7 +156,10 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext def commandToggleSpectatorMode(contents: String): Unit = { val currentSpectatorActivation = { - if (avatar != null) { + if (player != null) { + val avtr = player.avatar + player.isAlive && (avtr.permissions.canSpectate || avtr.permissions.canGM) + } else if (avatar != null) { avatar.permissions.canSpectate || avatar.permissions.canGM } else { false @@ -173,7 +176,9 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext if (sessionLogic.zoning.maintainInitialGmState) { sessionLogic.zoning.maintainInitialGmState = false } else { - val currentCsrActivation = (if (avatar != null) { + val currentCsrActivation = (if (player != null) { + player.isAlive && player.avatar.permissions.canGM + } else if (avatar != null) { avatar.permissions.canGM } else { false diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala index ebb354d06..5ed83c9b1 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala @@ -5,11 +5,10 @@ import akka.actor.{ActorContext, typed} import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations} import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.{PlanetSideGameObject, Vehicle, Vehicles} +import net.psforever.objects.{Vehicle, Vehicles} import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vehicles.control.BfrFlight -import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ChatMsg, ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} diff --git a/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala index 5c766a279..0d33f5aba 100644 --- a/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala @@ -224,7 +224,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit if (ops.confirmAIDamageTarget(pkt, list.map(_._1))) { list.foreach { case (target, projectile, hitPos, _) => - ops.checkForHitPositionDiscrepancy(projectile.GUID, hitPos, target) + ops.checkForHitPositionDiscrepancy(pkt.attacker_guid, hitPos, target) ops.resolveProjectileInteraction(target, projectile, DamageResolution.Hit, hitPos) } } diff --git a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala index f09d66b5f..e96e6cee6 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala @@ -426,7 +426,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) - case AvatarResponse.Killed(mount) => + case AvatarResponse.Killed(_, mount) => //log and chat messages val cause = player.LastDamage.flatMap { damage => val interaction = damage.interaction diff --git a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala index c8a32df66..869369fc7 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala @@ -58,6 +58,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex )= pkt sessionLogic.persist() sessionLogic.turnCounterFunc(avatarGuid) + sessionLogic.updateBlockMap(player, pos) ops.fallHeightTracker(pos.z) // if (isCrouching && !player.Crouching) { // //dev stuff goes here diff --git a/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala b/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala index cc65f07a8..4fa43730f 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala @@ -119,6 +119,7 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { } // player.spectator = true + player.allowInteraction = false data.chat.JoinChannel(SpectatorChannel) val newPlayer = SpectatorModeLogic.spectatorCharacter(player) newPlayer.LogActivity(player.History.headOption) @@ -153,6 +154,7 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { val sendResponse: PlanetSidePacket => Unit = data.sendResponse // player.spectator = false + player.allowInteraction = true data.general.stop() player.avatar.shortcuts.slice(1, 4) .zipWithIndex diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index c753e1bbb..995f7666d 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -2,11 +2,13 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} -import net.psforever.objects.Default +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.{Default, PlanetSideGameObject} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.objects.zones.exp -import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.Service +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage, AvatarServiceResponse} import scala.collection.mutable // @@ -172,6 +174,29 @@ class SessionAvatarHandlers( AvatarAction.PlanetsideAttributeToAll(revivalTargetGuid, attribute_type=0, health) ) } + + def killedWhileMounted(obj: PlanetSideGameObject with Mountable, playerGuid: PlanetSideGUID): Unit = { + val playerName = player.Name + //boot cadaver from mount on client + context.self ! AvatarServiceResponse( + playerName, + Service.defaultPlayerGUID, + AvatarResponse.SendResponse( + ObjectDetachMessage(obj.GUID, playerGuid, player.Position, Vector3.Zero) + ) + ) + //player no longer seated + obj.PassengerInSeat(player).foreach { seatNumber => + //boot cadaver from mount internally (vehicle perspective) + obj.Seats(seatNumber).unmount(player) + //inform client-specific logic + context.self ! Mountable.MountMessages( + player, + Mountable.CanDismount(obj, seatNumber, 0) + ) + } + player.VehicleSeated = None + } } object SessionAvatarHandlers { diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index 98bf7d2d9..5633e76f7 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -510,8 +510,6 @@ class WeaponAndProjectileOperations( case target: AutomatedTurret.Target => sessionLogic.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret") .collect { - case turret: AutomatedTurret if turret.Target.isEmpty => - Nil case turret: AutomatedTurret => prepareAIProjectile(target, CompileAutomatedTurretDamageData(turret, projectileTypeId)) } @@ -547,7 +545,7 @@ class WeaponAndProjectileOperations( case target: AutomatedTurret.Target => turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) } - turret.Target.isEmpty + turret.Target.nonEmpty } .getOrElse(false) } diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala index 8f66d3078..527ffc520 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -2142,6 +2142,7 @@ class ZoningOperations( sendResponse(LoadMapMessage(mapName, id, 40100, 25, weaponsEnabled, map.checksum)) if (isAcceptableNextSpawnPoint) { //important! the LoadMapMessage must be processed by the client before the avatar is created + player.allowInteraction = true setupAvatarFunc() //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable sessionLogic.turnCounterFunc = interimUngunnedVehicle match { @@ -2179,6 +2180,7 @@ class ZoningOperations( session = session.copy(player = tplayer) if (isAcceptableNextSpawnPoint) { //try this spawn point + player.allowInteraction = true setupAvatarFunc() //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable sessionLogic.turnCounterFunc = interimUngunnedVehicle match { diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index f0e14b768..ee5fef561 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -3,7 +3,6 @@ package net.psforever.objects.avatar import akka.actor.{Actor, ActorRef, Props, typed} import net.psforever.actors.session.AvatarActor -import net.psforever.actors.zone.ZoneActor import net.psforever.login.WorldSession.{DropEquipmentFromInventory, HoldNewEquipmentUp, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} import net.psforever.objects._ import net.psforever.objects.ce.Deployable @@ -18,7 +17,6 @@ import net.psforever.objects.serverobject.containable.{Containable, ContainableB import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.damage.{AggravatedBehavior, Damageable, DamageableEntity} -import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.vital._ import net.psforever.objects.vital.resolution.ResolutionCalculations.Output @@ -35,7 +33,7 @@ import net.psforever.objects.serverobject.repair.Repairable import net.psforever.objects.sourcing.{AmenitySource, PlayerSource} import net.psforever.objects.vital.collision.CollisionReason import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} -import net.psforever.objects.vital.interaction.{Adversarial, DamageInteraction, DamageResult} +import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.packet.PlanetSideGamePacket import scala.concurrent.duration._ @@ -970,36 +968,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm damageLog.info(s"${player.Name} killed ${player.Sex.pronounObject}self") } - // This would normally happen async as part of AvatarAction.Killed, but if it doesn't happen before deleting calling AvatarAction.ObjectDelete on the player the LLU will end up invisible to others if carried - // Therefore, queue it up to happen first. - events ! AvatarServiceMessage(nameChannel, AvatarAction.DropSpecialItem()) - - events ! AvatarServiceMessage( - nameChannel, - AvatarAction.Killed(player_guid, target.VehicleSeated) - ) //align client interface fields with state - zone.GUID(target.VehicleSeated) match { - case Some(obj: Mountable) => - //boot cadaver from mount internally (vehicle perspective) - obj.PassengerInSeat(target) match { - case Some(index) => - obj.Seats(index).unmount(target) - case _ => ; - } - //boot cadaver from mount on client - events ! AvatarServiceMessage( - nameChannel, - AvatarAction.SendResponse( - Service.defaultPlayerGUID, - ObjectDetachMessage(obj.GUID, player_guid, target.Position, Vector3.Zero) - ) - ) - //make player invisible on client - events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 29, 1)) - //only the dead player should "see" their own body, so that the death camera has something to focus on - events ! AvatarServiceMessage(zoneChannel, AvatarAction.ObjectDelete(player_guid, player_guid)) - case _ => ; - } + events ! AvatarServiceMessage(nameChannel, AvatarAction.Killed(player_guid, cause, target.VehicleSeated)) //align client interface fields with state events ! AvatarServiceMessage(zoneChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 0, 0)) //health if (target.Capacitor > 0) { target.Capacitor = 0 @@ -1013,28 +982,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm DestroyMessage(player_guid, attribute, Service.defaultPlayerGUID, pos) ) //how many players get this message? ) - //TODO other methods of death? - val pentry = PlayerSource(target) - cause - .adversarial - .collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out } - .orElse { - target.LastDamage.collect { - case attack if System.currentTimeMillis() - attack.interaction.hitTime < (10 seconds).toMillis => - attack - .adversarial - .collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out } - }.flatten - } match { - case Some(adversarial) => - events ! AvatarServiceMessage( - zoneChannel, - AvatarAction.DestroyDisplay(adversarial.attacker, pentry, adversarial.implement) - ) - case _ => - events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0)) - } - zone.actor ! ZoneActor.RewardThisDeath(player) } def suicide() : Unit = { diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala index c16487aea..4771f421b 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala @@ -289,7 +289,7 @@ object GlobalDefinitionsProjectile { chainblade_projectile.Damage1 = 0 chainblade_projectile.ProjectileDamageType = DamageType.Direct chainblade_projectile.InitialVelocity = 100 - chainblade_projectile.Lifespan = .02f + chainblade_projectile.Lifespan = .03f //.02f ProjectileDefinition.CalculateDerivedFields(chainblade_projectile) chainblade_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff) @@ -601,7 +601,7 @@ object GlobalDefinitionsProjectile { forceblade_projectile.Damage1 = 0 forceblade_projectile.ProjectileDamageType = DamageType.Direct forceblade_projectile.InitialVelocity = 100 - forceblade_projectile.Lifespan = .02f + forceblade_projectile.Lifespan = .03f //.02f ProjectileDefinition.CalculateDerivedFields(forceblade_projectile) forceblade_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff) @@ -942,7 +942,7 @@ object GlobalDefinitionsProjectile { katana_projectile.Damage1 = 0 katana_projectile.ProjectileDamageType = DamageType.Direct katana_projectile.InitialVelocity = 100 - katana_projectile.Lifespan = .03f + katana_projectile.Lifespan = .04f //.03f ProjectileDefinition.CalculateDerivedFields(katana_projectile) katana_projectileb.Name = "katana_projectileb" @@ -1088,7 +1088,7 @@ object GlobalDefinitionsProjectile { magcutter_projectile.Damage1 = 0 magcutter_projectile.ProjectileDamageType = DamageType.Direct magcutter_projectile.InitialVelocity = 100 - magcutter_projectile.Lifespan = .02f + magcutter_projectile.Lifespan = .03f //.02f ProjectileDefinition.CalculateDerivedFields(magcutter_projectile) magcutter_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff) diff --git a/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala b/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala index c2bceeb6a..bd46892ef 100644 --- a/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala +++ b/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala @@ -3,6 +3,8 @@ package net.psforever.objects.vital.damage import net.psforever.objects.vital.interaction.DamageInteraction +import scala.annotation.unused + /** * A series of methods for extraction of the base damage against a given target type * as well as incorporating damage modifiers from the other aspects of the interaction. @@ -11,7 +13,7 @@ object DamageCalculations { type Selector = DamageProfile => Int //raw damage selectors - def AgainstNothing(profile : DamageProfile) : Int = 0 + def AgainstNothing(@unused profile : DamageProfile) : Int = 0 def AgainstExoSuit(profile : DamageProfile) : Int = profile.Damage0 diff --git a/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala b/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala index c78cb1b1e..cbaf923cb 100644 --- a/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala +++ b/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala @@ -185,10 +185,6 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int) */ def addTo(o: BlockMapEntity): Boolean = { o match { - case p: Player if p.spectator => - livePlayers.removeFrom(p) - corpses.removeFrom(p) - false case p: Player if p.isBackpack => //when adding to the "corpse" list, first attempt to remove from the "player" list livePlayers.removeFrom(p) @@ -317,7 +313,7 @@ object SectorGroup { new SectorGroup( rangeX, rangeY, - sector.livePlayerList, + sector.livePlayerList.filterNot(p => p.spectator || !p.allowInteraction), sector.corpseList, sector.vehicleList, sector.equipmentOnGroundList, @@ -372,7 +368,7 @@ object SectorGroup { new SectorGroup( rangeX, rangeY, - sector.livePlayerList, + sector.livePlayerList.filterNot(p => p.spectator || !p.allowInteraction), sector.corpseList, sector.vehicleList, sector.equipmentOnGroundList, @@ -386,7 +382,7 @@ object SectorGroup { new SectorGroup( rangeX, rangeY, - sectors.flatMap { _.livePlayerList }.toList.distinct, + sectors.flatMap { _.livePlayerList }.toList.distinct.filterNot(p => p.spectator || !p.allowInteraction), sectors.flatMap { _.corpseList }.toList.distinct, sectors.flatMap { _.vehicleList }.toList.distinct, sectors.flatMap { _.equipmentOnGroundList }.toList.distinct, diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index eec4d7044..d7e585f0a 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -138,9 +138,9 @@ class AvatarService(zone: Zone) extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.HitHint(source_guid)) ) - case AvatarAction.Killed(player_guid, mount_guid) => + case AvatarAction.Killed(player_guid, cause, mount_guid) => AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Killed(mount_guid)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Killed(cause, mount_guid)) ) case AvatarAction.LoadPlayer(player_guid, object_id, target_guid, cdata, pdata) => val pkt = pdata match { diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index 8c3484ddf..ec2959b11 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -8,6 +8,7 @@ import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.ImplantAction @@ -54,7 +55,7 @@ object AvatarAction { final case class GenericObjectAction(player_guid: PlanetSideGUID, object_guid: PlanetSideGUID, action_code: Int) extends Action final case class HitHint(source_guid: PlanetSideGUID, player_guid: PlanetSideGUID) extends Action - final case class Killed(player_guid: PlanetSideGUID, mount_guid: Option[PlanetSideGUID]) extends Action + final case class Killed(player_guid: PlanetSideGUID, cause: DamageResult, mount_guid: Option[PlanetSideGUID]) extends Action final case class LoadPlayer( player_guid: PlanetSideGUID, object_id: Int, diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala index c90505842..897d105b7 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -8,6 +8,7 @@ import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.vital.interaction.DamageResult import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.{ImplantAction, ObjectCreateMessage} @@ -46,7 +47,7 @@ object AvatarResponse { final case class EquipmentInHand(pkt: ObjectCreateMessage) extends Response final case class GenericObjectAction(object_guid: PlanetSideGUID, action_code: Int) extends Response final case class HitHint(source_guid: PlanetSideGUID) extends Response - final case class Killed(mount_guid: Option[PlanetSideGUID]) extends Response + final case class Killed(cause: DamageResult, mount_guid: Option[PlanetSideGUID]) extends Response final case class LoadPlayer(pkt: ObjectCreateMessage) extends Response final case class LoadProjectile(pkt: ObjectCreateMessage) extends Response final case class ObjectDelete(item_guid: PlanetSideGUID, unk: Int) extends Response diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala index e464288ae..3c343b439 100644 --- a/src/test/scala/objects/PlayerControlTest.scala +++ b/src/test/scala/objects/PlayerControlTest.scala @@ -16,7 +16,6 @@ import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.objects._ import net.psforever.objects.definition.ProjectileDefinition import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget import net.psforever.objects.serverobject.environment.{DeepSquare, EnvironmentAttribute, Pool} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.vital.base.DamageResolution @@ -533,7 +532,7 @@ class PlayerControlDeathStandingTest extends ActorTest { assert(player2.isAlive) player2.Actor ! Vitality.Damage(applyDamageTo) - val msg_avatar = avatarProbe.receiveN(7, 500 milliseconds) + val msg_avatar = avatarProbe.receiveN(5, 500 milliseconds) val msg_stamina = probe.receiveOne(500 milliseconds) activityProbe.expectNoMessage(200 milliseconds) assert( @@ -550,31 +549,25 @@ class PlayerControlDeathStandingTest extends ActorTest { ) assert( msg_avatar(1) match { - case AvatarServiceMessage("TestCharacter2", AvatarAction.DropSpecialItem()) => true - case _ => false - } - ) - assert( - msg_avatar(2) match { - case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), None)) => true + case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), _, None)) => true case _ => false } ) assert( - msg_avatar(3) match { + msg_avatar(2) match { case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true case _ => false } ) assert( - msg_avatar(4) match { + msg_avatar(3) match { case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 7, _)) => true case _ => false } ) assert( - msg_avatar(5) match { + msg_avatar(4) match { case AvatarServiceMessage( "TestCharacter2", AvatarAction.SendResponse(_, DestroyMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _)) @@ -583,14 +576,6 @@ class PlayerControlDeathStandingTest extends ActorTest { case _ => false } ) - assert( - msg_avatar(6) match { - case AvatarServiceMessage("test", AvatarAction.DestroyDisplay(killer, victim, _, _)) - if killer.Name.equals(player1.Name) && victim.Name.equals(player2.Name) => - true - case _ => false - } - ) assert(player2.Health <= player2.Definition.DamageDestroysAt) assert(player2.Armor == 0) assert(!player2.isAlive) @@ -667,7 +652,7 @@ class PlayerControlDeathStandingTest extends ActorTest { // assert(player2.isAlive) // // player2.Actor ! Vitality.Damage(applyDamageTo) -// val msg_avatar = avatarProbe.receiveN(8, 1500 milliseconds) +// val msg_avatar = avatarProbe.receiveN(3, 1500 milliseconds) // val msg_stamina = probe.receiveOne(500 milliseconds) // activityProbe.expectNoMessage(200 milliseconds) // assert( @@ -678,70 +663,30 @@ class PlayerControlDeathStandingTest extends ActorTest { // ) // assert( // msg_avatar.head match { -// case AvatarServiceMessage("TestCharacter2", AvatarAction.DropSpecialItem()) => true -// case _ => false +// case AvatarServiceMessage( +// "TestCharacter2", +// AvatarAction.Killed(PlanetSideGUID(2), _, Some(PlanetSideGUID(7))) +// ) => +// true +// case _ => false // } // ) // assert( // msg_avatar(1) match { -// case AvatarServiceMessage( -// "TestCharacter2", -// AvatarAction.Killed(PlanetSideGUID(2), Some(PlanetSideGUID(7))) -// ) => -// true -// case _ => false +// case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true +// case _ => false // } // ) // assert( // msg_avatar(2) match { // case AvatarServiceMessage( // "TestCharacter2", -// AvatarAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(7), PlanetSideGUID(2), _, _, _, _)) -// ) => -// true -// case _ => false -// } -// ) -// assert( -// msg_avatar(3) match { -// case AvatarServiceMessage( -// "TestCharacter2", -// AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 29, 1) -// ) => -// true -// case _ => false -// } -// ) -// assert( -// msg_avatar(4) match { -// case AvatarServiceMessage("test", AvatarAction.ObjectDelete(PlanetSideGUID(2), PlanetSideGUID(2), _)) => true -// case _ => false -// } -// ) -// assert( -// msg_avatar(5) match { -// case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true -// case _ => false -// } -// ) -// assert( -// msg_avatar(6) match { -// case AvatarServiceMessage( -// "TestCharacter2", // AvatarAction.SendResponse(_, DestroyMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _)) // ) => // true // case _ => false // } // ) -// assert( -// msg_avatar(7) match { -// case AvatarServiceMessage("test", AvatarAction.DestroyDisplay(killer, victim, _, _)) -// if killer.Name.equals(player1.Name) && victim.Name.equals(player2.Name) => -// true -// case _ => false -// } -// ) // assert(player2.Health <= player2.Definition.DamageDestroysAt) // assert(!player2.isAlive) // }