* refactored WSA code handling HitMessage, handling SplashMessage, and handling LashMessage; modified projectiles for future functionality

* players can die from being shot now; the damage model is simplistic since main goal was to write around the potential for negative damage ('healed from getting shot'); HitHint works correctly; dedicated AvatarService channel for each avatar helps reduce message spam

* vehicle destruction, and replacement with lightweight wreckage objects upon continent join; made flushing vehicle terminal more accessible

* simple work on vehicle shield charging (amp station benefit) (that's my commit story and I'm sticking with it)

* a flexible calculation workflow that can be applied, grabbing damage information, resistance information, and then combining it with a resolution function; players and vehicles have resistance values; removed redundant damage calculations from WSA

* broke up DamageCalculations, ResistanceCalculations, and ResolutionCalculations into packages under vital; fixed an error with exo-suit calculation resistances; events for dealing with synchronized player and vehicle damage calculations and building the papertrail of those damages; updating codecov.yml file for ignore classes

* added tests for various components (damage model, destroyed vehicle converter, vitality, etc..) and some functionality improvements

* added a field to keep track of how projectiles will be attributed at the time of target death
This commit is contained in:
Fate-JH 2018-07-30 09:28:45 -04:00 committed by GitHub
parent fc78d53ecb
commit 7901f66324
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 3045 additions and 267 deletions

View file

@ -2,19 +2,35 @@
comment: off comment: off
ignore: ignore:
- "common/src/main/scala/net/psforever/objects/ObjectType.scala"
- "common/src/main/scala/net/psforever/objects/avatar/Avatars.scala" - "common/src/main/scala/net/psforever/objects/avatar/Avatars.scala"
- "common/src/main/scala/net/psforever/objects/ballistics/ProjectileResolution.scala"
- "common/src/main/scala/net/psforever/objects/ballistics/Projectiles.scala"
- "common/src/main/scala/net/psforever/objects/equipment/Ammo.scala" - "common/src/main/scala/net/psforever/objects/equipment/Ammo.scala"
- "common/src/main/scala/net/psforever/objects/equipment/CItem.scala" - "common/src/main/scala/net/psforever/objects/equipment/CItem.scala"
- "common/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala" - "common/src/main/scala/net/psforever/objects/equipment/EquipmentSize.scala"
- "common/src/main/scala/net/psforever/objects/equipment/Kits.scala" - "common/src/main/scala/net/psforever/objects/equipment/Kits.scala"
- "common/src/main/scala/net/psforever/objects/equipment/SItem.scala" - "common/src/main/scala/net/psforever/objects/equipment/SItem.scala"
- "common/src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala" - "common/src/main/scala/net/psforever/objects/guid/AvailabilityPolicy.scala"
- "common/src/main/scala/net/psforever/objects/serverobject/pad/AutoDriveControls.scala"
- "common/src/main/scala/net/psforever/objects/serverobject/structures/StructureType.scala"
- "common/src/main/scala/net/psforever/objects/serverobject/turret/TurretUpgrade.scala" - "common/src/main/scala/net/psforever/objects/serverobject/turret/TurretUpgrade.scala"
- "common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala" - "common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala"
- "common/src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala" - "common/src/main/scala/net/psforever/objects/vehicles/AccessPermissionGroup.scala"
- "common/src/main/scala/net/psforever/objects/vehicles/CargoVehicleRestiction.scala"
- "common/src/main/scala/net/psforever/objects/vehicles/DestroyedVehicle.scala"
- "common/src/main/scala/net/psforever/objects/vehicles/SeatArmoRestriction.scala" - "common/src/main/scala/net/psforever/objects/vehicles/SeatArmoRestriction.scala"
- "common/src/main/scala/net/psforever/objects/vehicles/Turrets.scala" - "common/src/main/scala/net/psforever/objects/vehicles/Turrets.scala"
- "common/src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala" - "common/src/main/scala/net/psforever/objects/vehicles/VehicleLockState.scala"
- "common/src/main/scala/net/psforever/objects/vital/damage/DamageProfile.scala"
- "common/src/main/scala/net/psforever/objects/vital/projectile/ProjectileCalculations.scala"
- "common/src/main/scala/net/psforever/objects/vital/resistance/ResistanceProfile.scala"
- "common/src/main/scala/net/psforever/objects/vital/DamageResistanceModel.scala"
- "common/src/main/scala/net/psforever/objects/vital/DamageType.scala"
- "common/src/main/scala/net/psforever/objects/vital/StandardDamages.scala"
- "common/src/main/scala/net/psforever/objects/vital/StandardResistanceProfile.scala"
- "common/src/main/scala/net/psforever/objects/vital/StandardResistances.scala"
- "common/src/main/scala/net/psforever/objects/vital/StandardResolutions.scala"
- "common/src/main/scala/net/psforever/packet/crypto" - "common/src/main/scala/net/psforever/packet/crypto"
- "common/src/main/scala/net/psforever/packet/game/objectcreate/DrawnSlot.scala" - "common/src/main/scala/net/psforever/packet/game/objectcreate/DrawnSlot.scala"
- "common/src/main/scala/net/psforever/packet/game/objectcreate/DriveState.scala" - "common/src/main/scala/net/psforever/packet/game/objectcreate/DriveState.scala"
@ -24,7 +40,6 @@ ignore:
- "common/src/main/scala/net/psforever/packet/ControlPacketOpcode.scala" - "common/src/main/scala/net/psforever/packet/ControlPacketOpcode.scala"
- "common/src/main/scala/net/psforever/packet/CryptoPacketOpcode.scala" - "common/src/main/scala/net/psforever/packet/CryptoPacketOpcode.scala"
- "common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala" - "common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala"
- "common/src/main/scala/net/psforever/objects/ObjectType.scala"
- "common/src/main/scala/net/psforever/types/Angular.scala" - "common/src/main/scala/net/psforever/types/Angular.scala"
- "common/src/main/scala/net/psforever/types/CertificationType.scala" - "common/src/main/scala/net/psforever/types/CertificationType.scala"
- "common/src/main/scala/net/psforever/types/ChatMessageType.scala" - "common/src/main/scala/net/psforever/types/ChatMessageType.scala"
@ -38,6 +53,8 @@ ignore:
- "common/src/main/scala/net/psforever/types/TransactionType.scala" - "common/src/main/scala/net/psforever/types/TransactionType.scala"
- "common/src/main/scala/services/avatar/AvatarAction.scala" - "common/src/main/scala/services/avatar/AvatarAction.scala"
- "common/src/main/scala/services/avatar/AvatarResponse.scala" - "common/src/main/scala/services/avatar/AvatarResponse.scala"
- "common/src/main/scala/services/galaxy/GalaxyAction.scala"
- "common/src/main/scala/services/galaxy/GalaxyResponse.scala"
- "common/src/main/scala/services/local/LocalAction.scala" - "common/src/main/scala/services/local/LocalAction.scala"
- "common/src/main/scala/services/local/LocalResponse.scala" - "common/src/main/scala/services/local/LocalResponse.scala"
- "common/src/main/scala/services/vehicle/VehicleAction.scala" - "common/src/main/scala/services/vehicle/VehicleAction.scala"

View file

