simplified the implant management process by removing an unnecessary middleman object; corrected tests that included implants; worked implants into player PacketConverter

This commit is contained in:
FateJH 2017-09-07 23:31:25 -04:00
parent 5962227ad2
commit 5b4ba246e1
9 changed files with 335 additions and 223 deletions

View file

@ -270,6 +270,42 @@ 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) {

View file

@ -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)
}
}

View file

@ -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.None))
def apply() : ImplantSlot = {
new ImplantSlot()
}

View file

@ -1,7 +1,7 @@
// 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
@ -334,26 +334,22 @@ class Player(private val name : String,
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
}
}
@ -364,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) {
@ -377,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
@ -388,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 => ;
}
})
@ -590,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

View file

@ -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
}
/**

View file

@ -1,9 +1,9 @@
// 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.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle}
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
@ -18,9 +18,9 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
obj.Armor / obj.MaxArmor * 255, //TODO not precise
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)
)
)
@ -132,14 +132,14 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
* @see `ImplantEntry` in `DetailedCharacterData`
*/
private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = {
obj.Implants.map(impl => {
impl.Installed match {
case Some(module) =>
if(impl.Implant.get.Ready) {
ImplantEntry(module, None)
obj.Implants.map(slot => {
slot.Installed match {
case Some(_) =>
if(slot.Initialized) {
ImplantEntry(slot.Implant, None)
}
else {
ImplantEntry(module, Some(impl.Implant.get.Timer.toInt))
ImplantEntry(slot.Implant, Some(slot.Installed.get.Initialization.toInt))
}
case None =>
ImplantEntry(ImplantType.None, None)
@ -147,6 +147,35 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
}).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.
@ -211,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

View file

@ -14,7 +14,8 @@ 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
* @param activation the activation timer;
* technically, this is "unconfirmed"
* @see `ImplantType`
*/
final case class ImplantEntry(implant : ImplantType.Value,

View file

@ -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
}
}
}

View file

@ -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 {