Forget How to Wear Clothes (#806)

* in forgetting an exo-suit certification, one must not be wearing that type of exo-suit afterwards

* making the conditional more straightforward

* fixed issue with purchase times; can now share max purchase cooldowns
This commit is contained in:
Fate-JH 2021-05-06 07:31:40 -04:00 committed by GitHub
parent cbb48d1442
commit 262b7d2ec6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 208 additions and 191 deletions

View file

@ -82,6 +82,9 @@ game {
# Modify the amount of NTU drain per autorepair tick for facility amenities
amenity-autorepair-drain-rate = 0.5
# Purchases timers for the mechanized assault exo-suits all update at the same time when any of them would update
shared-max-cooldown = no
# HART system, shuttles and facilities
hart {
# How long the shuttle is not boarding passengers (going through the motions)

View file

@ -5,57 +5,16 @@ import java.util.concurrent.atomic.AtomicInteger
import akka.actor.Cancellable
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import net.psforever.objects.avatar.{Avatar, BattleRank, Certification, Cosmetic, Implant}
import net.psforever.objects.avatar._
import net.psforever.objects.definition.converter.CharacterSelectConverter
import net.psforever.objects.definition.{
AmmoBoxDefinition,
BasicDefinition,
ConstructionItemDefinition,
ImplantDefinition,
KitDefinition,
SimpleItemDefinition,
ToolDefinition
}
import net.psforever.objects.definition._
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.loadouts.{InfantryLoadout, Loadout}
import net.psforever.objects.{
Account,
AmmoBox,
ConstructionItem,
GlobalDefinitions,
Kit,
Player,
Session,
SimpleItem,
Tool
}
import net.psforever.objects._
import net.psforever.packet.game.objectcreate.ObjectClass
import net.psforever.packet.game.{
ActionProgressMessage,
ActionResultMessage,
AvatarImplantMessage,
AvatarVehicleTimerMessage,
BattleExperienceMessage,
CharacterInfoMessage,
CreateShortcutMessage,
FavoritesMessage,
ImplantAction,
ItemTransactionResultMessage,
ObjectCreateDetailedMessage,
PlanetSideZoneID,
PlanetsideAttributeMessage
}
import net.psforever.types.{
CharacterSex,
CharacterVoice,
ExoSuitType,
ImplantType,
LoadoutType,
PlanetSideEmpire,
PlanetSideGUID,
TransactionType
}
import net.psforever.packet.game._
import net.psforever.types._
import net.psforever.util.Database._
import net.psforever.persistence
import net.psforever.util.{Config, DefinitionUtil}
@ -269,8 +228,8 @@ class AvatarActor(
def postStartBehaviour(): Behavior[Command] = {
account match {
case Some(account) =>
buffer.unstashAll(active(account))
case Some(_account) =>
buffer.unstashAll(active(_account))
case _ =>
Behaviors.same
}
@ -328,7 +287,7 @@ class AvatarActor(
result.onComplete {
case Success(_) =>
log.debug(s"AvatarActor: created character ${name} for account ${account.name}")
log.debug(s"AvatarActor: created character $name for account ${account.name}")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass)
sendAvatars(account)
case Failure(e) => log.error(e)("db failure")
@ -445,8 +404,8 @@ class AvatarActor(
sessionActor ! SessionActor.SendResponse(
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = false)
)
case Success(replace) =>
replace.foreach { cert =>
case Success(_replace) =>
_replace.foreach { cert =>
sessionActor ! SessionActor.SendResponse(
PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value)
)
@ -519,15 +478,31 @@ class AvatarActor(
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = false)
)
case Success(certs) =>
val player = session.get.player
context.self ! ReplaceAvatar(avatar.copy(certifications = avatar.certifications.diff(certs)))
certs.foreach { cert =>
sessionActor ! SessionActor.SendResponse(
PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value)
PlanetsideAttributeMessage(player.GUID, 25, cert.value)
)
}
sessionActor ! SessionActor.SendResponse(
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
)
//wearing invalid armor?
if (
if (certification == Certification.ReinforcedExoSuit) player.ExoSuit == ExoSuitType.Reinforced
else if (certification == Certification.InfiltrationSuit) player.ExoSuit == ExoSuitType.Infiltration
else if (player.ExoSuit == ExoSuitType.MAX) {
lazy val subtype = InfantryLoadout.DetermineSubtypeA(ExoSuitType.MAX, player.Slot(slot = 0).Equipment)
if (certification == Certification.UniMAX) true
else if (certification == Certification.AAMAX) subtype == 1
else if (certification == Certification.AIMAX) subtype == 2
else if (certification == Certification.AVMAX) subtype == 3
else false
} else false
) {
player.Actor ! PlayerControl.SetExoSuit(ExoSuitType.Standard, 0)
}
}
}
Behaviors.same
@ -591,24 +566,24 @@ class AvatarActor(
case LearnImplant(terminalGuid, definition) =>
// TODO there used to be a terminal check here, do we really need it?
val index = avatar.implants.zipWithIndex.collectFirst {
case (Some(implant), index) if implant.definition.implantType == definition.implantType => index
case (None, index) if index < avatar.br.implantSlots => index
case (Some(implant), _index) if implant.definition.implantType == definition.implantType => _index
case (None, _index) if _index < avatar.br.implantSlots => _index
}
index match {
case Some(index) =>
case Some(_index) =>
import ctx._
ctx
.run(query[persistence.Implant].insert(_.name -> lift(definition.Name), _.avatarId -> lift(avatar.id)))
.onComplete {
case Success(_) =>
context.self ! ReplaceAvatar(
avatar.copy(implants = avatar.implants.updated(index, Some(Implant(definition))))
avatar.copy(implants = avatar.implants.updated(_index, Some(Implant(definition))))
)
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(
session.get.player.GUID,
ImplantAction.Add,
index,
_index,
definition.implantType.value
)
)
@ -630,10 +605,10 @@ class AvatarActor(
case SellImplant(terminalGuid, definition) =>
// TODO there used to be a terminal check here, do we really need it?
val index = avatar.implants.zipWithIndex.collectFirst {
case (Some(implant), index) if implant.definition.implantType == definition.implantType => index
case (Some(implant), _index) if implant.definition.implantType == definition.implantType => _index
}
index match {
case Some(index) =>
case Some(_index) =>
import ctx._
ctx
.run(
@ -644,9 +619,9 @@ class AvatarActor(
)
.onComplete {
case Success(_) =>
context.self ! ReplaceAvatar(avatar.copy(implants = avatar.implants.updated(index, None)))
context.self ! ReplaceAvatar(avatar.copy(implants = avatar.implants.updated(_index, None)))
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, index, 0)
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, _index, 0)
)
sessionActor ! SessionActor.SendResponse(
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
@ -721,15 +696,19 @@ class AvatarActor(
Behaviors.same
case UpdatePurchaseTime(definition, time) =>
//only send for items with cooldowns
Avatar.purchaseCooldowns.get(definition) match {
case Some(cooldown) =>
// TODO save to db
avatar = avatar.copy(purchaseTimes = avatar.purchaseTimes.updated(definition.Name, time))
updatePurchaseTimer(definition.Name, cooldown.toSeconds, unk1 = true)
case None => ;
//log.warn(s"UpdatePurchaseTime message for item '${definition.Name}' without cooldown")
// TODO save to db
var newTimes = avatar.purchaseTimes
resolveSharedPurchaseTimeNames(resolvePurchaseTimeName(avatar.faction, definition)).foreach {
case (item, name) =>
Avatar.purchaseCooldowns.get(item) match {
case Some(cooldown) =>
//only send for items with cooldowns
newTimes = newTimes.updated(item.Name, time)
updatePurchaseTimer(name, cooldown.toSeconds, unk1 = true)
case _ => ;
}
}
avatar = avatar.copy(purchaseTimes = newTimes)
Behaviors.same
case UpdateUseTime(definition, time) =>
@ -858,7 +837,7 @@ class AvatarActor(
Behaviors.same
case ConsumeStamina(stamina) =>
assert(stamina > 0, s"consumed stamina must be larger than 0, but is: ${stamina}")
assert(stamina > 0, s"consumed stamina must be larger than 0, but is: $stamina")
consumeStamina(stamina)
Behaviors.same
@ -1260,7 +1239,7 @@ class AvatarActor(
doll.ExoSuit = ExoSuitType(loadout.exosuitId)
loadout.items.split("/").foreach {
case value =>
value =>
val (objectType, objectIndex, objectId, toolAmmo) = value.split(",") match {
case Array(a, b: String, c: String) => (a, b.toInt, c.toInt, None)
case Array(a, b: String, c: String, d) => (a, b.toInt, c.toInt, Some(d))
@ -1318,9 +1297,9 @@ class AvatarActor(
def defaultStaminaRegen(): Cancellable = {
context.system.scheduler.scheduleWithFixedDelay(0.5 seconds, 0.5 seconds)(() => {
(session, _avatar) match {
case (Some(session), Some(_)) =>
case (Some(_session), Some(_)) =>
if (
!avatar.staminaFull && (session.player.VehicleSeated.nonEmpty || !session.player.isMoving && !session.player.Jumping)
!avatar.staminaFull && (_session.player.VehicleSeated.nonEmpty || !_session.player.isMoving && !_session.player.Jumping)
) {
context.self ! RestoreStamina(1)
}
@ -1340,6 +1319,47 @@ class AvatarActor(
})
}
def resolvePurchaseTimeName(faction: PlanetSideEmpire.Value, item: BasicDefinition): (BasicDefinition, String) = {
val factionName : String = faction.toString.toLowerCase
val name = item match {
case GlobalDefinitions.trhev_dualcycler |
GlobalDefinitions.nchev_scattercannon |
GlobalDefinitions.vshev_quasar =>
s"${factionName}hev_antipersonnel"
case GlobalDefinitions.trhev_pounder |
GlobalDefinitions.nchev_falcon |
GlobalDefinitions.vshev_comet =>
s"${factionName}hev_antivehicular"
case GlobalDefinitions.trhev_burster |
GlobalDefinitions.nchev_sparrow |
GlobalDefinitions.vshev_starfire =>
s"${factionName}hev_antiaircraft"
case _ =>
item.Name
}
(item, name)
}
def resolveSharedPurchaseTimeNames(pair: (BasicDefinition, String)): Seq[(BasicDefinition, String)] = {
val (_, name) = pair
if (name.matches("(tr|nc|vs)hev_.+") && Config.app.game.sharedMaxCooldown) {
val faction = name.take(2)
(if (faction.equals("nc")) {
Seq(GlobalDefinitions.nchev_scattercannon, GlobalDefinitions.nchev_falcon, GlobalDefinitions.nchev_sparrow)
}
else if (faction.equals("vs")) {
Seq(GlobalDefinitions.vshev_quasar, GlobalDefinitions.vshev_comet, GlobalDefinitions.vshev_starfire)
}
else {
Seq(GlobalDefinitions.trhev_dualcycler, GlobalDefinitions.trhev_pounder, GlobalDefinitions.trhev_burster)
}).zip(
Seq(s"${faction}hev_antipersonnel", s"${faction}hev_antivehicular", s"${faction}hev_antiaircraft")
)
} else {
Seq(pair)
}
}
def refreshPurchaseTimes(keys: Set[String]): Unit = {
var keysToDrop: Seq[String] = Nil
keys.foreach { key =>
@ -1348,23 +1368,7 @@ class AvatarActor(
val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()).getSeconds
Avatar.purchaseCooldowns.find(_._1.Name == name) match {
case Some((obj, cooldown)) if cooldown.toSeconds - secondsSincePurchase > 0 =>
val faction : String = avatar.faction.toString.toLowerCase
val name = obj match {
case GlobalDefinitions.trhev_dualcycler |
GlobalDefinitions.nchev_scattercannon |
GlobalDefinitions.vshev_quasar =>
s"${faction}hev_antipersonnel"
case GlobalDefinitions.trhev_pounder |
GlobalDefinitions.nchev_falcon |
GlobalDefinitions.vshev_comet =>
s"${faction}hev_antivehicular"
case GlobalDefinitions.trhev_burster |
GlobalDefinitions.nchev_sparrow |
GlobalDefinitions.vshev_starfire =>
s"${faction}hev_antiaircraft"
case _ =>
obj.Name
}
val (_, name) = resolvePurchaseTimeName(avatar.faction, obj)
updatePurchaseTimer(name, cooldown.toSeconds - secondsSincePurchase, unk1 = true)
case _ =>

View file

@ -221,107 +221,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
}
}
case PlayerControl.SetExoSuit(exosuit: ExoSuitType.Value, subtype: Int) =>
setExoSuit(exosuit, subtype)
case Terminal.TerminalMessage(_, msg, order) =>
order match {
case Terminal.BuyExosuit(exosuit, subtype) =>
var toDelete: List[InventoryItem] = Nil
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player)
val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype
val allowedToChangeArmor = Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
player.avatar.purchaseCooldown(weapon) match {
case Some(_) =>
false
case None =>
avatarActor ! AvatarActor.UpdatePurchaseTime(weapon)
true
}
} else {
true
})
val result = if (requestToChangeArmor && allowedToChangeArmor) {
log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit")
val beforeHolsters = Players.clearHolsters(player.Holsters().iterator)
val beforeInventory = player.Inventory.Clear()
//change suit
val originalArmor = player.Armor
player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit
val toMaxArmor = player.MaxArmor
val toArmor = if (originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) {
player.History(HealFromExoSuitChange(PlayerSource(player), exosuit))
player.Armor = toMaxArmor
} else {
player.Armor = originalArmor
}
//ensure arm is down, even if it needs to go back up
if (player.DrawnSlot != Player.HandsDownSlot) {
player.DrawnSlot = Player.HandsDownSlot
}
val normalHolsters = if (originalSuit == ExoSuitType.MAX) {
val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max)
toDelete ++= maxWeapons
normalWeapons
} else {
beforeHolsters
}
//populate holsters
val (afterHolsters, finalInventory) = if (exosuit == ExoSuitType.MAX) {
(
normalHolsters,
Players.fillEmptyHolsters(List(player.Slot(4)).iterator, normalHolsters) ++ beforeInventory
)
} else if (originalSuit == exosuit) { //note - this will rarely be the situation
(normalHolsters, Players.fillEmptyHolsters(player.Holsters().iterator, normalHolsters))
} else {
val (afterHolsters, toInventory) =
normalHolsters.partition(elem => elem.obj.Size == player.Slot(elem.start).Size)
afterHolsters.foreach({ elem => player.Slot(elem.start).Equipment = elem.obj })
val remainder = Players.fillEmptyHolsters(player.Holsters().iterator, toInventory ++ beforeInventory)
(
player.HolsterItems(),
remainder
)
}
//put items back into inventory
val (stow, drop) = if (originalSuit == exosuit) {
(finalInventory, Nil)
} else {
val (a, b) = GridInventory.recoverInventory(finalInventory, player.Inventory)
(
a,
b.map {
InventoryItem(_, -1)
}
)
}
stow.foreach { elem =>
player.Inventory.InsertQuickly(elem.start, elem.obj)
}
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants()
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Zone.id,
AvatarAction.ChangeExosuit(
player.GUID,
toArmor,
exosuit,
subtype,
player.LastDrawnSlot,
exosuit == ExoSuitType.MAX && requestToChangeArmor,
beforeHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
afterHolsters,
beforeInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
stow,
drop,
toDelete.map { case InventoryItem(obj, _) => (obj, obj.GUID) }
)
)
true
} else {
false
}
val result = setExoSuit(exosuit, subtype)
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result)
@ -511,6 +417,107 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case _ => ;
}
def setExoSuit(exosuit: ExoSuitType.Value, subtype: Int): Boolean = {
var toDelete: List[InventoryItem] = Nil
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player)
val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype
val allowedToChangeArmor = Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
player.avatar.purchaseCooldown(weapon) match {
case Some(_) =>
false
case None =>
avatarActor ! AvatarActor.UpdatePurchaseTime(weapon)
true
}
} else {
true
})
if (requestToChangeArmor && allowedToChangeArmor) {
log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit")
val beforeHolsters = Players.clearHolsters(player.Holsters().iterator)
val beforeInventory = player.Inventory.Clear()
//change suit
val originalArmor = player.Armor
player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit
val toMaxArmor = player.MaxArmor
val toArmor = if (originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) {
player.History(HealFromExoSuitChange(PlayerSource(player), exosuit))
player.Armor = toMaxArmor
} else {
player.Armor = originalArmor
}
//ensure arm is down, even if it needs to go back up
if (player.DrawnSlot != Player.HandsDownSlot) {
player.DrawnSlot = Player.HandsDownSlot
}
val normalHolsters = if (originalSuit == ExoSuitType.MAX) {
val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max)
toDelete ++= maxWeapons
normalWeapons
} else {
beforeHolsters
}
//populate holsters
val (afterHolsters, finalInventory) = if (exosuit == ExoSuitType.MAX) {
(
normalHolsters,
Players.fillEmptyHolsters(List(player.Slot(4)).iterator, normalHolsters) ++ beforeInventory
)
} else if (originalSuit == exosuit) { //note - this will rarely be the situation
(normalHolsters, Players.fillEmptyHolsters(player.Holsters().iterator, normalHolsters))
} else {
val (afterHolsters, toInventory) =
normalHolsters.partition(elem => elem.obj.Size == player.Slot(elem.start).Size)
afterHolsters.foreach({ elem => player.Slot(elem.start).Equipment = elem.obj })
val remainder = Players.fillEmptyHolsters(player.Holsters().iterator, toInventory ++ beforeInventory)
(
player.HolsterItems(),
remainder
)
}
//put items back into inventory
val (stow, drop) = if (originalSuit == exosuit) {
(finalInventory, Nil)
} else {
val (a, b) = GridInventory.recoverInventory(finalInventory, player.Inventory)
(
a,
b.map {
InventoryItem(_, -1)
}
)
}
stow.foreach { elem =>
player.Inventory.InsertQuickly(elem.start, elem.obj)
}
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants()
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Zone.id,
AvatarAction.ChangeExosuit(
player.GUID,
toArmor,
exosuit,
subtype,
player.LastDrawnSlot,
exosuit == ExoSuitType.MAX && requestToChangeArmor,
beforeHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
afterHolsters,
beforeInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
stow,
drop,
toDelete.map { case InventoryItem(obj, _) => (obj, obj.GUID) }
)
)
true
} else {
false
}
}
override protected def PerformDamage(
target: Target,
applyDamageTo: Output
@ -1139,6 +1146,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
}
object PlayerControl {
/** na */
final case class SetExoSuit(exosuit: ExoSuitType.Value, subtype: Int)
/**
* Transform an applicable Aura effect into its `PlanetsideAttributeMessage` value.
* @see `Aura`

View file

@ -88,8 +88,7 @@ object InfantryLoadout {
/**
* The sub-type of the player's uniform, as used in `FavoritesMessage`.<br>
* <br>
* The values for `Standard`, `Infiltration`, and the generic `MAX` are not perfectly known.
* The latter-most exo-suit option is presumed.
* The values for a specific `MAX` type is only known by knowing the subtype.
* @param suit the player's uniform
* @param subtype the mechanized assault exo-suit subtype as determined by their arm weapons
* @return the numeric subtype

View file

@ -137,7 +137,8 @@ case class GameConfig(
bepRate: Double,
cepRate: Double,
newAvatar: NewAvatar,
hart: HartConfig
hart: HartConfig,
sharedMaxCooldown: Boolean
)
case class NewAvatar(