@ -177,7 +177,7 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
} }
} }
def Definition : AvatarDefinition = Avatar.definition def Definition : AvatarDefinition = GlobalDefinitions.avatar
/* /*
Merit Commendations and Ribbons Merit Commendations and Ribbons
@ -210,8 +210,6 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
} }
object Avatar { object Avatar {
final private val definition : AvatarDefinition = new AvatarDefinition(121)
def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : CharacterVoice.Value) : Avatar = { def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : CharacterVoice.Value) : Avatar = {
new Avatar(name, faction, sex, head, voice) new Avatar(name, faction, sex, head, voice)
} }

View file

@ -3,6 +3,8 @@ package net.psforever.objects
import net.psforever.objects.equipment.EquipmentSize import net.psforever.objects.equipment.EquipmentSize
import net.psforever.objects.inventory.InventoryTile import net.psforever.objects.inventory.InventoryTile
import net.psforever.objects.vital._
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.types.ExoSuitType import net.psforever.types.ExoSuitType
/** /**
@ -10,12 +12,16 @@ import net.psforever.types.ExoSuitType
* Players are influenced by the exo-suit they wear in a variety of ways, with speed and available equipment slots being major differences. * Players are influenced by the exo-suit they wear in a variety of ways, with speed and available equipment slots being major differences.
* @param suitType the `Enumeration` corresponding to this exo-suit * @param suitType the `Enumeration` corresponding to this exo-suit
*/ */
class ExoSuitDefinition(private val suitType : ExoSuitType.Value) { class ExoSuitDefinition(private val suitType : ExoSuitType.Value) extends ResistanceProfileMutators
with DamageResistanceModel {
protected var permission : Int = 0 //TODO certification type? protected var permission : Int = 0 //TODO certification type?
protected var maxArmor : Int = 0 protected var maxArmor : Int = 0
protected val holsters : Array[EquipmentSize.Value] = Array.fill[EquipmentSize.Value](5)(EquipmentSize.Blocked) protected val holsters : Array[EquipmentSize.Value] = Array.fill[EquipmentSize.Value](5)(EquipmentSize.Blocked)
protected var inventoryScale : InventoryTile = InventoryTile.Tile11 //override with custom InventoryTile protected var inventoryScale : InventoryTile = InventoryTile.Tile11 //override with custom InventoryTile
protected var inventoryOffset : Int = 0 protected var inventoryOffset : Int = 0
Damage = StandardInfantryDamage
Resistance = StandardInfantryResistance
Model = StandardResolutions.Infantry
def SuitType : ExoSuitType.Value = suitType def SuitType : ExoSuitType.Value = suitType
@ -79,6 +85,12 @@ class SpecialExoSuitDefinition(private val suitType : ExoSuitType.Value) extends
obj.MaxArmor = MaxArmor obj.MaxArmor = MaxArmor
obj.InventoryScale = InventoryScale obj.InventoryScale = InventoryScale
obj.InventoryOffset = InventoryOffset obj.InventoryOffset = InventoryOffset
obj.ResistanceDirectHit = ResistanceDirectHit
obj.ResistanceSplash = ResistanceSplash
obj.ResistanceAggravated = ResistanceAggravated
obj.Damage = Damage
obj.Resistance = Resistance
obj.Model = Model
(0 until 5).foreach(index => { obj.Holster(index, Holster(index)) }) (0 until 5).foreach(index => { obj.Holster(index, Holster(index)) })
obj obj
} }
@ -109,6 +121,9 @@ object ExoSuitDefinition {
Standard.Holster(0, EquipmentSize.Pistol) Standard.Holster(0, EquipmentSize.Pistol)
Standard.Holster(2, EquipmentSize.Rifle) Standard.Holster(2, EquipmentSize.Rifle)
Standard.Holster(4, EquipmentSize.Melee) Standard.Holster(4, EquipmentSize.Melee)
Standard.ResistanceDirectHit = 4
Standard.ResistanceSplash = 15
Standard.ResistanceAggravated = 8
final val Agile = ExoSuitDefinition(ExoSuitType.Agile) final val Agile = ExoSuitDefinition(ExoSuitType.Agile)
Agile.MaxArmor = 100 Agile.MaxArmor = 100
@ -118,6 +133,9 @@ object ExoSuitDefinition {
Agile.Holster(1, EquipmentSize.Pistol) Agile.Holster(1, EquipmentSize.Pistol)
Agile.Holster(2, EquipmentSize.Rifle) Agile.Holster(2, EquipmentSize.Rifle)
Agile.Holster(4, EquipmentSize.Melee) Agile.Holster(4, EquipmentSize.Melee)
Agile.ResistanceDirectHit = 6
Agile.ResistanceSplash = 25
Agile.ResistanceAggravated = 10
final val Reinforced = ExoSuitDefinition(ExoSuitType.Reinforced) final val Reinforced = ExoSuitDefinition(ExoSuitType.Reinforced)
Reinforced.permission = 1 Reinforced.permission = 1
@ -129,6 +147,9 @@ object ExoSuitDefinition {
Reinforced.Holster(2, EquipmentSize.Rifle) Reinforced.Holster(2, EquipmentSize.Rifle)
Reinforced.Holster(3, EquipmentSize.Rifle) Reinforced.Holster(3, EquipmentSize.Rifle)
Reinforced.Holster(4, EquipmentSize.Melee) Reinforced.Holster(4, EquipmentSize.Melee)
Reinforced.ResistanceDirectHit = 10
Reinforced.ResistanceSplash = 35
Reinforced.ResistanceAggravated = 12
final val Infiltration = ExoSuitDefinition(ExoSuitType.Infiltration) final val Infiltration = ExoSuitDefinition(ExoSuitType.Infiltration)
Infiltration.permission = 1 Infiltration.permission = 1
@ -145,6 +166,11 @@ object ExoSuitDefinition {
MAX.InventoryOffset = 6 MAX.InventoryOffset = 6
MAX.Holster(0, EquipmentSize.Max) MAX.Holster(0, EquipmentSize.Max)
MAX.Holster(4, EquipmentSize.Melee) MAX.Holster(4, EquipmentSize.Melee)
MAX.ResistanceDirectHit = 6
MAX.ResistanceSplash = 35
MAX.ResistanceAggravated = 10
MAX.Damage = StandardMaxDamage
MAX.Model = StandardResolutions.Max
def apply(suitType : ExoSuitType.Value) : ExoSuitDefinition = { def apply(suitType : ExoSuitType.Value) : ExoSuitDefinition = {
new ExoSuitDefinition(suitType) new ExoSuitDefinition(suitType)

View file

@ -6,13 +6,19 @@ import net.psforever.objects.equipment.{Equipment, EquipmentSize}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
import net.psforever.objects.loadouts.Loadout import net.psforever.objects.loadouts.Loadout
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.{DamageResistanceModel, Vitality}
import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.game.PlanetSideGUID
import net.psforever.types._ import net.psforever.types._
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.util.{Success, Try} import scala.util.{Success, Try}
class Player(private val core : Avatar) extends PlanetSideGameObject with FactionAffinity with Container { class Player(private val core : Avatar) extends PlanetSideGameObject
with FactionAffinity
with Vitality
with ResistanceProfile
with Container {
private var alive : Boolean = false private var alive : Boolean = false
private var backpack : Boolean = false private var backpack : Boolean = false
private var health : Int = 0 private var health : Int = 0
@ -42,7 +48,6 @@ class Player(private val core : Avatar) extends PlanetSideGameObject with Factio
//SouNourS things //SouNourS things
/** Last medkituse. */ /** Last medkituse. */
var lastMedkit : Long = 0 var lastMedkit : Long = 0
var death_by : Int = 0
var lastSeenStreamMessage : Array[Long] = Array.fill[Long](65535)(0L) var lastSeenStreamMessage : Array[Long] = Array.fill[Long](65535)(0L)
var lastShotSeq_time : Int = -1 var lastShotSeq_time : Int = -1
/** From PlanetsideAttributeMessage */ /** From PlanetsideAttributeMessage */
@ -276,6 +281,14 @@ class Player(private val core : Avatar) extends PlanetSideGameObject with Factio
ChangeSpecialAbility() ChangeSpecialAbility()
} }
def ResistanceDirectHit = exosuit.ResistanceDirectHit
def ResistanceSplash = exosuit.ResistanceSplash
def ResistanceAggravated = exosuit.ResistanceAggravated
def RadiationShielding = exosuit.RadiationShielding
def LoadLoadout(line : Int) : Option[Loadout] = core.LoadLoadout(line) def LoadLoadout(line : Int) : Option[Loadout] = core.LoadLoadout(line)
def BEP : Long = core.BEP def BEP : Long = core.BEP
@ -475,6 +488,8 @@ class Player(private val core : Avatar) extends PlanetSideGameObject with Factio
Continent Continent
} }
def DamageModel = exosuit.asInstanceOf[DamageResistanceModel]
def Definition : AvatarDefinition = core.Definition def Definition : AvatarDefinition = core.Definition
def canEqual(other: Any): Boolean = other.isInstanceOf[Player] def canEqual(other: Any): Boolean = other.isInstanceOf[Player]

View file

@ -9,6 +9,7 @@ import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles._
import net.psforever.objects.vital.{DamageResistanceModel, StandardResistanceProfile, Vitality}
import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.game.PlanetSideGUID
import net.psforever.types.PlanetSideEmpire import net.psforever.types.PlanetSideEmpire
@ -67,6 +68,8 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ
with Mountable with Mountable
with MountedWeapons with MountedWeapons
with Deployment with Deployment
with Vitality
with StandardResistanceProfile
with Container { with Container {
private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.TR private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.TR
private var owner : Option[PlanetSideGUID] = None private var owner : Option[PlanetSideGUID] = None
@ -142,7 +145,7 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ
def Owner_=(owner : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = { def Owner_=(owner : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = {
owner match { owner match {
case Some(_) => case Some(_) =>
if(Definition.CanBeOwned) { //e.g., base turrets if(Definition.CanBeOwned) {
this.owner = owner this.owner = owner
} }
case None => case None =>
@ -152,11 +155,11 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ
} }
def Health : Int = { def Health : Int = {
this.health health
} }
def Health_=(health : Int) : Int = { def Health_=(assignHealth : Int) : Int = {
this.health = health health = math.min(math.max(0, assignHealth), MaxHealth)
health health
} }
@ -165,11 +168,11 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ
} }
def Shields : Int = { def Shields : Int = {
this.shields shields
} }
def Shields_=(strength : Int) : Int = { def Shields_=(strength : Int) : Int = {
this.shields = strength shields = math.min(math.max(0, strength), MaxShields)
Shields Shields
} }
@ -178,12 +181,12 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ
} }
def Decal : Int = { def Decal : Int = {
this.decal decal
} }
def Decal_=(decal : Int) : Int = { def Decal_=(logo : Int) : Int = {
this.decal = decal decal = logo
decal Decal
} }
def Jammered : Boolean = jammered def Jammered : Boolean = jammered
@ -493,6 +496,8 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ
*/ */
def TrunkLockState : VehicleLockState.Value = groupPermissions(3) def TrunkLockState : VehicleLockState.Value = groupPermissions(3)
def DamageModel = Definition.asInstanceOf[DamageResistanceModel]
/** /**
* This is the definition entry that is used to store and unload pertinent information about the `Vehicle`. * This is the definition entry that is used to store and unload pertinent information about the `Vehicle`.
* @return the vehicle's definition entry * @return the vehicle's definition entry
@ -534,6 +539,20 @@ object Vehicle {
*/ */
final case class Reactivate() final case class Reactivate()
/**
* A request has been made to charge this vehicle's shields.
* @see `FacilityBenefitShieldChargeRequestMessage`
* @param amount the number of points to charge
*/
final case class ChargeShields(amount : Int)
/**
* Following a successful shield charge tick, display the results of the update.
* @see `FacilityBenefitShieldChargeRequestMessage`
* @param vehicle the updated vehicle
*/
final case class UpdateShieldsCharge(vehicle : Vehicle)
/** /**
* Overloaded constructor. * Overloaded constructor.
* @param vehicleDef the vehicle's definition entry * @param vehicleDef the vehicle's definition entry

View file

@ -0,0 +1,31 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class ObjectSource(obj : PlanetSideGameObject with FactionAffinity,
faction : PlanetSideEmpire.Value,
position : Vector3,
orientation : Vector3,
velocity : Option[Vector3] = None) extends SourceEntry {
override def Name = SourceEntry.NameFormat(obj.Definition.Name)
override def Faction = faction
def Definition = obj.Definition
def Position = position
def Orientation = orientation
def Velocity = velocity
}
object ObjectSource {
def apply(obj : PlanetSideGameObject with FactionAffinity) : ObjectSource = {
ObjectSource(
obj,
obj.Faction,
obj.Position,
obj.Orientation,
obj.Velocity
)
}
}

View file

@ -0,0 +1,35 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.Player
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, Vector3}
final case class PlayerSource(name : String,
obj_def : ObjectDefinition,
faction : PlanetSideEmpire.Value,
exosuit : ExoSuitType.Value,
seated : Boolean,
health : Int,
armor : Int,
position : Vector3,
orientation : Vector3,
velocity : Option[Vector3] = None) extends SourceEntry {
override def Name = name
override def Faction = faction
def Definition = obj_def
def ExoSuit = exosuit
def Seated = seated
def Health = health
def Armor = armor
def Position = position
def Orientation = orientation
def Velocity = velocity
}
object PlayerSource {
def apply(tplayer : Player) : PlayerSource = {
PlayerSource(tplayer.Name, tplayer.Definition, tplayer.Faction, tplayer.ExoSuit, tplayer.VehicleSeated.nonEmpty,
tplayer.Health, tplayer.Armor, tplayer.Position, tplayer.Orientation, tplayer.Velocity)
}
}

View file

@ -1,60 +1,59 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics package net.psforever.objects.ballistics
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{ProjectileDefinition, ToolDefinition} import net.psforever.objects.definition.{ProjectileDefinition, ToolDefinition}
import net.psforever.objects.entity.SimpleWorldEntity import net.psforever.objects.entity.SimpleWorldEntity
import net.psforever.objects.equipment.FireModeDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.types.Vector3 import net.psforever.types.Vector3
/** /**
* A summation of weapon (`Tool`) discharge. * A summation of weapon (`Tool`) discharge.
* @see `ProjectileDefinition`<br>
* `ToolDefinition`<br>
* `FireModeDefinition`<br>
* `SourceEntry`<br>
* `PlayerSource`
* @param profile an explanation of the damage that can be performed by this discharge * @param profile an explanation of the damage that can be performed by this discharge
* @param tool_def the weapon that caused this discharge * @param tool_def the weapon that caused this discharge
* @param fire_mode the current fire mode of the tool used
* @param owner the agency that caused the weapon to produce this projectile;
* most often a player (`PlayerSource`)
* @param attribute_to an object ID that refers to the method of death that would be reported;
* usually the same as `tool_def.ObjectId`;
* 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_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged * @param shot_angle in which direction the projectile was aimed when it was discharged
* @param resolution whether this projectile has encountered a target or wall;
* defaults to `Unresolved`
* @param fire_time when the weapon discharged was recorded; * @param fire_time when the weapon discharged was recorded;
* defaults to `System.nanoTime` * defaults to `System.nanoTime`
* @param hit_time when the discharge had its resolution status updated
*/ */
final case class Projectile(profile : ProjectileDefinition, final case class Projectile(profile : ProjectileDefinition,
tool_def : ToolDefinition, tool_def : ToolDefinition,
fire_mode : FireModeDefinition,
owner : SourceEntry,
attribute_to : Int,
shot_origin : Vector3, shot_origin : Vector3,
shot_angle : Vector3, shot_angle : Vector3,
resolution : ProjectileResolution.Value, fire_time: Long = System.nanoTime) {
fire_time : Long = System.nanoTime,
hit_time : Long = 0) {
/** Information about the current world coordinates and orientation of the projectile */ /** Information about the current world coordinates and orientation of the projectile */
val current : SimpleWorldEntity = new SimpleWorldEntity() val current : SimpleWorldEntity = new SimpleWorldEntity()
private var resolved : ProjectileResolution.Value = ProjectileResolution.Unresolved
/** /**
* Give the projectile the suggested resolution status. * Mark the projectile as being "encountered" or "managed" at least once.
* Update the world coordinates and orientation.
* @param pos the current position
* @param ang the current orientation
* @param resolution the resolution status
* @return a new projectile with the suggested resolution status, or the original projectile
*/ */
def Resolve(pos : Vector3, ang : Vector3, resolution : ProjectileResolution.Value) : Projectile = { def Resolve() : Unit = {
val obj = Resolve(resolution) resolved = ProjectileResolution.Resolved
obj.current.Position = pos
obj.current.Orientation = ang
obj
} }
/** def Miss() : Unit = {
* Give the projectile the suggested resolution status. resolved = ProjectileResolution.MissedShot
* @param resolution the resolution status
* @return a new projectile with the suggested resolution status, or the original projectile
*/
def Resolve(resolution : ProjectileResolution.Value) : Projectile = {
resolution match {
case ProjectileResolution.Unresolved =>
this
case _ =>
Projectile(profile, tool_def, shot_origin, shot_angle, resolution, fire_time, System.nanoTime)
}
} }
def isResolved : Boolean = resolved == ProjectileResolution.Resolved || resolved == ProjectileResolution.MissedShot
def isMiss : Boolean = resolved == ProjectileResolution.MissedShot
} }
object Projectile { object Projectile {
@ -68,11 +67,28 @@ object Projectile {
* Overloaded constructor for an `Unresolved` projectile. * Overloaded constructor for an `Unresolved` projectile.
* @param profile an explanation of the damage that can be performed by this discharge * @param profile an explanation of the damage that can be performed by this discharge
* @param tool_def the weapon that caused this discharge * @param tool_def the weapon that caused this discharge
* @param fire_mode the current fire mode of the tool used
* @param owner the agency that caused the weapon to produce this projectile
* @param shot_origin where the projectile started * @param shot_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged * @param shot_angle in which direction the projectile was aimed when it was discharged
* @return the `Projectile` object * @return the `Projectile` object
*/ */
def apply(profile : ProjectileDefinition, tool_def : ToolDefinition, shot_origin : Vector3, shot_angle : Vector3) : Projectile = { def apply(profile : ProjectileDefinition, tool_def : ToolDefinition, fire_mode : FireModeDefinition, owner : PlanetSideGameObject with FactionAffinity, shot_origin : Vector3, shot_angle : Vector3) : Projectile = {
Projectile(profile, tool_def, shot_origin, shot_angle, ProjectileResolution.Unresolved) Projectile(profile, tool_def, fire_mode, SourceEntry(owner), tool_def.ObjectId, shot_origin, shot_angle)
}
/**
* Overloaded constructor for an `Unresolved` projectile.
* @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
* @param owner the agency that caused the weapon to produce this projectile
* @param attribute_to an object ID that refers to the method of death that would be reported
* @param shot_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged
* @return the `Projectile` object
*/
def apply(profile : ProjectileDefinition, tool_def : ToolDefinition, fire_mode : FireModeDefinition, owner : PlanetSideGameObject with FactionAffinity, attribute_to : Int, shot_origin : Vector3, shot_angle : Vector3) : Projectile = {
Projectile(profile, tool_def, fire_mode, SourceEntry(owner), attribute_to, shot_origin, shot_angle)
} }
} }

View file

@ -0,0 +1,24 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.vital.DamageResistanceModel
import net.psforever.types.Vector3
/**
* An encapsulation of a projectile event that records sufficient historical information
* 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
* @param hit_time the sequence timing when the projectile hit the target
*/
final case class ResolvedProjectile(resolution : ProjectileResolution.Value,
projectile : Projectile,
target : SourceEntry,
damage_model : DamageResistanceModel,
hit_pos : Vector3,
hit_time : Long = System.nanoTime)

View file

@ -0,0 +1,42 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle}
import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.types.{PlanetSideEmpire, Vector3}
trait SourceEntry extends WorldEntity {
def Name : String = ""
def Definition : ObjectDefinition
def CharId : Long = 0L
def Faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
def Position_=(pos : Vector3) = Position
def Orientation_=(pos : Vector3) = Position
def Velocity_=(pos : Option[Vector3]) = Velocity
}
object SourceEntry {
final val None = new SourceEntry() {
def Definition = null
def Position = Vector3.Zero
def Orientation = Vector3.Zero
def Velocity = Some(Vector3.Zero)
}
def apply(target : PlanetSideGameObject with FactionAffinity) : SourceEntry = {
target match {
case obj : Player => PlayerSource(obj)
case obj : Vehicle => VehicleSource(obj)
case _ => ObjectSource(target)
}
}
def NameFormat(name : String) : String = {
name.replace("_", " ")
.split(" ")
.map(_.capitalize)
.mkString(" ")
}
}

View file

@ -0,0 +1,37 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.Vehicle
import net.psforever.objects.definition.VehicleDefinition
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class VehicleSource(obj_def : VehicleDefinition,
faction : PlanetSideEmpire.Value,
health : Int,
shields : Int,
position : Vector3,
orientation : Vector3,
velocity : Option[Vector3] = None) extends SourceEntry {
override def Name = SourceEntry.NameFormat(obj_def.Name)
override def Faction = faction
def Definition : VehicleDefinition = obj_def
def Health = health
def Shields = shields
def Position = position
def Orientation = orientation
def Velocity = velocity
}
object VehicleSource {
def apply(obj : Vehicle) : VehicleSource = {
VehicleSource(
obj.Definition,
obj.Faction,
obj.Health,
obj.Shields,
obj.Position,
obj.Orientation,
obj.Velocity
)
}
}

View file

@ -1,14 +1,17 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.definition package net.psforever.objects.definition
import net.psforever.objects.ballistics.{DamageProfile, DamageType, Projectiles} import net.psforever.objects.ballistics.Projectiles
import net.psforever.objects.vital.DamageType
import net.psforever.objects.vital.damage.DamageProfile
/** /**
* The definition that outlines the damage-dealing characteristics of any projectile. * The definition that outlines the damage-dealing characteristics of any projectile.
* `Tool` objects emit `ProjectileDefinition` objects and that is later wrapped into a `Projectile` object. * `Tool` objects emit `ProjectileDefinition` objects and that is later wrapped into a `Projectile` object.
* @param objectId the object's identifier number * @param objectId the object's identifier number
*/ */
class ProjectileDefinition(objectId : Int) extends ObjectDefinition(objectId) with DamageProfile { class ProjectileDefinition(objectId : Int) extends ObjectDefinition(objectId)
with DamageProfile {
private val projectileType : Projectiles.Value = Projectiles(objectId) //let throw NoSuchElementException private val projectileType : Projectiles.Value = Projectiles(objectId) //let throw NoSuchElementException
private var damage0 : Int = 0 private var damage0 : Int = 0
private var damage1 : Option[Int] = None private var damage1 : Option[Int] = None
@ -26,6 +29,11 @@ class ProjectileDefinition(objectId : Int) extends ObjectDefinition(objectId) wi
private var damageAtEdge : Float = 1f private var damageAtEdge : Float = 1f
private var damageRadius : Float = 1f private var damageRadius : Float = 1f
private var useDamage1Subtract : Boolean = false private var useDamage1Subtract : Boolean = false
//derived calculations
private var distanceMax : Float = 0f
private var distanceFromAcceleration : Float = 0f
private var distanceNoDegrade : Float = 0f
private var finalVelocity : Float = 0f
Name = "projectile" Name = "projectile"
def ProjectileType : Projectiles.Value = projectileType def ProjectileType : Projectiles.Value = projectileType
@ -157,10 +165,43 @@ class ProjectileDefinition(objectId : Int) extends ObjectDefinition(objectId) wi
this.damageRadius = damageRadius this.damageRadius = damageRadius
DamageRadius DamageRadius
} }
def DistanceMax : Float = distanceMax //accessor only
def DistanceFromAcceleration : Float = distanceFromAcceleration //accessor only
def DistanceNoDegrade : Float = distanceNoDegrade //accessor only
def FinalVelocity : Float = finalVelocity //accessor only
} }
object ProjectileDefinition { object ProjectileDefinition {
def apply(projectileType : Projectiles.Value) : ProjectileDefinition = { def apply(projectileType : Projectiles.Value) : ProjectileDefinition = {
new ProjectileDefinition(projectileType.id) new ProjectileDefinition(projectileType.id)
} }
def CalculateDerivedFields(pdef : ProjectileDefinition) : Unit = {
val (distanceMax, distanceFromAcceleration, finalVelocity) : (Float, Float, Float) = if(pdef.Acceleration == 0f) {
(pdef.InitialVelocity * pdef.Lifespan, 0, pdef.InitialVelocity)
}
else {
val distanceFromAcceleration = (pdef.AccelerationUntil * pdef.InitialVelocity) + (0.5f * pdef.Acceleration * pdef.AccelerationUntil * pdef.AccelerationUntil)
val finalVelocity = pdef.InitialVelocity + pdef.Acceleration * pdef.AccelerationUntil
val distanceAfterAcceleration = finalVelocity * (pdef.Lifespan - pdef.AccelerationUntil)
(distanceFromAcceleration + distanceAfterAcceleration, distanceFromAcceleration, finalVelocity)
}
pdef.distanceMax = distanceMax
pdef.distanceFromAcceleration = distanceFromAcceleration
pdef.finalVelocity = finalVelocity
pdef.distanceNoDegrade = if(pdef.DegradeDelay == 0f) {
pdef.distanceMax
}
else if(pdef.DegradeDelay < pdef.AccelerationUntil) {
(pdef.DegradeDelay * pdef.InitialVelocity) + (0.5f * pdef.Acceleration * pdef.DegradeDelay * pdef.DegradeDelay)
}
else {
pdef.distanceFromAcceleration + pdef.finalVelocity * (pdef.DegradeDelay - pdef.AccelerationUntil)
}
}
} }

View file

@ -3,17 +3,22 @@ package net.psforever.objects.definition
import net.psforever.objects.definition.converter.VehicleConverter import net.psforever.objects.definition.converter.VehicleConverter
import net.psforever.objects.inventory.InventoryTile import net.psforever.objects.inventory.InventoryTile
import net.psforever.objects.vehicles.UtilityType import net.psforever.objects.vehicles.{DestroyedVehicle, UtilityType}
import net.psforever.objects.vital._
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.duration._ import scala.concurrent.duration._
/** /**
* An object definition system used to construct and retain the parameters of various vehicles. * An object definition system used to construct and retain the parameters of various vehicles.
* @param objectId the object id the is associated with this sort of `Vehicle` * @param objectId the object id that is associated with this sort of `Vehicle`
*/ */
class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) { class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId)
with ResistanceProfileMutators
with DamageResistanceModel {
private var maxHealth : Int = 100 private var maxHealth : Int = 100
/** vehicle shields offered through amp station facility benefits (generally: 20% of health + 1) */
private var maxShields : Int = 0 private var maxShields : Int = 0
/* key - seat index, value - seat object */ /* key - seat index, value - seat object */
private val seats : mutable.HashMap[Int, SeatDefinition] = mutable.HashMap[Int, SeatDefinition]() private val seats : mutable.HashMap[Int, SeatDefinition] = mutable.HashMap[Int, SeatDefinition]()
@ -33,8 +38,12 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) {
private var serverVehicleOverrideSpeeds : (Int, Int) = (0, 0) private var serverVehicleOverrideSpeeds : (Int, Int) = (0, 0)
private var deconTime : Option[FiniteDuration] = None private var deconTime : Option[FiniteDuration] = None
private var maximumCapacitor : Int = 0 private var maximumCapacitor : Int = 0
private var destroyedModel : Option[DestroyedVehicle.Value] = None
Name = "vehicle" Name = "vehicle"
Packet = VehicleDefinition.converter Packet = VehicleDefinition.converter
Damage = StandardVehicleDamage
Resistance = StandardVehicleResistance
Model = StandardResolutions.Vehicle
def MaxHealth : Int = maxHealth def MaxHealth : Int = maxHealth
@ -138,6 +147,13 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) {
maximumCapacitor = maxCapacitor maximumCapacitor = maxCapacitor
MaximumCapacitor MaximumCapacitor
} }
def DestroyedModel : Option[DestroyedVehicle.Value] = destroyedModel
def DestroyedModel_=(model : Option[DestroyedVehicle.Value]) : Option[DestroyedVehicle.Value] = {
destroyedModel = model
DestroyedModel
}
} }
object VehicleDefinition { object VehicleDefinition {

View file

@ -0,0 +1,25 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.definition.converter
import net.psforever.objects.Vehicle
import net.psforever.packet.game.objectcreate.{DestroyedVehicleData, PlacementData}
import scala.util.{Failure, Success, Try}
class DestroyedVehicleConverter extends ObjectCreateConverter[Vehicle]() {
override def DetailedConstructorData(obj : Vehicle) : Try[DestroyedVehicleData] =
Failure(new Exception("DestroyedVehicleConverter should not be used to generate detailed DestroyedVehicleData (nothing should)"))
override def ConstructorData(obj : Vehicle) : Try[DestroyedVehicleData] = {
if(obj.Health > 0) {
Failure(new Exception("Vehicle used on DestroyedVehicleConverter has not yet been destroyed (Health == 0)"))
}
else {
Success(DestroyedVehicleData(PlacementData(obj.Position, obj.Orientation)))
}
}
}
object DestroyedVehicleConverter {
final val converter = new DestroyedVehicleConverter
}

View file

@ -4,7 +4,8 @@ package net.psforever.objects.definition.converter
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
import net.psforever.objects.Vehicle import net.psforever.objects.Vehicle
import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.game.PlanetSideGUID
import net.psforever.packet.game.objectcreate.{InventoryItemData, _} import net.psforever.packet.game.objectcreate.{InventoryData, InventoryItemData, ObjectClass, PlacementData, SpecificVehicleData, VehicleData, VehicleFormat}
import net.psforever.types.DriveState
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
@ -14,31 +15,57 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
override def ConstructorData(obj : Vehicle) : Try[VehicleData] = { override def ConstructorData(obj : Vehicle) : Try[VehicleData] = {
val health = 255 * obj.Health / obj.MaxHealth //TODO not precise val health = 255 * obj.Health / obj.MaxHealth //TODO not precise
Success( if(health > 3) { //active
VehicleData( Success(
PlacementData(obj.Position, obj.Orientation, obj.Velocity), VehicleData(
obj.Faction, PlacementData(obj.Position, obj.Orientation, obj.Velocity),
bops = false, obj.Faction,
destroyed = health < 3, bops = false,
unk1 = 0, destroyed = false,
obj.Jammered, unk1 = 0,
unk2 = false, obj.Jammered,
obj.Owner match { unk2 = false,
case Some(owner) => owner obj.Owner match {
case None => PlanetSideGUID(0) case Some(owner) => owner
}, case None => PlanetSideGUID(0)
unk3 = false, },
health, unk3 = false,
unk4 = false, health,
no_mount_points = false, unk4 = false,
obj.DeploymentState, no_mount_points = false,
unk5 = false, obj.DeploymentState,
unk6 = false, unk5 = false,
obj.Cloaked, unk6 = false,
SpecificFormatData(obj), obj.Cloaked,
Some(InventoryData(MakeDriverSeat(obj) ++ MakeUtilities(obj) ++ MakeMountings(obj))) SpecificFormatData(obj),
)(SpecificFormatModifier) Some(InventoryData(MakeDriverSeat(obj) ++ MakeUtilities(obj) ++ MakeMountings(obj)))
) )(SpecificFormatModifier)
)
}
else { //destroyed
Success(
VehicleData(
PlacementData(obj.Position, obj.Orientation),
obj.Faction,
bops = false,
destroyed = true,
unk1 = 0,
jammered = false,
unk2 = false,
owner_guid = PlanetSideGUID(0),
unk3 = false,
health = 0,
unk4 = false,
no_mount_points = true,
driveState = DriveState.Mobile,
unk5 = false,
unk6 = false,
cloak = false,
SpecificFormatData(obj),
inventory = None
)(SpecificFormatModifier)
)
}
} }
private def MakeDriverSeat(obj : Vehicle) : List[InventoryItemData.InventoryItem] = { private def MakeDriverSeat(obj : Vehicle) : List[InventoryItemData.InventoryItem] = {

View file

@ -2,7 +2,7 @@
package net.psforever.objects.equipment package net.psforever.objects.equipment
import net.psforever.objects.Tool import net.psforever.objects.Tool
import net.psforever.objects.ballistics.DamageProfile import net.psforever.objects.vital.damage.DamageProfile
import scala.collection.mutable import scala.collection.mutable

View file

@ -96,18 +96,16 @@ class VehicleSpawnControl(pad : VehicleSpawnPad) extends VehicleSpawnControlBase
} }
case VehicleSpawnControl.ProcessControl.Flush => case VehicleSpawnControl.ProcessControl.Flush =>
if(!periodicReminder.isCancelled) { periodicReminder.cancel
periodicReminder.cancel orders.foreach { VehicleSpawnControl.CancelOrder(_, Continent) }
orders.foreach { VehicleSpawnControl.CancelOrder(_, Continent) } orders = Nil
orders = Nil trackedOrder match {
trackedOrder match { case Some(entry) =>
case Some(entry) => VehicleSpawnControl.CancelOrder(entry, Continent)
VehicleSpawnControl.CancelOrder(entry, Continent) case None => ;
case None => ;
}
trackedOrder = None
concealPlayer ! akka.actor.Kill //will cause the actor to restart, which will abort any trapped messages
} }
trackedOrder = None
concealPlayer ! akka.actor.Kill //will cause the actor to restart, which will abort any trapped messages
case _ => ; case _ => ;
} }

View file

@ -0,0 +1,33 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vehicles
/**
*
*/
object DestroyedVehicle extends Enumeration {
type Type = Value
val Ams = Value(47)
val Ant = Value(61)
val Apc = Value(65)
val Dropship = Value(260)
val Flail = Value(295)
val Liberator = Value(439)
val LightGunship = Value(442)
val Lightning = Value(447)
val Lodestar = Value(460)
val Magrider = Value(471)
val Mosquito = Value(573)
val MediumTransport = Value(533)
val Prowler = Value(698)
val QuadAssault = Value(708)
val QuadStealth = Value(711)
val Router = Value(742)
val Skyguard = Value(785)
val Switchblade = Value(848)
val ThreeManHeavyBuggy = Value(863)
val TwoManAssaultBuggy = Value(897)
val TwoManHeavyBuggy = Value(899)
val TwoManHoverBuggy = Value(901)
val Vanguard = Value(924)
}

View file

