* 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
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/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/CItem.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/SItem.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/CommonMessages.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/Turrets.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/game/objectcreate/DrawnSlot.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/CryptoPacketOpcode.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/CertificationType.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/services/avatar/AvatarAction.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/LocalResponse.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
@ -210,8 +210,6 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
}
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 = {
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.inventory.InventoryTile
import net.psforever.objects.vital._
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
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.
* @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 maxArmor : Int = 0
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 inventoryOffset : Int = 0
Damage = StandardInfantryDamage
Resistance = StandardInfantryResistance
Model = StandardResolutions.Infantry
def SuitType : ExoSuitType.Value = suitType
@ -79,6 +85,12 @@ class SpecialExoSuitDefinition(private val suitType : ExoSuitType.Value) extends
obj.MaxArmor = MaxArmor
obj.InventoryScale = InventoryScale
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)) })
obj
}
@ -109,6 +121,9 @@ object ExoSuitDefinition {
Standard.Holster(0, EquipmentSize.Pistol)
Standard.Holster(2, EquipmentSize.Rifle)
Standard.Holster(4, EquipmentSize.Melee)
Standard.ResistanceDirectHit = 4
Standard.ResistanceSplash = 15
Standard.ResistanceAggravated = 8
final val Agile = ExoSuitDefinition(ExoSuitType.Agile)
Agile.MaxArmor = 100
@ -118,6 +133,9 @@ object ExoSuitDefinition {
Agile.Holster(1, EquipmentSize.Pistol)
Agile.Holster(2, EquipmentSize.Rifle)
Agile.Holster(4, EquipmentSize.Melee)
Agile.ResistanceDirectHit = 6
Agile.ResistanceSplash = 25
Agile.ResistanceAggravated = 10
final val Reinforced = ExoSuitDefinition(ExoSuitType.Reinforced)
Reinforced.permission = 1
@ -129,6 +147,9 @@ object ExoSuitDefinition {
Reinforced.Holster(2, EquipmentSize.Rifle)
Reinforced.Holster(3, EquipmentSize.Rifle)
Reinforced.Holster(4, EquipmentSize.Melee)
Reinforced.ResistanceDirectHit = 10
Reinforced.ResistanceSplash = 35
Reinforced.ResistanceAggravated = 12
final val Infiltration = ExoSuitDefinition(ExoSuitType.Infiltration)
Infiltration.permission = 1
@ -145,6 +166,11 @@ object ExoSuitDefinition {
MAX.InventoryOffset = 6
MAX.Holster(0, EquipmentSize.Max)
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 = {
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.loadouts.Loadout
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.types._
import scala.annotation.tailrec
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 backpack : Boolean = false
private var health : Int = 0
@ -42,7 +48,6 @@ class Player(private val core : Avatar) extends PlanetSideGameObject with Factio
//SouNourS things
/** Last medkituse. */
var lastMedkit : Long = 0
var death_by : Int = 0
var lastSeenStreamMessage : Array[Long] = Array.fill[Long](65535)(0L)
var lastShotSeq_time : Int = -1
/** From PlanetsideAttributeMessage */
@ -276,6 +281,14 @@ class Player(private val core : Avatar) extends PlanetSideGameObject with Factio
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 BEP : Long = core.BEP
@ -475,6 +488,8 @@ class Player(private val core : Avatar) extends PlanetSideGameObject with Factio
Continent
}
def DamageModel = exosuit.asInstanceOf[DamageResistanceModel]
def Definition : AvatarDefinition = core.Definition
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.deploy.Deployment
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.{DamageResistanceModel, StandardResistanceProfile, Vitality}
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.types.PlanetSideEmpire
@ -67,6 +68,8 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ
with Mountable
with MountedWeapons
with Deployment
with Vitality
with StandardResistanceProfile
with Container {
private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.TR
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] = {
owner match {
case Some(_) =>
if(Definition.CanBeOwned) { //e.g., base turrets
if(Definition.CanBeOwned) {
this.owner = owner
}
case None =>
@ -152,11 +155,11 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ
}
def Health : Int = {
this.health
health
}
def Health_=(health : Int) : Int = {
this.health = health
def Health_=(assignHealth : Int) : Int = {
health = math.min(math.max(0, assignHealth), MaxHealth)
health
}
@ -165,11 +168,11 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ
}
def Shields : Int = {
this.shields
shields
}
def Shields_=(strength : Int) : Int = {
this.shields = strength
shields = math.min(math.max(0, strength), MaxShields)
Shields
}
@ -178,12 +181,12 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ
}
def Decal : Int = {
this.decal
decal
}
def Decal_=(decal : Int) : Int = {
this.decal = decal
decal
def Decal_=(logo : Int) : Int = {
decal = logo
Decal
}
def Jammered : Boolean = jammered
@ -493,6 +496,8 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ
*/
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`.
* @return the vehicle's definition entry
@ -534,6 +539,20 @@ object Vehicle {
*/
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.
* @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
package net.psforever.objects.ballistics
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{ProjectileDefinition, ToolDefinition}
import net.psforever.objects.entity.SimpleWorldEntity
import net.psforever.objects.equipment.FireModeDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.types.Vector3
/**
* 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 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_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;
* defaults to `System.nanoTime`
* @param hit_time when the discharge had its resolution status updated
*/
final case class Projectile(profile : ProjectileDefinition,
tool_def : ToolDefinition,
fire_mode : FireModeDefinition,
owner : SourceEntry,
attribute_to : Int,
shot_origin : Vector3,
shot_angle : Vector3,
resolution : ProjectileResolution.Value,
fire_time : Long = System.nanoTime,
hit_time : Long = 0) {
fire_time: Long = System.nanoTime) {
/** Information about the current world coordinates and orientation of the projectile */
val current : SimpleWorldEntity = new SimpleWorldEntity()
private var resolved : ProjectileResolution.Value = ProjectileResolution.Unresolved
/**
* Give the projectile the suggested resolution status.
* 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
* Mark the projectile as being "encountered" or "managed" at least once.
*/
def Resolve(pos : Vector3, ang : Vector3, resolution : ProjectileResolution.Value) : Projectile = {
val obj = Resolve(resolution)
obj.current.Position = pos
obj.current.Orientation = ang
obj
def Resolve() : Unit = {
resolved = ProjectileResolution.Resolved
}
/**
* Give the projectile the suggested resolution status.
* @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 Miss() : Unit = {
resolved = ProjectileResolution.MissedShot
}
def isResolved : Boolean = resolved == ProjectileResolution.Resolved || resolved == ProjectileResolution.MissedShot
def isMiss : Boolean = resolved == ProjectileResolution.MissedShot
}
object Projectile {
@ -68,11 +67,28 @@ object Projectile {
* 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 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, shot_origin : Vector3, shot_angle : Vector3) : Projectile = {
Projectile(profile, tool_def, shot_origin, shot_angle, ProjectileResolution.Unresolved)
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, 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
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.
* `Tool` objects emit `ProjectileDefinition` objects and that is later wrapped into a `Projectile` object.
* @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 var damage0 : Int = 0
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 damageRadius : Float = 1f
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"
def ProjectileType : Projectiles.Value = projectileType
@ -157,10 +165,43 @@ class ProjectileDefinition(objectId : Int) extends ObjectDefinition(objectId) wi
this.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 {
def apply(projectileType : Projectiles.Value) : ProjectileDefinition = {
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.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.concurrent.duration._
/**
* 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
/** vehicle shields offered through amp station facility benefits (generally: 20% of health + 1) */
private var maxShields : Int = 0
/* key - seat index, value - seat object */
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 deconTime : Option[FiniteDuration] = None
private var maximumCapacitor : Int = 0
private var destroyedModel : Option[DestroyedVehicle.Value] = None
Name = "vehicle"
Packet = VehicleDefinition.converter
Damage = StandardVehicleDamage
Resistance = StandardVehicleResistance
Model = StandardResolutions.Vehicle
def MaxHealth : Int = maxHealth
@ -138,6 +147,13 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) {
maximumCapacitor = maxCapacitor
MaximumCapacitor
}
def DestroyedModel : Option[DestroyedVehicle.Value] = destroyedModel
def DestroyedModel_=(model : Option[DestroyedVehicle.Value]) : Option[DestroyedVehicle.Value] = {
destroyedModel = model
DestroyedModel
}
}
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.Vehicle
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}
@ -14,31 +15,57 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
override def ConstructorData(obj : Vehicle) : Try[VehicleData] = {
val health = 255 * obj.Health / obj.MaxHealth //TODO not precise
Success(
VehicleData(
PlacementData(obj.Position, obj.Orientation, obj.Velocity),
obj.Faction,
bops = false,
destroyed = health < 3,
unk1 = 0,
obj.Jammered,
unk2 = false,
obj.Owner match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
},
unk3 = false,
health,
unk4 = false,
no_mount_points = false,
obj.DeploymentState,
unk5 = false,
unk6 = false,
obj.Cloaked,
SpecificFormatData(obj),
Some(InventoryData(MakeDriverSeat(obj) ++ MakeUtilities(obj) ++ MakeMountings(obj)))
)(SpecificFormatModifier)
)
if(health > 3) { //active
Success(
VehicleData(
PlacementData(obj.Position, obj.Orientation, obj.Velocity),
obj.Faction,
bops = false,
destroyed = false,
unk1 = 0,
obj.Jammered,
unk2 = false,
obj.Owner match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
},
unk3 = false,
health,
unk4 = false,
no_mount_points = false,
obj.DeploymentState,
unk5 = false,
unk6 = false,
obj.Cloaked,
SpecificFormatData(obj),
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] = {

View file

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

View file

@ -96,18 +96,16 @@ class VehicleSpawnControl(pad : VehicleSpawnPad) extends VehicleSpawnControlBase
}
case VehicleSpawnControl.ProcessControl.Flush =>
if(!periodicReminder.isCancelled) {
periodicReminder.cancel
orders.foreach { VehicleSpawnControl.CancelOrder(_, Continent) }
orders = Nil
trackedOrder match {
case Some(entry) =>
VehicleSpawnControl.CancelOrder(entry, Continent)
case None => ;
}
trackedOrder = None
concealPlayer ! akka.actor.Kill //will cause the actor to restart, which will abort any trapped messages
periodicReminder.cancel
orders.foreach { VehicleSpawnControl.CancelOrder(_, Continent) }
orders = Nil
trackedOrder match {
case Some(entry) =>
VehicleSpawnControl.CancelOrder(entry, Continent)
case None => ;
}
trackedOrder = None
concealPlayer ! akka.actor.Kill //will cause the actor to restart, which will abort any trapped messages
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 net.psforever.objects.Vehicle
import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.deploy.DeploymentBehavior
import net.psforever.objects.vital.{VehicleShieldCharge, Vitality}
import net.psforever.types.ExoSuitType
/**
@ -59,6 +61,22 @@ class VehicleControl(vehicle : Vehicle) extends Actor
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) =>
val originalAffinity = vehicle.Faction
if(originalAffinity != (vehicle.Faction = faction)) {
@ -81,3 +99,24 @@ class VehicleControl(vehicle : Vehicle) extends Actor
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
package net.psforever.objects.ballistics
package net.psforever.objects.vital
/**
* 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
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.

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
package net.psforever.packet.game
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.PlanetSideEmpire
import scodec.Codec
@ -15,47 +16,43 @@ import scodec.codecs._
* 3) victim information<br>
* 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 a suicide, the player attributed is the player who was killed (message format displays only the victim).
* The victim's name is byte-aligned with a 5-bit buffer.<br>
* In the case of a suicide, the player attributed is the player who was killed (message format displays only the victim).<br>
* <br>
* 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."
* This holds true for every entry within thie same login session, at least.
* Blanking these values out does not change anything about the format of the event message.
* In the case of absentee kills, for example, where there is no killer listed, this field has been zero'd (`00000000`).<br>
* The same value will be seen in every `DestroyDisplayMessage` that includes that player,
* with respect to whether they are listed as the "killer" or as the "victim."
* This holds true for every entry within the same login session, at least.
* 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>
* The faction affiliation is different from the normal way `PlanetSideEmpire` values are recorded.
* The higher nibble will reflect the first part of the `PlanetSideEmpire` value.
* An extra `20` will be added if the player is in a vehicle or turret at the time.
* When marked as being in a vehicle or turret, the player's name will be enclosed within square brackets.
* 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>
* <br>
* 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.
* When marked as being in a vehicle or a turret, the player's name will be enclosed within square brackets.
* The length of the player's name found at the start of the character string does not reflect
* whether or not there will be square brackets (fortunately).
* The color of the resulting icon is borrowed from the attributed killer's faction affiliation if it can be determined
* and the type of icon is the same as an object id.
* An unidentified method or a missing icon defaults to a skull and crossbones.
* @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_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 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_charId Same as CharacterInfoMessage
* @param victim_charId same as CharacterInfoMessage
* @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,
killer_charId : Long,
killer_empire : PlanetSideEmpire.Value,
killer_inVehicle : Boolean,
killer_in_vehicle : Boolean,
unk : Int,
method : Int,
victim : String,
victim_charId : Long,
victim_empire : PlanetSideEmpire.Value,
victim_inVehicle : Boolean
victim_in_vehicle : Boolean
)
extends PlanetSideGamePacket {
type Packet = DestroyDisplayMessage
@ -68,12 +65,12 @@ object DestroyDisplayMessage extends Marshallable[DestroyDisplayMessage] {
("killer" | PacketHelpers.encodedWideString) ::
("killer_charId" | ulongL(32)) ::
("killer_empire" | PlanetSideEmpire.codec) ::
("killer_inVehicle" | bool) ::
("killer_in_vehicle" | bool) ::
("unk" | uint16L) ::
("method" | uint16L) ::
("victim" | PacketHelpers.encodedWideStringAligned(5)) ::
("victim_charId" | ulongL(32)) ::
("victim_empire" | PlanetSideEmpire.codec) ::
("victim_inVehicle" | bool)
("victim_in_vehicle" | bool)
).as[DestroyDisplayMessage]
}

View file

@ -156,6 +156,10 @@ final case class PlanetsideAttributeMessage(player_guid : PlanetSideGUID,
}
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] = (
("player_guid" | PlanetSideGUID.codec) ::
("attribute_type" | uint8L) ::

View file

@ -1,14 +1,15 @@
// Copyright (c) 2017 PSForever
package services.avatar
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.{PlanetSideGameObject, Player}
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.Container
import net.psforever.objects.zones.Zone
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.types.ExoSuitType
import net.psforever.types.{ExoSuitType, Vector3}
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_Stop(player_guid : PlanetSideGUID, weapon_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 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 KilledWhileInVehicle(player_guid : PlanetSideGUID) 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 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 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
import net.psforever.objects.Player
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.equipment.Equipment
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID, PlayerStateMessageUpstream}
import net.psforever.packet.game.objectcreate.ConstructorData
import net.psforever.types.ExoSuitType
import net.psforever.types.{ExoSuitType, Vector3}
object AvatarResponse {
trait Response
@ -17,8 +18,13 @@ object AvatarResponse {
final case class ChangeFireState_Start(weapon_guid : PlanetSideGUID) extends Response
final case class ChangeFireState_Stop(weapon_guid : PlanetSideGUID) 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 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 ObjectDelete(item_guid : PlanetSideGUID, unk : 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 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(
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) =>
val definition = item.Definition
val objectData = DroppedItemData(
@ -86,6 +98,14 @@ class AvatarService extends Actor {
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) =>
val pkt = pdata match {
case Some(data) =>
@ -169,22 +189,6 @@ class AvatarService extends Actor {
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 =>
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 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 ObjectDelete(player_guid : PlanetSideGUID, weapon_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 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

View file

@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever
package services.vehicle
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.{PlanetSideGameObject, Vehicle}
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 DismountVehicle(bailType : BailType.Value , unk2 : Boolean) 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 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 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 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 RevealPlayer(player_guid : PlanetSideGUID) 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(
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) =>
VehicleEvents.publish(
VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.SeatPermissions(vehicle_guid, seat_group, permission))

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
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.definition._
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.serverobject.terminals.Terminal
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.vehicles.DestroyedVehicle
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.packet.game.objectcreate._
import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3}
@ -368,7 +369,40 @@ class ConverterTest extends Specification {
ams.Utilities(4)().GUID = PlanetSideGUID(417)
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
package objects
import net.psforever.objects.{GlobalDefinitions, LocalProjectile, Tool}
import net.psforever.objects.ballistics.{DamageType, Projectile, ProjectileResolution, Projectiles}
import net.psforever.objects._
import net.psforever.objects.ballistics._
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
class ProjectileTest extends Specification {
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
val fury = Vehicle(GlobalDefinitions.fury)
"LocalProjectile" should {
"construct" in {
val obj = new LocalProjectile() //since they're just placeholders, they only need to construct
obj.Definition.ObjectId mustEqual 0
obj.Definition.Name mustEqual "projectile"
}
"local projectile range" in {
Projectile.BaseUID < Projectile.RangeUID mustEqual true
}
}
"ProjectileDefinition" should {
@ -151,48 +161,156 @@ class ProjectileTest extends Specification {
}
}
"Projectile" should {
"construct" in {
val beamer_wep = Tool(GlobalDefinitions.beamer)
val projectile = beamer_wep.Projectile
val obj = Projectile(projectile, beamer_wep.Definition, Vector3(1.2f, 3.4f, 5.6f), Vector3(0.2f, 0.4f, 0.6f))
"SourceEntry" should {
"construct for players" in {
SourceEntry(player) match {
case o : PlayerSource =>
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
obj.tool_def mustEqual GlobalDefinitions.beamer
"construct for vehicles" in {
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_angle mustEqual Vector3(0.2f, 0.4f, 0.6f)
obj.resolution mustEqual ProjectileResolution.Unresolved
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 {
val beamer_wep = Tool(GlobalDefinitions.beamer)
val projectile = beamer_wep.Projectile
val obj = Projectile(projectile, beamer_wep.Definition, Vector3(1.2f, 3.4f, 5.6f), Vector3(0.2f, 0.4f, 0.6f))
val obj2 = obj.Resolve(ProjectileResolution.MissedShot)
val obj = Projectile(projectile, beamer_def, firemode, PlayerSource(player), beamer_def.ObjectId, Vector3.Zero, Vector3.Zero)
obj.isResolved mustEqual false
obj.isMiss mustEqual false
obj.resolution mustEqual ProjectileResolution.Unresolved
obj.fire_time <= System.nanoTime mustEqual true
obj.hit_time mustEqual 0
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
obj.Resolve()
obj.isResolved mustEqual true
obj.isMiss mustEqual false
}
"resolve, with coordinates" in {
val beamer_wep = Tool(GlobalDefinitions.beamer)
val projectile = beamer_wep.Projectile
val obj = Projectile(projectile, beamer_wep.Definition, Vector3(1.2f, 3.4f, 5.6f), Vector3(0.2f, 0.4f, 0.6f))
val obj2 = obj.Resolve(Vector3(7.2f, 8.4f, 9.6f), Vector3(1.2f, 1.4f, 1.6f), ProjectileResolution.Resolved)
"missed" in {
val obj = Projectile(projectile, beamer_def, firemode, PlayerSource(player), beamer_def.ObjectId, Vector3.Zero, Vector3.Zero)
obj.isResolved mustEqual false
obj.isMiss mustEqual false
obj.resolution mustEqual ProjectileResolution.Unresolved
obj.current.Position mustEqual Vector3.Zero
obj.current.Orientation mustEqual Vector3.Zero
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)
obj.Miss()
obj.isResolved mustEqual true
obj.isMiss mustEqual true
}
}
"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 base.ActorTest
import net.psforever.objects._
import net.psforever.objects.ballistics.{PlayerSource, Projectile, ProjectileResolution, ResolvedProjectile}
import net.psforever.objects.definition.{SeatDefinition, VehicleDefinition}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.{VehicleShieldCharge, Vitality}
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.types.{CharacterVoice, ExoSuitType}
import net.psforever.types._
import org.specs2.mutable._
import scala.concurrent.duration.Duration
import scala.concurrent.duration._
class VehicleTest extends Specification {
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 {
import net.psforever.objects.Avatar
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._
class StandardRemoverActorTest extends ActorTest {
ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
"RemoverActor" should {
"handle a simple task" in {
expectNoMsg(500 milliseconds)
val probe = TestProbe()
val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere)
val reply1 = probe.receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert])
val reply2 = probe.receiveOne(200 milliseconds)
assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert])
probe.expectNoMsg(1 seconds) //delay
val reply3 = probe.receiveOne(300 milliseconds)
assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert])
val reply4 = probe.receiveOne(300 milliseconds)
assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
val reply5 = probe.receiveOne(300 milliseconds)
assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert])
val reply6 = probe.receiveOne(500 milliseconds)
assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
val reply7 = probe.receiveOne(500 milliseconds)
assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
}
}
}
//class StandardRemoverActorTest extends ActorTest {
// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver")
//
// "RemoverActor" should {
// "handle a simple task" in {
// expectNoMsg(500 milliseconds)
// val probe = TestProbe()
// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover")
// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere)
//
// val reply1 = probe.receiveOne(200 milliseconds)
// assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert])
// val reply2 = probe.receiveOne(200 milliseconds)
// assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert])
// probe.expectNoMsg(1 seconds) //delay
// val reply3 = probe.receiveOne(300 milliseconds)
// assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert])
// val reply4 = probe.receiveOne(300 milliseconds)
// assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert])
// val reply5 = probe.receiveOne(300 milliseconds)
// assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert])
// val reply6 = probe.receiveOne(500 milliseconds)
// assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert])
// val reply7 = probe.receiveOne(500 milliseconds)
// assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert])
// }
// }
//}
//class DelayedRemoverActorTest extends ActorTest {
// 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 services.ServiceManager.Lookup
import net.psforever.objects._
import net.psforever.objects.ballistics._
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.loadouts._
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.Terminal.TerminalMessage
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.packet.game.objectcreate._
import net.psforever.types._
@ -53,8 +56,6 @@ import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.Success
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 {
import WorldSessionActor._
@ -109,6 +110,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
implicit def boolToInt(b : Boolean) : Int = if(b) 1 else 0
override def postStop() = {
//TODO normally, player avatar persists a minute or so after disconnect; we are subject to the SessionReaper
clientKeepAlive.cancel
reviveTimer.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) {
//actually being alive or manually deconstructing
DismountVehicleOnLogOut()
continent.Population ! Zone.Population.Release(avatar)
player.Position = Vector3.Zero //save character before doing this
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectDelete(player_guid, player_guid))
player.Position = Vector3.Zero
if(player.VehicleSeated.nonEmpty) {
//quickly and briefly kill player to avoid disembark animation
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)
//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)) {
//player disconnected while waiting for a revive
//similar to handling ReleaseAvatarRequestMessage
player.Release
continent.Population ! Zone.Population.Release(avatar)
player.VehicleSeated match {
case None =>
FriskCorpse(player) //TODO eliminate dead letters
@ -189,7 +194,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Some(vehicle : Vehicle) =>
vehicle.Seat(vehicle.PassengerInSeat(player).get).get.Occupant = None
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}"))
@ -392,7 +397,30 @@ class WorldSessionActor extends Actor with MDCContextAware {
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) {
sendResponse(pkt)
}
@ -402,6 +430,25 @@ class WorldSessionActor extends Actor with MDCContextAware {
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) =>
if(tplayer_guid != guid) {
sendResponse(pkt)
@ -544,7 +591,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(0) }
reply match {
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) =>
sendResponse(ObjectAttachMessage(pad_guid, vehicle_guid, 3))
@ -577,6 +624,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(pkt)
}
case VehicleResponse.HitHint(source_guid) =>
sendResponse(HitHint(source_guid, player.GUID))
case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) =>
if(tplayer_guid != guid) {
//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))
}
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) =>
sendResponse(GenericObjectActionMessage(pad_guid, 92))
@ -787,6 +842,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Mountable.CanMount(obj : Vehicle, seat_num) =>
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
obj.Owner match {
case Some(owner_guid) =>
@ -807,7 +869,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
//update mounted weapon belonging to seat
weapon.AmmoSlots.foreach(slot => { //update the magazine(s) in the weapon, specifically
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
}
@ -859,6 +921,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Terminal.TerminalMessage(tplayer, msg, order) =>
order match {
case Terminal.BuyExosuit(exosuit, subtype) => //refresh armor points
tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), exosuit))
if(tplayer.ExoSuit == exosuit) {
if(exosuit == ExoSuitType.MAX) {
//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
log.info(s"$tplayer wants to change equipment loadout to their option #${msg.unk1 + 1}")
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, true))
tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), exosuit))
//ensure arm is down
tplayer.DrawnSlot = Player.HandsDownSlot
sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true))
@ -1105,7 +1169,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
else {
//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
stow
}
@ -1121,7 +1184,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
if(!tplayer.Certifications.contains(cert)) {
log.info(s"$tplayer is learning the $cert certification for $cost points")
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))
}
else {
@ -1133,7 +1196,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
if(tplayer.Certifications.contains(cert)) {
log.info(s"$tplayer is forgetting the $cert certification for $cost points")
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))
}
else {
@ -1289,9 +1352,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
case VehicleSpawnPad.PlayerSeatedInVehicle(vehicle, pad) =>
val vehicle_guid = vehicle.GUID
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)
ServerVehicleLock(vehicle)
@ -1551,6 +1611,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list
sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 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")
case InterstellarCluster.GiveWorld(zoneId, zone) =>
@ -1750,6 +1811,50 @@ class WorldSessionActor extends Actor with MDCContextAware {
case DelayedProximityUnitStop(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) =>
log.info(s"Received a direct message: $pkt")
sendResponse(pkt)
@ -1780,7 +1885,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
//TODO begin temp player character auto-loading; remove later
import net.psforever.objects.GlobalDefinitions._
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 += MediumAssault
avatar.Certifications += StandardExoSuit
@ -1814,6 +1919,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
avatar.Certifications += AdvancedEngineering
avatar.Certifications += FortificationEngineering
avatar.Certifications += AssaultEngineering
this.avatar = avatar
AwardBattleExperiencePoints(avatar, 1000000L)
player = new Player(avatar)
//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 {
TurnPlayerIntoCorpse
}
//load active vehicles in zone
continent.Vehicles.foreach(vehicle => {
//load vehicles in zone
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 vdefinition = vehicle.Definition
sendResponse(ObjectCreateMessage(vdefinition.ObjectId, vehicle_guid, vdefinition.Packet.ConstructorData(vehicle).get))
@ -2014,9 +2122,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
})
ReloadVehicleAccessPermissions(vehicle)
})
// Loop over vehicles again to add cargohold occupants after all vehicles have been created on the local client
continent.Vehicles.foreach(vehicle => {
//Loop over vehicles again to add cargohold occupants after all vehicles have been created on the local client
vehicles.foreach(vehicle => {
vehicle.CargoHolds.foreach({ case (cargo_num, cargo) => {
cargo.Occupant match {
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
continent.Map.TerminalToInterface.foreach({ case ((terminal_guid, interface_guid)) =>
@ -2198,8 +2315,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
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
sendResponse(ObjectDeleteMessage(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
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) =>
@ -2289,7 +2402,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
if(messagetype == ChatMessageType.CMT_SUICIDE) {
if(player.isAlive && deadState != DeadState.Release) {
KillPlayer(player)
Suicide(player)
}
}
if(messagetype == ChatMessageType.CMT_DESTROY) {
@ -2726,10 +2839,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Some(_ : LocalProjectile) =>
FindProjectileEntry(object_guid) match {
case Some(projectile) =>
if(projectile.resolution != ProjectileResolution.Unresolved) {
log.warn(s"RequestDestroy: tried to clean up missed projectile ${object_guid.guid} but it was already resolved")
if(projectile.isResolved) {
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 =>
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(ObjectDeleteMessage(kit.GUID, 0))
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
sendResponse(PlanetsideAttributeMessage(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
tool.Discharge
val projectileIndex = projectile_guid.guid - Projectile.BaseUID
val ang = obj match {
case _ : Player =>
obj.Orientation //TODO upper body facing
val projectilePlace = projectiles(projectileIndex)
if(projectilePlace match {
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 =>
tool.Orientation //TODO this is too simplistic
(tool.Orientation, obj.Definition.ObjectId) //TODO this is too simplistic to find proper angle
case _ =>
Vector3.Zero
(obj.Orientation, obj.Definition.ObjectId)
}
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 _ => ;
}
@ -3243,15 +3365,62 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ HitMessage(seq_time, projectile_guid, unk1, hit_info, unk2, unk3, unk4) =>
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) =>
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) =>
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) =>
log.info("AvatarFirstTimeEvent: " + msg)
@ -3422,9 +3591,21 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
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)
case msg @ CreateShortcutMessage(player_guid, slot, unk, add, shortcut) =>
@ -3433,8 +3614,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ FriendsRequest(action, friend) =>
log.info("FriendsRequest: "+msg)
case msg @ HitHint(source, player_guid) =>
log.info("HitHint: "+msg)
case msg @ HitHint(source_guid, player_guid) =>
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) =>
log.info("TargetingImplantRequest: "+msg)
@ -3977,7 +4164,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
val vehicle_guid = vehicle.GUID
(0 to 3).foreach(group => {
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>
* <br>
@ -4898,6 +5096,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
continent.GUID(tplayer.VehicleSeated.get) match {
case Some(obj : Vehicle) =>
TotalDriverVehicleControl(obj)
UnAccessContents(obj)
case _ => ;
}
//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()
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
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 = {
val healthFull : Boolean = if(player.Health < player.MaxHealth) {
player.History(HealFromTerm(PlayerSource(player), 10, 0, unit.Definition))
HealAction(player)
}
else {
true
}
val armorFull : Boolean = if(player.Armor < player.MaxArmor) {
player.History(HealFromTerm(PlayerSource(player), 0, 10, unit.Definition))
ArmorRepairAction(player)
}
else {
@ -5216,6 +5440,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
*/
def ProximityHealCrystal(unit : Terminal with ProximityUnit) : Unit = {
val healthFull : Boolean = if(player.Health < player.MaxHealth) {
player.History(HealFromTerm(PlayerSource(player), 10, 0, unit.Definition))
HealAction(player)
}
else {
@ -5364,11 +5589,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
* @param resolution the resolution status to promote 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 {
case Some(projectile) =>
val index = projectile_guid.guid - Projectile.BaseUID
ResolveProjectileEntry(projectile, index, resolution)
ResolveProjectileEntry(projectile, index, resolution, target, pos)
case None =>
log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found")
None
@ -5385,13 +5610,18 @@ class WorldSessionActor extends Actor with MDCContextAware {
* @param resolution the resolution status to promote the projectile
* @return a copy of the projectile
*/
def ResolveProjectileEntry(projectile : Projectile, index : Int, resolution : ProjectileResolution.Value) : Option[Projectile] = {
if(projectiles(index).contains(projectile)) {
projectiles(index) = Some(projectile.Resolve(ProjectileResolution.Resolved))
Some(projectile.Resolve(resolution))
def ResolveProjectileEntry(projectile : Projectile, index : Int, resolution : ProjectileResolution.Value, target : PlanetSideGameObject with FactionAffinity with Vitality, pos : Vector3) : Option[ResolvedProjectile] = {
if(!projectiles(index).contains(projectile)) {
log.error(s"expected projectile could not be found at $index; can not resolve")
None
}
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 {
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))
}
/**
* 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) = {
log.error(error)
sendResponse(ConnectionClose())