mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-02-21 07:33:34 +00:00
* bog-standard order_terminal amenities now take damage up to the point of destruction and can be repaired from destruction to functional to the point of being fully repaired; this is mostly proof fo concept * restored proper destruction to FacilityTurrets; extended proper rrepairs to FacilityTurrets; co-opted terminal hacking into TerminalControl; started to expand on hacking protocol, but chose restraint * changes made thus that a clear Definition hierarchy is established; all of this is in line with making future changes to repair/destroy variables, and making generic the repair code * all meaningful facility amenities take damage and can be repaired; spawn tubes can be destroyed and the base will properly lose spawns (and show it on the map); some hack logic has been redistributed into the appropriate control objects, following in the wake of repair/damage logic * deployables are repairable; the TRAP has been converted into a ComplexDeployable; changed the nature of the Repairable traits * player bank repair and medapp heal has been moved out from WSA into PlayerControl * overhaul of Progress callback system and the inclusion of player revival as a Progress activity * begun relocating functionality for hacking outside of WSA; set up behavoir mixin for cargo operations, in order to move vehicle hack function, but did not yet integrate * integration of the actor behavior mixin for vehicle cargo operations to support the integration of vehicle hacking finalization * establishing inheritance/override potential of Damageable activity; Generator and SpawnTube map behavior behavior (currently inactive) * ImplantTerminalMech objects now have a 'with-coordinates' constructor and a deprecated 'no-coordinates' constructor; implants mechs and interfaces are now damageable and repairable, and their damage state can also block mounting * generators are destroyed and repaired properly, and even explode, killing a radius-worth of players * destroy and repair pass on deployables, except for explosive types * Damageable pass; client synchronization pass * helpful comments * some tests for damageable and repairable; refined output and repaired existing tests * enabled friendly fire check and recovery * handled friendly fire against allied mines; moved jammer code to common damageable behavior * tweaks to damageability, infantry heal and repair, and sensor and explosive animations * animations; framework for future vitals events; closing database connections * adding some deployable tests; fixing a bunch of other tests; History is back * testing for basic Damageable functions; removing a log message * finicky animation stuff * event messages to the Generator to represent health changes * damage against BFR's is now only used against mythical creatures * test fix
416 lines
17 KiB
Scala
416 lines
17 KiB
Scala
// Copyright (c) 2017 PSForever
|
|
package objects
|
|
|
|
import net.psforever.objects._
|
|
import net.psforever.objects.vital.damage.{DamageCalculations, DamageProfile}
|
|
import DamageCalculations._
|
|
import net.psforever.objects.vital.resistance.ResistanceCalculations
|
|
import ResistanceCalculations._
|
|
import net.psforever.objects.vital.resolution.ResolutionCalculations
|
|
import ResolutionCalculations._
|
|
import net.psforever.objects.ballistics._
|
|
import net.psforever.objects.vital.Vitality
|
|
import net.psforever.types._
|
|
import org.specs2.mutable.Specification
|
|
|
|
class DamageCalculationsTests extends Specification {
|
|
"DamageCalculations" should {
|
|
val wep = GlobalDefinitions.galaxy_gunship_cannon
|
|
val wep_fmode = Tool(wep).FireMode
|
|
val wep_prof = wep_fmode.Modifiers.asInstanceOf[DamageProfile]
|
|
val proj = wep.ProjectileTypes.head
|
|
val proj_prof = proj.asInstanceOf[DamageProfile]
|
|
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
|
|
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
|
|
val target = Vehicle(GlobalDefinitions.fury)
|
|
target.Position = Vector3(10, 0, 0)
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3(50, 50, 0))
|
|
"extract no damage numbers" in {
|
|
NoDamageAgainst(proj_prof) mustEqual 0
|
|
}
|
|
|
|
"extract damage against exosuit target" in {
|
|
DamageAgainstExoSuit(proj_prof) mustEqual 50
|
|
}
|
|
|
|
"extract damage against MAX target" in {
|
|
DamageAgainstMaxSuit(proj_prof) mustEqual 75
|
|
}
|
|
|
|
"extract damage against vehicle target" in {
|
|
DamageAgainstVehicle(proj_prof) mustEqual 82
|
|
}
|
|
|
|
"extract damage against aircraft target" in {
|
|
DamageAgainstAircraft(proj_prof) mustEqual 82
|
|
}
|
|
|
|
"extract damage against something" in {
|
|
DamageAgainstBFR(proj_prof) mustEqual 66
|
|
}
|
|
|
|
"extract a complete damage profile (1)" in {
|
|
val result = DamageAgainstVehicle(proj_prof) + DamageAgainstVehicle(wep_prof)
|
|
val func : (DamageProfile, List[DamageProfile]) => Int = DamageWithModifiers(DamageAgainstVehicle)
|
|
func(proj_prof, List(wep_prof)) mustEqual result
|
|
}
|
|
|
|
"extract a complete damage profile (2)" in {
|
|
val result = 2 * DamageAgainstVehicle(proj_prof) + DamageAgainstVehicle(wep_prof)
|
|
val func : (DamageProfile, List[DamageProfile]) => Int = DamageWithModifiers(DamageAgainstVehicle)
|
|
func(proj_prof, List(proj_prof, wep_prof)) mustEqual result
|
|
}
|
|
|
|
"calculate no distance" in {
|
|
NoDistance(resprojectile) mustEqual 0
|
|
}
|
|
|
|
"calculate too far distance" in {
|
|
TooFar(resprojectile) mustEqual Float.MaxValue
|
|
}
|
|
|
|
"calculate distance between target and source" in {
|
|
DistanceBetweenTargetandSource(resprojectile) mustEqual 67.38225f
|
|
}
|
|
|
|
"calculate distance between target and explosion (splash)" in {
|
|
DistanceFromExplosionToTarget(resprojectile) mustEqual 63.031242f
|
|
}
|
|
|
|
"calculate no damage from components" in {
|
|
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
|
|
val distance = DistanceFromExplosionToTarget(resprojectile)
|
|
DamageCalculations.NoDamage(projectile, result, distance) mustEqual 0
|
|
}
|
|
|
|
"calculate degraded damage from components (near)" in {
|
|
val projectile_alt = Projectile(GlobalDefinitions.galaxy_gunship_gun_projectile, //need projectile with degrade
|
|
wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
|
|
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
|
|
DirectHitDamageWithDegrade(projectile_alt, result, 0) mustEqual 132
|
|
}
|
|
|
|
"calculate degraded damage from components (medium)" in {
|
|
val projectile_alt = Projectile(GlobalDefinitions.galaxy_gunship_gun_projectile, //need projectile with degrade
|
|
wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
|
|
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
|
|
DirectHitDamageWithDegrade(projectile_alt, result, 250) mustEqual 103
|
|
}
|
|
|
|
"calculate degraded damage from components (far)" in {
|
|
val projectile_alt = Projectile(GlobalDefinitions.galaxy_gunship_gun_projectile, //need projectile with degrade
|
|
wep, wep_fmode, player, Vector3(2, 2, 0), Vector3.Zero)
|
|
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
|
|
DirectHitDamageWithDegrade(projectile_alt, result, 1000) mustEqual 0
|
|
}
|
|
|
|
"calculate splash damage from components (near)" in {
|
|
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
|
|
SplashDamageWithRadialDegrade(projectile, result, 0) mustEqual 132
|
|
}
|
|
|
|
"calculate splash damage from components (medium)" in {
|
|
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
|
|
SplashDamageWithRadialDegrade(projectile, result, 5) mustEqual 13
|
|
}
|
|
|
|
"calculate splash damage from components (far)" in {
|
|
val result = DamageWithModifiers(DamageAgainstVehicle)(proj_prof, List(wep_prof))
|
|
SplashDamageWithRadialDegrade(projectile, result, 6) mustEqual 0
|
|
}
|
|
}
|
|
}
|
|
|
|
class ResistanceCalculationsTests extends Specification {
|
|
val wep = GlobalDefinitions.galaxy_gunship_cannon
|
|
val wep_fmode = Tool(wep).FireMode
|
|
val proj = wep.ProjectileTypes.head //GlobalDefinitions.heavy_grenade_projectile
|
|
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
|
|
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2,2,0), Vector3.Zero)
|
|
|
|
"ResistanceCalculations" should {
|
|
"ignore all targets" in {
|
|
val target = Vehicle(GlobalDefinitions.fury)
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
|
|
InvalidTarget(resprojectile).isFailure mustEqual true
|
|
}
|
|
|
|
"discern standard infantry targets" in {
|
|
val target = player
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
|
|
ValidInfantryTarget(resprojectile).isSuccess mustEqual true
|
|
ValidMaxTarget(resprojectile).isSuccess mustEqual false
|
|
ValidVehicleTarget(resprojectile).isSuccess mustEqual false
|
|
ValidAircraftTarget(resprojectile).isSuccess mustEqual false
|
|
}
|
|
|
|
"discern mechanized infantry targets" in {
|
|
val target = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
|
|
target.ExoSuit = ExoSuitType.MAX
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
|
|
ValidInfantryTarget(resprojectile).isSuccess mustEqual false
|
|
ValidMaxTarget(resprojectile).isSuccess mustEqual true
|
|
ValidVehicleTarget(resprojectile).isSuccess mustEqual false
|
|
ValidAircraftTarget(resprojectile).isSuccess mustEqual false
|
|
}
|
|
|
|
"discern ground vehicle targets" in {
|
|
val target = Vehicle(GlobalDefinitions.fury)
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
|
|
ValidInfantryTarget(resprojectile).isSuccess mustEqual false
|
|
ValidMaxTarget(resprojectile).isSuccess mustEqual false
|
|
ValidVehicleTarget(resprojectile).isSuccess mustEqual true
|
|
ValidAircraftTarget(resprojectile).isSuccess mustEqual false
|
|
}
|
|
|
|
"discern flying vehicle targets" in {
|
|
val target = Vehicle(GlobalDefinitions.mosquito)
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
|
|
ValidInfantryTarget(resprojectile).isSuccess mustEqual false
|
|
ValidMaxTarget(resprojectile).isSuccess mustEqual false
|
|
ValidVehicleTarget(resprojectile).isSuccess mustEqual false
|
|
ValidAircraftTarget(resprojectile).isSuccess mustEqual true
|
|
}
|
|
|
|
"extract no resistance values" in {
|
|
NoResistExtractor(SourceEntry(player)) mustEqual 0
|
|
}
|
|
|
|
"extract resistance values from exo-suit" in {
|
|
val pSource = PlayerSource(player)
|
|
ExoSuitDirectExtractor(pSource) mustEqual 4
|
|
ExoSuitSplashExtractor(pSource) mustEqual 15
|
|
ExoSuitAggravatedExtractor(pSource) mustEqual 8
|
|
ExoSuitRadiationExtractor(pSource) mustEqual 0
|
|
}
|
|
|
|
"extract resistance values from vehicle" in {
|
|
val vSource = VehicleSource(Vehicle(GlobalDefinitions.fury))
|
|
VehicleDirectExtractor(vSource) mustEqual 0
|
|
VehicleSplashExtractor(vSource) mustEqual 0
|
|
VehicleAggravatedExtractor(vSource) mustEqual 0
|
|
VehicleRadiationExtractor(vSource) mustEqual 0
|
|
}
|
|
}
|
|
}
|
|
|
|
class ResolutionCalculationsTests extends Specification {
|
|
val wep = GlobalDefinitions.suppressor
|
|
val wep_fmode = Tool(wep).FireMode
|
|
val proj = wep.ProjectileTypes.head
|
|
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
|
|
player.Spawn
|
|
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2,2,0), Vector3.Zero)
|
|
|
|
"ResolutionCalculations" should {
|
|
"calculate no damage" in {
|
|
val target = player
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target), target.DamageModel, Vector3.Zero)
|
|
ResolutionCalculations.NoDamage(resprojectile)(50,50) mustEqual 0
|
|
}
|
|
|
|
"calculate no infantry damage for vehicles" in {
|
|
val target1 = Vehicle(GlobalDefinitions.fury) //!
|
|
val resprojectile1 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target1), target1.DamageModel, Vector3.Zero)
|
|
InfantryDamageAfterResist(resprojectile1)(50, 10) mustEqual (0,0)
|
|
|
|
val target2 = player
|
|
val resprojectile2 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target2), target2.DamageModel, Vector3.Zero)
|
|
InfantryDamageAfterResist(resprojectile2)(50, 10) mustEqual (40,10)
|
|
}
|
|
|
|
"calculate health and armor damage for infantry target" in {
|
|
InfantryDamageAfterResist(100,100)(50, 10) mustEqual (40,10)
|
|
}
|
|
|
|
"calculate health and armor damage, with bleed through damage, for infantry target" in {
|
|
//health = 100, armor = 5 -> resist 10 but only have 5, so rollover extra -> damages (40+5, 5)
|
|
InfantryDamageAfterResist(100,5)(50, 10) mustEqual (45,5)
|
|
}
|
|
|
|
"calculate health damage for infantry target" in {
|
|
//health = 100, armor = 0
|
|
InfantryDamageAfterResist(100,0)(50, 10) mustEqual (50,0)
|
|
}
|
|
|
|
"calculate armor damage for infantry target" in {
|
|
//resistance > damage
|
|
InfantryDamageAfterResist(100,100)(50, 60) mustEqual (0,50)
|
|
}
|
|
|
|
val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
|
|
player2.ExoSuit = ExoSuitType.MAX
|
|
player2.Spawn
|
|
"calculate no max damage for vehicles" in {
|
|
val target1 = Vehicle(GlobalDefinitions.fury) //!
|
|
val resprojectile1 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target1), target1.DamageModel, Vector3.Zero)
|
|
MaxDamageAfterResist(resprojectile1)(50, 10) mustEqual (0,0)
|
|
|
|
val target2 = player2
|
|
val resprojectile2 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target2), target2.DamageModel, Vector3.Zero)
|
|
MaxDamageAfterResist(resprojectile2)(50, 10) mustEqual (0,40)
|
|
}
|
|
|
|
"calculate health and armor damage for max target" in {
|
|
MaxDamageAfterResist(100,5)(50, 10) mustEqual (35,5)
|
|
}
|
|
|
|
"calculate health damage for max target" in {
|
|
//health = 100, armor = 0
|
|
MaxDamageAfterResist(100,0)(50, 10) mustEqual (40,0)
|
|
}
|
|
|
|
"calculate armor damage for max target" in {
|
|
//resistance > damage
|
|
MaxDamageAfterResist(100,100)(50, 10) mustEqual (0,40)
|
|
}
|
|
|
|
"do not care if target is infantry for vehicle calculations" in {
|
|
val target1 = player
|
|
val resprojectile1 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target1), target1.DamageModel, Vector3.Zero)
|
|
VehicleDamageAfterResist(resprojectile1)(50, 10) mustEqual 40
|
|
|
|
val target2 = Vehicle(GlobalDefinitions.fury) //!
|
|
val resprojectile2 = ResolvedProjectile(ProjectileResolution.Splash, projectile, SourceEntry(target2), target2.DamageModel, Vector3.Zero)
|
|
VehicleDamageAfterResist(resprojectile2)(50, 10) mustEqual 40
|
|
}
|
|
}
|
|
|
|
"calculate resisted damage for vehicle target" in {
|
|
VehicleDamageAfterResist(50, 10) mustEqual 40
|
|
}
|
|
|
|
"calculate un-resisted damage for vehicle target" in {
|
|
VehicleDamageAfterResist(50, 0) mustEqual 50
|
|
}
|
|
}
|
|
|
|
class DamageModelTests extends Specification {
|
|
val wep = GlobalDefinitions.suppressor
|
|
val wep_tool = Tool(wep)
|
|
val wep_fmode = wep_tool.FireMode
|
|
val proj = wep.ProjectileTypes.head
|
|
val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
|
|
player.Spawn
|
|
val projectile = Projectile(proj, wep, wep_fmode, player, Vector3(2,2,0), Vector3.Zero)
|
|
|
|
"DamageModel" should {
|
|
"be a part of vitality" in {
|
|
player.isInstanceOf[Vitality] mustEqual true
|
|
try {
|
|
player.getClass.getDeclaredMethod("DamageModel").hashCode()
|
|
}
|
|
catch {
|
|
case _ : Exception =>
|
|
ko //the method doesn't exist
|
|
}
|
|
|
|
wep_tool.isInstanceOf[Vitality] mustEqual false
|
|
try {
|
|
wep_tool.getClass.getDeclaredMethod("DamageModel").hashCode()
|
|
ko
|
|
}
|
|
catch {
|
|
case _ : Exception =>
|
|
ok //the method doesn't exist
|
|
}
|
|
ok
|
|
}
|
|
|
|
"resolve infantry targets" in {
|
|
val tplayer = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
|
|
tplayer.Spawn
|
|
tplayer.Health mustEqual 100
|
|
tplayer.Armor mustEqual 50
|
|
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(tplayer), tplayer.DamageModel, Vector3.Zero)
|
|
val func : Any=>ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile)
|
|
|
|
func(tplayer)
|
|
tplayer.Health mustEqual 87
|
|
tplayer.Armor mustEqual 46
|
|
}
|
|
|
|
"resolve infantry targets in a specific way" in {
|
|
val tplayer = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
|
|
tplayer.Spawn
|
|
tplayer.Health mustEqual 100
|
|
tplayer.Armor mustEqual 50
|
|
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(tplayer), tplayer.DamageModel, Vector3.Zero)
|
|
val func : Any=>ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile, ProjectileResolution.Splash)
|
|
|
|
func(tplayer)
|
|
tplayer.Health mustEqual 98
|
|
tplayer.Armor mustEqual 35
|
|
}
|
|
|
|
"resolve infantry targets, with damage overflow" in {
|
|
val tplayer = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute))
|
|
tplayer.Spawn
|
|
tplayer.Health mustEqual 100
|
|
tplayer.Armor mustEqual 50
|
|
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(tplayer), tplayer.DamageModel, Vector3.Zero)
|
|
val func : Any=>ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile)
|
|
tplayer.Armor = 0
|
|
|
|
func(tplayer)
|
|
tplayer.Health mustEqual 83
|
|
tplayer.Armor mustEqual 0
|
|
}
|
|
|
|
"resolve vehicle targets" in {
|
|
val vehicle = Vehicle(GlobalDefinitions.fury)
|
|
vehicle.Health mustEqual 650
|
|
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(vehicle), vehicle.DamageModel, Vector3.Zero)
|
|
val func : Any=>ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile)
|
|
|
|
func(vehicle)
|
|
vehicle.Health mustEqual 641
|
|
}
|
|
|
|
"resolve vehicle targets (with shields)" in {
|
|
val vehicle = Vehicle(GlobalDefinitions.fury)
|
|
vehicle.Shields = 10
|
|
vehicle.Health mustEqual 650
|
|
vehicle.Shields mustEqual 10
|
|
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(vehicle), vehicle.DamageModel, Vector3.Zero)
|
|
val func : Any=>ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile)
|
|
|
|
func(vehicle)
|
|
vehicle.Health mustEqual 650
|
|
vehicle.Shields mustEqual 1
|
|
}
|
|
|
|
"resolve vehicle targets (losing shields)" in {
|
|
val vehicle = Vehicle(GlobalDefinitions.fury)
|
|
vehicle.Shields = 10
|
|
vehicle.Health mustEqual 650
|
|
vehicle.Shields mustEqual 10
|
|
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(vehicle), vehicle.DamageModel, Vector3.Zero)
|
|
val func : Any=>ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile)
|
|
|
|
func(vehicle)
|
|
vehicle.Health mustEqual 650
|
|
vehicle.Shields mustEqual 1
|
|
func(vehicle)
|
|
vehicle.Health mustEqual 642
|
|
vehicle.Shields mustEqual 0
|
|
}
|
|
|
|
"resolve vehicle targets in a specific way" in {
|
|
val vehicle = Vehicle(GlobalDefinitions.fury)
|
|
vehicle.Health mustEqual 650
|
|
|
|
val resprojectile = ResolvedProjectile(ProjectileResolution.Hit, projectile, SourceEntry(vehicle), vehicle.DamageModel, Vector3.Zero)
|
|
val func : Any=>ResolvedProjectile = resprojectile.damage_model.Calculate(resprojectile, ProjectileResolution.Splash)
|
|
|
|
func(vehicle)
|
|
vehicle.Health mustEqual 641
|
|
}
|
|
}
|
|
}
|