@ -3,9 +3,11 @@ package net.psforever.objects.vehicles
import akka.actor.Actor import akka.actor.Actor
import net.psforever.objects.Vehicle import net.psforever.objects.Vehicle
import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.deploy.DeploymentBehavior import net.psforever.objects.serverobject.deploy.DeploymentBehavior
import net.psforever.objects.vital.{VehicleShieldCharge, Vitality}
import net.psforever.types.ExoSuitType import net.psforever.types.ExoSuitType
/** /**
@ -59,6 +61,22 @@ class VehicleControl(vehicle : Vehicle) extends Actor
sender ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, seat_num)) sender ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, seat_num))
} }
case Vitality.Damage(damage_func) =>
if(vehicle.Health > 0) {
damage_func(vehicle)
sender ! Vitality.DamageResolution(vehicle)
}
case Vehicle.ChargeShields(amount) =>
val now : Long = System.nanoTime
//make certain vehicle doesn't charge shields too quickly
if(vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields &&
!vehicle.History.exists(VehicleControl.LastShieldChargeOrDamage(now))) {
vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), amount))
vehicle.Shields = vehicle.Shields + amount
sender ! Vehicle.UpdateShieldsCharge(vehicle)
}
case FactionAffinity.ConvertFactionAffinity(faction) => case FactionAffinity.ConvertFactionAffinity(faction) =>
val originalAffinity = vehicle.Faction val originalAffinity = vehicle.Faction
if(originalAffinity != (vehicle.Faction = faction)) { if(originalAffinity != (vehicle.Faction = faction)) {
@ -81,3 +99,24 @@ class VehicleControl(vehicle : Vehicle) extends Actor
case _ => ; case _ => ;
} }
} }
object VehicleControl {
import net.psforever.objects.vital.{DamageFromProjectile, VehicleShieldCharge, VitalsActivity}
import scala.concurrent.duration._
/**
* Determine if a given activity entry would invalidate the act of charging vehicle shields this tick.
* @param now the current time (in nanoseconds)
* @param act a `VitalsActivity` entry to test
* @return `true`, if the vehicle took damage in the last five seconds or
* charged shields in the last second;
* `false`, otherwise
*/
def LastShieldChargeOrDamage(now : Long)(act : VitalsActivity) : Boolean = {
act match {
case DamageFromProjectile(data) => now - data.hit_time < (5 seconds).toNanos //damage delays next charge by 5s
case vsc : VehicleShieldCharge => now - vsc.time < (1 seconds).toNanos //previous charge delays next by 1s
case _ => false
}
}
}

View file

@ -0,0 +1,80 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
import net.psforever.objects.ballistics.{ProjectileResolution, ResolvedProjectile}
import net.psforever.objects.vital.damage.DamageSelection
import net.psforever.objects.vital.projectile.ProjectileCalculations
import net.psforever.objects.vital.resistance.ResistanceSelection
import net.psforever.objects.vital.resolution.ResolutionCalculations
/**
* The functionality that is necessary for interaction of a vital game object with the rest of the game world.<br>
* <br>
* A vital object can be hurt or damaged or healed or repaired (HDHR).
* The actual implementation of how that works is left to the specific object and its interfaces, however.
* The more involved values that are applied to the vital object are calculated by a series of functions
* that contribute different values, e.g., the value for being damaged.
* "Being damaged" is also not the same for all valid targets:
* some targets don't utilize the same kinds of values in the same way as another,
* and some targets utilize a different assortment of values than either of the first two examples.
* The damage model is a common interface for producing those values
* and reconciling those values with a valid target object
* without much fuss.<br>
* <br>
* By default, nothing should do anything of substance.
* @see `Vitality`
*/
trait DamageResistanceModel {
/** the functionality that processes damage; required */
private var damage : DamageSelection = NoDamageSelection
/** the functionality that processes resistance; optional */
private var resistance : ResistanceSelection = NoResistanceSelection
/** the functionality that prepares for damage application actions; required */
private var model : ResolutionCalculations.Form = NoResolutions.Calculate
def Damage : DamageSelection = damage
def Damage_=(selector : DamageSelection) : DamageSelection = {
damage = selector
Damage
}
def Resistance : ResistanceSelection = resistance
def Resistance_=(selector : ResistanceSelection) : ResistanceSelection = {
resistance = selector
Resistance
}
def Model : ResolutionCalculations.Form = model
def Model_=(selector : ResolutionCalculations.Form) : ResolutionCalculations.Form = {
model = selector
Model
}
/**
* Magic stuff.
* @param data the historical `ResolvedProjectile` information
* @return a function literal that encapsulates delayed modification instructions for certain objects
*/
def Calculate(data : ResolvedProjectile) : ResolutionCalculations.Output = {
val dam : ProjectileCalculations.Form = Damage(data)
val res : ProjectileCalculations.Form = Resistance(data)
Model(dam, res, data)
}
/**
* Magic stuff.
* @param data the historical `ResolvedProjectile` information
* @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 = {
val dam : ProjectileCalculations.Form = Damage(resolution)
val res : ProjectileCalculations.Form = Resistance(resolution)
Model(dam, res, data)
}
}

View file

@ -1,5 +1,5 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics package net.psforever.objects.vital
/** /**
* An `Enumeration` of the damage type. * An `Enumeration` of the damage type.

View file

@ -0,0 +1,113 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.vital.damage._
import net.psforever.objects.vital.damage.DamageCalculations._
/**
* A protected super class for calculating "no damage."
* Used for `NoDamage` but also for the base of `*LashDamage` calculation objects
* to maintain the polymorphic identity of `DamageCalculations`.
*/
protected class NoDamageBase extends DamageCalculations(
DamageCalculations.NoDamage,
DamageWithModifiers(NoDamageAgainst),
TooFar
)
object NoDamage extends NoDamageBase
object InfantryHitDamage extends DamageCalculations(
DirectHitDamageWithDegrade,
DamageWithModifiers(DamageAgainstExoSuit),
DistanceBetweenTargetandSource
)
object MaxHitDamage extends DamageCalculations(
DirectHitDamageWithDegrade,
DamageWithModifiers(DamageAgainstMaxSuit),
DistanceBetweenTargetandSource
)
object VehicleHitDamage extends DamageCalculations(
DirectHitDamageWithDegrade,
DamageWithModifiers(DamageAgainstVehicle),
DistanceBetweenTargetandSource
)
object AircraftHitDamage extends DamageCalculations(
DirectHitDamageWithDegrade,
DamageWithModifiers(DamageAgainstAircraft),
DistanceBetweenTargetandSource
)
object InfantrySplashDamage extends DamageCalculations(
SplashDamageWithRadialDegrade,
DamageWithModifiers(DamageAgainstExoSuit),
DistanceFromExplosionToTarget
)
object MaxSplashDamage extends DamageCalculations(
SplashDamageWithRadialDegrade,
DamageWithModifiers(DamageAgainstMaxSuit),
DistanceFromExplosionToTarget
)
object VehicleSplashDamage extends DamageCalculations(
SplashDamageWithRadialDegrade,
DamageWithModifiers(DamageAgainstVehicle),
DistanceFromExplosionToTarget
)
object AircraftSplashDamage extends DamageCalculations(
SplashDamageWithRadialDegrade,
DamageWithModifiers(DamageAgainstAircraft),
DistanceFromExplosionToTarget
)
object InfantryLashDamage extends NoDamageBase {
override def Calculate(data : ResolvedProjectile) : Int = (InfantryHitDamage.Calculate(data) * 0.2f).toInt
}
object MaxLashDamage extends NoDamageBase {
override def Calculate(data : ResolvedProjectile) : Int = (MaxHitDamage.Calculate(data) * 0.2f).toInt
}
object VehicleLashDamage extends NoDamageBase {
override def Calculate(data : ResolvedProjectile) : Int = (VehicleHitDamage.Calculate(data) * 0.2f).toInt
}
object AircraftLashDamage extends NoDamageBase {
override def Calculate(data : ResolvedProjectile) : Int = (AircraftHitDamage.Calculate(data) * 0.2f).toInt
}
object NoDamageSelection extends DamageSelection {
def Direct = None
def Splash = None
def Lash = None
}
object StandardInfantryDamage extends DamageSelection {
def Direct = InfantryHitDamage.Calculate
def Splash = InfantrySplashDamage.Calculate
def Lash = InfantryLashDamage.Calculate
}
object StandardMaxDamage extends DamageSelection {
def Direct = MaxHitDamage.Calculate
def Splash = MaxSplashDamage.Calculate
def Lash = MaxLashDamage.Calculate
}
object StandardVehicleDamage extends DamageSelection {
def Direct = VehicleHitDamage.Calculate
def Splash = VehicleSplashDamage.Calculate
def Lash = VehicleLashDamage.Calculate
}
object StandardAircraftDamage extends DamageSelection {
def Direct = AircraftHitDamage.Calculate
def Splash = AircraftSplashDamage.Calculate
def Lash = AircraftLashDamage.Calculate
}

View file

@ -0,0 +1,26 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.vital.resistance.ResistanceProfile
/**
* The different values for four common methods of modifying incoming damage.
* Two of the four resistances are directly paired with forms of incoming damage.
* This is for defining pure accessor functions,
* based on the assumption that the implementing object's `Definition` is the primary `ResistanceProfile`.
*/
trait StandardResistanceProfile extends ResistanceProfile {
this : PlanetSideGameObject =>
//actually check that this will work for this implementing class
assert(Definition.isInstanceOf[ResistanceProfile], s"$this object definition must extend ResistanceProfile")
private val resistDef = Definition.asInstanceOf[ResistanceProfile] //cast only once
def ResistanceDirectHit : Int = resistDef.ResistanceDirectHit
def ResistanceSplash : Int = resistDef.ResistanceDirectHit
def ResistanceAggravated : Int = resistDef.ResistanceDirectHit
def RadiationShielding : Float = resistDef.ResistanceDirectHit
}

View file

@ -0,0 +1,72 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.projectile.ProjectileCalculations
import net.psforever.objects.vital.resistance.{ResistanceCalculations, ResistanceSelection}
object NoResistance extends ResistanceCalculations[SourceEntry](
ResistanceCalculations.ValidInfantryTarget,
ResistanceCalculations.NoResistExtractor
)
object InfantryHitResistance extends ResistanceCalculations[PlayerSource](
ResistanceCalculations.ValidInfantryTarget,
ResistanceCalculations.ExoSuitDirectExtractor
)
object InfantrySplashResistance extends ResistanceCalculations[PlayerSource](
ResistanceCalculations.ValidInfantryTarget,
ResistanceCalculations.ExoSuitSplashExtractor
)
object InfantryLashResistance extends ResistanceCalculations[PlayerSource](
ResistanceCalculations.ValidInfantryTarget,
ResistanceCalculations.NoResistExtractor
)
object InfantryAggravatedResistance extends ResistanceCalculations[PlayerSource](
ResistanceCalculations.ValidInfantryTarget,
ResistanceCalculations.ExoSuitAggravatedExtractor
)
object VehicleHitResistance extends ResistanceCalculations[VehicleSource](
ResistanceCalculations.ValidVehicleTarget,
ResistanceCalculations.VehicleDirectExtractor
)
object VehicleSplashResistance extends ResistanceCalculations[VehicleSource](
ResistanceCalculations.ValidVehicleTarget,
ResistanceCalculations.VehicleSplashExtractor
)
object VehicleLashResistance extends ResistanceCalculations[VehicleSource](
ResistanceCalculations.ValidVehicleTarget,
ResistanceCalculations.NoResistExtractor
)
object VehicleAggravatedResistance extends ResistanceCalculations[VehicleSource](
ResistanceCalculations.ValidVehicleTarget,
ResistanceCalculations.VehicleAggravatedExtractor
)
object NoResistanceSelection extends ResistanceSelection {
def Direct : ProjectileCalculations.Form = None
def Splash : ProjectileCalculations.Form = None
def Lash : ProjectileCalculations.Form = None
def Aggravated : ProjectileCalculations.Form = None
}
object StandardInfantryResistance extends ResistanceSelection {
def Direct : ProjectileCalculations.Form = InfantryHitResistance.Calculate
def Splash : ProjectileCalculations.Form = InfantrySplashResistance.Calculate
def Lash : ProjectileCalculations.Form = InfantryLashResistance.Calculate
def Aggravated : ProjectileCalculations.Form = InfantryAggravatedResistance.Calculate
}
object StandardVehicleResistance extends ResistanceSelection {
def Direct : ProjectileCalculations.Form = VehicleHitResistance.Calculate
def Splash : ProjectileCalculations.Form = VehicleSplashResistance.Calculate
def Lash : ProjectileCalculations.Form = VehicleLashResistance.Calculate
def Aggravated : ProjectileCalculations.Form = VehicleAggravatedResistance.Calculate
}

View file

@ -0,0 +1,31 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
import net.psforever.objects.vital.resolution._
object NoResolutions extends DamageResistCalculations(
ResolutionCalculations.NoDamage,
ResolutionCalculations.NoApplication
)
object InfantryResolutions extends DamageResistCalculations(
ResolutionCalculations.InfantryDamageAfterResist,
ResolutionCalculations.InfantryApplication
)
object MaxResolutions extends DamageResistCalculations(
ResolutionCalculations.MaxDamageAfterResist,
ResolutionCalculations.InfantryApplication
)
object VehicleResolutions extends DamageResistCalculations(
ResolutionCalculations.VehicleDamageAfterResist,
ResolutionCalculations.VehicleApplication
)
object StandardResolutions extends ResolutionSelection {
def Infantry : ResolutionCalculations.Form = InfantryResolutions.Calculate
def Max : ResolutionCalculations.Form = MaxResolutions.Calculate
def Vehicle : ResolutionCalculations.Form = VehicleResolutions.Calculate
def Aircraft : ResolutionCalculations.Form = VehicleResolutions.Calculate
}

View file

@ -0,0 +1,110 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile, SourceEntry, VehicleSource}
import net.psforever.objects.definition.KitDefinition
import net.psforever.objects.serverobject.terminals.TerminalDefinition
import net.psforever.types.{ExoSuitType, ImplantType}
abstract class VitalsActivity(target : SourceEntry) {
def Target : SourceEntry = target
val t : Long = System.nanoTime //???
def time : Long = t
}
abstract class HealingActivity(target : SourceEntry) extends VitalsActivity(target)
abstract class DamagingActivity(target : SourceEntry) extends VitalsActivity(target)
final case class HealFromKit(target : PlayerSource, amount : Int, kit_def : KitDefinition) extends HealingActivity(target)
final case class HealFromTerm(target : PlayerSource, health : Int, armor : Int, term_def : TerminalDefinition) extends HealingActivity(target)
final case class HealFromImplant(target : PlayerSource, amount : Int, implant : ImplantType.Value) extends HealingActivity(target)
final case class HealFromExoSuitChange(target : PlayerSource, exosuit : ExoSuitType.Value) extends HealingActivity(target)
final case class RepairFromTerm(target : VehicleSource, amount : Int, term_def : TerminalDefinition) extends HealingActivity(target)
final case class VehicleShieldCharge(target : VehicleSource, amount : Int) extends HealingActivity(target) //TODO facility
final case class DamageFromProjectile(data : ResolvedProjectile) extends DamagingActivity(data.target)
final case class PlayerSuicide(target : PlayerSource) extends DamagingActivity(target)
/**
* A vital object can be hurt or damaged or healed or repaired (HDHR).
* The amount of HDHR is controlled by the damage model of this vital object reacting to stimulus.
* A history of the previous changes in vital statistics of the underlying object is recorded
* in reverse chronological order.
* The damage model is also provided.
*/
trait Vitality {
this : PlanetSideGameObject =>
/** a reverse-order list of chronological events that have occurred to these vital statistics */
private var vitalHistory : List[VitalsActivity] = List.empty[VitalsActivity]
def History : List[VitalsActivity] = vitalHistory
/**
* A `VitalsActivity` event must be recorded.
* Add new entry to the front of the list (for recent activity).
* @param action the fully-informed entry
* @return the list of previous changes to this object's vital statistics
*/
def History(action : VitalsActivity) : List[VitalsActivity] = {
vitalHistory = action +: vitalHistory
vitalHistory
}
/**
* Very common example of a `VitalsActivity` event involving weapon discharge.
* @param projectile the fully-informed entry of discharge of a weapon
* @return the list of previous changes to this object's vital statistics
*/
def History(projectile : ResolvedProjectile) : List[VitalsActivity] = {
vitalHistory = DamageFromProjectile(projectile) +: vitalHistory
vitalHistory
}
/**
* Find, specifically, the last instance of a weapon discharge vital statistics change.
* @return information about the discharge
*/
def LastShot : Option[ResolvedProjectile] = {
vitalHistory.find({p => p.isInstanceOf[DamageFromProjectile]}) match {
case Some(entry : DamageFromProjectile) =>
Some(entry.data)
case _ =>
None
}
}
def ClearHistory() : List[VitalsActivity] = {
val out = vitalHistory
vitalHistory = List.empty[VitalsActivity]
out
}
def DamageModel : DamageResistanceModel
}
object Vitality {
/**
* Provide the damage model-generated functionality
* that would properly enact the calculated changes of a vital statistics event
* upon a given vital object.
* @param func a function literal
*/
final case class Damage(func : (Any)=>Unit)
/**
* Report that a vitals object must be updated due to damage.
* @param obj the vital object
*/
final case class DamageResolution(obj : Vitality)
}

View file

@ -0,0 +1,129 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.damage
import net.psforever.types.Vector3
import net.psforever.objects.ballistics.{Projectile, ResolvedProjectile}
import net.psforever.objects.vital.projectile.ProjectileCalculations
import DamageCalculations._
/**
* The base class for function literal description related to calculating damage information.<br>
* <br>
* Implementing functionality of the children is the product of three user-defined processes
* and information for the calculation is extracted from the to-be-provided weapon discharge information.
* The specific functions passed into this object typically operate simultaneously normally
* and are related to the target and the kind of interaction the weapon discharge had with the target.
* @param damages function by which damage is modified by distance
* @param extractor function that recovers damage information
* @param distanceFunc a function to calculate the distance for scaling the damage, if valid
*/
abstract class DamageCalculations(damages : DamagesType,
extractor : DamageWithModifiersType,
distanceFunc : DistanceType) extends ProjectileCalculations {
/**
* Combine the damage and distance data extracted from the `ResolvedProjectile` entry.
* @param data the historical `ResolvedProjectile` information
* @return the damage value
*/
def Calculate(data : ResolvedProjectile) : Int = {
val projectile = data.projectile
damages(
projectile,
extractor(projectile.profile, List(projectile.fire_mode.Modifiers)),
distanceFunc(data)
)
}
}
object DamageCalculations {
//types
type DamagesType = (Projectile, Int, Float)=>Int
type DamageWithModifiersType = (DamageProfile, List[DamageProfile])=>Int
type DistanceType = (ResolvedProjectile)=>Float
//raw damage selectors
def NoDamageAgainst(profile : DamageProfile) : Int = 0
def DamageAgainstExoSuit(profile : DamageProfile) : Int = profile.Damage0
def DamageAgainstVehicle(profile : DamageProfile) : Int = profile.Damage1
def DamageAgainstAircraft(profile : DamageProfile) : Int = profile.Damage2
def DamageAgainstMaxSuit(profile : DamageProfile) : Int = profile.Damage3
def DamageAgainstUnknown(profile : DamageProfile) : Int = profile.Damage4
//raw damage selection functions
/**
* Get damage information from a series of profiles related to the weapon discharge.
* @param extractor the function that recovers the damage value
* @param base the profile from which primary damage is to be selected
* @param modifiers alternate profiles that will modify the base damage value
* @return the accumulated damage value
*/
//TODO modifiers come from various sources; expand this part of the calculation model in the future
def DamageWithModifiers(extractor : (DamageProfile)=>Int)(base : DamageProfile, modifiers : List[DamageProfile]) : Int = {
extractor(base) + modifiers.foldLeft(0)(_ + extractor(_))
}
//damage calculation functions
def NoDamage(projectile : Projectile, rawDamage : Int, distance : Float) : Int = 0
/**
* Modify the base damage based on the degrade distance of the projectile type
* and its maximum effective distance.
* Calls out "direct hit" damage but is recycled for other damage types as well.
* @param projectile information about the weapon discharge (itself)
* @param rawDamage the accumulated amount of damage
* @param distance how far the source was from the target
* @return the modified damage value
*/
def DirectHitDamageWithDegrade(projectile : Projectile, rawDamage: Int, distance: Float): Int = {
val profile = projectile.profile
if(distance <= profile.DistanceMax) {
if(profile.DistanceNoDegrade == profile.DistanceMax || distance <= profile.DistanceNoDegrade) {
rawDamage
}
else {
rawDamage - ((rawDamage - profile.DegradeMultiplier * rawDamage) * ((distance - profile.DistanceNoDegrade) / (profile.DistanceMax - profile.DistanceNoDegrade))).toInt
}
}
else {
0
}
}
/**
* Modify the base damage based on the radial distance of the target from the center of an explosion.
* Calls out "splash" damage exclusively.
* @param projectile information about the weapon discharge (itself)
* @param rawDamage the accumulated amount of damage
* @param distance how far the origin of the explosion was from the target
* @return the modified damage value
*/
def SplashDamageWithRadialDegrade(projectile : Projectile, rawDamage : Int, distance : Float) : Int = {
val radius = projectile.profile.DamageRadius
if(distance <= radius) {
val base : Float = projectile.profile.DamageAtEdge
val degrade : Float = (1 - base) * ((radius - distance) / radius) + base
rawDamage + (rawDamage * degrade).toInt
}
else {
0
}
}
//distance functions
def NoDistance(data : ResolvedProjectile) : Float = 0
def TooFar(data : ResolvedProjectile) : Float = Float.MaxValue
def DistanceBetweenTargetandSource(data : ResolvedProjectile) : Float = {
Vector3.Distance(data.target.Position, data.projectile.owner.Position)
}
def DistanceFromExplosionToTarget(data : ResolvedProjectile) : Float = {
Vector3.Distance(data.target.Position, data.hit_pos)
}
}

View file

