mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-20 02:24:45 +00:00
Merge branch 'master' into continents
This commit is contained in:
commit
bf178a1b34
|
|
@ -162,6 +162,36 @@ object GlobalDefinitions {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the definition for a piece of `Equipment` determine if it is a grenade-type weapon.
|
||||
* Only the normal grenades count; the grenade packs are excluded.
|
||||
* @param edef the `EquipmentDefinition` of the item
|
||||
* @return `true`, if it is a grenade-type weapon; `false`, otherwise
|
||||
*/
|
||||
def isGrenade(edef : EquipmentDefinition) : Boolean = {
|
||||
edef match {
|
||||
case `frag_grenade` | `jammer_grenade` | `plasma_grenade` =>
|
||||
true
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the definition for a piece of `Equipment` determine if it is a grenade-type weapon.
|
||||
* Only the grenade packs count; the normal grenades are excluded.
|
||||
* @param edef the `EquipmentDefinition` of the item
|
||||
* @return `true`, if it is a grenade-type weapon; `false`, otherwise
|
||||
*/
|
||||
def isGrenadePack(edef : EquipmentDefinition) : Boolean = {
|
||||
edef match {
|
||||
case `frag_cartridge` | `jammer_cartridge` | `plasma_cartridge` =>
|
||||
true
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the definition for a piece of `Equipment` determine with which faction it aligns if it is a weapon.
|
||||
* Only checks `Tool` objects.
|
||||
|
|
@ -242,6 +272,43 @@ object GlobalDefinitions {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Implants
|
||||
*/
|
||||
val
|
||||
advanced_regen = ImplantDefinition(0)
|
||||
|
||||
val
|
||||
targeting = ImplantDefinition(1)
|
||||
|
||||
val
|
||||
audio_amplifier = ImplantDefinition(2)
|
||||
|
||||
val
|
||||
darklight_vision = ImplantDefinition(3)
|
||||
|
||||
val
|
||||
melee_booster = ImplantDefinition(4)
|
||||
|
||||
val
|
||||
personal_shield = ImplantDefinition(5)
|
||||
|
||||
val
|
||||
range_magnifier = ImplantDefinition(6)
|
||||
|
||||
val
|
||||
second_wind = ImplantDefinition(7)
|
||||
|
||||
val
|
||||
silent_run = ImplantDefinition(8)
|
||||
|
||||
val
|
||||
surge = ImplantDefinition(9)
|
||||
|
||||
/*
|
||||
Equipment (locker_container, kits, ammunition, weapons)
|
||||
*/
|
||||
import net.psforever.packet.game.objectcreate.ObjectClass
|
||||
val
|
||||
locker_container = new EquipmentDefinition(456) {
|
||||
Name = "locker container"
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.definition.{ImplantDefinition, Stance}
|
||||
import net.psforever.types.{ExoSuitType, ImplantType}
|
||||
|
||||
/**
|
||||
* A type of installable player utility that grants a perk, usually in exchange for stamina (energy).<br>
|
||||
* <br>
|
||||
* An implant starts with a never-to-initialized timer value of -1 and will not report as `Ready` until the timer is 0.
|
||||
* The `Timer`, however, will report to the user a time of 0 since negative time does not make sense.
|
||||
* Although the `Timer` can be manually set, using `Reset` is the better way to default the initialization timer to the correct amount.
|
||||
* An external script will be necessary to operate the actual initialization countdown.
|
||||
* An implant must be `Ready` before it can be `Active`.
|
||||
* The `Timer` must be set (or reset) (or countdown) to 0 to be `Ready` and then it must be activated.
|
||||
* @param implantDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields
|
||||
*/
|
||||
class Implant(implantDef : ImplantDefinition) {
|
||||
private var active : Boolean = false
|
||||
private var initTimer : Long = -1L
|
||||
|
||||
def Name : String = implantDef.Name
|
||||
|
||||
def Ready : Boolean = initTimer == 0L
|
||||
|
||||
def Active : Boolean = active
|
||||
|
||||
def Active_=(isActive : Boolean) : Boolean = {
|
||||
active = Ready && isActive
|
||||
Active
|
||||
}
|
||||
|
||||
def Timer : Long = math.max(0, initTimer)
|
||||
|
||||
def Timer_=(time : Long) : Long = {
|
||||
initTimer = math.max(0, time)
|
||||
Timer
|
||||
}
|
||||
|
||||
def MaxTimer : Long = implantDef.Initialization
|
||||
|
||||
def ActivationCharge : Int = Definition.ActivationCharge
|
||||
|
||||
/**
|
||||
* Calculate the stamina consumption of the implant for any given moment of being active after its activation.
|
||||
* As implant energy use can be influenced by both exo-suit worn and general stance held, both are considered.
|
||||
* @param suit the exo-suit being worn
|
||||
* @param stance the player's stance
|
||||
* @return the amount of stamina (energy) that is consumed
|
||||
*/
|
||||
def Charge(suit : ExoSuitType.Value, stance : Stance.Value) : Int = {
|
||||
if(active) {
|
||||
implantDef.DurationChargeBase + implantDef.DurationChargeByExoSuit(suit) + implantDef.DurationChargeByStance(stance)
|
||||
}
|
||||
else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Place an implant back in its initializing state.
|
||||
*/
|
||||
def Reset() : Unit = {
|
||||
Active = false
|
||||
Timer = MaxTimer
|
||||
}
|
||||
|
||||
/**
|
||||
* Place an implant back in its pre-initialization state.
|
||||
* The implant is inactive and can not proceed to a `Ready` condition naturally from this state.
|
||||
*/
|
||||
def Jammed() : Unit = {
|
||||
Active = false
|
||||
Timer = -1
|
||||
}
|
||||
|
||||
def Definition : ImplantDefinition = implantDef
|
||||
}
|
||||
|
||||
object Implant {
|
||||
def default : Implant = new Implant(ImplantDefinition(ImplantType.RangeMagnifier))
|
||||
|
||||
def apply(implantDef : ImplantDefinition) : Implant = {
|
||||
new Implant(implantDef)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +1,117 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.definition.ImplantDefinition
|
||||
import net.psforever.types.ImplantType
|
||||
import net.psforever.objects.definition.{ImplantDefinition, Stance}
|
||||
import net.psforever.types.{ExoSuitType, ImplantType}
|
||||
|
||||
/**
|
||||
* A slot "on the player" into which an implant is installed.<br>
|
||||
* A slot "on the player" into which an implant is installed.
|
||||
* In total, players have three implant slots.<br>
|
||||
* <br>
|
||||
* In total, players have three implant slots.
|
||||
* At battle rank one (BR1), however, all of those slots are locked.
|
||||
* The player earns implants at BR16, BR12, and BR18.
|
||||
* A locked implant slot can not be used.
|
||||
* (The code uses "not yet unlocked" logic.)
|
||||
* When unlocked, an implant may be installed into that slot.<br>
|
||||
* <br>
|
||||
* The default implant that the underlying slot utilizes is the "Range Magnifier."
|
||||
* Until the `Installed` condition is some value other than `None`, however, the implant in the slot will not work.
|
||||
* All implants slots start as "locked" and must be "unlocked" through battle rank advancement.
|
||||
* Only after it is "unlocked" may an implant be "installed" into the slot.
|
||||
* Upon installation, it undergoes an initialization period and then, after which, it is ready for user activation.
|
||||
* Being jammed de-activates the implant, put it into a state of "not being ready," and causes the initialization to repeat.
|
||||
*/
|
||||
class ImplantSlot {
|
||||
/** is this slot available for holding an implant */
|
||||
private var unlocked : Boolean = false
|
||||
/** whether this implant is ready for use */
|
||||
private var initialized : Boolean = false
|
||||
/** is this implant active */
|
||||
private var active : Boolean = false
|
||||
/** what implant is currently installed in this slot; None if there is no implant currently installed */
|
||||
private var installed : Option[ImplantType.Value] = None
|
||||
/** the entry for that specific implant used by the a player; always occupied by some type of implant */
|
||||
private var implant : Implant = ImplantSlot.default
|
||||
private var implant : Option[ImplantDefinition] = None
|
||||
|
||||
def Unlocked : Boolean = unlocked
|
||||
|
||||
def Unlocked_=(lock : Boolean) : Boolean = {
|
||||
unlocked = lock
|
||||
unlocked = lock || unlocked
|
||||
Unlocked
|
||||
}
|
||||
|
||||
def Installed : Option[ImplantType.Value] = installed
|
||||
def Initialized : Boolean = initialized
|
||||
|
||||
def Implant : Option[Implant] = if(Installed.isDefined) { Some(implant) } else { None }
|
||||
def Initialized_=(init : Boolean) : Boolean = {
|
||||
initialized = Installed.isDefined && init
|
||||
Active = Active && initialized //can not be active just yet
|
||||
Initialized
|
||||
}
|
||||
|
||||
def Implant_=(anImplant : Option[Implant]) : Option[Implant] = {
|
||||
anImplant match {
|
||||
case Some(module) =>
|
||||
Implant = module
|
||||
case None =>
|
||||
installed = None
|
||||
def Active : Boolean = active
|
||||
|
||||
def Active_=(state : Boolean) : Boolean = {
|
||||
active = Initialized && state
|
||||
Active
|
||||
}
|
||||
|
||||
def Implant : ImplantType.Value = if(Installed.isDefined) {
|
||||
implant.get.Type
|
||||
}
|
||||
else {
|
||||
Active = false
|
||||
Initialized = false
|
||||
ImplantType.None
|
||||
}
|
||||
|
||||
def Implant_=(anImplant : ImplantDefinition) : ImplantType.Value = {
|
||||
Implant_=(Some(anImplant))
|
||||
}
|
||||
|
||||
def Implant_=(anImplant : Option[ImplantDefinition]) : ImplantType.Value = {
|
||||
if(Unlocked) {
|
||||
anImplant match {
|
||||
case Some(_) =>
|
||||
implant = anImplant
|
||||
case None =>
|
||||
implant = None
|
||||
}
|
||||
}
|
||||
Implant
|
||||
}
|
||||
|
||||
def Implant_=(anImplant : Implant) : Option[Implant] = {
|
||||
implant = anImplant
|
||||
installed = Some(anImplant.Definition.Type)
|
||||
Implant
|
||||
def Installed : Option[ImplantDefinition] = implant
|
||||
|
||||
def MaxTimer : Long = Implant match {
|
||||
case ImplantType.None =>
|
||||
-1L
|
||||
case _ =>
|
||||
Installed.get.Initialization
|
||||
}
|
||||
|
||||
def ActivationCharge : Int = {
|
||||
if(Active) {
|
||||
Installed.get.ActivationCharge
|
||||
}
|
||||
else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the stamina consumption of the implant for any given moment of being active after its activation.
|
||||
* As implant energy use can be influenced by both exo-suit worn and general stance held, both are considered.
|
||||
* @param suit the exo-suit being worn
|
||||
* @param stance the player's stance
|
||||
* @return the amount of stamina (energy) that is consumed
|
||||
*/
|
||||
def Charge(suit : ExoSuitType.Value, stance : Stance.Value) : Int = {
|
||||
if(Active) {
|
||||
val inst = Installed.get
|
||||
inst.DurationChargeBase + inst.DurationChargeByExoSuit(suit) + inst.DurationChargeByStance(stance)
|
||||
}
|
||||
else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
def Jammed() : Unit = {
|
||||
Active = false
|
||||
Initialized = false
|
||||
}
|
||||
}
|
||||
|
||||
object ImplantSlot {
|
||||
private val default = new Implant(ImplantDefinition(ImplantType.RangeMagnifier))
|
||||
|
||||
def apply() : ImplantSlot = {
|
||||
new ImplantSlot()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects
|
||||
|
||||
import net.psforever.objects.definition.AvatarDefinition
|
||||
import net.psforever.objects.definition.{AvatarDefinition, ImplantDefinition}
|
||||
import net.psforever.objects.equipment.{Equipment, EquipmentSize}
|
||||
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
|
||||
import net.psforever.packet.game.PlanetSideGUID
|
||||
import net.psforever.types._
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.mutable
|
||||
|
||||
class Player(private val name : String,
|
||||
private val faction : PlanetSideEmpire.Value,
|
||||
|
|
@ -33,6 +34,9 @@ class Player(private val name : String,
|
|||
|
||||
private val loadouts : Array[Option[InfantryLoadout]] = Array.fill[Option[InfantryLoadout]](10)(None)
|
||||
|
||||
private var bep : Long = 0
|
||||
private var cep : Long = 0
|
||||
private val certifications : mutable.Set[CertificationType.Value] = mutable.Set[CertificationType.Value]()
|
||||
private val implants : Array[ImplantSlot] = Array.fill[ImplantSlot](3)(new ImplantSlot)
|
||||
|
||||
// private var tosRibbon : MeritCommendation.Value = MeritCommendation.None
|
||||
|
|
@ -312,28 +316,40 @@ class Player(private val name : String,
|
|||
exosuit = suit
|
||||
}
|
||||
|
||||
def BEP : Long = bep
|
||||
|
||||
def BEP_=(battleExperiencePoints : Long) : Long = {
|
||||
bep = math.max(0L, math.min(battleExperiencePoints, 4294967295L))
|
||||
BEP
|
||||
}
|
||||
|
||||
def CEP : Long = cep
|
||||
|
||||
def CEP_=(commandExperiencePoints : Long) : Long = {
|
||||
cep = math.max(0L, math.min(commandExperiencePoints, 4294967295L))
|
||||
CEP
|
||||
}
|
||||
|
||||
def Certifications : mutable.Set[CertificationType.Value] = certifications
|
||||
|
||||
def Implants : Array[ImplantSlot] = implants
|
||||
|
||||
def Implant(slot : Int) : Option[ImplantType.Value] = {
|
||||
if(-1 < slot && slot < implants.length) { implants(slot).Installed } else { None }
|
||||
def Implant(slot : Int) : ImplantType.Value = {
|
||||
if(-1 < slot && slot < implants.length) { implants(slot).Implant } else { ImplantType.None }
|
||||
}
|
||||
|
||||
def Implant(implantType : ImplantType.Value) : Option[Implant] = {
|
||||
implants.find(_.Installed.contains(implantType)) match {
|
||||
case Some(slot) =>
|
||||
slot.Implant
|
||||
case None =>
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
def InstallImplant(implant : Implant) : Boolean = {
|
||||
getAvailableImplantSlot(implants.iterator, implant.Definition.Type) match {
|
||||
case Some(slot) =>
|
||||
slot.Implant = implant
|
||||
slot.Implant.get.Reset()
|
||||
true
|
||||
def InstallImplant(implant : ImplantDefinition) : Boolean = {
|
||||
implants.find({p => p.Installed.contains(implant)}) match { //try to find the installed implant
|
||||
case None =>
|
||||
//install in a free slot
|
||||
getAvailableImplantSlot(implants.iterator, implant.Type) match {
|
||||
case Some(slot) =>
|
||||
slot.Implant = implant
|
||||
true
|
||||
case None =>
|
||||
false
|
||||
}
|
||||
case Some(_) =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -344,7 +360,7 @@ class Player(private val name : String,
|
|||
}
|
||||
else {
|
||||
val slot = iter.next
|
||||
if(!slot.Unlocked || slot.Installed.contains(implantType)) {
|
||||
if(!slot.Unlocked || slot.Implant == implantType) {
|
||||
None
|
||||
}
|
||||
else if(slot.Installed.isEmpty) {
|
||||
|
|
@ -357,7 +373,7 @@ class Player(private val name : String,
|
|||
}
|
||||
|
||||
def UninstallImplant(implantType : ImplantType.Value) : Boolean = {
|
||||
implants.find({slot => slot.Installed.contains(implantType)}) match {
|
||||
implants.find({slot => slot.Implant == implantType}) match {
|
||||
case Some(slot) =>
|
||||
slot.Implant = None
|
||||
true
|
||||
|
|
@ -368,9 +384,9 @@ class Player(private val name : String,
|
|||
|
||||
def ResetAllImplants() : Unit = {
|
||||
implants.foreach(slot => {
|
||||
slot.Implant match {
|
||||
case Some(implant) =>
|
||||
implant.Reset()
|
||||
slot.Installed match {
|
||||
case Some(_) =>
|
||||
slot.Initialized = false
|
||||
case None => ;
|
||||
}
|
||||
})
|
||||
|
|
@ -570,7 +586,7 @@ object Player {
|
|||
//hand over implants
|
||||
(0 until 3).foreach(index => {
|
||||
if(obj.Implants(index).Unlocked = player.Implants(index).Unlocked) {
|
||||
obj.Implants(index).Implant = player.Implants(index).Implant
|
||||
obj.Implants(index).Implant = player.Implants(index).Installed
|
||||
}
|
||||
})
|
||||
//hand over knife
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ import scala.collection.mutable
|
|||
* An `Enumeration` of a variety of poses or generalized movement.
|
||||
*/
|
||||
object Stance extends Enumeration {
|
||||
val Crouching,
|
||||
Standing,
|
||||
Walking, //not used, but should still be defined
|
||||
Running = Value
|
||||
val
|
||||
Crouching,
|
||||
CrouchWalking, //not used, but should still be defined
|
||||
Standing,
|
||||
Walking, //not used, but should still be defined
|
||||
Running
|
||||
= Value
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.definition.converter
|
||||
|
||||
import net.psforever.objects.{EquipmentSlot, Player}
|
||||
import net.psforever.objects.{EquipmentSlot, GlobalDefinitions, ImplantSlot, Player}
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.packet.game.PlanetSideGUID
|
||||
import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle}
|
||||
import net.psforever.types.GrenadeState
|
||||
import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle}
|
||||
import net.psforever.types.{GrenadeState, ImplantType}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.{Success, Try}
|
||||
|
|
@ -17,11 +16,11 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
|
|||
MakeAppearanceData(obj),
|
||||
obj.Health / obj.MaxHealth * 255, //TODO not precise
|
||||
obj.Armor / obj.MaxArmor * 255, //TODO not precise
|
||||
UniformStyle.Normal,
|
||||
0,
|
||||
DressBattleRank(obj),
|
||||
DressCommandRank(obj),
|
||||
recursiveMakeImplantEffects(obj.Implants.iterator),
|
||||
None, //TODO cosmetics
|
||||
None, //TODO implant effects
|
||||
InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)),
|
||||
InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)), //TODO is sorting necessary?
|
||||
GetDrawnSlot(obj)
|
||||
)
|
||||
)
|
||||
|
|
@ -32,20 +31,21 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
|
|||
Success(
|
||||
DetailedCharacterData(
|
||||
MakeAppearanceData(obj),
|
||||
obj.BEP,
|
||||
obj.CEP,
|
||||
obj.MaxHealth,
|
||||
obj.Health,
|
||||
obj.Armor,
|
||||
1, 7, 7,
|
||||
obj.MaxStamina,
|
||||
obj.Stamina,
|
||||
28, 4, 44, 84, 104, 1900,
|
||||
obj.Certifications.toList.sortBy(_.id), //TODO is sorting necessary?
|
||||
MakeImplantEntries(obj),
|
||||
List.empty[String], //TODO fte list
|
||||
List.empty[String], //TODO tutorial list
|
||||
InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)),
|
||||
GetDrawnSlot(obj)
|
||||
)
|
||||
)
|
||||
//TODO tidy this mess up
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -64,8 +64,8 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
|
|||
"",
|
||||
0,
|
||||
obj.isBackpack,
|
||||
obj.Orientation.y.toInt,
|
||||
obj.FacingYawUpper.toInt,
|
||||
obj.Orientation.y,
|
||||
obj.FacingYawUpper,
|
||||
true,
|
||||
GrenadeState.None,
|
||||
false,
|
||||
|
|
@ -75,6 +75,107 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the appropriate `UniformStyle` design for a player's accumulated battle experience points.
|
||||
* At certain battle ranks, all exo-suits undergo some form of coloration change.
|
||||
* @param obj the `Player` game object
|
||||
* @return the resulting uniform upgrade level
|
||||
*/
|
||||
private def DressBattleRank(obj : Player) : UniformStyle.Value = {
|
||||
val bep : Long = obj.BEP
|
||||
if(bep > 2583440) { //BR25+
|
||||
UniformStyle.ThirdUpgrade
|
||||
}
|
||||
else if(bep > 308989) { //BR14+
|
||||
UniformStyle.SecondUpgrade
|
||||
}
|
||||
else if(bep > 44999) { //BR7+
|
||||
UniformStyle.FirstUpgrade
|
||||
}
|
||||
else { //BR1+
|
||||
UniformStyle.Normal
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the appropriate design for a player's accumulated command experience points.
|
||||
* Visual cues for command rank include armlets, anklets, and, finally, a backpack, awarded at different ranks.
|
||||
* @param obj the `Player` game object
|
||||
* @return the resulting uniform upgrade level
|
||||
*/
|
||||
private def DressCommandRank(obj : Player) : Int = {
|
||||
val cep = obj.CEP
|
||||
if(cep > 599999) {
|
||||
5
|
||||
}
|
||||
else if(cep > 299999) {
|
||||
4
|
||||
}
|
||||
else if(cep > 149999) {
|
||||
3
|
||||
}
|
||||
else if(cep > 49999) {
|
||||
2
|
||||
}
|
||||
else if(cep > 9999) {
|
||||
1
|
||||
}
|
||||
else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an `Array` of `Implant` objects into a `List` of `ImplantEntry` objects suitable as packet data.
|
||||
* @param obj the `Player` game object
|
||||
* @return the resulting implant `List`
|
||||
* @see `ImplantEntry` in `DetailedCharacterData`
|
||||
*/
|
||||
private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = {
|
||||
obj.Implants.map(slot => {
|
||||
slot.Installed match {
|
||||
case Some(_) =>
|
||||
if(slot.Initialized) {
|
||||
ImplantEntry(slot.Implant, None)
|
||||
}
|
||||
else {
|
||||
ImplantEntry(slot.Implant, Some(slot.Installed.get.Initialization.toInt))
|
||||
}
|
||||
case None =>
|
||||
ImplantEntry(ImplantType.None, None)
|
||||
}
|
||||
}).toList
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an active implant whose effect will be displayed on this player.
|
||||
* @param iter an `Iterator` of `ImplantSlot` objects
|
||||
* @return the effect of an active implant
|
||||
*/
|
||||
@tailrec private def recursiveMakeImplantEffects(iter : Iterator[ImplantSlot]) : Option[ImplantEffects.Value] = {
|
||||
if(!iter.hasNext) {
|
||||
None
|
||||
}
|
||||
else {
|
||||
val slot = iter.next
|
||||
if(slot.Active) {
|
||||
import GlobalDefinitions._
|
||||
slot.Installed match {
|
||||
case Some(`advanced_regen`) =>
|
||||
Some(ImplantEffects.RegenEffects)
|
||||
case Some(`darklight_vision`) =>
|
||||
Some(ImplantEffects.DarklightEffects)
|
||||
case Some(`personal_shield`) =>
|
||||
Some(ImplantEffects.PersonalShieldEffects)
|
||||
case Some(`surge`) =>
|
||||
Some(ImplantEffects.SurgeEffects)
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
recursiveMakeImplantEffects(iter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a player with an inventory, convert the contents of that inventory into converted-decoded packet data.
|
||||
* The inventory is not represented in a `0x17` `Player`, so the conversion is only valid for `0x18` avatars.
|
||||
|
|
@ -139,6 +240,14 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
|
|||
InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given some equipment holsters, convert the contents of those holsters into converted-decoded packet data.
|
||||
* @param iter an `Iterator` of `EquipmentSlot` objects that are a part of the player's holsters
|
||||
* @param builder the function used to transform to the decoded packet form
|
||||
* @param list the current `List` of transformed data
|
||||
* @param index which holster is currently being explored
|
||||
* @return the `List` of inventory data created from the holsters
|
||||
*/
|
||||
@tailrec private def recursiveMakeHolsters(iter : Iterator[EquipmentSlot], builder : ((Int, Equipment) => InternalSlot), list : List[InternalSlot] = Nil, index : Int = 0) : List[InternalSlot] = {
|
||||
if(!iter.hasNext) {
|
||||
list
|
||||
|
|
|
|||
|
|
@ -117,12 +117,27 @@ final case class CharacterAppearanceData(pos : PlacementData,
|
|||
val placementSize : Long = pos.bitsize
|
||||
val nameStringSize : Long = StreamBitSize.stringBitSize(basic_appearance.name, 16) + CharacterAppearanceData.namePadding(pos.vel)
|
||||
val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) + CharacterAppearanceData.outfitNamePadding
|
||||
val altModelSize = if(on_zipline || backpack) { 1L } else { 0L }
|
||||
val altModelSize = CharacterAppearanceData.altModelBit(this).getOrElse(0)
|
||||
335L + placementSize + nameStringSize + outfitStringSize + altModelSize
|
||||
}
|
||||
}
|
||||
|
||||
object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
|
||||
/**
|
||||
* When a player is released-dead or attached to a zipline, their basic infantry model is replaced with a different one.
|
||||
* In the former casde, a backpack.
|
||||
* In the latter case, a ball of colored energy.
|
||||
* In this state, the length of the stream of data is modified.
|
||||
* @param app the appearance
|
||||
* @return the length of the variable field that exists when using alternate models
|
||||
*/
|
||||
def altModelBit(app : CharacterAppearanceData) : Option[Int] = if(app.backpack || app.on_zipline) {
|
||||
Some(1)
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the padding of the player's name.
|
||||
* The padding will always be a number 0-7.
|
||||
|
|
@ -151,7 +166,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
|
|||
("faction" | PlanetSideEmpire.codec) ::
|
||||
("black_ops" | bool) ::
|
||||
(("alt_model" | bool) >>:~ { alt_model => //modifies stream format (to display alternate player models)
|
||||
ignore(1) :: //unknown
|
||||
ignore(1) :: //unknown
|
||||
("jammered" | bool) ::
|
||||
bool :: //crashes client
|
||||
uint(16) :: //unknown, but usually 0
|
||||
|
|
@ -204,7 +219,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
|
|||
Attempt.failure(Err(s"character $name's faction can not declare as neutral"))
|
||||
|
||||
case CharacterAppearanceData(pos, BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons) =>
|
||||
val has_outfit_name : Long = outfit.length.toLong //todo this is a kludge
|
||||
val has_outfit_name : Long = outfit.length.toLong //TODO this is a kludge
|
||||
var alt_model : Boolean = false
|
||||
var alt_model_extrabit : Option[Boolean] = None
|
||||
if(zipline || bpack) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,31 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.packet.game.objectcreate
|
||||
|
||||
import net.psforever.newcodecs.newcodecs
|
||||
import net.psforever.packet.{Marshallable, PacketHelpers}
|
||||
import net.psforever.types.{CertificationType, ImplantType}
|
||||
import scodec.{Attempt, Codec}
|
||||
import scodec.codecs._
|
||||
import shapeless.{::, HNil}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
/**
|
||||
* An entry in the `List` of valid implant slots in `DetailedCharacterData`.
|
||||
* `activation`, if defined, indicates the time remaining (in seconds?) before an implant becomes usable.
|
||||
* @param implant the type of implant
|
||||
* @param activation the activation timer;
|
||||
* technically, this is "unconfirmed"
|
||||
* @see `ImplantType`
|
||||
*/
|
||||
final case class ImplantEntry(implant : ImplantType.Value,
|
||||
activation : Option[Int]) extends StreamBitSize {
|
||||
override def bitsize : Long = {
|
||||
val activationSize = if(activation.isDefined) { 12L } else { 5L }
|
||||
5L + activationSize
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the avatar portion of `ObjectCreateDetailedMessage` packet data.
|
||||
* This densely-packed information outlines most of the specifics required to depict a character as an avatar.<br>
|
||||
|
|
@ -15,16 +35,16 @@ import shapeless.{::, HNil}
|
|||
* <br>
|
||||
* Divisions exist to make the data more manageable.
|
||||
* The first division of data only manages the general appearance of the player's in-game model.
|
||||
* The second division (currently, the fields actually in this class) manages the status of the character as an avatar.
|
||||
* It is shared between `DetailedCharacterData` avatars and `CharacterData` player characters.
|
||||
* The second division (of fields) manages the status of the character as an avatar.
|
||||
* In general, it passes more thorough data about the character that the client can display to the owner of the client.
|
||||
* For example, health is a full number, rather than a percentage.
|
||||
* Just as prominent is the list of first time events and the list of completed tutorials.
|
||||
* The third subdivision is also exclusive to avatar-prepared characters and contains (omitted).
|
||||
* The fourth is the inventory (composed of `Direct`-type objects).<br>
|
||||
* <br>
|
||||
* Exploration:<br>
|
||||
* Lots of analysis needed for the remainder of the byte data.
|
||||
* The fourth is the inventory (composed of `Direct`-type objects).
|
||||
* @param appearance data about the avatar's basic aesthetics
|
||||
* @param bep the avatar's battle experience points, which determines his Battle Rank
|
||||
* @param cep the avatar's command experience points, which determines his Command Rank
|
||||
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value;
|
||||
* range is 0-65535
|
||||
* @param health for `x / y` of hitpoints, this is the avatar's `x` value;
|
||||
|
|
@ -42,32 +62,25 @@ import shapeless.{::, HNil}
|
|||
* range is 0-65535
|
||||
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value;
|
||||
* range is 0-65535
|
||||
* @param unk4 na;
|
||||
* defaults to 28
|
||||
* @param unk5 na;
|
||||
* defaults to 4
|
||||
* @param unk6 na;
|
||||
* defaults to 44
|
||||
* @param unk7 na;
|
||||
* defaults to 84
|
||||
* @param unk8 na;
|
||||
* defaults to 104
|
||||
* @param unk9 na;
|
||||
* defaults to 1900
|
||||
* @param certs the `List` of active certifications
|
||||
* @param implants the `List` of implant slots currently possessed by this avatar
|
||||
* @param firstTimeEvents the list of first time events performed by this avatar;
|
||||
* the size field is a 32-bit number;
|
||||
* the first entry may be padded
|
||||
* @param tutorials the list of tutorials completed by this avatar;
|
||||
* @param tutorials the `List` of tutorials completed by this avatar;
|
||||
* the size field is a 32-bit number;
|
||||
* the first entry may be padded
|
||||
* @param inventory the avatar's inventory
|
||||
* @param drawn_slot the holster that is initially drawn
|
||||
* @see `CharacterAppearanceData`
|
||||
* @see `CharacterData`
|
||||
* @see `InventoryData`
|
||||
* @see `DrawnSlot`
|
||||
* @see `CharacterAppearanceData`<br>
|
||||
* `CharacterData`<br>
|
||||
* `CertificationType`<br>
|
||||
* `InventoryData`<br>
|
||||
* `DrawnSlot`
|
||||
*/
|
||||
final case class DetailedCharacterData(appearance : CharacterAppearanceData,
|
||||
bep : Long,
|
||||
cep : Long,
|
||||
healthMax : Int,
|
||||
health : Int,
|
||||
armor : Int,
|
||||
|
|
@ -76,177 +89,252 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData,
|
|||
unk3 : Int, //7
|
||||
staminaMax : Int,
|
||||
stamina : Int,
|
||||
unk4 : Int, //28
|
||||
unk5 : Int, //4
|
||||
unk6 : Int, //44
|
||||
unk7 : Int, //84
|
||||
unk8 : Int, //104
|
||||
unk9 : Int, //1900
|
||||
certs : List[CertificationType.Value],
|
||||
implants : List[ImplantEntry],
|
||||
firstTimeEvents : List[String],
|
||||
tutorials : List[String],
|
||||
inventory : Option[InventoryData],
|
||||
drawn_slot : DrawnSlot.Value = DrawnSlot.None
|
||||
) extends ConstructorData {
|
||||
) extends ConstructorData {
|
||||
|
||||
override def bitsize : Long = {
|
||||
//factor guard bool values into the base size, not its corresponding optional field
|
||||
//factor guard bool values into the base size, not corresponding optional fields, unless contained or enumerated
|
||||
val appearanceSize = appearance.bitsize
|
||||
val certSize = (certs.length + 1) * 8 //cert list
|
||||
var implantSize : Long = 0L //implant list
|
||||
for(entry <- implants) {
|
||||
implantSize += entry.bitsize
|
||||
}
|
||||
val implantPadding = DetailedCharacterData.implantFieldPadding(implants, CharacterAppearanceData.altModelBit(appearance))
|
||||
val fteLen = firstTimeEvents.size //fte list
|
||||
var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen)
|
||||
var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen, implantPadding)
|
||||
for(str <- firstTimeEvents) {
|
||||
eventListSize += StreamBitSize.stringBitSize(str)
|
||||
}
|
||||
val tutLen = tutorials.size //tutorial list
|
||||
var tutorialListSize : Long = 32L + DetailedCharacterData.tutPadding(fteLen, tutLen)
|
||||
var tutorialListSize : Long = 32L + DetailedCharacterData.tutPadding(fteLen, tutLen, implantPadding)
|
||||
for(str <- tutorials) {
|
||||
tutorialListSize += StreamBitSize.stringBitSize(str)
|
||||
}
|
||||
var inventorySize : Long = 0L //inventory
|
||||
if(inventory.isDefined) {
|
||||
inventorySize = inventory.get.bitsize
|
||||
val inventorySize : Long = if(inventory.isDefined) { //inventory
|
||||
inventory.get.bitsize
|
||||
}
|
||||
713L + appearanceSize + eventListSize + tutorialListSize + inventorySize
|
||||
else {
|
||||
0L
|
||||
}
|
||||
649L + appearanceSize + certSize + implantSize + eventListSize + tutorialListSize + inventorySize
|
||||
}
|
||||
}
|
||||
|
||||
object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
|
||||
/**
|
||||
* Overloaded constructor for `DetailedCharacterData` that skips all the unknowns by assigning defaulted values.
|
||||
* It also allows for a not-optional inventory.
|
||||
* Overloaded constructor for `DetailedCharacterData` that requires an inventory and drops unknown values.
|
||||
* @param appearance data about the avatar's basic aesthetics
|
||||
* @param bep the avatar's battle experience points, which determines his Battle Rank
|
||||
* @param cep the avatar's command experience points, which determines his Command Rank
|
||||
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value
|
||||
* @param health for `x / y` of hitpoints, this is the avatar's `x` value
|
||||
* @param armor for `x / y` of armor points, this is the avatar's `x` value
|
||||
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value
|
||||
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value
|
||||
* @param firstTimeEvents the list of first time events performed by this avatar
|
||||
* @param tutorials the list of tutorials completed by this avatar
|
||||
* @param inventory the avatar's inventory
|
||||
* @param drawn_slot the holster that is initially drawn
|
||||
* @return a `DetailedCharacterData` object
|
||||
*/
|
||||
def apply(appearance : CharacterAppearanceData, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
|
||||
new DetailedCharacterData(appearance, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, 28, 4, 44, 84, 104, 1900, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
|
||||
|
||||
/**
|
||||
* Overloaded constructor for `DetailedCharacterData` that allows for a not-optional inventory.
|
||||
* @param appearance data about the avatar's basic aesthetics
|
||||
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value
|
||||
* @param health for `x / y` of hitpoints, this is the avatar's `x` value
|
||||
* @param armor for `x / y` of armor points, this is the avatar's `x` value
|
||||
* @param unk1 na
|
||||
* @param unk2 na
|
||||
* @param unk3 na
|
||||
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value
|
||||
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value
|
||||
* @param unk4 na
|
||||
* @param unk5 na
|
||||
* @param unk6 na
|
||||
* @param unk7 na
|
||||
* @param unk8 na
|
||||
* @param unk9 na
|
||||
* @param certs the `List` of active certifications
|
||||
* @param implants the `List` of implant slots currently possessed by this avatar
|
||||
* @param firstTimeEvents the list of first time events performed by this avatar
|
||||
* @param tutorials the list of tutorials completed by this avatar
|
||||
* @param inventory the avatar's inventory
|
||||
* @param drawn_slot the holster that is initially drawn
|
||||
* @return a `DetailedCharacterData` object
|
||||
*/
|
||||
def apply(appearance : CharacterAppearanceData, healthMax : Int, health : Int, armor : Int, unk1 : Int, unk2 : Int, unk3 : Int, staminaMax : Int, stamina : Int, unk4 : Int, unk5 : Int, unk6 : Int, unk7 : Int, unk8 : Int, unk9 : Int, firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
|
||||
new DetailedCharacterData(appearance, healthMax, health, armor, unk1, unk2, unk3, staminaMax, stamina, unk4, unk5, unk6, unk7, unk8, unk9, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
|
||||
def apply(appearance : CharacterAppearanceData, bep : Long, cep : Long, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, certs : List[CertificationType.Value], implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
|
||||
new DetailedCharacterData(appearance, bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
|
||||
|
||||
/**
|
||||
* `Codec` for entries in the `List` of implants.
|
||||
*/
|
||||
private val implant_entry_codec : Codec[ImplantEntry] = (
|
||||
("implant" | ImplantType.codec) ::
|
||||
(bool >>:~ { guard =>
|
||||
newcodecs.binary_choice(guard, uintL(5), uintL(12)).hlist
|
||||
})
|
||||
).xmap[ImplantEntry] (
|
||||
{
|
||||
case implant :: true :: _ :: HNil =>
|
||||
ImplantEntry(implant, None)
|
||||
|
||||
case implant :: false :: extra :: HNil =>
|
||||
ImplantEntry(implant, Some(extra))
|
||||
},
|
||||
{
|
||||
case ImplantEntry(implant, None) =>
|
||||
implant :: true :: 0 :: HNil
|
||||
|
||||
case ImplantEntry(implant, Some(extra)) =>
|
||||
implant :: false :: extra :: HNil
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* A player's battle rank, determined by their battle experience points, determines how many implants to which they have access.
|
||||
* Starting with "no implants" at BR1, a player earns one at each of the three ranks: BR6, BR12, and BR18.
|
||||
* @param bep battle experience points
|
||||
* @return the number of accessible implant slots
|
||||
*/
|
||||
private def numberOfImplantSlots(bep : Long) : Int = {
|
||||
if(bep > 754370) { //BR18+
|
||||
3
|
||||
}
|
||||
else if(bep > 197753) { //BR12+
|
||||
2
|
||||
}
|
||||
else if(bep > 29999) { //BR6+
|
||||
1
|
||||
}
|
||||
else { //BR1+
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The padding value of the first entry in either of two byte-aligned `List` structures.
|
||||
* @param implants implant entries
|
||||
* @return the pad length in bits `0 <= n < 8`
|
||||
*/
|
||||
private def implantFieldPadding(implants : List[ImplantEntry], varBit : Option[Int] = None) : Int = {
|
||||
val base : Int = 5 //the offset with no implant entries
|
||||
val baseOffset : Int = base - varBit.getOrElse(0)
|
||||
val resultA = if(baseOffset < 0) { 8 - baseOffset } else { baseOffset % 8 }
|
||||
|
||||
var implantOffset : Int = 0
|
||||
implants.foreach({entry =>
|
||||
implantOffset += entry.bitsize.toInt
|
||||
})
|
||||
val resultB : Int = resultA - (implantOffset % 8)
|
||||
if(resultB < 0) { 8 - resultB } else { resultB }
|
||||
}
|
||||
|
||||
/**
|
||||
* Players with certain battle rank will always have a certain number of implant slots.
|
||||
* The encoding requires it.
|
||||
* Pad empty slots onto the end of a list of
|
||||
* @param size the required number of implant slots
|
||||
* @param list the `List` of implant slots
|
||||
* @return a fully-populated (or over-populated) `List` of implant slots
|
||||
* @see `ImplantEntry`
|
||||
*/
|
||||
@tailrec private def recursiveEnsureImplantSlots(size : Int, list : List[ImplantEntry] = Nil) : List[ImplantEntry] = {
|
||||
if(list.length >= size) {
|
||||
list
|
||||
}
|
||||
else {
|
||||
recursiveEnsureImplantSlots(size, list :+ ImplantEntry(ImplantType.None, None))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the padding of the first entry in the first time events list.
|
||||
* The padding will always be a number 0-7.
|
||||
* @param len the length of the list
|
||||
* @return the pad length in bits
|
||||
* @param len the length of the first time events list
|
||||
* @param implantPadding the padding that resulted from implant entries
|
||||
* @return the pad length in bits `0 <= n < 8`
|
||||
*/
|
||||
private def ftePadding(len : Long) : Int = {
|
||||
//TODO the parameters for this function are not correct
|
||||
private def ftePadding(len : Long, implantPadding : Int) : Int = {
|
||||
//TODO the proper padding length should reflect all variability in the stream prior to this point
|
||||
if(len > 0) {
|
||||
5
|
||||
implantPadding
|
||||
}
|
||||
else
|
||||
else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the padding of the first entry in the completed tutorials list.
|
||||
* The padding will always be a number 0-7.<br>
|
||||
* Get the padding of the first entry in the completed tutorials list.<br>
|
||||
* <br>
|
||||
* The tutorials list follows the first time event list and that contains byte-aligned strings too.
|
||||
* While there will be more to the padding, this other list is important.
|
||||
* Any elements in that list causes the automatic byte-alignment of this list's first entry.
|
||||
* @param len the length of the list
|
||||
* @return the pad length in bits
|
||||
* The tutorials list follows the first time event list and also contains byte-aligned strings.
|
||||
* If the both lists are populated or empty at the same time, the first entry will not need padding.
|
||||
* If the first time events list is unpopulated, but this list is populated, the first entry will need padding bits.
|
||||
* @param len the length of the first time events list
|
||||
* @param len2 the length of the tutorial list
|
||||
* @param implantPadding the padding that resulted from implant entries
|
||||
* @return the pad length in bits `n < 8`
|
||||
*/
|
||||
private def tutPadding(len : Long, len2 : Long) : Int = {
|
||||
if(len > 0) //automatic alignment from previous List
|
||||
0
|
||||
else if(len2 > 0) //need to align for elements
|
||||
5
|
||||
else //both lists are empty
|
||||
0
|
||||
private def tutPadding(len : Long, len2 : Long, implantPadding : Int) : Int = {
|
||||
if(len > 0) {
|
||||
0 //automatic alignment from previous List
|
||||
}
|
||||
else if(len2 > 0) {
|
||||
implantPadding //need to align for elements
|
||||
}
|
||||
else {
|
||||
0 //both lists are empty
|
||||
}
|
||||
}
|
||||
|
||||
implicit val codec : Codec[DetailedCharacterData] = (
|
||||
("appearance" | CharacterAppearanceData.codec) ::
|
||||
ignore(160) ::
|
||||
("healthMax" | uint16L) ::
|
||||
("health" | uint16L) ::
|
||||
ignore(1) ::
|
||||
("armor" | uint16L) ::
|
||||
ignore(9) ::
|
||||
("unk1" | uint8L) ::
|
||||
ignore(8) ::
|
||||
("unk2" | uint4L) ::
|
||||
("unk3" | uintL(3)) ::
|
||||
("staminaMax" | uint16L) ::
|
||||
("stamina" | uint16L) ::
|
||||
ignore(149) ::
|
||||
("unk4" | uint16L) ::
|
||||
("unk5" | uint8L) ::
|
||||
("unk6" | uint8L) ::
|
||||
("unk7" | uint8L) ::
|
||||
("unk8" | uint8L) ::
|
||||
("unk9" | uintL(12)) ::
|
||||
ignore(19) ::
|
||||
(("firstTimeEvent_length" | uint32L) >>:~ { len =>
|
||||
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) ::
|
||||
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
|
||||
(("tutorial_length" | uint32L) >>:~ { len2 =>
|
||||
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutPadding(len, len2) )) ::
|
||||
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
|
||||
ignore(207) ::
|
||||
optional(bool, "inventory" | InventoryData.codec_detailed) ::
|
||||
("drawn_slot" | DrawnSlot.codec) ::
|
||||
bool //usually false
|
||||
})
|
||||
})
|
||||
("appearance" | CharacterAppearanceData.codec) >>:~ { app =>
|
||||
("bep" | uint32L) >>:~ { bep =>
|
||||
("cep" | uint32L) ::
|
||||
ignore(96) ::
|
||||
("healthMax" | uint16L) ::
|
||||
("health" | uint16L) ::
|
||||
ignore(1) ::
|
||||
("armor" | uint16L) ::
|
||||
ignore(9) ::
|
||||
("unk1" | uint8L) ::
|
||||
ignore(8) ::
|
||||
("unk2" | uint4L) ::
|
||||
("unk3" | uintL(3)) ::
|
||||
("staminaMax" | uint16L) ::
|
||||
("stamina" | uint16L) ::
|
||||
ignore(147) ::
|
||||
("certs" | listOfN(uint8L, CertificationType.codec)) ::
|
||||
optional(bool, uint32L) :: //ask about sample CCRIDER
|
||||
ignore(4) ::
|
||||
(("implants" | PacketHelpers.listOfNSized(numberOfImplantSlots(bep), implant_entry_codec)) >>:~ { implants =>
|
||||
ignore(12) ::
|
||||
(("firstTimeEvent_length" | uint32L) >>:~ { len =>
|
||||
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned(ftePadding(len, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) ::
|
||||
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
|
||||
(("tutorial_length" | uint32L) >>:~ { len2 =>
|
||||
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) ::
|
||||
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
|
||||
ignore(207) ::
|
||||
optional(bool, "inventory" | InventoryData.codec_detailed) ::
|
||||
("drawn_slot" | DrawnSlot.codec) ::
|
||||
bool //usually false
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
).exmap[DetailedCharacterData] (
|
||||
{
|
||||
case app :: _ :: b :: c :: _ :: d :: _ :: e :: _ :: f :: g :: h :: i :: _ :: j :: k :: l :: m :: n :: o :: _ :: _ :: q :: r :: _ :: t :: u :: _ :: v :: w :: false :: HNil =>
|
||||
case app :: bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: inv :: drawn :: false :: HNil =>
|
||||
//prepend the displaced first elements to their lists
|
||||
val fteList : List[String] = if(q.isDefined) { q.get :: r } else r
|
||||
val tutList : List[String] = if(t.isDefined) { t.get :: u } else u
|
||||
Attempt.successful(DetailedCharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, v, w))
|
||||
val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else fte1
|
||||
val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else tut1
|
||||
Attempt.successful(DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, inv, drawn))
|
||||
},
|
||||
{
|
||||
case DetailedCharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, p, q) =>
|
||||
case DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, inv, drawn) =>
|
||||
val implantCapacity : Int = numberOfImplantSlots(bep)
|
||||
val implantList = if(implants.length > implantCapacity) {
|
||||
implants.slice(0, implantCapacity)
|
||||
}
|
||||
else {
|
||||
recursiveEnsureImplantSlots(implantCapacity, implants)
|
||||
}
|
||||
//shift the first elements off their lists
|
||||
var fteListCopy = fteList
|
||||
var firstEvent : Option[String] = None
|
||||
if(fteList.nonEmpty) {
|
||||
firstEvent = Some(fteList.head)
|
||||
fteListCopy = fteList.drop(1)
|
||||
val (firstEvent, fteListCopy) = fteList match {
|
||||
case (f : String) +: Nil => (Some(f), Nil)
|
||||
case ((f : String) +: (rest : List[String])) => (Some(f), rest)
|
||||
case Nil => (None, Nil)
|
||||
}
|
||||
var tutListCopy = tutList
|
||||
var firstTutorial : Option[String] = None
|
||||
if(tutList.nonEmpty) {
|
||||
firstTutorial = Some(tutList.head)
|
||||
tutListCopy = tutList.drop(1)
|
||||
val (firstTutorial, tutListCopy) = tutList match {
|
||||
case (f : String) +: Nil => (Some(f), Nil)
|
||||
case ((f : String) +: (rest : List[String])) => (Some(f), rest)
|
||||
case Nil => (None, Nil)
|
||||
}
|
||||
Attempt.successful(app :: () :: b :: c :: () :: d :: () :: e :: () :: f :: g :: h :: i :: () :: j :: k :: l :: m :: n :: o :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: p :: q :: false :: HNil)
|
||||
Attempt.successful(app :: bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: None :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: inv :: drawn :: false :: HNil)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.types
|
||||
|
||||
import net.psforever.packet.PacketHelpers
|
||||
import scodec.codecs._
|
||||
/**
|
||||
* An `Enumeration` of the available certifications.<br>
|
||||
* <br>
|
||||
* As indicated, the following certifications are always enqueued on an avatar's permissions:
|
||||
* `StandardAssault`, `StandardExoSuit`, `AgileExoSuit`.
|
||||
* They must still be included in any formal lists of permitted equipment for a user.
|
||||
* The other noted certifications require all prerequisite certifications listed or they themselves will not be listed:
|
||||
* `ElectronicsExpert` and `AdvancedEngineering`.
|
||||
* No other certification requires its prerequisites explicitly listed to be listed itself.
|
||||
* Any certification that contains multiple other certifications overrides those individual certifications in the list.
|
||||
* There is no certification for the Advanced Nanite Transport.<br>
|
||||
* <br>
|
||||
* In terms of pricing, `StandardAssault`, `StandardExoSuit`, and `AgileExoSuit` are costless.
|
||||
* A certification that contains multiple other certifications acts as the overriding cost.
|
||||
* (Taking `UniMAX` while owning `AAMAX` will refund the `AAMAX` cost and replace it with the `UniMAX` cost.)
|
||||
*/
|
||||
object CertificationType extends Enumeration {
|
||||
type Type = Value
|
||||
val
|
||||
//0
|
||||
StandardAssault, //always listed
|
||||
MediumAssault,
|
||||
HeavyAssault,
|
||||
SpecialAssault,
|
||||
AntiVehicular,
|
||||
Sniping,
|
||||
EliteAssault,
|
||||
AirCalvaryScout,
|
||||
AirCalvaryInterceptor,
|
||||
AirCalvaryAssault,
|
||||
//10
|
||||
AirSupport,
|
||||
ATV,
|
||||
LightScout,
|
||||
AssaultBuggy,
|
||||
ArmoredAssault1,
|
||||
ArmoredAssault2,
|
||||
GroundTransport,
|
||||
GroundSupport,
|
||||
BattleFrameRobotics,
|
||||
Flail,
|
||||
//20
|
||||
Switchblade,
|
||||
Harasser,
|
||||
Phantasm,
|
||||
GalaxyGunship,
|
||||
BFRAntiAircraft,
|
||||
BFRAntiInfantry,
|
||||
StandardExoSuit, //always listed
|
||||
AgileExoSuit, //always listed
|
||||
ReinforcedExoSuit,
|
||||
InfiltrationSuit,
|
||||
//30
|
||||
AAMAX,
|
||||
AIMAX,
|
||||
AVMAX,
|
||||
UniMAX,
|
||||
Medical,
|
||||
AdvancedMedical,
|
||||
Hacking,
|
||||
AdvancedHacking,
|
||||
ExpertHacking,
|
||||
DataCorruption,
|
||||
//40
|
||||
ElectronicsExpert, //requires Hacking and AdvancedHacking
|
||||
Engineering,
|
||||
CombatEngineering,
|
||||
FortificationEngineering,
|
||||
AssaultEngineering,
|
||||
AdvancedEngineering //requires Engineering and CombatEngineering
|
||||
= Value
|
||||
|
||||
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
|
||||
}
|
||||
|
|
@ -9,16 +9,16 @@ import scodec.codecs._
|
|||
* <br>
|
||||
* Implant:<br>
|
||||
* `
|
||||
* 00 - Regeneration (advanced_regen)<br>
|
||||
* 01 - Enhanced Targeting (targeting)<br>
|
||||
* 02 - Audio Amplifier (audio_amplifier)<br>
|
||||
* 03 - Darklight Vision (darklight_vision)<br>
|
||||
* 04 - Melee Booster (melee_booster)<br>
|
||||
* 05 - Personal Shield (personal_shield)<br>
|
||||
* 06 - Range Magnifier (range_magnifier)<br>
|
||||
* 07 - Second Wind `(na)`<br>
|
||||
* 08 - Sensor Shield (silent_run)<br>
|
||||
* 09 - Surge (surge)<br>
|
||||
* 0 - Regeneration (advanced_regen)<br>
|
||||
* 1 - Enhanced Targeting (targeting)<br>
|
||||
* 2 - Audio Amplifier (audio_amplifier)<br>
|
||||
* 3 - Darklight Vision (darklight_vision)<br>
|
||||
* 4 - Melee Booster (melee_booster)<br>
|
||||
* 5 - Personal Shield (personal_shield)<br>
|
||||
* 6 - Range Magnifier (range_magnifier)<br>
|
||||
* 7 - Second Wind `(na)`<br>
|
||||
* 8 - Sensor Shield (silent_run)<br>
|
||||
* 9 - Surge (surge)
|
||||
* `
|
||||
*/
|
||||
object ImplantType extends Enumeration {
|
||||
|
|
@ -34,5 +34,7 @@ object ImplantType extends Enumeration {
|
|||
SilentRun,
|
||||
Surge = Value
|
||||
|
||||
val None = Value(15) //TODO unconfirmed
|
||||
|
||||
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class DisplayedAwardMessageTest extends Specification {
|
|||
PacketCoding.DecodePacket(string).require match {
|
||||
case DisplayedAwardMessage(player_guid, ribbon, bar) =>
|
||||
player_guid mustEqual PlanetSideGUID(1695)
|
||||
ribbon mustEqual MeritCommendation.TwoYearTR
|
||||
ribbon mustEqual MeritCommendation.TwoYearVS
|
||||
bar mustEqual RibbonBarsSlot.TermOfService
|
||||
case _ =>
|
||||
ko
|
||||
|
|
@ -22,7 +22,7 @@ class DisplayedAwardMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"encode" in {
|
||||
val msg = DisplayedAwardMessage(PlanetSideGUID(1695), MeritCommendation.TwoYearTR, RibbonBarsSlot.TermOfService)
|
||||
val msg = DisplayedAwardMessage(PlanetSideGUID(1695), MeritCommendation.TwoYearVS, RibbonBarsSlot.TermOfService)
|
||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
|
|
|
|||
|
|
@ -206,6 +206,8 @@ class ObjectCreateDetailedMessageTest extends Specification {
|
|||
char.appearance.ribbons.middle mustEqual MeritCommendation.None
|
||||
char.appearance.ribbons.lower mustEqual MeritCommendation.None
|
||||
char.appearance.ribbons.tos mustEqual MeritCommendation.None
|
||||
char.bep mustEqual 0
|
||||
char.cep mustEqual 0
|
||||
char.healthMax mustEqual 100
|
||||
char.health mustEqual 100
|
||||
char.armor mustEqual 50 //standard exosuit value
|
||||
|
|
@ -214,12 +216,15 @@ class ObjectCreateDetailedMessageTest extends Specification {
|
|||
char.unk3 mustEqual 7
|
||||
char.staminaMax mustEqual 100
|
||||
char.stamina mustEqual 100
|
||||
char.unk4 mustEqual 28
|
||||
char.unk5 mustEqual 4
|
||||
char.unk6 mustEqual 44
|
||||
char.unk7 mustEqual 84
|
||||
char.unk8 mustEqual 104
|
||||
char.unk9 mustEqual 1900
|
||||
char.certs.length mustEqual 7
|
||||
char.certs.head mustEqual CertificationType.StandardAssault
|
||||
char.certs(1) mustEqual CertificationType.MediumAssault
|
||||
char.certs(2) mustEqual CertificationType.ATV
|
||||
char.certs(3) mustEqual CertificationType.Harasser
|
||||
char.certs(4) mustEqual CertificationType.StandardExoSuit
|
||||
char.certs(5) mustEqual CertificationType.AgileExoSuit
|
||||
char.certs(6) mustEqual CertificationType.ReinforcedExoSuit
|
||||
char.implants.length mustEqual 0
|
||||
char.firstTimeEvents.size mustEqual 4
|
||||
char.firstTimeEvents.head mustEqual "xpe_sanctuary_help"
|
||||
char.firstTimeEvents(1) mustEqual "xpe_th_firemodes"
|
||||
|
|
@ -405,14 +410,25 @@ class ObjectCreateDetailedMessageTest extends Specification {
|
|||
Nil
|
||||
val obj = DetailedCharacterData(
|
||||
app,
|
||||
0,
|
||||
0,
|
||||
100, 100,
|
||||
50,
|
||||
1, 7, 7,
|
||||
100, 100,
|
||||
28, 4, 44, 84, 104, 1900,
|
||||
List(
|
||||
CertificationType.StandardAssault,
|
||||
CertificationType.MediumAssault,
|
||||
CertificationType.ATV,
|
||||
CertificationType.Harasser,
|
||||
CertificationType.StandardExoSuit,
|
||||
CertificationType.AgileExoSuit,
|
||||
CertificationType.ReinforcedExoSuit
|
||||
),
|
||||
List(),
|
||||
"xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil,
|
||||
List.empty,
|
||||
InventoryData(inv),
|
||||
Some(InventoryData(inv)),
|
||||
DrawnSlot.Pistol1
|
||||
)
|
||||
val msg = ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj)
|
||||
|
|
|
|||
|
|
@ -691,10 +691,10 @@ class ObjectCreateMessageTest extends Specification {
|
|||
pc.appearance.is_cloaking mustEqual false
|
||||
pc.appearance.charging_pose mustEqual false
|
||||
pc.appearance.on_zipline mustEqual false
|
||||
pc.appearance.ribbons.upper mustEqual MeritCommendation.Loser4
|
||||
pc.appearance.ribbons.middle mustEqual MeritCommendation.HeavyInfantry3
|
||||
pc.appearance.ribbons.lower mustEqual MeritCommendation.TankBuster6
|
||||
pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearNC
|
||||
pc.appearance.ribbons.upper mustEqual MeritCommendation.MarkovVeteran
|
||||
pc.appearance.ribbons.middle mustEqual MeritCommendation.HeavyInfantry4
|
||||
pc.appearance.ribbons.lower mustEqual MeritCommendation.TankBuster7
|
||||
pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearTR
|
||||
pc.health mustEqual 255
|
||||
pc.armor mustEqual 253
|
||||
pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade
|
||||
|
|
@ -787,10 +787,10 @@ class ObjectCreateMessageTest extends Specification {
|
|||
pc.appearance.is_cloaking mustEqual false
|
||||
pc.appearance.charging_pose mustEqual false
|
||||
pc.appearance.on_zipline mustEqual false
|
||||
pc.appearance.ribbons.upper mustEqual MeritCommendation.Jacking
|
||||
pc.appearance.ribbons.middle mustEqual MeritCommendation.ScavengerTR6
|
||||
pc.appearance.ribbons.upper mustEqual MeritCommendation.Jacking2
|
||||
pc.appearance.ribbons.middle mustEqual MeritCommendation.ScavengerVS1
|
||||
pc.appearance.ribbons.lower mustEqual MeritCommendation.AMSSupport4
|
||||
pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearTR
|
||||
pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearVS
|
||||
pc.health mustEqual 0
|
||||
pc.armor mustEqual 0
|
||||
pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade
|
||||
|
|
@ -1115,10 +1115,10 @@ class ObjectCreateMessageTest extends Specification {
|
|||
GrenadeState.None,
|
||||
false, false, false,
|
||||
RibbonBars(
|
||||
MeritCommendation.Loser4,
|
||||
MeritCommendation.HeavyInfantry3,
|
||||
MeritCommendation.TankBuster6,
|
||||
MeritCommendation.SixYearNC
|
||||
MeritCommendation.MarkovVeteran,
|
||||
MeritCommendation.HeavyInfantry4,
|
||||
MeritCommendation.TankBuster7,
|
||||
MeritCommendation.SixYearTR
|
||||
)
|
||||
),
|
||||
255, 253,
|
||||
|
|
@ -1172,10 +1172,10 @@ class ObjectCreateMessageTest extends Specification {
|
|||
GrenadeState.None,
|
||||
false, false, false,
|
||||
RibbonBars(
|
||||
MeritCommendation.Jacking,
|
||||
MeritCommendation.ScavengerTR6,
|
||||
MeritCommendation.Jacking2,
|
||||
MeritCommendation.ScavengerVS1,
|
||||
MeritCommendation.AMSSupport4,
|
||||
MeritCommendation.SixYearTR
|
||||
MeritCommendation.SixYearVS
|
||||
)
|
||||
),
|
||||
0, 0,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package objects
|
||||
|
||||
import net.psforever.objects.Implant
|
||||
import net.psforever.objects.ImplantSlot
|
||||
import net.psforever.objects.definition.{ImplantDefinition, Stance}
|
||||
import net.psforever.types.{ExoSuitType, ImplantType}
|
||||
import org.specs2.mutable._
|
||||
|
|
@ -16,61 +16,122 @@ class ImplantTest extends Specification {
|
|||
sample.DurationChargeByExoSuit += ExoSuitType.Standard -> 1
|
||||
sample.DurationChargeByStance += Stance.Running -> 1
|
||||
|
||||
"define" in {
|
||||
sample.Initialization mustEqual 90000
|
||||
sample.ActivationCharge mustEqual 3
|
||||
sample.DurationChargeBase mustEqual 1
|
||||
sample.DurationChargeByExoSuit(ExoSuitType.Agile) mustEqual 2
|
||||
sample.DurationChargeByExoSuit(ExoSuitType.Reinforced) mustEqual 2
|
||||
sample.DurationChargeByExoSuit(ExoSuitType.Standard) mustEqual 1
|
||||
sample.DurationChargeByExoSuit(ExoSuitType.Infiltration) mustEqual 0 //default value
|
||||
sample.DurationChargeByStance(Stance.Running) mustEqual 1
|
||||
sample.DurationChargeByStance(Stance.Crouching) mustEqual 0 //default value
|
||||
sample.Type mustEqual ImplantType.SilentRun
|
||||
"ImplantDefinition" should {
|
||||
"define" in {
|
||||
sample.Initialization mustEqual 90000
|
||||
sample.ActivationCharge mustEqual 3
|
||||
sample.DurationChargeBase mustEqual 1
|
||||
sample.DurationChargeByExoSuit(ExoSuitType.Agile) mustEqual 2
|
||||
sample.DurationChargeByExoSuit(ExoSuitType.Reinforced) mustEqual 2
|
||||
sample.DurationChargeByExoSuit(ExoSuitType.Standard) mustEqual 1
|
||||
sample.DurationChargeByExoSuit(ExoSuitType.Infiltration) mustEqual 0 //default value
|
||||
sample.DurationChargeByStance(Stance.Running) mustEqual 1
|
||||
sample.DurationChargeByStance(Stance.Crouching) mustEqual 0 //default value
|
||||
sample.Type mustEqual ImplantType.SilentRun
|
||||
}
|
||||
}
|
||||
|
||||
"construct" in {
|
||||
val obj = new Implant(sample)
|
||||
obj.Definition.Type mustEqual sample.Type
|
||||
obj.Active mustEqual false
|
||||
obj.Ready mustEqual false
|
||||
obj.Timer mustEqual 0
|
||||
}
|
||||
"ImplantSlot" should {
|
||||
"construct" in {
|
||||
val obj = new ImplantSlot
|
||||
obj.Unlocked mustEqual false
|
||||
obj.Initialized mustEqual false
|
||||
obj.Active mustEqual false
|
||||
obj.Implant mustEqual ImplantType.None
|
||||
obj.Installed mustEqual None
|
||||
}
|
||||
|
||||
"reset/init their timer" in {
|
||||
val obj = new Implant(sample)
|
||||
obj.Timer mustEqual 0
|
||||
obj.Reset()
|
||||
obj.Timer mustEqual 90000
|
||||
}
|
||||
"load an implant when locked" in {
|
||||
val obj = new ImplantSlot
|
||||
obj.Unlocked mustEqual false
|
||||
obj.Implant mustEqual ImplantType.None
|
||||
|
||||
"reset/init their readiness condition" in {
|
||||
val obj = new Implant(sample)
|
||||
obj.Ready mustEqual false
|
||||
obj.Timer = 0
|
||||
obj.Ready mustEqual true
|
||||
obj.Reset()
|
||||
obj.Ready mustEqual false
|
||||
}
|
||||
obj.Implant = sample
|
||||
obj.Implant mustEqual ImplantType.None
|
||||
}
|
||||
|
||||
"not activate until they are ready" in {
|
||||
val obj = new Implant(sample)
|
||||
obj.Active = true
|
||||
obj.Active mustEqual false
|
||||
obj.Timer = 0
|
||||
obj.Active = true
|
||||
obj.Active mustEqual true
|
||||
}
|
||||
"load an implant when unlocked" in {
|
||||
val obj = new ImplantSlot
|
||||
obj.Unlocked mustEqual false
|
||||
obj.Implant mustEqual ImplantType.None
|
||||
sample.Type mustEqual ImplantType.SilentRun
|
||||
|
||||
"not cost energy while not active" in {
|
||||
val obj = new Implant(sample)
|
||||
obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 0
|
||||
}
|
||||
obj.Unlocked = true
|
||||
obj.Implant = sample
|
||||
obj.Implant mustEqual ImplantType.SilentRun
|
||||
}
|
||||
|
||||
"cost energy while active" in {
|
||||
val obj = new Implant(sample)
|
||||
obj.Timer = 0
|
||||
obj.Active = true
|
||||
obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 4
|
||||
"can not re-lock an unlocked implant slot" in {
|
||||
val obj = new ImplantSlot
|
||||
obj.Unlocked mustEqual false
|
||||
|
||||
obj.Unlocked = false
|
||||
obj.Unlocked mustEqual false
|
||||
obj.Unlocked = true
|
||||
obj.Unlocked mustEqual true
|
||||
obj.Unlocked = false
|
||||
obj.Unlocked mustEqual true
|
||||
}
|
||||
|
||||
"initialize without an implant" in {
|
||||
val obj = new ImplantSlot
|
||||
obj.Initialized mustEqual false
|
||||
obj.Initialized = true
|
||||
obj.Initialized mustEqual false
|
||||
}
|
||||
|
||||
"initialize an implant" in {
|
||||
val obj = new ImplantSlot
|
||||
obj.Initialized mustEqual false
|
||||
|
||||
obj.Unlocked = true
|
||||
obj.Implant = sample
|
||||
obj.Initialized = true
|
||||
obj.Initialized mustEqual true
|
||||
}
|
||||
|
||||
"activate an uninitialized implant" in {
|
||||
val obj = new ImplantSlot
|
||||
obj.Unlocked = true
|
||||
obj.Implant = sample
|
||||
obj.Initialized mustEqual false
|
||||
obj.Active mustEqual false
|
||||
|
||||
obj.Active = true
|
||||
obj.Active mustEqual false
|
||||
}
|
||||
|
||||
"activate an initialized implant" in {
|
||||
val obj = new ImplantSlot
|
||||
obj.Unlocked = true
|
||||
obj.Implant = sample
|
||||
obj.Initialized mustEqual false
|
||||
obj.Active mustEqual false
|
||||
|
||||
obj.Initialized = true
|
||||
obj.Active = true
|
||||
obj.Active mustEqual true
|
||||
}
|
||||
|
||||
"not cost energy while not active" in {
|
||||
val obj = new ImplantSlot
|
||||
obj.Unlocked = true
|
||||
obj.Implant = sample
|
||||
obj.Initialized = true
|
||||
obj.Active mustEqual false
|
||||
obj.ActivationCharge mustEqual 0
|
||||
obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 0
|
||||
}
|
||||
|
||||
"cost energy while active" in {
|
||||
val obj = new ImplantSlot
|
||||
obj.Unlocked = true
|
||||
obj.Implant = sample
|
||||
obj.Initialized = true
|
||||
obj.Active = true
|
||||
obj.Active mustEqual true
|
||||
obj.ActivationCharge mustEqual 3
|
||||
obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package objects
|
||||
|
||||
import net.psforever.objects.{Implant, Player, SimpleItem}
|
||||
import net.psforever.objects.{Player, SimpleItem}
|
||||
import net.psforever.objects.definition.{ImplantDefinition, SimpleItemDefinition}
|
||||
import net.psforever.objects.equipment.EquipmentSize
|
||||
import net.psforever.types.{CharacterGender, ExoSuitType, ImplantType, PlanetSideEmpire}
|
||||
|
|
@ -106,31 +106,39 @@ class PlayerTest extends Specification {
|
|||
obj.LastDrawnSlot mustEqual 1
|
||||
}
|
||||
|
||||
"install no implants until a slot is unlocked" in {
|
||||
val testplant : Implant = Implant(ImplantDefinition(1))
|
||||
"install an implant" in {
|
||||
val testplant : ImplantDefinition = ImplantDefinition(1)
|
||||
val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
|
||||
obj.Implants(0).Unlocked mustEqual false
|
||||
obj.Implant(0) mustEqual None
|
||||
obj.InstallImplant(testplant)
|
||||
obj.Implant(0) mustEqual None
|
||||
obj.Implant(ImplantType(1)) mustEqual None
|
||||
|
||||
obj.Implants(0).Unlocked = true
|
||||
obj.InstallImplant(testplant)
|
||||
obj.Implant(0) mustEqual Some(testplant.Definition.Type)
|
||||
obj.Implant(ImplantType(1)) mustEqual Some(testplant)
|
||||
obj.InstallImplant(testplant) mustEqual true
|
||||
obj.Implants.find({p => p.Implant == ImplantType(1)}) match { //find the installed implant
|
||||
case Some(slot) =>
|
||||
slot.Installed mustEqual Some(testplant)
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
ok
|
||||
}
|
||||
|
||||
"can not install the same type of implant twice" in {
|
||||
val testplant1 : ImplantDefinition = ImplantDefinition(1)
|
||||
val testplant2 : ImplantDefinition = ImplantDefinition(1)
|
||||
val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
|
||||
obj.Implants(0).Unlocked = true
|
||||
obj.Implants(1).Unlocked = true
|
||||
obj.InstallImplant(testplant1) mustEqual true
|
||||
obj.InstallImplant(testplant2) mustEqual false
|
||||
}
|
||||
|
||||
"uninstall implants" in {
|
||||
val testplant : Implant = Implant(ImplantDefinition(1))
|
||||
val testplant : ImplantDefinition = ImplantDefinition(1)
|
||||
val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
|
||||
obj.Implants(0).Unlocked = true
|
||||
obj.InstallImplant(testplant)
|
||||
obj.Implant(ImplantType(1)) mustEqual Some(testplant)
|
||||
obj.InstallImplant(testplant) mustEqual true
|
||||
obj.Implants(0).Installed mustEqual Some(testplant)
|
||||
|
||||
obj.UninstallImplant(ImplantType(1))
|
||||
obj.Implant(0) mustEqual None
|
||||
obj.Implant(ImplantType(1)) mustEqual None
|
||||
obj.UninstallImplant(testplant.Type)
|
||||
obj.Implants(0).Installed mustEqual None
|
||||
}
|
||||
|
||||
"administrate" in {
|
||||
|
|
|
|||
|
|
@ -601,6 +601,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
player = Player("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1)
|
||||
player.Position = Vector3(3674.8438f, 2726.789f, 91.15625f)
|
||||
player.Orientation = Vector3(0f, 0f, 90f)
|
||||
player.Certifications += CertificationType.StandardAssault
|
||||
player.Certifications += CertificationType.MediumAssault
|
||||
player.Certifications += CertificationType.StandardExoSuit
|
||||
player.Certifications += CertificationType.AgileExoSuit
|
||||
player.Certifications += CertificationType.ReinforcedExoSuit
|
||||
player.Certifications += CertificationType.ATV
|
||||
player.Certifications += CertificationType.Harasser
|
||||
player.Slot(0).Equipment = beamer1
|
||||
player.Slot(2).Equipment = suppressor1
|
||||
player.Slot(4).Equipment = forceblade1
|
||||
|
|
@ -902,6 +909,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
|
|||
case msg @ ChangeAmmoMessage(item_guid, unk1) =>
|
||||
log.info("ChangeAmmo: " + msg)
|
||||
|
||||
case msg @ AvatarImplantMessage(_, _, _, _) => //(player_guid, unk1, unk2, implant) =>
|
||||
log.info("AvatarImplantMessage: " + msg)
|
||||
|
||||
case msg @ UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType) =>
|
||||
log.info("UseItem: " + msg)
|
||||
// TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok)
|
||||
|
|
|
|||
Loading…
Reference in a new issue