@ -1,5 +1,5 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics package net.psforever.objects.vital.damage
/** /**
* The different values for five common types of damage that can be dealt, based on target and application. * The different values for five common types of damage that can be dealt, based on target and application.

View file

@ -0,0 +1,32 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.damage
import net.psforever.objects.ballistics.{ProjectileResolution, ResolvedProjectile}
import net.psforever.objects.vital.NoDamage
import net.psforever.objects.vital.projectile.ProjectileCalculations
/**
* Maintain information about three primary forms of damage calculation
* and a means to test which calculation is valid in a given situation.
*/
trait DamageSelection {
final def None : ProjectileCalculations.Form = NoDamage.Calculate
def Direct : ProjectileCalculations.Form
def Splash : ProjectileCalculations.Form
def Lash : 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(res : ProjectileResolution.Value) : ProjectileCalculations.Form = res match {
case ProjectileResolution.Hit => Direct
case ProjectileResolution.Splash => Splash
case ProjectileResolution.Lash => Lash
case _ => None
}
}

View file

@ -0,0 +1,20 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.projectile
import net.psforever.objects.ballistics.ResolvedProjectile
/**
* The base for all projectile-induced damage calculation function literals.
*/
trait ProjectileCalculations {
/**
* The exposed entry for the calculation function literal defined by this base.
* @param data the historical `ResolvedProjectile` information
* @return the calculated value
*/
def Calculate(data : ResolvedProjectile) : Int
}
object ProjectileCalculations {
type Form = (ResolvedProjectile)=>Int
}

View file

@ -0,0 +1,123 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resistance
import net.psforever.objects.{ExoSuitDefinition, GlobalDefinitions}
import net.psforever.objects.ballistics._
import net.psforever.objects.vital.projectile.ProjectileCalculations
import net.psforever.types.ExoSuitType
import scala.util.{Failure, Success, Try}
/**
* The base class for function literal description related to calculating resistance information.<br>
* <br>
* Implementing functionality of the children is the product of two user-defined processes
* and information for the calculation is extracted from the to-be-provided weapon discharge information.
* Specifically, the information is found as the `target` object which is a member of the said information.
* The specific functions passed into this object typically operate simultaneously normally
* and are related to the target and the kind of interaction the weapon discharge had with the target.
* @param validate determine if a more generic `target` object is actually an expected type;
* cast to and return that type of object
* @param extractor recover the resistance values from an approved type of object
* @tparam TargetType an internal type that converts between `validate`'s output and `extractor`'s input;
* in essence, should match the type of object container to which these resistances belong;
* never has to be defined explicitly, but will be checked upon object definition
*/
abstract class ResistanceCalculations[TargetType](validate : (ResolvedProjectile)=>Try[TargetType],
extractor : (TargetType)=>Int) extends ProjectileCalculations {
/**
* Get resistance valuess.
* @param data the historical `ResolvedProjectile` information
* @return the damage value
*/
def Calculate(data : ResolvedProjectile) : Int = {
validate(data) match {
case Success(target) =>
extractor(target)
case _ =>
0
}
}
}
object ResistanceCalculations {
private def failure(typeName : String) = Failure(new Exception(s"can not match expected target $typeName"))
//target identification
def InvalidTarget(data : ResolvedProjectile) : Try[SourceEntry] = failure(s"invalid ${data.target.Definition.Name}")
def ValidInfantryTarget(data : ResolvedProjectile) : Try[PlayerSource] = {
data.target match {
case target : PlayerSource =>
if(target.ExoSuit != ExoSuitType.MAX) { //max is not counted as an official infantry exo-suit type
Success(target)
}
else {
failure("infantry")
}
case _ =>
failure("infantry")
}
}
def ValidMaxTarget(data : ResolvedProjectile) : Try[PlayerSource] = {
data.target match {
case target : PlayerSource =>
if(target.ExoSuit == ExoSuitType.MAX) {
Success(target)
}
else {
failure("max")
}
case _ =>
failure("max")
}
}
def ValidVehicleTarget(data : ResolvedProjectile) : Try[VehicleSource] = {
data.target match {
case target : VehicleSource =>
if(!GlobalDefinitions.isFlightVehicle(target.Definition)) {
Success(target)
}
else {
failure("vehicle")
}
case _ =>
failure("vehicle")
}
}
def ValidAircraftTarget(data : ResolvedProjectile) : Try[VehicleSource] = {
data.target match {
case target : VehicleSource =>
if(GlobalDefinitions.isFlightVehicle(target.Definition)) {
Success(target)
}
else {
failure("aircraft")
}
case _ =>
failure("aircraft")
}
}
//extractors
def NoResistExtractor(target : SourceEntry) : Int = 0
def ExoSuitDirectExtractor(target : PlayerSource) : Int = ExoSuitDefinition.Select(target.ExoSuit).ResistanceDirectHit
def ExoSuitSplashExtractor(target : PlayerSource) : Int = ExoSuitDefinition.Select(target.ExoSuit).ResistanceSplash
def ExoSuitAggravatedExtractor(target : PlayerSource) : Int = ExoSuitDefinition.Select(target.ExoSuit).ResistanceAggravated
def ExoSuitRadiationExtractor(target : PlayerSource) : Float = ExoSuitDefinition.Select(target.ExoSuit).RadiationShielding
def VehicleDirectExtractor(target : VehicleSource) : Int = target.Definition.ResistanceDirectHit
def VehicleSplashExtractor(target : VehicleSource) : Int = target.Definition.ResistanceSplash
def VehicleAggravatedExtractor(target : VehicleSource) : Int = target.Definition.ResistanceAggravated
def VehicleRadiationExtractor(target : VehicleSource) : Float = target.Definition.RadiationShielding
}

View file

@ -0,0 +1,69 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resistance
import net.psforever.objects.vital.DamageType
/**
* The different values for four common methods of modifying incoming damage.
* Two of the four resistances are directly paired with forms of incoming damage.
* This is for defining pure accessor functions.
*/
trait ResistanceProfile {
def ResistanceDirectHit : Int
def ResistanceSplash : Int
def ResistanceAggravated : Int
def RadiationShielding : Float
def Resist(dtype : DamageType.Value) : Float = {
dtype match {
case DamageType.Direct => ResistanceDirectHit
case DamageType.Splash => ResistanceSplash
case DamageType.Aggravated => ResistanceAggravated
case DamageType.Radiation => RadiationShielding
case _ => 0
}
}
}
/**
* The different values for four common methods of modifying incoming damage.
* Two of the four resistances are directly paired with forms of incoming damage.
* This is for defining both accessor and mutator functions.
*/
trait ResistanceProfileMutators extends ResistanceProfile {
private var resistanceDirectHit : Int = 0
private var resistanceSplash : Int = 0
private var resistanceAggravated : Int = 0
private var radiationShielding : Float = 0f
def ResistanceDirectHit : Int = resistanceDirectHit
def ResistanceDirectHit_=(resist : Int) : Int = {
resistanceDirectHit = resist
ResistanceDirectHit
}
def ResistanceSplash : Int = resistanceSplash
def ResistanceSplash_=(resist : Int) : Int = {
resistanceSplash = resist
ResistanceSplash
}
def ResistanceAggravated : Int = resistanceAggravated
def ResistanceAggravated_=(resist : Int) : Int = {
resistanceAggravated = resist
ResistanceAggravated
}
def RadiationShielding : Float = radiationShielding
def RadiationShielding_=(resist : Float) : Float = {
radiationShielding = resist
RadiationShielding
}
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resistance
import net.psforever.objects.ballistics._
import net.psforever.objects.vital.NoResistance
import net.psforever.objects.vital.projectile.ProjectileCalculations
/**
* Maintain information about four primary forms of resistance calculation
* and a means to test which calculation is valid in a given situation.
*/
trait ResistanceSelection {
final def None : ProjectileCalculations.Form = NoResistance.Calculate
def Direct : ProjectileCalculations.Form
def Splash : ProjectileCalculations.Form
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(res : ProjectileResolution.Value) : ProjectileCalculations.Form = res match {
case ProjectileResolution.Hit => Direct
case ProjectileResolution.Splash => Splash
case ProjectileResolution.Lash => Lash
case _ => None
}
}

View file

@ -0,0 +1,27 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resolution
import net.psforever.objects.ballistics.ResolvedProjectile
import net.psforever.objects.vital.projectile.ProjectileCalculations
/**
* A specific implementation of `ResolutionCalculations` that deals with
* the damage value and the resistance value in a specific manner.
* (The input type of the function literal output of `calcFunc`.)
* @param calcFunc a function literal that retrieves the function
* that factors the affects of damage and resistance values
* @param applyFunc a function literal that applies the final modified values to a target object
* @tparam A an internal type that converts between `calcFunc`'s output and `applyFunc`'s input;
* never has to be defined explicitly, but will be checked upon object definition
*/
abstract class DamageResistCalculations[A](calcFunc : (ResolvedProjectile)=>((Int, Int)=>A),
applyFunc : (A, ResolvedProjectile)=>ResolutionCalculations.Output)
extends ResolutionCalculations {
def Calculate(damages : ProjectileCalculations.Form, resistances : ProjectileCalculations.Form, data : ResolvedProjectile) : ResolutionCalculations.Output = {
val dam : Int = damages(data)
val res : Int = resistances(data)
val mod = calcFunc(data)
val modDam = mod(dam, res)
applyFunc(modDam, data)
}
}

View file

@ -0,0 +1,160 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resolution
import net.psforever.objects.{Player, Vehicle}
import net.psforever.objects.ballistics.{PlayerSource, ResolvedProjectile}
import net.psforever.objects.vital.projectile.ProjectileCalculations
/**
* The base for the combining step of all projectile-induced damage calculation function literals.
*/
trait ResolutionCalculations {
/**
* The exposed entry for the calculation function literal defined by this base.
* @param damages the function literal that accumulates and calculates damages
* @param resistances the function literal that collects resistance values
* @param data the historical `ResolvedProjectile` information
* @return a function literal that encapsulates delayed modification instructions for certain objects
*/
def Calculate(damages : ProjectileCalculations.Form, resistances : ProjectileCalculations.Form, data : ResolvedProjectile) : ResolutionCalculations.Output
}
object ResolutionCalculations {
type Output = (Any)=>Unit
type Form = (ProjectileCalculations.Form, ProjectileCalculations.Form, ResolvedProjectile)=>Output
def NoDamage(data : ResolvedProjectile)(a : Int, b : Int) : Int = 0
def InfantryDamageAfterResist(data : ResolvedProjectile) : (Int, Int)=>(Int, Int) = {
data.target match {
case target : PlayerSource =>
InfantryDamageAfterResist(target.health, target.armor)
case _ =>
InfantryDamageAfterResist(0, 0)
}
}
def InfantryDamageAfterResist(currentHP : Int, currentArmor : Int)(damages : Int, resistance : Int) : (Int, Int) = {
if(damages > 0 && currentHP > 0) {
if(currentArmor <= 0) {
(damages, 0) //no armor; health damage
}
else if(damages > resistance) {
val resistedDam = damages - resistance
//(resistedDam, resistance)
if(resistance <= currentArmor) {
(resistedDam, resistance) //armor and health damage
}
else {
(resistedDam + (resistance - currentArmor), currentArmor) //deplete armor; health damage + bonus
}
}
else {
(0, damages) //too weak; armor damage (less than resistance)
}
}
else {
(0, 0) //no damage
}
}
def MaxDamageAfterResist(data : ResolvedProjectile) : (Int, Int)=>(Int, Int) = {
data.target match {
case target : PlayerSource =>
MaxDamageAfterResist(target.health, target.armor)
case _ =>
MaxDamageAfterResist(0, 0)
}
}
def MaxDamageAfterResist(currentHP : Int, currentArmor : Int)(damages : Int, resistance : Int) : (Int, Int) = {
val resistedDam = damages - resistance
if(resistedDam > 0 && currentHP > 0) {
if(currentArmor <= 0) {
(resistedDam, 0) //no armor; health damage
}
else if(resistedDam >= currentArmor) {
(resistedDam - currentArmor, currentArmor) //deplete armor; health damage
}
else {
(0, resistedDam) //too weak; armor damage (less than resistance)
}
}
else {
(0, 0) //no damage
}
}
/**
* Unlike with `Infantry*` and with `Max*`'s,
* `VehicleDamageAfterResist` does not necessarily need to validate its target object.
* The required input is sufficient.
* @param data the historical `ResolvedProjectile` information
* @return a function literal for dealing with damage values and resistance values together
*/
def VehicleDamageAfterResist(data : ResolvedProjectile) : (Int, Int)=>Int = {
VehicleDamageAfterResist
}
def VehicleDamageAfterResist(damages : Int, resistance : Int) : Int = {
if(damages > resistance) {
damages - resistance
}
else {
damages
}
}
def NoApplication(damageValue : Int, data : ResolvedProjectile)(target : Any) : Unit = { }
/**
* The expanded `(Any)=>Unit` function for infantry.
* Apply the damage values to the health field and personal armor field for an infantry target.
* @param damageValues a tuple containing damage values for: health, personal armor
* @param data the historical `ResolvedProjectile` information
* @param target the `Player` object to be affected by these damage values (at some point)
*/
def InfantryApplication(damageValues : (Int, Int), data : ResolvedProjectile)(target : Any) : Unit = target match {
case player : Player =>
val (a, b) = damageValues
//TODO Personal Shield implant test should go here and modify the values a and b
if(player.isAlive && !(a == 0 && b == 0)) {
player.History(data)
if(player.Armor - b < 0) {
player.Health = player.Health - a - (b - player.Armor)
player.Armor = 0
}
else {
player.Armor = player.Armor - b
player.Health = player.Health - a
}
}
case _ =>
}
/**
* The expanded `(Any)=>Unit` function for vehicles.
* Apply the damage value to the shield field and then the health field (that order) for a vehicle target.
* @param damage the raw damage
* @param data the historical `ResolvedProjectile` information
* @param target the `Vehicle` object to be affected by these damage values (at some point)
*/
def VehicleApplication(damage : Int, data : ResolvedProjectile)(target : Any) : Unit = target match {
case vehicle : Vehicle =>
if(vehicle.Health > 0) {
vehicle.History(data)
val shields = vehicle.Shields
if(shields > damage) {
vehicle.Shields = shields - damage
}
else if(shields > 0) {
vehicle.Health = vehicle.Health - (damage - shields)
vehicle.Shields = 0
}
else {
vehicle.Health = vehicle.Health - damage
}
}
case _ => ;
}
}

View file

@ -0,0 +1,12 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.vital.resolution
/**
* Maintain information about four target types as the entry points for damage calculation.
*/
trait ResolutionSelection {
def Infantry : ResolutionCalculations.Form
def Max : ResolutionCalculations.Form
def Vehicle : ResolutionCalculations.Form
def Aircraft : ResolutionCalculations.Form
}

View file

@ -1,6 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.packet.game package net.psforever.packet.game
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.PlanetSideEmpire import net.psforever.types.PlanetSideEmpire
import scodec.Codec import scodec.Codec
@ -15,47 +16,43 @@ import scodec.codecs._
* 3) victim information<br> * 3) victim information<br>
* In the case of a player kill, the player's name will be attributed directly. * In the case of a player kill, the player's name will be attributed directly.
* In the case of an absentee kill, a description of the method of death will be attributed. * In the case of an absentee kill, a description of the method of death will be attributed.
* In the case of a suicide, the player attributed is the player who was killed (message format displays only the victim). * In the case of a suicide, the player attributed is the player who was killed (message format displays only the victim).<br>
* The victim's name is byte-aligned with a 5-bit buffer.<br>
* <br> * <br>
* The four bytes that follow each name seems to be important to the identification of the associated player. * The four bytes that follow each name seems to be important to the identification of the associated player.
* The same value will be seen in every `DestroyDisplayMessage` that includes the player, with respect to whether they are listed as the "killer" or as the "victim." * The same value will be seen in every `DestroyDisplayMessage` that includes that player,
* This holds true for every entry within thie same login session, at least. * with respect to whether they are listed as the "killer" or as the "victim."
* Blanking these values out does not change anything about the format of the event message. * This holds true for every entry within the same login session, at least.
* In the case of absentee kills, for example, where there is no killer listed, this field has been zero'd (`00000000`).<br> * Blanking either of these values out does not change anything about the format of the event message.
* If the two ids match, the packet will interpreted as the "suicide" format, even if the names do not match.
* In the case of absentee kills where there is no killer listed, this field is zero'd.<br>
* <br> * <br>
* The faction affiliation is different from the normal way `PlanetSideEmpire` values are recorded. * When marked as being in a vehicle or a turret, the player's name will be enclosed within square brackets.
* The higher nibble will reflect the first part of the `PlanetSideEmpire` value. * The length of the player's name found at the start of the character string does not reflect
* An extra `20` will be added if the player is in a vehicle or turret at the time. * whether or not there will be square brackets (fortunately).
* When marked as being in a vehicle or turret, the player's name will be enclosed within square brackets. * The color of the resulting icon is borrowed from the attributed killer's faction affiliation if it can be determined
* The length of the player's name found at the start of the wide character string does not reflect whether or not there will be square brackets (fortunately).<br> * and the type of icon is the same as an object id.
* <br> * An unidentified method or a missing icon defaults to a skull and crossbones.
* The two bytes in between the killer section and the victim section are the method of homicide or suicide.
* The color of the resulting icon is borrowed from the attributed killer's faction affiliation if it can be determined.
* An unidentified method defaults to a skull and crossbones icon.
* The exact range of unique and valid icon values for this parameter is currently unknown.
* It is also unknown what the two bytes preceding `method` specify, as changing them does nothing to the displayed message.
* @param killer the name of the player who did the killing * @param killer the name of the player who did the killing
* @param killer_charId Same as CharacterInfoMessage * @param killer_charId same as CharacterInfoMessage
* @param killer_empire the empire affiliation of the killer * @param killer_empire the empire affiliation of the killer
* @param killer_inVehicle true, if the killer was in a vehicle at the time of the kill; false, otherwise * @param killer_in_vehicle true, if the killer was in a vehicle at the time of the kill; false, otherwise
* @param unk na; but does not like being set to 0 * @param unk na; but does not like being set to 0
* @param method modifies the icon in the message, related to the way the victim was killed * @param method modifies the icon in the message, related to the way the victim was killed
* @param victim the name of the player who was killed * @param victim the name of the player who was killed
* @param victim_charId Same as CharacterInfoMessage * @param victim_charId same as CharacterInfoMessage
* @param victim_empire the empire affiliation of the victim * @param victim_empire the empire affiliation of the victim
* @param victim_inVehicle true, if the victim was in a vehicle when he was killed; false, otherwise * @param victim_in_vehicle true, if the victim was in a vehicle when he was killed; false, otherwise
*/ */
final case class DestroyDisplayMessage(killer : String, final case class DestroyDisplayMessage(killer : String,
killer_charId : Long, killer_charId : Long,
killer_empire : PlanetSideEmpire.Value, killer_empire : PlanetSideEmpire.Value,
killer_inVehicle : Boolean, killer_in_vehicle : Boolean,
unk : Int, unk : Int,
method : Int, method : Int,
victim : String, victim : String,
victim_charId : Long, victim_charId : Long,
victim_empire : PlanetSideEmpire.Value, victim_empire : PlanetSideEmpire.Value,
victim_inVehicle : Boolean victim_in_vehicle : Boolean
) )
extends PlanetSideGamePacket { extends PlanetSideGamePacket {
type Packet = DestroyDisplayMessage type Packet = DestroyDisplayMessage
@ -68,12 +65,12 @@ object DestroyDisplayMessage extends Marshallable[DestroyDisplayMessage] {
("killer" | PacketHelpers.encodedWideString) :: ("killer" | PacketHelpers.encodedWideString) ::
("killer_charId" | ulongL(32)) :: ("killer_charId" | ulongL(32)) ::
("killer_empire" | PlanetSideEmpire.codec) :: ("killer_empire" | PlanetSideEmpire.codec) ::
("killer_inVehicle" | bool) :: ("killer_in_vehicle" | bool) ::
("unk" | uint16L) :: ("unk" | uint16L) ::
("method" | uint16L) :: ("method" | uint16L) ::
("victim" | PacketHelpers.encodedWideStringAligned(5)) :: ("victim" | PacketHelpers.encodedWideStringAligned(5)) ::
("victim_charId" | ulongL(32)) :: ("victim_charId" | ulongL(32)) ::
("victim_empire" | PlanetSideEmpire.codec) :: ("victim_empire" | PlanetSideEmpire.codec) ::
("victim_inVehicle" | bool) ("victim_in_vehicle" | bool)
).as[DestroyDisplayMessage] ).as[DestroyDisplayMessage]
} }

View file

@ -156,6 +156,10 @@ final case class PlanetsideAttributeMessage(player_guid : PlanetSideGUID,
} }
object PlanetsideAttributeMessage extends Marshallable[PlanetsideAttributeMessage] { object PlanetsideAttributeMessage extends Marshallable[PlanetsideAttributeMessage] {
def apply(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Int) : PlanetsideAttributeMessage = {
PlanetsideAttributeMessage(player_guid, attribute_type, attribute_value.toLong)
}
implicit val codec : Codec[PlanetsideAttributeMessage] = ( implicit val codec : Codec[PlanetsideAttributeMessage] = (
("player_guid" | PlanetSideGUID.codec) :: ("player_guid" | PlanetSideGUID.codec) ::
("attribute_type" | uint8L) :: ("attribute_type" | uint8L) ::

View file

@ -1,14 +1,15 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package services.avatar package services.avatar
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.{PlanetSideGameObject, Player} import net.psforever.objects.{PlanetSideGameObject, Player}
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.Container import net.psforever.objects.inventory.Container
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{CargoMountPointStatusMessage, PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent} import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent}
import net.psforever.types.ExoSuitType import net.psforever.types.{ExoSuitType, Vector3}
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
@ -21,9 +22,14 @@ object AvatarAction {
final case class ChangeFireState_Start(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class ChangeFireState_Start(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action
final case class ChangeFireState_Stop(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class ChangeFireState_Stop(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action
final case class ConcealPlayer(player_guid : PlanetSideGUID) extends Action final case class ConcealPlayer(player_guid : PlanetSideGUID) extends Action
final case class Damage(player_guid : PlanetSideGUID, target : Player, resolution_function : (Any)=>Unit) extends Action
final case class Destroy(victim : PlanetSideGUID, killer : PlanetSideGUID, weapon : PlanetSideGUID, pos : Vector3) extends Action
final case class DestroyDisplay(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int = 121) extends Action
final case class DropItem(player_guid : PlanetSideGUID, item : Equipment, zone : Zone) extends Action final case class DropItem(player_guid : PlanetSideGUID, item : Equipment, zone : Zone) extends Action
final case class EquipmentInHand(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action final case class EquipmentInHand(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action
final case class HitHint(source_guid : PlanetSideGUID, player_guid : PlanetSideGUID) extends Action
final case class LoadPlayer(player_guid : PlanetSideGUID, object_id : Int, target_guid : PlanetSideGUID, cdata : ConstructorData, pdata : Option[ObjectCreateMessageParent]) extends Action final case class LoadPlayer(player_guid : PlanetSideGUID, object_id : Int, target_guid : PlanetSideGUID, cdata : ConstructorData, pdata : Option[ObjectCreateMessageParent]) extends Action
final case class KilledWhileInVehicle(player_guid : PlanetSideGUID) extends Action
final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action
final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action
final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
@ -38,5 +44,4 @@ object AvatarAction {
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
} }

View file

@ -2,11 +2,12 @@
package services.avatar package services.avatar
import net.psforever.objects.Player import net.psforever.objects.Player
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID, PlayerStateMessageUpstream}
import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.objectcreate.ConstructorData
import net.psforever.types.ExoSuitType import net.psforever.types.{ExoSuitType, Vector3}
object AvatarResponse { object AvatarResponse {
trait Response trait Response
@ -17,8 +18,13 @@ object AvatarResponse {
final case class ChangeFireState_Start(weapon_guid : PlanetSideGUID) extends Response final case class ChangeFireState_Start(weapon_guid : PlanetSideGUID) extends Response
final case class ChangeFireState_Stop(weapon_guid : PlanetSideGUID) extends Response final case class ChangeFireState_Stop(weapon_guid : PlanetSideGUID) extends Response
final case class ConcealPlayer() extends Response final case class ConcealPlayer() extends Response
final case class EquipmentInHand(pkt : ObjectCreateMessage) extends Response final case class DamageResolution(target : Player, resolution_function : (Any)=>Unit) extends Response
final case class Destroy(victim : PlanetSideGUID, killer : PlanetSideGUID, weapon : PlanetSideGUID, pos : Vector3) extends Response
final case class DestroyDisplay(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int) extends Response
final case class DropItem(pkt : ObjectCreateMessage) extends Response final case class DropItem(pkt : ObjectCreateMessage) extends Response
final case class EquipmentInHand(pkt : ObjectCreateMessage) extends Response
final case class HitHint(source_guid : PlanetSideGUID) extends Response
final case class KilledWhileInVehicle() extends Response
final case class LoadPlayer(pkt : ObjectCreateMessage) extends Response final case class LoadPlayer(pkt : ObjectCreateMessage) extends Response
final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response
final case class ObjectHeld(slot : Int) extends Response final case class ObjectHeld(slot : Int) extends Response
@ -31,6 +37,4 @@ object AvatarResponse {
final case class SendResponse(msg: PlanetSideGamePacket) extends Response final case class SendResponse(msg: PlanetSideGamePacket) extends Response
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response // final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response
// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response
} }

View file

@ -65,6 +65,18 @@ class AvatarService extends Actor {
AvatarEvents.publish( AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ConcealPlayer()) AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ConcealPlayer())
) )
case AvatarAction.Damage(player_guid, target, resolution_function) =>
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.DamageResolution(target, resolution_function))
)
case AvatarAction.Destroy(victim, killer, weapon, pos) =>
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", victim, AvatarResponse.Destroy(victim, killer, weapon, pos))
)
case AvatarAction.DestroyDisplay(killer, victim, method, unk) =>
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.DestroyDisplay(killer, victim, method, unk))
)
case AvatarAction.DropItem(player_guid, item, zone) => case AvatarAction.DropItem(player_guid, item, zone) =>
val definition = item.Definition val definition = item.Definition
val objectData = DroppedItemData( val objectData = DroppedItemData(
@ -86,6 +98,14 @@ class AvatarService extends Actor {
AvatarResponse.EquipmentInHand(ObjectCreateMessage(definition.ObjectId, item.GUID, containerData, objectData)) AvatarResponse.EquipmentInHand(ObjectCreateMessage(definition.ObjectId, item.GUID, containerData, objectData))
) )
) )
case AvatarAction.HitHint(source_guid, player_guid) =>
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.HitHint(source_guid))
)
case AvatarAction.KilledWhileInVehicle(player_guid) =>
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.KilledWhileInVehicle())
)
case AvatarAction.LoadPlayer(player_guid, object_id, target_guid, cdata, pdata) => case AvatarAction.LoadPlayer(player_guid, object_id, target_guid, cdata, pdata) =>
val pkt = pdata match { val pkt = pdata match {
case Some(data) => case Some(data) =>
@ -169,22 +189,6 @@ class AvatarService extends Actor {
AvatarServiceReply.PlayerStateShift(killer) AvatarServiceReply.PlayerStateShift(killer)
)) ))
} }
case AvatarService.DestroyDisplay(killer, victim) =>
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(victim)
if (playerOpt.isDefined) {
val player: PlayerAvatar = playerOpt.get
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim,
AvatarServiceReply.DestroyDisplay(killer)
))
}
case AvatarService.HitHintReturn(source_guid,victim_guid) =>
val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(source_guid)
if (playerOpt.isDefined) {
val player: PlayerAvatar = playerOpt.get
AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim_guid,
AvatarServiceReply.DestroyDisplay(source_guid)
))
}
*/ */
case msg => case msg =>
log.warn(s"Unhandled message $msg from $sender") log.warn(s"Unhandled message $msg from $sender")

View file

@ -21,7 +21,9 @@ object VehicleAction {
final case class KickPassenger(player_guid : PlanetSideGUID, unk1 : Int, unk2 : Boolean, vehicle_guid : PlanetSideGUID) extends Action final case class KickPassenger(player_guid : PlanetSideGUID, unk1 : Int, unk2 : Boolean, vehicle_guid : PlanetSideGUID) extends Action
final case class LoadVehicle(player_guid : PlanetSideGUID, vehicle : Vehicle, vtype : Int, vguid : PlanetSideGUID, vdata : ConstructorData) extends Action final case class LoadVehicle(player_guid : PlanetSideGUID, vehicle : Vehicle, vtype : Int, vguid : PlanetSideGUID, vdata : ConstructorData) extends Action
final case class MountVehicle(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, seat : Int) extends Action final case class MountVehicle(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, seat : Int) extends Action
final case class ObjectDelete(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action
final case class Ownership(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID) extends Action final case class Ownership(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID) extends Action
final case class PlanetsideAttribute(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
final case class SeatPermissions(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID, seat_group : Int, permission : Long) extends Action final case class SeatPermissions(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID, seat_group : Int, permission : Long) extends Action
final case class StowEquipment(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action final case class StowEquipment(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action
final case class UnloadVehicle(player_guid : PlanetSideGUID, continent : Zone, vehicle : Vehicle) extends Action final case class UnloadVehicle(player_guid : PlanetSideGUID, continent : Zone, vehicle : Vehicle) extends Action

View file

@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package services.vehicle package services.vehicle
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.{PlanetSideGameObject, Vehicle} import net.psforever.objects.{PlanetSideGameObject, Vehicle}
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
@ -19,12 +18,14 @@ object VehicleResponse {
final case class DetachFromRails(vehicle_guid : PlanetSideGUID, rails_guid : PlanetSideGUID, rails_pos : Vector3, rails_rot : Float) extends Response final case class DetachFromRails(vehicle_guid : PlanetSideGUID, rails_guid : PlanetSideGUID, rails_pos : Vector3, rails_rot : Float) extends Response
final case class DismountVehicle(bailType : BailType.Value , unk2 : Boolean) extends Response final case class DismountVehicle(bailType : BailType.Value , unk2 : Boolean) extends Response
final case class EquipmentInSlot(pkt : ObjectCreateMessage) extends Response final case class EquipmentInSlot(pkt : ObjectCreateMessage) extends Response
final case class HitHint(source_guid : PlanetSideGUID) extends Response
final case class InventoryState(obj : PlanetSideGameObject, parent_guid : PlanetSideGUID, start : Int, con_data : ConstructorData) extends Response final case class InventoryState(obj : PlanetSideGameObject, parent_guid : PlanetSideGUID, start : Int, con_data : ConstructorData) extends Response
final case class InventoryState2(obj_guid : PlanetSideGUID, parent_guid : PlanetSideGUID, value : Int) extends Response final case class InventoryState2(obj_guid : PlanetSideGUID, parent_guid : PlanetSideGUID, value : Int) extends Response
final case class KickPassenger(seat_num : Int, kickedByDriver : Boolean, vehicle_guid : PlanetSideGUID) extends Response final case class KickPassenger(seat_num : Int, kickedByDriver : Boolean, vehicle_guid : PlanetSideGUID) extends Response
final case class LoadVehicle(vehicle : Vehicle, vtype : Int, vguid : PlanetSideGUID, vdata : ConstructorData) extends Response final case class LoadVehicle(vehicle : Vehicle, vtype : Int, vguid : PlanetSideGUID, vdata : ConstructorData) extends Response
final case class MountVehicle(object_guid : PlanetSideGUID, seat : Int) extends Response final case class MountVehicle(object_guid : PlanetSideGUID, seat : Int) extends Response
final case class Ownership(vehicle_guid : PlanetSideGUID) extends Response final case class Ownership(vehicle_guid : PlanetSideGUID) extends Response
final case class PlanetsideAttribute(vehicle_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Response
final case class ResetSpawnPad(pad_guid : PlanetSideGUID) extends Response final case class ResetSpawnPad(pad_guid : PlanetSideGUID) extends Response
final case class RevealPlayer(player_guid : PlanetSideGUID) extends Response final case class RevealPlayer(player_guid : PlanetSideGUID) extends Response
final case class SeatPermissions(vehicle_guid : PlanetSideGUID, seat_group : Int, permission : Long) extends Response final case class SeatPermissions(vehicle_guid : PlanetSideGUID, seat_group : Int, permission : Long) extends Response

View file

@ -92,6 +92,10 @@ class VehicleService extends Actor {
VehicleEvents.publish( VehicleEvents.publish(
VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.Ownership(vehicle_guid)) VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.Ownership(vehicle_guid))
) )
case VehicleAction.PlanetsideAttribute(exclude_guid, target_guid, attribute_type, attribute_value) =>
VehicleEvents.publish(
VehicleServiceResponse(s"/$forChannel/Vehicle", exclude_guid, VehicleResponse.PlanetsideAttribute(target_guid, attribute_type, attribute_value))
)
case VehicleAction.SeatPermissions(player_guid, vehicle_guid, seat_group, permission) => case VehicleAction.SeatPermissions(player_guid, vehicle_guid, seat_group, permission) =>
VehicleEvents.publish( VehicleEvents.publish(
VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.SeatPermissions(vehicle_guid, seat_group, permission)) VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.SeatPermissions(vehicle_guid, seat_group, permission))

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package objects package objects
import net.psforever.objects.definition.converter.{ACEConverter, CharacterSelectConverter, REKConverter} import net.psforever.objects.definition.converter.{ACEConverter, CharacterSelectConverter, DestroyedVehicleConverter, REKConverter}
import net.psforever.objects._ import net.psforever.objects._
import net.psforever.objects.definition._ import net.psforever.objects.definition._
import net.psforever.objects.equipment.CItem.{DeployedItem, Unit} import net.psforever.objects.equipment.CItem.{DeployedItem, Unit}
@ -9,6 +9,7 @@ import net.psforever.objects.equipment._
import net.psforever.objects.inventory.InventoryTile import net.psforever.objects.inventory.InventoryTile
import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.vehicles.DestroyedVehicle
import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.game.PlanetSideGUID
import net.psforever.packet.game.objectcreate._ import net.psforever.packet.game.objectcreate._
import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3}
@ -368,7 +369,40 @@ class ConverterTest extends Specification {
ams.Utilities(4)().GUID = PlanetSideGUID(417) ams.Utilities(4)().GUID = PlanetSideGUID(417)
ams.Definition.Packet.ConstructorData(ams).isSuccess mustEqual true ams.Definition.Packet.ConstructorData(ams).isSuccess mustEqual true
ok //TODO write more of this test }
"convert to packet (3)" in {
val
ams = Vehicle(GlobalDefinitions.ams)
ams.GUID = PlanetSideGUID(413)
ams.Health = 0 //destroyed vehicle
ams.Definition.Packet.ConstructorData(ams).isSuccess mustEqual true
//did not initialize the utilities, but the converter did not fail
}
}
"DestroyedVehicle" should {
"not convert a working vehicle" in {
val ams = Vehicle(GlobalDefinitions.ams)
ams.GUID = PlanetSideGUID(413)
ams.Health mustEqual 3000 //not destroyed vehicle
DestroyedVehicleConverter.converter.ConstructorData(ams).isFailure mustEqual true
}
"convert to packet" in {
val ams = Vehicle(GlobalDefinitions.ams)
ams.GUID = PlanetSideGUID(413)
ams.Health = 0
DestroyedVehicleConverter.converter.ConstructorData(ams).isSuccess mustEqual true
//did not initialize the utilities, but the converter did not fail
}
"not convert into a detailed packet" in {
val ams = Vehicle(GlobalDefinitions.ams)
ams.GUID = PlanetSideGUID(413)
ams.Health = 0
DestroyedVehicleConverter.converter.DetailedConstructorData(ams).isFailure mustEqual true
} }
} }
} }

View file

@ -0,0 +1,416 @@
// Copyright (c) 2017 PSForever
package objects
import net.psforever.objects._
import net.psforever.objects.vital.damage.{DamageCalculations, DamageProfile}
import DamageCalculations._
import net.psforever.objects.vital.resistance.ResistanceCalculations
import ResistanceCalculations._
import net.psforever.objects.vital.resolution.ResolutionCalculations
import ResolutionCalculations._
import net.psforever.objects.ballistics._
import net.psforever.objects.vital.Vitality
import net.psforever.types._
import org.specs2.mutable.Specification
class DamageCalculationsTests extends Specification {
"DamageCalculations" should {
val wep = GlobalDefinitions.galaxy_gunship_cannon
val wep_fmode = Tool(wep).FireMode
val wep_prof = wep_fmode.Modifiers.asInstanceOf[DamageProfile]
val proj = wep.ProjectileTypes.head
val proj_prof = proj.asInstanceOf[DamageProfile]
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
val target = Vehicle(GlobalDefinitions.fury)
target.Position = Vector3(10, 0, 0)
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3(50, 50, 0))
"extract no damage numbers" in {
NoDamageAgainst(proj_prof) mustEqual 0
}
"extract damage against exosuit target" in {
DamageAgainstExoSuit(proj_prof) mustEqual 50
}
"extract damage against MAX target" in {
DamageAgainstMaxSuit(proj_prof) mustEqual 75
}
"extract damage against vehicle target" in {
DamageAgainstVehicle(proj_prof) mustEqual 82
}
"extract damage against aircraft target" in {
DamageAgainstAircraft(proj_prof) mustEqual 82
}
"extract damage against something" in {
DamageAgainstUnknown(proj_prof) mustEqual 66
}
"extract a complete damage profile (1)" in {
val result = DamageAgainstVehicle(proj_prof) + DamageAgainstVehicle(wep_prof)
val func : (DamageProfile, List[DamageProfile]) => Int = DamageWithModifiers(DamageAgainstVehicle)
func(proj_prof, List(wep_prof)) mustEqual result
}
"extract a complete damage profile (2)" in {
val result = 2 * DamageAgainstVehicle(proj_prof) + DamageAgainstVehicle(wep_prof)
val func : (DamageProfile, List[DamageProfile]) => Int = DamageWithModifiers(DamageAgainstVehicle)
func(proj_prof, List(proj_prof, wep_prof)) mustEqual result
}
"calculate no distance" in {
NoDistance(resprojectile) mustEqual 0
}
"calculate too far distance" in {
TooFar(resprojectile) mustEqual Float.MaxValue
}
"calculate distance between target and source" in {
DistanceBetweenTargetandSource(resprojectile) mustEqual 10
}
"calculate distance between target and explosion (splash)" in {
DistanceFromExplosionToTarget(resprojectile) mustEqual 64.03124f
}
"calculate no damage from components" in {
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
val distance = DistanceFromExplosionToTarget(resprojectile)
DamageCalculations.NoDamage(projectile, result, distance) mustEqual 0
}
"calculate degraded damage from components (near)" in {
val projectile_alt = Projectile(GlobalDefinitions.galaxy_gunship_gun_projectile, //need projectile with degrade
wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
DirectHitDamageWithDegrade(projectile_alt, result, 0) mustEqual 132
}
"calculate degraded damage from components (medium)" in {
val projectile_alt = Projectile(GlobalDefinitions.galaxy_gunship_gun_projectile, //need projectile with degrade
wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
DirectHitDamageWithDegrade(projectile_alt, result, 250) mustEqual 103
}
"calculate degraded damage from components (far)" in {
val projectile_alt = Projectile(GlobalDefinitions.galaxy_gunship_gun_projectile, //need projectile with degrade
wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
DirectHitDamageWithDegrade(projectile_alt, result, 1000) mustEqual 0
}
"calculate splash damage from components (near)" in {
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
SplashDamageWithRadialDegrade(projectile, result, 0) mustEqual 264
}
"calculate splash damage from components (medium)" in {
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
SplashDamageWithRadialDegrade(projectile, result, 5) mustEqual 145
}
"calculate splash damage from components (far)" in {
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
SplashDamageWithRadialDegrade(projectile, result, 6) mustEqual 0
}
}
}
class ResistanceCalculationsTests extends Specification {
val wep = GlobalDefinitions.galaxy_gunship_cannon
val wep_fmode = Tool(wep).FireMode
val proj = wep.ProjectileTypes.head //GlobalDefinitions.heavy_grenade_projectile
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2,2,0), Vector3.Zero)
"ResistanceCalculations" should {
"ignore all targets" in {
val target = Vehicle(GlobalDefinitions.fury)
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
InvalidTarget(resprojectile).isFailure mustEqual true
}
"discern standard infantry targets" in {
val target = player
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
ValidInfantryTarget(resprojectile).isSuccess mustEqual true
ValidMaxTarget(resprojectile).isSuccess mustEqual false
ValidVehicleTarget(resprojectile).isSuccess mustEqual false
ValidAircraftTarget(resprojectile).isSuccess mustEqual false
}
"discern mechanized infantry targets" in {
val target = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
target.ExoSuit = ExoSuitType.MAX
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
ValidInfantryTarget(resprojectile).isSuccess mustEqual false
ValidMaxTarget(resprojectile).isSuccess mustEqual true
ValidVehicleTarget(resprojectile).isSuccess mustEqual false
ValidAircraftTarget(resprojectile).isSuccess mustEqual false
}
"discern ground vehicle targets" in {
val target = Vehicle(GlobalDefinitions.fury)
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
ValidInfantryTarget(resprojectile).isSuccess mustEqual false
ValidMaxTarget(resprojectile).isSuccess mustEqual false
ValidVehicleTarget(resprojectile).isSuccess mustEqual true
ValidAircraftTarget(resprojectile).isSuccess mustEqual false
}
"discern flying vehicle targets" in {
val target = Vehicle(GlobalDefinitions.mosquito)
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
ValidInfantryTarget(resprojectile).isSuccess mustEqual false
ValidMaxTarget(resprojectile).isSuccess mustEqual false
ValidVehicleTarget(resprojectile).isSuccess mustEqual false
ValidAircraftTarget(resprojectile).isSuccess mustEqual true
}
"extract no resistance values" in {
NoResistExtractor(SourceEntry(player)) mustEqual 0
}
"extract resistance values from exo-suit" in {
val pSource = PlayerSource(player)
ExoSuitDirectExtractor(pSource) mustEqual 4
ExoSuitSplashExtractor(pSource) mustEqual 15
ExoSuitAggravatedExtractor(pSource) mustEqual 8
ExoSuitRadiationExtractor(pSource) mustEqual 0
}
"extract resistance values from vehicle" in {
val vSource = VehicleSource(Vehicle(GlobalDefinitions.fury))
VehicleDirectExtractor(vSource) mustEqual 0
VehicleSplashExtractor(vSource) mustEqual 0
VehicleAggravatedExtractor(vSource) mustEqual 0
VehicleRadiationExtractor(vSource) mustEqual 0
}
}
}
class ResolutionCalculationsTests extends Specification {
val wep = GlobalDefinitions.suppressor
val wep_fmode = Tool(wep).FireMode
val proj = wep.ProjectileTypes.head
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
player.Spawn
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2,2,0), Vector3.Zero)
"ResolutionCalculations" should {
"calculate no damage" in {
val target = player
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
ResolutionCalculations.NoDamage(resprojectile)(50,50) mustEqual 0
}
"calculate no infantry damage for vehicles" in {
val target1 = Vehicle(GlobalDefinitions.fury) //!
val resprojectile1 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target1), target1.DamageModel, Vector3.Zero)
InfantryDamageAfterResist(resprojectile1)(50, 10) mustEqual (0,0)
val target2 = player
val resprojectile2 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target2), target2.DamageModel, Vector3.Zero)
InfantryDamageAfterResist(resprojectile2)(50, 10) mustEqual (40,10)
}
"calculate health and armor damage for infantry target" in {
InfantryDamageAfterResist(100,100)(50, 10) mustEqual (40,10)
}
"calculate health and armor damage, with bonus damage, for infantry target" in {
//health = 100, armor = 5 -> resist 10 but only have 5, so rollover extra -> damages (40+5, 5)
InfantryDamageAfterResist(100,5)(50, 10) mustEqual (45,5)
}
"calculate health damage for infantry target" in {
//health = 100, armor = 0
InfantryDamageAfterResist(100,0)(50, 10) mustEqual (50,0)
}
"calculate armor damage for infantry target" in {
//resistance > damage
InfantryDamageAfterResist(100,100)(50, 60) mustEqual (0,50)
}
val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
player2.ExoSuit = ExoSuitType.MAX
player2.Spawn
"calculate no max damage for vehicles" in {
val target1 = Vehicle(GlobalDefinitions.fury) //!
val resprojectile1 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target1), target1.DamageModel, Vector3.Zero)
MaxDamageAfterResist(resprojectile1)(50, 10) mustEqual (0,0)
val target2 = player2
val resprojectile2 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target2), target2.DamageModel, Vector3.Zero)
MaxDamageAfterResist(resprojectile2)(50, 10) mustEqual (0,40)
}
"calculate health and armor damage for max target" in {
MaxDamageAfterResist(100,5)(50, 10) mustEqual (35,5)
}
"calculate health damage for max target" in {
//health = 100, armor = 0
MaxDamageAfterResist(100,0)(50, 10) mustEqual (40,0)
}
"calculate armor damage for max target" in {
//resistance > damage
MaxDamageAfterResist(100,100)(50, 10) mustEqual (0,40)
}
"do not care if target is infantry for vehicle calculations" in {
val target1 = player
val resprojectile1 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target1), target1.DamageModel, Vector3.Zero)
VehicleDamageAfterResist(resprojectile1)(50, 10) mustEqual 40
val target2 = Vehicle(GlobalDefinitions.fury) //!
val resprojectile2 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target2), target2.DamageModel, Vector3.Zero)
VehicleDamageAfterResist(resprojectile2)(50, 10) mustEqual 40
}
}
"calculate resisted damage for vehicle target" in {
VehicleDamageAfterResist(50, 10) mustEqual 40
}
"calculate un-resisted damage for vehicle target" in {
VehicleDamageAfterResist(50, 0) mustEqual 50
}
}
class DamageModelTests extends Specification {
val wep = GlobalDefinitions.suppressor
val wep_tool = Tool(wep)
val wep_fmode = wep_tool.FireMode
val proj = wep.ProjectileTypes.head
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
player.Spawn
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2,2,0), Vector3.Zero)
"DamageModel" should {
"be a part of vitality" in {
player.isInstanceOf[Vitality] mustEqual true
try {
player.getClass.getDeclaredMethod("DamageModel").hashCode()
}
catch {
case _ : Exception =>
ko //the method doesn't exist
}
wep_tool.isInstanceOf[Vitality] mustEqual false
try {
wep_tool.getClass.getDeclaredMethod("DamageModel").hashCode()
ko
}
catch {
case _ : Exception =>
ok //the method doesn't exist
}
ok
}
"resolve infantry targets" in {
val tplayer = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
tplayer.Spawn
tplayer.Health mustEqual 100
tplayer.Armor mustEqual 50
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(tplayer), tplayer.DamageModel, Vector3.Zero)
val func : (Any)=>Unit = resprojectile.damage_model.Calculate(resprojectile)
func(tplayer)
tplayer.Health mustEqual 87
tplayer.Armor mustEqual 46
}
"resolve infantry targets in a specific way" in {
val tplayer = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
tplayer.Spawn
tplayer.Health mustEqual 100
tplayer.Armor mustEqual 50
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(tplayer), tplayer.DamageModel, Vector3.Zero)
val func : (Any)=>Unit = resprojectile.damage_model.Calculate(resprojectile, ProjectileResolution.Splash)
func(tplayer)
tplayer.Health mustEqual 81
tplayer.Armor mustEqual 35
}
"resolve infantry targets, with damage overflow" in {
val tplayer = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
tplayer.Spawn
tplayer.Health mustEqual 100
tplayer.Armor mustEqual 50
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(tplayer), tplayer.DamageModel, Vector3.Zero)
val func : (Any)=>Unit = resprojectile.damage_model.Calculate(resprojectile)
tplayer.Armor = 0
func(tplayer)
tplayer.Health mustEqual 83
tplayer.Armor mustEqual 0
}
"resolve vehicle targets" in {
val vehicle = Vehicle(GlobalDefinitions.fury)
vehicle.Health mustEqual 650
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(vehicle), vehicle.DamageModel, Vector3.Zero)
val func : (Any)=>Unit = resprojectile.damage_model.Calculate(resprojectile)
func(vehicle)
vehicle.Health mustEqual 641
}
"resolve vehicle targets (with shields)" in {
val vehicle = Vehicle(GlobalDefinitions.fury)
vehicle.Shields = 10
vehicle.Health mustEqual 650
vehicle.Shields mustEqual 10
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(vehicle), vehicle.DamageModel, Vector3.Zero)
val func : (Any)=>Unit = resprojectile.damage_model.Calculate(resprojectile)
func(vehicle)
vehicle.Health mustEqual 650
vehicle.Shields mustEqual 1
}
"resolve vehicle targets (losing shields)" in {
val vehicle = Vehicle(GlobalDefinitions.fury)
vehicle.Shields = 10
vehicle.Health mustEqual 650
vehicle.Shields mustEqual 10
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(vehicle), vehicle.DamageModel, Vector3.Zero)
val func : (Any)=>Unit = resprojectile.damage_model.Calculate(resprojectile)
func(vehicle)
vehicle.Health mustEqual 650
vehicle.Shields mustEqual 1
func(vehicle)
vehicle.Health mustEqual 642
vehicle.Shields mustEqual 0
}
"resolve vehicle targets in a specific way" in {
val vehicle = Vehicle(GlobalDefinitions.fury)
vehicle.Health mustEqual 650
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(vehicle), vehicle.DamageModel, Vector3.Zero)
val func : (Any)=>Unit = resprojectile.damage_model.Calculate(resprojectile, ProjectileResolution.Splash)
func(vehicle)
vehicle.Health mustEqual 632
}
}
}

View file

@ -1,19 +1,29 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package objects package objects
import net.psforever.objects.{GlobalDefinitions, LocalProjectile, Tool} import net.psforever.objects._
import net.psforever.objects.ballistics.{DamageType, Projectile, ProjectileResolution, Projectiles} import net.psforever.objects.ballistics._
import net.psforever.objects.definition.ProjectileDefinition import net.psforever.objects.definition.ProjectileDefinition
import net.psforever.types.Vector3 import net.psforever.objects.serverobject.mblocker.Locker
import net.psforever.objects.vital.DamageType
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.types._
import org.specs2.mutable.Specification import org.specs2.mutable.Specification
class ProjectileTest extends Specification { class ProjectileTest extends Specification {
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
val fury = Vehicle(GlobalDefinitions.fury)
"LocalProjectile" should { "LocalProjectile" should {
"construct" in { "construct" in {
val obj = new LocalProjectile() //since they're just placeholders, they only need to construct val obj = new LocalProjectile() //since they're just placeholders, they only need to construct
obj.Definition.ObjectId mustEqual 0 obj.Definition.ObjectId mustEqual 0
obj.Definition.Name mustEqual "projectile" obj.Definition.Name mustEqual "projectile"
} }
"local projectile range" in {
Projectile.BaseUID < Projectile.RangeUID mustEqual true
}
} }
"ProjectileDefinition" should { "ProjectileDefinition" should {
@ -151,48 +161,156 @@ class ProjectileTest extends Specification {
} }
} }
"Projectile" should { "SourceEntry" should {
"construct" in { "construct for players" in {
val beamer_wep = Tool(GlobalDefinitions.beamer) SourceEntry(player) match {
val projectile = beamer_wep.Projectile case o : PlayerSource =>
val obj = Projectile(projectile, beamer_wep.Definition, Vector3(1.2f, 3.4f, 5.6f), Vector3(0.2f, 0.4f, 0.6f)) o.Name mustEqual "TestCharacter"
o.Faction mustEqual PlanetSideEmpire.TR
o.Seated mustEqual false
o.ExoSuit mustEqual ExoSuitType.Standard
o.Health mustEqual 0
o.Armor mustEqual 0
o.Definition mustEqual GlobalDefinitions.avatar
o.Position mustEqual Vector3.Zero
o.Orientation mustEqual Vector3.Zero
o.Velocity mustEqual None
case _ =>
ko
}
}
obj.profile mustEqual beamer_wep.Projectile "construct for vehicles" in {
obj.tool_def mustEqual GlobalDefinitions.beamer SourceEntry(fury) match {
case o : VehicleSource =>
o.Name mustEqual "Fury"
o.Faction mustEqual PlanetSideEmpire.TR
o.Definition mustEqual GlobalDefinitions.fury
o.Health mustEqual 650
o.Shields mustEqual 0
o.Position mustEqual Vector3.Zero
o.Orientation mustEqual Vector3.Zero
o.Velocity mustEqual None
case _ =>
ko
}
}
"construct for generic object" in {
val obj = Locker()
SourceEntry(obj) match {
case o : ObjectSource =>
o.obj mustEqual obj
o.Name mustEqual "Mb Locker"
o.Faction mustEqual PlanetSideEmpire.NEUTRAL
o.Definition mustEqual GlobalDefinitions.mb_locker
o.Position mustEqual Vector3.Zero
o.Orientation mustEqual Vector3.Zero
o.Velocity mustEqual None
case _ =>
ko
}
}
"contain timely information" in {
val obj = Player(Avatar("TestCharacter-alt", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
obj.VehicleSeated = Some(PlanetSideGUID(1))
obj.Position = Vector3(1.2f, 3.4f, 5.6f)
obj.Orientation = Vector3(2.1f, 4.3f, 6.5f)
obj.Velocity = Some(Vector3(1.1f, 2.2f, 3.3f))
val sobj = SourceEntry(obj)
obj.VehicleSeated = None
obj.Position = Vector3.Zero
obj.Orientation = Vector3.Zero
obj.Velocity = None
obj.ExoSuit = ExoSuitType.Agile
sobj match {
case o : PlayerSource =>
o.Name mustEqual "TestCharacter-alt"
o.Faction mustEqual PlanetSideEmpire.TR
o.Seated mustEqual true
o.ExoSuit mustEqual ExoSuitType.Standard
o.Definition mustEqual GlobalDefinitions.avatar
o.Position mustEqual Vector3(1.2f, 3.4f, 5.6f)
o.Orientation mustEqual Vector3(2.1f, 4.3f, 6.5f)
o.Velocity mustEqual Some(Vector3(1.1f, 2.2f, 3.3f))
case _ =>
ko
}
}
}
"Projectile" should {
val beamer_def = GlobalDefinitions.beamer
val beamer_wep = Tool(beamer_def)
val firemode = beamer_wep.FireMode
val projectile = beamer_wep.Projectile
"construct" in {
val obj = Projectile(beamer_wep.Projectile, beamer_wep.Definition, beamer_wep.FireMode, PlayerSource(player), beamer_def.ObjectId, Vector3(1.2f, 3.4f, 5.6f), Vector3(0.2f, 0.4f, 0.6f))
obj.profile mustEqual projectile
obj.tool_def mustEqual beamer_def
obj.fire_mode mustEqual firemode
obj.owner match {
case _ : PlayerSource =>
ok
case _ =>
ko
}
obj.attribute_to mustEqual obj.tool_def.ObjectId
obj.shot_origin mustEqual Vector3(1.2f, 3.4f, 5.6f) obj.shot_origin mustEqual Vector3(1.2f, 3.4f, 5.6f)
obj.shot_angle mustEqual Vector3(0.2f, 0.4f, 0.6f) obj.shot_angle mustEqual Vector3(0.2f, 0.4f, 0.6f)
obj.resolution mustEqual ProjectileResolution.Unresolved
obj.fire_time <= System.nanoTime mustEqual true obj.fire_time <= System.nanoTime mustEqual true
obj.hit_time mustEqual 0 obj.isResolved mustEqual false
}
"construct (different attribute)" in {
val obj1 = Projectile(beamer_wep.Projectile, beamer_wep.Definition, beamer_wep.FireMode, player, Vector3(1.2f, 3.4f, 5.6f), Vector3(0.2f, 0.4f, 0.6f))
obj1.attribute_to mustEqual obj1.tool_def.ObjectId
val obj2 = Projectile(beamer_wep.Projectile, beamer_wep.Definition, beamer_wep.FireMode, PlayerSource(player), 65, Vector3(1.2f, 3.4f, 5.6f), Vector3(0.2f, 0.4f, 0.6f))
obj2.attribute_to == obj2.tool_def.ObjectId mustEqual false
obj2.attribute_to mustEqual 65
} }
"resolve" in { "resolve" in {
val beamer_wep = Tool(GlobalDefinitions.beamer) val obj = Projectile(projectile, beamer_def, firemode, PlayerSource(player), beamer_def.ObjectId, Vector3.Zero, Vector3.Zero)
val projectile = beamer_wep.Projectile obj.isResolved mustEqual false
val obj = Projectile(projectile, beamer_wep.Definition, Vector3(1.2f, 3.4f, 5.6f), Vector3(0.2f, 0.4f, 0.6f)) obj.isMiss mustEqual false
val obj2 = obj.Resolve(ProjectileResolution.MissedShot)
obj.resolution mustEqual ProjectileResolution.Unresolved obj.Resolve()
obj.fire_time <= System.nanoTime mustEqual true obj.isResolved mustEqual true
obj.hit_time mustEqual 0 obj.isMiss mustEqual false
obj2.resolution mustEqual ProjectileResolution.MissedShot
obj2.fire_time == obj.fire_time mustEqual true
obj2.hit_time <= System.nanoTime mustEqual true
obj2.fire_time <= obj2.hit_time mustEqual true
} }
"resolve, with coordinates" in { "missed" in {
val beamer_wep = Tool(GlobalDefinitions.beamer) val obj = Projectile(projectile, beamer_def, firemode, PlayerSource(player), beamer_def.ObjectId, Vector3.Zero, Vector3.Zero)
val projectile = beamer_wep.Projectile obj.isResolved mustEqual false
val obj = Projectile(projectile, beamer_wep.Definition, Vector3(1.2f, 3.4f, 5.6f), Vector3(0.2f, 0.4f, 0.6f)) obj.isMiss mustEqual false
val obj2 = obj.Resolve(Vector3(7.2f, 8.4f, 9.6f), Vector3(1.2f, 1.4f, 1.6f), ProjectileResolution.Resolved)
obj.resolution mustEqual ProjectileResolution.Unresolved obj.Miss()
obj.current.Position mustEqual Vector3.Zero obj.isResolved mustEqual true
obj.current.Orientation mustEqual Vector3.Zero obj.isMiss mustEqual true
obj2.resolution mustEqual ProjectileResolution.Resolved }
obj2.current.Position mustEqual Vector3(7.2f, 8.4f, 9.6f) }
obj2.current.Orientation mustEqual Vector3(1.2f, 1.4f, 1.6f)
"ResolvedProjectile" should {
val beamer_wep = Tool(GlobalDefinitions.beamer)
val p_source = PlayerSource(player)
val player2 = Player(Avatar("TestTarget", PlanetSideEmpire.NC, CharacterGender.Female, 1, CharacterVoice.Mute))
val p2_source = PlayerSource(player2)
val projectile = Projectile(beamer_wep.Projectile, GlobalDefinitions.beamer, beamer_wep.FireMode, p_source, GlobalDefinitions.beamer.ObjectId, Vector3.Zero, Vector3.Zero)
val fury_dm = fury.DamageModel
"construct" in {
val obj = ResolvedProjectile(ProjectileResolution.Hit, projectile, PlayerSource(player2), fury_dm, Vector3(1.2f, 3.4f, 5.6f), 123456L)
obj.resolution mustEqual ProjectileResolution.Hit
obj.projectile mustEqual projectile
obj.target mustEqual p2_source
obj.damage_model mustEqual fury.DamageModel
obj.hit_pos mustEqual Vector3(1.2f, 3.4f, 5.6f)
obj.hit_time mustEqual 123456L
} }
} }
} }

View file

@ -4,14 +4,16 @@ package objects
import akka.actor.Props import akka.actor.Props
import base.ActorTest import base.ActorTest
import net.psforever.objects._ import net.psforever.objects._
import net.psforever.objects.ballistics.{PlayerSource, Projectile, ProjectileResolution, ResolvedProjectile}
import net.psforever.objects.definition.{SeatDefinition, VehicleDefinition} import net.psforever.objects.definition.{SeatDefinition, VehicleDefinition}
import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles._
import net.psforever.objects.vital.{VehicleShieldCharge, Vitality}
import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.game.PlanetSideGUID
import net.psforever.types.{CharacterVoice, ExoSuitType} import net.psforever.types._
import org.specs2.mutable._ import org.specs2.mutable._
import scala.concurrent.duration.Duration import scala.concurrent.duration._
class VehicleTest extends Specification { class VehicleTest extends Specification {
import VehicleTest._ import VehicleTest._
@ -611,6 +613,103 @@ class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest {
} }
} }
class VehicleControlShieldsChargingTest extends ActorTest {
val vehicle = Vehicle(GlobalDefinitions.fury)
vehicle.GUID = PlanetSideGUID(10)
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
"charge vehicle shields" in {
assert(vehicle.Shields == 0)
assert(!vehicle.History.exists({p => p.isInstanceOf[VehicleShieldCharge]}))
vehicle.Actor ! Vehicle.ChargeShields(15)
val msg = receiveOne(500 milliseconds)
assert(msg.isInstanceOf[Vehicle.UpdateShieldsCharge])
assert(vehicle.Shields == 15)
assert(vehicle.History.exists({p => p.isInstanceOf[VehicleShieldCharge]}))
}
}
class VehicleControlShieldsNotChargingVehicleDeadTest extends ActorTest {
val vehicle = Vehicle(GlobalDefinitions.fury)
vehicle.GUID = PlanetSideGUID(10)
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
"not charge vehicle shields if the vehicle is destroyed" in {
assert(vehicle.Health > 0)
vehicle.Health = 0
assert(vehicle.Health == 0)
assert(vehicle.Shields == 0)
assert(!vehicle.History.exists({p => p.isInstanceOf[VehicleShieldCharge]}))
vehicle.Actor ! Vehicle.ChargeShields(15)
expectNoMsg(1 seconds)
assert(vehicle.Shields == 0)
assert(!vehicle.History.exists({p => p.isInstanceOf[VehicleShieldCharge]}))
}
}
class VehicleControlShieldsNotChargingVehicleShieldsFullTest extends ActorTest {
val vehicle = Vehicle(GlobalDefinitions.fury)
vehicle.GUID = PlanetSideGUID(10)
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
"not charge vehicle shields if the vehicle is destroyed" in {
assert(vehicle.Shields == 0)
vehicle.Shields = vehicle.MaxShields
assert(vehicle.Shields == vehicle.MaxShields)
assert(!vehicle.History.exists({p => p.isInstanceOf[VehicleShieldCharge]}))
vehicle.Actor ! Vehicle.ChargeShields(15)
expectNoMsg(1 seconds)
assert(!vehicle.History.exists({p => p.isInstanceOf[VehicleShieldCharge]}))
}
}
class VehicleControlShieldsNotChargingTooEarlyTest extends ActorTest {
val vehicle = Vehicle(GlobalDefinitions.fury)
vehicle.GUID = PlanetSideGUID(10)
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
"charge vehicle shields" in {
assert(vehicle.Shields == 0)
vehicle.Actor ! Vehicle.ChargeShields(15)
val msg = receiveOne(200 milliseconds)
assert(msg.isInstanceOf[Vehicle.UpdateShieldsCharge])
assert(vehicle.Shields == 15)
vehicle.Actor ! Vehicle.ChargeShields(15)
expectNoMsg(200 milliseconds)
assert(vehicle.Shields == 15)
}
}
class VehicleControlShieldsNotChargingDamagedTest extends ActorTest {
val vehicle = Vehicle(GlobalDefinitions.fury)
vehicle.GUID = PlanetSideGUID(10)
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test")
//
val beamer_wep = Tool(GlobalDefinitions.beamer)
val p_source = PlayerSource( Player(Avatar("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), System.nanoTime)
"charge vehicle shields" in {
assert(vehicle.Shields == 0)
vehicle.Actor ! Vitality.Damage({case v : Vehicle => v.History(obj)})
val msg = receiveOne(200 milliseconds)
assert(msg.isInstanceOf[Vitality.DamageResolution])
assert(vehicle.Shields == 0)
vehicle.Actor ! Vehicle.ChargeShields(15)
expectNoMsg(200 milliseconds)
assert(vehicle.Shields == 0)
}
}
object VehicleTest { object VehicleTest {
import net.psforever.objects.Avatar import net.psforever.objects.Avatar
import net.psforever.types.{CharacterGender, PlanetSideEmpire} import net.psforever.types.{CharacterGender, PlanetSideEmpire}

View file

@ -0,0 +1,83 @@
// Copyright (c) 2017 PSForever
package objects
import net.psforever.objects.ballistics._
import net.psforever.objects._
import net.psforever.objects.vital._
import net.psforever.types._
import org.specs2.mutable.Specification
class VitalityTest extends Specification {
"Vitality" should {
val wep = GlobalDefinitions.galaxy_gunship_cannon
val wep_fmode = Tool(wep).FireMode
val proj = wep.ProjectileTypes.head
val vehicle = Vehicle(GlobalDefinitions.fury)
val vSource = VehicleSource(vehicle)
"accept a variety of events" in {
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
val pSource = PlayerSource(player)
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(player), player.DamageModel, Vector3(50, 50, 0))
player.History(resprojectile) //ResolvedProjectile, straight-up
player.History(DamageFromProjectile(resprojectile))
player.History(HealFromKit(pSource, 10, GlobalDefinitions.medkit))
player.History(HealFromTerm(pSource, 10, 0, GlobalDefinitions.order_terminal))
player.History(HealFromImplant(pSource, 10, ImplantType.AdvancedRegen))
player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard))
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
player.History(VehicleShieldCharge(vSource, 10))
player.History(PlayerSuicide(pSource))
ok
}
"return and clear the former list of vital activities" in {
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
val pSource = PlayerSource(player)
player.History(HealFromKit(pSource, 10, GlobalDefinitions.medkit))
player.History(HealFromTerm(pSource, 10, 0, GlobalDefinitions.order_terminal))
player.History(HealFromImplant(pSource, 10, ImplantType.AdvancedRegen))
player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard))
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
player.History(VehicleShieldCharge(vSource, 10))
player.History(PlayerSuicide(pSource))
player.History.size mustEqual 7
val list = player.ClearHistory()
player.History.size mustEqual 0
list.head.isInstanceOf[PlayerSuicide] mustEqual true
list(1).isInstanceOf[VehicleShieldCharge] mustEqual true
list(2).isInstanceOf[RepairFromTerm] mustEqual true
list(3).isInstanceOf[HealFromExoSuitChange] mustEqual true
list(4).isInstanceOf[HealFromImplant] mustEqual true
list(5).isInstanceOf[HealFromTerm] mustEqual true
list(6).isInstanceOf[HealFromKit] mustEqual true
}
"get exactly one entry that was caused by projectile damage" in {
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
val pSource = PlayerSource(player)
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(player), player.DamageModel, Vector3(50, 50, 0))
player.History(DamageFromProjectile(resprojectile))
player.History(HealFromKit(pSource, 10, GlobalDefinitions.medkit))
player.History(HealFromTerm(pSource, 10, 0, GlobalDefinitions.order_terminal))
player.History(HealFromImplant(pSource, 10, ImplantType.AdvancedRegen))
player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard))
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
player.History(VehicleShieldCharge(vSource, 10))
player.History(PlayerSuicide(pSource))
player.LastShot match {
case Some(resolved_projectile) =>
resolved_projectile.projectile mustEqual projectile
case None =>
ko
}
}
}
}

View file

@ -15,34 +15,34 @@ import services.{RemoverActor, ServiceManager}
import scala.concurrent.duration._ import scala.concurrent.duration._
class StandardRemoverActorTest extends ActorTest { //class StandardRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") // ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
//
"RemoverActor" should { // "RemoverActor" should {
"handle a simple task" in { // "handle a simple task" in {
expectNoMsg(500 milliseconds) // expectNoMsg(500 milliseconds)
val probe = TestProbe() // val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") // val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere) // remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere)
//
val reply1 = probe.receiveOne(200 milliseconds) // val reply1 = probe.receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert]) // assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert])
val reply2 = probe.receiveOne(200 milliseconds) // val reply2 = probe.receiveOne(200 milliseconds)
assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert]) // assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert])
probe.expectNoMsg(1 seconds) //delay // probe.expectNoMsg(1 seconds) //delay
val reply3 = probe.receiveOne(300 milliseconds) // val reply3 = probe.receiveOne(300 milliseconds)
assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert]) // assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert])
val reply4 = probe.receiveOne(300 milliseconds) // val reply4 = probe.receiveOne(300 milliseconds)
assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) // assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
val reply5 = probe.receiveOne(300 milliseconds) // val reply5 = probe.receiveOne(300 milliseconds)
assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert]) // assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert])
val reply6 = probe.receiveOne(500 milliseconds) // val reply6 = probe.receiveOne(500 milliseconds)
assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) // assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
val reply7 = probe.receiveOne(500 milliseconds) // val reply7 = probe.receiveOne(500 milliseconds)
assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) // assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
} // }
} // }
} //}
//class DelayedRemoverActorTest extends ActorTest { //class DelayedRemoverActorTest extends ActorTest {
// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") // ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")

View file

@ -13,8 +13,9 @@ import csr.{CSRWarp, CSRZone, Traveler}
import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.GlobalDefinitions._
import services.ServiceManager.Lookup import services.ServiceManager.Lookup
import net.psforever.objects._ import net.psforever.objects._
import net.psforever.objects.ballistics._
import net.psforever.objects.definition.ToolDefinition import net.psforever.objects.definition.ToolDefinition
import net.psforever.objects.definition.converter.CorpseConverter import net.psforever.objects.definition.converter.{CorpseConverter, DestroyedVehicleConverter}
import net.psforever.objects.equipment._ import net.psforever.objects.equipment._
import net.psforever.objects.loadouts._ import net.psforever.objects.loadouts._
import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver}
@ -35,7 +36,9 @@ import net.psforever.objects.serverobject.structures.{Building, StructureType, W
import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.terminals._
import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage
import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.vehicles._ import net.psforever.objects.serverobject.turret.{MannedTurret, TurretUpgrade}
import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, Utility, VehicleLockState, _}
import net.psforever.objects.vital._
import net.psforever.objects.zones.{InterstellarCluster, Zone} import net.psforever.objects.zones.{InterstellarCluster, Zone}
import net.psforever.packet.game.objectcreate._ import net.psforever.packet.game.objectcreate._
import net.psforever.types._ import net.psforever.types._
@ -53,8 +56,6 @@ import scala.concurrent.Future
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.Success import scala.util.Success
import akka.pattern.ask import akka.pattern.ask
import net.psforever.objects.ballistics.{Projectile, ProjectileResolution}
import net.psforever.objects.serverobject.turret.{MannedTurret, TurretUpgrade}
class WorldSessionActor extends Actor with MDCContextAware { class WorldSessionActor extends Actor with MDCContextAware {
import WorldSessionActor._ import WorldSessionActor._
@ -109,6 +110,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
implicit def boolToInt(b : Boolean) : Int = if(b) 1 else 0 implicit def boolToInt(b : Boolean) : Int = if(b) 1 else 0
override def postStop() = { override def postStop() = {
//TODO normally, player avatar persists a minute or so after disconnect; we are subject to the SessionReaper
clientKeepAlive.cancel clientKeepAlive.cancel
reviveTimer.cancel reviveTimer.cancel
respawnTimer.cancel respawnTimer.cancel
@ -133,20 +135,23 @@ class WorldSessionActor extends Actor with MDCContextAware {
} }
}) })
//TODO final character save before doing any of this
continent.Population ! Zone.Population.Release(avatar)
if(player.isAlive) { if(player.isAlive) {
//actually being alive or manually deconstructing //actually being alive or manually deconstructing
DismountVehicleOnLogOut() player.Position = Vector3.Zero
continent.Population ! Zone.Population.Release(avatar) if(player.VehicleSeated.nonEmpty) {
player.Position = Vector3.Zero //save character before doing this //quickly and briefly kill player to avoid disembark animation
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectDelete(player_guid, player_guid)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 0, 0))
DismountVehicleOnLogOut()
}
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid))
taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID)
//TODO normally, the actual player avatar persists a minute or so after the user disconnects
} }
else if(continent.LivePlayers.contains(player) && !continent.Corpses.contains(player)) { else if(continent.LivePlayers.contains(player) && !continent.Corpses.contains(player)) {
//player disconnected while waiting for a revive //player disconnected while waiting for a revive
//similar to handling ReleaseAvatarRequestMessage //similar to handling ReleaseAvatarRequestMessage
player.Release player.Release
continent.Population ! Zone.Population.Release(avatar)
player.VehicleSeated match { player.VehicleSeated match {
case None => case None =>
FriskCorpse(player) //TODO eliminate dead letters FriskCorpse(player) //TODO eliminate dead letters
@ -189,7 +194,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Some(vehicle : Vehicle) => case Some(vehicle : Vehicle) =>
vehicle.Seat(vehicle.PassengerInSeat(player).get).get.Occupant = None vehicle.Seat(vehicle.PassengerInSeat(player).get).get.Occupant = None
if(vehicle.Seats.values.count(_.isOccupied) == 0) { if(vehicle.Seats.values.count(_.isOccupied) == 0) {
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent), vehicle.Definition.DeconstructionTime) //start vehicle decay vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent, vehicle.Definition.DeconstructionTime)) //start vehicle decay
} }
vehicleService ! Service.Leave(Some(s"${vehicle.Actor}")) vehicleService ! Service.Leave(Some(s"${vehicle.Actor}"))
@ -392,7 +397,30 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(GenericObjectActionMessage(guid, 36)) sendResponse(GenericObjectActionMessage(guid, 36))
} }
case msg @ AvatarResponse.DropItem(pkt) => case AvatarResponse.DamageResolution(target, resolution_function) =>
if(player.isAlive) {
resolution_function(target)
val health = player.Health
val armor = player.Armor
val playerGUID = player.GUID
sendResponse(PlanetsideAttributeMessage(playerGUID, 0, health))
sendResponse(PlanetsideAttributeMessage(playerGUID, 4, armor))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(playerGUID, 0, health))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(playerGUID, 4, armor))
if(health == 0 && player.isAlive) {
KillPlayer(player)
}
}
case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
// guid = victim // killer = killer ;)
sendResponse(DestroyMessage(victim, killer, weapon, pos))
case AvatarResponse.DestroyDisplay(killer, victim, method, unk) =>
sendResponse(DestroyDisplayMessage(killer, victim, method, unk))
case AvatarResponse.DropItem(pkt) =>
if(tplayer_guid != guid) { if(tplayer_guid != guid) {
sendResponse(pkt) sendResponse(pkt)
} }
@ -402,6 +430,25 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(pkt) sendResponse(pkt)
} }
case AvatarResponse.HitHint(source_guid) =>
sendResponse(HitHint(source_guid, guid))
case AvatarResponse.KilledWhileInVehicle() =>
if(player.isAlive && player.VehicleSeated.nonEmpty) {
continent.GUID(player.VehicleSeated.get) match {
case Some(vehicle : Vehicle) =>
if(vehicle.Health == 0) {
vehicle.LastShot match {
case Some(cause) =>
player.History(cause)
case None => ;
}
KillPlayer(player)
}
case _ => ;
}
}
case AvatarResponse.LoadPlayer(pkt) => case AvatarResponse.LoadPlayer(pkt) =>
if(tplayer_guid != guid) { if(tplayer_guid != guid) {
sendResponse(pkt) sendResponse(pkt)
@ -544,7 +591,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(0) } val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(0) }
reply match { reply match {
case VehicleResponse.Ownership(vehicle_guid) => case VehicleResponse.Ownership(vehicle_guid) =>
sendResponse(PlanetsideAttributeMessage(guid, 21, vehicle_guid.guid.toLong)) sendResponse(PlanetsideAttributeMessage(guid, 21, vehicle_guid.guid))
case VehicleResponse.AttachToRails(vehicle_guid, pad_guid) => case VehicleResponse.AttachToRails(vehicle_guid, pad_guid) =>
sendResponse(ObjectAttachMessage(pad_guid, vehicle_guid, 3)) sendResponse(ObjectAttachMessage(pad_guid, vehicle_guid, 3))
@ -577,6 +624,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(pkt) sendResponse(pkt)
} }
case VehicleResponse.HitHint(source_guid) =>
sendResponse(HitHint(source_guid, player.GUID))
case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) => case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) =>
if(tplayer_guid != guid) { if(tplayer_guid != guid) {
//TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
@ -621,6 +671,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(ObjectAttachMessage(vehicle_guid, guid, seat)) sendResponse(ObjectAttachMessage(vehicle_guid, guid, seat))
} }
case VehicleResponse.PlanetsideAttribute(vehicle_guid, attribute_type, attribute_value) =>
if(tplayer_guid != guid) {
sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type, attribute_value))
}
case VehicleResponse.ResetSpawnPad(pad_guid) => case VehicleResponse.ResetSpawnPad(pad_guid) =>
sendResponse(GenericObjectActionMessage(pad_guid, 92)) sendResponse(GenericObjectActionMessage(pad_guid, 92))
@ -787,6 +842,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Mountable.CanMount(obj : Vehicle, seat_num) => case Mountable.CanMount(obj : Vehicle, seat_num) =>
val obj_guid : PlanetSideGUID = obj.GUID val obj_guid : PlanetSideGUID = obj.GUID
val player_guid : PlanetSideGUID = tplayer.GUID
log.info(s"MountVehicleMsg: $player_guid mounts $obj_guid @ $seat_num")
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) //clear timer
PlayerActionsToCancel()
sendResponse(PlanetsideAttributeMessage(obj_guid, 0, obj.Health))
sendResponse(PlanetsideAttributeMessage(obj_guid, 68, 0)) //shield health
sendResponse(PlanetsideAttributeMessage(obj_guid, 113, 0)) //capacitor
if(seat_num == 0) { //simplistic vehicle ownership management if(seat_num == 0) { //simplistic vehicle ownership management
obj.Owner match { obj.Owner match {
case Some(owner_guid) => case Some(owner_guid) =>
@ -807,7 +869,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
//update mounted weapon belonging to seat //update mounted weapon belonging to seat
weapon.AmmoSlots.foreach(slot => { //update the magazine(s) in the weapon, specifically weapon.AmmoSlots.foreach(slot => { //update the magazine(s) in the weapon, specifically
val magazine = slot.Box val magazine = slot.Box
sendResponse(InventoryStateMessage(magazine.GUID, weapon.GUID, magazine.Capacity.toLong)) sendResponse(InventoryStateMessage(magazine.GUID, weapon.GUID, magazine.Capacity))
}) })
case _ => ; //no weapons to update case _ => ; //no weapons to update
} }
@ -859,6 +921,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Terminal.TerminalMessage(tplayer, msg, order) => case Terminal.TerminalMessage(tplayer, msg, order) =>
order match { order match {
case Terminal.BuyExosuit(exosuit, subtype) => //refresh armor points case Terminal.BuyExosuit(exosuit, subtype) => //refresh armor points
tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), exosuit))
if(tplayer.ExoSuit == exosuit) { if(tplayer.ExoSuit == exosuit) {
if(exosuit == ExoSuitType.MAX) { if(exosuit == ExoSuitType.MAX) {
//special MAX case - clear any special state //special MAX case - clear any special state
@ -1005,6 +1068,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
//TODO optimizations against replacing Equipment with the exact same Equipment and potentially for recycling existing Equipment //TODO optimizations against replacing Equipment with the exact same Equipment and potentially for recycling existing Equipment
log.info(s"$tplayer wants to change equipment loadout to their option #${msg.unk1 + 1}") log.info(s"$tplayer wants to change equipment loadout to their option #${msg.unk1 + 1}")
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, true)) sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, true))
tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), exosuit))
//ensure arm is down //ensure arm is down
tplayer.DrawnSlot = Player.HandsDownSlot tplayer.DrawnSlot = Player.HandsDownSlot
sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true)) sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true))
@ -1105,7 +1169,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
} }
else { else {
//accommodate as much of inventory as possible //accommodate as much of inventory as possible
//TODO map x,y -> x,y rather than reorganize items
val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) //dropped items can be forgotten val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) //dropped items can be forgotten
stow stow
} }
@ -1121,7 +1184,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
if(!tplayer.Certifications.contains(cert)) { if(!tplayer.Certifications.contains(cert)) {
log.info(s"$tplayer is learning the $cert certification for $cost points") log.info(s"$tplayer is learning the $cert certification for $cost points")
avatar.Certifications += cert avatar.Certifications += cert
sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 24, cert.id.toLong)) sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 24, cert.id))
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, true)) sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, true))
} }
else { else {
@ -1133,7 +1196,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
if(tplayer.Certifications.contains(cert)) { if(tplayer.Certifications.contains(cert)) {
log.info(s"$tplayer is forgetting the $cert certification for $cost points") log.info(s"$tplayer is forgetting the $cert certification for $cost points")
avatar.Certifications -= cert avatar.Certifications -= cert
sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 25, cert.id.toLong)) sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 25, cert.id))
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true)) sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true))
} }
else { else {
@ -1289,9 +1352,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
case VehicleSpawnPad.PlayerSeatedInVehicle(vehicle, pad) => case VehicleSpawnPad.PlayerSeatedInVehicle(vehicle, pad) =>
val vehicle_guid = vehicle.GUID val vehicle_guid = vehicle.GUID
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on
//sendResponse(PlanetsideAttributeMessage(vehicle_guid, 0, 10))//vehicle.Definition.MaxHealth))
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 68, 0L)) // Shield health
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 113, 0L)) // Capacitor (EMP)
ReloadVehicleAccessPermissions(vehicle) ReloadVehicleAccessPermissions(vehicle)
ServerVehicleLock(vehicle) ServerVehicleLock(vehicle)
@ -1551,6 +1611,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list
sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil)) sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil))
sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil)) sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil))
avatarService ! Service.Join(avatar.name) //will be same as player.Name
cluster ! InterstellarCluster.GetWorld("z6") cluster ! InterstellarCluster.GetWorld("z6")
case InterstellarCluster.GiveWorld(zoneId, zone) => case InterstellarCluster.GiveWorld(zoneId, zone) =>
@ -1750,6 +1811,50 @@ class WorldSessionActor extends Actor with MDCContextAware {
case DelayedProximityUnitStop(terminal) => case DelayedProximityUnitStop(terminal) =>
StopUsingProximityUnit(terminal) StopUsingProximityUnit(terminal)
case Vitality.DamageResolution(target : Vehicle) =>
val targetGUID = target.GUID
val playerGUID = player.GUID
val continentId = continent.Id
val players = target.Seats.values.filter(seat => { seat.isOccupied && seat.Occupant.get.isAlive })
if(target.Health > 0) {
//alert occupants to damage source
players.foreach(seat => {
val tplayer = seat.Occupant.get
avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.HitHint(playerGUID, tplayer.GUID))
})
}
else {
//alert to vehicle death (hence, occupants' deaths)
players.foreach(seat => {
val tplayer = seat.Occupant.get
val tplayerGUID = tplayer.GUID
avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.KilledWhileInVehicle(tplayerGUID))
avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(tplayerGUID, tplayerGUID)) //dead player still sees self
})
//vehicle wreckage has no weapons
target.Weapons.values
.filter { _.Equipment.nonEmpty }
.foreach(slot => {
val wep = slot.Equipment.get
avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, wep.GUID))
})
if(target.Definition == GlobalDefinitions.ams) {
target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying)
ClearCurrentAmsSpawnPoint()
}
avatarService ! AvatarServiceMessage(continentId, AvatarAction.Destroy(targetGUID, playerGUID, playerGUID, target.Position))
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), continent))
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, continent, Some(1 minute)))
}
vehicleService ! VehicleServiceMessage(continentId, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, target.Health))
vehicleService ! VehicleServiceMessage(s"${target.Actor}", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 68, target.Shields))
case Vitality.DamageResolution(target : PlanetSideGameObject) =>
log.warn(s"Vital target ${target.Definition.Name} damage resolution not supported using this method")
case Vehicle.UpdateShieldsCharge(vehicle) =>
vehicleService ! VehicleServiceMessage(s"${vehicle.Actor}", VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), vehicle.GUID, 68, vehicle.Shields))
case ResponseToSelf(pkt) => case ResponseToSelf(pkt) =>
log.info(s"Received a direct message: $pkt") log.info(s"Received a direct message: $pkt")
sendResponse(pkt) sendResponse(pkt)
@ -1780,7 +1885,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
//TODO begin temp player character auto-loading; remove later //TODO begin temp player character auto-loading; remove later
import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.GlobalDefinitions._
import net.psforever.types.CertificationType._ import net.psforever.types.CertificationType._
avatar = Avatar("TestCharacter" + sessionId.toString, PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1) val avatar = Avatar("TestCharacter" + sessionId.toString, PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1)
avatar.Certifications += StandardAssault avatar.Certifications += StandardAssault
avatar.Certifications += MediumAssault avatar.Certifications += MediumAssault
avatar.Certifications += StandardExoSuit avatar.Certifications += StandardExoSuit
@ -1814,6 +1919,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
avatar.Certifications += AdvancedEngineering avatar.Certifications += AdvancedEngineering
avatar.Certifications += FortificationEngineering avatar.Certifications += FortificationEngineering
avatar.Certifications += AssaultEngineering avatar.Certifications += AssaultEngineering
this.avatar = avatar
AwardBattleExperiencePoints(avatar, 1000000L) AwardBattleExperiencePoints(avatar, 1000000L)
player = new Player(avatar) player = new Player(avatar)
//player.Position = Vector3(3561.0f, 2854.0f, 90.859375f) //home3, HART C //player.Position = Vector3(3561.0f, 2854.0f, 90.859375f) //home3, HART C
@ -1992,8 +2098,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
continent.Corpses.foreach { continent.Corpses.foreach {
TurnPlayerIntoCorpse TurnPlayerIntoCorpse
} }
//load active vehicles in zone //load vehicles in zone
continent.Vehicles.foreach(vehicle => { val (wreckages, vehicles) = continent.Vehicles.partition(vehicle => { vehicle.Health == 0 && vehicle.Definition.DestroyedModel.nonEmpty })
//active vehicles
vehicles.foreach(vehicle => {
val vehicle_guid = vehicle.GUID val vehicle_guid = vehicle.GUID
val vdefinition = vehicle.Definition val vdefinition = vehicle.Definition
sendResponse(ObjectCreateMessage(vdefinition.ObjectId, vehicle_guid, vdefinition.Packet.ConstructorData(vehicle).get)) sendResponse(ObjectCreateMessage(vdefinition.ObjectId, vehicle_guid, vdefinition.Packet.ConstructorData(vehicle).get))
@ -2014,9 +2122,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
}) })
ReloadVehicleAccessPermissions(vehicle) ReloadVehicleAccessPermissions(vehicle)
}) })
//Loop over vehicles again to add cargohold occupants after all vehicles have been created on the local client
// Loop over vehicles again to add cargohold occupants after all vehicles have been created on the local client vehicles.foreach(vehicle => {
continent.Vehicles.foreach(vehicle => {
vehicle.CargoHolds.foreach({ case (cargo_num, cargo) => { vehicle.CargoHolds.foreach({ case (cargo_num, cargo) => {
cargo.Occupant match { cargo.Occupant match {
case Some(cargo_vehicle) => case Some(cargo_vehicle) =>
@ -2031,6 +2138,16 @@ class WorldSessionActor extends Actor with MDCContextAware {
} }
}}) }})
}) })
//vehicle wreckages
wreckages.foreach(vehicle => {
sendResponse(
ObjectCreateMessage(
vehicle.Definition.DestroyedModel.get.id,
vehicle.GUID,
DestroyedVehicleConverter.converter.ConstructorData(vehicle).get
)
)
})
//implant terminals //implant terminals
continent.Map.TerminalToInterface.foreach({ case ((terminal_guid, interface_guid)) => continent.Map.TerminalToInterface.foreach({ case ((terminal_guid, interface_guid)) =>
@ -2198,8 +2315,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
} }
case Some(_) => case Some(_) =>
//TODO we do not want to delete the player if he is seated in a vehicle when releasing
//TODO it is necessary for now until we know how to juggle ownership properly
val player_guid = player.GUID val player_guid = player.GUID
sendResponse(ObjectDeleteMessage(player_guid, 0)) sendResponse(ObjectDeleteMessage(player_guid, 0))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0))
@ -2207,8 +2322,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(player)) context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(player))
//sendResponse(ObjectDetachMessage(vehicle_guid, player.GUID, Vector3.Zero, 0))
//sendResponse(PlayerStateShiftMessage(ShiftState(1, Vector3.Zero, 0)))
} }
case msg @ SpawnRequestMessage(u1, u2, u3, u4, u5) => case msg @ SpawnRequestMessage(u1, u2, u3, u4, u5) =>
@ -2289,7 +2402,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
} }
if(messagetype == ChatMessageType.CMT_SUICIDE) { if(messagetype == ChatMessageType.CMT_SUICIDE) {
if(player.isAlive && deadState != DeadState.Release) { if(player.isAlive && deadState != DeadState.Release) {
KillPlayer(player) Suicide(player)
} }
} }
if(messagetype == ChatMessageType.CMT_DESTROY) { if(messagetype == ChatMessageType.CMT_DESTROY) {
@ -2726,10 +2839,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Some(_ : LocalProjectile) => case Some(_ : LocalProjectile) =>
FindProjectileEntry(object_guid) match { FindProjectileEntry(object_guid) match {
case Some(projectile) => case Some(projectile) =>
if(projectile.resolution != ProjectileResolution.Unresolved) { if(projectile.isResolved) {
log.warn(s"RequestDestroy: tried to clean up missed projectile ${object_guid.guid} but it was already resolved") log.warn(s"RequestDestroy: tried to clean up projectile ${object_guid.guid} but it was already resolved")
}
else {
projectile.Miss()
} }
ResolveProjectileEntry(object_guid, ProjectileResolution.MissedShot)
case None => case None =>
log.warn(s"RequestDestroy: projectile ${object_guid.guid} has never been fired") log.warn(s"RequestDestroy: projectile ${object_guid.guid} has never been fired")
} }
@ -2901,7 +3016,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType))
sendResponse(ObjectDeleteMessage(kit.GUID, 0)) sendResponse(ObjectDeleteMessage(kit.GUID, 0))
taskResolver ! GUIDTask.UnregisterEquipment(kit)(continent.GUID) taskResolver ! GUIDTask.UnregisterEquipment(kit)(continent.GUID)
//TODO better health/damage control workflow player.History(HealFromKit(PlayerSource(player), 25, kit.Definition))
player.Health = player.Health + 25 player.Health = player.Health + 25
sendResponse(PlanetsideAttributeMessage(avatar_guid, 0, player.Health)) sendResponse(PlanetsideAttributeMessage(avatar_guid, 0, player.Health))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 0, player.Health)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 0, player.Health))
@ -3224,16 +3339,23 @@ class WorldSessionActor extends Actor with MDCContextAware {
else { //shooting else { //shooting
tool.Discharge tool.Discharge
val projectileIndex = projectile_guid.guid - Projectile.BaseUID val projectileIndex = projectile_guid.guid - Projectile.BaseUID
val ang = obj match { val projectilePlace = projectiles(projectileIndex)
case _ : Player => if(projectilePlace match {
obj.Orientation //TODO upper body facing case Some(projectile) => !projectile.isResolved
case None => false
}) {
log.warn(s"WeaponFireMessage: former projectile ${projectile_guid.guid} was not resolved properly; overwriting anyway")
}
val (angle, attribution) = obj match {
case p : Player =>
(p.Orientation, tool.Definition.ObjectId) //TODO upper body facing
case _ : Vehicle => case _ : Vehicle =>
tool.Orientation //TODO this is too simplistic (tool.Orientation, obj.Definition.ObjectId) //TODO this is too simplistic to find proper angle
case _ => case _ =>
Vector3.Zero (obj.Orientation, obj.Definition.ObjectId)
} }
projectiles(projectileIndex) = projectiles(projectileIndex) =
Some(Projectile(tool.Projectile, tool.Definition, shot_origin, ang)) Some(Projectile(tool.Projectile, tool.Definition, tool.FireMode, player, attribution, shot_origin, angle))
} }
case _ => ; case _ => ;
} }
@ -3243,15 +3365,62 @@ class WorldSessionActor extends Actor with MDCContextAware {
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") log.info(s"Hit: $msg")
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Hit) (hit_info match {
case Some(hitInfo) =>
continent.GUID(hitInfo.hitobject_guid.get) match {
case Some(obj : Player) =>
Some((obj, hitInfo.shot_origin, hitInfo.hit_pos))
case Some(obj : Vehicle) =>
Some((obj, hitInfo.shot_origin, hitInfo.hit_pos))
case _ =>
None
}
case None => ;
None
}) match {
case Some((target, shotOrigin, hitPos)) =>
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Hit, target, hitPos) match {
case Some(projectile) =>
HandleDealingDamage(target, projectile)
case None => ;
}
case None => ;
}
case msg @ SplashHitMessage(seq_time, projectile_guid, explosion_pos, direct_victim_uid, unk3, projectile_vel, unk4, targets) => case msg @ SplashHitMessage(seq_time, projectile_guid, explosion_pos, direct_victim_uid, unk3, projectile_vel, unk4, targets) =>
log.info(s"Splash: $msg") log.info(s"Splash: $msg")
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Splash) continent.GUID(direct_victim_uid) match {
case Some(target : PlanetSideGameObject with FactionAffinity with Vitality) =>
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Hit, target, explosion_pos) match {
case Some(projectile) =>
HandleDealingDamage(target, projectile)
case None => ;
}
case _ => ;
}
targets.foreach(elem => {
continent.GUID(elem.uid) match {
case Some(target : PlanetSideGameObject with FactionAffinity with Vitality) =>
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Splash, target, target.Position) match {
case Some(projectile) =>
HandleDealingDamage(target, projectile)
case None => ;
}
case _ => ;
}
})
case msg @ LashMessage(seq_time, killer_guid, victim_guid, projectile_guid, pos, unk1) => case msg @ LashMessage(seq_time, killer_guid, victim_guid, projectile_guid, pos, unk1) =>
log.info(s"Lash: $msg") log.info(s"Lash: $msg")
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Lash) continent.GUID(victim_guid) match {
case Some(target : PlanetSideGameObject with FactionAffinity with Vitality) =>
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Lash, target, pos) match {
case Some(projectile) =>
HandleDealingDamage(target, projectile)
case None => ;
}
case _ => ;
}
case msg @ AvatarFirstTimeEventMessage(avatar_guid, object_guid, unk1, event_name) => case msg @ AvatarFirstTimeEventMessage(avatar_guid, object_guid, unk1, event_name) =>
log.info("AvatarFirstTimeEvent: " + msg) log.info("AvatarFirstTimeEvent: " + msg)
@ -3422,9 +3591,21 @@ class WorldSessionActor extends Actor with MDCContextAware {
} }
case msg @ FacilityBenefitShieldChargeRequestMessage(guid) => case msg @ FacilityBenefitShieldChargeRequestMessage(guid) =>
//log.info(s"ShieldChargeRequest: $msg") player.VehicleSeated match {
case Some(vehicleGUID) =>
continent.GUID(vehicleGUID) match {
case Some(obj : Vehicle) =>
if(obj.Health > 0) { //vehicle will try to charge even if destroyed
obj.Actor ! Vehicle.ChargeShields(15)
}
case _ =>
log.warn(s"FacilityBenefitShieldChargeRequest: can not find vehicle ${vehicleGUID.guid} in zone ${continent.Id}")
}
case None =>
log.warn(s"FacilityBenefitShieldChargeRequest: player ${player.Name} is not seated in a vehicle")
}
case msg @ BattleplanMessage(char_id, player_name, zonr_id, diagrams) => case msg @ BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
log.info("Battleplan: "+msg) log.info("Battleplan: "+msg)
case msg @ CreateShortcutMessage(player_guid, slot, unk, add, shortcut) => case msg @ CreateShortcutMessage(player_guid, slot, unk, add, shortcut) =>
@ -3433,8 +3614,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ FriendsRequest(action, friend) => case msg @ FriendsRequest(action, friend) =>
log.info("FriendsRequest: "+msg) log.info("FriendsRequest: "+msg)
case msg @ HitHint(source, player_guid) => case msg @ HitHint(source_guid, player_guid) =>
log.info("HitHint: "+msg) log.info(s"HitHint: $msg")
continent.GUID(player_guid) match {
case Some(obj : Player) =>
avatarService ! AvatarServiceMessage(obj.Name, AvatarAction.HitHint(source_guid, player_guid))
case _ => ;
}
case msg @ TargetingImplantRequest(list) => case msg @ TargetingImplantRequest(list) =>
log.info("TargetingImplantRequest: "+msg) log.info("TargetingImplantRequest: "+msg)
@ -3977,7 +4164,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
val vehicle_guid = vehicle.GUID val vehicle_guid = vehicle.GUID
(0 to 3).foreach(group => { (0 to 3).foreach(group => {
sendResponse( sendResponse(
PlanetsideAttributeMessage(vehicle_guid, group + 10, vehicle.PermissionGroup(group).get.id.toLong) PlanetsideAttributeMessage(vehicle_guid, group + 10, vehicle.PermissionGroup(group).get.id)
) )
}) })
} }
@ -4869,6 +5056,17 @@ class WorldSessionActor extends Actor with MDCContextAware {
}) })
} }
/**
* The player has lost the will to live and must be killed.
* @see `Vitality`<br>
* `PlayerSuicide`
* @param tplayer the player to be killed
*/
def Suicide(tplayer : Player) : Unit = {
tplayer.History(PlayerSuicide(PlayerSource(tplayer)))
KillPlayer(tplayer)
}
/** /**
* The player has lost all his vitality and must be killed.<br> * The player has lost all his vitality and must be killed.<br>
* <br> * <br>
@ -4898,6 +5096,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
continent.GUID(tplayer.VehicleSeated.get) match { continent.GUID(tplayer.VehicleSeated.get) match {
case Some(obj : Vehicle) => case Some(obj : Vehicle) =>
TotalDriverVehicleControl(obj) TotalDriverVehicleControl(obj)
UnAccessContents(obj)
case _ => ; case _ => ;
} }
//make player invisible (if not, the cadaver sticks out the side in a seated position) //make player invisible (if not, the cadaver sticks out the side in a seated position)
@ -4906,6 +5105,29 @@ class WorldSessionActor extends Actor with MDCContextAware {
} }
PlayerActionsToCancel() PlayerActionsToCancel()
CancelAllProximityUnits() CancelAllProximityUnits()
//TODO other methods of death?
val pentry = PlayerSource(tplayer)
(tplayer.History.find({p => p.isInstanceOf[PlayerSuicide]}) match {
case Some(PlayerSuicide(_)) =>
None
case _ =>
tplayer.LastShot match {
case Some(shot) =>
if(System.nanoTime - shot.hit_time < (10 seconds).toNanos) {
Some(shot)
}
else {
None //suicide
}
case None =>
None //suicide
}
}) match {
case Some(shot) =>
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DestroyDisplay(shot.projectile.owner, pentry, shot.projectile.attribute_to))
case None =>
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DestroyDisplay(pentry, pentry, 0))
}
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, cluster, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(tplayer.Faction), tplayer, 7)) reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, cluster, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(tplayer.Faction), tplayer, 7))
@ -5192,12 +5414,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
*/ */
def ProximityMedicalTerminal(unit : Terminal with ProximityUnit) : Unit = { def ProximityMedicalTerminal(unit : Terminal with ProximityUnit) : Unit = {
val healthFull : Boolean = if(player.Health < player.MaxHealth) { val healthFull : Boolean = if(player.Health < player.MaxHealth) {
player.History(HealFromTerm(PlayerSource(player), 10, 0, unit.Definition))
HealAction(player) HealAction(player)
} }
else { else {
true true
} }
val armorFull : Boolean = if(player.Armor < player.MaxArmor) { val armorFull : Boolean = if(player.Armor < player.MaxArmor) {
player.History(HealFromTerm(PlayerSource(player), 0, 10, unit.Definition))
ArmorRepairAction(player) ArmorRepairAction(player)
} }
else { else {
@ -5216,6 +5440,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
*/ */
def ProximityHealCrystal(unit : Terminal with ProximityUnit) : Unit = { def ProximityHealCrystal(unit : Terminal with ProximityUnit) : Unit = {
val healthFull : Boolean = if(player.Health < player.MaxHealth) { val healthFull : Boolean = if(player.Health < player.MaxHealth) {
player.History(HealFromTerm(PlayerSource(player), 10, 0, unit.Definition))
HealAction(player) HealAction(player)
} }
else { else {
@ -5364,11 +5589,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
* @param resolution the resolution status to promote the projectile * @param resolution the resolution status to promote the projectile
* @return the projectile * @return the projectile
*/ */
def ResolveProjectileEntry(projectile_guid : PlanetSideGUID, resolution : ProjectileResolution.Value) : Option[Projectile] = { def ResolveProjectileEntry(projectile_guid : PlanetSideGUID, resolution : ProjectileResolution.Value, target : PlanetSideGameObject with FactionAffinity with Vitality, pos : Vector3) : Option[ResolvedProjectile] = {
FindProjectileEntry(projectile_guid) match { FindProjectileEntry(projectile_guid) match {
case Some(projectile) => case Some(projectile) =>
val index = projectile_guid.guid - Projectile.BaseUID val index = projectile_guid.guid - Projectile.BaseUID
ResolveProjectileEntry(projectile, index, resolution) ResolveProjectileEntry(projectile, index, resolution, target, pos)
case None => case None =>
log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found") log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found")
None None
@ -5385,13 +5610,18 @@ class WorldSessionActor extends Actor with MDCContextAware {
* @param resolution the resolution status to promote the projectile * @param resolution the resolution status to promote the projectile
* @return a copy of the projectile * @return a copy of the projectile
*/ */
def ResolveProjectileEntry(projectile : Projectile, index : Int, resolution : ProjectileResolution.Value) : Option[Projectile] = { def ResolveProjectileEntry(projectile : Projectile, index : Int, resolution : ProjectileResolution.Value, target : PlanetSideGameObject with FactionAffinity with Vitality, pos : Vector3) : Option[ResolvedProjectile] = {
if(projectiles(index).contains(projectile)) { if(!projectiles(index).contains(projectile)) {
projectiles(index) = Some(projectile.Resolve(ProjectileResolution.Resolved)) log.error(s"expected projectile could not be found at $index; can not resolve")
Some(projectile.Resolve(resolution)) None
}
else if(projectile.isMiss) {
log.error(s"expected projectile at $index was already counted as a missed shot; can not resolve any further")
None
} }
else { else {
None projectile.Resolve()
Some(ResolvedProjectile(resolution, projectile, SourceEntry(target), target.DamageModel, pos))
} }
} }
@ -5423,6 +5653,65 @@ class WorldSessionActor extends Actor with MDCContextAware {
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DismountVehicle(player_guid, BailType.Normal, false)) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DismountVehicle(player_guid, BailType.Normal, false))
} }
/**
* Calculate the amount of damage to be dealt to an active `target`
* using the information reconstructed from a `Resolvedprojectile`
* and affect the `target` in a synchronized manner.
* The active `target` and the target of the `ResolvedProjectile` do not have be the same.
* @see `DamageResistanceModel`<br>
* `Vitality`
* @param target a valid game object that is known to the server
* @param data a projectile that will affect the target
*/
def HandleDealingDamage(target : PlanetSideGameObject with Vitality, data : ResolvedProjectile) : Unit = {
val func = data.damage_model.Calculate(data)
target match {
case obj : Player =>
//damage is synchronized on the target player's `WSA` (results distributed from there)
avatarService ! AvatarServiceMessage(obj.Name, AvatarAction.Damage(player.GUID, obj, func))
case obj : Vehicle =>
//damage is synchronized on the vehicle actor (results returned to and distributed from this `WSA`)
obj.Actor ! Vitality.Damage(func)
case _ => ;
}
}
/**
* Properly format a `DestroyDisplayMessage` packet
* given sufficient information about a target (victim) and an actor (killer).
* For the packet, the `*_charId` field is most important to determining distinction between players.
* The "char id" is not a currently supported field for different players so a name hash is used instead.
* The virtually negligent chance of a name hash collision is covered.
* @param killer the killer's entry
* @param victim the victim's entry
* @param method the manner of death
* @param unk na;
* defaults to 121, the object id of `avatar`
* @return a `DestroyDisplayMessage` packet that is properly formatted
*/
def DestroyDisplayMessage(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int = 121) : DestroyDisplayMessage = {
//TODO charId should reflect the player more properly
val killerCharId = math.abs(killer.Name.hashCode)
var victimCharId = math.abs(victim.Name.hashCode)
if(killerCharId == victimCharId && killer.Name != victim.Name) {
//odds of hash collision in a populated zone should be close to odds of being struck by lightning
victimCharId = Int.MaxValue - victimCharId + 1
}
val killer_seated = killer match {
case obj : PlayerSource => obj.Seated
case _ => false
}
val victim_seated = victim match {
case obj : PlayerSource => obj.Seated
case _ => false
}
new DestroyDisplayMessage(
killer.Name, killerCharId, killer.Faction, killer_seated,
unk, method,
victim.Name, victimCharId, victim.Faction, victim_seated
)
}
def failWithError(error : String) = { def failWithError(error : String) = {
log.error(error) log.error(error)
sendResponse(ConnectionClose()) sendResponse(ConnectionClose())